React and D3.js
wattenberger.com
wattenberger.com
So for me, I feel like I got most of the answer from: get a ref from useRef, and use a useEffect to watch a data set and map the data to the ref using D3 itself.
If you're already a D3 expert or you have a bunch of existing D3 graphs you want to plop into an existing app, sure, you can use the ref approach. But if you want modular, reusable React components to build out an application, the approach Amelia outlines here works great.
d3 version is clear, flexible and composable but react version seems hacky and limited.
I think putting d3 in useEffect is pretty perfect way of merging d3 and React DOM operations.
What are the drawbacks?
I agree with you here but
> there is also huge performance benefits in letting only React control the DOM.
Compared to using d3 to operate on the DOM directly?[0] Do you have any benchmarks to back that up? Because in my experience, d3 is incredibly fast. Then again, maybe we're thinking about different applications? (Most of my experience is with animating stuff with d3.)
[0]: Whether or not React is used alongside with it should not matter much, I think, as long as one doesn't trigger re-rendering of the React DOM.
It also isn't re-inventing the wheel. d3 and React are based on fundamentally different models of programming (d3 is very imperative). I have done two or three pretty complex charting apps, and the only way to really break that cycle is to use the dispatcher which allows you to break up the flow of d3...you still end up with a ton of cruft, and lots of confusing constructs (you have to be very specific about what state is needed where and when...for example, some constructs in d3 require data immediately i.e. an axis, and others do not i.e. line, and some constructs require other constructs i.e. the dependency between lines and axis...clearly this conflicts with the data flow of your typical React app quite heavily).
There's a point in this article where they've replaced D3's axes code with a new React implementation. "Okay! So this is definitely more code. But that makes sense, since we're basically duplicating some of the d3 library code, in our own code base." I mean, yes. It makes sense. But it is, ceteris paribus, way worse than D3, where axes generation is kind of a fundamental basic idiom of the library, which we're here replacing with idiosyncratic new code.
I get that the code might be better in some other direction, but just as code, it's worse. The claim is that it's more readable, but: it's only more readable if you don't do a lot of D3. To me this is shades of people trying to talk about Lisp function names or APL or whatever.
I 100% agree though, it is very janky. The issues aren't limited to the points you mention: state everywhere, errors getting swallowed, problems when you update (for example, the Axis component has a useMemo that renders once on load and doesn't change...this isn't a good idea because, presumably, you have to re-render on every update), on and on. The issue isn't d3 but making components reusable within React.
d3's dispatch model fits well with React. But the data flow/dependencies just don't work with React at all (i.e. having to declare operations in a certain order). I am not 100% sure that the component library approach works. I get the idea but you need to actually tackle the problem of state and updating, which means working with the imperative model (i.e. not declaring your chart elements in React but using d3's imperative code, and hooking that into React's update cycle...for example, how would you use d3-brush with declarative code? Maybe I am missing something but it doesn't seem possible). I have no idea whether it is possible to integrate these two approaches fully...probably is but I surely don't know how (my approach so far is largely imperative, relies heavily on dispatching events, and splits state between data/chart constants/individual charts...I think it is possible to go further with hooks but I haven't had time to explore this fully...the declarative approach of actually writing svg elements and g elements seems too brittle).
That cannot be true. D3 is directly optimized for efficient updates, vs the comparatively inefficient virtual dom + diffing. Every performance oriented tool actually skips the react rendering lifecycle (even redux does that).
Unless, of course, you’re triggering re-renders of the element D3 is controlling, which should not be the case.
How do you prove that with numbers?
Regarding how I should confirm this is the case, I'd just run a controlled experiment and compare the frame rate produced by the two approaches.
1. People make good faith assumptions on performance all the time and those assumptions tend to be either wrong or wildly inaccurately. The most important part of a performance difference is not that something is faster, but by how much faster it is. Without numbers to know the size of difference the conversation becomes meaningless.
2. People, particularly in the context of favorite frameworks and the DOM, regularly speak to performance complete out of context. They believe they are comparing like items when they aren't even remotely related. When it comes to performance there are typically only two things that are measured: execution time on the CPU or human writing time. When it comes to CPU execution time you need to start from the same state and end with an identical product for the comparison to be valid. A comparison between a React component and DOM manipulation aren't comparable enough to buy you that comparison. Its like comparing the production of a hammer factory to the speed of a nail gun.
Both React and D3 can render to the DOM (D3 additionally can do a whole lot else to ease working with data). Trying to get both to control what the DOM looks like is a pain.
So your options are either to not use React (this is fine), or to let React handle the DOM and only use the data manipulation parts of D3 (this is useful when integrating with a larger React application, i.e. if you're already using it to manage your DOM).
The useRef approach is essentially the worst of both worlds.
Here's a small demo if anyone's interested: https://youtu.be/N9haFA_MwdM
The simplicity of the declarative approach, where you just specify the desired state, and it will animate everything is really great to work with.
If anyone would like to play around with it, I recommend this tutorial series on observablehq very much: https://observablehq.com/collection/@sxywu/introduction-to-d...
Had lots of fun going through it.
But either way, the real value in d3 are all the layouting functions, where you dump in your data, and it gives you a path to draw. How you then draw this data is really less important.
That isn’t correct. SVG is an XML island that composes into DOM nodes with each receiving the benefits of CSS plus the visual rendering instructions SVG provides. You can even walk the interior of an SVG in your browser developer tools and experimentally add CSS as you wish.
I have been using Echarts for the last 4 years on a near daily basis and I've yet to find a visualization I couldn't do with it.
[1] https://github.com/apache/echarts + https://github.com/hustcc/echarts-for-react
Some examples here: https://echarts.apache.org/examples
This is such a beautifully and well written article and it's probably as close as I'll get to a digestible tutorial on it, so idk why I'm having such a tough time. I am very proficient at React and have been told multiple times that I'm a top performer at FAANG so idk what's so hard about D3.
A quick example (I haven't done d3 in a few months so maybe I got it wrong):
const drawBars = (plot, records) =>
plot.selectAll('rect.bar')
.data(records)
.join('rect')
.attr('class', 'bar')
.attr('height', d => d.value)
drawBars(d3.select('#plot'), records)
// same in React
const Bars = ({ records }) => {
return (
<>
{ records.map(d => (
<rect className="bar" height={d.value} />
)) }
</>
)
}
ReactDOM.render(
<Bars records={records} />,
document.getElementById('#plot')
)But its user-facing API is hot garbage.
It's actually impressive that they've managed to come up with such an confusing and hard to track API for concepts that are themselves not fundamentally confusing. But it is what it is, it's established, it's here, so you kind of just have to bite the bullet and push through.
It is real pain with D3.js to keep the code in nicely separated modules or functions.
Where it gets more complex is data and fitting that into an imperative model with very few flow control constructs: updating, creating multiple elements at once, doing weird things like calling select on elements that don't exist yet, and for most chart types there is a topological ordering on the graph of actions required to create a chart (i.e. you have to create an axis before you create a line because you need the axis that maps your data points to x,y positions)...combined with the fact that d3 throws almost no errors or misleading errors with bad inputs, and it can definitely great pretty tricky. The saving grace is the dispatcher which you can use dispatch events, and this does make it possible to break up your code.
Also, it is nothing like React. Trying to get d3 working with React is like trying to stuff a sausage through the eye of a needle. It is a great package but it is becoming a bit outdated as the world moves towards things like React. Also, for most users it is superfluous. I still use chart.js, and only move things over when I have problems.
I wonder if is better to have the d3 function inside of useCallback() and pass data as a parameter where the function isn't re evaluated rather than in useEffect where the function is run from the top again?
This is really the central point:
>The crux of the issue is that they both want to handle the DOM.
If you want to use D3 and React together, you need to figure out how to handle this, and there are several reasonable ways to do that. Letting React handle rendering is a natural approach, and works especially well if you are embedding your SVG components in an existing React application.
But the "hand a ref off to D3 and keep React out of that element" approach can also work, especially if you are more familiar with D3 DOM manipulation already.
[1]: https://pganalyze.com/blog/building-svg-components-in-react
But to me, the code is longer, more unwieldly, more prone to error, and to top it off, it also inherits all of React's annoying idiosyncrasies. It's weird to even type this out, but Java's Swing felt more intuitive than seeing D3 on top of React.
For example:
<circle
cx="150"
cy="77"
r="40"
/>
Just the fact that this could technically be written as draw.circle(150, 77, 40) in any sane programming language should give you some pause.This is comparing apples and oranges, though.
Your example only uses the required properties that any circle will have. Once you start adding details like strokes and fills, you will probably need extra function calls in the imperative case. Those function calls will probably update some hidden state that defines the style for whatever is drawn afterwards, which means you’re going to have to either keep track of that implicit state if you want it to apply to multiple things you’re drawing or reset it after each drawing call.
If you do want to manipulate a group of related elements collectively, for example to transform or style a certain part of your image as a group rather than adjusting the drawing for each element individually, the React/SVG version allows you to specify those properties and their scope explicitly by applying them to a common parent element, and everything stays clearly structured and can be examined in browser dev tools. The equivalent with an imperative drawing library will probably require individual adjustments for the parameters to each drawing command, or again some sort of implicit state that needs to be managed.
If you want to apply more complex effects, possibly to a group of related elements, the declarative version lets you attach a powerful filter as easily as a fill colour. Again, this means the browser is doing the heavy lifting of working out how to apply the effect it specifies to whatever that combination of elements originally looks like, instead of having to deal with potentially complicated dependencies manually while drawing imperatively.
If you want any sort of interactivity with the elements in your image, with the React/SVG version you can attach event handlers and let the browser work out which events should be sent where. With imperative drawing code, you’re probably going to have to manage any interactions yourself, including reinventing all the hit testing logic you get for free with the browser.
I’m not going to argue that there’s no place for imperative graphics libraries or anything silly like that, but there are plenty of reasons you might prefer a declarative interface depending on what you need to do. Ironically, I would argue that the advantages a declarative style offers when it’s a good fit are much the same as you listed in your comment: the code typically ends up shorter, clearer, and less error-prone.
JSX vs normal JS code has nothing to do with imperative vs declarative.
<circle cx="150" cy="77" r="40" />
and draw.circle(150, 77, 40)
is that the former uses named parameters and the latter does not, and I suspect a lot of us would consider the former to be the more explicit, less error-prone choice in that case.I generally agree with the rest of your comment, but that’s just a bad way to show your point.
> for whatever reason it's really starting to take off
React is ten years old, so it's done more than start. There's a reason people prefer declarative over imperative, which is that it represents the intention of the programmer. The declarative approach can have its backend (implementation) tweaked to the nth degree and still the description (the declaration) will remain constant, and that's what we as humans want -- separation between the request ("I want X drawn") and the fulfillment ("draw two lines, one here one there") of said request. This is why there's also SwiftUI and things like QML.
> draw.circle(150, 77, 40)
Not only does that lack any kind of description of the order (is it radius, position, or position, radius?), but it isn't extensible if we later want to add more functionality (after all there are a lot of aspects of how you could draw a circle, including transparency). If you opt for putting those in different calls, like Processing, now you have (an implicit) state to deal with, and we know what happens with state -- it becomes a problem. So much so that languages like Clojure were invented specifically because state causes so many bugs.
oh I guess that is confusing, I just meant to invoke the attitude of react purists to using refs (like in the article), although imo it is sometimes the best and 'cleanest' way of expressing what you want to do
I remember trying to find the class for the element I'm manipulating imperatively in jQuery in an equally arbitrary CSS file only to realize almost none of those attributes are applied cause some other more specific selector in another file took precedence. That's obviously due to poor organization, but at least React is readable with HTML, JS, and CSS all right next to each other with very clear intent due to the declarative style.
Nothing in React requires to have one component per file. And you can do the same with jQuery or vanilla JS.
> equally arbitrary CSS file
Something makes programmers, who are usually very opinionated about their code architecture & patterns, to just throw it all away and just pile on shitty css code. It's almost as if they don't actually put any thought into it. Could it be that their Java code patterns just cargo-culting and they don't truly understand it? That can't be it, something else?
> React is readable with HTML, JS, and CSS all right next to each other
React has JSX, not HTML. And nothing requires CSS to be in the same component or file. It's still just an organizational issue.
No. It couldn't be. Because circle is an element of a tree so you'd have to pass in the parent node.
But the example you chose doesn't shock me that much. Why do you think draw.circle(150, 77, 40) is saner and more intuitive than <Circle cx="150" cy="77" r="40" />? It's fine to me if it renders a <svg> inside its parent node. Also the named parameters on the second function is a great addition for code clarity.
<Circle draw={[150, 77, 40]} />
It's a bit hideous, yes. But it illustrates the point: most people prefer what are effectively named parameters. // alternative interface
<Circle draw={{ cx: 150, cy: 77, r: 40 }} />
Since all three are of the same type and potentially within the same order of magnitude, there's no way to discern what the values represent.In any case, you're free to pass an entire collection at once:
// individual props
<Circle {...props} />
// pass all options in one prop
<Circle draw={props} />React just uses js, almost plain.
Otherwise you would have to write lengthy, nested createComponent calls.
> Animating elements out is not very straightforward in React, so let's keep all of the <circle>s rendered, and give them an opacity if they're not in the currently shown circles.
...Which is the crux of the problem I always run into trying to do complicated animations in React
We talked about it here, with mbostock too https://news.ycombinator.com/item?id=22675551
You can see a good example of what D3 is used for: https://observablehq.com/@d3/gallery
I don't see that it matters that it's not marketed as a dataviz library the way D3 is, given that it can clearly do everything D3 can (I guess aside from manipulating the DOM but why would that be necessary when every relevant browser supports canvas WebGL at this point?). So what is the issue? -... Accessibility, maybe? Seems pretty surmountable to me at first glance, if that's what it is. Or is there a performance gap? I guess that could be the case, although it would be kinda weird given that PixiJS is fast enough to handle 30+ FPS of richly graphical interaction
For anyone interested, here's an early access and practical book on visualisation with D3.js https://datacrayon.com/shop/product/visualisation-with-d3/