116 lines
3.5 KiB
TypeScript
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;
|