Skip to content

Instantly share code, notes, and snippets.

@Kcnarf
Last active December 3, 2018 08:35
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/57f8446c1e940e3f47a5 to your computer and use it in GitHub Desktop.
Save Kcnarf/57f8446c1e940e3f47a5 to your computer and use it in GitHub Desktop.
timeline - trend, confidence interval, outliers
license: mit

An example of how to draw a trend line with it's 95%-CI (Confidence Interval of 95%).

Usages :

  • Drag & Drop each point to see the impact on the trend line and 95%-CI
  • Drag & Drop the timeline to see that this has no impact nor on the trend, nor on the 95%-CI
  • increase or decrease the dispersion of the time serie to see that this impacts the 95%-CI, but not the trend
  • points outside a confidence interval may be considered outliers; they are important points, and finding the root/business cause of such points may be crucial (even if they may be the consequence of evidence (eg. increase of sell due to a discount)).

Notes:

  • trend line computed using least square method
  • 95%-CI computed using +/- 1,96*standard deviation; other intervals (e.g. 99%-CI) can be used

Acknowledgments:

<!DOCTYPE html>
<meta charset="utf-8">
<style>
#controls{
position: absolute;
right: 0px;
}
.grid>line, .grid>.intersect {
fill: none;
stroke: #ddd;
shape-rendering: crispEdges;
vector-effect: non-scaling-stroke;
}
.axis path,
.axis line {
fill: none;
stroke: black;
shape-rendering: crispEdges;
}
.axis text {
font-family: sans-serif;
font-size: 11px;
}
.timeline {
fill: none;
stroke: steelblue;
stroke-width: 2px;
opacity: 0.2;
}
.timeline.draggable:hover, .timeline.dragging {
stroke: pink;
opacity: 1;
cursor: ns-resize;
}
.dot {
fill: lightsteelblue;
stroke: white;
stroke-width: 3px;
}
.dot.draggable:hover, .dot.dragging {
fill: pink;
cursor: ns-resize;
}
.trend {
stroke: steelblue;
}
.ci {
fill: steelblue;
fill-opacity: 0.1;
}
</style>
<body>
<div id="controls">
<button onclick="invertTrend();">invert trend</button>
<button onclick="disperse();">increase dispersion</button>
<button onclick="concentrate();">decrease dispersion</button>
<button onclick="makeOutlier();">make outlier</button>
</div>
<script src="https://d3js.org/d3.v3.min.js"></script>
<script>
var rawTimeSerie = []
var timeSerie = [];
var trend = 0;
var interception = 0;
var WITH_TRANSITION = true;
var WITHOUT_TRANSITION = false
var duration = 500;
var xAxisLabelHeight= 20;
var yAxisLabelWidth= 20;
var margin = {top: 20, right: 20, bottom: (20+xAxisLabelHeight), left: (20+yAxisLabelWidth)},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
var drag = d3.behavior.drag()
.origin(function(d) { return d; })
.on("dragstart", dragStarted)
.on("drag", dragged)
.on("dragend", dragEnded);
var dragTimeline= d3.behavior.drag()
.origin(function(d) { return d; })
.on("dragstart", dragStarted)
.on("drag", draggedTimeline)
.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 xAxisDef = d3.svg.axis()
.scale(x);
var yAxisDef = d3.svg.axis()
.scale(y)
.orient("left");
var svg = d3.select("body").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + (margin.left) + "," + (height+margin.top) + ")")
var container = svg.append("g");
var grid = container.append("g")
.attr("class", "grid");
var intersects = [];
d3.range(1, x.invert(width)).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("x", 6)
.attr("y", -height+10)
.style("text-anchor", "start")
.text("Amount");
var CIArea = container.append("path")
.attr("class", "ci")
.attr("d", "M"+[x(0),y(20)]+"L"+[x(20),y(20)]+"v"+(y(-10))+"L"+[x(0),y(10)]+"Z");
var timeline = container.append("path")
.classed("timeline draggable", true)
.attr("d", line)
.call(dragTimeline);
var dotContainer = container.append("g")
.classed("dots", true);
var trendLine = container.append("line")
.attr("class", "trend")
.attr("x1", x(0))
.attr("y1", y(0))
.attr("x2", x(20))
.attr("y2", y(0));
d3.csv("timeserie.csv", dottype, function(error, dots) {
updateTimeline(WITHOUT_TRANSITION);
updateDots(WITHOUT_TRANSITION);
updateTrendAndCI(WITHOUT_TRANSITION);
});
function dottype(d) {
d.x = +d.x;
d.y = +d.y;
rawTimeSerie.push(d);
timeSerie.push(d);
return d;
}
var line = d3.svg.line()
.x(function(d) { return x(d.x); })
.y(function(d) { return y(d.y); });
function updateDots(withTransition) {
dots = dotContainer.selectAll(".dot")
.data(timeSerie);
dots.enter()
.append("circle")
.classed("dot draggable", true)
.attr("r", 5)
.attr("cx", function(d) { return x(d.x); })
.call(drag);
dots.transition()
.duration(withTransition? duration : 0)
.attr("cy", function(d) { return y(d.y); })
}
function updateTimeline(withTransition) {
timeline.data(timeSerie).transition()
.duration(withTransition? duration : 0)
.attr("d", line(timeSerie));
}
function updateTrendAndCI(withTransition) {
// The objective is to draw a line that is the closest line from each point
// (cf. https://en.wikipedia.org/wiki/Linear_regression)
// A simple regression line is of the form y=ax+b, where a is the trend of the time serie
// below code computes 'a' and 'b'
//furthermore, to compute the CI, we have to compute the standard deviation
var serieLength = timeSerie.length;
var timeInterval = 1
var countSum = 0;
var squareCountSum = 0;
var orderCountSum = 0;
timeSerie.forEach(function(d){
countSum += d.y;
squareCountSum += (d.y)*(d.y);
orderCountSum += (d.x)*(d.y);
});
var a = (12*orderCountSum - 6*(serieLength+1)*countSum)/(timeInterval*serieLength*(serieLength-1)*(serieLength+1));
var b = (2*(2*serieLength+1)*countSum - 6*orderCountSum)/(serieLength*(serieLength-1));
trend = a;
interception = b;
var variance = ( squareCountSum
- 2*b*countSum
- 2*a*orderCountSum
+ serieLength*(b*b)
+ b*a*serieLength*(serieLength+1)
+ ((a*a)*serieLength*(serieLength+1)*(2*serieLength+1))/6
) / serieLength
var stdDev = Math.sqrt(variance)
var confidence = 1.96*stdDev
//drawing
trendLine
.transition()
.duration(withTransition? duration : 0)
.attr("y1", y(b))
.attr("y2", y(a*serieLength+b));
CIArea
.transition()
.duration(withTransition? duration : 0)
.attr("d", "M"+[x(0),y(b+confidence)]+"L"+[x(20),y(a*serieLength+b+confidence)]+"v"+(y(-2*confidence))+"L"+[x(0),y(b-confidence)]+"Z");
}
function invertTrend() {
var serieLength = timeSerie.length;
var countSum = 0;
var mean = 0;
timeSerie.forEach(function (d) {
countSum += d.y
});
mean = countSum/serieLength;
timeSerie.forEach(function (d) {
d.y = (mean-d.y)+mean;
});
updateTimeline(WITH_TRANSITION);
updateDots(WITH_TRANSITION);
updateTrendAndCI(WITH_TRANSITION);
}
function changeDispersion(scale) {
var serieLength = timeSerie.length;
var timeInterval = 1;
var ySum = 0;
var timeYSum = 0;
timeSerie.forEach(function(d){
ySum += d.y;
timeYSum += d.x*d.y;
});
var trend = (12*timeYSum - 6*(serieLength+1)*ySum)/(timeInterval*serieLength*(serieLength-1)*(serieLength+1));
var intercept = (2*(2*serieLength+1)*ySum - 6*timeYSum)/(serieLength*(serieLength-1));
var expected;
timeSerie.forEach(function(d){
expected = d.x*trend + intercept;
d.y = expected + scale*(d.y-expected);
});
updateTimeline(WITH_TRANSITION);
updateDots(WITH_TRANSITION);
updateTrendAndCI(WITH_TRANSITION);
}
function disperse(serieName) {
changeDispersion(1.6)
}
function concentrate(serieName) {
changeDispersion(0.625)
}
function makeOutlier() {
if (trend > 0) {
timeSerie[18].y = 5;
} else {
timeSerie[18].y = 45;
}
updateTimeline(WITH_TRANSITION);
updateDots(WITH_TRANSITION);
updateTrendAndCI(WITH_TRANSITION);
}
function dragStarted(d) {
d3.select(this).classed("dragging", true);
}
function dragged(d) {
d.y += y.invert(d3.event.dy)
d3.select(this).attr("cy", y(d.y));
updateTimeline(WITHOUT_TRANSITION);
updateDots(WITHOUT_TRANSITION);
updateTrendAndCI(WITHOUT_TRANSITION);
}
function dragEnded(d) {
d3.select(this).classed("dragging", false);
}
function draggedTimeline(d) {
var rawdy = y.invert(d3.event.dy);
timeSerie.forEach(function(d){
d.y += rawdy;
});
updateTimeline(WITHOUT_TRANSITION);
updateDots(WITHOUT_TRANSITION);
updateTrendAndCI(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