If you don’t know about Raphaël, you’d better ask somebody. It provides a vector drawing API that works in all major browsers (by abstracting between SVG and VML).
I’ve been working on a JavaScript charting library called Krang. Krang is designed to take a data set and produce any chart (line chart, pie chart, bar chart) using Raphaël’s drawing commands. It’s nascent, but stable, and my friend and colleague Thomas Fuchs has already used it in one of his projects.
Configuration complexity
I realized very early in Krang’s development process that configuration complexity would threaten to kick my ass. In other words, a good charting library should give the user utmost control over look and feel — with the robustness and flexibility that CSS gives over the look and feel of HTML.
The standard pattern in Prototype and script.aculo.us has the developer provide an options
argument in the constructor; whatever properties are contained in this object are copied onto a set of “default” options.
var Foo = Class.create({
initialize: function(element, options) {
this.element = $(element);
this.options = Object.extend({
logFoobars: true,
onThud: Prototype.K
}, options || {});
}
});
I see two problems with this, though.
What if I have a crapload of options?
Imagine something like this. Would this be any fun at all, for either me (the author) or you (the developer using the code)?
Chart.Bar = Class.create({
initialize: function(element, options) {
this.element = $(element);
this.options = Object.extend({
width: 800,
height: 300,
barColor: "#003399",
barBorderWidth: 0,
barBorderColor: 'auto',
barGutter: 5,
barOpacity: 1.0,
barLabel: false,
barLabelPosition: 'above',
barLabelFontFamily: 'Lucida Grande',
barLabelSize: '12px',
barLabelColor: '#000',
barLabelTextFilter: Prototype.K,
barGradientType: 'linear',
barGradientVector: [0, 0, '100%', 0],
gutterTop: 20,
gutterBottom: 30,
gutterLeft: 100,
gutterRight: 30,
gridColor: '#eee',
gridLinesHorizontal: 10,
gridLinesVertical: 10,
borderColor: '#bbb',
borderWidth: 1
}, options || {});
}
});
And that’s not all. I stopped out of courtesy. It may seem like overkill to have this many configurable items, but nearly all of these are nontrivial look‐and‐feel decisions. You’d expect this sort of control if you were generating a chart with Excel, Numbers, or something similar. And existing Flash‐based charting tools (like XML/SWF Charts) allow for just as much customization.
Anyway, that’s the first problem.
What about inheritance?
As you might expect, all charts in Krang inherit from a base class called Chart.Base
. Furthermore, both bar charts and line charts inherit from an abstract class called Chart.Area
, which handles stuff common to both kinds of charts: drawing a grid, converting “chart‐based” X/Y coordinates to raw drawing coordinates, etc.
The point is this: what if Chart.Base
, Chart.Area
, and Chart.Line
all want to set options? Won’t they step on each other? I could make it so that only the “final” class in the inheritance chain handles the options, but there are two problems with that. First, it means I’ll be duplicating code (by handling options for each individual chart that should be handled at Chart.Base
); second, it means that you (the developer) would have a hard time subclassing (for instance) Chart.Line
and adding further customizations.
The solutions
Each of these problems has its own solution. We’ll take them one at a time.
Deep‐extended options
Wait, never mind — I already fixed this one. That’s what we did last time.
When I deep‐extend one set of options onto another, I can nest my key‐value pairs to an arbitrary depth, like so:
this.options = Object.deepExtend({
width: 800,
height: 300,
bar: {
color: "#003399",
gutter: 5,
opacity: 1.0,
border: {
width: 0,
color: 'auto',
},
label: {
enabled: false,
position: 'above',
font: {
family: 'Lucida Grande',
size: '12px',
color: '#000'
},
filter: Prototype.K
}
},
/* ... */
}, options || {});
Though the options are just as complex as they were before, we’ve made them far more manageable and intuitive.
The Configurable
mixin
The solution to the inheritance problem is an updated and expanded version of the pattern I mentioned in my book. This is what it looks like:
var Mixin = {};
Mixin.Configurable = {
setOptions: function(options) {
// Prepare the `options` object for the first time if we haven't yet.'
if (!this.options) {
this.options = {};
var constructor = this.constructor;
if (constructor.superclass) {
// Build the inheritance chain.
var chain = [], klass = constructor;
while (klass = klass.superclass) {
chain.push(klass);
}
chain = chain.reverse();
// Starting with the base class, extend each set of options onto
// the `options` property.
for (var i = 0, l = chain.length; i <; l; i++) {
Object.deepExtend(this.options, chain[i].DEFAULT_OPTIONS || {});
}
}
Object.deepExtend(this.options, constructor.DEFAULT_OPTIONS || {});
}
// Finally, extend with the passed-in `options` object.
return Object.deepExtend(this.options, options || {});
}
};
In other words, it’s a mixin that adds a setOptions
method to any class that includes it. The setOptions
method first determines if it needs to create a this.options
object; if so, it determines the class hierarchy, and builds a complete set of default options for the class. (It looks to a DEFAULT_OPTIONS
constant defined on the class itself.) Then it copies over those options with whatever has been provided in the options
argument.
In Krang, I need only include this mixin in my base chart class, then define some default options that would be common to all charts.
Chart.Base = Class.create(Mixin.Configurable, {
initialize: function(container, options) {
this.container = $(container);
this.setOptions(options);
},
/* ... */
});
Chart.Base.DEFAULT_OPTIONS = {
animate: {
duration: 0.5,
easing: 'linear'
}
};
Notice how I call this.setOptions(options)
in my constructor.
Thereafter, whenever I define a subclass of Chart.Base
, I also define any default options that are unique to that chart subclass. If I override the initialize
method, I should make sure to call the superclass’s method, or else call this.setOptions
again.
Chart.Area = Class.create(Chart.Base, {
initialize: function($super, container, options) {
$super(container, options);
// (other logic)
},
/* ... */
});
/* Options relating to drawing a two-axis chart. */
Chart.Area.DEFAULT_OPTIONS = {
gutter: { top: 30, bottom: 30, left: 100, right: 30 },
grid: {
color: "#eee"
},
/* ... */
};
Chart.Bar = Class.create(Chart.Area, /* ... */);
/* Options relating only to bar charts. */
Chart.Bar.DEFAULT_OPTIONS = {
bar: {
color: '#039',
border: {
width: 0,
color: 'auto'
}
},
/* ... */
};
Redefine any options later on
The setOptions
method also gives you the opportunity to set new options on the class at any time after instantiation:
var chart = new Chart.Bar('some_element', {
bar: { color: '#def' }
});
// Later on, the chart's bars should change colors,
// perhaps to reflect new data.
chart.setOptions({
bar: { color: '#999' }
});
The killer app: customize by subclassing
In these examples, many of the options have to do with look and feel. Once you’ve decided how a chart is going to look, you’re not going to change these options very often. Must they be included every time you instantiate a chart?
var chart = new Chart.Bar('some_element', {
bar: {
color: '#c50',
border: { width: 2 }
},
font: {
family: 'Trebuchet MS'
},
/* ... */
});
If you’re going to be creating this kind of chart more than once, create a subclass instead. You need not define any new methods in the subclass, but define the DEFAULT_OPTIONS
property with whatever custom options you like.
var CustomBarChart = Class.create(Chart.Bar);
CustomBarChart.DEFAULT_OPTIONS = {
bar: {
color: '#c50',
border: { width: 2 }
},
font: {
family: 'Trebuchet MS'
},
/* ... */
};
The example code can then be shortened:
var chart = new CustomBarChart('some_element');
Epilogue
This pattern is likely too elaborate to fit the needs of something like Prototype. But I expect I’ll be using it whenever I build something that has a complex class hierarchy. Perhaps you might find it useful as well.
Comments
I’m actually really surprised that Prototype hasn’t had this in core all along.
MooTools has had the Options mixin in core since forever ago. You guys should really check out what all MooTools has instead of recreating the wheel. Or just use MooTools instead. ;)
@Thomas: As far as I can tell, MooTools’
Options
mixin doesn’t handle inheritance. Please correct me if I’m wrong.Yes, the MooTools Options mixin handles inheritance. It would be pretty useless if it didn’t ;)
Ah, I see it now. Though your “recreating the wheel” remark is a bit asinine, since in order to do it the way MooTools has we’d need to adopt a lot of other magical conventions, as you must know. Our class system is lighter and handles things a lot differently than that of MooTools.
Yes, of course the Prototype Class is different. Please forgive my asinine remark. I’m really glad that PrototypeJS is getting more power like this and I’m excited to see the awesome things that you guys come up with for 2.0.
Any chance of a more powerful Class in 2.0?
You are forgiven. :-)
Oh, we’ve got all kinds of plans for 2.0.