Files
goldenChart/frontend/src/components/PaneSeparatorOverlay.tsx
T

116 lines
3.5 KiB
TypeScript

import React, { useCallback, useEffect, useRef } from 'react';
import type { ChartManager } from '../utils/ChartManager';
import type { ChartPaneSeparatorOptions, PaneSeparatorStyle } from '../types/chartPaneSeparator';
import { classifyPaneBoundary, paneSeparatorOptionsKey } from '../types/chartPaneSeparator';
import { plotColorCss } from '../utils/plotColorUtils';
interface Props {
manager: ChartManager;
options: ChartPaneSeparatorOptions;
}
function applyLineDash(ctx: CanvasRenderingContext2D, style: PaneSeparatorStyle['lineStyle']) {
if (style === 'dashed') ctx.setLineDash([6, 4]);
else if (style === 'dotted') ctx.setLineDash([2, 3]);
else ctx.setLineDash([]);
}
function strokeBoundary(
ctx: CanvasRenderingContext2D,
cssW: number,
y: number,
style: PaneSeparatorStyle,
) {
if (!style.visible) return;
ctx.strokeStyle = plotColorCss(style.color);
ctx.lineWidth = style.width;
applyLineDash(ctx, style.lineStyle);
const lineY = y + style.width / 2;
ctx.beginPath();
ctx.moveTo(0, lineY);
ctx.lineTo(cssW, lineY);
ctx.stroke();
}
const PaneSeparatorOverlay: React.FC<Props> = ({ manager, options }) => {
const canvasRef = useRef<HTMLCanvasElement>(null);
const optionsKey = paneSeparatorOptionsKey(options);
const redraw = useCallback(() => {
const canvas = canvasRef.current;
const container = manager.getContainer();
if (!canvas || !container) return;
const cssW = container.clientWidth;
const cssH = container.clientHeight;
if (cssW <= 0 || cssH <= 0) return;
const dpr = window.devicePixelRatio || 1;
canvas.width = Math.round(cssW * dpr);
canvas.height = Math.round(cssH * dpr);
canvas.style.width = `${cssW}px`;
canvas.style.height = `${cssH}px`;
const ctx = canvas.getContext('2d');
if (!ctx) return;
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
ctx.clearRect(0, 0, cssW, cssH);
const layouts = manager.getPaneLayouts()
.filter(l => l.height > 8)
.sort((a, b) => a.topY - b.topY);
if (layouts.length < 2) return;
for (let i = 0; i < layouts.length - 1; i++) {
const lower = layouts[i];
const upper = layouts[i + 1];
const kind = classifyPaneBoundary(lower.paneIndex, upper.paneIndex);
if (kind === 'other') continue;
const style = kind === 'mainToIndicator'
? options.mainToIndicator
: options.indicatorToIndicator;
const y = lower.topY + lower.height;
strokeBoundary(ctx, cssW, y, style);
}
}, [manager, options, optionsKey]);
useEffect(() => {
redraw();
let layoutRaf = 0;
const scheduleRedraw = () => {
cancelAnimationFrame(layoutRaf);
layoutRaf = requestAnimationFrame(redraw);
};
const unsub = manager.subscribePaneLayout(scheduleRedraw);
const container = manager.getContainer();
const ro = new ResizeObserver(scheduleRedraw);
if (container) ro.observe(container);
const raf1 = requestAnimationFrame(redraw);
const raf2 = requestAnimationFrame(() => requestAnimationFrame(redraw));
return () => {
unsub();
ro.disconnect();
cancelAnimationFrame(layoutRaf);
cancelAnimationFrame(raf1);
cancelAnimationFrame(raf2);
};
}, [manager, redraw, optionsKey]);
return (
<canvas
ref={canvasRef}
className="pane-separator-overlay"
aria-hidden
style={{
position: 'absolute',
inset: 0,
pointerEvents: 'none',
zIndex: 4,
}}
/>
);
};
export default PaneSeparatorOverlay;