From 45512fd2952f50073a91f19f1458accd54896202 Mon Sep 17 00:00:00 2001 From: Franc Camps-Febrer Date: Fri, 21 Dec 2018 11:43:24 +0100 Subject: [PATCH 01/18] Make timeline controls plain svg --- src/components/Timeline.jsx | 51 ++++++++++++++++- src/components/TimelineHandles.jsx | 25 ++++++++ src/js/timeline/timeline.js | 91 ++++++++++++++---------------- 3 files changed, 117 insertions(+), 50 deletions(-) create mode 100644 src/components/TimelineHandles.jsx diff --git a/src/components/Timeline.jsx b/src/components/Timeline.jsx index 28aba21..f0dce23 100644 --- a/src/components/Timeline.jsx +++ b/src/components/Timeline.jsx @@ -6,18 +6,21 @@ import hash from 'object-hash'; import copy from '../js/data/copy.json'; import { formatterWithYear, isNotNullNorUndefined } from '../js/utilities'; import TimelineHeader from './presentational/TimelineHeader'; +import TimelineHandles from './TimelineHandles.jsx'; import TimelineLogic from '../js/timeline/timeline.js'; + class Timeline extends React.Component { constructor(props) { super(props); + this.svgRef = React.createRef() this.state = { isFolded: false }; } componentDidMount() { - this.timeline = new TimelineLogic(this.props.app, this.props.ui, this.props.methods); + this.timeline = new TimelineLogic(this.svgRef.current, this.props.app, this.props.ui, this.props.methods); this.timeline.update(this.props.domain, this.props.app); } @@ -33,6 +36,48 @@ class Timeline extends React.Component { }); } + getClientDims() { + const WIDTH_CONTROLS = 100; + const HEIGHT = 140; + let WIDTH = 0; + + if (document.querySelector(`#${this.props.ui.dom.timeline}`) !== null) { + const boundingClient = document.querySelector(`#${this.props.ui.dom.timeline}`).getBoundingClientRect(); + WIDTH = boundingClient.width - WIDTH_CONTROLS; + } + return { + height: HEIGHT, + width: WIDTH, + width_controls: WIDTH_CONTROLS, + height_controls: 115, + margin_left: 120 + } + } + + onMoveTime(dir) { + if (this.timeline) { + return this.timeline.moveTime(dir); + } + return ''; + } + + renderSVG() { + const { width, height, margin_left } = this.getClientDims(); + + return ( + + + + + { this.onMoveTime(dir) }} /> + + ); + } + render() { const { isNarrative, app, ui } = this.props let classes = `timeline-wrapper ${(this.state.isFolded) ? ' folded' : ''}`; @@ -47,7 +92,9 @@ class Timeline extends React.Component { hideInfo={isNarrative} />
-
+
+ {this.renderSVG()} +
); diff --git a/src/components/TimelineHandles.jsx b/src/components/TimelineHandles.jsx new file mode 100644 index 0000000..7e69432 --- /dev/null +++ b/src/components/TimelineHandles.jsx @@ -0,0 +1,25 @@ +import React from 'react'; + +class TimelineHandles extends React.Component { + + render() { + const dims = this.props.dims; + return ( + + this.props.onMoveTime('backwards')}> + + + + + this.props.onMoveTime('forward')}> + + + + + + ) + } + +} + +export default TimelineHandles; \ No newline at end of file diff --git a/src/js/timeline/timeline.js b/src/js/timeline/timeline.js index 042ea21..d362782 100644 --- a/src/js/timeline/timeline.js +++ b/src/js/timeline/timeline.js @@ -13,7 +13,7 @@ import hash from 'object-hash'; import esLocale from '../data/es-MX.json'; import copy from '../data/copy.json'; -export default function(newApp, ui, methods) { +export default function(svg, newApp, ui, methods) { d3.timeFormatDefaultLocale(esLocale); const domain = { @@ -60,27 +60,27 @@ export default function(newApp, ui, methods) { * Initilize SVG elements and groups */ const dom = {}; +console.log(svg) + dom.svg = d3.select(svg) + //d3.select(`#${ui.dom.timeline}`) + //.append('svg') + //.attr('width', WIDTH) + //.attr('height', HEIGHT); - dom.svg = - d3.select(`#${ui.dom.timeline}`) - .append('svg') - .attr('width', WIDTH) - .attr('height', HEIGHT); +// dom.clip = dom.svg.append("svg:clipPath") +// .attr("id", "clip") +// .append("svg:rect") +// .attr("x", margin.left) +// .attr("y", 0) +// .attr("width", WIDTH - margin.left) +// .attr("height", HEIGHT - 25); - dom.clip = dom.svg.append("svg:clipPath") - .attr("id", "clip") - .append("svg:rect") - .attr("x", margin.left) - .attr("y", 0) - .attr("width", WIDTH - margin.left) - .attr("height", HEIGHT - 25); - - dom.controls = - d3.select(`#${ui.dom.timeline}`) - .append('svg') - .attr('class', 'time-controls') - .attr('width', WIDTH_CONTROLS) - .attr('height', HEIGHT); + dom.controls = d3.select(svg) + //d3.select(`#${ui.dom.timeline}`) + //.append('svg') + //.attr('class', 'time-controls') + //.attr('width', WIDTH_CONTROLS) + //.attr('height', HEIGHT); /* @@ -124,13 +124,13 @@ export default function(newApp, ui, methods) { /* * Time Controls */ - dom.forward = dom.svg.append('g').attr('class', 'time-controls-inline'); - dom.forward.append('circle'); - dom.forward.append('path'); - - dom.backwards = dom.svg.append('g').attr('class', 'time-controls-inline'); - dom.backwards.append('circle'); - dom.backwards.append('path'); +// dom.forward = dom.svg.append('g').attr('class', 'time-controls-inline'); +// dom.forward.append('circle'); +// dom.forward.append('path'); +// +// dom.backwards = dom.svg.append('g').attr('class', 'time-controls-inline'); +// dom.backwards.append('circle'); +// dom.backwards.append('path'); dom.zooms = dom.controls.append('g'); @@ -491,21 +491,21 @@ export default function(newApp, ui, methods) { }); // These controls on timeline svg - dom.backwards.select('circle') - .attr('transform', `translate(${scale.x.range()[0] + 20}, 62)`) - .attr('r', 15); - - dom.backwards.select('path') - .attr('d', d3.symbol().type(d3.symbolTriangle).size(80)) - .attr('transform', `translate(${scale.x.range()[0] + 20}, 62)rotate(270)`); - - dom.forward.select('circle') - .attr('transform', `translate(${scale.x.range()[1] - 20}, 62)`) - .attr('r', 15); - - dom.forward.select('path') - .attr('d', d3.symbol().type(d3.symbolTriangle).size(80)) - .attr('transform', `translate(${scale.x.range()[1] - 20}, 62)rotate(90)`); +// dom.backwards.select('circle') +// .attr('transform', `translate(${scale.x.range()[0] + 20}, 62)`) +// .attr('r', 15); +// +// dom.backwards.select('path') +// .attr('d', d3.symbol().type(d3.symbolTriangle).size(80)) +// .attr('transform', `translate(${scale.x.range()[0] + 20}, 62)rotate(270)`); +// +// dom.forward.select('circle') +// .attr('transform', `translate(${scale.x.range()[1] - 20}, 62)`) +// .attr('r', 15); +// +// dom.forward.select('path') +// .attr('d', d3.symbol().type(d3.symbolTriangle).size(80)) +// .attr('transform', `translate(${scale.x.range()[1] - 20}, 62)rotate(90)`); dom.zooms.selectAll('text') .text(d => d.label) @@ -513,12 +513,6 @@ export default function(newApp, ui, methods) { .attr('y', (d, i) => (i * 15) + 20) .classed('active', level => level.active); - dom.forward - .on('click', () => moveTime('forward')); - - dom.backwards - .on('click', () => moveTime('backwards')); - dom.zooms.selectAll('text') .on('click', zoom => applyZoom(zoom)); } @@ -589,6 +583,7 @@ export default function(newApp, ui, methods) { } return { + moveTime, update, }; } From 323b31b3d7d92c501b7afd04fd2d70bff03a0c91 Mon Sep 17 00:00:00 2001 From: Franc Camps-Febrer Date: Fri, 21 Dec 2018 12:05:31 +0100 Subject: [PATCH 02/18] Make timeline zoom controls reactified --- src/components/Timeline.jsx | 21 +++++++--- src/components/TimelineHandles.jsx | 2 +- src/components/TimelineZoomControls.jsx | 29 +++++++++++++ src/js/timeline/timeline.js | 55 +++++++++++++------------ 4 files changed, 73 insertions(+), 34 deletions(-) create mode 100644 src/components/TimelineZoomControls.jsx diff --git a/src/components/Timeline.jsx b/src/components/Timeline.jsx index f0dce23..c719778 100644 --- a/src/components/Timeline.jsx +++ b/src/components/Timeline.jsx @@ -7,6 +7,7 @@ import copy from '../js/data/copy.json'; import { formatterWithYear, isNotNullNorUndefined } from '../js/utilities'; import TimelineHeader from './presentational/TimelineHeader'; import TimelineHandles from './TimelineHandles.jsx'; +import TimelineZoomControls from './TimelineZoomControls.jsx'; import TimelineLogic from '../js/timeline/timeline.js'; @@ -43,7 +44,7 @@ class Timeline extends React.Component { if (document.querySelector(`#${this.props.ui.dom.timeline}`) !== null) { const boundingClient = document.querySelector(`#${this.props.ui.dom.timeline}`).getBoundingClientRect(); - WIDTH = boundingClient.width - WIDTH_CONTROLS; + WIDTH = boundingClient.width; } return { height: HEIGHT, @@ -61,19 +62,27 @@ class Timeline extends React.Component { return ''; } + onApplyZoom(zoom) { + if (this.timeline) { + return this.timeline.applyZoom(zoom); + } + return ''; + } + renderSVG() { - const { width, height, margin_left } = this.getClientDims(); + const dims = this.getClientDims(); return ( - + - { this.onMoveTime(dir) }} /> + { this.onMoveTime(dir) }} /> + { this.onApplyZoom(zoom); }} /> ); } diff --git a/src/components/TimelineHandles.jsx b/src/components/TimelineHandles.jsx index 7e69432..1e8b5df 100644 --- a/src/components/TimelineHandles.jsx +++ b/src/components/TimelineHandles.jsx @@ -11,7 +11,7 @@ class TimelineHandles extends React.Component { - this.props.onMoveTime('forward')}> + this.props.onMoveTime('forward')}> diff --git a/src/components/TimelineZoomControls.jsx b/src/components/TimelineZoomControls.jsx new file mode 100644 index 0000000..51ce7a7 --- /dev/null +++ b/src/components/TimelineZoomControls.jsx @@ -0,0 +1,29 @@ +import React from 'react'; + +class TimelineZoomControls extends React.Component { + + renderZoom(zoom, idx) { + return ( + this.props.onApplyZoom(zoom)} + > + {zoom.label} + + ) + } + + render() { + const dims = this.props.dims; + + return ( + + {this.props.zoomLevels.map((z, idx) => this.renderZoom(z, idx))} + + ); + } +} + +export default TimelineZoomControls; \ No newline at end of file diff --git a/src/js/timeline/timeline.js b/src/js/timeline/timeline.js index d362782..64ed9ba 100644 --- a/src/js/timeline/timeline.js +++ b/src/js/timeline/timeline.js @@ -132,12 +132,12 @@ console.log(svg) // dom.backwards.append('circle'); // dom.backwards.append('path'); - dom.zooms = dom.controls.append('g'); - - dom.zooms.selectAll('.zoom-level-button') - .data(app.zoomLevels) - .enter().append('text') - .attr('class', 'zoom-level-button'); +// dom.zooms = dom.controls.append('g'); +// +// dom.zooms.selectAll('.zoom-level-button') +// .data(app.zoomLevels) +// .enter().append('text') +// .attr('class', 'zoom-level-button'); /* @@ -251,18 +251,18 @@ console.log(svg) /* * TODO: Highlight zoom level based on time range selected */ - function highlightZoomLevel(zoom) { - app.zoomLevels.forEach((level) => { - if (level.label === zoom.label) { - level.active = true; - } else { - level.active = false; - } - }); - - dom.zooms.selectAll('text') - .classed('active', level => level.active); - } +// function highlightZoomLevel(zoom) { +// app.zoomLevels.forEach((level) => { +// if (level.label === zoom.label) { +// level.active = true; +// } else { +// level.active = false; +// } +// }); +// +// dom.zooms.selectAll('text') +// .classed('active', level => level.active); +// } /** @@ -270,7 +270,7 @@ console.log(svg) * @param {object} zoom: zoom level from zoomLevels */ function applyZoom(zoom) { - highlightZoomLevel(zoom); +// highlightZoomLevel(zoom); const extent = getTimeScaleExtent(); const newCentralTime = d3.timeMinute.offset(scale.x.domain()[0], extent / 2); @@ -507,14 +507,14 @@ console.log(svg) // .attr('d', d3.symbol().type(d3.symbolTriangle).size(80)) // .attr('transform', `translate(${scale.x.range()[1] - 20}, 62)rotate(90)`); - dom.zooms.selectAll('text') - .text(d => d.label) - .attr('x', 60) - .attr('y', (d, i) => (i * 15) + 20) - .classed('active', level => level.active); - - dom.zooms.selectAll('text') - .on('click', zoom => applyZoom(zoom)); +// dom.zooms.selectAll('text') +// .text(d => d.label) +// .attr('x', 60) +// .attr('y', (d, i) => (i * 15) + 20) +// .classed('active', level => level.active); +// +// dom.zooms.selectAll('text') +// .on('click', zoom => applyZoom(zoom)); } @@ -583,6 +583,7 @@ console.log(svg) } return { + applyZoom, moveTime, update, }; From f4fee8ab07c0a57f09c52cb82f1cd9e3fb5ae950 Mon Sep 17 00:00:00 2001 From: Franc Camps-Febrer Date: Fri, 21 Dec 2018 12:36:06 +0100 Subject: [PATCH 03/18] Reactify timeline labels --- src/components/Map.jsx | 3 +- src/components/Timeline.jsx | 34 +++---- src/components/TimelineLabels.jsx | 21 ++++ src/js/timeline/timeline.js | 153 +----------------------------- 4 files changed, 42 insertions(+), 169 deletions(-) create mode 100644 src/components/TimelineLabels.jsx diff --git a/src/components/Map.jsx b/src/components/Map.jsx index 6f93158..7ced231 100644 --- a/src/components/Map.jsx +++ b/src/components/Map.jsx @@ -112,7 +112,6 @@ class Map extends React.Component { } renderSVG() { - if (this.map === null) return ''; const pane = this.map.getPanes().overlayPane; const { width, height } = this.getClientDims(); @@ -203,7 +202,7 @@ class Map extends React.Component { return (
- {this.renderSVG()} + {(this.map !== null) ? this.renderSVG() : ''} {(this.map !== null) ? this.renderMarkers() : ''} {(this.map !== null) ? this.renderSites() : ''} {(this.map !== null) ? this.renderEvents() : ''} diff --git a/src/components/Timeline.jsx b/src/components/Timeline.jsx index c719778..015ee6e 100644 --- a/src/components/Timeline.jsx +++ b/src/components/Timeline.jsx @@ -9,20 +9,29 @@ import TimelineHeader from './presentational/TimelineHeader'; import TimelineHandles from './TimelineHandles.jsx'; import TimelineZoomControls from './TimelineZoomControls.jsx'; import TimelineLogic from '../js/timeline/timeline.js'; - +import TimelineLabels from './TimelineLabels.jsx'; class Timeline extends React.Component { constructor(props) { super(props); this.svgRef = React.createRef() this.state = { - isFolded: false + isFolded: false, + dims: { + height: 140, + width: 0, + width_controls: 100, + height_controls: 115, + margin_left: 120 + } }; } componentDidMount() { this.timeline = new TimelineLogic(this.svgRef.current, this.props.app, this.props.ui, this.props.methods); this.timeline.update(this.props.domain, this.props.app); + this.computeDims(); + window.addEventListener('resize', () => { this.computeDims(); }); } componentWillReceiveProps(nextProps) { @@ -37,21 +46,11 @@ class Timeline extends React.Component { }); } - getClientDims() { - const WIDTH_CONTROLS = 100; - const HEIGHT = 140; - let WIDTH = 0; - + computeDims() { if (document.querySelector(`#${this.props.ui.dom.timeline}`) !== null) { const boundingClient = document.querySelector(`#${this.props.ui.dom.timeline}`).getBoundingClientRect(); - WIDTH = boundingClient.width; - } - return { - height: HEIGHT, - width: WIDTH, - width_controls: WIDTH_CONTROLS, - height_controls: 115, - margin_left: 120 + const WIDTH = boundingClient.width; + this.setState({ dims: Object.assign({}, this.state.dims, { width: WIDTH }) }); } } @@ -70,8 +69,8 @@ class Timeline extends React.Component { } renderSVG() { - const dims = this.getClientDims(); - + const dims = this.state.dims; + return ( { this.onMoveTime(dir) }} /> { this.onApplyZoom(zoom); }} /> + ); } diff --git a/src/components/TimelineLabels.jsx b/src/components/TimelineLabels.jsx new file mode 100644 index 0000000..7390acf --- /dev/null +++ b/src/components/TimelineLabels.jsx @@ -0,0 +1,21 @@ +import React from 'react'; + +import { formatterWithYear } from '../js/utilities'; + +const TimelineLabels = ({ dims, timelabels }) => { + + return ( + + + + + {formatterWithYear(timelabels[0])} + + + {formatterWithYear(timelabels[1])} + + + ) +} + +export default TimelineLabels; \ No newline at end of file diff --git a/src/js/timeline/timeline.js b/src/js/timeline/timeline.js index 64ed9ba..91c8709 100644 --- a/src/js/timeline/timeline.js +++ b/src/js/timeline/timeline.js @@ -25,7 +25,6 @@ export default function(svg, newApp, ui, methods) { timerange: newApp.timerange, selected: [], language: newApp.language, - zoomLevels: newApp.zoomLevels } // Dimension of the client @@ -60,28 +59,8 @@ export default function(svg, newApp, ui, methods) { * Initilize SVG elements and groups */ const dom = {}; -console.log(svg) + dom.svg = d3.select(svg) - //d3.select(`#${ui.dom.timeline}`) - //.append('svg') - //.attr('width', WIDTH) - //.attr('height', HEIGHT); - -// dom.clip = dom.svg.append("svg:clipPath") -// .attr("id", "clip") -// .append("svg:rect") -// .attr("x", margin.left) -// .attr("y", 0) -// .attr("width", WIDTH - margin.left) -// .attr("height", HEIGHT - 25); - - dom.controls = d3.select(svg) - //d3.select(`#${ui.dom.timeline}`) - //.append('svg') - //.attr('class', 'time-controls') - //.attr('width', WIDTH_CONTROLS) - //.attr('height', HEIGHT); - /* * Axis group elements @@ -100,18 +79,6 @@ console.log(svg) .attr('transform', `translate(${WIDTH}, 0)`) .attr('class', 'yAxis'); - dom.axis.boundaries = dom.svg.selectAll('.axisBoundaries') - .data([0, 1]) - .enter().append('line') - .attr('class', 'axisBoundaries'); - - dom.axis.label0 = dom.svg.append('text') - .attr('class', 'timeLabel0 timeLabel'); - - dom.axis.label1 = dom.svg.append('text') - .attr('class', 'timelabelF timeLabel'); - - /* * Plottable elements */ @@ -121,25 +88,6 @@ console.log(svg) dom.markers = dom.body.append('g'); - /* - * Time Controls - */ -// dom.forward = dom.svg.append('g').attr('class', 'time-controls-inline'); -// dom.forward.append('circle'); -// dom.forward.append('path'); -// -// dom.backwards = dom.svg.append('g').attr('class', 'time-controls-inline'); -// dom.backwards.append('circle'); -// dom.backwards.append('path'); - -// dom.zooms = dom.controls.append('g'); -// -// dom.zooms.selectAll('.zoom-level-button') -// .data(app.zoomLevels) -// .enter().append('text') -// .attr('class', 'zoom-level-button'); - - /* * Initialize axis function and element group */ @@ -248,30 +196,11 @@ console.log(svg) } - /* - * TODO: Highlight zoom level based on time range selected - */ -// function highlightZoomLevel(zoom) { -// app.zoomLevels.forEach((level) => { -// if (level.label === zoom.label) { -// level.active = true; -// } else { -// level.active = false; -// } -// }); -// -// dom.zooms.selectAll('text') -// .classed('active', level => level.active); -// } - - /** * Apply zoom level to timeline * @param {object} zoom: zoom level from zoomLevels */ function applyZoom(zoom) { -// highlightZoomLevel(zoom); - const extent = getTimeScaleExtent(); const newCentralTime = d3.timeMinute.offset(scale.x.domain()[0], extent / 2); @@ -367,24 +296,6 @@ console.log(svg) axis.x1.scale(scale.x); } - - /** - * Display the current time range in the time label above the timeline - */ - function renderTimeLabels() { - dom.axis.label0 - .attr('x', 5) - .attr('y', 15) - .text(formatterWithYear(app.timerange[0])); - - dom.axis.label1 - .attr('x', WIDTH - 5) - .attr('y', 15) - .text(formatterWithYear(app.timerange[1])) - .style('text-anchor', 'end'); - } - - /** * Makes a circular ring mark in all selected events * @param {object} eventPoint: object with eventPoint data (time, loc, tags) @@ -469,55 +380,8 @@ console.log(svg) dom.axis.y .call(axis.y) .call(drag); - - dom.axis.boundaries - .attr('x1', (d, i) => scale.x.range()[i]) - .attr('x2', (d, i) => scale.x.range()[i]) - .attr('y1', 10) - .attr('y2', 20); - - dom.axis.label1 - .attr('x', scale.x.range()[1] - 5); } - - /** - * Render left and right time shifting controls - */ - function renderTimeControls() { - const zoomLabels = copy[app.language].timeline.zooms; - app.zoomLevels.forEach((level, i) => { - level.label = zoomLabels[i]; - }); - - // These controls on timeline svg -// dom.backwards.select('circle') -// .attr('transform', `translate(${scale.x.range()[0] + 20}, 62)`) -// .attr('r', 15); -// -// dom.backwards.select('path') -// .attr('d', d3.symbol().type(d3.symbolTriangle).size(80)) -// .attr('transform', `translate(${scale.x.range()[0] + 20}, 62)rotate(270)`); -// -// dom.forward.select('circle') -// .attr('transform', `translate(${scale.x.range()[1] - 20}, 62)`) -// .attr('r', 15); -// -// dom.forward.select('path') -// .attr('d', d3.symbol().type(d3.symbolTriangle).size(80)) -// .attr('transform', `translate(${scale.x.range()[1] - 20}, 62)rotate(90)`); - -// dom.zooms.selectAll('text') -// .text(d => d.label) -// .attr('x', 60) -// .attr('y', (d, i) => (i * 15) + 20) -// .classed('active', level => level.active); -// -// dom.zooms.selectAll('text') -// .on('click', zoom => applyZoom(zoom)); - } - - /** * Updates data displayed by this timeline, but only render if necessary * @param {Object} domain: Redux state domain subtree @@ -561,27 +425,16 @@ console.log(svg) app.selected = newApp.selected.slice(0); } - if (isNewDomain || isNewAppProps) renderContent(); - if (isNewAppProps) renderContext(); + if (isNewDomain || isNewAppProps) render(); } - function renderContext() { - renderTimeControls(); - renderTimeLabels(); - } - - function renderContent() { + function render() { updateAxis(); renderAxis(); renderEvents(); renderHighlight(); } - function render() { - renderContext(); - renderContent(); - } - return { applyZoom, moveTime, From 345a1f2be29b355d078b06e1ef026b791669a4f9 Mon Sep 17 00:00:00 2001 From: Franc Camps-Febrer Date: Sat, 22 Dec 2018 10:43:12 +0100 Subject: [PATCH 04/18] Add reactified event markers to timeline --- src/components/Timeline.jsx | 2 + src/components/TimelineMarkers.jsx | 27 ++++++++++ src/js/timeline/timeline.js | 83 +++--------------------------- 3 files changed, 37 insertions(+), 75 deletions(-) create mode 100644 src/components/TimelineMarkers.jsx diff --git a/src/components/Timeline.jsx b/src/components/Timeline.jsx index 015ee6e..fb8e713 100644 --- a/src/components/Timeline.jsx +++ b/src/components/Timeline.jsx @@ -10,6 +10,7 @@ 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' class Timeline extends React.Component { constructor(props) { @@ -83,6 +84,7 @@ class Timeline extends React.Component { { this.onMoveTime(dir) }} /> { this.onApplyZoom(zoom); }} /> + this.timeline.getEventX(e)} getEventY={(e) => this.timeline.getEventY(e)} /> ); } diff --git a/src/components/TimelineMarkers.jsx b/src/components/TimelineMarkers.jsx new file mode 100644 index 0000000..54de8f1 --- /dev/null +++ b/src/components/TimelineMarkers.jsx @@ -0,0 +1,27 @@ +import React from 'react'; + +class TimelineMarkers extends React.Component { + + renderMarker(event) { + return ( + + + ) + } + + render () { + return ( + + {this.props.selected.map(event => this.renderMarker(event))} + + ); + } +} + +export default TimelineMarkers; \ No newline at end of file diff --git a/src/js/timeline/timeline.js b/src/js/timeline/timeline.js index 91c8709..bd317e8 100644 --- a/src/js/timeline/timeline.js +++ b/src/js/timeline/timeline.js @@ -4,14 +4,9 @@ Allows brushing and selecting periods of time in it TODO: is it possible to express this idiomatically as React? */ -import { - areEqual, - parseDate, - formatterWithYear -} from '../utilities'; +import { parseDate } from '../utilities'; import hash from 'object-hash'; import esLocale from '../data/es-MX.json'; -import copy from '../data/copy.json'; export default function(svg, newApp, ui, methods) { d3.timeFormatDefaultLocale(esLocale); @@ -23,18 +18,12 @@ export default function(svg, newApp, ui, methods) { } const app = { timerange: newApp.timerange, - selected: [], - language: newApp.language, + selected: [] } // Dimension of the client const WIDTH_CONTROLS = 100; - const HEIGHT = 140; - const boundingClient = d3.select(`#${ui.dom.timeline}`).node().getBoundingClientRect(); - let WIDTH = boundingClient.width - WIDTH_CONTROLS; - - // Highlight events with a larger white ring marker - const markerRadius = 15; + let WIDTH = getCurrentWidth() - WIDTH_CONTROLS; // NB: is it possible to do this with SCSS? // A: Maybe, although we are using it programmatically here for now @@ -85,7 +74,6 @@ export default function(svg, newApp, ui, methods) { dom.body = dom.svg.append("g").attr("clip-path", "url(#clip)"); dom.events = dom.body.append('g'); - dom.markers = dom.body.append('g'); /* @@ -131,7 +119,6 @@ export default function(svg, newApp, ui, methods) { if (d3.select(`#${ui.dom.timeline}`).node() !== null) { WIDTH = getCurrentWidth() - WIDTH_CONTROLS; - dom.svg.attr('width', WIDTH); scale.x.range([margin.left, WIDTH]); axis.y.tickSize(WIDTH - margin.left); dom.axis.y.attr('transform', `translate(${WIDTH}, 0)`) @@ -140,16 +127,7 @@ export default function(svg, newApp, ui, methods) { }); } addResizeListener(); - - - /** - * Return which color event circle should be based on incident type - * @param {object} eventPoint data object - */ - function getEventPointFillColor(eventPoint) { - return methods.getCategoryColor(eventPoint.category); - } - + /** * Given an event, get all the filtered events that happen simultaneously @@ -185,17 +163,6 @@ export default function(svg, newApp, ui, methods) { return (scale.x.domain()[1].getTime() - scale.x.domain()[0].getTime()) / 60000; } - - /* - * Given a number of minutes, calculate the width based on current scale.x - * @param {number} minutes: number of minutes - */ - function getWidthOfTime(minutes) { - const allMins = getTimeScaleExtent(); - return (minutes * WIDTH) / allMins; - } - - /** * Apply zoom level to timeline * @param {object} zoom: zoom level from zoomLevels @@ -286,39 +253,6 @@ export default function(svg, newApp, ui, methods) { .classed('mouseover', false); } - - /** - * It automatically sets brush timeline to a domain set by the params - */ - function updateTimeRange() { - scale.x.domain(app.timerange); - axis.x0.scale(scale.x); - axis.x1.scale(scale.x); - } - - /** - * Makes a circular ring mark in all selected events - * @param {object} eventPoint: object with eventPoint data (time, loc, tags) - */ - function renderHighlight() { - const markers = dom.markers - .selectAll('circle') - .data(app.selected); - - markers - .enter() - .append('circle') - .attr('class', 'timeline-marker') - .merge(markers) - .attr('cy', eventPoint => getEventY(eventPoint)) - .attr('cx', eventPoint => getEventX(eventPoint)) - .attr('r', 10) - .style('opacity', .9); - - markers.exit().remove(); - } - - /** * Return event circles of different groups */ @@ -342,10 +276,8 @@ export default function(svg, newApp, ui, methods) { .attr('class', 'event') .attr('cx', eventPoint => getEventX(eventPoint)) .attr('cy', eventPoint => getEventY(eventPoint)) - .style('fill', eventPoint => getEventPointFillColor(eventPoint)) - .on('click', eventPoint => { - return methods.onSelect(getAllEventsAtOnce(eventPoint)) - }) + .style('fill', eventPoint => methods.getCategoryColor(eventPoint.category)) + .on('click', eventPoint => methods.onSelect(getAllEventsAtOnce(eventPoint))) .on('mouseover', handleMouseOver) .on('mouseout', handleMouseOut) .transition() @@ -432,10 +364,11 @@ export default function(svg, newApp, ui, methods) { updateAxis(); renderAxis(); renderEvents(); - renderHighlight(); } return { + getEventX, + getEventY, applyZoom, moveTime, update, From 5222fc24813a4a12e857e620be019c83771bd47f Mon Sep 17 00:00:00 2001 From: Franc Camps-Febrer Date: Thu, 27 Dec 2018 17:40:17 +0100 Subject: [PATCH 05/18] Center timeline --- src/js/timeline/timeline.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/js/timeline/timeline.js b/src/js/timeline/timeline.js index bd317e8..c475d1f 100644 --- a/src/js/timeline/timeline.js +++ b/src/js/timeline/timeline.js @@ -323,7 +323,9 @@ export default function(svg, newApp, ui, methods) { const groupStep = (106 - 30) / domain.categories.length; let groupYs = Array.apply(null, Array(domain.categories.length)); groupYs = groupYs.map((g, i) => { - return 30 + i * groupStep; + //return 30 + i * groupStep; + const h = 106 - 30 + return (i + 1) * h / groupYs.length - 15; }); scale.y = d3.scaleOrdinal() From 870d9158a5e2a270d2a72e35e91a63e7d58e2fad Mon Sep 17 00:00:00 2001 From: Franc Camps-Febrer Date: Wed, 2 Jan 2019 12:00:14 +0100 Subject: [PATCH 06/18] Reactify events and categories --- src/components/Timeline.jsx | 14 +++- src/components/TimelineCategories.jsx | 31 +++++++ src/components/TimelineEvents.jsx | 41 ++++++++++ src/js/timeline/timeline.js | 111 ++++---------------------- 4 files changed, 98 insertions(+), 99 deletions(-) create mode 100644 src/components/TimelineCategories.jsx create mode 100644 src/components/TimelineEvents.jsx diff --git a/src/components/Timeline.jsx b/src/components/Timeline.jsx index fb8e713..eacec4c 100644 --- a/src/components/Timeline.jsx +++ b/src/components/Timeline.jsx @@ -11,6 +11,8 @@ 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'; +import TimelineCategories from './TimelineCategories.jsx'; class Timeline extends React.Component { constructor(props) { @@ -59,14 +61,14 @@ class Timeline extends React.Component { if (this.timeline) { return this.timeline.moveTime(dir); } - return ''; + return null; } onApplyZoom(zoom) { if (this.timeline) { return this.timeline.applyZoom(zoom); } - return ''; + return null; } renderSVG() { @@ -84,7 +86,15 @@ class Timeline extends React.Component { { this.onMoveTime(dir) }} /> { this.onApplyZoom(zoom); }} /> + this.timeline.getEventX(e)} getEventY={(e) => this.timeline.getEventY(e)} /> + this.timeline.getEventX(e)} + getEventY={(e) => this.timeline.getEventY(e)} + getCategoryColor={this.props.methods.getCategoryColor} + onSelect={this.props.methods.onSelect} + /> ); } diff --git a/src/components/TimelineCategories.jsx b/src/components/TimelineCategories.jsx new file mode 100644 index 0000000..fc84ed9 --- /dev/null +++ b/src/components/TimelineCategories.jsx @@ -0,0 +1,31 @@ +import React from 'react'; + +class TimelineCategories extends React.Component { + + getY(idx) { + const h = 76; + console.log((idx + 1) * h / this.props.categories.length) + return (idx + 1) * h / this.props.categories.length; + } + + renderCategory(cat, idx) { + return ( + + + {cat.category} + + ); + } + + render () { + console.log(this.props.categories) + return ( + + + {this.props.categories.map((cat, idx) => { return this.renderCategory(cat, idx); })} + + ); + } +} + +export default TimelineCategories; \ No newline at end of file diff --git a/src/components/TimelineEvents.jsx b/src/components/TimelineEvents.jsx new file mode 100644 index 0000000..9b8462e --- /dev/null +++ b/src/components/TimelineEvents.jsx @@ -0,0 +1,41 @@ +import React from 'react'; + +class TimelineEvents extends React.Component { + + getAllEventsAtOnce(eventPoint) { + const timestamp = eventPoint.timestamp; + const category = eventPoint.category; + return this.props.events + .filter(event => (event.timestamp === timestamp && category === event.category)) + } + + renderEvent(event) { + return ( + {this.props.onSelect(this.getAllEventsAtOnce(event))}} + > + + ) + } + + render () { + return ( + + {this.props.events.map(event => this.renderEvent(event))} + + ); + } +} + +export default TimelineEvents; \ No newline at end of file diff --git a/src/js/timeline/timeline.js b/src/js/timeline/timeline.js index c475d1f..7745341 100644 --- a/src/js/timeline/timeline.js +++ b/src/js/timeline/timeline.js @@ -12,13 +12,10 @@ export default function(svg, newApp, ui, methods) { d3.timeFormatDefaultLocale(esLocale); const domain = { - events: [], - categories: [], - narratives: [] + categories: [] } const app = { timerange: newApp.timerange, - selected: [] } // Dimension of the client @@ -64,17 +61,9 @@ export default function(svg, newApp, ui, methods) { .attr('transform', `translate(0, 105)`) .attr('class', 'axis axisHourText'); - dom.axis.y = dom.svg.append('g') + /*dom.axis.y = dom.svg.append('g') .attr('transform', `translate(${WIDTH}, 0)`) - .attr('class', 'yAxis'); - - /* - * Plottable elements - */ - - dom.body = dom.svg.append("g").attr("clip-path", "url(#clip)"); - dom.events = dom.body.append('g'); - + .attr('class', 'yAxis');*/ /* * Initialize axis function and element group @@ -95,9 +84,9 @@ export default function(svg, newApp, ui, methods) { .tickSize(0) .tickFormat(d3.timeFormat('%H:%M')); - axis.y = + /*axis.y = d3.axisLeft(scale.y) - .tickValues([]); + .tickValues([]);*/ updateAxis(); @@ -127,19 +116,6 @@ export default function(svg, newApp, ui, methods) { }); } addResizeListener(); - - - /** - * Given an event, get all the filtered events that happen simultaneously - * @param {object} eventPoint: regular eventPoint data - */ - function getAllEventsAtOnce(eventPoint) { - const timestamp = eventPoint.timestamp; - const category = eventPoint.category; - return domain.events - .filter(event => (event.timestamp === timestamp && category === event.category)) - } - /* * Get y height of eventPoint, considering the ordinal Y scale @@ -225,7 +201,9 @@ export default function(svg, newApp, ui, methods) { const newDomainF = d3.timeSecond.offset(app.timerange[1], timeShift); scale.x.domain([newDomain0, newDomainF]); - render(); +// render(); + app.timerange = scale.x.domain(); + methods.onUpdateTimerange(scale.x.domain()); }) .on('end', () => { toggleTransition(true); @@ -233,60 +211,6 @@ export default function(svg, newApp, ui, methods) { methods.onUpdateTimerange(scale.x.domain()); }); - - /** - * Highlight event circle on hover - */ - function handleMouseOver() { - d3.select(this) - .attr('r', 7) - .classed('mouseover', true); - } - - - /** - * Unhighlight event when mouse out - */ - function handleMouseOut() { - d3.select(this) - .attr('r', 5) - .classed('mouseover', false); - } - - /** - * Return event circles of different groups - */ - function renderEvents() { - const eventsDom = dom.events - .selectAll('.event') - .data(domain.events, d => d.id); - - eventsDom - .exit() - .remove(); - - eventsDom - .transition() - .duration(transitionDuration) - .attr('cx', eventPoint => getEventX(eventPoint)); - - eventsDom - .enter() - .append('circle') - .attr('class', 'event') - .attr('cx', eventPoint => getEventX(eventPoint)) - .attr('cy', eventPoint => getEventY(eventPoint)) - .style('fill', eventPoint => methods.getCategoryColor(eventPoint.category)) - .on('click', eventPoint => methods.onSelect(getAllEventsAtOnce(eventPoint))) - .on('mouseover', handleMouseOver) - .on('mouseout', handleMouseOut) - .transition() - .delay(300) - .duration(200) - .attr('r', 5); - } - - /** * Render axis on timeline and viewbox boundaries */ @@ -307,11 +231,11 @@ export default function(svg, newApp, ui, methods) { .duration(transitionDuration) .call(axis.x1); - axis.y.tickSize(WIDTH - margin.left); + /*axis.y.tickSize(WIDTH - margin.left); dom.axis.y .call(axis.y) - .call(drag); + .call(drag);*/ } /** @@ -320,21 +244,18 @@ export default function(svg, newApp, ui, methods) { * @param {Object} app: Redux state app subtree */ function updateAxis() { - const groupStep = (106 - 30) / domain.categories.length; let groupYs = Array.apply(null, Array(domain.categories.length)); groupYs = groupYs.map((g, i) => { - //return 30 + i * groupStep; - const h = 106 - 30 - return (i + 1) * h / groupYs.length - 15; + const h = 76; + return (i + 1) * h / groupYs.length; }); - scale.y = d3.scaleOrdinal() .domain(domain.categories) .range(groupYs); - axis.y = + /*axis.y = d3.axisLeft(scale.y) - .tickValues(domain.categories.map(c => c.category)); + .tickValues(domain.categories.map(c => c.category));*/ } @@ -350,13 +271,10 @@ export default function(svg, newApp, ui, methods) { if (isNewDomain) { domain.categories = newDomain.categories; - domain.events = newDomain.events; - domain.narratives = newDomain.narratives; } if (isNewAppProps) { app.timerange = newApp.timerange; - app.selected = newApp.selected.slice(0); } if (isNewDomain || isNewAppProps) render(); @@ -365,7 +283,6 @@ export default function(svg, newApp, ui, methods) { function render() { updateAxis(); renderAxis(); - renderEvents(); } return { From 475ed806209df0b9f07715eab6c9239503da7585 Mon Sep 17 00:00:00 2001 From: Franc Camps-Febrer Date: Thu, 3 Jan 2019 16:41:53 +0100 Subject: [PATCH 07/18] Add grabbable background to timeline --- src/components/Timeline.jsx | 5 +- src/components/TimelineCategories.jsx | 24 +++++- src/components/TimelineMarkers.jsx | 14 +++- src/js/timeline/timeline.js | 112 +++++++++++++++----------- src/scss/timeline.scss | 8 ++ 5 files changed, 106 insertions(+), 57 deletions(-) diff --git a/src/components/Timeline.jsx b/src/components/Timeline.jsx index eacec4c..b7d802c 100644 --- a/src/components/Timeline.jsx +++ b/src/components/Timeline.jsx @@ -86,7 +86,10 @@ class Timeline extends React.Component { { this.onMoveTime(dir) }} /> { this.onApplyZoom(zoom); }} /> - + {/* { this.timeline.onDragStart(ev) }} + onDrag={(ev) => { this.timeline.onDrag(ev); }} + />*/} this.timeline.getEventX(e)} getEventY={(e) => this.timeline.getEventY(e)} /> - + { this.props.onDragStart(ev) }} + onDrag={(ev) => { this.props.onDrag(ev) }} + onMouseDown={this.setState({ isDragging: true })} + onMouseUp={this.setState({ isDragging: false })} + stroke="red" x2="-720"> {cat.category} ); } render () { - console.log(this.props.categories) return ( - + {this.props.categories.map((cat, idx) => { return this.renderCategory(cat, idx); })} diff --git a/src/components/TimelineMarkers.jsx b/src/components/TimelineMarkers.jsx index 54de8f1..893caea 100644 --- a/src/components/TimelineMarkers.jsx +++ b/src/components/TimelineMarkers.jsx @@ -6,10 +6,14 @@ class TimelineMarkers extends React.Component { return ( ) @@ -17,7 +21,9 @@ class TimelineMarkers extends React.Component { render () { return ( - + {this.props.selected.map(event => this.renderMarker(event))} ); diff --git a/src/js/timeline/timeline.js b/src/js/timeline/timeline.js index 7745341..23bd0b8 100644 --- a/src/js/timeline/timeline.js +++ b/src/js/timeline/timeline.js @@ -55,16 +55,14 @@ export default function(svg, newApp, ui, methods) { 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'); - /*dom.axis.y = dom.svg.append('g') - .attr('transform', `translate(${WIDTH}, 0)`) - .attr('class', 'yAxis');*/ - /* * Initialize axis function and element group */ @@ -84,13 +82,12 @@ export default function(svg, newApp, ui, methods) { .tickSize(0) .tickFormat(d3.timeFormat('%H:%M')); - /*axis.y = + axis.y = d3.axisLeft(scale.y) - .tickValues([]);*/ + .tickValues([]); updateAxis(); - /** * Adapt dimensions when resizing */ @@ -126,7 +123,6 @@ export default function(svg, newApp, ui, methods) { return scale.y(yGroup); } - /* * Get x position of eventPoint, considering the time scale * @param {object} eventPoint: regular eventPoint data @@ -139,6 +135,10 @@ export default function(svg, newApp, ui, methods) { return (scale.x.domain()[1].getTime() - scale.x.domain()[0].getTime()) / 60000; } + function getScaleX() { + return scale.x; + } + /** * Apply zoom level to timeline * @param {object} zoom: zoom level from zoomLevels @@ -186,41 +186,41 @@ export default function(svg, newApp, ui, methods) { /* * Setup drag behavior */ + function onDragStart(ev) { + d3.event.sourceEvent.stopPropagation(); + dragPos0 = d3.event.x; + toggleTransition(false); + } + + function onDrag() { + const drag0 = scale.x.invert(dragPos0).getTime(); + const dragNow = scale.x.invert(d3.event.x).getTime(); + const timeShift = (drag0 - dragNow) / 1000; + + const newDomain0 = d3.timeSecond.offset(app.timerange[0], timeShift); + const newDomainF = d3.timeSecond.offset(app.timerange[1], timeShift); + + scale.x.domain([newDomain0, newDomainF]); + render(); + //app.timerange = scale.x.domain(); + //methods.onUpdateTimerange(scale.x.domain()); + } + + function onDragEnd() { + toggleTransition(true); + app.timerange = scale.x.domain(); + methods.onUpdateTimerange(scale.x.domain()); + } + const drag = d3.drag() - .on('start', () => { - d3.event.sourceEvent.stopPropagation(); - dragPos0 = d3.event.x; - toggleTransition(false); - }) - .on('drag', () => { - const drag0 = scale.x.invert(dragPos0).getTime(); - const dragNow = scale.x.invert(d3.event.x).getTime(); - const timeShift = (drag0 - dragNow) / 1000; - - const newDomain0 = d3.timeSecond.offset(app.timerange[0], timeShift); - const newDomainF = d3.timeSecond.offset(app.timerange[1], timeShift); - - scale.x.domain([newDomain0, newDomainF]); -// render(); - app.timerange = scale.x.domain(); - methods.onUpdateTimerange(scale.x.domain()); - }) - .on('end', () => { - toggleTransition(true); - app.timerange = scale.x.domain(); - methods.onUpdateTimerange(scale.x.domain()); - }); + .on('start', onDragStart) + .on('drag', onDrag) + .on('end', onDragEnd); /** * Render axis on timeline and viewbox boundaries */ function renderAxis() { - dom.axis.x0 - .call(drag); - - dom.axis.x1 - .call(drag); - dom.axis.x0 .transition() .duration(transitionDuration) @@ -231,11 +231,26 @@ export default function(svg, newApp, ui, methods) { .duration(transitionDuration) .call(axis.x1); - /*axis.y.tickSize(WIDTH - margin.left); + axis.y.tickSize(WIDTH - margin.left); + + if (!dom.axis.dragGrabber) { + dom.axis.dragGrabber = dom.svg.insert('rect', ':first-child') + .attr('class', 'drag-grabber') + .attr('x', margin.left) + .attr('y', 20) + .attr('width', WIDTH - margin.left) + .attr('height', 80) + .call(drag); + } + + if (!dom.axis.y) { + dom.axis.y = dom.svg.insert('g', ':first-child') + .attr('transform', `translate(${WIDTH}, 0)`) + .attr('class', 'yAxis'); + } dom.axis.y - .call(axis.y) - .call(drag);*/ + .call(axis.y); } /** @@ -253,9 +268,9 @@ export default function(svg, newApp, ui, methods) { .domain(domain.categories) .range(groupYs); - /*axis.y = + axis.y = d3.axisLeft(scale.y) - .tickValues(domain.categories.map(c => c.category));*/ + .tickValues(domain.categories.map(c => c.category)); } @@ -269,13 +284,8 @@ export default function(svg, newApp, ui, methods) { const isNewDomain = (hash(domain) !== hash(newDomain)); const isNewAppProps = (hash(app) !== hash(newApp)); - if (isNewDomain) { - domain.categories = newDomain.categories; - } - - if (isNewAppProps) { - app.timerange = newApp.timerange; - } + if (isNewDomain) domain.categories = newDomain.categories; + if (isNewAppProps) app.timerange = newApp.timerange; if (isNewDomain || isNewAppProps) render(); } @@ -286,10 +296,14 @@ export default function(svg, newApp, ui, methods) { } return { + getScaleX, getEventX, getEventY, applyZoom, moveTime, update, + onDragStart, + onDrag, + onDragEnd }; } diff --git a/src/scss/timeline.scss b/src/scss/timeline.scss index 24666a4..c7b1802 100644 --- a/src/scss/timeline.scss +++ b/src/scss/timeline.scss @@ -171,12 +171,20 @@ .yAxis { .tick line { + stroke: $midwhite; stroke-width: 15px; cursor: -webkit-grab; cursor: -moz-grab; } } + .drag-grabber { + cursor: -webkit-grab; + cursor: -moz-grab; + fill: $offwhite; + opacity: 0.05; + } + .axisBoundaries { stroke: $offwhite; stroke-width: 1; From 9bb3c9c2abec6271939d834952ed97fefe94d384 Mon Sep 17 00:00:00 2001 From: Franc Camps-Febrer Date: Thu, 3 Jan 2019 17:24:35 +0100 Subject: [PATCH 08/18] Comment and clean late timeline js --- src/components/Timeline.jsx | 59 +++++++++---- src/components/TimelineCategories.jsx | 49 ----------- src/components/TimelineEvents.jsx | 2 +- src/components/TimelineMarkers.jsx | 2 +- src/js/timeline/timeline.js | 118 ++++++++++++-------------- src/scss/timeline.scss | 1 - 6 files changed, 100 insertions(+), 131 deletions(-) delete mode 100644 src/components/TimelineCategories.jsx diff --git a/src/components/Timeline.jsx b/src/components/Timeline.jsx index b7d802c..0c54cd1 100644 --- a/src/components/Timeline.jsx +++ b/src/components/Timeline.jsx @@ -12,7 +12,6 @@ import TimelineLogic from '../js/timeline/timeline.js'; 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) { @@ -26,20 +25,22 @@ class Timeline extends React.Component { width_controls: 100, height_controls: 115, margin_left: 120 - } + }, + softTimeUpdate: 0 }; } componentDidMount() { - this.timeline = new TimelineLogic(this.svgRef.current, this.props.app, this.props.ui, this.props.methods); - this.timeline.update(this.props.domain, this.props.app); + 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, nextProps.app); + this.timeline.update(nextProps.domain.categories, nextProps.app.timerange); } } @@ -69,7 +70,23 @@ class Timeline extends React.Component { return this.timeline.applyZoom(zoom); } return null; - } + } + + renderTimelineClip() { + const dims = this.state.dims; + + return ( + + + + + ); + } renderSVG() { const dims = this.state.dims; @@ -80,17 +97,25 @@ class Timeline extends React.Component { width={dims.width} height={dims.height} > - - - - { this.onMoveTime(dir) }} /> - { this.onApplyZoom(zoom); }} /> - - {/* { this.timeline.onDragStart(ev) }} - onDrag={(ev) => { this.timeline.onDrag(ev); }} - />*/} - this.timeline.getEventX(e)} getEventY={(e) => this.timeline.getEventY(e)} /> + {this.renderTimelineClip()} + { this.onMoveTime(dir) }} + /> + { this.onApplyZoom(zoom); }} + /> + + this.timeline.getEventX(e)} + getEventY={(e) => this.timeline.getEventY(e)} + /> this.timeline.getEventX(e)} diff --git a/src/components/TimelineCategories.jsx b/src/components/TimelineCategories.jsx deleted file mode 100644 index 07a3da3..0000000 --- a/src/components/TimelineCategories.jsx +++ /dev/null @@ -1,49 +0,0 @@ -import React from 'react'; - -class TimelineCategories extends React.Component { - - constructor() { - super(); - - this.state = { - isDragging: false, - dragPos0: 0, - scaleXDomain - } - } - - getY(idx) { - const h = 76; - console.log((idx + 1) * h / this.props.categories.length) - return (idx + 1) * h / this.props.categories.length; - } - - renderCategory(cat, idx) { - return ( - - { this.props.onDragStart(ev) }} - onDrag={(ev) => { this.props.onDrag(ev) }} - onMouseDown={this.setState({ isDragging: true })} - onMouseUp={this.setState({ isDragging: false })} - stroke="red" x2="-720"> - {cat.category} - - ); - } - - render () { - return ( - - - {this.props.categories.map((cat, idx) => { return this.renderCategory(cat, idx); })} - - ); - } -} - -export default TimelineCategories; \ No newline at end of file diff --git a/src/components/TimelineEvents.jsx b/src/components/TimelineEvents.jsx index 9b8462e..60c676e 100644 --- a/src/components/TimelineEvents.jsx +++ b/src/components/TimelineEvents.jsx @@ -17,7 +17,7 @@ class TimelineEvents extends React.Component { cy={0} style={{ 'transform': `translate(${this.props.getEventX(event)}px, ${this.props.getEventY(event)}px)`, - 'transition': 'transform 0.5s ease' + 'transition': 'transform 0.3s ease' }} r={5} fill={this.props.getCategoryColor(event.category)} diff --git a/src/components/TimelineMarkers.jsx b/src/components/TimelineMarkers.jsx index 893caea..a97b3cf 100644 --- a/src/components/TimelineMarkers.jsx +++ b/src/components/TimelineMarkers.jsx @@ -10,7 +10,7 @@ class TimelineMarkers extends React.Component { cy={0} style={{ 'transform': `translate(${this.props.getEventX(event)}px, ${this.props.getEventY(event)}px)`, - 'transition': 'transform 0.5s ease', + 'transition': 'transform 0.3s ease', 'opacity': 0.9 }} r="10" diff --git a/src/js/timeline/timeline.js b/src/js/timeline/timeline.js index 23bd0b8..4c41b51 100644 --- a/src/js/timeline/timeline.js +++ b/src/js/timeline/timeline.js @@ -8,34 +8,31 @@ import { parseDate } from '../utilities'; import hash from 'object-hash'; import esLocale from '../data/es-MX.json'; -export default function(svg, newApp, ui, methods) { +export default function(svg, ui, methods) { d3.timeFormatDefaultLocale(esLocale); - const domain = { - categories: [] - } - const app = { - timerange: newApp.timerange, - } + let categories = []; + let timerange = [null, null]; // Dimension of the client const WIDTH_CONTROLS = 100; let WIDTH = getCurrentWidth() - WIDTH_CONTROLS; + const HEIGHT = 80; // NB: is it possible to do this with SCSS? // A: Maybe, although we are using it programmatically here for now - const margin = { left: 120 }; + const margin = { left: 120, top: 20 }; // Drag behavior let dragPos0; - let transitionDuration = 500; + let transitionDuration = 300; /** * Create scales */ const scale = {}; scale.x = d3.scaleTime() - .domain(app.timerange) + .domain(timerange) .range([margin.left, WIDTH]); scale.y = d3.scaleOrdinal() @@ -72,13 +69,13 @@ export default function(svg, newApp, ui, methods) { d3.axisBottom(scale.x) .ticks(10) .tickPadding(5) - .tickSize(80) + .tickSize(HEIGHT) .tickFormat(d3.timeFormat('%d %b')); axis.x1 = d3.axisBottom(scale.x) .ticks(10) - .tickPadding(20) + .tickPadding(margin.top) .tickSize(0) .tickFormat(d3.timeFormat('%H:%M')); @@ -114,7 +111,7 @@ export default function(svg, newApp, ui, methods) { } addResizeListener(); - /* + /** * Get y height of eventPoint, considering the ordinal Y scale * @param {object} eventPoint: regular eventPoint data */ @@ -123,7 +120,7 @@ export default function(svg, newApp, ui, methods) { return scale.y(yGroup); } - /* + /** * Get x position of eventPoint, considering the time scale * @param {object} eventPoint: regular eventPoint data */ @@ -131,14 +128,13 @@ export default function(svg, newApp, ui, methods) { return scale.x(parseDate(eventPoint.timestamp)); } + /** + * Returns the time scale (x) extent in minutes + */ function getTimeScaleExtent() { return (scale.x.domain()[1].getTime() - scale.x.domain()[0].getTime()) / 60000; } - function getScaleX() { - return scale.x; - } - /** * Apply zoom level to timeline * @param {object} zoom: zoom level from zoomLevels @@ -147,10 +143,11 @@ export default function(svg, newApp, ui, methods) { const extent = getTimeScaleExtent(); const newCentralTime = d3.timeMinute.offset(scale.x.domain()[0], extent / 2); - const domain0 = d3.timeMinute.offset(newCentralTime, -zoom.duration / 2); - const domainF = d3.timeMinute.offset(newCentralTime, zoom.duration / 2); + scale.x.domain([ + d3.timeMinute.offset(newCentralTime, -zoom.duration / 2), + d3.timeMinute.offset(newCentralTime, zoom.duration / 2) + ]); - scale.x.domain([domain0, domainF]); methods.onUpdateTimerange(scale.x.domain()); } @@ -192,23 +189,27 @@ export default function(svg, newApp, ui, methods) { toggleTransition(false); } + /* + * Drag and update + */ function onDrag() { const drag0 = scale.x.invert(dragPos0).getTime(); const dragNow = scale.x.invert(d3.event.x).getTime(); const timeShift = (drag0 - dragNow) / 1000; - const newDomain0 = d3.timeSecond.offset(app.timerange[0], timeShift); - const newDomainF = d3.timeSecond.offset(app.timerange[1], timeShift); + const newDomain0 = d3.timeSecond.offset(timerange[0], timeShift); + const newDomainF = d3.timeSecond.offset(timerange[1], timeShift); scale.x.domain([newDomain0, newDomainF]); render(); - //app.timerange = scale.x.domain(); - //methods.onUpdateTimerange(scale.x.domain()); + // Updates components without updating timerange + methods.onSoftUpdate(1); } function onDragEnd() { toggleTransition(true); - app.timerange = scale.x.domain(); + timerange = scale.x.domain(); + methods.onSoftUpdate(0); methods.onUpdateTimerange(scale.x.domain()); } @@ -217,6 +218,26 @@ export default function(svg, newApp, ui, methods) { .on('drag', onDrag) .on('end', onDragEnd); + /** + * Updates data displayed by this timeline, but only render if necessary + * @param {Object} domain: Redux state domain subtree + * @param {Object} app: Redux state app subtree + */ + function updateAxis() { + let groupYs = Array.apply(null, Array(categories.length)); + groupYs = groupYs.map((g, i) => { + return (i + 1) * HEIGHT / groupYs.length; + }); + scale.y = d3.scaleOrdinal() + .domain(categories) + .range(groupYs); + + axis.y = + d3.axisLeft(scale.y) + .tickValues(categories.map(c => c.category)); + } + + /** * Render axis on timeline and viewbox boundaries */ @@ -237,9 +258,9 @@ export default function(svg, newApp, ui, methods) { dom.axis.dragGrabber = dom.svg.insert('rect', ':first-child') .attr('class', 'drag-grabber') .attr('x', margin.left) - .attr('y', 20) + .attr('y', margin.top) .attr('width', WIDTH - margin.left) - .attr('height', 80) + .attr('height', HEIGHT) .call(drag); } @@ -253,41 +274,18 @@ export default function(svg, newApp, ui, methods) { .call(axis.y); } - /** - * Updates data displayed by this timeline, but only render if necessary - * @param {Object} domain: Redux state domain subtree - * @param {Object} app: Redux state app subtree - */ - function updateAxis() { - let groupYs = Array.apply(null, Array(domain.categories.length)); - groupYs = groupYs.map((g, i) => { - const h = 76; - return (i + 1) * h / groupYs.length; - }); - scale.y = d3.scaleOrdinal() - .domain(domain.categories) - .range(groupYs); - - axis.y = - d3.axisLeft(scale.y) - .tickValues(domain.categories.map(c => c.category)); - } - /** * Updates displayable data on the timeline: events, selected and * potentially adjusts time range - * @param {Object} newDomain: object of arrays of events and categories - * @param {Object} newApp: object of time range and selected events + * @param {Object} newCategories: object of arrays of categories + * @param {Object} newTimerange: object of time range */ - function update(newDomain, newApp) { - const isNewDomain = (hash(domain) !== hash(newDomain)); - const isNewAppProps = (hash(app) !== hash(newApp)); + function update(newCategories, newTimerange) { + if (hash(categories) !== hash(newCategories)) categories = newCategories; + if (hash(timerange) !== hash(newTimerange)) timerange = newTimerange; - if (isNewDomain) domain.categories = newDomain.categories; - if (isNewAppProps) app.timerange = newApp.timerange; - - if (isNewDomain || isNewAppProps) render(); + render(); } function render() { @@ -296,14 +294,10 @@ export default function(svg, newApp, ui, methods) { } return { - getScaleX, getEventX, getEventY, applyZoom, moveTime, - update, - onDragStart, - onDrag, - onDragEnd + update }; } diff --git a/src/scss/timeline.scss b/src/scss/timeline.scss index c7b1802..54d63a3 100644 --- a/src/scss/timeline.scss +++ b/src/scss/timeline.scss @@ -192,7 +192,6 @@ } .event { - fill: $event_default; cursor: pointer; opacity: .7; From d8b27cea546e2ecc8556ebf9157431b3137865fd Mon Sep 17 00:00:00 2001 From: Franc Camps-Febrer Date: Thu, 3 Jan 2019 17:33:44 +0100 Subject: [PATCH 09/18] Separate TimelineClip --- src/components/Timeline.jsx | 26 +++++-------------- src/components/presentational/TimelineClip.js | 15 +++++++++++ src/js/timeline/timeline.js | 2 +- 3 files changed, 22 insertions(+), 21 deletions(-) create mode 100644 src/components/presentational/TimelineClip.js diff --git a/src/components/Timeline.jsx b/src/components/Timeline.jsx index 0c54cd1..5a119f3 100644 --- a/src/components/Timeline.jsx +++ b/src/components/Timeline.jsx @@ -4,8 +4,9 @@ import * as selectors from '../selectors'; import hash from 'object-hash'; import copy from '../js/data/copy.json'; -import { formatterWithYear, isNotNullNorUndefined } from '../js/utilities'; +import { formatterWithYear } from '../js/utilities'; import TimelineHeader from './presentational/TimelineHeader'; +import TimelineClip from './presentational/TimelineClip'; import TimelineHandles from './TimelineHandles.jsx'; import TimelineZoomControls from './TimelineZoomControls.jsx'; import TimelineLogic from '../js/timeline/timeline.js'; @@ -51,8 +52,9 @@ class Timeline extends React.Component { } computeDims() { - if (document.querySelector(`#${this.props.ui.dom.timeline}`) !== null) { - const boundingClient = document.querySelector(`#${this.props.ui.dom.timeline}`).getBoundingClientRect(); + const dom = this.props.ui.dom.timeline; + if (document.querySelector(`#${dom}`) !== null) { + const boundingClient = document.querySelector(`#${dom}`).getBoundingClientRect(); const WIDTH = boundingClient.width; this.setState({ dims: Object.assign({}, this.state.dims, { width: WIDTH }) }); } @@ -72,22 +74,6 @@ class Timeline extends React.Component { return null; } - renderTimelineClip() { - const dims = this.state.dims; - - return ( - - - - - ); - } - renderSVG() { const dims = this.state.dims; @@ -97,7 +83,7 @@ class Timeline extends React.Component { width={dims.width} height={dims.height} > - {this.renderTimelineClip()} + { this.onMoveTime(dir) }} diff --git a/src/components/presentational/TimelineClip.js b/src/components/presentational/TimelineClip.js new file mode 100644 index 0000000..29491da --- /dev/null +++ b/src/components/presentational/TimelineClip.js @@ -0,0 +1,15 @@ +import React from 'react'; + +const TimelineClip = ({ dims }) => ( + + + + +); + +export default TimelineClip; diff --git a/src/js/timeline/timeline.js b/src/js/timeline/timeline.js index 4c41b51..b844363 100644 --- a/src/js/timeline/timeline.js +++ b/src/js/timeline/timeline.js @@ -284,7 +284,7 @@ export default function(svg, ui, methods) { function update(newCategories, newTimerange) { if (hash(categories) !== hash(newCategories)) categories = newCategories; if (hash(timerange) !== hash(newTimerange)) timerange = newTimerange; - + scale.x.domain(timerange); render(); } From a0c654aafc624ecf15fd99b324984e0a5cb4e1ef Mon Sep 17 00:00:00 2001 From: Franc Camps-Febrer Date: Thu, 3 Jan 2019 18:41:57 +0100 Subject: [PATCH 10/18] Reactifyf scale Y --- src/components/Timeline.jsx | 30 ++++++++++-- src/components/TimelineCategories.jsx | 55 +++++++++++++++++++++ src/js/timeline/timeline.js | 70 ++------------------------- src/scss/timeline.scss | 6 +++ 4 files changed, 92 insertions(+), 69 deletions(-) create mode 100644 src/components/TimelineCategories.jsx diff --git a/src/components/Timeline.jsx b/src/components/Timeline.jsx index 5a119f3..78a313b 100644 --- a/src/components/Timeline.jsx +++ b/src/components/Timeline.jsx @@ -13,6 +13,7 @@ import TimelineLogic from '../js/timeline/timeline.js'; 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) { @@ -27,12 +28,15 @@ class Timeline extends React.Component { height_controls: 115, margin_left: 120 }, - softTimeUpdate: 0 + softTimeUpdate: 0, + scaleY: null }; } componentDidMount() { - this.methods = Object.assign({}, this.props.methods, { onSoftUpdate: (toggle) => { this.setState({ softTimeUpdate: toggle }) }}); + 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(); @@ -42,9 +46,23 @@ class Timeline extends React.Component { 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) }); } } + /** + * Get y height of eventPoint, considering the ordinal Y scale + * @param {object} eventPoint: regular eventPoint data + */ + getEventY(eventPoint) { + return this.state.scaleY(eventPoint.category); + } + + onClickArrow() { this.setState((prevState, props) => { return {isFolded: !prevState.isFolded}; @@ -84,6 +102,10 @@ class Timeline extends React.Component { height={dims.height} > + { this.onMoveTime(dir) }} @@ -100,12 +122,12 @@ class Timeline extends React.Component { this.timeline.getEventX(e)} - getEventY={(e) => this.timeline.getEventY(e)} + getEventY={(e) => this.getEventY(e)/*this.timeline.getEventY(e)*/} /> this.timeline.getEventX(e)} - getEventY={(e) => this.timeline.getEventY(e)} + getEventY={(e) => this.getEventY(e)/*this.timeline.getEventY(e)*/} getCategoryColor={this.props.methods.getCategoryColor} onSelect={this.props.methods.onSelect} /> diff --git a/src/components/TimelineCategories.jsx b/src/components/TimelineCategories.jsx new file mode 100644 index 0000000..9d2e625 --- /dev/null +++ b/src/components/TimelineCategories.jsx @@ -0,0 +1,55 @@ +import React from 'react'; + +class TimelineCategories extends React.Component { + + constructor() { + super(); + this.grabRef = React.createRef() + this.state = { + isInitialized: false + } + } + + 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); + + d3.select(this.grabRef.current) + .call(drag); + + this.setState({ isInitialized: true }); + } + } + + getY(idx) { + return (idx + 1) * 80 / this.props.categories.length + } + + renderCategory(category, idx) { + return ( + + + {category.category} + + ) + } + + render () { + return ( + + {this.props.categories.map((cat, idx) => this.renderCategory(cat, idx))} + + + ); + } +} + +export default TimelineCategories; \ No newline at end of file diff --git a/src/js/timeline/timeline.js b/src/js/timeline/timeline.js index b844363..9e65864 100644 --- a/src/js/timeline/timeline.js +++ b/src/js/timeline/timeline.js @@ -2,7 +2,6 @@ TIMELINE Displays events over the course of the night Allows brushing and selecting periods of time in it - TODO: is it possible to express this idiomatically as React? */ import { parseDate } from '../utilities'; import hash from 'object-hash'; @@ -35,9 +34,6 @@ export default function(svg, ui, methods) { .domain(timerange) .range([margin.left, WIDTH]); - scale.y = d3.scaleOrdinal() - - /** * Initilize SVG elements and groups */ @@ -79,10 +75,6 @@ export default function(svg, ui, methods) { .tickSize(0) .tickFormat(d3.timeFormat('%H:%M')); - axis.y = - d3.axisLeft(scale.y) - .tickValues([]); - updateAxis(); /** @@ -103,7 +95,6 @@ export default function(svg, ui, methods) { WIDTH = getCurrentWidth() - WIDTH_CONTROLS; scale.x.range([margin.left, WIDTH]); - axis.y.tickSize(WIDTH - margin.left); dom.axis.y.attr('transform', `translate(${WIDTH}, 0)`) render(null); } @@ -111,14 +102,6 @@ export default function(svg, ui, methods) { } addResizeListener(); - /** - * Get y height of eventPoint, considering the ordinal Y scale - * @param {object} eventPoint: regular eventPoint data - */ - function getEventY(eventPoint) { - const yGroup = eventPoint.category; - return scale.y(yGroup); - } /** * Get x position of eventPoint, considering the time scale @@ -184,6 +167,7 @@ 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); @@ -213,30 +197,6 @@ export default function(svg, ui, methods) { methods.onUpdateTimerange(scale.x.domain()); } - const drag = d3.drag() - .on('start', onDragStart) - .on('drag', onDrag) - .on('end', onDragEnd); - - /** - * Updates data displayed by this timeline, but only render if necessary - * @param {Object} domain: Redux state domain subtree - * @param {Object} app: Redux state app subtree - */ - function updateAxis() { - let groupYs = Array.apply(null, Array(categories.length)); - groupYs = groupYs.map((g, i) => { - return (i + 1) * HEIGHT / groupYs.length; - }); - scale.y = d3.scaleOrdinal() - .domain(categories) - .range(groupYs); - - axis.y = - d3.axisLeft(scale.y) - .tickValues(categories.map(c => c.category)); - } - /** * Render axis on timeline and viewbox boundaries @@ -251,27 +211,6 @@ export default function(svg, ui, methods) { .transition() .duration(transitionDuration) .call(axis.x1); - - axis.y.tickSize(WIDTH - margin.left); - - if (!dom.axis.dragGrabber) { - dom.axis.dragGrabber = dom.svg.insert('rect', ':first-child') - .attr('class', 'drag-grabber') - .attr('x', margin.left) - .attr('y', margin.top) - .attr('width', WIDTH - margin.left) - .attr('height', HEIGHT) - .call(drag); - } - - if (!dom.axis.y) { - dom.axis.y = dom.svg.insert('g', ':first-child') - .attr('transform', `translate(${WIDTH}, 0)`) - .attr('class', 'yAxis'); - } - - dom.axis.y - .call(axis.y); } @@ -289,15 +228,16 @@ export default function(svg, ui, methods) { } function render() { - updateAxis(); renderAxis(); } return { getEventX, - getEventY, applyZoom, moveTime, - update + update, + onDragStart, + onDrag, + onDragEnd }; } diff --git a/src/scss/timeline.scss b/src/scss/timeline.scss index 54d63a3..8e495b3 100644 --- a/src/scss/timeline.scss +++ b/src/scss/timeline.scss @@ -176,6 +176,12 @@ cursor: -webkit-grab; cursor: -moz-grab; } + + .tick text { + font-size: 10px; + font-family: 'Lato'; + text-anchor: end; + } } .drag-grabber { From f45f4c4fdd60eced72dccfe10318bec2a181ec2f Mon Sep 17 00:00:00 2001 From: Franc Camps-Febrer Date: Fri, 4 Jan 2019 07:22:02 +0100 Subject: [PATCH 11/18] Reactify scale of time --- src/components/Timeline.jsx | 133 +++++++++++++++++++++----- src/components/TimelineAxis.jsx | 70 ++++++++++++++ src/components/TimelineCategories.jsx | 22 +++-- src/js/timeline/timeline.js | 104 -------------------- 4 files changed, 195 insertions(+), 134 deletions(-) create mode 100644 src/components/TimelineAxis.jsx 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, From 98494e683d5017b1a13224c6cb3df2abec6c9dde Mon Sep 17 00:00:00 2001 From: Franc Camps-Febrer Date: Fri, 4 Jan 2019 08:04:03 +0100 Subject: [PATCH 12/18] Readjust event handlers --- src/components/Timeline.jsx | 72 +++++++------ src/components/TimelineAxis.jsx | 5 +- src/components/TimelineCategories.jsx | 9 +- src/components/TimelineEvents.jsx | 2 +- src/js/timeline/timeline.js | 139 -------------------------- 5 files changed, 51 insertions(+), 176 deletions(-) delete mode 100644 src/js/timeline/timeline.js diff --git a/src/components/Timeline.jsx b/src/components/Timeline.jsx index 03ec64e..6a11e5c 100644 --- a/src/components/Timeline.jsx +++ b/src/components/Timeline.jsx @@ -33,27 +33,25 @@ class Timeline extends React.Component { softTimeUpdate: 0, scaleX: null, scaleY: null, - timerange: [null, null] + timerange: [null, null], + dragPos0: null }; } componentDidMount() { - this.methods = Object.assign({}, this.props.methods, { - onSoftUpdate: (toggle) => { this.setState({ softTimeUpdate: toggle }) } - }); - this.computeDims(); window.addEventListener('resize', () => { this.computeDims(); }); } componentWillReceiveProps(nextProps) { if (hash(nextProps) !== hash(this.props)) { - let groupYs = Array.apply(null, Array(nextProps.domain.categories.length)); - groupYs = groupYs.map((g, i) => (i + 1) * 80 / groupYs.length); + const categories = nextProps.domain.categories; + const cats = categories.map((g, i) => (i + 1) * 80 / categories.length); this.setState({ + timerange: nextProps.app.timerange, 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) + scaleY: d3.scaleOrdinal().domain(nextProps.domain.categories).range(cats) }); } } @@ -102,7 +100,7 @@ class Timeline extends React.Component { * @param {String} direction: 'forward' / 'backwards' */ onMoveTime(direction) { - this.methods.onSelect(); + this.props.methods.onSelect(); const extent = this.getTimeScaleExtent(); const newCentralTime = d3.timeMinute.offset(this.state.scaleX.domain()[0], extent / 2); @@ -117,9 +115,17 @@ class Timeline extends React.Component { } this.state.scaleX.domain([domain0, domainF]); - this.methods.onUpdateTimerange(this.state.scaleX.domain()); + this.props.methods.onUpdateTimerange(this.state.scaleX.domain()); } + /** + * 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 @@ -134,42 +140,48 @@ class Timeline extends React.Component { d3.timeMinute.offset(newCentralTime, zoom.duration / 2) ]); - this.methods.onUpdateTimerange(this.state.scaleX.domain()); + this.props.methods.onUpdateTimerange(this.state.scaleX.domain()); + } + + toggleTransition(isTransition) { + this.setState({ transitionDuration: (isTransition) ? 300 : 0 }); } /* * Setup drag behavior */ - onDragStart(ev) { + onDragStart() { d3.event.sourceEvent.stopPropagation(); - dragPos0 = d3.event.x; - this.toggleTransition(false); + this.setState({ + dragPos0: d3.event.x + }, () => { + this.toggleTransition(false); + }); } /* * Drag and update */ onDrag() { - const drag0 = this.state.scaleX.invert(dragPos0).getTime(); + 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(timerange[0], timeShift); - const newDomainF = d3.timeSecond.offset(timerange[1], timeShift); + const newDomain0 = d3.timeSecond.offset(this.state.timerange[0], timeShift); + const newDomainF = d3.timeSecond.offset(this.state.timerange[1], timeShift); this.state.scaleX.domain([newDomain0, newDomainF]); - //render(); + // Updates components without updating timerange - this.methods.onSoftUpdate(1); + this.onSoftTimeRangeUpdate([newDomain0, newDomainF]); } onDragEnd() { - toggleTransition(true); + this.toggleTransition(true); this.setState({ timerange: this.state.scaleX.domain() }, () => { - this.methods.onSoftUpdate(0); - this.methods.onUpdateTimerange(scale.x.domain()); + this.props.methods.onUpdateTimerange(this.state.scaleX.domain()); }); } @@ -186,13 +198,14 @@ class Timeline extends React.Component { { this.onDragStart() }} + onDrag={() => { this.onDrag() }} + onDragEnd={() => { this.onDragEnd() }} categories={this.props.domain.categories} /> this.getEventX(e)} getEventY={(e) => this.getEventY(e)} getCategoryColor={this.props.methods.getCategoryColor} + transitionDuration={this.state.transitionDuration} onSelect={this.props.methods.onSelect} /> @@ -231,9 +245,9 @@ class Timeline extends React.Component { return (
{ this.onClickArrow(); }} hideInfo={isNarrative} /> diff --git a/src/components/TimelineAxis.jsx b/src/components/TimelineAxis.jsx index e41b2f1..0daafe4 100644 --- a/src/components/TimelineAxis.jsx +++ b/src/components/TimelineAxis.jsx @@ -8,7 +8,6 @@ class TimelineAxis extends React.Component { this.xAxis1Ref = React.createRef(); this.state = { isInitialized: false, - transitionDuration: 300 } } @@ -35,12 +34,12 @@ class TimelineAxis extends React.Component { if (this.state.isInitialized) { d3.select(this.xAxis0Ref.current) .transition() - .duration(this.state.transitionDuration) + .duration(this.props.transitionDuration) .call(this.x0); d3.select(this.xAxis1Ref.current) .transition() - .duration(this.state.transitionDuration) + .duration(this.props.transitionDuration) .call(this.x1); } } diff --git a/src/components/TimelineCategories.jsx b/src/components/TimelineCategories.jsx index 241185f..c84fcb4 100644 --- a/src/components/TimelineCategories.jsx +++ b/src/components/TimelineCategories.jsx @@ -11,7 +11,7 @@ class TimelineCategories extends React.Component { } componentDidUpdate() { - if (!this.state.isInitialized && this.props.timeline) { + if (!this.state.isInitialized) { const drag = d3.drag() .on('start', this.props.onDragStart) .on('drag', this.props.onDrag) @@ -29,10 +29,11 @@ class TimelineCategories extends React.Component { } renderCategory(category, idx) { + const dims = this.props.dims; return ( - - {category.category} + + {category.category} ) } @@ -50,7 +51,7 @@ class TimelineCategories extends React.Component { class="drag-grabber" x={dims.margin_left} y="20" - width={dims.width - dims.margin_left} + width={dims.width - dims.margin_left - dims.width_controls} height={dims.trackHeight} > diff --git a/src/components/TimelineEvents.jsx b/src/components/TimelineEvents.jsx index 60c676e..9b1a752 100644 --- a/src/components/TimelineEvents.jsx +++ b/src/components/TimelineEvents.jsx @@ -17,7 +17,7 @@ class TimelineEvents extends React.Component { cy={0} style={{ 'transform': `translate(${this.props.getEventX(event)}px, ${this.props.getEventY(event)}px)`, - 'transition': 'transform 0.3s ease' + 'transition': `transform ${this.props.transitionDuration / 1000}s ease` }} r={5} fill={this.props.getCategoryColor(event.category)} diff --git a/src/js/timeline/timeline.js b/src/js/timeline/timeline.js deleted file mode 100644 index b0e138e..0000000 --- a/src/js/timeline/timeline.js +++ /dev/null @@ -1,139 +0,0 @@ -/* - TIMELINE - Displays events over the course of the night - Allows brushing and selecting periods of time in it -*/ -import { parseDate } from '../utilities'; -import hash from 'object-hash'; -import esLocale from '../data/es-MX.json'; - -export default function(svg, ui, methods) { - d3.timeFormatDefaultLocale(esLocale); - - let categories = []; - let timerange = [null, null]; - - // Dimension of the client - const WIDTH_CONTROLS = 100; - let WIDTH = getCurrentWidth() - WIDTH_CONTROLS; - const HEIGHT = 80; - - // NB: is it possible to do this with SCSS? - // A: Maybe, although we are using it programmatically here for now - const margin = { left: 120, top: 20 }; - - // Drag behavior - let dragPos0; - let transitionDuration = 300; - - /** - * Create scales - */ - const scale = {}; - scale.x = d3.scaleTime() - .domain(timerange) - .range([margin.left, WIDTH]); - - - - /** - * Adapt dimensions when resizing - */ - function getCurrentWidth() { - return d3.select(`#${ui.dom.timeline}`).node() - .getBoundingClientRect().width; - } - - - /** - * Resize timeline one window resice - */ - function addResizeListener() { - window.addEventListener('resize', () => { - if (d3.select(`#${ui.dom.timeline}`).node() !== null) { - WIDTH = getCurrentWidth() - WIDTH_CONTROLS; - - scale.x.range([margin.left, WIDTH]); - dom.axis.y.attr('transform', `translate(${WIDTH}, 0)`) - render(null); - } - }); - } - addResizeListener(); - - - /** - * Get x position of eventPoint, considering the time scale - * @param {object} eventPoint: regular eventPoint data - */ - function getEventX(eventPoint) { - return scale.x(parseDate(eventPoint.timestamp)); - } - - /** - * Returns the time scale (x) extent in minutes - */ - function getTimeScaleExtent() { - return (scale.x.domain()[1].getTime() - scale.x.domain()[0].getTime()) / 60000; - } - - function toggleTransition(isTransition) { - transitionDuration = (isTransition) ? 500 : 0; - } - - - /* - * Setup drag behavior - */ - function onDragStart(ev) { - d3.event.sourceEvent.stopPropagation(); - dragPos0 = d3.event.x; - toggleTransition(false); - } - - /* - * Drag and update - */ - function onDrag() { - const drag0 = scale.x.invert(dragPos0).getTime(); - const dragNow = scale.x.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); - - scale.x.domain([newDomain0, newDomainF]); - render(); - // Updates components without updating timerange - methods.onSoftUpdate(1); - } - - function onDragEnd() { - toggleTransition(true); - timerange = scale.x.domain(); - methods.onSoftUpdate(0); - methods.onUpdateTimerange(scale.x.domain()); - } - - - /** - * Updates displayable data on the timeline: events, selected and - * potentially adjusts time range - * @param {Object} newCategories: object of arrays of categories - * @param {Object} newTimerange: object of time range - */ - function update(newCategories, newTimerange) { - if (hash(categories) !== hash(newCategories)) categories = newCategories; - if (hash(timerange) !== hash(newTimerange)) timerange = newTimerange; - scale.x.domain(timerange); - render(); - } - - return { - getEventX, - update, - onDragStart, - onDrag, - onDragEnd - }; -} From 1519ee086893aab7a472d4e441bd0d6830502197 Mon Sep 17 00:00:00 2001 From: Franc Camps-Febrer Date: Fri, 4 Jan 2019 08:24:53 +0100 Subject: [PATCH 13/18] Refine dragging behavior --- src/components/Timeline.jsx | 41 +++++++++++++++++++----------- src/components/TimelineMarkers.jsx | 2 +- 2 files changed, 27 insertions(+), 16 deletions(-) diff --git a/src/components/Timeline.jsx b/src/components/Timeline.jsx index 6a11e5c..fde5759 100644 --- a/src/components/Timeline.jsx +++ b/src/components/Timeline.jsx @@ -34,7 +34,8 @@ class Timeline extends React.Component { scaleX: null, scaleY: null, timerange: [null, null], - dragPos0: null + dragPos0: null, + transitionDuration: 300 }; } @@ -46,12 +47,20 @@ class Timeline extends React.Component { componentWillReceiveProps(nextProps) { if (hash(nextProps) !== hash(this.props)) { const categories = nextProps.domain.categories; - const cats = categories.map((g, i) => (i + 1) * 80 / categories.length); + const catsYpos = categories.map((g, i) => (i + 1) * 80 / categories.length); this.setState({ timerange: nextProps.app.timerange, 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(cats) + scaleY: d3.scaleOrdinal().domain(nextProps.domain.categories).range(catsYpos) + }); + } + } + + componentDidUpdate(prevProps, prevState) { + if (prevState.timerange !== this.state.timerange) { + this.setState({ + scaleX: d3.scaleTime().domain(this.state.timerange).range([this.state.dims.margin_left, this.state.dims.width]) }); } } @@ -90,8 +99,10 @@ class Timeline extends React.Component { const dom = this.props.ui.dom.timeline; if (document.querySelector(`#${dom}`) !== null) { const boundingClient = document.querySelector(`#${dom}`).getBoundingClientRect(); - const WIDTH = boundingClient.width; - this.setState({ dims: Object.assign({}, this.state.dims, { width: WIDTH }) }); + + this.setState({ + dims: Object.assign({}, this.state.dims, { width: boundingClient.width }) + }); } } @@ -114,8 +125,9 @@ class Timeline extends React.Component { domainF = newCentralTime; } - this.state.scaleX.domain([domain0, domainF]); - this.props.methods.onUpdateTimerange(this.state.scaleX.domain()); + this.setState({ timerange: [domain0, domainF] }, () => { + this.props.methods.onUpdateTimerange(this.state.timerange); + }); } /** @@ -135,12 +147,12 @@ class Timeline extends React.Component { const extent = this.getTimeScaleExtent(); const newCentralTime = d3.timeMinute.offset(this.state.scaleX.domain()[0], extent / 2); - this.state.scaleX.domain([ + this.setState({ timerange: [ d3.timeMinute.offset(newCentralTime, -zoom.duration / 2), d3.timeMinute.offset(newCentralTime, zoom.duration / 2) - ]); - - this.props.methods.onUpdateTimerange(this.state.scaleX.domain()); + ]}, () => { + this.props.methods.onUpdateTimerange(this.state.timerange); + }); } toggleTransition(isTransition) { @@ -167,10 +179,8 @@ class Timeline extends React.Component { const dragNow = this.state.scaleX.invert(d3.event.x).getTime(); const timeShift = (drag0 - dragNow) / 1000; - const newDomain0 = d3.timeSecond.offset(this.state.timerange[0], timeShift); - const newDomainF = d3.timeSecond.offset(this.state.timerange[1], timeShift); - - this.state.scaleX.domain([newDomain0, newDomainF]); + 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]); @@ -225,6 +235,7 @@ class Timeline extends React.Component { selected={this.props.app.selected} getEventX={(e) => this.getEventX(e)} getEventY={(e) => this.getEventY(e)} + transitionDuration={this.state.transitionDuration} /> Date: Fri, 4 Jan 2019 08:41:47 +0100 Subject: [PATCH 14/18] Adjust x domain --- src/components/Timeline.jsx | 45 ++++++++++++++++++++---------- src/components/TimelineMarkers.jsx | 2 +- 2 files changed, 31 insertions(+), 16 deletions(-) diff --git a/src/components/Timeline.jsx b/src/components/Timeline.jsx index fde5759..dd21199 100644 --- a/src/components/Timeline.jsx +++ b/src/components/Timeline.jsx @@ -41,27 +41,40 @@ class Timeline extends React.Component { componentDidMount() { this.computeDims(); - window.addEventListener('resize', () => { this.computeDims(); }); + this.addEventListeners(); } componentWillReceiveProps(nextProps) { if (hash(nextProps) !== hash(this.props)) { - const categories = nextProps.domain.categories; - const catsYpos = categories.map((g, i) => (i + 1) * 80 / categories.length); - + console.log(nextProps.domain.categories) this.setState({ timerange: nextProps.app.timerange, - 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(catsYpos) + 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: d3.scaleTime().domain(this.state.timerange).range([this.state.dims.margin_left, this.state.dims.width]) - }); + this.setState({ scaleX: this.makeScaleX() }); } } @@ -78,6 +91,7 @@ class Timeline extends React.Component { * @param {object} eventPoint: regular eventPoint data */ getEventY(eventPoint) { + console.log(eventPoint.category, this.state.scaleY(eventPoint.category)) return this.state.scaleY(eventPoint.category); } @@ -186,13 +200,12 @@ class Timeline extends React.Component { this.onSoftTimeRangeUpdate([newDomain0, newDomainF]); } + /** + * Stop dragging and update data + */ onDragEnd() { this.toggleTransition(true); - this.setState({ - timerange: this.state.scaleX.domain() - }, () => { - this.props.methods.onUpdateTimerange(this.state.scaleX.domain()); - }); + this.props.methods.onUpdateTimerange(this.state.timerange); } renderSVG() { @@ -204,7 +217,9 @@ class Timeline extends React.Component { width={dims.width} height={dims.height} > - + Date: Fri, 4 Jan 2019 15:54:15 +0100 Subject: [PATCH 15/18] Fix marker position --- src/components/Timeline.jsx | 15 +++++++++------ src/components/TimelineCategories.jsx | 2 +- src/components/TimelineMarkers.jsx | 2 +- src/store/initial.js | 10 +++++----- 4 files changed, 16 insertions(+), 13 deletions(-) diff --git a/src/components/Timeline.jsx b/src/components/Timeline.jsx index dd21199..77bd3dd 100644 --- a/src/components/Timeline.jsx +++ b/src/components/Timeline.jsx @@ -46,10 +46,14 @@ class Timeline extends React.Component { componentWillReceiveProps(nextProps) { if (hash(nextProps) !== hash(this.props)) { - console.log(nextProps.domain.categories) this.setState({ timerange: nextProps.app.timerange, - scaleX: this.makeScaleX(), + scaleX: this.makeScaleX() + }); + } + + if (hash(nextProps.domain.categories) !== hash(this.props.domain.categories)) { + this.setState({ scaleY: this.makeScaleY(nextProps.domain.categories) }); } @@ -91,7 +95,6 @@ class Timeline extends React.Component { * @param {object} eventPoint: regular eventPoint data */ getEventY(eventPoint) { - console.log(eventPoint.category, this.state.scaleY(eventPoint.category)) return this.state.scaleY(eventPoint.category); } @@ -145,9 +148,9 @@ class Timeline extends React.Component { } /** - * Shift time range by moving forward or backwards - * WITHOUT updating the store - * @param {String} direction: 'forward' / 'backwards' + * Change display of time range + * WITHOUT updating the store, or data shown. + * Used for updates in the middle of a transition, for performance purposes */ onSoftTimeRangeUpdate(timerange) { this.setState({ timerange }); diff --git a/src/components/TimelineCategories.jsx b/src/components/TimelineCategories.jsx index c84fcb4..d1cd15f 100644 --- a/src/components/TimelineCategories.jsx +++ b/src/components/TimelineCategories.jsx @@ -33,7 +33,7 @@ class TimelineCategories extends React.Component { return ( - {category.category} + {category.category} ) } diff --git a/src/components/TimelineMarkers.jsx b/src/components/TimelineMarkers.jsx index 18a32a2..d79ac88 100644 --- a/src/components/TimelineMarkers.jsx +++ b/src/components/TimelineMarkers.jsx @@ -10,7 +10,7 @@ class TimelineMarkers extends React.Component { cy={0} style={{ 'transform': `translate(${this.props.getEventX(event)}px, ${this.props.getEventY(event)}px)`, - 'transition': `transform ${this.props.transitionDuration}s ease`, + 'transition': `transform ${this.props.transitionDuration / 1000}s ease`, 'opacity': 0.9 }} r="10" diff --git a/src/store/initial.js b/src/store/initial.js index c18baa0..8db1642 100644 --- a/src/store/initial.js +++ b/src/store/initial.js @@ -55,27 +55,27 @@ const initial = { language: 'en-US', mapAnchor: process.env.MAP_ANCHOR, zoomLevels: [{ - label: '3 años', + label: '3 years', duration: 1576800, active: false }, { - label: '3 meses', + label: '3 months', duration: 129600, active: false }, { - label: '3 días', + label: '3 days', duration: 4320, active: false }, { - label: '12 horas', + label: '12 hours', duration: 720, active: false }, { - label: '2 horas', + label: '2 hours', duration: 120, active: false }, From 9fad640fbe5065a17b819a841d1a7e5a374842ae Mon Sep 17 00:00:00 2001 From: Franc Camps-Febrer Date: Fri, 4 Jan 2019 15:59:04 +0100 Subject: [PATCH 16/18] Move labels and handles to presentational --- src/components/Timeline.jsx | 4 +- src/components/TimelineHandles.jsx | 25 ----------- src/components/TimelineLabels.jsx | 21 --------- .../presentational/TimelineHandles.js | 26 +++++++++++ .../presentational/TimelineLabels.js | 44 +++++++++++++++++++ 5 files changed, 72 insertions(+), 48 deletions(-) delete mode 100644 src/components/TimelineHandles.jsx delete mode 100644 src/components/TimelineLabels.jsx create mode 100644 src/components/presentational/TimelineHandles.js create mode 100644 src/components/presentational/TimelineLabels.js diff --git a/src/components/Timeline.jsx b/src/components/Timeline.jsx index 77bd3dd..deafaa3 100644 --- a/src/components/Timeline.jsx +++ b/src/components/Timeline.jsx @@ -8,9 +8,9 @@ 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 TimelineHandles from './presentational/TimelineHandles.js'; import TimelineZoomControls from './TimelineZoomControls.jsx'; -import TimelineLabels from './TimelineLabels.jsx'; +import TimelineLabels from './presentational/TimelineLabels.js'; import TimelineMarkers from './TimelineMarkers.jsx' import TimelineEvents from './TimelineEvents.jsx'; import TimelineCategories from './TimelineCategories.jsx'; diff --git a/src/components/TimelineHandles.jsx b/src/components/TimelineHandles.jsx deleted file mode 100644 index 1e8b5df..0000000 --- a/src/components/TimelineHandles.jsx +++ /dev/null @@ -1,25 +0,0 @@ -import React from 'react'; - -class TimelineHandles extends React.Component { - - render() { - const dims = this.props.dims; - return ( - - this.props.onMoveTime('backwards')}> - - - - - this.props.onMoveTime('forward')}> - - - - - - ) - } - -} - -export default TimelineHandles; \ No newline at end of file diff --git a/src/components/TimelineLabels.jsx b/src/components/TimelineLabels.jsx deleted file mode 100644 index 7390acf..0000000 --- a/src/components/TimelineLabels.jsx +++ /dev/null @@ -1,21 +0,0 @@ -import React from 'react'; - -import { formatterWithYear } from '../js/utilities'; - -const TimelineLabels = ({ dims, timelabels }) => { - - return ( - - - - - {formatterWithYear(timelabels[0])} - - - {formatterWithYear(timelabels[1])} - - - ) -} - -export default TimelineLabels; \ No newline at end of file diff --git a/src/components/presentational/TimelineHandles.js b/src/components/presentational/TimelineHandles.js new file mode 100644 index 0000000..30e0ca2 --- /dev/null +++ b/src/components/presentational/TimelineHandles.js @@ -0,0 +1,26 @@ +import React from 'react'; + +const TimelineHandles = ({ dims, onMoveTime }) => { + + return ( + + this.props.onMoveTime('backwards')} + > + + + + this.props.onMoveTime('forward')} + > + + + + + ) + +} + +export default TimelineHandles; \ No newline at end of file diff --git a/src/components/presentational/TimelineLabels.js b/src/components/presentational/TimelineLabels.js new file mode 100644 index 0000000..bd6f8ad --- /dev/null +++ b/src/components/presentational/TimelineLabels.js @@ -0,0 +1,44 @@ +import React from 'react'; + +import { formatterWithYear } from '../js/utilities'; + +const TimelineLabels = ({ dims, timelabels }) => { + + return ( + + + + + + + {formatterWithYear(timelabels[0])} + + + {formatterWithYear(timelabels[1])} + + + ) +} + +export default TimelineLabels; \ No newline at end of file From 8fe6600c67381a114aae89d72e1dc576258d6f43 Mon Sep 17 00:00:00 2001 From: Franc Camps-Febrer Date: Fri, 4 Jan 2019 19:44:13 +0100 Subject: [PATCH 17/18] Make some timeline into presentational comps --- src/components/Timeline.jsx | 7 ++-- src/components/TimelineEvents.jsx | 41 ------------------- src/components/TimelineMarkers.jsx | 33 --------------- src/components/TimelineZoomControls.jsx | 29 ------------- .../presentational/TimelineEvents.js | 40 ++++++++++++++++++ .../presentational/TimelineHandles.js | 4 +- .../presentational/TimelineLabels.js | 2 +- .../presentational/TimelineMarkers.js | 31 ++++++++++++++ .../presentational/TimelineZoomControls.js | 25 +++++++++++ 9 files changed, 102 insertions(+), 110 deletions(-) delete mode 100644 src/components/TimelineEvents.jsx delete mode 100644 src/components/TimelineMarkers.jsx delete mode 100644 src/components/TimelineZoomControls.jsx create mode 100644 src/components/presentational/TimelineEvents.js create mode 100644 src/components/presentational/TimelineMarkers.js create mode 100644 src/components/presentational/TimelineZoomControls.js diff --git a/src/components/Timeline.jsx b/src/components/Timeline.jsx index deafaa3..1eb8e76 100644 --- a/src/components/Timeline.jsx +++ b/src/components/Timeline.jsx @@ -9,10 +9,10 @@ import TimelineHeader from './presentational/TimelineHeader'; import TimelineAxis from './TimelineAxis.jsx'; import TimelineClip from './presentational/TimelineClip'; import TimelineHandles from './presentational/TimelineHandles.js'; -import TimelineZoomControls from './TimelineZoomControls.jsx'; +import TimelineZoomControls from './presentational/TimelineZoomControls.js'; import TimelineLabels from './presentational/TimelineLabels.js'; -import TimelineMarkers from './TimelineMarkers.jsx' -import TimelineEvents from './TimelineEvents.jsx'; +import TimelineMarkers from './presentational/TimelineMarkers.js' +import TimelineEvents from './presentational/TimelineEvents.js'; import TimelineCategories from './TimelineCategories.jsx'; class Timeline extends React.Component { @@ -30,7 +30,6 @@ class Timeline extends React.Component { margin_top: 20, trackHeight: 80 }, - softTimeUpdate: 0, scaleX: null, scaleY: null, timerange: [null, null], diff --git a/src/components/TimelineEvents.jsx b/src/components/TimelineEvents.jsx deleted file mode 100644 index 9b1a752..0000000 --- a/src/components/TimelineEvents.jsx +++ /dev/null @@ -1,41 +0,0 @@ -import React from 'react'; - -class TimelineEvents extends React.Component { - - getAllEventsAtOnce(eventPoint) { - const timestamp = eventPoint.timestamp; - const category = eventPoint.category; - return this.props.events - .filter(event => (event.timestamp === timestamp && category === event.category)) - } - - renderEvent(event) { - return ( - {this.props.onSelect(this.getAllEventsAtOnce(event))}} - > - - ) - } - - render () { - return ( - - {this.props.events.map(event => this.renderEvent(event))} - - ); - } -} - -export default TimelineEvents; \ No newline at end of file diff --git a/src/components/TimelineMarkers.jsx b/src/components/TimelineMarkers.jsx deleted file mode 100644 index d79ac88..0000000 --- a/src/components/TimelineMarkers.jsx +++ /dev/null @@ -1,33 +0,0 @@ -import React from 'react'; - -class TimelineMarkers extends React.Component { - - renderMarker(event) { - return ( - - - ) - } - - render () { - return ( - - {this.props.selected.map(event => this.renderMarker(event))} - - ); - } -} - -export default TimelineMarkers; \ No newline at end of file diff --git a/src/components/TimelineZoomControls.jsx b/src/components/TimelineZoomControls.jsx deleted file mode 100644 index 51ce7a7..0000000 --- a/src/components/TimelineZoomControls.jsx +++ /dev/null @@ -1,29 +0,0 @@ -import React from 'react'; - -class TimelineZoomControls extends React.Component { - - renderZoom(zoom, idx) { - return ( - this.props.onApplyZoom(zoom)} - > - {zoom.label} - - ) - } - - render() { - const dims = this.props.dims; - - return ( - - {this.props.zoomLevels.map((z, idx) => this.renderZoom(z, idx))} - - ); - } -} - -export default TimelineZoomControls; \ No newline at end of file diff --git a/src/components/presentational/TimelineEvents.js b/src/components/presentational/TimelineEvents.js new file mode 100644 index 0000000..5dc8dce --- /dev/null +++ b/src/components/presentational/TimelineEvents.js @@ -0,0 +1,40 @@ +import React from 'react'; + +const TimelineEvents = ({ events, getEventX, getEventY, + getCategoryColor, onSelect, transitionDuration }) => { + + function getAllEventsAtOnce(eventPoint) { + const timestamp = eventPoint.timestamp; + const category = eventPoint.category; + return events + .filter(event => (event.timestamp === timestamp && category === event.category)) + } + + function renderEvent(event) { + return ( + {onSelect(getAllEventsAtOnce(event))}} + > + + ) + } + + return ( + + {events.map(event => renderEvent(event))} + + ); +} + +export default TimelineEvents; \ No newline at end of file diff --git a/src/components/presentational/TimelineHandles.js b/src/components/presentational/TimelineHandles.js index 30e0ca2..3e65f9b 100644 --- a/src/components/presentational/TimelineHandles.js +++ b/src/components/presentational/TimelineHandles.js @@ -6,14 +6,14 @@ const TimelineHandles = ({ dims, onMoveTime }) => { this.props.onMoveTime('backwards')} + onClick={() => onMoveTime('backwards')} > this.props.onMoveTime('forward')} + onClick={() => onMoveTime('forward')} > diff --git a/src/components/presentational/TimelineLabels.js b/src/components/presentational/TimelineLabels.js index bd6f8ad..3f165aa 100644 --- a/src/components/presentational/TimelineLabels.js +++ b/src/components/presentational/TimelineLabels.js @@ -1,6 +1,6 @@ import React from 'react'; -import { formatterWithYear } from '../js/utilities'; +import { formatterWithYear } from '../../js/utilities.js'; const TimelineLabels = ({ dims, timelabels }) => { diff --git a/src/components/presentational/TimelineMarkers.js b/src/components/presentational/TimelineMarkers.js new file mode 100644 index 0000000..7126015 --- /dev/null +++ b/src/components/presentational/TimelineMarkers.js @@ -0,0 +1,31 @@ +import React from 'react'; + +const TimelineMarkers = ({ getEventX, getEventY, transitionDuration, selected }) => { + function renderMarker(event) { + return ( + + + ) + } + + return ( + + {selected.map(event => renderMarker(event))} + + ); +} + +export default TimelineMarkers; \ No newline at end of file diff --git a/src/components/presentational/TimelineZoomControls.js b/src/components/presentational/TimelineZoomControls.js new file mode 100644 index 0000000..9b4f6c2 --- /dev/null +++ b/src/components/presentational/TimelineZoomControls.js @@ -0,0 +1,25 @@ +import React from 'react'; + +const TimelineZoomControls = ({ zoomLevels, dims, onApplyZoom }) => { + + function renderZoom(zoom, idx) { + return ( + onApplyZoom(zoom)} + > + {zoom.label} + + ) + } + + return ( + + {zoomLevels.map((z, idx) => renderZoom(z, idx))} + + ); +} + +export default TimelineZoomControls; \ No newline at end of file From 7168cd10f1e011a04f97f41e2107042818097e70 Mon Sep 17 00:00:00 2001 From: Franc Camps-Febrer Date: Mon, 7 Jan 2019 08:25:32 +0100 Subject: [PATCH 18/18] Dim events on timeline in narrative mode --- src/components/Timeline.jsx | 1 + .../presentational/TimelineEvents.js | 27 ++++++++++++++----- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/src/components/Timeline.jsx b/src/components/Timeline.jsx index 1eb8e76..e68db1d 100644 --- a/src/components/Timeline.jsx +++ b/src/components/Timeline.jsx @@ -256,6 +256,7 @@ class Timeline extends React.Component { /> this.getEventX(e)} getEventY={(e) => this.getEventY(e)} getCategoryColor={this.props.methods.getCategoryColor} diff --git a/src/components/presentational/TimelineEvents.js b/src/components/presentational/TimelineEvents.js index 5dc8dce..2ce4086 100644 --- a/src/components/presentational/TimelineEvents.js +++ b/src/components/presentational/TimelineEvents.js @@ -1,6 +1,6 @@ import React from 'react'; -const TimelineEvents = ({ events, getEventX, getEventY, +const TimelineEvents = ({ events, narrative, getEventX, getEventY, getCategoryColor, onSelect, transitionDuration }) => { function getAllEventsAtOnce(eventPoint) { @@ -11,17 +11,32 @@ const TimelineEvents = ({ events, getEventX, getEventY, } function renderEvent(event) { + let styleProps = ({ + fill: getCategoryColor(event.category), + fillOpacity: 0.8, + transform: `translate(${getEventX(event)}px, ${getEventY(event)}px)`, + transition: `transform ${transitionDuration / 1000}s ease` + }); + + if (narrative) { + const { steps } = narrative + const isInNarrative = steps.map(s => s.id).includes(event.id) + + if (!isInNarrative) { + styleProps = { + ...styleProps, + fillOpacity: 0.1 + } + } + } + return ( {onSelect(getAllEventsAtOnce(event))}} >