import type { ChartOptions as ChartJsChartOptions, CartesianScaleOptions, RadialLinearScaleOptions, Scale, Tick, GridLineOptions, } from 'chart.js'; import { DateTime } from 'luxon'; import type { DeepPartial } from 'chart.js/dist/types/utils'; import type { DataFrame } from 'types'; import { assureMaxLength, defaultCompactNumberFormat } from 'components/utils/formatter'; import type { CartesianAxisConfiguration, ChartOptions, GridLinesConfiguration, TicksConfiguration, TimeCartesianAxisConfiguration, TimeDisplayFormats, } from './types'; import { defaultValue, singleChartJsColor } from './utils'; import { getSafeTimeUnit } from './timeScaleValidation'; const TICK_MAX_LENGTH = 40; function computeFormatTick( displayTick: TicksConfiguration['display'], type: CartesianAxisConfiguration['type'], formatNumber: (value: number) => string ) { if (type === 'time') { return null; } function formatTick(this: Scale, tickValue: number, _index: number, ticks: Tick[]) { const minAbsTickValue = Math.min(...ticks.map((tick) => Math.abs(tick.value))); if (displayTick === 'single' && tickValue !== minAbsTickValue) { return ''; } if (type === 'category') { return assureMaxLength(this.getLabelForValue(tickValue), TICK_MAX_LENGTH); } return formatNumber(tickValue); } return formatTick; } const computeGridLineColor: ( display: GridLinesConfiguration['display'] ) => GridLineOptions['color'] = (display) => (context) => { if (!context?.scale?.ticks) return 'rgba(0, 0, 0, 0)'; const ticksAbsoluteValues = context.scale.ticks.map((tick) => Math.abs(tick.value)); const minAbsoluteTicksIndex = ticksAbsoluteValues.indexOf(Math.min(...ticksAbsoluteValues)); if (display) { if (context.index === minAbsoluteTicksIndex) return 'rgba(0, 0, 0, 0.4)'; if (display !== 'single') return 'rgba(0, 0, 0, 0.1)'; } return 'rgba(0, 0, 0, 0)'; }; const DATE_TOOLTIP_FORMATS = { millisecond: 'h:mm:ss.SSS a', second: DateTime.DATETIME_MED_WITH_SECONDS, minute: DateTime.DATETIME_MED, hour: DateTime.DATETIME_MED, day: { day: 'numeric', month: 'long' }, week: "'W'WW yyyy", month: { month: 'long', year: 'numeric' }, quarter: "'Q'q - yyyy", year: { year: 'numeric' }, }; function getDateTooltipFormat( unit?: TimeCartesianAxisConfiguration['timeUnit'], customFormats?: TimeDisplayFormats ) { return unit ? customFormats?.[unit] || DATE_TOOLTIP_FORMATS[unit] : undefined; } export default function buildScales( options: ChartOptions, dataFrame?: DataFrame ): ChartJsChartOptions['scales'] { const scales: ChartJsChartOptions['scales'] = {}; // X Axis if (options.axis?.x) { // For time scales, validate that the requested timeUnit won't crash ChartJS // If incompatible, fallback to auto-detection (undefined) const xAxis = options.axis.x; const requestedTimeUnit = xAxis.type === 'time' ? xAxis.timeUnit : undefined; const safeTimeUnit = xAxis.type === 'time' && dataFrame?.length ? getSafeTimeUnit(dataFrame, requestedTimeUnit, options.labelColumn) : requestedTimeUnit; scales.x = { border: { display: false }, ...(options.axis.x.type === 'linear' && { beginAtZero: defaultValue(options?.axis?.x?.beginAtZero, true), }), stacked: options.axis?.assemblage?.stacked, max: options?.axis?.x?.type === 'linear' && options.axis?.assemblage?.percentaged ? 100 : undefined, type: options?.axis?.x?.type, ...(options?.axis?.x?.type === 'time' ? { time: { unit: safeTimeUnit, tooltipFormat: getDateTooltipFormat( safeTimeUnit, options?.tooltip?.timeDisplayFormats ), isoWeekday: true, displayFormats: { ...(options?.axis?.x?.timeDisplayFormats || {}), }, }, } : {}), display: options?.axis?.x?.display, offset: defaultValue(options?.axis?.x?.offset, false), title: { display: options?.axis?.x?.title?.display, color: defaultValue(options?.axis?.x?.title?.color, 'rgb(0, 0, 0)'), align: options?.axis?.x?.title?.align, text: options?.axis?.x?.title?.text, font: { weight: defaultValue(options?.axis?.x?.title?.font?.weight, '400'), size: defaultValue(options?.axis?.x?.title?.font?.size, 12), }, }, grid: { display: !!defaultValue(options.axis?.x?.gridLines?.display, true), offset: false, color: computeGridLineColor( defaultValue(options.axis?.x?.gridLines?.display, true) ), }, ticks: { display: !!defaultValue(options?.axis?.x?.ticks?.display, true), color: defaultValue(options?.axis?.x?.ticks?.color, 'rgb(86, 86, 86)'), callback: computeFormatTick( defaultValue(options?.axis?.x?.ticks?.display, true), options?.axis?.x?.type, defaultValue(options?.axis?.x?.ticks?.format, defaultCompactNumberFormat) ), }, } as DeepPartial; } // Y Axis if (options.axis?.y) { scales.y = { border: { display: false }, ...(options.axis.y.type === 'linear' && { beginAtZero: defaultValue(options?.axis?.y?.beginAtZero, true), }), stacked: options.axis?.assemblage?.stacked, max: options?.axis?.y?.type === 'linear' && options.axis?.assemblage?.percentaged ? 100 : undefined, type: options?.axis?.y?.type, display: options?.axis?.y?.display, title: { display: options?.axis?.y?.title?.display, align: options?.axis?.y?.title?.align, text: options?.axis?.y?.title?.text, color: defaultValue(options?.axis?.y?.title?.color, 'rgb(0, 0, 0)'), font: { weight: defaultValue(options?.axis?.y?.title?.font?.weight, '400'), size: defaultValue(options?.axis?.y?.title?.font?.size, 12), }, }, grid: { display: !!defaultValue(options.axis?.y?.gridLines?.display, true), color: computeGridLineColor( defaultValue(options.axis?.y?.gridLines?.display, true) ), }, ticks: { display: !!defaultValue(options?.axis?.y?.ticks?.display, true), color: defaultValue( singleChartJsColor(options?.axis?.y?.ticks?.color), 'rgb(86, 86, 86)' ), callback: computeFormatTick( defaultValue(options?.axis?.y?.ticks?.display, true), options?.axis?.y?.type, defaultValue(options?.axis?.y?.ticks?.format, defaultCompactNumberFormat) ), }, } as DeepPartial; } else { scales.y = { display: false }; } // R Axis if (options.axis?.r) { scales.r = { border: { display: false }, beginAtZero: defaultValue(options?.axis?.r?.beginAtZero, true), ticks: { display: defaultValue(options?.axis?.r?.ticks?.display, true), color: defaultValue(options?.axis?.r?.ticks?.color, 'rgb(86, 86, 86)'), callback: computeFormatTick( defaultValue(options?.axis?.r?.ticks?.display, true), undefined, defaultValue(options?.axis?.r?.ticks?.format, defaultCompactNumberFormat) ), }, grid: { display: defaultValue(options.axis?.r?.gridLines?.display, true), offset: false, color: computeGridLineColor( defaultValue(options.axis?.r?.gridLines?.display, true) ), }, } as DeepPartial; } return scales; }