mirror of
https://github.com/bellingcat/ukraine-timemap.git
synced 2026-06-12 21:38:35 +03:00
significantly refactor presentational components
This commit is contained in:
15
src/components/presentational/Timeline/Clip.js
Normal file
15
src/components/presentational/Timeline/Clip.js
Normal file
@@ -0,0 +1,15 @@
|
||||
import React from 'react';
|
||||
|
||||
const TimelineClip = ({ dims }) => (
|
||||
<clipPath id="clip">
|
||||
<rect
|
||||
x={dims.margin_left}
|
||||
y="0"
|
||||
width={dims.width - dims.margin_left - dims.width_controls}
|
||||
height={dims.height - 25}
|
||||
>
|
||||
</rect>
|
||||
</clipPath>
|
||||
);
|
||||
|
||||
export default TimelineClip;
|
||||
28
src/components/presentational/Timeline/DatetimeDot.js
Normal file
28
src/components/presentational/Timeline/DatetimeDot.js
Normal file
@@ -0,0 +1,28 @@
|
||||
import React from 'react'
|
||||
|
||||
export default ({
|
||||
category,
|
||||
events,
|
||||
x,
|
||||
y,
|
||||
onSelect,
|
||||
styleProps,
|
||||
extraRender
|
||||
}) => (
|
||||
<g
|
||||
className='datetime'
|
||||
transform={`translate(${x}, ${y})`}
|
||||
onClick={() => onSelect(events)}
|
||||
>
|
||||
<circle
|
||||
className="event"
|
||||
cx={0}
|
||||
cy={0}
|
||||
style={styleProps}
|
||||
r={5}
|
||||
>
|
||||
</circle>
|
||||
{ extraRender ? extraRender() : null }
|
||||
</g>
|
||||
)
|
||||
|
||||
90
src/components/presentational/Timeline/Events.js
Normal file
90
src/components/presentational/Timeline/Events.js
Normal file
@@ -0,0 +1,90 @@
|
||||
import React from 'react';
|
||||
import DatetimeDot from './DatetimeDot'
|
||||
|
||||
// return a list of lists, where each list corresponds to a single category
|
||||
function getDotsToRender(events) {
|
||||
// each datetime needs to render as many dots as there are distinct
|
||||
// categories in the events contained by the datetime.
|
||||
// To this end, eventsByCategory is an intermediate data structure that
|
||||
// groups a datetime's events by distinct categories
|
||||
const eventsByCategory = {}
|
||||
events.forEach(ev => {
|
||||
if (eventsByCategory[ev.category]) {
|
||||
eventsByCategory[ev.category].events.push((ev))
|
||||
} else {
|
||||
eventsByCategory[ev.category] = {
|
||||
category: ev.category,
|
||||
events: [ ev ]
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return Object.values(eventsByCategory)
|
||||
}
|
||||
|
||||
const TimelineEvents = ({
|
||||
datetimes,
|
||||
narrative,
|
||||
getDatetimeX,
|
||||
getCategoryY,
|
||||
getCategoryColor,
|
||||
onSelect,
|
||||
transitionDuration,
|
||||
styleDatetime
|
||||
}) => {
|
||||
function renderDatetime(datetime) {
|
||||
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) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
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 styleProps = ({
|
||||
fill: getCategoryColor(dot.category),
|
||||
fillOpacity: 1,
|
||||
transition: `transform ${transitionDuration / 1000}s ease`,
|
||||
...extraStyles
|
||||
})
|
||||
|
||||
return (
|
||||
<DatetimeDot
|
||||
onSelect={onSelect}
|
||||
category={dot.category}
|
||||
events={dot.events}
|
||||
x={getDatetimeX(datetime)}
|
||||
y={getCategoryY(dot.category)}
|
||||
styleProps={styleProps}
|
||||
extraRender={extraRender}
|
||||
/>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<g
|
||||
clipPath={"url(#clip)"}
|
||||
>
|
||||
{datetimes.map(datetime => renderDatetime(datetime))}
|
||||
</g>
|
||||
);
|
||||
}
|
||||
|
||||
export default TimelineEvents;
|
||||
26
src/components/presentational/Timeline/Handles.js
Normal file
26
src/components/presentational/Timeline/Handles.js
Normal file
@@ -0,0 +1,26 @@
|
||||
import React from 'react';
|
||||
|
||||
const TimelineHandles = ({ dims, onMoveTime }) => {
|
||||
|
||||
return (
|
||||
<g className="time-controls-inline">
|
||||
<g
|
||||
transform={`translate(${dims.margin_left - 20}, 120)`}
|
||||
onClick={() => onMoveTime('backwards')}
|
||||
>
|
||||
<circle r="15"></circle>
|
||||
<path d="M0,-7.847549217020565L6.796176979388489,3.9237746085102825L-6.796176979388489,3.9237746085102825Z" transform="rotate(270)"></path>
|
||||
</g>
|
||||
<g
|
||||
transform={`translate(${dims.width - dims.width_controls + 20}, 120)`}
|
||||
onClick={() => onMoveTime('forward')}
|
||||
>
|
||||
<circle r="15"></circle>
|
||||
<path d="M0,-7.847549217020565L6.796176979388489,3.9237746085102825L-6.796176979388489,3.9237746085102825Z" transform="rotate(90)"></path>
|
||||
</g>
|
||||
</g>
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
export default TimelineHandles;
|
||||
15
src/components/presentational/Timeline/Header.js
Normal file
15
src/components/presentational/Timeline/Header.js
Normal file
@@ -0,0 +1,15 @@
|
||||
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'></i></p>
|
||||
</div>
|
||||
<div className={`timeline-info ${hideInfo ? 'hidden' : ''}`}>
|
||||
<p>{title}</p>
|
||||
<p>{date0} - {date1}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default TimelineHeader;
|
||||
44
src/components/presentational/Timeline/Labels.js
Normal file
44
src/components/presentational/Timeline/Labels.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import React from 'react';
|
||||
|
||||
import { formatterWithYear } from '../../../js/utilities.js';
|
||||
|
||||
const TimelineLabels = ({ dims, timelabels }) => {
|
||||
|
||||
return (
|
||||
<g>
|
||||
<line
|
||||
class="axisBoundaries"
|
||||
x1={dims.margin_left}
|
||||
x2={dims.margin_left}
|
||||
y1="10"
|
||||
y2="20"
|
||||
>
|
||||
</line>
|
||||
<line
|
||||
class="axisBoundaries"
|
||||
x1={dims.width - dims.width_controls}
|
||||
x2={dims.width - dims.width_controls}
|
||||
y1="10"
|
||||
y2="20"
|
||||
>
|
||||
</line>
|
||||
{/* <text */}
|
||||
{/* class="timeLabel0 timeLabel" */}
|
||||
{/* x="5" */}
|
||||
{/* y="15" */}
|
||||
{/* > */}
|
||||
{/* {formatterWithYear(timelabels[0])} */}
|
||||
{/* </text> */}
|
||||
{/* <text */}
|
||||
{/* class="timelabelF timeLabel" */}
|
||||
{/* x={dims.width - dims.width_controls - 5} */}
|
||||
{/* y="15" */}
|
||||
{/* style={{ textAnchor: 'end' }} */}
|
||||
{/* > */}
|
||||
{/* {formatterWithYear(timelabels[1])} */}
|
||||
{/* </text> */}
|
||||
</g>
|
||||
)
|
||||
}
|
||||
|
||||
export default TimelineLabels;
|
||||
31
src/components/presentational/Timeline/Markers.js
Normal file
31
src/components/presentational/Timeline/Markers.js
Normal file
@@ -0,0 +1,31 @@
|
||||
import React from 'react';
|
||||
|
||||
const TimelineMarkers = ({ getEventX, getCategoryY, transitionDuration, selected }) => {
|
||||
function renderMarker(event) {
|
||||
return (
|
||||
<circle
|
||||
className="timeline-marker"
|
||||
cx={0}
|
||||
cy={0}
|
||||
style={{
|
||||
'transform': `translate(${getEventX(event)}px, ${getCategoryY(event.category)}px)`,
|
||||
'-webkit-transition': `transform ${transitionDuration / 1000}s ease`,
|
||||
'-moz-transition': 'none',
|
||||
'opacity': 0.9
|
||||
}}
|
||||
r="10"
|
||||
>
|
||||
</circle>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<g
|
||||
clipPath={"url(#clip)"}
|
||||
>
|
||||
{selected.map(event => renderMarker(event))}
|
||||
</g>
|
||||
);
|
||||
}
|
||||
|
||||
export default TimelineMarkers;
|
||||
25
src/components/presentational/Timeline/ZoomControls.js
Normal file
25
src/components/presentational/Timeline/ZoomControls.js
Normal file
@@ -0,0 +1,25 @@
|
||||
import React from 'react';
|
||||
|
||||
const TimelineZoomControls = ({ extent, zoomLevels, dims, onApplyZoom }) => {
|
||||
function renderZoom(zoom, idx) {
|
||||
const isActive = (zoom.duration === extent)
|
||||
return (
|
||||
<text
|
||||
className={`zoom-level-button ${isActive ? 'active' : ''}`}
|
||||
x="60"
|
||||
y={(idx * 15) + 20}
|
||||
onClick={() => onApplyZoom(zoom)}
|
||||
>
|
||||
{zoom.label}
|
||||
</text>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<g transform={`translate(${dims.width - dims.width_controls}, 0)`}>
|
||||
{zoomLevels.map((z, idx) => renderZoom(z, idx))}
|
||||
</g>
|
||||
);
|
||||
}
|
||||
|
||||
export default TimelineZoomControls;
|
||||
Reference in New Issue
Block a user