Commit 36adaa41 authored by S Anand's avatar S Anand
Browse files

ENH: mapviewer drilldown. Fixes #48 @tejesh.p

parent 9b0c2faa
Pipeline #50162 passed with stage
in 3 minutes and 8 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",
......
......@@ -12,45 +12,49 @@ Each template receives these variables:
<!-- This is the root template that renders all other components on this page -->
<!-- var template_ -->
<div class="position-relative">
<div class="formhandler">
<div class="formhandler-table-header d-flex justify-content-between mb-2">
<div class="d-flex flex-wrap">
<div class="edit mr-2"></div>
<div class="add mr-2"></div>
<div class="count mr-2"></div>
<div class="page mr-2"></div>
<div class="size mr-2"></div>
</div>
<div class="d-flex">
<div class="filters"></div>
<div class="export"></div>
</div>
<div class="position-relative">
<div class="formhandler">
<div class="formhandler-table-header d-flex justify-content-between mb-2">
<div class="d-flex flex-wrap">
<div class="edit mr-2"></div>
<div class="add mr-2"></div>
<div class="count mr-2"></div>
<div class="page mr-2"></div>
<div class="size mr-2"></div>
</div>
<div class="d-flex">
<div class="filters"></div>
<div class="export"></div>
</div>
<div class="<%- (options.table == 'grid') ? 'table_grid' : 'table table-responsive' %>"></div>
</div>
<div class="loader pos-cc d-none">
<div class="fa fa-spinner fa-spin fa-3x fa-fw"></div>
<span class="sr-only">Loading...</span>
<div class="<%- (options.table == 'grid') ? 'table_grid' : 'table table-responsive' %>"></div>
</div>
<div class="loader pos-cc d-none">
<div class="fa fa-spinner fa-spin fa-3x fa-fw"></div>
<span class="sr-only">Loading...</span>
</div>
</div>
<div class="modal formhandler-table-modal" id="fh-modal-<%- idcount %>" tabindex="-1" role="dialog" aria-labelledby="fh-label-<%- idcount %>"
aria-hidden="true">
<div class="modal-dialog modal-sm" role="document">
<div class="modal-content">
<form class="formhandler-table-modal-form modal-body">
<label id="fh-label-<%- idcount %>" for="formhandler-table-modal-value">Value</label>
<p>
<input class="form-control" name="filter_input">
</p>
<div>
<button type="button" class="btn btn-sm btn-secondary mr-1" data-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-sm btn-primary mr-1">Apply filter</button>
<a class="btn btn-sm btn-danger remove-action urlfilter" data-dismiss="modal" data-target="#" href="#">Remove filter</button>
</div>
</form>
</div>
<!-- .modal-content -->
</div>
<div class="modal formhandler-table-modal" id="fh-modal-<%- idcount %>" tabindex="-1" role="dialog" aria-labelledby="fh-label-<%- idcount %>" aria-hidden="true">
<div class="modal-dialog modal-sm" role="document">
<div class="modal-content">
<form class="formhandler-table-modal-form modal-body">
<label id="fh-label-<%- idcount %>" for="formhandler-table-modal-value">Value</label>
<p>
<input class="form-control" name="filter_input">
</p>
<div>
<button type="button" class="btn btn-sm btn-secondary mr-1" data-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-sm btn-primary mr-1">Apply filter</button>
<a class="btn btn-sm btn-danger remove-action urlfilter" data-dismiss="modal" data-target="#" href="#">Remove filter</button>
</div>
</form>
</div><!-- .modal-content -->
</div><!-- .modal-dialog -->
</div><!-- .modal -->
<!-- .modal-dialog -->
</div>
<!-- .modal -->
<!-- end -->
<!-- var template_table -->
......@@ -67,86 +71,103 @@ Each template receives these variables:
<table class="table table-sm table-striped">
<thead>
<% _.each(cols, function(colinfo) {
col_defaults(colinfo, data)
var menu_item = false
var col_id = idcount++
%>
<th class="<%- args['_sort'] == colinfo.name ? 'table-primary' : args['_sort'] == '-' + colinfo.name ? 'table-danger' : '' %>" data-col="<%- colinfo.name %>">
col_defaults(colinfo, data)
var menu_item = false
var col_id = idcount++
%>
<th class="<%- args['_sort'] == colinfo.name ? 'table-primary' : args['_sort'] == '-' + colinfo.name ? 'table-danger' : '' %>"
data-col="<%- colinfo.name %>">
<div class="dropdown">
<a href="#" class="dropdown-toggle text-nowrap" id="fh-dd-<%- col_id %>" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<%- colinfo.title || colinfo.name %>
</a>
<div class="dropdown-menu" aria-labelledby="fh-dd-<%- col_id %>">
<% _.each(colinfo.sort, function(title, op) { menu_item = true
var active = args['_sort'] == op + colinfo.name %>
var active = args['_sort'] == op + colinfo.name %>
<a class="dropdown-item urlfilter <%- active ? 'active': '' %>" href="?_sort=<%- active ? '' : op + colinfo.name %>">
<%- title %>
</a>
<% }) %>
<% if (menu_item) { %><div class="dropdown-divider"></div><% menu_item = false } %>
<% _.each(colinfo.filters, function(title, op) { menu_item = true %>
<a class="dropdown-item <%= colinfo.name + op in args ? 'active' : '' %>" href="#" data-op="<%- op %>" data-toggle="modal" data-target="#fh-modal-<%- form_id %>">
<%- title %>
</a>
<% }) %>
<% if (menu_item) { %><div class="dropdown-divider"></div><% menu_item = false } %>
<% if (colinfo.hideable) { %>
<a class="dropdown-item urlfilter" href="?_c=-<%- colinfo.name %>" data-mode="add">Hide</a>
<% } %>
<% }) %>
<% if (menu_item) { %>
<div class="dropdown-divider"></div>
<% menu_item = false } %>
<% _.each(colinfo.filters, function(title, op) { menu_item = true %>
<a class="dropdown-item <%= colinfo.name + op in args ? 'active' : '' %>" href="#" data-op="<%- op %>" data-toggle="modal"
data-target="#fh-modal-<%- form_id %>">
<%- title %>
</a>
<% }) %>
<% if (menu_item) { %>
<div class="dropdown-divider"></div>
<% menu_item = false } %>
<% if (colinfo.hideable) { %>
<a class="dropdown-item urlfilter" href="?_c=-<%- colinfo.name %>" data-mode="add">Hide</a>
<% } %>
</div>
</div>
</th>
<% }) %>
<% }) %>
</thead>
<tbody>
<% if (isAdd) { %>
<tr class="new-row">
<% _.each(cols, function(colinfo) { %>
<% if (!colinfo.template) { %>
<td><input class="form-control form-control-sm" data-key="<%- colinfo.name %>" value=""/></td>
<% } else { %>
<td></td>
<% } %>
<% }) %>
</tr>
<% } %>
<% if (options.rowTemplate) { %>
<% _.each(data, function(row) { %>
<%= typeof options.rowTemplate == 'function' ? options.rowTemplate({row: row, data: data, col: cols}) : _.template(options.rowTemplate)({row: row, data: data, cols: cols}) %>
<% }) %>
<% } else {%>
<% _.each(data, function(row, rowIndex) { %>
<tr data-val="<%- JSON.stringify(row) %>" data-row="<%- rowIndex %>">
<% _.each(cols, function(colinfo) { %>
<% var fmt = typeof(colinfo.format),
val = row[colinfo.name],
isEditable = colinfo.editable === undefined ? true : colinfo.editable,
disp = fmt == "function" ?
colinfo.format({name: colinfo.name, value: val, row: row, data:data }) :
fmt === "string" && colinfo.type === "number" ?
numeral(val).format(colinfo.format) :
fmt === "string" && colinfo.type === "date" ?
moment(val).format(colinfo.format):
val,
col_link %>
<% if (!isEdit && 'link' in colinfo) var col_link = typeof colinfo.link == 'function' ? colinfo.link({row: row}) : _.template(colinfo.link)({row: row}) %>
<% if (colinfo.template) { %>
<%= typeof colinfo.template == 'function' ? colinfo.template({value: val, format: disp, link: col_link, data: data}) : _.template(colinfo.template)(({value: val, format: disp, link: col_link, data: data})) %>
<% } else if (col_link) { %>
<td><a href="<%- col_link %>" target="_blank"><%= disp %></a></td>
<% } else { %>
<% if (isEdit && isEditable) { %>
<td><input class="form-control form-control-sm" data-key="<%- colinfo.name %>" value="<%- val %>"/></td>
<% } else { %>
<td><a class="urlfilter" href="?<%- colinfo.name %>=<%- val %>&amp;_offset=">
<%= disp %>
</a></td>
<% } %>
<td>
<input class="form-control form-control-sm" data-key="<%- colinfo.name %>" value="" />
</td>
<% } else { %>
<td></td>
<% } %>
<% }) %>
</tr>
<% }) %>
<% } %>
<% }) %>
</tr>
<% } %>
<% if (options.rowTemplate) { %>
<% _.each(data, function(row) { %>
<%= typeof options.rowTemplate == 'function' ? options.rowTemplate({row: row, data: data, col: cols}) : _.template(options.rowTemplate)({row: row, data: data, cols: cols}) %>
<% }) %>
<% } else {%>
<% _.each(data, function(row, rowIndex) { %>
<tr data-val="<%- JSON.stringify(row) %>" data-row="<%- rowIndex %>">
<% _.each(cols, function(colinfo) { %>
<% var fmt = typeof(colinfo.format),
val = row[colinfo.name],
isEditable = colinfo.editable === undefined ? true : colinfo.editable,
disp = fmt == "function" ?
colinfo.format({name: colinfo.name, value: val, row: row, data:data }) :
fmt === "string" && colinfo.type === "number" ?
numeral(val).format(colinfo.format) :
fmt === "string" && colinfo.type === "date" ?
moment(val).format(colinfo.format):
val,
col_link %>
<% if (!isEdit && 'link' in colinfo) var col_link = typeof colinfo.link == 'function' ? colinfo.link({row: row}) : _.template(colinfo.link)({row: row}) %>
<% if (colinfo.template) { %>
<%= typeof colinfo.template == 'function' ? colinfo.template({value: val, format: disp, link: col_link, data: data}) : _.template(colinfo.template)(({value: val, format: disp, link: col_link, data: data})) %>
<% } else if (col_link) { %>
<td>
<a href="<%- col_link %>" target="_blank">
<%= disp %>
</a>
</td>
<% } else { %>
<% if (isEdit && isEditable) { %>
<td>
<input class="form-control form-control-sm" data-key="<%- colinfo.name %>" value="<%- val %>"
/>
</td>
<% } else { %>
<td>
<a class="urlfilter" href="?<%- colinfo.name %>=<%- val %>&amp;_offset=">
<%= disp %>
</a>
</td>
<% } %>
<% } %>
<% }) %>
</tr>
<% }) %>
<% } %>
</tbody>
</table>
<!-- end -->
......@@ -157,190 +178,196 @@ Each template receives these variables:
lo = Math.max(page - 2, 1),
hi = last_page !== null ? Math.min(last_page, page + 2) : page + 2
%>
<ul class="pagination pagination-sm">
<li class="page-item <%- page <= 1 ? 'disabled' : '' %>">
<a class="page-link" href="?_offset=<%- meta.offset - meta.limit %>">Previous</a>
</li>
<% if (lo > 1) { %>
<li class="page-item">
<!-- TODO: Review Shouldn't this be _offset=0 ? -->
<a class="page-link" href="?_offset=">1</a>
</li>
<% if (lo > 2) { %>
<li class="page-item disabled">
<a class="page-link" href="#">...</a>
</li>
<% } %>
<% } %>
<% _.each(_.range(lo, hi + 1), function(pg) { %>
<li class="page-item <%- pg == page ? 'active' : '' %>">
<a class="page-link" href="?_offset=<%- meta.limit * (pg - 1) || '' %>"><%- pg %></a>
<ul class="pagination pagination-sm">
<li class="page-item <%- page <= 1 ? 'disabled' : '' %>">
<a class="page-link" href="?_offset=<%- meta.offset - meta.limit %>">Previous</a>
</li>
<% }) %>
<% if ('count' in meta) { %>
<% if (hi + 1 < last_page) { %>
<li class="page-item disabled">
<a class="page-link" href="#">...</a>
</li>
<% } %>
<% if (hi < last_page || lo > hi) { %>
<% if (lo > 1) { %>
<li class="page-item">
<a class="page-link" href="?_offset=<%- meta.limit * (last_page - 1) %>"><%- last_page %></a>
<!-- TODO: Review Shouldn't this be _offset=0 ? -->
<a class="page-link" href="?_offset=">1</a>
</li>
<% } %>
<% } %>
<% if (lo > 2) { %>
<li class="page-item disabled">
<a class="page-link" href="#">...</a>
</li>
<% } %>
<% } %>
<% _.each(_.range(lo, hi + 1), function(pg) { %>
<li class="page-item <%- pg == page ? 'active' : '' %>">
<a class="page-link" href="?_offset=<%- meta.limit * (pg - 1) || '' %>">
<%- pg %>
</a>
</li>
<% }) %>
<% if ('count' in meta) { %>
<% if (hi + 1 < last_page) { %>
<li class="page-item disabled">
<a class="page-link" href="#">...</a>
</li>
<% } %>
<% if (hi < last_page || lo > hi) { %>
<li class="page-item">
<a class="page-link" href="?_offset=<%- meta.limit * (last_page - 1) %>">
<%- last_page %>
</a>
</li>
<% } %>
<% } %>
<li class="page-item <%- (last_page === null) || (page < last_page) ? '' : 'disabled' %>">
<a class="page-link" href="?_offset=<%- meta.offset + meta.limit %>">Next</a>
</li>
</ul>
<!-- end -->
<li class="page-item <%- (last_page === null) || (page < last_page) ? '' : 'disabled' %>">
<a class="page-link" href="?_offset=<%- meta.offset + meta.limit %>">Next</a>
</li>
</ul>
<!-- end -->
<!-- var template_size -->
<% if (meta.limit) { %>
<div class="btn-group btn-group-sm" role="group">
<button id="formhandler-size-<%- idcount++ %>" type="button" class="btn btn-light btn-sm dropdown-toggle" data-toggle="dropdown"
aria-haspopup="true" aria-expanded="false">
<%- meta.limit %> rows
</button>
<div class="dropdown-menu" aria-labelledby="formhandler-size-<%- idcount %>">
<% _.each(options.sizeValues, function(size) { %>
<a class="dropdown-item <%- meta.limit == size ? 'active' : '' %> urlfilter" href="?_limit=<%- size %>">
<%- size %>
</a>
<% }) %>
<!-- var template_size -->
<% if (meta.limit) { %>
<div class="btn-group btn-group-sm" role="group">
<button id="formhandler-size-<%- idcount++ %>" type="button" class="btn btn-light btn-sm dropdown-toggle" data-toggle="dropdown"
aria-haspopup="true" aria-expanded="false">
<%- meta.limit %> rows
</button>
<div class="dropdown-menu" aria-labelledby="formhandler-size-<%- idcount %>">
<% _.each(options.sizeValues, function(size) { %>
<a class="dropdown-item <%- meta.limit == size ? 'active' : '' %> urlfilter" href="?_limit=<%- size %>">
<%- size %>
</a>
<% }) %>
</div>
</div>
</div>
<% } %>
<!-- end -->
<% } %>
<!-- end -->
<!-- var template_count -->
<% if ('count' in meta) { %>
<span class="btn btn-sm btn-light"><%- meta.count %> rows</span>
<% } %>
<!-- end -->
<!-- var template_count -->
<% if ('count' in meta) { %>
<span class="btn btn-sm btn-light">
<%- meta.count %> rows</span>
<% } %>
<!-- end -->
<!-- var template_edit -->
<button type="button" class="btn btn-primary mr-2 btn-sm edit-btn">
Edit
</button>
<!-- end -->
<!-- var template_edit -->
<button type="button" class="btn btn-primary mr-2 btn-sm edit-btn">
Edit
</button>
<!-- end -->
<!-- var template_add -->
<button type="button" class="btn btn-primary mr-2 btn-sm add-btn">
Add
</button>
<!-- end -->
<!-- var template_add -->
<button type="button" class="btn btn-primary mr-2 btn-sm add-btn">
Add
</button>
<!-- end -->
<!-- var template_export -->
<div class="btn-group btn-group-sm" role="group">
<button id="formhandler-export-<%- idcount++ %>" type="button" class="btn btn-light btn-sm dropdown-toggle" data-toggle="dropdown"
aria-haspopup="true" aria-expanded="false">
Export as
</button>
<div class="dropdown-menu" aria-labelledby="formhandler-export-<%- idcount %>">
<% _.each(options.exportFormats, function(label, key) { %>
<a class="dropdown-item" href="<%- parse(options.src).update(args).update({_format: key}) %>">
<%- label %>
</a>
<% }) %>
</div>
</div>
<!-- end -->
<!-- var template_export -->
<div class="btn-group btn-group-sm" role="group">
<button id="formhandler-export-<%- idcount++ %>" type="button" class="btn btn-light btn-sm dropdown-toggle" data-toggle="dropdown"
aria-haspopup="true" aria-expanded="false">
Export as
</button>
<div class="dropdown-menu" aria-labelledby="formhandler-export-<%- idcount %>">
<% _.each(options.exportFormats, function(label, key) { %>
<a class="dropdown-item" href="<%- parse(options.src).update(args).update({_format: key}) %>">
<%- label %>
</a>
<% }) %>
</div>
</div>
<!-- end -->
<!-- var template_filters -->
<div class="p-1">
<% var qparts = parse('?') %>
<% _.each(args['_c'], function(col_name) { %>
<% qparts.update({_c: col_name}, 'add') %>
<% var hide_col = col_name[0] == '-' %>
<% var display_name = hide_col ? col_name.slice(1) : col_name %>
<a href="?_c=<%- col_name %>"
data-mode="del"
class="badge badge-pill <%- hide_col ? 'badge-dark' : 'badge-danger' %> urlfilter"
title="<%- hide_col ? 'Show' : 'Hide' %> column <%- display_name %>">
<%- display_name %>
</a>
<% }) %>
<% _.each(args, function(list_values, key) { %>
<% if (key.charAt(0) !== '_' && key !== 'c') { %>
<% _.each(args[key], function(col_name) { %>
<% var update = {}; update[key] = col_name; qparts.update(update) %>
<a href="?<%- key %>=<%- col_name %>" data-mode="del" class="badge badge-pill badge-dark urlfilter"
title="Clear <%- key %> filter">
<%- key %> = <%- col_name %>
</a>
<% }) %>
<% } %>
<% }) %>
<% qparts = qparts.toString() %>
<% if (qparts && qparts != '?') { %>
<a href="?<%- qparts.slice(1) %>"
class="badge badge-pill badge-danger urlfilter"
data-mode="del"
title="Clear all filters">×</a>
<% } %>
</div>
<!-- end -->
<!-- var template_filters -->
<div class="p-1">
<% var qparts = parse('?') %>
<% _.each(args['_c'], function(col_name) { %>
<% qparts.update({_c: col_name}, 'add') %>
<% var hide_col = col_name[0] == '-' %>
<% var display_name = hide_col ? col_name.slice(1) : col_name %>
<a href="?_c=<%- col_name %>" data-mode="del" class="badge badge-pill <%- hide_col ? 'badge-dark' : 'badge-danger' %> urlfilter"
title="<%- hide_col ? 'Show' : 'Hide' %> column <%- display_name %>">
<%- display_name %>
</a>
<% }) %>
<% _.each(args, function(list_values, key) { %>
<% if (key.charAt(0) !== '_' && key !== 'c') { %>