matterviz 0.4.1 → 0.4.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 (499) hide show
  1. package/dist/EmptyState.svelte +10 -4
  2. package/dist/FilePicker.svelte +48 -60
  3. package/dist/Icon.svelte +7 -4
  4. package/dist/MillerIndexInput.svelte +2 -2
  5. package/dist/api/optimade.js +10 -7
  6. package/dist/app.css +68 -0
  7. package/dist/brillouin/BrillouinZone.svelte +90 -91
  8. package/dist/brillouin/BrillouinZone.svelte.d.ts +5 -7
  9. package/dist/brillouin/BrillouinZoneControls.svelte +3 -6
  10. package/dist/brillouin/BrillouinZoneExportPane.svelte +18 -14
  11. package/dist/brillouin/BrillouinZoneInfoPane.svelte +6 -10
  12. package/dist/brillouin/BrillouinZoneScene.svelte +46 -68
  13. package/dist/brillouin/BrillouinZoneScene.svelte.d.ts +1 -1
  14. package/dist/brillouin/compute.js +10 -16
  15. package/dist/brillouin/geometry.js +2 -2
  16. package/dist/chempot-diagram/ChemPotDiagram.svelte +48 -50
  17. package/dist/chempot-diagram/ChemPotDiagram2D.svelte +61 -169
  18. package/dist/chempot-diagram/ChemPotDiagram2D.svelte.d.ts +1 -1
  19. package/dist/chempot-diagram/ChemPotDiagram3D.svelte +559 -755
  20. package/dist/chempot-diagram/ChemPotDiagram3D.svelte.d.ts +1 -1
  21. package/dist/chempot-diagram/async-compute.svelte.js +13 -14
  22. package/dist/chempot-diagram/color.d.ts +21 -1
  23. package/dist/chempot-diagram/color.js +70 -1
  24. package/dist/chempot-diagram/compute.d.ts +7 -0
  25. package/dist/chempot-diagram/compute.js +85 -26
  26. package/dist/chempot-diagram/controls-state.svelte.js +1 -1
  27. package/dist/chempot-diagram/export.js +1 -6
  28. package/dist/chempot-diagram/temperature.js +9 -10
  29. package/dist/colors/index.d.ts +0 -4
  30. package/dist/colors/index.js +19 -20
  31. package/dist/composition/BarChart.svelte +30 -26
  32. package/dist/composition/BubbleChart.svelte +17 -17
  33. package/dist/composition/Composition.svelte +10 -19
  34. package/dist/composition/Formula.svelte +20 -27
  35. package/dist/composition/FormulaFilter.svelte +115 -184
  36. package/dist/composition/PieChart.svelte +55 -41
  37. package/dist/composition/format.js +7 -9
  38. package/dist/composition/index.d.ts +2 -0
  39. package/dist/composition/index.js +14 -0
  40. package/dist/composition/parse.d.ts +2 -2
  41. package/dist/composition/parse.js +61 -115
  42. package/dist/constants.d.ts +0 -1
  43. package/dist/constants.js +17 -26
  44. package/dist/convex-hull/ConvexHull.svelte +12 -15
  45. package/dist/convex-hull/ConvexHull.svelte.d.ts +1 -1
  46. package/dist/convex-hull/ConvexHull2D.svelte +93 -94
  47. package/dist/convex-hull/ConvexHull2D.svelte.d.ts +1 -1
  48. package/dist/convex-hull/ConvexHull3D.svelte +127 -186
  49. package/dist/convex-hull/ConvexHull3D.svelte.d.ts +1 -1
  50. package/dist/convex-hull/ConvexHull4D.svelte +131 -180
  51. package/dist/convex-hull/ConvexHull4D.svelte.d.ts +1 -1
  52. package/dist/convex-hull/ConvexHullChrome.svelte +36 -40
  53. package/dist/convex-hull/ConvexHullControls.svelte +46 -48
  54. package/dist/convex-hull/ConvexHullStats.svelte +193 -195
  55. package/dist/convex-hull/ConvexHullTooltip.svelte +5 -12
  56. package/dist/convex-hull/GasPressureControls.svelte +5 -12
  57. package/dist/convex-hull/StructurePopup.svelte +18 -22
  58. package/dist/convex-hull/StructurePopup.svelte.d.ts +2 -0
  59. package/dist/convex-hull/TemperatureSlider.svelte +4 -10
  60. package/dist/convex-hull/canvas-interactions.svelte.js +2 -1
  61. package/dist/convex-hull/gas-thermodynamics.js +21 -53
  62. package/dist/convex-hull/helpers.d.ts +8 -10
  63. package/dist/convex-hull/helpers.js +37 -43
  64. package/dist/convex-hull/hull-state.svelte.d.ts +1 -1
  65. package/dist/convex-hull/thermodynamics.js +37 -44
  66. package/dist/coordination/CoordinationBarPlot.svelte +49 -53
  67. package/dist/coordination/CoordinationBarPlot.svelte.d.ts +1 -1
  68. package/dist/coordination/calc-coordination.d.ts +1 -1
  69. package/dist/coordination/calc-coordination.js +28 -25
  70. package/dist/element/BohrAtom.svelte +5 -9
  71. package/dist/element/ElementHeading.svelte +4 -1
  72. package/dist/element/ElementPhoto.svelte +11 -9
  73. package/dist/element/ElementStats.svelte +12 -10
  74. package/dist/element/ElementTile.svelte +44 -67
  75. package/dist/element/Nucleus.svelte +4 -2
  76. package/dist/element/data.d.ts +3 -2
  77. package/dist/element/data.js +5 -1
  78. package/dist/element/helpers.d.ts +4 -0
  79. package/dist/element/helpers.js +18 -0
  80. package/dist/element/index.d.ts +2 -5
  81. package/dist/element/index.js +4 -19
  82. package/dist/feedback/DragOverlay.svelte +5 -5
  83. package/dist/feedback/DragOverlay.svelte.d.ts +2 -2
  84. package/dist/feedback/StatusMessage.svelte +7 -4
  85. package/dist/fermi-surface/FermiSlice.svelte +21 -18
  86. package/dist/fermi-surface/FermiSurface.svelte +35 -44
  87. package/dist/fermi-surface/FermiSurface.svelte.d.ts +7 -13
  88. package/dist/fermi-surface/FermiSurfaceControls.svelte +6 -20
  89. package/dist/fermi-surface/FermiSurfaceControls.svelte.d.ts +1 -1
  90. package/dist/fermi-surface/FermiSurfaceScene.svelte +64 -136
  91. package/dist/fermi-surface/FermiSurfaceScene.svelte.d.ts +1 -1
  92. package/dist/fermi-surface/FermiSurfaceTooltip.svelte +9 -6
  93. package/dist/fermi-surface/compute.js +25 -58
  94. package/dist/fermi-surface/parse.js +54 -88
  95. package/dist/file-viewer/JsonBrowser.svelte +1043 -0
  96. package/dist/file-viewer/JsonBrowser.svelte.d.ts +9 -0
  97. package/dist/file-viewer/PlotPanel.svelte +350 -0
  98. package/dist/file-viewer/PlotPanel.svelte.d.ts +9 -0
  99. package/dist/file-viewer/TrajectoryWithDos.svelte +30 -0
  100. package/dist/file-viewer/TrajectoryWithDos.svelte.d.ts +10 -0
  101. package/dist/file-viewer/detect.d.ts +11 -0
  102. package/dist/file-viewer/detect.js +381 -0
  103. package/dist/file-viewer/eligibility.d.ts +4 -0
  104. package/dist/file-viewer/eligibility.js +53 -0
  105. package/dist/file-viewer/host-protocol.d.ts +50 -0
  106. package/dist/file-viewer/host-transfer.d.ts +25 -0
  107. package/dist/file-viewer/host-transfer.js +42 -0
  108. package/dist/file-viewer/index.d.ts +5 -0
  109. package/dist/file-viewer/index.js +5 -0
  110. package/dist/file-viewer/main.d.ts +44 -0
  111. package/dist/file-viewer/main.js +535 -0
  112. package/dist/file-viewer/parse.d.ts +9 -0
  113. package/dist/file-viewer/parse.js +170 -0
  114. package/dist/file-viewer/plot-utils.d.ts +24 -0
  115. package/dist/file-viewer/plot-utils.js +191 -0
  116. package/dist/file-viewer/types.d.ts +4 -0
  117. package/dist/file-viewer/types.js +5 -0
  118. package/dist/heatmap-matrix/HeatmapMatrix.svelte +153 -219
  119. package/dist/heatmap-matrix/HeatmapMatrix.svelte.d.ts +2 -2
  120. package/dist/heatmap-matrix/HeatmapMatrixControls.svelte +9 -7
  121. package/dist/heatmap-matrix/HeatmapMatrixControls.svelte.d.ts +1 -1
  122. package/dist/heatmap-matrix/index.d.ts +5 -0
  123. package/dist/icons.d.ts +4 -0
  124. package/dist/icons.js +4 -0
  125. package/dist/index.d.ts +2 -2
  126. package/dist/index.js +1 -1
  127. package/dist/io/ExportPane.svelte +11 -7
  128. package/dist/io/decompress.d.ts +4 -1
  129. package/dist/io/decompress.js +24 -36
  130. package/dist/io/export.d.ts +11 -4
  131. package/dist/io/export.js +75 -19
  132. package/dist/io/fetch.d.ts +0 -3
  133. package/dist/io/fetch.js +0 -11
  134. package/dist/io/file-drop.js +60 -35
  135. package/dist/io/index.d.ts +0 -1
  136. package/dist/io/index.js +2 -9
  137. package/dist/io/is-binary.d.ts +9 -0
  138. package/dist/io/is-binary.js +45 -1
  139. package/dist/io/url-drop.d.ts +2 -0
  140. package/dist/io/url-drop.js +40 -40
  141. package/dist/isosurface/Isosurface.svelte +700 -215
  142. package/dist/isosurface/Isosurface.svelte.d.ts +7 -1
  143. package/dist/isosurface/IsosurfaceControls.svelte +590 -116
  144. package/dist/isosurface/IsosurfaceControls.svelte.d.ts +1 -1
  145. package/dist/isosurface/VolumeSlice.svelte +224 -0
  146. package/dist/isosurface/VolumeSlice.svelte.d.ts +28 -0
  147. package/dist/isosurface/coloring.d.ts +20 -0
  148. package/dist/isosurface/coloring.js +118 -0
  149. package/dist/isosurface/geometry-worker-types.d.ts +38 -0
  150. package/dist/isosurface/geometry.worker.d.ts +1 -0
  151. package/dist/isosurface/geometry.worker.js +59 -0
  152. package/dist/isosurface/grid.d.ts +10 -0
  153. package/dist/isosurface/grid.js +31 -0
  154. package/dist/isosurface/index.d.ts +4 -0
  155. package/dist/isosurface/index.js +4 -0
  156. package/dist/isosurface/parse-vaspwave.d.ts +3 -0
  157. package/dist/isosurface/parse-vaspwave.js +138 -0
  158. package/dist/isosurface/parse.js +6 -6
  159. package/dist/isosurface/profile.d.ts +12 -0
  160. package/dist/isosurface/profile.js +19 -0
  161. package/dist/isosurface/sampling.d.ts +34 -0
  162. package/dist/isosurface/sampling.js +408 -0
  163. package/dist/isosurface/slice-rendering.d.ts +13 -0
  164. package/dist/isosurface/slice-rendering.js +79 -0
  165. package/dist/isosurface/slice.d.ts +22 -2
  166. package/dist/isosurface/slice.js +200 -116
  167. package/dist/isosurface/types.d.ts +24 -1
  168. package/dist/isosurface/types.js +120 -8
  169. package/dist/json-path.d.ts +4 -0
  170. package/dist/json-path.js +89 -0
  171. package/dist/labels.d.ts +6 -2
  172. package/dist/labels.js +44 -14
  173. package/dist/layout/FullscreenButton.svelte +2 -1
  174. package/dist/layout/FullscreenToggle.svelte +10 -4
  175. package/dist/layout/InfoCard.svelte +8 -21
  176. package/dist/layout/InfoTag.svelte +1 -3
  177. package/dist/layout/NumberRangeInput.svelte +46 -0
  178. package/dist/layout/NumberRangeInput.svelte.d.ts +12 -0
  179. package/dist/layout/PropertyFilter.svelte +2 -6
  180. package/dist/layout/SettingsSection.svelte +57 -62
  181. package/dist/layout/SettingsSection.svelte.d.ts +6 -7
  182. package/dist/layout/SubpageGrid.svelte +3 -1
  183. package/dist/layout/ViewerChrome.svelte +16 -11
  184. package/dist/layout/ViewerChrome.svelte.d.ts +2 -1
  185. package/dist/layout/fullscreen.js +3 -5
  186. package/dist/layout/fullscreen.svelte.js +1 -2
  187. package/dist/layout/index.d.ts +1 -0
  188. package/dist/layout/index.js +1 -0
  189. package/dist/layout/json-tree/JsonNode.svelte +22 -59
  190. package/dist/layout/json-tree/JsonTree.svelte +101 -172
  191. package/dist/layout/json-tree/JsonValue.svelte +9 -32
  192. package/dist/layout/json-tree/utils.d.ts +0 -3
  193. package/dist/layout/json-tree/utils.js +50 -188
  194. package/dist/marching-cubes.d.ts +8 -1
  195. package/dist/marching-cubes.js +115 -48
  196. package/dist/math.d.ts +5 -2
  197. package/dist/math.js +48 -38
  198. package/dist/overlays/ContextMenu.svelte +19 -34
  199. package/dist/overlays/CopyButton.svelte +11 -5
  200. package/dist/overlays/DragControlTab.svelte +6 -1
  201. package/dist/overlays/DraggablePane.svelte +110 -74
  202. package/dist/overlays/DraggablePane.svelte.d.ts +2 -1
  203. package/dist/overlays/GlassChip.svelte +32 -0
  204. package/dist/overlays/GlassChip.svelte.d.ts +8 -0
  205. package/dist/overlays/InfoPaneCards.svelte +11 -23
  206. package/dist/overlays/index.d.ts +7 -0
  207. package/dist/overlays/index.js +26 -0
  208. package/dist/overlays/portal.d.ts +2 -0
  209. package/dist/overlays/portal.js +14 -0
  210. package/dist/periodic-table/PeriodicTable.svelte +113 -145
  211. package/dist/periodic-table/PeriodicTable.svelte.d.ts +5 -3
  212. package/dist/periodic-table/PeriodicTableControls.svelte +37 -115
  213. package/dist/periodic-table/TableInset.svelte +9 -4
  214. package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte +68 -105
  215. package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte.d.ts +6 -8
  216. package/dist/phase-diagram/PhaseDiagramControls.svelte +70 -94
  217. package/dist/phase-diagram/PhaseDiagramControls.svelte.d.ts +1 -1
  218. package/dist/phase-diagram/PhaseDiagramEditorPane.svelte +2 -1
  219. package/dist/phase-diagram/PhaseDiagramExportPane.svelte +12 -12
  220. package/dist/phase-diagram/PhaseDiagramTooltip.svelte +54 -58
  221. package/dist/phase-diagram/TdbInfoPanel.svelte +21 -15
  222. package/dist/phase-diagram/build-diagram.js +4 -9
  223. package/dist/phase-diagram/diagram-input.d.ts +1 -2
  224. package/dist/phase-diagram/parse.js +15 -15
  225. package/dist/phase-diagram/svg-to-diagram.js +28 -27
  226. package/dist/phase-diagram/utils.js +2 -3
  227. package/dist/plot/bar/BarPlot.svelte +461 -426
  228. package/dist/plot/bar/BarPlot.svelte.d.ts +2 -0
  229. package/dist/plot/bar/BarPlotControls.svelte +1 -3
  230. package/dist/plot/bar/BarPlotControls.svelte.d.ts +4 -6
  231. package/dist/plot/bar/SpacegroupBarPlot.svelte +46 -77
  232. package/dist/plot/bar/data.d.ts +5 -4
  233. package/dist/plot/bar/data.js +2 -1
  234. package/dist/plot/box/BoxPlot.svelte +322 -226
  235. package/dist/plot/box/BoxPlot.svelte.d.ts +2 -0
  236. package/dist/plot/box/Violin.svelte.d.ts +1 -0
  237. package/dist/plot/box/box-plot.js +1 -1
  238. package/dist/plot/core/auto-place.d.ts +1 -1
  239. package/dist/plot/core/auto-place.js +2 -2
  240. package/dist/plot/core/axis-utils.d.ts +12 -7
  241. package/dist/plot/core/axis-utils.js +48 -48
  242. package/dist/plot/core/components/AxisLabel.svelte +82 -20
  243. package/dist/plot/core/components/AxisLabel.svelte.d.ts +1 -0
  244. package/dist/plot/core/components/ColorBar.svelte +101 -106
  245. package/dist/plot/core/components/ColorScaleSelect.svelte +1 -1
  246. package/dist/plot/core/components/FillArea.svelte +10 -17
  247. package/dist/plot/core/components/HierarchyControls.svelte +249 -0
  248. package/dist/plot/{sunburst/SunburstControls.svelte.d.ts → core/components/HierarchyControls.svelte.d.ts} +12 -8
  249. package/dist/plot/core/components/InteractiveAxisLabel.svelte +1 -1
  250. package/dist/plot/core/components/InteractiveAxisLabel.svelte.d.ts +1 -1
  251. package/dist/plot/core/components/Line.svelte +18 -9
  252. package/dist/plot/core/components/Line.svelte.d.ts +2 -0
  253. package/dist/plot/core/components/PlotAxis.svelte +40 -17
  254. package/dist/plot/core/components/PlotControls.svelte +65 -41
  255. package/dist/plot/core/components/PlotLegend.svelte +31 -35
  256. package/dist/plot/core/components/PlotMarginals.svelte +616 -0
  257. package/dist/plot/core/components/PlotMarginals.svelte.d.ts +15 -0
  258. package/dist/plot/core/components/PlotTooltip.svelte +1 -1
  259. package/dist/plot/core/components/PortalSelect.svelte +4 -8
  260. package/dist/plot/core/components/ReferenceLine.svelte +27 -24
  261. package/dist/plot/core/components/ReferenceLine3D.svelte +54 -57
  262. package/dist/plot/core/components/ReferencePlane.svelte +7 -8
  263. package/dist/plot/core/components/ZeroLines.svelte +4 -8
  264. package/dist/plot/core/components/ZoomRect.svelte +1 -2
  265. package/dist/plot/core/components/index.d.ts +1 -0
  266. package/dist/plot/core/components/index.js +1 -0
  267. package/dist/plot/core/data-cleaning-signal.d.ts +46 -0
  268. package/dist/plot/core/data-cleaning-signal.js +467 -0
  269. package/dist/plot/core/data-cleaning.d.ts +2 -42
  270. package/dist/plot/core/data-cleaning.js +27 -474
  271. package/dist/plot/core/data-transform.d.ts +1 -2
  272. package/dist/plot/core/data-transform.js +9 -15
  273. package/dist/plot/core/fill-utils.d.ts +3 -1
  274. package/dist/plot/core/fill-utils.js +28 -6
  275. package/dist/plot/core/index.d.ts +1 -0
  276. package/dist/plot/core/index.js +1 -0
  277. package/dist/plot/core/interactions.js +0 -2
  278. package/dist/plot/core/layout.d.ts +15 -6
  279. package/dist/plot/core/layout.js +57 -38
  280. package/dist/plot/core/marginals.d.ts +170 -0
  281. package/dist/plot/core/marginals.js +436 -0
  282. package/dist/plot/core/pan-zoom.svelte.d.ts +2 -4
  283. package/dist/plot/core/pan-zoom.svelte.js +34 -17
  284. package/dist/plot/core/placed-tween.svelte.d.ts +3 -0
  285. package/dist/plot/core/placed-tween.svelte.js +68 -21
  286. package/dist/plot/core/reference-line.js +1 -1
  287. package/dist/plot/core/scales.d.ts +23 -0
  288. package/dist/plot/core/scales.js +39 -13
  289. package/dist/plot/core/svg.js +0 -1
  290. package/dist/plot/core/types/fills.d.ts +101 -0
  291. package/dist/plot/core/types/fills.js +15 -0
  292. package/dist/plot/core/types/plot-3d.d.ts +87 -0
  293. package/dist/plot/core/types/plot-3d.js +4 -0
  294. package/dist/plot/core/types/reference-lines.d.ts +140 -0
  295. package/dist/plot/core/types/reference-lines.js +9 -0
  296. package/dist/plot/core/types.d.ts +29 -324
  297. package/dist/plot/core/types.js +23 -20
  298. package/dist/plot/core/utils/hierarchy-chart.d.ts +52 -0
  299. package/dist/plot/core/utils/hierarchy-chart.js +191 -0
  300. package/dist/plot/core/utils/hierarchy-labels.d.ts +20 -0
  301. package/dist/plot/core/utils/hierarchy-labels.js +100 -0
  302. package/dist/plot/core/utils/label-placement.js +2 -2
  303. package/dist/plot/histogram/Histogram.svelte +314 -340
  304. package/dist/plot/histogram/Histogram.svelte.d.ts +7 -7
  305. package/dist/plot/histogram/HistogramControls.svelte +30 -28
  306. package/dist/plot/histogram/histogram.d.ts +31 -0
  307. package/dist/plot/histogram/histogram.js +70 -0
  308. package/dist/plot/histogram/index.d.ts +1 -0
  309. package/dist/plot/histogram/index.js +1 -0
  310. package/dist/plot/index.d.ts +1 -0
  311. package/dist/plot/index.js +1 -0
  312. package/dist/plot/sankey/Sankey.svelte +78 -69
  313. package/dist/plot/sankey/Sankey.svelte.d.ts +2 -2
  314. package/dist/plot/sankey/SankeyControls.svelte +22 -16
  315. package/dist/plot/sankey/sankey.js +13 -9
  316. package/dist/plot/scatter/BinnedScatterPlot.svelte +311 -141
  317. package/dist/plot/scatter/BinnedScatterPlot.svelte.d.ts +7 -0
  318. package/dist/plot/scatter/ElementScatter.svelte +4 -1
  319. package/dist/plot/scatter/ScatterPlot.svelte +517 -410
  320. package/dist/plot/scatter/ScatterPlot.svelte.d.ts +2 -0
  321. package/dist/plot/scatter/ScatterPlotControls.svelte +18 -19
  322. package/dist/plot/scatter/ScatterPlotControls.svelte.d.ts +1 -1
  323. package/dist/plot/scatter/ScatterPoint.svelte +16 -9
  324. package/dist/plot/scatter/adaptive-density.d.ts +1 -1
  325. package/dist/plot/scatter/adaptive-density.js +36 -20
  326. package/dist/plot/scatter/binned-scatter-types.d.ts +3 -2
  327. package/dist/plot/scatter/binned-scatter-types.js +6 -1
  328. package/dist/plot/scatter/scatter-data.d.ts +1 -1
  329. package/dist/plot/scatter/scatter-data.js +8 -7
  330. package/dist/plot/scatter-3d/ScatterPlot3D.svelte +25 -34
  331. package/dist/plot/scatter-3d/ScatterPlot3D.svelte.d.ts +2 -2
  332. package/dist/plot/scatter-3d/ScatterPlot3DControls.svelte +8 -5
  333. package/dist/plot/scatter-3d/ScatterPlot3DScene.svelte +113 -116
  334. package/dist/plot/scatter-3d/ScatterPlot3DScene.svelte.d.ts +2 -2
  335. package/dist/plot/scatter-3d/Surface3D.svelte +4 -13
  336. package/dist/plot/sunburst/Sunburst.svelte +269 -347
  337. package/dist/plot/sunburst/Sunburst.svelte.d.ts +2 -2
  338. package/dist/plot/sunburst/index.d.ts +1 -1
  339. package/dist/plot/sunburst/index.js +2 -1
  340. package/dist/plot/sunburst/render.d.ts +2 -2
  341. package/dist/plot/sunburst/render.js +47 -22
  342. package/dist/plot/sunburst/sunburst.d.ts +5 -1
  343. package/dist/plot/sunburst/sunburst.js +10 -3
  344. package/dist/plot/treemap/Treemap.svelte +1022 -0
  345. package/dist/plot/treemap/Treemap.svelte.d.ts +90 -0
  346. package/dist/plot/treemap/index.d.ts +4 -0
  347. package/dist/plot/treemap/index.js +4 -0
  348. package/dist/plot/treemap/labels.d.ts +38 -0
  349. package/dist/plot/treemap/labels.js +80 -0
  350. package/dist/plot/treemap/treemap.d.ts +28 -0
  351. package/dist/plot/treemap/treemap.js +102 -0
  352. package/dist/rdf/RdfPlot.svelte +37 -59
  353. package/dist/rdf/RdfPlot.svelte.d.ts +1 -1
  354. package/dist/rdf/calc-rdf.js +63 -52
  355. package/dist/rdf/index.d.ts +0 -1
  356. package/dist/sanitize.js +85 -27
  357. package/dist/scene/SceneCamera.svelte +1 -1
  358. package/dist/scene/SceneCamera.svelte.d.ts +1 -1
  359. package/dist/scene/bind-renderer.svelte.js +14 -6
  360. package/dist/scene/index.d.ts +1 -2
  361. package/dist/scene/index.js +1 -1
  362. package/dist/scene/{props.js → props.svelte.js} +15 -1
  363. package/dist/settings.d.ts +19 -9
  364. package/dist/settings.js +229 -525
  365. package/dist/spectral/Bands.svelte +207 -221
  366. package/dist/spectral/BandsAndDos.svelte +11 -9
  367. package/dist/spectral/BrillouinBandsDos.svelte +19 -42
  368. package/dist/spectral/Dos.svelte +92 -93
  369. package/dist/spectral/helpers.d.ts +3 -3
  370. package/dist/spectral/helpers.js +65 -140
  371. package/dist/state.svelte.js +5 -18
  372. package/dist/structure/Arrow.svelte +5 -18
  373. package/dist/structure/ArrowInstances.svelte +160 -0
  374. package/dist/structure/ArrowInstances.svelte.d.ts +16 -0
  375. package/dist/structure/AtomLegend.svelte +120 -85
  376. package/dist/structure/AtomLegend.svelte.d.ts +2 -3
  377. package/dist/structure/Bond.svelte +31 -23
  378. package/dist/structure/CanvasTooltip.svelte +15 -3
  379. package/dist/structure/CellSelect.svelte +45 -35
  380. package/dist/structure/Cylinder.svelte +24 -40
  381. package/dist/structure/InstancedAtoms.svelte +114 -0
  382. package/dist/structure/InstancedAtoms.svelte.d.ts +15 -0
  383. package/dist/structure/Lattice.svelte +85 -149
  384. package/dist/structure/SiteLabels.svelte +122 -0
  385. package/dist/structure/SiteLabels.svelte.d.ts +18 -0
  386. package/dist/structure/Structure.svelte +636 -504
  387. package/dist/structure/Structure.svelte.d.ts +4 -2
  388. package/dist/structure/StructureCarousel.svelte +637 -0
  389. package/dist/structure/StructureCarousel.svelte.d.ts +19 -0
  390. package/dist/structure/StructureControls.svelte +165 -425
  391. package/dist/structure/StructureControls.svelte.d.ts +1 -1
  392. package/dist/structure/StructureExportPane.svelte +17 -21
  393. package/dist/structure/StructureInfoPane.svelte +128 -133
  394. package/dist/structure/StructureInfoPane.svelte.d.ts +1 -1
  395. package/dist/structure/StructureScene.svelte +535 -578
  396. package/dist/structure/StructureScene.svelte.d.ts +6 -2
  397. package/dist/structure/StructureViewport.svelte +451 -0
  398. package/dist/structure/StructureViewport.svelte.d.ts +65 -0
  399. package/dist/structure/atom-label-placement.d.ts +16 -12
  400. package/dist/structure/atom-label-placement.js +70 -37
  401. package/dist/structure/atom-properties.d.ts +2 -0
  402. package/dist/structure/atom-properties.js +105 -38
  403. package/dist/structure/bond-order-perception.js +3 -8
  404. package/dist/structure/bonding.d.ts +9 -3
  405. package/dist/structure/bonding.js +95 -46
  406. package/dist/structure/edit-history.d.ts +6 -0
  407. package/dist/structure/edit-history.js +27 -0
  408. package/dist/structure/export.d.ts +1 -0
  409. package/dist/structure/export.js +89 -105
  410. package/dist/structure/format-detect.d.ts +1 -0
  411. package/dist/structure/format-detect.js +20 -0
  412. package/dist/structure/geometry.d.ts +9 -0
  413. package/dist/structure/geometry.js +27 -0
  414. package/dist/structure/index.d.ts +16 -1
  415. package/dist/structure/index.js +15 -10
  416. package/dist/structure/measure.d.ts +1 -1
  417. package/dist/structure/measure.js +6 -12
  418. package/dist/structure/parse.d.ts +2 -1
  419. package/dist/structure/parse.js +272 -313
  420. package/dist/structure/partial-occupancy.js +10 -5
  421. package/dist/structure/pbc.js +85 -107
  422. package/dist/structure/polyhedra.d.ts +0 -1
  423. package/dist/structure/polyhedra.js +7 -21
  424. package/dist/structure/prop-groups.d.ts +85 -0
  425. package/dist/structure/prop-groups.js +18 -0
  426. package/dist/structure/supercell.d.ts +1 -2
  427. package/dist/structure/supercell.js +7 -13
  428. package/dist/symmetry/SymmetryElements.svelte +11 -15
  429. package/dist/symmetry/SymmetryStats.svelte +55 -71
  430. package/dist/symmetry/WyckoffTable.svelte +10 -28
  431. package/dist/symmetry/cell-transform.js +6 -14
  432. package/dist/symmetry/index.d.ts +1 -0
  433. package/dist/symmetry/index.js +13 -10
  434. package/dist/symmetry/spacegroups.js +3 -2
  435. package/dist/table/HeatmapTable.svelte +1142 -447
  436. package/dist/table/HeatmapTable.svelte.d.ts +12 -2
  437. package/dist/table/ToggleMenu.svelte +168 -53
  438. package/dist/table/ToggleMenu.svelte.d.ts +1 -1
  439. package/dist/table/index.d.ts +21 -6
  440. package/dist/table/index.js +34 -21
  441. package/dist/theme/ThemeControl.svelte +16 -8
  442. package/dist/theme/embedded.d.ts +18 -0
  443. package/dist/theme/embedded.js +206 -0
  444. package/dist/theme/index.d.ts +5 -5
  445. package/dist/theme/index.js +5 -12
  446. package/dist/theme/themes.mjs +44 -66
  447. package/dist/tooltip/KCoords.svelte +4 -1
  448. package/dist/tooltip/TooltipContent.svelte +3 -6
  449. package/dist/tooltip/TooltipContent.svelte.d.ts +1 -1
  450. package/dist/tooltip/index.d.ts +8 -1
  451. package/dist/trajectory/Trajectory.svelte +613 -386
  452. package/dist/trajectory/Trajectory.svelte.d.ts +1 -1
  453. package/dist/trajectory/TrajectoryError.svelte +12 -74
  454. package/dist/trajectory/TrajectoryExportPane.svelte +14 -32
  455. package/dist/trajectory/TrajectoryInfoPane.svelte +127 -164
  456. package/dist/trajectory/TrajectoryInfoPane.svelte.d.ts +2 -1
  457. package/dist/trajectory/extract.js +61 -49
  458. package/dist/trajectory/format-detect.d.ts +2 -0
  459. package/dist/trajectory/format-detect.js +17 -13
  460. package/dist/trajectory/frame-reader.d.ts +2 -0
  461. package/dist/trajectory/frame-reader.js +35 -32
  462. package/dist/trajectory/helpers.d.ts +2 -0
  463. package/dist/trajectory/helpers.js +34 -56
  464. package/dist/trajectory/index.d.ts +1 -0
  465. package/dist/trajectory/index.js +5 -8
  466. package/dist/trajectory/parse/ase.js +1 -1
  467. package/dist/trajectory/parse/h5-utils.d.ts +13 -0
  468. package/dist/trajectory/parse/h5-utils.js +109 -0
  469. package/dist/trajectory/parse/hdf5.d.ts +1 -1
  470. package/dist/trajectory/parse/hdf5.js +116 -106
  471. package/dist/trajectory/parse/index.d.ts +6 -6
  472. package/dist/trajectory/parse/index.js +25 -33
  473. package/dist/trajectory/parse/lammps.js +8 -12
  474. package/dist/trajectory/parse/vasp.js +10 -7
  475. package/dist/trajectory/parse/vaspout-electronic.d.ts +13 -0
  476. package/dist/trajectory/parse/vaspout-electronic.js +207 -0
  477. package/dist/trajectory/parse/vaspout-h5.d.ts +4 -0
  478. package/dist/trajectory/parse/vaspout-h5.js +281 -0
  479. package/dist/trajectory/parse/xyz.js +59 -9
  480. package/dist/trajectory/plotting.d.ts +8 -9
  481. package/dist/trajectory/plotting.js +113 -64
  482. package/dist/utils.d.ts +3 -0
  483. package/dist/utils.js +7 -0
  484. package/dist/xrd/XrdPlot.svelte +79 -118
  485. package/dist/xrd/broadening.js +2 -1
  486. package/dist/xrd/calc-xrd.js +24 -62
  487. package/dist/xrd/parse.d.ts +0 -1
  488. package/dist/xrd/parse.js +69 -95
  489. package/package.json +94 -34
  490. package/readme.md +6 -5
  491. package/dist/api/mp.d.ts +0 -6
  492. package/dist/api/mp.js +0 -22
  493. package/dist/plot/sunburst/SunburstControls.svelte +0 -200
  494. package/dist/scene/types.d.ts +0 -26
  495. package/dist/tooltip/types.d.ts +0 -8
  496. package/dist/trajectory/constants.d.ts +0 -6
  497. package/dist/trajectory/constants.js +0 -11
  498. /package/dist/{scene/types.js → file-viewer/host-protocol.js} +0 -0
  499. /package/dist/{tooltip/types.js → isosurface/geometry-worker-types.js} +0 -0
@@ -5,9 +5,9 @@
5
5
  import { StatusMessage } from '../feedback'
6
6
  import Spinner from '../feedback/Spinner.svelte'
7
7
  import Icon from '../Icon.svelte'
8
- import { drag_over_handlers, handle_url_drop, load_from_url } from '../io'
8
+ import * as io from '../io'
9
9
  import { forward_window_keydown, handle_and_prevent } from '../keyboard'
10
- import { format_num, trajectory_property_config } from '../labels'
10
+ import { format_num, trajectory_property_config, type TrajPropertyConfig } from '../labels'
11
11
  import type { Vec2 } from '../math'
12
12
  import { sanitize_html } from '../sanitize'
13
13
  import { FullscreenButton, type FullscreenToggleProp, toggle_fullscreen } from '../layout'
@@ -17,21 +17,29 @@
17
17
  import { Histogram, ScatterPlot } from '../plot'
18
18
  import { toggle_series_visibility } from '../plot/core/utils/series-visibility'
19
19
  import { DEFAULTS } from '../settings'
20
+ import type { AnyStructure } from '../structure'
20
21
  import Structure from '../structure/Structure.svelte'
21
22
  import { scaleLinear } from 'd3-scale'
22
23
  import type { ComponentProps, Snippet } from 'svelte'
23
- import { untrack } from 'svelte'
24
+ import { onMount, untrack } from 'svelte'
24
25
  import { tooltip } from 'svelte-multiselect/attachments'
25
26
  import type { HTMLAttributes } from 'svelte/elements'
27
+ import { SvelteSet } from 'svelte/reactivity'
26
28
  import { full_data_extractor } from './extract'
27
29
  import type {
28
30
  ParseProgress,
29
31
  TrajectoryDataExtractor,
30
32
  TrajectoryFrame,
33
+ TrajectoryMetadata,
31
34
  TrajectoryType,
32
35
  TrajHandlerData,
33
36
  } from './index'
34
- import { TrajectoryError, TrajectoryExportPane, TrajectoryInfoPane } from './index'
37
+ import {
38
+ FRAME_LOAD_DEBOUNCE_MS,
39
+ TrajectoryError,
40
+ TrajectoryExportPane,
41
+ TrajectoryInfoPane,
42
+ } from './index'
35
43
  import type { AtomTypeMapping, LoadingOptions } from './parse'
36
44
  import {
37
45
  get_unsupported_format_message,
@@ -41,6 +49,7 @@
41
49
  } from './parse'
42
50
  import {
43
51
  generate_axis_labels,
52
+ generate_axis_scale_types,
44
53
  generate_plot_series,
45
54
  generate_streaming_plot_series,
46
55
  should_hide_plot,
@@ -64,6 +73,12 @@
64
73
  total_frames: number
65
74
  on_step_change: (idx: number) => void
66
75
  }
76
+ type PlotMetadataStreamMessage = {
77
+ command?: string
78
+ file_path?: string
79
+ plot_metadata?: TrajectoryMetadata[]
80
+ is_complete?: boolean
81
+ }
67
82
 
68
83
  let {
69
84
  trajectory = $bindable(),
@@ -106,85 +121,86 @@
106
121
  wrapper = $bindable(),
107
122
  fullscreen = $bindable(false),
108
123
  ...rest
109
- }: EventHandlers & HTMLAttributes<HTMLDivElement> & {
110
- // trajectory data - can be provided directly or loaded from file
111
- trajectory?: TrajectoryType
112
- // URL to load trajectory from (alternative to providing trajectory directly)
113
- data_url?: string
114
- // current step index being displayed
115
- current_step_idx?: number
116
- // custom function to extract plot data from trajectory frames
117
- data_extractor?: TrajectoryDataExtractor
118
-
119
- // file drop handlers
120
- allow_file_drop?: boolean
121
- // layout configuration - 'auto' (default) adapts to element size, 'horizontal'/'vertical' forces layout
122
- layout?: `auto` | Orientation
123
- // structure viewer props (passed to Structure component)
124
- structure_props?: ComponentProps<typeof Structure>
125
- // plot props (passed to ScatterPlot component)
126
- scatter_props?: ComponentProps<typeof ScatterPlot>
127
- // histogram props (passed to Histogram component, excluding series which is handled separately)
128
- histogram_props?: Omit<ComponentProps<typeof Histogram>, `series`>
129
- // spinner props (passed to Spinner component)
130
- spinner_props?: ComponentProps<typeof Spinner>
131
- // custom snippets for additional UI elements
132
- trajectory_controls?: Snippet<[ControlsProps]>
133
- // Custom error snippet for advanced error handling
134
- error_snippet?: Snippet<[{ error_msg: string; on_dismiss: () => void }]>
135
- // Controls visibility configuration.
136
- // - 'always': controls always visible
137
- // - 'hover': controls visible on component hover (default)
138
- // - 'never': controls never visible
139
- // - object: { mode, hidden, style } for fine-grained control
140
- // Control names: 'filename', 'nav', 'step', 'fps', 'info-pane', 'export-pane', 'view-mode', 'fullscreen'
141
- show_controls?: ShowControlsProp
142
- // show/hide the fullscreen button
143
- fullscreen_toggle?: FullscreenToggleProp
144
- // automatically start playing when trajectory data is loaded
145
- auto_play?: boolean
146
- // display mode: 'structure+scatter' (default), 'structure' (only structure), 'scatter' (only scatter), 'histogram' (only histogram), 'structure+histogram' (structure with histogram)
147
- display_mode?:
148
- | `structure+scatter`
149
- | `structure`
150
- | `scatter`
151
- | `histogram`
152
- | `structure+histogram`
153
- // step labels configuration for slider
154
- // - positive number: number of evenly spaced ticks
155
- // - negative number: spacing between ticks (e.g. -10 = every 10th step)
156
- // - array: exact step indices to label
157
- // - undefined: no labels
158
- step_labels?: number | number[]
159
- // visible properties - bindable array of property keys currently shown in the plot
160
- // - controls which trajectory properties are plotted (e.g. ['energy', 'volume', 'force_max'])
161
- // - bindable: reflects current visibility state and can be used for external control
162
- // - if not provided, uses default visible properties (energy, force_max, stress_frobenius)
163
- // - if specified properties don't exist in data, falls back to automatic selection
164
- visible_properties?: string[]
165
- // custom labels for trajectory properties - maps property keys to display labels
166
- // - e.g. {energy: 'Total Energy', volume: 'Cell Volume', force_max: 'Max Force'}
167
- // - merged with built-in trajectory_property_config
168
- ELEM_PROPERTY_LABELS?: Record<string, string>
169
- fps_range?: Vec2 // allowed FPS range [min_fps, max_fps]
170
- fps?: number // frame rate for playback
171
- // Loading options for large files
172
- loading_options?: LoadingOptions
173
- // Map LAMMPS atom types to element symbols (e.g. {1: 'Na', 2: 'Cl'})
174
- atom_type_mapping?: AtomTypeMapping
175
- // Disable plot skimming (mouse over plot doesn't update structure/step slider)
176
- plot_skimming?: boolean
177
- // bindable: true while the pointer is over the viewer (drives hover-scoped shortcuts)
178
- hovered?: boolean
179
- // bindable: whether the (structure) controls pane is currently open
180
- controls_open?: boolean
181
- // bindable: whether the trajectory info pane is currently open
182
- info_pane_open?: boolean
183
- // bindable: top-level wrapper element
184
- wrapper?: HTMLDivElement
185
- // bindable: fullscreen state
186
- fullscreen?: boolean
187
- } = $props()
124
+ }: EventHandlers &
125
+ HTMLAttributes<HTMLDivElement> & {
126
+ // trajectory data - can be provided directly or loaded from file
127
+ trajectory?: TrajectoryType
128
+ // URL to load trajectory from (alternative to providing trajectory directly)
129
+ data_url?: string
130
+ // current step index being displayed
131
+ current_step_idx?: number
132
+ // custom function to extract plot data from trajectory frames
133
+ data_extractor?: TrajectoryDataExtractor
134
+
135
+ // file drop handlers
136
+ allow_file_drop?: boolean
137
+ // layout configuration - 'auto' (default) adapts to element size, 'horizontal'/'vertical' forces layout
138
+ layout?: `auto` | Orientation
139
+ // structure viewer props (passed to Structure component)
140
+ structure_props?: ComponentProps<typeof Structure>
141
+ // plot props (passed to ScatterPlot component)
142
+ scatter_props?: ComponentProps<typeof ScatterPlot>
143
+ // histogram props (passed to Histogram component, excluding series which is handled separately)
144
+ histogram_props?: Omit<ComponentProps<typeof Histogram>, `series`>
145
+ // spinner props (passed to Spinner component)
146
+ spinner_props?: ComponentProps<typeof Spinner>
147
+ // custom snippets for additional UI elements
148
+ trajectory_controls?: Snippet<[ControlsProps]>
149
+ // Custom error snippet for advanced error handling
150
+ error_snippet?: Snippet<[{ error_msg: string; on_dismiss: () => void }]>
151
+ // Controls visibility configuration.
152
+ // - 'always': controls always visible
153
+ // - 'hover': controls visible on component hover (default)
154
+ // - 'never': controls never visible
155
+ // - object: { mode, hidden, style } for fine-grained control
156
+ // Control names: 'filename', 'nav', 'step', 'fps', 'info-pane', 'export-pane', 'view-mode', 'fullscreen'
157
+ show_controls?: ShowControlsProp
158
+ // show/hide the fullscreen button
159
+ fullscreen_toggle?: FullscreenToggleProp
160
+ // automatically start playing when trajectory data is loaded
161
+ auto_play?: boolean
162
+ // display mode: 'structure+scatter' (default), 'structure' (only structure), 'scatter' (only scatter), 'histogram' (only histogram), 'structure+histogram' (structure with histogram)
163
+ display_mode?:
164
+ | `structure+scatter`
165
+ | `structure`
166
+ | `scatter`
167
+ | `histogram`
168
+ | `structure+histogram`
169
+ // step labels configuration for slider
170
+ // - positive number: number of evenly spaced ticks
171
+ // - negative number: spacing between ticks (e.g. -10 = every 10th step)
172
+ // - array: exact step indices to label
173
+ // - undefined: no labels
174
+ step_labels?: number | number[]
175
+ // visible properties - bindable array of property keys currently shown in the plot
176
+ // - controls which trajectory properties are plotted (e.g. ['energy', 'volume', 'force_max'])
177
+ // - bindable: reflects current visibility state and can be used for external control
178
+ // - if not provided, uses default visible properties (energy, force_max, stress_frobenius)
179
+ // - if specified properties don't exist in data, falls back to automatic selection
180
+ visible_properties?: string[]
181
+ // custom labels for trajectory properties - maps property keys to display labels
182
+ // - e.g. {energy: 'Total Energy', volume: 'Cell Volume', force_max: 'Max Force'}
183
+ // - merged with built-in trajectory_property_config
184
+ ELEM_PROPERTY_LABELS?: Record<string, string>
185
+ fps_range?: Vec2 // allowed FPS range [min_fps, max_fps]
186
+ fps?: number // frame rate for playback
187
+ // Loading options for large files
188
+ loading_options?: LoadingOptions
189
+ // Map LAMMPS atom types to element symbols (e.g. {1: 'Na', 2: 'Cl'})
190
+ atom_type_mapping?: AtomTypeMapping
191
+ // Disable plot skimming (mouse over plot doesn't update structure/step slider)
192
+ plot_skimming?: boolean
193
+ // bindable: true while the pointer is over the viewer (drives hover-scoped shortcuts)
194
+ hovered?: boolean
195
+ // bindable: whether the (structure) controls pane is currently open
196
+ controls_open?: boolean
197
+ // bindable: whether the trajectory info pane is currently open
198
+ info_pane_open?: boolean
199
+ // bindable: top-level wrapper element
200
+ wrapper?: HTMLDivElement
201
+ // bindable: fullscreen state
202
+ fullscreen?: boolean
203
+ } = $props()
188
204
 
189
205
  let dragover = $state(false)
190
206
  let loading = $state(false)
@@ -193,22 +209,22 @@
193
209
  let parse_warning_msg = $state<string | undefined>(undefined)
194
210
  $effect(() => {
195
211
  const warnings = trajectory?.metadata?.parse_warnings
196
- parse_warning_msg = Array.isArray(warnings) && warnings.length > 0
197
- ? `${warnings.length} parse warning${warnings.length > 1 ? `s` : ``}: ${
198
- warnings.join(`; `)
199
- }`
200
- : undefined
212
+ parse_warning_msg =
213
+ Array.isArray(warnings) && warnings.length > 0
214
+ ? `${warnings.length} parse warning${warnings.length > 1 ? `s` : ``}: ${warnings.join(
215
+ `; `,
216
+ )}`
217
+ : undefined
201
218
  })
202
219
  let is_playing = $state(false)
203
- let play_interval: ReturnType<typeof setInterval> | undefined = $state(undefined)
220
+ // requestAnimationFrame handle for the playback loop (rAF auto-pauses in background tabs and
221
+ // self-throttles, unlike setInterval which drifts and queues work when a frame render overruns)
222
+ let play_raf: number | undefined
204
223
 
205
224
  // Ensure fps is within the allowed range
206
225
  $effect(() => {
207
- if (fps < fps_range[0]) {
208
- fps = fps_range[0]
209
- } else if (fps > fps_range[1]) {
210
- fps = fps_range[1]
211
- }
226
+ const clamped = Math.max(fps_range[0], Math.min(fps_range[1], fps))
227
+ if (clamped !== fps) fps = clamped
212
228
  })
213
229
  let current_filename = $state<string | undefined>(undefined)
214
230
  let current_file_path = $state<string | null>(null)
@@ -218,6 +234,9 @@
218
234
  let element_size = $state({ width: 0, height: 0 })
219
235
  let filename_copied = $state(false)
220
236
  let orig_data = $state<string | ArrayBuffer | null>(null)
237
+ let data_url_load_id = 0
238
+ let loaded_data_url: string | undefined
239
+ let url_owned_trajectory: TrajectoryType | undefined
221
240
 
222
241
  let controls_config = $derived(normalize_show_controls(show_controls))
223
242
 
@@ -232,13 +251,26 @@
232
251
  })
233
252
 
234
253
  // Get total frame count (supports both regular and indexed trajectories)
235
- let total_frames = $derived(
236
- trajectory?.total_frames || trajectory?.frames.length || 0,
237
- )
254
+ let total_frames = $derived(trajectory?.total_frames || trajectory?.frames.length || 0)
255
+
256
+ // Clamp out-of-range step indices (e.g. an initial current_step_idx of
257
+ // Number.MAX_SAFE_INTEGER means "jump to the last frame", used by hosts
258
+ // restoring viewer position across trajectory reloads). Hosts are notified
259
+ // of the correction so their recorded position matches the shown frame.
260
+ $effect(() => {
261
+ if (total_frames > 0 && current_step_idx > total_frames - 1) {
262
+ current_step_idx = total_frames - 1
263
+ notify_step_change()
264
+ } else if (current_step_idx < 0) {
265
+ current_step_idx = 0
266
+ notify_step_change()
267
+ }
268
+ })
238
269
 
239
270
  // Current frame - load on demand for indexed trajectories
240
271
  let current_frame = $state<TrajectoryFrame | null>(null)
241
272
  let frame_load_request_id = 0
273
+ let frame_load_timeout: ReturnType<typeof setTimeout> | undefined
242
274
 
243
275
  // Auto-play when trajectory changes (handles both props and file loading)
244
276
  $effect(() => {
@@ -252,8 +284,10 @@
252
284
  if (trajectory && current_step_idx >= 0 && current_step_idx < total_frames) {
253
285
  if (trajectory.frame_loader) {
254
286
  // Load frame on demand (works for both indexed files and external streaming)
255
- load_frame_on_demand(current_step_idx)
287
+ current_frame = null
288
+ schedule_frame_load_on_demand(current_step_idx)
256
289
  } else {
290
+ clear_frame_load_timeout()
257
291
  // Use in-memory frame for regular trajectories
258
292
  current_frame = trajectory.frames[current_step_idx] || null
259
293
  }
@@ -262,11 +296,175 @@
262
296
  }
263
297
  })
264
298
 
299
+ // LRU cache of decoded frames (keyed to the current trajectory) so scrub/playback over
300
+ // indexed/streaming trajectories avoids re-reading frames and prefetch can warm upcoming ones.
301
+ // Capped by frame count AND a total-atom budget (cache many tiny frames or few huge ones).
302
+ const FRAME_CACHE_MAX = 64
303
+ const FRAME_CACHE_MAX_ATOMS = 200_000
304
+ let frame_cache = new Map<number, TrajectoryFrame>()
305
+ let frame_cache_owner: TrajectoryType | undefined
306
+ // Frames currently being read (direct or prefetch) so we don't kick off duplicate loads
307
+ const inflight_frames = new Set<number>()
308
+ let streaming_file_path = $derived(
309
+ trajectory?.metadata?.streaming_file_path as string | undefined,
310
+ )
311
+ let plot_metadata_loading = $derived(trajectory?.metadata?.plot_metadata_loading === true)
312
+
313
+ const clear_frame_load_timeout = () => {
314
+ if (frame_load_timeout) clearTimeout(frame_load_timeout)
315
+ frame_load_timeout = undefined
316
+ }
317
+
318
+ const skip_stale_url_stream = () =>
319
+ Boolean(data_url && loaded_data_url && data_url !== loaded_data_url)
320
+
321
+ // Replace the trajectory with an updated copy, keeping URL ownership if it applied.
322
+ // No-ops while a data_url switch is in flight so stale streams can't mutate the old model.
323
+ const update_trajectory = (updates: Partial<TrajectoryType>) => {
324
+ if (!trajectory || skip_stale_url_stream()) return
325
+ const preserves_url_ownership = trajectory === url_owned_trajectory
326
+ trajectory = { ...trajectory, ...updates }
327
+ if (preserves_url_ownership) url_owned_trajectory = trajectory
328
+ }
329
+
330
+ const merge_plot_metadata = (batch: TrajectoryMetadata[]) => {
331
+ if (batch.length === 0) return
332
+ update_trajectory({ plot_metadata: [...(trajectory?.plot_metadata ?? []), ...batch] })
333
+ }
334
+
335
+ const finish_plot_metadata_loading = () =>
336
+ update_trajectory({
337
+ metadata: { ...trajectory?.metadata, plot_metadata_loading: false },
338
+ })
339
+
340
+ onMount(() => {
341
+ const handle_plot_metadata_stream = (event: MessageEvent<PlotMetadataStreamMessage>) => {
342
+ // Global listener: other code posts arbitrary messages (including null data)
343
+ if (typeof event.data !== `object` || event.data === null) return
344
+ const { command, file_path, is_complete, plot_metadata } = event.data
345
+ if (command !== `plot_metadata_stream` || file_path !== streaming_file_path) return
346
+ if (Array.isArray(plot_metadata)) merge_plot_metadata(plot_metadata)
347
+ if (is_complete) finish_plot_metadata_loading()
348
+ }
349
+ globalThis.addEventListener(`message`, handle_plot_metadata_stream)
350
+ return () => {
351
+ globalThis.removeEventListener(`message`, handle_plot_metadata_stream)
352
+ clear_frame_load_timeout()
353
+ }
354
+ })
355
+
356
+ // Reset per-trajectory caches when the trajectory changes (frames belong to the old one)
357
+ function ensure_frame_cache_owner() {
358
+ if (frame_cache_owner !== trajectory) {
359
+ frame_cache = new Map()
360
+ inflight_frames.clear()
361
+ frame_cache_owner = trajectory
362
+ }
363
+ }
364
+ // Sync LRU read (delete + re-insert refreshes recency); undefined on miss
365
+ function cache_get(frame_idx: number): TrajectoryFrame | undefined {
366
+ const hit = frame_cache.get(frame_idx)
367
+ if (!hit) return undefined
368
+ frame_cache.delete(frame_idx)
369
+ frame_cache.set(frame_idx, hit)
370
+ return hit
371
+ }
372
+ // Sync LRU write, evicting oldest entries until under both the frame and atom budgets
373
+ function cache_put(frame_idx: number, frame: TrajectoryFrame) {
374
+ frame_cache.set(frame_idx, frame)
375
+ let total_atoms = 0
376
+ for (const cached of frame_cache.values())
377
+ total_atoms += cached.structure?.sites?.length ?? 0
378
+ while (
379
+ frame_cache.size > 1 &&
380
+ (frame_cache.size > FRAME_CACHE_MAX || total_atoms > FRAME_CACHE_MAX_ATOMS)
381
+ ) {
382
+ const oldest = frame_cache.keys().next().value
383
+ if (oldest === undefined) break
384
+ total_atoms -= frame_cache.get(oldest)?.structure?.sites?.length ?? 0
385
+ frame_cache.delete(oldest)
386
+ }
387
+ }
388
+
389
+ // Warm the next couple of frames in the background (fire-and-forget) so playback/forward-scrub
390
+ // doesn't stall on a serial per-frame read; skips frames already cached or in flight
391
+ function prefetch_frames(from_idx: number) {
392
+ const frame_loader = trajectory?.frame_loader
393
+ if (!frame_loader) return
394
+ if (trajectory?.indexed_frames && trajectory.indexed_frames.length < total_frames) return
395
+ const owner = trajectory
396
+ for (const ahead of [1, 2]) {
397
+ const idx = from_idx + ahead
398
+ if (idx >= total_frames || frame_cache.has(idx) || inflight_frames.has(idx)) continue
399
+ inflight_frames.add(idx)
400
+ frame_loader
401
+ .load_frame(orig_data || ``, idx)
402
+ .then((frame) => {
403
+ if (frame && frame_cache_owner === owner) cache_put(idx, frame)
404
+ })
405
+ .catch(() => {})
406
+ .finally(() => inflight_frames.delete(idx))
407
+ }
408
+ }
409
+
410
+ function use_cached_or_in_memory_frame(
411
+ load_trajectory: TrajectoryType,
412
+ frame_idx: number,
413
+ ): boolean {
414
+ const cached = cache_get(frame_idx)
415
+ if (cached) {
416
+ current_frame = cached
417
+ prefetch_frames(frame_idx)
418
+ return true
419
+ }
420
+
421
+ const in_memory_frame = load_trajectory.frames[frame_idx]
422
+ if (in_memory_frame) {
423
+ cache_put(frame_idx, in_memory_frame)
424
+ current_frame = in_memory_frame
425
+ prefetch_frames(frame_idx)
426
+ return true
427
+ }
428
+ return false
429
+ }
430
+
431
+ function schedule_frame_load_on_demand(frame_idx: number) {
432
+ const load_trajectory = trajectory
433
+ if (!load_trajectory?.frame_loader) return
434
+ ensure_frame_cache_owner()
435
+ frame_load_request_id++
436
+
437
+ if (use_cached_or_in_memory_frame(load_trajectory, frame_idx)) return
438
+
439
+ clear_frame_load_timeout()
440
+ // Debouncing during playback would keep resetting the timer at fps above
441
+ // ~1000/FRAME_LOAD_DEBOUNCE_MS and stall uncached streamed frames entirely.
442
+ // untrack: this runs synchronously inside the step-change $effect, so tracking
443
+ // is_playing would re-run that effect on every play/pause toggle and kick off
444
+ // duplicate loads for a frame that's already in flight.
445
+ if (untrack(() => is_playing)) {
446
+ void load_frame_on_demand(frame_idx)
447
+ return
448
+ }
449
+ const request_id = frame_load_request_id
450
+ frame_load_timeout = setTimeout(() => {
451
+ frame_load_timeout = undefined
452
+ if (
453
+ request_id === frame_load_request_id &&
454
+ trajectory === load_trajectory &&
455
+ current_step_idx === frame_idx
456
+ ) {
457
+ load_frame_on_demand(frame_idx)
458
+ }
459
+ }, FRAME_LOAD_DEBOUNCE_MS)
460
+ }
461
+
265
462
  // Load frame on demand - works for both indexed files and external streaming
266
463
  async function load_frame_on_demand(frame_idx: number) {
267
464
  const load_trajectory = trajectory
268
465
  const frame_loader = load_trajectory?.frame_loader
269
466
  if (!load_trajectory || !frame_loader) return
467
+ ensure_frame_cache_owner()
270
468
 
271
469
  const request_id = ++frame_load_request_id
272
470
  const request_is_current = () =>
@@ -274,13 +472,18 @@
274
472
  trajectory === load_trajectory &&
275
473
  current_step_idx === frame_idx
276
474
 
475
+ if (use_cached_or_in_memory_frame(load_trajectory, frame_idx)) return
476
+ inflight_frames.add(frame_idx) // let concurrent prefetch skip the frame we're already loading
277
477
  try {
278
478
  const frame = await frame_loader.load_frame(
279
- orig_data || ``, // Use original_data for indexed files, empty string for external streaming
479
+ orig_data || ``, // original_data for indexed files, empty string for external streaming
280
480
  frame_idx,
281
481
  )
482
+ // cache the decoded frame even if it arrived stale (still valid data for that index)
483
+ if (frame && frame_cache_owner === load_trajectory) cache_put(frame_idx, frame)
282
484
  if (!request_is_current()) return
283
485
  current_frame = frame
486
+ prefetch_frames(frame_idx) // warm upcoming frames for smooth playback/scrub
284
487
  } catch (error) {
285
488
  if (!request_is_current()) return
286
489
  console.error(`Failed to load frame ${frame_idx}:`, error)
@@ -292,14 +495,22 @@
292
495
  step_idx: frame_idx,
293
496
  frame_count: total_frames,
294
497
  })
498
+ } finally {
499
+ inflight_frames.delete(frame_idx)
295
500
  }
296
501
  }
297
502
 
298
- // Current frame structure for display
299
- let current_structure = $derived(current_frame?.structure)
503
+ // Current frame structure for display. Holds the last resolved structure so the 3D
504
+ // view doesn't blank while an uncached frame loads on demand (current_frame is nulled
505
+ // during loads to keep the info pane from showing the previous frame's data).
506
+ let current_structure = $state<AnyStructure | undefined>(undefined)
507
+ $effect(() => {
508
+ if (current_frame?.structure) current_structure = current_frame.structure
509
+ else if (!trajectory) current_structure = undefined
510
+ })
300
511
 
301
512
  // Track hidden elements (persists across frame changes)
302
- let hidden_elements = $state(new Set<ElementSymbol>())
513
+ let hidden_elements = $state(new SvelteSet<ElementSymbol>())
303
514
 
304
515
  let step_label_positions = $derived.by((): number[] => {
305
516
  if (!step_labels || total_frames <= 1) return []
@@ -310,11 +521,14 @@
310
521
 
311
522
  if (typeof step_labels === `number`) {
312
523
  if (step_labels > 0) {
313
- return scaleLinear().domain([0, total_frames - 1]).nice()
524
+ return scaleLinear()
525
+ .domain([0, total_frames - 1])
526
+ .nice()
314
527
  .ticks(Math.min(step_labels, total_frames))
315
528
  .map((tick) => Math.round(tick))
316
- .filter((tick, idx, ticks) =>
317
- tick >= 0 && tick < total_frames && ticks.indexOf(tick) === idx
529
+ .filter(
530
+ (tick, idx, ticks) =>
531
+ tick >= 0 && tick < total_frames && ticks.indexOf(tick) === idx,
318
532
  )
319
533
  }
320
534
  if (step_labels < 0) {
@@ -335,11 +549,13 @@
335
549
  let extended_config = $derived.by(() => {
336
550
  if (!ELEM_PROPERTY_LABELS) return trajectory_property_config
337
551
 
338
- const custom_config: Record<string, { label: string; unit: string }> = {}
552
+ const custom_config: Record<string, TrajPropertyConfig> = {}
339
553
  for (const [key, label] of Object.entries(ELEM_PROPERTY_LABELS)) {
340
- const existing = trajectory_property_config[key] ||
341
- trajectory_property_config[key.toLowerCase()]
342
- custom_config[key] = { label, unit: existing?.unit || `` }
554
+ const existing =
555
+ trajectory_property_config[key] || trajectory_property_config[key.toLowerCase()]
556
+ // Spread the existing config so fields like axis_group survive the
557
+ // label override (losing axis_group would break dedicated-axis grouping)
558
+ custom_config[key] = { ...existing, label, unit: existing?.unit || `` }
343
559
  }
344
560
  return { ...trajectory_property_config, ...custom_config }
345
561
  })
@@ -392,7 +608,8 @@
392
608
 
393
609
  // Only update if changed (use untrack to avoid circular dependency)
394
610
  const current = untrack(() => visible_properties) || []
395
- const has_changed = visible_keys.length !== current.length ||
611
+ const has_changed =
612
+ visible_keys.length !== current.length ||
396
613
  !visible_keys.every((key, idx) => key === current[idx])
397
614
 
398
615
  if (has_changed) {
@@ -407,82 +624,74 @@
407
624
  plot_series = toggle_series_visibility(plot_series, series_idx)
408
625
  }
409
626
 
627
+ // Streamed trajectories plot sampled per-frame metadata, so x values are frame numbers
628
+ let x_axis_quantity = $derived(trajectory?.plot_metadata ? `Frame` : `Step`)
410
629
  let x_axis = $derived({
411
- label: `Step`,
412
- format: `.3~s`,
630
+ label: x_axis_quantity,
631
+ // ~g (not ~s) so sub-1 values read as 0.8 rather than SI "800m"; integer steps stay clean
632
+ format: `~g`,
413
633
  ticks: step_label_positions,
414
634
  })
415
635
  // Generate axis labels based on first visible series on each axis
416
636
  let y_axis_labels = $derived(generate_axis_labels(plot_series))
637
+ // Axes whose visible series are all-positive and span many decades (e.g. SCF
638
+ // convergence residuals) default to log scale instead of a linear hockey stick
639
+ let y_axis_scale_types = $derived(generate_axis_scale_types(plot_series))
417
640
  let y_axis = $derived({
418
641
  label: y_axis_labels.y1,
419
- format: `.2~s`,
642
+ format: `~g`,
420
643
  label_shift: { y: 10 },
644
+ scale_type: y_axis_scale_types.y1,
421
645
  })
422
646
  let y2_axis = $derived({
423
647
  label: y_axis_labels.y2,
424
- format: `.2~s`,
648
+ format: `~g`,
425
649
  label_shift: { y: 80 },
650
+ scale_type: y_axis_scale_types.y2,
426
651
  })
427
-
428
652
  // hide plot if all plotted values are constant (no variation)
429
653
  let show_plot = $derived(
430
- display_mode !== `structure` && !should_hide_plot(trajectory, plot_series),
654
+ display_mode !== `structure` &&
655
+ (plot_metadata_loading || !should_hide_plot(trajectory, plot_series)),
431
656
  )
432
-
433
- // Determine what to show based on display mode
434
657
  let show_structure = $derived(![`scatter`, `histogram`].includes(display_mode))
435
- let actual_show_plot = $derived(display_mode !== `structure` && show_plot)
436
658
 
437
659
  // Check if there are any Y2 series to determine padding
438
- let has_y2_series = $derived(
439
- plot_series.some((srs) => srs.y_axis === `y2` && srs.visible),
440
- )
441
-
442
- // Step navigation functions
660
+ let has_y2_series = $derived(plot_series.some((srs) => srs.y_axis === `y2` && srs.visible))
661
+ // Report the current step to on_step_change consumers. Also wired to the step
662
+ // slider/number input whose bind:value bypasses the navigation functions
663
+ // below — those handlers pass the event target's value explicitly because
664
+ // bind:value may not have written the binding yet when oninput fires.
665
+ function notify_step_change(step_idx: number = current_step_idx) {
666
+ if (!trajectory || !Number.isFinite(step_idx)) return
667
+ const last_frame = Math.max(total_frames - 1, 0)
668
+ const clamped_step = Math.min(Math.max(Math.round(step_idx), 0), last_frame)
669
+ on_step_change?.({
670
+ trajectory,
671
+ step_idx: clamped_step,
672
+ frame_count: total_frames,
673
+ frame: current_frame || undefined,
674
+ })
675
+ }
676
+ // Step navigation functions (streaming frame loading is handled by the reactive effect)
443
677
  function next_step() {
444
678
  if (current_step_idx < total_frames - 1) {
445
679
  current_step_idx++
446
- // Streaming frame loading handled by reactive effect
447
- if (trajectory) {
448
- on_step_change?.({
449
- trajectory,
450
- step_idx: current_step_idx,
451
- frame_count: total_frames,
452
- frame: current_frame || undefined,
453
- })
454
- }
680
+ notify_step_change()
455
681
  }
456
682
  }
457
683
 
458
684
  function prev_step() {
459
685
  if (current_step_idx > 0) {
460
686
  current_step_idx--
461
- // Streaming frame loading handled by reactive effect
462
- if (trajectory) {
463
- on_step_change?.({
464
- trajectory,
465
- step_idx: current_step_idx,
466
- frame_count: total_frames,
467
- frame: current_frame || undefined,
468
- })
469
- }
687
+ notify_step_change()
470
688
  }
471
689
  }
472
690
 
473
691
  function go_to_step(idx: number) {
474
692
  if (idx >= 0 && idx < total_frames) {
475
693
  current_step_idx = idx
476
- // Note: streaming frame loading is handled by reactive effect
477
- // Handle callbacks for both traditional and streaming modes
478
- if (trajectory) {
479
- on_step_change?.({
480
- trajectory,
481
- step_idx: current_step_idx,
482
- frame_count: total_frames,
483
- frame: current_frame || undefined,
484
- })
485
- }
694
+ notify_step_change()
486
695
  }
487
696
  }
488
697
 
@@ -501,7 +710,7 @@
501
710
  reader.addEventListener(`error`, () => reject(new Error(`Failed to read file`)))
502
711
 
503
712
  // Read as text for text-based formats, binary for others
504
- if (file.name.toLowerCase().match(/\.(xyz|json|extxyz|lammpstrj)$/)) {
713
+ if (/\.(?:xyz|json|extxyz|lammpstrj)$/.test(file.name.toLowerCase())) {
505
714
  reader.readAsText(file)
506
715
  } else reader.readAsArrayBuffer(file)
507
716
  })
@@ -528,44 +737,50 @@
528
737
  })
529
738
  }
530
739
  }
531
- $effect(() => { // Effect to manage playback interval
532
- // Only watch is_playing and frame_rate_ms, not play_interval itself
533
- const playing = is_playing
534
- const rate_ms = 1000 / fps
535
-
536
- // Read current interval once (untrack to avoid circular dependency)
537
- const current_interval = untrack(() => play_interval)
538
- if (playing) {
539
- // Clear existing interval if it exists
540
- if (current_interval !== undefined) clearInterval(current_interval)
541
-
542
- // Create new interval with current frame rate
543
- play_interval = setInterval(() => {
544
- if (current_step_idx >= total_frames - 1) {
545
- if (trajectory) {
546
- on_end?.({
547
- trajectory,
548
- step_idx: current_step_idx,
549
- frame_count: total_frames,
550
- frame: current_frame || undefined,
551
- })
552
- }
553
- go_to_step(0) // Loop back to 1st step
554
- if (trajectory) {
555
- on_loop?.({ trajectory, frame_count: total_frames })
556
- }
557
- } else next_step()
558
- }, rate_ms)
559
- } else if (current_interval !== undefined) {
560
- // Clear interval when not playing
561
- clearInterval(current_interval)
562
- play_interval = undefined
563
- }
564
- })
740
+ // Advance one frame (or loop back to start), firing the matching callbacks
741
+ function advance_playback() {
742
+ if (current_step_idx >= total_frames - 1) {
743
+ if (trajectory) {
744
+ on_end?.({
745
+ trajectory,
746
+ step_idx: current_step_idx,
747
+ frame_count: total_frames,
748
+ frame: current_frame || undefined,
749
+ })
750
+ }
751
+ go_to_step(0) // loop back to 1st step
752
+ if (trajectory) on_loop?.({ trajectory, frame_count: total_frames })
753
+ } else next_step()
754
+ }
565
755
 
566
- // Cleanup interval on component destroy
567
- $effect(() => () => {
568
- if (play_interval !== undefined) clearInterval(play_interval)
756
+ // rAF playback loop. Only tracks `is_playing`; `fps`/`current_step_idx`/`total_frames` are read
757
+ // live in the untracked tick, so changing fps mid-play retargets the cadence without restarting.
758
+ // Clamped delta + at most one advance per tick lets a background tab resume cleanly (no jump),
759
+ // but also caps playback at the ~60fps refresh rate (= default fps_range max; higher won't help).
760
+ $effect(() => {
761
+ if (!is_playing) return
762
+ let last = performance.now()
763
+ let accumulated_ms = 0
764
+ const tick = (now: number) => {
765
+ // stop if the trajectory went away or is no longer animatable (e.g. swapped mid-play)
766
+ if (!trajectory || total_frames <= 1) {
767
+ is_playing = false
768
+ return
769
+ }
770
+ accumulated_ms += Math.min(now - last, 250)
771
+ last = now
772
+ const step_ms = 1000 / Math.max(0.1, fps)
773
+ if (accumulated_ms >= step_ms) {
774
+ accumulated_ms = Math.min(accumulated_ms - step_ms, step_ms)
775
+ advance_playback()
776
+ }
777
+ play_raf = requestAnimationFrame(tick)
778
+ }
779
+ play_raf = requestAnimationFrame(tick)
780
+ return () => {
781
+ if (play_raf !== undefined) cancelAnimationFrame(play_raf)
782
+ play_raf = undefined
783
+ }
569
784
  })
570
785
 
571
786
  // Handle internal file format drops
@@ -582,10 +797,7 @@
582
797
  const array_buffer = await response.arrayBuffer()
583
798
  await load_trajectory_data(array_buffer, file_info.name)
584
799
  } else {
585
- console.warn(
586
- `Binary file without ArrayBuffer or blob URL:`,
587
- file_info.name,
588
- )
800
+ console.warn(`Binary file without ArrayBuffer or blob URL:`, file_info.name)
589
801
  }
590
802
  } else {
591
803
  await load_trajectory_data(file_info.content, file_info.name)
@@ -607,26 +819,23 @@
607
819
 
608
820
  try {
609
821
  // Check for our custom internal file format first
610
- const internal_data = event.dataTransfer?.getData(
611
- `application/x-matterviz-file`,
612
- )
822
+ const internal_data = event.dataTransfer?.getData(`application/x-matterviz-file`)
613
823
  if (internal_data) {
614
824
  const handled = await handle_internal_file_drop(internal_data)
615
825
  if (handled) return
616
826
  }
617
827
 
618
828
  // Handle URL-based files (e.g. from FilePicker)
619
- const handled = await handle_url_drop(event, async (content, filename) => {
620
- current_filename = filename
621
- file_size = content instanceof ArrayBuffer
622
- ? content.byteLength
623
- : new Blob([content]).size
624
- await load_trajectory_data(content, filename)
625
- }).catch(() => false)
626
-
627
- if (handled) {
628
- return
629
- }
829
+ const handled = await io
830
+ .handle_url_drop(event, async (content, filename) => {
831
+ current_filename = filename
832
+ file_size =
833
+ content instanceof ArrayBuffer ? content.byteLength : new Blob([content]).size
834
+ await load_trajectory_data(content, filename)
835
+ })
836
+ .catch(() => false)
837
+
838
+ if (handled) return
630
839
 
631
840
  // Handle file system drops with optimized large file support
632
841
  const file = event.dataTransfer?.files[0]
@@ -658,33 +867,58 @@
658
867
  }
659
868
  }
660
869
 
661
- $effect(() => { // Load trajectory from URL when data_url is provided
662
- if (data_url && !trajectory) {
663
- loading = true
664
- error_msg = null
665
-
666
- load_from_url(data_url, async (content, filename) => {
667
- current_filename = filename
668
- file_size = content instanceof ArrayBuffer
669
- ? content.byteLength
670
- : new Blob([content]).size
671
- await load_trajectory_data(content, filename)
870
+ // Load trajectory from URL when data_url is provided. Track the model produced by
871
+ // this effect so caller-owned trajectory props keep precedence while URL-owned
872
+ // models can reload when data_url changes.
873
+ $effect(() => {
874
+ const requested_url = data_url
875
+ const current_trajectory = trajectory
876
+ const caller_owns_trajectory = Boolean(
877
+ current_trajectory && current_trajectory !== url_owned_trajectory,
878
+ )
879
+ if (!requested_url || caller_owns_trajectory) {
880
+ loaded_data_url = undefined
881
+ url_owned_trajectory = undefined
882
+ return
883
+ }
884
+ if (loaded_data_url === requested_url) return
885
+
886
+ const load_id = ++data_url_load_id
887
+ const is_current = () => load_id === data_url_load_id
888
+ loading = true
889
+ error_msg = null
890
+
891
+ io.load_from_url(requested_url, async (content, filename) => {
892
+ if (!is_current()) return
893
+ current_filename = filename
894
+ file_size =
895
+ content instanceof ArrayBuffer ? content.byteLength : new Blob([content]).size
896
+ await load_trajectory_data(content, filename, {
897
+ on_trajectory_loaded: (loaded_trajectory) => {
898
+ if (!is_current()) return
899
+ url_owned_trajectory = loaded_trajectory
900
+ loaded_data_url = requested_url
901
+ },
902
+ should_commit: is_current,
672
903
  })
673
- .then(() => {
674
- loading = false
675
- })
676
- .catch((err: Error) => {
677
- console.error(`Failed to load trajectory from URL:`, err)
678
- error_msg = `Failed to load trajectory: ${err.message}`
679
- current_filename = undefined
680
- file_size = undefined
681
- loading = false
682
- on_error?.({
683
- error_msg,
684
- filename: current_filename || undefined,
685
- file_size: file_size || undefined,
686
- })
687
- })
904
+ })
905
+ .catch((err: Error) => {
906
+ if (!is_current()) return
907
+ console.error(`Failed to load trajectory from URL:`, err)
908
+ error_msg = `Failed to load trajectory: ${err.message}`
909
+ current_filename = undefined
910
+ file_size = undefined
911
+ on_error?.({ error_msg, filename: io.basename_from_url(requested_url) })
912
+ })
913
+ .finally(() => {
914
+ if (is_current()) loading = false
915
+ })
916
+
917
+ return () => {
918
+ if (is_current()) {
919
+ data_url_load_id += 1
920
+ loading = false
921
+ }
688
922
  }
689
923
  })
690
924
 
@@ -693,87 +927,82 @@
693
927
  on_frame_rate_change?.({ trajectory, fps })
694
928
  })
695
929
 
696
- async function load_trajectory_data(data: string | ArrayBuffer, filename: string) {
930
+ async function load_trajectory_data(
931
+ data: string | ArrayBuffer,
932
+ filename: string,
933
+ options: {
934
+ on_trajectory_loaded?: (loaded_trajectory: TrajectoryType) => void
935
+ should_commit?: () => boolean
936
+ } = {},
937
+ ) {
938
+ const { on_trajectory_loaded, should_commit = () => true } = options
697
939
  loading = true
698
940
  error_msg = null
699
941
  parsing_progress = null
700
942
 
701
943
  // Reset previous loading state
702
944
  orig_data = null
945
+ const file_size_bytes =
946
+ data instanceof ArrayBuffer ? data.byteLength : new Blob([data]).size
703
947
 
704
948
  try {
705
949
  const data_size = data instanceof ArrayBuffer ? data.byteLength : data.length
706
950
 
707
951
  // Determine loading strategy based on file size
708
- const bin_file_threshold = loading_options.bin_file_threshold ??
709
- MAX_BIN_FILE_SIZE
710
- const text_file_threshold = loading_options.text_file_threshold ??
711
- MAX_TEXT_FILE_SIZE
712
- if (
952
+ const bin_file_threshold = loading_options.bin_file_threshold ?? MAX_BIN_FILE_SIZE
953
+ const text_file_threshold = loading_options.text_file_threshold ?? MAX_TEXT_FILE_SIZE
954
+ const is_large_file =
713
955
  (data instanceof ArrayBuffer && data_size > bin_file_threshold) ||
714
956
  (typeof data === `string` && data_size > text_file_threshold)
715
- ) { // Large files: Use indexed loading
716
- await load_with_indexing(data, filename)
717
- } else {
718
- // Small files: Use regular loading
719
- const merged_options = { ...loading_options, atom_type_mapping }
720
- trajectory = await parse_trajectory_async(data, filename, (progress) => {
721
- parsing_progress = progress
722
- }, merged_options)
723
- }
957
+
958
+ // Large files get indexed loading by default (loading_options can override)
959
+ const parsed_trajectory = await parse_trajectory_async(
960
+ data,
961
+ filename,
962
+ (progress) => {
963
+ if (should_commit()) parsing_progress = progress
964
+ },
965
+ {
966
+ ...(is_large_file ? { use_indexing: true } : {}),
967
+ ...loading_options,
968
+ atom_type_mapping,
969
+ },
970
+ )
971
+ if (!should_commit()) return
972
+ trajectory = parsed_trajectory
973
+ if (trajectory) on_trajectory_loaded?.(trajectory)
974
+ // Keep original data only when parsing attached a frame_loader for on-demand loads.
975
+ // Direct-parse fallbacks load all frames upfront, so retaining a duplicate wastes memory.
976
+ orig_data = trajectory?.frame_loader ? data : null
724
977
 
725
978
  current_step_idx = 0
726
979
  current_filename = filename
727
-
728
- const file_size_bytes = data instanceof ArrayBuffer
729
- ? data.byteLength
730
- : new Blob([data]).size
731
- on_file_load?.({ // emit file load event
732
- trajectory,
733
- frame_count: trajectory?.frames.length ?? 0,
734
- total_atoms: trajectory?.frames[0]?.structure.sites.length ?? 0,
980
+ file_size = file_size_bytes
981
+
982
+ const loaded_trajectory = trajectory
983
+ on_file_load?.({
984
+ // emit file load event
985
+ trajectory: loaded_trajectory,
986
+ frame_count: loaded_trajectory?.frames.length ?? 0,
987
+ total_atoms: loaded_trajectory?.frames[0]?.structure.sites.length ?? 0,
735
988
  filename,
736
989
  file_size: file_size_bytes,
737
990
  })
738
991
  } catch (err) {
992
+ if (!should_commit()) return
739
993
  const unsupported_message = get_unsupported_format_message(
740
994
  filename,
741
995
  typeof data === `string` ? data : ``,
742
996
  )
743
997
  error_msg = unsupported_message || `Failed to parse trajectory: ${err}`
998
+ on_error?.({ error_msg, filename, file_size: file_size_bytes })
744
999
  current_filename = undefined
745
1000
  file_size = undefined
746
-
747
- on_error?.({ // emit error event
748
- error_msg,
749
- filename: current_filename || undefined,
750
- file_size: file_size || undefined,
751
- })
752
1001
  } finally {
753
- parsing_progress = null
754
- loading = false
755
- }
756
- }
757
-
758
- // Load using indexed parsing for large files
759
- async function load_with_indexing(data: string | ArrayBuffer, filename: string) {
760
- try { // Use indexed parsing for efficient large file handling
761
- const merged_options = {
762
- use_indexing: true,
763
- ...loading_options,
764
- atom_type_mapping,
1002
+ if (should_commit()) {
1003
+ parsing_progress = null
1004
+ loading = false
765
1005
  }
766
- trajectory = await parse_trajectory_async(data, filename, (progress) => {
767
- parsing_progress = progress
768
- }, merged_options)
769
-
770
- // Keep original data for on-demand frame loads only when indexed parsing attached a
771
- // frame_loader. Direct-parse fallbacks (e.g. large JSON or extensionless blob:
772
- // filenames) load all frames upfront, so retaining a full duplicate payload wastes memory.
773
- orig_data = trajectory?.frame_loader ? data : null
774
- } catch (error) {
775
- console.error(`Indexed loading failed:`, error)
776
- throw error
777
1006
  }
778
1007
  }
779
1008
 
@@ -808,8 +1037,7 @@
808
1037
  // Don't handle shortcuts if user is typing in an input field (but allow if it's our step input and not focused)
809
1038
  const target = event.target instanceof HTMLElement ? event.target : null
810
1039
  const is_step_input = target?.classList.contains(`step-input`) ?? false
811
- const is_input_focused =
812
- target?.tagName === `INPUT` || target?.tagName === `TEXTAREA`
1040
+ const is_input_focused = target?.tagName === `INPUT` || target?.tagName === `TEXTAREA`
813
1041
 
814
1042
  // Skip if typing in an input that's not our step input
815
1043
  if (is_input_focused && !is_step_input) return false
@@ -848,7 +1076,7 @@
848
1076
  go_to_step(Math.max(0, current_step_idx - 25))
849
1077
  } else if (event.key === `PageDown`) {
850
1078
  go_to_step(Math.min(total_frames - 1, current_step_idx + 25))
851
- } // Interface shortcuts
1079
+ } // Interface shortcuts
852
1080
  else if (event.key === `f` && fullscreen_toggle) toggle_fullscreen(wrapper)
853
1081
  // 'i' key handled by the TrajectoryInfoPane's built-in toggle
854
1082
  // Playback speed shortcuts (only when playing)
@@ -858,14 +1086,14 @@
858
1086
  } else if (event.key === `-` && is_playing) {
859
1087
  fps = Math.max(fps_range[0], fps - 0.2)
860
1088
  on_frame_rate_change?.({ trajectory, fps })
861
- } // System shortcuts
1089
+ } // System shortcuts
862
1090
  else if (event.key === `Escape`) {
863
1091
  if (document.fullscreenElement) document.exitFullscreen()
864
1092
  else if (view_mode_dropdown_open) view_mode_dropdown_open = false
865
1093
  // Escape key for info pane handled by DraggablePane
866
- } // Number keys 0-9 - jump to percentage of trajectory
1094
+ } // Number keys 0-9 - jump to percentage of trajectory
867
1095
  else if (event.key >= `0` && event.key <= `9`) {
868
- go_to_step(Math.floor((parseInt(event.key, 10) / 10) * (total_frames - 1)))
1096
+ go_to_step(Math.floor((Number(event.key) / 10) * (total_frames - 1)))
869
1097
  } else handled = false
870
1098
 
871
1099
  return handled
@@ -889,8 +1117,12 @@
889
1117
 
890
1118
  <div
891
1119
  class:dragover
892
- class:active={is_playing || structure_info_open || controls_open ||
893
- scatter_controls.open || trajectory_export_open || info_pane_open}
1120
+ class:active={is_playing ||
1121
+ structure_info_open ||
1122
+ controls_open ||
1123
+ scatter_controls.open ||
1124
+ trajectory_export_open ||
1125
+ info_pane_open}
894
1126
  bind:this={wrapper}
895
1127
  bind:clientWidth={element_size.width}
896
1128
  bind:clientHeight={element_size.height}
@@ -900,13 +1132,17 @@
900
1132
  onmouseenter={() => (hovered = true)}
901
1133
  onmouseleave={() => (hovered = false)}
902
1134
  ondrop={handle_file_drop}
903
- {...drag_over_handlers({ allow: () => allow_file_drop, set_dragover: (over) => dragover = over })}
1135
+ {...io.drag_over_handlers({
1136
+ allow: () => allow_file_drop,
1137
+ set_dragover: (over) => (dragover = over),
1138
+ })}
904
1139
  onclick={handle_click_outside}
905
1140
  onkeydown={handle_and_prevent(onkeydown)}
906
1141
  {...rest}
907
- class="trajectory {actual_layout} {rest.class ?? ``}"
1142
+ class={[`trajectory`, actual_layout, rest.class]}
908
1143
  class:show-both-views={[`structure+scatter`, `structure+histogram`].includes(display_mode) &&
909
- actual_show_plot && show_structure}
1144
+ show_plot &&
1145
+ show_structure}
910
1146
  >
911
1147
  {#if loading}
912
1148
  {@const text = parsing_progress
@@ -918,11 +1154,7 @@
918
1154
  {...spinner_props}
919
1155
  />
920
1156
  {:else if error_msg}
921
- <TrajectoryError
922
- {error_msg}
923
- on_dismiss={() => (error_msg = null)}
924
- {error_snippet}
925
- />
1157
+ <TrajectoryError {error_msg} on_dismiss={() => (error_msg = null)} {error_snippet} />
926
1158
  {:else if trajectory}
927
1159
  {#if parse_warning_msg}
928
1160
  <StatusMessage
@@ -934,17 +1166,14 @@
934
1166
  {/if}
935
1167
  <!-- Trajectory Controls -->
936
1168
  {#if controls_config.mode !== `never`}
937
- <div
938
- class="trajectory-controls {controls_config.class}"
939
- style={controls_config.style}
940
- >
1169
+ <div class="trajectory-controls {controls_config.class}" style={controls_config.style}>
941
1170
  {#if trajectory_controls}
942
1171
  {@render trajectory_controls({
943
- trajectory,
944
- current_step_idx,
945
- total_frames: total_frames,
946
- on_step_change: go_to_step,
947
- })}
1172
+ trajectory,
1173
+ current_step_idx,
1174
+ total_frames: total_frames,
1175
+ on_step_change: go_to_step,
1176
+ })}
948
1177
  {:else}
949
1178
  {#if current_filename && controls_config.visible(`filename`)}
950
1179
  <button
@@ -955,7 +1184,7 @@
955
1184
  if (current_filename) {
956
1185
  navigator.clipboard.writeText(current_filename)
957
1186
  filename_copied = true
958
- setTimeout(() => filename_copied = false, 1000)
1187
+ setTimeout(() => (filename_copied = false), 1000)
959
1188
  }
960
1189
  }}
961
1190
  >
@@ -963,7 +1192,7 @@
963
1192
  {#if filename_copied}
964
1193
  <Icon
965
1194
  icon="Check"
966
- style="color: var(--success-color); position: absolute; right: 3pt; top: 50%; transform: translateY(-50%); font-size: 16px; animation: fade-in 0.1s; background: var(--surface-bg-hover); border-radius: 50%"
1195
+ style="--icon-size: 16px; color: var(--success-color); position: absolute; right: 3pt; top: 50%; transform: translateY(-50%); animation: fade-in 0.1s; background: var(--surface-bg-hover); border-radius: 50%; padding: 2px; box-sizing: content-box"
967
1196
  />
968
1197
  {/if}
969
1198
  </button>
@@ -975,14 +1204,16 @@
975
1204
  <button
976
1205
  onclick={prev_step}
977
1206
  disabled={current_step_idx === 0 || is_playing}
978
- title="Previous step"
1207
+ title="Previous step (←) · Home: first · j: −10 · PageUp: −25"
979
1208
  >
980
1209
 
981
1210
  </button>
982
1211
  <button
983
1212
  onclick={toggle_play}
984
1213
  disabled={total_frames <= 1}
985
- title={is_playing ? `Pause playback` : `Play trajectory`}
1214
+ title={`${
1215
+ is_playing ? `Pause` : `Play`
1216
+ } (Space) · ←/→ step · 0-9 jump % · +/- speed · f fullscreen`}
986
1217
  class="play-button"
987
1218
  class:playing={is_playing}
988
1219
  >
@@ -991,7 +1222,7 @@
991
1222
  <button
992
1223
  onclick={next_step}
993
1224
  disabled={current_step_idx === total_frames - 1 || is_playing}
994
- title="Next step"
1225
+ title="Next step (→) · End: last · l: +10 · PageDown: +25"
995
1226
  >
996
1227
 
997
1228
  </button>
@@ -1006,6 +1237,7 @@
1006
1237
  min="0"
1007
1238
  max={total_frames - 1}
1008
1239
  bind:value={current_step_idx}
1240
+ oninput={(event) => notify_step_change(event.currentTarget.valueAsNumber)}
1009
1241
  class="step-input"
1010
1242
  title="Enter step number to jump to"
1011
1243
  aria-label="Step input"
@@ -1018,15 +1250,15 @@
1018
1250
  min="0"
1019
1251
  max={total_frames - 1}
1020
1252
  bind:value={current_step_idx}
1253
+ oninput={(event) => notify_step_change(event.currentTarget.valueAsNumber)}
1021
1254
  class="step-slider"
1022
1255
  title="Drag to navigate steps"
1023
1256
  />
1024
1257
  {#if step_label_positions.length > 0}
1025
1258
  <div class="step-labels">
1026
1259
  {#each step_label_positions as step_idx (step_idx)}
1027
- {@const position_percent = total_frames > 1
1028
- ? (step_idx / (total_frames - 1)) * 100
1029
- : 0}
1260
+ {@const position_percent =
1261
+ total_frames > 1 ? (step_idx / (total_frames - 1)) * 100 : 0}
1030
1262
  {@const adjusted_position = 1.5 + (position_percent * (100 - 2)) / 100}
1031
1263
  <div class="step-tick" style:left="{adjusted_position}%"></div>
1032
1264
  <div class="step-label" style:left="{adjusted_position}%">
@@ -1039,8 +1271,8 @@
1039
1271
  </div>
1040
1272
  {/if}
1041
1273
 
1042
- <!-- Frame rate control - only shown when playing -->
1043
- {#if is_playing && controls_config.visible(`fps`)}
1274
+ <!-- Frame rate control: shown for any multi-frame trajectory so speed can be set before play -->
1275
+ {#if total_frames > 1 && controls_config.visible(`fps`)}
1044
1276
  <label
1045
1277
  class="fps-section"
1046
1278
  style="font-size: 0.9em; display: flex; align-items: center; gap: 5pt; margin-inline: 6pt"
@@ -1052,7 +1284,7 @@
1052
1284
  max={fps_range[1]}
1053
1285
  bind:value={fps}
1054
1286
  title="Frame rate: {format_num(fps, `.2~s`)} fps"
1055
- style="width: clamp(60px, 8cqw, 90px); accent-color: var(--accent-color)"
1287
+ style="width: clamp(60px, 8cqw, 90px)"
1056
1288
  />
1057
1289
  <input
1058
1290
  type="number"
@@ -1070,6 +1302,7 @@
1070
1302
  {#if trajectory && controls_config.visible(`info-pane`)}
1071
1303
  <TrajectoryInfoPane
1072
1304
  {trajectory}
1305
+ {current_frame}
1073
1306
  {current_step_idx}
1074
1307
  {current_filename}
1075
1308
  {current_file_path}
@@ -1091,8 +1324,7 @@
1091
1324
  />
1092
1325
  {/if}
1093
1326
  <!-- Display mode dropdown -->
1094
- {#if plot_series.length > 0 &&
1095
- controls_config.visible(`view-mode`)}
1327
+ {#if plot_series.length > 0 && controls_config.visible(`view-mode`)}
1096
1328
  <div class="view-mode-dropdown-wrapper">
1097
1329
  <button
1098
1330
  onclick={() => (view_mode_dropdown_open = !view_mode_dropdown_open)}
@@ -1102,40 +1334,21 @@
1102
1334
  style="background-color: transparent; padding: 0"
1103
1335
  >
1104
1336
  <Icon
1105
- icon={({
1106
- structure: `Atom`,
1107
- 'structure+scatter': `TwoColumns`,
1108
- 'structure+histogram': `TwoColumns`,
1109
- scatter: `ScatterPlot`,
1110
- histogram: `Histogram`,
1111
- } as const)[display_mode]}
1337
+ icon={(
1338
+ {
1339
+ structure: `Atom`,
1340
+ 'structure+scatter': `TwoColumns`,
1341
+ 'structure+histogram': `TwoColumns`,
1342
+ scatter: `ScatterPlot`,
1343
+ histogram: `Histogram`,
1344
+ } as const
1345
+ )[display_mode]}
1112
1346
  />
1113
1347
  <Icon icon={view_mode_dropdown_open ? `ArrowUp` : `ArrowDown`} />
1114
1348
  </button>
1115
1349
  {#if view_mode_dropdown_open}
1116
1350
  <div class="view-mode-dropdown">
1117
- {#each [
1118
- { mode: `structure`, icon: `Atom`, label: `Structure-only` },
1119
- {
1120
- mode: `structure+scatter`,
1121
- icon: `TwoColumns`,
1122
- label: `Structure + Scatter`,
1123
- },
1124
- {
1125
- mode: `structure+histogram`,
1126
- icon: `TwoColumns`,
1127
- label: `Structure + Histogram`,
1128
- },
1129
- { mode: `scatter`, icon: `ScatterPlot`, label: `Scatter-only` },
1130
- {
1131
- mode: `histogram`,
1132
- icon: `Histogram`,
1133
- label: `Histogram-only`,
1134
- },
1135
- ] as const as
1136
- option
1137
- (option.mode)
1138
- }
1351
+ {#each [{ mode: `structure`, icon: `Atom`, label: `Structure-only` }, { mode: `structure+scatter`, icon: `TwoColumns`, label: `Structure + Scatter` }, { mode: `structure+histogram`, icon: `TwoColumns`, label: `Structure + Histogram` }, { mode: `scatter`, icon: `ScatterPlot`, label: `Scatter-only` }, { mode: `histogram`, icon: `Histogram`, label: `Histogram-only` }] as const as option (option.mode)}
1139
1352
  <button
1140
1353
  class="view-mode-option"
1141
1354
  class:selected={display_mode === option.mode}
@@ -1170,7 +1383,7 @@
1170
1383
 
1171
1384
  <div
1172
1385
  class="content-area"
1173
- class:hide-plot={!actual_show_plot}
1386
+ class:hide-plot={!show_plot}
1174
1387
  class:hide-structure={!show_structure}
1175
1388
  class:show-both={[`structure+scatter`, `structure+histogram`].includes(display_mode)}
1176
1389
  class:show-structure-only={display_mode === `structure`}
@@ -1191,8 +1404,12 @@
1191
1404
  />
1192
1405
  {/if}
1193
1406
 
1194
- {#if actual_show_plot}
1195
- {#if display_mode === `scatter` || display_mode === `structure+scatter`}
1407
+ {#if show_plot}
1408
+ {#if plot_metadata_loading}
1409
+ <div class="plot-metadata-loading plot">
1410
+ <Spinner text="Sampling trajectory plot data..." style="--spinner-size: 1.4em" />
1411
+ </div>
1412
+ {:else if display_mode === `scatter` || display_mode === `structure+scatter`}
1196
1413
  <ScatterPlot
1197
1414
  series={plot_series}
1198
1415
  {x_axis}
@@ -1201,27 +1418,22 @@
1201
1418
  controls={scatter_controls}
1202
1419
  current_x_value={current_step_idx}
1203
1420
  change={plot_skimming ? handle_plot_change : undefined}
1204
- padding={{ t: 20, b: 60, l: 52, r: has_y2_series ? 100 : 20 }}
1421
+ padding={{ t: 20, b: 60, r: has_y2_series ? 100 : 20 }}
1205
1422
  range_padding={0}
1206
1423
  style="height: 100%"
1207
1424
  {...scatter_props}
1208
1425
  legend={{
1209
- ...scatter_props.legend ?? {},
1426
+ ...(scatter_props.legend ?? {}),
1210
1427
  on_toggle: (series_idx: number) => {
1211
1428
  handle_legend_toggle(series_idx)
1212
1429
  scatter_props.legend?.on_toggle?.(series_idx)
1213
1430
  },
1214
1431
  }}
1215
- class="plot {scatter_props.class ?? ``}"
1432
+ class={[`plot`, scatter_props.class]}
1216
1433
  >
1217
- {#snippet tooltip({
1218
- x,
1219
- y,
1220
- metadata,
1221
- label,
1222
- }: ScatterHandlerProps)}
1434
+ {#snippet tooltip({ x, y, metadata, label }: ScatterHandlerProps)}
1223
1435
  {@const formatted_y = typeof y === `number` ? format_num(y) : y}
1224
- Step: {Math.round(x)}<br />
1436
+ {x_axis_quantity}: {Math.round(x)}<br />
1225
1437
  {@html sanitize_html(metadata?.series_label || label || `Value`)}: {formatted_y}
1226
1438
  {/snippet}
1227
1439
  </ScatterPlot>
@@ -1242,7 +1454,7 @@
1242
1454
  histogram_props.on_series_toggle?.(series_idx)
1243
1455
  }}
1244
1456
  style="height: 100%"
1245
- class="plot {histogram_props.class ?? ``}"
1457
+ class={[`plot`, histogram_props.class]}
1246
1458
  --ctrl-btn-top="6ex"
1247
1459
  >
1248
1460
  {#snippet tooltip({
@@ -1266,8 +1478,8 @@
1266
1478
  <EmptyState class="trajectory-empty-state">
1267
1479
  <h3 id="load-trajectory">Load Trajectory</h3>
1268
1480
  <p>
1269
- Drop a trajectory file here (.xyz, .extxyz, .json, .json.gz, XDATCAR, .traj, .h5)
1270
- or provide trajectory data via props
1481
+ Drop a trajectory file here (.xyz, .extxyz, .json, .json.gz, XDATCAR, .traj, .h5) or
1482
+ provide trajectory data via props
1271
1483
  </p>
1272
1484
  <strong style="display: block; margin-block: 1em 1ex">Supported formats:</strong>
1273
1485
  <ul>
@@ -1279,9 +1491,7 @@
1279
1491
  <li>HDF5 trajectory files (.h5, .hdf5)</li>
1280
1492
  <li>Compressed files (.gz)</li>
1281
1493
  </ul>
1282
- <p>
1283
- 💡 Force vectors will be automatically displayed when present in trajectory data
1284
- </p>
1494
+ <p>💡 Force vectors will be automatically displayed when present in trajectory data</p>
1285
1495
  </EmptyState>
1286
1496
  {/if}
1287
1497
  </div>
@@ -1299,9 +1509,6 @@
1299
1509
  contain: layout;
1300
1510
  z-index: var(--traj-z-index, 1);
1301
1511
  container-type: size; /* enable cqh for panes if explicit height is set */
1302
- :global(.plot) {
1303
- background: var(--surface-bg);
1304
- }
1305
1512
  &.active {
1306
1513
  z-index: 2; /* needed so info/control panes from an active viewer overlay those of the next (if there is one) */
1307
1514
  .trajectory-controls {
@@ -1350,10 +1557,19 @@
1350
1557
  grid-template-rows: 1fr !important;
1351
1558
  }
1352
1559
  }
1560
+ .plot-metadata-loading {
1561
+ display: flex;
1562
+ align-items: center;
1563
+ justify-content: center;
1564
+ gap: 0.6em;
1565
+ min-height: 0;
1566
+ color: var(--text-muted, currentColor);
1567
+ background: var(--surface-bg);
1568
+ }
1353
1569
  .trajectory-controls {
1354
1570
  display: flex;
1355
1571
  align-items: center;
1356
- gap: clamp(2pt, 1cqw, 1ex);
1572
+ gap: clamp(4pt, 1.6cqw, 1.5ex);
1357
1573
  padding: clamp(2pt, 0.5cqw, 1ex) clamp(4pt, 1cqw, 1.2ex);
1358
1574
  background: var(--surface-bg-hover);
1359
1575
  backdrop-filter: blur(4px);
@@ -1373,7 +1589,7 @@
1373
1589
  }
1374
1590
  button {
1375
1591
  background: var(--btn-bg);
1376
- font-size: clamp(0.8rem, 2cqw, 1rem);
1592
+ font-size: var(--ctrl-btn-icon-size, clamp(0.7rem, 2cqmin, 0.85rem));
1377
1593
  &:hover:not(:disabled) {
1378
1594
  background: var(--btn-bg-hover);
1379
1595
  }
@@ -1389,7 +1605,14 @@
1389
1605
  .nav-section {
1390
1606
  display: flex;
1391
1607
  align-items: center;
1392
- gap: clamp(1pt, 0.5cqw, 5pt);
1608
+ gap: clamp(3pt, 1cqw, 9pt);
1609
+ }
1610
+ /* nudge control-button icons slightly larger via transform */
1611
+ .nav-section button,
1612
+ .view-mode-button,
1613
+ .info-section
1614
+ :global(:is(.trajectory-info-toggle, .trajectory-export-toggle, .fullscreen-button)) {
1615
+ transform: scale(1.15);
1393
1616
  }
1394
1617
  .step-section {
1395
1618
  display: flex;
@@ -1411,7 +1634,8 @@
1411
1634
  }
1412
1635
  .step-slider {
1413
1636
  width: 100%;
1414
- accent-color: var(--accent-color);
1637
+ position: relative;
1638
+ z-index: 1; /* keep the slider knob above the step labels (which follow it in the DOM) */
1415
1639
  }
1416
1640
  .step-labels {
1417
1641
  position: absolute;
@@ -1469,7 +1693,7 @@
1469
1693
  position: relative;
1470
1694
  }
1471
1695
  .info-section :global(:is(.trajectory-info-toggle, .trajectory-export-toggle)) {
1472
- font-size: clamp(1rem, 2.2cqw, 1.1rem);
1696
+ font-size: var(--ctrl-btn-icon-size, clamp(0.7rem, 2cqmin, 0.85rem));
1473
1697
  }
1474
1698
  .play-button {
1475
1699
  min-width: clamp(32px, 4cqw, 36px);
@@ -1479,10 +1703,7 @@
1479
1703
  &.playing {
1480
1704
  background: var(--traj-pause-btn-bg, var(--btn-bg, rgba(0, 0, 0, 0.1)));
1481
1705
  &:hover:not(:disabled) {
1482
- background: var(
1483
- --traj-pause-btn-bg-hover,
1484
- var(--btn-bg-hover, rgba(0, 0, 0, 0.1))
1485
- );
1706
+ background: var(--traj-pause-btn-bg-hover, var(--btn-bg-hover, rgba(0, 0, 0, 0.1)));
1486
1707
  }
1487
1708
  }
1488
1709
  }
@@ -1530,14 +1751,20 @@
1530
1751
  .view-mode-dropdown-wrapper {
1531
1752
  display: flex;
1532
1753
  position: relative;
1754
+ z-index: var(--trajectory-view-mode-z-index, 20);
1533
1755
  }
1534
1756
  .view-mode-dropdown {
1535
1757
  position: absolute;
1536
1758
  top: 115%;
1537
1759
  right: 0;
1538
- background: var(--surface-bg);
1760
+ z-index: 1;
1761
+ min-width: max-content;
1762
+ background: var(--trajectory-view-mode-bg, light-dark(#fff, #2f3137));
1539
1763
  border-radius: 4px;
1540
- box-shadow: 0 8px 16px -4px rgba(0, 0, 0, 0.3), 0 4px 8px -2px rgba(0, 0, 0, 0.1);
1764
+ box-shadow:
1765
+ 0 8px 16px -4px rgba(0, 0, 0, 0.3),
1766
+ 0 4px 8px -2px rgba(0, 0, 0, 0.1);
1767
+ pointer-events: auto;
1541
1768
  }
1542
1769
  .view-mode-option {
1543
1770
  display: flex;