mirror of https://github.com/apache/superset.git
Geoviz state management fix (#6260)
* Fix deckgl getPoints * Fix CSS * Fix zoom * Fix CategoricalDeckGLContainer * Fix cypress
This commit is contained in:
parent
0584e3629f
commit
a57603adb4
|
@ -5,7 +5,7 @@ superset/bin/superset db upgrade
|
|||
superset/bin/superset load_test_users
|
||||
superset/bin/superset load_examples
|
||||
superset/bin/superset init
|
||||
superset/bin/superset runserver &
|
||||
flask run -p 8081 --with-threads --reload --debugger &
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ describe('getBreakPoints', () => {
|
|||
|
||||
it('returns sorted break points', () => {
|
||||
const fd = { break_points: ['0', '10', '100', '50', '1000'] };
|
||||
const result = getBreakPoints(fd);
|
||||
const result = getBreakPoints(fd, []);
|
||||
const expected = ['0', '10', '50', '100', '1000'];
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
|
|
@ -1,10 +1,19 @@
|
|||
.play-slider {
|
||||
position: relative;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
height: 40px;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.play-slider-controls {
|
||||
flex: 0 0 80px;
|
||||
text-align: middle;
|
||||
}
|
||||
|
||||
.play-slider-scrobbler {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.slider.slider-horizontal {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Mousetrap from 'mousetrap';
|
||||
import { Row, Col } from 'react-bootstrap';
|
||||
import { t } from '@superset-ui/translation';
|
||||
import BootrapSliderWrapper from '../components/BootstrapSliderWrapper';
|
||||
import './PlaySlider.css';
|
||||
|
@ -124,13 +123,13 @@ export default class PlaySlider extends React.PureComponent {
|
|||
render() {
|
||||
const { start, end, step, orientation, reversed, disabled, range, values } = this.props;
|
||||
return (
|
||||
<Row className="play-slider">
|
||||
<Col md={1} className="padded">
|
||||
<div className="play-slider">
|
||||
<div className="play-slider-controls padded">
|
||||
<i className="fa fa-step-backward fa-lg slider-button " onClick={this.stepBackward} />
|
||||
<i className={this.getPlayClass()} onClick={this.play} />
|
||||
<i className="fa fa-step-forward fa-lg slider-button " onClick={this.stepForward} />
|
||||
</Col>
|
||||
<Col md={11} className="padded">
|
||||
</div>
|
||||
<div className="play-slider-scrobbler padded">
|
||||
<BootrapSliderWrapper
|
||||
value={range ? values : values[0]}
|
||||
range={range}
|
||||
|
@ -143,8 +142,8 @@ export default class PlaySlider extends React.PureComponent {
|
|||
reversed={reversed}
|
||||
disabled={disabled ? 'disabled' : 'enabled'}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,8 @@ import PropTypes from 'prop-types';
|
|||
import DeckGLContainer from './DeckGLContainer';
|
||||
import PlaySlider from '../PlaySlider';
|
||||
|
||||
const PLAYSLIDER_HEIGHT = 20; // px
|
||||
|
||||
const propTypes = {
|
||||
getLayers: PropTypes.func.isRequired,
|
||||
start: PropTypes.number.isRequired,
|
||||
|
@ -30,6 +32,14 @@ export default class AnimatableDeckGLContainer extends React.Component {
|
|||
super(props);
|
||||
const { getLayers, start, end, getStep, values, disabled, viewport, ...other } = props;
|
||||
this.other = other;
|
||||
|
||||
this.onViewportChange = this.onViewportChange.bind(this);
|
||||
}
|
||||
onViewportChange(viewport) {
|
||||
const originalViewport = this.props.disabled
|
||||
? { ...viewport }
|
||||
: { ...viewport, height: viewport.height + PLAYSLIDER_HEIGHT };
|
||||
this.props.onViewportChange(originalViewport);
|
||||
}
|
||||
render() {
|
||||
const {
|
||||
|
@ -41,18 +51,24 @@ export default class AnimatableDeckGLContainer extends React.Component {
|
|||
children,
|
||||
getLayers,
|
||||
values,
|
||||
viewport,
|
||||
onViewportChange,
|
||||
onValuesChange,
|
||||
viewport,
|
||||
} = this.props;
|
||||
const layers = getLayers(values);
|
||||
|
||||
// leave space for the play slider
|
||||
const modifiedViewport = {
|
||||
...viewport,
|
||||
height: disabled ? viewport.height : viewport.height - PLAYSLIDER_HEIGHT,
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<DeckGLContainer
|
||||
{...this.other}
|
||||
viewport={viewport}
|
||||
viewport={modifiedViewport}
|
||||
layers={layers}
|
||||
onViewportChange={onViewportChange}
|
||||
onViewportChange={this.onViewportChange}
|
||||
/>
|
||||
{!disabled &&
|
||||
<PlaySlider
|
||||
|
|
|
@ -8,6 +8,7 @@ import { getScale } from '../../modules/colors/CategoricalColorNamespace';
|
|||
import { hexToRGB } from '../../modules/colors';
|
||||
import { getPlaySliderParams } from '../../modules/time';
|
||||
import sandboxedEval from '../../modules/sandbox';
|
||||
import { fitViewport } from './layers/common';
|
||||
|
||||
function getCategories(fd, data) {
|
||||
const c = fd.color_picker || { r: 0, g: 0, b: 0, a: 1 };
|
||||
|
@ -34,6 +35,7 @@ const propTypes = {
|
|||
setControlValue: PropTypes.func.isRequired,
|
||||
viewport: PropTypes.object.isRequired,
|
||||
getLayer: PropTypes.func.isRequired,
|
||||
getPoints: PropTypes.func.isRequired,
|
||||
payload: PropTypes.object.isRequired,
|
||||
onAddFilter: PropTypes.func,
|
||||
setTooltip: PropTypes.func,
|
||||
|
@ -49,12 +51,7 @@ export default class CategoricalDeckGLContainer extends React.PureComponent {
|
|||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
const fd = props.formData;
|
||||
const timeGrain = fd.time_grain_sqla || fd.granularity || 'PT1M';
|
||||
const timestamps = props.payload.data.features.map(f => f.__timestamp);
|
||||
const { start, end, getStep, values, disabled } = getPlaySliderParams(timestamps, timeGrain);
|
||||
const categories = getCategories(fd, props.payload.data.features);
|
||||
this.state = { start, end, getStep, values, disabled, categories, viewport: props.viewport };
|
||||
this.state = CategoricalDeckGLContainer.getDerivedStateFromProps(props);
|
||||
|
||||
this.getLayers = this.getLayers.bind(this);
|
||||
this.onValuesChange = this.onValuesChange.bind(this);
|
||||
|
@ -62,6 +59,51 @@ export default class CategoricalDeckGLContainer extends React.PureComponent {
|
|||
this.toggleCategory = this.toggleCategory.bind(this);
|
||||
this.showSingleCategory = this.showSingleCategory.bind(this);
|
||||
}
|
||||
static getDerivedStateFromProps(props, state) {
|
||||
const features = props.payload.data.features || [];
|
||||
const timestamps = features.map(f => f.__timestamp);
|
||||
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 };
|
||||
}
|
||||
|
||||
// the granularity has to be read from the payload form_data, not the
|
||||
// props formData which comes from the instantaneous controls state
|
||||
const granularity = (
|
||||
props.payload.form_data.time_grain_sqla ||
|
||||
props.payload.form_data.granularity ||
|
||||
'P1D'
|
||||
);
|
||||
|
||||
const {
|
||||
start,
|
||||
end,
|
||||
getStep,
|
||||
values,
|
||||
disabled,
|
||||
} = getPlaySliderParams(timestamps, granularity);
|
||||
|
||||
const viewport = props.formData.autozoom
|
||||
? fitViewport(props.viewport, props.getPoints(features))
|
||||
: props.viewport;
|
||||
|
||||
return {
|
||||
start,
|
||||
end,
|
||||
getStep,
|
||||
values,
|
||||
disabled,
|
||||
viewport,
|
||||
selected: [],
|
||||
lastClick: 0,
|
||||
formData: props.payload.form_data,
|
||||
categories,
|
||||
};
|
||||
}
|
||||
onValuesChange(values) {
|
||||
this.setState({
|
||||
values: Array.isArray(values)
|
||||
|
@ -80,7 +122,9 @@ export default class CategoricalDeckGLContainer extends React.PureComponent {
|
|||
onAddFilter,
|
||||
setTooltip,
|
||||
} = this.props;
|
||||
let features = [...payload.data.features];
|
||||
let features = payload.data.features
|
||||
? [...payload.data.features]
|
||||
: [];
|
||||
|
||||
// Add colors from categories or fixed color
|
||||
features = this.addColor(features, fd);
|
||||
|
|
|
@ -4,6 +4,8 @@ import MapGL from 'react-map-gl';
|
|||
import DeckGL from 'deck.gl';
|
||||
import 'mapbox-gl/dist/mapbox-gl.css';
|
||||
|
||||
const TICK = 1000; // milliseconds
|
||||
|
||||
const propTypes = {
|
||||
viewport: PropTypes.object.isRequired,
|
||||
layers: PropTypes.array.isRequired,
|
||||
|
@ -22,42 +24,42 @@ export default class DeckGLContainer extends React.Component {
|
|||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
viewport: props.viewport,
|
||||
previousViewport: props.viewport,
|
||||
timer: setInterval(this.tick, TICK),
|
||||
};
|
||||
this.tick = this.tick.bind(this);
|
||||
this.onViewportChange = this.onViewportChange.bind(this);
|
||||
}
|
||||
componentWillMount() {
|
||||
const timer = setInterval(this.tick, 1000);
|
||||
this.setState(() => ({ timer }));
|
||||
}
|
||||
componentWillReceiveProps(nextProps) {
|
||||
this.setState(() => ({
|
||||
viewport: { ...nextProps.viewport },
|
||||
previousViewport: this.state.viewport,
|
||||
}));
|
||||
static getDerivedStateFromProps(nextProps, prevState) {
|
||||
if (nextProps.viewport !== prevState.viewport) {
|
||||
return {
|
||||
viewport: { ...nextProps.viewport },
|
||||
previousViewport: prevState.viewport,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
componentWillUnmount() {
|
||||
clearInterval(this.state.timer);
|
||||
}
|
||||
onViewportChange(viewport) {
|
||||
const vp = Object.assign({}, viewport);
|
||||
delete vp.width;
|
||||
delete vp.height;
|
||||
const newVp = { ...this.state.viewport, ...vp };
|
||||
// delete vp.width;
|
||||
// delete vp.height;
|
||||
const newVp = { ...this.state.previousViewport, ...vp };
|
||||
|
||||
this.setState(() => ({ viewport: newVp }));
|
||||
// this.setState(() => ({ viewport: newVp }));
|
||||
this.props.onViewportChange(newVp);
|
||||
}
|
||||
tick() {
|
||||
// Limiting updating viewport controls through Redux at most 1*sec
|
||||
if (this.state.previousViewport !== this.state.viewport) {
|
||||
if (this.state && this.state.previousViewport !== this.props.viewport) {
|
||||
const setCV = this.props.setControlValue;
|
||||
const vp = this.state.viewport;
|
||||
const vp = this.props.viewport;
|
||||
if (setCV) {
|
||||
setCV('viewport', vp);
|
||||
}
|
||||
this.setState(() => ({ previousViewport: this.state.viewport }));
|
||||
this.setState(() => ({ previousViewport: this.props.viewport }));
|
||||
}
|
||||
}
|
||||
layers() {
|
||||
|
@ -68,7 +70,7 @@ export default class DeckGLContainer extends React.Component {
|
|||
return this.props.layers;
|
||||
}
|
||||
render() {
|
||||
const { viewport } = this.state;
|
||||
const { viewport } = this.props;
|
||||
return (
|
||||
<MapGL
|
||||
{...viewport}
|
||||
|
|
|
@ -59,13 +59,9 @@ export function createCategoricalDeckGLComponent(getLayer, getPoints) {
|
|||
setControlValue,
|
||||
onAddFilter,
|
||||
setTooltip,
|
||||
viewport: originalViewport,
|
||||
viewport,
|
||||
} = props;
|
||||
|
||||
const viewport = formData.autozoom
|
||||
? fitViewport(originalViewport, getPoints(payload.data.features))
|
||||
: originalViewport;
|
||||
|
||||
return (
|
||||
<CategoricalDeckGLContainer
|
||||
formData={formData}
|
||||
|
@ -76,6 +72,7 @@ export function createCategoricalDeckGLComponent(getLayer, getPoints) {
|
|||
payload={payload}
|
||||
onAddFilter={onAddFilter}
|
||||
setTooltip={setTooltip}
|
||||
getPoints={getPoints}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -9,12 +9,16 @@ import AnimatableDeckGLContainer from '../../AnimatableDeckGLContainer';
|
|||
import Legend from '../../../Legend';
|
||||
import { getBuckets, getBreakPointColorScaler } from '../../utils';
|
||||
|
||||
import { commonLayerProps } from '../common';
|
||||
import { commonLayerProps, fitViewport } from '../common';
|
||||
import { getPlaySliderParams } from '../../../../modules/time';
|
||||
import sandboxedEval from '../../../../modules/sandbox';
|
||||
|
||||
const DOUBLE_CLICK_TRESHOLD = 250; // milliseconds
|
||||
|
||||
function getPoints(features) {
|
||||
return features.map(d => d.polygon).flat();
|
||||
}
|
||||
|
||||
function getElevation(d, colorScaler) {
|
||||
/* in deck.gl 5.3.4 (used in Superset as of 2018-10-24), if a polygon has
|
||||
* opacity zero it will make everything behind it have opacity zero,
|
||||
|
@ -90,30 +94,60 @@ const defaultProps = {
|
|||
setTooltip() {},
|
||||
};
|
||||
|
||||
class DeckGLPolygon extends React.PureComponent {
|
||||
class DeckGLPolygon extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
const fd = props.formData;
|
||||
const timeGrain = fd.time_grain_sqla || fd.granularity || 'PT1M';
|
||||
const timestamps = props.payload.data.features.map(f => f.__timestamp);
|
||||
const { start, end, getStep, values, disabled } = getPlaySliderParams(timestamps, timeGrain);
|
||||
this.state = {
|
||||
start,
|
||||
end,
|
||||
getStep,
|
||||
values,
|
||||
disabled,
|
||||
viewport: props.viewport,
|
||||
selected: [],
|
||||
lastClick: 0,
|
||||
};
|
||||
this.state = DeckGLPolygon.getDerivedStateFromProps(props);
|
||||
|
||||
this.getLayers = this.getLayers.bind(this);
|
||||
this.onSelect = this.onSelect.bind(this);
|
||||
this.onValuesChange = this.onValuesChange.bind(this);
|
||||
this.onViewportChange = this.onViewportChange.bind(this);
|
||||
}
|
||||
static getDerivedStateFromProps(props, state) {
|
||||
// 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 features = props.payload.data.features || [];
|
||||
const timestamps = features.map(f => f.__timestamp);
|
||||
|
||||
// the granularity has to be read from the payload form_data, not the
|
||||
// props formData which comes from the instantaneous controls state
|
||||
const granularity = (
|
||||
props.payload.form_data.time_grain_sqla ||
|
||||
props.payload.form_data.granularity ||
|
||||
'P1D'
|
||||
);
|
||||
|
||||
const {
|
||||
start,
|
||||
end,
|
||||
getStep,
|
||||
values,
|
||||
disabled,
|
||||
} = getPlaySliderParams(timestamps, granularity);
|
||||
|
||||
const viewport = props.formData.autozoom
|
||||
? fitViewport(props.viewport, getPoints(features))
|
||||
: props.viewport;
|
||||
|
||||
return {
|
||||
start,
|
||||
end,
|
||||
getStep,
|
||||
values,
|
||||
disabled,
|
||||
viewport,
|
||||
selected: [],
|
||||
lastClick: 0,
|
||||
formData: props.payload.form_data,
|
||||
};
|
||||
}
|
||||
onSelect(polygon) {
|
||||
const { formData, onAddFilter } = this.props;
|
||||
|
||||
|
|
|
@ -64,19 +64,55 @@ class DeckGLScreenGrid extends React.PureComponent {
|
|||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
const fd = props.formData;
|
||||
const timeGrain = fd.time_grain_sqla || fd.granularity || 'PT1M';
|
||||
const timestamps = props.payload.data.features.map(f => f.__timestamp);
|
||||
const { start, end, getStep, values, disabled } = getPlaySliderParams(timestamps, timeGrain);
|
||||
const viewport = fd.autozoom
|
||||
? fitViewport(props.viewport, getPoints(props.payload.data.features))
|
||||
: props.viewport;
|
||||
this.state = { start, end, getStep, values, disabled, viewport };
|
||||
this.state = DeckGLScreenGrid.getDerivedStateFromProps(props);
|
||||
|
||||
this.getLayers = this.getLayers.bind(this);
|
||||
this.onValuesChange = this.onValuesChange.bind(this);
|
||||
this.onViewportChange = this.onViewportChange.bind(this);
|
||||
}
|
||||
static getDerivedStateFromProps(props, state) {
|
||||
// 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 features = props.payload.data.features || [];
|
||||
const timestamps = features.map(f => f.__timestamp);
|
||||
|
||||
// the granularity has to be read from the payload form_data, not the
|
||||
// props formData which comes from the instantaneous controls state
|
||||
const granularity = (
|
||||
props.payload.form_data.time_grain_sqla ||
|
||||
props.payload.form_data.granularity ||
|
||||
'P1D'
|
||||
);
|
||||
|
||||
const {
|
||||
start,
|
||||
end,
|
||||
getStep,
|
||||
values,
|
||||
disabled,
|
||||
} = getPlaySliderParams(timestamps, granularity);
|
||||
|
||||
const viewport = props.formData.autozoom
|
||||
? fitViewport(props.viewport, getPoints(features))
|
||||
: props.viewport;
|
||||
|
||||
return {
|
||||
start,
|
||||
end,
|
||||
getStep,
|
||||
values,
|
||||
disabled,
|
||||
viewport,
|
||||
selected: [],
|
||||
lastClick: 0,
|
||||
formData: props.payload.form_data,
|
||||
};
|
||||
}
|
||||
onValuesChange(values) {
|
||||
this.setState({
|
||||
values: Array.isArray(values)
|
||||
|
|
|
@ -7,6 +7,9 @@ export function getBreakPoints({
|
|||
num_buckets: formDataNumBuckets,
|
||||
metric,
|
||||
}, features) {
|
||||
if (!features) {
|
||||
return [];
|
||||
}
|
||||
if (formDataBreakPoints === undefined || formDataBreakPoints.length === 0) {
|
||||
// compute evenly distributed break points based on number of buckets
|
||||
const numBuckets = formDataNumBuckets
|
||||
|
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue