From 44b0dfb57eedcc974cecf7f54ff9bbe0871a5cb6 Mon Sep 17 00:00:00 2001 From: Lachlan Kermode Date: Wed, 12 Dec 2018 16:38:15 +0000 Subject: [PATCH] Revert "Refactor Timeline component and timeline, mapping logic" --- src/components/Timeline.jsx | 42 +++- .../presentational/TimelineHeader.js | 15 -- src/js/map/map.js | 51 ++-- src/js/timeline/timeline.js | 222 ++++++++---------- 4 files changed, 144 insertions(+), 186 deletions(-) delete mode 100644 src/components/presentational/TimelineHeader.js diff --git a/src/components/Timeline.jsx b/src/components/Timeline.jsx index e2139a5..273fc98 100644 --- a/src/components/Timeline.jsx +++ b/src/components/Timeline.jsx @@ -4,7 +4,6 @@ import * as selectors from '../selectors'; import copy from '../js/data/copy.json'; import { formatterWithYear } from '../js/utilities'; -import TimelineHeader from './presentational/TimelineHeader'; import TimelineLogic from '../js/timeline/timeline.js'; class Timeline extends React.Component { @@ -16,12 +15,18 @@ class Timeline extends React.Component { } componentDidMount() { - this.timeline = new TimelineLogic(this.props.app, this.props.ui, this.props.methods); + const ui = { + dom: this.props.dom + } + + this.timeline = new TimelineLogic(this.props.app, ui, this.props.methods); this.timeline.update(this.props.domain, this.props.app); + this.timeline.render(this.props.domain); } componentWillReceiveProps(nextProps) { this.timeline.update(nextProps.domain, nextProps.app); + this.timeline.render(nextProps.domain); } onClickArrow() { @@ -30,19 +35,34 @@ class Timeline extends React.Component { }); } + renderLabels() { + const labels = copy[this.props.language].timeline.labels; + return this.props.categories.map((label) => { + const groupLen = this.props.categories.length + return (
{label}
); + }); + } + render() { + const labels_title_lang = copy[this.props.app.language].timeline.labels_title; + const info_lang = copy[this.props.app.language].timeline.info; let classes = `timeline-wrapper ${(this.state.isFolded) ? ' folded' : ''}`; + const date0 = formatterWithYear(this.props.app.timerange[0]); + const date1 = formatterWithYear(this.props.app.timerange[1]); return (
- { this.onClickArrow(); }} - /> +
+
this.onClickArrow()}> +

+
+
+

{info_lang}

+

{date0} - {date1}

+
+
-
+
); @@ -62,9 +82,7 @@ function mapStateToProps(state) { language: state.app.language, zoomLevels: state.app.zoomLevels }, - ui: { - dom: state.ui.dom, - } + dom: state.ui.dom, } } diff --git a/src/components/presentational/TimelineHeader.js b/src/components/presentational/TimelineHeader.js deleted file mode 100644 index d75a605..0000000 --- a/src/components/presentational/TimelineHeader.js +++ /dev/null @@ -1,15 +0,0 @@ -import React from 'react'; - -const TimelineHeader = ({ title, date0, date1, onClick }) => ( -
-
onClick()}> -

-
-
-

{title}

-

{date0} - {date1}

-
-
-); - -export default TimelineHeader; diff --git a/src/js/map/map.js b/src/js/map/map.js index e16b149..3e30e93 100644 --- a/src/js/map/map.js +++ b/src/js/map/map.js @@ -122,49 +122,40 @@ Stop and start the development process in terminal after you have added your tok } function getSVGBoundaries() { - const mapNode = d3.select('.leaflet-map-pane').node(); - - // We'll get the transform of the leaflet container, - // which will let us offset the SVG by the same quantity - const transform = window - .getComputedStyle(mapNode) - .getPropertyValue('transform'); - - // However getComputedStyle returns an awkward string of the format - // matrix(0, 0, 1, 0, 0.56523, 123123), hence this awkwardness return { - transformX: +transform.split(',')[4], - transformY: +transform.split(',')[5].substring(0, transform.split(',')[5].length - 2) + topLeft: projectPoint(maxBoundaries[0]), + bottomRight: projectPoint(maxBoundaries[1]) } } function updateSVG() { - const boundingClient = d3.select(`#${ui.dom.map}`).node().getBoundingClientRect(); - let WIDTH = boundingClient.width; - let HEIGHT = boundingClient.height; + const boundaries = getSVGBoundaries(); + const { + topLeft, + bottomRight + } = boundaries; + svg.attr('width', bottomRight.x - topLeft.x + 200) + .attr('height', bottomRight.y - topLeft.y + 200) + .style('left', `${topLeft.x - 100}px`) + .style('top', `${topLeft.y - 100}px`); - // Offset with leaflet map transform boundaries - const { transformX, transformY } = getSVGBoundaries(); - - svg.attr('width', WIDTH) - .attr('height', HEIGHT) - .style('left', -transformX) - .style('top', -transformY) + g.attr('transform', `translate(${-(topLeft.x - 100)},${-(topLeft.y - 100)})`); g.selectAll('.location').attr('transform', (d) => { const newPoint = projectPoint([+d.latitude, +d.longitude]); - return `translate(${newPoint.x + transformX},${newPoint.y + transformY})`; + return `translate(${newPoint.x},${newPoint.y})`; }); - const sequenceLine = d3.line() - .x(d => getCoords(d).x + transformX) - .y(d => getCoords(d).y + transformY); - g.selectAll('.narrative') .attr('d', sequenceLine); + + const busLine = d3.line() + .x(d => lMap.latLngToLayerPoint(d).x) + .y(d => lMap.latLngToLayerPoint(d).y) + .curve(d3.curveMonotoneX); } - lMap.on("zoomend viewreset moveend", updateSVG); + lMap.on("zoom viewreset move", updateSVG); /** * Returns latitud / longitude @@ -233,7 +224,9 @@ Stop and start the development process in terminal after you have added your tok function getLocationEventsDistribution(location) { const eventCount = {}; const categories = domain.categories; - + // categories.sort((a, b) => { + // return (+a.slice(-2) > +b.slice(-2)); + // }); categories.forEach(cat => { eventCount[cat.category] = 0 }); diff --git a/src/js/timeline/timeline.js b/src/js/timeline/timeline.js index 3598c94..8a009e9 100644 --- a/src/js/timeline/timeline.js +++ b/src/js/timeline/timeline.js @@ -9,49 +9,54 @@ import { parseDate, formatterWithYear } from '../utilities'; -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(app, ui, methods) { d3.timeFormatDefaultLocale(esLocale); - const domain = { - events: [], - categories: [], - } - const app = { - selected: [], - highlighted: null, - zoomLevels: newApp.zoomLevels, - timerange: newApp.timerange, - language: newApp.language - } - - // 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; - - // 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 zoomLevels = app.zoomLevels; + let events = []; + let categories = []; + let selected = []; + let timerange = app.timerange; // Drag behavior let dragPos0; let transitionDuration = 500; + // Dimension of the client + const WIDTH_CONTROLS = 100; + const boundingClient = d3.select(`#${ui.dom.timeline}`).node().getBoundingClientRect(); + let WIDTH = boundingClient.width - WIDTH_CONTROLS; + const HEIGHT = 140; + const markerRadius = 15; + // margin + // NB: is it possible to do this with SCSS? + // A: Maybe, although we are using it programmatically here for now + const mg = { + l: 120 + }; + /** * Create scales */ const scale = {}; - scale.x = d3.scaleTime(); - scale.y = d3.scaleOrdinal() + scale.x = d3.scaleTime() + .domain(timerange) + .range([mg.l, WIDTH]); + + // calculate group step between categories + const groupStep = (106 - 30) / categories.length; + const groupYs = new Array(categories.length); + groupYs.map((g, i) => { + return 30 + i * groupStep; + }); + + scale.y = d3.scaleOrdinal() + .domain(categories) + .range(groupYs); /** * Initilize SVG elements and groups @@ -71,10 +76,10 @@ export default function(newApp, ui, methods) { .attr('width', WIDTH_CONTROLS) .attr('height', HEIGHT); - /* * Axis group elements */ + dom.axis = {}; dom.axis.x0 = dom.svg.append('g') @@ -100,14 +105,11 @@ export default function(newApp, ui, methods) { dom.axis.label1 = dom.svg.append('text') .attr('class', 'timelabelF timeLabel'); - /* * Plottable elements */ dom.dataset = dom.svg.append('g'); dom.events = dom.dataset.append('g'); - dom.markers = dom.svg.append('g'); - /* * Time Controls @@ -123,11 +125,10 @@ export default function(newApp, ui, methods) { dom.zooms = dom.controls.append('g'); dom.zooms.selectAll('.zoom-level-button') - .data(app.zoomLevels) + .data(zoomLevels) .enter().append('text') .attr('class', 'zoom-level-button'); - /* * Initialize axis function and element group */ @@ -151,8 +152,37 @@ export default function(newApp, ui, methods) { d3.axisLeft(scale.y) .tickValues([]); - updateAxis(); + /* + * Setup drag behavior + */ + 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(timerange[0], timeShift); + const newDomainF = d3.timeSecond.offset(timerange[1], timeShift); + + scale.x.domain([newDomain0, newDomainF]) + render(); + }) + .on('end', () => { + toggleTransition(true); + methods.onUpdateTimerange(scale.x.domain()); + }); + + /* + * SVG groups for marker + */ + + dom.markers = dom.svg.append('g'); /** * Adapt dimensions when resizing @@ -162,7 +192,6 @@ export default function(newApp, ui, methods) { .getBoundingClientRect().width; } - /** * Resize timeline one window resice */ @@ -172,8 +201,8 @@ export default function(newApp, ui, methods) { WIDTH = getCurrentWidth() - WIDTH_CONTROLS; dom.svg.attr('width', WIDTH); - scale.x.range([margin.left, WIDTH]); - axis.y.tickSize(WIDTH - margin.left); + scale.x.range([mg.l, WIDTH]); + axis.y.tickSize(WIDTH - mg.l); dom.axis.y.attr('transform', `translate(${WIDTH}, 0)`) render(null); } @@ -181,7 +210,6 @@ export default function(newApp, ui, methods) { } addResizeListener(); - /** * Return which color event circle should be based on incident type * @param {object} eventPoint data object @@ -190,7 +218,6 @@ export default function(newApp, ui, methods) { return methods.getCategoryColor(eventPoint.category); } - /** * Given an event, get all the filtered events that happen simultaneously * @param {object} eventPoint: regular eventPoint data @@ -198,11 +225,10 @@ export default function(newApp, ui, methods) { function getAllEventsAtOnce(eventPoint) { const timestamp = eventPoint.timestamp; const category = eventPoint.category; - return domain.events + return events .filter(event => (event.timestamp === timestamp && category === event.category)) } - /* * Get y height of eventPoint, considering the ordinal Y scale * @param {object} eventPoint: regular eventPoint data @@ -212,7 +238,6 @@ export default function(newApp, ui, methods) { return scale.y(yGroup); } - /* * Get x position of eventPoint, considering the time scale * @param {object} eventPoint: regular eventPoint data @@ -225,7 +250,6 @@ export default function(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 @@ -235,12 +259,8 @@ export default function(newApp, ui, methods) { return (minutes * WIDTH) / allMins; } - - /* - * TODO: Highlight zoom level based on time range selected - */ function highlightZoomLevel(zoom) { - app.zoomLevels.forEach((level) => { + zoomLevels.forEach((level) => { if (level.label === zoom.label) { level.active = true; } else { @@ -252,7 +272,6 @@ export default function(newApp, ui, methods) { .classed('active', level => level.active); } - /** * Apply zoom level to timeline * @param {object} zoom: zoom level from zoomLevels @@ -270,7 +289,6 @@ export default function(newApp, ui, methods) { methods.onUpdateTimerange(scale.x.domain()); } - /** * Shift time range by moving forward or backwards * @param {String} direction: 'forward' / 'backwards' @@ -298,33 +316,6 @@ export default function(newApp, ui, methods) { transitionDuration = (isTransition) ? 500 : 0; } - - /* - * Setup drag behavior - */ - 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(); - }) - .on('end', () => { - toggleTransition(true); - methods.onUpdateTimerange(scale.x.domain()); - }); - - /** * Highlight event circle on hover */ @@ -334,7 +325,6 @@ export default function(newApp, ui, methods) { .classed('mouseover', true); } - /** * Unhighlight event when mouse out */ @@ -344,12 +334,11 @@ export default function(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); + scale.x.domain(timerange); axis.x0.scale(scale.x); axis.x1.scale(scale.x); } @@ -362,24 +351,23 @@ export default function(newApp, ui, methods) { dom.axis.label0 .attr('x', 5) .attr('y', 15) - .text(formatterWithYear(app.timerange[0])); + .text(formatterWithYear(timerange[0])); dom.axis.label1 .attr('x', WIDTH - 5) .attr('y', 15) - .text(formatterWithYear(app.timerange[1])) + .text(formatterWithYear(timerange[1])) .style('text-anchor', 'end'); } - /** - * Makes a circular ring mark in all selected events + * Makes a circular rinig mark in one particular location at a time * @param {object} eventPoint: object with eventPoint data (time, loc, tags) */ function renderHighlight() { const markers = dom.markers .selectAll('circle') - .data(app.selected); + .data(selected); markers .enter() @@ -394,14 +382,13 @@ export default function(newApp, ui, methods) { markers.exit().remove(); } - /** * Return event circles of different groups */ function renderEvents() { const eventsDom = dom.events .selectAll('.event') - .data(domain.events, d => d.id); + .data(events, d => d.id); eventsDom .exit() @@ -430,7 +417,6 @@ export default function(newApp, ui, methods) { .attr('r', 5); } - /** * Render axis on timeline and viewbox boundaries */ @@ -451,7 +437,7 @@ export default function(newApp, ui, methods) { .duration(transitionDuration) .call(axis.x1); - axis.y.tickSize(WIDTH - margin.left); + axis.y.tickSize(WIDTH - mg.l); dom.axis.y .call(axis.y) @@ -467,13 +453,12 @@ export default function(newApp, ui, methods) { .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) => { + zoomLevels.forEach((level, i) => { level.label = zoomLabels[i]; }); @@ -510,72 +495,49 @@ export default function(newApp, ui, methods) { .on('click', zoom => applyZoom(zoom)); } - /** * 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() { - scale.x = d3.scaleTime() - .domain(app.timerange) - .range([margin.left, WIDTH]); - - const groupStep = (106 - 30) / domain.categories.length; - let groupYs = Array.apply(null, Array(domain.categories.length)); + function updateAxis(domain) { + const categories = domain.categories + const groupStep = (106 - 30) / categories.length; + let groupYs = Array.apply(null, Array(categories.length)); groupYs = groupYs.map((g, i) => { return 30 + i * groupStep; }); scale.y = d3.scaleOrdinal() - .domain(domain.categories) + .domain(categories) .range(groupYs); axis.y = d3.axisLeft(scale.y) - .tickValues(domain.categories.map(c => c.category)); + .tickValues(categories.map(c => c.category)); } + function update(domain, app) { + updateAxis(domain); + renderAxis(); - /** - * 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 - */ - function update(newDomain, newApp) { - if (hash(domain) !== hash(newDomain)) { - domain.categories = newDomain.categories; - domain.events = newDomain.events; - updateAxis(); - renderContext(); - } - if (hash(app) !== hash(newApp)) { - app.timerange = newApp.timerange; - app.selected = newApp.selected.slice(0); - updateTimeRange(); - renderTimeLabels(); - renderEventsAndHighlight(); - } + events = domain.events; + timerange = app.timerange; + selected = app.selected.slice(0); + updateTimeRange(); } - function renderContext() { + function render() { renderAxis(); renderTimeControls(); renderTimeLabels(); - } - function renderEventsAndHighlight() { renderEvents(); renderHighlight(); } - function render() { - renderContext(); - renderEventsAndHighlight(); - } - return { update, + render, }; }