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,274 +1,712 @@
1
- <script lang="ts">import { get_alphabetical_formula } from '../composition/format';
2
- import Icon from '../Icon.svelte';
3
- import { format_num } from '../labels';
4
- import Histogram from '../plot/Histogram.svelte';
5
- import HeatmapTable from '../table/HeatmapTable.svelte';
6
- import { SvelteSet } from 'svelte/reactivity';
7
- let { phase_stats, stable_entries, unstable_entries, ...rest } = $props();
8
- let copied_items = new SvelteSet();
9
- let view_mode = $state(`stats`);
10
- async function copy_to_clipboard(label, value, key) {
1
+ <script lang="ts">
2
+ import {
3
+ get_alphabetical_formula,
4
+ get_electro_neg_formula,
5
+ get_reduced_formula,
6
+ } from '../composition'
7
+ import Icon from '../Icon.svelte'
8
+ import { format_num } from '../labels'
9
+ import { sanitize_html } from '../sanitize'
10
+ import Histogram from '../plot/Histogram.svelte'
11
+ import type { Label, RowData } from '../table'
12
+ import HeatmapTable from '../table/HeatmapTable.svelte'
13
+ import type { HTMLAttributes } from 'svelte/elements'
14
+ import { SvelteMap, SvelteSet } from 'svelte/reactivity'
15
+ import type { ConvexHullEntry, PhaseArityField, PhaseStats } from './types'
16
+ import { get_arity, is_on_hull } from './helpers'
17
+
18
+ let {
19
+ phase_stats,
20
+ stable_entries,
21
+ unstable_entries,
22
+ layout = `toggle`,
23
+ on_entry_click,
24
+ highlighted_entry_id,
25
+ min_n_elements = $bindable(1),
26
+ entry_href,
27
+ ...rest
28
+ }:
29
+ & HTMLAttributes<HTMLDivElement>
30
+ & {
31
+ phase_stats: PhaseStats | null
32
+ stable_entries: ConvexHullEntry[]
33
+ unstable_entries: ConvexHullEntry[]
34
+ // 'toggle' shows stats/table with toggle buttons (default)
35
+ // 'side-by-side' shows both stats and table next to each other without toggle
36
+ layout?: `toggle` | `side-by-side`
37
+ // Called when a table row is clicked, with the corresponding entry
38
+ on_entry_click?: (entry: ConvexHullEntry) => void
39
+ // Entry ID to highlight in the table (e.g. current material on detail page)
40
+ highlighted_entry_id?: string
41
+ // Minimum number of elements filter for table (bindable for URL sync)
42
+ min_n_elements?: number
43
+ // Generate URL for an entry (makes ID column a clickable link)
44
+ entry_href?: (entry: ConvexHullEntry) => string | null
45
+ } = $props()
46
+
47
+ let copied_items = new SvelteSet<string>()
48
+ let view_mode = $state<`stats` | `table`>(`stats`)
49
+ // Formula filter: when set, table shows only entries with this reduced formula
50
+ let formula_filter = $state(``)
51
+ let show_export_dropdown = $state(false)
52
+
53
+ async function copy_to_clipboard(label: string, value: string, key: string) {
11
54
  try {
12
- await navigator.clipboard.writeText(`${label}: ${value}`);
13
- copied_items.add(key);
14
- setTimeout(() => copied_items.delete(key), 1000);
55
+ await navigator.clipboard.writeText(`${label}: ${value}`)
56
+ copied_items.add(key)
57
+ setTimeout(() => copied_items.delete(key), 1000)
58
+ } catch (error) {
59
+ console.error(`Failed to copy to clipboard:`, error)
15
60
  }
16
- catch (error) {
17
- console.error(`Failed to copy to clipboard:`, error);
18
- }
19
- }
20
- // Shared concatenation of stable + unstable for histograms
21
- let all_entries = $derived([...stable_entries, ...unstable_entries]);
22
- // Prepare histogram data for formation energies and hull distances
23
- let e_form_data = $derived.by(() => {
24
- const energies = all_entries
25
- .map((entry) => entry.e_form_per_atom ?? entry.energy_per_atom)
26
- .filter((val) => val !== undefined && isFinite(val));
27
- return [{
28
- x: [],
29
- y: energies,
30
- label: `Formation Energy`,
31
- line_style: { stroke: `steelblue` },
32
- }];
33
- });
34
- let hull_distance_data = $derived.by(() => {
35
- const distances = all_entries
36
- .map((entry) => entry.e_above_hull)
37
- .filter((val) => val !== undefined && isFinite(val));
38
- return [{
39
- x: [],
40
- y: distances,
41
- label: `E above hull`,
42
- line_style: { stroke: `coral` },
43
- }];
44
- });
45
- let pane_data = $derived.by(() => {
46
- if (!phase_stats)
47
- return [];
48
- const sections = [];
49
- // Determine system dimensionality from chemical_system string (count elements)
50
- const num_elements = phase_stats.chemical_system.split(`-`).length;
51
- const max_arity = Math.max(num_elements, phase_stats.quaternary > 0
52
- ? 4
53
- : phase_stats.ternary > 0
54
- ? 3
55
- : phase_stats.binary > 0
56
- ? 2
57
- : 1);
58
- const phase_items = [
59
- {
61
+ }
62
+ function handle_copy_keydown(
63
+ event: KeyboardEvent,
64
+ label: string,
65
+ value: string,
66
+ key: string,
67
+ ): void {
68
+ if (event.key !== `Enter` && event.key !== ` `) return
69
+ event.preventDefault()
70
+ copy_to_clipboard(label, value, key)
71
+ }
72
+
73
+ // Shared concatenation of stable + unstable for histograms
74
+ let all_entries = $derived([...stable_entries, ...unstable_entries])
75
+
76
+ // Static arity labels for phase breakdown display
77
+ const arity_types: [string, PhaseArityField, number][] = [
78
+ [`Unary`, `unary`, 1],
79
+ [`Binary`, `binary`, 2],
80
+ [`Ternary`, `ternary`, 3],
81
+ [`Quaternary`, `quaternary`, 4],
82
+ [`Quinary+`, `quinary_plus`, 5],
83
+ ]
84
+
85
+ const histogram_props = {
86
+ bins: 50,
87
+ y_axis: { label: ``, ticks: 3 },
88
+ show_legend: false,
89
+ show_controls: false,
90
+ padding: { t: 5, b: 22, l: 35, r: 5 },
91
+ style: `height: 100px; --histogram-min-height: 100px`,
92
+ } as const
93
+
94
+ // Prepare histogram data for formation energies and hull distances
95
+ let e_form_data = $derived([{
96
+ x: [] as number[],
97
+ y: all_entries
98
+ .map((entry) => entry.e_form_per_atom ?? entry.energy_per_atom)
99
+ .filter((val): val is number => val !== undefined && isFinite(val)),
100
+ label: `Formation Energy`,
101
+ }])
102
+
103
+ let hull_distance_data = $derived([{
104
+ x: [] as number[],
105
+ y: all_entries
106
+ .map((entry) => entry.e_above_hull)
107
+ .filter((val): val is number => val !== undefined && isFinite(val)),
108
+ label: `E above hull`,
109
+ }])
110
+
111
+ let pane_data = $derived.by(() => {
112
+ if (!phase_stats) return []
113
+
114
+ const pct = (count: number) =>
115
+ phase_stats.total > 0 ? format_num(count / phase_stats.total, `.1~%`) : `0%`
116
+
117
+ return [
118
+ {
119
+ title: ``,
120
+ items: [
121
+ {
60
122
  label: `Total entries in ${phase_stats.chemical_system}`,
61
123
  value: format_num(phase_stats.total),
62
124
  key: `total-entries`,
63
- },
64
- ];
65
- // Only show phase types that exist or are within expected dimensionality
66
- const arity_types = [
67
- [`Unary`, `unary`, 1],
68
- [`Binary`, `binary`, 2],
69
- [`Ternary`, `ternary`, 3],
70
- [`Quaternary`, `quaternary`, 4],
71
- ];
72
- for (const [display, field, min_arity] of arity_types) {
73
- const count = phase_stats[field];
74
- if (count > 0 || max_arity >= min_arity) {
75
- phase_items.push({
76
- label: `${display} phases`,
77
- value: `${format_num(count)} (${format_num(count / phase_stats.total, `.1~%`)})`,
78
- key: `${field}-phases`,
79
- });
80
- }
81
- }
82
- sections.push({ title: ``, items: phase_items });
83
- // Stability
84
- const stable_item = {
85
- label: `Stable phases`,
86
- value: `${format_num(phase_stats.stable)} (${format_num(phase_stats.stable / phase_stats.total, `.1~%`)})`,
87
- key: `stable-phases`,
88
- };
89
- const unstable_item = {
90
- label: `Unstable phases`,
91
- value: `${format_num(phase_stats.unstable)} (${format_num(phase_stats.unstable / phase_stats.total, `.1~%`)})`,
92
- key: `unstable-phases`,
93
- };
94
- sections.push({ title: `Stability`, items: [stable_item, unstable_item] });
95
- // Energy Statistics
96
- const energy_item = {
97
- label: `Min / avg / max (eV/atom)`,
98
- value: `${format_num(phase_stats.energy_range.min, `.3f`)} / ${format_num(phase_stats.energy_range.avg, `.3f`)} / ${format_num(phase_stats.energy_range.max, `.3f`)}`,
99
- key: `formation-energy`,
100
- };
101
- sections.push({
125
+ },
126
+ // Only show phase types that exist or are within the max_arity
127
+ // used when computing stats (respects zeroed-out counts)
128
+ ...arity_types
129
+ .filter(([, field, arity]) =>
130
+ phase_stats[field] > 0 || phase_stats.max_arity >= arity
131
+ )
132
+ .map(([display, field]) => ({
133
+ label: `${display} phases`,
134
+ value: `${format_num(phase_stats[field])} (${pct(phase_stats[field])})`,
135
+ key: `${field}-phases`,
136
+ })),
137
+ ],
138
+ },
139
+ {
140
+ title: `Stability`,
141
+ items: [
142
+ {
143
+ label: `Stable phases`,
144
+ value: `${format_num(phase_stats.stable)} (${pct(phase_stats.stable)})`,
145
+ key: `stable-phases`,
146
+ },
147
+ {
148
+ label: `Unstable phases`,
149
+ value: `${format_num(phase_stats.unstable)} (${
150
+ pct(phase_stats.unstable)
151
+ })`,
152
+ key: `unstable-phases`,
153
+ },
154
+ ],
155
+ },
156
+ {
102
157
  title: `E<sub>form</sub> distribution`,
103
- items: [energy_item],
104
- });
105
- // Hull Distance
106
- const hull_distance_item = {
107
- label: `Max / avg (eV/atom)`,
108
- value: `${format_num(phase_stats.hull_distance.max, `.3f`)} / ${format_num(phase_stats.hull_distance.avg, `.3f`)}`,
109
- key: `hull-distance`,
110
- };
111
- sections.push({
158
+ items: [{
159
+ label: `Min / avg / max (eV/atom)`,
160
+ value: [
161
+ phase_stats.energy_range.min,
162
+ phase_stats.energy_range.avg,
163
+ phase_stats.energy_range.max,
164
+ ]
165
+ .map((val) => format_num(val, `.3f`)).join(` / `),
166
+ key: `formation-energy`,
167
+ }],
168
+ },
169
+ {
112
170
  title: `E<sub>above hull</sub> distribution`,
113
- items: [hull_distance_item],
114
- });
115
- return sections;
116
- });
117
- // Table view: visible entries and feature flags
118
- let visible_entries = $derived(all_entries.filter((entry) => entry.visible));
119
- let has_raw = $derived(visible_entries.some((entry) => entry.energy_per_atom !== undefined));
120
- let has_ids = $derived(visible_entries.some((entry) => entry.entry_id));
121
- let table_data = $derived(visible_entries.map((entry) => {
122
- const counts = Object.values(entry.composition);
123
- const n_atoms = counts.reduce((sum, count) => sum + count, 0);
124
- const row = {
125
- Formula: entry.reduced_formula ?? entry.name ??
126
- get_alphabetical_formula(entry.composition, true, ``),
127
- 'E<sub>hull</sub>': entry.e_above_hull ?? null,
128
- 'E<sub>form</sub>': entry.e_form_per_atom ?? entry.energy_per_atom ?? null,
129
- };
130
- if (has_raw)
131
- row[`E<sub>raw</sub>`] = entry.energy_per_atom;
132
- if (has_ids)
133
- row.ID = entry.entry_id;
134
- row[`N<sub>el</sub>`] = counts.filter((count) => count > 0).length;
135
- row[`N<sub>at</sub>`] = n_atoms;
136
- return row;
137
- }));
138
- let table_columns = $derived.by(() => {
139
- const cols = [
140
- { label: `Formula`, color_scale: null },
141
- {
142
- label: `E<sub>hull</sub>`,
143
- better: `lower`,
144
- color_scale: `interpolateRdYlGn`,
145
- format: `.4f`,
146
- description: `Energy above convex hull (eV/atom)`,
147
- },
148
- {
149
- label: `E<sub>form</sub>`,
150
- better: `lower`,
151
- color_scale: `interpolateBlues`,
152
- format: `.4f`,
153
- description: `Formation energy (eV/atom)`,
154
- },
155
- ];
156
- if (has_raw) {
157
- cols.push({
158
- label: `E<sub>raw</sub>`,
159
- color_scale: `interpolateCool`,
160
- format: `.4f`,
161
- description: `Raw energy per atom (eV/atom)`,
162
- });
171
+ items: [{
172
+ label: `Max / avg (eV/atom)`,
173
+ value: [phase_stats.hull_distance.max, phase_stats.hull_distance.avg]
174
+ .map((val) => format_num(val, `.3f`)).join(` / `),
175
+ key: `hull-distance`,
176
+ }],
177
+ },
178
+ ]
179
+ })
180
+
181
+ // Subsystem coverage: count entries per element pair for the stats pane
182
+ let subsystem_coverage = $derived.by(() => {
183
+ if (!phase_stats) return null
184
+ const elements = phase_stats.chemical_system.split(`-`)
185
+ if (elements.length < 3 || elements.length > 10) return null
186
+ // Count entries containing each pair
187
+ const pair_counts = new SvelteMap<string, number>()
188
+ for (const entry of all_entries) {
189
+ const active =
190
+ (Object.keys(entry.composition) as (keyof typeof entry.composition)[])
191
+ .filter((el) => (entry.composition[el] ?? 0) > 0)
192
+ // Count all pairs present in this entry
193
+ for (let idx_a = 0; idx_a < active.length; idx_a++) {
194
+ for (let idx_b = idx_a + 1; idx_b < active.length; idx_b++) {
195
+ const key = [active[idx_a], active[idx_b]].sort().join(`-`)
196
+ pair_counts.set(key, (pair_counts.get(key) ?? 0) + 1)
197
+ }
198
+ }
163
199
  }
164
- if (has_ids) {
165
- cols.push({ label: `ID`, color_scale: null, description: `Entry identifier` });
200
+ // Build pairs list sorted by element order in chemical_system
201
+ return elements.flatMap((el_a, idx_a) =>
202
+ elements.slice(idx_a + 1).map((el_b) => {
203
+ const key = [el_a, el_b].sort().join(`-`)
204
+ return { pair: key, count: pair_counts.get(key) ?? 0 }
205
+ })
206
+ )
207
+ })
208
+ let subsystem_coverage_summary = $derived(
209
+ subsystem_coverage?.map(({ pair, count }) => `${pair}: ${count}`).join(` | `) ??
210
+ null,
211
+ )
212
+
213
+ // Table view: visible entries filtered by min element count and formula
214
+ let visible_entries = $derived(
215
+ all_entries.filter((entry) => {
216
+ if (!entry.visible) return false
217
+ if (min_n_elements > 1 && get_arity(entry) < min_n_elements) return false
218
+ if (
219
+ active_formula_filter &&
220
+ composition_key(entry.composition) !== active_formula_filter
221
+ ) return false
222
+ return true
223
+ }),
224
+ )
225
+ let has_raw = $derived(
226
+ visible_entries.some((entry) => entry.energy_per_atom !== undefined),
227
+ )
228
+ let has_ids = $derived(visible_entries.some((entry) => entry.entry_id))
229
+ let max_n_el = $derived(
230
+ all_entries.reduce((max, entry) => Math.max(max, get_arity(entry)), 1),
231
+ )
232
+
233
+ // Sortable HTML cell with a hidden data-sort-value for HeatmapTable sorting
234
+ const sort_span = (sort_val: number | string, display: string, attrs = ``) =>
235
+ `<span data-sort-value="${sort_val}"${attrs ? ` ${attrs}` : ``}>${display}</span>`
236
+
237
+ // Escape HTML special chars to prevent XSS when rendering user-supplied strings via {@html}
238
+ const escape_html = (str: string): string =>
239
+ str
240
+ .replace(/&/g, `&amp;`)
241
+ .replace(/</g, `&lt;`)
242
+ .replace(/>/g, `&gt;`)
243
+ .replace(/"/g, `&quot;`)
244
+ .replace(/'/g, `&#39;`)
245
+ const unescape_html = (str: string, max_rounds = 5): string => {
246
+ let decoded = str
247
+ for (let round_idx = 0; round_idx < max_rounds; round_idx++) {
248
+ const next_decoded = decoded
249
+ .replace(/&amp;/g, `&`)
250
+ .replace(/&lt;/g, `<`)
251
+ .replace(/&gt;/g, `>`)
252
+ .replace(/&quot;/g, `"`)
253
+ .replace(/&#39;/g, `'`)
254
+ if (next_decoded === decoded) break
255
+ decoded = next_decoded
256
+ }
257
+ return decoded
258
+ }
259
+ // Convert legacy/html formula strings like Fe<sub>2</sub>O<sub>3</sub> back to plain
260
+ // stoichiometric input before parsing/reordering.
261
+ const normalize_formula_markup = (formula: string): string =>
262
+ unescape_html(formula)
263
+ .replaceAll(/<sub>\s*([^<]+?)\s*<\/sub>/gi, `$1`)
264
+ .replaceAll(/<[^>]+>/g, ``)
265
+ .replaceAll(/\s+/g, ``)
266
+ const sanitize_href = (href: string | null | undefined): string | null => {
267
+ const trimmed_href = href?.trim()
268
+ if (!trimmed_href) return null
269
+ const lower_href = trimmed_href.toLowerCase()
270
+ const blocked_schemes = [`javascript:`, `data:`, `vbscript:`]
271
+ if (blocked_schemes.some((scheme) => lower_href.startsWith(scheme))) return null
272
+ return trimmed_href
273
+ }
274
+ // Serialize reduced composition to a stable string key for polymorph counting
275
+ const composition_key = (comp: Record<string, number>): string =>
276
+ get_alphabetical_formula(get_reduced_formula(comp), true, ``)
277
+
278
+ // Count polymorphs per reduced formula across all entries
279
+ let polymorph_counts = $derived.by(() => {
280
+ const counts = new SvelteMap<string, number>()
281
+ for (const entry of all_entries) {
282
+ const key = composition_key(entry.composition)
283
+ counts.set(key, (counts.get(key) ?? 0) + 1)
284
+ }
285
+ return counts
286
+ })
287
+ let poly_formulas = $derived(
288
+ [...polymorph_counts.entries()]
289
+ .filter(([, count]) => count > 1)
290
+ .sort(([, count_a], [, count_b]) => count_b - count_a),
291
+ )
292
+ let has_polymorphs = $derived(poly_formulas.length > 0)
293
+ let active_formula_filter = $derived.by(() => {
294
+ if (!formula_filter || !has_polymorphs) return ``
295
+ return poly_formulas.some(([formula]) => formula === formula_filter)
296
+ ? formula_filter
297
+ : ``
298
+ })
299
+ $effect(() => {
300
+ if (formula_filter && formula_filter !== active_formula_filter) {
301
+ formula_filter = ``
166
302
  }
167
- cols.push({
303
+ })
304
+
305
+ // Build table rows and a WeakMap from row→entry for the click handler
306
+ let { table_data, entry_by_row } = $derived.by(() => {
307
+ const map = new WeakMap<RowData, ConvexHullEntry>()
308
+ const rows = visible_entries.map((entry, idx) => {
309
+ const n_atoms = Object.values(entry.composition).reduce(
310
+ (sum, count) => sum + count,
311
+ 0,
312
+ )
313
+ const on_hull = is_on_hull(entry)
314
+ const formula_source = entry.reduced_formula ?? entry.name ??
315
+ get_alphabetical_formula(entry.composition, true, ``)
316
+ const normalized_formula = normalize_formula_markup(formula_source)
317
+ const formatted_formula = get_electro_neg_formula(normalized_formula)
318
+ const formula_html = formatted_formula || escape_html(normalized_formula)
319
+ // Match by entry_id or common data fields (mat_id, structure_id)
320
+ // since entry_id may be wrapped in HTML (e.g. <a> tags)
321
+ const entry_data = entry.data as Record<string, unknown> | undefined
322
+ const is_highlighted = !!(highlighted_entry_id && (
323
+ entry.entry_id === highlighted_entry_id ||
324
+ entry_data?.mat_id === highlighted_entry_id ||
325
+ entry_data?.structure_id === highlighted_entry_id
326
+ ))
327
+ const row: RowData = {
328
+ '#': sort_span(idx + 1, `${idx + 1}`),
329
+ Formula: on_hull ? `<strong>${formula_html}</strong>` : formula_html,
330
+ 'E<sub>hull</sub>': entry.e_above_hull ?? null,
331
+ 'E<sub>form</sub>': entry.e_form_per_atom ?? entry.energy_per_atom ?? null,
332
+ }
333
+ if (has_raw) row[`E<sub>raw</sub>`] = entry.energy_per_atom
334
+ if (has_ids) {
335
+ const safe_href = sanitize_href(entry_href?.(entry))
336
+ const safe_id = entry.entry_id ? escape_html(entry.entry_id) : undefined
337
+ row.ID = safe_href && safe_id
338
+ ? `<a href="${
339
+ escape_html(safe_href)
340
+ }" target="_blank" rel="noopener">${safe_id}</a>`
341
+ : safe_id
342
+ }
343
+ if (has_polymorphs) {
344
+ const comp_key = composition_key(entry.composition)
345
+ const poly_count = polymorph_counts.get(comp_key) ?? 1
346
+ row.Poly = poly_count
347
+ }
348
+ row[`N<sub>el</sub>`] = get_arity(entry)
349
+ row[`N<sub>at</sub>`] = n_atoms
350
+ // Highlight row for current material
351
+ if (is_highlighted) {
352
+ row.style =
353
+ `background: color-mix(in srgb, var(--hull-stable-color, #22c55e) 15%, transparent)`
354
+ }
355
+ map.set(row, entry)
356
+ return row
357
+ })
358
+ return { table_data: rows, entry_by_row: map }
359
+ })
360
+
361
+ function handle_row_click(_event: KeyboardEvent | MouseEvent, row: RowData): void {
362
+ const entry = entry_by_row.get(row)
363
+ if (entry) on_entry_click?.(entry)
364
+ }
365
+
366
+ let table_columns: Label[] = $derived(
367
+ [
368
+ { label: `#`, color_scale: null, description: `Row number` },
369
+ { label: `Formula`, color_scale: null },
370
+ {
371
+ label: `E<sub>hull</sub>`,
372
+ better: `lower`,
373
+ color_scale: `interpolateRdYlGn`,
374
+ format: `.4f`,
375
+ description: `Energy above convex hull (eV/atom)`,
376
+ },
377
+ {
378
+ label: `E<sub>form</sub>`,
379
+ better: `lower`,
380
+ color_scale: `interpolateBlues`,
381
+ format: `.4f`,
382
+ description: `Formation energy (eV/atom)`,
383
+ },
384
+ ...(has_raw
385
+ ? [{
386
+ label: `E<sub>raw</sub>`,
387
+ color_scale: `interpolateCool` as const,
388
+ format: `.4f`,
389
+ description: `Raw energy per atom (eV/atom)`,
390
+ }]
391
+ : []),
392
+ ...(has_ids
393
+ ? [{ label: `ID`, color_scale: null, description: `Entry identifier` }]
394
+ : []),
395
+ ...(has_polymorphs
396
+ ? [{
397
+ label: `Poly`,
398
+ color_scale: null,
399
+ description: `Number of polymorphs (same reduced formula)`,
400
+ }]
401
+ : []),
402
+ {
168
403
  label: `N<sub>el</sub>`,
169
404
  color_scale: null,
170
405
  description: `Number of elements`,
171
- }, {
406
+ },
407
+ {
172
408
  label: `N<sub>at</sub>`,
173
409
  color_scale: null,
174
410
  format: `d`,
175
411
  description: `Number of atoms in unit cell`,
176
- });
177
- return cols;
178
- });
412
+ },
413
+ ] satisfies Label[],
414
+ )
415
+
416
+ const html_to_text = (val: unknown): string => {
417
+ if (val == null) return ``
418
+ if (typeof val !== `string`) return String(val)
419
+ const temp_el = document.createElement(`div`)
420
+ temp_el.innerHTML = val
421
+ return temp_el.textContent?.trim() ?? ``
422
+ }
423
+ const csv_escape = (val: string): string =>
424
+ /[",\n]/.test(val) ? `"${val.replaceAll(`"`, `""`)}"` : val
425
+ const get_export_filename = (format: `csv` | `json`): string => {
426
+ const system = (phase_stats?.chemical_system ?? `convex-hull-stats`)
427
+ .toLowerCase()
428
+ .replaceAll(/\s+/g, `-`)
429
+ return `${system}.${format}`
430
+ }
431
+ const build_export_rows = () => {
432
+ const column_labels = table_columns.map((col) => col.label)
433
+ return table_data.map((row) =>
434
+ Object.fromEntries(
435
+ column_labels.map((label) => [html_to_text(label), html_to_text(row[label])]),
436
+ )
437
+ )
438
+ }
439
+ const download_file = (
440
+ content: string,
441
+ filename: string,
442
+ mime_type: string,
443
+ ): void => {
444
+ const blob = new Blob([content], { type: mime_type })
445
+ const object_url = URL.createObjectURL(blob)
446
+ const link_el = document.createElement(`a`)
447
+ link_el.href = object_url
448
+ link_el.download = filename
449
+ document.body.append(link_el)
450
+ link_el.click()
451
+ link_el.remove()
452
+ URL.revokeObjectURL(object_url)
453
+ }
454
+ function export_table(format: `csv` | `json`): void {
455
+ const rows = build_export_rows()
456
+ if (format === `json`) {
457
+ download_file(
458
+ JSON.stringify(rows, null, 2),
459
+ get_export_filename(`json`),
460
+ `application/json;charset=utf-8`,
461
+ )
462
+ return
463
+ }
464
+ const headers = rows.length > 0 ? Object.keys(rows[0]) : []
465
+ const csv_lines = [
466
+ headers.map(csv_escape).join(`,`),
467
+ ...rows.map((row) =>
468
+ headers.map((header) => csv_escape(row[header] ?? ``)).join(`,`)
469
+ ),
470
+ ]
471
+ download_file(
472
+ csv_lines.join(`\n`),
473
+ get_export_filename(`csv`),
474
+ `text/csv;charset=utf-8`,
475
+ )
476
+ }
179
477
  </script>
180
478
 
181
- <div {...rest} class="convex-hull-stats {rest.class ?? ``}">
182
- <div class="view-toggle">
183
- <button class:active={view_mode === `stats`} onclick={() => view_mode = `stats`}>
184
- Stats
185
- </button>
186
- <button class:active={view_mode === `table`} onclick={() => view_mode = `table`}>
187
- Table
188
- </button>
189
- </div>
190
- {#if view_mode === `stats`}
191
- {#each pane_data as section, sec_idx (sec_idx)}
192
- {#if sec_idx > 0}<hr />{/if}
193
- <section>
194
- {#if section.title}
195
- <h5>{@html section.title}</h5>
196
- {/if}
197
- {#each section.items as item (item.key ?? item.label)}
198
- {@const { key, label, value } = item}
199
- <div
200
- class="clickable stat-item"
201
- data-testid={key ? `pd-${key}` : undefined}
202
- title="Click to copy: {label}: {value}"
203
- onclick={() => copy_to_clipboard(item.label, String(item.value), key ?? item.label)}
204
- role="button"
205
- tabindex="0"
206
- onkeydown={(event) => {
207
- if (event.key === `Enter` || event.key === ` `) {
208
- event.preventDefault()
209
- copy_to_clipboard(item.label, String(item.value), key ?? item.label)
210
- }
479
+ {#snippet stats_panel()}
480
+ {#each pane_data as section, sec_idx (sec_idx)}
481
+ {#if sec_idx > 0}<hr />{/if}
482
+ <section>
483
+ {#if section.title}
484
+ <h5>{@html sanitize_html(section.title)}</h5>
485
+ {/if}
486
+ {#each section.items as item (item.key ?? item.label)}
487
+ {@const { key, label, value } = item}
488
+ <div
489
+ class="clickable stat-item"
490
+ data-testid={key ? `pd-${key}` : undefined}
491
+ title="Click to copy: {label}: {value}"
492
+ onclick={() => copy_to_clipboard(item.label, String(item.value), key ?? item.label)}
493
+ role="button"
494
+ tabindex="0"
495
+ onkeydown={(event) =>
496
+ handle_copy_keydown(
497
+ event,
498
+ item.label,
499
+ String(item.value),
500
+ key ?? item.label,
501
+ )}
502
+ >
503
+ <span>{@html sanitize_html(label)}:</span>
504
+ <span>{@html sanitize_html(value)}</span>
505
+ {#if key && copied_items.has(key)}
506
+ <Icon
507
+ icon="Check"
508
+ style="color: var(--success-color, #10b981); width: 12px; height: 12px"
509
+ class="copy-checkmark"
510
+ />
511
+ {/if}
512
+ </div>
513
+ {/each}
514
+
515
+ {#if sec_idx === 0 && subsystem_coverage}
516
+ <div
517
+ class="clickable stat-item subsystem-coverage-row"
518
+ data-testid="pd-binary-subsystem-coverage"
519
+ title="Click to copy: Binary subsystem coverage: {subsystem_coverage_summary ?? ``}"
520
+ onclick={() =>
521
+ copy_to_clipboard(
522
+ `Binary subsystem coverage`,
523
+ subsystem_coverage_summary ?? ``,
524
+ `binary-subsystem-coverage`,
525
+ )}
526
+ role="button"
527
+ tabindex="0"
528
+ onkeydown={(event) =>
529
+ handle_copy_keydown(
530
+ event,
531
+ `Binary subsystem coverage`,
532
+ subsystem_coverage_summary ?? ``,
533
+ `binary-subsystem-coverage`,
534
+ )}
535
+ >
536
+ <span class="subsystem-label"
537
+ >Binary subsystem coverage ({subsystem_coverage.length} pairs)</span>
538
+ <span class="subsystem-chips">
539
+ {#each subsystem_coverage as { pair, count } (pair)}
540
+ <span class="subsystem-chip" class:has-entries={count > 0}>
541
+ <span class="pair">{pair}</span>
542
+ <span class="count">{count}</span>
543
+ </span>
544
+ {/each}
545
+ </span>
546
+ {#if copied_items.has(`binary-subsystem-coverage`)}
547
+ <Icon
548
+ icon="Check"
549
+ style="color: var(--success-color, #10b981); width: 12px; height: 12px"
550
+ class="copy-checkmark"
551
+ />
552
+ {/if}
553
+ </div>
554
+ {/if}
555
+
556
+ {#if section.title === `E<sub>form</sub> distribution` &&
557
+ e_form_data[0].y.length > 0}
558
+ <Histogram
559
+ {...histogram_props}
560
+ series={e_form_data}
561
+ x_axis={{ label: ``, format: `.2f` }}
562
+ bar={{ color: `steelblue`, opacity: 0.7 }}
563
+ />
564
+ {/if}
565
+
566
+ {#if section.title === `E<sub>above hull</sub> distribution` &&
567
+ hull_distance_data[0].y.length > 0}
568
+ <Histogram
569
+ {...histogram_props}
570
+ series={hull_distance_data}
571
+ x_axis={{ label: ``, format: `.2f`, range: [0, null] }}
572
+ bar={{ color: `coral`, opacity: 0.7 }}
573
+ />
574
+ {/if}
575
+ </section>
576
+ {/each}
577
+ {/snippet}
578
+
579
+ {#snippet table_panel()}
580
+ <div class="table-filters">
581
+ {#if max_n_el > 2}
582
+ <label>
583
+ Min N<sub>el</sub>:
584
+ <select bind:value={min_n_elements}>
585
+ {#each Array.from({ length: max_n_el }, (_, idx) => idx + 1) as nel (nel)}
586
+ <option value={nel}>{nel}{nel === 1 ? ` (all)` : ``}</option>
587
+ {/each}
588
+ </select>
589
+ </label>
590
+ {/if}
591
+ {#if has_polymorphs}
592
+ <label>
593
+ Polymorphs:
594
+ <select bind:value={formula_filter}>
595
+ <option value="">all</option>
596
+ {#each poly_formulas as [formula, count] (formula)}
597
+ <option value={formula}>{formula} ({count})</option>
598
+ {/each}
599
+ </select>
600
+ </label>
601
+ {/if}
602
+ <span class="filter-count">{visible_entries.length} entries</span>
603
+ <span class="filter-spacer"></span>
604
+ <div class="export-actions">
605
+ <button
606
+ class="icon-btn"
607
+ class:active={show_export_dropdown}
608
+ title="Export"
609
+ onclick={() => show_export_dropdown = !show_export_dropdown}
610
+ >
611
+ <Icon icon="Export" style="width: 14px" />
612
+ </button>
613
+ {#if show_export_dropdown}
614
+ <div class="export-dropdown">
615
+ <button
616
+ class="dropdown-option"
617
+ onclick={() => {
618
+ export_table(`csv`)
619
+ show_export_dropdown = false
211
620
  }}
212
621
  >
213
- <span>{@html label}:</span>
214
- <span>{@html value}</span>
215
- {#if key && copied_items.has(key)}
216
- <Icon
217
- icon="Check"
218
- style="color: var(--success-color, #10b981); width: 12px; height: 12px"
219
- class="copy-checkmark"
220
- />
221
- {/if}
222
- </div>
223
- {/each}
224
-
225
- {#if section.title === `E<sub>form</sub> distribution` &&
226
- e_form_data[0].y.length > 0}
227
- <Histogram
228
- series={e_form_data}
229
- bins={50}
230
- x_axis={{ label: ``, format: `.2f` }}
231
- y_axis={{ label: ``, ticks: 3 }}
232
- show_legend={false}
233
- show_controls={false}
234
- padding={{ t: 5, b: 22, l: 35, r: 5 }}
235
- style="height: 100px; --histogram-min-height: 100px"
236
- bar={{ color: `steelblue`, opacity: 0.7 }}
237
- />
238
- {/if}
622
+ <Icon icon="Download" style="width: 12px" /> CSV
623
+ </button>
624
+ <button
625
+ class="dropdown-option"
626
+ onclick={() => {
627
+ export_table(`json`)
628
+ show_export_dropdown = false
629
+ }}
630
+ >
631
+ <Icon icon="Download" style="width: 12px" /> JSON
632
+ </button>
633
+ </div>
634
+ {/if}
635
+ </div>
636
+ </div>
637
+ <HeatmapTable
638
+ data={table_data}
639
+ columns={table_columns}
640
+ initial_sort={{ column: `E<sub>hull</sub>`, direction: `asc` }}
641
+ scroll_style={layout === `side-by-side`
642
+ ? `flex: 1 1 0; max-width: 100%; overflow: auto`
643
+ : `max-height: var(--hull-stats-max-height, 500px)`}
644
+ style="width: 100%"
645
+ root_style={layout === `side-by-side`
646
+ ? `flex: 1 1 0; min-height: 0; margin-inline: 0`
647
+ : undefined}
648
+ onrowclick={on_entry_click ? handle_row_click : undefined}
649
+ export_data={false}
650
+ />
651
+ {/snippet}
239
652
 
240
- {#if section.title === `E<sub>above hull</sub> distribution` &&
241
- hull_distance_data[0].y.length > 0}
242
- <Histogram
243
- series={hull_distance_data}
244
- bins={50}
245
- x_axis={{ label: ``, format: `.2f`, range: [0, null] }}
246
- y_axis={{ label: ``, ticks: 3 }}
247
- show_legend={false}
248
- show_controls={false}
249
- padding={{ t: 5, b: 22, l: 35, r: 5 }}
250
- style="height: 100px; --histogram-min-height: 100px"
251
- bar={{ color: `coral`, opacity: 0.7 }}
252
- />
253
- {/if}
254
- </section>
255
- {/each}
256
- {:else}
257
- <HeatmapTable
258
- data={table_data}
259
- columns={table_columns}
260
- initial_sort={{ column: `E<sub>hull</sub>`, direction: `asc` }}
261
- scroll_style="max-height: var(--hull-stats-max-height, 500px)"
262
- style="width: 100%"
263
- />
264
- {/if}
265
- </div>
653
+ {#if layout === `side-by-side`}
654
+ <div {...rest} class="convex-hull-stats side-by-side {rest.class ?? ``}">
655
+ <div class="stats-pane">
656
+ {@render stats_panel()}
657
+ </div>
658
+ <div class="table-pane">
659
+ {@render table_panel()}
660
+ </div>
661
+ </div>
662
+ {:else}
663
+ <div {...rest} class="convex-hull-stats {rest.class ?? ``}">
664
+ <div class="view-toggle">
665
+ <button class:active={view_mode === `stats`} onclick={() => view_mode = `stats`}>
666
+ Stats
667
+ </button>
668
+ <button class:active={view_mode === `table`} onclick={() => view_mode = `table`}>
669
+ Table
670
+ </button>
671
+ </div>
672
+ {#if view_mode === `stats`}
673
+ {@render stats_panel()}
674
+ {:else}
675
+ {@render table_panel()}
676
+ {/if}
677
+ </div>
678
+ {/if}
266
679
 
267
680
  <style>
268
681
  .convex-hull-stats {
269
682
  background: var(--hull-stats-bg, var(--hull-bg));
270
683
  border-radius: var(--hull-border-radius, var(--border-radius, 3pt));
271
- padding: 1em;
684
+ padding: var(--hull-stats-padding, 1em);
685
+ }
686
+ .convex-hull-stats.side-by-side {
687
+ display: flex;
688
+ gap: var(--hull-stats-gap, 1.5em);
689
+ align-items: stretch;
690
+ width: fit-content;
691
+ max-width: 100%;
692
+ margin-inline: auto;
693
+ }
694
+ .stats-pane {
695
+ flex: 0 0 auto;
696
+ width: fit-content;
697
+ min-width: var(--hull-stats-pane-min-width, 200px);
698
+ max-width: var(--hull-stats-pane-max-width, 320px);
699
+ }
700
+ .table-pane {
701
+ flex: 1 1 0;
702
+ max-width: 100%;
703
+ min-width: 0;
704
+ overflow: auto;
705
+ display: flex;
706
+ flex-direction: column;
707
+ }
708
+ .convex-hull-stats :global(tbody tr[onclick]) {
709
+ cursor: pointer;
272
710
  }
273
711
  section div {
274
712
  display: flex;
@@ -301,7 +739,7 @@ let table_columns = $derived.by(() => {
301
739
  }
302
740
  }
303
741
  .stat-item span:first-child {
304
- color: var(--text-color-muted, #666);
742
+ color: var(--text-color-muted, light-dark(#666, #bbb));
305
743
  }
306
744
  section h5 {
307
745
  margin: 0 0 6px 0;
@@ -312,8 +750,9 @@ let table_columns = $derived.by(() => {
312
750
  }
313
751
  .view-toggle button {
314
752
  flex: 1;
315
- padding: 4pt 8pt;
316
- border: 1px solid color-mix(in srgb, currentColor 20%, transparent);
753
+ padding: 2pt 8pt;
754
+ border: 1px solid
755
+ var(--hull-stats-border-color, color-mix(in srgb, currentColor 20%, transparent));
317
756
  background: transparent;
318
757
  color: inherit;
319
758
  cursor: pointer;
@@ -327,7 +766,140 @@ let table_columns = $derived.by(() => {
327
766
  border-left: none;
328
767
  }
329
768
  .view-toggle button.active {
330
- background: light-dark(rgba(0, 0, 0, 0.1), rgba(255, 255, 255, 0.15));
769
+ background: var(
770
+ --hull-stats-toggle-active-bg,
771
+ light-dark(rgba(0, 0, 0, 0.1), rgba(255, 255, 255, 0.15))
772
+ );
331
773
  font-weight: 500;
332
774
  }
775
+ .table-filters {
776
+ display: flex;
777
+ align-items: center;
778
+ flex-wrap: wrap;
779
+ gap: 0.75em;
780
+ margin-bottom: 6pt;
781
+ font-size: 0.85em;
782
+ label {
783
+ display: flex;
784
+ align-items: center;
785
+ gap: 0.4em;
786
+ sub {
787
+ margin-left: -0.2em;
788
+ font-size: 0.72em;
789
+ line-height: 0;
790
+ vertical-align: baseline;
791
+ position: relative;
792
+ top: 0.33em;
793
+ }
794
+ }
795
+ select {
796
+ padding: 2pt 4pt;
797
+ border: 1px solid
798
+ var(--hull-stats-border-color, color-mix(in srgb, currentColor 20%, transparent));
799
+ border-radius: 3pt;
800
+ background: transparent;
801
+ color: inherit;
802
+ font-size: inherit;
803
+ }
804
+ }
805
+ .filter-spacer {
806
+ flex: 1 1 auto;
807
+ }
808
+ .export-actions {
809
+ position: relative;
810
+ .icon-btn {
811
+ padding: 2pt 6pt;
812
+ border: 1px solid
813
+ var(--hull-stats-border-color, color-mix(in srgb, currentColor 20%, transparent));
814
+ border-radius: 3pt;
815
+ background: transparent;
816
+ color: inherit;
817
+ cursor: pointer;
818
+ display: inline-flex;
819
+ align-items: center;
820
+ justify-content: center;
821
+ }
822
+ .icon-btn:hover {
823
+ background: color-mix(in srgb, currentColor 8%, transparent);
824
+ }
825
+ .icon-btn.active {
826
+ background: color-mix(in srgb, currentColor 12%, transparent);
827
+ }
828
+ }
829
+ .export-dropdown {
830
+ position: absolute;
831
+ right: 0;
832
+ top: calc(100% + 4px);
833
+ display: flex;
834
+ flex-direction: column;
835
+ min-width: 88px;
836
+ padding: 3pt;
837
+ border: 1px solid
838
+ var(--hull-stats-border-color, color-mix(in srgb, currentColor 20%, transparent));
839
+ border-radius: 4pt;
840
+ background: var(--page-bg, Canvas);
841
+ z-index: 4;
842
+ box-shadow: 0 2px 8px color-mix(in srgb, black 20%, transparent);
843
+ .dropdown-option {
844
+ display: inline-flex;
845
+ align-items: center;
846
+ gap: 5px;
847
+ border: none;
848
+ border-radius: 3pt;
849
+ background: transparent;
850
+ color: inherit;
851
+ cursor: pointer;
852
+ text-align: left;
853
+ padding: 3pt 6pt;
854
+ }
855
+ .dropdown-option:hover {
856
+ background: color-mix(in srgb, currentColor 8%, transparent);
857
+ }
858
+ }
859
+ .table-pane :global(.control-buttons) {
860
+ display: none;
861
+ margin: 0;
862
+ }
863
+ .filter-count {
864
+ color: var(--text-color-muted, light-dark(#666, #bbb));
865
+ font-size: 0.9em;
866
+ }
867
+ .subsystem-coverage-row {
868
+ flex-wrap: wrap;
869
+ gap: 4pt 1em;
870
+ justify-content: flex-start;
871
+ .subsystem-label {
872
+ color: var(--text-color-muted, light-dark(#666, #bbb));
873
+ font-size: 0.9em;
874
+ }
875
+ .subsystem-chips {
876
+ display: flex;
877
+ flex-wrap: wrap;
878
+ gap: 4pt;
879
+ }
880
+ }
881
+ .subsystem-chip {
882
+ display: inline-flex;
883
+ align-items: center;
884
+ gap: 0;
885
+ padding: 1pt 5pt;
886
+ border-radius: 3pt;
887
+ font-size: 0.78em;
888
+ line-height: 1.2;
889
+ background: color-mix(in srgb, currentColor 5%, transparent);
890
+ color: var(--text-color-muted, light-dark(#666, #bbb));
891
+ .pair {
892
+ font-weight: 500;
893
+ }
894
+ .count {
895
+ margin-left: 3pt;
896
+ font-size: 0.9em;
897
+ font-weight: 600;
898
+ color: color-mix(in srgb, currentColor 70%, transparent);
899
+ }
900
+ }
901
+ .subsystem-chip.has-entries {
902
+ background: color-mix(in srgb, var(--hull-stable-color, #22c55e) 15%, transparent);
903
+ color: inherit;
904
+ }
333
905
  </style>