D3 chart data scrubber in Angular

By Jenny Fung on June 5th, 2018

When Angular (Angular 2) came out in 2016, there was a vacuum period when popular libraries such as d3.js, Leaflet, and even Bootstrap, lagged behind in compatibility. Two years later, the Angular community has come a long way with reliable support libraries and blog posts. In 2016 we endeavored to use Angular and d3.js to visualize projected climate data from Azavea’s Climate API. One particular challenge was creating a data scrubber, a moving display of moused-over data in the d3.js visualization. The scrubber should:

  1. Update the data textbox while mousing over the visualization
  2. Pointer tip aligns with the data point
  3. Compatible with modern evergreen browsers (Edge, Firefox, Chrome, Safari)

 

Temperate data scrubber

Above: The finished product in Chrome. The moving feature is the data scrubber.

This blog post is a belated lessons learned on the data scrubber feature. The starting point assumes you have a working, dynamic d3 visualization that rerenders on data changes. For setup, see here or here.

The data scrubber and tooltips

Standard d3-tooltips are static, attached attributes which isn’t what the scrubber is. Instead, we layered SVG elements that are created, transformed, and destroyed based on mouse events.

private drawScrubber(): void {
  // Vertical scrub line. Exists outside scrubber cluster because it moves independently
  this.svg.append('line')
    .attr('class', 'scrubline' + ' ' + this.id)
    .attr('x1', 0).attr('x2', 0)
    .attr('y1', 0).attr('y2', this.height)
    .classed('hidden', true);

  // Other scrubber elements
  this.scrubber = this.svg.append('g')
    .attr('class', this.id)
    .classed('hidden', true);

  this.scrubber.append('circle')
    .attr('r', 4.5);
  
  // Pseudo-textbox
  this.scrubber.append('rect')
    .attr('class', 'scrubber-box' + ' ' + this.id)
    .attr('height', 20);
  
  this.scrubber.append('text')
    .attr('class', 'scrubber-text' + ' ' + this.id);

 

Multi-browser event listening

Events are tricky, especially among browsers. $event properties and their values differ across browsers as well as across event capture tools. For a scrubber, we need two consistent pieces of DOM information from the$event: a mouseover boolean and absolute x-coordinates. Here’s an evaluation of various javascript event handlers that were attempted:

  1. d3’s built-in SVG event handler (e.g, your_svg.on('mouseover')) — Missing crucial event positioning information in Firefox.
  2. jQuery events — Desired event information was available across browsers, and event listeners expired after first use. Not useful, since we want continuous listening.
  3. Angular HostListeners— Most functional solution because we get x-coordinate info and continuous listening.
@HostListener('mouseover', ['$event'])

Now we have an event listener to toggle and transform the scrubber elements. Is it listening in the right place?

SVG madness

A step further, the scrubber should only operate if the mouse hovers the bounds of the visualization, disappearing upon mouse-off. No d3 element in the line graph covers just that area — they also house axes labels, padding, etc. To remedy, we overlaid a transparent SVG to the graph’s exact dimensions. We must detect the mouse moving over the transparent SVG to show and update the scrubber.

Parent component

  public isHovering = false;
 
  // Mousemove event must be at this level to listen to mousing over rect#overlay
  @HostListener('mouseover', ['$event'])
  onMouseOver(event) {
    this.isHovering = event.target.id === 'overlay' ? true : false;
  }

Parent template

<ccc-line-graph
  [hover]="isHovering">
</ccc-line-graph>

Child component

export class LineGraphComponent implements OnChanges {

  @Input() public hover: Boolean;
  
  private svg;
  private id: string;

  // If the chart is being hovered over, handle mouse movements
  @HostListener('mousemove', ['$event'])
  onMouseMove(event) {
    if (this.hover) {
      this.redrawScrubber(event);
    }
  }

  ngOnChanges(): void {
    this.drawScrubber();
  }

  private drawScrubber(): void {
    /* For simplicity, omit scrubber svg code snippet from above */
    
    // Scrubber sensory zone (set to size of graph) intentionally drawn last
    // It overlays all other svg elements for uncompromised mouseover detection
    this.svg.append('rect')
      .attr('id', 'overlay')
      .attr('height', this.height)
      .attr('width', this.width);
 
    // Toggle scrubber visibility
    this.hover ? $('.' + this.id).toggleClass('hidden', false) :  $('.' + this.id).toggleClass('hidden', true);
  }  

  private redrawScrubber(event) {
    /* For simplicity, omit code that transforms scrubber along the x-plane*/
  }

Input and ngOnChanges()is a powerful combination that makes graphing on-the-fly possible. Detecting mouseover from the parent leverages `ngOnChanges` change detection and makes the scrubber more responsive to being displayed and removed. An earlier attempt relied only on listening at the line-graph level and produced an inconsistent and jumpy scrubber. I don’t know the expense of having two interacting HostListeners, but it hasn’t negatively impacted performance at the tidy size of our app. With minor adjustments, this communication and SVG structure will also work if for multiple visualizations and scrubbers. Note a caveat that the scrubber follows the data points, not the line, so it a better solution for more complex data visualizations.

Check out the full code for some browser-specific adjustments were made to control for differences in event x-coordinate calculations.

All in all, the end result was a smooth data scrubber in evergreen browsers:

Chrome

IE11

Firefox

Animated GIF - Find & Share on GIPHY

Safari

Animated GIF - Find & Share on GIPHY

Some simplifications to the code snippets were made for this post. Check out the fully open-source project code on Stackblitz and its current code.