Last active
February 25, 2016 13:42
-
-
Save kleem/3b0a78a3fbaee030369a to your computer and use it in GitHub Desktop.
Cassandra horizon charts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(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 + ")"; }; | |
} | |
})(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"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