import { ElementRef } from '@angular/core'; import Chart, { CategoryScale, ChartArea, LinearScale } from 'chart.js/auto'; import { ChartColorsStringEnum } from '../../enums'; import { IBaseDataset, IChartData } from '../../models'; export class ChartHelper { public static hexToRgba(colorHex: string, opacity: number = 1): string { colorHex = colorHex.replace(/^#/, ''); if (colorHex.length === 3) { colorHex .split('') .map((char) => char + char) .join(''); } const bigint = parseInt(colorHex, 16); const red = (bigint >> 16) & 255; const green = (bigint >> 8) & 255; const blue = bigint & 255; return `rgba(${red}, ${green}, ${blue}, ${opacity})`; } static rgbToRgba(rgb: string = 'rgb(0, 0, 0)', opacity: number): string { const rgbValues = rgb.match(/\d+/g); if (rgbValues && rgbValues.length >= 3) { const red = rgbValues[0]; const green = rgbValues[1]; const blue = rgbValues[2]; return `rgba(${red}, ${green}, ${blue}, ${opacity})`; } else return rgb; } static changeOpacityOfRgbOrRgba(color: string, opacity: number): string { const rgbaRegex = /^rgba?\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})(?:,\s*(\d?\.?\d+))?\)$/; const match = color.match(rgbaRegex); if (match) { const red = parseInt(match[1], 10); const green = parseInt(match[2], 10); const blue = parseInt(match[3], 10); return `rgba(${red}, ${green}, ${blue}, ${opacity})`; } return color; } static calculateMinValue(data: number[]): number { return Math.min(...data); } static calculateMaxValue(data: number[]): number { return Math.max(...data); } static createGradient( chartContext: CanvasRenderingContext2D, chartCanvas: ElementRef, color: string, color2: string, opacityColor1: number = 1, opacityColor2: number = 1, areColorsInRgbaFormat: boolean = false ): CanvasGradient { const gradient = chartContext.createLinearGradient( 0, 0, 0, chartCanvas.nativeElement.height ); let fillColorTop = ''; let fillColorBottom = ''; if (areColorsInRgbaFormat) { fillColorTop = this.changeOpacityOfRgbOrRgba(color, opacityColor1); fillColorBottom = this.changeOpacityOfRgbOrRgba( color2, opacityColor2 ); } else { fillColorTop = this.hexToRgba(color, opacityColor1); fillColorBottom = this.hexToRgba(color2, opacityColor2); } gradient.addColorStop(0, fillColorTop); gradient.addColorStop(0.8, fillColorBottom); return gradient; } static createGradientWithShiftValue( chartContext: CanvasRenderingContext2D, chartHeight: number, color: string, color2: string, shiftValue: number, hasFade?: boolean ): CanvasGradient { const gradient = chartContext.createLinearGradient( 0, 0, 0, chartHeight ); const fillColorTop = this.hexToRgba(color, 0.8); const fillColorBottom = this.hexToRgba(color2, 0.8); if (!hasFade) { gradient.addColorStop(0, fillColorTop); gradient.addColorStop(shiftValue, fillColorTop); gradient.addColorStop(shiftValue, fillColorBottom); gradient.addColorStop(1, fillColorBottom); return gradient; } gradient.addColorStop(0, fillColorTop); gradient.addColorStop(shiftValue, ChartColorsStringEnum.WHITE); gradient.addColorStop(shiftValue, fillColorBottom); gradient.addColorStop(1, ChartColorsStringEnum.WHITE); return gradient; } static calculateTickPositions( xScale: CategoryScale | LinearScale ): number[] { return xScale.ticks.map((_, index: number) => xScale.getPixelForTick(index) ); } static highlightSegment( ctx: CanvasRenderingContext2D, xScale: CategoryScale | LinearScale, chartArea: ChartArea, chartHeight: number, index: number ): void { const { segmentStart, segmentWidth } = ChartHelper.getSegmentPosition( xScale, index ); const borderRadius = 2; ctx.save(); ctx.fillStyle = ChartColorsStringEnum.HOVER_SEGMENT; // Draw segment with border-radius at the top corners ctx.beginPath(); ctx.moveTo(segmentStart + borderRadius, chartArea.top); ctx.lineTo(segmentStart + segmentWidth - borderRadius, chartArea.top); ctx.arcTo( segmentStart + segmentWidth, chartArea.top, segmentStart + segmentWidth, chartArea.top + borderRadius, borderRadius ); ctx.lineTo(segmentStart + segmentWidth, chartHeight); ctx.lineTo(segmentStart, chartHeight); ctx.lineTo(segmentStart, chartArea.top + borderRadius); ctx.arcTo( segmentStart, chartArea.top, segmentStart + borderRadius, chartArea.top, borderRadius ); ctx.closePath(); ctx.fill(); ctx.restore(); } // This is a dumb function whose only function is only to position legend elements one below under // Call it in for public static drawDoughnutLegend( chart: Chart, position: { indx: number; // TODO if needed, convert to object offsetTop: number; }, text: string, // Initial idea fontSize: number = 18, color: string = '#424242' ): void { const { ctx } = chart; if (!ctx) return; ctx.save(); const datasetMeta = chart.getDatasetMeta(0); const xCoord = datasetMeta?.data[0]?.x; const yCoord = (datasetMeta?.data[0]?.y / 3) * 2 + position.indx * 20 + position.offsetTop; if (xCoord === undefined || yCoord == undefined) return; ctx.font = `bold ${fontSize}px Montserrat`; ctx.fillStyle = color; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText(text, xCoord, yCoord); ctx.restore(); } static highlightPoint( ctx: CanvasRenderingContext2D, chart: Chart, index: number, chartData: IChartData ): void { chartData.datasets.forEach((dataset, datasetIndex) => { const meta = chart.getDatasetMeta(datasetIndex); if (!meta || !meta.data[index]) return; const point = meta.data[index]; const yScale = chart.scales[dataset.yAxisID || 'y-axis-0']; const xScale = chart.scales['x']; const value = dataset.data[index]; if (value === undefined || value === null) return; const xPixel = xScale.getPixelForValue(index); const yPixel = Math.abs(yScale.getPixelForValue(value as number)); if (!point) return; ctx.save(); ctx.beginPath(); ctx.arc(xPixel, yPixel, 4, 0, 2 * Math.PI); ctx.fillStyle = ChartColorsStringEnum.WHITE; ctx.fill(); ctx.lineWidth = 2; ctx.strokeStyle = dataset.borderColor || ChartColorsStringEnum.TRANSPARENT; ctx.stroke(); ctx.restore(); }); } private static getSegmentPosition( xScale: CategoryScale | LinearScale, index: number ): { segmentStart: number; segmentWidth: number } { const tickPositions = ChartHelper.calculateTickPositions(xScale); const segmentWidth = tickPositions[1] - tickPositions[0]; const segmentStart = tickPositions[index] - segmentWidth / 2; return { segmentStart, segmentWidth }; } public static convertRgbToRgba( colorValue: string, alpha: number = 1 ): string { if (alpha < 0 || alpha > 1) return colorValue; const rgbaMatch = colorValue.match( /rgba\((\d+),\s*(\d+),\s*(\d+),\s*(\d+(\.\d+)?)\)/ ); if (rgbaMatch) { const [, r, g, b] = rgbaMatch; // Return updated RGBA with the new alpha return `rgba(${r}, ${g}, ${b}, ${alpha})`; } // Extract the RGB values using a regular expression const rgbMatch = colorValue.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/); if (!rgbMatch) return colorValue; // Destructure the matched values const [, r, g, b] = rgbMatch; // Return the RGBA string return `rgba(${r}, ${g}, ${b}, ${alpha})`; } public static calculateDatasetMaxValue( datasets: { data: number[] }[] ): number { return Math.max(...datasets.flatMap((dataset) => dataset.data)); } }