mirror of
https://github.com/bellingcat/ukraine-timemap.git
synced 2026-06-12 13:28:36 +03:00
WIP: overridden colours and shapes
This commit is contained in:
@@ -35,7 +35,7 @@ export function fetchDomain () {
|
||||
.catch(() => handleError(domainMsg('categories')))
|
||||
|
||||
let narPromise = Promise.resolve([])
|
||||
if (process.env.features.USE_CATEGORIES) {
|
||||
if (process.env.features.USE_NARRATIVES) {
|
||||
narPromise = fetch(NARRATIVE_URL)
|
||||
.then(response => response.json())
|
||||
.catch(() => handleError(domainMsg('narratives')))
|
||||
|
||||
@@ -10,6 +10,7 @@ export const sizes = {
|
||||
}
|
||||
|
||||
export default {
|
||||
fallbackEventColor: colors.fa_red,
|
||||
darkBackground: colors.black,
|
||||
primaryHighlight: colors.yellow,
|
||||
secondaryHighlight: colors.white,
|
||||
|
||||
@@ -175,13 +175,13 @@ export function selectTypeFromPathWithPoster (path, poster) {
|
||||
return { type: typeForPath(path), path, poster }
|
||||
}
|
||||
|
||||
export function getEventOpacity (events) {
|
||||
export function calcOpacity (num) {
|
||||
/* Events have opacity 0.5 by default, and get added to according to how many
|
||||
* 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 */
|
||||
const base = events.length >= 1 ? 0.3 : 0
|
||||
return base + (Math.min(0.5, 0.08 * (events.length - 1)))
|
||||
const base = num >= 1 ? 0.6 : 0
|
||||
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 }) }
|
||||
|
||||
@@ -69,7 +69,12 @@ class Dashboard extends React.Component {
|
||||
}
|
||||
|
||||
getCategoryColor (category) {
|
||||
return this.props.ui.style.categories[category] || this.props.ui.style.categories['default']
|
||||
const cat = this.props.ui.style.categories[category]
|
||||
if (cat) {
|
||||
return cat
|
||||
} else {
|
||||
return this.props.ui.style.categories['default']
|
||||
}
|
||||
}
|
||||
|
||||
getNarrativeLinks (event) {
|
||||
|
||||
@@ -49,7 +49,7 @@ class Timeline extends React.Component {
|
||||
if ((hash(nextProps.domain.categories) !== hash(this.props.domain.categories)) || hash(nextProps.dimensions) !== hash(this.props.dimensions)) {
|
||||
const { trackHeight, marginTop } = nextProps.dimensions
|
||||
this.setState({
|
||||
scaleY: this.makeScaleY(nextProps.domain.categoriesWithTimeline, trackHeight, marginTop)
|
||||
scaleY: this.makeScaleY(nextProps.domain.categories, trackHeight, marginTop)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -319,7 +319,7 @@ class Timeline extends React.Component {
|
||||
onDragStart={() => { this.onDragStart() }}
|
||||
onDrag={() => { this.onDrag() }}
|
||||
onDragEnd={() => { this.onDragEnd() }}
|
||||
categories={this.props.domain.categoriesWithTimeline}
|
||||
categories={this.props.domain.categories}
|
||||
/>
|
||||
<Handles
|
||||
dims={dims}
|
||||
@@ -341,6 +341,7 @@ class Timeline extends React.Component {
|
||||
noCategories={this.props.domain.categories && this.props.domain.categories.length}
|
||||
/>
|
||||
<Events
|
||||
events={this.props.domain.events}
|
||||
datetimes={this.props.domain.datetimes}
|
||||
styleDatetime={this.styleDatetime}
|
||||
narrative={this.props.app.narrative}
|
||||
@@ -364,9 +365,9 @@ function mapStateToProps (state) {
|
||||
dimensions: selectors.selectDimensions(state),
|
||||
isNarrative: !!state.app.narrative,
|
||||
domain: {
|
||||
events: selectors.selectEvents(state),
|
||||
datetimes: selectors.selectDatetimes(state),
|
||||
categories: selectors.getCategories(state),
|
||||
categoriesWithTimeline: selectors.selectCategoriesWithTimeline(state),
|
||||
narratives: state.domain.narratives
|
||||
},
|
||||
app: {
|
||||
|
||||
@@ -31,7 +31,7 @@ class TimelineAxis extends React.Component {
|
||||
this.x0 =
|
||||
d3.axisBottom(this.props.scaleX)
|
||||
.ticks(10)
|
||||
.tickPadding(5)
|
||||
.tickPadding(0)
|
||||
.tickSize(this.props.dims.trackHeight)
|
||||
.tickFormat(d3.timeFormat(fstFmt))
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react'
|
||||
import { Portal } from 'react-portal'
|
||||
import colors from '../../../common/global.js'
|
||||
import { getEventOpacity } from '../../../common/utilities'
|
||||
import { calcOpacity } from '../../../common/utilities'
|
||||
|
||||
function MapEvents ({ getCategoryColor, categories, projectPoint, styleLocation, selected, narrative, onSelect, svg, locations }) {
|
||||
function getCoordinatesForPercent (radius, percent) {
|
||||
@@ -34,7 +34,7 @@ function MapEvents ({ getCategoryColor, categories, projectPoint, styleLocation,
|
||||
fill: getCategoryColor(locCategory),
|
||||
stroke: colors.darkBackground,
|
||||
strokeWidth: 0,
|
||||
fillOpacity: getEventOpacity(location.events),
|
||||
fillOpacity: calcOpacity(location.events.length),
|
||||
...extraStyles
|
||||
})
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react'
|
||||
import DatetimeDot from './DatetimeDot'
|
||||
import DatetimeBar from './DatetimeBar'
|
||||
import Project from './Project'
|
||||
import { getEventOpacity } from '../../../common/utilities'
|
||||
import { calcOpacity } from '../../../common/utilities'
|
||||
import { sizes } from '../../../common/global'
|
||||
|
||||
// return a list of lists, where each list corresponds to a single category
|
||||
@@ -28,6 +28,7 @@ function getDotsToRender (events) {
|
||||
const HAS_PROJECTS = 'ASSOCIATIVE_EVENTS_BY_TAG' in process.env.features && process.env.features.ASSOCIATIVE_EVENTS_BY_TAG
|
||||
|
||||
const TimelineEvents = ({
|
||||
events,
|
||||
datetimes,
|
||||
narrative,
|
||||
getDatetimeX,
|
||||
@@ -38,7 +39,45 @@ const TimelineEvents = ({
|
||||
styleDatetime,
|
||||
dims
|
||||
}) => {
|
||||
function renderDot (event, colour) {
|
||||
const props = ({
|
||||
fill: colour,
|
||||
fillOpacity: calcOpacity(1),
|
||||
transition: `transform ${transitionDuration / 1000}s ease`
|
||||
})
|
||||
return <DatetimeDot
|
||||
onSelect={() => onSelect([event])}
|
||||
category={event.category}
|
||||
events={[event]}
|
||||
x={getDatetimeX(event.timestamp)}
|
||||
y={getCategoryY(event.category)}
|
||||
r={sizes.eventDotR}
|
||||
styleProps={props}
|
||||
/>
|
||||
}
|
||||
|
||||
function renderBar (event, colour) {
|
||||
const evOpacity = calcOpacity(1)
|
||||
const props = {
|
||||
fill: colour,
|
||||
fillOpacity: HAS_PROJECTS
|
||||
? event.projectOffset >= 0 ? evOpacity : 0.05
|
||||
: 0.6
|
||||
}
|
||||
return <DatetimeBar
|
||||
onSelect={() => onSelect([event])}
|
||||
category={event.category}
|
||||
events={[event]}
|
||||
x={getDatetimeX(event.timestamp)}
|
||||
y={dims.marginTop}
|
||||
width={sizes.eventDotR / 4}
|
||||
height={dims.trackHeight}
|
||||
styleProps={props}
|
||||
/>
|
||||
}
|
||||
|
||||
function renderDatetime (datetime) {
|
||||
// narrative checking for non-rendering still uses datetimes as legacy TODO(lachlan)
|
||||
if (narrative) {
|
||||
const { steps } = narrative
|
||||
// check all events in the datetime before rendering in narrative
|
||||
@@ -56,35 +95,28 @@ const TimelineEvents = ({
|
||||
}
|
||||
}
|
||||
|
||||
/* 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]
|
||||
|
||||
const categoryColor = getCategoryColor(dot.category)
|
||||
// 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 locatedProps = ({
|
||||
fill: categoryColor,
|
||||
fillOpacity: getEventOpacity(locatedEvents),
|
||||
transition: `transform ${transitionDuration / 1000}s ease`,
|
||||
...extraStyles
|
||||
})
|
||||
|
||||
const unlocatedProps = {
|
||||
fill: categoryColor,
|
||||
fillOpacity: HAS_PROJECTS
|
||||
? unlocatedEvents.some(ev => ev.projectOffset >= 0) ? getEventOpacity(unlocatedEvents) : 0.05
|
||||
: getEventOpacity(unlocatedEvents) / 4
|
||||
? unlocatedEvents.some(ev => ev.projectOffset >= 0) ? calcOpacity(unlocatedEvents.length) : 0.05
|
||||
: calcOpacity(unlocatedEvents.length) / 4
|
||||
}
|
||||
|
||||
const extraRender = customStyles[1]
|
||||
|
||||
let bar = <DatetimeBar
|
||||
onSelect={() => onSelect(unlocatedEvents)}
|
||||
category={dot.category}
|
||||
@@ -112,16 +144,7 @@ const TimelineEvents = ({
|
||||
}
|
||||
return (
|
||||
<g className='datetime'>
|
||||
{locatedEvents.length >= 1 && <DatetimeDot
|
||||
onSelect={() => onSelect(locatedEvents)}
|
||||
category={dot.category}
|
||||
events={locatedEvents}
|
||||
x={getDatetimeX(datetime.timestamp)}
|
||||
y={getCategoryY(dot.category)}
|
||||
r={sizes.eventDotR}
|
||||
styleProps={locatedProps}
|
||||
extraRender={extraRender}
|
||||
/>}
|
||||
{locatedEvents.length >= 1 && renderCircle()}
|
||||
{unlocatedEvents.length >= 1 && bar}
|
||||
{extraRender ? extraRender() : null}
|
||||
</g>
|
||||
@@ -129,16 +152,19 @@ const TimelineEvents = ({
|
||||
})
|
||||
}
|
||||
|
||||
// const projOffsets = {}
|
||||
// const pEvents = datetimes.filter(dt => dt.events.some(ev => ev.project !== null))
|
||||
// pEvents.forEach(({ events }) => {
|
||||
// events.forEach(ev => {
|
||||
// if (!projOffsets.hasOwnProperty(ev.project)) {
|
||||
// projOffsets[ev.project] = ev.projectOffset
|
||||
// }
|
||||
// })
|
||||
// })
|
||||
function renderEvent (event) {
|
||||
let renderShape = renderDot
|
||||
if (event.shape) {
|
||||
if (event.shape === 'bar') {
|
||||
renderShape = renderBar
|
||||
}
|
||||
}
|
||||
|
||||
const colour = event.colour ? event.colour : getCategoryColor(event.category)
|
||||
return renderShape(event, colour)
|
||||
}
|
||||
|
||||
/* set `renderProjects` */
|
||||
let renderProjects = () => null
|
||||
if (process.env.features.ASSOCIATIVE_EVENTS_BY_TAG) {
|
||||
const projects = datetimes[1]
|
||||
@@ -160,7 +186,8 @@ const TimelineEvents = ({
|
||||
clipPath={'url(#clip)'}
|
||||
>
|
||||
{renderProjects()}
|
||||
{datetimes.map(datetime => renderDatetime(datetime))}
|
||||
{/* {datetimes.map(datetime => renderDatetime(datetime))} */}
|
||||
{events.map(event => renderEvent(event))}
|
||||
</g>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -11,9 +11,8 @@ const TimelineMarkers = ({
|
||||
noCategories
|
||||
}) => {
|
||||
function renderMarker (event) {
|
||||
const isLocated = !!event.latitude && !!event.longitude
|
||||
return isLocated ? (
|
||||
<circle
|
||||
function renderCircle () {
|
||||
return <circle
|
||||
className='timeline-marker'
|
||||
cx={0}
|
||||
cy={0}
|
||||
@@ -30,12 +29,13 @@ const TimelineMarkers = ({
|
||||
}}
|
||||
r={sizes.eventDotR * 2}
|
||||
/>
|
||||
) : (
|
||||
<rect
|
||||
}
|
||||
function renderBar () {
|
||||
return <rect
|
||||
className='timeline-marker'
|
||||
x={0}
|
||||
y={0}
|
||||
width={sizes.eventDotR}
|
||||
width={sizes.eventDotR / 3}
|
||||
height={dims.contentHeight - 55}
|
||||
stroke={styles ? styles.stroke : colors.primaryHighlight}
|
||||
stroke-opacity='1'
|
||||
@@ -43,10 +43,19 @@ const TimelineMarkers = ({
|
||||
stroke-dasharray={styles ? styles['stroke-dasharray'] : '2,2'}
|
||||
style={{
|
||||
'transform': `translate(${getEventX(event.timestamp)}px)`,
|
||||
'opacity': 0.9
|
||||
'opacity': 0.7
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
const isLocated = !!event.latitude && !!event.longitude
|
||||
switch (event.shape) {
|
||||
case 'circle':
|
||||
return renderCircle()
|
||||
case 'bar':
|
||||
return renderBar()
|
||||
default:
|
||||
return isLocated ? renderBar() : renderCircle()
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -20,7 +20,9 @@ const eventSchema = Joi.object().keys({
|
||||
time_display: Joi.string().allow(''),
|
||||
|
||||
// nested
|
||||
narrative___stepStyles: Joi.array()
|
||||
narrative___stepStyles: Joi.array(),
|
||||
shape: Joi.string().allow(''),
|
||||
colour: Joi.string().allow('')
|
||||
})
|
||||
.and('latitude', 'longitude')
|
||||
.and('date', 'timestamp')
|
||||
|
||||
@@ -281,30 +281,6 @@ export const selectSelected = createSelector(
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* 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])
|
||||
}
|
||||
)
|
||||
|
||||
export const selectDimensions = createSelector(
|
||||
[getTimelineDimensions],
|
||||
(dimensions) => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { mergeDeepLeft } from 'ramda'
|
||||
import colors from '../common/global.js'
|
||||
import global from '../common/global'
|
||||
|
||||
const initial = {
|
||||
/*
|
||||
@@ -105,12 +105,12 @@ const initial = {
|
||||
tiles: 'openstreetmap', // ['openstreetmap', 'streets', 'satellite']
|
||||
style: {
|
||||
categories: {
|
||||
default: colors.fa_red
|
||||
default: global.fallbackEventColor
|
||||
},
|
||||
narratives: {
|
||||
default: {
|
||||
opacity: 0.9,
|
||||
stroke: colors.fa_red,
|
||||
stroke: global.fallbackEventColor,
|
||||
strokeWidth: 3
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user