matterviz 0.4.0 → 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 (527) 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 +132 -219
  7. package/dist/brillouin/BrillouinZone.svelte.d.ts +9 -20
  8. package/dist/brillouin/BrillouinZoneControls.svelte +3 -6
  9. package/dist/brillouin/BrillouinZoneExportPane.svelte +47 -96
  10. package/dist/brillouin/BrillouinZoneExportPane.svelte.d.ts +1 -1
  11. package/dist/brillouin/BrillouinZoneInfoPane.svelte +15 -42
  12. package/dist/brillouin/BrillouinZoneInfoPane.svelte.d.ts +2 -3
  13. package/dist/brillouin/BrillouinZoneScene.svelte +63 -230
  14. package/dist/brillouin/BrillouinZoneScene.svelte.d.ts +3 -23
  15. package/dist/brillouin/ReciprocalVectors.svelte +39 -0
  16. package/dist/brillouin/ReciprocalVectors.svelte.d.ts +9 -0
  17. package/dist/brillouin/compute.d.ts +2 -0
  18. package/dist/brillouin/compute.js +80 -77
  19. package/dist/brillouin/geometry.d.ts +8 -0
  20. package/dist/brillouin/geometry.js +57 -0
  21. package/dist/brillouin/index.d.ts +2 -0
  22. package/dist/brillouin/index.js +2 -0
  23. package/dist/brillouin/types.d.ts +2 -2
  24. package/dist/chempot-diagram/ChemPotDiagram.svelte +33 -38
  25. package/dist/chempot-diagram/ChemPotDiagram.svelte.d.ts +1 -1
  26. package/dist/chempot-diagram/ChemPotDiagram2D.svelte +143 -246
  27. package/dist/chempot-diagram/ChemPotDiagram2D.svelte.d.ts +4 -1
  28. package/dist/chempot-diagram/ChemPotDiagram3D.svelte +652 -976
  29. package/dist/chempot-diagram/ChemPotDiagram3D.svelte.d.ts +7 -1
  30. package/dist/chempot-diagram/async-compute.svelte.js +1 -1
  31. package/dist/chempot-diagram/color.d.ts +3 -6
  32. package/dist/chempot-diagram/color.js +5 -5
  33. package/dist/chempot-diagram/compute.d.ts +3 -3
  34. package/dist/chempot-diagram/compute.js +3 -1
  35. package/dist/chempot-diagram/controls-state.svelte.d.ts +10 -0
  36. package/dist/chempot-diagram/controls-state.svelte.js +42 -0
  37. package/dist/chempot-diagram/export.d.ts +47 -0
  38. package/dist/chempot-diagram/export.js +133 -0
  39. package/dist/chempot-diagram/index.d.ts +1 -0
  40. package/dist/chempot-diagram/index.js +1 -0
  41. package/dist/chempot-diagram/pointer.d.ts +0 -10
  42. package/dist/chempot-diagram/pointer.js +4 -4
  43. package/dist/chempot-diagram/types.d.ts +3 -3
  44. package/dist/colors/index.d.ts +0 -4
  45. package/dist/colors/index.js +21 -22
  46. package/dist/composition/BarChart.svelte +37 -31
  47. package/dist/composition/BubbleChart.svelte +17 -17
  48. package/dist/composition/Composition.svelte +7 -11
  49. package/dist/composition/Formula.svelte +18 -25
  50. package/dist/composition/FormulaFilter.svelte +96 -98
  51. package/dist/composition/PieChart.svelte +60 -46
  52. package/dist/composition/chem-sys.js +3 -2
  53. package/dist/composition/format.js +4 -3
  54. package/dist/composition/index.d.ts +2 -0
  55. package/dist/composition/index.js +14 -0
  56. package/dist/composition/parse.d.ts +2 -3
  57. package/dist/composition/parse.js +27 -28
  58. package/dist/constants.js +11 -9
  59. package/dist/controls.d.ts +1 -0
  60. package/dist/controls.js +0 -1
  61. package/dist/convex-hull/ConvexHull.svelte +20 -25
  62. package/dist/convex-hull/ConvexHull.svelte.d.ts +1 -4
  63. package/dist/convex-hull/ConvexHull2D.svelte +176 -256
  64. package/dist/convex-hull/ConvexHull2D.svelte.d.ts +1 -1
  65. package/dist/convex-hull/ConvexHull3D.svelte +269 -804
  66. package/dist/convex-hull/ConvexHull3D.svelte.d.ts +1 -1
  67. package/dist/convex-hull/ConvexHull4D.svelte +236 -755
  68. package/dist/convex-hull/ConvexHull4D.svelte.d.ts +1 -1
  69. package/dist/convex-hull/ConvexHullChrome.svelte +264 -0
  70. package/dist/convex-hull/ConvexHullChrome.svelte.d.ts +30 -0
  71. package/dist/convex-hull/ConvexHullControls.svelte +128 -49
  72. package/dist/convex-hull/ConvexHullControls.svelte.d.ts +7 -6
  73. package/dist/convex-hull/ConvexHullInfoPane.svelte +18 -5
  74. package/dist/convex-hull/ConvexHullInfoPane.svelte.d.ts +6 -5
  75. package/dist/convex-hull/ConvexHullStats.svelte +210 -351
  76. package/dist/convex-hull/ConvexHullStats.svelte.d.ts +3 -1
  77. package/dist/convex-hull/ConvexHullTooltip.svelte +16 -14
  78. package/dist/convex-hull/ConvexHullTooltip.svelte.d.ts +2 -1
  79. package/dist/convex-hull/GasPressureControls.svelte +5 -12
  80. package/dist/convex-hull/StructurePopup.svelte +17 -21
  81. package/dist/convex-hull/StructurePopup.svelte.d.ts +2 -0
  82. package/dist/convex-hull/TemperatureSlider.svelte +4 -10
  83. package/dist/convex-hull/barycentric-coords.d.ts +2 -4
  84. package/dist/convex-hull/barycentric-coords.js +6 -33
  85. package/dist/convex-hull/canvas-interactions.svelte.d.ts +79 -0
  86. package/dist/convex-hull/canvas-interactions.svelte.js +279 -0
  87. package/dist/convex-hull/helpers.d.ts +47 -17
  88. package/dist/convex-hull/helpers.js +186 -101
  89. package/dist/convex-hull/hull-state.svelte.d.ts +44 -0
  90. package/dist/convex-hull/hull-state.svelte.js +124 -0
  91. package/dist/convex-hull/index.d.ts +9 -7
  92. package/dist/convex-hull/index.js +7 -2
  93. package/dist/convex-hull/thermodynamics.js +109 -937
  94. package/dist/convex-hull/types.d.ts +12 -4
  95. package/dist/convex-hull/types.js +12 -0
  96. package/dist/coordination/CoordinationBarPlot.svelte +41 -43
  97. package/dist/coordination/CoordinationBarPlot.svelte.d.ts +1 -1
  98. package/dist/coordination/calc-coordination.d.ts +1 -1
  99. package/dist/coordination/calc-coordination.js +28 -25
  100. package/dist/element/BohrAtom.svelte +6 -8
  101. package/dist/element/ElementHeading.svelte +4 -1
  102. package/dist/element/ElementPhoto.svelte +11 -9
  103. package/dist/element/ElementStats.svelte +12 -10
  104. package/dist/element/ElementTile.svelte +24 -29
  105. package/dist/element/ElementTile.svelte.d.ts +1 -1
  106. package/dist/element/Nucleus.svelte +4 -2
  107. package/dist/element/data.d.ts +1 -2
  108. package/dist/element/helpers.d.ts +4 -0
  109. package/dist/element/helpers.js +18 -0
  110. package/dist/element/index.d.ts +1 -0
  111. package/dist/element/index.js +3 -0
  112. package/dist/feedback/DragOverlay.svelte +5 -3
  113. package/dist/feedback/DragOverlay.svelte.d.ts +2 -1
  114. package/dist/feedback/StatusMessage.svelte +20 -7
  115. package/dist/fermi-surface/FermiSlice.svelte +21 -18
  116. package/dist/fermi-surface/FermiSurface.svelte +78 -165
  117. package/dist/fermi-surface/FermiSurface.svelte.d.ts +9 -20
  118. package/dist/fermi-surface/FermiSurfaceControls.svelte +6 -20
  119. package/dist/fermi-surface/FermiSurfaceScene.svelte +93 -272
  120. package/dist/fermi-surface/FermiSurfaceScene.svelte.d.ts +3 -23
  121. package/dist/fermi-surface/FermiSurfaceTooltip.svelte +9 -6
  122. package/dist/fermi-surface/compute.js +14 -13
  123. package/dist/fermi-surface/export.js +4 -15
  124. package/dist/fermi-surface/index.d.ts +0 -1
  125. package/dist/fermi-surface/index.js +0 -1
  126. package/dist/fermi-surface/parse.d.ts +1 -1
  127. package/dist/fermi-surface/parse.js +84 -97
  128. package/dist/fermi-surface/types.d.ts +2 -2
  129. package/dist/heatmap-matrix/HeatmapMatrix.svelte +161 -185
  130. package/dist/heatmap-matrix/HeatmapMatrix.svelte.d.ts +6 -5
  131. package/dist/heatmap-matrix/HeatmapMatrixControls.svelte +12 -9
  132. package/dist/heatmap-matrix/HeatmapMatrixControls.svelte.d.ts +5 -5
  133. package/dist/heatmap-matrix/index.d.ts +8 -2
  134. package/dist/icons.d.ts +4 -0
  135. package/dist/icons.js +4 -0
  136. package/dist/index.d.ts +1 -0
  137. package/dist/index.js +1 -0
  138. package/dist/io/ExportPane.svelte +170 -0
  139. package/dist/io/ExportPane.svelte.d.ts +17 -0
  140. package/dist/io/decompress.d.ts +2 -1
  141. package/dist/io/decompress.js +22 -36
  142. package/dist/io/export.d.ts +9 -1
  143. package/dist/io/export.js +37 -29
  144. package/dist/io/fetch.d.ts +2 -4
  145. package/dist/io/fetch.js +0 -11
  146. package/dist/io/file-drop.d.ts +7 -0
  147. package/dist/io/file-drop.js +13 -0
  148. package/dist/io/index.d.ts +1 -0
  149. package/dist/io/index.js +3 -0
  150. package/dist/io/is-binary.d.ts +9 -0
  151. package/dist/io/is-binary.js +45 -1
  152. package/dist/io/types.d.ts +13 -0
  153. package/dist/io/url-drop.js +13 -25
  154. package/dist/isosurface/Isosurface.svelte +17 -23
  155. package/dist/isosurface/IsosurfaceControls.svelte +37 -42
  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 +50 -48
  159. package/dist/labels.d.ts +6 -2
  160. package/dist/labels.js +44 -14
  161. package/dist/layout/FullscreenButton.svelte +34 -0
  162. package/dist/layout/FullscreenButton.svelte.d.ts +10 -0
  163. package/dist/layout/FullscreenToggle.svelte +12 -12
  164. package/dist/layout/InfoCard.svelte +7 -18
  165. package/dist/layout/InfoTag.svelte +1 -3
  166. package/dist/layout/NumberRangeInput.svelte +46 -0
  167. package/dist/layout/NumberRangeInput.svelte.d.ts +12 -0
  168. package/dist/layout/PropertyFilter.svelte +2 -6
  169. package/dist/layout/SettingsSection.svelte +21 -17
  170. package/dist/layout/SubpageGrid.svelte +3 -1
  171. package/dist/layout/ViewerChrome.svelte +115 -0
  172. package/dist/layout/ViewerChrome.svelte.d.ts +18 -0
  173. package/dist/layout/fullscreen.d.ts +4 -0
  174. package/dist/layout/fullscreen.js +3 -5
  175. package/dist/layout/fullscreen.svelte.d.ts +8 -0
  176. package/dist/layout/fullscreen.svelte.js +36 -0
  177. package/dist/layout/index.d.ts +4 -0
  178. package/dist/layout/index.js +4 -0
  179. package/dist/layout/json-tree/JsonNode.svelte +8 -11
  180. package/dist/layout/json-tree/JsonTree.svelte +53 -77
  181. package/dist/layout/json-tree/JsonValue.svelte +7 -15
  182. package/dist/layout/json-tree/utils.js +14 -23
  183. package/dist/math.d.ts +9 -3
  184. package/dist/math.js +37 -21
  185. package/dist/overlays/ContextMenu.svelte +4 -2
  186. package/dist/overlays/CopyButton.svelte +11 -5
  187. package/dist/overlays/DragControlTab.svelte +6 -1
  188. package/dist/overlays/DraggablePane.svelte +81 -26
  189. package/dist/overlays/DraggablePane.svelte.d.ts +1 -0
  190. package/dist/overlays/GlassChip.svelte +30 -0
  191. package/dist/overlays/GlassChip.svelte.d.ts +8 -0
  192. package/dist/overlays/InfoPaneCards.svelte +11 -23
  193. package/dist/overlays/index.d.ts +11 -0
  194. package/dist/overlays/index.js +26 -0
  195. package/dist/overlays/portal.d.ts +2 -0
  196. package/dist/overlays/portal.js +14 -0
  197. package/dist/periodic-table/PeriodicTable.svelte +115 -146
  198. package/dist/periodic-table/PeriodicTable.svelte.d.ts +5 -3
  199. package/dist/periodic-table/PeriodicTableControls.svelte +11 -63
  200. package/dist/periodic-table/TableInset.svelte +9 -4
  201. package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte +50 -71
  202. package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte.d.ts +6 -8
  203. package/dist/phase-diagram/PhaseDiagramControls.svelte +73 -96
  204. package/dist/phase-diagram/PhaseDiagramControls.svelte.d.ts +4 -3
  205. package/dist/phase-diagram/PhaseDiagramEditorPane.svelte +4 -2
  206. package/dist/phase-diagram/PhaseDiagramEditorPane.svelte.d.ts +2 -3
  207. package/dist/phase-diagram/PhaseDiagramExportPane.svelte +49 -134
  208. package/dist/phase-diagram/PhaseDiagramExportPane.svelte.d.ts +3 -4
  209. package/dist/phase-diagram/PhaseDiagramTooltip.svelte +54 -58
  210. package/dist/phase-diagram/TdbInfoPanel.svelte +21 -15
  211. package/dist/phase-diagram/build-diagram.js +3 -3
  212. package/dist/phase-diagram/colors.js +1 -1
  213. package/dist/phase-diagram/diagram-input.d.ts +1 -2
  214. package/dist/phase-diagram/parse.d.ts +2 -1
  215. package/dist/phase-diagram/parse.js +15 -15
  216. package/dist/phase-diagram/svg-to-diagram.js +21 -15
  217. package/dist/phase-diagram/utils.js +2 -3
  218. package/dist/plot/bar/BarPlot.svelte +494 -692
  219. package/dist/plot/bar/BarPlot.svelte.d.ts +9 -15
  220. package/dist/plot/bar/BarPlotControls.svelte +1 -3
  221. package/dist/plot/bar/BarPlotControls.svelte.d.ts +5 -7
  222. package/dist/plot/bar/SpacegroupBarPlot.svelte +31 -37
  223. package/dist/plot/bar/data.d.ts +5 -4
  224. package/dist/plot/bar/data.js +2 -1
  225. package/dist/plot/box/BoxPlot.svelte +361 -439
  226. package/dist/plot/box/BoxPlot.svelte.d.ts +6 -3
  227. package/dist/plot/box/BoxPlotControls.svelte.d.ts +1 -1
  228. package/dist/plot/box/Violin.svelte.d.ts +2 -1
  229. package/dist/plot/box/box-plot.d.ts +3 -2
  230. package/dist/plot/box/box-plot.js +6 -3
  231. package/dist/plot/box/kde.d.ts +2 -1
  232. package/dist/plot/box/kde.js +4 -4
  233. package/dist/plot/core/auto-place.d.ts +1 -1
  234. package/dist/plot/core/auto-place.js +5 -2
  235. package/dist/plot/core/axis-utils.d.ts +12 -7
  236. package/dist/plot/core/axis-utils.js +48 -48
  237. package/dist/plot/core/components/AxisLabel.svelte +82 -20
  238. package/dist/plot/core/components/AxisLabel.svelte.d.ts +1 -0
  239. package/dist/plot/core/components/ColorBar.svelte +106 -111
  240. package/dist/plot/core/components/ColorBar.svelte.d.ts +5 -4
  241. package/dist/plot/core/components/ColorScaleSelect.svelte +1 -1
  242. package/dist/plot/core/components/FillArea.svelte +10 -17
  243. package/dist/plot/core/components/HierarchyControls.svelte +249 -0
  244. package/dist/plot/{sunburst/SunburstControls.svelte.d.ts → core/components/HierarchyControls.svelte.d.ts} +12 -8
  245. package/dist/plot/core/components/InteractiveAxisLabel.svelte +1 -1
  246. package/dist/plot/core/components/InteractiveAxisLabel.svelte.d.ts +1 -1
  247. package/dist/plot/core/components/Line.svelte +21 -11
  248. package/dist/plot/core/components/Line.svelte.d.ts +5 -2
  249. package/dist/plot/core/components/PlotAxis.svelte +42 -18
  250. package/dist/plot/core/components/PlotAxis.svelte.d.ts +2 -1
  251. package/dist/plot/core/components/PlotControls.svelte +65 -41
  252. package/dist/plot/core/components/PlotControls.svelte.d.ts +1 -1
  253. package/dist/plot/core/components/PlotLegend.svelte +31 -35
  254. package/dist/plot/core/components/PlotMarginals.svelte +616 -0
  255. package/dist/plot/core/components/PlotMarginals.svelte.d.ts +15 -0
  256. package/dist/plot/core/components/PlotTooltip.svelte +1 -1
  257. package/dist/plot/core/components/PortalSelect.svelte +4 -8
  258. package/dist/plot/core/components/ReferenceLine.svelte +27 -24
  259. package/dist/plot/core/components/ReferenceLine3D.svelte +56 -59
  260. package/dist/plot/core/components/ReferenceLine3D.svelte.d.ts +4 -4
  261. package/dist/plot/core/components/ReferencePlane.svelte +9 -10
  262. package/dist/plot/core/components/ReferencePlane.svelte.d.ts +4 -4
  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 +29 -476
  271. package/dist/plot/core/data-transform.js +9 -5
  272. package/dist/plot/core/fill-utils.d.ts +7 -4
  273. package/dist/plot/core/fill-utils.js +34 -9
  274. package/dist/plot/core/index.d.ts +1 -0
  275. package/dist/plot/core/index.js +1 -0
  276. package/dist/plot/core/interactions.d.ts +5 -1
  277. package/dist/plot/core/interactions.js +14 -2
  278. package/dist/plot/core/layout.d.ts +3 -1
  279. package/dist/plot/core/layout.js +22 -13
  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 +35 -0
  283. package/dist/plot/core/pan-zoom.svelte.js +221 -0
  284. package/dist/plot/core/placed-tween.svelte.d.ts +21 -0
  285. package/dist/plot/core/placed-tween.svelte.js +68 -0
  286. package/dist/plot/core/reference-line.d.ts +10 -10
  287. package/dist/plot/core/reference-line.js +7 -7
  288. package/dist/plot/core/scales.d.ts +40 -25
  289. package/dist/plot/core/scales.js +49 -21
  290. package/dist/plot/core/svg.d.ts +2 -1
  291. package/dist/plot/core/types/fills.d.ts +101 -0
  292. package/dist/plot/core/types/fills.js +15 -0
  293. package/dist/plot/core/types/plot-3d.d.ts +87 -0
  294. package/dist/plot/core/types/plot-3d.js +4 -0
  295. package/dist/plot/core/types/reference-lines.d.ts +140 -0
  296. package/dist/plot/core/types/reference-lines.js +9 -0
  297. package/dist/plot/core/types.d.ts +47 -331
  298. package/dist/plot/core/types.js +23 -20
  299. package/dist/plot/core/utils/hierarchy-chart.d.ts +51 -0
  300. package/dist/plot/core/utils/hierarchy-chart.js +179 -0
  301. package/dist/plot/core/utils/hierarchy-labels.d.ts +20 -0
  302. package/dist/plot/core/utils/hierarchy-labels.js +99 -0
  303. package/dist/plot/core/utils/label-placement.d.ts +1 -1
  304. package/dist/plot/core/utils/label-placement.js +5 -5
  305. package/dist/plot/core/utils.d.ts +2 -1
  306. package/dist/plot/histogram/Histogram.svelte +373 -631
  307. package/dist/plot/histogram/Histogram.svelte.d.ts +7 -7
  308. package/dist/plot/histogram/HistogramControls.svelte +30 -28
  309. package/dist/plot/histogram/HistogramControls.svelte.d.ts +1 -1
  310. package/dist/plot/histogram/histogram.d.ts +31 -0
  311. package/dist/plot/histogram/histogram.js +70 -0
  312. package/dist/plot/histogram/index.d.ts +1 -0
  313. package/dist/plot/histogram/index.js +1 -0
  314. package/dist/plot/index.d.ts +1 -0
  315. package/dist/plot/index.js +1 -0
  316. package/dist/plot/sankey/Sankey.svelte +79 -73
  317. package/dist/plot/sankey/Sankey.svelte.d.ts +3 -3
  318. package/dist/plot/sankey/SankeyControls.svelte +22 -16
  319. package/dist/plot/sankey/sankey.js +3 -1
  320. package/dist/plot/scatter/BinnedScatterPlot.svelte +264 -112
  321. package/dist/plot/scatter/BinnedScatterPlot.svelte.d.ts +11 -4
  322. package/dist/plot/scatter/ElementScatter.svelte +4 -1
  323. package/dist/plot/scatter/ScatterPlot.svelte +623 -821
  324. package/dist/plot/scatter/ScatterPlot.svelte.d.ts +9 -15
  325. package/dist/plot/scatter/ScatterPlotControls.svelte +18 -19
  326. package/dist/plot/scatter/ScatterPlotControls.svelte.d.ts +1 -1
  327. package/dist/plot/scatter/ScatterPoint.svelte +16 -9
  328. package/dist/plot/scatter/binned-scatter-types.d.ts +6 -12
  329. package/dist/plot/scatter/binned-scatter-types.js +6 -1
  330. package/dist/plot/scatter/index.d.ts +1 -1
  331. package/dist/plot/scatter/scatter-data.d.ts +1 -1
  332. package/dist/plot/scatter/scatter-data.js +8 -7
  333. package/dist/plot/scatter-3d/ScatterPlot3D.svelte +40 -60
  334. package/dist/plot/scatter-3d/ScatterPlot3D.svelte.d.ts +6 -14
  335. package/dist/plot/scatter-3d/ScatterPlot3DControls.svelte +17 -15
  336. package/dist/plot/scatter-3d/ScatterPlot3DControls.svelte.d.ts +5 -5
  337. package/dist/plot/scatter-3d/ScatterPlot3DScene.svelte +232 -234
  338. package/dist/plot/scatter-3d/ScatterPlot3DScene.svelte.d.ts +5 -14
  339. package/dist/plot/scatter-3d/Surface3D.svelte +9 -17
  340. package/dist/plot/scatter-3d/Surface3D.svelte.d.ts +4 -3
  341. package/dist/plot/sunburst/Sunburst.svelte +256 -338
  342. package/dist/plot/sunburst/Sunburst.svelte.d.ts +4 -3
  343. package/dist/plot/sunburst/index.d.ts +1 -1
  344. package/dist/plot/sunburst/index.js +2 -1
  345. package/dist/plot/sunburst/render.d.ts +2 -2
  346. package/dist/plot/sunburst/render.js +47 -22
  347. package/dist/plot/sunburst/sunburst.d.ts +5 -1
  348. package/dist/plot/sunburst/sunburst.js +14 -4
  349. package/dist/plot/treemap/Treemap.svelte +907 -0
  350. package/dist/plot/treemap/Treemap.svelte.d.ts +84 -0
  351. package/dist/plot/treemap/index.d.ts +3 -0
  352. package/dist/plot/treemap/index.js +4 -0
  353. package/dist/plot/treemap/treemap.d.ts +26 -0
  354. package/dist/plot/treemap/treemap.js +99 -0
  355. package/dist/rdf/RdfPlot.svelte +22 -26
  356. package/dist/rdf/RdfPlot.svelte.d.ts +1 -1
  357. package/dist/rdf/calc-rdf.js +4 -8
  358. package/dist/sanitize.js +14 -3
  359. package/dist/scene/SceneCamera.svelte +62 -0
  360. package/dist/scene/SceneCamera.svelte.d.ts +19 -0
  361. package/dist/scene/bind-renderer.svelte.d.ts +2 -0
  362. package/dist/scene/bind-renderer.svelte.js +22 -0
  363. package/dist/scene/index.d.ts +4 -0
  364. package/dist/scene/index.js +5 -0
  365. package/dist/scene/props.js +52 -0
  366. package/dist/scene/types.d.ts +26 -0
  367. package/dist/scene/types.js +1 -0
  368. package/dist/settings.d.ts +33 -11
  369. package/dist/settings.js +136 -51
  370. package/dist/spectral/Bands.svelte +231 -237
  371. package/dist/spectral/Bands.svelte.d.ts +3 -2
  372. package/dist/spectral/BandsAndDos.svelte +29 -29
  373. package/dist/spectral/BrillouinBandsDos.svelte +21 -29
  374. package/dist/spectral/Dos.svelte +75 -77
  375. package/dist/spectral/Dos.svelte.d.ts +2 -1
  376. package/dist/spectral/helpers.d.ts +9 -9
  377. package/dist/spectral/helpers.js +66 -70
  378. package/dist/state.svelte.d.ts +0 -7
  379. package/dist/state.svelte.js +5 -24
  380. package/dist/structure/Arrow.svelte +6 -21
  381. package/dist/structure/ArrowInstances.svelte +160 -0
  382. package/dist/structure/ArrowInstances.svelte.d.ts +16 -0
  383. package/dist/structure/AtomLegend.svelte +115 -75
  384. package/dist/structure/AtomLegend.svelte.d.ts +1 -1
  385. package/dist/structure/Bond.svelte +31 -23
  386. package/dist/structure/CanvasTooltip.svelte +16 -3
  387. package/dist/structure/CellSelect.svelte +22 -29
  388. package/dist/structure/CellSelect.svelte.d.ts +2 -1
  389. package/dist/structure/Cylinder.svelte +24 -40
  390. package/dist/structure/InstancedAtoms.svelte +114 -0
  391. package/dist/structure/InstancedAtoms.svelte.d.ts +15 -0
  392. package/dist/structure/Lattice.svelte +85 -149
  393. package/dist/structure/SiteLabels.svelte +122 -0
  394. package/dist/structure/SiteLabels.svelte.d.ts +18 -0
  395. package/dist/structure/Structure.svelte +725 -735
  396. package/dist/structure/Structure.svelte.d.ts +8 -16
  397. package/dist/structure/StructureCarousel.svelte +630 -0
  398. package/dist/structure/StructureCarousel.svelte.d.ts +19 -0
  399. package/dist/structure/StructureControls.svelte +298 -325
  400. package/dist/structure/StructureControls.svelte.d.ts +5 -3
  401. package/dist/structure/StructureExportPane.svelte +62 -168
  402. package/dist/structure/StructureExportPane.svelte.d.ts +4 -5
  403. package/dist/structure/StructureInfoPane.svelte +77 -69
  404. package/dist/structure/StructureInfoPane.svelte.d.ts +6 -6
  405. package/dist/structure/StructureScene.svelte +758 -600
  406. package/dist/structure/StructureScene.svelte.d.ts +24 -20
  407. package/dist/structure/StructureViewport.svelte +386 -0
  408. package/dist/structure/StructureViewport.svelte.d.ts +63 -0
  409. package/dist/structure/atom-label-placement.d.ts +18 -0
  410. package/dist/structure/atom-label-placement.js +115 -0
  411. package/dist/structure/atom-properties.d.ts +3 -1
  412. package/dist/structure/atom-properties.js +115 -54
  413. package/dist/structure/bond-order-perception.js +2 -4
  414. package/dist/structure/bonding.d.ts +12 -2
  415. package/dist/structure/bonding.js +172 -73
  416. package/dist/structure/edit-history.d.ts +6 -0
  417. package/dist/structure/edit-history.js +27 -0
  418. package/dist/structure/export.d.ts +25 -4
  419. package/dist/structure/export.js +103 -153
  420. package/dist/structure/format-detect.d.ts +1 -0
  421. package/dist/structure/format-detect.js +22 -0
  422. package/dist/structure/geometry.d.ts +9 -0
  423. package/dist/structure/geometry.js +27 -0
  424. package/dist/structure/index.d.ts +18 -1
  425. package/dist/structure/index.js +15 -2
  426. package/dist/structure/measure.d.ts +1 -1
  427. package/dist/structure/measure.js +6 -12
  428. package/dist/structure/parse.d.ts +5 -3
  429. package/dist/structure/parse.js +551 -567
  430. package/dist/structure/partial-occupancy.d.ts +0 -1
  431. package/dist/structure/partial-occupancy.js +5 -3
  432. package/dist/structure/pbc.d.ts +1 -1
  433. package/dist/structure/pbc.js +204 -51
  434. package/dist/structure/polyhedra.d.ts +40 -0
  435. package/dist/structure/polyhedra.js +587 -0
  436. package/dist/structure/site.d.ts +4 -0
  437. package/dist/structure/site.js +1 -0
  438. package/dist/structure/supercell.d.ts +1 -2
  439. package/dist/structure/supercell.js +10 -15
  440. package/dist/structure/validation.js +5 -6
  441. package/dist/symmetry/SymmetryElementControls.svelte +69 -0
  442. package/dist/symmetry/SymmetryElementControls.svelte.d.ts +9 -0
  443. package/dist/symmetry/SymmetryElements.svelte +350 -0
  444. package/dist/symmetry/SymmetryElements.svelte.d.ts +24 -0
  445. package/dist/symmetry/SymmetryStats.svelte +142 -49
  446. package/dist/symmetry/WyckoffTable.svelte +58 -16
  447. package/dist/symmetry/WyckoffTable.svelte.d.ts +3 -0
  448. package/dist/symmetry/cell-transform.js +8 -14
  449. package/dist/symmetry/index.d.ts +15 -4
  450. package/dist/symmetry/index.js +308 -81
  451. package/dist/symmetry/spacegroups.d.ts +5 -1
  452. package/dist/symmetry/spacegroups.js +18 -3
  453. package/dist/symmetry/symmetry-elements.d.ts +33 -0
  454. package/dist/symmetry/symmetry-elements.js +521 -0
  455. package/dist/symmetry/wyckoff-db.d.ts +9 -0
  456. package/dist/symmetry/wyckoff-db.js +87 -0
  457. package/dist/table/HeatmapTable.svelte +1104 -378
  458. package/dist/table/HeatmapTable.svelte.d.ts +12 -2
  459. package/dist/table/ToggleMenu.svelte +5 -12
  460. package/dist/table/index.d.ts +21 -6
  461. package/dist/table/index.js +34 -21
  462. package/dist/theme/ThemeControl.svelte +16 -8
  463. package/dist/theme/index.d.ts +1 -5
  464. package/dist/theme/index.js +4 -10
  465. package/dist/theme/themes.mjs +14 -7
  466. package/dist/tooltip/KCoords.svelte +4 -1
  467. package/dist/tooltip/TooltipContent.svelte +2 -5
  468. package/dist/trajectory/Trajectory.svelte +556 -352
  469. package/dist/trajectory/Trajectory.svelte.d.ts +10 -22
  470. package/dist/trajectory/TrajectoryError.svelte +11 -8
  471. package/dist/trajectory/TrajectoryExportPane.svelte +28 -54
  472. package/dist/trajectory/TrajectoryExportPane.svelte.d.ts +4 -5
  473. package/dist/trajectory/TrajectoryInfoPane.svelte +88 -99
  474. package/dist/trajectory/TrajectoryInfoPane.svelte.d.ts +3 -2
  475. package/dist/trajectory/constants.js +6 -2
  476. package/dist/trajectory/extract.js +73 -71
  477. package/dist/trajectory/format-detect.d.ts +0 -1
  478. package/dist/trajectory/format-detect.js +13 -17
  479. package/dist/trajectory/frame-reader.d.ts +2 -1
  480. package/dist/trajectory/frame-reader.js +74 -136
  481. package/dist/trajectory/helpers.d.ts +12 -2
  482. package/dist/trajectory/helpers.js +68 -37
  483. package/dist/trajectory/index.d.ts +1 -0
  484. package/dist/trajectory/index.js +3 -0
  485. package/dist/trajectory/parse/ase.d.ts +9 -1
  486. package/dist/trajectory/parse/ase.js +47 -32
  487. package/dist/trajectory/parse/diagnostics.d.ts +3 -0
  488. package/dist/trajectory/parse/diagnostics.js +14 -0
  489. package/dist/trajectory/parse/h5-utils.d.ts +13 -0
  490. package/dist/trajectory/parse/h5-utils.js +109 -0
  491. package/dist/trajectory/parse/hdf5.d.ts +1 -1
  492. package/dist/trajectory/parse/hdf5.js +116 -106
  493. package/dist/trajectory/parse/index.d.ts +3 -4
  494. package/dist/trajectory/parse/index.js +68 -119
  495. package/dist/trajectory/parse/lammps.d.ts +0 -2
  496. package/dist/trajectory/parse/lammps.js +14 -16
  497. package/dist/trajectory/parse/pymatgen.d.ts +2 -0
  498. package/dist/trajectory/parse/pymatgen.js +74 -0
  499. package/dist/trajectory/parse/vasp.js +13 -9
  500. package/dist/trajectory/parse/vaspout-electronic.d.ts +13 -0
  501. package/dist/trajectory/parse/vaspout-electronic.js +207 -0
  502. package/dist/trajectory/parse/vaspout-h5.d.ts +5 -0
  503. package/dist/trajectory/parse/vaspout-h5.js +284 -0
  504. package/dist/trajectory/parse/xyz.d.ts +9 -21
  505. package/dist/trajectory/parse/xyz.js +38 -39
  506. package/dist/trajectory/plotting.d.ts +8 -10
  507. package/dist/trajectory/plotting.js +102 -127
  508. package/dist/utils.d.ts +4 -0
  509. package/dist/utils.js +8 -1
  510. package/dist/xrd/XrdPlot.svelte +56 -85
  511. package/dist/xrd/broadening.d.ts +2 -1
  512. package/dist/xrd/broadening.js +2 -1
  513. package/dist/xrd/calc-xrd.js +8 -15
  514. package/dist/xrd/index.d.ts +2 -2
  515. package/dist/xrd/parse.d.ts +0 -1
  516. package/dist/xrd/parse.js +56 -73
  517. package/package.json +77 -36
  518. package/dist/api/mp.d.ts +0 -6
  519. package/dist/api/mp.js +0 -22
  520. package/dist/element/data.json +0 -11864
  521. package/dist/fermi-surface/marching-cubes.d.ts +0 -2
  522. package/dist/fermi-surface/marching-cubes.js +0 -2
  523. package/dist/plot/core/hover-lock.svelte.d.ts +0 -14
  524. package/dist/plot/core/hover-lock.svelte.js +0 -45
  525. package/dist/plot/sunburst/SunburstControls.svelte +0 -200
  526. package/dist/structure/label-placement.d.ts +0 -14
  527. package/dist/structure/label-placement.js +0 -72
@@ -1,15 +1,17 @@
1
1
  <script lang="ts">
2
- import type { ShowControlsProp } from '../controls'
3
- import { normalize_show_controls } from '../controls'
2
+ import { normalize_show_controls, type ShowControlsProp } from '../controls'
4
3
  import type { ElementSymbol } from '../element'
5
4
  import EmptyState from '../EmptyState.svelte'
5
+ import { StatusMessage } from '../feedback'
6
6
  import Spinner from '../feedback/Spinner.svelte'
7
7
  import Icon from '../Icon.svelte'
8
- import { handle_url_drop, load_from_url } from '../io'
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
+ import type { Vec2 } from '../math'
11
12
  import { sanitize_html } from '../sanitize'
12
- import { toggle_fullscreen } from '../layout'
13
+ import { FullscreenButton, type FullscreenToggleProp, toggle_fullscreen } from '../layout'
14
+ import { sync_fullscreen } from '../layout/fullscreen.svelte'
13
15
  import type { ControlsConfig, DataSeries, Orientation, Point } from '../plot'
14
16
  import type { ScatterHandlerProps } from '../plot/core/types'
15
17
  import { Histogram, ScatterPlot } from '../plot'
@@ -18,18 +20,25 @@
18
20
  import Structure from '../structure/Structure.svelte'
19
21
  import { scaleLinear } from 'd3-scale'
20
22
  import type { ComponentProps, Snippet } from 'svelte'
21
- import { untrack } from 'svelte'
23
+ import { onMount, untrack } from 'svelte'
22
24
  import { tooltip } from 'svelte-multiselect/attachments'
23
25
  import type { HTMLAttributes } from 'svelte/elements'
26
+ import { SvelteSet } from 'svelte/reactivity'
24
27
  import { full_data_extractor } from './extract'
25
28
  import type {
26
29
  ParseProgress,
27
30
  TrajectoryDataExtractor,
28
31
  TrajectoryFrame,
32
+ TrajectoryMetadata,
29
33
  TrajectoryType,
30
34
  TrajHandlerData,
31
35
  } from './index'
32
- import { TrajectoryError, TrajectoryExportPane, TrajectoryInfoPane } from './index'
36
+ import {
37
+ FRAME_LOAD_DEBOUNCE_MS,
38
+ TrajectoryError,
39
+ TrajectoryExportPane,
40
+ TrajectoryInfoPane,
41
+ } from './index'
33
42
  import type { AtomTypeMapping, LoadingOptions } from './parse'
34
43
  import {
35
44
  get_unsupported_format_message,
@@ -39,6 +48,7 @@
39
48
  } from './parse'
40
49
  import {
41
50
  generate_axis_labels,
51
+ generate_axis_scale_types,
42
52
  generate_plot_series,
43
53
  generate_streaming_plot_series,
44
54
  should_hide_plot,
@@ -62,6 +72,12 @@
62
72
  total_frames: number
63
73
  on_step_change: (idx: number) => void
64
74
  }
75
+ type PlotMetadataStreamMessage = {
76
+ command?: string
77
+ file_path?: string
78
+ plot_metadata?: TrajectoryMetadata[]
79
+ is_complete?: boolean
80
+ }
65
81
 
66
82
  let {
67
83
  trajectory = $bindable(),
@@ -99,116 +115,120 @@
99
115
  atom_type_mapping,
100
116
  plot_skimming = true,
101
117
  hovered = $bindable(false),
118
+ controls_open = $bindable(false),
119
+ info_pane_open = $bindable(false),
120
+ wrapper = $bindable(),
121
+ fullscreen = $bindable(false),
102
122
  ...rest
103
- }: EventHandlers & HTMLAttributes<HTMLDivElement> & {
104
- // trajectory data - can be provided directly or loaded from file
105
- trajectory?: TrajectoryType
106
- // URL to load trajectory from (alternative to providing trajectory directly)
107
- data_url?: string
108
- // current step index being displayed
109
- current_step_idx?: number
110
- // custom function to extract plot data from trajectory frames
111
- data_extractor?: TrajectoryDataExtractor
112
-
113
- // file drop handlers
114
- allow_file_drop?: boolean
115
- // layout configuration - 'auto' (default) adapts to element size, 'horizontal'/'vertical' forces layout
116
- layout?: `auto` | Orientation
117
- // structure viewer props (passed to Structure component)
118
- structure_props?: ComponentProps<typeof Structure>
119
- // plot props (passed to ScatterPlot component)
120
- scatter_props?: ComponentProps<typeof ScatterPlot>
121
- // histogram props (passed to Histogram component, excluding series which is handled separately)
122
- histogram_props?: Omit<ComponentProps<typeof Histogram>, `series`>
123
- // spinner props (passed to Spinner component)
124
- spinner_props?: ComponentProps<typeof Spinner>
125
- // custom snippets for additional UI elements
126
- trajectory_controls?: Snippet<[ControlsProps]>
127
- // Custom error snippet for advanced error handling
128
- error_snippet?: Snippet<[{ error_msg: string; on_dismiss: () => void }]>
129
- // Controls visibility configuration.
130
- // - 'always': controls always visible
131
- // - 'hover': controls visible on component hover (default)
132
- // - 'never': controls never visible
133
- // - object: { mode, hidden, style } for fine-grained control
134
- // Control names: 'filename', 'nav', 'step', 'fps', 'info-pane', 'export-pane', 'view-mode', 'fullscreen'
135
- show_controls?: ShowControlsProp
136
- // show/hide the fullscreen button
137
- fullscreen_toggle?: Snippet<[{ fullscreen: boolean }]> | boolean
138
- // automatically start playing when trajectory data is loaded
139
- auto_play?: boolean
140
- // display mode: 'structure+scatter' (default), 'structure' (only structure), 'scatter' (only scatter), 'histogram' (only histogram), 'structure+histogram' (structure with histogram)
141
- display_mode?:
142
- | `structure+scatter`
143
- | `structure`
144
- | `scatter`
145
- | `histogram`
146
- | `structure+histogram`
147
- // step labels configuration for slider
148
- // - positive number: number of evenly spaced ticks
149
- // - negative number: spacing between ticks (e.g. -10 = every 10th step)
150
- // - array: exact step indices to label
151
- // - undefined: no labels
152
- step_labels?: number | number[]
153
- // visible properties - bindable array of property keys currently shown in the plot
154
- // - controls which trajectory properties are plotted (e.g. ['energy', 'volume', 'force_max'])
155
- // - bindable: reflects current visibility state and can be used for external control
156
- // - if not provided, uses default visible properties (energy, force_max, stress_frobenius)
157
- // - if specified properties don't exist in data, falls back to automatic selection
158
- visible_properties?: string[]
159
- // custom labels for trajectory properties - maps property keys to display labels
160
- // - e.g. {energy: 'Total Energy', volume: 'Cell Volume', force_max: 'Max Force'}
161
- // - merged with built-in trajectory_property_config
162
- ELEM_PROPERTY_LABELS?: Record<string, string>
163
- // units configuration - developers can override these (deprecated - use ELEM_PROPERTY_LABELS instead)
164
- units?: {
165
- energy?: string
166
- energy_per_atom?: string
167
- force_max?: string
168
- force_norm?: string
169
- stress_max?: string
170
- volume?: string
171
- density?: string
172
- temperature?: string
173
- pressure?: string
174
- length?: string
175
- a?: string
176
- b?: string
177
- c?: string
178
- [key: string]: string | undefined
179
- }
180
- fps_range?: [number, number] // allowed FPS range [min_fps, max_fps]
181
- fps?: number // frame rate for playback
182
- // Loading options for large files
183
- loading_options?: LoadingOptions
184
- // Map LAMMPS atom types to element symbols (e.g. {1: 'Na', 2: 'Cl'})
185
- atom_type_mapping?: AtomTypeMapping
186
- // Disable plot skimming (mouse over plot doesn't update structure/step slider)
187
- plot_skimming?: boolean
188
- // bindable: true while the pointer is over the viewer (drives hover-scoped shortcuts)
189
- hovered?: boolean
190
- } = $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()
191
203
 
192
204
  let dragover = $state(false)
193
205
  let loading = $state(false)
194
206
  let error_msg = $state<string | null>(null)
207
+ // Non-fatal parse warnings from trajectory metadata (set by parser via attach_parse_warnings); dismissible
208
+ let parse_warning_msg = $state<string | undefined>(undefined)
209
+ $effect(() => {
210
+ const warnings = trajectory?.metadata?.parse_warnings
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
217
+ })
195
218
  let is_playing = $state(false)
196
- 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
197
222
 
198
223
  // Ensure fps is within the allowed range
199
224
  $effect(() => {
200
- if (fps < fps_range[0]) {
201
- fps = fps_range[0]
202
- } else if (fps > fps_range[1]) {
203
- fps = fps_range[1]
204
- }
225
+ const clamped = Math.max(fps_range[0], Math.min(fps_range[1], fps))
226
+ if (clamped !== fps) fps = clamped
205
227
  })
206
228
  let current_filename = $state<string | undefined>(undefined)
207
229
  let current_file_path = $state<string | null>(null)
208
230
  let file_size = $state<number | undefined>(undefined)
209
231
  let file_object = $state<File | null>(null)
210
- let wrapper = $state<HTMLDivElement | undefined>(undefined)
211
- let info_pane_open = $state(false)
212
232
  let parsing_progress = $state<ParseProgress | null>(null)
213
233
  let element_size = $state({ width: 0, height: 0 })
214
234
  let filename_copied = $state(false)
@@ -227,13 +247,26 @@
227
247
  })
228
248
 
229
249
  // Get total frame count (supports both regular and indexed trajectories)
230
- let total_frames = $derived(
231
- trajectory?.total_frames || trajectory?.frames.length || 0,
232
- )
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
+ })
233
265
 
234
266
  // Current frame - load on demand for indexed trajectories
235
267
  let current_frame = $state<TrajectoryFrame | null>(null)
236
268
  let frame_load_request_id = 0
269
+ let frame_load_timeout: ReturnType<typeof setTimeout> | undefined
237
270
 
238
271
  // Auto-play when trajectory changes (handles both props and file loading)
239
272
  $effect(() => {
@@ -247,8 +280,9 @@
247
280
  if (trajectory && current_step_idx >= 0 && current_step_idx < total_frames) {
248
281
  if (trajectory.frame_loader) {
249
282
  // Load frame on demand (works for both indexed files and external streaming)
250
- load_frame_on_demand(current_step_idx)
283
+ schedule_frame_load_on_demand(current_step_idx)
251
284
  } else {
285
+ clear_frame_load_timeout()
252
286
  // Use in-memory frame for regular trajectories
253
287
  current_frame = trajectory.frames[current_step_idx] || null
254
288
  }
@@ -257,11 +291,169 @@
257
291
  }
258
292
  })
259
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
+
260
451
  // Load frame on demand - works for both indexed files and external streaming
261
452
  async function load_frame_on_demand(frame_idx: number) {
262
453
  const load_trajectory = trajectory
263
454
  const frame_loader = load_trajectory?.frame_loader
264
455
  if (!load_trajectory || !frame_loader) return
456
+ ensure_frame_cache_owner()
265
457
 
266
458
  const request_id = ++frame_load_request_id
267
459
  const request_is_current = () =>
@@ -269,13 +461,18 @@
269
461
  trajectory === load_trajectory &&
270
462
  current_step_idx === frame_idx
271
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
272
466
  try {
273
467
  const frame = await frame_loader.load_frame(
274
- 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
275
469
  frame_idx,
276
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)
277
473
  if (!request_is_current()) return
278
474
  current_frame = frame
475
+ prefetch_frames(frame_idx) // warm upcoming frames for smooth playback/scrub
279
476
  } catch (error) {
280
477
  if (!request_is_current()) return
281
478
  console.error(`Failed to load frame ${frame_idx}:`, error)
@@ -287,6 +484,8 @@
287
484
  step_idx: frame_idx,
288
485
  frame_count: total_frames,
289
486
  })
487
+ } finally {
488
+ inflight_frames.delete(frame_idx)
290
489
  }
291
490
  }
292
491
 
@@ -294,7 +493,7 @@
294
493
  let current_structure = $derived(current_frame?.structure)
295
494
 
296
495
  // Track hidden elements (persists across frame changes)
297
- let hidden_elements = $state(new Set<ElementSymbol>())
496
+ let hidden_elements = $state(new SvelteSet<ElementSymbol>())
298
497
 
299
498
  let step_label_positions = $derived.by((): number[] => {
300
499
  if (!step_labels || total_frames <= 1) return []
@@ -305,11 +504,14 @@
305
504
 
306
505
  if (typeof step_labels === `number`) {
307
506
  if (step_labels > 0) {
308
- return scaleLinear().domain([0, total_frames - 1]).nice()
507
+ return scaleLinear()
508
+ .domain([0, total_frames - 1])
509
+ .nice()
309
510
  .ticks(Math.min(step_labels, total_frames))
310
511
  .map((tick) => Math.round(tick))
311
- .filter((tick, idx, ticks) =>
312
- 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,
313
515
  )
314
516
  }
315
517
  if (step_labels < 0) {
@@ -330,11 +532,13 @@
330
532
  let extended_config = $derived.by(() => {
331
533
  if (!ELEM_PROPERTY_LABELS) return trajectory_property_config
332
534
 
333
- const custom_config: Record<string, { label: string; unit: string }> = {}
535
+ const custom_config: Record<string, TrajPropertyConfig> = {}
334
536
  for (const [key, label] of Object.entries(ELEM_PROPERTY_LABELS)) {
335
- const existing = trajectory_property_config[key] ||
336
- trajectory_property_config[key.toLowerCase()]
337
- 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 || `` }
338
542
  }
339
543
  return { ...trajectory_property_config, ...custom_config }
340
544
  })
@@ -387,7 +591,8 @@
387
591
 
388
592
  // Only update if changed (use untrack to avoid circular dependency)
389
593
  const current = untrack(() => visible_properties) || []
390
- const has_changed = visible_keys.length !== current.length ||
594
+ const has_changed =
595
+ visible_keys.length !== current.length ||
391
596
  !visible_keys.every((key, idx) => key === current[idx])
392
597
 
393
598
  if (has_changed) {
@@ -402,27 +607,35 @@
402
607
  plot_series = toggle_series_visibility(plot_series, series_idx)
403
608
  }
404
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`)
405
612
  let x_axis = $derived({
406
- label: `Step`,
407
- 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`,
408
616
  ticks: step_label_positions,
409
617
  })
410
618
  // Generate axis labels based on first visible series on each axis
411
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))
412
623
  let y_axis = $derived({
413
624
  label: y_axis_labels.y1,
414
- format: `.2~s`,
625
+ format: `~g`,
415
626
  label_shift: { y: 10 },
627
+ scale_type: y_axis_scale_types.y1,
416
628
  })
417
629
  let y2_axis = $derived({
418
630
  label: y_axis_labels.y2,
419
- format: `.2~s`,
631
+ format: `~g`,
420
632
  label_shift: { y: 80 },
633
+ scale_type: y_axis_scale_types.y2,
421
634
  })
422
-
423
635
  // hide plot if all plotted values are constant (no variation)
424
636
  let show_plot = $derived(
425
- display_mode !== `structure` && !should_hide_plot(trajectory, plot_series),
637
+ display_mode !== `structure` &&
638
+ (plot_metadata_loading || !should_hide_plot(trajectory, plot_series)),
426
639
  )
427
640
 
428
641
  // Determine what to show based on display mode
@@ -430,23 +643,28 @@
430
643
  let actual_show_plot = $derived(display_mode !== `structure` && show_plot)
431
644
 
432
645
  // Check if there are any Y2 series to determine padding
433
- let has_y2_series = $derived(
434
- plot_series.some((srs) => srs.y_axis === `y2` && srs.visible),
435
- )
436
-
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
+ }
437
662
  // Step navigation functions
438
663
  function next_step() {
439
664
  if (current_step_idx < total_frames - 1) {
440
665
  current_step_idx++
441
666
  // Streaming frame loading handled by reactive effect
442
- if (trajectory) {
443
- on_step_change?.({
444
- trajectory,
445
- step_idx: current_step_idx,
446
- frame_count: total_frames,
447
- frame: current_frame || undefined,
448
- })
449
- }
667
+ notify_step_change()
450
668
  }
451
669
  }
452
670
 
@@ -454,14 +672,7 @@
454
672
  if (current_step_idx > 0) {
455
673
  current_step_idx--
456
674
  // Streaming frame loading handled by reactive effect
457
- if (trajectory) {
458
- on_step_change?.({
459
- trajectory,
460
- step_idx: current_step_idx,
461
- frame_count: total_frames,
462
- frame: current_frame || undefined,
463
- })
464
- }
675
+ notify_step_change()
465
676
  }
466
677
  }
467
678
 
@@ -470,14 +681,7 @@
470
681
  current_step_idx = idx
471
682
  // Note: streaming frame loading is handled by reactive effect
472
683
  // Handle callbacks for both traditional and streaming modes
473
- if (trajectory) {
474
- on_step_change?.({
475
- trajectory,
476
- step_idx: current_step_idx,
477
- frame_count: total_frames,
478
- frame: current_frame || undefined,
479
- })
480
- }
684
+ notify_step_change()
481
685
  }
482
686
  }
483
687
 
@@ -496,7 +700,7 @@
496
700
  reader.addEventListener(`error`, () => reject(new Error(`Failed to read file`)))
497
701
 
498
702
  // Read as text for text-based formats, binary for others
499
- if (file.name.toLowerCase().match(/\.(xyz|json|extxyz|lammpstrj)$/)) {
703
+ if (/\.(?:xyz|json|extxyz|lammpstrj)$/.test(file.name.toLowerCase())) {
500
704
  reader.readAsText(file)
501
705
  } else reader.readAsArrayBuffer(file)
502
706
  })
@@ -523,44 +727,50 @@
523
727
  })
524
728
  }
525
729
  }
526
- $effect(() => { // Effect to manage playback interval
527
- // Only watch is_playing and frame_rate_ms, not play_interval itself
528
- const playing = is_playing
529
- const rate_ms = 1000 / fps
530
-
531
- // Read current interval once (untrack to avoid circular dependency)
532
- const current_interval = untrack(() => play_interval)
533
- if (playing) {
534
- // Clear existing interval if it exists
535
- if (current_interval !== undefined) clearInterval(current_interval)
536
-
537
- // Create new interval with current frame rate
538
- play_interval = setInterval(() => {
539
- if (current_step_idx >= total_frames - 1) {
540
- if (trajectory) {
541
- on_end?.({
542
- trajectory,
543
- step_idx: current_step_idx,
544
- frame_count: total_frames,
545
- frame: current_frame || undefined,
546
- })
547
- }
548
- go_to_step(0) // Loop back to 1st step
549
- if (trajectory) {
550
- on_loop?.({ trajectory, frame_count: total_frames })
551
- }
552
- } else next_step()
553
- }, rate_ms)
554
- } else if (current_interval !== undefined) {
555
- // Clear interval when not playing
556
- clearInterval(current_interval)
557
- play_interval = undefined
558
- }
559
- })
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
+ }
560
745
 
561
- // Cleanup interval on component destroy
562
- $effect(() => () => {
563
- 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
+ }
564
774
  })
565
775
 
566
776
  // Handle internal file format drops
@@ -577,10 +787,7 @@
577
787
  const array_buffer = await response.arrayBuffer()
578
788
  await load_trajectory_data(array_buffer, file_info.name)
579
789
  } else {
580
- console.warn(
581
- `Binary file without ArrayBuffer or blob URL:`,
582
- file_info.name,
583
- )
790
+ console.warn(`Binary file without ArrayBuffer or blob URL:`, file_info.name)
584
791
  }
585
792
  } else {
586
793
  await load_trajectory_data(file_info.content, file_info.name)
@@ -602,9 +809,7 @@
602
809
 
603
810
  try {
604
811
  // Check for our custom internal file format first
605
- const internal_data = event.dataTransfer?.getData(
606
- `application/x-matterviz-file`,
607
- )
812
+ const internal_data = event.dataTransfer?.getData(`application/x-matterviz-file`)
608
813
  if (internal_data) {
609
814
  const handled = await handle_internal_file_drop(internal_data)
610
815
  if (handled) return
@@ -613,9 +818,8 @@
613
818
  // Handle URL-based files (e.g. from FilePicker)
614
819
  const handled = await handle_url_drop(event, async (content, filename) => {
615
820
  current_filename = filename
616
- file_size = content instanceof ArrayBuffer
617
- ? content.byteLength
618
- : new Blob([content]).size
821
+ file_size =
822
+ content instanceof ArrayBuffer ? content.byteLength : new Blob([content]).size
619
823
  await load_trajectory_data(content, filename)
620
824
  }).catch(() => false)
621
825
 
@@ -653,16 +857,16 @@
653
857
  }
654
858
  }
655
859
 
656
- $effect(() => { // Load trajectory from URL when data_url is provided
860
+ $effect(() => {
861
+ // Load trajectory from URL when data_url is provided
657
862
  if (data_url && !trajectory) {
658
863
  loading = true
659
864
  error_msg = null
660
865
 
661
866
  load_from_url(data_url, async (content, filename) => {
662
867
  current_filename = filename
663
- file_size = content instanceof ArrayBuffer
664
- ? content.byteLength
665
- : new Blob([content]).size
868
+ file_size =
869
+ content instanceof ArrayBuffer ? content.byteLength : new Blob([content]).size
666
870
  await load_trajectory_data(content, filename)
667
871
  })
668
872
  .then(() => {
@@ -700,30 +904,34 @@
700
904
  const data_size = data instanceof ArrayBuffer ? data.byteLength : data.length
701
905
 
702
906
  // Determine loading strategy based on file size
703
- const bin_file_threshold = loading_options.bin_file_threshold ??
704
- MAX_BIN_FILE_SIZE
705
- const text_file_threshold = loading_options.text_file_threshold ??
706
- 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
707
909
  if (
708
910
  (data instanceof ArrayBuffer && data_size > bin_file_threshold) ||
709
911
  (typeof data === `string` && data_size > text_file_threshold)
710
- ) { // Large files: Use indexed loading
912
+ ) {
913
+ // Large files: Use indexed loading
711
914
  await load_with_indexing(data, filename)
712
915
  } else {
713
916
  // Small files: Use regular loading
714
917
  const merged_options = { ...loading_options, atom_type_mapping }
715
- trajectory = await parse_trajectory_async(data, filename, (progress) => {
716
- parsing_progress = progress
717
- }, merged_options)
918
+ trajectory = await parse_trajectory_async(
919
+ data,
920
+ filename,
921
+ (progress) => {
922
+ parsing_progress = progress
923
+ },
924
+ merged_options,
925
+ )
718
926
  }
719
927
 
720
928
  current_step_idx = 0
721
929
  current_filename = filename
722
930
 
723
- const file_size_bytes = data instanceof ArrayBuffer
724
- ? data.byteLength
725
- : new Blob([data]).size
726
- 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
727
935
  trajectory,
728
936
  frame_count: trajectory?.frames.length ?? 0,
729
937
  total_atoms: trajectory?.frames[0]?.structure.sites.length ?? 0,
@@ -739,7 +947,8 @@
739
947
  current_filename = undefined
740
948
  file_size = undefined
741
949
 
742
- on_error?.({ // emit error event
950
+ on_error?.({
951
+ // emit error event
743
952
  error_msg,
744
953
  filename: current_filename || undefined,
745
954
  file_size: file_size || undefined,
@@ -752,15 +961,21 @@
752
961
 
753
962
  // Load using indexed parsing for large files
754
963
  async function load_with_indexing(data: string | ArrayBuffer, filename: string) {
755
- try { // Use indexed parsing for efficient large file handling
964
+ try {
965
+ // Use indexed parsing for efficient large file handling
756
966
  const merged_options = {
757
967
  use_indexing: true,
758
968
  ...loading_options,
759
969
  atom_type_mapping,
760
970
  }
761
- trajectory = await parse_trajectory_async(data, filename, (progress) => {
762
- parsing_progress = progress
763
- }, merged_options)
971
+ trajectory = await parse_trajectory_async(
972
+ data,
973
+ filename,
974
+ (progress) => {
975
+ parsing_progress = progress
976
+ },
977
+ merged_options,
978
+ )
764
979
 
765
980
  // Keep original data for on-demand frame loads only when indexed parsing attached a
766
981
  // frame_loader. Direct-parse fallbacks (e.g. large JSON or extensionless blob:
@@ -803,8 +1018,7 @@
803
1018
  // Don't handle shortcuts if user is typing in an input field (but allow if it's our step input and not focused)
804
1019
  const target = event.target instanceof HTMLElement ? event.target : null
805
1020
  const is_step_input = target?.classList.contains(`step-input`) ?? false
806
- const is_input_focused =
807
- target?.tagName === `INPUT` || target?.tagName === `TEXTAREA`
1021
+ const is_input_focused = target?.tagName === `INPUT` || target?.tagName === `TEXTAREA`
808
1022
 
809
1023
  // Skip if typing in an input that's not our step input
810
1024
  if (is_input_focused && !is_step_input) return false
@@ -843,7 +1057,7 @@
843
1057
  go_to_step(Math.max(0, current_step_idx - 25))
844
1058
  } else if (event.key === `PageDown`) {
845
1059
  go_to_step(Math.min(total_frames - 1, current_step_idx + 25))
846
- } // Interface shortcuts
1060
+ } // Interface shortcuts
847
1061
  else if (event.key === `f` && fullscreen_toggle) toggle_fullscreen(wrapper)
848
1062
  // 'i' key handled by the TrajectoryInfoPane's built-in toggle
849
1063
  // Playback speed shortcuts (only when playing)
@@ -853,14 +1067,14 @@
853
1067
  } else if (event.key === `-` && is_playing) {
854
1068
  fps = Math.max(fps_range[0], fps - 0.2)
855
1069
  on_frame_rate_change?.({ trajectory, fps })
856
- } // System shortcuts
1070
+ } // System shortcuts
857
1071
  else if (event.key === `Escape`) {
858
1072
  if (document.fullscreenElement) document.exitFullscreen()
859
1073
  else if (view_mode_dropdown_open) view_mode_dropdown_open = false
860
1074
  // Escape key for info pane handled by DraggablePane
861
- } // Number keys 0-9 - jump to percentage of trajectory
1075
+ } // Number keys 0-9 - jump to percentage of trajectory
862
1076
  else if (event.key >= `0` && event.key <= `9`) {
863
- 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)))
864
1078
  } else handled = false
865
1079
 
866
1080
  return handled
@@ -868,25 +1082,28 @@
868
1082
 
869
1083
  // Separate state variables for each pane to match component prop types
870
1084
  let structure_info_open = $state(false)
871
- let structure_controls_open = $state(false)
872
1085
  let scatter_controls = $state<ControlsConfig>({ open: false })
873
1086
  let trajectory_export_open = $state(false)
874
- let fullscreen = $state(false)
875
- </script>
876
1087
 
877
- <svelte:document
878
- onfullscreenchange={() => {
879
- fullscreen = !!document.fullscreenElement
880
- on_fullscreen_change?.({ trajectory, fullscreen })
881
- }}
882
- />
1088
+ sync_fullscreen({
1089
+ get_wrapper: () => wrapper,
1090
+ get_fullscreen: () => fullscreen,
1091
+ set_fullscreen: (val) => (fullscreen = val),
1092
+ bg_css_var: `--traj-bg-fullscreen`,
1093
+ on_change: (val) => on_fullscreen_change?.({ trajectory, fullscreen: val }),
1094
+ })
1095
+ </script>
883
1096
 
884
1097
  <svelte:window onkeydown={forward_window_keydown(() => hovered, onkeydown)} />
885
1098
 
886
1099
  <div
887
1100
  class:dragover
888
- class:active={is_playing || structure_info_open || structure_controls_open ||
889
- 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}
890
1107
  bind:this={wrapper}
891
1108
  bind:clientWidth={element_size.width}
892
1109
  bind:clientHeight={element_size.height}
@@ -896,21 +1113,17 @@
896
1113
  onmouseenter={() => (hovered = true)}
897
1114
  onmouseleave={() => (hovered = false)}
898
1115
  ondrop={handle_file_drop}
899
- ondragover={(event) => {
900
- event.preventDefault()
901
- if (!allow_file_drop) return
902
- dragover = true
903
- }}
904
- ondragleave={(event) => {
905
- event.preventDefault()
906
- dragover = false
907
- }}
1116
+ {...drag_over_handlers({
1117
+ allow: () => allow_file_drop,
1118
+ set_dragover: (over) => (dragover = over),
1119
+ })}
908
1120
  onclick={handle_click_outside}
909
1121
  onkeydown={handle_and_prevent(onkeydown)}
910
1122
  {...rest}
911
- class="trajectory {actual_layout} {rest.class ?? ``}"
1123
+ class={[`trajectory`, actual_layout, rest.class]}
912
1124
  class:show-both-views={[`structure+scatter`, `structure+histogram`].includes(display_mode) &&
913
- actual_show_plot && show_structure}
1125
+ actual_show_plot &&
1126
+ show_structure}
914
1127
  >
915
1128
  {#if loading}
916
1129
  {@const text = parsing_progress
@@ -922,25 +1135,26 @@
922
1135
  {...spinner_props}
923
1136
  />
924
1137
  {:else if error_msg}
925
- <TrajectoryError
926
- {error_msg}
927
- on_dismiss={() => (error_msg = null)}
928
- {error_snippet}
929
- />
1138
+ <TrajectoryError {error_msg} on_dismiss={() => (error_msg = null)} {error_snippet} />
930
1139
  {:else if trajectory}
1140
+ {#if parse_warning_msg}
1141
+ <StatusMessage
1142
+ bind:message={parse_warning_msg}
1143
+ type="warning"
1144
+ dismissible
1145
+ style="position: absolute; bottom: 4pt; left: 4pt; right: 4pt; z-index: 2; font-size: 0.85em"
1146
+ />
1147
+ {/if}
931
1148
  <!-- Trajectory Controls -->
932
1149
  {#if controls_config.mode !== `never`}
933
- <div
934
- class="trajectory-controls {controls_config.class}"
935
- style={controls_config.style}
936
- >
1150
+ <div class="trajectory-controls {controls_config.class}" style={controls_config.style}>
937
1151
  {#if trajectory_controls}
938
1152
  {@render trajectory_controls({
939
- trajectory,
940
- current_step_idx,
941
- total_frames: total_frames,
942
- on_step_change: go_to_step,
943
- })}
1153
+ trajectory,
1154
+ current_step_idx,
1155
+ total_frames: total_frames,
1156
+ on_step_change: go_to_step,
1157
+ })}
944
1158
  {:else}
945
1159
  {#if current_filename && controls_config.visible(`filename`)}
946
1160
  <button
@@ -951,7 +1165,7 @@
951
1165
  if (current_filename) {
952
1166
  navigator.clipboard.writeText(current_filename)
953
1167
  filename_copied = true
954
- setTimeout(() => filename_copied = false, 1000)
1168
+ setTimeout(() => (filename_copied = false), 1000)
955
1169
  }
956
1170
  }}
957
1171
  >
@@ -959,7 +1173,7 @@
959
1173
  {#if filename_copied}
960
1174
  <Icon
961
1175
  icon="Check"
962
- 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"
963
1177
  />
964
1178
  {/if}
965
1179
  </button>
@@ -971,14 +1185,16 @@
971
1185
  <button
972
1186
  onclick={prev_step}
973
1187
  disabled={current_step_idx === 0 || is_playing}
974
- title="Previous step"
1188
+ title="Previous step (←) · Home: first · j: −10 · PageUp: −25"
975
1189
  >
976
1190
 
977
1191
  </button>
978
1192
  <button
979
1193
  onclick={toggle_play}
980
1194
  disabled={total_frames <= 1}
981
- title={is_playing ? `Pause playback` : `Play trajectory`}
1195
+ title={`${
1196
+ is_playing ? `Pause` : `Play`
1197
+ } (Space) · ←/→ step · 0-9 jump % · +/- speed · f fullscreen`}
982
1198
  class="play-button"
983
1199
  class:playing={is_playing}
984
1200
  >
@@ -987,7 +1203,7 @@
987
1203
  <button
988
1204
  onclick={next_step}
989
1205
  disabled={current_step_idx === total_frames - 1 || is_playing}
990
- title="Next step"
1206
+ title="Next step (→) · End: last · l: +10 · PageDown: +25"
991
1207
  >
992
1208
 
993
1209
  </button>
@@ -1002,6 +1218,7 @@
1002
1218
  min="0"
1003
1219
  max={total_frames - 1}
1004
1220
  bind:value={current_step_idx}
1221
+ oninput={(event) => notify_step_change(event.currentTarget.valueAsNumber)}
1005
1222
  class="step-input"
1006
1223
  title="Enter step number to jump to"
1007
1224
  aria-label="Step input"
@@ -1014,15 +1231,15 @@
1014
1231
  min="0"
1015
1232
  max={total_frames - 1}
1016
1233
  bind:value={current_step_idx}
1234
+ oninput={(event) => notify_step_change(event.currentTarget.valueAsNumber)}
1017
1235
  class="step-slider"
1018
1236
  title="Drag to navigate steps"
1019
1237
  />
1020
1238
  {#if step_label_positions.length > 0}
1021
1239
  <div class="step-labels">
1022
1240
  {#each step_label_positions as step_idx (step_idx)}
1023
- {@const position_percent = total_frames > 1
1024
- ? (step_idx / (total_frames - 1)) * 100
1025
- : 0}
1241
+ {@const position_percent =
1242
+ total_frames > 1 ? (step_idx / (total_frames - 1)) * 100 : 0}
1026
1243
  {@const adjusted_position = 1.5 + (position_percent * (100 - 2)) / 100}
1027
1244
  <div class="step-tick" style:left="{adjusted_position}%"></div>
1028
1245
  <div class="step-label" style:left="{adjusted_position}%">
@@ -1035,8 +1252,8 @@
1035
1252
  </div>
1036
1253
  {/if}
1037
1254
 
1038
- <!-- Frame rate control - only shown when playing -->
1039
- {#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`)}
1040
1257
  <label
1041
1258
  class="fps-section"
1042
1259
  style="font-size: 0.9em; display: flex; align-items: center; gap: 5pt; margin-inline: 6pt"
@@ -1048,7 +1265,7 @@
1048
1265
  max={fps_range[1]}
1049
1266
  bind:value={fps}
1050
1267
  title="Frame rate: {format_num(fps, `.2~s`)} fps"
1051
- style="width: clamp(60px, 8cqw, 90px); accent-color: var(--accent-color)"
1268
+ style="width: clamp(60px, 8cqw, 90px)"
1052
1269
  />
1053
1270
  <input
1054
1271
  type="number"
@@ -1087,8 +1304,7 @@
1087
1304
  />
1088
1305
  {/if}
1089
1306
  <!-- Display mode dropdown -->
1090
- {#if plot_series.length > 0 &&
1091
- controls_config.visible(`view-mode`)}
1307
+ {#if plot_series.length > 0 && controls_config.visible(`view-mode`)}
1092
1308
  <div class="view-mode-dropdown-wrapper">
1093
1309
  <button
1094
1310
  onclick={() => (view_mode_dropdown_open = !view_mode_dropdown_open)}
@@ -1098,40 +1314,21 @@
1098
1314
  style="background-color: transparent; padding: 0"
1099
1315
  >
1100
1316
  <Icon
1101
- icon={({
1102
- structure: `Atom`,
1103
- 'structure+scatter': `TwoColumns`,
1104
- 'structure+histogram': `TwoColumns`,
1105
- scatter: `ScatterPlot`,
1106
- histogram: `Histogram`,
1107
- } 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]}
1108
1326
  />
1109
1327
  <Icon icon={view_mode_dropdown_open ? `ArrowUp` : `ArrowDown`} />
1110
1328
  </button>
1111
1329
  {#if view_mode_dropdown_open}
1112
1330
  <div class="view-mode-dropdown">
1113
- {#each [
1114
- { mode: `structure`, icon: `Atom`, label: `Structure-only` },
1115
- {
1116
- mode: `structure+scatter`,
1117
- icon: `TwoColumns`,
1118
- label: `Structure + Scatter`,
1119
- },
1120
- {
1121
- mode: `structure+histogram`,
1122
- icon: `TwoColumns`,
1123
- label: `Structure + Histogram`,
1124
- },
1125
- { mode: `scatter`, icon: `ScatterPlot`, label: `Scatter-only` },
1126
- {
1127
- mode: `histogram`,
1128
- icon: `Histogram`,
1129
- label: `Histogram-only`,
1130
- },
1131
- ] as const as
1132
- option
1133
- (option.mode)
1134
- }
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)}
1135
1332
  <button
1136
1333
  class="view-mode-option"
1137
1334
  class:selected={display_mode === option.mode}
@@ -1150,22 +1347,14 @@
1150
1347
  </div>
1151
1348
  {/if}
1152
1349
  <!-- Fullscreen button - rightmost position -->
1153
- {#if fullscreen_toggle &&
1154
- controls_config.visible(`fullscreen`)}
1155
- <button
1156
- type="button"
1157
- onclick={() => fullscreen_toggle && toggle_fullscreen(wrapper)}
1158
- title="{fullscreen ? `Exit` : `Enter`} fullscreen"
1350
+ {#if fullscreen_toggle && controls_config.visible(`fullscreen`)}
1351
+ <FullscreenButton
1352
+ {fullscreen}
1353
+ toggle={fullscreen_toggle}
1354
+ {wrapper}
1159
1355
  aria-label="{fullscreen ? `Exit` : `Enter`} fullscreen"
1160
- aria-pressed={fullscreen}
1161
1356
  class="fullscreen-button"
1162
- >
1163
- {#if typeof fullscreen_toggle === `function`}
1164
- {@render fullscreen_toggle({ fullscreen })}
1165
- {:else}
1166
- <Icon icon="{fullscreen ? `Exit` : ``}Fullscreen" />
1167
- {/if}
1168
- </button>
1357
+ />
1169
1358
  {/if}
1170
1359
  </div>
1171
1360
  {/if}
@@ -1189,14 +1378,18 @@
1189
1378
  show_image_atoms: false, // Default to false to avoid atoms popping in/out at cell edges
1190
1379
  ...structure_props,
1191
1380
  }}
1192
- bind:controls_open={structure_controls_open}
1381
+ bind:controls_open
1193
1382
  bind:info_pane_open={structure_info_open}
1194
1383
  bind:hidden_elements
1195
1384
  />
1196
1385
  {/if}
1197
1386
 
1198
1387
  {#if actual_show_plot}
1199
- {#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`}
1200
1393
  <ScatterPlot
1201
1394
  series={plot_series}
1202
1395
  {x_axis}
@@ -1205,27 +1398,22 @@
1205
1398
  controls={scatter_controls}
1206
1399
  current_x_value={current_step_idx}
1207
1400
  change={plot_skimming ? handle_plot_change : undefined}
1208
- 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 }}
1209
1402
  range_padding={0}
1210
1403
  style="height: 100%"
1211
1404
  {...scatter_props}
1212
1405
  legend={{
1213
- ...scatter_props.legend ?? {},
1406
+ ...(scatter_props.legend ?? {}),
1214
1407
  on_toggle: (series_idx: number) => {
1215
1408
  handle_legend_toggle(series_idx)
1216
1409
  scatter_props.legend?.on_toggle?.(series_idx)
1217
1410
  },
1218
1411
  }}
1219
- class="plot {scatter_props.class ?? ``}"
1412
+ class={[`plot`, scatter_props.class]}
1220
1413
  >
1221
- {#snippet tooltip({
1222
- x,
1223
- y,
1224
- metadata,
1225
- label,
1226
- }: ScatterHandlerProps)}
1414
+ {#snippet tooltip({ x, y, metadata, label }: ScatterHandlerProps)}
1227
1415
  {@const formatted_y = typeof y === `number` ? format_num(y) : y}
1228
- Step: {Math.round(x)}<br />
1416
+ {x_axis_quantity}: {Math.round(x)}<br />
1229
1417
  {@html sanitize_html(metadata?.series_label || label || `Value`)}: {formatted_y}
1230
1418
  {/snippet}
1231
1419
  </ScatterPlot>
@@ -1246,7 +1434,7 @@
1246
1434
  histogram_props.on_series_toggle?.(series_idx)
1247
1435
  }}
1248
1436
  style="height: 100%"
1249
- class="plot {histogram_props.class ?? ``}"
1437
+ class={[`plot`, histogram_props.class]}
1250
1438
  --ctrl-btn-top="6ex"
1251
1439
  >
1252
1440
  {#snippet tooltip({
@@ -1270,8 +1458,8 @@
1270
1458
  <EmptyState class="trajectory-empty-state">
1271
1459
  <h3 id="load-trajectory">Load Trajectory</h3>
1272
1460
  <p>
1273
- Drop a trajectory file here (.xyz, .extxyz, .json, .json.gz, XDATCAR, .traj, .h5)
1274
- 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
1275
1463
  </p>
1276
1464
  <strong style="display: block; margin-block: 1em 1ex">Supported formats:</strong>
1277
1465
  <ul>
@@ -1283,9 +1471,7 @@
1283
1471
  <li>HDF5 trajectory files (.h5, .hdf5)</li>
1284
1472
  <li>Compressed files (.gz)</li>
1285
1473
  </ul>
1286
- <p>
1287
- 💡 Force vectors will be automatically displayed when present in trajectory data
1288
- </p>
1474
+ <p>💡 Force vectors will be automatically displayed when present in trajectory data</p>
1289
1475
  </EmptyState>
1290
1476
  {/if}
1291
1477
  </div>
@@ -1303,9 +1489,6 @@
1303
1489
  contain: layout;
1304
1490
  z-index: var(--traj-z-index, 1);
1305
1491
  container-type: size; /* enable cqh for panes if explicit height is set */
1306
- :global(.plot) {
1307
- background: var(--surface-bg);
1308
- }
1309
1492
  &.active {
1310
1493
  z-index: 2; /* needed so info/control panes from an active viewer overlay those of the next (if there is one) */
1311
1494
  .trajectory-controls {
@@ -1316,7 +1499,7 @@
1316
1499
  height: 100vh !important;
1317
1500
  width: 100vw !important;
1318
1501
  border-radius: 0 !important;
1319
- background: var(--surface-bg);
1502
+ background: var(--traj-bg-fullscreen, var(--surface-bg));
1320
1503
  overflow: hidden;
1321
1504
  }
1322
1505
  &.horizontal .content-area {
@@ -1354,10 +1537,19 @@
1354
1537
  grid-template-rows: 1fr !important;
1355
1538
  }
1356
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
+ }
1357
1549
  .trajectory-controls {
1358
1550
  display: flex;
1359
1551
  align-items: center;
1360
- gap: clamp(2pt, 1cqw, 1ex);
1552
+ gap: clamp(4pt, 1.6cqw, 1.5ex);
1361
1553
  padding: clamp(2pt, 0.5cqw, 1ex) clamp(4pt, 1cqw, 1.2ex);
1362
1554
  background: var(--surface-bg-hover);
1363
1555
  backdrop-filter: blur(4px);
@@ -1377,7 +1569,7 @@
1377
1569
  }
1378
1570
  button {
1379
1571
  background: var(--btn-bg);
1380
- font-size: clamp(0.8rem, 2cqw, 1rem);
1572
+ font-size: var(--ctrl-btn-icon-size, clamp(0.7rem, 2cqmin, 0.85rem));
1381
1573
  &:hover:not(:disabled) {
1382
1574
  background: var(--btn-bg-hover);
1383
1575
  }
@@ -1393,7 +1585,14 @@
1393
1585
  .nav-section {
1394
1586
  display: flex;
1395
1587
  align-items: center;
1396
- 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);
1397
1596
  }
1398
1597
  .step-section {
1399
1598
  display: flex;
@@ -1415,7 +1614,8 @@
1415
1614
  }
1416
1615
  .step-slider {
1417
1616
  width: 100%;
1418
- 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) */
1419
1619
  }
1420
1620
  .step-labels {
1421
1621
  position: absolute;
@@ -1458,7 +1658,8 @@
1458
1658
  opacity: 0;
1459
1659
  }
1460
1660
  }
1461
- .fullscreen-button {
1661
+ /* :global pierces into FullscreenButton's markup (anchored on local .info-section) */
1662
+ .info-section :global(.fullscreen-button) {
1462
1663
  background: transparent !important;
1463
1664
  padding: 0;
1464
1665
  &:hover:not(:disabled) {
@@ -1472,7 +1673,7 @@
1472
1673
  position: relative;
1473
1674
  }
1474
1675
  .info-section :global(:is(.trajectory-info-toggle, .trajectory-export-toggle)) {
1475
- font-size: clamp(1rem, 2.2cqw, 1.1rem);
1676
+ font-size: var(--ctrl-btn-icon-size, clamp(0.7rem, 2cqmin, 0.85rem));
1476
1677
  }
1477
1678
  .play-button {
1478
1679
  min-width: clamp(32px, 4cqw, 36px);
@@ -1482,10 +1683,7 @@
1482
1683
  &.playing {
1483
1684
  background: var(--traj-pause-btn-bg, var(--btn-bg, rgba(0, 0, 0, 0.1)));
1484
1685
  &:hover:not(:disabled) {
1485
- background: var(
1486
- --traj-pause-btn-bg-hover,
1487
- var(--btn-bg-hover, rgba(0, 0, 0, 0.1))
1488
- );
1686
+ background: var(--traj-pause-btn-bg-hover, var(--btn-bg-hover, rgba(0, 0, 0, 0.1)));
1489
1687
  }
1490
1688
  }
1491
1689
  }
@@ -1533,14 +1731,20 @@
1533
1731
  .view-mode-dropdown-wrapper {
1534
1732
  display: flex;
1535
1733
  position: relative;
1734
+ z-index: var(--trajectory-view-mode-z-index, 20);
1536
1735
  }
1537
1736
  .view-mode-dropdown {
1538
1737
  position: absolute;
1539
1738
  top: 115%;
1540
1739
  right: 0;
1541
- background: var(--surface-bg);
1740
+ z-index: 1;
1741
+ min-width: max-content;
1742
+ background: var(--trajectory-view-mode-bg, light-dark(#fff, #2f3137));
1542
1743
  border-radius: 4px;
1543
- 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;
1544
1748
  }
1545
1749
  .view-mode-option {
1546
1750
  display: flex;