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
@@ -21,11 +21,19 @@ export type {
21
21
  FigureSvgDocumentOptions,
22
22
  } from './figureSvgDocument.ts';
23
23
  export { figureSvgDocument } from './figureSvgDocument.ts';
24
+ export type {
25
+ FigureLegendCard,
26
+ FigureLegendMark,
27
+ FigureLegendPrint,
28
+ FigureLegendText,
29
+ } from './figureLegend.ts';
30
+ export { figureLegendMarkup } from './figureLegend.ts';
24
31
  export type { FigureBounds } from './figureTarget.ts';
25
32
  export {
26
33
  figureBounds,
27
34
  figureDrawings,
28
35
  figureElement,
36
+ figureLegends,
29
37
  figureSize,
30
38
  } from './figureTarget.ts';
31
39
  export { sanitizeFileName } from './sanitizeFileName.ts';
@@ -7,6 +7,7 @@ export type SiteId =
7
7
  | 'smiles'
8
8
  | 'chemcalc'
9
9
  | 'nmrium'
10
+ | 'metabo'
10
11
  | 'derepflow'
11
12
  | 'surge'
12
13
  | 'tex'
@@ -133,6 +134,19 @@ export const ECOSYSTEM_SITES: readonly EcosystemSite[] = [
133
134
  brandAlt: '#2b143e',
134
135
  mark: { plate: '#2b143e', accent: '#ea580c' },
135
136
  },
137
+ {
138
+ id: 'metabo',
139
+ // Not `.cheminfo`: the second half of a one-word name is the domain the
140
+ // site actually lives at, and this one is a sibling of NMRium rather than
141
+ // of the cheminfo tools.
142
+ name: { lead: 'metabo', alt: 'nmrium', dot: true },
143
+ host: 'metabo.nmrium.com',
144
+ repository: 'https://github.com/cheminfo/metabo',
145
+ tagline: 'Turn a batch of NMR spectra into a map of your samples.',
146
+ brand: '#4d7c0f',
147
+ brandAlt: '#b91c1c',
148
+ mark: { plate: '#4d7c0f', accent: '#b91c1c' },
149
+ },
136
150
  {
137
151
  id: 'derepflow',
138
152
  name: { lead: 'Derep', alt: 'Flow' },
@@ -148,6 +148,32 @@ export const GLYPHS: Record<SiteId, (alt: string) => ReactNode> = {
148
148
  />
149
149
  </g>
150
150
  ),
151
+ // Two groups of samples with the outline that holds each: the picture the
152
+ // tool exists to draw. One is drawn and one is filled rather than both
153
+ // outlined, because two rings crossing close up into a blob at 16 px —
154
+ // checked at 16, 20, 24, 32 and 64 before this was settled on.
155
+ metabo: (accent) => (
156
+ <>
157
+ <ellipse
158
+ cx="12"
159
+ cy="12.5"
160
+ rx="5.8"
161
+ ry="9"
162
+ fill="none"
163
+ stroke="#ffffff"
164
+ strokeWidth="3"
165
+ transform="rotate(-32 12 12.5)"
166
+ />
167
+ <ellipse
168
+ cx="20.5"
169
+ cy="20"
170
+ rx="5.4"
171
+ ry="8.6"
172
+ fill={accent}
173
+ transform="rotate(32 20.5 20)"
174
+ />
175
+ </>
176
+ ),
151
177
  // One formula branching into every structure it can be.
152
178
  surge: (alt) => (
153
179
  <>
@@ -103,7 +103,13 @@ export function OverlayLegend(props: OverlayLegendProps): ReactElement {
103
103
  const written = folded === 0 ? entries : entries.slice(0, room - 1);
104
104
 
105
105
  return (
106
- <div style={overlayCardStyle(placement, metrics)} data-testid={testId}>
106
+ <div
107
+ style={overlayCardStyle(placement, metrics)}
108
+ // The key is chrome, since its entries are pressed, but it is part of
109
+ // the picture all the same: the export redraws it into the saved file.
110
+ data-figure="legend"
111
+ data-testid={testId}
112
+ >
107
113
  <div style={overlayGroundStyle(awake ? 1 : RESTING_OPACITY, busy)} />
108
114
  <div
109
115
  role="group"
@@ -29,6 +29,8 @@ export interface ProjectionCopy {
29
29
  intro: {
30
30
  /** The map. */
31
31
  map: string;
32
+ /** The cloud. */
33
+ space: string;
32
34
  /** The pair grid. */
33
35
  pairs: string;
34
36
  /**
@@ -49,6 +51,8 @@ export interface ProjectionCopy {
49
51
  legend: {
50
52
  /** The map. Carries `{groups}` and `{coverage}`. */
51
53
  map: string;
54
+ /** The cloud. Carries `{groups}` and `{coverage}`. */
55
+ space: string;
52
56
  /** The map with no outlines. Carries `{groups}`. */
53
57
  mapNoEllipse: string;
54
58
  /** The pair grid. Carries `{groups}`. */
@@ -126,6 +130,7 @@ export interface ProjectionCopy {
126
130
  export const PROJECTION_COPY: ProjectionCopy = {
127
131
  tab: {
128
132
  map: 'Map',
133
+ space: 'Map in 3D',
129
134
  pairs: 'Every pair',
130
135
  variables: 'What differs',
131
136
  shares: 'How much each explains',
@@ -134,6 +139,8 @@ export const PROJECTION_COPY: ProjectionCopy = {
134
139
  bar: PROJECTION_BAR_WORDS,
135
140
  intro: {
136
141
  map: 'Each dot is one sample. Dots that sit together are alike; dots far apart are the ones that differ most. The two axes are the strongest patterns of difference, called components.',
142
+ space:
143
+ 'The same map with a third component, in a box you can turn. Two groups that sit on top of each other on the flat map often come apart as soon as the box moves. The frame says which axis is which; the numbers are in the card you get by pointing at a dot.',
137
144
  pairs:
138
145
  'The same map drawn for every pair of components. A grouping the first two miss often shows up in another pair.',
139
146
  variablesContinuous:
@@ -149,6 +156,8 @@ export const PROJECTION_COPY: ProjectionCopy = {
149
156
  },
150
157
  legend: {
151
158
  map: 'Colour = {groups}. Each outline covers about {coverage} of that group, assuming the group is roughly bell-shaped.',
159
+ space:
160
+ 'Colour = {groups}. Each shell holds about {coverage} of that group, assuming the group is roughly bell-shaped. A shell has to hold a sample in three directions at once, so it is wider than the outline the same share draws on the flat map.',
152
161
  mapNoEllipse: 'Colour = {groups}.',
153
162
  pairs:
154
163
  'Colour = {groups}. The strip along the diagonal shows how the samples spread out along that component on its own.',
@@ -1,5 +1,6 @@
1
1
  import type { EllipseSize } from '../../scatter/core/confidenceEllipse.ts';
2
2
  import type { ScatterSelectionMode } from '../../scatter/core/scatterSelection.ts';
3
+ import type { CloudGesture } from '../../scatter3d/core/cloudGesture.ts';
3
4
 
4
5
  import type { ProjectionResult } from './projectionResult.ts';
5
6
  import { drawableVariablesView } from './variablesView.ts';
@@ -33,6 +34,17 @@ export interface ProjectionOptions {
33
34
  * @default 1
34
35
  */
35
36
  yAxis: number;
37
+ /**
38
+ * Which axis runs away from the reader, on the cloud alone. The map and the
39
+ * pair grid never read it, so moving it leaves them where they were.
40
+ * @default 2
41
+ */
42
+ zAxis: number;
43
+ /**
44
+ * What a drag over the cloud does: turn the box, or draw a lasso.
45
+ * @default 'turn'
46
+ */
47
+ cloudGesture: CloudGesture;
36
48
  /**
37
49
  * What decides a dot's colour.
38
50
  * @default 'group'
@@ -123,6 +135,8 @@ export type ProjectionOptionId = keyof ProjectionOptions;
123
135
  export const DEFAULT_PROJECTION_OPTIONS: ProjectionOptions = {
124
136
  xAxis: 0,
125
137
  yAxis: 1,
138
+ zAxis: 2,
139
+ cloudGesture: 'turn',
126
140
  colorBy: 'group',
127
141
  ellipse: { kind: 'coverage', probability: 0.95 },
128
142
  pointRadius: 3.5,
@@ -163,14 +177,21 @@ export function resolveProjectionOptions(
163
177
  const axisCount = result.axes.length;
164
178
  const lastAxis = Math.max(0, axisCount - 1);
165
179
  const xAxis = clampIndex(pick(overrides, 'xAxis'), lastAxis);
180
+ const yAxis = apart(
181
+ clampIndex(pick(overrides, 'yAxis'), lastAxis),
182
+ xAxis,
183
+ lastAxis,
184
+ );
166
185
 
167
186
  return {
168
187
  xAxis,
169
- yAxis: apart(
170
- clampIndex(pick(overrides, 'yAxis'), lastAxis),
171
- xAxis,
188
+ yAxis,
189
+ zAxis: apartFrom(
190
+ clampIndex(pick(overrides, 'zAxis'), lastAxis),
191
+ [xAxis, yAxis],
172
192
  lastAxis,
173
193
  ),
194
+ cloudGesture: pick(overrides, 'cloudGesture'),
174
195
  colorBy: pick(overrides, 'colorBy'),
175
196
  ellipse: pick(overrides, 'ellipse'),
176
197
  pointRadius: atLeast(pick(overrides, 'pointRadius'), 3.5, 0),
@@ -223,6 +244,30 @@ function apart(yAxis: number, xAxis: number, lastAxis: number): number {
223
244
  return xAxis > 0 ? xAxis - 1 : Math.min(1, lastAxis);
224
245
  }
225
246
 
247
+ /**
248
+ * An axis that is none of the ones already drawn.
249
+ *
250
+ * The cloud's third direction has two axes to avoid rather than one, and an
251
+ * axis drawn against itself in a box is a flat map stood on its edge — worse
252
+ * than the diagonal line it is on a map, because the reader cannot see from
253
+ * the picture that it happened.
254
+ * @param wanted - The axis asked for, already inside the result.
255
+ * @param taken - The axes already drawn.
256
+ * @param lastAxis - The highest axis there is.
257
+ * @returns The axis to draw; the lowest one still free when the wanted one is taken, and the wanted one back when every axis is.
258
+ */
259
+ function apartFrom(
260
+ wanted: number,
261
+ taken: readonly number[],
262
+ lastAxis: number,
263
+ ): number {
264
+ if (!taken.includes(wanted)) return wanted;
265
+ for (let axis = 0; axis <= lastAxis; axis++) {
266
+ if (!taken.includes(axis)) return axis;
267
+ }
268
+ return wanted;
269
+ }
270
+
226
271
  function clampCount(
227
272
  value: number,
228
273
  fallback: number,
@@ -58,6 +58,14 @@ export const PROJECTION_HELP: Record<ProjectionOptionId, ProjectionHelp> = {
58
58
  title: 'Up',
59
59
  body: 'Which pattern of difference runs bottom to top.',
60
60
  },
61
+ zAxis: {
62
+ title: 'Into',
63
+ body: 'Which pattern of difference runs away from you, into the box. Only the cloud draws it; the map and the pair grid never read it.',
64
+ },
65
+ cloudGesture: {
66
+ title: 'Drag does',
67
+ body: 'Turning the box and drawing a lasso both want a plain drag, so you say which one it is. Inside a lasso, Shift still adds and Alt still takes away.',
68
+ },
61
69
  colorBy: {
62
70
  title: 'Colour by',
63
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.',
@@ -137,6 +145,7 @@ export const PROJECTION_HELP: Record<ProjectionOptionId, ProjectionHelp> = {
137
145
  */
138
146
  export const PROJECTION_SHORT_TAB: Record<ProjectionTab, string> = {
139
147
  map: 'Map',
148
+ space: '3D',
140
149
  pairs: 'Pairs',
141
150
  variables: 'Differs',
142
151
  shares: 'Explains',
@@ -155,6 +164,8 @@ export interface ProjectionBarWords {
155
164
  colorBy: string;
156
165
  /** In front of how much of a group its outline covers. */
157
166
  ellipse: string;
167
+ /** In front of what a drag over the cloud does. */
168
+ cloudGesture: string;
158
169
  /** In front of how many components the pair grid lays out. */
159
170
  pairCount: string;
160
171
  /** In front of what the "what differs" panels are drawing. */
@@ -197,6 +208,7 @@ export const PROJECTION_BAR_WORDS: ProjectionBarWords = {
197
208
  key: {
198
209
  colorBy: 'Colour',
199
210
  ellipse: 'Outlines',
211
+ cloudGesture: 'Drag',
200
212
  pairCount: 'Components',
201
213
  variablesView: 'Show',
202
214
  shareTarget: 'Target',
@@ -228,6 +240,8 @@ export const PROJECTION_BAR_WORDS: ProjectionBarWords = {
228
240
  export const PROJECTION_PANEL_NAME: Record<ProjectionOptionId, string> = {
229
241
  xAxis: 'Across',
230
242
  yAxis: 'Up',
243
+ zAxis: 'Into',
244
+ cloudGesture: 'Drag does',
231
245
  colorBy: 'Colour by',
232
246
  ellipse: 'Outlines',
233
247
  pointRadius: 'Dot size',
@@ -260,6 +274,8 @@ export const PROJECTION_PANEL_SECTION = {
260
274
  drawing: 'Drawing',
261
275
  /** What a drag over the figure does to the selection. */
262
276
  selecting: 'Selecting',
277
+ /** What a drag over the cloud does at all, before the selection hears of it. */
278
+ handling: 'Handling',
263
279
  /** What the panels of the "what differs" tab are measured against. */
264
280
  scale: 'Scale',
265
281
  } as const;
@@ -31,6 +31,18 @@ export const PROJECTION_TAB_DEFAULTS: Record<
31
31
  showIds: DEFAULT_PROJECTION_OPTIONS.showIds,
32
32
  selectMode: DEFAULT_PROJECTION_OPTIONS.selectMode,
33
33
  },
34
+ space: {
35
+ xAxis: DEFAULT_PROJECTION_OPTIONS.xAxis,
36
+ yAxis: DEFAULT_PROJECTION_OPTIONS.yAxis,
37
+ zAxis: DEFAULT_PROJECTION_OPTIONS.zAxis,
38
+ cloudGesture: DEFAULT_PROJECTION_OPTIONS.cloudGesture,
39
+ colorBy: DEFAULT_PROJECTION_OPTIONS.colorBy,
40
+ ellipse: DEFAULT_PROJECTION_OPTIONS.ellipse,
41
+ pointRadius: DEFAULT_PROJECTION_OPTIONS.pointRadius,
42
+ showGroupLabels: DEFAULT_PROJECTION_OPTIONS.showGroupLabels,
43
+ showIds: DEFAULT_PROJECTION_OPTIONS.showIds,
44
+ selectMode: DEFAULT_PROJECTION_OPTIONS.selectMode,
45
+ },
34
46
  pairs: {
35
47
  pairCount: DEFAULT_PROJECTION_OPTIONS.pairCount,
36
48
  colorBy: DEFAULT_PROJECTION_OPTIONS.colorBy,
@@ -1,10 +1,18 @@
1
1
  import type { ProjectionAxis, ProjectionResult } from './projectionResult.ts';
2
2
 
3
3
  /** Which tab of a projection viewer is showing. The ids are stable, so a site may put one in its own URL. */
4
- export type ProjectionTab = 'map' | 'pairs' | 'variables' | 'shares';
4
+ export type ProjectionTab = 'map' | 'space' | 'pairs' | 'variables' | 'shares';
5
5
 
6
6
  const PAIRS_MINIMUM_AXES = 3;
7
7
 
8
+ /**
9
+ * How many axes a cloud needs, which is one per direction of the box.
10
+ *
11
+ * A run that produced two is not shown a third empty axis: the cloud would be
12
+ * a flat map stood on its edge, which says less than the map it came from.
13
+ */
14
+ const SPACE_MINIMUM_AXES = 3;
15
+
8
16
  /**
9
17
  * The tabs a result can actually fill.
10
18
  *
@@ -32,6 +40,7 @@ export function projectionTabs(
32
40
  const tabs: ProjectionTab[] = ['map'];
33
41
  const enoughAxes = result.axes.length >= PAIRS_MINIMUM_AXES;
34
42
 
43
+ if (result.axes.length >= SPACE_MINIMUM_AXES) tabs.push('space');
35
44
  if (enoughAxes) tabs.push('pairs');
36
45
  if (result.loadings !== undefined) tabs.push('variables');
37
46
  if (enoughAxes && everyAxisPublishesAShare(result.axes)) tabs.push('shares');
@@ -3,6 +3,7 @@ import { useMemo, useState } from 'react';
3
3
 
4
4
  import type { ChartViewport } from '../../chart/core/chartViewport.ts';
5
5
  import { ScatterPlot } from '../../scatter/ui/ScatterPlot.tsx';
6
+ import type { ScatterPointOpen } from '../../scatter/ui/scatterPlotProps.ts';
6
7
  import type { SelectionChange } from '../../scatter/ui/useScatterSelection.ts';
7
8
  import type { ProjectionCopy } from '../core/projectionCopy.ts';
8
9
  import type { ProjectionOptions } from '../core/projectionOptions.ts';
@@ -82,6 +83,11 @@ export interface ProjectionMapTabProps {
82
83
  * @default undefined
83
84
  */
84
85
  onHoverChange?: (index: number) => void;
86
+ /**
87
+ * Called when the reader double-clicks a sample.
88
+ * @default undefined
89
+ */
90
+ onPointDoubleClick?: (point: ScatterPointOpen) => void;
85
91
  /**
86
92
  * The card that follows the pointer. The viewer builds it, because it has to
87
93
  * say the same thing on every tab.
@@ -121,6 +127,7 @@ export function ProjectionMapTab(props: ProjectionMapTabProps): ReactElement {
121
127
  const { result, groups, options, copy, chrome, width, height } = props;
122
128
  const { ids, selected, viewport = null, report = null } = props;
123
129
  const { onSelectionChange, onHoverChange, hoverCard } = props;
130
+ const { onPointDoubleClick } = props;
124
131
  const { onViewportChange, wheelZoom, touchLasso, testId } = props;
125
132
  const { scores, fittedCount, markers } = result;
126
133
  const { xAxis, yAxis, colorBy, showGroupMeans, pointRadius } = options;
@@ -173,6 +180,7 @@ export function ProjectionMapTab(props: ProjectionMapTabProps): ReactElement {
173
180
  onSelectionChange={onSelectionChange}
174
181
  selectMode={selectMode}
175
182
  onHoverChange={onHoverChange}
183
+ onPointDoubleClick={onPointDoubleClick}
176
184
  onLassoChange={setDrawing}
177
185
  touchLasso={touchLasso}
178
186
  testId={testId}
@@ -1,6 +1,7 @@
1
1
  import type { PointerEvent, ReactElement } from 'react';
2
2
  import { useRef, useState } from 'react';
3
3
 
4
+ import type { ScatterPointOpen } from '../../scatter/ui/scatterPlotProps.ts';
4
5
  import type { ProjectionCopy } from '../core/projectionCopy.ts';
5
6
  import type { ProjectionResult } from '../core/projectionResult.ts';
6
7
  import type { ProjectionSamples } from '../core/projectionSamples.ts';
@@ -9,10 +10,12 @@ import { ProjectionMapTab } from './ProjectionMapTab.tsx';
9
10
  import { ProjectionPairsTab } from './ProjectionPairsTab.tsx';
10
11
  import { ProjectionReadout } from './ProjectionReadout.tsx';
11
12
  import { ProjectionSharesTab } from './ProjectionSharesTab.tsx';
13
+ import { ProjectionSpaceTab } from './ProjectionSpaceTab.tsx';
12
14
  import type { ProjectionVariableTrack } from './ProjectionVariablesTab.tsx';
13
15
  import { ProjectionVariablesTab } from './ProjectionVariablesTab.tsx';
14
16
  import type { ProjectionMapView } from './projectionMapView.ts';
15
17
  import type { ProjectionModels } from './projectionTabModels.ts';
18
+ import type { ProjectionSampleOpen } from './projectionViewerProps.ts';
16
19
  import type { ProjectionStateApi } from './useProjectionState.ts';
17
20
 
18
21
  /** What {@link ProjectionPanel} needs. */
@@ -46,6 +49,11 @@ export interface ProjectionPanelProps {
46
49
  * @default undefined
47
50
  */
48
51
  onHoverSample?: (id: string | null) => void;
52
+ /**
53
+ * Called when the reader double-clicks a sample, on either figure.
54
+ * @default undefined
55
+ */
56
+ onSampleDoubleClick?: (sample: ProjectionSampleOpen) => void;
49
57
  /**
50
58
  * Called as the pointer crosses the "what differs" panels.
51
59
  * @default undefined
@@ -83,6 +91,7 @@ export function ProjectionPanel(props: ProjectionPanelProps): ReactElement {
83
91
  const { state, result, samples, copy, models, map } = props;
84
92
  const { width, height, formatValue, baseId, panelId, strip } = props;
85
93
  const { onHoverSample, onTrackVariable, touchLasso, wheelZoom } = props;
94
+ const { onSampleDoubleClick } = props;
86
95
 
87
96
  const box = useRef<HTMLDivElement>(null);
88
97
  const pointer = useRef(NOWHERE);
@@ -102,6 +111,19 @@ export function ProjectionPanel(props: ProjectionPanelProps): ReactElement {
102
111
  onHoverSample?.(index < 0 ? null : (samples.ids[index] ?? null));
103
112
  }
104
113
 
114
+ // The figures speak in row numbers, because that is what a hit test hands
115
+ // back; every callback out of the viewer speaks in names, so a caller never
116
+ // has to keep a second copy of the row order. The translation is one line
117
+ // and it belongs here, once, rather than in each of the two tabs.
118
+ const openSample =
119
+ onSampleDoubleClick === undefined
120
+ ? undefined
121
+ : (point: ScatterPointOpen) => {
122
+ const id = samples.ids[point.index];
123
+ if (id === undefined) return;
124
+ onSampleDoubleClick({ ...point, id });
125
+ };
126
+
105
127
  return (
106
128
  <div
107
129
  ref={box}
@@ -143,6 +165,46 @@ export function ProjectionPanel(props: ProjectionPanelProps): ReactElement {
143
165
  height={height}
144
166
  />
145
167
  ) : null}
168
+ {state.tab === 'space' ? (
169
+ <ProjectionSpaceTab
170
+ result={result}
171
+ groups={state.groups}
172
+ options={state.options}
173
+ copy={copy}
174
+ chrome={map.spaceChrome}
175
+ ids={samples.ids}
176
+ width={width}
177
+ height={height}
178
+ selected={state.selected}
179
+ camera={map.camera}
180
+ onCameraChange={map.setCamera}
181
+ zoom={map.zoom}
182
+ onZoomChange={map.setZoom}
183
+ wheelZoom={wheelZoom}
184
+ report={map.report}
185
+ onSelectionChange={map.settle}
186
+ onHoverChange={noteHover}
187
+ onPointDoubleClick={openSample}
188
+ hoverCard={
189
+ anchor.index < 0 ? null : (
190
+ <ProjectionReadout
191
+ index={anchor.index}
192
+ result={result}
193
+ samples={samples}
194
+ groups={state.groups}
195
+ xAxis={state.options.xAxis}
196
+ yAxis={state.options.yAxis}
197
+ zAxis={state.options.zAxis}
198
+ x={anchor.x}
199
+ y={anchor.y}
200
+ boxWidth={width}
201
+ boxHeight={height}
202
+ formatValue={formatValue}
203
+ />
204
+ )
205
+ }
206
+ />
207
+ ) : null}
146
208
  {state.tab === 'map' ? (
147
209
  <ProjectionMapTab
148
210
  result={result}
@@ -160,6 +222,7 @@ export function ProjectionPanel(props: ProjectionPanelProps): ReactElement {
160
222
  selected={state.selected}
161
223
  onSelectionChange={map.settle}
162
224
  onHoverChange={noteHover}
225
+ onPointDoubleClick={openSample}
163
226
  touchLasso={touchLasso}
164
227
  hoverCard={
165
228
  anchor.index < 0 ? null : (
@@ -25,6 +25,13 @@ export interface ProjectionReadoutProps {
25
25
  xAxis: number;
26
26
  /** Which axis is drawn vertically. */
27
27
  yAxis: number;
28
+ /**
29
+ * Which axis runs away from the reader, on the cloud. It is the one place a
30
+ * cloud's third number is written at all: the box carries no tick labels, so
31
+ * a reader who wants a value points at the sample.
32
+ * @default undefined — the card names two axes, as the map's does
33
+ */
34
+ zAxis?: number;
28
35
  /** Where the pointer is, in pixels from the figure's left. */
29
36
  x: number;
30
37
  /** Where the pointer is, in pixels from the figure's top. */
@@ -80,6 +87,7 @@ export function ProjectionReadout(props: ProjectionReadoutProps): ReactElement {
80
87
  groups,
81
88
  xAxis,
82
89
  yAxis,
90
+ zAxis,
83
91
  x,
84
92
  y,
85
93
  boxWidth,
@@ -91,10 +99,13 @@ export function ProjectionReadout(props: ProjectionReadoutProps): ReactElement {
91
99
  testId,
92
100
  } = props;
93
101
 
102
+ const drawn = useMemo(
103
+ () => (zAxis === undefined ? [xAxis, yAxis] : [xAxis, yAxis, zAxis]),
104
+ [xAxis, yAxis, zAxis],
105
+ );
94
106
  const rows = useMemo(
95
- () =>
96
- readoutRows(index, result, samples, groups, xAxis, yAxis, formatValue),
97
- [formatValue, groups, index, result, samples, xAxis, yAxis],
107
+ () => readoutRows(index, result, samples, groups, drawn, formatValue),
108
+ [drawn, formatValue, groups, index, result, samples],
98
109
  );
99
110
 
100
111
  return (
@@ -118,15 +129,14 @@ function readoutRows(
118
129
  result: ProjectionResult,
119
130
  samples: ProjectionSamples,
120
131
  groups: ResolvedProjectionGroups,
121
- xAxis: number,
122
- yAxis: number,
132
+ drawn: readonly number[],
123
133
  formatValue: (value: number) => string,
124
134
  ): OverlayReadoutRow[] {
125
135
  const rows: OverlayReadoutRow[] = [];
126
136
  const { axes, scores } = result;
127
137
  const known = index >= 0 && index < scores.rows;
128
138
 
129
- for (const axis of [xAxis, yAxis]) {
139
+ for (const axis of drawn) {
130
140
  const name = axes[axis]?.name;
131
141
  if (name === undefined) continue;
132
142
  rows.push({