088be4cAn interactive tutorial on the Dirichlet problem, accuracy against costJeremy Magland 1// Marks drawn inside a Plot frame: polylines, markers, reference slopes and
2// direct end-labels. Every mark is thin (2px lines, >=8px markers) and every
3// marker carries a shape as well as a colour.
4import type { ReactNode } from 'react'
5import type { Num } from '../engine/types.ts'
6import { ink, surface } from './palette.ts'
7import { linePath, type Frame } from './scales.ts'
9export type Shape = 'circle' | 'square' | 'triangle' | 'diamond' | 'cross' | 'plus' | 'star'
11export function Line({
12 x,
13 y,
14 f,
15 color,
16 dash,
17 width = 2,
18 opacity = 1,
19}: {
20 x: readonly number[]
21 y: readonly Num[]
22 f: Frame
23 color: string
24 dash?: string
25 width?: number
26 opacity?: number
27}) {
28 return (
29 <path
30 d={linePath(x, y, f)}
31 fill="none"
32 stroke={color}
33 strokeWidth={width}
34 strokeDasharray={dash}
35 strokeLinejoin="round"
36 strokeLinecap="round"
37 opacity={opacity}
38 />
39 )
40}
42/** One marker, centred on (cx, cy) in pixels, about 9px across. */
43export function markerPath(shape: Shape, cx: number, cy: number, r = 4.5): string {
44 switch (shape) {
45 case 'square':
46 return `M${cx - r} ${cy - r}h${2 * r}v${2 * r}h${-2 * r}z`
47 case 'triangle':
48 return `M${cx} ${cy - r * 1.15}L${cx + r} ${cy + r * 0.8}L${cx - r} ${cy + r * 0.8}z`
49 case 'diamond':
50 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`
51 case 'cross':
52 return `M${cx - r} ${cy - r}L${cx + r} ${cy + r}M${cx + r} ${cy - r}L${cx - r} ${cy + r}`
53 case 'plus':
54 return `M${cx - r * 1.2} ${cy}h${2.4 * r}M${cx} ${cy - r * 1.2}v${2.4 * r}`
55 case 'star': {
56 let d = ''
57 for (let k = 0; k < 3; k++) {
58 const a = (Math.PI * k) / 3
59 d += `M${cx - r * Math.cos(a)} ${cy - r * Math.sin(a)}L${cx + r * Math.cos(a)} ${cy + r * Math.sin(a)}`
60 }
61 return d
62 }
63 default:
64 return `M${cx - r} ${cy}a${r} ${r} 0 1 0 ${2 * r} 0a${r} ${r} 0 1 0 ${-2 * r} 0z`
65 }
66}
68export function Markers({
69 x,
70 y,
71 f,
72 color,
73 shape = 'circle',
74 r = 4.5,
75}: {
76 x: readonly number[]
77 y: readonly Num[]
78 f: Frame
79 color: string
80 shape?: Shape
81 r?: number
82}) {
83 const stroked = shape === 'cross' || shape === 'plus' || shape === 'star'
84 const out: ReactNode[] = []
85 for (let i = 0; i < x.length && i < y.length; i++) {
86 const v = y[i]
87 if (v == null || !isFinite(v) || !isFinite(x[i])) continue
88 const px = f.sx(x[i])
89 const py = f.sy(v)
90 if (py < -20 || py > f.ih + 20) continue
91 out.push(
92 <path
93 key={i}
94 d={markerPath(shape, px, py, r)}
95 fill={stroked ? 'none' : color}
96 stroke={stroked ? color : surface}
97 strokeWidth={stroked ? 2 : 1.5}
98 />,
99 )
100 }
101 return <g>{out}</g>
102}
104/**
105 * A dashed reference line of slope p through a chosen point, for log-log
106 * convergence plots: the eye reads "is the data parallel to this?" much better
107 * than it reads a printed rate.
108 */
109export function RefSlope({
110 f,
111 p,
112 through,
113 label,
114 span = 0.6,
115 labelAt = 'start',
116}: {
117 f: Frame
118 /** the exponent: error ~ n^p */
119 p: number
120 /** a point the line passes through, in data coordinates */
121 through: [number, number]
122 label: string
123 /** fraction of the x range the line covers */
124 span?: number
125 labelAt?: 'start' | 'end'
126}) {
127 const [lx, hx] = f.xDomain
128 const l = Math.log10(lx)
129 const h = Math.log10(hx)
130 const c = Math.log10(through[0])
131 const x0 = Math.pow(10, Math.max(l, c - (span * (h - l)) / 2))
132 const x1 = Math.pow(10, Math.min(h, c + (span * (h - l)) / 2))
133 const y = (xx: number) => through[1] * Math.pow(xx / through[0], p)
134 return (
135 <g>
136 <path
137 d={`M${f.sx(x0)} ${f.sy(y(x0))}L${f.sx(x1)} ${f.sy(y(x1))}`}
138 stroke={ink.muted}
139 strokeWidth={1.5}
140 strokeDasharray="5 4"
141 fill="none"
142 />
143 {labelAt === 'end' ? (
144 <text x={f.sx(x1) - 3} y={f.sy(y(x1)) - 6} fill={ink.muted} fontSize={11} textAnchor="end">
145 {label}
146 </text>
147 ) : (
148 <text x={f.sx(x0) + 3} y={f.sy(y(x0)) + 14} fill={ink.muted} fontSize={11} textAnchor="start">
149 {label}
150 </text>
151 )}
152 </g>
153 )
154}
156/** A label placed at the end of a curve, so the reader need not use the legend. */
157export function EndLabel({
158 f,
159 x,
160 y,
161 text,
162 color,
163 dx = 6,
164 dy = 4,
165 anchor = 'start',
166}: {
167 f: Frame
168 x: number
169 y: number
170 text: string
171 color: string
172 dx?: number
173 dy?: number
174 anchor?: 'start' | 'end' | 'middle'
175}) {
176 return (
177 <text
178 x={f.sx(x) + dx}
179 y={f.sy(y) + dy}
180 fill={color}
181 fontSize={11}
182 textAnchor={anchor}
183 style={{ paintOrder: 'stroke', stroke: surface, strokeWidth: 3 }}
184 >
185 {text}
186 </text>
187 )
188}