mirror of
https://github.com/bellingcat/ukraine-timemap.git
synced 2026-06-08 03:18:36 +03:00
WIP: gritty rewrite of timestamp handling
This commit is contained in:
@@ -21,6 +21,7 @@
|
||||
"js-yaml": "^3.13.1",
|
||||
"leaflet": "^1.0.3",
|
||||
"marked": "^0.7.0",
|
||||
"moment": "^2.26.0",
|
||||
"normalizr": "^3.2.3",
|
||||
"npm-check-updates": "^3.1.20",
|
||||
"object-hash": "^1.3.0",
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
import { timeFormat, timeParse } from 'd3'
|
||||
|
||||
import moment from 'moment'
|
||||
|
||||
let { DATE_FMT, TIME_FMT } = process.env
|
||||
if (!DATE_FMT) DATE_FMT = 'MM/DD/YYYY'
|
||||
if (!TIME_FMT) TIME_FMT = 'HH:mm'
|
||||
|
||||
export function calcDatetime (date, time) {
|
||||
if (!time) time = '00:00'
|
||||
const dt = moment(`${date} ${time}`, `${DATE_FMT} ${TIME_FMT}`)
|
||||
return dt.toDate()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get URI params to start with predefined set of
|
||||
* https://stackoverflow.com/questions/901115/how-can-i-get-query-string-values-in-javascript
|
||||
@@ -39,10 +51,9 @@ export function isNotNullNorUndefined (variable) {
|
||||
}
|
||||
|
||||
/*
|
||||
* Capitalizes _only_ the first letter of a string
|
||||
* Taken from: https://stackoverflow.com/questions/1026069/how-do-i-make-the-first-letter-of-a-string-uppercase-in-javascript
|
||||
*/
|
||||
export function capitalizeFirstLetter (string) {
|
||||
export function capitalize (string) {
|
||||
return string.charAt(0).toUpperCase() + string.slice(1)
|
||||
}
|
||||
|
||||
@@ -53,34 +64,6 @@ export function trimAndEllipse (string, stringNum) {
|
||||
return string
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a Date object given a datetime string of the format: "2016-09-10T07:00:00"
|
||||
* @param {string} datetime
|
||||
*/
|
||||
export function parseDate (datetime) {
|
||||
return new Date(datetime.slice(0, 4),
|
||||
datetime.slice(5, 7) - 1,
|
||||
datetime.slice(8, 10),
|
||||
datetime.slice(11, 13),
|
||||
datetime.slice(14, 16),
|
||||
datetime.slice(17, 19)
|
||||
)
|
||||
}
|
||||
|
||||
export function formatterWithYear (datetime) {
|
||||
return timeFormat('%d %b %Y, %H:%M')(datetime)
|
||||
}
|
||||
|
||||
export function formatter (datetime) {
|
||||
return timeFormat('%d %b, %H:%M')(datetime)
|
||||
}
|
||||
|
||||
export const parseTimestamp = ts => timeParse('%Y-%m-%dT%H:%M:%S')(ts)
|
||||
|
||||
export function compareTimestamp (a, b) {
|
||||
return (parseTimestamp(a.timestamp) > parseTimestamp(b.timestamp))
|
||||
}
|
||||
|
||||
/**
|
||||
* Inset the full source represenation from 'allSources' into an event. The
|
||||
* function is 'curried' to allow easy use with maps. To use for a single
|
||||
@@ -184,9 +167,17 @@ export function calcOpacity (num) {
|
||||
return base + (Math.min(0.5, 0.08 * (num - 1)))
|
||||
}
|
||||
|
||||
export const dateMin = function () { return Array.prototype.slice.call(arguments).reduce(function (a, b) { return a < b ? a : b }) }
|
||||
export const dateMin = function () {
|
||||
return Array.prototype.slice.call(arguments).reduce(function (a, b) {
|
||||
return a < b ? a : b
|
||||
})
|
||||
}
|
||||
|
||||
export const dateMax = function () { return Array.prototype.slice.call(arguments).reduce(function (a, b) { return a > b ? a : b }) }
|
||||
export const dateMax = function () {
|
||||
return Array.prototype.slice.call(arguments).reduce(function (a, b) {
|
||||
return a > b ? a : b
|
||||
})
|
||||
}
|
||||
|
||||
/** Taken from
|
||||
* https://stackoverflow.com/questions/22697936/binary-search-in-javascript
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
import copy from '../common/data/copy.json'
|
||||
import {
|
||||
parseDate,
|
||||
formatterWithYear
|
||||
} from '../common/utilities'
|
||||
import React from 'react'
|
||||
|
||||
import CardTimestamp from './presentational/Card/Timestamp'
|
||||
import CardTime from './presentational/Card/Time'
|
||||
import CardLocation from './presentational/Card/Location'
|
||||
import CardCaret from './presentational/Card/Caret'
|
||||
import CardTags from './presentational/Card/Tags'
|
||||
@@ -27,11 +23,9 @@ class Card extends React.Component {
|
||||
})
|
||||
}
|
||||
|
||||
makeTimelabel (timestamp) {
|
||||
if (timestamp === null) return null
|
||||
const parsedTimestamp = parseDate(timestamp)
|
||||
const timelabel = formatterWithYear(parsedTimestamp)
|
||||
return timelabel
|
||||
makeTimelabel (datetime) {
|
||||
if (datetime === null) return null
|
||||
return datetime.toLocaleDateString()
|
||||
}
|
||||
|
||||
renderSummary () {
|
||||
@@ -87,8 +81,8 @@ class Card extends React.Component {
|
||||
}
|
||||
|
||||
// NB: should be internaionalized.
|
||||
renderTimestamp () {
|
||||
let timelabel = this.makeTimelabel(this.props.event.timestamp)
|
||||
renderTime() {
|
||||
let timelabel = this.makeTimelabel(this.props.event.datetime)
|
||||
|
||||
let precision = this.props.event.time_display
|
||||
if (precision === '_date_only') {
|
||||
@@ -104,7 +98,7 @@ class Card extends React.Component {
|
||||
}
|
||||
|
||||
return (
|
||||
<CardTimestamp
|
||||
<CardTime
|
||||
makeTimelabel={timelabel}
|
||||
language={this.props.language}
|
||||
timelabel={timelabel}
|
||||
@@ -132,7 +126,7 @@ class Card extends React.Component {
|
||||
return (
|
||||
<div className='card-container'>
|
||||
<div className='card-row details'>
|
||||
{this.renderTimestamp()}
|
||||
{this.renderTime()}
|
||||
{this.renderLocation()}
|
||||
</div>
|
||||
{this.renderSummary()}
|
||||
|
||||
@@ -16,7 +16,7 @@ import Notification from './Notification.jsx'
|
||||
import StaticPage from './StaticPage'
|
||||
import TemplateCover from './TemplateCover'
|
||||
|
||||
import { parseDate, binarySearch } from '../common/utilities'
|
||||
import { binarySearch } from '../common/utilities'
|
||||
import { isMobile } from 'react-device-detect'
|
||||
|
||||
class Dashboard extends React.Component {
|
||||
@@ -61,17 +61,18 @@ class Dashboard extends React.Component {
|
||||
const idx = binarySearch(
|
||||
events,
|
||||
selected,
|
||||
(e1, e2) => new Date(e1.timestamp) - new Date(e2.timestamp)
|
||||
(e1, e2) => e1.datetime - e2.datetime
|
||||
)
|
||||
// check events before
|
||||
let ptr = idx - 1
|
||||
while (events[idx].timestamp === events[ptr].timestamp) {
|
||||
console.log(events)
|
||||
while (events[idx].datetime === events[ptr].datetime) {
|
||||
matchedEvents.push(events[ptr])
|
||||
ptr -= 1
|
||||
}
|
||||
// check events after
|
||||
ptr = idx + 1
|
||||
while (events[idx].timestamp === events[ptr].timestamp) {
|
||||
while (events[idx].datetime === events[ptr].datetime) {
|
||||
matchedEvents.push(events[ptr])
|
||||
ptr += 1
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import * as selectors from '../selectors'
|
||||
import hash from 'object-hash'
|
||||
|
||||
import copy from '../common/data/copy.json'
|
||||
import { formatterWithYear, parseDate } from '../common/utilities'
|
||||
import Header from './presentational/Timeline/Header'
|
||||
import Axis from './TimelineAxis.jsx'
|
||||
import Clip from './presentational/Timeline/Clip'
|
||||
@@ -28,14 +27,13 @@ class Timeline extends React.Component {
|
||||
dims: props.dimensions,
|
||||
scaleX: null,
|
||||
scaleY: null,
|
||||
timerange: [null, null],
|
||||
timerange: [null, null], // two datetimes
|
||||
dragPos0: null,
|
||||
transitionDuration: 300
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this.computeDims()
|
||||
this.addEventListeners()
|
||||
}
|
||||
|
||||
@@ -60,7 +58,7 @@ class Timeline extends React.Component {
|
||||
|
||||
if (hash(nextProps.app.selected) !== hash(this.props.app.selected)) {
|
||||
if (!!nextProps.app.selected && nextProps.app.selected.length > 0) {
|
||||
this.onCenterTime(parseDate(nextProps.app.selected[0].timestamp))
|
||||
this.onCenterTime(nextProps.app.selected[0].datetime)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -68,9 +66,10 @@ class Timeline extends React.Component {
|
||||
addEventListeners () {
|
||||
window.addEventListener('resize', () => { this.computeDims() })
|
||||
let element = document.querySelector('.timeline-wrapper')
|
||||
element.addEventListener('transitionend', (event) => {
|
||||
this.computeDims()
|
||||
})
|
||||
if (element !== null)
|
||||
element.addEventListener('transitionend', (event) => {
|
||||
this.computeDims()
|
||||
})
|
||||
}
|
||||
|
||||
makeScaleX () {
|
||||
@@ -191,8 +190,8 @@ class Timeline extends React.Component {
|
||||
if (rangeLimits) {
|
||||
// If the store contains absolute time limits,
|
||||
// make sure the zoom doesn't go over them
|
||||
const minDate = parseDate(rangeLimits[0])
|
||||
const maxDate = parseDate(rangeLimits[1])
|
||||
const minDate = rangeLimits[0]
|
||||
const maxDate = rangeLimits[1]
|
||||
|
||||
if (newDomain0 < minDate) {
|
||||
newDomain0 = minDate
|
||||
@@ -243,8 +242,8 @@ class Timeline extends React.Component {
|
||||
if (rangeLimits) {
|
||||
// If the store contains absolute time limits,
|
||||
// make sure the zoom doesn't go over them
|
||||
const minDate = parseDate(rangeLimits[0])
|
||||
const maxDate = parseDate(rangeLimits[1])
|
||||
const minDate = rangeLimits[0]
|
||||
const maxDate = rangeLimits[1]
|
||||
|
||||
newDomain0 = (newDomain0 < minDate) ? minDate : newDomain0
|
||||
newDomainF = (newDomainF > maxDate) ? maxDate : newDomainF
|
||||
@@ -262,8 +261,8 @@ class Timeline extends React.Component {
|
||||
this.props.methods.onUpdateTimerange(this.state.timerange)
|
||||
}
|
||||
|
||||
getDatetimeX (timestamp) {
|
||||
return this.state.scaleX(parseDate(timestamp))
|
||||
getDatetimeX (datetime) {
|
||||
return this.state.scaleX(datetime)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -294,8 +293,8 @@ class Timeline extends React.Component {
|
||||
<div className={classes} style={extraStyle}>
|
||||
<Header
|
||||
title={copy[this.props.app.language].timeline.info}
|
||||
date0={formatterWithYear(this.state.timerange[0])}
|
||||
date1={formatterWithYear(this.state.timerange[1])}
|
||||
from={this.state.timerange[0]}
|
||||
to={this.state.timerange[1]}
|
||||
onClick={() => { this.onClickArrow() }}
|
||||
hideInfo={isNarrative}
|
||||
/>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import React from 'react'
|
||||
|
||||
import { capitalizeFirstLetter } from '../../../common/utilities.js'
|
||||
import { capitalize } from '../../../common/utilities.js'
|
||||
|
||||
const CardCategory = ({ categoryTitle, categoryLabel, color }) => (
|
||||
<div className='card-row card-cell category'>
|
||||
<h4>{categoryTitle}</h4>
|
||||
<p>
|
||||
{capitalizeFirstLetter(categoryLabel)}
|
||||
{capitalize(categoryLabel)}
|
||||
<span className='color-category' style={{ background: color }} />
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,7 @@ import React from 'react'
|
||||
import copy from '../../../common/data/copy.json'
|
||||
import { isNotNullNorUndefined } from '../../../common/utilities'
|
||||
|
||||
const CardTimestamp = ({ timelabel, language, precision }) => {
|
||||
const CardTime = ({ timelabel, language, precision }) => {
|
||||
// const daytimeLang = copy[language].cardstack.timestamp
|
||||
// const estimatedLang = copy[language].cardstack.estimated
|
||||
const unknownLang = copy[language].cardstack.unknown_time
|
||||
@@ -29,4 +29,4 @@ const CardTimestamp = ({ timelabel, language, precision }) => {
|
||||
}
|
||||
}
|
||||
|
||||
export default CardTimestamp
|
||||
export default CardTime
|
||||
@@ -92,19 +92,26 @@ const TimelineEvents = ({
|
||||
}
|
||||
}
|
||||
|
||||
let defaultY = getCategoryY(event.category)
|
||||
let eventY = getCategoryY(event.category)
|
||||
const isNonlocated = !event.latitude && !event.longitude
|
||||
if (features.GRAPH_NONLOCATED && isNonlocated) {
|
||||
const { project } = event
|
||||
if (project) {
|
||||
const { offset } = projects[project]
|
||||
eventY = dims.marginTop + offset + sizes.eventDotR
|
||||
}
|
||||
}
|
||||
|
||||
let colour = event.colour ? event.colour : getCategoryColor(event.category)
|
||||
const styles = {
|
||||
fill: colour,
|
||||
fillOpacity: defaultY > 0 ? calcOpacity(1) : 0,
|
||||
fillOpacity: eventY > 0 ? calcOpacity(1) : 0,
|
||||
transition: `transform ${transitionDuration / 1000}s ease`
|
||||
}
|
||||
|
||||
return renderShape(event, styles, {
|
||||
x: getDatetimeX(event.timestamp),
|
||||
y: (features.GRAPH_NONLOCATED && !event.latitude && !event.longitude)
|
||||
? event.projectOffset >= 0 ? dims.trackHeight - event.projectOffset : dims.marginTop
|
||||
: getCategoryY ? defaultY : () => null,
|
||||
x: getDatetimeX(event.datetime),
|
||||
y: eventY,
|
||||
onSelect: () => onSelect(event),
|
||||
dims,
|
||||
highlights: features.HIGHLIGHT_GROUPS ? getHighlights(event.tags[features.HIGHLIGHT_GROUPS.tagIndexIndicatingGroup]) : [],
|
||||
@@ -113,11 +120,12 @@ const TimelineEvents = ({
|
||||
}
|
||||
|
||||
/* set `renderProjects` */
|
||||
// TODO(lachlan): remove hardcoded 'Legislation'
|
||||
let renderProjects = () => null
|
||||
if (features.GRAPH_NONLOCATED) {
|
||||
renderProjects = function () {
|
||||
return <React.Fragment>
|
||||
{projects.map(project => <Project
|
||||
{Object.values(projects).map(project => <Project
|
||||
{...project}
|
||||
onClick={() => console.log(project)}
|
||||
getX={getDatetimeX}
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
import React from 'react'
|
||||
|
||||
const TimelineHeader = ({ title, date0, date1, onClick, hideInfo }) => (
|
||||
<div className='timeline-header'>
|
||||
<div className='timeline-toggle' onClick={() => onClick()}>
|
||||
<p><i className='arrow-down' /></p>
|
||||
const TimelineHeader = ({ title, from, to, onClick, hideInfo }) => {
|
||||
const d0 = from && from.toLocaleDateString()
|
||||
const d1 = to && to.toLocaleDateString()
|
||||
return (
|
||||
<div className='timeline-header'>
|
||||
<div className='timeline-toggle' onClick={() => onClick()}>
|
||||
<p><i className='arrow-down' /></p>
|
||||
</div>
|
||||
<div className={`timeline-info ${hideInfo ? 'hidden' : ''}`}>
|
||||
<p>{title}</p>
|
||||
<p>{d0} - {d1}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`timeline-info ${hideInfo ? 'hidden' : ''}`}>
|
||||
<p>{title}</p>
|
||||
<p>{date0} - {date1}</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
export default TimelineHeader
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import React from 'react'
|
||||
|
||||
import { formatterWithYear } from '../../../js/utilities.js'
|
||||
|
||||
const TimelineLabels = ({ dims, timelabels }) => {
|
||||
return (
|
||||
<g>
|
||||
@@ -24,7 +22,7 @@ const TimelineLabels = ({ dims, timelabels }) => {
|
||||
x='5'
|
||||
y='15'
|
||||
>
|
||||
{formatterWithYear(timelabels[0])}
|
||||
{timelabels[0]}
|
||||
</text>
|
||||
<text
|
||||
class='timelabelF timeLabel'
|
||||
@@ -32,7 +30,7 @@ const TimelineLabels = ({ dims, timelabels }) => {
|
||||
y='15'
|
||||
style={{ textAnchor: 'end' }}
|
||||
>
|
||||
{formatterWithYear(timelabels[1])}
|
||||
{timelabels[1]}
|
||||
</text>
|
||||
</g>
|
||||
)
|
||||
|
||||
@@ -2,21 +2,23 @@ import React from 'react'
|
||||
import { sizes } from '../../../common/global'
|
||||
|
||||
export default ({
|
||||
id,
|
||||
offset,
|
||||
id,
|
||||
start,
|
||||
end,
|
||||
getX,
|
||||
y,
|
||||
dims,
|
||||
colour,
|
||||
onClick
|
||||
}) => {
|
||||
const length = getX(end) - getX(start)
|
||||
if (offset === undefined) return null
|
||||
return <rect
|
||||
onClick={onClick}
|
||||
className='project'
|
||||
x={getX(start)}
|
||||
y={dims.trackHeight - (offset + sizes.eventDotR)}
|
||||
y={dims.marginTop + 100}
|
||||
width={length}
|
||||
style={{ fill: colour, fillOpacity: 0.2 }}
|
||||
height={2 * sizes.eventDotR}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import initial from '../store/initial.js'
|
||||
import { parseDate, toggleFlagAC } from '../common/utilities'
|
||||
import { toggleFlagAC } from '../common/utilities'
|
||||
|
||||
import {
|
||||
UPDATE_HIGHLIGHTED,
|
||||
@@ -44,12 +44,12 @@ function updateNarrative (appState, action) {
|
||||
|
||||
// Compute narrative time range and map bounds
|
||||
if (action.narrative) {
|
||||
minTime = parseDate('2100-01-01T00:00:00')
|
||||
maxTime = parseDate('1900-01-01T00:00:00')
|
||||
minTime = appState.timeline.rangeLimits[0]
|
||||
maxTime = appState.timeline.rangeLimits[1]
|
||||
|
||||
// Find max and mins coordinates of narrative events
|
||||
action.narrative.steps.forEach(step => {
|
||||
const stepTime = parseDate(step.timestamp)
|
||||
const stepTime = step.datetime
|
||||
if (stepTime < minTime) minTime = stepTime
|
||||
if (stepTime > maxTime) maxTime = stepTime
|
||||
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
import initial from '../store/initial.js'
|
||||
|
||||
import { UPDATE_DOMAIN, MARK_NOTIFICATIONS_READ } from '../actions'
|
||||
import { parseDateTimes } from './utils/helpers.js'
|
||||
import { validateDomain } from './utils/validators.js'
|
||||
import { validateDomain } from './validate/validators.js'
|
||||
|
||||
function updateDomain (domainState, action) {
|
||||
action.domain.events = parseDateTimes(action.domain.events)
|
||||
|
||||
// return Object.assign({}, domainState, validate(action.domain))
|
||||
return {
|
||||
...domainState,
|
||||
...validateDomain(action.domain)
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import { timeFormat, timeParse } from 'd3'
|
||||
|
||||
export function parseDateTimes (arrayToParse) {
|
||||
const parsedArray = []
|
||||
|
||||
arrayToParse.forEach(item => {
|
||||
let incomingDateTime = `${item.date}T00:00`
|
||||
if (item.time) incomingDateTime = `${item.date}T${item.time}`
|
||||
const parser = timeParse(process.env.INCOMING_DATETIME_FORMAT)
|
||||
item.timestamp = timeFormat('%Y-%m-%dT%H:%M:%S')(parser(incomingDateTime))
|
||||
|
||||
parsedArray.push(item)
|
||||
})
|
||||
|
||||
return parsedArray
|
||||
}
|
||||
|
||||
export function capitalize (string) {
|
||||
return string.charAt(0).toUpperCase() + string.slice(1)
|
||||
}
|
||||
@@ -16,7 +16,6 @@ const eventSchema = Joi.object().keys({
|
||||
sources: Joi.array(),
|
||||
tags: Joi.array().allow(''),
|
||||
comments: Joi.string().allow(''),
|
||||
timestamp: Joi.string(),
|
||||
time_display: Joi.string().allow(''),
|
||||
|
||||
// nested
|
||||
@@ -25,7 +24,6 @@ const eventSchema = Joi.object().keys({
|
||||
colour: Joi.string().allow('')
|
||||
})
|
||||
.and('latitude', 'longitude')
|
||||
.and('date', 'timestamp')
|
||||
.or('timestamp', 'latitude')
|
||||
.or('date', 'latitude')
|
||||
|
||||
export default eventSchema
|
||||
@@ -1,13 +1,13 @@
|
||||
import Joi from 'joi'
|
||||
|
||||
import eventSchema from '../schema/eventSchema'
|
||||
import categorySchema from '../schema/categorySchema'
|
||||
import siteSchema from '../schema/siteSchema'
|
||||
import narrativeSchema from '../schema/narrativeSchema'
|
||||
import sourceSchema from '../schema/sourceSchema'
|
||||
import shapeSchema from '../schema/shapeSchema'
|
||||
import eventSchema from './eventSchema'
|
||||
import categorySchema from './categorySchema'
|
||||
import siteSchema from './siteSchema'
|
||||
import narrativeSchema from './narrativeSchema'
|
||||
import sourceSchema from './sourceSchema'
|
||||
import shapeSchema from './shapeSchema'
|
||||
|
||||
import { capitalize } from './helpers.js'
|
||||
import { calcDatetime, capitalize } from '../../common/utilities'
|
||||
|
||||
/*
|
||||
* Create an error notification object
|
||||
@@ -153,8 +153,12 @@ export function validateDomain (domain) {
|
||||
}
|
||||
sanitizedDomain.tags = domain.tags
|
||||
|
||||
// sort events by timestamp
|
||||
sanitizedDomain.events.sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp))
|
||||
// append events with datetime and sort
|
||||
sanitizedDomain.events.forEach(event => {
|
||||
event.datetime = calcDatetime(event.date, event.time)
|
||||
})
|
||||
|
||||
sanitizedDomain.events.sort((a, b) => a.datetime - b.datetime)
|
||||
|
||||
return sanitizedDomain
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
import { parseTimestamp } from '../common/utilities'
|
||||
/**
|
||||
* Some handy helpers
|
||||
*/
|
||||
@@ -8,7 +7,7 @@ import { parseTimestamp } from '../common/utilities'
|
||||
* returns true/false if the event falls within timeRange
|
||||
*/
|
||||
export function isTimeRangedIn (event, timeRange) {
|
||||
const eventTime = parseTimestamp(event.timestamp)
|
||||
const eventTime = event.datetime
|
||||
return (
|
||||
timeRange[0] < eventTime &&
|
||||
eventTime < timeRange[1]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createSelector } from 'reselect'
|
||||
import { compareTimestamp, insetSourceFrom, dateMin, dateMax } from '../common/utilities'
|
||||
import { insetSourceFrom, dateMin, dateMax } from '../common/utilities'
|
||||
import { isTimeRangedIn } from './helpers'
|
||||
import { sizes } from '../common/global'
|
||||
|
||||
@@ -96,7 +96,7 @@ export const selectNarratives = createSelector(
|
||||
Object.keys(narratives).forEach(key => {
|
||||
const steps = narratives[key].steps
|
||||
|
||||
steps.sort(compareTimestamp)
|
||||
steps.sort((a, b) => a.datetime - b.datetime)
|
||||
|
||||
if (narrativesMeta.find(n => n.id === key)) {
|
||||
narratives[key] = {
|
||||
@@ -159,29 +159,64 @@ export const selectEventsWithProjects = createSelector(
|
||||
if (!features.GRAPH_NONLOCATED) {
|
||||
return [events, []]
|
||||
}
|
||||
const projSize = 2 * sizes.eventDotR
|
||||
const projectIdx = features.GRAPH_NONLOCATED.projectIdx || 0
|
||||
const getProject = ev => ev.tags[projectIdx]
|
||||
const projects = {}
|
||||
|
||||
// get all projects
|
||||
events = events.reduce((acc, event) => {
|
||||
const project = event.tags.length >= 1 && !event.latitude && !event.longitude ? getProject(event) : null
|
||||
|
||||
// add project if it doesn't exist
|
||||
if (project !== null) {
|
||||
if (projects.hasOwnProperty(project)) {
|
||||
projects[project].start = dateMin(projects[project].start, event.timestamp)
|
||||
projects[project].end = dateMax(projects[project].end, event.timestamp)
|
||||
projects[project].start = dateMin(projects[project].start, event.datetime)
|
||||
projects[project].end = dateMax(projects[project].end, event.datetime)
|
||||
} else {
|
||||
projects[project] = { start: event.timestamp, end: event.timestamp }
|
||||
projects[project] = { start: event.datetime, end: event.datetime, key: project }
|
||||
}
|
||||
}
|
||||
acc.push({ ...event, project })
|
||||
return acc
|
||||
}, [])
|
||||
|
||||
const projKeys = Object.keys(projects)
|
||||
let projObjs = Object.values(projects)
|
||||
projObjs.sort((a, b) => a.start - b.start)
|
||||
|
||||
// active projects is a data structure with projObjs.length empty slots
|
||||
let activeProjs = {}
|
||||
projObjs.forEach((_, idx) => { activeProjs[idx] = null })
|
||||
|
||||
const projectsWithOffset = projObjs.reduce((acc, proj, theIdx) => {
|
||||
if (theIdx >= 1) { acc[proj.key] = proj; return acc }
|
||||
// remove any project that have ended from slots
|
||||
let j = 0
|
||||
while (j < projObjs.length) {
|
||||
if (!activeProjs[j]) {
|
||||
j++
|
||||
continue
|
||||
}
|
||||
const projInSlot = projects[activeProjs[j]]
|
||||
if (projInSlot.end > proj.start) {
|
||||
activeProjs[j] = null
|
||||
}
|
||||
j++
|
||||
}
|
||||
let i = 0
|
||||
// find the first empty slot
|
||||
while (activeProjs[i]) i++
|
||||
// put proj in slot
|
||||
activeProjs[i] = proj.key
|
||||
|
||||
proj.offset = i * projSize
|
||||
console.log(`${proj.key}:-- ${proj.offset}`)
|
||||
acc[proj.key] = proj
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
/*
|
||||
events = events.reduce((acc, event) => {
|
||||
// infer activeProjects from timestamp
|
||||
const activeProjects = []
|
||||
projKeys.forEach((k, idx) => {
|
||||
if (event.timestamp >= projects[k].start && event.timestamp <= projects[k].end) {
|
||||
@@ -192,11 +227,16 @@ export const selectEventsWithProjects = createSelector(
|
||||
// infer projectOffset using activeProjects
|
||||
// TODO(lachlan) projects get overlaid if they start at the same time...
|
||||
const activeIdx = activeProjects.indexOf(event.project)
|
||||
let projectOffset = (activeIdx + 3) * (2.5 * sizes.eventDotR)
|
||||
let projectOffset = activeIdx * projSize
|
||||
if (activeIdx === -1) {
|
||||
// project isn't in previously calculated list of projects
|
||||
projectOffset = -1
|
||||
}
|
||||
if (event.project !== null && !projects[event.project].hasOwnProperty('offset')) {
|
||||
if (event.project !== null) {
|
||||
if (projects[event.project].hasOwnProperty('offset')) {
|
||||
// project is already active
|
||||
projectOffset = (activeIdx + 1) * projSize
|
||||
}
|
||||
projects[event.project].offset = projectOffset
|
||||
projects[event.project].category = event.category
|
||||
} else if (event.project !== null) {
|
||||
@@ -205,8 +245,9 @@ export const selectEventsWithProjects = createSelector(
|
||||
acc.push({ ...event, projectOffset })
|
||||
return acc
|
||||
}, [])
|
||||
*/
|
||||
|
||||
return [events, projects]
|
||||
return [events, projectsWithOffset]
|
||||
}
|
||||
)
|
||||
|
||||
@@ -223,15 +264,7 @@ export const selectProjects = createSelector(
|
||||
if (!features.GRAPH_NONLOCATED) {
|
||||
return []
|
||||
}
|
||||
// reduce projEvents to get _events
|
||||
const projects = []
|
||||
const projKeys = Object.keys(eventsWithProjects[1])
|
||||
|
||||
projKeys.forEach(projId => {
|
||||
projects.push({ ...eventsWithProjects[1][projId], id: projId })
|
||||
})
|
||||
|
||||
return projects
|
||||
return eventsWithProjects[1]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -60,12 +60,12 @@ const initial = {
|
||||
},
|
||||
timeline: {
|
||||
dimensions: {
|
||||
height: 250,
|
||||
height: 450,
|
||||
width: 0,
|
||||
marginLeft: 100,
|
||||
marginTop: 15,
|
||||
marginBottom: 60,
|
||||
contentHeight: 200,
|
||||
contentHeight: 400,
|
||||
width_controls: 100
|
||||
},
|
||||
range: [
|
||||
|
||||
10
yarn.lock
10
yarn.lock
@@ -5855,6 +5855,11 @@ mocha@^5.2.0:
|
||||
mkdirp "0.5.1"
|
||||
supports-color "5.4.0"
|
||||
|
||||
moment@^2.26.0:
|
||||
version "2.26.0"
|
||||
resolved "https://registry.yarnpkg.com/moment/-/moment-2.26.0.tgz#5e1f82c6bafca6e83e808b30c8705eed0dcbd39a"
|
||||
integrity sha512-oIixUO+OamkUkwjhAVE18rAMfRJNsNe/Stid/gwHSOfHrOtw9EhAY2AHvdKZ/k/MggcYELFCJz/Sn2pL8b8JMw==
|
||||
|
||||
move-concurrently@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92"
|
||||
@@ -7200,6 +7205,11 @@ react-tabs@3.0.0:
|
||||
classnames "^2.2.0"
|
||||
prop-types "^15.5.0"
|
||||
|
||||
react-zoom-pan-pinch@^1.6.1:
|
||||
version "1.6.1"
|
||||
resolved "https://registry.yarnpkg.com/react-zoom-pan-pinch/-/react-zoom-pan-pinch-1.6.1.tgz#da16267c258ab37e8ebcdc7c252794a9633e91ec"
|
||||
integrity sha512-J2eM0gZ04XiUWvmKZrOhSAB2zjyoK7kw2POIeN1X0yTTlmp6HPGV0zYfjnlkhgt8nQwpvXAbsF/oAnkuiwk1kA==
|
||||
|
||||
react@^16.6.3:
|
||||
version "16.6.3"
|
||||
resolved "https://registry.yarnpkg.com/react/-/react-16.6.3.tgz#25d77c91911d6bbdd23db41e70fb094cc1e0871c"
|
||||
|
||||
Reference in New Issue
Block a user