mirror of
https://github.com/bellingcat/ukraine-timemap.git
synced 2026-06-11 21:08:36 +03:00
refactor timeline config to redux state
This commit is contained in:
@@ -1,90 +1,90 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import * as selectors from '../selectors';
|
||||
import hash from 'object-hash';
|
||||
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/Timeline/Header';
|
||||
import TimelineAxis from './TimelineAxis.jsx';
|
||||
import TimelineClip from './presentational/Timeline/Clip';
|
||||
import TimelineHandles from './presentational/Timeline/Handles.js';
|
||||
import TimelineZoomControls from './presentational/Timeline/ZoomControls.js';
|
||||
import TimelineLabels from './presentational/Timeline/Labels.js';
|
||||
import TimelineMarkers from './presentational/Timeline/Markers.js'
|
||||
import TimelineEvents from './presentational/Timeline/Events.js';
|
||||
import TimelineCategories from './TimelineCategories.jsx';
|
||||
import copy from '../js/data/copy.json'
|
||||
import { formatterWithYear, parseDate } from '../js/utilities'
|
||||
import Header from './presentational/Timeline/Header'
|
||||
import Axis from './TimelineAxis.jsx'
|
||||
import Clip from './presentational/Timeline/Clip'
|
||||
import Handles from './presentational/Timeline/Handles.js'
|
||||
import ZoomControls from './presentational/Timeline/ZoomControls.js'
|
||||
import Labels from './presentational/Timeline/Labels.js'
|
||||
import Markers from './presentational/Timeline/Markers.js'
|
||||
import Events from './presentational/Timeline/Events.js'
|
||||
import Categories from './TimelineCategories.jsx'
|
||||
|
||||
class Timeline extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
super(props)
|
||||
this.styleDatetime = this.styleDatetime.bind(this)
|
||||
this.getDatetimeX = this.getDatetimeX.bind(this)
|
||||
this.onApplyZoom = this.onApplyZoom.bind(this)
|
||||
this.svgRef = React.createRef()
|
||||
this.state = {
|
||||
isFolded: false,
|
||||
dims: props.app.dims,
|
||||
dims: props.app.timeline.dimensions,
|
||||
scaleX: null,
|
||||
scaleY: null,
|
||||
timerange: [null, null],
|
||||
dragPos0: null,
|
||||
transitionDuration: 300
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.computeDims();
|
||||
this.addEventListeners();
|
||||
this.computeDims()
|
||||
this.addEventListeners()
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (hash(nextProps) !== hash(this.props)) {
|
||||
this.setState({
|
||||
timerange: nextProps.app.timerange,
|
||||
timerange: nextProps.app.timeline.range,
|
||||
scaleX: this.makeScaleX()
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
if (hash(nextProps.domain.categories) !== hash(this.props.domain.categories)) {
|
||||
this.setState({
|
||||
scaleY: this.makeScaleY(nextProps.domain.categories)
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
if (hash(nextProps.app.selected) !== hash(this.props.app.selected)) {
|
||||
if (!!nextProps.app.selected && nextProps.app.selected.length > 0) {
|
||||
this.onCenterTime(parseDate(nextProps.app.selected[0].timestamp));
|
||||
this.onCenterTime(parseDate(nextProps.app.selected[0].timestamp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addEventListeners() {
|
||||
window.addEventListener('resize', () => { this.computeDims(); });
|
||||
let element = document.querySelector('.timeline-wrapper');
|
||||
window.addEventListener('resize', () => { this.computeDims() })
|
||||
let element = document.querySelector('.timeline-wrapper')
|
||||
element.addEventListener("transitionend", (event) => {
|
||||
this.computeDims();
|
||||
});
|
||||
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]);
|
||||
.range([this.state.dims.margin_left, this.state.dims.width - this.state.dims.width_controls])
|
||||
}
|
||||
|
||||
makeScaleY(categories) {
|
||||
const tickHeight = 15;
|
||||
const catsYpos = categories.map((g, i) => (i + 1) * this.state.dims.trackHeight / categories.length + tickHeight / 2);
|
||||
const tickHeight = 15
|
||||
const catsYpos = categories.map((g, i) => (i + 1) * this.state.dims.trackHeight / categories.length + tickHeight / 2)
|
||||
return d3.scaleOrdinal()
|
||||
.domain(categories)
|
||||
.range(catsYpos);
|
||||
.range(catsYpos)
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
if (prevState.timerange !== this.state.timerange) {
|
||||
this.setState({ scaleX: this.makeScaleX() });
|
||||
this.setState({ scaleX: this.makeScaleX() })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,20 +93,20 @@ class Timeline extends React.Component {
|
||||
*/
|
||||
getTimeScaleExtent() {
|
||||
if (!this.state.scaleX) return 0
|
||||
const timeDomain = this.state.scaleX.domain();
|
||||
return (timeDomain[1].getTime() - timeDomain[0].getTime()) / 60000;
|
||||
const timeDomain = this.state.scaleX.domain()
|
||||
return (timeDomain[1].getTime() - timeDomain[0].getTime()) / 60000
|
||||
}
|
||||
|
||||
onClickArrow() {
|
||||
this.setState((prevState, props) => {
|
||||
return {isFolded: !prevState.isFolded};
|
||||
});
|
||||
return {isFolded: !prevState.isFolded}
|
||||
})
|
||||
}
|
||||
|
||||
computeDims() {
|
||||
const dom = this.props.ui.dom.timeline;
|
||||
const dom = this.props.ui.dom.timeline
|
||||
if (document.querySelector(`#${dom}`) !== null) {
|
||||
const boundingClient = document.querySelector(`#${dom}`).getBoundingClientRect();
|
||||
const boundingClient = document.querySelector(`#${dom}`).getBoundingClientRect()
|
||||
|
||||
this.setState({
|
||||
dims: {
|
||||
@@ -117,7 +117,7 @@ class Timeline extends React.Component {
|
||||
() => {
|
||||
this.setState({ scaleX: this.makeScaleX()
|
||||
})
|
||||
});
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,34 +126,34 @@ class Timeline extends React.Component {
|
||||
* @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);
|
||||
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);
|
||||
let domain0 = newCentralTime
|
||||
let domainF = d3.timeMinute.offset(newCentralTime, extent)
|
||||
|
||||
// if backwards
|
||||
if (direction === 'backwards') {
|
||||
domain0 = d3.timeMinute.offset(newCentralTime, -extent);
|
||||
domainF = newCentralTime;
|
||||
domain0 = d3.timeMinute.offset(newCentralTime, -extent)
|
||||
domainF = newCentralTime
|
||||
}
|
||||
|
||||
this.setState({ timerange: [domain0, domainF] }, () => {
|
||||
this.props.methods.onUpdateTimerange(this.state.timerange);
|
||||
});
|
||||
this.props.methods.onUpdateTimerange(this.state.timerange)
|
||||
})
|
||||
}
|
||||
|
||||
onCenterTime(newCentralTime) {
|
||||
const extent = this.getTimeScaleExtent();
|
||||
const extent = this.getTimeScaleExtent()
|
||||
|
||||
const domain0 = d3.timeMinute.offset(newCentralTime, -extent/2);
|
||||
const domainF = d3.timeMinute.offset(newCentralTime, +extent/2);
|
||||
const domain0 = d3.timeMinute.offset(newCentralTime, -extent/2)
|
||||
const domainF = d3.timeMinute.offset(newCentralTime, +extent/2)
|
||||
|
||||
this.setState({ timerange: [domain0, domainF] }, () => {
|
||||
this.props.methods.onUpdateTimerange(this.state.timerange);
|
||||
});
|
||||
this.props.methods.onUpdateTimerange(this.state.timerange)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -162,7 +162,7 @@ class Timeline extends React.Component {
|
||||
* Used for updates in the middle of a transition, for performance purposes
|
||||
*/
|
||||
onSoftTimeRangeUpdate(timerange) {
|
||||
this.setState({ timerange });
|
||||
this.setState({ timerange })
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -170,54 +170,55 @@ class Timeline extends React.Component {
|
||||
* @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);
|
||||
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);
|
||||
});
|
||||
this.props.methods.onUpdateTimerange(this.state.timerange)
|
||||
})
|
||||
}
|
||||
|
||||
toggleTransition(isTransition) {
|
||||
this.setState({ transitionDuration: (isTransition) ? 300 : 0 });
|
||||
this.setState({ transitionDuration: (isTransition) ? 300 : 0 })
|
||||
}
|
||||
|
||||
/*
|
||||
* Setup drag behavior
|
||||
*/
|
||||
onDragStart() {
|
||||
d3.event.sourceEvent.stopPropagation();
|
||||
d3.event.sourceEvent.stopPropagation()
|
||||
this.setState({
|
||||
dragPos0: d3.event.x
|
||||
}, () => {
|
||||
this.toggleTransition(false);
|
||||
});
|
||||
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 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);
|
||||
const { range } = this.props.app.timeline
|
||||
const newDomain0 = d3.timeSecond.offset(range[0], timeShift)
|
||||
const newDomainF = d3.timeSecond.offset(range[1], timeShift)
|
||||
|
||||
// Updates components without updating timerange
|
||||
this.onSoftTimeRangeUpdate([newDomain0, newDomainF]);
|
||||
this.onSoftTimeRangeUpdate([newDomain0, newDomainF])
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop dragging and update data
|
||||
*/
|
||||
onDragEnd() {
|
||||
this.toggleTransition(true);
|
||||
this.props.methods.onUpdateTimerange(this.state.timerange);
|
||||
this.toggleTransition(true)
|
||||
this.props.methods.onUpdateTimerange(this.state.timerange)
|
||||
}
|
||||
|
||||
getDatetimeX(dt) {
|
||||
@@ -243,17 +244,17 @@ class Timeline extends React.Component {
|
||||
|
||||
render() {
|
||||
const { isNarrative, app, ui } = this.props
|
||||
let classes = `timeline-wrapper ${(this.state.isFolded) ? ' folded' : ''}`;
|
||||
classes += (app.narrative !== null) ? ' narrative-mode' : '';
|
||||
const dims = this.state.dims;
|
||||
let classes = `timeline-wrapper ${(this.state.isFolded) ? ' folded' : ''}`
|
||||
classes += (app.narrative !== null) ? ' narrative-mode' : ''
|
||||
const { dims } = this.state
|
||||
|
||||
return (
|
||||
<div className={classes}>
|
||||
<TimelineHeader
|
||||
<Header
|
||||
title={copy[this.props.app.language].timeline.info}
|
||||
date0={formatterWithYear(this.state.timerange[0])}
|
||||
date1={formatterWithYear(this.state.timerange[1])}
|
||||
onClick={() => { this.onClickArrow(); }}
|
||||
onClick={() => { this.onClickArrow() }}
|
||||
hideInfo={isNarrative}
|
||||
/>
|
||||
<div className="timeline-content">
|
||||
@@ -263,43 +264,43 @@ class Timeline extends React.Component {
|
||||
width={dims.width}
|
||||
height={dims.height}
|
||||
>
|
||||
<TimelineClip
|
||||
<Clip
|
||||
dims={dims}
|
||||
/>
|
||||
<TimelineAxis
|
||||
<Axis
|
||||
dims={dims}
|
||||
timerange={this.props.app.timerange}
|
||||
transitionDuration={this.state.transitionDuration}
|
||||
scaleX={this.state.scaleX}
|
||||
/>
|
||||
<TimelineCategories
|
||||
<Categories
|
||||
dims={dims}
|
||||
onDragStart={() => { this.onDragStart() }}
|
||||
onDrag={() => { this.onDrag() }}
|
||||
onDragEnd={() => { this.onDragEnd() }}
|
||||
categories={this.props.domain.categories}
|
||||
/>
|
||||
<TimelineHandles
|
||||
<Handles
|
||||
dims={dims}
|
||||
onMoveTime={(dir) => { this.onMoveTime(dir) }}
|
||||
/>
|
||||
<TimelineZoomControls
|
||||
<ZoomControls
|
||||
extent={this.getTimeScaleExtent()}
|
||||
zoomLevels={this.props.app.zoomLevels}
|
||||
zoomLevels={this.props.app.timeline.zoomLevels}
|
||||
dims={dims}
|
||||
onApplyZoom={this.onApplyZoom}
|
||||
/>
|
||||
<TimelineLabels
|
||||
<Labels
|
||||
dims={dims}
|
||||
timelabels={this.state.timerange}
|
||||
/>
|
||||
<TimelineMarkers
|
||||
<Markers
|
||||
selected={this.props.app.selected}
|
||||
getEventX={this.getDatetimeX}
|
||||
getCategoryY={this.state.scaleY}
|
||||
transitionDuration={this.state.transitionDuration}
|
||||
/>
|
||||
<TimelineEvents
|
||||
<Events
|
||||
datetimes={this.props.domain.datetimes}
|
||||
styleDatetime={this.styleDatetime}
|
||||
narrative={this.props.app.narrative}
|
||||
@@ -313,7 +314,7 @@ class Timeline extends React.Component {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -326,11 +327,9 @@ function mapStateToProps(state) {
|
||||
narratives: state.domain.narratives
|
||||
},
|
||||
app: {
|
||||
timerange: selectors.getTimeRange(state),
|
||||
dims: state.app.timeline.dimensions,
|
||||
selected: state.app.selected,
|
||||
language: state.app.language,
|
||||
zoomLevels: state.app.timeline.zoomLevels,
|
||||
timeline: state.app.timeline,
|
||||
narrative: state.app.narrative
|
||||
},
|
||||
ui: {
|
||||
@@ -339,4 +338,4 @@ function mapStateToProps(state) {
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(Timeline);
|
||||
export default connect(mapStateToProps)(Timeline)
|
||||
|
||||
Reference in New Issue
Block a user