mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
Merge branch 'master' of github.com:elastic/kibana into apps/home
This commit is contained in:
commit
3d82d404cb
28 changed files with 2172 additions and 1552 deletions
|
@ -16,12 +16,12 @@ define(function (require) {
|
|||
defaults: {
|
||||
mapType: 'Scaled Circle Markers',
|
||||
isDesaturated: true,
|
||||
addTooltip: true,
|
||||
heatMaxZoom: 16,
|
||||
heatMinOpacity: 0.1,
|
||||
heatRadius: 25,
|
||||
heatBlur: 15,
|
||||
heatNormalizeData: true,
|
||||
addTooltip: true
|
||||
},
|
||||
mapTypes: ['Scaled Circle Markers', 'Shaded Circle Markers', 'Shaded Geohash Grid', 'Heatmap'],
|
||||
canDesaturate: !!supports.cssFilters,
|
||||
|
@ -84,7 +84,8 @@ define(function (require) {
|
|||
18: 12
|
||||
};
|
||||
|
||||
agg.params.precision = Math.min(zoomPrecision[event.zoom], config.get('visualization:tileMap:maxPrecision'));
|
||||
var precision = config.get('visualization:tileMap:maxPrecision');
|
||||
agg.params.precision = Math.min(zoomPrecision[event.zoom], precision);
|
||||
|
||||
courier.fetch();
|
||||
}
|
||||
|
|
|
@ -27,6 +27,12 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<form role="form">
|
||||
<input aria-label="Filter" ng-model="fieldFilter" class="form-control span12" type="text" placeholder="Filter" />
|
||||
</form>
|
||||
|
||||
<br />
|
||||
|
||||
<ul class="nav nav-tabs">
|
||||
<li class="kbn-settings-tab" ng-class="{ active: state.tab === fieldType.index }" ng-repeat="fieldType in fieldTypes">
|
||||
<a ng-click="changeTab(fieldType)">
|
||||
|
|
|
@ -3,3 +3,5 @@
|
|||
rows="rows"
|
||||
per-page="perPage">
|
||||
</paginated-table>
|
||||
|
||||
<p class="text-center default-message" ng-if="rows.length === 0">No matching fields found.</p>
|
||||
|
|
|
@ -3,12 +3,13 @@ define(function (require) {
|
|||
require('components/paginated_table/paginated_table');
|
||||
|
||||
require('modules').get('apps/settings')
|
||||
.directive('indexedFields', function () {
|
||||
.directive('indexedFields', function ($filter) {
|
||||
var yesTemplate = '<i class="fa fa-check" aria-label="yes"></i>';
|
||||
var noTemplate = '';
|
||||
var nameHtml = require('plugins/kibana/settings/sections/indices/_field_name.html');
|
||||
var typeHtml = require('plugins/kibana/settings/sections/indices/_field_type.html');
|
||||
var controlsHtml = require('plugins/kibana/settings/sections/indices/_field_controls.html');
|
||||
var filter = $filter('filter');
|
||||
|
||||
return {
|
||||
restrict: 'E',
|
||||
|
@ -26,11 +27,16 @@ define(function (require) {
|
|||
{ title: 'controls', sortable: false }
|
||||
];
|
||||
|
||||
$scope.$watchCollection('indexPattern.fields', function () {
|
||||
$scope.$watchMulti(['[]indexPattern.fields', 'fieldFilter'], refreshRows);
|
||||
|
||||
function refreshRows() {
|
||||
// clear and destroy row scopes
|
||||
_.invoke(rowScopes.splice(0), '$destroy');
|
||||
|
||||
$scope.rows = $scope.indexPattern.getNonScriptedFields().map(function (field) {
|
||||
var fields = filter($scope.indexPattern.getNonScriptedFields(), $scope.fieldFilter);
|
||||
_.find($scope.fieldTypes, {index: 'indexedFields'}).count = fields.length; // Update the tab count
|
||||
|
||||
$scope.rows = fields.map(function (field) {
|
||||
var childScope = _.assign($scope.$new(), { field: field });
|
||||
rowScopes.push(childScope);
|
||||
|
||||
|
@ -60,7 +66,7 @@ define(function (require) {
|
|||
}
|
||||
];
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
|
|
@ -17,4 +17,4 @@
|
|||
per-page="perPage">
|
||||
</paginated-table>
|
||||
|
||||
<div ng-if="rows.length === 0">No scripted fields</div>
|
||||
<p class="text-center" ng-if="rows.length === 0">No matching scripted fields found.</p>
|
||||
|
|
|
@ -3,9 +3,11 @@ define(function (require) {
|
|||
require('components/paginated_table/paginated_table');
|
||||
|
||||
require('modules').get('apps/settings')
|
||||
.directive('scriptedFields', function (kbnUrl, Notifier) {
|
||||
.directive('scriptedFields', function (kbnUrl, Notifier, $filter) {
|
||||
var rowScopes = []; // track row scopes, so they can be destroyed as needed
|
||||
var popularityHtml = require('plugins/kibana/settings/sections/indices/_field_popularity.html');
|
||||
var controlsHtml = require('plugins/kibana/settings/sections/indices/_field_controls.html');
|
||||
var filter = $filter('filter');
|
||||
|
||||
var notify = new Notifier();
|
||||
|
||||
|
@ -27,11 +29,16 @@ define(function (require) {
|
|||
{ title: 'controls', sortable: false }
|
||||
];
|
||||
|
||||
$scope.$watch('indexPattern.fields', function () {
|
||||
$scope.$watchMulti(['[]indexPattern.fields', 'fieldFilter'], refreshRows);
|
||||
|
||||
function refreshRows() {
|
||||
_.invoke(rowScopes, '$destroy');
|
||||
rowScopes.length = 0;
|
||||
|
||||
$scope.rows = $scope.indexPattern.getScriptedFields().map(function (field) {
|
||||
var fields = filter($scope.indexPattern.getScriptedFields(), $scope.fieldFilter);
|
||||
_.find($scope.fieldTypes, {index: 'scriptedFields'}).count = fields.length; // Update the tab count
|
||||
|
||||
$scope.rows = fields.map(function (field) {
|
||||
var rowScope = $scope.$new();
|
||||
rowScope.field = field;
|
||||
rowScopes.push(rowScope);
|
||||
|
@ -46,7 +53,7 @@ define(function (require) {
|
|||
}
|
||||
];
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
$scope.addDateScripts = function () {
|
||||
var conflictFields = [];
|
||||
|
|
|
@ -42,13 +42,13 @@ define(function (require) {
|
|||
$scope.services = _.sortBy(data, 'title');
|
||||
var tab = $scope.services[0];
|
||||
if ($state.tab) tab = _.find($scope.services, {title: $state.tab});
|
||||
$scope.changeTab(tab);
|
||||
|
||||
$scope.$watch('state.tab', function (tab) {
|
||||
if (!tab) $scope.changeTab($scope.services[0]);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
$scope.$watch('state.tab', function (tab) {
|
||||
if (!tab) $scope.changeTab($scope.services[0]);
|
||||
});
|
||||
|
||||
$scope.toggleAll = function () {
|
||||
if ($scope.selectedItems.length === $scope.currentTab.data.length) {
|
||||
|
|
|
@ -177,6 +177,10 @@ kbn-settings-indices {
|
|||
margin: 5px 0;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
p.text-center {
|
||||
padding-top: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -16,30 +16,35 @@ define(function (require) {
|
|||
var geohash = unwrap(row[geoI]);
|
||||
if (!geohash) return;
|
||||
|
||||
// fetch latLn of northwest and southeast corners, and center point
|
||||
var location = decodeGeoHash(geohash);
|
||||
var center = [
|
||||
location.longitude[2],
|
||||
location.latitude[2]
|
||||
|
||||
var centerLatLng = [
|
||||
location.latitude[2],
|
||||
location.longitude[2]
|
||||
];
|
||||
|
||||
// order is nw, ne, se, sw
|
||||
var rectangle = [
|
||||
[location.longitude[0], location.latitude[0]],
|
||||
[location.longitude[1], location.latitude[0]],
|
||||
[location.longitude[1], location.latitude[1]],
|
||||
[location.longitude[0], location.latitude[1]]
|
||||
[location.latitude[0], location.longitude[0]],
|
||||
[location.latitude[0], location.longitude[1]],
|
||||
[location.latitude[1], location.longitude[1]],
|
||||
[location.latitude[1], location.longitude[0]],
|
||||
];
|
||||
|
||||
// geoJson coords use LngLat, so we reverse the centerLatLng
|
||||
// See here for details: http://geojson.org/geojson-spec.html#positions
|
||||
features.push({
|
||||
type: 'Feature',
|
||||
geometry: {
|
||||
type: 'Point',
|
||||
coordinates: center
|
||||
coordinates: centerLatLng.slice(0).reverse()
|
||||
},
|
||||
properties: {
|
||||
geohash: geohash,
|
||||
value: unwrap(row[metricI]),
|
||||
aggConfigResult: getAcr(row[metricI]),
|
||||
center: center,
|
||||
center: centerLatLng,
|
||||
rectangle: rectangle
|
||||
}
|
||||
});
|
||||
|
|
|
@ -10,10 +10,18 @@ kbn-agg-table-group {
|
|||
flex: 1 1 auto;
|
||||
flex-direction: column;
|
||||
|
||||
&-paginated-table {
|
||||
&-paginated {
|
||||
flex: 1 1 auto;
|
||||
overflow: auto;
|
||||
|
||||
tr:hover td {
|
||||
background-color: lighten(@gray-lighter, 4%);
|
||||
}
|
||||
|
||||
.cell-hover:hover {
|
||||
background-color: @gray-lighter;
|
||||
}
|
||||
|
||||
th i.fa-sort {
|
||||
color: @gray-light;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
define(function (require) {
|
||||
return function SearchLooperService(Private, Promise, Notifier) {
|
||||
return function SearchLooperService(Private, Promise, Notifier, $rootScope) {
|
||||
var fetch = Private(require('components/courier/fetch/fetch'));
|
||||
var searchStrategy = Private(require('components/courier/fetch/strategy/search'));
|
||||
var requestQueue = Private(require('components/courier/_request_queue'));
|
||||
|
@ -12,6 +12,7 @@ define(function (require) {
|
|||
* @type {Looper}
|
||||
*/
|
||||
var searchLooper = new Looper(null, function () {
|
||||
$rootScope.$broadcast('courier:searchRefresh');
|
||||
return fetch.these(
|
||||
requestQueue.getInactive(searchStrategy)
|
||||
);
|
||||
|
|
|
@ -243,6 +243,23 @@ define(function (require) {
|
|||
return visData;
|
||||
};
|
||||
|
||||
/**
|
||||
* get min and max for all cols, rows of data
|
||||
*
|
||||
* @method getMaxMin
|
||||
* @return {Object}
|
||||
*/
|
||||
Data.prototype.getGeoExtents = function () {
|
||||
var visData = this.getVisData();
|
||||
|
||||
return _.reduce(_.pluck(visData, 'geoJson.properties'), function (minMax, props) {
|
||||
return {
|
||||
min: Math.min(props.min, minMax.min),
|
||||
max: Math.max(props.max, minMax.max)
|
||||
};
|
||||
}, { min: Infinity, max: -Infinity });
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns array of chart data objects for pie data objects
|
||||
*
|
||||
|
|
|
@ -27,7 +27,7 @@ define(function (require) {
|
|||
|
||||
var events = this.events = new Dispatch(handler);
|
||||
|
||||
if (handler._attr.addTooltip) {
|
||||
if (_.get(this.handler, '_attr.addTooltip')) {
|
||||
var $el = this.handler.el;
|
||||
var formatter = this.handler.data.get('tooltipFormatter');
|
||||
|
||||
|
@ -35,7 +35,7 @@ define(function (require) {
|
|||
this.tooltip = new Tooltip('chart', $el, formatter, events);
|
||||
}
|
||||
|
||||
this._attr = _.defaults(handler._attr || {}, {});
|
||||
this._attr = _.defaults(this.handler._attr || {}, {});
|
||||
this._addIdentifier = _.bind(this._addIdentifier, this);
|
||||
}
|
||||
|
||||
|
|
303
src/ui/components/vislib/visualizations/_map.js
Normal file
303
src/ui/components/vislib/visualizations/_map.js
Normal file
|
@ -0,0 +1,303 @@
|
|||
define(function (require) {
|
||||
return function MapFactory(Private) {
|
||||
var _ = require('lodash');
|
||||
var $ = require('jquery');
|
||||
var L = require('leaflet');
|
||||
require('leaflet-draw');
|
||||
|
||||
var defaultMapZoom = 2;
|
||||
var defaultMapCenter = [15, 5];
|
||||
var defaultMarkerType = 'Scaled Circle Markers';
|
||||
|
||||
var mapTiles = {
|
||||
url: 'https://otile{s}-s.mqcdn.com/tiles/1.0.0/map/{z}/{x}/{y}.jpeg',
|
||||
options: {
|
||||
attribution: 'Tiles by <a href="http://www.mapquest.com/">MapQuest</a> — ' +
|
||||
'Map data © <a href="http://openstreetmap.org">OpenStreetMap</a> contributors, ' +
|
||||
'<a href="http://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>',
|
||||
subdomains: '1234'
|
||||
}
|
||||
};
|
||||
|
||||
var markerTypes = {
|
||||
'Scaled Circle Markers': Private(require('components/vislib/visualizations/marker_types/scaled_circles')),
|
||||
'Shaded Circle Markers': Private(require('components/vislib/visualizations/marker_types/shaded_circles')),
|
||||
'Shaded Geohash Grid': Private(require('components/vislib/visualizations/marker_types/geohash_grid')),
|
||||
'Heatmap': Private(require('components/vislib/visualizations/marker_types/heatmap')),
|
||||
};
|
||||
|
||||
/**
|
||||
* Tile Map Maps
|
||||
*
|
||||
* @class Map
|
||||
* @constructor
|
||||
* @param container {HTML Element} Element to render map into
|
||||
* @param chartData {Object} Elasticsearch query results for this map
|
||||
* @param params {Object} Parameters used to build a map
|
||||
*/
|
||||
function Map(container, chartData, params) {
|
||||
this._container = $(container).get(0);
|
||||
this._chartData = chartData;
|
||||
|
||||
// keep a reference to all of the optional params
|
||||
this._events = _.get(params, 'events');
|
||||
this._markerType = markerTypes[params.markerType] ? params.markerType : defaultMarkerType;
|
||||
this._valueFormatter = params.valueFormatter || _.identity;
|
||||
this._tooltipFormatter = params.tooltipFormatter || _.identity;
|
||||
this._geoJson = _.get(this._chartData, 'geoJson');
|
||||
this._attr = params.attr || {};
|
||||
|
||||
var mapOptions = {
|
||||
minZoom: 1,
|
||||
maxZoom: 18,
|
||||
noWrap: true,
|
||||
maxBounds: L.latLngBounds([-90, -220], [90, 220]),
|
||||
scrollWheelZoom: false,
|
||||
fadeAnimation: false,
|
||||
};
|
||||
|
||||
this._createMap(mapOptions);
|
||||
}
|
||||
|
||||
Map.prototype.addBoundingControl = function () {
|
||||
if (this._boundingControl) return;
|
||||
|
||||
var self = this;
|
||||
var drawOptions = { draw: {} };
|
||||
|
||||
_.each(['polyline', 'polygon', 'circle', 'marker', 'rectangle'], function (drawShape) {
|
||||
if (self._events && !self._events.listenerCount(drawShape)) {
|
||||
drawOptions.draw[drawShape] = false;
|
||||
} else {
|
||||
drawOptions.draw[drawShape] = {
|
||||
shapeOptions: {
|
||||
stroke: false,
|
||||
color: '#000'
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
this._boundingControl = new L.Control.Draw(drawOptions);
|
||||
this.map.addControl(this._boundingControl);
|
||||
};
|
||||
|
||||
Map.prototype.addFitControl = function () {
|
||||
if (this._fitControl) return;
|
||||
|
||||
var self = this;
|
||||
var fitContainer = L.DomUtil.create('div', 'leaflet-control leaflet-bar leaflet-control-fit');
|
||||
|
||||
// Add button to fit container to points
|
||||
var FitControl = L.Control.extend({
|
||||
options: {
|
||||
position: 'topleft'
|
||||
},
|
||||
onAdd: function (map) {
|
||||
$(fitContainer).html('<a class="fa fa-crop" href="#" title="Fit Data Bounds"></a>')
|
||||
.on('click', function (e) {
|
||||
e.preventDefault();
|
||||
self._fitBounds();
|
||||
});
|
||||
|
||||
return fitContainer;
|
||||
},
|
||||
onRemove: function (map) {
|
||||
$(fitContainer).off('click');
|
||||
}
|
||||
});
|
||||
|
||||
this._fitControl = new FitControl();
|
||||
this.map.addControl(this._fitControl);
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds label div to each map when data is split
|
||||
*
|
||||
* @method addTitle
|
||||
* @param mapLabel {String}
|
||||
* @return {undefined}
|
||||
*/
|
||||
Map.prototype.addTitle = function (mapLabel) {
|
||||
if (this._label) return;
|
||||
|
||||
var label = this._label = L.control();
|
||||
|
||||
label.onAdd = function () {
|
||||
this._div = L.DomUtil.create('div', 'tilemap-info tilemap-label');
|
||||
this.update();
|
||||
return this._div;
|
||||
};
|
||||
label.update = function () {
|
||||
this._div.innerHTML = '<h2>' + _.escape(mapLabel) + '</h2>';
|
||||
};
|
||||
|
||||
// label.addTo(this.map);
|
||||
this.map.addControl(label);
|
||||
};
|
||||
|
||||
/**
|
||||
* remove css class for desat filters on map tiles
|
||||
*
|
||||
* @method saturateTiles
|
||||
* @return undefined
|
||||
*/
|
||||
Map.prototype.saturateTiles = function () {
|
||||
if (!this._attr.isDesaturated) {
|
||||
$('img.leaflet-tile-loaded').addClass('filters-off');
|
||||
}
|
||||
};
|
||||
|
||||
Map.prototype.updateSize = function () {
|
||||
this.map.invalidateSize({
|
||||
debounceMoveend: true
|
||||
});
|
||||
};
|
||||
|
||||
Map.prototype.destroy = function () {
|
||||
if (this._label) this._label.removeFrom(this.map);
|
||||
if (this._fitControl) this._fitControl.removeFrom(this.map);
|
||||
if (this._boundingControl) this._boundingControl.removeFrom(this.map);
|
||||
if (this._markers) this._markers.destroy();
|
||||
this.map.remove();
|
||||
this.map = undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Switch type of data overlay for map:
|
||||
* creates featurelayer from mapData (geoJson)
|
||||
*
|
||||
* @method _addMarkers
|
||||
*/
|
||||
Map.prototype._addMarkers = function () {
|
||||
if (!this._geoJson) return;
|
||||
if (this._markers) this._markers.destroy();
|
||||
|
||||
this._markers = this._createMarkers({
|
||||
tooltipFormatter: this._tooltipFormatter,
|
||||
valueFormatter: this._valueFormatter,
|
||||
attr: this._attr
|
||||
});
|
||||
|
||||
if (this._geoJson.features.length > 1) {
|
||||
this._markers.addLegend();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Create the marker instance using the given options
|
||||
*
|
||||
* @method _createMarkers
|
||||
* @param options {Object} options to give to marker class
|
||||
* @return {Object} marker layer
|
||||
*/
|
||||
Map.prototype._createMarkers = function (options) {
|
||||
var MarkerType = markerTypes[this._markerType];
|
||||
return new MarkerType(this.map, this._geoJson, options);
|
||||
};
|
||||
|
||||
Map.prototype._attachEvents = function () {
|
||||
var self = this;
|
||||
var saturateTiles = self.saturateTiles.bind(self);
|
||||
|
||||
this._tileLayer.on('tileload', saturateTiles);
|
||||
|
||||
this.map.on('unload', function () {
|
||||
self._tileLayer.off('tileload', saturateTiles);
|
||||
});
|
||||
|
||||
this.map.on('moveend', function setZoomCenter(ev) {
|
||||
// update internal center and zoom references
|
||||
self._mapCenter = self.map.getCenter();
|
||||
self._mapZoom = self.map.getZoom();
|
||||
self._addMarkers();
|
||||
|
||||
if (!self._events) return;
|
||||
|
||||
self._events.emit('mapMoveEnd', {
|
||||
chart: self._chartData,
|
||||
map: self.map,
|
||||
center: self._mapCenter,
|
||||
zoom: self._mapZoom,
|
||||
});
|
||||
});
|
||||
|
||||
this.map.on('draw:created', function (e) {
|
||||
var drawType = e.layerType;
|
||||
if (!self._events || !self._events.listenerCount(drawType)) return;
|
||||
|
||||
// TODO: Different drawTypes need differ info. Need a switch on the object creation
|
||||
var bounds = e.layer.getBounds();
|
||||
|
||||
self._events.emit(drawType, {
|
||||
e: e,
|
||||
chart: self._chartData,
|
||||
bounds: {
|
||||
top_left: {
|
||||
lat: bounds.getNorthWest().lat,
|
||||
lon: bounds.getNorthWest().lng
|
||||
},
|
||||
bottom_right: {
|
||||
lat: bounds.getSouthEast().lat,
|
||||
lon: bounds.getSouthEast().lng
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
this.map.on('zoomend', function () {
|
||||
self._mapZoom = self.map.getZoom();
|
||||
if (!self._events) return;
|
||||
|
||||
self._events.emit('mapZoomEnd', {
|
||||
chart: self._chartData,
|
||||
map: self.map,
|
||||
zoom: self._mapZoom,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
Map.prototype._createMap = function (mapOptions) {
|
||||
if (this.map) this.destroy();
|
||||
|
||||
// get center and zoom from mapdata, or use defaults
|
||||
this._mapCenter = _.get(this._geoJson, 'properties.center') || defaultMapCenter;
|
||||
this._mapZoom = _.get(this._geoJson, 'properties.zoom') || defaultMapZoom;
|
||||
|
||||
// add map tiles layer, using the mapTiles object settings
|
||||
this._tileLayer = L.tileLayer(mapTiles.url, mapTiles.options);
|
||||
|
||||
// append tile layers, center and zoom to the map options
|
||||
mapOptions.layers = this._tileLayer;
|
||||
mapOptions.center = this._mapCenter;
|
||||
mapOptions.zoom = this._mapZoom;
|
||||
|
||||
this.map = L.map(this._container, mapOptions);
|
||||
this._attachEvents();
|
||||
this._addMarkers();
|
||||
};
|
||||
|
||||
/**
|
||||
* zoom map to fit all features in featureLayer
|
||||
*
|
||||
* @method _fitBounds
|
||||
* @param map {Leaflet Object}
|
||||
* @return {boolean}
|
||||
*/
|
||||
Map.prototype._fitBounds = function () {
|
||||
this.map.fitBounds(this._getDataRectangles());
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the Rectangles representing the geohash grid
|
||||
*
|
||||
* @return {LatLngRectangles[]}
|
||||
*/
|
||||
Map.prototype._getDataRectangles = function () {
|
||||
if (!this._geoJson) return [];
|
||||
return _.pluck(this._geoJson.features, 'properties.rectangle');
|
||||
};
|
||||
|
||||
return Map;
|
||||
};
|
||||
});
|
|
@ -0,0 +1,265 @@
|
|||
define(function (require) {
|
||||
return function MarkerFactory(d3) {
|
||||
var _ = require('lodash');
|
||||
var $ = require('jquery');
|
||||
var L = require('leaflet');
|
||||
|
||||
/**
|
||||
* Base map marker overlay, all other markers inherit from this class
|
||||
*
|
||||
* @param map {Leaflet Object}
|
||||
* @param geoJson {geoJson Object}
|
||||
* @param params {Object}
|
||||
*/
|
||||
function BaseMarker(map, geoJson, params) {
|
||||
this.map = map;
|
||||
this.geoJson = geoJson;
|
||||
this.popups = [];
|
||||
|
||||
this._tooltipFormatter = params.tooltipFormatter || _.identity;
|
||||
this._valueFormatter = params.valueFormatter || _.identity;
|
||||
this._attr = params.attr || {};
|
||||
|
||||
// set up the default legend colors
|
||||
this.quantizeLegendColors();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds legend div to each map when data is split
|
||||
* uses d3 scale from BaseMarker.prototype.quantizeLegendColors
|
||||
*
|
||||
* @method addLegend
|
||||
* @return {undefined}
|
||||
*/
|
||||
BaseMarker.prototype.addLegend = function () {
|
||||
// ensure we only ever create 1 legend
|
||||
if (this._legend) return;
|
||||
|
||||
var self = this;
|
||||
|
||||
// create the legend control, keep a reference
|
||||
self._legend = L.control({position: 'bottomright'});
|
||||
|
||||
self._legend.onAdd = function () {
|
||||
// creates all the neccessary DOM elements for the control, adds listeners
|
||||
// on relevant map events, and returns the element containing the control
|
||||
var $div = $('<div>').addClass('tilemap-legend');
|
||||
|
||||
_.each(self._legendColors, function (color, i) {
|
||||
var labelText = self._legendQuantizer
|
||||
.invertExtent(color)
|
||||
.map(self._valueFormatter)
|
||||
.join(' – ');
|
||||
|
||||
var label = $('<div>').text(labelText);
|
||||
|
||||
var icon = $('<i>').css({
|
||||
background: color,
|
||||
'border-color': self.darkerColor(color)
|
||||
});
|
||||
|
||||
label.append(icon);
|
||||
$div.append(label);
|
||||
});
|
||||
|
||||
return $div.get(0);
|
||||
};
|
||||
|
||||
self._legend.addTo(self.map);
|
||||
};
|
||||
|
||||
/**
|
||||
* Apply style with shading to feature
|
||||
*
|
||||
* @method applyShadingStyle
|
||||
* @param value {Object}
|
||||
* @return {Object}
|
||||
*/
|
||||
BaseMarker.prototype.applyShadingStyle = function (value) {
|
||||
var color = this._legendQuantizer(value);
|
||||
|
||||
return {
|
||||
fillColor: color,
|
||||
color: this.darkerColor(color),
|
||||
weight: 1.5,
|
||||
opacity: 1,
|
||||
fillOpacity: 0.75
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Binds popup and events to each feature on map
|
||||
*
|
||||
* @method bindPopup
|
||||
* @param feature {Object}
|
||||
* @param layer {Object}
|
||||
* return {undefined}
|
||||
*/
|
||||
BaseMarker.prototype.bindPopup = function (feature, layer) {
|
||||
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();
|
||||
}
|
||||
self._showTooltip(feature);
|
||||
},
|
||||
mouseout: function (e) {
|
||||
self._hidePopup();
|
||||
}
|
||||
});
|
||||
|
||||
self.popups.push(popup);
|
||||
};
|
||||
|
||||
/**
|
||||
* d3 method returns a darker hex color,
|
||||
* used for marker stroke color
|
||||
*
|
||||
* @method darkerColor
|
||||
* @param color {String} hex color
|
||||
* @param amount? {Number} amount to darken by
|
||||
* @return {String} hex color
|
||||
*/
|
||||
BaseMarker.prototype.darkerColor = function (color, amount) {
|
||||
amount = amount || 1.3;
|
||||
return d3.hcl(color).darker(amount).toString();
|
||||
};
|
||||
|
||||
BaseMarker.prototype.destroy = function () {
|
||||
var self = this;
|
||||
|
||||
// remove popups
|
||||
self.popups = self.popups.filter(function (popup) {
|
||||
popup.off('mouseover').off('mouseout');
|
||||
});
|
||||
|
||||
if (self._legend) {
|
||||
self.map.removeControl(self._legend);
|
||||
self._legend = undefined;
|
||||
}
|
||||
|
||||
// remove marker layer from map
|
||||
if (self._markerGroup) {
|
||||
self.map.removeLayer(self._markerGroup);
|
||||
self._markerGroup = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
BaseMarker.prototype._addToMap = function () {
|
||||
this.map.addLayer(this._markerGroup);
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates leaflet marker group, passing options to L.geoJson
|
||||
*
|
||||
* @method _createMarkerGroup
|
||||
* @param options {Object} Options to pass to L.geoJson
|
||||
*/
|
||||
BaseMarker.prototype._createMarkerGroup = function (options) {
|
||||
var self = this;
|
||||
var defaultOptions = {
|
||||
onEachFeature: function (feature, layer) {
|
||||
self.bindPopup(feature, layer);
|
||||
},
|
||||
style: function (feature) {
|
||||
var value = _.get(feature, 'properties.value');
|
||||
return self.applyShadingStyle(value);
|
||||
},
|
||||
filter: self._filterToMapBounds()
|
||||
};
|
||||
|
||||
this._markerGroup = L.geoJson(this.geoJson, _.defaults(defaultOptions, options));
|
||||
this._addToMap();
|
||||
};
|
||||
|
||||
/**
|
||||
* return whether feature is within map bounds
|
||||
*
|
||||
* @method _filterToMapBounds
|
||||
* @param map {Leaflet Object}
|
||||
* @return {boolean}
|
||||
*/
|
||||
BaseMarker.prototype._filterToMapBounds = function () {
|
||||
var self = this;
|
||||
return function (feature) {
|
||||
var mapBounds = self.map.getBounds();
|
||||
var bucketRectBounds = _.get(feature, 'properties.rectangle');
|
||||
return mapBounds.intersects(bucketRectBounds);
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if event latlng is within bounds of mapData
|
||||
* features and shows tooltip for that feature
|
||||
*
|
||||
* @method _showTooltip
|
||||
* @param feature {LeafletFeature}
|
||||
* @param latLng? {Leaflet latLng}
|
||||
* @return undefined
|
||||
*/
|
||||
BaseMarker.prototype._showTooltip = function (feature, latLng) {
|
||||
if (!this.map) return;
|
||||
var lat = _.get(feature, 'geometry.coordinates.1');
|
||||
var lng = _.get(feature, 'geometry.coordinates.0');
|
||||
latLng = latLng || L.latLng(lat, lng);
|
||||
|
||||
var content = this._tooltipFormatter(feature);
|
||||
|
||||
if (!content) return;
|
||||
this._createTooltip(content, latLng);
|
||||
};
|
||||
|
||||
BaseMarker.prototype._createTooltip = function (content, latLng) {
|
||||
L.popup({autoPan: false})
|
||||
.setLatLng(latLng)
|
||||
.setContent(content)
|
||||
.openOn(this.map);
|
||||
};
|
||||
|
||||
/**
|
||||
* Closes the tooltip on the map
|
||||
*
|
||||
* @method _hidePopup
|
||||
* @return undefined
|
||||
*/
|
||||
BaseMarker.prototype._hidePopup = function () {
|
||||
if (!this.map) return;
|
||||
|
||||
this.map.closePopup();
|
||||
};
|
||||
|
||||
/**
|
||||
* d3 quantize scale returns a hex color, used for marker fill color
|
||||
*
|
||||
* @method quantizeLegendColors
|
||||
* return {undefined}
|
||||
*/
|
||||
BaseMarker.prototype.quantizeLegendColors = function () {
|
||||
var min = _.get(this.geoJson, 'properties.allmin', 0);
|
||||
var max = _.get(this.geoJson, 'properties.allmax', 1);
|
||||
var quantizeDomain = (min !== max) ? [min, max] : d3.scale.quantize().domain();
|
||||
|
||||
var reds1 = ['#ff6128'];
|
||||
var reds3 = ['#fecc5c', '#fd8d3c', '#e31a1c'];
|
||||
var reds5 = ['#fed976', '#feb24c', '#fd8d3c', '#f03b20', '#bd0026'];
|
||||
var bottomCutoff = 2;
|
||||
var middleCutoff = 24;
|
||||
|
||||
if (max - min <= bottomCutoff) {
|
||||
this._legendColors = reds1;
|
||||
} else if (max - min <= middleCutoff) {
|
||||
this._legendColors = reds3;
|
||||
} else {
|
||||
this._legendColors = reds5;
|
||||
}
|
||||
|
||||
this._legendQuantizer = d3.scale.quantize().domain(quantizeDomain).range(this._legendColors);
|
||||
};
|
||||
|
||||
return BaseMarker;
|
||||
};
|
||||
});
|
|
@ -0,0 +1,40 @@
|
|||
define(function (require) {
|
||||
return function GeohashGridMarkerFactory(Private) {
|
||||
var _ = require('lodash');
|
||||
var L = require('leaflet');
|
||||
|
||||
var BaseMarker = Private(require('components/vislib/visualizations/marker_types/base_marker'));
|
||||
|
||||
/**
|
||||
* Map overlay: rectangles that show the geohash grid bounds
|
||||
*
|
||||
* @param map {Leaflet Object}
|
||||
* @param geoJson {geoJson Object}
|
||||
* @param params {Object}
|
||||
*/
|
||||
_.class(GeohashGridMarker).inherits(BaseMarker);
|
||||
function GeohashGridMarker(map, geoJson, params) {
|
||||
var self = this;
|
||||
GeohashGridMarker.Super.apply(this, arguments);
|
||||
|
||||
// super min and max from all chart data
|
||||
var min = this.geoJson.properties.allmin;
|
||||
var max = this.geoJson.properties.allmax;
|
||||
|
||||
this._createMarkerGroup({
|
||||
pointToLayer: function (feature, latlng) {
|
||||
var geohashRect = feature.properties.rectangle;
|
||||
// get bounds from northEast[3] and southWest[1]
|
||||
// corners in geohash rectangle
|
||||
var corners = [
|
||||
[geohashRect[3][0], geohashRect[3][1]],
|
||||
[geohashRect[1][0], geohashRect[1][1]]
|
||||
];
|
||||
return L.rectangle(corners);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return GeohashGridMarker;
|
||||
};
|
||||
});
|
211
src/ui/components/vislib/visualizations/marker_types/heatmap.js
Normal file
211
src/ui/components/vislib/visualizations/marker_types/heatmap.js
Normal file
|
@ -0,0 +1,211 @@
|
|||
define(function (require) {
|
||||
return function HeatmapMarkerFactory(Private, d3) {
|
||||
var _ = require('lodash');
|
||||
var L = require('leaflet');
|
||||
require('leaflet-heat');
|
||||
|
||||
var BaseMarker = Private(require('components/vislib/visualizations/marker_types/base_marker'));
|
||||
|
||||
/**
|
||||
* Map overlay: canvas layer with leaflet.heat plugin
|
||||
*
|
||||
* @param map {Leaflet Object}
|
||||
* @param geoJson {geoJson Object}
|
||||
* @param params {Object}
|
||||
*/
|
||||
_.class(HeatmapMarker).inherits(BaseMarker);
|
||||
function HeatmapMarker(map, geoJson, params) {
|
||||
var self = this;
|
||||
this._disableTooltips = false;
|
||||
HeatmapMarker.Super.apply(this, arguments);
|
||||
|
||||
this._createMarkerGroup({
|
||||
radius: +this._attr.heatRadius,
|
||||
blur: +this._attr.heatBlur,
|
||||
maxZoom: +this._attr.heatMaxZoom,
|
||||
minOpacity: +this._attr.heatMinOpacity
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Does nothing, heatmaps don't have a legend
|
||||
*
|
||||
* @method addLegend
|
||||
* @return {undefined}
|
||||
*/
|
||||
HeatmapMarker.prototype.addLegend = _.noop;
|
||||
|
||||
HeatmapMarker.prototype._createMarkerGroup = function (options) {
|
||||
var max = _.get(this.geoJson, 'properties.allmax');
|
||||
var points = this._dataToHeatArray(max);
|
||||
|
||||
this._markerGroup = L.heatLayer(points, options);
|
||||
this._fixTooltips();
|
||||
this._addToMap();
|
||||
};
|
||||
|
||||
HeatmapMarker.prototype._fixTooltips = function () {
|
||||
var self = this;
|
||||
var debouncedMouseMoveLocation = _.debounce(mouseMoveLocation.bind(this), 15, {
|
||||
'leading': true,
|
||||
'trailing': false
|
||||
});
|
||||
|
||||
if (!this._disableTooltips && this._attr.addTooltip) {
|
||||
this.map.on('mousemove', debouncedMouseMoveLocation);
|
||||
this.map.on('mouseout', function () {
|
||||
self.map.closePopup();
|
||||
});
|
||||
this.map.on('mousedown', function () {
|
||||
self._disableTooltips = true;
|
||||
self.map.closePopup();
|
||||
});
|
||||
this.map.on('mouseup', function () {
|
||||
self._disableTooltips = false;
|
||||
});
|
||||
}
|
||||
|
||||
function mouseMoveLocation(e) {
|
||||
var latlng = e.latlng;
|
||||
|
||||
this.map.closePopup();
|
||||
|
||||
// unhighlight all svgs
|
||||
d3.selectAll('path.geohash', this.chartEl).classed('geohash-hover', false);
|
||||
|
||||
if (!this.geoJson.features.length || this._disableTooltips) {
|
||||
return;
|
||||
}
|
||||
|
||||
// find nearest feature to event latlng
|
||||
var feature = this._nearestFeature(latlng);
|
||||
|
||||
// show tooltip if close enough to event latlng
|
||||
if (this._tooltipProximity(latlng, feature)) {
|
||||
this._showTooltip(feature, latlng);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* returns a memoized Leaflet latLng for given geoJson feature
|
||||
*
|
||||
* @method addLatLng
|
||||
* @param feature {geoJson Object}
|
||||
* @return {Leaflet latLng Object}
|
||||
*/
|
||||
HeatmapMarker.prototype._getLatLng = _.memoize(function (feature) {
|
||||
return L.latLng(
|
||||
feature.geometry.coordinates[1],
|
||||
feature.geometry.coordinates[0]
|
||||
);
|
||||
}, function (feature) {
|
||||
// turn coords into a string for the memoize cache
|
||||
return [feature.geometry.coordinates[1], feature.geometry.coordinates[0]].join(',');
|
||||
});
|
||||
|
||||
/**
|
||||
* Finds nearest feature in mapData to event latlng
|
||||
*
|
||||
* @method _nearestFeature
|
||||
* @param latLng {Leaflet latLng}
|
||||
* @return nearestPoint {Leaflet latLng}
|
||||
*/
|
||||
HeatmapMarker.prototype._nearestFeature = function (latLng) {
|
||||
var self = this;
|
||||
var nearest;
|
||||
|
||||
if (latLng.lng < -180 || latLng.lng > 180) {
|
||||
return;
|
||||
}
|
||||
|
||||
_.reduce(this.geoJson.features, function (distance, feature) {
|
||||
var featureLatLng = self._getLatLng(feature);
|
||||
var dist = latLng.distanceTo(featureLatLng);
|
||||
|
||||
if (dist < distance) {
|
||||
nearest = feature;
|
||||
return dist;
|
||||
}
|
||||
|
||||
return distance;
|
||||
}, Infinity);
|
||||
|
||||
return nearest;
|
||||
};
|
||||
|
||||
/**
|
||||
* display tooltip if feature is close enough to event latlng
|
||||
*
|
||||
* @method _tooltipProximity
|
||||
* @param latlng {Leaflet latLng Object}
|
||||
* @param feature {geoJson Object}
|
||||
* @return {Boolean}
|
||||
*/
|
||||
HeatmapMarker.prototype._tooltipProximity = function (latlng, feature) {
|
||||
if (!feature) return;
|
||||
|
||||
var showTip = false;
|
||||
var featureLatLng = this._getLatLng(feature);
|
||||
|
||||
// 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(this.map.getZoom());
|
||||
var distance = latlng.distanceTo(featureLatLng);
|
||||
|
||||
// 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 - featureLatLng.lng);
|
||||
|
||||
if (distance < proximity && lngDif < maxLngDif) {
|
||||
showTip = true;
|
||||
}
|
||||
|
||||
var testScale = d3.scale.pow().exponent(0.2)
|
||||
.domain([1, 18])
|
||||
.range([1500000, 50]);
|
||||
return showTip;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* returns data for data for heat map intensity
|
||||
* if heatNormalizeData attribute is checked/true
|
||||
• normalizes data for heat map intensity
|
||||
*
|
||||
* @method _dataToHeatArray
|
||||
* @param max {Number}
|
||||
* @return {Array}
|
||||
*/
|
||||
HeatmapMarker.prototype._dataToHeatArray = function (max) {
|
||||
var self = this;
|
||||
var mapData = this.geoJson;
|
||||
|
||||
return this.geoJson.features.map(function (feature) {
|
||||
var lat = feature.properties.center[0];
|
||||
var lng = feature.properties.center[1];
|
||||
var heatIntensity;
|
||||
|
||||
if (!self._attr.heatNormalizeData) {
|
||||
// show bucket value on heatmap
|
||||
heatIntensity = feature.properties.value;
|
||||
} else {
|
||||
// show bucket value normalized to max value
|
||||
heatIntensity = parseInt(feature.properties.value / max * 100);
|
||||
}
|
||||
|
||||
return [lat, lng, heatIntensity];
|
||||
});
|
||||
};
|
||||
|
||||
return HeatmapMarker;
|
||||
};
|
||||
});
|
|
@ -0,0 +1,60 @@
|
|||
define(function (require) {
|
||||
return function ScaledCircleMarkerFactory(Private) {
|
||||
var _ = require('lodash');
|
||||
var L = require('leaflet');
|
||||
|
||||
var BaseMarker = Private(require('components/vislib/visualizations/marker_types/base_marker'));
|
||||
|
||||
/**
|
||||
* Map overlay: circle markers that are scaled to illustrate values
|
||||
*
|
||||
* @param map {Leaflet Object}
|
||||
* @param mapData {geoJson Object}
|
||||
* @param params {Object}
|
||||
*/
|
||||
_.class(ScaledCircleMarker).inherits(BaseMarker);
|
||||
function ScaledCircleMarker(map, geoJson, params) {
|
||||
var self = this;
|
||||
ScaledCircleMarker.Super.apply(this, arguments);
|
||||
|
||||
// multiplier to reduce size of all circles
|
||||
var scaleFactor = 0.6;
|
||||
|
||||
this._createMarkerGroup({
|
||||
pointToLayer: function (feature, latlng) {
|
||||
var value = feature.properties.value;
|
||||
var scaledRadius = self._radiusScale(value) * scaleFactor;
|
||||
return L.circleMarker(latlng).setRadius(scaledRadius);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* radiusScale returns a number for scaled circle markers
|
||||
* for relative sizing of markers
|
||||
*
|
||||
* @method _radiusScale
|
||||
* @param value {Number}
|
||||
* @return {Number}
|
||||
*/
|
||||
ScaledCircleMarker.prototype._radiusScale = function (value) {
|
||||
var precisionBiasBase = 5;
|
||||
var precisionBiasNumerator = 200;
|
||||
var zoom = this.map.getZoom();
|
||||
var maxValue = this.geoJson.properties.allmax;
|
||||
var precision = _.max(this.geoJson.features.map(function (feature) {
|
||||
return String(feature.properties.geohash).length;
|
||||
}));
|
||||
|
||||
var pct = Math.abs(value) / Math.abs(maxValue);
|
||||
var zoomRadius = 0.5 * Math.pow(2, zoom);
|
||||
var precisionScale = precisionBiasNumerator / Math.pow(precisionBiasBase, precision);
|
||||
|
||||
// square root value percentage
|
||||
return Math.pow(pct, 0.5) * zoomRadius * precisionScale;
|
||||
};
|
||||
|
||||
|
||||
return ScaledCircleMarker;
|
||||
};
|
||||
});
|
|
@ -0,0 +1,68 @@
|
|||
define(function (require) {
|
||||
return function ShadedCircleMarkerFactory(Private) {
|
||||
var _ = require('lodash');
|
||||
var L = require('leaflet');
|
||||
|
||||
var BaseMarker = Private(require('components/vislib/visualizations/marker_types/base_marker'));
|
||||
|
||||
/**
|
||||
* Map overlay: circle markers that are shaded to illustrate values
|
||||
*
|
||||
* @param map {Leaflet Object}
|
||||
* @param mapData {geoJson Object}
|
||||
* @return {Leaflet object} featureLayer
|
||||
*/
|
||||
_.class(ShadedCircleMarker).inherits(BaseMarker);
|
||||
function ShadedCircleMarker(map, geoJson, params) {
|
||||
var self = this;
|
||||
ShadedCircleMarker.Super.apply(this, arguments);
|
||||
|
||||
// super min and max from all chart data
|
||||
var min = this.geoJson.properties.allmin;
|
||||
var max = this.geoJson.properties.allmax;
|
||||
|
||||
// multiplier to reduce size of all circles
|
||||
var scaleFactor = 0.8;
|
||||
|
||||
this._createMarkerGroup({
|
||||
pointToLayer: function (feature, latlng) {
|
||||
var radius = self._geohashMinDistance(feature) * scaleFactor;
|
||||
return L.circle(latlng, radius);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* _geohashMinDistance returns a min distance in meters for sizing
|
||||
* circle markers to fit within geohash grid rectangle
|
||||
*
|
||||
* @method _geohashMinDistance
|
||||
* @param feature {Object}
|
||||
* @return {Number}
|
||||
*/
|
||||
ShadedCircleMarker.prototype._geohashMinDistance = function (feature) {
|
||||
var centerPoint = _.get(feature, 'properties.center');
|
||||
var geohashRect = _.get(feature, 'properties.rectangle');
|
||||
|
||||
// centerPoint is an array of [lat, lng]
|
||||
// geohashRect is the 4 corners of the geoHash rectangle
|
||||
// an array that starts at the southwest corner and proceeds
|
||||
// clockwise, each value being an array of [lat, lng]
|
||||
|
||||
// center lat and southeast lng
|
||||
var east = L.latLng([centerPoint[0], geohashRect[2][1]]);
|
||||
// southwest lat and center lng
|
||||
var north = L.latLng([geohashRect[3][0], centerPoint[1]]);
|
||||
|
||||
// get latLng of geohash center point
|
||||
var center = L.latLng([centerPoint[0], centerPoint[1]]);
|
||||
|
||||
// get smallest radius at center of geohash grid rectangle
|
||||
var eastRadius = Math.floor(center.distanceTo(east));
|
||||
var northRadius = Math.floor(center.distanceTo(north));
|
||||
return _.min([eastRadius, northRadius]);
|
||||
};
|
||||
|
||||
return ShadedCircleMarker;
|
||||
};
|
||||
});
|
|
@ -1,21 +1,11 @@
|
|||
define(function (require) {
|
||||
return function TileMapFactory(d3, Private, config) {
|
||||
return function TileMapFactory(d3, Private) {
|
||||
var _ = require('lodash');
|
||||
var $ = require('jquery');
|
||||
var L = require('leaflet');
|
||||
require('leaflet-heat');
|
||||
require('leaflet-draw');
|
||||
require('components/vislib/styles/main.less');
|
||||
|
||||
var Chart = Private(require('components/vislib/visualizations/_chart'));
|
||||
var defaultMapZoom = 2;
|
||||
var defaultMapCenter = [15, 5];
|
||||
|
||||
// Convenience function to turn around the LngLat recieved from ES
|
||||
function cloneAndReverse(arr) {
|
||||
var l = arr.length;
|
||||
return arr.map(function (curr, idx) { return arr[l - (idx + 1)]; });
|
||||
}
|
||||
var Map = Private(require('components/vislib/visualizations/_map'));
|
||||
|
||||
/**
|
||||
* Tile Map Visualization: renders maps
|
||||
|
@ -35,696 +25,33 @@ define(function (require) {
|
|||
|
||||
TileMap.Super.apply(this, arguments);
|
||||
|
||||
|
||||
// track the map objects
|
||||
this.maps = [];
|
||||
this.originalConfig = chartData || {};
|
||||
_.assign(this, this.originalConfig);
|
||||
this._chartData = chartData || {};
|
||||
_.assign(this, this._chartData);
|
||||
|
||||
this._attr.mapZoom = _.get(this.geoJson, 'properties.zoom') || defaultMapZoom;
|
||||
this._attr.mapCenter = _.get(this.geoJson, 'properties.center') || defaultMapCenter;
|
||||
|
||||
// add allmin and allmax to geoJson
|
||||
var allMinMax = this.getMinMax(handler.data.data);
|
||||
this.geoJson.properties.allmin = allMinMax.min;
|
||||
this.geoJson.properties.allmax = allMinMax.max;
|
||||
this._appendGeoExtents();
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders tile map
|
||||
* Draws tile map, called on chart render
|
||||
*
|
||||
* @method draw
|
||||
* @return {Function} - function to add a map to a selection
|
||||
*/
|
||||
TileMap.prototype.draw = function () {
|
||||
var self = this;
|
||||
var mapData = this.geoJson;
|
||||
|
||||
// clean up old maps
|
||||
self.destroy();
|
||||
|
||||
// clear maps array
|
||||
self.maps = [];
|
||||
self.popups = [];
|
||||
|
||||
var worldBounds = L.latLngBounds([-90, -220], [90, 220]);
|
||||
|
||||
return function (selection) {
|
||||
selection.each(function () {
|
||||
// add leaflet latLngs to properties for tooltip
|
||||
self.addLatLng(self.geoJson);
|
||||
|
||||
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, ' +
|
||||
'<a href="http://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>',
|
||||
subdomains: '1234'
|
||||
});
|
||||
|
||||
var drawOptions = {draw: {}};
|
||||
_.each(['polyline', 'polygon', 'circle', 'marker', 'rectangle'], function (drawShape) {
|
||||
if (!self.events.listenerCount(drawShape)) {
|
||||
drawOptions.draw[drawShape] = false;
|
||||
} else {
|
||||
drawOptions.draw[drawShape] = {
|
||||
shapeOptions: {
|
||||
stroke: false,
|
||||
color: '#000'
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
var mapOptions = {
|
||||
minZoom: 1,
|
||||
maxZoom: 18,
|
||||
layers: tileLayer,
|
||||
center: self._attr.mapCenter,
|
||||
zoom: self._attr.mapZoom,
|
||||
noWrap: true,
|
||||
maxBounds: worldBounds,
|
||||
scrollWheelZoom: false,
|
||||
fadeAnimation: false,
|
||||
};
|
||||
|
||||
var map = L.map(div[0], mapOptions);
|
||||
var featureLayer = self.markerType(map).addTo(map);
|
||||
|
||||
if (mapData.features.length) {
|
||||
map.addControl(new L.Control.Draw(drawOptions));
|
||||
}
|
||||
|
||||
function saturateTiles() {
|
||||
self.saturateTiles();
|
||||
}
|
||||
|
||||
tileLayer.on('tileload', saturateTiles);
|
||||
|
||||
map.on('unload', function () {
|
||||
tileLayer.off('tileload', saturateTiles);
|
||||
});
|
||||
|
||||
map.on('moveend', function setZoomCenter() {
|
||||
self._attr.mapZoom = map.getZoom();
|
||||
self._attr.mapCenter = map.getCenter();
|
||||
|
||||
self.events.emit('mapMoveEnd', {
|
||||
chart: self.originalConfig,
|
||||
zoom: self._attr.mapZoom,
|
||||
center: self._attr.mapCenter
|
||||
});
|
||||
|
||||
map.removeLayer(featureLayer);
|
||||
|
||||
featureLayer = self.markerType(map).addTo(map);
|
||||
});
|
||||
|
||||
map.on('draw:created', function (e) {
|
||||
var drawType = e.layerType;
|
||||
if (!self.events.listenerCount(drawType)) return;
|
||||
|
||||
// TODO: Different drawTypes need differ info. Need a switch on the object creation
|
||||
var bounds = e.layer.getBounds();
|
||||
|
||||
self.events.emit(drawType, {
|
||||
e: e,
|
||||
chart: self.originalConfig,
|
||||
bounds: {
|
||||
top_left: {
|
||||
lat: bounds.getNorthWest().lat,
|
||||
lon: bounds.getNorthWest().lng
|
||||
},
|
||||
bottom_right: {
|
||||
lat: bounds.getSouthEast().lat,
|
||||
lon: bounds.getSouthEast().lng
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
map.on('zoomend', function () {
|
||||
self.events.emit('mapZoomEnd', {
|
||||
chart: self.originalConfig,
|
||||
zoom: map.getZoom()
|
||||
});
|
||||
});
|
||||
|
||||
// add title for splits
|
||||
if (self.title) {
|
||||
self.addTitle(self.title, map);
|
||||
}
|
||||
|
||||
if (mapData && mapData.features.length > 0) {
|
||||
var fitContainer = L.DomUtil.create('div', 'leaflet-control leaflet-bar leaflet-control-fit');
|
||||
|
||||
// Add button to fit container to points
|
||||
var FitControl = L.Control.extend({
|
||||
options: {
|
||||
position: 'topleft'
|
||||
},
|
||||
onAdd: function (map) {
|
||||
$(fitContainer).html('<a class="fa fa-crop" href="#" title="Fit Data Bounds"></a>')
|
||||
.on('click', function (e) {
|
||||
e.preventDefault();
|
||||
self.fitBounds(map, mapData.features);
|
||||
});
|
||||
|
||||
return fitContainer;
|
||||
},
|
||||
onRemove: function (map) {
|
||||
$(fitContainer).off('click');
|
||||
}
|
||||
});
|
||||
map.fitControl = new FitControl();
|
||||
map.addControl(map.fitControl);
|
||||
} else {
|
||||
map.fitControl = undefined;
|
||||
}
|
||||
|
||||
self.maps.push(map);
|
||||
self._appendMap(this);
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* return whether feature is within map bounds
|
||||
*
|
||||
* @method _filterToMapBounds
|
||||
* @param map {Leaflet Object}
|
||||
* @return {boolean}
|
||||
*/
|
||||
TileMap.prototype._filterToMapBounds = function (map) {
|
||||
return function (feature) {
|
||||
var mapBounds = map.getBounds();
|
||||
var bucketRectBounds = feature.properties.rectangle.map(cloneAndReverse);
|
||||
|
||||
return mapBounds.intersects(bucketRectBounds);
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* get min and max for all cols, rows of data
|
||||
*
|
||||
* @method getMaxMin
|
||||
* @param data {Object}
|
||||
* @return {Object}
|
||||
*/
|
||||
TileMap.prototype.getMinMax = function (data) {
|
||||
var min = [];
|
||||
var max = [];
|
||||
var allData;
|
||||
|
||||
if (data.rows) {
|
||||
allData = data.rows;
|
||||
} else if (data.columns) {
|
||||
allData = data.columns;
|
||||
} else {
|
||||
allData = [data];
|
||||
}
|
||||
|
||||
allData.forEach(function (datum) {
|
||||
min.push(datum.geoJson.properties.min);
|
||||
max.push(datum.geoJson.properties.max);
|
||||
});
|
||||
|
||||
var minMax = {
|
||||
min: _.min(min),
|
||||
max: _.max(max)
|
||||
};
|
||||
|
||||
return minMax;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the Rectangles representing the geohash grid
|
||||
*
|
||||
* @return {LatLngRectangles[]}
|
||||
*/
|
||||
TileMap.prototype._getDataRectangles = function () {
|
||||
return _(this.geoJson.features)
|
||||
.pluck('properties.rectangle')
|
||||
.invoke('map', cloneAndReverse)
|
||||
.value();
|
||||
};
|
||||
|
||||
/**
|
||||
* add Leaflet latLng to mapData properties
|
||||
*
|
||||
* @method addLatLng
|
||||
* @return undefined
|
||||
*/
|
||||
TileMap.prototype.addLatLng = function () {
|
||||
this.geoJson.features.forEach(function (feature) {
|
||||
feature.properties.latLng = L.latLng(
|
||||
feature.geometry.coordinates[1],
|
||||
feature.geometry.coordinates[0]
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* zoom map to fit all features in featureLayer
|
||||
*
|
||||
* @method fitBounds
|
||||
* @param map {Leaflet Object}
|
||||
* @return {boolean}
|
||||
*/
|
||||
TileMap.prototype.fitBounds = function (map) {
|
||||
map.fitBounds(this._getDataRectangles());
|
||||
};
|
||||
|
||||
/**
|
||||
* remove css class for desat filters on map tiles
|
||||
*
|
||||
* @method saturateTiles
|
||||
* @return undefined
|
||||
*/
|
||||
TileMap.prototype.saturateTiles = function () {
|
||||
if (!this._attr.isDesaturated) {
|
||||
$('img.leaflet-tile-loaded').addClass('filters-off');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Finds nearest feature in mapData to event latlng
|
||||
*
|
||||
* @method nearestFeature
|
||||
* @param point {Leaflet Object}
|
||||
* @return nearestPoint {Leaflet Object}
|
||||
*/
|
||||
TileMap.prototype.nearestFeature = function (point) {
|
||||
var mapData = this.geoJson;
|
||||
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 map {LeafletMap}
|
||||
* @param feature {LeafletFeature}
|
||||
* @return undefined
|
||||
*/
|
||||
TileMap.prototype.showTooltip = function (map, feature) {
|
||||
if (!this.tooltipFormatter) return;
|
||||
|
||||
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 {Leaflet Object}
|
||||
* @param mapData {geoJson Object}
|
||||
* @return {Leaflet object} featureLayer
|
||||
*/
|
||||
TileMap.prototype.markerType = function (map) {
|
||||
if (this._attr.mapType === 'Scaled Circle Markers') {
|
||||
return this.scaledCircleMarkers(map);
|
||||
}
|
||||
|
||||
if (this._attr.mapType === 'Heatmap') {
|
||||
return this.heatMap(map);
|
||||
}
|
||||
|
||||
if (this._attr.mapType === 'Shaded Circle Markers') {
|
||||
return this.shadedCircleMarkers(map);
|
||||
}
|
||||
|
||||
if (this._attr.mapType === 'Shaded Geohash Grid') {
|
||||
return this.shadedGeohashGrid(map);
|
||||
}
|
||||
|
||||
return this.scaledCircleMarkers(map);
|
||||
};
|
||||
|
||||
/**
|
||||
* Type of data overlay for map:
|
||||
* creates featurelayer from mapData (geoJson)
|
||||
* with circle markers that are scaled to illustrate values
|
||||
*
|
||||
* @method scaledCircleMarkers
|
||||
* @param map {Leaflet Object}
|
||||
* @param mapData {geoJson Object}
|
||||
* @return {Leaflet object} featureLayer
|
||||
*/
|
||||
TileMap.prototype.scaledCircleMarkers = function (map) {
|
||||
var self = this;
|
||||
var mapData = self.geoJson;
|
||||
|
||||
// super min and max from all chart data
|
||||
var min = mapData.properties.allmin;
|
||||
var max = mapData.properties.allmax;
|
||||
var zoom = map.getZoom();
|
||||
var precision = _.max(mapData.features.map(function (feature) {
|
||||
return String(feature.properties.geohash).length;
|
||||
}));
|
||||
|
||||
// multiplier to reduce size of all circles
|
||||
var scaleFactor = 0.6;
|
||||
|
||||
var radiusScaler = 2.5;
|
||||
|
||||
var featureLayer = L.geoJson(mapData, {
|
||||
pointToLayer: function (feature, latlng) {
|
||||
var value = feature.properties.value;
|
||||
var scaledRadius = self.radiusScale(value, max, zoom, precision) * scaleFactor;
|
||||
return L.circleMarker(latlng).setRadius(scaledRadius);
|
||||
},
|
||||
onEachFeature: function (feature, layer) {
|
||||
self.bindPopup(feature, layer, map);
|
||||
},
|
||||
style: function (feature) {
|
||||
return self.applyShadingStyle(feature, min, max);
|
||||
},
|
||||
filter: self._filterToMapBounds(map)
|
||||
});
|
||||
|
||||
self.addLegend(map);
|
||||
|
||||
return featureLayer;
|
||||
};
|
||||
|
||||
/**
|
||||
* Type of data overlay for map:
|
||||
* creates featurelayer from mapData (geoJson)
|
||||
* with circle markers that are shaded to illustrate values
|
||||
*
|
||||
* @method shadedCircleMarkers
|
||||
* @param map {Leaflet Object}
|
||||
* @param mapData {geoJson Object}
|
||||
* @return {Leaflet object} featureLayer
|
||||
*/
|
||||
TileMap.prototype.shadedCircleMarkers = function (map) {
|
||||
var self = this;
|
||||
var mapData = self.geoJson;
|
||||
// super min and max from all chart data
|
||||
var min = mapData.properties.allmin;
|
||||
var max = mapData.properties.allmax;
|
||||
|
||||
// multiplier to reduce size of all circles
|
||||
var scaleFactor = 0.8;
|
||||
|
||||
var featureLayer = L.geoJson(mapData, {
|
||||
pointToLayer: function (feature, latlng) {
|
||||
var radius = self.geohashMinDistance(feature) * scaleFactor;
|
||||
return L.circle(latlng, radius);
|
||||
},
|
||||
onEachFeature: function (feature, layer) {
|
||||
self.bindPopup(feature, layer, map);
|
||||
},
|
||||
style: function (feature) {
|
||||
return self.applyShadingStyle(feature, min, max);
|
||||
},
|
||||
filter: self._filterToMapBounds(map)
|
||||
});
|
||||
|
||||
self.addLegend(map);
|
||||
|
||||
return featureLayer;
|
||||
};
|
||||
|
||||
/**
|
||||
* Type of data overlay for map:
|
||||
* creates featurelayer from mapData (geoJson)
|
||||
* with rectangles that show the geohash grid bounds
|
||||
*
|
||||
* @method geohashGrid
|
||||
* @param map {Leaflet Object}
|
||||
* @param mapData {geoJson Object}
|
||||
* @return {undefined}
|
||||
*/
|
||||
TileMap.prototype.shadedGeohashGrid = function (map) {
|
||||
var self = this;
|
||||
var mapData = self.geoJson;
|
||||
|
||||
// super min and max from all chart data
|
||||
var min = mapData.properties.allmin;
|
||||
var max = mapData.properties.allmax;
|
||||
|
||||
var bounds;
|
||||
|
||||
var featureLayer = L.geoJson(mapData, {
|
||||
pointToLayer: function (feature, latlng) {
|
||||
var geohashRect = feature.properties.rectangle;
|
||||
// get bounds from northEast[3] and southWest[1]
|
||||
// corners in geohash rectangle
|
||||
var corners = [
|
||||
[geohashRect[3][1], geohashRect[3][0]],
|
||||
[geohashRect[1][1], geohashRect[1][0]]
|
||||
];
|
||||
return L.rectangle(corners);
|
||||
},
|
||||
onEachFeature: function (feature, layer) {
|
||||
self.bindPopup(feature, layer, map);
|
||||
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();
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
style: function (feature) {
|
||||
return self.applyShadingStyle(feature, min, max);
|
||||
},
|
||||
filter: self._filterToMapBounds(map)
|
||||
});
|
||||
|
||||
self.addLegend(map);
|
||||
|
||||
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) {
|
||||
var self = this;
|
||||
var mapData = this.geoJson;
|
||||
var points = this.dataToHeatArray(mapData.properties.allmax);
|
||||
|
||||
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);
|
||||
|
||||
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 addTitle
|
||||
* @param mapLabel {String}
|
||||
* @param map {Leaflet Object}
|
||||
* @return {undefined}
|
||||
*/
|
||||
TileMap.prototype.addTitle = function (mapLabel, map) {
|
||||
var label = L.control();
|
||||
label.onAdd = function () {
|
||||
this._div = L.DomUtil.create('div', 'tilemap-info tilemap-label');
|
||||
this.update();
|
||||
return this._div;
|
||||
};
|
||||
label.update = function () {
|
||||
this._div.innerHTML = '<h2>' + _.escape(mapLabel) + '</h2>';
|
||||
};
|
||||
label.addTo(map);
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds legend div to each map when data is split
|
||||
* uses d3 scale from TileMap.prototype.quantizeColorScale
|
||||
*
|
||||
* @method addLegend
|
||||
* @param map {Leaflet Object}
|
||||
* @return {undefined}
|
||||
*/
|
||||
TileMap.prototype.addLegend = function (map) {
|
||||
// only draw the legend for maps with multiple items
|
||||
if (this.geoJson.features.length <= 1) return;
|
||||
|
||||
var self = this;
|
||||
var isLegend = $('div.tilemap-legend', this.chartEl).length;
|
||||
|
||||
if (isLegend) return; // Don't add Legend if already one
|
||||
|
||||
var valueFormatter = this.valueFormatter || _.identity;
|
||||
var legend = L.control({position: 'bottomright'});
|
||||
|
||||
legend.onAdd = function () {
|
||||
var $div = $('<div>').addClass('tilemap-legend');
|
||||
|
||||
_.each(self._attr.colors, function (color, i) {
|
||||
var icon = $('<i>').css({
|
||||
background: color,
|
||||
'border-color': self.darkerColor(color)
|
||||
});
|
||||
|
||||
var range = self._attr.cScale
|
||||
.invertExtent(color)
|
||||
.map(valueFormatter)
|
||||
.join(' – ');
|
||||
|
||||
$div.append(i > 0 ? '<br>' : '').append(icon).append(range);
|
||||
});
|
||||
|
||||
return $div.get(0);
|
||||
};
|
||||
legend.addTo(map);
|
||||
};
|
||||
|
||||
/**
|
||||
* Apply style with shading to feature
|
||||
*
|
||||
* @method applyShadingStyle
|
||||
* @param feature {Object}
|
||||
* @param min {Number}
|
||||
* @param max {Number}
|
||||
* @return {Object}
|
||||
*/
|
||||
TileMap.prototype.applyShadingStyle = function (feature, min, max) {
|
||||
var self = this;
|
||||
var value = feature.properties.value;
|
||||
var color = self.quantizeColorScale(value, min, max);
|
||||
|
||||
return {
|
||||
fillColor: color,
|
||||
color: self.darkerColor(color),
|
||||
weight: 1.5,
|
||||
opacity: 1,
|
||||
fillOpacity: 0.75
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Invalidate the size of the map, so that leaflet will resize to fit.
|
||||
* then moves to center
|
||||
|
@ -734,169 +61,10 @@ define(function (require) {
|
|||
*/
|
||||
TileMap.prototype.resizeArea = function () {
|
||||
this.maps.forEach(function (map) {
|
||||
map.invalidateSize({
|
||||
debounceMoveend: true
|
||||
});
|
||||
map.updateSize();
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Binds popup and events to each feature on map
|
||||
*
|
||||
* @method bindPopup
|
||||
* @param feature {Object}
|
||||
* @param layer {Object}
|
||||
* return {undefined}
|
||||
*/
|
||||
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(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 (max) {
|
||||
var self = this;
|
||||
var mapData = this.geoJson;
|
||||
|
||||
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 value on heatmap
|
||||
heatIntensity = feature.properties.value;
|
||||
} else {
|
||||
// show bucket value normalized to max value
|
||||
heatIntensity = parseInt(feature.properties.value / max * 100);
|
||||
}
|
||||
|
||||
return [lat, lng, heatIntensity];
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* geohashMinDistance returns a min distance in meters for sizing
|
||||
* circle markers to fit within geohash grid rectangle
|
||||
*
|
||||
* @method geohashMinDistance
|
||||
* @param feature {Object}
|
||||
* @return {Number}
|
||||
*/
|
||||
TileMap.prototype.geohashMinDistance = function (feature) {
|
||||
var centerPoint = feature.properties.center;
|
||||
var geohashRect = feature.properties.rectangle;
|
||||
|
||||
// get lat[1] and lng[0] of geohash center point
|
||||
// apply lat to east[2] and lng to north[3] sides of rectangle
|
||||
// to get radius at center of geohash grid recttangle
|
||||
var center = L.latLng([centerPoint[1], centerPoint[0]]);
|
||||
var east = L.latLng([centerPoint[1], geohashRect[2][0]]);
|
||||
var north = L.latLng([geohashRect[3][1], centerPoint[0]]);
|
||||
|
||||
var eastRadius = Math.floor(center.distanceTo(east));
|
||||
var northRadius = Math.floor(center.distanceTo(north));
|
||||
|
||||
return _.min([eastRadius, northRadius]);
|
||||
};
|
||||
|
||||
/**
|
||||
* radiusScale returns a number for scaled circle markers
|
||||
* square root of value / max
|
||||
* multiplied by a value based on map zoom
|
||||
* multiplied by a value based on data precision
|
||||
* for relative sizing of markers
|
||||
*
|
||||
* @method radiusScale
|
||||
* @param value {Number}
|
||||
* @param max {Number}
|
||||
* @param zoom {Number}
|
||||
* @param precision {Number}
|
||||
* @return {Number}
|
||||
*/
|
||||
TileMap.prototype.radiusScale = function (value, max, zoom, precision) {
|
||||
// exp = 0.5 for square root ratio
|
||||
// exp = 1 for linear ratio
|
||||
var exp = 0.5;
|
||||
var precisionBiasNumerator = 200;
|
||||
var precisionBiasBase = 5;
|
||||
var pct = Math.abs(value) / Math.abs(max);
|
||||
var constantZoomRadius = 0.5 * Math.pow(2, zoom);
|
||||
var precisionScale = precisionBiasNumerator / Math.pow(precisionBiasBase, precision);
|
||||
|
||||
return Math.pow(pct, exp) * constantZoomRadius * precisionScale;
|
||||
};
|
||||
|
||||
/**
|
||||
* d3 quantize scale returns a hex color,
|
||||
* used for marker fill color
|
||||
*
|
||||
* @method quantizeColorScale
|
||||
* @param value {Number}
|
||||
* @param min {Number}
|
||||
* @param max {Number}
|
||||
* @return {String} hex color
|
||||
*/
|
||||
TileMap.prototype.quantizeColorScale = function (value, min, max) {
|
||||
var reds5 = ['#fed976', '#feb24c', '#fd8d3c', '#f03b20', '#bd0026'];
|
||||
var reds3 = ['#fecc5c', '#fd8d3c', '#e31a1c'];
|
||||
var reds1 = ['#ff6128'];
|
||||
var colors = this._attr.colors = reds5;
|
||||
|
||||
if (max - min < 3) {
|
||||
colors = this._attr.colors = reds1;
|
||||
} else if (max - min < 25) {
|
||||
colors = this._attr.colors = reds3;
|
||||
}
|
||||
|
||||
var cScale = this._attr.cScale = d3.scale.quantize()
|
||||
.domain([min, max])
|
||||
.range(colors);
|
||||
|
||||
if (max === min) {
|
||||
return colors[0];
|
||||
} else {
|
||||
return cScale(value);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* d3 method returns a darker hex color,
|
||||
* used for marker stroke color
|
||||
*
|
||||
* @method darkerColor
|
||||
* @param color {String} hex color
|
||||
* @return {String} hex color
|
||||
*/
|
||||
TileMap.prototype.darkerColor = function (color) {
|
||||
var darker = d3.hcl(color).darker(1.3).toString();
|
||||
return darker;
|
||||
};
|
||||
|
||||
/**
|
||||
* clean up the maps
|
||||
*
|
||||
|
@ -904,24 +72,57 @@ define(function (require) {
|
|||
* @return {undefined}
|
||||
*/
|
||||
TileMap.prototype.destroy = function () {
|
||||
if (this.popups) {
|
||||
this.popups.forEach(function (popup) {
|
||||
popup.off('mouseover').off('mouseout');
|
||||
});
|
||||
this.popups = [];
|
||||
this.maps = this.maps.filter(function (map) {
|
||||
map.destroy();
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds allmin and allmax properties to geoJson data
|
||||
*
|
||||
* @method _appendMap
|
||||
* @param selection {Object} d3 selection
|
||||
*/
|
||||
TileMap.prototype._appendGeoExtents = function () {
|
||||
// add allmin and allmax to geoJson
|
||||
var geoMinMax = this.handler.data.getGeoExtents();
|
||||
this.geoJson.properties.allmin = geoMinMax.min;
|
||||
this.geoJson.properties.allmax = geoMinMax.max;
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders map
|
||||
*
|
||||
* @method _appendMap
|
||||
* @param selection {Object} d3 selection
|
||||
*/
|
||||
TileMap.prototype._appendMap = function (selection) {
|
||||
var container = $(selection).addClass('tilemap');
|
||||
|
||||
var map = new Map(container, this._chartData, {
|
||||
// center: this._attr.mapCenter,
|
||||
// zoom: this._attr.mapZoom,
|
||||
events: this.events,
|
||||
markerType: this._attr.mapType,
|
||||
tooltipFormatter: this.tooltipFormatter,
|
||||
valueFormatter: this.valueFormatter,
|
||||
attr: this._attr
|
||||
});
|
||||
|
||||
// add title for splits
|
||||
if (this.title) {
|
||||
map.addTitle(this.title);
|
||||
}
|
||||
|
||||
if (this.maps) {
|
||||
this.maps.forEach(function (map) {
|
||||
if (map.fitControl) {
|
||||
map.fitControl.removeFrom(map);
|
||||
}
|
||||
map.remove();
|
||||
});
|
||||
// add fit to bounds control
|
||||
if (_.get(this.geoJson, 'features.length') > 0) {
|
||||
map.addFitControl();
|
||||
map.addBoundingControl();
|
||||
}
|
||||
|
||||
this.maps.push(map);
|
||||
};
|
||||
|
||||
return TileMap;
|
||||
|
||||
};
|
||||
});
|
||||
|
|
21
test/unit/fixtures/tilemap_map.js
Normal file
21
test/unit/fixtures/tilemap_map.js
Normal file
|
@ -0,0 +1,21 @@
|
|||
define(function (require) {
|
||||
var sinon = require('test_utils/auto_release_sinon');
|
||||
|
||||
function MockMap(container, chartData, params) {
|
||||
this.container = container;
|
||||
this.chartData = chartData;
|
||||
this.params = params;
|
||||
|
||||
// stub required methods
|
||||
this.addStubs();
|
||||
}
|
||||
|
||||
MockMap.prototype.addStubs = function () {
|
||||
this.addTitle = sinon.stub();
|
||||
this.addFitControl = sinon.stub();
|
||||
this.addBoundingControl = sinon.stub();
|
||||
this.destroy = sinon.stub();
|
||||
};
|
||||
|
||||
return MockMap;
|
||||
});
|
|
@ -98,49 +98,75 @@ define(function (require) {
|
|||
});
|
||||
|
||||
describe('properties', function () {
|
||||
it('includes one feature per row in the table', function () {
|
||||
this.timeout(0);
|
||||
describe('includes one feature per row in the table', function () {
|
||||
this.timeout(60000);
|
||||
|
||||
var table = makeTable();
|
||||
var chart = makeSingleChart(table);
|
||||
var geoColI = _.findIndex(table.columns, { aggConfig: aggs.geo });
|
||||
var metricColI = _.findIndex(table.columns, { aggConfig: aggs.metric });
|
||||
var table;
|
||||
var chart;
|
||||
var geoColI;
|
||||
var metricColI;
|
||||
|
||||
table.rows.forEach(function (row, i) {
|
||||
var feature = chart.geoJson.features[i];
|
||||
expect(feature).to.have.property('geometry');
|
||||
expect(feature.geometry).to.be.an('object');
|
||||
expect(feature).to.have.property('properties');
|
||||
expect(feature.properties).to.be.an('object');
|
||||
before(function () {
|
||||
table = makeTable();
|
||||
chart = makeSingleChart(table);
|
||||
geoColI = _.findIndex(table.columns, { aggConfig: aggs.geo });
|
||||
metricColI = _.findIndex(table.columns, { aggConfig: aggs.metric });
|
||||
});
|
||||
|
||||
var geometry = feature.geometry;
|
||||
expect(geometry.type).to.be('Point');
|
||||
expect(geometry).to.have.property('coordinates');
|
||||
expect(geometry.coordinates).to.be.an('array');
|
||||
expect(geometry.coordinates).to.have.length(2);
|
||||
expect(geometry.coordinates[0]).to.be.a('number');
|
||||
expect(geometry.coordinates[1]).to.be.a('number');
|
||||
it('should be geoJson format', function () {
|
||||
table.rows.forEach(function (row, i) {
|
||||
var feature = chart.geoJson.features[i];
|
||||
expect(feature).to.have.property('geometry');
|
||||
expect(feature.geometry).to.be.an('object');
|
||||
expect(feature).to.have.property('properties');
|
||||
expect(feature.properties).to.be.an('object');
|
||||
});
|
||||
});
|
||||
|
||||
var props = feature.properties;
|
||||
expect(props).to.be.an('object');
|
||||
expect(props).to.only.have.keys(
|
||||
'value', 'geohash', 'aggConfigResult',
|
||||
'rectangle', 'center'
|
||||
);
|
||||
it('should have valid geometry data', function () {
|
||||
table.rows.forEach(function (row, i) {
|
||||
var geometry = chart.geoJson.features[i].geometry;
|
||||
expect(geometry.type).to.be('Point');
|
||||
expect(geometry).to.have.property('coordinates');
|
||||
expect(geometry.coordinates).to.be.an('array');
|
||||
expect(geometry.coordinates).to.have.length(2);
|
||||
expect(geometry.coordinates[0]).to.be.a('number');
|
||||
expect(geometry.coordinates[1]).to.be.a('number');
|
||||
});
|
||||
});
|
||||
|
||||
expect(props.center).to.eql(geometry.coordinates);
|
||||
if (props.value != null) expect(props.value).to.be.a('number');
|
||||
expect(props.geohash).to.be.a('string');
|
||||
it('should have value properties data', function () {
|
||||
table.rows.forEach(function (row, i) {
|
||||
var props = chart.geoJson.features[i].properties;
|
||||
var keys = ['value', 'geohash', 'aggConfigResult', 'rectangle', 'center'];
|
||||
expect(props).to.be.an('object');
|
||||
expect(props).to.only.have.keys(keys);
|
||||
expect(props.geohash).to.be.a('string');
|
||||
if (props.value != null) expect(props.value).to.be.a('number');
|
||||
});
|
||||
});
|
||||
|
||||
if (tableOpts.asAggConfigResults) {
|
||||
expect(props.aggConfigResult).to.be(row[metricColI]);
|
||||
expect(props.value).to.be(row[metricColI].value);
|
||||
expect(props.geohash).to.be(row[geoColI].value);
|
||||
} else {
|
||||
expect(props.aggConfigResult).to.be(null);
|
||||
expect(props.value).to.be(row[metricColI]);
|
||||
expect(props.geohash).to.be(row[geoColI]);
|
||||
}
|
||||
it('should use latLng in properties and lngLat in geometry', function () {
|
||||
table.rows.forEach(function (row, i) {
|
||||
var geometry = chart.geoJson.features[i].geometry;
|
||||
var props = chart.geoJson.features[i].properties;
|
||||
expect(props.center).to.eql(geometry.coordinates.slice(0).reverse());
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle both AggConfig and non-AggConfig results', function () {
|
||||
table.rows.forEach(function (row, i) {
|
||||
var props = chart.geoJson.features[i].properties;
|
||||
if (tableOpts.asAggConfigResults) {
|
||||
expect(props.aggConfigResult).to.be(row[metricColI]);
|
||||
expect(props.value).to.be(row[metricColI].value);
|
||||
expect(props.geohash).to.be(row[geoColI].value);
|
||||
} else {
|
||||
expect(props.aggConfigResult).to.be(null);
|
||||
expect(props.value).to.be(row[metricColI]);
|
||||
expect(props.geohash).to.be(row[geoColI]);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -52,17 +52,17 @@ define(function (require) {
|
|||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
45,
|
||||
0
|
||||
],
|
||||
[
|
||||
45,
|
||||
45
|
||||
],
|
||||
[
|
||||
0,
|
||||
45
|
||||
],
|
||||
[
|
||||
45,
|
||||
45
|
||||
],
|
||||
[
|
||||
45,
|
||||
0
|
||||
]
|
||||
]
|
||||
}
|
||||
|
@ -110,20 +110,20 @@ define(function (require) {
|
|||
},
|
||||
'rectangle': [
|
||||
[
|
||||
90,
|
||||
0
|
||||
0,
|
||||
90
|
||||
],
|
||||
[
|
||||
135,
|
||||
0
|
||||
0,
|
||||
135
|
||||
],
|
||||
[
|
||||
135,
|
||||
45
|
||||
45,
|
||||
135
|
||||
],
|
||||
[
|
||||
90,
|
||||
45
|
||||
45,
|
||||
90
|
||||
]
|
||||
]
|
||||
}
|
||||
|
@ -171,20 +171,20 @@ define(function (require) {
|
|||
},
|
||||
'rectangle': [
|
||||
[
|
||||
-90,
|
||||
-45
|
||||
-45,
|
||||
-90
|
||||
],
|
||||
[
|
||||
-45,
|
||||
-45
|
||||
],
|
||||
[
|
||||
-45,
|
||||
0
|
||||
0,
|
||||
-45
|
||||
],
|
||||
[
|
||||
-90,
|
||||
0
|
||||
0,
|
||||
-90
|
||||
]
|
||||
]
|
||||
}
|
||||
|
@ -232,20 +232,20 @@ define(function (require) {
|
|||
},
|
||||
'rectangle': [
|
||||
[
|
||||
-90,
|
||||
0
|
||||
0,
|
||||
-90
|
||||
],
|
||||
[
|
||||
-45,
|
||||
0
|
||||
0,
|
||||
-45
|
||||
],
|
||||
[
|
||||
-45,
|
||||
45
|
||||
45,
|
||||
-45
|
||||
],
|
||||
[
|
||||
-90,
|
||||
45
|
||||
45,
|
||||
-90
|
||||
]
|
||||
]
|
||||
}
|
||||
|
@ -293,20 +293,20 @@ define(function (require) {
|
|||
},
|
||||
'rectangle': [
|
||||
[
|
||||
0,
|
||||
45
|
||||
45,
|
||||
0
|
||||
],
|
||||
[
|
||||
45,
|
||||
45
|
||||
],
|
||||
[
|
||||
45,
|
||||
90
|
||||
90,
|
||||
45
|
||||
],
|
||||
[
|
||||
0,
|
||||
90
|
||||
90,
|
||||
0
|
||||
]
|
||||
]
|
||||
}
|
||||
|
@ -354,17 +354,17 @@ define(function (require) {
|
|||
},
|
||||
'rectangle': [
|
||||
[
|
||||
45,
|
||||
0
|
||||
],
|
||||
[
|
||||
90,
|
||||
0
|
||||
],
|
||||
[
|
||||
90,
|
||||
0,
|
||||
45
|
||||
],
|
||||
[
|
||||
0,
|
||||
90
|
||||
],
|
||||
[
|
||||
45,
|
||||
90
|
||||
],
|
||||
[
|
||||
45,
|
||||
45
|
||||
|
@ -415,17 +415,17 @@ define(function (require) {
|
|||
},
|
||||
'rectangle': [
|
||||
[
|
||||
0,
|
||||
-45
|
||||
],
|
||||
[
|
||||
45,
|
||||
-45
|
||||
],
|
||||
[
|
||||
45,
|
||||
-45,
|
||||
0
|
||||
],
|
||||
[
|
||||
-45,
|
||||
45
|
||||
],
|
||||
[
|
||||
0,
|
||||
45
|
||||
],
|
||||
[
|
||||
0,
|
||||
0
|
||||
|
@ -476,20 +476,20 @@ define(function (require) {
|
|||
},
|
||||
'rectangle': [
|
||||
[
|
||||
-135,
|
||||
0
|
||||
0,
|
||||
-135
|
||||
],
|
||||
[
|
||||
-90,
|
||||
0
|
||||
0,
|
||||
-90
|
||||
],
|
||||
[
|
||||
-90,
|
||||
45
|
||||
45,
|
||||
-90
|
||||
],
|
||||
[
|
||||
-135,
|
||||
45
|
||||
45,
|
||||
-135
|
||||
]
|
||||
]
|
||||
}
|
||||
|
@ -537,20 +537,20 @@ define(function (require) {
|
|||
},
|
||||
'rectangle': [
|
||||
[
|
||||
-45,
|
||||
0
|
||||
0,
|
||||
-45
|
||||
],
|
||||
[
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
0,
|
||||
45
|
||||
45,
|
||||
0
|
||||
],
|
||||
[
|
||||
-45,
|
||||
45
|
||||
45,
|
||||
-45
|
||||
]
|
||||
]
|
||||
}
|
||||
|
@ -601,17 +601,17 @@ define(function (require) {
|
|||
45,
|
||||
45
|
||||
],
|
||||
[
|
||||
90,
|
||||
45
|
||||
],
|
||||
[
|
||||
90,
|
||||
90
|
||||
],
|
||||
[
|
||||
45,
|
||||
90
|
||||
],
|
||||
[
|
||||
90,
|
||||
90
|
||||
],
|
||||
[
|
||||
90,
|
||||
45
|
||||
]
|
||||
]
|
||||
}
|
||||
|
@ -659,20 +659,20 @@ define(function (require) {
|
|||
},
|
||||
'rectangle': [
|
||||
[
|
||||
135,
|
||||
-45
|
||||
-45,
|
||||
135
|
||||
],
|
||||
[
|
||||
180,
|
||||
-45
|
||||
-45,
|
||||
180
|
||||
],
|
||||
[
|
||||
180,
|
||||
0
|
||||
0,
|
||||
180
|
||||
],
|
||||
[
|
||||
135,
|
||||
0
|
||||
0,
|
||||
135
|
||||
]
|
||||
]
|
||||
}
|
||||
|
@ -720,17 +720,17 @@ define(function (require) {
|
|||
},
|
||||
'rectangle': [
|
||||
[
|
||||
90,
|
||||
45
|
||||
],
|
||||
[
|
||||
135,
|
||||
45
|
||||
],
|
||||
[
|
||||
135,
|
||||
45,
|
||||
90
|
||||
],
|
||||
[
|
||||
45,
|
||||
135
|
||||
],
|
||||
[
|
||||
90,
|
||||
135
|
||||
],
|
||||
[
|
||||
90,
|
||||
90
|
||||
|
@ -781,20 +781,20 @@ define(function (require) {
|
|||
},
|
||||
'rectangle': [
|
||||
[
|
||||
-135,
|
||||
45
|
||||
45,
|
||||
-135
|
||||
],
|
||||
[
|
||||
-90,
|
||||
45
|
||||
45,
|
||||
-90
|
||||
],
|
||||
[
|
||||
-90,
|
||||
90
|
||||
90,
|
||||
-90
|
||||
],
|
||||
[
|
||||
-135,
|
||||
90
|
||||
90,
|
||||
-135
|
||||
]
|
||||
]
|
||||
}
|
||||
|
@ -842,20 +842,20 @@ define(function (require) {
|
|||
},
|
||||
'rectangle': [
|
||||
[
|
||||
90,
|
||||
-45
|
||||
-45,
|
||||
90
|
||||
],
|
||||
[
|
||||
135,
|
||||
-45
|
||||
-45,
|
||||
135
|
||||
],
|
||||
[
|
||||
135,
|
||||
0
|
||||
0,
|
||||
135
|
||||
],
|
||||
[
|
||||
90,
|
||||
0
|
||||
0,
|
||||
90
|
||||
]
|
||||
]
|
||||
}
|
||||
|
@ -906,17 +906,17 @@ define(function (require) {
|
|||
-45,
|
||||
-45
|
||||
],
|
||||
[
|
||||
0,
|
||||
-45
|
||||
],
|
||||
[
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
-45,
|
||||
0
|
||||
],
|
||||
[
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
0,
|
||||
-45
|
||||
]
|
||||
]
|
||||
}
|
||||
|
@ -964,20 +964,20 @@ define(function (require) {
|
|||
},
|
||||
'rectangle': [
|
||||
[
|
||||
-90,
|
||||
45
|
||||
45,
|
||||
-90
|
||||
],
|
||||
[
|
||||
-45,
|
||||
45
|
||||
45,
|
||||
-45
|
||||
],
|
||||
[
|
||||
-45,
|
||||
90
|
||||
90,
|
||||
-45
|
||||
],
|
||||
[
|
||||
-90,
|
||||
90
|
||||
90,
|
||||
-90
|
||||
]
|
||||
]
|
||||
}
|
||||
|
@ -1025,20 +1025,20 @@ define(function (require) {
|
|||
},
|
||||
'rectangle': [
|
||||
[
|
||||
-180,
|
||||
45
|
||||
45,
|
||||
-180
|
||||
],
|
||||
[
|
||||
-135,
|
||||
45
|
||||
45,
|
||||
-135
|
||||
],
|
||||
[
|
||||
-135,
|
||||
90
|
||||
90,
|
||||
-135
|
||||
],
|
||||
[
|
||||
-180,
|
||||
90
|
||||
90,
|
||||
-180
|
||||
]
|
||||
]
|
||||
}
|
||||
|
@ -1086,20 +1086,20 @@ define(function (require) {
|
|||
},
|
||||
'rectangle': [
|
||||
[
|
||||
-45,
|
||||
45
|
||||
45,
|
||||
-45
|
||||
],
|
||||
[
|
||||
0,
|
||||
45
|
||||
45,
|
||||
0
|
||||
],
|
||||
[
|
||||
0,
|
||||
90
|
||||
90,
|
||||
0
|
||||
],
|
||||
[
|
||||
-45,
|
||||
90
|
||||
90,
|
||||
-45
|
||||
]
|
||||
]
|
||||
}
|
||||
|
@ -1147,20 +1147,20 @@ define(function (require) {
|
|||
},
|
||||
'rectangle': [
|
||||
[
|
||||
135,
|
||||
45
|
||||
45,
|
||||
135
|
||||
],
|
||||
[
|
||||
180,
|
||||
45
|
||||
45,
|
||||
180
|
||||
],
|
||||
[
|
||||
180,
|
||||
90
|
||||
90,
|
||||
180
|
||||
],
|
||||
[
|
||||
135,
|
||||
90
|
||||
90,
|
||||
135
|
||||
]
|
||||
]
|
||||
}
|
||||
|
@ -1208,20 +1208,20 @@ define(function (require) {
|
|||
},
|
||||
'rectangle': [
|
||||
[
|
||||
135,
|
||||
0
|
||||
0,
|
||||
135
|
||||
],
|
||||
[
|
||||
180,
|
||||
0
|
||||
0,
|
||||
180
|
||||
],
|
||||
[
|
||||
180,
|
||||
45
|
||||
45,
|
||||
180
|
||||
],
|
||||
[
|
||||
135,
|
||||
45
|
||||
45,
|
||||
135
|
||||
]
|
||||
]
|
||||
}
|
||||
|
@ -1272,17 +1272,17 @@ define(function (require) {
|
|||
-90,
|
||||
-90
|
||||
],
|
||||
[
|
||||
-45,
|
||||
-90
|
||||
],
|
||||
[
|
||||
-45,
|
||||
-45
|
||||
],
|
||||
[
|
||||
-90,
|
||||
-45
|
||||
],
|
||||
[
|
||||
-45,
|
||||
-45
|
||||
],
|
||||
[
|
||||
-45,
|
||||
-90
|
||||
]
|
||||
]
|
||||
}
|
||||
|
@ -1330,20 +1330,20 @@ define(function (require) {
|
|||
},
|
||||
'rectangle': [
|
||||
[
|
||||
45,
|
||||
-45
|
||||
-45,
|
||||
45
|
||||
],
|
||||
[
|
||||
90,
|
||||
-45
|
||||
-45,
|
||||
90
|
||||
],
|
||||
[
|
||||
90,
|
||||
0
|
||||
0,
|
||||
90
|
||||
],
|
||||
[
|
||||
45,
|
||||
0
|
||||
0,
|
||||
45
|
||||
]
|
||||
]
|
||||
}
|
||||
|
@ -1391,17 +1391,17 @@ define(function (require) {
|
|||
},
|
||||
'rectangle': [
|
||||
[
|
||||
-45,
|
||||
-90
|
||||
],
|
||||
[
|
||||
0,
|
||||
-90
|
||||
],
|
||||
[
|
||||
0,
|
||||
-90,
|
||||
-45
|
||||
],
|
||||
[
|
||||
-90,
|
||||
0
|
||||
],
|
||||
[
|
||||
-45,
|
||||
0
|
||||
],
|
||||
[
|
||||
-45,
|
||||
-45
|
||||
|
@ -1452,20 +1452,20 @@ define(function (require) {
|
|||
},
|
||||
'rectangle': [
|
||||
[
|
||||
135,
|
||||
-90
|
||||
-90,
|
||||
135
|
||||
],
|
||||
[
|
||||
180,
|
||||
-90
|
||||
-90,
|
||||
180
|
||||
],
|
||||
[
|
||||
180,
|
||||
-45
|
||||
-45,
|
||||
180
|
||||
],
|
||||
[
|
||||
135,
|
||||
-45
|
||||
-45,
|
||||
135
|
||||
]
|
||||
]
|
||||
}
|
||||
|
@ -1513,20 +1513,20 @@ define(function (require) {
|
|||
},
|
||||
'rectangle': [
|
||||
[
|
||||
-180,
|
||||
-45
|
||||
-45,
|
||||
-180
|
||||
],
|
||||
[
|
||||
-135,
|
||||
-45
|
||||
-45,
|
||||
-135
|
||||
],
|
||||
[
|
||||
-135,
|
||||
0
|
||||
0,
|
||||
-135
|
||||
],
|
||||
[
|
||||
-180,
|
||||
0
|
||||
0,
|
||||
-180
|
||||
]
|
||||
]
|
||||
}
|
||||
|
@ -1574,20 +1574,20 @@ define(function (require) {
|
|||
},
|
||||
'rectangle': [
|
||||
[
|
||||
0,
|
||||
-90
|
||||
-90,
|
||||
0
|
||||
],
|
||||
[
|
||||
45,
|
||||
-90
|
||||
-90,
|
||||
45
|
||||
],
|
||||
[
|
||||
45,
|
||||
-45
|
||||
-45,
|
||||
45
|
||||
],
|
||||
[
|
||||
0,
|
||||
-45
|
||||
-45,
|
||||
0
|
||||
]
|
||||
]
|
||||
}
|
||||
|
@ -1635,20 +1635,20 @@ define(function (require) {
|
|||
},
|
||||
'rectangle': [
|
||||
[
|
||||
90,
|
||||
-90
|
||||
-90,
|
||||
90
|
||||
],
|
||||
[
|
||||
135,
|
||||
-90
|
||||
-90,
|
||||
135
|
||||
],
|
||||
[
|
||||
135,
|
||||
-45
|
||||
-45,
|
||||
135
|
||||
],
|
||||
[
|
||||
90,
|
||||
-45
|
||||
-45,
|
||||
90
|
||||
]
|
||||
]
|
||||
}
|
||||
|
@ -1696,20 +1696,20 @@ define(function (require) {
|
|||
},
|
||||
'rectangle': [
|
||||
[
|
||||
45,
|
||||
-90
|
||||
-90,
|
||||
45
|
||||
],
|
||||
[
|
||||
90,
|
||||
-90
|
||||
-90,
|
||||
90
|
||||
],
|
||||
[
|
||||
90,
|
||||
-45
|
||||
-45,
|
||||
90
|
||||
],
|
||||
[
|
||||
45,
|
||||
-45
|
||||
-45,
|
||||
45
|
||||
]
|
||||
]
|
||||
}
|
||||
|
@ -1757,20 +1757,20 @@ define(function (require) {
|
|||
},
|
||||
'rectangle': [
|
||||
[
|
||||
-135,
|
||||
-45
|
||||
-45,
|
||||
-135
|
||||
],
|
||||
[
|
||||
-90,
|
||||
-45
|
||||
-45,
|
||||
-90
|
||||
],
|
||||
[
|
||||
-90,
|
||||
0
|
||||
0,
|
||||
-90
|
||||
],
|
||||
[
|
||||
-135,
|
||||
0
|
||||
0,
|
||||
-135
|
||||
]
|
||||
]
|
||||
}
|
||||
|
@ -1818,20 +1818,20 @@ define(function (require) {
|
|||
},
|
||||
'rectangle': [
|
||||
[
|
||||
-135,
|
||||
-90
|
||||
-90,
|
||||
-135
|
||||
],
|
||||
[
|
||||
-90,
|
||||
-90
|
||||
],
|
||||
[
|
||||
-90,
|
||||
-45
|
||||
-45,
|
||||
-90
|
||||
],
|
||||
[
|
||||
-135,
|
||||
-45
|
||||
-45,
|
||||
-135
|
||||
]
|
||||
]
|
||||
}
|
||||
|
|
|
@ -258,5 +258,63 @@ define(function (require) {
|
|||
});
|
||||
});
|
||||
|
||||
describe('geohashGrid methods', function () {
|
||||
var data;
|
||||
var geohashGridData = {
|
||||
hits: 3954,
|
||||
rows: [{
|
||||
title: 'Top 5 _type: apache',
|
||||
label: 'Top 5 _type: apache',
|
||||
geoJson: {
|
||||
type: 'FeatureCollection',
|
||||
features: [],
|
||||
properties: {
|
||||
min: 2,
|
||||
max: 331,
|
||||
zoom: 3,
|
||||
center: [
|
||||
47.517200697839414,
|
||||
-112.06054687499999
|
||||
]
|
||||
}
|
||||
},
|
||||
}, {
|
||||
title: 'Top 5 _type: nginx',
|
||||
label: 'Top 5 _type: nginx',
|
||||
geoJson: {
|
||||
type: 'FeatureCollection',
|
||||
features: [],
|
||||
properties: {
|
||||
min: 1,
|
||||
max: 88,
|
||||
zoom: 3,
|
||||
center: [
|
||||
47.517200697839414,
|
||||
-112.06054687499999
|
||||
]
|
||||
}
|
||||
},
|
||||
}]
|
||||
};
|
||||
|
||||
beforeEach(function () {
|
||||
data = new Data(geohashGridData, {});
|
||||
});
|
||||
|
||||
describe('getVisData', function () {
|
||||
it('should return the rows property', function () {
|
||||
var visData = data.getVisData();
|
||||
expect(visData).to.eql(geohashGridData.rows);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getGeoExtents', function () {
|
||||
it('should return the min and max geoJson properties', function () {
|
||||
var minMax = data.getGeoExtents();
|
||||
expect(minMax.min).to.be(1);
|
||||
expect(minMax.max).to.be(331);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,398 +0,0 @@
|
|||
define(function (require) {
|
||||
var angular = require('angular');
|
||||
var _ = require('lodash');
|
||||
var $ = require('jquery');
|
||||
var L = require('leaflet');
|
||||
|
||||
// Data
|
||||
var dataArray = [
|
||||
require('vislib_fixtures/mock_data/geohash/_geo_json'),
|
||||
require('vislib_fixtures/mock_data/geohash/_columns'),
|
||||
require('vislib_fixtures/mock_data/geohash/_rows')
|
||||
];
|
||||
var names = ['geojson', 'columns', 'rows'];
|
||||
// TODO: Test the specific behavior of each these
|
||||
var mapTypes = ['Scaled Circle Markers', 'Shaded Circle Markers', 'Shaded Geohash Grid', 'Heatmap'];
|
||||
|
||||
angular.module('TileMapFactory', ['kibana']);
|
||||
|
||||
|
||||
function bootstrapAndRender(data, type) {
|
||||
var vis;
|
||||
var visLibParams = {
|
||||
isDesaturated: true,
|
||||
type: 'tile_map',
|
||||
mapType: type
|
||||
};
|
||||
|
||||
module('TileMapFactory');
|
||||
inject(function (Private) {
|
||||
vis = Private(require('vislib_fixtures/_vis_fixture'))(visLibParams);
|
||||
require('css!components/vislib/styles/main');
|
||||
vis.render(data);
|
||||
});
|
||||
|
||||
return vis;
|
||||
|
||||
}
|
||||
|
||||
function destroyVis(vis) {
|
||||
$(vis.el).remove();
|
||||
vis = null;
|
||||
}
|
||||
|
||||
describe('TileMap Tests', function () {
|
||||
describe('Rendering each types of tile map', function () {
|
||||
dataArray.forEach(function (data, i) {
|
||||
|
||||
mapTypes.forEach(function (type, j) {
|
||||
|
||||
describe('draw() ' + mapTypes[j] + ' with ' + names[i], function () {
|
||||
var vis;
|
||||
|
||||
beforeEach(function () {
|
||||
vis = bootstrapAndRender(data, type);
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
destroyVis(vis);
|
||||
});
|
||||
|
||||
it('should return a function', function () {
|
||||
vis.handler.charts.forEach(function (chart) {
|
||||
expect(chart.draw()).to.be.a(Function);
|
||||
});
|
||||
});
|
||||
|
||||
it('should create .leaflet-container as a by product of map rendering', function () {
|
||||
expect($(vis.el).find('.leaflet-container').length).to.be.above(0);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Leaflet controls', function () {
|
||||
var vis;
|
||||
var leafletContainer;
|
||||
|
||||
beforeEach(function () {
|
||||
vis = bootstrapAndRender(dataArray[0], 'Scaled Circle Markers');
|
||||
leafletContainer = $(vis.el).find('.leaflet-container');
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
destroyVis(vis);
|
||||
});
|
||||
|
||||
it('should attach the zoom controls', function () {
|
||||
expect(leafletContainer.find('.leaflet-control-zoom-in').length).to.be(1);
|
||||
expect(leafletContainer.find('.leaflet-control-zoom-out').length).to.be(1);
|
||||
});
|
||||
|
||||
it('should attach the filter drawing button', function () {
|
||||
expect(leafletContainer.find('.leaflet-draw').length).to.be(1);
|
||||
});
|
||||
|
||||
it('should attach the crop button', function () {
|
||||
expect(leafletContainer.find('.leaflet-control-fit').length).to.be(1);
|
||||
});
|
||||
|
||||
it('should not attach the filter or crop buttons if no data is present', function () {
|
||||
var noData = {
|
||||
geoJson: {
|
||||
features: [],
|
||||
properties: {
|
||||
label: null,
|
||||
length: 30,
|
||||
min: 1,
|
||||
max: 608,
|
||||
precision: 1,
|
||||
allmin: 1,
|
||||
allmax: 608
|
||||
},
|
||||
hits: 20
|
||||
}
|
||||
};
|
||||
vis.render(noData);
|
||||
leafletContainer = $(vis.el).find('.leaflet-container');
|
||||
|
||||
expect(leafletContainer.find('.leaflet-control-fit').length).to.be(0);
|
||||
expect(leafletContainer.find('.leaflet-draw').length).to.be(0);
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
// Probably only neccesary to test one of these as we already know the the map will render
|
||||
|
||||
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 () {
|
||||
destroyVis(vis);
|
||||
});
|
||||
|
||||
describe('_filterToMapBounds method', function () {
|
||||
it('should filter out data points that are outside of the map bounds', function () {
|
||||
vis.handler.charts.forEach(function (chart) {
|
||||
chart.maps.forEach(function (map) {
|
||||
var featuresLength = chart.geoJson.features.length;
|
||||
var mapFeatureLength;
|
||||
|
||||
function getSize(obj) {
|
||||
var size = 0;
|
||||
var key;
|
||||
|
||||
for (key in obj) { if (obj.hasOwnProperty(key)) size++; }
|
||||
return size;
|
||||
}
|
||||
|
||||
map.setZoom(13); // Zoom in on the map!
|
||||
mapFeatureLength = getSize(map._layers);
|
||||
|
||||
expect(mapFeatureLength).to.be.lessThan(featuresLength);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('geohashMinDistance method', function () {
|
||||
it('should return a number', function () {
|
||||
vis.handler.charts.forEach(function (chart) {
|
||||
expect(_.isFinite(chart.geohashMinDistance(feature))).to.be(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('radiusScale method', function () {
|
||||
var countdata = [0, 10, 20, 30, 40, 50, 60];
|
||||
var max = 60;
|
||||
var zoom = _.random(1, 18);
|
||||
var constantZoomRadius = 0.5 * Math.pow(2, zoom);
|
||||
var precision = _.random(1, 12);
|
||||
var precisionScale = 200 / Math.pow(5, precision);
|
||||
var prev = -1;
|
||||
|
||||
it('test array should return a number equal to radius', function () {
|
||||
countdata.forEach(function (data, i) {
|
||||
vis.handler.charts.forEach(function (chart) {
|
||||
var count = data;
|
||||
var pct = count / max;
|
||||
var exp = 0.5;
|
||||
var radius = Math.pow(pct, exp) * constantZoomRadius * precisionScale;
|
||||
var test = chart.radiusScale(count, max, zoom, precision);
|
||||
|
||||
expect(test).to.be.a('number');
|
||||
expect(test).to.be(radius);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('test array should return a radius greater than previous', function () {
|
||||
countdata.forEach(function (data, i) {
|
||||
vis.handler.charts.forEach(function (chart) {
|
||||
var count = data;
|
||||
var pct = count / max;
|
||||
var exp = 0.5;
|
||||
var radius = Math.pow(pct, exp) * constantZoomRadius * precisionScale;
|
||||
var test = chart.radiusScale(count, max, zoom, precision);
|
||||
|
||||
expect(test).to.be.above(prev);
|
||||
prev = chart.radiusScale(count, max, zoom, precision);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('quantizeColorScale method', function () {
|
||||
it('should return a hex color', function () {
|
||||
vis.handler.charts.forEach(function (chart) {
|
||||
var reds = ['#fed976', '#feb24c', '#fd8d3c', '#f03b20', '#bd0026'];
|
||||
var count = Math.random() * 300;
|
||||
var min = 0;
|
||||
var max = 300;
|
||||
expect(_.indexOf(reds, chart.quantizeColorScale(count, min, max))).to.not.be(-1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getMinMax method', function () {
|
||||
it('should return an object', function () {
|
||||
vis.handler.charts.forEach(function (chart) {
|
||||
var data = chart.handler.data.data;
|
||||
expect(chart.getMinMax(data)).to.be.an(Object);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return the min of all features.properties.value', function () {
|
||||
vis.handler.charts.forEach(function (chart) {
|
||||
var data = chart.handler.data.data;
|
||||
var min = _.chain(data.geoJson.features)
|
||||
.pluck('properties.value')
|
||||
.min()
|
||||
.value();
|
||||
expect(chart.getMinMax(data).min).to.be(min);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return the max of all features.properties.value', function () {
|
||||
vis.handler.charts.forEach(function (chart) {
|
||||
var data = chart.handler.data.data;
|
||||
var max = _.chain(data.geoJson.features)
|
||||
.pluck('properties.value')
|
||||
.max()
|
||||
.value();
|
||||
expect(chart.getMinMax(data).max).to.be(max);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('dataToHeatArray method', function () {
|
||||
it('should return an array', function () {
|
||||
vis.handler.charts.forEach(function (chart) {
|
||||
expect(chart.dataToHeatArray(max)).to.be.an(Array);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return an array item for each feature', function () {
|
||||
vis.handler.charts.forEach(function (chart) {
|
||||
expect(chart.dataToHeatArray(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.value;
|
||||
var array = chart.dataToHeatArray(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.value / max * 100);
|
||||
var array = chart.dataToHeatArray(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 featureLayer = _.sample(_.filter(map._layers, 'feature'));
|
||||
|
||||
expect($('.popup-stub', vis.el).length).to.be(0);
|
||||
featureLayer.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)).to.be.an(Object);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return a geoJson feature', function () {
|
||||
vis.handler.charts.forEach(function (chart) {
|
||||
expect(chart.nearestFeature(point).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)).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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
});
|
198
test/unit/specs/vislib/visualizations/tile_maps/map.js
Normal file
198
test/unit/specs/vislib/visualizations/tile_maps/map.js
Normal file
|
@ -0,0 +1,198 @@
|
|||
define(function (require) {
|
||||
var angular = require('angular');
|
||||
var _ = require('lodash');
|
||||
var $ = require('jquery');
|
||||
var L = require('leaflet');
|
||||
|
||||
var sinon = require('test_utils/auto_release_sinon');
|
||||
var geoJsonData = require('vislib_fixtures/mock_data/geohash/_geo_json');
|
||||
|
||||
// // Data
|
||||
// var dataArray = [
|
||||
// ['geojson', require('vislib_fixtures/mock_data/geohash/_geo_json')],
|
||||
// ['columns', require('vislib_fixtures/mock_data/geohash/_columns')],
|
||||
// ['rows', require('vislib_fixtures/mock_data/geohash/_rows')],
|
||||
// ];
|
||||
|
||||
// // TODO: Test the specific behavior of each these
|
||||
// var mapTypes = [
|
||||
// 'Scaled Circle Markers',
|
||||
// 'Shaded Circle Markers',
|
||||
// 'Shaded Geohash Grid',
|
||||
// 'Heatmap'
|
||||
// ];
|
||||
|
||||
angular.module('MapFactory', ['kibana']);
|
||||
|
||||
describe('TileMap Map Tests', function () {
|
||||
this.timeout(0);
|
||||
var $mockMapEl = $('<div>');
|
||||
var Map;
|
||||
var leafletStubs = {};
|
||||
var leafletMocks = {};
|
||||
|
||||
beforeEach(function () {
|
||||
module('MapFactory');
|
||||
inject(function (Private) {
|
||||
// mock parts of leaflet
|
||||
leafletMocks.tileLayer = { on: sinon.stub() };
|
||||
leafletMocks.map = { on: sinon.stub() };
|
||||
leafletStubs.tileLayer = sinon.stub(L, 'tileLayer', _.constant(leafletMocks.tileLayer));
|
||||
leafletStubs.map = sinon.stub(L, 'map', _.constant(leafletMocks.map));
|
||||
|
||||
Map = Private(require('components/vislib/visualizations/_map'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('instantiation', function () {
|
||||
var map;
|
||||
var createStub;
|
||||
|
||||
beforeEach(function () {
|
||||
createStub = sinon.stub(Map.prototype, '_createMap', _.noop);
|
||||
map = new Map($mockMapEl, geoJsonData, {});
|
||||
});
|
||||
|
||||
it('should create the map', function () {
|
||||
expect(createStub.callCount).to.equal(1);
|
||||
});
|
||||
|
||||
it('should add zoom controls', function () {
|
||||
var mapOptions = createStub.firstCall.args[0];
|
||||
|
||||
expect(mapOptions).to.be.an('object');
|
||||
if (mapOptions.zoomControl) expect(mapOptions.zoomControl).to.be.ok();
|
||||
else expect(mapOptions.zoomControl).to.be(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createMap', function () {
|
||||
var map;
|
||||
var mapStubs;
|
||||
|
||||
beforeEach(function () {
|
||||
mapStubs = {
|
||||
destroy: sinon.stub(Map.prototype, 'destroy'),
|
||||
attachEvents: sinon.stub(Map.prototype, '_attachEvents'),
|
||||
addMarkers: sinon.stub(Map.prototype, '_addMarkers'),
|
||||
};
|
||||
|
||||
map = new Map($mockMapEl, geoJsonData, {});
|
||||
});
|
||||
|
||||
it('should create the create leaflet objects', function () {
|
||||
expect(leafletStubs.tileLayer.callCount).to.equal(1);
|
||||
expect(leafletStubs.map.callCount).to.equal(1);
|
||||
|
||||
var callArgs = leafletStubs.map.firstCall.args;
|
||||
var mapOptions = callArgs[1];
|
||||
expect(callArgs[0]).to.be($mockMapEl.get(0));
|
||||
expect(mapOptions).to.have.property('zoom');
|
||||
expect(mapOptions).to.have.property('center');
|
||||
});
|
||||
|
||||
it('should attach events and add markers', function () {
|
||||
expect(mapStubs.attachEvents.callCount).to.equal(1);
|
||||
expect(mapStubs.addMarkers.callCount).to.equal(1);
|
||||
});
|
||||
|
||||
it('should call destroy only if a map exists', function () {
|
||||
expect(mapStubs.destroy.callCount).to.equal(0);
|
||||
map._createMap({});
|
||||
expect(mapStubs.destroy.callCount).to.equal(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('attachEvents', function () {
|
||||
var map;
|
||||
|
||||
beforeEach(function () {
|
||||
sinon.stub(Map.prototype, '_createMap', function () {
|
||||
this._tileLayer = leafletMocks.tileLayer;
|
||||
this.map = leafletMocks.map;
|
||||
this._attachEvents();
|
||||
});
|
||||
map = new Map($mockMapEl, geoJsonData, {});
|
||||
});
|
||||
|
||||
it('should attach interaction events', function () {
|
||||
var expectedTileEvents = ['tileload'];
|
||||
var expectedMapEvents = ['draw:created', 'moveend', 'zoomend', 'unload'];
|
||||
var matchedEvents = {
|
||||
tiles: 0,
|
||||
maps: 0,
|
||||
};
|
||||
|
||||
_.times(leafletMocks.tileLayer.on.callCount, function (index) {
|
||||
var ev = leafletMocks.tileLayer.on.getCall(index).args[0];
|
||||
if (_.includes(expectedTileEvents, ev)) matchedEvents.tiles++;
|
||||
});
|
||||
expect(matchedEvents.tiles).to.equal(expectedTileEvents.length);
|
||||
|
||||
_.times(leafletMocks.map.on.callCount, function (index) {
|
||||
var ev = leafletMocks.map.on.getCall(index).args[0];
|
||||
if (_.includes(expectedMapEvents, ev)) matchedEvents.maps++;
|
||||
});
|
||||
expect(matchedEvents.maps).to.equal(expectedMapEvents.length);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('addMarkers', function () {
|
||||
var map;
|
||||
var createStub;
|
||||
|
||||
beforeEach(function () {
|
||||
sinon.stub(Map.prototype, '_createMap');
|
||||
createStub = sinon.stub(Map.prototype, '_createMarkers', _.constant({ addLegend: _.noop }));
|
||||
map = new Map($mockMapEl, geoJsonData, {});
|
||||
});
|
||||
|
||||
it('should pass the map options to the marker', function () {
|
||||
map._addMarkers();
|
||||
|
||||
var args = createStub.firstCall.args[0];
|
||||
expect(args).to.have.property('tooltipFormatter');
|
||||
expect(args).to.have.property('valueFormatter');
|
||||
expect(args).to.have.property('attr');
|
||||
});
|
||||
|
||||
it('should destroy existing markers', function () {
|
||||
var destroyStub = sinon.stub();
|
||||
map._markers = { destroy: destroyStub };
|
||||
map._addMarkers();
|
||||
|
||||
expect(destroyStub.callCount).to.be(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDataRectangles', function () {
|
||||
var map;
|
||||
|
||||
beforeEach(function () {
|
||||
sinon.stub(Map.prototype, '_createMap');
|
||||
map = new Map($mockMapEl, geoJsonData, {});
|
||||
});
|
||||
|
||||
it('should return an empty array if no data', function () {
|
||||
map = new Map($mockMapEl, {}, {});
|
||||
var rects = map._getDataRectangles();
|
||||
expect(rects).to.have.length(0);
|
||||
});
|
||||
|
||||
it('should return an array of arrays of rectangles', function () {
|
||||
var rects = map._getDataRectangles();
|
||||
_.times(5, function () {
|
||||
var index = _.random(rects.length - 1);
|
||||
var rect = rects[index];
|
||||
var featureRect = geoJsonData.geoJson.features[index].properties.rectangle;
|
||||
expect(rect.length).to.equal(featureRect.length);
|
||||
|
||||
// should swap the array
|
||||
var checkIndex = _.random(rect.length - 1);
|
||||
expect(rect[checkIndex]).to.eql(featureRect[checkIndex]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
373
test/unit/specs/vislib/visualizations/tile_maps/markers.js
Normal file
373
test/unit/specs/vislib/visualizations/tile_maps/markers.js
Normal file
|
@ -0,0 +1,373 @@
|
|||
define(function (require) {
|
||||
var angular = require('angular');
|
||||
var _ = require('lodash');
|
||||
var $ = require('jquery');
|
||||
var L = require('leaflet');
|
||||
var sinon = require('test_utils/auto_release_sinon');
|
||||
var geoJsonData = require('vislib_fixtures/mock_data/geohash/_geo_json');
|
||||
// defaults to roughly the lower 48 US states
|
||||
var defaultSWCoords = [13.496, -143.789];
|
||||
var defaultNECoords = [55.526, -57.919];
|
||||
var bounds = {};
|
||||
var MarkerType;
|
||||
var map;
|
||||
|
||||
angular.module('MarkerFactory', ['kibana']);
|
||||
|
||||
function setBounds(southWest, northEast) {
|
||||
bounds.southWest = L.latLng(southWest || defaultSWCoords);
|
||||
bounds.northEast = L.latLng(northEast || defaultNECoords);
|
||||
}
|
||||
|
||||
function getBounds() {
|
||||
return L.latLngBounds(bounds.southWest, bounds.northEast);
|
||||
}
|
||||
|
||||
var mockMap = {
|
||||
addLayer: _.noop,
|
||||
closePopup: _.noop,
|
||||
getBounds: getBounds,
|
||||
removeControl: _.noop,
|
||||
removeLayer: _.noop,
|
||||
getZoom: _.constant(5)
|
||||
};
|
||||
|
||||
describe('Marker Tests', function () {
|
||||
var mapData;
|
||||
var markerLayer;
|
||||
|
||||
function createMarker(MarkerClass, geoJson) {
|
||||
mapData = _.assign({}, geoJsonData.geoJson, geoJson || {});
|
||||
mapData.properties.allmin = mapData.properties.min;
|
||||
mapData.properties.allmax = mapData.properties.max;
|
||||
|
||||
return new MarkerClass(mockMap, mapData, {
|
||||
valueFormatter: geoJsonData.valueFormatter
|
||||
});
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
setBounds();
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
if (markerLayer) {
|
||||
markerLayer.destroy();
|
||||
markerLayer = undefined;
|
||||
}
|
||||
});
|
||||
|
||||
describe('Base Methods', function () {
|
||||
var MarkerClass;
|
||||
|
||||
beforeEach(function () {
|
||||
module('MarkerFactory');
|
||||
inject(function (Private) {
|
||||
MarkerClass = Private(require('components/vislib/visualizations/marker_types/base_marker'));
|
||||
markerLayer = createMarker(MarkerClass);
|
||||
});
|
||||
});
|
||||
|
||||
describe('filterToMapBounds', function () {
|
||||
it('should not filter any features', function () {
|
||||
// set bounds to the entire world
|
||||
setBounds([-87.252, -343.828], [87.252, 343.125]);
|
||||
var boundFilter = markerLayer._filterToMapBounds();
|
||||
var mapFeature = mapData.features.filter(boundFilter);
|
||||
|
||||
expect(mapFeature.length).to.equal(mapData.features.length);
|
||||
});
|
||||
|
||||
it('should filter out data points that are outside of the map bounds', function () {
|
||||
// set bounds to roughly US southwest
|
||||
setBounds([31.690, -124.387], [42.324, -102.919]);
|
||||
var boundFilter = markerLayer._filterToMapBounds();
|
||||
var mapFeature = mapData.features.filter(boundFilter);
|
||||
|
||||
expect(mapFeature.length).to.be.lessThan(mapData.features.length);
|
||||
});
|
||||
});
|
||||
|
||||
describe('legendQuantizer', function () {
|
||||
it('should return a range of hex colors', function () {
|
||||
var minColor = markerLayer._legendQuantizer(mapData.properties.allmin);
|
||||
var maxColor = markerLayer._legendQuantizer(mapData.properties.allmax);
|
||||
|
||||
expect(minColor.substring(0, 1)).to.equal('#');
|
||||
expect(minColor).to.have.length(7);
|
||||
expect(maxColor.substring(0, 1)).to.equal('#');
|
||||
expect(maxColor).to.have.length(7);
|
||||
expect(minColor).to.not.eql(maxColor);
|
||||
});
|
||||
|
||||
it('should return a color with 1 color', function () {
|
||||
var geoJson = { properties: { min: 1, max: 1 } };
|
||||
markerLayer = createMarker(MarkerClass, geoJson);
|
||||
|
||||
// ensure the quantizer domain is correct
|
||||
var color = markerLayer._legendQuantizer(1);
|
||||
expect(color).to.not.be(undefined);
|
||||
expect(color.substring(0, 1)).to.equal('#');
|
||||
|
||||
// should always get the same color back
|
||||
_.times(5, function () {
|
||||
var num = _.random(0, 100);
|
||||
var randColor = markerLayer._legendQuantizer(0);
|
||||
expect(randColor).to.equal(color);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('applyShadingStyle', function () {
|
||||
it('should return a style object', function () {
|
||||
var style = markerLayer.applyShadingStyle(100);
|
||||
expect(style).to.be.an('object');
|
||||
|
||||
var keys = _.keys(style);
|
||||
var expected = ['fillColor', 'color'];
|
||||
_.each(expected, function (key) {
|
||||
expect(keys).to.contain(key);
|
||||
});
|
||||
});
|
||||
|
||||
it('should use the legendQuantizer', function () {
|
||||
var spy = sinon.spy(markerLayer, '_legendQuantizer');
|
||||
var style = markerLayer.applyShadingStyle(100);
|
||||
expect(spy.callCount).to.equal(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('showTooltip', function () {
|
||||
it('should use the tooltip formatter', function () {
|
||||
var content;
|
||||
var sample = _.sample(mapData.features);
|
||||
|
||||
var stub = sinon.stub(markerLayer, '_tooltipFormatter', function (val) {
|
||||
return;
|
||||
});
|
||||
|
||||
markerLayer._showTooltip(sample);
|
||||
|
||||
expect(stub.callCount).to.equal(1);
|
||||
expect(stub.firstCall.calledWith(sample)).to.be(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('addLegend', function () {
|
||||
var addToSpy;
|
||||
var leafletControlStub;
|
||||
|
||||
beforeEach(function () {
|
||||
addToSpy = sinon.spy();
|
||||
leafletControlStub = sinon.stub(L, 'control', function (options) {
|
||||
return {
|
||||
addTo: addToSpy
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
it('should do nothing if there is already a legend', function () {
|
||||
markerLayer._legend = { legend: 'exists' }; // anything truthy
|
||||
|
||||
markerLayer.addLegend();
|
||||
expect(leafletControlStub.callCount).to.equal(0);
|
||||
});
|
||||
|
||||
it('should create a leaflet control', function () {
|
||||
markerLayer.addLegend();
|
||||
expect(leafletControlStub.callCount).to.equal(1);
|
||||
expect(addToSpy.callCount).to.equal(1);
|
||||
expect(addToSpy.firstCall.calledWith(markerLayer.map)).to.be(true);
|
||||
expect(markerLayer._legend).to.have.property('onAdd');
|
||||
});
|
||||
|
||||
it('should use the value formatter', function () {
|
||||
var formatterSpy = sinon.spy(markerLayer, '_valueFormatter');
|
||||
// called twice for every legend color defined
|
||||
var expectedCallCount = markerLayer._legendColors.length * 2;
|
||||
|
||||
markerLayer.addLegend();
|
||||
var legend = markerLayer._legend.onAdd();
|
||||
|
||||
expect(formatterSpy.callCount).to.equal(expectedCallCount);
|
||||
expect(legend).to.be.a(HTMLDivElement);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Shaded Circles', function () {
|
||||
beforeEach(function () {
|
||||
module('MarkerFactory');
|
||||
inject(function (Private) {
|
||||
var MarkerClass = Private(require('components/vislib/visualizations/marker_types/shaded_circles'));
|
||||
markerLayer = createMarker(MarkerClass);
|
||||
});
|
||||
});
|
||||
|
||||
describe('geohashMinDistance method', function () {
|
||||
it('should return a finite number', function () {
|
||||
var sample = _.sample(mapData.features);
|
||||
var distance = markerLayer._geohashMinDistance(sample);
|
||||
|
||||
expect(distance).to.be.a('number');
|
||||
expect(_.isFinite(distance)).to.be(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Scaled Circles', function () {
|
||||
var zoom;
|
||||
|
||||
beforeEach(function () {
|
||||
module('MarkerFactory');
|
||||
|
||||
zoom = _.random(1, 18);
|
||||
sinon.stub(mockMap, 'getZoom', _.constant(zoom));
|
||||
|
||||
inject(function (Private) {
|
||||
var MarkerClass = Private(require('components/vislib/visualizations/marker_types/scaled_circles'));
|
||||
markerLayer = createMarker(MarkerClass);
|
||||
});
|
||||
});
|
||||
|
||||
describe('radiusScale method', function () {
|
||||
var valueArray = [10, 20, 30, 40, 50, 60];
|
||||
var max = _.max(valueArray);
|
||||
var prev = -1;
|
||||
|
||||
it('should return 0 for value of 0', function () {
|
||||
expect(markerLayer._radiusScale(0)).to.equal(0);
|
||||
});
|
||||
|
||||
it('should return a scaled value for negative and positive numbers', function () {
|
||||
var upperBound = markerLayer._radiusScale(max);
|
||||
var results = [];
|
||||
|
||||
function roundValue(value) {
|
||||
// round number to 6 decimal places
|
||||
var r = Math.pow(10, 6);
|
||||
return Math.round(value * r) / r;
|
||||
}
|
||||
|
||||
_.each(valueArray, function (value, i) {
|
||||
var ratio = Math.pow(value / max, 0.5);
|
||||
var comparison = ratio * upperBound;
|
||||
var radius = markerLayer._radiusScale(value);
|
||||
var negRadius = markerLayer._radiusScale(value * -1);
|
||||
results.push(radius);
|
||||
|
||||
expect(negRadius).to.equal(radius);
|
||||
expect(roundValue(radius)).to.equal(roundValue(comparison));
|
||||
|
||||
// check that the radius is getting larger
|
||||
if (i > 0) {
|
||||
expect(radius).to.be.above(results[i - 1]);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Heatmaps', function () {
|
||||
beforeEach(function () {
|
||||
module('MarkerFactory');
|
||||
|
||||
inject(function (Private) {
|
||||
var MarkerClass = Private(require('components/vislib/visualizations/marker_types/heatmap'));
|
||||
markerLayer = createMarker(MarkerClass);
|
||||
});
|
||||
});
|
||||
|
||||
describe('dataToHeatArray', function () {
|
||||
var max;
|
||||
|
||||
beforeEach(function () {
|
||||
max = mapData.properties.allmax;
|
||||
});
|
||||
|
||||
it('should return an array or values for each feature', function () {
|
||||
var arr = markerLayer._dataToHeatArray(max);
|
||||
expect(arr).to.be.an('array');
|
||||
expect(arr).to.have.length(mapData.features.length);
|
||||
|
||||
});
|
||||
|
||||
it('should return an array item with lat, lng, metric for each feature', function () {
|
||||
_.times(3, function () {
|
||||
var arr = markerLayer._dataToHeatArray(max);
|
||||
var index = _.random(mapData.features.length - 1);
|
||||
var feature = mapData.features[index];
|
||||
var featureValue = feature.properties.value;
|
||||
var featureArr = feature.geometry.coordinates.slice(0).concat(featureValue);
|
||||
expect(arr[index]).to.eql(featureArr);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return an array item with lat, lng, normalized metric for each feature', function () {
|
||||
_.times(5, function () {
|
||||
markerLayer._attr.heatNormalizeData = true;
|
||||
|
||||
var arr = markerLayer._dataToHeatArray(max);
|
||||
var index = _.random(mapData.features.length - 1);
|
||||
var feature = mapData.features[index];
|
||||
var featureValue = parseInt(feature.properties.value / max * 100);
|
||||
var featureArr = feature.geometry.coordinates.slice(0).concat(featureValue);
|
||||
expect(arr[index]).to.eql(featureArr);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('tooltipProximity', function () {
|
||||
it('should return true if feature is close enough to event latlng', function () {
|
||||
_.times(5, function () {
|
||||
var feature = _.sample(mapData.features);
|
||||
var point = markerLayer._getLatLng(feature);
|
||||
var arr = markerLayer._tooltipProximity(point, feature);
|
||||
expect(arr).to.be(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return false if feature is not close enough to event latlng', function () {
|
||||
_.times(5, function () {
|
||||
var feature = _.sample(mapData.features);
|
||||
var point = L.latLng(90, -180);
|
||||
var arr = markerLayer._tooltipProximity(point, feature);
|
||||
expect(arr).to.be(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('nearestFeature', function () {
|
||||
it('should return nearest geoJson feature object', function () {
|
||||
_.times(5, function () {
|
||||
var feature = _.sample(mapData.features);
|
||||
var point = markerLayer._getLatLng(feature);
|
||||
var nearestPoint = markerLayer._nearestFeature(point);
|
||||
expect(nearestPoint).to.equal(feature);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getLatLng', function () {
|
||||
it('should return a leaflet latLng object', function () {
|
||||
var feature = _.sample(mapData.features);
|
||||
var latLng = markerLayer._getLatLng(feature);
|
||||
var compare = L.latLng(feature.geometry.coordinates.slice(0).reverse());
|
||||
expect(latLng).to.eql(compare);
|
||||
});
|
||||
|
||||
it('should memoize the result', function () {
|
||||
var spy = sinon.spy(L, 'latLng');
|
||||
var feature = _.sample(mapData.features);
|
||||
|
||||
markerLayer._getLatLng(feature);
|
||||
expect(spy.callCount).to.be(1);
|
||||
|
||||
markerLayer._getLatLng(feature);
|
||||
expect(spy.callCount).to.be(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
});
|
137
test/unit/specs/vislib/visualizations/tile_maps/tile_map.js
Normal file
137
test/unit/specs/vislib/visualizations/tile_maps/tile_map.js
Normal file
|
@ -0,0 +1,137 @@
|
|||
define(function (require) {
|
||||
var angular = require('angular');
|
||||
var _ = require('lodash');
|
||||
var $ = require('jquery');
|
||||
var sinon = require('test_utils/auto_release_sinon');
|
||||
|
||||
var geoJsonData = require('vislib_fixtures/mock_data/geohash/_geo_json');
|
||||
var MockMap = require('fixtures/tilemap_map');
|
||||
var mockChartEl = $('<div>');
|
||||
|
||||
var TileMap;
|
||||
var extentsStub;
|
||||
|
||||
|
||||
angular.module('TileMapFactory', ['kibana']);
|
||||
|
||||
function createTileMap(handler, chartEl, chartData) {
|
||||
handler = handler || {};
|
||||
chartEl = chartEl || mockChartEl;
|
||||
chartData = chartData || geoJsonData;
|
||||
|
||||
var tilemap = new TileMap(handler, chartEl, chartData);
|
||||
return tilemap;
|
||||
}
|
||||
|
||||
describe('TileMap Tests', function () {
|
||||
var tilemap;
|
||||
|
||||
beforeEach(function () {
|
||||
module('TileMapFactory');
|
||||
inject(function (Private) {
|
||||
Private.stub(require('components/vislib/visualizations/_map'), MockMap);
|
||||
TileMap = Private(require('components/vislib/visualizations/tile_map'));
|
||||
extentsStub = sinon.stub(TileMap.prototype, '_appendGeoExtents', _.noop);
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(function () {
|
||||
tilemap = createTileMap();
|
||||
});
|
||||
|
||||
it('should inherit props from chartData', function () {
|
||||
_.each(geoJsonData, function (val, prop) {
|
||||
expect(tilemap).to.have.property(prop, val);
|
||||
});
|
||||
});
|
||||
|
||||
it('should append geoExtents', function () {
|
||||
expect(extentsStub.callCount).to.equal(1);
|
||||
});
|
||||
|
||||
describe('draw', function () {
|
||||
it('should return a function', function () {
|
||||
expect(tilemap.draw()).to.be.a('function');
|
||||
});
|
||||
|
||||
it('should call destroy for clean state', function () {
|
||||
var destroySpy = sinon.spy(tilemap, 'destroy');
|
||||
tilemap.draw();
|
||||
expect(destroySpy.callCount).to.equal(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('appendMap', function () {
|
||||
var $selection;
|
||||
|
||||
beforeEach(function () {
|
||||
$selection = $('<div>');
|
||||
expect(tilemap.maps).to.have.length(0);
|
||||
tilemap._appendMap($selection);
|
||||
});
|
||||
|
||||
it('should add the tilemap class', function () {
|
||||
expect($selection.hasClass('tilemap')).to.equal(true);
|
||||
});
|
||||
|
||||
it('should append maps and required controls', function () {
|
||||
expect(tilemap.maps).to.have.length(1);
|
||||
var map = tilemap.maps[0];
|
||||
expect(map.addTitle.callCount).to.equal(0);
|
||||
expect(map.addFitControl.callCount).to.equal(1);
|
||||
expect(map.addBoundingControl.callCount).to.equal(1);
|
||||
});
|
||||
|
||||
it('should only add controls if data exists', function () {
|
||||
var noData = {
|
||||
geoJson: {
|
||||
features: [],
|
||||
properties: {},
|
||||
hits: 20
|
||||
}
|
||||
};
|
||||
var tilemap = createTileMap(null, null, noData);
|
||||
|
||||
tilemap._appendMap($selection);
|
||||
expect(tilemap.maps).to.have.length(1);
|
||||
|
||||
var map = tilemap.maps[0];
|
||||
expect(map.addTitle.callCount).to.equal(0);
|
||||
expect(map.addFitControl.callCount).to.equal(0);
|
||||
expect(map.addBoundingControl.callCount).to.equal(0);
|
||||
});
|
||||
|
||||
it('should append title if set in the data object', function () {
|
||||
var mapTitle = 'Test Title';
|
||||
var tilemap = createTileMap(null, null, _.assign({ title: mapTitle }, geoJsonData));
|
||||
tilemap._appendMap($selection);
|
||||
var map = tilemap.maps[0];
|
||||
|
||||
expect(map.addTitle.callCount).to.equal(1);
|
||||
expect(map.addTitle.firstCall.calledWith(mapTitle)).to.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('destroy', function () {
|
||||
var maps = [];
|
||||
var mapCount = 5;
|
||||
|
||||
beforeEach(function () {
|
||||
_.times(mapCount, function () {
|
||||
maps.push(new MockMap());
|
||||
});
|
||||
tilemap.maps = maps;
|
||||
expect(tilemap.maps).to.have.length(mapCount);
|
||||
tilemap.destroy();
|
||||
});
|
||||
|
||||
it('should destroy all the maps', function () {
|
||||
expect(tilemap.maps).to.have.length(0);
|
||||
expect(maps).to.have.length(mapCount);
|
||||
_.each(maps, function (map) {
|
||||
expect(map.destroy.callCount).to.equal(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Add table
Add a link
Reference in a new issue