import React from "react"; import type { CurveLayerSpec, PointLayerSpec, ViewportTransform } from "./types"; interface CurveLayerProps { curves?: CurveLayerSpec[]; points?: PointLayerSpec[]; viewport: ViewportTransform; compactLabels?: boolean; } export const CurveLayer: React.FC = ({ curves, points, viewport, compactLabels = false, }) => { if (!curves || curves.length === 0) return null; const placedCurveLabels: { sx: number; sy: number; text: string }[] = []; const screenPoints = (points ?? []).map((p) => ({ sx: viewport.toScreenX(p.x), sy: viewport.toScreenY(p.y), })); return ( {curves.map((curve, curveIdx) => { const xMin = Math.max(0, curve.xMin ?? 0); const xMax = Math.min(viewport.maxX, curve.xMax ?? viewport.maxX); if (xMax <= xMin) return null; const steps = Math.max(20, curve.steps ?? 120); const dx = (xMax - xMin) / steps; const segments: { x: number; y: number }[][] = []; let currentSegment: { x: number; y: number }[] = []; for (let i = 0; i <= steps; i++) { const x = xMin + i * dx; let y: number | null = null; try { y = curve.fn(x); } catch { y = null; } if ( y !== null && !Number.isNaN(y) && Number.isFinite(y) && y >= 0 && y <= viewport.maxY ) { currentSegment.push({ x, y }); } else { if (currentSegment.length > 0) { segments.push(currentSegment); currentSegment = []; } } } if (currentSegment.length > 0) { segments.push(currentSegment); } if (segments.length === 0) return null; const pathData = segments .map((seg) => seg .map((pt, idx) => { const sx = viewport.toScreenX(pt.x); const sy = viewport.toScreenY(pt.y); return `${idx === 0 ? "M" : "L"} ${sx.toFixed(2)} ${sy.toFixed(2)}`; }) .join(" ") ) .join(" "); let bestSeg = segments[0]; for (const s of segments) { if (s.length > bestSeg.length) bestSeg = s; } let labelPos: { sx: number; sy: number; anchor: "start" | "end" } | null = null; if (curve.label && bestSeg.length >= 2) { // Thử các vị trí ở đuôi phải và đầu trái của đường cong để tránh mọi điểm (A, B, C) và nhãn đường cong khác const candidateFractions = [ 0.88 - (curveIdx % 3) * 0.07, 0.76, 0.66, 0.22 + (curveIdx % 3) * 0.07, 0.14, 0.93, ]; for (const frac of candidateFractions) { const idx = Math.max( 0, Math.min(bestSeg.length - 1, Math.floor(bestSeg.length * frac)) ); const pt = bestSeg[idx]; const sx = viewport.toScreenX(pt.x); const sy = viewport.toScreenY(pt.y) - 9; const collidesCurve = placedCurveLabels.some( (prev) => Math.abs(prev.sx - sx) < 52 && Math.abs(prev.sy - sy) < 16 ); const collidesPoint = screenPoints.some( (sp) => Math.hypot(sp.sx - sx, sp.sy - sy) < 42 ); if (!collidesCurve && !collidesPoint) { labelPos = { sx, sy, anchor: frac < 0.4 ? "start" : "end", }; break; } } if (labelPos) { placedCurveLabels.push({ sx: labelPos.sx, sy: labelPos.sy, text: curve.label, }); } } const displayLabel = compactLabels && curve.label ? curve.label.split(":")[0].split("=")[0].trim() : curve.label; return ( {displayLabel && labelPos && ( {displayLabel} )} ); })} ); };