While I had to integrate a timeline chart in a Vue.js project, I got a bit frustrated with existing solutions. Being confronted with poor user experiences, feature creep and a lack of simple customization options, I decided to build my own component from scratch.
I started with defining the AC. It had to be simple, yet flexible.
- Shows events in a timeline based on timestamps
- Events can be grouped per row
- Events can either be a point in time or a time range
- Events are clickable
- Markers can be added to the timeline
- Labels are displayed (years, months, hours, etc.)
- Infinitely horizontally scrollable
- Reactively updates when data changes
- A start/end timestamp can be set to limit the timeline view
- Styling and markup are customizable using CSS and slots
eventsto allow further customization
Some other solutions offer additional features, such as showing a tooltip when hovering over an event, being able to select/activate an event or displaying a marker with the current time. I decided to avoid feature creep and leave that up to the user.
I wanted to make sure the component offered a minimum starting point, while still providing an intuitive user experience and the extensibility to add additional features like the ones mentioned above.
The first step
When building a timeline, you need a starting point.
Years ago I had built a day-view timeline for a project, which had a clear start (
0:00 midnight) and end (
0:00 next day). This made it easy (or so I thought) to calculate the position of an event on the timeline. I also kept track of the zoom level and scroll position independently.
This time, that was not an option (and also a really bad idea due to rounding issues), because the timeline had to be able to scroll to the past and future indefinitely. To have a proper reference point, only the viewport (the visible part of the timeline) start and end timestamps are needed.
This solved 4 issues at once in a very clean way: by adjusting just the viewport range, you can zoom, scroll, calculate the width and position of an event and limit the rendered elements to only the ones that are visible to the user.
Based on the zoom level, the labels should be displayed in a different format. For example, when zoomed out, the labels should be displayed as years. When zoomed in, the labels should be displayed as months (or minutes/seconds when zoomed in even further).
As you don't want to display all labels at once, you need to determine which labels are relevant to show.
This was a bit of a head-scratcher, as it requires a lot of unique calculations. For each unit of time, different logic is needed. For example: there's 24 hours in a day and 60 minutes in an hour. To make things worse: there's not a set number of days in a month.
Also, when multiple units of time overlap, you'd want to display all relevant units. For example, when you're zoomed-in to time-level (hour/minutes), it would be nice to see the day-change on
0:00 as well, and on January 1st 0:00 you'd want to see which year the date belongs to.
After 2 days of work, I ended up with a component that met all the AC. There's a lot more going on than the points mentioned above, but these are the essential parts that can be used as a starting point to kickstart any similar project.
The code is live on GitHub and is licensed under the MIT license.