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.
Files changed (157) hide show
  1. package/README.md +96 -0
  2. package/lib/core.d.ts +1 -0
  3. package/lib/core.d.ts.map +1 -1
  4. package/lib/core.js +1 -0
  5. package/lib/core.js.map +1 -1
  6. package/lib/credits/core/credits.d.ts +17 -0
  7. package/lib/credits/core/credits.d.ts.map +1 -1
  8. package/lib/credits/core/credits.js +20 -0
  9. package/lib/credits/core/credits.js.map +1 -1
  10. package/lib/ecosystem/core/sites.d.ts.map +1 -1
  11. package/lib/ecosystem/core/sites.js +40 -0
  12. package/lib/ecosystem/core/sites.js.map +1 -1
  13. package/lib/ecosystem/core/types.d.ts +1 -1
  14. package/lib/ecosystem/core/types.d.ts.map +1 -1
  15. package/lib/ecosystem/ui/glyphs.d.ts.map +1 -1
  16. package/lib/ecosystem/ui/glyphs.js +11 -0
  17. package/lib/ecosystem/ui/glyphs.js.map +1 -1
  18. package/lib/parallel/core/index.d.ts +29 -0
  19. package/lib/parallel/core/index.d.ts.map +1 -0
  20. package/lib/parallel/core/index.js +22 -0
  21. package/lib/parallel/core/index.js.map +1 -0
  22. package/lib/parallel/core/parallelAxes.d.ts +103 -0
  23. package/lib/parallel/core/parallelAxes.d.ts.map +1 -0
  24. package/lib/parallel/core/parallelAxes.js +146 -0
  25. package/lib/parallel/core/parallelAxes.js.map +1 -0
  26. package/lib/parallel/core/parallelBrush.d.ts +100 -0
  27. package/lib/parallel/core/parallelBrush.d.ts.map +1 -0
  28. package/lib/parallel/core/parallelBrush.js +149 -0
  29. package/lib/parallel/core/parallelBrush.js.map +1 -0
  30. package/lib/parallel/core/parallelExtent.d.ts +28 -0
  31. package/lib/parallel/core/parallelExtent.d.ts.map +1 -0
  32. package/lib/parallel/core/parallelExtent.js +47 -0
  33. package/lib/parallel/core/parallelExtent.js.map +1 -0
  34. package/lib/parallel/core/parallelFilter.d.ts +49 -0
  35. package/lib/parallel/core/parallelFilter.d.ts.map +1 -0
  36. package/lib/parallel/core/parallelFilter.js +107 -0
  37. package/lib/parallel/core/parallelFilter.js.map +1 -0
  38. package/lib/parallel/core/parallelHit.d.ts +47 -0
  39. package/lib/parallel/core/parallelHit.d.ts.map +1 -0
  40. package/lib/parallel/core/parallelHit.js +81 -0
  41. package/lib/parallel/core/parallelHit.js.map +1 -0
  42. package/lib/parallel/core/parallelOrder.d.ts +36 -0
  43. package/lib/parallel/core/parallelOrder.d.ts.map +1 -0
  44. package/lib/parallel/core/parallelOrder.js +65 -0
  45. package/lib/parallel/core/parallelOrder.js.map +1 -0
  46. package/lib/parallel/core/parallelPaint.d.ts +108 -0
  47. package/lib/parallel/core/parallelPaint.d.ts.map +1 -0
  48. package/lib/parallel/core/parallelPaint.js +169 -0
  49. package/lib/parallel/core/parallelPaint.js.map +1 -0
  50. package/lib/parallel/core/parallelPalette.d.ts +40 -0
  51. package/lib/parallel/core/parallelPalette.d.ts.map +1 -0
  52. package/lib/parallel/core/parallelPalette.js +81 -0
  53. package/lib/parallel/core/parallelPalette.js.map +1 -0
  54. package/lib/parallel/core/parallelSelection.d.ts +43 -0
  55. package/lib/parallel/core/parallelSelection.d.ts.map +1 -0
  56. package/lib/parallel/core/parallelSelection.js +87 -0
  57. package/lib/parallel/core/parallelSelection.js.map +1 -0
  58. package/lib/parallel/core/parallelTicks.d.ts +34 -0
  59. package/lib/parallel/core/parallelTicks.d.ts.map +1 -0
  60. package/lib/parallel/core/parallelTicks.js +119 -0
  61. package/lib/parallel/core/parallelTicks.js.map +1 -0
  62. package/lib/parallel/core/parallelTypes.d.ts +140 -0
  63. package/lib/parallel/core/parallelTypes.d.ts.map +1 -0
  64. package/lib/parallel/core/parallelTypes.js +29 -0
  65. package/lib/parallel/core/parallelTypes.js.map +1 -0
  66. package/lib/parallel/ui/ParallelAxesLayer.d.ts +28 -0
  67. package/lib/parallel/ui/ParallelAxesLayer.d.ts.map +1 -0
  68. package/lib/parallel/ui/ParallelAxesLayer.js +29 -0
  69. package/lib/parallel/ui/ParallelAxesLayer.js.map +1 -0
  70. package/lib/parallel/ui/ParallelCoordinates.d.ts +21 -0
  71. package/lib/parallel/ui/ParallelCoordinates.d.ts.map +1 -0
  72. package/lib/parallel/ui/ParallelCoordinates.js +114 -0
  73. package/lib/parallel/ui/ParallelCoordinates.js.map +1 -0
  74. package/lib/parallel/ui/ParallelLabels.d.ts +32 -0
  75. package/lib/parallel/ui/ParallelLabels.d.ts.map +1 -0
  76. package/lib/parallel/ui/ParallelLabels.js +37 -0
  77. package/lib/parallel/ui/ParallelLabels.js.map +1 -0
  78. package/lib/parallel/ui/ParallelTooltip.d.ts +26 -0
  79. package/lib/parallel/ui/ParallelTooltip.d.ts.map +1 -0
  80. package/lib/parallel/ui/ParallelTooltip.js +30 -0
  81. package/lib/parallel/ui/ParallelTooltip.js.map +1 -0
  82. package/lib/parallel/ui/index.d.ts +29 -0
  83. package/lib/parallel/ui/index.d.ts.map +1 -0
  84. package/lib/parallel/ui/index.js +20 -0
  85. package/lib/parallel/ui/index.js.map +1 -0
  86. package/lib/parallel/ui/parallelCoordinatesProps.d.ts +203 -0
  87. package/lib/parallel/ui/parallelCoordinatesProps.d.ts.map +1 -0
  88. package/lib/parallel/ui/parallelCoordinatesProps.js +11 -0
  89. package/lib/parallel/ui/parallelCoordinatesProps.js.map +1 -0
  90. package/lib/parallel/ui/parallelGestureModel.d.ts +83 -0
  91. package/lib/parallel/ui/parallelGestureModel.d.ts.map +1 -0
  92. package/lib/parallel/ui/parallelGestureModel.js +110 -0
  93. package/lib/parallel/ui/parallelGestureModel.js.map +1 -0
  94. package/lib/parallel/ui/parallelInk.d.ts +76 -0
  95. package/lib/parallel/ui/parallelInk.d.ts.map +1 -0
  96. package/lib/parallel/ui/parallelInk.js +113 -0
  97. package/lib/parallel/ui/parallelInk.js.map +1 -0
  98. package/lib/parallel/ui/parallelPlotModel.d.ts +55 -0
  99. package/lib/parallel/ui/parallelPlotModel.d.ts.map +1 -0
  100. package/lib/parallel/ui/parallelPlotModel.js +113 -0
  101. package/lib/parallel/ui/parallelPlotModel.js.map +1 -0
  102. package/lib/parallel/ui/parallelStyles.d.ts +106 -0
  103. package/lib/parallel/ui/parallelStyles.d.ts.map +1 -0
  104. package/lib/parallel/ui/parallelStyles.js +153 -0
  105. package/lib/parallel/ui/parallelStyles.js.map +1 -0
  106. package/lib/parallel/ui/useParallelAxisDrag.d.ts +60 -0
  107. package/lib/parallel/ui/useParallelAxisDrag.d.ts.map +1 -0
  108. package/lib/parallel/ui/useParallelAxisDrag.js +96 -0
  109. package/lib/parallel/ui/useParallelAxisDrag.js.map +1 -0
  110. package/lib/parallel/ui/useParallelCanvas.d.ts +49 -0
  111. package/lib/parallel/ui/useParallelCanvas.d.ts.map +1 -0
  112. package/lib/parallel/ui/useParallelCanvas.js +58 -0
  113. package/lib/parallel/ui/useParallelCanvas.js.map +1 -0
  114. package/lib/parallel/ui/useParallelGesture.d.ts +72 -0
  115. package/lib/parallel/ui/useParallelGesture.d.ts.map +1 -0
  116. package/lib/parallel/ui/useParallelGesture.js +164 -0
  117. package/lib/parallel/ui/useParallelGesture.js.map +1 -0
  118. package/lib/structure/ui/useToolbarFloor.d.ts.map +1 -1
  119. package/lib/structure/ui/useToolbarFloor.js +29 -22
  120. package/lib/structure/ui/useToolbarFloor.js.map +1 -1
  121. package/lib/ui.d.ts +1 -0
  122. package/lib/ui.d.ts.map +1 -1
  123. package/lib/ui.js +1 -0
  124. package/lib/ui.js.map +1 -1
  125. package/package.json +1 -1
  126. package/src/core.ts +1 -0
  127. package/src/credits/core/credits.ts +21 -0
  128. package/src/ecosystem/core/sites.ts +40 -0
  129. package/src/ecosystem/core/types.ts +4 -1
  130. package/src/ecosystem/ui/glyphs.tsx +46 -0
  131. package/src/parallel/core/index.ts +96 -0
  132. package/src/parallel/core/parallelAxes.ts +207 -0
  133. package/src/parallel/core/parallelBrush.ts +211 -0
  134. package/src/parallel/core/parallelExtent.ts +56 -0
  135. package/src/parallel/core/parallelFilter.ts +131 -0
  136. package/src/parallel/core/parallelHit.ts +106 -0
  137. package/src/parallel/core/parallelOrder.ts +73 -0
  138. package/src/parallel/core/parallelPaint.ts +242 -0
  139. package/src/parallel/core/parallelPalette.ts +93 -0
  140. package/src/parallel/core/parallelSelection.ts +99 -0
  141. package/src/parallel/core/parallelTicks.ts +168 -0
  142. package/src/parallel/core/parallelTypes.ts +159 -0
  143. package/src/parallel/ui/ParallelAxesLayer.tsx +129 -0
  144. package/src/parallel/ui/ParallelCoordinates.tsx +252 -0
  145. package/src/parallel/ui/ParallelLabels.tsx +77 -0
  146. package/src/parallel/ui/ParallelTooltip.tsx +55 -0
  147. package/src/parallel/ui/index.ts +65 -0
  148. package/src/parallel/ui/parallelCoordinatesProps.ts +217 -0
  149. package/src/parallel/ui/parallelGestureModel.ts +159 -0
  150. package/src/parallel/ui/parallelInk.ts +182 -0
  151. package/src/parallel/ui/parallelPlotModel.ts +130 -0
  152. package/src/parallel/ui/parallelStyles.ts +188 -0
  153. package/src/parallel/ui/useParallelAxisDrag.ts +147 -0
  154. package/src/parallel/ui/useParallelCanvas.ts +104 -0
  155. package/src/parallel/ui/useParallelGesture.ts +260 -0
  156. package/src/structure/ui/useToolbarFloor.ts +27 -22
  157. package/src/ui.ts +1 -0
@@ -0,0 +1,104 @@
1
+ /**
2
+ * The two canvases, and when they are repainted.
3
+ *
4
+ * Two rather than one, because the mass of lines changes when the data or the
5
+ * brushes do, while the row under the pointer changes on every frame the
6
+ * pointer moves. Repainting ten thousand lines to move one highlight is the
7
+ * difference between a figure that follows the pointer and one that lags it.
8
+ */
9
+
10
+ import type { RefObject } from 'react';
11
+ import { useEffect, useRef } from 'react';
12
+
13
+ import type { ParallelAxisLayout } from '../core/parallelAxes.ts';
14
+ import type { ParallelHighlight } from '../core/parallelPaint.ts';
15
+ import {
16
+ paintParallelHighlights,
17
+ paintParallelLines,
18
+ preparePlotCanvas,
19
+ } from '../core/parallelPaint.ts';
20
+
21
+ import type { ParallelInkValues } from './parallelInk.ts';
22
+
23
+ /** What {@link useParallelCanvas} needs. */
24
+ export interface ParallelCanvasOptions {
25
+ /** Total width of the figure, in CSS pixels. */
26
+ width: number;
27
+ /** Its total height. */
28
+ height: number;
29
+ /** The axes, from left to right. */
30
+ layouts: readonly ParallelAxisLayout[];
31
+ /** Each axis's column, in the same order. */
32
+ values: ReadonlyArray<ArrayLike<number>>;
33
+ /** How many rows are drawn. */
34
+ count: number;
35
+ /** Which rows the brushes keep, or `null` when they keep every one. */
36
+ included: Uint8Array | null;
37
+ /** Which colour each row takes, or `null` when nothing is colouring them. */
38
+ steps: Uint8Array | null;
39
+ /** The colours a step indexes. */
40
+ palette: readonly string[];
41
+ /** The rows drawn over the mass, the last one on top. */
42
+ highlights: readonly ParallelHighlight[];
43
+ /** The colours to paint with. */
44
+ ink: ParallelInkValues;
45
+ }
46
+
47
+ /** The two canvases a figure stacks. */
48
+ export interface ParallelCanvases {
49
+ /** The mass of lines, underneath. */
50
+ lines: RefObject<HTMLCanvasElement | null>;
51
+ /** The singled-out rows, over them. */
52
+ marks: RefObject<HTMLCanvasElement | null>;
53
+ }
54
+
55
+ /**
56
+ * Keep both canvases sized to the device pixel grid and painted.
57
+ * @param options - See {@link ParallelCanvasOptions}.
58
+ * @returns The two refs to hand to the `<canvas>` elements.
59
+ */
60
+ export function useParallelCanvas(
61
+ options: ParallelCanvasOptions,
62
+ ): ParallelCanvases {
63
+ const { width, height, layouts, values, count } = options;
64
+ const { included, steps, palette, highlights, ink } = options;
65
+ const lines = useRef<HTMLCanvasElement>(null);
66
+ const marks = useRef<HTMLCanvasElement>(null);
67
+ const { line, excluded, includedAlpha, halo } = ink;
68
+
69
+ useEffect(() => {
70
+ const context = preparePlotCanvas(lines.current, width, height);
71
+ if (context === null) return;
72
+ paintParallelLines(context, {
73
+ layouts,
74
+ values,
75
+ count,
76
+ included,
77
+ steps,
78
+ palette,
79
+ line,
80
+ excluded,
81
+ includedAlpha,
82
+ });
83
+ }, [
84
+ width,
85
+ height,
86
+ layouts,
87
+ values,
88
+ count,
89
+ included,
90
+ steps,
91
+ palette,
92
+ line,
93
+ excluded,
94
+ includedAlpha,
95
+ ]);
96
+
97
+ useEffect(() => {
98
+ const context = preparePlotCanvas(marks.current, width, height);
99
+ if (context === null) return;
100
+ paintParallelHighlights(context, { highlights, layouts, values, halo });
101
+ }, [width, height, highlights, layouts, values, halo]);
102
+
103
+ return { lines, marks };
104
+ }
@@ -0,0 +1,260 @@
1
+ /**
2
+ * One pointer over the whole figure, serving both the brush and the hover.
3
+ *
4
+ * A hook cannot be called inside a `map`, so a brush per axis would have to
5
+ * live in refs and be attached by hand; one surface that works out from the
6
+ * press which axis was grabbed has no such problem, and it gives the hover
7
+ * search the same once-a-frame coalescing the drag already pays for.
8
+ *
9
+ * The band follows the pointer live and the interval is reported on release
10
+ * only: a table filtering on the reported interval is asked to re-render once
11
+ * per gesture rather than sixty times a second.
12
+ */
13
+
14
+ import type { MouseEvent as ReactMouseEvent } from 'react';
15
+ import { useMemo, useRef, useState } from 'react';
16
+
17
+ import type {
18
+ ScatterSurfaceProps,
19
+ SurfaceEvent,
20
+ } from '../../scatter/ui/lassoGesture.ts';
21
+ import { useCapturedPointer } from '../../scatter/ui/useCapturedPointer.ts';
22
+ import type { ParallelAxisLayout } from '../core/parallelAxes.ts';
23
+ import { PARALLEL_MARGIN } from '../core/parallelAxes.ts';
24
+ import type { ParallelBand, ParallelBrushDrag } from '../core/parallelBrush.ts';
25
+ import {
26
+ parallelBandAt,
27
+ parallelBandOf,
28
+ parallelBrushTarget,
29
+ } from '../core/parallelBrush.ts';
30
+ import { parallelNearestRow } from '../core/parallelHit.ts';
31
+ import { parallelRangeList } from '../core/parallelSelection.ts';
32
+ import type { ParallelRange, ParallelRanges } from '../core/parallelTypes.ts';
33
+
34
+ import type { ParallelDraft } from './parallelGestureModel.ts';
35
+ import {
36
+ parallelAxisById,
37
+ parallelAxisNear,
38
+ parallelBandsOf,
39
+ parallelCommitOf,
40
+ parallelPointOf,
41
+ } from './parallelGestureModel.ts';
42
+
43
+ /** What {@link useParallelGesture} needs. */
44
+ export interface ParallelGestureOptions {
45
+ /** The axes, from left to right. */
46
+ layouts: readonly ParallelAxisLayout[];
47
+ /** Each axis's column, in the same order. */
48
+ values: ReadonlyArray<ArrayLike<number>>;
49
+ /** How many rows are drawn. */
50
+ count: number;
51
+ /** Height of the drawing area, in pixels. */
52
+ innerHeight: number;
53
+ /** Which rows a brush keeps, so an excluded line cannot be pointed at. */
54
+ included: Uint8Array | null;
55
+ /** The intervals in force, whoever owns them. */
56
+ ranges: ParallelRanges;
57
+ /**
58
+ * Whether one axis may keep several intervals at once.
59
+ * @default false
60
+ */
61
+ several?: boolean | undefined;
62
+ /** Called when a brush is released, with every interval that axis now keeps. */
63
+ onRangeChange: (axisId: string, ranges: readonly ParallelRange[]) => void;
64
+ /** Called on every frame a band grows. */
65
+ onRangePreview?:
66
+ ((axisId: string, ranges: readonly ParallelRange[]) => void) | undefined;
67
+ /** The row the caller says is hovered, if it owns that. */
68
+ hovered?: number | undefined;
69
+ /** Called with the row under the pointer, or `-1`. */
70
+ onHoverChange?: ((index: number) => void) | undefined;
71
+ /** Called when a line is clicked, with the row, or `-1` for empty ground. */
72
+ onRowClick?: ((index: number) => void) | undefined;
73
+ }
74
+
75
+ /** The gesture, as the figure renders it. */
76
+ export interface ParallelGesture {
77
+ /** Spread onto the transparent rectangle laid over the figure. */
78
+ surface: ScatterSurfaceProps;
79
+ /** Put on the same rectangle, for the click that picks a line. */
80
+ onClick: (event: ReactMouseEvent<Element>) => void;
81
+ /** The bands to draw on each axis, the one being dragged included. */
82
+ bands: ReadonlyMap<string, ParallelBand[]>;
83
+ /** The row under the pointer, or `-1`. */
84
+ hovered: number;
85
+ /** Where the pointer is in the figure, or `null` when it is away. */
86
+ pointer: { x: number; y: number } | null;
87
+ /** Whether a band is being dragged right now. */
88
+ brushing: boolean;
89
+ }
90
+
91
+ /**
92
+ * The brush and the hover, over one surface.
93
+ * @param options - See {@link ParallelGestureOptions}.
94
+ * @returns The gesture. See {@link ParallelGesture}.
95
+ */
96
+ export function useParallelGesture(
97
+ options: ParallelGestureOptions,
98
+ ): ParallelGesture {
99
+ const { layouts, values, count, innerHeight, included, ranges } = options;
100
+ const { several = false } = options;
101
+ const { onRangeChange, onRangePreview, onHoverChange, onRowClick } = options;
102
+
103
+ const [draft, setDraft] = useState<ParallelDraft | null>(null);
104
+ const [ownHovered, setOwnHovered] = useState(-1);
105
+ const [pointer, setPointer] = useState<{ x: number; y: number } | null>(null);
106
+ const hovered = options.hovered ?? ownHovered;
107
+
108
+ const dragRef = useRef<ParallelBrushDrag | null>(null);
109
+ const pointRef = useRef({ x: 0, y: 0 });
110
+ const reportedRef = useRef(-1);
111
+ const claimedRef = useRef(false);
112
+
113
+ function commit(
114
+ drag: ParallelBrushDrag,
115
+ band: ParallelBand,
116
+ ): readonly ParallelRange[] {
117
+ const layout = parallelAxisById(layouts, drag.axis);
118
+ if (layout === undefined) return parallelRangeList(ranges[drag.axis]);
119
+ return parallelCommitOf({
120
+ list: parallelRangeList(ranges[drag.axis]),
121
+ index: drag.index,
122
+ band,
123
+ layout,
124
+ still: unmoved(drag, band),
125
+ several,
126
+ });
127
+ }
128
+
129
+ function report(index: number): void {
130
+ if (index === reportedRef.current) return;
131
+ reportedRef.current = index;
132
+ setOwnHovered(index);
133
+ onHoverChange?.(index);
134
+ }
135
+
136
+ function findRow(point: { x: number; y: number }): number {
137
+ return parallelNearestRow(
138
+ point,
139
+ layouts,
140
+ values,
141
+ count,
142
+ included ?? undefined,
143
+ );
144
+ }
145
+
146
+ function press(event: SurfaceEvent): boolean {
147
+ const point = parallelPointOf(event);
148
+ const index = parallelAxisNear(point.x, layouts);
149
+ const layout = layouts[index];
150
+ if (layout === undefined || point.y < 0 || point.y > innerHeight) {
151
+ claimedRef.current = false;
152
+ return false;
153
+ }
154
+ const list = parallelRangeList(ranges[layout.id]);
155
+ const bands: ParallelBand[] = [];
156
+ for (const range of list) bands.push(parallelBandOf(range, layout));
157
+ const target = parallelBrushTarget(point.y, bands);
158
+ const held = target.index === -1 ? undefined : bands[target.index];
159
+ const drag: ParallelBrushDrag = {
160
+ axis: layout.id,
161
+ index: target.index,
162
+ grip: target.grip,
163
+ originY: point.y,
164
+ origin: held ?? { top: point.y, bottom: point.y },
165
+ };
166
+ dragRef.current = drag;
167
+ pointRef.current = point;
168
+ claimedRef.current = true;
169
+ setDraft({
170
+ axis: layout.id,
171
+ index: target.index,
172
+ band: parallelBandAt(drag, point.y, innerHeight),
173
+ });
174
+ return true;
175
+ }
176
+
177
+ function frame(claimed: boolean): void {
178
+ const point = pointRef.current;
179
+ const drag = dragRef.current;
180
+ if (claimed && drag !== null) {
181
+ const band = parallelBandAt(drag, point.y, innerHeight);
182
+ setDraft({ axis: drag.axis, index: drag.index, band });
183
+ onRangePreview?.(drag.axis, commit(drag, band));
184
+ return;
185
+ }
186
+ setPointer({
187
+ x: point.x + PARALLEL_MARGIN.left,
188
+ y: point.y + PARALLEL_MARGIN.top,
189
+ });
190
+ report(findRow(point));
191
+ }
192
+
193
+ function release(event: SurfaceEvent | null, keep: boolean): void {
194
+ const drag = dragRef.current;
195
+ dragRef.current = null;
196
+ setDraft(null);
197
+ if (drag === null) return;
198
+ if (event !== null) pointRef.current = parallelPointOf(event);
199
+ if (!keep) {
200
+ onRangePreview?.(drag.axis, parallelRangeList(ranges[drag.axis]));
201
+ return;
202
+ }
203
+ const kept = commit(
204
+ drag,
205
+ parallelBandAt(drag, pointRef.current.y, innerHeight),
206
+ );
207
+ onRangePreview?.(drag.axis, kept);
208
+ onRangeChange(drag.axis, kept);
209
+ }
210
+
211
+ const { surface } = useCapturedPointer({
212
+ touch: false,
213
+ cursor: 'crosshair',
214
+ onPress: press,
215
+ onMove: (event) => {
216
+ pointRef.current = parallelPointOf(event);
217
+ return true;
218
+ },
219
+ onFrame: frame,
220
+ onRelease: release,
221
+ onLeave: () => {
222
+ setPointer(null);
223
+ report(-1);
224
+ },
225
+ });
226
+
227
+ const bands = useMemo(
228
+ () => parallelBandsOf(layouts, ranges, draft),
229
+ [layouts, ranges, draft],
230
+ );
231
+
232
+ function handleClick(event: ReactMouseEvent<Element>): void {
233
+ if (claimedRef.current || onRowClick === undefined) return;
234
+ onRowClick(findRow(parallelPointOf(event)));
235
+ }
236
+
237
+ return {
238
+ surface,
239
+ onClick: handleClick,
240
+ bands,
241
+ hovered,
242
+ pointer,
243
+ brushing: draft !== null,
244
+ };
245
+ }
246
+
247
+ /**
248
+ * Whether a press took hold of an interval and let it go where it found it,
249
+ * which is a click on that interval rather than a move of it.
250
+ * @param drag - The drag, as it began.
251
+ * @param band - The band as the pointer left it.
252
+ * @returns Whether nothing moved.
253
+ */
254
+ function unmoved(drag: ParallelBrushDrag, band: ParallelBand): boolean {
255
+ return (
256
+ drag.index !== -1 &&
257
+ band.top === drag.origin.top &&
258
+ band.bottom === drag.origin.bottom
259
+ );
260
+ }
@@ -38,44 +38,49 @@ export function useToolbarFloor(
38
38
  const container = containerRef.current;
39
39
  if (container === null) return;
40
40
 
41
- let observer: ResizeObserver | null = null;
42
- let frame = 0;
43
- let attempts = 0;
41
+ let toolbar: HTMLCanvasElement | null = null;
42
+ let size: ResizeObserver | null = null;
44
43
 
45
- // The editor builds itself asynchronously, so the toolbar is usually not
46
- // there yet on the first look; watching for it is what keeps a slow first
47
- // paint from leaving the container at its unmeasured height for good.
48
44
  const attach = (): void => {
49
- const toolbar = findToolbar(container);
50
- if (toolbar === null) {
51
- attempts++;
52
- if (attempts > MAX_ATTEMPTS) return;
53
- frame = globalThis.requestAnimationFrame(attach);
54
- return;
55
- }
56
- observer = new ResizeObserver(() => {
45
+ const found = findToolbar(container);
46
+ if (found === null) return;
47
+ size?.disconnect();
48
+ toolbar = found;
49
+ size = new ResizeObserver(() => {
57
50
  // offsetHeight, because a dialog opens under a scaling transform and a
58
51
  // measured rectangle would be that of the half drawn toolbar.
59
- const floor = toolbarFloorHeight(toolbar.offsetHeight, minHeight);
60
- container.style.minHeight = `${floor}px`;
52
+ const height = found.offsetHeight;
53
+ // Zero is a toolbar being torn down or not yet laid out, never a
54
+ // toolbar that needs no room: writing the fallback for it would undo a
55
+ // correct floor and leave the palette cut off.
56
+ if (height === 0) return;
57
+ container.style.minHeight = `${toolbarFloorHeight(height, minHeight)}px`;
61
58
  });
62
- observer.observe(toolbar);
59
+ size.observe(found);
63
60
  };
64
61
 
62
+ // The editor is imported lazily and builds itself asynchronously, so it is
63
+ // almost never there on the first look, and how long it takes is a cold
64
+ // module graph and a network away from anything this code can predict. So
65
+ // the arrival is watched for rather than waited out: a deadline that
66
+ // expires leaves the toolbar cut off for the life of the page, with
67
+ // nothing on screen to say why.
68
+ const arrivals = new MutationObserver(() => {
69
+ if (toolbar?.isConnected === true) return;
70
+ attach();
71
+ });
72
+ arrivals.observe(container, { childList: true, subtree: true });
65
73
  attach();
66
74
 
67
75
  return () => {
68
- if (frame !== 0) globalThis.cancelAnimationFrame(frame);
69
- observer?.disconnect();
76
+ arrivals.disconnect();
77
+ size?.disconnect();
70
78
  };
71
79
  }, [minHeight, revision]);
72
80
 
73
81
  return containerRef;
74
82
  }
75
83
 
76
- /** Roughly two seconds of frames, after which the editor is not coming. */
77
- const MAX_ATTEMPTS = 120;
78
-
79
84
  /**
80
85
  * Find the toolbar canvas of the editor.
81
86
  *
package/src/ui.ts CHANGED
@@ -15,6 +15,7 @@ export * from './error/ui/index.ts';
15
15
  export * from './help/ui/index.ts';
16
16
  export * from './hooks/ui/index.ts';
17
17
  export * from './overlay/ui/index.ts';
18
+ export * from './parallel/ui/index.ts';
18
19
  export * from './pedagogy/ui/index.ts';
19
20
  export * from './periodic/ui/index.ts';
20
21
  export * from './projection/ui/index.ts';