mirror of
https://github.com/apache/superset.git
synced 2026-06-01 13:49:21 +00:00
feat: Add Deck.gl Contour Layer (#24154)
This commit is contained in:
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user