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.
Files changed (170) hide show
  1. package/lib/parallel/core/index.d.ts +7 -5
  2. package/lib/parallel/core/index.d.ts.map +1 -1
  3. package/lib/parallel/core/index.js +5 -3
  4. package/lib/parallel/core/index.js.map +1 -1
  5. package/lib/parallel/core/parallelBrush.d.ts +22 -1
  6. package/lib/parallel/core/parallelBrush.d.ts.map +1 -1
  7. package/lib/parallel/core/parallelBrush.js +53 -3
  8. package/lib/parallel/core/parallelBrush.js.map +1 -1
  9. package/lib/parallel/core/parallelFilter.d.ts +14 -1
  10. package/lib/parallel/core/parallelFilter.d.ts.map +1 -1
  11. package/lib/parallel/core/parallelFilter.js +44 -11
  12. package/lib/parallel/core/parallelFilter.js.map +1 -1
  13. package/lib/parallel/core/parallelOrder.d.ts +36 -0
  14. package/lib/parallel/core/parallelOrder.d.ts.map +1 -0
  15. package/lib/parallel/core/parallelOrder.js +65 -0
  16. package/lib/parallel/core/parallelOrder.js.map +1 -0
  17. package/lib/parallel/core/parallelPaint.d.ts +4 -0
  18. package/lib/parallel/core/parallelPaint.d.ts.map +1 -1
  19. package/lib/parallel/core/parallelPaint.js +15 -6
  20. package/lib/parallel/core/parallelPaint.js.map +1 -1
  21. package/lib/parallel/core/parallelPalette.d.ts +10 -1
  22. package/lib/parallel/core/parallelPalette.d.ts.map +1 -1
  23. package/lib/parallel/core/parallelPalette.js +14 -1
  24. package/lib/parallel/core/parallelPalette.js.map +1 -1
  25. package/lib/parallel/core/parallelSelection.d.ts +43 -0
  26. package/lib/parallel/core/parallelSelection.d.ts.map +1 -0
  27. package/lib/parallel/core/parallelSelection.js +87 -0
  28. package/lib/parallel/core/parallelSelection.js.map +1 -0
  29. package/lib/parallel/core/parallelTicks.d.ts.map +1 -1
  30. package/lib/parallel/core/parallelTicks.js +7 -4
  31. package/lib/parallel/core/parallelTicks.js.map +1 -1
  32. package/lib/parallel/core/parallelTypes.d.ts +12 -1
  33. package/lib/parallel/core/parallelTypes.d.ts.map +1 -1
  34. package/lib/parallel/core/parallelTypes.js.map +1 -1
  35. package/lib/parallel/ui/ParallelAxesLayer.d.ts +2 -2
  36. package/lib/parallel/ui/ParallelAxesLayer.d.ts.map +1 -1
  37. package/lib/parallel/ui/ParallelAxesLayer.js +2 -2
  38. package/lib/parallel/ui/ParallelAxesLayer.js.map +1 -1
  39. package/lib/parallel/ui/ParallelCoordinates.d.ts.map +1 -1
  40. package/lib/parallel/ui/ParallelCoordinates.js +10 -5
  41. package/lib/parallel/ui/ParallelCoordinates.js.map +1 -1
  42. package/lib/parallel/ui/ParallelLabels.d.ts +6 -0
  43. package/lib/parallel/ui/ParallelLabels.d.ts.map +1 -1
  44. package/lib/parallel/ui/ParallelLabels.js +14 -2
  45. package/lib/parallel/ui/ParallelLabels.js.map +1 -1
  46. package/lib/parallel/ui/index.d.ts +4 -0
  47. package/lib/parallel/ui/index.d.ts.map +1 -1
  48. package/lib/parallel/ui/index.js +2 -0
  49. package/lib/parallel/ui/index.js.map +1 -1
  50. package/lib/parallel/ui/parallelCoordinatesProps.d.ts +26 -8
  51. package/lib/parallel/ui/parallelCoordinatesProps.d.ts.map +1 -1
  52. package/lib/parallel/ui/parallelGestureModel.d.ts +37 -6
  53. package/lib/parallel/ui/parallelGestureModel.d.ts.map +1 -1
  54. package/lib/parallel/ui/parallelGestureModel.js +42 -12
  55. package/lib/parallel/ui/parallelGestureModel.js.map +1 -1
  56. package/lib/parallel/ui/parallelPlotModel.d.ts +1 -1
  57. package/lib/parallel/ui/parallelPlotModel.d.ts.map +1 -1
  58. package/lib/parallel/ui/parallelPlotModel.js +3 -3
  59. package/lib/parallel/ui/parallelPlotModel.js.map +1 -1
  60. package/lib/parallel/ui/parallelStyles.d.ts +26 -3
  61. package/lib/parallel/ui/parallelStyles.d.ts.map +1 -1
  62. package/lib/parallel/ui/parallelStyles.js +26 -4
  63. package/lib/parallel/ui/parallelStyles.js.map +1 -1
  64. package/lib/parallel/ui/useParallelAxisDrag.d.ts +60 -0
  65. package/lib/parallel/ui/useParallelAxisDrag.d.ts.map +1 -0
  66. package/lib/parallel/ui/useParallelAxisDrag.js +96 -0
  67. package/lib/parallel/ui/useParallelAxisDrag.js.map +1 -0
  68. package/lib/parallel/ui/useParallelGesture.d.ts +10 -5
  69. package/lib/parallel/ui/useParallelGesture.d.ts.map +1 -1
  70. package/lib/parallel/ui/useParallelGesture.js +46 -21
  71. package/lib/parallel/ui/useParallelGesture.js.map +1 -1
  72. package/lib/structure/core/editorGuide.d.ts +69 -0
  73. package/lib/structure/core/editorGuide.d.ts.map +1 -0
  74. package/lib/structure/core/editorGuide.js +162 -0
  75. package/lib/structure/core/editorGuide.js.map +1 -0
  76. package/lib/structure/core/editorToolbar.d.ts +45 -0
  77. package/lib/structure/core/editorToolbar.d.ts.map +1 -0
  78. package/lib/structure/core/editorToolbar.js +132 -0
  79. package/lib/structure/core/editorToolbar.js.map +1 -0
  80. package/lib/structure/core/editorToolbarGeometry.d.ts +36 -0
  81. package/lib/structure/core/editorToolbarGeometry.d.ts.map +1 -0
  82. package/lib/structure/core/editorToolbarGeometry.js +61 -0
  83. package/lib/structure/core/editorToolbarGeometry.js.map +1 -0
  84. package/lib/structure/core/index.d.ts +6 -0
  85. package/lib/structure/core/index.d.ts.map +1 -1
  86. package/lib/structure/core/index.js +3 -0
  87. package/lib/structure/core/index.js.map +1 -1
  88. package/lib/structure/core/pendingCall.d.ts +21 -0
  89. package/lib/structure/core/pendingCall.d.ts.map +1 -0
  90. package/lib/structure/core/pendingCall.js +37 -0
  91. package/lib/structure/core/pendingCall.js.map +1 -0
  92. package/lib/structure/ui/EditorCanvas.d.ts +1 -16
  93. package/lib/structure/ui/EditorCanvas.d.ts.map +1 -1
  94. package/lib/structure/ui/EditorCanvas.js +5 -18
  95. package/lib/structure/ui/EditorCanvas.js.map +1 -1
  96. package/lib/structure/ui/EditorHelpButton.d.ts +22 -0
  97. package/lib/structure/ui/EditorHelpButton.d.ts.map +1 -0
  98. package/lib/structure/ui/EditorHelpButton.js +38 -0
  99. package/lib/structure/ui/EditorHelpButton.js.map +1 -0
  100. package/lib/structure/ui/KeyCaps.d.ts +12 -0
  101. package/lib/structure/ui/KeyCaps.d.ts.map +1 -0
  102. package/lib/structure/ui/KeyCaps.js +29 -0
  103. package/lib/structure/ui/KeyCaps.js.map +1 -0
  104. package/lib/structure/ui/StructureEditor.d.ts +9 -2
  105. package/lib/structure/ui/StructureEditor.d.ts.map +1 -1
  106. package/lib/structure/ui/StructureEditor.js +17 -24
  107. package/lib/structure/ui/StructureEditor.js.map +1 -1
  108. package/lib/structure/ui/StructureEditorHelp.d.ts +18 -0
  109. package/lib/structure/ui/StructureEditorHelp.d.ts.map +1 -0
  110. package/lib/structure/ui/StructureEditorHelp.js +68 -0
  111. package/lib/structure/ui/StructureEditorHelp.js.map +1 -0
  112. package/lib/structure/ui/ToolbarTooltip.d.ts +20 -0
  113. package/lib/structure/ui/ToolbarTooltip.d.ts.map +1 -0
  114. package/lib/structure/ui/ToolbarTooltip.js +49 -0
  115. package/lib/structure/ui/ToolbarTooltip.js.map +1 -0
  116. package/lib/structure/ui/editorChange.d.ts +52 -0
  117. package/lib/structure/ui/editorChange.d.ts.map +1 -0
  118. package/lib/structure/ui/editorChange.js +34 -0
  119. package/lib/structure/ui/editorChange.js.map +1 -0
  120. package/lib/structure/ui/index.d.ts +3 -1
  121. package/lib/structure/ui/index.d.ts.map +1 -1
  122. package/lib/structure/ui/index.js +1 -0
  123. package/lib/structure/ui/index.js.map +1 -1
  124. package/lib/structure/ui/useToolbarFloor.d.ts.map +1 -1
  125. package/lib/structure/ui/useToolbarFloor.js +6 -45
  126. package/lib/structure/ui/useToolbarFloor.js.map +1 -1
  127. package/lib/structure/ui/useToolbarHover.d.ts +28 -0
  128. package/lib/structure/ui/useToolbarHover.d.ts.map +1 -0
  129. package/lib/structure/ui/useToolbarHover.js +96 -0
  130. package/lib/structure/ui/useToolbarHover.js.map +1 -0
  131. package/lib/structure/ui/watchToolbar.d.ts +17 -0
  132. package/lib/structure/ui/watchToolbar.d.ts.map +1 -0
  133. package/lib/structure/ui/watchToolbar.js +56 -0
  134. package/lib/structure/ui/watchToolbar.js.map +1 -0
  135. package/package.json +1 -1
  136. package/src/parallel/core/index.ts +16 -0
  137. package/src/parallel/core/parallelBrush.ts +69 -3
  138. package/src/parallel/core/parallelFilter.ts +52 -15
  139. package/src/parallel/core/parallelOrder.ts +73 -0
  140. package/src/parallel/core/parallelPaint.ts +16 -5
  141. package/src/parallel/core/parallelPalette.ts +14 -1
  142. package/src/parallel/core/parallelSelection.ts +99 -0
  143. package/src/parallel/core/parallelTicks.ts +7 -3
  144. package/src/parallel/core/parallelTypes.ts +13 -1
  145. package/src/parallel/ui/ParallelAxesLayer.tsx +10 -4
  146. package/src/parallel/ui/ParallelCoordinates.tsx +22 -3
  147. package/src/parallel/ui/ParallelLabels.tsx +23 -2
  148. package/src/parallel/ui/index.ts +15 -0
  149. package/src/parallel/ui/parallelCoordinatesProps.ts +26 -8
  150. package/src/parallel/ui/parallelGestureModel.ts +63 -13
  151. package/src/parallel/ui/parallelPlotModel.ts +3 -3
  152. package/src/parallel/ui/parallelStyles.ts +46 -4
  153. package/src/parallel/ui/useParallelAxisDrag.ts +147 -0
  154. package/src/parallel/ui/useParallelGesture.ts +61 -28
  155. package/src/structure/core/editorGuide.ts +221 -0
  156. package/src/structure/core/editorToolbar.ts +175 -0
  157. package/src/structure/core/editorToolbarGeometry.ts +80 -0
  158. package/src/structure/core/index.ts +29 -0
  159. package/src/structure/core/pendingCall.ts +59 -0
  160. package/src/structure/ui/EditorCanvas.tsx +9 -35
  161. package/src/structure/ui/EditorHelpButton.tsx +70 -0
  162. package/src/structure/ui/KeyCaps.tsx +40 -0
  163. package/src/structure/ui/StructureEditor.tsx +39 -21
  164. package/src/structure/ui/StructureEditorHelp.tsx +136 -0
  165. package/src/structure/ui/ToolbarTooltip.tsx +95 -0
  166. package/src/structure/ui/editorChange.ts +81 -0
  167. package/src/structure/ui/index.ts +5 -1
  168. package/src/structure/ui/useToolbarFloor.ts +7 -46
  169. package/src/structure/ui/useToolbarHover.ts +134 -0
  170. 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 = parallelYToValue(band.bottom, axis);
133
- const high = parallelYToValue(band.top, axis);
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 interval, ready to be tested row by row. */
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 kept. */
21
- minimum: number;
22
- /** The highest value kept. */
23
- maximum: number;
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 - The interval each axis keeps, or `null` for none.
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 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
- });
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
- if (value >= range.minimum && value <= range.maximum) continue;
75
- included[row] = 0;
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
- const color = palette[step];
153
- if (color !== undefined) context.strokeStyle = color;
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 skips that axis rather than collapsing onto one of its ends.
223
- if (!Number.isFinite(y)) continue;
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 (let index = 0; index < scale.values.length; index++) {
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
- ? (scale.labels[index] ?? formatDecimal(value, scale.decimals))
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, ParallelRange | null>>;
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 band each axis carries right now, the one being dragged included. */
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 band = bands.get(layout.id) ?? null;
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 === null ? null : <BrushBand band={band} ink={bandInk} />}
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, range: ParallelRange | null): void {
115
+ function commitRange(axisId: string, kept: readonly ParallelRange[]): void {
113
116
  if (ownedRanges === undefined) {
114
- setOwnRanges((previous) => ({ ...previous, [axisId]: range }));
117
+ setOwnRanges((previous) => ({ ...previous, [axisId]: kept }));
115
118
  }
116
- report?.(axisId, range);
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