@opendatasoft/visualizations
Version:
Opendatasoft's components to easily build dashboards and visualizations.
138 lines (131 loc) • 5.17 kB
text/typescript
import type { LegendOptions, ChartConfiguration, Chart } from 'chart.js';
import type { _DeepPartialObject } from 'chart.js/dist/types/utils';
import { assureMaxLength } from 'components/utils/formatter';
import { CATEGORY_ITEM_VARIANT } from 'components/Legend/types';
import type { ChartOptions } from './types';
import { defaultValue, DEFAULT_GREY_COLOR } from './utils';
const LEGEND_MAX_LENGTH = 50;
const handleHoverPieChart: LegendOptions<'pie'>['onHover'] = (_, item, legend) => {
const { tooltip, chartArea } = legend.chart;
if (tooltip) {
// FIXME: `TooltipModel` doesn't have a `setActiveElements` method.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(tooltip as any).setActiveElements(
[
{
datasetIndex: 0,
index: (item as any).index, // eslint-disable-line @typescript-eslint/no-explicit-any
},
],
{
x: (chartArea.left + chartArea.right) / 2,
y: (chartArea.top + chartArea.bottom) / 2,
}
);
}
legend.chart.update();
};
const handleLeavePieChart: LegendOptions<'pie'>['onLeave'] = (_evt, _item, legend) => {
legend.chart.update();
};
export function buildLegend(options: ChartOptions) {
const legend: _DeepPartialObject<LegendOptions<'pie'>> = {
display: defaultValue(options?.legend?.display, false),
position: defaultValue(options?.legend?.position, 'bottom'),
align: defaultValue(options?.legend?.align, 'center'),
...(options.series[0]?.type === 'pie' && { onHover: handleHoverPieChart }),
...(options.series[0]?.type === 'pie' && { onLeave: handleLeavePieChart }),
labels: {
boxWidth: 20,
boxHeight: defaultValue(options?.legend?.boxStyle, 'rect') === 'rect' ? 16 : 0,
filter: (item) => {
/* eslint-disable no-param-reassign */
const text = options?.legend?.labels?.text;
if (text) {
const index =
typeof (item as any).index === 'number' // eslint-disable-line @typescript-eslint/no-explicit-any
? (item as any).index // eslint-disable-line @typescript-eslint/no-explicit-any
: item.datasetIndex;
item.text = text(index);
}
item.text = assureMaxLength(item.text, LEGEND_MAX_LENGTH);
if (options?.legend?.boxStyle === 'dash') {
item.lineWidth = 1;
item.lineDash = [4, 2];
} else if (options?.legend?.boxStyle === 'rect') {
item.borderRadius = 3;
}
return true;
/* eslint-enable no-param-reassign */
},
},
};
return legend;
}
function buildLegendLabels(
index: number,
options: ChartOptions,
chartConfig: ChartConfiguration
): string {
const text = options?.legend?.labels?.text;
if (text) {
return assureMaxLength(text(index), LEGEND_MAX_LENGTH);
}
return `${chartConfig.data.labels?.[index]}`;
}
export function buildPieAndDoughnutCustomLegend({
chart,
options,
chartConfig,
}: {
chart: Chart;
options: ChartOptions;
chartConfig: ChartConfiguration;
}) {
const { series } = options;
const backgroundColors = series[0].backgroundColor?.length
? series[0].backgroundColor
: [DEFAULT_GREY_COLOR];
return {
type: 'category' as const,
position: defaultValue(options?.legend?.position, 'bottom'),
align: defaultValue(options?.legend?.align, 'center'),
items: chartConfig.data.datasets[0].data.map((_data, i) => ({
color: backgroundColors[i % backgroundColors.length],
variant: CATEGORY_ITEM_VARIANT.Box,
dashed: false,
label: buildLegendLabels(i, options, chartConfig),
onClick: (index: number) => {
if (chart) {
chart.toggleDataVisibility(index);
chart.update();
}
},
onHover: (index: number, isVisible = true) => {
const { tooltip, chartArea } = chart;
if (tooltip && isVisible) {
tooltip.setActiveElements(
[
{
datasetIndex: 0,
index,
},
],
{
x: (chartArea.left + chartArea.right) / 2,
y: (chartArea.top + chartArea.bottom) / 2,
}
);
}
chart.update();
},
onLeave: () => {
const { tooltip } = chart;
if (tooltip) {
tooltip.setActiveElements([], { x: 0, y: 0 });
}
chart.update();
},
})),
};
}