2

I was trying to write a plugin for d3.js recently and have been confused with something perhaps trivial. There is an explanation on d3's website on how to go about creating reusable charts. The pattern looks something like this (only the most important details, full code is here):

Design Pattern 1: From d3 website

function timeSeriesChart() {
  var margin = {top: 20, right: 20, bottom: 20, left: 20},
      ...
      area = d3.svg.area().x(X).y1(Y),
      line = d3.svg.line().x(X).y(Y);

  function chart(selection) {
    selection.each(function(data) {

      // Convert data to standard representation greedily;
      // this is needed for nondeterministic accessors.
      data = data.map(function(d, i) {
        return [xValue.call(data, d, i), yValue.call(data, d, i)];
      });

      // Update the x-scale.
      ...

      // Update the y-scale.
      ...

      // Select the svg element, if it exists.
      var svg = d3.select(this).selectAll("svg").data([data]);
      ...

      // Otherwise, create the skeletal chart.
      var gEnter = svg.enter().append("svg").append("g");
      ...    
  }

  // The x-accessor for the path generator; xScale ∘ xValue.
  function X(d) {      }

  // The x-accessor for the path generator; yScale ∘ yValue.
  function Y(d) {      }

  chart.margin = function(_) {
    if (!arguments.length) return margin;
    margin = _;
    return chart;
  };

  chart.width = function(_) {
    if (!arguments.length) return width;
    width = _;
    return chart;
  };

  chart.height = function(_) {
    if (!arguments.length) return height;
    height = _;
    return chart;
  };

  chart.x = function(_) {
    if (!arguments.length) return xValue;
    xValue = _;
    return chart;
  };

  chart.y = function(_) {
    if (!arguments.length) return yValue;
    yValue = _;
    return chart;
  };

  return chart;
}

I have no doubt that this pattern is robust especially because it was suggested by the creator of d3 himself. However, I have been using the following pattern for some time without problems before I came across that post (similar to how plugins are created in general):

Design Pattern 2: General way of creating plugins

(function() {
    var kg = {
        version: '0.1a'
    };

    window.kg = kg;

    kg.chart = {};

    // ==========================================================
    // CHART::SAMPLE CHART TYPE
    // ==========================================================
    kg.chart.samplechart = {

        // ----------------------------------------------------------
        // CONFIGURATION PARAMETERS
        // ----------------------------------------------------------
        WIDTH: 900,
        HEIGHT: 500,
        MARGINS: {
            top: 20,
            right: 20,
            bottom: 20,
            left: 60,
            padding: 40
        },
        xRange: d3.time.scale(),
        yRange: d3.scale.linear(),
        xAxis: d3.svg.axis(),
        yAxis: d3.svg.axis(),
        data: {},

        // ----------------------------------------------------------
        // INIT FUNCTION
        // ----------------------------------------------------------
        init: function() {
            // Initialize and add graph to the given div
            this.update();
        },

        // ----------------------------------------------------------
        // Redraws the graph
        // ----------------------------------------------------------
        update: function() {
            var parentThis = this;
            var newData = parentThis.data;

            // Continue with adding points/lines to the chart


        },
        // ----------------------------------------------------------
        // Gets random data for graph demonstration
        // ----------------------------------------------------------
        getRandomData: function() {
            // Useful for demo purposes  

        }
    };

    // ==========================================================
    // HELPER FUNCTIONS
    // ==========================================================

}());


// EXAMPLE: This renders the chart. 
kg.chart.samplechart.vis = d3.select("#visualization");
kg.chart.samplechart.data = kg.chart.samplechart.getRandomData();
kg.chart.samplechart.init();​    

I have been using Design Pattern 2 for some time without any problems (I agree it is not super clean but I'm working on it). After looking at Design Pattern 1, I just felt it had too much redundancy. For instance, look at the last blocks of code that make the internal variables accessible (chart.margin = function(_) {} etc.).

Perhaps this is good practice but it makes maintenance cumbersome because this has to be repeated for every different chart type (as seen here in a library called NVD3, currently under development) and increases both development effort and risk of bugs.

I would like to know what kind of serious problems I would face if I continue with my pattern or how my pattern can be improved or made closer to spirit of Design Pattern 1. I am trying to avoid changing patterns at this point because that would require a full rewrite and will introduce new bugs into a somewhat stable mini-library. Any suggestions?

Legend
  • 113,822
  • 119
  • 272
  • 400
  • I think the last blocks that 'make the internal variables accessible' are doing more than you think - they're more like dynamic accessors. The getter returns the property value, whereas the setter sets the property value then **returns the object itself**. This is done in order to [make the functions chainable](http://stackoverflow.com/questions/7730334/how-to-make-chainable-function-in-javascript). – chrisfrancis27 Jul 19 '12 at 20:29
  • @ChrisFrancis: Thank you. Is it possible to make *Pattern 2* chainable? – Legend Jul 19 '12 at 20:32
  • Certainly - on a very simplistic level, you can take any public method on your plugin that isn't explicitly returning a value (for instance your `init()` method) and make it `return this`. This is essentially the pattern jQuery uses for chaining functions together. – chrisfrancis27 Jul 19 '12 at 20:35

1 Answers1

4

In fact, you can find your second pattern in the d3 source code.

(function(){
...
d3 = {version: "2.9.6"}; // semver
...

d3.svg = {};
d3.svg.arc = function() {
  var innerRadius = d3_svg_arcInnerRadius,
      outerRadius = d3_svg_arcOuterRadius,
...

But the components and generators, like scale, axis, area and layout, tend to use the pattern we can call "charts as closures with getter-setter methods" or " higher-order programming through configurable functions". You can follow this discussion on the Google Group thread for rationale.

Personally, I don't like this redundancy neither, even if it is useful and fairly readable. So I generate these getters and setters automatically using a custom function:

d3.helper.createAccessors = function(visExport) {
    for (var n in visExport.opts) {
        if (!visExport.opts.hasOwnProperty(n)) continue;
        visExport[n] = (function(n) {
            return function(v) {
                return arguments.length ? (visExport.opts[n] = v, this) : visExport.opts[n];
            }
        })(n);
    }
};

Which I use like this at the end of my chart module:

d3.helper.createAccessors(chart, opts);

Where opts is the name of all the public function:

var opts = {
            width: 200,
            margin: [5, 0, 20, 0],
            height: 200,
            showPoints: true,
            showAreas: false,
            enableTooltips: true,
            dotSize: 4
        };

Here is a complete example: http://jsfiddle.net/christopheviau/YPAYz/

Biovisualize
  • 2,455
  • 21
  • 20
  • +1 Thank you! I accepted this as an answer but would you mind providing me with a complete example? :) It'll stay here for archival reasons too. Thanks again! – Legend Jul 31 '12 at 23:04
  • The example doesn't show a createAccessors function. Are you missing a ".call" when calling createAccessors? – zenw0lf Apr 04 '15 at 17:30