import { Chart as ChartJS, ChartData, ChartDataset, ChartOptions, Filler, LinearScale, LineElement, PointElement, } from 'chart.js'; import { DeepPartial } from 'ts-essentials'; import { useState } from 'react'; import { deepmerge, deepmergeCustom } from 'deepmerge-ts'; import { theme } from 'twin.macro'; import { hexToRgba } from '@/lib/helpers'; ChartJS.register(LineElement, PointElement, Filler, LinearScale); const options: ChartOptions<'line'> = { responsive: true, animation: false, plugins: { legend: { display: false }, title: { display: false }, tooltip: { enabled: false }, }, layout: { padding: 0, }, scales: { x: { min: 0, max: 19, type: 'linear', grid: { display: false, drawBorder: false, }, ticks: { display: false, }, }, y: { min: 0, type: 'linear', grid: { display: true, color: theme('colors.slate.700'), drawBorder: false, }, ticks: { display: true, count: 3, color: theme('colors.slate.200'), font: { family: theme('fontFamily.sans'), size: 11, weight: '400', }, }, }, }, elements: { point: { radius: 0, }, line: { tension: 0.15, }, }, }; function getOptions(opts?: DeepPartial> | undefined): ChartOptions<'line'> { // @ts-expect-error go away return deepmerge(options, opts ?? {}); } type ChartDatasetCallback = (value: ChartDataset<'line'>, index: number) => ChartDataset<'line'>; function getEmptyData(label: string, sets = 1, callback?: ChartDatasetCallback | undefined): ChartData<'line'> { const next = callback || (value => value); return { labels: Array(20) .fill(0) .map((_, index) => index), datasets: Array(sets) .fill(0) .map((_, index) => next( { fill: true, label, data: Array(20).fill(-5), borderColor: theme('colors.cyan.400'), backgroundColor: hexToRgba(theme('colors.cyan.700'), 0.5), }, index, ), ), }; } const merge = deepmergeCustom({ mergeArrays: false }); interface UseChartOptions { sets: number; options?: DeepPartial> | number | undefined; callback?: ChartDatasetCallback | undefined; } function useChart(label: string, opts?: UseChartOptions) { const options = getOptions( typeof opts?.options === 'number' ? { scales: { y: { min: 0, suggestedMax: opts.options } } } : opts?.options, ); const [data, setData] = useState(getEmptyData(label, opts?.sets || 1, opts?.callback)); const push = (items: number | null | (number | null)[]) => setData(state => merge(state, { datasets: (Array.isArray(items) ? items : [items]).map((item, index) => ({ ...state.datasets[index], data: state.datasets[index]?.data ?.slice(1) ?.concat(typeof item === 'number' ? Number(item.toFixed(2)) : item) ?? [], })), }), ); const clear = () => setData(state => merge(state, { datasets: state.datasets.map(value => ({ ...value, data: Array(20).fill(-5), })), }), ); return { props: { data, options }, push, clear }; } function useChartTickLabel(label: string, max: number, tickLabel: string, roundTo?: number) { return useChart(label, { sets: 1, options: { scales: { y: { suggestedMax: max, ticks: { callback(value) { return `${roundTo ? Number(value).toFixed(roundTo) : value}${tickLabel}`; }, }, }, }, }, }); } export { useChart, useChartTickLabel, getOptions, getEmptyData };