cabana/src/components/CanGraph.js

416 lines
12 KiB
JavaScript

import React, { Component } from 'react';
import Measure from 'react-measure';
import PropTypes from 'prop-types';
import cx from 'classnames';
import { Vega } from 'react-vega';
import Signal from '../models/can/signal';
import GraphData from '../models/graph-data';
import CanPlotSpec from '../vega/CanPlot';
import debounce from '../utils/debounce';
const DefaultPlotInnerStyle = {
position: 'absolute',
top: 0,
left: 0
};
export default class CanGraph extends Component {
static emptyTable = [];
static propTypes = {
plottedSignal: PropTypes.string,
messages: PropTypes.object,
messageId: PropTypes.string,
messageName: PropTypes.string,
signalSpec: PropTypes.instanceOf(Signal),
segment: PropTypes.array,
unplot: PropTypes.func,
onRelativeTimeClick: PropTypes.func,
currentTime: PropTypes.number,
onSegmentChanged: PropTypes.func,
onDragStart: PropTypes.func,
onDragEnd: PropTypes.func,
container: PropTypes.node,
dragPos: PropTypes.object,
canReceiveGraphDrop: PropTypes.bool,
onGraphRefAvailable: PropTypes.func,
plottedSignals: PropTypes.array
};
constructor(props) {
super(props);
this.state = {
plotInnerStyle: null,
shiftX: 0,
shiftY: 0,
bounds: null,
isDataInserted: false,
data: this.getGraphData(props),
spec: this.getGraphSpec(props)
};
this.onNewView = this.onNewView.bind(this);
this.onSignalClickTime = this.onSignalClickTime.bind(this);
this.onSignalSegment = this.onSignalSegment.bind(this);
this.onDragAnchorMouseDown = this.onDragAnchorMouseDown.bind(this);
this.onDragAnchorMouseUp = this.onDragAnchorMouseUp.bind(this);
this.onDragStart = this.onDragStart.bind(this);
this.onPlotResize = this.onPlotResize.bind(this);
this.insertData = this.insertData.bind(this);
}
getGraphData(props) {
let firstRelTime = -1;
let lastRelTime = -1;
const series = props.plottedSignals
.map((signals) => {
const { messageId, signalUid } = signals;
const { entries } = props.messages[messageId];
if (entries.length) {
let messageRelTime = entries[0].relTime;
if (firstRelTime === -1) {
firstRelTime = messageRelTime;
} else {
firstRelTime = Math.min(firstRelTime, messageRelTime);
}
messageRelTime = entries[entries.length - 1].relTime;
lastRelTime = Math.max(lastRelTime, messageRelTime);
}
return GraphData._calcGraphData(
props.messages[messageId],
signalUid,
0
);
})
.reduce((m, v) => m.concat(v), []);
return {
updated: Date.now(),
series,
firstRelTime,
lastRelTime
};
}
getGraphSpec(props) {
return {
...CanPlotSpec,
scales: [
{
...CanPlotSpec.scales[0],
domainMin: props.segment[0],
domainMax: props.segment[1]
},
...CanPlotSpec.scales.slice(1)
]
};
}
segmentIsNew(newSegment) {
return (
newSegment.length !== this.props.segment.length
|| !newSegment.every((val, idx) => this.props.segment[idx] === val)
);
}
visualChanged(prevProps, nextProps) {
return (
prevProps.canReceiveGraphDrop !== nextProps.canReceiveGraphDrop
|| JSON.stringify(prevProps.dragPos) !== JSON.stringify(nextProps.dragPos)
);
}
onPlotResize(options) {
if (!this.view) {
return;
}
let bounds = null; // eslint-disable-line no-unused-vars
if (options && options.bounds) {
this.setState({ bounds: options.bounds });
bounds = options.bounds;
} else {
bounds = this.state.bounds;
}
this.view.runAfter(this.updateBounds);
}
updateBounds = debounce(() => {
this.view.signal('width', this.state.bounds.width - 70);
this.view.signal('height', 0.4 * (this.state.bounds.width - 70)); // 5:2 aspect ratio
this.view.run();
}, 100);
insertData = debounce(() => {
if (!this.view) {
console.log('Cannot insertData');
return;
}
// adding plot points by diff isn't faster since it basically has to be n^2
// out-of-order events make it so that you can't just check the bounds
const { series } = this.state.data;
const changeset = this.view
.changeset()
.remove((v) => true)
.insert(series);
this.view.change('table', changeset);
this.view.run();
}, 250);
componentWillReceiveProps(nextProps) {
if (
nextProps.dragPos
&& JSON.stringify(nextProps.dragPos) !== JSON.stringify(this.props.dragPos)
) {
this.updateStyleFromDragPos(nextProps.dragPos);
} else if (!nextProps.dragPos && this.state.plotInnerStyle !== null) {
this.setState({ plotInnerStyle: null });
}
if (
this.props.messages !== nextProps.messages
|| this.props.plottedSignal !== nextProps.plottedSignal
) {
console.log('Calculating new data!');
const data = this.getGraphData(nextProps);
// if (
// data.series.length === this.state.data.series.length
// && data.firstRelTime === this.state.data.firstRelTime
// && data.lastRelTime === this.state.data.lastRelTime
// && JSON.stringify(nextProps.signalSpec) === JSON.stringify(this.props.signalSpec)
// ) {
// // do nothing, the data didn't *actually* change
// } else {
console.log('Inserting new data!');
this.setState({ data });
// }
}
if (this.segmentIsNew(nextProps.segment)) {
this.setState({ spec: this.getGraphSpec(nextProps) });
}
}
shouldComponentUpdate(nextProps, nextState) {
if (!this.view) {
return true;
}
if (this.state.spec !== nextState.spec) {
return true;
}
if (this.state.data !== nextState.data) {
this.insertData();
}
if (this.props.currentTime !== nextProps.currentTime) {
this.view.signal('videoTime', nextProps.currentTime);
}
if (this.segmentIsNew(nextProps.segment)) {
if (nextProps.segment.length > 0) {
// Set segmented domain
this.view.signal('segment', nextProps.segment);
} else {
// Reset segment to full domain
this.view.signal('segment', 0);
}
}
this.view.runAsync();
return false;
}
componentDidUpdate(oldProps, oldState) {
if (this.view) {
if (this.props.segment.length > 0) {
// Set segmented domain
this.view.signal('segment', this.props.segment);
} else {
// Reset segment to full domain
this.view.signal('segment', 0);
}
this.view.signal('videoTime', this.props.currentTime);
this.view.runAsync();
}
}
updateStyleFromDragPos({ left, top }) {
const plotInnerStyle = { ...this.state.plotInnerStyle };
plotInnerStyle.left = left;
plotInnerStyle.top = top;
this.setState({ plotInnerStyle });
}
onNewView(view) {
this.view = view;
if (this.state.bounds) {
this.onPlotResize();
}
if (this.props.segment.length > 0) {
view.signal('segment', this.props.segment);
}
view.signal('videoTime', this.props.currentTime);
this.insertData();
}
onSignalClickTime(signal, clickTime) {
// console.log('onSignalClickTime', signal, clickTime);
if (clickTime !== undefined) {
this.props.onRelativeTimeClick(this.props.messageId, clickTime);
}
}
onSignalSegment(signal, segment) {
// console.log('onSignalSegment', signal, segment);
if (!Array.isArray(segment)) {
return;
}
this.props.onSegmentChanged(this.props.messageId, segment);
if (!this.view) {
return;
}
this.view.runAfter(() => {
const state = this.view.getState();
state.subcontext[0].signals.brush = 0;
this.view.setState(state);
this.insertData();
});
}
plotInnerStyleFromMouseEvent(e) {
const { shiftX, shiftY } = this.state;
const plotInnerStyle = { ...DefaultPlotInnerStyle };
const rect = this.props.container.getBoundingClientRect();
const x = e.clientX - rect.left - shiftX;
const y = e.clientY - rect.top - shiftY;
plotInnerStyle.left = x;
plotInnerStyle.top = y;
return plotInnerStyle;
}
onDragAnchorMouseDown(e) {
e.persist();
const shiftX = e.clientX - e.target.getBoundingClientRect().left;
const shiftY = e.clientY - e.target.getBoundingClientRect().top;
this.setState({ shiftX, shiftY }, () => {
this.setState({ plotInnerStyle: this.plotInnerStyleFromMouseEvent(e) });
});
this.props.onDragStart(
this.props.messageId,
this.props.signalSpec.uid,
shiftX,
shiftY
);
}
onDragAnchorMouseUp(e) {
this.props.onDragEnd();
this.setState({
plotInnerStyle: null,
shiftX: 0,
shiftY: 0
});
}
onDragStart(e) {
e.preventDefault();
return false;
}
render() {
const { plotInnerStyle } = this.state;
const canReceiveDropClass = this.props.canReceiveGraphDrop
? 'is-droppable'
: null;
return (
<div
className="cabana-explorer-visuals-plot"
ref={this.props.onGraphRefAvailable}
>
<div
className={cx(
'cabana-explorer-visuals-plot-inner',
canReceiveDropClass
)}
style={plotInnerStyle || null}
>
<div
className="cabana-explorer-visuals-plot-draganchor"
onMouseDown={this.onDragAnchorMouseDown}
>
<span className="fa fa-bars" />
</div>
{this.props.plottedSignals.map(
({ messageId, signalUid, messageName }) => {
const signal = Object.values(
this.props.messages[messageId].frame.signals
).find((s) => s.uid === signalUid);
const colors = signal.getColors(messageId);
return (
<div
className="cabana-explorer-visuals-plot-header"
key={`${messageId}_${signal.uid}`}
>
<div className="cabana-explorer-visuals-plot-header-toggle">
<button
className="button--tiny"
onClick={() => this.props.unplot(messageId, signalUid)}
>
<span>Hide Plot</span>
</button>
</div>
<div className="cabana-explorer-visuals-plot-header-copy">
<div className="cabana-explorer-visuals-plot-message">
<span>
{messageName}
{' '}
{messageId}
</span>
</div>
<div className="cabana-explorer-visuals-plot-signal">
<div
className="cabana-explorer-visuals-plot-signal-color"
style={{ background: `rgb(${colors}` }}
/>
<strong>{signal.name}</strong>
</div>
</div>
</div>
);
}
)}
<Measure bounds onResize={this.onPlotResize}>
{({ measureRef }) => (
<div
ref={measureRef}
className="cabana-explorer-visuals-plot-container"
>
<Vega
onNewView={this.onNewView}
logLevel={1}
signalListeners={{
clickTime: this.onSignalClickTime,
segment: this.onSignalSegment
}}
renderer="canvas"
spec={this.state.spec}
actions={false}
data={{
table: this.state.data.series
}}
/>
</div>
)}
</Measure>
</div>
</div>
);
}
}