What is the idiomatic way to expand a native d3 component like d3.svg.axis ()?

To visualize the time series in d3, I want to highlight the years on the axis. I achieved this by creating my own xAxis renderer, which calls the axis built-in function, and then implements my own logic to format the ticks that it displays.

screenshot Here is how I did it ( see jsbin working example ):

  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); 

It does its job, but feels wrong; and he really did not expand the axis, but only wrapped it. Ideally, my customXAxis inherit the properties of the d3 axis component, so I could do things like this:

 customXAxis.ticks(10) 

Thanks to @meetamit and @drakes for this. Here is what I got: http://bl.ocks.org/HerbCaudill/ece2ff83bd4be586d9af

+7
javascript
source share
2 answers

Yes, you can do it all. Following mbostock's suggestions here in combination with `d3.rebind ' , you get:

 // This outer function is the thing that instantiates your custom axis. // It 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 the same as what you named xAxis in your code, but it hidden // within the custom class. So instantiating customXAxis also // instantiates the base d3.svg.axis() for you, and that 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 teachings... // And, because it created from within InstantiateCustomXAxis(), you // get a fresh new instance of your custom access every time you call // InstantiateCustomXAxis(). That 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 (ie 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 simply call

 myCustomXAxis = InstantiateCustomXAxis(); 

Now you can also call

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

And, of course, the following will continue:

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

Finally

This is an idiomatic way to implement classes in d3. Javascript has other ways to create classes, for example, using a prototype object, but d3's own reusable code uses the above method, not the prototype. And in this case, d3.rebind is a way to redirect method calls from a user class to what is essentially a subclass.

+9
source share

After repeatedly inspecting and cracking the code and talking with experienced d3 people, I found out that d3.svg.axis() is a function (not an object or class), so it cannot be extended and not wrapped. Thus, to โ€œexpandโ€ it, we will create a new axis, run the selection on the axis() base to select these marks, then copy all the properties from the axis() base in one fell swoop and return this to the expanded functional 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 completely โ€œinheritsโ€ the properties of the d3 axis components. You can safely do the following:

 customXAxis .ticks(2) .scale(xScale) .tickPadding(50) .tickFormat(dateFormatter); canvas.append("g").call(customXAxis); 

* Using the @HerbCaudill template and inspired by @meetamit ideas.

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

+3
source share

All Articles