@opendatasoft/visualizations
Version:
Opendatasoft's components to easily build dashboards and visualizations.
213 lines (201 loc) • 8.27 kB
text/typescript
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 { assureMaxLength, defaultCompactNumberFormat } from 'components/utils/formatter';
import type {
CartesianAxisConfiguration,
ChartOptions,
GridLinesConfiguration,
TicksConfiguration,
TimeCartesianAxisConfiguration,
TimeDisplayFormats,
} from './types';
import { defaultValue, singleChartJsColor } from './utils';
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): ChartJsChartOptions['scales'] {
const scales: ChartJsChartOptions['scales'] = {};
// X Axis
if (options.axis?.x) {
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: options?.axis?.x?.timeUnit,
tooltipFormat: getDateTooltipFormat(
options?.axis?.x?.timeUnit,
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<CartesianScaleOptions>;
}
// 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<CartesianScaleOptions>;
} 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<RadialLinearScaleOptions>;
}
return scales;
}