The “Configurable” pattern

Posted in Articles, Prototype

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