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> |