mirror of
https://github.com/apache/superset.git
synced 2026-04-19 08:04:53 +00:00
feat: Add Deck.gl Contour Layer (#24154)
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* 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 { ContourLayer } from 'deck.gl';
|
||||
import React from 'react';
|
||||
import { t } from '@superset-ui/core';
|
||||
import { commonLayerProps } from '../common';
|
||||
import sandboxedEval from '../../utils/sandbox';
|
||||
import { createDeckGLComponent, getLayerType } from '../../factory';
|
||||
import { ColorType } from '../../types';
|
||||
import TooltipRow from '../../TooltipRow';
|
||||
|
||||
function setTooltipContent(o: any) {
|
||||
return (
|
||||
<div className="deckgl-tooltip">
|
||||
<TooltipRow
|
||||
label={t('Centroid (Longitude and Latitude): ')}
|
||||
value={`(${o?.coordinate[0]}, ${o?.coordinate[1]})`}
|
||||
/>
|
||||
<TooltipRow
|
||||
label={t('Threshold: ')}
|
||||
value={`${o?.object?.contour?.threshold}`}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export const getLayer: getLayerType<unknown> = function (
|
||||
formData,
|
||||
payload,
|
||||
onAddFilter,
|
||||
setTooltip,
|
||||
) {
|
||||
const fd = formData;
|
||||
const {
|
||||
aggregation = 'SUM',
|
||||
js_data_mutator: jsFnMutator,
|
||||
contours: rawContours,
|
||||
cellSize = '200',
|
||||
} = fd;
|
||||
let data = payload.data.features;
|
||||
|
||||
const contours = rawContours?.map(
|
||||
(contour: {
|
||||
color: ColorType;
|
||||
lowerThreshold: number;
|
||||
upperThreshold?: number;
|
||||
strokeWidth?: number;
|
||||
}) => {
|
||||
const { lowerThreshold, upperThreshold, color, strokeWidth } = contour;
|
||||
if (upperThreshold) {
|
||||
// Isoband format
|
||||
return {
|
||||
threshold: [lowerThreshold, upperThreshold],
|
||||
color: [color.r, color.g, color.b],
|
||||
};
|
||||
}
|
||||
// Isoline format
|
||||
return {
|
||||
threshold: lowerThreshold,
|
||||
color: [color.r, color.g, color.b],
|
||||
strokeWidth,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
if (jsFnMutator) {
|
||||
// Applying user defined data mutator if defined
|
||||
const jsFnMutatorFunction = sandboxedEval(fd.js_data_mutator);
|
||||
data = jsFnMutatorFunction(data);
|
||||
}
|
||||
|
||||
return new ContourLayer({
|
||||
id: `contourLayer-${fd.slice_id}`,
|
||||
data,
|
||||
contours,
|
||||
cellSize: Number(cellSize || '200'),
|
||||
aggregation: aggregation.toUpperCase(),
|
||||
getPosition: (d: { position: number[]; weight: number }) => d.position,
|
||||
getWeight: (d: { weight: number }) => d.weight || 0,
|
||||
...commonLayerProps(fd, setTooltip, setTooltipContent),
|
||||
});
|
||||
};
|
||||
|
||||
function getPoints(data: any[]) {
|
||||
return data.map(d => d.position);
|
||||
}
|
||||
|
||||
export default createDeckGLComponent(getLayer, getPoints);
|
||||
@@ -0,0 +1,133 @@
|
||||
/**
|
||||
* 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,
|
||||
getStandardizedControls,
|
||||
sections,
|
||||
} from '@superset-ui/chart-controls';
|
||||
import { t, validateNonEmpty } from '@superset-ui/core';
|
||||
import {
|
||||
autozoom,
|
||||
filterNulls,
|
||||
jsColumns,
|
||||
jsDataMutator,
|
||||
jsOnclickHref,
|
||||
jsTooltip,
|
||||
mapboxStyle,
|
||||
spatial,
|
||||
viewport,
|
||||
} from '../../utilities/Shared_DeckGL';
|
||||
|
||||
const config: ControlPanelConfig = {
|
||||
controlPanelSections: [
|
||||
sections.legacyRegularTime,
|
||||
{
|
||||
label: t('Query'),
|
||||
expanded: true,
|
||||
controlSetRows: [
|
||||
[spatial],
|
||||
['row_limit'],
|
||||
['size'],
|
||||
[filterNulls],
|
||||
['adhoc_filters'],
|
||||
],
|
||||
},
|
||||
{
|
||||
label: t('Map'),
|
||||
expanded: true,
|
||||
controlSetRows: [
|
||||
[mapboxStyle, viewport],
|
||||
[autozoom],
|
||||
[
|
||||
{
|
||||
name: 'cellSize',
|
||||
config: {
|
||||
type: 'TextControl',
|
||||
label: t('Cell Size'),
|
||||
default: 300,
|
||||
isInt: true,
|
||||
description: t('The size of each cell in meters'),
|
||||
renderTrigger: true,
|
||||
clearable: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
name: 'aggregation',
|
||||
config: {
|
||||
type: 'SelectControl',
|
||||
label: t('Aggregation'),
|
||||
description: t(
|
||||
'The function to use when aggregating points into groups',
|
||||
),
|
||||
default: 'sum',
|
||||
clearable: false,
|
||||
renderTrigger: true,
|
||||
choices: [
|
||||
['sum', t('sum')],
|
||||
['min', t('min')],
|
||||
['max', t('max')],
|
||||
['mean', t('mean')],
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
name: 'contours',
|
||||
config: {
|
||||
type: 'ContourControl',
|
||||
label: t('Contours'),
|
||||
renderTrigger: true,
|
||||
description: t(
|
||||
'Define contour layers. Isolines represent a collection of line segments that ' +
|
||||
'serparate the area above and below a given threshold. Isobands represent a ' +
|
||||
'collection of polygons that fill the are containing values in a given ' +
|
||||
'threshold range.',
|
||||
),
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
{
|
||||
label: t('Advanced'),
|
||||
controlSetRows: [
|
||||
[jsColumns],
|
||||
[jsDataMutator],
|
||||
[jsTooltip],
|
||||
[jsOnclickHref],
|
||||
],
|
||||
},
|
||||
],
|
||||
controlOverrides: {
|
||||
size: {
|
||||
label: t('Weight'),
|
||||
description: t("Metric used as a weight for the grid's coloring"),
|
||||
validators: [validateNonEmpty],
|
||||
},
|
||||
},
|
||||
formDataOverrides: formData => ({
|
||||
...formData,
|
||||
size: getStandardizedControls().shiftMetric(),
|
||||
}),
|
||||
};
|
||||
|
||||
export default config;
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 63 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 203 KiB |
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* 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 { t, ChartMetadata, ChartPlugin } from '@superset-ui/core';
|
||||
import transformProps from '../../transformProps';
|
||||
import controlPanel from './controlPanel';
|
||||
import thumbnail from './images/thumbnail.png';
|
||||
|
||||
const metadata = new ChartMetadata({
|
||||
category: t('Map'),
|
||||
credits: ['https://uber.github.io/deck.gl'],
|
||||
description: t(
|
||||
'Uses Gaussian Kernel Density Estimation to visualize spatial distribution of data',
|
||||
),
|
||||
name: t('deck.gl Countour'),
|
||||
thumbnail,
|
||||
useLegacyApi: true,
|
||||
tags: [t('deckGL'), t('Spatial'), t('Comparison'), t('Experimental')],
|
||||
});
|
||||
|
||||
export default class ContourChartPlugin extends ChartPlugin {
|
||||
constructor() {
|
||||
super({
|
||||
loadChart: () => import('./Contour'),
|
||||
controlPanel,
|
||||
metadata,
|
||||
transformProps,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,7 @@ import PathChartPlugin from './layers/Path';
|
||||
import PolygonChartPlugin from './layers/Polygon';
|
||||
import ScatterChartPlugin from './layers/Scatter';
|
||||
import ScreengridChartPlugin from './layers/Screengrid';
|
||||
import ContourChartPlugin from './layers/Contour';
|
||||
|
||||
export default class DeckGLChartPreset extends Preset {
|
||||
constructor() {
|
||||
@@ -43,6 +44,7 @@ export default class DeckGLChartPreset extends Preset {
|
||||
new PolygonChartPlugin().configure({ key: 'deck_polygon' }),
|
||||
new ScatterChartPlugin().configure({ key: 'deck_scatter' }),
|
||||
new ScreengridChartPlugin().configure({ key: 'deck_screengrid' }),
|
||||
new ContourChartPlugin().configure({ key: 'deck_contour' }),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -21,3 +21,9 @@
|
||||
|
||||
export type Range = [number, number];
|
||||
export type Point = [number, number];
|
||||
export interface ColorType {
|
||||
r: number;
|
||||
g: number;
|
||||
b: number;
|
||||
a: number;
|
||||
}
|
||||
|
||||
@@ -17,4 +17,55 @@
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
declare module '*.png';
|
||||
declare module 'deck.gl' {
|
||||
import { Layer, LayerProps } from '@deck.gl/core';
|
||||
|
||||
interface HeatmapLayerProps<T extends object = any> extends LayerProps<T> {
|
||||
id?: string;
|
||||
data?: T[];
|
||||
getPosition?: (d: T) => number[] | null | undefined;
|
||||
getWeight?: (d: T) => number | null | undefined;
|
||||
radiusPixels?: number;
|
||||
colorRange?: number[][];
|
||||
threshold?: number;
|
||||
intensity?: number;
|
||||
aggregation?: string;
|
||||
}
|
||||
|
||||
interface ContourLayerProps<T extends object = any> extends LayerProps<T> {
|
||||
id?: string;
|
||||
data?: T[];
|
||||
getPosition?: (d: T) => number[] | null | undefined;
|
||||
getWeight?: (d: T) => number | null | undefined;
|
||||
contours: {
|
||||
color?: ColorType | undefined;
|
||||
lowerThreshold?: any | undefined;
|
||||
upperThreshold?: any | undefined;
|
||||
strokeWidth?: any | undefined;
|
||||
zIndex?: any | undefined;
|
||||
};
|
||||
cellSize: number;
|
||||
colorRange?: number[][];
|
||||
intensity?: number;
|
||||
aggregation?: string;
|
||||
}
|
||||
|
||||
export class HeatmapLayer<T extends object = any> extends Layer<
|
||||
T,
|
||||
HeatmapLayerProps<T>
|
||||
> {
|
||||
constructor(props: HeatmapLayerProps<T>);
|
||||
}
|
||||
|
||||
export class ContourLayer<T extends object = any> extends Layer<
|
||||
T,
|
||||
ContourLayerProps<T>
|
||||
> {
|
||||
constructor(props: ContourLayerProps<T>);
|
||||
}
|
||||
}
|
||||
|
||||
declare module '*.png' {
|
||||
const value: any;
|
||||
export default value;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with work for additional information
|
||||
* regarding copyright ownership. The ASF licenses file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { styled, t } from '@superset-ui/core';
|
||||
import { ContourOptionProps } from './types';
|
||||
import ContourPopoverTrigger from './ContourPopoverTrigger';
|
||||
import OptionWrapper from '../DndColumnSelectControl/OptionWrapper';
|
||||
|
||||
const StyledOptionWrapper = styled(OptionWrapper)`
|
||||
max-width: 100%;
|
||||
min-width: 100%;
|
||||
`;
|
||||
|
||||
const StyledListItem = styled.li`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const ColorPatch = styled.div<{ formattedColor: string }>`
|
||||
background-color: ${({ formattedColor }) => formattedColor};
|
||||
height: ${({ theme }) => theme.gridUnit}px;
|
||||
width: ${({ theme }) => theme.gridUnit}px;
|
||||
margin: 0 ${({ theme }) => theme.gridUnit}px;
|
||||
`;
|
||||
|
||||
const ContourOption = ({
|
||||
contour,
|
||||
index,
|
||||
saveContour,
|
||||
onClose,
|
||||
onShift,
|
||||
}: ContourOptionProps) => {
|
||||
const { lowerThreshold, upperThreshold, color, strokeWidth } = contour;
|
||||
|
||||
const isIsoband = upperThreshold;
|
||||
|
||||
const formattedColor = color
|
||||
? `rgba(${color.r}, ${color.g}, ${color.b}, 1)`
|
||||
: 'undefined';
|
||||
|
||||
const formatIsoline = (threshold: number, width: number) =>
|
||||
`${t('Threshold')}: ${threshold}, ${t('color')}: ${formattedColor}, ${t(
|
||||
'stroke width',
|
||||
)}: ${width}`;
|
||||
|
||||
const formatIsoband = (threshold: number[]) =>
|
||||
`${t('Threshold')}: [${threshold[0]}, ${
|
||||
threshold[1]
|
||||
}], color: ${formattedColor}`;
|
||||
|
||||
const displayString = isIsoband
|
||||
? formatIsoband([lowerThreshold || -1, upperThreshold])
|
||||
: formatIsoline(lowerThreshold || -1, strokeWidth);
|
||||
|
||||
const overlay = (
|
||||
<div className="contour-tooltip-overlay">
|
||||
<StyledListItem>
|
||||
{t('Threshold: ')}
|
||||
{isIsoband
|
||||
? `[${lowerThreshold}, ${upperThreshold}]`
|
||||
: `${lowerThreshold}`}
|
||||
</StyledListItem>
|
||||
<StyledListItem>
|
||||
{t('Color: ')}
|
||||
<ColorPatch formattedColor={formattedColor} /> {formattedColor}
|
||||
</StyledListItem>
|
||||
{!isIsoband && (
|
||||
<StyledListItem>{`${t(
|
||||
'Stroke Width:',
|
||||
)} ${strokeWidth}`}</StyledListItem>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<ContourPopoverTrigger saveContour={saveContour} value={contour}>
|
||||
<StyledOptionWrapper
|
||||
index={index}
|
||||
label={displayString}
|
||||
type="ContourOption"
|
||||
withCaret
|
||||
clickClose={onClose}
|
||||
onShiftOptions={onShift}
|
||||
tooltipOverlay={overlay}
|
||||
/>
|
||||
</ContourPopoverTrigger>
|
||||
);
|
||||
};
|
||||
|
||||
export default ContourOption;
|
||||
@@ -0,0 +1,351 @@
|
||||
/**
|
||||
* 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 React, { useState, useEffect } from 'react';
|
||||
import { Row, Col } from 'src/components';
|
||||
import Button from 'src/components/Button';
|
||||
import Tabs from 'src/components/Tabs';
|
||||
import { legacyValidateInteger, styled, t } from '@superset-ui/core';
|
||||
import ControlHeader from '../../ControlHeader';
|
||||
import TextControl from '../TextControl';
|
||||
import ColorPickerControl from '../ColorPickerControl';
|
||||
import {
|
||||
ContourPopoverControlProps,
|
||||
ColorType,
|
||||
ContourType,
|
||||
ErrorMapType,
|
||||
} from './types';
|
||||
|
||||
enum CONTOUR_TYPES {
|
||||
Isoline = 'ISOLINE',
|
||||
Isoband = 'ISOBAND',
|
||||
}
|
||||
|
||||
const ContourActionsContainer = styled.div`
|
||||
margin-top: ${({ theme }) => theme.gridUnit * 2}px;
|
||||
`;
|
||||
|
||||
const StyledRow = styled(Row)`
|
||||
width: 100%;
|
||||
gap: ${({ theme }) => theme.gridUnit * 2}px;
|
||||
`;
|
||||
|
||||
const isIsoband = (contour: ContourType) => {
|
||||
if (Object.keys(contour).length < 4) {
|
||||
return false;
|
||||
}
|
||||
return contour.upperThreshold && contour.lowerThreshold;
|
||||
};
|
||||
|
||||
const getTabKey = (contour: ContourType | undefined) =>
|
||||
contour && isIsoband(contour) ? CONTOUR_TYPES.Isoband : CONTOUR_TYPES.Isoline;
|
||||
|
||||
const determineErrorMap = (tab: string, contour: ContourType) => {
|
||||
const errorMap: ErrorMapType = {
|
||||
lowerThreshold: [],
|
||||
upperThreshold: [],
|
||||
strokeWidth: [],
|
||||
color: [],
|
||||
};
|
||||
// Isoline and Isoband validation
|
||||
const lowerThresholdError = legacyValidateInteger(contour.lowerThreshold);
|
||||
if (lowerThresholdError) errorMap.lowerThreshold.push(lowerThresholdError);
|
||||
|
||||
// Isoline only validation
|
||||
if (tab === CONTOUR_TYPES.Isoline) {
|
||||
const strokeWidthError = legacyValidateInteger(contour.strokeWidth);
|
||||
if (strokeWidthError) errorMap.strokeWidth.push(strokeWidthError);
|
||||
}
|
||||
|
||||
// Isoband only validation
|
||||
if (tab === CONTOUR_TYPES.Isoband) {
|
||||
const upperThresholdError = legacyValidateInteger(contour.upperThreshold);
|
||||
if (upperThresholdError) errorMap.upperThreshold.push(upperThresholdError);
|
||||
if (
|
||||
!upperThresholdError &&
|
||||
!lowerThresholdError &&
|
||||
contour.upperThreshold &&
|
||||
contour.lowerThreshold
|
||||
) {
|
||||
const lower = parseFloat(contour.lowerThreshold);
|
||||
const upper = parseFloat(contour.upperThreshold);
|
||||
if (lower >= upper) {
|
||||
errorMap.lowerThreshold.push(
|
||||
t('Lower threshold must be lower than upper threshold'),
|
||||
);
|
||||
errorMap.upperThreshold.push(
|
||||
t('Upper threshold must be greater than lower threshold'),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return errorMap;
|
||||
};
|
||||
|
||||
const convertContourToNumeric = (contour: ContourType) => {
|
||||
const formattedContour = { ...contour };
|
||||
const numericKeys = ['lowerThreshold', 'upperThreshold', 'strokeWidth'];
|
||||
numericKeys.forEach(key => {
|
||||
formattedContour[key] = Number(formattedContour[key]);
|
||||
});
|
||||
return formattedContour;
|
||||
};
|
||||
|
||||
const formatIsoline = (contour: ContourType) => ({
|
||||
color: contour.color,
|
||||
lowerThreshold: contour.lowerThreshold,
|
||||
upperThreshold: undefined,
|
||||
strokeWidth: contour.strokeWidth,
|
||||
});
|
||||
|
||||
const formatIsoband = (contour: ContourType) => ({
|
||||
color: contour.color,
|
||||
lowerThreshold: contour.lowerThreshold,
|
||||
upperThreshold: contour.upperThreshold,
|
||||
strokeWidth: undefined,
|
||||
});
|
||||
|
||||
const DEFAULT_CONTOUR = {
|
||||
lowerThreshold: undefined,
|
||||
upperThreshold: undefined,
|
||||
color: undefined,
|
||||
strokeWidth: undefined,
|
||||
};
|
||||
|
||||
const ContourPopoverControl = ({
|
||||
value: initialValue,
|
||||
onSave,
|
||||
onClose,
|
||||
}: ContourPopoverControlProps) => {
|
||||
const [currentTab, setCurrentTab] = useState(getTabKey(initialValue));
|
||||
const [contour, setContour] = useState(initialValue || DEFAULT_CONTOUR);
|
||||
const [validationErrors, setValidationErrors] = useState(
|
||||
determineErrorMap(getTabKey(initialValue), initialValue || DEFAULT_CONTOUR),
|
||||
);
|
||||
const [isComplete, setIsComplete] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const isIsoband = currentTab === CONTOUR_TYPES.Isoband;
|
||||
const validLower =
|
||||
Boolean(contour.lowerThreshold) || contour.lowerThreshold === 0;
|
||||
const validUpper =
|
||||
Boolean(contour.upperThreshold) || contour.upperThreshold === 0;
|
||||
const validStrokeWidth =
|
||||
Boolean(contour.strokeWidth) || contour.strokeWidth === 0;
|
||||
const validColor =
|
||||
typeof contour.color === 'object' &&
|
||||
'r' in contour.color &&
|
||||
typeof contour.color.r === 'number' &&
|
||||
'g' in contour.color &&
|
||||
typeof contour.color.g === 'number' &&
|
||||
'b' in contour.color &&
|
||||
typeof contour.color.b === 'number' &&
|
||||
'a' in contour.color &&
|
||||
typeof contour.color.a === 'number';
|
||||
|
||||
const errors = determineErrorMap(currentTab, contour);
|
||||
if (errors !== validationErrors) setValidationErrors(errors);
|
||||
|
||||
const sectionIsComplete = isIsoband
|
||||
? validLower && validUpper && validColor
|
||||
: validLower && validColor && validStrokeWidth;
|
||||
|
||||
if (sectionIsComplete !== isComplete) setIsComplete(sectionIsComplete);
|
||||
}, [contour, currentTab]);
|
||||
|
||||
const onTabChange = (activeKey: any) => {
|
||||
setCurrentTab(activeKey);
|
||||
};
|
||||
|
||||
const updateStrokeWidth = (value: number | string) => {
|
||||
const newContour = { ...contour };
|
||||
newContour.strokeWidth = value;
|
||||
setContour(newContour);
|
||||
};
|
||||
|
||||
const updateColor = (rgb: ColorType) => {
|
||||
const newContour = { ...contour };
|
||||
newContour.color = { ...rgb, a: 100 };
|
||||
setContour(newContour);
|
||||
};
|
||||
|
||||
const updateLowerThreshold = (value: number | string) => {
|
||||
const newContour = { ...contour };
|
||||
newContour.lowerThreshold = value;
|
||||
setContour(newContour);
|
||||
};
|
||||
|
||||
const updateUpperThreshold = (value: number | string) => {
|
||||
const newContour = { ...contour };
|
||||
newContour.upperThreshold = value;
|
||||
setContour(newContour);
|
||||
};
|
||||
|
||||
const containsErrors = () => {
|
||||
const keys = Object.keys(validationErrors);
|
||||
return keys.some(key => validationErrors[key].length > 0);
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
if (isComplete && onSave) {
|
||||
const newContour =
|
||||
currentTab === CONTOUR_TYPES.Isoline
|
||||
? formatIsoline(contour)
|
||||
: formatIsoband(contour);
|
||||
onSave(convertContourToNumeric(newContour));
|
||||
if (onClose) onClose();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tabs
|
||||
id="contour-edit-tabs"
|
||||
onChange={onTabChange}
|
||||
defaultActiveKey={getTabKey(initialValue)}
|
||||
>
|
||||
<Tabs.TabPane
|
||||
className="adhoc-filter-edit-tab"
|
||||
key={CONTOUR_TYPES.Isoline}
|
||||
tab={t('Isoline')}
|
||||
>
|
||||
<div key={CONTOUR_TYPES.Isoline} className="isoline-popover-section">
|
||||
<StyledRow>
|
||||
<Col flex="1">
|
||||
<ControlHeader
|
||||
name="isoline-threshold"
|
||||
label={t('Threshold')}
|
||||
description={t(
|
||||
'Defines the value that determines the boundary between different regions or levels in the data ',
|
||||
)}
|
||||
validationErrors={validationErrors.lowerThreshold}
|
||||
hovered
|
||||
/>
|
||||
<TextControl
|
||||
value={contour.lowerThreshold}
|
||||
onChange={updateLowerThreshold}
|
||||
/>
|
||||
</Col>
|
||||
</StyledRow>
|
||||
<StyledRow>
|
||||
<Col flex="1">
|
||||
<ControlHeader
|
||||
name="isoline-stroke-width"
|
||||
label={t('Stroke Width')}
|
||||
description={t('The width of the Isoline in pixels')}
|
||||
validationErrors={validationErrors.strokeWidth}
|
||||
hovered
|
||||
/>
|
||||
<TextControl
|
||||
value={contour.strokeWidth || ''}
|
||||
onChange={updateStrokeWidth}
|
||||
/>
|
||||
</Col>
|
||||
<Col flex="1">
|
||||
<ControlHeader
|
||||
name="isoline-color"
|
||||
label={t('Color')}
|
||||
description={t('The color of the isoline')}
|
||||
validationErrors={validationErrors.color}
|
||||
hovered
|
||||
/>
|
||||
<ColorPickerControl
|
||||
value={typeof contour === 'object' && contour?.color}
|
||||
onChange={updateColor}
|
||||
/>
|
||||
</Col>
|
||||
</StyledRow>
|
||||
</div>
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane
|
||||
className="adhoc-filter-edit-tab"
|
||||
key={CONTOUR_TYPES.Isoband}
|
||||
tab={t('Isoband')}
|
||||
>
|
||||
<div key={CONTOUR_TYPES.Isoband} className="isoline-popover-section">
|
||||
<StyledRow>
|
||||
<Col flex="1">
|
||||
<ControlHeader
|
||||
name="isoband-threshold-lower"
|
||||
label={t('Lower Threshold')}
|
||||
description={t(
|
||||
'The lower limit of the threshold range of the Isoband',
|
||||
)}
|
||||
validationErrors={validationErrors.lowerThreshold}
|
||||
hovered
|
||||
/>
|
||||
<TextControl
|
||||
value={contour.lowerThreshold || ''}
|
||||
onChange={updateLowerThreshold}
|
||||
/>
|
||||
</Col>
|
||||
<Col flex="1">
|
||||
<ControlHeader
|
||||
name="isoband-threshold-upper"
|
||||
label={t('Upper Threshold')}
|
||||
description={t(
|
||||
'The upper limit of the threshold range of the Isoband',
|
||||
)}
|
||||
validationErrors={validationErrors.upperThreshold}
|
||||
hovered
|
||||
/>
|
||||
<TextControl
|
||||
value={contour.upperThreshold || ''}
|
||||
onChange={updateUpperThreshold}
|
||||
/>
|
||||
</Col>
|
||||
</StyledRow>
|
||||
<StyledRow>
|
||||
<Col flex="1">
|
||||
<ControlHeader
|
||||
name="isoband-color"
|
||||
label={t('Color')}
|
||||
description={t('The color of the isoband')}
|
||||
validationErrors={validationErrors.color}
|
||||
hovered
|
||||
/>
|
||||
<ColorPickerControl
|
||||
value={contour?.color}
|
||||
onChange={updateColor}
|
||||
/>
|
||||
</Col>
|
||||
</StyledRow>
|
||||
</div>
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
<ContourActionsContainer>
|
||||
<Button buttonSize="small" onClick={onClose} cta>
|
||||
{t('Close')}
|
||||
</Button>
|
||||
<Button
|
||||
data-test="adhoc-filter-edit-popover-save-button"
|
||||
disabled={!isComplete || containsErrors()}
|
||||
buttonStyle="primary"
|
||||
buttonSize="small"
|
||||
className="m-r-5"
|
||||
onClick={handleSave}
|
||||
cta
|
||||
>
|
||||
{t('Save')}
|
||||
</Button>
|
||||
</ContourActionsContainer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ContourPopoverControl;
|
||||
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* 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 React, { useState } from 'react';
|
||||
import ContourPopoverControl from './ContourPopoverControl';
|
||||
import ControlPopover from '../ControlPopover/ControlPopover';
|
||||
import { ContourPopoverTriggerProps } from './types';
|
||||
|
||||
const ContourPopoverTrigger = ({
|
||||
value: initialValue,
|
||||
saveContour,
|
||||
isControlled,
|
||||
visible: controlledVisibility,
|
||||
toggleVisibility,
|
||||
...props
|
||||
}: ContourPopoverTriggerProps) => {
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
|
||||
const visible = isControlled ? controlledVisibility : isVisible;
|
||||
const setVisibility =
|
||||
isControlled && toggleVisibility ? toggleVisibility : setIsVisible;
|
||||
|
||||
const popoverContent = (
|
||||
<ContourPopoverControl
|
||||
value={initialValue}
|
||||
onSave={saveContour}
|
||||
onClose={() => setVisibility(false)}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<ControlPopover
|
||||
trigger="click"
|
||||
content={popoverContent}
|
||||
defaultVisible={visible}
|
||||
visible={visible}
|
||||
onVisibleChange={setVisibility}
|
||||
destroyTooltipOnHide
|
||||
>
|
||||
{props.children}
|
||||
</ControlPopover>
|
||||
);
|
||||
};
|
||||
|
||||
export default ContourPopoverTrigger;
|
||||
@@ -0,0 +1,143 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with work for additional information
|
||||
* regarding copyright ownership. The ASF licenses file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { styled, t } from '@superset-ui/core';
|
||||
import DndSelectLabel from 'src/explore/components/controls/DndColumnSelectControl/DndSelectLabel';
|
||||
import ContourPopoverTrigger from './ContourPopoverTrigger';
|
||||
import ContourOption from './ContourOption';
|
||||
import { ContourType, ContourControlProps } from './types';
|
||||
|
||||
const DEFAULT_CONTOURS: ContourType[] = [
|
||||
{
|
||||
lowerThreshold: 4,
|
||||
color: { r: 255, g: 0, b: 255, a: 100 },
|
||||
strokeWidth: 1,
|
||||
zIndex: 0,
|
||||
},
|
||||
{
|
||||
lowerThreshold: 5,
|
||||
color: { r: 0, g: 255, b: 0, a: 100 },
|
||||
strokeWidth: 2,
|
||||
zIndex: 1,
|
||||
},
|
||||
{
|
||||
lowerThreshold: 6,
|
||||
upperThreshold: 10,
|
||||
color: { r: 0, g: 0, b: 255, a: 100 },
|
||||
zIndex: 2,
|
||||
},
|
||||
];
|
||||
|
||||
const NewContourFormatPlaceholder = styled('div')`
|
||||
position: relative;
|
||||
width: calc(100% - ${({ theme }) => theme.gridUnit}px);
|
||||
bottom: ${({ theme }) => theme.gridUnit * 4}px;
|
||||
left: 0;
|
||||
`;
|
||||
|
||||
const ContourControl = ({ onChange, ...props }: ContourControlProps) => {
|
||||
const [popoverVisible, setPopoverVisible] = useState(false);
|
||||
const [contours, setContours] = useState<ContourType[]>(
|
||||
props?.value ? props?.value : DEFAULT_CONTOURS,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
// add z-index to contours
|
||||
const newContours = contours.map((contour, index) => ({
|
||||
...contour,
|
||||
zIndex: (index + 1) * 10,
|
||||
}));
|
||||
onChange?.(newContours);
|
||||
}, [onChange, contours]);
|
||||
|
||||
const togglePopover = (visible: boolean) => {
|
||||
setPopoverVisible(visible);
|
||||
};
|
||||
|
||||
const handleClickGhostButton = () => {
|
||||
togglePopover(true);
|
||||
};
|
||||
|
||||
const saveContour = (contour: ContourType) => {
|
||||
setContours([...contours, contour]);
|
||||
togglePopover(false);
|
||||
};
|
||||
|
||||
const removeContour = (index: number) => {
|
||||
const newContours = [...contours];
|
||||
newContours.splice(index, 1);
|
||||
setContours(newContours);
|
||||
};
|
||||
|
||||
const onShiftContour = (hoverIndex: number, dragIndex: number) => {
|
||||
const newContours = [...contours];
|
||||
[newContours[hoverIndex], newContours[dragIndex]] = [
|
||||
newContours[dragIndex],
|
||||
newContours[hoverIndex],
|
||||
];
|
||||
setContours(newContours);
|
||||
};
|
||||
|
||||
const editContour = (contour: ContourType, index: number) => {
|
||||
const newContours = [...contours];
|
||||
newContours[index] = contour;
|
||||
setContours(newContours);
|
||||
};
|
||||
|
||||
const valuesRenderer = () =>
|
||||
contours.map((contour, index) => (
|
||||
<ContourOption
|
||||
key={index}
|
||||
saveContour={(newContour: ContourType) =>
|
||||
editContour(newContour, index)
|
||||
}
|
||||
contour={contour}
|
||||
index={index}
|
||||
onClose={removeContour}
|
||||
onShift={onShiftContour}
|
||||
/>
|
||||
));
|
||||
|
||||
const ghostButtonText = t('Click to add a contour');
|
||||
|
||||
return (
|
||||
<>
|
||||
<DndSelectLabel
|
||||
onDrop={() => {}}
|
||||
canDrop={() => true}
|
||||
valuesRenderer={valuesRenderer}
|
||||
accept={[]}
|
||||
ghostButtonText={ghostButtonText}
|
||||
onClickGhostButton={handleClickGhostButton}
|
||||
{...props}
|
||||
/>
|
||||
<ContourPopoverTrigger
|
||||
saveContour={saveContour}
|
||||
isControlled
|
||||
visible={popoverVisible}
|
||||
toggleVisibility={setPopoverVisible}
|
||||
>
|
||||
<NewContourFormatPlaceholder />
|
||||
</ContourPopoverTrigger>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ContourControl;
|
||||
@@ -0,0 +1,55 @@
|
||||
import { OptionValueType } from 'src/explore/components/controls/DndColumnSelectControl/types';
|
||||
import { ControlComponentProps } from 'src/explore/components/Control';
|
||||
|
||||
export interface ColorType {
|
||||
r: number;
|
||||
g: number;
|
||||
b: number;
|
||||
a: number;
|
||||
}
|
||||
|
||||
export interface ContourType extends OptionValueType {
|
||||
color?: ColorType | undefined;
|
||||
lowerThreshold?: any | undefined;
|
||||
upperThreshold?: any | undefined;
|
||||
strokeWidth?: any | undefined;
|
||||
}
|
||||
|
||||
export interface ErrorMapType {
|
||||
lowerThreshold: string[];
|
||||
upperThreshold: string[];
|
||||
strokeWidth: string[];
|
||||
color: string[];
|
||||
}
|
||||
|
||||
export interface ContourControlProps
|
||||
extends ControlComponentProps<OptionValueType[]> {
|
||||
contours?: {};
|
||||
}
|
||||
|
||||
export interface ContourPopoverTriggerProps {
|
||||
description?: string;
|
||||
hovered?: boolean;
|
||||
value?: ContourType;
|
||||
children?: React.ReactNode;
|
||||
saveContour: (contour: ContourType) => void;
|
||||
isControlled?: boolean;
|
||||
visible?: boolean;
|
||||
toggleVisibility?: (visibility: boolean) => void;
|
||||
}
|
||||
|
||||
export interface ContourPopoverControlProps {
|
||||
description?: string;
|
||||
hovered?: boolean;
|
||||
value?: ContourType;
|
||||
onSave?: (contour: ContourType) => void;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
export interface ContourOptionProps {
|
||||
contour: ContourType;
|
||||
index: number;
|
||||
saveContour: (contour: ContourType) => void;
|
||||
onClose: (index: number) => void;
|
||||
onShift: (hoverIndex: number, dragIndex: number) => void;
|
||||
}
|
||||
@@ -59,6 +59,7 @@ export default function OptionWrapper(
|
||||
isExtra,
|
||||
datasourceWarningMessage,
|
||||
canDelete = true,
|
||||
tooltipOverlay,
|
||||
...rest
|
||||
} = props;
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
@@ -123,12 +124,20 @@ export default function OptionWrapper(
|
||||
(!isDragging &&
|
||||
labelRef &&
|
||||
labelRef.current &&
|
||||
labelRef.current.scrollWidth > labelRef.current.clientWidth);
|
||||
labelRef.current.scrollWidth > labelRef.current.clientWidth) ||
|
||||
(!isDragging && tooltipOverlay);
|
||||
|
||||
const LabelContent = () => {
|
||||
if (!shouldShowTooltip) {
|
||||
return <span>{label}</span>;
|
||||
}
|
||||
if (tooltipOverlay) {
|
||||
return (
|
||||
<Tooltip overlay={tooltipOverlay}>
|
||||
<span>{label}</span>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Tooltip title={tooltipTitle || label}>
|
||||
<span>{label}</span>
|
||||
|
||||
@@ -32,6 +32,7 @@ export interface OptionProps {
|
||||
isExtra?: boolean;
|
||||
datasourceWarningMessage?: string;
|
||||
canDelete?: boolean;
|
||||
tooltipOverlay?: ReactNode;
|
||||
}
|
||||
|
||||
export interface OptionItemInterface {
|
||||
|
||||
@@ -30,7 +30,7 @@ export interface TextControlProps<T extends InputValueType = InputValueType> {
|
||||
disabled?: boolean;
|
||||
isFloat?: boolean;
|
||||
isInt?: boolean;
|
||||
onChange?: (value: T, errors: any) => {};
|
||||
onChange?: (value: T, errors: any) => void;
|
||||
onFocus?: () => {};
|
||||
placeholder?: string;
|
||||
value?: T | null;
|
||||
|
||||
@@ -40,6 +40,7 @@ import MetricsControl from './MetricControl/MetricsControl';
|
||||
import AdhocFilterControl from './FilterControl/AdhocFilterControl';
|
||||
import FilterBoxItemControl from './FilterBoxItemControl';
|
||||
import ConditionalFormattingControl from './ConditionalFormattingControl';
|
||||
import ContourControl from './ContourControl';
|
||||
import DndColumnSelectControl, {
|
||||
DndColumnSelect,
|
||||
DndFilterSelect,
|
||||
@@ -80,6 +81,7 @@ const controlMap = {
|
||||
FilterBoxItemControl,
|
||||
ConditionalFormattingControl,
|
||||
XAxisSortControl,
|
||||
ContourControl,
|
||||
...sharedControlComponents,
|
||||
};
|
||||
export default controlMap;
|
||||
|
||||
@@ -2411,6 +2411,27 @@ class DeckHeatmap(BaseDeckGLViz):
|
||||
return super().get_data(df)
|
||||
|
||||
|
||||
class DeckContour(BaseDeckGLViz):
|
||||
|
||||
"""deck.gl's ContourLayer"""
|
||||
|
||||
viz_type = "deck_contour"
|
||||
verbose_name = _("Deck.gl - Contour")
|
||||
spatial_control_keys = ["spatial"]
|
||||
|
||||
def get_properties(self, data: dict[str, Any]) -> dict[str, Any]:
|
||||
return {
|
||||
"position": data.get("spatial"),
|
||||
"weight": (data.get(self.metric_label) if self.metric_label else None) or 1,
|
||||
}
|
||||
|
||||
def get_data(self, df: pd.DataFrame) -> VizData:
|
||||
self.metric_label = ( # pylint: disable=attribute-defined-outside-init
|
||||
utils.get_metric_name(self.metric) if self.metric else None
|
||||
)
|
||||
return super().get_data(df)
|
||||
|
||||
|
||||
class DeckGeoJson(BaseDeckGLViz):
|
||||
|
||||
"""deck.gl's GeoJSONLayer"""
|
||||
|
||||
Reference in New Issue
Block a user