agmission/Development/client/src/app/dashboard/utils/chart-builders.ts

128 lines
4.4 KiB
TypeScript

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