Commit bb097888 authored by Tejesh's avatar Tejesh 🖖
Browse files

mapviewer drilldown merge

parent 9b0c2faa
Pipeline #49989 passed with stage
in 3 minutes and 6 seconds
......@@ -1114,7 +1114,7 @@ attribute.
},
attrs: {
fillColor: { // Fill the regions
metric: 'score', // with the "score" column from state_score.json
metric: 'score', // with the "score" column state_score.json
range: 'RdYlGn' // using a RdYlGn gradient
},
tooltip: function(prop) { // On hover, show this HTML tooltip
......@@ -1127,6 +1127,59 @@ attribute.
</script>
```
Drilldown feature example:
```html
<div id="geojson-map" style="height:300px">
<script>
var map = g1.mapviewer({
id: 'geojson-map',
layers: {
worldMap: { type: 'tile', url: 'http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png' },
indiaGeojson: {
type: 'geojson',
url: 'india-states.geojson',
link: {
url: 'state_score.json', // Load data from this file
dataKey: 'name', // Join this column from the URL (data)
mapKey: 'ST_NM' // with this property in the GeoJSON
},
attrs: {
fillColor: { // Fill the regions
metric: 'score', // with the "score" column state_score.json
range: 'RdYlGn' // using a RdYlGn gradient
},
tooltip: function(prop) { // On hover, show this HTML tooltip
return prop.ST_NM + ': ' + prop.TOT_P
}
}
}
},
drilldown: {
rootLayer: 'indiaGeojson',
levels: [
{
layerName: function(properties) {return properties['STATE'] + '-layer'},
layerOptions: {
url: function(properties) {return properties['STATE'] + '-census.json'},
type: 'geojson',
attrs: {
fillColor: {
metric: 'DT_CEN_CD',
range: 'RdYlGn'
},
tooltip: function (properties) {
return 'DISTRICT: ' + properties['DISTRICT']
}
}
}
}
]
}
})
</script>
```
**Note**: You can use `type: 'topojson'` when loading TopoJSON maps.
### g1.mapviewer options
......@@ -1140,7 +1193,7 @@ attribute.
- tile
- geojson
- topojson
- marker
- marker (`link`: option is not yet supported )
- circleMarker
- `tile` layer MUST have a url: that has the URL template for the leaflet tile layer.
- `url`: A string of the form - `http://{s}.somedomain.com/blabla/{z}/{x}/{y}{r}.png`
......@@ -1179,6 +1232,11 @@ attribute.
- `longitude`: String (mandatory). Must be column name that contains longitude of marker
- `options`: supports same options as [circleMarker options](http://leafletjs.com/reference-1.3.0.html#circlemarker-radius)
- `attrs` same as `attrs` for `geojson` type layer
- `drilldown`:
- `rootLayer`: `geojson/topojson` layer that acts as root layer to drilldown further.
- `levels`: Array of objects that provides layer info
- `layerName`: Can be a string or function. Function takes argument as `properties` of parentLayer feature
- `layerOptions`: Same as layer options in `layers` option. If `url` is function, `url` takes argument as `properties` of parentLayer feature
- `zoomHandler`: <!-- TODO -->
- `zoomlevel`: must be a `geojson` layer name or function. If given a `geojson` layer name, shows this layer and hides all other `geojson` layers. Optimize this to load only layer that is visible in viewport.
......@@ -1188,9 +1246,9 @@ Zooms map view to fit the layer. Supports same options as [fitBounds options](ht
### g1.mapviewer events
- `mapload` is fired when all the map layers are loaded.
- `layersload` is fired when all layers are saved in mapviewer.gLayers
- tooltip is rendered on each layer only after layers are loaded.
- `layersloaded` is fired when all layers are saved in mapviewer.gLayers (used interally).
- tooltip is rendered on each layer only after `layersload` is fired
- `layerName + 'loaded'` is fired for each layer with name as `layerName`
## Contributing
......
......@@ -6,5 +6,4 @@ url:
handler: FormHandler
kwargs:
url: test/formhandler.csv
xsrf_cookies: false # TODO: enable this and test
id: ID
......@@ -10,7 +10,7 @@
"url": "git@code.gramener.com:s.anand/g1.git"
},
"scripts": {
"lint": "eslint index.js src",
"lint": "eslint index.js src && eclint check '**/*.html' '**/*.js' '**/*.css' '**/*.yaml' '**/*.md'",
"build": "rimraf dist && json2module package.json > src/package.js && rollup -c",
"dev": "rimraf dist && json2module package.json > src/package.js && rollup -c -w",
"pretest": "npm run build && browserify -s tape -r tape -o test/tape.js",
......
This diff is collapsed.
......@@ -57,7 +57,10 @@ export var MapViewer = class MapViewer {
for (let layerName in self.options.layers) {
self.buildLayer(layerName, self.options.layers[layerName])
}
self.renderTooltip()
self.setupTooltip()
self.drilldown()
self.current_level = 0
self.drilldown_stack = []
}
}
}
......@@ -105,44 +108,20 @@ MapViewer.prototype.cacheData = function (layerName, url) {
MapViewer.prototype._saveLayer = function (layerName, layer) {
var self = this, allLayersLoaded = true
self.gLayers[layerName] = layer
// sort order of layers to same order as the order given in config
self.gLayers[layerName].addTo(self.map)
if ('layers' in self.options) {
for (var key in self.gLayers) {
if (self.gLayers[key]) {
self.map.removeLayer(self.gLayers[key])
self.gLayers[key].addTo(self.map)
} else {
if (!self.gLayers[key]) {
allLayersLoaded = false
}
}
}
if (allLayersLoaded === true) {
self.mapDiv.dispatchEvent(new Event('layersload'))
self.fire('layersloaded')
}
}
MapViewer.prototype.renderTooltip = function () {
var self = this
self.on('layersload', function () {
for (let layerName in self.options.layers) {
if (self.options.layers[layerName].attrs && self.options.layers[layerName].attrs.tooltip) {
self.gLayers[layerName].eachLayer(function (sublayer) {
var tooltipContent = self.options.layers[layerName].attrs.tooltip
if (typeof (self.options.layers[layerName].attrs.tooltip) === 'function') {
tooltipContent = self.options.layers[layerName].attrs.tooltip(sublayer.feature.properties)
}
sublayer.bindTooltip(tooltipContent)
})
}
}
self.mapDiv.dispatchEvent(new Event('mapload'))
})
}
MapViewer.prototype.on = function (type, callback) {
this.mapDiv.addEventListener(type, callback)
}
MapViewer.prototype.mergeData = function (mapJSON, dataTable, mapKey, dataKey) {
var dataTableIndex = {}
dataTable.forEach(function (row) {
......@@ -158,6 +137,7 @@ MapViewer.prototype.mergeData = function (mapJSON, dataTable, mapKey, dataKey) {
}
})
return mapJSON
break
default:
mapJSON.map(function (json) {
var row = dataTableIndex[json[mapKey]]
......@@ -166,30 +146,45 @@ MapViewer.prototype.mergeData = function (mapJSON, dataTable, mapKey, dataKey) {
}
})
return mapJSON
break
}
}
MapViewer.prototype.fire = function (eventName) {
this.mapDiv.dispatchEvent(new Event(eventName))
}
MapViewer.prototype.on = function (type, callback, options) {
this.mapDiv.addEventListener(type, callback, options)
}
MapViewer.prototype.off = function (eventName, callback, options) {
this.mapDiv.removeEventListener(eventName, callback, options)
}
MapViewer.prototype.buildLayer = function (layerName, layerConfig) {
var self = this, gLayer
switch (layerConfig.type.toLowerCase()) {
case 'tile':
gLayer = L.tileLayer(layerConfig.url, layerConfig.options)
this._saveLayer(layerName, gLayer)
self.fire(layerName + 'loaded')
break
case 'geojson':
case 'topojson':
case 'geojson':
self.cacheData(layerName, layerConfig[dataOrURL(layerConfig)]).then(function (mapJSON) {
gLayer = new L.TopoJSON(mapJSON, layerConfig.options)
self.fitToLayer(gLayer)
self._saveLayer(layerName, gLayer)
if ('link' in layerConfig) {
self.cacheData(layerName, layerConfig.link[dataOrURL(layerConfig.link)]).then(function (tableData) {
self.mergeData(mapJSON, tableData, layerConfig.link.mapKey, layerConfig.link.dataKey)
self.fitToLayer(layerName)
if ('attrs' in layerConfig) self._choropleth(layerName)
if ('attrs' in layerConfig) self._choropleth(layerName, layerConfig)
self.fire(layerName+'loaded')
})
} else {
if ('attrs' in layerConfig) self._choropleth(layerName)
self.fitToLayer(layerName)
if ('attrs' in layerConfig) self._choropleth(layerName, layerConfig)
self.fire(layerName + 'loaded')
}
})
break
......@@ -203,8 +198,9 @@ MapViewer.prototype.buildLayer = function (layerName, layerConfig) {
mark.feature.properties = d
pointLayers.push(mark)
})
self.fitToLayer(L.featureGroup(pointLayers))
self._saveLayer(layerName, L.featureGroup(pointLayers))
self.fitToLayer(layerName)
self.fire(layerName + 'loaded')
})
break
case 'circle':
......@@ -218,16 +214,17 @@ MapViewer.prototype.buildLayer = function (layerName, layerConfig) {
mark.feature.properties = d
pointLayers.push(mark)
})
self.fitToLayer(L.featureGroup(pointLayers))
self._saveLayer(layerName, L.featureGroup(pointLayers))
if ('link' in layerConfig) {
self.cacheData(layerName, layerConfig.link[dataOrURL(layerConfig.link)]).then(function (tableData) {
self.mergeData(mapJSON, tableData, layerConfig.link.mapKey, layerConfig.link.dataKey)
if ('attrs' in layerConfig) self._choropleth(layerName)
self.fitToLayer(layerName)
self.mergeData(pointjson, tableData, layerConfig.link.mapKey, layerConfig.link.dataKey)
if ('attrs' in layerConfig) self._choropleth(layerName, layerConfig)
self.fire(layerName + 'loaded')
})
} else {
if ('attrs' in layerConfig) self._choropleth(layerName)
self.fitToLayer(layerName)
if ('attrs' in layerConfig) self._choropleth(layerName, layerConfig)
self.fire(layerName + 'loaded')
}
})
break
......@@ -236,21 +233,21 @@ MapViewer.prototype.buildLayer = function (layerName, layerConfig) {
}
}
MapViewer.prototype._choropleth = function (layerName) {
MapViewer.prototype._choropleth = function (layerName, layerConfig) {
var layer = this.gLayers[layerName], self = this
layer.eachLayer(function (sublayer) {
var style = {}, prop, metricFormula, metric, domain
for (prop in self.options.layers[layerName].attrs) {
if (prop.toLowerCase() == 'tooltip') continue
metric = self.options.layers[layerName].attrs[prop].metric
for (prop in layerConfig.attrs) {
if(prop.toLowerCase() == 'tooltip') continue
metric = layerConfig.attrs[prop].metric
if (typeof (metric) === 'string')
metricFormula = (row) => row[metric]
else
metricFormula = metric
if (self.options.layers[layerName].attrs[prop].domain)
domain = self.options.layers[layerName].attrs[prop].domain
if (layerConfig.attrs[prop].domain)
domain = layerConfig.attrs[prop].domain
else
domain = self._calculateMinMax(layer, metricFormula)
......@@ -258,16 +255,18 @@ MapViewer.prototype._choropleth = function (layerName) {
style[prop] = scale([], {
metric: metric,
domain: domain,
scheme: self.options.layers[layerName].attrs[prop].range
scheme: layerConfig.attrs[prop].range
})(sublayer.feature.properties)
}
}
sublayer.setStyle(style)
})
}
// * @method _calculateMinMax(layer, <function> metricFormula ): <Array>
// * Analogous to d3.extent but for feature.properties
// * Private/internal method
/*
* @method _calculateMinMax(layer, <function> metricFormula ): <Array>
* Analogous to d3.extent but for feature.properties
* Private/internal method
*/
MapViewer.prototype._calculateMinMax = function (layer, metricFormula) {
var minVal, maxVal
layer.eachLayer(function (sublayer) {
......@@ -283,8 +282,83 @@ MapViewer.prototype._calculateMinMax = function (layer, metricFormula) {
})
return [minVal, maxVal]
}
MapViewer.prototype.drilldown = function (drilldown) {
var self = this
self.on('layersloaded', function() {
if(self.options.drilldown) {
self.drilldown_recursive(self.options.drilldown.rootLayer)
}
}, {
once: true
})
}
MapViewer.prototype.drilldown_recursive = function (currentLayer) {
var self = this
const levels = this.options.drilldown.levels
self.gLayers[currentLayer].eachLayer(function (sublayer) {
// TODO: use .once instead of .off and .on
sublayer.off('click')
sublayer.
on('click', function () {
if (levels.length == self.current_level) {
self.fitToLayer(sublayer)
} else {
var nextLayer = levels[self.current_level]
if (typeof(nextLayer.layerName) == 'function') {
nextLayer.layerName = nextLayer.layerName(sublayer.feature.properties)
}
if (typeof (nextLayer.layerOptions.url) == 'function') {
nextLayer.layerOptions.url = nextLayer.layerOptions.url(sublayer.feature.properties)
}
self.options.layers[nextLayer.layerName] = nextLayer.layerOptions
self.buildLayer(nextLayer.layerName, nextLayer.layerOptions)
// remove this layer and store it in a stack
self.drilldown_stack.push(currentLayer)
self.drilldown_stack.push(nextLayer.layerName)
self.map.removeLayer(self.gLayers[currentLayer])
self.current_level += 1
self.on(nextLayer.layerName + 'loaded', function() {
if(nextLayer.layerOptions.attrs && nextLayer.layerOptions.attrs.tooltip)
self.renderTooltip(nextLayer.layerName, nextLayer.layerOptions)
// attach drilldown events for sublayers
self.drilldown_recursive(nextLayer.layerName)
}, {once: true})
}
})
})
}
MapViewer.prototype.drillup = function () {
var self = this
self.current_level -= 1
self.map.removeLayer(self.gLayers[self.drilldown_stack.pop()])
var current_level_layer = self.drilldown_stack.pop()
self.fitToLayer(self.gLayers[current_level_layer])
self.gLayers[current_level_layer].addTo(self.map)
}
MapViewer.prototype.setupTooltip = function () {
var self = this
self.on('layersloaded', function () {
for (let layerName in self.options.layers) {
if (self.options.layers[layerName].attrs && self.options.layers[layerName].attrs.tooltip) {
self.renderTooltip(layerName, self.options.layers[layerName])
}
}
self.fire('mapviewerloaded')
})
}
MapViewer.prototype.renderTooltip = function (layerName, layerConfig) {
this.gLayers[layerName].eachLayer(function (sublayer) {
var tooltipContent = layerConfig.attrs.tooltip
if (typeof (layerConfig.attrs.tooltip) === 'function') {
tooltipContent = layerConfig.attrs.tooltip(sublayer.feature.properties)
}
sublayer.bindTooltip(tooltipContent)
})
}
/*
* @method fitToLayer(<String> layerName ): this
* @method fitToLayer(<String> layerName, <Object> options ): this
* options are same options as fitBounds options
* Zooms map view to fit the layer
*
* @example
......@@ -293,8 +367,9 @@ MapViewer.prototype._calculateMinMax = function (layer, metricFormula) {
mapviewer.fitToLayer('indiaGeojson')
*```
*/
MapViewer.prototype.fitToLayer = function (layerName, options = {}) {
this.map.fitBounds(this.gLayers[layerName].getBounds(), options)
MapViewer.prototype.fitToLayer = function (layerName, options = this.options.fitbounds) {
var layer = typeof (layerName) == 'string' ? this.gLayers[layerName] : layerName
this.map.fitBounds(layer.getBounds(), options)
}
function dataOrURL(conf) {
......
This diff is collapsed.
[{"Continent":"Europe"},{"Continent":"Asia"},{"Continent":"Asia"},{"Continent":"North America"},{"Continent":"Europe"},{"Continent":"Asia"},{"Continent":"Africa"},{"Continent":"South America"},{"Continent":"Europe"},{"Continent":"Oceania"},{"Continent":"Asia"},{"Continent":"Europe"},{"Continent":"North America"},{"Continent":"Asia"},{"Continent":"Europe"},{"Continent":"Africa"},{"Continent":"Europe"},{"Continent":"Asia"},{"Continent":"Africa"},{"Continent":"Africa"},{"Continent":"Asia"},{"Continent":"South America"},{"Continent":"South America"},{"Continent":"North America"},{"Continent":"Asia"},{"Continent":"Africa"},{"Continent":"Europe"},{"Continent":"North America"},{"Continent":"North America"},{"Continent":"Africa"},{"Continent":"Africa"},{"Continent":"Africa"},{"Continent":"Europe"},{"Continent":"Africa"},{"Continent":"South America"},{"Continent":"Africa"},{"Continent":"Asia"},{"Continent":"South America"},{"Continent":"North America"},{"Continent":"North America"},{"Continent":"Africa"},{"Continent":"Asia"},{"Continent":"Europe"},{"Continent":"Europe"},{"Continent":"Africa"},{"Continent":"Europe"},{"Continent":"North America"},{"Continent":"North America"},{"Continent":"Africa"},{"Continent":"South America"},{"Continent":"Europe"},{"Continent":"Africa"},{"Continent":"Africa"},{"Continent":"Africa"},{"Continent":"Europe"},{"Continent":"Africa"},{"Continent":"Europe"},{"Continent":"Oceania"},{"Continent":"Oceania"},{"Continent":"Europe"},{"Continent":"Africa"},{"Continent":"Europe"},{"Continent":"North America"},{"Continent":"Asia"},{"Continent":"Africa"},{"Continent":"Africa"},{"Continent":"Africa"},{"Continent":"Africa"},{"Continent":"Europe"},{"Continent":"North America"},{"Continent":"Africa"},{"Continent":"South America"},{"Continent":"North America"},{"Continent":"Europe"},{"Continent":"North America"},{"Continent":"Europe"},{"Continent":"Asia"},{"Continent":"Europe"},{"Continent":"Asia"},{"Continent":"Asia"},{"Continent":"Asia"},{"Continent":"Asia"},{"Continent":"Europe"},{"Continent":"Europe"},{"Continent":"North America"},{"Continent":"Asia"},{"Continent":"Asia"},{"Continent":"Africa"},{"Continent":"Asia"},{"Continent":"Asia"},{"Continent":"Oceania"},{"Continent":"Africa"},{"Continent":"North America"},{"Continent":"Asia"},{"Continent":"Asia"},{"Continent":"Europe"},{"Continent":"Asia"},{"Continent":"Asia"},{"Continent":"Asia"},{"Continent":"Asia"},{"Continent":"North America"},{"Continent":"Europe"},{"Continent":"Asia"},{"Continent":"Africa"},{"Continent":"Africa"},{"Continent":"Europe"},{"Continent":"Europe"},{"Continent":"Europe"},{"Continent":"Africa"},{"Continent":"Africa"},{"Continent":"Europe"},{"Continent":"Europe"},{"Continent":"Europe"},{"Continent":"Africa"},{"Continent":"Oceania"},{"Continent":"Europe"},{"Continent":"Africa"},{"Continent":"Asia"},{"Continent":"Asia"},{"Continent":"Africa"},{"Continent":"Europe"},{"Continent":"Africa"},{"Continent":"Asia"},{"Continent":"Africa"},{"Continent":"North America"},{"Continent":"Asia"},{"Continent":"Africa"},{"Continent":"Africa"},{"Continent":"Africa"},{"Continent":"Africa"},{"Continent":"North America"},{"Continent":"Europe"},{"Continent":"Europe"},{"Continent":"Asia"},{"Continent":"Oceania"},{"Continent":"Oceania"},{"Continent":"Asia"},{"Continent":"North America"},{"Continent":"South America"},{"Continent":"Oceania"},{"Continent":"Asia"},{"Continent":"Asia"},{"Continent":"Europe"},{"Continent":"Europe"},{"Continent":"Oceania"},{"Continent":"South America"},{"Continent":"Asia"},{"Continent":"Europe"},{"Continent":"Europe"},{"Continent":"Europe"},{"Continent":"Africa"},{"Continent":"Asia"},{"Continent":"Oceania"},{"Continent":"Africa"},{"Continent":"Africa"},{"Continent":"Europe"},{"Continent":"Asia"},{"Continent":"Europe"},{"Continent":"Europe"},{"Continent":"Africa"},{"Continent":"Europe"},{"Continent":"Africa"},{"Continent":"Africa"},{"Continent":"South America"},{"Continent":"Africa"},{"Continent":"North America"},{"Continent":"Asia"},{"Continent":"Africa"},{"Continent":"Africa"},{"Continent":"Africa"},{"Continent":"Asia"},{"Continent":"Asia"},{"Continent":"Asia"},{"Continent":"Asia"},{"Continent":"Africa"},{"Continent":"Oceania"},{"Continent":"Asia"},{"Continent":"North America"},{"Continent":"Oceania"},{"Continent":"Asia"},{"Continent":"Africa"},{"Continent":"Europe"},{"Continent":"Africa"},{"Continent":"North America"},{"Continent":"South America"},{"Continent":"Asia"},{"Continent":"Europe"},{"Continent":"North America"},{"Continent":"South America"},{"Continent":"Asia"},{"Continent":"Oceania"},{"Continent":"Oceania"},{"Continent":"Asia"},{"Continent":"Africa"},{"Continent":"Africa"},{"Continent":"Africa"}]
[
{
"Continent": "Europe"
},
{
"Continent": "Asia"
},
{
"Continent": "Asia"
},
{
"Continent": "North America"
},
{
"Continent": "Europe"
},
{
"Continent": "Asia"
},
{
"Continent": "Africa"
},
{
"Continent": "South America"
},
{
"Continent": "Europe"
},
{
"Continent": "Oceania"
},
{
"Continent": "Asia"
},
{
"Continent": "Europe"
},
{
"Continent": "North America"
},
{
"Continent": "Asia"
},
{
"Continent": "Europe"
},
{
"Continent": "Africa"
},
{
"Continent": "Europe"
},
{
"Continent": "Asia"
},
{
"Continent": "Africa"
},
{
"Continent": "Africa"
},
{
"Continent": "Asia"
},
{
"Continent": "South America"
},
{
"Continent": "South America"
},
{
"Continent": "North America"
},
{
"Continent": "Asia"
},
{
"Continent": "Africa"
},
{
"Continent": "Europe"
},
{
"Continent": "North America"
},
{
"Continent": "North America"
},
{
"Continent": "Africa"
},
{
"Continent": "Africa"
},
{
"Continent": "Africa"
},
{
"Continent": "Europe"
},
{
"Continent": "Africa"
},
{
"Continent": "South America"
},
{
"Continent": "Africa"
},
{
"Continent": "Asia"
},
{
"Continent": "South America"
},
{
"Continent": "North America"
},
{
"Continent": "North America"
},
{
"Continent": "Africa"
},
{
"Continent": "Asia"
},
{
"Continent": "Europe"
},
{
"Continent": "Europe"
},
{
"Continent": "Africa"
},
{
"Continent": "Europe"
},
{
"Continent": "North America"
},
{
"Continent": "North America"
},
{
"Continent": "Africa"
},
{
"Continent": "South America"
},
{
"Continent": "Europe"
},
{
"Continent": "Africa"
},
{
"Continent": "Africa"
},
{
"Continent": "Africa"
},
{
"Continent": "Europe"
},
{
"Continent": "Africa"
},
{
"Continent": "Europe"
},
{