vertical slider brushs
input rate of change - output values
we could also add sliders to the value chart and reverse the calculation to get growth rate...
<!DOCTYPE html> | |
<meta charset="utf-8"> | |
<style> | |
body{ | |
font-family: sans-serif; | |
} | |
svg{ | |
border: solid 1px #eee; | |
} | |
.axis { | |
font: 10px sans-serif; | |
-webkit-user-select: none; | |
-moz-user-select: none; | |
user-select: none; | |
} | |
.axis .domain { | |
fill: none; | |
stroke: #000; | |
shape-rendering:crispEdges; | |
} | |
.axis .halo { | |
fill: none; | |
stroke-width: 8px; | |
} | |
.slider .handle { | |
fill: #fff; | |
stroke: #000; | |
stroke-opacity: .5; | |
stroke-width: 1.25px; | |
pointer-events: none; | |
} | |
#growth-line{ | |
stroke:#333; | |
fill:none; | |
} | |
#value-line{ | |
stroke:#333; | |
fill:none; | |
} | |
</style> | |
<body> | |
<h2>Set growth rate:</h2> | |
<div id="input"></div> | |
<h2>Value:</h2> | |
<div id="output"></div> | |
</body> | |
<script src="http://d3js.org/d3.v3.min.js"></script> | |
<script> | |
var sliderCount = 0; | |
function nextID(){ | |
sliderCount ++; | |
return 'slider-'+sliderCount; | |
} | |
var width = 750; | |
var height = 350; | |
var sliderHeight = 300; | |
var initialValue = 45; | |
var growthData = [ | |
{ | |
label:'jan2001', | |
growthPct:70}, | |
{ | |
label:'jan2002', | |
growthPct:50}, | |
{ | |
label:'jan2003', | |
growthPct:30}, | |
{ | |
label:'jan2004', | |
growthPct:10}, | |
{ | |
label:'jan2005', | |
growthPct:0}]; | |
var startValue = 200; | |
growthData = calculateValues(growthData, startValue); | |
console.log(growthData); | |
//basic SVG structure | |
var marginTransform = 'translate(0,20)'; | |
var inputSVG = d3.select('#input').append('svg').attr('width',width).attr('height',height); | |
var growthChart = inputSVG.append('g').attr('id','growthChart').attr('transform', marginTransform); | |
var sliders = inputSVG.append('g').attr('id','ui').attr('transform', marginTransform); | |
var outputSVG = d3.select('#output').append('svg').attr('width',width).attr('height',height); | |
var valueChart = outputSVG.append('g').attr('id','valueChart').attr('transform', marginTransform); | |
var growthRateScale = d3.scale.linear() | |
.domain([0,100]) | |
.range([sliderHeight,0]) | |
.clamp(true); | |
var valueScale = d3.scale.linear() | |
.domain([0, 2000]) | |
.range([sliderHeight,0]); | |
var valueAxis = d3.svg.axis() | |
.scale(valueScale) | |
.orient('left') | |
.tickSize(-5) | |
.tickPadding(12); | |
valueChart.append('g').attr('class','y axis').attr('transform','translate(100,0)') | |
.call(valueAxis); | |
var xScale = d3.scale.linear() | |
.domain([0,growthData.length]) | |
.range([100,width]); | |
var growthLine = d3.svg.line() | |
.x(function(d,i) { return xScale(i); }) | |
.y(function(d) { return growthRateScale(d.growthPct); }); | |
var valueLine = d3.svg.line() | |
.x(function(d,i) { return xScale(i); }) | |
.y(function(d) { return valueScale(d.value); }); | |
var lookup = {}; | |
for(var i=0; i<growthData.length;i++){ | |
lookup[growthData[i].label] = i; | |
addVerticalSlider(sliders, {x:xScale(i), y:0}, growthRateScale, setGrowth, growthData[i].label, growthData[i].growthPct); | |
} | |
addGrowthChart(growthChart, growthData); | |
addValueChart(valueChart, growthData); | |
function setGrowth(id,value){ | |
growthData[ lookup[id] ].growthPct = value; | |
growthData = calculateValues(growthData,100); | |
updateGrowthChart(); | |
updateValueChart(); | |
} | |
function addValueChart(parent, data){ | |
parent.append('path').datum(data).attr('id','value-line') | |
.attr('d',valueLine); | |
} | |
function updateValueChart(){ | |
valueChart.select('#value-line') | |
.transition().duration(10).attr("d", valueLine); | |
} | |
function updateGrowthChart(){ | |
growthChart.select('#growth-line') | |
.transition().duration(10).attr("d", growthLine); | |
} | |
function addGrowthChart(parent, data){ | |
parent.append('path').datum(data).attr('id','growth-line') | |
.attr('d',growthLine); | |
} | |
function addVerticalSlider( parent, position, scale, callback, id, defaultValue ){ | |
if(!id){ | |
id = nextID(); | |
} | |
var sliderID = id; | |
var sliderHitWidth = 100; | |
function brushed(){ | |
var value = sliderBrush.extent()[0]; | |
if (d3.event.sourceEvent) { // not a programmatic event | |
value = scale.invert(d3.mouse(this)[1]); | |
sliderBrush.extent([value, value]); | |
} | |
callback(sliderID,value); | |
handle.attr('cy',scale(value)); | |
} | |
var sliderBrush = d3.svg.brush() | |
.y(scale) | |
.extent([0,0]) | |
.on('brush', brushed); | |
var sliderAxis = d3.svg.axis() | |
.scale(scale) | |
.orient('left') | |
.tickFormat(function(d){return d+'%'}) | |
.tickSize(0) | |
.tickPadding(12); | |
var sliderRoot = sliders.append('g') | |
.attr('class','vslider-container') | |
.attr('id',sliderID) | |
.attr('transform','translate(' + position.x + ',' + position.y + ')'); | |
sliderRoot.append('g').attr('class','y axis') | |
.call(sliderAxis) | |
.select('.domain') | |
.select(function(){ return this.parentNode.appendChild(this.cloneNode(true)) }) | |
.attr('class','halo'); | |
var slider = sliderRoot.append("g") | |
.attr("class", "slider") | |
.call(sliderBrush); | |
slider.selectAll(".extent,.resize") | |
.remove(); | |
slider.select('.background') | |
.attr('width', sliderHitWidth) | |
.attr('x',-sliderHitWidth/2); | |
var handle = slider.append('circle') | |
.attr('class','handle') | |
.attr('r',10); | |
slider | |
.call(sliderBrush.event) | |
.transition() // gratuitous intro! | |
.duration(750) | |
.call(sliderBrush.extent([defaultValue, defaultValue])) | |
.call(sliderBrush.event); | |
}; | |
function calculateValues(data, first){ | |
if(!first){ | |
first = 100; | |
} | |
if(!data[0].value && first){ | |
data[0].value = first; | |
} | |
for (var i=1;i<data.length;i++){ | |
data[i].value = data[i-1].value * ( data[i-1].growthPct/100 + 1 ); | |
} | |
return data; | |
} | |
d3.select(self.frameElement).style("height", "775px"); | |
</script> |