|
<!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="//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> |