d3 v4 + react + ES6: How to create axis programmatically? - d3.js

I am trying to get my head around d3 in its current version 4. Specifically: I a trying to create the x axis for a simple line chart in react and es6.
I have seen the examples of Mike Bostock and how he does it:
svg.append("g")
.attr("class", "axis axis--x")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x));
But that is neither react nor ES6.
On another site I have seen the following variant:
renderAxis() {
var node = this.refs.axis;
var axis = d3.svg.axis().orient(this.props.orient).ticks(5).scale(this.props.scale);
d3.select(node).call(axis);
}
render() {
return <g className="axis" ref="axis" transform={this.props.translate}></g>
}
This is react and ES6 but not d3 in version 4.
I could try to adopt the version 3 code to version 4 of d3 but: The d3.select bothers me extremely. I don't want to make DOM-calls when I am using react (or some other library within, like d3). React should be used to render into the DOM in the most efficient way, not to get me DOM nodes.
So, my question is:
What is the react-way to create me a simple x axis? Or, if there is yet not such an answer: What is the react way to adopt the quoted code of Mike Bostock?

After a couple years of trying to figure out d3 in React, this is the best way my team and I have found of doing axes:
const Axis = props => {
const axisRef = axis => {
axis && props.axisCreator(select(axis));
};
return <g className={props.className} ref={axisRef} />;
};
Notes
axisCreator is the function returned by axisBottom, axisLeft, etc. It is created outside the component to expose to the user the full power of the d3-axis library.
className allows the user to style the axis however he or she wants.
This example uses refs, which the React docs caution against, but it uses refs to integrate with a third-party DOM library, which is one of the reasons to use refs that the React docs call out specifically.
It is understandable that using d3's select bothers you, but it's required if you want to let d3 manipulate the DOM, and if you don't let d3 manipulate the DOM, you lose out on all the functionality of d3 (which is an incredibly cool library).

If you are using React then you are pretty much giving control of how the DOM works to React because of it's internal workings. React will render your components inside a virtual DOM tree and then figure out the difference between the DOM tree in the page were you inserted the root component and the former. It will apply the difference between the two trees so that the tree in the page will look like the virtual one.
Mixing d3 and React requires a bit of a trick. All the elements you use d3 for (nodes or attributes) should not be owned by React. Otherwise weird stuff happens. Let's say your transform attribute for the g.axis node is set by d3, that implies that you don't render it in React. You let your d3 logic have exclusive ownership of it.
Now comes the next step. If you want to use other tools to describe DOM nodes then you got to put that logic inside componentWillMount and componentDidUpdate. Basically you only render the g element inside the render() method of React and then inside those two React lifecycle handles you can change the attributes of the g element and what's inside.
I will point you towards this post that goes a bit more in detail about the hows, but I would also like to give my two cents. From my perspective and experience with these two it is best to only use d3 for it's helper functions (generic math functions). Let the rendering logic (DOM composition) be done purely in React even though there's code duplication that arises. It is way easier to maintain code that is consistent and that doesn't have two different approaches to rendering mixed together.
TL;DR; Either give attributes or nodes ownership to either React or d3 for them to work together; or my recommendation, make your own axis component in React that outputs the same DOM elements as the d3 one (or different if you desire other functionality or style) and don't use d3 for rendering.
Hope I helped.

The code for rendering axis using React directly, rather than d3's select API, is actually not that complex. react-d3-axis provides a pretty simple, ~100 loc implementation. Going this approach lets you use d3 for stuff like data transformations and scale interpolation, and use React for rendering.

Related

Using D3 axis functionality on a canvas

I'm trying to draw an axis on a canvas using D3 functionality (as exampled here https://www.tutorialsteacher.com/d3js/axes-in-d3). However every example I've seen is using an SVG while I wish to use canvas. On the other hand I didn't see any indication, in their documentation or elsewhere, that it can't be done in canvas.
Is it possible? If so, how?
In short: no, that's not possible.
D3 is pretty much render agnostic, meaning it can be used to create SVG, Canvas, other HTML elements etc. However, some modules are indeed quite specific, and that's the case of d3-axis.
If you have a look at d3-axis source code you'll see that it append SVG <path>, <line> and <text> elements for creating the axis. For instance:
path.enter().insert("path", ".tick")
Finally, here you have a discussion on this subject, where Bostock (D3 creator) abandons the idea of modifying the d3-axis module for creating axes on HTML canvas.

Is there a kind of caching? - SVG (D3) Graph remembers its old position

I'm backend developer for several years but a newbie in frontend issues.
I used "graphviz" (using d3.js) to draw an SVG graph from DOT notation.
Everything is working fine but one thing I don't get in my mind:
If I "open" another (or the same one) graph its starting position is the
same as this from the previous drawn graph even if I completely remove
the whole node content from the dom as follows:
var svg = d3.selectAll("svg");
var otherBelow = svg.selectAll("*");
otherBelow.remove();
// svg.remove();
Doing this and checking the page source the nodes below SVG are realy dropped
but drawing the new graph it has exactly the position of the previously
moved graph in "transform" attribute. Doing a work around by resetting the
position bevore solves this problem but then the problem remains for the
"moving on mousedown" capability. Then the graph immediately "jumps" to the old
ones position. But therefor I can't even get an information about somewhere
in the page source. Really the generated page code is 100% the same (with
diff tool) but has a different behaviour. Don't understand how this is possible.
So now my question: Is there a kind of caching? Or is there perhaps the
browser cache used somehow internally? How to fix this?
P.s. if I remove the SVG node itself I get a completely courious behaviour.
Then the newly drawn graph is not movable at all.
This (ugly) workaround does it for me.
// Snipped to render a new graph and reset its position
// #graph -> id of d3-graphviz div-container
document.getElementById('graph').innerHTML = ''
setTimeout(() => {
d3.select("#graph").graphviz()
.dot(yourDotData)
.render()
}, 50)
Explanation/Assumption why this works:
It seems that the deletion of the old graph with .innerHTML = ''and the creation of the new one should not happen in the same rendering phase/at the same time, therefore the timeout function.
The second part of the workaround is to use a new graphivz-instance to render the new graph, therefore the
d3.select(...) within the timeout function.
The graphviz renderer is still present on the element it was created on and has all its data still intact.
Removing the svg and then reuse the renderer is not a valid use case. Nico's answer is probably correct, but there are better ways to do it, but in order to tell you how you should do it instead I would need so see all of your code so I can understand what you really want to do.

D3 force layout in Canvas: adding & updating nodes (& links)

I am handling a large dataset to render for D3 force-layout, but realised that the performance suffers a lot using SVG. Read that rendering in canvas is much better, so am trying to understand it now.
One of the functions I need to code for is the addition of new nodes & links in the existing graph (without a refresh), as well as new links. Is there any hack that can be done to do this? Since canvas doesn't have DOM structure like SVG for me to select and update...
Am referencing to this canvas force-layout create using d3v4.
https://bl.ocks.org/mbostock/ad70335eeef6d167bc36fd3c04378048
Thanks!
There's a really good post that someone put together about using canvas called Working with D3.js and Canvas.
In short I'd recommend doing some data-binding into some dummy HTML, and using the results of this to render your output.
First create a fake DOM element that you can use
const fakeContainer = d3.select(document.createElement("custom"));
Now data-bind to it. Note, only create it the once, re-use it for re-renders.
const join = fakeContainer
.selectAll("circle")
.data(myData);
join.exit().remove();
join.enter().append("circle");
Then when it comes to rendering:
fakeContainer.each(function(d) {
// Render a canvas circle
});

how to make chart column with drill down with D3 Js

Could some one please suggest how to do column-drilldown with D3 JS library,
below example is from Hightchart,
http://www.highcharts.com/demo/column-drilldown
A complete code example for this problem is probably quite extensive, so I'll mostly keep to how you would approach it and assume you know enough of D3 to turn the concept into code.
Lets assume you have the functionality for drawing a general bar chart.
Part of that functionality would priobably be things like
Setting up your svg element and containers
Setting up your scales (one for x and one for y)
Adding axes based on the scales you have created
Adding your bars to the svg container
4.1 Make sure you have your data set available as an array
4.2 Create an enter selection for the available data and append rectelements
4.3 Update attributes like x, y for all your available bar nodes
4.4 Remove any nodes on your exit selection
Voila you have a simple bar chart. Nothing new in that and you can have a look at the code details here -> https://bl.ocks.org/mbostock/3885304
Now in order to do the drill down:
In order to avoid lots of code repetition it probably makes sense to separate the above steps into functions. So for example a setup function that just creates your svg and containers as well as your scales.
Important about the setup function is that you do not need to rerun it on drill through.
Second you will want an update function. This contains steps 3+, which you will need to rerun in order to update your charts on drill through.
One addition here would be adding functionality for updating your scale domain in the beginning (as your data changes on drill through and you want to reflect that in your scales).
Now that you have those two functions all you really need to do is:
Add a click handler to your axis labels or your bars (click on bars might be easier for now). You cans use d3's .on() function for that.
In that event you will want to subset your data by the value of the clicked bar (or get a new data set for the bar value depending on how your data is structured) and then run the update function we created above with the new data.
It could look something like this:
d3.selectAll('.bar-nodes')
.on('click', function(d) {
var updatedData = updateData(d);
updateChart(updatedData);
});
If anything is unclear some more specific questions would be good.
Hope that helps.

Drawing Non-Continuous Horizon Charts (d3)

(apologies for my lack of knowledge upfront)
Horizon Charts seem to be handled differently than area charts in D3. I've seen previous answers on non-continuous data in area charts, using line.defined. However, I'm not seeing how to accomplish that same effect in a horizon chart.
End goal: Where data is missing, skip or otherwise mask later, that point. As in this line.defined example (http://bl.ocks.org/mbostock/3035090), it shouldn't affect the curve of the path.
Ideally, the data in the json would just be a missing value, but I can add a type signifier if needed.
Is there a better approach - like adding a filled rectangle after the path was created in those spots to mask it?
I've updated the d3.horizon plugin to support setting area.defined for the area instance used internally.
For example, you can say:
var horizon = d3.horizon()
.width(…)
.height(…)
.defined(function(d) { return !isNaN(d[1]); });

Resources