From 0a43738992603f707b354aae6cd38944206d30aa Mon Sep 17 00:00:00 2001 From: Nate Kelley Date: Fri, 18 Apr 2025 13:12:55 -0600 Subject: [PATCH] add scales --- .../ui/charts/BusterChartJS/ChartJSTheme.ts | 8 +- .../plugins/chartjs-plugin-tick-duplicate.ts | 83 ++++++++++++++++++- .../hooks/useOptions/useXAxis/useXAxis.ts | 5 +- 3 files changed, 89 insertions(+), 7 deletions(-) diff --git a/web/src/components/ui/charts/BusterChartJS/ChartJSTheme.ts b/web/src/components/ui/charts/BusterChartJS/ChartJSTheme.ts index c30cef6b6..d6a1fe806 100644 --- a/web/src/components/ui/charts/BusterChartJS/ChartJSTheme.ts +++ b/web/src/components/ui/charts/BusterChartJS/ChartJSTheme.ts @@ -20,7 +20,8 @@ import { BubbleController, PieController, ScatterController, - DoughnutController + DoughnutController, + registry } from 'chart.js'; import { ChartMountedPlugin } from './core/plugins'; import ChartDeferred from 'chartjs-plugin-deferred'; @@ -31,6 +32,11 @@ import { isServer } from '@tanstack/react-query'; import './core/plugins/chartjs-plugin-dayjs'; import { truncateText } from '@/lib/text'; +import { DeduplicatedTimeScale } from './core/plugins/chartjs-plugin-tick-duplicate'; + +// Register the scale properly +registry.addScales(DeduplicatedTimeScale); + const fontFamily = isServer ? 'Roobert_Pro' : getComputedStyle(document.documentElement).getPropertyValue('--font-sans'); diff --git a/web/src/components/ui/charts/BusterChartJS/core/plugins/chartjs-plugin-tick-duplicate.ts b/web/src/components/ui/charts/BusterChartJS/core/plugins/chartjs-plugin-tick-duplicate.ts index 7412f14ec..609c96629 100644 --- a/web/src/components/ui/charts/BusterChartJS/core/plugins/chartjs-plugin-tick-duplicate.ts +++ b/web/src/components/ui/charts/BusterChartJS/core/plugins/chartjs-plugin-tick-duplicate.ts @@ -1,10 +1,25 @@ -import { ChartType, Chart, Plugin } from 'chart.js'; +import { ChartType, TimeScale } from 'chart.js'; declare module 'chart.js' { interface PluginOptionsByType {} -} -import { TimeScale } from 'chart.js'; + // Add interface extension for TimeScale + interface TimeScale { + _unit: + | 'millisecond' + | 'second' + | 'minute' + | 'hour' + | 'day' + | 'week' + | 'month' + | 'year' + | 'quarter'; + _adapter: { + format(time: unknown, format: string): string; + }; + } +} const originalBuildTicks = TimeScale.prototype.buildTicks; @@ -12,6 +27,7 @@ const originalBuildTicks = TimeScale.prototype.buildTicks; TimeScale.prototype.buildTicks = function () { // Step 1: Get default ticks using the original method const defaultTicks = originalBuildTicks.call(this); + console.log('defaultTicks', defaultTicks); // Step 2: Access tick callback and display format const tickCallback = this.options.ticks?.callback; @@ -57,3 +73,64 @@ TimeScale.prototype.buildTicks = function () { // Step 6: Return the filtered ticks return uniqueTicks; }; + +export class DeduplicatedTimeScale extends TimeScale { + static id = 'deduplicated-time'; + static defaults = TimeScale.defaults; + + /** + * Override buildTicks to eliminate duplicate ticks based on formatted values. + * @returns {Array} Array of unique tick objects + */ + buildTicks() { + console.log('buildTicks'); + // Step 1: Get default ticks from parent TimeScale + const defaultTicks = super.buildTicks(); + + // Step 2: Access tick callback and display format + const tickCallback = this.options.ticks?.callback; + const displayFormat = + this.options.time?.displayFormats?.[this._unit] || + this.options.time?.displayFormats?.month || + 'MMM'; + const format = this._adapter.format.bind(this._adapter); + + // Step 3: Track seen labels and collect unique ticks + const seen = new Set(); + const uniqueTicks = []; + + for (let i = 0; i < defaultTicks.length; i++) { + const tick = defaultTicks[i]; + + // Step 4: Generate tick label + let label; + try { + if (typeof tickCallback === 'function') { + // Pass tick value, index, and ticks array to callback + label = tickCallback.call(this, tick.value, i, defaultTicks); + } else { + // Format using the adapter with the appropriate display format + label = format(tick.value, displayFormat); + } + } catch (e) { + console.error('Tick callback error at index', i, e); + label = '???'; + } + + // Ensure label is a string for consistent comparison + const stringLabel = String(label ?? ''); + + // Step 5: Only include tick if label is unique + if (!seen.has(stringLabel)) { + seen.add(stringLabel); + uniqueTicks.push({ + ...tick, + label: stringLabel // Ensure the tick object has the correct label + }); + } + } + + // Step 6: Return the filtered ticks + return uniqueTicks; + } +} diff --git a/web/src/components/ui/charts/BusterChartJS/hooks/useOptions/useXAxis/useXAxis.ts b/web/src/components/ui/charts/BusterChartJS/hooks/useOptions/useXAxis/useXAxis.ts index 1087ebde0..5c5fa3ee0 100644 --- a/web/src/components/ui/charts/BusterChartJS/hooks/useOptions/useXAxis/useXAxis.ts +++ b/web/src/components/ui/charts/BusterChartJS/hooks/useOptions/useXAxis/useXAxis.ts @@ -10,7 +10,7 @@ import { import { useMemoizedFn } from '@/hooks'; import { useMemo } from 'react'; import { DeepPartial } from 'utility-types'; -import type { ScaleChartOptions, Scale, GridLineOptions } from 'chart.js'; +import type { ScaleChartOptions, Scale, GridLineOptions, TimeScale } from 'chart.js'; import { useXAxisTitle } from '../../../../commonHelpers/useXAxisTitle'; import { useIsStacked } from '../useIsStacked'; import { formatLabel, isNumericColumnType, truncateText } from '@/lib'; @@ -145,8 +145,7 @@ export const useXAxis = ({ const xColumnLabelFormat = xAxisColumnFormats[xKey]; const isAutoFormat = xColumnLabelFormat.dateFormat === 'auto'; if (isAutoFormat) { - //@ts-ignore - const unit = this.chart.scales['x']._unit as + const unit = (this.chart.scales['x'] as TimeScale)._unit as | 'millisecond' | 'second' | 'minute'