Skip to content

Instantly share code, notes, and snippets.

@Kcnarf
Last active December 3, 2018 08:34
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Kcnarf/614f51eec5f953d9000b to your computer and use it in GitHub Desktop.
Save Kcnarf/614f51eec5f953d9000b to your computer and use it in GitHub Desktop.
timeline - comparing trends
license: mit

An example of how to compare trends of several time series.

Question: a trend of +10 over 100 things is higher or lower than a trend of +5 over 10 things ?

Response: it depends ... if you are interested in the raw trend, then the first trend is higher (+10 > +5); but if you are interested in the relative trend, then the second trend is higher (+10/100 < +5/10)

In the above viz, the left graph concerns raw data, and the right graph deals with relative data. If you want to compare raw trends, look at the left graph; and if you want to compare relative trends, look at the right one.

Relative values (dots on the rigth graph) are the raw values divided by the mean of their respective serie. The mean is used because it has interesting mathematical/computationnal properties over other possible ways to divide values (such as min, max, median, ...). In the statistical world, data divided by their respective mean are called reduced (reduced value, reduced trend, ..., but the reduced standard deviation is called coefficient of variation).

Usages :

  • in the left graph, Drag & Drop a point to see the overall impact (on its trend, its related reduced values, and its related reduced trend)
  • in the left graph, Drag & Drop a timeline (to change each vaue of the related time serie), and see the impact on the related reduced trend;

Notes:

  • trend line computed using least square method

Acknowledgments:

<!DOCTYPE html>
<meta charset="utf-8">
<style>
body {
position: relative;
background-color: #ddd;
margin: auto;
}
#under-construction {
display: none;
position: absolute;
top: 200px;
left: 300px;
font-size: 40px;
}
#flow {
position: absolute;
top: 234px;
left: 469px;
font-size: 30px;
color: darkgrey;
}
#controls {
position: absolute;
top: 5px;
right: 5px;
}
.viz {
position: absolute;
background-color: white;
border-radius: 10px;
top: 5px;
height: 490px;
}
.viz#raw {
left: 5px;
}
.viz#reduced {
right: 5px;
}
.axis path,
.axis line {
fill: none;
stroke: black;
shape-rendering: crispEdges;
}
.axis text {
font-family: sans-serif;
font-size: 11px;
}
.grid>line, .grid>.intersect {
fill: none;
stroke: #ddd;
shape-rendering: crispEdges;
vector-effect: non-scaling-stroke;
}
.legend {
font-size: 12px;
}
.dot {
fill: lightsteelblue;
stroke: white;
stroke-width: 3px;
}
.dot.serie2 {
fill: darkseagreen;
}
.dot.reduced {
opacity: 0.5;
}
.dot.draggable:hover, .dot.dragging {
fill: pink;
cursor: ns-resize;
}
.timeline {
fill: none;
stroke: steelblue;
stroke-width: 2px;
opacity: 0.1;
}
.timeline.serie2 {
stroke: darkseagreen;
}
.timeline.draggable:hover, .timeline.dragging {
stroke: pink;
opacity: 1;
cursor: ns-resize;
}
.trend {
stroke: steelblue;
stroke-width: 2px;
}
.trend.serie2 {
stroke: green;
}
</style>
<body>
<div class="viz" id="raw">
<div id="controls">
<button onclick="makeSameTrend();">[un]make same trend</button>
</div>
</div>
<div class="viz" id="reduced"></div>
<div id="flow">
&#8614;
</div>
<div id="under-construction">
UNDER CONSTRUCTION
</div>
<script src="https://d3js.org/d3.v3.min.js"></script>
<script>
var timeSerie = [];
var mean1 = 0;
var mean2 = 0;
var sameTrend = false;
var WITH_TRANSITION = true;
var WITHOUT_TRANSITION = false
var duration = 500;
var vizMargin = 5,
flowWidth = 10
legendHeight = 0,
xAxisLabelHeight = 10,
yAxisLabelWidth = 10,
margin = {top: 20, right: 20, bottom: 20, left: 20},
svgWidth = 960/2 - 2*vizMargin - flowWidth,
svgHeight = 500 - 2*vizMargin,
width = svgWidth - margin.left - margin.right - yAxisLabelWidth,
height = svgHeight - margin.top - margin.bottom - xAxisLabelHeight - legendHeight;
var drag1 = d3.behavior.drag()
.origin(function(d) { return d; })
.on("dragstart", dragStarted)
.on("drag", dragged1)
.on("dragend", dragEnded);
var drag2 = d3.behavior.drag()
.origin(function(d) { return d; })
.on("dragstart", dragStarted)
.on("drag", dragged2)
.on("dragend", dragEnded);
var dragTimeline1= d3.behavior.drag()
.origin(function(d) { return d; })
.on("dragstart", dragStarted)
.on("drag", draggedTimeline1)
.on("dragend", dragEnded);
var dragTimeline2= d3.behavior.drag()
.origin(function(d) { return d; })
.on("dragstart", dragStarted)
.on("drag", draggedTimeline2)
.on("dragend", dragEnded);
var x = d3.scale.linear()
.domain([0, 20])
.range([0, width]);
var y = d3.scale.linear()
.domain([0, 50])
.range([0, -height]);
var reducedY = d3.scale.linear()
.domain([0, 2])
.range([0, -height]);
var xAxisDef = d3.svg.axis()
.scale(x)
.ticks(11);
var yAxisDef = d3.svg.axis()
.scale(y)
.orient("left");
var reducedYAxisDef = d3.svg.axis()
.scale(reducedY)
.orient("left");
var svg = d3.select("#raw").append("svg")
.attr("width", svgWidth)
.attr("height", svgHeight)
.append("g")
.attr("transform", "translate(" + [margin.left, margin.top] + ")");
var container = svg.append("g")
.attr("id", "graph")
.attr("transform", "translate(" + [yAxisLabelWidth, height] + ")");
var grid = container.append("g")
.attr("class", "grid");
var intersects = [];
d3.range(2, x.invert(width), 2).forEach(function(a) { d3.range(5, y.invert(-height),5).forEach(function(b) { intersects.push([a,b])})});
grid.selectAll(".intersect")
.data(intersects)
.enter().append("path")
.classed("intersect", true)
.attr("d", function(d) { return "M"+[x(d[0])-1,y(d[1])]+"h3M"+[x(d[0]),y(d[1])-1]+"v3"});
container.append("g")
.attr("class", "axis x")
.call(xAxisDef);
container.append("text")
.attr("x", width)
.attr("y", -6)
.style("text-anchor", "end")
.text("Time");
container.append("g")
.attr("class", "axis y")
.call(yAxisDef);
container.append("text")
.attr("transform", "rotate(-90)")
.attr("x", height)
.attr("y", 16)
.style("text-anchor", "end")
.text("Amount");
var timeline1 = container.append("path")
.classed("timeline serie1 draggable", true)
.attr("d", line1)
.call(dragTimeline1);
var timeline2 = container.append("path")
.classed("timeline serie1 draggable", true)
.attr("d", line2)
.call(dragTimeline2);
var dotContainer = container.append("g")
.classed("dots", true);
var trendLine1 = container.append("line")
.datum(1)
.attr("class", "trend draggable serie1")
.attr("x1", x(0))
.attr("y1", y(0))
.attr("x2", x(20))
.attr("y2", y(0));
var trendLine2 = container.append("line")
.datum(2)
.attr("class", "trend draggable serie2")
.attr("x1", x(0))
.attr("y1", y(0))
.attr("x2", x(20))
.attr("y2", y(0));
svg = d3.select("#reduced").append("svg")
.attr("width", svgWidth)
.attr("height", svgHeight)
.append("g")
.attr("transform", "translate(" + [margin.left, margin.top] + ")");
var container2 = svg.append("g")
.attr("id", "graph reduced")
.attr("transform", "translate(" + [yAxisLabelWidth, height] + ")");
var grid2 = container2.append("g")
.attr("class", "grid");
var intersects = [];
d3.range(2, x.invert(width), 2).forEach(function(a) { d3.range(0.2, reducedY.invert(-height),0.2).forEach(function(b) { intersects.push([a,b])})});
grid2.selectAll(".intersect")
.data(intersects)
.enter().append("path")
.classed("intersect", true)
.attr("d", function(d) { return "M"+[x(d[0])-1,reducedY(d[1])]+"h3M"+[x(d[0]),reducedY(d[1])-1]+"v3"});
grid2.append("line")
.classed("x-line", true)
.attr("x1", x(0))
.attr("y1", reducedY(1))
.attr("x2", x(20))
.attr("y2", reducedY(1));
container2.append("g")
.attr("class", "axis x")
.call(xAxisDef);
container2.append("text")
.attr("x", width)
.attr("y", -6)
.style("text-anchor", "end")
.text("Time");
container2.append("g")
.attr("class", "axis y")
.call(reducedYAxisDef);
container2.append("text")
.attr("transform", "rotate(-90)")
.attr("x", height)
.attr("y", 16)
.style("text-anchor", "end")
.text("Relative Amount");
container2.append("text")
.attr("transform", "rotate(-90)")
.attr("x", height)
.attr("y", 30)
.style("text-anchor", "end")
.style("font-size", "12px")
.text("(Amount / Mean)");
container2.append("text")
.attr("style", "font-size:12px")
.attr("x", x(20))
.attr("y", reducedY(1)-3)
.style("text-anchor", "end")
.text("All relative series have");
container2.append("text")
.attr("style", "font-size:12px")
.attr("x", x(20))
.attr("y", reducedY(1)+9)
.style("text-anchor", "end")
.text("the same mean");
var reducedTimeline1 = container2.append("path")
.classed("timeline serie1 reduced draggable", true)
.attr("d", reducedLine1)
.call(dragTimeline1);
var reducedTimeline2 = container2.append("path")
.classed("timeline serie2 reduced draggable", true)
.attr("d", reducedLine2)
.call(dragTimeline2);
var reducedDotContainer = container2.append("g")
.classed("dots reduced", true);
var reducedTrendLine1 = container2.append("line")
.attr("class", "trend serie1 reduced")
.attr("x1", x(0))
.attr("y1", y(0))
.attr("x2", x(20))
.attr("y2", y(0));
var reducedTrendLine2 = container2.append("line")
.attr("class", "trend serie2 reduced")
.attr("x1", x(0))
.attr("y1", y(0))
.attr("x2", x(20))
.attr("y2", y(0));
d3.csv("timeserie.csv", dottype, function(error, dots) {
updateMeans();
updateTimelines(WITHOUT_TRANSITION);
updateDots(WITHOUT_TRANSITION);
updateTrends(WITHOUT_TRANSITION);
});
function dottype(d) {
d.x = +d.x;
d.y1 = +d.y+d.x/2-10;
d.y2 = +d.y1/2-2;
timeSerie.push(d);
return d;
}
var line1 = d3.svg.line()
.x(function(d) { return x(d.x); })
.y(function(d) { return y(d.y1); });
var line2 = d3.svg.line()
.x(function(d) { return x(d.x); })
.y(function(d) { return y(d.y2); });
var reducedLine1 = d3.svg.line()
.x(function(d) { return x(d.x); })
.y(function(d) { return reducedY(d.y1/mean1); });
var reducedLine2 = d3.svg.line()
.x(function(d) { return x(d.x); })
.y(function(d) { return reducedY(d.y2/mean2); });
function makeSameTrend() {
var meanDelta = mean1-mean2;
timeSerie.forEach(function(d){
if (sameTrend) {
d.y2 = +d.y1/2-2;
} else {
d.y2 = d.y1-meanDelta;
}
});
mean2 = sameTrend? mean1/2-2 : mean1-meanDelta;
sameTrend = !sameTrend
updateDots(WITH_TRANSITION);
updateTimelines(WITH_TRANSITION);
updateTrends(WITH_TRANSITION);
}
function updateMeans() {
var serieLength = timeSerie.length;
var countSum1 = 0;
var countSum2 = 0;
timeSerie.forEach(function(d){
countSum1 += d.y1;
countSum2 += d.y2;
});
mean1 = countSum1/serieLength;
mean2 = countSum2/serieLength;
}
function updateDots(withTransition) {
var dots = dotContainer.selectAll(".dot.serie1")
.data(timeSerie);
dots.enter()
.append("circle")
.classed("dot draggable serie1", true)
.attr("r", 5)
.attr("cx", function(d) { return x(d.x); })
.attr("cy", function(d) { return y(d.y1); })
.call(drag1);
dots.transition()
.duration(withTransition? duration : 0)
.attr("cy", function(d) { return y(d.y1); })
dots = dotContainer.selectAll(".dot.serie2")
.data(timeSerie);
dots.enter()
.append("circle")
.classed("dot draggable serie2", true)
.attr("r", 5)
.attr("cx", function(d) { return x(d.x); })
.attr("cy", function(d) { return y(d.y2); })
.call(drag2);
dots.transition()
.duration(withTransition? duration : 0)
.attr("cy", function(d) { return y(d.y2); })
var reducedDots = reducedDotContainer.selectAll(".dot.serie1.reduced")
.data(timeSerie);
reducedDots.enter()
.append("circle")
.classed("dot serie1 reduced", true)
.attr("r", 5)
.attr("cx", function(d) { return x(d.x); })
.attr("cy", function(d) { return reducedY(d.y1/mean1); });
reducedDots.transition()
.duration(withTransition? duration : 0)
.attr("cy", function(d) { return reducedY(d.y1/mean1); })
reducedDots = reducedDotContainer.selectAll(".dot.serie2.reduced")
.data(timeSerie);
reducedDots.enter()
.append("circle")
.classed("dot serie2 reduced", true)
.attr("r", 5)
.attr("cx", function(d) { return x(d.x); })
.attr("cy", function(d) { return reducedY(d.y2/mean2); });
reducedDots.transition()
.duration(withTransition? duration : 0)
.attr("cy", function(d) { return reducedY(d.y2/mean2); })
}
function updateTimelines(withTransition) {
timeline1.data(timeSerie).transition()
.duration(withTransition? duration : 0)
.attr("d", line1(timeSerie));
timeline2.data(timeSerie).transition()
.duration(withTransition? duration : 0)
.attr("d", line2(timeSerie));
reducedTimeline1.data(timeSerie).transition()
.duration(withTransition? duration : 0)
.attr("d", reducedLine1(timeSerie));
reducedTimeline2.data(timeSerie).transition()
.duration(withTransition? duration : 0)
.attr("d", reducedLine2(timeSerie));
}
function updateTrends(withTransition) {
var serieLength = timeSerie.length;
var timeInterval = 1
var countSum1 = 0;
var orderCountSum1 = 0;
var countSum2 = 0;
var orderCountSum2 = 0;
timeSerie.forEach(function(d){
countSum1 += d.y1;
orderCountSum1 += (d.x)*(d.y1);
countSum2 += d.y2;
orderCountSum2 += (d.x)*(d.y2);
});
var a1 = (12*orderCountSum1 - 6*(serieLength+1)*countSum1)/(timeInterval*serieLength*(serieLength-1)*(serieLength+1));
var b1 = (2*(2*serieLength+1)*countSum1 - 6*orderCountSum1)/(serieLength*(serieLength-1));
var a2 = (12*orderCountSum2 - 6*(serieLength+1)*countSum2)/(timeInterval*serieLength*(serieLength-1)*(serieLength+1));
var b2 = (2*(2*serieLength+1)*countSum2 - 6*orderCountSum2)/(serieLength*(serieLength-1));
trendLine1
.transition()
.duration(withTransition? duration : 0)
.attr("y1", y(b1))
.attr("y2", y(a1*serieLength+b1));
trendLine2
.transition()
.duration(withTransition? duration : 0)
.attr("y1", y(b2))
.attr("y2", y(a2*serieLength+b2));
reducedTrendLine1
.transition()
.duration(withTransition? duration : 0)
.attr("y1", reducedY(b1/mean1))
.attr("y2", reducedY((a1*serieLength+b1)/mean1));
reducedTrendLine2
.transition()
.duration(withTransition? duration : 0)
.attr("y1", reducedY(b2/mean2))
.attr("y2", reducedY((a2*serieLength+b2)/mean2));
}
function dragStarted(d) {
d3.select(this).classed("dragging", true);
}
function dragged1(d) {
d.y1 += y.invert(d3.event.dy)
d3.select(this).attr("cy", y(d.y1));
updateMeans(WITHOUT_TRANSITION);
updateDots(WITHOUT_TRANSITION);
updateTimelines(WITHOUT_TRANSITION);
updateTrends(WITHOUT_TRANSITION);
}
function dragged2(d) {
d.y2 += y.invert(d3.event.dy)
d3.select(this).attr("cy", y(d.y2));
updateMeans(WITHOUT_TRANSITION);
updateDots(WITHOUT_TRANSITION);
updateTimelines(WITHOUT_TRANSITION);
updateTrends(WITHOUT_TRANSITION);
}
function dragEnded(d) {
d3.select(this).classed("dragging", false);
}
function draggedTimeline1(d) {
var rawdy = y.invert(d3.event.dy);
timeSerie.forEach(function(d){
d.y1 += rawdy;
});
mean1 += rawdy;
updateDots(WITHOUT_TRANSITION);
updateTimelines(WITHOUT_TRANSITION);
updateTrends(WITHOUT_TRANSITION);
}
function draggedTimeline2(d) {
var rawdy = y.invert(d3.event.dy);
timeSerie.forEach(function(d){
d.y2 += rawdy;
});
mean2 += rawdy;
updateDots(WITHOUT_TRANSITION);
updateTimelines(WITHOUT_TRANSITION);
updateTrends(WITHOUT_TRANSITION);
}
</script>
x y
1 24
2 25
3 22
4 28
5 26
6 29
7 31
8 30
9 34
10 34
11 33
12 34
13 39
14 44
15 42
16 44
17 41
18 41
19 44
20 44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment