Revert "Refactor Timeline component and timeline, mapping logic"

This commit is contained in:
Lachlan Kermode
2018-12-12 16:38:15 +00:00
committed by GitHub
parent 9d4909dea8
commit 44b0dfb57e
4 changed files with 144 additions and 186 deletions

View File

@@ -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 (<div className="timeline-label">{label}</div>);
});
}
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 (
<div className={classes}>
<TimelineHeader
title={copy[this.props.app.language].timeline.info}
date0={formatterWithYear(this.props.app.timerange[0])}
date1={formatterWithYear(this.props.app.timerange[1])}
onClick={() => { this.onClickArrow(); }}
/>
<div className="timeline-header">
<div className="timeline-toggle" onClick={() => this.onClickArrow()}>
<p><i className="arrow-down"></i></p>
</div>
<div className="timeline-info">
<p>{info_lang}</p>
<p>{date0} - {date1}</p>
</div>
</div>
<div className="timeline-content">
<div id={this.props.ui.dom.timeline} className="timeline" />
<div id="timeline" className="timeline" />
</div>
</div>
);
@@ -62,9 +82,7 @@ function mapStateToProps(state) {
language: state.app.language,
zoomLevels: state.app.zoomLevels
},
ui: {
dom: state.ui.dom,
}
dom: state.ui.dom,
}
}

View File

@@ -1,15 +0,0 @@
import React from 'react';
const TimelineHeader = ({ title, date0, date1, onClick }) => (
<div className="timeline-header">
<div className="timeline-toggle" onClick={() => onClick()}>
<p><i className="arrow-down"></i></p>
</div>
<div className="timeline-info">
<p>{title}</p>
<p>{date0} - {date1}</p>
</div>
</div>
);
export default TimelineHeader;

View File

@@ -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
});

View File

@@ -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,
};
}