Filter by a Product in Angular
Using ApexCharts with Angular
This Filter by a Product example wires ApexCharts into Angular through the ng-apexcharts component.
Install it with npm install ng-apexcharts apexcharts, then mount the chart with <apx-chart [series]="series" [chart]="chart"></apx-chart>.
import { Component, AfterViewInit, OnDestroy, ViewChild } from '@angular/core';
import {
ChartComponent,
ApexAxisChartSeries,
ApexNonAxisChartSeries,
ApexChart,
ApexXAxis,
ApexYAxis,
ApexTitleSubtitle,
ApexDataLabels,
ApexStroke,
ApexFill,
ApexLegend,
ApexTooltip,
ApexMarkers,
ApexPlotOptions,
ApexResponsive,
ApexGrid,
ApexAnnotations,
ApexStates,
ApexTheme,
NgApexchartsModule,
} from 'ng-apexcharts';
export type ChartOptions = {
series?: ApexAxisChartSeries | ApexNonAxisChartSeries;
chart?: ApexChart;
xaxis?: ApexXAxis;
yaxis?: ApexYAxis | ApexYAxis[];
title?: ApexTitleSubtitle;
subtitle?: ApexTitleSubtitle;
dataLabels?: ApexDataLabels;
stroke?: ApexStroke;
fill?: ApexFill;
legend?: ApexLegend;
tooltip?: ApexTooltip;
markers?: ApexMarkers;
plotOptions?: ApexPlotOptions;
responsive?: ApexResponsive[];
grid?: ApexGrid;
annotations?: ApexAnnotations;
states?: ApexStates;
theme?: ApexTheme;
colors?: string[];
labels?: any;
};
@Component({
selector: 'app-chart',
standalone: true,
imports: [NgApexchartsModule],
templateUrl: './chart.component.html',
})
export class AppChart implements AfterViewInit, OnDestroy {
@ViewChild('chart') chart!: ChartComponent;
private PRODUCTS: any = ['Laptops', 'Phones', 'Tablets', 'Audio'];
private REGIONS: any = ['North', 'South'];
private QUARTERS: any = ['Q1', 'Q2', 'Q3', 'Q4'];
private REVENUE: any = {
Laptops: { North: [42, 38, 45, 61], South: [30, 28, 33, 44] },
Phones: { North: [55, 49, 52, 70], South: [48, 51, 47, 66] },
Tablets: { North: [18, 15, 17, 26], South: [22, 19, 21, 25] },
Audio: { North: [12, 14, 13, 21], South: [9, 11, 10, 16] },
};
private sum = (a: any): any => {
return a.reduce(function (s, v) {
return s + v
}, 0)
};
private productTotal = (p: any): any => {
return this.sum(
this.REGIONS.map(function (r) {
return this.sum(this.REVENUE[p][r])
}),
)
};
private regionQuarters = (r: any, onlyProduct: any): any => {
return this.QUARTERS.map(function (q, k) {
return this.sum(
this.PRODUCTS.filter(function (p) {
return !onlyProduct || p === onlyProduct
}).map(function (p) {
return this.REVENUE[p][r][k]
}),
)
})
};
private picked: any = null;
private productSeries = (): any => {
var totals = this.PRODUCTS.map(this.productTotal)
var s = { name: 'Revenue', data: totals }
// On the source chart the pick is the whole of its own column, so it stays
// solid while the others fade.
if (this.picked) {
s.highlightData = this.PRODUCTS.map(function (p, j) {
return p === this.picked ? totals[j] : null
})
}
return [s]
};
private quarterSeries = (): any => {
return this.REGIONS.map(function (r) {
var s = { name: r, data: this.regionQuarters(r) }
if (this.picked) s.highlightData = this.regionQuarters(r, this.picked)
return s
})
};
private pickProduct = (index: any): any => {
var product = this.PRODUCTS[index]
this.picked = this.picked === product ? null : product
var readout = document.getElementById('readout')
if (readout) {
readout.innerHTML = this.picked
? 'Filtered by <b>' + this.picked + '</b>. Click it again to clear.'
: 'Click a product to filter the quarters by it.'
}
// Let the click handler unwind before both charts redraw.
Promise.resolve().then(function () {
ApexCharts.getChartByID('products').updateSeries(this.productSeries())
ApexCharts.getChartByID('quarters').updateSeries(this.quarterSeries())
})
};
public chartOptions: Partial<ChartOptions> = {
series: this.productSeries(),
chart: {
id: 'products',
type: 'bar',
height: 320,
toolbar: { show: false },
events: {
dataPointSelection: (e, ctx, opts) => {
this.pickProduct(opts.dataPointIndex)
},
},
},
colors: ['#0f766e'],
plotOptions: {
bar: { horizontal: true, barHeight: '64%', borderRadius: 3 },
},
states: { active: { filter: { type: 'none' } } },
dataLabels: { enabled: false },
xaxis: {
categories: this.PRODUCTS,
tickAmount: 3,
labels: {
formatter: (v) => {
return '$' + v + 'k'
},
},
},
// On the source chart the pick is its own column, so a tooltip row for the
// part would only repeat the total.
highlightFilter: {
tooltip: { show: false },
dataLabels: { value: 'whole' },
},
legend: { show: false },
title: { text: 'Revenue by product', style: { fontSize: '13px' } },
tooltip: {
y: {
formatter: (v) => {
return '$' + v + 'k'
},
},
},
};
public chartOptions2: Partial<ChartOptions> = {
series: this.quarterSeries(),
chart: {
id: 'quarters',
type: 'bar',
height: 320,
toolbar: { show: false },
},
colors: ['#2563eb', '#f59e0b'],
plotOptions: {
bar: { columnWidth: '70%', borderRadius: 3 },
},
dataLabels: { enabled: false },
stroke: { show: true, width: 1, colors: ['transparent'] },
xaxis: { categories: this.QUARTERS },
yaxis: {
labels: {
formatter: (v) => {
return '$' + v + 'k'
},
},
},
legend: { position: 'bottom' },
title: { text: 'Revenue by quarter', style: { fontSize: '13px' } },
tooltip: {
shared: true,
intersect: false,
y: {
formatter: (v) => {
return '$' + v + 'k'
},
},
},
};
ngAfterViewInit() {
(window as any).ApexCharts.setLicense('APEX-eyJleHBpcnlEYXRlIjoiMjEyNi0wNy0wNCIsImlzc3VlRGF0ZSI6IjIwMjYtMDctMjgiLCJwbGFuIjoicHJlbWl1bSIsImRvbWFpbnMiOlsiYXBleGNoYXJ0cy5jb20iLCIxMjcuMC4wLjEiLCJsb2NhbGhvc3QiXSwic2lnIjoieVBmb1VCc0Z3TU9ZdUEyaEZkR0I2Y1FtZ0JITUtXcVdJSjB2NVRESXRZbFR3eDJMUmh6R2x0RUc3VXJ4X0s3b25ZMWRZb2Z2VGItN01ydFYyNDVyOWcifQ==');
function pickProduct(index) {
var product = this.PRODUCTS[index]
this.picked = this.picked === product ? null : product
var readout = document.getElementById('readout')
if (readout) {
readout.innerHTML = this.picked
? 'Filtered by <b>' + this.picked + '</b>. Click it again to clear.'
: 'Click a product to filter the quarters by it.'
}
// Let the click handler unwind before both charts redraw.
Promise.resolve().then(function () {
ApexCharts.getChartByID('products').updateSeries(this.productSeries())
ApexCharts.getChartByID('quarters').updateSeries(this.quarterSeries())
})
}
}
ngOnDestroy() {
// no cleanup needed
}
}