Data Table Filtering Using a D3 Timeline
A step-by-step tutorial on how to use Angular framework, D3.js, and Bootstrap to implement data filtering and visualization using a D3 timeline.
Join the DZone community and get the full member experience.
Join For FreeHere I am going to explain how we can implement data table filtering using a timeline. It will be a great approach to interact with records in a table and also to have better visualization of the data. To make this happen, I will use Angular framework, D3.js, moment.js, and a bit of Bootstrap.
Getting Started
Start with generating an Angular application :
ng new filtering-timeline
Once the app is created, we are ready to install all the required libraries I mentioned above.
npm install bootstrap
Note that bootstrap links should be added in angular.json
. The last step is the installation of the D3 library.
npm install d3 && npm install @types/d3 --save-dev
That is it with setting up the environment, except for one thing — the data set. For that purpose, I prepared some random records about earthquakes that happened during 2021. I placed these in a separate typescript file in assets. Here is the example:
export const DATA: any = [
{
place: 'Týrnavos, Greece',
magnitude: 6.3,
depth: 8,
distance: 47,
time: 1614766569000
},
];
Let's add representation of this data in an application. To achieve that, we can simply implement a bootstrap data table in app.component.html.
<header>
<span>Earthquakes analytics</span>
</header>
<section class="title">
<div class="table-container">
<div class="widget-header">Earthquekes info</div>
<table id="example" class="table table-striped" style="width:100%">
<thead>
<tr>
<th>Place</th>
<th>Magnitude</th>
<th>Depth</th>
<th>Distance</th>
<th>Time</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let item of data">
<td>{{item.place}}</td>
<td>{{item.magnitude}} mmw</td>
<td>{{item.depth}} km</td>
<td>{{item.distance}} km</td>
<td>{{item.time | date:"dd/MM/yy"}}</td>
</tr>
</tbody>
<tfoot>
</tfoot>
</table>
</div>
</section>
After some styling enhancements, the app should now look like this:
Here comes the main part: timeline implementation. It's better to keep that functionality in a separate part of a project, for example in the custom directive, so we need to create one and add it to the template.
ng g directive timeline
Now it can be inserted into the app.component
template.
<div class="timeline-container">
<div class="widget-header">Timeline</div>
<div class="timeline" appTimeline [data]="data"></div>
</div>
Inside the directive, we need to import D3 and moment.js. An ElementRef wrapper is also required to reference our HTML element where we will append the timeline.
After that, create a function named initTimeLine
that will draw the timeline. Inside it, we can begin defining the dimensions of an element.
const element = this.element.nativeElement;
const width = 1100;
const height = 100;
Then time range of the timeline has to be added. It is going to be the start and end of the 2021 year.
const maxTs = 1640980799000;
const minTs = 1609444800000;
Now, we can start drawing the x-axis.
// Appending svg to the div
const svg = d3.select(element)
.append("svg")
.attr("viewBox", `0 -20 1100 100`)
// Appending context
const context = svg.append('g')
.attr('class', 'context')
.attr('transform', 'translate("40, 125")');
// Adding timeline timescale
const x = d3.scaleTime()
.range([0, width])
.domain([new Date(minTs), new Date(maxTs)]);
// Adding x axis
const xAxis = d3.axisBottom(x);
// Appending ticks
context.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0,50)")
.call(xAxis);
The next step is to add data representation.
// Transforming data to bars representation
const bar = svg.selectAll('bar').data(this.data).enter();
// Appending bars to timeline and placing them in accordance with time
bar.append('rect')
.attr('x', (d: any) => x(d.time))
.attr('width', '10px')
.attr('height', height/2)
.attr('fill', '#007FFF')
And the final step is to implement a brush to be able to filter out data.
// Adding brush timescale
const x2 = d3.scaleTime()
.range([0, width])
.domain(x.domain());
// Adding brush
const brush = d3.brushX(x2)
.extent([[0, 0], [width, 50]]);
// Appending brush
svg.append('g')
.call(brush)
.call(brush.move, x.range())
.selectAll('rect')
.attr('y', 0);
The result is a timeline with a brush, which allows us to select the specific timeframe. Still, it does not have any impact on the data table and does not filter records in it. To achieve the desired functionality, we need to improve our logic a bit.
Let's add a callback function to the brush, which will be fired once we stop brushing. This function will return the selected time frame, which is needed to convert timestamps and pass to the app component using the @Output decorator and EventEmitter.
//Updating brush with callback
const brush = d3.brushX(x2)
.extent([[0, 0], [width, 50]])
.on('end', brushed);
//Adding @Output decorator to a timeline directive
@Output() timeRange = new EventEmitter<object>();
//Implementing brushed function
const brushed = (event: any) => {
// selected area on timeline
const selection = event.selection;
// transforming selected area to timestamps
const dateRange = selection.map(x.invert, x);
const timeStart = moment(dateRange[0]).valueOf();
const timeEnd = moment(dateRange[1]).valueOf();
const updatedTs = {
tsMin: timeStart,
tsMax: timeEnd
};
// sending new timestamps to app component
this.timeRange.emit(updatedTs);
};
Return to the app.component
to finalize everything. Now we are passing the updated timeframe from the timeline. The last thing is to properly implement the data table filtering according to the new timestamps. For that, custom pipe can be used. It receives a time range and filters the data set.
app.component.html
//Adding pipe to *ngFor
<tr *ngFor="let item of data | filterTime:timeStart:timeEnd;">
<td>{{item.place}}</td>
<td>{{item.magnitude}} mmw</td>
<td>{{item.depth}} km</td>
<td>{{item.distance}} km</td>
<td>{{item.time | date:"dd/MM/yy"}}</td>
</tr>
filter-time.pipe.ts
//filterTime pipe implementation
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'filterTime'
})
export class FilterTimePipe implements PipeTransform {
transform(items: any[], tsMin: number, tsMax: number): any {
if (!items || !tsMin || !tsMax) {
return items;
}
return items.filter(item => item.time > tsMin && item.time < tsMax);
}
}
Now the application should look like this:
And here is the way it looks after brushing:
That's it! You can try it on your own. There are also some improvements that can be made, such as changing the bar's height depending on the earthquake's magnitude. The source code of the app can be found in my GitHub.
Opinions expressed by DZone contributors are their own.
Comments