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.
- package/lib/chrome/ui/NavLink.d.ts.map +1 -1
- package/lib/chrome/ui/NavLink.js +4 -3
- package/lib/chrome/ui/NavLink.js.map +1 -1
- package/lib/chrome/ui/SiteFooter.d.ts +8 -0
- package/lib/chrome/ui/SiteFooter.d.ts.map +1 -1
- package/lib/chrome/ui/SiteFooter.js +7 -2
- package/lib/chrome/ui/SiteFooter.js.map +1 -1
- package/lib/chrome/ui/SiteHeader.d.ts +11 -0
- package/lib/chrome/ui/SiteHeader.d.ts.map +1 -1
- package/lib/chrome/ui/SiteHeader.js +7 -2
- package/lib/chrome/ui/SiteHeader.js.map +1 -1
- package/lib/chrome/ui/navItem.d.ts +12 -0
- package/lib/chrome/ui/navItem.d.ts.map +1 -1
- package/lib/chrome/ui/navItem.js.map +1 -1
- package/lib/core.d.ts +1 -0
- package/lib/core.d.ts.map +1 -1
- package/lib/core.js +1 -0
- package/lib/core.js.map +1 -1
- package/lib/download/core/figureLegend.d.ts +90 -0
- package/lib/download/core/figureLegend.d.ts.map +1 -0
- package/lib/download/core/figureLegend.js +77 -0
- package/lib/download/core/figureLegend.js.map +1 -0
- package/lib/download/core/figureSvg.d.ts.map +1 -1
- package/lib/download/core/figureSvg.js +71 -1
- package/lib/download/core/figureSvg.js.map +1 -1
- package/lib/download/core/figureSvgDocument.d.ts +9 -2
- package/lib/download/core/figureSvgDocument.d.ts.map +1 -1
- package/lib/download/core/figureSvgDocument.js +4 -1
- package/lib/download/core/figureSvgDocument.js.map +1 -1
- package/lib/download/core/figureTarget.d.ts +11 -0
- package/lib/download/core/figureTarget.d.ts.map +1 -1
- package/lib/download/core/figureTarget.js +19 -0
- package/lib/download/core/figureTarget.js.map +1 -1
- package/lib/download/core/index.d.ts +3 -1
- package/lib/download/core/index.d.ts.map +1 -1
- package/lib/download/core/index.js +2 -1
- package/lib/download/core/index.js.map +1 -1
- package/lib/ecosystem/core/sites.d.ts +1 -1
- package/lib/ecosystem/core/sites.d.ts.map +1 -1
- package/lib/ecosystem/core/sites.js +13 -0
- package/lib/ecosystem/core/sites.js.map +1 -1
- package/lib/ecosystem/ui/glyphs.d.ts.map +1 -1
- package/lib/ecosystem/ui/glyphs.js +5 -0
- package/lib/ecosystem/ui/glyphs.js.map +1 -1
- package/lib/overlay/ui/OverlayLegend.d.ts.map +1 -1
- package/lib/overlay/ui/OverlayLegend.js +1 -1
- package/lib/overlay/ui/OverlayLegend.js.map +1 -1
- package/lib/projection/core/projectionCopy.d.ts +4 -0
- package/lib/projection/core/projectionCopy.d.ts.map +1 -1
- package/lib/projection/core/projectionCopy.js +3 -0
- package/lib/projection/core/projectionCopy.js.map +1 -1
- package/lib/projection/core/projectionOptions.d.ts +12 -0
- package/lib/projection/core/projectionOptions.d.ts.map +1 -1
- package/lib/projection/core/projectionOptions.js +27 -1
- package/lib/projection/core/projectionOptions.js.map +1 -1
- package/lib/projection/core/projectionStrings.d.ts +4 -0
- package/lib/projection/core/projectionStrings.d.ts.map +1 -1
- package/lib/projection/core/projectionStrings.js +14 -0
- package/lib/projection/core/projectionStrings.js.map +1 -1
- package/lib/projection/core/projectionTabDefaults.d.ts.map +1 -1
- package/lib/projection/core/projectionTabDefaults.js +12 -0
- package/lib/projection/core/projectionTabDefaults.js.map +1 -1
- package/lib/projection/core/projectionTabs.d.ts +1 -1
- package/lib/projection/core/projectionTabs.d.ts.map +1 -1
- package/lib/projection/core/projectionTabs.js +9 -0
- package/lib/projection/core/projectionTabs.js.map +1 -1
- package/lib/projection/ui/ProjectionMapTab.d.ts +6 -0
- package/lib/projection/ui/ProjectionMapTab.d.ts.map +1 -1
- package/lib/projection/ui/ProjectionMapTab.js +2 -1
- package/lib/projection/ui/ProjectionMapTab.js.map +1 -1
- package/lib/projection/ui/ProjectionPanel.d.ts +6 -0
- package/lib/projection/ui/ProjectionPanel.d.ts.map +1 -1
- package/lib/projection/ui/ProjectionPanel.js +15 -1
- package/lib/projection/ui/ProjectionPanel.js.map +1 -1
- package/lib/projection/ui/ProjectionReadout.d.ts +7 -0
- package/lib/projection/ui/ProjectionReadout.d.ts.map +1 -1
- package/lib/projection/ui/ProjectionReadout.js +5 -4
- package/lib/projection/ui/ProjectionReadout.js.map +1 -1
- package/lib/projection/ui/ProjectionSpaceControls.d.ts +51 -0
- package/lib/projection/ui/ProjectionSpaceControls.d.ts.map +1 -0
- package/lib/projection/ui/ProjectionSpaceControls.js +71 -0
- package/lib/projection/ui/ProjectionSpaceControls.js.map +1 -0
- package/lib/projection/ui/ProjectionSpaceMore.d.ts +32 -0
- package/lib/projection/ui/ProjectionSpaceMore.d.ts.map +1 -0
- package/lib/projection/ui/ProjectionSpaceMore.js +57 -0
- package/lib/projection/ui/ProjectionSpaceMore.js.map +1 -0
- package/lib/projection/ui/ProjectionSpaceTab.d.ts +98 -0
- package/lib/projection/ui/ProjectionSpaceTab.d.ts.map +1 -0
- package/lib/projection/ui/ProjectionSpaceTab.js +66 -0
- package/lib/projection/ui/ProjectionSpaceTab.js.map +1 -0
- package/lib/projection/ui/ProjectionViewer.d.ts +1 -0
- package/lib/projection/ui/ProjectionViewer.d.ts.map +1 -1
- package/lib/projection/ui/ProjectionViewer.js +2 -1
- package/lib/projection/ui/ProjectionViewer.js.map +1 -1
- package/lib/projection/ui/index.d.ts +1 -0
- package/lib/projection/ui/index.d.ts.map +1 -1
- package/lib/projection/ui/index.js.map +1 -1
- package/lib/projection/ui/projectionBarModel.d.ts.map +1 -1
- package/lib/projection/ui/projectionBarModel.js +3 -2
- package/lib/projection/ui/projectionBarModel.js.map +1 -1
- package/lib/projection/ui/projectionBarReadings.d.ts.map +1 -1
- package/lib/projection/ui/projectionBarReadings.js +17 -0
- package/lib/projection/ui/projectionBarReadings.js.map +1 -1
- package/lib/projection/ui/projectionBarSlots.d.ts.map +1 -1
- package/lib/projection/ui/projectionBarSlots.js +17 -0
- package/lib/projection/ui/projectionBarSlots.js.map +1 -1
- package/lib/projection/ui/projectionCloudChoices.d.ts +18 -0
- package/lib/projection/ui/projectionCloudChoices.d.ts.map +1 -0
- package/lib/projection/ui/projectionCloudChoices.js +19 -0
- package/lib/projection/ui/projectionCloudChoices.js.map +1 -0
- package/lib/projection/ui/projectionEllipsoid.d.ts +38 -0
- package/lib/projection/ui/projectionEllipsoid.d.ts.map +1 -0
- package/lib/projection/ui/projectionEllipsoid.js +71 -0
- package/lib/projection/ui/projectionEllipsoid.js.map +1 -0
- package/lib/projection/ui/projectionMapChrome.d.ts +8 -0
- package/lib/projection/ui/projectionMapChrome.d.ts.map +1 -1
- package/lib/projection/ui/projectionMapChrome.js +10 -6
- package/lib/projection/ui/projectionMapChrome.js.map +1 -1
- package/lib/projection/ui/projectionMapView.d.ts +13 -0
- package/lib/projection/ui/projectionMapView.d.ts.map +1 -1
- package/lib/projection/ui/projectionMapView.js +26 -0
- package/lib/projection/ui/projectionMapView.js.map +1 -1
- package/lib/projection/ui/projectionSpaceModel.d.ts +14 -0
- package/lib/projection/ui/projectionSpaceModel.d.ts.map +1 -0
- package/lib/projection/ui/projectionSpaceModel.js +14 -0
- package/lib/projection/ui/projectionSpaceModel.js.map +1 -0
- package/lib/projection/ui/projectionViewerProps.d.ts +34 -0
- package/lib/projection/ui/projectionViewerProps.d.ts.map +1 -1
- package/lib/scatter/ui/ScatterPlot.d.ts +1 -1
- package/lib/scatter/ui/ScatterPlot.d.ts.map +1 -1
- package/lib/scatter/ui/ScatterPlot.js +18 -2
- package/lib/scatter/ui/ScatterPlot.js.map +1 -1
- package/lib/scatter/ui/lassoGesture.d.ts +14 -2
- package/lib/scatter/ui/lassoGesture.d.ts.map +1 -1
- package/lib/scatter/ui/lassoGesture.js +4 -1
- package/lib/scatter/ui/lassoGesture.js.map +1 -1
- package/lib/scatter/ui/scatterPlotProps.d.ts +34 -0
- package/lib/scatter/ui/scatterPlotProps.d.ts.map +1 -1
- package/lib/scatter/ui/useScatterInteraction.d.ts +9 -0
- package/lib/scatter/ui/useScatterInteraction.d.ts.map +1 -1
- package/lib/scatter/ui/useScatterInteraction.js +9 -1
- package/lib/scatter/ui/useScatterInteraction.js.map +1 -1
- package/lib/scatter3d/core/cloudGesture.d.ts +17 -0
- package/lib/scatter3d/core/cloudGesture.d.ts.map +1 -0
- package/lib/scatter3d/core/cloudGesture.js +2 -0
- package/lib/scatter3d/core/cloudGesture.js.map +1 -0
- package/lib/scatter3d/core/confidenceEllipsoid.d.ts +63 -0
- package/lib/scatter3d/core/confidenceEllipsoid.d.ts.map +1 -0
- package/lib/scatter3d/core/confidenceEllipsoid.js +128 -0
- package/lib/scatter3d/core/confidenceEllipsoid.js.map +1 -0
- package/lib/scatter3d/core/cubeFrame.d.ts +40 -0
- package/lib/scatter3d/core/cubeFrame.d.ts.map +1 -0
- package/lib/scatter3d/core/cubeFrame.js +58 -0
- package/lib/scatter3d/core/cubeFrame.js.map +1 -0
- package/lib/scatter3d/core/cubePoints.d.ts +35 -0
- package/lib/scatter3d/core/cubePoints.d.ts.map +1 -0
- package/lib/scatter3d/core/cubePoints.js +50 -0
- package/lib/scatter3d/core/cubePoints.js.map +1 -0
- package/lib/scatter3d/core/ellipsoidCoverage.d.ts +32 -0
- package/lib/scatter3d/core/ellipsoidCoverage.d.ts.map +1 -0
- package/lib/scatter3d/core/ellipsoidCoverage.js +83 -0
- package/lib/scatter3d/core/ellipsoidCoverage.js.map +1 -0
- package/lib/scatter3d/core/ellipsoidSilhouette.d.ts +42 -0
- package/lib/scatter3d/core/ellipsoidSilhouette.d.ts.map +1 -0
- package/lib/scatter3d/core/ellipsoidSilhouette.js +52 -0
- package/lib/scatter3d/core/ellipsoidSilhouette.js.map +1 -0
- package/lib/scatter3d/core/index.d.ts +29 -0
- package/lib/scatter3d/core/index.d.ts.map +1 -0
- package/lib/scatter3d/core/index.js +22 -0
- package/lib/scatter3d/core/index.js.map +1 -0
- package/lib/scatter3d/core/orbitCamera.d.ts +113 -0
- package/lib/scatter3d/core/orbitCamera.d.ts.map +1 -0
- package/lib/scatter3d/core/orbitCamera.js +104 -0
- package/lib/scatter3d/core/orbitCamera.js.map +1 -0
- package/lib/scatter3d/core/symmetricEigen.d.ts +47 -0
- package/lib/scatter3d/core/symmetricEigen.d.ts.map +1 -0
- package/lib/scatter3d/core/symmetricEigen.js +219 -0
- package/lib/scatter3d/core/symmetricEigen.js.map +1 -0
- package/lib/scatter3d/ui/CloudFrameLayer.d.ts +33 -0
- package/lib/scatter3d/ui/CloudFrameLayer.d.ts.map +1 -0
- package/lib/scatter3d/ui/CloudFrameLayer.js +37 -0
- package/lib/scatter3d/ui/CloudFrameLayer.js.map +1 -0
- package/lib/scatter3d/ui/CloudPointLayer.d.ts +62 -0
- package/lib/scatter3d/ui/CloudPointLayer.d.ts.map +1 -0
- package/lib/scatter3d/ui/CloudPointLayer.js +67 -0
- package/lib/scatter3d/ui/CloudPointLayer.js.map +1 -0
- package/lib/scatter3d/ui/CloudShellLayer.d.ts +52 -0
- package/lib/scatter3d/ui/CloudShellLayer.d.ts.map +1 -0
- package/lib/scatter3d/ui/CloudShellLayer.js +40 -0
- package/lib/scatter3d/ui/CloudShellLayer.js.map +1 -0
- package/lib/scatter3d/ui/ScatterCloud.d.ts +24 -0
- package/lib/scatter3d/ui/ScatterCloud.d.ts.map +1 -0
- package/lib/scatter3d/ui/ScatterCloud.js +189 -0
- package/lib/scatter3d/ui/ScatterCloud.js.map +1 -0
- package/lib/scatter3d/ui/cloudGeometry.d.ts +108 -0
- package/lib/scatter3d/ui/cloudGeometry.d.ts.map +1 -0
- package/lib/scatter3d/ui/cloudGeometry.js +122 -0
- package/lib/scatter3d/ui/cloudGeometry.js.map +1 -0
- package/lib/scatter3d/ui/cloudShellStyles.d.ts +36 -0
- package/lib/scatter3d/ui/cloudShellStyles.d.ts.map +1 -0
- package/lib/scatter3d/ui/cloudShellStyles.js +62 -0
- package/lib/scatter3d/ui/cloudShellStyles.js.map +1 -0
- package/lib/scatter3d/ui/cloudZoom.d.ts +21 -0
- package/lib/scatter3d/ui/cloudZoom.d.ts.map +1 -0
- package/lib/scatter3d/ui/cloudZoom.js +22 -0
- package/lib/scatter3d/ui/cloudZoom.js.map +1 -0
- package/lib/scatter3d/ui/index.d.ts +29 -0
- package/lib/scatter3d/ui/index.d.ts.map +1 -0
- package/lib/scatter3d/ui/index.js +21 -0
- package/lib/scatter3d/ui/index.js.map +1 -0
- package/lib/scatter3d/ui/scatterCloudModel.d.ts +60 -0
- package/lib/scatter3d/ui/scatterCloudModel.d.ts.map +1 -0
- package/lib/scatter3d/ui/scatterCloudModel.js +66 -0
- package/lib/scatter3d/ui/scatterCloudModel.js.map +1 -0
- package/lib/scatter3d/ui/scatterCloudProps.d.ts +185 -0
- package/lib/scatter3d/ui/scatterCloudProps.d.ts.map +1 -0
- package/lib/scatter3d/ui/scatterCloudProps.js +9 -0
- package/lib/scatter3d/ui/scatterCloudProps.js.map +1 -0
- package/lib/scatter3d/ui/useOrbitDrag.d.ts +60 -0
- package/lib/scatter3d/ui/useOrbitDrag.d.ts.map +1 -0
- package/lib/scatter3d/ui/useOrbitDrag.js +129 -0
- package/lib/scatter3d/ui/useOrbitDrag.js.map +1 -0
- package/lib/ui.d.ts +1 -0
- package/lib/ui.d.ts.map +1 -1
- package/lib/ui.js +1 -0
- package/lib/ui.js.map +1 -1
- package/package.json +1 -1
- package/src/chrome/ui/NavLink.tsx +5 -3
- package/src/chrome/ui/SiteFooter.tsx +16 -1
- package/src/chrome/ui/SiteHeader.tsx +19 -1
- package/src/chrome/ui/navItem.ts +12 -0
- package/src/core.ts +1 -0
- package/src/download/core/figureLegend.ts +167 -0
- package/src/download/core/figureSvg.ts +81 -1
- package/src/download/core/figureSvgDocument.ts +13 -3
- package/src/download/core/figureTarget.ts +21 -0
- package/src/download/core/index.ts +8 -0
- package/src/ecosystem/core/sites.ts +14 -0
- package/src/ecosystem/ui/glyphs.tsx +26 -0
- package/src/overlay/ui/OverlayLegend.tsx +7 -1
- package/src/projection/core/projectionCopy.ts +9 -0
- package/src/projection/core/projectionOptions.ts +48 -3
- package/src/projection/core/projectionStrings.ts +16 -0
- package/src/projection/core/projectionTabDefaults.ts +12 -0
- package/src/projection/core/projectionTabs.ts +10 -1
- package/src/projection/ui/ProjectionMapTab.tsx +8 -0
- package/src/projection/ui/ProjectionPanel.tsx +63 -0
- package/src/projection/ui/ProjectionReadout.tsx +16 -6
- package/src/projection/ui/ProjectionSpaceControls.tsx +219 -0
- package/src/projection/ui/ProjectionSpaceMore.tsx +169 -0
- package/src/projection/ui/ProjectionSpaceTab.tsx +217 -0
- package/src/projection/ui/ProjectionViewer.tsx +3 -0
- package/src/projection/ui/index.ts +1 -0
- package/src/projection/ui/projectionBarModel.ts +3 -2
- package/src/projection/ui/projectionBarReadings.ts +18 -0
- package/src/projection/ui/projectionBarSlots.tsx +19 -0
- package/src/projection/ui/projectionCloudChoices.ts +23 -0
- package/src/projection/ui/projectionEllipsoid.ts +78 -0
- package/src/projection/ui/projectionMapChrome.ts +26 -5
- package/src/projection/ui/projectionMapView.ts +45 -0
- package/src/projection/ui/projectionSpaceModel.ts +14 -0
- package/src/projection/ui/projectionViewerProps.ts +35 -0
- package/src/scatter/ui/ScatterPlot.tsx +21 -2
- package/src/scatter/ui/lassoGesture.ts +15 -2
- package/src/scatter/ui/scatterPlotProps.ts +35 -0
- package/src/scatter/ui/useScatterInteraction.ts +22 -1
- package/src/scatter3d/core/cloudGesture.ts +16 -0
- package/src/scatter3d/core/confidenceEllipsoid.ts +177 -0
- package/src/scatter3d/core/cubeFrame.ts +72 -0
- package/src/scatter3d/core/cubePoints.ts +75 -0
- package/src/scatter3d/core/ellipsoidCoverage.ts +86 -0
- package/src/scatter3d/core/ellipsoidSilhouette.ts +76 -0
- package/src/scatter3d/core/index.ts +52 -0
- package/src/scatter3d/core/orbitCamera.ts +159 -0
- package/src/scatter3d/core/symmetricEigen.ts +273 -0
- package/src/scatter3d/ui/CloudFrameLayer.tsx +97 -0
- package/src/scatter3d/ui/CloudPointLayer.tsx +132 -0
- package/src/scatter3d/ui/CloudShellLayer.tsx +107 -0
- package/src/scatter3d/ui/ScatterCloud.tsx +312 -0
- package/src/scatter3d/ui/cloudGeometry.ts +200 -0
- package/src/scatter3d/ui/cloudShellStyles.ts +74 -0
- package/src/scatter3d/ui/cloudZoom.ts +22 -0
- package/src/scatter3d/ui/index.ts +41 -0
- package/src/scatter3d/ui/scatterCloudModel.ts +98 -0
- package/src/scatter3d/ui/scatterCloudProps.ts +191 -0
- package/src/scatter3d/ui/useOrbitDrag.ts +220 -0
- package/src/ui.ts +1 -0
- package/styles/chrome.css +29 -4
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How far the reader may stand back from the box, and how near they may get.
|
|
3
|
+
*
|
|
4
|
+
* A cloud without a zoom cannot be read where its groups overlap, which on a
|
|
5
|
+
* projection is exactly where the reader is looking. The range is kept modest
|
|
6
|
+
* at both ends: past four the frame is off the picture and there is nothing
|
|
7
|
+
* left to say which way is which, and below four tenths the cloud is a smudge
|
|
8
|
+
* in the middle of an empty box.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** How far the wheel may zoom out and in. */
|
|
12
|
+
export const CLOUD_ZOOM_RANGE = { min: 0.4, max: 4 } as const;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* A zoom held inside what the box can usefully be seen at.
|
|
16
|
+
* @param zoom - The zoom asked for.
|
|
17
|
+
* @returns The zoom to draw at; `1` for anything that is not a number.
|
|
18
|
+
*/
|
|
19
|
+
export function clampCloudZoom(zoom: number): number {
|
|
20
|
+
if (!Number.isFinite(zoom)) return 1;
|
|
21
|
+
return Math.min(CLOUD_ZOOM_RANGE.max, Math.max(CLOUD_ZOOM_RANGE.min, zoom));
|
|
22
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A cloud of samples in a box the reader can turn.
|
|
3
|
+
*
|
|
4
|
+
* It is the `scatter` domain's twin, and deliberately so: the same selection,
|
|
5
|
+
* the same lasso, the same hover card, the same labels and the same group
|
|
6
|
+
* colours, so that a reader who has learned the map has learned this too. What
|
|
7
|
+
* is new is everything the third axis forces — a box to say which way is up, a
|
|
8
|
+
* drag that turns it, glass shells instead of outlines, and one scale across
|
|
9
|
+
* all three axes, because a solid seen from an angle has nowhere to write
|
|
10
|
+
* three sets of tick labels.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export type { CloudFrameLayerProps } from './CloudFrameLayer.tsx';
|
|
14
|
+
export { CloudFrameLayer } from './CloudFrameLayer.tsx';
|
|
15
|
+
export type { CloudPointLayerProps } from './CloudPointLayer.tsx';
|
|
16
|
+
export { CloudPointLayer } from './CloudPointLayer.tsx';
|
|
17
|
+
export type { CloudShell, CloudShellLayerProps } from './CloudShellLayer.tsx';
|
|
18
|
+
export { CloudShellLayer } from './CloudShellLayer.tsx';
|
|
19
|
+
export type {
|
|
20
|
+
CloudCloud,
|
|
21
|
+
CloudOutline,
|
|
22
|
+
CloudSegment,
|
|
23
|
+
CloudView,
|
|
24
|
+
} from './cloudGeometry.ts';
|
|
25
|
+
export {
|
|
26
|
+
CLOUD_FILL,
|
|
27
|
+
CLOUD_MARGIN,
|
|
28
|
+
cloudView,
|
|
29
|
+
projectCloud,
|
|
30
|
+
projectSegments,
|
|
31
|
+
projectShellOutline,
|
|
32
|
+
} from './cloudGeometry.ts';
|
|
33
|
+
export type { ShellGlassStop } from './cloudShellStyles.ts';
|
|
34
|
+
export { SHELL_FILL_OPACITY, shellGlassStops } from './cloudShellStyles.ts';
|
|
35
|
+
export { CLOUD_ZOOM_RANGE, clampCloudZoom } from './cloudZoom.ts';
|
|
36
|
+
export type { CloudShellsInput } from './scatterCloudModel.ts';
|
|
37
|
+
export { cloudLabel, cloudShells } from './scatterCloudModel.ts';
|
|
38
|
+
export type { CloudGesture, ScatterCloudProps } from './scatterCloudProps.ts';
|
|
39
|
+
export { ScatterCloud } from './ScatterCloud.tsx';
|
|
40
|
+
export type { OrbitDrag, OrbitDragOptions } from './useOrbitDrag.ts';
|
|
41
|
+
export { useOrbitDrag } from './useOrbitDrag.ts';
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a cloud is made of before any of it is drawn: the shell around each
|
|
3
|
+
* group, and the sentence a screen reader is given instead of the picture.
|
|
4
|
+
*
|
|
5
|
+
* It is kept out of the component for the same reason the flat scatter's model
|
|
6
|
+
* is — a shell has to be cut before there is anything to project, and cutting
|
|
7
|
+
* it inside the render callback would cut it again on every turn.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { EllipseSize } from '../../scatter/core/confidenceEllipse.ts';
|
|
11
|
+
import type { ScatterGroupInk } from '../../scatter/ui/scatterPlotModel.ts';
|
|
12
|
+
import type { ScatterGroup } from '../../scatter/ui/scatterPlotProps.ts';
|
|
13
|
+
import { confidenceEllipsoid } from '../core/confidenceEllipsoid.ts';
|
|
14
|
+
import type { Vector3 } from '../core/orbitCamera.ts';
|
|
15
|
+
|
|
16
|
+
import type { CloudShell } from './CloudShellLayer.tsx';
|
|
17
|
+
|
|
18
|
+
/** What {@link cloudShells} needs to fit one shell per group. */
|
|
19
|
+
export interface CloudShellsInput {
|
|
20
|
+
/** Every sample, in cube units. */
|
|
21
|
+
cube: readonly Vector3[];
|
|
22
|
+
/** Which group each sample belongs to, as an index into `groups`. */
|
|
23
|
+
groupOf: ArrayLike<number>;
|
|
24
|
+
/** The groups, in the order they are coloured. */
|
|
25
|
+
groups: readonly ScatterGroup[];
|
|
26
|
+
/** The colour and the strength each group is drawn at. */
|
|
27
|
+
ink: ScatterGroupInk;
|
|
28
|
+
/** How large the shells are. */
|
|
29
|
+
size: EllipseSize;
|
|
30
|
+
/**
|
|
31
|
+
* How many samples a group needs before it is given a shell.
|
|
32
|
+
* @default 4
|
|
33
|
+
*/
|
|
34
|
+
minimumPoints?: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* One shell per group that has enough samples to have a shape.
|
|
39
|
+
*
|
|
40
|
+
* The shells are fitted in cube units rather than in the data's own, because
|
|
41
|
+
* the three axes were stretched to one cube and a shell fitted before that
|
|
42
|
+
* stretch would not sit on the cloud the reader is looking at. It is the same
|
|
43
|
+
* reasoning the map uses when it fits its outlines in pixels.
|
|
44
|
+
*
|
|
45
|
+
* A group the reader has switched off in the legend keeps its shell and loses
|
|
46
|
+
* its strength, so the shape it was making is still visible behind the groups
|
|
47
|
+
* that are left — which is the whole point of switching one off.
|
|
48
|
+
* @param input - See {@link CloudShellsInput}.
|
|
49
|
+
* @returns The shells, in group order. A group too small for a shape is left out rather than given a shell that describes its handful of samples.
|
|
50
|
+
*/
|
|
51
|
+
export function cloudShells(input: CloudShellsInput): CloudShell[] {
|
|
52
|
+
const { cube, groupOf, groups, ink, size, minimumPoints } = input;
|
|
53
|
+
|
|
54
|
+
const held: Vector3[][] = groups.map(() => []);
|
|
55
|
+
const count = Math.min(cube.length, groupOf.length);
|
|
56
|
+
for (let index = 0; index < count; index++) {
|
|
57
|
+
const group = groupOf[index];
|
|
58
|
+
if (group === undefined || group < 0 || group >= groups.length) continue;
|
|
59
|
+
(held[group] as Vector3[]).push(cube[index] as Vector3);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const shells: CloudShell[] = [];
|
|
63
|
+
for (const [group, entry] of groups.entries()) {
|
|
64
|
+
const ellipsoid = confidenceEllipsoid(held[group] as Vector3[], {
|
|
65
|
+
size,
|
|
66
|
+
minimumPoints,
|
|
67
|
+
});
|
|
68
|
+
if (ellipsoid === null) continue;
|
|
69
|
+
shells.push({
|
|
70
|
+
id: entry.id,
|
|
71
|
+
color: ink.colors[group] ?? entry.color,
|
|
72
|
+
ellipsoid,
|
|
73
|
+
opacity: ink.opacities[group] ?? 1,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
return shells;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* What a screen reader is told the figure shows.
|
|
81
|
+
*
|
|
82
|
+
* It names all three axes, because a reader who cannot see the box has no
|
|
83
|
+
* frame to read them off, and the frame is where a sighted reader learns which
|
|
84
|
+
* axis is which.
|
|
85
|
+
* @param xLabel - What the axis running left to right is called.
|
|
86
|
+
* @param yLabel - What the axis running bottom to top is called.
|
|
87
|
+
* @param zLabel - What the axis running away from the reader is called.
|
|
88
|
+
* @param count - How many samples there are.
|
|
89
|
+
* @returns The sentence.
|
|
90
|
+
*/
|
|
91
|
+
export function cloudLabel(
|
|
92
|
+
xLabel: string,
|
|
93
|
+
yLabel: string,
|
|
94
|
+
zLabel: string,
|
|
95
|
+
count: number,
|
|
96
|
+
): string {
|
|
97
|
+
return `A three-dimensional scatter plot of ${count} samples: ${xLabel} across, ${yLabel} up, and ${zLabel} into the picture.`;
|
|
98
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a caller hands a `ScatterCloud`.
|
|
3
|
+
*
|
|
4
|
+
* The types live beside the component rather than inside it because the
|
|
5
|
+
* component's own file would otherwise be mostly prose, exactly as the flat
|
|
6
|
+
* scatter's are.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { ReactNode } from 'react';
|
|
10
|
+
|
|
11
|
+
import type { EllipseSize } from '../../scatter/core/confidenceEllipse.ts';
|
|
12
|
+
import type { ScatterSelectionMode } from '../../scatter/core/scatterSelection.ts';
|
|
13
|
+
import type {
|
|
14
|
+
ScatterGroup,
|
|
15
|
+
ScatterPointOpen,
|
|
16
|
+
} from '../../scatter/ui/scatterPlotProps.ts';
|
|
17
|
+
import type { SelectionChange } from '../../scatter/ui/useScatterSelection.ts';
|
|
18
|
+
import type { CloudGesture } from '../core/cloudGesture.ts';
|
|
19
|
+
import type { OrbitCamera } from '../core/orbitCamera.ts';
|
|
20
|
+
|
|
21
|
+
export type { CloudGesture } from '../core/cloudGesture.ts';
|
|
22
|
+
|
|
23
|
+
/** What a `ScatterCloud` needs. */
|
|
24
|
+
export interface ScatterCloudProps {
|
|
25
|
+
/** Where every sample sits along the axis that runs left to right, in data units. */
|
|
26
|
+
x: ArrayLike<number>;
|
|
27
|
+
/** Along the axis that runs bottom to top, in the same order. */
|
|
28
|
+
y: ArrayLike<number>;
|
|
29
|
+
/** Along the axis that runs away from the reader. */
|
|
30
|
+
z: ArrayLike<number>;
|
|
31
|
+
/** Total width, in pixels, from `useContainerSize`. */
|
|
32
|
+
width: number;
|
|
33
|
+
/** Total height. */
|
|
34
|
+
height: number;
|
|
35
|
+
/** What the axis running left to right is called, written on the frame. */
|
|
36
|
+
xLabel: string;
|
|
37
|
+
/** What the axis running bottom to top is called. */
|
|
38
|
+
yLabel: string;
|
|
39
|
+
/** What the axis running away from the reader is called. */
|
|
40
|
+
zLabel: string;
|
|
41
|
+
/**
|
|
42
|
+
* Which group each sample belongs to, as an index into `groups`. A sample
|
|
43
|
+
* with `-1`, or an index outside the range, is drawn in the muted ink and
|
|
44
|
+
* left out of every shell.
|
|
45
|
+
* @default undefined — every sample is one crowd
|
|
46
|
+
*/
|
|
47
|
+
groupOf?: ArrayLike<number>;
|
|
48
|
+
/**
|
|
49
|
+
* The groups, in the order they are coloured and listed.
|
|
50
|
+
* @default undefined
|
|
51
|
+
*/
|
|
52
|
+
groups?: readonly ScatterGroup[];
|
|
53
|
+
/**
|
|
54
|
+
* Which groups are drawn faint, by id — what a legend entry switches.
|
|
55
|
+
* @default undefined — every group is drawn in full
|
|
56
|
+
*/
|
|
57
|
+
mutedGroups?: ReadonlySet<string>;
|
|
58
|
+
/**
|
|
59
|
+
* How large the group shells are, or `null` for none. A share is read in
|
|
60
|
+
* three dimensions here, so the same share gives a wider shell than the same
|
|
61
|
+
* call gives on the map — which is the point of asking for a share rather
|
|
62
|
+
* than for a distance.
|
|
63
|
+
* @default null
|
|
64
|
+
*/
|
|
65
|
+
ellipsoid?: EllipseSize | null;
|
|
66
|
+
/**
|
|
67
|
+
* How many samples a group needs before it is given a shell. Below four a
|
|
68
|
+
* group has no volume to measure, so anything less is read as four.
|
|
69
|
+
* @default 4
|
|
70
|
+
*/
|
|
71
|
+
ellipsoidMinimumPoints?: number;
|
|
72
|
+
/**
|
|
73
|
+
* How solid the glass is; `0` draws no shells at all.
|
|
74
|
+
* @default SHELL_FILL_OPACITY
|
|
75
|
+
*/
|
|
76
|
+
ellipsoidFillOpacity?: number;
|
|
77
|
+
/**
|
|
78
|
+
* Whether each group's name is written once, over the middle of its own
|
|
79
|
+
* shell. It needs `groups`, since it is their names it writes.
|
|
80
|
+
* @default false
|
|
81
|
+
*/
|
|
82
|
+
showGroupLabels?: boolean;
|
|
83
|
+
/**
|
|
84
|
+
* What each sample is called, written beside its own dot. A sample whose
|
|
85
|
+
* entry is `undefined` is left unnamed.
|
|
86
|
+
* @default undefined — no sample is named
|
|
87
|
+
*/
|
|
88
|
+
pointLabels?: ReadonlyArray<string | undefined>;
|
|
89
|
+
/**
|
|
90
|
+
* Radius of a dot at the middle of the box, in pixels. One at the front is
|
|
91
|
+
* drawn larger and one at the back smaller, about this.
|
|
92
|
+
* @default 3.5
|
|
93
|
+
*/
|
|
94
|
+
pointRadius?: number;
|
|
95
|
+
/**
|
|
96
|
+
* The index from which samples are drawn as outlines rather than filled,
|
|
97
|
+
* which is how one the model was fitted on is told from one placed into it
|
|
98
|
+
* afterwards.
|
|
99
|
+
* @default undefined — every sample is filled
|
|
100
|
+
*/
|
|
101
|
+
outlinedFrom?: number;
|
|
102
|
+
/**
|
|
103
|
+
* What a drag does. The cloud never changes it on its own, because the
|
|
104
|
+
* control belongs in the bar above the figure rather than on top of the
|
|
105
|
+
* picture, so it is a plain setting rather than a controlled value.
|
|
106
|
+
* @default 'turn'
|
|
107
|
+
*/
|
|
108
|
+
gesture?: CloudGesture;
|
|
109
|
+
/**
|
|
110
|
+
* Where the reader is standing. Present, the caller owns the camera, which
|
|
111
|
+
* is how a site puts a viewpoint in a share link.
|
|
112
|
+
* @default undefined — the cloud keeps its own
|
|
113
|
+
*/
|
|
114
|
+
camera?: OrbitCamera;
|
|
115
|
+
/**
|
|
116
|
+
* Called with the camera every frame of a turn leaves behind.
|
|
117
|
+
* @default undefined
|
|
118
|
+
*/
|
|
119
|
+
onCameraChange?: (camera: OrbitCamera) => void;
|
|
120
|
+
/**
|
|
121
|
+
* How far in the reader has zoomed; `1` is the resting size. Present, the
|
|
122
|
+
* caller owns it.
|
|
123
|
+
* @default undefined — the cloud keeps its own
|
|
124
|
+
*/
|
|
125
|
+
zoom?: number;
|
|
126
|
+
/**
|
|
127
|
+
* Called with the zoom a wheel asks for.
|
|
128
|
+
* @default undefined
|
|
129
|
+
*/
|
|
130
|
+
onZoomChange?: (zoom: number) => void;
|
|
131
|
+
/**
|
|
132
|
+
* Whether the wheel zooms the box. On, because a cloud that cannot be zoomed
|
|
133
|
+
* cannot be read where its groups overlap; a figure set in a column of prose
|
|
134
|
+
* should turn it off so a reader on their way past keeps their scroll.
|
|
135
|
+
* @default true
|
|
136
|
+
*/
|
|
137
|
+
wheelZoom?: boolean;
|
|
138
|
+
/**
|
|
139
|
+
* The selected rows. Present, the caller owns the selection.
|
|
140
|
+
* @default undefined — the cloud keeps its own
|
|
141
|
+
*/
|
|
142
|
+
selected?: readonly number[];
|
|
143
|
+
/**
|
|
144
|
+
* The rows selected before the reader touches anything.
|
|
145
|
+
* @default undefined
|
|
146
|
+
*/
|
|
147
|
+
defaultSelected?: readonly number[];
|
|
148
|
+
/**
|
|
149
|
+
* Called when a lasso is released or a dot is tapped — never while a lasso
|
|
150
|
+
* is being drawn, and never while the box is being turned.
|
|
151
|
+
* @default undefined
|
|
152
|
+
*/
|
|
153
|
+
onSelectionChange?: (change: SelectionChange) => void;
|
|
154
|
+
/**
|
|
155
|
+
* What a gesture with no modifier held does to the selection. Shift always
|
|
156
|
+
* adds and Alt always removes, exactly as on the map.
|
|
157
|
+
* @default 'replace'
|
|
158
|
+
*/
|
|
159
|
+
selectMode?: ScatterSelectionMode;
|
|
160
|
+
/**
|
|
161
|
+
* Called with the row under the pointer, or `-1`.
|
|
162
|
+
* @default undefined
|
|
163
|
+
*/
|
|
164
|
+
onHoverChange?: (index: number) => void;
|
|
165
|
+
/**
|
|
166
|
+
* Called when the reader double-clicks a sample, exactly as on the flat map:
|
|
167
|
+
* the gesture for "tell me more about this one", or "let me change it".
|
|
168
|
+
*
|
|
169
|
+
* It fires whichever gesture a drag is set to, because a double click is not
|
|
170
|
+
* a drag — a reader turning the box must not have to switch to Select to
|
|
171
|
+
* open a sample. The sample is selected first, since a double click is also
|
|
172
|
+
* two clicks; the map says why, and the two behave alike on purpose.
|
|
173
|
+
* @default undefined
|
|
174
|
+
*/
|
|
175
|
+
onPointDoubleClick?: (point: ScatterPointOpen) => void;
|
|
176
|
+
/**
|
|
177
|
+
* What floats over the figure — a bar, a key, a caption, a card.
|
|
178
|
+
* @default undefined
|
|
179
|
+
*/
|
|
180
|
+
overlay?: ReactNode;
|
|
181
|
+
/**
|
|
182
|
+
* What a screen reader is told the figure shows.
|
|
183
|
+
* @default a sentence built from the three axis names and the sample count
|
|
184
|
+
*/
|
|
185
|
+
label?: string;
|
|
186
|
+
/**
|
|
187
|
+
* Value of the `data-testid` attribute of the wrapper.
|
|
188
|
+
* @default undefined
|
|
189
|
+
*/
|
|
190
|
+
testId?: string;
|
|
191
|
+
}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import type { PointerEvent as ReactPointerEvent } from 'react';
|
|
2
|
+
import { useCallback, useMemo, useRef, useState } from 'react';
|
|
3
|
+
|
|
4
|
+
import type { ScatterSelectionMode } from '../../scatter/core/scatterSelection.ts';
|
|
5
|
+
import type { ScatterSurfaceProps } from '../../scatter/ui/lassoGesture.ts';
|
|
6
|
+
import {
|
|
7
|
+
capturePointer,
|
|
8
|
+
dropFrame,
|
|
9
|
+
modifierMode,
|
|
10
|
+
scheduleFrame,
|
|
11
|
+
surfacePosition,
|
|
12
|
+
} from '../../scatter/ui/lassoGesture.ts';
|
|
13
|
+
import type { OrbitCamera } from '../core/orbitCamera.ts';
|
|
14
|
+
import { orbitByDrag } from '../core/orbitCamera.ts';
|
|
15
|
+
|
|
16
|
+
/** What {@link useOrbitDrag} needs. */
|
|
17
|
+
export interface OrbitDragOptions {
|
|
18
|
+
/**
|
|
19
|
+
* Whether a drag turns the box at all. Off, the surface still reports a tap,
|
|
20
|
+
* so the reader can pick a sample while the drag belongs to something else.
|
|
21
|
+
* @default true
|
|
22
|
+
*/
|
|
23
|
+
enabled?: boolean;
|
|
24
|
+
/** Called with the camera each frame of a drag leaves behind. */
|
|
25
|
+
onOrbit: (turn: (camera: OrbitCamera) => OrbitCamera) => void;
|
|
26
|
+
/**
|
|
27
|
+
* Called when a press and release happened in one place — a click, however
|
|
28
|
+
* the reader's hand shook.
|
|
29
|
+
* @default undefined
|
|
30
|
+
*/
|
|
31
|
+
onTap?: (x: number, y: number, mode: ScatterSelectionMode) => void;
|
|
32
|
+
/**
|
|
33
|
+
* Called with the pointer's position on every frame it moves without
|
|
34
|
+
* turning, which is what raises the card under it.
|
|
35
|
+
* @default undefined
|
|
36
|
+
*/
|
|
37
|
+
onMove?: (x: number, y: number) => void;
|
|
38
|
+
/**
|
|
39
|
+
* Called when the pointer leaves the surface without a drag in progress.
|
|
40
|
+
* @default undefined
|
|
41
|
+
*/
|
|
42
|
+
onLeave?: () => void;
|
|
43
|
+
/**
|
|
44
|
+
* What a tap with no modifier held does to the selection.
|
|
45
|
+
* @default 'replace'
|
|
46
|
+
*/
|
|
47
|
+
mode?: ScatterSelectionMode;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** A drag that turns the box, and the taps it lets through. */
|
|
51
|
+
export interface OrbitDrag {
|
|
52
|
+
/** Whether the box is being turned right now. */
|
|
53
|
+
turning: boolean;
|
|
54
|
+
/** Spread onto the transparent rectangle over the figure. */
|
|
55
|
+
surface: ScatterSurfaceProps;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Turning the box with the pointer.
|
|
60
|
+
*
|
|
61
|
+
* It is the lasso's twin and is built the same way — the pointer is captured
|
|
62
|
+
* on the way down so a drag that wanders off the figure keeps turning it, all
|
|
63
|
+
* three ways a capture can end lead to one function, and moves are coalesced
|
|
64
|
+
* to one a frame because a pointer emits many per frame and each one that
|
|
65
|
+
* reaches the caller repaints every dot and every face.
|
|
66
|
+
*
|
|
67
|
+
* A press that goes nowhere is a tap rather than a turn of no degrees, because
|
|
68
|
+
* nobody presses a mouse button without moving it a pixel or two and a reader
|
|
69
|
+
* who meant to pick a sample must not have to hold their hand still to do it.
|
|
70
|
+
* @param options - See {@link OrbitDragOptions}.
|
|
71
|
+
* @returns The drag. See {@link OrbitDrag}.
|
|
72
|
+
*/
|
|
73
|
+
export function useOrbitDrag(options: OrbitDragOptions): OrbitDrag {
|
|
74
|
+
const {
|
|
75
|
+
enabled = true,
|
|
76
|
+
onOrbit,
|
|
77
|
+
onTap,
|
|
78
|
+
onMove,
|
|
79
|
+
onLeave,
|
|
80
|
+
mode: restingMode = 'replace',
|
|
81
|
+
} = options;
|
|
82
|
+
|
|
83
|
+
const pointerRef = useRef<number | null>(null);
|
|
84
|
+
const lastRef = useRef({ x: 0, y: 0 });
|
|
85
|
+
const travelRef = useRef(0);
|
|
86
|
+
const frameRef = useRef<number | null>(null);
|
|
87
|
+
const pendingRef = useRef<PendingTurn | null>(null);
|
|
88
|
+
const [turning, setTurning] = useState(false);
|
|
89
|
+
|
|
90
|
+
const flush = useCallback(() => {
|
|
91
|
+
frameRef.current = null;
|
|
92
|
+
const next = pendingRef.current;
|
|
93
|
+
pendingRef.current = null;
|
|
94
|
+
if (next === null) return;
|
|
95
|
+
if (pointerRef.current === null) {
|
|
96
|
+
onMove?.(next.x, next.y);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
onOrbit((camera) => orbitByDrag(camera, next.dx, next.dy));
|
|
100
|
+
}, [onMove, onOrbit]);
|
|
101
|
+
|
|
102
|
+
const end = useCallback(
|
|
103
|
+
(event: ReactPointerEvent<Element>, commit: boolean) => {
|
|
104
|
+
if (event.pointerId !== pointerRef.current) return;
|
|
105
|
+
pointerRef.current = null;
|
|
106
|
+
pendingRef.current = null;
|
|
107
|
+
dropFrame(frameRef);
|
|
108
|
+
setTurning(false);
|
|
109
|
+
if (!commit || travelRef.current > TAP_SLACK) return;
|
|
110
|
+
const at = surfacePosition(event, 0, 0);
|
|
111
|
+
onTap?.(at.x, at.y, modifierMode(event, restingMode));
|
|
112
|
+
},
|
|
113
|
+
[onTap, restingMode],
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
const handleDown = useCallback((event: ReactPointerEvent<Element>) => {
|
|
117
|
+
if (pointerRef.current !== null || event.button > 0) return;
|
|
118
|
+
pointerRef.current = event.pointerId;
|
|
119
|
+
lastRef.current = { x: event.clientX, y: event.clientY };
|
|
120
|
+
travelRef.current = 0;
|
|
121
|
+
setTurning(true);
|
|
122
|
+
capturePointer(event);
|
|
123
|
+
}, []);
|
|
124
|
+
|
|
125
|
+
const handleMove = useCallback(
|
|
126
|
+
(event: ReactPointerEvent<Element>) => {
|
|
127
|
+
const active = pointerRef.current;
|
|
128
|
+
if (active === null) {
|
|
129
|
+
const at = surfacePosition(event, 0, 0);
|
|
130
|
+
pendingRef.current = { x: at.x, y: at.y, dx: 0, dy: 0 };
|
|
131
|
+
scheduleFrame(frameRef, flush);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (event.pointerId !== active) return;
|
|
135
|
+
const last = lastRef.current;
|
|
136
|
+
const dx = event.clientX - last.x;
|
|
137
|
+
const dy = event.clientY - last.y;
|
|
138
|
+
lastRef.current = { x: event.clientX, y: event.clientY };
|
|
139
|
+
travelRef.current += Math.abs(dx) + Math.abs(dy);
|
|
140
|
+
if (!enabled) return;
|
|
141
|
+
const waiting = pendingRef.current;
|
|
142
|
+
pendingRef.current = {
|
|
143
|
+
x: 0,
|
|
144
|
+
y: 0,
|
|
145
|
+
dx: (waiting?.dx ?? 0) + dx,
|
|
146
|
+
dy: (waiting?.dy ?? 0) + dy,
|
|
147
|
+
};
|
|
148
|
+
scheduleFrame(frameRef, flush);
|
|
149
|
+
},
|
|
150
|
+
[enabled, flush],
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
const handleUp = useCallback(
|
|
154
|
+
(event: ReactPointerEvent<Element>) => {
|
|
155
|
+
end(event, true);
|
|
156
|
+
},
|
|
157
|
+
[end],
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
const handleAbandon = useCallback(
|
|
161
|
+
(event: ReactPointerEvent<Element>) => {
|
|
162
|
+
end(event, false);
|
|
163
|
+
},
|
|
164
|
+
[end],
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
const handleLeave = useCallback(() => {
|
|
168
|
+
if (pointerRef.current !== null) return;
|
|
169
|
+
pendingRef.current = null;
|
|
170
|
+
onLeave?.();
|
|
171
|
+
}, [onLeave]);
|
|
172
|
+
|
|
173
|
+
const surface = useMemo<ScatterSurfaceProps>(
|
|
174
|
+
() => ({
|
|
175
|
+
onPointerDown: handleDown,
|
|
176
|
+
onPointerMove: handleMove,
|
|
177
|
+
onPointerUp: handleUp,
|
|
178
|
+
onPointerCancel: handleAbandon,
|
|
179
|
+
onLostPointerCapture: handleAbandon,
|
|
180
|
+
onPointerLeave: handleLeave,
|
|
181
|
+
style: {
|
|
182
|
+
cursor: enabled ? (turning ? 'grabbing' : 'grab') : 'default',
|
|
183
|
+
touchAction: 'none',
|
|
184
|
+
},
|
|
185
|
+
}),
|
|
186
|
+
[
|
|
187
|
+
enabled,
|
|
188
|
+
handleAbandon,
|
|
189
|
+
handleDown,
|
|
190
|
+
handleLeave,
|
|
191
|
+
handleMove,
|
|
192
|
+
handleUp,
|
|
193
|
+
turning,
|
|
194
|
+
],
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
return { turning, surface };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* How far a press may travel and still be a tap, in pixels of total travel.
|
|
202
|
+
*
|
|
203
|
+
* Generous on purpose: a mouse moves a pixel or two under the click of its own
|
|
204
|
+
* button, and a finger moves rather more, so a stricter number turns "pick
|
|
205
|
+
* this sample" into "turn the box by nothing at all" for anybody whose hand is
|
|
206
|
+
* not steady.
|
|
207
|
+
*/
|
|
208
|
+
const TAP_SLACK = 5;
|
|
209
|
+
|
|
210
|
+
/** What the pointer has done since the last frame, waiting for the next. */
|
|
211
|
+
interface PendingTurn {
|
|
212
|
+
/** Where it is, for a move that is not turning anything. */
|
|
213
|
+
x: number;
|
|
214
|
+
/** Where it is. */
|
|
215
|
+
y: number;
|
|
216
|
+
/** How far it has moved across since the last frame. */
|
|
217
|
+
dx: number;
|
|
218
|
+
/** How far it has moved down. */
|
|
219
|
+
dy: number;
|
|
220
|
+
}
|
package/src/ui.ts
CHANGED
|
@@ -18,6 +18,7 @@ export * from './pedagogy/ui/index.ts';
|
|
|
18
18
|
export * from './periodic/ui/index.ts';
|
|
19
19
|
export * from './projection/ui/index.ts';
|
|
20
20
|
export * from './scatter/ui/index.ts';
|
|
21
|
+
export * from './scatter3d/ui/index.ts';
|
|
21
22
|
export * from './share/ui/index.ts';
|
|
22
23
|
export * from './shared/ui/index.ts';
|
|
23
24
|
export * from './spectra/ui/index.ts';
|
package/styles/chrome.css
CHANGED
|
@@ -29,13 +29,13 @@
|
|
|
29
29
|
--shadow-md: 0 4px 14px rgb(16 32 48 / 10%);
|
|
30
30
|
--shadow-lg: 0 12px 32px rgb(16 32 48 / 16%);
|
|
31
31
|
|
|
32
|
-
--header-height:
|
|
32
|
+
--header-height: 3rem;
|
|
33
33
|
--page-max: 84rem;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
/* The size goes on `body`, never on `html`: `rem` has to stay the browser's
|
|
37
37
|
16px, or every measurement written here shrinks by 6.25% and the bar renders
|
|
38
|
-
|
|
38
|
+
45px tall on one site and 48px on its sibling. */
|
|
39
39
|
body {
|
|
40
40
|
font-family:
|
|
41
41
|
-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial,
|
|
@@ -75,6 +75,14 @@ button {
|
|
|
75
75
|
gap: 1rem;
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
/* A bar over something that fills the window rather than over a column of
|
|
79
|
+
content. The cap exists so the bar lines up with the page under it; on a
|
|
80
|
+
workbench there is no such column, and the centred bar reads as a mistake —
|
|
81
|
+
the brand floats inwards while the tool below runs to both edges. */
|
|
82
|
+
.app-header__inner--full {
|
|
83
|
+
max-width: none;
|
|
84
|
+
}
|
|
85
|
+
|
|
78
86
|
.brand {
|
|
79
87
|
display: flex;
|
|
80
88
|
align-items: center;
|
|
@@ -148,7 +156,7 @@ button {
|
|
|
148
156
|
.nav-link {
|
|
149
157
|
display: inline-flex;
|
|
150
158
|
align-items: center;
|
|
151
|
-
padding: 0.
|
|
159
|
+
padding: 0.3rem 0.7rem;
|
|
152
160
|
border: 0;
|
|
153
161
|
border-radius: 8px;
|
|
154
162
|
background: none;
|
|
@@ -175,6 +183,16 @@ button {
|
|
|
175
183
|
color: var(--accent);
|
|
176
184
|
}
|
|
177
185
|
|
|
186
|
+
/* A page the site cannot open yet: still listed, so the reader learns it is
|
|
187
|
+
there, but plainly not pressable. It never takes the hover wash, or it would
|
|
188
|
+
read as reachable under the pointer. */
|
|
189
|
+
.nav-link--disabled,
|
|
190
|
+
.nav-link--disabled:hover {
|
|
191
|
+
background: none;
|
|
192
|
+
color: var(--text-faint);
|
|
193
|
+
cursor: default;
|
|
194
|
+
}
|
|
195
|
+
|
|
178
196
|
.nav-link svg {
|
|
179
197
|
width: 14px;
|
|
180
198
|
height: 14px;
|
|
@@ -184,7 +202,8 @@ button {
|
|
|
184
202
|
type is brought to the one the links beside them are set in. */
|
|
185
203
|
.citation-button .bp6-button,
|
|
186
204
|
.ecosystem-button .bp6-button {
|
|
187
|
-
|
|
205
|
+
min-height: 0;
|
|
206
|
+
padding: 0.3rem 0.7rem;
|
|
188
207
|
border-radius: 8px;
|
|
189
208
|
color: var(--text-muted);
|
|
190
209
|
font-size: 0.875rem;
|
|
@@ -214,6 +233,12 @@ button {
|
|
|
214
233
|
margin: 0 auto;
|
|
215
234
|
}
|
|
216
235
|
|
|
236
|
+
/* Under a tool that fills the window, for the reason the bar runs full width
|
|
237
|
+
above it: the two edges of the chrome answer the two edges of the page. */
|
|
238
|
+
.app-footer__inner--full {
|
|
239
|
+
max-width: none;
|
|
240
|
+
}
|
|
241
|
+
|
|
217
242
|
/* ── Printing ────────────────────────────────────────────────────────────── */
|
|
218
243
|
|
|
219
244
|
/* Students print the cheatsheet. Let them, without the chrome. */
|