diff --git a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Contour/Contour.tsx b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Contour/Contour.tsx new file mode 100644 index 0000000000000..2c46bd6fc257f --- /dev/null +++ b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Contour/Contour.tsx @@ -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 ( +
+ + +
+ ); +} +export const getLayer: getLayerType = 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); diff --git a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Contour/controlPanel.ts b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Contour/controlPanel.ts new file mode 100644 index 0000000000000..238029aada4b6 --- /dev/null +++ b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Contour/controlPanel.ts @@ -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; diff --git a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Contour/images/thumbnail.png b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Contour/images/thumbnail.png new file mode 100644 index 0000000000000..eb9b541307cf1 Binary files /dev/null and b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Contour/images/thumbnail.png differ diff --git a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Contour/images/thumbnailLarge.png b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Contour/images/thumbnailLarge.png new file mode 100644 index 0000000000000..fef905c95ff79 Binary files /dev/null and b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Contour/images/thumbnailLarge.png differ diff --git a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Contour/index.ts b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Contour/index.ts new file mode 100644 index 0000000000000..01f14467c72ea --- /dev/null +++ b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Contour/index.ts @@ -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, + }); + } +} diff --git a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/preset.ts b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/preset.ts index 2f9a1c6b9e1f3..d6e41c6e9b3da 100644 --- a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/preset.ts +++ b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/preset.ts @@ -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' }), ], }); } diff --git a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/types.ts b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/types.ts index 9177da614d6fd..aadd859d775ca 100644 --- a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/types.ts +++ b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/types.ts @@ -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; +} diff --git a/superset-frontend/plugins/legacy-preset-chart-deckgl/types/external.d.ts b/superset-frontend/plugins/legacy-preset-chart-deckgl/types/external.d.ts index 9543307b69d60..7093762a5c76b 100644 --- a/superset-frontend/plugins/legacy-preset-chart-deckgl/types/external.d.ts +++ b/superset-frontend/plugins/legacy-preset-chart-deckgl/types/external.d.ts @@ -17,4 +17,55 @@ * under the License. */ -declare module '*.png'; +declare module 'deck.gl' { + import { Layer, LayerProps } from '@deck.gl/core'; + + interface HeatmapLayerProps extends LayerProps { + 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 extends LayerProps { + 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 extends Layer< + T, + HeatmapLayerProps + > { + constructor(props: HeatmapLayerProps); + } + + export class ContourLayer extends Layer< + T, + ContourLayerProps + > { + constructor(props: ContourLayerProps); + } +} + +declare module '*.png' { + const value: any; + export default value; +} diff --git a/superset-frontend/src/explore/components/controls/ContourControl/ContourOption.tsx b/superset-frontend/src/explore/components/controls/ContourControl/ContourOption.tsx new file mode 100644 index 0000000000000..8bc2669038aca --- /dev/null +++ b/superset-frontend/src/explore/components/controls/ContourControl/ContourOption.tsx @@ -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 = ( +
+ + {t('Threshold: ')} + {isIsoband + ? `[${lowerThreshold}, ${upperThreshold}]` + : `${lowerThreshold}`} + + + {t('Color: ')} + {formattedColor} + + {!isIsoband && ( + {`${t( + 'Stroke Width:', + )} ${strokeWidth}`} + )} +
+ ); + + return ( + + + + ); +}; + +export default ContourOption; diff --git a/superset-frontend/src/explore/components/controls/ContourControl/ContourPopoverControl.tsx b/superset-frontend/src/explore/components/controls/ContourControl/ContourPopoverControl.tsx new file mode 100644 index 0000000000000..ebcf47222131e --- /dev/null +++ b/superset-frontend/src/explore/components/controls/ContourControl/ContourPopoverControl.tsx @@ -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 ( + <> + + +
+ + + + + + + + + + + + + + + + +
+
+ +
+ + + + + + + + + + + + + + + + +
+
+
+ + + + + + ); +}; + +export default ContourPopoverControl; diff --git a/superset-frontend/src/explore/components/controls/ContourControl/ContourPopoverTrigger.tsx b/superset-frontend/src/explore/components/controls/ContourControl/ContourPopoverTrigger.tsx new file mode 100644 index 0000000000000..b989731fc0511 --- /dev/null +++ b/superset-frontend/src/explore/components/controls/ContourControl/ContourPopoverTrigger.tsx @@ -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 = ( + setVisibility(false)} + /> + ); + + return ( + + {props.children} + + ); +}; + +export default ContourPopoverTrigger; diff --git a/superset-frontend/src/explore/components/controls/ContourControl/index.tsx b/superset-frontend/src/explore/components/controls/ContourControl/index.tsx new file mode 100644 index 0000000000000..09d1ef825d9ed --- /dev/null +++ b/superset-frontend/src/explore/components/controls/ContourControl/index.tsx @@ -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( + 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) => ( + + editContour(newContour, index) + } + contour={contour} + index={index} + onClose={removeContour} + onShift={onShiftContour} + /> + )); + + const ghostButtonText = t('Click to add a contour'); + + return ( + <> + {}} + canDrop={() => true} + valuesRenderer={valuesRenderer} + accept={[]} + ghostButtonText={ghostButtonText} + onClickGhostButton={handleClickGhostButton} + {...props} + /> + + + + + ); +}; + +export default ContourControl; diff --git a/superset-frontend/src/explore/components/controls/ContourControl/types.ts b/superset-frontend/src/explore/components/controls/ContourControl/types.ts new file mode 100644 index 0000000000000..aab03df3800e9 --- /dev/null +++ b/superset-frontend/src/explore/components/controls/ContourControl/types.ts @@ -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 { + 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; +} diff --git a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/OptionWrapper.tsx b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/OptionWrapper.tsx index e2272288326a9..6cf5c188cbbac 100644 --- a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/OptionWrapper.tsx +++ b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/OptionWrapper.tsx @@ -59,6 +59,7 @@ export default function OptionWrapper( isExtra, datasourceWarningMessage, canDelete = true, + tooltipOverlay, ...rest } = props; const ref = useRef(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 {label}; } + if (tooltipOverlay) { + return ( + + {label} + + ); + } return ( {label} diff --git a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/types.ts b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/types.ts index 9f445a607db1e..32724863c8c37 100644 --- a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/types.ts +++ b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/types.ts @@ -32,6 +32,7 @@ export interface OptionProps { isExtra?: boolean; datasourceWarningMessage?: string; canDelete?: boolean; + tooltipOverlay?: ReactNode; } export interface OptionItemInterface { diff --git a/superset-frontend/src/explore/components/controls/TextControl/index.tsx b/superset-frontend/src/explore/components/controls/TextControl/index.tsx index 38877575d9fb9..f15bd95c1b6e1 100644 --- a/superset-frontend/src/explore/components/controls/TextControl/index.tsx +++ b/superset-frontend/src/explore/components/controls/TextControl/index.tsx @@ -30,7 +30,7 @@ export interface TextControlProps { disabled?: boolean; isFloat?: boolean; isInt?: boolean; - onChange?: (value: T, errors: any) => {}; + onChange?: (value: T, errors: any) => void; onFocus?: () => {}; placeholder?: string; value?: T | null; diff --git a/superset-frontend/src/explore/components/controls/index.js b/superset-frontend/src/explore/components/controls/index.js index 725077cc8f99c..cba3c27f5569d 100644 --- a/superset-frontend/src/explore/components/controls/index.js +++ b/superset-frontend/src/explore/components/controls/index.js @@ -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; diff --git a/superset/viz.py b/superset/viz.py index df77cfadead9c..2e697a77becf8 100644 --- a/superset/viz.py +++ b/superset/viz.py @@ -2411,6 +2411,27 @@ def get_data(self, df: pd.DataFrame) -> VizData: 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"""