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 (
{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 (
{messageName} {' '} {messageId}
{signal.name}
); } )} {({ measureRef }) => (
)}
); } }