Built with blockbuilder.org
Fork from spline editor adding axes and animation. (binded in shiny to create bivariate slider)
license: mit |
Built with blockbuilder.org
Fork from spline editor adding axes and animation. (binded in shiny to create bivariate slider)
<!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; | |
top: 20px; | |
left: 80px; | |
} | |
rect { | |
fill: none; | |
pointer-events: all; | |
} | |
ellipse { | |
fill: steelblue; | |
stroke: steelblue; | |
stroke-width: 1px; | |
} | |
circle, | |
.line { | |
fill: none; | |
stroke: steelblue; | |
stroke-width: 1.5px; | |
} | |
circle { | |
fill: #fff; | |
fill-opacity: .2; | |
cursor: move; | |
} | |
.selected { | |
fill: #ff7f0e; | |
stroke: #ff7f0e; | |
} | |
.tooltip { | |
position: absolute; | |
width: 200px; | |
height: 28px; | |
pointer-events: none; | |
} | |
.axis { | |
shape-rendering: crispEdges; | |
} | |
.x.axis line, | |
.x.axis path { | |
fill: none; | |
stroke: #000; | |
} | |
.y.axis line, | |
.y.axis path { | |
fill: none; | |
stroke: #000; | |
} | |
</style> | |
<form> | |
<br> | |
<select id="interpolate"></select> | |
</form> | |
<script src="//d3js.org/d3.v3.min.js"></script> | |
<script> | |
var x= {n:3,width:600,height:500,anim:1,loop:1}; | |
var margin={top:20,right:20,bottom:30,left:40}; | |
var width = x.width-margin.left-margin.right; | |
var height = x.height-margin.top-margin.bottom; | |
var data = {x:[1,2,3], y:[Math.random(),Math.random(),Math.random()]} | |
var axisName=Object.keys(data); | |
var points = d3.range(0, x.n).map(function(i) { | |
return [data[axisName[0]][i], data[axisName[1]][i]]; | |
}); | |
var pointsFloat = points[0]; | |
// setup x | |
var xValue = function(d) { return d[0];}, // data -> value | |
xScale = d3.scale.linear() | |
.domain([d3.min(points, xValue)-1, d3.max(points, xValue)+1]) | |
.range([ 0, width ]), | |
xMap = function(d) { return xScale(xValue(d));}, // data -> display | |
xAxis = d3.svg.axis().scale(xScale).orient("bottom"); | |
// setup y | |
var yValue = function(d) { return d[1];}, // data -> value | |
yScale = d3.scale.linear() | |
.domain([d3.min(points, yValue)-1, d3.max(points, yValue)+1]) | |
.range([ height, 0 ]), // value -> display | |
yMap = function(d) { return yScale(yValue(d));}, // data -> display | |
yAxis = d3.svg.axis().scale(yScale).orient("left"); | |
//setup inverse scales | |
var xScaleInv = d3.scale.linear() | |
.range([d3.min(points, xValue)-1, d3.max(points, xValue)+1]) | |
.domain([ 0, width ]), | |
yScaleInv = d3.scale.linear() | |
.range([d3.min(points, yValue)-1, d3.max(points, yValue)+1]) | |
.domain([ height, 0 ]), | |
xMapInv = function(d) { return xScaleInv(d);}, | |
yMapInv = function(d) { return yScaleInv(d);}; | |
// add tooltip | |
var tooltip = d3.select("body").append("div") | |
.attr("class", "tooltip") | |
.style("opacity", 0); | |
// add line | |
var dragged = null, | |
selected = points[0]; | |
var line = d3.svg.line() | |
.x(function(d) { return xMap(d); }) | |
.y(function(d) { return yMap(d); }); | |
//append svg to body | |
var svg = d3.select('body').append("svg") | |
.attr("width", width+margin.left+margin.right) | |
.attr("height", height+margin.top+margin.bottom).append("g") | |
.attr("transform", "translate(" + margin.left + "," + margin.top + ")") | |
.attr("tabindex", 1); | |
// append rect | |
svg.append("rect") | |
.attr("width", width) | |
.attr("height", height) | |
.on("mousedown", mousedown); | |
// append line | |
svg.append("path") | |
.datum(points) | |
.attr("class", "line") | |
.call(redraw); | |
// map mouse actions | |
d3.select(window) | |
.on("mousemove", mousemove) | |
.on("mouseup", mouseup) | |
.on("keydown", keydown); | |
//populate form with interpolation options | |
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(); | |
// add x-axis | |
svg.append("g") | |
.attr("class", "x axis") | |
.attr("transform", "translate(0," + height + ")") | |
.call(xAxis) | |
.append("text") | |
.attr("class", "label") | |
.attr("x", width) | |
.attr("y", -6) | |
.style("text-anchor", "end") | |
.text(axisName[0]); | |
// add y-axis | |
svg.append("g") | |
.attr("class", "y axis") | |
.attr('transform', 'translate(0,0)') | |
.call(yAxis) | |
.append("text") | |
.attr("class", "label") | |
.attr("transform", "rotate(-90)") | |
.attr("y", 6) | |
.attr("dy", ".71em") | |
.style("text-anchor", "end") | |
.text(axisName[1]); | |
// function that redraws path on mouse clicks and change to form | |
function redraw() { | |
var pauseValues = { | |
lastT: 0, | |
currentT: 0 | |
}; | |
var path=svg.select("path").attr("d", line); | |
var circle = svg.selectAll("circle").data(points); | |
circle.enter().append("circle") | |
.attr("cx", xMap) | |
.attr("cy", yMap); | |
circle.on("mouseover", function(d) { | |
tooltip.transition() | |
.duration(200) | |
.style("opacity", 0.9); | |
tooltip.html("(" + d3.round(xValue(d),1) + ", " + d3.round(yValue(d),1) + ")") | |
.style("left", (d3.event.pageX) + "px") | |
.style("top", (d3.event.pageY) + "px"); | |
}) | |
.on("mouseout", function(d) { | |
tooltip.transition() | |
.duration(500) | |
.style("opacity", 0); | |
}); | |
circle.classed("selected", function(d) { return d === selected; }) | |
.attr("cx", xMap) | |
.attr("cy", yMap); | |
circle.exit().remove(); | |
if (d3.event) { | |
d3.event.preventDefault(); | |
d3.event.stopPropagation(); | |
} | |
circle.on("mousedown", function(d) { selected = dragged = d; redraw(); }) | |
.transition() | |
.duration(750) | |
.ease("elastic") | |
.attr("r", 2); | |
function transition() { | |
svg.selectAll('ellipse').transition() | |
.duration(duration - (duration * pauseValues.lastT)) | |
.attrTween("transform", translateAlong(svg.selectAll('path').node())) | |
.each("end", function(){ | |
pauseValues = { | |
lastT: 0, | |
currentT: 0 | |
}; | |
pathpoints={x:[],y:[]}; | |
if(x.loop==1) transition(); | |
}); | |
} | |
function translateAlong(path) { | |
var l = path.getTotalLength(); | |
return function(d, i, a) { | |
return function(t) { | |
t += pauseValues.lastT; | |
var p = path.getPointAtLength(t * l); | |
pauseValues.currentT = t; | |
if(typeof(Shiny) !== "undefined"){ | |
Shiny.onInputChange(el.id + "_update",{".pathData": JSON.stringify(pathpoints)}); | |
} | |
return "translate(" + p.x + "," + p.y + ")"; | |
}; | |
}; | |
} | |
if(x.anim==1){ | |
var pause =0; | |
var duration=10000; | |
var circleBig = svg.append("ellipse") | |
.attr("rx", 5) | |
.attr("ry", 5) | |
.attr('transform','translate('+ xMap(pointsFloat) + ',' + yMap(pointsFloat) +')'); | |
pauseButton(svg,width -margin.right-10 , margin.top-15); | |
//transition(); | |
} | |
function pauseButton(svg,x, y){ | |
var Pbutton = svg.append("g") | |
.attr("transform", "translate("+ x +","+ y +")") | |
.attr('id', 'PbtnId' ); | |
Pbutton | |
.append("rect") | |
.attr("width", 25) | |
.attr("height", 25) | |
.attr("rx", 6) | |
.style("fill", "steelblue"); | |
Pbutton | |
.append("path") | |
.attr('id', 'pathId' ) | |
.attr("d", "M5 5 L5 20 L20 13 Z") | |
.style("fill", "white"); | |
Pbutton | |
.on("mousedown", function() { | |
d3.select(this).select("rect") | |
.style("fill","white") | |
.transition().style("fill","steelblue"); | |
pause++; | |
if(pause%2==0){ | |
svg.selectAll("ellipse").transition().duration(0); | |
setTimeout(function() { | |
pauseValues.lastT=pauseValues.currentT; | |
}, 100); | |
Pbutton.select("path").attr("d", "M5 5 L5 20 L20 13 Z"); | |
}else{ | |
Pbutton.select("path").attr("d", "M5 5 L10 5 L10 20 L5 20 M15 5 L20 5 L20 20 L15 20 Z"); | |
transition(); | |
} | |
}); | |
} | |
} | |
//update line with changes to form selection | |
function change() { | |
line | |
.tension(0) | |
.interpolate(this.value); | |
svg.selectAll("ellipse").remove(); | |
redraw(); | |
} | |
// mouse click down | |
function mousedown() { | |
points.push(selected = dragged = d3.mouse(svg.node())); | |
var m = d3.mouse(svg.node()); | |
dragged[0] = xMapInv(Math.max(0, Math.min(width, m[0]))); | |
dragged[1] = yMapInv(Math.max(0, Math.min(height, m[1]))); | |
svg.selectAll("ellipse").remove(); | |
redraw(); | |
} | |
// mouse drag | |
function mousemove() { | |
if (!dragged) return; | |
var m = d3.mouse(svg.node()); | |
dragged[0] = xMapInv(Math.max(0, Math.min(width, m[0]))); | |
dragged[1] = yMapInv(Math.max(0, Math.min(height, m[1]))); | |
svg.selectAll("ellipse").remove(); | |
redraw(); | |
} | |
// mouse release | |
function mouseup() { | |
if (!dragged) return; | |
mousemove(); | |
dragged = null; | |
} | |
//key press events | |
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; | |
svg.selectAll("ellipse").remove(); | |
redraw(); | |
break; | |
} | |
} | |
} | |
</script> |