import React from 'react'; import { connect } from 'react-redux'; import * as selectors from '../selectors'; import hash from 'object-hash'; import copy from '../js/data/copy.json'; import { formatterWithYear, parseDate } from '../js/utilities'; import TimelineHeader from './presentational/TimelineHeader'; import TimelineAxis from './TimelineAxis.jsx'; import TimelineClip from './presentational/TimelineClip'; import TimelineHandles from './TimelineHandles.jsx'; import TimelineZoomControls from './TimelineZoomControls.jsx'; import TimelineLabels from './TimelineLabels.jsx'; import TimelineMarkers from './TimelineMarkers.jsx' import TimelineEvents from './TimelineEvents.jsx'; import TimelineCategories from './TimelineCategories.jsx'; class Timeline extends React.Component { constructor(props) { super(props); this.svgRef = React.createRef() this.state = { isFolded: false, dims: { height: 140, width: 0, width_controls: 100, height_controls: 115, margin_left: 120, margin_top: 20, trackHeight: 80 }, softTimeUpdate: 0, scaleX: null, scaleY: null, timerange: [null, null], dragPos0: null, transitionDuration: 300 }; } componentDidMount() { this.computeDims(); this.addEventListeners(); } componentWillReceiveProps(nextProps) { if (hash(nextProps) !== hash(this.props)) { console.log(nextProps.domain.categories) this.setState({ timerange: nextProps.app.timerange, scaleX: this.makeScaleX(), scaleY: this.makeScaleY(nextProps.domain.categories) }); } } addEventListeners() { window.addEventListener('resize', () => { this.computeDims(); }); } makeScaleX() { return d3.scaleTime() .domain(this.state.timerange) .range([this.state.dims.margin_left, this.state.dims.width - this.state.dims.width_controls]); } makeScaleY(categories) { const catsYpos = categories.map((g, i) => (i + 1) * this.state.dims.trackHeight / categories.length); return d3.scaleOrdinal() .domain(categories) .range(catsYpos); } componentDidUpdate(prevProps, prevState) { if (prevState.timerange !== this.state.timerange) { this.setState({ scaleX: this.makeScaleX() }); } } /** * Get x position of eventPoint, considering the time scale * @param {object} eventPoint: regular eventPoint data */ getEventX(eventPoint) { return this.state.scaleX(parseDate(eventPoint.timestamp)); } /** * Get y height of eventPoint, considering the ordinal Y scale * @param {object} eventPoint: regular eventPoint data */ getEventY(eventPoint) { console.log(eventPoint.category, this.state.scaleY(eventPoint.category)) return this.state.scaleY(eventPoint.category); } /** * Returns the time scale (x) extent in minutes */ getTimeScaleExtent() { const timeDomain = this.state.scaleX.domain(); return (timeDomain[1].getTime() - timeDomain[0].getTime()) / 60000; } onClickArrow() { this.setState((prevState, props) => { return {isFolded: !prevState.isFolded}; }); } computeDims() { const dom = this.props.ui.dom.timeline; if (document.querySelector(`#${dom}`) !== null) { const boundingClient = document.querySelector(`#${dom}`).getBoundingClientRect(); this.setState({ dims: Object.assign({}, this.state.dims, { width: boundingClient.width }) }); } } /** * Shift time range by moving forward or backwards * @param {String} direction: 'forward' / 'backwards' */ onMoveTime(direction) { this.props.methods.onSelect(); const extent = this.getTimeScaleExtent(); const newCentralTime = d3.timeMinute.offset(this.state.scaleX.domain()[0], extent / 2); // if forward let domain0 = newCentralTime; let domainF = d3.timeMinute.offset(newCentralTime, extent); // if backwards if (direction === 'backwards') { domain0 = d3.timeMinute.offset(newCentralTime, -extent); domainF = newCentralTime; } this.setState({ timerange: [domain0, domainF] }, () => { this.props.methods.onUpdateTimerange(this.state.timerange); }); } /** * Shift time range by moving forward or backwards * WITHOUT updating the store * @param {String} direction: 'forward' / 'backwards' */ onSoftTimeRangeUpdate(timerange) { this.setState({ timerange }); } /** * Apply zoom level to timeline * @param {object} zoom: zoom level from zoomLevels */ onApplyZoom(zoom) { const extent = this.getTimeScaleExtent(); const newCentralTime = d3.timeMinute.offset(this.state.scaleX.domain()[0], extent / 2); this.setState({ timerange: [ d3.timeMinute.offset(newCentralTime, -zoom.duration / 2), d3.timeMinute.offset(newCentralTime, zoom.duration / 2) ]}, () => { this.props.methods.onUpdateTimerange(this.state.timerange); }); } toggleTransition(isTransition) { this.setState({ transitionDuration: (isTransition) ? 300 : 0 }); } /* * Setup drag behavior */ onDragStart() { d3.event.sourceEvent.stopPropagation(); this.setState({ dragPos0: d3.event.x }, () => { this.toggleTransition(false); }); } /* * Drag and update */ onDrag() { const drag0 = this.state.scaleX.invert(this.state.dragPos0).getTime(); const dragNow = this.state.scaleX.invert(d3.event.x).getTime(); const timeShift = (drag0 - dragNow) / 1000; const newDomain0 = d3.timeSecond.offset(this.props.app.timerange[0], timeShift); const newDomainF = d3.timeSecond.offset(this.props.app.timerange[1], timeShift); // Updates components without updating timerange this.onSoftTimeRangeUpdate([newDomain0, newDomainF]); } /** * Stop dragging and update data */ onDragEnd() { this.toggleTransition(true); this.props.methods.onUpdateTimerange(this.state.timerange); } renderSVG() { const dims = this.state.dims; return ( ); } render() { const { isNarrative, app, ui } = this.props let classes = `timeline-wrapper ${(this.state.isFolded) ? ' folded' : ''}`; classes += (app.narrative !== null) ? ' narrative-mode' : ''; return (