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
@@ -1,7 +1,4 @@
1
- import {
2
- CHART_SERIES_COLORS,
3
- chartSeriesColor,
4
- } from '../../chart/core/chartPalette.ts';
1
+ import type { OverlaySampleShape } from '../../overlay/core/overlayMarks.ts';
5
2
 
6
3
  /** One line of the card that appears when the pointer rests on a sample. */
7
4
  export interface ProjectionField {
@@ -11,46 +8,72 @@ export interface ProjectionField {
11
8
  value: string;
12
9
  }
13
10
 
14
- /** Who the rows are: what they are called, what they belong to, what is known. */
15
- export interface ProjectionSamples {
11
+ /**
12
+ * One way of sorting the rows into groups — a species, a batch, the cluster a
13
+ * run put each sample in.
14
+ */
15
+ export interface ProjectionGrouping {
16
16
  /**
17
- * A stable name per row, in the score matrix's row order. It is the currency
18
- * of the selection callbacks, so a caller never has to keep a second index.
17
+ * What the options name it by, e.g. `species`. It is kept apart from the
18
+ * label so that a saved choice still finds the grouping once its label is
19
+ * reworded, and it may not be `none`, which is the word for no grouping.
19
20
  */
20
- ids: readonly string[];
21
+ id: string;
22
+ /** What the set of groups is called, for the key and the hover card: `Species`. */
23
+ label: string;
21
24
  /**
22
- * What each row belongs to — a species, a batch, a cluster. A row with no
25
+ * Which group each row is in, in the score matrix's row order. A row with no
23
26
  * group is drawn in the muted ink and left out of every outline.
24
- * @default undefined — every row is one crowd
25
27
  */
26
- groups?: ReadonlyArray<string | undefined>;
28
+ groups: ReadonlyArray<string | undefined>;
27
29
  /**
28
- * The order the groups are listed and coloured in.
30
+ * The order the groups are listed, coloured and shaped in.
29
31
  * @default the order the groups first appear in `groups`
30
32
  */
31
- groupOrder?: readonly string[];
32
- /**
33
- * What the set of groups is called, for the legend's heading and the hover
34
- * card — `Species`, `Cluster`, `Batch`.
35
- * @default 'Group'
36
- */
37
- groupLabel?: string;
33
+ order?: readonly string[];
38
34
  /**
39
35
  * A colour per group. Anything not named here takes the next unused colour
40
36
  * of the shared palette, so two groups never share one.
41
37
  * @default undefined — every group is coloured from the palette
42
38
  */
43
- groupColors?: Readonly<Record<string, string>>;
39
+ colors?: Readonly<Record<string, string>>;
40
+ }
41
+
42
+ /** Who the rows are: what they are called, what they belong to, what is known. */
43
+ export interface ProjectionSamples {
44
+ /**
45
+ * A stable key per row, in the score matrix's row order. It is the currency
46
+ * of the selection callbacks, so a caller never has to keep a second index.
47
+ */
48
+ ids: readonly string[];
49
+ /**
50
+ * What each row is written as: the title of its hover card and the name
51
+ * beside its dot. A page whose keys are not names a reader should read — a
52
+ * database id, a spectrum's uuid — writes its names here and keeps `ids`
53
+ * stable, and two samples may then share a name without either becoming
54
+ * impossible to select.
55
+ * @default the ids
56
+ */
57
+ labels?: readonly string[];
58
+ /**
59
+ * The ways the rows are grouped. The first colours the dots until the reader
60
+ * picks another, and the next one gives them their shapes, so two groupings
61
+ * of the same samples — the clusters a run found and the classes the reader
62
+ * gave — are compared on one picture. Every one of them is written on the
63
+ * hover card.
64
+ * @default undefined — every row is one crowd
65
+ */
66
+ groupings?: readonly ProjectionGrouping[];
44
67
  /**
45
68
  * Everything else worth showing about one row. A callback rather than an
46
69
  * array, so a table of ten thousand rows builds one record when a reader
47
70
  * points at one row, and none otherwise.
48
- * @default undefined — the card shows the id, the group and the two axes
71
+ * @default undefined — the card shows the id, the groups and the two axes
49
72
  */
50
73
  fields?: (index: number) => readonly ProjectionField[];
51
74
  }
52
75
 
53
- /** The groups as the viewer draws them, resolved once per change. */
76
+ /** The grouping that colours the dots, as the viewer draws it. */
54
77
  export interface ResolvedProjectionGroups {
55
78
  /** What the set of groups is called. */
56
79
  label: string;
@@ -69,102 +92,21 @@ export interface ResolvedProjectionGroups {
69
92
  groupOf: Int32Array;
70
93
  }
71
94
 
72
- const NO_GROUP = -1;
73
- const DEFAULT_GROUP_LABEL = 'Group';
74
-
75
- interface GroupEntry {
76
- id: string;
95
+ /** The grouping that shapes the dots, as the viewer draws it. */
96
+ export interface ResolvedProjectionShapes {
97
+ /** What the set of groups is called. */
77
98
  label: string;
78
- color: string;
79
- count: number;
80
- }
81
-
82
- /**
83
- * The groups a set of samples has, in a shape a legend and a plot can both
84
- * read.
85
- *
86
- * A group named in `groupOrder` but held by no row is kept, so a legend does
87
- * not reshuffle when a filter empties one; a row naming a group that is not in
88
- * `groupOrder` is appended in the order it appears.
89
- * @param samples - Who the rows are.
90
- * @param count - How many rows there are, which is the score matrix's height.
91
- * @returns The resolved groups. A set of samples with no `groups` gives no
92
- * entries at all, and every row lands at `-1`.
93
- */
94
- export function resolveProjectionGroups(
95
- samples: ProjectionSamples,
96
- count: number,
97
- ): ResolvedProjectionGroups {
98
- const rowCount = Number.isFinite(count) ? Math.max(0, Math.floor(count)) : 0;
99
- const label = samples.groupLabel ?? DEFAULT_GROUP_LABEL;
100
- const groupOf = new Int32Array(rowCount).fill(NO_GROUP);
101
- const { groups, groupColors, groupOrder } = samples;
102
- if (groups === undefined) return { label, entries: [], groupOf };
103
-
104
- const entries: GroupEntry[] = [];
105
- const indexOfGroup = new Map<string, number>();
106
- if (groupOrder !== undefined) {
107
- for (const id of groupOrder) appendGroup(entries, indexOfGroup, id);
108
- }
109
-
110
- for (let row = 0; row < rowCount; row++) {
111
- const id = groups[row];
112
- if (id === undefined) continue;
113
- const at = appendGroup(entries, indexOfGroup, id);
114
- groupOf[row] = at;
115
- const entry = entries[at];
116
- if (entry !== undefined) entry.count++;
117
- }
118
-
119
- paintGroups(entries, groupColors);
120
- return { label, entries, groupOf };
121
- }
122
-
123
- function appendGroup(
124
- entries: GroupEntry[],
125
- indexOfGroup: Map<string, number>,
126
- id: string,
127
- ): number {
128
- const known = indexOfGroup.get(id);
129
- if (known !== undefined) return known;
130
- const at = entries.length;
131
- indexOfGroup.set(id, at);
132
- entries.push({ id, label: id, color: '', count: 0 });
133
- return at;
134
- }
135
-
136
- function paintGroups(
137
- entries: GroupEntry[],
138
- pinned: Readonly<Record<string, string>> | undefined,
139
- ): void {
140
- // Every pinned colour is taken out of the palette before any group draws
141
- // from it, so pinning the third group's hue onto the first does not leave
142
- // two groups wearing it.
143
- const taken = new Set<string>();
144
- if (pinned !== undefined) {
145
- for (const entry of entries) {
146
- const color = pinned[entry.id];
147
- if (color !== undefined) taken.add(color.toLowerCase());
148
- }
149
- }
150
-
151
- let cursor = 0;
152
- for (const entry of entries) {
153
- const own = pinned?.[entry.id];
154
- if (own !== undefined) {
155
- entry.color = own;
156
- continue;
157
- }
158
- let color = chartSeriesColor(cursor, 'group');
159
- while (
160
- cursor < CHART_SERIES_COLORS.length &&
161
- taken.has(color.toLowerCase())
162
- ) {
163
- cursor++;
164
- color = chartSeriesColor(cursor, 'group');
165
- }
166
- cursor++;
167
- taken.add(color.toLowerCase());
168
- entry.color = color;
169
- }
99
+ /** The groups, in the order they are listed and given their shapes. */
100
+ entries: ReadonlyArray<{
101
+ /** The group's own name, which is also its id. */
102
+ id: string;
103
+ /** What it is called. */
104
+ label: string;
105
+ /** Its shape. */
106
+ shape: OverlaySampleShape;
107
+ /** How many rows are in it. */
108
+ count: number;
109
+ }>;
110
+ /** Which group each row is in, as an index into `entries`, or `-1`. */
111
+ shapeOf: Int32Array;
170
112
  }
@@ -70,6 +70,14 @@ export const PROJECTION_HELP: Record<ProjectionOptionId, ProjectionHelp> = {
70
70
  title: 'Colour by',
71
71
  body: 'What a dot’s colour stands for. Colouring by nothing is useful when the groups are what you are trying to find rather than what you already know.',
72
72
  },
73
+ shapeBy: {
74
+ title: 'Shape by',
75
+ body: 'What a dot’s shape stands for. With colour for one grouping and shape for another, you see at a glance where two ways of sorting the same samples disagree — the clusters a run found against the classes you gave. Past five groups the shapes stop being told apart, so a grouping that large is not offered.',
76
+ example: {
77
+ code: 'Colour = cluster · Shape = class',
78
+ note: 'A square among circles of the same colour is a sample the clustering put with another class.',
79
+ },
80
+ },
73
81
  ellipse: {
74
82
  title: 'Group outlines',
75
83
  body: 'Each outline is drawn to cover about that share of its group’s samples, assuming the group is roughly bell-shaped. It is a summary of where a group sits, not a boundary.',
@@ -95,7 +103,7 @@ export const PROJECTION_HELP: Record<ProjectionOptionId, ProjectionHelp> = {
95
103
  body: 'Writes every sample’s own name beside its dot. It is what turns a dot sitting on its own into something you can go and look up. In a crowd there is only room for some of the names, so the ones that do not fit are left out rather than written over each other.',
96
104
  example: {
97
105
  code: '0326_3a',
98
- note: 'The name is whatever you handed in as the sample’s id, so it is the one you can find again in your own files.',
106
+ note: 'The name is the one the sample was loaded with, so it is the one you can find again in your own files.',
99
107
  },
100
108
  },
101
109
  pairCount: {
@@ -189,6 +197,8 @@ export interface ProjectionBarWords {
189
197
  };
190
198
  /** What "the colour stands for nothing" reads. */
191
199
  uncoloured: string;
200
+ /** What "the shape stands for nothing" reads. */
201
+ unshaped: string;
192
202
  /** What "no outlines at all" reads where there is no room for a sentence. */
193
203
  noOutlines: string;
194
204
  /** What the settings gathered into one chip are called together. */
@@ -218,6 +228,7 @@ export const PROJECTION_BAR_WORDS: ProjectionBarWords = {
218
228
  showIds: 'Show sample ID',
219
229
  },
220
230
  uncoloured: 'Nothing',
231
+ unshaped: 'Nothing',
221
232
  noOutlines: 'None',
222
233
  settings: 'How this is drawn',
223
234
  view: {
@@ -8,8 +8,12 @@ import type {
8
8
  ProjectionOptionId,
9
9
  ProjectionOptions,
10
10
  } from './projectionOptions.ts';
11
- import { DEFAULT_PROJECTION_OPTIONS } from './projectionOptions.ts';
11
+ import {
12
+ DEFAULT_PROJECTION_OPTIONS,
13
+ NO_GROUPING,
14
+ } from './projectionOptions.ts';
12
15
  import type { ProjectionResult } from './projectionResult.ts';
16
+ import type { ProjectionGrouping } from './projectionSamples.ts';
13
17
  import { drawableVariablesView } from './variablesView.ts';
14
18
 
15
19
  /**
@@ -19,15 +23,20 @@ import { drawableVariablesView } from './variablesView.ts';
19
23
  * A saved option outlives the data it was made on: reload with fewer samples
20
24
  * and axis eight no longer exists. Rather than let a plot ask for a column
21
25
  * that is not there, the two axes are pulled back inside what was computed and
22
- * kept apart, the counts are held inside what exists, and a view whose data is
23
- * missing falls back to `weights`.
26
+ * kept apart, the counts are held inside what exists, a view whose data is
27
+ * missing falls back to `weights`, and the colour and the shape name groupings
28
+ * the samples actually carry.
24
29
  * @param overrides - What the caller asked for.
25
30
  * @param result - The reduced space the options will be applied to.
31
+ * @param groupings - The groupings the samples carry.
32
+ * @default undefined — the colour and the shape are left as they were asked
33
+ * for, since nothing says which groupings exist
26
34
  * @returns Every option, complete and safe.
27
35
  */
28
36
  export function resolveProjectionOptions(
29
37
  overrides: Partial<ProjectionOptions> | undefined,
30
38
  result: ProjectionResult,
39
+ groupings?: readonly ProjectionGrouping[],
31
40
  ): ProjectionOptions {
32
41
  const axisCount = result.axes.length;
33
42
  const lastAxis = Math.max(0, axisCount - 1);
@@ -36,6 +45,7 @@ export function resolveProjectionOptions(
36
45
  pick(overrides, 'yAxis'),
37
46
  axisCount,
38
47
  );
48
+ const colorBy = colourChoice(pick(overrides, 'colorBy'), groupings);
39
49
 
40
50
  return {
41
51
  xAxis,
@@ -46,7 +56,8 @@ export function resolveProjectionOptions(
46
56
  lastAxis,
47
57
  ),
48
58
  cloudGesture: pick(overrides, 'cloudGesture'),
49
- colorBy: pick(overrides, 'colorBy'),
59
+ colorBy,
60
+ shapeBy: shapeChoice(pick(overrides, 'shapeBy'), groupings, colorBy),
50
61
  ellipse: pick(overrides, 'ellipse'),
51
62
  pointRadius: atLeast(pick(overrides, 'pointRadius'), 3.5, 0),
52
63
  showGroupMeans: pick(overrides, 'showGroupMeans'),
@@ -90,6 +101,57 @@ function pick<Key extends ProjectionOptionId>(
90
101
  return value === undefined ? DEFAULT_PROJECTION_OPTIONS[key] : value;
91
102
  }
92
103
 
104
+ /**
105
+ * The grouping the colour will actually stand for.
106
+ *
107
+ * A saved choice outlives the data it was made on, so an id no grouping
108
+ * carries — or the empty string, which is how "the usual one" is asked for —
109
+ * falls to the first grouping, while `none` stays none.
110
+ * @param wanted - The id asked for.
111
+ * @param groupings - The groupings the samples carry.
112
+ * @returns A grouping's id, or `none`.
113
+ */
114
+ function colourChoice(
115
+ wanted: string,
116
+ groupings: readonly ProjectionGrouping[] | undefined,
117
+ ): string {
118
+ if (wanted === NO_GROUPING || groupings === undefined) return wanted;
119
+ const ids = groupingIds(groupings);
120
+ return ids.includes(wanted) ? wanted : (ids[0] ?? NO_GROUPING);
121
+ }
122
+
123
+ /**
124
+ * The grouping the shape will actually stand for.
125
+ *
126
+ * The usual one is the second grouping, since the first is the colour's; it is
127
+ * never read as "whichever the colour leaves free", or turning the colour off
128
+ * would suddenly give the dots shapes they did not have. Only when the colour
129
+ * has taken the second does the shape fall back to the first.
130
+ * @param wanted - The id asked for.
131
+ * @param groupings - The groupings the samples carry.
132
+ * @param colorBy - The grouping the colour already stands for.
133
+ * @returns A grouping's id, or `none`.
134
+ */
135
+ function shapeChoice(
136
+ wanted: string,
137
+ groupings: readonly ProjectionGrouping[] | undefined,
138
+ colorBy: string,
139
+ ): string {
140
+ if (wanted === NO_GROUPING || groupings === undefined) return wanted;
141
+ const ids = groupingIds(groupings);
142
+ if (wanted !== colorBy && ids.includes(wanted)) return wanted;
143
+ const [first, second] = ids;
144
+ if (second === undefined) return NO_GROUPING;
145
+ if (second !== colorBy) return second;
146
+ return first ?? NO_GROUPING;
147
+ }
148
+
149
+ function groupingIds(groupings: readonly ProjectionGrouping[]): string[] {
150
+ const ids: string[] = [];
151
+ for (const { id } of groupings) ids.push(id);
152
+ return ids;
153
+ }
154
+
93
155
  /**
94
156
  * An axis that is none of the ones already drawn.
95
157
  *
@@ -27,8 +27,8 @@ import {
27
27
  projectionFoldedCaptionStyle,
28
28
  } from './projectionBarStyles.ts';
29
29
  import type { ProjectionMapView } from './projectionMapView.ts';
30
+ import type { ProjectionStateApi } from './projectionStateApi.ts';
30
31
  import type { ProjectionModels } from './projectionTabModels.ts';
31
- import type { ProjectionStateApi } from './useProjectionState.ts';
32
32
 
33
33
  /** What {@link ProjectionBar} needs. */
34
34
  interface ProjectionBarProps {
@@ -81,7 +81,8 @@ interface ProjectionBarProps {
81
81
  export function ProjectionBar(props: ProjectionBarProps): ReactElement {
82
82
  const { state, result, copy, models, map, baseId, panelId } = props;
83
83
  const { fileName, width } = props;
84
- const { tab, tabs, options, groups, selected, setOptions, setTab } = state;
84
+ const { tab, tabs, options, groups, groupings, selected } = state;
85
+ const { setOptions, setTab } = state;
85
86
 
86
87
  // Read here rather than off the surface below, because the row the bar is
87
88
  // laid into is the surface's own parent: it has to know how tall a control
@@ -119,6 +120,7 @@ export function ProjectionBar(props: ProjectionBarProps): ReactElement {
119
120
  map,
120
121
  options,
121
122
  groups,
123
+ groupings,
122
124
  selected,
123
125
  width,
124
126
  readings,
@@ -0,0 +1,91 @@
1
+ import type { ReactElement } from 'react';
2
+
3
+ import { OverlaySegmented } from '../../overlay/ui/OverlaySegmented.tsx';
4
+ import { OverlaySelect } from '../../overlay/ui/OverlaySelect.tsx';
5
+ import type { ProjectionCopy } from '../core/projectionCopy.ts';
6
+ import type { ProjectionOptions } from '../core/projectionOptions.ts';
7
+ import type { ProjectionGrouping } from '../core/projectionSamples.ts';
8
+
9
+ import {
10
+ projectionColourChoices,
11
+ projectionColourPatch,
12
+ projectionShapeChoices,
13
+ } from './projectionMapChoices.ts';
14
+
15
+ /** What a grouping picker needs. */
16
+ interface ProjectionGroupingPickerProps {
17
+ /** What the figure is showing, already resolved against the result. */
18
+ options: ProjectionOptions;
19
+ /** Called with only the options that changed. */
20
+ onChange: (patch: Partial<ProjectionOptions>) => void;
21
+ /** The words the viewer writes. */
22
+ copy: ProjectionCopy;
23
+ /** Every grouping the samples carry. */
24
+ groupings: readonly ProjectionGrouping[];
25
+ /** What the setting is called beside the control. */
26
+ label: string;
27
+ }
28
+
29
+ /** What the shape picker needs on top of what every grouping picker takes. */
30
+ interface ProjectionShapePickerProps extends ProjectionGroupingPickerProps {
31
+ /** How many rows there are, which is what a grouping is cut over. */
32
+ rows: number;
33
+ }
34
+
35
+ /**
36
+ * What the colour stands for, as segments while the choices fit on one row
37
+ * and as a list past that.
38
+ * @param props - See {@link ProjectionGroupingPickerProps}.
39
+ * @returns The picker, greyed while the samples carry no grouping.
40
+ */
41
+ export function ProjectionColourPicker(
42
+ props: ProjectionGroupingPickerProps,
43
+ ): ReactElement {
44
+ const { options, onChange, copy, groupings, label } = props;
45
+ const choices = projectionColourChoices(groupings, copy.bar.uncoloured);
46
+ const Picker =
47
+ choices.length > MOST_SEGMENTS ? OverlaySelect : OverlaySegmented;
48
+
49
+ return (
50
+ <Picker
51
+ label={label}
52
+ help={copy.help.colorBy}
53
+ value={options.colorBy}
54
+ disabled={groupings.length === 0}
55
+ options={choices}
56
+ onChange={(colorBy: string) =>
57
+ onChange(projectionColourPatch(options, colorBy))
58
+ }
59
+ />
60
+ );
61
+ }
62
+
63
+ /**
64
+ * What the shape stands for. It is always a list, so a grouping that cannot be
65
+ * shaped keeps its name, greyed, with the reason on the pointer.
66
+ * @param props - See {@link ProjectionShapePickerProps}.
67
+ * @returns The picker, greyed while the samples carry no grouping.
68
+ */
69
+ export function ProjectionShapePicker(
70
+ props: ProjectionShapePickerProps,
71
+ ): ReactElement {
72
+ const { options, onChange, copy, groupings, label, rows } = props;
73
+
74
+ return (
75
+ <OverlaySelect
76
+ label={label}
77
+ help={copy.help.shapeBy}
78
+ value={options.shapeBy}
79
+ disabled={groupings.length === 0}
80
+ options={projectionShapeChoices(groupings, options, rows, copy)}
81
+ onChange={(shapeBy) => onChange({ shapeBy })}
82
+ />
83
+ );
84
+ }
85
+
86
+ /**
87
+ * How many choices a row of segments holds before it is written as a list: a
88
+ * segment carries a grouping's whole name, and past three they no longer fit
89
+ * the width of a panel.
90
+ */
91
+ const MOST_SEGMENTS = 3;
@@ -2,16 +2,15 @@ import type { ReactElement } from 'react';
2
2
 
3
3
  import type { OverlayTier } from '../../overlay/core/overlayTiers.ts';
4
4
  import { OverlayChip } from '../../overlay/ui/OverlayChip.tsx';
5
- import { OverlaySegmented } from '../../overlay/ui/OverlaySegmented.tsx';
6
5
  import { OverlaySelect } from '../../overlay/ui/OverlaySelect.tsx';
7
6
  import { OverlayToggle } from '../../overlay/ui/OverlayToggle.tsx';
8
7
  import { OverlayValueMenu } from '../../overlay/ui/OverlayValueMenu.tsx';
9
8
  import type { ProjectionCopy } from '../core/projectionCopy.ts';
10
- import type {
11
- ProjectionColorBy,
12
- ProjectionOptions,
13
- } from '../core/projectionOptions.ts';
9
+ import type { ProjectionOptions } from '../core/projectionOptions.ts';
10
+ import { NO_GROUPING } from '../core/projectionOptions.ts';
11
+ import type { ProjectionGrouping } from '../core/projectionSamples.ts';
14
12
 
13
+ import { ProjectionColourPicker } from './ProjectionGroupingPickers.tsx';
15
14
  import type { ProjectionReading } from './projectionBarReadings.ts';
16
15
  import { projectionChipSettings } from './projectionBarReadings.ts';
17
16
  import {
@@ -21,6 +20,7 @@ import {
21
20
  } from './projectionEllipse.ts';
22
21
  import {
23
22
  projectionColourChoices,
23
+ projectionColourPatch,
24
24
  projectionOutlineChoices,
25
25
  } from './projectionMapChoices.ts';
26
26
 
@@ -32,15 +32,13 @@ export interface ProjectionMapControlsProps {
32
32
  onChange: (patch: Partial<ProjectionOptions>) => void;
33
33
  /** The words the bar writes, already merged over the defaults. */
34
34
  copy: ProjectionCopy;
35
- /** What the set of groups is called, which is what `Colour by` offers. */
36
- groupLabel: string;
37
35
  /**
38
- * Whether the samples carry groups at all. Without them the colour and the
39
- * outlines stand for nothing, so their controls are greyed rather than
40
- * removed: a reader who cannot find a control assumes the figure has none.
41
- * @default false
36
+ * Every grouping the samples carry, which is what `Colour by` offers.
37
+ * Without one the colour and the outlines stand for nothing, so their
38
+ * controls are greyed rather than removed: a reader who cannot find a
39
+ * control assumes the figure has none.
42
40
  */
43
- hasGroups?: boolean;
41
+ groupings: readonly ProjectionGrouping[];
44
42
  /**
45
43
  * What the two settings currently read, with the figure's own colours. They
46
44
  * are built once for the bar and handed down rather than spelled again here,
@@ -88,18 +86,18 @@ export interface ProjectionMapControlsProps {
88
86
  export function ProjectionMapControls(
89
87
  props: ProjectionMapControlsProps,
90
88
  ): ReactElement {
91
- const { options, onChange, copy, groupLabel, hasGroups = false } = props;
89
+ const { options, onChange, copy, groupings } = props;
92
90
  const { readings = NO_READINGS, tier = 'full' } = props;
93
91
  const { bar, help, reason } = copy;
94
92
 
95
- const uncoloured = !hasGroups || options.colorBy === 'none';
93
+ const hasGroups = groupings.length > 0;
94
+ const uncoloured = !hasGroups || options.colorBy === NO_GROUPING;
96
95
  const settings = (
97
96
  <MapSettings
98
97
  options={options}
99
98
  onChange={onChange}
100
99
  copy={copy}
101
- groupLabel={groupLabel}
102
- hasGroups={hasGroups}
100
+ groupings={groupings}
103
101
  />
104
102
  );
105
103
 
@@ -142,7 +140,7 @@ export function ProjectionMapControls(
142
140
 
143
141
  return (
144
142
  <>
145
- <OverlayValueMenu<ProjectionColorBy>
143
+ <OverlayValueMenu
146
144
  label={help.colorBy.title}
147
145
  keyWord={bar.key.colorBy}
148
146
  showKey={tier === 'full'}
@@ -150,8 +148,10 @@ export function ProjectionMapControls(
150
148
  swatches={swatchesOf(readings)}
151
149
  disabled={!hasGroups}
152
150
  disabledReason={reason.noGroups}
153
- options={projectionColourChoices(groupLabel, bar.uncoloured)}
154
- onChange={(colorBy) => onChange({ colorBy })}
151
+ options={projectionColourChoices(groupings, bar.uncoloured)}
152
+ onChange={(colorBy) =>
153
+ onChange(projectionColourPatch(options, colorBy))
154
+ }
155
155
  />
156
156
  <OverlayValueMenu
157
157
  label={help.ellipse.title}
@@ -191,10 +191,8 @@ interface MapSettingsProps {
191
191
  onChange: (patch: Partial<ProjectionOptions>) => void;
192
192
  /** The words the panel writes. */
193
193
  copy: ProjectionCopy;
194
- /** What the set of groups is called. */
195
- groupLabel: string;
196
- /** Whether the samples carry groups at all. */
197
- hasGroups: boolean;
194
+ /** Every grouping the samples carry. */
195
+ groupings: readonly ProjectionGrouping[];
198
196
  }
199
197
 
200
198
  /**
@@ -207,24 +205,23 @@ interface MapSettingsProps {
207
205
  * @returns The captioned controls.
208
206
  */
209
207
  function MapSettings(props: MapSettingsProps): ReactElement {
210
- const { options, onChange, copy, groupLabel, hasGroups } = props;
211
- const { bar, help, outline } = copy;
208
+ const { options, onChange, copy, groupings } = props;
209
+ const { help, outline } = copy;
212
210
 
213
211
  return (
214
212
  <>
215
- <OverlaySegmented<ProjectionColorBy>
213
+ <ProjectionColourPicker
216
214
  label={help.colorBy.title}
217
- help={help.colorBy}
218
- value={options.colorBy}
219
- disabled={!hasGroups}
220
- options={projectionColourChoices(groupLabel, bar.uncoloured)}
221
- onChange={(colorBy) => onChange({ colorBy })}
215
+ options={options}
216
+ onChange={onChange}
217
+ copy={copy}
218
+ groupings={groupings}
222
219
  />
223
220
  <OverlaySelect
224
221
  label={help.ellipse.title}
225
222
  help={help.ellipse}
226
223
  value={ellipseKey(options.ellipse)}
227
- disabled={!hasGroups || options.colorBy === 'none'}
224
+ disabled={groupings.length === 0 || options.colorBy === NO_GROUPING}
228
225
  options={ellipseChoices(options.ellipse, outline)}
229
226
  onChange={(value) => onChange({ ellipse: ellipseSize(value) })}
230
227
  />
@@ -3,8 +3,8 @@ import type { ReactElement } from 'react';
3
3
  import type { OverlayLegendEntry } from '../../overlay/ui/OverlayLegend.tsx';
4
4
  import { OverlayLegend } from '../../overlay/ui/OverlayLegend.tsx';
5
5
 
6
+ import { projectionMapCorner } from './projectionMapCorner.ts';
6
7
  import type { ProjectionMapCloud } from './projectionMapModel.ts';
7
- import { projectionMapCorner } from './projectionMapModel.ts';
8
8
  import {
9
9
  PROJECTION_PLOT_AREA_STYLE,
10
10
  PROJECTION_PLOT_ROOM,