Search code examples
angularchart.jsstorybookchoroplethangular-storybook

Storybook: Changing the value of the control doesnot rerender the Chart.js canvas


I am using Angular based Storybook. All I wanted to do is to re-render the chart based on the values given in the Storybook's control. But the Chart remains static even after changing the value of the control. I tried so many workarounds, but still am at square one. The chart I wanted to display is a choropleth. I have used Chartjs and chartjs-chart-geo library to display the chart.

My component in Storybook :

import { Component, Input, Output, EventEmitter, OnInit } from '@angular/core';
import * as Chart from 'chart.js';
import * as ChartGeo from 'chartjs-chart-geo';
import { HttpClient } from '@angular/common/http';
@Component({
    selector: 'storybook-choropleth',
    template: `<div>
    <canvas id="mapCanvas"></canvas>
  </div>
  `,
    styleUrls: ['./choropleth.css'],
})
export default class ChoroplethComponent implements OnInit {
    @Input()
    url = 'https://unpkg.com/world-atlas/countries-50m.json';
    /**
     * Type of projecttion
     */
    // @Input()
    chartProjection: 'azimuthalEqualArea' | 'azimuthalEquidistant' | 'gnomonic' | 'orthographic' | 'stereographic'
        | 'equalEarth' | 'albers' | 'albersUsa' | 'conicConformal' | 'conicEqualArea' | 'conicEquidistant' | 'equirectangular' | 'mercator'
        | 'transverseMercator' | 'naturalEarth1' = 'mercator';
    chart: any;
    geoData: any;
    countries: any;
    constructor(
        private http: HttpClient
    ) {
    }
    ngOnInit() {
        this.getGeoData();
    }
    getGeoData() {
        this.http.get(this.url).subscribe((data) => {
            this.countries = ChartGeo.topojson.feature(data, data['objects']['countries']).features;
            let t = <HTMLCanvasElement>document.getElementById('mapCanvas');
            if (this.chart !== undefined) {
                this.chart.destroy();
            }
            // exclude antartica
            this.countries.splice(239, 1);
            console.log(this.countries);
            let dts = {
                labels: this.countries.map((d) => d.properties.name),
                datasets: [{
                    label: 'Countries',
                    data: this.countries.map((d) => ({ feature: d, value: Math.random() })),
                }]
            };
            console.log(this.countries);
            let configOptions = {
                maintainAspectRatio: true,
                responsive: true,
                showOutline: false,
                showGraticule: false,
                scale: {
                    projection: this.chartProjection
                } as any,
                geo: {
                    colorScale: {
                        display: true,
                        interpolate: 'blues',
                        missing: 'white',
                        legend: {
                            display: 'true',
                            position: 'bottom-right'
                        }
                    }
                }
            };
            
            this.chart = new Chart(t.getContext('2d'),
                {
                    type: 'choropleth',
                    data: dts,
                    options: configOptions
                }
            );
        });
    }
    getDts() {
        this.getGeoData();
        let dts = {
            labels: this.geoData.map((i) => i.properties.name),
            datasets: [
                {
                    outline: this.geoData,
                    data: this.geoData.map((i) => ({
                        feature: i,
                        value: i.properties.confirmed
                    }))
                }
            ]
        };
        return dts;
    }
    getConfigOptions() {
        let configOptions = {
            maintainAspectRatio: true,
            responsive: true,
            showOutline: false,
            showGraticule: false,
            scale: {
                projection: 'mercator'
            } as any,
            geo: {
                colorScale: {
                    display: true,
                    interpolate: 'reds',
                    missing: 'white',
                    legend: {
                        display: 'true',
                        position: 'bottom-right'
                    }
                }
            }
        };
        return configOptions;
    }
}

My story.ts:

import { Story, Meta, moduleMetadata } from '@storybook/angular';
import Choropleth from './choropleth.component';
import { HttpClientModule } from '@angular/common/http';
import { withKnobs } from '@storybook/addon-knobs';
import { NO_ERRORS_SCHEMA } from '@angular/core';
export default {
    title: 'Choropleth',
    component: Choropleth,
    decorators: [
        withKnobs,
        moduleMetadata({
            //:point_down: Imports both components to allow component composition with storybook
            imports: [HttpClientModule],
            schemas: [NO_ERRORS_SCHEMA],
        })
    ],
    argTypes: {
        backgroundColor: { control: 'color' }
    },
} as Meta;
const Template: Story<Choropleth> = (args: Choropleth) => ({
    component: Choropleth,
    props: args
});
export const Primary = Template.bind({});
Primary.args = {
    url: 'https://unpkg.com/world-atlas/countries-50m.json'
};

In this, I wanted to keep the URL dynamic. As the URL is changed in the storybook's control, I wanted to re-render the map accordingly. Any help would be hugely appreciated.


Solution

  • The getGeoData method which sets up the chart is called only during component initialization and it wont run when @Input values change. For these scenarios Angular provides ngOnChanges lifecycle hook. And this is where we need to tell Angular what needs to be done when @Input values change.

    ngOnChanges(changes: SimpleChanges) {
      for (const propName in changes) {
        switch(propName) {
          case 'url':
          // action that needs to happen when url changes
          break;
          case 'chartProjection':
          // action that needs to happen when chartProjection changes
          break;
        }
      }
    }