简体   繁体   中英

What's the idiomatic way to extend a native d3 component like d3.svg.axis()?

For a time series visualization in d3, I want to highlight years on the axis. I've accomplished this by making my own xAxis renderer, which invokes the native axis function and then implements my own custom logic to format the ticks that it renders.

截图 This is how I've done it ( see working example on jsbin ):

  xAxis = d3.svg.axis()
    .scale(xScale)

  customXAxis = function(){
    xAxis(this);
    d3.selectAll('.tick', this)
      .classed("year", isYear);
  };

  ...

  xAxis.ticks(10);

  xAxisElement = canvas.append("g")
    .classed("axis x", true)
    .call(customXAxis);

This gets the job done, but feels wrong; and it hasn't really extended the axis, it's only wrapped it. Ideally my customXAxis would inherit the properties of d3's axis component, so I would be able to do things like this:

customXAxis.ticks(10)

Thanks to @meetamit and @drakes for putting this together. Here's what I've ended up with: http://bl.ocks.org/HerbCaudill/ece2ff83bd4be586d9af

Yep, you can do all that. Following mbostock's suggestions here in conjunction with `d3.rebind' you get:

// This outer function is the thing that instantiates your custom axis.
// It's equivalent to the function d3.svg.axis(), which instantiates a d3 axis.
function InstantiateCustomXAxis() {
  // Create an instance of the axis, which serves as the base instance here
  // It's the same as what you named xAxis in your code, but it's hidden
  // within the custom class. So instantiating customXAxis also
  // instantiates the base d3.svg.axis() for you, and that's a good thing.
  var base = d3.svg.axis();

  // This is just like you had it, but using the parameter "selection" instead of
  // the "this" object. Still the same as what you had before, but more
  // in line with Bostock's teachings...
  // And, because it's created from within InstantiateCustomXAxis(), you
  // get a fresh new instance of your custom access every time you call
  // InstantiateCustomXAxis(). That's important if there are multiple
  // custom axes on the page.
  var customXAxis = function(selection) {
    selection.call(base);

    // note: better to use selection.selectAll instead of d3.selectAll, since there
    // may be multiple axes on the page and you only want the one in the selection
    selection.selectAll('.tick', this)
      .classed("year", isYear);
  }

  // This makes ticks() and scale() be functions (aka methods) of customXAxis().
  // Calling those functions forwards the call to the functions implemented on
  // base (i.e. functions of the d3 axis). You'll want to list every (or all)
  // d3 axis method(s) that you plan to call on your custom axis
  d3.rebind(customXAxis, base, 'ticks', 'scale');// etc...

  // return it
  return customXAxis;
}

To use this class, you just call

myCustomXAxis = InstantiateCustomXAxis();

You can now also call

myCustomXAxis
  .scale(d3.scale.ordinal())
  .ticks(5)

And of course the following will continue to work:

xAxisElement = canvas.append("g")
  .classed("axis x", true)
  .call(myCustomXAxis);

In summary

That's the idiomatic way to implement classes within d3. Javascript has other ways to create classes, like using the prototype object, but d3's own reusable code uses the above method — not the prototype way. And, within that, d3.rebind is the way to forward method calls from the custom class to what is essentially the subclass.

After a lot of code inspection and hacking, and talking with experienced d3 people, I've learned that d3.svg.axis() is a function (not an object nor a class) so it can't be extended nor wrapped. So, to "extend" it we will create a new axis, run a selection on the base axis() to get those tick marks selected, then copy over all the properties from the base axis() in one fell swoop, and return this extended-functionality version.

var customXAxis = (function() {
  var base = d3.svg.axis();

  // Select and apply a style to your tick marks
  var newAxis = function(selection) {
    selection.call(base);
    selection.selectAll('.tick', this)
      .classed("year", isYear);
  };

  // Copy all the base axis methods like 'ticks', 'scale', etc.
  for(var key in base) {
    if (base.hasOwnProperty(key)) {
       d3.rebind(newAxis, base, key);
    }
  }

  return newAxis;
})();

customXAxis now fully "inherits" the properties of d3's axis component. You can safely do the following:

customXAxis
.ticks(2)
.scale(xScale)
.tickPadding(50)
.tickFormat(dateFormatter);

canvas.append("g").call(customXAxis);

*With the help of @HerbCaudill's boilerplate code, and inspired by @meetamit's ideas.

Demo: http://jsbin.com/kabonokeki/5/

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM