I'm working on a d3 chart (Multiple line chart).
I'm trying to represent a stock prediction, so basically the chart contains two lines: stock values line and an other one for my prediction.
The prediction is monthly, all days of month are represented in the chart.
In order to choose the month I have added a dropdown menu.
I appended a circle on each daily data, and works well for the first time. When user tries to change the month, the old circles are not updated, but the new ones are added.
Follow the code about circles:
topicEnter.append("g").selectAll("circle")
.data(function(d){return d.values})
.enter()
.append("circle")
.attr("r", 5)
.attr("cx", function(dd){return x(dd.date)})
.attr("cy", function(dd){return y(dd.probability)})
.attr("fill", "none")
.attr("stroke", "black");
I have done a fiddle to understand better the situation and in order to show code.
What am I missing here? Why don't the circles update themself with the lines?
To solve the issue about circles not updating you can do the following:
function update(topics) {
// Calculate min and max values with arrow functions
const minValue = d3.min(topics, t => d3.min(t.values, v => v.probability));
const maxValue = d3.max(topics, t => d3.max(t.values, v => v.probability));
y.domain([minValue, maxValue]);
x2.domain(x.domain());
y2.domain(y.domain());
// update axes
d3.transition(svg).select('.y.axis').call(yAxis);
d3.transition(svg).select('.x.axis').call(xAxis);
// Update context
var contextUpdate = context.selectAll(".topic").data(topics);
contextUpdate.exit().remove();
contextUpdate.select('path')
.transition().duration(600)
.call(drawCtxPath);
contextUpdate.enter().append('g') // append new topics
.attr('class', 'topic')
.append('path').call(drawCtxPath);
// New data join
var focusUpdate = focus.selectAll('.topic').data(topics);
// Remove extra topics not found in data
focusUpdate.exit().remove(); //remove topics
// Update paths
focusUpdate.select('path')
.transition().duration(600)
.call(drawPath)
// Update circles
var circlesUpdate = focusUpdate
.selectAll('.topic-circle')
.data(d => d.values);
circlesUpdate.exit().remove();
circlesUpdate.transition().duration(600).call(drawCircle);
circlesUpdate.enter().append('circle').call(drawCircle);
// Add new topics
var newTopics = focusUpdate.enter().append('g') // append new topics
.attr('class', 'topic');
// Add new paths
newTopics.append('path').call(drawPath)
// Add new circles
newTopics.selectAll('.topic-circle')
.data(d => d.values)
.enter()
.append('circle')
.call(drawCircle);
}
With these helper functions to reduce code duplication:
function drawCtxPath(path) {
path.attr("d", d => line2(d.values))
.style("stroke", d => color(d.name));
}
function drawPath(path) {
path.attr("d", d => line(d.values))
.attr('clip-path', 'url(#clip)')
.style("stroke", d => color(d.name));
}
function drawCircle(circle) {
circle.attr('class', 'topic-circle')
.attr('clip-path', 'url(#clip)')
.attr("r", d => 5)
.attr("cx", d => x(d.date))
.attr("cy", d => y(d.probability))
.attr("fill", "none")
.attr("stroke", "black");
}
I think there are some additional issues in your code, when you select the same month twice you get an error, we can fix that by doing the following:
d3.select('#month_chart').on("change", function() {
// Get selected value of the select
var month = this.options[this.selectedIndex].value;
// Since you have hardcoded data we need to return a new array
// This is why if you select the same month twice your code breaks
// since parseDate will fail since the data will be already parsed
// the second time
var monthData = get_monthly_data(month).map(d => {
return {
date: parseDate(d.date),
predicted_bool: d.predicted_bool,
target: d.target
};
});
// Lets use arrow functions!
var keys = d3.keys(monthData[0]).filter(k => k !== 'date');
color.domain(keys);
// More arrow functions!
var topics = keys.map(key => {
return {
name: key,
values: monthData.map(d => {
return {
date: d.date,
probability: +d[key]
};
})
};
});
x.domain(d3.extent(monthData, d => d.date));
update(topics);
});
// A good ol' switch
function get_monthly_data(month) {
switch (month) {
case 'gennaio':
return data_1;
case 'febbraio':
return data_2;
case 'marzo':
return data_3;
default:
return data_1;
}
}
Working jsfiddle:
https://jsfiddle.net/g699scgt/37/
The problem is your update cycle, but there are a good number of examples of the enter, update, exit process in d3.
But essentially:
You append a new g element for each batch of circles, which means you have an empty selection (no circles are in that g yet) each time and each data point is appended (and none are removed). You don't need this extra append. Take a look at the DOM structure on each append in your existing code.
Your enter() selection returns new elements - not modified elements. So if your total number of elements remains the same you will have an empty enter() selection. You'll want to update existing elements separately (alternatively, remove them all and append them all every time).
You'll want something closer to this:
// set the data
circles = topic.selectAll("circle")
.data(function(d){return d.values});
// update existing circles
circles.attr("cx", function(dd){return x(dd.date)})
.attr("cy", function(dd){return y(dd.probability)});
// add new circles
circles.enter()
.append("circle")
.attr("r", 5)
.attr("cx", function(dd){return x(dd.date)})
.attr("cy", function(dd){return y(dd.probability)})
.attr("fill", "none")
.attr("stroke", "black");
// remove excess circles
circles.exit().remove();
You'll likely also want to revise the lines that append the lines to reflect the enter, update, exit cycle in d3.
Related
I have a problem with General Update Pattern in d3.js. Every time (even if the same data is coming) all my elements go to both enter and exit selections. Update is empty.
This is my function :
function render() {
const test = svg
.selectAll('.test')
.data(data, d => d.name);
console.log(test)
const onEnter = test
.enter()
.append('text')
.attr('class', 'test')
.attr('x', 50)
.attr('y', 100)
.attr('fill', 'black')
.datum(d => d.values)
.text(d => {
return d[0].value
})
.attr('y', (_, i) => 50 + 25 * i)
}
I discovered that the datum bind is the problem (without it everything works as expected), but I have no idea how to fix it and why it causes a problem. I need to use datum, the real case is much bigger.
You can test it on this jsfiddle. When you click update button you can see in console enter and exit selections with 2 elements, while I would expect 2 elements to be in _groups array.
Why binding datum causes that General Update Pattern works like that?
For context: I am running D3 inside a React application, and tying the D3 update pattern to the React lifecycle methods.
The Problem:
When I const selection = select(".svg-circle-node-class").data(nodeArray), D3 always provides me EVERY node in the array, EVERY TIME the function is run, regardless of whether new Nodes have been added to the array or not. I expect the function selection.enter() to provide me with NEW nodes, and selection.exit() to provide me the non-existent nodes for removal, and for any operations done directly on this selection without using enter() should provide me with all the remaining nodes, like so:
This issue leads me to be unable to differentiate between new and old items and this result in me always re-appending new SVG elements, therefore duplicating everything every time there's a state change. I want to just update values of these elements instead.
My specific situation is the following: I have an implementation of a Graph, structured like so:
class Graph {
nodes: Node[];
}
class DirectedEdge {
from: Node;
to: Node;
}
class Node {
x: number;
y: number;
edges: DirectedEdge[];
id: string; // unique for each node
}
And in my react component, I am tracking the state of the graph by saving an instance of it in the component state, like so:
const n1, n2: Node = ...;
interface IState {
g: Graph = new Graph(n1, n2), // constructor connects two graphs with an edge
degree: number // a parameter to change some shapes
}
And I tied the initialization of the chart to the componentDidMount lifecycle method, and here's the function for doing that:
/** Once HTML elements have loaded, this method is run to initialize the SVG elements using D3. */
private initializeGraph(): void {
const mainGroup = select(this.svgElement.current)
.append("g")
.attr("id", "main");
// append nodes svg group
this.nodeElements = mainGroup.append("g")
.attr("id", "nodes")
.selectAll<SVGCircleElement, Node>(".directed-graph-node")
.data<Node>(this.state.g.nodes, _ => _.id);
// append edges svg group
this.edgeElements = mainGroup.append("g")
.attr("id", "edges")
.selectAll<SVGPathElement, DirectedEdge>(".directed-graph-edge")
.data<DirectedEdge>(this.state.g.nodes.flatMap(_ => _.edges), _ => _.id);
}
And I tied an updateGraph function to the componentDidUpdate lifecycle function, causing it to be called every time there's a change to the state (i.e., in this case, caused by the parameter 'degree' changing. But I want to be able to update the (x,y) positions of each node at every update as well).
private updateGraph(): void {
if (!this.nodeElements || !this.edgeElements) return;
console.log("before", this.nodeElements, this.edgeElements);
// select nodes & edges
const graphNodes = this.nodeElements
.data<Node>(this.state.g.nodes, _ => _.id);
const graphEdges = this.edgeElements
.data<DirectedEdge>(this.state.g.nodes.flatMap(_ => _.edges), _ => _.id);
console.log("after", graphNodes, graphEdges);
// update nodes with their current position
graphNodes.attr("cx", node => node.x)
.attr("cy", node => node.y);
// add newly added nodes if any
graphNodes.enter()
.append("circle")
.attr("class", ".directed-graph-node")
.attr("stroke", "steelblue")
.attr("cx", node => node.x)
.attr("cy", node => node.y)
.attr("r", 2.5)
.call(drag<SVGCircleElement, Node>());
// remove nodes that don't exist anymore
graphNodes.exit().remove();
// add ID of node
graphNodes.append("text")
.attr("x", node => node.x)
.attr("y", node => node.y)
.attr("font-size", 3)
.text(d => d.id);
// function to draw edge
const drawEdge = (edge: DirectedEdge, context: Path): Path => {
const {from, to} = edge;
context.moveTo(from.x, from.y);
// some math is done and some lines are drawn here
return context;
};
// add new edges
const edgeGroupOnEnter = graphEdges.enter()
.append("g")
.attr("class", ".directed-graph-edge");
// remove old unused edges
graphEdges.exit().remove();
// update path of unchanged edges
graphEdges.attr("d", edge => drawEdge(edge, path()).toString())
// draw new edges
edgeGroupOnEnter.append("path")
.attr("stroke", "red")
.attr("fill", "none")
.attr("d", edge => drawEdge(edge, path()).toString());
// add midpoint for debug
edgeGroupOnEnter.append("circle")
.attr("stroke", "red")
.attr("cx", edge => edge.from.midpoint(edge.to).x)
.attr("cy", edge => edge.from.midpoint(edge.to).y)
.attr("r", 0.5);
}
Does anyone have a clue where I am messing up?
In D3, you have to perform the data join again if you want to get the new enter/update/exit selections. What happens in your code is:
You do a data join once in your initialize function (for each of your chart elements). That data join marks every node as new and returns every node, and you then cache those results.
In your update function, you use those cached results every time.
Instead, perform the data join on update, every time the graph changes, instead of on initialize. An example with nodeElements:
private initializeGraph(): void {
const mainGroup = select(this.svgElement.current)
.append("g")
.attr("id", "main");
// append nodes svg group
this.nodeElements = mainGroup.append("g")
.attr("id", "nodes")
}
private updateGraph(): void {
// select nodes & edges
const graphNodes = this.nodeElements
.selectAll<SVGCircleElement, Node>(".directed-graph-node")
.data<Node>(this.state.g.nodes, _ => _.id);
// update nodes with their current position
graphNodes.attr("cx", node => node.x)
.attr("cy", node => node.y);
// add newly added nodes if any
graphNodes.enter()
.append("circle")
.attr("class", ".directed-graph-node")
.attr("stroke", "steelblue")
.attr("cx", node => node.x)
.attr("cy", node => node.y)
.attr("r", 2.5)
.call(drag<SVGCircleElement, Node>());
// remove nodes that don't exist anymore
graphNodes.exit().remove();
}
As you can see, this pattern is pretty heavy-handed. We can use selection.join() instead. It allows us to remove the duplicate code on enter and update and be less heavy.
private updateGraph(): void {
const graphNodes = this.nodeElements
.selectAll<SVGCircleElement, Node>(".directed-graph-node")
// data() join()
.data<Node>(this.state.g.nodes, _ => _.id)
.join(
enter => enter.append("circle")
.attr("class", ".directed-graph-node")
.attr("stroke", "steelblue")
.attr("r", 2.5)
.call(drag<SVGCircleElement, Node>()),
update => update,
exit => exit.remove();
)
// enter + update past this point
.attr("cx", node => node.x)
.attr("cy", node => node.y)
}
I understand that merge can be used to combine enter and update selections in d3 v4, as in the simple example here: https://bl.ocks.org/mbostock/3808218.
I have a scatter plot in which multiple variables are displayed on a shared x-axis, for different groups selected by a dropdown box. When a new group is selected, the overall set of datapoints is updated, with points for each variable added like this:
.each(function(d, i) {
var min = d3.min(d.values, function(d) { return d.value; } );
var max = d3.max(d.values, function(d) { return d.value; } );
// Join new data with old elements
var points = d3.select(this).selectAll("circle")
.data(d.values, function(d) { return (d.Plot); } );
// Add new elements
points.enter().append("circle")
.attr("cy", y(d.key))
.attr("r", 10)
.style("opacity", 0.5)
.style("fill", function(d) { return elevColor(d.Elevation); })
.merge(points) //(?)
.transition()
.attr("cx", function(d) { return x((d.value-min)/(max-min)); });
// Remove old elements not present in new data
points.exit().remove();
This whole piece of code is largely duplicated for the overall enter selection and again in the overall update selection (as opposed to the individual variables), which seems less than ideal. How would merge be used to to remove this duplicated code?
The full example is here: http://plnkr.co/edit/VE0CtevC3XSCpeLtJmxq?p=preview
I'm the author of the solution for your past question, which you linked in this one. I provided that solution in a comment, not as a proper answer, because I was in a hurry and I wrote a lazy solution, full of duplication — as you say here. As I commented in the same question, the solution for reducing the duplication is using merge.
Right now, in your code, there is duplication regarding the setup of the "update" and "enter" selections:
var update = g.selectAll(".datapoints")
.data(filtered[0].values);
var enter = update.enter().append("g")
.attr("class", "datapoints");
update.each(function(d, i){
//code here
});
enter.each(function(d, i){
//same code here
});
To avoid the duplication, we merge the selections. This is how you can do it:
var enter = update.enter().append("g")
.attr("class", "datapoints")
.merge(update)
.each(function(d, i) {
//etc...
Here is the updated Plunker: http://plnkr.co/edit/MADPLmfiqpLSj9aGK8SC?p=preview
What am I missing?
I am allowing users to remove and plot their own data point. My line path is drawn with this and it works fine.
let self = this;
let line = D3['line']()
.x(function (d) { return self.getColX(d.x); })
.y(function (d) { return self.getRowY(d.y); });
this.group = this.canvas.append("g")
.attr("transform", "translate(25,25)");
//bind the data
this.group.selectAll("path")
.data(this.data[this.site].drawLine)
.enter()
.append("path")
.attr("d", line)
.attr("fill", "none")
.attr("stroke", "black");
this.group.selectAll('path').exit().remove()
My problem is, if I pop the last coordinates out and add a new one, call the draw function, the new points gets added correctly but the old points doesn't get remove.
For example: my line goes from (x,y): (1,3) to (3,6) to (7,8), if i remove (7,8) and replace that with 5,6. i will see a new line from (3,6) to (5,6) but the line from (3,6) to (7,8) which is no longer in the data array still there.
The enter() and exit() selections are created after D3 compares your selection with the data provided. So they are available in the return of these calls:
this.group.selectAll("path")
.data(this.data[this.site].drawLine)
And that's why new data is appended, enter() works just fine.
With this.group.selectAll('path').exit().remove() you create a new selection but is not comparing the selection against any data, therefore enter() and exit() selections aren't available to work with.
Long story short, just apply .exit().remove() to your initial selection and it should work. Something like this:
let update = this.group.selectAll("path")
.data(this.data[this.site].drawLine)
update.enter()
.append("path")
.attr("d", line)
.attr("fill", "none")
.attr("stroke", "black")
update.exit()
.remove()
The following toy problem illustrates my issue. I have an array of "locations", say a treasure map. Each item in the array for example monsters or treasure could exist at multiple locations on the map. e.g.
locations = [
{name:'treasure', color: 'blue', coords:[[100,100], [200,300]]},
{name:'monsters', color: 'red', coords:[[100,150], [220,420], [50,50]]}
]
Now I want to plot these using D3. The bad/naive approach (that works - see here for fiddle), would look like this:
for location in locations
for coords in location.coords
svg.append('circle')
.attr('cx', coords[0])
.attr('cy', coords[1])
.attr('r', 8)
.style('fill', location.color)
.datum(location)
However, when I modify the contents of the data, I don't want to have to run this naive code each time. It appears that using data() and enter() is the "correct" way to do it, but I can't figure out how it works with the sub-coordinates. e.g.
svg.selectAll('circle').data(locations).enter().append('circle')
.attr('cx', (d) -> d.coords[0][0])
.attr('cy', (d) -> d.coords[0][1])
.attr('r', 8)
.style('fill', (d) -> d.color)
This works great, but as you can see I am only printing the FIRST coordinate for each location, where I want to print them all. I suspect the only way to do this is to flatten my data array so there are 5 entries in total - 3 monsters and 2 treasure items.
Just wondering if there is a way to handle this better using D3.
For this, you need nested selections. The idea is that instead of appending a single element per data item, you append several. In code, it looks like this:
// append a `g` element for each data item to hold the circles
var groups = svg.selectAll("g.circle").data(locations)
.enter().append("g").attr("class", "circle");
// now select all the circles in each group and append the new ones
groups.selectAll("circle")
// the d in this function references a single data item in locations
.data(function(d) { return d.coords; })
.enter().append("circle")
.attr("cx", function(d) { return d[0]; })
.attr("cy", function(d) { return d[1]; });
It works the same for update and exit selections.