I have a bunch of data that is coded with the Census FIPS code for states and counties (i.e. New York is FIPS 36, Kings County is FIPS 36047). I'm mapping that data using the d3.geo.albersUSA projection from the TopoJSON file here, which uses FIPS codes as the IDs for the state and county features. This is great for choropleths, where I just need to join on ID.
However, I want to draw lines from the centroid of one feature to another using the path.centroid(feature) and the LineString path type. Here's a simplified example of my data:
Start_State_FIPS, End_State_FIPS, Count_of_things
2,36,3
1,36,13
5,36,5
4,36,8
6,36,13
8,36,3
I'm using this same data to plot circles on the map, using the count_of_things field to set the radius. That's working no problem. To set up the lines, I created a map var with the FIPS code and the feature centroid, then used the FIPS code key to pull the start-end points from my data.
My code is drawing lines, but definitely not between centroid points. I didn't think I needed to do anything with the projection of the points, since they're coming from the features that are already adjusted for the map projection, but maybe I'm wrong. Here's my code:
var arclines = svg.append('g')
data_nested = d3.map(my_data)
var state_points = new Map();
var statesarc = topojson.feature(us, us.objects.states).features
statesarc.forEach(function(d) {
state_points.set(d.id, path.centroid(d))
})
arcdata = []
data_nested.values().forEach(function(d) {
arcline = {source: state_points.get(parseInt(d.Start_State_FIPS)), endpoint: state_points.get(parseInt(d.End_State_FIPS))}
arcdata.push(arcline)
})
arclines.selectAll("path")
.data(mydata)
.enter.append("path")
.attr('d', function(d) { return path({type: "LineString", coordinates: [d.source, d.endpoint]}) })
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])
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.
I am using a tutorial to learn how to generate maps in D3.v3, but I am using D3.v4. I am just trying to get some circles to appear on the map (see below). The code works except that the circles are over Nevada and should be in the Bay Area. I imagine this is a mismatch between projections of the map and the projected coordinates. I am not sure what projection the map is in, but I have tried to force it to be albersUsa (see commented out commands where I generate path) but this causes the entire map to disappear. Any help would be appreciated!
<!DOCTYPE html>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://d3js.org/topojson.v2.min.js"></script>
<script>
var w = 960,
h = 600;
var projection = d3.geoAlbersUsa();
var path = d3.geoPath()
//.projection(projection)
d3.json("https://d3js.org/us-10m.v1.json", function(error, us) {
if (error) throw error;
var svg = d3.select("body").append("svg")
.attr("width", w)
.attr("height", h);
svg.selectAll("path")
.data(topojson.feature(us, us.objects.states).features)
.enter().append("path")
.attr("class", "states")
.attr("d", path);
svg.append("path")
.attr("class", "state-borders")
.attr("d", path(topojson.mesh(us, us.objects.states)))
svg.append("path")
.attr("class", "county-borders")
.attr("d", path(topojson.mesh(us, us.objects.counties)));
aa = [-122.490402, 37.786453];
bb = [-122.389809, 37.72728];
svg.selectAll("circle")
.data([aa,bb]).enter()
.append("circle")
.attr("cx", function (d) { return projection(d)[0]; })
.attr("cy", function (d) { return projection(d)[1]; })
.attr("r", "8px")
.attr("fill", "red")
});
</script>
Your US json is already projected, and to show it you use a null projection:
var path = d3.geoPath()
//.projection(projection)
Without defining a projection, your topojson/geojson coordinates will be translated to straight pixel coordinates. It just so happens that this particular topojson file has pixel coordinates that are within [0,0] and [960,600], almost the same size as a default bl.ock view. Without knowing the projection used too create that file you cannot replicated that projection to align geographic features to your data. Unless you place your features with pixel values directly and skip the projection altogether (not useful for points not near identifiable landmarks or where precision matters).
Your US topojson features disappear when projecting with a geoUsaAlbers() because you are taking pixel coordinates on a plane and transforming them to svg coordinates as though they were points on a three dimensional globe (d3 projections expect latitude longitude pairs).
Instead, use a topojson or geojson that is unprojected. That is to say it contains latitude/longitude pairs and project that data along with your points. See this bl.ock for an example with unprojected (lat/long pairs) json for the US using your code (but assigning a projection to path).
To check if you have latitude/longitude pairs you can view the geometry of these features in a geojson file easily and see if the values are valid long, lat points. For topojson, the topojson library converts features to geojson, so you can view the geometries after this conversion.
Here's an unprojected topojson of the US: https://bl.ocks.org/mbostock/raw/4090846/us.json
Let's say you really wanted to use the same topojson file though, well we can probably deduce the projection it uses. First, I'll show the difference between your projected points (by using an unprojected outline of the US) and the already projected topojson (the unprojected topojson is projected with d3.geoAlbersUsa() and the projected with a null projection):
Chances are the projection d3.geoAlbersUsa is optimized for a bl.ocks.org default viewport, 960x500. The unprojected dataset has a bounding box of roughly 960x600, so perhaps if we increase the scale by a factor of 600/500 and adjust the translate we can align our features in an svg that is 960x600:
var projection = d3.geoAlbersUsa();
var scale = projection.scale() * 600 / 500;
projection.scale(scale).translate([960/2,600/2])
var projectedPath = d3.geoPath().projection(projection);
And, this appears to align fairly well, I can't see the difference between the two:
Here's a block showing the aligned features.
But as I mention in the comments, even if you can align the features:
any zoom or centering would be problematic as you need to use a geoTransform on already projected data but a geoProjection on the raw geographic data. Using all (uniformly) projected data or all unprojected data makes life simpler.
I downloaded .geojson files from mapzen metro extracts that is supposed to show the outline of a neighborhood. However, when I run the javascript code that I have written, nothing is appended to the "g" element and thus nothing shows up.
Here is the code that I have now:
var canvas = d3.select("body").append("svg")
.attr("width", 760)
.attr("height", 700);
d3.json("wayland.geojson", function (data){
console.log(data);
var nb = canvas.append("g")
.attr("class","nb");
var group = nb.selectAll("path")
.data(data)
.enter()
.append("path")
.attr("d", d3.geoPath());
});
The geojson file in question is valid and is a Feature, so I was just wondering how to map such a file correctly.
Object {id: 85854865, type: "Feature", properties: Object, bbox: Array(4), geometry: Object}
You need to define a projection for d3.geoPath(), if you do not specify a projection, d3 defaults to a null projection. A null projection takes geographic coordinates and simply turns them into svg/canvas coordinates with no transformation at all. Thus, if you have lat/long pairs in your geojson, only points from 0,0 to 90,180 will show, so anything in the western or sourthern hemispheres will not work. However, no errors will be produced because the null projection and the geoPath are working as expected.
Note, d3 projections take coordinates that use the WGS84 datum, that is latitude longitude pairs using the WGS84 ellipsoid, generally most lat long pairs will use this datum (GPS, google earth, etc). If your data is projected already, then d3 geoProjections are not what you need, you'll need geoTransforms
Instead of using a null projection, try to define a projection. There are a few ways to do this, one is the fitExtent method (as you are using d3 v4):
var projection = d3.geoMercator()
.fitExtent([[40,40],[width-40,height-40]], geojson);
This will take a set of geojson features (not topojson) and place a 40 pixel buffer around the features using a mercator map projection.
Another option is to look at your bbox coordinates and find the center of your area of interest (or use Google Earth etc) and set the projection manually:
var projection = d3.geoAlbers()
.center([0,y])
.rotate([-x,0])
.scale(10000);
Scale values increase as you zoom in, so starting with a low value is always useful to ensure you are looking in the right area. Neighborhood level details will be very zoomed in though.
Projection type won't be too important at a neighborhood level, but setting the parameters correctly will be.
Lastly, you'll need to make sure your geoPath uses your projection:
geoPath.projection(projection);
I am drawing a map with D3.js importing it from a topojson file.
In order to draw it I user the following code:
d3.json(input_file, function(error, data) {
var units = topojson.feature(data, data.objects.countries),
view = d3.select("#map_view");
// ... SETUP OF THE PROJECTION AND PATH
view.selectAll("path")
.data(units.features)
.enter()
.append("path")
.attr("d", path);
}
This code will plots all the shapes defined in the input topojson file.
Now I want to remove few outlier small islands which I don't need which are located in a specified region in spherical coordinates.
Since now units object defines everything that is drawn, I tried to remove the shapes in the following way:
I identify the index of the country in the features list and index of the arc that lies in the desired region and remove the arc from the array:
var coordinates = units.features[5].geometry.coordinates,
outlierId = 23;
coordinates.splice(outlierId, 1);
Now when I check the coordinates array of this country I see no arc with coordinates in the excluded region.
But it is still drawn on the map although the modified units object enters the .data() method. It looks like information about the shapes is coming from somewhere else. How can it be?