block by Kcnarf 57f8446c1e940e3f47a5

timeline - trend, confidence interval, outliers

Full Screen

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

Usages :

Notes:

Acknowledgments:

index.html

<!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>

timeserie.csv

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