// 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 ring around the data point under the cursor, and a box of its numbers.
*
* Drawn as SVG rather than an HTML overlay, like everything else in this
* layer, so it needs no positioning relative to the host element. The box
* width is estimated from the character count, which is why the lines are kept
* short; it flips to the other side of the point rather than leave the frame.
*/
export function PointTip({
f,
px,
py,
color,
lines,
}: {
f: Frame
px: number
py: number
color: string
/** first line is the series, in its own colour; the rest are the numbers */
lines: string[]
}) {
const size = 11.5
const pad = 6
const lh = 15
const w = Math.max(...lines.map((l) => l.length)) * 6.15 + 2 * pad
const h = lines.length * lh + 2 * pad - 3
const right = px + 14 + w < f.iw
const bx = Math.min(Math.max(right ? px + 14 : px - 14 - w, 2), Math.max(2, f.iw - w - 2))
const by = Math.min(Math.max(py - h / 2, 2), Math.max(2, f.ih - h - 2))
return (
{lines.map((l, i) => (
{l}
))}
)
}
/** 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}
)
}