react-cheminfo 0.22.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 (206) hide show
  1. package/README.md +13 -3
  2. package/lib/overlay/core/index.d.ts +2 -2
  3. package/lib/overlay/core/index.d.ts.map +1 -1
  4. package/lib/overlay/core/index.js +1 -1
  5. package/lib/overlay/core/index.js.map +1 -1
  6. package/lib/overlay/core/overlayMarks.d.ts +20 -5
  7. package/lib/overlay/core/overlayMarks.d.ts.map +1 -1
  8. package/lib/overlay/core/overlayMarks.js +31 -5
  9. package/lib/overlay/core/overlayMarks.js.map +1 -1
  10. package/lib/overlay/ui/OverlayLegendMark.js +5 -0
  11. package/lib/overlay/ui/OverlayLegendMark.js.map +1 -1
  12. package/lib/projection/core/index.d.ts +4 -4
  13. package/lib/projection/core/index.d.ts.map +1 -1
  14. package/lib/projection/core/index.js +2 -2
  15. package/lib/projection/core/index.js.map +1 -1
  16. package/lib/projection/core/projectionCopy.d.ts +2 -0
  17. package/lib/projection/core/projectionCopy.d.ts.map +1 -1
  18. package/lib/projection/core/projectionCopy.js +1 -0
  19. package/lib/projection/core/projectionCopy.js.map +1 -1
  20. package/lib/projection/core/projectionGroupings.d.ts +44 -0
  21. package/lib/projection/core/projectionGroupings.d.ts.map +1 -0
  22. package/lib/projection/core/projectionGroupings.js +132 -0
  23. package/lib/projection/core/projectionGroupings.js.map +1 -0
  24. package/lib/projection/core/projectionOptions.d.ts +16 -6
  25. package/lib/projection/core/projectionOptions.d.ts.map +1 -1
  26. package/lib/projection/core/projectionOptions.js +7 -1
  27. package/lib/projection/core/projectionOptions.js.map +1 -1
  28. package/lib/projection/core/projectionPanelWords.d.ts +6 -0
  29. package/lib/projection/core/projectionPanelWords.d.ts.map +1 -1
  30. package/lib/projection/core/projectionPanelWords.js +4 -0
  31. package/lib/projection/core/projectionPanelWords.js.map +1 -1
  32. package/lib/projection/core/projectionSamples.d.ts +63 -32
  33. package/lib/projection/core/projectionSamples.d.ts.map +1 -1
  34. package/lib/projection/core/projectionSamples.js +1 -80
  35. package/lib/projection/core/projectionSamples.js.map +1 -1
  36. package/lib/projection/core/projectionStrings.d.ts +2 -0
  37. package/lib/projection/core/projectionStrings.d.ts.map +1 -1
  38. package/lib/projection/core/projectionStrings.js +10 -1
  39. package/lib/projection/core/projectionStrings.js.map +1 -1
  40. package/lib/projection/core/resolveProjectionOptions.d.ts +8 -3
  41. package/lib/projection/core/resolveProjectionOptions.d.ts.map +1 -1
  42. package/lib/projection/core/resolveProjectionOptions.js +58 -5
  43. package/lib/projection/core/resolveProjectionOptions.js.map +1 -1
  44. package/lib/projection/ui/ProjectionBar.d.ts +1 -1
  45. package/lib/projection/ui/ProjectionBar.d.ts.map +1 -1
  46. package/lib/projection/ui/ProjectionBar.js +3 -1
  47. package/lib/projection/ui/ProjectionBar.js.map +1 -1
  48. package/lib/projection/ui/ProjectionGroupingPickers.d.ts +38 -0
  49. package/lib/projection/ui/ProjectionGroupingPickers.d.ts.map +1 -0
  50. package/lib/projection/ui/ProjectionGroupingPickers.js +33 -0
  51. package/lib/projection/ui/ProjectionGroupingPickers.js.map +1 -0
  52. package/lib/projection/ui/ProjectionMapControls.d.ts +6 -7
  53. package/lib/projection/ui/ProjectionMapControls.d.ts.map +1 -1
  54. package/lib/projection/ui/ProjectionMapControls.js +11 -9
  55. package/lib/projection/ui/ProjectionMapControls.js.map +1 -1
  56. package/lib/projection/ui/ProjectionMapLegend.d.ts.map +1 -1
  57. package/lib/projection/ui/ProjectionMapLegend.js +1 -1
  58. package/lib/projection/ui/ProjectionMapLegend.js.map +1 -1
  59. package/lib/projection/ui/ProjectionMapMore.d.ts +2 -2
  60. package/lib/projection/ui/ProjectionMapMore.d.ts.map +1 -1
  61. package/lib/projection/ui/ProjectionMapMore.js +7 -5
  62. package/lib/projection/ui/ProjectionMapMore.js.map +1 -1
  63. package/lib/projection/ui/ProjectionMapTab.d.ts +15 -6
  64. package/lib/projection/ui/ProjectionMapTab.d.ts.map +1 -1
  65. package/lib/projection/ui/ProjectionMapTab.js +11 -7
  66. package/lib/projection/ui/ProjectionMapTab.js.map +1 -1
  67. package/lib/projection/ui/ProjectionPairsMore.d.ts +3 -7
  68. package/lib/projection/ui/ProjectionPairsMore.d.ts.map +1 -1
  69. package/lib/projection/ui/ProjectionPairsMore.js +4 -5
  70. package/lib/projection/ui/ProjectionPairsMore.js.map +1 -1
  71. package/lib/projection/ui/ProjectionPairsTab.d.ts.map +1 -1
  72. package/lib/projection/ui/ProjectionPairsTab.js +2 -2
  73. package/lib/projection/ui/ProjectionPairsTab.js.map +1 -1
  74. package/lib/projection/ui/ProjectionPanel.d.ts +1 -1
  75. package/lib/projection/ui/ProjectionPanel.d.ts.map +1 -1
  76. package/lib/projection/ui/ProjectionPanel.js +1 -1
  77. package/lib/projection/ui/ProjectionPanel.js.map +1 -1
  78. package/lib/projection/ui/ProjectionReadout.d.ts +7 -1
  79. package/lib/projection/ui/ProjectionReadout.d.ts.map +1 -1
  80. package/lib/projection/ui/ProjectionReadout.js +19 -7
  81. package/lib/projection/ui/ProjectionReadout.js.map +1 -1
  82. package/lib/projection/ui/ProjectionSpaceControls.d.ts +3 -5
  83. package/lib/projection/ui/ProjectionSpaceControls.d.ts.map +1 -1
  84. package/lib/projection/ui/ProjectionSpaceControls.js +9 -7
  85. package/lib/projection/ui/ProjectionSpaceControls.js.map +1 -1
  86. package/lib/projection/ui/ProjectionSpaceMore.d.ts +2 -2
  87. package/lib/projection/ui/ProjectionSpaceMore.d.ts.map +1 -1
  88. package/lib/projection/ui/ProjectionSpaceMore.js +5 -4
  89. package/lib/projection/ui/ProjectionSpaceMore.js.map +1 -1
  90. package/lib/projection/ui/ProjectionSpaceTab.d.ts +11 -3
  91. package/lib/projection/ui/ProjectionSpaceTab.d.ts.map +1 -1
  92. package/lib/projection/ui/ProjectionSpaceTab.js +7 -4
  93. package/lib/projection/ui/ProjectionSpaceTab.js.map +1 -1
  94. package/lib/projection/ui/ProjectionViewer.d.ts.map +1 -1
  95. package/lib/projection/ui/ProjectionViewer.js +1 -0
  96. package/lib/projection/ui/ProjectionViewer.js.map +1 -1
  97. package/lib/projection/ui/projectionBarReadings.d.ts +4 -1
  98. package/lib/projection/ui/projectionBarReadings.d.ts.map +1 -1
  99. package/lib/projection/ui/projectionBarReadings.js +14 -4
  100. package/lib/projection/ui/projectionBarReadings.js.map +1 -1
  101. package/lib/projection/ui/projectionBarSlots.d.ts +4 -2
  102. package/lib/projection/ui/projectionBarSlots.d.ts.map +1 -1
  103. package/lib/projection/ui/projectionBarSlots.js +3 -7
  104. package/lib/projection/ui/projectionBarSlots.js.map +1 -1
  105. package/lib/projection/ui/projectionMapChoices.d.ts +31 -6
  106. package/lib/projection/ui/projectionMapChoices.d.ts.map +1 -1
  107. package/lib/projection/ui/projectionMapChoices.js +58 -9
  108. package/lib/projection/ui/projectionMapChoices.js.map +1 -1
  109. package/lib/projection/ui/projectionMapChrome.d.ts +3 -9
  110. package/lib/projection/ui/projectionMapChrome.d.ts.map +1 -1
  111. package/lib/projection/ui/projectionMapChrome.js +41 -27
  112. package/lib/projection/ui/projectionMapChrome.js.map +1 -1
  113. package/lib/projection/ui/projectionMapCorner.d.ts +25 -0
  114. package/lib/projection/ui/projectionMapCorner.d.ts.map +1 -0
  115. package/lib/projection/ui/projectionMapCorner.js +46 -0
  116. package/lib/projection/ui/projectionMapCorner.js.map +1 -0
  117. package/lib/projection/ui/projectionMapModel.d.ts +9 -18
  118. package/lib/projection/ui/projectionMapModel.d.ts.map +1 -1
  119. package/lib/projection/ui/projectionMapModel.js +14 -39
  120. package/lib/projection/ui/projectionMapModel.js.map +1 -1
  121. package/lib/projection/ui/projectionMapView.d.ts +3 -1
  122. package/lib/projection/ui/projectionMapView.d.ts.map +1 -1
  123. package/lib/projection/ui/projectionMapView.js +9 -4
  124. package/lib/projection/ui/projectionMapView.js.map +1 -1
  125. package/lib/projection/ui/projectionSelectionSentence.d.ts +10 -0
  126. package/lib/projection/ui/projectionSelectionSentence.d.ts.map +1 -0
  127. package/lib/projection/ui/projectionSelectionSentence.js +18 -0
  128. package/lib/projection/ui/projectionSelectionSentence.js.map +1 -0
  129. package/lib/projection/ui/projectionStateApi.d.ts +47 -0
  130. package/lib/projection/ui/projectionStateApi.d.ts.map +1 -0
  131. package/lib/projection/ui/projectionStateApi.js +9 -0
  132. package/lib/projection/ui/projectionStateApi.js.map +1 -0
  133. package/lib/projection/ui/projectionTabModels.js +1 -1
  134. package/lib/projection/ui/projectionTabModels.js.map +1 -1
  135. package/lib/projection/ui/useProjectionState.d.ts +1 -29
  136. package/lib/projection/ui/useProjectionState.d.ts.map +1 -1
  137. package/lib/projection/ui/useProjectionState.js +13 -5
  138. package/lib/projection/ui/useProjectionState.js.map +1 -1
  139. package/lib/scatter/core/index.d.ts +1 -0
  140. package/lib/scatter/core/index.d.ts.map +1 -1
  141. package/lib/scatter/core/index.js +1 -0
  142. package/lib/scatter/core/index.js.map +1 -1
  143. package/lib/scatter/core/pointShape.d.ts +18 -0
  144. package/lib/scatter/core/pointShape.d.ts.map +1 -0
  145. package/lib/scatter/core/pointShape.js +70 -0
  146. package/lib/scatter/core/pointShape.js.map +1 -0
  147. package/lib/scatter/ui/ScatterPlot.d.ts.map +1 -1
  148. package/lib/scatter/ui/ScatterPlot.js +3 -2
  149. package/lib/scatter/ui/ScatterPlot.js.map +1 -1
  150. package/lib/scatter/ui/ScatterPointLayer.d.ts +14 -1
  151. package/lib/scatter/ui/ScatterPointLayer.d.ts.map +1 -1
  152. package/lib/scatter/ui/ScatterPointLayer.js +15 -3
  153. package/lib/scatter/ui/ScatterPointLayer.js.map +1 -1
  154. package/lib/scatter/ui/scatterFigureProps.d.ts +13 -0
  155. package/lib/scatter/ui/scatterFigureProps.d.ts.map +1 -1
  156. package/lib/scatter/ui/scatterMarkGlyph.d.ts +2 -1
  157. package/lib/scatter/ui/scatterMarkGlyph.d.ts.map +1 -1
  158. package/lib/scatter/ui/scatterMarkGlyph.js +7 -1
  159. package/lib/scatter/ui/scatterMarkGlyph.js.map +1 -1
  160. package/lib/scatter3d/ui/ScatterCloud.d.ts.map +1 -1
  161. package/lib/scatter3d/ui/ScatterCloud.js +2 -1
  162. package/lib/scatter3d/ui/ScatterCloud.js.map +1 -1
  163. package/package.json +1 -1
  164. package/src/overlay/core/index.ts +6 -2
  165. package/src/overlay/core/overlayMarks.ts +51 -6
  166. package/src/overlay/ui/OverlayLegendMark.tsx +5 -0
  167. package/src/projection/core/index.ts +11 -3
  168. package/src/projection/core/projectionCopy.ts +3 -0
  169. package/src/projection/core/projectionGroupings.ts +174 -0
  170. package/src/projection/core/projectionOptions.ts +18 -7
  171. package/src/projection/core/projectionPanelWords.ts +10 -0
  172. package/src/projection/core/projectionSamples.ts +62 -120
  173. package/src/projection/core/projectionStrings.ts +12 -1
  174. package/src/projection/core/resolveProjectionOptions.ts +66 -4
  175. package/src/projection/ui/ProjectionBar.tsx +4 -2
  176. package/src/projection/ui/ProjectionGroupingPickers.tsx +91 -0
  177. package/src/projection/ui/ProjectionMapControls.tsx +29 -32
  178. package/src/projection/ui/ProjectionMapLegend.tsx +1 -1
  179. package/src/projection/ui/ProjectionMapMore.tsx +14 -4
  180. package/src/projection/ui/ProjectionMapTab.tsx +27 -9
  181. package/src/projection/ui/ProjectionPairsMore.tsx +12 -21
  182. package/src/projection/ui/ProjectionPairsTab.tsx +5 -2
  183. package/src/projection/ui/ProjectionPanel.tsx +7 -3
  184. package/src/projection/ui/ProjectionReadout.tsx +28 -8
  185. package/src/projection/ui/ProjectionSpaceControls.tsx +20 -27
  186. package/src/projection/ui/ProjectionSpaceMore.tsx +12 -3
  187. package/src/projection/ui/ProjectionSpaceTab.tsx +23 -6
  188. package/src/projection/ui/ProjectionViewer.tsx +1 -0
  189. package/src/projection/ui/projectionBarReadings.ts +24 -8
  190. package/src/projection/ui/projectionBarSlots.tsx +10 -9
  191. package/src/projection/ui/projectionMapChoices.ts +71 -14
  192. package/src/projection/ui/projectionMapChrome.ts +55 -30
  193. package/src/projection/ui/projectionMapCorner.ts +67 -0
  194. package/src/projection/ui/projectionMapModel.ts +20 -59
  195. package/src/projection/ui/projectionMapView.ts +15 -8
  196. package/src/projection/ui/projectionSelectionSentence.ts +22 -0
  197. package/src/projection/ui/projectionStateApi.ts +57 -0
  198. package/src/projection/ui/projectionTabModels.ts +1 -1
  199. package/src/projection/ui/useProjectionState.ts +33 -37
  200. package/src/scatter/core/index.ts +1 -0
  201. package/src/scatter/core/pointShape.ts +80 -0
  202. package/src/scatter/ui/ScatterPlot.tsx +4 -1
  203. package/src/scatter/ui/ScatterPointLayer.tsx +39 -11
  204. package/src/scatter/ui/scatterFigureProps.ts +13 -0
  205. package/src/scatter/ui/scatterMarkGlyph.tsx +9 -1
  206. package/src/scatter3d/ui/ScatterCloud.tsx +3 -0
@@ -13,6 +13,7 @@ import type { OverlayMetrics } from '../../overlay/core/overlayMetrics.ts';
13
13
  import type { OverlayChipSetting } from '../../overlay/ui/OverlayChip.tsx';
14
14
  import type { ProjectionCopy } from '../core/projectionCopy.ts';
15
15
  import type { ProjectionOptions } from '../core/projectionOptions.ts';
16
+ import { NO_GROUPING } from '../core/projectionOptions.ts';
16
17
  import type { ResolvedProjectionGroups } from '../core/projectionSamples.ts';
17
18
  import type { ProjectionTab } from '../core/projectionTabs.ts';
18
19
 
@@ -48,7 +49,10 @@ interface ProjectionReadingsInput {
48
49
  copy: ProjectionCopy;
49
50
  /** What the figure is showing, already made safe against the result. */
50
51
  options: ProjectionOptions;
51
- /** The groups as every figure draws them, whose colours the map writes. */
52
+ /**
53
+ * The grouping the figures are coloured by, whose colours the map writes;
54
+ * empty while nothing colours them.
55
+ */
52
56
  groups: ResolvedProjectionGroups;
53
57
  }
54
58
 
@@ -83,10 +87,9 @@ export function projectionBarReadings(
83
87
  ];
84
88
  }
85
89
  if (tab === 'space') {
86
- const coverage =
87
- options.colorBy === 'group' && groups.entries.length > 0
88
- ? ellipseCoverageText(options.ellipse, 'space')
89
- : '';
90
+ const coverage = isColoured(options, groups)
91
+ ? ellipseCoverageText(options.ellipse, 'space')
92
+ : '';
90
93
  return [
91
94
  {
92
95
  key: bar.key.cloudGesture,
@@ -163,14 +166,14 @@ export function projectionChipSettings(
163
166
  * Empty while the colour stands for nothing, so a bar that says `Nothing`
164
167
  * does not carry a row of dots contradicting it.
165
168
  * @param options - What the figure is showing.
166
- * @param groups - The groups as every figure draws them.
169
+ * @param groups - The grouping the figures are coloured by.
167
170
  * @returns The colours, in the order the figure draws them.
168
171
  */
169
172
  function projectionGroupColors(
170
173
  options: ProjectionOptions,
171
174
  groups: ResolvedProjectionGroups,
172
175
  ): readonly string[] {
173
- if (options.colorBy !== 'group') return NO_COLORS;
176
+ if (!isColoured(options, groups)) return NO_COLORS;
174
177
  const colors: string[] = [];
175
178
  for (const entry of groups.entries) colors.push(entry.color);
176
179
  return colors;
@@ -195,7 +198,7 @@ function mapReadings(
195
198
  groups: ResolvedProjectionGroups,
196
199
  ): readonly ProjectionReading[] {
197
200
  const { bar, help } = copy;
198
- const colored = options.colorBy === 'group';
201
+ const colored = isColoured(options, groups);
199
202
  const coverage = ellipseCoverageText(options.ellipse);
200
203
  return [
201
204
  {
@@ -212,5 +215,18 @@ function mapReadings(
212
215
  ];
213
216
  }
214
217
 
218
+ /**
219
+ * Whether the colour stands for a grouping at all.
220
+ * @param options - What the figure is showing.
221
+ * @param groups - The grouping the figures are coloured by.
222
+ * @returns `true` when the dots are painted by group.
223
+ */
224
+ function isColoured(
225
+ options: ProjectionOptions,
226
+ groups: ResolvedProjectionGroups,
227
+ ): boolean {
228
+ return options.colorBy !== NO_GROUPING && groups.entries.length > 0;
229
+ }
230
+
215
231
  /** No colours at all, which is what an uncoloured map paints. */
216
232
  const NO_COLORS: readonly string[] = [];
@@ -4,7 +4,10 @@ import type { OverlayTier } from '../../overlay/core/overlayTiers.ts';
4
4
  import type { ProjectionCopy } from '../core/projectionCopy.ts';
5
5
  import type { ProjectionOptions } from '../core/projectionOptions.ts';
6
6
  import type { ProjectionResult } from '../core/projectionResult.ts';
7
- import type { ResolvedProjectionGroups } from '../core/projectionSamples.ts';
7
+ import type {
8
+ ProjectionGrouping,
9
+ ResolvedProjectionGroups,
10
+ } from '../core/projectionSamples.ts';
8
11
  import type { ProjectionTab } from '../core/projectionTabs.ts';
9
12
 
10
13
  import { ProjectionMapControls } from './ProjectionMapControls.tsx';
@@ -50,8 +53,10 @@ interface ProjectionBarSlotsInput {
50
53
  map: ProjectionMapView;
51
54
  /** Every option, already made safe against the result. */
52
55
  options: ProjectionOptions;
53
- /** The groups as every figure draws them. */
56
+ /** The grouping the figures are coloured by; empty while nothing colours them. */
54
57
  groups: ResolvedProjectionGroups;
58
+ /** Every grouping the samples carry, which the pickers choose between. */
59
+ groupings: readonly ProjectionGrouping[];
55
60
  /** The selected rows, as indices. */
56
61
  selected: readonly number[];
57
62
  /** Width of the viewer, in pixels. */
@@ -79,16 +84,14 @@ export function projectionBarSlots(
79
84
  input: ProjectionBarSlotsInput,
80
85
  ): ProjectionBarSlots {
81
86
  const { tab, result, copy, models, map, readings, tier } = input;
82
- const { options, groups, selected, width, onChange } = input;
87
+ const { options, groups, groupings, selected, width, onChange } = input;
83
88
 
84
- const hasGroups = groups.entries.length > 0;
85
- const colored = options.colorBy === 'group' && hasGroups;
86
- const shared = { options, onChange, copy, groupLabel: groups.label };
89
+ const colored = groups.entries.length > 0;
90
+ const shared = { options, onChange, copy, groupings };
87
91
 
88
92
  if (tab === 'pairs') {
89
93
  const pairs = {
90
94
  ...shared,
91
- hasGroups,
92
95
  axisCount: result.axes.length,
93
96
  width,
94
97
  };
@@ -102,7 +105,6 @@ export function projectionBarSlots(
102
105
  const space = {
103
106
  ...shared,
104
107
  result,
105
- hasGroups,
106
108
  selectedCount: selected.length,
107
109
  onResetView: map.resetSpaceView,
108
110
  onClearSelection: map.clearSelection,
@@ -144,7 +146,6 @@ export function projectionBarSlots(
144
146
  const forMap = {
145
147
  ...shared,
146
148
  result,
147
- hasGroups,
148
149
  selectedCount: selected.length,
149
150
  onZoomToSelection: map.zoomToSelection,
150
151
  onResetView: map.resetView,
@@ -1,5 +1,5 @@
1
1
  /**
2
- * What the map's two settings offer, in the two lengths the bar needs.
2
+ * What the map's settings offer, in the lengths the bar needs.
3
3
  *
4
4
  * The same choices are written twice — once as bare shares on a value menu
5
5
  * whose own heading already says what they are shares of, and once as whole
@@ -9,10 +9,10 @@
9
9
 
10
10
  import type { OverlayOption } from '../../overlay/ui/OverlayRow.tsx';
11
11
  import type { ProjectionCopy } from '../core/projectionCopy.ts';
12
- import type {
13
- ProjectionColorBy,
14
- ProjectionOptions,
15
- } from '../core/projectionOptions.ts';
12
+ import { resolveProjectionShapes } from '../core/projectionGroupings.ts';
13
+ import type { ProjectionOptions } from '../core/projectionOptions.ts';
14
+ import { NO_GROUPING } from '../core/projectionOptions.ts';
15
+ import type { ProjectionGrouping } from '../core/projectionSamples.ts';
16
16
 
17
17
  import {
18
18
  ellipseChoices,
@@ -21,19 +21,76 @@ import {
21
21
  } from './projectionEllipse.ts';
22
22
 
23
23
  /**
24
- * What the colour may stand for.
25
- * @param groupLabel - What the set of groups is called.
24
+ * What the colour may stand for: each grouping the samples carry, or nothing.
25
+ * @param groupings - The groupings the samples carry.
26
26
  * @param uncoloured - What "the colour stands for nothing" reads.
27
- * @returns The two choices.
27
+ * @returns The choices, in the order the groupings were handed in.
28
28
  */
29
29
  export function projectionColourChoices(
30
- groupLabel: string,
30
+ groupings: readonly ProjectionGrouping[],
31
31
  uncoloured: string,
32
- ): ReadonlyArray<OverlayOption<ProjectionColorBy>> {
33
- return [
34
- { value: 'group', label: groupLabel },
35
- { value: 'none', label: uncoloured },
36
- ];
32
+ ): readonly OverlayOption[] {
33
+ const choices: OverlayOption[] = [];
34
+ for (const { id, label } of groupings) choices.push({ value: id, label });
35
+ choices.push({ value: NO_GROUPING, label: uncoloured });
36
+ return choices;
37
+ }
38
+
39
+ /**
40
+ * What the shape may stand for: each grouping the samples carry, or nothing.
41
+ *
42
+ * A grouping the colour already stands for is kept in the list but greyed, as
43
+ * is one of more groups than there are shapes, so the reader sees why it cannot
44
+ * be picked rather than wondering where it went.
45
+ * @param groupings - The groupings the samples carry.
46
+ * @param options - What the figure is showing, whose colour is taken.
47
+ * @param rows - How many rows there are, which is what a grouping is cut over.
48
+ * @param copy - The words the viewer writes.
49
+ * @returns The choices, in the order the groupings were handed in.
50
+ */
51
+ export function projectionShapeChoices(
52
+ groupings: readonly ProjectionGrouping[],
53
+ options: ProjectionOptions,
54
+ rows: number,
55
+ copy: ProjectionCopy,
56
+ ): readonly OverlayOption[] {
57
+ const choices: OverlayOption[] = [];
58
+ for (const grouping of groupings) {
59
+ const { id, label } = grouping;
60
+ const reason =
61
+ id === options.colorBy
62
+ ? copy.reason.shapeIsColour
63
+ : resolveProjectionShapes(grouping, rows) === null
64
+ ? copy.reason.tooManyShapes
65
+ : undefined;
66
+ choices.push({
67
+ value: id,
68
+ label,
69
+ disabled: reason !== undefined,
70
+ title: reason,
71
+ });
72
+ }
73
+ choices.push({ value: NO_GROUPING, label: copy.bar.unshaped });
74
+ return choices;
75
+ }
76
+
77
+ /**
78
+ * What changes when the colour is given to another grouping.
79
+ *
80
+ * A grouping drawn as the shape that is picked for the colour swaps places
81
+ * with it rather than being dropped, so the reader never loses one of the two
82
+ * groupings they were comparing by trading them round.
83
+ * @param options - What the figure is showing.
84
+ * @param colorBy - The grouping picked for the colour.
85
+ * @returns The options that change.
86
+ */
87
+ export function projectionColourPatch(
88
+ options: ProjectionOptions,
89
+ colorBy: string,
90
+ ): Partial<ProjectionOptions> {
91
+ return colorBy !== NO_GROUPING && colorBy === options.shapeBy
92
+ ? { colorBy, shapeBy: options.colorBy }
93
+ : { colorBy };
37
94
  }
38
95
 
39
96
  /**
@@ -8,13 +8,15 @@
8
8
  * honest as the controls around it change.
9
9
  */
10
10
 
11
- import { formatInteger } from '../../format/core/numbers.ts';
12
11
  import type { OverlayLegendEntry } from '../../overlay/ui/OverlayLegend.tsx';
13
12
  import type { EllipseSize } from '../../scatter/core/confidenceEllipse.ts';
14
13
  import { fillCopy } from '../core/fillCopy.ts';
15
14
  import type { ProjectionCopy } from '../core/projectionCopy.ts';
16
15
  import type { ProjectionMarker } from '../core/projectionResult.ts';
17
- import type { ResolvedProjectionGroups } from '../core/projectionSamples.ts';
16
+ import type {
17
+ ResolvedProjectionGroups,
18
+ ResolvedProjectionShapes,
19
+ } from '../core/projectionSamples.ts';
18
20
 
19
21
  import { ellipseCoverageText } from './projectionEllipse.ts';
20
22
  import { UNGROUPED_INK } from './projectionMapModel.ts';
@@ -27,6 +29,8 @@ interface ProjectionMapChromeInput {
27
29
  groups: ResolvedProjectionGroups;
28
30
  /** Whether colour stands for the group at all. */
29
31
  colored: boolean;
32
+ /** The grouping the dots are shaped by, or `null` while they are all discs. */
33
+ shapes: ResolvedProjectionShapes | null;
30
34
  /** How large the group outlines are, or `null` for none. */
31
35
  ellipse: EllipseSize | null;
32
36
  /** How many samples a group needs before it is outlined at all. */
@@ -82,6 +86,7 @@ export function projectionMapChrome(
82
86
  input: ProjectionMapChromeInput,
83
87
  ): ProjectionMapChrome {
84
88
  const { copy, groups, colored, ellipse, showGroupMeans, markers } = input;
89
+ const { shapes } = input;
85
90
  const { minimumPoints, fittedCount, total, figure = 'map' } = input;
86
91
  const entries: OverlayLegendEntry[] = [];
87
92
  const skipped: string[] = [];
@@ -108,6 +113,7 @@ export function projectionMapChrome(
108
113
  });
109
114
  }
110
115
  }
116
+ appendShapeEntries(entries, shapes);
111
117
  appendMarkerEntries(entries, markers, groups);
112
118
 
113
119
  const hollow = fittedCount < total;
@@ -121,7 +127,7 @@ export function projectionMapChrome(
121
127
  }
122
128
 
123
129
  return {
124
- title: legendTitle(copy, groups, colored),
130
+ title: legendTitle(copy, groups, colored, shapes),
125
131
  entries,
126
132
  caption: captionFor(
127
133
  copy,
@@ -136,44 +142,60 @@ export function projectionMapChrome(
136
142
  }
137
143
 
138
144
  /**
139
- * The sentence reporting what a gesture picked out.
140
- * @param copy - The words the map writes.
141
- * @param count - How many samples are selected.
142
- * @param total - How many there are in all.
143
- * @returns The sentence.
144
- */
145
- export function projectionSelectionSentence(
146
- copy: ProjectionCopy,
147
- count: number,
148
- total: number,
149
- ): string {
150
- if (count === 0) return copy.sentence.selectionNone;
151
- return fillCopy(copy.sentence.selection, {
152
- count: formatInteger(count),
153
- total: formatInteger(total),
154
- });
155
- }
156
-
157
- /**
158
- * The sentence over the legend.
145
+ * The sentence over the legend, naming what the colour and the shape stand
146
+ * for.
159
147
  *
160
- * When colour stands for nothing the entries left are all shape, so the title
161
- * names the shape channel: a legend naming no encoding at all is the one fault
162
- * this title exists to prevent, and it is not fixed by leaving the title out.
148
+ * When neither stands for a grouping the entries left are all marks, so the
149
+ * title names the shape channel: a legend naming no encoding at all is the one
150
+ * fault this title exists to prevent, and it is not fixed by leaving it out.
163
151
  * @param copy - The words the map writes.
164
152
  * @param groups - The groups as the map colours them.
165
153
  * @param colored - Whether colour stands for the group at all.
154
+ * @param shapes - The grouping the dots are shaped by, or `null`.
166
155
  * @returns The title.
167
156
  */
168
157
  function legendTitle(
169
158
  copy: ProjectionCopy,
170
159
  groups: ResolvedProjectionGroups,
171
160
  colored: boolean,
161
+ shapes: ResolvedProjectionShapes | null,
172
162
  ): string {
173
- if (!colored || groups.entries.length === 0) return copy.legend.shapes;
174
- return fillCopy(copy.legend.mapNoEllipse, {
175
- groups: groups.label.toLowerCase(),
176
- });
163
+ const parts: string[] = [];
164
+ if (colored && groups.entries.length > 0) {
165
+ parts.push(
166
+ fillCopy(copy.legend.mapNoEllipse, {
167
+ groups: groups.label.toLowerCase(),
168
+ }),
169
+ );
170
+ }
171
+ if (shapes !== null) {
172
+ parts.push(
173
+ fillCopy(copy.legend.shapedBy, { shapes: shapes.label.toLowerCase() }),
174
+ );
175
+ }
176
+ return parts.length === 0 ? copy.legend.shapes : parts.join(' ');
177
+ }
178
+
179
+ /**
180
+ * One entry per shape, drawn in the muted ink: a colour on a shape's entry
181
+ * would claim a group of the colour grouping it does not stand for.
182
+ * @param entries - The legend, appended to in place.
183
+ * @param shapes - The grouping the dots are shaped by, or `null`.
184
+ */
185
+ function appendShapeEntries(
186
+ entries: OverlayLegendEntry[],
187
+ shapes: ResolvedProjectionShapes | null,
188
+ ): void {
189
+ if (shapes === null) return;
190
+ for (const entry of shapes.entries) {
191
+ entries.push({
192
+ id: `shape-${entry.id}`,
193
+ label: entry.label,
194
+ color: SHAPE_INK,
195
+ shape: entry.shape,
196
+ count: entry.count,
197
+ });
198
+ }
177
199
  }
178
200
 
179
201
  function captionFor(
@@ -221,3 +243,6 @@ function appendMarkerEntries(
221
243
  });
222
244
  }
223
245
  }
246
+
247
+ /** The ink a shape's entry is drawn in, the one the key uses for marks that name no colour. */
248
+ const SHAPE_INK = 'var(--text-muted)';
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Which corner of the map the floating key takes.
3
+ *
4
+ * It is a module of its own because it is the one piece of the map that has to
5
+ * know where the dots fall before anything is drawn over them, and it is all
6
+ * arithmetic that can be checked without rendering anything.
7
+ */
8
+
9
+ import { emptiestCorner } from '../../overlay/core/emptiestCorner.ts';
10
+ import type { OverlayCorner } from '../../overlay/core/overlayPlacement.ts';
11
+
12
+ import type { ProjectionMapCloud } from './projectionMapModel.ts';
13
+
14
+ /**
15
+ * The corner of the map with the fewest dots under it.
16
+ *
17
+ * All four are offered. The chrome that asks is laid out inside the plot
18
+ * rectangle rather than over the figure's box, so no corner of it costs the
19
+ * reader an axis label, and the only thing left to weigh is how many samples
20
+ * each one would cover.
21
+ * @param cloud - Where every sample sits, in data units.
22
+ * @param xDomain - The range the horizontal axis covers.
23
+ * @param yDomain - The range the vertical axis covers.
24
+ * @param width - Width of the plot rectangle, in pixels.
25
+ * @param height - Its height.
26
+ * @returns The corner the card sits in.
27
+ */
28
+ export function projectionMapCorner(
29
+ cloud: ProjectionMapCloud,
30
+ xDomain: readonly [number, number],
31
+ yDomain: readonly [number, number],
32
+ width: number,
33
+ height: number,
34
+ ): OverlayCorner {
35
+ return emptiestCorner(
36
+ placeAlong(cloud.x, xDomain, width, false),
37
+ placeAlong(cloud.y, yDomain, height, true),
38
+ { width, height, cardWidth: CARD_WIDTH, cardHeight: CARD_HEIGHT },
39
+ );
40
+ }
41
+
42
+ function placeAlong(
43
+ values: Float64Array,
44
+ domain: readonly [number, number],
45
+ size: number,
46
+ flip: boolean,
47
+ ): Float64Array {
48
+ const span = domain[1] - domain[0];
49
+ const factor = span === 0 ? 0 : size / span;
50
+ const pixels = new Float64Array(values.length);
51
+ for (let index = 0; index < values.length; index++) {
52
+ const along = ((values[index] ?? Number.NaN) - domain[0]) * factor;
53
+ pixels[index] = flip ? size - along : along;
54
+ }
55
+ return pixels;
56
+ }
57
+
58
+ /**
59
+ * How much of the plot the floating key is expected to cover.
60
+ *
61
+ * It is an estimate on purpose — the card has not been laid out when the
62
+ * corner is chosen — and it is the compact key's size rather than the roomier
63
+ * card the overlay domain assumes: a corner counted as three species wide
64
+ * would call a corner busy that the key never reaches.
65
+ */
66
+ const CARD_WIDTH = 170;
67
+ const CARD_HEIGHT = 84;
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Where a map's samples land, and which corner of the picture is free.
2
+ * Where a map's samples land, and what a plot is handed to draw them.
3
3
  *
4
4
  * It sits beside the component rather than inside it because a plot has to
5
5
  * know where its dots fall before it can decide where its controls may sit,
@@ -14,15 +14,17 @@ import {
14
14
  import { chartAxisTitle } from '../../chart/core/chartLabels.ts';
15
15
  import type { ChartViewport } from '../../chart/core/chartViewport.ts';
16
16
  import type { MatrixLike } from '../../chart/core/matrix.ts';
17
- import { emptiestCorner } from '../../overlay/core/emptiestCorner.ts';
18
- import type { OverlayCorner } from '../../overlay/core/overlayPlacement.ts';
17
+ import type { OverlaySampleShape } from '../../overlay/core/overlayMarks.ts';
19
18
  import type { ScatterGroup } from '../../scatter/ui/scatterFigureProps.ts';
20
19
  import type { ScatterMarker } from '../../scatter/ui/scatterPlotProps.ts';
21
20
  import type {
22
21
  ProjectionMarker,
23
22
  ProjectionResult,
24
23
  } from '../core/projectionResult.ts';
25
- import type { ResolvedProjectionGroups } from '../core/projectionSamples.ts';
24
+ import type {
25
+ ResolvedProjectionGroups,
26
+ ResolvedProjectionShapes,
27
+ } from '../core/projectionSamples.ts';
26
28
 
27
29
  /** Where every sample sits on the map, in the embedding's own units. */
28
30
  export interface ProjectionMapCloud {
@@ -119,34 +121,6 @@ export function projectionMapViewport(
119
121
  };
120
122
  }
121
123
 
122
- /**
123
- * The corner of the map with the fewest dots under it.
124
- *
125
- * All four are offered. The chrome that asks is laid out inside the plot
126
- * rectangle rather than over the figure's box, so no corner of it costs the
127
- * reader an axis label, and the only thing left to weigh is how many samples
128
- * each one would cover.
129
- * @param cloud - Where every sample sits, in data units.
130
- * @param xDomain - The range the horizontal axis covers.
131
- * @param yDomain - The range the vertical axis covers.
132
- * @param width - Width of the plot rectangle, in pixels.
133
- * @param height - Its height.
134
- * @returns The corner the card sits in.
135
- */
136
- export function projectionMapCorner(
137
- cloud: ProjectionMapCloud,
138
- xDomain: readonly [number, number],
139
- yDomain: readonly [number, number],
140
- width: number,
141
- height: number,
142
- ): OverlayCorner {
143
- return emptiestCorner(
144
- placeAlong(cloud.x, xDomain, width, false),
145
- placeAlong(cloud.y, yDomain, height, true),
146
- { width, height, cardWidth: CARD_WIDTH, cardHeight: CARD_HEIGHT },
147
- );
148
- }
149
-
150
124
  /**
151
125
  * The reference points of a result, placed on the two axes the map draws.
152
126
  * @param markers - The reference points, in the embedding's own units.
@@ -192,6 +166,20 @@ export function projectionScatterGroups(
192
166
  return plotted;
193
167
  }
194
168
 
169
+ /**
170
+ * The shapes in the order a plot names them by index.
171
+ * @param shapes - The grouping the dots are shaped by, or `null`.
172
+ * @returns One shape per group, or nothing while every dot is a disc.
173
+ */
174
+ export function projectionScatterShapes(
175
+ shapes: ResolvedProjectionShapes | null,
176
+ ): OverlaySampleShape[] | undefined {
177
+ if (shapes === null) return undefined;
178
+ const list: OverlaySampleShape[] = [];
179
+ for (const entry of shapes.entries) list.push(entry.shape);
180
+ return list;
181
+ }
182
+
195
183
  /**
196
184
  * What one axis of the map is called.
197
185
  *
@@ -220,32 +208,5 @@ function withRoom(
220
208
  return [extent.min, extent.max];
221
209
  }
222
210
 
223
- function placeAlong(
224
- values: Float64Array,
225
- domain: readonly [number, number],
226
- size: number,
227
- flip: boolean,
228
- ): Float64Array {
229
- const span = domain[1] - domain[0];
230
- const factor = span === 0 ? 0 : size / span;
231
- const pixels = new Float64Array(values.length);
232
- for (let index = 0; index < values.length; index++) {
233
- const along = ((values[index] ?? Number.NaN) - domain[0]) * factor;
234
- pixels[index] = flip ? size - along : along;
235
- }
236
- return pixels;
237
- }
238
-
239
- /**
240
- * How much of the plot the floating key is expected to cover.
241
- *
242
- * It is an estimate on purpose — the card has not been laid out when the
243
- * corner is chosen — and it is the compact key's size rather than the roomier
244
- * card the overlay domain assumes: a corner counted as three species wide
245
- * would call a corner busy that the key never reaches.
246
- */
247
- const CARD_WIDTH = 170;
248
- const CARD_HEIGHT = 84;
249
-
250
211
  /** Room left around a framed selection, as a share of its own span. */
251
212
  const SELECTION_ROOM = 0.1;
@@ -18,23 +18,25 @@ import type { OrbitCamera } from '../../scatter3d/core/orbitCamera.ts';
18
18
  import { DEFAULT_ORBIT_CAMERA } from '../../scatter3d/core/orbitCamera.ts';
19
19
  import type { ProjectionCopy } from '../core/projectionCopy.ts';
20
20
  import type { ProjectionOptions } from '../core/projectionOptions.ts';
21
+ import { NO_GROUPING } from '../core/projectionOptions.ts';
21
22
  import type {
22
23
  ProjectionMarker,
23
24
  ProjectionResult,
24
25
  } from '../core/projectionResult.ts';
25
- import type { ResolvedProjectionGroups } from '../core/projectionSamples.ts';
26
+ import type {
27
+ ResolvedProjectionGroups,
28
+ ResolvedProjectionShapes,
29
+ } from '../core/projectionSamples.ts';
26
30
 
27
31
  import type { SelectionReport } from './ProjectionMapCaption.tsx';
28
32
  import type { ProjectionMapChrome } from './projectionMapChrome.ts';
29
- import {
30
- projectionMapChrome,
31
- projectionSelectionSentence,
32
- } from './projectionMapChrome.ts';
33
+ import { projectionMapChrome } from './projectionMapChrome.ts';
33
34
  import {
34
35
  MINIMUM_OUTLINE_POINTS,
35
36
  projectionMapDomain,
36
37
  projectionMapViewport,
37
38
  } from './projectionMapModel.ts';
39
+ import { projectionSelectionSentence } from './projectionSelectionSentence.ts';
38
40
  import { MINIMUM_SHELL_POINTS } from './projectionSpaceModel.ts';
39
41
 
40
42
  /** What {@link useProjectionMapView} needs. */
@@ -45,6 +47,8 @@ interface ProjectionMapViewInput {
45
47
  copy: ProjectionCopy;
46
48
  /** The groups as every figure of this viewer draws them. */
47
49
  groups: ResolvedProjectionGroups;
50
+ /** The grouping the dots are shaped by, or `null` while they are all discs. */
51
+ shapes: ResolvedProjectionShapes | null;
48
52
  /** Every option, already made safe against the result. */
49
53
  options: ProjectionOptions;
50
54
  /** The selected rows, as indices into the score matrix. */
@@ -93,7 +97,7 @@ export interface ProjectionMapView {
93
97
  export function useProjectionMapView(
94
98
  input: ProjectionMapViewInput,
95
99
  ): ProjectionMapView {
96
- const { result, copy, groups, options, selected, onSettle } = input;
100
+ const { result, copy, groups, shapes, options, selected, onSettle } = input;
97
101
  const { scores, fittedCount, markers } = result;
98
102
  const { colorBy, ellipse, showGroupMeans, xAxis, yAxis } = options;
99
103
 
@@ -139,13 +143,14 @@ export function useProjectionMapView(
139
143
  setCloudZoom(1);
140
144
  }, []);
141
145
 
142
- const colored = colorBy === 'group' && groups.entries.length > 0;
146
+ const colored = colorBy !== NO_GROUPING && groups.entries.length > 0;
143
147
  const chrome = useMemo(
144
148
  () =>
145
149
  projectionMapChrome({
146
150
  copy,
147
151
  groups,
148
152
  colored,
153
+ shapes,
149
154
  ellipse: colored ? ellipse : null,
150
155
  minimumPoints: MINIMUM_OUTLINE_POINTS,
151
156
  showGroupMeans,
@@ -157,6 +162,7 @@ export function useProjectionMapView(
157
162
  copy,
158
163
  groups,
159
164
  colored,
165
+ shapes,
160
166
  ellipse,
161
167
  showGroupMeans,
162
168
  markers,
@@ -171,6 +177,7 @@ export function useProjectionMapView(
171
177
  copy,
172
178
  groups,
173
179
  colored,
180
+ shapes,
174
181
  ellipse: colored ? ellipse : null,
175
182
  minimumPoints: MINIMUM_SHELL_POINTS,
176
183
  showGroupMeans: false,
@@ -179,7 +186,7 @@ export function useProjectionMapView(
179
186
  total: scores.rows,
180
187
  figure: 'space',
181
188
  }),
182
- [copy, groups, colored, ellipse, fittedCount, scores],
189
+ [copy, groups, colored, shapes, ellipse, fittedCount, scores],
183
190
  );
184
191
 
185
192
  const settle = useCallback(
@@ -0,0 +1,22 @@
1
+ import { formatInteger } from '../../format/core/numbers.ts';
2
+ import { fillCopy } from '../core/fillCopy.ts';
3
+ import type { ProjectionCopy } from '../core/projectionCopy.ts';
4
+
5
+ /**
6
+ * The sentence reporting what a gesture picked out.
7
+ * @param copy - The words the map writes.
8
+ * @param count - How many samples are selected.
9
+ * @param total - How many there are in all.
10
+ * @returns The sentence.
11
+ */
12
+ export function projectionSelectionSentence(
13
+ copy: ProjectionCopy,
14
+ count: number,
15
+ total: number,
16
+ ): string {
17
+ if (count === 0) return copy.sentence.selectionNone;
18
+ return fillCopy(copy.sentence.selection, {
19
+ count: formatInteger(count),
20
+ total: formatInteger(total),
21
+ });
22
+ }