Search code examples
angularnvd3.jsangular2-routingng2-nvd3

Browser errors on size change when ng2-nvd3 chart is out of view


Whilst using ng2-nvd3 combined with angular 2 routing capabilities, I've noticed that the console gets full with the following error:

d3.min.js:1 Error: <g> attribute transform: Expected number, "translate(NaN,5)"

I managed to pin point the problem to a single use-case:

  • Change the route-outlet to point to a route with an ng2-nvd3 chart
  • Change it back to point to a route without an ng2-nvd3 chart
  • Change the size of the window (or the size of the developer console)
  • See error in console

This can be easily reproduced with this plunker:

  • Open the developer console
  • Click on the "Chart" link
  • Click on the "No Chart" link
  • Change the size of the developer console (or the browser window - same effect) - make sure to release the mouse button and not just change the size.
  • Error appears in the console

How can one avoid this error?


Solution

  • In the ng2-nvd3 source code line 118 we can see that there is a resize handler being registered

    self.chart.resizeHandler = nv.utils.windowResize(function() {...})
    

    This resize handler needs to be cleared when the chart gets out of view (like in the case above) - yet it won't happen automatically.

    To solve this one need to get a reference to the chart itself by adding

    @ViewChild('chart') chart;
    

    To the component (and naming the chart in the template by adding #chart attribute to the nvd3 element) This will allow us to invoke

    this.chart.chart.resizeHandler.clear();
    

    To clear out the handler from the resize event. But we still need to know when the chart is out of view - there are many ways to achieve that, each with it's own subtleties (ngOnDestroy / ngOnChanges etc.). The one that worked for me is, though a bit aggressive, cleared the problem completely:

    I've injected the root Router to the main component and subscribed to changes on it, delegating them via a service with an EventEmitter Then I've injected that service to the chart component and registered to that event (as far as I checked, you can't just inject the router to the chart component as it won't be the root router but rather some child router and thus won't get all the route changes) In the event handler I've created a one-off invocation of

    this.chart.chart.resizeHandler.clear();
    

    RoutingService:

    import { Injectable, EventEmitter } from 'angular2/core';
    
    @Injectable()
    export class RoutingService {
        public onRouteChange$: EventEmitter<string>;
    
        constructor() {
            this.onRouteChange$ = new EventEmitter();
        }
    
        routeChange(route: string) {
            this.onRouteChange$.emit(route);
        }
    }
    

    MainComponent constructor:

      constructor(private router:Router, private routingService:RoutingService){
        router.subscribe( val => routingService.routeChange(val));
      }
    

    ChartComponent constructor:

      constructor(private routingService:RoutingService) {
        let sub:any = routingService.onRouteChange$.subscribe(route => {
            if (route != 'chart') {
                this.chart.chart.resizeHandler.clear();
                if (sub) {
                  sub.unsubscribe();
                }
            }
        });
      }
    

    You can see it in the this plunker