mirror of
https://github.com/apache/superset.git
synced 2026-04-19 08:04:53 +00:00
feat(deckgl): add cross-filters to deck.gl charts (#33789)
This commit is contained in:
18
superset-frontend/package-lock.json
generated
18
superset-frontend/package-lock.json
generated
@@ -14976,6 +14976,13 @@
|
||||
"integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/ngeohash": {
|
||||
"version": "0.6.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/ngeohash/-/ngeohash-0.6.8.tgz",
|
||||
"integrity": "sha512-A90x3HMwE1yXbWCnd0ztHzv8rAQPjwTzX2diYI/6OrWm/3oairDaehw5WPWJFgZ+8+J/OuF99IbipmMa2le6tQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.15.30",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.30.tgz",
|
||||
@@ -38972,6 +38979,15 @@
|
||||
"node": ">= 0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ngeohash": {
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/ngeohash/-/ngeohash-0.6.3.tgz",
|
||||
"integrity": "sha512-kltF0cOxgx1AbmVzKxYZaoB0aj7mOxZeHaerEtQV0YaqnkXNq26WWqMmJ6lTqShYxVRWZ/mwvvTrNeOwdslWiw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=v0.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/nise": {
|
||||
"version": "6.1.1",
|
||||
"resolved": "https://registry.npmjs.org/nise/-/nise-6.1.1.tgz",
|
||||
@@ -59828,6 +59844,7 @@
|
||||
"d3-scale": "^3.0.0",
|
||||
"lodash": "^4.17.21",
|
||||
"mousetrap": "^1.6.5",
|
||||
"ngeohash": "^0.6.3",
|
||||
"prop-types": "^15.8.1",
|
||||
"underscore": "^1.13.7",
|
||||
"urijs": "^1.19.11",
|
||||
@@ -59835,6 +59852,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/mapbox__geojson-extent": "^1.0.3",
|
||||
"@types/ngeohash": "^0.6.8",
|
||||
"@types/underscore": "^1.13.0",
|
||||
"@types/urijs": "^1.19.25"
|
||||
},
|
||||
|
||||
@@ -55,7 +55,11 @@ export enum AppSection {
|
||||
Embedded = 'EMBEDDED',
|
||||
}
|
||||
|
||||
export type FilterState = { value?: any; [key: string]: any };
|
||||
export type FilterState = {
|
||||
value?: any;
|
||||
customColumnLabel?: string;
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
export type DataMask = {
|
||||
extraFormData?: ExtraFormData;
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
"d3-scale": "^3.0.0",
|
||||
"lodash": "^4.17.21",
|
||||
"mousetrap": "^1.6.5",
|
||||
"ngeohash": "^0.6.3",
|
||||
"prop-types": "^15.8.1",
|
||||
"underscore": "^1.13.7",
|
||||
"urijs": "^1.19.11",
|
||||
@@ -45,6 +46,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/mapbox__geojson-extent": "^1.0.3",
|
||||
"@types/ngeohash": "^0.6.8",
|
||||
"@types/underscore": "^1.13.0",
|
||||
"@types/urijs": "^1.19.25"
|
||||
},
|
||||
|
||||
@@ -28,10 +28,12 @@ import { memo, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import {
|
||||
CategoricalColorNamespace,
|
||||
Datasource,
|
||||
FilterState,
|
||||
HandlerFunction,
|
||||
JsonObject,
|
||||
JsonValue,
|
||||
QueryFormData,
|
||||
SetDataMaskHook,
|
||||
} from '@superset-ui/core';
|
||||
import type { Layer } from '@deck.gl/core';
|
||||
import Legend from './components/Legend';
|
||||
@@ -44,7 +46,7 @@ import {
|
||||
DeckGLContainerStyledWrapper,
|
||||
} from './DeckGLContainer';
|
||||
import { Point } from './types';
|
||||
import { getLayerType } from './factory';
|
||||
import { GetLayerType } from './factory';
|
||||
import { TooltipProps } from './components/Tooltip';
|
||||
|
||||
const { getScale } = CategoricalColorNamespace;
|
||||
@@ -78,10 +80,14 @@ export type CategoricalDeckGLContainerProps = {
|
||||
height: number;
|
||||
width: number;
|
||||
viewport: Viewport;
|
||||
getLayer: getLayerType<unknown>;
|
||||
getLayer: GetLayerType<unknown>;
|
||||
payload: JsonObject;
|
||||
onAddFilter?: HandlerFunction;
|
||||
setControlValue: (control: string, value: JsonValue) => void;
|
||||
filterState: FilterState;
|
||||
setDataMask: SetDataMaskHook;
|
||||
onContextMenu: HandlerFunction;
|
||||
emitCrossFilters: boolean;
|
||||
};
|
||||
|
||||
const CategoricalDeckGLContainer = (props: CategoricalDeckGLContainerProps) => {
|
||||
@@ -146,7 +152,16 @@ const CategoricalDeckGLContainer = (props: CategoricalDeckGLContainerProps) => {
|
||||
}, []);
|
||||
|
||||
const getLayers = useCallback(() => {
|
||||
const { getLayer, payload, formData: fd, onAddFilter } = props;
|
||||
const {
|
||||
getLayer,
|
||||
payload,
|
||||
formData: fd,
|
||||
onAddFilter,
|
||||
onContextMenu,
|
||||
filterState,
|
||||
setDataMask,
|
||||
emitCrossFilters,
|
||||
} = props;
|
||||
let features = payload.data.features ? [...payload.data.features] : [];
|
||||
|
||||
// Add colors from categories or fixed color
|
||||
@@ -169,13 +184,17 @@ const CategoricalDeckGLContainer = (props: CategoricalDeckGLContainerProps) => {
|
||||
};
|
||||
|
||||
return [
|
||||
getLayer(
|
||||
fd,
|
||||
filteredPayload,
|
||||
getLayer({
|
||||
formData: fd,
|
||||
payload: filteredPayload,
|
||||
onAddFilter,
|
||||
setTooltip,
|
||||
props.datasource,
|
||||
) as Layer,
|
||||
datasource: props.datasource,
|
||||
onContextMenu,
|
||||
filterState,
|
||||
setDataMask,
|
||||
emitCrossFilters,
|
||||
}) as Layer,
|
||||
];
|
||||
}, [addColor, categories, props, setTooltip]);
|
||||
|
||||
|
||||
@@ -24,10 +24,12 @@ import {
|
||||
forwardRef,
|
||||
memo,
|
||||
ReactNode,
|
||||
MouseEvent,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useState,
|
||||
useRef,
|
||||
} from 'react';
|
||||
import { isEqual } from 'lodash';
|
||||
import { StaticMap } from 'react-map-gl';
|
||||
@@ -58,6 +60,14 @@ export const DeckGLContainer = memo(
|
||||
const [lastUpdate, setLastUpdate] = useState<number | null>(null);
|
||||
const [viewState, setViewState] = useState(props.viewport);
|
||||
const prevViewport = usePrevious(props.viewport);
|
||||
const glContextRef = useRef<WebGL2RenderingContext | null>(null);
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
glContextRef.current?.getExtension('WEBGL_lose_context')?.loseContext();
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
useImperativeHandle(ref, () => ({ setTooltip }), []);
|
||||
|
||||
@@ -106,7 +116,13 @@ export const DeckGLContainer = memo(
|
||||
|
||||
return (
|
||||
<>
|
||||
<div style={{ position: 'relative', width, height }}>
|
||||
<div
|
||||
style={{ position: 'relative', width, height }}
|
||||
onContextMenu={(e: MouseEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<DeckGL
|
||||
controller
|
||||
width={width}
|
||||
@@ -114,6 +130,9 @@ export const DeckGLContainer = memo(
|
||||
layers={layers()}
|
||||
viewState={viewState}
|
||||
onViewStateChange={onViewStateChange}
|
||||
onAfterRender={context => {
|
||||
glContextRef.current = context.gl;
|
||||
}}
|
||||
>
|
||||
<StaticMap
|
||||
preserveDrawingBuffer
|
||||
|
||||
@@ -25,6 +25,11 @@ import {
|
||||
JsonObject,
|
||||
HandlerFunction,
|
||||
usePrevious,
|
||||
SetDataMaskHook,
|
||||
DataMask,
|
||||
FilterState,
|
||||
JsonValue,
|
||||
ContextMenuFilters,
|
||||
} from '@superset-ui/core';
|
||||
|
||||
import {
|
||||
@@ -36,37 +41,56 @@ import fitViewport, { Viewport } from './utils/fitViewport';
|
||||
import { Point } from './types';
|
||||
import { TooltipProps } from './components/Tooltip';
|
||||
|
||||
type deckGLComponentProps = {
|
||||
type DeckGLComponentProps = {
|
||||
datasource: Datasource;
|
||||
formData: QueryFormData;
|
||||
height: number;
|
||||
onAddFilter: HandlerFunction;
|
||||
onContextMenu: HandlerFunction;
|
||||
payload: JsonObject;
|
||||
setControlValue: () => void;
|
||||
viewport: Viewport;
|
||||
width: number;
|
||||
filterState: FilterState;
|
||||
setDataMask: SetDataMaskHook;
|
||||
emitCrossFilters: boolean;
|
||||
};
|
||||
export interface getLayerType<T> {
|
||||
(
|
||||
formData: QueryFormData,
|
||||
payload: JsonObject,
|
||||
onAddFilter: HandlerFunction | undefined,
|
||||
setTooltip: (tooltip: TooltipProps['tooltip']) => void,
|
||||
datasource?: Datasource,
|
||||
): T;
|
||||
|
||||
export interface GetLayerTypeParams {
|
||||
formData: QueryFormData;
|
||||
payload: JsonObject;
|
||||
onAddFilter?: HandlerFunction;
|
||||
setTooltip: (tooltip: TooltipProps['tooltip']) => void;
|
||||
setDataMask?: (dataMask: DataMask) => void;
|
||||
onContextMenu?: (
|
||||
clientX: number,
|
||||
clientY: number,
|
||||
filters?: ContextMenuFilters,
|
||||
) => void;
|
||||
datasource?: Datasource;
|
||||
filterState?: FilterState;
|
||||
selected?: JsonObject[];
|
||||
onSelect?: (value: JsonValue) => void;
|
||||
emitCrossFilters?: boolean;
|
||||
}
|
||||
interface getPointsType {
|
||||
|
||||
export interface GetLayerType<T> {
|
||||
(params: GetLayerTypeParams): T;
|
||||
}
|
||||
|
||||
interface GetPointsType {
|
||||
(data: JsonObject[]): Point[];
|
||||
}
|
||||
|
||||
export function createDeckGLComponent(
|
||||
getLayer: getLayerType<unknown>,
|
||||
getPoints: getPointsType,
|
||||
getLayer: GetLayerType<unknown>,
|
||||
getPoints: GetPointsType,
|
||||
) {
|
||||
// Higher order component
|
||||
return memo((props: deckGLComponentProps) => {
|
||||
return memo((props: DeckGLComponentProps) => {
|
||||
const containerRef = useRef<DeckGLContainerHandle>();
|
||||
const prevFormData = usePrevious(props.formData);
|
||||
const prevFilterState = usePrevious(props.filterState);
|
||||
const prevPayload = usePrevious(props.payload);
|
||||
const getAdjustedViewport = () => {
|
||||
const { width, height, formData } = props;
|
||||
@@ -90,10 +114,27 @@ export function createDeckGLComponent(
|
||||
}, []);
|
||||
|
||||
const computeLayer = useCallback(
|
||||
(props: deckGLComponentProps) => {
|
||||
const { formData, payload, onAddFilter } = props;
|
||||
(props: DeckGLComponentProps) => {
|
||||
const {
|
||||
formData,
|
||||
payload,
|
||||
onAddFilter,
|
||||
filterState,
|
||||
setDataMask,
|
||||
onContextMenu,
|
||||
emitCrossFilters,
|
||||
} = props;
|
||||
|
||||
return getLayer(formData, payload, onAddFilter, setTooltip) as Layer;
|
||||
return getLayer({
|
||||
formData,
|
||||
payload,
|
||||
onAddFilter,
|
||||
setTooltip,
|
||||
setDataMask,
|
||||
onContextMenu,
|
||||
filterState,
|
||||
emitCrossFilters,
|
||||
}) as Layer;
|
||||
},
|
||||
[setTooltip],
|
||||
);
|
||||
@@ -102,12 +143,20 @@ export function createDeckGLComponent(
|
||||
|
||||
useEffect(() => {
|
||||
// Only recompute the layer if anything BUT the viewport has changed
|
||||
const prevFdNoVP = { ...prevFormData, viewport: null };
|
||||
const currFdNoVP = { ...props.formData, viewport: null };
|
||||
const prevFdNoVP = {
|
||||
...prevFormData,
|
||||
...prevFilterState,
|
||||
viewport: null,
|
||||
};
|
||||
const currFdNoVP = {
|
||||
...props.formData,
|
||||
...props.filterState,
|
||||
viewport: null,
|
||||
};
|
||||
if (!isEqual(prevFdNoVP, currFdNoVP) || prevPayload !== props.payload) {
|
||||
setLayer(computeLayer(props));
|
||||
}
|
||||
}, [computeLayer, prevFormData, prevPayload, props]);
|
||||
}, [computeLayer, prevFormData, prevFilterState, prevPayload, props]);
|
||||
|
||||
const { formData, payload, setControlValue, height, width } = props;
|
||||
|
||||
@@ -128,10 +177,10 @@ export function createDeckGLComponent(
|
||||
}
|
||||
|
||||
export function createCategoricalDeckGLComponent(
|
||||
getLayer: getLayerType<Layer>,
|
||||
getPoints: getPointsType,
|
||||
getLayer: GetLayerType<Layer>,
|
||||
getPoints: GetPointsType,
|
||||
) {
|
||||
return function Component(props: deckGLComponentProps) {
|
||||
return function Component(props: DeckGLComponentProps) {
|
||||
const {
|
||||
datasource,
|
||||
formData,
|
||||
@@ -140,6 +189,10 @@ export function createCategoricalDeckGLComponent(
|
||||
setControlValue,
|
||||
viewport,
|
||||
width,
|
||||
setDataMask,
|
||||
filterState,
|
||||
onContextMenu,
|
||||
emitCrossFilters,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
@@ -154,6 +207,10 @@ export function createCategoricalDeckGLComponent(
|
||||
getPoints={getPoints}
|
||||
width={width}
|
||||
height={height}
|
||||
setDataMask={setDataMask}
|
||||
onContextMenu={onContextMenu}
|
||||
filterState={filterState}
|
||||
emitCrossFilters={emitCrossFilters}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -17,16 +17,10 @@
|
||||
* under the License.
|
||||
*/
|
||||
import { ArcLayer } from '@deck.gl/layers';
|
||||
import {
|
||||
HandlerFunction,
|
||||
JsonObject,
|
||||
QueryFormData,
|
||||
t,
|
||||
} from '@superset-ui/core';
|
||||
import { JsonObject, QueryFormData, t } from '@superset-ui/core';
|
||||
import { commonLayerProps } from '../common';
|
||||
import { createCategoricalDeckGLComponent } from '../../factory';
|
||||
import { GetLayerType, createCategoricalDeckGLComponent } from '../../factory';
|
||||
import TooltipRow from '../../TooltipRow';
|
||||
import { TooltipProps } from '../../components/Tooltip';
|
||||
import { Point } from '../../types';
|
||||
|
||||
export function getPoints(data: JsonObject[]) {
|
||||
@@ -60,12 +54,16 @@ function setTooltipContent(formData: QueryFormData) {
|
||||
);
|
||||
}
|
||||
|
||||
export function getLayer(
|
||||
fd: QueryFormData,
|
||||
payload: JsonObject,
|
||||
onAddFilter: HandlerFunction,
|
||||
setTooltip: (tooltip: TooltipProps['tooltip']) => void,
|
||||
) {
|
||||
export const getLayer: GetLayerType<ArcLayer> = function ({
|
||||
formData,
|
||||
payload,
|
||||
setTooltip,
|
||||
filterState,
|
||||
setDataMask,
|
||||
onContextMenu,
|
||||
emitCrossFilters,
|
||||
}) {
|
||||
const fd = formData;
|
||||
const data = payload.data.features;
|
||||
const sc = fd.color_picker;
|
||||
const tc = fd.target_color_picker;
|
||||
@@ -78,8 +76,16 @@ export function getLayer(
|
||||
d.targetColor || d.color || [tc.r, tc.g, tc.b, 255 * tc.a],
|
||||
id: `path-layer-${fd.slice_id}` as const,
|
||||
getWidth: fd.stroke_width ? fd.stroke_width : 3,
|
||||
...commonLayerProps(fd, setTooltip, setTooltipContent(fd)),
|
||||
...commonLayerProps({
|
||||
formData: fd,
|
||||
setTooltip,
|
||||
setTooltipContent: setTooltipContent(fd),
|
||||
onContextMenu,
|
||||
setDataMask,
|
||||
filterState,
|
||||
emitCrossFilters,
|
||||
}),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default createCategoricalDeckGLComponent(getLayer, getPoints);
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { t, ChartMetadata, ChartPlugin } from '@superset-ui/core';
|
||||
import { t, ChartMetadata, ChartPlugin, Behavior } from '@superset-ui/core';
|
||||
import thumbnail from './images/thumbnail.png';
|
||||
import example from './images/example.png';
|
||||
import transformProps from '../../transformProps';
|
||||
@@ -25,6 +25,11 @@ import controlPanel from './controlPanel';
|
||||
const metadata = new ChartMetadata({
|
||||
category: t('Map'),
|
||||
credits: ['https://uber.github.io/deck.gl'],
|
||||
behaviors: [
|
||||
Behavior.InteractiveChart,
|
||||
Behavior.DrillBy,
|
||||
Behavior.DrillToDetail,
|
||||
],
|
||||
description: t(
|
||||
'Plot the distance (like flight paths) between origin and destination.',
|
||||
),
|
||||
|
||||
@@ -21,7 +21,7 @@ import { Position } from '@deck.gl/core';
|
||||
import { t } from '@superset-ui/core';
|
||||
import { commonLayerProps } from '../common';
|
||||
import sandboxedEval from '../../utils/sandbox';
|
||||
import { createDeckGLComponent, getLayerType } from '../../factory';
|
||||
import { GetLayerType, createDeckGLComponent } from '../../factory';
|
||||
import { ColorType } from '../../types';
|
||||
import TooltipRow from '../../TooltipRow';
|
||||
|
||||
@@ -39,12 +39,15 @@ function setTooltipContent(o: any) {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export const getLayer: getLayerType<unknown> = function (
|
||||
export const getLayer: GetLayerType<ContourLayer> = function ({
|
||||
formData,
|
||||
payload,
|
||||
onAddFilter,
|
||||
filterState,
|
||||
setDataMask,
|
||||
onContextMenu,
|
||||
setTooltip,
|
||||
) {
|
||||
emitCrossFilters,
|
||||
}) {
|
||||
const fd = formData;
|
||||
const {
|
||||
aggregation = 'SUM',
|
||||
@@ -93,7 +96,15 @@ export const getLayer: getLayerType<unknown> = function (
|
||||
getPosition: (d: { position: number[]; weight: number }) =>
|
||||
d.position as Position,
|
||||
getWeight: (d: { weight: number }) => d.weight || 0,
|
||||
...commonLayerProps(fd, setTooltip, setTooltipContent),
|
||||
...commonLayerProps({
|
||||
formData: fd,
|
||||
setTooltip,
|
||||
setTooltipContent,
|
||||
onContextMenu,
|
||||
setDataMask,
|
||||
filterState,
|
||||
emitCrossFilters,
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { t, ChartMetadata, ChartPlugin } from '@superset-ui/core';
|
||||
import { t, ChartMetadata, ChartPlugin, Behavior } from '@superset-ui/core';
|
||||
import transformProps from '../../transformProps';
|
||||
import controlPanel from './controlPanel';
|
||||
import thumbnail from './images/thumbnail.png';
|
||||
@@ -33,6 +33,7 @@ const metadata = new ChartMetadata({
|
||||
thumbnail,
|
||||
useLegacyApi: true,
|
||||
tags: [t('deckGL'), t('Spatial'), t('Comparison')],
|
||||
behaviors: [Behavior.InteractiveChart],
|
||||
});
|
||||
|
||||
export default class ContourChartPlugin extends ChartPlugin {
|
||||
|
||||
@@ -23,10 +23,12 @@ import { GeoJsonLayer } from '@deck.gl/layers';
|
||||
import { Feature, Geometry, GeoJsonProperties } from 'geojson';
|
||||
import geojsonExtent from '@mapbox/geojson-extent';
|
||||
import {
|
||||
FilterState,
|
||||
HandlerFunction,
|
||||
JsonObject,
|
||||
JsonValue,
|
||||
QueryFormData,
|
||||
SetDataMaskHook,
|
||||
} from '@superset-ui/core';
|
||||
|
||||
import {
|
||||
@@ -40,6 +42,7 @@ import TooltipRow from '../../TooltipRow';
|
||||
import fitViewport, { Viewport } from '../../utils/fitViewport';
|
||||
import { TooltipProps } from '../../components/Tooltip';
|
||||
import { Point } from '../../types';
|
||||
import { GetLayerType } from '../../factory';
|
||||
|
||||
type ProcessedFeature = Feature<Geometry, GeoJsonProperties> & {
|
||||
properties: JsonObject;
|
||||
@@ -119,12 +122,15 @@ function setTooltipContent(o: JsonObject) {
|
||||
const getFillColor = (feature: JsonObject) => feature?.properties?.fillColor;
|
||||
const getLineColor = (feature: JsonObject) => feature?.properties?.strokeColor;
|
||||
|
||||
export function getLayer(
|
||||
formData: QueryFormData,
|
||||
payload: JsonObject,
|
||||
onAddFilter: HandlerFunction,
|
||||
setTooltip: (tooltip: TooltipProps['tooltip']) => void,
|
||||
) {
|
||||
export const getLayer: GetLayerType<GeoJsonLayer> = function ({
|
||||
formData,
|
||||
onContextMenu,
|
||||
filterState,
|
||||
setDataMask,
|
||||
payload,
|
||||
setTooltip,
|
||||
emitCrossFilters,
|
||||
}) {
|
||||
const fd = formData;
|
||||
const fc = fd.fill_color_picker;
|
||||
const sc = fd.stroke_color_picker;
|
||||
@@ -159,9 +165,17 @@ export function getLayer(
|
||||
getLineWidth: fd.line_width || 1,
|
||||
pointRadiusScale: fd.point_radius_scale,
|
||||
lineWidthUnits: fd.line_width_unit,
|
||||
...commonLayerProps(fd, setTooltip, setTooltipContent),
|
||||
...commonLayerProps({
|
||||
formData: fd,
|
||||
setTooltip,
|
||||
setTooltipContent,
|
||||
setDataMask,
|
||||
filterState,
|
||||
onContextMenu,
|
||||
emitCrossFilters,
|
||||
}),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export type DeckGLGeoJsonProps = {
|
||||
formData: QueryFormData;
|
||||
@@ -171,6 +185,9 @@ export type DeckGLGeoJsonProps = {
|
||||
onAddFilter: HandlerFunction;
|
||||
height: number;
|
||||
width: number;
|
||||
filterState: FilterState;
|
||||
onContextMenu: HandlerFunction;
|
||||
setDataMask: SetDataMaskHook;
|
||||
};
|
||||
|
||||
export function getPoints(data: Point[]) {
|
||||
@@ -217,7 +234,15 @@ const DeckGLGeoJson = (props: DeckGLGeoJsonProps) => {
|
||||
width,
|
||||
]);
|
||||
|
||||
const layer = getLayer(formData, payload, onAddFilter, setTooltip);
|
||||
const layer = getLayer({
|
||||
onContextMenu: props.onContextMenu,
|
||||
filterState: props.filterState,
|
||||
setDataMask: props.setDataMask,
|
||||
setTooltip,
|
||||
onAddFilter,
|
||||
payload,
|
||||
formData,
|
||||
});
|
||||
|
||||
return (
|
||||
<DeckGLContainerStyledWrapper
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { t, ChartMetadata, ChartPlugin } from '@superset-ui/core';
|
||||
import { t, ChartMetadata, ChartPlugin, Behavior } from '@superset-ui/core';
|
||||
import thumbnail from './images/thumbnail.png';
|
||||
import example from './images/example.png';
|
||||
import transformProps from '../../transformProps';
|
||||
@@ -33,6 +33,7 @@ const metadata = new ChartMetadata({
|
||||
thumbnail,
|
||||
useLegacyApi: true,
|
||||
tags: [t('deckGL'), t('2D')],
|
||||
behaviors: [Behavior.InteractiveChart],
|
||||
});
|
||||
|
||||
export default class GeojsonChartPlugin extends ChartPlugin {
|
||||
|
||||
@@ -18,19 +18,13 @@
|
||||
*/
|
||||
import { Color } from '@deck.gl/core';
|
||||
import { GridLayer } from '@deck.gl/aggregation-layers';
|
||||
import {
|
||||
t,
|
||||
CategoricalColorNamespace,
|
||||
JsonObject,
|
||||
QueryFormData,
|
||||
} from '@superset-ui/core';
|
||||
import { t, CategoricalColorNamespace, JsonObject } from '@superset-ui/core';
|
||||
|
||||
import { commonLayerProps, getAggFunc } from '../common';
|
||||
import sandboxedEval from '../../utils/sandbox';
|
||||
import { hexToRGB } from '../../utils/colors';
|
||||
import { createDeckGLComponent } from '../../factory';
|
||||
import { createDeckGLComponent, GetLayerType } from '../../factory';
|
||||
import TooltipRow from '../../TooltipRow';
|
||||
import { TooltipProps } from '../../components/Tooltip';
|
||||
|
||||
function setTooltipContent(o: JsonObject) {
|
||||
return (
|
||||
@@ -49,12 +43,15 @@ function setTooltipContent(o: JsonObject) {
|
||||
);
|
||||
}
|
||||
|
||||
export function getLayer(
|
||||
formData: QueryFormData,
|
||||
payload: JsonObject,
|
||||
onAddFilter: () => void,
|
||||
setTooltip: (tooltip: TooltipProps['tooltip']) => void,
|
||||
) {
|
||||
export const getLayer: GetLayerType<GridLayer> = function ({
|
||||
formData,
|
||||
payload,
|
||||
setTooltip,
|
||||
setDataMask,
|
||||
onContextMenu,
|
||||
filterState,
|
||||
emitCrossFilters,
|
||||
}) {
|
||||
const fd = formData;
|
||||
const appliedScheme = fd.color_scheme;
|
||||
const colorScale = CategoricalColorNamespace.getScale(appliedScheme);
|
||||
@@ -82,9 +79,17 @@ export function getLayer(
|
||||
getElevationValue: aggFunc,
|
||||
// @ts-ignore
|
||||
getColorValue: aggFunc,
|
||||
...commonLayerProps(fd, setTooltip, setTooltipContent),
|
||||
...commonLayerProps({
|
||||
formData: fd,
|
||||
setDataMask,
|
||||
setTooltip,
|
||||
setTooltipContent,
|
||||
filterState,
|
||||
onContextMenu,
|
||||
emitCrossFilters,
|
||||
}),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export function getPoints(data: JsonObject[]) {
|
||||
return data.map(d => d.position);
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { t, ChartMetadata, ChartPlugin } from '@superset-ui/core';
|
||||
import { t, ChartMetadata, ChartPlugin, Behavior } from '@superset-ui/core';
|
||||
import thumbnail from './images/thumbnail.png';
|
||||
import example from './images/example.png';
|
||||
import transformProps from '../../transformProps';
|
||||
@@ -33,6 +33,7 @@ const metadata = new ChartMetadata({
|
||||
exampleGallery: [{ url: example }],
|
||||
useLegacyApi: true,
|
||||
tags: [t('deckGL'), t('3D'), t('Comparison')],
|
||||
behaviors: [Behavior.InteractiveChart],
|
||||
});
|
||||
|
||||
export default class GridChartPlugin extends ChartPlugin {
|
||||
|
||||
@@ -22,7 +22,7 @@ import { t, getSequentialSchemeRegistry, JsonObject } from '@superset-ui/core';
|
||||
import { commonLayerProps } from '../common';
|
||||
import sandboxedEval from '../../utils/sandbox';
|
||||
import { hexToRGB } from '../../utils/colors';
|
||||
import { createDeckGLComponent, getLayerType } from '../../factory';
|
||||
import { GetLayerType, createDeckGLComponent } from '../../factory';
|
||||
import TooltipRow from '../../TooltipRow';
|
||||
|
||||
function setTooltipContent(o: JsonObject) {
|
||||
@@ -35,12 +35,15 @@ function setTooltipContent(o: JsonObject) {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export const getLayer: getLayerType<unknown> = (
|
||||
export const getLayer: GetLayerType<HeatmapLayer> = ({
|
||||
formData,
|
||||
payload,
|
||||
onAddFilter,
|
||||
onContextMenu,
|
||||
filterState,
|
||||
setDataMask,
|
||||
setTooltip,
|
||||
) => {
|
||||
payload,
|
||||
emitCrossFilters,
|
||||
}) => {
|
||||
const fd = formData;
|
||||
const {
|
||||
intensity = 1,
|
||||
@@ -75,7 +78,15 @@ export const getLayer: getLayerType<unknown> = (
|
||||
getPosition: (d: { position: Position; weight: number }) => d.position,
|
||||
getWeight: (d: { position: number[]; weight: number }) =>
|
||||
d.weight ? d.weight : 1,
|
||||
...commonLayerProps(fd, setTooltip, setTooltipContent),
|
||||
...commonLayerProps({
|
||||
formData: fd,
|
||||
setTooltip,
|
||||
setTooltipContent,
|
||||
setDataMask,
|
||||
filterState,
|
||||
onContextMenu,
|
||||
emitCrossFilters,
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { t, ChartMetadata, ChartPlugin } from '@superset-ui/core';
|
||||
import { t, ChartMetadata, ChartPlugin, Behavior } from '@superset-ui/core';
|
||||
import transformProps from '../../transformProps';
|
||||
import controlPanel from './controlPanel';
|
||||
import thumbnail from './images/thumbnail.png';
|
||||
@@ -33,6 +33,7 @@ const metadata = new ChartMetadata({
|
||||
thumbnail,
|
||||
useLegacyApi: true,
|
||||
tags: [t('deckGL'), t('Spatial'), t('Comparison')],
|
||||
behaviors: [Behavior.InteractiveChart],
|
||||
});
|
||||
|
||||
export default class HeatmapChartPlugin extends ChartPlugin {
|
||||
|
||||
@@ -18,19 +18,13 @@
|
||||
*/
|
||||
import { Color } from '@deck.gl/core';
|
||||
import { HexagonLayer } from '@deck.gl/aggregation-layers';
|
||||
import {
|
||||
t,
|
||||
CategoricalColorNamespace,
|
||||
QueryFormData,
|
||||
JsonObject,
|
||||
} from '@superset-ui/core';
|
||||
import { t, CategoricalColorNamespace, JsonObject } from '@superset-ui/core';
|
||||
|
||||
import { commonLayerProps, getAggFunc } from '../common';
|
||||
import sandboxedEval from '../../utils/sandbox';
|
||||
import { hexToRGB } from '../../utils/colors';
|
||||
import { createDeckGLComponent } from '../../factory';
|
||||
import { GetLayerType, createDeckGLComponent } from '../../factory';
|
||||
import TooltipRow from '../../TooltipRow';
|
||||
import { TooltipProps } from '../../components/Tooltip';
|
||||
|
||||
function setTooltipContent(o: JsonObject) {
|
||||
return (
|
||||
@@ -48,12 +42,15 @@ function setTooltipContent(o: JsonObject) {
|
||||
);
|
||||
}
|
||||
|
||||
export function getLayer(
|
||||
formData: QueryFormData,
|
||||
payload: JsonObject,
|
||||
onAddFilter: () => void,
|
||||
setTooltip: (tooltip: TooltipProps['tooltip']) => void,
|
||||
) {
|
||||
export const getLayer: GetLayerType<HexagonLayer> = function ({
|
||||
formData,
|
||||
payload,
|
||||
setTooltip,
|
||||
onContextMenu,
|
||||
filterState,
|
||||
setDataMask,
|
||||
emitCrossFilters,
|
||||
}) {
|
||||
const fd = formData;
|
||||
const appliedScheme = fd.color_scheme;
|
||||
const colorScale = CategoricalColorNamespace.getScale(appliedScheme);
|
||||
@@ -80,9 +77,17 @@ export function getLayer(
|
||||
getElevationValue: aggFunc,
|
||||
// @ts-ignore
|
||||
getColorValue: aggFunc,
|
||||
...commonLayerProps(fd, setTooltip, setTooltipContent),
|
||||
...commonLayerProps({
|
||||
formData: fd,
|
||||
setTooltip,
|
||||
setTooltipContent,
|
||||
setDataMask,
|
||||
filterState,
|
||||
onContextMenu,
|
||||
emitCrossFilters,
|
||||
}),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export function getPoints(data: JsonObject[]) {
|
||||
return data.map(d => d.position);
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { t, ChartMetadata, ChartPlugin } from '@superset-ui/core';
|
||||
import { t, ChartMetadata, ChartPlugin, Behavior } from '@superset-ui/core';
|
||||
import thumbnail from './images/thumbnail.png';
|
||||
import example from './images/example.png';
|
||||
import transformProps from '../../transformProps';
|
||||
@@ -33,6 +33,7 @@ const metadata = new ChartMetadata({
|
||||
thumbnail,
|
||||
useLegacyApi: true,
|
||||
tags: [t('deckGL'), t('3D'), t('Geo'), t('Comparison')],
|
||||
behaviors: [Behavior.InteractiveChart],
|
||||
});
|
||||
|
||||
export default class HexChartPlugin extends ChartPlugin {
|
||||
|
||||
@@ -18,12 +18,11 @@
|
||||
* under the License.
|
||||
*/
|
||||
import { PathLayer } from '@deck.gl/layers';
|
||||
import { JsonObject, QueryFormData } from '@superset-ui/core';
|
||||
import { JsonObject } from '@superset-ui/core';
|
||||
import { commonLayerProps } from '../common';
|
||||
import sandboxedEval from '../../utils/sandbox';
|
||||
import { createDeckGLComponent } from '../../factory';
|
||||
import { GetLayerType, createDeckGLComponent } from '../../factory';
|
||||
import TooltipRow from '../../TooltipRow';
|
||||
import { TooltipProps } from '../../components/Tooltip';
|
||||
import { Point } from '../../types';
|
||||
|
||||
function setTooltipContent(o: JsonObject) {
|
||||
@@ -42,12 +41,15 @@ function setTooltipContent(o: JsonObject) {
|
||||
);
|
||||
}
|
||||
|
||||
export function getLayer(
|
||||
formData: QueryFormData,
|
||||
payload: JsonObject,
|
||||
onAddFilter: () => void,
|
||||
setTooltip: (tooltip: TooltipProps['tooltip']) => void,
|
||||
) {
|
||||
export const getLayer: GetLayerType<PathLayer> = function ({
|
||||
formData,
|
||||
payload,
|
||||
onContextMenu,
|
||||
filterState,
|
||||
setDataMask,
|
||||
setTooltip,
|
||||
emitCrossFilters,
|
||||
}) {
|
||||
const fd = formData;
|
||||
const c = fd.color_picker;
|
||||
const fixedColor = [c.r, c.g, c.b, 255 * c.a];
|
||||
@@ -72,9 +74,17 @@ export function getLayer(
|
||||
rounded: true,
|
||||
widthScale: 1,
|
||||
widthUnits: fd.line_width_unit,
|
||||
...commonLayerProps(fd, setTooltip, setTooltipContent),
|
||||
...commonLayerProps({
|
||||
formData: fd,
|
||||
setTooltip,
|
||||
setTooltipContent,
|
||||
setDataMask,
|
||||
filterState,
|
||||
onContextMenu,
|
||||
emitCrossFilters,
|
||||
}),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export function getPoints(data: JsonObject[]) {
|
||||
let points: Point[] = [];
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { t, ChartMetadata, ChartPlugin } from '@superset-ui/core';
|
||||
import { t, ChartMetadata, ChartPlugin, Behavior } from '@superset-ui/core';
|
||||
import thumbnail from './images/thumbnail.png';
|
||||
import example from './images/example.png';
|
||||
import transformProps from '../../transformProps';
|
||||
@@ -31,6 +31,7 @@ const metadata = new ChartMetadata({
|
||||
exampleGallery: [{ url: example }],
|
||||
useLegacyApi: true,
|
||||
tags: [t('deckGL'), t('Web')],
|
||||
behaviors: [Behavior.InteractiveChart],
|
||||
});
|
||||
|
||||
export default class PathChartPlugin extends ChartPlugin {
|
||||
|
||||
@@ -23,10 +23,14 @@
|
||||
|
||||
import { memo, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import {
|
||||
ContextMenuFilters,
|
||||
ensureIsArray,
|
||||
FilterState,
|
||||
HandlerFunction,
|
||||
JsonObject,
|
||||
JsonValue,
|
||||
QueryFormData,
|
||||
SetDataMaskHook,
|
||||
t,
|
||||
} from '@superset-ui/core';
|
||||
|
||||
@@ -45,6 +49,7 @@ import {
|
||||
DeckGLContainerStyledWrapper,
|
||||
} from '../../DeckGLContainer';
|
||||
import { TooltipProps } from '../../components/Tooltip';
|
||||
import { GetLayerType } from '../../factory';
|
||||
|
||||
const DOUBLE_CLICK_THRESHOLD = 250; // milliseconds
|
||||
|
||||
@@ -90,15 +95,18 @@ function setTooltipContent(formData: PolygonFormData) {
|
||||
};
|
||||
}
|
||||
|
||||
export function getLayer(
|
||||
formData: PolygonFormData,
|
||||
payload: JsonObject,
|
||||
onAddFilter: HandlerFunction,
|
||||
setTooltip: (tooltip: TooltipProps['tooltip']) => void,
|
||||
selected: JsonObject[],
|
||||
onSelect: (value: JsonValue) => void,
|
||||
) {
|
||||
const fd = formData;
|
||||
export const getLayer: GetLayerType<PolygonLayer> = function ({
|
||||
formData,
|
||||
payload,
|
||||
setTooltip,
|
||||
filterState,
|
||||
setDataMask,
|
||||
onContextMenu,
|
||||
onSelect,
|
||||
selected,
|
||||
emitCrossFilters,
|
||||
}) {
|
||||
const fd = formData as PolygonFormData;
|
||||
const fc = fd.fill_color_picker;
|
||||
const sc = fd.stroke_color_picker;
|
||||
let data = [...payload.data.features];
|
||||
@@ -125,7 +133,7 @@ export function getLayer(
|
||||
number,
|
||||
number,
|
||||
]) || [0, 0, 0, 0];
|
||||
if (selected.length > 0 && !selected.includes(d[fd.line_column])) {
|
||||
if (!ensureIsArray(selected).includes(d[fd.line_column])) {
|
||||
baseColor[3] /= 2;
|
||||
}
|
||||
|
||||
@@ -153,9 +161,18 @@ export function getLayer(
|
||||
getElevation: (d: any) => getElevation(d, colorScaler),
|
||||
elevationScale: fd.multiplier,
|
||||
fp64: true,
|
||||
...commonLayerProps(fd, setTooltip, tooltipContentGenerator, onSelect),
|
||||
...commonLayerProps({
|
||||
formData: fd,
|
||||
setTooltip,
|
||||
setTooltipContent: tooltipContentGenerator,
|
||||
onSelect,
|
||||
filterState,
|
||||
onContextMenu,
|
||||
setDataMask,
|
||||
emitCrossFilters,
|
||||
}),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export type PolygonFormData = QueryFormData & {
|
||||
break_points: string[];
|
||||
@@ -171,6 +188,14 @@ export type DeckGLPolygonProps = {
|
||||
onAddFilter: HandlerFunction;
|
||||
width: number;
|
||||
height: number;
|
||||
onContextMenu?: (
|
||||
clientX: number,
|
||||
clientY: number,
|
||||
filters?: ContextMenuFilters,
|
||||
) => void;
|
||||
setDataMask?: SetDataMaskHook;
|
||||
filterState?: FilterState;
|
||||
emitCrossFilters?: boolean;
|
||||
};
|
||||
|
||||
export function getPoints(data: JsonObject[]) {
|
||||
@@ -251,28 +276,35 @@ const DeckGLPolygon = (props: DeckGLPolygonProps) => {
|
||||
);
|
||||
|
||||
const getLayers = useCallback(() => {
|
||||
const {
|
||||
formData,
|
||||
payload,
|
||||
onAddFilter,
|
||||
onContextMenu,
|
||||
setDataMask,
|
||||
filterState,
|
||||
emitCrossFilters,
|
||||
} = props;
|
||||
|
||||
if (props.payload.data.features === undefined) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const layer = getLayer(
|
||||
props.formData,
|
||||
props.payload,
|
||||
props.onAddFilter,
|
||||
const layer = getLayer({
|
||||
formData,
|
||||
payload,
|
||||
onAddFilter,
|
||||
setTooltip,
|
||||
selected,
|
||||
onSelect,
|
||||
);
|
||||
onContextMenu,
|
||||
setDataMask,
|
||||
filterState,
|
||||
emitCrossFilters,
|
||||
});
|
||||
|
||||
return [layer];
|
||||
}, [
|
||||
onSelect,
|
||||
props.formData,
|
||||
props.onAddFilter,
|
||||
props.payload,
|
||||
selected,
|
||||
setTooltip,
|
||||
]);
|
||||
}, [onSelect, selected, setTooltip, props]);
|
||||
|
||||
const { payload, formData, setControlValue } = props;
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { t, ChartMetadata, ChartPlugin } from '@superset-ui/core';
|
||||
import { t, ChartMetadata, ChartPlugin, Behavior } from '@superset-ui/core';
|
||||
import thumbnail from './images/thumbnail.png';
|
||||
import example from './images/example.png';
|
||||
import transformProps from '../../transformProps';
|
||||
@@ -33,6 +33,7 @@ const metadata = new ChartMetadata({
|
||||
exampleGallery: [{ url: example }],
|
||||
useLegacyApi: true,
|
||||
tags: [t('deckGL'), t('3D'), t('Multi-Dimensions'), t('Geo')],
|
||||
behaviors: [Behavior.InteractiveChart],
|
||||
});
|
||||
|
||||
export default class PolygonChartPlugin extends ChartPlugin {
|
||||
|
||||
@@ -18,17 +18,15 @@
|
||||
*/
|
||||
import { ScatterplotLayer } from '@deck.gl/layers';
|
||||
import {
|
||||
Datasource,
|
||||
getMetricLabel,
|
||||
JsonObject,
|
||||
QueryFormData,
|
||||
t,
|
||||
} from '@superset-ui/core';
|
||||
import { commonLayerProps } from '../common';
|
||||
import { createCategoricalDeckGLComponent } from '../../factory';
|
||||
import { createCategoricalDeckGLComponent, GetLayerType } from '../../factory';
|
||||
import TooltipRow from '../../TooltipRow';
|
||||
import { unitToRadius } from '../../utils/geo';
|
||||
import { TooltipProps } from '../../components/Tooltip';
|
||||
|
||||
export function getPoints(data: JsonObject[]) {
|
||||
return data.map(d => d.position);
|
||||
@@ -64,13 +62,16 @@ function setTooltipContent(
|
||||
};
|
||||
}
|
||||
|
||||
export function getLayer(
|
||||
formData: QueryFormData,
|
||||
payload: JsonObject,
|
||||
onAddFilter: () => void,
|
||||
setTooltip: (tooltip: TooltipProps['tooltip']) => void,
|
||||
datasource: Datasource,
|
||||
) {
|
||||
export const getLayer: GetLayerType<ScatterplotLayer> = function ({
|
||||
formData,
|
||||
payload,
|
||||
setTooltip,
|
||||
setDataMask,
|
||||
filterState,
|
||||
onContextMenu,
|
||||
datasource,
|
||||
emitCrossFilters,
|
||||
}) {
|
||||
const fd = formData;
|
||||
const dataWithRadius = payload.data.features.map((d: JsonObject) => {
|
||||
let radius = unitToRadius(fd.point_unit, d.radius) || 10;
|
||||
@@ -95,12 +96,16 @@ export function getLayer(
|
||||
radiusMinPixels: Number(fd.min_radius) || undefined,
|
||||
radiusMaxPixels: Number(fd.max_radius) || undefined,
|
||||
stroked: false,
|
||||
...commonLayerProps(
|
||||
fd,
|
||||
...commonLayerProps({
|
||||
formData: fd,
|
||||
setTooltip,
|
||||
setTooltipContent(fd, datasource?.verboseMap),
|
||||
),
|
||||
setTooltipContent: setTooltipContent(fd, datasource?.verboseMap),
|
||||
setDataMask,
|
||||
filterState,
|
||||
onContextMenu,
|
||||
emitCrossFilters,
|
||||
}),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default createCategoricalDeckGLComponent(getLayer, getPoints);
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { t, ChartMetadata, ChartPlugin } from '@superset-ui/core';
|
||||
import { t, ChartMetadata, ChartPlugin, Behavior } from '@superset-ui/core';
|
||||
import thumbnail from './images/thumbnail.png';
|
||||
import example from './images/example.png';
|
||||
import transformProps from '../../transformProps';
|
||||
@@ -41,6 +41,7 @@ const metadata = new ChartMetadata({
|
||||
t('Intensity'),
|
||||
t('Density'),
|
||||
],
|
||||
behaviors: [Behavior.InteractiveChart],
|
||||
});
|
||||
|
||||
export default class ScatterChartPlugin extends ChartPlugin {
|
||||
|
||||
@@ -18,21 +18,13 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
/* eslint no-underscore-dangle: ["error", { "allow": ["", "__timestamp"] }] */
|
||||
|
||||
import { memo, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { ScreenGridLayer } from '@deck.gl/aggregation-layers';
|
||||
import { JsonObject, JsonValue, QueryFormData, t } from '@superset-ui/core';
|
||||
import { JsonObject, t } from '@superset-ui/core';
|
||||
import sandboxedEval from '../../utils/sandbox';
|
||||
import { commonLayerProps } from '../common';
|
||||
import TooltipRow from '../../TooltipRow';
|
||||
// eslint-disable-next-line import/extensions
|
||||
import fitViewport, { Viewport } from '../../utils/fitViewport';
|
||||
import {
|
||||
DeckGLContainerHandle,
|
||||
DeckGLContainerStyledWrapper,
|
||||
} from '../../DeckGLContainer';
|
||||
import { TooltipProps } from '../../components/Tooltip';
|
||||
import { GetLayerType, createDeckGLComponent } from '../../factory';
|
||||
|
||||
export function getPoints(data: JsonObject[]) {
|
||||
return data.map(d => d.position);
|
||||
@@ -55,12 +47,15 @@ function setTooltipContent(o: JsonObject) {
|
||||
);
|
||||
}
|
||||
|
||||
export function getLayer(
|
||||
formData: QueryFormData,
|
||||
payload: JsonObject,
|
||||
onAddFilter: () => void,
|
||||
setTooltip: (tooltip: TooltipProps['tooltip']) => void,
|
||||
) {
|
||||
export const getLayer: GetLayerType<ScreenGridLayer> = function ({
|
||||
formData,
|
||||
setDataMask,
|
||||
filterState,
|
||||
onContextMenu,
|
||||
payload,
|
||||
setTooltip,
|
||||
emitCrossFilters,
|
||||
}) {
|
||||
const fd = formData;
|
||||
const c = fd.color_picker;
|
||||
let data = payload.data.features.map((d: JsonObject) => ({
|
||||
@@ -84,77 +79,16 @@ export function getLayer(
|
||||
maxColor: [c.r, c.g, c.b, 255 * c.a],
|
||||
outline: false,
|
||||
getWeight: (d: any) => d.weight || 0,
|
||||
...commonLayerProps(fd, setTooltip, setTooltipContent),
|
||||
...commonLayerProps({
|
||||
formData: fd,
|
||||
setDataMask,
|
||||
setTooltip,
|
||||
setTooltipContent,
|
||||
filterState,
|
||||
onContextMenu,
|
||||
emitCrossFilters,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
export type DeckGLScreenGridProps = {
|
||||
formData: QueryFormData;
|
||||
payload: JsonObject;
|
||||
setControlValue: (control: string, value: JsonValue) => void;
|
||||
viewport: Viewport;
|
||||
width: number;
|
||||
height: number;
|
||||
onAddFilter: () => void;
|
||||
};
|
||||
|
||||
const DeckGLScreenGrid = (props: DeckGLScreenGridProps) => {
|
||||
const containerRef = useRef<DeckGLContainerHandle>();
|
||||
|
||||
const getAdjustedViewport = useCallback(() => {
|
||||
const features = props.payload.data.features || [];
|
||||
|
||||
const { width, height, formData } = props;
|
||||
|
||||
if (formData.autozoom) {
|
||||
return fitViewport(props.viewport, {
|
||||
width,
|
||||
height,
|
||||
points: getPoints(features),
|
||||
});
|
||||
}
|
||||
return props.viewport;
|
||||
}, [props]);
|
||||
|
||||
const [stateFormData, setStateFormData] = useState(props.payload.form_data);
|
||||
const [viewport, setViewport] = useState(getAdjustedViewport());
|
||||
|
||||
useEffect(() => {
|
||||
if (props.payload.form_data !== stateFormData) {
|
||||
setViewport(getAdjustedViewport());
|
||||
setStateFormData(props.payload.form_data);
|
||||
}
|
||||
}, [getAdjustedViewport, props.payload.form_data, stateFormData]);
|
||||
|
||||
const setTooltip = useCallback((tooltip: TooltipProps['tooltip']) => {
|
||||
const { current } = containerRef;
|
||||
if (current) {
|
||||
current.setTooltip(tooltip);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const getLayers = useCallback(() => {
|
||||
const layer = getLayer(props.formData, props.payload, () => {}, setTooltip);
|
||||
|
||||
return [layer];
|
||||
}, [props.formData, props.payload, setTooltip]);
|
||||
|
||||
const { formData, payload, setControlValue } = props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<DeckGLContainerStyledWrapper
|
||||
ref={containerRef}
|
||||
viewport={viewport}
|
||||
layers={getLayers()}
|
||||
setControlValue={setControlValue}
|
||||
mapStyle={formData.mapbox_style}
|
||||
mapboxApiAccessToken={payload.data.mapboxApiKey}
|
||||
width={props.width}
|
||||
height={props.height}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(DeckGLScreenGrid);
|
||||
export default createDeckGLComponent(getLayer, getPoints);
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { t, ChartMetadata, ChartPlugin } from '@superset-ui/core';
|
||||
import { t, ChartMetadata, ChartPlugin, Behavior } from '@superset-ui/core';
|
||||
import thumbnail from './images/thumbnail.png';
|
||||
import example from './images/example.png';
|
||||
import transformProps from '../../transformProps';
|
||||
@@ -33,6 +33,7 @@ const metadata = new ChartMetadata({
|
||||
exampleGallery: [{ url: example }],
|
||||
useLegacyApi: true,
|
||||
tags: [t('deckGL'), t('Comparison'), t('Intensity'), t('Density')],
|
||||
behaviors: [Behavior.InteractiveChart],
|
||||
});
|
||||
|
||||
export default class ScreengridChartPlugin extends ChartPlugin {
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { PickingInfo } from '@deck.gl/core';
|
||||
import { JsonObject, QueryFormData } from '@superset-ui/core';
|
||||
import { getAggFunc, commonLayerProps } from './common';
|
||||
|
||||
@@ -99,22 +100,22 @@ describe('commonLayerProps', () => {
|
||||
...partialformData,
|
||||
js_tooltip: 'tooltip => tooltip.content',
|
||||
} as QueryFormData;
|
||||
const props = commonLayerProps(
|
||||
const props = commonLayerProps({
|
||||
formData,
|
||||
mockSetTooltip,
|
||||
mockSetTooltipContent,
|
||||
);
|
||||
setTooltip: mockSetTooltip,
|
||||
setTooltipContent: mockSetTooltipContent,
|
||||
});
|
||||
expect(props.pickable).toBe(true);
|
||||
expect(props.onHover).toBeDefined();
|
||||
});
|
||||
|
||||
it('calls onHover and sets tooltip', () => {
|
||||
const formData = { ...partialformData, js_tooltip: null } as QueryFormData;
|
||||
const props = commonLayerProps(
|
||||
const props = commonLayerProps({
|
||||
formData,
|
||||
mockSetTooltip,
|
||||
mockSetTooltipContent,
|
||||
);
|
||||
setTooltip: mockSetTooltip,
|
||||
setTooltipContent: mockSetTooltipContent,
|
||||
});
|
||||
|
||||
const mockObject = { picked: true, x: 10, y: 20 };
|
||||
props.onHover?.(mockObject);
|
||||
@@ -131,15 +132,30 @@ describe('commonLayerProps', () => {
|
||||
table_filter: true,
|
||||
line_column: 'name',
|
||||
} as QueryFormData;
|
||||
const props = commonLayerProps(
|
||||
const props = commonLayerProps({
|
||||
formData,
|
||||
mockSetTooltip,
|
||||
mockSetTooltipContent,
|
||||
mockOnSelect,
|
||||
);
|
||||
setTooltip: mockSetTooltip,
|
||||
setTooltipContent: mockSetTooltipContent,
|
||||
onSelect: mockOnSelect,
|
||||
});
|
||||
|
||||
const mockObject = { object: { name: 'John Doe' } };
|
||||
props.onClick?.(mockObject);
|
||||
const pickingData = {
|
||||
color: [],
|
||||
index: 1,
|
||||
coordinate: [-122.40138935788005, 37.77785781376027],
|
||||
devicePixel: [345, 428],
|
||||
pixel: [172, 116.484375],
|
||||
pixelRatio: 2,
|
||||
picked: true,
|
||||
sourceLayer: {},
|
||||
viewport: { zoom: 10 },
|
||||
layer: {},
|
||||
x: 172,
|
||||
y: 116.484375,
|
||||
object: { name: 'John Doe' },
|
||||
} as unknown as PickingInfo;
|
||||
|
||||
props.onClick?.(pickingData, {});
|
||||
expect(mockOnSelect).toHaveBeenCalledWith('John Doe');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -28,16 +28,38 @@ import {
|
||||
variance as d3variance,
|
||||
deviation as d3deviation,
|
||||
} from 'd3-array';
|
||||
import { JsonObject, JsonValue, QueryFormData } from '@superset-ui/core';
|
||||
import {
|
||||
FilterState,
|
||||
HandlerFunction,
|
||||
JsonObject,
|
||||
JsonValue,
|
||||
QueryFormData,
|
||||
SetDataMaskHook,
|
||||
} from '@superset-ui/core';
|
||||
import { Layer, PickingInfo } from '@deck.gl/core';
|
||||
import sandboxedEval from '../utils/sandbox';
|
||||
import { TooltipProps } from '../components/Tooltip';
|
||||
import { getCrossFilterDataMask } from '../utils/crossFiltersDataMask';
|
||||
|
||||
export function commonLayerProps(
|
||||
formData: QueryFormData,
|
||||
setTooltip: (tooltip: TooltipProps['tooltip']) => void,
|
||||
setTooltipContent: (content: JsonObject) => ReactNode,
|
||||
onSelect?: (value: JsonValue) => void,
|
||||
) {
|
||||
export function commonLayerProps({
|
||||
formData,
|
||||
setDataMask,
|
||||
setTooltip,
|
||||
setTooltipContent,
|
||||
onSelect,
|
||||
onContextMenu,
|
||||
filterState,
|
||||
emitCrossFilters,
|
||||
}: {
|
||||
formData: QueryFormData;
|
||||
setDataMask?: SetDataMaskHook;
|
||||
setTooltip: (tooltip: TooltipProps['tooltip']) => void;
|
||||
setTooltipContent: (content: JsonObject) => ReactNode;
|
||||
onSelect?: (value: JsonValue) => void;
|
||||
filterState?: FilterState;
|
||||
onContextMenu?: HandlerFunction;
|
||||
emitCrossFilters?: boolean;
|
||||
}) {
|
||||
const fd = formData;
|
||||
let onHover;
|
||||
let tooltipContentGenerator = setTooltipContent;
|
||||
@@ -58,6 +80,7 @@ export function commonLayerProps(
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
let onClick;
|
||||
if (fd.js_onclick_href) {
|
||||
onClick = (o: any) => {
|
||||
@@ -70,12 +93,30 @@ export function commonLayerProps(
|
||||
onSelect(o.object[fd.line_column]);
|
||||
return true;
|
||||
};
|
||||
} else if (emitCrossFilters) {
|
||||
onClick = (data: PickingInfo, event: any) => {
|
||||
const crossFilters = getCrossFilterDataMask({
|
||||
data,
|
||||
filterState,
|
||||
formData,
|
||||
});
|
||||
|
||||
if (event.leftButton && setDataMask !== undefined && crossFilters) {
|
||||
setDataMask(crossFilters.dataMask);
|
||||
} else if (event.rightButton && onContextMenu !== undefined) {
|
||||
onContextMenu(event.center.x, event.center.y, {
|
||||
drillToDetail: [],
|
||||
crossFilter: crossFilters,
|
||||
drillBy: {},
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
onClick,
|
||||
onClick: onClick as Layer['onClick'],
|
||||
onHover,
|
||||
pickable: Boolean(onHover),
|
||||
pickable: Boolean(onHover || onClick),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -22,22 +22,39 @@ import { ChartProps } from '@superset-ui/core';
|
||||
const NOOP = () => {};
|
||||
|
||||
export default function transformProps(chartProps: ChartProps) {
|
||||
const { datasource, height, hooks, queriesData, rawFormData, width } =
|
||||
chartProps;
|
||||
const { onAddFilter = NOOP, setControlValue = NOOP } = hooks;
|
||||
const {
|
||||
datasource,
|
||||
height,
|
||||
hooks,
|
||||
queriesData,
|
||||
rawFormData,
|
||||
width,
|
||||
filterState,
|
||||
emitCrossFilters,
|
||||
} = chartProps;
|
||||
const {
|
||||
onAddFilter = NOOP,
|
||||
onContextMenu = NOOP,
|
||||
setControlValue = NOOP,
|
||||
setDataMask = NOOP,
|
||||
} = hooks;
|
||||
|
||||
return {
|
||||
datasource,
|
||||
emitCrossFilters,
|
||||
formData: rawFormData,
|
||||
height,
|
||||
onAddFilter,
|
||||
onContextMenu,
|
||||
payload: queriesData[0],
|
||||
setControlValue,
|
||||
filterState,
|
||||
viewport: {
|
||||
...rawFormData.viewport,
|
||||
height,
|
||||
width,
|
||||
},
|
||||
width,
|
||||
setDataMask,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,462 @@
|
||||
/**
|
||||
* 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 { TimeGranularity } from '@superset-ui/core';
|
||||
import { PickingInfo } from '@deck.gl/core';
|
||||
import {
|
||||
getCrossFilterDataMask,
|
||||
LayerFormData,
|
||||
SpatialData,
|
||||
} from './crossFiltersDataMask';
|
||||
|
||||
const formData: LayerFormData = {
|
||||
metric: 'value',
|
||||
colorPicker: {
|
||||
r: 0,
|
||||
g: 122,
|
||||
b: 135,
|
||||
a: 1,
|
||||
},
|
||||
compareLag: 1,
|
||||
timeGrainSqla: TimeGranularity.QUARTER,
|
||||
granularitySqla: 'ds',
|
||||
compareSuffix: 'over last quarter',
|
||||
viz_type: 'deck_grid',
|
||||
yAxisFormat: '.3s',
|
||||
datasource: 'test_datasource',
|
||||
};
|
||||
|
||||
const pickingData = {
|
||||
color: [],
|
||||
index: 1,
|
||||
coordinate: [-122.40138935788005, 37.77785781376027],
|
||||
devicePixel: [345, 428],
|
||||
pixel: [172, 116.484375],
|
||||
pixelRatio: 2,
|
||||
picked: true,
|
||||
sourceLayer: {},
|
||||
viewport: { zoom: 10 },
|
||||
layer: {},
|
||||
x: 172,
|
||||
y: 116.484375,
|
||||
} as unknown as PickingInfo;
|
||||
|
||||
describe('getCrossFilterDataMask', () => {
|
||||
it('handles latlong type', () => {
|
||||
const latlongFormData = {
|
||||
...formData,
|
||||
spatial: {
|
||||
latCol: 'LAT',
|
||||
lonCol: 'LON',
|
||||
type: 'latlong',
|
||||
} as SpatialData,
|
||||
};
|
||||
|
||||
const latlongPickingData = {
|
||||
...pickingData,
|
||||
object: {
|
||||
col: 14,
|
||||
row: 34,
|
||||
colorValue: 1369,
|
||||
elevationValue: 1369,
|
||||
count: 5,
|
||||
pointIndices: [2, 1425, 4107, 4410, 4737],
|
||||
points: [
|
||||
{
|
||||
position: [-122.4205965, 37.8054735],
|
||||
weight: 1349,
|
||||
},
|
||||
{
|
||||
position: [-122.4215375, 37.8058583],
|
||||
weight: 8,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const dataMask = getCrossFilterDataMask({
|
||||
formData: latlongFormData,
|
||||
data: latlongPickingData,
|
||||
filterState: {},
|
||||
});
|
||||
|
||||
const expected = {
|
||||
dataMask: {
|
||||
extraFormData: {
|
||||
filters: [
|
||||
{
|
||||
col: 'LON',
|
||||
op: '==',
|
||||
val: -122.4205965,
|
||||
},
|
||||
{
|
||||
col: 'LAT',
|
||||
op: '==',
|
||||
val: 37.8054735,
|
||||
},
|
||||
],
|
||||
},
|
||||
filterState: {
|
||||
value: [-122.4205965, 37.8054735],
|
||||
customColumnLabel: 'LON, LAT',
|
||||
},
|
||||
},
|
||||
isCurrentValueSelected: false,
|
||||
};
|
||||
|
||||
expect(dataMask).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('handles latlong type with active filters', () => {
|
||||
const latlongFormData = {
|
||||
...formData,
|
||||
spatial: {
|
||||
latCol: 'LAT',
|
||||
lonCol: 'LON',
|
||||
type: 'latlong',
|
||||
} as SpatialData,
|
||||
};
|
||||
|
||||
const latlongPickingData = {
|
||||
...pickingData,
|
||||
object: {
|
||||
col: 14,
|
||||
row: 34,
|
||||
colorValue: 1369,
|
||||
elevationValue: 1369,
|
||||
count: 5,
|
||||
pointIndices: [2, 1425, 4107, 4410, 4737],
|
||||
points: [
|
||||
{
|
||||
position: [-122.4205965, 37.8054735],
|
||||
weight: 1349,
|
||||
},
|
||||
{
|
||||
position: [-122.4215375, 37.8058583],
|
||||
weight: 8,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const dataMask = getCrossFilterDataMask({
|
||||
formData: latlongFormData,
|
||||
data: latlongPickingData,
|
||||
filterState: { value: [-122.4205965, 37.8054735] },
|
||||
});
|
||||
|
||||
const expected = {
|
||||
dataMask: {
|
||||
extraFormData: {
|
||||
filters: [],
|
||||
},
|
||||
filterState: {
|
||||
value: null,
|
||||
},
|
||||
},
|
||||
isCurrentValueSelected: true,
|
||||
};
|
||||
|
||||
expect(dataMask).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('handles delimited type', () => {
|
||||
const delimitedFormData = {
|
||||
...formData,
|
||||
spatial: {
|
||||
lonlatCol: 'LONLAT',
|
||||
delimiter: ',',
|
||||
type: 'delimited',
|
||||
} as SpatialData,
|
||||
};
|
||||
|
||||
const delimitedPickingData = {
|
||||
...pickingData,
|
||||
object: {
|
||||
points: [
|
||||
{
|
||||
position: [-122.4205965, 37.8054735],
|
||||
weight: 1349,
|
||||
},
|
||||
{
|
||||
position: [-122.4215375, 37.8058583],
|
||||
weight: 8,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const dataMask = getCrossFilterDataMask({
|
||||
formData: delimitedFormData,
|
||||
data: delimitedPickingData,
|
||||
filterState: {},
|
||||
});
|
||||
|
||||
const expected = {
|
||||
dataMask: {
|
||||
extraFormData: {
|
||||
filters: [
|
||||
{
|
||||
col: 'LONLAT',
|
||||
op: '==',
|
||||
val: `-122.4205965,37.8054735`,
|
||||
},
|
||||
],
|
||||
},
|
||||
filterState: {
|
||||
value: [`-122.4205965,37.8054735`],
|
||||
},
|
||||
},
|
||||
isCurrentValueSelected: false,
|
||||
};
|
||||
|
||||
expect(dataMask).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('handles delimited type with reversed lon/lat', () => {
|
||||
const delimitedFormData = {
|
||||
...formData,
|
||||
spatial: {
|
||||
lonlatCol: 'LONLAT',
|
||||
delimiter: ',',
|
||||
type: 'delimited',
|
||||
reverseCheckbox: true,
|
||||
} as SpatialData,
|
||||
};
|
||||
|
||||
const delimitedPickingData = {
|
||||
...pickingData,
|
||||
object: {
|
||||
points: [
|
||||
{
|
||||
position: [-122.4205965, 37.8054735],
|
||||
weight: 1349,
|
||||
},
|
||||
{
|
||||
position: [-122.4215375, 37.8058583],
|
||||
weight: 8,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const dataMask = getCrossFilterDataMask({
|
||||
formData: delimitedFormData,
|
||||
data: delimitedPickingData,
|
||||
filterState: {},
|
||||
});
|
||||
|
||||
const expected = {
|
||||
dataMask: {
|
||||
extraFormData: {
|
||||
filters: [
|
||||
{
|
||||
col: 'LONLAT',
|
||||
op: '==',
|
||||
val: `37.8054735,-122.4205965`,
|
||||
},
|
||||
],
|
||||
},
|
||||
filterState: {
|
||||
value: [`37.8054735,-122.4205965`],
|
||||
},
|
||||
},
|
||||
isCurrentValueSelected: false,
|
||||
};
|
||||
|
||||
expect(dataMask).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('handles geohash type', () => {
|
||||
const geohashFormData = {
|
||||
...formData,
|
||||
spatial: {
|
||||
geohashCol: 'geohash',
|
||||
reverseCheckbox: false,
|
||||
type: 'geohash',
|
||||
} as SpatialData,
|
||||
};
|
||||
|
||||
const geohashPickingData = {
|
||||
...pickingData,
|
||||
object: {
|
||||
points: [
|
||||
{
|
||||
position: [-122.42059646174312, 37.805473459884524],
|
||||
weight: 1349,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const dataMask = getCrossFilterDataMask({
|
||||
formData: geohashFormData,
|
||||
data: geohashPickingData,
|
||||
filterState: {},
|
||||
});
|
||||
|
||||
const expected = {
|
||||
dataMask: {
|
||||
extraFormData: {
|
||||
filters: [
|
||||
{
|
||||
col: 'geohash',
|
||||
op: '==',
|
||||
val: `9q8zn620c751`,
|
||||
},
|
||||
],
|
||||
},
|
||||
filterState: {
|
||||
value: ['9q8zn620c751'],
|
||||
},
|
||||
},
|
||||
isCurrentValueSelected: false,
|
||||
};
|
||||
|
||||
expect(dataMask).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('handles start and end postions (Arc Chart)', () => {
|
||||
const arcFormData = {
|
||||
...formData,
|
||||
start_spatial: {
|
||||
geohashCol: 'geohash',
|
||||
reverseCheckbox: false,
|
||||
type: 'geohash',
|
||||
} as SpatialData,
|
||||
end_spatial: {
|
||||
latCol: 'LAT_DEST',
|
||||
lonCol: 'LON_DEST',
|
||||
type: 'latlong',
|
||||
} as SpatialData,
|
||||
};
|
||||
|
||||
const arkPickingData = {
|
||||
...pickingData,
|
||||
object: {
|
||||
sourcePosition: [-122.42059646174312, 37.805473459884524],
|
||||
targetPosition: [-122.4215375, 37.8058583],
|
||||
},
|
||||
};
|
||||
|
||||
const dataMask = getCrossFilterDataMask({
|
||||
formData: arcFormData,
|
||||
data: arkPickingData,
|
||||
filterState: {},
|
||||
});
|
||||
|
||||
const expected = {
|
||||
dataMask: {
|
||||
extraFormData: {
|
||||
filters: [
|
||||
{
|
||||
col: 'geohash',
|
||||
op: '==',
|
||||
val: `9q8zn620c751`,
|
||||
},
|
||||
{
|
||||
col: 'LON_DEST',
|
||||
op: '==',
|
||||
val: -122.4215375,
|
||||
},
|
||||
{
|
||||
col: 'LAT_DEST',
|
||||
op: '==',
|
||||
val: 37.8058583,
|
||||
},
|
||||
],
|
||||
},
|
||||
filterState: {
|
||||
value: [['9q8zn620c751'], [-122.4215375, 37.8058583]],
|
||||
customColumnLabel: 'Start geohash end LAT_DEST, LON_DEST',
|
||||
},
|
||||
},
|
||||
isCurrentValueSelected: false,
|
||||
};
|
||||
|
||||
expect(dataMask).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('handles Charts with GPU aggregation', () => {
|
||||
const latlongGPUFormData = {
|
||||
...formData,
|
||||
spatial: {
|
||||
latCol: 'LAT',
|
||||
lonCol: 'LON',
|
||||
type: 'latlong',
|
||||
} as SpatialData,
|
||||
};
|
||||
|
||||
const latlongGPUPickingData = {
|
||||
...pickingData,
|
||||
object: {
|
||||
col: 14,
|
||||
row: 34,
|
||||
colorValue: 1369,
|
||||
elevationValue: 1369,
|
||||
count: 5,
|
||||
pointIndices: [2, 1425, 4107, 4410, 4737],
|
||||
},
|
||||
};
|
||||
|
||||
const dataMask = getCrossFilterDataMask({
|
||||
formData: latlongGPUFormData,
|
||||
data: latlongGPUPickingData,
|
||||
filterState: {},
|
||||
});
|
||||
|
||||
const expected = {
|
||||
dataMask: {
|
||||
extraFormData: {
|
||||
filters: [
|
||||
{
|
||||
col: 'LON',
|
||||
op: '>=',
|
||||
val: -122.41076435788005,
|
||||
},
|
||||
{
|
||||
col: 'LAT',
|
||||
op: '>=',
|
||||
val: 37.76848281376027,
|
||||
},
|
||||
{
|
||||
col: 'LON',
|
||||
op: '<=',
|
||||
val: -122.39201435788004,
|
||||
},
|
||||
{
|
||||
col: 'LAT',
|
||||
op: '<=',
|
||||
val: 37.78723281376027,
|
||||
},
|
||||
],
|
||||
},
|
||||
filterState: {
|
||||
value: [
|
||||
[-122.41076435788005, 37.76848281376027],
|
||||
[-122.39201435788004, 37.78723281376027],
|
||||
],
|
||||
customColumnLabel: 'From LON, LAT to LON, LAT',
|
||||
},
|
||||
},
|
||||
isCurrentValueSelected: false,
|
||||
};
|
||||
|
||||
expect(dataMask).toStrictEqual(expected);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,422 @@
|
||||
/**
|
||||
* 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 { PickingInfo, Viewport } from '@deck.gl/core';
|
||||
import {
|
||||
ContextMenuFilters,
|
||||
FilterState,
|
||||
QueryObjectFilterClause,
|
||||
SqlaFormData,
|
||||
} from '@superset-ui/core';
|
||||
import ngeohash from 'ngeohash';
|
||||
|
||||
const GEOHASH_PRECISION = 12;
|
||||
const VIEWPORT_BUFFER_FACTOR = 0.3;
|
||||
const ZOOM_DIVISOR = 2;
|
||||
|
||||
export const spatialTypes = {
|
||||
latlong: 'latlong',
|
||||
delimited: 'delimited',
|
||||
geohash: 'geohash',
|
||||
} as const;
|
||||
|
||||
type SpatialType = (typeof spatialTypes)[keyof typeof spatialTypes];
|
||||
|
||||
export type SpatialData = {
|
||||
latCol?: string;
|
||||
lonCol?: string;
|
||||
lonlatCol?: string;
|
||||
reverseCheckbox?: boolean;
|
||||
delimiter?: string;
|
||||
type: SpatialType;
|
||||
geohashCol?: string;
|
||||
line_column?: string;
|
||||
};
|
||||
|
||||
export interface LayerFormData extends SqlaFormData {
|
||||
start_spatial?: SpatialData;
|
||||
end_spatial?: SpatialData;
|
||||
spatial?: SpatialData;
|
||||
line_column?: string;
|
||||
geojson?: string;
|
||||
}
|
||||
|
||||
export interface FilterResult {
|
||||
filters: QueryObjectFilterClause[];
|
||||
values: FilterState;
|
||||
customColumnLabel?: string;
|
||||
}
|
||||
|
||||
export interface PositionBounds {
|
||||
from: [number, number];
|
||||
to: [number, number];
|
||||
}
|
||||
|
||||
export interface ValidatedPickingData {
|
||||
position?: [number, number];
|
||||
positionBounds?: PositionBounds;
|
||||
sourcePosition?: [number, number];
|
||||
targetPosition?: [number, number];
|
||||
path?: string;
|
||||
geometry?: any;
|
||||
}
|
||||
|
||||
const getFiltersBySpatialType = ({
|
||||
position,
|
||||
positionBounds,
|
||||
spatialData,
|
||||
}: {
|
||||
position: [number, number];
|
||||
spatialData: SpatialData;
|
||||
positionBounds?: PositionBounds;
|
||||
}) => {
|
||||
const {
|
||||
lonCol,
|
||||
latCol,
|
||||
lonlatCol,
|
||||
geohashCol,
|
||||
reverseCheckbox,
|
||||
type,
|
||||
delimiter,
|
||||
} = spatialData;
|
||||
let values: any[] = [];
|
||||
let filters: QueryObjectFilterClause[] = [];
|
||||
let customColumnLabel;
|
||||
|
||||
if (!position && !positionBounds)
|
||||
throw new Error('Position of picked data is required');
|
||||
|
||||
switch (type) {
|
||||
case spatialTypes.latlong: {
|
||||
if (lonCol != null && latCol != null) {
|
||||
const cols = [lonCol, latCol];
|
||||
|
||||
if (position) {
|
||||
values = position;
|
||||
customColumnLabel = cols.join(', ');
|
||||
|
||||
filters = [
|
||||
...cols.map(
|
||||
(col, index) =>
|
||||
({
|
||||
col,
|
||||
op: '==',
|
||||
val: position[index],
|
||||
}) as QueryObjectFilterClause,
|
||||
),
|
||||
];
|
||||
} else if (positionBounds) {
|
||||
values = [positionBounds.from, positionBounds.to];
|
||||
customColumnLabel = `From ${lonCol}, ${latCol} to ${lonCol}, ${latCol}`;
|
||||
|
||||
filters = [
|
||||
...cols.map(
|
||||
(col, index) =>
|
||||
({
|
||||
col,
|
||||
op: '>=',
|
||||
val: positionBounds.from[index],
|
||||
}) as QueryObjectFilterClause,
|
||||
),
|
||||
...cols.map(
|
||||
(col, index) =>
|
||||
({
|
||||
col,
|
||||
op: '<=',
|
||||
val: positionBounds.to[index],
|
||||
}) as QueryObjectFilterClause,
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case spatialTypes.delimited: {
|
||||
const col = lonlatCol ?? geohashCol;
|
||||
|
||||
if (!col) throw new Error('Column is required');
|
||||
|
||||
const val = (reverseCheckbox ? position.reverse() : position).join(
|
||||
delimiter,
|
||||
);
|
||||
|
||||
values = [val];
|
||||
|
||||
filters = [
|
||||
{
|
||||
col,
|
||||
op: '==',
|
||||
val,
|
||||
},
|
||||
];
|
||||
|
||||
break;
|
||||
}
|
||||
case spatialTypes.geohash: {
|
||||
const col = lonlatCol ?? geohashCol;
|
||||
|
||||
if (!col) throw new Error('Column is required');
|
||||
|
||||
const [lon, lat] = position;
|
||||
const val = ngeohash.encode(lat, lon, GEOHASH_PRECISION);
|
||||
|
||||
values = [val];
|
||||
|
||||
filters = [
|
||||
{
|
||||
col,
|
||||
op: '==',
|
||||
val,
|
||||
},
|
||||
];
|
||||
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
values = [];
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
filters,
|
||||
values,
|
||||
customColumnLabel,
|
||||
};
|
||||
};
|
||||
|
||||
const calculatePickedPositionBounds = ({
|
||||
pickedCoordinates,
|
||||
viewport,
|
||||
}: {
|
||||
pickedCoordinates: number[];
|
||||
viewport: Viewport;
|
||||
}): PositionBounds => {
|
||||
const buffer =
|
||||
VIEWPORT_BUFFER_FACTOR / Math.pow(2, viewport.zoom / ZOOM_DIVISOR);
|
||||
|
||||
return {
|
||||
from: [pickedCoordinates[0] - buffer, pickedCoordinates[1] - buffer],
|
||||
to: [pickedCoordinates[0] + buffer, pickedCoordinates[1] + buffer],
|
||||
};
|
||||
};
|
||||
|
||||
const getSpatialColumnLabel = ({
|
||||
latCol,
|
||||
lonCol,
|
||||
geohashCol,
|
||||
line_column,
|
||||
}: {
|
||||
latCol?: string;
|
||||
lonCol?: string;
|
||||
geohashCol?: string;
|
||||
line_column?: string;
|
||||
}) => {
|
||||
if (latCol && lonCol) {
|
||||
return `${latCol}, ${lonCol}`;
|
||||
}
|
||||
if (geohashCol) {
|
||||
return geohashCol;
|
||||
}
|
||||
if (line_column) {
|
||||
return line_column;
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
const getStartEndSpatialFilters = ({
|
||||
formData,
|
||||
data,
|
||||
}: {
|
||||
formData: LayerFormData;
|
||||
data: PickingInfo;
|
||||
}): FilterResult => {
|
||||
const sourcePosition: [number, number] = data.object?.sourcePosition;
|
||||
const targetPosition: [number, number] = data.object?.targetPosition;
|
||||
|
||||
if (!sourcePosition || !targetPosition)
|
||||
throw new Error('Position of picked data is required');
|
||||
|
||||
if (!formData.start_spatial || !formData.end_spatial)
|
||||
throw new Error('Spatial data is required');
|
||||
|
||||
const customColumnLabel = `Start ${getSpatialColumnLabel(formData.start_spatial)} end ${getSpatialColumnLabel(formData.end_spatial)}`;
|
||||
|
||||
const startSpatialFilters = getFiltersBySpatialType({
|
||||
position: sourcePosition,
|
||||
spatialData: formData.start_spatial,
|
||||
});
|
||||
|
||||
const endSpatialFilters = getFiltersBySpatialType({
|
||||
position: targetPosition,
|
||||
spatialData: formData.end_spatial,
|
||||
});
|
||||
|
||||
if (!startSpatialFilters || !endSpatialFilters)
|
||||
throw new Error('Failed to generate filters');
|
||||
|
||||
return {
|
||||
values: [startSpatialFilters.values, endSpatialFilters.values],
|
||||
filters: [
|
||||
...(startSpatialFilters.filters || []),
|
||||
...(endSpatialFilters.filters || []),
|
||||
],
|
||||
customColumnLabel,
|
||||
};
|
||||
};
|
||||
|
||||
const getSpatialFilters = ({
|
||||
formData,
|
||||
data,
|
||||
}: {
|
||||
formData: LayerFormData;
|
||||
data: PickingInfo;
|
||||
}): FilterResult => {
|
||||
const position = (data.object?.points?.[0]?.position ||
|
||||
data.object?.position) as [number, number];
|
||||
|
||||
let positionBounds: PositionBounds | undefined;
|
||||
|
||||
if (!position && data.coordinate && data.viewport) {
|
||||
const pickedPositionBounds = calculatePickedPositionBounds({
|
||||
pickedCoordinates: data.coordinate,
|
||||
viewport: data.viewport,
|
||||
});
|
||||
|
||||
positionBounds = pickedPositionBounds;
|
||||
}
|
||||
|
||||
if (!formData.spatial) throw new Error('Spatial data is required');
|
||||
|
||||
return getFiltersBySpatialType({
|
||||
position,
|
||||
positionBounds,
|
||||
spatialData: formData.spatial,
|
||||
});
|
||||
};
|
||||
|
||||
const getLineColumnFilters = ({
|
||||
formData,
|
||||
data,
|
||||
}: {
|
||||
formData: LayerFormData;
|
||||
data: PickingInfo;
|
||||
}): FilterResult => {
|
||||
const path = (data?.object?.path || data.object?.polygon) as string;
|
||||
const val = JSON.stringify(path);
|
||||
|
||||
if (!formData.line_column) throw new Error('Line column is required');
|
||||
if (!path) throw new Error('Position of picked data is required');
|
||||
|
||||
return {
|
||||
values: [val],
|
||||
filters: [
|
||||
{
|
||||
col: {
|
||||
expressionType: 'SQL',
|
||||
sqlExpression: `REPLACE(${formData.line_column}, ' ', '')`,
|
||||
label: formData.line_column,
|
||||
},
|
||||
op: '==',
|
||||
val,
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
const getGeojsonFilters = ({
|
||||
formData,
|
||||
data,
|
||||
}: {
|
||||
formData: LayerFormData;
|
||||
data: PickingInfo;
|
||||
}): FilterResult => {
|
||||
const geometry = data.object?.geometry?.coordinates;
|
||||
|
||||
if (!geometry) throw new Error('Position of picked data is required');
|
||||
|
||||
const val = `%${JSON.stringify(geometry)}%`;
|
||||
|
||||
return {
|
||||
values: [val],
|
||||
filters: [
|
||||
{
|
||||
col: {
|
||||
expressionType: 'SQL',
|
||||
sqlExpression: `REPLACE(${formData.geojson}, ' ', '')`,
|
||||
label: formData.geojson,
|
||||
},
|
||||
op: 'LIKE',
|
||||
val,
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
export const getCrossFilterDataMask = ({
|
||||
data,
|
||||
filterState,
|
||||
formData,
|
||||
}: {
|
||||
data: PickingInfo;
|
||||
filterState?: FilterState;
|
||||
formData: LayerFormData;
|
||||
}) => {
|
||||
let values: FilterState['value'] = [];
|
||||
let filters: QueryObjectFilterClause[] = [];
|
||||
let customColumnLabel: string | undefined;
|
||||
|
||||
if (formData.start_spatial && formData.end_spatial) {
|
||||
const result = getStartEndSpatialFilters({ formData, data });
|
||||
({ values, filters, customColumnLabel } = result);
|
||||
} else if (formData.spatial?.type) {
|
||||
const result = getSpatialFilters({ formData, data });
|
||||
({ values, filters, customColumnLabel } = result);
|
||||
} else if (formData.line_column) {
|
||||
const result = getLineColumnFilters({ formData, data });
|
||||
({ values, filters, customColumnLabel } = result);
|
||||
} else if (formData.geojson) {
|
||||
const result = getGeojsonFilters({ formData, data });
|
||||
({ values, filters, customColumnLabel } = result);
|
||||
} else {
|
||||
throw new Error('No valid spatial configuration found in form data');
|
||||
}
|
||||
|
||||
const isSelected =
|
||||
values &&
|
||||
filterState?.value?.every(
|
||||
(val: string, i: number) => val.toString() === values[i].toString(),
|
||||
);
|
||||
|
||||
if (isSelected) {
|
||||
values = [];
|
||||
}
|
||||
|
||||
return {
|
||||
dataMask: {
|
||||
extraFormData: {
|
||||
filters: values.length ? filters : [],
|
||||
},
|
||||
filterState: {
|
||||
value: values.length ? values : null,
|
||||
...(customColumnLabel && values.length ? { customColumnLabel } : {}),
|
||||
},
|
||||
},
|
||||
isCurrentValueSelected: isSelected || false,
|
||||
} as ContextMenuFilters['crossFilter'];
|
||||
};
|
||||
@@ -66,7 +66,8 @@ const CrossFilterTag = (props: {
|
||||
useCSSTextTruncation<HTMLSpanElement>();
|
||||
const [valueRef, valueIsTruncated] = useCSSTextTruncation<HTMLSpanElement>();
|
||||
|
||||
const columnLabel = getColumnLabel(filter.column ?? '');
|
||||
const columnLabel =
|
||||
filter.customColumnLabel || getColumnLabel(filter.column ?? '');
|
||||
|
||||
return (
|
||||
<StyledTag
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
/**
|
||||
* 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 { CHART_TYPE } from 'src/dashboard/util/componentTypes';
|
||||
import { getCrossFilterIndicator } from './selectors';
|
||||
|
||||
describe('getCrossFilterIndicator', () => {
|
||||
const chartId = 123;
|
||||
const chartLayoutItems = [
|
||||
{
|
||||
id: 'chart-123',
|
||||
type: CHART_TYPE,
|
||||
children: [],
|
||||
parents: ['ROOT_ID'],
|
||||
meta: {
|
||||
chartId,
|
||||
sliceName: 'Test Chart',
|
||||
uuid: 'uuid-123',
|
||||
height: 10,
|
||||
width: 10,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
it('returns correct indicator with label from filterState.label', () => {
|
||||
const dataMask = {
|
||||
filterState: { label: 'foo', value: 'bar' },
|
||||
extraFormData: {},
|
||||
};
|
||||
const result = getCrossFilterIndicator(chartId, dataMask, chartLayoutItems);
|
||||
expect(result).toEqual({
|
||||
column: undefined,
|
||||
name: 'Test Chart',
|
||||
path: ['ROOT_ID', 'chart-123'],
|
||||
value: 'foo',
|
||||
});
|
||||
});
|
||||
|
||||
it('returns correct indicator with label from filterState.value', () => {
|
||||
const dataMask = {
|
||||
filterState: { value: ['bar', 'baz'] },
|
||||
extraFormData: {},
|
||||
};
|
||||
const result = getCrossFilterIndicator(chartId, dataMask, chartLayoutItems);
|
||||
expect(result).toEqual({
|
||||
column: undefined,
|
||||
name: 'Test Chart',
|
||||
path: ['ROOT_ID', 'chart-123'],
|
||||
value: 'bar, baz',
|
||||
});
|
||||
});
|
||||
|
||||
it('returns correct indicator with column and customColumnLabel', () => {
|
||||
const dataMask = {
|
||||
filterState: {
|
||||
value: 'valA',
|
||||
filters: { col: 'col' },
|
||||
customColumnLabel: 'label',
|
||||
},
|
||||
extraFormData: {},
|
||||
};
|
||||
const result = getCrossFilterIndicator(chartId, dataMask, chartLayoutItems);
|
||||
expect(result).toEqual({
|
||||
column: 'col',
|
||||
name: 'Test Chart',
|
||||
path: ['ROOT_ID', 'chart-123'],
|
||||
value: 'valA',
|
||||
customColumnLabel: 'label',
|
||||
});
|
||||
});
|
||||
|
||||
it('returns correct indicator with column from extraFormData.filters', () => {
|
||||
const filterClause = { col: 'colB', op: 'IS NOT NULL' as const };
|
||||
const dataMask = {
|
||||
filterState: { value: 'valB' },
|
||||
extraFormData: { filters: [filterClause] },
|
||||
};
|
||||
const result = getCrossFilterIndicator(chartId, dataMask, chartLayoutItems);
|
||||
expect(result).toEqual({
|
||||
column: 'colB',
|
||||
name: 'Test Chart',
|
||||
path: ['ROOT_ID', 'chart-123'],
|
||||
value: 'valB',
|
||||
});
|
||||
});
|
||||
|
||||
it('returns correct indicator with column from filterState.filters', () => {
|
||||
const dataMask = {
|
||||
filterState: { value: 'valC', filters: { colC: 'something' } },
|
||||
extraFormData: {},
|
||||
};
|
||||
const result = getCrossFilterIndicator(chartId, dataMask, chartLayoutItems);
|
||||
expect(result).toEqual({
|
||||
column: 'colC',
|
||||
name: 'Test Chart',
|
||||
path: ['ROOT_ID', 'chart-123'],
|
||||
value: 'valC',
|
||||
});
|
||||
});
|
||||
|
||||
it('returns empty name and path if chartLayoutItem is not found', () => {
|
||||
const dataMask = {
|
||||
filterState: { value: 'valD' },
|
||||
extraFormData: {},
|
||||
};
|
||||
const result = getCrossFilterIndicator(999, dataMask, chartLayoutItems);
|
||||
expect(result).toEqual({
|
||||
column: undefined,
|
||||
name: '',
|
||||
path: [''],
|
||||
value: 'valD',
|
||||
});
|
||||
});
|
||||
|
||||
it('returns null value if no label or value in filterState', () => {
|
||||
const dataMask = {
|
||||
filterState: {},
|
||||
extraFormData: {},
|
||||
};
|
||||
const result = getCrossFilterIndicator(chartId, dataMask, chartLayoutItems);
|
||||
expect(result).toEqual({
|
||||
column: undefined,
|
||||
name: 'Test Chart',
|
||||
path: ['ROOT_ID', 'chart-123'],
|
||||
value: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -157,6 +157,7 @@ export type Indicator = {
|
||||
value?: any;
|
||||
status?: IndicatorStatus;
|
||||
path?: string[];
|
||||
customColumnLabel?: string;
|
||||
};
|
||||
|
||||
export type CrossFilterIndicator = Indicator & { emitterId: number };
|
||||
@@ -170,6 +171,7 @@ export const getCrossFilterIndicator = (
|
||||
const filters = dataMask?.extraFormData?.filters;
|
||||
const label = extractLabel(filterState);
|
||||
const filtersState = filterState?.filters;
|
||||
const customColumnLabel = filterState?.customColumnLabel;
|
||||
const column =
|
||||
filters?.[0]?.col || (filtersState && Object.keys(filtersState)[0]);
|
||||
|
||||
@@ -185,6 +187,7 @@ export const getCrossFilterIndicator = (
|
||||
'',
|
||||
path: [...(chartLayoutItem?.parents ?? []), chartLayoutItem?.id || ''],
|
||||
value: label,
|
||||
customColumnLabel,
|
||||
};
|
||||
return filterObject;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user