神刀安全网

A better way to structure D3 code

Note: This blog post is aimed at beginner-to-intermediate users of the D3 JavaScript library . If you want to skip straight to the good stuff, check out the accompanying example on bl.ocks.org .

Code written using D3 is difficult to manage. Even in a simple line chart, there will be almost a dozen important variables such as (deep breath): width, height, margins, SVG, x-scale, x-axis generator function, x-axis SVG group, x-scale, y-axis generator function, y-axis SVG group, line generator function, path element and (most important of all) a data array/object. And that’s just the bare minimum.

Because most of these need to be accessible at several points in a script, the temptation is to structure the entire thing in one giant function. Many examples on bl.ocks.org are essentially unstructured, which makes the concepts nice and clear but in real-world code can lead to an unmanageable mess.

Credit where credit is due: I was introduced to this idea by my colleague Jason French . I’ve since adopted it and use it regularly. This is my attempt at formalising it.

A solution: object-oriented programming

Think of a D3 chart or visualisation as a ‘widget’ on the page. This provides a number of benefits:

  • All of the chart’s related properties and functions are kept in a single place, both in script files and during execution.
  • Multiple instances of the chart can exist on the same page without conflicting.
  • External controls (buttons, sliders, etc) can easily modify the chart without risking breaking anything.

Here’s what we’re aiming for: being able to create the chart as if it were a Highcharts/C3-style thing.

var chart = new Chart({     element: document.querySelector('.chart-container'),     data: [         [new Date(2016,0,1), 10],         [new Date(2016,1,1), 70],         [new Date(2016,2,1), 30],         [new Date(2016,3,1), 10],         [new Date(2016,4,1), 40]     ] }); 

Which we could then modify like so:

// load in new data chart.setData( newData );  // change line colour chart.setColor( 'blue' );  // redraw chart, perhaps on window resize chart.redraw(); 

A quick introduction to constructor functions

Before we move onto the D3-specific stuff, it’s worth learning how to use constructor functions. This is a useful general-purpose pattern for JavaScript code used frequently in both JavaScript’s native functions and in third-party libraries.

You may already be familiar with:

var d = new Date(2016,0,1); 

This creates a new object stored in d , which is based on (but does not replace) the original Date object. Date is a constructor, and d is an instance of Date .

We can make our own constructor functions like so:

var Cat = function() {     // nothing here yet }; Cat.prototype.cry = function() {     return 'meoww'; } 

The .prototype bit defines an instance method , which will be available to each instance of the Cat constructor. We would call it like so:

var bob = new Cat(); bob.cry(); // => 'meoww' 

Inside of the constructor function, there is a special variable called this which refers to the current instance. We can use it to share variables between instance methods.

var Cat = function(crySound) {     this.crySound = crySound; }; Cat.prototype.cry = function() {     return this.crySound; } 

In this case, we are customising the new cat’s crySound .

var bob = new Cat('meoww'); var noodle = new Cat('miaow'); bob.cry(); // => 'meoww' noodle.cry(); // => 'miaow' 

Because each instance is a new object, this style of coding is called object-oriented programming .

There’s a lot more to constructor functions, and if you want to learn more I recommend reading CSS Tricks’ Understanding JavaScript Constructors and Douglas Crockford’s more hardcore Classical Inheritance in JavaScript .

A chart as a constructor function

Instead of Cat – which is obviously a fairly useless constructor – we could instead make a constructor for a D3 chart:

var Chart = function(opts) {     // stuff } Chart.prototype.setColor = function() {     // more stuff } Chart.prototype.setData = function() {     // even more stuff } 

Here’s a live example of a chart made using a Chart constructor. Try clicking the buttons below and resizing the window.

And here’s the corresponding JavaScript for the chart. To see how it’s being used, read the full code on bl.ocks.org .

A few things worth emphasising here:

  • Each instance method fulfils a specific purpose. Only some of them are ‘public’ and are meant to be called externally.
  • Some public methods (in this case, setColor ) don’t require redrawing the entire chart. Others, like setData , do.
  • Only variables used in other instance methods are added to this .
  • draw needs to be able to work both on initial load and on updates.
  • If you wanted to have method instances which trigged animations (for example, transitioning axes ), you would need to make draw more complex and not simple wipe the element clean each time.

Watch out for anonymous functions

The only catch with using constructor functions is that the value of this will change inside of anonymous functions – which, in D3, are everywhere.

What do I mean by that? Inside of Chart or a Chart.prototype method, this refers to the Chart instance, as expected.

var Chart = function(opts) {     // here, `this` is the chart } Chart.prototype.setColor = function() {     // here, `this` is still the chart } 

However, the value of this can change when inside an anonymous function:

Chart.prototype.example = function() {     // here, `this` is the chart     var line = d3.svg.line()         .x(function(d) {             // but in here, `this` is the SVG line element         }); } 

There’s a simple solution, which is to load this into a variable called _this :

Chart.prototype.example = function() {     var _this = this;     var line = d3.svg.line()         .x(function(d) {             // in here, `this` is the SVG line element             // but `_this` (with an underscore) is the chart         }); } 

Hardly difficult to get around, then, but worth keeping in mind. Some people prefer to use that instead of _this , which is just as good.

Rules to live by

To keep the Chart function’s responsibilities from spiralling out of control, I try to stick by these rules:

  • A chart’s appearance should not change if you call its draw() function without changing anything else. Or, to put it in programmer jargon: they must maintain state.
  • A chart does not load its own data. Data is passed to it. Ideally already formatted as nice friendly arrays.
  • A chart does not affect anything outside of its parent element. Pass in callback functions as arguments, if necessary.
  • A chart’s internal functions should each be kept short. A good length is “not taller than your screen”. (Alas, this rule is easily broken.)
  • Make a new constructor for a different type of chart ( LineChart , BarChart , etc), rather than relying on “if” statements.

It works, honest

I’ve used this pattern several times now in graphics published on WSJ.com, including ECB Meets, Euro Reacts and The World’s Safest Bonds Are Actually Wild Risks . In both cases, the constructor pattern made managing these dynamically-updating charts a breeze. The rest of the code, on the other hand…

Ps. If you enjoyed this, you might like my previous blog post on how my JavaScript coding style has changed since 2014 .

Thanks to Amelia Bellamy-Royds for providing feedback on a draft of this post.

转载本站任何文章请注明:转载至神刀安全网,谢谢神刀安全网 » A better way to structure D3 code

分享到:更多 ()

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址