mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Merge] Merged with latest master
This commit is contained in:
commit
23a22d5aae
46 changed files with 2508 additions and 1871 deletions
|
@ -283,7 +283,7 @@ A green oval with the filter definition displays right under the query box:
|
|||
image::images/tutorial-visualize-map-3.png[]
|
||||
|
||||
Hover on the filter to display the controls to toggle, pin, invert, or delete the filter. Save this chart with the name
|
||||
_Bar Example_.
|
||||
_Map Example_.
|
||||
|
||||
Finally, we're going to define a sample Markdown widget to display on our dashboard. Click on *New Visualization*, then
|
||||
*Markdown widget*, to display a very simple Markdown entry field:
|
||||
|
@ -323,4 +323,4 @@ clicking the *Share* button to display HTML embedding code as well as a direct l
|
|||
=== Wrapping Up
|
||||
|
||||
Now that you've handled the basic aspects of Kibana's functionality, you're ready to explore Kibana in further detail.
|
||||
Take a look at the rest of the documentation for more details!
|
||||
Take a look at the rest of the documentation for more details!
|
||||
|
|
|
@ -9,8 +9,7 @@
|
|||
the Visualize tab to Discover to a Dashboard.
|
||||
* {k4pull}2731[Pull Request 2731]: Field formatting options now supported in Settings.
|
||||
* {k4pull}3154[Pull Request 3154]: New chart: Bubble chart, derived from the basic line chart.
|
||||
* {k4pull}3212[Pull Request 3212]: You can now install Kibana on Linux with a package manager such as `yum` or
|
||||
`apt-get`.
|
||||
* {k4pull}3212[Pull Request 3212]: You can now install Kibana on Linux with a package manager such as `apt-get`.
|
||||
* {k4pull}3271[Pull Request 3271] and {k4pull}3262[3262]: New aggregations: IPv4 and Date range aggregations enable
|
||||
you to specify buckets for these qualities.
|
||||
* {k4pull}3290[Pull Request 3290]: You can select a time interval for the Discover display of time series data.
|
||||
|
|
|
@ -95,7 +95,7 @@
|
|||
"husky": "^0.8.1",
|
||||
"istanbul": "^0.3.15",
|
||||
"jade": "^1.8.2",
|
||||
"license-checker": "3.0.3",
|
||||
"license-checker": "^3.1.0",
|
||||
"libesvm": "^1.0.1",
|
||||
"load-grunt-config": "^0.7.0",
|
||||
"marked": "^0.3.3",
|
||||
|
|
|
@ -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
|
||||
}
|
||||
});
|
||||
|
|
|
@ -11,10 +11,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;
|
||||
}
|
||||
|
@ -32,4 +40,4 @@ kbn-agg-table-group {
|
|||
padding: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,7 +28,7 @@ define(function (require) {
|
|||
title: 'Terms',
|
||||
makeLabel: function (agg) {
|
||||
var params = agg.params;
|
||||
return params.order.display + ' ' + params.size + ' ' + params.field.displayName;
|
||||
return params.field.displayName + ': ' + params.order.display;
|
||||
},
|
||||
createFilter: createFilter,
|
||||
params: [
|
||||
|
@ -53,21 +53,6 @@ define(function (require) {
|
|||
name: 'size',
|
||||
default: 5
|
||||
},
|
||||
{
|
||||
name: 'order',
|
||||
type: 'optioned',
|
||||
default: 'desc',
|
||||
editor: require('text!components/agg_types/controls/order_and_size.html'),
|
||||
options: [
|
||||
{ display: 'Top', val: 'desc' },
|
||||
{ display: 'Bottom', val: 'asc' }
|
||||
],
|
||||
write: _.noop // prevent default write, it's handled by orderAgg
|
||||
},
|
||||
{
|
||||
name: 'orderBy',
|
||||
write: _.noop // prevent default write, it's handled by orderAgg
|
||||
},
|
||||
{
|
||||
name: 'orderAgg',
|
||||
type: AggConfig,
|
||||
|
@ -122,6 +107,12 @@ define(function (require) {
|
|||
// we aren't creating a custom aggConfig
|
||||
if (!orderBy || orderBy !== 'custom') {
|
||||
params.orderAgg = null;
|
||||
|
||||
if (orderBy === '_term') {
|
||||
params.orderBy = '_term';
|
||||
return;
|
||||
}
|
||||
|
||||
// ensure that orderBy is set to a valid agg
|
||||
if (!_.find($scope.responseValueAggs, { id: orderBy })) {
|
||||
params.orderBy = null;
|
||||
|
@ -146,7 +137,12 @@ define(function (require) {
|
|||
output.params.valueType = agg.field().type === 'number' ? 'float' : agg.field().type;
|
||||
}
|
||||
|
||||
if (!orderAgg || orderAgg.type.name === 'count') {
|
||||
if (!orderAgg) {
|
||||
order[agg.params.orderBy || '_count'] = dir;
|
||||
return;
|
||||
}
|
||||
|
||||
if (orderAgg.type.name === 'count') {
|
||||
order._count = dir;
|
||||
return;
|
||||
}
|
||||
|
@ -159,6 +155,21 @@ define(function (require) {
|
|||
output.subAggs = (output.subAggs || []).concat(orderAgg);
|
||||
order[orderAggId] = dir;
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'order',
|
||||
type: 'optioned',
|
||||
default: 'desc',
|
||||
editor: require('text!components/agg_types/controls/order_and_size.html'),
|
||||
options: [
|
||||
{ display: 'Descending', val: 'desc' },
|
||||
{ display: 'Ascending', val: 'asc' }
|
||||
],
|
||||
write: _.noop // prevent default write, it's handled by orderAgg
|
||||
},
|
||||
{
|
||||
name: 'orderBy',
|
||||
write: _.noop // prevent default write, it's handled by orderAgg
|
||||
}
|
||||
]
|
||||
});
|
||||
|
|
|
@ -31,7 +31,7 @@
|
|||
<div class="hintbox" ng-if="!indexedFields.length">
|
||||
<p>
|
||||
<i class="fa fa-danger text-danger"></i>
|
||||
<strong>No Compatible Fields:</strong> The "{{ vis.indexPattern.id }}" index pattern does not any of the following field types: {{ agg.type.params.byName.field.filterFieldTypes | commaList:false }}
|
||||
<strong>No Compatible Fields:</strong> The "{{ vis.indexPattern.id }}" index pattern does not contain any of the following field types: {{ agg.type.params.byName.field.filterFieldTypes | commaList:false }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -15,6 +15,9 @@
|
|||
<option value="custom" ng-selected="agg.params.orderBy === 'custom'">
|
||||
Custom Metric
|
||||
</option>
|
||||
<option value="_term" ng-selected="agg.params.orderBy === '_term'">
|
||||
Term
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div ng-show="agg.params.orderAgg" class="vis-editor-agg-order-agg">
|
||||
|
|
|
@ -128,6 +128,14 @@ define(function () {
|
|||
'format:currency:defaultPattern': {
|
||||
type: 'string',
|
||||
value: '($0,0.[00])'
|
||||
},
|
||||
'timepicker:timeDefaults': {
|
||||
type: 'json',
|
||||
value: JSON.stringify({
|
||||
from: 'now-15m',
|
||||
to: 'now',
|
||||
mode: 'quick'
|
||||
}, null, 2)
|
||||
}
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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)
|
||||
);
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
define(function (require) {
|
||||
require('modules')
|
||||
.get('kibana')
|
||||
.service('timefilter', function (Private, globalState, $rootScope) {
|
||||
.service('timefilter', function (Private, globalState, $rootScope, config) {
|
||||
|
||||
var _ = require('lodash');
|
||||
var angular = require('angular');
|
||||
|
@ -27,11 +27,7 @@
|
|||
|
||||
self.enabled = false;
|
||||
|
||||
var timeDefaults = {
|
||||
from: 'now-15m',
|
||||
to: 'now',
|
||||
mode: 'quick'
|
||||
};
|
||||
var timeDefaults = config.get('timepicker:timeDefaults');
|
||||
|
||||
var refreshIntervalDefaults = {
|
||||
display: 'Off',
|
||||
|
|
|
@ -32,7 +32,11 @@ define(function (require) {
|
|||
* @returns {D3.Selection|D3.Transition.Transition} DOM element with chart titles
|
||||
*/
|
||||
ChartTitle.prototype.render = function () {
|
||||
return d3.select(this.el).selectAll('.chart-title').call(this.draw());
|
||||
var el = d3.select(this.el).select('.chart-title').node();
|
||||
var width = el ? el.clientWidth : 0;
|
||||
var height = el ? el.clientHeight : 0;
|
||||
|
||||
return d3.select(this.el).selectAll('.chart-title').call(this.draw(width, height));
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -89,33 +93,21 @@ define(function (require) {
|
|||
* @method draw
|
||||
* @returns {Function} Appends chart titles to a D3 selection
|
||||
*/
|
||||
ChartTitle.prototype.draw = function () {
|
||||
ChartTitle.prototype.draw = function (width, height) {
|
||||
var self = this;
|
||||
|
||||
return function (selection) {
|
||||
selection.each(function () {
|
||||
var div = d3.select(this);
|
||||
var dataType = this.parentNode.__data__.rows ? 'rows' : 'columns';
|
||||
var width = $(this).width();
|
||||
var height = $(this).height();
|
||||
var size = dataType === 'rows' ? height : width;
|
||||
var txtHtOffset = 11;
|
||||
|
||||
self.validateWidthandHeight(width, height);
|
||||
|
||||
div.append('svg')
|
||||
.attr('width', function () {
|
||||
if (dataType === 'rows') {
|
||||
return height;
|
||||
}
|
||||
return width;
|
||||
})
|
||||
.attr('height', function () {
|
||||
if (dataType === 'rows') {
|
||||
return width;
|
||||
}
|
||||
return height;
|
||||
})
|
||||
.attr('width', width)
|
||||
.attr('height', height)
|
||||
.append('text')
|
||||
.attr('transform', function () {
|
||||
if (dataType === 'rows') {
|
||||
|
|
|
@ -243,6 +243,23 @@ define(function (require) {
|
|||
return visData;
|
||||
};
|
||||
|
||||
/**
|
||||
* get min and max for all cols, rows of data
|
||||
*
|
||||
* @method getMaxMin
|
||||
* @return {Object}
|
||||
*/
|
||||
Data.prototype.getGeoExtents = function () {
|
||||
var visData = this.getVisData();
|
||||
|
||||
return _.reduce(_.pluck(visData, 'geoJson.properties'), function (minMax, props) {
|
||||
return {
|
||||
min: Math.min(props.min, minMax.min),
|
||||
max: Math.max(props.max, minMax.max)
|
||||
};
|
||||
}, { min: Infinity, max: -Infinity });
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns array of chart data objects for pie data objects
|
||||
*
|
||||
|
|
|
@ -27,7 +27,7 @@ define(function (require) {
|
|||
|
||||
var events = this.events = new Dispatch(handler);
|
||||
|
||||
if (handler._attr.addTooltip) {
|
||||
if (_.get(this.handler, '_attr.addTooltip')) {
|
||||
var $el = this.handler.el;
|
||||
var formatter = this.handler.data.get('tooltipFormatter');
|
||||
|
||||
|
@ -35,7 +35,7 @@ define(function (require) {
|
|||
this.tooltip = new Tooltip('chart', $el, formatter, events);
|
||||
}
|
||||
|
||||
this._attr = _.defaults(handler._attr || {}, {});
|
||||
this._attr = _.defaults(this.handler._attr || {}, {});
|
||||
this._addIdentifier = _.bind(this._addIdentifier, this);
|
||||
}
|
||||
|
||||
|
|
303
src/kibana/components/vislib/visualizations/_map.js
Normal file
303
src/kibana/components/vislib/visualizations/_map.js
Normal file
|
@ -0,0 +1,303 @@
|
|||
define(function (require) {
|
||||
return function MapFactory(Private) {
|
||||
var _ = require('lodash');
|
||||
var $ = require('jquery');
|
||||
var L = require('leaflet');
|
||||
require('leaflet-draw');
|
||||
|
||||
var defaultMapZoom = 2;
|
||||
var defaultMapCenter = [15, 5];
|
||||
var defaultMarkerType = 'Scaled Circle Markers';
|
||||
|
||||
var mapTiles = {
|
||||
url: 'https://otile{s}-s.mqcdn.com/tiles/1.0.0/map/{z}/{x}/{y}.jpeg',
|
||||
options: {
|
||||
attribution: 'Tiles by <a href="http://www.mapquest.com/">MapQuest</a> — ' +
|
||||
'Map data © <a href="http://openstreetmap.org">OpenStreetMap</a> contributors, ' +
|
||||
'<a href="http://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>',
|
||||
subdomains: '1234'
|
||||
}
|
||||
};
|
||||
|
||||
var markerTypes = {
|
||||
'Scaled Circle Markers': Private(require('components/vislib/visualizations/marker_types/scaled_circles')),
|
||||
'Shaded Circle Markers': Private(require('components/vislib/visualizations/marker_types/shaded_circles')),
|
||||
'Shaded Geohash Grid': Private(require('components/vislib/visualizations/marker_types/geohash_grid')),
|
||||
'Heatmap': Private(require('components/vislib/visualizations/marker_types/heatmap')),
|
||||
};
|
||||
|
||||
/**
|
||||
* Tile Map Maps
|
||||
*
|
||||
* @class Map
|
||||
* @constructor
|
||||
* @param container {HTML Element} Element to render map into
|
||||
* @param chartData {Object} Elasticsearch query results for this map
|
||||
* @param params {Object} Parameters used to build a map
|
||||
*/
|
||||
function Map(container, chartData, params) {
|
||||
this._container = $(container).get(0);
|
||||
this._chartData = chartData;
|
||||
|
||||
// keep a reference to all of the optional params
|
||||
this._events = _.get(params, 'events');
|
||||
this._markerType = markerTypes[params.markerType] ? params.markerType : defaultMarkerType;
|
||||
this._valueFormatter = params.valueFormatter || _.identity;
|
||||
this._tooltipFormatter = params.tooltipFormatter || _.identity;
|
||||
this._geoJson = _.get(this._chartData, 'geoJson');
|
||||
this._attr = params.attr || {};
|
||||
|
||||
var mapOptions = {
|
||||
minZoom: 1,
|
||||
maxZoom: 18,
|
||||
noWrap: true,
|
||||
maxBounds: L.latLngBounds([-90, -220], [90, 220]),
|
||||
scrollWheelZoom: false,
|
||||
fadeAnimation: false,
|
||||
};
|
||||
|
||||
this._createMap(mapOptions);
|
||||
}
|
||||
|
||||
Map.prototype.addBoundingControl = function () {
|
||||
if (this._boundingControl) return;
|
||||
|
||||
var self = this;
|
||||
var drawOptions = { draw: {} };
|
||||
|
||||
_.each(['polyline', 'polygon', 'circle', 'marker', 'rectangle'], function (drawShape) {
|
||||
if (self._events && !self._events.listenerCount(drawShape)) {
|
||||
drawOptions.draw[drawShape] = false;
|
||||
} else {
|
||||
drawOptions.draw[drawShape] = {
|
||||
shapeOptions: {
|
||||
stroke: false,
|
||||
color: '#000'
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
this._boundingControl = new L.Control.Draw(drawOptions);
|
||||
this.map.addControl(this._boundingControl);
|
||||
};
|
||||
|
||||
Map.prototype.addFitControl = function () {
|
||||
if (this._fitControl) return;
|
||||
|
||||
var self = this;
|
||||
var fitContainer = L.DomUtil.create('div', 'leaflet-control leaflet-bar leaflet-control-fit');
|
||||
|
||||
// Add button to fit container to points
|
||||
var FitControl = L.Control.extend({
|
||||
options: {
|
||||
position: 'topleft'
|
||||
},
|
||||
onAdd: function (map) {
|
||||
$(fitContainer).html('<a class="fa fa-crop" href="#" title="Fit Data Bounds"></a>')
|
||||
.on('click', function (e) {
|
||||
e.preventDefault();
|
||||
self._fitBounds();
|
||||
});
|
||||
|
||||
return fitContainer;
|
||||
},
|
||||
onRemove: function (map) {
|
||||
$(fitContainer).off('click');
|
||||
}
|
||||
});
|
||||
|
||||
this._fitControl = new FitControl();
|
||||
this.map.addControl(this._fitControl);
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds label div to each map when data is split
|
||||
*
|
||||
* @method addTitle
|
||||
* @param mapLabel {String}
|
||||
* @return {undefined}
|
||||
*/
|
||||
Map.prototype.addTitle = function (mapLabel) {
|
||||
if (this._label) return;
|
||||
|
||||
var label = this._label = L.control();
|
||||
|
||||
label.onAdd = function () {
|
||||
this._div = L.DomUtil.create('div', 'tilemap-info tilemap-label');
|
||||
this.update();
|
||||
return this._div;
|
||||
};
|
||||
label.update = function () {
|
||||
this._div.innerHTML = '<h2>' + _.escape(mapLabel) + '</h2>';
|
||||
};
|
||||
|
||||
// label.addTo(this.map);
|
||||
this.map.addControl(label);
|
||||
};
|
||||
|
||||
/**
|
||||
* remove css class for desat filters on map tiles
|
||||
*
|
||||
* @method saturateTiles
|
||||
* @return undefined
|
||||
*/
|
||||
Map.prototype.saturateTiles = function () {
|
||||
if (!this._attr.isDesaturated) {
|
||||
$('img.leaflet-tile-loaded').addClass('filters-off');
|
||||
}
|
||||
};
|
||||
|
||||
Map.prototype.updateSize = function () {
|
||||
this.map.invalidateSize({
|
||||
debounceMoveend: true
|
||||
});
|
||||
};
|
||||
|
||||
Map.prototype.destroy = function () {
|
||||
if (this._label) this._label.removeFrom(this.map);
|
||||
if (this._fitControl) this._fitControl.removeFrom(this.map);
|
||||
if (this._boundingControl) this._boundingControl.removeFrom(this.map);
|
||||
if (this._markers) this._markers.destroy();
|
||||
this.map.remove();
|
||||
this.map = undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Switch type of data overlay for map:
|
||||
* creates featurelayer from mapData (geoJson)
|
||||
*
|
||||
* @method _addMarkers
|
||||
*/
|
||||
Map.prototype._addMarkers = function () {
|
||||
if (!this._geoJson) return;
|
||||
if (this._markers) this._markers.destroy();
|
||||
|
||||
this._markers = this._createMarkers({
|
||||
tooltipFormatter: this._tooltipFormatter,
|
||||
valueFormatter: this._valueFormatter,
|
||||
attr: this._attr
|
||||
});
|
||||
|
||||
if (this._geoJson.features.length > 1) {
|
||||
this._markers.addLegend();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Create the marker instance using the given options
|
||||
*
|
||||
* @method _createMarkers
|
||||
* @param options {Object} options to give to marker class
|
||||
* @return {Object} marker layer
|
||||
*/
|
||||
Map.prototype._createMarkers = function (options) {
|
||||
var MarkerType = markerTypes[this._markerType];
|
||||
return new MarkerType(this.map, this._geoJson, options);
|
||||
};
|
||||
|
||||
Map.prototype._attachEvents = function () {
|
||||
var self = this;
|
||||
var saturateTiles = self.saturateTiles.bind(self);
|
||||
|
||||
this._tileLayer.on('tileload', saturateTiles);
|
||||
|
||||
this.map.on('unload', function () {
|
||||
self._tileLayer.off('tileload', saturateTiles);
|
||||
});
|
||||
|
||||
this.map.on('moveend', function setZoomCenter(ev) {
|
||||
// update internal center and zoom references
|
||||
self._mapCenter = self.map.getCenter();
|
||||
self._mapZoom = self.map.getZoom();
|
||||
self._addMarkers();
|
||||
|
||||
if (!self._events) return;
|
||||
|
||||
self._events.emit('mapMoveEnd', {
|
||||
chart: self._chartData,
|
||||
map: self.map,
|
||||
center: self._mapCenter,
|
||||
zoom: self._mapZoom,
|
||||
});
|
||||
});
|
||||
|
||||
this.map.on('draw:created', function (e) {
|
||||
var drawType = e.layerType;
|
||||
if (!self._events || !self._events.listenerCount(drawType)) return;
|
||||
|
||||
// TODO: Different drawTypes need differ info. Need a switch on the object creation
|
||||
var bounds = e.layer.getBounds();
|
||||
|
||||
self._events.emit(drawType, {
|
||||
e: e,
|
||||
chart: self._chartData,
|
||||
bounds: {
|
||||
top_left: {
|
||||
lat: bounds.getNorthWest().lat,
|
||||
lon: bounds.getNorthWest().lng
|
||||
},
|
||||
bottom_right: {
|
||||
lat: bounds.getSouthEast().lat,
|
||||
lon: bounds.getSouthEast().lng
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
this.map.on('zoomend', function () {
|
||||
self._mapZoom = self.map.getZoom();
|
||||
if (!self._events) return;
|
||||
|
||||
self._events.emit('mapZoomEnd', {
|
||||
chart: self._chartData,
|
||||
map: self.map,
|
||||
zoom: self._mapZoom,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
Map.prototype._createMap = function (mapOptions) {
|
||||
if (this.map) this.destroy();
|
||||
|
||||
// get center and zoom from mapdata, or use defaults
|
||||
this._mapCenter = _.get(this._geoJson, 'properties.center') || defaultMapCenter;
|
||||
this._mapZoom = _.get(this._geoJson, 'properties.zoom') || defaultMapZoom;
|
||||
|
||||
// add map tiles layer, using the mapTiles object settings
|
||||
this._tileLayer = L.tileLayer(mapTiles.url, mapTiles.options);
|
||||
|
||||
// append tile layers, center and zoom to the map options
|
||||
mapOptions.layers = this._tileLayer;
|
||||
mapOptions.center = this._mapCenter;
|
||||
mapOptions.zoom = this._mapZoom;
|
||||
|
||||
this.map = L.map(this._container, mapOptions);
|
||||
this._attachEvents();
|
||||
this._addMarkers();
|
||||
};
|
||||
|
||||
/**
|
||||
* zoom map to fit all features in featureLayer
|
||||
*
|
||||
* @method _fitBounds
|
||||
* @param map {Leaflet Object}
|
||||
* @return {boolean}
|
||||
*/
|
||||
Map.prototype._fitBounds = function () {
|
||||
this.map.fitBounds(this._getDataRectangles());
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the Rectangles representing the geohash grid
|
||||
*
|
||||
* @return {LatLngRectangles[]}
|
||||
*/
|
||||
Map.prototype._getDataRectangles = function () {
|
||||
if (!this._geoJson) return [];
|
||||
return _.pluck(this._geoJson.features, 'properties.rectangle');
|
||||
};
|
||||
|
||||
return Map;
|
||||
};
|
||||
});
|
|
@ -0,0 +1,265 @@
|
|||
define(function (require) {
|
||||
return function MarkerFactory(d3) {
|
||||
var _ = require('lodash');
|
||||
var $ = require('jquery');
|
||||
var L = require('leaflet');
|
||||
|
||||
/**
|
||||
* Base map marker overlay, all other markers inherit from this class
|
||||
*
|
||||
* @param map {Leaflet Object}
|
||||
* @param geoJson {geoJson Object}
|
||||
* @param params {Object}
|
||||
*/
|
||||
function BaseMarker(map, geoJson, params) {
|
||||
this.map = map;
|
||||
this.geoJson = geoJson;
|
||||
this.popups = [];
|
||||
|
||||
this._tooltipFormatter = params.tooltipFormatter || _.identity;
|
||||
this._valueFormatter = params.valueFormatter || _.identity;
|
||||
this._attr = params.attr || {};
|
||||
|
||||
// set up the default legend colors
|
||||
this.quantizeLegendColors();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds legend div to each map when data is split
|
||||
* uses d3 scale from BaseMarker.prototype.quantizeLegendColors
|
||||
*
|
||||
* @method addLegend
|
||||
* @return {undefined}
|
||||
*/
|
||||
BaseMarker.prototype.addLegend = function () {
|
||||
// ensure we only ever create 1 legend
|
||||
if (this._legend) return;
|
||||
|
||||
var self = this;
|
||||
|
||||
// create the legend control, keep a reference
|
||||
self._legend = L.control({position: 'bottomright'});
|
||||
|
||||
self._legend.onAdd = function () {
|
||||
// creates all the neccessary DOM elements for the control, adds listeners
|
||||
// on relevant map events, and returns the element containing the control
|
||||
var $div = $('<div>').addClass('tilemap-legend');
|
||||
|
||||
_.each(self._legendColors, function (color, i) {
|
||||
var labelText = self._legendQuantizer
|
||||
.invertExtent(color)
|
||||
.map(self._valueFormatter)
|
||||
.join(' – ');
|
||||
|
||||
var label = $('<div>').text(labelText);
|
||||
|
||||
var icon = $('<i>').css({
|
||||
background: color,
|
||||
'border-color': self.darkerColor(color)
|
||||
});
|
||||
|
||||
label.append(icon);
|
||||
$div.append(label);
|
||||
});
|
||||
|
||||
return $div.get(0);
|
||||
};
|
||||
|
||||
self._legend.addTo(self.map);
|
||||
};
|
||||
|
||||
/**
|
||||
* Apply style with shading to feature
|
||||
*
|
||||
* @method applyShadingStyle
|
||||
* @param value {Object}
|
||||
* @return {Object}
|
||||
*/
|
||||
BaseMarker.prototype.applyShadingStyle = function (value) {
|
||||
var color = this._legendQuantizer(value);
|
||||
|
||||
return {
|
||||
fillColor: color,
|
||||
color: this.darkerColor(color),
|
||||
weight: 1.5,
|
||||
opacity: 1,
|
||||
fillOpacity: 0.75
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Binds popup and events to each feature on map
|
||||
*
|
||||
* @method bindPopup
|
||||
* @param feature {Object}
|
||||
* @param layer {Object}
|
||||
* return {undefined}
|
||||
*/
|
||||
BaseMarker.prototype.bindPopup = function (feature, layer) {
|
||||
var self = this;
|
||||
|
||||
var popup = layer.on({
|
||||
mouseover: function (e) {
|
||||
var layer = e.target;
|
||||
// bring layer to front if not older browser
|
||||
if (!L.Browser.ie && !L.Browser.opera) {
|
||||
layer.bringToFront();
|
||||
}
|
||||
self._showTooltip(feature);
|
||||
},
|
||||
mouseout: function (e) {
|
||||
self._hidePopup();
|
||||
}
|
||||
});
|
||||
|
||||
self.popups.push(popup);
|
||||
};
|
||||
|
||||
/**
|
||||
* d3 method returns a darker hex color,
|
||||
* used for marker stroke color
|
||||
*
|
||||
* @method darkerColor
|
||||
* @param color {String} hex color
|
||||
* @param amount? {Number} amount to darken by
|
||||
* @return {String} hex color
|
||||
*/
|
||||
BaseMarker.prototype.darkerColor = function (color, amount) {
|
||||
amount = amount || 1.3;
|
||||
return d3.hcl(color).darker(amount).toString();
|
||||
};
|
||||
|
||||
BaseMarker.prototype.destroy = function () {
|
||||
var self = this;
|
||||
|
||||
// remove popups
|
||||
self.popups = self.popups.filter(function (popup) {
|
||||
popup.off('mouseover').off('mouseout');
|
||||
});
|
||||
|
||||
if (self._legend) {
|
||||
self.map.removeControl(self._legend);
|
||||
self._legend = undefined;
|
||||
}
|
||||
|
||||
// remove marker layer from map
|
||||
if (self._markerGroup) {
|
||||
self.map.removeLayer(self._markerGroup);
|
||||
self._markerGroup = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
BaseMarker.prototype._addToMap = function () {
|
||||
this.map.addLayer(this._markerGroup);
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates leaflet marker group, passing options to L.geoJson
|
||||
*
|
||||
* @method _createMarkerGroup
|
||||
* @param options {Object} Options to pass to L.geoJson
|
||||
*/
|
||||
BaseMarker.prototype._createMarkerGroup = function (options) {
|
||||
var self = this;
|
||||
var defaultOptions = {
|
||||
onEachFeature: function (feature, layer) {
|
||||
self.bindPopup(feature, layer);
|
||||
},
|
||||
style: function (feature) {
|
||||
var value = _.get(feature, 'properties.value');
|
||||
return self.applyShadingStyle(value);
|
||||
},
|
||||
filter: self._filterToMapBounds()
|
||||
};
|
||||
|
||||
this._markerGroup = L.geoJson(this.geoJson, _.defaults(defaultOptions, options));
|
||||
this._addToMap();
|
||||
};
|
||||
|
||||
/**
|
||||
* return whether feature is within map bounds
|
||||
*
|
||||
* @method _filterToMapBounds
|
||||
* @param map {Leaflet Object}
|
||||
* @return {boolean}
|
||||
*/
|
||||
BaseMarker.prototype._filterToMapBounds = function () {
|
||||
var self = this;
|
||||
return function (feature) {
|
||||
var mapBounds = self.map.getBounds();
|
||||
var bucketRectBounds = _.get(feature, 'properties.rectangle');
|
||||
return mapBounds.intersects(bucketRectBounds);
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if event latlng is within bounds of mapData
|
||||
* features and shows tooltip for that feature
|
||||
*
|
||||
* @method _showTooltip
|
||||
* @param feature {LeafletFeature}
|
||||
* @param latLng? {Leaflet latLng}
|
||||
* @return undefined
|
||||
*/
|
||||
BaseMarker.prototype._showTooltip = function (feature, latLng) {
|
||||
if (!this.map) return;
|
||||
var lat = _.get(feature, 'geometry.coordinates.1');
|
||||
var lng = _.get(feature, 'geometry.coordinates.0');
|
||||
latLng = latLng || L.latLng(lat, lng);
|
||||
|
||||
var content = this._tooltipFormatter(feature);
|
||||
|
||||
if (!content) return;
|
||||
this._createTooltip(content, latLng);
|
||||
};
|
||||
|
||||
BaseMarker.prototype._createTooltip = function (content, latLng) {
|
||||
L.popup({autoPan: false})
|
||||
.setLatLng(latLng)
|
||||
.setContent(content)
|
||||
.openOn(this.map);
|
||||
};
|
||||
|
||||
/**
|
||||
* Closes the tooltip on the map
|
||||
*
|
||||
* @method _hidePopup
|
||||
* @return undefined
|
||||
*/
|
||||
BaseMarker.prototype._hidePopup = function () {
|
||||
if (!this.map) return;
|
||||
|
||||
this.map.closePopup();
|
||||
};
|
||||
|
||||
/**
|
||||
* d3 quantize scale returns a hex color, used for marker fill color
|
||||
*
|
||||
* @method quantizeLegendColors
|
||||
* return {undefined}
|
||||
*/
|
||||
BaseMarker.prototype.quantizeLegendColors = function () {
|
||||
var min = _.get(this.geoJson, 'properties.allmin', 0);
|
||||
var max = _.get(this.geoJson, 'properties.allmax', 1);
|
||||
var quantizeDomain = (min !== max) ? [min, max] : d3.scale.quantize().domain();
|
||||
|
||||
var reds1 = ['#ff6128'];
|
||||
var reds3 = ['#fecc5c', '#fd8d3c', '#e31a1c'];
|
||||
var reds5 = ['#fed976', '#feb24c', '#fd8d3c', '#f03b20', '#bd0026'];
|
||||
var bottomCutoff = 2;
|
||||
var middleCutoff = 24;
|
||||
|
||||
if (max - min <= bottomCutoff) {
|
||||
this._legendColors = reds1;
|
||||
} else if (max - min <= middleCutoff) {
|
||||
this._legendColors = reds3;
|
||||
} else {
|
||||
this._legendColors = reds5;
|
||||
}
|
||||
|
||||
this._legendQuantizer = d3.scale.quantize().domain(quantizeDomain).range(this._legendColors);
|
||||
};
|
||||
|
||||
return BaseMarker;
|
||||
};
|
||||
});
|
|
@ -0,0 +1,40 @@
|
|||
define(function (require) {
|
||||
return function GeohashGridMarkerFactory(Private) {
|
||||
var _ = require('lodash');
|
||||
var L = require('leaflet');
|
||||
|
||||
var BaseMarker = Private(require('components/vislib/visualizations/marker_types/base_marker'));
|
||||
|
||||
/**
|
||||
* Map overlay: rectangles that show the geohash grid bounds
|
||||
*
|
||||
* @param map {Leaflet Object}
|
||||
* @param geoJson {geoJson Object}
|
||||
* @param params {Object}
|
||||
*/
|
||||
_.class(GeohashGridMarker).inherits(BaseMarker);
|
||||
function GeohashGridMarker(map, geoJson, params) {
|
||||
var self = this;
|
||||
GeohashGridMarker.Super.apply(this, arguments);
|
||||
|
||||
// super min and max from all chart data
|
||||
var min = this.geoJson.properties.allmin;
|
||||
var max = this.geoJson.properties.allmax;
|
||||
|
||||
this._createMarkerGroup({
|
||||
pointToLayer: function (feature, latlng) {
|
||||
var geohashRect = feature.properties.rectangle;
|
||||
// get bounds from northEast[3] and southWest[1]
|
||||
// corners in geohash rectangle
|
||||
var corners = [
|
||||
[geohashRect[3][0], geohashRect[3][1]],
|
||||
[geohashRect[1][0], geohashRect[1][1]]
|
||||
];
|
||||
return L.rectangle(corners);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return GeohashGridMarker;
|
||||
};
|
||||
});
|
|
@ -0,0 +1,211 @@
|
|||
define(function (require) {
|
||||
return function HeatmapMarkerFactory(Private, d3) {
|
||||
var _ = require('lodash');
|
||||
var L = require('leaflet');
|
||||
require('leaflet-heat');
|
||||
|
||||
var BaseMarker = Private(require('components/vislib/visualizations/marker_types/base_marker'));
|
||||
|
||||
/**
|
||||
* Map overlay: canvas layer with leaflet.heat plugin
|
||||
*
|
||||
* @param map {Leaflet Object}
|
||||
* @param geoJson {geoJson Object}
|
||||
* @param params {Object}
|
||||
*/
|
||||
_.class(HeatmapMarker).inherits(BaseMarker);
|
||||
function HeatmapMarker(map, geoJson, params) {
|
||||
var self = this;
|
||||
this._disableTooltips = false;
|
||||
HeatmapMarker.Super.apply(this, arguments);
|
||||
|
||||
this._createMarkerGroup({
|
||||
radius: +this._attr.heatRadius,
|
||||
blur: +this._attr.heatBlur,
|
||||
maxZoom: +this._attr.heatMaxZoom,
|
||||
minOpacity: +this._attr.heatMinOpacity
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Does nothing, heatmaps don't have a legend
|
||||
*
|
||||
* @method addLegend
|
||||
* @return {undefined}
|
||||
*/
|
||||
HeatmapMarker.prototype.addLegend = _.noop;
|
||||
|
||||
HeatmapMarker.prototype._createMarkerGroup = function (options) {
|
||||
var max = _.get(this.geoJson, 'properties.allmax');
|
||||
var points = this._dataToHeatArray(max);
|
||||
|
||||
this._markerGroup = L.heatLayer(points, options);
|
||||
this._fixTooltips();
|
||||
this._addToMap();
|
||||
};
|
||||
|
||||
HeatmapMarker.prototype._fixTooltips = function () {
|
||||
var self = this;
|
||||
var debouncedMouseMoveLocation = _.debounce(mouseMoveLocation.bind(this), 15, {
|
||||
'leading': true,
|
||||
'trailing': false
|
||||
});
|
||||
|
||||
if (!this._disableTooltips && this._attr.addTooltip) {
|
||||
this.map.on('mousemove', debouncedMouseMoveLocation);
|
||||
this.map.on('mouseout', function () {
|
||||
self.map.closePopup();
|
||||
});
|
||||
this.map.on('mousedown', function () {
|
||||
self._disableTooltips = true;
|
||||
self.map.closePopup();
|
||||
});
|
||||
this.map.on('mouseup', function () {
|
||||
self._disableTooltips = false;
|
||||
});
|
||||
}
|
||||
|
||||
function mouseMoveLocation(e) {
|
||||
var latlng = e.latlng;
|
||||
|
||||
this.map.closePopup();
|
||||
|
||||
// unhighlight all svgs
|
||||
d3.selectAll('path.geohash', this.chartEl).classed('geohash-hover', false);
|
||||
|
||||
if (!this.geoJson.features.length || this._disableTooltips) {
|
||||
return;
|
||||
}
|
||||
|
||||
// find nearest feature to event latlng
|
||||
var feature = this._nearestFeature(latlng);
|
||||
|
||||
// show tooltip if close enough to event latlng
|
||||
if (this._tooltipProximity(latlng, feature)) {
|
||||
this._showTooltip(feature, latlng);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* returns a memoized Leaflet latLng for given geoJson feature
|
||||
*
|
||||
* @method addLatLng
|
||||
* @param feature {geoJson Object}
|
||||
* @return {Leaflet latLng Object}
|
||||
*/
|
||||
HeatmapMarker.prototype._getLatLng = _.memoize(function (feature) {
|
||||
return L.latLng(
|
||||
feature.geometry.coordinates[1],
|
||||
feature.geometry.coordinates[0]
|
||||
);
|
||||
}, function (feature) {
|
||||
// turn coords into a string for the memoize cache
|
||||
return [feature.geometry.coordinates[1], feature.geometry.coordinates[0]].join(',');
|
||||
});
|
||||
|
||||
/**
|
||||
* Finds nearest feature in mapData to event latlng
|
||||
*
|
||||
* @method _nearestFeature
|
||||
* @param latLng {Leaflet latLng}
|
||||
* @return nearestPoint {Leaflet latLng}
|
||||
*/
|
||||
HeatmapMarker.prototype._nearestFeature = function (latLng) {
|
||||
var self = this;
|
||||
var nearest;
|
||||
|
||||
if (latLng.lng < -180 || latLng.lng > 180) {
|
||||
return;
|
||||
}
|
||||
|
||||
_.reduce(this.geoJson.features, function (distance, feature) {
|
||||
var featureLatLng = self._getLatLng(feature);
|
||||
var dist = latLng.distanceTo(featureLatLng);
|
||||
|
||||
if (dist < distance) {
|
||||
nearest = feature;
|
||||
return dist;
|
||||
}
|
||||
|
||||
return distance;
|
||||
}, Infinity);
|
||||
|
||||
return nearest;
|
||||
};
|
||||
|
||||
/**
|
||||
* display tooltip if feature is close enough to event latlng
|
||||
*
|
||||
* @method _tooltipProximity
|
||||
* @param latlng {Leaflet latLng Object}
|
||||
* @param feature {geoJson Object}
|
||||
* @return {Boolean}
|
||||
*/
|
||||
HeatmapMarker.prototype._tooltipProximity = function (latlng, feature) {
|
||||
if (!feature) return;
|
||||
|
||||
var showTip = false;
|
||||
var featureLatLng = this._getLatLng(feature);
|
||||
|
||||
// zoomScale takes map zoom and returns proximity value for tooltip display
|
||||
// domain (input values) is map zoom (min 1 and max 18)
|
||||
// range (output values) is distance in meters
|
||||
// used to compare proximity of event latlng to feature latlng
|
||||
var zoomScale = d3.scale.linear()
|
||||
.domain([1, 4, 7, 10, 13, 16, 18])
|
||||
.range([1000000, 300000, 100000, 15000, 2000, 150, 50]);
|
||||
|
||||
var proximity = zoomScale(this.map.getZoom());
|
||||
var distance = latlng.distanceTo(featureLatLng);
|
||||
|
||||
// maxLngDif is max difference in longitudes
|
||||
// to prevent feature tooltip from appearing 360°
|
||||
// away from event latlng
|
||||
var maxLngDif = 40;
|
||||
var lngDif = Math.abs(latlng.lng - featureLatLng.lng);
|
||||
|
||||
if (distance < proximity && lngDif < maxLngDif) {
|
||||
showTip = true;
|
||||
}
|
||||
|
||||
var testScale = d3.scale.pow().exponent(0.2)
|
||||
.domain([1, 18])
|
||||
.range([1500000, 50]);
|
||||
return showTip;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* returns data for data for heat map intensity
|
||||
* if heatNormalizeData attribute is checked/true
|
||||
• normalizes data for heat map intensity
|
||||
*
|
||||
* @method _dataToHeatArray
|
||||
* @param max {Number}
|
||||
* @return {Array}
|
||||
*/
|
||||
HeatmapMarker.prototype._dataToHeatArray = function (max) {
|
||||
var self = this;
|
||||
var mapData = this.geoJson;
|
||||
|
||||
return this.geoJson.features.map(function (feature) {
|
||||
var lat = feature.properties.center[0];
|
||||
var lng = feature.properties.center[1];
|
||||
var heatIntensity;
|
||||
|
||||
if (!self._attr.heatNormalizeData) {
|
||||
// show bucket value on heatmap
|
||||
heatIntensity = feature.properties.value;
|
||||
} else {
|
||||
// show bucket value normalized to max value
|
||||
heatIntensity = parseInt(feature.properties.value / max * 100);
|
||||
}
|
||||
|
||||
return [lat, lng, heatIntensity];
|
||||
});
|
||||
};
|
||||
|
||||
return HeatmapMarker;
|
||||
};
|
||||
});
|
|
@ -0,0 +1,60 @@
|
|||
define(function (require) {
|
||||
return function ScaledCircleMarkerFactory(Private) {
|
||||
var _ = require('lodash');
|
||||
var L = require('leaflet');
|
||||
|
||||
var BaseMarker = Private(require('components/vislib/visualizations/marker_types/base_marker'));
|
||||
|
||||
/**
|
||||
* Map overlay: circle markers that are scaled to illustrate values
|
||||
*
|
||||
* @param map {Leaflet Object}
|
||||
* @param mapData {geoJson Object}
|
||||
* @param params {Object}
|
||||
*/
|
||||
_.class(ScaledCircleMarker).inherits(BaseMarker);
|
||||
function ScaledCircleMarker(map, geoJson, params) {
|
||||
var self = this;
|
||||
ScaledCircleMarker.Super.apply(this, arguments);
|
||||
|
||||
// multiplier to reduce size of all circles
|
||||
var scaleFactor = 0.6;
|
||||
|
||||
this._createMarkerGroup({
|
||||
pointToLayer: function (feature, latlng) {
|
||||
var value = feature.properties.value;
|
||||
var scaledRadius = self._radiusScale(value) * scaleFactor;
|
||||
return L.circleMarker(latlng).setRadius(scaledRadius);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* radiusScale returns a number for scaled circle markers
|
||||
* for relative sizing of markers
|
||||
*
|
||||
* @method _radiusScale
|
||||
* @param value {Number}
|
||||
* @return {Number}
|
||||
*/
|
||||
ScaledCircleMarker.prototype._radiusScale = function (value) {
|
||||
var precisionBiasBase = 5;
|
||||
var precisionBiasNumerator = 200;
|
||||
var zoom = this.map.getZoom();
|
||||
var maxValue = this.geoJson.properties.allmax;
|
||||
var precision = _.max(this.geoJson.features.map(function (feature) {
|
||||
return String(feature.properties.geohash).length;
|
||||
}));
|
||||
|
||||
var pct = Math.abs(value) / Math.abs(maxValue);
|
||||
var zoomRadius = 0.5 * Math.pow(2, zoom);
|
||||
var precisionScale = precisionBiasNumerator / Math.pow(precisionBiasBase, precision);
|
||||
|
||||
// square root value percentage
|
||||
return Math.pow(pct, 0.5) * zoomRadius * precisionScale;
|
||||
};
|
||||
|
||||
|
||||
return ScaledCircleMarker;
|
||||
};
|
||||
});
|
|
@ -0,0 +1,68 @@
|
|||
define(function (require) {
|
||||
return function ShadedCircleMarkerFactory(Private) {
|
||||
var _ = require('lodash');
|
||||
var L = require('leaflet');
|
||||
|
||||
var BaseMarker = Private(require('components/vislib/visualizations/marker_types/base_marker'));
|
||||
|
||||
/**
|
||||
* Map overlay: circle markers that are shaded to illustrate values
|
||||
*
|
||||
* @param map {Leaflet Object}
|
||||
* @param mapData {geoJson Object}
|
||||
* @return {Leaflet object} featureLayer
|
||||
*/
|
||||
_.class(ShadedCircleMarker).inherits(BaseMarker);
|
||||
function ShadedCircleMarker(map, geoJson, params) {
|
||||
var self = this;
|
||||
ShadedCircleMarker.Super.apply(this, arguments);
|
||||
|
||||
// super min and max from all chart data
|
||||
var min = this.geoJson.properties.allmin;
|
||||
var max = this.geoJson.properties.allmax;
|
||||
|
||||
// multiplier to reduce size of all circles
|
||||
var scaleFactor = 0.8;
|
||||
|
||||
this._createMarkerGroup({
|
||||
pointToLayer: function (feature, latlng) {
|
||||
var radius = self._geohashMinDistance(feature) * scaleFactor;
|
||||
return L.circle(latlng, radius);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* _geohashMinDistance returns a min distance in meters for sizing
|
||||
* circle markers to fit within geohash grid rectangle
|
||||
*
|
||||
* @method _geohashMinDistance
|
||||
* @param feature {Object}
|
||||
* @return {Number}
|
||||
*/
|
||||
ShadedCircleMarker.prototype._geohashMinDistance = function (feature) {
|
||||
var centerPoint = _.get(feature, 'properties.center');
|
||||
var geohashRect = _.get(feature, 'properties.rectangle');
|
||||
|
||||
// centerPoint is an array of [lat, lng]
|
||||
// geohashRect is the 4 corners of the geoHash rectangle
|
||||
// an array that starts at the southwest corner and proceeds
|
||||
// clockwise, each value being an array of [lat, lng]
|
||||
|
||||
// center lat and southeast lng
|
||||
var east = L.latLng([centerPoint[0], geohashRect[2][1]]);
|
||||
// southwest lat and center lng
|
||||
var north = L.latLng([geohashRect[3][0], centerPoint[1]]);
|
||||
|
||||
// get latLng of geohash center point
|
||||
var center = L.latLng([centerPoint[0], centerPoint[1]]);
|
||||
|
||||
// get smallest radius at center of geohash grid rectangle
|
||||
var eastRadius = Math.floor(center.distanceTo(east));
|
||||
var northRadius = Math.floor(center.distanceTo(north));
|
||||
return _.min([eastRadius, northRadius]);
|
||||
};
|
||||
|
||||
return ShadedCircleMarker;
|
||||
};
|
||||
});
|
|
@ -1,21 +1,11 @@
|
|||
define(function (require) {
|
||||
return function TileMapFactory(d3, Private, config) {
|
||||
return function TileMapFactory(d3, Private) {
|
||||
var _ = require('lodash');
|
||||
var $ = require('jquery');
|
||||
var L = require('leaflet');
|
||||
require('leaflet-heat');
|
||||
require('leaflet-draw');
|
||||
require('css!components/vislib/styles/main');
|
||||
|
||||
var Chart = Private(require('components/vislib/visualizations/_chart'));
|
||||
var defaultMapZoom = 2;
|
||||
var defaultMapCenter = [15, 5];
|
||||
|
||||
// Convenience function to turn around the LngLat recieved from ES
|
||||
function cloneAndReverse(arr) {
|
||||
var l = arr.length;
|
||||
return arr.map(function (curr, idx) { return arr[l - (idx + 1)]; });
|
||||
}
|
||||
var Map = Private(require('components/vislib/visualizations/_map'));
|
||||
|
||||
/**
|
||||
* Tile Map Visualization: renders maps
|
||||
|
@ -35,696 +25,33 @@ define(function (require) {
|
|||
|
||||
TileMap.Super.apply(this, arguments);
|
||||
|
||||
|
||||
// track the map objects
|
||||
this.maps = [];
|
||||
this.originalConfig = chartData || {};
|
||||
_.assign(this, this.originalConfig);
|
||||
this._chartData = chartData || {};
|
||||
_.assign(this, this._chartData);
|
||||
|
||||
this._attr.mapZoom = _.get(this.geoJson, 'properties.zoom') || defaultMapZoom;
|
||||
this._attr.mapCenter = _.get(this.geoJson, 'properties.center') || defaultMapCenter;
|
||||
|
||||
// add allmin and allmax to geoJson
|
||||
var allMinMax = this.getMinMax(handler.data.data);
|
||||
this.geoJson.properties.allmin = allMinMax.min;
|
||||
this.geoJson.properties.allmax = allMinMax.max;
|
||||
this._appendGeoExtents();
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders tile map
|
||||
* Draws tile map, called on chart render
|
||||
*
|
||||
* @method draw
|
||||
* @return {Function} - function to add a map to a selection
|
||||
*/
|
||||
TileMap.prototype.draw = function () {
|
||||
var self = this;
|
||||
var mapData = this.geoJson;
|
||||
|
||||
// clean up old maps
|
||||
self.destroy();
|
||||
|
||||
// clear maps array
|
||||
self.maps = [];
|
||||
self.popups = [];
|
||||
|
||||
var worldBounds = L.latLngBounds([-90, -220], [90, 220]);
|
||||
|
||||
return function (selection) {
|
||||
selection.each(function () {
|
||||
// add leaflet latLngs to properties for tooltip
|
||||
self.addLatLng(self.geoJson);
|
||||
|
||||
var div = $(this).addClass('tilemap');
|
||||
var tileLayer = L.tileLayer('https://otile{s}-s.mqcdn.com/tiles/1.0.0/map/{z}/{x}/{y}.jpeg', {
|
||||
attribution: 'Tiles by <a href="http://www.mapquest.com/">MapQuest</a> — ' +
|
||||
'Map data © <a href="http://openstreetmap.org">OpenStreetMap</a> contributors, ' +
|
||||
'<a href="http://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>',
|
||||
subdomains: '1234'
|
||||
});
|
||||
|
||||
var drawOptions = {draw: {}};
|
||||
_.each(['polyline', 'polygon', 'circle', 'marker', 'rectangle'], function (drawShape) {
|
||||
if (!self.events.listenerCount(drawShape)) {
|
||||
drawOptions.draw[drawShape] = false;
|
||||
} else {
|
||||
drawOptions.draw[drawShape] = {
|
||||
shapeOptions: {
|
||||
stroke: false,
|
||||
color: '#000'
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
var mapOptions = {
|
||||
minZoom: 1,
|
||||
maxZoom: 18,
|
||||
layers: tileLayer,
|
||||
center: self._attr.mapCenter,
|
||||
zoom: self._attr.mapZoom,
|
||||
noWrap: true,
|
||||
maxBounds: worldBounds,
|
||||
scrollWheelZoom: false,
|
||||
fadeAnimation: false,
|
||||
};
|
||||
|
||||
var map = L.map(div[0], mapOptions);
|
||||
var featureLayer = self.markerType(map).addTo(map);
|
||||
|
||||
if (mapData.features.length) {
|
||||
map.addControl(new L.Control.Draw(drawOptions));
|
||||
}
|
||||
|
||||
function saturateTiles() {
|
||||
self.saturateTiles();
|
||||
}
|
||||
|
||||
tileLayer.on('tileload', saturateTiles);
|
||||
|
||||
map.on('unload', function () {
|
||||
tileLayer.off('tileload', saturateTiles);
|
||||
});
|
||||
|
||||
map.on('moveend', function setZoomCenter() {
|
||||
self._attr.mapZoom = map.getZoom();
|
||||
self._attr.mapCenter = map.getCenter();
|
||||
|
||||
self.events.emit('mapMoveEnd', {
|
||||
chart: self.originalConfig,
|
||||
zoom: self._attr.mapZoom,
|
||||
center: self._attr.mapCenter
|
||||
});
|
||||
|
||||
map.removeLayer(featureLayer);
|
||||
|
||||
featureLayer = self.markerType(map).addTo(map);
|
||||
});
|
||||
|
||||
map.on('draw:created', function (e) {
|
||||
var drawType = e.layerType;
|
||||
if (!self.events.listenerCount(drawType)) return;
|
||||
|
||||
// TODO: Different drawTypes need differ info. Need a switch on the object creation
|
||||
var bounds = e.layer.getBounds();
|
||||
|
||||
self.events.emit(drawType, {
|
||||
e: e,
|
||||
chart: self.originalConfig,
|
||||
bounds: {
|
||||
top_left: {
|
||||
lat: bounds.getNorthWest().lat,
|
||||
lon: bounds.getNorthWest().lng
|
||||
},
|
||||
bottom_right: {
|
||||
lat: bounds.getSouthEast().lat,
|
||||
lon: bounds.getSouthEast().lng
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
map.on('zoomend', function () {
|
||||
self.events.emit('mapZoomEnd', {
|
||||
chart: self.originalConfig,
|
||||
zoom: map.getZoom()
|
||||
});
|
||||
});
|
||||
|
||||
// add title for splits
|
||||
if (self.title) {
|
||||
self.addTitle(self.title, map);
|
||||
}
|
||||
|
||||
if (mapData && mapData.features.length > 0) {
|
||||
var fitContainer = L.DomUtil.create('div', 'leaflet-control leaflet-bar leaflet-control-fit');
|
||||
|
||||
// Add button to fit container to points
|
||||
var FitControl = L.Control.extend({
|
||||
options: {
|
||||
position: 'topleft'
|
||||
},
|
||||
onAdd: function (map) {
|
||||
$(fitContainer).html('<a class="fa fa-crop" href="#" title="Fit Data Bounds"></a>')
|
||||
.on('click', function (e) {
|
||||
e.preventDefault();
|
||||
self.fitBounds(map, mapData.features);
|
||||
});
|
||||
|
||||
return fitContainer;
|
||||
},
|
||||
onRemove: function (map) {
|
||||
$(fitContainer).off('click');
|
||||
}
|
||||
});
|
||||
map.fitControl = new FitControl();
|
||||
map.addControl(map.fitControl);
|
||||
} else {
|
||||
map.fitControl = undefined;
|
||||
}
|
||||
|
||||
self.maps.push(map);
|
||||
self._appendMap(this);
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* return whether feature is within map bounds
|
||||
*
|
||||
* @method _filterToMapBounds
|
||||
* @param map {Leaflet Object}
|
||||
* @return {boolean}
|
||||
*/
|
||||
TileMap.prototype._filterToMapBounds = function (map) {
|
||||
return function (feature) {
|
||||
var mapBounds = map.getBounds();
|
||||
var bucketRectBounds = feature.properties.rectangle.map(cloneAndReverse);
|
||||
|
||||
return mapBounds.intersects(bucketRectBounds);
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* get min and max for all cols, rows of data
|
||||
*
|
||||
* @method getMaxMin
|
||||
* @param data {Object}
|
||||
* @return {Object}
|
||||
*/
|
||||
TileMap.prototype.getMinMax = function (data) {
|
||||
var min = [];
|
||||
var max = [];
|
||||
var allData;
|
||||
|
||||
if (data.rows) {
|
||||
allData = data.rows;
|
||||
} else if (data.columns) {
|
||||
allData = data.columns;
|
||||
} else {
|
||||
allData = [data];
|
||||
}
|
||||
|
||||
allData.forEach(function (datum) {
|
||||
min.push(datum.geoJson.properties.min);
|
||||
max.push(datum.geoJson.properties.max);
|
||||
});
|
||||
|
||||
var minMax = {
|
||||
min: _.min(min),
|
||||
max: _.max(max)
|
||||
};
|
||||
|
||||
return minMax;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the Rectangles representing the geohash grid
|
||||
*
|
||||
* @return {LatLngRectangles[]}
|
||||
*/
|
||||
TileMap.prototype._getDataRectangles = function () {
|
||||
return _(this.geoJson.features)
|
||||
.pluck('properties.rectangle')
|
||||
.invoke('map', cloneAndReverse)
|
||||
.value();
|
||||
};
|
||||
|
||||
/**
|
||||
* add Leaflet latLng to mapData properties
|
||||
*
|
||||
* @method addLatLng
|
||||
* @return undefined
|
||||
*/
|
||||
TileMap.prototype.addLatLng = function () {
|
||||
this.geoJson.features.forEach(function (feature) {
|
||||
feature.properties.latLng = L.latLng(
|
||||
feature.geometry.coordinates[1],
|
||||
feature.geometry.coordinates[0]
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* zoom map to fit all features in featureLayer
|
||||
*
|
||||
* @method fitBounds
|
||||
* @param map {Leaflet Object}
|
||||
* @return {boolean}
|
||||
*/
|
||||
TileMap.prototype.fitBounds = function (map) {
|
||||
map.fitBounds(this._getDataRectangles());
|
||||
};
|
||||
|
||||
/**
|
||||
* remove css class for desat filters on map tiles
|
||||
*
|
||||
* @method saturateTiles
|
||||
* @return undefined
|
||||
*/
|
||||
TileMap.prototype.saturateTiles = function () {
|
||||
if (!this._attr.isDesaturated) {
|
||||
$('img.leaflet-tile-loaded').addClass('filters-off');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Finds nearest feature in mapData to event latlng
|
||||
*
|
||||
* @method nearestFeature
|
||||
* @param point {Leaflet Object}
|
||||
* @return nearestPoint {Leaflet Object}
|
||||
*/
|
||||
TileMap.prototype.nearestFeature = function (point) {
|
||||
var mapData = this.geoJson;
|
||||
var distance = Infinity;
|
||||
var nearest;
|
||||
|
||||
if (point.lng < -180 || point.lng > 180) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (var i = 0; i < mapData.features.length; i++) {
|
||||
var dist = point.distanceTo(mapData.features[i].properties.latLng);
|
||||
if (dist < distance) {
|
||||
distance = dist;
|
||||
nearest = mapData.features[i];
|
||||
}
|
||||
}
|
||||
nearest.properties.eventDistance = distance;
|
||||
|
||||
return nearest;
|
||||
};
|
||||
|
||||
/**
|
||||
* display tooltip if feature is close enough to event latlng
|
||||
*
|
||||
* @method tooltipProximity
|
||||
* @param latlng {Leaflet Object}
|
||||
* @param zoom {Number}
|
||||
* @param feature {geoJson Object}
|
||||
* @param map {Leaflet Object}
|
||||
* @return boolean
|
||||
*/
|
||||
TileMap.prototype.tooltipProximity = function (latlng, zoom, feature, map) {
|
||||
if (!feature) return;
|
||||
|
||||
var showTip = false;
|
||||
|
||||
// zoomScale takes map zoom and returns proximity value for tooltip display
|
||||
// domain (input values) is map zoom (min 1 and max 18)
|
||||
// range (output values) is distance in meters
|
||||
// used to compare proximity of event latlng to feature latlng
|
||||
var zoomScale = d3.scale.linear()
|
||||
.domain([1, 4, 7, 10, 13, 16, 18])
|
||||
.range([1000000, 300000, 100000, 15000, 2000, 150, 50]);
|
||||
|
||||
var proximity = zoomScale(zoom);
|
||||
var distance = latlng.distanceTo(feature.properties.latLng);
|
||||
|
||||
// maxLngDif is max difference in longitudes
|
||||
// to prevent feature tooltip from appearing 360°
|
||||
// away from event latlng
|
||||
var maxLngDif = 40;
|
||||
var lngDif = Math.abs(latlng.lng - feature.properties.latLng.lng);
|
||||
|
||||
if (distance < proximity && lngDif < maxLngDif) {
|
||||
showTip = true;
|
||||
}
|
||||
|
||||
delete feature.properties.eventDistance;
|
||||
|
||||
var testScale = d3.scale.pow().exponent(0.2)
|
||||
.domain([1, 18])
|
||||
.range([1500000, 50]);
|
||||
return showTip;
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if event latlng is within bounds of mapData
|
||||
* features and shows tooltip for that feature
|
||||
*
|
||||
* @method showTooltip
|
||||
* @param map {LeafletMap}
|
||||
* @param feature {LeafletFeature}
|
||||
* @return undefined
|
||||
*/
|
||||
TileMap.prototype.showTooltip = function (map, feature) {
|
||||
if (!this.tooltipFormatter) return;
|
||||
|
||||
var content = this.tooltipFormatter(feature);
|
||||
if (!content) return;
|
||||
|
||||
var lat = feature.geometry.coordinates[1];
|
||||
var lng = feature.geometry.coordinates[0];
|
||||
var latLng = L.latLng(lat, lng);
|
||||
|
||||
L.popup({autoPan: false})
|
||||
.setLatLng(latLng)
|
||||
.setContent(content)
|
||||
.openOn(map);
|
||||
};
|
||||
|
||||
/**
|
||||
* Switch type of data overlay for map:
|
||||
* creates featurelayer from mapData (geoJson)
|
||||
*
|
||||
* @method markerType
|
||||
* @param map {Leaflet Object}
|
||||
* @param mapData {geoJson Object}
|
||||
* @return {Leaflet object} featureLayer
|
||||
*/
|
||||
TileMap.prototype.markerType = function (map) {
|
||||
if (this._attr.mapType === 'Scaled Circle Markers') {
|
||||
return this.scaledCircleMarkers(map);
|
||||
}
|
||||
|
||||
if (this._attr.mapType === 'Heatmap') {
|
||||
return this.heatMap(map);
|
||||
}
|
||||
|
||||
if (this._attr.mapType === 'Shaded Circle Markers') {
|
||||
return this.shadedCircleMarkers(map);
|
||||
}
|
||||
|
||||
if (this._attr.mapType === 'Shaded Geohash Grid') {
|
||||
return this.shadedGeohashGrid(map);
|
||||
}
|
||||
|
||||
return this.scaledCircleMarkers(map);
|
||||
};
|
||||
|
||||
/**
|
||||
* Type of data overlay for map:
|
||||
* creates featurelayer from mapData (geoJson)
|
||||
* with circle markers that are scaled to illustrate values
|
||||
*
|
||||
* @method scaledCircleMarkers
|
||||
* @param map {Leaflet Object}
|
||||
* @param mapData {geoJson Object}
|
||||
* @return {Leaflet object} featureLayer
|
||||
*/
|
||||
TileMap.prototype.scaledCircleMarkers = function (map) {
|
||||
var self = this;
|
||||
var mapData = self.geoJson;
|
||||
|
||||
// super min and max from all chart data
|
||||
var min = mapData.properties.allmin;
|
||||
var max = mapData.properties.allmax;
|
||||
var zoom = map.getZoom();
|
||||
var precision = _.max(mapData.features.map(function (feature) {
|
||||
return String(feature.properties.geohash).length;
|
||||
}));
|
||||
|
||||
// multiplier to reduce size of all circles
|
||||
var scaleFactor = 0.6;
|
||||
|
||||
var radiusScaler = 2.5;
|
||||
|
||||
var featureLayer = L.geoJson(mapData, {
|
||||
pointToLayer: function (feature, latlng) {
|
||||
var value = feature.properties.value;
|
||||
var scaledRadius = self.radiusScale(value, max, zoom, precision) * scaleFactor;
|
||||
return L.circleMarker(latlng).setRadius(scaledRadius);
|
||||
},
|
||||
onEachFeature: function (feature, layer) {
|
||||
self.bindPopup(feature, layer, map);
|
||||
},
|
||||
style: function (feature) {
|
||||
return self.applyShadingStyle(feature, min, max);
|
||||
},
|
||||
filter: self._filterToMapBounds(map)
|
||||
});
|
||||
|
||||
self.addLegend(map);
|
||||
|
||||
return featureLayer;
|
||||
};
|
||||
|
||||
/**
|
||||
* Type of data overlay for map:
|
||||
* creates featurelayer from mapData (geoJson)
|
||||
* with circle markers that are shaded to illustrate values
|
||||
*
|
||||
* @method shadedCircleMarkers
|
||||
* @param map {Leaflet Object}
|
||||
* @param mapData {geoJson Object}
|
||||
* @return {Leaflet object} featureLayer
|
||||
*/
|
||||
TileMap.prototype.shadedCircleMarkers = function (map) {
|
||||
var self = this;
|
||||
var mapData = self.geoJson;
|
||||
// super min and max from all chart data
|
||||
var min = mapData.properties.allmin;
|
||||
var max = mapData.properties.allmax;
|
||||
|
||||
// multiplier to reduce size of all circles
|
||||
var scaleFactor = 0.8;
|
||||
|
||||
var featureLayer = L.geoJson(mapData, {
|
||||
pointToLayer: function (feature, latlng) {
|
||||
var radius = self.geohashMinDistance(feature) * scaleFactor;
|
||||
return L.circle(latlng, radius);
|
||||
},
|
||||
onEachFeature: function (feature, layer) {
|
||||
self.bindPopup(feature, layer, map);
|
||||
},
|
||||
style: function (feature) {
|
||||
return self.applyShadingStyle(feature, min, max);
|
||||
},
|
||||
filter: self._filterToMapBounds(map)
|
||||
});
|
||||
|
||||
self.addLegend(map);
|
||||
|
||||
return featureLayer;
|
||||
};
|
||||
|
||||
/**
|
||||
* Type of data overlay for map:
|
||||
* creates featurelayer from mapData (geoJson)
|
||||
* with rectangles that show the geohash grid bounds
|
||||
*
|
||||
* @method geohashGrid
|
||||
* @param map {Leaflet Object}
|
||||
* @param mapData {geoJson Object}
|
||||
* @return {undefined}
|
||||
*/
|
||||
TileMap.prototype.shadedGeohashGrid = function (map) {
|
||||
var self = this;
|
||||
var mapData = self.geoJson;
|
||||
|
||||
// super min and max from all chart data
|
||||
var min = mapData.properties.allmin;
|
||||
var max = mapData.properties.allmax;
|
||||
|
||||
var bounds;
|
||||
|
||||
var featureLayer = L.geoJson(mapData, {
|
||||
pointToLayer: function (feature, latlng) {
|
||||
var geohashRect = feature.properties.rectangle;
|
||||
// get bounds from northEast[3] and southWest[1]
|
||||
// corners in geohash rectangle
|
||||
var corners = [
|
||||
[geohashRect[3][1], geohashRect[3][0]],
|
||||
[geohashRect[1][1], geohashRect[1][0]]
|
||||
];
|
||||
return L.rectangle(corners);
|
||||
},
|
||||
onEachFeature: function (feature, layer) {
|
||||
self.bindPopup(feature, layer, map);
|
||||
layer.on({
|
||||
mouseover: function (e) {
|
||||
var layer = e.target;
|
||||
// bring layer to front if not older browser
|
||||
if (!L.Browser.ie && !L.Browser.opera) {
|
||||
layer.bringToFront();
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
style: function (feature) {
|
||||
return self.applyShadingStyle(feature, min, max);
|
||||
},
|
||||
filter: self._filterToMapBounds(map)
|
||||
});
|
||||
|
||||
self.addLegend(map);
|
||||
|
||||
return featureLayer;
|
||||
};
|
||||
|
||||
/**
|
||||
* Type of data overlay for map:
|
||||
* creates canvas layer from mapData (geoJson)
|
||||
* with leaflet.heat plugin
|
||||
*
|
||||
* @method heatMap
|
||||
* @param map {Leaflet Object}
|
||||
* @param mapData {geoJson Object}
|
||||
* @return featureLayer {Leaflet object}
|
||||
*/
|
||||
TileMap.prototype.heatMap = function (map) {
|
||||
var self = this;
|
||||
var mapData = this.geoJson;
|
||||
var points = this.dataToHeatArray(mapData.properties.allmax);
|
||||
|
||||
var options = {
|
||||
radius: +this._attr.heatRadius,
|
||||
blur: +this._attr.heatBlur,
|
||||
maxZoom: +this._attr.heatMaxZoom,
|
||||
minOpacity: +this._attr.heatMinOpacity
|
||||
};
|
||||
|
||||
var featureLayer = L.heatLayer(points, options);
|
||||
|
||||
if (self._attr.addTooltip && self.tooltipFormatter && !self._attr.disableTooltips) {
|
||||
map.on('mousemove', _.debounce(mouseMoveLocation, 15, {
|
||||
'leading': true,
|
||||
'trailing': false
|
||||
}));
|
||||
map.on('mouseout', function (e) {
|
||||
map.closePopup();
|
||||
});
|
||||
map.on('mousedown', function () {
|
||||
self._attr.disableTooltips = true;
|
||||
map.closePopup();
|
||||
});
|
||||
map.on('mouseup', function () {
|
||||
self._attr.disableTooltips = false;
|
||||
});
|
||||
}
|
||||
|
||||
function mouseMoveLocation(e) {
|
||||
map.closePopup();
|
||||
|
||||
// unhighlight all svgs
|
||||
d3.selectAll('path.geohash', this.chartEl).classed('geohash-hover', false);
|
||||
|
||||
if (!mapData.features.length || self._attr.disableTooltips) {
|
||||
return;
|
||||
}
|
||||
|
||||
var latlng = e.latlng;
|
||||
|
||||
// find nearest feature to event latlng
|
||||
var feature = self.nearestFeature(latlng);
|
||||
|
||||
var zoom = map.getZoom();
|
||||
|
||||
// show tooltip if close enough to event latlng
|
||||
if (self.tooltipProximity(latlng, zoom, feature, map)) {
|
||||
self.showTooltip(map, feature, latlng);
|
||||
}
|
||||
}
|
||||
|
||||
return featureLayer;
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds label div to each map when data is split
|
||||
*
|
||||
* @method addTitle
|
||||
* @param mapLabel {String}
|
||||
* @param map {Leaflet Object}
|
||||
* @return {undefined}
|
||||
*/
|
||||
TileMap.prototype.addTitle = function (mapLabel, map) {
|
||||
var label = L.control();
|
||||
label.onAdd = function () {
|
||||
this._div = L.DomUtil.create('div', 'tilemap-info tilemap-label');
|
||||
this.update();
|
||||
return this._div;
|
||||
};
|
||||
label.update = function () {
|
||||
this._div.innerHTML = '<h2>' + _.escape(mapLabel) + '</h2>';
|
||||
};
|
||||
label.addTo(map);
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds legend div to each map when data is split
|
||||
* uses d3 scale from TileMap.prototype.quantizeColorScale
|
||||
*
|
||||
* @method addLegend
|
||||
* @param map {Leaflet Object}
|
||||
* @return {undefined}
|
||||
*/
|
||||
TileMap.prototype.addLegend = function (map) {
|
||||
// only draw the legend for maps with multiple items
|
||||
if (this.geoJson.features.length <= 1) return;
|
||||
|
||||
var self = this;
|
||||
var isLegend = $('div.tilemap-legend', this.chartEl).length;
|
||||
|
||||
if (isLegend) return; // Don't add Legend if already one
|
||||
|
||||
var valueFormatter = this.valueFormatter || _.identity;
|
||||
var legend = L.control({position: 'bottomright'});
|
||||
|
||||
legend.onAdd = function () {
|
||||
var $div = $('<div>').addClass('tilemap-legend');
|
||||
|
||||
_.each(self._attr.colors, function (color, i) {
|
||||
var icon = $('<i>').css({
|
||||
background: color,
|
||||
'border-color': self.darkerColor(color)
|
||||
});
|
||||
|
||||
var range = self._attr.cScale
|
||||
.invertExtent(color)
|
||||
.map(valueFormatter)
|
||||
.join(' – ');
|
||||
|
||||
$div.append(i > 0 ? '<br>' : '').append(icon).append(range);
|
||||
});
|
||||
|
||||
return $div.get(0);
|
||||
};
|
||||
legend.addTo(map);
|
||||
};
|
||||
|
||||
/**
|
||||
* Apply style with shading to feature
|
||||
*
|
||||
* @method applyShadingStyle
|
||||
* @param feature {Object}
|
||||
* @param min {Number}
|
||||
* @param max {Number}
|
||||
* @return {Object}
|
||||
*/
|
||||
TileMap.prototype.applyShadingStyle = function (feature, min, max) {
|
||||
var self = this;
|
||||
var value = feature.properties.value;
|
||||
var color = self.quantizeColorScale(value, min, max);
|
||||
|
||||
return {
|
||||
fillColor: color,
|
||||
color: self.darkerColor(color),
|
||||
weight: 1.5,
|
||||
opacity: 1,
|
||||
fillOpacity: 0.75
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Invalidate the size of the map, so that leaflet will resize to fit.
|
||||
* then moves to center
|
||||
|
@ -734,169 +61,10 @@ define(function (require) {
|
|||
*/
|
||||
TileMap.prototype.resizeArea = function () {
|
||||
this.maps.forEach(function (map) {
|
||||
map.invalidateSize({
|
||||
debounceMoveend: true
|
||||
});
|
||||
map.updateSize();
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Binds popup and events to each feature on map
|
||||
*
|
||||
* @method bindPopup
|
||||
* @param feature {Object}
|
||||
* @param layer {Object}
|
||||
* return {undefined}
|
||||
*/
|
||||
TileMap.prototype.bindPopup = function (feature, layer, map) {
|
||||
var self = this;
|
||||
var popup = layer.on({
|
||||
mouseover: function (e) {
|
||||
var layer = e.target;
|
||||
// bring layer to front if not older browser
|
||||
if (!L.Browser.ie && !L.Browser.opera) {
|
||||
layer.bringToFront();
|
||||
}
|
||||
var latlng = L.latLng(feature.geometry.coordinates[0], feature.geometry.coordinates[1]);
|
||||
self.showTooltip(map, feature, latlng);
|
||||
},
|
||||
mouseout: function (e) {
|
||||
map.closePopup();
|
||||
}
|
||||
});
|
||||
|
||||
this.popups.push(popup);
|
||||
};
|
||||
|
||||
/**
|
||||
* retuns data for data for heat map intensity
|
||||
* if heatNormalizeData attribute is checked/true
|
||||
• normalizes data for heat map intensity
|
||||
*
|
||||
* @param mapData {geoJson Object}
|
||||
* @param nax {Number}
|
||||
* @method dataToHeatArray
|
||||
* @return {Array}
|
||||
*/
|
||||
TileMap.prototype.dataToHeatArray = function (max) {
|
||||
var self = this;
|
||||
var mapData = this.geoJson;
|
||||
|
||||
return mapData.features.map(function (feature) {
|
||||
var lat = feature.geometry.coordinates[1];
|
||||
var lng = feature.geometry.coordinates[0];
|
||||
var heatIntensity;
|
||||
|
||||
if (!self._attr.heatNormalizeData) {
|
||||
// show bucket value on heatmap
|
||||
heatIntensity = feature.properties.value;
|
||||
} else {
|
||||
// show bucket value normalized to max value
|
||||
heatIntensity = parseInt(feature.properties.value / max * 100);
|
||||
}
|
||||
|
||||
return [lat, lng, heatIntensity];
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* geohashMinDistance returns a min distance in meters for sizing
|
||||
* circle markers to fit within geohash grid rectangle
|
||||
*
|
||||
* @method geohashMinDistance
|
||||
* @param feature {Object}
|
||||
* @return {Number}
|
||||
*/
|
||||
TileMap.prototype.geohashMinDistance = function (feature) {
|
||||
var centerPoint = feature.properties.center;
|
||||
var geohashRect = feature.properties.rectangle;
|
||||
|
||||
// get lat[1] and lng[0] of geohash center point
|
||||
// apply lat to east[2] and lng to north[3] sides of rectangle
|
||||
// to get radius at center of geohash grid recttangle
|
||||
var center = L.latLng([centerPoint[1], centerPoint[0]]);
|
||||
var east = L.latLng([centerPoint[1], geohashRect[2][0]]);
|
||||
var north = L.latLng([geohashRect[3][1], centerPoint[0]]);
|
||||
|
||||
var eastRadius = Math.floor(center.distanceTo(east));
|
||||
var northRadius = Math.floor(center.distanceTo(north));
|
||||
|
||||
return _.min([eastRadius, northRadius]);
|
||||
};
|
||||
|
||||
/**
|
||||
* radiusScale returns a number for scaled circle markers
|
||||
* square root of value / max
|
||||
* multiplied by a value based on map zoom
|
||||
* multiplied by a value based on data precision
|
||||
* for relative sizing of markers
|
||||
*
|
||||
* @method radiusScale
|
||||
* @param value {Number}
|
||||
* @param max {Number}
|
||||
* @param zoom {Number}
|
||||
* @param precision {Number}
|
||||
* @return {Number}
|
||||
*/
|
||||
TileMap.prototype.radiusScale = function (value, max, zoom, precision) {
|
||||
// exp = 0.5 for square root ratio
|
||||
// exp = 1 for linear ratio
|
||||
var exp = 0.5;
|
||||
var precisionBiasNumerator = 200;
|
||||
var precisionBiasBase = 5;
|
||||
var pct = Math.abs(value) / Math.abs(max);
|
||||
var constantZoomRadius = 0.5 * Math.pow(2, zoom);
|
||||
var precisionScale = precisionBiasNumerator / Math.pow(precisionBiasBase, precision);
|
||||
|
||||
return Math.pow(pct, exp) * constantZoomRadius * precisionScale;
|
||||
};
|
||||
|
||||
/**
|
||||
* d3 quantize scale returns a hex color,
|
||||
* used for marker fill color
|
||||
*
|
||||
* @method quantizeColorScale
|
||||
* @param value {Number}
|
||||
* @param min {Number}
|
||||
* @param max {Number}
|
||||
* @return {String} hex color
|
||||
*/
|
||||
TileMap.prototype.quantizeColorScale = function (value, min, max) {
|
||||
var reds5 = ['#fed976', '#feb24c', '#fd8d3c', '#f03b20', '#bd0026'];
|
||||
var reds3 = ['#fecc5c', '#fd8d3c', '#e31a1c'];
|
||||
var reds1 = ['#ff6128'];
|
||||
var colors = this._attr.colors = reds5;
|
||||
|
||||
if (max - min < 3) {
|
||||
colors = this._attr.colors = reds1;
|
||||
} else if (max - min < 25) {
|
||||
colors = this._attr.colors = reds3;
|
||||
}
|
||||
|
||||
var cScale = this._attr.cScale = d3.scale.quantize()
|
||||
.domain([min, max])
|
||||
.range(colors);
|
||||
|
||||
if (max === min) {
|
||||
return colors[0];
|
||||
} else {
|
||||
return cScale(value);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* d3 method returns a darker hex color,
|
||||
* used for marker stroke color
|
||||
*
|
||||
* @method darkerColor
|
||||
* @param color {String} hex color
|
||||
* @return {String} hex color
|
||||
*/
|
||||
TileMap.prototype.darkerColor = function (color) {
|
||||
var darker = d3.hcl(color).darker(1.3).toString();
|
||||
return darker;
|
||||
};
|
||||
|
||||
/**
|
||||
* clean up the maps
|
||||
*
|
||||
|
@ -904,24 +72,57 @@ define(function (require) {
|
|||
* @return {undefined}
|
||||
*/
|
||||
TileMap.prototype.destroy = function () {
|
||||
if (this.popups) {
|
||||
this.popups.forEach(function (popup) {
|
||||
popup.off('mouseover').off('mouseout');
|
||||
});
|
||||
this.popups = [];
|
||||
this.maps = this.maps.filter(function (map) {
|
||||
map.destroy();
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds allmin and allmax properties to geoJson data
|
||||
*
|
||||
* @method _appendMap
|
||||
* @param selection {Object} d3 selection
|
||||
*/
|
||||
TileMap.prototype._appendGeoExtents = function () {
|
||||
// add allmin and allmax to geoJson
|
||||
var geoMinMax = this.handler.data.getGeoExtents();
|
||||
this.geoJson.properties.allmin = geoMinMax.min;
|
||||
this.geoJson.properties.allmax = geoMinMax.max;
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders map
|
||||
*
|
||||
* @method _appendMap
|
||||
* @param selection {Object} d3 selection
|
||||
*/
|
||||
TileMap.prototype._appendMap = function (selection) {
|
||||
var container = $(selection).addClass('tilemap');
|
||||
|
||||
var map = new Map(container, this._chartData, {
|
||||
// center: this._attr.mapCenter,
|
||||
// zoom: this._attr.mapZoom,
|
||||
events: this.events,
|
||||
markerType: this._attr.mapType,
|
||||
tooltipFormatter: this.tooltipFormatter,
|
||||
valueFormatter: this.valueFormatter,
|
||||
attr: this._attr
|
||||
});
|
||||
|
||||
// add title for splits
|
||||
if (this.title) {
|
||||
map.addTitle(this.title);
|
||||
}
|
||||
|
||||
if (this.maps) {
|
||||
this.maps.forEach(function (map) {
|
||||
if (map.fitControl) {
|
||||
map.fitControl.removeFrom(map);
|
||||
}
|
||||
map.remove();
|
||||
});
|
||||
// add fit to bounds control
|
||||
if (_.get(this.geoJson, 'features.length') > 0) {
|
||||
map.addFitControl();
|
||||
map.addBoundingControl();
|
||||
}
|
||||
|
||||
this.maps.push(map);
|
||||
};
|
||||
|
||||
return TileMap;
|
||||
|
||||
};
|
||||
});
|
||||
|
|
|
@ -34,7 +34,7 @@ define(function (require) {
|
|||
// Use this for cache busting partials
|
||||
.constant('cacheBust', window.KIBANA_COMMIT_SHA)
|
||||
// The minimum Elasticsearch version required to run Kibana
|
||||
.constant('minimumElasticsearchVersion', '2.0.0')
|
||||
.constant('minimumElasticsearchVersion', '2.0.0.beta1')
|
||||
// When we need to identify the current session of the app, ef shard preference
|
||||
.constant('sessionId', Date.now())
|
||||
// attach the route manager's known routes
|
||||
|
|
|
@ -16,12 +16,12 @@ define(function (require) {
|
|||
defaults: {
|
||||
mapType: 'Scaled Circle Markers',
|
||||
isDesaturated: true,
|
||||
addTooltip: true,
|
||||
heatMaxZoom: 16,
|
||||
heatMinOpacity: 0.1,
|
||||
heatRadius: 25,
|
||||
heatBlur: 15,
|
||||
heatNormalizeData: true,
|
||||
addTooltip: true
|
||||
},
|
||||
mapTypes: ['Scaled Circle Markers', 'Shaded Circle Markers', 'Shaded Geohash Grid', 'Heatmap'],
|
||||
canDesaturate: !!supports.cssFilters,
|
||||
|
@ -84,7 +84,8 @@ define(function (require) {
|
|||
18: 12
|
||||
};
|
||||
|
||||
agg.params.precision = Math.min(zoomPrecision[event.zoom], config.get('visualization:tileMap:maxPrecision'));
|
||||
var precision = config.get('visualization:tileMap:maxPrecision');
|
||||
agg.params.precision = Math.min(zoomPrecision[event.zoom], precision);
|
||||
|
||||
courier.fetch();
|
||||
}
|
||||
|
|
|
@ -27,6 +27,12 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<form role="form">
|
||||
<input aria-label="Filter" ng-model="fieldFilter" class="form-control span12" type="text" placeholder="Filter" />
|
||||
</form>
|
||||
|
||||
<br />
|
||||
|
||||
<ul class="nav nav-tabs">
|
||||
<li class="kbn-settings-tab" ng-class="{ active: state.tab === fieldType.index }" ng-repeat="fieldType in fieldTypes">
|
||||
<a ng-click="changeTab(fieldType)">
|
||||
|
|
|
@ -3,3 +3,5 @@
|
|||
rows="rows"
|
||||
per-page="perPage">
|
||||
</paginated-table>
|
||||
|
||||
<p class="text-center default-message" ng-if="rows.length === 0">No matching fields found.</p>
|
||||
|
|
|
@ -3,12 +3,13 @@ define(function (require) {
|
|||
require('components/paginated_table/paginated_table');
|
||||
|
||||
require('modules').get('apps/settings')
|
||||
.directive('indexedFields', function () {
|
||||
.directive('indexedFields', function ($filter) {
|
||||
var yesTemplate = '<i class="fa fa-check" aria-label="yes"></i>';
|
||||
var noTemplate = '';
|
||||
var nameHtml = require('text!plugins/settings/sections/indices/_field_name.html');
|
||||
var typeHtml = require('text!plugins/settings/sections/indices/_field_type.html');
|
||||
var controlsHtml = require('text!plugins/settings/sections/indices/_field_controls.html');
|
||||
var filter = $filter('filter');
|
||||
|
||||
return {
|
||||
restrict: 'E',
|
||||
|
@ -26,11 +27,16 @@ define(function (require) {
|
|||
{ title: 'controls', sortable: false }
|
||||
];
|
||||
|
||||
$scope.$watchCollection('indexPattern.fields', function () {
|
||||
$scope.$watchMulti(['[]indexPattern.fields', 'fieldFilter'], refreshRows);
|
||||
|
||||
function refreshRows() {
|
||||
// clear and destroy row scopes
|
||||
_.invoke(rowScopes.splice(0), '$destroy');
|
||||
|
||||
$scope.rows = $scope.indexPattern.getNonScriptedFields().map(function (field) {
|
||||
var fields = filter($scope.indexPattern.getNonScriptedFields(), $scope.fieldFilter);
|
||||
_.find($scope.fieldTypes, {index: 'indexedFields'}).count = fields.length; // Update the tab count
|
||||
|
||||
$scope.rows = fields.map(function (field) {
|
||||
var childScope = _.assign($scope.$new(), { field: field });
|
||||
rowScopes.push(childScope);
|
||||
|
||||
|
@ -60,7 +66,7 @@ define(function (require) {
|
|||
}
|
||||
];
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
|
|
@ -17,4 +17,4 @@
|
|||
per-page="perPage">
|
||||
</paginated-table>
|
||||
|
||||
<div ng-if="rows.length === 0">No scripted fields</div>
|
||||
<p class="text-center" ng-if="rows.length === 0">No matching scripted fields found.</p>
|
||||
|
|
|
@ -3,9 +3,11 @@ define(function (require) {
|
|||
require('components/paginated_table/paginated_table');
|
||||
|
||||
require('modules').get('apps/settings')
|
||||
.directive('scriptedFields', function (kbnUrl, Notifier) {
|
||||
.directive('scriptedFields', function (kbnUrl, Notifier, $filter) {
|
||||
var rowScopes = []; // track row scopes, so they can be destroyed as needed
|
||||
var popularityHtml = require('text!plugins/settings/sections/indices/_field_popularity.html');
|
||||
var controlsHtml = require('text!plugins/settings/sections/indices/_field_controls.html');
|
||||
var filter = $filter('filter');
|
||||
|
||||
var notify = new Notifier();
|
||||
|
||||
|
@ -27,11 +29,16 @@ define(function (require) {
|
|||
{ title: 'controls', sortable: false }
|
||||
];
|
||||
|
||||
$scope.$watch('indexPattern.fields', function () {
|
||||
$scope.$watchMulti(['[]indexPattern.fields', 'fieldFilter'], refreshRows);
|
||||
|
||||
function refreshRows() {
|
||||
_.invoke(rowScopes, '$destroy');
|
||||
rowScopes.length = 0;
|
||||
|
||||
$scope.rows = $scope.indexPattern.getScriptedFields().map(function (field) {
|
||||
var fields = filter($scope.indexPattern.getScriptedFields(), $scope.fieldFilter);
|
||||
_.find($scope.fieldTypes, {index: 'scriptedFields'}).count = fields.length; // Update the tab count
|
||||
|
||||
$scope.rows = fields.map(function (field) {
|
||||
var rowScope = $scope.$new();
|
||||
rowScope.field = field;
|
||||
rowScopes.push(rowScope);
|
||||
|
@ -46,7 +53,7 @@ define(function (require) {
|
|||
}
|
||||
];
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
$scope.addDateScripts = function () {
|
||||
var conflictFields = [];
|
||||
|
|
|
@ -42,13 +42,13 @@ define(function (require) {
|
|||
$scope.services = _.sortBy(data, 'title');
|
||||
var tab = $scope.services[0];
|
||||
if ($state.tab) tab = _.find($scope.services, {title: $state.tab});
|
||||
$scope.changeTab(tab);
|
||||
|
||||
$scope.$watch('state.tab', function (tab) {
|
||||
if (!tab) $scope.changeTab($scope.services[0]);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
$scope.$watch('state.tab', function (tab) {
|
||||
if (!tab) $scope.changeTab($scope.services[0]);
|
||||
});
|
||||
|
||||
$scope.toggleAll = function () {
|
||||
if ($scope.selectedItems.length === $scope.currentTab.data.length) {
|
||||
|
|
|
@ -180,6 +180,10 @@ kbn-settings-indices {
|
|||
margin: 5px 0;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
p.text-center {
|
||||
padding-top: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -15,10 +15,10 @@ module.exports = function (path) {
|
|||
if (config.ca) settings['elasticsearch.ssl.ca'] = config.ca;
|
||||
if (config.verify_ssl) settings['elasticsearch.ssl.verify'] = config.verify_ssl;
|
||||
if (config.default_app_id) settings['kibana.defaultAppId'] = config.default_app_id;
|
||||
if (config.ping_timeout) settings['elastcsearch.pingTimeout'] = config.ping_timeout;
|
||||
if (config.request_timeout) settings['elastcsearch.requestTimeout'] = config.request_timeout;
|
||||
if (config.shard_timeout) settings['elastcsearch.shardTimeout'] = config.shard_timeout;
|
||||
if (config.startup_timeout) settings['elastcsearch.startupTimeout'] = config.startup_timeout;
|
||||
if (config.ping_timeout) settings['elasticsearch.pingTimeout'] = config.ping_timeout;
|
||||
if (config.request_timeout) settings['elasticsearch.requestTimeout'] = config.request_timeout;
|
||||
if (config.shard_timeout) settings['elasticsearch.shardTimeout'] = config.shard_timeout;
|
||||
if (config.startup_timeout) settings['elasticsearch.startupTimeout'] = config.startup_timeout;
|
||||
if (config.ssl_cert_file) settings['kibana.server.ssl.cert'] = config.ssl_cert_file;
|
||||
if (config.ssl_key_file) settings['kibana.server.ssl.key'] = config.ssl_key_file;
|
||||
if (config.pid_file) settings['config.server.pidFile'] = config.pid_file;
|
||||
|
|
|
@ -6,6 +6,7 @@ var plugins = function (dir) {
|
|||
if (!dir) return [];
|
||||
var files = glob.sync(path.join(dir, '*', 'index.js')) || [];
|
||||
return files.map(function (file) {
|
||||
dir = dir.replace(/\\/g, '/');
|
||||
return file.replace(dir, 'plugins').replace(/\.js$/, '');
|
||||
});
|
||||
};
|
||||
|
@ -23,5 +24,4 @@ module.exports = function (server) {
|
|||
cache = bundled_plugin_ids.concat(bundled_plugins, external_plugins);
|
||||
}
|
||||
return cache;
|
||||
};
|
||||
|
||||
};
|
|
@ -5,17 +5,18 @@ var Promise = require('bluebird');
|
|||
var checkPath = require('../config/check_path');
|
||||
|
||||
module.exports = function (globPath) {
|
||||
globPath = globPath || join( __dirname, '..', '..', 'plugins', '*', 'index.js');
|
||||
globPath = globPath || join(__dirname, '..', '..', 'plugins', '*', 'index.js');
|
||||
return glob.sync(globPath).map(function (file) {
|
||||
var module = require(file);
|
||||
var regex = new RegExp('([^' + path.sep + ']+)' + path.sep + 'index.js');
|
||||
var regex = new RegExp('([^/]+)/index.js');
|
||||
|
||||
var matches = file.match(regex);
|
||||
if (!module.name && matches) {
|
||||
module.name = matches[1];
|
||||
}
|
||||
|
||||
// has a public folder?
|
||||
var publicPath = module.publicPath || join(path.dirname(file), 'public');
|
||||
var publicPath = (module.publicPath || join(path.dirname(file), 'public')).replace(/\\/g, '/');
|
||||
if (checkPath(publicPath)) {
|
||||
module.publicPath = publicPath;
|
||||
if (!module.publicPlugins) {
|
||||
|
@ -28,4 +29,4 @@ module.exports = function (globPath) {
|
|||
|
||||
return module;
|
||||
});
|
||||
};
|
||||
};
|
|
@ -19,7 +19,7 @@
|
|||
<section class="section">
|
||||
<h4>What is this page?</h4>
|
||||
<p>This page is your sanity check, and your savior. You can check for potential problems</p>
|
||||
<p>Here is the status of your kibana instance and the plugins you have installed along with some, statistics to asses potential problems.</p>
|
||||
<p>Here is the status of your kibana instance and the plugins you have installed along with some statistics to asses potential problems.</p>
|
||||
</section>
|
||||
<div class="system_status_wrapper system_status_{{ui.systemStatus().label}}">
|
||||
<h3 class="title">
|
||||
|
|
|
@ -1,261 +1,266 @@
|
|||
window.define(['angular', 'jquery', 'lodash', 'moment', 'numeral', 'text!status/chartTemplate.html', 'nvd3_directives'],
|
||||
function (angular, $, _, moment, numeral, chartTemplate) {
|
||||
define(function (require) {
|
||||
var angular = require('angular');
|
||||
var $ = require('jquery');
|
||||
var _ = require('lodash');
|
||||
var moment = require('moment');
|
||||
var numeral = require('numeral');
|
||||
require('nvd3_directives');
|
||||
|
||||
// Make sure we don't have to deal with statuses by hand
|
||||
function getStatus(plugin) {
|
||||
var statusMap = {
|
||||
green: {
|
||||
label: 'success',
|
||||
msg: 'Ready',
|
||||
idx: 1
|
||||
},
|
||||
yellow: {
|
||||
label: 'warning',
|
||||
msg: 'S.N.A.F.U.',
|
||||
idx: 2
|
||||
},
|
||||
red: {
|
||||
label: 'danger',
|
||||
msg: 'Danger Will Robinson! Danger!',
|
||||
idx: 3
|
||||
},
|
||||
loading: {
|
||||
label: 'info',
|
||||
msg: 'Loading...',
|
||||
idx: 0
|
||||
}
|
||||
};
|
||||
if (!_.isObject(plugin) || _.isUndefined(plugin)) {
|
||||
plugin = {state: plugin};
|
||||
}
|
||||
return statusMap[plugin.state];
|
||||
}
|
||||
function getLabel(plugin) { return getStatus(plugin).label; }
|
||||
|
||||
// The Kibana App
|
||||
angular.module('KibanaStatusApp', ['nvd3'])
|
||||
.controller('StatusPage', ['$scope', '$http', '$window', '$timeout', function ($scope, $http, $window, $timeout) {
|
||||
// the object representing all of the elements the ui touches
|
||||
$scope.ui = {
|
||||
// show the system status by going through all of the plugins,
|
||||
// and making sure they're green.
|
||||
systemStatus: (function () {
|
||||
// for convenience
|
||||
function getIdx(plugin) { return getStatus(plugin).idx; }
|
||||
|
||||
return function () {
|
||||
var currentStatus = 'loading';
|
||||
var currentIdx = getIdx(currentStatus);
|
||||
|
||||
// FIXME eh, not too thrilled about this.
|
||||
var status = _.reduce($scope.ui.plugins, function (curr, plugin, key) {
|
||||
var pluginIdx = getIdx(plugin);
|
||||
if (pluginIdx > currentIdx) {
|
||||
// set the current status
|
||||
currentStatus = plugin.state;
|
||||
currentIdx = getIdx(plugin);
|
||||
}
|
||||
return currentStatus;
|
||||
}, 'loading');
|
||||
|
||||
// give the ui the label for colors and such
|
||||
return getStatus(status);
|
||||
};
|
||||
}()),
|
||||
charts: {},
|
||||
plugins: []
|
||||
};
|
||||
|
||||
var windowHasFocus = true;
|
||||
angular.element($window).bind({
|
||||
blur: function () { windowHasFocus = false; },
|
||||
focus: function () {
|
||||
windowHasFocus = true;
|
||||
getAppStatus();
|
||||
}
|
||||
});
|
||||
|
||||
// To make sure that the alert box doesn't keep showing up
|
||||
var hasHttpError = false;
|
||||
|
||||
function getAppStatus() {
|
||||
// go ahead and get the info you want
|
||||
$http
|
||||
.get('/status/health')
|
||||
.success(function (data) {
|
||||
// Assign the propper variables to the scope and change them as necessary
|
||||
|
||||
// setup The charts
|
||||
// wrap the metrics data and append the average
|
||||
_.mapValues(data.metrics, function (metric, name) {
|
||||
var currentMetricObj = $scope.ui.charts[name];
|
||||
var newMetricObj = {data: metric, key: name};
|
||||
if (currentMetricObj) {
|
||||
currentMetricObj.data = metric;
|
||||
} else {
|
||||
$scope.ui.charts[name] = newMetricObj;
|
||||
}
|
||||
});
|
||||
|
||||
// give the plugins their proper name so CSS classes can be properply applied
|
||||
$scope.ui.plugins = _.mapValues(data.status, function (plugin) {
|
||||
plugin.uiStatus = getLabel(plugin);
|
||||
return plugin;
|
||||
});
|
||||
|
||||
// Finally notify that there has been a succesful request
|
||||
hasHttpError = false;
|
||||
|
||||
if (windowHasFocus) {
|
||||
// go ahead and get another status in 5 seconds
|
||||
$timeout(getAppStatus, 5000);
|
||||
}
|
||||
})
|
||||
.error(function () {
|
||||
if (!hasHttpError) {
|
||||
window.alert('Something went terribly wrong while making the request!!! Perhaps your server is down?');
|
||||
hasHttpError = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Start it all up
|
||||
getAppStatus();
|
||||
}])
|
||||
.directive('kbSparkline', function () {
|
||||
var directiveDef = {
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
data: '=chartData',
|
||||
key: '=chartKey'
|
||||
},
|
||||
template: chartTemplate,
|
||||
link: function ($scope, $el, attrs) {
|
||||
|
||||
var metricNumberType = numberType($scope.key);
|
||||
|
||||
var options = makeChartOptions(metricNumberType);
|
||||
$scope.chart = { niceName: niceName($scope.key), options: options };
|
||||
$scope.$watch('data', function (newData) {
|
||||
var metricList = convertData(newData);
|
||||
var average = calcAvg(metricList);
|
||||
$scope.chart.data = metricList;
|
||||
$scope.chart.average = average;
|
||||
});
|
||||
|
||||
|
||||
function convertData(data) {
|
||||
// Metric Values format
|
||||
// metric: [[xValue, yValue], ...]
|
||||
// LoadMetric:
|
||||
// metric: [[xValue, [yValue, yValue2, yValue3]], ...]
|
||||
// return [
|
||||
// {type: 'line', key: name, yAxis: 1, values: [{x: xValue, y: yValue}, ...]},
|
||||
// {type: 'line', key: name, yAxis: 1, values: [{x: xValue, y: yValue1}, ...]},
|
||||
// {type: 'line', key: name, yAxis: 1, values: [{x: xValue, y: yValue2}, ...]}]
|
||||
//
|
||||
// Go through all of the metric values and split the values out.
|
||||
// returns an array of all of the averages
|
||||
|
||||
var metricList = [];
|
||||
data.forEach(function (vector) {
|
||||
vector = _.flatten(vector);
|
||||
var x = vector.shift();
|
||||
vector.forEach(function (yValue, idx) {
|
||||
if (!metricList[idx]) {
|
||||
metricList[idx] = {
|
||||
key: idx,
|
||||
values: []
|
||||
};
|
||||
}
|
||||
// unshift to make sure they're in the correct order
|
||||
metricList[idx].values.unshift({x: x, y: yValue});
|
||||
});
|
||||
});
|
||||
return metricList;
|
||||
}
|
||||
|
||||
function calcAvg(metricList) {
|
||||
return metricList.map(function (data) {
|
||||
var uglySum = data.values.reduce(function (sumSoFar, vector) {
|
||||
return sumSoFar + vector.y;
|
||||
}, 0);
|
||||
return formatNumber(uglySum / data.values.length, metricNumberType);
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
// Turns thisIsASentence to
|
||||
// This Is A Sentence
|
||||
function niceName(name) {
|
||||
return name
|
||||
.split(/(?=[A-Z])/)
|
||||
.map(function (word) { return word[0].toUpperCase() + _.rest(word).join(''); })
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
function formatNumber(num, which) {
|
||||
var format = '0.00';
|
||||
var postfix = '';
|
||||
switch (which) {
|
||||
case 'time':
|
||||
return moment(num).format('HH:mm:ss');
|
||||
case 'byte':
|
||||
format += 'b';
|
||||
break;
|
||||
case 'ms':
|
||||
postfix = 'ms';
|
||||
break;
|
||||
}
|
||||
return numeral(num).format(format) + postfix;
|
||||
}
|
||||
function numberType(key) {
|
||||
var byteMetrics = ['heapTotal', 'heapUsed', 'rss'];
|
||||
var msMetrics = ['delay', 'responseTimeAvg', 'responseTimeMax'];
|
||||
var preciseMetric = ['requests', 'load'];
|
||||
if ( byteMetrics.indexOf(key) > -1 ) {
|
||||
return 'byte';
|
||||
} else if (msMetrics.indexOf(key) > -1 ) {
|
||||
return 'ms';
|
||||
} else {
|
||||
return 'precise';
|
||||
}
|
||||
}
|
||||
var makeChartOptions = _.memoize(function (type) {
|
||||
return {
|
||||
chart: {
|
||||
type: 'lineChart',
|
||||
height: 200,
|
||||
showLegend: false,
|
||||
showXAxis: false,
|
||||
showYAxis: false,
|
||||
useInteractiveGuideline: true,
|
||||
tooltips: true,
|
||||
pointSize: 0,
|
||||
color: ['#444', '#777', '#aaa'],
|
||||
margin: {
|
||||
top: 10,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 20
|
||||
},
|
||||
xAxis: { tickFormat: function (d) { return formatNumber(d, 'time'); } },
|
||||
yAxis: { tickFormat: function (d) { return formatNumber(d, type); }, },
|
||||
y: function (d) { return d.y; },
|
||||
x: function (d) { return d.x; }
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
return directiveDef;
|
||||
});
|
||||
|
||||
return {
|
||||
init: function () {
|
||||
$(function () {
|
||||
angular.bootstrap(window.document, ['nvd3', 'KibanaStatusApp']);
|
||||
});
|
||||
// Make sure we don't have to deal with statuses by hand
|
||||
function getStatus(plugin) {
|
||||
var statusMap = {
|
||||
green: {
|
||||
label: 'success',
|
||||
msg: 'Ready',
|
||||
idx: 1
|
||||
},
|
||||
yellow: {
|
||||
label: 'warning',
|
||||
msg: 'S.N.A.F.U.',
|
||||
idx: 2
|
||||
},
|
||||
red: {
|
||||
label: 'danger',
|
||||
msg: 'Danger Will Robinson! Danger!',
|
||||
idx: 3
|
||||
},
|
||||
loading: {
|
||||
label: 'info',
|
||||
msg: 'Loading...',
|
||||
idx: 0
|
||||
}
|
||||
};
|
||||
if (!_.isObject(plugin) || _.isUndefined(plugin)) {
|
||||
plugin = {state: plugin};
|
||||
}
|
||||
return statusMap[plugin.state];
|
||||
}
|
||||
function getLabel(plugin) { return getStatus(plugin).label; }
|
||||
|
||||
});
|
||||
// The Kibana App
|
||||
angular.module('KibanaStatusApp', ['nvd3'])
|
||||
.controller('StatusPage', ['$scope', '$http', '$window', '$timeout', function ($scope, $http, $window, $timeout) {
|
||||
// the object representing all of the elements the ui touches
|
||||
$scope.ui = {
|
||||
// show the system status by going through all of the plugins,
|
||||
// and making sure they're green.
|
||||
systemStatus: (function () {
|
||||
// for convenience
|
||||
function getIdx(plugin) { return getStatus(plugin).idx; }
|
||||
|
||||
return function () {
|
||||
var currentStatus = 'loading';
|
||||
var currentIdx = getIdx(currentStatus);
|
||||
|
||||
// FIXME eh, not too thrilled about this.
|
||||
var status = _.reduce($scope.ui.plugins, function (curr, plugin, key) {
|
||||
var pluginIdx = getIdx(plugin);
|
||||
if (pluginIdx > currentIdx) {
|
||||
// set the current status
|
||||
currentStatus = plugin.state;
|
||||
currentIdx = getIdx(plugin);
|
||||
}
|
||||
return currentStatus;
|
||||
}, 'loading');
|
||||
|
||||
// give the ui the label for colors and such
|
||||
return getStatus(status);
|
||||
};
|
||||
}()),
|
||||
charts: {},
|
||||
plugins: []
|
||||
};
|
||||
|
||||
var windowHasFocus = true;
|
||||
angular.element($window).bind({
|
||||
blur: function () { windowHasFocus = false; },
|
||||
focus: function () {
|
||||
windowHasFocus = true;
|
||||
getAppStatus();
|
||||
}
|
||||
});
|
||||
|
||||
// To make sure that the alert box doesn't keep showing up
|
||||
var hasHttpError = false;
|
||||
|
||||
function getAppStatus() {
|
||||
// go ahead and get the info you want
|
||||
$http
|
||||
.get('/status/health')
|
||||
.success(function (data) {
|
||||
// Assign the propper variables to the scope and change them as necessary
|
||||
|
||||
// setup The charts
|
||||
// wrap the metrics data and append the average
|
||||
_.mapValues(data.metrics, function (metric, name) {
|
||||
var currentMetricObj = $scope.ui.charts[name];
|
||||
var newMetricObj = {data: metric, key: name};
|
||||
if (currentMetricObj) {
|
||||
currentMetricObj.data = metric;
|
||||
} else {
|
||||
$scope.ui.charts[name] = newMetricObj;
|
||||
}
|
||||
});
|
||||
|
||||
// give the plugins their proper name so CSS classes can be properply applied
|
||||
$scope.ui.plugins = _.mapValues(data.status, function (plugin) {
|
||||
plugin.uiStatus = getLabel(plugin);
|
||||
return plugin;
|
||||
});
|
||||
|
||||
// Finally notify that there has been a succesful request
|
||||
hasHttpError = false;
|
||||
|
||||
if (windowHasFocus) {
|
||||
// go ahead and get another status in 5 seconds
|
||||
$timeout(getAppStatus, 5000);
|
||||
}
|
||||
})
|
||||
.error(function () {
|
||||
if (!hasHttpError) {
|
||||
window.alert('Something went terribly wrong while making the request!!! Perhaps your server is down?');
|
||||
hasHttpError = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Start it all up
|
||||
getAppStatus();
|
||||
}])
|
||||
.directive('kbSparkline', function () {
|
||||
var directiveDef = {
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
data: '=chartData',
|
||||
key: '=chartKey'
|
||||
},
|
||||
template: require('text!status/chartTemplate.html'),
|
||||
link: function ($scope, $el, attrs) {
|
||||
|
||||
var metricNumberType = numberType($scope.key);
|
||||
|
||||
var options = makeChartOptions(metricNumberType);
|
||||
$scope.chart = { niceName: niceName($scope.key), options: options };
|
||||
$scope.$watch('data', function (newData) {
|
||||
var metricList = convertData(newData);
|
||||
var average = calcAvg(metricList);
|
||||
$scope.chart.data = metricList;
|
||||
$scope.chart.average = average;
|
||||
});
|
||||
|
||||
|
||||
function convertData(data) {
|
||||
// Metric Values format
|
||||
// metric: [[xValue, yValue], ...]
|
||||
// LoadMetric:
|
||||
// metric: [[xValue, [yValue, yValue2, yValue3]], ...]
|
||||
// return [
|
||||
// {type: 'line', key: name, yAxis: 1, values: [{x: xValue, y: yValue}, ...]},
|
||||
// {type: 'line', key: name, yAxis: 1, values: [{x: xValue, y: yValue1}, ...]},
|
||||
// {type: 'line', key: name, yAxis: 1, values: [{x: xValue, y: yValue2}, ...]}]
|
||||
//
|
||||
// Go through all of the metric values and split the values out.
|
||||
// returns an array of all of the averages
|
||||
|
||||
var metricList = [];
|
||||
data.forEach(function (vector) {
|
||||
vector = _.flatten(vector);
|
||||
var x = vector.shift();
|
||||
vector.forEach(function (yValue, idx) {
|
||||
if (!metricList[idx]) {
|
||||
metricList[idx] = {
|
||||
key: idx,
|
||||
values: []
|
||||
};
|
||||
}
|
||||
// unshift to make sure they're in the correct order
|
||||
metricList[idx].values.unshift({x: x, y: yValue});
|
||||
});
|
||||
});
|
||||
return metricList;
|
||||
}
|
||||
|
||||
function calcAvg(metricList) {
|
||||
return metricList.map(function (data) {
|
||||
var uglySum = data.values.reduce(function (sumSoFar, vector) {
|
||||
return sumSoFar + vector.y;
|
||||
}, 0);
|
||||
return formatNumber(uglySum / data.values.length, metricNumberType);
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
// Turns thisIsASentence to
|
||||
// This Is A Sentence
|
||||
function niceName(name) {
|
||||
return name
|
||||
.split(/(?=[A-Z])/)
|
||||
.map(function (word) { return word[0].toUpperCase() + _.rest(word).join(''); })
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
function formatNumber(num, which) {
|
||||
var format = '0.00';
|
||||
var postfix = '';
|
||||
switch (which) {
|
||||
case 'time':
|
||||
return moment(num).format('HH:mm:ss');
|
||||
case 'byte':
|
||||
format += 'b';
|
||||
break;
|
||||
case 'ms':
|
||||
postfix = 'ms';
|
||||
break;
|
||||
}
|
||||
return numeral(num).format(format) + postfix;
|
||||
}
|
||||
function numberType(key) {
|
||||
var byteMetrics = ['heapTotal', 'heapUsed', 'rss'];
|
||||
var msMetrics = ['delay', 'responseTimeAvg', 'responseTimeMax'];
|
||||
var preciseMetric = ['requests', 'load'];
|
||||
if ( byteMetrics.indexOf(key) > -1 ) {
|
||||
return 'byte';
|
||||
} else if (msMetrics.indexOf(key) > -1 ) {
|
||||
return 'ms';
|
||||
} else {
|
||||
return 'precise';
|
||||
}
|
||||
}
|
||||
var makeChartOptions = _.memoize(function (type) {
|
||||
return {
|
||||
chart: {
|
||||
type: 'lineChart',
|
||||
height: 200,
|
||||
showLegend: false,
|
||||
showXAxis: false,
|
||||
showYAxis: false,
|
||||
useInteractiveGuideline: true,
|
||||
tooltips: true,
|
||||
pointSize: 0,
|
||||
color: ['#444', '#777', '#aaa'],
|
||||
margin: {
|
||||
top: 10,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 20
|
||||
},
|
||||
xAxis: { tickFormat: function (d) { return formatNumber(d, 'time'); } },
|
||||
yAxis: { tickFormat: function (d) { return formatNumber(d, type); }, },
|
||||
y: function (d) { return d.y; },
|
||||
x: function (d) { return d.x; }
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
return directiveDef;
|
||||
});
|
||||
|
||||
return {
|
||||
init: function () {
|
||||
$(function () {
|
||||
angular.bootstrap(window.document, ['nvd3', 'KibanaStatusApp']);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
});
|
||||
|
|
|
@ -73,7 +73,7 @@
|
|||
}
|
||||
.plugin_key {
|
||||
font-weight:bold;
|
||||
padding:0;
|
||||
padding:0px 5px;
|
||||
}
|
||||
.plugin_status_danger {
|
||||
color:#da1e04;
|
||||
|
|
|
@ -35,7 +35,8 @@ module.exports = function (grunt) {
|
|||
'FileSaver@undefined': ['MIT'],
|
||||
'cycle@1.0.3': ['Public-Domain'],
|
||||
'pkginfo@0.2.3': ['MIT'],
|
||||
'uglify-js@2.2.5': ['BSD']
|
||||
'uglify-js@2.2.5': ['BSD'],
|
||||
'amdefine@0.1.1': ['BSD-3-Clause', 'MIT']
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -37,11 +37,12 @@ module.exports = function (grunt) {
|
|||
var invalidLicenses = _.filter(licenseStats, function (pkg) { return !pkg.valid;});
|
||||
|
||||
if (grunt.option('only-invalid')) {
|
||||
console.log(invalidLicenses);
|
||||
grunt.log.debug(JSON.stringify(invalidLicenses, null, 2));
|
||||
} else {
|
||||
console.log(licenseStats);
|
||||
grunt.log.debug(JSON.stringify(licenseStats, null, 2));
|
||||
}
|
||||
|
||||
|
||||
if (invalidLicenses.length) {
|
||||
grunt.fail.warn('Non-confirming licenses: ' + _.pluck(invalidLicenses, 'name').join(', ') +
|
||||
'. Use --only-invalid for details.', invalidLicenses.length);
|
||||
|
|
21
test/unit/fixtures/tilemap_map.js
Normal file
21
test/unit/fixtures/tilemap_map.js
Normal file
|
@ -0,0 +1,21 @@
|
|||
define(function (require) {
|
||||
var sinon = require('test_utils/auto_release_sinon');
|
||||
|
||||
function MockMap(container, chartData, params) {
|
||||
this.container = container;
|
||||
this.chartData = chartData;
|
||||
this.params = params;
|
||||
|
||||
// stub required methods
|
||||
this.addStubs();
|
||||
}
|
||||
|
||||
MockMap.prototype.addStubs = function () {
|
||||
this.addTitle = sinon.stub();
|
||||
this.addFitControl = sinon.stub();
|
||||
this.addBoundingControl = sinon.stub();
|
||||
this.destroy = sinon.stub();
|
||||
};
|
||||
|
||||
return MockMap;
|
||||
});
|
|
@ -98,49 +98,75 @@ define(function (require) {
|
|||
});
|
||||
|
||||
describe('properties', function () {
|
||||
it('includes one feature per row in the table', function () {
|
||||
this.timeout(0);
|
||||
describe('includes one feature per row in the table', function () {
|
||||
this.timeout(60000);
|
||||
|
||||
var table = makeTable();
|
||||
var chart = makeSingleChart(table);
|
||||
var geoColI = _.findIndex(table.columns, { aggConfig: aggs.geo });
|
||||
var metricColI = _.findIndex(table.columns, { aggConfig: aggs.metric });
|
||||
var table;
|
||||
var chart;
|
||||
var geoColI;
|
||||
var metricColI;
|
||||
|
||||
table.rows.forEach(function (row, i) {
|
||||
var feature = chart.geoJson.features[i];
|
||||
expect(feature).to.have.property('geometry');
|
||||
expect(feature.geometry).to.be.an('object');
|
||||
expect(feature).to.have.property('properties');
|
||||
expect(feature.properties).to.be.an('object');
|
||||
before(function () {
|
||||
table = makeTable();
|
||||
chart = makeSingleChart(table);
|
||||
geoColI = _.findIndex(table.columns, { aggConfig: aggs.geo });
|
||||
metricColI = _.findIndex(table.columns, { aggConfig: aggs.metric });
|
||||
});
|
||||
|
||||
var geometry = feature.geometry;
|
||||
expect(geometry.type).to.be('Point');
|
||||
expect(geometry).to.have.property('coordinates');
|
||||
expect(geometry.coordinates).to.be.an('array');
|
||||
expect(geometry.coordinates).to.have.length(2);
|
||||
expect(geometry.coordinates[0]).to.be.a('number');
|
||||
expect(geometry.coordinates[1]).to.be.a('number');
|
||||
it('should be geoJson format', function () {
|
||||
table.rows.forEach(function (row, i) {
|
||||
var feature = chart.geoJson.features[i];
|
||||
expect(feature).to.have.property('geometry');
|
||||
expect(feature.geometry).to.be.an('object');
|
||||
expect(feature).to.have.property('properties');
|
||||
expect(feature.properties).to.be.an('object');
|
||||
});
|
||||
});
|
||||
|
||||
var props = feature.properties;
|
||||
expect(props).to.be.an('object');
|
||||
expect(props).to.only.have.keys(
|
||||
'value', 'geohash', 'aggConfigResult',
|
||||
'rectangle', 'center'
|
||||
);
|
||||
it('should have valid geometry data', function () {
|
||||
table.rows.forEach(function (row, i) {
|
||||
var geometry = chart.geoJson.features[i].geometry;
|
||||
expect(geometry.type).to.be('Point');
|
||||
expect(geometry).to.have.property('coordinates');
|
||||
expect(geometry.coordinates).to.be.an('array');
|
||||
expect(geometry.coordinates).to.have.length(2);
|
||||
expect(geometry.coordinates[0]).to.be.a('number');
|
||||
expect(geometry.coordinates[1]).to.be.a('number');
|
||||
});
|
||||
});
|
||||
|
||||
expect(props.center).to.eql(geometry.coordinates);
|
||||
if (props.value != null) expect(props.value).to.be.a('number');
|
||||
expect(props.geohash).to.be.a('string');
|
||||
it('should have value properties data', function () {
|
||||
table.rows.forEach(function (row, i) {
|
||||
var props = chart.geoJson.features[i].properties;
|
||||
var keys = ['value', 'geohash', 'aggConfigResult', 'rectangle', 'center'];
|
||||
expect(props).to.be.an('object');
|
||||
expect(props).to.only.have.keys(keys);
|
||||
expect(props.geohash).to.be.a('string');
|
||||
if (props.value != null) expect(props.value).to.be.a('number');
|
||||
});
|
||||
});
|
||||
|
||||
if (tableOpts.asAggConfigResults) {
|
||||
expect(props.aggConfigResult).to.be(row[metricColI]);
|
||||
expect(props.value).to.be(row[metricColI].value);
|
||||
expect(props.geohash).to.be(row[geoColI].value);
|
||||
} else {
|
||||
expect(props.aggConfigResult).to.be(null);
|
||||
expect(props.value).to.be(row[metricColI]);
|
||||
expect(props.geohash).to.be(row[geoColI]);
|
||||
}
|
||||
it('should use latLng in properties and lngLat in geometry', function () {
|
||||
table.rows.forEach(function (row, i) {
|
||||
var geometry = chart.geoJson.features[i].geometry;
|
||||
var props = chart.geoJson.features[i].properties;
|
||||
expect(props.center).to.eql(geometry.coordinates.slice(0).reverse());
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle both AggConfig and non-AggConfig results', function () {
|
||||
table.rows.forEach(function (row, i) {
|
||||
var props = chart.geoJson.features[i].properties;
|
||||
if (tableOpts.asAggConfigResults) {
|
||||
expect(props.aggConfigResult).to.be(row[metricColI]);
|
||||
expect(props.value).to.be(row[metricColI].value);
|
||||
expect(props.geohash).to.be(row[geoColI].value);
|
||||
} else {
|
||||
expect(props.aggConfigResult).to.be(null);
|
||||
expect(props.value).to.be(row[metricColI]);
|
||||
expect(props.geohash).to.be(row[geoColI]);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -52,17 +52,17 @@ define(function (require) {
|
|||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
45,
|
||||
0
|
||||
],
|
||||
[
|
||||
45,
|
||||
45
|
||||
],
|
||||
[
|
||||
0,
|
||||
45
|
||||
],
|
||||
[
|
||||
45,
|
||||
45
|
||||
],
|
||||
[
|
||||
45,
|
||||
0
|
||||
]
|
||||
]
|
||||
}
|
||||
|
@ -110,20 +110,20 @@ define(function (require) {
|
|||
},
|
||||
'rectangle': [
|
||||
[
|
||||
90,
|
||||
0
|
||||
0,
|
||||
90
|
||||
],
|
||||
[
|
||||
135,
|
||||
0
|
||||
0,
|
||||
135
|
||||
],
|
||||
[
|
||||
135,
|
||||
45
|
||||
45,
|
||||
135
|
||||
],
|
||||
[
|
||||
90,
|
||||
45
|
||||
45,
|
||||
90
|
||||
]
|
||||
]
|
||||
}
|
||||
|
@ -171,20 +171,20 @@ define(function (require) {
|
|||
},
|
||||
'rectangle': [
|
||||
[
|
||||
-90,
|
||||
-45
|
||||
-45,
|
||||
-90
|
||||
],
|
||||
[
|
||||
-45,
|
||||
-45
|
||||
],
|
||||
[
|
||||
-45,
|
||||
0
|
||||
0,
|
||||
-45
|
||||
],
|
||||
[
|
||||
-90,
|
||||
0
|
||||
0,
|
||||
-90
|
||||
]
|
||||
]
|
||||
}
|
||||
|
@ -232,20 +232,20 @@ define(function (require) {
|
|||
},
|
||||
'rectangle': [
|
||||
[
|
||||
-90,
|
||||
0
|
||||
0,
|
||||
-90
|
||||
],
|
||||
[
|
||||
-45,
|
||||
0
|
||||
0,
|
||||
-45
|
||||
],
|
||||
[
|
||||
-45,
|
||||
45
|
||||
45,
|
||||
-45
|
||||
],
|
||||
[
|
||||
-90,
|
||||
45
|
||||
45,
|
||||
-90
|
||||
]
|
||||
]
|
||||
}
|
||||
|
@ -293,20 +293,20 @@ define(function (require) {
|
|||
},
|
||||
'rectangle': [
|
||||
[
|
||||
0,
|
||||
45
|
||||
45,
|
||||
0
|
||||
],
|
||||
[
|
||||
45,
|
||||
45
|
||||
],
|
||||
[
|
||||
45,
|
||||
90
|
||||
90,
|
||||
45
|
||||
],
|
||||
[
|
||||
0,
|
||||
90
|
||||
90,
|
||||
0
|
||||
]
|
||||
]
|
||||
}
|
||||
|
@ -354,17 +354,17 @@ define(function (require) {
|
|||
},
|
||||
'rectangle': [
|
||||
[
|
||||
45,
|
||||
0
|
||||
],
|
||||
[
|
||||
90,
|
||||
0
|
||||
],
|
||||
[
|
||||
90,
|
||||
0,
|
||||
45
|
||||
],
|
||||
[
|
||||
0,
|
||||
90
|
||||
],
|
||||
[
|
||||
45,
|
||||
90
|
||||
],
|
||||
[
|
||||
45,
|
||||
45
|
||||
|
@ -415,17 +415,17 @@ define(function (require) {
|
|||
},
|
||||
'rectangle': [
|
||||
[
|
||||
0,
|
||||
-45
|
||||
],
|
||||
[
|
||||
45,
|
||||
-45
|
||||
],
|
||||
[
|
||||
45,
|
||||
-45,
|
||||
0
|
||||
],
|
||||
[
|
||||
-45,
|
||||
45
|
||||
],
|
||||
[
|
||||
0,
|
||||
45
|
||||
],
|
||||
[
|
||||
0,
|
||||
0
|
||||
|
@ -476,20 +476,20 @@ define(function (require) {
|
|||
},
|
||||
'rectangle': [
|
||||
[
|
||||
-135,
|
||||
0
|
||||
0,
|
||||
-135
|
||||
],
|
||||
[
|
||||
-90,
|
||||
0
|
||||
0,
|
||||
-90
|
||||
],
|
||||
[
|
||||
-90,
|
||||
45
|
||||
45,
|
||||
-90
|
||||
],
|
||||
[
|
||||
-135,
|
||||
45
|
||||
45,
|
||||
-135
|
||||
]
|
||||
]
|
||||
}
|
||||
|
@ -537,20 +537,20 @@ define(function (require) {
|
|||
},
|
||||
'rectangle': [
|
||||
[
|
||||
-45,
|
||||
0
|
||||
0,
|
||||
-45
|
||||
],
|
||||
[
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
0,
|
||||
45
|
||||
45,
|
||||
0
|
||||
],
|
||||
[
|
||||
-45,
|
||||
45
|
||||
45,
|
||||
-45
|
||||
]
|
||||
]
|
||||
}
|
||||
|
@ -601,17 +601,17 @@ define(function (require) {
|
|||
45,
|
||||
45
|
||||
],
|
||||
[
|
||||
90,
|
||||
45
|
||||
],
|
||||
[
|
||||
90,
|
||||
90
|
||||
],
|
||||
[
|
||||
45,
|
||||
90
|
||||
],
|
||||
[
|
||||
90,
|
||||
90
|
||||
],
|
||||
[
|
||||
90,
|
||||
45
|
||||
]
|
||||
]
|
||||
}
|
||||
|
@ -659,20 +659,20 @@ define(function (require) {
|
|||
},
|
||||
'rectangle': [
|
||||
[
|
||||
135,
|
||||
-45
|
||||
-45,
|
||||
135
|
||||
],
|
||||
[
|
||||
180,
|
||||
-45
|
||||
-45,
|
||||
180
|
||||
],
|
||||
[
|
||||
180,
|
||||
0
|
||||
0,
|
||||
180
|
||||
],
|
||||
[
|
||||
135,
|
||||
0
|
||||
0,
|
||||
135
|
||||
]
|
||||
]
|
||||
}
|
||||
|
@ -720,17 +720,17 @@ define(function (require) {
|
|||
},
|
||||
'rectangle': [
|
||||
[
|
||||
90,
|
||||
45
|
||||
],
|
||||
[
|
||||
135,
|
||||
45
|
||||
],
|
||||
[
|
||||
135,
|
||||
45,
|
||||
90
|
||||
],
|
||||
[
|
||||
45,
|
||||
135
|
||||
],
|
||||
[
|
||||
90,
|
||||
135
|
||||
],
|
||||
[
|
||||
90,
|
||||
90
|
||||
|
@ -781,20 +781,20 @@ define(function (require) {
|
|||
},
|
||||
'rectangle': [
|
||||
[
|
||||
-135,
|
||||
45
|
||||
45,
|
||||
-135
|
||||
],
|
||||
[
|
||||
-90,
|
||||
45
|
||||
45,
|
||||
-90
|
||||
],
|
||||
[
|
||||
-90,
|
||||
90
|
||||
90,
|
||||
-90
|
||||
],
|
||||
[
|
||||
-135,
|
||||
90
|
||||
90,
|
||||
-135
|
||||
]
|
||||
]
|
||||
}
|
||||
|
@ -842,20 +842,20 @@ define(function (require) {
|
|||
},
|
||||
'rectangle': [
|
||||
[
|
||||
90,
|
||||
-45
|
||||
-45,
|
||||
90
|
||||
],
|
||||
[
|
||||
135,
|
||||
-45
|
||||
-45,
|
||||
135
|
||||
],
|
||||
[
|
||||
135,
|
||||
0
|
||||
0,
|
||||
135
|
||||
],
|
||||
[
|
||||
90,
|
||||
0
|
||||
0,
|
||||
90
|
||||
]
|
||||
]
|
||||
}
|
||||
|
@ -906,17 +906,17 @@ define(function (require) {
|
|||
-45,
|
||||
-45
|
||||
],
|
||||
[
|
||||
0,
|
||||
-45
|
||||
],
|
||||
[
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
-45,
|
||||
0
|
||||
],
|
||||
[
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
0,
|
||||
-45
|
||||
]
|
||||
]
|
||||
}
|
||||
|
@ -964,20 +964,20 @@ define(function (require) {
|
|||
},
|
||||
'rectangle': [
|
||||
[
|
||||
-90,
|
||||
45
|
||||
45,
|
||||
-90
|
||||
],
|
||||
[
|
||||
-45,
|
||||
45
|
||||
45,
|
||||
-45
|
||||
],
|
||||
[
|
||||
-45,
|
||||
90
|
||||
90,
|
||||
-45
|
||||
],
|
||||
[
|
||||
-90,
|
||||
90
|
||||
90,
|
||||
-90
|
||||
]
|
||||
]
|
||||
}
|
||||
|
@ -1025,20 +1025,20 @@ define(function (require) {
|
|||
},
|
||||
'rectangle': [
|
||||
[
|
||||
-180,
|
||||
45
|
||||
45,
|
||||
-180
|
||||
],
|
||||
[
|
||||
-135,
|
||||
45
|
||||
45,
|
||||
-135
|
||||
],
|
||||
[
|
||||
-135,
|
||||
90
|
||||
90,
|
||||
-135
|
||||
],
|
||||
[
|
||||
-180,
|
||||
90
|
||||
90,
|
||||
-180
|
||||
]
|
||||
]
|
||||
}
|
||||
|
@ -1086,20 +1086,20 @@ define(function (require) {
|
|||
},
|
||||
'rectangle': [
|
||||
[
|
||||
-45,
|
||||
45
|
||||
45,
|
||||
-45
|
||||
],
|
||||
[
|
||||
0,
|
||||
45
|
||||
45,
|
||||
0
|
||||
],
|
||||
[
|
||||
0,
|
||||
90
|
||||
90,
|
||||
0
|
||||
],
|
||||
[
|
||||
-45,
|
||||
90
|
||||
90,
|
||||
-45
|
||||
]
|
||||
]
|
||||
}
|
||||
|
@ -1147,20 +1147,20 @@ define(function (require) {
|
|||
},
|
||||
'rectangle': [
|
||||
[
|
||||
135,
|
||||
45
|
||||
45,
|
||||
135
|
||||
],
|
||||
[
|
||||
180,
|
||||
45
|
||||
45,
|
||||
180
|
||||
],
|
||||
[
|
||||
180,
|
||||
90
|
||||
90,
|
||||
180
|
||||
],
|
||||
[
|
||||
135,
|
||||
90
|
||||
90,
|
||||
135
|
||||
]
|
||||
]
|
||||
}
|
||||
|
@ -1208,20 +1208,20 @@ define(function (require) {
|
|||
},
|
||||
'rectangle': [
|
||||
[
|
||||
135,
|
||||
0
|
||||
0,
|
||||
135
|
||||
],
|
||||
[
|
||||
180,
|
||||
0
|
||||
0,
|
||||
180
|
||||
],
|
||||
[
|
||||
180,
|
||||
45
|
||||
45,
|
||||
180
|
||||
],
|
||||
[
|
||||
135,
|
||||
45
|
||||
45,
|
||||
135
|
||||
]
|
||||
]
|
||||
}
|
||||
|
@ -1272,17 +1272,17 @@ define(function (require) {
|
|||
-90,
|
||||
-90
|
||||
],
|
||||
[
|
||||
-45,
|
||||
-90
|
||||
],
|
||||
[
|
||||
-45,
|
||||
-45
|
||||
],
|
||||
[
|
||||
-90,
|
||||
-45
|
||||
],
|
||||
[
|
||||
-45,
|
||||
-45
|
||||
],
|
||||
[
|
||||
-45,
|
||||
-90
|
||||
]
|
||||
]
|
||||
}
|
||||
|
@ -1330,20 +1330,20 @@ define(function (require) {
|
|||
},
|
||||
'rectangle': [
|
||||
[
|
||||
45,
|
||||
-45
|
||||
-45,
|
||||
45
|
||||
],
|
||||
[
|
||||
90,
|
||||
-45
|
||||
-45,
|
||||
90
|
||||
],
|
||||
[
|
||||
90,
|
||||
0
|
||||
0,
|
||||
90
|
||||
],
|
||||
[
|
||||
45,
|
||||
0
|
||||
0,
|
||||
45
|
||||
]
|
||||
]
|
||||
}
|
||||
|
@ -1391,17 +1391,17 @@ define(function (require) {
|
|||
},
|
||||
'rectangle': [
|
||||
[
|
||||
-45,
|
||||
-90
|
||||
],
|
||||
[
|
||||
0,
|
||||
-90
|
||||
],
|
||||
[
|
||||
0,
|
||||
-90,
|
||||
-45
|
||||
],
|
||||
[
|
||||
-90,
|
||||
0
|
||||
],
|
||||
[
|
||||
-45,
|
||||
0
|
||||
],
|
||||
[
|
||||
-45,
|
||||
-45
|
||||
|
@ -1452,20 +1452,20 @@ define(function (require) {
|
|||
},
|
||||
'rectangle': [
|
||||
[
|
||||
135,
|
||||
-90
|
||||
-90,
|
||||
135
|
||||
],
|
||||
[
|
||||
180,
|
||||
-90
|
||||
-90,
|
||||
180
|
||||
],
|
||||
[
|
||||
180,
|
||||
-45
|
||||
-45,
|
||||
180
|
||||
],
|
||||
[
|
||||
135,
|
||||
-45
|
||||
-45,
|
||||
135
|
||||
]
|
||||
]
|
||||
}
|
||||
|
@ -1513,20 +1513,20 @@ define(function (require) {
|
|||
},
|
||||
'rectangle': [
|
||||
[
|
||||
-180,
|
||||
-45
|
||||
-45,
|
||||
-180
|
||||
],
|
||||
[
|
||||
-135,
|
||||
-45
|
||||
-45,
|
||||
-135
|
||||
],
|
||||
[
|
||||
-135,
|
||||
0
|
||||
0,
|
||||
-135
|
||||
],
|
||||
[
|
||||
-180,
|
||||
0
|
||||
0,
|
||||
-180
|
||||
]
|
||||
]
|
||||
}
|
||||
|
@ -1574,20 +1574,20 @@ define(function (require) {
|
|||
},
|
||||
'rectangle': [
|
||||
[
|
||||
0,
|
||||
-90
|
||||
-90,
|
||||
0
|
||||
],
|
||||
[
|
||||
45,
|
||||
-90
|
||||
-90,
|
||||
45
|
||||
],
|
||||
[
|
||||
45,
|
||||
-45
|
||||
-45,
|
||||
45
|
||||
],
|
||||
[
|
||||
0,
|
||||
-45
|
||||
-45,
|
||||
0
|
||||
]
|
||||
]
|
||||
}
|
||||
|
@ -1635,20 +1635,20 @@ define(function (require) {
|
|||
},
|
||||
'rectangle': [
|
||||
[
|
||||
90,
|
||||
-90
|
||||
-90,
|
||||
90
|
||||
],
|
||||
[
|
||||
135,
|
||||
-90
|
||||
-90,
|
||||
135
|
||||
],
|
||||
[
|
||||
135,
|
||||
-45
|
||||
-45,
|
||||
135
|
||||
],
|
||||
[
|
||||
90,
|
||||
-45
|
||||
-45,
|
||||
90
|
||||
]
|
||||
]
|
||||
}
|
||||
|
@ -1696,20 +1696,20 @@ define(function (require) {
|
|||
},
|
||||
'rectangle': [
|
||||
[
|
||||
45,
|
||||
-90
|
||||
-90,
|
||||
45
|
||||
],
|
||||
[
|
||||
90,
|
||||
-90
|
||||
-90,
|
||||
90
|
||||
],
|
||||
[
|
||||
90,
|
||||
-45
|
||||
-45,
|
||||
90
|
||||
],
|
||||
[
|
||||
45,
|
||||
-45
|
||||
-45,
|
||||
45
|
||||
]
|
||||
]
|
||||
}
|
||||
|
@ -1757,20 +1757,20 @@ define(function (require) {
|
|||
},
|
||||
'rectangle': [
|
||||
[
|
||||
-135,
|
||||
-45
|
||||
-45,
|
||||
-135
|
||||
],
|
||||
[
|
||||
-90,
|
||||
-45
|
||||
-45,
|
||||
-90
|
||||
],
|
||||
[
|
||||
-90,
|
||||
0
|
||||
0,
|
||||
-90
|
||||
],
|
||||
[
|
||||
-135,
|
||||
0
|
||||
0,
|
||||
-135
|
||||
]
|
||||
]
|
||||
}
|
||||
|
@ -1818,20 +1818,20 @@ define(function (require) {
|
|||
},
|
||||
'rectangle': [
|
||||
[
|
||||
-135,
|
||||
-90
|
||||
-90,
|
||||
-135
|
||||
],
|
||||
[
|
||||
-90,
|
||||
-90
|
||||
],
|
||||
[
|
||||
-90,
|
||||
-45
|
||||
-45,
|
||||
-90
|
||||
],
|
||||
[
|
||||
-135,
|
||||
-45
|
||||
-45,
|
||||
-135
|
||||
]
|
||||
]
|
||||
}
|
||||
|
|
|
@ -258,5 +258,63 @@ define(function (require) {
|
|||
});
|
||||
});
|
||||
|
||||
describe('geohashGrid methods', function () {
|
||||
var data;
|
||||
var geohashGridData = {
|
||||
hits: 3954,
|
||||
rows: [{
|
||||
title: 'Top 5 _type: apache',
|
||||
label: 'Top 5 _type: apache',
|
||||
geoJson: {
|
||||
type: 'FeatureCollection',
|
||||
features: [],
|
||||
properties: {
|
||||
min: 2,
|
||||
max: 331,
|
||||
zoom: 3,
|
||||
center: [
|
||||
47.517200697839414,
|
||||
-112.06054687499999
|
||||
]
|
||||
}
|
||||
},
|
||||
}, {
|
||||
title: 'Top 5 _type: nginx',
|
||||
label: 'Top 5 _type: nginx',
|
||||
geoJson: {
|
||||
type: 'FeatureCollection',
|
||||
features: [],
|
||||
properties: {
|
||||
min: 1,
|
||||
max: 88,
|
||||
zoom: 3,
|
||||
center: [
|
||||
47.517200697839414,
|
||||
-112.06054687499999
|
||||
]
|
||||
}
|
||||
},
|
||||
}]
|
||||
};
|
||||
|
||||
beforeEach(function () {
|
||||
data = new Data(geohashGridData, {});
|
||||
});
|
||||
|
||||
describe('getVisData', function () {
|
||||
it('should return the rows property', function () {
|
||||
var visData = data.getVisData();
|
||||
expect(visData).to.eql(geohashGridData.rows);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getGeoExtents', function () {
|
||||
it('should return the min and max geoJson properties', function () {
|
||||
var minMax = data.getGeoExtents();
|
||||
expect(minMax.min).to.be(1);
|
||||
expect(minMax.max).to.be(331);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,398 +0,0 @@
|
|||
define(function (require) {
|
||||
var angular = require('angular');
|
||||
var _ = require('lodash');
|
||||
var $ = require('jquery');
|
||||
var L = require('leaflet');
|
||||
|
||||
// Data
|
||||
var dataArray = [
|
||||
require('vislib_fixtures/mock_data/geohash/_geo_json'),
|
||||
require('vislib_fixtures/mock_data/geohash/_columns'),
|
||||
require('vislib_fixtures/mock_data/geohash/_rows')
|
||||
];
|
||||
var names = ['geojson', 'columns', 'rows'];
|
||||
// TODO: Test the specific behavior of each these
|
||||
var mapTypes = ['Scaled Circle Markers', 'Shaded Circle Markers', 'Shaded Geohash Grid', 'Heatmap'];
|
||||
|
||||
angular.module('TileMapFactory', ['kibana']);
|
||||
|
||||
|
||||
function bootstrapAndRender(data, type) {
|
||||
var vis;
|
||||
var visLibParams = {
|
||||
isDesaturated: true,
|
||||
type: 'tile_map',
|
||||
mapType: type
|
||||
};
|
||||
|
||||
module('TileMapFactory');
|
||||
inject(function (Private) {
|
||||
vis = Private(require('vislib_fixtures/_vis_fixture'))(visLibParams);
|
||||
require('css!components/vislib/styles/main');
|
||||
vis.render(data);
|
||||
});
|
||||
|
||||
return vis;
|
||||
|
||||
}
|
||||
|
||||
function destroyVis(vis) {
|
||||
$(vis.el).remove();
|
||||
vis = null;
|
||||
}
|
||||
|
||||
describe('TileMap Tests', function () {
|
||||
describe('Rendering each types of tile map', function () {
|
||||
dataArray.forEach(function (data, i) {
|
||||
|
||||
mapTypes.forEach(function (type, j) {
|
||||
|
||||
describe('draw() ' + mapTypes[j] + ' with ' + names[i], function () {
|
||||
var vis;
|
||||
|
||||
beforeEach(function () {
|
||||
vis = bootstrapAndRender(data, type);
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
destroyVis(vis);
|
||||
});
|
||||
|
||||
it('should return a function', function () {
|
||||
vis.handler.charts.forEach(function (chart) {
|
||||
expect(chart.draw()).to.be.a(Function);
|
||||
});
|
||||
});
|
||||
|
||||
it('should create .leaflet-container as a by product of map rendering', function () {
|
||||
expect($(vis.el).find('.leaflet-container').length).to.be.above(0);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Leaflet controls', function () {
|
||||
var vis;
|
||||
var leafletContainer;
|
||||
|
||||
beforeEach(function () {
|
||||
vis = bootstrapAndRender(dataArray[0], 'Scaled Circle Markers');
|
||||
leafletContainer = $(vis.el).find('.leaflet-container');
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
destroyVis(vis);
|
||||
});
|
||||
|
||||
it('should attach the zoom controls', function () {
|
||||
expect(leafletContainer.find('.leaflet-control-zoom-in').length).to.be(1);
|
||||
expect(leafletContainer.find('.leaflet-control-zoom-out').length).to.be(1);
|
||||
});
|
||||
|
||||
it('should attach the filter drawing button', function () {
|
||||
expect(leafletContainer.find('.leaflet-draw').length).to.be(1);
|
||||
});
|
||||
|
||||
it('should attach the crop button', function () {
|
||||
expect(leafletContainer.find('.leaflet-control-fit').length).to.be(1);
|
||||
});
|
||||
|
||||
it('should not attach the filter or crop buttons if no data is present', function () {
|
||||
var noData = {
|
||||
geoJson: {
|
||||
features: [],
|
||||
properties: {
|
||||
label: null,
|
||||
length: 30,
|
||||
min: 1,
|
||||
max: 608,
|
||||
precision: 1,
|
||||
allmin: 1,
|
||||
allmax: 608
|
||||
},
|
||||
hits: 20
|
||||
}
|
||||
};
|
||||
vis.render(noData);
|
||||
leafletContainer = $(vis.el).find('.leaflet-container');
|
||||
|
||||
expect(leafletContainer.find('.leaflet-control-fit').length).to.be(0);
|
||||
expect(leafletContainer.find('.leaflet-draw').length).to.be(0);
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
// Probably only neccesary to test one of these as we already know the the map will render
|
||||
|
||||
describe('Methods', function () {
|
||||
var vis;
|
||||
var leafletContainer;
|
||||
var map;
|
||||
var mapData;
|
||||
var i;
|
||||
var feature;
|
||||
var point;
|
||||
var min;
|
||||
var max;
|
||||
var zoom;
|
||||
|
||||
beforeEach(function () {
|
||||
vis = bootstrapAndRender(dataArray[0], 'Scaled Circle Markers');
|
||||
leafletContainer = $(vis.el).find('.leaflet-container');
|
||||
map = vis.handler.charts[0].maps[0];
|
||||
mapData = vis.data.geoJson;
|
||||
i = _.random(0, mapData.features.length - 1);
|
||||
feature = mapData.features[i];
|
||||
point = feature.properties.latLng;
|
||||
min = mapData.properties.allmin;
|
||||
max = mapData.properties.allmax;
|
||||
zoom = _.random(1, 12);
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
destroyVis(vis);
|
||||
});
|
||||
|
||||
describe('_filterToMapBounds method', function () {
|
||||
it('should filter out data points that are outside of the map bounds', function () {
|
||||
vis.handler.charts.forEach(function (chart) {
|
||||
chart.maps.forEach(function (map) {
|
||||
var featuresLength = chart.geoJson.features.length;
|
||||
var mapFeatureLength;
|
||||
|
||||
function getSize(obj) {
|
||||
var size = 0;
|
||||
var key;
|
||||
|
||||
for (key in obj) { if (obj.hasOwnProperty(key)) size++; }
|
||||
return size;
|
||||
}
|
||||
|
||||
map.setZoom(13); // Zoom in on the map!
|
||||
mapFeatureLength = getSize(map._layers);
|
||||
|
||||
expect(mapFeatureLength).to.be.lessThan(featuresLength);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('geohashMinDistance method', function () {
|
||||
it('should return a number', function () {
|
||||
vis.handler.charts.forEach(function (chart) {
|
||||
expect(_.isFinite(chart.geohashMinDistance(feature))).to.be(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('radiusScale method', function () {
|
||||
var countdata = [0, 10, 20, 30, 40, 50, 60];
|
||||
var max = 60;
|
||||
var zoom = _.random(1, 18);
|
||||
var constantZoomRadius = 0.5 * Math.pow(2, zoom);
|
||||
var precision = _.random(1, 12);
|
||||
var precisionScale = 200 / Math.pow(5, precision);
|
||||
var prev = -1;
|
||||
|
||||
it('test array should return a number equal to radius', function () {
|
||||
countdata.forEach(function (data, i) {
|
||||
vis.handler.charts.forEach(function (chart) {
|
||||
var count = data;
|
||||
var pct = count / max;
|
||||
var exp = 0.5;
|
||||
var radius = Math.pow(pct, exp) * constantZoomRadius * precisionScale;
|
||||
var test = chart.radiusScale(count, max, zoom, precision);
|
||||
|
||||
expect(test).to.be.a('number');
|
||||
expect(test).to.be(radius);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('test array should return a radius greater than previous', function () {
|
||||
countdata.forEach(function (data, i) {
|
||||
vis.handler.charts.forEach(function (chart) {
|
||||
var count = data;
|
||||
var pct = count / max;
|
||||
var exp = 0.5;
|
||||
var radius = Math.pow(pct, exp) * constantZoomRadius * precisionScale;
|
||||
var test = chart.radiusScale(count, max, zoom, precision);
|
||||
|
||||
expect(test).to.be.above(prev);
|
||||
prev = chart.radiusScale(count, max, zoom, precision);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('quantizeColorScale method', function () {
|
||||
it('should return a hex color', function () {
|
||||
vis.handler.charts.forEach(function (chart) {
|
||||
var reds = ['#fed976', '#feb24c', '#fd8d3c', '#f03b20', '#bd0026'];
|
||||
var count = Math.random() * 300;
|
||||
var min = 0;
|
||||
var max = 300;
|
||||
expect(_.indexOf(reds, chart.quantizeColorScale(count, min, max))).to.not.be(-1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getMinMax method', function () {
|
||||
it('should return an object', function () {
|
||||
vis.handler.charts.forEach(function (chart) {
|
||||
var data = chart.handler.data.data;
|
||||
expect(chart.getMinMax(data)).to.be.an(Object);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return the min of all features.properties.value', function () {
|
||||
vis.handler.charts.forEach(function (chart) {
|
||||
var data = chart.handler.data.data;
|
||||
var min = _.chain(data.geoJson.features)
|
||||
.pluck('properties.value')
|
||||
.min()
|
||||
.value();
|
||||
expect(chart.getMinMax(data).min).to.be(min);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return the max of all features.properties.value', function () {
|
||||
vis.handler.charts.forEach(function (chart) {
|
||||
var data = chart.handler.data.data;
|
||||
var max = _.chain(data.geoJson.features)
|
||||
.pluck('properties.value')
|
||||
.max()
|
||||
.value();
|
||||
expect(chart.getMinMax(data).max).to.be(max);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('dataToHeatArray method', function () {
|
||||
it('should return an array', function () {
|
||||
vis.handler.charts.forEach(function (chart) {
|
||||
expect(chart.dataToHeatArray(max)).to.be.an(Array);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return an array item for each feature', function () {
|
||||
vis.handler.charts.forEach(function (chart) {
|
||||
expect(chart.dataToHeatArray(max).length).to.be(mapData.features.length);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return an array item with lat, lng, metric for each feature', function () {
|
||||
vis.handler.charts.forEach(function (chart) {
|
||||
var lat = feature.geometry.coordinates[1];
|
||||
var lng = feature.geometry.coordinates[0];
|
||||
var intensity = feature.properties.value;
|
||||
var array = chart.dataToHeatArray(max);
|
||||
expect(array[i][0]).to.be(lat);
|
||||
expect(array[i][1]).to.be(lng);
|
||||
expect(array[i][2]).to.be(intensity);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return an array item with lat, lng, normalized metric for each feature', function () {
|
||||
vis.handler.charts.forEach(function (chart) {
|
||||
chart._attr.heatNormalizeData = true;
|
||||
var lat = feature.geometry.coordinates[1];
|
||||
var lng = feature.geometry.coordinates[0];
|
||||
var intensity = parseInt(feature.properties.value / max * 100);
|
||||
var array = chart.dataToHeatArray(max);
|
||||
expect(array[i][0]).to.be(lat);
|
||||
expect(array[i][1]).to.be(lng);
|
||||
expect(array[i][2]).to.be(intensity);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('applyShadingStyle method', function () {
|
||||
it('should return an object', function () {
|
||||
vis.handler.charts.forEach(function (chart) {
|
||||
expect(chart.applyShadingStyle(feature, min, max)).to.be.an(Object);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('showTooltip method', function () {
|
||||
it('should create a .leaflet-popup-kibana div for the tooltip', function () {
|
||||
vis.handler.charts.forEach(function (chart) {
|
||||
chart.tooltipFormatter = function (str) {
|
||||
return '<div class="popup-stub"></div>';
|
||||
};
|
||||
|
||||
var featureLayer = _.sample(_.filter(map._layers, 'feature'));
|
||||
|
||||
expect($('.popup-stub', vis.el).length).to.be(0);
|
||||
featureLayer.fire('mouseover');
|
||||
expect($('.popup-stub', vis.el).length).to.be(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('tooltipProximity method', function () {
|
||||
it('should return true if feature is close enough to event latlng to display tooltip', function () {
|
||||
vis.handler.charts.forEach(function (chart) {
|
||||
expect(chart.tooltipProximity(point, zoom, feature, map)).to.be(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return false if feature is not close enough to event latlng to display tooltip', function () {
|
||||
vis.handler.charts.forEach(function (chart) {
|
||||
var point = L.latLng(90, -180);
|
||||
expect(chart.tooltipProximity(point, zoom, feature, map)).to.be(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('nearestFeature method', function () {
|
||||
it('should return an object', function () {
|
||||
vis.handler.charts.forEach(function (chart) {
|
||||
expect(chart.nearestFeature(point)).to.be.an(Object);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return a geoJson feature', function () {
|
||||
vis.handler.charts.forEach(function (chart) {
|
||||
expect(chart.nearestFeature(point).type).to.be('Feature');
|
||||
});
|
||||
});
|
||||
|
||||
it('should return the geoJson feature with same latlng as point', function () {
|
||||
vis.handler.charts.forEach(function (chart) {
|
||||
expect(chart.nearestFeature(point)).to.be(feature);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('addLatLng method', function () {
|
||||
it('should add object to properties of each feature', function () {
|
||||
vis.handler.charts.forEach(function (chart) {
|
||||
expect(feature.properties.latLng).to.be.an(Object);
|
||||
});
|
||||
});
|
||||
|
||||
it('should add latLng with lat to properties of each feature', function () {
|
||||
vis.handler.charts.forEach(function (chart) {
|
||||
var lat = feature.geometry.coordinates[1];
|
||||
expect(feature.properties.latLng.lat).to.be(lat);
|
||||
});
|
||||
});
|
||||
|
||||
it('should add latLng with lng to properties of each feature', function () {
|
||||
vis.handler.charts.forEach(function (chart) {
|
||||
var lng = feature.geometry.coordinates[0];
|
||||
expect(feature.properties.latLng.lng).to.be(lng);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
});
|
198
test/unit/specs/vislib/visualizations/tile_maps/map.js
Normal file
198
test/unit/specs/vislib/visualizations/tile_maps/map.js
Normal file
|
@ -0,0 +1,198 @@
|
|||
define(function (require) {
|
||||
var angular = require('angular');
|
||||
var _ = require('lodash');
|
||||
var $ = require('jquery');
|
||||
var L = require('leaflet');
|
||||
|
||||
var sinon = require('test_utils/auto_release_sinon');
|
||||
var geoJsonData = require('vislib_fixtures/mock_data/geohash/_geo_json');
|
||||
|
||||
// // Data
|
||||
// var dataArray = [
|
||||
// ['geojson', require('vislib_fixtures/mock_data/geohash/_geo_json')],
|
||||
// ['columns', require('vislib_fixtures/mock_data/geohash/_columns')],
|
||||
// ['rows', require('vislib_fixtures/mock_data/geohash/_rows')],
|
||||
// ];
|
||||
|
||||
// // TODO: Test the specific behavior of each these
|
||||
// var mapTypes = [
|
||||
// 'Scaled Circle Markers',
|
||||
// 'Shaded Circle Markers',
|
||||
// 'Shaded Geohash Grid',
|
||||
// 'Heatmap'
|
||||
// ];
|
||||
|
||||
angular.module('MapFactory', ['kibana']);
|
||||
|
||||
describe('TileMap Map Tests', function () {
|
||||
this.timeout(0);
|
||||
var $mockMapEl = $('<div>');
|
||||
var Map;
|
||||
var leafletStubs = {};
|
||||
var leafletMocks = {};
|
||||
|
||||
beforeEach(function () {
|
||||
module('MapFactory');
|
||||
inject(function (Private) {
|
||||
// mock parts of leaflet
|
||||
leafletMocks.tileLayer = { on: sinon.stub() };
|
||||
leafletMocks.map = { on: sinon.stub() };
|
||||
leafletStubs.tileLayer = sinon.stub(L, 'tileLayer', _.constant(leafletMocks.tileLayer));
|
||||
leafletStubs.map = sinon.stub(L, 'map', _.constant(leafletMocks.map));
|
||||
|
||||
Map = Private(require('components/vislib/visualizations/_map'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('instantiation', function () {
|
||||
var map;
|
||||
var createStub;
|
||||
|
||||
beforeEach(function () {
|
||||
createStub = sinon.stub(Map.prototype, '_createMap', _.noop);
|
||||
map = new Map($mockMapEl, geoJsonData, {});
|
||||
});
|
||||
|
||||
it('should create the map', function () {
|
||||
expect(createStub.callCount).to.equal(1);
|
||||
});
|
||||
|
||||
it('should add zoom controls', function () {
|
||||
var mapOptions = createStub.firstCall.args[0];
|
||||
|
||||
expect(mapOptions).to.be.an('object');
|
||||
if (mapOptions.zoomControl) expect(mapOptions.zoomControl).to.be.ok();
|
||||
else expect(mapOptions.zoomControl).to.be(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createMap', function () {
|
||||
var map;
|
||||
var mapStubs;
|
||||
|
||||
beforeEach(function () {
|
||||
mapStubs = {
|
||||
destroy: sinon.stub(Map.prototype, 'destroy'),
|
||||
attachEvents: sinon.stub(Map.prototype, '_attachEvents'),
|
||||
addMarkers: sinon.stub(Map.prototype, '_addMarkers'),
|
||||
};
|
||||
|
||||
map = new Map($mockMapEl, geoJsonData, {});
|
||||
});
|
||||
|
||||
it('should create the create leaflet objects', function () {
|
||||
expect(leafletStubs.tileLayer.callCount).to.equal(1);
|
||||
expect(leafletStubs.map.callCount).to.equal(1);
|
||||
|
||||
var callArgs = leafletStubs.map.firstCall.args;
|
||||
var mapOptions = callArgs[1];
|
||||
expect(callArgs[0]).to.be($mockMapEl.get(0));
|
||||
expect(mapOptions).to.have.property('zoom');
|
||||
expect(mapOptions).to.have.property('center');
|
||||
});
|
||||
|
||||
it('should attach events and add markers', function () {
|
||||
expect(mapStubs.attachEvents.callCount).to.equal(1);
|
||||
expect(mapStubs.addMarkers.callCount).to.equal(1);
|
||||
});
|
||||
|
||||
it('should call destroy only if a map exists', function () {
|
||||
expect(mapStubs.destroy.callCount).to.equal(0);
|
||||
map._createMap({});
|
||||
expect(mapStubs.destroy.callCount).to.equal(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('attachEvents', function () {
|
||||
var map;
|
||||
|
||||
beforeEach(function () {
|
||||
sinon.stub(Map.prototype, '_createMap', function () {
|
||||
this._tileLayer = leafletMocks.tileLayer;
|
||||
this.map = leafletMocks.map;
|
||||
this._attachEvents();
|
||||
});
|
||||
map = new Map($mockMapEl, geoJsonData, {});
|
||||
});
|
||||
|
||||
it('should attach interaction events', function () {
|
||||
var expectedTileEvents = ['tileload'];
|
||||
var expectedMapEvents = ['draw:created', 'moveend', 'zoomend', 'unload'];
|
||||
var matchedEvents = {
|
||||
tiles: 0,
|
||||
maps: 0,
|
||||
};
|
||||
|
||||
_.times(leafletMocks.tileLayer.on.callCount, function (index) {
|
||||
var ev = leafletMocks.tileLayer.on.getCall(index).args[0];
|
||||
if (_.includes(expectedTileEvents, ev)) matchedEvents.tiles++;
|
||||
});
|
||||
expect(matchedEvents.tiles).to.equal(expectedTileEvents.length);
|
||||
|
||||
_.times(leafletMocks.map.on.callCount, function (index) {
|
||||
var ev = leafletMocks.map.on.getCall(index).args[0];
|
||||
if (_.includes(expectedMapEvents, ev)) matchedEvents.maps++;
|
||||
});
|
||||
expect(matchedEvents.maps).to.equal(expectedMapEvents.length);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('addMarkers', function () {
|
||||
var map;
|
||||
var createStub;
|
||||
|
||||
beforeEach(function () {
|
||||
sinon.stub(Map.prototype, '_createMap');
|
||||
createStub = sinon.stub(Map.prototype, '_createMarkers', _.constant({ addLegend: _.noop }));
|
||||
map = new Map($mockMapEl, geoJsonData, {});
|
||||
});
|
||||
|
||||
it('should pass the map options to the marker', function () {
|
||||
map._addMarkers();
|
||||
|
||||
var args = createStub.firstCall.args[0];
|
||||
expect(args).to.have.property('tooltipFormatter');
|
||||
expect(args).to.have.property('valueFormatter');
|
||||
expect(args).to.have.property('attr');
|
||||
});
|
||||
|
||||
it('should destroy existing markers', function () {
|
||||
var destroyStub = sinon.stub();
|
||||
map._markers = { destroy: destroyStub };
|
||||
map._addMarkers();
|
||||
|
||||
expect(destroyStub.callCount).to.be(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDataRectangles', function () {
|
||||
var map;
|
||||
|
||||
beforeEach(function () {
|
||||
sinon.stub(Map.prototype, '_createMap');
|
||||
map = new Map($mockMapEl, geoJsonData, {});
|
||||
});
|
||||
|
||||
it('should return an empty array if no data', function () {
|
||||
map = new Map($mockMapEl, {}, {});
|
||||
var rects = map._getDataRectangles();
|
||||
expect(rects).to.have.length(0);
|
||||
});
|
||||
|
||||
it('should return an array of arrays of rectangles', function () {
|
||||
var rects = map._getDataRectangles();
|
||||
_.times(5, function () {
|
||||
var index = _.random(rects.length - 1);
|
||||
var rect = rects[index];
|
||||
var featureRect = geoJsonData.geoJson.features[index].properties.rectangle;
|
||||
expect(rect.length).to.equal(featureRect.length);
|
||||
|
||||
// should swap the array
|
||||
var checkIndex = _.random(rect.length - 1);
|
||||
expect(rect[checkIndex]).to.eql(featureRect[checkIndex]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
373
test/unit/specs/vislib/visualizations/tile_maps/markers.js
Normal file
373
test/unit/specs/vislib/visualizations/tile_maps/markers.js
Normal file
|
@ -0,0 +1,373 @@
|
|||
define(function (require) {
|
||||
var angular = require('angular');
|
||||
var _ = require('lodash');
|
||||
var $ = require('jquery');
|
||||
var L = require('leaflet');
|
||||
var sinon = require('test_utils/auto_release_sinon');
|
||||
var geoJsonData = require('vislib_fixtures/mock_data/geohash/_geo_json');
|
||||
// defaults to roughly the lower 48 US states
|
||||
var defaultSWCoords = [13.496, -143.789];
|
||||
var defaultNECoords = [55.526, -57.919];
|
||||
var bounds = {};
|
||||
var MarkerType;
|
||||
var map;
|
||||
|
||||
angular.module('MarkerFactory', ['kibana']);
|
||||
|
||||
function setBounds(southWest, northEast) {
|
||||
bounds.southWest = L.latLng(southWest || defaultSWCoords);
|
||||
bounds.northEast = L.latLng(northEast || defaultNECoords);
|
||||
}
|
||||
|
||||
function getBounds() {
|
||||
return L.latLngBounds(bounds.southWest, bounds.northEast);
|
||||
}
|
||||
|
||||
var mockMap = {
|
||||
addLayer: _.noop,
|
||||
closePopup: _.noop,
|
||||
getBounds: getBounds,
|
||||
removeControl: _.noop,
|
||||
removeLayer: _.noop,
|
||||
getZoom: _.constant(5)
|
||||
};
|
||||
|
||||
describe('Marker Tests', function () {
|
||||
var mapData;
|
||||
var markerLayer;
|
||||
|
||||
function createMarker(MarkerClass, geoJson) {
|
||||
mapData = _.assign({}, geoJsonData.geoJson, geoJson || {});
|
||||
mapData.properties.allmin = mapData.properties.min;
|
||||
mapData.properties.allmax = mapData.properties.max;
|
||||
|
||||
return new MarkerClass(mockMap, mapData, {
|
||||
valueFormatter: geoJsonData.valueFormatter
|
||||
});
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
setBounds();
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
if (markerLayer) {
|
||||
markerLayer.destroy();
|
||||
markerLayer = undefined;
|
||||
}
|
||||
});
|
||||
|
||||
describe('Base Methods', function () {
|
||||
var MarkerClass;
|
||||
|
||||
beforeEach(function () {
|
||||
module('MarkerFactory');
|
||||
inject(function (Private) {
|
||||
MarkerClass = Private(require('components/vislib/visualizations/marker_types/base_marker'));
|
||||
markerLayer = createMarker(MarkerClass);
|
||||
});
|
||||
});
|
||||
|
||||
describe('filterToMapBounds', function () {
|
||||
it('should not filter any features', function () {
|
||||
// set bounds to the entire world
|
||||
setBounds([-87.252, -343.828], [87.252, 343.125]);
|
||||
var boundFilter = markerLayer._filterToMapBounds();
|
||||
var mapFeature = mapData.features.filter(boundFilter);
|
||||
|
||||
expect(mapFeature.length).to.equal(mapData.features.length);
|
||||
});
|
||||
|
||||
it('should filter out data points that are outside of the map bounds', function () {
|
||||
// set bounds to roughly US southwest
|
||||
setBounds([31.690, -124.387], [42.324, -102.919]);
|
||||
var boundFilter = markerLayer._filterToMapBounds();
|
||||
var mapFeature = mapData.features.filter(boundFilter);
|
||||
|
||||
expect(mapFeature.length).to.be.lessThan(mapData.features.length);
|
||||
});
|
||||
});
|
||||
|
||||
describe('legendQuantizer', function () {
|
||||
it('should return a range of hex colors', function () {
|
||||
var minColor = markerLayer._legendQuantizer(mapData.properties.allmin);
|
||||
var maxColor = markerLayer._legendQuantizer(mapData.properties.allmax);
|
||||
|
||||
expect(minColor.substring(0, 1)).to.equal('#');
|
||||
expect(minColor).to.have.length(7);
|
||||
expect(maxColor.substring(0, 1)).to.equal('#');
|
||||
expect(maxColor).to.have.length(7);
|
||||
expect(minColor).to.not.eql(maxColor);
|
||||
});
|
||||
|
||||
it('should return a color with 1 color', function () {
|
||||
var geoJson = { properties: { min: 1, max: 1 } };
|
||||
markerLayer = createMarker(MarkerClass, geoJson);
|
||||
|
||||
// ensure the quantizer domain is correct
|
||||
var color = markerLayer._legendQuantizer(1);
|
||||
expect(color).to.not.be(undefined);
|
||||
expect(color.substring(0, 1)).to.equal('#');
|
||||
|
||||
// should always get the same color back
|
||||
_.times(5, function () {
|
||||
var num = _.random(0, 100);
|
||||
var randColor = markerLayer._legendQuantizer(0);
|
||||
expect(randColor).to.equal(color);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('applyShadingStyle', function () {
|
||||
it('should return a style object', function () {
|
||||
var style = markerLayer.applyShadingStyle(100);
|
||||
expect(style).to.be.an('object');
|
||||
|
||||
var keys = _.keys(style);
|
||||
var expected = ['fillColor', 'color'];
|
||||
_.each(expected, function (key) {
|
||||
expect(keys).to.contain(key);
|
||||
});
|
||||
});
|
||||
|
||||
it('should use the legendQuantizer', function () {
|
||||
var spy = sinon.spy(markerLayer, '_legendQuantizer');
|
||||
var style = markerLayer.applyShadingStyle(100);
|
||||
expect(spy.callCount).to.equal(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('showTooltip', function () {
|
||||
it('should use the tooltip formatter', function () {
|
||||
var content;
|
||||
var sample = _.sample(mapData.features);
|
||||
|
||||
var stub = sinon.stub(markerLayer, '_tooltipFormatter', function (val) {
|
||||
return;
|
||||
});
|
||||
|
||||
markerLayer._showTooltip(sample);
|
||||
|
||||
expect(stub.callCount).to.equal(1);
|
||||
expect(stub.firstCall.calledWith(sample)).to.be(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('addLegend', function () {
|
||||
var addToSpy;
|
||||
var leafletControlStub;
|
||||
|
||||
beforeEach(function () {
|
||||
addToSpy = sinon.spy();
|
||||
leafletControlStub = sinon.stub(L, 'control', function (options) {
|
||||
return {
|
||||
addTo: addToSpy
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
it('should do nothing if there is already a legend', function () {
|
||||
markerLayer._legend = { legend: 'exists' }; // anything truthy
|
||||
|
||||
markerLayer.addLegend();
|
||||
expect(leafletControlStub.callCount).to.equal(0);
|
||||
});
|
||||
|
||||
it('should create a leaflet control', function () {
|
||||
markerLayer.addLegend();
|
||||
expect(leafletControlStub.callCount).to.equal(1);
|
||||
expect(addToSpy.callCount).to.equal(1);
|
||||
expect(addToSpy.firstCall.calledWith(markerLayer.map)).to.be(true);
|
||||
expect(markerLayer._legend).to.have.property('onAdd');
|
||||
});
|
||||
|
||||
it('should use the value formatter', function () {
|
||||
var formatterSpy = sinon.spy(markerLayer, '_valueFormatter');
|
||||
// called twice for every legend color defined
|
||||
var expectedCallCount = markerLayer._legendColors.length * 2;
|
||||
|
||||
markerLayer.addLegend();
|
||||
var legend = markerLayer._legend.onAdd();
|
||||
|
||||
expect(formatterSpy.callCount).to.equal(expectedCallCount);
|
||||
expect(legend).to.be.a(HTMLDivElement);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Shaded Circles', function () {
|
||||
beforeEach(function () {
|
||||
module('MarkerFactory');
|
||||
inject(function (Private) {
|
||||
var MarkerClass = Private(require('components/vislib/visualizations/marker_types/shaded_circles'));
|
||||
markerLayer = createMarker(MarkerClass);
|
||||
});
|
||||
});
|
||||
|
||||
describe('geohashMinDistance method', function () {
|
||||
it('should return a finite number', function () {
|
||||
var sample = _.sample(mapData.features);
|
||||
var distance = markerLayer._geohashMinDistance(sample);
|
||||
|
||||
expect(distance).to.be.a('number');
|
||||
expect(_.isFinite(distance)).to.be(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Scaled Circles', function () {
|
||||
var zoom;
|
||||
|
||||
beforeEach(function () {
|
||||
module('MarkerFactory');
|
||||
|
||||
zoom = _.random(1, 18);
|
||||
sinon.stub(mockMap, 'getZoom', _.constant(zoom));
|
||||
|
||||
inject(function (Private) {
|
||||
var MarkerClass = Private(require('components/vislib/visualizations/marker_types/scaled_circles'));
|
||||
markerLayer = createMarker(MarkerClass);
|
||||
});
|
||||
});
|
||||
|
||||
describe('radiusScale method', function () {
|
||||
var valueArray = [10, 20, 30, 40, 50, 60];
|
||||
var max = _.max(valueArray);
|
||||
var prev = -1;
|
||||
|
||||
it('should return 0 for value of 0', function () {
|
||||
expect(markerLayer._radiusScale(0)).to.equal(0);
|
||||
});
|
||||
|
||||
it('should return a scaled value for negative and positive numbers', function () {
|
||||
var upperBound = markerLayer._radiusScale(max);
|
||||
var results = [];
|
||||
|
||||
function roundValue(value) {
|
||||
// round number to 6 decimal places
|
||||
var r = Math.pow(10, 6);
|
||||
return Math.round(value * r) / r;
|
||||
}
|
||||
|
||||
_.each(valueArray, function (value, i) {
|
||||
var ratio = Math.pow(value / max, 0.5);
|
||||
var comparison = ratio * upperBound;
|
||||
var radius = markerLayer._radiusScale(value);
|
||||
var negRadius = markerLayer._radiusScale(value * -1);
|
||||
results.push(radius);
|
||||
|
||||
expect(negRadius).to.equal(radius);
|
||||
expect(roundValue(radius)).to.equal(roundValue(comparison));
|
||||
|
||||
// check that the radius is getting larger
|
||||
if (i > 0) {
|
||||
expect(radius).to.be.above(results[i - 1]);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Heatmaps', function () {
|
||||
beforeEach(function () {
|
||||
module('MarkerFactory');
|
||||
|
||||
inject(function (Private) {
|
||||
var MarkerClass = Private(require('components/vislib/visualizations/marker_types/heatmap'));
|
||||
markerLayer = createMarker(MarkerClass);
|
||||
});
|
||||
});
|
||||
|
||||
describe('dataToHeatArray', function () {
|
||||
var max;
|
||||
|
||||
beforeEach(function () {
|
||||
max = mapData.properties.allmax;
|
||||
});
|
||||
|
||||
it('should return an array or values for each feature', function () {
|
||||
var arr = markerLayer._dataToHeatArray(max);
|
||||
expect(arr).to.be.an('array');
|
||||
expect(arr).to.have.length(mapData.features.length);
|
||||
|
||||
});
|
||||
|
||||
it('should return an array item with lat, lng, metric for each feature', function () {
|
||||
_.times(3, function () {
|
||||
var arr = markerLayer._dataToHeatArray(max);
|
||||
var index = _.random(mapData.features.length - 1);
|
||||
var feature = mapData.features[index];
|
||||
var featureValue = feature.properties.value;
|
||||
var featureArr = feature.geometry.coordinates.slice(0).concat(featureValue);
|
||||
expect(arr[index]).to.eql(featureArr);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return an array item with lat, lng, normalized metric for each feature', function () {
|
||||
_.times(5, function () {
|
||||
markerLayer._attr.heatNormalizeData = true;
|
||||
|
||||
var arr = markerLayer._dataToHeatArray(max);
|
||||
var index = _.random(mapData.features.length - 1);
|
||||
var feature = mapData.features[index];
|
||||
var featureValue = parseInt(feature.properties.value / max * 100);
|
||||
var featureArr = feature.geometry.coordinates.slice(0).concat(featureValue);
|
||||
expect(arr[index]).to.eql(featureArr);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('tooltipProximity', function () {
|
||||
it('should return true if feature is close enough to event latlng', function () {
|
||||
_.times(5, function () {
|
||||
var feature = _.sample(mapData.features);
|
||||
var point = markerLayer._getLatLng(feature);
|
||||
var arr = markerLayer._tooltipProximity(point, feature);
|
||||
expect(arr).to.be(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return false if feature is not close enough to event latlng', function () {
|
||||
_.times(5, function () {
|
||||
var feature = _.sample(mapData.features);
|
||||
var point = L.latLng(90, -180);
|
||||
var arr = markerLayer._tooltipProximity(point, feature);
|
||||
expect(arr).to.be(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('nearestFeature', function () {
|
||||
it('should return nearest geoJson feature object', function () {
|
||||
_.times(5, function () {
|
||||
var feature = _.sample(mapData.features);
|
||||
var point = markerLayer._getLatLng(feature);
|
||||
var nearestPoint = markerLayer._nearestFeature(point);
|
||||
expect(nearestPoint).to.equal(feature);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getLatLng', function () {
|
||||
it('should return a leaflet latLng object', function () {
|
||||
var feature = _.sample(mapData.features);
|
||||
var latLng = markerLayer._getLatLng(feature);
|
||||
var compare = L.latLng(feature.geometry.coordinates.slice(0).reverse());
|
||||
expect(latLng).to.eql(compare);
|
||||
});
|
||||
|
||||
it('should memoize the result', function () {
|
||||
var spy = sinon.spy(L, 'latLng');
|
||||
var feature = _.sample(mapData.features);
|
||||
|
||||
markerLayer._getLatLng(feature);
|
||||
expect(spy.callCount).to.be(1);
|
||||
|
||||
markerLayer._getLatLng(feature);
|
||||
expect(spy.callCount).to.be(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
});
|
137
test/unit/specs/vislib/visualizations/tile_maps/tile_map.js
Normal file
137
test/unit/specs/vislib/visualizations/tile_maps/tile_map.js
Normal file
|
@ -0,0 +1,137 @@
|
|||
define(function (require) {
|
||||
var angular = require('angular');
|
||||
var _ = require('lodash');
|
||||
var $ = require('jquery');
|
||||
var sinon = require('test_utils/auto_release_sinon');
|
||||
|
||||
var geoJsonData = require('vislib_fixtures/mock_data/geohash/_geo_json');
|
||||
var MockMap = require('fixtures/tilemap_map');
|
||||
var mockChartEl = $('<div>');
|
||||
|
||||
var TileMap;
|
||||
var extentsStub;
|
||||
|
||||
|
||||
angular.module('TileMapFactory', ['kibana']);
|
||||
|
||||
function createTileMap(handler, chartEl, chartData) {
|
||||
handler = handler || {};
|
||||
chartEl = chartEl || mockChartEl;
|
||||
chartData = chartData || geoJsonData;
|
||||
|
||||
var tilemap = new TileMap(handler, chartEl, chartData);
|
||||
return tilemap;
|
||||
}
|
||||
|
||||
describe('TileMap Tests', function () {
|
||||
var tilemap;
|
||||
|
||||
beforeEach(function () {
|
||||
module('TileMapFactory');
|
||||
inject(function (Private) {
|
||||
Private.stub(require('components/vislib/visualizations/_map'), MockMap);
|
||||
TileMap = Private(require('components/vislib/visualizations/tile_map'));
|
||||
extentsStub = sinon.stub(TileMap.prototype, '_appendGeoExtents', _.noop);
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(function () {
|
||||
tilemap = createTileMap();
|
||||
});
|
||||
|
||||
it('should inherit props from chartData', function () {
|
||||
_.each(geoJsonData, function (val, prop) {
|
||||
expect(tilemap).to.have.property(prop, val);
|
||||
});
|
||||
});
|
||||
|
||||
it('should append geoExtents', function () {
|
||||
expect(extentsStub.callCount).to.equal(1);
|
||||
});
|
||||
|
||||
describe('draw', function () {
|
||||
it('should return a function', function () {
|
||||
expect(tilemap.draw()).to.be.a('function');
|
||||
});
|
||||
|
||||
it('should call destroy for clean state', function () {
|
||||
var destroySpy = sinon.spy(tilemap, 'destroy');
|
||||
tilemap.draw();
|
||||
expect(destroySpy.callCount).to.equal(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('appendMap', function () {
|
||||
var $selection;
|
||||
|
||||
beforeEach(function () {
|
||||
$selection = $('<div>');
|
||||
expect(tilemap.maps).to.have.length(0);
|
||||
tilemap._appendMap($selection);
|
||||
});
|
||||
|
||||
it('should add the tilemap class', function () {
|
||||
expect($selection.hasClass('tilemap')).to.equal(true);
|
||||
});
|
||||
|
||||
it('should append maps and required controls', function () {
|
||||
expect(tilemap.maps).to.have.length(1);
|
||||
var map = tilemap.maps[0];
|
||||
expect(map.addTitle.callCount).to.equal(0);
|
||||
expect(map.addFitControl.callCount).to.equal(1);
|
||||
expect(map.addBoundingControl.callCount).to.equal(1);
|
||||
});
|
||||
|
||||
it('should only add controls if data exists', function () {
|
||||
var noData = {
|
||||
geoJson: {
|
||||
features: [],
|
||||
properties: {},
|
||||
hits: 20
|
||||
}
|
||||
};
|
||||
var tilemap = createTileMap(null, null, noData);
|
||||
|
||||
tilemap._appendMap($selection);
|
||||
expect(tilemap.maps).to.have.length(1);
|
||||
|
||||
var map = tilemap.maps[0];
|
||||
expect(map.addTitle.callCount).to.equal(0);
|
||||
expect(map.addFitControl.callCount).to.equal(0);
|
||||
expect(map.addBoundingControl.callCount).to.equal(0);
|
||||
});
|
||||
|
||||
it('should append title if set in the data object', function () {
|
||||
var mapTitle = 'Test Title';
|
||||
var tilemap = createTileMap(null, null, _.assign({ title: mapTitle }, geoJsonData));
|
||||
tilemap._appendMap($selection);
|
||||
var map = tilemap.maps[0];
|
||||
|
||||
expect(map.addTitle.callCount).to.equal(1);
|
||||
expect(map.addTitle.firstCall.calledWith(mapTitle)).to.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('destroy', function () {
|
||||
var maps = [];
|
||||
var mapCount = 5;
|
||||
|
||||
beforeEach(function () {
|
||||
_.times(mapCount, function () {
|
||||
maps.push(new MockMap());
|
||||
});
|
||||
tilemap.maps = maps;
|
||||
expect(tilemap.maps).to.have.length(mapCount);
|
||||
tilemap.destroy();
|
||||
});
|
||||
|
||||
it('should destroy all the maps', function () {
|
||||
expect(tilemap.maps).to.have.length(0);
|
||||
expect(maps).to.have.length(mapCount);
|
||||
_.each(maps, function (map) {
|
||||
expect(map.destroy.callCount).to.equal(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Add table
Add a link
Reference in a new issue