export interface TrendDataPoint { day: string; value: number; } export interface ChartConfig { chartData: any; chartOptions: any; } interface DatasetOverrides { label: string; backgroundColor: string | string[]; datalabels?: object; } interface YAxisTicks { min: number; max: number; stepSize: number; } export class ChartBuilderUtils { private static readonly GREEN_DARK = '#2E7D32'; private static readonly GRID_LINE = '#F0F0F0'; private static readonly AXIS_LABEL = '#3a4450'; private static readonly DATA_LABEL = '#222222'; private static readonly SHARED_X_AXIS = [ { gridLines: { display: false }, ticks: { fontColor: ChartBuilderUtils.AXIS_LABEL } } ]; private static buildYAxis(ticks: YAxisTicks) { return [{ gridLines: { color: ChartBuilderUtils.GRID_LINE }, ticks: { fontColor: ChartBuilderUtils.AXIS_LABEL, beginAtZero: true, ...ticks } }]; } /** * Computes a nice Y-axis scale from the actual data values. * `preferredMax` acts as both the zero-data fallback and a minimum floor — * the computed max will never be less than `preferredMax`. */ private static niceYTicks(values: number[], preferredMax: number): YAxisTicks { const dataMax = values.length > 0 ? Math.max(...values) : 0; // Use whichever is larger: data-driven headroom or the preferred floor const raw = Math.max(dataMax * 1.2, preferredMax); const magnitude = Math.pow(10, Math.floor(Math.log10(raw / 5))); const stepSize = Math.ceil((raw / 5) / magnitude) * magnitude; const max = stepSize * 5; return { min: 0, max, stepSize }; } private static buildBaseChartConfig( data: TrendDataPoint[], dataset: DatasetOverrides, yAxisTicks: YAxisTicks, tooltipUnit: string, plugins?: object ): ChartConfig { const values = data.map(d => d.value); return { chartData: { labels: data.map(d => d.day), datasets: [{ label: dataset.label, data: values, fill: false, borderColor: ChartBuilderUtils.GREEN_DARK, backgroundColor: dataset.backgroundColor, pointBackgroundColor: ChartBuilderUtils.GREEN_DARK, pointRadius: 4, tension: 0.3, ...(dataset.datalabels ? { datalabels: dataset.datalabels } : {}) }] }, chartOptions: { responsive: true, maintainAspectRatio: false, legend: { display: false }, ...(plugins ? { plugins } : {}), scales: { xAxes: ChartBuilderUtils.SHARED_X_AXIS, yAxes: ChartBuilderUtils.buildYAxis(yAxisTicks) }, tooltips: { callbacks: { label: (item: any) => ` ${item.yLabel} ${tooltipUnit}` } } } }; } private static interpolateGreenByValue(values: number[]): string[] { const light = { r: 165, g: 214, b: 167 }; const dark = { r: 46, g: 125, b: 50 }; const min = Math.min(...values); const max = Math.max(...values); return values.map(val => { const t = (max === min) ? 0 : (val - min) / (max - min); return `rgb(${Math.round(light.r + (dark.r - light.r) * t)},${Math.round(light.g + (dark.g - light.g) * t)},${Math.round(light.b + (dark.b - light.b) * t)})`; }); } static hoursChart(data: TrendDataPoint[]): ChartConfig { const values = data.map(d => d.value); return ChartBuilderUtils.buildBaseChartConfig( data, { label: $localize`:Hours flown chart dataset label@@hoursChartLabel:Hours Flown`, backgroundColor: ChartBuilderUtils.GREEN_DARK }, ChartBuilderUtils.niceYTicks(values, 5), $localize`:Hours unit abbreviation@@unitHrs:hrs` ); } static hectaresChart(data: TrendDataPoint[], areaUnit = 'ha'): ChartConfig { const values = data.map(d => d.value); const preferredMax = areaUnit === 'ac' ? 2000 : 500; const datalabels = { anchor: 'end', align: 'end', color: ChartBuilderUtils.DATA_LABEL, font: { weight: 'bold' }, formatter: (v: number) => v }; const datasetLabel = areaUnit === 'ac' ? $localize`:Acres sprayed chart dataset label@@acresChartLabel:Acres Sprayed` : $localize`:Hectares sprayed chart dataset label@@hectaresChartLabel:Hectares Sprayed`; return ChartBuilderUtils.buildBaseChartConfig( data, { label: datasetLabel, backgroundColor: ChartBuilderUtils.interpolateGreenByValue(values), datalabels }, ChartBuilderUtils.niceYTicks(values, preferredMax), areaUnit, { datalabels } ); } }