react-cheminfo 0.14.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (207) hide show
  1. package/README.md +175 -21
  2. package/lib/about/core/about.d.ts +13 -7
  3. package/lib/about/core/about.d.ts.map +1 -1
  4. package/lib/about/core/about.js +4 -2
  5. package/lib/about/core/about.js.map +1 -1
  6. package/lib/about/ui/AboutBuild.d.ts +20 -0
  7. package/lib/about/ui/AboutBuild.d.ts.map +1 -0
  8. package/lib/about/ui/AboutBuild.js +38 -0
  9. package/lib/about/ui/AboutBuild.js.map +1 -0
  10. package/lib/about/ui/AboutPage.d.ts +7 -0
  11. package/lib/about/ui/AboutPage.d.ts.map +1 -1
  12. package/lib/about/ui/AboutPage.js +5 -2
  13. package/lib/about/ui/AboutPage.js.map +1 -1
  14. package/lib/about/ui/index.d.ts +2 -0
  15. package/lib/about/ui/index.d.ts.map +1 -1
  16. package/lib/about/ui/index.js +1 -0
  17. package/lib/about/ui/index.js.map +1 -1
  18. package/lib/build/core/buildInfo.d.ts +39 -0
  19. package/lib/build/core/buildInfo.d.ts.map +1 -0
  20. package/lib/build/core/buildInfo.js +20 -0
  21. package/lib/build/core/buildInfo.js.map +1 -0
  22. package/lib/build/core/current.d.ts +12 -0
  23. package/lib/build/core/current.d.ts.map +1 -0
  24. package/lib/build/core/current.js +11 -0
  25. package/lib/build/core/current.js.map +1 -0
  26. package/lib/build/core/index.d.ts +3 -0
  27. package/lib/build/core/index.d.ts.map +1 -0
  28. package/lib/build/core/index.js +2 -0
  29. package/lib/build/core/index.js.map +1 -0
  30. package/lib/build/vite/buildInfo.d.ts +57 -0
  31. package/lib/build/vite/buildInfo.d.ts.map +1 -0
  32. package/lib/build/vite/buildInfo.js +151 -0
  33. package/lib/build/vite/buildInfo.js.map +1 -0
  34. package/lib/build/vite/gitHead.d.ts +29 -0
  35. package/lib/build/vite/gitHead.d.ts.map +1 -0
  36. package/lib/build/vite/gitHead.js +102 -0
  37. package/lib/build/vite/gitHead.js.map +1 -0
  38. package/lib/build/vite/index.d.ts +4 -0
  39. package/lib/build/vite/index.d.ts.map +1 -0
  40. package/lib/build/vite/index.js +3 -0
  41. package/lib/build/vite/index.js.map +1 -0
  42. package/lib/build-info.d.ts +3 -0
  43. package/lib/build-info.d.ts.map +1 -0
  44. package/lib/build-info.js +2 -0
  45. package/lib/build-info.js.map +1 -0
  46. package/lib/core.d.ts +2 -0
  47. package/lib/core.d.ts.map +1 -1
  48. package/lib/core.js +2 -0
  49. package/lib/core.js.map +1 -1
  50. package/lib/credits/core/credits.d.ts +35 -0
  51. package/lib/credits/core/credits.d.ts.map +1 -1
  52. package/lib/credits/core/credits.js +41 -0
  53. package/lib/credits/core/credits.js.map +1 -1
  54. package/lib/ecosystem/core/sites.d.ts.map +1 -1
  55. package/lib/ecosystem/core/sites.js +40 -0
  56. package/lib/ecosystem/core/sites.js.map +1 -1
  57. package/lib/ecosystem/core/types.d.ts +1 -1
  58. package/lib/ecosystem/core/types.d.ts.map +1 -1
  59. package/lib/ecosystem/ui/Wordmark.d.ts +14 -3
  60. package/lib/ecosystem/ui/Wordmark.d.ts.map +1 -1
  61. package/lib/ecosystem/ui/Wordmark.js +8 -4
  62. package/lib/ecosystem/ui/Wordmark.js.map +1 -1
  63. package/lib/ecosystem/ui/glyphs.d.ts.map +1 -1
  64. package/lib/ecosystem/ui/glyphs.js +11 -0
  65. package/lib/ecosystem/ui/glyphs.js.map +1 -1
  66. package/lib/parallel/core/index.d.ts +27 -0
  67. package/lib/parallel/core/index.d.ts.map +1 -0
  68. package/lib/parallel/core/index.js +20 -0
  69. package/lib/parallel/core/index.js.map +1 -0
  70. package/lib/parallel/core/parallelAxes.d.ts +103 -0
  71. package/lib/parallel/core/parallelAxes.d.ts.map +1 -0
  72. package/lib/parallel/core/parallelAxes.js +146 -0
  73. package/lib/parallel/core/parallelAxes.js.map +1 -0
  74. package/lib/parallel/core/parallelBrush.d.ts +79 -0
  75. package/lib/parallel/core/parallelBrush.d.ts.map +1 -0
  76. package/lib/parallel/core/parallelBrush.js +99 -0
  77. package/lib/parallel/core/parallelBrush.js.map +1 -0
  78. package/lib/parallel/core/parallelExtent.d.ts +28 -0
  79. package/lib/parallel/core/parallelExtent.d.ts.map +1 -0
  80. package/lib/parallel/core/parallelExtent.js +47 -0
  81. package/lib/parallel/core/parallelExtent.js.map +1 -0
  82. package/lib/parallel/core/parallelFilter.d.ts +36 -0
  83. package/lib/parallel/core/parallelFilter.d.ts.map +1 -0
  84. package/lib/parallel/core/parallelFilter.js +74 -0
  85. package/lib/parallel/core/parallelFilter.js.map +1 -0
  86. package/lib/parallel/core/parallelHit.d.ts +47 -0
  87. package/lib/parallel/core/parallelHit.d.ts.map +1 -0
  88. package/lib/parallel/core/parallelHit.js +81 -0
  89. package/lib/parallel/core/parallelHit.js.map +1 -0
  90. package/lib/parallel/core/parallelPaint.d.ts +104 -0
  91. package/lib/parallel/core/parallelPaint.d.ts.map +1 -0
  92. package/lib/parallel/core/parallelPaint.js +160 -0
  93. package/lib/parallel/core/parallelPaint.js.map +1 -0
  94. package/lib/parallel/core/parallelPalette.d.ts +31 -0
  95. package/lib/parallel/core/parallelPalette.d.ts.map +1 -0
  96. package/lib/parallel/core/parallelPalette.js +68 -0
  97. package/lib/parallel/core/parallelPalette.js.map +1 -0
  98. package/lib/parallel/core/parallelTicks.d.ts +34 -0
  99. package/lib/parallel/core/parallelTicks.d.ts.map +1 -0
  100. package/lib/parallel/core/parallelTicks.js +116 -0
  101. package/lib/parallel/core/parallelTicks.js.map +1 -0
  102. package/lib/parallel/core/parallelTypes.d.ts +129 -0
  103. package/lib/parallel/core/parallelTypes.d.ts.map +1 -0
  104. package/lib/parallel/core/parallelTypes.js +29 -0
  105. package/lib/parallel/core/parallelTypes.js.map +1 -0
  106. package/lib/parallel/ui/ParallelAxesLayer.d.ts +28 -0
  107. package/lib/parallel/ui/ParallelAxesLayer.d.ts.map +1 -0
  108. package/lib/parallel/ui/ParallelAxesLayer.js +29 -0
  109. package/lib/parallel/ui/ParallelAxesLayer.js.map +1 -0
  110. package/lib/parallel/ui/ParallelCoordinates.d.ts +21 -0
  111. package/lib/parallel/ui/ParallelCoordinates.d.ts.map +1 -0
  112. package/lib/parallel/ui/ParallelCoordinates.js +109 -0
  113. package/lib/parallel/ui/ParallelCoordinates.js.map +1 -0
  114. package/lib/parallel/ui/ParallelLabels.d.ts +26 -0
  115. package/lib/parallel/ui/ParallelLabels.d.ts.map +1 -0
  116. package/lib/parallel/ui/ParallelLabels.js +25 -0
  117. package/lib/parallel/ui/ParallelLabels.js.map +1 -0
  118. package/lib/parallel/ui/ParallelTooltip.d.ts +26 -0
  119. package/lib/parallel/ui/ParallelTooltip.d.ts.map +1 -0
  120. package/lib/parallel/ui/ParallelTooltip.js +30 -0
  121. package/lib/parallel/ui/ParallelTooltip.js.map +1 -0
  122. package/lib/parallel/ui/index.d.ts +25 -0
  123. package/lib/parallel/ui/index.d.ts.map +1 -0
  124. package/lib/parallel/ui/index.js +18 -0
  125. package/lib/parallel/ui/index.js.map +1 -0
  126. package/lib/parallel/ui/parallelCoordinatesProps.d.ts +185 -0
  127. package/lib/parallel/ui/parallelCoordinatesProps.d.ts.map +1 -0
  128. package/lib/parallel/ui/parallelCoordinatesProps.js +11 -0
  129. package/lib/parallel/ui/parallelCoordinatesProps.js.map +1 -0
  130. package/lib/parallel/ui/parallelGestureModel.d.ts +52 -0
  131. package/lib/parallel/ui/parallelGestureModel.d.ts.map +1 -0
  132. package/lib/parallel/ui/parallelGestureModel.js +80 -0
  133. package/lib/parallel/ui/parallelGestureModel.js.map +1 -0
  134. package/lib/parallel/ui/parallelInk.d.ts +76 -0
  135. package/lib/parallel/ui/parallelInk.d.ts.map +1 -0
  136. package/lib/parallel/ui/parallelInk.js +113 -0
  137. package/lib/parallel/ui/parallelInk.js.map +1 -0
  138. package/lib/parallel/ui/parallelPlotModel.d.ts +55 -0
  139. package/lib/parallel/ui/parallelPlotModel.d.ts.map +1 -0
  140. package/lib/parallel/ui/parallelPlotModel.js +113 -0
  141. package/lib/parallel/ui/parallelPlotModel.js.map +1 -0
  142. package/lib/parallel/ui/parallelStyles.d.ts +83 -0
  143. package/lib/parallel/ui/parallelStyles.d.ts.map +1 -0
  144. package/lib/parallel/ui/parallelStyles.js +131 -0
  145. package/lib/parallel/ui/parallelStyles.js.map +1 -0
  146. package/lib/parallel/ui/useParallelCanvas.d.ts +49 -0
  147. package/lib/parallel/ui/useParallelCanvas.d.ts.map +1 -0
  148. package/lib/parallel/ui/useParallelCanvas.js +58 -0
  149. package/lib/parallel/ui/useParallelCanvas.js.map +1 -0
  150. package/lib/parallel/ui/useParallelGesture.d.ts +67 -0
  151. package/lib/parallel/ui/useParallelGesture.d.ts.map +1 -0
  152. package/lib/parallel/ui/useParallelGesture.js +139 -0
  153. package/lib/parallel/ui/useParallelGesture.js.map +1 -0
  154. package/lib/structure/ui/useToolbarFloor.d.ts.map +1 -1
  155. package/lib/structure/ui/useToolbarFloor.js +29 -22
  156. package/lib/structure/ui/useToolbarFloor.js.map +1 -1
  157. package/lib/ui.d.ts +1 -0
  158. package/lib/ui.d.ts.map +1 -1
  159. package/lib/ui.js +1 -0
  160. package/lib/ui.js.map +1 -1
  161. package/lib/vite.d.ts +1 -0
  162. package/lib/vite.d.ts.map +1 -1
  163. package/lib/vite.js +1 -0
  164. package/lib/vite.js.map +1 -1
  165. package/package.json +2 -1
  166. package/src/about/core/about.ts +18 -9
  167. package/src/about/ui/AboutBuild.tsx +83 -0
  168. package/src/about/ui/AboutPage.tsx +15 -7
  169. package/src/about/ui/index.ts +2 -0
  170. package/src/build/core/buildInfo.ts +48 -0
  171. package/src/build/core/current.ts +12 -0
  172. package/src/build/core/index.ts +2 -0
  173. package/src/build/vite/buildInfo.ts +178 -0
  174. package/src/build/vite/gitHead.ts +114 -0
  175. package/src/build/vite/index.ts +8 -0
  176. package/src/build-info.ts +2 -0
  177. package/src/core.ts +2 -0
  178. package/src/credits/core/credits.ts +42 -0
  179. package/src/ecosystem/core/sites.ts +40 -0
  180. package/src/ecosystem/core/types.ts +4 -1
  181. package/src/ecosystem/ui/Wordmark.tsx +22 -7
  182. package/src/ecosystem/ui/glyphs.tsx +46 -0
  183. package/src/parallel/core/index.ts +80 -0
  184. package/src/parallel/core/parallelAxes.ts +207 -0
  185. package/src/parallel/core/parallelBrush.ts +145 -0
  186. package/src/parallel/core/parallelExtent.ts +56 -0
  187. package/src/parallel/core/parallelFilter.ts +94 -0
  188. package/src/parallel/core/parallelHit.ts +106 -0
  189. package/src/parallel/core/parallelPaint.ts +231 -0
  190. package/src/parallel/core/parallelPalette.ts +80 -0
  191. package/src/parallel/core/parallelTicks.ts +164 -0
  192. package/src/parallel/core/parallelTypes.ts +147 -0
  193. package/src/parallel/ui/ParallelAxesLayer.tsx +123 -0
  194. package/src/parallel/ui/ParallelCoordinates.tsx +233 -0
  195. package/src/parallel/ui/ParallelLabels.tsx +56 -0
  196. package/src/parallel/ui/ParallelTooltip.tsx +55 -0
  197. package/src/parallel/ui/index.ts +50 -0
  198. package/src/parallel/ui/parallelCoordinatesProps.ts +199 -0
  199. package/src/parallel/ui/parallelGestureModel.ts +109 -0
  200. package/src/parallel/ui/parallelInk.ts +182 -0
  201. package/src/parallel/ui/parallelPlotModel.ts +130 -0
  202. package/src/parallel/ui/parallelStyles.ts +146 -0
  203. package/src/parallel/ui/useParallelCanvas.ts +104 -0
  204. package/src/parallel/ui/useParallelGesture.ts +227 -0
  205. package/src/structure/ui/useToolbarFloor.ts +27 -22
  206. package/src/ui.ts +1 -0
  207. package/src/vite.ts +1 -0
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Which rows the brushes keep.
3
+ *
4
+ * Exported rather than kept inside the figure, because a table beside the
5
+ * figure has to filter on the same answer. Two definitions of "kept" is the
6
+ * fault this file exists to prevent: a table showing fourteen rows under a
7
+ * plot drawing one is not a rounding error, it is two different programs.
8
+ */
9
+
10
+ import type {
11
+ ParallelAxis,
12
+ ParallelRange,
13
+ ParallelRanges,
14
+ } from './parallelTypes.ts';
15
+
16
+ /** One axis's interval, ready to be tested row by row. */
17
+ interface ActiveRange {
18
+ /** The column being tested. */
19
+ values: ArrayLike<number>;
20
+ /** The lowest value kept. */
21
+ minimum: number;
22
+ /** The highest value kept. */
23
+ maximum: number;
24
+ }
25
+
26
+ /**
27
+ * Whether one value falls inside an interval.
28
+ * @param range - The interval, low value first.
29
+ * @param value - The value to test.
30
+ * @returns Whether it is kept. Both ends are inclusive, and a value that is
31
+ * not a number is kept by no interval at all — a row whose property has not
32
+ * been worked out yet cannot be said to be in range.
33
+ */
34
+ export function parallelRangeKeeps(
35
+ range: ParallelRange,
36
+ value: number,
37
+ ): boolean {
38
+ return value >= range[0] && value <= range[1];
39
+ }
40
+
41
+ /**
42
+ * Which rows every brush keeps at once.
43
+ * @param axes - The axes, whose ids the intervals are keyed by.
44
+ * @param ranges - The interval each axis keeps, or `null` for none.
45
+ * @param count - How many rows there are.
46
+ * @returns One byte per row, `1` for kept. An interval keyed on an axis that
47
+ * is not drawn is ignored rather than emptying the figure, so a range left
48
+ * over from a column the reader has since hidden does not silently filter
49
+ * everything away.
50
+ */
51
+ export function parallelIncludedMask(
52
+ axes: readonly ParallelAxis[],
53
+ ranges: ParallelRanges,
54
+ count: number,
55
+ ): Uint8Array {
56
+ const included = new Uint8Array(count).fill(1);
57
+ const active: ActiveRange[] = [];
58
+ for (const axis of axes) {
59
+ const range = ranges[axis.id];
60
+ if (range === null || range === undefined) continue;
61
+ active.push({
62
+ values: axis.values,
63
+ minimum: Math.min(range[0], range[1]),
64
+ maximum: Math.max(range[0], range[1]),
65
+ });
66
+ }
67
+ if (active.length === 0) return included;
68
+
69
+ for (const range of active) {
70
+ const rows = Math.min(count, range.values.length);
71
+ for (let row = 0; row < rows; row++) {
72
+ if (included[row] === 0) continue;
73
+ const value = range.values[row] as number;
74
+ if (value >= range.minimum && value <= range.maximum) continue;
75
+ included[row] = 0;
76
+ }
77
+ // A row the figure has no value for on a brushed axis is not in range.
78
+ for (let row = rows; row < count; row++) included[row] = 0;
79
+ }
80
+ return included;
81
+ }
82
+
83
+ /**
84
+ * How many rows a mask keeps.
85
+ * @param included - One byte per row, `1` for kept.
86
+ * @returns The count.
87
+ */
88
+ export function parallelKeptCount(included: Uint8Array): number {
89
+ let kept = 0;
90
+ for (const row of included) {
91
+ if (row !== 0) kept++;
92
+ }
93
+ return kept;
94
+ }
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Which row the pointer is over.
3
+ *
4
+ * Only the segment between the two axes the pointer sits between is tested, so
5
+ * the cost is one distance per row whatever the number of axes. The vertical
6
+ * gap to the segment is projected onto its normal, so a steep line is no
7
+ * harder to pick than a flat one — without which the only rows a reader can
8
+ * ever point at are the uninteresting ones.
9
+ */
10
+
11
+ import type { ParallelAxisLayout } from './parallelAxes.ts';
12
+ import { parallelPixelAt } from './parallelAxes.ts';
13
+
14
+ /** How far the pointer may sit from a line and still pick it, in pixels. */
15
+ export const PARALLEL_HOVER_TOLERANCE = 6;
16
+
17
+ /** A pointer position, relative to the top left of the drawing area. */
18
+ export interface ParallelPoint {
19
+ /** Pixels from the left of the drawing area. */
20
+ x: number;
21
+ /** Pixels from its top. */
22
+ y: number;
23
+ }
24
+
25
+ /** The pair of axes a pointer sits between, as indices into the layouts. */
26
+ export interface ParallelSegment {
27
+ /** The axis to the left of the pointer. */
28
+ left: number;
29
+ /** The axis to its right. */
30
+ right: number;
31
+ }
32
+
33
+ /**
34
+ * The pair of axes a horizontal position sits between.
35
+ * @param x - Pixels from the left of the drawing area.
36
+ * @param axes - The axes, from left to right.
37
+ * @param tolerance - How far past the first and the last axis still counts. Defaults to {@link PARALLEL_HOVER_TOLERANCE}.
38
+ * @returns The pair, or `null` when there are fewer than two axes or the
39
+ * position is past either end of the figure.
40
+ */
41
+ export function parallelSegmentAt(
42
+ x: number,
43
+ axes: readonly ParallelAxisLayout[],
44
+ tolerance: number = PARALLEL_HOVER_TOLERANCE,
45
+ ): ParallelSegment | null {
46
+ const first = axes[0];
47
+ const last = axes.at(-1);
48
+ if (first === undefined || last === undefined || axes.length < 2) return null;
49
+ if (x < first.x - tolerance || x > last.x + tolerance) return null;
50
+ for (let index = 1; index < axes.length; index++) {
51
+ const right = axes[index];
52
+ if (right === undefined) continue;
53
+ if (x <= right.x || index === axes.length - 1) {
54
+ return { left: index - 1, right: index };
55
+ }
56
+ }
57
+ return null;
58
+ }
59
+
60
+ /**
61
+ * The row whose line runs closest to the pointer.
62
+ * @param point - Where the pointer is, relative to the drawing area.
63
+ * @param axes - The axes, from left to right.
64
+ * @param values - Each axis's column, in the same order.
65
+ * @param count - How many rows are drawn.
66
+ * @param included - Which rows a brush keeps, a zero meaning the row is not pickable. Defaults to every row.
67
+ * @param tolerance - How far the pointer may sit from a line. Defaults to {@link PARALLEL_HOVER_TOLERANCE}.
68
+ * @returns The row, or `-1` when the pointer is over no line.
69
+ */
70
+ export function parallelNearestRow(
71
+ point: ParallelPoint,
72
+ axes: readonly ParallelAxisLayout[],
73
+ values: ReadonlyArray<ArrayLike<number>>,
74
+ count: number,
75
+ included?: Uint8Array,
76
+ tolerance: number = PARALLEL_HOVER_TOLERANCE,
77
+ ): number {
78
+ const segment = parallelSegmentAt(point.x, axes, tolerance);
79
+ if (segment === null) return -1;
80
+ const left = axes[segment.left];
81
+ const right = axes[segment.right];
82
+ const leftColumn = values[segment.left];
83
+ const rightColumn = values[segment.right];
84
+ if (left === undefined || right === undefined) return -1;
85
+ if (leftColumn === undefined || rightColumn === undefined) return -1;
86
+
87
+ const width = right.x - left.x;
88
+ if (width <= 0) return -1;
89
+ const ratio = (point.x - left.x) / width;
90
+ const rows = Math.min(count, leftColumn.length, rightColumn.length);
91
+
92
+ let nearest = -1;
93
+ let nearestDistance = tolerance;
94
+ for (let row = 0; row < rows; row++) {
95
+ if (included?.[row] === 0) continue;
96
+ const leftY = parallelPixelAt(left.pixels, leftColumn[row] as number);
97
+ const rightY = parallelPixelAt(right.pixels, rightColumn[row] as number);
98
+ const gap = Math.abs(leftY + (rightY - leftY) * ratio - point.y);
99
+ const distance = (gap * width) / Math.hypot(width, rightY - leftY);
100
+ if (distance < nearestDistance) {
101
+ nearestDistance = distance;
102
+ nearest = row;
103
+ }
104
+ }
105
+ return nearest;
106
+ }
@@ -0,0 +1,231 @@
1
+ /**
2
+ * The lines, painted into a canvas.
3
+ *
4
+ * The first canvas figure in the family, and for one reason: ten thousand
5
+ * polylines of a dozen points each is a hundred and twenty thousand SVG
6
+ * coordinates the browser would keep as elements. Everything a reader points
7
+ * at, brushes or reads is still SVG or HTML over the top, so only the mass of
8
+ * lines is pixels — which is also why saving the figure keeps the axes and
9
+ * loses the lines, and why a site that needs a downloadable figure should say
10
+ * so before choosing this one.
11
+ */
12
+
13
+ import type { ParallelAxisLayout } from './parallelAxes.ts';
14
+ import { PARALLEL_MARGIN, parallelPixelAt } from './parallelAxes.ts';
15
+
16
+ /** How strongly one kept line is drawn, so that the mass reads as a density. */
17
+ export const PARALLEL_INCLUDED_ALPHA = 0.35;
18
+
19
+ /** How wide a singled-out line's halo is, in pixels. */
20
+ const HALO_WIDTH = 5;
21
+
22
+ /** How wide a singled-out line itself is. */
23
+ const HIGHLIGHT_WIDTH = 2;
24
+
25
+ /** How large the dot at each of its axis crossings is. */
26
+ const HIGHLIGHT_DOT_RADIUS = 3;
27
+
28
+ const FULL_TURN = 2 * Math.PI;
29
+
30
+ /** What one pass of the line painter needs. */
31
+ export interface ParallelPaint {
32
+ /** The axes, from left to right, already placed and measured. */
33
+ layouts: readonly ParallelAxisLayout[];
34
+ /** Each axis's column, in the same order. */
35
+ values: ReadonlyArray<ArrayLike<number>>;
36
+ /** How many rows to draw. */
37
+ count: number;
38
+ /**
39
+ * Which rows the brushes keep, a zero meaning the row is drawn underneath in
40
+ * the excluded ink.
41
+ * @default null — every row is kept and nothing is drawn underneath
42
+ */
43
+ included?: Uint8Array | null;
44
+ /**
45
+ * Which colour each row takes, as a step into `palette`.
46
+ * @default null — every line takes `line`
47
+ */
48
+ steps?: Uint8Array | null;
49
+ /**
50
+ * The colours a step indexes, from `parallelPalette`.
51
+ * @default [] — nothing to index
52
+ */
53
+ palette?: readonly string[];
54
+ /** The ink a line takes when no quantity is colouring the lines. */
55
+ line: string;
56
+ /** The ink the rows a brush left out are drawn in. */
57
+ excluded: string;
58
+ /**
59
+ * How strongly a kept line is drawn. Turn it down for a dense library.
60
+ * @default PARALLEL_INCLUDED_ALPHA
61
+ */
62
+ includedAlpha?: number;
63
+ }
64
+
65
+ /** One row drawn over the mass, and the ink it is drawn in. */
66
+ export interface ParallelHighlight {
67
+ /** The row. */
68
+ row: number;
69
+ /** Its ink. */
70
+ color: string;
71
+ }
72
+
73
+ /** What the highlight pass needs. */
74
+ export interface ParallelHighlightPaint {
75
+ /** The rows to draw, the last one on top. */
76
+ highlights: readonly ParallelHighlight[];
77
+ /** The axes, from left to right. */
78
+ layouts: readonly ParallelAxisLayout[];
79
+ /** Each axis's column, in the same order. */
80
+ values: ReadonlyArray<ArrayLike<number>>;
81
+ /** The ink the halo under each line is drawn in. */
82
+ halo: string;
83
+ }
84
+
85
+ /**
86
+ * Resize a canvas to the device pixel grid and move its origin to the drawing
87
+ * area, which also clears whatever it held.
88
+ * @param canvas - The canvas, or `null` before it is mounted.
89
+ * @param width - Width of the figure, in CSS pixels.
90
+ * @param height - Its height, in CSS pixels.
91
+ * @returns The context, or `null` when there is nothing to draw on.
92
+ */
93
+ export function preparePlotCanvas(
94
+ canvas: HTMLCanvasElement | null,
95
+ width: number,
96
+ height: number,
97
+ ): CanvasRenderingContext2D | null {
98
+ if (canvas === null) return null;
99
+ const context = canvas.getContext('2d');
100
+ if (context === null) return null;
101
+ const pixelRatio = Math.max(globalThis.devicePixelRatio || 1, 1);
102
+ // Assigning the backing store is what clears it; there is no second clear.
103
+ canvas.width = Math.round(width * pixelRatio);
104
+ canvas.height = Math.round(height * pixelRatio);
105
+ context.resetTransform();
106
+ context.scale(pixelRatio, pixelRatio);
107
+ context.translate(PARALLEL_MARGIN.left, PARALLEL_MARGIN.top);
108
+ return context;
109
+ }
110
+
111
+ /**
112
+ * Draw one line per row: the ones a brush left out first, underneath, then the
113
+ * kept ones over them.
114
+ *
115
+ * The excluded rows are one single path stroked once, in a light grey rather
116
+ * than a translucent black: overlapping segments of one stroked path
117
+ * accumulate alpha, so a translucent black saturates to black wherever the
118
+ * library is dense, while a light grey is a colour the pile can only converge
119
+ * on.
120
+ * @param context - A context already scaled and moved to the drawing area by {@link preparePlotCanvas}.
121
+ * @param paint - See {@link ParallelPaint}.
122
+ */
123
+ export function paintParallelLines(
124
+ context: CanvasRenderingContext2D,
125
+ paint: ParallelPaint,
126
+ ): void {
127
+ const { layouts, values, count, line, excluded, includedAlpha } = paint;
128
+ if (layouts.length < 2) return;
129
+ const included = paint.included ?? null;
130
+ const steps = paint.steps ?? null;
131
+ const palette = paint.palette ?? [];
132
+ context.lineWidth = 1;
133
+
134
+ if (included !== null) {
135
+ context.strokeStyle = excluded;
136
+ context.beginPath();
137
+ for (let row = 0; row < count; row++) {
138
+ if (included[row] !== 0) continue;
139
+ traceParallelRow(context, row, layouts, values);
140
+ }
141
+ context.stroke();
142
+ }
143
+
144
+ context.globalAlpha = includedAlpha ?? PARALLEL_INCLUDED_ALPHA;
145
+ context.strokeStyle = line;
146
+ let previousStep = -1;
147
+ for (let row = 0; row < count; row++) {
148
+ if (included !== null && included[row] === 0) continue;
149
+ if (steps !== null) {
150
+ const step = steps[row] as number;
151
+ if (step !== previousStep) {
152
+ const color = palette[step];
153
+ if (color !== undefined) context.strokeStyle = color;
154
+ previousStep = step;
155
+ }
156
+ }
157
+ context.beginPath();
158
+ traceParallelRow(context, row, layouts, values);
159
+ context.stroke();
160
+ }
161
+ context.globalAlpha = 1;
162
+ }
163
+
164
+ /**
165
+ * Draw a few singled-out rows over the mass, each over a halo and dotted at
166
+ * every axis crossing.
167
+ * @param context - A context already scaled and moved to the drawing area.
168
+ * @param paint - See {@link ParallelHighlightPaint}.
169
+ */
170
+ export function paintParallelHighlights(
171
+ context: CanvasRenderingContext2D,
172
+ paint: ParallelHighlightPaint,
173
+ ): void {
174
+ const { highlights, layouts, values, halo } = paint;
175
+ if (layouts.length < 2) return;
176
+ context.lineJoin = 'round';
177
+ context.lineCap = 'round';
178
+ for (const { row, color } of highlights) {
179
+ context.beginPath();
180
+ traceParallelRow(context, row, layouts, values);
181
+ context.strokeStyle = halo;
182
+ context.lineWidth = HALO_WIDTH;
183
+ context.stroke();
184
+ context.strokeStyle = color;
185
+ context.lineWidth = HIGHLIGHT_WIDTH;
186
+ context.stroke();
187
+
188
+ context.fillStyle = color;
189
+ for (let index = 0; index < layouts.length; index++) {
190
+ const layout = layouts[index];
191
+ const column = values[index];
192
+ if (layout === undefined || column === undefined) continue;
193
+ const y = parallelPixelAt(layout.pixels, column[row] as number);
194
+ if (!Number.isFinite(y)) continue;
195
+ context.beginPath();
196
+ context.arc(layout.x, y, HIGHLIGHT_DOT_RADIUS, 0, FULL_TURN);
197
+ context.fill();
198
+ }
199
+ }
200
+ }
201
+
202
+ /**
203
+ * Add one row's line to the path being built.
204
+ * @param context - A context already scaled and moved to the drawing area.
205
+ * @param row - The row to trace.
206
+ * @param layouts - The axes, from left to right.
207
+ * @param values - Each axis's column, in the same order.
208
+ */
209
+ export function traceParallelRow(
210
+ context: CanvasRenderingContext2D,
211
+ row: number,
212
+ layouts: readonly ParallelAxisLayout[],
213
+ values: ReadonlyArray<ArrayLike<number>>,
214
+ ): void {
215
+ let started = false;
216
+ for (let index = 0; index < layouts.length; index++) {
217
+ const layout = layouts[index];
218
+ const column = values[index];
219
+ if (layout === undefined || column === undefined) continue;
220
+ 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;
224
+ if (started) {
225
+ context.lineTo(layout.x, y);
226
+ } else {
227
+ context.moveTo(layout.x, y);
228
+ started = true;
229
+ }
230
+ }
231
+ }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * The colour each line is drawn in, as a step into a sampled ramp.
3
+ *
4
+ * A canvas reassigns `strokeStyle` once per line, and a thousand different
5
+ * colours is a thousand style writes. Quantising the ramp to 121 steps and
6
+ * keeping the rows in the order they came means the style is written about
7
+ * 121 times for the whole figure — while no reader can tell a hundred-and-
8
+ * twenty-first of a ramp apart from its neighbour.
9
+ */
10
+
11
+ import type { ColorScale } from '../../color/core/interpolate.ts';
12
+ import { evenScale, sampleScale } from '../../color/core/interpolate.ts';
13
+ import { positionInRange } from '../../color/core/scale.ts';
14
+ import { resolveColorScale } from '../../color/core/scaleText.ts';
15
+ import { DEFAULT_COLOR_SCALE_ID } from '../../color/core/scales.ts';
16
+
17
+ import { parallelExtent } from './parallelExtent.ts';
18
+ import type { ParallelColorBy } from './parallelTypes.ts';
19
+
20
+ /** How many steps the ramp is quantised into; there is one more colour than steps. */
21
+ export const PARALLEL_PALETTE_STEPS = 120;
22
+
23
+ /**
24
+ * The ramp, sampled into the steps a line's colour is looked up by.
25
+ * @param scale - The ramp, as the registry hands it over or as a plain list of colours. Defaults to viridis.
26
+ * @returns `PARALLEL_PALETTE_STEPS + 1` colours, from the low end to the high end.
27
+ */
28
+ export function parallelPalette(
29
+ scale?: ColorScale | readonly string[],
30
+ ): readonly string[] {
31
+ return sampleScale(resolveRamp(scale), PARALLEL_PALETTE_STEPS + 1);
32
+ }
33
+
34
+ /**
35
+ * Which step of the ramp each row takes.
36
+ *
37
+ * Worked out once per data change rather than per repaint, and held as bytes:
38
+ * a library of ten thousand rows costs ten kilobytes and the painter's inner
39
+ * loop reads an integer instead of placing a value on a ramp.
40
+ * @param color - The quantity the lines are coloured by.
41
+ * @param count - How many rows there are.
42
+ * @returns One step per row, from `0` to {@link PARALLEL_PALETTE_STEPS}.
43
+ */
44
+ export function parallelColorSteps(
45
+ color: ParallelColorBy,
46
+ count: number,
47
+ ): Uint8Array {
48
+ const steps = new Uint8Array(count);
49
+ const extent = parallelExtent(color.values, count);
50
+ const min = color.min ?? extent.min;
51
+ const max = color.max ?? extent.max;
52
+ const logarithmic = color.logarithmic ?? false;
53
+ const span = max - min;
54
+ const rows = Math.min(count, color.values.length);
55
+ for (let row = 0; row < rows; row++) {
56
+ const value = color.values[row] as number;
57
+ steps[row] = logarithmic
58
+ ? Math.round(
59
+ positionInRange(value, min, max, { logarithmic: true }) *
60
+ PARALLEL_PALETTE_STEPS,
61
+ )
62
+ : colorStep(value, min, span);
63
+ }
64
+ return steps;
65
+ }
66
+
67
+ function resolveRamp(scale?: ColorScale | readonly string[]): ColorScale {
68
+ if (scale === undefined) {
69
+ return resolveColorScale(DEFAULT_COLOR_SCALE_ID).scale;
70
+ }
71
+ return 'stops' in scale ? scale : evenScale(scale);
72
+ }
73
+
74
+ function colorStep(value: number, min: number, span: number): number {
75
+ if (span <= 0) return 0;
76
+ const ratio = (value - min) / span;
77
+ if (Number.isNaN(ratio) || ratio < 0) return 0;
78
+ if (ratio > 1) return PARALLEL_PALETTE_STEPS;
79
+ return Math.round(ratio * PARALLEL_PALETTE_STEPS);
80
+ }
@@ -0,0 +1,164 @@
1
+ /**
2
+ * How one axis is graduated.
3
+ *
4
+ * Five evenly spaced marks across the raw extent, not `chartAxisScale`'s nice
5
+ * ones: nicing moves the ends of the axis, and a figure whose whole job is to
6
+ * compare rows against each other must end exactly where the data does. A
7
+ * caller who wants round numbers asks for them with `nice`, per axis.
8
+ */
9
+
10
+ import {
11
+ chartAxisScale,
12
+ chartTickDecimals,
13
+ } from '../../chart/core/chartAxisScale.ts';
14
+ import { formatDecimal } from '../../format/core/numbers.ts';
15
+ import { roundTo } from '../../format/core/roundTo.ts';
16
+
17
+ import type { ParallelAxis, ParallelTick } from './parallelTypes.ts';
18
+
19
+ /** How many graduations an axis carries when the caller asks for no number. */
20
+ export const PARALLEL_TICK_COUNT = 5;
21
+
22
+ /** The most decimals a graduation is ever written with. */
23
+ const MOST_TICK_DECIMALS = 6;
24
+
25
+ /** How far a label may sit from its own value: a tenth of the spacing. */
26
+ const LABEL_TOLERANCE = 10;
27
+
28
+ /** An axis's ends and the graduations written along it. */
29
+ export interface ParallelGraduation {
30
+ /** The two ends of the axis, low first, after any nicing. */
31
+ domain: readonly [number, number];
32
+ /** The graduations, from the bottom of the axis to the top. */
33
+ ticks: readonly ParallelTick[];
34
+ }
35
+
36
+ /**
37
+ * The graduations of one axis, and the ends they are spread between.
38
+ *
39
+ * Three ways, in order: the caller's own list wins outright — a coded quantity
40
+ * reading `none`, `low`, `high` is nobody else's to invent; `nice` hands the
41
+ * axis to `chartAxisScale`, which rounds both ends outward to whole steps; and
42
+ * otherwise the extent is divided evenly and a mark whose label repeats the
43
+ * one below it is dropped, so an axis of whole numbers from 0 to 2 reads
44
+ * `0 1 2` rather than writing `1` twice.
45
+ * @param axis - The axis, for its formatting and how many marks it wants.
46
+ * @param min - The value at the bottom of the axis.
47
+ * @param max - The value at its top.
48
+ * @returns The ends and the graduations. See {@link ParallelGraduation}.
49
+ */
50
+ export function parallelTicks(
51
+ axis: ParallelAxis,
52
+ min: number,
53
+ max: number,
54
+ ): ParallelGraduation {
55
+ const domain = [min, max] as const;
56
+ if (axis.ticks !== undefined) return { domain, ticks: axis.ticks };
57
+
58
+ const wanted = Math.max(2, Math.trunc(axis.tickCount ?? PARALLEL_TICK_COUNT));
59
+ if (axis.nice === true && axis.scale !== 'log') {
60
+ return nicedGraduation(axis, min, max, wanted);
61
+ }
62
+
63
+ const values = evenValues(min, max, wanted, axis.scale === 'log');
64
+ return { domain, ticks: labelled(values, axis.format) };
65
+ }
66
+
67
+ function nicedGraduation(
68
+ axis: ParallelAxis,
69
+ min: number,
70
+ max: number,
71
+ wanted: number,
72
+ ): ParallelGraduation {
73
+ const scale = chartAxisScale(min, max, { count: wanted, nice: true });
74
+ const ticks: ParallelTick[] = [];
75
+ for (let index = 0; index < scale.values.length; index++) {
76
+ const value = scale.values[index] as number;
77
+ const label =
78
+ axis.format === undefined
79
+ ? (scale.labels[index] ?? formatDecimal(value, scale.decimals))
80
+ : axis.format(value);
81
+ if (ticks.at(-1)?.label === label) continue;
82
+ ticks.push({ value, label });
83
+ }
84
+ return { domain: scale.domain, ticks };
85
+ }
86
+
87
+ function evenValues(
88
+ min: number,
89
+ max: number,
90
+ wanted: number,
91
+ logarithmic: boolean,
92
+ ): number[] {
93
+ const values: number[] = [];
94
+ const low = logarithmic ? Math.log10(Math.max(min, smallestStep(max))) : min;
95
+ const high = logarithmic ? Math.log10(Math.max(max, smallestStep(max))) : max;
96
+ const span = high - low;
97
+ for (let index = 0; index < wanted; index++) {
98
+ const placed = low + (span * index) / (wanted - 1);
99
+ values.push(logarithmic ? 10 ** placed : placed);
100
+ }
101
+ return values;
102
+ }
103
+
104
+ function labelled(
105
+ values: readonly number[],
106
+ format: ((value: number) => string) | undefined,
107
+ ): ParallelTick[] {
108
+ const decimals = format === undefined ? tickDecimals(values) : 0;
109
+ const ticks: ParallelTick[] = [];
110
+ for (const value of values) {
111
+ const label =
112
+ format === undefined ? formatDecimal(value, decimals) : format(value);
113
+ if (ticks.at(-1)?.label === label) continue;
114
+ ticks.push({ value, label });
115
+ }
116
+ return ticks;
117
+ }
118
+
119
+ /**
120
+ * As many decimals as the spacing needs, and one more whenever rounding would
121
+ * move a mark by more than a tenth of that spacing — which is what keeps an
122
+ * axis from 12.3 to 157 reading `12 48 85 121 157` while one from −1.5 to 2.5
123
+ * still reads `−1.5 −0.5 0.5 1.5 2.5` rather than rounding both away.
124
+ * @param values - The graduations, in the axis's own units.
125
+ * @returns How many decimals each of their labels carries.
126
+ */
127
+ function tickDecimals(values: readonly number[]): number {
128
+ const step = smallestGap(values);
129
+ const tolerance = step / LABEL_TOLERANCE;
130
+ let decimals = chartTickDecimals(step);
131
+ while (
132
+ decimals < MOST_TICK_DECIMALS &&
133
+ !faithful(values, decimals, tolerance)
134
+ ) {
135
+ decimals++;
136
+ }
137
+ return decimals;
138
+ }
139
+
140
+ function faithful(
141
+ values: readonly number[],
142
+ decimals: number,
143
+ tolerance: number,
144
+ ): boolean {
145
+ for (const value of values) {
146
+ if (Math.abs(value - roundTo(value, decimals)) > tolerance) return false;
147
+ }
148
+ return true;
149
+ }
150
+
151
+ function smallestGap(values: readonly number[]): number {
152
+ let smallest = Number.POSITIVE_INFINITY;
153
+ for (let index = 1; index < values.length; index++) {
154
+ const gap = Math.abs(
155
+ (values[index] as number) - (values[index - 1] as number),
156
+ );
157
+ if (gap > 0 && gap < smallest) smallest = gap;
158
+ }
159
+ return Number.isFinite(smallest) ? smallest : 1;
160
+ }
161
+
162
+ function smallestStep(max: number): number {
163
+ return max > 0 ? max * 1e-12 : 1;
164
+ }