Growth chart
<!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>
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...