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.
Related
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>
I have a world map made with d3js v4 and topojson which has Zoom / Drag / Circles. Everything seems fine except I cant scale the circles togheter with the zoom.
When I scroll into the map, my circles stay at the same size, which makes them way to big compared to the map.
How can I apply the transformation to the circles when I zoom?
var width = 660,
height = 400;
var zoom = d3.zoom()
.scaleExtent([1, 10])
.on("zoom", zoomed);
var projection = d3.geoMercator()
.center([50, 10]) //long and lat starting position
.scale(150) //starting zoom position
.rotate([10,0]); //where world split occurs
var svg = d3.select("svg")
.attr("width", width)
.attr("height", height)
.call(zoom);
var path = d3.geoPath()
.projection(projection);
var g = svg.append("g");
//Zoom functionality
function zoomed() {
const currentTransform = d3.event.transform;
g.attr("transform", currentTransform);
}
d3.select(".zoom-in").on("click", function() {
zoom.scaleBy(svg.transition().duration(750), 1.2);
});
d3.select(".zoom-out").on("click", function() {
zoom.scaleBy(svg.transition().duration(750), 0.8);
});
// load and display the world and locations
d3.json("https://gist.githubusercontent.com/d3noob/5193723/raw/world-110m2.json", function(error, topology) {
var world = g.selectAll("path")
.data(topojson.object(topology, topology.objects.countries).geometries)
.enter()
.append("path")
.attr("d", path)
;
var locations = g.selectAll("circle")
.data(devicesAll)
.enter()
.append("circle")
.attr("cx", function(d) {return projection([d.LastLocation.lon, d.LastLocation.lat])[0];})
.attr("cy", function(d) {return projection([d.LastLocation.lon, d.LastLocation.lat])[1];})
.attr("r", 2)
.style("fill", "black")
.style("opacity", 1)
;
var simulation = d3.forceSimulation()
.force('x', d3.forceX().x(function(d) {return projection([d.LastLocation.lon, d.LastLocation.lat])[0]}))
.force('y', d3.forceY().y(function(d) {return projection([d.LastLocation.lon, d.LastLocation.lat])[1]}))
.force("charge", d3.forceManyBody().strength(0.5)) // Nodes are attracted one each other of value is > 0
.force("collide", d3.forceCollide().strength(.1).radius(2).iterations(2)) // Force that avoids circle overlapping
// Apply these forces to the nodes and update their positions.
// Once the force algorithm is happy with positions ('alpha' value is low enough), simulations will stop.
simulation
.nodes(devicesAll)
.on("tick", function(d){
locations
.attr("cx", function(d){ return d.x; })
.attr("cy", function(d){ return d.y; })
});
If i understood your problem correctly, you need to add it to your zoom behaviour.
//Zoom functionality
function zoomed() {
const currentTransform = d3.event.transform;
g.attr("transform", currentTransform);
}
here, you are applying your transformation to the elements, which is fine. However, you're not applying any logic to the radius.
That logic is up to you to make, and it will depend on the k property of the transform event (currentTransform.k).
I will use a some dummy logic for your radius. Your scale extent is between 1 and 10, you need a logic in which the radius decreases as the zoom increases (bigger k). It is also important that your radius doesn't go lower than 1, because the area of the circle will decrease much faster (remember the area depends on r^2, and r^2 < r for r < 1)
So my logic will be: the radius is 2.1 - (k / 10). Again, I'm oversimplifying, you can change it or tune it for your specific case.
In the end, it should look something like this:
//Zoom functionality
function zoomed() {
const currentTransform = d3.event.transform;
g.attr("transform", currentTransform);
g.selectAll("circle")
.attr("r", 2.1 - (currentTransform.k / 10))
}
I haven't tested the code, but tell me if this works! Maybe you can add it to a jsfiddle if needed
I have a rect holding a clip path, which I'm applying to a group (holding a tree). I have a zoom function bound to the rect which transforms the group, which works fine. I've applied the clip path to the group, and when it first renders it looks like it should. However, after panning or zooming, the drawn tree extends beyond the bounds of the clip path while maintaining its previously-clipped appearance.
var svg = d3.select(this.$.chart);
var svg2 = svg.select("svg");
var main = svg2.append("g")
.attr("class","main")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var treeContainer = svg2.append('g')
.attr("transform", "translate(" + margin2.left + "," + margin2.top + ")")
var treeBaseRect = treeContainer.append("rect") // the rectangle which holds the clip path and zoom actions for the tree.
.attr("width", width + margin2.right)
.attr("height", height2)
.style("fill", "#eee")
.style("pointer-events", "all")
.call(d3.zoom().scaleExtent([0.1, 3]).on("zoom", function () {
svgGroup.attr("transform", d3.event.transform)
}));
treeContainer.append('defs').append("clipPath")
.attr("id", "clip")
.append("rect")
.attr("width", width + margin2.right)
.attr("height", height2);
var svgGroup = treeContainer.append("g")
.attr("clip-path","url(#clip)");
Here's some screenshots. First one shows the initial render, which is fine (the clip area is the darker grey rectangle):
Then after doing a scroll zoom or pan, note how the tree is still 'originally' clipped, and not being clipped properly outside of the gray area:
And this is my clip path rect in the DOM structure:
You can tell that the clip rect is still where it's meant to be, but the tree is completely ignoring it. No idea why.
Apparently I hadn't added enough groups. The easy solution was to add another append("g") to the svgGroup I was creating, for a last line looking like this:
var svgGroup = treeContainer.append("g")
.attr("clip-path","url(#clip)")
.append("g");
D3.js panning appears to be slower and more choppy than zooming when the svg has many elements. I made an example on JSFiddle http://jsfiddle.net/cornhundred/cfeu1ws2/10/ and the code is also shown below
var num_rect = 3000;
var zoom = d3.behavior.zoom()
.on("zoom", zoomed);
function zoomed() {
console.log('zooming or panning');
d3.select('svg')
.select('#rect_group')
.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")");
}
svg = d3.select("body")
.append("svg:svg").attr("width", 800).attr("height", 800)
.call(zoom);
var rect_group = d3.select('svg')
.append('g')
.attr('id', 'rect_group');
var data = _.range(num_rect);
var color = d3.scale.linear().domain([0, num_rect]).range(['red', 'blue']);
rect_group.selectAll('rect')
.data(data)
.enter()
.append('rect')
.attr('height', 175)
.attr('width', 5)
.attr('x', function (d) {return 50 + d / (0.003 * num_rect);})
.attr('y', 50)
.style('fill', function (d) {return color(d);});
In this example I'm appending num_rects rectangles onto the svg side-by-side and adding some coloring so its clear there are many rectangles. I'm also making a console log whenever the zoomed function is called.
Increasing num_rect above ~3000 caused panning to be choppy while zooming remained smooth. This can ben seen in the behavior of the visualization or from the frequency of the console logs - 'zooming or panning'. This is odd since I would expect panning to be as cpu intensive than zooming.
This behavior also appears to be browser-specific - I'm only seeing this in chrome (which is also odd since Chrome is usually the best at rendering D3.js visualizations).
I'm trying to replicate http://bl.ocks.org/mbostock/4060606 using a UK Counties map.
I followed the following steps - pretty much what is suggested on http://bost.ocks.org/mike/map:
1) I downloaded the shapefile from Ordnance Survey and extracted some data using qGIS
2) when ready, I translated the shapefile into GeoJSON using ogr2ogr
3) I converted the GeoJSON into topoJSON making sure the IDs were preserved
I pretty much copied the original example for the choropleth from mbostock. However, instead of a nice map, I get a... circle. I wonder if I'm doing some errors with the projection?
For completeness, this is the javascript part of the page:
var width = 960,
height = 600;
var rateById = d3.map();
var quantize = d3.scale.quantize()
.domain([0, .15])
.range(d3.range(9).map(function(i) { return "q" + i + "-9"; }));
var projection = d3.geo.albers()
.center([0, 55.4])
.rotate([4.4, 0])
.parallels([50, 60])
.scale(50)
.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);
queue()
.defer(d3.json, "uk.topo.json")
.defer(d3.tsv, "unemployment.tsv", function(d) { rateById.set(d.id, +d.rate); })
.await(ready);
function ready(error, uk) {
svg.append("g")
.attr("class", "counties")
.selectAll("path")
.data(topojson.feature(uk, uk.objects.counties).features)
.enter().append("path")
// .attr("class", function(d) { return quantize(rateById.get(d.id)); })
.attr("class", "q5-9" )
.attr("d", path);
// svg.append("path")
// .datum(topojson.mesh(us, us.objects.states, function(a, b) { return a !== b; }))
// .attr("class", "states")
// .attr("d", path);
}
d3.select(self.frameElement).style("height", height + "px");
The counties topoJSON is too big to be pasted here, but roughly it's:
{"type":"Topology","objects":{"counties":{"type":"GeometryCollection","bbox":[220956.7,35190.3,655967.1,586683],"geometries":[{"type":"Polygon","properties":{"name":"Buckinghamshire"},"id":11901,"arcs":[[-1,-2,-3,-4,-5,-6]]},{"type":"Polygon","properties":{"name":"Cambridgeshire"},"id":1386,"arcs":[[-7,-8,-9,-10,-11,-12,-13,-14]]},{"type":"Polygon","properties":{"name":"Cumbria"},"id":13244,"arcs":[[-15,-16,-17]]},{"type":"Polygon","properties":{"name":"Derbs"},"id":13688,"arcs":[[-18,-19,-20,-21],[-22]]},...},"arcs":[[[5876,2688],[-67,53],[-21,101],[7,65],[96,66],[-7,66],[-78,69],[-234,12],[-5,42],...},"transform":{"scale":[43.5053905390539,55.15478547854785],"translate":[220956.7,35190.3]}}
I'm not a great expert here so I might be doing something fundamentally wrong. However, I have one certainty:
the UK counties map is correct, as it displays correctly on http://www.mapshaper.org/
Any idea? I'm happy to paste the complete files if needed.
Thanks!
The coordinates seem to be already projected (i.e. cartesian coordinates).
In this case you should use
d3.geo.path().projection(null);
But make sure you scale your topojson first to the desired size
topojson --width=960 --height=500 --margin=10 --cartesian -o out.json -- in.shp
Or reproject the shapefile first using ogr2ogr
ogr2ogr -t_srs EPSG:4326 out.shp in.shp