d

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore.

15 St Margarets, NY 10033
(+381) 11 123 4567
ouroffice@aware.com

 

KMF

Data Table Filtering Using a D3 Timeline

Here 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. 

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:Earthquakes analytics screenshot.

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. 

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:

Earthquake analytics screenshot #2.

And here is the way it looks after brushing:  

Earthquake analytics screenshot #3.

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.

Credit: Source link

Previous Next
Close
Test Caption
Test Description goes like this