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/4c41a9094cd2b32966be to your computer and use it in GitHub Desktop.
Save Kcnarf/4c41a9094cd2b32966be to your computer and use it in GitHub Desktop.
timeline - trend
license: mit

An example of how to draw a trend line.

Usages :

  • Drag & Drop each point to see the impact on the trend line

Notes:

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;
}
.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>
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