-
Notifications
You must be signed in to change notification settings - Fork 8.6k
Region map visualization #10937
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Region map visualization #10937
Changes from 53 commits
Commits
Show all changes
57 commits
Select commit
Hold shift + click to select a range
1107f0c
add vector map
thomasneirynck 5e9e79a
do not use light colors in the vector map
thomasneirynck 4544daa
rebase and add vector map to map category
thomasneirynck b6de16e
show warning when not all data can be joined (with optional advanced …
thomasneirynck 9b1a943
some cleanup
thomasneirynck f6f966d
feedback
thomasneirynck a0c88fb
name value
thomasneirynck b392eb4
fix tooltip
thomasneirynck 58ba775
fix bucket labels
thomasneirynck 8c56bbc
move up in closure
thomasneirynck 457cfcc
remove boilerplate
thomasneirynck 3bbcf46
check in test stub
thomasneirynck 966b641
add selenium tests
thomasneirynck b9572a5
remove log
thomasneirynck 7524634
remove cruft
thomasneirynck 0446b84
fix faulty merge after rebase
thomasneirynck 34801b8
simplify geojson files, use natural earth data
thomasneirynck 9659efc
fix imports after rebase
thomasneirynck d010705
remove default services/add boilerplate
thomasneirynck 45737ba
stub-in retrieval of vector map metadata
thomasneirynck c7b4e91
use manifest
thomasneirynck d897daf
test new manifest structure on vector map
thomasneirynck c527dec
rename map types
thomasneirynck 4b97689
update to use new production urls
thomasneirynck 3569b39
rename to region_map
thomasneirynck 424a507
use relative paths
thomasneirynck f7b54c3
incorporate feedback (1)
thomasneirynck 94c2e54
start stripping old manifest loading from codebase
thomasneirynck edda3d6
fix typo
thomasneirynck 92b674e
remove obsolete tilemap-settings file
thomasneirynck 3cac581
remove obsolete tilemap-settings file
thomasneirynck 1885256
remove obsolete tilemap-settings file
thomasneirynck b36f895
mixin query parameters for each request
thomasneirynck 7915fc6
prevent same layer from being added twice
thomasneirynck ecbee2b
review feedback
thomasneirynck 9010b42
fill in combo-box
thomasneirynck 2cfabf5
fix error message
thomasneirynck ae4bcc2
fix error messages
thomasneirynck fbddb91
cleanup
thomasneirynck 333c2a7
add new icon
thomasneirynck 7cd8065
fix .eslintignore file
thomasneirynck dd91337
fix kibana_map test
thomasneirynck 6bdf4d5
move old tilemaptests to new service_test module
thomasneirynck c3045b1
move old tilemaptests to new service_test module
thomasneirynck 4efb34f
add vector layer test
thomasneirynck 16c3bf2
fix rebase
thomasneirynck a45b6b7
redo selenium test after rebase
thomasneirynck 91e2ffb
move loading of vector-layers to directive/disable play button on upd…
thomasneirynck c739884
add sibling aggregations
thomasneirynck 5c75704
fix rename
thomasneirynck 91c5ac2
remove log statement
thomasneirynck f462397
remove unnecessary files
thomasneirynck a4c3a5c
minor feedback
thomasneirynck ab4fe96
improve naming/remove obsolete configs
thomasneirynck fa93788
remove unused setting
thomasneirynck 5122ce1
should respect overridden urls
thomasneirynck 59cf9df
remove test cruft
thomasneirynck File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| export default function (kibana) { | ||
|
|
||
| return new kibana.Plugin({ | ||
| uiExports: { | ||
| visTypes: ['plugins/region_map/region_map_vis'] | ||
| } | ||
| }); | ||
|
|
||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| { | ||
| "name": "region_map", | ||
| "version": "kibana" | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,291 @@ | ||
| import $ from 'jquery'; | ||
| import L from 'leaflet'; | ||
| import _ from 'lodash'; | ||
| import d3 from 'd3'; | ||
| import { KibanaMapLayer } from 'ui/vis_maps/kibana_map_layer'; | ||
| import { truncatedColorMaps } from 'ui/vislib/components/color/truncated_colormaps'; | ||
|
|
||
| export default class ChoroplethLayer extends KibanaMapLayer { | ||
|
|
||
| constructor(geojsonUrl) { | ||
| super(); | ||
|
|
||
|
|
||
| this._metrics = null; | ||
| this._joinField = null; | ||
| this._colorRamp = truncatedColorMaps[Object.keys(truncatedColorMaps)[0]]; | ||
| this._tooltipFormatter = () => ''; | ||
|
|
||
| this._geojsonUrl = geojsonUrl; | ||
| this._leafletLayer = L.geoJson(null, { | ||
| onEachFeature: (feature, layer) => { | ||
| layer.on('click', () => { | ||
| this.emit('select', feature.properties[this._joinField]); | ||
| }); | ||
| let location = null; | ||
| layer.on({ | ||
| mouseover: () => { | ||
| const tooltipContents = this._tooltipFormatter(feature); | ||
| if (!location) { | ||
| const leafletGeojon = L.geoJson(feature); | ||
| location = leafletGeojon.getBounds().getCenter(); | ||
| } | ||
|
|
||
| this.emit('showTooltip', { | ||
| content: tooltipContents, | ||
| position: location | ||
| }); | ||
| }, | ||
| mouseout: () => { | ||
| this.emit('hideTooltip'); | ||
| } | ||
| }); | ||
| }, | ||
| style: emptyStyle | ||
| }); | ||
|
|
||
| this._loaded = false; | ||
| this._error = false; | ||
| $.ajax({ | ||
| dataType: 'json', | ||
| url: geojsonUrl, | ||
| success: (data) => { | ||
| this._leafletLayer.addData(data); | ||
| this._loaded = true; | ||
| this._setStyle(); | ||
| }, | ||
| error: () => { | ||
| this._loaded = true; | ||
| this._error = true; | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| _setStyle() { | ||
| if (this._error || (!this._loaded || !this._metrics || !this._joinField)) { | ||
| return; | ||
| } | ||
|
|
||
| const styler = makeChoroplethStyler(this._metrics, this._colorRamp, this._joinField); | ||
| this._leafletLayer.setStyle(styler.getLeafletStyleFunction); | ||
|
|
||
| if (this._metrics && this._metrics.length > 0) { | ||
| const { min, max } = getMinMax(this._metrics); | ||
| this._legendColors = getLegendColors(this._colorRamp); | ||
| const quantizeDomain = (min !== max) ? [min, max] : d3.scale.quantize().domain(); | ||
| this._legendQuantizer = d3.scale.quantize().domain(quantizeDomain).range(this._legendColors); | ||
| } | ||
| this.emit('styleChanged', { | ||
| mismatches: styler.getMismatches() | ||
| }); | ||
| } | ||
|
|
||
| getMetrics() { | ||
| return this._metrics; | ||
| } | ||
|
|
||
| getMetricsAgg() { | ||
| return this._metricsAgg; | ||
| } | ||
|
|
||
| getUrl() { | ||
| return this._geojsonUrl; | ||
| } | ||
|
|
||
| setTooltipFormatter(tooltipFormatter, metricsAgg, fieldName) { | ||
| this._tooltipFormatter = (geojsonFeature) => { | ||
| if (!this._metrics) { | ||
| return ''; | ||
| } | ||
| const match = this._metrics.find((bucket) => { | ||
| return bucket.term === geojsonFeature.properties[this._joinField]; | ||
| }); | ||
| return tooltipFormatter(metricsAgg, match, fieldName); | ||
| }; | ||
| } | ||
|
|
||
| setJoinField(joinfield) { | ||
| if (joinfield === this._joinField) { | ||
| return; | ||
| } | ||
| this._joinField = joinfield; | ||
| this._setStyle(); | ||
| } | ||
|
|
||
|
|
||
| setMetrics(metrics, metricsAgg) { | ||
| this._metrics = metrics; | ||
| this._metricsAgg = metricsAgg; | ||
| this._valueFormatter = this._metricsAgg.fieldFormatter(); | ||
| this._setStyle(); | ||
| } | ||
|
|
||
| setColorRamp(colorRamp) { | ||
| if (_.isEqual(colorRamp, this._colorRamp)) { | ||
| return; | ||
| } | ||
| this._colorRamp = colorRamp; | ||
| this._setStyle(); | ||
| } | ||
|
|
||
| equalsGeoJsonUrl(geojsonUrl) { | ||
| return this._geojsonUrl === geojsonUrl; | ||
| } | ||
|
|
||
| appendLegendContents(jqueryDiv) { | ||
|
|
||
|
|
||
| if (!this._legendColors || !this._legendQuantizer || !this._metricsAgg) { | ||
| return; | ||
| } | ||
|
|
||
| const titleText = this._metricsAgg.makeLabel(); | ||
| const $title = $('<div>').addClass('tilemap-legend-title').text(titleText); | ||
| jqueryDiv.append($title); | ||
|
|
||
| this._legendColors.forEach((color) => { | ||
|
|
||
| const labelText = this._legendQuantizer | ||
| .invertExtent(color) | ||
| .map(this._valueFormatter) | ||
| .join(' – '); | ||
|
|
||
| const label = $('<div>'); | ||
| const icon = $('<i>').css({ | ||
| background: color, | ||
| 'border-color': makeColorDarker(color) | ||
| }); | ||
|
|
||
| const text = $('<span>').text(labelText); | ||
| label.append(icon); | ||
| label.append(text); | ||
|
|
||
| jqueryDiv.append(label); | ||
| }); | ||
|
|
||
| } | ||
|
|
||
| } | ||
|
|
||
|
|
||
| function makeColorDarker(color) { | ||
| const amount = 1.3;//magic number, carry over from earlier | ||
| return d3.hcl(color).darker(amount).toString(); | ||
| } | ||
|
|
||
|
|
||
| function getMinMax(data) { | ||
| let min = data[0].value; | ||
| let max = data[0].value; | ||
| for (let i = 1; i < data.length; i += 1) { | ||
| min = Math.min(data[i].value, min); | ||
| max = Math.max(data[i].value, max); | ||
| } | ||
| return { min, max }; | ||
| } | ||
|
|
||
|
|
||
| function makeChoroplethStyler(data, colorramp, joinField) { | ||
|
|
||
|
|
||
| if (data.length === 0) { | ||
| return { | ||
| getLeafletStyleFunction: function () { | ||
| return emptyStyle(); | ||
| }, | ||
| getMismatches: function () { | ||
| return []; | ||
| } | ||
| }; | ||
| } | ||
|
|
||
| const { min, max } = getMinMax(data); | ||
| data = data.slice(); | ||
| return { | ||
| getLeafletStyleFunction: function (geojsonFeature) { | ||
| let lastIndex = -1; | ||
| const match = data.find((bucket, index) => { | ||
| lastIndex = index; | ||
| if (typeof bucket.term === 'string' && typeof geojsonFeature.properties[joinField] === 'string') { | ||
| return normalizeString(bucket.term) === normalizeString(geojsonFeature.properties[joinField]); | ||
| } else { | ||
| return bucket.term === geojsonFeature.properties[joinField]; | ||
| } | ||
| }); | ||
|
|
||
| if (!match) { | ||
| return emptyStyle(); | ||
| } | ||
|
|
||
| data.splice(lastIndex, 1); | ||
| return { | ||
| fillColor: getChoroplethColor(match.value, min, max, colorramp), | ||
| weight: 2, | ||
| opacity: 1, | ||
| color: 'white', | ||
| fillOpacity: 0.7 | ||
| }; | ||
| }, | ||
| /** | ||
| * should not be called until before getLeafletStyleFunction has been called | ||
| * @return {Array} | ||
| */ | ||
| getMismatches: function () { | ||
| return data.map((bucket) => bucket.term); | ||
| } | ||
| }; | ||
|
|
||
|
|
||
| } | ||
|
|
||
|
|
||
| function normalizeString(string) { | ||
| return string.trim().toLowerCase(); | ||
| } | ||
|
|
||
|
|
||
| function getLegendColors(colorRamp) { | ||
| const colors = []; | ||
| colors[0] = getColor(colorRamp, 0); | ||
| colors[1] = getColor(colorRamp, Math.floor(colorRamp.length * 1 / 4)); | ||
| colors[2] = getColor(colorRamp, Math.floor(colorRamp.length * 2 / 4)); | ||
| colors[3] = getColor(colorRamp, Math.floor(colorRamp.length * 3 / 4)); | ||
| colors[4] = getColor(colorRamp, colorRamp.length - 1); | ||
| return colors; | ||
| } | ||
|
|
||
| function getColor(colorRamp, i) { | ||
|
|
||
| if (!colorRamp[i]) { | ||
| return getColor(); | ||
| } | ||
|
|
||
| const color = colorRamp[i][1]; | ||
| const red = Math.floor(color[0] * 255); | ||
| const green = Math.floor(color[1] * 255); | ||
| const blue = Math.floor(color[2] * 255); | ||
| return `rgb(${red},${green},${blue})`; | ||
| } | ||
|
|
||
|
|
||
| function getChoroplethColor(value, min, max, colorRamp) { | ||
| if (min === max) { | ||
| return getColor(colorRamp, colorRamp.length - 1); | ||
| } | ||
| const fraction = (value - min) / (max - min); | ||
| const index = Math.round(colorRamp.length * fraction) - 1; | ||
| const i = Math.max(Math.min(colorRamp.length - 1, index), 0); | ||
|
|
||
| return getColor(colorRamp, i); | ||
| } | ||
|
|
||
| const emptyStyleObject = { | ||
| weight: 1, | ||
| opacity: 0.6, | ||
| color: 'rgb(200,200,200)', | ||
| fillOpacity: 0 | ||
| }; | ||
| function emptyStyle() { | ||
| return emptyStyleObject; | ||
| } | ||
|
|
||
10 changes: 10 additions & 0 deletions
10
src/core_plugins/region_map/public/images/icon-vector-map.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doing
data.slice()here and then doingdata.splice(lastIndex, 1)below inside of thegetLeafletStyleFunctionis rather confusing. We're usinggetLeafletStyleFunctionto mutatedatathat is also used bygetMismatches, so the order that these functions are called becomes significant. Was this intentional?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes, that order is significant. the getMisMatches is just for the client to be able to read out the errors of the inner-join (ie. the style function).