Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Play scrubber #4336

Merged
merged 29 commits into from
Feb 16, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
c31e642
Initial working prototype
betodealmeida Jan 25, 2018
4ba5e6e
Small fixes
betodealmeida Jan 25, 2018
e66ce7a
Refactoring dekgl
betodealmeida Jan 26, 2018
a112a62
Show all data when no time grain is selected
betodealmeida Jan 27, 2018
b870862
Refactor layers
betodealmeida Jan 29, 2018
5d6f62f
Standardize function name
betodealmeida Jan 29, 2018
4caa673
Fix exports
betodealmeida Jan 29, 2018
52369ac
Fix require
betodealmeida Jan 29, 2018
387ce72
Initial working prototype
betodealmeida Jan 25, 2018
bd2d45e
Small fixes
betodealmeida Jan 25, 2018
ef59605
Show all data when no time grain is selected
betodealmeida Jan 27, 2018
cdd7139
Moving play bar to correct location
betodealmeida Jan 30, 2018
dcf9561
Split component
betodealmeida Jan 31, 2018
4946c33
Working on CSS
betodealmeida Jan 31, 2018
6718276
Merge branch 'play_time' of github.com:lyft/incubator-superset into p…
betodealmeida Jan 31, 2018
932b2fb
Remove control
betodealmeida Jan 31, 2018
62ddd0e
Positioning the play slider
betodealmeida Jan 31, 2018
813d1b0
Fix refresh of slider state
betodealmeida Jan 31, 2018
a5697a1
Fix lint
betodealmeida Jan 31, 2018
f9b3c65
Small fixes
betodealmeida Feb 1, 2018
f8e9ce6
Smoother animation for scans
betodealmeida Feb 1, 2018
5f34f63
Rebase
betodealmeida Feb 1, 2018
01be0dc
Fix versions
betodealmeida Feb 1, 2018
a18f8e0
Play/pause with spacebar.
betodealmeida Feb 2, 2018
1f0e026
Small fixes
betodealmeida Feb 2, 2018
1afa41e
Clean stuff that went to other PRs
betodealmeida Feb 2, 2018
f3c1a45
Address issues
betodealmeida Feb 9, 2018
092f1e0
Refactor scatter animation
betodealmeida Feb 15, 2018
ec92c63
Merge branch 'master' into play_time
betodealmeida Feb 15, 2018
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions superset/assets/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
"@data-ui/sparkline": "^0.0.49",
"babel-register": "^6.24.1",
"bootstrap": "^3.3.6",
"bootstrap-slider": "^10.0.0",
"brace": "^0.10.0",
"brfs": "^1.4.3",
"cal-heatmap": "3.6.2",
Expand All @@ -70,6 +71,7 @@
"mapbox-gl": "^0.43.0",
"mathjs": "^3.20.2",
"moment": "^2.20.1",
"mousetrap": "^1.6.1",
"mustache": "^2.2.1",
"nvd3": "1.8.6",
"po2json": "^0.4.5",
Expand All @@ -80,6 +82,7 @@
"react-addons-shallow-compare": "^15.4.2",
"react-alert": "^2.3.0",
"react-bootstrap": "^0.31.5",
"react-bootstrap-slider": "2.0.1",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool, looks suited to support a nice SliderControl eventually.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, it should be straightforward to add the SliderControl — I actually added it at some point, and then removed.

(The latest version of the component is incompatible with other dependencies, which is why I fixed the version to 2.0.1, BTW.)

"react-bootstrap-table": "^4.0.2",
"react-color": "^2.13.8",
"react-datetime": "2.9.0",
Expand Down
21 changes: 21 additions & 0 deletions superset/assets/visualizations/PlaySlider.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
.play-slider {
height: 100px;
Copy link
Member

@mistercrunch mistercrunch Feb 7, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

counldn't figure out how the DeckGL container knows how to set itself to height() -100 or is it just rendering on top of it?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's the space currently left in the bottom, below the DeckGL container. TBH I wasn't really sure about the best way to place this.

margin-top: -5px;
}

.slider-selection {
background: #efefef;
}

.slider-handle {
background: #b3b3b3;
}

.slider.slider-horizontal {
width: 100% !important;
}

.slider-button {
color: #b3b3b3;
margin-right: 5px;
}
136 changes: 136 additions & 0 deletions superset/assets/visualizations/PlaySlider.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Row, Col } from 'react-bootstrap';

import Mousetrap from 'mousetrap';

import 'bootstrap-slider/dist/css/bootstrap-slider.min.css';
import ReactBootstrapSlider from 'react-bootstrap-slider';
import './PlaySlider.css';

import { t } from '../javascripts/locales';

const propTypes = {
start: PropTypes.number.isRequired,
step: PropTypes.number.isRequired,
end: PropTypes.number.isRequired,
values: PropTypes.array.isRequired,
onChange: PropTypes.func,
loopDuration: PropTypes.number,
maxFrames: PropTypes.number,
orientation: PropTypes.oneOf(['horizontal', 'vertical']),
reversed: PropTypes.bool,
disabled: PropTypes.bool,
};

const defaultProps = {
onChange: () => {},
loopDuration: 15000,
maxFrames: 100,
orientation: 'horizontal',
reversed: false,
disabled: false,
};

export default class PlaySlider extends React.PureComponent {
constructor(props) {
super(props);
this.state = { intervalId: null };

const range = props.end - props.start;
const frames = Math.min(props.maxFrames, range / props.step);
const width = range / frames;
this.intervalMilliseconds = props.loopDuration / frames;
this.increment = width < props.step ? props.step : width - (width % props.step);

this.onChange = this.onChange.bind(this);
this.play = this.play.bind(this);
this.pause = this.pause.bind(this);
this.step = this.step.bind(this);
this.getPlayClass = this.getPlayClass.bind(this);
this.formatter = this.formatter.bind(this);
}
componentDidMount() {
Mousetrap.bind(['space'], this.play);
}
componentWillUnmount() {
Mousetrap.unbind(['space']);
}
onChange(event) {
this.props.onChange(event.target.value);
if (this.state.intervalId != null) {
this.pause();
}
}
getPlayClass() {
if (this.state.intervalId == null) {
return 'fa fa-play fa-lg slider-button';
}
return 'fa fa-pause fa-lg slider-button';
}
play() {
if (this.props.disabled) {
return;
}
if (this.state.intervalId != null) {
this.pause();
} else {
const id = setInterval(this.step, this.intervalMilliseconds);
this.setState({ intervalId: id });
}
}
pause() {
clearInterval(this.state.intervalId);
this.setState({ intervalId: null });
}
step() {
if (this.props.disabled) {
return;
}
let values = this.props.values.map(value => value + this.increment);
if (values[1] > this.props.end) {
const cr = values[0] - this.props.start;
values = values.map(value => value - cr);
}
this.props.onChange(values);
}
formatter(values) {
if (this.props.disabled) {
return t('Data has no time steps');
}

let parts = values;
if (!Array.isArray(values)) {
parts = [values];
} else if (values[0] === values[1]) {
parts = [values[0]];
}
return parts.map(value => (new Date(value)).toUTCString()).join(' : ');
}
render() {
return (
<Row className="play-slider">
<Col md={1} className="padded">
<i className={this.getPlayClass()} onClick={this.play} />
<i className="fa fa-step-forward fa-lg slider-button " onClick={this.step} />
</Col>
<Col md={11} className="padded">
<ReactBootstrapSlider
value={this.props.values}
formatter={this.formatter}
change={this.onChange}
min={this.props.start}
max={this.props.end}
step={this.props.step}
orientation={this.props.orientation}
reversed={this.props.reversed}
disabled={this.props.disabled ? 'disabled' : 'enabled'}
/>
</Col>
</Row>
);
}
}

PlaySlider.propTypes = propTypes;
PlaySlider.defaultProps = defaultProps;
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import React from 'react';
import PropTypes from 'prop-types';

import DeckGLContainer from './DeckGLContainer';
import PlaySlider from '../PlaySlider';

const propTypes = {
getLayers: PropTypes.func.isRequired,
start: PropTypes.number.isRequired,
end: PropTypes.number.isRequired,
step: PropTypes.number.isRequired,
values: PropTypes.array.isRequired,
disabled: PropTypes.bool,
viewport: PropTypes.object.isRequired,
};

const defaultProps = {
disabled: false,
};

export default class AnimatableDeckGLContainer extends React.Component {
constructor(props) {
super(props);
const { getLayers, start, end, step, values, disabled, viewport, ...other } = props;
this.state = { values, viewport };
this.other = other;
}
componentWillReceiveProps(nextProps) {
this.setState({ values: nextProps.values, viewport: nextProps.viewport });
}
render() {
const layers = this.props.getLayers(this.state.values);
return (
<div>
<DeckGLContainer
{...this.other}
viewport={this.state.viewport}
layers={layers}
onViewportChange={newViewport => this.setState({ viewport: newViewport })}
/>
{!this.props.disabled &&
<PlaySlider
start={this.props.start}
end={this.props.end}
step={this.props.step}
values={this.state.values}
onChange={newValues => this.setState({ values: newValues })}
/>
}
</div>
);
}
}

AnimatableDeckGLContainer.propTypes = propTypes;
AnimatableDeckGLContainer.defaultProps = defaultProps;
116 changes: 109 additions & 7 deletions superset/assets/visualizations/deckgl/layers/scatter.jsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,45 @@
/* eslint no-underscore-dangle: ["error", { "allow": ["", "__timestamp"] }] */

import React from 'react';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';

import { ScatterplotLayer } from 'deck.gl';

import DeckGLContainer from './../DeckGLContainer';
import AnimatableDeckGLContainer from '../AnimatableDeckGLContainer';

import * as common from './common';
import { getColorFromScheme, hexToRGB } from '../../../javascripts/modules/colors';
import { unitToRadius } from '../../../javascripts/modules/geo';
import sandboxedEval from '../../../javascripts/modules/sandbox';

function getStep(timeGrain) {
// grain in microseconds
const MINUTE = 60 * 1000;
const HOUR = 60 * MINUTE;
const DAY = 24 * HOUR;
const WEEK = 7 * DAY;
const MONTH = 30 * DAY;
const YEAR = 365 * DAY;

const milliseconds = {
'Time Column': MINUTE,
min: MINUTE,
hour: HOUR,
day: DAY,
week: WEEK,
month: MONTH,
year: YEAR,
};

return milliseconds[timeGrain];
}

function getPoints(data) {
return data.map(d => d.position);
}

function getLayer(formData, payload, slice) {
function getLayer(formData, payload, slice, inFrame) {
const fd = formData;
const c = fd.color_picker || { r: 0, g: 0, b: 0, a: 1 };
const fixedColor = [c.r, c.g, c.b, 255 * c.a];
Expand Down Expand Up @@ -41,6 +68,10 @@ function getLayer(formData, payload, slice) {
data = jsFnMutator(data);
}

if (inFrame != null) {
data = data.filter(inFrame);
}

return new ScatterplotLayer({
id: `scatter-layer-${fd.slice_id}`,
data,
Expand All @@ -50,6 +81,78 @@ function getLayer(formData, payload, slice) {
});
}

const propTypes = {
slice: PropTypes.object.isRequired,
payload: PropTypes.object.isRequired,
setControlValue: PropTypes.func.isRequired,
viewport: PropTypes.object.isRequired,
};

class DeckGLScatter extends React.PureComponent {
/* eslint-disable no-unused-vars */
static getDerivedStateFromProps(nextProps, prevState) {
const fd = nextProps.slice.formData;
const timeGrain = fd.time_grain_sqla || fd.granularity || 'min';

// find start and end based on the data
const timestamps = nextProps.payload.data.features.map(f => f.__timestamp);
let start = Math.min(...timestamps);
let end = Math.max(...timestamps);

// lock start and end to the closest steps
const step = getStep(timeGrain);
start -= start % step;
end += step - end % step;

const values = timeGrain != null ? [start, start + step] : [start, end];
const disabled = timestamps.every(timestamp => timestamp === null);

return { start, end, step, values, disabled };
}
constructor(props) {
super(props);
this.state = DeckGLScatter.getDerivedStateFromProps(props);

this.getLayers = this.getLayers.bind(this);
}
componentWillReceiveProps(nextProps) {
this.setState(DeckGLScatter.getDerivedStateFromProps(nextProps, this.state));
}
getLayers(values) {
let inFrame;
if (values[0] === values[1] || values[1] === this.end) {
inFrame = t => t.__timestamp >= values[0] && t.__timestamp <= values[1];
} else {
inFrame = t => t.__timestamp >= values[0] && t.__timestamp < values[1];
}
const layer = getLayer(
this.props.slice.formData,
this.props.payload,
this.props.slice,
inFrame);

return [layer];
}
render() {
return (
<AnimatableDeckGLContainer
getLayers={this.getLayers}
start={this.state.start}
end={this.state.end}
step={this.state.step}
values={this.state.values}
disabled={this.state.disabled}
viewport={this.props.viewport}
mapboxApiAccessToken={this.props.payload.data.mapboxApiKey}
mapStyle={this.props.slice.formData.mapbox_style}
setControlValue={this.props.setControlValue}
/>
);
}
}

DeckGLScatter.propTypes = propTypes;

function deckScatter(slice, payload, setControlValue) {
const layer = getLayer(slice.formData, payload, slice);
const fd = slice.formData;
Expand All @@ -66,12 +169,11 @@ function deckScatter(slice, payload, setControlValue) {
}

ReactDOM.render(
<DeckGLContainer
mapboxApiAccessToken={payload.data.mapboxApiKey}
viewport={viewport}
layers={[layer]}
mapStyle={fd.mapbox_style}
<DeckGLScatter
slice={slice}
payload={payload}
setControlValue={setControlValue}
viewport={viewport}
/>,
document.getElementById(slice.containerId),
);
Expand Down
Loading