| (function() { | |
| d3.horizon = function() { | |
| var bands = 1, // between 1 and 5, typically | |
| mode = "offset", // or mirror | |
| interpolate = "linear", // or basis, monotone, step-before, etc. | |
| x = d3_horizonX, | |
| y = d3_horizonY, | |
| series = function(d){ return d; }, | |
| w = 960, | |
| h = 40, | |
| overallYMax = null, | |
| duration = 0; | |
| var color = d3.scale.linear() | |
| .domain([-1, 0, 0, 1]) | |
| .range(["#08519c", "#bdd7e7", "#bae4b3", "#006d2c"]) | |
| .interpolate(d3.interpolateHcl); | |
| // For each small multiple… | |
| function horizon(g) { | |
| g.each(function(d, i) { | |
| d = series(d); | |
| var g = d3.select(this), | |
| n = 2 * bands + 1, | |
| xMin = Infinity, | |
| xMax = -Infinity, | |
| yMax = -Infinity, | |
| x0, // old x-scale | |
| y0, // old y-scale | |
| id; // unique id for paths | |
| // Compute x- and y-values along with extents. | |
| var data = d.map(function(d, i) { | |
| var xv = x.call(this, d, i), | |
| yv = y.call(this, d, i); | |
| if (xv < xMin) xMin = xv; | |
| if (xv > xMax) xMax = xv; | |
| if (-yv > yMax) yMax = -yv; | |
| if (yv > yMax) yMax = yv; | |
| return [xv, yv]; | |
| }); | |
| if(overallYMax !== null) { | |
| yMax = overallYMax; | |
| } | |
| // Compute the new x- and y-scales, and transform. | |
| var x1 = d3.scale.linear().domain([xMin, xMax]).range([0, w]), | |
| y1 = d3.scale.linear().domain([0, yMax]).range([0, h * bands]), | |
| t1 = d3_horizonTransform(bands, h, mode); | |
| // Retrieve the old scales, if this is an update. | |
| if (this.__chart__) { | |
| x0 = this.__chart__.x; | |
| y0 = this.__chart__.y; | |
| t0 = this.__chart__.t; | |
| id = this.__chart__.id; | |
| } else { | |
| x0 = x1.copy(); | |
| y0 = y1.copy(); | |
| t0 = t1; | |
| id = ++d3_horizonId; | |
| } | |
| // We'll use a defs to store the area path and the clip path. | |
| var defs = g.selectAll("defs") | |
| .data([null]); | |
| // The clip path is a simple rect. | |
| defs.enter().append("defs").append("clipPath") | |
| .attr("id", "d3_horizon_clip" + id) | |
| .append("rect") | |
| .attr("width", w) | |
| .attr("height", h); | |
| defs.select("rect").transition() | |
| .duration(duration) | |
| .attr("width", w) | |
| .attr("height", h); | |
| // We'll use a container to clip all horizon layers at once. | |
| g.selectAll("g") | |
| .data([null]) | |
| .enter().append("g") | |
| .attr("clip-path", "url(#d3_horizon_clip" + id + ")"); | |
| // Instantiate each copy of the path with different transforms. | |
| var path = g.select("g").selectAll("path") | |
| .data(d3.range(-1, -bands - 1, -1).concat(d3.range(1, bands + 1)), Number); | |
| var d0 = d3_horizonArea | |
| .interpolate(interpolate) | |
| .x(function(d) { return x0(d[0]); }) | |
| .y0(h * bands) | |
| .y1(function(d) { return h * bands - y0(d[1]); }) | |
| (data); | |
| var d1 = d3_horizonArea | |
| .x(function(d) { return x1(d[0]); }) | |
| .y1(function(d) { return h * bands - y1(d[1]); }) | |
| (data); | |
| path.enter().append("path") | |
| .style("fill", color) | |
| .attr("transform", t0) | |
| .attr("d", d0); | |
| path.transition() | |
| .duration(duration) | |
| .style("fill", color) | |
| .attr("transform", t1) | |
| .attr("d", d1); | |
| path.exit().transition() | |
| .duration(duration) | |
| .attr("transform", t1) | |
| .attr("d", d1) | |
| .remove(); | |
| // Stash the new scales. | |
| this.__chart__ = {x: x1, y: y1, t: t1, id: id}; | |
| }); | |
| d3.timer.flush(); | |
| } | |
| horizon.duration = function(x) { | |
| if (!arguments.length) return duration; | |
| duration = +x; | |
| return horizon; | |
| }; | |
| horizon.bands = function(x) { | |
| if (!arguments.length) return bands; | |
| bands = +x; | |
| color.domain([-bands, 0, 0, bands]); | |
| return horizon; | |
| }; | |
| horizon.mode = function(x) { | |
| if (!arguments.length) return mode; | |
| mode = x + ""; | |
| return horizon; | |
| }; | |
| horizon.colors = function(x) { | |
| if (!arguments.length) return color.range(); | |
| color.range(x); | |
| return horizon; | |
| }; | |
| horizon.interpolate = function(x) { | |
| if (!arguments.length) return interpolate; | |
| interpolate = x + ""; | |
| return horizon; | |
| }; | |
| horizon.x = function(z) { | |
| if (!arguments.length) return x; | |
| x = z; | |
| return horizon; | |
| }; | |
| horizon.y = function(z) { | |
| if (!arguments.length) return y; | |
| y = z; | |
| return horizon; | |
| }; | |
| horizon.overallYMax = function(_overallYMax) { | |
| if (!arguments.length) return overallYMax; | |
| overallYMax = +_overallYMax; | |
| return horizon; | |
| }; | |
| horizon.series = function(_series) { | |
| if (!arguments.length) return series; | |
| series = _series; | |
| return horizon; | |
| }; | |
| horizon.width = function(x) { | |
| if (!arguments.length) return w; | |
| w = +x; | |
| return horizon; | |
| }; | |
| horizon.height = function(x) { | |
| if (!arguments.length) return h; | |
| h = +x; | |
| return horizon; | |
| }; | |
| return horizon; | |
| }; | |
| var d3_horizonArea = d3.svg.area(), | |
| d3_horizonId = 0; | |
| function d3_horizonX(d) { | |
| return d[0]; | |
| } | |
| function d3_horizonY(d) { | |
| return d[1]; | |
| } | |
| function d3_horizonTransform(bands, h, mode) { | |
| return mode == "offset" | |
| ? function(d) { return "translate(0," + (d + (d < 0) - bands) * h + ")"; } | |
| : function(d) { return (d < 0 ? "scale(1,-1)" : "") + "translate(0," + (d - bands) * h + ")"; }; | |
| } | |
| })(); |
| forum = 'drugsforum' | |
| keyword = 'cocaine' | |
| H = 25 | |
| START = 886291200+86400*4 #First monday of year 1998 | |
| # START = 949363200+86400*2 #First monday of year 2000 | |
| END = 1450310400 | |
| # FIXME se la API li cambiano, devono restituire i nuovi valori. Se non lo dicono, il valore di end dev'essere una tacca più avanti, altrimenti manca l'ultimo valore. | |
| #FIXME lo step non può essere fisso in secondi, ci vuole un'aggregazione per mesi o settimane veri | |
| STEP = 604800 # 1 week | |
| #STEP = 2592000 # 30 days | |
| charts = d3.select('#charts') | |
| width = charts.node().getBoundingClientRect().width - 22 # take scrollbar into account | |
| height = charts.node().getBoundingClientRect().height | |
| colors = [d3.hcl(0,0,0),d3.hcl(0,0,0),d3.hcl(90,70,100),d3.hcl(-60,70,0)] | |
| horizon = d3.horizon() | |
| .width(width) | |
| .height(H) | |
| .bands(5) | |
| .mode("offset") | |
| .interpolate("step-after") | |
| .x((d) -> d.date) | |
| .y((d) -> d.value) | |
| .series((d) -> d.data) | |
| .colors(colors) | |
| redraw = () -> | |
| d3.select('body').classed 'wait', true | |
| d3.json "http://wafi.iit.cnr.it/cas-scraper2/api/timeseries/getForumTermTs.php?db=#{forum}&term=#{keyword}&start=#{START}&end=#{END}&step=#{STEP}", (result) -> | |
| d3.json "http://wafi.iit.cnr.it/cas-scraper2/api/meta/getForumName.php?db=#{forum}", (forum_names) -> | |
| forum_name_index = {} | |
| forum_names.forEach (d) -> | |
| forum_name_index[d.id] = d | |
| d3.select('body').classed 'wait', false | |
| # use a common Y scale for all the multiples | |
| horizon.overallYMax d3.max result.forums, (f) -> d3.max f.data, (d) -> +d.value | |
| # fill missing datapoints | |
| start = START*1000 | |
| end = END*1000+(STEP*1000) | |
| # fill the missing datapoints | |
| result.forums.forEach (f) -> | |
| index = {} | |
| f.data.forEach (d) -> | |
| d.date = 1000*d.date | |
| index[d.date] = d | |
| datapoints = d3.range(start, end, STEP*1000).map (date) -> | |
| if date of index | |
| return index[date] | |
| else | |
| return {date: date, value: 0} | |
| f.data = datapoints | |
| # sort by mean value | |
| result.forums.forEach (f) -> | |
| f.mean = d3.mean f.data, (d) -> d.value | |
| result.forums.sort (a,b) -> d3.descending(a.mean,b.mean) | |
| # empty the charts | |
| charts.selectAll 'svg' | |
| .remove() | |
| hcharts = charts.selectAll 'svg' | |
| .data(result.forums) | |
| svg = hcharts.enter().append 'svg' | |
| .attr | |
| width: width | |
| height: H+1 | |
| .style | |
| top: (d,i) -> i * (H+2) + 'px' | |
| svg.append 'line' | |
| .attr | |
| x1: 0 | |
| x2: width | |
| y1: H | |
| y2: H | |
| stroke: '#DDD' | |
| svg | |
| .call(horizon) | |
| svg.append 'text' | |
| .text (d) -> | |
| forum_name_index[d.forum_id].title | |
| .attr | |
| class: 'label' | |
| x: 10 | |
| y: H-3 | |
| d3.select("#keyword").node().value = keyword | |
| d3.select("#keyword").on 'keyup', () -> | |
| if(d3.event.keyCode == 13) | |
| keyword = this.value | |
| .replace(/ and /gi, "%2BAND%2B") # URL encoding of queries | |
| .replace(/ or /gi, "%2BOR%2B") | |
| .replace(/-/g, "%2D") | |
| .replace(/ /g, "%2B") | |
| redraw() | |
| d3.select "#forum_ctrl" | |
| .on "change", () -> | |
| forum = this.options[this.selectedIndex].value | |
| redraw() | |
| redraw() |
| body, html { | |
| padding: 0; | |
| margin: 0; | |
| width: 100%; | |
| height: 100%; | |
| overflow: hidden; | |
| font-family: sans-serif; | |
| font-size: 12px; | |
| } | |
| body.wait { | |
| cursor: progress; | |
| } | |
| body { | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| #bar { | |
| border-bottom: 1px solid #BBB; | |
| margin-bottom: 2px; | |
| background: #DDD; | |
| } | |
| #bar > * { | |
| margin: 2px; | |
| } | |
| #charts { | |
| height: 0; | |
| flex-grow: 1; | |
| background: white; | |
| position: relative; | |
| overflow-y: scroll; | |
| } | |
| svg { | |
| shape-rendering: crispEdges; | |
| position: absolute; | |
| } | |
| .label { | |
| font-size: 10px; | |
| fill: #444; | |
| text-shadow: -1px -1px white, -1px 1px white, 1px 1px white, 1px -1px white, -1px 0 white, 0 1px white, 1px 0 white, 0 -1px white; | |
| } | |
| input { | |
| padding : 0 2px; | |
| margin : 0; | |
| width : 240px; | |
| } |
| <!doctype html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8"> | |
| <title>Horizon Bar Chart</title> | |
| <link rel="stylesheet" href="index.css"> | |
| <script src="http://d3js.org/d3.v3.min.js"></script> | |
| <script src="horizon.js"></script> | |
| </head> | |
| <body> | |
| <div id="bar"> | |
| <div> | |
| <label>Select source:</label> | |
| <select id="forum_ctrl"> | |
| <option value="drugsforum">Drugs-forum</option> | |
| <option value="bluelight">Bluelight</option> | |
| </select> | |
| </div> | |
| <div id="search"> | |
| <label>Keyword:</label> | |
| <input type="search" id="keyword"> | |
| </div> | |
| </div> | |
| <div id="charts"></div> | |
| <script src="index.js"></script> | |
| </body> | |
| </html> |
| // Generated by CoffeeScript 1.10.0 | |
| (function() { | |
| var END, H, START, STEP, charts, colors, forum, height, horizon, keyword, redraw, width; | |
| forum = 'drugsforum'; | |
| keyword = 'cocaine'; | |
| H = 25; | |
| START = 886291200 + 86400 * 4; | |
| END = 1450310400; | |
| STEP = 604800; | |
| charts = d3.select('#charts'); | |
| width = charts.node().getBoundingClientRect().width - 22; | |
| height = charts.node().getBoundingClientRect().height; | |
| colors = [d3.hcl(0, 0, 0), d3.hcl(0, 0, 0), d3.hcl(90, 70, 100), d3.hcl(-60, 70, 0)]; | |
| horizon = d3.horizon().width(width).height(H).bands(5).mode("offset").interpolate("step-after").x(function(d) { | |
| return d.date; | |
| }).y(function(d) { | |
| return d.value; | |
| }).series(function(d) { | |
| return d.data; | |
| }).colors(colors); | |
| redraw = function() { | |
| d3.select('body').classed('wait', true); | |
| return d3.json("http://wafi.iit.cnr.it/cas-scraper2/api/timeseries/getForumTermTs.php?db=" + forum + "&term=" + keyword + "&start=" + START + "&end=" + END + "&step=" + STEP, function(result) { | |
| return d3.json("http://wafi.iit.cnr.it/cas-scraper2/api/meta/getForumName.php?db=" + forum, function(forum_names) { | |
| var end, forum_name_index, hcharts, start, svg; | |
| forum_name_index = {}; | |
| forum_names.forEach(function(d) { | |
| return forum_name_index[d.id] = d; | |
| }); | |
| d3.select('body').classed('wait', false); | |
| horizon.overallYMax(d3.max(result.forums, function(f) { | |
| return d3.max(f.data, function(d) { | |
| return +d.value; | |
| }); | |
| })); | |
| start = START * 1000; | |
| end = END * 1000 + (STEP * 1000); | |
| result.forums.forEach(function(f) { | |
| var datapoints, index; | |
| index = {}; | |
| f.data.forEach(function(d) { | |
| d.date = 1000 * d.date; | |
| return index[d.date] = d; | |
| }); | |
| datapoints = d3.range(start, end, STEP * 1000).map(function(date) { | |
| if (date in index) { | |
| return index[date]; | |
| } else { | |
| return { | |
| date: date, | |
| value: 0 | |
| }; | |
| } | |
| }); | |
| return f.data = datapoints; | |
| }); | |
| result.forums.forEach(function(f) { | |
| return f.mean = d3.mean(f.data, function(d) { | |
| return d.value; | |
| }); | |
| }); | |
| result.forums.sort(function(a, b) { | |
| return d3.descending(a.mean, b.mean); | |
| }); | |
| charts.selectAll('svg').remove(); | |
| hcharts = charts.selectAll('svg').data(result.forums); | |
| svg = hcharts.enter().append('svg').attr({ | |
| width: width, | |
| height: H + 1 | |
| }).style({ | |
| top: function(d, i) { | |
| return i * (H + 2) + 'px'; | |
| } | |
| }); | |
| svg.append('line').attr({ | |
| x1: 0, | |
| x2: width, | |
| y1: H, | |
| y2: H, | |
| stroke: '#DDD' | |
| }); | |
| svg.call(horizon); | |
| return svg.append('text').text(function(d) { | |
| return forum_name_index[d.forum_id].title; | |
| }).attr({ | |
| "class": 'label', | |
| x: 10, | |
| y: H - 3 | |
| }); | |
| }); | |
| }); | |
| }; | |
| d3.select("#keyword").node().value = keyword; | |
| d3.select("#keyword").on('keyup', function() { | |
| if (d3.event.keyCode === 13) { | |
| keyword = this.value.replace(/ and /gi, "%2BAND%2B").replace(/ or /gi, "%2BOR%2B").replace(/-/g, "%2D").replace(/ /g, "%2B"); | |
| return redraw(); | |
| } | |
| }); | |
| d3.select("#forum_ctrl").on("change", function() { | |
| forum = this.options[this.selectedIndex].value; | |
| return redraw(); | |
| }); | |
| redraw(); | |
| }).call(this); |
| { | |
| "data": [ | |
| { | |
| "date": 1042156800, | |
| "value": 0 | |
| }, | |
| { | |
| "date": 1044748800, | |
| "value": 50 | |
| }, | |
| { | |
| "date": 1047340800, | |
| "value": 100 | |
| }, | |
| { | |
| "date": 1049932800, | |
| "value": 150 | |
| }, | |
| { | |
| "date": 1052524800, | |
| "value": 200 | |
| }, | |
| { | |
| "date": 1055116800, | |
| "value": 250 | |
| }, | |
| { | |
| "date": 1057708800, | |
| "value": 300 | |
| }, | |
| { | |
| "date": 1060300800, | |
| "value": 350 | |
| }, | |
| { | |
| "date": 1062892800, | |
| "value": 375 | |
| }, | |
| { | |
| "date": 1065484800, | |
| "value": 425 | |
| }, | |
| { | |
| "date": 1068076800, | |
| "value": 1000 | |
| }, | |
| { | |
| "date": 1070668800, | |
| "value": 1025 | |
| }, | |
| { | |
| "date": 1073260800, | |
| "value": 750 | |
| }, | |
| { | |
| "date": 1075852800, | |
| "value": 0 | |
| }, | |
| { | |
| "date": 1078444800, | |
| "value": 50 | |
| }, | |
| { | |
| "date": 1081036800, | |
| "value": 675 | |
| }, | |
| { | |
| "date": 1083628800, | |
| "value": 50 | |
| }, | |
| { | |
| "date": 1086220800, | |
| "value": 150 | |
| }, | |
| { | |
| "date": 1088812800, | |
| "value": 50 | |
| }], | |
| "tot": 896311 | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment