A minor addition to the cannonical brush and zoom using an area chart.
Focus should be on the graph as it is sunk in an iframe, otherwise the event listener may not fire.
xxxxxxxxxx
<meta charset="utf-8">
<style>
.area {
fill: steelblue;
clip-path: url(#clip);
}
.zoom {
cursor: move;
fill: none;
pointer-events: all;
}
</style>
<svg width="960" height="500"></svg>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script>
var svg = d3.select("svg"),
margin = {top: 20, right: 20, bottom: 110, left: 40},
margin2 = {top: 430, right: 20, bottom: 30, left: 40},
width = +svg.attr("width") - margin.left - margin.right,
height = +svg.attr("height") - margin.top - margin.bottom,
height2 = +svg.attr("height") - margin2.top - margin2.bottom;
var parseDate = d3.timeParse("%b %Y");
var x = d3.scaleTime().range([0, width]),
x2 = d3.scaleTime().range([0, width]),
y = d3.scaleLinear().range([height, 0]),
y2 = d3.scaleLinear().range([height2, 0]);
var xAxis = d3.axisBottom(x),
xAxis2 = d3.axisBottom(x2),
yAxis = d3.axisLeft(y);
var brush = d3.brushX()
.extent([[0, 0], [width, height2]])
.on("brush end", brushed);
var zoom = d3.zoom()
.scaleExtent([1, Infinity])
.translateExtent([[0, 0], [width, height]])
.extent([[0, 0], [width, height]])
.on("zoom", zoomed);
var area = d3.area()
.curve(d3.curveMonotoneX)
.x(function(d) { return x(d.date); })
.y0(height)
.y1(function(d) { return y(d.price); });
var area2 = d3.area()
.curve(d3.curveMonotoneX)
.x(function(d) { return x2(d.date); })
.y0(height2)
.y1(function(d) { return y2(d.price); });
svg.append("defs").append("clipPath")
.attr("id", "clip")
.append("rect")
.attr("width", width)
.attr("height", height);
var focus = svg.append("g")
.attr("class", "focus")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var context = svg.append("g")
.attr("class", "context")
.attr("transform", "translate(" + margin2.left + "," + margin2.top + ")");
var identity = d3.zoomIdentity;
d3.csv("sp500.csv", type, function(error, data) {
if (error) throw error;
x.domain(d3.extent(data, function(d) { return d.date; }));
y.domain([0, d3.max(data, function(d) { return d.price; })]);
x2.domain(x.domain());
y2.domain(y.domain());
focus.append("path")
.datum(data)
.attr("class", "area")
.attr("d", area);
focus.append("g")
.attr("class", "axis axis--x")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
focus.append("g")
.attr("class", "axis axis--y")
.call(yAxis);
context.append("path")
.datum(data)
.attr("class", "area")
.attr("d", area2);
context.append("g")
.attr("class", "axis axis--x")
.attr("transform", "translate(0," + height2 + ")")
.call(xAxis2);
context.append("g")
.attr("class", "brush")
.call(brush)
.call(brush.move, x.range());
svg.append("rect")
.attr("class", "zoom")
.attr("width", width)
.attr("height", height)
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
.call(zoom);
});
function brushed() {
if (d3.event.sourceEvent && d3.event.sourceEvent.type === "zoom") return; // ignore brush-by-zoom
var s = d3.event.selection || x2.range();
x.domain(s.map(x2.invert, x2));
focus.select(".area").attr("d", area);
focus.select(".axis--x").call(xAxis);
svg.select(".zoom").call(zoom.transform, d3.zoomIdentity
.scale(width / (s[1] - s[0]))
.translate(-s[0], 0));
}
function zoomed() {
if (d3.event.sourceEvent && d3.event.sourceEvent.type === "brush") return; // ignore zoom-by-brush
var t = d3.event.transform;
transform = t;
console.log(d3.event.transform);
x.domain(t.rescaleX(x2).domain());
focus.select(".area").attr("d", area);
focus.select(".axis--x").call(xAxis);
context.select(".brush").call(brush.move, x.range().map(t.invertX, t));
}
function type(d) {
d.date = parseDate(d.date);
d.price = +d.price;
return d;
}
d3.select("body")
.on("keypress", function() {
// zoom out
if (d3.event.key == "s") {
var x0 = x2(x.domain()[0]);
var x1 = x2(x.domain()[1]);
var currentDifference = Math.abs(x0-x1);
x0 -= currentDifference / 2 / 3
x1 += currentDifference / 2 / 3
updateBrushAndZoom(x0,x1);
}
// zoom in
else if (d3.event.key == "w") {
var x0 = x2(x.domain()[0]);
var x1 = x2(x.domain()[1]);
var currentDifference = Math.abs(x0-x1);
x0 += currentDifference / 2 / 3
x1 -= currentDifference / 2 / 3
updateBrushAndZoom(x0,x1);
}
// pan left:
else if (d3.event.key == "a") {
var x0 = x2(x.domain()[0]);
var x1 = x2(x.domain()[1]);
var currentDifference = Math.abs(x0-x1);
x0 -= currentDifference / 2 / 3
x1 -= currentDifference / 2 / 3
updateBrushAndZoom(x0,x1);
}
// pan right
else if (d3.event.key == "d") {
var x0 = x2(x.domain()[0]);
var x1 = x2(x.domain()[1]);
var currentDifference = Math.abs(x0-x1);
x0 += currentDifference / 2 / 3;
x1 += currentDifference / 2 / 3;
updateBrushAndZoom(x0,x1);
}
})
function updateBrushAndZoom(x0,x1) {
if (x0 < x2.range()[0]) x0 = x2.range()[0];
if (x1 > x2.range()[1]) x1 = x2.range()[1];
x.domain([x0,x1].map(x2.invert, x2));
focus.select(".area").attr("d", area);
focus.select(".axis--x").call(xAxis);
var identity = d3.zoomIdentity
.scale(width/ (x1 - x0))
.translate(-x0, 0);
context.select(".brush").call(brush.move, x.range().map(identity.invertX, identity));
svg.select(".zoom").call(zoom.transform, identity);
}
</script>
https://d3js.org/d3.v4.min.js