mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
Merge pull request #3830 from rashidkpc/feature/tilemap-heatmap
Tilemap heatmap
This commit is contained in:
commit
777c3d0e61
11 changed files with 624 additions and 71 deletions
|
@ -39,6 +39,7 @@
|
|||
"inflection": "~1.3.5",
|
||||
"jquery": "~2.1.0",
|
||||
"leaflet": "0.7.3",
|
||||
"Leaflet.heat": "Leaflet/Leaflet.heat#627ede7c11bbe43",
|
||||
"lesshat": "~3.0.2",
|
||||
"lodash": "~2.4.1",
|
||||
"moment": "~2.9.0",
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
<table>
|
||||
<tbody>
|
||||
<tr ng-repeat="detail in details" >
|
||||
<td><b>{{detail.label}}</b></td>
|
||||
<td>{{detail.value}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
|
@ -0,0 +1,32 @@
|
|||
define(function (require) {
|
||||
return function TileMapTooltipFormatter($compile, $rootScope) {
|
||||
var $ = require('jquery');
|
||||
|
||||
var $tooltipScope = $rootScope.$new();
|
||||
var $tooltip = $(require('text!components/agg_response/geo_json/_tooltip.html'));
|
||||
$compile($tooltip)($tooltipScope);
|
||||
|
||||
return function tooltipFormatter(feature) {
|
||||
if (!feature) return '';
|
||||
|
||||
var details = $tooltipScope.details = [];
|
||||
|
||||
var lat = feature.geometry.coordinates[1];
|
||||
var lng = feature.geometry.coordinates[0];
|
||||
|
||||
var metric = {
|
||||
label: feature.properties.valueLabel,
|
||||
value: feature.properties.count
|
||||
};
|
||||
var location = {
|
||||
label: 'Center',
|
||||
value: lat.toFixed(4) + ', ' + lng.toFixed(4)
|
||||
};
|
||||
|
||||
details.push(metric, location);
|
||||
|
||||
$tooltipScope.$apply();
|
||||
return $tooltip[0].outerHTML;
|
||||
};
|
||||
};
|
||||
});
|
|
@ -3,6 +3,8 @@ define(function (require) {
|
|||
var _ = require('lodash');
|
||||
|
||||
var readRows = require('components/agg_response/geo_json/_read_rows');
|
||||
var tooltipFormatter = Private(require('components/agg_response/geo_json/_tooltip_formatter'));
|
||||
|
||||
function findCol(table, name) {
|
||||
return _.findIndex(table.columns, function (col) {
|
||||
return col.aggConfig.schema.name === name;
|
||||
|
@ -25,6 +27,7 @@ define(function (require) {
|
|||
});
|
||||
|
||||
var chart = {};
|
||||
chart.tooltipFormatter = tooltipFormatter;
|
||||
var geoJson = chart.geoJson = {
|
||||
type: 'FeatureCollection',
|
||||
features: []
|
||||
|
@ -37,6 +40,10 @@ define(function (require) {
|
|||
max: 0
|
||||
};
|
||||
|
||||
if (agg.metric._opts.params && agg.metric._opts.params.field) {
|
||||
props.metricField = agg.metric._opts.params.field;
|
||||
}
|
||||
|
||||
// set precision from the bucketting column, if we have one
|
||||
if (agg.geo) {
|
||||
props.precision = _.parseInt(agg.geo.params.precision);
|
||||
|
|
|
@ -76,19 +76,56 @@
|
|||
|
||||
.leaflet-popup {
|
||||
margin-bottom: 16px !important;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.leaflet-popup-content-wrapper {
|
||||
background: rgba(70, 82, 93, 0.95) !important;
|
||||
color: @gray-lighter !important;
|
||||
background: @tooltip-bg !important;
|
||||
color: @tooltip-color !important;
|
||||
border-radius: 4px !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.leaflet-popup-content {
|
||||
padding: 8px !important;
|
||||
margin: 0 !important;
|
||||
line-height: 14px !important;
|
||||
line-height: 1.1 !important;
|
||||
font-size: 12px;
|
||||
font-weight: normal;
|
||||
word-wrap: break-word;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
|
||||
> :last-child {
|
||||
margin-bottom: @tooltip-space;
|
||||
}
|
||||
|
||||
> * {
|
||||
margin: @tooltip-space @tooltip-space 0;
|
||||
}
|
||||
|
||||
table {
|
||||
td,th {
|
||||
padding: @tooltip-space-tight;
|
||||
|
||||
&.row-bucket {
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
|
||||
// if there is a header, give it a border that matches
|
||||
// those in the body
|
||||
thead tr {
|
||||
border-bottom: 1px solid @gray;
|
||||
}
|
||||
|
||||
// only apply to tr in the body, not the header
|
||||
tbody tr {
|
||||
border-top: 1px solid @gray;
|
||||
&:first-child {
|
||||
border-top: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.leaflet-popup-tip-container, .leaflet-popup-close-button {
|
||||
|
@ -109,7 +146,7 @@
|
|||
}
|
||||
|
||||
.leaflet-draw-tooltip {
|
||||
display: none;
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* filter to desaturate mapquest tiles */
|
||||
|
|
|
@ -3,11 +3,11 @@ define(function (require) {
|
|||
var _ = require('lodash');
|
||||
var $ = require('jquery');
|
||||
var L = require('leaflet');
|
||||
require('leaflet-heat');
|
||||
require('leaflet-draw');
|
||||
|
||||
var Dispatch = Private(require('components/vislib/lib/dispatch'));
|
||||
var Chart = Private(require('components/vislib/visualizations/_chart'));
|
||||
var errors = require('errors');
|
||||
|
||||
require('css!components/vislib/styles/main');
|
||||
|
||||
|
@ -29,11 +29,14 @@ define(function (require) {
|
|||
if (!(this instanceof TileMap)) {
|
||||
return new TileMap(handler, chartEl, chartData);
|
||||
}
|
||||
|
||||
TileMap.Super.apply(this, arguments);
|
||||
|
||||
// track the map objects
|
||||
this.maps = [];
|
||||
|
||||
this.tooltipFormatter = chartData.tooltipFormatter;
|
||||
|
||||
this.events = new Dispatch(handler);
|
||||
|
||||
// add allmin and allmax to geoJson
|
||||
|
@ -55,7 +58,7 @@ define(function (require) {
|
|||
// clean up old maps
|
||||
self.destroy();
|
||||
|
||||
// create a new maps array
|
||||
// clear maps array
|
||||
self.maps = [];
|
||||
self.popups = [];
|
||||
|
||||
|
@ -68,10 +71,10 @@ define(function (require) {
|
|||
|
||||
selection.each(function (data) {
|
||||
|
||||
var mapData = data.geoJson;
|
||||
var div = $(this).addClass('tilemap');
|
||||
// add leaflet latLngs to properties for tooltip
|
||||
var mapData = self.addLatLng(data.geoJson);
|
||||
|
||||
var featureLayer;
|
||||
var div = $(this).addClass('tilemap');
|
||||
var tileLayer = L.tileLayer('https://otile{s}-s.mqcdn.com/tiles/1.0.0/map/{z}/{x}/{y}.jpeg', {
|
||||
attribution: 'Tiles by <a href="http://www.mapquest.com/">MapQuest</a> — ' +
|
||||
'Map data © <a href="http://openstreetmap.org">OpenStreetMap</a> contributors, ' +
|
||||
|
@ -79,7 +82,6 @@ define(function (require) {
|
|||
subdomains: '1234'
|
||||
});
|
||||
|
||||
|
||||
var drawOptions = {draw: {}};
|
||||
_.each(['polyline', 'polygon', 'circle', 'marker', 'rectangle'], function (drawShape) {
|
||||
if (!self.events.listenerCount(drawShape)) {
|
||||
|
@ -108,25 +110,28 @@ define(function (require) {
|
|||
|
||||
var map = L.map(div[0], mapOptions);
|
||||
|
||||
var featureLayer = self.markerType(map, mapData).addTo(map);
|
||||
|
||||
if (data.geoJson.features.length) {
|
||||
map.addControl(new L.Control.Draw(drawOptions));
|
||||
}
|
||||
|
||||
tileLayer.on('tileload', function () {
|
||||
function saturateTiles() {
|
||||
self.saturateTiles();
|
||||
});
|
||||
}
|
||||
|
||||
featureLayer = self.markerType(map, mapData).addTo(map);
|
||||
tileLayer.on('tileload', saturateTiles);
|
||||
|
||||
map.on('unload', function () {
|
||||
tileLayer.off('tileload', self.saturateTiles);
|
||||
tileLayer.off('tileload', saturateTiles);
|
||||
});
|
||||
|
||||
map.on('moveend', function setZoomCenter() {
|
||||
self._attr.mapZoom = map.getZoom();
|
||||
self._attr.mapCenter = map.getCenter();
|
||||
|
||||
featureLayer.clearLayers();
|
||||
map.removeLayer(featureLayer);
|
||||
|
||||
featureLayer = self.markerType(map, mapData).addTo(map);
|
||||
});
|
||||
|
||||
|
@ -196,7 +201,11 @@ define(function (require) {
|
|||
};
|
||||
|
||||
/**
|
||||
* Return features within the map bounds
|
||||
* return whether feature is within map bounds
|
||||
*
|
||||
* @method _filterToMapBounds
|
||||
* @param map {Leaflet Object}
|
||||
* @return {boolean}
|
||||
*/
|
||||
TileMap.prototype._filterToMapBounds = function (map) {
|
||||
function cloneAndReverse(arr) { return _(_.clone(arr)).reverse().value(); }
|
||||
|
@ -242,19 +251,35 @@ define(function (require) {
|
|||
};
|
||||
|
||||
/**
|
||||
* zoom map to fit all features in featureLayer
|
||||
* add Leaflet latLng to mapData properties
|
||||
*
|
||||
* @method fitBounds
|
||||
* @param map {Object}
|
||||
* @param featureLayer {Leaflet object}
|
||||
* @return {Leaflet object} featureLayer
|
||||
* @method addLatLng
|
||||
* @param mapData {geoJson Object}
|
||||
* @return mapData {geoJson Object}
|
||||
*/
|
||||
TileMap.prototype.fitBounds = function (map, featureLayer) {
|
||||
map.fitBounds(featureLayer.getBounds());
|
||||
TileMap.prototype.addLatLng = function (mapData) {
|
||||
for (var i = 0; i < mapData.features.length; i++) {
|
||||
var latLng = L.latLng(mapData.features[i].geometry.coordinates[1], mapData.features[i].geometry.coordinates[0]);
|
||||
mapData.features[i].properties.latLng = latLng;
|
||||
}
|
||||
|
||||
return mapData;
|
||||
};
|
||||
|
||||
/**
|
||||
* remove css class on map tiles
|
||||
* zoom map to fit all features in featureLayer
|
||||
*
|
||||
* @method fitBounds
|
||||
* @param map {Leaflet Object}
|
||||
* @param mapData {geoJson Object}
|
||||
* @return {undefined}
|
||||
*/
|
||||
TileMap.prototype.fitBounds = function (map, mapData) {
|
||||
map.fitBounds(mapData._latlngs || mapData.getBounds());
|
||||
};
|
||||
|
||||
/**
|
||||
* remove css class for desat filters on map tiles
|
||||
*
|
||||
* @method saturateTiles
|
||||
* @return {Leaflet object} featureLayer
|
||||
|
@ -265,17 +290,119 @@ define(function (require) {
|
|||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Finds nearest feature in mapData to event latlng
|
||||
*
|
||||
* @method nearestFeature
|
||||
* @param point {Leaflet Object}
|
||||
* @param mapData {geoJson Object}
|
||||
* @return nearestPoint {Leaflet Object}
|
||||
*/
|
||||
TileMap.prototype.nearestFeature = function (point, mapData) {
|
||||
var self = this;
|
||||
var distance = Infinity;
|
||||
var nearest;
|
||||
|
||||
if (point.lng < -180 || point.lng > 180) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (var i = 0; i < mapData.features.length; i++) {
|
||||
var dist = point.distanceTo(mapData.features[i].properties.latLng);
|
||||
if (dist < distance) {
|
||||
distance = dist;
|
||||
nearest = mapData.features[i];
|
||||
}
|
||||
}
|
||||
nearest.properties.eventDistance = distance;
|
||||
|
||||
return nearest;
|
||||
};
|
||||
|
||||
/**
|
||||
* display tooltip if feature is close enough to event latlng
|
||||
*
|
||||
* @method tooltipProximity
|
||||
* @param latlng {Leaflet Object}
|
||||
* @param zoom {Number}
|
||||
* @param feature {geoJson Object}
|
||||
* @param map {Leaflet Object}
|
||||
* @return boolean
|
||||
*/
|
||||
TileMap.prototype.tooltipProximity = function (latlng, zoom, feature, map) {
|
||||
if (!feature) {
|
||||
return;
|
||||
}
|
||||
|
||||
var showTip = false;
|
||||
|
||||
// zoomScale takes map zoom and returns proximity value for tooltip display
|
||||
// domain (input values) is map zoom (min 1 and max 18)
|
||||
// range (output values) is distance in meters
|
||||
// used to compare proximity of event latlng to feature latlng
|
||||
var zoomScale = d3.scale.linear()
|
||||
.domain([1, 4, 7, 10, 13, 16, 18])
|
||||
.range([1000000, 300000, 100000, 15000, 2000, 150, 50]);
|
||||
|
||||
var proximity = zoomScale(zoom);
|
||||
var distance = latlng.distanceTo(feature.properties.latLng);
|
||||
|
||||
// maxLngDif is max difference in longitudes
|
||||
// to prevent feature tooltip from appearing 360°
|
||||
// away from event latlng
|
||||
var maxLngDif = 40;
|
||||
var lngDif = Math.abs(latlng.lng - feature.properties.latLng.lng);
|
||||
|
||||
if (distance < proximity && lngDif < maxLngDif) {
|
||||
showTip = true;
|
||||
}
|
||||
|
||||
delete feature.properties.eventDistance;
|
||||
|
||||
var testScale = d3.scale.pow().exponent(0.2)
|
||||
.domain([1, 18])
|
||||
.range([1500000, 50]);
|
||||
return showTip;
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if event latlng is within bounds of mapData
|
||||
* features and shows tooltip for that feature
|
||||
*
|
||||
* @method showTooltip
|
||||
* @param e {Event}
|
||||
* @param map {Leaflet Object}
|
||||
* @param mapData {geoJson Object}
|
||||
* @return {undefined}
|
||||
*/
|
||||
TileMap.prototype.showTooltip = function (map, feature) {
|
||||
var content = this.tooltipFormatter(feature);
|
||||
if (!content) {
|
||||
return;
|
||||
}
|
||||
|
||||
var lat = feature.geometry.coordinates[1];
|
||||
var lng = feature.geometry.coordinates[0];
|
||||
var latLng = L.latLng(lat, lng);
|
||||
|
||||
L.popup({autoPan: false})
|
||||
.setLatLng(latLng)
|
||||
.setContent(content)
|
||||
.openOn(map);
|
||||
};
|
||||
|
||||
/**
|
||||
* Switch type of data overlay for map:
|
||||
* creates featurelayer from mapData (geoJson)
|
||||
*
|
||||
* @method markerType
|
||||
* @param map {Object}
|
||||
* @param mapData {Object}
|
||||
* @param map {Leaflet Object}
|
||||
* @param mapData {geoJson Object}
|
||||
* @return {Leaflet object} featureLayer
|
||||
*/
|
||||
TileMap.prototype.markerType = function (map, mapData) {
|
||||
var featureLayer;
|
||||
|
||||
if (mapData) {
|
||||
if (this._attr.mapType === 'Scaled Circle Markers') {
|
||||
featureLayer = this.scaledCircleMarkers(map, mapData);
|
||||
|
@ -283,6 +410,8 @@ define(function (require) {
|
|||
featureLayer = this.shadedCircleMarkers(map, mapData);
|
||||
} else if (this._attr.mapType === 'Shaded Geohash Grid') {
|
||||
featureLayer = this.shadedGeohashGrid(map, mapData);
|
||||
} else if (this._attr.mapType === 'Heatmap') {
|
||||
featureLayer = this.heatMap(map, mapData);
|
||||
} else {
|
||||
featureLayer = this.scaledCircleMarkers(map, mapData);
|
||||
}
|
||||
|
@ -297,8 +426,8 @@ define(function (require) {
|
|||
* with circle markers that are scaled to illustrate values
|
||||
*
|
||||
* @method scaledCircleMarkers
|
||||
* @param map {Object}
|
||||
* @param mapData {Object}
|
||||
* @param map {Leaflet Object}
|
||||
* @param mapData {geoJson Object}
|
||||
* @return {Leaflet object} featureLayer
|
||||
*/
|
||||
TileMap.prototype.scaledCircleMarkers = function (map, mapData) {
|
||||
|
@ -322,7 +451,7 @@ define(function (require) {
|
|||
return L.circleMarker(latlng).setRadius(scaledRadius);
|
||||
},
|
||||
onEachFeature: function (feature, layer) {
|
||||
self.bindPopup(feature, layer);
|
||||
self.bindPopup(feature, layer, map);
|
||||
},
|
||||
style: function (feature) {
|
||||
return self.applyShadingStyle(feature, min, max);
|
||||
|
@ -344,8 +473,8 @@ define(function (require) {
|
|||
* with circle markers that are shaded to illustrate values
|
||||
*
|
||||
* @method shadedCircleMarkers
|
||||
* @param map {Object}
|
||||
* @param mapData {Object}
|
||||
* @param map {Leaflet Object}
|
||||
* @param mapData {geoJson Object}
|
||||
* @return {Leaflet object} featureLayer
|
||||
*/
|
||||
TileMap.prototype.shadedCircleMarkers = function (map, mapData) {
|
||||
|
@ -365,7 +494,7 @@ define(function (require) {
|
|||
return L.circle(latlng, radius);
|
||||
},
|
||||
onEachFeature: function (feature, layer) {
|
||||
self.bindPopup(feature, layer);
|
||||
self.bindPopup(feature, layer, map);
|
||||
},
|
||||
style: function (feature) {
|
||||
return self.applyShadingStyle(feature, min, max);
|
||||
|
@ -387,8 +516,8 @@ define(function (require) {
|
|||
* with rectangles that show the geohash grid bounds
|
||||
*
|
||||
* @method geohashGrid
|
||||
* @param map {Object}
|
||||
* @param mapData {Object}
|
||||
* @param map {Leaflet Object}
|
||||
* @param mapData {geoJson Object}
|
||||
* @return {undefined}
|
||||
*/
|
||||
TileMap.prototype.shadedGeohashGrid = function (map, mapData) {
|
||||
|
@ -404,15 +533,15 @@ define(function (require) {
|
|||
pointToLayer: function (feature, latlng) {
|
||||
var geohashRect = feature.properties.rectangle;
|
||||
// get bounds from northEast[3] and southWest[1]
|
||||
// points in geohash rectangle
|
||||
var bounds = [
|
||||
// corners in geohash rectangle
|
||||
var corners = [
|
||||
[geohashRect[3][1], geohashRect[3][0]],
|
||||
[geohashRect[1][1], geohashRect[1][0]]
|
||||
];
|
||||
return L.rectangle(bounds);
|
||||
return L.rectangle(corners);
|
||||
},
|
||||
onEachFeature: function (feature, layer) {
|
||||
self.bindPopup(feature, layer);
|
||||
self.bindPopup(feature, layer, map);
|
||||
layer.on({
|
||||
mouseover: function (e) {
|
||||
var layer = e.target;
|
||||
|
@ -437,12 +566,79 @@ define(function (require) {
|
|||
return featureLayer;
|
||||
};
|
||||
|
||||
/**
|
||||
* Type of data overlay for map:
|
||||
* creates canvas layer from mapData (geoJson)
|
||||
* with leaflet.heat plugin
|
||||
*
|
||||
* @method heatMap
|
||||
* @param map {Leaflet Object}
|
||||
* @param mapData {geoJson Object}
|
||||
* @return featureLayer {Leaflet object}
|
||||
*/
|
||||
TileMap.prototype.heatMap = function (map, mapData) {
|
||||
var self = this;
|
||||
var max = mapData.properties.allmax;
|
||||
var points = this.dataToHeatArray(mapData, max);
|
||||
|
||||
var options = {
|
||||
radius: +this._attr.heatRadius,
|
||||
blur: +this._attr.heatBlur,
|
||||
maxZoom: +this._attr.heatMaxZoom,
|
||||
minOpacity: +this._attr.heatMinOpacity
|
||||
};
|
||||
|
||||
var featureLayer = L.heatLayer(points, options);
|
||||
|
||||
if (self._attr.addTooltip && self.tooltipFormatter && !self._attr.disableTooltips) {
|
||||
map.on('mousemove', _.debounce(mouseMoveLocation, 15, {
|
||||
'leading': true,
|
||||
'trailing': false
|
||||
}));
|
||||
map.on('mouseout', function (e) {
|
||||
map.closePopup();
|
||||
});
|
||||
map.on('mousedown', function () {
|
||||
self._attr.disableTooltips = true;
|
||||
map.closePopup();
|
||||
});
|
||||
map.on('mouseup', function () {
|
||||
self._attr.disableTooltips = false;
|
||||
});
|
||||
}
|
||||
|
||||
function mouseMoveLocation(e) {
|
||||
map.closePopup();
|
||||
|
||||
// unhighlight all svgs
|
||||
d3.selectAll('path.geohash', this.chartEl).classed('geohash-hover', false);
|
||||
|
||||
if (!mapData.features.length || self._attr.disableTooltips) {
|
||||
return;
|
||||
}
|
||||
|
||||
var latlng = e.latlng;
|
||||
|
||||
// find nearest feature to event latlng
|
||||
var feature = self.nearestFeature(latlng, mapData);
|
||||
|
||||
var zoom = map.getZoom();
|
||||
|
||||
// show tooltip if close enough to event latlng
|
||||
if (self.tooltipProximity(latlng, zoom, feature, map)) {
|
||||
self.showTooltip(map, feature, latlng);
|
||||
}
|
||||
}
|
||||
|
||||
return featureLayer;
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds label div to each map when data is split
|
||||
*
|
||||
* @method addLabel
|
||||
* @param mapLabel {String}
|
||||
* @param map {Object}
|
||||
* @param map {Leaflet Object}
|
||||
* @return {undefined}
|
||||
*/
|
||||
TileMap.prototype.addLabel = function (mapLabel, map) {
|
||||
|
@ -464,7 +660,7 @@ define(function (require) {
|
|||
*
|
||||
* @method addLegend
|
||||
* @param data {Object}
|
||||
* @param map {Object}
|
||||
* @param map {Leaflet Object}
|
||||
* @return {undefined}
|
||||
*/
|
||||
TileMap.prototype.addLegend = function (data, map) {
|
||||
|
@ -534,6 +730,7 @@ define(function (require) {
|
|||
* Invalidate the size of the map, so that leaflet will resize to fit.
|
||||
* then moves to center
|
||||
*
|
||||
* @method resizeArea
|
||||
* @return {undefined}
|
||||
*/
|
||||
TileMap.prototype.resizeArea = function () {
|
||||
|
@ -552,26 +749,54 @@ define(function (require) {
|
|||
* @param layer {Object}
|
||||
* return {undefined}
|
||||
*/
|
||||
TileMap.prototype.bindPopup = function (feature, layer) {
|
||||
var props = feature.properties;
|
||||
var popup = L.popup({
|
||||
className: 'leaflet-popup-kibana',
|
||||
autoPan: false
|
||||
})
|
||||
.setContent(
|
||||
'Geohash: ' + props.geohash + '<br>' +
|
||||
'Center: ' + props.center[1].toFixed(1) + ', ' + props.center[0].toFixed(1) + '<br>' +
|
||||
props.valueLabel + ': ' + props.count
|
||||
);
|
||||
layer.bindPopup(popup)
|
||||
.on('mouseover', function (e) {
|
||||
layer.openPopup();
|
||||
})
|
||||
.on('mouseout', function (e) {
|
||||
layer.closePopup();
|
||||
TileMap.prototype.bindPopup = function (feature, layer, map) {
|
||||
var self = this;
|
||||
var popup = layer.on({
|
||||
mouseover: function (e) {
|
||||
var layer = e.target;
|
||||
// bring layer to front if not older browser
|
||||
if (!L.Browser.ie && !L.Browser.opera) {
|
||||
layer.bringToFront();
|
||||
}
|
||||
var latlng = L.latLng(feature.geometry.coordinates[0], feature.geometry.coordinates[1]);
|
||||
self.showTooltip(map, feature, latlng);
|
||||
},
|
||||
mouseout: function (e) {
|
||||
map.closePopup();
|
||||
}
|
||||
});
|
||||
|
||||
this.popups.push({elem: popup, layer: layer});
|
||||
this.popups.push(popup);
|
||||
};
|
||||
|
||||
/**
|
||||
* retuns data for data for heat map intensity
|
||||
* if heatNormalizeData attribute is checked/true
|
||||
• normalizes data for heat map intensity
|
||||
*
|
||||
* @param mapData {geoJson Object}
|
||||
* @param nax {Number}
|
||||
* @method dataToHeatArray
|
||||
* @return {Array}
|
||||
*/
|
||||
TileMap.prototype.dataToHeatArray = function (mapData, max) {
|
||||
var self = this;
|
||||
|
||||
return mapData.features.map(function (feature) {
|
||||
var lat = feature.geometry.coordinates[1];
|
||||
var lng = feature.geometry.coordinates[0];
|
||||
var heatIntensity;
|
||||
|
||||
if (!self._attr.heatNormalizeData) {
|
||||
// show bucket count on heatmap
|
||||
heatIntensity = feature.properties.count;
|
||||
} else {
|
||||
// show bucket count normalized to max value
|
||||
heatIntensity = parseInt(feature.properties.count / max * 100);
|
||||
}
|
||||
|
||||
return [lat, lng, heatIntensity];
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -609,8 +834,12 @@ define(function (require) {
|
|||
* @method radiusScale
|
||||
* @param count {Number}
|
||||
* @param max {Number}
|
||||
<<<<<<< HEAD
|
||||
* @param feature {Object}
|
||||
=======
|
||||
* @param zoom {Number}
|
||||
* @param precision {Number}
|
||||
>>>>>>> f0414d554915d151e2cdc3501bd3c7fd1889a0a8
|
||||
* @return {Number}
|
||||
*/
|
||||
TileMap.prototype.radiusScale = function (count, max, zoom, precision) {
|
||||
|
@ -681,8 +910,7 @@ define(function (require) {
|
|||
TileMap.prototype.destroy = function () {
|
||||
if (this.popups) {
|
||||
this.popups.forEach(function (popup) {
|
||||
popup.elem.off('mouseover').off('mouseout');
|
||||
popup.layer.unbindPopup(popup.elem);
|
||||
popup.off('mouseover').off('mouseout');
|
||||
});
|
||||
this.popups = [];
|
||||
}
|
||||
|
|
|
@ -1,16 +1,110 @@
|
|||
<!-- vis type specific options -->
|
||||
<div class="form-group">
|
||||
<label>Map type</label>
|
||||
<select
|
||||
name="agg"
|
||||
<select name="agg"
|
||||
class="form-control"
|
||||
ng-model="vis.params.mapType"
|
||||
ng-init="vis.params.mapType || vis.type.params.mapTypes[0]"
|
||||
ng-options="mapType as mapType for mapType in vis.type.params.mapTypes">
|
||||
ng-options="mapType as mapType for mapType in vis.type.params.mapTypes"
|
||||
>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="vis-option-item">
|
||||
<div ng-if="vis.params.mapType === 'Heatmap'" class="form-group">
|
||||
<div>
|
||||
<label>
|
||||
Radius
|
||||
<kbn-info placement="right" info="Size of heatmap dots. Default: 25"></kbn-info>
|
||||
</label>
|
||||
<div class="vis-editor-agg-form-row">
|
||||
<input
|
||||
name="heatRadius"
|
||||
ng-model="vis.params.heatRadius"
|
||||
required
|
||||
class="form-control"
|
||||
type="range"
|
||||
min="5"
|
||||
max="50"
|
||||
step="1"
|
||||
>
|
||||
<div class="form-group vis-editor-agg-form-value">
|
||||
{{vis.params.heatRadius}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label>
|
||||
Blur
|
||||
<kbn-info placement="right" info="Amount of blur applied to dots. Default: 15"></kbn-info>
|
||||
</label>
|
||||
<div class="vis-editor-agg-form-row">
|
||||
<input
|
||||
name="heatBlur"
|
||||
ng-model="vis.params.heatBlur"
|
||||
required
|
||||
class="form-control"
|
||||
type="range"
|
||||
min="1"
|
||||
max="25"
|
||||
step="1"
|
||||
>
|
||||
<div class="form-group vis-editor-agg-form-value">
|
||||
{{vis.params.heatBlur}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label>
|
||||
Maximum zoom
|
||||
<kbn-info placement="right" info="Map zoom at which all dots are displayed at full intensity. Default: 16"></kbn-info>
|
||||
</label>
|
||||
<div class="vis-editor-agg-form-row">
|
||||
<input
|
||||
name="heatMaxZoom"
|
||||
ng-model="vis.params.heatMaxZoom"
|
||||
required
|
||||
class="form-control"
|
||||
type="range"
|
||||
min="1"
|
||||
max="18"
|
||||
step="1"
|
||||
>
|
||||
<div class="vis-editor-agg-form-value">
|
||||
{{vis.params.heatMaxZoom}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label>
|
||||
Minimum opacity
|
||||
<kbn-info placement="right" info="Minimum opacity of dots. Default: 0.1"></kbn-info>
|
||||
</label>
|
||||
<div class="vis-editor-agg-form-row">
|
||||
<input
|
||||
name="heatMinOpacity"
|
||||
ng-model="vis.params.heatMinOpacity"
|
||||
required
|
||||
class="form-control"
|
||||
type="range"
|
||||
min="0"
|
||||
max="1.0"
|
||||
step="0.01"
|
||||
>
|
||||
<div class="vis-editor-agg-form-value">
|
||||
{{vis.params.heatMinOpacity}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="vis.params.addTooltip">
|
||||
Show Tooltip
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="vis-option-item form-group">
|
||||
<label>
|
||||
<input type="checkbox"
|
||||
name="isDesaturated"
|
||||
|
|
|
@ -14,9 +14,15 @@ define(function (require) {
|
|||
params: {
|
||||
defaults: {
|
||||
mapType: 'Scaled Circle Markers',
|
||||
isDesaturated: true
|
||||
isDesaturated: true,
|
||||
heatMaxZoom: 16,
|
||||
heatMinOpacity: 0.1,
|
||||
heatRadius: 25,
|
||||
heatBlur: 15,
|
||||
heatNormalizeData: true,
|
||||
addTooltip: true
|
||||
},
|
||||
mapTypes: ['Scaled Circle Markers', 'Shaded Circle Markers', 'Shaded Geohash Grid'],
|
||||
mapTypes: ['Scaled Circle Markers', 'Shaded Circle Markers', 'Shaded Geohash Grid', 'Heatmap'],
|
||||
editor: require('text!plugins/vis_types/vislib/editors/tile_map.html')
|
||||
},
|
||||
listeners: {
|
||||
|
|
|
@ -3,4 +3,4 @@
|
|||
view options
|
||||
</div>
|
||||
<div class="visualization-options"></div>
|
||||
</li>
|
||||
</li>
|
|
@ -25,6 +25,7 @@ require.config({
|
|||
faker: 'bower_components/Faker/faker',
|
||||
file_saver: 'bower_components/FileSaver/FileSaver',
|
||||
gridster: 'bower_components/gridster/dist/jquery.gridster',
|
||||
'leaflet-heat': 'bower_components/Leaflet.heat/dist/leaflet-heat',
|
||||
inflection: 'bower_components/inflection/lib/inflection',
|
||||
jquery: 'bower_components/jquery/dist/jquery',
|
||||
leaflet: 'bower_components/leaflet/dist/leaflet',
|
||||
|
@ -51,6 +52,9 @@ require.config({
|
|||
'ace-json': ['ace'],
|
||||
'angular-ui-ace': ['angular', 'ace', 'ace-json'],
|
||||
'ng-clip': ['angular', 'zeroclipboard'],
|
||||
'leaflet-heat': {
|
||||
deps: ['leaflet']
|
||||
},
|
||||
inflection: {
|
||||
exports: 'inflection'
|
||||
},
|
||||
|
|
|
@ -12,7 +12,7 @@ define(function (require) {
|
|||
];
|
||||
var names = ['geojson', 'columns', 'rows'];
|
||||
// TODO: Test the specific behavior of each these
|
||||
var mapTypes = ['Scaled Circle Markers', 'Shaded Circle Markers', 'Shaded Geohash Grid'];
|
||||
var mapTypes = ['Scaled Circle Markers', 'Shaded Circle Markers', 'Shaded Geohash Grid', 'Heatmap'];
|
||||
|
||||
angular.module('TileMapFactory', ['kibana']);
|
||||
|
||||
|
@ -25,6 +25,7 @@ define(function (require) {
|
|||
mapType: type
|
||||
};
|
||||
|
||||
|
||||
module('TileMapFactory');
|
||||
inject(function (Private) {
|
||||
vis = Private(require('vislib_fixtures/_vis_fixture'))(visLibParams);
|
||||
|
@ -130,10 +131,26 @@ define(function (require) {
|
|||
describe('Methods', function () {
|
||||
var vis;
|
||||
var leafletContainer;
|
||||
var map;
|
||||
var mapData;
|
||||
var i;
|
||||
var feature;
|
||||
var point;
|
||||
var min;
|
||||
var max;
|
||||
var zoom;
|
||||
|
||||
beforeEach(function () {
|
||||
vis = bootstrapAndRender(dataArray[0], 'Scaled Circle Markers');
|
||||
leafletContainer = $(vis.el).find('.leaflet-container');
|
||||
map = vis.handler.charts[0].maps[0];
|
||||
mapData = vis.data.geoJson;
|
||||
i = _.random(0, mapData.features.length - 1);
|
||||
feature = mapData.features[i];
|
||||
point = feature.properties.latLng;
|
||||
min = mapData.properties.allmin;
|
||||
max = mapData.properties.allmax;
|
||||
zoom = _.random(1, 12);
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
|
@ -167,8 +184,7 @@ define(function (require) {
|
|||
describe('geohashMinDistance method', function () {
|
||||
it('should return a number', function () {
|
||||
vis.handler.charts.forEach(function (chart) {
|
||||
var feature = chart.chartData.geoJson.features[0];
|
||||
expect(_.isNumber(chart.geohashMinDistance(feature))).to.be(true);
|
||||
expect(_.isFinite(chart.geohashMinDistance(feature))).to.be(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -255,6 +271,126 @@ define(function (require) {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('dataToHeatArray method', function () {
|
||||
it('should return an array', function () {
|
||||
vis.handler.charts.forEach(function (chart) {
|
||||
expect(chart.dataToHeatArray(mapData, max)).to.be.an(Array);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return an array item for each feature', function () {
|
||||
vis.handler.charts.forEach(function (chart) {
|
||||
expect(chart.dataToHeatArray(mapData, max).length).to.be(mapData.features.length);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return an array item with lat, lng, metric for each feature', function () {
|
||||
vis.handler.charts.forEach(function (chart) {
|
||||
var lat = feature.geometry.coordinates[1];
|
||||
var lng = feature.geometry.coordinates[0];
|
||||
var intensity = feature.properties.count;
|
||||
var array = chart.dataToHeatArray(mapData, max);
|
||||
expect(array[i][0]).to.be(lat);
|
||||
expect(array[i][1]).to.be(lng);
|
||||
expect(array[i][2]).to.be(intensity);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return an array item with lat, lng, normalized metric for each feature', function () {
|
||||
vis.handler.charts.forEach(function (chart) {
|
||||
chart._attr.heatNormalizeData = true;
|
||||
var lat = feature.geometry.coordinates[1];
|
||||
var lng = feature.geometry.coordinates[0];
|
||||
var intensity = parseInt(feature.properties.count / max * 100);
|
||||
var array = chart.dataToHeatArray(mapData, max);
|
||||
expect(array[i][0]).to.be(lat);
|
||||
expect(array[i][1]).to.be(lng);
|
||||
expect(array[i][2]).to.be(intensity);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('applyShadingStyle method', function () {
|
||||
it('should return an object', function () {
|
||||
vis.handler.charts.forEach(function (chart) {
|
||||
expect(chart.applyShadingStyle(feature, min, max)).to.be.an(Object);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('showTooltip method', function () {
|
||||
it('should create a .leaflet-popup-kibana div for the tooltip', function () {
|
||||
vis.handler.charts.forEach(function (chart) {
|
||||
chart.tooltipFormatter = function (str) {
|
||||
return '<div class="popup-stub"></div>';
|
||||
};
|
||||
var layerIds = _.keys(map._layers);
|
||||
var id = layerIds[_.random(1, layerIds.length - 1)]; // layer 0 is tileLayer
|
||||
map._layers[id].fire('mouseover');
|
||||
expect($('.popup-stub', vis.el).length).to.be(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('tooltipProximity method', function () {
|
||||
it('should return true if feature is close enough to event latlng to display tooltip', function () {
|
||||
vis.handler.charts.forEach(function (chart) {
|
||||
expect(chart.tooltipProximity(point, zoom, feature, map)).to.be(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return false if feature is not close enough to event latlng to display tooltip', function () {
|
||||
vis.handler.charts.forEach(function (chart) {
|
||||
var point = L.latLng(90, -180);
|
||||
expect(chart.tooltipProximity(point, zoom, feature, map)).to.be(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('nearestFeature method', function () {
|
||||
it('should return an object', function () {
|
||||
vis.handler.charts.forEach(function (chart) {
|
||||
expect(chart.nearestFeature(point, mapData)).to.be.an(Object);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return a geoJson feature', function () {
|
||||
vis.handler.charts.forEach(function (chart) {
|
||||
expect(chart.nearestFeature(point, mapData).type).to.be('Feature');
|
||||
});
|
||||
});
|
||||
|
||||
it('should return the geoJson feature with same latlng as point', function () {
|
||||
vis.handler.charts.forEach(function (chart) {
|
||||
expect(chart.nearestFeature(point, mapData)).to.be(feature);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('addLatLng method', function () {
|
||||
it('should add object to properties of each feature', function () {
|
||||
vis.handler.charts.forEach(function (chart) {
|
||||
expect(feature.properties.latLng).to.be.an(Object);
|
||||
});
|
||||
});
|
||||
|
||||
it('should add latLng with lat to properties of each feature', function () {
|
||||
vis.handler.charts.forEach(function (chart) {
|
||||
var lat = feature.geometry.coordinates[1];
|
||||
expect(feature.properties.latLng.lat).to.be(lat);
|
||||
});
|
||||
});
|
||||
|
||||
it('should add latLng with lng to properties of each feature', function () {
|
||||
vis.handler.charts.forEach(function (chart) {
|
||||
var lng = feature.geometry.coordinates[0];
|
||||
expect(feature.properties.latLng.lng).to.be(lng);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue