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()?

cereallarceny
  • 4,913
  • 4
  • 39
  • 74
  • Theres an old post written by john resig about inheritance in javascript that might serve what you are trying to do. http://ejohn.org/blog/simple-javascript-inheritance/ . Also might be worth looking for an updated/adapted version. Here is one I found with a quick google search http://stackoverflow.com/questions/15050816/is-john-resigs-javascript-inheritance-snippet-deprecated – user1318677 Sep 09 '14 at 17:00

1 Answers1

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.

cereallarceny
  • 4,913
  • 4
  • 39
  • 74
meetamit
  • 24,727
  • 9
  • 57
  • 68
  • 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 Sep 09 '14 at 19:28
  • @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. – meetamit Sep 09 '14 at 20:04
  • 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. – meetamit Sep 09 '14 at 20:09
  • 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 Sep 09 '14 at 20:11
  • It also might be worth checking out this related SO post: http://stackoverflow.com/questions/15762580/d3-reusable-chart-function-creating-multiple-charts – cereallarceny Sep 09 '14 at 20:15
  • @cereallarceny That's not something that d3 has an opinion about or a solution for. It's up to what you like using. I use Backbone a lot, so I usually extend a `Backbone.View`. Backbone views have/create/manage their DOM elements and a model for data, and they're supposed to render themselves. So inside the view's `render()` method I call something like `d3.select(this.el).datum(this.model.getData()).call(bar())`. – meetamit Sep 09 '14 at 20:18
  • The answer you linked to is recommending the same thing as bostock's example (and me too). But that answer is also not answering your question about abstracting stuff away from the end-user. – meetamit Sep 09 '14 at 20:21
  • I see. I know a couple people who have had issues with this very same thing. Perhaps it would be a good idea to have an example out there that implements these concepts well. All of the resources out there I've found have still left me just asking more questions than getting answers. – cereallarceny Sep 09 '14 at 20:26
  • This answer doesn't technically work. `var base = base();` says undefined is not a function. – cereallarceny Sep 16 '14 at 22:18
  • @cereallarceny oops... it's because `base` was both the name of the `var` and the `function`. I modified the answer, renaming the instance returned by that function to `var _base = base();`. Should work now. – meetamit Sep 16 '14 at 23:15
  • Sorry @meetamit, I'm getting an error again when calling rebind: `Cannot read property 'width' of undefined` – cereallarceny Sep 18 '14 at 01:53
  • @cereallarceny it works in [this jsFiddle](http://jsfiddle.net/meetamit/71sk8nfr/) – meetamit Sep 18 '14 at 02:35
  • You're right, but you need not return rebind: http://jsfiddle.net/71sk8nfr/1/ Nice work! – cereallarceny Sep 18 '14 at 02:40
  • 1
    @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. – meetamit Sep 18 '14 at 02:47
  • Absolutely! Only thing I can't figure out now is where the initial graph generation goes. If I'm to generate a bar chart I have to create an `svg` element (amongst others) but I only need to do this one time. Does it happen outside or inside the `my()` function? By the way, thanks for all your help @meetamit, you've been extremely helpful. – cereallarceny Sep 19 '14 at 00:43
  • Nevermind, I believe I answered my own question: http://bost.ocks.org/mike/chart/time-series-chart.js – cereallarceny Sep 19 '14 at 01:32
  • @cereallarceny Yeah, that's a way to do it. Do look closely at what happens on the line `var svg = d3.select(this).selectAll("svg").data([data]);`, because it's potentially misleading. This is indeed a way to create the svg just once, by binding it to a single element array `[data]`. However, if you changed `.data([data])` to `.data([null])`, it would still have that same effect of creating the SVG just once. (The drawing of the line and area further below would fail as a result, but NOT the SVG creation). If you can understand why, then there you have it... – meetamit Sep 19 '14 at 15:43