matterviz 0.4.0 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (527) hide show
  1. package/dist/EmptyState.svelte +10 -4
  2. package/dist/FilePicker.svelte +12 -14
  3. package/dist/Icon.svelte +7 -4
  4. package/dist/MillerIndexInput.svelte +2 -2
  5. package/dist/app.css +53 -0
  6. package/dist/brillouin/BrillouinZone.svelte +132 -219
  7. package/dist/brillouin/BrillouinZone.svelte.d.ts +9 -20
  8. package/dist/brillouin/BrillouinZoneControls.svelte +3 -6
  9. package/dist/brillouin/BrillouinZoneExportPane.svelte +47 -96
  10. package/dist/brillouin/BrillouinZoneExportPane.svelte.d.ts +1 -1
  11. package/dist/brillouin/BrillouinZoneInfoPane.svelte +15 -42
  12. package/dist/brillouin/BrillouinZoneInfoPane.svelte.d.ts +2 -3
  13. package/dist/brillouin/BrillouinZoneScene.svelte +63 -230
  14. package/dist/brillouin/BrillouinZoneScene.svelte.d.ts +3 -23
  15. package/dist/brillouin/ReciprocalVectors.svelte +39 -0
  16. package/dist/brillouin/ReciprocalVectors.svelte.d.ts +9 -0
  17. package/dist/brillouin/compute.d.ts +2 -0
  18. package/dist/brillouin/compute.js +80 -77
  19. package/dist/brillouin/geometry.d.ts +8 -0
  20. package/dist/brillouin/geometry.js +57 -0
  21. package/dist/brillouin/index.d.ts +2 -0
  22. package/dist/brillouin/index.js +2 -0
  23. package/dist/brillouin/types.d.ts +2 -2
  24. package/dist/chempot-diagram/ChemPotDiagram.svelte +33 -38
  25. package/dist/chempot-diagram/ChemPotDiagram.svelte.d.ts +1 -1
  26. package/dist/chempot-diagram/ChemPotDiagram2D.svelte +143 -246
  27. package/dist/chempot-diagram/ChemPotDiagram2D.svelte.d.ts +4 -1
  28. package/dist/chempot-diagram/ChemPotDiagram3D.svelte +652 -976
  29. package/dist/chempot-diagram/ChemPotDiagram3D.svelte.d.ts +7 -1
  30. package/dist/chempot-diagram/async-compute.svelte.js +1 -1
  31. package/dist/chempot-diagram/color.d.ts +3 -6
  32. package/dist/chempot-diagram/color.js +5 -5
  33. package/dist/chempot-diagram/compute.d.ts +3 -3
  34. package/dist/chempot-diagram/compute.js +3 -1
  35. package/dist/chempot-diagram/controls-state.svelte.d.ts +10 -0
  36. package/dist/chempot-diagram/controls-state.svelte.js +42 -0
  37. package/dist/chempot-diagram/export.d.ts +47 -0
  38. package/dist/chempot-diagram/export.js +133 -0
  39. package/dist/chempot-diagram/index.d.ts +1 -0
  40. package/dist/chempot-diagram/index.js +1 -0
  41. package/dist/chempot-diagram/pointer.d.ts +0 -10
  42. package/dist/chempot-diagram/pointer.js +4 -4
  43. package/dist/chempot-diagram/types.d.ts +3 -3
  44. package/dist/colors/index.d.ts +0 -4
  45. package/dist/colors/index.js +21 -22
  46. package/dist/composition/BarChart.svelte +37 -31
  47. package/dist/composition/BubbleChart.svelte +17 -17
  48. package/dist/composition/Composition.svelte +7 -11
  49. package/dist/composition/Formula.svelte +18 -25
  50. package/dist/composition/FormulaFilter.svelte +96 -98
  51. package/dist/composition/PieChart.svelte +60 -46
  52. package/dist/composition/chem-sys.js +3 -2
  53. package/dist/composition/format.js +4 -3
  54. package/dist/composition/index.d.ts +2 -0
  55. package/dist/composition/index.js +14 -0
  56. package/dist/composition/parse.d.ts +2 -3
  57. package/dist/composition/parse.js +27 -28
  58. package/dist/constants.js +11 -9
  59. package/dist/controls.d.ts +1 -0
  60. package/dist/controls.js +0 -1
  61. package/dist/convex-hull/ConvexHull.svelte +20 -25
  62. package/dist/convex-hull/ConvexHull.svelte.d.ts +1 -4
  63. package/dist/convex-hull/ConvexHull2D.svelte +176 -256
  64. package/dist/convex-hull/ConvexHull2D.svelte.d.ts +1 -1
  65. package/dist/convex-hull/ConvexHull3D.svelte +269 -804
  66. package/dist/convex-hull/ConvexHull3D.svelte.d.ts +1 -1
  67. package/dist/convex-hull/ConvexHull4D.svelte +236 -755
  68. package/dist/convex-hull/ConvexHull4D.svelte.d.ts +1 -1
  69. package/dist/convex-hull/ConvexHullChrome.svelte +264 -0
  70. package/dist/convex-hull/ConvexHullChrome.svelte.d.ts +30 -0
  71. package/dist/convex-hull/ConvexHullControls.svelte +128 -49
  72. package/dist/convex-hull/ConvexHullControls.svelte.d.ts +7 -6
  73. package/dist/convex-hull/ConvexHullInfoPane.svelte +18 -5
  74. package/dist/convex-hull/ConvexHullInfoPane.svelte.d.ts +6 -5
  75. package/dist/convex-hull/ConvexHullStats.svelte +210 -351
  76. package/dist/convex-hull/ConvexHullStats.svelte.d.ts +3 -1
  77. package/dist/convex-hull/ConvexHullTooltip.svelte +16 -14
  78. package/dist/convex-hull/ConvexHullTooltip.svelte.d.ts +2 -1
  79. package/dist/convex-hull/GasPressureControls.svelte +5 -12
  80. package/dist/convex-hull/StructurePopup.svelte +17 -21
  81. package/dist/convex-hull/StructurePopup.svelte.d.ts +2 -0
  82. package/dist/convex-hull/TemperatureSlider.svelte +4 -10
  83. package/dist/convex-hull/barycentric-coords.d.ts +2 -4
  84. package/dist/convex-hull/barycentric-coords.js +6 -33
  85. package/dist/convex-hull/canvas-interactions.svelte.d.ts +79 -0
  86. package/dist/convex-hull/canvas-interactions.svelte.js +279 -0
  87. package/dist/convex-hull/helpers.d.ts +47 -17
  88. package/dist/convex-hull/helpers.js +186 -101
  89. package/dist/convex-hull/hull-state.svelte.d.ts +44 -0
  90. package/dist/convex-hull/hull-state.svelte.js +124 -0
  91. package/dist/convex-hull/index.d.ts +9 -7
  92. package/dist/convex-hull/index.js +7 -2
  93. package/dist/convex-hull/thermodynamics.js +109 -937
  94. package/dist/convex-hull/types.d.ts +12 -4
  95. package/dist/convex-hull/types.js +12 -0
  96. package/dist/coordination/CoordinationBarPlot.svelte +41 -43
  97. package/dist/coordination/CoordinationBarPlot.svelte.d.ts +1 -1
  98. package/dist/coordination/calc-coordination.d.ts +1 -1
  99. package/dist/coordination/calc-coordination.js +28 -25
  100. package/dist/element/BohrAtom.svelte +6 -8
  101. package/dist/element/ElementHeading.svelte +4 -1
  102. package/dist/element/ElementPhoto.svelte +11 -9
  103. package/dist/element/ElementStats.svelte +12 -10
  104. package/dist/element/ElementTile.svelte +24 -29
  105. package/dist/element/ElementTile.svelte.d.ts +1 -1
  106. package/dist/element/Nucleus.svelte +4 -2
  107. package/dist/element/data.d.ts +1 -2
  108. package/dist/element/helpers.d.ts +4 -0
  109. package/dist/element/helpers.js +18 -0
  110. package/dist/element/index.d.ts +1 -0
  111. package/dist/element/index.js +3 -0
  112. package/dist/feedback/DragOverlay.svelte +5 -3
  113. package/dist/feedback/DragOverlay.svelte.d.ts +2 -1
  114. package/dist/feedback/StatusMessage.svelte +20 -7
  115. package/dist/fermi-surface/FermiSlice.svelte +21 -18
  116. package/dist/fermi-surface/FermiSurface.svelte +78 -165
  117. package/dist/fermi-surface/FermiSurface.svelte.d.ts +9 -20
  118. package/dist/fermi-surface/FermiSurfaceControls.svelte +6 -20
  119. package/dist/fermi-surface/FermiSurfaceScene.svelte +93 -272
  120. package/dist/fermi-surface/FermiSurfaceScene.svelte.d.ts +3 -23
  121. package/dist/fermi-surface/FermiSurfaceTooltip.svelte +9 -6
  122. package/dist/fermi-surface/compute.js +14 -13
  123. package/dist/fermi-surface/export.js +4 -15
  124. package/dist/fermi-surface/index.d.ts +0 -1
  125. package/dist/fermi-surface/index.js +0 -1
  126. package/dist/fermi-surface/parse.d.ts +1 -1
  127. package/dist/fermi-surface/parse.js +84 -97
  128. package/dist/fermi-surface/types.d.ts +2 -2
  129. package/dist/heatmap-matrix/HeatmapMatrix.svelte +161 -185
  130. package/dist/heatmap-matrix/HeatmapMatrix.svelte.d.ts +6 -5
  131. package/dist/heatmap-matrix/HeatmapMatrixControls.svelte +12 -9
  132. package/dist/heatmap-matrix/HeatmapMatrixControls.svelte.d.ts +5 -5
  133. package/dist/heatmap-matrix/index.d.ts +8 -2
  134. package/dist/icons.d.ts +4 -0
  135. package/dist/icons.js +4 -0
  136. package/dist/index.d.ts +1 -0
  137. package/dist/index.js +1 -0
  138. package/dist/io/ExportPane.svelte +170 -0
  139. package/dist/io/ExportPane.svelte.d.ts +17 -0
  140. package/dist/io/decompress.d.ts +2 -1
  141. package/dist/io/decompress.js +22 -36
  142. package/dist/io/export.d.ts +9 -1
  143. package/dist/io/export.js +37 -29
  144. package/dist/io/fetch.d.ts +2 -4
  145. package/dist/io/fetch.js +0 -11
  146. package/dist/io/file-drop.d.ts +7 -0
  147. package/dist/io/file-drop.js +13 -0
  148. package/dist/io/index.d.ts +1 -0
  149. package/dist/io/index.js +3 -0
  150. package/dist/io/is-binary.d.ts +9 -0
  151. package/dist/io/is-binary.js +45 -1
  152. package/dist/io/types.d.ts +13 -0
  153. package/dist/io/url-drop.js +13 -25
  154. package/dist/isosurface/Isosurface.svelte +17 -23
  155. package/dist/isosurface/IsosurfaceControls.svelte +37 -42
  156. package/dist/isosurface/parse-vaspwave.d.ts +3 -0
  157. package/dist/isosurface/parse-vaspwave.js +138 -0
  158. package/dist/isosurface/parse.js +50 -48
  159. package/dist/labels.d.ts +6 -2
  160. package/dist/labels.js +44 -14
  161. package/dist/layout/FullscreenButton.svelte +34 -0
  162. package/dist/layout/FullscreenButton.svelte.d.ts +10 -0
  163. package/dist/layout/FullscreenToggle.svelte +12 -12
  164. package/dist/layout/InfoCard.svelte +7 -18
  165. package/dist/layout/InfoTag.svelte +1 -3
  166. package/dist/layout/NumberRangeInput.svelte +46 -0
  167. package/dist/layout/NumberRangeInput.svelte.d.ts +12 -0
  168. package/dist/layout/PropertyFilter.svelte +2 -6
  169. package/dist/layout/SettingsSection.svelte +21 -17
  170. package/dist/layout/SubpageGrid.svelte +3 -1
  171. package/dist/layout/ViewerChrome.svelte +115 -0
  172. package/dist/layout/ViewerChrome.svelte.d.ts +18 -0
  173. package/dist/layout/fullscreen.d.ts +4 -0
  174. package/dist/layout/fullscreen.js +3 -5
  175. package/dist/layout/fullscreen.svelte.d.ts +8 -0
  176. package/dist/layout/fullscreen.svelte.js +36 -0
  177. package/dist/layout/index.d.ts +4 -0
  178. package/dist/layout/index.js +4 -0
  179. package/dist/layout/json-tree/JsonNode.svelte +8 -11
  180. package/dist/layout/json-tree/JsonTree.svelte +53 -77
  181. package/dist/layout/json-tree/JsonValue.svelte +7 -15
  182. package/dist/layout/json-tree/utils.js +14 -23
  183. package/dist/math.d.ts +9 -3
  184. package/dist/math.js +37 -21
  185. package/dist/overlays/ContextMenu.svelte +4 -2
  186. package/dist/overlays/CopyButton.svelte +11 -5
  187. package/dist/overlays/DragControlTab.svelte +6 -1
  188. package/dist/overlays/DraggablePane.svelte +81 -26
  189. package/dist/overlays/DraggablePane.svelte.d.ts +1 -0
  190. package/dist/overlays/GlassChip.svelte +30 -0
  191. package/dist/overlays/GlassChip.svelte.d.ts +8 -0
  192. package/dist/overlays/InfoPaneCards.svelte +11 -23
  193. package/dist/overlays/index.d.ts +11 -0
  194. package/dist/overlays/index.js +26 -0
  195. package/dist/overlays/portal.d.ts +2 -0
  196. package/dist/overlays/portal.js +14 -0
  197. package/dist/periodic-table/PeriodicTable.svelte +115 -146
  198. package/dist/periodic-table/PeriodicTable.svelte.d.ts +5 -3
  199. package/dist/periodic-table/PeriodicTableControls.svelte +11 -63
  200. package/dist/periodic-table/TableInset.svelte +9 -4
  201. package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte +50 -71
  202. package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte.d.ts +6 -8
  203. package/dist/phase-diagram/PhaseDiagramControls.svelte +73 -96
  204. package/dist/phase-diagram/PhaseDiagramControls.svelte.d.ts +4 -3
  205. package/dist/phase-diagram/PhaseDiagramEditorPane.svelte +4 -2
  206. package/dist/phase-diagram/PhaseDiagramEditorPane.svelte.d.ts +2 -3
  207. package/dist/phase-diagram/PhaseDiagramExportPane.svelte +49 -134
  208. package/dist/phase-diagram/PhaseDiagramExportPane.svelte.d.ts +3 -4
  209. package/dist/phase-diagram/PhaseDiagramTooltip.svelte +54 -58
  210. package/dist/phase-diagram/TdbInfoPanel.svelte +21 -15
  211. package/dist/phase-diagram/build-diagram.js +3 -3
  212. package/dist/phase-diagram/colors.js +1 -1
  213. package/dist/phase-diagram/diagram-input.d.ts +1 -2
  214. package/dist/phase-diagram/parse.d.ts +2 -1
  215. package/dist/phase-diagram/parse.js +15 -15
  216. package/dist/phase-diagram/svg-to-diagram.js +21 -15
  217. package/dist/phase-diagram/utils.js +2 -3
  218. package/dist/plot/bar/BarPlot.svelte +494 -692
  219. package/dist/plot/bar/BarPlot.svelte.d.ts +9 -15
  220. package/dist/plot/bar/BarPlotControls.svelte +1 -3
  221. package/dist/plot/bar/BarPlotControls.svelte.d.ts +5 -7
  222. package/dist/plot/bar/SpacegroupBarPlot.svelte +31 -37
  223. package/dist/plot/bar/data.d.ts +5 -4
  224. package/dist/plot/bar/data.js +2 -1
  225. package/dist/plot/box/BoxPlot.svelte +361 -439
  226. package/dist/plot/box/BoxPlot.svelte.d.ts +6 -3
  227. package/dist/plot/box/BoxPlotControls.svelte.d.ts +1 -1
  228. package/dist/plot/box/Violin.svelte.d.ts +2 -1
  229. package/dist/plot/box/box-plot.d.ts +3 -2
  230. package/dist/plot/box/box-plot.js +6 -3
  231. package/dist/plot/box/kde.d.ts +2 -1
  232. package/dist/plot/box/kde.js +4 -4
  233. package/dist/plot/core/auto-place.d.ts +1 -1
  234. package/dist/plot/core/auto-place.js +5 -2
  235. package/dist/plot/core/axis-utils.d.ts +12 -7
  236. package/dist/plot/core/axis-utils.js +48 -48
  237. package/dist/plot/core/components/AxisLabel.svelte +82 -20
  238. package/dist/plot/core/components/AxisLabel.svelte.d.ts +1 -0
  239. package/dist/plot/core/components/ColorBar.svelte +106 -111
  240. package/dist/plot/core/components/ColorBar.svelte.d.ts +5 -4
  241. package/dist/plot/core/components/ColorScaleSelect.svelte +1 -1
  242. package/dist/plot/core/components/FillArea.svelte +10 -17
  243. package/dist/plot/core/components/HierarchyControls.svelte +249 -0
  244. package/dist/plot/{sunburst/SunburstControls.svelte.d.ts → core/components/HierarchyControls.svelte.d.ts} +12 -8
  245. package/dist/plot/core/components/InteractiveAxisLabel.svelte +1 -1
  246. package/dist/plot/core/components/InteractiveAxisLabel.svelte.d.ts +1 -1
  247. package/dist/plot/core/components/Line.svelte +21 -11
  248. package/dist/plot/core/components/Line.svelte.d.ts +5 -2
  249. package/dist/plot/core/components/PlotAxis.svelte +42 -18
  250. package/dist/plot/core/components/PlotAxis.svelte.d.ts +2 -1
  251. package/dist/plot/core/components/PlotControls.svelte +65 -41
  252. package/dist/plot/core/components/PlotControls.svelte.d.ts +1 -1
  253. package/dist/plot/core/components/PlotLegend.svelte +31 -35
  254. package/dist/plot/core/components/PlotMarginals.svelte +616 -0
  255. package/dist/plot/core/components/PlotMarginals.svelte.d.ts +15 -0
  256. package/dist/plot/core/components/PlotTooltip.svelte +1 -1
  257. package/dist/plot/core/components/PortalSelect.svelte +4 -8
  258. package/dist/plot/core/components/ReferenceLine.svelte +27 -24
  259. package/dist/plot/core/components/ReferenceLine3D.svelte +56 -59
  260. package/dist/plot/core/components/ReferenceLine3D.svelte.d.ts +4 -4
  261. package/dist/plot/core/components/ReferencePlane.svelte +9 -10
  262. package/dist/plot/core/components/ReferencePlane.svelte.d.ts +4 -4
  263. package/dist/plot/core/components/ZeroLines.svelte +4 -8
  264. package/dist/plot/core/components/ZoomRect.svelte +1 -2
  265. package/dist/plot/core/components/index.d.ts +1 -0
  266. package/dist/plot/core/components/index.js +1 -0
  267. package/dist/plot/core/data-cleaning-signal.d.ts +46 -0
  268. package/dist/plot/core/data-cleaning-signal.js +467 -0
  269. package/dist/plot/core/data-cleaning.d.ts +2 -42
  270. package/dist/plot/core/data-cleaning.js +29 -476
  271. package/dist/plot/core/data-transform.js +9 -5
  272. package/dist/plot/core/fill-utils.d.ts +7 -4
  273. package/dist/plot/core/fill-utils.js +34 -9
  274. package/dist/plot/core/index.d.ts +1 -0
  275. package/dist/plot/core/index.js +1 -0
  276. package/dist/plot/core/interactions.d.ts +5 -1
  277. package/dist/plot/core/interactions.js +14 -2
  278. package/dist/plot/core/layout.d.ts +3 -1
  279. package/dist/plot/core/layout.js +22 -13
  280. package/dist/plot/core/marginals.d.ts +170 -0
  281. package/dist/plot/core/marginals.js +436 -0
  282. package/dist/plot/core/pan-zoom.svelte.d.ts +35 -0
  283. package/dist/plot/core/pan-zoom.svelte.js +221 -0
  284. package/dist/plot/core/placed-tween.svelte.d.ts +21 -0
  285. package/dist/plot/core/placed-tween.svelte.js +68 -0
  286. package/dist/plot/core/reference-line.d.ts +10 -10
  287. package/dist/plot/core/reference-line.js +7 -7
  288. package/dist/plot/core/scales.d.ts +40 -25
  289. package/dist/plot/core/scales.js +49 -21
  290. package/dist/plot/core/svg.d.ts +2 -1
  291. package/dist/plot/core/types/fills.d.ts +101 -0
  292. package/dist/plot/core/types/fills.js +15 -0
  293. package/dist/plot/core/types/plot-3d.d.ts +87 -0
  294. package/dist/plot/core/types/plot-3d.js +4 -0
  295. package/dist/plot/core/types/reference-lines.d.ts +140 -0
  296. package/dist/plot/core/types/reference-lines.js +9 -0
  297. package/dist/plot/core/types.d.ts +47 -331
  298. package/dist/plot/core/types.js +23 -20
  299. package/dist/plot/core/utils/hierarchy-chart.d.ts +51 -0
  300. package/dist/plot/core/utils/hierarchy-chart.js +179 -0
  301. package/dist/plot/core/utils/hierarchy-labels.d.ts +20 -0
  302. package/dist/plot/core/utils/hierarchy-labels.js +99 -0
  303. package/dist/plot/core/utils/label-placement.d.ts +1 -1
  304. package/dist/plot/core/utils/label-placement.js +5 -5
  305. package/dist/plot/core/utils.d.ts +2 -1
  306. package/dist/plot/histogram/Histogram.svelte +373 -631
  307. package/dist/plot/histogram/Histogram.svelte.d.ts +7 -7
  308. package/dist/plot/histogram/HistogramControls.svelte +30 -28
  309. package/dist/plot/histogram/HistogramControls.svelte.d.ts +1 -1
  310. package/dist/plot/histogram/histogram.d.ts +31 -0
  311. package/dist/plot/histogram/histogram.js +70 -0
  312. package/dist/plot/histogram/index.d.ts +1 -0
  313. package/dist/plot/histogram/index.js +1 -0
  314. package/dist/plot/index.d.ts +1 -0
  315. package/dist/plot/index.js +1 -0
  316. package/dist/plot/sankey/Sankey.svelte +79 -73
  317. package/dist/plot/sankey/Sankey.svelte.d.ts +3 -3
  318. package/dist/plot/sankey/SankeyControls.svelte +22 -16
  319. package/dist/plot/sankey/sankey.js +3 -1
  320. package/dist/plot/scatter/BinnedScatterPlot.svelte +264 -112
  321. package/dist/plot/scatter/BinnedScatterPlot.svelte.d.ts +11 -4
  322. package/dist/plot/scatter/ElementScatter.svelte +4 -1
  323. package/dist/plot/scatter/ScatterPlot.svelte +623 -821
  324. package/dist/plot/scatter/ScatterPlot.svelte.d.ts +9 -15
  325. package/dist/plot/scatter/ScatterPlotControls.svelte +18 -19
  326. package/dist/plot/scatter/ScatterPlotControls.svelte.d.ts +1 -1
  327. package/dist/plot/scatter/ScatterPoint.svelte +16 -9
  328. package/dist/plot/scatter/binned-scatter-types.d.ts +6 -12
  329. package/dist/plot/scatter/binned-scatter-types.js +6 -1
  330. package/dist/plot/scatter/index.d.ts +1 -1
  331. package/dist/plot/scatter/scatter-data.d.ts +1 -1
  332. package/dist/plot/scatter/scatter-data.js +8 -7
  333. package/dist/plot/scatter-3d/ScatterPlot3D.svelte +40 -60
  334. package/dist/plot/scatter-3d/ScatterPlot3D.svelte.d.ts +6 -14
  335. package/dist/plot/scatter-3d/ScatterPlot3DControls.svelte +17 -15
  336. package/dist/plot/scatter-3d/ScatterPlot3DControls.svelte.d.ts +5 -5
  337. package/dist/plot/scatter-3d/ScatterPlot3DScene.svelte +232 -234
  338. package/dist/plot/scatter-3d/ScatterPlot3DScene.svelte.d.ts +5 -14
  339. package/dist/plot/scatter-3d/Surface3D.svelte +9 -17
  340. package/dist/plot/scatter-3d/Surface3D.svelte.d.ts +4 -3
  341. package/dist/plot/sunburst/Sunburst.svelte +256 -338
  342. package/dist/plot/sunburst/Sunburst.svelte.d.ts +4 -3
  343. package/dist/plot/sunburst/index.d.ts +1 -1
  344. package/dist/plot/sunburst/index.js +2 -1
  345. package/dist/plot/sunburst/render.d.ts +2 -2
  346. package/dist/plot/sunburst/render.js +47 -22
  347. package/dist/plot/sunburst/sunburst.d.ts +5 -1
  348. package/dist/plot/sunburst/sunburst.js +14 -4
  349. package/dist/plot/treemap/Treemap.svelte +907 -0
  350. package/dist/plot/treemap/Treemap.svelte.d.ts +84 -0
  351. package/dist/plot/treemap/index.d.ts +3 -0
  352. package/dist/plot/treemap/index.js +4 -0
  353. package/dist/plot/treemap/treemap.d.ts +26 -0
  354. package/dist/plot/treemap/treemap.js +99 -0
  355. package/dist/rdf/RdfPlot.svelte +22 -26
  356. package/dist/rdf/RdfPlot.svelte.d.ts +1 -1
  357. package/dist/rdf/calc-rdf.js +4 -8
  358. package/dist/sanitize.js +14 -3
  359. package/dist/scene/SceneCamera.svelte +62 -0
  360. package/dist/scene/SceneCamera.svelte.d.ts +19 -0
  361. package/dist/scene/bind-renderer.svelte.d.ts +2 -0
  362. package/dist/scene/bind-renderer.svelte.js +22 -0
  363. package/dist/scene/index.d.ts +4 -0
  364. package/dist/scene/index.js +5 -0
  365. package/dist/scene/props.js +52 -0
  366. package/dist/scene/types.d.ts +26 -0
  367. package/dist/scene/types.js +1 -0
  368. package/dist/settings.d.ts +33 -11
  369. package/dist/settings.js +136 -51
  370. package/dist/spectral/Bands.svelte +231 -237
  371. package/dist/spectral/Bands.svelte.d.ts +3 -2
  372. package/dist/spectral/BandsAndDos.svelte +29 -29
  373. package/dist/spectral/BrillouinBandsDos.svelte +21 -29
  374. package/dist/spectral/Dos.svelte +75 -77
  375. package/dist/spectral/Dos.svelte.d.ts +2 -1
  376. package/dist/spectral/helpers.d.ts +9 -9
  377. package/dist/spectral/helpers.js +66 -70
  378. package/dist/state.svelte.d.ts +0 -7
  379. package/dist/state.svelte.js +5 -24
  380. package/dist/structure/Arrow.svelte +6 -21
  381. package/dist/structure/ArrowInstances.svelte +160 -0
  382. package/dist/structure/ArrowInstances.svelte.d.ts +16 -0
  383. package/dist/structure/AtomLegend.svelte +115 -75
  384. package/dist/structure/AtomLegend.svelte.d.ts +1 -1
  385. package/dist/structure/Bond.svelte +31 -23
  386. package/dist/structure/CanvasTooltip.svelte +16 -3
  387. package/dist/structure/CellSelect.svelte +22 -29
  388. package/dist/structure/CellSelect.svelte.d.ts +2 -1
  389. package/dist/structure/Cylinder.svelte +24 -40
  390. package/dist/structure/InstancedAtoms.svelte +114 -0
  391. package/dist/structure/InstancedAtoms.svelte.d.ts +15 -0
  392. package/dist/structure/Lattice.svelte +85 -149
  393. package/dist/structure/SiteLabels.svelte +122 -0
  394. package/dist/structure/SiteLabels.svelte.d.ts +18 -0
  395. package/dist/structure/Structure.svelte +725 -735
  396. package/dist/structure/Structure.svelte.d.ts +8 -16
  397. package/dist/structure/StructureCarousel.svelte +630 -0
  398. package/dist/structure/StructureCarousel.svelte.d.ts +19 -0
  399. package/dist/structure/StructureControls.svelte +298 -325
  400. package/dist/structure/StructureControls.svelte.d.ts +5 -3
  401. package/dist/structure/StructureExportPane.svelte +62 -168
  402. package/dist/structure/StructureExportPane.svelte.d.ts +4 -5
  403. package/dist/structure/StructureInfoPane.svelte +77 -69
  404. package/dist/structure/StructureInfoPane.svelte.d.ts +6 -6
  405. package/dist/structure/StructureScene.svelte +758 -600
  406. package/dist/structure/StructureScene.svelte.d.ts +24 -20
  407. package/dist/structure/StructureViewport.svelte +386 -0
  408. package/dist/structure/StructureViewport.svelte.d.ts +63 -0
  409. package/dist/structure/atom-label-placement.d.ts +18 -0
  410. package/dist/structure/atom-label-placement.js +115 -0
  411. package/dist/structure/atom-properties.d.ts +3 -1
  412. package/dist/structure/atom-properties.js +115 -54
  413. package/dist/structure/bond-order-perception.js +2 -4
  414. package/dist/structure/bonding.d.ts +12 -2
  415. package/dist/structure/bonding.js +172 -73
  416. package/dist/structure/edit-history.d.ts +6 -0
  417. package/dist/structure/edit-history.js +27 -0
  418. package/dist/structure/export.d.ts +25 -4
  419. package/dist/structure/export.js +103 -153
  420. package/dist/structure/format-detect.d.ts +1 -0
  421. package/dist/structure/format-detect.js +22 -0
  422. package/dist/structure/geometry.d.ts +9 -0
  423. package/dist/structure/geometry.js +27 -0
  424. package/dist/structure/index.d.ts +18 -1
  425. package/dist/structure/index.js +15 -2
  426. package/dist/structure/measure.d.ts +1 -1
  427. package/dist/structure/measure.js +6 -12
  428. package/dist/structure/parse.d.ts +5 -3
  429. package/dist/structure/parse.js +551 -567
  430. package/dist/structure/partial-occupancy.d.ts +0 -1
  431. package/dist/structure/partial-occupancy.js +5 -3
  432. package/dist/structure/pbc.d.ts +1 -1
  433. package/dist/structure/pbc.js +204 -51
  434. package/dist/structure/polyhedra.d.ts +40 -0
  435. package/dist/structure/polyhedra.js +587 -0
  436. package/dist/structure/site.d.ts +4 -0
  437. package/dist/structure/site.js +1 -0
  438. package/dist/structure/supercell.d.ts +1 -2
  439. package/dist/structure/supercell.js +10 -15
  440. package/dist/structure/validation.js +5 -6
  441. package/dist/symmetry/SymmetryElementControls.svelte +69 -0
  442. package/dist/symmetry/SymmetryElementControls.svelte.d.ts +9 -0
  443. package/dist/symmetry/SymmetryElements.svelte +350 -0
  444. package/dist/symmetry/SymmetryElements.svelte.d.ts +24 -0
  445. package/dist/symmetry/SymmetryStats.svelte +142 -49
  446. package/dist/symmetry/WyckoffTable.svelte +58 -16
  447. package/dist/symmetry/WyckoffTable.svelte.d.ts +3 -0
  448. package/dist/symmetry/cell-transform.js +8 -14
  449. package/dist/symmetry/index.d.ts +15 -4
  450. package/dist/symmetry/index.js +308 -81
  451. package/dist/symmetry/spacegroups.d.ts +5 -1
  452. package/dist/symmetry/spacegroups.js +18 -3
  453. package/dist/symmetry/symmetry-elements.d.ts +33 -0
  454. package/dist/symmetry/symmetry-elements.js +521 -0
  455. package/dist/symmetry/wyckoff-db.d.ts +9 -0
  456. package/dist/symmetry/wyckoff-db.js +87 -0
  457. package/dist/table/HeatmapTable.svelte +1104 -378
  458. package/dist/table/HeatmapTable.svelte.d.ts +12 -2
  459. package/dist/table/ToggleMenu.svelte +5 -12
  460. package/dist/table/index.d.ts +21 -6
  461. package/dist/table/index.js +34 -21
  462. package/dist/theme/ThemeControl.svelte +16 -8
  463. package/dist/theme/index.d.ts +1 -5
  464. package/dist/theme/index.js +4 -10
  465. package/dist/theme/themes.mjs +14 -7
  466. package/dist/tooltip/KCoords.svelte +4 -1
  467. package/dist/tooltip/TooltipContent.svelte +2 -5
  468. package/dist/trajectory/Trajectory.svelte +556 -352
  469. package/dist/trajectory/Trajectory.svelte.d.ts +10 -22
  470. package/dist/trajectory/TrajectoryError.svelte +11 -8
  471. package/dist/trajectory/TrajectoryExportPane.svelte +28 -54
  472. package/dist/trajectory/TrajectoryExportPane.svelte.d.ts +4 -5
  473. package/dist/trajectory/TrajectoryInfoPane.svelte +88 -99
  474. package/dist/trajectory/TrajectoryInfoPane.svelte.d.ts +3 -2
  475. package/dist/trajectory/constants.js +6 -2
  476. package/dist/trajectory/extract.js +73 -71
  477. package/dist/trajectory/format-detect.d.ts +0 -1
  478. package/dist/trajectory/format-detect.js +13 -17
  479. package/dist/trajectory/frame-reader.d.ts +2 -1
  480. package/dist/trajectory/frame-reader.js +74 -136
  481. package/dist/trajectory/helpers.d.ts +12 -2
  482. package/dist/trajectory/helpers.js +68 -37
  483. package/dist/trajectory/index.d.ts +1 -0
  484. package/dist/trajectory/index.js +3 -0
  485. package/dist/trajectory/parse/ase.d.ts +9 -1
  486. package/dist/trajectory/parse/ase.js +47 -32
  487. package/dist/trajectory/parse/diagnostics.d.ts +3 -0
  488. package/dist/trajectory/parse/diagnostics.js +14 -0
  489. package/dist/trajectory/parse/h5-utils.d.ts +13 -0
  490. package/dist/trajectory/parse/h5-utils.js +109 -0
  491. package/dist/trajectory/parse/hdf5.d.ts +1 -1
  492. package/dist/trajectory/parse/hdf5.js +116 -106
  493. package/dist/trajectory/parse/index.d.ts +3 -4
  494. package/dist/trajectory/parse/index.js +68 -119
  495. package/dist/trajectory/parse/lammps.d.ts +0 -2
  496. package/dist/trajectory/parse/lammps.js +14 -16
  497. package/dist/trajectory/parse/pymatgen.d.ts +2 -0
  498. package/dist/trajectory/parse/pymatgen.js +74 -0
  499. package/dist/trajectory/parse/vasp.js +13 -9
  500. package/dist/trajectory/parse/vaspout-electronic.d.ts +13 -0
  501. package/dist/trajectory/parse/vaspout-electronic.js +207 -0
  502. package/dist/trajectory/parse/vaspout-h5.d.ts +5 -0
  503. package/dist/trajectory/parse/vaspout-h5.js +284 -0
  504. package/dist/trajectory/parse/xyz.d.ts +9 -21
  505. package/dist/trajectory/parse/xyz.js +38 -39
  506. package/dist/trajectory/plotting.d.ts +8 -10
  507. package/dist/trajectory/plotting.js +102 -127
  508. package/dist/utils.d.ts +4 -0
  509. package/dist/utils.js +8 -1
  510. package/dist/xrd/XrdPlot.svelte +56 -85
  511. package/dist/xrd/broadening.d.ts +2 -1
  512. package/dist/xrd/broadening.js +2 -1
  513. package/dist/xrd/calc-xrd.js +8 -15
  514. package/dist/xrd/index.d.ts +2 -2
  515. package/dist/xrd/parse.d.ts +0 -1
  516. package/dist/xrd/parse.js +56 -73
  517. package/package.json +77 -36
  518. package/dist/api/mp.d.ts +0 -6
  519. package/dist/api/mp.js +0 -22
  520. package/dist/element/data.json +0 -11864
  521. package/dist/fermi-surface/marching-cubes.d.ts +0 -2
  522. package/dist/fermi-surface/marching-cubes.js +0 -2
  523. package/dist/plot/core/hover-lock.svelte.d.ts +0 -14
  524. package/dist/plot/core/hover-lock.svelte.js +0 -45
  525. package/dist/plot/sunburst/SunburstControls.svelte +0 -200
  526. package/dist/structure/label-placement.d.ts +0 -14
  527. package/dist/structure/label-placement.js +0 -72
@@ -9,14 +9,8 @@
9
9
  } from '../colors'
10
10
  import { normalize_show_controls } from '../controls'
11
11
  import { sanitize_html } from '../sanitize'
12
- import { ClickFeedback, DragOverlay, Spinner } from '../feedback'
13
- import Icon from '../Icon.svelte'
14
- import {
15
- set_fullscreen_bg,
16
- setup_fullscreen_effect,
17
- toggle_fullscreen,
18
- } from '../layout'
19
- import { ColorBar, PlotTooltip } from '../plot'
12
+ import { Spinner } from '../feedback'
13
+ import { ColorBar } from '../plot'
20
14
  import { create_pulse_animation } from '../effects.svelte'
21
15
  import { DEFAULTS } from '../settings'
22
16
  import type { AnyStructure } from '../structure'
@@ -25,38 +19,36 @@
25
19
  compute_4d_coords,
26
20
  TETRAHEDRON_VERTICES,
27
21
  } from './barycentric-coords'
28
- import ConvexHullControls from './ConvexHullControls.svelte'
29
- import ConvexHullInfoPane from './ConvexHullInfoPane.svelte'
30
- import ConvexHullTooltip from './ConvexHullTooltip.svelte'
22
+ import { create_canvas_interactions } from './canvas-interactions.svelte'
23
+ import ConvexHullChrome from './ConvexHullChrome.svelte'
31
24
  import GasPressureControls from './GasPressureControls.svelte'
32
25
  import * as helpers from './helpers'
26
+ import { create_hull_data_pipeline } from './hull-state.svelte'
33
27
  import type { BaseConvexHullProps, Hull3DProps } from './index'
34
28
  import { CONVEX_HULL_STYLE, default_controls, default_hull_config } from './index'
35
- import StructurePopup from './StructurePopup.svelte'
36
29
  import TemperatureSlider from './TemperatureSlider.svelte'
37
30
  import type { Point4D } from './thermodynamics'
38
31
  import * as thermo from './thermodynamics'
39
- import type {
40
- ConvexHullEntry,
41
- HighlightStyle,
42
- HoverData3D,
43
- HullFaceColorMode,
44
- } from './types'
32
+ import type { ConvexHullEntry, HighlightStyle, HullFaceColorMode, PhaseData } from './types'
33
+ import { MAGNETIC_ORDERING_CATEGORY } from './types'
45
34
  import { compute_hull_stability } from './helpers'
46
35
 
47
36
  let {
48
37
  entries = [],
49
38
  controls = {},
50
39
  config = {},
40
+ show_controls,
51
41
  on_point_click,
52
42
  on_point_hover,
53
43
  fullscreen = $bindable(DEFAULTS.convex_hull.quaternary.fullscreen),
54
- enable_fullscreen = true,
44
+ fullscreen_toggle = true,
55
45
  enable_info_pane = true,
56
46
  wrapper = $bindable(),
57
47
  label_threshold = 50,
58
48
  show_stable = $bindable(DEFAULTS.convex_hull.quaternary.show_stable),
59
49
  show_unstable = $bindable(DEFAULTS.convex_hull.quaternary.show_unstable),
50
+ entry_category = MAGNETIC_ORDERING_CATEGORY,
51
+ hidden_categories = $bindable([]),
60
52
  show_hull_faces = $bindable(DEFAULTS.convex_hull.quaternary.show_hull_faces),
61
53
  hull_face_opacity = $bindable(DEFAULTS.convex_hull.quaternary.hull_face_opacity),
62
54
  hull_face_color_mode = $bindable(
@@ -64,23 +56,17 @@
64
56
  ),
65
57
  element_colors = vesta_hex,
66
58
  color_mode = $bindable(DEFAULTS.convex_hull.quaternary.color_mode),
67
- color_scale = $bindable(
68
- DEFAULTS.convex_hull.quaternary.color_scale as D3InterpolateName,
69
- ),
59
+ color_scale = $bindable(DEFAULTS.convex_hull.quaternary.color_scale as D3InterpolateName),
70
60
  info_pane_open = $bindable(DEFAULTS.convex_hull.quaternary.info_pane_open),
71
- legend_pane_open = $bindable(DEFAULTS.convex_hull.quaternary.legend_pane_open),
61
+ controls_open = $bindable(DEFAULTS.convex_hull.quaternary.legend_pane_open),
72
62
  max_hull_dist_show_phases = $bindable(
73
63
  DEFAULTS.convex_hull.quaternary.max_hull_dist_show_phases,
74
64
  ),
75
65
  max_hull_dist_show_labels = $bindable(
76
66
  DEFAULTS.convex_hull.quaternary.max_hull_dist_show_labels,
77
67
  ),
78
- show_stable_labels = $bindable(
79
- DEFAULTS.convex_hull.quaternary.show_stable_labels,
80
- ),
81
- show_unstable_labels = $bindable(
82
- DEFAULTS.convex_hull.quaternary.show_unstable_labels,
83
- ),
68
+ show_stable_labels = $bindable(DEFAULTS.convex_hull.quaternary.show_stable_labels),
69
+ show_unstable_labels = $bindable(DEFAULTS.convex_hull.quaternary.show_unstable_labels),
84
70
  on_file_drop,
85
71
  enable_click_selection = true,
86
72
  enable_structure_preview = true,
@@ -99,12 +85,13 @@
99
85
  children,
100
86
  tooltip,
101
87
  ...rest
102
- }: BaseConvexHullProps<ConvexHullEntry> & Hull3DProps & {
103
- highlight_style?: HighlightStyle
104
- } = $props()
88
+ }: BaseConvexHullProps<ConvexHullEntry> &
89
+ Hull3DProps & {
90
+ highlight_style?: HighlightStyle
91
+ } = $props()
105
92
 
106
93
  const merged_controls = $derived({ ...default_controls, ...controls })
107
- const controls_config = $derived(normalize_show_controls(merged_controls.show))
94
+ const controls_config = $derived(normalize_show_controls(show_controls))
108
95
  const merged_config = $derived({
109
96
  ...default_hull_config,
110
97
  ...config,
@@ -114,90 +101,36 @@
114
101
 
115
102
  // Reactive dark mode detection for canvas text color
116
103
  let dark_mode = $state(is_dark_mode())
117
- $effect(() => watch_dark_mode((dark) => dark_mode = dark))
104
+ $effect(() => watch_dark_mode((dark) => (dark_mode = dark)))
118
105
  const text_color = $derived(helpers.get_canvas_text_color(dark_mode))
119
106
 
120
- // Temperature-dependent free energy support
121
- const { has_temp_data, available_temperatures } = $derived(
122
- helpers.analyze_temperature_data(entries),
123
- )
124
-
125
- // Initialize or reset temperature when it's undefined or no longer valid
126
- $effect(() => {
127
- if (
128
- has_temp_data &&
129
- available_temperatures.length > 0 &&
130
- (temperature === undefined || !available_temperatures.includes(temperature))
131
- ) temperature = available_temperatures[0]
132
- })
133
-
134
- // Filter entries by temperature when in temperature mode
135
- const temp_filtered_entries = $derived(
136
- has_temp_data && temperature !== undefined
137
- ? helpers.filter_entries_at_temperature(entries, temperature, {
138
- interpolate: interpolate_temperature,
139
- max_interpolation_gap,
140
- })
141
- : entries,
142
- )
143
-
144
- // Gas-dependent chemical potential support (corrections based on T, P)
145
- const {
146
- entries: gas_corrected_entries,
147
- analysis: gas_analysis,
148
- merged_config: merged_gas_config,
149
- } = $derived(
150
- helpers.get_gas_corrected_entries(
151
- temp_filtered_entries,
152
- gas_config,
153
- gas_pressures,
154
- temperature ?? helpers.DEFAULT_GAS_TEMP,
155
- ),
156
- )
157
-
158
- let { // Compute energy mode information
159
- has_precomputed_e_form,
160
- has_precomputed_hull,
161
- can_compute_e_form,
162
- can_compute_hull,
163
- energy_mode,
164
- unary_refs,
165
- } = $derived(
166
- helpers.compute_energy_mode_info(
167
- gas_corrected_entries,
168
- thermo.find_lowest_energy_unary_refs,
169
- energy_source_mode,
170
- ),
171
- )
172
-
173
- const effective_entries = $derived(
174
- helpers.get_effective_entries(
175
- gas_corrected_entries,
176
- energy_mode,
177
- unary_refs,
178
- thermo.compute_e_form_per_atom,
179
- ),
180
- )
181
-
182
- // Process convex hull data with unified PhaseData interface using effective entries
183
- const pd_data = $derived(thermo.process_hull_entries(effective_entries))
184
-
185
- // Pre-compute polymorph stats once for O(1) tooltip lookups
186
- const polymorph_stats_map = $derived(
187
- helpers.compute_all_polymorph_stats(effective_entries),
188
- )
189
-
190
- const elements = $derived.by(() => {
191
- if (pd_data.elements.length > 4) {
192
- console.error(
193
- `ConvexHull4D: Dataset contains ${pd_data.elements.length} elements, but quaternary diagrams require exactly 4. Found: [${
194
- pd_data.elements.join(`, `)
195
- }]`,
196
- )
197
- return []
198
- }
199
- return pd_data.elements
107
+ // Shared reactive data pipeline (temperature → gas → energy mode → hull data → threshold)
108
+ // Explicit generic breaks the circular type inference through the all_enriched_entries thunk
109
+ const hull_data = create_hull_data_pipeline<ConvexHullEntry>({
110
+ dim: 4,
111
+ entries: () => entries,
112
+ temperature: () => temperature,
113
+ interpolate_temperature: () => interpolate_temperature,
114
+ max_interpolation_gap: () => max_interpolation_gap,
115
+ gas_config: () => gas_config,
116
+ gas_pressures: () => gas_pressures,
117
+ energy_source_mode: () => energy_source_mode,
118
+ all_enriched_entries: () => all_enriched_entries,
119
+ max_hull_dist_show_phases: () => max_hull_dist_show_phases,
120
+ show_stable: () => show_stable,
121
+ show_unstable: () => show_unstable,
122
+ entry_category: () => entry_category,
123
+ hidden_categories: () => hidden_categories,
124
+ keep_plot_entry: helpers.entry_within_hull_dist,
125
+ set_temperature: (next_temp) => (temperature = next_temp),
126
+ set_max_hull_dist_show_phases: (value) => (max_hull_dist_show_phases = value),
127
+ set_stable_entries: (value) => (stable_entries = value),
128
+ set_unstable_entries: (value) => (unstable_entries = value),
200
129
  })
130
+ const merged_gas_config = $derived(hull_data.merged_gas_config)
131
+ const pd_data = $derived(hull_data.pd_data)
132
+ const elements = $derived(hull_data.elements)
133
+ const plot_entries = $derived(hull_data.plot_entries)
201
134
 
202
135
  // Compute 4D hull for visualization (always compute when we have formation energies)
203
136
  const hull_4d = $derived.by(() => {
@@ -205,8 +138,9 @@
205
138
 
206
139
  try {
207
140
  // Get coords with formation energies, excluding entries that don't participate in hull
208
- const coords = compute_4d_coords(pd_data.entries, elements)
209
- .filter((ent) => !ent.exclude_from_hull)
141
+ const coords = compute_4d_coords(pd_data.entries, elements).filter(
142
+ (ent) => !ent.exclude_from_hull,
143
+ )
210
144
 
211
145
  // Convert to 4D points for hull computation using barycentric coordinates (composition fractions)
212
146
  const points_4d: Point4D[] = coords
@@ -234,84 +168,46 @@
234
168
  })
235
169
 
236
170
  // Enrich coords with e_above_hull (before filtering)
237
- const all_enriched_entries = $derived.by(() => {
171
+ // Explicit return type breaks circular type inference with the hull_data pipeline
172
+ const all_enriched_entries = $derived.by((): ConvexHullEntry[] => {
238
173
  if (elements.length !== 4) return []
239
174
  try {
240
175
  const coords = compute_4d_coords(pd_data.entries, elements)
241
- if (energy_mode !== `on-the-fly` || hull_4d.length === 0) return coords
176
+ if (hull_data.energy_mode !== `on-the-fly` || hull_4d.length === 0) return coords
242
177
 
243
178
  // Build 4D points, tracking original indices for mapping hull distances back
244
179
  const valid = coords.flatMap((entry, idx) => {
245
180
  if (
246
181
  !Number.isFinite(entry.e_form_per_atom) ||
247
182
  ![entry.x, entry.y, entry.z].every(Number.isFinite)
248
- ) return []
183
+ )
184
+ return []
249
185
  const amounts = elements.map((el) => entry.composition[el] || 0)
250
- const total = amounts.reduce((sum, amt) => sum + amt, 0)
186
+ const total = amounts.reduce((sum, amt) => sum + amt, 0)
251
187
  if (!(total > 0)) return []
252
- const [x, y, z] = amounts.map((amt) => amt / total)
188
+ const [x, y, z] = amounts.map((amt) => amt / total)
253
189
  return [x, y, z].every(Number.isFinite)
254
190
  ? [{ idx, pt: { x, y, z, w: entry.e_form_per_atom ?? NaN } }]
255
191
  : []
256
192
  })
257
- const raw_dists = thermo.compute_e_above_hull_4d(valid.map((item) => item.pt), hull_4d)
193
+ const raw_dists = thermo.compute_e_above_hull_4d(
194
+ valid.map((item) => item.pt),
195
+ hull_4d,
196
+ )
258
197
  const hull_map = new Map(valid.map((item, hull_idx) => [item.idx, raw_dists[hull_idx]]))
259
- return coords.map((entry, idx) => {
260
- const raw = hull_map.get(idx)
261
- if (raw === undefined) return { ...entry, e_above_hull: raw, is_stable: false }
262
- return { ...entry, ...compute_hull_stability(raw, entry.exclude_from_hull) }
263
- })
198
+ // missing/non-finite distance (no energy or point outside hull projection) -> unknown
199
+ return coords.map((entry, idx) => ({
200
+ ...entry,
201
+ ...compute_hull_stability(hull_map.get(idx), entry.exclude_from_hull),
202
+ }))
264
203
  } catch (err) {
265
204
  console.error(`Error computing quaternary coordinates:`, err)
266
205
  return []
267
206
  }
268
207
  })
269
208
 
270
- // Auto threshold: show all for few entries, use default for many, interpolate between
271
- const max_hull_dist_in_data = $derived(
272
- helpers.calc_max_hull_dist_in_data(all_enriched_entries),
273
- )
274
- const auto_default_threshold = $derived(helpers.compute_auto_hull_dist_threshold(
275
- all_enriched_entries.length,
276
- max_hull_dist_in_data,
277
- DEFAULTS.convex_hull.quaternary.max_hull_dist_show_phases,
278
- ))
279
-
280
- const next_auto_threshold = helpers.auto_threshold_reset(
281
- DEFAULTS.convex_hull.quaternary.max_hull_dist_show_phases,
282
- )
283
- $effect(() => {
284
- max_hull_dist_show_phases = next_auto_threshold(
285
- entries,
286
- max_hull_dist_show_phases,
287
- auto_default_threshold,
288
- ) ?? max_hull_dist_show_phases
289
- })
290
-
291
- // Filter by threshold; visibility is a view predicate, not entry state.
292
- const plot_entries = $derived(
293
- all_enriched_entries.filter((entry) => {
294
- // Always include stable entries and elemental reference points
295
- if (helpers.entry_is_stable(entry)) return true
296
- return typeof entry.e_above_hull === `number` &&
297
- entry.e_above_hull <= max_hull_dist_show_phases
298
- }),
299
- )
300
- const visible_entries = $derived(helpers.visible_entries(
301
- plot_entries,
302
- show_stable,
303
- show_unstable,
304
- ))
305
-
306
- // Stable and unstable entries exposed as bindable props
307
- $effect(() => {
308
- stable_entries = plot_entries.filter(helpers.entry_is_stable)
309
- unstable_entries = plot_entries.filter(helpers.entry_is_unstable)
310
- })
311
-
312
209
  let canvas: HTMLCanvasElement | undefined = undefined
313
210
  let ctx: CanvasRenderingContext2D | null = null
314
- let frame_id = 0 // Performance optimization
315
211
 
316
212
  // Camera state - following Materials Project's 3D camera setup
317
213
  let camera = $state({
@@ -322,43 +218,60 @@
322
218
  center_y: 20, // Slight offset to avoid legend overlap
323
219
  })
324
220
 
325
- // Interaction state
326
- let is_dragging = $state(false)
327
- let drag_started = $state(false)
328
- let last_mouse = $state({ x: 0, y: 0 })
329
- let hover_data = $state.raw<HoverData3D<ConvexHullEntry> | null>(null)
330
- let copy_feedback = $state({ visible: false, position: { x: 0, y: 0 } })
331
-
332
- // Drag and drop state
333
- let drag_over = $state(false)
334
-
335
- // Structure popup state
336
- let modal_open = $state(false)
337
- let selected_structure = $state<AnyStructure | null>(null)
338
- let modal_place_right = $state(true)
339
- $effect(() => {
340
- const current_selection = helpers.current_entry(selected_entry, plot_entries)
341
- const stale_selection = selected_entry && !current_selection
342
- if (stale_selection) selected_entry = null
343
- else if (current_selection && !helpers.same_entry(current_selection, selected_entry)) {
344
- selected_entry = current_selection
345
- }
346
- const current_hover = helpers.current_entry(hover_data?.entry, plot_entries)
347
- if (hover_data?.entry && !current_hover) {
348
- hover_data = null
349
- on_point_hover?.(null)
350
- } else if (hover_data && current_hover && current_hover !== hover_data.entry) {
351
- hover_data = { ...hover_data, entry: current_hover }
352
- }
353
- if (modal_open) {
354
- const structure = current_selection && extract_structure_from_entry(current_selection)
355
- if (structure) selected_structure = structure
356
- else {
357
- modal_open = false
358
- selected_structure = null
221
+ // Shared canvas-interaction scaffold (mouse/keyboard handlers, hover/drag/popup
222
+ // state, canvas sizing, render scheduler). Rotation math + keydown actions stay local.
223
+ const interactions = create_canvas_interactions({
224
+ wheel_clamp: [1.0, 15],
225
+ fullscreen_bg_var: `--hull-4d-bg-fullscreen`,
226
+ canvas: () => canvas,
227
+ wrapper: () => wrapper,
228
+ ctx: () => ctx,
229
+ set_ctx: (context) => (ctx = context),
230
+ set_canvas_dims: (dims) => (canvas_dims = dims),
231
+ visible_entries: () => hull_data.visible_entries,
232
+ plot_entries: () => plot_entries,
233
+ selected_entry: () => selected_entry,
234
+ set_selected_entry: (entry) => (selected_entry = entry),
235
+ fullscreen: () => fullscreen,
236
+ enable_click_selection: () => enable_click_selection,
237
+ enable_structure_preview: () => enable_structure_preview,
238
+ on_point_click: () => on_point_click,
239
+ on_point_hover: () => on_point_hover,
240
+ on_file_drop: () => on_file_drop,
241
+ entry_category: () => entry_category,
242
+ zoom: () => camera.zoom,
243
+ set_zoom: (zoom) => (camera.zoom = zoom),
244
+ project_point: project_3d_point,
245
+ extract_structure: extract_structure_from_entry,
246
+ render_frame,
247
+ on_drag: (dx, dy, panning) => {
248
+ if (panning) {
249
+ camera.center_x += dx
250
+ camera.center_y += dy
251
+ } else {
252
+ camera.rotation_y += dx * 0.005
253
+ camera.rotation_x = Math.max(
254
+ -Math.PI / 3,
255
+ Math.min(Math.PI / 3, camera.rotation_x - dy * 0.005),
256
+ )
359
257
  }
360
- }
258
+ },
259
+ // Reset pan center when entering/exiting fullscreen
260
+ on_fullscreen_change: () => {
261
+ camera.center_x = 0
262
+ camera.center_y = 20
263
+ },
264
+ actions: () => ({
265
+ r: reset_camera,
266
+ b: () => (color_mode = color_mode === `stability` ? `energy` : `stability`),
267
+ s: () => (show_stable = !show_stable),
268
+ u: () => (show_unstable = !show_unstable),
269
+ h: () => (show_hull_faces = !show_hull_faces),
270
+ l: () => (show_stable_labels = !show_stable_labels),
271
+ }),
361
272
  })
273
+ const { render_once } = interactions
274
+ const sorted_points_cache = $derived(interactions.sorted_points_cache)
362
275
 
363
276
  // Hull face color (customizable via controls)
364
277
  let hull_face_color = $state(`#4caf50`)
@@ -371,9 +284,7 @@
371
284
  let pulse_opacity = $derived(0.3 + 0.4 * pulse.unit)
372
285
 
373
286
  // Merge highlight style with defaults
374
- const merged_highlight_style = $derived(
375
- helpers.merge_highlight_style(highlight_style),
376
- )
287
+ const merged_highlight_style = $derived(helpers.merge_highlight_style(highlight_style))
377
288
 
378
289
  // Helper to check if entry is highlighted
379
290
  const is_highlighted = (entry: ConvexHullEntry): boolean =>
@@ -382,31 +293,27 @@
382
293
  // Re-render when important state changes
383
294
  $effect(() => {
384
295
  // oxfmt-ignore
385
- void [show_hull_faces, color_mode, color_scale, camera.rotation_x, camera.rotation_y, camera.zoom, camera.center_x, camera.center_y, plot_entries, hull_face_color, hull_face_opacity, hull_face_color_mode, element_colors, text_color, elements] // track reactively
296
+ void [show_hull_faces, color_mode, color_scale, camera.rotation_x, camera.rotation_y, camera.zoom, camera.center_x, camera.center_y, plot_entries, hull_data.visible_entries, hull_face_color, hull_face_opacity, hull_face_color_mode, element_colors, text_color, elements] // track reactively
386
297
 
387
298
  render_once()
388
299
  })
389
300
 
390
301
  // Visibility toggles are now bindable props
391
302
 
392
- // Smart label defaults - hide labels if too many entries
303
+ // Smart label defaults: hide labels for large datasets. Applied once per dataset
304
+ // (keyed on the entries prop) so later entry-count changes from temperature/gas
305
+ // filtering don't clobber the user's label toggles.
306
+ let label_defaults_applied_for: PhaseData[] | null = null
393
307
  $effect(() => {
394
- const total_entries = effective_entries.length
395
- if (total_entries > label_threshold) {
396
- show_stable_labels = false
397
- } else {
398
- // For smaller datasets, show stable labels by default
399
- show_stable_labels = true
400
- }
308
+ if (label_defaults_applied_for === entries) return
309
+ label_defaults_applied_for = entries
310
+ show_stable_labels = hull_data.effective_entries.length <= label_threshold
401
311
  show_unstable_labels = false
402
312
  })
403
313
 
404
- // Function to extract structure data from a convex hull entry
405
- function extract_structure_from_entry(
406
- entry: ConvexHullEntry,
407
- ): AnyStructure | null {
408
- const orig_entry = entries.find((ent) => ent.entry_id === entry.entry_id)
409
- return orig_entry?.structure as AnyStructure || null
314
+ // function (not const) so the create_canvas_interactions options above can reference it
315
+ function extract_structure_from_entry(entry: ConvexHullEntry): AnyStructure | null {
316
+ return helpers.extract_structure_from_entry(entries, entry)
410
317
  }
411
318
 
412
319
  const reset_camera = () => {
@@ -420,17 +327,17 @@
420
327
  reset_camera()
421
328
  fullscreen = DEFAULTS.convex_hull.quaternary.fullscreen
422
329
  info_pane_open = DEFAULTS.convex_hull.quaternary.info_pane_open
423
- legend_pane_open = DEFAULTS.convex_hull.quaternary.legend_pane_open
330
+ controls_open = DEFAULTS.convex_hull.quaternary.legend_pane_open
424
331
  color_mode = DEFAULTS.convex_hull.quaternary.color_mode
425
332
  color_scale = DEFAULTS.convex_hull.quaternary.color_scale as D3InterpolateName
426
333
  show_stable = DEFAULTS.convex_hull.quaternary.show_stable
427
334
  show_unstable = DEFAULTS.convex_hull.quaternary.show_unstable
335
+ hidden_categories = []
428
336
  show_stable_labels = DEFAULTS.convex_hull.quaternary.show_stable_labels
429
337
  show_unstable_labels = DEFAULTS.convex_hull.quaternary.show_unstable_labels
430
338
  // Use auto-computed threshold based on entry count instead of static default
431
- max_hull_dist_show_phases = auto_default_threshold
432
- max_hull_dist_show_labels =
433
- DEFAULTS.convex_hull.quaternary.max_hull_dist_show_labels
339
+ max_hull_dist_show_phases = hull_data.auto_default_threshold
340
+ max_hull_dist_show_labels = DEFAULTS.convex_hull.quaternary.max_hull_dist_show_labels
434
341
  show_hull_faces = DEFAULTS.convex_hull.quaternary.show_hull_faces
435
342
  hull_face_color = DEFAULTS.convex_hull.quaternary.hull_face_color
436
343
  hull_face_opacity = DEFAULTS.convex_hull.quaternary.hull_face_opacity
@@ -438,69 +345,6 @@
438
345
  .hull_face_color_mode as HullFaceColorMode
439
346
  }
440
347
 
441
- const handle_keydown = (event: KeyboardEvent) => {
442
- const target = event.target
443
- // Skip if focus is on an interactive element that handles Enter natively
444
- const interactive_selector =
445
- `input,textarea,select,button,a,[contenteditable="true"],[role="button"],[tabindex]:not([tabindex="-1"])`
446
- if (
447
- target instanceof HTMLElement &&
448
- target.matches(interactive_selector) &&
449
- target !== canvas
450
- ) return
451
-
452
- // Prevent double handling from canvas + wrapper bubbling
453
- if (event.target !== event.currentTarget && event.currentTarget !== canvas) return
454
-
455
- // Handle Enter for keyboard accessibility - select hovered entry
456
- if (event.key === `Enter`) {
457
- const entry = hover_data?.entry
458
- if (entry) {
459
- on_point_click?.(entry)
460
- if (enable_click_selection) {
461
- selected_entry = entry
462
- if (enable_structure_preview) {
463
- const structure = extract_structure_from_entry(entry)
464
- if (structure) {
465
- selected_structure = structure
466
- modal_place_right = helpers.calculate_modal_side(wrapper)
467
- modal_open = true
468
- }
469
- }
470
- }
471
- } else if (modal_open) {
472
- close_structure_popup()
473
- }
474
- return
475
- }
476
-
477
- const actions: Record<string, () => void> = {
478
- r: reset_camera,
479
- b: () => color_mode = color_mode === `stability` ? `energy` : `stability`,
480
- s: () => show_stable = !show_stable,
481
- u: () => show_unstable = !show_unstable,
482
- h: () => show_hull_faces = !show_hull_faces,
483
- l: () => show_stable_labels = !show_stable_labels,
484
- }
485
- actions[event.key.toLowerCase()]?.()
486
- }
487
-
488
- async function handle_file_drop(event: DragEvent): Promise<void> {
489
- drag_over = false
490
- const data = await helpers.parse_hull_entries_from_drop(event)
491
- if (data) on_file_drop?.(data)
492
- }
493
-
494
- async function copy_entry_data(
495
- entry: ConvexHullEntry,
496
- position: { x: number; y: number },
497
- ) {
498
- await helpers.copy_entry_to_clipboard(entry, position, (visible, pos) => {
499
- copy_feedback.visible = visible
500
- copy_feedback.position = pos
501
- })
502
- }
503
-
504
348
  const get_point_color = (entry: ConvexHullEntry): string =>
505
349
  helpers.get_point_color_for_entry(
506
350
  entry,
@@ -511,7 +355,7 @@
511
355
 
512
356
  // Cache energy color scale per frame/setting
513
357
  const energy_color_scale = $derived.by(() =>
514
- helpers.get_energy_color_scale(color_mode, color_scale, plot_entries)
358
+ helpers.get_energy_color_scale(color_mode, color_scale, plot_entries),
515
359
  )
516
360
 
517
361
  // Convex hull statistics - compute internally and expose via bindable prop
@@ -672,30 +516,10 @@
672
516
  const [p0, p1, p2, p3] = tet.vertices
673
517
 
674
518
  // Convert barycentric coordinates to tetrahedral 3D coordinates
675
- const tet0 = barycentric_to_tetrahedral([
676
- p0.x,
677
- p0.y,
678
- p0.z,
679
- 1 - p0.x - p0.y - p0.z,
680
- ])
681
- const tet1 = barycentric_to_tetrahedral([
682
- p1.x,
683
- p1.y,
684
- p1.z,
685
- 1 - p1.x - p1.y - p1.z,
686
- ])
687
- const tet2 = barycentric_to_tetrahedral([
688
- p2.x,
689
- p2.y,
690
- p2.z,
691
- 1 - p2.x - p2.y - p2.z,
692
- ])
693
- const tet3 = barycentric_to_tetrahedral([
694
- p3.x,
695
- p3.y,
696
- p3.z,
697
- 1 - p3.x - p3.y - p3.z,
698
- ])
519
+ const tet0 = barycentric_to_tetrahedral([p0.x, p0.y, p0.z, 1 - p0.x - p0.y - p0.z])
520
+ const tet1 = barycentric_to_tetrahedral([p1.x, p1.y, p1.z, 1 - p1.x - p1.y - p1.z])
521
+ const tet2 = barycentric_to_tetrahedral([p2.x, p2.y, p2.z, 1 - p2.x - p2.y - p2.z])
522
+ const tet3 = barycentric_to_tetrahedral([p3.x, p3.y, p3.z, 1 - p3.x - p3.y - p3.z])
699
523
 
700
524
  // Project to 2D screen space
701
525
  const proj0 = project_3d_point(tet0.x, tet0.y, tet0.z)
@@ -709,8 +533,14 @@
709
533
  (p0.x + p1.x + p2.x + p3.x) / 4,
710
534
  (p0.y + p1.y + p2.y + p3.y) / 4,
711
535
  (p0.z + p1.z + p2.z + p3.z) / 4,
712
- ((1 - p0.x - p0.y - p0.z) + (1 - p1.x - p1.y - p1.z) +
713
- (1 - p2.x - p2.y - p2.z) + (1 - p3.x - p3.y - p3.z)) / 4,
536
+ (1 -
537
+ p0.x -
538
+ p0.y -
539
+ p0.z +
540
+ (1 - p1.x - p1.y - p1.z) +
541
+ (1 - p2.x - p2.y - p2.z) +
542
+ (1 - p3.x - p3.y - p3.z)) /
543
+ 4,
714
544
  ]
715
545
 
716
546
  // Each tetrahedron has 4 triangular faces
@@ -736,14 +566,14 @@
736
566
  triangles.sort((a, b) => a.avg_depth - b.avg_depth)
737
567
 
738
568
  // Lazy computation for uniform mode: normalize alpha by formation energy
739
- let norm_alpha: ((w: number) => number) | null = null
569
+ let norm_alpha: ((energy: number) => number) | null = null
740
570
  if (hull_face_color_mode === `uniform`) {
741
571
  norm_alpha = (energy: number) => {
742
- const t = Math.max(
572
+ const frac = Math.max(
743
573
  0,
744
574
  Math.min(1, (0 - energy) / Math.max(1e-6, 0 - formation_energy_min)),
745
575
  )
746
- return t * hull_face_opacity
576
+ return frac * hull_face_opacity
747
577
  }
748
578
  }
749
579
 
@@ -752,7 +582,7 @@
752
582
  let min_w = 0
753
583
  if (hull_face_color_mode === `formation_energy`) {
754
584
  const all_avg_w = triangles.map((tri) => tri.avg_w)
755
- min_w = Math.min(...all_avg_w)
585
+ min_w = helpers.array_min(all_avg_w)
756
586
  energy_face_scale = helpers.get_energy_color_scale(
757
587
  `energy`,
758
588
  color_scale,
@@ -770,7 +600,7 @@
770
600
  }
771
601
  if (hull_face_color_mode === `dominant_element`) {
772
602
  // Find element with highest fraction
773
- const max_idx = tri.centroid_bary.indexOf(Math.max(...tri.centroid_bary))
603
+ const max_idx = tri.centroid_bary.indexOf(helpers.array_max(tri.centroid_bary))
774
604
  const el = elements[max_idx]
775
605
  return element_colors[el] ?? `#888888`
776
606
  }
@@ -784,9 +614,10 @@
784
614
  for (const tri of triangles) {
785
615
  const [v0, v1, v2] = tri.vertices
786
616
  // Uniform mode uses variable opacity; other modes use fixed opacity
787
- const alpha = hull_face_color_mode === `uniform`
788
- ? (norm_alpha?.(tri.avg_w) ?? hull_face_opacity)
789
- : hull_face_opacity
617
+ const alpha =
618
+ hull_face_color_mode === `uniform`
619
+ ? (norm_alpha?.(tri.avg_w) ?? hull_face_opacity)
620
+ : hull_face_opacity
790
621
  const face_color = get_face_color(tri)
791
622
 
792
623
  ctx.save()
@@ -809,59 +640,16 @@
809
640
 
810
641
  function draw_data_points(): void {
811
642
  if (!ctx || sorted_points_cache.length === 0) return
812
-
813
- for (const { entry, projected } of sorted_points_cache) {
814
- const is_stable = helpers.entry_is_stable(entry)
815
- const is_entry_highlighted = is_highlighted(entry)
816
- const color = get_point_color(entry)
817
- const size = (entry.size || (is_stable ? 6 : 4)) * canvas_dims.scale
818
- const marker = entry.marker || `circle`
819
-
820
- // Shadow
821
- const shadow_offset = Math.abs(entry.z) * 2 * canvas_dims.scale
822
- ctx.fillStyle = `rgba(0, 0, 0, 0.2)`
823
- const shadow_path = helpers.create_marker_path(size * 0.8, marker)
824
- ctx.save()
825
- ctx.translate(projected.x + shadow_offset, projected.y + shadow_offset)
826
- ctx.fill(shadow_path)
827
- ctx.restore()
828
-
829
- // Highlights
830
- if (selected_entry && entry.entry_id === selected_entry.entry_id) {
831
- helpers.draw_selection_highlight(
832
- ctx,
833
- projected,
834
- size,
835
- canvas_dims.scale,
836
- pulse.time,
837
- pulse_opacity,
838
- )
839
- }
840
- if (is_entry_highlighted) {
841
- helpers.draw_highlight_effect(
842
- ctx,
843
- projected,
844
- size,
845
- canvas_dims.scale,
846
- pulse.time,
847
- merged_highlight_style,
848
- )
849
- }
850
-
851
- // Main point with marker symbol
852
- ctx.fillStyle =
853
- is_entry_highlighted && merged_highlight_style.effect === `color`
854
- ? merged_highlight_style.color
855
- : color
856
- ctx.strokeStyle = is_stable ? `#ffffff` : `#000000`
857
- ctx.lineWidth = 0.5 * canvas_dims.scale
858
- const marker_path = helpers.create_marker_path(size, marker)
859
- ctx.save()
860
- ctx.translate(projected.x, projected.y)
861
- ctx.fill(marker_path)
862
- ctx.stroke(marker_path)
863
- ctx.restore()
864
- }
643
+ helpers.draw_hull_points(ctx, sorted_points_cache, {
644
+ scale: canvas_dims.scale,
645
+ shadow_factor: 2,
646
+ selected_entry,
647
+ is_highlighted,
648
+ get_point_color,
649
+ highlight_style: merged_highlight_style,
650
+ pulse_time: pulse.time,
651
+ pulse_opacity,
652
+ })
865
653
 
866
654
  if (!merged_config.show_labels) return
867
655
 
@@ -871,9 +659,12 @@
871
659
  .filter((entry) => {
872
660
  if (entry.is_element) return false
873
661
  const is_stable = helpers.entry_is_stable(entry)
874
- return (is_stable && show_stable_labels) ||
875
- (!is_stable && show_unstable_labels &&
662
+ return (
663
+ (is_stable && show_stable_labels) ||
664
+ (!is_stable &&
665
+ show_unstable_labels &&
876
666
  (entry.e_above_hull ?? 0) <= max_hull_dist_show_labels)
667
+ )
877
668
  }),
878
669
  )
879
670
 
@@ -925,169 +716,7 @@
925
716
  draw_data_points() // Draw data points (on top)
926
717
  }
927
718
 
928
- function handle_mouse_down(event: MouseEvent) {
929
- is_dragging = true
930
- drag_started = false
931
- hover_data = null
932
- on_point_hover?.(null)
933
- last_mouse = { x: event.clientX, y: event.clientY }
934
- }
935
-
936
- const handle_mouse_move = (event: MouseEvent) => {
937
- if (!is_dragging) return
938
- const [dx, dy] = [event.clientX - last_mouse.x, event.clientY - last_mouse.y]
939
-
940
- // Mark as drag if any movement occurred
941
- if (dx !== 0 || dy !== 0) drag_started = true
942
-
943
- // With Cmd/Ctrl held: pan the view instead of rotating
944
- if (event.metaKey || event.ctrlKey) {
945
- camera.center_x += dx
946
- camera.center_y += dy
947
- } else {
948
- camera.rotation_y += dx * 0.005
949
- camera.rotation_x = Math.max(
950
- -Math.PI / 3,
951
- Math.min(Math.PI / 3, camera.rotation_x - dy * 0.005),
952
- )
953
- }
954
- last_mouse = { x: event.clientX, y: event.clientY }
955
- }
956
-
957
- const handle_wheel = (event: WheelEvent) => {
958
- event.preventDefault()
959
- camera.zoom = Math.max(
960
- 1.0,
961
- Math.min(15, camera.zoom * (event.deltaY > 0 ? 0.98 : 1.02)),
962
- )
963
- }
964
-
965
- const handle_hover = (event: MouseEvent) => {
966
- if (is_dragging) return
967
- const entry = find_entry_at_mouse(event)
968
- hover_data = entry
969
- ? { entry, position: { x: event.clientX, y: event.clientY } }
970
- : null
971
- on_point_hover?.(hover_data)
972
- }
973
-
974
- const find_entry_at_mouse = (event: MouseEvent): ConvexHullEntry | null =>
975
- helpers.find_hull_entry_at_mouse(
976
- canvas,
977
- event,
978
- visible_entries,
979
- (x: number, y: number, z: number) => {
980
- const projected = project_3d_point(x, y, z)
981
- return { x: projected.x, y: projected.y }
982
- },
983
- )
984
-
985
- const handle_click = (event: MouseEvent) => {
986
- event.stopPropagation()
987
-
988
- // Check if this was a drag operation (any mouse movement during drag)
989
- const was_drag = drag_started
990
- drag_started = false // Reset for next interaction
991
- if (was_drag) return // Don't trigger click if this was a drag
992
-
993
- const entry = find_entry_at_mouse(event)
994
- if (entry) {
995
- on_point_click?.(entry)
996
- if (enable_click_selection) {
997
- selected_entry = entry
998
- if (enable_structure_preview) {
999
- const structure = extract_structure_from_entry(entry)
1000
- if (structure) {
1001
- selected_structure = structure
1002
- modal_place_right = helpers.calculate_modal_side(wrapper)
1003
- modal_open = true
1004
- }
1005
- }
1006
- }
1007
- } else if (modal_open) close_structure_popup()
1008
- }
1009
-
1010
- function close_structure_popup() {
1011
- modal_open = false
1012
- selected_structure = null
1013
- selected_entry = null
1014
- }
1015
-
1016
- const handle_double_click = (event: MouseEvent) => {
1017
- const entry = find_entry_at_mouse(event)
1018
- if (entry) {
1019
- copy_entry_data(entry, {
1020
- x: event.clientX,
1021
- y: event.clientY,
1022
- })
1023
- }
1024
- }
1025
-
1026
- function render_once() {
1027
- if (!frame_id) {
1028
- frame_id = requestAnimationFrame(() => {
1029
- render_frame()
1030
- frame_id = 0
1031
- })
1032
- }
1033
- }
1034
-
1035
- function update_canvas_size() {
1036
- if (!canvas) return
1037
- const dpr = globalThis.devicePixelRatio || 1
1038
- const container = canvas.parentElement
1039
- const rect = container?.getBoundingClientRect()
1040
- const [width, height] = rect ? [rect.width, rect.height] : [400, 400]
1041
-
1042
- const new_width = Math.max(0, Math.round(width * dpr))
1043
- const new_height = Math.max(0, Math.round(height * dpr))
1044
- canvas_dims = { width, height, scale: Math.min(width, height) / 600 }
1045
-
1046
- if (!ctx || canvas.width !== new_width || canvas.height !== new_height) {
1047
- canvas.width = new_width
1048
- canvas.height = new_height
1049
- ctx = canvas.getContext(`2d`)
1050
- if (ctx) {
1051
- ctx.setTransform(dpr, 0, 0, dpr, 0, 0)
1052
- ctx.imageSmoothingEnabled = true
1053
- ctx.imageSmoothingQuality = `high`
1054
- }
1055
- }
1056
- render_once()
1057
- }
1058
-
1059
- $effect(() => {
1060
- if (!canvas) return
1061
-
1062
- // Initial setup
1063
- update_canvas_size()
1064
-
1065
- // Watch for resize events - only update canvas, don't reset camera
1066
- const resize_observer = new ResizeObserver(update_canvas_size)
1067
-
1068
- const container = canvas.parentElement
1069
- if (container) resize_observer.observe(container)
1070
-
1071
- return () => { // Cleanup on unmount
1072
- if (frame_id) cancelAnimationFrame(frame_id)
1073
- resize_observer.disconnect()
1074
- }
1075
- })
1076
-
1077
- // Fullscreen handling with camera reset
1078
- let was_fullscreen = $state(fullscreen)
1079
- $effect(() => {
1080
- setup_fullscreen_effect(fullscreen, wrapper, (entering_fullscreen) => {
1081
- if (entering_fullscreen !== was_fullscreen) {
1082
- camera.center_x = 0
1083
- camera.center_y = 20
1084
- was_fullscreen = entering_fullscreen
1085
- }
1086
- })
1087
- set_fullscreen_bg(wrapper, fullscreen, `--hull-4d-bg-fullscreen`)
1088
- })
1089
-
1090
- // Performance: Cache canvas dimensions and pre-compute sorted point projections
719
+ // Performance: Cache canvas dimensions and formation energy minimum
1091
720
  let canvas_dims = $state({ width: 600, height: 600, scale: 1 })
1092
721
  const formation_energy_min = $derived.by(() => {
1093
722
  let min_energy = 0
@@ -1096,65 +725,41 @@
1096
725
  }
1097
726
  return min_energy
1098
727
  })
1099
- const sorted_points_cache = $derived.by(() => {
1100
- if (!canvas || visible_entries.length === 0) return []
1101
- return visible_entries
1102
- .map((entry) => ({
1103
- entry,
1104
- projected: project_3d_point(entry.x, entry.y, entry.z),
1105
- }))
1106
- .sort((a, b) => a.projected.depth - b.projected.depth)
1107
- })
1108
728
 
1109
- let style = $derived(
1110
- `--hull-stable-color:${merged_config.colors?.stable || `#0072B2`};
1111
- --hull-unstable-color:${merged_config.colors?.unstable || `#E69F00`};
1112
- --hull-edge-color:${merged_config.colors?.edge || `var(--text-color, #212121)`};
1113
- --hull-text-color:${
1114
- merged_config.colors?.annotation || `var(--text-color, #212121)`
1115
- }`,
1116
- )
729
+ let style = $derived(helpers.hull_style_css(merged_config.colors))
1117
730
  </script>
1118
731
 
1119
732
  <svelte:document
1120
733
  onfullscreenchange={() => {
1121
- fullscreen = Boolean(document.fullscreenElement)
734
+ // tie fullscreen state to this component's own wrapper, not any fullscreen element
735
+ fullscreen = document.fullscreenElement === wrapper
1122
736
  }}
1123
- onmousemove={handle_mouse_move}
1124
- onmouseup={() => [is_dragging, drag_started] = [false, false]}
737
+ onmousemove={interactions.handle_mouse_move}
738
+ onmouseup={interactions.handle_mouse_up}
1125
739
  />
1126
740
 
1127
741
  <div
1128
742
  {...rest}
1129
- class="convex-hull-4d {rest.class ?? ``}"
1130
- class:dragover={drag_over}
743
+ class={[`convex-hull-4d`, rest.class]}
744
+ class:dragover={interactions.drag_over}
1131
745
  style={`${style}; ${rest.style ?? ``}`}
1132
746
  data-has-selection={selected_entry !== null}
1133
- data-has-hover={hover_data !== null}
1134
- data-is-dragging={is_dragging}
747
+ data-has-hover={interactions.hover_data !== null}
748
+ data-is-dragging={interactions.is_dragging}
1135
749
  data-rotation-x={camera.rotation_x.toFixed(4)}
1136
750
  data-rotation-y={camera.rotation_y.toFixed(4)}
1137
751
  bind:this={wrapper}
1138
752
  role="application"
1139
753
  tabindex="-1"
1140
- onkeydown={handle_keydown}
1141
- ondrop={handle_file_drop}
1142
- ondragover={(event) => {
1143
- event.preventDefault()
1144
- drag_over = true
1145
- }}
1146
- ondragleave={(event) => {
1147
- event.preventDefault()
1148
- drag_over = false
1149
- }}
754
+ {...interactions.wrapper_handlers}
1150
755
  aria-label="Convex hull visualization"
1151
756
  >
1152
757
  {@render children?.({
1153
- stable_entries,
1154
- unstable_entries,
1155
- highlighted_entries,
1156
- selected_entry,
1157
- })}
758
+ stable_entries,
759
+ unstable_entries,
760
+ highlighted_entries,
761
+ selected_entry,
762
+ })}
1158
763
  <h3 style="position: absolute; left: 1em; top: 1ex; margin: 0">
1159
764
  {@html sanitize_html(merged_controls.title || phase_stats?.chemical_system || ``)}
1160
765
  </h3>
@@ -1163,12 +768,7 @@
1163
768
  bind:this={canvas}
1164
769
  tabindex="0"
1165
770
  aria-label={merged_controls.title || phase_stats?.chemical_system || `4D Convex Hull`}
1166
- onmousedown={handle_mouse_down}
1167
- onmousemove={handle_hover}
1168
- onclick={handle_click}
1169
- onkeydown={handle_keydown}
1170
- ondblclick={handle_double_click}
1171
- onwheel={handle_wheel}
771
+ {...interactions.canvas_handlers}
1172
772
  ></canvas>
1173
773
 
1174
774
  {#if entries.length === 0}
@@ -1180,14 +780,9 @@
1180
780
 
1181
781
  <!-- Energy above hull Color Bar -->
1182
782
  {#if color_mode === `energy` && plot_entries.length > 0}
1183
- {@const hull_distances = plot_entries
1184
- .map((entry) => entry.e_above_hull)
1185
- .filter((val): val is number => typeof val === `number`)}
1186
- {@const min_energy = hull_distances.length > 0 ? Math.min(...hull_distances) : 0}
1187
- {@const max_energy = hull_distances.length > 0 ? Math.max(...hull_distances, 0.1) : 0.1}
1188
783
  <ColorBar
1189
784
  title="Energy above hull (eV/atom)"
1190
- range={[min_energy, max_energy]}
785
+ range={helpers.hull_distance_range(plot_entries)}
1191
786
  {color_scale}
1192
787
  wrapper_style="position: absolute; bottom: 2em; left: 1em; width: 200px;"
1193
788
  bar_style="height: 12px;"
@@ -1195,137 +790,61 @@
1195
790
  />
1196
791
  {/if}
1197
792
 
1198
- <!-- Control buttons (top-right corner like Structure.svelte) -->
1199
- {#if controls_config.mode !== `never`}
1200
- <section class="control-buttons {controls_config.class}">
1201
- {#if controls_config.visible(`reset`)}
1202
- <button
1203
- type="button"
1204
- onclick={reset_all}
1205
- title="Reset camera view (R key)"
1206
- class="reset-camera-btn"
1207
- >
1208
- <Icon icon="Reset" />
1209
- </button>
1210
- {/if}
1211
-
1212
- {#if enable_info_pane && phase_stats && controls_config.visible(`info-pane`)}
1213
- <ConvexHullInfoPane
1214
- bind:pane_open={info_pane_open}
1215
- {phase_stats}
1216
- {stable_entries}
1217
- {unstable_entries}
1218
- {show_stable}
1219
- {show_unstable}
1220
- {max_hull_dist_show_phases}
1221
- {max_hull_dist_show_labels}
1222
- {label_threshold}
1223
- toggle_props={{ class: `info-btn` }}
1224
- />
1225
- {/if}
1226
-
1227
- {#if enable_fullscreen && controls_config.visible(`fullscreen`)}
1228
- <button
1229
- type="button"
1230
- onclick={() => toggle_fullscreen(wrapper)}
1231
- title="{fullscreen ? `Exit` : `Enter`} fullscreen"
1232
- class="fullscreen-btn"
1233
- >
1234
- <Icon icon="{fullscreen ? `Exit` : ``}Fullscreen" />
1235
- </button>
1236
- {/if}
1237
-
1238
- <!-- Legend controls pane -->
1239
- {#if controls_config.visible(`controls`)}
1240
- <ConvexHullControls
1241
- bind:controls_open={legend_pane_open}
1242
- bind:color_mode
1243
- bind:color_scale
1244
- bind:show_stable
1245
- bind:show_unstable
1246
- bind:show_stable_labels
1247
- bind:show_unstable_labels
1248
- bind:max_hull_dist_show_phases
1249
- bind:max_hull_dist_show_labels
1250
- {max_hull_dist_in_data}
1251
- {stable_entries}
1252
- {unstable_entries}
1253
- {camera}
1254
- {merged_controls}
1255
- toggle_props={{ class: `legend-controls-btn` }}
1256
- {show_hull_faces}
1257
- on_hull_faces_change={(value: boolean) => show_hull_faces = value}
1258
- {hull_face_color}
1259
- on_hull_face_color_change={(value: string) => hull_face_color = value}
1260
- {hull_face_opacity}
1261
- on_hull_face_opacity_change={(value: number) => hull_face_opacity = value}
1262
- {hull_face_color_mode}
1263
- on_hull_face_color_mode_change={(value: HullFaceColorMode) =>
1264
- hull_face_color_mode = value}
1265
- bind:energy_source_mode
1266
- {has_precomputed_e_form}
1267
- {can_compute_e_form}
1268
- {has_precomputed_hull}
1269
- {can_compute_hull}
1270
- />
1271
- {/if}
1272
- </section>
1273
- {/if}
1274
-
1275
- {#if has_temp_data && temperature !== undefined}
1276
- <TemperatureSlider {available_temperatures} bind:temperature />
793
+ <!-- Toolbar + tooltip/copy-feedback/drag/structure-popup chrome -->
794
+ <ConvexHullChrome
795
+ {interactions}
796
+ {hull_data}
797
+ {controls_config}
798
+ {reset_all}
799
+ reset_title="Reset view and settings"
800
+ {enable_info_pane}
801
+ {phase_stats}
802
+ {label_threshold}
803
+ {fullscreen}
804
+ {fullscreen_toggle}
805
+ {wrapper}
806
+ {camera}
807
+ {merged_controls}
808
+ {stable_entries}
809
+ {unstable_entries}
810
+ {get_point_color}
811
+ {merged_highlight_style}
812
+ {is_highlighted}
813
+ {tooltip}
814
+ {selected_entry}
815
+ bind:show_hull_faces
816
+ bind:hull_face_color
817
+ bind:hull_face_opacity
818
+ bind:hull_face_color_mode
819
+ bind:info_pane_open
820
+ bind:controls_open
821
+ bind:color_mode
822
+ bind:color_scale
823
+ bind:show_stable
824
+ bind:show_unstable
825
+ {entry_category}
826
+ bind:hidden_categories
827
+ bind:show_stable_labels
828
+ bind:show_unstable_labels
829
+ bind:max_hull_dist_show_phases
830
+ bind:max_hull_dist_show_labels
831
+ bind:energy_source_mode
832
+ />
833
+
834
+ {#if hull_data.has_temp_data && temperature !== undefined}
835
+ <TemperatureSlider
836
+ available_temperatures={hull_data.available_temperatures}
837
+ bind:temperature
838
+ />
1277
839
  {/if}
1278
840
 
1279
- {#if gas_analysis.has_gas_dependent_elements && merged_gas_config}
841
+ {#if hull_data.gas_analysis.has_gas_dependent_elements && merged_gas_config}
1280
842
  <GasPressureControls
1281
843
  config={merged_gas_config}
1282
844
  bind:pressures={gas_pressures}
1283
845
  temperature={temperature ?? 300}
1284
846
  />
1285
847
  {/if}
1286
-
1287
- <!-- Hover tooltip -->
1288
- {#if hover_data}
1289
- {@const { entry, position } = hover_data}
1290
- {@const entry_highlight = is_highlighted(entry) ? merged_highlight_style : undefined}
1291
- {@const tooltip_style =
1292
- `z-index: ${CONVEX_HULL_STYLE.z_index.tooltip}; backdrop-filter: blur(4px);
1293
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);`}
1294
- <PlotTooltip
1295
- x={position.x}
1296
- y={position.y}
1297
- offset={{ x: 10, y: -10 }}
1298
- bg_color={get_point_color(entry)}
1299
- fixed
1300
- style={tooltip_style}
1301
- >
1302
- <ConvexHullTooltip
1303
- {entry}
1304
- {polymorph_stats_map}
1305
- highlight_style={entry_highlight}
1306
- {tooltip}
1307
- />
1308
- </PlotTooltip>
1309
- {/if}
1310
-
1311
- <!-- Copy-to-clipboard feedback (double-click on point) -->
1312
- <ClickFeedback bind:visible={copy_feedback.visible} position={copy_feedback.position} />
1313
-
1314
- <!-- Drag over overlay -->
1315
- <DragOverlay visible={drag_over} />
1316
-
1317
- {#if modal_open && selected_structure}
1318
- <StructurePopup
1319
- structure={selected_structure}
1320
- place_right={modal_place_right}
1321
- stats={{
1322
- id: selected_entry?.entry_id,
1323
- e_above_hull: selected_entry?.e_above_hull,
1324
- e_form: selected_entry?.e_form_per_atom,
1325
- }}
1326
- onclose={close_structure_popup}
1327
- />
1328
- {/if}
1329
848
  </div>
1330
849
 
1331
850
  <style>
@@ -1353,42 +872,4 @@
1353
872
  canvas:active {
1354
873
  cursor: grabbing;
1355
874
  }
1356
- .control-buttons {
1357
- position: absolute;
1358
- top: 1ex;
1359
- right: 1ex;
1360
- display: flex;
1361
- gap: 8px;
1362
- transition: opacity 0.2s ease-in-out;
1363
- }
1364
- .control-buttons.hover-visible {
1365
- opacity: 0;
1366
- pointer-events: none;
1367
- }
1368
- .convex-hull-4d:hover .control-buttons.hover-visible,
1369
- .convex-hull-4d:focus-within .control-buttons.hover-visible {
1370
- opacity: 1;
1371
- pointer-events: auto;
1372
- }
1373
- .control-buttons.always-visible {
1374
- opacity: 1;
1375
- pointer-events: auto;
1376
- }
1377
- .control-buttons :global(.draggable-pane) {
1378
- z-index: 1001 !important;
1379
- }
1380
- .control-buttons :global(button) {
1381
- background: transparent;
1382
- border: none;
1383
- padding: 4px;
1384
- cursor: pointer;
1385
- border-radius: 3px;
1386
- color: var(--text-color, currentColor);
1387
- transition: background-color 0.2s;
1388
- display: flex;
1389
- font-size: clamp(0.85em, 2cqmin, 1.3em);
1390
- }
1391
- .control-buttons :global(button):hover {
1392
- background-color: color-mix(in srgb, currentColor 8%, transparent);
1393
- }
1394
875
  </style>