X-Git-Url: https://nos-oignons.net/gitweb/website.git/blobdiff_plain/29308b629a3fe9aee088ba984fe8f595f2f6275f..4c2924543ea0f426c3342fe8430ca606a6dbc85c:/assets/weights_graphs.js diff --git a/assets/weights_graphs.js b/assets/weights_graphs.js new file mode 100644 index 0000000..915a8b8 --- /dev/null +++ b/assets/weights_graphs.js @@ -0,0 +1,247 @@ +function WeightsDrawer(selector) { + this.selector = selector; + this.current_source = "consensus_weight_fraction"; + this.current_period = "1_month"; + this.weights_data = {}; + this.svg = null; +}; +WeightsDrawer.prototype = new WeightsDrawer(); + +WeightsDrawer.margin = {top: 50, right: 10, bottom: 90, left: 130}; +WeightsDrawer.width = 600 - WeightsDrawer.margin.left - WeightsDrawer.margin.right; +WeightsDrawer.height = 400 - WeightsDrawer.margin.top - WeightsDrawer.margin.bottom; + +WeightsDrawer.parseTime = d3.time.format("%Y-%m-%d %H:%M:%S").parse; +WeightsDrawer.percentFormatter = d3.format(".2%"); + +WeightsDrawer.x = d3.time.scale() + .range([0, WeightsDrawer.width]); + +WeightsDrawer.y = d3.scale.linear() + .range([WeightsDrawer.height, 0]); + +WeightsDrawer.xAxis = d3.svg.axis() + .scale(WeightsDrawer.x) + .orient("bottom"); + +WeightsDrawer.yAxis = d3.svg.axis() + .scale(WeightsDrawer.y) + .orient("left") + .tickFormat(function(d) { return (d == 0) ? "" : WeightsDrawer.percentFormatter(d); }); + +WeightsDrawer.area = d3.svg.area() + .x(function(d) { return WeightsDrawer.x(d.date); }) + .y0(function(d) { return WeightsDrawer.y(d.y0); }) + .y1(function(d) { return WeightsDrawer.y(d.y0 + d.y); }); + +WeightsDrawer.stack = d3.layout.stack() + .values(function(d) { return d.values; }); + +WeightsDrawer.onionoo_url = "https://onionoo.torproject.org/weights?type=relay&contact=adminsys@nos-oignons.net"; + +WeightsDrawer.periods = [ + { id: "1_month", label: L10n.t_1_month }, + { id: "3_months", label: L10n.t_3_months }, + { id: "1_year", label: L10n.t_1_year }, + { id: "5_years", label: L10n.t_5_years }, + ]; + +WeightsDrawer.current_period = WeightsDrawer.periods[0].id; + +WeightsDrawer.sources = [ + { id: "consensus_weight_fraction", label: L10n.consensus_weight }, + { id: "exit_probability", label: L10n.exit_probability }, + ]; + +WeightsDrawer.current_source = WeightsDrawer.sources[0].id; + +WeightsDrawer.extract_values = function(history, interval, minTime, maxTime) { + var values = []; + var first = history ? WeightsDrawer.parseTime(history.first) : maxTime; + var last = history ? WeightsDrawer.parseTime(history.last) : minTime; + var i = 0; + for (var current = minTime; current <= maxTime; current = d3.time.second.offset(current, interval)) { + values.push({ date: current, + y: (first <= current && current <= last) ? history.factor * history.values[i++] : 0 + }); + } + return values; +} + +WeightsDrawer.color = d3.scale.ordinal(); +WeightsDrawer.color.domain(nos_oignons_relays.map(function(r) {return r.fingerprint})); +WeightsDrawer.color.range(nos_oignons_relays.map(function(r) {return r.color})); + +WeightsDrawer.prototype.draw_weights_graph = function(raw_data) { + var drawer = this; + + // Purge non running relays + raw_data.relays.forEach(function(r, i) { + if (typeof r.consensus_weight_fraction === 'undefined') { + raw_data.relays.splice(i, 1); + } + }); + + var form_source = d3.select(drawer.selector).append("form") + .attr("action", "#"); + WeightsDrawer.sources.forEach(function(s) { + var div = form_source.append("div"); + var radio = div.append("input") + .attr("type", "radio") + .attr("name", "source") + .attr("id", "source_" + s.id) + .on("click", function() { drawer.update_source(s.id); }); + div.append("label") + .attr("for", "source_" + s.id) + .text(s.label); + if (s.id == WeightsDrawer.sources[0].id) { + radio.attr("checked", true); + } + }); + + drawer.svg = d3.select(drawer.selector).append("svg") + .attr("width", WeightsDrawer.width + WeightsDrawer.margin.left + WeightsDrawer.margin.right) + .attr("height", WeightsDrawer.height + WeightsDrawer.margin.top + WeightsDrawer.margin.bottom) + .append("g") + .attr("transform", "translate(" + WeightsDrawer.margin.left + "," + WeightsDrawer.margin.top + ")"); + + var form_period = d3.select(drawer.selector).append("form") + .attr("action", "#"); + WeightsDrawer.periods.forEach(function(p) { + var div = form_period.append("div"); + var radio = div.append("input") + .attr("type", "radio") + .attr("name", "period") + .attr("id", "period_" + p.id) + .on("click", function() { drawer.update_period(p.id); }); + div.append("label") + .attr("for", "period_" + p.id) + .text(p.label); + if (p.id == WeightsDrawer.periods[0].id) { + radio.attr("checked", true); + } + }); + + WeightsDrawer.sources.map(function(s) { return s.id; }).forEach(function(source) { + drawer.weights_data[source] = {}; + WeightsDrawer.periods.map(function(p) { return p.id; }).forEach(function(period) { + var interval = d3.max(raw_data.relays, function(d) { + return d[source][period] && d[source][period].interval; + }); + raw_data.relays.forEach(function(d) { + if ((d[source][period] && d[source][period].interval != interval)) { + throw "PANIC: Different interval for different relays in the same time period."; + } + }); + var minTime = d3.min(raw_data.relays, function(d) { + return d[source][period] && WeightsDrawer.parseTime(d[source][period].first); + }); + var maxTime = d3.min(raw_data.relays, function(d) { + return d[source][period] && WeightsDrawer.parseTime(d[source][period].last); + }); + + var maxValue = 0; + + var relays = WeightsDrawer.color.domain().map(function(fingerprint) { + var relay_data = raw_data["relays"].filter(function(d) { return d.fingerprint == fingerprint; })[0]; + + var history = relay_data[source][period]; + var values = WeightsDrawer.extract_values(history, interval, minTime, maxTime); + maxValue = maxValue + d3.max(values, function(d) { return d.y; }); + + return { + fingerprint: fingerprint, + values: values, + }; + }); + drawer.weights_data[source][period] = { + minTime: minTime, + maxTime: maxTime, + maxValue: maxValue, + relays: relays + }; + }); + }); + + WeightsDrawer.y.domain([0, drawer.weights_data[WeightsDrawer.current_source][WeightsDrawer.current_period].maxValue]); + + WeightsDrawer.x.domain([drawer.weights_data[WeightsDrawer.current_source][WeightsDrawer.current_period].minTime, drawer.weights_data[WeightsDrawer.current_source][WeightsDrawer.current_period].maxTime]); + + var weight_graph = drawer.svg.selectAll(".weight_graph") + .data(WeightsDrawer.stack(drawer.weights_data[WeightsDrawer.current_source][WeightsDrawer.current_period].relays)) + .enter().append("path") + .attr("class", "weight_graph area") + .attr("d", function(d) { return WeightsDrawer.area(d.values); }) + .style("fill", function(d) { return WeightsDrawer.color(d.fingerprint); }); + + drawer.svg.append("g") + .attr("class", "x axis") + .attr("transform", "translate(0," + WeightsDrawer.height + ")") + .call(WeightsDrawer.xAxis) + .selectAll("text") + .style("text-anchor", "end") + .attr("transform", "rotate(-90) translate(-10, 0)"); + + drawer.svg.append("g") + .attr("class", "y axis") + .call(WeightsDrawer.yAxis); + + var legend = drawer.svg.selectAll(".legend") + .data(WeightsDrawer.color.domain().slice().reverse()) + .enter().append("g") + .attr("class", "legend") + .attr("transform", function(d, i) { return "translate(0," + ((i * 20) - WeightsDrawer.margin.top) + ")"; }); + + legend.append("rect") + .attr("x", WeightsDrawer.width - 18) + .attr("width", 18) + .attr("height", 18) + .style("fill", WeightsDrawer.color); + + legend.append("text") + .attr("x", WeightsDrawer.width - 24) + .attr("y", 9) + .attr("dy", ".35em") + .style("text-anchor", "end") + .text(function(d) { + return nos_oignons_relays.filter(function(r) { return r.fingerprint == d; })[0].name; + }); +}; + +WeightsDrawer.prototype.refresh_graph = function() { + var drawer = this; + + WeightsDrawer.x.domain([drawer.weights_data[drawer.current_source][drawer.current_period].minTime, drawer.weights_data[drawer.current_source][drawer.current_period].maxTime]); + WeightsDrawer.y.domain([0, drawer.weights_data[drawer.current_source][drawer.current_period].maxValue]); + var t = drawer.svg.transition().duration(300); + t.select(".x.axis").call(WeightsDrawer.xAxis); + t.select(".y.axis").call(WeightsDrawer.yAxis); + t.selectAll(".weight_graph").style("fill-opacity", 0); + t.each("end", function() { + d3.selectAll(".weight_graph").data(WeightsDrawer.stack(drawer.weights_data[drawer.current_source][drawer.current_period].relays)); + d3.selectAll(".weight_graph").attr("d", function(d) { return WeightsDrawer.area(d.values); }) + var t2 = drawer.svg.transition().duration(100); + t2.selectAll(".weight_graph").style("fill-opacity", 1); + }); + d3.selectAll(".x.axis text") + .style("text-anchor", "end") + .attr("transform", "rotate(-90) translate(-10, 0)"); +} + +WeightsDrawer.prototype.update_source = function(source) { + this.current_source = source; + this.refresh_graph(); +} + +WeightsDrawer.prototype.update_period = function(period) { + this.current_period = period; + this.refresh_graph(); +} + +WeightsDrawer.prototype.draw = function() { + var drawer = this; + d3.json(WeightsDrawer.onionoo_url, function(error, raw_data) { + d3.select(drawer.selector).text(""); + drawer.draw_weights_graph(raw_data); + }); +};