Merge branch 'master' of github.com:elastic/kibana into apps/home

This commit is contained in:
Spencer Alger 2015-07-08 18:05:09 -07:00
commit 3d82d404cb
28 changed files with 2172 additions and 1552 deletions

View file

@ -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();
}

View file

@ -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)">

View file

@ -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>

View file

@ -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) {
}
];
});
});
}
}
};
});

View file

@ -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>

View file

@ -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 = [];

View file

@ -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) {

View file

@ -177,6 +177,10 @@ kbn-settings-indices {
margin: 5px 0;
text-align: right;
}
p.text-center {
padding-top: 1em;
}
}

View file

@ -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
}
});

View file

@ -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;
}

View file

@ -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)
);

View file

@ -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
*

View file

@ -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);
}

View 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> &mdash; ' +
'Map data &copy; <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;
};
});

View file

@ -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;
};
});

View file

@ -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;
};
});

View 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;
};
});

View file

@ -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;
};
});

View file

@ -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;
};
});

View file

@ -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> &mdash; ' +
'Map data &copy; <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;
};
});

View 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;
});

View file

@ -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]);
}
});
});
});
});

View file

@ -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
]
]
}

View file

@ -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);
});
});
});
});
});

View file

@ -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);
});
});
});
});
});
});

View 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]);
});
});
});
});
});

View 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);
});
});
});
});
});

View 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);
});
});
});
});
});