This question is a follow-up from my previous post .
After successfully creating a World Bubble Map based on geolocation on Observable Notebook, with help from Chris.
I want to make my Notebook user-friendly. For instance, I have already some sets of data that are plotted as bubbles according to parameter selection. Now what I want to achieve is, if somebody wants to use their own set of data on my Observable Notebook I want their data to be plotted as bubbles based on geolocation of the countries (same as my data).(Make a path where they can upload their own data) And since the dropdown menu content has been manually modified according to the parameters name, will it be possible to make the dropdown menu content change automatically when someone adds a new set of data ?
How can I do something like this ? Any suggestions? Thank you in advance.
A link to my Notebook : Bubble Maps
This is partial solution with some open Issues :
I have created a second map where users can use their own set of data to plot the bubbles. Although, it will only work if the Country Code of uploaded .CSV data is the same as ISO_A2 (Country Code)from the .JSON file used in my Notebook. There are some issues I could not resolve. Mainly 1 . Some countries appear on the top left corner of the map which is not actually correct (the second map should actually plot the countries like the first map as both map has same longitude and latitude) 2. The same zoom function is working for the first map but not for the second map (that is kind of confusing me) 3. Will it be possible to generate input buttons automatically from the uploaded file, or should it be done manually every time. If I could get some help and suggestions, it would be so great. I have included a dummy data in my notebook (in the data paneāFile Attachments) which can be used to test the second map. Here is the link : Bubble Map
The Radio button code:
viewof userparameter = Inputs.select(new Map([ ["Area (Sq. Km)", "areasqkm"],["Population", "population"], ["Airports", "airports"],["Gold in tonnes","Goldtonnes"],["GDP in dollar","gdpdollar"] ]), { value: "Area (Sq. Km)", label: "User Parameter to show:" })
The zoom properties:
viewof userparameter = Inputs.select(new Map([ ["Area (Sq. Km)", "areasqkm"],["Population", "population"], ["Airports", "airports"],["Gold in tonnes","Goldtonnes"],["GDP in dollar","gdpdollar"] ]), { value: "Area (Sq. Km)", label: "User Parameter to show:" })
Related
Background:
I'm currently integrating HERE maps into our web-based application. I'm trying both - HERE provided Javascript API and Leaflet at the same time to find the best approach for our use-case.
While JavaScript API provided by HERE maps is OK, rendering wise Leaflet performs much better when using raster tiles.
Issue:
It would be fine by me to use raster tiles + leaflet, but our application also needs to display traffic incidents data.
Traffic incident data is provided by HERE in JSON and XML formats (Documentation link, Example JSON). They provide [Z]/[X]/[Y], quadkey, prox, bbox, or corridor filters which can be used to retrieve filtered data set.
I've tried using [Z]/[X]/[Y] addressing with custom L.TileLayer implementation which loads appropriate JSON, converts it to GeoJSON and displays GeoJSON on map. However that approach is very inefficient and significant performance drop is visible.
Question:
Maybe anyone has already solved this issue and could share any insights on how the HERE traffic incidents could be shown on Leaflet map without encountering performance issues?
I created the following script, which works without any performance issues:
var fg = L.featureGroup().addTo(map);
function loadTraffic(data) {
fg.clearLayers();
var d = data.TRAFFICITEMS.TRAFFICITEM.map((r) => {
var latlngs = [];
if (r.LOCATION.GEOLOC) {
if (r.LOCATION.GEOLOC.ORIGIN) {
latlngs.push(L.latLng(r.LOCATION.GEOLOC.ORIGIN.LATITUDE, r.LOCATION.GEOLOC.ORIGIN.LONGITUDE));
}
if (r.LOCATION.GEOLOC.TO) {
if (L.Util.isArray(r.LOCATION.GEOLOC.TO)) {
r.LOCATION.GEOLOC.TO.forEach((latlng) => {
latlngs.push(L.latLng(latlng.LATITUDE, latlng.LONGITUDE));
})
} else {
latlngs.push(L.latLng(r.LOCATION.GEOLOC.TO.LATITUDE, r.LOCATION.GEOLOC.TO.LONGITUDE));
}
}
}
var desc = r.TRAFFICITEMDESCRIPTION.find(x => x.TYPE === "short_desc").content;
return {
latlngs,
desc
}
})
console.log(d);
d.forEach((road)=>{
L.polyline(road.latlngs,{color: 'red'}).addTo(fg).bindPopup(road.desc);
});
map.fitBounds(fg.getBounds())
}
If this script is not working for you, please share your json file.
Ok, so I've found a solution for this task. Apparently I was on a good path, I only needed to optimize my implementation.
What I had to do to achieve appropriate performance is:
Create custom CircleMarker extension which would draw custom icon on canvas
Create JS worker which would fetch the data from a given URL, transform it to GeoJSON and return GeoJSON to it's listener
Create custom GridLayer implementation, which, in fetchTile function, creates worker instance, passes it a link with appropriate [Z]/[X]/[Y] coordinates already set, adds listener, which listens for worker's done event and returns empty tile
On worker's done event, custom GridLayer implementation creates GeoJSON layer, adds it to the dictionary with coordinates as a key and, if zoom level is still the same - adds that layer to the map
Add zoomend observer on a map, which removes any layers that does not match current zoom level from the map
Now the map is definitely usable and works way faster than original HERE JS API.
P.S. Sorry, but I can't share the implementation itself due to our company policies.
There are many beautiful charts on Geographical Maps that can be drawn with AmCharts JS library as in https://www.amcharts.com/demos/#javascript-maps
However, I was wondering if it is possible to create a Custom Map. For example, I want to create a Global map with all Color-coded countries, but want to show US and Canada as one individual Country without any intermediate boundary between them. All other Countries should remain same.
Really appreciate for any pointer on above direction.
Thanks,
With amCharts v4, we have switched to using GeoJSON for our maps.
While I'm sure there's some kind of way to merge geographical polygons using mapshaper, I haven't tried it out myself yet and have been getting comfortable using the free software, QGIS.
There's already a tutorial out there on merging polygons if you're interested. I'll give a quick, specific rundown here anyway.
Download & install QGIS, then go ahead and grab the latest version of amCharts v4 worldLow.json (or worldHigh.json if you want more detail), in this case I've used worldLow.json:
https://github.com/amcharts/amcharts4-geodata/blob/master/dist/script/json/worldLow.json
Raw file:
https://raw.githubusercontent.com/amcharts/amcharts4-geodata/master/dist/script/json/worldLow.json
Make a copy of the .json file or otherwise rename it, it will be overwritten in the steps below.
Open QGIS and start a new project
Menu: Layer -> Add Vector Layer -> Source(s) -> Vector Dataset(s): Choose above file -> Add
Enable editing on the layer by either: Clicking pencil icon, Menu: Layer -> Toggle Editing, or in the bottom left Layers panel right click the layer and select Toggle Editing in the context menu
Menu: Edit -> Select -> Select feature(s)
Select the US and Canada polygons.
Menu: Edit -> Merge selected features (near the bottom)
Save your project, save layer edits, now your json file will be updated.
It should look something like this:
https://gist.github.com/notacouch/485b8525a360c15690f1ab23cbf04940
Now to refer to this in your map, you can do so via:
// Create map polygon series
var polygonSeries = chart.series.push(new am4maps.MapPolygonSeries());
polygonSeries.geodataSource.url = "https://gist.githubusercontent.com/notacouch/485b8525a360c15690f1ab23cbf04940/raw/e6742c279571ae02166027817351a4250b1bea69/worldLow--canadia.json";
In addition, you can generate a new color per country by using the default ColorSet, e.g.
// Have amCharts generate a new color for each mapPolygon once they're available
//
// Learn more about Event Listeners:
// {#link https://www.amcharts.com/docs/v4/concepts/event-listeners/}
polygonSeries.events.on("datavalidated", function() {
polygonSeries.mapPolygons.each(function(polygon, index) {
// Learn more about ColorSets:
// {#link https://www.amcharts.com/docs/v4/concepts/colors/#Color_sets}
polygon.fill = chart.colors.getIndex(index);
});
});
// Create hover state and set alternative fill color
//
// Learn more about States:
// {#link https://www.amcharts.com/docs/v4/concepts/states/}
var hs = polygonTemplate.states.create("hover");
// Darken the polygon's current color
//
// Learn more about Adapters:
// {#link https://www.amcharts.com/docs/v4/concepts/adapters/}
hs.adapter.add("fill", function(fill) {
return fill.brighten(-0.2);
});
Here's a demo with all that thrown together:
https://codepen.io/team/amcharts/pen/abd95bebcfb65a74d9472043d63351fc
That's probably undesirable, you can also color via data binding, or via a heat map.
For customizing maps in v4 in general, I recommend checking out our guide on Creating custom maps. Between mapshaper, QGIS, and all the map-related files out there, there's bound to be a way to get the customization you want and possibly in short order, too.
Hope this helps.
my coding knowledge is next to nothing, but I am having heaps of fun trying to learn.
I have been amending the code from Leaflet TimeDimension example 9: http://apps.socib.es/Leaflet.TimeDimension/examples/example9.html to try and show animal movement through time on a map created in Leaflet.
I have a self written GPX file and KML track containing the individual animal's lats and longs through time. I have also added the time dimension so that I can play the time slider and my custom icons move across the map as I would like. However, I have now hit a wall.
I would like to be able to click on a marker (at any stage during the time sliders duration [marker position moves]) and bring up information about that marker in a popup. This would be easy if there were actual markers (marker.bindPopup()) but the markers are created from a track in a GPX file:
example of an animals track in the GPX file:
...
<trk><name>THE DESCRIPTION I WANT TO DISPLAY IN A POPUP</name><number>2</number><trkseg>
<trkpt lat="-40" lon="120"><ele>2</ele><time>2018-01-06T10:09:57Z</time></trkpt>
<trkpt lat="-41" lon="122"><ele>2</ele><time>2018-01-21T16:45:57Z</time></trkpt>
</trkseg></trk>
...
and I do not know how to call the name or the description of one of these tracks and bind the information in a popup (there are multiple sets of tracks [each with a unique name and description] in the single GPX file).
There are two main solutions I have been looking into (that I think might be possible) but can't quite get either to work.
use get_name() from the GPX.js script: https://github.com/mpetazzoni/leaflet-gpx
It says:
If you want to display additional information about the GPX track, you can do so in the 'loaded' event handler, calling one of the following methods on the GPX object e.target:
get_name(): returns the name of the GPX track
Here is the demo code: https://git.nxfifteen.rocks/rocks/core/raw/056ddab410a40496a917cb150be8d92f4cc205cf/map.php but this code is well beyond my understanding and I haven't been able to figure out how to make it work
I have tried:
function onClick(e) {
alert(this.get_name());
}
but it doesn't work (not sure if its heading in the right direction or not).
Somehow in the customLayer make the bindPopup link to the name of the GPX track
Here is the section of code in my .js file that calls and loads the GPX and KML layer:
var customLayer = L.geoJson(null, {
pointToLayer: function (feature, latLng) {
if (feature.properties.hasOwnProperty('last')) {
return new L.Marker(latLng, {
icon: icon
}).bindPopup ("yo");
}
return L.circleMarker(latLng).bindPopup ("hey");
}
});
var gpxLayer = omnivore.gpx('data/data.gpx', null, customLayer).on('ready', function(e) {
map.fitBounds(gpxLayer.getBounds(), {
paddingTopLeft: [160, 160],
paddingBottomRight: [100, 100]
});
});
var gpxTimeLayer = L.timeDimension.layer.geoJson(gpxLayer, {
updateTimeDimension: true,
addlastPoint: true,
waitForReady: true
});
var kmlLayer = omnivore.kml('data/data.kml');
var kmlTimeLayer = L.timeDimension.layer.geoJson(kmlLayer, {
updateTimeDimension: true,
addlastPoint: true,
waitForReady: true
});
gpxTimeLayer.addTo(map);
In the variable: customLayer that is linked to the GPX track using omnivore, I can add a clickable popup that says "yo" to every animals marker. I can pause the time slider at any point and when I click a marker "yo" popus up (almost what I need).
I was wondering if there is some way to, instead of it saying "yo", tell it to return the name of the track that has been clicked, so that each marker will display its respective name (the name will contain the description I want it to show).
Sorry this is such a long winded question, and thank you in advance for any solutions, tip or pointers on whether my attempts just need a bit of tweaking, or if there is a different better solution out there.
I am doing a choropleth map using leaflet and JSON data. I got a clean tutorial in leaflet site. In that tutorial the used us-states.js JSON file where the choropleth was based on the feature.properties.density feature of the JSON if I am not wrong. If we see the JS file we can found there is a field called "ID" which is state id. I have a database where I have 5 different categories of data based on state ID. Categories are population, density, male_population, female_population, literacy_rate. I am calling those data through ajax and get a GeoJSON data state-wise as follows (Its a dummy data).
[{"state_id":"01","population":"123456","density":"1234","male_pop":"65432","female_pop":"57421","literacy_rate":"98"}]
...
[{"state_id":"50","population":"123456","density":"1234","male_pop":"65432","female_pop":"57421","literacy_rate":"98"}]
I want to integrate this data as choropleth value. when I call on literacy function the variation of color will be based on literacy_rate data. I can make the changes in getcolor function category wise.
Thanks
I would recommend using Leaflet Data Visualization Framework (DVF) plugin, located here. This plugin contains a set of Layers, (such as a Choropleth layer) and a set of functions to help color-code your data.
The example located here will demonstrate the DVF Choropleth Data layer.
Using DVF, you can create a color function to create your scaling color values. For example, this function will generate a function from yellow to red. The point values contain the range of values, and the HSL Hue.
var yellowToRed = new L.HSLHueFunction(new L.Point(50, 60), new L.Point(100, 0));
DVF contains a L.ChoroplethDataLayer layer that will accept your GeoJSON, and your color function, and will generate the layer for you.
var layer = new L.ChoroplethDataLayer(geoJSON, {
// For the full options, see the documentation
displayOptions: {
// The display will be colored by your 'density' property in your GeoJSON. This accesses the feature object directory, so the 'properties' prefix is required if you're going to access a GeoJSON property on your data.
'properties.density': {
// A legend will automatically be generated for you. You can add this as a control. This displayName property will be the title for this layer's legend.
displayName: 'Density',
color: yellowToRed
}
};
});
This is just a simple example. For your data, it might look slightly different. Go through the DVF documentation and samples and you'll find what's right for you.
Setup
I have a polygon map of all the Provinces in South Africa.
This is then pulled into a v3 Google Map as a FusionTablesLayer as follows:
// The Google Map object
map = new google.maps.Map( mapCanvas, mapOptions );
// The FusionTables layer
layer['provinces'] = new google.maps.FusionTablesLayer({
query: {
select: '*',
from: '16L-UK_1OZxGw6DlKR8V8yP4XZrtmNdOMugRRNrQ'
},
clickable: true,
suppressInfoWindows: true // Hide the default FusionTables InfoWindow
});
Right after that, I attach a click event listener to the FusionTablesLayer, and build a custom InfoBox object as follows:
google.maps.event.addListener(layer['provinces'], 'click', function(e){
/*
Here I build the infoBox HTML using e.row[]
eg: html = '<div>' + e.row['Province'].value + '</div>';
*/
// Attach the infoBox to the click
infoBox.setContent(html);
infoBox.setPosition(e.latLng);
infoBox.open(map);
});
After that, I render the layer on the map:
// Render the layer on the map
layer['provinces'].setMap(map);
All of this works. No problem.
Problem
The click event returns all the columns in the respective row of the FusionTable, and attaches it to the variable e above.
Now, each row in the FusionTable has a very long KML string - from 114kb to 2.5MB - and this is returned in e.infoWindowHtml as well as e.row['Polygon'].value.
e: (Object)
infoWindowHtml: "_Very_ long string in here."
latLng: Q
pixelOffset: T
row: (Object)
Number: (Object)
Polygon: (Object)
Province: (Object)
The request doesn't take very long due to heavy caching on Google's side, but after clicking on a Province, it takes almost 5 seconds for the infoBox to pop up.
tl;dr
The infoBox.open(map) method, after clicking on a FusionTables polygon, is very slow. How do I speed it up?
Update
The data is cached after the first click. Is there a way to cache the data before the first click?
Alternatively, is there a way to limit the returned variables attached to e, i.e.: remove the 'Polygon' data from the click request?
I found the answer by chance.
You can customise what gets returned by selecting the proper columns in the Change info window layout... window.
Map tab Tools > Change info window layout...
I deselected 'Polygon', and left 'Number' and 'Province' selected.
The columns you select are then attached to e in the click eventListener:
e: (Object)
infoWindowHtml: "Better string"
latLng: Q
pixelOffset: T
row: (Object)
Number: (Object)
Province: (Object)
Important Note (here's the luck)
After selecting the proper columns, you need to change the FusionTable data in some meaningful way to clear the strong caching on Google's side.
The columns selected in the Automatic tab are returned.
It seems that the Custom tab gets ignored.
Its speedy the second time to open the popup, so I presume it gets cached. The KML's are rather large tho, thats your bottleneck - can't you simplify them? For a decent overview you don't need that much detail. That should speed it up. (try replacing one province with just a rectangle and see if that helps)