From 154b62f9245d1d1d24327ed93283c515a914a0a0 Mon Sep 17 00:00:00 2001 From: Lachlan Kermode Date: Mon, 18 May 2020 10:28:25 +0200 Subject: [PATCH] remove 'timestamps' in rendering events, clean projects --- src/components/Timeline.jsx | 7 +- .../presentational/Timeline/Events.js | 236 +++++------------- .../presentational/Timeline/Project.js | 4 +- src/selectors/index.js | 184 ++++++-------- 4 files changed, 150 insertions(+), 281 deletions(-) diff --git a/src/components/Timeline.jsx b/src/components/Timeline.jsx index a40121e..928e600 100644 --- a/src/components/Timeline.jsx +++ b/src/components/Timeline.jsx @@ -341,8 +341,8 @@ class Timeline extends React.Component { noCategories={this.props.domain.categories && this.props.domain.categories.length} /> { - if (eventsByCategory[ev.category]) { - eventsByCategory[ev.category].events.push((ev)) - } else { - eventsByCategory[ev.category] = { - category: ev.category, - events: [ ev ] - } - } - }) +const GRAPH_NONLOCATED = 'GRAPH_NONLOCATED' in process.env.features && process.env.features.GRAPH_NONLOCATED - return Object.values(eventsByCategory) +function renderDot (event, styles, props) { + return +} + +function renderBar (event, styles, props) { + const fillOpacity = GRAPH_NONLOCATED + ? event.projectOffset >= 0 ? styles.opacity : 0.05 + : 0.6 + + return +} + +function renderDiamond (event, styles, props) { + return +} + +function renderStar (event, styles, props) { + return } -const HAS_PROJECTS = 'ASSOCIATIVE_EVENTS_BY_TAG' in process.env.features && process.env.features.ASSOCIATIVE_EVENTS_BY_TAG const TimelineEvents = ({ events, - datetimes, + projects, narrative, getDatetimeX, getCategoryY, getCategoryColor, onSelect, transitionDuration, - styleDatetime, + // styleDatetime, dims }) => { - function renderDot (event, colour) { - const props = ({ - fill: colour, - fillOpacity: calcOpacity(1), - transition: `transform ${transitionDuration / 1000}s ease` - }) - return onSelect([event])} - category={event.category} - events={[event]} - x={getDatetimeX(event.timestamp)} - y={getCategoryY(event.category)} - r={sizes.eventDotR} - styleProps={props} - /> - } + const narIds = narrative ? narrative.steps.map(s => s.id) : [] - function renderBar (event, colour) { - const evOpacity = calcOpacity(1) - const props = { - fill: colour, - fillOpacity: HAS_PROJECTS - ? event.projectOffset >= 0 ? evOpacity : 0.05 - : 0.6 - } - return onSelect([event])} - category={event.category} - events={[event]} - x={getDatetimeX(event.timestamp)} - y={dims.marginTop} - width={sizes.eventDotR / 4} - height={dims.trackHeight} - styleProps={props} - /> - } - - function renderDiamond (event, colour) { - const props = ({ - fill: colour, - fillOpacity: calcOpacity(1), - transition: `transform ${transitionDuration / 1000}s ease` - }) - return onSelect([event])} - x={getDatetimeX(event.timestamp)} - y={getCategoryY(event.category)} - r={1.8 * sizes.eventDotR} - styleProps={props} - /> - } - - function renderStar (event, colour) { - const props = ({ - fill: colour, - fillOpacity: calcOpacity(1), - transition: `transform ${transitionDuration / 1000}s ease`, - fillRule: 'nonzero' - - }) - return onSelect([event])} - x={getDatetimeX(event.timestamp)} - y={getCategoryY(event.category)} - r={1.8 * sizes.eventDotR} - styleProps={props} - transform='rotate(90)' - /> - } - - function renderDatetime (datetime) { - // narrative checking for non-rendering still uses datetimes as legacy TODO(lachlan) + function renderEvent (event) { if (narrative) { - const { steps } = narrative - // check all events in the datetime before rendering in narrative - let isInNarrative = false - for (let i = 0; i < datetime.events.length; i++) { - const event = datetime.events[i] - if (steps.map(s => s.id).includes(event.id)) { - isInNarrative = true - break - } - } - - if (!isInNarrative) { + if (!(narIds.includes(event.id))) { return null } } - /* DEFAULTS TODO(lachlan): clean up */ - const dotsToRender = getDotsToRender(datetime.events) - - return dotsToRender.map(dot => { - const customStyles = styleDatetime ? styleDatetime(datetime, dot.category) : null - const extraStyles = customStyles[0] - const extraRender = customStyles[1] - - // default to category for colour, and located/unlocated for shape - const locatedEvents = dot.events.filter(ev => ev.latitude && ev.longitude) - const unlocatedEvents = dot.events.filter(ev => !ev.latitude || !ev.longitude) - - // TODO: work out smarter way to manage opacity w.r.t. length - // i.e. render (count - 1) extra dots with a bit of noise in position - // and that, when clicked, all open the same events. - - const unlocatedProps = { - fillOpacity: HAS_PROJECTS - ? unlocatedEvents.some(ev => ev.projectOffset >= 0) ? calcOpacity(unlocatedEvents.length) : 0.05 - : calcOpacity(unlocatedEvents.length) / 4 - } - - let bar = onSelect(unlocatedEvents)} - category={dot.category} - events={unlocatedEvents} - x={getDatetimeX(datetime.timestamp)} - y={dims.marginTop} - width={sizes.eventDotR} - height={dims.trackHeight} - styleProps={unlocatedProps} - /> - if (process.env.features.ASSOCIATIVE_EVENTS_BY_TAG) { - // render all dots individually - bar = - {unlocatedEvents.map(ev => ( onSelect(unlocatedEvents)} - category={dot.category} - events={[ev]} - x={getDatetimeX(datetime.timestamp)} - y={ev.projectOffset >= 0 ? dims.trackHeight - ev.projectOffset : dims.marginTop} - width={sizes.eventDotR} - height={ev.projectOffset >= 0 ? sizes.eventDotR * 2 : dims.trackHeight} - styleProps={unlocatedProps} - />))} - - } - return ( - - {locatedEvents.length >= 1 && renderCircle()} - {unlocatedEvents.length >= 1 && bar} - {extraRender ? extraRender() : null} - - ) - }) - } - - function renderEvent (event) { let renderShape = renderDot if (event.shape) { if (event.shape === 'bar') { @@ -200,18 +92,29 @@ const TimelineEvents = ({ } const colour = event.colour ? event.colour : getCategoryColor(event.category) - return renderShape(event, colour) + const styles = { + fill: colour, + fillOpacity: calcOpacity(1), + transition: `transform ${transitionDuration / 1000}s ease` + } + return renderShape(event, styles, { + x: getDatetimeX(event.timestamp), + y: (GRAPH_NONLOCATED && !event.latitude && !event.longitude) + ? event.projectOffset >= 0 ? dims.trackHeight - event.projectOffset : dims.marginTop + : getCategoryY(event.category), + onSelect: () => onSelect([event]), + dims + }) } /* set `renderProjects` */ let renderProjects = () => null - if (process.env.features.ASSOCIATIVE_EVENTS_BY_TAG) { - const projects = datetimes[1] - datetimes = datetimes[0] + if (GRAPH_NONLOCATED) { renderProjects = function () { return {projects.map(project => console.log(project)} getX={getDatetimeX} dims={dims} colour={getCategoryColor(project.category)} @@ -225,7 +128,6 @@ const TimelineEvents = ({ clipPath={'url(#clip)'} > {renderProjects()} - {/* {datetimes.map(datetime => renderDatetime(datetime))} */} {events.map(event => renderEvent(event))} ) diff --git a/src/components/presentational/Timeline/Project.js b/src/components/presentational/Timeline/Project.js index 8b47bc2..dfd7027 100644 --- a/src/components/presentational/Timeline/Project.js +++ b/src/components/presentational/Timeline/Project.js @@ -16,9 +16,9 @@ export default ({ onClick={onClick} className='project' x={getX(start)} - y={dims.trackHeight - offset} + y={dims.trackHeight - (offset + sizes.eventDotR)} width={length} - style={{ fill: colour, fillOpacity: 0.1 }} + style={{ fill: colour, fillOpacity: 0.2 }} height={2 * sizes.eventDotR} /> } diff --git a/src/selectors/index.js b/src/selectors/index.js index aa1dedc..d116e90 100644 --- a/src/selectors/index.js +++ b/src/selectors/index.js @@ -2,7 +2,7 @@ import { createSelector } from 'reselect' import { compareTimestamp, insetSourceFrom, dateMin, dateMax } from '../common/utilities' import { isTimeRangedIn, shuffle } from './helpers' import { sizes } from '../common/global' -const HAS_PROJECTS = 'ASSOCIATIVE_EVENTS_BY_TAG' in process.env.features && process.env.features.ASSOCIATIVE_EVENTS_BY_TAG +const GRAPH_NONLOCATED = 'GRAPH_NONLOCATED' in process.env.features && process.env.features.GRAPH_NONLOCATED // Input selectors export const getEvents = state => state.domain.events @@ -45,7 +45,7 @@ export const selectEvents = createSelector( const isActiveTag = isMatchingTag || activeTags.length === 0 const isActiveCategory = activeCategories.includes(event.category) || activeCategories.length === 0 let isActiveTime = isTimeRangedIn(event, timeRange) - isActiveTime = HAS_PROJECTS ? ((!event.latitude && !event.longitude) || isActiveTime) : isActiveTime + isActiveTime = GRAPH_NONLOCATED ? ((!event.latitude && !event.longitude) || isActiveTime) : isActiveTime if (isActiveTime && isActiveTag && isActiveCategory) { acc[event.id] = { ...event } @@ -145,6 +145,13 @@ export const selectLocations = createSelector( } ) +export const selectProjectedEvents = createSelector( + [selectEvents], + events => { + + } +) + /** * Group events by 'datetime'. Each datetime is an object: { @@ -154,116 +161,77 @@ export const selectLocations = createSelector( events: [...] } */ -export const selectDatetimes = createSelector( +export const selectEventsAndProjects = createSelector( [selectEvents], events => { - const projects = {} - const datetimes = {} - events.forEach(event => { - const { timestamp } = event - /** Create timestamp with fresh dtKey always by default */ - let dtIdx = 1 - let dtKey = `${timestamp}_${dtIdx}` - let tsExists = datetimes.hasOwnProperty(dtKey) - while (tsExists) { - dtIdx += 1 - dtKey = `${timestamp}_${dtIdx}` - tsExists = datetimes.hasOwnProperty(dtKey) - } - - if (HAS_PROJECTS) { - const project = event.tags.length >= 1 && !event.latitude && !event.longitude ? event.tags[0] : null - event = { ...event, project } - 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) - } else { - projects[project] = { start: event.timestamp, end: event.timestamp } - } - } - } - - /** We need to work out whether we can add the event to an existing - * timestamp, or whether we need to create a new one. What determines - * this is whether or not ALL events in a timestamp have a matching - * project. We not only need to check the current dtKey, but also all - * dtKeys that have the same timestamp. - * - * It's a pretty whack algorithm, but I think it does what it's supposed - * to. This is only run when projects are showing. - * TODO: find a more module way to interface with this code. - */ - let shouldCreate = true - if (HAS_PROJECTS && dtIdx >= 2 && !(!!event.latitude && !!event.longitude) && event.project !== null) { - const allExistingIdxs = [...Array(dtIdx - 1).keys()].map(k => k + 1) - let foundMatching = false - allExistingIdxs.forEach(_idx => { - const _dtKey = `${timestamp}_${_idx}` - const isSameTimestampAndAllSameProjects = datetimes[_dtKey].events.every(ev => ev.project === event.project) - if (isSameTimestampAndAllSameProjects) { - dtKey = _dtKey - foundMatching = true - } - }) - if (!foundMatching) { - shouldCreate = true - } - } - if (shouldCreate) { - datetimes[dtKey] = { - timestamp: event.timestamp, - date: event.date, - time: event.time, - events: [event] - } - } else { - datetimes[dtKey].events.push(event) - } - }) - - const output = [] - if (HAS_PROJECTS) { - const projKeys = Object.keys(projects) - let sortedDts = Object.keys(datetimes) - - sortedDts.sort((a, b) => { - const x = a.substring(0, a.length - 2) - const y = b.substring(0, b.length - 2) - return new Date(x) - new Date(y) - }) - sortedDts.forEach(dt => { - const activeProjects = [] - projKeys.forEach((k, idx) => { - if (dt >= projects[k].start && dt <= projects[k].end) activeProjects.push(k) - }) - output.push({ - ...datetimes[dt], - events: datetimes[dt].events.map(ev => { - const activeIdx = activeProjects.indexOf(ev.project) - let projectOffset = (activeIdx + 1) * (2.5 * sizes.eventDotR) - if (activeIdx === -1) projectOffset = -1 - if (ev.project !== null && !projects[ev.project].hasOwnProperty('offset')) { - projects[ev.project].offset = projectOffset - projects[ev.project].category = ev.category - } else if (ev.project !== null) { - projectOffset = projects[ev.project].offset - } - return { - ...ev, - projectOffset - } - }) - }) - }) - const projectsOut = [] - Object.keys(projects).forEach(projId => { - projectsOut.push({ ...projects[projId], id: projId }) - }) - return [output, projectsOut] + if (!GRAPH_NONLOCATED) { + return [events, []] } - return Object.values(datetimes) + // 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 projects = {} + // const activeProjects = [] + const projEvents = 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) + } else { + projects[project] = { start: event.timestamp, end: event.timestamp } + } + } + acc.push({ ...event, project }) + return acc + }, []) + + // reduce projEvents to get _events + const projKeys = Object.keys(projects) + const _events = projEvents.reduce((acc, event) => { + // infer activeProjects from timestamp + const activeProjects = [] + projKeys.forEach((k, idx) => { + if (event.timestamp >= projects[k].start && event.timestamp <= projects[k].end) { + activeProjects.push(k) + } + }) + + // infer projectOffset using activeProjects + // TODO(lachlan) projects get overlaid on the first layer... + const activeIdx = activeProjects.indexOf(event.project) + let projectOffset = (activeIdx + 3) * (2.5 * sizes.eventDotR) + if (activeIdx === -1) { + projectOffset = -1 + } + if (event.project !== null && !projects[event.project].hasOwnProperty('offset')) { + projects[event.project].offset = projectOffset + projects[event.project].category = event.category + } else if (event.project !== null) { + projectOffset = projects[event.project].offset + } + acc.push({ ...event, projectOffset }) + return acc + }, []) + + const _projects = [] + projKeys.forEach(projId => { + _projects.push({ ...projects[projId], id: projId }) + }) + + return [_events, _projects] } )