matterviz 0.4.1 → 0.4.2

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