D3
OG
Old school D3 from simpler times
All examples
By author
By category
About
Kcnarf
Full window
Github gist
timeline - trend
An example of how to draw a trend line.
Usages :
Drag & Drop each point to see the impact on the trend line
Notes:
trend line computetd using
least square method
experiment how
outliers
(aberant values) have a
huge impact
on the trend line (cf.
Timeline - trend, confidence interval, outliers
for outlier detection)
Acknowledgments:
done with D3 v3.5.5
blockbuilder.org
drag behavior from
mbostock
's block:
Drag + Zoom
<!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; } .dot { fill: lightsteelblue; stroke: white; stroke-width: 3px; } .dot.draggable:hover, .dot.dragging { fill: pink; cursor: ns-resize; } .timeline { fill: none; stroke: steelblue; stroke-width: 2px; opacity: 0.2; } .timeline.draggable:hover, .timeline.dragging { stroke: pink; opacity: 1; cursor: ns-resize; } .trend { stroke: steelblue; stroke-width: 2px; } </style> <body> <div id="controls"> <button onclick="invertTrend();">invert trend</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 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") .classed("trend", true) .attr("x1", x(0)) .attr("y1", y(0)) .attr("x2", x(20)) .attr("y2", y(0)); d3.csv("timeserie.csv", dottype, function(error, dots) { updateDots(WITHOUT_TRANSITION); updateTimeline(WITHOUT_TRANSITION); updateTrend(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 updateTrend(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' var serieLength = timeSerie.length; var timeInterval = 1 var countSum = 0; var orderCountSum = 0; timeSerie.forEach(function(d){ countSum += 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; trendLine .transition() .duration(withTransition? duration : 0) .attr("y1", y(b)) .attr("y2", y(a*serieLength+b)); } 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; }); updateDots(WITH_TRANSITION); updateTimeline(WITH_TRANSITION); updateTrend(WITH_TRANSITION); } function makeOutlier() { if (trend > 0) { timeSerie[18].y = 5; } else { timeSerie[18].y = 45; } updateDots(WITH_TRANSITION); updateTimeline(WITH_TRANSITION); updateTrend(WITH_TRANSITION); } function dragStarted(d) { d3.select(this).classed("dragging", true); } function dragged(d) { d.y += y.invert(d3.event.dy) updateDots(WITHOUT_TRANSITION); updateTimeline(WITHOUT_TRANSITION); updateTrend(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); updateTrend(WITHOUT_TRANSITION); } </script>
https://d3js.org/d3.v3.min.js