@altpsyche/maths 0.2.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +52 -1
- package/dist/figure/axis.d.ts +75 -0
- package/dist/figure/axis.js +133 -0
- package/dist/figure/path-data.d.ts +29 -0
- package/dist/figure/path-data.js +283 -0
- package/dist/figure/plot.d.ts +100 -0
- package/dist/figure/plot.js +261 -0
- package/dist/figure/scale.d.ts +32 -0
- package/dist/figure/scale.js +32 -0
- package/dist/figure/ticks.d.ts +46 -0
- package/dist/figure/ticks.js +97 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +6 -0
- package/dist/values/interval.d.ts +42 -0
- package/dist/values/interval.js +66 -0
- package/package.json +2 -1
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A function of one number drawn as a curve.
|
|
3
|
+
*
|
|
4
|
+
* The function is sampled at a fixed number of places and the samples are joined
|
|
5
|
+
* by cubics that leave each one at the slope the function has there. Joining them
|
|
6
|
+
* by straight lines instead is what makes a plotted sine look faceted, and the
|
|
7
|
+
* slope costs nothing to work out because it comes from samples already taken.
|
|
8
|
+
*
|
|
9
|
+
* A curve is cut where it leaves the graph. A pole otherwise draws as a line
|
|
10
|
+
* straight up the picture, and the coordinates on either side of it run to
|
|
11
|
+
* numbers a painter has nowhere to put.
|
|
12
|
+
*
|
|
13
|
+
* The count is fixed and the curve is never subdivided by how much it bends.
|
|
14
|
+
* Subdivision hands back a different number of points as the curve changes, and
|
|
15
|
+
* one path is walked into another by pairing their points, so a curve that
|
|
16
|
+
* resamples itself between frames could not be morphed into anything.
|
|
17
|
+
*/
|
|
18
|
+
import { type Interval } from '../values/interval.js';
|
|
19
|
+
import { type Coords } from './scale.js';
|
|
20
|
+
import { type GroupNode } from './node.js';
|
|
21
|
+
import type { Fill, Stroke } from './mark.js';
|
|
22
|
+
import { type Path } from './path.js';
|
|
23
|
+
export interface PlotOptions {
|
|
24
|
+
/** How many pieces the curve is cut into. */
|
|
25
|
+
samples?: number;
|
|
26
|
+
/** The run of x the curve is drawn over, which is the whole width of the graph
|
|
27
|
+
* where it is left out. */
|
|
28
|
+
over?: Interval;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* The curve of a function over a run of x, in the figure's own units, as one
|
|
32
|
+
* subpath per stretch of it that is on the graph.
|
|
33
|
+
*
|
|
34
|
+
* Each piece is a Hermite cubic written as a Bézier: the controls sit a third of
|
|
35
|
+
* the way along in x and carry the sample's own slope, which is the placement
|
|
36
|
+
* that makes the cubic pass through both samples at both slopes.
|
|
37
|
+
*/
|
|
38
|
+
export declare function plot(coords: Coords, of: (x: number) => number, options?: PlotOptions): Path;
|
|
39
|
+
export interface AreaOptions extends PlotOptions {
|
|
40
|
+
/** The height the region is measured down to, which is the axis itself where
|
|
41
|
+
* it is left out. A height off the graph sits at the near edge instead. */
|
|
42
|
+
baseline?: number;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* The region between a curve and a level line, closed, as one subpath per
|
|
46
|
+
* stretch of the curve that is on the graph.
|
|
47
|
+
*
|
|
48
|
+
* The curve is the same one `plot` draws over the same run, so the top of the
|
|
49
|
+
* region and the curve laid over it are the same geometry rather than two
|
|
50
|
+
* samplings that agree to within a sample.
|
|
51
|
+
*/
|
|
52
|
+
export declare function areaUnder(coords: Coords, of: (x: number) => number, over: Interval, options?: AreaOptions): Path;
|
|
53
|
+
export interface BarsOptions {
|
|
54
|
+
fill?: Fill;
|
|
55
|
+
stroke?: Stroke;
|
|
56
|
+
/** How many bars the run is cut into. */
|
|
57
|
+
bars?: number;
|
|
58
|
+
/** The run of x the bars cover, which is the whole width of the graph where it
|
|
59
|
+
* is left out. */
|
|
60
|
+
over?: Interval;
|
|
61
|
+
/** Where in each bar its height is read: at the left edge, the right edge or
|
|
62
|
+
* the middle. The three are what a reader is shown to see that the first is
|
|
63
|
+
* always short and the second always over. */
|
|
64
|
+
height?: 'left' | 'right' | 'middle';
|
|
65
|
+
/** The level the bars stand on. */
|
|
66
|
+
baseline?: number;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* The bars under a curve, each one named by its place in the run so a stagger
|
|
70
|
+
* can reach them one at a time.
|
|
71
|
+
*
|
|
72
|
+
* A bar whose top is off the graph is cut at the edge, and a bar whose height is
|
|
73
|
+
* not a number is left out. The style sits on the group rather than on each bar,
|
|
74
|
+
* which is what lets the whole run fade as one thing.
|
|
75
|
+
*/
|
|
76
|
+
export declare function riemannBars(name: string, coords: Coords, of: (x: number) => number, options?: BarsOptions): GroupNode;
|
|
77
|
+
/**
|
|
78
|
+
* The slope of a function at a point, from the central difference either side of
|
|
79
|
+
* it.
|
|
80
|
+
*
|
|
81
|
+
* The difference either side rather than one side is what makes the error fall
|
|
82
|
+
* as the step squared instead of the step, and it costs the same two calls.
|
|
83
|
+
*/
|
|
84
|
+
export declare function slopeOf(of: (x: number) => number, x: number, step?: number): number;
|
|
85
|
+
export interface TangentOptions {
|
|
86
|
+
/** How far the line reaches either side of the point, in graph units. */
|
|
87
|
+
reach?: number;
|
|
88
|
+
/** The step the slope is read over, for a function whose own scale asks for a
|
|
89
|
+
* different one. */
|
|
90
|
+
step?: number;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* The tangent to a curve at a point, as a straight line held inside the graph.
|
|
94
|
+
*
|
|
95
|
+
* The line is cut where it leaves the graph rather than sampled and broken like
|
|
96
|
+
* a curve, because a straight line crosses each edge once and the crossing is
|
|
97
|
+
* arithmetic rather than a search. A tangent at a steep place otherwise runs the
|
|
98
|
+
* width of the picture and out of it.
|
|
99
|
+
*/
|
|
100
|
+
export declare function tangentAt(coords: Coords, of: (x: number) => number, x: number, options?: TangentOptions): Path;
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A function of one number drawn as a curve.
|
|
3
|
+
*
|
|
4
|
+
* The function is sampled at a fixed number of places and the samples are joined
|
|
5
|
+
* by cubics that leave each one at the slope the function has there. Joining them
|
|
6
|
+
* by straight lines instead is what makes a plotted sine look faceted, and the
|
|
7
|
+
* slope costs nothing to work out because it comes from samples already taken.
|
|
8
|
+
*
|
|
9
|
+
* A curve is cut where it leaves the graph. A pole otherwise draws as a line
|
|
10
|
+
* straight up the picture, and the coordinates on either side of it run to
|
|
11
|
+
* numbers a painter has nowhere to put.
|
|
12
|
+
*
|
|
13
|
+
* The count is fixed and the curve is never subdivided by how much it bends.
|
|
14
|
+
* Subdivision hands back a different number of points as the curve changes, and
|
|
15
|
+
* one path is walked into another by pairing their points, so a curve that
|
|
16
|
+
* resamples itself between frames could not be morphed into anything.
|
|
17
|
+
*/
|
|
18
|
+
import { interval } from '../values/interval.js';
|
|
19
|
+
import { vec2 } from '../values/vec2.js';
|
|
20
|
+
import { pointOf, scaled } from './scale.js';
|
|
21
|
+
import { group, shape } from './node.js';
|
|
22
|
+
import { line, rect, straight } from './path.js';
|
|
23
|
+
/**
|
|
24
|
+
* How many pieces a curve is cut into when a figure does not say.
|
|
25
|
+
*
|
|
26
|
+
* A sine over two turns at this count leaves the drawn curve within 3.3e-4
|
|
27
|
+
* figure units of the true one, which is under a tenth of a pixel on the largest
|
|
28
|
+
* surface anything here is drawn at.
|
|
29
|
+
*/
|
|
30
|
+
const SAMPLES = 96;
|
|
31
|
+
/**
|
|
32
|
+
* The slope at each sample of one run, from the central difference of its
|
|
33
|
+
* neighbours, which is the tangent a Catmull-Rom spline uses.
|
|
34
|
+
*
|
|
35
|
+
* An end with evenly spaced neighbours takes the three-point one-sided
|
|
36
|
+
* difference, which is second order like the middle and exact for a quadratic.
|
|
37
|
+
* An end that was cut at the edge of the graph is not evenly spaced, so it takes
|
|
38
|
+
* the two-point difference instead: measured on a parabola at 64 samples, the
|
|
39
|
+
* three-point ends leave an uncut curve exact where the two-point ends leave it
|
|
40
|
+
* 3.7e-4 figure units out.
|
|
41
|
+
*/
|
|
42
|
+
function slopes(xs, ys) {
|
|
43
|
+
const last = xs.length - 1;
|
|
44
|
+
if (last < 1)
|
|
45
|
+
return [0];
|
|
46
|
+
const even = (a, b, c) => Math.abs((b - a) - (c - b)) < Math.abs(c - a) * 1e-9;
|
|
47
|
+
const out = [];
|
|
48
|
+
for (let at = 0; at <= last; at++) {
|
|
49
|
+
if (last < 2)
|
|
50
|
+
out.push((ys[last] - ys[0]) / (xs[last] - xs[0]));
|
|
51
|
+
else if (at === 0) {
|
|
52
|
+
out.push(even(xs[0], xs[1], xs[2])
|
|
53
|
+
? (-3 * ys[0] + 4 * ys[1] - ys[2]) / (xs[2] - xs[0])
|
|
54
|
+
: (ys[1] - ys[0]) / (xs[1] - xs[0]));
|
|
55
|
+
}
|
|
56
|
+
else if (at === last) {
|
|
57
|
+
out.push(even(xs[last - 2], xs[last - 1], xs[last])
|
|
58
|
+
? (3 * ys[last] - 4 * ys[last - 1] + ys[last - 2]) / (xs[last] - xs[last - 2])
|
|
59
|
+
: (ys[last] - ys[last - 1]) / (xs[last] - xs[last - 1]));
|
|
60
|
+
}
|
|
61
|
+
else
|
|
62
|
+
out.push((ys[at + 1] - ys[at - 1]) / (xs[at + 1] - xs[at - 1]));
|
|
63
|
+
}
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
/** How many times the gap either side of the edge is halved when looking for the
|
|
67
|
+
* place the curve crosses it. Twenty-four leaves it within a millionth of one
|
|
68
|
+
* sample's width. */
|
|
69
|
+
const HALVINGS = 24;
|
|
70
|
+
/**
|
|
71
|
+
* The place between a sample on the graph and a sample off it where the curve
|
|
72
|
+
* crosses the edge, by halving the gap between them.
|
|
73
|
+
*
|
|
74
|
+
* The y it hands back is held on the edge rather than taken from the function,
|
|
75
|
+
* so the cut end sits exactly on the boundary instead of a millionth past it.
|
|
76
|
+
*/
|
|
77
|
+
function crossing(of, drawable, bounds, inside, outside) {
|
|
78
|
+
let near = inside;
|
|
79
|
+
let far = outside;
|
|
80
|
+
for (let halving = 0; halving < HALVINGS; halving++) {
|
|
81
|
+
const middle = (near + far) / 2;
|
|
82
|
+
if (drawable(of(middle)))
|
|
83
|
+
near = middle;
|
|
84
|
+
else
|
|
85
|
+
far = middle;
|
|
86
|
+
}
|
|
87
|
+
return { x: near, y: interval.clampTo(bounds, of(near)) };
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* The curve of a function over a run of x, in the figure's own units, as one
|
|
91
|
+
* subpath per stretch of it that is on the graph.
|
|
92
|
+
*
|
|
93
|
+
* Each piece is a Hermite cubic written as a Bézier: the controls sit a third of
|
|
94
|
+
* the way along in x and carry the sample's own slope, which is the placement
|
|
95
|
+
* that makes the cubic pass through both samples at both slopes.
|
|
96
|
+
*/
|
|
97
|
+
export function plot(coords, of, options = {}) {
|
|
98
|
+
const samples = Math.max(1, Math.round(options.samples ?? SAMPLES));
|
|
99
|
+
const { from, to } = interval.ordered(options.over ?? coords.x.graph);
|
|
100
|
+
if (!(to > from))
|
|
101
|
+
return [];
|
|
102
|
+
const grain = ((to - from) / samples) * 1e-9;
|
|
103
|
+
const drawable = (y) => Number.isFinite(y) && interval.holds(coords.y.graph, y);
|
|
104
|
+
const xs = [];
|
|
105
|
+
const ys = [];
|
|
106
|
+
const on = [];
|
|
107
|
+
for (let at = 0; at <= samples; at++) {
|
|
108
|
+
const x = from + ((to - from) * at) / samples;
|
|
109
|
+
const y = of(x);
|
|
110
|
+
xs.push(x);
|
|
111
|
+
ys.push(y);
|
|
112
|
+
on.push(drawable(y));
|
|
113
|
+
}
|
|
114
|
+
const path = [];
|
|
115
|
+
let at = 0;
|
|
116
|
+
while (at <= samples) {
|
|
117
|
+
if (!on[at]) {
|
|
118
|
+
at++;
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
let end = at;
|
|
122
|
+
while (end + 1 <= samples && on[end + 1])
|
|
123
|
+
end++;
|
|
124
|
+
const runX = xs.slice(at, end + 1);
|
|
125
|
+
const runY = ys.slice(at, end + 1);
|
|
126
|
+
if (at > 0) {
|
|
127
|
+
const cut = crossing(of, drawable, coords.y.graph, xs[at], xs[at - 1]);
|
|
128
|
+
// A sample sitting exactly on the edge leaves nothing between it and the
|
|
129
|
+
// crossing, and a piece of no width has no slope to leave at.
|
|
130
|
+
if (xs[at] - cut.x > grain) {
|
|
131
|
+
runX.unshift(cut.x);
|
|
132
|
+
runY.unshift(cut.y);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (end < samples) {
|
|
136
|
+
const cut = crossing(of, drawable, coords.y.graph, xs[end], xs[end + 1]);
|
|
137
|
+
if (cut.x - xs[end] > grain) {
|
|
138
|
+
runX.push(cut.x);
|
|
139
|
+
runY.push(cut.y);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (runX.length > 1) {
|
|
143
|
+
const slope = slopes(runX, runY);
|
|
144
|
+
const curves = [];
|
|
145
|
+
for (let piece = 0; piece + 1 < runX.length; piece++) {
|
|
146
|
+
const reach = (runX[piece + 1] - runX[piece]) / 3;
|
|
147
|
+
curves.push({
|
|
148
|
+
control1: pointOf(coords, runX[piece] + reach, runY[piece] + reach * slope[piece]),
|
|
149
|
+
control2: pointOf(coords, runX[piece + 1] - reach, runY[piece + 1] - reach * slope[piece + 1]),
|
|
150
|
+
to: pointOf(coords, runX[piece + 1], runY[piece + 1]),
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
path.push({ start: pointOf(coords, runX[0], runY[0]), curves, closed: false });
|
|
154
|
+
}
|
|
155
|
+
at = end + 1;
|
|
156
|
+
}
|
|
157
|
+
return path;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* The region between a curve and a level line, closed, as one subpath per
|
|
161
|
+
* stretch of the curve that is on the graph.
|
|
162
|
+
*
|
|
163
|
+
* The curve is the same one `plot` draws over the same run, so the top of the
|
|
164
|
+
* region and the curve laid over it are the same geometry rather than two
|
|
165
|
+
* samplings that agree to within a sample.
|
|
166
|
+
*/
|
|
167
|
+
export function areaUnder(coords, of, over, options = {}) {
|
|
168
|
+
const foot = scaled(coords.y, interval.clampTo(coords.y.graph, options.baseline ?? 0));
|
|
169
|
+
return plot(coords, of, { ...options, over }).map((top) => {
|
|
170
|
+
const last = top.curves.length > 0 ? top.curves[top.curves.length - 1].to : top.start;
|
|
171
|
+
const under = vec2(last.x, foot);
|
|
172
|
+
const back = vec2(top.start.x, foot);
|
|
173
|
+
return {
|
|
174
|
+
start: top.start,
|
|
175
|
+
curves: [...top.curves, straight(last, under), straight(under, back), straight(back, top.start)],
|
|
176
|
+
closed: true,
|
|
177
|
+
};
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* The bars under a curve, each one named by its place in the run so a stagger
|
|
182
|
+
* can reach them one at a time.
|
|
183
|
+
*
|
|
184
|
+
* A bar whose top is off the graph is cut at the edge, and a bar whose height is
|
|
185
|
+
* not a number is left out. The style sits on the group rather than on each bar,
|
|
186
|
+
* which is what lets the whole run fade as one thing.
|
|
187
|
+
*/
|
|
188
|
+
export function riemannBars(name, coords, of, options = {}) {
|
|
189
|
+
const bars = Math.max(1, Math.round(options.bars ?? 8));
|
|
190
|
+
const { from, to } = interval.ordered(options.over ?? coords.x.graph);
|
|
191
|
+
const foot = interval.clampTo(coords.y.graph, options.baseline ?? 0);
|
|
192
|
+
const read = options.height ?? 'left';
|
|
193
|
+
const children = [];
|
|
194
|
+
for (let bar = 0; bar < bars; bar++) {
|
|
195
|
+
const left = from + ((to - from) * bar) / bars;
|
|
196
|
+
const right = from + ((to - from) * (bar + 1)) / bars;
|
|
197
|
+
const x = read === 'left' ? left : read === 'right' ? right : (left + right) / 2;
|
|
198
|
+
const y = of(x);
|
|
199
|
+
if (!Number.isFinite(y))
|
|
200
|
+
continue;
|
|
201
|
+
const top = interval.clampTo(coords.y.graph, y);
|
|
202
|
+
const corner = pointOf(coords, left, foot);
|
|
203
|
+
const far = pointOf(coords, right, top);
|
|
204
|
+
children.push(shape(String(bar), rect(corner, far.x - corner.x, far.y - corner.y), {}));
|
|
205
|
+
}
|
|
206
|
+
return group(name, children, { style: { fill: options.fill, stroke: options.stroke } });
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* The step the central difference is taken over, against the size of x.
|
|
210
|
+
*
|
|
211
|
+
* The cube root of the smallest gap between two doubles is the step where the
|
|
212
|
+
* two errors in a central difference are the same size: the formula's own error
|
|
213
|
+
* falls as the step squared and the rounding error rises as one over the step.
|
|
214
|
+
*/
|
|
215
|
+
const STEP = Math.cbrt(Number.EPSILON);
|
|
216
|
+
/**
|
|
217
|
+
* The slope of a function at a point, from the central difference either side of
|
|
218
|
+
* it.
|
|
219
|
+
*
|
|
220
|
+
* The difference either side rather than one side is what makes the error fall
|
|
221
|
+
* as the step squared instead of the step, and it costs the same two calls.
|
|
222
|
+
*/
|
|
223
|
+
export function slopeOf(of, x, step = STEP * Math.max(Math.abs(x), 1)) {
|
|
224
|
+
return (of(x + step) - of(x - step)) / (2 * step);
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* The tangent to a curve at a point, as a straight line held inside the graph.
|
|
228
|
+
*
|
|
229
|
+
* The line is cut where it leaves the graph rather than sampled and broken like
|
|
230
|
+
* a curve, because a straight line crosses each edge once and the crossing is
|
|
231
|
+
* arithmetic rather than a search. A tangent at a steep place otherwise runs the
|
|
232
|
+
* width of the picture and out of it.
|
|
233
|
+
*/
|
|
234
|
+
export function tangentAt(coords, of, x, options = {}) {
|
|
235
|
+
const reach = options.reach ?? interval.span(coords.x.graph) / 8;
|
|
236
|
+
const height = of(x);
|
|
237
|
+
const slope = slopeOf(of, x, options.step);
|
|
238
|
+
if (!Number.isFinite(height) || !Number.isFinite(slope))
|
|
239
|
+
return [];
|
|
240
|
+
const graphX = interval.ordered(coords.x.graph);
|
|
241
|
+
const graphY = interval.ordered(coords.y.graph);
|
|
242
|
+
let low = Math.max(x - reach, graphX.from);
|
|
243
|
+
let high = Math.min(x + reach, graphX.to);
|
|
244
|
+
if (slope === 0) {
|
|
245
|
+
if (!interval.holds(graphY, height))
|
|
246
|
+
return [];
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
const atY = (y) => x + (y - height) / slope;
|
|
250
|
+
const first = atY(graphY.from);
|
|
251
|
+
const second = atY(graphY.to);
|
|
252
|
+
low = Math.max(low, Math.min(first, second));
|
|
253
|
+
high = Math.min(high, Math.max(first, second));
|
|
254
|
+
}
|
|
255
|
+
if (!(high > low))
|
|
256
|
+
return [];
|
|
257
|
+
// Held on the graph so an end cut at an edge sits on it rather than a rounding
|
|
258
|
+
// error past it.
|
|
259
|
+
const at = (t) => pointOf(coords, t, interval.clampTo(graphY, height + slope * (t - x)));
|
|
260
|
+
return line(at(low), at(high));
|
|
261
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The mapping from the numbers on an axis to places in a figure.
|
|
3
|
+
*
|
|
4
|
+
* A scale is two intervals: the run of numbers a graph counts through, and where
|
|
5
|
+
* that run lands in the figure's own units. Two of them together turn a pair of
|
|
6
|
+
* graph numbers into a point.
|
|
7
|
+
*
|
|
8
|
+
* The mapping is a value a caller holds rather than something read back out of a
|
|
9
|
+
* drawn group. Axes draw two lines and a plotted curve needs the mapping, so a
|
|
10
|
+
* curve drawn over axes that were never drawn has to work.
|
|
11
|
+
*/
|
|
12
|
+
import { type Interval } from '../values/interval.js';
|
|
13
|
+
import { type Vec2 } from '../values/vec2.js';
|
|
14
|
+
export interface Scale {
|
|
15
|
+
/** The numbers the axis counts through. */
|
|
16
|
+
readonly graph: Interval;
|
|
17
|
+
/** Where those numbers land, in the figure's own units. */
|
|
18
|
+
readonly units: Interval;
|
|
19
|
+
}
|
|
20
|
+
export declare function scaleOf(graph: Interval, units: Interval): Scale;
|
|
21
|
+
/** A number on the axis, as a place in the figure's own units. */
|
|
22
|
+
export declare function scaled(scale: Scale, value: number): number;
|
|
23
|
+
/** A place in the figure's own units, as a number on the axis, which is what a
|
|
24
|
+
* reader pointing at the picture is asking for. */
|
|
25
|
+
export declare function unscaled(scale: Scale, place: number): number;
|
|
26
|
+
export interface Coords {
|
|
27
|
+
readonly x: Scale;
|
|
28
|
+
readonly y: Scale;
|
|
29
|
+
}
|
|
30
|
+
export declare function coordsOf(x: Scale, y: Scale): Coords;
|
|
31
|
+
/** A pair of graph numbers as a point in the figure's own units. */
|
|
32
|
+
export declare function pointOf(coords: Coords, x: number, y: number): Vec2;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The mapping from the numbers on an axis to places in a figure.
|
|
3
|
+
*
|
|
4
|
+
* A scale is two intervals: the run of numbers a graph counts through, and where
|
|
5
|
+
* that run lands in the figure's own units. Two of them together turn a pair of
|
|
6
|
+
* graph numbers into a point.
|
|
7
|
+
*
|
|
8
|
+
* The mapping is a value a caller holds rather than something read back out of a
|
|
9
|
+
* drawn group. Axes draw two lines and a plotted curve needs the mapping, so a
|
|
10
|
+
* curve drawn over axes that were never drawn has to work.
|
|
11
|
+
*/
|
|
12
|
+
import { interval } from '../values/interval.js';
|
|
13
|
+
import { vec2 } from '../values/vec2.js';
|
|
14
|
+
export function scaleOf(graph, units) {
|
|
15
|
+
return { graph, units };
|
|
16
|
+
}
|
|
17
|
+
/** A number on the axis, as a place in the figure's own units. */
|
|
18
|
+
export function scaled(scale, value) {
|
|
19
|
+
return interval.remap(value, scale.graph, scale.units);
|
|
20
|
+
}
|
|
21
|
+
/** A place in the figure's own units, as a number on the axis, which is what a
|
|
22
|
+
* reader pointing at the picture is asking for. */
|
|
23
|
+
export function unscaled(scale, place) {
|
|
24
|
+
return interval.remap(place, scale.units, scale.graph);
|
|
25
|
+
}
|
|
26
|
+
export function coordsOf(x, y) {
|
|
27
|
+
return { x, y };
|
|
28
|
+
}
|
|
29
|
+
/** A pair of graph numbers as a point in the figure's own units. */
|
|
30
|
+
export function pointOf(coords, x, y) {
|
|
31
|
+
return vec2(scaled(coords.x, x), scaled(coords.y, y));
|
|
32
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where the marks along an axis go, and what is written under each one.
|
|
3
|
+
*
|
|
4
|
+
* A reader adds up an axis in their head, so the steps between its ticks have to
|
|
5
|
+
* be numbers a head adds: one, two or five times a power of ten. That is the
|
|
6
|
+
* whole of the choice here, and the rest of this file is walking the interval in
|
|
7
|
+
* those steps and writing each one down without floating point noise in it.
|
|
8
|
+
*/
|
|
9
|
+
import { type Interval } from '../values/interval.js';
|
|
10
|
+
export interface Tick {
|
|
11
|
+
/** In the graph's own units, rounded to the decimals its own label shows, so
|
|
12
|
+
* the number drawn and the number placed are the same number. */
|
|
13
|
+
readonly value: number;
|
|
14
|
+
readonly label: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* The gap between one tick and the next for an interval that wants about this
|
|
18
|
+
* many of them.
|
|
19
|
+
*
|
|
20
|
+
* The published version rounds the whole span before dividing it, which suits a
|
|
21
|
+
* graph free to move its own bounds outward to the next nice number. An axis
|
|
22
|
+
* here is given its bounds and keeps them, so the span is divided as it stands:
|
|
23
|
+
* over eight ranges that turned a worst count error of four ticks into one.
|
|
24
|
+
*/
|
|
25
|
+
export declare function tickStep(bounds: Interval, about?: number): number;
|
|
26
|
+
/**
|
|
27
|
+
* A tick's value written out, with as many decimals as its step needs and no
|
|
28
|
+
* more.
|
|
29
|
+
*
|
|
30
|
+
* A step of a fifth reaches three fifths as 0.6000000000000001, and printing the
|
|
31
|
+
* number as it stands puts that in the picture. The decimal count comes from the
|
|
32
|
+
* step rather than from the value, so every label along one axis is written to
|
|
33
|
+
* the same width.
|
|
34
|
+
*/
|
|
35
|
+
export declare function labelFor(value: number, step: number): string;
|
|
36
|
+
/**
|
|
37
|
+
* Every multiple of the step inside the interval, from its lower bound upward,
|
|
38
|
+
* whichever way round the interval was given.
|
|
39
|
+
*
|
|
40
|
+
* The values are counted as multiples rather than reached by adding the step
|
|
41
|
+
* over and over, because the additions drift and the last one then misses the
|
|
42
|
+
* bound it sits on.
|
|
43
|
+
*/
|
|
44
|
+
export declare function multiplesOn(bounds: Interval, step: number): readonly number[];
|
|
45
|
+
/** Every tick inside the interval, with the number each one shows. */
|
|
46
|
+
export declare function ticksOn(bounds: Interval, about?: number): readonly Tick[];
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where the marks along an axis go, and what is written under each one.
|
|
3
|
+
*
|
|
4
|
+
* A reader adds up an axis in their head, so the steps between its ticks have to
|
|
5
|
+
* be numbers a head adds: one, two or five times a power of ten. That is the
|
|
6
|
+
* whole of the choice here, and the rest of this file is walking the interval in
|
|
7
|
+
* those steps and writing each one down without floating point noise in it.
|
|
8
|
+
*/
|
|
9
|
+
import { interval as intervalOf } from '../values/interval.js';
|
|
10
|
+
/**
|
|
11
|
+
* The nearest one, two, five or ten times a power of ten, which is Heckbert's
|
|
12
|
+
* nice numbers from Graphics Gems.
|
|
13
|
+
*
|
|
14
|
+
* Rounding up gives a step that never asks for more ticks than were wanted;
|
|
15
|
+
* rounding to nearest gives the count closest to it, which is what an axis
|
|
16
|
+
* wants because a step slightly too small crowds the labels.
|
|
17
|
+
*/
|
|
18
|
+
function niceNumber(value, toNearest) {
|
|
19
|
+
const power = Math.floor(Math.log10(value));
|
|
20
|
+
const decade = 10 ** power;
|
|
21
|
+
const leading = value / decade;
|
|
22
|
+
const nice = toNearest
|
|
23
|
+
? leading < 1.5
|
|
24
|
+
? 1
|
|
25
|
+
: leading < 3
|
|
26
|
+
? 2
|
|
27
|
+
: leading < 7
|
|
28
|
+
? 5
|
|
29
|
+
: 10
|
|
30
|
+
: leading <= 1
|
|
31
|
+
? 1
|
|
32
|
+
: leading <= 2
|
|
33
|
+
? 2
|
|
34
|
+
: leading <= 5
|
|
35
|
+
? 5
|
|
36
|
+
: 10;
|
|
37
|
+
return nice * decade;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The gap between one tick and the next for an interval that wants about this
|
|
41
|
+
* many of them.
|
|
42
|
+
*
|
|
43
|
+
* The published version rounds the whole span before dividing it, which suits a
|
|
44
|
+
* graph free to move its own bounds outward to the next nice number. An axis
|
|
45
|
+
* here is given its bounds and keeps them, so the span is divided as it stands:
|
|
46
|
+
* over eight ranges that turned a worst count error of four ticks into one.
|
|
47
|
+
*/
|
|
48
|
+
export function tickStep(bounds, about = 6) {
|
|
49
|
+
const span = intervalOf.span(bounds);
|
|
50
|
+
if (!Number.isFinite(span) || span === 0)
|
|
51
|
+
return 0;
|
|
52
|
+
return niceNumber(span / Math.max(1, Math.round(about) - 1), true);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* A tick's value written out, with as many decimals as its step needs and no
|
|
56
|
+
* more.
|
|
57
|
+
*
|
|
58
|
+
* A step of a fifth reaches three fifths as 0.6000000000000001, and printing the
|
|
59
|
+
* number as it stands puts that in the picture. The decimal count comes from the
|
|
60
|
+
* step rather than from the value, so every label along one axis is written to
|
|
61
|
+
* the same width.
|
|
62
|
+
*/
|
|
63
|
+
export function labelFor(value, step) {
|
|
64
|
+
const decimals = step > 0 ? Math.max(0, -Math.floor(Math.log10(step))) : 0;
|
|
65
|
+
const written = value.toFixed(decimals);
|
|
66
|
+
// A value a hair below zero rounds to a signed zero, which prints its minus.
|
|
67
|
+
return Number(written) === 0 ? (0).toFixed(decimals) : written;
|
|
68
|
+
}
|
|
69
|
+
/** How far past a bound a value may sit and still count as on it, against the
|
|
70
|
+
* step, so a tick landing on its own end is not lost to a rounding error. */
|
|
71
|
+
const ON_THE_BOUND = 1e-9;
|
|
72
|
+
/**
|
|
73
|
+
* Every multiple of the step inside the interval, from its lower bound upward,
|
|
74
|
+
* whichever way round the interval was given.
|
|
75
|
+
*
|
|
76
|
+
* The values are counted as multiples rather than reached by adding the step
|
|
77
|
+
* over and over, because the additions drift and the last one then misses the
|
|
78
|
+
* bound it sits on.
|
|
79
|
+
*/
|
|
80
|
+
export function multiplesOn(bounds, step) {
|
|
81
|
+
if (!(step > 0))
|
|
82
|
+
return [];
|
|
83
|
+
const { from, to } = intervalOf.ordered(bounds);
|
|
84
|
+
const slack = step * ON_THE_BOUND;
|
|
85
|
+
const values = [];
|
|
86
|
+
for (let count = Math.ceil(from / step - ON_THE_BOUND); count * step <= to + slack; count++) {
|
|
87
|
+
// Twelve digits drops the noise the multiplication leaves in the last few
|
|
88
|
+
// and shifts no value a step of any size could land on.
|
|
89
|
+
values.push(Number((count * step).toPrecision(12)));
|
|
90
|
+
}
|
|
91
|
+
return values;
|
|
92
|
+
}
|
|
93
|
+
/** Every tick inside the interval, with the number each one shows. */
|
|
94
|
+
export function ticksOn(bounds, about = 6) {
|
|
95
|
+
const step = tickStep(bounds, about);
|
|
96
|
+
return multiplesOn(bounds, step).map((value) => ({ value, label: labelFor(value, step) }));
|
|
97
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -14,15 +14,26 @@ export { vec2 } from './values/vec2.js';
|
|
|
14
14
|
export type { Vec2 } from './values/vec2.js';
|
|
15
15
|
export { vec3 } from './values/vec3.js';
|
|
16
16
|
export type { Vec3 } from './values/vec3.js';
|
|
17
|
+
export { interval } from './values/interval.js';
|
|
18
|
+
export type { Interval } from './values/interval.js';
|
|
17
19
|
export { mat3 } from './values/mat3.js';
|
|
18
20
|
export type { Mat3 } from './values/mat3.js';
|
|
19
21
|
export { SAME_TIME, keyAt, sampleTrack, sampleTracks, withKey, withoutKey } from './timing/track.js';
|
|
20
22
|
export type { Key, Track, TrackValue, Tracks } from './timing/track.js';
|
|
21
23
|
export { arc, circle, line, polygon, polyline, pointCount, pointOn, rect, straight, transformPath } from './figure/path.js';
|
|
22
24
|
export type { Cubic, Path, Subpath } from './figure/path.js';
|
|
25
|
+
export { pathFromData } from './figure/path-data.js';
|
|
23
26
|
export type { Colour, Fill, Mark, PathMark, Stroke, TextMark } from './figure/mark.js';
|
|
24
27
|
export { byAspect, fractionOf, matchingAspect, resolveExtent, viewMatrix } from './figure/extent.js';
|
|
25
28
|
export type { Extent, ExtentChoice, Fit } from './figure/extent.js';
|
|
29
|
+
export { areaUnder, plot, riemannBars, slopeOf, tangentAt } from './figure/plot.js';
|
|
30
|
+
export type { AreaOptions, BarsOptions, PlotOptions, TangentOptions } from './figure/plot.js';
|
|
31
|
+
export { axes, numberLine, numberPlane } from './figure/axis.js';
|
|
32
|
+
export type { AxesOptions, NumberLineOptions, NumberPlaneOptions } from './figure/axis.js';
|
|
33
|
+
export { coordsOf, pointOf, scaleOf, scaled, unscaled } from './figure/scale.js';
|
|
34
|
+
export type { Coords, Scale } from './figure/scale.js';
|
|
35
|
+
export { labelFor, tickStep, ticksOn } from './figure/ticks.js';
|
|
36
|
+
export type { Tick } from './figure/ticks.js';
|
|
26
37
|
export { flatten, group, shape, text } from './figure/node.js';
|
|
27
38
|
export type { GroupNode, Node, ShapeNode, Style, TextNode, TextOptions } from './figure/node.js';
|
|
28
39
|
export { fadeIn, fadeOut, fadeTo, draw, morph, moveBy } from './figure/animation.js';
|
package/dist/index.js
CHANGED
|
@@ -11,10 +11,16 @@ export { clamp, inverseLerp, lerp, remap } from './values/scalar.js';
|
|
|
11
11
|
export { curveFor, easeIn, easeOut, linear, smoothstep } from './values/ease.js';
|
|
12
12
|
export { vec2 } from './values/vec2.js';
|
|
13
13
|
export { vec3 } from './values/vec3.js';
|
|
14
|
+
export { interval } from './values/interval.js';
|
|
14
15
|
export { mat3 } from './values/mat3.js';
|
|
15
16
|
export { SAME_TIME, keyAt, sampleTrack, sampleTracks, withKey, withoutKey } from './timing/track.js';
|
|
16
17
|
export { arc, circle, line, polygon, polyline, pointCount, pointOn, rect, straight, transformPath } from './figure/path.js';
|
|
18
|
+
export { pathFromData } from './figure/path-data.js';
|
|
17
19
|
export { byAspect, fractionOf, matchingAspect, resolveExtent, viewMatrix } from './figure/extent.js';
|
|
20
|
+
export { areaUnder, plot, riemannBars, slopeOf, tangentAt } from './figure/plot.js';
|
|
21
|
+
export { axes, numberLine, numberPlane } from './figure/axis.js';
|
|
22
|
+
export { coordsOf, pointOf, scaleOf, scaled, unscaled } from './figure/scale.js';
|
|
23
|
+
export { labelFor, tickStep, ticksOn } from './figure/ticks.js';
|
|
18
24
|
export { flatten, group, shape, text } from './figure/node.js';
|
|
19
25
|
export { fadeIn, fadeOut, fadeTo, draw, morph, moveBy } from './figure/animation.js';
|
|
20
26
|
export { Timeline } from './figure/timeline.js';
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export interface Interval {
|
|
2
|
+
readonly from: number;
|
|
3
|
+
readonly to: number;
|
|
4
|
+
}
|
|
5
|
+
declare function makeInterval(from: number, to: number): Interval;
|
|
6
|
+
/** How far the interval reaches, without a sign, so an interval given either way
|
|
7
|
+
* round reports the same width. */
|
|
8
|
+
declare function span(interval: Interval): number;
|
|
9
|
+
/** Both bounds counting as inside, whichever way round they were given. */
|
|
10
|
+
declare function holds(interval: Interval, value: number): boolean;
|
|
11
|
+
/** The same two bounds with the lower one first, for anything that has to walk
|
|
12
|
+
* from one end to the other and would otherwise step backwards. */
|
|
13
|
+
declare function ordered(interval: Interval): Interval;
|
|
14
|
+
/** A fraction of the way along, and past either bound when the fraction is
|
|
15
|
+
* outside zero to one. */
|
|
16
|
+
declare function at(interval: Interval, along: number): number;
|
|
17
|
+
/** Held inside the two bounds, whichever way round they were given. */
|
|
18
|
+
declare function clampTo(interval: Interval, value: number): number;
|
|
19
|
+
/**
|
|
20
|
+
* The place a value holds in one interval, read at the same place in another.
|
|
21
|
+
*
|
|
22
|
+
* A source of no width has no place to read, so this hands back the target's
|
|
23
|
+
* first bound rather than an infinity that then spreads through every coordinate
|
|
24
|
+
* built on it.
|
|
25
|
+
*/
|
|
26
|
+
declare function remap(value: number, source: Interval, target: Interval): number;
|
|
27
|
+
/**
|
|
28
|
+
* The interval calls under one name, so a call site says which kind of thing it
|
|
29
|
+
* is reading and an import line says what these operate on.
|
|
30
|
+
*
|
|
31
|
+
* The width is not called `length`: a function's own `length` is how many
|
|
32
|
+
* arguments it takes, it is not writable, and assigning one throws.
|
|
33
|
+
*/
|
|
34
|
+
export declare const interval: typeof makeInterval & {
|
|
35
|
+
span: typeof span;
|
|
36
|
+
holds: typeof holds;
|
|
37
|
+
ordered: typeof ordered;
|
|
38
|
+
at: typeof at;
|
|
39
|
+
clampTo: typeof clampTo;
|
|
40
|
+
remap: typeof remap;
|
|
41
|
+
};
|
|
42
|
+
export {};
|