Center and Rotate Projection in d3 v4 - javascript

I am trying to make a static, interactive map of Wisconsin counties.
I am using Albers Equal Area Conic projection and I have tried .rotate, .center, .fitExtent, but whenever I add these into the code, the map completely disappears.
Anyone know what could be going on?
Here's the code:
var margin = {top: 20, left: 20, bottom: 20, right: 20}
height = 600- margin.top - margin.bottom,
width = 960 - margin.left - margin.right;
var svg2 = d3.select("#map2").append("svg")
.attr("height", height)
.attr("width", width)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.right + ")");
d3.queue()
.defer(d3.json, "WiscCountiesNoProjection.json")
.await(ready);
var projection2 = d3.geoAlbers()
.translate([width/3, height/1])
.scale(4000)
var path2 = d3.geoPath()
.projection(projection2)
function ready (error, data) {
var counties = topojson.feature(data, data.objects.WiscCounties).features
svg2.selectAll(".counties")
.data(counties)
.enter().append("path")
.attr("class", "counties")
.attr("d", path2)
}
And here is what it looks like:

You don't go into the details of how you're calling the different methods, but here's a few general tips on how to work with them:
fitExtent or the short hand version fitSize should definitely make your object appear on the SVG if you're not applying any other transformations. A minimum working example would be:
const proj = d3.geoAlbers()
.fitSize([width, height], wisconsin)
That should result in a nicely fitted, albeit not correctly rotated Wisconsin. If it doesn't, wisconsin is not a valid GeoJSON object, ie not a FeatureCollection, single Feature or geometric object.
The next question is how to fix the rotation. For conic projections, as I understand, you generally want to find the center of your object of interest and rotate by the inverse of the longitude. A very friendly StackOverflow user explained the details here: https://stackoverflow.com/a/41133970/4745643
In the case of Wisconsin the center of the state has a longitude of almost exactly -90°, so we do this:
const proj = d3.geoAlbers()
.rotate([90, 0, 0])
.fitSize([width, height], wisconsin)
Note that we're rotating the map before we fit the object into our SVG. As a general rule of thumb, you should apply spherical transforms before zooming and fitting your map (I shall follow up with a more detailed explanation in a moment).
That should leave you with a nicely rotated and fitted state:

Related

d3.js alter projection on each iteration of the data loop

I'm very new to d3.js so my apologies if this is a stupid question.
When iterating over a geojson FeatureCollection list, i would like to change the projection on each item. is this possible?
My code looks something like this:
var width = 200,
var height = 200;
var svg = d3.select('#content g.map')
.append("svg")
.attr("width", width)
.attr("height", height);
var projection = d3.geoAlbers()
.fitExtent(
[
[0, 0],
[width, height],
],
features
);
let geoGenerator = d3.geoPath().projection(projection)
var group = svg.append('g');
group.selectAll('path')
.data(geojson.features)
.enter()
.append('path')
.attr('d', geoGenerator)
.attr('stroke-width', '3')
.attr('stroke', 'black')
.attr('fill', 'none');
I'm using geoAlbers() with .fitExtend(), where my projection is drawn according to all elements in the geojson file. But i would like to draw it for each element independently.
My goal is to create a plot where each element in the array is the same size. Any help is appreciated!
You can use projection.fitSize or projection.fitExtent to modify a projection so that it will project a geojson feature to fill the specified bounding box. Normally these methods are used for a feature collection or a single feature, but there is no reason that we can't use it for each feature.
projection.fitSize/fitExtent only modify a projection's translate and scale values: they only zoom and pan the projected features. To make a grid using a conical projection like an Albers, you'll want to recalculate the projection's sequant lines/parallels for each feature or you may risk severe distortion in shapes. The use of a cylindrical projection removes this need (I've used a Mercator to simplify a solution). However, you should, in certain cases, calculate an appropriate anti-meridian for each feature so that no feature would span it, most d3 projections use 180 degrees east west as the default anti-meridian, however, you can change this for each projection by rotating the projection with projection.rotate(). The use of d3.geoBounds or d3.geoCenter could help facilitate this, my solution below does not account for these edge cases.
The snippet below uses g elements, one per feature, for positioning, then appends a path, and, by using selection.each(), calculates the projection parameters needed using projection.fitSize() so that the features bounding box is of the right size (each bounding box is size pixels square).
d3.json('https://unpkg.com/world-atlas#1/world/110m.json').then(function(world) {
let width = 960,
height = 500,
size = 40;
let projection = d3.geoMercator()
let path = d3.geoPath(projection);
let svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
let grid = svg.selectAll(null)
.data(topojson.feature(world, world.objects.countries).features)
.enter()
.append("g")
.attr("transform", function(d,i) {
return "translate("+[i%16*size,Math.floor(i/16)*size]+")";
})
let countries = grid.append("path")
.each(function(feature) {
projection.fitSize([size,size],feature)
d3.select(this).attr("d", path);
})
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<script src="https://d3js.org/topojson.v2.min.js"></script>

d3.js Donut chart only draws a single cutoff slice inside a react component

I have made a piechart in javascript and I'm now trying to bring it inside a react component.
However, it only draws a single slice and that one isn't even complete it's cutoff.
Here are images of the cut off slice from the react component and how it looks just rendering in html.
I am not sure why.
This is my function drawing it:
function drawDonut(inputData: DealNumbers[]) {
const radius = Math.min(width, height) / 2;
const color = d3.scaleOrdinal(d3.schemeCategory10);
const arc = d3.arc()
.outerRadius(radius - margin.top)
.innerRadius(radius - radius * 0.5);
const pie = d3
.pie()
.padAngle(0)
.value((d) => d.deals);
svg
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom)
.append('g')
.attr('transform', `translate(${(width / 2) + margin.left}, ${(height / 2) + margin.top})`);
svg
.selectAll('.arc')
.data(pie(inputData))
.enter()
.append('g')
.attr('class', 'arc')
.append('path')
.attr('d', arc)
.style('fill', (d) => color(d.data.name));
It is pretty much the same except that I'm using a svg view box and I pass width and height as props instead of hardcoding it.
It draws perfectly fine from a javascript.
Thanks for any hints and tips you are able and take the time to give!
Best Regards,
Oliver
The issue was with the view box (moving the origin from 0,0 to -width, -height worked), a workaround for some reason I have had to copy intermediate variables
const pwidth = width
const pheight = height
and use these instead of width. Then I don't need to adjust the viewboxs origin. Which is a bit strange to say the least.
I won't accept this answer here as I have no idea why.
But adding it so someone else may find an answer if they're looking for it.

How to get map to show longitude and latitude accurately with d3

I'm using d3.js to try to map coordinates for a csv data file that contains an order id, latitude & longitude of the address the order was shipped to, and the amount that was spent on the order. I've tried mapping them out linearly and tried using a log scale, but the plot points still seem skewed. I was trying to get it to look like a map of the US which it slightly resembles, but the map seems warped/skewed. I made sure that the longitude was set to the x-axis and that latitude was set for the y-axis. The radius of the circles are related to the amount spent on orders. I'm wondering if it has something to do with the scale used, but this is my first time trying to mess with d3, so any help/advice would be appreciated!
var outerWidth = 500;
var outerHeight = 250;
var margin = { left: -50, top: 0, right: -50, bottom: 0 };
var xColumn = "longitude";
var yColumn = "latitude";
var rColumn = "total";
var dollarPerPixel = 10;
var innerWidth = outerWidth - margin.left - margin.right;
var innerHeight = outerHeight - margin.top - margin.bottom;
var svg = d3.select("body").append("svg")
.attr("width", outerWidth)
.attr("height", outerHeight);
var g = svg.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var xScale = d3.scale.log().range([0, innerWidth]);
var yScale = d3.scale.log().range([innerHeight, 0]);
var rScale = d3.scale.sqrt();
function render(data){
xScale.domain( d3.extent(data, function (d){ return d[xColumn]; }));
yScale.domain( d3.extent(data, function (d){ return d[yColumn]; }));
rScale.domain([0, d3.max(data, function (d){ return d[rColumn]; })]);
// Compute the size of the biggest circle as a function of dollarPerPixel.
var dollarMax = rScale.domain()[1];
var rMin = 0;
var rMax = Math.sqrt(dollarMax / (dollarPerPixel * Math.PI));
rScale.range([rMin, rMax]);
var circles = g.selectAll("circle").data(data);
circles.enter().append("circle");
circles
.attr("cx", function (d){ return xScale(d[xColumn]); })
.attr("cy", function (d){ return yScale(d[yColumn]); })
.attr("r", function (d){ return rScale(d[rColumn]); });
circles.exit().remove();
}
function type(d){
d.latitude = +d.latitude;
d.longitude = +d.longitude;
d.total = +d.total;
return d;
}
d3.csv("data.csv", type, render);
While scales may seem to be an appropriate method for plotting geographic points: don't use this approach.
You lose control over rotation of a projection and you cannot use a non cylindrical projection (only unrotated cylindrical projections can plot lat and long independently). But it also makes it very hard to align features positioned by scales with other map elements if they don't use the same approach.
Instead, D3 has a wide range of built in projections.
The projections take a [longitude,latitude] pair and return a [x,y] coordinate. Latitudes and longitudes must be in degrees, x and y are in pixels.
To create a projection you can use:
var projection = d3.geoMercator() // or geoAlbers, geoSupportedProjection, etc.
To use it, just pass it a coordinate:
projection([long,lat]) // [x,y]
In your case this might look like (for the cx, cy looks similar)
.attr("cx", function(d) { return projection([d.long,d.lat])[0] })
Now this projection is centered at 0,0 degrees by default and set up for a 960x500 pixel map. But you can modify scale, center and rotation, for example:
var projection = d3.geoMercator().center([-100,35]).scale(1000)
For a more complete run down of projection methods you should look at the documentation for d3-geo.
In your case there is a special composite projection that covers the US, d3.geoAlbersUsa, which has room for Hawaii and Alaska. But, because of its composite nature is less flexible, though you can still scale it. The default scale anticipates 960x600 pixel map (setting larger map scales spreads the map over a larger area).

Simplifying KML for use with D3

I am working with D3 Maps and have a fairly large KML. On each path I have a mouseover event attached that changes colour and displays a tooltip. All works, but the size of the polygon paths and their complexity slows interactivity down.
For my intended purpose, it is not necessary to have such high detail for the map. So I would like to slim down my KML and the polygons inside it, similar to this but without the interactivity.
MAP CODE
var width = 1000;
var height = 1100;
var rotate = 60; // so that [-60, 0] becomes initial center of projection
var maxlat = 55; // clip northern and southern poles (infinite in mercator)
// normally you'd look this up. this point is in the middle of uk
var center = [-1.485000, 52.567000];
// instantiate the projection object
var projection = d3.geo.conicConformal()
.center(center)
.clipAngle(180)
// size of the map itself, you may want to play around with this in
// relation to your canvas size
.scale(10000)
// center the map in the middle of the canvas
.translate([width / 2, height / 2])
.precision(.1);
var zoom = d3.behavior.zoom()
.scaleExtent([1, 15])
.on("zoom", zoomed);
var svg = d3.select('#map').append('svg')
.attr('width', width)
.attr('height', height);
var g = svg.append("g");
svg.call(zoom).call(zoom.event);
var tooltip = d3.select("body")
.append('div')
.style('position', 'absolute')
.style('z-index', '10')
.style('visibility', 'hidden')
.attr('class', 'county-info')
.text('a simple tooltip');
var path = d3.geo.path().projection(projection);
d3.json("data/map-england.json", function(err, data) {
g.selectAll('path')
.data(data.features)
.enter().append('path')
.attr('d', path)
.attr('class', 'border')
.attr('stroke-width', '.5')
.attr('id', function(d) { return d.properties.Name.replace(/ /g,'').toLowerCase(); })
.on("mouseover", function(d) {
d3.select(this).classed("active", true );
tooltip
.style('left', (d3.event.pageX - 15) + 'px')
.style('top', (d3.event.pageY - 50) + 'px')
.text(d.properties.Description)
.style("visibility", "visible");
})
.on("mouseout", function(d) {
d3.select(this).classed("active", false );
tooltip.style('visibility', 'hidden');
});
});
function zoomed() {
g.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")");
}
Is there an online tool where I can upload my KML and have it give me back the same KML but simplified?
If not, are there any easy examples that show how one could simplify the paths without any extra interactive code?
D3 Map Pan and Zoom Performance on Chrome
I had to simplify the paths using the node TopoJson package as Tom suggested. However I couldn't get this to work in Windows! So much hassle with dependencies and pack versions ...etc.
After much pain, I decided getting it to work in Windows was mission impossible. So I went and created a Virtual Machine running Ubuntu. I was up and running with node and TopoJson in no time.
After simplifying the paths, the map, hover and everything was super smooth.

Drawing Kenyan counties d3.geo.path()

I'm a bit of a newb with D3 and I'm trying to work on a project for the organization I work for. I need to draw a choropleth map of Kenya with some data we collected. I'm working off Scott Murray's book Interactive Data Visualization for the Web. In his book he uses the following to generate paths from a json file of US States:
//Width and height
var w = 500;
var h = 300;
//Define default path generator
var path = d3.geo.path();
//Create SVG element
var svg = d3.select("body")
.append("svg")
.attr("width", w)
.attr("height", h);
//Load in GeoJSON data
d3.json("us-states.json", function(json) {
//Bind data and create one path per GeoJSON feature
svg.selectAll("path")
.data(json.features)
.enter()
.append("path")
.attr("d", path);
});
I tried adapting this code to draw Kenyan counties from a json file I created from the Kenya shapefile I downloaded. The structure of the json file looks just like that of the US states file but when I look at the HTML in a browser I don't see any lines. I check the console and the path placeholders are there there is no data. If I swap in the US-states.json file I see the paths with data and the map in the browser.
Can someone help me please.
Thanks
I am doing something similar for Nairobi. As Lars has said in the comments it looks like you haven't set a projection for the map. The code below uses a mercator projection and the map is centered on Nairobi.
(Note that the scale is very zoomed and you would have to decrease this to get the whole of Kenya in).
var margin = {top: 50, right: 20, bottom: 20, left: 60},
dynwidth = $("#nairobistock").width(),
rowheight = 460;
width = dynwidth - margin.left - margin.right,
height = rowheight - margin.top - margin.bottom;
var projection = d3.geo.mercator()
.center([36.8, -1.3])
.scale([90000])
.translate([width/2, height/2]);
var nairobipathing = d3.geo.path().projection(projection);
var svg = d3.select("#nairobistock").append("svg")
.attr("width", (width + margin.left + margin.right) )
.attr("height", (height + margin.top + margin.bottom) );
d3.json("topojson/KEN-3topo.json", function(error, nairobi){
if (error) return console.error(error);
console.log(nairobi);
console.log("topojson added");
svg.selectAll("path")
.data(topojson.feature(nairobi, nairobi.objects.suburbs).features)
.enter()
.append("path")
.attr("class", function(d) {return d.ID;})
.attr("d", nairobipathing );
});
Hope this helps.

Categories