react-cheminfo 0.15.0 → 0.16.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 +96 -0
- package/lib/core.d.ts +1 -0
- package/lib/core.d.ts.map +1 -1
- package/lib/core.js +1 -0
- package/lib/core.js.map +1 -1
- package/lib/credits/core/credits.d.ts +17 -0
- package/lib/credits/core/credits.d.ts.map +1 -1
- package/lib/credits/core/credits.js +20 -0
- package/lib/credits/core/credits.js.map +1 -1
- package/lib/ecosystem/core/sites.d.ts.map +1 -1
- package/lib/ecosystem/core/sites.js +40 -0
- package/lib/ecosystem/core/sites.js.map +1 -1
- package/lib/ecosystem/core/types.d.ts +1 -1
- package/lib/ecosystem/core/types.d.ts.map +1 -1
- package/lib/ecosystem/ui/glyphs.d.ts.map +1 -1
- package/lib/ecosystem/ui/glyphs.js +11 -0
- package/lib/ecosystem/ui/glyphs.js.map +1 -1
- package/lib/parallel/core/index.d.ts +27 -0
- package/lib/parallel/core/index.d.ts.map +1 -0
- package/lib/parallel/core/index.js +20 -0
- package/lib/parallel/core/index.js.map +1 -0
- package/lib/parallel/core/parallelAxes.d.ts +103 -0
- package/lib/parallel/core/parallelAxes.d.ts.map +1 -0
- package/lib/parallel/core/parallelAxes.js +146 -0
- package/lib/parallel/core/parallelAxes.js.map +1 -0
- package/lib/parallel/core/parallelBrush.d.ts +79 -0
- package/lib/parallel/core/parallelBrush.d.ts.map +1 -0
- package/lib/parallel/core/parallelBrush.js +99 -0
- package/lib/parallel/core/parallelBrush.js.map +1 -0
- package/lib/parallel/core/parallelExtent.d.ts +28 -0
- package/lib/parallel/core/parallelExtent.d.ts.map +1 -0
- package/lib/parallel/core/parallelExtent.js +47 -0
- package/lib/parallel/core/parallelExtent.js.map +1 -0
- package/lib/parallel/core/parallelFilter.d.ts +36 -0
- package/lib/parallel/core/parallelFilter.d.ts.map +1 -0
- package/lib/parallel/core/parallelFilter.js +74 -0
- package/lib/parallel/core/parallelFilter.js.map +1 -0
- package/lib/parallel/core/parallelHit.d.ts +47 -0
- package/lib/parallel/core/parallelHit.d.ts.map +1 -0
- package/lib/parallel/core/parallelHit.js +81 -0
- package/lib/parallel/core/parallelHit.js.map +1 -0
- package/lib/parallel/core/parallelPaint.d.ts +104 -0
- package/lib/parallel/core/parallelPaint.d.ts.map +1 -0
- package/lib/parallel/core/parallelPaint.js +160 -0
- package/lib/parallel/core/parallelPaint.js.map +1 -0
- package/lib/parallel/core/parallelPalette.d.ts +31 -0
- package/lib/parallel/core/parallelPalette.d.ts.map +1 -0
- package/lib/parallel/core/parallelPalette.js +68 -0
- package/lib/parallel/core/parallelPalette.js.map +1 -0
- package/lib/parallel/core/parallelTicks.d.ts +34 -0
- package/lib/parallel/core/parallelTicks.d.ts.map +1 -0
- package/lib/parallel/core/parallelTicks.js +116 -0
- package/lib/parallel/core/parallelTicks.js.map +1 -0
- package/lib/parallel/core/parallelTypes.d.ts +129 -0
- package/lib/parallel/core/parallelTypes.d.ts.map +1 -0
- package/lib/parallel/core/parallelTypes.js +29 -0
- package/lib/parallel/core/parallelTypes.js.map +1 -0
- package/lib/parallel/ui/ParallelAxesLayer.d.ts +28 -0
- package/lib/parallel/ui/ParallelAxesLayer.d.ts.map +1 -0
- package/lib/parallel/ui/ParallelAxesLayer.js +29 -0
- package/lib/parallel/ui/ParallelAxesLayer.js.map +1 -0
- package/lib/parallel/ui/ParallelCoordinates.d.ts +21 -0
- package/lib/parallel/ui/ParallelCoordinates.d.ts.map +1 -0
- package/lib/parallel/ui/ParallelCoordinates.js +109 -0
- package/lib/parallel/ui/ParallelCoordinates.js.map +1 -0
- package/lib/parallel/ui/ParallelLabels.d.ts +26 -0
- package/lib/parallel/ui/ParallelLabels.d.ts.map +1 -0
- package/lib/parallel/ui/ParallelLabels.js +25 -0
- package/lib/parallel/ui/ParallelLabels.js.map +1 -0
- package/lib/parallel/ui/ParallelTooltip.d.ts +26 -0
- package/lib/parallel/ui/ParallelTooltip.d.ts.map +1 -0
- package/lib/parallel/ui/ParallelTooltip.js +30 -0
- package/lib/parallel/ui/ParallelTooltip.js.map +1 -0
- package/lib/parallel/ui/index.d.ts +25 -0
- package/lib/parallel/ui/index.d.ts.map +1 -0
- package/lib/parallel/ui/index.js +18 -0
- package/lib/parallel/ui/index.js.map +1 -0
- package/lib/parallel/ui/parallelCoordinatesProps.d.ts +185 -0
- package/lib/parallel/ui/parallelCoordinatesProps.d.ts.map +1 -0
- package/lib/parallel/ui/parallelCoordinatesProps.js +11 -0
- package/lib/parallel/ui/parallelCoordinatesProps.js.map +1 -0
- package/lib/parallel/ui/parallelGestureModel.d.ts +52 -0
- package/lib/parallel/ui/parallelGestureModel.d.ts.map +1 -0
- package/lib/parallel/ui/parallelGestureModel.js +80 -0
- package/lib/parallel/ui/parallelGestureModel.js.map +1 -0
- package/lib/parallel/ui/parallelInk.d.ts +76 -0
- package/lib/parallel/ui/parallelInk.d.ts.map +1 -0
- package/lib/parallel/ui/parallelInk.js +113 -0
- package/lib/parallel/ui/parallelInk.js.map +1 -0
- package/lib/parallel/ui/parallelPlotModel.d.ts +55 -0
- package/lib/parallel/ui/parallelPlotModel.d.ts.map +1 -0
- package/lib/parallel/ui/parallelPlotModel.js +113 -0
- package/lib/parallel/ui/parallelPlotModel.js.map +1 -0
- package/lib/parallel/ui/parallelStyles.d.ts +83 -0
- package/lib/parallel/ui/parallelStyles.d.ts.map +1 -0
- package/lib/parallel/ui/parallelStyles.js +131 -0
- package/lib/parallel/ui/parallelStyles.js.map +1 -0
- package/lib/parallel/ui/useParallelCanvas.d.ts +49 -0
- package/lib/parallel/ui/useParallelCanvas.d.ts.map +1 -0
- package/lib/parallel/ui/useParallelCanvas.js +58 -0
- package/lib/parallel/ui/useParallelCanvas.js.map +1 -0
- package/lib/parallel/ui/useParallelGesture.d.ts +67 -0
- package/lib/parallel/ui/useParallelGesture.d.ts.map +1 -0
- package/lib/parallel/ui/useParallelGesture.js +139 -0
- package/lib/parallel/ui/useParallelGesture.js.map +1 -0
- package/lib/structure/ui/useToolbarFloor.d.ts.map +1 -1
- package/lib/structure/ui/useToolbarFloor.js +29 -22
- package/lib/structure/ui/useToolbarFloor.js.map +1 -1
- package/lib/ui.d.ts +1 -0
- package/lib/ui.d.ts.map +1 -1
- package/lib/ui.js +1 -0
- package/lib/ui.js.map +1 -1
- package/package.json +1 -1
- package/src/core.ts +1 -0
- package/src/credits/core/credits.ts +21 -0
- package/src/ecosystem/core/sites.ts +40 -0
- package/src/ecosystem/core/types.ts +4 -1
- package/src/ecosystem/ui/glyphs.tsx +46 -0
- package/src/parallel/core/index.ts +80 -0
- package/src/parallel/core/parallelAxes.ts +207 -0
- package/src/parallel/core/parallelBrush.ts +145 -0
- package/src/parallel/core/parallelExtent.ts +56 -0
- package/src/parallel/core/parallelFilter.ts +94 -0
- package/src/parallel/core/parallelHit.ts +106 -0
- package/src/parallel/core/parallelPaint.ts +231 -0
- package/src/parallel/core/parallelPalette.ts +80 -0
- package/src/parallel/core/parallelTicks.ts +164 -0
- package/src/parallel/core/parallelTypes.ts +147 -0
- package/src/parallel/ui/ParallelAxesLayer.tsx +123 -0
- package/src/parallel/ui/ParallelCoordinates.tsx +233 -0
- package/src/parallel/ui/ParallelLabels.tsx +56 -0
- package/src/parallel/ui/ParallelTooltip.tsx +55 -0
- package/src/parallel/ui/index.ts +50 -0
- package/src/parallel/ui/parallelCoordinatesProps.ts +199 -0
- package/src/parallel/ui/parallelGestureModel.ts +109 -0
- package/src/parallel/ui/parallelInk.ts +182 -0
- package/src/parallel/ui/parallelPlotModel.ts +130 -0
- package/src/parallel/ui/parallelStyles.ts +146 -0
- package/src/parallel/ui/useParallelCanvas.ts +104 -0
- package/src/parallel/ui/useParallelGesture.ts +227 -0
- package/src/structure/ui/useToolbarFloor.ts +27 -22
- package/src/ui.ts +1 -0
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where each axis stands, how far it reaches, and how a value on it becomes a
|
|
3
|
+
* pixel.
|
|
4
|
+
*
|
|
5
|
+
* The first axis sits flush with the left of the drawing area and the last one
|
|
6
|
+
* flush with the right, rather than inset the way a two-axis chart insets its
|
|
7
|
+
* plot: the axes *are* the figure here, and a reader comparing the leftmost
|
|
8
|
+
* column against the rightmost must see both of them, not a margin.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { chartScale } from '../../chart/core/chartScale.ts';
|
|
12
|
+
|
|
13
|
+
import { parallelExtent } from './parallelExtent.ts';
|
|
14
|
+
import { parallelTicks } from './parallelTicks.ts';
|
|
15
|
+
import type {
|
|
16
|
+
ParallelAxis,
|
|
17
|
+
ParallelAxisScale,
|
|
18
|
+
ParallelTick,
|
|
19
|
+
} from './parallelTypes.ts';
|
|
20
|
+
|
|
21
|
+
/** Room kept around the drawing area for the axis names and the tick labels. */
|
|
22
|
+
export const PARALLEL_MARGIN = {
|
|
23
|
+
/** Above the drawing area, where the axis names are written. */
|
|
24
|
+
top: 26,
|
|
25
|
+
/** To the right of it, so the last axis's labels are not clipped. */
|
|
26
|
+
right: 54,
|
|
27
|
+
/** Below it. */
|
|
28
|
+
bottom: 22,
|
|
29
|
+
/** To its left, where the first axis's tick labels are written. */
|
|
30
|
+
left: 54,
|
|
31
|
+
} as const;
|
|
32
|
+
|
|
33
|
+
/** How a value on one axis becomes a pixel, as the two numbers a multiply-add needs. */
|
|
34
|
+
export interface ParallelAxisPixels {
|
|
35
|
+
/** The pixel the value zero lands on — or the value one, on a log axis. */
|
|
36
|
+
readonly offset: number;
|
|
37
|
+
/** Pixels per unit, negative because an axis grows upward. Zero when the axis is flat. */
|
|
38
|
+
readonly factor: number;
|
|
39
|
+
/** Whether a value is read through its base-10 logarithm first. */
|
|
40
|
+
readonly logarithmic: boolean;
|
|
41
|
+
/** The smallest value a log axis can place; anything at or below it is pinned there. */
|
|
42
|
+
readonly floor: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** One axis, placed and measured, ready to draw. */
|
|
46
|
+
export interface ParallelAxisLayout {
|
|
47
|
+
/** The axis this lays out. */
|
|
48
|
+
id: string;
|
|
49
|
+
/** Its name. */
|
|
50
|
+
label: string;
|
|
51
|
+
/** The unit written after the name, or an empty string. */
|
|
52
|
+
unit: string;
|
|
53
|
+
/** Horizontal centre, in pixels from the left of the drawing area. */
|
|
54
|
+
x: number;
|
|
55
|
+
/** The value at the bottom of the axis. */
|
|
56
|
+
min: number;
|
|
57
|
+
/** The value at its top. */
|
|
58
|
+
max: number;
|
|
59
|
+
/** How values are placed between them. */
|
|
60
|
+
scale: ParallelAxisScale;
|
|
61
|
+
/** Its graduations, already written out, from the bottom up. */
|
|
62
|
+
ticks: readonly ParallelTick[];
|
|
63
|
+
/** How a value on it becomes a pixel, worked out once here. */
|
|
64
|
+
pixels: ParallelAxisPixels;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** A flat axis: every value lands on the top of the drawing area. */
|
|
68
|
+
const FLAT: ParallelAxisPixels = {
|
|
69
|
+
offset: 0,
|
|
70
|
+
factor: 0,
|
|
71
|
+
logarithmic: false,
|
|
72
|
+
floor: 1,
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/** How far below the largest value a log axis may reach when its low end is not positive. */
|
|
76
|
+
const LOG_FLOOR_SHARE = 1e-12;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Place every axis across the drawing area and work out what it reaches.
|
|
80
|
+
* @param axes - The axes, from left to right.
|
|
81
|
+
* @param count - How many rows the columns are read over.
|
|
82
|
+
* @param innerWidth - Width between the first and the last axis, in pixels.
|
|
83
|
+
* @param innerHeight - Height of the drawing area, in pixels.
|
|
84
|
+
* @returns One layout per axis, in the order they were given. A single axis is
|
|
85
|
+
* centred; two or more are spread evenly with the outer ones flush with the
|
|
86
|
+
* edges.
|
|
87
|
+
*/
|
|
88
|
+
export function parallelAxisLayouts(
|
|
89
|
+
axes: readonly ParallelAxis[],
|
|
90
|
+
count: number,
|
|
91
|
+
innerWidth: number,
|
|
92
|
+
innerHeight: number,
|
|
93
|
+
): ParallelAxisLayout[] {
|
|
94
|
+
const layouts: ParallelAxisLayout[] = [];
|
|
95
|
+
const total = axes.length;
|
|
96
|
+
for (let index = 0; index < total; index++) {
|
|
97
|
+
const axis = axes[index];
|
|
98
|
+
if (axis === undefined) continue;
|
|
99
|
+
const extent = axisExtent(axis, count);
|
|
100
|
+
const graduation = parallelTicks(axis, extent.min, extent.max);
|
|
101
|
+
const [min, max] = graduation.domain;
|
|
102
|
+
const scale = axis.scale ?? 'linear';
|
|
103
|
+
layouts.push({
|
|
104
|
+
id: axis.id,
|
|
105
|
+
label: axis.label,
|
|
106
|
+
unit: axis.unit ?? '',
|
|
107
|
+
x: total === 1 ? innerWidth / 2 : (innerWidth * index) / (total - 1),
|
|
108
|
+
min,
|
|
109
|
+
max,
|
|
110
|
+
scale,
|
|
111
|
+
ticks: graduation.ticks,
|
|
112
|
+
pixels: parallelAxisPixels(min, max, scale, innerHeight),
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
return layouts;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* The mapping that puts `max` at the top of the drawing area and `min` at its
|
|
120
|
+
* bottom.
|
|
121
|
+
* @param min - The value at the bottom.
|
|
122
|
+
* @param max - The value at the top.
|
|
123
|
+
* @param scale - How values are placed between them.
|
|
124
|
+
* @param innerHeight - Height of the drawing area, in pixels.
|
|
125
|
+
* @returns The mapping. An axis with no span, or one drawn in no height at
|
|
126
|
+
* all, comes back flat, which pins every value to the top rather than dividing
|
|
127
|
+
* by zero.
|
|
128
|
+
*/
|
|
129
|
+
export function parallelAxisPixels(
|
|
130
|
+
min: number,
|
|
131
|
+
max: number,
|
|
132
|
+
scale: ParallelAxisScale,
|
|
133
|
+
innerHeight: number,
|
|
134
|
+
): ParallelAxisPixels {
|
|
135
|
+
if (max - min <= 0 || innerHeight <= 0) return FLAT;
|
|
136
|
+
if (scale !== 'log') {
|
|
137
|
+
const linear = chartScale(min, max, innerHeight, 0);
|
|
138
|
+
return { ...linear, logarithmic: false, floor: 1 };
|
|
139
|
+
}
|
|
140
|
+
const floor = min > 0 ? min : max > 0 ? max * LOG_FLOOR_SHARE : 1;
|
|
141
|
+
const low = Math.log10(floor);
|
|
142
|
+
const high = Math.log10(Math.max(max, floor));
|
|
143
|
+
if (high - low <= 0) return FLAT;
|
|
144
|
+
const mapping = chartScale(low, high, innerHeight, 0);
|
|
145
|
+
return { ...mapping, logarithmic: true, floor };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Where one value falls on an axis, in pixels.
|
|
150
|
+
*
|
|
151
|
+
* For a single conversion — a tick, a brushed edge, a highlighted crossing.
|
|
152
|
+
* Inside a loop over rows, read `axis.pixels` and write the multiply-add out.
|
|
153
|
+
* @param pixels - The axis's mapping.
|
|
154
|
+
* @param value - The value to place.
|
|
155
|
+
* @returns The pixel, measured from the top of the drawing area. A value
|
|
156
|
+
* outside the axis lands outside the drawing area and is not clipped.
|
|
157
|
+
*/
|
|
158
|
+
export function parallelPixelAt(
|
|
159
|
+
pixels: ParallelAxisPixels,
|
|
160
|
+
value: number,
|
|
161
|
+
): number {
|
|
162
|
+
if (pixels.factor === 0) return pixels.offset;
|
|
163
|
+
const placed = pixels.logarithmic
|
|
164
|
+
? Math.log10(Math.max(value, pixels.floor))
|
|
165
|
+
: value;
|
|
166
|
+
return pixels.offset + placed * pixels.factor;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Where one value falls on an axis, in pixels.
|
|
171
|
+
* @param value - The value to place.
|
|
172
|
+
* @param axis - The axis it belongs to.
|
|
173
|
+
* @returns The pixel, measured from the top of the drawing area.
|
|
174
|
+
*/
|
|
175
|
+
export function parallelValueToY(
|
|
176
|
+
value: number,
|
|
177
|
+
axis: ParallelAxisLayout,
|
|
178
|
+
): number {
|
|
179
|
+
return parallelPixelAt(axis.pixels, value);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* What value a pixel on an axis stands for — the brush's direction.
|
|
184
|
+
* @param y - The pixel, measured from the top of the drawing area.
|
|
185
|
+
* @param axis - The axis it belongs to.
|
|
186
|
+
* @returns The value there, or the top of a flat axis. Zero comes back
|
|
187
|
+
* unsigned, so an interval that reaches the end of an axis never reads `-0`.
|
|
188
|
+
*/
|
|
189
|
+
export function parallelYToValue(y: number, axis: ParallelAxisLayout): number {
|
|
190
|
+
const { pixels, max } = axis;
|
|
191
|
+
if (pixels.factor === 0) return max;
|
|
192
|
+
const placed = (y - pixels.offset) / pixels.factor;
|
|
193
|
+
if (pixels.logarithmic) return 10 ** placed;
|
|
194
|
+
return placed === 0 ? 0 : placed;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function axisExtent(
|
|
198
|
+
axis: ParallelAxis,
|
|
199
|
+
count: number,
|
|
200
|
+
): { min: number; max: number } {
|
|
201
|
+
const domain = axis.domain;
|
|
202
|
+
if (domain === undefined) return parallelExtent(axis.values, count);
|
|
203
|
+
return {
|
|
204
|
+
min: Math.min(domain[0], domain[1]),
|
|
205
|
+
max: Math.max(domain[0], domain[1]),
|
|
206
|
+
};
|
|
207
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The brush, as arithmetic rather than as a library.
|
|
3
|
+
*
|
|
4
|
+
* Everything a vertical brush does — create by dragging on a bare axis, move
|
|
5
|
+
* the band, drag either edge, clear by pressing outside it — is four answers
|
|
6
|
+
* to one question: which part of the axis the press landed on. Written this
|
|
7
|
+
* way it is a pure function of a press and a pointer, so the whole gesture is
|
|
8
|
+
* covered by tests in a runtime with no DOM at all, which is the one thing a
|
|
9
|
+
* DOM-mutating brush could never be.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { ParallelAxisLayout } from './parallelAxes.ts';
|
|
13
|
+
import { parallelValueToY, parallelYToValue } from './parallelAxes.ts';
|
|
14
|
+
import type { ParallelRange } from './parallelTypes.ts';
|
|
15
|
+
|
|
16
|
+
/** A band kept on one axis, in pixels from the top of the drawing area. */
|
|
17
|
+
export interface ParallelBand {
|
|
18
|
+
/** The upper edge, which is the higher value. */
|
|
19
|
+
top: number;
|
|
20
|
+
/** The lower edge, which is the lower value. */
|
|
21
|
+
bottom: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Which part of an axis a press landed on. */
|
|
25
|
+
export type ParallelBrushGrip = 'create' | 'move' | 'top' | 'bottom';
|
|
26
|
+
|
|
27
|
+
/** A drag in progress on one axis. */
|
|
28
|
+
export interface ParallelBrushDrag {
|
|
29
|
+
/** The axis being brushed, by its id. */
|
|
30
|
+
axis: string;
|
|
31
|
+
/** What the press took hold of. */
|
|
32
|
+
grip: ParallelBrushGrip;
|
|
33
|
+
/** Where the press landed, in pixels from the top of the drawing area. */
|
|
34
|
+
originY: number;
|
|
35
|
+
/** The band as it stood when the press landed. */
|
|
36
|
+
origin: ParallelBand;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** How close to an edge a press has to land to take hold of it, in pixels. */
|
|
40
|
+
export const PARALLEL_BRUSH_HANDLE_REACH = 4;
|
|
41
|
+
|
|
42
|
+
/** How tall a released band has to be to count as a band rather than a click. */
|
|
43
|
+
export const PARALLEL_BRUSH_SMALLEST = 1;
|
|
44
|
+
|
|
45
|
+
/** How far either side of an axis a press still reaches it, in pixels. */
|
|
46
|
+
export const PARALLEL_BRUSH_HALF_WIDTH = 9;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* What a press at that height takes hold of.
|
|
50
|
+
* @param y - Where the press landed, in pixels from the top of the drawing area.
|
|
51
|
+
* @param band - The band already on that axis, or `null` when it carries none.
|
|
52
|
+
* @returns The grip. A press within {@link PARALLEL_BRUSH_HANDLE_REACH} of an
|
|
53
|
+
* edge takes that edge, a press inside the band moves it, and anything else —
|
|
54
|
+
* including a press on a bare axis — starts a new one.
|
|
55
|
+
*/
|
|
56
|
+
export function parallelBrushGrip(
|
|
57
|
+
y: number,
|
|
58
|
+
band: ParallelBand | null,
|
|
59
|
+
): ParallelBrushGrip {
|
|
60
|
+
if (band === null) return 'create';
|
|
61
|
+
if (Math.abs(y - band.top) <= PARALLEL_BRUSH_HANDLE_REACH) return 'top';
|
|
62
|
+
if (Math.abs(y - band.bottom) <= PARALLEL_BRUSH_HANDLE_REACH) return 'bottom';
|
|
63
|
+
if (y > band.top && y < band.bottom) return 'move';
|
|
64
|
+
return 'create';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The band a drag stands at, now that the pointer is there.
|
|
69
|
+
* @param drag - The drag, as it began.
|
|
70
|
+
* @param y - Where the pointer is, in pixels from the top of the drawing area.
|
|
71
|
+
* @param height - Height of the drawing area, in pixels.
|
|
72
|
+
* @returns The band, held inside the drawing area at both ends. Dragging one
|
|
73
|
+
* edge past the other swaps them rather than inverting the band, and a band
|
|
74
|
+
* being moved keeps its height rather than being squashed against an end.
|
|
75
|
+
*/
|
|
76
|
+
export function parallelBandAt(
|
|
77
|
+
drag: ParallelBrushDrag,
|
|
78
|
+
y: number,
|
|
79
|
+
height: number,
|
|
80
|
+
): ParallelBand {
|
|
81
|
+
const to = clamp(y, 0, height);
|
|
82
|
+
if (drag.grip === 'create') return order(drag.originY, to);
|
|
83
|
+
if (drag.grip === 'top') return order(to, drag.origin.bottom);
|
|
84
|
+
if (drag.grip === 'bottom') return order(drag.origin.top, to);
|
|
85
|
+
const shift = clamp(
|
|
86
|
+
to - drag.originY,
|
|
87
|
+
-drag.origin.top,
|
|
88
|
+
height - drag.origin.bottom,
|
|
89
|
+
);
|
|
90
|
+
return {
|
|
91
|
+
top: drag.origin.top + shift,
|
|
92
|
+
bottom: drag.origin.bottom + shift,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Whether a released band keeps anything, or was a click that clears the axis.
|
|
98
|
+
* @param band - The band, as the pointer left it.
|
|
99
|
+
* @returns Whether it is tall enough to be an interval.
|
|
100
|
+
*/
|
|
101
|
+
export function parallelIsBand(band: ParallelBand): boolean {
|
|
102
|
+
return band.bottom - band.top >= PARALLEL_BRUSH_SMALLEST;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Where an interval sits on its axis, in pixels.
|
|
107
|
+
* @param range - The interval, low value first.
|
|
108
|
+
* @param axis - The axis it belongs to.
|
|
109
|
+
* @returns The band.
|
|
110
|
+
*/
|
|
111
|
+
export function parallelBandOf(
|
|
112
|
+
range: ParallelRange,
|
|
113
|
+
axis: ParallelAxisLayout,
|
|
114
|
+
): ParallelBand {
|
|
115
|
+
return order(
|
|
116
|
+
parallelValueToY(range[1], axis),
|
|
117
|
+
parallelValueToY(range[0], axis),
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* What interval a band keeps, in the axis's own units.
|
|
123
|
+
* @param band - The band, in pixels from the top of the drawing area.
|
|
124
|
+
* @param axis - The axis it belongs to.
|
|
125
|
+
* @returns The interval, low value first — which is the order every filter in
|
|
126
|
+
* the family reads, and getting it backwards keeps nothing at all.
|
|
127
|
+
*/
|
|
128
|
+
export function parallelRangeOf(
|
|
129
|
+
band: ParallelBand,
|
|
130
|
+
axis: ParallelAxisLayout,
|
|
131
|
+
): ParallelRange {
|
|
132
|
+
const low = parallelYToValue(band.bottom, axis);
|
|
133
|
+
const high = parallelYToValue(band.top, axis);
|
|
134
|
+
return low <= high ? [low, high] : [high, low];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function order(a: number, b: number): ParallelBand {
|
|
138
|
+
return a <= b ? { top: a, bottom: b } : { top: b, bottom: a };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function clamp(value: number, low: number, high: number): number {
|
|
142
|
+
if (value < low) return low;
|
|
143
|
+
if (value > high) return high;
|
|
144
|
+
return value;
|
|
145
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The two ends of one axis, read off its own column.
|
|
3
|
+
*
|
|
4
|
+
* Deliberately not `chartPadExtent`: a parallel-coordinates axis ends exactly
|
|
5
|
+
* on the data, because the reader compares rows against each other rather than
|
|
6
|
+
* against a round number, and the best row has to touch the top of its axis.
|
|
7
|
+
* The only opening is for a column with no spread at all, which would
|
|
8
|
+
* otherwise draw no axis and place every row on one pixel.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** How far a column with no spread at all is opened, on each side. */
|
|
12
|
+
export const PARALLEL_DEGENERATE_PADDING = 0.5;
|
|
13
|
+
|
|
14
|
+
/** The two ends of an axis, low first. */
|
|
15
|
+
export interface ParallelExtent {
|
|
16
|
+
/** The value at the bottom of the axis. */
|
|
17
|
+
min: number;
|
|
18
|
+
/** The value at its top. */
|
|
19
|
+
max: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The smallest and the largest finite value of one column.
|
|
24
|
+
* @param values - The column, read in place.
|
|
25
|
+
* @param count - How many rows to read, from the start.
|
|
26
|
+
* @returns The extent. A column holding no finite value at all comes back as
|
|
27
|
+
* half a unit either side of zero, and a constant one as half a unit either
|
|
28
|
+
* side of its value, so that both still draw an axis.
|
|
29
|
+
*/
|
|
30
|
+
export function parallelExtent(
|
|
31
|
+
values: ArrayLike<number>,
|
|
32
|
+
count: number,
|
|
33
|
+
): ParallelExtent {
|
|
34
|
+
const rows = Math.min(count, values.length);
|
|
35
|
+
let min = Number.POSITIVE_INFINITY;
|
|
36
|
+
let max = Number.NEGATIVE_INFINITY;
|
|
37
|
+
for (let row = 0; row < rows; row++) {
|
|
38
|
+
const value = values[row] as number;
|
|
39
|
+
if (!Number.isFinite(value)) continue;
|
|
40
|
+
if (value < min) min = value;
|
|
41
|
+
if (value > max) max = value;
|
|
42
|
+
}
|
|
43
|
+
if (min > max) {
|
|
44
|
+
return {
|
|
45
|
+
min: -PARALLEL_DEGENERATE_PADDING,
|
|
46
|
+
max: PARALLEL_DEGENERATE_PADDING,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
if (min === max) {
|
|
50
|
+
return {
|
|
51
|
+
min: min - PARALLEL_DEGENERATE_PADDING,
|
|
52
|
+
max: max + PARALLEL_DEGENERATE_PADDING,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
return { min, max };
|
|
56
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which rows the brushes keep.
|
|
3
|
+
*
|
|
4
|
+
* Exported rather than kept inside the figure, because a table beside the
|
|
5
|
+
* figure has to filter on the same answer. Two definitions of "kept" is the
|
|
6
|
+
* fault this file exists to prevent: a table showing fourteen rows under a
|
|
7
|
+
* plot drawing one is not a rounding error, it is two different programs.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type {
|
|
11
|
+
ParallelAxis,
|
|
12
|
+
ParallelRange,
|
|
13
|
+
ParallelRanges,
|
|
14
|
+
} from './parallelTypes.ts';
|
|
15
|
+
|
|
16
|
+
/** One axis's interval, ready to be tested row by row. */
|
|
17
|
+
interface ActiveRange {
|
|
18
|
+
/** The column being tested. */
|
|
19
|
+
values: ArrayLike<number>;
|
|
20
|
+
/** The lowest value kept. */
|
|
21
|
+
minimum: number;
|
|
22
|
+
/** The highest value kept. */
|
|
23
|
+
maximum: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Whether one value falls inside an interval.
|
|
28
|
+
* @param range - The interval, low value first.
|
|
29
|
+
* @param value - The value to test.
|
|
30
|
+
* @returns Whether it is kept. Both ends are inclusive, and a value that is
|
|
31
|
+
* not a number is kept by no interval at all — a row whose property has not
|
|
32
|
+
* been worked out yet cannot be said to be in range.
|
|
33
|
+
*/
|
|
34
|
+
export function parallelRangeKeeps(
|
|
35
|
+
range: ParallelRange,
|
|
36
|
+
value: number,
|
|
37
|
+
): boolean {
|
|
38
|
+
return value >= range[0] && value <= range[1];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Which rows every brush keeps at once.
|
|
43
|
+
* @param axes - The axes, whose ids the intervals are keyed by.
|
|
44
|
+
* @param ranges - The interval each axis keeps, or `null` for none.
|
|
45
|
+
* @param count - How many rows there are.
|
|
46
|
+
* @returns One byte per row, `1` for kept. An interval keyed on an axis that
|
|
47
|
+
* is not drawn is ignored rather than emptying the figure, so a range left
|
|
48
|
+
* over from a column the reader has since hidden does not silently filter
|
|
49
|
+
* everything away.
|
|
50
|
+
*/
|
|
51
|
+
export function parallelIncludedMask(
|
|
52
|
+
axes: readonly ParallelAxis[],
|
|
53
|
+
ranges: ParallelRanges,
|
|
54
|
+
count: number,
|
|
55
|
+
): Uint8Array {
|
|
56
|
+
const included = new Uint8Array(count).fill(1);
|
|
57
|
+
const active: ActiveRange[] = [];
|
|
58
|
+
for (const axis of axes) {
|
|
59
|
+
const range = ranges[axis.id];
|
|
60
|
+
if (range === null || range === undefined) continue;
|
|
61
|
+
active.push({
|
|
62
|
+
values: axis.values,
|
|
63
|
+
minimum: Math.min(range[0], range[1]),
|
|
64
|
+
maximum: Math.max(range[0], range[1]),
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
if (active.length === 0) return included;
|
|
68
|
+
|
|
69
|
+
for (const range of active) {
|
|
70
|
+
const rows = Math.min(count, range.values.length);
|
|
71
|
+
for (let row = 0; row < rows; row++) {
|
|
72
|
+
if (included[row] === 0) continue;
|
|
73
|
+
const value = range.values[row] as number;
|
|
74
|
+
if (value >= range.minimum && value <= range.maximum) continue;
|
|
75
|
+
included[row] = 0;
|
|
76
|
+
}
|
|
77
|
+
// A row the figure has no value for on a brushed axis is not in range.
|
|
78
|
+
for (let row = rows; row < count; row++) included[row] = 0;
|
|
79
|
+
}
|
|
80
|
+
return included;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* How many rows a mask keeps.
|
|
85
|
+
* @param included - One byte per row, `1` for kept.
|
|
86
|
+
* @returns The count.
|
|
87
|
+
*/
|
|
88
|
+
export function parallelKeptCount(included: Uint8Array): number {
|
|
89
|
+
let kept = 0;
|
|
90
|
+
for (const row of included) {
|
|
91
|
+
if (row !== 0) kept++;
|
|
92
|
+
}
|
|
93
|
+
return kept;
|
|
94
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which row the pointer is over.
|
|
3
|
+
*
|
|
4
|
+
* Only the segment between the two axes the pointer sits between is tested, so
|
|
5
|
+
* the cost is one distance per row whatever the number of axes. The vertical
|
|
6
|
+
* gap to the segment is projected onto its normal, so a steep line is no
|
|
7
|
+
* harder to pick than a flat one — without which the only rows a reader can
|
|
8
|
+
* ever point at are the uninteresting ones.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { ParallelAxisLayout } from './parallelAxes.ts';
|
|
12
|
+
import { parallelPixelAt } from './parallelAxes.ts';
|
|
13
|
+
|
|
14
|
+
/** How far the pointer may sit from a line and still pick it, in pixels. */
|
|
15
|
+
export const PARALLEL_HOVER_TOLERANCE = 6;
|
|
16
|
+
|
|
17
|
+
/** A pointer position, relative to the top left of the drawing area. */
|
|
18
|
+
export interface ParallelPoint {
|
|
19
|
+
/** Pixels from the left of the drawing area. */
|
|
20
|
+
x: number;
|
|
21
|
+
/** Pixels from its top. */
|
|
22
|
+
y: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** The pair of axes a pointer sits between, as indices into the layouts. */
|
|
26
|
+
export interface ParallelSegment {
|
|
27
|
+
/** The axis to the left of the pointer. */
|
|
28
|
+
left: number;
|
|
29
|
+
/** The axis to its right. */
|
|
30
|
+
right: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The pair of axes a horizontal position sits between.
|
|
35
|
+
* @param x - Pixels from the left of the drawing area.
|
|
36
|
+
* @param axes - The axes, from left to right.
|
|
37
|
+
* @param tolerance - How far past the first and the last axis still counts. Defaults to {@link PARALLEL_HOVER_TOLERANCE}.
|
|
38
|
+
* @returns The pair, or `null` when there are fewer than two axes or the
|
|
39
|
+
* position is past either end of the figure.
|
|
40
|
+
*/
|
|
41
|
+
export function parallelSegmentAt(
|
|
42
|
+
x: number,
|
|
43
|
+
axes: readonly ParallelAxisLayout[],
|
|
44
|
+
tolerance: number = PARALLEL_HOVER_TOLERANCE,
|
|
45
|
+
): ParallelSegment | null {
|
|
46
|
+
const first = axes[0];
|
|
47
|
+
const last = axes.at(-1);
|
|
48
|
+
if (first === undefined || last === undefined || axes.length < 2) return null;
|
|
49
|
+
if (x < first.x - tolerance || x > last.x + tolerance) return null;
|
|
50
|
+
for (let index = 1; index < axes.length; index++) {
|
|
51
|
+
const right = axes[index];
|
|
52
|
+
if (right === undefined) continue;
|
|
53
|
+
if (x <= right.x || index === axes.length - 1) {
|
|
54
|
+
return { left: index - 1, right: index };
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The row whose line runs closest to the pointer.
|
|
62
|
+
* @param point - Where the pointer is, relative to the drawing area.
|
|
63
|
+
* @param axes - The axes, from left to right.
|
|
64
|
+
* @param values - Each axis's column, in the same order.
|
|
65
|
+
* @param count - How many rows are drawn.
|
|
66
|
+
* @param included - Which rows a brush keeps, a zero meaning the row is not pickable. Defaults to every row.
|
|
67
|
+
* @param tolerance - How far the pointer may sit from a line. Defaults to {@link PARALLEL_HOVER_TOLERANCE}.
|
|
68
|
+
* @returns The row, or `-1` when the pointer is over no line.
|
|
69
|
+
*/
|
|
70
|
+
export function parallelNearestRow(
|
|
71
|
+
point: ParallelPoint,
|
|
72
|
+
axes: readonly ParallelAxisLayout[],
|
|
73
|
+
values: ReadonlyArray<ArrayLike<number>>,
|
|
74
|
+
count: number,
|
|
75
|
+
included?: Uint8Array,
|
|
76
|
+
tolerance: number = PARALLEL_HOVER_TOLERANCE,
|
|
77
|
+
): number {
|
|
78
|
+
const segment = parallelSegmentAt(point.x, axes, tolerance);
|
|
79
|
+
if (segment === null) return -1;
|
|
80
|
+
const left = axes[segment.left];
|
|
81
|
+
const right = axes[segment.right];
|
|
82
|
+
const leftColumn = values[segment.left];
|
|
83
|
+
const rightColumn = values[segment.right];
|
|
84
|
+
if (left === undefined || right === undefined) return -1;
|
|
85
|
+
if (leftColumn === undefined || rightColumn === undefined) return -1;
|
|
86
|
+
|
|
87
|
+
const width = right.x - left.x;
|
|
88
|
+
if (width <= 0) return -1;
|
|
89
|
+
const ratio = (point.x - left.x) / width;
|
|
90
|
+
const rows = Math.min(count, leftColumn.length, rightColumn.length);
|
|
91
|
+
|
|
92
|
+
let nearest = -1;
|
|
93
|
+
let nearestDistance = tolerance;
|
|
94
|
+
for (let row = 0; row < rows; row++) {
|
|
95
|
+
if (included?.[row] === 0) continue;
|
|
96
|
+
const leftY = parallelPixelAt(left.pixels, leftColumn[row] as number);
|
|
97
|
+
const rightY = parallelPixelAt(right.pixels, rightColumn[row] as number);
|
|
98
|
+
const gap = Math.abs(leftY + (rightY - leftY) * ratio - point.y);
|
|
99
|
+
const distance = (gap * width) / Math.hypot(width, rightY - leftY);
|
|
100
|
+
if (distance < nearestDistance) {
|
|
101
|
+
nearestDistance = distance;
|
|
102
|
+
nearest = row;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return nearest;
|
|
106
|
+
}
|