// Marks drawn inside a Plot frame: polylines, markers, reference slopes and // direct end-labels. Every mark is thin (2px lines, >=8px markers) and every // marker carries a shape as well as a colour. import type { ReactNode } from 'react' import type { Num } from '../engine/types.ts' import { ink, surface } from './palette.ts' import { linePath, type Frame } from './scales.ts' export type Shape = 'circle' | 'square' | 'triangle' | 'diamond' | 'cross' | 'plus' | 'star' export function Line({ x, y, f, color, dash, width = 2, opacity = 1, }: { x: readonly number[] y: readonly Num[] f: Frame color: string dash?: string width?: number opacity?: number }) { return ( ) } /** One marker, centred on (cx, cy) in pixels, about 9px across. */ export function markerPath(shape: Shape, cx: number, cy: number, r = 4.5): string { switch (shape) { case 'square': return `M${cx - r} ${cy - r}h${2 * r}v${2 * r}h${-2 * r}z` case 'triangle': return `M${cx} ${cy - r * 1.15}L${cx + r} ${cy + r * 0.8}L${cx - r} ${cy + r * 0.8}z` case 'diamond': return `M${cx} ${cy - r * 1.2}L${cx + r * 1.2} ${cy}L${cx} ${cy + r * 1.2}L${cx - r * 1.2} ${cy}z` case 'cross': return `M${cx - r} ${cy - r}L${cx + r} ${cy + r}M${cx + r} ${cy - r}L${cx - r} ${cy + r}` case 'plus': return `M${cx - r * 1.2} ${cy}h${2.4 * r}M${cx} ${cy - r * 1.2}v${2.4 * r}` case 'star': { let d = '' for (let k = 0; k < 3; k++) { const a = (Math.PI * k) / 3 d += `M${cx - r * Math.cos(a)} ${cy - r * Math.sin(a)}L${cx + r * Math.cos(a)} ${cy + r * Math.sin(a)}` } return d } default: return `M${cx - r} ${cy}a${r} ${r} 0 1 0 ${2 * r} 0a${r} ${r} 0 1 0 ${-2 * r} 0z` } } export function Markers({ x, y, f, color, shape = 'circle', r = 4.5, }: { x: readonly number[] y: readonly Num[] f: Frame color: string shape?: Shape r?: number }) { const stroked = shape === 'cross' || shape === 'plus' || shape === 'star' const out: ReactNode[] = [] for (let i = 0; i < x.length && i < y.length; i++) { const v = y[i] if (v == null || !isFinite(v) || !isFinite(x[i])) continue const px = f.sx(x[i]) const py = f.sy(v) if (py < -20 || py > f.ih + 20) continue out.push( , ) } return {out} } /** * A dashed reference line of slope p through a chosen point, for log-log * convergence plots: the eye reads "is the data parallel to this?" much better * than it reads a printed rate. */ export function RefSlope({ f, p, through, label, span = 0.6, labelAt = 'start', }: { f: Frame /** the exponent: error ~ n^p */ p: number /** a point the line passes through, in data coordinates */ through: [number, number] label: string /** fraction of the x range the line covers */ span?: number labelAt?: 'start' | 'end' }) { const [lx, hx] = f.xDomain const l = Math.log10(lx) const h = Math.log10(hx) const c = Math.log10(through[0]) const x0 = Math.pow(10, Math.max(l, c - (span * (h - l)) / 2)) const x1 = Math.pow(10, Math.min(h, c + (span * (h - l)) / 2)) const y = (xx: number) => through[1] * Math.pow(xx / through[0], p) return ( {labelAt === 'end' ? ( {label} ) : ( {label} )} ) } /** A label placed at the end of a curve, so the reader need not use the legend. */ export function EndLabel({ f, x, y, text, color, dx = 6, dy = 4, anchor = 'start', }: { f: Frame x: number y: number text: string color: string dx?: number dy?: number anchor?: 'start' | 'end' | 'middle' }) { return ( {text} ) }