diff --git a/src/components/Timeline.jsx b/src/components/Timeline.jsx index 78a313b..03ec64e 100644 --- a/src/components/Timeline.jsx +++ b/src/components/Timeline.jsx @@ -4,12 +4,12 @@ import * as selectors from '../selectors'; import hash from 'object-hash'; import copy from '../js/data/copy.json'; -import { formatterWithYear } from '../js/utilities'; +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 TimelineLogic from '../js/timeline/timeline.js'; import TimelineLabels from './TimelineLabels.jsx'; import TimelineMarkers from './TimelineMarkers.jsx' import TimelineEvents from './TimelineEvents.jsx'; @@ -26,10 +26,14 @@ class Timeline extends React.Component { width: 0, width_controls: 100, height_controls: 115, - margin_left: 120 + margin_left: 120, + margin_top: 20, + trackHeight: 80 }, softTimeUpdate: 0, - scaleY: null + scaleX: null, + scaleY: null, + timerange: [null, null] }; } @@ -37,23 +41,31 @@ class Timeline extends React.Component { this.methods = Object.assign({}, this.props.methods, { onSoftUpdate: (toggle) => { this.setState({ softTimeUpdate: toggle }) } }); - this.timeline = new TimelineLogic(this.svgRef.current, this.props.ui, this.methods); - this.timeline.update(this.props.domain.categories, this.props.app.timerange); + this.computeDims(); window.addEventListener('resize', () => { this.computeDims(); }); } componentWillReceiveProps(nextProps) { if (hash(nextProps) !== hash(this.props)) { - this.timeline.update(nextProps.domain.categories, nextProps.app.timerange); - let groupYs = Array.apply(null, Array(nextProps.domain.categories.length)); groupYs = groupYs.map((g, i) => (i + 1) * 80 / groupYs.length); - this.setState({ scaleY: d3.scaleOrdinal().domain(nextProps.domain.categories).range(groupYs) }); + this.setState({ + scaleX: d3.scaleTime().domain(nextProps.app.timerange).range([this.state.dims.margin_left, this.state.dims.width]), + scaleY: d3.scaleOrdinal().domain(nextProps.domain.categories).range(groupYs) + }); } } + /** + * 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 @@ -62,6 +74,13 @@ class Timeline extends React.Component { 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) => { @@ -78,18 +97,80 @@ class Timeline extends React.Component { } } - onMoveTime(dir) { - if (this.timeline) { - return this.timeline.moveTime(dir); + /** + * Shift time range by moving forward or backwards + * @param {String} direction: 'forward' / 'backwards' + */ + onMoveTime(direction) { + this.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; } - return null; + + this.state.scaleX.domain([domain0, domainF]); + this.methods.onUpdateTimerange(this.state.scaleX.domain()); } + + /** + * Apply zoom level to timeline + * @param {object} zoom: zoom level from zoomLevels + */ onApplyZoom(zoom) { - if (this.timeline) { - return this.timeline.applyZoom(zoom); - } - return null; + const extent = this.getTimeScaleExtent(); + const newCentralTime = d3.timeMinute.offset(this.state.scaleX.domain()[0], extent / 2); + + this.state.scaleX.domain([ + d3.timeMinute.offset(newCentralTime, -zoom.duration / 2), + d3.timeMinute.offset(newCentralTime, zoom.duration / 2) + ]); + + this.methods.onUpdateTimerange(this.state.scaleX.domain()); + } + + /* + * Setup drag behavior + */ + onDragStart(ev) { + d3.event.sourceEvent.stopPropagation(); + dragPos0 = d3.event.x; + this.toggleTransition(false); + } + + /* + * Drag and update + */ + onDrag() { + const drag0 = this.state.scaleX.invert(dragPos0).getTime(); + const dragNow = this.state.scaleX.invert(d3.event.x).getTime(); + const timeShift = (drag0 - dragNow) / 1000; + + const newDomain0 = d3.timeSecond.offset(timerange[0], timeShift); + const newDomainF = d3.timeSecond.offset(timerange[1], timeShift); + + this.state.scaleX.domain([newDomain0, newDomainF]); + //render(); + // Updates components without updating timerange + this.methods.onSoftUpdate(1); + } + + onDragEnd() { + toggleTransition(true); + this.setState({ + timerange: this.state.scaleX.domain() + }, () => { + this.methods.onSoftUpdate(0); + this.methods.onUpdateTimerange(scale.x.domain()); + }); } renderSVG() { @@ -102,8 +183,16 @@ class Timeline extends React.Component { height={dims.height} > + this.timeline.getEventX(e)} - getEventY={(e) => this.getEventY(e)/*this.timeline.getEventY(e)*/} + getEventX={(e) => this.getEventX(e)} + getEventY={(e) => this.getEventY(e)} /> this.timeline.getEventX(e)} - getEventY={(e) => this.getEventY(e)/*this.timeline.getEventY(e)*/} + getEventX={(e) => this.getEventX(e)} + getEventY={(e) => this.getEventY(e)} getCategoryColor={this.props.methods.getCategoryColor} onSelect={this.props.methods.onSelect} /> diff --git a/src/components/TimelineAxis.jsx b/src/components/TimelineAxis.jsx new file mode 100644 index 0000000..e41b2f1 --- /dev/null +++ b/src/components/TimelineAxis.jsx @@ -0,0 +1,70 @@ +import React from 'react'; + +class TimelineAxis extends React.Component { + + constructor() { + super(); + this.xAxis0Ref = React.createRef(); + this.xAxis1Ref = React.createRef(); + this.state = { + isInitialized: false, + transitionDuration: 300 + } + } + + + componentDidUpdate() { + if (this.props.scaleX) { + this.x0 = + d3.axisBottom(this.props.scaleX) + .ticks(10) + .tickPadding(5) + .tickSize(this.props.dims.trackHeight) + .tickFormat(d3.timeFormat('%d %b')); + + this.x1 = + d3.axisBottom(this.props.scaleX) + .ticks(10) + .tickPadding(this.props.dims.margin_top) + .tickSize(0) + .tickFormat(d3.timeFormat('%H:%M')); + + if (!this.state.isInitialized) this.setState({ isInitialized: true }); + } + + if (this.state.isInitialized) { + d3.select(this.xAxis0Ref.current) + .transition() + .duration(this.state.transitionDuration) + .call(this.x0); + + d3.select(this.xAxis1Ref.current) + .transition() + .duration(this.state.transitionDuration) + .call(this.x1); + } + } + + render () { + return ( + + + + + + + ); + } +} + +export default TimelineAxis; \ No newline at end of file diff --git a/src/components/TimelineCategories.jsx b/src/components/TimelineCategories.jsx index 9d2e625..241185f 100644 --- a/src/components/TimelineCategories.jsx +++ b/src/components/TimelineCategories.jsx @@ -13,9 +13,9 @@ class TimelineCategories extends React.Component { componentDidUpdate() { if (!this.state.isInitialized && this.props.timeline) { const drag = d3.drag() - .on('start', this.props.timeline.onDragStart) - .on('drag', this.props.timeline.onDrag) - .on('end', this.props.timeline.onDragEnd); + .on('start', this.props.onDragStart) + .on('drag', this.props.onDrag) + .on('end', this.props.onDragEnd); d3.select(this.grabRef.current) .call(drag); @@ -25,27 +25,33 @@ class TimelineCategories extends React.Component { } getY(idx) { - return (idx + 1) * 80 / this.props.categories.length + return (idx + 1) * this.props.dims.trackHeight / this.props.categories.length } renderCategory(category, idx) { return ( - - {category.category} + + {category.category} ) } render () { + const dims = this.props.dims; + return ( {this.props.categories.map((cat, idx) => this.renderCategory(cat, idx))} ); diff --git a/src/js/timeline/timeline.js b/src/js/timeline/timeline.js index 9e65864..b0e138e 100644 --- a/src/js/timeline/timeline.js +++ b/src/js/timeline/timeline.js @@ -34,48 +34,7 @@ export default function(svg, ui, methods) { .domain(timerange) .range([margin.left, WIDTH]); - /** - * Initilize SVG elements and groups - */ - const dom = {}; - dom.svg = d3.select(svg) - - /* - * Axis group elements - */ - dom.axis = {}; - - dom.axis.x0 = dom.svg.append('g') - .attr('transform', `translate(0, 25)`) - .attr('clip-path', 'url(#clip') - .attr('class', 'axis xAxis'); - - dom.axis.x1 = dom.svg.append('g') - .attr('transform', `translate(0, 105)`) - .attr('clip-path', 'url(#clip') - .attr('class', 'axis axisHourText'); - - /* - * Initialize axis function and element group - */ - const axis = {}; - - axis.x0 = - d3.axisBottom(scale.x) - .ticks(10) - .tickPadding(5) - .tickSize(HEIGHT) - .tickFormat(d3.timeFormat('%d %b')); - - axis.x1 = - d3.axisBottom(scale.x) - .ticks(10) - .tickPadding(margin.top) - .tickSize(0) - .tickFormat(d3.timeFormat('%H:%M')); - - updateAxis(); /** * Adapt dimensions when resizing @@ -118,46 +77,6 @@ export default function(svg, ui, methods) { return (scale.x.domain()[1].getTime() - scale.x.domain()[0].getTime()) / 60000; } - /** - * Apply zoom level to timeline - * @param {object} zoom: zoom level from zoomLevels - */ - function applyZoom(zoom) { - const extent = getTimeScaleExtent(); - const newCentralTime = d3.timeMinute.offset(scale.x.domain()[0], extent / 2); - - scale.x.domain([ - d3.timeMinute.offset(newCentralTime, -zoom.duration / 2), - d3.timeMinute.offset(newCentralTime, zoom.duration / 2) - ]); - - methods.onUpdateTimerange(scale.x.domain()); - } - - - /** - * Shift time range by moving forward or backwards - * @param {String} direction: 'forward' / 'backwards' - */ - function moveTime(direction) { - methods.onSelect(); - const extent = getTimeScaleExtent(); - const newCentralTime = d3.timeMinute.offset(scale.x.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; - } - - scale.x.domain([domain0, domainF]); - methods.onUpdateTimerange(scale.x.domain()); - } - function toggleTransition(isTransition) { transitionDuration = (isTransition) ? 500 : 0; } @@ -167,7 +86,6 @@ export default function(svg, ui, methods) { * Setup drag behavior */ function onDragStart(ev) { - console.log('ohoh') d3.event.sourceEvent.stopPropagation(); dragPos0 = d3.event.x; toggleTransition(false); @@ -198,22 +116,6 @@ export default function(svg, ui, methods) { } - /** - * Render axis on timeline and viewbox boundaries - */ - function renderAxis() { - dom.axis.x0 - .transition() - .duration(transitionDuration) - .call(axis.x0); - - dom.axis.x1 - .transition() - .duration(transitionDuration) - .call(axis.x1); - } - - /** * Updates displayable data on the timeline: events, selected and * potentially adjusts time range @@ -227,14 +129,8 @@ export default function(svg, ui, methods) { render(); } - function render() { - renderAxis(); - } - return { getEventX, - applyZoom, - moveTime, update, onDragStart, onDrag,