react-cheminfo 0.21.0 → 0.23.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 (253) hide show
  1. package/README.md +14 -4
  2. package/lib/build/vite/gitHead.d.ts.map +1 -1
  3. package/lib/build/vite/gitHead.js +17 -2
  4. package/lib/build/vite/gitHead.js.map +1 -1
  5. package/lib/clipboard/ui/ClickToCopy.d.ts +69 -0
  6. package/lib/clipboard/ui/ClickToCopy.d.ts.map +1 -0
  7. package/lib/clipboard/ui/ClickToCopy.js +66 -0
  8. package/lib/clipboard/ui/ClickToCopy.js.map +1 -0
  9. package/lib/clipboard/ui/CopyButton.d.ts +1 -1
  10. package/lib/clipboard/ui/CopyButton.js +1 -1
  11. package/lib/clipboard/ui/CopyableValue.d.ts +3 -3
  12. package/lib/clipboard/ui/CopyableValue.d.ts.map +1 -1
  13. package/lib/clipboard/ui/CopyableValue.js +5 -5
  14. package/lib/clipboard/ui/CopyableValue.js.map +1 -1
  15. package/lib/clipboard/ui/index.d.ts +2 -0
  16. package/lib/clipboard/ui/index.d.ts.map +1 -1
  17. package/lib/clipboard/ui/index.js +1 -0
  18. package/lib/clipboard/ui/index.js.map +1 -1
  19. package/lib/ecosystem/core/groups.d.ts +4 -3
  20. package/lib/ecosystem/core/groups.d.ts.map +1 -1
  21. package/lib/ecosystem/core/groups.js +8 -2
  22. package/lib/ecosystem/core/groups.js.map +1 -1
  23. package/lib/ecosystem/core/sites.d.ts.map +1 -1
  24. package/lib/ecosystem/core/sites.js +39 -0
  25. package/lib/ecosystem/core/sites.js.map +1 -1
  26. package/lib/ecosystem/core/types.d.ts +1 -1
  27. package/lib/ecosystem/core/types.d.ts.map +1 -1
  28. package/lib/ecosystem/ui/glyphs.d.ts.map +1 -1
  29. package/lib/ecosystem/ui/glyphs.js +9 -0
  30. package/lib/ecosystem/ui/glyphs.js.map +1 -1
  31. package/lib/error/ui/ErrorFallback.d.ts.map +1 -1
  32. package/lib/error/ui/ErrorFallback.js +1 -1
  33. package/lib/error/ui/ErrorFallback.js.map +1 -1
  34. package/lib/overlay/core/index.d.ts +2 -2
  35. package/lib/overlay/core/index.d.ts.map +1 -1
  36. package/lib/overlay/core/index.js +1 -1
  37. package/lib/overlay/core/index.js.map +1 -1
  38. package/lib/overlay/core/overlayMarks.d.ts +20 -5
  39. package/lib/overlay/core/overlayMarks.d.ts.map +1 -1
  40. package/lib/overlay/core/overlayMarks.js +31 -5
  41. package/lib/overlay/core/overlayMarks.js.map +1 -1
  42. package/lib/overlay/ui/OverlayLegendMark.js +5 -0
  43. package/lib/overlay/ui/OverlayLegendMark.js.map +1 -1
  44. package/lib/pedagogy/ui/ReferenceSectionBlock.d.ts.map +1 -1
  45. package/lib/pedagogy/ui/ReferenceSectionBlock.js +2 -1
  46. package/lib/pedagogy/ui/ReferenceSectionBlock.js.map +1 -1
  47. package/lib/projection/core/index.d.ts +4 -4
  48. package/lib/projection/core/index.d.ts.map +1 -1
  49. package/lib/projection/core/index.js +2 -2
  50. package/lib/projection/core/index.js.map +1 -1
  51. package/lib/projection/core/projectionCopy.d.ts +2 -0
  52. package/lib/projection/core/projectionCopy.d.ts.map +1 -1
  53. package/lib/projection/core/projectionCopy.js +1 -0
  54. package/lib/projection/core/projectionCopy.js.map +1 -1
  55. package/lib/projection/core/projectionGroupings.d.ts +44 -0
  56. package/lib/projection/core/projectionGroupings.d.ts.map +1 -0
  57. package/lib/projection/core/projectionGroupings.js +132 -0
  58. package/lib/projection/core/projectionGroupings.js.map +1 -0
  59. package/lib/projection/core/projectionOptions.d.ts +16 -6
  60. package/lib/projection/core/projectionOptions.d.ts.map +1 -1
  61. package/lib/projection/core/projectionOptions.js +7 -1
  62. package/lib/projection/core/projectionOptions.js.map +1 -1
  63. package/lib/projection/core/projectionPanelWords.d.ts +6 -0
  64. package/lib/projection/core/projectionPanelWords.d.ts.map +1 -1
  65. package/lib/projection/core/projectionPanelWords.js +4 -0
  66. package/lib/projection/core/projectionPanelWords.js.map +1 -1
  67. package/lib/projection/core/projectionSamples.d.ts +63 -32
  68. package/lib/projection/core/projectionSamples.d.ts.map +1 -1
  69. package/lib/projection/core/projectionSamples.js +1 -80
  70. package/lib/projection/core/projectionSamples.js.map +1 -1
  71. package/lib/projection/core/projectionStrings.d.ts +2 -0
  72. package/lib/projection/core/projectionStrings.d.ts.map +1 -1
  73. package/lib/projection/core/projectionStrings.js +10 -1
  74. package/lib/projection/core/projectionStrings.js.map +1 -1
  75. package/lib/projection/core/resolveProjectionOptions.d.ts +8 -3
  76. package/lib/projection/core/resolveProjectionOptions.d.ts.map +1 -1
  77. package/lib/projection/core/resolveProjectionOptions.js +58 -5
  78. package/lib/projection/core/resolveProjectionOptions.js.map +1 -1
  79. package/lib/projection/ui/ProjectionBar.d.ts +1 -1
  80. package/lib/projection/ui/ProjectionBar.d.ts.map +1 -1
  81. package/lib/projection/ui/ProjectionBar.js +3 -1
  82. package/lib/projection/ui/ProjectionBar.js.map +1 -1
  83. package/lib/projection/ui/ProjectionGroupingPickers.d.ts +38 -0
  84. package/lib/projection/ui/ProjectionGroupingPickers.d.ts.map +1 -0
  85. package/lib/projection/ui/ProjectionGroupingPickers.js +33 -0
  86. package/lib/projection/ui/ProjectionGroupingPickers.js.map +1 -0
  87. package/lib/projection/ui/ProjectionMapControls.d.ts +6 -7
  88. package/lib/projection/ui/ProjectionMapControls.d.ts.map +1 -1
  89. package/lib/projection/ui/ProjectionMapControls.js +11 -9
  90. package/lib/projection/ui/ProjectionMapControls.js.map +1 -1
  91. package/lib/projection/ui/ProjectionMapLegend.d.ts.map +1 -1
  92. package/lib/projection/ui/ProjectionMapLegend.js +1 -1
  93. package/lib/projection/ui/ProjectionMapLegend.js.map +1 -1
  94. package/lib/projection/ui/ProjectionMapMore.d.ts +2 -2
  95. package/lib/projection/ui/ProjectionMapMore.d.ts.map +1 -1
  96. package/lib/projection/ui/ProjectionMapMore.js +7 -5
  97. package/lib/projection/ui/ProjectionMapMore.js.map +1 -1
  98. package/lib/projection/ui/ProjectionMapTab.d.ts +15 -6
  99. package/lib/projection/ui/ProjectionMapTab.d.ts.map +1 -1
  100. package/lib/projection/ui/ProjectionMapTab.js +11 -7
  101. package/lib/projection/ui/ProjectionMapTab.js.map +1 -1
  102. package/lib/projection/ui/ProjectionPairsMore.d.ts +3 -7
  103. package/lib/projection/ui/ProjectionPairsMore.d.ts.map +1 -1
  104. package/lib/projection/ui/ProjectionPairsMore.js +4 -5
  105. package/lib/projection/ui/ProjectionPairsMore.js.map +1 -1
  106. package/lib/projection/ui/ProjectionPairsTab.d.ts.map +1 -1
  107. package/lib/projection/ui/ProjectionPairsTab.js +2 -2
  108. package/lib/projection/ui/ProjectionPairsTab.js.map +1 -1
  109. package/lib/projection/ui/ProjectionPanel.d.ts +1 -1
  110. package/lib/projection/ui/ProjectionPanel.d.ts.map +1 -1
  111. package/lib/projection/ui/ProjectionPanel.js +1 -1
  112. package/lib/projection/ui/ProjectionPanel.js.map +1 -1
  113. package/lib/projection/ui/ProjectionReadout.d.ts +7 -1
  114. package/lib/projection/ui/ProjectionReadout.d.ts.map +1 -1
  115. package/lib/projection/ui/ProjectionReadout.js +19 -7
  116. package/lib/projection/ui/ProjectionReadout.js.map +1 -1
  117. package/lib/projection/ui/ProjectionSpaceControls.d.ts +3 -5
  118. package/lib/projection/ui/ProjectionSpaceControls.d.ts.map +1 -1
  119. package/lib/projection/ui/ProjectionSpaceControls.js +9 -7
  120. package/lib/projection/ui/ProjectionSpaceControls.js.map +1 -1
  121. package/lib/projection/ui/ProjectionSpaceMore.d.ts +2 -2
  122. package/lib/projection/ui/ProjectionSpaceMore.d.ts.map +1 -1
  123. package/lib/projection/ui/ProjectionSpaceMore.js +5 -4
  124. package/lib/projection/ui/ProjectionSpaceMore.js.map +1 -1
  125. package/lib/projection/ui/ProjectionSpaceTab.d.ts +11 -3
  126. package/lib/projection/ui/ProjectionSpaceTab.d.ts.map +1 -1
  127. package/lib/projection/ui/ProjectionSpaceTab.js +7 -4
  128. package/lib/projection/ui/ProjectionSpaceTab.js.map +1 -1
  129. package/lib/projection/ui/ProjectionViewer.d.ts.map +1 -1
  130. package/lib/projection/ui/ProjectionViewer.js +1 -0
  131. package/lib/projection/ui/ProjectionViewer.js.map +1 -1
  132. package/lib/projection/ui/projectionBarReadings.d.ts +4 -1
  133. package/lib/projection/ui/projectionBarReadings.d.ts.map +1 -1
  134. package/lib/projection/ui/projectionBarReadings.js +14 -4
  135. package/lib/projection/ui/projectionBarReadings.js.map +1 -1
  136. package/lib/projection/ui/projectionBarSlots.d.ts +4 -2
  137. package/lib/projection/ui/projectionBarSlots.d.ts.map +1 -1
  138. package/lib/projection/ui/projectionBarSlots.js +3 -7
  139. package/lib/projection/ui/projectionBarSlots.js.map +1 -1
  140. package/lib/projection/ui/projectionMapChoices.d.ts +31 -6
  141. package/lib/projection/ui/projectionMapChoices.d.ts.map +1 -1
  142. package/lib/projection/ui/projectionMapChoices.js +58 -9
  143. package/lib/projection/ui/projectionMapChoices.js.map +1 -1
  144. package/lib/projection/ui/projectionMapChrome.d.ts +3 -9
  145. package/lib/projection/ui/projectionMapChrome.d.ts.map +1 -1
  146. package/lib/projection/ui/projectionMapChrome.js +41 -27
  147. package/lib/projection/ui/projectionMapChrome.js.map +1 -1
  148. package/lib/projection/ui/projectionMapCorner.d.ts +25 -0
  149. package/lib/projection/ui/projectionMapCorner.d.ts.map +1 -0
  150. package/lib/projection/ui/projectionMapCorner.js +46 -0
  151. package/lib/projection/ui/projectionMapCorner.js.map +1 -0
  152. package/lib/projection/ui/projectionMapModel.d.ts +9 -18
  153. package/lib/projection/ui/projectionMapModel.d.ts.map +1 -1
  154. package/lib/projection/ui/projectionMapModel.js +14 -39
  155. package/lib/projection/ui/projectionMapModel.js.map +1 -1
  156. package/lib/projection/ui/projectionMapView.d.ts +3 -1
  157. package/lib/projection/ui/projectionMapView.d.ts.map +1 -1
  158. package/lib/projection/ui/projectionMapView.js +9 -4
  159. package/lib/projection/ui/projectionMapView.js.map +1 -1
  160. package/lib/projection/ui/projectionSelectionSentence.d.ts +10 -0
  161. package/lib/projection/ui/projectionSelectionSentence.d.ts.map +1 -0
  162. package/lib/projection/ui/projectionSelectionSentence.js +18 -0
  163. package/lib/projection/ui/projectionSelectionSentence.js.map +1 -0
  164. package/lib/projection/ui/projectionStateApi.d.ts +47 -0
  165. package/lib/projection/ui/projectionStateApi.d.ts.map +1 -0
  166. package/lib/projection/ui/projectionStateApi.js +9 -0
  167. package/lib/projection/ui/projectionStateApi.js.map +1 -0
  168. package/lib/projection/ui/projectionTabModels.js +1 -1
  169. package/lib/projection/ui/projectionTabModels.js.map +1 -1
  170. package/lib/projection/ui/useProjectionState.d.ts +1 -29
  171. package/lib/projection/ui/useProjectionState.d.ts.map +1 -1
  172. package/lib/projection/ui/useProjectionState.js +13 -5
  173. package/lib/projection/ui/useProjectionState.js.map +1 -1
  174. package/lib/scatter/core/index.d.ts +1 -0
  175. package/lib/scatter/core/index.d.ts.map +1 -1
  176. package/lib/scatter/core/index.js +1 -0
  177. package/lib/scatter/core/index.js.map +1 -1
  178. package/lib/scatter/core/pointShape.d.ts +18 -0
  179. package/lib/scatter/core/pointShape.d.ts.map +1 -0
  180. package/lib/scatter/core/pointShape.js +70 -0
  181. package/lib/scatter/core/pointShape.js.map +1 -0
  182. package/lib/scatter/ui/ScatterPlot.d.ts.map +1 -1
  183. package/lib/scatter/ui/ScatterPlot.js +3 -2
  184. package/lib/scatter/ui/ScatterPlot.js.map +1 -1
  185. package/lib/scatter/ui/ScatterPointLayer.d.ts +14 -1
  186. package/lib/scatter/ui/ScatterPointLayer.d.ts.map +1 -1
  187. package/lib/scatter/ui/ScatterPointLayer.js +15 -3
  188. package/lib/scatter/ui/ScatterPointLayer.js.map +1 -1
  189. package/lib/scatter/ui/scatterFigureProps.d.ts +13 -0
  190. package/lib/scatter/ui/scatterFigureProps.d.ts.map +1 -1
  191. package/lib/scatter/ui/scatterMarkGlyph.d.ts +2 -1
  192. package/lib/scatter/ui/scatterMarkGlyph.d.ts.map +1 -1
  193. package/lib/scatter/ui/scatterMarkGlyph.js +7 -1
  194. package/lib/scatter/ui/scatterMarkGlyph.js.map +1 -1
  195. package/lib/scatter3d/ui/ScatterCloud.d.ts.map +1 -1
  196. package/lib/scatter3d/ui/ScatterCloud.js +2 -1
  197. package/lib/scatter3d/ui/ScatterCloud.js.map +1 -1
  198. package/package.json +1 -1
  199. package/src/build/vite/gitHead.ts +17 -2
  200. package/src/clipboard/ui/ClickToCopy.tsx +190 -0
  201. package/src/clipboard/ui/CopyButton.tsx +2 -2
  202. package/src/clipboard/ui/CopyableValue.tsx +11 -6
  203. package/src/clipboard/ui/index.ts +2 -0
  204. package/src/ecosystem/core/groups.ts +10 -3
  205. package/src/ecosystem/core/sites.ts +39 -0
  206. package/src/ecosystem/core/types.ts +4 -1
  207. package/src/ecosystem/ui/glyphs.tsx +51 -0
  208. package/src/error/ui/ErrorFallback.tsx +3 -1
  209. package/src/overlay/core/index.ts +6 -2
  210. package/src/overlay/core/overlayMarks.ts +51 -6
  211. package/src/overlay/ui/OverlayLegendMark.tsx +5 -0
  212. package/src/pedagogy/ui/ReferenceSectionBlock.tsx +10 -1
  213. package/src/projection/core/index.ts +11 -3
  214. package/src/projection/core/projectionCopy.ts +3 -0
  215. package/src/projection/core/projectionGroupings.ts +174 -0
  216. package/src/projection/core/projectionOptions.ts +18 -7
  217. package/src/projection/core/projectionPanelWords.ts +10 -0
  218. package/src/projection/core/projectionSamples.ts +62 -120
  219. package/src/projection/core/projectionStrings.ts +12 -1
  220. package/src/projection/core/resolveProjectionOptions.ts +66 -4
  221. package/src/projection/ui/ProjectionBar.tsx +4 -2
  222. package/src/projection/ui/ProjectionGroupingPickers.tsx +91 -0
  223. package/src/projection/ui/ProjectionMapControls.tsx +29 -32
  224. package/src/projection/ui/ProjectionMapLegend.tsx +1 -1
  225. package/src/projection/ui/ProjectionMapMore.tsx +14 -4
  226. package/src/projection/ui/ProjectionMapTab.tsx +27 -9
  227. package/src/projection/ui/ProjectionPairsMore.tsx +12 -21
  228. package/src/projection/ui/ProjectionPairsTab.tsx +5 -2
  229. package/src/projection/ui/ProjectionPanel.tsx +7 -3
  230. package/src/projection/ui/ProjectionReadout.tsx +28 -8
  231. package/src/projection/ui/ProjectionSpaceControls.tsx +20 -27
  232. package/src/projection/ui/ProjectionSpaceMore.tsx +12 -3
  233. package/src/projection/ui/ProjectionSpaceTab.tsx +23 -6
  234. package/src/projection/ui/ProjectionViewer.tsx +1 -0
  235. package/src/projection/ui/projectionBarReadings.ts +24 -8
  236. package/src/projection/ui/projectionBarSlots.tsx +10 -9
  237. package/src/projection/ui/projectionMapChoices.ts +71 -14
  238. package/src/projection/ui/projectionMapChrome.ts +55 -30
  239. package/src/projection/ui/projectionMapCorner.ts +67 -0
  240. package/src/projection/ui/projectionMapModel.ts +20 -59
  241. package/src/projection/ui/projectionMapView.ts +15 -8
  242. package/src/projection/ui/projectionSelectionSentence.ts +22 -0
  243. package/src/projection/ui/projectionStateApi.ts +57 -0
  244. package/src/projection/ui/projectionTabModels.ts +1 -1
  245. package/src/projection/ui/useProjectionState.ts +33 -37
  246. package/src/scatter/core/index.ts +1 -0
  247. package/src/scatter/core/pointShape.ts +80 -0
  248. package/src/scatter/ui/ScatterPlot.tsx +4 -1
  249. package/src/scatter/ui/ScatterPointLayer.tsx +39 -11
  250. package/src/scatter/ui/scatterFigureProps.ts +13 -0
  251. package/src/scatter/ui/scatterMarkGlyph.tsx +9 -1
  252. package/src/scatter3d/ui/ScatterCloud.tsx +3 -0
  253. package/styles/chrome.css +163 -0
@@ -0,0 +1,57 @@
1
+ /**
2
+ * What the one hook every tab shares hands out.
3
+ *
4
+ * The type lives beside the hook rather than inside it because the hook is
5
+ * short and its answer is not: a reader looking for how the state is kept
6
+ * should not have to scroll past a page of documented fields to find it.
7
+ */
8
+
9
+ import type { ScatterSelectionMode } from '../../scatter/core/scatterSelection.ts';
10
+ import type { SelectionChange } from '../../scatter/ui/useScatterSelection.ts';
11
+ import type { ProjectionOptions } from '../core/projectionOptions.ts';
12
+ import type {
13
+ ProjectionGrouping,
14
+ ResolvedProjectionGroups,
15
+ ResolvedProjectionShapes,
16
+ } from '../core/projectionSamples.ts';
17
+ import type { ProjectionTab } from '../core/projectionTabs.ts';
18
+
19
+ import type { ProjectionSelection } from './projectionSelection.ts';
20
+
21
+ /** What every tab reads, and the five ways any of them changes it. */
22
+ export interface ProjectionStateApi {
23
+ /** The tabs this result can fill, in reading order. */
24
+ tabs: readonly ProjectionTab[];
25
+ /** The one showing. */
26
+ tab: ProjectionTab;
27
+ /** Move to another tab. One the result cannot fill is ignored. */
28
+ setTab: (tab: ProjectionTab) => void;
29
+ /**
30
+ * The grouping the dots are coloured by, resolved once per change; empty
31
+ * while the colour stands for nothing.
32
+ */
33
+ groups: ResolvedProjectionGroups;
34
+ /**
35
+ * The grouping the dots are shaped by, resolved once per change, or `null`
36
+ * while they are all discs.
37
+ */
38
+ shapes: ResolvedProjectionShapes | null;
39
+ /** Every grouping the samples carry, for the pickers that choose between them. */
40
+ groupings: readonly ProjectionGrouping[];
41
+ /** Every option, already made safe against the result. */
42
+ options: ProjectionOptions;
43
+ /** Change some of them; the rest are carried over and checked again. */
44
+ setOptions: (patch: Partial<ProjectionOptions>) => void;
45
+ /** The selected rows, as indices, which is what a plot draws. */
46
+ selected: readonly number[];
47
+ /** Settle a selection a plot's own gesture produced. */
48
+ settleSelection: (change: SelectionChange) => void;
49
+ /** Settle one from elsewhere — a legend entry, a table beside the figure. */
50
+ selectRows: (
51
+ rows: readonly number[],
52
+ mode: ScatterSelectionMode,
53
+ source: ProjectionSelection['source'],
54
+ ) => void;
55
+ /** Draw one pair of axes on the map, which is what a cell of the grid does. */
56
+ selectPair: (xAxis: number, yAxis: number) => void;
57
+ }
@@ -90,7 +90,7 @@ export function useProjectionModels(
90
90
  return {
91
91
  profiles,
92
92
  shares,
93
- sampleName: samples.ids[one] ?? `Row ${one + 1}`,
93
+ sampleName: samples.labels?.[one] ?? samples.ids[one] ?? `Row ${one + 1}`,
94
94
  };
95
95
  }
96
96
 
@@ -2,9 +2,13 @@ import { useCallback, useMemo, useState } from 'react';
2
2
 
3
3
  import type { ScatterSelectionMode } from '../../scatter/core/scatterSelection.ts';
4
4
  import type { SelectionChange } from '../../scatter/ui/useScatterSelection.ts';
5
+ import {
6
+ projectionGrouping,
7
+ resolveProjectionGroups,
8
+ resolveProjectionShapes,
9
+ } from '../core/projectionGroupings.ts';
5
10
  import type { ProjectionOptions } from '../core/projectionOptions.ts';
6
- import type { ResolvedProjectionGroups } from '../core/projectionSamples.ts';
7
- import { resolveProjectionGroups } from '../core/projectionSamples.ts';
11
+ import type { ProjectionGrouping } from '../core/projectionSamples.ts';
8
12
  import type { ProjectionTab } from '../core/projectionTabs.ts';
9
13
  import { projectionTabs } from '../core/projectionTabs.ts';
10
14
  import { resolveProjectionOptions } from '../core/resolveProjectionOptions.ts';
@@ -15,6 +19,7 @@ import {
15
19
  projectionNamesOf,
16
20
  projectionRowsOf,
17
21
  } from './projectionSelection.ts';
22
+ import type { ProjectionStateApi } from './projectionStateApi.ts';
18
23
  import type { ProjectionViewerProps } from './projectionViewerProps.ts';
19
24
 
20
25
  /**
@@ -41,34 +46,6 @@ type ProjectionStateOptions = Pick<
41
46
  | 'onOptionsChange'
42
47
  >;
43
48
 
44
- /** What every tab reads, and the five ways any of them changes it. */
45
- export interface ProjectionStateApi {
46
- /** The tabs this result can fill, in reading order. */
47
- tabs: readonly ProjectionTab[];
48
- /** The one showing. */
49
- tab: ProjectionTab;
50
- /** Move to another tab. One the result cannot fill is ignored. */
51
- setTab: (tab: ProjectionTab) => void;
52
- /** The groups as every figure draws them, resolved once per change. */
53
- groups: ResolvedProjectionGroups;
54
- /** Every option, already made safe against the result. */
55
- options: ProjectionOptions;
56
- /** Change some of them; the rest are carried over and checked again. */
57
- setOptions: (patch: Partial<ProjectionOptions>) => void;
58
- /** The selected rows, as indices, which is what a plot draws. */
59
- selected: readonly number[];
60
- /** Settle a selection a plot's own gesture produced. */
61
- settleSelection: (change: SelectionChange) => void;
62
- /** Settle one from elsewhere — a legend entry, a table beside the figure. */
63
- selectRows: (
64
- rows: readonly number[],
65
- mode: ScatterSelectionMode,
66
- source: ProjectionSelection['source'],
67
- ) => void;
68
- /** Draw one pair of axes on the map, which is what a cell of the grid does. */
69
- selectPair: (xAxis: number, yAxis: number) => void;
70
- }
71
-
72
49
  /**
73
50
  * The tab, the pair of axes, the selection and the options, controlled or not.
74
51
  *
@@ -121,13 +98,23 @@ export function useProjectionState(
121
98
  () => defaultOptions ?? {},
122
99
  );
123
100
 
101
+ const groupings = samples.groupings ?? NO_GROUPINGS;
102
+ const settled = useMemo(
103
+ () =>
104
+ resolveProjectionOptions(heldOptions ?? ownOptions, result, groupings),
105
+ [groupings, heldOptions, ownOptions, result],
106
+ );
107
+ const { colorBy, shapeBy } = settled;
108
+ const rowCount = result.scores.rows;
124
109
  const groups = useMemo(
125
- () => resolveProjectionGroups(samples, result.scores.rows),
126
- [result, samples],
110
+ () =>
111
+ resolveProjectionGroups(projectionGrouping(samples, colorBy), rowCount),
112
+ [colorBy, rowCount, samples],
127
113
  );
128
- const settled = useMemo(
129
- () => resolveProjectionOptions(heldOptions ?? ownOptions, result),
130
- [heldOptions, ownOptions, result],
114
+ const shapes = useMemo(
115
+ () =>
116
+ resolveProjectionShapes(projectionGrouping(samples, shapeBy), rowCount),
117
+ [rowCount, samples, shapeBy],
131
118
  );
132
119
 
133
120
  const setTab = useCallback(
@@ -142,11 +129,15 @@ export function useProjectionState(
142
129
 
143
130
  const setOptions = useCallback(
144
131
  (patch: Partial<ProjectionOptions>) => {
145
- const next = resolveProjectionOptions({ ...settled, ...patch }, result);
132
+ const next = resolveProjectionOptions(
133
+ { ...settled, ...patch },
134
+ result,
135
+ groupings,
136
+ );
146
137
  if (heldOptions === undefined) setOwnOptions(next);
147
138
  onOptionsChange?.(next);
148
139
  },
149
- [heldOptions, onOptionsChange, result, settled],
140
+ [groupings, heldOptions, onOptionsChange, result, settled],
150
141
  );
151
142
 
152
143
  const selectRows = useCallback(
@@ -189,6 +180,8 @@ export function useProjectionState(
189
180
  tab: inReach(tab, tabs) ?? inReach(ownTab, tabs) ?? tabs[0] ?? 'map',
190
181
  setTab,
191
182
  groups,
183
+ shapes,
184
+ groupings,
192
185
  options: settled,
193
186
  setOptions,
194
187
  selected:
@@ -222,3 +215,6 @@ function inReach(
222
215
  }
223
216
  return undefined;
224
217
  }
218
+
219
+ /** No groupings at all, shared so an absent list keeps one identity. */
220
+ const NO_GROUPINGS: readonly ProjectionGrouping[] = [];
@@ -38,6 +38,7 @@ export {
38
38
  resetLassoPath,
39
39
  } from './lassoPath.ts';
40
40
  export { pointInPolygon, pointsInPolygon, polygonBounds } from './polygon.ts';
41
+ export { pointShapePath } from './pointShape.ts';
41
42
  export type { ScatterGroupMoments } from './scatterGroupMoments.ts';
42
43
  export type {
43
44
  ScatterGroupSpread,
@@ -0,0 +1,80 @@
1
+ import { chartRoundPixel } from '../../chart/core/chartScale.ts';
2
+ import type { OverlayMarkShape } from '../../overlay/core/overlayMarks.ts';
3
+
4
+ /**
5
+ * The outline of a mark drawn as a filled polygon — a sample shaped by a
6
+ * second grouping, or a marker drawn in one of those shapes — as SVG path data.
7
+ *
8
+ * Every shape covers the area of the disc it stands in for. The eye weighs a
9
+ * mark by its ink, so a square drawn in the disc's own box would make its
10
+ * group read as the heavier one — which is a claim about the data that nothing
11
+ * in the data supports.
12
+ * @param shape - The shape.
13
+ * @param x - Horizontal centre, in pixels.
14
+ * @param y - Vertical centre, in pixels.
15
+ * @param radius - Radius of the disc the shape stands in for, in pixels.
16
+ * @returns The path data, or `null` for a disc, which is drawn as a circle,
17
+ * and for every mark that is not a filled polygon.
18
+ */
19
+ export function pointShapePath(
20
+ shape: OverlayMarkShape,
21
+ x: number,
22
+ y: number,
23
+ radius: number,
24
+ ): string | null {
25
+ if (shape === 'square') {
26
+ const half = SQUARE_HALF * radius;
27
+ return polygon([
28
+ [x - half, y - half],
29
+ [x + half, y - half],
30
+ [x + half, y + half],
31
+ [x - half, y + half],
32
+ ]);
33
+ }
34
+ if (shape === 'diamond') {
35
+ const reach = DIAMOND_REACH * radius;
36
+ return polygon([
37
+ [x, y - reach],
38
+ [x + reach, y],
39
+ [x, y + reach],
40
+ [x - reach, y],
41
+ ]);
42
+ }
43
+ if (shape === 'triangle' || shape === 'triangle-down') {
44
+ // The centroid sits on the point, so the triangle is balanced on the
45
+ // sample rather than hanging from it.
46
+ const tip = TRIANGLE_TIP * radius;
47
+ const half = TRIANGLE_HALF_BASE * radius;
48
+ const base = (TRIANGLE_TIP / 2) * radius;
49
+ const flip = shape === 'triangle' ? 1 : -1;
50
+ return polygon([
51
+ [x, y - flip * tip],
52
+ [x + half, y + flip * base],
53
+ [x - half, y + flip * base],
54
+ ]);
55
+ }
56
+ return null;
57
+ }
58
+
59
+ function polygon(corners: ReadonlyArray<readonly [number, number]>): string {
60
+ let path = '';
61
+ for (const [x, y] of corners) {
62
+ path += `${path === '' ? 'M' : 'L'}${chartRoundPixel(x)} ${chartRoundPixel(y)}`;
63
+ }
64
+ return `${path}Z`;
65
+ }
66
+
67
+ /** Half the side of a square as large as a disc of radius one: √π / 2. */
68
+ const SQUARE_HALF = Math.sqrt(Math.PI) / 2;
69
+
70
+ /** How far a diamond as large as that disc reaches from its centre: √(π / 2). */
71
+ const DIAMOND_REACH = Math.sqrt(Math.PI / 2);
72
+
73
+ /**
74
+ * The side of an equilateral triangle as large as that disc is √(4π / √3). Its
75
+ * tip stands side / √3 from the centroid, and its base half as far on the other
76
+ * side.
77
+ */
78
+ const TRIANGLE_SIDE = Math.sqrt((4 * Math.PI) / Math.sqrt(3));
79
+ const TRIANGLE_TIP = TRIANGLE_SIDE / Math.sqrt(3);
80
+ const TRIANGLE_HALF_BASE = TRIANGLE_SIDE / 2;
@@ -32,7 +32,8 @@ import { useScatterLabels } from './useScatterLabels.ts';
32
32
  */
33
33
  export function ScatterPlot(props: ScatterPlotProps): ReactElement {
34
34
  const { x, y, width, height, xAxis, yAxis, groupOf, groups } = props;
35
- const { mutedGroups, ellipse = null, ellipseMinimumPoints } = props;
35
+ const { mutedGroups, shapeOf, shapes } = props;
36
+ const { ellipse = null, ellipseMinimumPoints } = props;
36
37
  const { ellipseFillOpacity, showGroupMeans, markers } = props;
37
38
  const { pointRadius = 3.5, outlinedFrom, pointLabels } = props;
38
39
  const { showGroupLabels = false, selected, defaultSelected } = props;
@@ -127,6 +128,8 @@ export function ScatterPlot(props: ScatterPlotProps): ReactElement {
127
128
  groupOf={groupOf}
128
129
  colors={ink.colors}
129
130
  opacities={ink.opacities}
131
+ shapeOf={shapeOf}
132
+ shapes={shapes}
130
133
  radius={pointRadius}
131
134
  outlinedFrom={outlinedFrom}
132
135
  />
@@ -3,6 +3,8 @@ import { memo } from 'react';
3
3
 
4
4
  import { chartGroupIndex } from '../../chart/core/chartGroups.ts';
5
5
  import { chartRoundPixel } from '../../chart/core/chartScale.ts';
6
+ import type { OverlaySampleShape } from '../../overlay/core/overlayMarks.ts';
7
+ import { pointShapePath } from '../core/pointShape.ts';
6
8
  import type { ScreenPoints } from '../core/screenPoints.ts';
7
9
 
8
10
  /** What {@link ScatterPointLayer} draws. */
@@ -31,6 +33,17 @@ export interface ScatterPointLayerProps {
31
33
  * @default undefined — every group is drawn at full strength
32
34
  */
33
35
  opacities?: readonly number[];
36
+ /**
37
+ * Which shape each point takes, as an index into `shapes`. An entry of `-1`,
38
+ * or one outside the range, is a disc.
39
+ * @default undefined — every point is a disc
40
+ */
41
+ shapeOf?: ArrayLike<number>;
42
+ /**
43
+ * The shapes, in the order `shapeOf` names them.
44
+ * @default undefined
45
+ */
46
+ shapes?: readonly OverlaySampleShape[];
34
47
  /**
35
48
  * Colour of a point belonging to no group. It has to be the one ink on the
36
49
  * plot that names nothing, or a reader counts it as one more group.
@@ -77,7 +90,8 @@ export interface ScatterPointLayerProps {
77
90
  *
78
91
  * A hollow dot is the same dot at the same radius with its colour moved from
79
92
  * the fill to the stroke, so the two read as one kind of thing seen at two
80
- * levels of confidence rather than as two unrelated marks.
93
+ * levels of confidence rather than as two unrelated marks. A shaped point
94
+ * follows the same rule, and covers the area of the disc it stands in for.
81
95
  * @param props - See {@link ScatterPointLayerProps}.
82
96
  * @returns The cloud.
83
97
  */
@@ -89,6 +103,8 @@ export const ScatterPointLayer = memo(function ScatterPointLayer(
89
103
  groupOf,
90
104
  colors,
91
105
  opacities,
106
+ shapeOf,
107
+ shapes,
92
108
  fallbackColor = 'var(--text-faint)',
93
109
  radius = 3.5,
94
110
  radii,
@@ -102,6 +118,7 @@ export const ScatterPointLayer = memo(function ScatterPointLayer(
102
118
  const steps = order === undefined ? count : Math.min(count, order.length);
103
119
  const hollowFrom = outlineStart(outlinedFrom, count);
104
120
  const groups = colors?.length ?? 0;
121
+ const shapeCount = shapes?.length ?? 0;
105
122
  const ink = groupInk(colors, opacities, fallbackColor);
106
123
 
107
124
  const dots: ReactElement[] = [];
@@ -115,17 +132,28 @@ export const ScatterPointLayer = memo(function ScatterPointLayer(
115
132
  const color = ink.colors[slot] ?? fallbackColor;
116
133
  const hollow = index >= hollowFrom;
117
134
  const size = radii?.[index];
135
+ const reach = size === undefined ? radius : chartRoundPixel(size);
136
+ const shape = shapes?.[chartGroupIndex(shapeOf, index, shapeCount)];
137
+ const outline =
138
+ shape === undefined ? null : pointShapePath(shape, x, y, reach);
139
+ const paint = {
140
+ fill: hollow ? 'none' : color,
141
+ stroke: hollow ? color : undefined,
142
+ strokeWidth: hollow ? OUTLINE_WIDTH : undefined,
143
+ opacity: ink.opacities[slot],
144
+ };
118
145
  dots.push(
119
- <circle
120
- key={index}
121
- cx={chartRoundPixel(x)}
122
- cy={chartRoundPixel(y)}
123
- r={size === undefined ? radius : chartRoundPixel(size)}
124
- fill={hollow ? 'none' : color}
125
- stroke={hollow ? color : undefined}
126
- strokeWidth={hollow ? OUTLINE_WIDTH : undefined}
127
- opacity={ink.opacities[slot]}
128
- />,
146
+ outline === null ? (
147
+ <circle
148
+ key={index}
149
+ cx={chartRoundPixel(x)}
150
+ cy={chartRoundPixel(y)}
151
+ r={reach}
152
+ {...paint}
153
+ />
154
+ ) : (
155
+ <path key={index} d={outline} {...paint} />
156
+ ),
129
157
  );
130
158
  }
131
159
 
@@ -9,6 +9,7 @@
9
9
 
10
10
  import type { ReactNode } from 'react';
11
11
 
12
+ import type { OverlaySampleShape } from '../../overlay/core/overlayMarks.ts';
12
13
  import type { ScatterSelectionMode } from '../core/scatterSelection.ts';
13
14
 
14
15
  import type { SelectionChange } from './useScatterSelection.ts';
@@ -61,6 +62,18 @@ export interface ScatterGroupProps {
61
62
  * @default undefined — every group is drawn in full
62
63
  */
63
64
  mutedGroups?: ReadonlySet<string>;
65
+ /**
66
+ * Which shape each point takes, as an index into `shapes`: a second
67
+ * grouping of the same points, drawn while colour keeps the first. A point
68
+ * with `-1`, `NaN`, or an index outside the range, is a disc.
69
+ * @default undefined — every point is a disc
70
+ */
71
+ shapeOf?: ArrayLike<number>;
72
+ /**
73
+ * The shapes, in the order `shapeOf` names them.
74
+ * @default undefined
75
+ */
76
+ shapes?: readonly OverlaySampleShape[];
64
77
  /**
65
78
  * Radius of a dot, in pixels. A cloud draws the dot at the middle of its box
66
79
  * at this size, one at the front larger and one at the back smaller.
@@ -10,6 +10,7 @@ import type { ReactElement } from 'react';
10
10
 
11
11
  import { chartRoundPixel } from '../../chart/core/chartScale.ts';
12
12
  import type { OverlayMarkShape } from '../../overlay/core/overlayMarks.ts';
13
+ import { pointShapePath } from '../core/pointShape.ts';
13
14
 
14
15
  /** A point drawn over the cloud that is not a sample, already in pixels. */
15
16
  export interface ScatterPixelMark {
@@ -32,7 +33,8 @@ export interface ScatterPixelMark {
32
33
  * One mark, drawn as the glyph its shape asks for.
33
34
  *
34
35
  * A rule laid across the cloud names no point, so the two rule shapes are
35
- * drawn as the default cross.
36
+ * drawn as the default cross. The three filled polygons are the shapes a
37
+ * sample can take, drawn as large as the disc they stand in for.
36
38
  * @param mark - What to draw, already in the frame's pixels.
37
39
  * @param size - Side of the square the glyph is drawn in, in pixels.
38
40
  * @param key - What React reconciles the glyph by.
@@ -81,6 +83,12 @@ export function markGlyph(
81
83
  />
82
84
  );
83
85
  }
86
+ const polygon = pointShapePath(shape, x, y, half);
87
+ if (polygon !== null) {
88
+ return (
89
+ <path key={key} {...shared} d={polygon} fill={color} stroke="none" />
90
+ );
91
+ }
84
92
  return (
85
93
  <path
86
94
  key={key}
@@ -47,6 +47,7 @@ import { useOrbitDrag } from './useOrbitDrag.ts';
47
47
  export function ScatterCloud(props: ScatterCloudProps): ReactElement {
48
48
  const { x, y, z, width, height, xLabel, yLabel, zLabel } = props;
49
49
  const { groupOf, groups, mutedGroups, ellipsoid = null } = props;
50
+ const { shapeOf, shapes } = props;
50
51
  const { ellipsoidMinimumPoints, ellipsoidFillOpacity } = props;
51
52
  const { showGroupLabels = false, pointLabels, pointRadius = 3.5 } = props;
52
53
  const { outlinedFrom, selected, defaultSelected, onSelectionChange } = props;
@@ -198,6 +199,8 @@ export function ScatterCloud(props: ScatterCloudProps): ReactElement {
198
199
  groupOf={groupOf}
199
200
  colors={ink.colors}
200
201
  opacities={ink.opacities}
202
+ shapeOf={shapeOf}
203
+ shapes={shapes}
201
204
  radius={pointRadius}
202
205
  outlinedFrom={outlinedFrom}
203
206
  />
package/styles/chrome.css CHANGED
@@ -321,6 +321,169 @@ button {
321
321
  max-width: none;
322
322
  }
323
323
 
324
+ /* ── Selection ───────────────────────────────────────────────────────────── */
325
+
326
+ /* A tool is clicked and dragged, not read: a drag across a viewer, a double
327
+ click on a control or a stray triple click must not paint the page. What is
328
+ worth taking away is copied with a click instead (`<ClickToCopy>`). Form
329
+ controls stay editable, and what a visitor reads, quotes or reports stays
330
+ selectable: the About page, the glossary, a code block, a reference, an
331
+ error, and any region a site marks with `text-selectable`. The regions are
332
+ reopened down to their descendants, so a site's own `* { user-select: none }`
333
+ cannot close them again. */
334
+ body {
335
+ -webkit-user-select: none;
336
+ user-select: none;
337
+ }
338
+
339
+ input,
340
+ textarea,
341
+ select,
342
+ [contenteditable],
343
+ :is(
344
+ .text-selectable,
345
+ .about-page,
346
+ .glossary-index,
347
+ .code-block,
348
+ .citation-preview,
349
+ .error-fallback__message,
350
+ .bp6-callout.bp6-intent-danger
351
+ ),
352
+ :is(
353
+ .text-selectable,
354
+ .about-page,
355
+ .glossary-index,
356
+ .code-block,
357
+ .citation-preview,
358
+ .error-fallback__message,
359
+ .bp6-callout.bp6-intent-danger
360
+ )
361
+ * {
362
+ -webkit-user-select: text;
363
+ user-select: text;
364
+ }
365
+
366
+ /* ── Click to copy ───────────────────────────────────────────────────────── */
367
+
368
+ /* The copy cursor, the tint and the clipboard glyph are the promise; a tick
369
+ keeps it. The glyph is `clipboard`, never `duplicate`: two overlapping squares
370
+ read as "add another". Green and red are the confirmation's own colours,
371
+ the ones Blueprint gives success and danger. */
372
+ .click-to-copy {
373
+ position: relative;
374
+ border-radius: 4px;
375
+ cursor: copy;
376
+ transition: background-color 120ms ease;
377
+ }
378
+
379
+ .click-to-copy:focus-visible {
380
+ outline: 2px solid var(--accent);
381
+ outline-offset: 1px;
382
+ }
383
+
384
+ .click-to-copy[data-copy='copied'] {
385
+ background-color: color-mix(in oklab, #238551 15%, transparent);
386
+ }
387
+
388
+ .click-to-copy[data-copy='failed'] {
389
+ background-color: color-mix(in oklab, #cd4246 15%, transparent);
390
+ }
391
+
392
+ .click-to-copy__icon {
393
+ position: absolute;
394
+ top: 50%;
395
+ color: var(--text-muted);
396
+ opacity: 0;
397
+ pointer-events: none;
398
+ transition: opacity 120ms ease;
399
+ }
400
+
401
+ /* In a line of text the glyph floats just past the value, so the line keeps
402
+ its spacing; a block keeps room for it inside its right edge. */
403
+ .click-to-copy--inline .click-to-copy__icon {
404
+ left: 100%;
405
+ transform: translate(3px, -50%);
406
+ }
407
+
408
+ .click-to-copy--block {
409
+ padding-right: 22px;
410
+ }
411
+
412
+ .click-to-copy--block .click-to-copy__icon {
413
+ right: 4px;
414
+ transform: translateY(-50%);
415
+ }
416
+
417
+ .click-to-copy:focus-visible .click-to-copy__icon,
418
+ .click-to-copy[data-copy] .click-to-copy__icon {
419
+ opacity: 1;
420
+ }
421
+
422
+ .click-to-copy[data-copy='copied'] .click-to-copy__icon {
423
+ color: #238551;
424
+ }
425
+
426
+ .click-to-copy[data-copy='failed'] .click-to-copy__icon {
427
+ color: #cd4246;
428
+ }
429
+
430
+ /* Only where there is a pointer to hover with: on a touch screen the tint
431
+ would stay on after the tap, over the tick. */
432
+ @media (hover: hover) {
433
+ .click-to-copy:not([data-copy]):hover {
434
+ background-color: color-mix(in oklab, var(--accent) 12%, transparent);
435
+ }
436
+
437
+ .click-to-copy:hover .click-to-copy__icon {
438
+ opacity: 1;
439
+ }
440
+ }
441
+
442
+ /* Blueprint's interactive and striped tables paint and point every cell from
443
+ three classes deep, so a copyable cell repeats its class to stay ahead. */
444
+ .click-to-copy.click-to-copy.click-to-copy.click-to-copy:is(td, th) {
445
+ border-radius: 0;
446
+ cursor: copy;
447
+ }
448
+
449
+ .click-to-copy.click-to-copy.click-to-copy.click-to-copy:is(
450
+ td,
451
+ th
452
+ )[data-copy='copied'] {
453
+ background-color: color-mix(in oklab, #238551 15%, transparent);
454
+ }
455
+
456
+ .click-to-copy.click-to-copy.click-to-copy.click-to-copy:is(
457
+ td,
458
+ th
459
+ )[data-copy='failed'] {
460
+ background-color: color-mix(in oklab, #cd4246 15%, transparent);
461
+ }
462
+
463
+ @media (hover: hover) {
464
+ .click-to-copy.click-to-copy.click-to-copy.click-to-copy:is(td, th):not(
465
+ [data-copy]
466
+ ):hover {
467
+ background-color: color-mix(in oklab, var(--accent) 12%, transparent);
468
+ }
469
+ }
470
+
471
+ /* The glyph carries the confirmation to the eye; this carries it to a reader. */
472
+ .click-to-copy__status {
473
+ position: absolute;
474
+ width: 1px;
475
+ height: 1px;
476
+ overflow: hidden;
477
+ clip-path: inset(50%);
478
+ white-space: nowrap;
479
+ }
480
+
481
+ @media print {
482
+ .click-to-copy__icon {
483
+ display: none;
484
+ }
485
+ }
486
+
324
487
  /* ── Printing ────────────────────────────────────────────────────────────── */
325
488
 
326
489
  /* Students print the cheatsheet. Let them, without the chrome. */