ng2-charts icon indicating copy to clipboard operation
ng2-charts copied to clipboard

How to Custom legend template in ng2-chart

Open chauanthuan opened this issue 8 years ago • 30 comments

Hi everybody, Can anyone help me how to make custom legend in chartjs when using ng2-chart. In Chart.js V2 document, i using function generateLegend() to make it, but in angular 2 i dont see it. Thank you! Ps: Sorry, my english is not good.

chauanthuan avatar Oct 14 '16 10:10 chauanthuan

I don't know if you need this answer anymore but I had the same problem and would like to post my solution for others to benefit as well.

To create a custom legend using ng2-chart you have to do the following.

  1. Get a reference to the Chart using @ViewChild in your Component code

`import { BaseChartDirective } from 'ng2-charts';

@Component({ selector: 'my-selector', templateUrl: 'my-html-code.html' }) export class MyChartComponent { // ... stuff @ViewChild(BaseChartDirective) chartComponent: BaseChartDirective; legendData: any; // ... stuff }`

  1. For the input parameter you feed to the Chart, set the legend to "false" like so:

<canvas baseChart #myChart ... [options]="myChartOptions" [legend]="false" ... ></canvas>

This is so you can turn off the legend that is drawn directly to the canvas

  1. Create a custom callback function to replace the generateLegend() function with your own implementation. I used a closure so my callback function has access to my component variables and functions like so (this html editor doesn't seem to format the below javascript code):

private getLegendCallback = (function(self) { function handle(chart) { // Do stuff here to return an object model. // Do not return a string of html or anything like that. // You can return the legendItems directly or // you can use the reference to self to create // an object that contains whatever values you // need to customize your legend. return chart.legend.legendItems; } return function(chart) { return handle(chart); } })(this);

  1. Replace the default generateLegend function with your custom function in your options

myChartOptions = { /* ... stuff ... /, legendCallback: this.getLegendCallback, / ... stuff ... */ };

  1. Wherever it is appropriate in your code, execute the generateLegend( ) function that is now wired to your custom function, which will return an object containing your desired values and assign the object to a Component variable.

this.legendData = this.chartComponent.chart.generateLegend( );

  1. Now that you have the legend data in a variable, you have successfully brought the legend functionality into vanilla Angular 2 land. So, create a div and craft your legend as you please.

< div *ngIf="legendData"> < ul class="my-legend"> < li class="legend-item" *ngFor="let legendItem of legendData">{{legendItem.text}}</ li> </ ul> </ div>

The legendItem object will have the colors, fonts and all sorts of stuff. As I said before, if you need anything extra, just create and return an object that has whatever you need from your custom generateLegend( ) function. You can also add click events here the way you normally would in Angular. It worked beautifully for me.

I hope this helps.

Ricardo

rickyricky74 avatar Mar 24 '17 22:03 rickyricky74

Hi Ricardo, Can you please provide an example of what goes inside the method.

private getLegendCallback = (function(self) { function handle(chart) { // Do stuff here to return an object model. // Do not return a string of html or anything like that. // You can return the legendItems directly or // you can use the reference to self to create // an object that contains whatever values you // need to customize your legend. return chart.legend.legendItems; } return function(chart) { return handle(chart); } })(this);

bkartik2005 avatar Apr 14 '17 11:04 bkartik2005

The example I posted already has something inside the method. If you get rid of the comments I posted inside the function, you will see that what's left is a return statement with the legend items object provided by Chart.js. See below:

private getLegendCallback = (function(self) { function handle(chart) { return chart.legend.legendItems; // <-- THIS ... comes out in #5 of my orig post } return function(chart) { return handle(chart); } })(this);

This returns the legend items from the Chart as-is. You have the option of customizing that return value. In my case, I needed to add the value of the "slope" of the line to each legend item, which is not provided by Chart.js. This came from my calculations so I had to add the slope to each legend item by referencing the variable through the "self" reference.

Assuming you have something like this somewhere in your component ...

private slopes: any = { }; // populated from some ajax call or whatever

... then instead of this ...

return chart.legend.legendItems;

... you can do this ...

return chart.legend.legendItems.map(function(i) { i['slope'] = self.slopes[i.text]; return i; });

or something like that (pulling that from memory ... not tested ... but you get the gist).

I hope that helps.

Ricardo

rickyricky74 avatar Apr 14 '17 13:04 rickyricky74

hi Ricardo, I followed your instructions. Here is the snippet of my code.

legendData: any;

pieChartOptions: any = { legend: { legendCallback: this.getLegendCallback } }

private getLegendCallback = (function(self) { function handle(chart) { return chart.legend.legendItems; } return function(chart) { return handle(chart); } })(this);


HTML

  • {{legendItem.text}}
--------------------------------------------------------------

With this code, i am getting the following error.

Error in ./PieChartComponent class PieChartComponent - inline template:19:33 caused by: Cannot find a differ supporting object '

    ' of type 'string'. NgFor only supports binding to Iterables such as Arrays.

    Do you see anything wrong in my code.?

    bkartik2005 avatar Apr 14 '17 15:04 bkartik2005

    The code you posted is incomplete. I don't see the ngFor in your code so I can't see what object it's trying to iterate through. Make sure your legendData variable is an object or array that ngFor can iterate through. Set a break point in the browser at the return statement in the custom getLegendCallback function so you can inspect the contents of the legendItems and ensure its an array. Bottom line is I don't have enough information to spot the problem.

    rickyricky74 avatar Apr 14 '17 17:04 rickyricky74

    Sorry, did not paste the html code properly. Here is my HTML code

    <div >
        <div>
            <canvas baseChart
                    [(data)] = "totalCRCount"
                    [(labels)]="totalLabel"
                    [chartType]="chartType"
                    [options]="pieChartOptions"
                    [legend]="false">
            </canvas>
        </div>
        <div *ngIf="legendData">
            <ul class="my-legend">
             <li class="legend-item" *ngFor="let legendItem of legendData">{{legendItem.text}}</li>
            </ul>
        </div>  
    </div>
    

    Here is my ChartOptions code

    pieChartOptions: any = {
            legend: {
                legendCallback: this.getLegendCallback
            } 
        }
    
    private getLegendCallback = (function (self) {
            function handle(chart) {
                return chart.legend.legendItems;
            }
    
            return function (chart) {
                return handle(chart);
            }
        })(this);
    

    When i debug, i see the this.chart.chart.generateLegend() returns "<ul class="6-legend"></ul>" and never hits the break point at getLegendCallback().

    Here is the data

    [ { "year": 2017, "priority": "High", "crCount": 1 }, { "year": 2017, "priority": "E.R", "crCount": 1 }, { "year": 2017, "priority": "Normal", "crCount": 263 }, { "year": 2016, "priority": "High", "crCount": 6 }, { "year": 2016, "priority": "E.R", "crCount": 7 }, { "year": 2016, "priority": "Normal", "crCount": 1452 }, { "year": 2015, "priority": "E.R", "crCount": 4 }, { "year": 2015, "priority": "High", "crCount": 35 }, { "year": 2015, "priority": "Normal", "crCount": 825 }, { "year": 2014, "priority": "E.R", "crCount": 2 }, { "year": 2014, "priority": "High", "crCount": 41 }, { "year": 2014, "priority": "Normal", "crCount": 640 }, { "year": 2013, "priority": "E.R", "crCount": 1 }, { "year": 2013, "priority": "High", "crCount": 21 }, { "year": 2013, "priority": "Normal", "crCount": 418 }, { "year": 2012, "priority": "E.R", "crCount": 1 }, { "year": 2012, "priority": "High", "crCount": 10 }, { "year": 2012, "priority": "Normal", "crCount": 105 } ]

    bkartik2005 avatar Apr 14 '17 19:04 bkartik2005

    As far as the break point not hitting the custom function, it means that somehow your options are not being set properly. You might want to add a semi-colon after the closing curly brace of your options object just in case (not sure that matters). Other than that, I cannot see the problem. My guess is that the options are not being set for the chart somehow but I don't see why that is looking at the code you posted. EDIT: Scratch my previous statement about *ngFor ... was looking at your ngIf. Sorry.

    rickyricky74 avatar Apr 14 '17 19:04 rickyricky74

    I'm using Chart.js v2.5.0 by the way. Perhaps you're using a different version. In that case the placement of the legendCallback in the options object may be different.

    rickyricky74 avatar Apr 14 '17 19:04 rickyricky74

    Hi @rickyricky74, I'm flowed the step you posted, but I'm getting this error:

    ERROR TypeError: Cannot read property 'generateLegend' of undefined

    Can you help me to solve this ?

    This is my code:

    import { BaseChartDirective } from 'ng2-charts'; import {Component, OnInit, ViewChild} from '@angular/core';

    @Component({ selector: 'custom-chart', templateUrl: 'custom.chart.component.html' }) export class CustomChartComponent implements OnInit {

    @ViewChild(BaseChartDirective) chartComponent: BaseChartDirective;
    legendData: any;
    
    private getLegendCallback = (function(self) {
        function handle(chart) {
            // Do stuff here to return an object model.
            // Do not return a string of html or anything like that.
            // You can return the legendItems directly or
            // you can use the reference to self to create
            // an object that contains whatever values you
            // need to customize your legend.
            return chart.legend.legendItems;
        }
        return function(chart) {
            return handle(chart);
        };
    })(this);
    
    myChartOptions = {
        responsive: true,
        legendCallback: this.getLegendCallback
    };
    
    public lineChartLabels: Array<any> = ['SUN', 'MON', 'TUE', 'WED', 'THU', 'FR', 'SAT'];
    public lineChartType = 'line';
    // public lineChartOptions: any = {
    //    responsive: true
    // };
    
    public lineChartColors: Array<any> = [
        {
            backgroundColor: 'rgba(101,120,196,0.3)',
            borderColor: 'rgb(101,120,196)',
            pointBackgroundColor: 'rgb(101,120,196)',
            pointBorderColor: '#fff',
        },
        {
            backgroundColor: 'rgba(25,209,185,0.3)',
            borderColor: 'rgb(25,209,185)',
            pointBackgroundColor: 'rgb(25,209,185)',
            pointBorderColor: '#fff',
        }
    ];
    
    public lineChartColors2: Array<any> = [
        {
            backgroundColor: 'rgba(217,93,121,0.3)',
            borderColor: 'rgb(217,93,121)',
            pointBackgroundColor: 'rgb(217,93,121)',
            pointBorderColor: '#fff',
        },
        {
            backgroundColor: 'rgba(249,174,91,0.3)',
            borderColor: 'rgb(249,174,91)',
            pointBackgroundColor: 'rgb(249,174,91)',
            pointBorderColor: '#fff',
        }
    ];
    
    ngOnInit() {
        this.legendData = this.chartComponent.chart.generateLegend( );
    }
    

    }

    And this is how I used in the view:

    <custom-chart></custom-chart>

    yunier0525 avatar Sep 14 '17 17:09 yunier0525

    Hi @rickyricky74, I'm flowed the step you posted, but I'm getting this error:

    ERROR TypeError: Cannot read property 'generateLegend' of undefined

    Can you help me to solve this ?

    The angular DOM not detect the changes when the Component receive the data. To fix it, you need to add the detectChanges of ChangeDetectorRef.

    <canvas baseChart
        [datasets]="chartConfig.data"
        [labels]="chartConfig.labels"
        chartType="line"
        [options]="chartConfig.options"
        [legend]="false">
    </canvas>
    
    import { ChangeDetectorRef } from '@angular/core';
    
    @ViewChild(BaseChartDirective) baseChart: BaseChartDirective;
    constructor(private cdRef: ChangeDetectorRef) { }
    
    this.service.getChartData().subscribe(
        chartData => {
            this.chartConfig.data = chartData;
            this.cdRef.detectChanges();
            this.loadLegend();
        }
    }
    private loadLegend() {
        this.baseChart.chart.generateLegend();
    }
    

    This works for me.

    JacoboSegovia avatar Nov 22 '17 14:11 JacoboSegovia

    @JacoboSegovia Can you please provide a full example with details on what code goes where? I am not sure what this.service refers to.

    Thanks!

    areknow avatar Jan 11 '18 16:01 areknow

    I'm actually facing a similar issue. I"ve make a custom legend component and I'm able to show/hide using the above codes. However, this only works for bar chart. It raise the following exception when using it with donut chart:

    TypeError: Cannot read property '_meta' of undefined at Chart.getDatasetMeta (core.controller.js:656)

    Have anyone faced a similar issue please?

    GeraudWilling avatar Jan 23 '18 17:01 GeraudWilling

    I am facing same issue. If a I populate label details from initialized array, the label field gets rendered on html page and gets populated in data label. However if I use the extracted data from my service class(of same data type and values), data does not populate on html page, even if the values are extracted correctly from service class. This is what I am getting now. image

    And this is what I expect: image

    Also, label data should be shown on hower

    sshreya0808 avatar Feb 01 '18 12:02 sshreya0808

    @GeraudWilling the problem is when you call the function. try like this only to see if work

    in the component test(){ this.legendData = this.chartComponent.chart.generateLegend(); }

    in the view <button (click)="test()">Test Labels

    Sorry my english

    rudighert avatar Feb 15 '18 20:02 rudighert

    There is a simple way to get access to the auto generated legend object properties separately. Use the following:

    First add

    import { ChangeDetectorRef } from '@angular/core'
    import { BaseChartDirective } from 'ng2-charts'
    ..
    ..
    @ViewChild('partsBaseChart') partsBaseChart: BaseChartDirective
    

    After fetching your data, request defect change:

    
    return this.fetch(url).subscribe(
          (res: any)=> {
           
            // ... do something
            this.cdRef.detectChanges()
            this.partsChartLegendItems = this.partsBaseChart.chart.legend.legendItems
            // ...
          },
          (err: any)=> {
             // do handle errors ..
          }
        )
    

    Sample of the output of this.partsBaseChart.chart.legend.legendItems

    [{"text":"...","fillStyle":"rgba(255,99,132,0.6)","strokeStyle":"#fff","lineWidth":2,"hidden":false,"index":0}, ....]
    

    Chart options:

    pieChartOptions: any = {
      // no need for legend configuration at all
    }
    

    HTML Template:

    <canvas baseChart #partsBaseChart="base-chart" [legend]="!1" [data]="partsChartData" [options]="pieChartOptions" [labels]="partsChartLabels" [chartType]="pieChartType"></canvas>
    <div *ngIf="partsChartLegendItems">
        <ul class="custom-legend-list">
            <li *ngFor="let item of partsChartLegendItems;let i = index" class="custom-legend-item" (click)="partsChartData[i]>0 ? legendOnClick(item.text):!1">
                <span class="slice-color" [ngStyle]="{'background-color': item.fillStyle}"></span>
                <a href="javascript: void(0)" class="slice-title">{{ item.text }} ({{ partsChartData[i]||0 }})</a>
            </li>
        </ul>
    </div>
    

    sgaawc avatar Mar 13 '18 08:03 sgaawc

    Hi, im trying to make the labels scrollable, is there any way with ng2-charts??? help pls :)

    jdlopezq avatar Mar 23 '18 14:03 jdlopezq

    help me! :( I want to show percentage in the legend.

    Danielapariona avatar Jun 25 '18 07:06 Danielapariona

    Hi @Danielapariona my solution was: First, download and install Chart Piece Label In your component:

    component.ts

    public pieOptions:any = {
        pieceLabel: {
          render: function (args) {
            let total = args["dataset"].data.reduce((a, b) => a + b, 0);
            let percent = args.value*100/total;
            return percent.toFixed(1)+'%';
          }
      };
    

    and in yours component.html

    <canvas #pieChart baseChart width="550"
                [chartType]="pieChartType"
                [datasets]="datasets"
                [labels]="pieLabels"
                [colors]="colors"
                [options]="pieOptions">
              </canvas>
    

    rudighert avatar Jun 25 '18 18:06 rudighert

    I want the legend to look like that: selection_012

    Danielapariona avatar Jun 25 '18 19:06 Danielapariona

    any updates on this ?

    ghost avatar Oct 09 '18 05:10 ghost

    Can anybody show a running stackblitz example of custom legend?

    geniusunil avatar Jan 16 '19 09:01 geniusunil

    Hi @rickyricky74, I'm flowed the step you posted, but I'm getting this error:

    ERROR TypeError: Cannot read property 'generateLegend' of undefined

    Can you help me to solve this ?

    This is my code:

    import { BaseChartDirective } from 'ng2-charts'; import {Component, OnInit, ViewChild} from '@angular/core';

    @component({ selector: 'custom-chart', templateUrl: 'custom.chart.component.html' }) export class CustomChartComponent implements OnInit {

    @ViewChild(BaseChartDirective) chartComponent: BaseChartDirective;
    legendData: any;
    
    private getLegendCallback = (function(self) {
        function handle(chart) {
            // Do stuff here to return an object model.
            // Do not return a string of html or anything like that.
            // You can return the legendItems directly or
            // you can use the reference to self to create
            // an object that contains whatever values you
            // need to customize your legend.
            return chart.legend.legendItems;
        }
        return function(chart) {
            return handle(chart);
        };
    })(this);
    
    myChartOptions = {
        responsive: true,
        legendCallback: this.getLegendCallback
    };
    
    public lineChartLabels: Array<any> = ['SUN', 'MON', 'TUE', 'WED', 'THU', 'FR', 'SAT'];
    public lineChartType = 'line';
    // public lineChartOptions: any = {
    //    responsive: true
    // };
    
    public lineChartColors: Array<any> = [
        {
            backgroundColor: 'rgba(101,120,196,0.3)',
            borderColor: 'rgb(101,120,196)',
            pointBackgroundColor: 'rgb(101,120,196)',
            pointBorderColor: '#fff',
        },
        {
            backgroundColor: 'rgba(25,209,185,0.3)',
            borderColor: 'rgb(25,209,185)',
            pointBackgroundColor: 'rgb(25,209,185)',
            pointBorderColor: '#fff',
        }
    ];
    
    public lineChartColors2: Array<any> = [
        {
            backgroundColor: 'rgba(217,93,121,0.3)',
            borderColor: 'rgb(217,93,121)',
            pointBackgroundColor: 'rgb(217,93,121)',
            pointBorderColor: '#fff',
        },
        {
            backgroundColor: 'rgba(249,174,91,0.3)',
            borderColor: 'rgb(249,174,91)',
            pointBackgroundColor: 'rgb(249,174,91)',
            pointBorderColor: '#fff',
        }
    ];
    
    ngOnInit() {
        this.legendData = this.chartComponent.chart.generateLegend( );
    }
    

    }

    And this is how I used in the view:

    <custom-chart></custom-chart>

    @rickyricky74 solution works for me. There is only one change. To resolve "Cannot read property 'generateLegend' of undefined`", replace your ngOnInit function with this -

    `ngOnInit() {

    setInterval(() => {

    this.legendData = this.chartComponent.chart.generateLegend( );
    

    }, 10);

    } `

    geniusunil avatar Jan 17 '19 09:01 geniusunil

    Use only html template without manual change detection changes. HTML Template:

    <canvas baseChart #partsBaseChart="base-chart" [legend]="!1" [data]="partsChartData" [options]="pieChartOptions" [labels]="partsChartLabels" [chartType]="pieChartType"></canvas>
    <div *ngIf="partsBaseChart?.chart?.legend?.legendItems">
        <ul class="custom-legend-list">
            <li *ngFor="let item of partsBaseChart.chart.legend.legendItems; let i = index" class="custom-legend-item" (click)="legendOnClick(item.text)">
                <span class="slice-color" [ngStyle]="{'background-color': item.fillStyle}"></span>
                <span class="slice-title">{{ item.text }} </span>
            </li>
        </ul>
    </div>
    

    cantacell avatar Jun 14 '19 10:06 cantacell

    How to use the html code to display the data value and legend description

    Totot0 avatar Jul 09 '19 20:07 Totot0

    Any working solution for this ???

    suhailkc avatar Jul 15 '19 13:07 suhailkc

    How to use the html code to display the data value and legend description

    For the value I created a new array and accessing it with the same index of the legendItems:

    into html template

    <li *ngFor="let item of partsBaseChart.chart.legend.legendItems; let i = index" class="custom-legend-item" (click)="legendOnClick(item.text)">
                <span class="slice-color" [ngStyle]="{'background-color': item.fillStyle}"></span>
                <span class="slice-title">{{ item.text }} </span>
                <span>{{chart.graphValues[i]?.value}}</span>
            </li>
    

    cantacell avatar Jul 15 '19 14:07 cantacell

    For anyone who comes to this looking at the original solution, I found an issue with @bkartik2005 's implementation that was tripping me up:

    pieChartOptions: any = {
            legend: {
                legendCallback: this.getLegendCallback
            } 
        }
    

    Is incorrect. legendCallback isn't part of the legend object. legendCallback is it's own option. If you follow the original solution provided by @rickyricky74 and place the legendCallback outside the legend object this works.

    dcp3450 avatar Aug 29 '19 17:08 dcp3450

    Use only html template without manual change detection changes. HTML Template:

    <canvas baseChart #partsBaseChart="base-chart" [legend]="!1" [data]="partsChartData" [options]="pieChartOptions" [labels]="partsChartLabels" [chartType]="pieChartType"></canvas>
    <div *ngIf="partsBaseChart?.chart?.legend?.legendItems">
        <ul class="custom-legend-list">
            <li *ngFor="let item of partsBaseChart.chart.legend.legendItems; let i = index" class="custom-legend-item" (click)="legendOnClick(item.text)">
                <span class="slice-color" [ngStyle]="{'background-color': item.fillStyle}"></span>
                <span class="slice-title">{{ item.text }} </span>
            </li>
        </ul>
    </div>
    

    Hi @cantacell, I tried using this code to get rid of manual change detection, but it throws an error saying - "Property legend doesn't exist on type chart". Did you do anything else apart from this code in your HTML file?

    kukrejashikha02 avatar Jul 03 '20 00:07 kukrejashikha02

    So, I did some snooping around the baseChart directive and found a bit of an easier solution for updating the chart while persisting those hidden values [stackblitz link below]. I really hope this helps someone!

    For sake of clarity... I made the legend itself with simple buttons that use *ngFor index in html :

    <a *ngFor="let data of chartData; let i = index;" 
         (click)="onSelect(i)">
    
      <span [ngClass]="data.hidden ? 'hidden' : 'showing'">
        {{ data.label }}
      </span>
    
    </a>
    

    In the ts file, I added the BaseChartDirective

      @ViewChild(BaseChartDirective) baseChart: BaseChartDirective;
    

    Then, in the method that receives the index, I would change that item's hidden value to true and update the chart by calling update() on the baseChart directive (which I first assigned to a new ci variable). If all hidden values were set to true then I'd Object.assign them to be false again with another update() call on the baseChart:

      onSelect(indexItem): void {
        const ci = this.baseChart;
        this.chartData[indexItem].hidden = true;
        ci.update();
    
        if (this.chartData.every(each => each.hidden === true)) {
          this.chartData.map(item => Object.assign(item, {hidden: false}))
          ci.update();
        }
      }
    

    I put this on stackblitz to help: https://stackblitz.com/edit/ng2-chartjs-customlegend?file=src/app/line-chart/line-chart.component.ts

    riapacheco avatar Mar 29 '21 23:03 riapacheco

    @riapacheco Your example doesn't show how to display chart color palette in the legend, so not a complete integration.

    VictorZakharov avatar Sep 27 '22 21:09 VictorZakharov