react-cheminfo 0.16.0 → 0.18.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/lib/parallel/core/index.d.ts +7 -5
- package/lib/parallel/core/index.d.ts.map +1 -1
- package/lib/parallel/core/index.js +5 -3
- package/lib/parallel/core/index.js.map +1 -1
- package/lib/parallel/core/parallelBrush.d.ts +22 -1
- package/lib/parallel/core/parallelBrush.d.ts.map +1 -1
- package/lib/parallel/core/parallelBrush.js +53 -3
- package/lib/parallel/core/parallelBrush.js.map +1 -1
- package/lib/parallel/core/parallelFilter.d.ts +14 -1
- package/lib/parallel/core/parallelFilter.d.ts.map +1 -1
- package/lib/parallel/core/parallelFilter.js +44 -11
- package/lib/parallel/core/parallelFilter.js.map +1 -1
- 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 +4 -0
- package/lib/parallel/core/parallelPaint.d.ts.map +1 -1
- package/lib/parallel/core/parallelPaint.js +15 -6
- package/lib/parallel/core/parallelPaint.js.map +1 -1
- package/lib/parallel/core/parallelPalette.d.ts +10 -1
- package/lib/parallel/core/parallelPalette.d.ts.map +1 -1
- package/lib/parallel/core/parallelPalette.js +14 -1
- package/lib/parallel/core/parallelPalette.js.map +1 -1
- 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.map +1 -1
- package/lib/parallel/core/parallelTicks.js +7 -4
- package/lib/parallel/core/parallelTicks.js.map +1 -1
- package/lib/parallel/core/parallelTypes.d.ts +12 -1
- package/lib/parallel/core/parallelTypes.d.ts.map +1 -1
- package/lib/parallel/core/parallelTypes.js.map +1 -1
- package/lib/parallel/ui/ParallelAxesLayer.d.ts +2 -2
- package/lib/parallel/ui/ParallelAxesLayer.d.ts.map +1 -1
- package/lib/parallel/ui/ParallelAxesLayer.js +2 -2
- package/lib/parallel/ui/ParallelAxesLayer.js.map +1 -1
- package/lib/parallel/ui/ParallelCoordinates.d.ts.map +1 -1
- package/lib/parallel/ui/ParallelCoordinates.js +10 -5
- package/lib/parallel/ui/ParallelCoordinates.js.map +1 -1
- package/lib/parallel/ui/ParallelLabels.d.ts +6 -0
- package/lib/parallel/ui/ParallelLabels.d.ts.map +1 -1
- package/lib/parallel/ui/ParallelLabels.js +14 -2
- package/lib/parallel/ui/ParallelLabels.js.map +1 -1
- package/lib/parallel/ui/index.d.ts +4 -0
- package/lib/parallel/ui/index.d.ts.map +1 -1
- package/lib/parallel/ui/index.js +2 -0
- package/lib/parallel/ui/index.js.map +1 -1
- package/lib/parallel/ui/parallelCoordinatesProps.d.ts +26 -8
- package/lib/parallel/ui/parallelCoordinatesProps.d.ts.map +1 -1
- package/lib/parallel/ui/parallelGestureModel.d.ts +37 -6
- package/lib/parallel/ui/parallelGestureModel.d.ts.map +1 -1
- package/lib/parallel/ui/parallelGestureModel.js +42 -12
- package/lib/parallel/ui/parallelGestureModel.js.map +1 -1
- package/lib/parallel/ui/parallelPlotModel.d.ts +1 -1
- package/lib/parallel/ui/parallelPlotModel.d.ts.map +1 -1
- package/lib/parallel/ui/parallelPlotModel.js +3 -3
- package/lib/parallel/ui/parallelPlotModel.js.map +1 -1
- package/lib/parallel/ui/parallelStyles.d.ts +26 -3
- package/lib/parallel/ui/parallelStyles.d.ts.map +1 -1
- package/lib/parallel/ui/parallelStyles.js +26 -4
- package/lib/parallel/ui/parallelStyles.js.map +1 -1
- 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/useParallelGesture.d.ts +10 -5
- package/lib/parallel/ui/useParallelGesture.d.ts.map +1 -1
- package/lib/parallel/ui/useParallelGesture.js +46 -21
- package/lib/parallel/ui/useParallelGesture.js.map +1 -1
- package/lib/structure/core/editorGuide.d.ts +69 -0
- package/lib/structure/core/editorGuide.d.ts.map +1 -0
- package/lib/structure/core/editorGuide.js +162 -0
- package/lib/structure/core/editorGuide.js.map +1 -0
- package/lib/structure/core/editorToolbar.d.ts +45 -0
- package/lib/structure/core/editorToolbar.d.ts.map +1 -0
- package/lib/structure/core/editorToolbar.js +132 -0
- package/lib/structure/core/editorToolbar.js.map +1 -0
- package/lib/structure/core/editorToolbarGeometry.d.ts +36 -0
- package/lib/structure/core/editorToolbarGeometry.d.ts.map +1 -0
- package/lib/structure/core/editorToolbarGeometry.js +61 -0
- package/lib/structure/core/editorToolbarGeometry.js.map +1 -0
- package/lib/structure/core/index.d.ts +6 -0
- package/lib/structure/core/index.d.ts.map +1 -1
- package/lib/structure/core/index.js +3 -0
- package/lib/structure/core/index.js.map +1 -1
- package/lib/structure/core/pendingCall.d.ts +21 -0
- package/lib/structure/core/pendingCall.d.ts.map +1 -0
- package/lib/structure/core/pendingCall.js +37 -0
- package/lib/structure/core/pendingCall.js.map +1 -0
- package/lib/structure/ui/EditorCanvas.d.ts +1 -16
- package/lib/structure/ui/EditorCanvas.d.ts.map +1 -1
- package/lib/structure/ui/EditorCanvas.js +5 -18
- package/lib/structure/ui/EditorCanvas.js.map +1 -1
- package/lib/structure/ui/EditorHelpButton.d.ts +22 -0
- package/lib/structure/ui/EditorHelpButton.d.ts.map +1 -0
- package/lib/structure/ui/EditorHelpButton.js +38 -0
- package/lib/structure/ui/EditorHelpButton.js.map +1 -0
- package/lib/structure/ui/KeyCaps.d.ts +12 -0
- package/lib/structure/ui/KeyCaps.d.ts.map +1 -0
- package/lib/structure/ui/KeyCaps.js +29 -0
- package/lib/structure/ui/KeyCaps.js.map +1 -0
- package/lib/structure/ui/StructureEditor.d.ts +9 -2
- package/lib/structure/ui/StructureEditor.d.ts.map +1 -1
- package/lib/structure/ui/StructureEditor.js +17 -24
- package/lib/structure/ui/StructureEditor.js.map +1 -1
- package/lib/structure/ui/StructureEditorHelp.d.ts +18 -0
- package/lib/structure/ui/StructureEditorHelp.d.ts.map +1 -0
- package/lib/structure/ui/StructureEditorHelp.js +68 -0
- package/lib/structure/ui/StructureEditorHelp.js.map +1 -0
- package/lib/structure/ui/ToolbarTooltip.d.ts +20 -0
- package/lib/structure/ui/ToolbarTooltip.d.ts.map +1 -0
- package/lib/structure/ui/ToolbarTooltip.js +49 -0
- package/lib/structure/ui/ToolbarTooltip.js.map +1 -0
- package/lib/structure/ui/editorChange.d.ts +52 -0
- package/lib/structure/ui/editorChange.d.ts.map +1 -0
- package/lib/structure/ui/editorChange.js +34 -0
- package/lib/structure/ui/editorChange.js.map +1 -0
- package/lib/structure/ui/index.d.ts +3 -1
- package/lib/structure/ui/index.d.ts.map +1 -1
- package/lib/structure/ui/index.js +1 -0
- package/lib/structure/ui/index.js.map +1 -1
- package/lib/structure/ui/useToolbarFloor.d.ts.map +1 -1
- package/lib/structure/ui/useToolbarFloor.js +6 -45
- package/lib/structure/ui/useToolbarFloor.js.map +1 -1
- package/lib/structure/ui/useToolbarHover.d.ts +28 -0
- package/lib/structure/ui/useToolbarHover.d.ts.map +1 -0
- package/lib/structure/ui/useToolbarHover.js +96 -0
- package/lib/structure/ui/useToolbarHover.js.map +1 -0
- package/lib/structure/ui/watchToolbar.d.ts +17 -0
- package/lib/structure/ui/watchToolbar.d.ts.map +1 -0
- package/lib/structure/ui/watchToolbar.js +56 -0
- package/lib/structure/ui/watchToolbar.js.map +1 -0
- package/package.json +1 -1
- package/src/parallel/core/index.ts +16 -0
- package/src/parallel/core/parallelBrush.ts +69 -3
- package/src/parallel/core/parallelFilter.ts +52 -15
- package/src/parallel/core/parallelOrder.ts +73 -0
- package/src/parallel/core/parallelPaint.ts +16 -5
- package/src/parallel/core/parallelPalette.ts +14 -1
- package/src/parallel/core/parallelSelection.ts +99 -0
- package/src/parallel/core/parallelTicks.ts +7 -3
- package/src/parallel/core/parallelTypes.ts +13 -1
- package/src/parallel/ui/ParallelAxesLayer.tsx +10 -4
- package/src/parallel/ui/ParallelCoordinates.tsx +22 -3
- package/src/parallel/ui/ParallelLabels.tsx +23 -2
- package/src/parallel/ui/index.ts +15 -0
- package/src/parallel/ui/parallelCoordinatesProps.ts +26 -8
- package/src/parallel/ui/parallelGestureModel.ts +63 -13
- package/src/parallel/ui/parallelPlotModel.ts +3 -3
- package/src/parallel/ui/parallelStyles.ts +46 -4
- package/src/parallel/ui/useParallelAxisDrag.ts +147 -0
- package/src/parallel/ui/useParallelGesture.ts +61 -28
- package/src/structure/core/editorGuide.ts +221 -0
- package/src/structure/core/editorToolbar.ts +175 -0
- package/src/structure/core/editorToolbarGeometry.ts +80 -0
- package/src/structure/core/index.ts +29 -0
- package/src/structure/core/pendingCall.ts +59 -0
- package/src/structure/ui/EditorCanvas.tsx +9 -35
- package/src/structure/ui/EditorHelpButton.tsx +70 -0
- package/src/structure/ui/KeyCaps.tsx +40 -0
- package/src/structure/ui/StructureEditor.tsx +39 -21
- package/src/structure/ui/StructureEditorHelp.tsx +136 -0
- package/src/structure/ui/ToolbarTooltip.tsx +95 -0
- package/src/structure/ui/editorChange.ts +81 -0
- package/src/structure/ui/index.ts +5 -1
- package/src/structure/ui/useToolbarFloor.ts +7 -46
- package/src/structure/ui/useToolbarHover.ts +134 -0
- package/src/structure/ui/watchToolbar.ts +59 -0
|
@@ -28,6 +28,8 @@ export type ParallelBrushGrip = 'create' | 'move' | 'top' | 'bottom';
|
|
|
28
28
|
export interface ParallelBrushDrag {
|
|
29
29
|
/** The axis being brushed, by its id. */
|
|
30
30
|
axis: string;
|
|
31
|
+
/** Which of that axis's intervals is being written, or `-1` for a new one. */
|
|
32
|
+
index: number;
|
|
31
33
|
/** What the press took hold of. */
|
|
32
34
|
grip: ParallelBrushGrip;
|
|
33
35
|
/** Where the press landed, in pixels from the top of the drawing area. */
|
|
@@ -45,6 +47,15 @@ export const PARALLEL_BRUSH_SMALLEST = 1;
|
|
|
45
47
|
/** How far either side of an axis a press still reaches it, in pixels. */
|
|
46
48
|
export const PARALLEL_BRUSH_HALF_WIDTH = 9;
|
|
47
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
|
+
|
|
48
59
|
/**
|
|
49
60
|
* What a press at that height takes hold of.
|
|
50
61
|
* @param y - Where the press landed, in pixels from the top of the drawing area.
|
|
@@ -64,6 +75,52 @@ export function parallelBrushGrip(
|
|
|
64
75
|
return 'create';
|
|
65
76
|
}
|
|
66
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
|
+
|
|
67
124
|
/**
|
|
68
125
|
* The band a drag stands at, now that the pointer is there.
|
|
69
126
|
* @param drag - The drag, as it began.
|
|
@@ -123,14 +180,23 @@ export function parallelBandOf(
|
|
|
123
180
|
* @param band - The band, in pixels from the top of the drawing area.
|
|
124
181
|
* @param axis - The axis it belongs to.
|
|
125
182
|
* @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.
|
|
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.
|
|
127
187
|
*/
|
|
128
188
|
export function parallelRangeOf(
|
|
129
189
|
band: ParallelBand,
|
|
130
190
|
axis: ParallelAxisLayout,
|
|
131
191
|
): ParallelRange {
|
|
132
|
-
const low =
|
|
133
|
-
|
|
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);
|
|
134
200
|
return low <= high ? [low, high] : [high, low];
|
|
135
201
|
}
|
|
136
202
|
|
|
@@ -5,22 +5,28 @@
|
|
|
5
5
|
* figure has to filter on the same answer. Two definitions of "kept" is the
|
|
6
6
|
* fault this file exists to prevent: a table showing fourteen rows under a
|
|
7
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.
|
|
8
13
|
*/
|
|
9
14
|
|
|
15
|
+
import { parallelRangeList } from './parallelSelection.ts';
|
|
10
16
|
import type {
|
|
11
17
|
ParallelAxis,
|
|
12
18
|
ParallelRange,
|
|
13
19
|
ParallelRanges,
|
|
14
20
|
} from './parallelTypes.ts';
|
|
15
21
|
|
|
16
|
-
/** One axis's
|
|
22
|
+
/** One axis's intervals, ready to be tested row by row. */
|
|
17
23
|
interface ActiveRange {
|
|
18
24
|
/** The column being tested. */
|
|
19
25
|
values: ArrayLike<number>;
|
|
20
|
-
/** The lowest value
|
|
21
|
-
minimum:
|
|
22
|
-
/** The highest value
|
|
23
|
-
maximum:
|
|
26
|
+
/** The lowest value each interval keeps. */
|
|
27
|
+
minimum: Float64Array;
|
|
28
|
+
/** The highest value each interval keeps. */
|
|
29
|
+
maximum: Float64Array;
|
|
24
30
|
}
|
|
25
31
|
|
|
26
32
|
/**
|
|
@@ -38,10 +44,28 @@ export function parallelRangeKeeps(
|
|
|
38
44
|
return value >= range[0] && value <= range[1];
|
|
39
45
|
}
|
|
40
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
|
+
|
|
41
65
|
/**
|
|
42
66
|
* Which rows every brush keeps at once.
|
|
43
67
|
* @param axes - The axes, whose ids the intervals are keyed by.
|
|
44
|
-
* @param ranges -
|
|
68
|
+
* @param ranges - What each axis keeps: one interval, several, or none.
|
|
45
69
|
* @param count - How many rows there are.
|
|
46
70
|
* @returns One byte per row, `1` for kept. An interval keyed on an axis that
|
|
47
71
|
* is not drawn is ignored rather than emptying the figure, so a range left
|
|
@@ -56,23 +80,36 @@ export function parallelIncludedMask(
|
|
|
56
80
|
const included = new Uint8Array(count).fill(1);
|
|
57
81
|
const active: ActiveRange[] = [];
|
|
58
82
|
for (const axis of axes) {
|
|
59
|
-
const
|
|
60
|
-
if (
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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 });
|
|
66
93
|
}
|
|
67
94
|
if (active.length === 0) return included;
|
|
68
95
|
|
|
69
96
|
for (const range of active) {
|
|
70
97
|
const rows = Math.min(count, range.values.length);
|
|
98
|
+
const intervals = range.minimum.length;
|
|
71
99
|
for (let row = 0; row < rows; row++) {
|
|
72
100
|
if (included[row] === 0) continue;
|
|
73
101
|
const value = range.values[row] as number;
|
|
74
|
-
|
|
75
|
-
|
|
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;
|
|
76
113
|
}
|
|
77
114
|
// A row the figure has no value for on a brushed axis is not in range.
|
|
78
115
|
for (let row = rows; row < count; row++) included[row] = 0;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which order the axes stand in, when the reader is the one deciding.
|
|
3
|
+
*
|
|
4
|
+
* A parallel-coordinates figure only shows a relationship between two columns
|
|
5
|
+
* that stand next to each other, so the order is not decoration: moving an
|
|
6
|
+
* axis is how a reader asks a different question of the same data. The
|
|
7
|
+
* arithmetic of the move is here, away from the pointer that starts it, so
|
|
8
|
+
* that dragging a name and pressing an arrow key are the same operation.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { ParallelAxisLayout } from './parallelAxes.ts';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The same ids with one of them moved.
|
|
15
|
+
* @param ids - The axes, from left to right.
|
|
16
|
+
* @param from - Which one is moving.
|
|
17
|
+
* @param to - The place it lands in, counted in the list it left.
|
|
18
|
+
* @returns The new order. The list comes back unchanged when either place is
|
|
19
|
+
* outside it or when nothing moves, so a caller can report without comparing.
|
|
20
|
+
*/
|
|
21
|
+
export function parallelMoveAxis(
|
|
22
|
+
ids: readonly string[],
|
|
23
|
+
from: number,
|
|
24
|
+
to: number,
|
|
25
|
+
): string[] {
|
|
26
|
+
const order = [...ids];
|
|
27
|
+
if (from < 0 || from >= order.length) return order;
|
|
28
|
+
if (to < 0 || to >= order.length || to === from) return order;
|
|
29
|
+
const [moved] = order.splice(from, 1);
|
|
30
|
+
if (moved === undefined) return [...ids];
|
|
31
|
+
order.splice(to, 0, moved);
|
|
32
|
+
return order;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Where an axis dragged to that position would land.
|
|
37
|
+
* @param x - Where the pointer is, in pixels from the left of the drawing area.
|
|
38
|
+
* @param layouts - The axes, from left to right.
|
|
39
|
+
* @returns The index of the axis whose place it would take, or `-1` when there
|
|
40
|
+
* is nowhere to land. Nearest axis rather than nearest gap: a reader dragging
|
|
41
|
+
* a name over its neighbour means the two to swap, and the neighbour's own
|
|
42
|
+
* name is the thing they are aiming at.
|
|
43
|
+
*/
|
|
44
|
+
export function parallelDropIndex(
|
|
45
|
+
x: number,
|
|
46
|
+
layouts: readonly ParallelAxisLayout[],
|
|
47
|
+
): number {
|
|
48
|
+
let nearest = -1;
|
|
49
|
+
let nearestDistance = Number.POSITIVE_INFINITY;
|
|
50
|
+
for (let index = 0; index < layouts.length; index++) {
|
|
51
|
+
const layout = layouts[index];
|
|
52
|
+
if (layout === undefined) continue;
|
|
53
|
+
const distance = Math.abs(x - layout.x);
|
|
54
|
+
if (distance < nearestDistance) {
|
|
55
|
+
nearestDistance = distance;
|
|
56
|
+
nearest = index;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return nearest;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The ids of the axes, from left to right.
|
|
64
|
+
* @param layouts - The axes, already placed.
|
|
65
|
+
* @returns Their ids, which is what an order is reported as.
|
|
66
|
+
*/
|
|
67
|
+
export function parallelAxisOrder(
|
|
68
|
+
layouts: readonly ParallelAxisLayout[],
|
|
69
|
+
): string[] {
|
|
70
|
+
const ids: string[] = [];
|
|
71
|
+
for (const layout of layouts) ids.push(layout.id);
|
|
72
|
+
return ids;
|
|
73
|
+
}
|
|
@@ -149,8 +149,10 @@ export function paintParallelLines(
|
|
|
149
149
|
if (steps !== null) {
|
|
150
150
|
const step = steps[row] as number;
|
|
151
151
|
if (step !== previousStep) {
|
|
152
|
-
|
|
153
|
-
|
|
152
|
+
// A step past the end of the ramp is a quantity nobody knows for this
|
|
153
|
+
// row; it is drawn in the neutral ink rather than in the ramp's first
|
|
154
|
+
// colour, which means the smallest value in the set.
|
|
155
|
+
context.strokeStyle = palette[step] ?? line;
|
|
154
156
|
previousStep = step;
|
|
155
157
|
}
|
|
156
158
|
}
|
|
@@ -201,6 +203,10 @@ export function paintParallelHighlights(
|
|
|
201
203
|
|
|
202
204
|
/**
|
|
203
205
|
* Add one row's line to the path being built.
|
|
206
|
+
*
|
|
207
|
+
* The line is one polyline per unbroken run of known values, so a row with a
|
|
208
|
+
* gap in the middle is drawn as two, and nothing is painted over an axis the
|
|
209
|
+
* row has no value on.
|
|
204
210
|
* @param context - A context already scaled and moved to the drawing area.
|
|
205
211
|
* @param row - The row to trace.
|
|
206
212
|
* @param layouts - The axes, from left to right.
|
|
@@ -218,9 +224,14 @@ export function traceParallelRow(
|
|
|
218
224
|
const column = values[index];
|
|
219
225
|
if (layout === undefined || column === undefined) continue;
|
|
220
226
|
const y = parallelPixelAt(layout.pixels, column[row] as number);
|
|
221
|
-
// A row whose value on this axis is not known yet has no point to draw
|
|
222
|
-
// the line
|
|
223
|
-
|
|
227
|
+
// A row whose value on this axis is not known yet has no point to draw,
|
|
228
|
+
// and the line breaks rather than bridging the axis: a segment drawn
|
|
229
|
+
// across it reads as a value the row does not hold, and the hit test,
|
|
230
|
+
// which has no endpoint there, cannot pick it back.
|
|
231
|
+
if (!Number.isFinite(y)) {
|
|
232
|
+
started = false;
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
224
235
|
if (started) {
|
|
225
236
|
context.lineTo(layout.x, y);
|
|
226
237
|
} else {
|
|
@@ -20,6 +20,15 @@ import type { ParallelColorBy } from './parallelTypes.ts';
|
|
|
20
20
|
/** How many steps the ramp is quantised into; there is one more colour than steps. */
|
|
21
21
|
export const PARALLEL_PALETTE_STEPS = 120;
|
|
22
22
|
|
|
23
|
+
/**
|
|
24
|
+
* The step a row whose colouring quantity is unknown takes.
|
|
25
|
+
*
|
|
26
|
+
* Past the end of the ramp on purpose: no colour indexes it, so the painter
|
|
27
|
+
* leaves such a row in the neutral ink rather than claiming for it the ink of
|
|
28
|
+
* the smallest value in the set.
|
|
29
|
+
*/
|
|
30
|
+
export const PARALLEL_PALETTE_UNKNOWN = PARALLEL_PALETTE_STEPS + 1;
|
|
31
|
+
|
|
23
32
|
/**
|
|
24
33
|
* The ramp, sampled into the steps a line's colour is looked up by.
|
|
25
34
|
* @param scale - The ramp, as the registry hands it over or as a plain list of colours. Defaults to viridis.
|
|
@@ -39,13 +48,16 @@ export function parallelPalette(
|
|
|
39
48
|
* loop reads an integer instead of placing a value on a ramp.
|
|
40
49
|
* @param color - The quantity the lines are coloured by.
|
|
41
50
|
* @param count - How many rows there are.
|
|
42
|
-
* @returns One step per row, from `0` to {@link PARALLEL_PALETTE_STEPS}
|
|
51
|
+
* @returns One step per row, from `0` to {@link PARALLEL_PALETTE_STEPS}, or
|
|
52
|
+
* {@link PARALLEL_PALETTE_UNKNOWN} for a row whose value is not a number.
|
|
43
53
|
*/
|
|
44
54
|
export function parallelColorSteps(
|
|
45
55
|
color: ParallelColorBy,
|
|
46
56
|
count: number,
|
|
47
57
|
): Uint8Array {
|
|
48
58
|
const steps = new Uint8Array(count);
|
|
59
|
+
// A row the column does not reach is as unknown as one holding no number.
|
|
60
|
+
steps.fill(PARALLEL_PALETTE_UNKNOWN);
|
|
49
61
|
const extent = parallelExtent(color.values, count);
|
|
50
62
|
const min = color.min ?? extent.min;
|
|
51
63
|
const max = color.max ?? extent.max;
|
|
@@ -54,6 +66,7 @@ export function parallelColorSteps(
|
|
|
54
66
|
const rows = Math.min(count, color.values.length);
|
|
55
67
|
for (let row = 0; row < rows; row++) {
|
|
56
68
|
const value = color.values[row] as number;
|
|
69
|
+
if (!Number.isFinite(value)) continue;
|
|
57
70
|
steps[row] = logarithmic
|
|
58
71
|
? Math.round(
|
|
59
72
|
positionInRange(value, min, max, { logarithmic: true }) *
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What an axis keeps, when it keeps more than one interval.
|
|
3
|
+
*
|
|
4
|
+
* The figure reports a list and accepts either a list or a bare interval, so a
|
|
5
|
+
* caller that only ever keeps one is not made to write `[[0, 1]]`. Everything
|
|
6
|
+
* downstream — the filter, the bands, the gesture — reads the list, which is
|
|
7
|
+
* what {@link parallelRangeList} is for: one normalisation, at the edge, and
|
|
8
|
+
* no `Array.isArray` scattered through the painter.
|
|
9
|
+
*
|
|
10
|
+
* Intervals on one axis are held sorted and disjoint. Two that touch are one
|
|
11
|
+
* interval drawn twice, and a reader dragging a band across its neighbour means
|
|
12
|
+
* to widen it rather than to stack two bands nobody can then take hold of.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { ParallelRange, ParallelSelection } from './parallelTypes.ts';
|
|
16
|
+
|
|
17
|
+
/** Nothing kept, shared so a normalisation never allocates for the common case. */
|
|
18
|
+
const NONE: readonly ParallelRange[] = [];
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The intervals one axis keeps, however the caller wrote them.
|
|
22
|
+
* @param selection - One interval, several, or nothing at all.
|
|
23
|
+
* @returns The intervals, in the order given. Read in place — the caller's own
|
|
24
|
+
* array comes back when it gave one.
|
|
25
|
+
*/
|
|
26
|
+
export function parallelRangeList(
|
|
27
|
+
selection: ParallelSelection | undefined,
|
|
28
|
+
): readonly ParallelRange[] {
|
|
29
|
+
if (selection === null || selection === undefined) return NONE;
|
|
30
|
+
if (selection.length === 0) return NONE;
|
|
31
|
+
// A bare interval is two numbers; a list of them holds arrays.
|
|
32
|
+
return typeof selection[0] === 'number'
|
|
33
|
+
? [selection as ParallelRange]
|
|
34
|
+
: (selection as readonly ParallelRange[]);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Whether an axis keeps anything at all.
|
|
39
|
+
* @param selection - What that axis keeps.
|
|
40
|
+
* @returns Whether any interval is in force.
|
|
41
|
+
*/
|
|
42
|
+
export function parallelHasRange(
|
|
43
|
+
selection: ParallelSelection | undefined,
|
|
44
|
+
): boolean {
|
|
45
|
+
return parallelRangeList(selection).length > 0;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The same intervals, sorted low to high and with the overlapping ones joined.
|
|
50
|
+
* @param ranges - The intervals, in any order, each low value first.
|
|
51
|
+
* @returns One list, sorted and disjoint. Intervals that touch at a point are
|
|
52
|
+
* joined too: a reader who dragged one band onto another meant one band.
|
|
53
|
+
*/
|
|
54
|
+
export function parallelMergeRanges(
|
|
55
|
+
ranges: readonly ParallelRange[],
|
|
56
|
+
): ParallelRange[] {
|
|
57
|
+
if (ranges.length === 0) return [];
|
|
58
|
+
const sorted = ranges
|
|
59
|
+
.map((range) =>
|
|
60
|
+
range[0] <= range[1] ? range : ([range[1], range[0]] as ParallelRange),
|
|
61
|
+
)
|
|
62
|
+
.toSorted((left, right) => left[0] - right[0]);
|
|
63
|
+
const merged: ParallelRange[] = [];
|
|
64
|
+
let low = (sorted[0] as ParallelRange)[0];
|
|
65
|
+
let high = (sorted[0] as ParallelRange)[1];
|
|
66
|
+
for (let index = 1; index < sorted.length; index++) {
|
|
67
|
+
const range = sorted[index] as ParallelRange;
|
|
68
|
+
if (range[0] > high) {
|
|
69
|
+
merged.push([low, high]);
|
|
70
|
+
low = range[0];
|
|
71
|
+
high = range[1];
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (range[1] > high) high = range[1];
|
|
75
|
+
}
|
|
76
|
+
merged.push([low, high]);
|
|
77
|
+
return merged;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The intervals an axis keeps once one of them is replaced, removed or added.
|
|
82
|
+
* @param ranges - The intervals in force.
|
|
83
|
+
* @param index - Which one is being written, or `-1` to add one.
|
|
84
|
+
* @param range - The interval it becomes, or `null` to take it away.
|
|
85
|
+
* @returns The new list, sorted and disjoint. Never the same array.
|
|
86
|
+
*/
|
|
87
|
+
export function parallelWriteRange(
|
|
88
|
+
ranges: readonly ParallelRange[],
|
|
89
|
+
index: number,
|
|
90
|
+
range: ParallelRange | null,
|
|
91
|
+
): ParallelRange[] {
|
|
92
|
+
const kept: ParallelRange[] = [];
|
|
93
|
+
for (let at = 0; at < ranges.length; at++) {
|
|
94
|
+
if (at === index) continue;
|
|
95
|
+
kept.push(ranges[at] as ParallelRange);
|
|
96
|
+
}
|
|
97
|
+
if (range !== null) kept.push(range);
|
|
98
|
+
return parallelMergeRanges(kept);
|
|
99
|
+
}
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import {
|
|
11
11
|
chartAxisScale,
|
|
12
12
|
chartTickDecimals,
|
|
13
|
+
chartTickLabel,
|
|
13
14
|
} from '../../chart/core/chartAxisScale.ts';
|
|
14
15
|
import { formatDecimal } from '../../format/core/numbers.ts';
|
|
15
16
|
import { roundTo } from '../../format/core/roundTo.ts';
|
|
@@ -71,12 +72,15 @@ function nicedGraduation(
|
|
|
71
72
|
wanted: number,
|
|
72
73
|
): ParallelGraduation {
|
|
73
74
|
const scale = chartAxisScale(min, max, { count: wanted, nice: true });
|
|
75
|
+
// Not `scale.labels`: those have a common power of ten lifted out of them,
|
|
76
|
+
// on the understanding that the caller writes it beside the axis name, and
|
|
77
|
+
// this figure writes only the name. Each graduation carries its own value.
|
|
78
|
+
const decimals = chartTickDecimals(scale.step);
|
|
74
79
|
const ticks: ParallelTick[] = [];
|
|
75
|
-
for (
|
|
76
|
-
const value = scale.values[index] as number;
|
|
80
|
+
for (const value of scale.values) {
|
|
77
81
|
const label =
|
|
78
82
|
axis.format === undefined
|
|
79
|
-
? (
|
|
83
|
+
? chartTickLabel(value, decimals)
|
|
80
84
|
: axis.format(value);
|
|
81
85
|
if (ticks.at(-1)?.label === label) continue;
|
|
82
86
|
ticks.push({ value, label });
|
|
@@ -86,8 +86,20 @@ export interface ParallelAxis {
|
|
|
86
86
|
/** An interval kept on one axis, in that axis's own units, low first. */
|
|
87
87
|
export type ParallelRange = readonly [number, number];
|
|
88
88
|
|
|
89
|
+
/**
|
|
90
|
+
* What one axis keeps: nothing, one interval, or several disjoint ones.
|
|
91
|
+
*
|
|
92
|
+
* A reader comparing molecules wants the light ones *and* the heavy ones, with
|
|
93
|
+
* nothing in between, which one interval cannot say. Several are read as a
|
|
94
|
+
* union — a row is kept when it falls in any of them — while the axes are
|
|
95
|
+
* still read as an intersection, so each axis narrows what the ones before it
|
|
96
|
+
* left. A bare interval means the same as a list holding only it, so a caller
|
|
97
|
+
* that only ever keeps one writes one.
|
|
98
|
+
*/
|
|
99
|
+
export type ParallelSelection = ParallelRange | readonly ParallelRange[] | null;
|
|
100
|
+
|
|
89
101
|
/** Which rows a set of brushes keeps, keyed by axis id. */
|
|
90
|
-
export type ParallelRanges = Readonly<Record<string,
|
|
102
|
+
export type ParallelRanges = Readonly<Record<string, ParallelSelection>>;
|
|
91
103
|
|
|
92
104
|
/** How the lines are coloured. */
|
|
93
105
|
export interface ParallelColorBy {
|
|
@@ -20,8 +20,8 @@ export interface ParallelAxesLayerProps {
|
|
|
20
20
|
layouts: readonly ParallelAxisLayout[];
|
|
21
21
|
/** Height of the drawing area, in pixels. */
|
|
22
22
|
innerHeight: number;
|
|
23
|
-
/** The
|
|
24
|
-
bands: ReadonlyMap<string, ParallelBand>;
|
|
23
|
+
/** The bands each axis carries right now, the one being dragged included. */
|
|
24
|
+
bands: ReadonlyMap<string, ParallelBand[]>;
|
|
25
25
|
/** The ink the axes, their ticks and their labels are drawn in. */
|
|
26
26
|
axisInk: string;
|
|
27
27
|
/** The ink a band is filled and outlined in. */
|
|
@@ -49,7 +49,7 @@ export function ParallelAxesLayer(props: ParallelAxesLayerProps): ReactElement {
|
|
|
49
49
|
textAnchor="middle"
|
|
50
50
|
>
|
|
51
51
|
{layouts.map((layout) => {
|
|
52
|
-
const
|
|
52
|
+
const drawn = bands.get(layout.id);
|
|
53
53
|
return (
|
|
54
54
|
<g
|
|
55
55
|
key={layout.id}
|
|
@@ -83,7 +83,13 @@ export function ParallelAxesLayer(props: ParallelAxesLayerProps): ReactElement {
|
|
|
83
83
|
fill="transparent"
|
|
84
84
|
pointerEvents="none"
|
|
85
85
|
/>
|
|
86
|
-
{band
|
|
86
|
+
{drawn?.map((band) => (
|
|
87
|
+
<BrushBand
|
|
88
|
+
key={`${band.top}:${band.bottom}`}
|
|
89
|
+
band={band}
|
|
90
|
+
ink={bandInk}
|
|
91
|
+
/>
|
|
92
|
+
))}
|
|
87
93
|
</g>
|
|
88
94
|
);
|
|
89
95
|
})}
|
|
@@ -30,8 +30,10 @@ import {
|
|
|
30
30
|
import {
|
|
31
31
|
PARALLEL_SVG_STYLE,
|
|
32
32
|
parallelCanvasStyle,
|
|
33
|
+
parallelDropMarkStyle,
|
|
33
34
|
parallelFigureStyle,
|
|
34
35
|
} from './parallelStyles.ts';
|
|
36
|
+
import { useParallelAxisDrag } from './useParallelAxisDrag.ts';
|
|
35
37
|
import { useParallelCanvas } from './useParallelCanvas.ts';
|
|
36
38
|
import { useParallelGesture } from './useParallelGesture.ts';
|
|
37
39
|
|
|
@@ -70,6 +72,7 @@ export function ParallelCoordinates(
|
|
|
70
72
|
const { ranges: ownedRanges, defaultRanges, included: givenMask } = props;
|
|
71
73
|
const { color: givenColor, colorAxis, onRangeChange: report } = props;
|
|
72
74
|
const { onRangePreview, hovered, onHoverChange, onRowClick } = props;
|
|
75
|
+
const { several = false, onAxisOrder } = props;
|
|
73
76
|
const count = parallelRowCount(axes, props.count);
|
|
74
77
|
const innerWidth = Math.max(
|
|
75
78
|
width - PARALLEL_MARGIN.left - PARALLEL_MARGIN.right,
|
|
@@ -109,11 +112,11 @@ export function ParallelCoordinates(
|
|
|
109
112
|
return parallelIncludedMask(axes, ranges, count);
|
|
110
113
|
}, [givenMask, axes, ranges, count]);
|
|
111
114
|
|
|
112
|
-
function commitRange(axisId: string,
|
|
115
|
+
function commitRange(axisId: string, kept: readonly ParallelRange[]): void {
|
|
113
116
|
if (ownedRanges === undefined) {
|
|
114
|
-
setOwnRanges((previous) => ({ ...previous, [axisId]:
|
|
117
|
+
setOwnRanges((previous) => ({ ...previous, [axisId]: kept }));
|
|
115
118
|
}
|
|
116
|
-
report?.(axisId,
|
|
119
|
+
report?.(axisId, kept);
|
|
117
120
|
}
|
|
118
121
|
|
|
119
122
|
const gesture = useParallelGesture({
|
|
@@ -123,6 +126,7 @@ export function ParallelCoordinates(
|
|
|
123
126
|
innerHeight,
|
|
124
127
|
included,
|
|
125
128
|
ranges,
|
|
129
|
+
several,
|
|
126
130
|
onRangeChange: commitRange,
|
|
127
131
|
onRangePreview,
|
|
128
132
|
hovered,
|
|
@@ -148,6 +152,10 @@ export function ParallelCoordinates(
|
|
|
148
152
|
ink,
|
|
149
153
|
});
|
|
150
154
|
|
|
155
|
+
const axisDrag = useParallelAxisDrag({ layouts, onAxisOrder });
|
|
156
|
+
const dropAt =
|
|
157
|
+
axisDrag.active === null ? undefined : layouts[axisDrag.active.to];
|
|
158
|
+
|
|
151
159
|
const label = given ?? parallelFigureLabel(axes, count);
|
|
152
160
|
const canvasStyle = parallelCanvasStyle(height);
|
|
153
161
|
const hover =
|
|
@@ -199,11 +207,22 @@ export function ParallelCoordinates(
|
|
|
199
207
|
onClick={gesture.onClick}
|
|
200
208
|
{...gesture.surface}
|
|
201
209
|
/>
|
|
210
|
+
{dropAt === undefined ? null : (
|
|
211
|
+
<line
|
|
212
|
+
data-parallel-drop={dropAt.id}
|
|
213
|
+
x1={PARALLEL_MARGIN.left + dropAt.x}
|
|
214
|
+
x2={PARALLEL_MARGIN.left + dropAt.x}
|
|
215
|
+
y1={PARALLEL_MARGIN.top}
|
|
216
|
+
y2={PARALLEL_MARGIN.top + innerHeight}
|
|
217
|
+
style={parallelDropMarkStyle(ink.selection)}
|
|
218
|
+
/>
|
|
219
|
+
)}
|
|
202
220
|
</svg>
|
|
203
221
|
<ParallelLabels
|
|
204
222
|
axes={axes}
|
|
205
223
|
layouts={layouts}
|
|
206
224
|
render={renderAxisLabel}
|
|
225
|
+
drag={axisDrag}
|
|
207
226
|
/>
|
|
208
227
|
{hover === null || renderTooltip === undefined ? null : (
|
|
209
228
|
<ParallelTooltip
|