valor-software / ng2-charts

Beautiful charts for Angular based on Chart.js
http://valor-software.github.io/ng2-charts/
MIT License
2.36k stars 574 forks source link

How to Custom legend template in ng2-chart #464

Closed chauanthuan closed 8 months ago

chauanthuan commented 8 years ago

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.

rickyricky74 commented 7 years ago

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

bkartik2005 commented 7 years ago

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);

rickyricky74 commented 7 years ago

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

bkartik2005 commented 7 years ago

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

rickyricky74 commented 7 years ago

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.

bkartik2005 commented 7 years ago

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 } ]

rickyricky74 commented 7 years ago

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 commented 7 years ago

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.

yunier0525 commented 7 years ago

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>

JacoboSegovia commented 6 years ago

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.

areknow commented 6 years ago

@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!

GeraudWilling commented 6 years ago

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?

sshreya0808 commented 6 years ago

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

rudighert commented 6 years ago

@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

sgaawc commented 6 years ago

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>
jdlopezq commented 6 years ago

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

Danielapariona commented 6 years ago

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

rudighert commented 6 years ago

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>
Danielapariona commented 6 years ago

I want the legend to look like that: selection_012

ghost commented 6 years ago

any updates on this ?

geniusunil commented 5 years ago

Can anybody show a running stackblitz example of custom legend?

geniusunil commented 5 years ago

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);

} `

cantacell commented 5 years ago

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>
Totot0 commented 5 years ago

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

suhailkc commented 5 years ago

Any working solution for this ???

cantacell commented 5 years ago

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>
dcp3450 commented 5 years ago

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.

kukrejashikha02 commented 4 years ago

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?

riapacheco commented 3 years ago

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

VictorZakharov commented 2 years ago

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