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 }) => (
-
-
-
-
{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,
};
}