I have a scatter graph built with d3.js. It plots circles in the graph for the spending habits of specific people.
I have a select menu that changes the specific user and updates the circles on the scatter graph.
The problem is the old circles are not removed on update.
Where are how should I use .remove() .update(), please see this plnkr for a working example
http://plnkr.co/edit/qtj1ulsVVCW2vGBvDLXO?p=info
First, Alan, I suggest you to adhere to some coding style convention to make your code readable. I know that D3 examples, and the library code per se, almost never promote code readability, but it's in your interest first, because it's much easier to maintain readable code.
Second, you need to understand how D3 works with enter, update and exit sets, when you change data. Mike Bostock's Thinking with Joins may be a good start. Unless you understand how the joins work, you won't be able to program dynamic D3 charts.
Third, here's a bug in updateScatter. name.length makes no sense because your first name variable is value. So it's not the case of deleting old data in the first place.
// Update circles for the selected user chosen in the select menu.
svg.selectAll(".markers")
.data(data.filter(function(d){ return d.FirstName.substring(0, name.length) === value;}))
.transition().duration(1000)
.attr("cx", function(d) { return xScale(d.timestamp); })
.attr("cy", function(d) { return yScale(d.price); });
Also what that weird equality comparison is d.FirstName.substring(0, name.length) === name. Your first name data is not even spaced in CSV file. Plain d.FirstName == name is fair enough. If you expect trailing spaces anyway, just trim your strings in the place where you coerce prices and dates.
This is how correct updateScatter may look look like:
function updateScatter()
{
var selectedFirstName = this.value;
var selectedData = data.filter(function(d)
{
return d.FirstName == selectedFirstName;
});
yScale.domain([
0,
d3.max(selectedData.map(function(d)
{
return d.price;
}))
]);
svg.select(".y.axis")
.transition().duration(750)
.call(yAxis);
// create *update* set
var markers = svg.selectAll(".markers").data(selectedData);
// create new circles, *enter* set
markers.enter()
.append('circle')
.attr("class", 'markers')
.attr("cx", function(d)
{
return xScale(d.timestamp);
})
.attr("cy", function(d)
{
return yScale(d.price);
})
.attr('r', 5)
.style('fill', function(d)
{
return colour(cValue(d));
});
// transition *update* set
markers.transition().duration(1000)
.attr("cx", function(d)
{
return xScale(d.timestamp);
})
.attr("cy", function(d)
{
return yScale(d.price);
});
// remove *exit* set
markers.exit().remove();
}
Related
I'm hoping someone can spot where I'm going wrong here as I've looked at it for over 24 hours and can't see the issue.
I have a fairly complex dataviz working nicely in D3 but the final step is to 'adjust' any overlapping points - primarily so that their individual tooltips are accessible (on hover) (e.g. I don't want to consider alternatives like 'growing' the points to show their combined status).
So, imagine a pinboard where every pin is a point with hit_x and hit_y coordinates. Everything is working perfectly (filtering, updating, etc) in what I understand to be a fairly standard D3 'update' pattern. Sometimes two pins might have the same coordinates.
I thought I'd use D3 forces (for the first time) to recognise the 'colliding' pins and then adjust their positions accordingly. However, whilst I can get a simple version working on Blockbuilder. I can't get the same thing working when applied to my dataviz, even when I simplify it considerably.
I think perhaps I don't 100% understand the simulation process when using from an update pattern. My (simplified) code is pasted below, and here's in effect what I think it should do:
Appropriately loaded/formatted data is passed to update()
Prepare points ready to attach svg objects (same as 'standard' update pattern).
Prepare a simulation (and initially run it via ticked()).
Visualise the data.
Rerun the simulation so that the collisions are detected...
...during which, ticked() should notice the collisions and adjust the points by adjusting d.x and d.y accordingly until there are no overlaps.
I'm sure there's something obvious I'm missing - possibly related to whether I should pass the 'points' to the simulation or the original data. If anyone can spot it then I'd be very grateful. ¯\(ツ)/¯
function ticked() {
console.log("Ticking...");
points
.attr("cx", function(d) {
return d.x;
})
.attr("cy", function(d) {
return d.y;
});
}
// Update visualisation
function update(data) {
// Animation transitions
var t1 = d3.transition().duration(3000);
// Add svg group for handling/styling points later
graph.select("g.points").remove();
var pointsG = graph.append("g")
.attr('class', 'points');
// Data: Points
// Join new data with old elements
points = pointsG.selectAll("circle.point")
.data(data, function(d) {
return d;
});
// Forces
collisionSimulation = d3.forceSimulation(points)
.force('charge', d3.forceManyBody().strength(10))
.force('x', d3.forceX(function(d) {
return xScale(d.point.hit_x);
}).strength(0.5))
.force('y', d3.forceY(function(d) {
return yScale(d.point.hit_y);
}).strength(0.5))
.alphaTarget(1)
.on('tick', ticked);
console.log(collisionSimulation);
// Remove old elements not present in new data
points.exit()
.transition(t1)
.attr('class', 'exit')
.remove();
// Append new elements
points.enter()
.append('circle')
.attr('class', 'point')
.attr('cx', function(d) {
return xScale(d.point.hit_x);
})
.attr('cy', function(d) {
return yScale(d.point.hit_y);
})
.attr('r', 5)
.merge(points);
collisionSimulation.nodes(points)
.force("collide", d3.forceCollide().strength(0.5).radius(function() {
return 5;
}));
collisionSimulation.alpha(0.5).restart();
}
OK, now resolved - though I would appreciate further input on the ticked() implementation.
I imagine someone will go through the same process as me at some point, so following is what I did to get things working. The crux of it is that I had been getting confused joining the forces with the visual objects. By passing the same original data to both, the forces were then able to act on the visual objects, rather than both existing as one.
I re-read the core information about data joins. I've read this a number of times over the past few months working with D3 but for some reason, reading it again made my forces / update pattern finally make (un)sense in my head.
Similarly, despite having used a few excellent Blocks examples for reference, I came across this Medium article which made things click for me. It doesn't mean the Blocks examples aren't great - just that for some reason this article helped me.
Thereafter, I updated my code and was able to get things working. I feel like I'm still missing a little bit of 'data join' magic in my ticked() function though as it seems odd to need to 'search' for the relevant items to act on rather than use a previously built reference. I'm sure I can optimise that, but if anyone else can input then great.
Hope that helps someone else out.
function ticked() {
console.log("Ticking...");
d3.select('g.points').selectAll('circle.point')
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
}
// Update visualisation
function update(data) {
// Animation transitions
var t1 = d3.transition().duration(3000);
// Add svg groups for organising data elements
graph.select("g.points").remove();
var pointsG = graph.append("g")
.attr('class', 'points');
// Points
// Join new data with old elements
points = pointsG.selectAll("circle.point")
.data(data, function(d){ return d; });
// Remove old elements not present in new data
points.exit()
.transition(t1)
.attr('class', 'exit')
.remove();
// Append new elements
points.enter()
.append('circle')
.attr('class', 'point')
.attr('cx', function(d){ return xScale(d.point.hit_x); })
.attr('cy', function(d){ return yScale(d.point.hit_y); })
.attr('r', 5)
.merge(points);
// Forces
collisionSimulation = d3.forceSimulation()
.nodes(data)
.force('charge', d3.forceManyBody().strength(2))
.force('x', d3.forceX(function(d) { return xScale(d.point.hit_x); }).strength(0.5))
.force('y', d3.forceY(function(d) { return yScale(d.point.hit_y); }).strength(0.5))
.force("collide", d3.forceCollide().strength(0.5).radius(function() { return 5; }))
.on('tick', ticked);
collisionSimulation.alpha(0.5).restart();
}
I've made a plunker that updates data from one csv file to another, the yaxis updates accordingly but the rectangles don't.
The .attr("height", function(d) { return Math.abs(y(d[0])) - y(d[1]); }); portion of the code still has the old data from the previous file (I'm guessing).
I'm guessing this is because I haven't declared .data(series) in the updateData() function, I remember doing something like this in another chart
g.selectAll(".bar").data(series).transition()
etc...
but this doesn't work in this chart.
I can't figure it out, any help is appreciated!
The problem was that you didn't join the new data to existing bars.
To make this work well, you will want to specify a key for category of data when you join the series to the g elements to ensure consistency (although I notice that category-1 is positive in the first dataset, and negative in the second, but this is test data i guess)
Here's the updated plunkr (https://plnkr.co/edit/EoEvVWiTji7y5V3SQTKJ?p=info), with the relevant code highlighted below:
g.append("g")
.selectAll("g")
.data(series, function(d){ return d.key }) //add function to assign a key
.enter().append("g")
.attr("class", "bars") //so its easy to select later on
//etc
...
function updateData() {
d3.csv("data2.csv", type, function(error, data) {
///etc
let bars = d3.selectAll(".bars") //select the g elements
bars.data(series, function(d){ return d.key }) //join the new data
.selectAll(".bar")
.data(function(d) { return d; })
.transition()
.duration(750)
.attr("y", function(d) { return y(d[1]); })
.attr("height", function(d) { return Math.abs(y(d[0])) - y(d[1]); });
I have some javascript inside a Flask application that uses d3 to render a bar graph from some randomly generated data in a specific range. It allows sorting by label or value, in ascending or descending order. Sorting works fine in every situation except specifically when sorting in ascending order by value.Most of the time, at least 2 of the bars are not in the right order.
From inspecting the code, the problem seems to be that the 'x' d3 attribute for these bars is not set correctly, so they're displayed in the wrong spot on the x axis. I figure it must be a javascript issue and not a Flask issue since the rest works but I can't seem to figure it out.
Data generation(done in Python in the original code but I generated it in js with jiphy here):
function data_bar_graph(num=32) {
data_min = 0.0;
data_max = 100.0;
output = [];
for (i in range(int(num))) {
output.push(dict(label=str(uuid.uuid4())[:2], value=random.uniform(data_min, data_max)));
return output;
}
}
The js file in question: https://jsfiddle.net/turtles_/bk3svowo/ (it doesn't work properly since I'm not sure how to pass the data or generate the graph in jsfiddle outside of flask, but you can see the code. I'll try to figure out the data passing if it's too abstract like this).
EDIT: I'm about 95% sure the error is somewhere in this following segment(only the x is sometimes wrong,rest of the values are sorted correctly so I'm thinking the .attr("x" has something weird. Does the following seem correct?
data = data.sort(sortComparator);
// Map x-axis labels
xScale.domain(
data.map(function(d)
{
return d.label;
}
));
// Map y-axis values
yScale.domain([0, d3.max(data,
function(d)
{
return d.value;
}
)]);
// Draw the axes
drawAxisLines(chart.g, xAxis, yAxis, computedHeight, 0, 0, 0);
var bars = chart.g.append("g")
.attr("class", "bars")
.selectAll(".bar")
.data(data)
.enter()
.append("rect")
.attr("x",
function(d)
{
return xScale(d.label);
}
)
.attr("y",
function(d)
{
return yScale(d.value);
}
)
.attr("fill",
function(d)
{
return colours(d.label);
}
)
.attr("height",
function(d)
{
return computedHeight - yScale(d.value);
}
)
.attr("width", xScale.rangeBand())
.style(cssStyling.bar);
}
sortComparator is the d3.ascending/d3.descending that was mentioned.
Since you don't have a working example, it's difficult to know exactly what the issue is, but there is commonly pitfall here: you're most likely comparing the numbers as strings (e.g. "12" < "2", whereas 12 > 2).
To make sure you're comparing numbers, you can do the following:
data = data.sort(function(a,b) { return d3.ascending(+a.value, +b.value); })
By preceding the fields with a +, you're coaxing those fields to numbers (which your Python code may not do automatically).
As project to get to know d3.js, I’m displaying tweets on a map in real-time. Everything has worked this far, and I’m very, very pleased with the library.
On the page, I’m trying to list all languages. On hover, I want all tweets of that language to pop up. All of this is done, except some items in the list pops up the tweets of another language. A wrong one, I might add.
This is how I project the dots on the map:
points.selectAll('circle')
.data(tweets)
.enter()
.append('circle')
// Attach node to tweet, so I can use refer to the nodes later
.each(function(d) {
d.node = this;
})
.attr('r', 1);
This is how I create the list:
var data = d3.nest()
// Group by language code
.key(function(d) { return d.lang.code; })
.entries(tweets)
// Sort list by amount of tweets in that language
.sort(function(a, b) {
return b.values.length - a.values.length;
});
var items = languages_dom
// Add items
.selectAll('li')
.data(data)
.enter()
.append('li');
// Used for debugging
.attr('data-lang', function(d) {
return d.key; // Group key = language code
})
// Set text
.text(function(d) {
var dt = d.values[0];
return dt.lang.name;
})
// Mouseover handler
.on('mouseover', function(d) {
// Compare attribute with
// These values are actually different
var attr = d3.select(this).attr('data-lang');
console.log(attr, d.key);
// Pop up each node
d.values.forEach(function(d) {
d = d3.select(d.node);
d.transition()
.duration(200)
.attr('opacity', 0.5)
.attr('r', 8);
});
});
Note that the script above is run several times. d.key refers to another value later in the chain, while I’m not modifying data in that chain.
Edit 22:08
Things seems to work fine when I’m not sorting the data. At least it’s a lead.
As noted in the comments, you're overwriting d in your forEach function. Instead, you should try something like
d.values.forEach(function(p) {
d3.select(p.node)
.transition()
.duration(200)
.attr('opacity', 0.5)
.attr('r', 8);
});
Notice the forEach variable is named p instead of d.
As the data changed, the old data seems to be kept somehow.
Either way, I simply deleted the list before applying the new data:
languages_dom
.selectAll('li')
.remove();
Can’t say this is graceful, nor performant, but it gets the job done :)
I have created a scatter chart with multiple y-axis.
I need to create a legend to indicate the scattering.
I have created a fiddle of the same in the link provided in comments.
Please help me out.
The reason is that in your code colors is an object, not an array.
D3 expects the data that is passed to it to be an array:
I've updated your fiddle - see http://jsfiddle.net/y3LEt/3/
The critical updates are:
var colors = [["Local", "#377EB8"],
["Global", "#4DAF4A"]];
legendRect
.attr("y", function(d, i) {
return i * 20;
})
.style("fill", function(d) {
return d[1];
});