Click to add new points. Hit the DELETE key to remove the selected point. Use the dropdown menu to change the interpolation mode.
Updated from mbostock's original gist to allow editing the coordinates in text fields.
<!DOCTYPE html> | |
<meta charset="utf-8"> | |
<title>Spline Editor</title> | |
<style> | |
body { | |
font: 13px sans-serif; | |
position: relative; | |
width: 960px; | |
height: 500px; | |
} | |
form { | |
position: absolute; | |
bottom: 10px; | |
left: 10px; | |
} | |
rect { | |
fill: none; | |
pointer-events: all; | |
} | |
circle, | |
.line { | |
fill: none; | |
stroke: steelblue; | |
stroke-width: 1.5px; | |
} | |
circle { | |
fill: #fff; | |
fill-opacity: .2; | |
cursor: move; | |
} | |
.selected { | |
fill: #ff7f0e; | |
stroke: #ff7f0e; | |
} | |
.coordField { | |
font-size: 1.4em; | |
border: dashed 2px #ccc; | |
padding-left: 1em; | |
padding-right: 1em; | |
padding-top: 0.3em; | |
padding-bottom: 0.3em; | |
} | |
li { | |
padding-top: 1em; | |
padding-bottom: 1em; | |
} | |
ul { | |
list-style-type: none; | |
} | |
#listContainer { | |
float: right; | |
} | |
</style> | |
<form> | |
<label for="interpolate">Interpolate:</label> | |
<select id="interpolate"></select><br> | |
</form> | |
<script src="http://d3js.org/d3.v3.min.js"></script> | |
<script> | |
var width = 480; | |
var height = 500; | |
var points = d3.range(1, 5).map(function(i) { | |
return [i * width / 5, 50 + Math.random() * (height - 100)]; | |
}); | |
var dragged = null, | |
selected = points[0]; | |
var line = d3.svg.line(); | |
var svg = d3.select("body").append("svg") | |
.attr("width", width) | |
.attr("height", height) | |
.attr("tabindex", 1); | |
svg.append("rect") | |
.attr("width", width) | |
.attr("height", height) | |
.on("mousedown", mousedown); | |
svg.append("path") | |
.datum(points) | |
.attr("class", "line") | |
.call(redraw); | |
d3.select(window) | |
.on("mousemove", mousemove) | |
.on("mouseup", mouseup) | |
.on("keydown", keydown); | |
d3.select("#interpolate") | |
.on("change", change) | |
.selectAll("option") | |
.data([ | |
"linear", | |
"step-before", | |
"step-after", | |
"basis", | |
"basis-open", | |
"basis-closed", | |
"cardinal", | |
"cardinal-open", | |
"cardinal-closed", | |
"monotone" | |
]) | |
.enter().append("option") | |
.attr("value", function(d) { return d; }) | |
.text(function(d) { return d; }); | |
svg.node().focus(); | |
function redraw() { | |
svg.select("path").attr("d", line); | |
var circle = svg.selectAll("circle") | |
.data(points, function(d) { return d; }); | |
circle.enter().append("circle") | |
.attr("r", 1e-6) | |
.on("mousedown", function(d) { selected = dragged = d; redraw(); }) | |
.transition() | |
.duration(750) | |
.ease("elastic") | |
.attr("r", 6.5); | |
circle | |
.classed("selected", function(d) { return d === selected; }) | |
.attr("cx", function(d) { return d[0]; }) | |
.attr("cy", function(d) { return d[1]; }); | |
circle.exit().remove(); | |
if (d3.event) { | |
d3.event.preventDefault(); | |
d3.event.stopPropagation(); | |
} | |
refreshList(); | |
} | |
// Set up text fields listing points. | |
function refreshList() { | |
var listSel = d3.select('#pointsList'); | |
if (listSel.empty()) { | |
var outerSel = d3.select('body').append('div').attr('id', 'listContainer') | |
.attr('height', '400px'); | |
listSel = outerSel.append('ul').attr('id', 'pointsList'); | |
} | |
// Append new list item hierarchies for each point. | |
var itemSel = listSel.selectAll('li').data(points, function(d) { return d; }); | |
itemSel.enter().append('li').call(function (sel) { | |
var spanXSel = sel.append('span').attr({ | |
id: 'xField', | |
contenteditable: true, | |
class: 'coordField' | |
}); | |
var spanYSel = sel.append('span').attr({ | |
id: 'yField', | |
contenteditable: true, | |
class: 'coordField' | |
}); | |
function editKeydown(d, i) { | |
switch (d3.event.keyCode) { | |
case 13: // Enter | |
// Stop editing. | |
this.blur(); | |
if (this.getAttribute('id') === 'xField') { | |
d[0] = parseFloat(this.textContent); | |
} | |
else { | |
d[1] = parseFloat(this.textContent); | |
} | |
redraw(); | |
break; | |
case 27: // Escape. | |
this.blur(); | |
// TODO: Revert. | |
break; | |
} | |
} | |
spanXSel.on("keydown", editKeydown); | |
spanYSel.on("keydown", editKeydown); | |
}); | |
// Remove hierarchies for removed points. | |
itemSel.exit().remove(); | |
// Update point values. | |
itemSel.selectAll('#xField').text(function(d) { return d[0]; }); | |
itemSel.selectAll('#yField').text(function(d) { return d[1]; }); | |
} | |
function change() { | |
line.interpolate(this.value); | |
redraw(); | |
} | |
function mousedown() { | |
points.push(selected = dragged = d3.mouse(svg.node())); | |
redraw(); | |
} | |
function mousemove() { | |
if (!dragged) return; | |
var m = d3.mouse(svg.node()); | |
dragged[0] = Math.max(0, Math.min(width, m[0])); | |
dragged[1] = Math.max(0, Math.min(height, m[1])); | |
redraw(); | |
// console.log(points); | |
} | |
function mouseup() { | |
if (!dragged) return; | |
mousemove(); | |
dragged = null; | |
} | |
function keydown() { | |
if (!selected || document.activeElement.tagName === 'SPAN') 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; | |
redraw(); | |
break; | |
} | |
} | |
} | |
</script> |