⇐ Previous | Next ⇒

Graphing Bitcoin data with D3.js

Post created 2013-12-23 14:44 by Gabe Koss.

A technology I have been curious to experiment with lately is D3.js a Javsascript graphing library using SVGs. A friend was looking for a little assistance creating a graph of the prices of bitcoins over time and I decided to try it out with D3.

The Data

The data coming in was JSON data containing a time key with a Unix timestamp and a last key containing the last see price at that moment.

[ {"time":1387774950.180666,"last":678.94998}, 
  ... 
  {"time":1387809762.27258,"last":670.0} ]

The Graph

Before digging into the code for this its nice to show the finished product. Here is the graph I ended up with:

It is worth noting that the X Axis times are static as this was built from a static json file.

The Code

Markup

The markup for this is very simple.

Include the JS

<script src="/js/d3-v3-min.js" charset="utf-8"></script>
<script src="/js/jquery-1-10-1-min.js"></script>
<script src="/js/d3_bitcoin.js"></script>

Some simple styles

#graph-wrapper { 
  padding: 10px; 
  background-color: rgba(20, 20, 20,0.2); 
} 

Element where the graph will live

<div id="graph-wrapper">
  <div id="graph"></div>
</div>

Set document variables

These are some helpful definitions which will be used throughout the script which builds the graph.

// Padding on left of graph
var graphPadding = 60;

// Width of SVG palette and of graph, respectively
var fullWidth = document.getElementById('graph').offsetWidth;
var width = (fullWidth-graphPadding);

// Height of SVG palette and of graph, respectively
var fullHeight = 320;
var height = (fullHeight-20);

Set up containers

Next, I added an SVG to div#graph. I stored this as palette and it has the dimensions of fullHeight x fullWidth.

// palette to draw graph and elements
var palette = d3.select("#graph").append("svg").
  attr("width", fullWidth).
  attr("height", fullHeight);

// secondary svg to hold graph, slightly smaller than palette
var lineContainer = palette.append("g").
  attr("x", graphPadding).
  attr("height", height).
  attr("width", width);

// Other groups
var edgeGroup = palette.append("g");
var lineGroup = palette.append("g");

Get the Data

The data for this request is being loaded from /json/trade_data.json and so I used jQuery to pull this in with $.getJSON. This is passed to the function as the data argument.

$(document).ready(function(){
  // Get the data and do the thing
  $.getJSON('/json/trade_data.json', function(data){
    // 
    // All code goes here
    // 
  }
}

Everything after this point was placed inside the function block.

Split Data

My approach was to split the array of hashes into two sequenced arrays called time and `last.

var time = [];
var last = [];

$(data).each(function(i,tick){
  time.push(tick.time);
  last.push(tick.last);
});

Set up X and Y coords

There are many ways to do this, but since I was only graphing a single set of data with two dimensions I bound these strongly to the coordinate systems. I created x and y objects to handle processing related to time and last respectively.

var x = {};
x.data   = time;

Set up a min and max to use for our graph domain and other purposes. This uses the helpers d3.min() and d3.max()and passes in thetime` data as the argument.

x.min    = d3.min(x.data);
x.max    = d3.max(x.data);

Set up the range for the x axis. This range is very important to creating the graph and requires a bit of setup. It needs a domain which is the data range that it is responsible for covering. In this case I used x.min and x.max.

It also needs a range which is the range of values which to map the data domain to. In this case it is the area covered by the graph on the x-axis: the graphPadding value to the fullWidth value.

x.domain = [x.min, x.max];
x.range  = [graphPadding, fullWidth]; 
x.scale  = d3.scale.linear().
              domain(x.domain).
              range(x.range);

Define a time range axis to display to the user. To keep all this data together I placed in a sub boject.

x.axis   = {
  min: new Date(x.min*1000),
  max: new Date(x.max*1000)
}
x.axis.scale = d3.time.scale().
                  domain([x.axis.min,x.axis.max]).
                  range(x.range);
x.axis.axis  = d3.svg.axis().
                  scale(x.axis.scale).
                  orient("bottom");

The y object I used is fairly similar to the x although a bit simpler.

var y = {};
y.data   = last;
y.min    = d3.min(y.data);
y.max    = d3.max(y.data);
y.domain = [y.max, y.min];
y.range  = [0, height];
y.scale  = d3.scale.linear().
              domain(y.domain).
              range(y.range);
y.axis   = {
  axis: d3.svg.axis().
           scale(y.scale).
           orient("left");
}

Add X and Y axis objects

I use the d3.svg.axis objects which were created as part of the x and y setup to create the scales on the left and bottom of the graph. Notice how these are appended to the edgeGroup and have to be moved around with transform operations.

edgeGroup.append("g").
  attr("stroke-width", 1).
  attr("transform", "translate("+graphPadding+",0)").
  attr("class", "axis").
  call(y.axis.axis);

edgeGroup.append("g").
  attr("transform", "translate(0,"+(fullHeight-20)+")").
  attr("class", "axis").
  call(x.axis.axis);

edgeGroup.selectAll('.axis path').
  style({
    'stroke': '#000',
    'fill': 'none',
    'stroke-width': '1px'
  });

Add the Line

The final step is to append a svg:path to the lineGroup. This path is rendered in the lineFunction.

var lineFunction = d3.svg.line()
  .y(function(d,i) { return y.scale(y.data[i]); })
  .x(function(d,i) { return x.scale(x.data[i]); })
  .interpolate("basis");

var lineGraph = lineGroup.append("svg:path").
  attr("d", lineFunction(data)).
  attr("stroke", "#000" ).
  attr("stroke-width", 1).
  attr("fill", "none");
comments powered by Disqus