Replacing jQuery with D3
blog.webkid.io
blog.webkid.io
- d3.selectAll is more verbose than $() but in my opinion, that's not a bad thing as I try to minimize DOM selections anyways;
- transitions are another area where D3 really shines, for cases where you don't/can't use CSS transitions, like displaying a discreet popup status message.
- d3 data binding is oh so convenient, and that's where the real magic happens. Combined with a queueing system like postal.js, we pretty much have the same thinking model as React. The DOM is now an expression of the data, and D3's joins figure out what DOM elements need to be updated. It's slightly lower level than React, in that you specify what happens on enter/update/exit, but in terms of efficiency, it seems great (disclaimer: we haven't run benchmarks);
- if you happen to like CoffeeScript, D3 turns into a very elegant DSL that is consistent for DOM editing, styling and event handling. Here's a simple lightbox modal dialog:
lightbox.shell = d3.select "body"
.div "#shadebox"
.style "opacity", 0
.on "click", ->
lightbox.removeLightbox()
d3.event.stopPropagation()
lightbox.body = lightbox.shell.append "div"
.attr "id", "lightbox"
.on "click", -> d3.event.stopPropagation()
lightbox.showLightbox()
return lightbox.body
- we've added a `.div` function that's shorthand for d3.append and automatically adds an id and/or class, to feel a bit more like Jade;- one downside we found with D3 is that we can't just grab any jquery plugin and throw it in our app.
It's great, but it has limitations. It only ever binds to a single `__data__` property on the DOM nodes (making it very very difficult to associate multiple sets of data through a single DOM tree) and which needs to be carefully re-bound to each sub-node if you wish to substitute the objects with new ones. And acting on the data (basically two-way binding) and then updating the d3 selection has to be done very carefully too.
I've run into many subtle errors that stem from this fairly primitive method of binding.
It probably could use some additional abstractions to make replaying updates on object replacement/modification easier.
To "substitute the objects with new ones", do another join, pass a key function as the second argument if necessary, and use the enter/update/exit pattern.
And key functions are non-trivial to create if 1. you use d3.nest[2] (not to be confused with nested selections) 2. you modify the data in a way that changes the grouping 3. you try to match existing nodes back to the changed groupings. Key-based computation is insufficient for dynamically matching DOM nodes to changing sets.
Those two things aren't showstoppers. They just make the data-binding fairly brittle.
The thing that it's bound to a non-namespaced __data__ property is more problematic in my opinion since you always have to pay attention to not overwrite the datum with a different selection on any DOMnode, otherwise you will break callbacks that rely on the data. It basically requires a 1:1 correspondence for DOM to data on the whole subtree.
This makes decomposing the selections into separate steps difficult. I.e. building a base tree and then populating the leaves with a separate data set can easily end up overwriting the data bindings of the base tree. Again, it's possible to avoid these pitfalls but if __data__ had been namespaced it would be far less hazardous.
[1] https://github.com/mbostock/d3/issues/2034 [2] https://github.com/mbostock/d3/wiki/Arrays#-nest
jQuery
$('.foo').find('.bar');
D3 d3.selectAll('.foo').selectAll('.bar');
The D3 API consistency is so juicy and delicious.$('.foo').append('<div class="bar" data-selected="true"/>');
in d3 I have to go the long way:
d3.select('.foo') .append('div') .classed('bar', true) .attr('data-selected', true);
I have recently worked a lot with this plugin https://github.com/gka/d3-jetpack which includes some very nice helper functions!
But if you can do that...
d3.select('.foo').append(d3.functor(node));
$('.foo .bar');
But I would assume you could do the same in D3? $.find('.foo').find('.bar')
It's just a different way of invoking it. The short-hand is more convenient since it accepts a selector as well as a plain DOM node and converts that to a jQuery elements. This makes things very easy and idiomatic.And don't even get me started on how jQuery hijacks `this` in callbacks...
I can't understand though why you would claim to dislike jQuery providing you a useful 'this'. You prefer to receive the window object?
Useful but poorly named isn't useful.
> I can't understand though why you would claim to dislike jQuery providing you a useful 'this'.
I prefer programming language libraries to work according to the semantics of the underlying programming language. Is that so wrong?
Nearly every programming language in existence has some feature(s) that you can shoot yourself in the foot with. Does that mean you should do it?
I think D3 and JQuery differ more with the dynamic data binding. In D3 you can bind a function to an DOM value.
The main reason I still use jQuery is that they unify their promise type with their ajax. It's easy to create code that works with any promise, ajax or otherwise.
d3.json = require('bluebird').promisify(d3.json);
[0]: https://github.com/petkaantonov/bluebirdNo, my main quibble here is that d3.xhr() is still short on convenience and flexibility as compared to $.ajax(). (For instance: you have to build GET URLs manually? No .contentType() as a shorthand for the header? No automatic JSON.stringify() when POSTing application/json data? No HTTP basic auth support? etc.)
[SPOILER: it's not a framework, it's simply native JS5]
What is questionable is "both JQuery and D3 are vanilla js". JQuery is a JS framework that is usually not considered "native". On modern browser it nowadays calls newer DOM API functions like one can do with vanilla/standard JS5. D3 is JS library that is mainly based around SVG, it's usually not considered "native", it calls SVG and DOM APIs like one can do in JS5. Each layer/framework on top means an additional overhead in combined JS file size.
"The Document Object Model (DOM) is a programming interface for HTML, XML and SVG documents. [...] Though often accessed using JavaScript, the DOM itself is not a part of the JavaScript language, and it can be accessed by other languages, though this is much less common."
https://developer.mozilla.org/en-US/docs/Web/API/Document_Ob...
It bothers me that I'm already using superagent for ajax stuff in a React project, and if I want to use D3 I will need to live with that extra bloat.
D3 is really amazing, and I'll be integrating it anyway.
$ = d3.selectAll.bind(d3);
Since `this` in the `selectAll` will refer to whatever is before the period. When we reassign to `$` in `$ = d3.selectAll`, we lose that. Silly, isn't it?I think it would be harder to use from the perspective of a new developer trying to learn D3. selectAll already has way too many responsibilities in my opinion, making it totally opaque what d3(...) really does. OTOH I guess it's kind of the same with jQuery(..), since it can do both selectors as well as create new Elements.
D3 is one of those libraries that I really admire from a conceptual point of view, but boy do I wish the API was more newbie-friendly.
It's rather indicative of an API that has room for improvement when you start noticing just how many tutorials, videos and books that have to explain D3.selectAll. "Well, you see — sometimes it doesn't really select anything, instead it does this other thing"
Not to mention the source code. Very sparsely commented.
Should we design our tools so that you can pick them up immediately or should we design them so they give us as much power as possible? I, personally, would prefer Englebart's violin.
A thoroughly false dichotomy.
It's not either or, it's varying degrees of both.
D3 is a powerful _drawing_ library and I've used it for totally custom visualizations where no canned tool would suffice. I think there's an un- or only partially-filled niche for more customizable _charting_ library that is more user-friendly.
d3.selectAll('.foo').selectAll('.bar');
$.find('.foo').find('.bar');
are consistent, whereas d3('.foo').selectAll('.bar')
$('.foo').find('.bar')
are not. IMHO, d3's API is cleaner.That said, the way D3 does selections/grouping is much more complex than jQuery. Sure, on the surface level they do the same thing (selecting DOM nodes), but where D3 diverges is the ability to represent DOM nodes as data you tie to it. So your array and your <circle> elements are 1:1, or perhaps your array elements have sub-arrays...D3 can easily tie those sub-arrays into sub-DOM-elements, all while letting you address different pieces of the tree easily.
They are two different beasts. One is a DOM library, one is a visualization library. There is really not a whole lot of intersection beyond some basic operations.
Event listeners in d3 are not delegated listeners. Implementing this feature actually is not an easy matter as you need to preserve the d3 data binding context and also attach to the nearest svg element because svg events don't bubble through it.