Skip to content

Commit

Permalink
fix: refactor Timeline
Browse files Browse the repository at this point in the history
  • Loading branch information
BRaimbault committed Dec 11, 2024
1 parent c6c6fcf commit 170ac85
Showing 1 changed file with 111 additions and 118 deletions.
229 changes: 111 additions & 118 deletions src/components/periods/Timeline.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,98 +62,16 @@ class Timeline extends Component {
static contextTypes = {
map: PropTypes.object,
}

static propTypes = {
period: PropTypes.object.isRequired,
periodId: PropTypes.string.isRequired,
periods: PropTypes.array.isRequired,
onChange: PropTypes.func.isRequired,
}

state = {
width: null,
mode: 'start',
}

componentDidMount() {
this.setWidth()
this.context.map.on('resize', this.setWidth)
}

componentDidUpdate() {
this.setTimeAxis()
}

componentWillUnmount() {
this.context.map.off('resize', this.setWidth)
}

render() {
const { mode } = this.state
const uniqueRanks = countUniqueRanks(this.props.periods)

this.setTimeScale()
const rectTotalHeight = RECT_HEIGHT + (uniqueRanks - 1) * RECT_OFFSET
return (
<svg
className={`dhis2-map-timeline ${styles.timeline}`}
style={{
height: `${32 + rectTotalHeight}`,
bottom: `30`,
}}
>
// play/pause button
<g
onClick={this.onPlayPause}
transform={`translate(7,${rectTotalHeight / 2})`}
className={styles.play}
>
<path d="M0 0h24v24H0z" fillOpacity="0.0" />
{mode === 'play' ? PAUSE_ICON : PLAY_ICON}
</g>
// rectangles
<g transform={`translate(${PADDING_LEFT},10)`}>
{this.getPeriodRects()}
</g>
// x axis
<g
transform={`translate(${PADDING_LEFT},${
12 + rectTotalHeight
})`}
ref={(node) => (this.node = node)}
/>
</svg>
)
}

// Returns array of period rectangles
getPeriodRects = () => {
const { period, periods } = this.props

const sortedPeriods = sortPeriodsByLevelRank(periods)

return sortedPeriods.map((item) => {
const isCurrent = period.id === item.id
const { id, startDate, endDate } = item
const x = this.timeScale(startDate)
const width = this.timeScale(endDate) - x

return (
<rect
key={id}
className={cx(styles.period, {
[styles.selected]: isCurrent,
})}
x={x}
y={item.levelRank * RECT_OFFSET}
width={width}
height={RECT_HEIGHT}
onClick={() => this.onPeriodClick(item)}
/>
)
})
}

// Set time scale
setTimeScale = () => {
const { periods } = this.props
Expand All @@ -163,10 +81,10 @@ class Timeline extends Component {
return
}

const { minStartDate: startDate, maxEndDate: endDate } = periods.reduce(
(acc, item) => {
const start = new Date(item.startDate)
const end = new Date(item.endDate)
const { minStartDate, maxEndDate } = periods.reduce(
(acc, { startDate, endDate }) => {
const start = new Date(startDate)
const end = new Date(endDate)
return {
minStartDate:
start < acc.minStartDate ? start : acc.minStartDate,
Expand All @@ -181,7 +99,7 @@ class Timeline extends Component {

// Link time domain to timeline width
this.timeScale = scaleTime()
.domain([startDate, endDate])
.domain([minStartDate, maxEndDate])
.range([0, width])
}

Expand All @@ -196,53 +114,29 @@ class Timeline extends Component {
: 1)
const { width } = this.state
const maxTicks = Math.round(width / LABEL_WIDTH)
const numTicks = maxTicks < numPeriods ? maxTicks : numPeriods
const timeAxis = axisBottom(this.timeScale)
const [startDate, endDate] = this.timeScale.domain()
const ticks = timeTicks(startDate, endDate, numTicks)

timeAxis.tickValues(ticks)
const numTicks = Math.min(maxTicks, numPeriods)
const ticks = timeTicks(...this.timeScale.domain(), numTicks)

const timeAxis = axisBottom(this.timeScale).tickValues(ticks)
select(this.node).call(timeAxis)
}

// Set timeline width from DOM el
// Set timeline width from DOM element
setWidth = () => {
if (this.node) {
// clientWith returns 0 for SVG elements in Firefox
const box = this.node.parentNode.getBoundingClientRect()
const width = box.right - box.left - PADDING_LEFT - PADDING_RIGHT

this.setState({ width })
}
}

// Handler for period click
onPeriodClick(period) {
// Switch to period if different
if (period.id !== this.props.period.id) {
this.props.onChange(period)
}

// Stop animation if running
this.stop()
}

// Handler for play/pause button
onPlayPause = () => {
if (this.state.mode === 'play') {
this.stop()
} else {
this.play()
}
}

// Play animation
play = () => {
const { period, periods, onChange } = this.props
const sortedPeriods = sortPeriodsByLevelAndStartDate(periods)
const index = sortedPeriods.findIndex((p) => p.id === period.id)
const isLastPeriod = index === sortedPeriods.length - 1
const currentIndex = sortedPeriods.findIndex((p) => p.id === period.id)
const isLastPeriod = currentIndex === sortedPeriods.length - 1

// If new animation
if (!this.timeout) {
Expand All @@ -260,7 +154,7 @@ class Timeline extends Component {
}

// Switch to next period
onChange(sortedPeriods[index + 1])
onChange(sortedPeriods[currentIndex + 1])
}

// Call itself after DELAY
Expand All @@ -273,6 +167,105 @@ class Timeline extends Component {
clearTimeout(this.timeout)
delete this.timeout
}

// Handler for play/pause button
onPlayPause = () => {
if (this.state.mode === 'play') {
this.stop()
} else {
this.play()
}
}

// Handler for period click
onPeriodClick(period) {
// Switch to period if different
if (period.id !== this.props.period.id) {
this.props.onChange(period)
}

// Stop animation if running
this.stop()
}

componentDidMount() {
this.setWidth()
this.context.map.on('resize', this.setWidth)
}

componentDidUpdate() {
this.setTimeAxis()
}

componentWillUnmount() {
this.context.map.off('resize', this.setWidth)
}

render() {
const { mode } = this.state
const uniqueRanks = countUniqueRanks(this.props.periods)
const rectTotalHeight = RECT_HEIGHT + (uniqueRanks - 1) * RECT_OFFSET

this.setTimeScale()
return (
<svg
className={`dhis2-map-timeline ${styles.timeline}`}
style={{
height: `${32 + rectTotalHeight}`,
bottom: `30`,
}}
>
{/* Play/Pause Button */}
<g
onClick={this.onPlayPause}
transform={`translate(7,${rectTotalHeight / 2})`}
className={styles.play}
>
<path d="M0 0h24v24H0z" fillOpacity="0.0" />
{mode === 'play' ? PAUSE_ICON : PLAY_ICON}
</g>
{/* Period Rectangles */}
<g transform={`translate(${PADDING_LEFT},10)`}>
{this.getPeriodRects()}
</g>
{/* X-Axis */}
<g
transform={`translate(${PADDING_LEFT},${
12 + rectTotalHeight
})`}
ref={(node) => (this.node = node)}
/>
</svg>
)
}

// Returns array of period rectangles
getPeriodRects = () => {
const { period, periods } = this.props

const sortedPeriods = sortPeriodsByLevelRank(periods)

return sortedPeriods.map((item) => {
const isCurrent = period.id === item.id
const { id, startDate, endDate } = item
const x = this.timeScale(startDate)
const width = this.timeScale(endDate) - x

return (
<rect
key={id}
className={cx(styles.period, {
[styles.selected]: isCurrent,
})}
x={x}
y={item.levelRank * RECT_OFFSET}
width={width}
height={RECT_HEIGHT}
onClick={() => this.onPeriodClick(item)}
/>
)
})
}
}

export default Timeline

0 comments on commit 170ac85

Please sign in to comment.