[TileMap][RegionMap] Implement custom renderers and expression builders (#84775) (#88308)

* Convert to typescript

* Move related files directly into plugin

* Implement toExpressionAst

* Remove build_pipeline dedicated fn

* Async import converter

* Create a custom renderer

* Remove ExprVis instance usage in maps visualizations

* Use uiState updates

* Create wrapper component

* Update rendering

* Create region map expression renderer

* Remove resize subscription

* Fix custom visualization expression

* Update interpreter functional tests

* Use types from geojson

Co-authored-by: Alexey Antonov <alexwizp@gmail.com>
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>

Co-authored-by: Alexey Antonov <alexwizp@gmail.com>
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Daniil 2021-01-14 16:09:05 +03:00 committed by GitHub
parent 52710be7ad
commit ae139f927f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
53 changed files with 841 additions and 387 deletions

View file

@ -110,7 +110,7 @@ export class ExpressionRenderHandler {
};
}
render = async (value: any, uiState: any = {}) => {
render = async (value: any, uiState?: any) => {
if (!value || typeof value !== 'object') {
return this.handleRenderError(new Error('invalid data provided to the expression renderer'));
}

View file

@ -17,8 +17,7 @@
* under the License.
*/
import { TmsLayer } from '../../index';
import { MapTypes } from './map_types';
import { TmsLayer } from '..';
export interface WMSOptions {
selectedTmsLayer?: TmsLayer;
@ -33,15 +32,3 @@ export interface WMSOptions {
styles?: string;
};
}
export interface TileMapVisParams {
colorSchema: string;
mapType: MapTypes;
isDesaturated: boolean;
addTooltip: boolean;
heatClusterSize: number;
legendPosition: 'bottomright' | 'bottomleft' | 'topright' | 'topleft';
mapZoom: number;
mapCenter: [number, number];
wms: WMSOptions;
}

View file

@ -22,7 +22,7 @@ import { EuiLink, EuiSpacer, EuiText, EuiScreenReaderOnly } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { TextInputOption } from '../../../vis_default_editor/public';
import { WMSOptions } from '../common/types/external_basemap_types';
import { WMSOptions } from '../common/types';
interface WmsInternalOptions {
wms: WMSOptions;

View file

@ -23,20 +23,19 @@ import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { TmsLayer } from '../index';
import { Vis } from '../../../visualizations/public';
import { RegionMapVisParams } from '../common/types/region_map_types';
import { SelectOption, SwitchOption } from '../../../vis_default_editor/public';
import { WmsInternalOptions } from './wms_internal_options';
import { WMSOptions, TileMapVisParams } from '../common/types/external_basemap_types';
import { WMSOptions } from '../common/types';
interface Props {
stateParams: TileMapVisParams | RegionMapVisParams;
interface Props<K> {
stateParams: K;
setValue: (title: 'wms', options: WMSOptions) => void;
vis: Vis;
}
const mapLayerForOption = ({ id }: TmsLayer) => ({ text: id, value: id });
function WmsOptions({ stateParams, setValue, vis }: Props) {
function WmsOptions<K extends { wms: WMSOptions }>({ stateParams, setValue, vis }: Props<K>) {
const { wms } = stateParams;
const { tmsLayers } = vis.type.editorConfig.collections;
const tmsLayerOptions = useMemo(() => tmsLayers.map(mapLayerForOption), [tmsLayers]);

View file

@ -17,17 +17,12 @@
* under the License.
*/
// @ts-ignore
import { PluginInitializerContext } from 'kibana/public';
import { MapsLegacyPlugin } from './plugin';
// @ts-ignore
import * as colorUtil from './map/color_util';
// @ts-ignore
import { KibanaMapLayer } from './map/kibana_map_layer';
// @ts-ignore
import { convertToGeoJson } from './map/convert_to_geojson';
// @ts-ignore
import { getPrecision, geoContains } from './map/decode_geo_hash';
import {
VectorLayer,
FileLayerField,
@ -46,10 +41,7 @@ export function plugin(initializerContext: PluginInitializerContext) {
/** @public */
export {
getPrecision,
geoContains,
colorUtil,
convertToGeoJson,
IServiceSettings,
KibanaMapLayer,
VectorLayer,

View file

@ -34,8 +34,9 @@ export function BaseMapsVisualizationProvider() {
* @constructor
*/
return class BaseMapsVisualization {
constructor(element, vis) {
this.vis = vis;
constructor(element, handlers, initialVisParams) {
this.handlers = handlers;
this._params = initialVisParams;
this._container = element;
this._kibanaMap = null;
this._chartData = null; //reference to data currently on the map.
@ -61,25 +62,31 @@ export function BaseMapsVisualizationProvider() {
* @param status
* @return {Promise}
*/
async render(esResponse, visParams) {
async render(esResponse = this._esResponse, visParams = this._params) {
await this._mapIsLoaded;
if (!this._kibanaMap) {
//the visualization has been destroyed;
return;
}
await this._mapIsLoaded;
this._kibanaMap.resize();
this.resize();
this._params = visParams;
await this._updateParams();
if (this._hasESResponseChanged(esResponse)) {
this._esResponse = esResponse;
await this._updateData(esResponse);
}
this._kibanaMap.useUiStateFromVisualization(this.vis);
this._kibanaMap.useUiStateFromVisualization(this.handlers.uiState);
await this._whenBaseLayerIsLoaded();
}
resize() {
this._kibanaMap?.resize();
}
/**
* Creates an instance of a kibana-map with a single baselayer and assigns it to the this._kibanaMap property.
* Clients can override this method to customize the initialization.
@ -87,11 +94,11 @@ export function BaseMapsVisualizationProvider() {
*/
async _makeKibanaMap() {
const options = {};
const uiState = this.vis.getUiState();
const zoomFromUiState = parseInt(uiState.get('mapZoom'));
const centerFromUIState = uiState.get('mapCenter');
options.zoom = !isNaN(zoomFromUiState) ? zoomFromUiState : this.vis.params.mapZoom;
options.center = centerFromUIState ? centerFromUIState : this.vis.params.mapCenter;
const zoomFromUiState = parseInt(this.handlers.uiState?.get('mapZoom'));
const centerFromUIState = this.handlers.uiState?.get('mapCenter');
const { mapZoom, mapCenter } = this._getMapsParams();
options.zoom = !isNaN(zoomFromUiState) ? zoomFromUiState : mapZoom;
options.center = centerFromUIState ? centerFromUIState : mapCenter;
const modules = await lazyLoadMapsLegacyModules();
this._kibanaMap = new modules.KibanaMap(this._container, options);
@ -100,7 +107,7 @@ export function BaseMapsVisualizationProvider() {
this._kibanaMap.addLegendControl();
this._kibanaMap.addFitControl();
this._kibanaMap.persistUiStateForVisualization(this.vis);
this._kibanaMap.persistUiStateForVisualization(this.handlers.uiState);
this._kibanaMap.on('baseLayer:loaded', () => {
this._baseLayerDirty = false;
@ -212,7 +219,7 @@ export function BaseMapsVisualizationProvider() {
}
_hasESResponseChanged(data) {
return this._chartData !== data;
return this._esResponse !== data;
}
/**
@ -223,15 +230,11 @@ export function BaseMapsVisualizationProvider() {
await this._updateBaseLayer();
this._kibanaMap.setLegendPosition(mapParams.legendPosition);
this._kibanaMap.setShowTooltip(mapParams.addTooltip);
this._kibanaMap.useUiStateFromVisualization(this.vis);
this._kibanaMap.useUiStateFromVisualization(this.handlers.uiState);
}
_getMapsParams() {
return {
...this.vis.type.visConfig.defaults,
type: this.vis.type.name,
...this._params,
};
return this._params;
}
_whenBaseLayerIsLoaded() {

View file

@ -0,0 +1,27 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { geohashColumns } from './geohash_columns';
test('geohashColumns', () => {
expect(geohashColumns(1)).toBe(8);
expect(geohashColumns(2)).toBe(8 * 4);
expect(geohashColumns(3)).toBe(8 * 4 * 8);
expect(geohashColumns(4)).toBe(8 * 4 * 8 * 4);
});

View file

@ -0,0 +1,38 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export function geohashColumns(precision: number): number {
return geohashCells(precision, 0);
}
/**
* Get the number of geohash cells for a given precision
*
* @param {number} precision the geohash precision (1<=precision<=12).
* @param {number} axis constant for the axis 0=lengthwise (ie. columns, along longitude), 1=heightwise (ie. rows, along latitude).
* @returns {number} Number of geohash cells (rows or columns) at that precision
*/
function geohashCells(precision: number, axis: number) {
let cells = 1;
for (let i = 1; i <= precision; i += 1) {
/* On odd precisions, rows divide by 4 and columns by 8. Vice-versa on even precisions */
cells *= i % 2 === axis ? 4 : 8;
}
return cells;
}

View file

@ -672,14 +672,13 @@ export class KibanaMap extends EventEmitter {
}
}
persistUiStateForVisualization(visualization) {
persistUiStateForVisualization(uiState) {
function persistMapStateInUiState() {
const uiState = visualization.getUiState();
const centerFromUIState = uiState.get('mapCenter');
const zoomFromUiState = parseInt(uiState.get('mapZoom'));
if (isNaN(zoomFromUiState) || this.getZoomLevel() !== zoomFromUiState) {
visualization.uiStateVal('mapZoom', this.getZoomLevel());
uiState.set('mapZoom', this.getZoomLevel());
}
const centerFromMap = this.getCenter();
if (
@ -687,24 +686,17 @@ export class KibanaMap extends EventEmitter {
centerFromMap.lon !== centerFromUIState[1] ||
centerFromMap.lat !== centerFromUIState[0]
) {
visualization.uiStateVal('mapCenter', [centerFromMap.lat, centerFromMap.lon]);
uiState.set('mapCenter', [centerFromMap.lat, centerFromMap.lon]);
}
}
this._leafletMap.on('resize', () => {
visualization.sessionState.mapBounds = this.getBounds();
});
this._leafletMap.on('load', () => {
visualization.sessionState.mapBounds = this.getBounds();
});
this.on('dragend', persistMapStateInUiState);
this.on('zoomend', persistMapStateInUiState);
}
useUiStateFromVisualization(visualization) {
const uiState = visualization.getUiState();
const zoomFromUiState = parseInt(uiState.get('mapZoom'));
const centerFromUIState = uiState.get('mapCenter');
useUiStateFromVisualization(uiState) {
const zoomFromUiState = parseInt(uiState?.get('mapZoom'));
const centerFromUIState = uiState?.get('mapCenter');
if (!isNaN(zoomFromUiState)) {
this.setZoomLevel(zoomFromUiState);
}

View file

@ -19,7 +19,7 @@
// @ts-ignore
import { getUiSettings } from '../kibana_services';
import { geohashColumns } from './decode_geo_hash';
import { geohashColumns } from './geohash_columns';
/**
* Get the number of geohash columns (world-wide) for a given precision

View file

@ -17,7 +17,7 @@
* under the License.
*/
import { geohashColumns } from './decode_geo_hash';
import { geohashColumns } from './geohash_columns';
const defaultMaxPrecision = 12;
const minGeoHashPixels = 16;

View file

@ -2,12 +2,9 @@
exports[`interpreter/functions#regionmap returns an object with the correct structure 1`] = `
Object {
"as": "visualization",
"as": "region_map_vis",
"type": "render",
"value": Object {
"params": Object {
"listenOnChange": true,
},
"visConfig": Object {
"addTooltip": true,
"colorSchema": "Yellow to Red",

View file

@ -0,0 +1,29 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React, { lazy } from 'react';
import { IServiceSettings } from 'src/plugins/maps_legacy/public';
import { VisOptionsProps } from 'src/plugins/vis_default_editor/public';
import { RegionMapVisParams } from '../region_map_types';
const RegionMapOptions = lazy(() => import('./region_map_options'));
export const createRegionMapOptions = (getServiceSettings: () => Promise<IServiceSettings>) => (
props: VisOptionsProps<RegionMapVisParams>
) => <RegionMapOptions {...props} getServiceSettings={getServiceSettings} />;

View file

@ -24,7 +24,8 @@ import { FormattedMessage } from '@kbn/i18n/react';
import { VisOptionsProps } from 'src/plugins/vis_default_editor/public';
import { FileLayerField, VectorLayer, IServiceSettings } from '../../../maps_legacy/public';
import { SelectOption, SwitchOption, NumberInputOption } from '../../../vis_default_editor/public';
import { RegionMapVisParams, WmsOptions } from '../../../maps_legacy/public';
import { WmsOptions } from '../../../maps_legacy/public';
import { RegionMapVisParams } from '../region_map_types';
const mapLayerForOption = ({ layerId, name }: VectorLayer) => ({
text: name,
@ -212,4 +213,6 @@ function RegionMapOptions(props: RegionMapOptionsProps) {
);
}
export { RegionMapOptions };
// default export required for React.Lazy
// eslint-disable-next-line import/no-default-export
export { RegionMapOptions as default };

View file

@ -44,9 +44,10 @@ import { RegionMapsConfigType } from './index';
import { MapsLegacyConfig } from '../../maps_legacy/config';
import { KibanaLegacyStart } from '../../kibana_legacy/public';
import { SharePluginStart } from '../../share/public';
import { getRegionMapRenderer } from './region_map_renderer';
/** @private */
interface RegionMapVisualizationDependencies {
export interface RegionMapVisualizationDependencies {
uiSettings: IUiSettingsClient;
regionmapsConfig: RegionMapsConfig;
getServiceSettings: () => Promise<IServiceSettings>;
@ -107,6 +108,7 @@ export class RegionMapPlugin implements Plugin<RegionMapPluginSetup, RegionMapPl
};
expressions.registerFunction(createRegionMapFn);
expressions.registerRenderer(getRegionMapRenderer(visualizationDependencies));
visualizations.createBaseVisualization(
createRegionMapTypeDefinition(visualizationDependencies)

View file

@ -17,7 +17,6 @@
* under the License.
*/
// eslint-disable-next-line
import { functionWrapper } from '../../expressions/common/expression_functions/specs/tests/utils';
import { createRegionMapFn } from './region_map_fn';
@ -57,11 +56,7 @@ describe('interpreter/functions#regionmap', () => {
};
it('returns an object with the correct structure', () => {
const actual = fn(
context,
{ visConfig: JSON.stringify(visConfig) },
{ logDatatable: jest.fn() }
);
const actual = fn(context, { visConfig: JSON.stringify(visConfig) });
expect(actual).toMatchSnapshot();
});
});

View file

@ -19,7 +19,27 @@
import { i18n } from '@kbn/i18n';
export const createRegionMapFn = () => ({
import type { ExpressionFunctionDefinition, Datatable, Render } from '../../expressions/public';
import { RegionMapVisConfig } from './region_map_types';
interface Arguments {
visConfig: string | null;
}
export interface RegionMapVisRenderValue {
visData: Datatable;
visType: 'region_map';
visConfig: RegionMapVisConfig;
}
export type RegionMapExpressionFunctionDefinition = ExpressionFunctionDefinition<
'regionmap',
Datatable,
Arguments,
Render<RegionMapVisRenderValue>
>;
export const createRegionMapFn = (): RegionMapExpressionFunctionDefinition => ({
name: 'regionmap',
type: 'render',
context: {
@ -32,24 +52,22 @@ export const createRegionMapFn = () => ({
visConfig: {
types: ['string', 'null'],
default: '"{}"',
help: '',
},
},
fn(context, args, handlers) {
const visConfig = JSON.parse(args.visConfig);
const visConfig = args.visConfig && JSON.parse(args.visConfig);
if (handlers?.inspectorAdapters?.tables) {
handlers.inspectorAdapters.tables.logDatatable('default', context);
}
return {
type: 'render',
as: 'visualization',
as: 'region_map_vis',
value: {
visData: context,
visType: 'region_map',
visConfig,
params: {
listenOnChange: true,
},
},
};
},

View file

@ -0,0 +1,52 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React, { lazy } from 'react';
import { render, unmountComponentAtNode } from 'react-dom';
import { ExpressionRenderDefinition } from 'src/plugins/expressions';
import { VisualizationContainer } from '../../visualizations/public';
import { RegionMapVisualizationDependencies } from './plugin';
import { RegionMapVisRenderValue } from './region_map_fn';
const RegionMapVisualization = lazy(() => import('./region_map_visualization_component'));
export const getRegionMapRenderer: (
deps: RegionMapVisualizationDependencies
) => ExpressionRenderDefinition<RegionMapVisRenderValue> = (deps) => ({
name: 'region_map_vis',
reuseDomNode: true,
render: async (domNode, { visConfig, visData }, handlers) => {
handlers.onDestroy(() => {
unmountComponentAtNode(domNode);
});
render(
<VisualizationContainer handlers={handlers}>
<RegionMapVisualization
deps={deps}
handlers={handlers}
visConfig={visConfig}
visData={visData}
/>
</VisualizationContainer>,
domNode
);
},
});

View file

@ -16,19 +16,24 @@
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import { i18n } from '@kbn/i18n';
import { mapToLayerWithId } from './util';
import { createRegionMapVisualization } from './region_map_visualization';
import { RegionMapOptions } from './components/region_map_options';
import { BaseVisTypeOptions } from '../../visualizations/public';
import { truncatedColorSchemas } from '../../charts/public';
import { ORIGIN } from '../../maps_legacy/public';
import { getDeprecationMessage } from './get_deprecation_message';
import { RegionMapVisualizationDependencies } from './plugin';
import { createRegionMapOptions } from './components';
import { toExpressionAst } from './to_ast';
import { RegionMapVisParams } from './region_map_types';
import { mapToLayerWithId } from './util';
export function createRegionMapTypeDefinition(dependencies) {
const { uiSettings, regionmapsConfig, getServiceSettings } = dependencies;
const visualization = createRegionMapVisualization(dependencies);
export function createRegionMapTypeDefinition({
uiSettings,
regionmapsConfig,
getServiceSettings,
}: RegionMapVisualizationDependencies): BaseVisTypeOptions<RegionMapVisParams> {
return {
name: 'region_map',
getInfoMessage: getDeprecationMessage,
@ -50,14 +55,11 @@ provided base maps, or add your own. Darker colors represent higher values.',
mapZoom: 2,
mapCenter: [0, 0],
outlineWeight: 1,
showAllShapes: true, //still under consideration
showAllShapes: true, // still under consideration
},
},
visualization,
editorConfig: {
optionsTemplate: (props) => (
<RegionMapOptions {...props} getServiceSettings={getServiceSettings} />
),
optionsTemplate: createRegionMapOptions(getServiceSettings),
collections: {
colorSchemas: truncatedColorSchemas,
vectorLayers: [],
@ -99,6 +101,7 @@ provided base maps, or add your own. Darker colors represent higher values.',
},
],
},
toExpressionAst,
setup: async (vis) => {
const serviceSettings = await getServiceSettings();
const tmsLayers = await serviceSettings.getTMSServices();
@ -111,7 +114,7 @@ provided base maps, or add your own. Darker colors represent higher values.',
mapToLayerWithId.bind(null, ORIGIN.KIBANA_YML)
);
let selectedLayer = vectorLayers[0];
let selectedJoinField = selectedLayer ? selectedLayer.fields[0] : null;
let selectedJoinField = selectedLayer ? selectedLayer.fields[0] : undefined;
if (regionmapsConfig.includeElasticMapsService) {
const layers = await serviceSettings.getFileLayers();
const newLayers = layers
@ -132,7 +135,7 @@ provided base maps, or add your own. Darker colors represent higher values.',
vis.type.editorConfig.collections.vectorLayers = [...vectorLayers, ...newLayers];
[selectedLayer] = vis.type.editorConfig.collections.vectorLayers;
selectedJoinField = selectedLayer ? selectedLayer.fields[0] : null;
selectedJoinField = selectedLayer ? selectedLayer.fields[0] : undefined;
if (selectedLayer && !vis.params.selectedLayer && selectedLayer.isEMS) {
vis.params.emsHotLink = await serviceSettings.getEMSHotLink(selectedLayer);

View file

@ -17,8 +17,8 @@
* under the License.
*/
import { VectorLayer, FileLayerField } from '../../index';
import { WMSOptions } from './external_basemap_types';
import { SchemaConfig } from 'src/plugins/visualizations/public';
import { VectorLayer, FileLayerField, WMSOptions } from '../../maps_legacy/public/index';
export interface RegionMapVisParams {
readonly addTooltip: true;
@ -34,3 +34,8 @@ export interface RegionMapVisParams {
selectedJoinField?: FileLayerField;
wms: WMSOptions;
}
export interface RegionMapVisConfig extends RegionMapVisParams {
metric: SchemaConfig;
bucket?: SchemaConfig;
}

View file

@ -30,9 +30,8 @@ export function createRegionMapVisualization({
getServiceSettings,
}) {
return class RegionMapsVisualization extends BaseMapsVisualization {
constructor(container, vis) {
super(container, vis);
this._vis = this.vis;
constructor(container, handlers, initialVisParams) {
super(container, handlers, initialVisParams);
this._choroplethLayer = null;
this._tooltipFormatter = mapTooltipProvider(container, tooltipFormatter);
}
@ -88,7 +87,7 @@ export function createRegionMapVisualization({
);
}
this._kibanaMap.useUiStateFromVisualization(this._vis);
this._kibanaMap.useUiStateFromVisualization(this.handlers.uiState);
}
async _loadConfig(fileLayerConfig) {
@ -201,11 +200,18 @@ export function createRegionMapVisualization({
this._choroplethLayer.on('select', (event) => {
const { rows, columns } = this._chartData;
const rowIndex = rows.findIndex((row) => row[columns[0].id] === event);
this._vis.API.events.filter({
table: this._chartData,
column: 0,
row: rowIndex,
value: event,
this.handlers.event({
name: 'filterBucket',
data: {
data: [
{
table: this._chartData,
column: 0,
row: rowIndex,
value: event,
},
],
},
});
});

View file

@ -0,0 +1,4 @@
.rgmChart__wrapper, .rgmChart {
flex: 1 1 0;
display: flex;
}

View file

@ -0,0 +1,103 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React, { useEffect, useMemo, useRef } from 'react';
import { EuiResizeObserver } from '@elastic/eui';
import { throttle } from 'lodash';
import { IInterpreterRenderHandlers, Datatable } from 'src/plugins/expressions';
import { PersistedState } from 'src/plugins/visualizations/public';
import { RegionMapVisualizationDependencies } from './plugin';
import { RegionMapVisConfig } from './region_map_types';
// @ts-expect-error
import { createRegionMapVisualization } from './region_map_visualization';
import './region_map_visualization.scss';
interface RegionMapVisController {
render(visData?: Datatable, visConfig?: RegionMapVisConfig): Promise<void>;
resize(): void;
destroy(): void;
}
interface TileMapVisualizationProps {
deps: RegionMapVisualizationDependencies;
handlers: IInterpreterRenderHandlers;
visData: Datatable;
visConfig: RegionMapVisConfig;
}
const RegionMapVisualization = ({
deps,
handlers,
visData,
visConfig,
}: TileMapVisualizationProps) => {
const chartDiv = useRef<HTMLDivElement>(null);
const visController = useRef<RegionMapVisController | null>(null);
const isFirstRender = useRef(true);
const uiState = handlers.uiState as PersistedState | undefined;
useEffect(() => {
if (chartDiv.current && isFirstRender.current) {
isFirstRender.current = false;
const Controller = createRegionMapVisualization(deps);
visController.current = new Controller(chartDiv.current, handlers, visConfig);
}
}, [deps, handlers, visConfig, visData]);
useEffect(() => {
visController.current?.render(visData, visConfig).then(handlers.done);
}, [visData, visConfig, handlers.done]);
useEffect(() => {
const onUiStateChange = () => {
visController.current?.render().then(handlers.done);
};
uiState?.on('change', onUiStateChange);
return () => {
uiState?.off('change', onUiStateChange);
};
}, [uiState, handlers.done]);
useEffect(() => {
return () => {
visController.current?.destroy();
visController.current = null;
};
}, []);
const updateChartSize = useMemo(() => throttle(() => visController.current?.resize(), 300), []);
return (
<EuiResizeObserver onResize={updateChartSize}>
{(resizeRef) => (
<div className="rgmChart__wrapper" ref={resizeRef}>
<div className="rgmChart" ref={chartDiv} />
</div>
)}
</EuiResizeObserver>
);
};
// default export required for React.Lazy
// eslint-disable-next-line import/no-default-export
export { RegionMapVisualization as default };

View file

@ -0,0 +1,59 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import {
EsaggsExpressionFunctionDefinition,
IndexPatternLoadExpressionFunctionDefinition,
} from '../../data/public';
import { buildExpression, buildExpressionFunction } from '../../expressions/public';
import { getVisSchemas, Vis, BuildPipelineParams } from '../../visualizations/public';
import { RegionMapExpressionFunctionDefinition } from './region_map_fn';
import { RegionMapVisConfig, RegionMapVisParams } from './region_map_types';
export const toExpressionAst = (vis: Vis<RegionMapVisParams>, params: BuildPipelineParams) => {
const esaggs = buildExpressionFunction<EsaggsExpressionFunctionDefinition>('esaggs', {
index: buildExpression([
buildExpressionFunction<IndexPatternLoadExpressionFunctionDefinition>('indexPatternLoad', {
id: vis.data.indexPattern!.id!,
}),
]),
metricsAtAllLevels: false,
partialRows: false,
aggs: vis.data.aggs!.aggs.map((agg) => buildExpression(agg.toExpressionAst())),
});
const schemas = getVisSchemas(vis, params);
const visConfig: RegionMapVisConfig = {
...vis.params,
metric: schemas.metric[0],
};
if (schemas.segment) {
visConfig.bucket = schemas.segment[0];
}
const regionmap = buildExpressionFunction<RegionMapExpressionFunctionDefinition>('regionmap', {
visConfig: JSON.stringify(visConfig),
});
const ast = buildExpression([esaggs, regionmap]);
return ast.toAst();
};

View file

@ -2,12 +2,9 @@
exports[`interpreter/functions#tilemap returns an object with the correct structure 1`] = `
Object {
"as": "visualization",
"as": "tile_map_vis",
"type": "render",
"value": Object {
"params": Object {
"listenOnChange": true,
},
"visConfig": Object {
"addTooltip": true,
"colorSchema": "Yellow to Red",

View file

@ -1,15 +0,0 @@
// SASSTODO: Does this selector exist today?
.tilemap {
margin-bottom: 6px;
border: $euiBorderThin;
position: relative;
}
/**
* 1. Visualizations have some padding by default but tilemaps look nice flush against the edge to maximize viewing
* space.
*/
// SASSTODO: Does this selector exist today?
.tile_map {
padding: 0; /* 1. */
}

View file

@ -0,0 +1,25 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React, { lazy } from 'react';
import type { TileMapOptionsProps } from './tile_map_options';
const TileMapOptions = lazy(() => import('./tile_map_options'));
export const TileMapOptionsLazy = (props: TileMapOptionsProps) => <TileMapOptions {...props} />;

View file

@ -28,7 +28,9 @@ import {
SwitchOption,
RangeOption,
} from '../../../vis_default_editor/public';
import { WmsOptions, TileMapVisParams, MapTypes } from '../../../maps_legacy/public';
import { WmsOptions } from '../../../maps_legacy/public';
import { TileMapVisParams } from '../types';
import { MapTypes } from '../utils/map_types';
export type TileMapOptionsProps = VisOptionsProps<TileMapVisParams>;
@ -102,4 +104,6 @@ function TileMapOptions(props: TileMapOptionsProps) {
);
}
export { TileMapOptions };
// default export required for React.Lazy
// eslint-disable-next-line import/no-default-export
export { TileMapOptions as default };

View file

@ -19,11 +19,12 @@
import { min, isEqual } from 'lodash';
import { i18n } from '@kbn/i18n';
import { KibanaMapLayer, MapTypes } from '../../maps_legacy/public';
import { KibanaMapLayer } from '../../maps_legacy/public';
import { HeatmapMarkers } from './markers/heatmap';
import { ScaledCirclesMarkers } from './markers/scaled_circles';
import { ShadedCirclesMarkers } from './markers/shaded_circles';
import { GeohashGridMarkers } from './markers/geohash_grid';
import { MapTypes } from './utils/map_types';
export class GeohashLayer extends KibanaMapLayer {
constructor(featureCollection, featureCollectionMetaData, options, zoom, kibanaMap, leaflet) {

View file

@ -1,8 +0,0 @@
// Prefix all styles with "tlm" to avoid conflicts.
// Examples
// tlmChart
// tlmChart__legend
// tlmChart__legend--small
// tlmChart__legend-isLoading
@import 'tile_map';

View file

@ -25,13 +25,6 @@ import {
} from 'kibana/public';
import { Plugin as ExpressionsPublicPlugin } from '../../expressions/public';
import { VisualizationsSetup } from '../../visualizations/public';
// TODO: Determine why visualizations don't populate without this
import 'angular-sanitize';
// @ts-ignore
import { createTileMapFn } from './tile_map_fn';
// @ts-ignore
import { createTileMapTypeDefinition } from './tile_map_type';
import { IServiceSettings, MapsLegacyPluginSetup } from '../../maps_legacy/public';
import { DataPublicPluginStart } from '../../data/public';
import {
@ -44,12 +37,16 @@ import {
import { KibanaLegacyStart } from '../../kibana_legacy/public';
import { SharePluginStart } from '../../share/public';
import { createTileMapFn } from './tile_map_fn';
import { createTileMapTypeDefinition } from './tile_map_type';
import { getTileMapRenderer } from './tile_map_renderer';
export interface TileMapConfigType {
tilemap: any;
}
/** @private */
interface TileMapVisualizationDependencies {
export interface TileMapVisualizationDependencies {
uiSettings: IUiSettingsClient;
getZoomPrecision: any;
getPrecision: any;
@ -98,7 +95,8 @@ export class TileMapPlugin implements Plugin<TileMapPluginSetup, TileMapPluginSt
getServiceSettings,
};
expressions.registerFunction(() => createTileMapFn(visualizationDependencies));
expressions.registerFunction(createTileMapFn);
expressions.registerRenderer(getTileMapRenderer(visualizationDependencies));
visualizations.createBaseVisualization(createTileMapTypeDefinition(visualizationDependencies));

View file

@ -17,11 +17,10 @@
* under the License.
*/
// eslint-disable-next-line
import { functionWrapper } from '../../expressions/common/expression_functions/specs/tests/utils';
import { createTileMapFn } from './tile_map_fn';
jest.mock('../../maps_legacy/public', () => ({
jest.mock('./utils', () => ({
convertToGeoJson: jest.fn().mockReturnValue({
featureCollection: {
type: 'FeatureCollection',
@ -36,7 +35,7 @@ jest.mock('../../maps_legacy/public', () => ({
}),
}));
import { convertToGeoJson } from '../../maps_legacy/public';
import { convertToGeoJson } from './utils';
describe('interpreter/functions#tilemap', () => {
const fn = functionWrapper(createTileMapFn());
@ -79,18 +78,14 @@ describe('interpreter/functions#tilemap', () => {
jest.clearAllMocks();
});
it('returns an object with the correct structure', () => {
const actual = fn(
context,
{ visConfig: JSON.stringify(visConfig) },
{ logDatatable: jest.fn() }
);
it('returns an object with the correct structure', async () => {
const actual = await fn(context, { visConfig: JSON.stringify(visConfig) });
expect(actual).toMatchSnapshot();
});
it('calls response handler with correct values', () => {
it('calls response handler with correct values', async () => {
const { geohash, metric, geocentroid } = visConfig.dimensions;
fn(context, { visConfig: JSON.stringify(visConfig) }, { logDatatable: jest.fn() });
await fn(context, { visConfig: JSON.stringify(visConfig) });
expect(convertToGeoJson).toHaveBeenCalledTimes(1);
expect(convertToGeoJson).toHaveBeenCalledWith(context, {
geohash,

View file

@ -16,10 +16,30 @@
* specific language governing permissions and limitations
* under the License.
*/
import { convertToGeoJson } from '../../maps_legacy/public';
import { i18n } from '@kbn/i18n';
export const createTileMapFn = () => ({
import type { ExpressionFunctionDefinition, Datatable, Render } from '../../expressions/public';
import { TileMapVisConfig, TileMapVisData } from './types';
interface Arguments {
visConfig: string | null;
}
export interface TileMapVisRenderValue {
visData: TileMapVisData;
visType: 'tile_map';
visConfig: TileMapVisConfig;
}
export type TileMapExpressionFunctionDefinition = ExpressionFunctionDefinition<
'tilemap',
Datatable,
Arguments,
Promise<Render<TileMapVisRenderValue>>
>;
export const createTileMapFn = (): TileMapExpressionFunctionDefinition => ({
name: 'tilemap',
type: 'render',
context: {
@ -32,34 +52,30 @@ export const createTileMapFn = () => ({
visConfig: {
types: ['string', 'null'],
default: '"{}"',
help: '',
},
},
fn(context, args, handlers) {
const visConfig = JSON.parse(args.visConfig);
async fn(context, args, handlers) {
const visConfig = args.visConfig && JSON.parse(args.visConfig);
const { geohash, metric, geocentroid } = visConfig.dimensions;
const { convertToGeoJson } = await import('./utils');
const convertedData = convertToGeoJson(context, {
geohash,
metric,
geocentroid,
});
if (geohash && geohash.accessor) {
convertedData.meta.geohash = context.columns[geohash.accessor].meta;
}
if (handlers?.inspectorAdapters?.tables) {
handlers.inspectorAdapters.tables.logDatatable('default', context);
}
return {
type: 'render',
as: 'visualization',
as: 'tile_map_vis',
value: {
visData: convertedData,
visType: 'tile_map',
visConfig,
params: {
listenOnChange: true,
},
},
};
},

View file

@ -0,0 +1,52 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React, { lazy } from 'react';
import { render, unmountComponentAtNode } from 'react-dom';
import { ExpressionRenderDefinition } from 'src/plugins/expressions';
import { VisualizationContainer } from '../../visualizations/public';
import { TileMapVisualizationDependencies } from './plugin';
import { TileMapVisRenderValue } from './tile_map_fn';
const TileMapVisualization = lazy(() => import('./tile_map_visualization_component'));
export const getTileMapRenderer: (
deps: TileMapVisualizationDependencies
) => ExpressionRenderDefinition<TileMapVisRenderValue> = (deps) => ({
name: 'tile_map_vis',
reuseDomNode: true,
render: async (domNode, { visConfig, visData }, handlers) => {
handlers.onDestroy(() => {
unmountComponentAtNode(domNode);
});
render(
<VisualizationContainer handlers={handlers}>
<TileMapVisualization
deps={deps}
handlers={handlers}
visData={visData}
visConfig={visConfig}
/>
</VisualizationContainer>,
domNode
);
},
});

View file

@ -17,17 +17,22 @@
* under the License.
*/
import React from 'react';
import { i18n } from '@kbn/i18n';
import { convertToGeoJson, MapTypes } from '../../maps_legacy/public';
import { createTileMapVisualization } from './tile_map_visualization';
import { TileMapOptions } from './components/tile_map_options';
import { supportsCssFilters } from './css_filters';
import { BaseVisTypeOptions } from 'src/plugins/visualizations/public';
import { truncatedColorSchemas } from '../../charts/public';
import { getDeprecationMessage } from './get_deprecation_message';
export function createTileMapTypeDefinition(dependencies) {
const CoordinateMapsVisualization = createTileMapVisualization(dependencies);
// @ts-expect-error
import { supportsCssFilters } from './css_filters';
import { TileMapOptionsLazy } from './components';
import { getDeprecationMessage } from './get_deprecation_message';
import { TileMapVisualizationDependencies } from './plugin';
import { toExpressionAst } from './to_ast';
import { TileMapVisParams } from './types';
import { MapTypes } from './utils/map_types';
export function createTileMapTypeDefinition(
dependencies: TileMapVisualizationDependencies
): BaseVisTypeOptions<TileMapVisParams> {
const { uiSettings, getServiceSettings } = dependencies;
return {
@ -54,8 +59,7 @@ export function createTileMapTypeDefinition(dependencies) {
wms: uiSettings.get('visualization:tileMap:WMSdefaults'),
},
},
visualization: CoordinateMapsVisualization,
responseHandler: convertToGeoJson,
toExpressionAst,
editorConfig: {
collections: {
colorSchemas: truncatedColorSchemas,
@ -113,7 +117,7 @@ export function createTileMapTypeDefinition(dependencies) {
],
tmsLayers: [],
},
optionsTemplate: (props) => <TileMapOptions {...props} />,
optionsTemplate: TileMapOptionsLazy,
schemas: [
{
group: 'metrics',

View file

@ -19,12 +19,9 @@
import { get, round } from 'lodash';
import { getFormatService, getQueryService, getKibanaLegacy } from './services';
import {
geoContains,
mapTooltipProvider,
lazyLoadMapsLegacyModules,
} from '../../maps_legacy/public';
import { mapTooltipProvider, lazyLoadMapsLegacyModules } from '../../maps_legacy/public';
import { tooltipFormatter } from './tooltip_formatter';
import { geoContains } from './utils';
function scaleBounds(bounds) {
const scale = 0.5; // scale bounds by 50%
@ -57,8 +54,8 @@ export const createTileMapVisualization = (dependencies) => {
const { getZoomPrecision, getPrecision, BaseMapsVisualization } = dependencies;
return class CoordinateMapsVisualization extends BaseMapsVisualization {
constructor(element, vis) {
super(element, vis);
constructor(element, handlers, initialVisParams) {
super(element, handlers, initialVisParams);
this._geohashLayer = null;
this._tooltipFormatter = mapTooltipProvider(element, tooltipFormatter);
@ -84,10 +81,10 @@ export const createTileMapVisualization = (dependencies) => {
// todo: autoPrecision should be vis parameter, not aggConfig one
const zoomPrecision = getZoomPrecision();
updateVarsObject.data.precision = geohashAgg.sourceParams.params.autoPrecision
? zoomPrecision[this.vis.getUiState().get('mapZoom')]
? zoomPrecision[this.handlers.uiState.get('mapZoom')]
: getPrecision(geohashAgg.sourceParams.params.precision);
this.vis.eventsSubject.next(updateVarsObject);
this.handlers.event(updateVarsObject);
};
async render(esResponse, visParams) {
@ -96,13 +93,12 @@ export const createTileMapVisualization = (dependencies) => {
}
async _makeKibanaMap() {
await super._makeKibanaMap();
await super._makeKibanaMap(this._params);
let previousPrecision = this._kibanaMap.getGeohashPrecision();
let precisionChange = false;
const uiState = this.vis.getUiState();
uiState.on('change', (prop) => {
this.handlers.uiState.on('change', (prop) => {
if (prop === 'mapZoom' || prop === 'mapCenter') {
this.updateGeohashAgg();
}
@ -250,8 +246,6 @@ export const createTileMapVisualization = (dependencies) => {
const { filterManager } = getQueryService();
filterManager.addFilters([filter]);
this.vis.updateState();
}
_getGeoHashAgg() {

View file

@ -0,0 +1,4 @@
.tlmChart__wrapper, .tlmChart {
flex: 1 1 0;
display: flex;
}

View file

@ -0,0 +1,103 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React, { useEffect, useMemo, useRef } from 'react';
import { EuiResizeObserver } from '@elastic/eui';
import { throttle } from 'lodash';
import { IInterpreterRenderHandlers } from 'src/plugins/expressions';
import { PersistedState } from 'src/plugins/visualizations/public';
import { TileMapVisualizationDependencies } from './plugin';
import { TileMapVisConfig, TileMapVisData } from './types';
// @ts-expect-error
import { createTileMapVisualization } from './tile_map_visualization';
import './tile_map_visualization.scss';
interface TileMapVisController {
render(visData?: TileMapVisData, visConfig?: TileMapVisConfig): Promise<void>;
resize(): void;
destroy(): void;
}
interface TileMapVisualizationProps {
deps: TileMapVisualizationDependencies;
handlers: IInterpreterRenderHandlers;
visData: TileMapVisData;
visConfig: TileMapVisConfig;
}
const TileMapVisualization = ({
deps,
handlers,
visData,
visConfig,
}: TileMapVisualizationProps) => {
const chartDiv = useRef<HTMLDivElement>(null);
const visController = useRef<TileMapVisController | null>(null);
const isFirstRender = useRef(true);
const uiState = handlers.uiState as PersistedState;
useEffect(() => {
if (chartDiv.current && isFirstRender.current) {
isFirstRender.current = false;
const Controller = createTileMapVisualization(deps);
visController.current = new Controller(chartDiv.current, handlers, visConfig);
}
}, [deps, handlers, visConfig, visData]);
useEffect(() => {
visController.current?.render(visData, visConfig).then(handlers.done);
}, [visData, visConfig, handlers.done]);
useEffect(() => {
const onUiStateChange = () => {
visController.current?.render().then(handlers.done);
};
uiState.on('change', onUiStateChange);
return () => {
uiState.off('change', onUiStateChange);
};
}, [uiState, handlers.done]);
useEffect(() => {
return () => {
visController.current?.destroy();
visController.current = null;
};
}, []);
const updateChartSize = useMemo(() => throttle(() => visController.current?.resize(), 300), []);
return (
<EuiResizeObserver onResize={updateChartSize}>
{(resizeRef) => (
<div className="tlmChart__wrapper" ref={resizeRef}>
<div className="tlmChart" ref={chartDiv} />
</div>
)}
</EuiResizeObserver>
);
};
// default export required for React.Lazy
// eslint-disable-next-line import/no-default-export
export { TileMapVisualization as default };

View file

@ -0,0 +1,59 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import {
EsaggsExpressionFunctionDefinition,
IndexPatternLoadExpressionFunctionDefinition,
} from '../../data/public';
import { buildExpression, buildExpressionFunction } from '../../expressions/public';
import { getVisSchemas, Vis, BuildPipelineParams } from '../../visualizations/public';
import { TileMapExpressionFunctionDefinition } from './tile_map_fn';
import { TileMapVisConfig, TileMapVisParams } from './types';
export const toExpressionAst = (vis: Vis<TileMapVisParams>, params: BuildPipelineParams) => {
const esaggs = buildExpressionFunction<EsaggsExpressionFunctionDefinition>('esaggs', {
index: buildExpression([
buildExpressionFunction<IndexPatternLoadExpressionFunctionDefinition>('indexPatternLoad', {
id: vis.data.indexPattern!.id!,
}),
]),
metricsAtAllLevels: false,
partialRows: false,
aggs: vis.data.aggs!.aggs.map((agg) => buildExpression(agg.toExpressionAst())),
});
const schemas = getVisSchemas(vis, params);
const visConfig: TileMapVisConfig = {
...vis.params,
dimensions: {
metric: schemas.metric[0],
geohash: schemas.segment ? schemas.segment[0] : null,
geocentroid: schemas.geo_centroid ? schemas.geo_centroid[0] : null,
},
};
const tilemap = buildExpressionFunction<TileMapExpressionFunctionDefinition>('tilemap', {
visConfig: JSON.stringify(visConfig),
});
const ast = buildExpression([esaggs, tilemap]);
return ast.toAst();
};

View file

@ -0,0 +1,57 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { FeatureCollection } from 'geojson';
import type { SchemaConfig } from 'src/plugins/visualizations/public';
import type { DatatableColumnMeta } from 'src/plugins/expressions';
import type { WMSOptions } from 'src/plugins/maps_legacy/public';
import type { MapTypes } from './utils/map_types';
export interface TileMapVisData {
featureCollection: FeatureCollection;
meta: {
min: number;
max: number;
geohash?: DatatableColumnMeta;
geohashPrecision: number | undefined;
geohashGridDimensionsAtEquator: [number, number] | undefined;
};
}
export interface TileMapVisDimensions {
metric: SchemaConfig;
geohash: SchemaConfig | null;
geocentroid: SchemaConfig | null;
}
export interface TileMapVisParams {
colorSchema: string;
mapType: MapTypes;
isDesaturated: boolean;
addTooltip: boolean;
heatClusterSize: number;
legendPosition: 'bottomright' | 'bottomleft' | 'topright' | 'topleft';
mapZoom: number;
mapCenter: [number, number];
wms: WMSOptions;
}
export interface TileMapVisConfig extends TileMapVisParams {
dimensions: TileMapVisDimensions;
}

View file

@ -17,11 +17,17 @@
* under the License.
*/
import { Feature } from 'geojson';
import type { Datatable } from '../../../expressions/public';
import type { TileMapVisDimensions, TileMapVisData } from '../types';
import { decodeGeoHash } from './decode_geo_hash';
import { gridDimensions } from './grid_dimensions';
export function convertToGeoJson(tabifiedResponse, { geohash, geocentroid, metric }) {
let features;
export function convertToGeoJson(
tabifiedResponse: Datatable,
{ geohash, geocentroid, metric }: TileMapVisDimensions
): TileMapVisData {
let features: Feature[];
let min = Infinity;
let max = -Infinity;
@ -41,7 +47,7 @@ export function convertToGeoJson(tabifiedResponse, { geohash, geocentroid, metri
if (!geohashValue) return false;
const geohashLocation = decodeGeoHash(geohashValue);
let pointCoordinates;
let pointCoordinates: number[];
if (geocentroidColumn) {
const location = row[geocentroidColumn.id];
pointCoordinates = [location.lon, location.lat];
@ -58,7 +64,7 @@ export function convertToGeoJson(tabifiedResponse, { geohash, geocentroid, metri
const centerLatLng = [geohashLocation.latitude[2], geohashLocation.longitude[2]];
if (geohash.params.useGeocentroid) {
if (geohash?.params.useGeocentroid) {
// see https://github.com/elastic/elasticsearch/issues/24694 for why clampGrid is used
pointCoordinates[0] = clampGrid(
pointCoordinates[0],
@ -86,35 +92,41 @@ export function convertToGeoJson(tabifiedResponse, { geohash, geocentroid, metri
geohash: geohashValue,
geohash_meta: {
center: centerLatLng,
rectangle: rectangle,
rectangle,
},
value: value,
value,
},
};
} as Feature;
})
.filter((row) => row);
.filter((row): row is Feature => !!row);
}
} else {
features = [];
}
const featureCollection = {
type: 'FeatureCollection',
features: features,
};
return {
featureCollection: featureCollection,
const convertedData: TileMapVisData = {
featureCollection: {
type: 'FeatureCollection',
features,
},
meta: {
min: min,
max: max,
geohashPrecision: geohash && geohash.params.precision,
geohashGridDimensionsAtEquator: geohash && gridDimensions(geohash.params.precision),
min,
max,
geohashPrecision: geohash?.params.precision,
geohashGridDimensionsAtEquator: geohash?.params.precision
? gridDimensions(geohash.params.precision)
: undefined,
},
};
if (geohash && geohash.accessor) {
convertedData.meta.geohash = tabifiedResponse.columns[geohash.accessor].meta;
}
return convertedData;
}
function clampGrid(val, min, max) {
function clampGrid(val: number, min: number, max: number) {
if (val > max) val = max;
else if (val < min) val = min;
return val;

View file

@ -17,14 +17,7 @@
* under the License.
*/
import { geohashColumns, decodeGeoHash } from './decode_geo_hash';
test('geohashColumns', () => {
expect(geohashColumns(1)).toBe(8);
expect(geohashColumns(2)).toBe(8 * 4);
expect(geohashColumns(3)).toBe(8 * 4 * 8);
expect(geohashColumns(4)).toBe(8 * 4 * 8 * 4);
});
import { decodeGeoHash } from './decode_geo_hash';
test('decodeGeoHash', () => {
expect(decodeGeoHash('drm3btev3e86')).toEqual({

View file

@ -55,10 +55,11 @@ export function decodeGeoHash(geohash: string): DecodedGeoHash {
});
lat[2] = (lat[0] + lat[1]) / 2;
lon[2] = (lon[0] + lon[1]) / 2;
return {
latitude: lat,
longitude: lon,
} as DecodedGeoHash;
};
}
function refineInterval(interval: number[], cd: number, mask: number) {
@ -69,26 +70,6 @@ function refineInterval(interval: number[], cd: number, mask: number) {
}
}
export function geohashColumns(precision: number): number {
return geohashCells(precision, 0);
}
/**
* Get the number of geohash cells for a given precision
*
* @param {number} precision the geohash precision (1<=precision<=12).
* @param {number} axis constant for the axis 0=lengthwise (ie. columns, along longitude), 1=heightwise (ie. rows, along latitude).
* @returns {number} Number of geohash cells (rows or columns) at that precision
*/
function geohashCells(precision: number, axis: number) {
let cells = 1;
for (let i = 1; i <= precision; i += 1) {
/* On odd precisions, rows divide by 4 and columns by 8. Vice-versa on even precisions */
cells *= i % 2 === axis ? 4 : 8;
}
return cells;
}
interface GeoBoundingBoxCoordinate {
lat: number;
lon: number;

View file

@ -19,7 +19,7 @@
// geohash precision mapping of geohash grid cell dimensions (width x height, in meters) at equator.
// https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-geohashgrid-aggregation.html#_cell_dimensions_at_the_equator
const gridAtEquator = {
const gridAtEquator: { [key: number]: [number, number] } = {
1: [5009400, 4992600],
2: [1252300, 624100],
3: [156500, 156000],
@ -34,6 +34,6 @@ const gridAtEquator = {
12: [0.037, 0.019],
};
export function gridDimensions(precision) {
export function gridDimensions(precision: number) {
return gridAtEquator[precision];
}

View file

@ -17,10 +17,5 @@
* under the License.
*/
/**
* Use * syntax so that these exports do not break when internal
* types are stripped.
*/
export * from './external_basemap_types';
export * from './map_types';
export * from './region_map_types';
export { convertToGeoJson } from './convert_to_geojson';
export { geoContains } from './decode_geo_hash';

View file

@ -1,9 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`visualize loader pipeline helpers: build pipeline buildPipeline calls toExpression on vis_type if it exists 1`] = `"kibana | kibana_context | test"`;
exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles region_map function with buckets 1`] = `"regionmap visConfig='{\\"metric\\":{\\"accessor\\":0,\\"label\\":\\"\\",\\"format\\":{},\\"params\\":{},\\"aggType\\":\\"\\"},\\"bucket\\":1}' "`;
exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles region_map function without buckets 1`] = `"regionmap visConfig='{\\"metric\\":{\\"accessor\\":0,\\"label\\":\\"\\",\\"format\\":{},\\"params\\":{},\\"aggType\\":\\"\\"}}' "`;
exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles tile_map function 1`] = `"tilemap visConfig='{\\"metric\\":{},\\"dimensions\\":{\\"metric\\":{\\"accessor\\":0,\\"label\\":\\"\\",\\"format\\":{},\\"params\\":{},\\"aggType\\":\\"\\"},\\"geohash\\":1,\\"geocentroid\\":3}}' "`;

View file

@ -17,14 +17,7 @@
* under the License.
*/
import {
prepareJson,
prepareString,
buildPipelineVisFunction,
buildPipeline,
SchemaConfig,
Schemas,
} from './build_pipeline';
import { prepareJson, prepareString, buildPipeline } from './build_pipeline';
import { Vis } from '..';
import { dataPluginMock } from '../../../../plugins/data/public/mocks';
import { parseExpression } from '../../../expressions/common';
@ -74,53 +67,6 @@ describe('visualize loader pipeline helpers: build pipeline', () => {
});
});
describe('buildPipelineVisFunction', () => {
let schemaConfig: SchemaConfig;
let schemasDef: Schemas;
let uiState: any;
beforeEach(() => {
schemaConfig = {
accessor: 0,
label: '',
format: {},
params: {},
aggType: '',
};
schemasDef = { metric: [schemaConfig] };
uiState = {};
});
describe('handles region_map function', () => {
it('without buckets', () => {
const params = { metric: {} };
const actual = buildPipelineVisFunction.region_map(params, schemasDef, uiState);
expect(actual).toMatchSnapshot();
});
it('with buckets', () => {
const schemas = {
...schemasDef,
segment: [1, 2],
};
const actual = buildPipelineVisFunction.region_map({}, schemas, uiState);
expect(actual).toMatchSnapshot();
});
});
it('handles tile_map function', () => {
const params = { metric: {} };
const schemas = {
...schemasDef,
segment: [1, 2],
geo_centroid: [3, 4],
};
const actual = buildPipelineVisFunction.tile_map(params, schemas, uiState);
expect(actual).toMatchSnapshot();
});
});
describe('buildPipeline', () => {
const dataStart = dataPluginMock.createStartContract();

View file

@ -23,9 +23,7 @@ import {
SerializedFieldFormat,
} from '../../../../plugins/expressions/public';
import { IAggConfig, search, TimefilterContract } from '../../../../plugins/data/public';
import { Vis, VisParams } from '../types';
import { Vis } from '../types';
const { isDateHistogramBucketAggConfig } = search.aggs;
interface SchemaConfigParams {
@ -55,25 +53,6 @@ export interface Schemas {
// catch all for schema name
[key: string]: any[] | undefined;
}
type BuildVisFunction = (
params: VisParams,
schemas: Schemas,
uiState: any,
meta?: { savedObjectId?: string }
) => string;
// eslint-disable-next-line @typescript-eslint/naming-convention
type buildVisConfigFunction = (schemas: Schemas, visParams?: VisParams) => VisParams;
interface BuildPipelineVisFunction {
[key: string]: BuildVisFunction;
}
interface BuildVisConfigFunction {
[key: string]: buildVisConfigFunction;
}
export interface BuildPipelineParams {
timefilter: TimefilterContract;
timeRange?: any;
@ -224,43 +203,6 @@ export const prepareDimension = (variable: string, data: any) => {
return expr;
};
export const buildPipelineVisFunction: BuildPipelineVisFunction = {
region_map: (params, schemas) => {
const visConfig = {
...params,
...buildVisConfig.region_map(schemas),
};
return `regionmap ${prepareJson('visConfig', visConfig)}`;
},
tile_map: (params, schemas) => {
const visConfig = {
...params,
...buildVisConfig.tile_map(schemas),
};
return `tilemap ${prepareJson('visConfig', visConfig)}`;
},
};
const buildVisConfig: BuildVisConfigFunction = {
region_map: (schemas) => {
const visConfig = {} as any;
visConfig.metric = schemas.metric[0];
if (schemas.segment) {
visConfig.bucket = schemas.segment[0];
}
return visConfig;
},
tile_map: (schemas) => {
const visConfig = {} as any;
visConfig.dimensions = {
metric: schemas.metric[0],
geohash: schemas.segment ? schemas.segment[0] : null,
geocentroid: schemas.geo_centroid ? schemas.geo_centroid[0] : null,
};
return visConfig;
},
};
export const buildPipeline = async (vis: Vis, params: BuildPipelineParams) => {
const { indexPattern, searchSource } = vis.data;
const query = searchSource!.getField('query');
@ -299,17 +241,8 @@ export const buildPipeline = async (vis: Vis, params: BuildPipelineParams) => {
});
}
pipeline += `| `;
}
const schemas = getSchemas(vis, params);
if (buildPipelineVisFunction[vis.type.name]) {
pipeline += buildPipelineVisFunction[vis.type.name](
{ title, ...vis.params },
schemas,
uiState
);
} else {
const schemas = getSchemas(vis, params);
const visConfig = { ...vis.params };
visConfig.dimensions = schemas;
visConfig.title = title;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View file

@ -1 +1 @@
{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":0},"metric":{"accessor":1,"format":{"id":"number"}}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visType":"region_map"}}
{"as":"region_map_vis","type":"render","value":{"visConfig":{"addTooltip":true,"bucket":{"accessor":0},"colorSchema":"Yellow to Red","isDisplayWarning":true,"legendPosition":"bottomright","mapCenter":[0,0],"mapZoom":2,"metric":{"accessor":1,"format":{"id":"number"}},"outlineWeight":1,"selectedJoinField":{},"selectedLayer":{},"showAllShapes":true,"wms":{}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visType":"region_map"}}

View file

@ -1 +1 @@
{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":0},"metric":{"accessor":1,"format":{"id":"number"}}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visType":"region_map"}}
{"as":"region_map_vis","type":"render","value":{"visConfig":{"addTooltip":true,"bucket":{"accessor":0},"colorSchema":"Yellow to Red","isDisplayWarning":true,"legendPosition":"bottomright","mapCenter":[0,0],"mapZoom":2,"metric":{"accessor":1,"format":{"id":"number"}},"outlineWeight":1,"selectedJoinField":{},"selectedLayer":{},"showAllShapes":true,"wms":{}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visType":"region_map"}}

View file

@ -110,7 +110,7 @@ export default function ({
await expectExpression('partial_test_2', metricExpr, context).toMatchSnapshot()
).toMatchScreenshot();
const regionMapExpr = `regionmap visConfig='{"metric":{"accessor":1,"format":{"id":"number"}},"bucket":{"accessor":0}}'`;
const regionMapExpr = `regionmap visConfig='{"metric":{"accessor":1,"format":{"id":"number"}},"bucket":{"accessor":0},"legendPosition":"bottomright","addTooltip":true,"colorSchema":"Yellow to Red","isDisplayWarning":true,"wms":{},"mapZoom":2,"mapCenter":[0,0],"outlineWeight":1,"showAllShapes":true,"selectedLayer":{},"selectedJoinField":{}}'`;
await (
await expectExpression('partial_test_3', regionMapExpr, context).toMatchSnapshot()
).toMatchScreenshot();