cross hair animations

This commit is contained in:
Nate Kelley 2025-04-02 15:55:47 -06:00
parent 5d4bde46a0
commit 0f5648c2c8
No known key found for this signature in database
GPG Key ID: FD90372AB8D98B4F
4 changed files with 138 additions and 119 deletions

View File

@ -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;

View File

@ -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<TType extends ChartType> {
animationDelay?: AnimationDelayPluginOptions;
}
}
export default AnimationDelayPlugin;

View File

@ -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;

View File

@ -0,0 +1 @@
https://andrew-dev-p.github.io/chartjs-showcase/drill-down-bar.html