feat: support polygon tooltip independently from superset app (#12)

* feat: support polygon tooltip independently from superset app

* fix: remove unintended changes

* fix: minor styling

* fix: tooltip

* fix: lowercase name

* fix: storybook
This commit is contained in:
Krist Wongsuphasawat 2020-03-18 18:01:00 -07:00 committed by Yongjie Zhao
parent 4f0d4e038b
commit abc4a04294
15 changed files with 151 additions and 37 deletions

View File

@ -42,7 +42,8 @@
"react-bootstrap-slider": "2.1.5",
"react-map-gl": "^4.0.10",
"underscore": "^1.8.3",
"urijs": "^1.18.10"
"urijs": "^1.18.10",
"xss": "^1.0.6"
},
"peerDependencies": {
"@superset-ui/chart": "^0.12.0",

View File

@ -55,6 +55,14 @@ const defaultProps = {
};
export default class AnimatableDeckGLContainer extends React.PureComponent {
containerRef = React.createRef();
setTooltip = tooltip => {
if (this.containerRef.current) {
this.containerRef.current.setTooltip(tooltip);
}
};
render() {
const {
start,
@ -78,6 +86,7 @@ export default class AnimatableDeckGLContainer extends React.PureComponent {
return (
<div>
<DeckGLContainer
ref={this.containerRef}
viewport={viewport}
layers={layers}
setControlValue={setControlValue}

View File

@ -72,6 +72,8 @@ const propTypes = {
};
export default class CategoricalDeckGLContainer extends React.PureComponent {
containerRef = React.createRef();
/*
* A Deck.gl container that handles categories.
*
@ -225,10 +227,17 @@ export default class CategoricalDeckGLContainer extends React.PureComponent {
this.setState({ categories });
}
setTooltip = tooltip => {
if (this.containerRef.current) {
this.containerRef.current.setTooltip(tooltip);
}
};
render() {
return (
<div style={{ position: 'relative' }}>
<AnimatableDeckGLContainer
ref={this.containerRef}
getLayers={this.getLayers}
start={this.state.start}
end={this.state.end}

View File

@ -24,6 +24,8 @@ import React from 'react';
import PropTypes from 'prop-types';
import { StaticMap } from 'react-map-gl';
import DeckGL from 'deck.gl';
// eslint-disable-next-line import/extensions
import Tooltip from './components/Tooltip';
import 'mapbox-gl/dist/mapbox-gl.css';
import './css/deckgl.css';
@ -55,6 +57,7 @@ export default class DeckGLContainer extends React.Component {
// This has to be placed after this.tick is bound to this
this.state = {
timer: setInterval(this.tick, TICK),
tooltip: null,
viewState: props.viewport,
};
}
@ -88,31 +91,38 @@ export default class DeckGLContainer extends React.Component {
return this.props.layers;
}
setTooltip = tooltip => {
this.setState({ tooltip });
};
render() {
const { children, bottomMargin, height, width } = this.props;
const { viewState } = this.state;
const { viewState, tooltip } = this.state;
const adjustedHeight = height - bottomMargin;
const layers = this.layers();
return (
<div style={{ position: 'relative', width, height: adjustedHeight }}>
<DeckGL
initWebGLParameters
controller
width={width}
height={adjustedHeight}
layers={layers}
viewState={viewState}
onViewStateChange={this.onViewStateChange}
>
<StaticMap
mapStyle={this.props.mapStyle}
mapboxApiAccessToken={this.props.mapboxApiAccessToken}
/>
</DeckGL>
{children}
</div>
<>
<div style={{ position: 'relative', width, height: adjustedHeight }}>
<DeckGL
initWebGLParameters
controller
width={width}
height={adjustedHeight}
layers={layers}
viewState={viewState}
onViewStateChange={this.onViewStateChange}
>
<StaticMap
mapStyle={this.props.mapStyle}
mapboxApiAccessToken={this.props.mapboxApiAccessToken}
/>
</DeckGL>
{children}
</div>
<Tooltip tooltip={tooltip} />
</>
);
}
}

View File

@ -0,0 +1,60 @@
import React, { useMemo, CSSProperties } from 'react';
import { filterXSS } from 'xss';
export type TooltipProps = {
tooltip:
| {
x: number;
y: number;
content: string;
}
| null
| undefined;
};
export default function Tooltip(props: TooltipProps) {
const { tooltip } = props;
if (typeof tooltip === 'undefined' || tooltip === null) {
return null;
}
const { x, y, content } = tooltip;
// eslint-disable-next-line react-hooks/rules-of-hooks
const style: CSSProperties = useMemo(
() => ({
position: 'absolute',
top: `${y}px`,
left: `${x}px`,
padding: '8px',
margin: '8px',
background: 'rgba(0, 0, 0, 0.8)',
color: '#fff',
maxWidth: '300px',
fontSize: '12px',
zIndex: 9,
pointerEvents: 'none',
}),
[x, y],
);
if (typeof content === 'string') {
// eslint-disable-next-line react-hooks/rules-of-hooks
const contentHtml = useMemo(
() => ({
__html: filterXSS(content, { stripIgnoreTag: true }),
}),
[content],
);
return (
<div style={style}>
<div
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={contentHtml}
/>
</div>
);
}
return <div style={style}>{content}</div>;
}

View File

@ -56,10 +56,13 @@ function setTooltipContent(formData) {
return (
<div className="deckgl-tooltip">
<TooltipRow
label={`${formData.line_column}: `}
value={`${o.object[formData.line_column]}`}
/>
{o.object.name && <TooltipRow label="name: " value={`${o.object.name}`} />}
{o.object[formData.line_column] && (
<TooltipRow
label={`${formData.line_column}: `}
value={`${o.object[formData.line_column]}`}
/>
)}
{formData.metric && (
<TooltipRow label={`${metricLabel}: `} value={`${o.object[metricLabel]}`} />
)}
@ -103,8 +106,9 @@ export function getLayer(formData, payload, onAddFilter, setTooltip, selected, o
return baseColor;
};
const tooltipContentGenerator =
fd.line_column && fd.metric && ['geohash', 'zipcode'].includes(fd.line_type)
fd.line_column && fd.metric && ['json', 'geohash', 'zipcode'].includes(fd.line_type)
? setTooltipContent(fd)
: undefined;
@ -132,17 +136,17 @@ const propTypes = {
setControlValue: PropTypes.func.isRequired,
viewport: PropTypes.object.isRequired,
onAddFilter: PropTypes.func,
setTooltip: PropTypes.func,
width: PropTypes.number.isRequired,
height: PropTypes.number.isRequired,
};
const defaultProps = {
onAddFilter() {},
setTooltip() {},
};
class DeckGLPolygon extends React.Component {
containerRef = React.createRef();
constructor(props) {
super(props);
@ -245,7 +249,7 @@ class DeckGLPolygon extends React.Component {
this.props.formData,
this.props.payload,
this.props.onAddFilter,
this.props.setTooltip,
this.setTooltip,
this.state.selected,
this.onSelect,
filters,
@ -254,6 +258,12 @@ class DeckGLPolygon extends React.Component {
return [layer];
}
setTooltip = tooltip => {
if (this.containerRef.current) {
this.containerRef.current.setTooltip(tooltip);
}
};
render() {
const { payload, formData, setControlValue } = this.props;
const { start, end, getStep, values, disabled, viewport } = this.state;
@ -267,6 +277,7 @@ class DeckGLPolygon extends React.Component {
return (
<div style={{ position: 'relative' }}>
<AnimatableDeckGLContainer
ref={this.containerRef}
aggregation
getLayers={this.getLayers}
start={start}

View File

@ -76,6 +76,6 @@ export default [
/>
),
storyName: 'Basic',
storyPath: 'legacy-|preset-chart-deckgl|ArcChartPlugin',
storyPath: 'legacy-preset-chart-deckgl|ArcChartPlugin',
},
];

View File

@ -47,6 +47,6 @@ export default [
/>
),
storyName: 'Basic',
storyPath: 'legacy-|preset-chart-deckgl|GridChartPlugin',
storyPath: 'legacy-preset-chart-deckgl|GridChartPlugin',
},
];

View File

@ -48,6 +48,6 @@ export default [
/>
),
storyName: 'Basic',
storyPath: 'legacy-|preset-chart-deckgl|HexChartPlugin',
storyPath: 'legacy-preset-chart-deckgl|HexChartPlugin',
},
];

View File

@ -56,6 +56,6 @@ export default [
/>
),
storyName: 'Basic',
storyPath: 'legacy-|preset-chart-deckgl|PathChartPlugin',
storyPath: 'legacy-preset-chart-deckgl|PathChartPlugin',
},
];

View File

@ -26,7 +26,7 @@ export default [
line_column: 'contour',
line_type: 'json',
adhoc_filters: [],
metric: 'count',
metric: 'population',
point_radius_fixed: { type: 'fix', value: 1000 },
row_limit: 10000,
reverse_long_lat: false,
@ -71,7 +71,7 @@ export default [
/>
),
storyName: 'Basic',
storyPath: 'legacy-|preset-chart-deckgl|PolygonChartPlugin',
storyPath: 'legacy-preset-chart-deckgl|PolygonChartPlugin',
},
{
renderStory: () => (
@ -119,6 +119,6 @@ export default [
/>
),
storyName: 'Single Polygon in geojson format',
storyPath: 'legacy-|preset-chart-deckgl|PolygonChartPlugin',
storyPath: 'legacy-preset-chart-deckgl|PolygonChartPlugin',
},
];

View File

@ -82,7 +82,8 @@ export default {
data: {
features: [
{
count: 1,
count: 10,
name: 'Test',
polygon: {
type: 'Feature',
properties: {},

View File

@ -55,6 +55,6 @@ export default [
/>
),
storyName: 'Basic',
storyPath: 'legacy-|preset-chart-deckgl|ScatterChartPlugin',
storyPath: 'legacy-preset-chart-deckgl|ScatterChartPlugin',
},
];

View File

@ -46,6 +46,6 @@ export default [
/>
),
storyName: 'Basic',
storyPath: 'legacy-|preset-chart-deckgl|ScreengridChartPlugin',
storyPath: 'legacy-preset-chart-deckgl|ScreengridChartPlugin',
},
];

View File

@ -6831,6 +6831,11 @@ cssesc@^3.0.0:
resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee"
integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==
cssfilter@0.0.10:
version "0.0.10"
resolved "https://registry.yarnpkg.com/cssfilter/-/cssfilter-0.0.10.tgz#c6d2672632a2e5c83e013e6864a42ce8defd20ae"
integrity sha1-xtJnJjKi5cg+AT5oZKQs6N79IK4=
csso@^4.0.2:
version "4.0.2"
resolved "https://registry.yarnpkg.com/csso/-/csso-4.0.2.tgz#e5f81ab3a56b8eefb7f0092ce7279329f454de3d"
@ -17859,6 +17864,14 @@ xmlhttprequest@1:
resolved "https://registry.yarnpkg.com/xmlhttprequest/-/xmlhttprequest-1.8.0.tgz#67fe075c5c24fef39f9d65f5f7b7fe75171968fc"
integrity sha1-Z/4HXFwk/vOfnWX197f+dRcZaPw=
xss@^1.0.6:
version "1.0.6"
resolved "https://registry.yarnpkg.com/xss/-/xss-1.0.6.tgz#eaf11e9fc476e3ae289944a1009efddd8a124b51"
integrity sha512-6Q9TPBeNyoTRxgZFk5Ggaepk/4vUOYdOsIUYvLehcsIZTFjaavbVnsuAkLA5lIFuug5hw8zxcB9tm01gsjph2A==
dependencies:
commander "^2.9.0"
cssfilter "0.0.10"
xtend@^4.0.0, xtend@^4.0.1, xtend@~4.0.1:
version "4.0.2"
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"