correct timeline selection

This commit is contained in:
Lachlan Kermode
2020-05-29 14:57:09 +02:00
parent 3fc1ff4c28
commit 3b4a3312d5
10 changed files with 104 additions and 70 deletions

View File

@@ -32,6 +32,7 @@
"react-portal": "^4.2.0",
"react-redux": "^5.0.4",
"react-tabs": "3.0.0",
"react-zoom-pan-pinch": "^1.6.1",
"redux": "^3.6.0",
"redux-thunk": "^2.2.0",
"reselect": "^3.0.1",

View File

@@ -187,3 +187,23 @@ export function calcOpacity (num) {
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 }) }
/** Taken from
* https://stackoverflow.com/questions/22697936/binary-search-in-javascript
* **/
export function binarySearch (ar, el, compareFn) {
var m = 0
var n = ar.length - 1
while (m <= n) {
var k = (n + m) >> 1
var cmp = compareFn(el, ar[k])
if (cmp > 0) {
m = k + 1
} else if (cmp < 0) {
n = k - 1
} else {
return k
}
}
return -m - 1
}

View File

@@ -16,7 +16,7 @@ import Notification from './Notification.jsx'
import StaticPage from './StaticPage'
import TemplateCover from './TemplateCover'
import { parseDate } from '../common/utilities'
import { parseDate, binarySearch } from '../common/utilities'
import { isMobile } from 'react-device-detect'
class Dashboard extends React.Component {
@@ -29,8 +29,6 @@ class Dashboard extends React.Component {
this.moveInNarrative = this.moveInNarrative.bind(this)
this.handleSelect = this.handleSelect.bind(this)
this.getCategoryColor = this.getCategoryColor.bind(this)
this.eventsById = {}
}
componentDidMount () {
@@ -49,23 +47,36 @@ class Dashboard extends React.Component {
this.props.actions.updateHighlighted((highlighted) || null)
}
getEventById (eventId) {
if (this.eventsById[eventId]) return this.eventsById[eventId]
this.eventsById[eventId] = this.props.domain.events.find(ev => ev.id === eventId)
return this.eventsById[eventId]
}
handleViewSource (source) {
this.props.actions.updateSource(source)
}
handleSelect (selected) {
if (selected) {
let eventsToSelect = selected.map(event => this.getEventById(event.id))
eventsToSelect = eventsToSelect.sort((a, b) => parseDate(a.timestamp) - parseDate(b.timestamp))
this.props.actions.updateSelected(eventsToSelect)
handleSelect (selected, axis) {
const matchedEvents = [selected]
const TIMELINE_AXIS = 0
if (axis === TIMELINE_AXIS) {
// find in events
const { events } = this.props.domain
const idx = binarySearch(
events,
selected,
(e1, e2) => new Date(e1.timestamp) - new Date(e2.timestamp)
)
// check events before
let ptr = idx - 1
while (events[idx].timestamp === events[ptr].timestamp) {
matchedEvents.push(events[ptr])
ptr -= 1
}
// check events after
ptr = idx + 1
while (events[idx].timestamp === events[ptr].timestamp) {
matchedEvents.push(events[ptr])
ptr += 1
}
}
this.props.actions.updateSelected(matchedEvents)
}
getCategoryColor (category) {

View File

@@ -272,7 +272,7 @@ function mapStateToProps (state) {
},
app: {
views: state.app.filters.views,
selected: state.app.selected,
selected: selectors.selectSelected(state),
highlighted: state.app.highlighted,
map: state.app.map,
narrative: state.app.narrative,

View File

@@ -14,6 +14,7 @@ import ZoomControls from './presentational/Timeline/ZoomControls.js'
import Markers from './presentational/Timeline/Markers.js'
import Events from './presentational/Timeline/Events.js'
import Categories from './TimelineCategories.jsx'
const TIMELINE_AXIS = 0
class Timeline extends React.Component {
constructor (props) {
@@ -336,14 +337,14 @@ class Timeline extends React.Component {
dims={dims}
selected={this.props.app.selected}
getEventX={this.getDatetimeX}
getY={e => this.state.scaleY(e.category)}
getCategoryY={this.state.scaleY}
transitionDuration={this.state.transitionDuration}
styles={this.props.ui.styles}
noCategories={this.props.domain.categories && this.props.domain.categories.length}
features={this.props.features}
/>
<Events
events={this.props.domain.eventsAndProjects[0]}
projects={this.props.domain.eventsAndProjects[1]}
events={this.props.domain.events}
projects={this.props.domain.projects}
styleDatetime={this.styleDatetime}
narrative={this.props.app.narrative}
getDatetimeX={this.getDatetimeX}
@@ -356,7 +357,7 @@ class Timeline extends React.Component {
}}
getCategoryColor={this.props.methods.getCategoryColor}
transitionDuration={this.state.transitionDuration}
onSelect={this.props.methods.onSelect}
onSelect={ev => this.props.methods.onSelect(ev, TIMELINE_AXIS)}
dims={dims}
features={this.props.features}
/>
@@ -373,7 +374,8 @@ function mapStateToProps (state) {
dimensions: selectors.selectDimensions(state),
isNarrative: !!state.app.narrative,
domain: {
eventsAndProjects: selectors.selectEventsAndProjects(state),
events: selectors.selectStackedEvents(state),
projects: selectors.selectProjects(state),
categories: selectors.getCategories(state),
narratives: state.domain.narratives
},

View File

@@ -21,7 +21,7 @@ function renderDot (event, styles, props) {
function renderBar (event, styles, props) {
const fillOpacity = props.features.GRAPH_NONLOCATED
? event.projectOffset >= 0 ? styles.opacity : 0.05
? event.projectOffset >= 0 ? styles.opacity : 0.5
: 0.6
return <DatetimeBar
@@ -80,7 +80,8 @@ const TimelineEvents = ({
}
}
let renderShape = renderDot
const isDot = (!!event.location && !!event.longitude) || (features.GRAPH_NONLOCATED && event.projectOffset !== -1)
let renderShape = isDot ? renderDot : renderBar
if (event.shape) {
if (event.shape === 'bar') {
renderShape = renderBar
@@ -103,7 +104,7 @@ const TimelineEvents = ({
y: (features.GRAPH_NONLOCATED && !event.latitude && !event.longitude)
? event.projectOffset >= 0 ? dims.trackHeight - event.projectOffset : dims.marginTop
: getCategoryY ? getCategoryY(event.category) : () => null,
onSelect: () => onSelect([event]),
onSelect: () => onSelect(event),
dims,
highlights: features.HIGHLIGHT_GROUPS ? getHighlights(event.tags[features.HIGHLIGHT_GROUPS.tagIndexIndicatingGroup]) : [],
features

View File

@@ -4,14 +4,18 @@ import colors, { sizes } from '../../../common/global'
const TimelineMarkers = ({
styles,
getEventX,
getY,
getCategoryY,
transitionDuration,
selected,
dims,
noCategories
features
}) => {
function renderMarker (event) {
function renderCircle () {
const yVal = (features.GRAPH_NONLOCATED && !event.latitude && !event.longitude)
? event.projectOffset >= 0 ? dims.trackHeight - event.projectOffset : dims.marginTop
: getCategoryY ? getCategoryY(event.category) : () => null
return <circle
className='timeline-marker'
cx={0}
@@ -22,7 +26,7 @@ const TimelineMarkers = ({
stroke-linejoin='round'
stroke-dasharray={styles ? styles['stroke-dasharray'] : '2,2'}
style={{
'transform': `translate(${getEventX(event.timestamp)}px, ${getY(event)}px)`,
'transform': `translate(${getEventX(event.timestamp)}px, ${yVal}px)`,
'-webkit-transition': `transform ${transitionDuration / 1000}s ease`,
'-moz-transition': 'none',
'opacity': 0.9
@@ -47,7 +51,7 @@ const TimelineMarkers = ({
}}
/>
}
const isLocated = !!event.latitude && !!event.longitude
const isDot = (!features.GRAPH_NONLOCATED && !!event.latitude && !!event.longitude) || (features.GRAPH_NONLOCATED && (event.projectOffset !== -1 || (!!event.latitude && !!event.longitude)))
switch (event.shape) {
case 'circle':
return renderCircle()
@@ -58,7 +62,7 @@ const TimelineMarkers = ({
case 'star':
return renderCircle()
default:
return isLocated ? renderBar() : renderCircle()
return isDot ? renderCircle() : renderBar()
}
}

View File

@@ -153,5 +153,8 @@ 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))
return sanitizedDomain
}

View File

@@ -153,44 +153,17 @@ export const selectLocations = createSelector(
}
)
export const selectProjectedEvents = createSelector(
[selectEvents],
events => {
}
)
/**
* Group events by 'datetime'. Each datetime is an object:
{
timestamp: '',
date: '8/23/2016',
time: '12:00',
events: [...]
}
*/
export const selectEventsAndProjects = createSelector(
export const selectEventsWithProjects = createSelector(
[selectEvents, getFeatures],
(events, features) => {
if (!features.GRAPH_NONLOCATED) {
return [events, []]
}
// NOTE: change this line if you want to extract projects from a different column
function getProject (ev) {
return ev.tags[0]
}
events.sort((a, b) => {
const x = a.timestamp.substring(0, a.timestamp.length - 2)
const y = b.timestamp.substring(0, b.timestamp.length - 2)
return new Date(x) - new Date(y)
})
// reduce events to get projects
const projectIdx = features.GRAPH_NONLOCATED.projectIdx || 0
const getProject = ev => ev.tags[projectIdx]
const projects = {}
// const activeProjects = []
const projEvents = events.reduce((acc, event) => {
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
@@ -206,9 +179,8 @@ export const selectEventsAndProjects = createSelector(
return acc
}, [])
// reduce projEvents to get _events
const projKeys = Object.keys(projects)
const _events = projEvents.reduce((acc, event) => {
events = events.reduce((acc, event) => {
// infer activeProjects from timestamp
const activeProjects = []
projKeys.forEach((k, idx) => {
@@ -218,7 +190,7 @@ export const selectEventsAndProjects = createSelector(
})
// infer projectOffset using activeProjects
// TODO(lachlan) projects get overlaid on the first layer...
// 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)
if (activeIdx === -1) {
@@ -234,12 +206,32 @@ export const selectEventsAndProjects = createSelector(
return acc
}, [])
const _projects = []
return [events, projects]
}
)
export const selectStackedEvents = createSelector(
[selectEventsWithProjects],
eventsWithProjects => {
return eventsWithProjects[0]
}
)
export const selectProjects = createSelector(
[selectEventsWithProjects, getFeatures],
(eventsWithProjects, features) => {
if (!features.GRAPH_NONLOCATED) {
return []
}
// reduce projEvents to get _events
const projects = []
const projKeys = Object.keys(eventsWithProjects[1])
projKeys.forEach(projId => {
_projects.push({ ...projects[projId], id: projId })
projects.push({ ...eventsWithProjects[1][projId], id: projId })
})
return [_events, _projects]
return projects
}
)

View File

@@ -60,12 +60,12 @@ const initial = {
},
timeline: {
dimensions: {
height: 250,
height: 1250,
width: 0,
marginLeft: 100,
marginTop: 15,
marginBottom: 60,
contentHeight: 200,
contentHeight: 800,
width_controls: 100
},
range: [