Coding simpler accessors in "Reusable Charts"

200 views
Skip to first unread message

Phrogz

unread,
Mar 16, 2012, 3:33:08 PM3/16/12
to d3...@googlegroups.com
In Towards Reusable Charts Mike proposes that charts be functions with accessor methods used for getting and setting values. The implementation shown to accomplish this uses closures over local variables, e.g.

function chart(){
  var width = 720; // default width
  function my() {
    // generate chart here, using `width`
  }
  my.width = function(value){
    if (!arguments.length) return width;
    width = value;
    return my;
  }; 
}

I initially followed this implementation for my own chart, but found the amount of tedious, nearly-identical code needed for each accessor to be annoying. In JavaScript, you can't dynamically access local variables by name, which forces us to re-write the same accessor boiler plate code with different variable names for each accessor property we want. If we instead store these properties on the function object (as the article did earlier) but then also provide accessor methods we retain the power of method chaining while also allowing our accessors to be metaprogrammed for us. In code:

function plotter(){
  makeAccessors(["width","height","css"]);
  go._height = 42; // set a default
  return go;
 
  function go(selection){
    console.log(go._width,go._height,go._css);
  }
 
  function makeAccessors(names){
    names.forEach(function(n){
      go[n]=function(v){
        return arguments.length ? (go["_"+n]=v,this) : go["_"+n];
      };
    });
  }
}

// Spoof a D3 selection
var selection = {
  call:function(f){ f.call(this,this); return this; }
};

var p1 = plotter().width(17);
var p2 = plotter().height(99).css(function(){}); // Yay method chaining!

selection.call(p1);                              // Yay default values!
//-> 17 42 undefined

selection.call(p2);                              // Overridden and complex values!
//-> undefined 99 function (){}

selection.call(p1);                              // Yay preserving values!
//-> 17 42 undefined

The only 'downside' to this technique is that one must write "go._width" instead of "width" in the implementation of the method. I find this to actually be slightly more self-documenting, keeping configuration parameters clearly separated and visually distinct from local variables.

Phrogz

unread,
Mar 16, 2012, 3:39:24 PM3/16/12
to d3...@googlegroups.com
One followup: if for some reason you find having the properties publicly exposed on the object to be 'dangerous', or you don't like the underscores, you can alternatively store all parameters in a local variable and still have your accessors created for you:

function plotter(){
  var opts={};
  makeAccessors(["width","height","css"]);
  opts.height = 42; // set a default
  return go;
  function go(selection){
    console.log(opts.width,opts.height,opts.css);
  }
  function makeAccessors(names){
    names.forEach(function(n){
      go[n]=function(v){ return arguments.length?(opts[n]=v,this):opts[n]; };
    });
  }
}

And then you just have to use opts.width in your code to access the default or user-set value.

Hrm...I may change my own implementation to use this now. :)

Phrogz

unread,
Mar 16, 2012, 3:44:44 PM3/16/12
to d3...@googlegroups.com
One more variation/improvement: if you enumerate all your accessor properties and their defaults first, you don't even need to explicitly name them. I now like this one the best:

function plotter(){
  var opts={       // enumerate all user-settable values and defaults
    width  : null,
    height : 42,
    css    : null
  };
  makeAccessors();
  return go;
 
  function go(selection){
    // your real plotting code here 
    console.log(opts.width,opts.height,opts.css);
  }
  function makeAccessors(){
    for (var n in opts){
      if (!opts.hasOwnProperty(n)) continue;

Rich Morin

unread,
Mar 16, 2012, 3:53:35 PM3/16/12
to d3...@googlegroups.com
On Mar 16, 2012, at 12:44, Phrogz wrote:

> One more variation/improvement: if you enumerate ...

sweet!

-r

--
http://www.cfcl.com/rdm Rich Morin
http://www.cfcl.com/rdm/resume r...@cfcl.com
http://www.cfcl.com/rdm/weblog +1 650-873-7841

Software system design, development, and documentation


Mike Bostock

unread,
Mar 16, 2012, 4:36:03 PM3/16/12
to d3...@googlegroups.com
Hmm. I agree it's a bit tedious. Consider this approach:

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

Then, you can define properties like so:

chart.foo = property(42);
chart.bar = property(true);

And you can access them to render the chart like so:

var foo = chart.foo(),
bar = chart.bar();

Mike

Mike Bostock

unread,
Mar 16, 2012, 4:38:06 PM3/16/12
to d3...@googlegroups.com
Also, you could change it to `return this` rather than `return chart`,
which would make it reusable across implementations:

function property(value) {
return function(_) {
if (!arguments.length) return value;
value = _;

return this;
};
}

Mike

Bob Monteverde

unread,
Mar 16, 2012, 5:00:40 PM3/16/12
to d3...@googlegroups.com
I don't find them too tedious, one thing I like is you can have certain checks, etc. in the set code.  Like if you never want something to have a negative width, make sure the width is at least the size of the margins.. etc.  I believe there are some examples doing similar checks.  Of course I'm sure there's ways to accomplish this while having some form of a accessor building function, but the technique describes in Towards Reusable charts definitely works for me.. plus its easy enough to copy and paste 5 lines of code, and edit 3 variable names.

Phrogz

unread,
Mar 16, 2012, 5:38:53 PM3/16/12
to d3...@googlegroups.com

On Friday, March 16, 2012 2:36:03 PM UTC-6, Mike Bostock wrote:
Consider this approach:

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

Then, you can define properties like so:

    chart.foo = property(42);
    chart.bar = property(true);

Yum, that is pretty nice.
 

And you can access them to render the chart like so:

      var foo = chart.foo(),
          bar = chart.bar();

…but I don't personally like having to re-type them as above, or invoke the accessor method every time the value is desired. Still, fun with metaprogramming :)
Message has been deleted
Message has been deleted

Phrogz

unread,
Mar 16, 2012, 7:45:25 PM3/16/12
to d3...@googlegroups.com

On Friday, March 16, 2012 1:44:44 PM UTC-6, Phrogz wrote:
  function makeAccessors(){
    for (var n in opts){
      if (!opts.hasOwnProperty(n)) continue;
      go[n]=function(v){ return arguments.length?(opts[n]=v,this):opts[n]; };
    }
  }

Whoops; without a closure that doesn't work so well. :)

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

Miles McCrocklin

unread,
Mar 16, 2012, 8:12:59 PM3/16/12
to d3...@googlegroups.com
I deleted two messages that didn't work because I don't want someone to search the group and find poorly written code. I agree with Bob though, most of the time it seems like it might be beneficial to do the checking within the setter.
Reply all
Reply to author
Forward
0 new messages