Skip to content

Instantly share code, notes, and snippets.

@kleem
Last active February 25, 2016 13:42
Show Gist options
  • Save kleem/3b0a78a3fbaee030369a to your computer and use it in GitHub Desktop.
Save kleem/3b0a78a3fbaee030369a to your computer and use it in GitHub Desktop.
Cassandra horizon charts
(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