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 = ({ manager, options }) => { const canvasRef = useRef(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 ( ); }; export default PaneSeparatorOverlay;