matterviz 0.3.2 → 0.3.4
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/dist/EmptyState.svelte +10 -2
- package/dist/FilePicker.svelte +123 -82
- package/dist/Icon.svelte +18 -12
- package/dist/MillerIndexInput.svelte +27 -21
- package/dist/api/optimade.js +6 -6
- package/dist/app.css +216 -207
- package/dist/brillouin/BrillouinZone.svelte +292 -149
- package/dist/brillouin/BrillouinZone.svelte.d.ts +1 -1
- package/dist/brillouin/BrillouinZoneControls.svelte +32 -5
- package/dist/brillouin/BrillouinZoneExportPane.svelte +69 -42
- package/dist/brillouin/BrillouinZoneExportPane.svelte.d.ts +1 -1
- package/dist/brillouin/BrillouinZoneInfoPane.svelte +99 -68
- package/dist/brillouin/BrillouinZoneScene.svelte +275 -163
- package/dist/brillouin/BrillouinZoneScene.svelte.d.ts +1 -1
- package/dist/brillouin/BrillouinZoneTooltip.svelte +17 -7
- package/dist/brillouin/compute.js +11 -6
- package/dist/chempot-diagram/ChemPotDiagram.svelte +162 -27
- package/dist/chempot-diagram/ChemPotDiagram2D.svelte +451 -281
- package/dist/chempot-diagram/ChemPotDiagram3D.svelte +2148 -1642
- package/dist/chempot-diagram/ChemPotScene3D.svelte +8 -5
- package/dist/chempot-diagram/async-compute.svelte.d.ts +3 -0
- package/dist/chempot-diagram/async-compute.svelte.js +77 -0
- package/dist/chempot-diagram/chempot-worker.d.ts +1 -0
- package/dist/chempot-diagram/chempot-worker.js +11 -0
- package/dist/chempot-diagram/color.js +1 -2
- package/dist/chempot-diagram/compute.d.ts +10 -0
- package/dist/chempot-diagram/compute.js +250 -88
- package/dist/chempot-diagram/index.d.ts +2 -1
- package/dist/chempot-diagram/index.js +2 -1
- package/dist/chempot-diagram/temperature.js +8 -9
- package/dist/chempot-diagram/types.d.ts +3 -0
- package/dist/chempot-diagram/types.js +1 -0
- package/dist/colors/index.d.ts +1 -1
- package/dist/colors/index.js +5 -3
- package/dist/composition/BarChart.svelte +128 -55
- package/dist/composition/BubbleChart.svelte +102 -49
- package/dist/composition/Composition.svelte +100 -79
- package/dist/composition/Formula.svelte +108 -62
- package/dist/composition/FormulaFilter.svelte +665 -537
- package/dist/composition/PieChart.svelte +183 -108
- package/dist/composition/format.d.ts +5 -0
- package/dist/composition/format.js +20 -3
- package/dist/composition/parse.js +14 -9
- package/dist/convex-hull/ConvexHull.svelte +93 -40
- package/dist/convex-hull/ConvexHull.svelte.d.ts +1 -1
- package/dist/convex-hull/ConvexHull2D.svelte +549 -360
- package/dist/convex-hull/ConvexHull2D.svelte.d.ts +1 -1
- package/dist/convex-hull/ConvexHull3D.svelte +1296 -827
- package/dist/convex-hull/ConvexHull3D.svelte.d.ts +1 -1
- package/dist/convex-hull/ConvexHull4D.svelte +1004 -688
- package/dist/convex-hull/ConvexHull4D.svelte.d.ts +1 -1
- package/dist/convex-hull/ConvexHullControls.svelte +115 -28
- package/dist/convex-hull/ConvexHullControls.svelte.d.ts +1 -1
- package/dist/convex-hull/ConvexHullInfoPane.svelte +29 -3
- package/dist/convex-hull/ConvexHullStats.svelte +425 -328
- package/dist/convex-hull/ConvexHullTooltip.svelte +40 -16
- package/dist/convex-hull/GasPressureControls.svelte +104 -61
- package/dist/convex-hull/StructurePopup.svelte +25 -4
- package/dist/convex-hull/TemperatureSlider.svelte +45 -25
- package/dist/convex-hull/barycentric-coords.js +13 -7
- package/dist/convex-hull/demo-temperature.js +8 -4
- package/dist/convex-hull/gas-thermodynamics.js +17 -12
- package/dist/convex-hull/helpers.d.ts +9 -0
- package/dist/convex-hull/helpers.js +77 -34
- package/dist/convex-hull/thermodynamics.js +61 -56
- package/dist/convex-hull/types.d.ts +9 -14
- package/dist/convex-hull/types.js +0 -17
- package/dist/coordination/CoordinationBarPlot.svelte +227 -154
- package/dist/element/BohrAtom.svelte +55 -12
- package/dist/element/ElementHeading.svelte +7 -2
- package/dist/element/ElementPhoto.svelte +15 -9
- package/dist/element/ElementStats.svelte +10 -4
- package/dist/element/ElementTile.svelte +137 -73
- package/dist/element/Nucleus.svelte +39 -11
- package/dist/element/data.js +1 -1
- package/dist/feedback/ClickFeedback.svelte +16 -5
- package/dist/feedback/DragOverlay.svelte +10 -2
- package/dist/feedback/Spinner.svelte +4 -2
- package/dist/feedback/StatusMessage.svelte +8 -2
- package/dist/fermi-surface/FermiSlice.svelte +118 -88
- package/dist/fermi-surface/FermiSurface.svelte +328 -187
- package/dist/fermi-surface/FermiSurface.svelte.d.ts +1 -1
- package/dist/fermi-surface/FermiSurfaceControls.svelte +113 -46
- package/dist/fermi-surface/FermiSurfaceControls.svelte.d.ts +1 -1
- package/dist/fermi-surface/FermiSurfaceScene.svelte +535 -342
- package/dist/fermi-surface/FermiSurfaceScene.svelte.d.ts +1 -1
- package/dist/fermi-surface/FermiSurfaceTooltip.svelte +14 -5
- package/dist/fermi-surface/compute.js +16 -20
- package/dist/fermi-surface/parse.js +24 -14
- package/dist/fermi-surface/symmetry.js +2 -7
- package/dist/fermi-surface/types.d.ts +3 -5
- package/dist/heatmap-matrix/HeatmapMatrix.svelte +1019 -765
- package/dist/heatmap-matrix/HeatmapMatrix.svelte.d.ts +1 -1
- package/dist/heatmap-matrix/HeatmapMatrixControls.svelte +76 -22
- package/dist/heatmap-matrix/HeatmapMatrixControls.svelte.d.ts +2 -3
- package/dist/icons.js +47 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/io/decompress.js +1 -1
- package/dist/io/export.d.ts +3 -0
- package/dist/io/export.js +129 -143
- package/dist/io/is-binary.js +2 -3
- package/dist/io/url-drop.js +1 -2
- package/dist/isosurface/Isosurface.svelte +202 -148
- package/dist/isosurface/IsosurfaceControls.svelte +46 -28
- package/dist/isosurface/parse.js +34 -29
- package/dist/isosurface/slice.js +5 -10
- package/dist/isosurface/types.d.ts +2 -1
- package/dist/isosurface/types.js +61 -12
- package/dist/labels.js +11 -8
- package/dist/layout/FullscreenToggle.svelte +11 -2
- package/dist/layout/InfoCard.svelte +38 -6
- package/dist/layout/InfoTag.svelte +63 -32
- package/dist/layout/PropertyFilter.svelte +82 -37
- package/dist/layout/SettingsSection.svelte +85 -55
- package/dist/layout/SubpageGrid.svelte +10 -2
- package/dist/layout/json-tree/JsonNode.svelte +183 -138
- package/dist/layout/json-tree/JsonTree.svelte +499 -413
- package/dist/layout/json-tree/JsonValue.svelte +127 -99
- package/dist/layout/json-tree/utils.js +4 -2
- package/dist/marching-cubes.js +25 -2
- package/dist/math.d.ts +13 -17
- package/dist/math.js +133 -67
- package/dist/overlays/ContextMenu.svelte +65 -40
- package/dist/overlays/DraggablePane.svelte +211 -139
- package/dist/periodic-table/PeriodicTable.svelte +278 -145
- package/dist/periodic-table/PeriodicTableControls.svelte +178 -128
- package/dist/periodic-table/PropertySelect.svelte +25 -7
- package/dist/periodic-table/TableInset.svelte +8 -3
- package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte +446 -309
- package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte.d.ts +1 -1
- package/dist/phase-diagram/PhaseDiagramControls.svelte +102 -43
- package/dist/phase-diagram/PhaseDiagramControls.svelte.d.ts +1 -1
- package/dist/phase-diagram/PhaseDiagramEditorPane.svelte +63 -40
- package/dist/phase-diagram/PhaseDiagramExportPane.svelte +71 -28
- package/dist/phase-diagram/PhaseDiagramExportPane.svelte.d.ts +1 -1
- package/dist/phase-diagram/PhaseDiagramTooltip.svelte +158 -101
- package/dist/phase-diagram/TdbInfoPanel.svelte +28 -4
- package/dist/phase-diagram/build-diagram.js +9 -9
- package/dist/phase-diagram/colors.js +1 -3
- package/dist/phase-diagram/parse.js +10 -9
- package/dist/phase-diagram/svg-to-diagram.js +53 -49
- package/dist/phase-diagram/utils.d.ts +1 -0
- package/dist/phase-diagram/utils.js +80 -25
- package/dist/plot/AxisLabel.svelte +28 -3
- package/dist/plot/BarPlot.svelte +1182 -734
- package/dist/plot/BarPlot.svelte.d.ts +2 -2
- package/dist/plot/BarPlotControls.svelte +31 -5
- package/dist/plot/BarPlotControls.svelte.d.ts +1 -1
- package/dist/plot/ColorBar.svelte +479 -329
- package/dist/plot/ColorScaleSelect.svelte +27 -6
- package/dist/plot/ElementScatter.svelte +36 -15
- package/dist/plot/FillArea.svelte +152 -95
- package/dist/plot/Histogram.svelte +934 -571
- package/dist/plot/Histogram.svelte.d.ts +1 -1
- package/dist/plot/HistogramControls.svelte +53 -9
- package/dist/plot/HistogramControls.svelte.d.ts +1 -1
- package/dist/plot/InteractiveAxisLabel.svelte +34 -11
- package/dist/plot/InteractiveAxisLabel.svelte.d.ts +1 -1
- package/dist/plot/Line.svelte +63 -28
- package/dist/plot/PlotControls.svelte +157 -114
- package/dist/plot/PlotControls.svelte.d.ts +1 -1
- package/dist/plot/PlotLegend.svelte +174 -91
- package/dist/plot/PlotTooltip.svelte +45 -6
- package/dist/plot/PortalSelect.svelte +175 -147
- package/dist/plot/ReferenceLine.svelte +76 -22
- package/dist/plot/ReferenceLine3D.svelte +132 -107
- package/dist/plot/ReferencePlane.svelte +146 -121
- package/dist/plot/ScatterPlot.svelte +1681 -1091
- package/dist/plot/ScatterPlot.svelte.d.ts +2 -2
- package/dist/plot/ScatterPlot3D.svelte +256 -131
- package/dist/plot/ScatterPlot3D.svelte.d.ts +2 -2
- package/dist/plot/ScatterPlot3DControls.svelte +113 -63
- package/dist/plot/ScatterPlot3DControls.svelte.d.ts +2 -1
- package/dist/plot/ScatterPlot3DScene.svelte +608 -403
- package/dist/plot/ScatterPlot3DScene.svelte.d.ts +2 -2
- package/dist/plot/ScatterPlotControls.svelte +65 -25
- package/dist/plot/ScatterPlotControls.svelte.d.ts +1 -1
- package/dist/plot/ScatterPoint.svelte +98 -26
- package/dist/plot/ScatterPoint.svelte.d.ts +1 -0
- package/dist/plot/SpacegroupBarPlot.svelte +142 -85
- package/dist/plot/Surface3D.svelte +159 -108
- package/dist/plot/ZeroLines.svelte +55 -3
- package/dist/plot/ZoomRect.svelte +4 -2
- package/dist/plot/axis-utils.js +1 -3
- package/dist/plot/data-cleaning.js +12 -28
- package/dist/plot/data-transform.js +2 -1
- package/dist/plot/fill-utils.js +2 -0
- package/dist/plot/layout.d.ts +4 -1
- package/dist/plot/layout.js +33 -14
- package/dist/plot/reference-line.d.ts +2 -2
- package/dist/plot/reference-line.js +7 -5
- package/dist/plot/scales.js +24 -36
- package/dist/plot/types.d.ts +11 -23
- package/dist/plot/types.js +6 -11
- package/dist/plot/utils/label-placement.d.ts +32 -15
- package/dist/plot/utils/label-placement.js +227 -66
- package/dist/plot/utils/series-visibility.js +2 -3
- package/dist/rdf/RdfPlot.svelte +143 -91
- package/dist/rdf/calc-rdf.js +4 -5
- package/dist/sanitize.d.ts +4 -0
- package/dist/sanitize.js +107 -0
- package/dist/settings.d.ts +18 -6
- package/dist/settings.js +46 -16
- package/dist/spectral/Bands.svelte +632 -453
- package/dist/spectral/BandsAndDos.svelte +90 -49
- package/dist/spectral/BrillouinBandsDos.svelte +151 -93
- package/dist/spectral/Dos.svelte +389 -258
- package/dist/spectral/helpers.js +55 -43
- package/dist/state.svelte.d.ts +1 -1
- package/dist/state.svelte.js +3 -2
- package/dist/structure/Arrow.svelte +59 -20
- package/dist/structure/AtomLegend.svelte +215 -134
- package/dist/structure/Bond.svelte +73 -47
- package/dist/structure/CanvasTooltip.svelte +10 -2
- package/dist/structure/CellSelect.svelte +72 -45
- package/dist/structure/Cylinder.svelte +33 -17
- package/dist/structure/Lattice.svelte +88 -33
- package/dist/structure/Structure.svelte +1063 -797
- package/dist/structure/Structure.svelte.d.ts +1 -1
- package/dist/structure/StructureControls.svelte +349 -118
- package/dist/structure/StructureExportPane.svelte +124 -89
- package/dist/structure/StructureExportPane.svelte.d.ts +1 -1
- package/dist/structure/StructureInfoPane.svelte +304 -237
- package/dist/structure/StructureScene.svelte +879 -443
- package/dist/structure/StructureScene.svelte.d.ts +15 -7
- package/dist/structure/atom-properties.js +8 -8
- package/dist/structure/bonding.js +6 -7
- package/dist/structure/export.js +14 -29
- package/dist/structure/ferrox-wasm.js +1 -1
- package/dist/structure/index.d.ts +13 -3
- package/dist/structure/index.js +83 -23
- package/dist/structure/measure.d.ts +2 -2
- package/dist/structure/measure.js +4 -44
- package/dist/structure/parse.js +113 -141
- package/dist/structure/partial-occupancy.js +7 -10
- package/dist/structure/pbc.d.ts +1 -0
- package/dist/structure/pbc.js +16 -6
- package/dist/structure/supercell.d.ts +2 -2
- package/dist/structure/supercell.js +12 -22
- package/dist/structure/validation.js +1 -2
- package/dist/symmetry/SymmetryStats.svelte +84 -41
- package/dist/symmetry/WyckoffTable.svelte +26 -6
- package/dist/symmetry/cell-transform.js +5 -3
- package/dist/symmetry/index.js +8 -7
- package/dist/symmetry/spacegroups.js +148 -148
- package/dist/table/HeatmapTable.svelte +790 -554
- package/dist/table/HeatmapTable.svelte.d.ts +1 -1
- package/dist/table/ToggleMenu.svelte +125 -92
- package/dist/table/index.js +2 -4
- package/dist/theme/ThemeControl.svelte +21 -12
- package/dist/time.js +4 -1
- package/dist/tooltip/TooltipContent.svelte +33 -8
- package/dist/trajectory/Trajectory.svelte +758 -558
- package/dist/trajectory/TrajectoryError.svelte +14 -3
- package/dist/trajectory/TrajectoryExportPane.svelte +137 -83
- package/dist/trajectory/TrajectoryInfoPane.svelte +272 -143
- package/dist/trajectory/extract.js +10 -26
- package/dist/trajectory/format-detect.js +5 -5
- package/dist/trajectory/frame-reader.d.ts +1 -1
- package/dist/trajectory/frame-reader.js +5 -12
- package/dist/trajectory/helpers.d.ts +0 -1
- package/dist/trajectory/helpers.js +2 -17
- package/dist/trajectory/index.js +14 -12
- package/dist/trajectory/parse/ase.js +5 -4
- package/dist/trajectory/parse/hdf5.js +26 -18
- package/dist/trajectory/parse/index.js +13 -18
- package/dist/trajectory/parse/lammps.js +17 -7
- package/dist/trajectory/parse/vasp.js +5 -2
- package/dist/trajectory/parse/xyz.js +8 -7
- package/dist/trajectory/plotting.js +13 -8
- package/dist/utils.d.ts +1 -0
- package/dist/utils.js +13 -0
- package/dist/xrd/XrdPlot.svelte +337 -247
- package/dist/xrd/broadening.js +14 -9
- package/dist/xrd/calc-xrd.js +12 -18
- package/dist/xrd/parse.d.ts +1 -1
- package/dist/xrd/parse.js +17 -17
- package/package.json +99 -103
- package/readme.md +1 -1
- /package/dist/theme/{themes.js → themes.mjs} +0 -0
|
@@ -1,463 +1,660 @@
|
|
|
1
1
|
<script
|
|
2
2
|
lang="ts"
|
|
3
3
|
generics="Metadata extends Record<string, unknown> = Record<string, unknown>"
|
|
4
|
-
>
|
|
5
|
-
import {
|
|
6
|
-
import
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
let
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
4
|
+
>
|
|
5
|
+
import type { D3ColorSchemeName, D3InterpolateName } from '../colors'
|
|
6
|
+
import { format_num } from '../labels'
|
|
7
|
+
import type { Vec2, Vec3 } from '../math'
|
|
8
|
+
import type {
|
|
9
|
+
AxisConfig3D,
|
|
10
|
+
CameraProjection3D,
|
|
11
|
+
DataSeries3D,
|
|
12
|
+
DisplayConfig3D,
|
|
13
|
+
InternalPoint3D,
|
|
14
|
+
RefLine3D,
|
|
15
|
+
RefPlane,
|
|
16
|
+
ScaleType,
|
|
17
|
+
Scatter3DHandlerEvent,
|
|
18
|
+
StyleOverrides3D,
|
|
19
|
+
Surface3DConfig,
|
|
20
|
+
} from './types'
|
|
21
|
+
import { T, useTask, useThrelte } from '@threlte/core'
|
|
22
|
+
import * as extras from '@threlte/extras'
|
|
23
|
+
import { scaleLinear } from 'd3-scale'
|
|
24
|
+
import { type ComponentProps, onDestroy, type Snippet, untrack } from 'svelte'
|
|
25
|
+
import type { Camera, Scene } from 'three'
|
|
26
|
+
import * as THREE from 'three'
|
|
27
|
+
import { Line2 } from 'three/examples/jsm/lines/Line2.js'
|
|
28
|
+
import { LineGeometry } from 'three/examples/jsm/lines/LineGeometry.js'
|
|
29
|
+
import { LineMaterial } from 'three/examples/jsm/lines/LineMaterial.js'
|
|
30
|
+
import { get_series_color } from './data-transform'
|
|
31
|
+
import { normalize_to_scene } from './reference-line'
|
|
32
|
+
import ReferenceLine3D from './ReferenceLine3D.svelte'
|
|
33
|
+
import ReferencePlane from './ReferencePlane.svelte'
|
|
34
|
+
import { create_color_scale, create_size_scale } from './scales'
|
|
35
|
+
import Surface3D from './Surface3D.svelte'
|
|
36
|
+
|
|
37
|
+
let {
|
|
38
|
+
series = [],
|
|
39
|
+
series_visibility = [],
|
|
40
|
+
x_axis = {},
|
|
41
|
+
y_axis = {},
|
|
42
|
+
z_axis = {},
|
|
43
|
+
display = {},
|
|
44
|
+
styles = {},
|
|
45
|
+
surfaces = [],
|
|
46
|
+
ref_lines = [],
|
|
47
|
+
ref_planes = [],
|
|
48
|
+
color_scale = { type: `linear`, scheme: `interpolateViridis` },
|
|
49
|
+
size_scale = { type: `linear`, radius_range: [0.05, 0.2] },
|
|
50
|
+
camera_position = [10, 10, 10] as Vec3,
|
|
51
|
+
camera_projection = `perspective` as CameraProjection3D,
|
|
52
|
+
auto_rotate = 0,
|
|
53
|
+
rotation_damping = 0,
|
|
54
|
+
fov = 50,
|
|
55
|
+
min_zoom = 0.1,
|
|
56
|
+
max_zoom = 100,
|
|
57
|
+
rotate_speed = 2,
|
|
58
|
+
zoom_speed = 2,
|
|
59
|
+
pan_speed = 2,
|
|
60
|
+
ambient_light = 0.6,
|
|
61
|
+
directional_light = 0.8,
|
|
62
|
+
sphere_segments = 16,
|
|
63
|
+
gizmo = true,
|
|
64
|
+
hovered_point = $bindable(null),
|
|
65
|
+
on_point_click,
|
|
66
|
+
on_point_hover,
|
|
67
|
+
tooltip,
|
|
68
|
+
scene = $bindable(),
|
|
69
|
+
camera = $bindable(),
|
|
70
|
+
orbit_controls = $bindable(),
|
|
71
|
+
width = 0,
|
|
72
|
+
height = 0,
|
|
73
|
+
}: {
|
|
74
|
+
series?: DataSeries3D<Metadata>[]
|
|
75
|
+
series_visibility?: boolean[]
|
|
76
|
+
x_axis?: AxisConfig3D
|
|
77
|
+
y_axis?: AxisConfig3D
|
|
78
|
+
z_axis?: AxisConfig3D
|
|
79
|
+
display?: DisplayConfig3D
|
|
80
|
+
styles?: StyleOverrides3D
|
|
81
|
+
surfaces?: Surface3DConfig[]
|
|
82
|
+
ref_lines?: RefLine3D[]
|
|
83
|
+
ref_planes?: RefPlane[]
|
|
84
|
+
color_scale?: {
|
|
85
|
+
type?: ScaleType
|
|
86
|
+
scheme?: D3ColorSchemeName | D3InterpolateName
|
|
87
|
+
value_range?: [number, number]
|
|
88
|
+
}
|
|
89
|
+
size_scale?: {
|
|
90
|
+
type?: ScaleType
|
|
91
|
+
radius_range?: [number, number]
|
|
92
|
+
value_range?: [number, number]
|
|
93
|
+
}
|
|
94
|
+
camera_position?: Vec3
|
|
95
|
+
camera_projection?: CameraProjection3D
|
|
96
|
+
auto_rotate?: number
|
|
97
|
+
rotation_damping?: number
|
|
98
|
+
fov?: number
|
|
99
|
+
min_zoom?: number
|
|
100
|
+
max_zoom?: number
|
|
101
|
+
rotate_speed?: number
|
|
102
|
+
zoom_speed?: number
|
|
103
|
+
pan_speed?: number
|
|
104
|
+
ambient_light?: number
|
|
105
|
+
directional_light?: number
|
|
106
|
+
sphere_segments?: number
|
|
107
|
+
gizmo?: boolean | ComponentProps<typeof extras.Gizmo>
|
|
108
|
+
hovered_point?: InternalPoint3D<Metadata> | null
|
|
109
|
+
on_point_click?: (data: Scatter3DHandlerEvent<Metadata>) => void
|
|
110
|
+
on_point_hover?: (data: Scatter3DHandlerEvent<Metadata> | null) => void
|
|
111
|
+
tooltip?: Snippet<[Scatter3DHandlerEvent<Metadata>]>
|
|
112
|
+
scene?: Scene
|
|
113
|
+
camera?: Camera
|
|
114
|
+
orbit_controls?: ComponentProps<typeof extras.OrbitControls>[`ref`]
|
|
115
|
+
width?: number
|
|
116
|
+
height?: number
|
|
117
|
+
} = $props()
|
|
118
|
+
|
|
119
|
+
const threlte = useThrelte()
|
|
120
|
+
$effect(() => {
|
|
121
|
+
scene = threlte.scene
|
|
122
|
+
camera = threlte.camera.current
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
extras.interactivity()
|
|
126
|
+
|
|
127
|
+
// Scene dimensions: x/y are horizontal (2:2), z is vertical (1)
|
|
128
|
+
// Note: In Three.js, Y is vertical. We map user's Z → Three.js Y (vertical)
|
|
129
|
+
// and user's Y → Three.js Z (depth). So scene_z here refers to Three.js Y.
|
|
130
|
+
const scene_x = 10 // user X → Three.js X (horizontal)
|
|
131
|
+
const scene_y = 10 // user Y → Three.js Z (depth/horizontal)
|
|
132
|
+
const scene_z = 5 // user Z → Three.js Y (vertical)
|
|
133
|
+
const half_x = scene_x / 2
|
|
134
|
+
const half_y = scene_y / 2
|
|
135
|
+
const half_z = scene_z / 2
|
|
136
|
+
|
|
137
|
+
// Dynamic backside positions - axes/grids/planes always face away from camera
|
|
138
|
+
// pos.x/y/z are the Three.js positions where axes attach (backside of cube)
|
|
139
|
+
let pos = $state({ x: -half_x, y: -half_z, z: -half_y })
|
|
140
|
+
|
|
141
|
+
// Update backside positions when camera crosses axis planes
|
|
142
|
+
useTask(() => {
|
|
143
|
+
if (!camera) return
|
|
144
|
+
const cam = camera.position
|
|
43
145
|
// Only update when sign changes to avoid triggering geometry recreation every frame
|
|
44
|
-
const new_x = cam.x > 0 ? -half_x : half_x
|
|
45
|
-
const new_y = cam.y > 0 ? -half_z : half_z
|
|
46
|
-
const new_z = cam.z > 0 ? -half_y : half_y
|
|
47
|
-
if (pos.x !== new_x)
|
|
48
|
-
|
|
49
|
-
if (pos.
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
146
|
+
const new_x = cam.x > 0 ? -half_x : half_x
|
|
147
|
+
const new_y = cam.y > 0 ? -half_z : half_z
|
|
148
|
+
const new_z = cam.z > 0 ? -half_y : half_y
|
|
149
|
+
if (pos.x !== new_x) pos.x = new_x
|
|
150
|
+
if (pos.y !== new_y) pos.y = new_y
|
|
151
|
+
if (pos.z !== new_z) pos.z = new_z
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
// Sign helpers for tick/label offsets (point outward from cube center)
|
|
155
|
+
const sign_x = $derived(pos.x < 0 ? -1 : 1)
|
|
156
|
+
const sign_y = $derived(pos.y < 0 ? -1 : 1)
|
|
157
|
+
|
|
158
|
+
// Flatten all points from series
|
|
159
|
+
let all_points = $derived(
|
|
160
|
+
series
|
|
161
|
+
.filter(Boolean)
|
|
162
|
+
.flatMap((srs, series_idx) =>
|
|
163
|
+
srs.x.map((x_val, point_idx) => ({
|
|
164
|
+
x: x_val,
|
|
165
|
+
y: srs.y[point_idx],
|
|
166
|
+
z: srs.z[point_idx],
|
|
167
|
+
series_idx,
|
|
168
|
+
point_idx,
|
|
169
|
+
color_value: srs.color_values?.[point_idx] ?? null,
|
|
170
|
+
size_value: srs.size_values?.[point_idx] ?? null,
|
|
171
|
+
metadata: Array.isArray(srs.metadata)
|
|
172
|
+
? srs.metadata[point_idx]
|
|
173
|
+
: srs.metadata,
|
|
174
|
+
point_style: Array.isArray(srs.point_style)
|
|
175
|
+
? srs.point_style[point_idx]
|
|
176
|
+
: srs.point_style,
|
|
177
|
+
}))
|
|
178
|
+
),
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
// Sample surface points for range calculation (10x10 grid)
|
|
182
|
+
function sample_surface(
|
|
183
|
+
surface: Surface3DConfig,
|
|
184
|
+
): { x: number; y: number; z: number }[] {
|
|
185
|
+
const n = 10
|
|
186
|
+
const pts: { x: number; y: number; z: number }[] = []
|
|
79
187
|
if (surface.type === `grid` && surface.z_fn) {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
}
|
|
188
|
+
const [x0, x1] = surface.x_range ?? [-1, 1]
|
|
189
|
+
const [y0, y1] = surface.y_range ?? [-1, 1]
|
|
190
|
+
for (let i = 0; i <= n; i++) {
|
|
191
|
+
for (let j = 0; j <= n; j++) {
|
|
192
|
+
const x = x0 + (i / n) * (x1 - x0), y = y0 + (j / n) * (y1 - y0)
|
|
193
|
+
pts.push({ x, y, z: surface.z_fn(x, y) })
|
|
87
194
|
}
|
|
88
|
-
|
|
89
|
-
else if (surface.type === `parametric` && surface.parametric_fn) {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
195
|
+
}
|
|
196
|
+
} else if (surface.type === `parametric` && surface.parametric_fn) {
|
|
197
|
+
const [u0, u1] = surface.u_range ?? [0, 1]
|
|
198
|
+
const [v0, v1] = surface.v_range ?? [0, 1]
|
|
199
|
+
for (let i = 0; i <= n; i++) {
|
|
200
|
+
for (let j = 0; j <= n; j++) {
|
|
201
|
+
pts.push(
|
|
202
|
+
surface.parametric_fn(u0 + (i / n) * (u1 - u0), v0 + (j / n) * (v1 - v0)),
|
|
203
|
+
)
|
|
96
204
|
}
|
|
205
|
+
}
|
|
206
|
+
} else if (surface.type === `triangulated` && surface.points) {
|
|
207
|
+
pts.push(...surface.points)
|
|
97
208
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
const valid = values.filter(isFinite)
|
|
108
|
-
if (!valid.length)
|
|
109
|
-
|
|
110
|
-
let [min, max] = [Math.min(...valid), Math.max(...valid)];
|
|
209
|
+
return pts.filter((pt) => isFinite(pt.x) && isFinite(pt.y) && isFinite(pt.z))
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Compute axis range with D3's nice() for clean boundaries
|
|
213
|
+
function compute_range(
|
|
214
|
+
values: number[],
|
|
215
|
+
range?: [number | null, number | null],
|
|
216
|
+
): Vec2 {
|
|
217
|
+
if (range?.[0] != null && range?.[1] != null) return range as Vec2
|
|
218
|
+
const valid = values.filter(isFinite)
|
|
219
|
+
if (!valid.length) return [0, 1]
|
|
220
|
+
let [min, max] = [Math.min(...valid), Math.max(...valid)]
|
|
111
221
|
const pad = min === max
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
if (range?.[0] == null)
|
|
115
|
-
|
|
116
|
-
if (range?.[1] == null)
|
|
117
|
-
max += pad;
|
|
222
|
+
? (min === 0 ? 1 : Math.abs(min * 0.1))
|
|
223
|
+
: (max - min) * 0.05
|
|
224
|
+
if (range?.[0] == null) min -= pad
|
|
225
|
+
if (range?.[1] == null) max += pad
|
|
118
226
|
return scaleLinear().domain([range?.[0] ?? min, range?.[1] ?? max]).nice()
|
|
119
|
-
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
let
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
],
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
227
|
+
.domain() as Vec2
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Collect xyz values from points and surfaces
|
|
231
|
+
let surface_samples = $derived(surfaces.flatMap(sample_surface))
|
|
232
|
+
let x_range = $derived(
|
|
233
|
+
compute_range([
|
|
234
|
+
...all_points.map((point) => point.x),
|
|
235
|
+
...surface_samples.map((point) => point.x),
|
|
236
|
+
], x_axis.range),
|
|
237
|
+
)
|
|
238
|
+
let y_range = $derived(
|
|
239
|
+
compute_range([
|
|
240
|
+
...all_points.map((point) => point.y),
|
|
241
|
+
...surface_samples.map((point) => point.y),
|
|
242
|
+
], y_axis.range),
|
|
243
|
+
)
|
|
244
|
+
let z_range = $derived(
|
|
245
|
+
compute_range([
|
|
246
|
+
...all_points.map((point) => point.z),
|
|
247
|
+
...surface_samples.map((point) => point.z),
|
|
248
|
+
], z_axis.range),
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
const normalize_x = (value: number) => normalize_to_scene(value, x_range, scene_x)
|
|
252
|
+
const normalize_y = (value: number) => normalize_to_scene(value, y_range, scene_y)
|
|
253
|
+
const normalize_z = (value: number) => normalize_to_scene(value, z_range, scene_z)
|
|
254
|
+
|
|
255
|
+
// Color/size scales
|
|
256
|
+
let all_color_values = $derived(
|
|
257
|
+
all_points.map((pt) => pt.color_value).filter((val): val is number => val != null),
|
|
258
|
+
)
|
|
259
|
+
let auto_color_range: [number, number] = $derived.by(() => {
|
|
260
|
+
if (!all_color_values.length) return [0, 1]
|
|
261
|
+
let min = all_color_values[0]
|
|
262
|
+
let max = all_color_values[0]
|
|
145
263
|
for (const val of all_color_values) {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
else if (val > max)
|
|
149
|
-
max = val;
|
|
264
|
+
if (val < min) min = val
|
|
265
|
+
else if (val > max) max = val
|
|
150
266
|
}
|
|
151
|
-
return [min, max]
|
|
152
|
-
})
|
|
153
|
-
let all_size_values = $derived(
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
267
|
+
return [min, max]
|
|
268
|
+
})
|
|
269
|
+
let all_size_values = $derived(
|
|
270
|
+
all_points.map((pt) => pt.size_value).filter((val): val is number => val != null),
|
|
271
|
+
)
|
|
272
|
+
let color_scale_fn = $derived(create_color_scale(color_scale, auto_color_range))
|
|
273
|
+
let size_scale_fn = $derived(create_size_scale(size_scale, all_size_values))
|
|
274
|
+
|
|
275
|
+
// Process points with normalized positions
|
|
276
|
+
// Swap Y/Z for Three.js: user Z → Three.js Y (vertical), user Y → Three.js Z (depth)
|
|
277
|
+
let processed_points = $derived(
|
|
278
|
+
all_points.map((pt): InternalPoint3D<Metadata> => ({
|
|
279
|
+
...pt,
|
|
280
|
+
x: normalize_x(pt.x), // user X → Three.js X
|
|
281
|
+
y: normalize_z(pt.z), // user Z → Three.js Y (vertical)
|
|
282
|
+
z: normalize_y(pt.y), // user Y → Three.js Z (depth)
|
|
283
|
+
})),
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
// Group points by radius, with per-instance colors
|
|
287
|
+
type RadiusGroup = {
|
|
288
|
+
radius: number
|
|
289
|
+
points: InternalPoint3D<Metadata>[]
|
|
290
|
+
colors: string[]
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
let radius_groups = $derived.by((): RadiusGroup[] => {
|
|
294
|
+
const groups: Record<string, RadiusGroup> = {}
|
|
166
295
|
for (const pt of processed_points) {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
groups[key].colors.push(color);
|
|
296
|
+
const srs = series[pt.series_idx]
|
|
297
|
+
if (!(series_visibility[pt.series_idx] ?? srs?.visible ?? true)) continue
|
|
298
|
+
const color = pt.color_value != null
|
|
299
|
+
? color_scale_fn(pt.color_value)
|
|
300
|
+
: pt.point_style?.fill ?? get_series_color(pt.series_idx)
|
|
301
|
+
const radius = pt.size_value != null
|
|
302
|
+
? size_scale_fn(pt.size_value)
|
|
303
|
+
: (pt.point_style?.radius ?? styles.point?.size ?? 2) * 0.05
|
|
304
|
+
const key = radius.toFixed(4)
|
|
305
|
+
;(groups[key] ??= { radius, points: [], colors: [] }).points.push(pt)
|
|
306
|
+
groups[key].colors.push(color)
|
|
179
307
|
}
|
|
180
|
-
return Object.values(groups)
|
|
181
|
-
})
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
let
|
|
185
|
-
let
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
308
|
+
return Object.values(groups)
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
// Projection settings - render point shadows on background planes
|
|
312
|
+
let proj_opacity = $derived(display.projection_opacity ?? 0.3)
|
|
313
|
+
let proj_scale = $derived(display.projection_scale ?? 0.5)
|
|
314
|
+
|
|
315
|
+
// Projection plane configs: each fixes one axis to the backside position
|
|
316
|
+
type ProjectionConfig = {
|
|
317
|
+
key: `xy` | `xz` | `yz`
|
|
318
|
+
get_pos: (pt: InternalPoint3D<Metadata>) => Vec3
|
|
319
|
+
}
|
|
320
|
+
let projection_configs = $derived(
|
|
321
|
+
([`xy`, `xz`, `yz`] as const)
|
|
322
|
+
.filter((key) => display.projections?.[key])
|
|
323
|
+
.map((key): ProjectionConfig => ({
|
|
324
|
+
key,
|
|
325
|
+
get_pos: key === `xy`
|
|
326
|
+
? (pt) => [pt.x, pos.y, pt.z]
|
|
327
|
+
: key === `xz`
|
|
328
|
+
? (pt) => [pt.x, pt.y, pos.z]
|
|
329
|
+
: (pt) => [pos.x, pt.y, pt.z],
|
|
330
|
+
})),
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
// Series line data for connecting points
|
|
334
|
+
type SeriesLineData = {
|
|
335
|
+
series_idx: number
|
|
336
|
+
color: string
|
|
337
|
+
width: number
|
|
338
|
+
dashed: boolean
|
|
339
|
+
line2: Line2
|
|
340
|
+
geometry: LineGeometry
|
|
341
|
+
material: LineMaterial
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Track previous lines for cleanup
|
|
345
|
+
let series_lines: SeriesLineData[] = $state([])
|
|
346
|
+
|
|
347
|
+
$effect(() => {
|
|
198
348
|
// Dispose old lines before creating new ones
|
|
199
349
|
for (const line_data of untrack(() => series_lines)) {
|
|
200
|
-
|
|
201
|
-
|
|
350
|
+
line_data.geometry.dispose()
|
|
351
|
+
line_data.material.dispose()
|
|
202
352
|
}
|
|
203
|
-
|
|
353
|
+
|
|
354
|
+
const lines: SeriesLineData[] = []
|
|
204
355
|
for (let series_idx = 0; series_idx < series.length; series_idx++) {
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
356
|
+
const srs = series[series_idx]
|
|
357
|
+
if (!srs?.line_style) continue
|
|
358
|
+
if (!(series_visibility[series_idx] ?? srs.visible ?? true)) continue
|
|
359
|
+
|
|
360
|
+
// Get points for this series in order
|
|
361
|
+
const series_points = processed_points
|
|
362
|
+
.filter((pt) => pt.series_idx === series_idx)
|
|
363
|
+
.sort((a, b) => a.point_idx - b.point_idx)
|
|
364
|
+
|
|
365
|
+
if (series_points.length < 2) continue
|
|
366
|
+
|
|
367
|
+
// Create fat line geometry (LineGeometry for Line2)
|
|
368
|
+
const positions: number[] = []
|
|
369
|
+
for (const pt of series_points) {
|
|
370
|
+
positions.push(pt.x, pt.y, pt.z)
|
|
371
|
+
}
|
|
372
|
+
const geometry = new LineGeometry()
|
|
373
|
+
geometry.setPositions(positions)
|
|
374
|
+
|
|
375
|
+
// Determine line style
|
|
376
|
+
const line_style = srs.line_style
|
|
377
|
+
const color = line_style.stroke ??
|
|
378
|
+
(Array.isArray(srs.point_style)
|
|
379
|
+
? srs.point_style[0]?.fill
|
|
380
|
+
: srs.point_style?.fill) ??
|
|
381
|
+
get_series_color(series_idx)
|
|
382
|
+
const line_width = line_style.stroke_width ?? 2
|
|
383
|
+
const dashed = Boolean(line_style.line_dash)
|
|
384
|
+
|
|
385
|
+
// Create LineMaterial for fat lines (linewidth is in pixels when resolution is set)
|
|
386
|
+
// Use placeholder resolution; the separate resolution effect updates it
|
|
387
|
+
const material = new LineMaterial({
|
|
388
|
+
color: new THREE.Color(color).getHex(),
|
|
389
|
+
linewidth: line_width, // Width in pixels
|
|
390
|
+
dashed,
|
|
391
|
+
dashScale: dashed ? 2 : 1,
|
|
392
|
+
dashSize: 0.1,
|
|
393
|
+
gapSize: 0.05,
|
|
394
|
+
resolution: new THREE.Vector2(1, 1),
|
|
395
|
+
})
|
|
396
|
+
|
|
397
|
+
const line2 = new Line2(geometry, material)
|
|
398
|
+
line2.computeLineDistances()
|
|
399
|
+
|
|
400
|
+
lines.push({
|
|
401
|
+
series_idx,
|
|
402
|
+
color,
|
|
403
|
+
width: line_width,
|
|
404
|
+
dashed,
|
|
405
|
+
line2,
|
|
406
|
+
geometry,
|
|
407
|
+
material,
|
|
408
|
+
})
|
|
254
409
|
}
|
|
255
|
-
series_lines = lines
|
|
256
|
-
})
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
const
|
|
410
|
+
series_lines = lines
|
|
411
|
+
})
|
|
412
|
+
|
|
413
|
+
// Update LineMaterial resolution when canvas size changes
|
|
414
|
+
$effect(() => {
|
|
415
|
+
const canvas_width = width || 1
|
|
416
|
+
const canvas_height = height || 1
|
|
261
417
|
for (const line_data of series_lines) {
|
|
262
|
-
|
|
418
|
+
line_data.material.resolution.set(canvas_width, canvas_height)
|
|
263
419
|
}
|
|
264
|
-
})
|
|
265
|
-
|
|
266
|
-
|
|
420
|
+
})
|
|
421
|
+
|
|
422
|
+
// Cleanup on component destroy
|
|
423
|
+
onDestroy(() => {
|
|
267
424
|
for (const { geometry, material } of series_lines) {
|
|
268
|
-
|
|
269
|
-
|
|
425
|
+
geometry.dispose()
|
|
426
|
+
material.dispose()
|
|
270
427
|
}
|
|
271
|
-
Object.values(axis_geometries).forEach((
|
|
428
|
+
Object.values(axis_geometries).forEach((geom) => geom.dispose())
|
|
272
429
|
for (const data of Object.values(axis_geom_data)) {
|
|
273
|
-
|
|
274
|
-
|
|
430
|
+
data.tick_geoms.forEach((geom) => geom.dispose())
|
|
431
|
+
data.grid_geoms.flat().forEach((geom) => geom.dispose())
|
|
275
432
|
}
|
|
276
|
-
})
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
const
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
433
|
+
})
|
|
434
|
+
|
|
435
|
+
// Generate axis ticks using D3's smart tick generation
|
|
436
|
+
function gen_ticks(
|
|
437
|
+
range: [number, number],
|
|
438
|
+
ticks?: AxisConfig3D[`ticks`],
|
|
439
|
+
): number[] {
|
|
440
|
+
if (Array.isArray(ticks)) return ticks
|
|
441
|
+
const [min, max] = range
|
|
442
|
+
if (!isFinite(min) || !isFinite(max) || min === max) return [min]
|
|
443
|
+
const count = typeof ticks === `number` ? ticks : 5
|
|
444
|
+
return scaleLinear().domain([min, max]).ticks(count)
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
let x_ticks = $derived(gen_ticks(x_range, x_axis.ticks))
|
|
448
|
+
let y_ticks = $derived(gen_ticks(y_range, y_axis.ticks))
|
|
449
|
+
let z_ticks = $derived(gen_ticks(z_range, z_axis.ticks))
|
|
450
|
+
|
|
451
|
+
// Create axis line geometry - reuses a shared Float32Array for efficiency
|
|
452
|
+
function create_line_geometry(start: Vec3, end: Vec3): THREE.BufferGeometry {
|
|
453
|
+
const geometry = new THREE.BufferGeometry()
|
|
454
|
+
const positions = new Float32Array([...start, ...end])
|
|
455
|
+
geometry.setAttribute(`position`, new THREE.BufferAttribute(positions, 3))
|
|
456
|
+
return geometry
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Build event data for point interactions
|
|
460
|
+
function make_event_data(
|
|
461
|
+
point: InternalPoint3D<Metadata>,
|
|
462
|
+
event?: MouseEvent,
|
|
463
|
+
): Scatter3DHandlerEvent<Metadata> | null {
|
|
464
|
+
const orig = all_points.find(
|
|
465
|
+
(pt) => pt.series_idx === point.series_idx && pt.point_idx === point.point_idx,
|
|
466
|
+
)
|
|
467
|
+
if (!orig) return null
|
|
302
468
|
return {
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
469
|
+
x: orig.x,
|
|
470
|
+
y: orig.y,
|
|
471
|
+
z: orig.z,
|
|
472
|
+
metadata: point.metadata ?? null,
|
|
473
|
+
label: series[point.series_idx]?.label ?? null,
|
|
474
|
+
series_idx: point.series_idx,
|
|
475
|
+
x_axis,
|
|
476
|
+
y_axis,
|
|
477
|
+
z_axis,
|
|
478
|
+
x_formatted: format_num(orig.x, x_axis.format || `.3~g`),
|
|
479
|
+
y_formatted: format_num(orig.y, y_axis.format || `.3~g`),
|
|
480
|
+
z_formatted: format_num(orig.z, z_axis.format || `.3~g`),
|
|
481
|
+
color_value: point.color_value,
|
|
482
|
+
fullscreen: false,
|
|
483
|
+
event,
|
|
484
|
+
point,
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function handle_point_enter(point: InternalPoint3D<Metadata>) {
|
|
489
|
+
hovered_point = point
|
|
490
|
+
const data = make_event_data(point)
|
|
491
|
+
if (data) on_point_hover?.(data)
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function handle_point_click(point: InternalPoint3D<Metadata>, event: MouseEvent) {
|
|
495
|
+
const data = make_event_data(point, event)
|
|
496
|
+
if (data) on_point_click?.(data)
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// Gizmo props - parent (ScatterPlot3D) handles className and ColorBar offset adjustments
|
|
500
|
+
let gizmo_props = $derived(
|
|
501
|
+
gizmo === false
|
|
502
|
+
? null
|
|
503
|
+
: gizmo === true
|
|
504
|
+
? { background: { enabled: false }, offset: { left: 5, bottom: 5 } }
|
|
505
|
+
: gizmo,
|
|
506
|
+
)
|
|
507
|
+
|
|
508
|
+
// Orbit controls - snappy with minimal inertia
|
|
509
|
+
let orbit_controls_props = $derived({
|
|
340
510
|
enableRotate: rotate_speed > 0,
|
|
341
511
|
rotateSpeed: rotate_speed,
|
|
342
512
|
enableZoom: zoom_speed > 0,
|
|
343
513
|
zoomSpeed: zoom_speed,
|
|
344
514
|
enablePan: pan_speed > 0,
|
|
345
515
|
panSpeed: pan_speed,
|
|
346
|
-
target: [0, 0, 0],
|
|
516
|
+
target: [0, 0, 0] as Vec3,
|
|
347
517
|
maxZoom: max_zoom,
|
|
348
518
|
minZoom: min_zoom,
|
|
349
519
|
autoRotate: Boolean(auto_rotate),
|
|
350
520
|
autoRotateSpeed: auto_rotate,
|
|
351
521
|
enableDamping: rotation_damping > 0,
|
|
352
522
|
dampingFactor: rotation_damping,
|
|
353
|
-
})
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
const
|
|
357
|
-
|
|
358
|
-
|
|
523
|
+
})
|
|
524
|
+
|
|
525
|
+
// Axis configuration for rendering
|
|
526
|
+
const tick_length = 0.15
|
|
527
|
+
type AxisKey = `x` | `y` | `z`
|
|
528
|
+
const AXIS_KEYS: readonly AxisKey[] = [`x`, `y`, `z`]
|
|
529
|
+
|
|
530
|
+
// Main axis line geometries - updated when backside positions change
|
|
531
|
+
let axis_geometries: Record<AxisKey, THREE.BufferGeometry> = $state({
|
|
359
532
|
x: create_line_geometry([-half_x, -half_z, -half_y], [half_x, -half_z, -half_y]),
|
|
360
533
|
y: create_line_geometry([-half_x, -half_z, -half_y], [-half_x, -half_z, half_y]),
|
|
361
534
|
z: create_line_geometry([-half_x, -half_z, -half_y], [-half_x, half_z, -half_y]),
|
|
362
|
-
})
|
|
363
|
-
|
|
535
|
+
})
|
|
536
|
+
|
|
537
|
+
$effect(() => {
|
|
364
538
|
// Capture pos values for dependency tracking
|
|
365
|
-
const { x: px, y: py, z: pz } = pos
|
|
539
|
+
const { x: px, y: py, z: pz } = pos
|
|
366
540
|
untrack(() => {
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
});
|
|
541
|
+
for (const key of AXIS_KEYS) axis_geometries[key].dispose()
|
|
542
|
+
})
|
|
370
543
|
// X-axis: spans full X, positioned at backside Y and Z
|
|
371
|
-
axis_geometries.x = create_line_geometry([-half_x, py, pz], [half_x, py, pz])
|
|
544
|
+
axis_geometries.x = create_line_geometry([-half_x, py, pz], [half_x, py, pz])
|
|
372
545
|
// Y-axis (user Y → Three.js Z): spans full Z, positioned at backside X and Y
|
|
373
|
-
axis_geometries.y = create_line_geometry([px, py, -half_y], [px, py, half_y])
|
|
546
|
+
axis_geometries.y = create_line_geometry([px, py, -half_y], [px, py, half_y])
|
|
374
547
|
// Z-axis (user Z → Three.js Y): spans full Y, positioned at backside X and Z
|
|
375
|
-
axis_geometries.z = create_line_geometry([px, -half_z, pz], [px, half_z, pz])
|
|
376
|
-
})
|
|
377
|
-
|
|
378
|
-
|
|
548
|
+
axis_geometries.z = create_line_geometry([px, -half_z, pz], [px, half_z, pz])
|
|
549
|
+
})
|
|
550
|
+
|
|
551
|
+
// Axis rendering config - all positions use backside `pos` values
|
|
552
|
+
let axes_config = $derived([
|
|
379
553
|
{
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
554
|
+
key: `x` as AxisKey,
|
|
555
|
+
color: `#ef4444`,
|
|
556
|
+
axis: x_axis,
|
|
557
|
+
ticks: x_ticks,
|
|
558
|
+
range: x_range,
|
|
559
|
+
get_tick_pos: (val: number): Vec3 => [normalize_x(val), pos.y, pos.z],
|
|
560
|
+
get_tick_end: (
|
|
561
|
+
val: number,
|
|
562
|
+
): Vec3 => [normalize_x(val), pos.y + sign_y * tick_length, pos.z],
|
|
563
|
+
get_grid_lines: (val: number): [Vec3, Vec3][] => {
|
|
564
|
+
const px = normalize_x(val)
|
|
565
|
+
return [
|
|
566
|
+
[[px, -half_z, pos.z], [px, half_z, pos.z]],
|
|
567
|
+
[[px, pos.y, -half_y], [px, pos.y, half_y]],
|
|
568
|
+
]
|
|
569
|
+
},
|
|
570
|
+
tick_label_pos: (
|
|
571
|
+
val: number,
|
|
572
|
+
): Vec3 => [normalize_x(val), pos.y + sign_y * 0.4, pos.z],
|
|
573
|
+
axis_label_pos: [0, pos.y + sign_y * 0.9, pos.z] as Vec3,
|
|
396
574
|
},
|
|
397
575
|
{
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
576
|
+
key: `y` as AxisKey,
|
|
577
|
+
color: `#22c55e`,
|
|
578
|
+
axis: y_axis,
|
|
579
|
+
ticks: y_ticks,
|
|
580
|
+
range: y_range,
|
|
581
|
+
get_tick_pos: (val: number): Vec3 => [pos.x, pos.y, normalize_y(val)],
|
|
582
|
+
get_tick_end: (
|
|
583
|
+
val: number,
|
|
584
|
+
): Vec3 => [pos.x, pos.y + sign_y * tick_length, normalize_y(val)],
|
|
585
|
+
get_grid_lines: (val: number): [Vec3, Vec3][] => {
|
|
586
|
+
const py = normalize_y(val)
|
|
587
|
+
return [
|
|
588
|
+
[[-half_x, pos.y, py], [half_x, pos.y, py]],
|
|
589
|
+
[[pos.x, -half_z, py], [pos.x, half_z, py]],
|
|
590
|
+
]
|
|
591
|
+
},
|
|
592
|
+
tick_label_pos: (
|
|
593
|
+
val: number,
|
|
594
|
+
): Vec3 => [pos.x + sign_x * 0.5, pos.y + sign_y * 0.4, normalize_y(val)],
|
|
595
|
+
axis_label_pos: [
|
|
596
|
+
pos.x,
|
|
597
|
+
pos.y + sign_y * 0.9,
|
|
598
|
+
pos.z < 0 ? half_y + 0.5 : -half_y - 0.5,
|
|
599
|
+
] as Vec3,
|
|
418
600
|
},
|
|
419
601
|
{
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
602
|
+
key: `z` as AxisKey,
|
|
603
|
+
color: `#3b82f6`,
|
|
604
|
+
axis: z_axis,
|
|
605
|
+
ticks: z_ticks,
|
|
606
|
+
range: z_range,
|
|
607
|
+
get_tick_pos: (val: number): Vec3 => [pos.x, normalize_z(val), pos.z],
|
|
608
|
+
get_tick_end: (
|
|
609
|
+
val: number,
|
|
610
|
+
): Vec3 => [pos.x + sign_x * tick_length, normalize_z(val), pos.z],
|
|
611
|
+
get_grid_lines: (val: number): [Vec3, Vec3][] => {
|
|
612
|
+
const pz = normalize_z(val)
|
|
613
|
+
return [
|
|
614
|
+
[[-half_x, pz, pos.z], [half_x, pz, pos.z]],
|
|
615
|
+
[[pos.x, pz, -half_y], [pos.x, pz, half_y]],
|
|
616
|
+
]
|
|
617
|
+
},
|
|
618
|
+
tick_label_pos: (
|
|
619
|
+
val: number,
|
|
620
|
+
): Vec3 => [pos.x + sign_x * 0.5, normalize_z(val), pos.z],
|
|
621
|
+
axis_label_pos: [pos.x + sign_x, 0, pos.z] as Vec3,
|
|
436
622
|
},
|
|
437
|
-
])
|
|
438
|
-
|
|
439
|
-
|
|
623
|
+
])
|
|
624
|
+
|
|
625
|
+
// Pre-computed geometries for tick marks and grid lines, indexed by axis and tick position
|
|
626
|
+
type AxisGeomData = {
|
|
627
|
+
tick_geoms: THREE.BufferGeometry[]
|
|
628
|
+
grid_geoms: THREE.BufferGeometry[][]
|
|
629
|
+
}
|
|
630
|
+
const empty_geom_data = (): AxisGeomData => ({ tick_geoms: [], grid_geoms: [] })
|
|
631
|
+
let axis_geom_data: Record<AxisKey, AxisGeomData> = $state({
|
|
440
632
|
x: empty_geom_data(),
|
|
441
633
|
y: empty_geom_data(),
|
|
442
634
|
z: empty_geom_data(),
|
|
443
|
-
})
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
635
|
+
})
|
|
636
|
+
|
|
637
|
+
// Recreate tick/grid geometries when axes config changes
|
|
638
|
+
$effect(() => {
|
|
639
|
+
const config = axes_config
|
|
447
640
|
// Dispose old geometries (untracked to avoid dependency cycle)
|
|
448
641
|
untrack(() => {
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
})
|
|
642
|
+
for (const key of AXIS_KEYS) {
|
|
643
|
+
axis_geom_data[key].tick_geoms.forEach((geom) => geom.dispose())
|
|
644
|
+
axis_geom_data[key].grid_geoms.flat().forEach((geom) => geom.dispose())
|
|
645
|
+
}
|
|
646
|
+
})
|
|
454
647
|
for (const { key, ticks, get_tick_pos, get_tick_end, get_grid_lines } of config) {
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
648
|
+
axis_geom_data[key] = {
|
|
649
|
+
tick_geoms: ticks.map((val) =>
|
|
650
|
+
create_line_geometry(get_tick_pos(val), get_tick_end(val))
|
|
651
|
+
),
|
|
652
|
+
grid_geoms: ticks.map((val) =>
|
|
653
|
+
get_grid_lines(val).map(([start, end]) => create_line_geometry(start, end))
|
|
654
|
+
),
|
|
655
|
+
}
|
|
459
656
|
}
|
|
460
|
-
})
|
|
657
|
+
})
|
|
461
658
|
</script>
|
|
462
659
|
|
|
463
660
|
{#if camera_projection === `perspective`}
|
|
@@ -596,7 +793,11 @@ $effect(() => {
|
|
|
596
793
|
|
|
597
794
|
<!-- Instanced scatter points with per-instance colors and event handling -->
|
|
598
795
|
{#each radius_groups as group (group.radius)}
|
|
599
|
-
<extras.InstancedMesh
|
|
796
|
+
<extras.InstancedMesh
|
|
797
|
+
limit={group.points.length}
|
|
798
|
+
range={group.points.length}
|
|
799
|
+
frustumCulled={false}
|
|
800
|
+
>
|
|
600
801
|
<T.SphereGeometry args={[1, sphere_segments, sphere_segments]} />
|
|
601
802
|
<T.MeshStandardMaterial vertexColors={false} />
|
|
602
803
|
{#each group.points as point, idx (`${point.series_idx}-${point.point_idx}`)}
|
|
@@ -618,7 +819,11 @@ $effect(() => {
|
|
|
618
819
|
<!-- Plane Projections - render point shadows on enabled background planes -->
|
|
619
820
|
{#each projection_configs as { key, get_pos } (key)}
|
|
620
821
|
{#each radius_groups as group (group.radius)}
|
|
621
|
-
<extras.InstancedMesh
|
|
822
|
+
<extras.InstancedMesh
|
|
823
|
+
limit={group.points.length}
|
|
824
|
+
range={group.points.length}
|
|
825
|
+
frustumCulled={false}
|
|
826
|
+
>
|
|
622
827
|
<T.SphereGeometry args={[1, 8, 8]} />
|
|
623
828
|
<T.MeshBasicMaterial transparent opacity={proj_opacity} depthWrite={false} />
|
|
624
829
|
{#each group.points as point, idx (`${key}-${point.series_idx}-${point.point_idx}`)}
|