d3 + leaflet path fill issue - javascript

I'm working on a timelapsed filled map using Leaflet as a baselayer and a d3 topojson file so I can color in some countries. I used http://bost.ocks.org/mike/leaflet/ to get started, and everything was going great until I tried to shade in the Russian Federation. Its landmass spans non-contiguous tiles, and when I try to add a fill style to my #RUS path, it behaves anomalously. Example is here: http://dataviz.du.edu/projects/scratch/study_abroad.html
Example will take 1.5 s to render completely, it shades 3 countries, with the Russian Federation shading last.
This example uses a topojson file that I have used in other, pure d3 projects and have filled #RUS in those contexts without this issue.
Can anyone help? Thanks in advance.

This example uses a topojson file that I have used in other, pure d3 projects and have filled #RUS in those contexts without this issue.
You must be mistaken because your TopoJSON file is actually corrupt. See here an example with that file straight from your server: http://plnkr.co/edit/QOTwV3?p=preview Mind that i'm using plain TopoJSON and Leaflet's GeoJSON layer but it's yielding the exact same results.
PS. Is there any reason as to why you're using D3 for this? Asking because what i see you doing can be done just using Leaflet and TopoJSON, without D3. Here's a simple example:
function delay(features) {
var geojsonLayer = new L.GeoJSON(null, {
style: getStyle,
}).addTo(map);
var delay = 100;
features.forEach(function(feature) {
delay = delay + 100;
setTimeout(function() {
geojsonLayer.addData(feature);
}, delay);
});
}
var url = 'http://crossorigin.me/http://dataviz.du.edu/projects/scratch/worldnew.json';
$.getJSON(url, function(data) {
var geojsonData = topojson.feature(data, data.objects.test);
delay(geojsonData.features);
});

Related

Geojson map with D3 only rendering a single path in a feature collection

I'm trying to draw a geojson map of some regions of Colombia. Currently it only shows a single path:,
My feature collection has 52 features, but I can only draw this one feature. I do not know what I'm doing wrong, I'm based my code on other tutorials. How can I do to show all the paths?
var features = mapData.features;
console.log(features);
// Update color scale domain based on data
// Draw each province as a path
mapLayer.selectAll('path')
.data(features)
.enter().append('path')
.attr('d', path)
.attr('vector-effect', 'non-scaling-stroke')
Here is my full code:
https://plnkr.co/edit/kSDtyyoWr9TSEDZ5Letv?p=preview
Problem
All your features are drawing, you are correctly using your path and enter cycle. To see, set your fill to none:
You can see them when inspecting the svg: all the paths are there.
Why don't you see them in the map when they have fill? Because the the polygons are inverted, they cover the entire world except for the region of interest. While most other geographic libraries/renderers treat geojson as Cartesian, D3 does not. This means winding order matters. Your coordinates are wound in the wrong order.
Solution
To properly fill, draw all features, and support mouse interaction, you'll need to reverse the winding order of the polygons. You can do this on the fly, or create new geojson files to store the pre-reversed data.
To do so let's take a look at your data. You are working with only features that are MultiPolygons, let's look at the structure:
{
type:"Feature",
properties: {...},
geometry: {
type: "MultiPolygon",
coordinate: /* coordinates here */
}
}
Coordinates are structured as so:
coordinates:[polygons] // an array of polygons
The individual polygons are structured as so:
[outer ring][inner ring][inner ring]... // an array of coordinates for an outer ring, an array of coordinates for each hole (inner ring).
Polygon rings are structured as an array of long lats, with the first and last values being the same.
[x,y],[x,y]....
So, to reverse the ordering of the coordinates, we need to reverse the items in the ring arrays:
features.forEach(function(feature) {
if(feature.geometry.type == "MultiPolygon") {
feature.geometry.coordinates.forEach(function(polygon) {
polygon.forEach(function(ring) {
ring.reverse();
})
})
}
})
If we had polygons in the mix too (they are slightly less nested), we could use:
features.forEach(function(feature) {
if(feature.geometry.type == "MultiPolygon") {
feature.geometry.coordinates.forEach(function(polygon) {
polygon.forEach(function(ring) {
ring.reverse();
})
})
}
else if (feature.geometry.type == "Polygon") {
feature.geometry.coordinates.forEach(function(ring) {
ring.reverse();
})
}
})
Here's an updated plunker
It is drawing all the paths. See the DOM for the SVG in a web page inspector to confirm. However, you are seeing only the top one which happens to be the larger area because of fills. Try adding .style('fill', 'none') to the paths addition in JS. or the following in CSS
path {
fill: none
}
If someone will see a similar problem, I created a tool which will help you to rewind or reverse geojson
https://observablehq.com/#bumbeishvili/rewind-geojson
You can run it as a snippet just bellow
<div class="notebook-content">
</div>
<script type="module">
import notebook from "https://api.observablehq.com/#bumbeishvili/rewind-geojson.js"; // "download code" url
document.querySelector('.notebook-content').innerHTML =notebook.modules[0].variables
.filter(d=>d)
.map((d,i)=>` <div class=" observable-wrapper div-number-${i}"></div>`)
.join('')
.concat('<div style="display:none" class="hidden"></div>')
import {Inspector, Runtime} from "https://unpkg.com/#observablehq/runtime#3/dist/runtime.js";
let i=1;
Runtime.load(notebook, (variable) => {
if(i==4 ){i++; return new Inspector(document.querySelector(`.hidden`));}
if(i==13)return;
return new Inspector(document.querySelector(`.observable-wrapper:nth-child(${i++})`));
});
</script>

Get KML style in OpenLayers 5

I have some code in an application that access the style of a selected feature in a KML layer. It was working in OpenLayers 3.1. I have now upgraded to 5.3.0 and it stopped working. See the relevant lines below:
var featStyle = feature.getStyleFunction().call(feature, map.getView().getResolution());
var strokeWidth = featStyle[0].getStroke().getWidth();
var strokeColor = featStyle[0].getStroke().getColor();
var fillColor = featStyle[0].getFill().getColor();
var fillOpacity = (Math.round(fillColor[3] * 100));
The line:
var featStyle = feature.getStyleFunction().call(feature, map.getView().getResolution());
Produces an error visible in the developer console:
TypeError: o.getGeometry is not a function[Learn More] KML.js:943
a KML.js:943
myFunctionName file.php:5371
onclick file.php:1
I can't find anything in the documentation or examples that shows how to properly access the KML style data for a given feature (not an entire layer/source). Is there a new way to do this or did I miss something?
Could it have to do with this?: https://github.com/IGNF/geoportal-sdk/issues/2 Plugged into Google translate it seems to say something about no longer storing style properties inside each feature but it does not seem to say where they are stored...
The KML is valid and displays on the map properly. I just can't seem to find a way to access the style data anymore.
In OpenLayers 3 and 4 a feature style function takes only a resolution argument but internally uses this so the function or call must be bound to the feature:
feature.getStyleFunction().bind(feature)(map.getView().getResolution());
or
feature.getStyleFunction().call(feature, map.getView().getResolution());
In OpenLayers 5 feature style function are similar to layer style functions and require the feature to be passed as an argument:
feature.getStyleFunction()(feature, map.getView().getResolution());

In D3, how to use SVG files as DOM objects with bound data and paths still exposed?

This is a problem that I was having, and solved, which seemed useful enough to share, at least because I suspect I will have the problem again.
Issue: Read in SVG image and expose its path elements
Create scatterplot points which are actually cute, little SVG images from files, with the SVG paths exposed. The interest in D3.js is at least partly due to its "infinite flexibility", and if I wanted just a simple plot, there are easier ways than D3.
This means:
I wanted to be able to read in the image from an SVG file.
I still wanted to be able to access the SVG paths. In this case, I wanted to be able to adjust the line thicknesses of lines in the logo, based upon bound data (similar to how we might adjust the circle radius based upon data).
Any way that I tried to do this importing the svg file as an image, for instance via <selection>.append('svg:image') or something similar, did not expose the SVG path details.
I solved this, and I am posting my solution below, in hopes of helping others.
Solution: d3.xml plus d3.queue
I needed to set up a d3.queue, where one external call was bringing in the data for the visualization, and the other was bringing in the SVG image mentioned above.
The entire visualization became fairly large and complex; the code below captures the relevant parts as I'd used them.
// First create helper function with callback return, to provide structure `d3.queue` needs.
readSvg = function(svg_path, callback) {
d3.xml(svg_path).mimeType("image/svg+xml")
.get(function(err, xml) {
if (err) {
throw err;
}
callback(null, xml.documentElement.outerHTML);
});
};
// Next make the `d3.queue` call.
// (In reality, the `makeViz` function would need to be defined first, but
// logically, I felt it easier to understand if I show this first.)
d3.queue()
.defer(readSvg, my_svg_image_path)
.defer(d3.json, my_data_path)
.await(makeViz);
// "Finally" display the visualizations using `makeViz`.
var makeViz = function(error, svg_image, data) {
if (error) {
console.log(error);
} else {
var points = svg.selectAll('.point')
.data(data).enter()
.append('g')
.classed('point', true)
.style('fill', my_fill)
.style('stroke-opacity', my_opacity)
.html(svg_image); // Note the `html` call!!!
// These circles are added to make the icons more easily "clickable".
// NOTE: THE VARIABLE "points" IS STILL ASSOCIATED WITH THE IMAGE, NOT
// THE CIRCLES BELOW!!! THIS IS ON PURPOSE, FOR EASIER
// ASSOCIATION AND MAPPING LATER!!
points.append('circle').attr('r', icon_size);
// Now call a function that associates all of the x & y coordinates according
// to the bound data and what the users click on. (`clicked` is a variable
// defined out of scope, and is part of the interactivity.)
updatePoints(data, points, clicked);
A few things to note:
The image had a lot of "empty space". Imagine if the image were a smiley face; most of the smiley face is nothing. Therefore, most of it is not clickable. I therefore created a parent g DOM element and attached the circle to it as well as the image. Therefore, the entire "smiley face" was clickable, including the white space.
By using this d3.xml.mimeType.get call, I was able to get the entire SVG structure of the file. Then sending on its .documentElement.outerHTML gave me just the parts I was looking for: all of the gs, svgs, paths, and all other elements of the image, and none of the other DOM stuff that comes along with an xml import.
Because I needed to import 2 things, I needed to use d3.queue. But because d3.queue works via an expected callback, I needed to structure that skeleton call with readSvg as I did. That function does not do much, except call the function that is passed to it, which is a function to return the data.
There might be a better or cleaner way to handle this part, but simply making a d3.xml call directly within d3.queue I could not get to work properly.
If anyone has better suggestions, that's great, but I can say this works.
Also, I'm always looking for tips, pointers, and suggestions for how to indent all these chained calls. Is the way I've shown here the best? I find it challenging to read d3 code sometimes because of the combination of chained calls and callback functions. (I'm in the process of adopting an async/await structure for some of my d3 work.)

With d3.js, how can I use the same script to create 3 charts with 3 datasources?

Imagine I have an index.html with #chart1 #chart2 and #chart3
I want to populate those three charts with three different data files, but using the same script.
Currently I just have the script copied 3 times in my script file, with the necessary updates made (i.e. swapping out #chart1 for #chart2 and 1.tsv1 for 2.tsv etc...
How can I do this more intelligently/elegantly?
An excellent tutorial by Mike Bostock outlining one possible way of doing this can be found here: Towards Reusable Charts
To try and summarize briefly, it proposes a standard way of defining reusable charts that fit nicely into the way D3 handles data and selections. First you define your chart:
function reusableChart() {
//variables, etc.
function my() {
// chart generation code
}
//getter-setter methods
return my;
}
You then would instantiate this chart generator, select elements and bind data to them in the usual D3 way, and finally call the generator function:
var myChart = reusableChart().setValue("example", 0);
d3.tsv("1.tsv", function(error, data) {
d3.select("#chart1").datum(data).call(myChart);
});
//etc..

Styling a geoJSON Feature Layer in MapBox

I just started playing with MapBox and am running into a confusing issue. I'm creating a map with a geoJSON layer using this code:
var map = L.mapbox.map('map', '<MapBoxID>');
var zipLayer = L.mapbox.featureLayer('data/blah.json');
zipLayer.addTo(map);
zipLayer.setStyle({color: 'red'});
The map appears and shows the geoJSON, but it ignores the styling. When I copy that last line into the JS console in my browser, it works fine, though.
What am I missing here? Also, I've tried at least a dozen different ways of including the style in the options directly in the featureLayer() call, but nothing has worked. How do I specify the style when creating the feature layer?
I'm guessing a bit here, since I don't know the Mapbox JS very well, but it sounds a lot like an async error. Strangely, I don't see anything in the Mapbox or Leaflet APIs about a callback for this function. But, you can pass straight GeoJSON to featureLayer(), so I'd suggest using jQuery (or your XHR library of choice) to grab the data:
var map = L.mapbox.map('map', '<MapBoxID>');
var zipLayer;
$.getJSON('data/blah.json', function(data) {
zipLayer = L.mapbox.featureLayer(data);
zipLayer.addTo(map);
zipLayer.setStyle({color: 'red'});
});
Hopefully that'll do the trick.
I would go the route of using the built-in featureLayer function, then listening for it to be ready. This should help get you pointed in the right direction:
var featureLayer = L.mapbox.featureLayer()
.loadURL('/example-single.geojson')
.on('ready', function(layer) {
this.eachLayer(function(marker) {
// See the following for styling hints:
// https://help.github.com/articles/mapping-geojson-files-on-github#styling-features
marker.setIcon(L.mapbox.marker.icon({
'marker-color': '#CC0000'
}));
});
})
.addTo(map);
Have you tried adding the zipLayer after setting the style?

Categories

Resources