Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

screenshot 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

like image 858
Herb Caudill Avatar asked Mar 27 '15 16:03

Herb Caudill


People also ask

How to create a new default axis in D3 using SVG?

The d3.svg.axis () Function in D3.js is used to create a new default axis. This function will construct a new axis on this axis, various operations can be performed. Parameters: This function does not accept any parameter. Return Value: This function returns the created an axis. Below programs illustrate the d3.svg.axis () function in D3.js:

What is an axis in D3?

An axis is made of lines, ticks and labels. An axis uses scale, so each axis will need to be given a scale to work with. D3 provides the following functions to draw axes.

Is D3 the best choice for data visualization?

The amazing amount of variety in the gallery demonstrates the versatility of D3 when it comes to visualizing data. D3 is the way to go if you want full freedom over what your data visualizations look like. I was inspired to write this article because there aren’t many resources that exist on D3 line charts let alone D3 and React-Native.

What's new in D3 V4?

The D3 v4 API is here. According to the changelog: D3 4.0 provides default styles and shorter syntax. In place of d3.svg.axis and axis.orient, D3 4.0 now provides four constructors for each orientation: d3.axisTop, d3.axisRight, d3.axisBottom, d3.axisLeft.


2 Answers

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.

like image 176
meetamit Avatar answered Oct 25 '22 15:10

meetamit


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/

like image 34
Drakes Avatar answered Oct 25 '22 17:10

Drakes