matterviz 0.3.2 → 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (280) hide show
  1. package/dist/EmptyState.svelte +10 -2
  2. package/dist/FilePicker.svelte +123 -82
  3. package/dist/Icon.svelte +18 -12
  4. package/dist/MillerIndexInput.svelte +27 -21
  5. package/dist/api/optimade.js +6 -6
  6. package/dist/app.css +216 -207
  7. package/dist/brillouin/BrillouinZone.svelte +292 -149
  8. package/dist/brillouin/BrillouinZone.svelte.d.ts +1 -1
  9. package/dist/brillouin/BrillouinZoneControls.svelte +32 -5
  10. package/dist/brillouin/BrillouinZoneExportPane.svelte +69 -42
  11. package/dist/brillouin/BrillouinZoneExportPane.svelte.d.ts +1 -1
  12. package/dist/brillouin/BrillouinZoneInfoPane.svelte +99 -68
  13. package/dist/brillouin/BrillouinZoneScene.svelte +275 -163
  14. package/dist/brillouin/BrillouinZoneScene.svelte.d.ts +1 -1
  15. package/dist/brillouin/BrillouinZoneTooltip.svelte +17 -7
  16. package/dist/brillouin/compute.js +11 -6
  17. package/dist/chempot-diagram/ChemPotDiagram.svelte +162 -27
  18. package/dist/chempot-diagram/ChemPotDiagram2D.svelte +451 -281
  19. package/dist/chempot-diagram/ChemPotDiagram3D.svelte +2148 -1642
  20. package/dist/chempot-diagram/ChemPotScene3D.svelte +8 -5
  21. package/dist/chempot-diagram/async-compute.svelte.d.ts +3 -0
  22. package/dist/chempot-diagram/async-compute.svelte.js +77 -0
  23. package/dist/chempot-diagram/chempot-worker.d.ts +1 -0
  24. package/dist/chempot-diagram/chempot-worker.js +11 -0
  25. package/dist/chempot-diagram/color.js +1 -2
  26. package/dist/chempot-diagram/compute.d.ts +10 -0
  27. package/dist/chempot-diagram/compute.js +250 -88
  28. package/dist/chempot-diagram/index.d.ts +2 -1
  29. package/dist/chempot-diagram/index.js +2 -1
  30. package/dist/chempot-diagram/temperature.js +8 -9
  31. package/dist/chempot-diagram/types.d.ts +3 -0
  32. package/dist/chempot-diagram/types.js +1 -0
  33. package/dist/colors/index.d.ts +1 -1
  34. package/dist/colors/index.js +5 -3
  35. package/dist/composition/BarChart.svelte +128 -55
  36. package/dist/composition/BubbleChart.svelte +102 -49
  37. package/dist/composition/Composition.svelte +100 -79
  38. package/dist/composition/Formula.svelte +108 -62
  39. package/dist/composition/FormulaFilter.svelte +665 -537
  40. package/dist/composition/PieChart.svelte +183 -108
  41. package/dist/composition/format.d.ts +5 -0
  42. package/dist/composition/format.js +20 -3
  43. package/dist/composition/parse.js +14 -9
  44. package/dist/convex-hull/ConvexHull.svelte +93 -40
  45. package/dist/convex-hull/ConvexHull.svelte.d.ts +1 -1
  46. package/dist/convex-hull/ConvexHull2D.svelte +549 -360
  47. package/dist/convex-hull/ConvexHull2D.svelte.d.ts +1 -1
  48. package/dist/convex-hull/ConvexHull3D.svelte +1296 -827
  49. package/dist/convex-hull/ConvexHull3D.svelte.d.ts +1 -1
  50. package/dist/convex-hull/ConvexHull4D.svelte +1004 -688
  51. package/dist/convex-hull/ConvexHull4D.svelte.d.ts +1 -1
  52. package/dist/convex-hull/ConvexHullControls.svelte +115 -28
  53. package/dist/convex-hull/ConvexHullControls.svelte.d.ts +1 -1
  54. package/dist/convex-hull/ConvexHullInfoPane.svelte +29 -3
  55. package/dist/convex-hull/ConvexHullStats.svelte +425 -328
  56. package/dist/convex-hull/ConvexHullTooltip.svelte +40 -16
  57. package/dist/convex-hull/GasPressureControls.svelte +104 -61
  58. package/dist/convex-hull/StructurePopup.svelte +25 -4
  59. package/dist/convex-hull/TemperatureSlider.svelte +45 -25
  60. package/dist/convex-hull/barycentric-coords.js +13 -7
  61. package/dist/convex-hull/demo-temperature.js +8 -4
  62. package/dist/convex-hull/gas-thermodynamics.js +17 -12
  63. package/dist/convex-hull/helpers.d.ts +9 -0
  64. package/dist/convex-hull/helpers.js +77 -34
  65. package/dist/convex-hull/thermodynamics.js +61 -56
  66. package/dist/convex-hull/types.d.ts +9 -14
  67. package/dist/convex-hull/types.js +0 -17
  68. package/dist/coordination/CoordinationBarPlot.svelte +227 -154
  69. package/dist/element/BohrAtom.svelte +55 -12
  70. package/dist/element/ElementHeading.svelte +7 -2
  71. package/dist/element/ElementPhoto.svelte +15 -9
  72. package/dist/element/ElementStats.svelte +10 -4
  73. package/dist/element/ElementTile.svelte +137 -73
  74. package/dist/element/Nucleus.svelte +39 -11
  75. package/dist/feedback/ClickFeedback.svelte +16 -5
  76. package/dist/feedback/DragOverlay.svelte +10 -2
  77. package/dist/feedback/Spinner.svelte +4 -2
  78. package/dist/feedback/StatusMessage.svelte +8 -2
  79. package/dist/fermi-surface/FermiSlice.svelte +118 -88
  80. package/dist/fermi-surface/FermiSurface.svelte +328 -187
  81. package/dist/fermi-surface/FermiSurface.svelte.d.ts +1 -1
  82. package/dist/fermi-surface/FermiSurfaceControls.svelte +113 -46
  83. package/dist/fermi-surface/FermiSurfaceControls.svelte.d.ts +1 -1
  84. package/dist/fermi-surface/FermiSurfaceScene.svelte +535 -342
  85. package/dist/fermi-surface/FermiSurfaceScene.svelte.d.ts +1 -1
  86. package/dist/fermi-surface/FermiSurfaceTooltip.svelte +14 -5
  87. package/dist/fermi-surface/compute.js +16 -20
  88. package/dist/fermi-surface/parse.js +24 -14
  89. package/dist/fermi-surface/symmetry.js +2 -7
  90. package/dist/fermi-surface/types.d.ts +3 -5
  91. package/dist/heatmap-matrix/HeatmapMatrix.svelte +1019 -765
  92. package/dist/heatmap-matrix/HeatmapMatrix.svelte.d.ts +1 -1
  93. package/dist/heatmap-matrix/HeatmapMatrixControls.svelte +76 -22
  94. package/dist/heatmap-matrix/HeatmapMatrixControls.svelte.d.ts +2 -3
  95. package/dist/icons.js +47 -0
  96. package/dist/index.d.ts +2 -1
  97. package/dist/index.js +2 -1
  98. package/dist/io/decompress.js +1 -1
  99. package/dist/io/export.d.ts +3 -0
  100. package/dist/io/export.js +129 -143
  101. package/dist/io/is-binary.js +2 -3
  102. package/dist/io/url-drop.js +1 -2
  103. package/dist/isosurface/Isosurface.svelte +202 -148
  104. package/dist/isosurface/IsosurfaceControls.svelte +46 -28
  105. package/dist/isosurface/parse.js +34 -29
  106. package/dist/isosurface/slice.js +5 -10
  107. package/dist/isosurface/types.d.ts +2 -1
  108. package/dist/isosurface/types.js +61 -12
  109. package/dist/labels.js +11 -8
  110. package/dist/layout/FullscreenToggle.svelte +11 -2
  111. package/dist/layout/InfoCard.svelte +38 -6
  112. package/dist/layout/InfoTag.svelte +63 -32
  113. package/dist/layout/PropertyFilter.svelte +82 -37
  114. package/dist/layout/SettingsSection.svelte +85 -55
  115. package/dist/layout/SubpageGrid.svelte +10 -2
  116. package/dist/layout/json-tree/JsonNode.svelte +183 -138
  117. package/dist/layout/json-tree/JsonTree.svelte +499 -413
  118. package/dist/layout/json-tree/JsonValue.svelte +127 -99
  119. package/dist/layout/json-tree/utils.js +4 -2
  120. package/dist/marching-cubes.js +25 -2
  121. package/dist/math.d.ts +13 -17
  122. package/dist/math.js +133 -67
  123. package/dist/overlays/ContextMenu.svelte +65 -40
  124. package/dist/overlays/DraggablePane.svelte +211 -139
  125. package/dist/periodic-table/PeriodicTable.svelte +278 -145
  126. package/dist/periodic-table/PeriodicTableControls.svelte +178 -128
  127. package/dist/periodic-table/PropertySelect.svelte +25 -7
  128. package/dist/periodic-table/TableInset.svelte +8 -3
  129. package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte +446 -309
  130. package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte.d.ts +1 -1
  131. package/dist/phase-diagram/PhaseDiagramControls.svelte +102 -43
  132. package/dist/phase-diagram/PhaseDiagramControls.svelte.d.ts +1 -1
  133. package/dist/phase-diagram/PhaseDiagramEditorPane.svelte +63 -40
  134. package/dist/phase-diagram/PhaseDiagramExportPane.svelte +71 -28
  135. package/dist/phase-diagram/PhaseDiagramExportPane.svelte.d.ts +1 -1
  136. package/dist/phase-diagram/PhaseDiagramTooltip.svelte +158 -101
  137. package/dist/phase-diagram/TdbInfoPanel.svelte +28 -4
  138. package/dist/phase-diagram/build-diagram.js +9 -9
  139. package/dist/phase-diagram/colors.js +1 -3
  140. package/dist/phase-diagram/parse.js +10 -9
  141. package/dist/phase-diagram/svg-to-diagram.js +53 -49
  142. package/dist/phase-diagram/utils.d.ts +1 -0
  143. package/dist/phase-diagram/utils.js +80 -25
  144. package/dist/plot/AxisLabel.svelte +28 -3
  145. package/dist/plot/BarPlot.svelte +1182 -734
  146. package/dist/plot/BarPlot.svelte.d.ts +2 -2
  147. package/dist/plot/BarPlotControls.svelte +31 -5
  148. package/dist/plot/BarPlotControls.svelte.d.ts +1 -1
  149. package/dist/plot/ColorBar.svelte +479 -329
  150. package/dist/plot/ColorScaleSelect.svelte +27 -6
  151. package/dist/plot/ElementScatter.svelte +36 -15
  152. package/dist/plot/FillArea.svelte +152 -95
  153. package/dist/plot/Histogram.svelte +934 -571
  154. package/dist/plot/Histogram.svelte.d.ts +1 -1
  155. package/dist/plot/HistogramControls.svelte +53 -9
  156. package/dist/plot/HistogramControls.svelte.d.ts +1 -1
  157. package/dist/plot/InteractiveAxisLabel.svelte +34 -11
  158. package/dist/plot/InteractiveAxisLabel.svelte.d.ts +1 -1
  159. package/dist/plot/Line.svelte +63 -28
  160. package/dist/plot/PlotControls.svelte +157 -114
  161. package/dist/plot/PlotControls.svelte.d.ts +1 -1
  162. package/dist/plot/PlotLegend.svelte +174 -91
  163. package/dist/plot/PlotTooltip.svelte +45 -6
  164. package/dist/plot/PortalSelect.svelte +175 -147
  165. package/dist/plot/ReferenceLine.svelte +76 -22
  166. package/dist/plot/ReferenceLine3D.svelte +132 -107
  167. package/dist/plot/ReferencePlane.svelte +146 -121
  168. package/dist/plot/ScatterPlot.svelte +1681 -1091
  169. package/dist/plot/ScatterPlot.svelte.d.ts +2 -2
  170. package/dist/plot/ScatterPlot3D.svelte +256 -131
  171. package/dist/plot/ScatterPlot3D.svelte.d.ts +2 -2
  172. package/dist/plot/ScatterPlot3DControls.svelte +113 -63
  173. package/dist/plot/ScatterPlot3DControls.svelte.d.ts +2 -1
  174. package/dist/plot/ScatterPlot3DScene.svelte +608 -403
  175. package/dist/plot/ScatterPlot3DScene.svelte.d.ts +2 -2
  176. package/dist/plot/ScatterPlotControls.svelte +65 -25
  177. package/dist/plot/ScatterPlotControls.svelte.d.ts +1 -1
  178. package/dist/plot/ScatterPoint.svelte +98 -26
  179. package/dist/plot/ScatterPoint.svelte.d.ts +1 -0
  180. package/dist/plot/SpacegroupBarPlot.svelte +142 -85
  181. package/dist/plot/Surface3D.svelte +159 -108
  182. package/dist/plot/ZeroLines.svelte +55 -3
  183. package/dist/plot/ZoomRect.svelte +4 -2
  184. package/dist/plot/axis-utils.js +1 -3
  185. package/dist/plot/data-cleaning.js +12 -28
  186. package/dist/plot/data-transform.js +2 -1
  187. package/dist/plot/fill-utils.js +2 -0
  188. package/dist/plot/layout.d.ts +4 -1
  189. package/dist/plot/layout.js +33 -14
  190. package/dist/plot/reference-line.d.ts +2 -2
  191. package/dist/plot/reference-line.js +7 -5
  192. package/dist/plot/scales.js +24 -36
  193. package/dist/plot/types.d.ts +11 -23
  194. package/dist/plot/types.js +6 -11
  195. package/dist/plot/utils/label-placement.d.ts +32 -15
  196. package/dist/plot/utils/label-placement.js +227 -66
  197. package/dist/plot/utils/series-visibility.js +2 -3
  198. package/dist/rdf/RdfPlot.svelte +143 -91
  199. package/dist/rdf/calc-rdf.js +4 -5
  200. package/dist/sanitize.d.ts +4 -0
  201. package/dist/sanitize.js +107 -0
  202. package/dist/settings.d.ts +18 -6
  203. package/dist/settings.js +46 -16
  204. package/dist/spectral/Bands.svelte +632 -453
  205. package/dist/spectral/BandsAndDos.svelte +90 -49
  206. package/dist/spectral/BrillouinBandsDos.svelte +151 -93
  207. package/dist/spectral/Dos.svelte +389 -258
  208. package/dist/spectral/helpers.js +55 -43
  209. package/dist/state.svelte.d.ts +1 -1
  210. package/dist/state.svelte.js +3 -2
  211. package/dist/structure/Arrow.svelte +59 -20
  212. package/dist/structure/AtomLegend.svelte +215 -134
  213. package/dist/structure/Bond.svelte +73 -47
  214. package/dist/structure/CanvasTooltip.svelte +10 -2
  215. package/dist/structure/CellSelect.svelte +72 -45
  216. package/dist/structure/Cylinder.svelte +33 -17
  217. package/dist/structure/Lattice.svelte +88 -33
  218. package/dist/structure/Structure.svelte +1063 -797
  219. package/dist/structure/Structure.svelte.d.ts +1 -1
  220. package/dist/structure/StructureControls.svelte +349 -118
  221. package/dist/structure/StructureExportPane.svelte +124 -89
  222. package/dist/structure/StructureExportPane.svelte.d.ts +1 -1
  223. package/dist/structure/StructureInfoPane.svelte +304 -237
  224. package/dist/structure/StructureScene.svelte +879 -443
  225. package/dist/structure/StructureScene.svelte.d.ts +15 -7
  226. package/dist/structure/atom-properties.js +8 -8
  227. package/dist/structure/bonding.js +6 -7
  228. package/dist/structure/export.js +14 -29
  229. package/dist/structure/ferrox-wasm.js +1 -1
  230. package/dist/structure/index.d.ts +13 -3
  231. package/dist/structure/index.js +83 -23
  232. package/dist/structure/measure.d.ts +2 -2
  233. package/dist/structure/measure.js +4 -44
  234. package/dist/structure/parse.js +113 -141
  235. package/dist/structure/partial-occupancy.js +7 -10
  236. package/dist/structure/pbc.d.ts +1 -0
  237. package/dist/structure/pbc.js +16 -6
  238. package/dist/structure/supercell.d.ts +2 -2
  239. package/dist/structure/supercell.js +12 -22
  240. package/dist/structure/validation.js +1 -2
  241. package/dist/symmetry/SymmetryStats.svelte +84 -41
  242. package/dist/symmetry/WyckoffTable.svelte +26 -6
  243. package/dist/symmetry/cell-transform.js +5 -3
  244. package/dist/symmetry/index.js +8 -7
  245. package/dist/symmetry/spacegroups.js +148 -148
  246. package/dist/table/HeatmapTable.svelte +790 -554
  247. package/dist/table/HeatmapTable.svelte.d.ts +1 -1
  248. package/dist/table/ToggleMenu.svelte +125 -92
  249. package/dist/table/index.js +2 -4
  250. package/dist/theme/ThemeControl.svelte +21 -12
  251. package/dist/time.js +4 -1
  252. package/dist/tooltip/TooltipContent.svelte +33 -8
  253. package/dist/trajectory/Trajectory.svelte +758 -558
  254. package/dist/trajectory/TrajectoryError.svelte +14 -3
  255. package/dist/trajectory/TrajectoryExportPane.svelte +137 -83
  256. package/dist/trajectory/TrajectoryInfoPane.svelte +272 -143
  257. package/dist/trajectory/extract.js +10 -26
  258. package/dist/trajectory/format-detect.js +5 -5
  259. package/dist/trajectory/frame-reader.d.ts +1 -1
  260. package/dist/trajectory/frame-reader.js +5 -12
  261. package/dist/trajectory/helpers.d.ts +0 -1
  262. package/dist/trajectory/helpers.js +2 -17
  263. package/dist/trajectory/index.js +14 -12
  264. package/dist/trajectory/parse/ase.js +5 -4
  265. package/dist/trajectory/parse/hdf5.js +26 -18
  266. package/dist/trajectory/parse/index.js +13 -18
  267. package/dist/trajectory/parse/lammps.js +17 -7
  268. package/dist/trajectory/parse/vasp.js +5 -2
  269. package/dist/trajectory/parse/xyz.js +8 -7
  270. package/dist/trajectory/plotting.js +13 -8
  271. package/dist/utils.d.ts +1 -0
  272. package/dist/utils.js +13 -0
  273. package/dist/xrd/XrdPlot.svelte +337 -247
  274. package/dist/xrd/broadening.js +14 -9
  275. package/dist/xrd/calc-xrd.js +12 -18
  276. package/dist/xrd/parse.d.ts +1 -1
  277. package/dist/xrd/parse.js +17 -17
  278. package/package.json +99 -103
  279. package/readme.md +1 -1
  280. /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
- >import { format_num } from '../labels';
5
- import { T, useTask, useThrelte } from '@threlte/core';
6
- import * as extras from '@threlte/extras';
7
- import { scaleLinear } from 'd3-scale';
8
- import { onDestroy, untrack } from 'svelte';
9
- import * as THREE from 'three';
10
- import { Line2 } from 'three/examples/jsm/lines/Line2.js';
11
- import { LineGeometry } from 'three/examples/jsm/lines/LineGeometry.js';
12
- import { LineMaterial } from 'three/examples/jsm/lines/LineMaterial.js';
13
- import { get_series_color } from './data-transform';
14
- import { normalize_to_scene } from './reference-line';
15
- import ReferenceLine3D from './ReferenceLine3D.svelte';
16
- import ReferencePlane from './ReferencePlane.svelte';
17
- import { create_color_scale, create_size_scale } from './scales';
18
- import Surface3D from './Surface3D.svelte';
19
- let { series = [], series_visibility = [], x_axis = {}, y_axis = {}, z_axis = {}, display = {}, styles = {}, surfaces = [], ref_lines = [], ref_planes = [], color_scale = { type: `linear`, scheme: `interpolateViridis` }, size_scale = { type: `linear`, radius_range: [0.05, 0.2] }, camera_position = [10, 10, 10], camera_projection = `perspective`, auto_rotate = 0, rotation_damping = 0, fov = 50, min_zoom = 0.1, max_zoom = 100, rotate_speed = 2, zoom_speed = 2, pan_speed = 2, ambient_light = 0.6, directional_light = 0.8, sphere_segments = 16, gizmo = true, hovered_point = $bindable(null), on_point_click, on_point_hover, tooltip, scene = $bindable(), camera = $bindable(), orbit_controls = $bindable(), width = 0, height = 0, } = $props();
20
- const threlte = useThrelte();
21
- $effect(() => {
22
- scene = threlte.scene;
23
- camera = threlte.camera.current;
24
- });
25
- extras.interactivity();
26
- // Scene dimensions: x/y are horizontal (2:2), z is vertical (1)
27
- // Note: In Three.js, Y is vertical. We map user's Z → Three.js Y (vertical)
28
- // and user's Y Three.js Z (depth). So scene_z here refers to Three.js Y.
29
- const scene_x = 10; // user X → Three.js X (horizontal)
30
- const scene_y = 10; // user Y → Three.js Z (depth/horizontal)
31
- const scene_z = 5; // user Z → Three.js Y (vertical)
32
- const half_x = scene_x / 2;
33
- const half_y = scene_y / 2;
34
- const half_z = scene_z / 2;
35
- // Dynamic backside positions - axes/grids/planes always face away from camera
36
- // pos.x/y/z are the Three.js positions where axes attach (backside of cube)
37
- let pos = $state({ x: -half_x, y: -half_z, z: -half_y });
38
- // Update backside positions when camera crosses axis planes
39
- useTask(() => {
40
- if (!camera)
41
- return;
42
- const cam = camera.position;
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
- pos.x = new_x;
49
- if (pos.y !== new_y)
50
- pos.y = new_y;
51
- if (pos.z !== new_z)
52
- pos.z = new_z;
53
- });
54
- // Sign helpers for tick/label offsets (point outward from cube center)
55
- const sign_x = $derived(pos.x < 0 ? -1 : 1);
56
- const sign_y = $derived(pos.y < 0 ? -1 : 1);
57
- // Flatten all points from series
58
- let all_points = $derived(series
59
- .filter(Boolean)
60
- .flatMap((srs, series_idx) => srs.x.map((x_val, point_idx) => ({
61
- x: x_val,
62
- y: srs.y[point_idx],
63
- z: srs.z[point_idx],
64
- series_idx,
65
- point_idx,
66
- color_value: srs.color_values?.[point_idx] ?? null,
67
- size_value: srs.size_values?.[point_idx] ?? null,
68
- metadata: Array.isArray(srs.metadata)
69
- ? srs.metadata[point_idx]
70
- : srs.metadata,
71
- point_style: Array.isArray(srs.point_style)
72
- ? srs.point_style[point_idx]
73
- : srs.point_style,
74
- }))));
75
- // Sample surface points for range calculation (10x10 grid)
76
- function sample_surface(surface) {
77
- const n = 10;
78
- const pts = [];
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
- const [x0, x1] = surface.x_range ?? [-1, 1];
81
- const [y0, y1] = surface.y_range ?? [-1, 1];
82
- for (let i = 0; i <= n; i++) {
83
- for (let j = 0; j <= n; j++) {
84
- const x = x0 + (i / n) * (x1 - x0), y = y0 + (j / n) * (y1 - y0);
85
- pts.push({ x, y, z: surface.z_fn(x, y) });
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
- const [u0, u1] = surface.u_range ?? [0, 1];
91
- const [v0, v1] = surface.v_range ?? [0, 1];
92
- for (let i = 0; i <= n; i++) {
93
- for (let j = 0; j <= n; j++) {
94
- pts.push(surface.parametric_fn(u0 + (i / n) * (u1 - u0), v0 + (j / n) * (v1 - v0)));
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
- else if (surface.type === `triangulated` && surface.points) {
99
- pts.push(...surface.points);
100
- }
101
- return pts.filter((pt) => isFinite(pt.x) && isFinite(pt.y) && isFinite(pt.z));
102
- }
103
- // Compute axis range with D3's nice() for clean boundaries
104
- function compute_range(values, range) {
105
- if (range?.[0] != null && range?.[1] != null)
106
- return range;
107
- const valid = values.filter(isFinite);
108
- if (!valid.length)
109
- return [0, 1];
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
- ? (min === 0 ? 1 : Math.abs(min * 0.1))
113
- : (max - min) * 0.05;
114
- if (range?.[0] == null)
115
- min -= pad;
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
- .domain();
120
- }
121
- // Collect xyz values from points and surfaces
122
- let surface_samples = $derived(surfaces.flatMap(sample_surface));
123
- let x_range = $derived(compute_range([
124
- ...all_points.map((p) => p.x),
125
- ...surface_samples.map((p) => p.x),
126
- ], x_axis.range));
127
- let y_range = $derived(compute_range([
128
- ...all_points.map((p) => p.y),
129
- ...surface_samples.map((p) => p.y),
130
- ], y_axis.range));
131
- let z_range = $derived(compute_range([
132
- ...all_points.map((p) => p.z),
133
- ...surface_samples.map((p) => p.z),
134
- ], z_axis.range));
135
- const normalize_x = (value) => normalize_to_scene(value, x_range, scene_x);
136
- const normalize_y = (value) => normalize_to_scene(value, y_range, scene_y);
137
- const normalize_z = (value) => normalize_to_scene(value, z_range, scene_z);
138
- // Color/size scales
139
- let all_color_values = $derived(all_points.map((pt) => pt.color_value).filter((v) => v != null));
140
- let auto_color_range = $derived.by(() => {
141
- if (!all_color_values.length)
142
- return [0, 1];
143
- let min = all_color_values[0];
144
- let max = all_color_values[0];
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
- if (val < min)
147
- min = val;
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(all_points.map((pt) => pt.size_value).filter((val) => val != null));
154
- let color_scale_fn = $derived(create_color_scale(color_scale, auto_color_range));
155
- let size_scale_fn = $derived(create_size_scale(size_scale, all_size_values));
156
- // Process points with normalized positions
157
- // Swap Y/Z for Three.js: user Z → Three.js Y (vertical), user Y → Three.js Z (depth)
158
- let processed_points = $derived(all_points.map((pt) => ({
159
- ...pt,
160
- x: normalize_x(pt.x), // user X → Three.js X
161
- y: normalize_z(pt.z), // user Z → Three.js Y (vertical)
162
- z: normalize_y(pt.y), // user Y → Three.js Z (depth)
163
- })));
164
- let radius_groups = $derived.by(() => {
165
- const groups = {};
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
- const srs = series[pt.series_idx];
168
- if (!(series_visibility[pt.series_idx] ?? srs?.visible ?? true))
169
- continue;
170
- const color = pt.color_value != null
171
- ? color_scale_fn(pt.color_value)
172
- : pt.point_style?.fill ?? get_series_color(pt.series_idx);
173
- const radius = pt.size_value != null
174
- ? size_scale_fn(pt.size_value)
175
- : (pt.point_style?.radius ?? styles.point?.size ?? 2) * 0.05;
176
- const key = radius.toFixed(4);
177
- (groups[key] ??= { radius, points: [], colors: [] }).points.push(pt);
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
- // Projection settings - render point shadows on background planes
183
- let proj_opacity = $derived(display.projection_opacity ?? 0.3);
184
- let proj_scale = $derived(display.projection_scale ?? 0.5);
185
- let projection_configs = $derived([`xy`, `xz`, `yz`]
186
- .filter((key) => display.projections?.[key])
187
- .map((key) => ({
188
- key,
189
- get_pos: key === `xy`
190
- ? (pt) => [pt.x, pos.y, pt.z]
191
- : key === `xz`
192
- ? (pt) => [pt.x, pt.y, pos.z]
193
- : (pt) => [pos.x, pt.y, pt.z],
194
- })));
195
- // Track previous lines for cleanup
196
- let series_lines = $state([]);
197
- $effect(() => {
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
- line_data.geometry.dispose();
201
- line_data.material.dispose();
350
+ line_data.geometry.dispose()
351
+ line_data.material.dispose()
202
352
  }
203
- const lines = [];
353
+
354
+ const lines: SeriesLineData[] = []
204
355
  for (let series_idx = 0; series_idx < series.length; series_idx++) {
205
- const srs = series[series_idx];
206
- if (!srs?.line_style)
207
- continue;
208
- if (!(series_visibility[series_idx] ?? srs.visible ?? true))
209
- continue;
210
- // Get points for this series in order
211
- const series_points = processed_points
212
- .filter((pt) => pt.series_idx === series_idx)
213
- .sort((a, b) => a.point_idx - b.point_idx);
214
- if (series_points.length < 2)
215
- continue;
216
- // Create fat line geometry (LineGeometry for Line2)
217
- const positions = [];
218
- for (const pt of series_points) {
219
- positions.push(pt.x, pt.y, pt.z);
220
- }
221
- const geometry = new LineGeometry();
222
- geometry.setPositions(positions);
223
- // Determine line style
224
- const line_style = srs.line_style;
225
- const color = line_style.stroke ??
226
- (Array.isArray(srs.point_style)
227
- ? srs.point_style[0]?.fill
228
- : srs.point_style?.fill) ??
229
- get_series_color(series_idx);
230
- const line_width = line_style.stroke_width ?? 2;
231
- const dashed = Boolean(line_style.line_dash);
232
- // Create LineMaterial for fat lines (linewidth is in pixels when resolution is set)
233
- // Use placeholder resolution; the separate resolution effect updates it
234
- const material = new LineMaterial({
235
- color: new THREE.Color(color).getHex(),
236
- linewidth: line_width, // Width in pixels
237
- dashed,
238
- dashScale: dashed ? 2 : 1,
239
- dashSize: 0.1,
240
- gapSize: 0.05,
241
- resolution: new THREE.Vector2(1, 1),
242
- });
243
- const line2 = new Line2(geometry, material);
244
- line2.computeLineDistances();
245
- lines.push({
246
- series_idx,
247
- color,
248
- width: line_width,
249
- dashed,
250
- line2,
251
- geometry,
252
- material,
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
- // Update LineMaterial resolution when canvas size changes
258
- $effect(() => {
259
- const canvas_width = width || 1;
260
- const canvas_height = height || 1;
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
- line_data.material.resolution.set(canvas_width, canvas_height);
418
+ line_data.material.resolution.set(canvas_width, canvas_height)
263
419
  }
264
- });
265
- // Cleanup on component destroy
266
- onDestroy(() => {
420
+ })
421
+
422
+ // Cleanup on component destroy
423
+ onDestroy(() => {
267
424
  for (const { geometry, material } of series_lines) {
268
- geometry.dispose();
269
- material.dispose();
425
+ geometry.dispose()
426
+ material.dispose()
270
427
  }
271
- Object.values(axis_geometries).forEach((g) => g.dispose());
428
+ Object.values(axis_geometries).forEach((geom) => geom.dispose())
272
429
  for (const data of Object.values(axis_geom_data)) {
273
- data.tick_geoms.forEach((g) => g.dispose());
274
- data.grid_geoms.flat().forEach((g) => g.dispose());
430
+ data.tick_geoms.forEach((geom) => geom.dispose())
431
+ data.grid_geoms.flat().forEach((geom) => geom.dispose())
275
432
  }
276
- });
277
- // Generate axis ticks using D3's smart tick generation
278
- function gen_ticks(range, ticks) {
279
- if (Array.isArray(ticks))
280
- return ticks;
281
- const [min, max] = range;
282
- if (!isFinite(min) || !isFinite(max) || min === max)
283
- return [min];
284
- const count = typeof ticks === `number` ? ticks : 5;
285
- return scaleLinear().domain([min, max]).ticks(count);
286
- }
287
- let x_ticks = $derived(gen_ticks(x_range, x_axis.ticks));
288
- let y_ticks = $derived(gen_ticks(y_range, y_axis.ticks));
289
- let z_ticks = $derived(gen_ticks(z_range, z_axis.ticks));
290
- // Create axis line geometry - reuses a shared Float32Array for efficiency
291
- function create_line_geometry(start, end) {
292
- const geometry = new THREE.BufferGeometry();
293
- const positions = new Float32Array([...start, ...end]);
294
- geometry.setAttribute(`position`, new THREE.BufferAttribute(positions, 3));
295
- return geometry;
296
- }
297
- // Build event data for point interactions
298
- function make_event_data(point, event) {
299
- const orig = all_points.find((pt) => pt.series_idx === point.series_idx && pt.point_idx === point.point_idx);
300
- if (!orig)
301
- return null;
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
- x: orig.x,
304
- y: orig.y,
305
- z: orig.z,
306
- metadata: point.metadata ?? null,
307
- label: series[point.series_idx]?.label ?? null,
308
- series_idx: point.series_idx,
309
- x_axis,
310
- y_axis,
311
- z_axis,
312
- x_formatted: format_num(orig.x, x_axis.format || `.3~g`),
313
- y_formatted: format_num(orig.y, y_axis.format || `.3~g`),
314
- z_formatted: format_num(orig.z, z_axis.format || `.3~g`),
315
- color_value: point.color_value,
316
- fullscreen: false,
317
- event,
318
- point,
319
- };
320
- }
321
- function handle_point_enter(point) {
322
- hovered_point = point;
323
- const data = make_event_data(point);
324
- if (data)
325
- on_point_hover?.(data);
326
- }
327
- function handle_point_click(point, event) {
328
- const data = make_event_data(point, event);
329
- if (data)
330
- on_point_click?.(data);
331
- }
332
- // Gizmo props - parent (ScatterPlot3D) handles className and ColorBar offset adjustments
333
- let gizmo_props = $derived(gizmo === false
334
- ? null
335
- : gizmo === true
336
- ? { background: { enabled: false }, offset: { left: 5, bottom: 5 } }
337
- : gizmo);
338
- // Orbit controls - snappy with minimal inertia
339
- let orbit_controls_props = $derived({
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
- // Axis configuration for rendering
355
- const tick_length = 0.15;
356
- const AXIS_KEYS = [`x`, `y`, `z`];
357
- // Main axis line geometries - updated when backside positions change
358
- let axis_geometries = $state({
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
- $effect(() => {
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
- for (const key of AXIS_KEYS)
368
- axis_geometries[key].dispose();
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
- // Axis rendering config - all positions use backside `pos` values
378
- let axes_config = $derived([
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
- key: `x`,
381
- color: `#ef4444`,
382
- axis: x_axis,
383
- ticks: x_ticks,
384
- range: x_range,
385
- get_tick_pos: (val) => [normalize_x(val), pos.y, pos.z],
386
- get_tick_end: (val) => [normalize_x(val), pos.y + sign_y * tick_length, pos.z],
387
- get_grid_lines: (val) => {
388
- const px = normalize_x(val);
389
- return [
390
- [[px, -half_z, pos.z], [px, half_z, pos.z]],
391
- [[px, pos.y, -half_y], [px, pos.y, half_y]],
392
- ];
393
- },
394
- tick_label_pos: (val) => [normalize_x(val), pos.y + sign_y * 0.4, pos.z],
395
- axis_label_pos: [0, pos.y + sign_y * 0.9, pos.z],
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
- key: `y`,
399
- color: `#22c55e`,
400
- axis: y_axis,
401
- ticks: y_ticks,
402
- range: y_range,
403
- get_tick_pos: (val) => [pos.x, pos.y, normalize_y(val)],
404
- get_tick_end: (val) => [pos.x, pos.y + sign_y * tick_length, normalize_y(val)],
405
- get_grid_lines: (val) => {
406
- const py = normalize_y(val);
407
- return [
408
- [[-half_x, pos.y, py], [half_x, pos.y, py]],
409
- [[pos.x, -half_z, py], [pos.x, half_z, py]],
410
- ];
411
- },
412
- tick_label_pos: (val) => [pos.x + sign_x * 0.5, pos.y + sign_y * 0.4, normalize_y(val)],
413
- axis_label_pos: [
414
- pos.x,
415
- pos.y + sign_y * 0.9,
416
- pos.z < 0 ? half_y + 0.5 : -half_y - 0.5,
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
- key: `z`,
421
- color: `#3b82f6`,
422
- axis: z_axis,
423
- ticks: z_ticks,
424
- range: z_range,
425
- get_tick_pos: (val) => [pos.x, normalize_z(val), pos.z],
426
- get_tick_end: (val) => [pos.x + sign_x * tick_length, normalize_z(val), pos.z],
427
- get_grid_lines: (val) => {
428
- const pz = normalize_z(val);
429
- return [
430
- [[-half_x, pz, pos.z], [half_x, pz, pos.z]],
431
- [[pos.x, pz, -half_y], [pos.x, pz, half_y]],
432
- ];
433
- },
434
- tick_label_pos: (val) => [pos.x + sign_x * 0.5, normalize_z(val), pos.z],
435
- axis_label_pos: [pos.x + sign_x, 0, pos.z],
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
- const empty_geom_data = () => ({ tick_geoms: [], grid_geoms: [] });
439
- let axis_geom_data = $state({
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
- // Recreate tick/grid geometries when axes config changes
445
- $effect(() => {
446
- const config = axes_config;
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
- for (const key of AXIS_KEYS) {
450
- axis_geom_data[key].tick_geoms.forEach((geom) => geom.dispose());
451
- axis_geom_data[key].grid_geoms.flat().forEach((geom) => geom.dispose());
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
- axis_geom_data[key] = {
456
- tick_geoms: ticks.map((v) => create_line_geometry(get_tick_pos(v), get_tick_end(v))),
457
- grid_geoms: ticks.map((v) => get_grid_lines(v).map(([s, e]) => create_line_geometry(s, e))),
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 range={group.points.length} frustumCulled={false}>
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 range={group.points.length} frustumCulled={false}>
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}`)}