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>
Related
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:
I'm trying to position labels on map overlapping-free by using using d3fc-label-label.js in combination with d3.js. While labeling the map by basic d3 functions works well, the approach with the help of d3fc-label-label.js (heavily inspired by this example) produces a map with all the labels placed in top left corner.
Here's the javascript part that does the job
var width = 1300,
height = 960;
var projection = d3.geoMercator()
.scale(500)
// Center the Map to middle of shown area
.center([10.0, 50.5])
.translate([width / 2, height / 2]);
// ??
var path = d3.geoPath()
.projection(projection)
.pointRadius(2);
// Set svg width & height
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
// var g = svg.append("g");
d3.json("europe_wgs84.geojson", function(error, map_data) {
if (error) return console.error(error);
// var places = topojson.feature(map_data, map_data.objects.places);
// "path" instead of ".subunit"
svg.selectAll("path")
.data(map_data.features)
.enter().append("path")
.attr("d", path)
.attr("class", function(d) { return "label " + d.id})
var labelPadding = 2;
// the component used to render each label
var textLabel = fc.layoutTextLabel()
.padding(labelPadding)
//.value(function(d) { return map_data.properties.iso; });
.value(function(d) { return d.properties.iso; });
// use simulate annealing to find minimum overlapping text label positions
var strategy = fc.layoutGreedy();
// create the layout that positions the labels
var labels = fc.layoutLabel(strategy)
.size(function(_, i, g) {
// measure the label and add the required padding
var textSize = d3.select(g[i])
.select('text')
.node()
.getBBox();
return [textSize.width + labelPadding * 2, textSize.height + labelPadding * 2];
})
.position(function(d) { return projection(d.geometry.coordinates); })
.component(textLabel);
// render!
svg.datum(map_data.features)
.call(labels);
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.0/d3.min.js"></script>
See the gist that includes the data and a HTML file.
I would guess the issue is related to append the labels correctly to path of the map. Sadly, I haven't figured it out and would greatly appreciate any help!
I believe the problem lies in the fact that you are not passing single coordinates as the label's position.
layoutLabel.position(accessor)
Specifies the position for each item in the associated array. The
accessor function is invoked exactly once per datum, and should return
the position as an array of two values, [x, y].
In the example you show, that you are basing the design on, the variable places contains point geometries, it is to these points that labels are appended. Looking in the topojson we find places looking like:
"places":{"type":"GeometryCollection","geometries":[{"type":"Point","coordinates":[5868,5064],"properties":{"name":"Ayr"}},{"type":"Point","coordinates":[7508,6637],"properties":{"name":"Aberdeen"}},{"type":"Point","coordinates":[6609,5933],"properties":{"name":"Perth"}},...
Note that geometries.coordinates of each point contains one coordinate. However, in your code, d.geometry.coordinates contains an array of coordinates as it contains the boundary points of the entire path of each feature. This will cause errors in label placement. Instead, you might want to use path.centroid(d), this will return a single coordinate that is at the center of each country/region/path. Placement might not be perfect, as an extreme example, a series of countries arranged as concentric rings will have the same centroid. Here is a basic block showing placement using path.centroid (this shows only the placement - not the formatting of the labels as I'm not familiar with this library extension).
If you were wondering why the linked example's regional labels appear nicely, in the example each region has a label appended at its centroid, bypassing d3fc-label-layout altogether:
svg.selectAll(".subunit-label")
.data(subunits.features)
.enter().append("text")
.attr("class", function(d) { return "subunit-label " + d.id; })
.attr("transform", function(d) { return "translate(" + path.centroid(d) + ")"; })
.attr("dy", ".35em")
.text(function(d) { return d.properties.name; });
I've been following the tutorial here: http://www.delimited.io/blog/2015/5/16/interactive-webgl-globes-with-threejs-and-d3
This example uses a Canvas element and then wraps this as a texture to the THREE SphereGeometry. I wasn't happy with how blurry the lines were rendering so opted to render as an SVG. The SVG path data is being constructed by D3 and topojson.
My question is this: can I translate this SVG data to a SphereGeometry texture? I'd like to be able to zoom and retain detail on the final object - if there's a way to do this with canvas I'd also be interested.
Here's my code that draws the SVG;
var width = 1024,
height = 512;
var projection = d3.geo.equirectangular()
.scale(163)
.translate([width / 2, height / 2]);
var path = d3.geo.path()
.projection(projection);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
d3.json("data/world.json", function(err, data) {
svg.append("path")
.datum(topojson.feature(data,data.objects.countries))
.attr("d", path)
.attr("class", "countries");
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.
I need to show value at corresponding place while mouseover a line/bar chart using d3.js
var toolTip = svg.selectAll("path")
.append("svg:title")
.text(getmouseoverdata(data)
);
function getmouseoverdata(d) {
return d;
}
Here i get all the data in the array while mouseover at any place in the graph.
But I want to show the value at corresponding place. How can I achieve it?
You can use d3js mouse event handler like this
var coordinates = [0, 0];
coordinates = d3.mouse(this);
var x = coordinates[0];
var y = coordinates[1];
It will provide you the current mouse coordinates.
If you're only looking to display the data elements when you mouseover the path/rect elements, you could try to add the titles directly onto those elements while they are being created?
For example:
var data = [0, 1, 2, 3, 4, 5],
size = 300;
var canvas = d3.select("body")
.append("svg:svg")
.attr("width", size)
.attr("height", size);
var pixels = size / data.length;
canvas.selectAll("rect").data(data).enter().append("svg:rect")
.attr("fill", "red")
.attr("height", function(d){return d * pixels})
.attr("width", pixels/2)
.attr("x", function(d,i){return i * pixels})
.attr("y", 0)
.append("title") //Adding the title element to the rectangles.
.text(function(d){return d});
This code should create five rectangles with their data element present in a tooltip if you mouseover the rectangle.
Hope this helps.
Edit based on comment:
For a bottom to top graph, you can change the y attribute like so:
.attr("y", function(d){return size - d * pixels})
This addition will cause the bar to start at the difference between the maxHeight of your graph and the size of the bar, effectively turning a top-to-bottom graph into a bottom-to-top graph.