feat(plugin): add plugin-chart-cartodiagram (#25869)

Co-authored-by: Jakob Miksch <jakob@meggsimum.de>
This commit is contained in:
Jan Suleiman
2025-01-06 17:58:03 +01:00
committed by GitHub
parent 5484db34f9
commit a986a61b5f
72 changed files with 8434 additions and 193 deletions

View File

@@ -0,0 +1,48 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF 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 { ChartLayer } from '../../src/components/ChartLayer';
import { ChartLayerOptions } from '../../src/types';
describe('ChartLayer', () => {
it('creates div and loading mask', () => {
const options: ChartLayerOptions = {
chartVizType: 'pie',
};
const chartLayer = new ChartLayer(options);
expect(chartLayer.loadingMask).toBeDefined();
expect(chartLayer.div).toBeDefined();
});
it('can remove chart elements', () => {
const options: ChartLayerOptions = {
chartVizType: 'pie',
};
const chartLayer = new ChartLayer(options);
chartLayer.charts = [
{
htmlElement: document.createElement('div'),
},
];
chartLayer.removeAllChartElements();
expect(chartLayer.charts).toEqual([]);
});
});

View File

@@ -0,0 +1,33 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF 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 { CartodiagramPlugin } from '../src';
/**
* The example tests in this file act as a starting point, and
* we encourage you to build more. These tests check that the
* plugin loads properly, and focus on `transformProps`
* to ake sure that data, controls, and props are all
* treated correctly (e.g. formData from plugin controls
* properly transform the data and/or any resulting props).
*/
describe('CartodiagramPlugin', () => {
it('exists', () => {
expect(CartodiagramPlugin).toBeDefined();
});
});

View File

@@ -0,0 +1,67 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF 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 { getChartBuildQueryRegistry } from '@superset-ui/core';
import buildQuery from '../../src/plugin/buildQuery';
describe('CartodiagramPlugin buildQuery', () => {
const selectedChartParams = {
extra_form_data: {},
groupby: [],
};
const selectedChart = {
viz_type: 'pie',
params: JSON.stringify(selectedChartParams),
};
const formData = {
datasource: '5__table',
granularity_sqla: 'ds',
series: 'foo',
viz_type: 'my_chart',
selected_chart: JSON.stringify(selectedChart),
geom_column: 'geom',
};
let chartQueryBuilderMock: jest.MockedFunction<any>;
beforeEach(() => {
chartQueryBuilderMock = jest.fn();
const registry = getChartBuildQueryRegistry();
registry.registerValue('pie', chartQueryBuilderMock);
});
afterEach(() => {
// remove registered buildQuery
const registry = getChartBuildQueryRegistry();
registry.clear();
});
it('should call the buildQuery function of the referenced chart', () => {
buildQuery(formData);
expect(chartQueryBuilderMock.mock.calls).toHaveLength(1);
});
it('should build groupby with geom in form data', () => {
const expectedParams = { ...selectedChartParams, groupby: ['geom'] };
buildQuery(formData);
expect(chartQueryBuilderMock.mock.calls[0][0]).toEqual(expectedParams);
});
});

View File

@@ -0,0 +1,26 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF 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 CartodiagramPlugin from '../../src/CartodiagramPlugin';
describe('CartodiagramPlugin', () => {
it('exists', () => {
expect(CartodiagramPlugin).toBeDefined();
});
});

View File

@@ -0,0 +1,150 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF 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 {
ChartProps,
getChartTransformPropsRegistry,
supersetTheme,
} from '@superset-ui/core';
import { LayerConf, MapViewConfigs, ZoomConfigs } from '../../src/types';
import transformProps from '../../src/plugin/transformProps';
import {
groupedTimeseriesChartData,
groupedTimeseriesLabelMap,
} from '../testData';
describe('CartodiagramPlugin transformProps', () => {
const chartSize: ZoomConfigs = {
type: 'FIXED',
configs: {
height: 10,
width: 10,
zoom: 1,
},
values: {
1: {
height: 10,
width: 10,
},
},
};
const layerConfigs: LayerConf[] = [
{
type: 'XYZ',
title: 'foo',
url: 'example.com',
},
];
const mapView: MapViewConfigs = {
mode: 'FIT_DATA',
zoom: 1,
latitude: 0,
longitude: 0,
fixedZoom: 1,
fixedLatitude: 0,
fixedLongitude: 0,
};
// only minimal subset of actual params
const selectedChartParams = {
groupby: ['bar'],
x_axis: 'mydate',
};
const selectedChart = {
id: 1,
viz_type: 'pie',
slice_name: 'foo',
params: JSON.stringify(selectedChartParams),
};
const formData = {
viz_type: 'cartodiagram',
geomColumn: 'geom',
selectedChart: JSON.stringify(selectedChart),
chartSize,
layerConfigs,
mapView,
chartBackgroundColor: '#000000',
chartBackgroundBorderRadius: 5,
};
const chartProps = new ChartProps({
formData,
width: 800,
height: 600,
queriesData: [
{
data: groupedTimeseriesChartData,
label_map: groupedTimeseriesLabelMap,
},
],
theme: supersetTheme,
});
let chartTransformPropsPieMock: jest.MockedFunction<any>;
let chartTransformPropsTimeseriesMock: jest.MockedFunction<any>;
beforeEach(() => {
chartTransformPropsPieMock = jest.fn();
chartTransformPropsTimeseriesMock = jest.fn();
const registry = getChartTransformPropsRegistry();
registry.registerValue('pie', chartTransformPropsPieMock);
registry.registerValue(
'echarts_timeseries',
chartTransformPropsTimeseriesMock,
);
});
afterEach(() => {
// remove registered transformProps
const registry = getChartTransformPropsRegistry();
registry.clear();
});
it('should call the transform props function of the referenced chart', () => {
transformProps(chartProps);
expect(chartTransformPropsPieMock).toHaveBeenCalled();
expect(chartTransformPropsTimeseriesMock).not.toHaveBeenCalled();
});
it('should transform chart props for viz', () => {
const transformedProps = transformProps(chartProps);
expect(transformedProps).toEqual(
expect.objectContaining({
width: chartProps.width,
height: chartProps.height,
geomColumn: formData.geomColumn,
selectedChart: expect.objectContaining({
viz_type: selectedChart.viz_type,
params: selectedChartParams,
}),
// The actual test for the created chartConfigs
// will be done in transformPropsUtil.test.ts
chartConfigs: expect.objectContaining({
type: 'FeatureCollection',
}),
chartVizType: selectedChart.viz_type,
chartSize,
layerConfigs,
mapView,
chartBackgroundColor: formData.chartBackgroundColor,
chartBackgroundBorderRadius: formData.chartBackgroundBorderRadius,
}),
);
});
});

View File

@@ -0,0 +1,113 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF 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.
*/
const coord1 = '[1,2]';
const coord2 = '[3,4]';
export const geom1 = `{"type":"Point","coordinates":${coord1}}`;
export const geom2 = `{"type":"Point","coordinates":${coord2}}`;
export const nonTimeSeriesChartData: any = [
{
geom: geom1,
my_value: 'apple',
my_count: 347,
},
{
geom: geom1,
my_value: 'apple',
my_count: 360,
},
{
geom: geom1,
my_value: 'lemon',
my_count: 335,
},
{
geom: geom1,
my_value: 'lemon',
my_count: 333,
},
{
geom: geom1,
my_value: 'lemon',
my_count: 353,
},
{
geom: geom1,
my_value: 'lemon',
my_count: 359,
},
{
geom: geom2,
my_value: 'lemon',
my_count: 347,
},
{
geom: geom2,
my_value: 'apple',
my_count: 335,
},
{
geom: geom2,
my_value: 'apple',
my_count: 356,
},
{
geom: geom2,
my_value: 'banana',
my_count: 218,
},
];
export const timeseriesChartData = [
{
[geom1]: 347,
[geom2]: 360,
mydate: 1564275000000,
},
{
[geom1]: 353,
[geom2]: 328,
mydate: 1564272000000,
},
];
export const groupedTimeseriesChartData = [
{
[`${geom1}, apple`]: 347,
[`${geom2}, apple`]: 360,
[`${geom1}, lemon`]: 352,
[`${geom2}, lemon`]: 364,
mydate: 1564275000000,
},
{
[`${geom1}, apple`]: 353,
[`${geom2}, apple`]: 328,
[`${geom1}, lemon`]: 346,
[`${geom2}, lemon`]: 333,
mydate: 1564272000000,
},
];
export const groupedTimeseriesLabelMap = {
[`${geom1}, apple`]: [geom1, 'apple'],
[`${geom2}, apple`]: [geom2, 'apple'],
[`${geom1}, lemon`]: [geom1, 'lemon'],
[`${geom2}, lemon`]: [geom2, 'lemon'],
};

View File

@@ -0,0 +1,22 @@
{
"compilerOptions": {
"composite": false,
"emitDeclarationOnly": false,
"noEmit": true,
"rootDir": "."
},
"extends": "../../../tsconfig.json",
"include": [
"**/*",
"../types/**/*",
"../../../types/**/*"
],
"references": [
{
"path": "../../../packages/superset-ui-chart-controls"
},
{
"path": "../../../packages/superset-ui-core"
},
]
}

View File

@@ -0,0 +1,77 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF 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 { ChartConfig } from '../../src/types';
import { isChartConfigEqual, simplifyConfig } from '../../src/util/chartUtil';
describe('chartUtil', () => {
const configA: ChartConfig = {
type: 'FeatureCollection',
features: [
{
type: 'Feature',
geometry: {
type: 'Point',
coordinates: [],
},
properties: {
refs: 'foo',
},
},
],
};
const configB: ChartConfig = {
type: 'FeatureCollection',
features: [
{
type: 'Feature',
geometry: {
type: 'Point',
coordinates: [],
},
properties: {
refs: 'foo',
foo: 'bar',
},
},
],
};
describe('simplifyConfig', () => {
it('removes the refs property from a feature', () => {
const simplifiedConfig = simplifyConfig(configA);
const propKeys = Object.keys(simplifiedConfig.features[0].properties);
expect(propKeys).toHaveLength(0);
});
});
describe('isChartConfigEqual', () => {
it('returns true, if configurations are equal', () => {
const isEqual = isChartConfigEqual(configA, structuredClone(configA));
expect(isEqual).toBe(true);
});
it('returns false if configurations are not equal', () => {
const isEqual = isChartConfigEqual(configA, configB);
expect(isEqual).toBe(false);
});
});
});

View File

@@ -0,0 +1,212 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF 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 {
ControlPanelConfig,
CustomControlItem,
} from '@superset-ui/chart-controls';
import {
getLayerConfig,
selectedChartMutator,
} from '../../src/util/controlPanelUtil';
describe('controlPanelUtil', () => {
describe('getLayerConfig', () => {
it('returns the correct layer config', () => {
const layerConfigs: CustomControlItem = {
name: 'layer_configs',
config: {
type: 'dummy',
renderTrigger: true,
label: 'Layers',
default: [
{
type: 'XYZ',
url: 'http://example.com/',
title: 'dummy title',
attribution: 'dummy attribution',
},
],
description: 'The configuration for the map layers',
},
};
const controlPanel: ControlPanelConfig = {
controlPanelSections: [
{
label: 'Configuration',
expanded: true,
controlSetRows: [],
},
{
label: 'Map Options',
expanded: true,
controlSetRows: [
[
{
name: 'map_view',
config: {
type: 'dummy',
},
},
],
[layerConfigs],
],
},
{
label: 'Chart Options',
expanded: true,
controlSetRows: [],
},
],
};
const extractedLayerConfigs = getLayerConfig(controlPanel);
expect(extractedLayerConfigs).toEqual(layerConfigs);
});
});
describe('selectedChartMutator', () => {
it('returns empty array for empty inputs', () => {
const response = {};
const value = undefined;
const result = selectedChartMutator(response, value);
expect(result).toEqual([]);
});
it('returns parsed value if response is empty', () => {
const response = {};
const sliceName = 'foobar';
const value = JSON.stringify({
id: 278,
params: '',
slice_name: sliceName,
viz_type: 'pie',
});
const result = selectedChartMutator(response, value);
expect(result[0].label).toEqual(sliceName);
});
it('returns response options if no value is chosen', () => {
const sliceName1 = 'foo';
const sliceName2 = 'bar';
const response = {
result: [
{
id: 1,
params: '{}',
slice_name: sliceName1,
viz_type: 'viz1',
},
{
id: 2,
params: '{}',
slice_name: sliceName2,
viz_type: 'viz2',
},
],
};
const value = undefined;
const result = selectedChartMutator(response, value);
expect(result[0].label).toEqual(sliceName1);
expect(result[1].label).toEqual(sliceName2);
});
it('returns correct result if id of chosen config does not exist in response', () => {
const response = {
result: [
{
id: 1,
params: '{}',
slice_name: 'foo',
viz_type: 'viz1',
},
{
id: 2,
params: '{}',
slice_name: 'bar',
viz_type: 'viz2',
},
],
};
const value = JSON.stringify({
id: 3,
params: '{}',
slice_name: 'my-slice',
viz_type: 'pie',
});
const result = selectedChartMutator(response, value);
// collect all ids in a set to prevent double entries
const ids = new Set();
result.forEach((item: any) => {
const config = JSON.parse(item.value);
const { id } = config;
ids.add(id);
});
const threeDifferentIds = ids.size === 3;
expect(threeDifferentIds).toEqual(true);
});
it('returns correct result if id of chosen config already exists', () => {
const response = {
result: [
{
id: 1,
params: '{}',
slice_name: 'foo',
viz_type: 'viz1',
},
{
id: 2,
params: '{}',
slice_name: 'bar',
viz_type: 'viz2',
},
],
};
const value = JSON.stringify({
id: 1,
params: '{}',
slice_name: 'my-slice',
viz_type: 'pie',
});
const result = selectedChartMutator(response, value);
const itemsIdWithId1 = result.filter((item: any) => {
const config = JSON.parse(item.value);
const { id } = config;
return id === 1;
});
expect(itemsIdWithId1.length).toEqual(2);
const labelsEqual = itemsIdWithId1[0].label === itemsIdWithId1[1].label;
expect(labelsEqual).toEqual(false);
});
});
});

View File

@@ -0,0 +1,102 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF 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 GeoJSON from 'ol/format/GeoJSON';
import { Point } from 'geojson';
import {
getExtentFromFeatures,
getProjectedCoordinateFromPointGeoJson,
} from '../../src/util/geometryUtil';
import { ChartConfig } from '../../src/types';
describe('geometryUtil', () => {
describe('getProjectedCoordinateFromPointGeoJson', () => {
it('returns a plausible result', () => {
const pointGeoJson: Point = {
type: 'Point',
coordinates: [6.6555, 49.74283],
};
const result = getProjectedCoordinateFromPointGeoJson(pointGeoJson);
expect(result.length).toEqual(2);
const valuesAreNumbers =
!Number.isNaN(result[0]) && !Number.isNaN(result[1]);
expect(valuesAreNumbers).toEqual(true);
});
});
describe('getExtentFromFeatures', () => {
it('computes correct extent with valid input', () => {
const expectedExtent = [1, 2, 3, 4];
const chartConfig: ChartConfig = {
type: 'FeatureCollection',
features: [
{
type: 'Feature',
geometry: {
type: 'Point',
coordinates: [expectedExtent[0], expectedExtent[1]],
},
properties: {
setDataMask: '',
labelMap: '',
labelMapB: '',
groupby: '',
selectedValues: '',
formData: '',
groupbyB: '',
seriesBreakdown: '',
legendData: '',
echartOptions: '',
},
},
{
type: 'Feature',
geometry: {
type: 'Point',
coordinates: [expectedExtent[2], expectedExtent[3]],
},
properties: {
setDataMask: '',
labelMap: '',
labelMapB: '',
groupby: '',
selectedValues: '',
formData: '',
groupbyB: '',
seriesBreakdown: '',
legendData: '',
echartOptions: '',
},
},
],
};
const features = new GeoJSON().readFeatures(chartConfig);
const extent = getExtentFromFeatures(features);
expect(extent).toEqual(expectedExtent);
});
it('returns undefined on invalid input', () => {
const emptyExtent = getExtentFromFeatures([]);
expect(emptyExtent).toBeUndefined();
});
});
});

View File

@@ -0,0 +1,95 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF 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 { WfsLayerConf } from '../../src/types';
import {
createLayer,
createWfsLayer,
createWmsLayer,
createXyzLayer,
} from '../../src/util/layerUtil';
describe('layerUtil', () => {
describe('createWmsLayer', () => {
it('exists', () => {
// function is trivial
expect(createWmsLayer).toBeDefined();
});
});
describe('createWfsLayer', () => {
it('properly applies style', async () => {
const colorToExpect = '#123456';
const wfsLayerConf: WfsLayerConf = {
title: 'osm:osm-fuel',
url: 'https://ows-demo.terrestris.de/geoserver/osm/wfs',
type: 'WFS',
version: '2.0.2',
typeName: 'osm:osm-fuel',
style: {
name: 'Default Style',
rules: [
{
name: 'Default Rule',
symbolizers: [
{
kind: 'Line',
color: '#000000',
width: 2,
},
{
kind: 'Mark',
wellKnownName: 'circle',
color: colorToExpect,
},
{
kind: 'Fill',
color: '#000000',
},
],
},
],
},
};
const wfsLayer = await createWfsLayer(wfsLayerConf);
const style = wfsLayer!.getStyle();
// @ts-ignore
expect(style!.length).toEqual(3);
const colorAtLayer = style![1].getImage().getFill().getColor();
expect(colorToExpect).toEqual(colorAtLayer);
});
});
describe('createXyzLayer', () => {
it('exists', () => {
// function is trivial
expect(createXyzLayer).toBeDefined();
});
});
describe('createLayer', () => {
it('exists', () => {
expect(createLayer).toBeDefined();
});
});
});

View File

@@ -0,0 +1,116 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF 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 Map from 'ol/Map.js';
import OSM from 'ol/source/OSM.js';
import TileLayer from 'ol/layer/Tile.js';
import View from 'ol/View.js';
import { ChartConfig } from '../../src/types';
import { fitMapToCharts } from '../../src/util/mapUtil';
describe('mapUtil', () => {
describe('fitMapToCharts', () => {
it('changes the center of the map', () => {
const chartConfig: ChartConfig = {
type: 'FeatureCollection',
features: [
{
type: 'Feature',
geometry: {
type: 'Point',
coordinates: [8.793, 53.04117],
},
properties: {
setDataMask: '',
labelMap: '',
labelMapB: '',
groupby: '',
selectedValues: '',
formData: '',
groupbyB: '',
seriesBreakdown: '',
legendData: '',
echartOptions: '',
},
},
{
type: 'Feature',
geometry: {
type: 'Point',
coordinates: [10.61833, 51.8],
},
properties: {
setDataMask: '',
labelMap: '',
labelMapB: '',
groupby: '',
selectedValues: '',
formData: '',
groupbyB: '',
seriesBreakdown: '',
legendData: '',
echartOptions: '',
},
},
{
type: 'Feature',
geometry: {
type: 'Point',
coordinates: [6.86883, 50.35667],
},
properties: {
setDataMask: '',
labelMap: '',
labelMapB: '',
groupby: '',
selectedValues: '',
formData: '',
groupbyB: '',
seriesBreakdown: '',
legendData: '',
echartOptions: '',
},
},
],
};
const initialCenter = [0, 0];
const olMap = new Map({
layers: [
new TileLayer({
source: new OSM(),
}),
],
target: 'map',
view: new View({
center: initialCenter,
zoom: 2,
}),
});
// should set center
fitMapToCharts(olMap, chartConfig);
const updatedCenter = olMap.getView().getCenter();
expect(initialCenter).not.toEqual(updatedCenter);
});
});
});

View File

@@ -0,0 +1,46 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF 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 { isVersionBelow } from '../../src/util/serviceUtil';
describe('serviceUtil', () => {
describe('isVersionBelow', () => {
describe('WMS', () => {
it('recognizes the higher version', () => {
const result = isVersionBelow('1.3.0', '1.1.0', 'WMS');
expect(result).toEqual(false);
});
it('recognizes the lower version', () => {
const result = isVersionBelow('1.1.1', '1.3.0', 'WMS');
expect(result).toEqual(true);
});
});
describe('WFS', () => {
it('recognizes the higher version', () => {
const result = isVersionBelow('2.0.2', '1.1.0', 'WFS');
expect(result).toEqual(false);
});
it('recognizes the lower version', () => {
const result = isVersionBelow('1.1.0', '2.0.2', 'WFS');
expect(result).toEqual(true);
});
});
});
});

View File

@@ -0,0 +1,249 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF 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 {
groupByLocation,
getChartConfigs,
parseSelectedChart,
getGeojsonColumns,
createColumnName,
groupByLocationGenericX,
stripGeomFromColnamesAndTypes,
stripGeomColumnFromLabelMap,
} from '../../src/util/transformPropsUtil';
import {
nonTimeSeriesChartData,
groupedTimeseriesChartData,
geom1,
geom2,
groupedTimeseriesLabelMap,
} from '../testData';
describe('transformPropsUtil', () => {
const groupedTimeseriesParams = {
x_axis: 'mydate',
};
const groupedTimeseriesQueryData = {
label_map: groupedTimeseriesLabelMap,
};
describe('getGeojsonColumns', () => {
it('gets the GeoJSON columns', () => {
const columns = ['foo', 'bar', geom1];
const result = getGeojsonColumns(columns);
expect(result).toHaveLength(1);
expect(result[0]).toEqual(2);
});
it('gets multiple GeoJSON columns', () => {
const columns = ['foo', geom2, 'bar', geom1];
const result = getGeojsonColumns(columns);
expect(result).toHaveLength(2);
expect(result[0]).toEqual(1);
expect(result[1]).toEqual(3);
});
it('returns empty array when no GeoJSON is included', () => {
const columns = ['foo', 'bar'];
const result = getGeojsonColumns(columns);
expect(result).toHaveLength(0);
});
});
describe('createColumnName', () => {
it('creates a columns name', () => {
const columns = ['foo', 'bar'];
const result = createColumnName(columns, []);
expect(result).toEqual('foo, bar');
});
it('ignores items provided by ignoreIdx', () => {
const columns = ['foo', 'bar', 'baz'];
const ignoreIdx = [1];
const result = createColumnName(columns, ignoreIdx);
expect(result).toEqual('foo, baz');
});
});
describe('groupByLocationGenericX', () => {
it('groups in the correct count of geometries', () => {
const result = groupByLocationGenericX(
groupedTimeseriesChartData,
groupedTimeseriesParams,
groupedTimeseriesQueryData,
);
const countOfGeometries = Object.keys(result).length;
expect(countOfGeometries).toEqual(2);
});
it('groups items by same geometry', () => {
const result = groupByLocationGenericX(
groupedTimeseriesChartData,
groupedTimeseriesParams,
groupedTimeseriesQueryData,
);
const allGeom1 = result[geom1].length === 2;
const allGeom2 = result[geom2].length === 2;
expect(allGeom1 && allGeom2).toBe(true);
});
});
describe('groupByLocation', () => {
it('groups in the correct count of geometries', () => {
const geometryColumn = 'geom';
const result = groupByLocation(nonTimeSeriesChartData, geometryColumn);
const countOfGeometries = Object.keys(result).length;
expect(countOfGeometries).toEqual(2);
});
it('groups items by same geometry', () => {
const geometryColumn = 'geom';
const result = groupByLocation(nonTimeSeriesChartData, geometryColumn);
const allGeom1 = result[geom1].length === 6;
const allGeom2 = result[geom2].length === 4;
expect(allGeom1 && allGeom2).toBe(true);
});
});
describe('stripGeomFromColnamesAndTypes', () => {
it('strips the geom from colnames with geom column', () => {
const queryData = {
colnames: ['foo', 'geom'],
coltypes: [0, 0],
};
const result = stripGeomFromColnamesAndTypes(queryData, 'geom');
expect(result).toEqual({
colnames: ['foo'],
coltypes: [0],
});
});
it('strips the geom from colnames with grouped columns', () => {
const queryData = {
colnames: ['foo', `bar, ${geom1}`],
coltypes: [0, 0],
};
const result = stripGeomFromColnamesAndTypes(queryData, 'geom');
expect(result).toEqual({
colnames: ['foo', 'bar'],
coltypes: [0, 0],
});
});
it('strips the geom from colnames with grouped columns without geom', () => {
const queryData = {
colnames: ['foo', `bar, baz`],
coltypes: [0, 0],
};
const result = stripGeomFromColnamesAndTypes(queryData, 'geom');
expect(result).toEqual({
colnames: ['foo', 'bar, baz'],
coltypes: [0, 0],
});
});
});
describe('stripGeomColumnFromLabelMap', () => {
it('strips the geom column from label_map', () => {
const labelMap = {
[`apple, ${geom1}`]: ['apple', geom1],
[`${geom2}, lemon`]: [geom2, 'lemon'],
geom: ['geom'],
};
const result = stripGeomColumnFromLabelMap(labelMap, 'geom');
expect(result).toEqual({
apple: ['apple'],
lemon: ['lemon'],
});
});
});
describe('getChartConfigs', () => {
let chartTransformer: jest.MockedFunction<any>;
const geomColumn = 'geom';
const pieChartConfig = {
params: {},
viz_type: 'pie',
};
const chartProps: any = {
queriesData: [
{
data: nonTimeSeriesChartData,
},
],
};
beforeEach(() => {
chartTransformer = jest.fn();
});
it('calls the transformProps function for every location', () => {
getChartConfigs(pieChartConfig, geomColumn, chartProps, chartTransformer);
expect(chartTransformer).toHaveBeenCalledTimes(2);
});
it('returns a geojson', () => {
const result = getChartConfigs(
pieChartConfig,
geomColumn,
chartProps,
chartTransformer,
);
expect(result).toEqual(
expect.objectContaining({
type: 'FeatureCollection',
features: expect.arrayContaining([
expect.objectContaining({
type: 'Feature',
}),
]),
}),
);
});
it('returns a feature for each location', () => {
const result = getChartConfigs(
pieChartConfig,
geomColumn,
chartProps,
chartTransformer,
);
expect(result.features).toHaveLength(2);
expect(result.features[0].geometry).toEqual(JSON.parse(geom1));
expect(result.features[1].geometry).toEqual(JSON.parse(geom2));
});
});
describe('parseSelectedChart', () => {
it('parses the inline stringified JSON', () => {
const selectedChartObject = {
id: 278,
params:
'{"adhoc_filters":[],"applied_time_extras":{},"datasource":"24__table","viz_type":"pie","time_range":"No filter","groupby":["nuclide"],"metric":{"expressionType":"SIMPLE","column":{"advanced_data_type":null,"certification_details":null,"certified_by":null,"column_name":"nuclide","description":null,"expression":null,"filterable":true,"groupby":true,"id":772,"is_certified":false,"is_dttm":false,"python_date_format":null,"type":"TEXT","type_generic":1,"verbose_name":null,"warning_markdown":null},"aggregate":"COUNT","sqlExpression":null,"isNew":false,"datasourceWarning":false,"hasCustomLabel":false,"label":"COUNT(nuclide)","optionName":"metric_k6d9mt9zujc_7v9szd1i0pl"},"dashboards":[]}',
slice_name: 'pie',
viz_type: 'pie',
};
const selectedChartString = JSON.stringify(selectedChartObject);
const result = parseSelectedChart(selectedChartString);
const expectedParams = JSON.parse(selectedChartObject.params);
expect(result.params).toEqual(expectedParams);
});
});
});