Skip to content

Instantly share code, notes, and snippets.

@stepheneb
Last active January 14, 2020 16:25
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save stepheneb/1920939 to your computer and use it in GitHub Desktop.
Save stepheneb/1920939 to your computer and use it in GitHub Desktop.
D3 Example: zoom, pan, and axis rescale

This example displays two independent graphs and is an extension of the original example.

  • Drag on the canvas to translate/pan the graph.
  • double-click on the canvas to zoom in
  • shift-double-click on the canvas to zoom out
  • Drag on one of the X or Y axis numeric labels to re-scale that axis
  • click on a data point to select it
  • drag a selected data point up or down to change it's Y value
  • enter the delete or backspace key to delete a selected data point
  • hold the ALT/Option key down and click an empty area of the graph to add a data point

Most of the UI should also work with the touch events generated by a tablet or SmartPhone.

source: gist.github.com/1920939

D3 References:

D3 Tutorials:

External D3 Tutorials

SVG Graphics

<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8">
<title>Two Graphs</title>
<script type="text/javascript" src="http://mbostock.github.com/d3/d3.v2.js"></script>
<script type="text/javascript" src="simple-graph.js"></script>
<style type="text/css">
body { font: 13px sans-serif; }
rect { fill: #fff; }
ul {
list-style-type: none;
margin: 0.5em 0em 0.5em 0em;
padding: 0px; }
ul li {
display: table-cell;
vertical-align: middle;
margin: 0em;
padding-right: 1em; }
.axis {
font-size: 1.3em; }
.chart {
background-color: #F7F2C5;
width: 475px;
height: 500px; }
circle, .line {
fill: none;
stroke: steelblue;
stroke-width: 2px; }
circle {
fill: white;
fill-opacity: 0.2;
cursor: move; }
circle.selected {
fill: #ff7f0e;
stroke: #ff7f0e; }
circle:hover {
fill: #ff7f0e;
stroke: #707f0e; }
circle.selected:hover {
fill: #ff7f0e;
stroke: #ff7f0e; }
</style>
</head>
<body>
<ul>
<li><div id="chart1" class="chart"></div></li>
<li><div id="chart2" class="chart"></div></li>
</ul>
<script type="text/javascript">
var graph1 = simpleGraph("#chart1");
var graph2 = simpleGraph().title("Simple Graph2").xmax(20).ymax(10);
d3.select("#chart2").call(graph2);
</script>
</body>
</html>
registerKeyboardHandler = function(callback) {
d3.select(window).on("keydown", callback);
};
function simpleGraph(elem, options) {
var cx = 600, cy = 300;
if (arguments.length) {
elem = d3.select(elem);
cx = elem.property("clientWidth");
cy = elem.property("clientHeight");
}
var vis, plot, title, xlabel, ylabel, points, xtic, ytic,
options = options || {
"xmax": 60, "xmin": 0,
"ymax": 40, "ymin": 0,
"title": "Simple Graph1",
"xlabel": "X Axis",
"ylabel": "Y Axis"
},
padding = {
"top": options.title ? 40 : 20,
"right": 30,
"bottom": options.xlabel ? 60 : 10,
"left": options.ylabel ? 70 : 45
},
size = {
"width": cx - padding.left - padding.right,
"height": cy - padding.top - padding.bottom
},
xValue = function(d) { return d[0]; },
yValue = function(d) { return d[1]; },
xScale = d3.scale.linear()
.domain([options.xmin, options.xmax])
.range([0, size.width]),
downx = Math.NaN,
yScale = d3.scale.linear()
.domain([options.ymax, options.ymin]).nice()
.range([0, size.height]).nice(),
downy = Math.NaN,
dragged = null,
selected = null,
line = d3.svg.line()
.x(function(d, i) { return xScale(points[i].x); })
.y(function(d, i) { return yScale(points[i].y); });
options.xrange = options.xmax - options.xmin;
options.yrange = options.ymax - options.ymin;
function graph(selection) {
if (!selection) { selection = elem; };
selection.each(function() {
if (this.clientWidth && this.clientHeight) {
cx = this.clientWidth;
cy = this.clientHeight;
size.width = cx - padding.left - padding.right;
size.height = cy - padding.top - padding.bottom;
}
fakeDataPoints();
updateXScale();
updateYScale();
vis = d3.select(this).append("svg")
.attr("width", cx)
.attr("height", cy)
.append("g")
.attr("transform", "translate(" + padding.left + "," + padding.top + ")");
plot = vis.append("rect")
.attr("width", size.width)
.attr("height", size.height)
.style("fill", "#EEEEEE")
.attr("pointer-events", "all")
.on("mousedown.drag", plot_drag)
.on("touchstart.drag", plot_drag)
.call(d3.behavior.zoom().x(xScale).y(yScale).on("zoom", redraw));
vis.append("svg")
.attr("top", 0)
.attr("left", 0)
.attr("width", size.width)
.attr("height", size.height)
.attr("viewBox", "0 0 "+size.width+" "+size.height)
.attr("class", "line")
.append("path")
.attr("class", "line")
.attr("d", line(points));
// add Chart Title
if (options.title) {
title = vis.append("text")
.attr("class", "title")
.text(options.title)
.attr("x", size.width/2)
.attr("dy","-0.8em")
.style("text-anchor","middle");
}
// Add the x-axis label
if (options.xlabel) {
xlabel = vis.append("text")
.attr("class", "axis")
.text(options.xlabel)
.attr("x", size.width/2)
.attr("y", size.height)
.attr("dy","2.4em")
.style("text-anchor","middle");
}
// add y-axis label
if (options.ylabel) {
ylabel = vis.append("g").append("text")
.attr("class", "axis")
.text(options.ylabel)
.style("text-anchor","middle")
.attr("transform","translate(" + -40 + " " + size.height/2+") rotate(-90)");
}
d3.select(this)
.on("mousemove.drag", mousemove)
.on("touchmove.drag", mousemove)
.on("mouseup.drag", mouseup)
.on("touchend.drag", mouseup);
redraw();
});
function fakeDataPoints() {
var yrange2 = options.yrange / 2,
yrange4 = yrange2 / 2;
options.datacount = size.width/30;
options.xtic = options.xrange / options.datacount;
options.ytic = options.yrange / options.datacount;
points = d3.range(options.datacount).map(function(i) {
return { x: i * options.xtic + options.xmin, y: options.ymin + yrange4 + Math.random() * yrange2 };
})
}
function keydown() {
if (!selected) return;
switch (d3.event.keyCode) {
case 8: // backspace
case 46: // delete
var i = points.indexOf(selected);
points.splice(i, 1);
selected = points.length ? points[i > 0 ? i - 1 : 0] : null;
update();
break;
}
}
// update the layout
function updateLayout() {
padding = {
"top": options.title ? 40 : 20,
"right": 30,
"bottom": options.xlabel ? 60 : 10,
"left": options.ylabel ? 70 : 45
};
size.width = cx - padding.left - padding.right;
size.height = cy - padding.top - padding.bottom;
plot.attr("width", size.width)
.attr("height", size.height);
}
// Update the x-scale.
function updateXScale() {
xScale.domain([options.xmin, options.xmax])
.range([0, size.width]);
}
// Update the y-scale.
function updateYScale() {
yScale.domain([options.ymin, options.ymax])
.range([size.height, 0]);
}
function redraw() {
var tx = function(d) {
return "translate(" + xScale(d) + ",0)";
},
ty = function(d) {
return "translate(0," + yScale(d) + ")";
},
stroke = function(d) {
return d ? "#ccc" : "#666";
},
fx = xScale.tickFormat(options.datacount),
fy = xScale.tickFormat(options.datacount);
// Regenerate x-ticks…
var gx = vis.selectAll("g.x")
.data(xScale.ticks(10), String)
.attr("transform", tx);
gx.select("text")
.text(fx);
var gxe = gx.enter().insert("g", "a")
.attr("class", "x")
.attr("transform", tx);
gxe.append("line")
.attr("stroke", stroke)
.attr("y1", 0)
.attr("y2", size.height);
gxe.append("text")
.attr("class", "axis")
.attr("y", size.height)
.attr("dy", "1em")
.attr("text-anchor", "middle")
.text(fx)
.style("cursor", "ew-resize")
.on("mouseover", function(d) { d3.select(this).style("font-weight", "bold");})
.on("mouseout", function(d) { d3.select(this).style("font-weight", "normal");})
.on("mousedown.drag", xaxis_drag)
.on("touchstart.drag", xaxis_drag);
gx.exit().remove();
// Regenerate y-ticks…
var gy = vis.selectAll("g.y")
.data(yScale.ticks(10), String)
.attr("transform", ty);
gy.select("text")
.text(fy);
var gye = gy.enter().insert("g", "a")
.attr("class", "y")
.attr("transform", ty)
.attr("background-fill", "#FFEEB6");
gye.append("line")
.attr("stroke", stroke)
.attr("x1", 0)
.attr("x2", size.width);
gye.append("text")
.attr("class", "axis")
.attr("x", -3)
.attr("dy", ".35em")
.attr("text-anchor", "end")
.text(fy)
.style("cursor", "ns-resize")
.on("mouseover", function(d) { d3.select(this).style("font-weight", "bold");})
.on("mouseout", function(d) { d3.select(this).style("font-weight", "normal");})
.on("mousedown.drag", yaxis_drag)
.on("touchstart.drag", yaxis_drag);
gy.exit().remove();
plot.call(d3.behavior.zoom().x(xScale).y(yScale).on("zoom", redraw));
update();
}
function update() {
var lines = vis.select("path").attr("d", line(points));
var circle = vis.select("svg").selectAll("circle")
.data(points, function(d) { return d; });
circle.enter().append("circle")
.attr("class", function(d) { return d === selected ? "selected" : null; })
.attr("cx", function(d) { return xScale(d.x); })
.attr("cy", function(d) { return yScale(d.y); })
.attr("r", 10.0)
.style("cursor", "ns-resize")
.on("mousedown.drag", datapoint_drag)
.on("touchstart.drag", datapoint_drag);
circle
.attr("class", function(d) { return d === selected ? "selected" : null; })
.attr("cx", function(d) { return xScale(d.x); })
.attr("cy", function(d) { return yScale(d.y); });
circle.exit().remove();
if (d3.event && d3.event.keyCode) {
d3.event.preventDefault();
d3.event.stopPropagation();
}
}
function plot_drag() {
registerKeyboardHandler(keydown);
d3.select('body').style("cursor", "move");
if (d3.event.altKey) {
var p = d3.svg.mouse(vis.node());
var newpoint = {};
newpoint.x = xScale.invert(Math.max(0, Math.min(size.width, p[0])));
newpoint.y = yScale.invert(Math.max(0, Math.min(size.height, p[1])));
points.push(newpoint);
points.sort(function(a, b) {
if (a.x < b.x) { return -1; }
if (a.x > b.x) { return 1; }
return 0;
});
selected = newpoint;
update();
d3.event.preventDefault();
d3.event.stopPropagation();
}
}
function xaxis_drag(d) {
document.onselectstart = function() { return false; };
var p = d3.svg.mouse(vis[0][0]);
downx = xScale.invert(p[0]);
}
function yaxis_drag(d) {
document.onselectstart = function() { return false; };
var p = d3.svg.mouse(vis[0][0]);
downy = yScale.invert(p[1]);
}
function datapoint_drag(d) {
registerKeyboardHandler(keydown);
document.onselectstart = function() { return false; };
selected = dragged = d;
update();
}
function mousemove() {
var p = d3.svg.mouse(vis[0][0]),
changex, changey, new_domain,
t = d3.event.changedTouches;
if (dragged) {
dragged.y = yScale.invert(Math.max(0, Math.min(size.height, p[1])));
update();
}
if (!isNaN(downx)) {
d3.select('body').style("cursor", "ew-resize");
var rupx = xScale.invert(p[0]),
xaxis1 = xScale.domain()[0],
xaxis2 = xScale.domain()[1],
xextent = xaxis2 - xaxis1;
if (rupx !== 0) {
changex = downx / rupx;
new_domain = [xaxis1, xaxis1 + (xextent * changex)];
xScale.domain(new_domain);
redraw();
}
d3.event.preventDefault();
d3.event.stopPropagation();
}
if (!isNaN(downy)) {
d3.select('body').style("cursor", "ns-resize");
var rupy = yScale.invert(p[1]),
yaxis1 = yScale.domain()[1],
yaxis2 = yScale.domain()[0],
yextent = yaxis2 - yaxis1;
if (rupy !== 0) {
changey = downy / rupy;
new_domain = [yaxis2, yaxis2 - yextent * changey];
yScale.domain(new_domain);
redraw();
}
d3.event.preventDefault();
d3.event.stopPropagation();
}
}
function mouseup() {
document.onselectstart = function() { return true; };
d3.select('body').style("cursor", "auto");
d3.select('body').style("cursor", "auto");
if (!isNaN(downx)) {
redraw();
downx = Math.NaN;
d3.event.preventDefault();
d3.event.stopPropagation();
}
if (!isNaN(downy)) {
redraw();
downy = Math.NaN;
d3.event.preventDefault();
d3.event.stopPropagation();
}
if (dragged) {
dragged = null;
}
}
// make these private variables and functions available
graph.elem = elem;
graph.redraw = redraw;
graph.updateXScale = updateXScale;
graph.updateYScale = updateYScale;
}
// update the title
function updateTitle() {
if (options.title && title) {
title.text(options.title);
}
}
// update the x-axis label
function updateXlabel() {
if (options.xlabel && xlabel) {
xlabel.text(options.xlabel);
}
}
// update the y-axis label
function updateYlabel() {
if (options.ylabel && ylabel) {
ylabel.text(options.ylabel);
} else {
ylabel.style("display", "none");
}
}
// The x-accessor for the path generator; xScale ∘ xValue.
function X(d) {
return xScale(d[0]);
}
// The x-accessor for the path generator; yScale ∘ yValue.
function Y(d) {
return yScale(d[1]);
}
function gRedraw() {
redraw();
}
graph.options = function(_) {
if (!arguments.length) return options;
// options = _;
return graph;
};
graph.margin = function(_) {
if (!arguments.length) return margin;
margin = _;
return graph;
};
graph.xmin = function(_) {
if (!arguments.length) return options.xmin;
options.xmin = _;
options.xrange = options.xmax - options.xmin;
if (graph.updateXScale) {
graph.updateXScale();
graph.redraw();
}
return graph;
};
graph.xmax = function(_) {
if (!arguments.length) return options.xmax;
options.xmax = _;
options.xrange = options.xmax - options.xmin;
if (graph.updateXScale) {
graph.updateXScale();
graph.redraw();
}
return graph;
};
graph.ymin = function(_) {
if (!arguments.length) return options.ymin;
options.ymin = _;
options.yrange = options.ymax - options.ymin;
if (graph.updateYScale) {
graph.updateYScale();
graph.redraw();
}
return graph;
};
graph.ymax = function(_) {
if (!arguments.length) return options.ymax;
options.ymax = _;
options.yrange = options.ymax - options.ymin;
if (graph.updateYScale) {
graph.updateYScale();
graph.redraw();
}
return graph;
};
graph.xLabel = function(_) {
if (!arguments.length) return options.xlabel;
options.xlabel = _;
updateXlabel();
return graph;
};
graph.yLabel = function(_) {
if (!arguments.length) return options.ylabel;
options.ylabel = _;
updateYlabel();
return graph;
};
graph.title = function(_) {
if (!arguments.length) return options.title;
options.title = _;
updateTitle();
return graph;
};
graph.width = function(_) {
if (!arguments.length) return size.width;
size.width = _;
return graph;
};
graph.height = function(_) {
if (!arguments.length) return size.height;
size.height = _;
return graph;
};
graph.x = function(_) {
if (!arguments.length) return xValue;
xValue = _;
return graph;
};
graph.y = function(_) {
if (!arguments.length) return yValue;
yValue = _;
return graph;
};
graph.elem = function(_) {
if (!arguments.length) return elem;
elem = d3.select(_);
elem.call(graph);
return graph;
};
if (elem) { elem.call(graph); }
return graph;
}
@mercicle
Copy link

Hi stumbled upon your code and was wondering if you know an answer to this question? http://stackoverflow.com/questions/17768226/position-of-an-element-based-on-the-current-d3-event-scale
Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment