indexzero
7/20/2013 - 4:07 AM

Sizing up the PaaS Industry

Sizing up the PaaS Industry

//
// All known pricing data.
//
var prices = {
  paas: {
    nodejitsu: {
      hourly: {
        'Micro':   0.0125,
        'Small':   0.015,
        'Regular': 0.013,
        'Large':   0.0125
      },
      monthly: {
        'Micro':   9.0,
        'Small':   33.0,
        'Regular': 50.0,
        'Large':   90.0
      }
    },
    comparison: {
      hourly: {
        'AppFog':       0.0033,
        'CloudFoundry': 0.0075,
        'Nodejitsu':    0.0125,
        'Modulus':      0.0129,
        'OpenShift':    0.02,
        'Heroku':       0.025,
        'AppHarbor':    0.034,
        'dotCloud':     0.048
      },
      monthly: {
        'AppFog':       20.0,
        'CloudFoundry': 21.6,
        'Nodejitsu':    9.0,
        'Modulus':      14.40,
        'OpenShift':    20.0,
        'Heroku':       36.0,
        'AppHarbor':    49.0,
        'dotCloud':     34.56
      }
    },
    'comparison-large-app': {
      hourly: {
        'Nodejitsu':    0.125,
        'CloudFoundry': 0.150,
        'AppFog':       0.1389,
        'Modulus':      (0.02 * 10),
        'OpenShift':    0.3077,
        'Heroku':       (0.05 * 9).toFixed(4),
        'dotCloud':     (0.048 * 10).toFixed(4),
        'AppHarbor':    0.6847
      },
      monthly: {
        'Nodejitsu':    90.00,
        'CloudFoundry': 108.00,
        'AppFog':       100.00,
        'Modulus':      144.00,
        'OpenShift':    221.60,
        'Heroku':       324.00,
        'dotCloud':     345.60,
        'AppHarbor':    439.00
      }
    }
  }
};
/********************
 * HTML CSS
 */
 
.chartWrap {
  margin: 0;
  padding: 0;
  overflow: hidden;
}
 
/********************
 * TOOLTIP CSS
 */
 
.nvtooltip {
  position: absolute;
  background-color: rgba(255,255,255,1);
  padding: 1px;
  border: 1px solid rgba(0,0,0,.2);
  z-index: 10000;
 
  font-family: Arial;
  font-size: 13px;
 
  transition: opacity 500ms linear;
  -moz-transition: opacity 500ms linear;
  -webkit-transition: opacity 500ms linear;
 
  transition-delay: 500ms;
  -moz-transition-delay: 500ms;
  -webkit-transition-delay: 500ms;
 
  -moz-box-shadow: 0 5px 10px rgba(0,0,0,.2);
  -webkit-box-shadow: 0 5px 10px rgba(0,0,0,.2);
  box-shadow: 0 5px 10px rgba(0,0,0,.2);
 
  -webkit-border-radius: 6px;
  -moz-border-radius: 6px;
  border-radius: 6px;
 
  pointer-events: none;
 
  -webkit-touch-callout: none;
  -webkit-user-select: none;
  -khtml-user-select: none;
  -moz-user-select: none;
  -ms-user-select: none;
  user-select: none;
}
 
.nvtooltip.x-nvtooltip,
.nvtooltip.y-nvtooltip {
  padding: 8px;
}
 
.nvtooltip h3 {
  margin: 0;
  padding: 4px 14px;
  line-height: 18px;
  font-weight: normal;
  background-color: #f7f7f7;
  text-align: center;
 
  border-bottom: 1px solid #ebebeb;
 
  -webkit-border-radius: 5px 5px 0 0;
  -moz-border-radius: 5px 5px 0 0;
  border-radius: 5px 5px 0 0;
}
 
.nvtooltip p {
  margin: 0;
  padding: 5px 14px;
  text-align: center;
}
 
.nvtooltip span {
  display: inline-block;
  margin: 2px 0;
}
 
.nvtooltip-pending-removal {
  position: absolute;
  pointer-events: none;
}
 
/********************
 * SVG CSS
 */
 
svg {
  -webkit-touch-callout: none;
  -webkit-user-select: none;
  -khtml-user-select: none;
  -moz-user-select: none;
  -ms-user-select: none;
  user-select: none;
  /* Trying to get SVG to act like a greedy block in all browsers */
  display: block;
  width:100%;
  height:100%;
}
 
svg text {
  font: normal 12px Arial;
}
 
svg .title {
 font: bold 14px Arial;
}
 
.nvd3 .nv-background {
  fill: white;
  fill-opacity: 0;
  /*
  pointer-events: none;
  */
}
 
.nvd3.nv-noData {
  font-size: 18px;
  font-weight: bold;
}
 
/**********
*  Legend
*/
 
.nvd3 .nv-legend .nv-series {
  cursor: pointer;
}
 
.nvd3 .nv-legend .disabled circle {
  fill-opacity: 0;
}
 
/**********
*  Axes
*/
 
.nvd3 .nv-axis path {
  fill: none;
  stroke: #000;
  stroke-opacity: .75;
  shape-rendering: crispEdges;
}
 
.nvd3 .nv-axis path.domain {
  stroke-opacity: .75;
}
 
.nvd3 .nv-axis.nv-x path.domain {
  stroke-opacity: 0;
}
 
.nvd3 .nv-axis line {
  fill: none;
  stroke: #000;
  stroke-opacity: .25;
  shape-rendering: crispEdges;
}
 
.nvd3 .nv-axis line.zero {
  stroke-opacity: .75;
}
 
.nvd3 .nv-axis .nv-axisMaxMin text {
  font-weight: bold;
}
 
.nvd3 .x  .nv-axis .nv-axisMaxMin text,
.nvd3 .x2 .nv-axis .nv-axisMaxMin text,
.nvd3 .x3 .nv-axis .nv-axisMaxMin text {
  text-anchor: middle
}
 
/**********
*  Bars
*/
 
.nvd3 .nv-bars .negative rect {
    zfill: brown;
}
 
.nvd3 .nv-bars rect {
  zfill: steelblue;
  fill-opacity: .75;
 
  transition: fill-opacity 250ms linear;
  -moz-transition: fill-opacity 250ms linear;
  -webkit-transition: fill-opacity 250ms linear;
}
 
.nvd3 .nv-bars rect:hover {
  fill-opacity: 1;
}
 
.nvd3 .nv-bars .hover rect {
  fill: lightblue;
}
 
.nvd3 .nv-bars text {
  fill: rgba(0,0,0,0);
}
 
.nvd3 .nv-bars .hover text {
  fill: rgba(0,0,0,1);
}
 
/**********
*  Bars
*/
 
.nvd3 .nv-multibar .nv-groups rect,
.nvd3 .nv-multibarHorizontal .nv-groups rect,
.nvd3 .nv-discretebar .nv-groups rect {
  stroke-opacity: 0;
 
  transition: fill-opacity 250ms linear;
  -moz-transition: fill-opacity 250ms linear;
  -webkit-transition: fill-opacity 250ms linear;
}
 
.nvd3 .nv-multibar .nv-groups rect:hover,
.nvd3 .nv-multibarHorizontal .nv-groups rect:hover,
.nvd3 .nv-discretebar .nv-groups rect:hover {
  fill-opacity: 1;
}
 
.nvd3 .nv-discretebar .nv-groups text,
.nvd3 .nv-multibarHorizontal .nv-groups text {
  font-weight: bold;
  fill: rgba(0,0,0,1);
  stroke: rgba(0,0,0,0);
}
<!doctype html>  
<html lang="en">
  
  <head>
    <meta charset="utf-8">
    
    <title>Platform-as-a-Service Pricing Comparison</title>

    <meta name="description" content="Platform-as-a-Service Pricing Comparison">
    <meta name="author" content="Charlie Robbins">

    <meta name="apple-mobile-web-app-capable" content="yes" />
    <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
    
    <link rel="stylesheet" href="nv.d3.css">
    <link href='http://fonts.googleapis.com/css?family=Arvo:400,700,400italic,700italic' rel='stylesheet' type='text/css'>

    <style>
      h2 {
        font-family: "Arvo", "Helvetica Neue", Helvetica, Arial, sans-serif;
        font-size: 24px;
        font-weight: 200;
        letter-spacing: -0.02em;
        color: black;
        text-align: center;
      }
      
      p {
        font-family: sans-serif;
        font-size: 18px;
        line-height: 24px;
      }
    
      figure {
        height: 450px;
        width: 900px;
      }
      
      .nv-x, #sparks div {
        font-family: "Arvo", "Helvetica Neue", Helvetica, Arial, sans-serif;
        font-size: 24px;
        font-weight: 200;
        letter-spacing: -0.02em;
        color: black;
        text-align: center;        
      }
      
      #sparks figure {
        clear: both;
        height: 150px;
        width: 300px;
        margin: 0;
      }
      
      #sparks div {
        float: left;
        font-size: 16px;
        text-decoration: underline;
      }
      
      .selection a {
        background-color: #aec7e8;
        border-radius: 10px;
        color: #666;
        cursor: pointer;
        display: inline-block;
        font-size: 14px;
        margin: 0 15px 0 0;
        padding: 8px 10px;
        text-decoration: none;
      }
      
      .selection a.shown {
        background-color: #1f77b4;
        color: #FFF;
      }
      
      .selection a:first-child { margin-left: 80px; }
      .narrow    a:first-child { margin-left: 45px; }
      hr { margin: 40px 0 }
    </style>
  </head>
  
  <body>
    <div style="width:900px;margin: 0 auto; text-align: left">
      <p>When we <a href="http://blog.nodejitsu.com/changes-in-nodejitsu-public-cloud">announced new pricing for our Individual Plans earlier this month</a>, we made a claim that <strong>even with these changes Nodejitsu is still the cheapest Platform-as-a-Service on the market today.</strong> This post aims to support that claim with the only thing that really matters: <em>data</em>.</p>
      <p>Best viewed at <a href="http://bl.ocks.org/indexzero/6043821">bl.ocks.org/indexzero/6043821</a></p>
      
      <section id="sparks">
        <div id="pricing-comparison-spark">
          PaaS Pricing Comparison
          <figure><svg></svg></figure>
        </div>
        <div id="individual-plans-spark">
          Nodejitsu Plans
          <figure><svg></svg></figure>
        </div>
        <div id="pricing-comparison-multiple-apps-spark">
          Large App Pricing Comparison
          <figure><svg></svg></figure>
        </div>
      </section>
      
      <hr>
      <div style="clear:both"></div>
      <section id="pricing-comparison">
        <h2>
          Platform-as-a-Service Pricing Comparison.
          <span class="selection">
            <a data-chart="pricing-comparison" data-series="comparison" data-time="hourly" class="shown">Hourly</a>
            <a data-chart="pricing-comparison" data-series="comparison" data-time="monthly">Monthly</a>
          </span>
        </h2>
        <figure><svg></svg></figure>
      </section>
      
      <hr>
      <section id="individual-plans">
        <h2>
          Nodejitsu Individual Plan Pricing Comparison.
          <span class="selection">
            <a data-chart="individual-plans" data-series="nodejitsu" data-time="hourly" class="shown">Hourly</a>
            <a data-chart="individual-plans" data-series="nodejitsu" data-time="monthly">Monthly</a>
          </span>
        </h2>
        <figure><svg></svg></figure>
      </section>
      
      <hr>
      <section id="pricing-comparison-multiple-apps">
        <h2>
          Platform-as-a-Service Pricing Comparison (complex app)
          <span class="selection narrow">
            <a data-chart="pricing-comparison-multiple-apps" data-series="comparison-large-app" data-time="hourly" class="shown">Hourly</a>
            <a data-chart="pricing-comparison-multiple-apps" data-series="comparison-large-app" data-time="monthly">Monthly</a>
          </span>
        </h2>
        <figure><svg></svg></figure>
      </section>
    </div>

    <script src="http://cdnjs.cloudflare.com/ajax/libs/jquery/2.0.3/jquery.min.js"></script>
    <script src="http://cdnjs.cloudflare.com/ajax/libs/d3/2.10.0/d3.v2.min.js"></script>
    <script src="http://cdnjs.cloudflare.com/ajax/libs/nvd3/1.0.0-beta/nv.d3.min.js"></script>
    <script src="comparison.js"></script>
  </body>
</html>
//
// All known pricing data.
//
var prices = {
  paas: {
    nodejitsu: {
      hourly: {
        'Micro':   0.0125,
        'Small':   0.015,
        'Regular': 0.013,
        'Large':   0.0125
      },
      monthly: {
        'Micro':   9.0,
        'Small':   33.0,
        'Regular': 50.0,
        'Large':   90.0
      }
    },
    comparison: {
      hourly: {
        'AppFog':       0.0033,
        'CloudFoundry': 0.0075,
        'Nodejitsu':    0.0125,
        'Modulus':      0.0129,
        'OpenShift':    0.02,
        'Heroku':       0.025,
        'AppHarbor':    0.034,
        'dotCloud':     0.048
      },
      monthly: {
        'AppFog':       20.0,
        'CloudFoundry': 21.6,
        'Nodejitsu':    9.0,
        'Modulus':      14.40,
        'OpenShift':    20.0,
        'Heroku':       36.0,
        'AppHarbor':    49.0,
        'dotCloud':     34.56
      }
    },
    'comparison-large-app': {
      hourly: {
        'Nodejitsu':    0.125,
        'CloudFoundry': 0.150,
        'AppFog':       0.1389,
        'Modulus':      (0.02 * 10),
        'OpenShift':    0.3077,
        'Heroku':       (0.05 * 9).toFixed(4),
        'dotCloud':     (0.048 * 10).toFixed(4),
        'AppHarbor':    0.6847
      },
      monthly: {
        'Nodejitsu':    90.00,
        'CloudFoundry': 108.00,
        'AppFog':       100.00,
        'Modulus':      144.00,
        'OpenShift':    221.60,
        'Heroku':       324.00,
        'dotCloud':     345.60,
        'AppHarbor':    439.00
      }
    }
  }
};

//
// Store all charts for later use
//
var charts = {};

//
// ### function tooltipContent (key, x, y, e, graph)
// Formats the tooltip based on the correct (full) y-value.
//
function tooltipContent(key, x, y, e, graph) {
  return '<h3>' + key + '</h3>' +
         '<p>' +  e.point.value + ' on ' + x + '</p>';
}

//
// ### function namedSeries (options)
// Generates a named series of options.data that
// conforms to the conventions of nvd3.js
//
function namedSeries(options) {
  return [{
    values: Object.keys(options.data)
      .map(function (key) {
        return {
          value: options.data[key],
          label: key
        }
      })
  }];  
}

//
// ### function createPricingSparkChart(options)
// Creates a new nvd3 chart (defaulting to `discreteBarChart`)
// for the specified `options` as a "spark chart" (i.e. small)
//
function createPricingSparkChart(options) {
  options.id += '-spark';
  options.staggerLabels = true;
  options.tooltips = false;
  options.showValues = false;
  delete options.xAxis;
  createPricingChart(options);
}

//
// ### function createPricingChart(options)
// Creates a new nvd3 chart (defaulting to `discreteBarChart`)
// for the specified `options`.
//
function createPricingChart(options) {
  options.type = options.type || 'discreteBarChart';
  
  if (typeof options.tooltips === 'undefined') { options.tooltips = true }
  if (typeof options.showValues === 'undefined') { options.showValues = true }
  
  nv.addGraph(function () {
    var chart = nv.models[options.type]()
      .x(function (d) { return d.label })
      .y(function (d) { return d.value });
      
    if (options.onChart) {
      chart = options.onChart(chart, options);
    }

    if (options.xAxis && options.xAxis.label) {
      chart.xAxis
        .axisLabel(options.xAxis.label);
    }

    var svg = d3.select('#' + options.id + ' figure svg');
    
    svg.datum(namedSeries(options))
      .transition()
      .duration(500)
      .call(chart);  
      
    nv.utils.windowResize(chart.update);
    
    if (!options.staggerLabels) {
      //
      // Select xAxis ticks for label styling.
      //
      var xTicks = svg.select('g.nv-wrap.nv-discreteBarWithAxes')
        .select('g')
        .select('.nv-x.nv-axis > g');

      xTicks
        .selectAll('g > text')
        .style('font', 'normal 14px "Arvo", "Helvetica Neue", Helvetica, Arial, sans-serif')
        .attr('transform', 'translate(0,2)');

      xTicks.select('g > text.nv-axislabel')
        .style('font', 'normal 20px "Arvo", "Helvetica Neue", Helvetica, Arial, sans-serif')
        .attr('transform', 'translate(0,10)')
        .style('text-decoration', 'underline');
    }
    
    charts[options.id] = chart;
    return chart;
  });
  
  if (!/-spark$/.test(options.id)) {
    createPricingSparkChart(
      Object.keys(options).reduce(function (copy, key) {
        copy[key] = options[key];
        return copy;
      }, {})
    );
  }
}

//
// Create a chart for Nodejitsu Individual Plans
//
createPricingChart({
  name: 'Nodejitsu Individual Plans',
  id: 'individual-plans',
  xAxis: { label: 'Plan' },
  data: prices.paas.nodejitsu.hourly,
  onChart: function (chart, options) {
    chart.yAxis
      .axisLabel('Price')
      .tickFormat(d3.format(',.3f'));
    
    return chart.staggerLabels(options.staggerLabels || false)
      .tooltips(options.tooltips)
      .tooltipContent(tooltipContent)
      .showValues(options.showValues)
      .valueFormat(d3.format(',.4f'));    
  }
});

//
// Create a chart for all Platform-as-a-Service providers.
//
createPricingChart({
  name: 'Pricing Comparison',
  id: 'pricing-comparison',
  xAxis: { label: 'PaaS Provider' },
  data: prices.paas.comparison.hourly,
  onChart: function (chart, options) {
    chart.yAxis
      .axisLabel('Price')
      .tickFormat(d3.format(',.3f'));
    
    return chart.staggerLabels(options.staggerLabels || false)
      .tooltips(options.tooltips)
      .tooltipContent(tooltipContent)
      .showValues(options.showValues)
      .valueFormat(d3.format(',.4f'));    
  }
});

//
// Create a chart for all Platform-as-a-Service providers.
//
createPricingChart({
  name: 'Pricing Comparison (complex hosting)',
  id: 'pricing-comparison-multiple-apps',
  xAxis: { label: 'PaaS Provider' },
  data: prices.paas['comparison-large-app'].hourly,
  onChart: function (chart, options) {
    chart.yAxis
      .axisLabel('Price')
      .tickFormat(d3.format(',.3f'));
    
    return chart.staggerLabels(options.staggerLabels || false)
      .tooltips(options.tooltips)
      .tooltipContent(tooltipContent)
      .showValues(options.showValues)
      .valueFormat(d3.format(',.4f'));    
  }
});

//
// Setup click handlers
//
$(function () {
  $('.selection a').each(function (i, el) {
    var anchor = $(el);
    anchor.click(function () {
      if (anchor.hasClass('shown')) { return; }
      var state = {
        shown: anchor.hasClass('shown'),
        chart: anchor.data('chart'),
        series: anchor.data('series'),
        time: anchor.data('time')
      };
      
      state.from = state.time === 'hourly'
        ? 'monthly'
        : 'hourly';
      
      $('#' + state.chart + ' .selection .shown').first().removeClass('shown');
      anchor.addClass('shown');
      
      var svg = d3.select('#' + state.chart + ' figure svg');
      svg.datum(namedSeries({ data: prices.paas[state.series][state.time] }))
        .transition()
        .duration(500)
        .call(charts[state.chart]);
    });
  });
});