The aim is to provide a generic and customizable implementation of a timeline with d3.js.
The result is that you fully keep control how the elements are rendered, with a set of performance optimizations, advanced behaviors for an improved user experience, and an understandable representation of data injections and surjections on distribution change.
Basically, it renders elements with x position based on its start
time, end
time and y position based on the row it belongs to in the provided data set. The whole is surrounded with a time axis (top) and a left axis not limited to any type of data.
Before being a timeline and depending on time, an instance of D3Timeline
is a D3BlockTable
and by extension an instance of D3Table
. This means the D3BlockTable
constructor can be used in case you don't want to represent data over time.
Some details:
- using
D3Table
extendingEventEmitter
:- you fully control how each element is rendered based on its data
- since elements have positions, culling is implemented
- using
D3BlockTable
extendingD3Table
:- you have a sized rect based on data properties
- since elements are blocks, clipping is implemented
- since elements are blocks, dragging is implemented with automatic scroll on borders
- a text element can be created for you, vertically aligned
- when a text is created, keeping text left alignment is implemented for handling the left part of the block being outside of the graph
- using
D3Timeline
extendingD3BlockTable
:- the X dimension becomes the time
The demo uses the latter based on some information generated with Faker.
The emitted events are:
"timeline:click"
: timeline is clicked (not an element in it)"timeline:move"
: timeline is moving (inside move, not its container)"timeline:resize"
: timeline is being resized"timeline:element:click"
: an element is clicked"timeline:element:dragend"
: an element is dropped.
These events are emitted with the following arguments provided to the listener:
data
: (only for"timeline:element:*"
events): the data bound to the elementtimeline
: the timeline instanceselection
: the d3 selection concernedd3Event
: the d3 event (as it's a reference, you do not lose it in asynchronous code)getTime
: a getter for the time concernedgetRow
: a getter for the row concerned.
Markers are totally separate elements. You provide them the timeline instance and it internally knows how to behave when the timeline moves. You can dynamically set them with a new time, so that it moves in the timeline. It represents itself as a vertical line, taking the whole timeline body height, with an overridable formatter for the label at the top of the line.
That being said, you have special types of markers using the D3TableMarker
(extending EventEmitter
):
- using
D3TableMouseTracker
extendingD3TableMarker
:- the marker will follow the mouse position and disappear when it's outside
- using
D3TableValueTracker
extendingD3TableMarker
:- the started marker will automatically follow the given value and disappear when it's outside
- using
D3TimelineTimeTracker
extendingD3TableValueTracker
:- the started tracker will automatically follow the current time and disappear when it's outside.
Data provided to the timeline instance have this basic structure:
Array<{
id: Number,
name: String,
elements: Array<{
id: Number,
uid: Number,
start: Date,
end: Date
}>
}>
Because you may change the dimension used for elements distribution in timeline rows. This type of change is not always a bijection, thus resulting in some elements having to merge into one (surjection), or one to become multiple separate elements (injection) when setting the same data but with a different distribution.
For this to be generically interpreted by the timeline, you have to provide an id
which can exist several times, and a uid
which can exist only once and that will be used by d3 to match with existing data.
The id
would be sufficient if each entity was going to be represented once. But if you choose a distribution based on another entity with a n:m relation with the former, your entity is likely to be represented several times, and with the same id
.
For example:
- injection: a distribution change may make a single element becoming several; an entering element (with not matched
uid
) will find its transform transition start (on being appended) with itsid
since a single element with thisid
was existing in the previous data set distribution. - surjection: conversely, a distribution change may make several elements becoming one; an exiting element (with not matched
uid
) will find the transform transition end (before removal) with itsid
since a single element with thisid
is entering in the new data set distribution.
- d3 is not included in the built file. If you include the built file, make sure d3 is included before timeline creation. If you import it with bundler tools like browserify or webpack, d3 is referenced as an import in the core files.
- requestAnimationFrame and cancelAnimationFrame and es5 shims may be included to support older browsers.
Tested on Chrome (desktop and android), Firefox, Safari (desktop and ipad).
Visit this block timeline demo to see d3-timeline at work, with options being controlled with dat-gui.
The responsiveness is based on window width and height.
To experiment improvements in the demo, run grunt
then make your edits and refresh (I didn't want livereload).
To build the dist file before committing, run grunt build
.
As of now, there are only end to end tests, which ensure:
- elements are drawn at the correct place with the right dimensions
- elements are clickable and emit the corresponding event
- elements are draggable and emit the corresponding events.
- pan X and Y while dragging
- pan Y while using the mousewheel
- zoom X while using zoom touch gesture or using the mousewheel with the control key pressed
- update X axises ticks interval while zooming X based on optional configuration
- clamped pan behaviors based on scales domains
- element drag and drop
- automatic scroll on element drag
- culling for elements outside the viewport even if in the dataset
- pan can render only on idle, making the culling visible (and that's the reason why culling distance can be configured)
- element drag can cause render only on idle too