maneric-charts
Version:
This project was generated using [Angular CLI](https://github.com/angular/angular-cli) version 20.3.0.
307 lines (298 loc) • 12.4 kB
JavaScript
import * as i0 from '@angular/core';
import { Injectable, EventEmitter, Output, Input, ViewChild, Component } from '@angular/core';
import { Chart, registerables } from 'chart.js';
import ChartDataLabels from 'chartjs-plugin-datalabels';
import { merge } from 'lodash';
import { CommonModule } from '@angular/common';
const defaultCenterTextPluginOptions = {
fontSize: 20,
fontColor: '#333',
};
const centerTextPlugin = {
id: 'centerText',
afterDraw(chart, args, options) {
const { ctx, chartArea: { left, right, top, bottom }, } = chart;
const opt = { ...defaultCenterTextPluginOptions, ...options };
const { fontSize, fontColor, subTitle, text, prefix } = opt;
const xCenter = (left + right) / 2;
const yCenter = (top + bottom) / 2;
const tFontSize = fontSize ?? 24;
ctx.save();
const dataset = chart.data.datasets[0].data;
const total = text ?? `${dataset.reduce((a, b) => a + b, 0)}`;
const totalWithPrefix = prefix ? `${prefix}${total}` : total;
ctx.font = `bold ${tFontSize}px Arial`;
ctx.fillStyle = fontColor;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(totalWithPrefix, xCenter, yCenter);
if (subTitle) {
ctx.font = '12px Arial';
ctx.fillStyle = fontColor;
ctx.fillText(subTitle, xCenter, (top + bottom) / 2 + tFontSize / 2);
}
ctx.restore();
},
};
class ChartColorService {
// Fallback palettes
category10 = [
'#1f77b4',
'#ff7f0e',
'#2ca02c',
'#d62728',
'#9467bd',
'#8c564b',
'#e377c2',
'#7f7f7f',
'#bcbd22',
'#17becf',
];
pastel = [
'#aec7e8',
'#ffbb78',
'#98df8a',
'#ff9896',
'#c5b0d5',
'#c49c94',
'#f7b6d2',
'#c7c7c7',
'#dbdb8d',
'#9edae5',
];
getForegroundColor() {
const container = document.querySelector('.app-container');
const styles = getComputedStyle(container);
return styles.getPropertyValue('--chart-foreground').trim() || '#fff';
}
/**
* Returns an array of colors for charts based on scheme and theme
*/
getColors(scheme, count, customColors) {
switch (scheme) {
case 'material':
return this.expandMaterialColors(count);
case 'category10':
return this.repeatPalette(this.category10, count);
case 'pastel':
return this.repeatPalette(this.pastel, count);
case 'custom':
return this.repeatPalette(customColors ?? [], count);
default:
return this.repeatPalette(this.category10, count);
}
}
/**
* Expand Angular Material theme colors into multiple shades
*/
expandMaterialColors(count) {
const container = document.querySelector('.app-container');
const styles = getComputedStyle(container);
const baseColors = [
styles.getPropertyValue('--chart-primary').trim() || '#1976d2',
styles.getPropertyValue('--chart-accent').trim() || '#9c27b0',
styles.getPropertyValue('--chart-warn').trim() || '#009688',
];
// Generate shades for each base color
const expanded = baseColors.flatMap((color) => this.generateShades(color, 3));
return this.repeatPalette(expanded, count);
}
/**
* Generate lighter/darker shades of a base color
*/
generateShades(hex, levels) {
const shades = [];
for (let i = -levels; i <= levels; i++) {
shades.push(this.adjustLightness(hex, i * 15));
}
return shades;
}
/**
* Adjust lightness of hex color
*/
adjustLightness(hex, percent) {
const num = parseInt(hex.replace('#', ''), 16);
let r = (num >> 16) + percent;
let g = ((num >> 8) & 0x00ff) + percent;
let b = (num & 0x0000ff) + percent;
r = Math.min(255, Math.max(0, r));
g = Math.min(255, Math.max(0, g));
b = Math.min(255, Math.max(0, b));
return `#${(b | (g << 8) | (r << 16)).toString(16).padStart(6, '0')}`;
}
/**
* Repeat palette until count is satisfied
*/
repeatPalette(palette, count) {
const result = [];
for (let i = 0; i < count; i++) {
result.push(palette[i % palette.length]);
}
return result;
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: ChartColorService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: ChartColorService, providedIn: 'root' });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: ChartColorService, decorators: [{
type: Injectable,
args: [{ providedIn: 'root' }]
}] });
Chart.register(...registerables, ChartDataLabels, centerTextPlugin);
class BaseChartComponent {
colorService;
canvas;
labels = [];
datasets = [];
title;
options;
colorScheme = 'material';
colors;
pointClick = new EventEmitter();
chartInit = new EventEmitter();
chart;
target = document.body.getElementsByTagName('div')[0];
constructor(colorService) {
this.colorService = colorService;
// Watch for theme changes (when Angular Material theme switches dynamically)
const observer = new MutationObserver(() => {
console.log('trigger update chart');
this.updateChart();
});
observer.observe(this.target, { attributes: true, attributeFilter: ['class'] });
}
ngAfterViewInit() {
this.renderChart();
}
ngOnChanges(changes) {
if (this.chart && (changes['datasets'] || changes['labels'] || changes['colors'])) {
this.updateChart();
}
}
renderChart() {
const ctx = this.canvas.nativeElement.getContext('2d');
if (!ctx)
return;
const datasetColors = this.colorService.getColors(this.colorScheme, this.datasets.length, this.colors);
const styledDatasets = this.datasets.map((d, i) => {
const backgroundColor = this.colorService.getColors(this.colorScheme, d.data.length, this.colors);
return {
...d,
backgroundColor,
borderColor: d.borderColor,
};
});
const config = this.buildConfig(styledDatasets);
if (!this.chart) {
this.chart = new Chart(ctx, config);
}
else {
this.chart = merge(this.chart, {
data: merge(this.chart.data, {
datasets: styledDatasets,
}),
options: config.options,
});
this.chart?.update();
}
this.chartInit.emit(this.chart);
}
buildConfig(styledDatasets) {
const foregroundColor = this.colorService.getForegroundColor();
console.log('foregroundColor', foregroundColor);
const options = merge({
responsive: true,
plugins: {
legend: {
labels: {
color: foregroundColor,
},
},
centerText: {
fontColor: foregroundColor,
},
},
onClick: (_e, elements) => {
if (elements.length > 0) {
const first = elements[0];
const datasetIndex = first.datasetIndex;
const index = first.index;
const label = this.labels[index];
const value = this.datasets[datasetIndex].data[index];
this.pointClick.emit({ label, value });
}
},
}, this.options);
return {
type: this.getChartType(),
data: {
labels: this.labels,
datasets: styledDatasets,
},
options,
};
}
updateChart() {
this.renderChart();
}
ngOnDestroy() {
this.chart?.destroy();
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: BaseChartComponent, deps: [{ token: ChartColorService }], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.0", type: BaseChartComponent, isStandalone: true, selector: "ng-component", inputs: { labels: "labels", datasets: "datasets", title: "title", options: "options", colorScheme: "colorScheme", colors: "colors" }, outputs: { pointClick: "pointClick", chartInit: "chartInit" }, viewQueries: [{ propertyName: "canvas", first: true, predicate: ["canvas"], descendants: true, static: true }], usesOnChanges: true, ngImport: i0, template: '', isInline: true });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: BaseChartComponent, decorators: [{
type: Component,
args: [{
template: '', // Extended by child components
}]
}], ctorParameters: () => [{ type: ChartColorService }], propDecorators: { canvas: [{
type: ViewChild,
args: ['canvas', { static: true }]
}], labels: [{
type: Input
}], datasets: [{
type: Input
}], title: [{
type: Input
}], options: [{
type: Input
}], colorScheme: [{
type: Input
}], colors: [{
type: Input
}], pointClick: [{
type: Output
}], chartInit: [{
type: Output
}] } });
class BarChartComponent extends BaseChartComponent {
getChartType() {
return 'bar';
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: BarChartComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.0", type: BarChartComponent, isStandalone: true, selector: "bar-chart", usesInheritance: true, ngImport: i0, template: "<canvas #canvas></canvas>", styles: [":host .chart-container{display:block;position:relative;width:100%;height:400px}\n"] });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: BarChartComponent, decorators: [{
type: Component,
args: [{ selector: 'bar-chart', template: "<canvas #canvas></canvas>", styles: [":host .chart-container{display:block;position:relative;width:100%;height:400px}\n"] }]
}] });
class DonutChartComponent extends BaseChartComponent {
getChartType() {
return 'doughnut';
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: DonutChartComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.0", type: DonutChartComponent, isStandalone: true, selector: "donut-chart", usesInheritance: true, ngImport: i0, template: "<canvas #canvas></canvas>", styles: [":host .chart-container{display:block;position:relative;width:100%}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }] });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: DonutChartComponent, decorators: [{
type: Component,
args: [{ selector: 'donut-chart', standalone: true, imports: [CommonModule], template: "<canvas #canvas></canvas>", styles: [":host .chart-container{display:block;position:relative;width:100%}\n"] }]
}] });
/*
* Public API Surface of maneric-charts
*/
// export * from './lib/maneric-charts';
/**
* Generated bundle index. Do not edit.
*/
export { BarChartComponent, BaseChartComponent, DonutChartComponent, centerTextPlugin };
//# sourceMappingURL=maneric-charts.mjs.map