implements first attempt of unlocated bars

needs profiling and better calculated styles
This commit is contained in:
Lachlan Kermode
2020-02-24 08:11:59 +13:00
parent afc84e61ac
commit 958afb6a41
9 changed files with 130 additions and 47 deletions

View File

@@ -180,5 +180,6 @@ export function getEventOpacity (events) {
* other events there are in the same render. The idea here is that the
* overlaying of events builds up a 'heat map' of the event space, where
* darker areas represent more events with proportion */
return 0.3 + (Math.min(0.5, 0.08 * (events.length - 1)))
const base = events.length >= 1 ? 0.3 : 0
return base + (Math.min(0.5, 0.08 * (events.length - 1)))
}

View File

@@ -303,7 +303,7 @@ class Timeline extends React.Component {
onDragStart={() => { this.onDragStart() }}
onDrag={() => { this.onDrag() }}
onDragEnd={() => { this.onDragEnd() }}
categories={this.props.domain.categories}
categories={this.props.domain.categoriesWithTimeline}
/>
<Handles
dims={dims}
@@ -346,6 +346,7 @@ function mapStateToProps (state) {
domain: {
datetimes: selectors.selectDatetimes(state),
categories: selectors.getCategories(state),
categoriesWithTimeline: selectors.selectCategoriesWithTimeline(state),
narratives: state.domain.narratives
},
app: {

View File

@@ -2,8 +2,8 @@ import React from 'react'
import * as d3 from 'd3'
class TimelineCategories extends React.Component {
constructor () {
super()
constructor (props) {
super(props)
this.grabRef = React.createRef()
this.state = {
isInitialized: false
@@ -38,9 +38,7 @@ class TimelineCategories extends React.Component {
const dims = this.props.dims
return (
<g
class='yAxis'
>
<g class='yAxis'>
{this.props.categories.map((cat, idx) => this.renderCategory(cat, idx))}
<rect
ref={this.grabRef}

View File

@@ -0,0 +1,21 @@
import React from 'react'
export default ({
category,
events,
x,
y,
onSelect,
styleProps,
extraRender
}) => (
<rect
onClick={onSelect}
className='event'
x={x}
y={y}
style={styleProps}
width={4}
height={55}
/>
)

View File

@@ -9,18 +9,12 @@ export default ({
styleProps,
extraRender
}) => (
<g
className='datetime'
transform={`translate(${x}, ${y})`}
onClick={() => onSelect(events)}
>
<circle
className='event'
cx={0}
cy={0}
style={styleProps}
r={5}
/>
{ extraRender ? extraRender() : null }
</g>
<circle
onClick={onSelect}
className='event'
cx={x}
cy={y}
style={styleProps}
r={5}
/>
)

View File

@@ -1,5 +1,6 @@
import React from 'react'
import DatetimeDot from './DatetimeDot'
import DatetimeBar from './DatetimeBar'
import { getEventOpacity } from '../../../common/utilities'
// return a list of lists, where each list corresponds to a single category
@@ -57,43 +58,52 @@ const TimelineEvents = ({
const customStyles = styleDatetime ? styleDatetime(datetime, dot.category) : null
const extraStyles = customStyles[0]
// const isLocated = dot.events.map(ev => !ev.latitude || !ev.longitude)
const categoryColor = getCategoryColor(dot.category)
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 styleProps = ({
fill: getCategoryColor(dot.category),
fillOpacity: getEventOpacity(dot.events),
const locatedProps = ({
fill: categoryColor,
fillOpacity: getEventOpacity(locatedEvents),
transition: `transform ${transitionDuration / 1000}s ease`,
...extraStyles
})
const extraRender = () => (
<React.Fragment>
{customStyles[1]}
</React.Fragment>
)
const unlocatedProps = {
fill: categoryColor,
fillOpacity: getEventOpacity(unlocatedEvents)
}
return (<React.Fragment>
<DatetimeDot
onSelect={onSelect}
category={dot.category}
events={dot.events}
x={getDatetimeX(datetime)}
y={getCategoryY(dot.category)}
styleProps={styleProps}
extraRender={extraRender}
/>
</React.Fragment>
const extraRender = customStyles[1]
return (
<g className='datetime'>
{locatedEvents.length >= 1 && <DatetimeDot
onSelect={() => onSelect(locatedEvents)}
category={dot.category}
events={locatedEvents}
x={getDatetimeX(datetime)}
y={getCategoryY(dot.category)}
styleProps={locatedProps}
extraRender={extraRender}
/>}
{unlocatedEvents.length >= 1 && <DatetimeBar
onSelect={() => onSelect(unlocatedEvents)}
category={dot.category}
events={unlocatedEvents}
x={getDatetimeX(datetime)}
y={40}
styleProps={unlocatedProps}
/>}
{extraRender ? extraRender() : null}
</g>
)
})
}
// console.log(datetimes
// .filter(d => d.events.some(e => e.category !== 'Legislation'))
// )
return (
<g
clipPath={'url(#clip)'}

View File

@@ -3,7 +3,8 @@ import colors from '../../../common/global.js'
const TimelineMarkers = ({ styles, getEventX, getCategoryY, transitionDuration, selected }) => {
function renderMarker (event) {
return (
const isLocated = !!event.latitude && !!event.longitude
return isLocated ? (
<circle
className='timeline-marker'
cx={0}
@@ -22,6 +23,26 @@ const TimelineMarkers = ({ styles, getEventX, getCategoryY, transitionDuration,
}}
r='10'
/>
) : (
<rect
className='timeline-marker'
x={0}
y={0}
width={4}
height={55}
stroke={styles ? styles.stroke : colors.primaryHighlight}
stroke-opacity='1'
stroke-width={styles ? styles['stroke-width'] : 2}
stroke-linecap=''
style={{
'transform': `translate(${getEventX(event)}px, 40px)`,
'-webkit-transition': `transform ${transitionDuration / 1000}s ease`,
'-moz-transition': 'none',
'opacity': 0.9
}}
r='10'
/>
)
}

View File

@@ -14,3 +14,16 @@ export function isTimeRangedIn (event, timeRange) {
eventTime < timeRange[1]
)
}
/**
* Shuffles array in place. ES6 version
* @param {Array} a items An array containing the items.
* https://stackoverflow.com/questions/6274339/how-can-i-shuffle-an-array
*/
export function shuffle (a) {
for (let i = a.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[a[i], a[j]] = [a[j], a[i]]
}
return a
}

View File

@@ -1,6 +1,6 @@
import { createSelector } from 'reselect'
import { compareTimestamp, insetSourceFrom } from '../common/utilities'
import { isTimeRangedIn } from './helpers'
import { isTimeRangedIn, shuffle } from './helpers'
// Input selectors
export const getEvents = state => state.domain.events
@@ -181,3 +181,27 @@ export const selectSelected = createSelector(
return selected.map(insetSourceFrom(sources))
}
)
/**
* Only categories that have events which are located should show on the
* timeline.
*/
export const selectCategoriesWithTimeline = createSelector(
[getCategories, getEvents],
(categories, events) => {
if (categories.length === 0) {
return categories
}
// check for located events in category
// shuffle first to improve chances of stopping more quickly
const hasLocated = {}
for (let event of shuffle(events)) {
if (Object.keys(hasLocated).length === categories.length) break
const cat = event.category
if (hasLocated[cat]) continue
const isLocated = !!event.longitude && !!event.latitude
if (isLocated) hasLocated[cat] = true
}
return categories.filter(cat => hasLocated[cat.category])
}
)