D3 change elements based on two different datasets? - javascript

I am new to d3 and no javascript expert. I've found similar questions, but none that are quite what I'm looking for.
I have two csv files, one with school data that looks like this
id, name, longitude, latitude
The other csv has network data
id, netId (this is other school ids that are connected), hits (this is basically how many connections are between the two schools)
I have the school data bound to circles on a map. What I'm trying to do is on a click, find each of the connected schools and transition their circles on the map based on how many connections they have.
I'm assuming this is going to need to be a series of for loops, but I'm not sure of the best way to do it.
Currently, skipping past all the map creation stuff, I have this:
var currData = [];
d3.csv("network.csv", function(networkData){
d3.csv("schoolData.csv", function(data) {
//create the circles
var circle = svg.selectAll("circle")
//bind the data
.data(data)
.enter()
.append("circle")
.attr("cx", function(d) {
return projection([d.lon, d.lat])[0];
})
.attr("cy", function(d) {
return projection([d.lon, d.lat])[1];
})
//initial style of circles (will move this into a class eventually)
.attr("r", 5)
.style("fill", "orange")
.style("opacity", 0.75)
//on click
.on("click", function (d) {
for (i = 0; i<= networkData.length; i++) {
if(d.id == networkData[i].id) {
currData.push({"netId": networkData[i].netId,
"hits": networkData[i].hits}
)}
};
This is returning the netId and hits if I log it to the console within the for loop, but I can't seem to access currData outside of the for loop. I'm not sure why.
I am also not sure of the best way to take each of the subsequent netIds and link them back to the school data bound to the circle elements. Am I making this more complicated than necessary? Does D3 have an easy way of doing this?
When a circle is clicked on the map, I need to find the id of schools in the network for the selected school and then make a change to their circles on the map based on the number of hits. Very much like this.
The data looks like this:
School Data:
id,school,lon,lat
1,UC Berkeley,-122.250502,37.872855
2,UC Los Angeles,-118.445227,34.06886
3,Cal State Los Angeles,-118.168266,34.068232
4,University of Southern California,-118.2866414,34.021801
Network Data:
id,netId,hits
193,220,1
193,229,5
193,226,1
193,49,1
124,226,11
124,201,1
278,175,7
341,227,1
341,310,2
341,135,1
101,201,1

If you want to be able to select an element based on its data attribute, the easiest way to do that is to give the element a class name based on that data. For example, all your school elements could have a class based on their id in the data, "s-1", "s-203", etc.
Then, when you find that id referenced in a row from your network data table, it is easy to select the corresponding element and change it however you want.
Here's an example. I'm using Array.forEach() instead of a for loop, but it is a similar idea to what you had. I'm not quite sure how you were using your filtered data array and why it wasn't working for you, but hopefully this will get you started:
http://fiddle.jshell.net/V5DCx/
However, instead of scanning through the net data on every click, it might be worth manipulating the data up front and including it in the school's data objects. You can use d3.nest to group the network data into sub-arrays based on the id, and then sort through the school array once, attaching the correct network sub-array to the school data before attaching the school data to your d3 graphics. Then it will be right there waiting in your function(d) event handler.

Related

d3 pie chart problems with exiting entering

After making several bar charts using enter, update, exit method in D3js, I wanted to try the same with a pie chart. I thought I applied selections correctly, but the pie chart won't update with the new data in JSON. I looked for similar examples online, but couldn't find one which involved an .on("click" method. I want users to compare the lifespans of humans and animals using a donut chart. I'm trying to implement the search tool through the database of animals right now.
here's what a data object looks like for the query Goat:
[{"Animal":"Male","Life_Span":73},{"Animal":"Goat","Life_Span":10}]
I'm having trouble with this code in particular:
var pie = d3.pie()
.sort(null)
.value(function(d) { return d.Life_Span; });
//code for accessing data, etc
//enter remove selections
var path = svg.selectAll("path")
.data(pie(newdata))
var enterdata =
path.enter().append("path")
.attr("d",arc)
path.exit().remove()
enterdata.exit().remove()
I posted the full code on Plunkr here: http://plnkr.co/edit/3QSAPxQpju63tIXRd9p7?p=preview
A few weeks into learning d3js, I'm still struggling with enter,update exit selections even after reading many tutorials on the subject. I would really appreciate any help. Thanks
In D3 v4.x, you need to merge the update and enter selections:
var enterdata = path.enter()
.append("path")
.merge(path)
.attr("d",arc)
You don't need an exit selection because your data array has always 2 objects: except for the first time, your enter selection is always 0, and your exit selection is always 0.
I took the liberty of colouring the paths in different colours, according to their indices:
.attr("fill", (d,i) => i ? "teal" : "brown");
Here is your updated plunker: http://plnkr.co/edit/l6OzmxVfid9Cj7iv7IMg?p=preview
PS: You have some problems in your code, which I'll not change (here I'm only answering your main question), but I reckon you should think about them:
Don't mix jQuery and D3. You can do everything here without jQuery
Don't load all your CSV every time the user chooses an animal. It doesn't make sense. Instead of that, put the click function inside the d3.csv function (that way, you load the CSV only once).

Coloring a map using D3.js

I'm new to StackOverflow and I just started using D3.
I need to show the values on a map. I saw this question that is very similar to what I should do.
What I'd like is to color the countries based on the values in the column Date to a CSV and based on selected year by user (radio button).
How can I do that?
I created a gray color scale and have included them in an array, then I created a method chooseColor(value) that returns the correct color based on the value of the country in that year.
I think it is not the most efficient method to do this thing...
Also in my CSV there are not all the countries present in the European Union. For example, I have no data on Russia so I "turned off" some countries putting an if inside the event on mouseover.
But I would also cut part of Russia in the map in order to enlarge the known countries. How can I do also that thing?
I looked at these examples: Choropleth and Threshold Choropleth by Mike Bostock on bl.ocks.org but I have not understand how to color the countries...
(I wanted to put links but I can't post more than 2 links because of my low reputation)
This is my code.
I apologize for my bad English. Thank you all,
Pier
EDIT
I admit I did not understand some things in your code.
Why I need events on mouseover and mouseout? And what are hover and rhover? I thought they were events related to this question. But in my case I don't need it, no?
Use array_values or d is the same, right? Does not change if I use d or array_values, right? It is a stupid question but it confused me.
I modified the makemap method in this way. I understand correctly how to use your code?
function makemap(error, europe, dessease) {
dess = dessease.slice();
counties = topojson.feature(europe, europe.objects.collection);
vector = svg.selectAll("path")
.data(counties.features)
.enter()
.append("path")
.attr("class", "county")
.attr("id", function(d) {
return "coun" + d.properties.indx;
})
.attr("d", path)
.style("fill", function(array_values) {
return color(array_values[d.country]);
});
In this case there is an error concerning d, of course. Sorry, I do not know where I'm wrong...
The country's color will depend on a value. So the color IS a function of "value". To do that you must to define a range of color based on your values:
var color = d3.scale.linear()
.domain([mn,mx]) // <--- min and MAX of your value
.range(["#ffffff","000000"]);
then define the color of your country:
svg.selectAll(".county")
.style("fill", function(array_values) {
return color(array_values[d.country]);
});
Must-Read: Jerome Cukier - d3: scales, and color

D3 and updating elements of a multidimensional array

Let's say that I have a 2d array called state that looks like [[0,1,0],[1,1,0],[1,2,1]]. The members of this array are constantly updating. I have D3 successfully rendering each member of the array with the following code:
function view(state)
const vis = d3.select('body')
.selectAll('div')
.data(state)
.enter()
.append('div')
.attr('style', 'display: flex')
.selectAll('span')
.data((d,i) => d)
.enter()
.append('span')
.text(d => d)
return vis
}
Now consider that this view function is called every time my state changes. How can I get D3 to re-render only the elements that have changed since the previous render? Currently, the page renders initially but D3 never re-renders any of the elements, despite the view function being called at each state change.
If you'd like to see the full source, I posted it here
Yeah to handle such a case you need to make use of enter which you are doing to create new elements and exit to remove any element not in the current array.
I ll recommend you to do it this way:
function view(state)
var visData = d3.select('body')
.selectAll('div')
.data(state, function(d){/*uniquely identify each element in array*/return d;});
const vis = visData.enter()
.append('div')
.attr('style', 'display: flex')
.selectAll('span')
.data((d,i) => d)
.enter()
.append('span')
.text(d => d);
//remove the unnecessary data divs.
visData.exit().remove();
return vis
}
Further on enter exit read
You have 2 cases when you want to render your data: when you add new view elements (divs, spans and text), and when you update your data (text). In your code you've done the first step only. You should do the second one.
d3.js provides selections for both cases. When you write
var div = d3.select('body').selectAll('div');
you get what already exists on your view. But if you add .enter(), i.e.
var div = d3.select('body').selectAll('div').enter();
it is already the second case. You are getting what doesn't exist yet, and should be created.
Since your data are changing, you should divide your code into two parts: view creation, view updating.
Here is a JSFiddle example how it could be done for you code. The example can be improved, and the create/update code sections can be separated and put to corresponding functions, see there.
Anyway, I encourage you to read the general update pattern for d3.js.

How to create legend for D3 Sequence Sunburst?

I'm modifying the original D3 Sequence Sunburst file to better suit my needs. The original colors variable is a hard-coded object. This clearly cannot be the best method. I'm using the flare.json example, which is larger, harder to read, and still much smaller than the json file I will be user after testing.
I'd like to randomly generate colors, apply them to each datum in the createvisualization function, but I'm new to D3, and do not know how to 1) fetch names (everything but the leaves) from the json file, and 2) pair them with their random color.
Edit:
Adding random colors and applying them turned out to be trivial,
var colors = d3.scale.category10();
...
.style("fill", function(d,i) { return colors(i); })
But I'm still note sure how to fetch the names of all non-leaves in the json, then create an array from both the random colors and the non-leaves.
Help here greatly appreciated.
To get the names of all non-leaf elements, you can do something like this.
var names = [];
function getNames(node) {
if(node.children) {
names.push(node.name);
node.children.forEach(function(c) { getNames(c); });
}
}
getNames(root);
After running this code, names will contain all the names you want. To then generate a legend from that, you can use the names array as the data:
var gs = svg.selectAll("g.name").data(names).enter().append("g").attr("class", "name");
gs.append("rect")
// set position, size etc
.attr("fill", d);
gs.append("text")
// set position etc
.text(String);
This will append a g element for each name and within each g element, append a rect that is filled with the colour corresponding to the name and a text element that shows the name.

Dynamically updating in d3 works for circles but not external SVGs

Suppose I want to dynamically update the position and number of circles on a page using d3. I can do this, using the .data(), .enter(), .exit() pattern. Here is a working example.
http://jsfiddle.net/csaid/MFBye/6/
function updatePositions(data) {
var circles = svg.selectAll("circle").data(data);
circles.enter().append("circle");
circles.exit().remove();
circles.attr("r", 6)
.attr("cx", 50)
.attr("cy", function (d) {
return 20 * d
});
}
However, when I try to do the same thing with external SVGs instead of circles, many of the new data points after the first update do not appear on the page. Example:
http://jsfiddle.net/csaid/bmdQz/8/
function updatePositions(data) {
var gs = svg.selectAll("g")
.data(data);
gs.enter().append("g");
gs.exit().remove();
gs.attr("transform", function (d, i) {
return "translate(50," + d * 20 + ")";
})
.each(function (d, i) {
var car = this.appendChild(importedNode.cloneNode(true));
d3.select(car).select("path")
});
}
I suspect this has something to do with the .each() used to append the external SVG objects, but I am at a loss for how to get around this. Also, the "cx" and "cy" attributes are specific for circles, and so I can't think how they could be used for external SVGs.
Thanks in advance!
There are two problems with your code. The first problem, and reason why you're not seeing all the data points, is that your external SVGs contain g elements, which you are selecting. What this means is that after you first appended the elements, any subsequent .selectAll("g") selections will contain elements from those external SVGs. This in turn means that the data you pass to .data() gets matched to those and hence your selections do not contain what you expect. This is easily fixed by adding a class to the g elements you add explicitly and selecting accordingly.
The second problem is that you're executing the code that appends the external SVGs as part of the update selection. This means that those elements get added multiple times -- not something you would notice (as they overlap), but not desirable either. This is easily fixed by moving the call to clone the nodes to the .enter() selection.
Complete jsfiddle here. As for your question about cx and cy, you don't really need them. You can set the position of any elements you append using the transform attribute, as you are doing already in your code.

Categories