react-cheminfo 0.10.0 → 0.11.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 (288) hide show
  1. package/lib/chrome/ui/NavLink.d.ts.map +1 -1
  2. package/lib/chrome/ui/NavLink.js +4 -3
  3. package/lib/chrome/ui/NavLink.js.map +1 -1
  4. package/lib/chrome/ui/SiteFooter.d.ts +8 -0
  5. package/lib/chrome/ui/SiteFooter.d.ts.map +1 -1
  6. package/lib/chrome/ui/SiteFooter.js +7 -2
  7. package/lib/chrome/ui/SiteFooter.js.map +1 -1
  8. package/lib/chrome/ui/SiteHeader.d.ts +11 -0
  9. package/lib/chrome/ui/SiteHeader.d.ts.map +1 -1
  10. package/lib/chrome/ui/SiteHeader.js +7 -2
  11. package/lib/chrome/ui/SiteHeader.js.map +1 -1
  12. package/lib/chrome/ui/navItem.d.ts +12 -0
  13. package/lib/chrome/ui/navItem.d.ts.map +1 -1
  14. package/lib/chrome/ui/navItem.js.map +1 -1
  15. package/lib/core.d.ts +1 -0
  16. package/lib/core.d.ts.map +1 -1
  17. package/lib/core.js +1 -0
  18. package/lib/core.js.map +1 -1
  19. package/lib/download/core/figureLegend.d.ts +90 -0
  20. package/lib/download/core/figureLegend.d.ts.map +1 -0
  21. package/lib/download/core/figureLegend.js +77 -0
  22. package/lib/download/core/figureLegend.js.map +1 -0
  23. package/lib/download/core/figureSvg.d.ts.map +1 -1
  24. package/lib/download/core/figureSvg.js +71 -1
  25. package/lib/download/core/figureSvg.js.map +1 -1
  26. package/lib/download/core/figureSvgDocument.d.ts +9 -2
  27. package/lib/download/core/figureSvgDocument.d.ts.map +1 -1
  28. package/lib/download/core/figureSvgDocument.js +4 -1
  29. package/lib/download/core/figureSvgDocument.js.map +1 -1
  30. package/lib/download/core/figureTarget.d.ts +11 -0
  31. package/lib/download/core/figureTarget.d.ts.map +1 -1
  32. package/lib/download/core/figureTarget.js +19 -0
  33. package/lib/download/core/figureTarget.js.map +1 -1
  34. package/lib/download/core/index.d.ts +3 -1
  35. package/lib/download/core/index.d.ts.map +1 -1
  36. package/lib/download/core/index.js +2 -1
  37. package/lib/download/core/index.js.map +1 -1
  38. package/lib/ecosystem/core/sites.d.ts +1 -1
  39. package/lib/ecosystem/core/sites.d.ts.map +1 -1
  40. package/lib/ecosystem/core/sites.js +13 -0
  41. package/lib/ecosystem/core/sites.js.map +1 -1
  42. package/lib/ecosystem/ui/glyphs.d.ts.map +1 -1
  43. package/lib/ecosystem/ui/glyphs.js +5 -0
  44. package/lib/ecosystem/ui/glyphs.js.map +1 -1
  45. package/lib/overlay/ui/OverlayLegend.d.ts.map +1 -1
  46. package/lib/overlay/ui/OverlayLegend.js +1 -1
  47. package/lib/overlay/ui/OverlayLegend.js.map +1 -1
  48. package/lib/projection/core/projectionCopy.d.ts +4 -0
  49. package/lib/projection/core/projectionCopy.d.ts.map +1 -1
  50. package/lib/projection/core/projectionCopy.js +3 -0
  51. package/lib/projection/core/projectionCopy.js.map +1 -1
  52. package/lib/projection/core/projectionOptions.d.ts +12 -0
  53. package/lib/projection/core/projectionOptions.d.ts.map +1 -1
  54. package/lib/projection/core/projectionOptions.js +27 -1
  55. package/lib/projection/core/projectionOptions.js.map +1 -1
  56. package/lib/projection/core/projectionStrings.d.ts +4 -0
  57. package/lib/projection/core/projectionStrings.d.ts.map +1 -1
  58. package/lib/projection/core/projectionStrings.js +14 -0
  59. package/lib/projection/core/projectionStrings.js.map +1 -1
  60. package/lib/projection/core/projectionTabDefaults.d.ts.map +1 -1
  61. package/lib/projection/core/projectionTabDefaults.js +12 -0
  62. package/lib/projection/core/projectionTabDefaults.js.map +1 -1
  63. package/lib/projection/core/projectionTabs.d.ts +1 -1
  64. package/lib/projection/core/projectionTabs.d.ts.map +1 -1
  65. package/lib/projection/core/projectionTabs.js +9 -0
  66. package/lib/projection/core/projectionTabs.js.map +1 -1
  67. package/lib/projection/ui/ProjectionMapTab.d.ts +6 -0
  68. package/lib/projection/ui/ProjectionMapTab.d.ts.map +1 -1
  69. package/lib/projection/ui/ProjectionMapTab.js +2 -1
  70. package/lib/projection/ui/ProjectionMapTab.js.map +1 -1
  71. package/lib/projection/ui/ProjectionPanel.d.ts +6 -0
  72. package/lib/projection/ui/ProjectionPanel.d.ts.map +1 -1
  73. package/lib/projection/ui/ProjectionPanel.js +15 -1
  74. package/lib/projection/ui/ProjectionPanel.js.map +1 -1
  75. package/lib/projection/ui/ProjectionReadout.d.ts +7 -0
  76. package/lib/projection/ui/ProjectionReadout.d.ts.map +1 -1
  77. package/lib/projection/ui/ProjectionReadout.js +5 -4
  78. package/lib/projection/ui/ProjectionReadout.js.map +1 -1
  79. package/lib/projection/ui/ProjectionSpaceControls.d.ts +51 -0
  80. package/lib/projection/ui/ProjectionSpaceControls.d.ts.map +1 -0
  81. package/lib/projection/ui/ProjectionSpaceControls.js +71 -0
  82. package/lib/projection/ui/ProjectionSpaceControls.js.map +1 -0
  83. package/lib/projection/ui/ProjectionSpaceMore.d.ts +32 -0
  84. package/lib/projection/ui/ProjectionSpaceMore.d.ts.map +1 -0
  85. package/lib/projection/ui/ProjectionSpaceMore.js +57 -0
  86. package/lib/projection/ui/ProjectionSpaceMore.js.map +1 -0
  87. package/lib/projection/ui/ProjectionSpaceTab.d.ts +98 -0
  88. package/lib/projection/ui/ProjectionSpaceTab.d.ts.map +1 -0
  89. package/lib/projection/ui/ProjectionSpaceTab.js +66 -0
  90. package/lib/projection/ui/ProjectionSpaceTab.js.map +1 -0
  91. package/lib/projection/ui/ProjectionViewer.d.ts +1 -0
  92. package/lib/projection/ui/ProjectionViewer.d.ts.map +1 -1
  93. package/lib/projection/ui/ProjectionViewer.js +2 -1
  94. package/lib/projection/ui/ProjectionViewer.js.map +1 -1
  95. package/lib/projection/ui/index.d.ts +1 -0
  96. package/lib/projection/ui/index.d.ts.map +1 -1
  97. package/lib/projection/ui/index.js.map +1 -1
  98. package/lib/projection/ui/projectionBarModel.d.ts.map +1 -1
  99. package/lib/projection/ui/projectionBarModel.js +3 -2
  100. package/lib/projection/ui/projectionBarModel.js.map +1 -1
  101. package/lib/projection/ui/projectionBarReadings.d.ts.map +1 -1
  102. package/lib/projection/ui/projectionBarReadings.js +17 -0
  103. package/lib/projection/ui/projectionBarReadings.js.map +1 -1
  104. package/lib/projection/ui/projectionBarSlots.d.ts.map +1 -1
  105. package/lib/projection/ui/projectionBarSlots.js +17 -0
  106. package/lib/projection/ui/projectionBarSlots.js.map +1 -1
  107. package/lib/projection/ui/projectionCloudChoices.d.ts +18 -0
  108. package/lib/projection/ui/projectionCloudChoices.d.ts.map +1 -0
  109. package/lib/projection/ui/projectionCloudChoices.js +19 -0
  110. package/lib/projection/ui/projectionCloudChoices.js.map +1 -0
  111. package/lib/projection/ui/projectionEllipsoid.d.ts +38 -0
  112. package/lib/projection/ui/projectionEllipsoid.d.ts.map +1 -0
  113. package/lib/projection/ui/projectionEllipsoid.js +71 -0
  114. package/lib/projection/ui/projectionEllipsoid.js.map +1 -0
  115. package/lib/projection/ui/projectionMapChrome.d.ts +8 -0
  116. package/lib/projection/ui/projectionMapChrome.d.ts.map +1 -1
  117. package/lib/projection/ui/projectionMapChrome.js +10 -6
  118. package/lib/projection/ui/projectionMapChrome.js.map +1 -1
  119. package/lib/projection/ui/projectionMapView.d.ts +13 -0
  120. package/lib/projection/ui/projectionMapView.d.ts.map +1 -1
  121. package/lib/projection/ui/projectionMapView.js +26 -0
  122. package/lib/projection/ui/projectionMapView.js.map +1 -1
  123. package/lib/projection/ui/projectionSpaceModel.d.ts +14 -0
  124. package/lib/projection/ui/projectionSpaceModel.d.ts.map +1 -0
  125. package/lib/projection/ui/projectionSpaceModel.js +14 -0
  126. package/lib/projection/ui/projectionSpaceModel.js.map +1 -0
  127. package/lib/projection/ui/projectionViewerProps.d.ts +34 -0
  128. package/lib/projection/ui/projectionViewerProps.d.ts.map +1 -1
  129. package/lib/scatter/ui/ScatterPlot.d.ts +1 -1
  130. package/lib/scatter/ui/ScatterPlot.d.ts.map +1 -1
  131. package/lib/scatter/ui/ScatterPlot.js +18 -2
  132. package/lib/scatter/ui/ScatterPlot.js.map +1 -1
  133. package/lib/scatter/ui/lassoGesture.d.ts +14 -2
  134. package/lib/scatter/ui/lassoGesture.d.ts.map +1 -1
  135. package/lib/scatter/ui/lassoGesture.js +4 -1
  136. package/lib/scatter/ui/lassoGesture.js.map +1 -1
  137. package/lib/scatter/ui/scatterPlotProps.d.ts +34 -0
  138. package/lib/scatter/ui/scatterPlotProps.d.ts.map +1 -1
  139. package/lib/scatter/ui/useScatterInteraction.d.ts +9 -0
  140. package/lib/scatter/ui/useScatterInteraction.d.ts.map +1 -1
  141. package/lib/scatter/ui/useScatterInteraction.js +9 -1
  142. package/lib/scatter/ui/useScatterInteraction.js.map +1 -1
  143. package/lib/scatter3d/core/cloudGesture.d.ts +17 -0
  144. package/lib/scatter3d/core/cloudGesture.d.ts.map +1 -0
  145. package/lib/scatter3d/core/cloudGesture.js +2 -0
  146. package/lib/scatter3d/core/cloudGesture.js.map +1 -0
  147. package/lib/scatter3d/core/confidenceEllipsoid.d.ts +63 -0
  148. package/lib/scatter3d/core/confidenceEllipsoid.d.ts.map +1 -0
  149. package/lib/scatter3d/core/confidenceEllipsoid.js +128 -0
  150. package/lib/scatter3d/core/confidenceEllipsoid.js.map +1 -0
  151. package/lib/scatter3d/core/cubeFrame.d.ts +40 -0
  152. package/lib/scatter3d/core/cubeFrame.d.ts.map +1 -0
  153. package/lib/scatter3d/core/cubeFrame.js +58 -0
  154. package/lib/scatter3d/core/cubeFrame.js.map +1 -0
  155. package/lib/scatter3d/core/cubePoints.d.ts +35 -0
  156. package/lib/scatter3d/core/cubePoints.d.ts.map +1 -0
  157. package/lib/scatter3d/core/cubePoints.js +50 -0
  158. package/lib/scatter3d/core/cubePoints.js.map +1 -0
  159. package/lib/scatter3d/core/ellipsoidCoverage.d.ts +32 -0
  160. package/lib/scatter3d/core/ellipsoidCoverage.d.ts.map +1 -0
  161. package/lib/scatter3d/core/ellipsoidCoverage.js +83 -0
  162. package/lib/scatter3d/core/ellipsoidCoverage.js.map +1 -0
  163. package/lib/scatter3d/core/ellipsoidSilhouette.d.ts +42 -0
  164. package/lib/scatter3d/core/ellipsoidSilhouette.d.ts.map +1 -0
  165. package/lib/scatter3d/core/ellipsoidSilhouette.js +52 -0
  166. package/lib/scatter3d/core/ellipsoidSilhouette.js.map +1 -0
  167. package/lib/scatter3d/core/index.d.ts +29 -0
  168. package/lib/scatter3d/core/index.d.ts.map +1 -0
  169. package/lib/scatter3d/core/index.js +22 -0
  170. package/lib/scatter3d/core/index.js.map +1 -0
  171. package/lib/scatter3d/core/orbitCamera.d.ts +113 -0
  172. package/lib/scatter3d/core/orbitCamera.d.ts.map +1 -0
  173. package/lib/scatter3d/core/orbitCamera.js +104 -0
  174. package/lib/scatter3d/core/orbitCamera.js.map +1 -0
  175. package/lib/scatter3d/core/symmetricEigen.d.ts +47 -0
  176. package/lib/scatter3d/core/symmetricEigen.d.ts.map +1 -0
  177. package/lib/scatter3d/core/symmetricEigen.js +219 -0
  178. package/lib/scatter3d/core/symmetricEigen.js.map +1 -0
  179. package/lib/scatter3d/ui/CloudFrameLayer.d.ts +33 -0
  180. package/lib/scatter3d/ui/CloudFrameLayer.d.ts.map +1 -0
  181. package/lib/scatter3d/ui/CloudFrameLayer.js +37 -0
  182. package/lib/scatter3d/ui/CloudFrameLayer.js.map +1 -0
  183. package/lib/scatter3d/ui/CloudPointLayer.d.ts +62 -0
  184. package/lib/scatter3d/ui/CloudPointLayer.d.ts.map +1 -0
  185. package/lib/scatter3d/ui/CloudPointLayer.js +67 -0
  186. package/lib/scatter3d/ui/CloudPointLayer.js.map +1 -0
  187. package/lib/scatter3d/ui/CloudShellLayer.d.ts +52 -0
  188. package/lib/scatter3d/ui/CloudShellLayer.d.ts.map +1 -0
  189. package/lib/scatter3d/ui/CloudShellLayer.js +40 -0
  190. package/lib/scatter3d/ui/CloudShellLayer.js.map +1 -0
  191. package/lib/scatter3d/ui/ScatterCloud.d.ts +24 -0
  192. package/lib/scatter3d/ui/ScatterCloud.d.ts.map +1 -0
  193. package/lib/scatter3d/ui/ScatterCloud.js +189 -0
  194. package/lib/scatter3d/ui/ScatterCloud.js.map +1 -0
  195. package/lib/scatter3d/ui/cloudGeometry.d.ts +108 -0
  196. package/lib/scatter3d/ui/cloudGeometry.d.ts.map +1 -0
  197. package/lib/scatter3d/ui/cloudGeometry.js +122 -0
  198. package/lib/scatter3d/ui/cloudGeometry.js.map +1 -0
  199. package/lib/scatter3d/ui/cloudShellStyles.d.ts +36 -0
  200. package/lib/scatter3d/ui/cloudShellStyles.d.ts.map +1 -0
  201. package/lib/scatter3d/ui/cloudShellStyles.js +62 -0
  202. package/lib/scatter3d/ui/cloudShellStyles.js.map +1 -0
  203. package/lib/scatter3d/ui/cloudZoom.d.ts +21 -0
  204. package/lib/scatter3d/ui/cloudZoom.d.ts.map +1 -0
  205. package/lib/scatter3d/ui/cloudZoom.js +22 -0
  206. package/lib/scatter3d/ui/cloudZoom.js.map +1 -0
  207. package/lib/scatter3d/ui/index.d.ts +29 -0
  208. package/lib/scatter3d/ui/index.d.ts.map +1 -0
  209. package/lib/scatter3d/ui/index.js +21 -0
  210. package/lib/scatter3d/ui/index.js.map +1 -0
  211. package/lib/scatter3d/ui/scatterCloudModel.d.ts +60 -0
  212. package/lib/scatter3d/ui/scatterCloudModel.d.ts.map +1 -0
  213. package/lib/scatter3d/ui/scatterCloudModel.js +66 -0
  214. package/lib/scatter3d/ui/scatterCloudModel.js.map +1 -0
  215. package/lib/scatter3d/ui/scatterCloudProps.d.ts +185 -0
  216. package/lib/scatter3d/ui/scatterCloudProps.d.ts.map +1 -0
  217. package/lib/scatter3d/ui/scatterCloudProps.js +9 -0
  218. package/lib/scatter3d/ui/scatterCloudProps.js.map +1 -0
  219. package/lib/scatter3d/ui/useOrbitDrag.d.ts +60 -0
  220. package/lib/scatter3d/ui/useOrbitDrag.d.ts.map +1 -0
  221. package/lib/scatter3d/ui/useOrbitDrag.js +129 -0
  222. package/lib/scatter3d/ui/useOrbitDrag.js.map +1 -0
  223. package/lib/ui.d.ts +1 -0
  224. package/lib/ui.d.ts.map +1 -1
  225. package/lib/ui.js +1 -0
  226. package/lib/ui.js.map +1 -1
  227. package/package.json +1 -1
  228. package/src/chrome/ui/NavLink.tsx +5 -3
  229. package/src/chrome/ui/SiteFooter.tsx +16 -1
  230. package/src/chrome/ui/SiteHeader.tsx +19 -1
  231. package/src/chrome/ui/navItem.ts +12 -0
  232. package/src/core.ts +1 -0
  233. package/src/download/core/figureLegend.ts +167 -0
  234. package/src/download/core/figureSvg.ts +81 -1
  235. package/src/download/core/figureSvgDocument.ts +13 -3
  236. package/src/download/core/figureTarget.ts +21 -0
  237. package/src/download/core/index.ts +8 -0
  238. package/src/ecosystem/core/sites.ts +14 -0
  239. package/src/ecosystem/ui/glyphs.tsx +26 -0
  240. package/src/overlay/ui/OverlayLegend.tsx +7 -1
  241. package/src/projection/core/projectionCopy.ts +9 -0
  242. package/src/projection/core/projectionOptions.ts +48 -3
  243. package/src/projection/core/projectionStrings.ts +16 -0
  244. package/src/projection/core/projectionTabDefaults.ts +12 -0
  245. package/src/projection/core/projectionTabs.ts +10 -1
  246. package/src/projection/ui/ProjectionMapTab.tsx +8 -0
  247. package/src/projection/ui/ProjectionPanel.tsx +63 -0
  248. package/src/projection/ui/ProjectionReadout.tsx +16 -6
  249. package/src/projection/ui/ProjectionSpaceControls.tsx +219 -0
  250. package/src/projection/ui/ProjectionSpaceMore.tsx +169 -0
  251. package/src/projection/ui/ProjectionSpaceTab.tsx +217 -0
  252. package/src/projection/ui/ProjectionViewer.tsx +3 -0
  253. package/src/projection/ui/index.ts +1 -0
  254. package/src/projection/ui/projectionBarModel.ts +3 -2
  255. package/src/projection/ui/projectionBarReadings.ts +18 -0
  256. package/src/projection/ui/projectionBarSlots.tsx +19 -0
  257. package/src/projection/ui/projectionCloudChoices.ts +23 -0
  258. package/src/projection/ui/projectionEllipsoid.ts +78 -0
  259. package/src/projection/ui/projectionMapChrome.ts +26 -5
  260. package/src/projection/ui/projectionMapView.ts +45 -0
  261. package/src/projection/ui/projectionSpaceModel.ts +14 -0
  262. package/src/projection/ui/projectionViewerProps.ts +35 -0
  263. package/src/scatter/ui/ScatterPlot.tsx +21 -2
  264. package/src/scatter/ui/lassoGesture.ts +15 -2
  265. package/src/scatter/ui/scatterPlotProps.ts +35 -0
  266. package/src/scatter/ui/useScatterInteraction.ts +22 -1
  267. package/src/scatter3d/core/cloudGesture.ts +16 -0
  268. package/src/scatter3d/core/confidenceEllipsoid.ts +177 -0
  269. package/src/scatter3d/core/cubeFrame.ts +72 -0
  270. package/src/scatter3d/core/cubePoints.ts +75 -0
  271. package/src/scatter3d/core/ellipsoidCoverage.ts +86 -0
  272. package/src/scatter3d/core/ellipsoidSilhouette.ts +76 -0
  273. package/src/scatter3d/core/index.ts +52 -0
  274. package/src/scatter3d/core/orbitCamera.ts +159 -0
  275. package/src/scatter3d/core/symmetricEigen.ts +273 -0
  276. package/src/scatter3d/ui/CloudFrameLayer.tsx +97 -0
  277. package/src/scatter3d/ui/CloudPointLayer.tsx +132 -0
  278. package/src/scatter3d/ui/CloudShellLayer.tsx +107 -0
  279. package/src/scatter3d/ui/ScatterCloud.tsx +312 -0
  280. package/src/scatter3d/ui/cloudGeometry.ts +200 -0
  281. package/src/scatter3d/ui/cloudShellStyles.ts +74 -0
  282. package/src/scatter3d/ui/cloudZoom.ts +22 -0
  283. package/src/scatter3d/ui/index.ts +41 -0
  284. package/src/scatter3d/ui/scatterCloudModel.ts +98 -0
  285. package/src/scatter3d/ui/scatterCloudProps.ts +191 -0
  286. package/src/scatter3d/ui/useOrbitDrag.ts +220 -0
  287. package/src/ui.ts +1 -0
  288. package/styles/chrome.css +29 -4
@@ -0,0 +1,219 @@
1
+ import type { ReactElement } from 'react';
2
+
3
+ import type { OverlayTier } from '../../overlay/core/overlayTiers.ts';
4
+ import type { OverlayChipSetting } from '../../overlay/ui/OverlayChip.tsx';
5
+ import { OverlayChip } from '../../overlay/ui/OverlayChip.tsx';
6
+ import { OverlaySegmented } from '../../overlay/ui/OverlaySegmented.tsx';
7
+ import { OverlaySelect } from '../../overlay/ui/OverlaySelect.tsx';
8
+ import { OverlayToggle } from '../../overlay/ui/OverlayToggle.tsx';
9
+ import { OverlayValueMenu } from '../../overlay/ui/OverlayValueMenu.tsx';
10
+ import type { CloudGesture } from '../../scatter3d/core/cloudGesture.ts';
11
+ import type { ProjectionCopy } from '../core/projectionCopy.ts';
12
+ import type {
13
+ ProjectionColorBy,
14
+ ProjectionOptions,
15
+ } from '../core/projectionOptions.ts';
16
+
17
+ import type { ProjectionReading } from './projectionBarReadings.ts';
18
+ import { CLOUD_GESTURE_CHOICES } from './projectionCloudChoices.ts';
19
+ import { ellipseKey, ellipseSize } from './projectionEllipse.ts';
20
+ import { ellipsoidChoices } from './projectionEllipsoid.ts';
21
+ import { projectionColourChoices } from './projectionMapChoices.ts';
22
+
23
+ /** What the cloud's controls are drawn from, wherever in the bar they sit. */
24
+ export interface ProjectionSpaceControlsProps {
25
+ /** What the cloud is showing, already resolved against the result. */
26
+ options: ProjectionOptions;
27
+ /** Called with only the options that changed, for the viewer to merge. */
28
+ onChange: (patch: Partial<ProjectionOptions>) => void;
29
+ /** The words the bar writes, already merged over the defaults. */
30
+ copy: ProjectionCopy;
31
+ /** What the set of groups is called. */
32
+ groupLabel: string;
33
+ /**
34
+ * Whether the samples carry groups at all. Without them the shells stand for
35
+ * nothing, so their control is greyed rather than removed.
36
+ * @default false
37
+ */
38
+ hasGroups?: boolean;
39
+ /**
40
+ * What the settings currently read, built once for the bar and handed down.
41
+ * @default [] — nothing is written, which is what the cog's panel wants
42
+ */
43
+ readings?: readonly ProjectionReading[];
44
+ /**
45
+ * How much of itself the bar is writing.
46
+ * @default 'full'
47
+ */
48
+ tier?: OverlayTier;
49
+ }
50
+
51
+ /**
52
+ * The cloud settings a reader changes while looking at the picture.
53
+ *
54
+ * One of them is not on the map's bar at all, and it is the first: what a
55
+ * plain drag does. Turning the box and drawing a lasso both want that drag,
56
+ * and neither can be given a modifier without taking one that already means
57
+ * "add" or "take away" inside a lasso — so the choice is written on the bar
58
+ * where the reader can see it, rather than left as a shortcut they would have
59
+ * to be told about. It leads the row because it is the only setting on the bar
60
+ * that changes what the reader's own hand does.
61
+ *
62
+ * The rest are the map's, unchanged: how large the shells are, and the two
63
+ * name switches. The axes and the dot size wait behind the cog exactly as they
64
+ * do on the map.
65
+ * @param props - See {@link ProjectionSpaceControlsProps}.
66
+ * @returns The controls.
67
+ */
68
+ export function ProjectionSpaceControls(
69
+ props: ProjectionSpaceControlsProps,
70
+ ): ReactElement {
71
+ const { options, onChange, copy, groupLabel, hasGroups = false } = props;
72
+ const { readings = NO_READINGS, tier = 'full' } = props;
73
+ const { bar, help } = copy;
74
+
75
+ const uncoloured = !hasGroups || options.colorBy === 'none';
76
+ const names = (
77
+ <>
78
+ <OverlayToggle
79
+ label={bar.switches.showGroupLabels}
80
+ help={help.showGroupLabels}
81
+ icon={GROUP_LABELS_GLYPH}
82
+ hideLabel
83
+ checked={options.showGroupLabels}
84
+ disabled={uncoloured}
85
+ onChange={(showGroupLabels) => onChange({ showGroupLabels })}
86
+ />
87
+ <OverlayToggle
88
+ label={bar.switches.showIds}
89
+ help={help.showIds}
90
+ icon={IDS_GLYPH}
91
+ hideLabel
92
+ checked={options.showIds}
93
+ onChange={(showIds) => onChange({ showIds })}
94
+ />
95
+ </>
96
+ );
97
+
98
+ if (tier === 'chip' || tier === 'tiny') {
99
+ return (
100
+ <>
101
+ <OverlayChip label={bar.settings} settings={chipSettings(readings)}>
102
+ <SpaceSettings
103
+ options={options}
104
+ onChange={onChange}
105
+ copy={copy}
106
+ groupLabel={groupLabel}
107
+ hasGroups={hasGroups}
108
+ />
109
+ </OverlayChip>
110
+ {names}
111
+ </>
112
+ );
113
+ }
114
+
115
+ return (
116
+ <>
117
+ <OverlayValueMenu<CloudGesture>
118
+ label={help.cloudGesture.title}
119
+ keyWord={bar.key.cloudGesture}
120
+ showKey={tier === 'full'}
121
+ value={options.cloudGesture}
122
+ options={CLOUD_GESTURE_CHOICES}
123
+ onChange={(cloudGesture) => onChange({ cloudGesture })}
124
+ />
125
+ <OverlayValueMenu
126
+ label={help.ellipse.title}
127
+ keyWord={bar.key.ellipse}
128
+ showKey={tier === 'full'}
129
+ value={ellipseKey(options.ellipse)}
130
+ disabled={uncoloured}
131
+ disabledReason={UNCOLOURED_REASON}
132
+ options={ellipsoidChoices(options.ellipse)}
133
+ onChange={(value) => onChange({ ellipse: ellipseSize(value) })}
134
+ />
135
+ {names}
136
+ </>
137
+ );
138
+ }
139
+
140
+ /**
141
+ * What the chip reads out, which is every setting spelled in full.
142
+ * @param readings - What the settings currently read.
143
+ * @returns The chip's own settings.
144
+ */
145
+ function chipSettings(
146
+ readings: readonly ProjectionReading[],
147
+ ): readonly OverlayChipSetting[] {
148
+ const settings: OverlayChipSetting[] = [];
149
+ for (const reading of readings) {
150
+ settings.push({
151
+ label: reading.label,
152
+ value: reading.value,
153
+ swatches: reading.swatches,
154
+ });
155
+ }
156
+ return settings;
157
+ }
158
+
159
+ /** What the captioned pair behind the chip is drawn from. */
160
+ interface SpaceSettingsProps {
161
+ /** What the cloud is showing, already resolved against the result. */
162
+ options: ProjectionOptions;
163
+ /** Called with only the options that changed. */
164
+ onChange: (patch: Partial<ProjectionOptions>) => void;
165
+ /** The words the panel writes. */
166
+ copy: ProjectionCopy;
167
+ /** What the set of groups is called, which is what `Colour by` offers. */
168
+ groupLabel: string;
169
+ /** Whether the samples carry groups at all. */
170
+ hasGroups: boolean;
171
+ }
172
+
173
+ /**
174
+ * The same settings with their captions, for the panel the chip opens.
175
+ * @param props - See {@link SpaceSettingsProps}.
176
+ * @returns The captioned controls.
177
+ */
178
+ function SpaceSettings(props: SpaceSettingsProps): ReactElement {
179
+ const { options, onChange, copy, groupLabel, hasGroups } = props;
180
+ const { bar, help } = copy;
181
+
182
+ return (
183
+ <>
184
+ <OverlaySegmented<CloudGesture>
185
+ label={help.cloudGesture.title}
186
+ help={help.cloudGesture}
187
+ value={options.cloudGesture}
188
+ options={CLOUD_GESTURE_CHOICES}
189
+ onChange={(cloudGesture) => onChange({ cloudGesture })}
190
+ />
191
+ <OverlaySegmented<ProjectionColorBy>
192
+ label={help.colorBy.title}
193
+ help={help.colorBy}
194
+ value={options.colorBy}
195
+ disabled={!hasGroups}
196
+ options={projectionColourChoices(groupLabel, bar.uncoloured)}
197
+ onChange={(colorBy) => onChange({ colorBy })}
198
+ />
199
+ <OverlaySelect
200
+ label={help.ellipse.title}
201
+ help={help.ellipse}
202
+ value={ellipseKey(options.ellipse)}
203
+ disabled={!hasGroups || options.colorBy === 'none'}
204
+ options={ellipsoidChoices(options.ellipse)}
205
+ onChange={(value) => onChange({ ellipse: ellipseSize(value) })}
206
+ />
207
+ </>
208
+ );
209
+ }
210
+
211
+ /** The two glyphs the name switches ride as, the same pair the map uses. */
212
+ const GROUP_LABELS_GLYPH = 'tag';
213
+ const IDS_GLYPH = 'id-number';
214
+
215
+ const UNCOLOURED_REASON =
216
+ 'Shells follow the groups, so colour the cloud by them first.';
217
+
218
+ /** No readings at all, which is what the panel behind the cog is handed. */
219
+ const NO_READINGS: readonly ProjectionReading[] = [];
@@ -0,0 +1,169 @@
1
+ import type { ReactElement } from 'react';
2
+
3
+ import { chartAxisTitle } from '../../chart/core/chartLabels.ts';
4
+ import { OverlayAction } from '../../overlay/ui/OverlayAction.tsx';
5
+ import { OverlayGroup } from '../../overlay/ui/OverlayGroup.tsx';
6
+ import { OverlayNumber } from '../../overlay/ui/OverlayNumber.tsx';
7
+ import { OverlayPanel } from '../../overlay/ui/OverlayPanel.tsx';
8
+ import type { OverlayOption } from '../../overlay/ui/OverlayRow.tsx';
9
+ import { OverlaySegmented } from '../../overlay/ui/OverlaySegmented.tsx';
10
+ import { OverlaySelect } from '../../overlay/ui/OverlaySelect.tsx';
11
+ import type { ScatterSelectionMode } from '../../scatter/core/scatterSelection.ts';
12
+ import type { CloudGesture } from '../../scatter3d/core/cloudGesture.ts';
13
+ import type { ProjectionResult } from '../core/projectionResult.ts';
14
+ import {
15
+ PROJECTION_PANEL_NAME,
16
+ PROJECTION_PANEL_SECTION,
17
+ } from '../core/projectionStrings.ts';
18
+ import { PROJECTION_TAB_DEFAULTS } from '../core/projectionTabDefaults.ts';
19
+
20
+ import type { ProjectionSpaceControlsProps } from './ProjectionSpaceControls.tsx';
21
+ import { CLOUD_GESTURE_CHOICES } from './projectionCloudChoices.ts';
22
+
23
+ /** What {@link ProjectionSpaceMore} needs beyond the cloud's own controls. */
24
+ export interface ProjectionSpaceMoreProps extends ProjectionSpaceControlsProps {
25
+ /** The reduced space the controls apply to; its axes fill the three pickers. */
26
+ result: ProjectionResult;
27
+ /**
28
+ * How many samples are selected, which is what the clearing action needs
29
+ * before it can do anything.
30
+ * @default 0
31
+ */
32
+ selectedCount?: number;
33
+ /** Puts the box back where it was first seen. */
34
+ onResetView: () => void;
35
+ /** Empties the selection. */
36
+ onClearSelection: () => void;
37
+ }
38
+
39
+ /**
40
+ * What the cloud keeps behind the cog: its three axes, the dot size, and what
41
+ * the two drags do.
42
+ *
43
+ * It is the map's panel with one picker more and one action fewer. There is no
44
+ * "zoom to selection" here, because a box has no frame to draw round a crowd:
45
+ * the reader turns it until the crowd is facing them, which is the gesture the
46
+ * whole tab exists for, and a button that guessed an angle for them would
47
+ * usually guess wrong.
48
+ * @param props - See {@link ProjectionSpaceMoreProps}.
49
+ * @returns The panel behind the cog.
50
+ */
51
+ export function ProjectionSpaceMore(
52
+ props: ProjectionSpaceMoreProps,
53
+ ): ReactElement {
54
+ const { result, options, onChange, copy } = props;
55
+ const { selectedCount = 0, onResetView, onClearSelection } = props;
56
+ const { action, help, tab } = copy;
57
+
58
+ const nothingPicked = selectedCount === 0;
59
+
60
+ return (
61
+ <OverlayPanel
62
+ title={tab.space}
63
+ onReset={() => onChange(PROJECTION_TAB_DEFAULTS.space)}
64
+ actions={
65
+ <>
66
+ <OverlayAction
67
+ text={action.resetView}
68
+ icon="reset"
69
+ onClick={onResetView}
70
+ />
71
+ <OverlayAction
72
+ text={action.clearSelection}
73
+ icon="cross"
74
+ disabled={nothingPicked}
75
+ onClick={onClearSelection}
76
+ />
77
+ </>
78
+ }
79
+ >
80
+ <OverlayGroup label={PROJECTION_PANEL_SECTION.axes} divider={false}>
81
+ <OverlaySelect
82
+ label={PROJECTION_PANEL_NAME.xAxis}
83
+ help={help.xAxis}
84
+ value={String(options.xAxis)}
85
+ options={axisChoices(result, [options.yAxis, options.zAxis])}
86
+ onChange={(value) => onChange({ xAxis: Number(value) })}
87
+ />
88
+ <OverlaySelect
89
+ label={PROJECTION_PANEL_NAME.yAxis}
90
+ help={help.yAxis}
91
+ value={String(options.yAxis)}
92
+ options={axisChoices(result, [options.xAxis, options.zAxis])}
93
+ onChange={(value) => onChange({ yAxis: Number(value) })}
94
+ />
95
+ <OverlaySelect
96
+ label={PROJECTION_PANEL_NAME.zAxis}
97
+ help={help.zAxis}
98
+ value={String(options.zAxis)}
99
+ options={axisChoices(result, [options.xAxis, options.yAxis])}
100
+ onChange={(value) => onChange({ zAxis: Number(value) })}
101
+ />
102
+ </OverlayGroup>
103
+ <OverlayGroup label={PROJECTION_PANEL_SECTION.drawing}>
104
+ <OverlayNumber
105
+ label={PROJECTION_PANEL_NAME.pointRadius}
106
+ help={help.pointRadius}
107
+ value={options.pointRadius}
108
+ min={1}
109
+ max={12}
110
+ step={0.5}
111
+ digits={1}
112
+ unit="px"
113
+ onChange={(pointRadius) => onChange({ pointRadius })}
114
+ />
115
+ </OverlayGroup>
116
+ <OverlayGroup label={PROJECTION_PANEL_SECTION.handling}>
117
+ <OverlaySegmented<CloudGesture>
118
+ label={PROJECTION_PANEL_NAME.cloudGesture}
119
+ help={help.cloudGesture}
120
+ value={options.cloudGesture}
121
+ options={CLOUD_GESTURE_CHOICES}
122
+ onChange={(cloudGesture) => onChange({ cloudGesture })}
123
+ />
124
+ </OverlayGroup>
125
+ <OverlayGroup label={PROJECTION_PANEL_SECTION.selecting}>
126
+ <OverlaySegmented<ScatterSelectionMode>
127
+ label={PROJECTION_PANEL_NAME.selectMode}
128
+ help={help.selectMode}
129
+ value={options.selectMode}
130
+ options={SELECT_MODE_CHOICES}
131
+ onChange={(selectMode) => onChange({ selectMode })}
132
+ />
133
+ </OverlayGroup>
134
+ </OverlayPanel>
135
+ );
136
+ }
137
+
138
+ const AXIS_TAKEN_NOTE = 'Already drawn on another axis.';
139
+
140
+ /** The same three words the map's panel offers, for the same reason. */
141
+ const SELECT_MODE_CHOICES: ReadonlyArray<OverlayOption<ScatterSelectionMode>> =
142
+ [
143
+ { value: 'replace', label: 'Replace' },
144
+ { value: 'add', label: 'Add' },
145
+ { value: 'remove', label: 'Remove' },
146
+ ];
147
+
148
+ /**
149
+ * The axes one picker offers, with the ones already drawn kept but unreachable.
150
+ * @param result - The reduced space being drawn.
151
+ * @param taken - The axes the other two pickers are showing.
152
+ * @returns The choices, in the order the run produced them.
153
+ */
154
+ function axisChoices(
155
+ result: ProjectionResult,
156
+ taken: readonly number[],
157
+ ): OverlayOption[] {
158
+ const choices: OverlayOption[] = [];
159
+ for (const [index, axis] of result.axes.entries()) {
160
+ const drawn = taken.includes(index);
161
+ choices.push({
162
+ value: String(index),
163
+ label: chartAxisTitle(axis.name, { share: axis.share }),
164
+ disabled: drawn,
165
+ title: drawn ? AXIS_TAKEN_NOTE : undefined,
166
+ });
167
+ }
168
+ return choices;
169
+ }
@@ -0,0 +1,217 @@
1
+ import type { ReactElement, ReactNode } from 'react';
2
+ import { useMemo } from 'react';
3
+
4
+ import type { ScatterPointOpen } from '../../scatter/ui/scatterPlotProps.ts';
5
+ import type { SelectionChange } from '../../scatter/ui/useScatterSelection.ts';
6
+ import type { OrbitCamera } from '../../scatter3d/core/orbitCamera.ts';
7
+ import { ScatterCloud } from '../../scatter3d/ui/ScatterCloud.tsx';
8
+ import type { ProjectionCopy } from '../core/projectionCopy.ts';
9
+ import type { ProjectionOptions } from '../core/projectionOptions.ts';
10
+ import type { ProjectionResult } from '../core/projectionResult.ts';
11
+ import type { ResolvedProjectionGroups } from '../core/projectionSamples.ts';
12
+
13
+ import type { SelectionReport } from './ProjectionMapCaption.tsx';
14
+ import { ProjectionMapCaption } from './ProjectionMapCaption.tsx';
15
+ import { ProjectionMapLegend } from './ProjectionMapLegend.tsx';
16
+ import { ProjectionSurface } from './ProjectionSurface.tsx';
17
+ import type { ProjectionMapChrome } from './projectionMapChrome.ts';
18
+ import {
19
+ projectionAxisTitle,
20
+ projectionMapCloud,
21
+ projectionScatterGroups,
22
+ } from './projectionMapModel.ts';
23
+ import { MINIMUM_SHELL_POINTS } from './projectionSpaceModel.ts';
24
+
25
+ /** What {@link ProjectionSpaceTab} needs. */
26
+ export interface ProjectionSpaceTabProps {
27
+ /** What the run produced, whatever produced it. */
28
+ result: ProjectionResult;
29
+ /** The groups, resolved once by the viewer so every tab colours alike. */
30
+ groups: ResolvedProjectionGroups;
31
+ /** What the cloud is showing, already resolved against `result`. */
32
+ options: ProjectionOptions;
33
+ /** The words the cloud writes, already merged over the defaults. */
34
+ copy: ProjectionCopy;
35
+ /** The key's title and entries, built once with the bar's explanation. */
36
+ chrome: ProjectionMapChrome;
37
+ /**
38
+ * What each sample is called, in the score matrix's row order.
39
+ * @default undefined — the setting has nothing to write and draws nothing
40
+ */
41
+ ids?: readonly string[];
42
+ /** Width of the figure, in pixels. */
43
+ width: number;
44
+ /** Height of the figure, in pixels. */
45
+ height: number;
46
+ /** The selected rows, as indices into the score matrix. */
47
+ selected: readonly number[];
48
+ /** Where the reader is standing, which the viewer keeps between visits. */
49
+ camera: OrbitCamera;
50
+ /** Called with the camera every frame of a turn leaves behind. */
51
+ onCameraChange: (camera: OrbitCamera) => void;
52
+ /** How far in the reader has zoomed. */
53
+ zoom: number;
54
+ /** Called with the zoom the wheel asks for. */
55
+ onZoomChange: (zoom: number) => void;
56
+ /**
57
+ * Whether the wheel zooms the box.
58
+ * @default true
59
+ */
60
+ wheelZoom?: boolean;
61
+ /**
62
+ * What the last gesture picked out, for the line over the picture.
63
+ * @default null — nothing has happened yet
64
+ */
65
+ report?: SelectionReport | null;
66
+ /**
67
+ * Called when a lasso is released or a dot is tapped.
68
+ * @default undefined
69
+ */
70
+ onSelectionChange?: (change: SelectionChange) => void;
71
+ /**
72
+ * Called with the row under the pointer, or `-1`.
73
+ * @default undefined
74
+ */
75
+ onHoverChange?: (index: number) => void;
76
+ /**
77
+ * Called when the reader double-clicks a sample.
78
+ * @default undefined
79
+ */
80
+ onPointDoubleClick?: (point: ScatterPointOpen) => void;
81
+ /**
82
+ * The card that follows the pointer, built by the viewer so that it says the
83
+ * same thing on every tab.
84
+ * @default undefined — no card is drawn
85
+ */
86
+ hoverCard?: ReactNode;
87
+ /**
88
+ * Value of the `data-testid` attribute of the figure.
89
+ * @default undefined
90
+ */
91
+ testId?: string;
92
+ }
93
+
94
+ /**
95
+ * The cloud: the same samples as the map, in a box the reader can turn.
96
+ *
97
+ * It earns a tab of its own rather than a switch on the map because it answers
98
+ * a different question. The map shows the two strongest patterns and is the
99
+ * right picture nine times in ten; the cloud is what a reader reaches for when
100
+ * two groups sit on top of each other there, because a third component often
101
+ * pulls them apart and no flat picture can show that.
102
+ *
103
+ * Everything it can share with the map it does share — the colours, the
104
+ * selection, the lasso, the key, the card and the names — so moving between
105
+ * the two tabs changes the picture and nothing else. What it cannot share is
106
+ * the scale: all three axes are stretched to one box, because a solid seen
107
+ * from an angle has nowhere to put three sets of tick labels, and a box drawn
108
+ * at three scales would tilt a group in a direction the data never did.
109
+ * @param props - See {@link ProjectionSpaceTabProps}.
110
+ * @returns The figure.
111
+ */
112
+ export function ProjectionSpaceTab(
113
+ props: ProjectionSpaceTabProps,
114
+ ): ReactElement {
115
+ const { result, groups, options, copy, chrome, width, height } = props;
116
+ const { ids, selected, report = null, camera, zoom } = props;
117
+ const { onCameraChange, onZoomChange, wheelZoom } = props;
118
+ const { onSelectionChange, onHoverChange, hoverCard, testId } = props;
119
+ const { onPointDoubleClick } = props;
120
+ const { scores, fittedCount } = result;
121
+ const { xAxis, yAxis, zAxis, colorBy, pointRadius } = options;
122
+ const { showGroupLabels, showIds, selectMode, cloudGesture } = options;
123
+ const { ellipse: shells } = options;
124
+
125
+ const flat = useMemo(
126
+ () => projectionMapCloud(scores, xAxis, yAxis),
127
+ [scores, xAxis, yAxis],
128
+ );
129
+ const depth = useMemo(
130
+ () => projectionMapCloud(scores, zAxis, zAxis).x,
131
+ [scores, zAxis],
132
+ );
133
+
134
+ const colored = colorBy === 'group' && groups.entries.length > 0;
135
+
136
+ return (
137
+ <div style={{ position: 'relative' }} data-testid={testId}>
138
+ <ScatterCloud
139
+ x={flat.x}
140
+ y={flat.y}
141
+ z={depth}
142
+ width={width}
143
+ height={height}
144
+ xLabel={axisName(result, xAxis)}
145
+ yLabel={axisName(result, yAxis)}
146
+ zLabel={axisName(result, zAxis)}
147
+ groupOf={colored ? groups.groupOf : undefined}
148
+ groups={colored ? projectionScatterGroups(groups) : undefined}
149
+ ellipsoid={colored ? shells : null}
150
+ ellipsoidMinimumPoints={MINIMUM_SHELL_POINTS}
151
+ showGroupLabels={colored && showGroupLabels}
152
+ pointLabels={showIds ? ids : undefined}
153
+ pointRadius={pointRadius}
154
+ outlinedFrom={fittedCount}
155
+ selected={selected}
156
+ onSelectionChange={onSelectionChange}
157
+ selectMode={selectMode}
158
+ onHoverChange={onHoverChange}
159
+ onPointDoubleClick={onPointDoubleClick}
160
+ gesture={cloudGesture}
161
+ camera={camera}
162
+ onCameraChange={onCameraChange}
163
+ zoom={zoom}
164
+ onZoomChange={onZoomChange}
165
+ wheelZoom={wheelZoom}
166
+ overlay={
167
+ <ProjectionSurface>
168
+ {chrome.entries.length === 0 ? null : (
169
+ <ProjectionMapLegend
170
+ title={chrome.title}
171
+ entries={chrome.entries}
172
+ cloud={flat}
173
+ xDomain={CUBE_DOMAIN}
174
+ yDomain={CUBE_DOMAIN}
175
+ width={width}
176
+ height={height}
177
+ />
178
+ )}
179
+ <ProjectionMapCaption
180
+ report={report}
181
+ copy={copy}
182
+ mode={selectMode}
183
+ drawing={false}
184
+ />
185
+ {hoverCard}
186
+ </ProjectionSurface>
187
+ }
188
+ />
189
+ </div>
190
+ );
191
+ }
192
+
193
+ /**
194
+ * What an arm of the frame is called.
195
+ *
196
+ * The frame is the cloud's only record of which axis is which, so an axis the
197
+ * result cannot name is given its number rather than left blank: an unlabelled
198
+ * arm reads as a drawing error, and the reader cannot tell which of the three
199
+ * it was.
200
+ * @param result - The reduced space being drawn.
201
+ * @param index - Which axis.
202
+ * @returns The name to write on that arm.
203
+ */
204
+ function axisName(result: ProjectionResult, index: number): string {
205
+ return projectionAxisTitle(result, index) ?? `Axis ${index + 1}`;
206
+ }
207
+
208
+ /**
209
+ * The range the key measures emptiness against.
210
+ *
211
+ * The key floats in whichever corner of the picture holds fewest dots, and it
212
+ * works that out from the cloud's own coordinates against a range. A turning
213
+ * box has no fixed range in data units — the same sample is at a different
214
+ * corner a second later — so the cube's own is used, which is stable and is
215
+ * what the corners of the picture actually are.
216
+ */
217
+ const CUBE_DOMAIN: readonly [number, number] = [-1, 1];
@@ -15,6 +15,7 @@ import { useProjectionState } from './useProjectionState.ts';
15
15
 
16
16
  export type { ProjectionVariableTrack } from './ProjectionVariablesTab.tsx';
17
17
  export type { ProjectionSelection } from './projectionSelection.ts';
18
+ export type { ProjectionSampleOpen } from './projectionViewerProps.ts';
18
19
  export type { ProjectionViewerProps } from './projectionViewerProps.ts';
19
20
 
20
21
  /**
@@ -45,6 +46,7 @@ export type { ProjectionViewerProps } from './projectionViewerProps.ts';
45
46
  export function ProjectionViewer(props: ProjectionViewerProps): ReactElement {
46
47
  const { result, samples, className, testId, onTrackVariable } = props;
47
48
  const { onHoverSample, touchLasso, height = PROJECTION_TAB_HEIGHT } = props;
49
+ const { onSampleDoubleClick } = props;
48
50
  const { wheelZoom = true } = props;
49
51
  const { formatValue = formatProjectionValue, fileName = 'projection' } =
50
52
  props;
@@ -110,6 +112,7 @@ export function ProjectionViewer(props: ProjectionViewerProps): ReactElement {
110
112
  panelId={panelId}
111
113
  strip={strip}
112
114
  onHoverSample={onHoverSample}
115
+ onSampleDoubleClick={onSampleDoubleClick}
113
116
  onTrackVariable={onTrackVariable}
114
117
  touchLasso={touchLasso}
115
118
  wheelZoom={wheelZoom}
@@ -13,6 +13,7 @@
13
13
 
14
14
  export type { PcaViewerProps } from './PcaViewer.tsx';
15
15
  export { PcaViewer } from './PcaViewer.tsx';
16
+ export type { ProjectionSampleOpen } from './projectionViewerProps.ts';
16
17
  export type { ProjectionSelection } from './projectionSelection.ts';
17
18
  export type { ProjectionVariableTrack } from './projectionVariablesModel.ts';
18
19
  export type { ProjectionViewerProps } from './projectionViewerProps.ts';
@@ -71,7 +71,8 @@ export interface ProjectionBarRoom {
71
71
  */
72
72
  export function projectionBarGlyphs(tab: ProjectionTab): number {
73
73
  if (tab === 'shares') return SAVE_GLYPHS + 1;
74
- return SAVE_GLYPHS + (tab === 'map' ? MAP_BAR_GLYPHS : BAR_GLYPHS);
74
+ const named = tab === 'map' || tab === 'space';
75
+ return SAVE_GLYPHS + (named ? MAP_BAR_GLYPHS : BAR_GLYPHS);
75
76
  }
76
77
 
77
78
  /** The one that takes the figure off the page, on every tab. */
@@ -80,7 +81,7 @@ const SAVE_GLYPHS = 1;
80
81
  /** The question mark and the cog, which almost every tab carries. */
81
82
  const BAR_GLYPHS = 2;
82
83
 
83
- /** Those two, and the map's own pair of switches. */
84
+ /** Those two, and the pair of name switches the map and the cloud both carry. */
84
85
  const MAP_BAR_GLYPHS = 4;
85
86
 
86
87
  /**
@@ -15,6 +15,7 @@ import type { ResolvedProjectionGroups } from '../core/projectionSamples.ts';
15
15
  import type { ProjectionTab } from '../core/projectionTabs.ts';
16
16
 
17
17
  import { ellipseCoverageText } from './projectionEllipse.ts';
18
+ import { ellipsoidCoverageText } from './projectionEllipsoid.ts';
18
19
  import { WHOLE_SHARE } from './projectionTabStyles.ts';
19
20
 
20
21
  /** One setting on the bar, as the reader sees it right now. */
@@ -81,6 +82,23 @@ export function projectionBarReadings(
81
82
  },
82
83
  ];
83
84
  }
85
+ if (tab === 'space') {
86
+ return [
87
+ {
88
+ key: bar.key.cloudGesture,
89
+ label: help.cloudGesture.title,
90
+ value: options.cloudGesture === 'turn' ? 'Turn' : 'Select',
91
+ },
92
+ {
93
+ key: bar.key.ellipse,
94
+ label: help.ellipse.title,
95
+ value:
96
+ options.colorBy === 'group' && groups.entries.length > 0
97
+ ? ellipsoidCoverageText(options.ellipse)
98
+ : bar.noOutlines,
99
+ },
100
+ ];
101
+ }
84
102
  if (tab === 'shares') {
85
103
  return [
86
104
  {