|
<!DOCTYPE html> |
|
<meta charset="utf-8"> |
|
<style> |
|
|
|
body { |
|
position: relative; |
|
background-color: #ddd; |
|
margin: auto; |
|
} |
|
|
|
#under-construction { |
|
display: none; |
|
position: absolute; |
|
top: 200px; |
|
left: 300px; |
|
font-size: 40px; |
|
} |
|
|
|
#flow { |
|
position: absolute; |
|
top: 234px; |
|
left: 469px; |
|
font-size: 30px; |
|
color: darkgrey; |
|
} |
|
|
|
#controls { |
|
position: absolute; |
|
top: 5px; |
|
right: 5px; |
|
} |
|
|
|
.viz { |
|
position: absolute; |
|
background-color: white; |
|
border-radius: 10px; |
|
top: 5px; |
|
height: 490px; |
|
} |
|
.viz#raw { |
|
left: 5px; |
|
} |
|
.viz#reduced { |
|
right: 5px; |
|
} |
|
|
|
.axis path, |
|
.axis line { |
|
fill: none; |
|
stroke: black; |
|
shape-rendering: crispEdges; |
|
} |
|
.axis text { |
|
font-family: sans-serif; |
|
font-size: 11px; |
|
} |
|
|
|
.grid>line, .grid>.intersect { |
|
fill: none; |
|
stroke: #ddd; |
|
shape-rendering: crispEdges; |
|
vector-effect: non-scaling-stroke; |
|
} |
|
|
|
.legend { |
|
font-size: 12px; |
|
} |
|
|
|
.dot { |
|
fill: lightsteelblue; |
|
stroke: white; |
|
stroke-width: 3px; |
|
} |
|
.dot.serie2 { |
|
fill: darkseagreen; |
|
} |
|
.dot.reduced { |
|
opacity: 0.5; |
|
} |
|
.dot.draggable:hover, .dot.dragging { |
|
fill: pink; |
|
cursor: ns-resize; |
|
} |
|
|
|
|
|
.timeline { |
|
fill: none; |
|
stroke: steelblue; |
|
stroke-width: 2px; |
|
opacity: 0.1; |
|
} |
|
.timeline.serie2 { |
|
stroke: darkseagreen; |
|
} |
|
.timeline.draggable:hover, .timeline.dragging { |
|
stroke: pink; |
|
opacity: 1; |
|
cursor: ns-resize; |
|
} |
|
|
|
.trend { |
|
stroke: steelblue; |
|
stroke-width: 2px; |
|
} |
|
.trend.serie2 { |
|
stroke: green; |
|
} |
|
|
|
</style> |
|
<body> |
|
<div class="viz" id="raw"> |
|
<div id="controls"> |
|
<button onclick="makeSameTrend();">[un]make same trend</button> |
|
</div> |
|
</div> |
|
<div class="viz" id="reduced"></div> |
|
<div id="flow"> |
|
↦ |
|
</div> |
|
<div id="under-construction"> |
|
UNDER CONSTRUCTION |
|
</div> |
|
<script src="//d3js.org/d3.v3.min.js"></script> |
|
<script> |
|
var timeSerie = []; |
|
var mean1 = 0; |
|
var mean2 = 0; |
|
var sameTrend = false; |
|
|
|
var WITH_TRANSITION = true; |
|
var WITHOUT_TRANSITION = false |
|
var duration = 500; |
|
|
|
var vizMargin = 5, |
|
flowWidth = 10 |
|
legendHeight = 0, |
|
xAxisLabelHeight = 10, |
|
yAxisLabelWidth = 10, |
|
margin = {top: 20, right: 20, bottom: 20, left: 20}, |
|
svgWidth = 960/2 - 2*vizMargin - flowWidth, |
|
svgHeight = 500 - 2*vizMargin, |
|
width = svgWidth - margin.left - margin.right - yAxisLabelWidth, |
|
height = svgHeight - margin.top - margin.bottom - xAxisLabelHeight - legendHeight; |
|
|
|
var drag1 = d3.behavior.drag() |
|
.origin(function(d) { return d; }) |
|
.on("dragstart", dragStarted) |
|
.on("drag", dragged1) |
|
.on("dragend", dragEnded); |
|
var drag2 = d3.behavior.drag() |
|
.origin(function(d) { return d; }) |
|
.on("dragstart", dragStarted) |
|
.on("drag", dragged2) |
|
.on("dragend", dragEnded); |
|
|
|
var dragTimeline1= d3.behavior.drag() |
|
.origin(function(d) { return d; }) |
|
.on("dragstart", dragStarted) |
|
.on("drag", draggedTimeline1) |
|
.on("dragend", dragEnded); |
|
var dragTimeline2= d3.behavior.drag() |
|
.origin(function(d) { return d; }) |
|
.on("dragstart", dragStarted) |
|
.on("drag", draggedTimeline2) |
|
.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 reducedY = d3.scale.linear() |
|
.domain([0, 2]) |
|
.range([0, -height]); |
|
|
|
var xAxisDef = d3.svg.axis() |
|
.scale(x) |
|
.ticks(11); |
|
|
|
var yAxisDef = d3.svg.axis() |
|
.scale(y) |
|
.orient("left"); |
|
|
|
var reducedYAxisDef = d3.svg.axis() |
|
.scale(reducedY) |
|
.orient("left"); |
|
|
|
var svg = d3.select("#raw").append("svg") |
|
.attr("width", svgWidth) |
|
.attr("height", svgHeight) |
|
.append("g") |
|
.attr("transform", "translate(" + [margin.left, margin.top] + ")"); |
|
|
|
var container = svg.append("g") |
|
.attr("id", "graph") |
|
.attr("transform", "translate(" + [yAxisLabelWidth, height] + ")"); |
|
|
|
var grid = container.append("g") |
|
.attr("class", "grid"); |
|
var intersects = []; |
|
d3.range(2, x.invert(width), 2).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("transform", "rotate(-90)") |
|
.attr("x", height) |
|
.attr("y", 16) |
|
.style("text-anchor", "end") |
|
.text("Amount"); |
|
|
|
var timeline1 = container.append("path") |
|
.classed("timeline serie1 draggable", true) |
|
.attr("d", line1) |
|
.call(dragTimeline1); |
|
var timeline2 = container.append("path") |
|
.classed("timeline serie1 draggable", true) |
|
.attr("d", line2) |
|
.call(dragTimeline2); |
|
|
|
var dotContainer = container.append("g") |
|
.classed("dots", true); |
|
|
|
var trendLine1 = container.append("line") |
|
.datum(1) |
|
.attr("class", "trend draggable serie1") |
|
.attr("x1", x(0)) |
|
.attr("y1", y(0)) |
|
.attr("x2", x(20)) |
|
.attr("y2", y(0)); |
|
var trendLine2 = container.append("line") |
|
.datum(2) |
|
.attr("class", "trend draggable serie2") |
|
.attr("x1", x(0)) |
|
.attr("y1", y(0)) |
|
.attr("x2", x(20)) |
|
.attr("y2", y(0)); |
|
|
|
svg = d3.select("#reduced").append("svg") |
|
.attr("width", svgWidth) |
|
.attr("height", svgHeight) |
|
.append("g") |
|
.attr("transform", "translate(" + [margin.left, margin.top] + ")"); |
|
|
|
var container2 = svg.append("g") |
|
.attr("id", "graph reduced") |
|
.attr("transform", "translate(" + [yAxisLabelWidth, height] + ")"); |
|
|
|
var grid2 = container2.append("g") |
|
.attr("class", "grid"); |
|
var intersects = []; |
|
d3.range(2, x.invert(width), 2).forEach(function(a) { d3.range(0.2, reducedY.invert(-height),0.2).forEach(function(b) { intersects.push([a,b])})}); |
|
grid2.selectAll(".intersect") |
|
.data(intersects) |
|
.enter().append("path") |
|
.classed("intersect", true) |
|
.attr("d", function(d) { return "M"+[x(d[0])-1,reducedY(d[1])]+"h3M"+[x(d[0]),reducedY(d[1])-1]+"v3"}); |
|
grid2.append("line") |
|
.classed("x-line", true) |
|
.attr("x1", x(0)) |
|
.attr("y1", reducedY(1)) |
|
.attr("x2", x(20)) |
|
.attr("y2", reducedY(1)); |
|
|
|
container2.append("g") |
|
.attr("class", "axis x") |
|
.call(xAxisDef); |
|
container2.append("text") |
|
.attr("x", width) |
|
.attr("y", -6) |
|
.style("text-anchor", "end") |
|
.text("Time"); |
|
|
|
container2.append("g") |
|
.attr("class", "axis y") |
|
.call(reducedYAxisDef); |
|
container2.append("text") |
|
.attr("transform", "rotate(-90)") |
|
.attr("x", height) |
|
.attr("y", 16) |
|
.style("text-anchor", "end") |
|
.text("Relative Amount"); |
|
container2.append("text") |
|
.attr("transform", "rotate(-90)") |
|
.attr("x", height) |
|
.attr("y", 30) |
|
.style("text-anchor", "end") |
|
.style("font-size", "12px") |
|
.text("(Amount / Mean)"); |
|
|
|
container2.append("text") |
|
.attr("style", "font-size:12px") |
|
.attr("x", x(20)) |
|
.attr("y", reducedY(1)-3) |
|
.style("text-anchor", "end") |
|
.text("All relative series have"); |
|
container2.append("text") |
|
.attr("style", "font-size:12px") |
|
.attr("x", x(20)) |
|
.attr("y", reducedY(1)+9) |
|
.style("text-anchor", "end") |
|
.text("the same mean"); |
|
|
|
var reducedTimeline1 = container2.append("path") |
|
.classed("timeline serie1 reduced draggable", true) |
|
.attr("d", reducedLine1) |
|
.call(dragTimeline1); |
|
var reducedTimeline2 = container2.append("path") |
|
.classed("timeline serie2 reduced draggable", true) |
|
.attr("d", reducedLine2) |
|
.call(dragTimeline2); |
|
|
|
var reducedDotContainer = container2.append("g") |
|
.classed("dots reduced", true); |
|
|
|
var reducedTrendLine1 = container2.append("line") |
|
.attr("class", "trend serie1 reduced") |
|
.attr("x1", x(0)) |
|
.attr("y1", y(0)) |
|
.attr("x2", x(20)) |
|
.attr("y2", y(0)); |
|
var reducedTrendLine2 = container2.append("line") |
|
.attr("class", "trend serie2 reduced") |
|
.attr("x1", x(0)) |
|
.attr("y1", y(0)) |
|
.attr("x2", x(20)) |
|
.attr("y2", y(0)); |
|
|
|
d3.csv("timeserie.csv", dottype, function(error, dots) { |
|
updateMeans(); |
|
updateTimelines(WITHOUT_TRANSITION); |
|
updateDots(WITHOUT_TRANSITION); |
|
updateTrends(WITHOUT_TRANSITION); |
|
}); |
|
|
|
function dottype(d) { |
|
d.x = +d.x; |
|
d.y1 = +d.y+d.x/2-10; |
|
d.y2 = +d.y1/2-2; |
|
timeSerie.push(d); |
|
return d; |
|
} |
|
|
|
var line1 = d3.svg.line() |
|
.x(function(d) { return x(d.x); }) |
|
.y(function(d) { return y(d.y1); }); |
|
var line2 = d3.svg.line() |
|
.x(function(d) { return x(d.x); }) |
|
.y(function(d) { return y(d.y2); }); |
|
|
|
var reducedLine1 = d3.svg.line() |
|
.x(function(d) { return x(d.x); }) |
|
.y(function(d) { return reducedY(d.y1/mean1); }); |
|
var reducedLine2 = d3.svg.line() |
|
.x(function(d) { return x(d.x); }) |
|
.y(function(d) { return reducedY(d.y2/mean2); }); |
|
|
|
function makeSameTrend() { |
|
var meanDelta = mean1-mean2; |
|
timeSerie.forEach(function(d){ |
|
if (sameTrend) { |
|
d.y2 = +d.y1/2-2; |
|
} else { |
|
d.y2 = d.y1-meanDelta; |
|
} |
|
}); |
|
mean2 = sameTrend? mean1/2-2 : mean1-meanDelta; |
|
sameTrend = !sameTrend |
|
updateDots(WITH_TRANSITION); |
|
updateTimelines(WITH_TRANSITION); |
|
updateTrends(WITH_TRANSITION); |
|
} |
|
|
|
function updateMeans() { |
|
var serieLength = timeSerie.length; |
|
var countSum1 = 0; |
|
var countSum2 = 0; |
|
|
|
timeSerie.forEach(function(d){ |
|
countSum1 += d.y1; |
|
countSum2 += d.y2; |
|
}); |
|
|
|
mean1 = countSum1/serieLength; |
|
mean2 = countSum2/serieLength; |
|
} |
|
|
|
function updateDots(withTransition) { |
|
var dots = dotContainer.selectAll(".dot.serie1") |
|
.data(timeSerie); |
|
dots.enter() |
|
.append("circle") |
|
.classed("dot draggable serie1", true) |
|
.attr("r", 5) |
|
.attr("cx", function(d) { return x(d.x); }) |
|
.attr("cy", function(d) { return y(d.y1); }) |
|
.call(drag1); |
|
dots.transition() |
|
.duration(withTransition? duration : 0) |
|
.attr("cy", function(d) { return y(d.y1); }) |
|
|
|
dots = dotContainer.selectAll(".dot.serie2") |
|
.data(timeSerie); |
|
dots.enter() |
|
.append("circle") |
|
.classed("dot draggable serie2", true) |
|
.attr("r", 5) |
|
.attr("cx", function(d) { return x(d.x); }) |
|
.attr("cy", function(d) { return y(d.y2); }) |
|
.call(drag2); |
|
dots.transition() |
|
.duration(withTransition? duration : 0) |
|
.attr("cy", function(d) { return y(d.y2); }) |
|
|
|
var reducedDots = reducedDotContainer.selectAll(".dot.serie1.reduced") |
|
.data(timeSerie); |
|
reducedDots.enter() |
|
.append("circle") |
|
.classed("dot serie1 reduced", true) |
|
.attr("r", 5) |
|
.attr("cx", function(d) { return x(d.x); }) |
|
.attr("cy", function(d) { return reducedY(d.y1/mean1); }); |
|
reducedDots.transition() |
|
.duration(withTransition? duration : 0) |
|
.attr("cy", function(d) { return reducedY(d.y1/mean1); }) |
|
|
|
reducedDots = reducedDotContainer.selectAll(".dot.serie2.reduced") |
|
.data(timeSerie); |
|
reducedDots.enter() |
|
.append("circle") |
|
.classed("dot serie2 reduced", true) |
|
.attr("r", 5) |
|
.attr("cx", function(d) { return x(d.x); }) |
|
.attr("cy", function(d) { return reducedY(d.y2/mean2); }); |
|
reducedDots.transition() |
|
.duration(withTransition? duration : 0) |
|
.attr("cy", function(d) { return reducedY(d.y2/mean2); }) |
|
} |
|
|
|
function updateTimelines(withTransition) { |
|
timeline1.data(timeSerie).transition() |
|
.duration(withTransition? duration : 0) |
|
.attr("d", line1(timeSerie)); |
|
timeline2.data(timeSerie).transition() |
|
.duration(withTransition? duration : 0) |
|
.attr("d", line2(timeSerie)); |
|
reducedTimeline1.data(timeSerie).transition() |
|
.duration(withTransition? duration : 0) |
|
.attr("d", reducedLine1(timeSerie)); |
|
reducedTimeline2.data(timeSerie).transition() |
|
.duration(withTransition? duration : 0) |
|
.attr("d", reducedLine2(timeSerie)); |
|
|
|
} |
|
|
|
function updateTrends(withTransition) { |
|
var serieLength = timeSerie.length; |
|
var timeInterval = 1 |
|
var countSum1 = 0; |
|
var orderCountSum1 = 0; |
|
var countSum2 = 0; |
|
var orderCountSum2 = 0; |
|
|
|
timeSerie.forEach(function(d){ |
|
countSum1 += d.y1; |
|
orderCountSum1 += (d.x)*(d.y1); |
|
countSum2 += d.y2; |
|
orderCountSum2 += (d.x)*(d.y2); |
|
}); |
|
|
|
var a1 = (12*orderCountSum1 - 6*(serieLength+1)*countSum1)/(timeInterval*serieLength*(serieLength-1)*(serieLength+1)); |
|
var b1 = (2*(2*serieLength+1)*countSum1 - 6*orderCountSum1)/(serieLength*(serieLength-1)); |
|
var a2 = (12*orderCountSum2 - 6*(serieLength+1)*countSum2)/(timeInterval*serieLength*(serieLength-1)*(serieLength+1)); |
|
var b2 = (2*(2*serieLength+1)*countSum2 - 6*orderCountSum2)/(serieLength*(serieLength-1)); |
|
|
|
trendLine1 |
|
.transition() |
|
.duration(withTransition? duration : 0) |
|
.attr("y1", y(b1)) |
|
.attr("y2", y(a1*serieLength+b1)); |
|
trendLine2 |
|
.transition() |
|
.duration(withTransition? duration : 0) |
|
.attr("y1", y(b2)) |
|
.attr("y2", y(a2*serieLength+b2)); |
|
reducedTrendLine1 |
|
.transition() |
|
.duration(withTransition? duration : 0) |
|
.attr("y1", reducedY(b1/mean1)) |
|
.attr("y2", reducedY((a1*serieLength+b1)/mean1)); |
|
reducedTrendLine2 |
|
.transition() |
|
.duration(withTransition? duration : 0) |
|
.attr("y1", reducedY(b2/mean2)) |
|
.attr("y2", reducedY((a2*serieLength+b2)/mean2)); |
|
} |
|
|
|
function dragStarted(d) { |
|
d3.select(this).classed("dragging", true); |
|
} |
|
|
|
function dragged1(d) { |
|
d.y1 += y.invert(d3.event.dy) |
|
d3.select(this).attr("cy", y(d.y1)); |
|
updateMeans(WITHOUT_TRANSITION); |
|
updateDots(WITHOUT_TRANSITION); |
|
updateTimelines(WITHOUT_TRANSITION); |
|
updateTrends(WITHOUT_TRANSITION); |
|
} |
|
|
|
function dragged2(d) { |
|
d.y2 += y.invert(d3.event.dy) |
|
d3.select(this).attr("cy", y(d.y2)); |
|
updateMeans(WITHOUT_TRANSITION); |
|
updateDots(WITHOUT_TRANSITION); |
|
updateTimelines(WITHOUT_TRANSITION); |
|
updateTrends(WITHOUT_TRANSITION); |
|
} |
|
|
|
function dragEnded(d) { |
|
d3.select(this).classed("dragging", false); |
|
} |
|
|
|
function draggedTimeline1(d) { |
|
var rawdy = y.invert(d3.event.dy); |
|
timeSerie.forEach(function(d){ |
|
d.y1 += rawdy; |
|
}); |
|
mean1 += rawdy; |
|
updateDots(WITHOUT_TRANSITION); |
|
updateTimelines(WITHOUT_TRANSITION); |
|
updateTrends(WITHOUT_TRANSITION); |
|
} |
|
|
|
function draggedTimeline2(d) { |
|
var rawdy = y.invert(d3.event.dy); |
|
timeSerie.forEach(function(d){ |
|
d.y2 += rawdy; |
|
}); |
|
mean2 += rawdy; |
|
updateDots(WITHOUT_TRANSITION); |
|
updateTimelines(WITHOUT_TRANSITION); |
|
updateTrends(WITHOUT_TRANSITION); |
|
} |
|
|
|
</script> |