0

The following example is based off of Mike Bostock's reusable charts proposal.

Given two functions (bar() and pie()) which each generate a different kind of chart:

function bar() {
  var width = 720, // default width
      height = 80; // default height

  function my() {
    console.log('bar created: ' + width + ' ' + height);
  }

  my.width = function(value) {
    if (!arguments.length) return width;
    width = value;
    return my;
  };

  my.height = function(value) {
    if (!arguments.length) return height;
    height = value;
    return my;
  };

  my.render = function() {
    my();

    return my;
  };

  return my;
}

function pie() {
  var width = 720, // default width
      height = 80; // default height

  function my() {
    console.log('pie created: ' + width + ' ' + height);
  }

  my.width = function(value) {
    if (!arguments.length) return width;
    width = value;
    return my;
  };

  my.height = function(value) {
    if (!arguments.length) return height;
    height = value;
    return my;
  };

  my.render = function() {
    my();

    return my;
  };

  return my;
}

I can call these functions via a chaining method as such:

bar().width(200).render();  // bar created: 200 80
pie().height(300).render();  // pie created 720 300

Is there a way to code these where my getter and setter methods are the same? For instance, I plan on having the width() functions in each of these be the exact same. How can I make the bar() and pie() functions inherit shared functions like width(), height(), render()?

1

1 Answer 1

2

Right... The thing about this particular style of instantiating functions and hanging additional methods off of them is that the variables have to be part of the scope of the function that instantiates the returned function (bar or pie). Since those variables are internal and inaccessible outside of this scope, there isn't a way to get at them when extending the instance.

Before going further, note that your implementation is a bit off. First, where you console.log('bar created: ' + width + ' ' + height);, semantically that's incorrect. That's not actually where it's created, but rather that's where it's rendered. It's created when you call bar().

Then, when you render this chart, instead of bar().width(200).render() what you're supposed to do is, e.g.

var barChart = bar().width(200);

d3.select('svg')
  .append('g')
  .call(barChart)

You don't need render(). Instead, the body of my() is your render. But, as per mbostocks suggestion, my() should take a d3 selection as param, which in this example would be the g element being appended. This is right of of the reusable charts tutorial you linked to; re-read it and you'll see what I mean.

Finally, in answer to your actual question. The way I do it is using d3.rebind. First you have to create a common base class that has the internal width variable and the width() getter/setter, like so:

function base() {
  var width;

  function my(selection) {
    console.log('base: ' + width);
  }

  my.width = function(value) {
    if (!arguments.length) return width;
    width = value;
    return my;
  };

  return my;
}

Next, you want to make bar (and pie) essentially extend base, like this:

function bar() {
  var _base = base(); // instantiate _base, which has width() method

  function my(selection) {
    // In here, if you want to get width, you call `_base.width()`
    // and, if there's some shared rendering functionality you want
    // to put in base, you can run it with `selection.call(_base)`
  }

  d3.rebind(my, _base, 'width'); // make width() a function of my
  return my;
}

The last line, copies width() to my so that you can call bar().width(). Here's a fiddle that works.

Sign up to request clarification or add additional context in comments.

17 Comments

Great answer! Mike Bostock's example maybe seems a bit unintuitive then to create a base function and have various graph types inherit... the answer you provide is correct, but would you suggest a different pattern instead?
@cereallarceny Despite pointing out a drawback, I like this d3/mbostock pattern and I use it myself a lot. But normally, I use it just when writing reusable charts with d3 — not for other stuff, like data models or whatever. It works well in d3 because of d3's selection.call(myInstance) idiom and the fact that myInstance is a function with its own getters/setters.
Also, in the d3 source code there are examples of d3.rebind being used to extend instances. d3.layout.hierarchy is a base class of d3.layout.treemap, d3.layout.cluster and other layouts.
I agree. However, my only remaining issue is where you generate the initial d3 graph. Is there a way to keep this generation abstracted away from the end-user? For instance, I don't want people to have to type: d3.select('svg').append('g').call(barChart). Am I missing something here? I understand now that my() should only be used to update existing graphs, but where in your example of base() and bar() does the initial d3 code go?
@cereallarceny True. Though it works either way, because my == d3.rebind(my, _base, 'width'). The problem was that I had forgotten to include return my; in base(). Thanks for editing the post.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.