Skip to content

Instantly share code, notes, and snippets.

@tomshanley
Last active April 26, 2019 06:28
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save tomshanley/15a2b09a95ccaf338650e50fd207fcbf to your computer and use it in GitHub Desktop.
Save tomshanley/15a2b09a95ccaf338650e50fd207fcbf to your computer and use it in GitHub Desktop.
Line Transition with min/max call out
license: gpl-3.0

A fork of mbostock's block: Line Transition

I've added circles for each data point that transition across the chart at the same time as the line chart, and red/orange lines to call out the minimum and maximum values since the chart started, and for within the current view.

<!DOCTYPE html>
<meta charset="utf-8">
<style>
body {
font-family: 'Catamaran', sans-serif;
margin: 20px;
top: 20px;
right: 20px;
bottom: 20px;
left: 20px;
}
.domain,
.x-axis {
stroke: grey;
stroke-width: 1px;
shape-rendering: crispEdges;
}
line.major-tick {
stroke: grey;
}
text.major-tick {
fill: grey;
font-size: 10px;
}
.major-tick {
text-anchor: middle;
}
.minor-tick {
opacity: 0
}
.tick line {
stroke: grey;
}
.actual-line {
fill: none;
stroke: #000;
stroke-width: 3px;
}
.historical-line line {
fill: none;
stroke: red;
stroke-width: 1.5px;
stroke-dasharray: 2, 2;
}
.historical-line text {
fill: red;
}
.current-line line {
fill: none;
stroke: orange;
stroke-width: 1.5px;
stroke-dasharray: 2, 2;
}
.current-line text {
fill: orange;
}
circle {
stroke: white;
}
</style>
<svg width="960" height="500"></svg>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://d3js.org/d3-scale-chromatic.v0.3.min.js"></script>
<link href="https://fonts.googleapis.com/css?family=Catamaran" rel="stylesheet">
<script>
var n = 40;
var random = d3.randomNormal(0, .2);
let id = 0;
var durationLength = 200;
var data = [];
for (i = 0; i < n; i++) {
data[i] = newDatum();
};
var extent = d3.extent(data, function (d) { return d.value; });
var historicalMin = extent[0];
var historicalMax = extent[1];
var currentMin = extent[0];
var currentMax = extent[1];
var svg = d3.select("svg"),
margin = { top: 20, right: 20, bottom: 20, left: 40 },
width = +svg.attr("width") - margin.left - margin.right,
height = +svg.attr("height") - margin.top - margin.bottom,
g = svg.append("g").attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var x = d3.scaleLinear()
.domain([0, n - 1])
.range([0, width]);
var y = d3.scaleLinear()
.domain([-1, 1])
.range([height, 0]);
var line = d3.line()
.x(function (d, i) { return x(i); })
.y(function (d, i) { return y(d.value); });
g.append("line")
.attr("class", "x-axis")
.attr("x1", 0)
.attr("y1", y(0) + 0.5)
.attr("x2", width)
.attr("y2", y(0) + 0.5)
g.append("defs").append("clipPath")
.attr("id", "clip")
.append("rect")
.attr("width", width)
.attr("height", height);
let yAxis = g.append("g")
.attr("class", "axis axis--y")
.call(d3.axisLeft(y));
yAxis.selectAll("path, line")
.style("stroke", "grey")
.style("stroke-width", "1px")
yAxis.selectAll("text")
.style("font-family", "Catamaran")
let historicalMaxLine = g.append("g")
.attr("class", "historical-line")
.attr("id", "historical-max")
.attr("transform", "translate(0, " + y(historicalMax) + ")");
historicalMaxLine.append("line")
.attr("x2", width)
historicalMaxLine.append("text")
.text("Historical max: " + round(historicalMax))
.attr("x", width)
.attr("y", -3)
.style("text-anchor", "end")
let historicalMinLine = g.append("g")
.attr("class", "historical-line")
.attr("id", "historical-min")
.attr("transform", "translate(0, " + y(historicalMin) + ")");
historicalMinLine.append("line")
.attr("x2", width)
historicalMinLine.append("text")
.text("Historical min: " + round(historicalMin))
.attr("x", width)
.attr("y", 14)
.style("text-anchor", "end")
let currentMaxLine = g.append("g")
.attr("class", "current-line")
.attr("id", "current-max")
.attr("transform", "translate(0, " + y(currentMax) + ")");
currentMaxLine.append("line")
.attr("x2", width)
currentMaxLine.append("text")
.text("Current max: " + round(currentMax))
.attr("x", width)
.attr("y", 14)
.style("text-anchor", "end")
let currentMinLine = g.append("g")
.attr("class", "current-line")
.attr("id", "current-max")
.attr("transform", "translate(0, " + y(currentMin) + ")");
currentMinLine.append("line")
.attr("x2", width)
currentMinLine.append("text")
.text("Current min: " + round(currentMin))
.attr("x", width)
.attr("y", -3)
.style("text-anchor", "end")
g.append("line")
.attr("class", "current-line")
.attr("id", "current-min")
.attr("x1", 0)
.attr("y1", y(currentMin))
.attr("x2", width)
.attr("y2", y(currentMin));
g.append("g")
.attr("clip-path", "url(#clip)")
.append("path")
.datum(data)
.attr("class", "actual-line")
.transition()
.duration(durationLength)
.ease(d3.easeLinear)
.on("start", tick);
let circlesG = g.append("g").classed("circles", true)
.attr("clip-path", "url(#clip)")
let circles = circlesG.selectAll("circle")
.data(data)
.enter()
.append("g")
.attr("transform", function (d, i) { return "translate(" + x(i) + ",0)"; })
circles.transition()
.ease(d3.easeLinear)
.duration(function (d, i) { return durationLength * i; })
.attr("transform", "translate(0,0)")
.on("end", function (d) {
d3.select(this).remove()
});
circles.call(appendCircle);
circles.call(appendTick);
function tick() {
// Push a new data point onto the back.
var dataPoint = newDatum();
historicalMin = historicalMin > dataPoint.value ? dataPoint.value : historicalMin;
historicalMax = historicalMax < dataPoint.value ? dataPoint.value : historicalMax;
data.push(dataPoint);
let newExtent = d3.extent(data, function (d) { return d.value; });
currentMin = newExtent[0]
currentMax = newExtent[1]
// Redraw the line.
d3.select(this)
.attr("d", line)
.attr("transform", null);
var newCircle = circlesG.append("g")
.datum(dataPoint)
.attr("transform", "translate(" + x(n) + ",0)")
newCircle.transition()
.ease(d3.easeLinear)
.duration(durationLength * n)
.attr("transform", "translate(0,0)")
.on("end", function (d) {
d3.select(this).remove()
});
newCircle.call(appendCircle)
newCircle.call(appendTick)
circlesG.selectAll("circle").style("fill", function (d) {
if (d.value == currentMax) {
return "orange";
} else if (d.value == currentMin) {
return "orange";
} else {
return "black";
};
});
// Slide it to the left.
d3.active(this)
.attr("transform", "translate(" + x(-1) + ",0)")
.transition()
.on("start", tick);
//update min and max lines
historicalMinLine.transition()
.attr("transform", "translate(0, " + y(historicalMin) + ")");
historicalMinLine.select("text")
.text("Historical min: " + round(historicalMin))
historicalMaxLine.transition()
.attr("transform", "translate(0, " + y(historicalMax) + ")");
historicalMaxLine.select("text")
.text("Historical max: " + round(historicalMax))
currentMinLine.transition()
.attr("transform", "translate(0, " + y(currentMin) + ")");
currentMinLine.select("text")
.text("Current min: " + round(currentMin))
currentMaxLine.transition()
.attr("transform", "translate(0, " + y(currentMax) + ")");
currentMaxLine.select("text")
.text("Current max: " + round(currentMax))
// Pop the old data point off the front.
data.shift();
}
function newDatum() {
var nd = {};
nd.id = id;
id = id + 1;
nd.value = random()
return nd;
};
function round(n) {
return n.toFixed(2);
};
function appendTick(selection) {
selection.append("line")
.attr("class", function (d) {
return (d.id % 5) == 0 ? "major-tick" : "minor-tick"
})
.attr("y1", y(0))
.attr("y2", y(-0.02));
selection.append("text")
.attr("class", function (d) {
return (d.id % 5) == 0 ? "major-tick" : "minor-tick"
})
.attr("y", y(0) + 16)
.text(function (d) { return d.id; })
};
function appendCircle(selection) {
selection.append("circle")
.attr("cy", function(d) {return y(d.value)})
.attr("r", 5)
};
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment