react-cheminfo 0.15.0 → 0.17.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 +29 -0
- package/lib/parallel/core/index.d.ts.map +1 -0
- package/lib/parallel/core/index.js +22 -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 +100 -0
- package/lib/parallel/core/parallelBrush.d.ts.map +1 -0
- package/lib/parallel/core/parallelBrush.js +149 -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 +49 -0
- package/lib/parallel/core/parallelFilter.d.ts.map +1 -0
- package/lib/parallel/core/parallelFilter.js +107 -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/parallelOrder.d.ts +36 -0
- package/lib/parallel/core/parallelOrder.d.ts.map +1 -0
- package/lib/parallel/core/parallelOrder.js +65 -0
- package/lib/parallel/core/parallelOrder.js.map +1 -0
- package/lib/parallel/core/parallelPaint.d.ts +108 -0
- package/lib/parallel/core/parallelPaint.d.ts.map +1 -0
- package/lib/parallel/core/parallelPaint.js +169 -0
- package/lib/parallel/core/parallelPaint.js.map +1 -0
- package/lib/parallel/core/parallelPalette.d.ts +40 -0
- package/lib/parallel/core/parallelPalette.d.ts.map +1 -0
- package/lib/parallel/core/parallelPalette.js +81 -0
- package/lib/parallel/core/parallelPalette.js.map +1 -0
- package/lib/parallel/core/parallelSelection.d.ts +43 -0
- package/lib/parallel/core/parallelSelection.d.ts.map +1 -0
- package/lib/parallel/core/parallelSelection.js +87 -0
- package/lib/parallel/core/parallelSelection.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 +119 -0
- package/lib/parallel/core/parallelTicks.js.map +1 -0
- package/lib/parallel/core/parallelTypes.d.ts +140 -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 +114 -0
- package/lib/parallel/ui/ParallelCoordinates.js.map +1 -0
- package/lib/parallel/ui/ParallelLabels.d.ts +32 -0
- package/lib/parallel/ui/ParallelLabels.d.ts.map +1 -0
- package/lib/parallel/ui/ParallelLabels.js +37 -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 +29 -0
- package/lib/parallel/ui/index.d.ts.map +1 -0
- package/lib/parallel/ui/index.js +20 -0
- package/lib/parallel/ui/index.js.map +1 -0
- package/lib/parallel/ui/parallelCoordinatesProps.d.ts +203 -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 +83 -0
- package/lib/parallel/ui/parallelGestureModel.d.ts.map +1 -0
- package/lib/parallel/ui/parallelGestureModel.js +110 -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 +106 -0
- package/lib/parallel/ui/parallelStyles.d.ts.map +1 -0
- package/lib/parallel/ui/parallelStyles.js +153 -0
- package/lib/parallel/ui/parallelStyles.js.map +1 -0
- package/lib/parallel/ui/useParallelAxisDrag.d.ts +60 -0
- package/lib/parallel/ui/useParallelAxisDrag.d.ts.map +1 -0
- package/lib/parallel/ui/useParallelAxisDrag.js +96 -0
- package/lib/parallel/ui/useParallelAxisDrag.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 +72 -0
- package/lib/parallel/ui/useParallelGesture.d.ts.map +1 -0
- package/lib/parallel/ui/useParallelGesture.js +164 -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 +96 -0
- package/src/parallel/core/parallelAxes.ts +207 -0
- package/src/parallel/core/parallelBrush.ts +211 -0
- package/src/parallel/core/parallelExtent.ts +56 -0
- package/src/parallel/core/parallelFilter.ts +131 -0
- package/src/parallel/core/parallelHit.ts +106 -0
- package/src/parallel/core/parallelOrder.ts +73 -0
- package/src/parallel/core/parallelPaint.ts +242 -0
- package/src/parallel/core/parallelPalette.ts +93 -0
- package/src/parallel/core/parallelSelection.ts +99 -0
- package/src/parallel/core/parallelTicks.ts +168 -0
- package/src/parallel/core/parallelTypes.ts +159 -0
- package/src/parallel/ui/ParallelAxesLayer.tsx +129 -0
- package/src/parallel/ui/ParallelCoordinates.tsx +252 -0
- package/src/parallel/ui/ParallelLabels.tsx +77 -0
- package/src/parallel/ui/ParallelTooltip.tsx +55 -0
- package/src/parallel/ui/index.ts +65 -0
- package/src/parallel/ui/parallelCoordinatesProps.ts +217 -0
- package/src/parallel/ui/parallelGestureModel.ts +159 -0
- package/src/parallel/ui/parallelInk.ts +182 -0
- package/src/parallel/ui/parallelPlotModel.ts +130 -0
- package/src/parallel/ui/parallelStyles.ts +188 -0
- package/src/parallel/ui/useParallelAxisDrag.ts +147 -0
- package/src/parallel/ui/useParallelCanvas.ts +104 -0
- package/src/parallel/ui/useParallelGesture.ts +260 -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,211 @@
|
|
|
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
|
+
/** Which of that axis's intervals is being written, or `-1` for a new one. */
|
|
32
|
+
index: number;
|
|
33
|
+
/** What the press took hold of. */
|
|
34
|
+
grip: ParallelBrushGrip;
|
|
35
|
+
/** Where the press landed, in pixels from the top of the drawing area. */
|
|
36
|
+
originY: number;
|
|
37
|
+
/** The band as it stood when the press landed. */
|
|
38
|
+
origin: ParallelBand;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** How close to an edge a press has to land to take hold of it, in pixels. */
|
|
42
|
+
export const PARALLEL_BRUSH_HANDLE_REACH = 4;
|
|
43
|
+
|
|
44
|
+
/** How tall a released band has to be to count as a band rather than a click. */
|
|
45
|
+
export const PARALLEL_BRUSH_SMALLEST = 1;
|
|
46
|
+
|
|
47
|
+
/** How far either side of an axis a press still reaches it, in pixels. */
|
|
48
|
+
export const PARALLEL_BRUSH_HALF_WIDTH = 9;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* How close to an axis's own end a band edge counts as standing on it.
|
|
52
|
+
*
|
|
53
|
+
* Half a pixel is below anything a pointer can aim at, and the two ends are
|
|
54
|
+
* where the rows a reader most wants sit: the axes end exactly on the data,
|
|
55
|
+
* so the extreme row is drawn on the end itself.
|
|
56
|
+
*/
|
|
57
|
+
const END_REACH = 0.5;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* What a press at that height takes hold of.
|
|
61
|
+
* @param y - Where the press landed, in pixels from the top of the drawing area.
|
|
62
|
+
* @param band - The band already on that axis, or `null` when it carries none.
|
|
63
|
+
* @returns The grip. A press within {@link PARALLEL_BRUSH_HANDLE_REACH} of an
|
|
64
|
+
* edge takes that edge, a press inside the band moves it, and anything else —
|
|
65
|
+
* including a press on a bare axis — starts a new one.
|
|
66
|
+
*/
|
|
67
|
+
export function parallelBrushGrip(
|
|
68
|
+
y: number,
|
|
69
|
+
band: ParallelBand | null,
|
|
70
|
+
): ParallelBrushGrip {
|
|
71
|
+
if (band === null) return 'create';
|
|
72
|
+
if (Math.abs(y - band.top) <= PARALLEL_BRUSH_HANDLE_REACH) return 'top';
|
|
73
|
+
if (Math.abs(y - band.bottom) <= PARALLEL_BRUSH_HANDLE_REACH) return 'bottom';
|
|
74
|
+
if (y > band.top && y < band.bottom) return 'move';
|
|
75
|
+
return 'create';
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Which of an axis's bands a press took hold of, and by what. */
|
|
79
|
+
export interface ParallelBrushTarget {
|
|
80
|
+
/** The band, as an index into the list, or `-1` when none was under it. */
|
|
81
|
+
index: number;
|
|
82
|
+
/** What the press took hold of. */
|
|
83
|
+
grip: ParallelBrushGrip;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Nothing under the press, so it starts a band of its own. */
|
|
87
|
+
const NEW_BAND: ParallelBrushTarget = { index: -1, grip: 'create' };
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Which band a press at that height took hold of, on an axis carrying several.
|
|
91
|
+
* @param y - Where the press landed, in pixels from the top of the drawing area.
|
|
92
|
+
* @param bands - The bands on that axis, in any order.
|
|
93
|
+
* @returns The band and the grip. An edge wins over the band it belongs to and
|
|
94
|
+
* over any band it lies inside, so two bands dragged flush against each other
|
|
95
|
+
* can still be pulled apart; a press on bare axis starts a new band.
|
|
96
|
+
*/
|
|
97
|
+
export function parallelBrushTarget(
|
|
98
|
+
y: number,
|
|
99
|
+
bands: readonly ParallelBand[],
|
|
100
|
+
): ParallelBrushTarget {
|
|
101
|
+
let nearest = NEW_BAND;
|
|
102
|
+
let nearestDistance = PARALLEL_BRUSH_HANDLE_REACH;
|
|
103
|
+
for (let index = 0; index < bands.length; index++) {
|
|
104
|
+
const band = bands[index] as ParallelBand;
|
|
105
|
+
const toTop = Math.abs(y - band.top);
|
|
106
|
+
if (toTop <= nearestDistance) {
|
|
107
|
+
nearestDistance = toTop;
|
|
108
|
+
nearest = { index, grip: 'top' };
|
|
109
|
+
}
|
|
110
|
+
const toBottom = Math.abs(y - band.bottom);
|
|
111
|
+
if (toBottom <= nearestDistance) {
|
|
112
|
+
nearestDistance = toBottom;
|
|
113
|
+
nearest = { index, grip: 'bottom' };
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (nearest !== NEW_BAND) return nearest;
|
|
117
|
+
for (let index = 0; index < bands.length; index++) {
|
|
118
|
+
const band = bands[index] as ParallelBand;
|
|
119
|
+
if (y > band.top && y < band.bottom) return { index, grip: 'move' };
|
|
120
|
+
}
|
|
121
|
+
return NEW_BAND;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* The band a drag stands at, now that the pointer is there.
|
|
126
|
+
* @param drag - The drag, as it began.
|
|
127
|
+
* @param y - Where the pointer is, in pixels from the top of the drawing area.
|
|
128
|
+
* @param height - Height of the drawing area, in pixels.
|
|
129
|
+
* @returns The band, held inside the drawing area at both ends. Dragging one
|
|
130
|
+
* edge past the other swaps them rather than inverting the band, and a band
|
|
131
|
+
* being moved keeps its height rather than being squashed against an end.
|
|
132
|
+
*/
|
|
133
|
+
export function parallelBandAt(
|
|
134
|
+
drag: ParallelBrushDrag,
|
|
135
|
+
y: number,
|
|
136
|
+
height: number,
|
|
137
|
+
): ParallelBand {
|
|
138
|
+
const to = clamp(y, 0, height);
|
|
139
|
+
if (drag.grip === 'create') return order(drag.originY, to);
|
|
140
|
+
if (drag.grip === 'top') return order(to, drag.origin.bottom);
|
|
141
|
+
if (drag.grip === 'bottom') return order(drag.origin.top, to);
|
|
142
|
+
const shift = clamp(
|
|
143
|
+
to - drag.originY,
|
|
144
|
+
-drag.origin.top,
|
|
145
|
+
height - drag.origin.bottom,
|
|
146
|
+
);
|
|
147
|
+
return {
|
|
148
|
+
top: drag.origin.top + shift,
|
|
149
|
+
bottom: drag.origin.bottom + shift,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Whether a released band keeps anything, or was a click that clears the axis.
|
|
155
|
+
* @param band - The band, as the pointer left it.
|
|
156
|
+
* @returns Whether it is tall enough to be an interval.
|
|
157
|
+
*/
|
|
158
|
+
export function parallelIsBand(band: ParallelBand): boolean {
|
|
159
|
+
return band.bottom - band.top >= PARALLEL_BRUSH_SMALLEST;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Where an interval sits on its axis, in pixels.
|
|
164
|
+
* @param range - The interval, low value first.
|
|
165
|
+
* @param axis - The axis it belongs to.
|
|
166
|
+
* @returns The band.
|
|
167
|
+
*/
|
|
168
|
+
export function parallelBandOf(
|
|
169
|
+
range: ParallelRange,
|
|
170
|
+
axis: ParallelAxisLayout,
|
|
171
|
+
): ParallelBand {
|
|
172
|
+
return order(
|
|
173
|
+
parallelValueToY(range[1], axis),
|
|
174
|
+
parallelValueToY(range[0], axis),
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* What interval a band keeps, in the axis's own units.
|
|
180
|
+
* @param band - The band, in pixels from the top of the drawing area.
|
|
181
|
+
* @param axis - The axis it belongs to.
|
|
182
|
+
* @returns The interval, low value first — which is the order every filter in
|
|
183
|
+
* the family reads, and getting it backwards keeps nothing at all. An edge
|
|
184
|
+
* standing on an end of the axis reports that end verbatim: inverting the
|
|
185
|
+
* placement is not exact, and one ulp is enough to drop the row the axis ends
|
|
186
|
+
* on from its own full-range brush.
|
|
187
|
+
*/
|
|
188
|
+
export function parallelRangeOf(
|
|
189
|
+
band: ParallelBand,
|
|
190
|
+
axis: ParallelAxisLayout,
|
|
191
|
+
): ParallelRange {
|
|
192
|
+
const low =
|
|
193
|
+
band.bottom >= parallelValueToY(axis.min, axis) - END_REACH
|
|
194
|
+
? axis.min
|
|
195
|
+
: parallelYToValue(band.bottom, axis);
|
|
196
|
+
const high =
|
|
197
|
+
band.top <= parallelValueToY(axis.max, axis) + END_REACH
|
|
198
|
+
? axis.max
|
|
199
|
+
: parallelYToValue(band.top, axis);
|
|
200
|
+
return low <= high ? [low, high] : [high, low];
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function order(a: number, b: number): ParallelBand {
|
|
204
|
+
return a <= b ? { top: a, bottom: b } : { top: b, bottom: a };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function clamp(value: number, low: number, high: number): number {
|
|
208
|
+
if (value < low) return low;
|
|
209
|
+
if (value > high) return high;
|
|
210
|
+
return value;
|
|
211
|
+
}
|
|
@@ -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,131 @@
|
|
|
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
|
+
* An axis carrying several intervals keeps a row that falls in any of them,
|
|
10
|
+
* while a row has to be kept by every brushed axis — union down an axis,
|
|
11
|
+
* intersection across them, which is the only reading under which brushing a
|
|
12
|
+
* second axis can never widen what the first one left.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { parallelRangeList } from './parallelSelection.ts';
|
|
16
|
+
import type {
|
|
17
|
+
ParallelAxis,
|
|
18
|
+
ParallelRange,
|
|
19
|
+
ParallelRanges,
|
|
20
|
+
} from './parallelTypes.ts';
|
|
21
|
+
|
|
22
|
+
/** One axis's intervals, ready to be tested row by row. */
|
|
23
|
+
interface ActiveRange {
|
|
24
|
+
/** The column being tested. */
|
|
25
|
+
values: ArrayLike<number>;
|
|
26
|
+
/** The lowest value each interval keeps. */
|
|
27
|
+
minimum: Float64Array;
|
|
28
|
+
/** The highest value each interval keeps. */
|
|
29
|
+
maximum: Float64Array;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Whether one value falls inside an interval.
|
|
34
|
+
* @param range - The interval, low value first.
|
|
35
|
+
* @param value - The value to test.
|
|
36
|
+
* @returns Whether it is kept. Both ends are inclusive, and a value that is
|
|
37
|
+
* not a number is kept by no interval at all — a row whose property has not
|
|
38
|
+
* been worked out yet cannot be said to be in range.
|
|
39
|
+
*/
|
|
40
|
+
export function parallelRangeKeeps(
|
|
41
|
+
range: ParallelRange,
|
|
42
|
+
value: number,
|
|
43
|
+
): boolean {
|
|
44
|
+
return value >= range[0] && value <= range[1];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Whether one value falls inside any of an axis's intervals.
|
|
49
|
+
* @param ranges - The intervals that axis keeps.
|
|
50
|
+
* @param value - The value to test.
|
|
51
|
+
* @returns Whether any of them keeps it. An axis keeping nothing keeps every
|
|
52
|
+
* value, which is what an unbrushed axis means.
|
|
53
|
+
*/
|
|
54
|
+
export function parallelSelectionKeeps(
|
|
55
|
+
ranges: readonly ParallelRange[],
|
|
56
|
+
value: number,
|
|
57
|
+
): boolean {
|
|
58
|
+
if (ranges.length === 0) return true;
|
|
59
|
+
for (const range of ranges) {
|
|
60
|
+
if (value >= range[0] && value <= range[1]) return true;
|
|
61
|
+
}
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Which rows every brush keeps at once.
|
|
67
|
+
* @param axes - The axes, whose ids the intervals are keyed by.
|
|
68
|
+
* @param ranges - What each axis keeps: one interval, several, or none.
|
|
69
|
+
* @param count - How many rows there are.
|
|
70
|
+
* @returns One byte per row, `1` for kept. An interval keyed on an axis that
|
|
71
|
+
* is not drawn is ignored rather than emptying the figure, so a range left
|
|
72
|
+
* over from a column the reader has since hidden does not silently filter
|
|
73
|
+
* everything away.
|
|
74
|
+
*/
|
|
75
|
+
export function parallelIncludedMask(
|
|
76
|
+
axes: readonly ParallelAxis[],
|
|
77
|
+
ranges: ParallelRanges,
|
|
78
|
+
count: number,
|
|
79
|
+
): Uint8Array {
|
|
80
|
+
const included = new Uint8Array(count).fill(1);
|
|
81
|
+
const active: ActiveRange[] = [];
|
|
82
|
+
for (const axis of axes) {
|
|
83
|
+
const list = parallelRangeList(ranges[axis.id]);
|
|
84
|
+
if (list.length === 0) continue;
|
|
85
|
+
const minimum = new Float64Array(list.length);
|
|
86
|
+
const maximum = new Float64Array(list.length);
|
|
87
|
+
for (let index = 0; index < list.length; index++) {
|
|
88
|
+
const range = list[index] as ParallelRange;
|
|
89
|
+
minimum[index] = Math.min(range[0], range[1]);
|
|
90
|
+
maximum[index] = Math.max(range[0], range[1]);
|
|
91
|
+
}
|
|
92
|
+
active.push({ values: axis.values, minimum, maximum });
|
|
93
|
+
}
|
|
94
|
+
if (active.length === 0) return included;
|
|
95
|
+
|
|
96
|
+
for (const range of active) {
|
|
97
|
+
const rows = Math.min(count, range.values.length);
|
|
98
|
+
const intervals = range.minimum.length;
|
|
99
|
+
for (let row = 0; row < rows; row++) {
|
|
100
|
+
if (included[row] === 0) continue;
|
|
101
|
+
const value = range.values[row] as number;
|
|
102
|
+
let kept = false;
|
|
103
|
+
for (let index = 0; index < intervals; index++) {
|
|
104
|
+
if (
|
|
105
|
+
value >= (range.minimum[index] as number) &&
|
|
106
|
+
value <= (range.maximum[index] as number)
|
|
107
|
+
) {
|
|
108
|
+
kept = true;
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (!kept) included[row] = 0;
|
|
113
|
+
}
|
|
114
|
+
// A row the figure has no value for on a brushed axis is not in range.
|
|
115
|
+
for (let row = rows; row < count; row++) included[row] = 0;
|
|
116
|
+
}
|
|
117
|
+
return included;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* How many rows a mask keeps.
|
|
122
|
+
* @param included - One byte per row, `1` for kept.
|
|
123
|
+
* @returns The count.
|
|
124
|
+
*/
|
|
125
|
+
export function parallelKeptCount(included: Uint8Array): number {
|
|
126
|
+
let kept = 0;
|
|
127
|
+
for (const row of included) {
|
|
128
|
+
if (row !== 0) kept++;
|
|
129
|
+
}
|
|
130
|
+
return kept;
|
|
131
|
+
}
|