Learning D3.js basics with Ruby (and Opal)

I always wanted to learn D3.js, problem is JavaScript is too awesome and that kept turning me off… till now!

Let’s take a random tutorial that we will attempt to translate into Ruby, for example this: http://bl.ocks.org/mbostock/3883245.

A couple of thing we need to bear in mind are these: D3 is not object-oriented and it’s a pretty complex library. We’re expecting some problems.

To have something to compare it to we may note, for example, that JQuery is basically a class ($) that exposes methods that returns other instances of the same class. That makes things easy enough when we try to use it from an OO language like Ruby.

Part I — The Setup

Following the tutorial, let’s setup the HTML page:

<!DOCTYPE html>
<html>
<meta charset="utf-8">
<style>
/* …uninteresting CSS from the tutorial here… */
</style>
<body>
<script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script>

<!-- OUR ADDITIONS HERE -->
<script src="http://cdn.opalrb.org/opal/current/opal.js"></script>
<script src="http://cdn.opalrb.org/opal/current/native.js"></script>
<script src="http://cdn.opalrb.org/opal/current/opal-parser.js"></script>

<script type="text/ruby" src="./app.rb"></script>

</body>
</html>

As you probably have noted we added a couple of libraries from the Opal CDN.

First we added opal.js, that is the runtime and core library that are necessary to run code compiled with Opal.

Then there’s native.js that we’ll use to interact with native objects (more details in this other post).

And last we have opal-parser.js and app.rb (that is declared as type="text/ruby"). The parser will look for all script tags marked as text/ruby and will load, parse and run them.

In additon we’re also providing a data.tsv file as described in the tutorial.

Part II — Code Ungarbling

About the process of translating

The approach I used to translate this code is quite simple, I copy/pasted all the JavaScript from the tutorial into app.rb and commented it out. Then I translated one line (or sentence) at a time tackling errors as they came.

Using Native

The first thing we need to do is to wrap the d3 global object with Native, so that is ready for Ruby consumption.

d3 = Native(`window.d3`)

Now let’s look at the first chunk of code

original code:

var parseDate = d3.time.format("%d-%b-%y").parse;

var x = d3.time.scale()
    .range([0, width]);

var y = d3.scale.linear()
    .range([height, 0]);

var xAxis = d3.svg.axis()
    .scale(x)
    .orient("bottom");

var yAxis = d3.svg.axis()
    .scale(y)
    .orient("left");

converted code

d3 = Native(window.d3)
time  = d3[:time]
scale = d3[:scale]
svg   = d3[:svg]

date_parser = time.format("%d-%b-%y")

x = time.scale.range([0, width])
y = scale.linear.range([height, 0])

x_axis = svg.axis.scale(x).orient(:bottom)
y_axis = svg.axis.scale(y).orient(:left)

By trying to run this code we discover early on that somehow Native is failing to deliver calls via method missing.

The reason is that bridged classes are a kind of their own and turns out that D3 tends to return augmented anonymous functions and arrays. Now both Array and Proc are bridged classes. That means that they are the same as their JS counterparts: Array and Function.

Native will have no effect on bridged classes (no wrapping) and therefore all calls to properties added by D3 will end in calls on undefined.

To solve this we’ll manually expose the methods on those classes, the final code looks like this:

module Native::Exposer
  def expose(*methods)
    methods.each do |name|
      define_method name do |*args,&block|
        args << block if block_given?
        `return #{self}[#{name}].apply(#{self},#{args.to_n})`
      end
    end
  end
end

Proc.extend Native::Exposer
Array.extend Native::Exposer

Proc.expose :range, :axis, :scale, :orient, :line, :x, :y, :parse, :domain
Array.expose :append, :attr, :call, :style, :text, :datum

The complete code can be found in this gist.

UPDATE: see the code in action on Ju-Jist, and read about building ju-jist in this article

A better solution

Let’s hope that in the future the Opal team (me included) will add method missing stubs1 to all bridged classes (instead of just adding them BasicObject).

Conclusion

We probably didn’t learn very much about how to use D3.js, but we discovered a bit of its internals, exposing an interesting style of JavaScript.


  1. (which is the underlying mechanism that powers method_missing support in Opal) 

Using SVG gradient masks with D3.js

If you’re reading this you’ll probably know what D3.js is. For anyone that doesn’t, D3.js is a great piece of JavaScript software that allows you to do some pretty awesome stuff – generally involving tons of data. It makes great use of HTML, SVG and CSS – nothing more. Neat, right?

The majority of D3.js use cases will obviously involve data representation. But what is data representation without some eyecandy goodness? You need that!

Well, in our last project we extensively used D3.js to show a lot of data, but we also needed some related graphic elements that users could zoom in and out without losing render quality or look weird. So, we decided to implement these directly in our SVG, using our beloved D3.js.

The design

Our design looked something like that (a picture is worth a thousand words):

image

It was clear we needed some sweet gradient masking using SVGs. So, how could we do that? It turned out it’s very simple.

The solution

Starting from a placeholder div for our SVG (let’s say with the id ‘svg-container’), we appended the two necessary elements for our mask in a <defs> tag inside the SVG: the vertical gradient (alpha-opaque-alpha) and the actual mask.

// Handy vars for our banner dimensions
var bannerWidth  = 1500,
    bannerHeight = 500;

// Let's append the SVG element.
d3.select('#svg-container')
    .append('svg:svg').attr('class', 'my-supah-svg')
    .append('svg:defs')
    .call(function (defs) {
      // Appending the gradient
      defs.append('svg:linearGradient')
        .attr('x1', '0')
        .attr('x2', '0')
        .attr('y1', '0')
        .attr('y2', '1')
        .attr('id', 'supah-gradient')
        .call(function (gradient) {
          gradient.append('svg:stop')
            .attr('offset', '0%')
            .attr('stop-color', 'white')
            .attr('stop-opacity', '0');
          gradient.append('svg:stop')
            .attr('offset', '50%')
            .attr('stop-color', 'white')
            .attr('stop-opacity', '1');
          gradient.append('svg:stop')
            .attr('offset', '100%')
            .attr('stop-color', 'white')
            .attr('stop-opacity', '0');
        });
      // Appending the mask
      defs.append('svg:mask')
        .attr('id', 'gradient-mask')
        .attr('width', bannerWidth)
        .attr('height', bannerHeight)
        .attr('x', 0)
        .attr('y', 0)
        // This is the rect that will actually
        // do the gradient masking job: as you can see it's
        // filled with the linear gradient.
        .call(function(mask) {
          mask.append('svg:rect')
            .attr('width', bannerWidth)
            .attr('height', bannerHeight)
            .attr('fill', 'url(#supah-gradient)')
        });
    })

These two elements formed our mask element. We just needed to apply it on our desired element – below there’s an example <rect> element with our mask on:

// Masking on a rect filled with a solid color
d3.select('.my-supah-svg').append('svg:rect')
  .attr('class', 'final-rect')
  .attr('width', bannerWidth)
  .attr('height', bannerHeight)
  .attr('x', 0)
  .attr('y', 0)
  .attr('fill', 'salmon')
  .attr('mask', 'url(#gradient-mask)');

We also needed to apply the masking on image elements – here’s another example for it:

// Masking on an actual image
d3.select('.my-supah-svg').append('svg:image')
  .attr('class', 'final-bg')
  .attr('xlink:href', 'http://lorempixel.com/output/technics-q-g-1500-500-4.jpg')
  .attr('width', bannerWidth)
  .attr('height', bannerHeight)
  .attr('x', 0)
  .attr('y', 0)
  .attr('mask', 'url(#gradient-mask)');

Easy peasy!

Just a little heads up: the mask element must have the same dimensions and position of the element being applied on. In our examples we set it to a fixed width/height and a 0 X/Y.

Conclusion

D3.js is a great, fun tool that can also be used for presentational purposes other than for serious and austere data graphs. Have fun!

Oh, and here you have the interactive demo on codepen!