tomgp
9/19/2013 - 1:21 PM

Growth chart

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