This question already has answers here:
D3.js Drawing geojson incorrectly
(2 answers)
Closed 2 years ago.
Creating a map using D3 V6, showing educational attainment by county. I have a counties.topojson and csvData.csv which are loaded:
var promises = [];
promises.push(d3.csv("data/csvData.csv")); //load attributes from csv
promises.push(d3.json("data/counties.topojson")); //load background spatial data
Promise.all(promises).then(callback);
and in a callback function assigned to variables csvData and counties. The counties are then translated using:
miCounties = topojson.feature(counties, counties.objects.collection).features;
The csvData is joined to the county data, and the join is confirmed in console.log(joinedCounties), within the callback function setEnumerationUnits() is called (where colorScale is quantile scale based on an array created from the csvData and map is the SVG element:
function setEnumerationUnits(joinedCounties,map,path,colorScale){
var counties = map.selectAll(".counties")
.data(joinedCounties)
.enter()
.append("path")
.attr("class", function(d){
return "counties " + d.properties.NAME;
})
.attr("d", path)
.style("fill", function(d) {
return choropleth(d.properties, colorScale);
})
I should also mention adding "fill" to the .counties class in CSS also creates the "spilling". I have checked the topojson in QGIS and Pro, which both appear normal. I have also tried a second source of data with the same results.
Here is the result:
Here is what is looks like without styling, no fill, just stroke defined in CSS:
I receive no errors in the console. I appreciate any help anyone can give! Thanks!
Thank you! The turf.rewind worked!!
here's what I added to make it work (after installing turf library):
miCounties.forEach(function(feature){
feature.geometry = turf.rewind(feature.geometry, {reverse:true});
One or more of your GeoJSON entries are the wrong way around. The values are correct, but they are in the wrong order. d3-geo generally expects GeoJSON features to be clockwise:
Spherical polygons also require a winding order convention to determine which side of the polygon is the inside: the exterior ring for polygons smaller than a hemisphere must be clockwise, while the exterior ring for polygons larger than a hemisphere must be anticlockwise.
You can fix the winding of your data using a plugin or tool like turf, which you can use to "rewind" your shapes - though you should use the reverse: true option.
Related
I am currently working on a project where I want to visualize airport locations in the USA on a geoJSON map and where the state is encoded by color.
So far I have managed to import my geoJSON file of US borders and my csv file of my airports dataset just fine. Furthermore, I have been able to visualize the map and adjusted the styling to my liking with the help of these ressources:
Observable - Making maps in D3
Making Bubble Maps in D3
Now I want to add the airport locations. My csv file that contains the list of airports includes a state abbreviation and latitude/longitude coordinates to help with the mapping. Since the first ressource mentioned above uses geoJSON to add the data, which is not available for me since I only have an csv file, I decided to go with the second tutorial to add my data to the map. However, when I try to project my longitude and latitude coordinates to carthesian coordinates, like in the example, nothing happens.
Just to check I have written a piece of code that logs the projected longitude and latitude arrays in the console and I realized that the function does indeed return an array of length 2 for each coordinate, but instead of storing a numerical value I get 'NaN' which does not cause an error when trying to add the data to the map but also doesn't make any dots show up on my map, naturally.
I have tried to look for solutions but couldn't find anything regarding this issue. The projection function works fine when using it for the map, so i don't quite understand why it does not work for the dataset as well.
You can take a look at my js file below. I had to remove the links for the actual datasets because they belong to my university and I am pretty sure I am not allowed to make them public. I also removed some code that I considered redundant from the snippet like specific values to allow for a better overview.
Also, I use D3 v5 (requirement from my university).
/* ===== Draw Map of the United States Part begins here ===== */
/* ===== USStates GeoJson =====*/
//load US states geo.json file from assets folder
d3.json("Imagine_Actual_link_here.json")
.then(function(states){
/* ===== Create svg canvas for said file =====*/
// set size of canvas
// specifies height and width of the canvas
// Create the svg Element
let svg = d3.select(".map")
.append("svg")
.attr("width", width)
.attr("height", height);
// Append empty placeholder g element to the SVG canvas
svg.append("g");
/* ===== Set Up Projection an draw paths ===== */
// Projection
var projection = d3.geoAlbers()
//scaling, rotating, etc.
// Create GeoPath that draws path
var dataGeoPath = d3.geoPath()
.projection(projection);
svg.selectAll("path")
.data(states.features)
.enter()
.append("path")
.attr("fill", "#ccc")
.attr("stroke", "#fff")
.attr("d", dataGeoPath);
/* ===== Draw Map of the United States Part end here ===== */
/* ===== Visualize the flight data Part begins here ===== */
/* ===== Load the data sets from the assets folder ===== */
// Load Airports data
d3.csv("Imagine_Another_link_here.html").then(function(airports){
//Create Colour Scale for all 50 States
var color = d3.scaleOrdinal()
// color scale for the states, not important right now, priority is fixing the coordinate issue
// Everything works fine so far!
// Now this is where things get complicated!
// Example loop to check return of projection function
airports.forEach((d) => {
console.log(projection([+d.longitude]), projection([+d.latitude]));
});
// Append Airports data to map
svg
.selectAll("myCircles")
.data(airports)
.enter().append("circle")
.attr("cx", function(d){ return projection([+d.longitude, +d.latitude])[0]}) //!! tries to add NaN as a value for x-axis
.attr("cy", function(d){ return projection([+d.longitude, +d.latitude])[1]}) //!! tries to add NaN as a value for the y axis
.attr("stroke-width", 1)
.attr("fill-opacity", .4)
//.style("fill", function(d){ return color(d.state)})
//.attr("stroke", function(d){ return color(d.state)})
});
/* ===== Visualize the flight data Part ends here ===== */
// Return the visualisation of the map
return svg.node();
});
Feel free to ask any questions if anything about my code is unclear.
You need to pass a two-element array to the projection:
console.log(projection([+d.longitude, +d.latitude])
Apologies in advance that my question does not include code, but rather is a high level question on D3 and how to build my app correctly. I will attempt to make my question as clear and concise as possible:
I am building a React / D3 app that creates a scatter graph of NBA team logos, that allows users to click buttons to choose variables for the X and Y axis. The user can also filter the graph to include only certain teams (those in a particular division of the NBA).
Here is a quick demo gif of the app that features the main problem I am having:
.
.
.
.
and here is the link to my app for anyone interested.
What is working correctly
When I change the X or Y axis button (2nd half of the gif), the team logos correctly slide to their new locations.
What is working incorrectly
When I change the division (1st half of gif), it changes the 5 team logos that are showing, which is correct. However, the animation (which I show a few times in the gif) is incorrect. The old logos should simply disappear in place, and the new logos should simply appear in place. Instead, the logos change and slide.
I understand why the animation is doing this - D3 sees 5 data points before the update, and 5 data points after the update, but doesn't distinguish that my points are unique (different team logos). Since the updated data points have new (x,y) locations (different stats for each team), it simply animates the old logos to the locations of the new logos.
My proposed fix
I think the structure of my app is holding be back with regards to fixing this. Currently, I have a container component that loads the data, filters the teams (based on the division selected), and then passes the filtered data (an array of objects with the team stats) into a graph component that creates the logo scatter graph.
If, on the other hand, I pass the full object (of all 30 teams) to the graph component, then could I fix this problem by simply having D3 change the "fill" of the markers to transparent when they are filtered out? This way, there are always 30 logos being plotted, although 25 would be invisible, and the logos displaying should animate correctly.
Thanks in advance for your thoughts!
Edit: please let me know if the post is unclear in any way and I will try to clarify. I try to avoid posting Qs without code, but this is a fairly high level Q that focuses on how the D3 general update pattern works, and how I can build a graph with a specific animation that works within the general update pattern framework.
Edit2: The radio buttons are built in the container component here. Using my API to grab the data from my database, and then using these radio buttons to filter the data, are all done in the container component. I am considering bringing these radio buttons into the graph component and building them with D3. I think I may have to.
Edit3: Should have shared earlier, here is the D3 code that makes these markers:
const update = svg.select('g.points')
.selectAll("rect")
.data(graphData);
// Second exit and remove
update.exit().remove();
// Third Update
update
.enter()
.append("rect")
.attr("transform", d => `translate(${(xScale(+d[xColname]) - logoRadius)}, ${(yScale(+d[yColname]) - logoRadius)})`)
.attr("x", 0).attr("y", 0)
.attr("height", logoRadius)
.attr("width", logoRadius)
.attr("fill", d => `url(#teamlogo-${d.teamAbbrev})`)
.attr("opacity", 0.95);
// And Fourth transition
update
.transition()
.duration(750)
.delay((d, i) => i * 15)
.attr("transform", d => `translate(${(xScale(+d[xColname]) - logoRadius)}, ${(yScale(+d[yColname]) - logoRadius)})`)
.attr("x", 0).attr("y", 0)
.attr("height", logoRadius)
.attr("width", logoRadius)
.attr("fill", d => `url(#teamlogo-${d.teamAbbrev})`)
.attr("opacity", 0.95);
The issue here seems to be just the lack of a key function during the data join.
You asked in the comments section:
Doesn't the D3 general update pattern just look for the number of objects in the data array?
If you set up a key function the answer is no. The thing is that...
If a key function is not specified, then the first datum in data is assigned to the first selected element, the second datum to the second selected element, and so on. A key function may be specified to control which datum is assigned to which element, replacing the default join-by-index, by computing a string identifier for each datum and element. (source)
So, if you don't set up a key function, because you have always just 5 teams you don't effectively have working enter and exit selections when you change the division, but just an update one: as you're binding the data by their order, D3 thinks that Chicago Bulls and Atlanta Hawks are the same team.
Solution: set up a key function.
It can be simple as using the team abbreviation (supposing they are unique):
const update = svg.select('g.points')
.selectAll("rect")
.data(graphData, function(d){
return d.teamAbbrv;
});
PS: Just a question not related to your problem: why are you appending rects here? Since you have logoRadius, doesn't appending circles seem more natural? On top of that, the data representation would be more accurate, since the center of the circle, regardless its size, is at the correct datum coordinate. That's not the case with a rectangle, in which the coordinates (x, y) represent its top left corner.
I am new to D3.js. However after being practiced the examples of this site, I tried to play ahead with Mr.John Coogan's map given here. The output that I found in his site is as under
But when I am trying to do the same thing by placing his .js,css,.json and index.html in plunker it is coming as
Problems
a) No States are getting displayed
b) Zoom and Pan is not working
In another word, at this point of time I am looking only for the Indian map to work exactly as the example shown by Mr. Coogan.
What needs to be done for this?
Here's the working plunk: http://plnkr.co/1EqpIFecwJmkbvypTyQD?p=preview
You needed to uncomment this line:
.call(d3.behavior.zoom().on("zoom", redraw))
on line 40 of the index.html in your plunk, and then zoom and pan will work.
The state colors (based on wealth) are not showing because of various, more complex errors. The error shown in the console (svg is not defined referencing line 78) is just the start (you need to replace svg with india, which is defined).
In fact the whole original gist your example is based on is really just a work in progress, but most of the answers for how to fix it can be found in this thread from the google group, from Mike Bostock himself.
Essentially, the json data loads asynchronously, so need to be loaded in series.
// load the two JSON files in series
d3.json("states.json", function(states) {
d3.json("wealth.json", function(wealthdata) {
...
});
});
Then you can just apend the relevant colorbrewer CSS class when you first create each path:
india.selectAll("path")
.data(states.features)
.enter().append("path")
.attr("d", path)
.attr("class", function(d) {
return "q" + quantize(wealthdata[d.id]) + "-9";
})
.attr("d", path);
But you also need to define the quantize scale, range...:
var quantize = d3.scale.quantize()
.range(d3.range(9));
... and domain (which you can only do once the data has been loaded:
quantize.domain([0, d3.max(d3.values(wealthdata))]);
This is a follow up question to my other one.
http://fiddle.jshell.net/zw8TR/16/
I have managed to utilise d3 to visualise the route between my map markers. I used this example by Mike Bostock as guidance.
The reason I'm using d3 for this instead of Leaflets built in Polyline, is because I'd like to experiment with d3's interpolation to smooth out some routes, and also create arc's for others (for flight routes). At this stage though, I'm just trying to find a way to get these to work for all routes.
The examples I've seen only use the interpolate() method with d3.svg.line(), whereas the Leaflet integration requires me to use d3.geo.path(). Is there a place in my code where it's possible to use this method with d3.geo.path()?
Another possibly helpful link.
And another.
Thanks for any help.
Doing this is a bit messier than using d3.geo.path because that already implements all the boilerplate functionality you need for maps, but it's certainly possible. The idea is to extract the list of user coordinates from the geo path and translate them to screen coordinates in the line function. This translation can be done in the .x() and .y() functions of the line.
var path1 = d3.svg.line()
.interpolate("cardinal")
.x(function(d) { return map.latLngToLayerPoint(new L.LatLng(d[1], d[0])).x; })
.y(function(d) { return map.latLngToLayerPoint(new L.LatLng(d[1], d[0])).y; });
Now we just need to extract the coordinates from the feature.
feature.attr("d", function(d) { return path1(d.geometry.coordinates); });
Complete example here.
I want to draw the outline of Berlin (germany) with D3.js.
The geoJSON file for Berlin i use is this (there Berlin-"bundesländer"):
http://opendatalab.de/projects/geojson-utilities/
But it just doesn't work with my .json file. I cant spot the difference to this .json file for us-states that works just fine with exactly the same code:
http://examples.oreilly.com/0636920026938/chapter_12/us-states.json
I also tried to completely copy the us-states file, delete all "features" entries except one and then only replace the coordinates-array of the one left with the coordinates-array of my file. But firebug tells me that the path's element has no "d" attribute at all.
I really appreciate any hint. Thank you in advance!
/*
I use the default albersUsa projection.
If I define path as : d3.geo.path().projection(null) some path-element is added.
But if that's the solution how can i translate that path ?
*/
var path = d3.geo.path();
d3.json("data/berlinBundeslaender_simplify2000.json",function(geoJson){
var color1 = d3.scale.category20();
mainSVG.selectAll("path")
.data(geoJson.features)
.enter()
.append("path")
.attr("d",path)
.attr("fill",function(d,i){return color1(i);});
});
The albersUSA projection will clip anything that's not within the lower 48 US states, Hawaii or Alaska. That is, anything else simply won't be displayed. Use a different projection for different parts of the world.