Skip to content

Instantly share code, notes, and snippets.

@bewest
Forked from stepheneb/index.html
Created September 2, 2011 02:30
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bewest/1187808 to your computer and use it in GitHub Desktop.
Save bewest/1187808 to your computer and use it in GitHub Desktop.
D3 Example: spline, zoom and pan

This example is a mashup of the D3 Spline and Zoom-Pan examples along with Ricardo Marimon's example of X-axis re-scaling by dragging.

  • 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 to change it's value
  • enter the delete or backspace key to delete a selected data point
  • drag a selected data point to change it's value
  • hold the ALT/Option key down and click an empty area of the graph to add a data point

source: gist.github.com/1182434

There's still a bug where if you scale an axis once, you can then translate/pan around but if you scale the same axis a second time when you next try and translate/pan the result of the first axis re-scale will re-appear. This bug also appears if you re-scale the axes and then add a new data point.

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>Spline + Pan</title>
<script type="text/javascript" src="http://mbostock.github.com/d3/d3.js"></script>
<style type="text/css">
body {
font: 13px sans-serif;
}
rect {
fill: #fff;
}
#chart {
background-color: #F7F2C5;
width: 960px;
height: 500px;
}
circle, .line {
fill: none;
stroke: steelblue;
stroke-width: 1.5px;
}
circle {
fill: #fff;
fill-opacity: .2;
cursor: move;
}
circle.selected {
fill: #ff7f0e;
stroke: #ff7f0e;
}
</style>
</head>
<body>
<div id="chart"></div>
<label for="interpolate">Interpolate:</label>
<select id="interpolate"></select>
<script type="text/javascript">
var size = [890, 450],
padding = [20, 30, 20, 40], // top right bottom left
mw = size[0] - padding[1] - padding[3],
mh = size[1] - padding[0] - padding[2],
tx = function(d) { return "translate(" + x(d) + ",0)"; },
ty = function(d) { return "translate(0," + y(d) + ")"; },
stroke = function(d) { return d ? "#ccc" : "#666"; },
points = d3.range(1, 10).map(function(i) {
return {x: i * size[0] / 5, y: 50 + Math.random() * (size[1] - 100)};
}),
// x-scale
x = d3.scale.linear()
.domain([d3.min(points, function(d) { return d.x; }), d3.max(points, function(d) { return d.x; })])
.nice()
.range([0, mw])
.nice(),
// drag x-axis logic
downscalex = x.copy(),
downx = Math.NaN,
// y-scale (inverted domain)
y = d3.scale.linear()
.domain([d3.max(points, function(d) { return d.y; }), d3.min(points, function(d) { return d.y; })])
.nice()
.range([0, mh])
.nice(),
line = d3.svg.line()
.x(function(d, i) { return x(points[i].x); })
.y(function(d, i) { return y(points[i].y); }),
// drag x-axis logic
downscaley = y.copy(),
downy = Math.NaN,
dragged = null,
selected = points[0];
var vis = d3.select("#chart").append("svg:svg")
.attr("width", size[0] + padding[3] + padding[1])
.attr("height", size[1] + padding[0] + padding[2])
// .style("background-fill", "#FFEEB6")
.append("svg:g")
.attr("transform", "translate(" + padding[3] + "," + padding[0] + ")");
var graph = vis.append("svg:rect")
.attr("width", size[0])
.attr("height", size[1])
// .attr("stroke", "none")
.style("fill", "#EEEEEE")
.attr("pointer-events", "all")
.call(d3.behavior.zoom().on("zoom", redraw))
.on("mousedown", function() {
if (d3.event.altKey) {
points.push(selected = dragged = d3.svg.mouse(vis.node()));
update();
d3.event.preventDefault();
d3.event.stopPropagation();
}
});
vis.append("svg:path")
.attr("class", "line")
.attr("d", line(points));
d3.select(window)
.on("mousemove", mousemove)
.on("mouseup", mouseup)
.on("keydown", keydown);
// Add interpolator dropdown
d3.select("#interpolate")
.on("change", function() {
line.interpolate(this.value);
update();
})
.selectAll("option")
.data([
"linear",
"step-before",
"step-after",
"basis",
"basis-open",
"basis-closed",
"cardinal",
"cardinal-open",
"cardinal-closed",
"monotone"
])
.enter().append("option")
.attr("value", String)
.text(String);
function update() {
var lines = vis.select("path").attr("d", line(points));
var circle = vis.selectAll("circle")
.data(points, function(d) { return d; });
circle.enter().append("svg:circle")
.attr("class", function(d) { return d === selected ? "selected" : null; })
.attr("cx", function(d) { return x(d.x); })
.attr("cy", function(d) { return y(d.y); })
.attr("r", 4.0)
.on("mousedown", function(d) {
selected = dragged = d;
update();
})
.transition()
.duration(300)
.ease("elastic")
.attr("r", 8.0);
circle
.attr("class", function(d) { return d === selected ? "selected" : null; })
.attr("cx", function(d) { return x(d.x); })
.attr("cy", function(d) { return y(d.y); });
circle.exit().remove();
if (d3.event && d3.event.keyCode) {
d3.event.preventDefault();
d3.event.stopPropagation();
}
}
function mousemove() {
if (!dragged) return;
var m = d3.svg.mouse(vis.node());
dragged.x = x.invert(Math.max(0, Math.min(size[0], m[0])));
dragged.y = y.invert(Math.max(0, Math.min(size[1], m[1])));
update();
}
function mouseup() {
if (!dragged) return;
mousemove();
dragged = null;
}
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;
}
}
}
redraw();
function redraw() {
if (d3.event && d3.event.transform && isNaN(downx) && isNaN(downy)) {
d3.event.transform(x, y);
};
var fx = x.tickFormat(10),
fy = y.tickFormat(10);
// Regenerate x-ticks…
var gx = vis.selectAll("g.x")
.data(x.ticks(10), String)
.attr("transform", tx);
gx.select("text")
.text(fx);
var gxe = gx.enter().insert("svg:g", "a")
.attr("class", "x")
.attr("transform", tx);
gxe.append("svg:line")
.attr("stroke", stroke)
.attr("y1", 0)
.attr("y2", size[1]);
gxe.append("svg:text")
.attr("y", size[1])
.attr("dy", "1em")
.attr("text-anchor", "middle")
.text(fx)
.on("mouseover", function(d) { d3.select(this).style("font-weight", "bold");})
.on("mouseout", function(d) { d3.select(this).style("font-weight", "normal");})
.on("mousedown", function(d) {
var p = d3.svg.mouse(vis[0][0]);
downx = x.invert(p[0]);
downscalex = null;
downscalex = x.copy();
// d3.behavior.zoom().off("zoom", redraw);
});
gx.exit().remove();
// Regenerate y-ticks…
var gy = vis.selectAll("g.y")
.data(y.ticks(10), String)
.attr("transform", ty);
gy.select("text")
.text(fy);
var gye = gy.enter().insert("svg:g", "a")
.attr("class", "y")
.attr("transform", ty)
.attr("background-fill", "#FFEEB6");
gye.append("svg:line")
.attr("stroke", stroke)
.attr("x1", 0)
.attr("x2", size[0]);
gye.append("svg:text")
.attr("x", -3)
.attr("dy", ".35em")
.attr("text-anchor", "end")
.text(fy)
.on("mouseover", function(d) { d3.select(this).style("font-weight", "bold");})
.on("mouseout", function(d) { d3.select(this).style("font-weight", "normal");})
.on("mousedown", function(d) {
var p = d3.svg.mouse(vis[0][0]);
downy = y.invert(p[1]);
downscaley = y.copy();
// d3.behavior.zoom().off("zoom", redraw);
});
gy.exit().remove();
update();
}
// attach the mousemove and mouseup to the body
// in case one wonders off the axis line
d3.select('body')
.on("mousemove", function(d) {
var p = d3.svg.mouse(vis[0][0]);
if (!isNaN(downx)) {
var rupx = downscalex.invert(p[0]),
xaxis1 = downscalex.domain()[0],
xaxis2 = downscalex.domain()[1],
xextent = xaxis2 - xaxis1;
if (rupx != 0) {
var changex, new_domain;
changex = downx / rupx;
new_domain = [xaxis1, xaxis1 + (xextent * changex)];
x.domain(new_domain);
redraw();
}
d3.event.preventDefault();
d3.event.stopPropagation();
};
if (!isNaN(downy)) {
rupy = downscaley.invert(p[1]),
yaxis1 = downscaley.domain()[1],
yaxis2 = downscaley.domain()[0],
yextent = yaxis2 - yaxis1;
if (rupy != 0) {
var changey, new_domain;
changey = downy / rupy;
new_domain = [yaxis1 + (yextent * changey), yaxis1];
y.domain(new_domain);
redraw();
}
d3.event.preventDefault();
d3.event.stopPropagation();
}
})
.on("mouseup", function(d) {
if (!isNaN(downx)) {
redraw();
downx = Math.NaN;
d3.event.preventDefault();
d3.event.stopPropagation();
// graph.call(d3.behavior.zoom().on("zoom", redraw));
};
if (!isNaN(downy)) {
redraw();
downy = Math.NaN;
d3.event.preventDefault();
d3.event.stopPropagation();
// graph.call(d3.behavior.zoom().on("zoom", redraw));
};
// d3.event.preventDefault();
// d3.event.stopPropagation();
});
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment