From 0f5648c2c8c086de8ed666125265922a3bd7056f Mon Sep 17 00:00:00 2001 From: Nate Kelley Date: Wed, 2 Apr 2025 15:55:47 -0600 Subject: [PATCH] cross hair animations --- .../ui/charts/BusterChartJS/ChartJSTheme.ts | 4 +- .../chartjs-plugin-animation-delay.tsx | 118 --------------- .../core/plugins/chartjs-plugin-crosshair.tsx | 134 ++++++++++++++++++ .../ui/charts/BusterChartJS/inspiration.txt | 1 + 4 files changed, 138 insertions(+), 119 deletions(-) delete mode 100644 web/src/components/ui/charts/BusterChartJS/core/plugins/chartjs-plugin-animation-delay.tsx create mode 100644 web/src/components/ui/charts/BusterChartJS/core/plugins/chartjs-plugin-crosshair.tsx create mode 100644 web/src/components/ui/charts/BusterChartJS/inspiration.txt diff --git a/web/src/components/ui/charts/BusterChartJS/ChartJSTheme.ts b/web/src/components/ui/charts/BusterChartJS/ChartJSTheme.ts index c53e4df6e..a90a756eb 100644 --- a/web/src/components/ui/charts/BusterChartJS/ChartJSTheme.ts +++ b/web/src/components/ui/charts/BusterChartJS/ChartJSTheme.ts @@ -24,6 +24,7 @@ import { scales } from 'chart.js'; import { ChartMountedPlugin } from './core/plugins'; +import CrosshairPlugin from './core/plugins/chartjs-plugin-crosshair'; import ChartDeferred from 'chartjs-plugin-deferred'; import ChartJsAnnotationPlugin from 'chartjs-plugin-annotation'; import ChartDataLabels from 'chartjs-plugin-datalabels'; @@ -69,7 +70,8 @@ ChartJS.register( LogarithmicScale, TimeScale, TimeSeriesScale, - ChartDataLabels + ChartDataLabels, + CrosshairPlugin ); ChartJS.defaults.responsive = true; diff --git a/web/src/components/ui/charts/BusterChartJS/core/plugins/chartjs-plugin-animation-delay.tsx b/web/src/components/ui/charts/BusterChartJS/core/plugins/chartjs-plugin-animation-delay.tsx deleted file mode 100644 index c7f860918..000000000 --- a/web/src/components/ui/charts/BusterChartJS/core/plugins/chartjs-plugin-animation-delay.tsx +++ /dev/null @@ -1,118 +0,0 @@ -import { Chart, ChartType } from 'chart.js'; - -/** - * Configuration options for the Animation Delay plugin - */ -interface AnimationDelayPluginOptions { - /** Delay between each data point animation in milliseconds */ - delayBetweenPoints?: number; - /** Additional delay between datasets in milliseconds */ - delayBetweenDatasets?: number; - /** Whether to reset the delay when the chart is updated */ - resetOnUpdate?: boolean; -} - -// Context object passed to the delay function -interface DelayFunctionContext { - type: string; - mode: string; - dataIndex: number; - datasetIndex: number; -} - -/** - * Chart.js plugin for animating chart elements with a sequential delay effect. - * This creates a "build-up" animation where each data point appears after the previous one. - */ -const AnimationDelayPlugin = { - id: 'animationDelay', - - defaults: { - delayBetweenPoints: 150, - delayBetweenDatasets: 50, - resetOnUpdate: true - }, - - beforeInit(chart: Chart): void { - // Add flag to track if animation delay is active - chart._animationDelayActive = false; - }, - - beforeUpdate(chart: Chart): void { - const options = chart.options.plugins?.animationDelay || {}; - const pluginOptions = { - ...this.defaults, - ...options - }; - - if (pluginOptions.resetOnUpdate) { - chart._animationDelayActive = false; - } - }, - - beforeDraw(chart: Chart): void { - // Skip if animation delay is already applied - if (chart._animationDelayActive) return; - - const options = chart.options.plugins?.animationDelay || {}; - const pluginOptions = { - ...this.defaults, - ...options - }; - - // Ensure animation object exists - if (!chart.options.animation) { - chart.options.animation = {}; - } - - // Store original settings - const originalAnimationObj = { ...chart.options.animation }; - - // Apply new animation with delay - chart.options.animation = { - ...originalAnimationObj, - - // Define the delay function to create sequential animation effect - delay(context: DelayFunctionContext): number { - let delay = 0; - - // Only apply to data points in default mode - if (context.type === 'data' && context.mode === 'default' && !chart._animationDelayActive) { - // Calculate delay based on dataIndex and datasetIndex - delay = - context.dataIndex * (pluginOptions.delayBetweenPoints || 150) + - context.datasetIndex * (pluginOptions.delayBetweenDatasets || 50); - } - - return delay; - }, - - // Override onComplete function - onComplete(): void { - // Mark animation as active once complete - chart._animationDelayActive = true; - - // Call original onComplete if it exists - if (originalAnimationObj.onComplete) { - originalAnimationObj.onComplete.call(chart); - } - } - }; - } -}; - -// Register the plugin globally -Chart.register(AnimationDelayPlugin); - -// Add TypeScript type definitions -declare module 'chart.js' { - interface Chart { - _animationDelayActive: boolean; - } - - interface PluginOptionsByType { - animationDelay?: AnimationDelayPluginOptions; - } -} - -export default AnimationDelayPlugin; diff --git a/web/src/components/ui/charts/BusterChartJS/core/plugins/chartjs-plugin-crosshair.tsx b/web/src/components/ui/charts/BusterChartJS/core/plugins/chartjs-plugin-crosshair.tsx new file mode 100644 index 000000000..1c0791692 --- /dev/null +++ b/web/src/components/ui/charts/BusterChartJS/core/plugins/chartjs-plugin-crosshair.tsx @@ -0,0 +1,134 @@ +import { Chart, ChartEvent, Plugin } from 'chart.js'; + +declare module 'chart.js' { + interface Chart { + $crosshair?: { + x: number | null; + y: number | null; + }; + } +} + +export interface CrosshairPluginOptions { + lineColor?: string; + lineWidth?: number; + lineDash?: number[]; + labelBackgroundColor?: string; + labelFont?: string; + labelFontColor?: string; + labelHeight?: number; +} + +const crosshairPlugin: Plugin<'line'> = { + id: 'crosshairPlugin', + + defaults: { + lineColor: 'rgba(102, 102, 102, 1)', + lineWidth: 2, + lineDash: [6, 6], + labelBackgroundColor: 'rgba(102, 102, 102, 1)', + labelFont: 'bold 12px sans-serif', + labelFontColor: 'white', + labelHeight: 24 + }, + + // Initialize the crosshair state + beforeInit(chart: Chart) { + chart.$crosshair = { x: null, y: null }; + }, + + // Capture mouse events to update the crosshair coordinates + afterEvent(chart: Chart, args: { event: ChartEvent }) { + const event = args.event; + if (event.type === 'mousemove') { + chart.$crosshair = chart.$crosshair || { x: null, y: null }; + chart.$crosshair.x = event.x; + chart.$crosshair.y = event.y; + } else if (event.type === 'mouseout') { + if (chart.$crosshair) { + chart.$crosshair.x = null; + chart.$crosshair.y = null; + } + } + }, + + // Draw the crosshair lines and labels after the chart is rendered + afterDraw(chart: Chart, args, options: CrosshairPluginOptions) { + const { + ctx, + chartArea: { top, bottom, left, right } + } = chart; + const crosshair = chart.$crosshair; + if (!crosshair) return; + const { x, y } = crosshair; + + // Only draw if the pointer is within the chart area + if (x !== null && y !== null && x >= left && x <= right && y >= top && y <= bottom) { + ctx.save(); + + // Set common line styles + ctx.strokeStyle = options.lineColor || 'rgba(102, 102, 102, 1)'; + ctx.lineWidth = options.lineWidth || 2; + ctx.setLineDash(options.lineDash || [6, 6]); + + // Draw vertical line + ctx.beginPath(); + ctx.moveTo(x, top); + ctx.lineTo(x, bottom); + ctx.stroke(); + ctx.closePath(); + + // Draw horizontal line + ctx.beginPath(); + ctx.moveTo(left, y); + ctx.lineTo(right, y); + ctx.stroke(); + ctx.closePath(); + + // Reset dash settings + ctx.setLineDash([]); + + const LABEL_HEIGHT = options.labelHeight || 24; + + // Get the scales for label values + const yScale = chart.scales.y; + const xScale = chart.scales.x; + + // --- Draw Y-Axis Label --- + const yValue = yScale.getValueForPixel(y); + const yLabel = yValue?.toFixed(2) || ''; + + ctx.beginPath(); + ctx.fillStyle = options.labelBackgroundColor || 'rgba(102, 102, 102, 1)'; + // Draw label rectangle on left margin + ctx.fillRect(0, y - LABEL_HEIGHT / 2, left, LABEL_HEIGHT); + ctx.closePath(); + + ctx.font = options.labelFont || 'bold 12px sans-serif'; + ctx.fillStyle = options.labelFontColor || 'white'; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillText(yLabel, left / 2, y); + + // --- Draw X-Axis Label --- + const xValue = xScale.getValueForPixel(x) || 0; + const xDate = new Date(xValue); + const xLabel = xDate.toLocaleString('en-US', { day: 'numeric', month: 'long' }); + const textWidth = ctx.measureText(xLabel).width; + const padding = 12; + const labelWidth = textWidth + padding; + + ctx.beginPath(); + ctx.fillStyle = options.labelBackgroundColor || 'rgba(102, 102, 102, 1)'; + ctx.fillRect(x - labelWidth / 2, bottom, labelWidth, LABEL_HEIGHT); + ctx.closePath(); + + ctx.fillStyle = options.labelFontColor || 'white'; + ctx.fillText(xLabel, x, bottom + LABEL_HEIGHT / 2); + + ctx.restore(); + } + } +}; + +export default crosshairPlugin; diff --git a/web/src/components/ui/charts/BusterChartJS/inspiration.txt b/web/src/components/ui/charts/BusterChartJS/inspiration.txt new file mode 100644 index 000000000..e1ac59e08 --- /dev/null +++ b/web/src/components/ui/charts/BusterChartJS/inspiration.txt @@ -0,0 +1 @@ +https://andrew-dev-p.github.io/chartjs-showcase/drill-down-bar.html \ No newline at end of file