This is an automated email from the ASF dual-hosted git repository.
kgabryje pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git
The following commit(s) were added to refs/heads/master by this push:
new 09e9cb484b chore: Convert deckgl class components to functional
(#25177)
09e9cb484b is described below
commit 09e9cb484b7bf411f19bbf4f5f0feb24e406a46e
Author: Kamil Gabryjelski <[email protected]>
AuthorDate: Thu Sep 7 13:28:09 2023 +0200
chore: Convert deckgl class components to functional (#25177)
---
.../src/CategoricalDeckGLContainer.tsx | 266 +++++++++------------
.../src/DeckGLContainer.tsx | 131 +++++-----
.../legacy-preset-chart-deckgl/src/Multi/Multi.tsx | 204 +++++++---------
.../legacy-preset-chart-deckgl/src/TooltipRow.tsx | 18 +-
.../legacy-preset-chart-deckgl/src/factory.tsx | 113 ++++-----
.../src/layers/Geojson/Geojson.tsx | 65 ++---
.../src/layers/Polygon/Polygon.tsx | 237 +++++++++---------
.../src/layers/Screengrid/Screengrid.tsx | 118 ++++-----
8 files changed, 518 insertions(+), 634 deletions(-)
diff --git
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/CategoricalDeckGLContainer.tsx
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/CategoricalDeckGLContainer.tsx
index 4348bf1561..fb150445f4 100644
---
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/CategoricalDeckGLContainer.tsx
+++
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/CategoricalDeckGLContainer.tsx
@@ -24,7 +24,7 @@
*/
/* eslint no-underscore-dangle: ["error", { "allow": ["", "__timestamp"] }] */
-import React from 'react';
+import React, { memo, useCallback, useEffect, useRef, useState } from 'react';
import {
CategoricalColorNamespace,
Datasource,
@@ -40,7 +40,7 @@ import sandboxedEval from './utils/sandbox';
// eslint-disable-next-line import/extensions
import fitViewport, { Viewport } from './utils/fitViewport';
import {
- DeckGLContainer,
+ DeckGLContainerHandle,
DeckGLContainerStyledWrapper,
} from './DeckGLContainer';
import { Point } from './types';
@@ -83,82 +83,72 @@ export type CategoricalDeckGLContainerProps = {
setControlValue: (control: string, value: JsonValue) => void;
};
-export type CategoricalDeckGLContainerState = {
- formData?: QueryFormData;
- viewport: Viewport;
- categories: JsonObject;
-};
-
-export default class CategoricalDeckGLContainer extends React.PureComponent<
- CategoricalDeckGLContainerProps,
- CategoricalDeckGLContainerState
-> {
- containerRef = React.createRef<DeckGLContainer>();
-
- /*
- * A Deck.gl container that handles categories.
- *
- * The container will have an interactive legend, populated from the
- * categories present in the data.
- */
- constructor(props: CategoricalDeckGLContainerProps) {
- super(props);
- this.state = this.getStateFromProps(props);
-
- this.getLayers = this.getLayers.bind(this);
- this.toggleCategory = this.toggleCategory.bind(this);
- this.showSingleCategory = this.showSingleCategory.bind(this);
- }
-
- UNSAFE_componentWillReceiveProps(nextProps: CategoricalDeckGLContainerProps)
{
- if (nextProps.payload.form_data !== this.state.formData) {
- this.setState({ ...this.getStateFromProps(nextProps) });
- }
- }
+const CategoricalDeckGLContainer = (props: CategoricalDeckGLContainerProps) =>
{
+ const containerRef = useRef<DeckGLContainerHandle>(null);
- // eslint-disable-next-line class-methods-use-this
- getStateFromProps(
- props: CategoricalDeckGLContainerProps,
- state?: CategoricalDeckGLContainerState,
- ) {
- const features = props.payload.data.features || [];
- const categories = getCategories(props.formData, features);
-
- // the state is computed only from the payload; if it hasn't changed, do
- // not recompute state since this would reset selections and/or the play
- // slider position due to changes in form controls
- if (state && props.payload.form_data === state.formData) {
- return { ...state, categories };
- }
-
- const { width, height, formData } = props;
- let { viewport } = props;
- if (formData.autozoom) {
+ const getAdjustedViewport = useCallback(() => {
+ let viewport = { ...props.viewport };
+ if (props.formData.autozoom) {
viewport = fitViewport(viewport, {
- width,
- height,
- points: props.getPoints(features),
+ width: props.width,
+ height: props.height,
+ points: props.getPoints(props.payload.data.features || []),
});
}
if (viewport.zoom < 0) {
viewport.zoom = 0;
}
+ return viewport;
+ }, [props]);
+
+ const [categories, setCategories] = useState<JsonObject>(
+ getCategories(props.formData, props.payload.data.features || []),
+ );
+ const [stateFormData, setStateFormData] = useState<JsonObject>(
+ props.payload.form_data,
+ );
+ const [viewport, setViewport] = useState(getAdjustedViewport());
+
+ useEffect(() => {
+ if (props.payload.form_data !== stateFormData) {
+ const features = props.payload.data.features || [];
+ const categories = getCategories(props.formData, features);
+
+ setViewport(getAdjustedViewport());
+ setStateFormData(props.payload.form_data);
+ setCategories(categories);
+ }
+ }, [getAdjustedViewport, props, stateFormData]);
- return {
- viewport,
- selected: [],
- lastClick: 0,
- formData: props.payload.form_data,
- categories,
- };
- }
+ const setTooltip = useCallback((tooltip: TooltipProps['tooltip']) => {
+ const { current } = containerRef;
+ if (current) {
+ current.setTooltip(tooltip);
+ }
+ }, []);
+
+ const addColor = useCallback((data: JsonObject[], fd: QueryFormData) => {
+ const c = fd.color_picker || { r: 0, g: 0, b: 0, a: 1 };
+ const colorFn = getScale(fd.color_scheme);
+
+ return data.map(d => {
+ let color;
+ if (fd.dimension) {
+ color = hexToRGB(colorFn(d.cat_color, fd.sliceId), c.a * 255);
+
+ return { ...d, color };
+ }
+
+ return d;
+ });
+ }, []);
- getLayers() {
- const { getLayer, payload, formData: fd, onAddFilter } = this.props;
+ const getLayers = useCallback(() => {
+ const { getLayer, payload, formData: fd, onAddFilter } = props;
let features = payload.data.features ? [...payload.data.features] : [];
// Add colors from categories or fixed color
- features = this.addColor(features, fd);
+ features = addColor(features, fd);
// Apply user defined data mutator if defined
if (fd.js_data_mutator) {
@@ -167,9 +157,8 @@ export default class CategoricalDeckGLContainer extends
React.PureComponent<
}
// Show only categories selected in the legend
- const cats = this.state.categories;
if (fd.dimension) {
- features = features.filter(d => cats[d.cat_color]?.enabled);
+ features = features.filter(d => categories[d.cat_color]?.enabled);
}
const filteredPayload = {
@@ -182,88 +171,69 @@ export default class CategoricalDeckGLContainer extends
React.PureComponent<
fd,
filteredPayload,
onAddFilter,
- this.setTooltip,
- this.props.datasource,
+ setTooltip,
+ props.datasource,
) as Layer,
];
- }
-
- // eslint-disable-next-line class-methods-use-this
- addColor(data: JsonObject[], fd: QueryFormData) {
- const c = fd.color_picker || { r: 0, g: 0, b: 0, a: 1 };
- const colorFn = getScale(fd.color_scheme);
-
- return data.map(d => {
- let color;
- if (fd.dimension) {
- color = hexToRGB(colorFn(d.cat_color, fd.sliceId), c.a * 255);
-
- return { ...d, color };
+ }, [addColor, categories, props, setTooltip]);
+
+ const toggleCategory = useCallback(
+ (category: string) => {
+ const categoryState = categories[category];
+ const categoriesExtended = {
+ ...categories,
+ [category]: {
+ ...categoryState,
+ enabled: !categoryState.enabled,
+ },
+ };
+
+ // if all categories are disabled, enable all -- similar to nvd3
+ if (Object.values(categoriesExtended).every(v => !v.enabled)) {
+ /* eslint-disable no-param-reassign */
+ Object.values(categoriesExtended).forEach(v => {
+ v.enabled = true;
+ });
}
-
- return d;
- });
- }
-
- toggleCategory(category: string) {
- const categoryState = this.state.categories[category];
- const categories = {
- ...this.state.categories,
- [category]: {
- ...categoryState,
- enabled: !categoryState.enabled,
- },
- };
-
- // if all categories are disabled, enable all -- similar to nvd3
- if (Object.values(categories).every(v => !v.enabled)) {
- /* eslint-disable no-param-reassign */
- Object.values(categories).forEach(v => {
- v.enabled = true;
+ setCategories(categoriesExtended);
+ },
+ [categories],
+ );
+
+ const showSingleCategory = useCallback(
+ (category: string) => {
+ const modifiedCategories = { ...categories };
+ Object.values(modifiedCategories).forEach(v => {
+ v.enabled = false;
});
- }
- this.setState({ categories });
- }
-
- showSingleCategory(category: string) {
- const categories = { ...this.state.categories };
- /* eslint-disable no-param-reassign */
- Object.values(categories).forEach(v => {
- v.enabled = false;
- });
- categories[category].enabled = true;
- this.setState({ categories });
- }
-
- setTooltip = (tooltip: TooltipProps['tooltip']) => {
- const { current } = this.containerRef;
- if (current) {
- current.setTooltip(tooltip);
- }
- };
+ modifiedCategories[category].enabled = true;
+ setCategories(modifiedCategories);
+ },
+ [categories],
+ );
+
+ return (
+ <div style={{ position: 'relative' }}>
+ <DeckGLContainerStyledWrapper
+ ref={containerRef}
+ viewport={viewport}
+ layers={getLayers()}
+ setControlValue={props.setControlValue}
+ mapStyle={props.formData.mapbox_style}
+ mapboxApiAccessToken={props.mapboxApiKey}
+ width={props.width}
+ height={props.height}
+ />
+ <Legend
+ forceCategorical
+ categories={categories}
+ format={props.formData.legend_format}
+ position={props.formData.legend_position}
+ showSingleCategory={showSingleCategory}
+ toggleCategory={toggleCategory}
+ />
+ </div>
+ );
+};
- render() {
- return (
- <div style={{ position: 'relative' }}>
- <DeckGLContainerStyledWrapper
- ref={this.containerRef}
- viewport={this.state.viewport}
- layers={this.getLayers()}
- setControlValue={this.props.setControlValue}
- mapStyle={this.props.formData.mapbox_style}
- mapboxApiAccessToken={this.props.mapboxApiKey}
- width={this.props.width}
- height={this.props.height}
- />
- <Legend
- forceCategorical
- categories={this.state.categories}
- format={this.props.formData.legend_format}
- position={this.props.formData.legend_position}
- showSingleCategory={this.showSingleCategory}
- toggleCategory={this.toggleCategory}
- />
- </div>
- );
- }
-}
+export default memo(CategoricalDeckGLContainer);
diff --git
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/DeckGLContainer.tsx
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/DeckGLContainer.tsx
index 29672febfb..7b8f61e18b 100644
---
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/DeckGLContainer.tsx
+++
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/DeckGLContainer.tsx
@@ -20,11 +20,19 @@
* specific language governing permissions and limitations
* under the License.
*/
-import React, { ReactNode } from 'react';
+import React, {
+ forwardRef,
+ memo,
+ ReactNode,
+ useCallback,
+ useEffect,
+ useImperativeHandle,
+ useState,
+} from 'react';
import { isEqual } from 'lodash';
import { StaticMap } from 'react-map-gl';
import DeckGL, { Layer } from 'deck.gl/typed';
-import { JsonObject, JsonValue, styled } from '@superset-ui/core';
+import { JsonObject, JsonValue, styled, usePrevious } from '@superset-ui/core';
import Tooltip, { TooltipProps } from './components/Tooltip';
import 'mapbox-gl/dist/mapbox-gl.css';
import { Viewport } from './utils/fitViewport';
@@ -43,76 +51,57 @@ export type DeckGLContainerProps = {
onViewportChange?: (viewport: Viewport) => void;
};
-export type DeckGLContainerState = {
- lastUpdate: number | null;
- viewState: Viewport;
- tooltip: TooltipProps['tooltip'];
- timer: ReturnType<typeof setInterval>;
-};
-
-export class DeckGLContainer extends React.Component<
- DeckGLContainerProps,
- DeckGLContainerState
-> {
- constructor(props: DeckGLContainerProps) {
- super(props);
- this.tick = this.tick.bind(this);
- this.onViewStateChange = this.onViewStateChange.bind(this);
- // This has to be placed after this.tick is bound to this
- this.state = {
- timer: setInterval(this.tick, TICK),
- tooltip: null,
- viewState: props.viewport,
- lastUpdate: null,
- };
- }
+export const DeckGLContainer = memo(
+ forwardRef((props: DeckGLContainerProps, ref) => {
+ const [tooltip, setTooltip] = useState<TooltipProps['tooltip']>(null);
+ const [lastUpdate, setLastUpdate] = useState<number | null>(null);
+ const [viewState, setViewState] = useState(props.viewport);
+ const prevViewport = usePrevious(props.viewport);
- UNSAFE_componentWillReceiveProps(nextProps: DeckGLContainerProps) {
- if (!isEqual(nextProps.viewport, this.props.viewport)) {
- this.setState({ viewState: nextProps.viewport });
- }
- }
+ useImperativeHandle(ref, () => ({ setTooltip }), []);
- componentWillUnmount() {
- clearInterval(this.state.timer);
- }
+ const tick = useCallback(() => {
+ // Rate limiting updating viewport controls as it triggers lots of
renders
+ if (lastUpdate && Date.now() - lastUpdate > TICK) {
+ const setCV = props.setControlValue;
+ if (setCV) {
+ setCV('viewport', viewState);
+ }
+ setLastUpdate(null);
+ }
+ }, [lastUpdate, props.setControlValue, viewState]);
- onViewStateChange({ viewState }: { viewState: JsonObject }) {
- this.setState({ viewState: viewState as Viewport, lastUpdate: Date.now()
});
- }
+ useEffect(() => {
+ const timer = setInterval(tick, TICK);
+ return clearInterval(timer);
+ }, [tick]);
- tick() {
- // Rate limiting updating viewport controls as it triggers lotsa renders
- const { lastUpdate } = this.state;
- if (lastUpdate && Date.now() - lastUpdate > TICK) {
- const setCV = this.props.setControlValue;
- if (setCV) {
- setCV('viewport', this.state.viewState);
+ useEffect(() => {
+ if (!isEqual(props.viewport, prevViewport)) {
+ setViewState(props.viewport);
}
- this.setState({ lastUpdate: null });
- }
- }
+ }, [prevViewport, props.viewport]);
- layers() {
- // Support for layer factory
- if (this.props.layers.some(l => typeof l === 'function')) {
- return this.props.layers.map(l =>
- typeof l === 'function' ? l() : l,
- ) as Layer[];
- }
-
- return this.props.layers as Layer[];
- }
+ const onViewStateChange = useCallback(
+ ({ viewState }: { viewState: JsonObject }) => {
+ setViewState(viewState as Viewport);
+ setLastUpdate(Date.now());
+ },
+ [],
+ );
- setTooltip = (tooltip: TooltipProps['tooltip']) => {
- this.setState({ tooltip });
- };
+ const layers = useCallback(() => {
+ // Support for layer factory
+ if (props.layers.some(l => typeof l === 'function')) {
+ return props.layers.map(l =>
+ typeof l === 'function' ? l() : l,
+ ) as Layer[];
+ }
- render() {
- const { children = null, height, width } = this.props;
- const { viewState, tooltip } = this.state;
+ return props.layers as Layer[];
+ }, [props.layers]);
- const layers = this.layers();
+ const { children = null, height, width } = props;
return (
<>
@@ -121,15 +110,15 @@ export class DeckGLContainer extends React.Component<
controller
width={width}
height={height}
- layers={layers}
+ layers={layers()}
viewState={viewState}
glOptions={{ preserveDrawingBuffer: true }}
- onViewStateChange={this.onViewStateChange}
+ onViewStateChange={onViewStateChange}
>
<StaticMap
preserveDrawingBuffer
- mapStyle={this.props.mapStyle || 'light'}
- mapboxApiAccessToken={this.props.mapboxApiAccessToken}
+ mapStyle={props.mapStyle || 'light'}
+ mapboxApiAccessToken={props.mapboxApiAccessToken}
/>
</DeckGL>
{children}
@@ -137,8 +126,8 @@ export class DeckGLContainer extends React.Component<
<Tooltip tooltip={tooltip} />
</>
);
- }
-}
+ }),
+);
export const DeckGLContainerStyledWrapper = styled(DeckGLContainer)`
.deckgl-tooltip > div {
@@ -146,3 +135,7 @@ export const DeckGLContainerStyledWrapper =
styled(DeckGLContainer)`
text-overflow: ellipsis;
}
`;
+
+export type DeckGLContainerHandle = typeof DeckGLContainer & {
+ setTooltip: (tooltip: ReactNode) => void;
+};
diff --git
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/Multi/Multi.tsx
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/Multi/Multi.tsx
index 5cfa02f704..540b094219 100644
--- a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/Multi/Multi.tsx
+++ b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/Multi/Multi.tsx
@@ -19,7 +19,7 @@
* specific language governing permissions and limitations
* under the License.
*/
-import React from 'react';
+import React, { memo, useCallback, useEffect, useRef, useState } from 'react';
import { isEqual } from 'lodash';
import {
Datasource,
@@ -28,11 +28,12 @@ import {
JsonValue,
QueryFormData,
SupersetClient,
+ usePrevious,
} from '@superset-ui/core';
import { Layer } from 'deck.gl/typed';
import {
- DeckGLContainer,
+ DeckGLContainerHandle,
DeckGLContainerStyledWrapper,
} from '../DeckGLContainer';
import { getExploreLongUrl } from '../utils/explore';
@@ -52,120 +53,97 @@ export type DeckMultiProps = {
onSelect: () => void;
};
-export type DeckMultiState = {
- subSlicesLayers: Record<number, Layer>;
- viewport?: Viewport;
-};
-
-class DeckMulti extends React.PureComponent<DeckMultiProps, DeckMultiState> {
- containerRef = React.createRef<DeckGLContainer>();
-
- constructor(props: DeckMultiProps) {
- super(props);
- this.state = { subSlicesLayers: {} };
- this.onViewportChange = this.onViewportChange.bind(this);
- }
-
- componentDidMount() {
- const { formData, payload } = this.props;
- this.loadLayers(formData, payload);
- }
-
- UNSAFE_componentWillReceiveProps(nextProps: DeckMultiProps) {
- const { formData, payload } = nextProps;
- const hasChanges = !isEqual(
- this.props.formData.deck_slices,
- nextProps.formData.deck_slices,
- );
- if (hasChanges) {
- this.loadLayers(formData, payload);
- }
- }
-
- onViewportChange(viewport: Viewport) {
- this.setState({ viewport });
- }
-
- loadLayers(
- formData: QueryFormData,
- payload: JsonObject,
- viewport?: Viewport,
- ) {
- this.setState({ subSlicesLayers: {}, viewport });
- payload.data.slices.forEach(
- (subslice: { slice_id: number } & JsonObject) => {
- // Filters applied to multi_deck are passed down to underlying charts
- // note that dashboard contextual information (filter_immune_slices
and such) aren't
- // taken into consideration here
- const filters = [
- ...(subslice.form_data.filters || []),
- ...(formData.filters || []),
- ...(formData.extra_filters || []),
- ];
- const subsliceCopy = {
- ...subslice,
- form_data: {
- ...subslice.form_data,
- filters,
- },
- };
-
- const url = getExploreLongUrl(subsliceCopy.form_data, 'json');
+const DeckMulti = (props: DeckMultiProps) => {
+ const containerRef = useRef<DeckGLContainerHandle>();
- if (url) {
- SupersetClient.get({
- endpoint: url,
- })
- .then(({ json }) => {
- const layer = layerGenerators[subsliceCopy.form_data.viz_type](
- subsliceCopy.form_data,
- json,
- this.props.onAddFilter,
- this.setTooltip,
- this.props.datasource,
- [],
- this.props.onSelect,
- );
- this.setState({
- subSlicesLayers: {
- ...this.state.subSlicesLayers,
- [subsliceCopy.slice_id]: layer,
- },
- });
- })
- .catch(() => {});
- }
- },
- );
- }
+ const [viewport, setViewport] = useState<Viewport>();
+ const [subSlicesLayers, setSubSlicesLayers] = useState<Record<number,
Layer>>(
+ {},
+ );
- setTooltip = (tooltip: TooltipProps['tooltip']) => {
- const { current } = this.containerRef;
+ const setTooltip = useCallback((tooltip: TooltipProps['tooltip']) => {
+ const { current } = containerRef;
if (current) {
current.setTooltip(tooltip);
}
- };
-
- render() {
- const { payload, formData, setControlValue, height, width } = this.props;
- const { subSlicesLayers } = this.state;
-
- const layers = Object.values(subSlicesLayers);
-
- return (
- <DeckGLContainerStyledWrapper
- ref={this.containerRef}
- mapboxApiAccessToken={payload.data.mapboxApiKey}
- viewport={this.state.viewport || this.props.viewport}
- layers={layers}
- mapStyle={formData.mapbox_style}
- setControlValue={setControlValue}
- onViewportChange={this.onViewportChange}
- height={height}
- width={width}
- />
- );
- }
-}
+ }, []);
+
+ const loadLayers = useCallback(
+ (formData: QueryFormData, payload: JsonObject, viewport?: Viewport) => {
+ setViewport(viewport);
+ setSubSlicesLayers({});
+ payload.data.slices.forEach(
+ (subslice: { slice_id: number } & JsonObject) => {
+ // Filters applied to multi_deck are passed down to underlying charts
+ // note that dashboard contextual information (filter_immune_slices
and such) aren't
+ // taken into consideration here
+ const filters = [
+ ...(subslice.form_data.filters || []),
+ ...(formData.filters || []),
+ ...(formData.extra_filters || []),
+ ];
+ const subsliceCopy = {
+ ...subslice,
+ form_data: {
+ ...subslice.form_data,
+ filters,
+ },
+ };
+
+ const url = getExploreLongUrl(subsliceCopy.form_data, 'json');
+
+ if (url) {
+ SupersetClient.get({
+ endpoint: url,
+ })
+ .then(({ json }) => {
+ const layer = layerGenerators[subsliceCopy.form_data.viz_type](
+ subsliceCopy.form_data,
+ json,
+ props.onAddFilter,
+ setTooltip,
+ props.datasource,
+ [],
+ props.onSelect,
+ );
+ setSubSlicesLayers(subSlicesLayers => ({
+ ...subSlicesLayers,
+ [subsliceCopy.slice_id]: layer,
+ }));
+ })
+ .catch(() => {});
+ }
+ },
+ );
+ },
+ [props.datasource, props.onAddFilter, props.onSelect, setTooltip],
+ );
+
+ const prevDeckSlices = usePrevious(props.formData.deck_slices);
+ useEffect(() => {
+ const { formData, payload } = props;
+ const hasChanges = !isEqual(prevDeckSlices, formData.deck_slices);
+ if (hasChanges) {
+ loadLayers(formData, payload);
+ }
+ }, [loadLayers, prevDeckSlices, props]);
+
+ const { payload, formData, setControlValue, height, width } = props;
+ const layers = Object.values(subSlicesLayers);
+
+ return (
+ <DeckGLContainerStyledWrapper
+ ref={containerRef}
+ mapboxApiAccessToken={payload.data.mapboxApiKey}
+ viewport={viewport || props.viewport}
+ layers={layers}
+ mapStyle={formData.mapbox_style}
+ setControlValue={setControlValue}
+ onViewportChange={setViewport}
+ height={height}
+ width={width}
+ />
+ );
+};
-export default DeckMulti;
+export default memo(DeckMulti);
diff --git
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/TooltipRow.tsx
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/TooltipRow.tsx
index 9d72f719fe..3e69258556 100644
--- a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/TooltipRow.tsx
+++ b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/TooltipRow.tsx
@@ -23,15 +23,11 @@ type TooltipRowProps = {
value: string;
};
-export default class TooltipRow extends React.PureComponent<TooltipRowProps> {
- render() {
- const { label, value } = this.props;
+const TooltipRow = ({ label, value }: TooltipRowProps) => (
+ <div>
+ {label}
+ <strong>{value}</strong>
+ </div>
+);
- return (
- <div>
- {label}
- <strong>{value}</strong>
- </div>
- );
- }
-}
+export default TooltipRow;
diff --git
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/factory.tsx
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/factory.tsx
index 4ddde91247..fb1255a2fd 100644
--- a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/factory.tsx
+++ b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/factory.tsx
@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
-import React from 'react';
+import React, { memo, useCallback, useEffect, useRef, useState } from 'react';
import { isEqual } from 'lodash';
import { Layer } from 'deck.gl/typed';
import {
@@ -24,11 +24,12 @@ import {
QueryFormData,
JsonObject,
HandlerFunction,
+ usePrevious,
} from '@superset-ui/core';
import {
DeckGLContainerStyledWrapper,
- DeckGLContainer,
+ DeckGLContainerHandle,
} from './DeckGLContainer';
import CategoricalDeckGLContainer from './CategoricalDeckGLContainer';
import fitViewport, { Viewport } from './utils/fitViewport';
@@ -57,91 +58,73 @@ export interface getLayerType<T> {
interface getPointsType {
(data: JsonObject[]): Point[];
}
-type deckGLComponentState = {
- viewport: Viewport;
- layer: Layer;
-};
export function createDeckGLComponent(
getLayer: getLayerType<unknown>,
getPoints: getPointsType,
-): React.ComponentClass<deckGLComponentProps> {
+) {
// Higher order component
- class Component extends React.PureComponent<
- deckGLComponentProps,
- deckGLComponentState
- > {
- containerRef: React.RefObject<DeckGLContainer> = React.createRef();
-
- constructor(props: deckGLComponentProps) {
- super(props);
-
+ return memo((props: deckGLComponentProps) => {
+ const containerRef = useRef<DeckGLContainerHandle>();
+ const prevFormData = usePrevious(props.formData);
+ const prevPayload = usePrevious(props.payload);
+ const getAdjustedViewport = () => {
const { width, height, formData } = props;
- let { viewport } = props;
if (formData.autozoom) {
- viewport = fitViewport(viewport, {
+ return fitViewport(props.viewport, {
width,
height,
points: getPoints(props.payload.data.features),
}) as Viewport;
}
+ return props.viewport;
+ };
- this.state = {
- viewport,
- layer: this.computeLayer(props),
- };
- this.onViewportChange = this.onViewportChange.bind(this);
- }
+ const [viewport, setViewport] = useState(getAdjustedViewport());
- UNSAFE_componentWillReceiveProps(nextProps: deckGLComponentProps) {
- // Only recompute the layer if anything BUT the viewport has changed
- const nextFdNoVP = { ...nextProps.formData, viewport: null };
- const currFdNoVP = { ...this.props.formData, viewport: null };
- if (
- !isEqual(nextFdNoVP, currFdNoVP) ||
- nextProps.payload !== this.props.payload
- ) {
- this.setState({ layer: this.computeLayer(nextProps) });
+ const setTooltip = useCallback((tooltip: TooltipProps['tooltip']) => {
+ const { current } = containerRef;
+ if (current) {
+ current?.setTooltip(tooltip);
}
- }
+ }, []);
- onViewportChange(viewport: Viewport) {
- this.setState({ viewport });
- }
+ const computeLayer = useCallback(
+ (props: deckGLComponentProps) => {
+ const { formData, payload, onAddFilter } = props;
- computeLayer(props: deckGLComponentProps) {
- const { formData, payload, onAddFilter } = props;
+ return getLayer(formData, payload, onAddFilter, setTooltip) as Layer;
+ },
+ [setTooltip],
+ );
- return getLayer(formData, payload, onAddFilter, this.setTooltip) as
Layer;
- }
+ const [layer, setLayer] = useState(computeLayer(props));
- setTooltip = (tooltip: TooltipProps['tooltip']) => {
- const { current } = this.containerRef;
- if (current) {
- current?.setTooltip(tooltip);
+ useEffect(() => {
+ // Only recompute the layer if anything BUT the viewport has changed
+ const prevFdNoVP = { ...prevFormData, viewport: null };
+ const currFdNoVP = { ...props.formData, viewport: null };
+ if (!isEqual(prevFdNoVP, currFdNoVP) || prevPayload !== props.payload) {
+ setLayer(computeLayer(props));
}
- };
+ }, [computeLayer, prevFormData, prevPayload, props]);
- render() {
- const { formData, payload, setControlValue, height, width } = this.props;
- const { layer, viewport } = this.state;
+ const { formData, payload, setControlValue, height, width } = props;
- return (
- <DeckGLContainerStyledWrapper
- ref={this.containerRef}
- mapboxApiAccessToken={payload.data.mapboxApiKey}
- viewport={viewport}
- layers={[layer]}
- mapStyle={formData.mapbox_style}
- setControlValue={setControlValue}
- width={width}
- height={height}
- onViewportChange={this.onViewportChange}
- />
- );
- }
- }
- return Component;
+ return (
+ <DeckGLContainerStyledWrapper
+ ref={containerRef}
+ mapboxApiAccessToken={payload.data.mapboxApiKey}
+ viewport={viewport}
+ layers={[layer]}
+ mapStyle={formData.mapbox_style}
+ setControlValue={setControlValue}
+ width={width}
+ height={height}
+ onViewportChange={setViewport}
+ />
+ );
+ });
}
export function createCategoricalDeckGLComponent(
diff --git
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Geojson/Geojson.tsx
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Geojson/Geojson.tsx
index 4aa827e45b..c8c9d4863c 100644
---
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Geojson/Geojson.tsx
+++
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Geojson/Geojson.tsx
@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
-import React from 'react';
+import React, { memo, useCallback, useMemo, useRef } from 'react';
import { GeoJsonLayer } from 'deck.gl/typed';
import geojsonExtent from '@mapbox/geojson-extent';
import {
@@ -27,7 +27,7 @@ import {
} from '@superset-ui/core';
import {
- DeckGLContainer,
+ DeckGLContainerHandle,
DeckGLContainerStyledWrapper,
} from '../../DeckGLContainer';
import { hexToRGB } from '../../utils/colors';
@@ -164,21 +164,19 @@ export type DeckGLGeoJsonProps = {
width: number;
};
-class DeckGLGeoJson extends React.Component<DeckGLGeoJsonProps> {
- containerRef = React.createRef<DeckGLContainer>();
-
- setTooltip = (tooltip: TooltipProps['tooltip']) => {
- const { current } = this.containerRef;
+const DeckGLGeoJson = (props: DeckGLGeoJsonProps) => {
+ const containerRef = useRef<DeckGLContainerHandle>();
+ const setTooltip = useCallback((tooltip: TooltipProps['tooltip']) => {
+ const { current } = containerRef;
if (current) {
current.setTooltip(tooltip);
}
- };
+ }, []);
- render() {
- const { formData, payload, setControlValue, onAddFilter, height, width } =
- this.props;
+ const { formData, payload, setControlValue, onAddFilter, height, width } =
+ props;
- let { viewport } = this.props;
+ const viewport: Viewport = useMemo(() => {
if (formData.autozoom) {
const points =
payload?.data?.features?.reduce?.(
@@ -194,29 +192,36 @@ class DeckGLGeoJson extends
React.Component<DeckGLGeoJsonProps> {
) || [];
if (points.length) {
- viewport = fitViewport(viewport, {
+ return fitViewport(props.viewport, {
width,
height,
points,
});
}
}
+ return props.viewport;
+ }, [
+ formData.autozoom,
+ height,
+ payload?.data?.features,
+ props.viewport,
+ width,
+ ]);
- const layer = getLayer(formData, payload, onAddFilter, this.setTooltip);
-
- return (
- <DeckGLContainerStyledWrapper
- ref={this.containerRef}
- mapboxApiAccessToken={payload.data.mapboxApiKey}
- viewport={viewport}
- layers={[layer]}
- mapStyle={formData.mapbox_style}
- setControlValue={setControlValue}
- height={height}
- width={width}
- />
- );
- }
-}
+ const layer = getLayer(formData, payload, onAddFilter, setTooltip);
+
+ return (
+ <DeckGLContainerStyledWrapper
+ ref={containerRef}
+ mapboxApiAccessToken={payload.data.mapboxApiKey}
+ viewport={viewport}
+ layers={[layer]}
+ mapStyle={formData.mapbox_style}
+ setControlValue={setControlValue}
+ height={height}
+ width={width}
+ />
+ );
+};
-export default DeckGLGeoJson;
+export default memo(DeckGLGeoJson);
diff --git
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Polygon/Polygon.tsx
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Polygon/Polygon.tsx
index 627125c398..460c4a3b51 100644
---
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Polygon/Polygon.tsx
+++
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Polygon/Polygon.tsx
@@ -21,7 +21,7 @@
*/
/* eslint no-underscore-dangle: ["error", { "allow": ["", "__timestamp"] }] */
-import React from 'react';
+import React, { memo, useCallback, useEffect, useRef, useState } from 'react';
import {
HandlerFunction,
JsonObject,
@@ -41,7 +41,7 @@ import sandboxedEval from '../../utils/sandbox';
import getPointsFromPolygon from '../../utils/getPointsFromPolygon';
import fitViewport, { Viewport } from '../../utils/fitViewport';
import {
- DeckGLContainer,
+ DeckGLContainerHandle,
DeckGLContainerStyledWrapper,
} from '../../DeckGLContainer';
import { TooltipProps } from '../../components/Tooltip';
@@ -173,145 +173,134 @@ export type DeckGLPolygonProps = {
height: number;
};
-export type DeckGLPolygonState = {
- lastClick: number;
- viewport: Viewport;
- formData: PolygonFormData;
- selected: JsonObject[];
-};
-
-class DeckGLPolygon extends React.PureComponent<
- DeckGLPolygonProps,
- DeckGLPolygonState
-> {
- containerRef = React.createRef<DeckGLContainer>();
-
- constructor(props: DeckGLPolygonProps) {
- super(props);
-
- this.state = DeckGLPolygon.getDerivedStateFromProps(
- props,
- ) as DeckGLPolygonState;
-
- this.getLayers = this.getLayers.bind(this);
- this.onSelect = this.onSelect.bind(this);
- }
-
- static getDerivedStateFromProps(
- props: DeckGLPolygonProps,
- state?: DeckGLPolygonState,
- ) {
- const { width, height, formData, payload } = props;
-
- // the state is computed only from the payload; if it hasn't changed, do
- // not recompute state since this would reset selections and/or the play
- // slider position due to changes in form controls
- if (state && payload.form_data === state.formData) {
- return null;
- }
-
- const features = payload.data.features || [];
+const DeckGLPolygon = (props: DeckGLPolygonProps) => {
+ const containerRef = useRef<DeckGLContainerHandle>();
- let { viewport } = props;
- if (formData.autozoom) {
+ const getAdjustedViewport = useCallback(() => {
+ let viewport = { ...props.viewport };
+ if (props.formData.autozoom) {
+ const features = props.payload.data.features || [];
viewport = fitViewport(viewport, {
- width,
- height,
+ width: props.width,
+ height: props.height,
points: features.flatMap(getPointsFromPolygon),
});
}
+ if (viewport.zoom < 0) {
+ viewport.zoom = 0;
+ }
+ return viewport;
+ }, [props]);
+
+ const [lastClick, setLastClick] = useState(0);
+ const [viewport, setViewport] = useState(getAdjustedViewport());
+ const [stateFormData, setStateFormData] = useState(props.payload.form_data);
+ const [selected, setSelected] = useState<JsonObject[]>([]);
+
+ useEffect(() => {
+ const { payload } = props;
+
+ if (payload.form_data !== stateFormData) {
+ setViewport(getAdjustedViewport());
+ setSelected([]);
+ setLastClick(0);
+ setStateFormData(payload.form_data);
+ }
+ }, [getAdjustedViewport, props, stateFormData, viewport]);
- return {
- viewport,
- selected: [],
- lastClick: 0,
- formData: payload.form_data,
- };
- }
-
- onSelect(polygon: JsonObject) {
- const { formData, onAddFilter } = this.props;
-
- const now = new Date().getDate();
- const doubleClick = now - this.state.lastClick <= DOUBLE_CLICK_THRESHOLD;
-
- // toggle selected polygons
- const selected = [...this.state.selected];
- if (doubleClick) {
- selected.splice(0, selected.length, polygon);
- } else if (formData.toggle_polygons) {
- const i = selected.indexOf(polygon);
- if (i === -1) {
- selected.push(polygon);
+ const setTooltip = useCallback((tooltip: TooltipProps['tooltip']) => {
+ const { current } = containerRef;
+ if (current) {
+ current.setTooltip(tooltip);
+ }
+ }, []);
+
+ const onSelect = useCallback(
+ (polygon: JsonObject) => {
+ const { formData, onAddFilter } = props;
+
+ const now = new Date().getDate();
+ const doubleClick = now - lastClick <= DOUBLE_CLICK_THRESHOLD;
+
+ // toggle selected polygons
+ const selectedCopy = [...selected];
+ if (doubleClick) {
+ selectedCopy.splice(0, selectedCopy.length, polygon);
+ } else if (formData.toggle_polygons) {
+ const i = selectedCopy.indexOf(polygon);
+ if (i === -1) {
+ selectedCopy.push(polygon);
+ } else {
+ selectedCopy.splice(i, 1);
+ }
} else {
- selected.splice(i, 1);
+ selectedCopy.splice(0, 1, polygon);
}
- } else {
- selected.splice(0, 1, polygon);
- }
- this.setState({ selected, lastClick: now });
- if (formData.table_filter) {
- onAddFilter(formData.line_column, selected, false, true);
- }
- }
+ setSelected(selectedCopy);
+ setLastClick(now);
+ if (formData.table_filter) {
+ onAddFilter(formData.line_column, selected, false, true);
+ }
+ },
+ [lastClick, props, selected],
+ );
- getLayers() {
- if (this.props.payload.data.features === undefined) {
+ const getLayers = useCallback(() => {
+ if (props.payload.data.features === undefined) {
return [];
}
const layer = getLayer(
- this.props.formData,
- this.props.payload,
- this.props.onAddFilter,
- this.setTooltip,
- this.state.selected,
- this.onSelect,
+ props.formData,
+ props.payload,
+ props.onAddFilter,
+ setTooltip,
+ selected,
+ onSelect,
);
return [layer];
- }
-
- setTooltip = (tooltip: TooltipProps['tooltip']) => {
- const { current } = this.containerRef;
- if (current) {
- current.setTooltip(tooltip);
- }
- };
-
- render() {
- const { payload, formData, setControlValue } = this.props;
-
- const fd = formData;
- const metricLabel = fd.metric ? fd.metric.label || fd.metric : null;
- const accessor = (d: JsonObject) => d[metricLabel];
-
- const buckets = getBuckets(formData, payload.data.features, accessor);
+ }, [
+ onSelect,
+ props.formData,
+ props.onAddFilter,
+ props.payload,
+ selected,
+ setTooltip,
+ ]);
+
+ const { payload, formData, setControlValue } = props;
+
+ const metricLabel = formData.metric
+ ? formData.metric.label || formData.metric
+ : null;
+ const accessor = (d: JsonObject) => d[metricLabel];
- return (
- <div style={{ position: 'relative' }}>
- <DeckGLContainerStyledWrapper
- ref={this.containerRef}
- viewport={this.state.viewport}
- layers={this.getLayers()}
- setControlValue={setControlValue}
- mapStyle={formData.mapbox_style}
- mapboxApiAccessToken={payload.data.mapboxApiKey}
- width={this.props.width}
- height={this.props.height}
+ const buckets = getBuckets(formData, payload.data.features, accessor);
+
+ return (
+ <div style={{ position: 'relative' }}>
+ <DeckGLContainerStyledWrapper
+ ref={containerRef}
+ viewport={viewport}
+ layers={getLayers()}
+ setControlValue={setControlValue}
+ mapStyle={formData.mapbox_style}
+ mapboxApiAccessToken={payload.data.mapboxApiKey}
+ width={props.width}
+ height={props.height}
+ />
+
+ {formData.metric !== null && (
+ <Legend
+ categories={buckets}
+ position={formData.legend_position}
+ format={formData.legend_format}
/>
+ )}
+ </div>
+ );
+};
- {formData.metric !== null && (
- <Legend
- categories={buckets}
- position={formData.legend_position}
- format={formData.legend_format}
- />
- )}
- </div>
- );
- }
-}
-
-export default DeckGLPolygon;
+export default memo(DeckGLPolygon);
diff --git
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Screengrid/Screengrid.tsx
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Screengrid/Screengrid.tsx
index 173770c6c1..7e47cc9530 100644
---
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Screengrid/Screengrid.tsx
+++
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Screengrid/Screengrid.tsx
@@ -20,7 +20,7 @@
*/
/* eslint no-underscore-dangle: ["error", { "allow": ["", "__timestamp"] }] */
-import React from 'react';
+import React, { memo, useCallback, useEffect, useRef, useState } from 'react';
import { ScreenGridLayer } from 'deck.gl/typed';
import { JsonObject, JsonValue, QueryFormData, t } from '@superset-ui/core';
import { noop } from 'lodash';
@@ -30,7 +30,7 @@ import TooltipRow from '../../TooltipRow';
// eslint-disable-next-line import/extensions
import fitViewport, { Viewport } from '../../utils/fitViewport';
import {
- DeckGLContainer,
+ DeckGLContainerHandle,
DeckGLContainerStyledWrapper,
} from '../../DeckGLContainer';
import { TooltipProps } from '../../components/Tooltip';
@@ -99,93 +99,63 @@ export type DeckGLScreenGridProps = {
onAddFilter: () => void;
};
-export type DeckGLScreenGridState = {
- viewport: Viewport;
- formData: QueryFormData;
-};
-
-class DeckGLScreenGrid extends React.PureComponent<
- DeckGLScreenGridProps,
- DeckGLScreenGridState
-> {
- containerRef = React.createRef<DeckGLContainer>();
-
- constructor(props: DeckGLScreenGridProps) {
- super(props);
-
- this.state = DeckGLScreenGrid.getDerivedStateFromProps(
- props,
- ) as DeckGLScreenGridState;
-
- this.getLayers = this.getLayers.bind(this);
- }
-
- static getDerivedStateFromProps(
- props: DeckGLScreenGridProps,
- state?: DeckGLScreenGridState,
- ) {
- // the state is computed only from the payload; if it hasn't changed, do
- // not recompute state since this would reset selections and/or the play
- // slider position due to changes in form controls
- if (state && props.payload.form_data === state.formData) {
- return null;
- }
+const DeckGLScreenGrid = (props: DeckGLScreenGridProps) => {
+ const containerRef = useRef<DeckGLContainerHandle>();
+ const getAdjustedViewport = useCallback(() => {
const features = props.payload.data.features || [];
const { width, height, formData } = props;
- let { viewport } = props;
if (formData.autozoom) {
- viewport = fitViewport(viewport, {
+ return fitViewport(props.viewport, {
width,
height,
points: getPoints(features),
});
}
+ return props.viewport;
+ }, [props]);
- return {
- viewport,
- formData: props.payload.form_data as QueryFormData,
- };
- }
+ const [stateFormData, setStateFormData] = useState(props.payload.form_data);
+ const [viewport, setViewport] = useState(getAdjustedViewport());
- getLayers() {
- const layer = getLayer(
- this.props.formData,
- this.props.payload,
- noop,
- this.setTooltip,
- );
-
- return [layer];
- }
+ useEffect(() => {
+ if (props.payload.form_data !== stateFormData) {
+ setViewport(getAdjustedViewport());
+ setStateFormData(props.payload.form_data);
+ }
+ }, [getAdjustedViewport, props.payload.form_data, stateFormData]);
- setTooltip = (tooltip: TooltipProps['tooltip']) => {
- const { current } = this.containerRef;
+ const setTooltip = useCallback((tooltip: TooltipProps['tooltip']) => {
+ const { current } = containerRef;
if (current) {
current.setTooltip(tooltip);
}
- };
-
- render() {
- const { formData, payload, setControlValue } = this.props;
-
- return (
- <div>
- <DeckGLContainerStyledWrapper
- ref={this.containerRef}
- viewport={this.state.viewport}
- layers={this.getLayers()}
- setControlValue={setControlValue}
- mapStyle={formData.mapbox_style}
- mapboxApiAccessToken={payload.data.mapboxApiKey}
- width={this.props.width}
- height={this.props.height}
- />
- </div>
- );
- }
-}
+ }, []);
+
+ const getLayers = useCallback(() => {
+ const layer = getLayer(props.formData, props.payload, noop, setTooltip);
+
+ return [layer];
+ }, [props.formData, props.payload, setTooltip]);
+
+ const { formData, payload, setControlValue } = props;
+
+ return (
+ <div>
+ <DeckGLContainerStyledWrapper
+ ref={containerRef}
+ viewport={viewport}
+ layers={getLayers()}
+ setControlValue={setControlValue}
+ mapStyle={formData.mapbox_style}
+ mapboxApiAccessToken={payload.data.mapboxApiKey}
+ width={props.width}
+ height={props.height}
+ />
+ </div>
+ );
+};
-export default DeckGLScreenGrid;
+export default memo(DeckGLScreenGrid);