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:
/* …uninteresting CSS from the tutorial here… */
<script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script>
<!-- OUR ADDITIONS HERE -->
<script type="text/ruby" src="./app.rb"></script>
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.
native.js that we’ll use to interact with native objects (more details in this other post).
And last we have
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
app.rb and commented it out. Then I translated one line (or sentence) at a time tackling errors as they came.
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
var parseDate = d3.time.format("%d-%b-%y").parse;
var x = d3.time.scale()
var y = d3.scale.linear()
var xAxis = d3.svg.axis()
var yAxis = d3.svg.axis()
d3 = Native(
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
Proc are bridged classes. That means that they are the same as their JS counterparts:
Native will have no effect on bridged classes (no wrapping) and therefore all calls to properties added by D3 will end in calls on
To solve this we’ll manually expose the methods on those classes, the final code looks like this:
methods.each do |name|
define_method name do |*args,&block|
args << block if block_given?
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