Geocentroid / tilemap bug fixes (#10871)

- adds the geo-centroid metric as the new default for visualizations
- various bug fixes and improvements
    - avoid unnecessary calls to manifest
    - avoid map flicker when zooming
    - enable scroll/pinch/touch zooming
    - avoid heatmap errors
    - ensure map fills screen in dashboard
    - ensure fit works consistently
    - relax tilemap constraints
    - remove support for multi-maps
- this refactor sets the stage for new vector map visualization which will reuse the same map components
This commit is contained in:
Thomas Neirynck 2017-03-29 11:23:35 -04:00 committed by GitHub
parent d41042a507
commit 556bfab85d
51 changed files with 3510 additions and 3158 deletions

View file

@ -56,7 +56,7 @@
<div>
<label>
Maximum zoom
&nbsp;<kbn-info placement="right" info="Map zoom at which all dots are displayed at full intensity. Default: 16"></kbn-info>
&nbsp;<kbn-info placement="right" info="Map zoom at which all dots are displayed at full intensity. Default: 0"></kbn-info>
</label>
<div class="vis-editor-agg-form-row">
<input

View file

@ -1,9 +1,7 @@
import _ from 'lodash';
import supports from 'ui/utils/supports';
import MapsVisTypeVislibVisTypeProvider from 'ui/vis_maps/maps_vis_type';
import VisSchemasProvider from 'ui/vis/schemas';
import AggResponseGeoJsonGeoJsonProvider from 'ui/agg_response/geo_json/geo_json';
import FilterBarPushFilterProvider from 'ui/filter_bar/push_filter';
import tileMapTemplate from 'plugins/kbn_vislib_vis_types/editors/tile_map.html';
export default function TileMapVisType(Private, getAppState, courier, config) {
@ -22,7 +20,7 @@ export default function TileMapVisType(Private, getAppState, courier, config) {
mapType: 'Scaled Circle Markers',
isDesaturated: true,
addTooltip: true,
heatMaxZoom: 16,
heatMaxZoom: 0,
heatMinOpacity: 0.1,
heatRadius: 25,
heatBlur: 15,
@ -45,41 +43,14 @@ export default function TileMapVisType(Private, getAppState, courier, config) {
value: 'topright',
text: 'top right',
}],
mapTypes: ['Scaled Circle Markers', 'Shaded Circle Markers', 'Shaded Geohash Grid', 'Heatmap'],
mapTypes: ['Scaled Circle Markers',
'Shaded Circle Markers',
'Shaded Geohash Grid',
'Heatmap'
],
canDesaturate: !!supports.cssFilters,
editor: tileMapTemplate
},
listeners: {
rectangle: function (event) {
const agg = _.get(event, 'chart.geohashGridAgg');
if (!agg) return;
const pushFilter = Private(FilterBarPushFilterProvider)(getAppState());
const indexPatternName = agg.vis.indexPattern.id;
const field = agg.fieldName();
const filter = { geo_bounding_box: {} };
filter.geo_bounding_box[field] = event.bounds;
pushFilter(filter, false, indexPatternName);
},
mapMoveEnd: function (event) {
const vis = _.get(event, 'chart.geohashGridAgg.vis');
if (vis && vis.hasUiState()) {
vis.getUiState().set('mapCenter', event.center);
}
},
mapZoomEnd: function (event) {
const vis = _.get(event, 'chart.geohashGridAgg.vis');
if (vis && vis.hasUiState()) {
vis.getUiState().set('mapZoom', event.zoom);
}
const autoPrecision = _.get(event, 'chart.geohashGridAgg.params.autoPrecision');
if (autoPrecision) {
courier.fetch();
}
}
},
responseConverter: geoJsonConverter,
implementsRenderComplete: true,
schemas: new Schemas([
@ -101,15 +72,6 @@ export default function TileMapVisType(Private, getAppState, courier, config) {
aggFilter: 'geohash_grid',
min: 1,
max: 1
},
{
group: 'buckets',
name: 'split',
title: 'Split Chart',
deprecate: true,
deprecateMessage: 'The Split Chart feature for Tile Maps has been deprecated.',
min: 0,
max: 1
}
])
});

View file

@ -62,7 +62,7 @@ module.exports = function (kibana) {
config: tilemapConfig,
},
manifestServiceUrl: serverConfig.get('tilemap.manifestServiceUrl')
},
}
};
},
},

View file

@ -46,8 +46,15 @@
vis-editor-agg-group {
.flex-parent(0, 1, auto);
}
}
.indented {
margin-left: 2em;
}
.vis-editor-content {
.flex-parent();
z-index: 0;

View file

@ -143,7 +143,6 @@ module.exports = () => Joi.object({
}),
profile: Joi.boolean().default(false)
}).default(),
status: Joi.object({
allowAnonymous: Joi.boolean().default(false)
}).default(),
@ -156,7 +155,7 @@ module.exports = () => Joi.object({
url: Joi.string(),
options: Joi.object({
attribution: Joi.string(),
minZoom: Joi.number().min(1, 'Must not be less than 1').default(1),
minZoom: Joi.number().min(0, 'Must be 0 or higher').default(0),
maxZoom: Joi.number().default(10),
tileSize: Joi.number(),
subdomains: Joi.array().items(Joi.string()).single(),

View file

@ -13,7 +13,7 @@ describe('GeoJson Agg Response Converter', function () {
let tabify;
let convert;
let esResponse;
let aggs;
let expectedAggs;
beforeEach(ngMock.module('kibana'));
beforeEach(ngMock.inject(function (Private) {
@ -28,8 +28,7 @@ describe('GeoJson Agg Response Converter', function () {
type: 'tile_map',
aggs: [
{ schema: 'metric', type: 'avg', params: { field: 'bytes' } },
{ schema: 'split', type: 'terms', params: { field: '@tags' } },
{ schema: 'segment', type: 'geohash_grid', params: { field: 'geo.coordinates', precision: 3 } }
{ schema: 'segment', type: 'geohash_grid', params: { field: 'geo.coordinates', precision: 3, useGeocentroid: false } }
],
params: {
isDesaturated: true,
@ -37,17 +36,16 @@ describe('GeoJson Agg Response Converter', function () {
}
});
aggs = {
expectedAggs = {
metric: vis.aggs[0],
split: vis.aggs[1],
geo: vis.aggs[2]
geo: vis.aggs[1]
};
}));
[ { asAggConfigResults: true }, { asAggConfigResults: false } ].forEach(function (tableOpts) {
function makeTable() {
return _.sample(_.sample(tabify(vis, esResponse, tableOpts).tables).tables);
return _.sample(tabify(vis, esResponse, tableOpts).tables);
}
function makeSingleChart(table) {
@ -72,8 +70,8 @@ describe('GeoJson Agg Response Converter', function () {
expect(chart.title).to.be(table.title());
expect(chart.tooltipFormatter).to.be.a('function');
expect(chart.valueFormatter).to.be(aggs.metric.fieldFormatter());
expect(chart.geohashGridAgg).to.be(aggs.geo);
expect(chart.valueFormatter).to.be(expectedAggs.metric.fieldFormatter());
expect(chart.geohashGridAgg).to.be(expectedAggs.geo);
expect(chart.geoJson).to.be.an('object');
});
@ -116,8 +114,8 @@ describe('GeoJson Agg Response Converter', function () {
before(function () {
table = makeTable();
chart = makeSingleChart(table);
geoColI = _.findIndex(table.columns, { aggConfig: aggs.geo });
metricColI = _.findIndex(table.columns, { aggConfig: aggs.metric });
geoColI = _.findIndex(table.columns, { aggConfig: expectedAggs.geo });
metricColI = _.findIndex(table.columns, { aggConfig: expectedAggs.metric });
});
it('should be geoJson format', function () {

View file

@ -15,10 +15,12 @@ export default function TileMapConverterFn(Private) {
const geoI = columnIndex('segment');
const metricI = columnIndex('metric');
const centroidI = _.findIndex(table.columns, (col) => col.aggConfig.type.name === 'geo_centroid');
const geoAgg = _.get(table.columns, [geoI, 'aggConfig']);
const metricAgg = _.get(table.columns, [metricI, 'aggConfig']);
const features = rowsToFeatures(table, geoI, metricI);
const features = rowsToFeatures(table, geoI, metricI, centroidI);
const values = features.map(function (feature) {
return feature.properties.value;
});

View file

@ -10,7 +10,8 @@ function unwrap(val) {
return getAcr(val) ? val.value : val;
}
function convertRowsToFeatures(table, geoI, metricI) {
function convertRowsToFeatures(table, geoI, metricI, centroidI) {
return _.transform(table.rows, function (features, row) {
const geohash = unwrap(row[geoI]);
if (!geohash) return;
@ -23,6 +24,16 @@ function convertRowsToFeatures(table, geoI, metricI) {
location.longitude[2]
];
//courtsey of @JacobBrandt: https://github.com/elastic/kibana/pull/9676/files#diff-c7c9f237e673ff486654f6cc6caa89f6
let point = centerLatLng;
const centroid = unwrap(row[centroidI]);
if (centroid) {
point = [
centroid.lat,
centroid.lon
];
}
// order is nw, ne, se, sw
const rectangle = [
[location.latitude[0], location.longitude[0]],
@ -37,7 +48,7 @@ function convertRowsToFeatures(table, geoI, metricI) {
type: 'Feature',
geometry: {
type: 'Point',
coordinates: centerLatLng.slice(0).reverse()
coordinates: point.slice(0).reverse()
},
properties: {
geohash: geohash,

View file

@ -8,7 +8,7 @@ describe('Geohash Agg', function () {
const paramWriter = new AggTypesBucketsGeoHashProvider(function PrivateMock() {
return function BucketMock(geohashProvider) {
return geohashProvider.params[4];
return geohashProvider.params[5];
};
}, {
get: function () {

View file

@ -8,8 +8,6 @@ import AggTypesParamTypesStringProvider from 'ui/agg_types/param_types/string';
import AggTypesParamTypesRawJsonProvider from 'ui/agg_types/param_types/raw_json';
import AggTypesParamTypesBaseProvider from 'ui/agg_types/param_types/base';
export default function AggParamsFactory(Private) {
const paramTypeMap = {
field: Private(AggTypesParamTypesFieldProvider),
optioned: Private(AggTypesParamTypesOptionedProvider),

View file

@ -110,6 +110,17 @@ export default function AggTypeFactory(Private) {
this.params = new AggParams(this.params);
}
/**
* Designed for multi-value metric aggs, this method can return a
* set of AggConfigs that should replace this aggConfig in requests
*
* @method getRequestAggs
* @returns {array[AggConfig]|undefined} - an array of aggConfig objects
* that should replace this one,
* or undefined
*/
this.getRequestAggs = config.getRequestAggs || _.noop;
/**
* Designed for multi-value metric aggs, this method can return a
* set of AggConfigs that should replace this aggConfig in result sets

View file

@ -1,10 +1,13 @@
import _ from 'lodash';
import AggTypesBucketsBucketAggTypeProvider from 'ui/agg_types/buckets/_bucket_agg_type';
import VisAggConfigProvider from 'ui/vis/agg_config';
import precisionTemplate from 'ui/agg_types/controls/precision.html';
import { geohashColumns } from 'ui/utils/decode_geo_hash';
export default function GeoHashAggDefinition(Private, config) {
const BucketAggType = Private(AggTypesBucketsBucketAggTypeProvider);
const AggConfig = Private(VisAggConfigProvider);
const defaultPrecision = 2;
const maxPrecision = parseInt(config.get('visualization:tileMap:maxPrecision'), 10) || 12;
/**
@ -54,6 +57,11 @@ export default function GeoHashAggDefinition(Private, config) {
default: true,
write: _.noop
},
{
name: 'useGeocentroid',
default: true,
write: _.noop
},
{
name: 'mapZoom',
write: _.noop
@ -79,6 +87,23 @@ export default function GeoHashAggDefinition(Private, config) {
output.params.precision = aggConfig.params.autoPrecision ? autoPrecisionVal : getPrecision(aggConfig.params.precision);
}
}
]
],
getRequestAggs: function (agg) {
if (!agg.params.useGeocentroid) {
return agg;
}
/**
* By default, add the geo_centroid aggregation
*/
return [agg, new AggConfig(agg.vis, {
type: 'geo_centroid',
enabled:true,
params: {
field: agg.getField()
},
schema: 'metric'
})];
}
});
}

View file

@ -18,7 +18,7 @@
</div>
</div>
<div class="vis-option-item">
<div class="vis-option-item indented">
<label>
<input type="checkbox"
name="autoPrecision"
@ -27,3 +27,12 @@
</label>
</div>
<div class="vis-option-item indented">
<label>
<input type="checkbox"
name="useGeocentroid"
ng-model="agg.params.useGeocentroid">
Place markers off grid (use geocentroid)
</label>
</div>

View file

@ -10,6 +10,7 @@ import AggTypesMetricsTopHitProvider from 'ui/agg_types/metrics/top_hit';
import AggTypesMetricsStdDeviationProvider from 'ui/agg_types/metrics/std_deviation';
import AggTypesMetricsCardinalityProvider from 'ui/agg_types/metrics/cardinality';
import AggTypesMetricsPercentilesProvider from 'ui/agg_types/metrics/percentiles';
import AggTypesMetricsGeoCentroidProvider from 'ui/agg_types/metrics/geo_centroid';
import AggTypesMetricsPercentileRanksProvider from 'ui/agg_types/metrics/percentile_ranks';
import AggTypesMetricsDerivativeProvider from 'ui/agg_types/metrics/derivative';
import AggTypesMetricsCumulativeSumProvider from 'ui/agg_types/metrics/cumulative_sum';
@ -28,6 +29,8 @@ import AggTypesMetricsBucketSumProvider from 'ui/agg_types/metrics/bucket_sum';
import AggTypesMetricsBucketAvgProvider from 'ui/agg_types/metrics/bucket_avg';
import AggTypesMetricsBucketMinProvider from 'ui/agg_types/metrics/bucket_min';
import AggTypesMetricsBucketMaxProvider from 'ui/agg_types/metrics/bucket_max';
export default function AggTypeService(Private) {
const aggs = {
@ -51,6 +54,7 @@ export default function AggTypeService(Private) {
Private(AggTypesMetricsBucketSumProvider),
Private(AggTypesMetricsBucketMinProvider),
Private(AggTypesMetricsBucketMaxProvider),
Private(AggTypesMetricsGeoCentroidProvider)
],
buckets: [
Private(AggTypesBucketsDateHistogramProvider),
@ -61,7 +65,7 @@ export default function AggTypeService(Private) {
Private(AggTypesBucketsTermsProvider),
Private(AggTypesBucketsFiltersProvider),
Private(AggTypesBucketsSignificantTermsProvider),
Private(AggTypesBucketsGeoHashProvider)
Private(AggTypesBucketsGeoHashProvider),
]
};

View file

@ -0,0 +1,22 @@
import AggTypesMetricsMetricAggTypeProvider from 'ui/agg_types/metrics/metric_agg_type';
export default function AggTypeMetricGeoCentroidProvider(Private) {
const MetricAggType = Private(AggTypesMetricsMetricAggTypeProvider);
return new MetricAggType({
name: 'geo_centroid',
title: 'Geo Centroid',
makeLabel: function () {
return 'Geo Centroid';
},
params: [
{
name: 'field',
filterFieldTypes: 'geo_point'
}
],
getValue: function (agg, bucket) {
return bucket[agg.id] && bucket[agg.id].location;
}
});
}

View file

@ -0,0 +1,38 @@
import { geohashColumns } from 'ui/utils/decode_geo_hash';
const maxPrecision = 12;
/**
* Map Leaflet zoom levels to geohash precision levels.
* The size of a geohash column-width on the map should be at least `minGeohashPixels` pixels wide.
*/
const zoomPrecisionMap = {};
const minGeohashPixels = 16;
function calculateZoomToPrecisionMap(maxZoom) {
for (let zoom = 0; zoom <= maxZoom; zoom += 1) {
if (typeof zoomPrecisionMap[zoom] === 'number') {
continue;
}
const worldPixels = 256 * Math.pow(2, zoom);
zoomPrecisionMap[zoom] = 1;
for (let precision = 2; precision <= maxPrecision; precision += 1) {
const columns = geohashColumns(precision);
if ((worldPixels / columns) >= minGeohashPixels) {
zoomPrecisionMap[zoom] = precision;
} else {
break;
}
}
}
}
export default function zoomToPrecision(mapZoom, maxPrecision, maxZoom) {
calculateZoomToPrecisionMap(typeof maxZoom === 'number' ? maxZoom : 21);
return Math.min(zoomPrecisionMap[mapZoom], maxPrecision);
}

View file

@ -260,6 +260,11 @@ export default function AggConfigFactory(Private) {
);
};
AggConfig.prototype.getRequestAggs = function () {
if (!this.type) return;
return this.type.getRequestAggs(this) || [this];
};
AggConfig.prototype.getResponseAggs = function () {
if (!this.type) return;
return this.type.getResponseAggs(this) || [this];

View file

@ -108,7 +108,6 @@ export default function AggConfigsFactory(Private) {
})
.value();
}
this.getRequestAggs()
.filter(function (config) {
return !config.type.hasNoDsl;
@ -145,12 +144,17 @@ export default function AggConfigsFactory(Private) {
});
removeParentAggs(dslTopLvl);
return dslTopLvl;
};
AggConfigs.prototype.getRequestAggs = function () {
return _.sortBy(this, function (agg) {
//collect all the aggregations
const aggregations = this.reduce((requestValuesAggs, agg) => {
const aggs = agg.getRequestAggs();
return aggs ? requestValuesAggs.concat(aggs) : requestValuesAggs;
}, []);
//move metrics to the end
return _.sortBy(aggregations, function (agg) {
return agg.schema.group === 'metrics' ? 1 : 0;
});
};

View file

@ -0,0 +1,494 @@
import expect from 'expect.js';
import KibanaMap from 'ui/vis_maps/kibana_map';
import GeohashLayer from 'ui/vis_maps/geohash_layer';
import sampleData from './geohash_sample_data';
describe('kibana_map tests', function () {
let domNode;
let kibanaMap;
function setupDOM() {
domNode = document.createElement('div');
domNode.style.top = '0';
domNode.style.left = '0';
domNode.style.width = '512px';
domNode.style.height = '512px';
domNode.style.position = 'fixed';
domNode.style['pointer-events'] = 'none';
document.body.appendChild(domNode);
}
function teardownDOM() {
domNode.innerHTML = '';
document.body.removeChild(domNode);
}
describe('GeohashGridLayer', function () {
beforeEach(async function () {
setupDOM();
kibanaMap = new KibanaMap(domNode, {
minZoom: 1,
maxZoom: 10
});
kibanaMap.setZoomLevel(3);
kibanaMap.setCenter({
lon: -100,
lat: 40
});
});
afterEach(function () {
kibanaMap.destroy();
teardownDOM();
});
[
{
options: { 'mapType': 'Scaled Circle Markers' },
expected: `[
{
"fill": "#bd0026",
"d": "M343,263.8A19.2,19.2,0,1,1,342.9,263.8 z"
},
{
"fill": "#bd0026",
"d": "M343,225.03843394373595A18.961566056264047,18.961566056264047,0,1,1,342.9,225.03843394373595 z"
},
{
"fill": "#bd0026",
"d": "M283,264.19815701843777A17.80184298156226,17.80184298156226,0,1,1,282.9,264.19815701843777 z"
},
{
"fill": "#f03b20",
"d": "M405,224.2748797495895A16.72512025041049,16.72512025041049,0,1,1,404.9,224.2748797495895 z"
},
{
"fill": "#f03b20",
"d": "M285,223.50180417608374A16.498195823916255,16.498195823916255,0,1,1,284.9,223.50180417608374 z"
},
{
"fill": "#f03b20",
"d": "M343,299.1036928470748A15.896307152925205,15.896307152925205,0,1,1,342.9,299.1036928470748 z"
},
{
"fill": "#f03b20",
"d": "M283,300.2846189453604A15.71538105463958,15.71538105463958,0,1,1,282.9,300.2846189453604 z"
},
{
"fill": "#fd8d3c",
"d": "M148,267.0272116156895A13.972788384310489,13.972788384310489,0,1,1,147.9,267.0272116156895 z"
},
{
"fill": "#feb24c",
"d": "M219,270.4178825645856A11.582117435414355,11.582117435414355,0,1,1,218.9,270.4178825645856 z"
},
{
"fill": "#feb24c",
"d": "M146,189.63311915018554A11.366880849814459,11.366880849814459,0,1,1,145.9,189.63311915018554 z"
},
{
"fill": "#feb24c",
"d": "M281,191.96973262756177A11.030267372438226,11.030267372438226,0,1,1,280.9,191.96973262756177 z"
},
{
"fill": "#feb24c",
"d": "M220,231.85362974571228A10.146370254287714,10.146370254287714,0,1,1,219.9,231.85362974571228 z"
},
{
"fill": "#feb24c",
"d": "M144,231.1923722152369A9.807627784763092,9.807627784763092,0,1,1,143.9,231.1923722152369 z"
},
{
"fill": "#feb24c",
"d": "M387,268.27221854599287A9.72778145400714,9.72778145400714,0,1,1,386.9,268.27221854599287 z"
},
{
"fill": "#feb24c",
"d": "M217,191.09542834646925A8.90457165353074,8.90457165353074,0,1,1,216.9,191.09542834646925 z"
},
{
"fill": "#fed976",
"d": "M218,300.40744573968243A8.592554260317598,8.592554260317598,0,1,1,217.9,300.40744573968243 z"
},
{
"fill": "#fed976",
"d": "M363,339.5411821762003A7.458817823799684,7.458817823799684,0,1,1,362.9,339.5411821762003 z"
},
{
"fill": "#fed976",
"d": "M331,205.43072931381437A6.569270686185644,6.569270686185644,0,1,1,330.9,205.43072931381437 z"
},
{
"fill": "#fed976",
"d": "M163,299.9012571034098A5.098742896590189,5.098742896590189,0,1,1,162.9,299.9012571034098 z"
},
{
"fill": "#fed976",
"d": "M34,77.6735731867532A4.326426813246795,4.326426813246795,0,1,1,33.9,77.6735731867532 z"
},
{
"fill": "#fed976",
"d": "M268,341.7954688958982A4.204531104101819,4.204531104101819,0,1,1,267.9,341.7954688958982 z"
},
{
"fill": "#fed976",
"d": "M71,118.82649906983305A4.173500930166947,4.173500930166947,0,1,1,70.9,118.82649906983305 z"
},
{
"fill": "#fed976",
"d": "M119,235.1169130974434A3.8830869025566206,3.8830869025566206,0,1,1,118.9,235.1169130974434 z"
},
{
"fill": "#fed976",
"d": "M451,396.15053353027315A3.849466469726874,3.849466469726874,0,1,1,450.9,396.15053353027315 z"
},
{
"fill": "#fed976",
"d": "M64,104.18445019554242A3.815549804457569,3.815549804457569,0,1,1,63.9,104.18445019554242 z"
},
{
"fill": "#fed976",
"d": "M7,15.430879972386867A3.5691200276131325,3.5691200276131325,0,1,1,6.9,15.430879972386867 z"
},
{
"fill": "#fed976",
"d": "M434,206.8985557756997A3.1014442243003013,3.1014442243003013,0,1,1,433.9,206.8985557756997 z"
},
{
"fill": "#fed976",
"d": "M119,201.2073035006183A2.792696499381677,2.792696499381677,0,1,1,118.9,201.2073035006183 z"
},
{
"fill": "#fed976",
"d": "M-1,420.89773444794906A2.1022655520509095,2.1022655520509095,0,1,1,-1.1,420.89773444794906 z"
},
{
"fill": "#fed976",
"d": "M443,217.859886428343A1.1401135716569843,1.1401135716569843,0,1,1,442.9,217.859886428343 z"
},
{
"fill": "#fed976",
"d": "M121,260.85988642834303A1.1401135716569843,1.1401135716569843,0,1,1,120.9,260.85988642834303 z"
},
{
"fill": "#fed976",
"d": "M-4,399.27892886445886A0.7210711355411324,0.7210711355411324,0,1,1,-4.1,399.27892886445886 z"
}
]`
},
{
options: { 'mapType': 'Shaded Circle Markers' },
expected: `[
{
"fill": "#bd0026",
"d": "M343,267A16,16,0,1,1,342.9,267 z"
},
{
"fill": "#bd0026",
"d": "M343,226A18,18,0,1,1,342.9,226 z"
},
{
"fill": "#bd0026",
"d": "M283,266A16,16,0,1,1,282.9,266 z"
},
{
"fill": "#f03b20",
"d": "M405,223A18,18,0,1,1,404.9,223 z"
},
{
"fill": "#f03b20",
"d": "M285,222A18,18,0,1,1,284.9,222 z"
},
{
"fill": "#f03b20",
"d": "M343,300A15,15,0,1,1,342.9,300 z"
},
{
"fill": "#f03b20",
"d": "M283,301A15,15,0,1,1,282.9,301 z"
},
{
"fill": "#fd8d3c",
"d": "M148,265A16,16,0,1,1,147.9,265 z"
},
{
"fill": "#feb24c",
"d": "M219,266A16,16,0,1,1,218.9,266 z"
},
{
"fill": "#feb24c",
"d": "M146,183A18,18,0,1,1,145.9,183 z"
},
{
"fill": "#feb24c",
"d": "M281,184A19,19,0,1,1,280.9,184 z"
},
{
"fill": "#feb24c",
"d": "M220,225A17,17,0,1,1,219.9,225 z"
},
{
"fill": "#feb24c",
"d": "M144,224A17,17,0,1,1,143.9,224 z"
},
{
"fill": "#feb24c",
"d": "M387,262A16,16,0,1,1,386.9,262 z"
},
{
"fill": "#feb24c",
"d": "M217,181A19,19,0,1,1,216.9,181 z"
},
{
"fill": "#fed976",
"d": "M218,293A16,16,0,1,1,217.9,293 z"
},
{
"fill": "#fed976",
"d": "M363,333A14,14,0,1,1,362.9,333 z"
},
{
"fill": "#fed976",
"d": "M331,194A18,18,0,1,1,330.9,194 z"
},
{
"fill": "#fed976",
"d": "M163,290A15,15,0,1,1,162.9,290 z"
},
{
"fill": "#fed976",
"d": "M34,56A26,26,0,1,1,33.9,56 z"
},
{
"fill": "#fed976",
"d": "M268,332A14,14,0,1,1,267.9,332 z"
},
{
"fill": "#fed976",
"d": "M71,100A23,23,0,1,1,70.9,100 z"
},
{
"fill": "#fed976",
"d": "M119,222A17,17,0,1,1,118.9,222 z"
},
{
"fill": "#fed976",
"d": "M451,387A13,13,0,1,1,450.9,387 z"
},
{
"fill": "#fed976",
"d": "M64,84A24,24,0,1,1,63.9,84 z"
},
{
"fill": "#fed976",
"d": "M7,-7A26,26,0,1,1,6.9,-7 z"
},
{
"fill": "#fed976",
"d": "M434,192A18,18,0,1,1,433.9,192 z"
},
{
"fill": "#fed976",
"d": "M119,185A19,19,0,1,1,118.9,185 z"
},
{
"fill": "#fed976",
"d": "M-1,410A13,13,0,1,1,-1.1,410 z"
},
{
"fill": "#fed976",
"d": "M443,201A18,18,0,1,1,442.9,201 z"
},
{
"fill": "#fed976",
"d": "M121,245A17,17,0,1,1,120.9,245 z"
},
{
"fill": "#fed976",
"d": "M-4,386A14,14,0,1,1,-4.1,386 z"
}
]`
},
{
options: { 'mapType': 'Shaded Geohash Grid' },
expected: `[
{
"fill": "#bd0026",
"d": "M313 301L313 261L377 261L377 301z"
},
{
"fill": "#bd0026",
"d": "M313 261L313 218L377 218L377 261z"
},
{
"fill": "#bd0026",
"d": "M249 301L249 261L313 261L313 301z"
},
{
"fill": "#f03b20",
"d": "M377 261L377 218L441 218L441 261z"
},
{
"fill": "#f03b20",
"d": "M249 261L249 218L313 218L313 261z"
},
{
"fill": "#f03b20",
"d": "M313 338L313 301L377 301L377 338z"
},
{
"fill": "#f03b20",
"d": "M249 338L249 301L313 301L313 338z"
},
{
"fill": "#fd8d3c",
"d": "M121 301L121 261L185 261L185 301z"
},
{
"fill": "#feb24c",
"d": "M185 301L185 261L249 261L249 301z"
},
{
"fill": "#feb24c",
"d": "M121 218L121 170L185 170L185 218z"
},
{
"fill": "#feb24c",
"d": "M249 218L249 170L313 170L313 218z"
},
{
"fill": "#feb24c",
"d": "M185 261L185 218L249 218L249 261z"
},
{
"fill": "#feb24c",
"d": "M121 261L121 218L185 218L185 261z"
},
{
"fill": "#feb24c",
"d": "M377 301L377 261L441 261L441 301z"
},
{
"fill": "#feb24c",
"d": "M185 218L185 170L249 170L249 218z"
},
{
"fill": "#fed976",
"d": "M185 338L185 301L249 301L249 338z"
},
{
"fill": "#fed976",
"d": "M313 374L313 338L377 338L377 374z"
},
{
"fill": "#fed976",
"d": "M313 218L313 170L377 170L377 218z"
},
{
"fill": "#fed976",
"d": "M121 338L121 301L185 301L185 338z"
},
{
"fill": "#fed976",
"d": "M-7 116L-7 54L57 54L57 116z"
},
{
"fill": "#fed976",
"d": "M249 374L249 338L313 338L313 374z"
},
{
"fill": "#fed976",
"d": "M57 170L57 116L121 116L121 170z"
},
{
"fill": "#fed976",
"d": "M57 261L57 218L121 218L121 261z"
},
{
"fill": "#fed976",
"d": "M441 408L441 374L505 374L505 408z"
},
{
"fill": "#fed976",
"d": "M57 116L57 54L121 54L121 116z"
},
{
"fill": "#fed976",
"d": "M-7 54L-7 -21L57 -21L57 54z"
},
{
"fill": "#fed976",
"d": "M377 218L377 170L441 170L441 218z"
},
{
"fill": "#fed976",
"d": "M57 218L57 170L121 170L121 218z"
},
{
"fill": "#fed976",
"d": "M-7 441L-7 408L57 408L57 441z"
},
{
"fill": "#fed976",
"d": "M441 261L441 218L505 218L505 261z"
},
{
"fill": "#fed976",
"d": "M57 301L57 261L121 261L121 301z"
},
{
"fill": "#fed976",
"d": "M-7 408L-7 374L57 374L57 408z"
}
]`
}
].forEach(function (test) {
it(test.options.mapType, function () {
const geohashGridOptions = test.options;
const geohashLayer = new GeohashLayer(sampleData, geohashGridOptions, kibanaMap.getZoomLevel(), kibanaMap);
kibanaMap.addLayer(geohashLayer);
const markersNodeList = domNode.querySelectorAll('path.leaflet-clickable');
const markerArray = [];
for (let i = 0; i < markersNodeList.length; i++) {
markerArray.push(markersNodeList[i]);
}
const expectedGeohashGridMarkers = test.expected;
const expectedMarkers = JSON.parse(expectedGeohashGridMarkers).map(path => {
return {
fill: path.fill,
coords: path.d.match(/[0-9\.]+/g).map(parseFloat)
};
});
const actualMarkers = markerArray.map(a => {
return {
fill: a.getAttribute('fill'),
coords: a.getAttribute('d').match(/[0-9\.]+/g).map(parseFloat)
};
});
expect(actualMarkers.length).to.equal(expectedMarkers.length);
for (let i = 0; i < expectedMarkers.length; i++) {
expect(actualMarkers[i].fill).to.equal(expectedMarkers[i].fill);
actualMarkers[i].coords.forEach((coord, c) => {
closeTo(actualMarkers[i].coords[c], expectedMarkers[i].coords[c]);
});
}
});
});
});
});
function closeTo(actual, expected) {
const epsilon = 1;//allow 2px slack
expect(actual - epsilon < expected && expected < actual + epsilon).to.equal(true);
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,137 @@
import expect from 'expect.js';
import KibanaMap from 'ui/vis_maps/kibana_map';
describe('kibana_map tests', function () {
let domNode;
let kibanaMap;
function setupDOM() {
domNode = document.createElement('div');
domNode.style.top = '0';
domNode.style.left = '0';
domNode.style.width = '512px';
domNode.style.height = '512px';
domNode.style.position = 'fixed';
domNode.style['pointer-events'] = 'none';
document.body.appendChild(domNode);
}
function teardownDOM() {
domNode.innerHTML = '';
document.body.removeChild(domNode);
}
describe('KibanaMap - basics', function () {
beforeEach(async function () {
setupDOM();
kibanaMap = new KibanaMap(domNode, {
minZoom: 1,
maxZoom: 10
});
});
afterEach(function () {
kibanaMap.destroy();
teardownDOM();
});
it('should instantiate with world in view', function () {
const bounds = kibanaMap.getBounds();
expect(bounds.bottom_right.lon).to.equal(180);
expect(bounds.top_left.lon).to.equal(-180);
expect(kibanaMap.getCenter().lon).to.equal(0);
expect(kibanaMap.getCenter().lat).to.equal(0);
expect(kibanaMap.getZoomLevel()).to.equal(1);
});
it('should resize to fit container', function () {
kibanaMap.setZoomLevel(2);
expect(kibanaMap.getCenter().lon).to.equal(0);
expect(kibanaMap.getCenter().lat).to.equal(0);
domNode.style.width = '1024px';
domNode.style.height = '1024px';
kibanaMap.resize();
expect(kibanaMap.getCenter().lon).to.equal(0);
expect(kibanaMap.getCenter().lat).to.equal(0);
const bounds = kibanaMap.getBounds();
expect(bounds.bottom_right.lon).to.equal(180);
expect(bounds.top_left.lon).to.equal(-180);
});
});
describe('KibanaMap - baseLayer', function () {
beforeEach(async function () {
setupDOM();
kibanaMap = new KibanaMap(domNode, {
minZoom: 1,
maxZoom: 10
});
});
afterEach(function () {
kibanaMap.destroy();
teardownDOM();
});
it('TMS', async function () {
const options = {
'url': 'https://tiles-stage.elastic.co/v2/default/{z}/{x}/{y}.png?elastic_tile_service_tos=agree&my_app_name=kibana',
'minZoom': 0,
'maxZoom': 12,
'attribution': '© [Elastic Tile Service](https://www.elastic.co/elastic-tile-service)'
};
return new Promise(function (resolve) {
kibanaMap.on('baseLayer:loaded', () => {
resolve();
});
kibanaMap.setBaseLayer({
baseLayerType: 'tms',
options: options
});
});
});
it('WMS', async function () {
const options = {
url: 'https://basemap.nationalmap.gov/arcgis/services/USGSTopo/ MapServer/WMSServer',
version: '1.3.0',
layers: '0',
format: 'image/png',
transparent: true,
attribution: 'Maps provided by USGS',
styles: '',
minZoom: 1,
maxZoom: 18
};
return new Promise(function (resolve) {
kibanaMap.on('baseLayer:loaded', () => {
resolve();
});
kibanaMap.setBaseLayer({
baseLayerType: 'wms',
options: options
});
});
});
});
});

View file

@ -1,215 +0,0 @@
import expect from 'expect.js';
import ngMock from 'ng_mock';
import _ from 'lodash';
import L from 'leaflet';
import sinon from 'auto-release-sinon';
import geoJsonData from 'fixtures/vislib/mock_data/geohash/_geo_json';
import $ from 'jquery';
import VislibVisualizationsMapProvider from 'ui/vis_maps/visualizations/_map';
// // Data
// const dataArray = [
// ['geojson', require('fixtures/vislib/mock_data/geohash/_geo_json')],
// ['columns', require('fixtures/vislib/mock_data/geohash/_columns')],
// ['rows', require('fixtures/vislib/mock_data/geohash/_rows')],
// ];
// TODO: Test the specific behavior of each these
// const mapTypes = [
// 'Scaled Circle Markers',
// 'Shaded Circle Markers',
// 'Shaded Geohash Grid',
// 'Heatmap'
// ];
describe('tilemaptest - TileMap Map Tests', function () {
const $mockMapEl = $('<div>');
let TileMapMap;
let tilemapSettings;
const leafletStubs = {};
const leafletMocks = {};
beforeEach(ngMock.module('kibana'));
beforeEach(ngMock.inject(function (Private, $injector) {
// mock parts of leaflet
leafletMocks.tileLayer = { on: sinon.stub() };
leafletMocks.map = { on: sinon.stub() };
leafletStubs.tileLayer = sinon.stub(L, 'tileLayer', _.constant(leafletMocks.tileLayer));
leafletStubs.tileLayer.wms = sinon.stub(L.tileLayer, 'wms', _.constant(leafletMocks.tileLayer));
leafletStubs.map = sinon.stub(L, 'map', _.constant(leafletMocks.map));
TileMapMap = Private(VislibVisualizationsMapProvider);
tilemapSettings = $injector.get('tilemapSettings');
}));
async function loadTileMapSettings() {
await tilemapSettings.loadSettings();
}
describe('instantiation', function () {
let createStub;
beforeEach(loadTileMapSettings);
beforeEach(async function () {
createStub = sinon.stub(TileMapMap.prototype, '_createMap', _.noop);
new TileMapMap($mockMapEl, geoJsonData, {});
});
it('should create the map', function () {
expect(createStub.callCount).to.equal(1);
});
});
describe('createMap', function () {
let map;
let mapStubs;
beforeEach(loadTileMapSettings);
beforeEach(function () {
mapStubs = {
destroy: sinon.stub(TileMapMap.prototype, 'destroy'),
attachEvents: sinon.stub(TileMapMap.prototype, '_attachEvents'),
addMarkers: sinon.stub(TileMapMap.prototype, '_addMarkers'),
};
map = new TileMapMap($mockMapEl, geoJsonData, {});
});
it('should create leaflet objects for tileLayer and map', function () {
expect(leafletStubs.tileLayer.callCount).to.equal(1);
expect(leafletStubs.map.callCount).to.equal(1);
const callArgs = leafletStubs.map.firstCall.args;
const 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);
});
it('should create a WMS layer if WMS is enabled', function () {
expect(L.tileLayer.wms.called).to.be(false);
map = new TileMapMap($mockMapEl, geoJsonData, { attr: { wms: { enabled: true } } });
map._createMap();
expect(L.tileLayer.wms.called).to.be(true);
});
it('should create layer with all options from `tilemapSettings.getOptions()`', () => {
sinon.assert.calledOnce(L.tileLayer);
const leafletOptions = tilemapSettings.getTMSOptions();
expect(L.tileLayer.firstCall.args[1]).to.eql(leafletOptions);
});
});
describe('attachEvents', function () {
beforeEach(loadTileMapSettings);
beforeEach(function () {
sinon.stub(TileMapMap.prototype, '_createMap', function () {
this._tileLayer = leafletMocks.tileLayer;
this.map = leafletMocks.map;
this._attachEvents();
});
new TileMapMap($mockMapEl, geoJsonData, {});
});
it('should attach interaction events', function () {
const expectedTileEvents = ['tileload'];
const expectedMapEvents = ['draw:created', 'moveend', 'zoomend', 'unload'];
const matchedEvents = {
tiles: 0,
maps: 0,
};
_.times(leafletMocks.tileLayer.on.callCount, function (index) {
const 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) {
const ev = leafletMocks.map.on.getCall(index).args[0];
if (_.includes(expectedMapEvents, ev)) matchedEvents.maps++;
});
expect(matchedEvents.maps).to.equal(expectedMapEvents.length);
});
});
describe('addMarkers', function () {
let map;
let createStub;
beforeEach(loadTileMapSettings);
beforeEach(function () {
sinon.stub(TileMapMap.prototype, '_createMap');
createStub = sinon.stub(TileMapMap.prototype, '_createMarkers', _.constant({ addLegend: _.noop }));
map = new TileMapMap($mockMapEl, geoJsonData, {});
});
it('should pass the map options to the marker', function () {
map._addMarkers();
const 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 () {
const destroyStub = sinon.stub();
map._markers = { destroy: destroyStub };
map._addMarkers();
expect(destroyStub.callCount).to.be(1);
});
});
describe('getDataRectangles', function () {
let map;
beforeEach(loadTileMapSettings);
beforeEach(function () {
sinon.stub(TileMapMap.prototype, '_createMap');
map = new TileMapMap($mockMapEl, geoJsonData, {});
});
it('should return an empty array if no data', function () {
map = new TileMapMap($mockMapEl, {}, {});
const rects = map._getDataRectangles();
expect(rects).to.have.length(0);
});
it('should return an array of arrays of rectangles', function () {
const rects = map._getDataRectangles();
_.times(5, function () {
const index = _.random(rects.length - 1);
const rect = rects[index];
const featureRect = geoJsonData.geoJson.features[index].properties.rectangle;
expect(rect.length).to.equal(featureRect.length);
// should swap the array
const checkIndex = _.random(rect.length - 1);
expect(rect[checkIndex]).to.eql(featureRect[checkIndex]);
});
});
});
});

View file

@ -1,362 +0,0 @@
import angular from 'angular';
import expect from 'expect.js';
import ngMock from 'ng_mock';
import _ from 'lodash';
import L from 'leaflet';
import sinon from 'auto-release-sinon';
import geoJsonData from 'fixtures/vislib/mock_data/geohash/_geo_json';
import VislibVisualizationsMarkerTypesBaseMarkerProvider from 'ui/vis_maps/visualizations/marker_types/base_marker';
import VislibVisualizationsMarkerTypesShadedCirclesProvider from 'ui/vis_maps/visualizations/marker_types/shaded_circles';
import VislibVisualizationsMarkerTypesScaledCirclesProvider from 'ui/vis_maps/visualizations/marker_types/scaled_circles';
import VislibVisualizationsMarkerTypesHeatmapProvider from 'ui/vis_maps/visualizations/marker_types/heatmap';
// defaults to roughly the lower 48 US states
const defaultSWCoords = [13.496, -143.789];
const defaultNECoords = [55.526, -57.919];
const bounds = {};
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);
}
const mockMap = {
addLayer: _.noop,
closePopup: _.noop,
getBounds: getBounds,
removeControl: _.noop,
removeLayer: _.noop,
getZoom: _.constant(5)
};
describe('tilemaptest - Marker Tests', function () {
let mapData;
let markerLayer;
function createMarker(MarkerClass, geoJson, tooltipFormatter) {
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,
tooltipFormatter: tooltipFormatter || null
});
}
beforeEach(function () {
setBounds();
});
afterEach(function () {
if (markerLayer) {
markerLayer.destroy();
markerLayer = undefined;
}
});
describe('Base Methods', function () {
let MarkerClass;
beforeEach(ngMock.module('MarkerFactory'));
beforeEach(ngMock.inject(function (Private) {
MarkerClass = Private(VislibVisualizationsMarkerTypesBaseMarkerProvider);
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]);
const boundFilter = markerLayer._filterToMapBounds();
const 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]);
const boundFilter = markerLayer._filterToMapBounds();
const 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 () {
const minColor = markerLayer._legendQuantizer(mapData.properties.allmin);
const 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 () {
const geoJson = { properties: { min: 1, max: 1 } };
markerLayer = createMarker(MarkerClass, geoJson);
// ensure the quantizer domain is correct
const 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 () {
const randColor = markerLayer._legendQuantizer(0);
expect(randColor).to.equal(color);
});
});
});
describe('applyShadingStyle', function () {
it('should return a style object', function () {
const style = markerLayer.applyShadingStyle(100);
expect(style).to.be.an('object');
const keys = _.keys(style);
const expected = ['fillColor', 'color'];
_.each(expected, function (key) {
expect(keys).to.contain(key);
});
});
it('should use the legendQuantizer', function () {
const spy = sinon.spy(markerLayer, '_legendQuantizer');
markerLayer.applyShadingStyle(100);
expect(spy.callCount).to.equal(1);
});
});
describe('showTooltip', function () {
it('should use the tooltip formatter', function () {
const sample = _.sample(mapData.features);
markerLayer = createMarker(MarkerClass, null, Function.prototype);//create marker with tooltip
markerLayer._attr.addTooltip = true;
const stub = sinon.stub(markerLayer, '_tooltipFormatter', function () {
return;
});
markerLayer._showTooltip(sample);
expect(stub.callCount).to.equal(1);
expect(stub.firstCall.calledWith(sample)).to.be(true);
});
});
describe('addLegend', function () {
let addToSpy;
let leafletControlStub;
beforeEach(function () {
addToSpy = sinon.spy();
leafletControlStub = sinon.stub(L, 'control', function () {
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 () {
const formatterSpy = sinon.spy(markerLayer, '_valueFormatter');
// called twice for every legend color defined
const expectedCallCount = markerLayer._legendColors.length * 2;
markerLayer.addLegend();
const legend = markerLayer._legend.onAdd();
expect(formatterSpy.callCount).to.equal(expectedCallCount);
expect(legend).to.be.a(HTMLDivElement);
});
});
});
describe('Shaded Circles', function () {
beforeEach(ngMock.module('MarkerFactory'));
beforeEach(ngMock.inject(function (Private) {
const MarkerClass = Private(VislibVisualizationsMarkerTypesShadedCirclesProvider);
markerLayer = createMarker(MarkerClass);
}));
describe('geohashMinDistance method', function () {
it('should return a finite number', function () {
const sample = _.sample(mapData.features);
const distance = markerLayer._geohashMinDistance(sample);
expect(distance).to.be.a('number');
expect(_.isFinite(distance)).to.be(true);
});
});
});
describe('Scaled Circles', function () {
let zoom;
beforeEach(ngMock.module('MarkerFactory'));
beforeEach(ngMock.inject(function (Private) {
zoom = _.random(1, 18);
sinon.stub(mockMap, 'getZoom', _.constant(zoom));
const MarkerClass = Private(VislibVisualizationsMarkerTypesScaledCirclesProvider);
markerLayer = createMarker(MarkerClass);
}));
describe('radiusScale method', function () {
const valueArray = [10, 20, 30, 40, 50, 60];
const max = _.max(valueArray);
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 () {
const upperBound = markerLayer._radiusScale(max);
const results = [];
function roundValue(value) {
// round number to 6 decimal places
const r = Math.pow(10, 6);
return Math.round(value * r) / r;
}
_.each(valueArray, function (value, i) {
const ratio = Math.pow(value / max, 0.5);
const comparison = ratio * upperBound;
const radius = markerLayer._radiusScale(value);
const 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(ngMock.module('MarkerFactory'));
beforeEach(ngMock.inject(function (Private) {
const MarkerClass = Private(VislibVisualizationsMarkerTypesHeatmapProvider);
markerLayer = createMarker(MarkerClass);
}));
describe('dataToHeatArray', function () {
let max;
beforeEach(function () {
max = mapData.properties.allmax;
});
it('should return an array or values for each feature', function () {
const 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 () {
const arr = markerLayer._dataToHeatArray(max);
const index = _.random(mapData.features.length - 1);
const feature = mapData.features[index];
const featureValue = feature.properties.value;
const 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;
const arr = markerLayer._dataToHeatArray(max);
const index = _.random(mapData.features.length - 1);
const feature = mapData.features[index];
const featureValue = feature.properties.value / max;
const 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 () {
const feature = _.sample(mapData.features);
const point = markerLayer._getLatLng(feature);
const 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 () {
const feature = _.sample(mapData.features);
const point = L.latLng(90, -180);
const arr = markerLayer._tooltipProximity(point, feature);
expect(arr).to.be(false);
});
});
});
describe('nearestFeature', function () {
it('should return nearest geoJson feature object', function () {
_.times(5, function () {
const feature = _.sample(mapData.features);
const point = markerLayer._getLatLng(feature);
const nearestPoint = markerLayer._nearestFeature(point);
expect(nearestPoint).to.equal(feature);
});
});
});
describe('getLatLng', function () {
it('should return a leaflet latLng object', function () {
const feature = _.sample(mapData.features);
const latLng = markerLayer._getLatLng(feature);
const compare = L.latLng(feature.geometry.coordinates.slice(0).reverse());
expect(latLng).to.eql(compare);
});
it('should memoize the result', function () {
const spy = sinon.spy(L, 'latLng');
const feature = _.sample(mapData.features);
markerLayer._getLatLng(feature);
expect(spy.callCount).to.be(1);
markerLayer._getLatLng(feature);
expect(spy.callCount).to.be(1);
});
});
});
});

View file

@ -1,137 +0,0 @@
import expect from 'expect.js';
import ngMock from 'ng_mock';
import _ from 'lodash';
import sinon from 'auto-release-sinon';
import geoJsonData from 'fixtures/vislib/mock_data/geohash/_geo_json';
import MockMap from 'fixtures/tilemap_map';
import $ from 'jquery';
import VislibVisualizationsTileMapProvider from 'ui/vis_maps/visualizations/tile_map';
const mockChartEl = $('<div>');
let TileMap;
let extentsStub;
function createTileMap(handler, chartEl, chartData) {
handler = handler || {
visConfig: {
get: function () {
return '';
}
},
uiState: {
get: function () {
return '';
}
}
};
chartEl = chartEl || mockChartEl;
chartData = chartData || geoJsonData;
return new TileMap(handler, chartEl, chartData);
}
describe('tilemaptest - TileMap Tests', function () {
let tilemap;
beforeEach(ngMock.module('kibana'));
beforeEach(ngMock.inject(function (Private) {
Private.stub(require('ui/vis_maps/visualizations/_map'), MockMap);
TileMap = Private(VislibVisualizationsTileMapProvider);
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');
});
});
describe('appendMap', function () {
let $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);
const 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 () {
const noData = {
geohashGridAgg: { vis: { params: {} } },
geoJson: {
features: [],
properties: {},
hits: 20
}
};
tilemap = createTileMap(null, null, noData);
tilemap._appendMap($selection);
expect(tilemap.maps).to.have.length(1);
const 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 () {
const mapTitle = 'Test Title';
tilemap = createTileMap(null, null, _.assign({ title: mapTitle }, geoJsonData));
tilemap._appendMap($selection);
const map = tilemap.maps[0];
expect(map.addTitle.callCount).to.equal(1);
expect(map.addTitle.firstCall.calledWith(mapTitle)).to.equal(true);
});
});
describe('destroy', function () {
const maps = [];
const 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);
});
});
});
});

View file

@ -0,0 +1,90 @@
import KibanaMapLayer from './kibana_map_layer';
import _ from 'lodash';
import Heatmap from './markers/heatmap';
import ScaledCircles from './markers/scaled_circles';
import ShadedCircles from './markers/shaded_circles';
import GeohashGrid from './markers/geohash_grid';
export default class GeohashLayer extends KibanaMapLayer {
constructor(featureCollection, options, zoom, kibanaMap) {
super();
this._geohashGeoJson = featureCollection;
this._geohashOptions = options;
this._zoom = zoom;
this._kibanaMap = kibanaMap;
this._createGeohashMarkers();
}
_createGeohashMarkers() {
const markerOptions = {
valueFormatter: this._geohashOptions.valueFormatter,
tooltipFormatter: this._geohashOptions.tooltipFormatter
};
switch (this._geohashOptions.mapType) {
case 'Scaled Circle Markers':
this._geohashMarkers = new ScaledCircles(this._geohashGeoJson, markerOptions, this._zoom, this._kibanaMap);
break;
case 'Shaded Circle Markers':
this._geohashMarkers = new ShadedCircles(this._geohashGeoJson, markerOptions, this._zoom, this._kibanaMap);
break;
case 'Shaded Geohash Grid':
this._geohashMarkers = new GeohashGrid(this._geohashGeoJson, markerOptions, this._zoom, this._kibanaMap);
break;
case 'Heatmap':
this._geohashMarkers = new Heatmap(this._geohashGeoJson, {
radius: parseFloat(this._geohashOptions.heatmap.heatRadius),
blur: parseFloat(this._geohashOptions.heatmap.heatBlur),
maxZoom: parseFloat(this._geohashOptions.heatmap.heatMaxZoom),
minOpaxity: parseFloat(this._geohashOptions.heatmap.heatMinOpacity),
heatNormalizeData: parseFloat(this._geohashOptions.heatmap.heatNormalizeData),
tooltipFormatter: this._geohashOptions.tooltipFormatter
}, this._zoom, this._kibanaMap);
break;
default:
throw new Error(`${this._geohashOptions.mapType} mapType not recognized`);
}
this._geohashMarkers.on('showTooltip', (event) => this.emit('showTooltip', event));
this._geohashMarkers.on('hideTooltip', (event) => this.emit('hideTooltip', event));
this._leafletLayer = this._geohashMarkers.getLeafletLayer();
}
appendLegendContents(jqueryDiv) {
return this._geohashMarkers.appendLegendContents(jqueryDiv);
}
movePointer(...args) {
this._geohashMarkers.movePointer(...args);
}
updateExtent() {
//this removal is required to trigger the bounds filter again
this._kibanaMap.removeLayer(this);
this._createGeohashMarkers();
this._kibanaMap.addLayer(this);
}
isReusable(options) {
if (_.isEqual(this._geohashOptions, options)) {
return true;
}
if (this._geohashOptions.mapType !== options.mapType) {
return false;
} else if (this._geohashOptions.mapType === 'Heatmap' && !_.isEqual(this._geohashOptions.heatmap, options)) {
return false;
} else {
return true;
}
}
}

View file

@ -0,0 +1,526 @@
import { EventEmitter } from 'events';
import L from 'leaflet';
import $ from 'jquery';
import _ from 'lodash';
import zoomToPrecision from 'ui/utils/zoom_to_precision';
const FitControl = L.Control.extend({
options: {
position: 'topleft'
},
initialize: function (fitContainer, kibanaMap) {
this._fitContainer = fitContainer;
this._kibanaMap = kibanaMap;
this._leafletMap = null;
},
onAdd: function (leafletMap) {
this._leafletMap = leafletMap;
$(this._fitContainer).html('<a class="fa fa-crop" href="#" title="Fit Data Bounds"></a>')
.on('click', e => {
e.preventDefault();
this._kibanaMap.fitToData();
});
return this._fitContainer;
},
onRemove: function () {
$(this._fitContainer).off('click');
}
});
const LegendControl = L.Control.extend({
options: {
position: 'topright'
},
updateContents() {
this._legendContainer.empty();
const $div = $('<div>').addClass('tilemap-legend');
this._legendContainer.append($div);
const layers = this._kibanaMap.getLayers();
layers.forEach((layer) =>layer.appendLegendContents($div));
},
initialize: function (container, kibanaMap, position) {
this._legendContainer = container;
this._kibanaMap = kibanaMap;
this.options.position = position;
},
onAdd: function () {
this._layerUpdateHandle = () => this.updateContents();
this._kibanaMap.on('layers:update', this._layerUpdateHandle);
this.updateContents();
return this._legendContainer.get(0);
},
onRemove: function () {
this._kibanaMap.removeListener('layers:update', this._layerUpdateHandle);
}
});
/**
* Collects map functionality required for Kibana.
* Serves as simple abstraction for leaflet as well.
*/
class KibanaMap extends EventEmitter {
constructor(containerNode, options) {
super();
this._containerNode = containerNode;
this._leafletBaseLayer = null;
this._baseLayerSettings = null;
this._baseLayerIsDesaturated = true;
this._leafletDrawControl = null;
this._leafletFitControl = null;
this._leafletLegendControl = null;
this._legendPosition = 'topright';
this._layers = [];
this._listeners = [];
this._showTooltip = false;
this._leafletMap = L.map(containerNode, {
minZoom: options.minZoom,
maxZoom: options.maxZoom
});
this._leafletMap.fitWorld();
const worldBounds = L.latLngBounds(L.latLng(-90, -180), L.latLng(90, 180));
this._leafletMap.setMaxBounds(worldBounds);
let previousZoom = this._leafletMap.getZoom();
this._leafletMap.on('zoomend', () => {
if (previousZoom !== this._leafletMap.getZoom()) {
previousZoom = this._leafletMap.getZoom();
this.emit('zoomchange');
}
});
this._leafletMap.on('zoomend', () => this.emit('zoomend'));
this._leafletMap.on('moveend', () => this.emit('moveend'));
this._leafletMap.on('dragend', e => this._layers.forEach(layer => layer.updateExtent('dragend', e)));
this._leafletMap.on('mousemove', e => this._layers.forEach(layer => layer.movePointer('mousemove', e)));
this._leafletMap.on('mouseout', e => this._layers.forEach(layer => layer.movePointer('mouseout', e)));
this._leafletMap.on('mousedown', e => this._layers.forEach(layer => layer.movePointer('mousedown', e)));
this._leafletMap.on('mouseup', e => this._layers.forEach(layer => layer.movePointer('mouseup', e)));
this._leafletMap.on('draw:created', event => {
const drawType = event.layerType;
if (drawType === 'rectangle') {
const bounds = event.layer.getBounds();
const southEast = bounds.getSouthEast();
const northWest = bounds.getNorthWest();
let southEastLng = southEast.lng;
if (southEastLng > 180) {
southEastLng -= 360;
}
let northWestLng = northWest.lng;
if (northWestLng < -180) {
northWestLng += 360;
}
const southEastLat = southEast.lat;
const northWestLat = northWest.lat;
//Bounds cannot be created unless they form a box with larger than 0 dimensions
//Invalid areas are rejected by ES.
if (southEastLat === northWestLat || southEastLng === northWestLng) {
return;
}
this.emit('drawCreated:rectangle', {
bounds: {
bottom_right: {
lat: southEastLat,
lon: southEastLng
},
top_left: {
lat: northWestLat,
lon: northWestLng
}
}
});
} else if (drawType === 'polygon') {
const latLongs = event.layer.getLatLngs();
this.emit('drawCreated:polygon', {
points: latLongs.map(leafletLatLng => {
return {
lat: leafletLatLng.lat,
lon: leafletLatLng.lng
};
})
});
}
});
this.resize();
}
setShowTooltip(showTooltip) {
this._showTooltip = showTooltip;
}
getLayers() {
return this._layers.slice();
}
addLayer(kibanaLayer) {
this.emit('layers:invalidate');
const onshowTooltip = (event) => {
if (!this._showTooltip) {
return;
}
if (!this._popup) {
this._popup = L.popup({ autoPan: false });
this._popup.setLatLng(event.position);
this._popup.setContent(event.content);
this._popup.openOn(this._leafletMap);
} else {
if (!this._popup.getLatLng().equals(event.position)) {
this._popup.setLatLng(event.position);
}
if (this._popup.getContent() !== event.content) {
this._popup.setContent(event.content);
}
}
};
kibanaLayer.on('showTooltip', onshowTooltip);
this._listeners.push({ name: 'showTooltip', handle: onshowTooltip, layer: kibanaLayer });
const onHideTooltip = () => {
this._leafletMap.closePopup();
this._popup = null;
};
kibanaLayer.on('hideTooltip', onHideTooltip);
this._listeners.push({ name: 'hideTooltip', handle: onHideTooltip, layer: kibanaLayer });
const onStyleChanged = () => {
if (this._leafletLegendControl) {
this._leafletLegendControl.updateContents();
}
};
kibanaLayer.on('styleChanged', onStyleChanged);
this._listeners.push({ name: 'styleChanged', handle: onStyleChanged, layer: kibanaLayer });
this._layers.push(kibanaLayer);
kibanaLayer.addToLeafletMap(this._leafletMap);
this.emit('layers:update');
}
removeLayer(layer) {
const index = this._layers.indexOf(layer);
if (index >= 0) {
this._layers.splice(index, 1);
layer.removeFromLeafletMap(this._leafletMap);
}
this._listeners.forEach(listener => {
if (listener.layer === layer) {
listener.layer.removeListener(listener.name, listener.handle);
}
});
}
destroy() {
if (this._leafletFitControl) {
this._leafletMap.removeControl(this._leafletFitControl);
}
if (this._leafletDrawControl) {
this._leafletMap.removeControl(this._leafletDrawControl);
}
if (this._leafletLegendControl) {
this._leafletMap.removeControl(this._leafletLegendControl);
}
this.setBaseLayer(null);
for (const layer of this._layers) {
layer.removeFromLeafletMap(this._leafletMap);
}
this._leafletMap.remove();
this._containerNode.innerHTML = '';
this._listeners.forEach(listener => listener.layer.removeListener(listener.name, listener.handle));
}
getCenter() {
const center = this._leafletMap.getCenter();
return { lon: center.lng, lat: center.lat };
}
setCenter(latitude, longitude) {
const latLong = L.latLng(latitude, longitude);
if (latLong.equals && !latLong.equals(this._leafletMap.getCenter())) {
this._leafletMap.setView(latLong);
}
}
setZoomLevel(zoomLevel) {
if (this._leafletMap.getZoom() !== zoomLevel) {
this._leafletMap.setZoom(zoomLevel);
}
}
getZoomLevel() {
return this._leafletMap.getZoom();
}
getAutoPrecision() {
return zoomToPrecision(this._leafletMap.getZoom(), 12, this._leafletMap.getMaxZoom());
}
getBounds() {
const bounds = this._leafletMap.getBounds();
if (!bounds) {
return null;
}
const southEast = bounds.getSouthEast();
const northWest = bounds.getNorthWest();
let southEastLng = southEast.lng;
if (southEastLng > 180) {
southEastLng -= 360;
}
let northWestLng = northWest.lng;
if (northWestLng < -180) {
northWestLng += 360;
}
const southEastLat = southEast.lat;
const northWestLat = northWest.lat;
//Bounds cannot be created unless they form a box with larger than 0 dimensions
//Invalid areas are rejected by ES.
if (southEastLat === northWestLat || southEastLng === northWestLng) {
return;
}
return {
bottom_right: {
lat: southEastLat,
lon: southEastLng
},
top_left: {
lat: northWestLat,
lon: northWestLng
}
};
}
setDesaturateBaseLayer(isDesaturated) {
if (isDesaturated === this._baseLayerIsDesaturated) {
return;
}
this._baseLayerIsDesaturated = isDesaturated;
this._updateDesaturation();
this._leafletBaseLayer.redraw();
}
addDrawControl() {
const drawOptions = {
draw: {
polyline: false,
marker: false,
circle: false,
polygon: false,
rectangle: {
shapeOptions: {
stroke: false,
color: '#000'
}
}
}
};
this._leafletDrawControl = new L.Control.Draw(drawOptions);
this._leafletMap.addControl(this._leafletDrawControl);
}
addFitControl() {
if (this._leafletFitControl || !this._leafletMap) {
return;
}
const fitContainer = L.DomUtil.create('div', 'leaflet-control leaflet-bar leaflet-control-fit');
this._leafletFitControl = new FitControl(fitContainer, this);
this._leafletMap.addControl(this._leafletFitControl);
}
addLegendControl() {
if (this._leafletLegendControl || !this._leafletMap) {
return;
}
this._updateLegend();
}
setLegendPosition(position) {
this._legendPosition = position;
if (this._leafletLegendControl) {
this._leafletMap.removeControl(this._leafletLegendControl);
this._updateLegend();
}
}
_updateLegend() {
const $wrapper = $('<div>').addClass('tilemap-legend-wrapper');
this._leafletLegendControl = new LegendControl($wrapper, this, this._legendPosition);
this._leafletMap.addControl(this._leafletLegendControl);
}
resize() {
this._leafletMap.invalidateSize();
this._updateExtent();
}
setBaseLayer(settings) {
if (_.isEqual(settings, this._baseLayerSettings)) {
return;
}
this._baseLayerSettings = settings;
if (settings === null) {
if (this._leafletBaseLayer && this._leafletMap) {
this._leafletMap.removeLayer(this._leafletBaseLayer);
this._leafletBaseLayer = null;
}
return;
}
if (this._leafletBaseLayer) {
this._leafletMap.removeLayer(this._leafletBaseLayer);
this._leafletBaseLayer = null;
}
let baseLayer;
if (settings.baseLayerType === 'wms') {
baseLayer = this._getWMSBaseLayer(settings.options);
} else if (settings.baseLayerType === 'tms') {
baseLayer = this._getTMSBaseLayer((settings.options));
}
baseLayer.on('tileload', () => this._updateDesaturation());
baseLayer.on('load', () => { this.emit('baseLayer:loaded');});
baseLayer.on('loading', () => {this.emit('baseLayer:loading');});
this._leafletBaseLayer = baseLayer;
this._leafletBaseLayer.addTo(this._leafletMap);
this._leafletBaseLayer.bringToBack();
if (settings.options.minZoom > this._leafletMap.getZoom()) {
this._leafletMap.setZoom(settings.options.minZoom);
}
this.resize();
}
isInside(bucketRectBounds) {
const mapBounds = this._leafletMap.getBounds();
return mapBounds.intersects(bucketRectBounds);
}
fitToData() {
if (!this._leafletMap) {
return;
}
let bounds = null;
this._layers.forEach(layer => {
const leafletLayer = layer.getLeafletLayer();
const b = leafletLayer.getBounds();
if (bounds) {
bounds.extend(b);
} else {
bounds = b;
}
});
if (bounds) {
this._leafletMap.fitBounds(bounds);
}
}
_getTMSBaseLayer(options) {
return L.tileLayer(options.url, {
minZoom: options.minZoom,
maxZoom: options.maxZoom,
subdomains: options.subdomains || [],
attribution: options.attribution
});
}
_getWMSBaseLayer(options) {
return L.tileLayer.wms(options.url, {
attribution: options.attribution,
format: options.format,
layers: options.layers,
minZoom: options.minZoom,
maxZoom: options.maxZoom,
styles: options.styles,
transparent: options.transparent,
version: options.version
});
}
_updateExtent() {
this._layers.forEach(layer => layer.updateExtent());
}
_updateDesaturation() {
const tiles = $('img.leaflet-tile-loaded');
if (this._baseLayerIsDesaturated) {
tiles.removeClass('filters-off');
} else if (!this._baseLayerIsDesaturated) {
tiles.addClass('filters-off');
}
}
persistUiStateForVisualization(visualization) {
this.on('moveend', () => {
const uiState = visualization.getUiState();
const centerFromUIState = uiState.get('mapCenter');
const zoomFromUiState = parseInt(uiState.get('mapZoom'));
if (isNaN(zoomFromUiState) || this.getZoomLevel() !== zoomFromUiState) {
uiState.set('mapZoom', this.getZoomLevel());
}
const centerFromMap = this.getCenter();
if (!centerFromUIState || centerFromMap.lon !== centerFromUIState[1] || centerFromMap.lat !== centerFromUIState[0]) {
uiState.set('mapCenter', [centerFromMap.lat, centerFromMap.lon]);
}
});
}
useUiStateFromVisualization(visualization) {
const uiState = visualization.getUiState();
const zoomFromUiState = parseInt(uiState.get('mapZoom'));
const centerFromUIState = uiState.get('mapCenter');
if (!isNaN(zoomFromUiState)) {
this.setZoomLevel(zoomFromUiState);
}
if (centerFromUIState) {
this.setCenter(centerFromUIState[0], centerFromUIState[1]);
}
}
}
export default KibanaMap;

View file

@ -0,0 +1,40 @@
import { EventEmitter } from 'events';
export default class KibanaMapLayer extends EventEmitter {
constructor() {
super();
this._leafletLayer = null;
}
getLeafletLayer() {
return this._leafletLayer;
}
addToLeafletMap(leafletMap) {
this._leafletLayer.addTo(leafletMap);
}
removeFromLeafletMap(leafletMap) {
leafletMap.removeLayer(this._leafletLayer);
}
appendLegendContents() {
}
updateExtent() {
}
movePointer() {
}
getBounds() {
}
}

View file

@ -1,200 +0,0 @@
import d3 from 'd3';
import _ from 'lodash';
export default function DataFactory() {
/**
* Provides an API for pulling values off the data
* and calculating values using the data
*
* @class Data
* @constructor
* @param data {Object} Elasticsearch query results
* @param attr {Object|*} Visualization options
*/
class Data {
constructor(data, uiState) {
this.uiState = uiState;
this.data = this.copyDataObj(data);
this._normalizeOrdered();
}
copyDataObj(data) {
const copyChart = data => {
const newData = {};
Object.keys(data).forEach(key => {
if (key !== 'series') {
newData[key] = data[key];
} else {
newData[key] = data[key].map(seri => {
return {
label: seri.label,
values: seri.values.map(val => {
const newVal = _.clone(val);
newVal.aggConfig = val.aggConfig;
newVal.aggConfigResult = val.aggConfigResult;
newVal.extraMetrics = val.extraMetrics;
return newVal;
})
};
});
}
});
return newData;
};
if (!data.series) {
const newData = {};
Object.keys(data).forEach(key => {
if (!['rows', 'columns'].includes(key)) {
newData[key] = data[key];
}
else {
newData[key] = data[key].map(chart => {
return copyChart(chart);
});
}
});
return newData;
}
return copyChart(data);
}
/**
* Returns an array of the actual x and y data value objects
* from data with series keys
*
* @method chartData
* @returns {*} Array of data objects
*/
chartData() {
if (!this.data.series) {
const arr = this.data.rows ? this.data.rows : this.data.columns;
return _.toArray(arr);
}
return [this.data];
}
/**
* Returns an array of chart data objects
*
* @method getVisData
* @returns {*} Array of chart data objects
*/
getVisData() {
let visData;
if (this.data.rows) {
visData = this.data.rows;
} else if (this.data.columns) {
visData = this.data.columns;
} else {
visData = [this.data];
}
return visData;
}
/**
* get min and max for all cols, rows of data
*
* @method getMaxMin
* @return {Object}
*/
getGeoExtents() {
const 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 });
}
/**
* Get attributes off the data, e.g. `tooltipFormatter` or `xAxisFormatter`
* pulls the value off the first item in the array
* these values are typically the same between data objects of the same chart
* TODO: May need to verify this or refactor
*
* @method get
* @param thing {String} Data object key
* @returns {*} Data object value
*/
get(thing, def) {
const source = (this.data.rows || this.data.columns || [this.data])[0];
return _.get(source, thing, def);
}
/**
* Return an array of all value objects
* Pluck the data.series array from each data object
* Create an array of all the value objects from the series array
*
* @method flatten
* @returns {Array} Value objects
*/
flatten() {
return _(this.chartData())
.pluck('series')
.flattenDeep()
.pluck('values')
.flattenDeep()
.value();
}
/**
* ensure that the datas ordered property has a min and max
* if the data represents an ordered date range.
*
* @return {undefined}
*/
_normalizeOrdered() {
const data = this.getVisData();
const self = this;
data.forEach(function (d) {
if (!d.ordered || !d.ordered.date) return;
const missingMin = d.ordered.min == null;
const missingMax = d.ordered.max == null;
if (missingMax || missingMin) {
const extent = d3.extent(self.xValues());
if (missingMin) d.ordered.min = extent[0];
if (missingMax) d.ordered.max = extent[1];
}
});
}
/**
* Calculates min and max values for all map data
* series.rows is an array of arrays
* each row is an array of values
* last value in row array is bucket count
*
* @method mapDataExtents
* @param series {Array} Array of data objects
* @returns {Array} min and max values
*/
mapDataExtents(series) {
const values = _.map(series.rows, function (row) {
return row[row.length - 1];
});
return [_.min(values), _.max(values)];
}
/**
* Get the maximum number of series, considering each chart
* individually.
*
* @return {number} - the largest number of series from all charts
*/
maxNumberOfSeries() {
return this.chartData().reduce(function (max, chart) {
return Math.max(max, chart.series.length);
}, 0);
}
}
return Data;
}

View file

@ -1,299 +0,0 @@
import d3 from 'd3';
import _ from 'lodash';
import $ from 'jquery';
import SimpleEmitter from 'ui/utils/simple_emitter';
export default function DispatchClass(Private, config) {
/**
* Handles event responses
*
* @class Dispatch
* @constructor
* @param handler {Object} Reference to Handler Class Object
*/
class Dispatch extends SimpleEmitter {
constructor(handler) {
super();
this.handler = handler;
this._listeners = {};
}
/**
* Response to click and hover events
*
* @param d {Object} Data point
* @param i {Number} Index number of data point
* @returns {{value: *, point: *, label: *, color: *, pointIndex: *,
* series: *, config: *, data: (Object|*),
* e: (d3.event|*), handler: (Object|*)}} Event response object
*/
eventResponse(d, i) {
const datum = d._input || d;
const data = d3.event.target.nearestViewportElement ?
d3.event.target.nearestViewportElement.__data__ : d3.event.target.__data__;
const label = d.label ? d.label : (d.series || 'Count');
const isSeries = !!(data && data.series);
const isSlices = !!(data && data.slices);
const series = isSeries ? data.series : undefined;
const slices = isSlices ? data.slices : undefined;
const handler = this.handler;
const color = _.get(handler, 'data.color');
const isPercentage = (handler && handler.visConfig.get('mode', 'normal') === 'percentage');
const eventData = {
value: d.y,
point: datum,
datum: datum,
label: label,
color: color ? color(label) : undefined,
pointIndex: i,
series: series,
slices: slices,
config: handler && handler.visConfig,
data: data,
e: d3.event,
handler: handler
};
if (isSeries) {
// Find object with the actual d value and add it to the point object
const object = _.find(series, { 'label': label });
if (object) {
eventData.value = +object.values[i].y;
if (isPercentage) {
// Add the formatted percentage to the point object
eventData.percent = (100 * d.y).toFixed(1) + '%';
}
}
}
return eventData;
}
/**
* Returns a function that adds events and listeners to a D3 selection
*
* @method addEvent
* @param event {String}
* @param callback {Function}
* @returns {Function}
*/
addEvent(event, callback) {
return function (selection) {
selection.each(function () {
const element = d3.select(this);
if (typeof callback === 'function') {
return element.on(event, callback);
}
});
};
}
/**
*
* @method addHoverEvent
* @returns {Function}
*/
addHoverEvent() {
const self = this;
const isClickable = this.listenerCount('click') > 0;
const addEvent = this.addEvent;
const $el = this.handler.el;
if (!this.handler.highlight) {
this.handler.highlight = self.highlight;
}
function hover(d, i) {
// Add pointer if item is clickable
if (isClickable) {
self.addMousePointer.call(this, arguments);
}
self.handler.highlight.call(this, $el);
self.emit('hover', self.eventResponse(d, i));
}
return addEvent('mouseover', hover);
}
/**
*
* @method addMouseoutEvent
* @returns {Function}
*/
addMouseoutEvent() {
const self = this;
const addEvent = this.addEvent;
const $el = this.handler.el;
if (!this.handler.unHighlight) {
this.handler.unHighlight = self.unHighlight;
}
function mouseout() {
self.handler.unHighlight.call(this, $el);
}
return addEvent('mouseout', mouseout);
}
/**
*
* @method addClickEvent
* @returns {Function}
*/
addClickEvent() {
const self = this;
const addEvent = this.addEvent;
function click(d, i) {
self.emit('click', self.eventResponse(d, i));
}
return addEvent('click', click);
}
/**
* Determine if we will allow brushing
*
* @method allowBrushing
* @returns {Boolean}
*/
allowBrushing() {
const xAxis = this.handler.categoryAxes[0];
//Allow brushing for ordered axis - date histogram and histogram
return Boolean(xAxis.ordered);
}
/**
* Determine if brushing is currently enabled
*
* @method isBrushable
* @returns {Boolean}
*/
isBrushable() {
return this.allowBrushing() && this.listenerCount('brush') > 0;
}
/**
* Mouseover Behavior
*
* @method addMousePointer
* @returns {d3.Selection}
*/
addMousePointer() {
return d3.select(this).style('cursor', 'pointer');
}
/**
* Highlight the element that is under the cursor
* by reducing the opacity of all the elements on the graph.
* @param element {d3.Selection}
* @method highlight
*/
highlight(element) {
const label = this.getAttribute('data-label');
if (!label) return;
const dimming = config.get('visualization:dimmingOpacity');
$(element).parent().find('[data-label]')
.css('opacity', 1)//Opacity 1 is needed to avoid the css application
.not((els, el) => String($(el).data('label')) === label)
.css('opacity', justifyOpacity(dimming));
}
/**
* Mouseout Behavior
*
* @param element {d3.Selection}
* @method unHighlight
*/
unHighlight(element) {
$('[data-label]', element.parentNode).css('opacity', 1);
}
/**
* Adds D3 brush to SVG and returns the brush function
*
* @param xScale {Function} D3 xScale function
* @param svg {HTMLElement} Reference to SVG
* @returns {*} Returns a D3 brush function and a SVG with a brush group attached
*/
createBrush(xScale, svg) {
const self = this;
const visConfig = self.handler.visConfig;
const { width, height } = svg.node().getBBox();
const isHorizontal = self.handler.categoryAxes[0].axisConfig.isHorizontal();
// Brush scale
const brush = d3.svg.brush();
if (isHorizontal) {
brush.x(xScale);
} else {
brush.y(xScale);
}
brush.on('brushend', function brushEnd() {
// Assumes data is selected at the chart level
// In this case, the number of data objects should always be 1
const data = d3.select(this).data()[0];
const isTimeSeries = (data.ordered && data.ordered.date);
// Allows for brushing on d3.scale.ordinal()
const selected = xScale.domain().filter(function (d) {
return (brush.extent()[0] <= xScale(d)) && (xScale(d) <= brush.extent()[1]);
});
const range = isTimeSeries ? brush.extent() : selected;
return self.emit('brush', {
range: range,
config: visConfig,
e: d3.event,
data: data
});
});
// if `addBrushing` is true, add brush canvas
if (self.listenerCount('brush')) {
const rect = svg.insert('g', 'g')
.attr('class', 'brush')
.call(brush)
.call(function (brushG) {
// hijack the brush start event to filter out right/middle clicks
const brushHandler = brushG.on('mousedown.brush');
if (!brushHandler) return; // touch events in use
brushG.on('mousedown.brush', function () {
if (validBrushClick(d3.event)) brushHandler.apply(this, arguments);
});
})
.selectAll('rect');
if (isHorizontal) {
rect.attr('height', height);
} else {
rect.attr('width', width);
}
return brush;
}
}
}
function validBrushClick(event) {
return event.button === 0;
}
function justifyOpacity(opacity) {
const decimalNumber = parseFloat(opacity, 10);
const fallbackOpacity = 0.5;
return (0 <= decimalNumber && decimalNumber <= 1) ? decimalNumber : fallbackOpacity;
}
return Dispatch;
}

View file

@ -1,50 +0,0 @@
import d3 from 'd3';
import MapSplitProvider from './splits/map_split';
export default function LayoutFactory(Private) {
const mapSplit = Private(MapSplitProvider);
class Layout {
constructor(el, config, data) {
this.el = el;
this.config = config;
this.data = data;
}
render() {
this.removeAll();
this.createLayout();
}
createLayout() {
const wrapper = this.appendElem(this.el, 'div', 'vis-wrapper');
wrapper.datum(this.data.data);
const colWrapper = this.appendElem(wrapper.node(), 'div', 'vis-col-wrapper');
const chartWrapper = this.appendElem(colWrapper.node(), 'div', 'chart-wrapper');
chartWrapper.call(mapSplit, colWrapper.node(), this.config);
}
appendElem(el, type, className) {
if (!el || !type || !className) {
throw new Error('Function requires that an el, type, and class be provided');
}
if (typeof el === 'string') {
// Create a DOM reference with a d3 selection
// Need to make sure that the `el` is bound to this object
// to prevent it from being appended to another Layout
el = d3.select(this.el)
.select(el)[0][0];
}
return d3.select(el)
.append(type)
.attr('class', className);
}
removeAll() {
return d3.select(this.el).selectAll('*').remove();
}
}
return Layout;
}

View file

@ -1,38 +0,0 @@
/**
* Provides vislib configuration, throws error if invalid property is accessed without providing defaults
*/
import _ from 'lodash';
export default function MapsConfigFactory() {
const DEFAULT_VIS_CONFIG = {
style: {
margin : { top: 10, right: 3, bottom: 5, left: 3 }
},
alerts: {},
categoryAxes: [],
valueAxes: []
};
class MapsConfig {
constructor(mapsConfigArgs) {
this._values = _.defaultsDeep({}, mapsConfigArgs, DEFAULT_VIS_CONFIG);
}
get(property, defaults) {
if (_.has(this._values, property) || typeof defaults !== 'undefined') {
return _.get(this._values, property, defaults);
} else {
throw new Error(`Accessing invalid config property: ${property}`);
return defaults;
}
}
set(property, value) {
return _.set(this._values, property, value);
}
}
return MapsConfig;
}

View file

@ -1,52 +0,0 @@
import d3 from 'd3';
define(function () {
return function ChartSplitFactory() {
/*
* Adds div DOM elements to the `.chart-wrapper` element based on the data layout.
* For example, if the data has rows, it returns the same number of
* `.chart` elements as row objects.
*/
return function split(selection) {
selection.each(function (data) {
const div = d3.select(this)
.attr('class', function () {
// Determine the parent class
if (data.rows) {
return 'chart-wrapper-row';
} else if (data.columns) {
return 'chart-wrapper-column';
} else {
return 'chart-wrapper';
}
});
let divClass;
const charts = div.selectAll('charts')
.append('div')
.data(function (d) {
// Determine the child class
if (d.rows) {
divClass = 'chart-row';
return d.rows;
} else if (d.columns) {
divClass = 'chart-column';
return d.columns;
} else {
divClass = 'chart';
return [d];
}
})
.enter()
.append('div')
.attr('class', function () {
return divClass;
});
if (!data.geoJson) {
charts.call(split);
}
});
};
};
});

View file

@ -1,7 +1,6 @@
import uiModules from 'ui/modules';
import _ from 'lodash';
import marked from 'marked';
import uiRoutes from 'ui/routes';
import { modifyUrl } from 'ui/url';
marked.setOptions({
@ -9,16 +8,6 @@ marked.setOptions({
sanitize: true // Sanitize HTML tags
});
/**
* Reloads the setting for each route,
* This is to ensure, that if the license changed during the lifecycle of the application,
* we get an update.
* tilemapSettings itself will take care that the manifest-service is not queried when not necessary.
*/
uiRoutes.afterSetupWork(function (tilemapSettings) {
return tilemapSettings.loadSettings();
});
uiModules.get('kibana')
.service('tilemapSettings', function ($http, tilemapsConfig, $sanitize, kbnVersion) {
const attributionFromConfig = $sanitize(marked(tilemapsConfig.deprecated.config.options.attribution || ''));
@ -190,6 +179,10 @@ uiModules.get('kibana')
}
isInitialized() {
return this._settingsInitialized;
}
/**
* Checks if there was an error during initialization of the parameters

View file

@ -1,123 +0,0 @@
import _ from 'lodash';
import $ from 'jquery';
import d3 from 'd3';
import MapsConfigProvider from './lib/maps_config';
import TileMapChartProvider from './visualizations/tile_map';
import EventsProvider from 'ui/events';
import MapsDataProvider from './lib/data';
import LayoutProvider from './lib/layout';
import './styles/_tilemap.less';
export default function MapsFactory(Private) {
const Events = Private(EventsProvider);
const MapsConfig = Private(MapsConfigProvider);
const TileMapChart = Private(TileMapChartProvider);
const Data = Private(MapsDataProvider);
const Layout = Private(LayoutProvider);
class Maps extends Events {
constructor($el, vis, mapsConfigArgs) {
super(arguments);
this.el = $el.get ? $el.get(0) : $el;
this.vis = vis;
this.mapsConfigArgs = mapsConfigArgs;
// memoize so that the same function is returned every time,
// allowing us to remove/re-add the same function
this.getProxyHandler = _.memoize(function (event) {
const self = this;
return function (e) {
self.emit(event, e);
};
});
this.enable = this.chartEventProxyToggle('on');
this.disable = this.chartEventProxyToggle('off');
}
chartEventProxyToggle(method) {
return function (event, chart) {
const proxyHandler = this.getProxyHandler(event);
_.each(chart ? [chart] : this.charts, function (chart) {
chart.events[method](event, proxyHandler);
});
};
}
on(event, listener) {
const first = this.listenerCount(event) === 0;
const ret = Events.prototype.on.call(this, event, listener);
const added = this.listenerCount(event) > 0;
// if this is the first listener added for the event
// enable the event in the handler
if (first && added && this.handler) this.handler.enable(event);
return ret;
}
off(event, listener) {
const last = this.listenerCount(event) === 1;
const ret = Events.prototype.off.call(this, event, listener);
const removed = this.listenerCount(event) === 0;
// Once all listeners are removed, disable the events in the handler
if (last && removed && this.handler) this.handler.disable(event);
return ret;
}
render(data, uiState) {
if (!data) {
throw new Error('No valid data!');
}
this.uiState = uiState;
this.data = new Data(data, this.uiState);
this.visConfig = new MapsConfig(this.mapsConfigArgs, this.data, this.uiState);
this.layout = new Layout(this.el, this.visConfig, this.data);
this.draw();
}
destroy() {
this.charts.forEach(chart => chart.destroy());
d3.select(this.el).selectAll('*').remove();
}
draw() {
// Destroy the charts before they get removed from the DOM on the new
// layout render.
if(this.charts !== undefined) {
this.charts.forEach(chart => chart.destroy());
}
this.layout.render();
const self = this;
this.charts = [];
let loadedCount = 0;
const chartSelection = d3.select(this.el).selectAll('.chart');
chartSelection.each(function (chartData) {
const chart = new TileMapChart(self, this, chartData);
self.activeEvents().forEach(function (event) {
self.enable(event, chart);
});
self.charts.push(chart);
chart.render();
chart.events.on('rendered', function () {
loadedCount++;
if (loadedCount === chartSelection.length) {
$(self.el).trigger('renderComplete');
}
});
});
}
}
return Maps;
}

View file

@ -1,86 +1,237 @@
import $ from 'jquery';
import _ from 'lodash';
import MapsProvider from 'ui/vis_maps/maps';
import VisRenderbotProvider from 'ui/vis/renderbot';
import MapsVisTypeBuildChartDataProvider from 'ui/vislib_vis_type/build_chart_data';
import FilterBarPushFilterProvider from 'ui/filter_bar/push_filter';
import KibanaMap from './kibana_map';
import GeohashLayer from './geohash_layer';
import './lib/tilemap_settings';
import './styles/_tilemap.less';
import { ResizeCheckerProvider } from 'ui/resize_checker';
module.exports = function MapsRenderbotFactory(Private, $injector, tilemapSettings, Notifier) {
const AngularPromise = $injector.get('Promise');
const Maps = Private(MapsProvider);
module.exports = function MapsRenderbotFactory(Private, $injector, tilemapSettings, Notifier, courier, getAppState) {
const ResizeChecker = Private(ResizeCheckerProvider);
const Renderbot = Private(VisRenderbotProvider);
const buildChartData = Private(MapsVisTypeBuildChartDataProvider);
const notify = new Notifier({
location: 'Tilemap'
});
const notify = new Notifier({ location: 'Tilemap' });
class MapsRenderbot extends Renderbot {
constructor(vis, $el, uiState) {
super(vis, $el, uiState);
this._buildChartData = buildChartData.bind(this);
this._geohashLayer = null;
this._kibanaMap = null;
this._kibanaMapReady = this._makeKibanaMap($el);
this._baseLayerDirty = true;
this._dataDirty = true;
this._paramsDirty = true;
this._resizeChecker = new ResizeChecker($el);
this._resizeChecker.on('resize', () => {
if (this._kibanaMap) {
this._kibanaMap.resize();
}
});
}
async _makeKibanaMap($el) {
if (!tilemapSettings.isInitialized()) {
await tilemapSettings.loadSettings();
}
if (tilemapSettings.getError()) {
//Still allow the visualization to be built, but show a toast that there was a problem retrieving map settings
//Even though the basemap will not display, the user will at least still see the overlay data
notify.warning(tilemapSettings.getError().message);
}
const containerElement = $($el)[0];
const minMaxZoom = tilemapSettings.getMinMaxZoom(false);
this._kibanaMap = new KibanaMap(containerElement, minMaxZoom);
this._kibanaMap.addDrawControl();
this._kibanaMap.addFitControl();
this._kibanaMap.addLegendControl();
this._kibanaMap.persistUiStateForVisualization(this.vis);
this._kibanaMap.useUiStateFromVisualization(this.vis);
let previousPrecision = this._kibanaMap.getAutoPrecision();
let precisionChange = false;
this._kibanaMap.on('zoomchange', () => {
precisionChange = (previousPrecision !== this._kibanaMap.getAutoPrecision());
previousPrecision = this._kibanaMap.getAutoPrecision();
});
this._kibanaMap.on('zoomend', () => {
const isAutoPrecision = _.get(this._chartData, 'geohashGridAgg.params.autoPrecision', true);
if (!isAutoPrecision) {
return;
}
this._dataDirty = true;
if (precisionChange) {
courier.fetch();
} else {
this._recreateGeohashLayer();
this._dataDirty = false;
this._doRenderComplete();
}
});
this._kibanaMap.on('drawCreated:rectangle', event => {
addSpatialFilter(_.get(this._chartData, 'geohashGridAgg'), 'geo_bounding_box', event.bounds);
});
this._kibanaMap.on('baseLayer:loaded', () => {
this._baseLayerDirty = false;
this._doRenderComplete();
});
this._kibanaMap.on('baseLayer:loading', () => {
this._baseLayerDirty = true;
});
}
_recreateGeohashLayer() {
if (this._geohashLayer) {
this._kibanaMap.removeLayer(this._geohashLayer);
}
if (!this._geohashGeoJson) {
return;
}
const geohashOptions = this._getGeohashOptions();
this._geohashLayer = new GeohashLayer(this._chartData.geoJson, geohashOptions, this._kibanaMap.getZoomLevel(), this._kibanaMap);
this._kibanaMap.addLayer(this._geohashLayer);
}
/**
* called on data change
* @param esResponse
*/
render(esResponse) {
this._dataDirty = true;
this._kibanaMapReady.then(() => {
this._chartData = this._buildChartData(esResponse);
this._geohashGeoJson = this._chartData.geoJson;
this._recreateGeohashLayer();
this._kibanaMap.useUiStateFromVisualization(this.vis);
this._kibanaMap.resize();
this._dataDirty = false;
this._doRenderComplete();
});
}
destroy() {
if (this._kibanaMap) {
this._kibanaMap.destroy();
}
}
/**
* called on options change (vis.params change)
*/
updateParams() {
this._paramsDirty = true;
this._kibanaMapReady.then(() => {
const mapParams = this._getMapsParams();
if (mapParams.wms.enabled) {
const { minZoom, maxZoom } = tilemapSettings.getMinMaxZoom(true);
this._kibanaMap.setBaseLayer({
baseLayerType: 'wms',
options: {
minZoom: minZoom,
maxZoom: maxZoom,
url: mapParams.wms.url,
...mapParams.wms.options
}
});
} else {
if (!tilemapSettings.hasError()) {
const url = tilemapSettings.getUrl();
const options = tilemapSettings.getTMSOptions();
this._kibanaMap.setBaseLayer({
baseLayerType: 'tms',
options: { url, ...options }
});
}
}
const geohashOptions = this._getGeohashOptions();
if (!this._geohashLayer || !this._geohashLayer.isReusable(geohashOptions)) {
this._recreateGeohashLayer();
}
this._kibanaMap.setDesaturateBaseLayer(mapParams.isDesaturated);
this._kibanaMap.setShowTooltip(mapParams.addTooltip);
this._kibanaMap.setLegendPosition(mapParams.legendPosition);
this._kibanaMap.useUiStateFromVisualization(this.vis);
this._kibanaMap.resize();
this._paramsDirty = false;
this._doRenderComplete();
});
}
_getMapsParams() {
return _.assign(
{},
this.vis.type.params.defaults,
{
type: this.vis.type.name,
hasTimeField: this.vis.indexPattern && this.vis.indexPattern.hasTimeField()// Add attribute which determines whether an index is time based or not.
},
this.vis.params
);
}
_getGeohashOptions() {
const newParams = this._getMapsParams();
return {
valueFormatter: this._chartData ? this._chartData.valueFormatter : null,
tooltipFormatter: this._chartData ? this._chartData.tooltipFormatter : null,
mapType: newParams.mapType,
heatmap: {
heatBlur: newParams.heatBlur,
heatMaxZoom: newParams.heatMaxZoom,
heatMinOpacity: newParams.heatMinOpacity,
heatNormalizeData: newParams.heatNormalizeData,
heatRadius: newParams.heatRadius
}
};
}
_doRenderComplete() {
if (this._paramsDirty || this._dataDirty || this._baseLayerDirty) {
return;
}
$(this.el).trigger('renderComplete');
}
_.class(MapsRenderbot).inherits(Renderbot);
function MapsRenderbot(vis, $el, uiState) {
MapsRenderbot.Super.call(this, vis, $el, uiState);
this._createVis();
}
MapsRenderbot.prototype._createVis = function () {
if (tilemapSettings.getError()) {
//Still allow the visualization to be build, but show a toast that there was a problem retrieving map settings
//Even though the basemap will not display, the user will at least still see the overlay data
notify.warning(tilemapSettings.getError().message);
function addSpatialFilter(agg, filterName, filterData) {
if (!agg) {
return;
}
if (this.mapsVis) this.destroy();
this.mapsParams = this._getMapsParams();
this.mapsVis = new Maps(this.$el[0], this.vis, this.mapsParams);
_.each(this.vis.listeners, (listener, event) => {
this.mapsVis.on(event, listener);
});
const indexPatternName = agg.vis.indexPattern.id;
const field = agg.fieldName();
const filter = {};
filter[filterName] = {};
filter[filterName][field] = filterData;
if (this.mapsData) {
this.mapsVis.render(this.mapsData, this.uiState);
}
};
const putFilter = Private(FilterBarPushFilterProvider)(getAppState());
return putFilter(filter, false, indexPatternName);
}
MapsRenderbot.prototype._getMapsParams = function () {
const self = this;
return _.assign(
{},
self.vis.type.params.defaults,
{
type: self.vis.type.name,
// Add attribute which determines whether an index is time based or not.
hasTimeField: self.vis.indexPattern && self.vis.indexPattern.hasTimeField()
},
self.vis.params
);
};
MapsRenderbot.prototype.buildChartData = buildChartData;
MapsRenderbot.prototype.render = function (esResponse) {
this.mapsData = this.buildChartData(esResponse);
return AngularPromise.delay(1).then(() => {
this.mapsVis.render(this.mapsData, this.uiState);
});
};
MapsRenderbot.prototype.destroy = function () {
const self = this;
const mapsVis = self.mapsVis;
_.forOwn(self.vis.listeners, function (listener, event) {
mapsVis.off(event, listener);
});
mapsVis.destroy();
};
MapsRenderbot.prototype.updateParams = function () {
const self = this;
// get full maps params object
const newParams = self._getMapsParams();
// if there's been a change, replace the vis
if (!_.isEqual(newParams, self.mapsParams)) self._createVis();
};
return MapsRenderbot;
};

View file

@ -0,0 +1,17 @@
import L from 'leaflet';
import ScaledCircles from './scaled_circles';
export default class GeohashGrid extends ScaledCircles {
getMarkerFunction() {
return function (feature) {
const geohashRect = feature.properties.rectangle;
// get bounds from northEast[3] and southWest[1]
// corners in geohash rectangle
const corners = [
[geohashRect[3][0], geohashRect[3][1]],
[geohashRect[1][0], geohashRect[1][1]]
];
return L.rectangle(corners);
};
}
}

View file

@ -0,0 +1,197 @@
import L from 'leaflet';
import _ from 'lodash';
import d3 from 'd3';
import { EventEmitter } from 'events';
/**
* Map overlay: canvas layer with leaflet.heat plugin
*
* @param map {Leaflet Object}
* @param geoJson {geoJson Object}
* @param params {Object}
*/
export default class Heatmap extends EventEmitter {
constructor(featureCollection, options, zoom) {
super();
this._geojsonFeatureCollection = featureCollection;
const max = _.get(featureCollection, 'properties.max');
const points = dataToHeatArray(max, options.heatNormalizeData, featureCollection);
this._leafletLayer = L.heatLayer(points, options);
this._tooltipFormatter = options.tooltipFormatter;
this._zoom = zoom;
this._disableTooltips = false;
this._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(',');
});
this._currentFeature = null;
this._addTooltips();
}
getBounds() {
return this._leafletLayer.getBounds();
}
getLeafletLayer() {
return this._leafletLayer;
}
appendLegendContents() {
}
movePointer(type, event) {
if (type === 'mousemove') {
this._deboundsMoveMoveLocation(event);
} else if (type === 'mouseout') {
this.emit('hideTooltip');
} else if (type === 'mousedown') {
this._disableTooltips = true;
this.emit('hideTooltip');
} else if (type === 'mouseup') {
this._disableTooltips = false;
}
}
_addTooltips() {
const mouseMoveLocation = (e) => {
if (!this._geojsonFeatureCollection.features.length || this._disableTooltips) {
this.emit('hideTooltip');
return;
}
const feature = this._nearestFeature(e.latlng);
if (this._tooltipProximity(e.latlng, feature)) {
const content = this._tooltipFormatter(feature);
if (!content) {
return;
}
this.emit('showTooltip', {
content: content,
position: e.latlng
});
} else { this.emit('hideTooltip');
}
};
this._deboundsMoveMoveLocation = _.debounce(mouseMoveLocation.bind(this), 15, {
'leading': true,
'trailing': false
});
}
/**
* Finds nearest feature in mapData to event latlng
*
* @method _nearestFeature
* @param latLng {Leaflet latLng}
* @return nearestPoint {Leaflet latLng}
*/
_nearestFeature(latLng) {
const self = this;
let nearest;
if (latLng.lng < -180 || latLng.lng > 180) {
return;
}
_.reduce(this._geojsonFeatureCollection.features, function (distance, feature) {
const featureLatLng = self._getLatLng(feature);
const 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}
*/
_tooltipProximity(latlng, feature) {
if (!feature) return;
let showTip = false;
const 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
const zoomScale = d3.scale.linear()
.domain([1, 4, 7, 10, 13, 16, 18])
.range([1000000, 300000, 100000, 15000, 2000, 150, 50]);
const proximity = zoomScale(this._zoom);
const distance = latlng.distanceTo(featureLatLng);
// maxLngDif is max difference in longitudes
// to prevent feature tooltip from appearing 360°
// away from event latlng
const maxLngDif = 40;
const lngDif = Math.abs(latlng.lng - featureLatLng.lng);
if (distance < proximity && lngDif < maxLngDif) {
showTip = true;
}
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}
*/
function dataToHeatArray(max, heatNormalizeData, featureCollection) {
return featureCollection.features.map((feature) => {
const lat = feature.geometry.coordinates[1];
const lng = feature.geometry.coordinates[0];
let heatIntensity;
if (!heatNormalizeData) {
// show bucket value on heatmap
heatIntensity = feature.properties.value;
} else {
// show bucket value normalized to max value
heatIntensity = feature.properties.value / max;
}
return [lat, lng, heatIntensity];
});
}

View file

@ -0,0 +1,231 @@
import L from 'leaflet';
import _ from 'lodash';
import d3 from 'd3';
import $ from 'jquery';
import { EventEmitter } from 'events';
export default class ScaledCircles extends EventEmitter {
constructor(featureCollection, options, targetZoom, kibanaMap) {
super();
this._geohashGeoJson = featureCollection;
this._zoom = targetZoom;
this._valueFormatter = options.valueFormatter;
this._tooltipFormatter = options.tooltipFormatter;
this._map = options.map;
this._legendColors = null;
this._legendQuantizer = null;
this._popups = [];
this._leafletLayer = L.geoJson(null, {
pointToLayer: this.getMarkerFunction(),
style: this.getStyleFunction(),
onEachFeature: (feature, layer) => {
this._bindPopup(feature, layer);
},
filter: (feature) => {
const bucketRectBounds = _.get(feature, 'properties.rectangle');
return kibanaMap.isInside(bucketRectBounds);
}
});
this._leafletLayer.addData(this._geohashGeoJson);
}
getLeafletLayer() {
return this._leafletLayer;
}
getStyleFunction() {
const min = _.get(this._geohashGeoJson, 'properties.min', 0);
const max = _.get(this._geohashGeoJson, 'properties.max', 1);
const quantizeDomain = (min !== max) ? [min, max] : d3.scale.quantize().domain();
this._legendColors = makeCircleMarkerLegendColors(min, max);
this._legendQuantizer = d3.scale.quantize().domain(quantizeDomain).range(this._legendColors);
return makeStyleFunction(min, max, this._legendColors, quantizeDomain);
}
movePointer() {
}
getLabel() {
if (this._popups.length) {
return this._popups[0].feature.properties.aggConfigResult.aggConfig.makeLabel();
}
return '';
}
appendLegendContents(jqueryDiv) {
if (!this._legendColors || !this._legendQuantizer) {
return;
}
const titleText = this.getLabel();
const $title = $('<div>').addClass('tilemap-legend-title').text(titleText);
jqueryDiv.append($title);
this._legendColors.forEach((color) => {
const labelText = this._legendQuantizer
.invertExtent(color)
.map(this._valueFormatter)
.join('  ');
const label = $('<div>');
const icon = $('<i>').css({
background: color,
'border-color': makeColorDarker(color)
});
const text = $('<span>').text(labelText);
label.append(icon);
label.append(text);
jqueryDiv.append(label);
});
}
/**
* Binds popup and events to each feature on map
*
* @method bindPopup
* @param feature {Object}
* @param layer {Object}
* return {undefined}
*/
_bindPopup(feature, layer) {
const popup = layer.on({
mouseover: (e) => {
const layer = e.target;
// bring layer to front if not older browser
if (!L.Browser.ie && !L.Browser.opera) {
layer.bringToFront();
}
this._showTooltip(feature);
},
mouseout: () => {
this.emit('hideTooltip');
}
});
this._popups.push(popup);
}
/**
* 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
*/
_showTooltip(feature, latLng) {
const lat = _.get(feature, 'geometry.coordinates.1');
const lng = _.get(feature, 'geometry.coordinates.0');
latLng = latLng || L.latLng(lat, lng);
const content = this._tooltipFormatter(feature);
if (!content) {
return;
}
this.emit('showTooltip', {
content: content,
position: latLng
});
}
getMarkerFunction() {
const scaleFactor = 0.6;
return (feature, latlng) => {
const value = feature.properties.value;
const scaledRadius = this._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}
*/
_radiusScale(value) {
//magic numbers
const precisionBiasBase = 5;
const precisionBiasNumerator = 200;
const precision = _.max(this._geohashGeoJson.features.map((feature) => {
return String(feature.properties.geohash).length;
}));
const pct = Math.abs(value) / Math.abs(this._geohashGeoJson.properties.max);
const zoomRadius = 0.5 * Math.pow(2, this._zoom);
const precisionScale = precisionBiasNumerator / Math.pow(precisionBiasBase, precision);
// square root value percentage
return Math.pow(pct, 0.5) * zoomRadius * precisionScale;
}
getBounds() {
return this._leafletLayer.getBounds();
}
}
/**
* d3 quantize scale returns a hex color, used for marker fill color
*
* @method quantizeLegendColors
* return {undefined}
*/
function makeCircleMarkerLegendColors(min, max) {
const reds1 = ['#ff6128'];
const reds3 = ['#fecc5c', '#fd8d3c', '#e31a1c'];
const reds5 = ['#fed976', '#feb24c', '#fd8d3c', '#f03b20', '#bd0026'];
const bottomCutoff = 2;
const middleCutoff = 24;
let legendColors;
if (max - min <= bottomCutoff) {
legendColors = reds1;
} else if (max - min <= middleCutoff) {
legendColors = reds3;
} else {
legendColors = reds5;
}
return legendColors;
}
function makeColorDarker(color) {
const amount = 1.3;//magic number, carry over from earlier
return d3.hcl(color).darker(amount).toString();
}
function makeStyleFunction(min, max, legendColors, quantizeDomain) {
const legendQuantizer = d3.scale.quantize().domain(quantizeDomain).range(legendColors);
return (feature) => {
const value = _.get(feature, 'properties.value');
const color = legendQuantizer(value);
return {
fillColor: color,
color: makeColorDarker(color),
weight: 1.5,
opacity: 1,
fillOpacity: 0.75
};
};
}

View file

@ -0,0 +1,46 @@
import L from 'leaflet';
import _ from 'lodash';
import ScaledCircles from './scaled_circles';
export default class ShadedCircles extends ScaledCircles {
getMarkerFunction() {
// multiplier to reduce size of all circles
const scaleFactor = 0.8;
return (feature, latlng) => {
const radius = this._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}
*/
_geohashMinDistance(feature) {
const centerPoint = _.get(feature, 'properties.center');
const 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
const east = L.latLng([centerPoint[0], geohashRect[2][1]]);
// southwest lat and center lng
const north = L.latLng([geohashRect[3][0], centerPoint[1]]);
// get latLng of geohash center point
const center = L.latLng([centerPoint[0], centerPoint[1]]);
// get smallest radius at center of geohash grid rectangle
const eastRadius = Math.floor(center.distanceTo(east));
const northRadius = Math.floor(center.distanceTo(north));
return _.min([eastRadius, northRadius]);
}
}

View file

@ -1,59 +0,0 @@
import d3 from 'd3';
import VislibLibDispatchProvider from '../lib/dispatch';
import TooltipProvider from 'ui/vis/components/tooltip';
export default function ChartBaseClass(Private) {
const Dispatch = Private(VislibLibDispatchProvider);
const Tooltip = Private(TooltipProvider);
/**
* The Base Class for all visualizations.
*
* @class Chart
* @constructor
* @param handler {Object} Reference to the Handler Class Constructor
* @param el {HTMLElement} HTML element to which the chart will be appended
* @param chartData {Object} Elasticsearch query results for this specific chart
*/
class Chart {
constructor(handler, el, chartData) {
this.handler = handler;
this.chartEl = el;
this.chartData = chartData;
this.tooltips = [];
const events = this.events = new Dispatch(handler);
if (this.handler.visConfig && this.handler.visConfig.get('addTooltip', false)) {
const $el = this.handler.el;
const formatter = this.handler.data.get('tooltipFormatter');
// Add tooltip
this.tooltip = new Tooltip('chart', $el, formatter, events);
this.tooltips.push(this.tooltip);
}
}
render() {
const selection = d3.select(this.chartEl);
selection.selectAll('*').remove();
selection.call(this.draw());
}
/**
* Removes all DOM elements from the root element
*
* @method destroy
*/
destroy() {
const selection = d3.select(this.chartEl);
this.events.removeAllListeners();
this.tooltips.forEach(function (tooltip) {
tooltip.destroy();
});
selection.remove();
}
}
return Chart;
}

View file

@ -1,362 +0,0 @@
import _ from 'lodash';
import $ from 'jquery';
import L from 'leaflet';
import VislibVisualizationsMarkerTypesScaledCirclesProvider from './marker_types/scaled_circles';
import VislibVisualizationsMarkerTypesShadedCirclesProvider from './marker_types/shaded_circles';
import VislibVisualizationsMarkerTypesGeohashGridProvider from './marker_types/geohash_grid';
import VislibVisualizationsMarkerTypesHeatmapProvider from './marker_types/heatmap';
import '../lib/tilemap_settings';
export default function MapFactory(Private, tilemapSettings) {
const defaultMapZoom = 2;
const defaultMapCenter = [15, 5];
const defaultMarkerType = 'Scaled Circle Markers';
const markerTypes = {
'Scaled Circle Markers': Private(VislibVisualizationsMarkerTypesScaledCirclesProvider),
'Shaded Circle Markers': Private(VislibVisualizationsMarkerTypesShadedCirclesProvider),
'Shaded Geohash Grid': Private(VislibVisualizationsMarkerTypesGeohashGridProvider),
'Heatmap': Private(VislibVisualizationsMarkerTypesHeatmapProvider),
};
/**
* 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
*/
class TileMapMap {
constructor(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;
this._geoJson = _.get(this._chartData, 'geoJson');
this._attr = params.attr || {};
const { minZoom, maxZoom } = tilemapSettings.getMinMaxZoom(this._isWMSEnabled());
const zoom = typeof params.zoom === 'number' ? params.zoom : defaultMapZoom;
this._mapZoom = Math.max(Math.min(zoom, maxZoom), minZoom);
this._mapCenter = params.center || defaultMapCenter;
this._createMap();
}
addBoundingControl() {
if (this._boundingControl) return;
const self = this;
const 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);
}
addFitControl() {
if (this._fitControl) return;
const self = this;
const fitContainer = L.DomUtil.create('div', 'leaflet-control leaflet-bar leaflet-control-fit');
// Add button to fit container to points
const FitControl = L.Control.extend({
options: {
position: 'topleft'
},
onAdd: function () {
$(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 () {
$(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}
*/
addTitle(mapLabel) {
if (this._label) return;
const 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
*/
saturateTiles() {
if (!this._attr.isDesaturated) {
$('img.leaflet-tile-loaded').addClass('filters-off');
}
}
updateSize() {
this.map.invalidateSize({
debounceMoveend: true
});
}
destroy() {
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
*/
_addMarkers() {
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
*/
_createMarkers(options) {
const MarkerType = markerTypes[this._markerType];
return new MarkerType(this.map, this._geoJson, options);
}
_attachEvents() {
const self = this;
const saturateTiles = self.saturateTiles.bind(self);
this._tileLayer.on('tileload', saturateTiles);
this._tileLayer.on('load', () => {
if (!self._events) {
return;
}
self._events.emit('rendered', {
chart: self._chartData,
map: self.map,
center: self._mapCenter,
zoom: self._mapZoom,
});
});
this.map.on('unload', function () {
self._tileLayer.off('tileload', saturateTiles);
});
this.map.on('moveend', function setZoomCenter() {
if (!self.map) return;
// update internal center and zoom references
const uglyCenter = self.map.getCenter();
self._mapCenter = [uglyCenter.lat, uglyCenter.lng];
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) {
const drawType = e.layerType;
if (!self._events || !self._events.listenerCount(drawType)) return;
// TODO: Different drawTypes need differ info. Need a switch on the object creation
const bounds = e.layer.getBounds();
const southEast = bounds.getSouthEast();
const northWest = bounds.getNorthWest();
let southEastLng = southEast.lng;
if (southEastLng > 180) {
southEastLng -= 360;
}
let northWestLng = northWest.lng;
if (northWestLng < -180) {
northWestLng += 360;
}
const southEastLat = southEast.lat;
const northWestLat = northWest.lat;
//Bounds cannot be created unless they form a box with larger than 0 dimensions
//Invalid areas are rejected by ES.
if (southEastLat === northWestLat || southEastLng === northWestLng) {
return;
}
self._events.emit(drawType, {
e: e,
chart: self._chartData,
bounds: {
bottom_right: {
lat: southEastLat,
lon: southEastLng
},
top_left: {
lat: northWestLat,
lon: northWestLng
}
}
});
});
this.map.on('zoomend', function () {
if (!self.map) return;
self._mapZoom = self.map.getZoom();
if (!self._events) return;
self._events.emit('mapZoomEnd', {
chart: self._chartData,
map: self.map,
zoom: self._mapZoom,
});
});
}
_isWMSEnabled() {
return this._attr.wms ? this._attr.wms.enabled : false;
}
_createTileLayer() {
if (this._isWMSEnabled()) {
const wmsOpts = this._attr.wms;
const { minZoom, maxZoom } = tilemapSettings.getMinMaxZoom(true);
// http://leafletjs.com/reference.html#tilelayer-wms-options
return L.tileLayer.wms(wmsOpts.url, {
// user settings
...wmsOpts.options,
minZoom: minZoom,
maxZoom: maxZoom,
});
}
const tileUrl = tilemapSettings.hasError() ? '' : tilemapSettings.getUrl();
const leafletOptions = tilemapSettings.getTMSOptions();
return L.tileLayer(tileUrl, leafletOptions);
}
/**
* Create the leaflet Map object. In our implementation this is basically just
* a container for the layer created by `this._createTileLayer()`. User settings
* are passed as options to the layer and inherited by the map so we can keep
* this function pretty generic.
*
* The map is responsible for the current center and zoom level though, as those
* are global to each map.
*
* @return undefined
*/
_createMap() {
if (this.map) this.destroy();
// expose at `this._tileLayer`, `this._attachEvents()` accesses it this way
this._tileLayer = this._createTileLayer();
// http://leafletjs.com/reference.html#map-options
this.map = L.map(this._container, {
center: this._mapCenter,
zoom: this._mapZoom,
layers: [this._tileLayer],
maxBounds: L.latLngBounds([-90, -220], [90, 220]),
scrollWheelZoom: false,
fadeAnimation: true,
});
this._attachEvents();
this._addMarkers();
}
/**
* zoom map to fit all features in featureLayer
*
* @method _fitBounds
* @param map {Leaflet Object}
* @return {boolean}
*/
_fitBounds() {
this.map.fitBounds(this._getDataRectangles());
}
/**
* Get the Rectangles representing the geohash grid
*
* @return {LatLngRectangles[]}
*/
_getDataRectangles() {
if (!this._geoJson) return [];
return _.pluck(this._geoJson.features, 'properties.rectangle');
}
}
return TileMapMap;
}

View file

@ -1,286 +0,0 @@
import d3 from 'd3';
import _ from 'lodash';
import $ from 'jquery';
import L from 'leaflet';
export default function MarkerFactory() {
/**
* Base map marker overlay, all other markers inherit from this class
*
* @param map {Leaflet Object}
* @param geoJson {geoJson Object}
* @param params {Object}
*/
class BaseMarker {
constructor(map, geoJson, params) {
this.map = map;
this.geoJson = geoJson;
this.popups = [];
this._tooltipFormatter = params.tooltipFormatter || null;
this._valueFormatter = params.valueFormatter || _.identity;
this._attr = params.attr || {};
// set up the default legend colors
this.quantizeLegendColors();
}
getLabel() {
if (this.popups.length) {
return this.popups[0].feature.properties.aggConfigResult.aggConfig.makeLabel();
}
return '';
}
/**
* Adds legend div to each map when data is split
* uses d3 scale from BaseMarker.prototype.quantizeLegendColors
*
* @method addLegend
* @return {undefined}
*/
addLegend() {
// ensure we only ever create 1 legend
if (this._legend) return;
const self = this;
// create the legend control, keep a reference
self._legend = L.control({ position: this._attr.legendPosition });
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
const $wrapper = $('<div>').addClass('tilemap-legend-wrapper');
const $div = $('<div>').addClass('tilemap-legend');
$wrapper.append($div);
const titleText = self.getLabel();
const $title = $('<div>').addClass('tilemap-legend-title').text(titleText);
$div.append($title);
_.each(self._legendColors, function (color) {
const labelText = self._legendQuantizer
.invertExtent(color)
.map(self._valueFormatter)
.join('  ');
const label = $('<div>');
const icon = $('<i>').css({
background: color,
'border-color': self.darkerColor(color)
});
const text = $('<span>').text(labelText);
label.append(icon);
label.append(text);
$div.append(label);
});
return $wrapper.get(0);
};
self._legend.addTo(self.map);
}
/**
* Apply style with shading to feature
*
* @method applyShadingStyle
* @param value {Object}
* @return {Object}
*/
applyShadingStyle(value) {
const 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}
*/
bindPopup(feature, layer) {
const self = this;
const popup = layer.on({
mouseover: function (e) {
const 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 () {
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
*/
darkerColor(color, amount) {
amount = amount || 1.3;
return d3.hcl(color).darker(amount).toString();
}
destroy() {
const 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;
}
}
_addToMap() {
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
*/
_createMarkerGroup(options) {
const self = this;
const defaultOptions = {
onEachFeature: function (feature, layer) {
self.bindPopup(feature, layer);
},
style: function (feature) {
const 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}
*/
_filterToMapBounds() {
const self = this;
return function (feature) {
const mapBounds = self.map.getBounds();
const 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
*/
_showTooltip(feature, latLng) {
const hasMap = !!this.map;
const hasTooltip = !!this._attr.addTooltip;
if (!hasMap || !hasTooltip) {
return;
}
const lat = _.get(feature, 'geometry.coordinates.1');
const lng = _.get(feature, 'geometry.coordinates.0');
latLng = latLng || L.latLng(lat, lng);
const content = this._tooltipFormatter(feature);
if (!content) return;
this._createTooltip(content, latLng);
}
_createTooltip(content, latLng) {
this._popup = L.popup({ autoPan: false });
this._popup.setLatLng(latLng)
.setContent(content)
.openOn(this.map);
}
/**
* Closes the tooltip on the map
*
* @method _hidePopup
* @return undefined
*/
_hidePopup() {
if (!this.map) return;
this._popup = null;
this.map.closePopup();
}
/**
* d3 quantize scale returns a hex color, used for marker fill color
*
* @method quantizeLegendColors
* return {undefined}
*/
quantizeLegendColors() {
const min = _.get(this.geoJson, 'properties.allmin', 0);
const max = _.get(this.geoJson, 'properties.allmax', 1);
const quantizeDomain = (min !== max) ? [min, max] : d3.scale.quantize().domain();
const reds1 = ['#ff6128'];
const reds3 = ['#fecc5c', '#fd8d3c', '#e31a1c'];
const reds5 = ['#fed976', '#feb24c', '#fd8d3c', '#f03b20', '#bd0026'];
const bottomCutoff = 2;
const middleCutoff = 24;
if (max - min <= bottomCutoff) {
this._legendColors = reds1;
} else if (max - min <= middleCutoff) {
this._legendColors = reds3;
} else {
this._legendColors = reds5;
}
this._legendQuantizer = d3.scale.quantize().domain(quantizeDomain).range(this._legendColors);
}
}
return BaseMarker;
}

View file

@ -1,34 +0,0 @@
import L from 'leaflet';
import VislibVisualizationsMarkerTypesBaseMarkerProvider from './base_marker';
export default function GeohashGridMarkerFactory(Private) {
const BaseMarker = Private(VislibVisualizationsMarkerTypesBaseMarkerProvider);
/**
* Map overlay: rectangles that show the geohash grid bounds
*
* @param map {Leaflet Object}
* @param geoJson {geoJson Object}
* @param params {Object}
*/
class GeohashGridMarker extends BaseMarker {
constructor(map, geoJson, params) {
super(map, geoJson, params);
this._createMarkerGroup({
pointToLayer: function (feature) {
const geohashRect = feature.properties.rectangle;
// get bounds from northEast[3] and southWest[1]
// corners in geohash rectangle
const corners = [
[geohashRect[3][0], geohashRect[3][1]],
[geohashRect[1][0], geohashRect[1][1]]
];
return L.rectangle(corners);
}
});
}
}
return GeohashGridMarker;
}

View file

@ -1,206 +0,0 @@
import d3 from 'd3';
import _ from 'lodash';
import L from 'leaflet';
import VislibVisualizationsMarkerTypesBaseMarkerProvider from './base_marker';
export default function HeatmapMarkerFactory(Private) {
const BaseMarker = Private(VislibVisualizationsMarkerTypesBaseMarkerProvider);
/**
* Map overlay: canvas layer with leaflet.heat plugin
*
* @param map {Leaflet Object}
* @param geoJson {geoJson Object}
* @param params {Object}
*/
class HeatmapMarker extends BaseMarker {
constructor(map, geoJson, params) {
super(map, geoJson, params);
this._disableTooltips = false;
this._createMarkerGroup({
radius: +this._attr.heatRadius,
blur: +this._attr.heatBlur,
maxZoom: +this._attr.heatMaxZoom,
minOpacity: +this._attr.heatMinOpacity
});
this.addLegend = _.noop;
this._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(',');
});
}
_createMarkerGroup(options) {
const max = _.get(this.geoJson, 'properties.allmax');
const points = this._dataToHeatArray(max);
this._markerGroup = L.heatLayer(points, options);
this._fixTooltips();
this._addToMap();
}
_fixTooltips() {
const self = this;
const 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) {
const latlng = e.latlng;
// unhighlight all svgs
d3.selectAll('path.geohash', this.chartEl).classed('geohash-hover', false);
if (!this.geoJson.features.length || this._disableTooltips) {
this._hidePopup();
return;
}
// find nearest feature to event latlng
const feature = this._nearestFeature(latlng);
// show tooltip if close enough to event latlng
if (this._tooltipProximity(latlng, feature)) {
if (this.currentFeature !== feature) {
this._hidePopup();
this.currentFeature = feature;
this._showTooltip(feature, latlng);
} else {
if (this._popup) {
this._popup.setLatLng(latlng);
}
}
} else {
this._hidePopup();
this.currentFeature = null;
}
}
}
/**
* Finds nearest feature in mapData to event latlng
*
* @method _nearestFeature
* @param latLng {Leaflet latLng}
* @return nearestPoint {Leaflet latLng}
*/
_nearestFeature(latLng) {
const self = this;
let nearest;
if (latLng.lng < -180 || latLng.lng > 180) {
return;
}
_.reduce(this.geoJson.features, function (distance, feature) {
const featureLatLng = self._getLatLng(feature);
const 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}
*/
_tooltipProximity(latlng, feature) {
if (!feature) return;
let showTip = false;
const 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
const zoomScale = d3.scale.linear()
.domain([1, 4, 7, 10, 13, 16, 18])
.range([1000000, 300000, 100000, 15000, 2000, 150, 50]);
const proximity = zoomScale(this.map.getZoom());
const distance = latlng.distanceTo(featureLatLng);
// maxLngDif is max difference in longitudes
// to prevent feature tooltip from appearing 360°
// away from event latlng
const maxLngDif = 40;
const lngDif = Math.abs(latlng.lng - featureLatLng.lng);
if (distance < proximity && lngDif < maxLngDif) {
showTip = true;
}
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}
*/
_dataToHeatArray(max) {
const self = this;
return this.geoJson.features.map(function (feature) {
const lat = feature.properties.center[0];
const lng = feature.properties.center[1];
let heatIntensity;
if (!self._attr.heatNormalizeData) {
// show bucket value on heatmap
heatIntensity = feature.properties.value;
} else {
// show bucket value normalized to max value
heatIntensity = feature.properties.value / max;
}
return [lat, lng, heatIntensity];
});
}
}
return HeatmapMarker;
}

View file

@ -1,59 +0,0 @@
import _ from 'lodash';
import L from 'leaflet';
import VislibVisualizationsMarkerTypesBaseMarkerProvider from './base_marker';
export default function ScaledCircleMarkerFactory(Private) {
const BaseMarker = Private(VislibVisualizationsMarkerTypesBaseMarkerProvider);
/**
* Map overlay: circle markers that are scaled to illustrate values
*
* @param map {Leaflet Object}
* @param mapData {geoJson Object}
* @param params {Object}
*/
class ScaledCircleMarker extends BaseMarker {
constructor(map, geoJson, params) {
super(map, geoJson, params);
// multiplier to reduce size of all circles
const scaleFactor = 0.6;
this._createMarkerGroup({
pointToLayer: (feature, latlng) => {
const value = feature.properties.value;
const scaledRadius = this._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}
*/
_radiusScale(value) {
const precisionBiasBase = 5;
const precisionBiasNumerator = 200;
const zoom = this.map.getZoom();
const maxValue = this.geoJson.properties.allmax;
const precision = _.max(this.geoJson.features.map(function (feature) {
return String(feature.properties.geohash).length;
}));
const pct = Math.abs(value) / Math.abs(maxValue);
const zoomRadius = 0.5 * Math.pow(2, zoom);
const precisionScale = precisionBiasNumerator / Math.pow(precisionBiasBase, precision);
// square root value percentage
return Math.pow(pct, 0.5) * zoomRadius * precisionScale;
}
}
return ScaledCircleMarker;
}

View file

@ -1,65 +0,0 @@
import _ from 'lodash';
import L from 'leaflet';
import VislibVisualizationsMarkerTypesBaseMarkerProvider from './base_marker';
export default function ShadedCircleMarkerFactory(Private) {
const BaseMarker = Private(VislibVisualizationsMarkerTypesBaseMarkerProvider);
/**
* Map overlay: circle markers that are shaded to illustrate values
*
* @param map {Leaflet Object}
* @param mapData {geoJson Object}
* @return {Leaflet object} featureLayer
*/
class ShadedCircleMarker extends BaseMarker {
constructor(map, geoJson, params) {
super(map, geoJson, params);
// multiplier to reduce size of all circles
const scaleFactor = 0.8;
this._createMarkerGroup({
pointToLayer: (feature, latlng) => {
const radius = this._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}
*/
_geohashMinDistance(feature) {
const centerPoint = _.get(feature, 'properties.center');
const 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
const east = L.latLng([centerPoint[0], geohashRect[2][1]]);
// southwest lat and center lng
const north = L.latLng([geohashRect[3][0], centerPoint[1]]);
// get latLng of geohash center point
const center = L.latLng([centerPoint[0], centerPoint[1]]);
// get smallest radius at center of geohash grid rectangle
const eastRadius = Math.floor(center.distanceTo(east));
const northRadius = Math.floor(center.distanceTo(north));
return _.min([eastRadius, northRadius]);
}
}
return ShadedCircleMarker;
}

View file

@ -1,128 +0,0 @@
import _ from 'lodash';
import $ from 'jquery';
import VislibVisualizationsChartProvider from './_chart';
import VislibVisualizationsMapProvider from './_map';
export default function TileMapFactory(Private) {
const Chart = Private(VislibVisualizationsChartProvider);
const TileMapMap = Private(VislibVisualizationsMapProvider);
/**
* Tile Map Visualization: renders maps
*
* @class TileMap
* @constructor
* @extends Chart
* @param handler {Object} Reference to the Handler Class Constructor
* @param chartEl {HTMLElement} HTML element to which the map will be appended
* @param chartData {Object} Elasticsearch query results for this map
*/
class TileMap extends Chart {
constructor(handler, chartEl, chartData) {
super(handler, chartEl, chartData);
// track the map objects
this.maps = [];
this._chartData = chartData || {};
_.assign(this, this._chartData);
this._appendGeoExtents();
}
/**
* Draws tile map, called on chart render
*
* @method draw
* @return {Function} - function to add a map to a selection
*/
draw() {
const self = this;
return function (selection) {
selection.each(function () {
self._appendMap(this);
});
};
}
/**
* Invalidate the size of the map, so that leaflet will resize to fit.
* then moves to center
*
* @method resizeArea
* @return {undefined}
*/
resizeArea() {
this.maps.forEach(function (map) {
map.updateSize();
});
}
/**
* clean up the maps
*
* @method destroy
* @return {undefined}
*/
destroy() {
this.maps = this.maps.filter(function (map) {
map.destroy();
});
}
/**
* Adds allmin and allmax properties to geoJson data
*
* @method _appendMap
* @param selection {Object} d3 selection
*/
_appendGeoExtents() {
// add allmin and allmax to geoJson
const 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
*/
_appendMap(selection) {
const container = $(selection).addClass('tilemap');
const uiStateParams = {
mapCenter: this.handler.uiState.get('mapCenter'),
mapZoom: this.handler.uiState.get('mapZoom')
};
const params = _.assign({}, _.get(this._chartData, 'geoAgg.vis.params'), uiStateParams);
const tooltipFormatter = this.handler.visConfig.get('addTooltip') ? this.tooltipFormatter : null;
const map = new TileMapMap(container, this._chartData, {
center: params.mapCenter,
zoom: params.mapZoom,
events: this.events,
markerType: this.handler.visConfig.get('mapType'),
tooltipFormatter: tooltipFormatter,
valueFormatter: this.valueFormatter,
attr: this.handler.visConfig._values
});
// add title for splits
if (this.title) {
map.addTitle(this.title);
}
// add fit to bounds control
if (_.get(this.geoJson, 'features.length') > 0) {
map.addFitControl();
map.addBoundingControl();
}
this.maps.push(map);
}
}
return TileMap;
}

View file

@ -1,330 +0,0 @@
import expect from 'expect.js';
import { bdd } from '../../../support';
import PageObjects from '../../../support/page_objects';
bdd.describe('visualize app', function describeIndexTests() {
const fromTime = '2015-09-19 06:31:44.000';
const toTime = '2015-09-23 18:31:44.000';
bdd.before(function () {
PageObjects.common.debug('navigateToApp visualize');
return PageObjects.common.navigateToUrl('visualize', 'new')
.then(function () {
PageObjects.common.debug('clickTileMap');
return PageObjects.visualize.clickTileMap();
})
.then(function () {
return PageObjects.visualize.clickNewSearch();
})
.then(function () {
PageObjects.common.debug('Set absolute time range from \"' + fromTime + '\" to \"' + toTime + '\"');
return PageObjects.header.setAbsoluteRange(fromTime, toTime);
})
.then(function () {
PageObjects.common.debug('select bucket Geo Coordinates');
return PageObjects.visualize.clickBucket('Geo Coordinates');
})
.then(function () {
PageObjects.common.debug('Click aggregation Geohash');
return PageObjects.visualize.selectAggregation('Geohash');
})
.then(function () {
PageObjects.common.debug('Click field geo.coordinates');
return PageObjects.common.try(function tryingForTime() {
return PageObjects.visualize.selectField('geo.coordinates');
});
})
.then(function () {
return PageObjects.visualize.clickGo();
})
.then(function () {
return PageObjects.header.waitUntilLoadingHasFinished();
});
});
bdd.describe('tile map chart', function indexPatternCreation() {
bdd.it('should show correct tile map data on default zoom level', function () {
const expectedTableData = [ 'dn 1,429', 'dp 1,418', '9y 1,215', '9z 1,099', 'dr 1,076',
'dj 982', '9v 938', '9q 722', '9w 475', 'cb 457', 'c2 453', '9x 420', 'dq 399',
'9r 396', '9t 274', 'c8 271', 'dh 214', 'b6 207', 'bd 206', 'b7 167', 'f0 141',
'be 128', '9m 126', 'bf 85', 'de 73', 'bg 71', '9p 71', 'c1 57', 'c4 50', '9u 48',
'f2 46', '8e 45', 'b3 38', 'bs 36', 'c0 31', '87 28', 'bk 23', '8f 18', 'b5 14',
'84 14', 'dx 9', 'bu 9', 'b1 9', 'b4 6', '9n 3', '8g 3'
];
return PageObjects.visualize.collapseChart()
.then(function () {
return PageObjects.settings.setPageSize('All');
})
.then(function getDataTableData() {
return PageObjects.visualize.getDataTableData()
.then(function showData(data) {
expect(data.trim().split('\n')).to.eql(expectedTableData);
return PageObjects.visualize.collapseChart();
});
});
});
bdd.it('should zoom out to level 1 from default level 2', function () {
const expectedPrecision2Circles = [ { color: '#750000', radius: 48 },
{ color: '#750000', radius: 48 },
{ color: '#750000', radius: 44 },
{ color: '#a40000', radius: 42 },
{ color: '#a40000', radius: 42 },
{ color: '#a40000', radius: 40 },
{ color: '#a40000', radius: 39 },
{ color: '#b45100', radius: 34 },
{ color: '#b67501', radius: 28 },
{ color: '#b67501', radius: 27 },
{ color: '#b67501', radius: 27 },
{ color: '#b67501', radius: 26 },
{ color: '#b67501', radius: 25 },
{ color: '#b67501', radius: 25 },
{ color: '#b99939', radius: 21 },
{ color: '#b99939', radius: 21 },
{ color: '#b99939', radius: 19 },
{ color: '#b99939', radius: 18 },
{ color: '#b99939', radius: 18 },
{ color: '#b99939', radius: 16 },
{ color: '#b99939', radius: 15 },
{ color: '#b99939', radius: 14 },
{ color: '#b99939', radius: 14 },
{ color: '#b99939', radius: 12 },
{ color: '#b99939', radius: 11 },
{ color: '#b99939', radius: 11 },
{ color: '#b99939', radius: 11 },
{ color: '#b99939', radius: 10 },
{ color: '#b99939', radius: 9 },
{ color: '#b99939', radius: 9 },
{ color: '#b99939', radius: 9 },
{ color: '#b99939', radius: 9 },
{ color: '#b99939', radius: 8 },
{ color: '#b99939', radius: 8 },
{ color: '#b99939', radius: 7 },
{ color: '#b99939', radius: 7 },
{ color: '#b99939', radius: 6 },
{ color: '#b99939', radius: 5 },
{ color: '#b99939', radius: 5 },
{ color: '#b99939', radius: 5 },
{ color: '#b99939', radius: 4 },
{ color: '#b99939', radius: 4 },
{ color: '#b99939', radius: 4 },
{ color: '#b99939', radius: 3 },
{ color: '#b99939', radius: 2 },
{ color: '#b99939', radius: 2 }
];
return PageObjects.visualize.clickMapZoomOut()
.then(function () {
return PageObjects.visualize.getMapZoomOutEnabled();
})
// we can tell we're at level 1 because zoom out is disabled
.then(function () {
return PageObjects.common.try(function tryingForTime() {
return PageObjects.visualize.getMapZoomOutEnabled()
.then(function (enabled) {
//should be able to zoom more as current config has 0 as min level.
expect(enabled).to.be(true);
});
});
})
.then(function () {
return PageObjects.common.try(function tryingForTime() {
return PageObjects.visualize.getTileMapData()
.then(function (data) {
expect(data).to.eql(expectedPrecision2Circles);
});
});
})
.then(function takeScreenshot() {
PageObjects.common.debug('Take screenshot (success)');
PageObjects.common.saveScreenshot('map-after-zoom-from-1-to-2');
});
});
bdd.it('Fit data bounds should zoom to level 3', function () {
const expectedPrecision2ZoomCircles = [ { color: '#750000', radius: 192 },
{ color: '#750000', radius: 191 },
{ color: '#750000', radius: 177 },
{ color: '#a40000', radius: 168 },
{ color: '#a40000', radius: 167 },
{ color: '#a40000', radius: 159 },
{ color: '#a40000', radius: 156 },
{ color: '#b45100', radius: 136 },
{ color: '#b67501', radius: 111 },
{ color: '#b67501', radius: 109 },
{ color: '#b67501', radius: 108 },
{ color: '#b67501', radius: 104 },
{ color: '#b67501', radius: 101 },
{ color: '#b67501', radius: 101 },
{ color: '#b99939', radius: 84 },
{ color: '#b99939', radius: 84 },
{ color: '#b99939', radius: 74 },
{ color: '#b99939', radius: 73 },
{ color: '#b99939', radius: 73 },
{ color: '#b99939', radius: 66 },
{ color: '#b99939', radius: 60 },
{ color: '#b99939', radius: 57 },
{ color: '#b99939', radius: 57 },
{ color: '#b99939', radius: 47 },
{ color: '#b99939', radius: 43 },
{ color: '#b99939', radius: 43 },
{ color: '#b99939', radius: 43 },
{ color: '#b99939', radius: 38 },
{ color: '#b99939', radius: 36 },
{ color: '#b99939', radius: 35 },
{ color: '#b99939', radius: 34 },
{ color: '#b99939', radius: 34 },
{ color: '#b99939', radius: 31 },
{ color: '#b99939', radius: 30 },
{ color: '#b99939', radius: 28 },
{ color: '#b99939', radius: 27 },
{ color: '#b99939', radius: 24 },
{ color: '#b99939', radius: 22 },
{ color: '#b99939', radius: 19 },
{ color: '#b99939', radius: 19 },
{ color: '#b99939', radius: 15 },
{ color: '#b99939', radius: 15 },
{ color: '#b99939', radius: 15 },
{ color: '#b99939', radius: 12 },
{ color: '#b99939', radius: 9 },
{ color: '#b99939', radius: 9 }
];
return PageObjects.visualize.clickMapFitDataBounds()
.then(function () {
return PageObjects.visualize.getTileMapData();
})
.then(function (data) {
expect(data).to.eql(expectedPrecision2ZoomCircles);
});
});
/*
** NOTE: Since we don't have a reliable way to know the zoom level, we can
** check some data after we save the viz, then zoom in and check that the data
** changed, then open the saved viz and check that it's back to the original data.
*/
bdd.it('should save with zoom level and load, take screenshot', function () {
const vizName1 = 'Visualization TileMap';
const expectedTableData = [ 'dr4 127', 'dr7 92', '9q5 91', '9qc 89', 'drk 87',
'dps 82', 'dph 82', 'dp3 79', 'dpe 78', 'dp8 77'
];
const expectedTableDataZoomed = [ 'dr5r 21', 'dps8 20', '9q5b 19', 'b6uc 17',
'9y63 17', 'c20g 16', 'dqfz 15', 'dr8h 14', 'dp8p 14', 'dp3k 14'
];
return PageObjects.visualize.clickMapZoomIn()
.then(function () {
return PageObjects.visualize.clickMapZoomIn();
})
.then(function () {
return PageObjects.visualize.saveVisualization(vizName1);
})
.then(function (message) {
PageObjects.common.debug('Saved viz message = ' + message);
expect(message).to.be('Visualization Editor: Saved Visualization \"' + vizName1 + '\"');
})
.then(function testVisualizeWaitForToastMessageGone() {
return PageObjects.visualize.waitForToastMessageGone();
})
.then(function () {
return PageObjects.visualize.collapseChart();
})
// we're not selecting page size all, so we only have to verify the first page of data
.then(function getDataTableData() {
PageObjects.common.debug('first get the zoom level 5 page data and verify it');
return PageObjects.visualize.getDataTableData();
})
.then(function showData(data) {
expect(data.trim().split('\n')).to.eql(expectedTableData);
return PageObjects.visualize.collapseChart();
})
.then(function () {
// zoom to level 6, and make sure we go back to the saved level 5
return PageObjects.visualize.clickMapZoomIn();
})
.then(function () {
return PageObjects.visualize.collapseChart();
})
.then(function getDataTableData() {
PageObjects.common.debug('second get the zoom level 6 page data and verify it');
return PageObjects.visualize.getDataTableData();
})
.then(function showData(data) {
expect(data.trim().split('\n')).to.eql(expectedTableDataZoomed);
return PageObjects.visualize.collapseChart();
})
.then(function () {
return PageObjects.visualize.loadSavedVisualization(vizName1);
})
.then(function waitForVisualization() {
return PageObjects.visualize.waitForVisualization();
})
// sleep a bit before taking the screenshot or it won't show data
.then(function sleep() {
return PageObjects.common.sleep(4000);
})
.then(function () {
return PageObjects.visualize.collapseChart();
})
.then(function getDataTableData() {
PageObjects.common.debug('third get the zoom level 5 page data and verify it');
return PageObjects.visualize.getDataTableData();
})
.then(function showData(data) {
expect(data.trim().split('\n')).to.eql(expectedTableData);
return PageObjects.visualize.collapseChart();
})
.then(function takeScreenshot() {
PageObjects.common.debug('Take screenshot');
PageObjects.common.saveScreenshot('Visualize-site-map');
});
});
bdd.it('should zoom in to level 10', function () {
// 6
return PageObjects.visualize.clickMapZoomIn()
.then(function () {
// 7
return PageObjects.visualize.clickMapZoomIn();
})
.then(function () {
// 8
return PageObjects.visualize.clickMapZoomIn();
})
.then(function () {
// 9
return PageObjects.visualize.clickMapZoomIn();
})
.then(function () {
return PageObjects.common.try(function tryingForTime() {
return PageObjects.visualize.getMapZoomInEnabled()
.then(function (enabled) {
expect(enabled).to.be(true);
});
});
})
.then(function () {
return PageObjects.visualize.clickMapZoomIn();
})
.then(function () {
return PageObjects.visualize.getMapZoomInEnabled();
})
// now we're at level 10 and zoom out should be disabled
.then(function (enabled) {
expect(enabled).to.be(false);
});
});
});
});

View file

@ -35,7 +35,6 @@ bdd.describe('visualize app', function () {
require('./_data_table');
require('./_metric_chart');
require('./_pie_chart');
require('./_tile_map');
require('./_vertical_bar_chart');
require('./_heatmap_chart');
require('./_point_series_options');