matterviz 0.3.1 → 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (358) hide show
  1. package/dist/EmptyState.svelte +10 -2
  2. package/dist/FilePicker.svelte +154 -96
  3. package/dist/Icon.svelte +20 -14
  4. package/dist/MillerIndexInput.svelte +27 -21
  5. package/dist/api/optimade.js +6 -6
  6. package/dist/app.css +216 -178
  7. package/dist/brillouin/BrillouinZone.svelte +299 -198
  8. package/dist/brillouin/BrillouinZone.svelte.d.ts +1 -1
  9. package/dist/brillouin/BrillouinZoneControls.svelte +32 -5
  10. package/dist/brillouin/BrillouinZoneExportPane.svelte +74 -55
  11. package/dist/brillouin/BrillouinZoneExportPane.svelte.d.ts +1 -1
  12. package/dist/brillouin/BrillouinZoneInfoPane.svelte +99 -68
  13. package/dist/brillouin/BrillouinZoneScene.svelte +277 -165
  14. package/dist/brillouin/BrillouinZoneScene.svelte.d.ts +1 -1
  15. package/dist/brillouin/BrillouinZoneTooltip.svelte +17 -7
  16. package/dist/brillouin/compute.js +11 -6
  17. package/dist/chempot-diagram/ChemPotDiagram.svelte +327 -0
  18. package/dist/chempot-diagram/ChemPotDiagram.svelte.d.ts +13 -0
  19. package/dist/chempot-diagram/ChemPotDiagram2D.svelte +847 -0
  20. package/dist/chempot-diagram/ChemPotDiagram2D.svelte.d.ts +16 -0
  21. package/dist/chempot-diagram/ChemPotDiagram3D.svelte +3194 -0
  22. package/dist/chempot-diagram/ChemPotDiagram3D.svelte.d.ts +16 -0
  23. package/dist/chempot-diagram/ChemPotScene3D.svelte +11 -0
  24. package/dist/chempot-diagram/ChemPotScene3D.svelte.d.ts +7 -0
  25. package/dist/chempot-diagram/async-compute.svelte.d.ts +3 -0
  26. package/dist/chempot-diagram/async-compute.svelte.js +77 -0
  27. package/dist/chempot-diagram/chempot-worker.d.ts +1 -0
  28. package/dist/chempot-diagram/chempot-worker.js +11 -0
  29. package/dist/chempot-diagram/color.d.ts +10 -0
  30. package/dist/chempot-diagram/color.js +32 -0
  31. package/dist/chempot-diagram/compute.d.ts +48 -0
  32. package/dist/chempot-diagram/compute.js +812 -0
  33. package/dist/chempot-diagram/index.d.ts +6 -0
  34. package/dist/chempot-diagram/index.js +6 -0
  35. package/dist/chempot-diagram/pointer.d.ts +16 -0
  36. package/dist/chempot-diagram/pointer.js +40 -0
  37. package/dist/chempot-diagram/temperature.d.ts +15 -0
  38. package/dist/chempot-diagram/temperature.js +36 -0
  39. package/dist/chempot-diagram/types.d.ts +86 -0
  40. package/dist/chempot-diagram/types.js +28 -0
  41. package/dist/colors/index.d.ts +3 -1
  42. package/dist/colors/index.js +9 -3
  43. package/dist/composition/BarChart.svelte +141 -77
  44. package/dist/composition/BubbleChart.svelte +107 -52
  45. package/dist/composition/Composition.svelte +100 -79
  46. package/dist/composition/Formula.svelte +108 -62
  47. package/dist/composition/FormulaFilter.svelte +973 -353
  48. package/dist/composition/FormulaFilter.svelte.d.ts +35 -1
  49. package/dist/composition/PieChart.svelte +199 -99
  50. package/dist/composition/PieChart.svelte.d.ts +1 -1
  51. package/dist/composition/format.d.ts +5 -0
  52. package/dist/composition/format.js +20 -3
  53. package/dist/composition/parse.js +14 -9
  54. package/dist/convex-hull/ConvexHull.svelte +93 -38
  55. package/dist/convex-hull/ConvexHull2D.svelte +551 -393
  56. package/dist/convex-hull/ConvexHull3D.svelte +1303 -825
  57. package/dist/convex-hull/ConvexHull4D.svelte +1012 -686
  58. package/dist/convex-hull/ConvexHullControls.svelte +115 -28
  59. package/dist/convex-hull/ConvexHullInfoPane.svelte +29 -3
  60. package/dist/convex-hull/ConvexHullStats.svelte +821 -249
  61. package/dist/convex-hull/ConvexHullStats.svelte.d.ts +6 -1
  62. package/dist/convex-hull/ConvexHullTooltip.svelte +41 -16
  63. package/dist/convex-hull/GasPressureControls.svelte +104 -61
  64. package/dist/convex-hull/StructurePopup.svelte +25 -4
  65. package/dist/convex-hull/TemperatureSlider.svelte +45 -25
  66. package/dist/convex-hull/barycentric-coords.js +13 -7
  67. package/dist/convex-hull/demo-temperature.d.ts +6 -0
  68. package/dist/convex-hull/demo-temperature.js +40 -0
  69. package/dist/convex-hull/gas-thermodynamics.js +17 -12
  70. package/dist/convex-hull/helpers.d.ts +10 -1
  71. package/dist/convex-hull/helpers.js +79 -38
  72. package/dist/convex-hull/index.d.ts +1 -0
  73. package/dist/convex-hull/index.js +1 -0
  74. package/dist/convex-hull/thermodynamics.d.ts +8 -21
  75. package/dist/convex-hull/thermodynamics.js +163 -69
  76. package/dist/convex-hull/types.d.ts +12 -12
  77. package/dist/convex-hull/types.js +0 -12
  78. package/dist/coordination/CoordinationBarPlot.svelte +232 -176
  79. package/dist/element/BohrAtom.svelte +56 -13
  80. package/dist/element/ElementHeading.svelte +7 -2
  81. package/dist/element/ElementPhoto.svelte +15 -9
  82. package/dist/element/ElementStats.svelte +10 -4
  83. package/dist/element/ElementTile.svelte +137 -73
  84. package/dist/element/Nucleus.svelte +39 -11
  85. package/dist/element/data.js +2 -14
  86. package/dist/element/data.json.gz +0 -0
  87. package/dist/element/types.d.ts +1 -0
  88. package/dist/feedback/ClickFeedback.svelte +16 -5
  89. package/dist/feedback/DragOverlay.svelte +10 -2
  90. package/dist/feedback/Spinner.svelte +4 -2
  91. package/dist/feedback/StatusMessage.svelte +8 -2
  92. package/dist/fermi-surface/FermiSlice.svelte +118 -88
  93. package/dist/fermi-surface/FermiSurface.svelte +336 -239
  94. package/dist/fermi-surface/FermiSurface.svelte.d.ts +1 -1
  95. package/dist/fermi-surface/FermiSurfaceControls.svelte +113 -46
  96. package/dist/fermi-surface/FermiSurfaceScene.svelte +536 -343
  97. package/dist/fermi-surface/FermiSurfaceScene.svelte.d.ts +1 -1
  98. package/dist/fermi-surface/FermiSurfaceTooltip.svelte +14 -5
  99. package/dist/fermi-surface/compute.js +16 -20
  100. package/dist/fermi-surface/parse.js +37 -33
  101. package/dist/fermi-surface/symmetry.js +2 -7
  102. package/dist/fermi-surface/types.d.ts +3 -5
  103. package/dist/heatmap-matrix/HeatmapMatrix.svelte +1527 -0
  104. package/dist/heatmap-matrix/HeatmapMatrix.svelte.d.ts +110 -0
  105. package/dist/heatmap-matrix/HeatmapMatrixControls.svelte +225 -0
  106. package/dist/heatmap-matrix/HeatmapMatrixControls.svelte.d.ts +30 -0
  107. package/dist/heatmap-matrix/index.d.ts +53 -0
  108. package/dist/heatmap-matrix/index.js +100 -0
  109. package/dist/heatmap-matrix/shared.d.ts +2 -0
  110. package/dist/heatmap-matrix/shared.js +4 -0
  111. package/dist/icons.d.ts +111 -0
  112. package/dist/icons.js +158 -0
  113. package/dist/index.d.ts +5 -2
  114. package/dist/index.js +5 -2
  115. package/dist/io/decompress.js +1 -1
  116. package/dist/io/export.d.ts +3 -0
  117. package/dist/io/export.js +138 -140
  118. package/dist/io/file-drop.d.ts +7 -0
  119. package/dist/io/file-drop.js +43 -0
  120. package/dist/io/index.d.ts +2 -2
  121. package/dist/io/index.js +2 -112
  122. package/dist/io/is-binary.js +2 -3
  123. package/dist/io/types.d.ts +1 -0
  124. package/dist/io/url-drop.d.ts +2 -0
  125. package/dist/io/url-drop.js +117 -0
  126. package/dist/isosurface/Isosurface.svelte +220 -110
  127. package/dist/isosurface/IsosurfaceControls.svelte +65 -28
  128. package/dist/isosurface/parse.js +104 -56
  129. package/dist/isosurface/slice.d.ts +2 -1
  130. package/dist/isosurface/slice.js +8 -13
  131. package/dist/isosurface/types.d.ts +14 -1
  132. package/dist/isosurface/types.js +152 -5
  133. package/dist/labels.d.ts +2 -1
  134. package/dist/labels.js +12 -8
  135. package/dist/layout/FullscreenToggle.svelte +11 -2
  136. package/dist/layout/InfoCard.svelte +38 -6
  137. package/dist/layout/InfoTag.svelte +125 -94
  138. package/dist/layout/PropertyFilter.svelte +82 -37
  139. package/dist/layout/SettingsSection.svelte +85 -55
  140. package/dist/layout/SubpageGrid.svelte +82 -0
  141. package/dist/layout/SubpageGrid.svelte.d.ts +14 -0
  142. package/dist/layout/index.d.ts +1 -0
  143. package/dist/layout/index.js +1 -0
  144. package/dist/layout/json-tree/JsonNode.svelte +266 -223
  145. package/dist/layout/json-tree/JsonTree.svelte +516 -429
  146. package/dist/layout/json-tree/JsonTree.svelte.d.ts +1 -1
  147. package/dist/layout/json-tree/JsonValue.svelte +281 -173
  148. package/dist/layout/json-tree/types.d.ts +10 -2
  149. package/dist/layout/json-tree/utils.d.ts +2 -0
  150. package/dist/layout/json-tree/utils.js +37 -2
  151. package/dist/marching-cubes.js +25 -2
  152. package/dist/math.d.ts +20 -17
  153. package/dist/math.js +474 -57
  154. package/dist/overlays/ContextMenu.svelte +66 -40
  155. package/dist/overlays/DraggablePane.svelte +331 -154
  156. package/dist/overlays/DraggablePane.svelte.d.ts +2 -0
  157. package/dist/periodic-table/PeriodicTable.svelte +278 -145
  158. package/dist/periodic-table/PeriodicTableControls.svelte +178 -128
  159. package/dist/periodic-table/PropertySelect.svelte +25 -7
  160. package/dist/periodic-table/TableInset.svelte +8 -3
  161. package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte +559 -267
  162. package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte.d.ts +6 -2
  163. package/dist/phase-diagram/PhaseDiagramControls.svelte +131 -51
  164. package/dist/phase-diagram/PhaseDiagramControls.svelte.d.ts +3 -2
  165. package/dist/phase-diagram/PhaseDiagramEditorPane.svelte +126 -0
  166. package/dist/phase-diagram/PhaseDiagramEditorPane.svelte.d.ts +15 -0
  167. package/dist/phase-diagram/PhaseDiagramExportPane.svelte +160 -110
  168. package/dist/phase-diagram/PhaseDiagramExportPane.svelte.d.ts +8 -1
  169. package/dist/phase-diagram/PhaseDiagramTooltip.svelte +217 -86
  170. package/dist/phase-diagram/PhaseDiagramTooltip.svelte.d.ts +6 -3
  171. package/dist/phase-diagram/TdbInfoPanel.svelte +28 -4
  172. package/dist/phase-diagram/build-diagram.js +9 -9
  173. package/dist/phase-diagram/colors.js +1 -3
  174. package/dist/phase-diagram/index.d.ts +2 -0
  175. package/dist/phase-diagram/index.js +2 -0
  176. package/dist/phase-diagram/parse.js +10 -9
  177. package/dist/phase-diagram/svg-to-diagram.d.ts +2 -0
  178. package/dist/phase-diagram/svg-to-diagram.js +869 -0
  179. package/dist/phase-diagram/types.d.ts +10 -0
  180. package/dist/phase-diagram/utils.d.ts +8 -4
  181. package/dist/phase-diagram/utils.js +219 -74
  182. package/dist/plot/AxisLabel.svelte +51 -0
  183. package/dist/plot/AxisLabel.svelte.d.ts +16 -0
  184. package/dist/plot/BarPlot.svelte +1461 -768
  185. package/dist/plot/BarPlot.svelte.d.ts +3 -3
  186. package/dist/plot/BarPlotControls.svelte +33 -6
  187. package/dist/plot/BarPlotControls.svelte.d.ts +1 -1
  188. package/dist/plot/ColorBar.svelte +533 -383
  189. package/dist/plot/ColorBar.svelte.d.ts +1 -1
  190. package/dist/plot/ColorScaleSelect.svelte +28 -7
  191. package/dist/plot/ElementScatter.svelte +38 -16
  192. package/dist/plot/FillArea.svelte +152 -92
  193. package/dist/plot/Histogram.svelte +1162 -709
  194. package/dist/plot/Histogram.svelte.d.ts +1 -1
  195. package/dist/plot/HistogramControls.svelte +81 -18
  196. package/dist/plot/HistogramControls.svelte.d.ts +6 -2
  197. package/dist/plot/InteractiveAxisLabel.svelte +34 -11
  198. package/dist/plot/InteractiveAxisLabel.svelte.d.ts +1 -1
  199. package/dist/plot/Line.svelte +63 -28
  200. package/dist/plot/PlotControls.svelte +221 -96
  201. package/dist/plot/PlotControls.svelte.d.ts +1 -1
  202. package/dist/plot/PlotLegend.svelte +174 -91
  203. package/dist/plot/PlotTooltip.svelte +45 -6
  204. package/dist/plot/PortalSelect.svelte +175 -146
  205. package/dist/plot/ReferenceLine.svelte +77 -22
  206. package/dist/plot/ReferenceLine.svelte.d.ts +1 -0
  207. package/dist/plot/ReferenceLine3D.svelte +132 -107
  208. package/dist/plot/ReferencePlane.svelte +146 -123
  209. package/dist/plot/ScatterPlot.svelte +1880 -1156
  210. package/dist/plot/ScatterPlot.svelte.d.ts +3 -3
  211. package/dist/plot/ScatterPlot3D.svelte +256 -131
  212. package/dist/plot/ScatterPlot3D.svelte.d.ts +2 -2
  213. package/dist/plot/ScatterPlot3DControls.svelte +300 -297
  214. package/dist/plot/ScatterPlot3DControls.svelte.d.ts +2 -1
  215. package/dist/plot/ScatterPlot3DScene.svelte +608 -406
  216. package/dist/plot/ScatterPlot3DScene.svelte.d.ts +2 -2
  217. package/dist/plot/ScatterPlotControls.svelte +150 -70
  218. package/dist/plot/ScatterPlotControls.svelte.d.ts +1 -1
  219. package/dist/plot/ScatterPoint.svelte +98 -26
  220. package/dist/plot/ScatterPoint.svelte.d.ts +1 -0
  221. package/dist/plot/SpacegroupBarPlot.svelte +142 -85
  222. package/dist/plot/Surface3D.svelte +159 -108
  223. package/dist/plot/ZeroLines.svelte +96 -0
  224. package/dist/plot/ZeroLines.svelte.d.ts +32 -0
  225. package/dist/plot/ZoomRect.svelte +23 -0
  226. package/dist/plot/ZoomRect.svelte.d.ts +8 -0
  227. package/dist/plot/axis-utils.d.ts +1 -1
  228. package/dist/plot/axis-utils.js +1 -3
  229. package/dist/plot/data-cleaning.js +12 -28
  230. package/dist/plot/data-transform.js +2 -1
  231. package/dist/plot/fill-utils.js +2 -0
  232. package/dist/plot/index.d.ts +6 -2
  233. package/dist/plot/index.js +6 -2
  234. package/dist/plot/interactions.d.ts +8 -10
  235. package/dist/plot/interactions.js +2 -3
  236. package/dist/plot/layout.d.ts +11 -2
  237. package/dist/plot/layout.js +44 -17
  238. package/dist/plot/reference-line.d.ts +5 -22
  239. package/dist/plot/reference-line.js +12 -84
  240. package/dist/plot/scales.js +24 -36
  241. package/dist/plot/types.d.ts +53 -40
  242. package/dist/plot/types.js +12 -7
  243. package/dist/plot/utils/label-placement.d.ts +32 -15
  244. package/dist/plot/utils/label-placement.js +227 -63
  245. package/dist/plot/utils/series-visibility.js +2 -3
  246. package/dist/plot/utils.d.ts +1 -0
  247. package/dist/plot/utils.js +14 -0
  248. package/dist/rdf/RdfPlot.svelte +173 -132
  249. package/dist/rdf/calc-rdf.js +4 -5
  250. package/dist/sanitize.d.ts +4 -0
  251. package/dist/sanitize.js +107 -0
  252. package/dist/settings.d.ts +21 -6
  253. package/dist/settings.js +63 -19
  254. package/dist/spectral/Bands.svelte +963 -412
  255. package/dist/spectral/Bands.svelte.d.ts +22 -2
  256. package/dist/spectral/BandsAndDos.svelte +90 -49
  257. package/dist/spectral/BrillouinBandsDos.svelte +151 -93
  258. package/dist/spectral/Dos.svelte +389 -258
  259. package/dist/spectral/helpers.d.ts +23 -1
  260. package/dist/spectral/helpers.js +119 -51
  261. package/dist/spectral/types.d.ts +2 -0
  262. package/dist/state.svelte.d.ts +1 -1
  263. package/dist/state.svelte.js +3 -2
  264. package/dist/structure/Arrow.svelte +59 -20
  265. package/dist/structure/AtomLegend.svelte +231 -129
  266. package/dist/structure/AtomLegend.svelte.d.ts +1 -1
  267. package/dist/structure/Bond.svelte +73 -47
  268. package/dist/structure/CanvasTooltip.svelte +10 -2
  269. package/dist/structure/CellSelect.svelte +148 -51
  270. package/dist/structure/Cylinder.svelte +33 -17
  271. package/dist/structure/Lattice.svelte +88 -33
  272. package/dist/structure/Structure.svelte +1077 -821
  273. package/dist/structure/Structure.svelte.d.ts +1 -1
  274. package/dist/structure/StructureControls.svelte +373 -139
  275. package/dist/structure/StructureControls.svelte.d.ts +1 -1
  276. package/dist/structure/StructureExportPane.svelte +124 -89
  277. package/dist/structure/StructureExportPane.svelte.d.ts +1 -1
  278. package/dist/structure/StructureInfoPane.svelte +304 -231
  279. package/dist/structure/StructureScene.svelte +919 -445
  280. package/dist/structure/StructureScene.svelte.d.ts +16 -7
  281. package/dist/structure/atom-properties.d.ts +6 -2
  282. package/dist/structure/atom-properties.js +42 -29
  283. package/dist/structure/bonding.js +6 -7
  284. package/dist/structure/export.js +22 -34
  285. package/dist/structure/ferrox-wasm-types.d.ts +3 -2
  286. package/dist/structure/ferrox-wasm-types.js +0 -3
  287. package/dist/structure/ferrox-wasm.d.ts +3 -2
  288. package/dist/structure/ferrox-wasm.js +2 -3
  289. package/dist/structure/index.d.ts +16 -0
  290. package/dist/structure/index.js +88 -6
  291. package/dist/structure/measure.d.ts +2 -2
  292. package/dist/structure/measure.js +4 -44
  293. package/dist/structure/parse.js +130 -155
  294. package/dist/structure/partial-occupancy.d.ts +25 -0
  295. package/dist/structure/partial-occupancy.js +99 -0
  296. package/dist/structure/pbc.d.ts +1 -0
  297. package/dist/structure/pbc.js +16 -6
  298. package/dist/structure/supercell.d.ts +2 -2
  299. package/dist/structure/supercell.js +12 -22
  300. package/dist/structure/validation.js +5 -3
  301. package/dist/symmetry/SymmetryStats.svelte +94 -37
  302. package/dist/symmetry/WyckoffTable.svelte +42 -14
  303. package/dist/symmetry/cell-transform.js +5 -3
  304. package/dist/symmetry/index.d.ts +7 -4
  305. package/dist/symmetry/index.js +87 -21
  306. package/dist/symmetry/spacegroups.js +148 -148
  307. package/dist/table/HeatmapTable.svelte +1112 -516
  308. package/dist/table/HeatmapTable.svelte.d.ts +12 -1
  309. package/dist/table/ToggleMenu.svelte +125 -90
  310. package/dist/table/index.d.ts +2 -0
  311. package/dist/table/index.js +2 -4
  312. package/dist/theme/ThemeControl.svelte +21 -12
  313. package/dist/time.js +4 -1
  314. package/dist/tooltip/TooltipContent.svelte +33 -8
  315. package/dist/trajectory/Trajectory.svelte +889 -687
  316. package/dist/trajectory/TrajectoryError.svelte +14 -3
  317. package/dist/trajectory/TrajectoryExportPane.svelte +148 -90
  318. package/dist/trajectory/TrajectoryExportPane.svelte.d.ts +1 -1
  319. package/dist/trajectory/TrajectoryInfoPane.svelte +272 -143
  320. package/dist/trajectory/constants.d.ts +6 -0
  321. package/dist/trajectory/constants.js +7 -0
  322. package/dist/trajectory/extract.js +13 -31
  323. package/dist/trajectory/format-detect.d.ts +9 -0
  324. package/dist/trajectory/format-detect.js +76 -0
  325. package/dist/trajectory/frame-reader.d.ts +17 -0
  326. package/dist/trajectory/frame-reader.js +332 -0
  327. package/dist/trajectory/helpers.d.ts +14 -0
  328. package/dist/trajectory/helpers.js +172 -0
  329. package/dist/trajectory/index.d.ts +1 -0
  330. package/dist/trajectory/index.js +23 -14
  331. package/dist/trajectory/parse/ase.d.ts +2 -0
  332. package/dist/trajectory/parse/ase.js +77 -0
  333. package/dist/trajectory/parse/hdf5.d.ts +2 -0
  334. package/dist/trajectory/parse/hdf5.js +129 -0
  335. package/dist/trajectory/parse/index.d.ts +12 -0
  336. package/dist/trajectory/parse/index.js +299 -0
  337. package/dist/trajectory/parse/lammps.d.ts +5 -0
  338. package/dist/trajectory/parse/lammps.js +179 -0
  339. package/dist/trajectory/parse/vasp.d.ts +2 -0
  340. package/dist/trajectory/parse/vasp.js +68 -0
  341. package/dist/trajectory/parse/xyz.d.ts +2 -0
  342. package/dist/trajectory/parse/xyz.js +110 -0
  343. package/dist/trajectory/plotting.js +13 -8
  344. package/dist/trajectory/types.d.ts +11 -0
  345. package/dist/trajectory/types.js +1 -0
  346. package/dist/utils.d.ts +3 -0
  347. package/dist/utils.js +17 -0
  348. package/dist/xrd/XrdPlot.svelte +337 -245
  349. package/dist/xrd/broadening.js +14 -9
  350. package/dist/xrd/calc-xrd.js +12 -19
  351. package/dist/xrd/parse.d.ts +1 -1
  352. package/dist/xrd/parse.js +17 -17
  353. package/package.json +103 -101
  354. package/readme.md +4 -4
  355. package/dist/trajectory/parse.d.ts +0 -42
  356. package/dist/trajectory/parse.js +0 -1267
  357. /package/dist/element/{data.json.d.ts → data.json.gz.d.ts} +0 -0
  358. /package/dist/theme/{themes.js → themes.mjs} +0 -0
@@ -1,636 +1,840 @@
1
- <script lang="ts">import { normalize_show_controls } from '../controls';
2
- import EmptyState from '../EmptyState.svelte';
3
- import Spinner from '../feedback/Spinner.svelte';
4
- import Icon from '../Icon.svelte';
5
- import { handle_url_drop, load_from_url } from '../io';
6
- import { format_num, trajectory_property_config } from '../labels';
7
- import { toggle_fullscreen } from '../layout';
8
- import { Histogram, ScatterPlot } from '../plot';
9
- import { toggle_series_visibility } from '../plot/utils/series-visibility';
10
- import { DEFAULTS } from '../settings';
11
- import Structure from '../structure/Structure.svelte';
12
- import { scaleLinear } from 'd3-scale';
13
- import { untrack } from 'svelte';
14
- import { tooltip } from 'svelte-multiselect/attachments';
15
- import { full_data_extractor } from './extract';
16
- import { TrajectoryError, TrajectoryExportPane, TrajectoryInfoPane } from './index';
17
- import { create_frame_loader, get_unsupported_format_message, MAX_BIN_FILE_SIZE, MAX_TEXT_FILE_SIZE, parse_trajectory_async, } from './parse';
18
- import { generate_axis_labels, generate_plot_series, generate_streaming_plot_series, should_hide_plot, } from './plotting';
19
- let { trajectory = $bindable(), data_url, current_step_idx = $bindable(0), data_extractor = full_data_extractor, allow_file_drop = true, layout = `auto`, structure_props = {}, scatter_props = {}, histogram_props = {}, spinner_props = {}, trajectory_controls, error_snippet, show_controls, fullscreen_toggle = DEFAULTS.trajectory.fullscreen_toggle, auto_play = false, display_mode = $bindable(`structure+scatter`), step_labels = 5, visible_properties = $bindable(), ELEM_PROPERTY_LABELS, on_play, on_pause, on_step_change, on_end, on_loop, on_frame_rate_change, on_display_mode_change, on_fullscreen_change, on_file_load, on_error, fps_range = DEFAULTS.trajectory.fps_range, fps = $bindable(5), loading_options = {}, atom_type_mapping, plot_skimming = true, ...rest } = $props();
20
- let dragover = $state(false);
21
- let loading = $state(false);
22
- let error_msg = $state(null);
23
- let is_playing = $state(false);
24
- let play_interval = $state(undefined);
25
- // Ensure fps is within the allowed range
26
- $effect(() => {
27
- if (fps < fps_range[0]) {
28
- fps = fps_range[0];
1
+ <script lang="ts">
2
+ import type { ShowControlsProp } from '../controls'
3
+ import { normalize_show_controls } from '../controls'
4
+ import type { ElementSymbol } from '../element'
5
+ import EmptyState from '../EmptyState.svelte'
6
+ import Spinner from '../feedback/Spinner.svelte'
7
+ import Icon from '../Icon.svelte'
8
+ import { handle_url_drop, load_from_url } from '../io'
9
+ import { format_num, trajectory_property_config } from '../labels'
10
+ import { sanitize_html } from '../sanitize'
11
+ import { toggle_fullscreen } from '../layout'
12
+ import type { ControlsConfig, DataSeries, Orientation, Point } from '../plot'
13
+ import { Histogram, ScatterPlot } from '../plot'
14
+ import { toggle_series_visibility } from '../plot/utils/series-visibility'
15
+ import { DEFAULTS } from '../settings'
16
+ import Structure from '../structure/Structure.svelte'
17
+ import { scaleLinear } from 'd3-scale'
18
+ import type { ComponentProps, Snippet } from 'svelte'
19
+ import { untrack } from 'svelte'
20
+ import { tooltip } from 'svelte-multiselect/attachments'
21
+ import type { HTMLAttributes } from 'svelte/elements'
22
+ import { full_data_extractor } from './extract'
23
+ import type {
24
+ ParseProgress,
25
+ TrajectoryDataExtractor,
26
+ TrajectoryFrame,
27
+ TrajectoryType,
28
+ TrajHandlerData,
29
+ } from './index'
30
+ import { TrajectoryError, TrajectoryExportPane, TrajectoryInfoPane } from './index'
31
+ import type { AtomTypeMapping, LoadingOptions } from './parse'
32
+ import {
33
+ create_frame_loader,
34
+ get_unsupported_format_message,
35
+ MAX_BIN_FILE_SIZE,
36
+ MAX_TEXT_FILE_SIZE,
37
+ parse_trajectory_async,
38
+ } from './parse'
39
+ import {
40
+ generate_axis_labels,
41
+ generate_plot_series,
42
+ generate_streaming_plot_series,
43
+ should_hide_plot,
44
+ } from './plotting'
45
+
46
+ type EventHandlers = {
47
+ on_play?: (data: TrajHandlerData) => void
48
+ on_pause?: (data: TrajHandlerData) => void
49
+ on_step_change?: (data: TrajHandlerData) => void
50
+ on_end?: (data: TrajHandlerData) => void
51
+ on_loop?: (data: TrajHandlerData) => void
52
+ on_frame_rate_change?: (data: TrajHandlerData) => void
53
+ on_display_mode_change?: (data: TrajHandlerData) => void
54
+ on_fullscreen_change?: (data: TrajHandlerData) => void
55
+ on_file_load?: (data: TrajHandlerData) => void
56
+ on_error?: (data: TrajHandlerData) => void
57
+ }
58
+ type ControlsProps = {
59
+ trajectory: TrajectoryType
60
+ current_step_idx: number
61
+ total_frames: number
62
+ on_step_change: (idx: number) => void
63
+ }
64
+
65
+ let {
66
+ trajectory = $bindable(),
67
+ data_url,
68
+ current_step_idx = $bindable(0),
69
+ data_extractor = full_data_extractor,
70
+ allow_file_drop = true,
71
+ layout = `auto`,
72
+ structure_props = {},
73
+ scatter_props = {},
74
+ histogram_props = {},
75
+ spinner_props = {},
76
+ trajectory_controls,
77
+ error_snippet,
78
+ show_controls,
79
+ fullscreen_toggle = DEFAULTS.trajectory.fullscreen_toggle,
80
+ auto_play = false,
81
+ display_mode = $bindable(`structure+scatter`),
82
+ step_labels = 5,
83
+ visible_properties = $bindable(),
84
+ ELEM_PROPERTY_LABELS,
85
+ on_play,
86
+ on_pause,
87
+ on_step_change,
88
+ on_end,
89
+ on_loop,
90
+ on_frame_rate_change,
91
+ on_display_mode_change,
92
+ on_fullscreen_change,
93
+ on_file_load,
94
+ on_error,
95
+ fps_range = DEFAULTS.trajectory.fps_range,
96
+ fps = $bindable(5),
97
+ loading_options = {},
98
+ atom_type_mapping,
99
+ plot_skimming = true,
100
+ ...rest
101
+ }: EventHandlers & HTMLAttributes<HTMLDivElement> & {
102
+ // trajectory data - can be provided directly or loaded from file
103
+ trajectory?: TrajectoryType
104
+ // URL to load trajectory from (alternative to providing trajectory directly)
105
+ data_url?: string
106
+ // current step index being displayed
107
+ current_step_idx?: number
108
+ // custom function to extract plot data from trajectory frames
109
+ data_extractor?: TrajectoryDataExtractor
110
+
111
+ // file drop handlers
112
+ allow_file_drop?: boolean
113
+ // layout configuration - 'auto' (default) adapts to element size, 'horizontal'/'vertical' forces layout
114
+ layout?: `auto` | Orientation
115
+ // structure viewer props (passed to Structure component)
116
+ structure_props?: ComponentProps<typeof Structure>
117
+ // plot props (passed to ScatterPlot component)
118
+ scatter_props?: ComponentProps<typeof ScatterPlot>
119
+ // histogram props (passed to Histogram component, excluding series which is handled separately)
120
+ histogram_props?: Omit<ComponentProps<typeof Histogram>, `series`>
121
+ // spinner props (passed to Spinner component)
122
+ spinner_props?: ComponentProps<typeof Spinner>
123
+ // custom snippets for additional UI elements
124
+ trajectory_controls?: Snippet<[ControlsProps]>
125
+ // Custom error snippet for advanced error handling
126
+ error_snippet?: Snippet<[{ error_msg: string; on_dismiss: () => void }]>
127
+ // Controls visibility configuration.
128
+ // - 'always': controls always visible
129
+ // - 'hover': controls visible on component hover (default)
130
+ // - 'never': controls never visible
131
+ // - object: { mode, hidden, style } for fine-grained control
132
+ // Control names: 'filename', 'nav', 'step', 'fps', 'info-pane', 'export-pane', 'view-mode', 'fullscreen'
133
+ show_controls?: ShowControlsProp
134
+ // show/hide the fullscreen button
135
+ fullscreen_toggle?: Snippet<[{ fullscreen: boolean }]> | boolean
136
+ // automatically start playing when trajectory data is loaded
137
+ auto_play?: boolean
138
+ // display mode: 'structure+scatter' (default), 'structure' (only structure), 'scatter' (only scatter), 'histogram' (only histogram), 'structure+histogram' (structure with histogram)
139
+ display_mode?:
140
+ | `structure+scatter`
141
+ | `structure`
142
+ | `scatter`
143
+ | `histogram`
144
+ | `structure+histogram`
145
+ // step labels configuration for slider
146
+ // - positive number: number of evenly spaced ticks
147
+ // - negative number: spacing between ticks (e.g. -10 = every 10th step)
148
+ // - array: exact step indices to label
149
+ // - undefined: no labels
150
+ step_labels?: number | number[]
151
+ // visible properties - bindable array of property keys currently shown in the plot
152
+ // - controls which trajectory properties are plotted (e.g. ['energy', 'volume', 'force_max'])
153
+ // - bindable: reflects current visibility state and can be used for external control
154
+ // - if not provided, uses default visible properties (energy, force_max, stress_frobenius)
155
+ // - if specified properties don't exist in data, falls back to automatic selection
156
+ visible_properties?: string[]
157
+ // custom labels for trajectory properties - maps property keys to display labels
158
+ // - e.g. {energy: 'Total Energy', volume: 'Cell Volume', force_max: 'Max Force'}
159
+ // - merged with built-in trajectory_property_config
160
+ ELEM_PROPERTY_LABELS?: Record<string, string>
161
+ // units configuration - developers can override these (deprecated - use ELEM_PROPERTY_LABELS instead)
162
+ units?: {
163
+ energy?: string
164
+ energy_per_atom?: string
165
+ force_max?: string
166
+ force_norm?: string
167
+ stress_max?: string
168
+ volume?: string
169
+ density?: string
170
+ temperature?: string
171
+ pressure?: string
172
+ length?: string
173
+ a?: string
174
+ b?: string
175
+ c?: string
176
+ [key: string]: string | undefined
29
177
  }
30
- else if (fps > fps_range[1]) {
31
- fps = fps_range[1];
178
+ fps_range?: [number, number] // allowed FPS range [min_fps, max_fps]
179
+ fps?: number // frame rate for playback
180
+ // Loading options for large files
181
+ loading_options?: LoadingOptions
182
+ // Map LAMMPS atom types to element symbols (e.g. {1: 'Na', 2: 'Cl'})
183
+ atom_type_mapping?: AtomTypeMapping
184
+ // Disable plot skimming (mouse over plot doesn't update structure/step slider)
185
+ plot_skimming?: boolean
186
+ } = $props()
187
+
188
+ let dragover = $state(false)
189
+ let loading = $state(false)
190
+ let error_msg = $state<string | null>(null)
191
+ let is_playing = $state(false)
192
+ let play_interval: ReturnType<typeof setInterval> | undefined = $state(undefined)
193
+
194
+ // Ensure fps is within the allowed range
195
+ $effect(() => {
196
+ if (fps < fps_range[0]) {
197
+ fps = fps_range[0]
198
+ } else if (fps > fps_range[1]) {
199
+ fps = fps_range[1]
32
200
  }
33
- });
34
- let current_filename = $state(undefined);
35
- let current_file_path = $state(null);
36
- let file_size = $state(undefined);
37
- let file_object = $state(null);
38
- let wrapper = $state(undefined);
39
- let info_pane_open = $state(false);
40
- let parsing_progress = $state(null);
41
- let element_size = $state({ width: 0, height: 0 });
42
- let filename_copied = $state(false);
43
- let orig_data = $state(null);
44
- let controls_config = $derived(normalize_show_controls(show_controls));
45
- // Reactive layout based on element aspect ratio (for auto mode)
46
- let actual_layout = $derived.by(() => {
47
- if (layout === `horizontal` || layout === `vertical`)
48
- return layout;
201
+ })
202
+ let current_filename = $state<string | undefined>(undefined)
203
+ let current_file_path = $state<string | null>(null)
204
+ let file_size = $state<number | undefined>(undefined)
205
+ let file_object = $state<File | null>(null)
206
+ let wrapper = $state<HTMLDivElement | undefined>(undefined)
207
+ let info_pane_open = $state(false)
208
+ let parsing_progress = $state<ParseProgress | null>(null)
209
+ let element_size = $state({ width: 0, height: 0 })
210
+ let filename_copied = $state(false)
211
+ let orig_data = $state<string | ArrayBuffer | null>(null)
212
+
213
+ let controls_config = $derived(normalize_show_controls(show_controls))
214
+
215
+ // Reactive layout based on element aspect ratio (for auto mode)
216
+ let actual_layout = $derived.by(() => {
217
+ if (layout === `horizontal` || layout === `vertical`) return layout
49
218
  // For auto layout, use element dimensions to determine orientation
50
219
  if (element_size.width > 0 && element_size.height > 0) {
51
- return element_size.width > element_size.height ? `horizontal` : `vertical`;
220
+ return element_size.width > element_size.height ? `horizontal` : `vertical`
52
221
  }
53
- return `horizontal`; // Fallback to horizontal if dimensions not available yet
54
- });
55
- // Get total frame count (supports both regular and indexed trajectories)
56
- let total_frames = $derived(trajectory?.total_frames || trajectory?.frames.length || 0);
57
- // Current frame - load on demand for indexed trajectories
58
- let current_frame = $state(null);
59
- // Auto-play when trajectory changes (handles both props and file loading)
60
- $effect(() => {
222
+ return `horizontal` // Fallback to horizontal if dimensions not available yet
223
+ })
224
+
225
+ // Get total frame count (supports both regular and indexed trajectories)
226
+ let total_frames = $derived(
227
+ trajectory?.total_frames || trajectory?.frames.length || 0,
228
+ )
229
+
230
+ // Current frame - load on demand for indexed trajectories
231
+ let current_frame = $state<TrajectoryFrame | null>(null)
232
+
233
+ // Auto-play when trajectory changes (handles both props and file loading)
234
+ $effect(() => {
61
235
  if (auto_play && trajectory && !untrack(() => is_playing) && total_frames > 1) {
62
- start_playback();
236
+ start_playback()
63
237
  }
64
- });
65
- // Update current frame when step changes
66
- $effect(() => {
238
+ })
239
+
240
+ // Update current frame when step changes
241
+ $effect(() => {
67
242
  if (trajectory && current_step_idx >= 0 && current_step_idx < total_frames) {
68
- if (trajectory.frame_loader) {
69
- // Load frame on demand (works for both indexed files and external streaming)
70
- load_frame_on_demand(current_step_idx);
71
- }
72
- else {
73
- // Use in-memory frame for regular trajectories
74
- current_frame = trajectory.frames[current_step_idx] || null;
75
- }
76
- }
77
- else {
78
- current_frame = null;
243
+ if (trajectory.frame_loader) {
244
+ // Load frame on demand (works for both indexed files and external streaming)
245
+ load_frame_on_demand(current_step_idx)
246
+ } else {
247
+ // Use in-memory frame for regular trajectories
248
+ current_frame = trajectory.frames[current_step_idx] || null
249
+ }
250
+ } else {
251
+ current_frame = null
79
252
  }
80
- });
81
- // Load frame on demand - works for both indexed files and external streaming
82
- async function load_frame_on_demand(frame_idx) {
83
- if (!trajectory?.frame_loader)
84
- return;
253
+ })
254
+
255
+ // Load frame on demand - works for both indexed files and external streaming
256
+ async function load_frame_on_demand(frame_idx: number) {
257
+ if (!trajectory?.frame_loader) return
258
+
85
259
  try {
86
- const frame = await trajectory.frame_loader.load_frame(orig_data || ``, // Use original_data for indexed files, empty string for external streaming
87
- frame_idx);
88
- current_frame = frame;
89
- }
90
- catch (error) {
91
- console.error(`Failed to load frame ${frame_idx}:`, error);
92
- current_frame = null;
93
- on_error?.({
94
- error_msg: `Failed to load frame ${frame_idx}: ${error}`,
95
- filename: current_filename,
96
- file_size,
97
- step_idx: frame_idx,
98
- frame_count: total_frames,
99
- });
260
+ const frame = await trajectory.frame_loader.load_frame(
261
+ orig_data || ``, // Use original_data for indexed files, empty string for external streaming
262
+ frame_idx,
263
+ )
264
+ current_frame = frame
265
+ } catch (error) {
266
+ console.error(`Failed to load frame ${frame_idx}:`, error)
267
+ current_frame = null
268
+ on_error?.({
269
+ error_msg: `Failed to load frame ${frame_idx}: ${error}`,
270
+ filename: current_filename,
271
+ file_size,
272
+ step_idx: frame_idx,
273
+ frame_count: total_frames,
274
+ })
100
275
  }
101
- }
102
- // Current frame structure for display
103
- let current_structure = $derived(current_frame?.structure);
104
- // Track hidden elements (persists across frame changes)
105
- let hidden_elements = $state(new Set());
106
- let step_label_positions = $derived.by(() => {
107
- if (!step_labels || total_frames <= 1)
108
- return [];
276
+ }
277
+
278
+ // Current frame structure for display
279
+ let current_structure = $derived(current_frame?.structure)
280
+
281
+ // Track hidden elements (persists across frame changes)
282
+ let hidden_elements = $state(new Set<ElementSymbol>())
283
+
284
+ let step_label_positions = $derived.by((): number[] => {
285
+ if (!step_labels || total_frames <= 1) return []
286
+
109
287
  if (Array.isArray(step_labels)) {
110
- return step_labels.filter((idx) => idx >= 0 && idx < total_frames);
288
+ return step_labels.filter((idx) => idx >= 0 && idx < total_frames)
111
289
  }
290
+
112
291
  if (typeof step_labels === `number`) {
113
- if (step_labels > 0) {
114
- return scaleLinear().domain([0, total_frames - 1]).nice()
115
- .ticks(Math.min(step_labels, total_frames))
116
- .map((t) => Math.round(t))
117
- .filter((t, i, arr) => t >= 0 && t < total_frames && arr.indexOf(t) === i);
118
- }
119
- if (step_labels < 0) {
120
- const spacing = Math.abs(step_labels);
121
- const positions = Array.from({ length: Math.ceil(total_frames / spacing) }, (_, idx) => idx * spacing);
122
- return positions.at(-1) === total_frames - 1
123
- ? positions
124
- : [...positions, total_frames - 1];
125
- }
292
+ if (step_labels > 0) {
293
+ return scaleLinear().domain([0, total_frames - 1]).nice()
294
+ .ticks(Math.min(step_labels, total_frames))
295
+ .map((t) => Math.round(t))
296
+ .filter((t, i, arr) => t >= 0 && t < total_frames && arr.indexOf(t) === i)
297
+ }
298
+ if (step_labels < 0) {
299
+ const spacing = Math.abs(step_labels)
300
+ const positions = Array.from(
301
+ { length: Math.ceil(total_frames / spacing) },
302
+ (_, idx) => idx * spacing,
303
+ )
304
+ return positions.at(-1) === total_frames - 1
305
+ ? positions
306
+ : [...positions, total_frames - 1]
307
+ }
126
308
  }
127
- return [];
128
- });
129
- // Build extended property config with custom labels if provided
130
- let extended_config = $derived.by(() => {
131
- if (!ELEM_PROPERTY_LABELS)
132
- return trajectory_property_config;
133
- const custom_config = {};
309
+ return []
310
+ })
311
+
312
+ // Build extended property config with custom labels if provided
313
+ let extended_config = $derived.by(() => {
314
+ if (!ELEM_PROPERTY_LABELS) return trajectory_property_config
315
+
316
+ const custom_config: Record<string, { label: string; unit: string }> = {}
134
317
  for (const [key, label] of Object.entries(ELEM_PROPERTY_LABELS)) {
135
- const existing = trajectory_property_config[key] ||
136
- trajectory_property_config[key.toLowerCase()];
137
- custom_config[key] = { label, unit: existing?.unit || `` };
318
+ const existing = trajectory_property_config[key] ||
319
+ trajectory_property_config[key.toLowerCase()]
320
+ custom_config[key] = { label, unit: existing?.unit || `` }
138
321
  }
139
- return { ...trajectory_property_config, ...custom_config };
140
- });
141
- // Plot series state (not derived so we can update on legend toggle)
142
- let plot_series = $state([]);
143
- // Regenerate plot series when trajectory, config, or visible_properties change
144
- $effect(() => {
145
- const keys_set = visible_properties ? new Set(visible_properties) : undefined;
322
+ return { ...trajectory_property_config, ...custom_config }
323
+ })
324
+
325
+ // Plot series state (not derived so we can update on legend toggle)
326
+ let plot_series = $state<DataSeries[]>([])
327
+ // Prevent circular updates when syncing legend toggles back to bindable visible_properties.
328
+ let syncing_visible_properties = false
329
+
330
+ // Regenerate plot series when trajectory, config, or visible_properties change
331
+ $effect(() => {
332
+ if (syncing_visible_properties) return
333
+ const keys_set = visible_properties ? new Set(visible_properties) : undefined
334
+
146
335
  if (trajectory?.plot_metadata) {
147
- plot_series = generate_streaming_plot_series(trajectory.plot_metadata, {
148
- property_config: extended_config,
149
- default_visible_properties: keys_set,
150
- });
336
+ plot_series = generate_streaming_plot_series(trajectory.plot_metadata, {
337
+ property_config: extended_config,
338
+ default_visible_properties: keys_set,
339
+ })
340
+ } else if (trajectory) {
341
+ plot_series = generate_plot_series(trajectory, data_extractor, {
342
+ property_config: extended_config,
343
+ default_visible_properties: keys_set,
344
+ })
345
+ } else {
346
+ plot_series = []
151
347
  }
152
- else if (trajectory) {
153
- plot_series = generate_plot_series(trajectory, data_extractor, {
154
- property_config: extended_config,
155
- default_visible_properties: keys_set,
156
- });
157
- }
158
- else {
159
- plot_series = [];
160
- }
161
- });
162
- // Update visible_properties binding when user toggles series visibility in legend
163
- $effect(() => {
164
- if (!plot_series.length)
165
- return;
348
+ })
349
+
350
+ // Update visible_properties binding when user toggles series visibility in legend
351
+ $effect(() => {
352
+ if (!plot_series.length) return
353
+
166
354
  // Extract property keys from visible series metadata
167
- const visible_keys = plot_series
168
- .filter((srs) => srs.visible)
169
- // Get property key from series metadata (stored during series generation)
170
- .map((srs) => {
171
- const metadata = Array.isArray(srs.metadata) ? srs.metadata[0] : srs.metadata;
172
- return metadata?.property_key;
355
+ const visible_keys = plot_series.flatMap((srs) => {
356
+ if (!srs.visible) return []
357
+ const metadata = Array.isArray(srs.metadata) ? srs.metadata[0] : srs.metadata
358
+ const key = metadata?.property_key
359
+ return key ? [key as string] : []
173
360
  })
174
- .filter((key) => Boolean(key));
361
+
175
362
  // Only update if changed (use untrack to avoid circular dependency)
176
- const current = untrack(() => visible_properties) || [];
363
+ const current = untrack(() => visible_properties) || []
177
364
  const has_changed = visible_keys.length !== current.length ||
178
- !visible_keys.every((key, idx) => key === current[idx]);
365
+ !visible_keys.every((key, idx) => key === current[idx])
366
+
179
367
  if (has_changed) {
180
- visible_properties = visible_keys;
368
+ syncing_visible_properties = true
369
+ visible_properties = visible_keys
370
+ queueMicrotask(() => (syncing_visible_properties = false))
181
371
  }
182
- });
183
- // Handler for legend toggle - updates plot_series state
184
- function handle_legend_toggle(series_idx) {
185
- plot_series = toggle_series_visibility(plot_series, series_idx);
186
- }
187
- let x_axis = $derived({
372
+ })
373
+
374
+ // Handler for legend toggle - updates plot_series state
375
+ function handle_legend_toggle(series_idx: number) {
376
+ plot_series = toggle_series_visibility(plot_series, series_idx)
377
+ }
378
+
379
+ let x_axis = $derived({
188
380
  label: `Step`,
189
381
  format: `.3~s`,
190
382
  ticks: step_label_positions,
191
- });
192
- // Generate axis labels based on first visible series on each axis
193
- let y_axis_labels = $derived(generate_axis_labels(plot_series));
194
- let y_axis = $derived({
383
+ })
384
+ // Generate axis labels based on first visible series on each axis
385
+ let y_axis_labels = $derived(generate_axis_labels(plot_series))
386
+ let y_axis = $derived({
195
387
  label: y_axis_labels.y1,
196
388
  format: `.2~s`,
197
- label_shift: { y: 20 },
198
- });
199
- let y2_axis = $derived({
389
+ label_shift: { y: 10 },
390
+ })
391
+ let y2_axis = $derived({
200
392
  label: y_axis_labels.y2,
201
393
  format: `.2~s`,
202
394
  label_shift: { y: 80 },
203
- });
204
- // hide plot if all plotted values are constant (no variation)
205
- let show_plot = $derived(display_mode !== `structure` && !should_hide_plot(trajectory, plot_series));
206
- // Determine what to show based on display mode
207
- let show_structure = $derived(![`scatter`, `histogram`].includes(display_mode));
208
- let actual_show_plot = $derived(display_mode !== `structure` && show_plot);
209
- // Check if there are any Y2 series to determine padding
210
- let has_y2_series = $derived(plot_series.some((srs) => srs.y_axis === `y2` && srs.visible));
211
- // Step navigation functions
212
- function next_step() {
395
+ })
396
+
397
+ // hide plot if all plotted values are constant (no variation)
398
+ let show_plot = $derived(
399
+ display_mode !== `structure` && !should_hide_plot(trajectory, plot_series),
400
+ )
401
+
402
+ // Determine what to show based on display mode
403
+ let show_structure = $derived(![`scatter`, `histogram`].includes(display_mode))
404
+ let actual_show_plot = $derived(display_mode !== `structure` && show_plot)
405
+
406
+ // Check if there are any Y2 series to determine padding
407
+ let has_y2_series = $derived(
408
+ plot_series.some((srs) => srs.y_axis === `y2` && srs.visible),
409
+ )
410
+
411
+ // Step navigation functions
412
+ function next_step() {
213
413
  if (current_step_idx < total_frames - 1) {
214
- current_step_idx++;
215
- // Streaming frame loading handled by reactive effect
216
- if (trajectory) {
217
- on_step_change?.({
218
- trajectory,
219
- step_idx: current_step_idx,
220
- frame_count: total_frames,
221
- frame: current_frame || undefined,
222
- });
223
- }
414
+ current_step_idx++
415
+ // Streaming frame loading handled by reactive effect
416
+ if (trajectory) {
417
+ on_step_change?.({
418
+ trajectory,
419
+ step_idx: current_step_idx,
420
+ frame_count: total_frames,
421
+ frame: current_frame || undefined,
422
+ })
423
+ }
224
424
  }
225
- }
226
- function prev_step() {
425
+ }
426
+
427
+ function prev_step() {
227
428
  if (current_step_idx > 0) {
228
- current_step_idx--;
229
- // Streaming frame loading handled by reactive effect
230
- if (trajectory) {
231
- on_step_change?.({
232
- trajectory,
233
- step_idx: current_step_idx,
234
- frame_count: total_frames,
235
- frame: current_frame || undefined,
236
- });
237
- }
429
+ current_step_idx--
430
+ // Streaming frame loading handled by reactive effect
431
+ if (trajectory) {
432
+ on_step_change?.({
433
+ trajectory,
434
+ step_idx: current_step_idx,
435
+ frame_count: total_frames,
436
+ frame: current_frame || undefined,
437
+ })
438
+ }
238
439
  }
239
- }
240
- function go_to_step(idx) {
440
+ }
441
+
442
+ function go_to_step(idx: number) {
241
443
  if (idx >= 0 && idx < total_frames) {
242
- current_step_idx = idx;
243
- // Note: streaming frame loading is handled by reactive effect
244
- // Handle callbacks for both traditional and streaming modes
245
- if (trajectory) {
246
- on_step_change?.({
247
- trajectory,
248
- step_idx: current_step_idx,
249
- frame_count: total_frames,
250
- frame: current_frame || undefined,
251
- });
252
- }
444
+ current_step_idx = idx
445
+ // Note: streaming frame loading is handled by reactive effect
446
+ // Handle callbacks for both traditional and streaming modes
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
+ }
253
455
  }
254
- }
255
- // Handle plot point clicks to jump to that step
256
- function handle_plot_change(data) {
456
+ }
457
+
458
+ // Handle plot point clicks to jump to that step
459
+ function handle_plot_change(data: (Point & { series: DataSeries }) | null) {
257
460
  if (data?.x !== undefined && typeof data.x === `number`) {
258
- go_to_step(Math.round(data.x));
461
+ go_to_step(Math.round(data.x))
259
462
  }
260
- }
261
- // Helper function to read file content
262
- async function read_file_content(file) {
463
+ }
464
+
465
+ // Helper function to read file content
466
+ async function read_file_content(file: File): Promise<string | ArrayBuffer> {
263
467
  return new Promise((resolve, reject) => {
264
- const reader = new FileReader();
265
- reader.onload = () => resolve(reader.result);
266
- reader.onerror = () => reject(new Error(`Failed to read file`));
267
- // Read as text for text-based formats, binary for others
268
- if (file.name.toLowerCase().match(/\.(xyz|json|extxyz|lammpstrj)$/)) {
269
- reader.readAsText(file);
270
- }
271
- else
272
- reader.readAsArrayBuffer(file);
273
- });
274
- }
275
- // Play/pause functionality
276
- function toggle_play() {
277
- if (is_playing)
278
- pause_playback();
279
- else
280
- start_playback();
281
- }
282
- function start_playback() {
283
- if (total_frames <= 1)
284
- return;
285
- is_playing = true;
468
+ const reader = new FileReader()
469
+ reader.onload = () => resolve(reader.result as string | ArrayBuffer)
470
+ reader.onerror = () => reject(new Error(`Failed to read file`))
471
+
472
+ // Read as text for text-based formats, binary for others
473
+ if (file.name.toLowerCase().match(/\.(xyz|json|extxyz|lammpstrj)$/)) {
474
+ reader.readAsText(file)
475
+ } else reader.readAsArrayBuffer(file)
476
+ })
477
+ }
478
+
479
+ // Play/pause functionality
480
+ function toggle_play() {
481
+ if (is_playing) pause_playback()
482
+ else start_playback()
483
+ }
484
+ function start_playback() {
485
+ if (total_frames <= 1) return
486
+ is_playing = true
286
487
  if (trajectory) {
287
- on_play?.({ trajectory, step_idx: current_step_idx, frame_count: total_frames });
488
+ on_play?.({ trajectory, step_idx: current_step_idx, frame_count: total_frames })
288
489
  }
289
- }
290
- function pause_playback() {
291
- is_playing = false;
490
+ }
491
+ function pause_playback() {
492
+ is_playing = false
292
493
  if (trajectory) {
293
- on_pause?.({
294
- trajectory: trajectory,
295
- step_idx: current_step_idx,
296
- frame_count: total_frames,
297
- });
494
+ on_pause?.({
495
+ trajectory: trajectory,
496
+ step_idx: current_step_idx,
497
+ frame_count: total_frames,
498
+ })
298
499
  }
299
- }
300
- $effect(() => {
500
+ }
501
+ $effect(() => { // Effect to manage playback interval
301
502
  // Only watch is_playing and frame_rate_ms, not play_interval itself
302
- const playing = is_playing;
303
- const rate_ms = 1000 / fps;
503
+ const playing = is_playing
504
+ const rate_ms = 1000 / fps
505
+
304
506
  if (playing) {
305
- // Clear existing interval if it exists - use untrack to avoid circular dependency
306
- const current_interval = untrack(() => play_interval);
307
- if (current_interval !== undefined)
308
- clearInterval(current_interval);
309
- // Create new interval with current frame rate
310
- play_interval = setInterval(() => {
311
- if (current_step_idx >= total_frames - 1) {
312
- if (trajectory) {
313
- on_end?.({
314
- trajectory,
315
- step_idx: current_step_idx,
316
- frame_count: total_frames,
317
- frame: current_frame || undefined,
318
- });
319
- }
320
- go_to_step(0); // Loop back to 1st step
321
- if (trajectory) {
322
- on_loop?.({ trajectory, frame_count: total_frames });
323
- }
324
- }
325
- else
326
- next_step();
327
- }, rate_ms);
328
- }
329
- else {
330
- // Clear interval when not playing - use untrack to avoid circular dependency
331
- const current_interval = untrack(() => play_interval);
332
- if (current_interval !== undefined) {
333
- clearInterval(current_interval);
334
- play_interval = undefined;
335
- }
507
+ // Clear existing interval if it exists - use untrack to avoid circular dependency
508
+ const current_interval = untrack(() => play_interval)
509
+ if (current_interval !== undefined) clearInterval(current_interval)
510
+
511
+ // Create new interval with current frame rate
512
+ play_interval = setInterval(() => {
513
+ if (current_step_idx >= total_frames - 1) {
514
+ if (trajectory) {
515
+ on_end?.({
516
+ trajectory,
517
+ step_idx: current_step_idx,
518
+ frame_count: total_frames,
519
+ frame: current_frame || undefined,
520
+ })
521
+ }
522
+ go_to_step(0) // Loop back to 1st step
523
+ if (trajectory) {
524
+ on_loop?.({ trajectory, frame_count: total_frames })
525
+ }
526
+ } else next_step()
527
+ }, rate_ms)
528
+ } else {
529
+ // Clear interval when not playing - use untrack to avoid circular dependency
530
+ const current_interval = untrack(() => play_interval)
531
+ if (current_interval !== undefined) {
532
+ clearInterval(current_interval)
533
+ play_interval = undefined
534
+ }
336
535
  }
337
- });
338
- // Cleanup interval on component destroy
339
- $effect(() => () => {
340
- if (play_interval !== undefined)
341
- clearInterval(play_interval);
342
- });
343
- // Handle internal file format drops
344
- async function handle_internal_file_drop(internal_data) {
536
+ })
537
+
538
+ // Cleanup interval on component destroy
539
+ $effect(() => () => {
540
+ if (play_interval !== undefined) clearInterval(play_interval)
541
+ })
542
+
543
+ // Handle internal file format drops
544
+ async function handle_internal_file_drop(internal_data: string): Promise<boolean> {
345
545
  try {
346
- const file_info = JSON.parse(internal_data);
347
- // Check if this is a binary file
348
- if (file_info.is_binary) {
349
- if (file_info.content instanceof ArrayBuffer) {
350
- await load_trajectory_data(file_info.content, file_info.name);
351
- }
352
- else if (file_info.content_url) {
353
- const response = await fetch(file_info.content_url);
354
- const array_buffer = await response.arrayBuffer();
355
- await load_trajectory_data(array_buffer, file_info.name);
356
- }
357
- else {
358
- console.warn(`Binary file without ArrayBuffer or blob URL:`, file_info.name);
359
- }
360
- }
361
- else {
362
- await load_trajectory_data(file_info.content, file_info.name);
546
+ const file_info = JSON.parse(internal_data)
547
+
548
+ // Check if this is a binary file
549
+ if (file_info.is_binary) {
550
+ if (file_info.content instanceof ArrayBuffer) {
551
+ await load_trajectory_data(file_info.content, file_info.name)
552
+ } else if (file_info.content_url) {
553
+ const response = await fetch(file_info.content_url)
554
+ const array_buffer = await response.arrayBuffer()
555
+ await load_trajectory_data(array_buffer, file_info.name)
556
+ } else {
557
+ console.warn(
558
+ `Binary file without ArrayBuffer or blob URL:`,
559
+ file_info.name,
560
+ )
363
561
  }
364
- return true;
365
- }
366
- catch (error) {
367
- console.warn(`Failed to parse internal file data:`, error);
368
- return false;
562
+ } else {
563
+ await load_trajectory_data(file_info.content, file_info.name)
564
+ }
565
+ return true
566
+ } catch (error) {
567
+ console.warn(`Failed to parse internal file data:`, error)
568
+ return false
369
569
  }
370
- }
371
- // Handle file drop events with optimized large file support
372
- async function handle_file_drop(event) {
373
- event.preventDefault();
374
- dragover = false;
375
- if (!allow_file_drop)
376
- return;
377
- loading = true;
570
+ }
571
+
572
+ // Handle file drop events with optimized large file support
573
+ async function handle_file_drop(event: DragEvent) {
574
+ event.preventDefault()
575
+ dragover = false
576
+ if (!allow_file_drop) return
577
+
578
+ loading = true
579
+
378
580
  try {
379
- // Check for our custom internal file format first
380
- const internal_data = event.dataTransfer?.getData(`application/x-matterviz-file`);
381
- if (internal_data) {
382
- const handled = await handle_internal_file_drop(internal_data);
383
- if (handled)
384
- return;
385
- }
386
- // Handle URL-based files (e.g. from FilePicker)
387
- const handled = await handle_url_drop(event, async (content, filename) => {
388
- current_filename = filename;
389
- file_size = content instanceof ArrayBuffer
390
- ? content.byteLength
391
- : new Blob([content]).size;
392
- await load_trajectory_data(content, filename);
393
- }).catch(() => false);
394
- if (handled) {
395
- return;
396
- }
397
- // Handle file system drops with optimized large file support
398
- const file = event.dataTransfer?.files[0];
399
- if (file) {
400
- file_size = file.size;
401
- current_file_path = file.webkitRelativePath || file.name;
402
- file_object = file;
403
- // Read file content directly
404
- const content = await read_file_content(file);
405
- await load_trajectory_data(content, file.name);
406
- }
407
- // Check for plain text data (fallback)
408
- const text_data = event.dataTransfer?.getData(`text/plain`);
409
- if (text_data) {
410
- file_size = new Blob([text_data]).size; // Calculate byte size of text data
411
- await load_trajectory_data(text_data, `trajectory.json`);
412
- return;
413
- }
414
- }
415
- catch (error) {
416
- console.error(`File drop failed:`, error);
417
- error_msg = `Failed to load file: ${error}`;
418
- on_error?.({ error_msg, filename: current_filename, file_size });
419
- }
420
- finally {
421
- loading = false;
581
+ // Check for our custom internal file format first
582
+ const internal_data = event.dataTransfer?.getData(
583
+ `application/x-matterviz-file`,
584
+ )
585
+ if (internal_data) {
586
+ const handled = await handle_internal_file_drop(internal_data)
587
+ if (handled) return
588
+ }
589
+
590
+ // Handle URL-based files (e.g. from FilePicker)
591
+ const handled = await handle_url_drop(event, async (content, filename) => {
592
+ current_filename = filename
593
+ file_size = content instanceof ArrayBuffer
594
+ ? content.byteLength
595
+ : new Blob([content]).size
596
+ await load_trajectory_data(content, filename)
597
+ }).catch(() => false)
598
+
599
+ if (handled) {
600
+ return
601
+ }
602
+
603
+ // Handle file system drops with optimized large file support
604
+ const file = event.dataTransfer?.files[0]
605
+ if (file) {
606
+ file_size = file.size
607
+ current_file_path = file.webkitRelativePath || file.name
608
+ file_object = file
609
+
610
+ // Read file content directly
611
+ const content = await read_file_content(file)
612
+ await load_trajectory_data(content, file.name)
613
+ }
614
+
615
+ // Check for plain text data (fallback)
616
+ const text_data = event.dataTransfer?.getData(`text/plain`)
617
+ if (text_data) {
618
+ file_size = new Blob([text_data]).size // Calculate byte size of text data
619
+ await load_trajectory_data(text_data, `trajectory.json`)
620
+ return
621
+ }
622
+ } catch (error) {
623
+ console.error(`File drop failed:`, error)
624
+ error_msg = `Failed to load file: ${error}`
625
+ on_error?.({ error_msg, filename: current_filename, file_size })
626
+ } finally {
627
+ loading = false
422
628
  }
423
- }
424
- $effect(() => {
629
+ }
630
+
631
+ $effect(() => { // Load trajectory from URL when data_url is provided
425
632
  if (data_url && !trajectory) {
426
- loading = true;
427
- error_msg = null;
428
- load_from_url(data_url, async (content, filename) => {
429
- current_filename = filename;
430
- file_size = content instanceof ArrayBuffer
431
- ? content.byteLength
432
- : new Blob([content]).size;
433
- await load_trajectory_data(content, filename);
434
- })
435
- .then(() => {
436
- loading = false;
633
+ loading = true
634
+ error_msg = null
635
+
636
+ load_from_url(data_url, async (content, filename) => {
637
+ current_filename = filename
638
+ file_size = content instanceof ArrayBuffer
639
+ ? content.byteLength
640
+ : new Blob([content]).size
641
+ await load_trajectory_data(content, filename)
642
+ })
643
+ .then(() => {
644
+ loading = false
437
645
  })
438
- .catch((err) => {
439
- console.error(`Failed to load trajectory from URL:`, err);
440
- error_msg = `Failed to load trajectory: ${err.message}`;
441
- current_filename = undefined;
442
- file_size = undefined;
443
- loading = false;
444
- on_error?.({
445
- error_msg,
446
- filename: current_filename || undefined,
447
- file_size: file_size || undefined,
448
- });
449
- });
450
- }
451
- });
452
- // Watch for frame rate changes
453
- $effect(() => {
454
- on_frame_rate_change?.({ trajectory, fps: fps });
455
- });
456
- async function load_trajectory_data(data, filename) {
457
- loading = true;
458
- error_msg = null;
459
- parsing_progress = null;
460
- // Reset previous loading state
461
- orig_data = null;
462
- try {
463
- const data_size = data instanceof ArrayBuffer ? data.byteLength : data.length;
464
- // Determine loading strategy based on file size
465
- const bin_file_threshold = loading_options.bin_file_threshold ??
466
- MAX_BIN_FILE_SIZE;
467
- const text_file_threshold = loading_options.text_file_threshold ??
468
- MAX_TEXT_FILE_SIZE;
469
- if ((data instanceof ArrayBuffer && data_size > bin_file_threshold) ||
470
- (typeof data === `string` && data_size > text_file_threshold)) { // Large files: Use indexed loading
471
- await load_with_indexing(data, filename);
472
- }
473
- else {
474
- // Small files: Use regular loading
475
- const merged_options = { ...loading_options, atom_type_mapping };
476
- trajectory = await parse_trajectory_async(data, filename, (progress) => {
477
- parsing_progress = progress;
478
- }, merged_options);
479
- }
480
- current_step_idx = 0;
481
- current_filename = filename;
482
- const file_size_bytes = data instanceof ArrayBuffer
483
- ? data.byteLength
484
- : new Blob([data]).size;
485
- on_file_load?.({
486
- trajectory,
487
- frame_count: trajectory?.frames.length ?? 0,
488
- total_atoms: trajectory?.frames[0]?.structure.sites.length ?? 0,
489
- filename,
490
- file_size: file_size_bytes,
491
- });
492
- }
493
- catch (err) {
494
- const unsupported_message = get_unsupported_format_message(filename, typeof data === `string` ? data : ``);
495
- error_msg = unsupported_message || `Failed to parse trajectory: ${err}`;
496
- current_filename = undefined;
497
- file_size = undefined;
498
- on_error?.({
646
+ .catch((err: Error) => {
647
+ console.error(`Failed to load trajectory from URL:`, err)
648
+ error_msg = `Failed to load trajectory: ${err.message}`
649
+ current_filename = undefined
650
+ file_size = undefined
651
+ loading = false
652
+ on_error?.({
499
653
  error_msg,
500
654
  filename: current_filename || undefined,
501
655
  file_size: file_size || undefined,
502
- });
503
- }
504
- finally {
505
- parsing_progress = null;
506
- loading = false;
656
+ })
657
+ })
507
658
  }
508
- }
509
- // Load using indexed parsing for large files
510
- async function load_with_indexing(data, filename) {
511
- try { // Use indexed parsing for efficient large file handling
512
- const merged_options = {
513
- use_indexing: true,
514
- ...loading_options,
515
- atom_type_mapping,
516
- };
659
+ })
660
+
661
+ // Watch for frame rate changes
662
+ $effect(() => {
663
+ on_frame_rate_change?.({ trajectory, fps: fps })
664
+ })
665
+
666
+ async function load_trajectory_data(data: string | ArrayBuffer, filename: string) {
667
+ loading = true
668
+ error_msg = null
669
+ parsing_progress = null
670
+
671
+ // Reset previous loading state
672
+ orig_data = null
673
+
674
+ try {
675
+ const data_size = data instanceof ArrayBuffer ? data.byteLength : data.length
676
+
677
+ // Determine loading strategy based on file size
678
+ const bin_file_threshold = loading_options.bin_file_threshold ??
679
+ MAX_BIN_FILE_SIZE
680
+ const text_file_threshold = loading_options.text_file_threshold ??
681
+ MAX_TEXT_FILE_SIZE
682
+ if (
683
+ (data instanceof ArrayBuffer && data_size > bin_file_threshold) ||
684
+ (typeof data === `string` && data_size > text_file_threshold)
685
+ ) { // Large files: Use indexed loading
686
+ await load_with_indexing(data, filename)
687
+ } else {
688
+ // Small files: Use regular loading
689
+ const merged_options = { ...loading_options, atom_type_mapping }
517
690
  trajectory = await parse_trajectory_async(data, filename, (progress) => {
518
- parsing_progress = progress;
519
- }, merged_options);
520
- // Attach frame loader and original data directly to trajectory for unified access
521
- orig_data = data;
522
- trajectory.frame_loader = create_frame_loader(filename);
691
+ parsing_progress = progress
692
+ }, merged_options)
693
+ }
694
+
695
+ current_step_idx = 0
696
+ current_filename = filename
697
+
698
+ const file_size_bytes = data instanceof ArrayBuffer
699
+ ? data.byteLength
700
+ : new Blob([data]).size
701
+ on_file_load?.({ // emit file load event
702
+ trajectory,
703
+ frame_count: trajectory?.frames.length ?? 0,
704
+ total_atoms: trajectory?.frames[0]?.structure.sites.length ?? 0,
705
+ filename,
706
+ file_size: file_size_bytes,
707
+ })
708
+ } catch (err) {
709
+ const unsupported_message = get_unsupported_format_message(
710
+ filename,
711
+ typeof data === `string` ? data : ``,
712
+ )
713
+ error_msg = unsupported_message || `Failed to parse trajectory: ${err}`
714
+ current_filename = undefined
715
+ file_size = undefined
716
+
717
+ on_error?.({ // emit error event
718
+ error_msg,
719
+ filename: current_filename || undefined,
720
+ file_size: file_size || undefined,
721
+ })
722
+ } finally {
723
+ parsing_progress = null
724
+ loading = false
523
725
  }
524
- catch (error) {
525
- console.error(`Indexed loading failed:`, error);
526
- throw error;
726
+ }
727
+
728
+ // Load using indexed parsing for large files
729
+ async function load_with_indexing(data: string | ArrayBuffer, filename: string) {
730
+ try { // Use indexed parsing for efficient large file handling
731
+ const merged_options = {
732
+ use_indexing: true,
733
+ ...loading_options,
734
+ atom_type_mapping,
735
+ }
736
+ trajectory = await parse_trajectory_async(data, filename, (progress) => {
737
+ parsing_progress = progress
738
+ }, merged_options)
739
+
740
+ // Attach frame loader and original data directly to trajectory for unified access
741
+ orig_data = data
742
+ trajectory.frame_loader = create_frame_loader(filename)
743
+ } catch (error) {
744
+ console.error(`Indexed loading failed:`, error)
745
+ throw error
527
746
  }
528
- }
529
- // Get current view mode label
530
- let current_view_label = $derived.by(() => {
531
- if (display_mode === `structure`)
532
- return `Structure Only`;
533
- if (display_mode === `scatter`)
534
- return `Scatter Only`;
535
- if (display_mode === `histogram`)
536
- return `Histogram Only`;
537
- if (display_mode === `structure+histogram`)
538
- return `Structure + Histogram`;
539
- if (display_mode === `structure+scatter`)
540
- return `Structure + Scatter`;
541
- throw new Error(`Unexpected display mode: ${display_mode}`);
542
- });
543
- let view_mode_dropdown_open = $state(false);
544
- // Handle click outside to close dropdowns
545
- function handle_click_outside(event) {
546
- const target = event.target;
747
+ }
748
+
749
+ // Get current view mode label
750
+ let current_view_label = $derived.by(() => {
751
+ if (display_mode === `structure`) return `Structure Only`
752
+ if (display_mode === `scatter`) return `Scatter Only`
753
+ if (display_mode === `histogram`) return `Histogram Only`
754
+ if (display_mode === `structure+histogram`) return `Structure + Histogram`
755
+ if (display_mode === `structure+scatter`) return `Structure + Scatter`
756
+ throw new Error(`Unexpected display mode: ${display_mode}`)
757
+ })
758
+
759
+ let view_mode_dropdown_open = $state(false)
760
+
761
+ // Handle click outside to close dropdowns
762
+ function handle_click_outside(event: MouseEvent) {
763
+ const target = event.target as Element
547
764
  if (view_mode_dropdown_open) {
548
- const dropdown_wrapper = target.closest(`.view-mode-dropdown-wrapper`);
549
- // Don't close if clicking on dropdown wrapper (contains both button and menu)
550
- if (!dropdown_wrapper)
551
- view_mode_dropdown_open = false;
765
+ const dropdown_wrapper = target.closest(`.view-mode-dropdown-wrapper`)
766
+ // Don't close if clicking on dropdown wrapper (contains both button and menu)
767
+ if (!dropdown_wrapper) view_mode_dropdown_open = false
552
768
  }
553
- }
554
- // Handle keyboard shortcuts
555
- function onkeydown(event) {
556
- if (!trajectory)
557
- return;
769
+ }
770
+
771
+ // Handle keyboard shortcuts
772
+ function onkeydown(event: KeyboardEvent) {
773
+ if (!trajectory) return
774
+
558
775
  // Don't handle shortcuts if user is typing in an input field (but allow if it's our step input and not focused)
559
- const target = event.target;
560
- const is_step_input = target.classList.contains(`step-input`);
776
+ const target = event.target as HTMLElement
777
+ const is_step_input = target.classList.contains(`step-input`)
561
778
  const is_input_focused = target.tagName === `INPUT` ||
562
- target.tagName === `TEXTAREA`;
779
+ target.tagName === `TEXTAREA`
780
+
563
781
  // Skip if typing in an input that's not our step input
564
- if (is_input_focused && !is_step_input)
565
- return;
782
+ if (is_input_focused && !is_step_input) return
783
+
566
784
  // If typing in step input, only handle certain navigation keys
567
785
  if (is_step_input && is_input_focused) {
568
- // Allow normal typing, but handle special navigation keys
569
- if ([`Escape`, `Enter`].includes(event.key))
570
- target.blur(); // Remove focus from input
571
- return;
786
+ // Allow normal typing, but handle special navigation keys
787
+ if ([`Escape`, `Enter`].includes(event.key)) target.blur() // Remove focus from input
788
+ return
572
789
  }
573
- const is_cmd_or_ctrl = event.metaKey || event.ctrlKey;
790
+
791
+ const is_cmd_or_ctrl = event.metaKey || event.ctrlKey
792
+
574
793
  // Navigation shortcuts
575
- if (event.key === ` `)
576
- toggle_play();
794
+ if (event.key === ` `) toggle_play()
577
795
  else if (event.key === `ArrowLeft`) {
578
- if (is_cmd_or_ctrl)
579
- go_to_step(0);
580
- else
581
- prev_step();
582
- }
583
- else if (event.key === `ArrowRight`) {
584
- if (is_cmd_or_ctrl)
585
- go_to_step(total_frames - 1);
586
- else
587
- next_step();
588
- }
589
- else if (event.key === `Home`)
590
- go_to_step(0);
591
- else if (event.key === `End`)
592
- go_to_step(total_frames - 1);
796
+ if (is_cmd_or_ctrl) go_to_step(0)
797
+ else prev_step()
798
+ } else if (event.key === `ArrowRight`) {
799
+ if (is_cmd_or_ctrl) go_to_step(total_frames - 1)
800
+ else next_step()
801
+ } else if (event.key === `Home`) go_to_step(0)
802
+ else if (event.key === `End`) go_to_step(total_frames - 1)
593
803
  else if (event.key === `j`) {
594
- go_to_step(Math.max(0, current_step_idx - 10));
595
- }
596
- else if (event.key === `l`) {
597
- go_to_step(Math.min(total_frames - 1, current_step_idx + 10));
598
- }
599
- else if (event.key === `PageUp`) {
600
- go_to_step(Math.max(0, current_step_idx - 25));
601
- }
602
- else if (event.key === `PageDown`) {
603
- go_to_step(Math.min(total_frames - 1, current_step_idx + 25));
804
+ go_to_step(Math.max(0, current_step_idx - 10))
805
+ } else if (event.key === `l`) {
806
+ go_to_step(Math.min(total_frames - 1, current_step_idx + 10))
807
+ } else if (event.key === `PageUp`) {
808
+ go_to_step(Math.max(0, current_step_idx - 25))
809
+ } else if (event.key === `PageDown`) {
810
+ go_to_step(Math.min(total_frames - 1, current_step_idx + 25))
604
811
  } // Interface shortcuts
605
- else if (event.key === `f` && fullscreen_toggle)
606
- toggle_fullscreen(wrapper);
812
+ else if (event.key === `f` && fullscreen_toggle) toggle_fullscreen(wrapper)
607
813
  // 'i' key handled by the TrajectoryInfoPane's built-in toggle
608
814
  // Playback speed shortcuts (only when playing)
609
815
  else if ((event.key === `=` || event.key === `+`) && is_playing) {
610
- fps = Math.min(fps_range[1], fps + 0.2);
611
- on_frame_rate_change?.({ trajectory, fps: fps });
612
- }
613
- else if (event.key === `-` && is_playing) {
614
- fps = Math.max(fps_range[0], fps - 0.2);
615
- on_frame_rate_change?.({ trajectory, fps: fps });
816
+ fps = Math.min(fps_range[1], fps + 0.2)
817
+ on_frame_rate_change?.({ trajectory, fps: fps })
818
+ } else if (event.key === `-` && is_playing) {
819
+ fps = Math.max(fps_range[0], fps - 0.2)
820
+ on_frame_rate_change?.({ trajectory, fps: fps })
616
821
  } // System shortcuts
617
822
  else if (event.key === `Escape`) {
618
- if (document.fullscreenElement)
619
- document.exitFullscreen();
620
- else if (view_mode_dropdown_open)
621
- view_mode_dropdown_open = false;
622
- // Escape key for info pane handled by DraggablePane
823
+ if (document.fullscreenElement) document.exitFullscreen()
824
+ else if (view_mode_dropdown_open) view_mode_dropdown_open = false
825
+ // Escape key for info pane handled by DraggablePane
623
826
  } // Number keys 0-9 - jump to percentage of trajectory
624
827
  else if (event.key >= `0` && event.key <= `9`) {
625
- go_to_step(Math.floor((parseInt(event.key, 10) / 10) * (total_frames - 1)));
828
+ go_to_step(Math.floor((parseInt(event.key, 10) / 10) * (total_frames - 1)))
626
829
  }
627
- }
628
- // Separate state variables for each pane to match component prop types
629
- let structure_info_open = $state(false);
630
- let structure_controls_open = $state(false);
631
- let scatter_controls = $state({ open: false });
632
- let trajectory_export_open = $state(false);
633
- let fullscreen = $state(false);
830
+ }
831
+
832
+ // Separate state variables for each pane to match component prop types
833
+ let structure_info_open = $state(false)
834
+ let structure_controls_open = $state(false)
835
+ let scatter_controls = $state<ControlsConfig>({ open: false })
836
+ let trajectory_export_open = $state(false)
837
+ let fullscreen = $state(false)
634
838
  </script>
635
839
 
636
840
  <svelte:document
@@ -960,7 +1164,7 @@ let fullscreen = $state(false);
960
1164
  controls={scatter_controls}
961
1165
  current_x_value={current_step_idx}
962
1166
  change={plot_skimming ? handle_plot_change : undefined}
963
- padding={{ t: 20, b: 60, l: 100, r: has_y2_series ? 100 : 20 }}
1167
+ padding={{ t: 20, b: 60, l: 52, r: has_y2_series ? 100 : 20 }}
964
1168
  range_padding={0}
965
1169
  style="height: 100%"
966
1170
  {...scatter_props}
@@ -973,14 +1177,10 @@ let fullscreen = $state(false);
973
1177
  }}
974
1178
  class="plot {scatter_props.class ?? ``}"
975
1179
  >
976
- {#snippet tooltip({ x, y, metadata })}
977
- {#if metadata?.series_label}
978
- Step: {Math.round(x)}<br />
979
- {@html metadata.series_label}: {typeof y === `number` ? format_num(y) : y}
980
- {:else}
981
- Step: {Math.round(x)}<br />
982
- Value: {typeof y === `number` ? format_num(y) : y}
983
- {/if}
1180
+ {#snippet tooltip({ x, y, metadata, label })}
1181
+ {@const formatted_y = typeof y === `number` ? format_num(y) : y}
1182
+ Step: {Math.round(x)}<br />
1183
+ {@html sanitize_html(metadata?.series_label || label || `Value`)}: {formatted_y}
984
1184
  {/snippet}
985
1185
  </ScatterPlot>
986
1186
  {:else if display_mode === `histogram` || display_mode === `structure+histogram`}
@@ -1004,9 +1204,9 @@ let fullscreen = $state(false);
1004
1204
  --ctrl-btn-top="6ex"
1005
1205
  >
1006
1206
  {#snippet tooltip({ value, count, property })}
1207
+ {#if property}<div><strong>{property}</strong></div>{/if}
1007
1208
  <div>Value: {format_num(value)}</div>
1008
1209
  <div>Count: {count}</div>
1009
- <div>{property}</div>
1010
1210
  {/snippet}
1011
1211
  </Histogram>
1012
1212
  {/if}
@@ -1049,61 +1249,56 @@ let fullscreen = $state(false);
1049
1249
  contain: layout;
1050
1250
  z-index: var(--traj-z-index, 1);
1051
1251
  container-type: size; /* enable cqh for panes if explicit height is set */
1052
- }
1053
- .trajectory :global(.plot) {
1054
- background: var(--surface-bg);
1055
- }
1056
- .trajectory.active {
1057
- z-index: 2; /* needed so info/control panes from an active viewer overlay those of the next (if there is one) */
1058
- }
1059
- .trajectory.active .trajectory-controls {
1060
- z-index: 5; /* needed so info/control panes from an active viewer its own plot when active, not sure why needed */
1061
- }
1062
- .trajectory:fullscreen {
1063
- height: 100vh !important;
1064
- width: 100vw !important;
1065
- border-radius: 0 !important;
1066
- background: var(--surface-bg);
1067
- overflow: hidden;
1252
+ :global(.plot) {
1253
+ background: var(--surface-bg);
1254
+ }
1255
+ &.active {
1256
+ z-index: 2; /* needed so info/control panes from an active viewer overlay those of the next (if there is one) */
1257
+ .trajectory-controls {
1258
+ z-index: 5; /* needed so info/control panes from an active viewer its own plot when active, not sure why needed */
1259
+ }
1260
+ }
1261
+ &:fullscreen {
1262
+ height: 100vh !important;
1263
+ width: 100vw !important;
1264
+ border-radius: 0 !important;
1265
+ background: var(--surface-bg);
1266
+ overflow: hidden;
1267
+ }
1268
+ &.horizontal .content-area {
1269
+ grid-template-columns: 1fr 1fr;
1270
+ grid-template-rows: 1fr;
1271
+ }
1272
+ &.vertical .content-area {
1273
+ grid-template-columns: 1fr;
1274
+ grid-template-rows: 1fr 1fr;
1275
+ }
1276
+ /* Display mode specific layouts */
1277
+ &:is(.horizontal, .vertical) .content-area:is(.show-structure-only, .show-plot-only) {
1278
+ grid-template-columns: 1fr !important;
1279
+ grid-template-rows: 1fr !important;
1280
+ }
1281
+ &.dragover {
1282
+ background-color: var(--traj-dragover-bg, var(--dragover-bg));
1283
+ border: var(--traj-dragover-border, var(--dragover-border));
1284
+ }
1285
+ /* Mode: hover - controls visible on component hover */
1286
+ &:hover .trajectory-controls.hover-visible,
1287
+ &:focus-within .trajectory-controls.hover-visible {
1288
+ opacity: 1;
1289
+ pointer-events: auto;
1290
+ }
1068
1291
  }
1069
1292
  /* Content area - grid container for equal sizing */
1070
1293
  .content-area {
1071
1294
  display: grid;
1072
1295
  flex: 1;
1073
1296
  min-height: 0; /* important for tall structure viewers not to overflow */
1074
- }
1075
- .trajectory.horizontal .content-area {
1076
- grid-template-columns: 1fr 1fr;
1077
- grid-template-rows: 1fr;
1078
- }
1079
- .trajectory.vertical .content-area {
1080
- grid-template-columns: 1fr;
1081
- grid-template-rows: 1fr 1fr;
1082
- }
1083
- /* When plot is hidden, structure takes full space */
1084
- .content-area.hide-plot {
1085
- grid-template-columns: 1fr !important;
1086
- grid-template-rows: 1fr !important;
1087
- }
1088
- /* When structure is hidden, plot takes full space */
1089
- .content-area.hide-structure {
1090
- grid-template-columns: 1fr !important;
1091
- grid-template-rows: 1fr !important;
1092
- }
1093
- /* Display mode specific layouts */
1094
- .trajectory.horizontal .content-area.show-structure-only,
1095
- .trajectory.vertical .content-area.show-structure-only {
1096
- grid-template-columns: 1fr !important;
1097
- grid-template-rows: 1fr !important;
1098
- }
1099
- .trajectory.horizontal .content-area.show-plot-only,
1100
- .trajectory.vertical .content-area.show-plot-only {
1101
- grid-template-columns: 1fr !important;
1102
- grid-template-rows: 1fr !important;
1103
- }
1104
- .trajectory.dragover {
1105
- background-color: var(--traj-dragover-bg, var(--dragover-bg));
1106
- border: var(--traj-dragover-border, var(--dragover-border));
1297
+ /* When plot or structure is hidden, the other takes full space */
1298
+ &:is(.hide-plot, .hide-structure) {
1299
+ grid-template-columns: 1fr !important;
1300
+ grid-template-rows: 1fr !important;
1301
+ }
1107
1302
  }
1108
1303
  .trajectory-controls {
1109
1304
  display: flex;
@@ -1117,27 +1312,29 @@ let fullscreen = $state(false);
1117
1312
  opacity: 0;
1118
1313
  pointer-events: none;
1119
1314
  transition: opacity 0.2s ease;
1120
- }
1121
- /* Mode: always - controls always visible */
1122
- .trajectory-controls.always-visible {
1123
- opacity: 1;
1124
- pointer-events: auto;
1125
- }
1126
- /* Mode: hover - controls visible on component hover */
1127
- .trajectory:hover .trajectory-controls.hover-visible {
1128
- opacity: 1;
1129
- pointer-events: auto;
1130
- }
1131
- /* Mode: never - stays hidden (default state, no additional CSS needed) */
1132
- .trajectory-controls:focus-within {
1133
- z-index: var(--traj-controls-z-index, 999999999);
1134
- }
1135
- .trajectory-controls button {
1136
- background: var(--btn-bg);
1137
- font-size: clamp(0.8rem, 2cqw, 1rem);
1138
- }
1139
- .trajectory-controls button:hover:not(:disabled) {
1140
- background: var(--btn-bg-hover);
1315
+ /* Mode: always - controls always visible */
1316
+ &.always-visible {
1317
+ opacity: 1;
1318
+ pointer-events: auto;
1319
+ }
1320
+ /* Mode: never - stays hidden (default state, no additional CSS needed) */
1321
+ &:focus-within {
1322
+ z-index: var(--traj-controls-z-index, 999999999);
1323
+ }
1324
+ button {
1325
+ background: var(--btn-bg);
1326
+ font-size: clamp(0.8rem, 2cqw, 1rem);
1327
+ &:hover:not(:disabled) {
1328
+ background: var(--btn-bg-hover);
1329
+ }
1330
+ }
1331
+ input[type='number'] {
1332
+ &::-webkit-outer-spin-button,
1333
+ &::-webkit-inner-spin-button {
1334
+ -webkit-appearance: none;
1335
+ margin: 0;
1336
+ }
1337
+ }
1141
1338
  }
1142
1339
  .nav-section {
1143
1340
  display: flex;
@@ -1210,68 +1407,73 @@ let fullscreen = $state(false);
1210
1407
  .fullscreen-button {
1211
1408
  background: transparent !important;
1212
1409
  padding: 0;
1213
- }
1214
- .fullscreen-button:hover:not(:disabled) {
1215
- background: var(--border-color);
1410
+ &:hover:not(:disabled) {
1411
+ background: var(--border-color);
1412
+ }
1216
1413
  }
1217
1414
  .info-section {
1218
1415
  display: flex;
1219
1416
  align-items: center;
1220
- gap: clamp(6pt, 1cqw, 1.5ex);
1417
+ gap: clamp(3pt, 0.6cqw, 1ex);
1221
1418
  position: relative;
1222
1419
  }
1420
+ .info-section :global(:is(.trajectory-info-toggle, .trajectory-export-toggle)) {
1421
+ font-size: clamp(1rem, 2.2cqw, 1.1rem);
1422
+ }
1223
1423
  .play-button {
1224
1424
  min-width: clamp(32px, 4cqw, 36px);
1225
- }
1226
- .play-button:hover:not(:disabled) {
1227
- background: var(--traj-play-btn-bg-hover, var(--btn-bg-hover, rgba(0, 0, 0, 0.2)));
1228
- }
1229
- .play-button.playing {
1230
- background: var(--traj-pause-btn-bg, var(--btn-bg, rgba(0, 0, 0, 0.1)));
1231
- }
1232
- .play-button.playing:hover:not(:disabled) {
1233
- background: var(--traj-pause-btn-bg-hover, var(--btn-bg-hover, rgba(0, 0, 0, 0.1)));
1425
+ &:hover:not(:disabled) {
1426
+ background: var(--traj-play-btn-bg-hover, var(--btn-bg-hover, rgba(0, 0, 0, 0.2)));
1427
+ }
1428
+ &.playing {
1429
+ background: var(--traj-pause-btn-bg, var(--btn-bg, rgba(0, 0, 0, 0.1)));
1430
+ &:hover:not(:disabled) {
1431
+ background: var(
1432
+ --traj-pause-btn-bg-hover,
1433
+ var(--btn-bg-hover, rgba(0, 0, 0, 0.1))
1434
+ );
1435
+ }
1436
+ }
1234
1437
  }
1235
1438
  :global(.trajectory-empty-state) {
1236
1439
  padding: 2rem;
1237
1440
  border-radius: var(--border-radius, 3pt);
1238
1441
  background: var(--dropzone-bg);
1442
+ :where(p, ul) {
1443
+ color: var(--text-color-muted);
1444
+ }
1445
+ :where(ul, li, strong) {
1446
+ max-width: var(--trajectory-empty-state-max-width, 500px);
1447
+ margin-inline: auto;
1448
+ }
1239
1449
  }
1240
- :global(.trajectory-empty-state) :where(p, ul) {
1241
- color: var(--text-color-muted);
1242
- }
1243
- :global(.trajectory-empty-state) :where(ul, li, strong) {
1244
- max-width: var(--trajectory-empty-state-max-width, 500px);
1245
- margin-inline: auto;
1246
- }
1247
- button:hover:not(:disabled) {
1248
- background: var(--border-color);
1249
- }
1250
- button:disabled {
1251
- background: var(--btn-disabled-bg);
1252
- color: var(--text-color-muted);
1253
- cursor: not-allowed;
1254
- }
1255
- .trajectory-controls input[type='number']::-webkit-outer-spin-button,
1256
- .trajectory-controls input[type='number']::-webkit-inner-spin-button {
1257
- -webkit-appearance: none;
1258
- margin: 0;
1450
+ button {
1451
+ &:hover:not(:disabled) {
1452
+ background: var(--border-color);
1453
+ }
1454
+ &:disabled {
1455
+ background: var(--btn-disabled-bg);
1456
+ color: var(--text-color-muted);
1457
+ cursor: not-allowed;
1458
+ }
1259
1459
  }
1260
1460
  /* Responsive design */
1261
1461
  @media (orientation: portrait) {
1262
- /* Fallback class for browsers without :has() support */
1263
- .trajectory.show-both-views {
1264
- min-height: calc(var(--min-height) * 2);
1265
- }
1266
- /* Modern browsers: use :has() for same effect */
1267
- @supports selector(:has(.content-area)) {
1268
- .trajectory:has(.content-area.show-both:not(.hide-plot):not(.hide-structure)) {
1462
+ .trajectory {
1463
+ /* Fallback class for browsers without :has() support */
1464
+ &.show-both-views {
1269
1465
  min-height: calc(var(--min-height) * 2);
1270
1466
  }
1271
- }
1272
- .trajectory .content-area.show-both:not(.hide-plot):not(.hide-structure) {
1273
- grid-template-columns: 1fr !important;
1274
- grid-template-rows: 1fr 1fr !important;
1467
+ /* Modern browsers: use :has() for same effect */
1468
+ @supports selector(:has(.content-area)) {
1469
+ &:has(.content-area.show-both:not(.hide-plot):not(.hide-structure)) {
1470
+ min-height: calc(var(--min-height) * 2);
1471
+ }
1472
+ }
1473
+ .content-area.show-both:not(.hide-plot):not(.hide-structure) {
1474
+ grid-template-columns: 1fr !important;
1475
+ grid-template-rows: 1fr 1fr !important;
1476
+ }
1275
1477
  }
1276
1478
  }
1277
1479
  .view-mode-dropdown-wrapper {
@@ -1297,19 +1499,19 @@ let fullscreen = $state(false);
1297
1499
  border-radius: 0;
1298
1500
  text-align: left;
1299
1501
  transition: background-color 0.15s ease;
1300
- }
1301
- .view-mode-option:first-child {
1302
- border-top-left-radius: 3px;
1303
- border-top-right-radius: 3px;
1304
- }
1305
- .view-mode-option.selected {
1306
- color: var(--accent-color);
1307
- }
1308
- .view-mode-option span {
1309
- font-weight: 500;
1310
- white-space: nowrap;
1311
- overflow: hidden;
1312
- text-overflow: ellipsis;
1313
- flex: 1;
1502
+ &:first-child {
1503
+ border-top-left-radius: 3px;
1504
+ border-top-right-radius: 3px;
1505
+ }
1506
+ &.selected {
1507
+ color: var(--accent-color);
1508
+ }
1509
+ span {
1510
+ font-weight: 500;
1511
+ white-space: nowrap;
1512
+ overflow: hidden;
1513
+ text-overflow: ellipsis;
1514
+ flex: 1;
1515
+ }
1314
1516
  }
1315
1517
  </style>