feat: Add Deck.gl Contour Layer (#24154)

This commit is contained in:
Matthew Chiang
2023-10-10 04:20:37 -05:00
committed by GitHub
parent 42d0474cc2
commit 512fb9a0bd
18 changed files with 1092 additions and 3 deletions

View File

@@ -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);

View File

@@ -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

View File

@@ -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,
});
}
}

View File

@@ -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' }),
],
});
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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>

View File

@@ -32,6 +32,7 @@ export interface OptionProps {
isExtra?: boolean;
datasourceWarningMessage?: string;
canDelete?: boolean;
tooltipOverlay?: ReactNode;
}
export interface OptionItemInterface {

View File

@@ -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;

View File

@@ -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;

View File

@@ -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"""