I hava a multi line chart with D3.js.
Initial rendering works just fine. When I try to update, only new lines are added instead of the old ones update/removed.
Example: http://jsfiddle.net/ty192n93/6/
This is the part where the line is rendered:
var node = svg.selectAll(".g.city")
.data(data, function(d) { return d.name; });
var enter = node.enter().append("g")
.attr("class", "city");
enter.append("path")
.attr("class", "line")
.attr("d", function(d) {
return line(d.data);
})
.style("stroke", function(d) {
return color(d.name);
});
// Text element left out
var remove = node.exit().remove();
How is it possible to update the old values (key function is specified) or remove them completely?
Your first selection is wrong. The selector .g.city selects all elements having both classes g and city. Your selection is valid, though, but will always return an empty selection putting all your data in the enter selection and leaving the exit selection empty. Instead, you are interested in group elements g having class city. Removing the first dot from the selector should do the trick:
var node = svg.selectAll("g.city") // <-- remove the dot
.data(data, function(d) { return d.name; });
Related
i have started with d3, on this moment i tray to edit a given script to meed the new requirments. I tray to reach a variabel but i can not access it. can someone help me out?
The problem is on line 5, i can not get "this.parentNode" but i need it.
Can someone point me in the right drection?
Thanks for your time
Greathings
function lines(x, yl, yr, n, svgElement, colors, Line) {
Line = Line || d3.line()
.x(function(d) { return x(d.time) + x.bandwidth() /2; })
.y(function(d) {
if(parseInt(d3.select(this.parentNode).attr("data-index")) !== scope.dataset.labels.indexOf(scope.rightas) && scope.rightas !== null) {
return yl(d.y);
}
return yr(d.y);
});
svgElement.selectAll('.line')
.data(function(d){ return [d]; })
.enter()
.append("path")
.attr("d", Line)
.attr("class", "line")
.style("fill", "none" )
.style("stroke", function(d, i, j) {
return d3.rgb(colors[d3.select(this.parentNode).attr("data-index")]).darker();
})
.style("stroke-width", "2")
}
You cannot get this DOM element from inside the line generator (with or without Angular). The line generator has no access to the element you're appending using it.
The API is clear about that:
When a line is generated, the x accessor will be invoked for each defined element in the input data array, being passed the element d, the index i, and the array data as three arguments. (emphasis mine)
The same, obviously, goes for the y accessor. So, as you can see, only the data is passed.
Therefore, this in this case will be simply the window, and there is no parentNode here.
Look at this demo (as the stack snippet takes a long time to console.log the window object, I'm using only this.name):
var line = d3.line()
.x(function(d) {
console.log(this.name);
return d;
})
.y(function(d) {
return d;
});
line([1]);
<script src="https://d3js.org/d3.v5.min.js"></script>
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.
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()
m creating a scatter plot and I have problems plotting my circles (datapoints).
When appending the first set of circles it is drawn fine on the graph. However the next set I try to append won't be draw for some reason. How can this be? Am I using the Enter/append/select wrong the 2nd time?
I have a JSFiddle with my code: http://jsfiddle.net/4wptM/
I have uncommented the parts where I load and manipulate my data and created the same array with a smaller sample. As it can be seen only the first set of circles are shown and the 2nd ones aren't.
My code of the circle section is pasted below:
var circle = SVGbody
.selectAll("circle")
.data(graphData[0])
.enter()
.append("circle")
.attr("cx",function(d){return xScale(100);})
.attr("cy",function(d){return yScale(parseFloat(d))})
.attr("r",5);
circle = SVGbody
.selectAll("circle")
.data(graphData[1])
.enter()
.append("circle")
.attr("cx",function(d){return xScale(200);})
.attr("cy",function(d){return yScale(parseFloat(d))})
.attr("r",5);
I just have those 2 copied to try and figure out the problem. The actual code I have is in the following for loop. When I run it in this loop it draws the circles for when the index is 1 and a few of when the index is 3.
for(var i = 0;i < graphData.length;i++){
var circle = SVGbody
.selectAll("circle")
.data(graphData[i])
.enter()
.append("circle")
.attr("cx",function(d){return xScale((i*100)+100);})
.attr("cy",function(d){return yScale(parseFloat(d))})
.attr("r",20);
//console.log(i + " ::::::: " + graphData[i])
}
Some help would be so greatly appretiated. I really can't figure out what I'm doing wrong.
When operating on a existing set of elements (as it is the case here on the second .selectAll("circle") the .data() method uses only these elements from the array which have no corresponding index in the selecton made with .selectAll(). To prevent this behaviour you will have to add a key function as the second parameter of .data() as explained here and here.
Here this function only has to return each element of the array:
function (d) {
return d;
}
In the fiddle it would look something like this:
var circle = SVGbody
.selectAll("circle")
.data(graphData[0], function(d) { return d; }) // <-- this one
.enter()
.append("circle")
.attr("cx",function(d){return xScale(100);})
.attr("cy",function(d){return yScale(parseFloat(d))})
.attr("r",5);
fiddle
And to change the fill color of an element you have to use .style("fill", ...) and not .attr("fill", ...)
.style("fill", "green")
I have a follow-up question to this question that I closed too hastily.
How/where would I put a .exit() or .transition() for the answers? In my original code, I'm saving the main chart in a variable (questions_chart), so I can say something like questions_chart.exit().remove(). However, if I put .exit().remove() after the .text() (last line in the code in Peter's answer), I get an error saying that object array has no method 'exit'
I have not tested this but will give it a shot...you would need to preserve the variable binding the data...like so:
var divs = d3.select("body").selectAll("div")
.data(data.questions);
divs
.enter().append("div") // this creates the question divs
.text(function(d) { return d.value; })
.selectAll("div")
.data(function(d) { return d.answers; })
.enter().append("div") // this creates the nested answer divs
.text(function(d) { return d.value; });
divs.exit().remove();
I am assuming you don't need to remove just the divs that are answers. Hope this helps.
UPDATE: giving it a shot at dealing with questions and answers...
var divs = d3.select("body").selectAll("div")
.data(data.questions);
var questionDivs = divs
.enter().append("div") // this creates the question divs
.text(function(d) { return d.value; });
divs.exit().remove();
var answerDivs = questionDivs
.selectAll("div")
.data(function(d) { return d.answers; });
answerDivs
.enter().append("div") // this creates the nested answer divs
.text(function(d) { return d.value; });
answerDivs.exit().remove();