matterviz 0.4.0 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (527) hide show
  1. package/dist/EmptyState.svelte +10 -4
  2. package/dist/FilePicker.svelte +12 -14
  3. package/dist/Icon.svelte +7 -4
  4. package/dist/MillerIndexInput.svelte +2 -2
  5. package/dist/app.css +53 -0
  6. package/dist/brillouin/BrillouinZone.svelte +132 -219
  7. package/dist/brillouin/BrillouinZone.svelte.d.ts +9 -20
  8. package/dist/brillouin/BrillouinZoneControls.svelte +3 -6
  9. package/dist/brillouin/BrillouinZoneExportPane.svelte +47 -96
  10. package/dist/brillouin/BrillouinZoneExportPane.svelte.d.ts +1 -1
  11. package/dist/brillouin/BrillouinZoneInfoPane.svelte +15 -42
  12. package/dist/brillouin/BrillouinZoneInfoPane.svelte.d.ts +2 -3
  13. package/dist/brillouin/BrillouinZoneScene.svelte +63 -230
  14. package/dist/brillouin/BrillouinZoneScene.svelte.d.ts +3 -23
  15. package/dist/brillouin/ReciprocalVectors.svelte +39 -0
  16. package/dist/brillouin/ReciprocalVectors.svelte.d.ts +9 -0
  17. package/dist/brillouin/compute.d.ts +2 -0
  18. package/dist/brillouin/compute.js +80 -77
  19. package/dist/brillouin/geometry.d.ts +8 -0
  20. package/dist/brillouin/geometry.js +57 -0
  21. package/dist/brillouin/index.d.ts +2 -0
  22. package/dist/brillouin/index.js +2 -0
  23. package/dist/brillouin/types.d.ts +2 -2
  24. package/dist/chempot-diagram/ChemPotDiagram.svelte +33 -38
  25. package/dist/chempot-diagram/ChemPotDiagram.svelte.d.ts +1 -1
  26. package/dist/chempot-diagram/ChemPotDiagram2D.svelte +143 -246
  27. package/dist/chempot-diagram/ChemPotDiagram2D.svelte.d.ts +4 -1
  28. package/dist/chempot-diagram/ChemPotDiagram3D.svelte +652 -976
  29. package/dist/chempot-diagram/ChemPotDiagram3D.svelte.d.ts +7 -1
  30. package/dist/chempot-diagram/async-compute.svelte.js +1 -1
  31. package/dist/chempot-diagram/color.d.ts +3 -6
  32. package/dist/chempot-diagram/color.js +5 -5
  33. package/dist/chempot-diagram/compute.d.ts +3 -3
  34. package/dist/chempot-diagram/compute.js +3 -1
  35. package/dist/chempot-diagram/controls-state.svelte.d.ts +10 -0
  36. package/dist/chempot-diagram/controls-state.svelte.js +42 -0
  37. package/dist/chempot-diagram/export.d.ts +47 -0
  38. package/dist/chempot-diagram/export.js +133 -0
  39. package/dist/chempot-diagram/index.d.ts +1 -0
  40. package/dist/chempot-diagram/index.js +1 -0
  41. package/dist/chempot-diagram/pointer.d.ts +0 -10
  42. package/dist/chempot-diagram/pointer.js +4 -4
  43. package/dist/chempot-diagram/types.d.ts +3 -3
  44. package/dist/colors/index.d.ts +0 -4
  45. package/dist/colors/index.js +21 -22
  46. package/dist/composition/BarChart.svelte +37 -31
  47. package/dist/composition/BubbleChart.svelte +17 -17
  48. package/dist/composition/Composition.svelte +7 -11
  49. package/dist/composition/Formula.svelte +18 -25
  50. package/dist/composition/FormulaFilter.svelte +96 -98
  51. package/dist/composition/PieChart.svelte +60 -46
  52. package/dist/composition/chem-sys.js +3 -2
  53. package/dist/composition/format.js +4 -3
  54. package/dist/composition/index.d.ts +2 -0
  55. package/dist/composition/index.js +14 -0
  56. package/dist/composition/parse.d.ts +2 -3
  57. package/dist/composition/parse.js +27 -28
  58. package/dist/constants.js +11 -9
  59. package/dist/controls.d.ts +1 -0
  60. package/dist/controls.js +0 -1
  61. package/dist/convex-hull/ConvexHull.svelte +20 -25
  62. package/dist/convex-hull/ConvexHull.svelte.d.ts +1 -4
  63. package/dist/convex-hull/ConvexHull2D.svelte +176 -256
  64. package/dist/convex-hull/ConvexHull2D.svelte.d.ts +1 -1
  65. package/dist/convex-hull/ConvexHull3D.svelte +269 -804
  66. package/dist/convex-hull/ConvexHull3D.svelte.d.ts +1 -1
  67. package/dist/convex-hull/ConvexHull4D.svelte +236 -755
  68. package/dist/convex-hull/ConvexHull4D.svelte.d.ts +1 -1
  69. package/dist/convex-hull/ConvexHullChrome.svelte +264 -0
  70. package/dist/convex-hull/ConvexHullChrome.svelte.d.ts +30 -0
  71. package/dist/convex-hull/ConvexHullControls.svelte +128 -49
  72. package/dist/convex-hull/ConvexHullControls.svelte.d.ts +7 -6
  73. package/dist/convex-hull/ConvexHullInfoPane.svelte +18 -5
  74. package/dist/convex-hull/ConvexHullInfoPane.svelte.d.ts +6 -5
  75. package/dist/convex-hull/ConvexHullStats.svelte +210 -351
  76. package/dist/convex-hull/ConvexHullStats.svelte.d.ts +3 -1
  77. package/dist/convex-hull/ConvexHullTooltip.svelte +16 -14
  78. package/dist/convex-hull/ConvexHullTooltip.svelte.d.ts +2 -1
  79. package/dist/convex-hull/GasPressureControls.svelte +5 -12
  80. package/dist/convex-hull/StructurePopup.svelte +17 -21
  81. package/dist/convex-hull/StructurePopup.svelte.d.ts +2 -0
  82. package/dist/convex-hull/TemperatureSlider.svelte +4 -10
  83. package/dist/convex-hull/barycentric-coords.d.ts +2 -4
  84. package/dist/convex-hull/barycentric-coords.js +6 -33
  85. package/dist/convex-hull/canvas-interactions.svelte.d.ts +79 -0
  86. package/dist/convex-hull/canvas-interactions.svelte.js +279 -0
  87. package/dist/convex-hull/helpers.d.ts +47 -17
  88. package/dist/convex-hull/helpers.js +186 -101
  89. package/dist/convex-hull/hull-state.svelte.d.ts +44 -0
  90. package/dist/convex-hull/hull-state.svelte.js +124 -0
  91. package/dist/convex-hull/index.d.ts +9 -7
  92. package/dist/convex-hull/index.js +7 -2
  93. package/dist/convex-hull/thermodynamics.js +109 -937
  94. package/dist/convex-hull/types.d.ts +12 -4
  95. package/dist/convex-hull/types.js +12 -0
  96. package/dist/coordination/CoordinationBarPlot.svelte +41 -43
  97. package/dist/coordination/CoordinationBarPlot.svelte.d.ts +1 -1
  98. package/dist/coordination/calc-coordination.d.ts +1 -1
  99. package/dist/coordination/calc-coordination.js +28 -25
  100. package/dist/element/BohrAtom.svelte +6 -8
  101. package/dist/element/ElementHeading.svelte +4 -1
  102. package/dist/element/ElementPhoto.svelte +11 -9
  103. package/dist/element/ElementStats.svelte +12 -10
  104. package/dist/element/ElementTile.svelte +24 -29
  105. package/dist/element/ElementTile.svelte.d.ts +1 -1
  106. package/dist/element/Nucleus.svelte +4 -2
  107. package/dist/element/data.d.ts +1 -2
  108. package/dist/element/helpers.d.ts +4 -0
  109. package/dist/element/helpers.js +18 -0
  110. package/dist/element/index.d.ts +1 -0
  111. package/dist/element/index.js +3 -0
  112. package/dist/feedback/DragOverlay.svelte +5 -3
  113. package/dist/feedback/DragOverlay.svelte.d.ts +2 -1
  114. package/dist/feedback/StatusMessage.svelte +20 -7
  115. package/dist/fermi-surface/FermiSlice.svelte +21 -18
  116. package/dist/fermi-surface/FermiSurface.svelte +78 -165
  117. package/dist/fermi-surface/FermiSurface.svelte.d.ts +9 -20
  118. package/dist/fermi-surface/FermiSurfaceControls.svelte +6 -20
  119. package/dist/fermi-surface/FermiSurfaceScene.svelte +93 -272
  120. package/dist/fermi-surface/FermiSurfaceScene.svelte.d.ts +3 -23
  121. package/dist/fermi-surface/FermiSurfaceTooltip.svelte +9 -6
  122. package/dist/fermi-surface/compute.js +14 -13
  123. package/dist/fermi-surface/export.js +4 -15
  124. package/dist/fermi-surface/index.d.ts +0 -1
  125. package/dist/fermi-surface/index.js +0 -1
  126. package/dist/fermi-surface/parse.d.ts +1 -1
  127. package/dist/fermi-surface/parse.js +84 -97
  128. package/dist/fermi-surface/types.d.ts +2 -2
  129. package/dist/heatmap-matrix/HeatmapMatrix.svelte +161 -185
  130. package/dist/heatmap-matrix/HeatmapMatrix.svelte.d.ts +6 -5
  131. package/dist/heatmap-matrix/HeatmapMatrixControls.svelte +12 -9
  132. package/dist/heatmap-matrix/HeatmapMatrixControls.svelte.d.ts +5 -5
  133. package/dist/heatmap-matrix/index.d.ts +8 -2
  134. package/dist/icons.d.ts +4 -0
  135. package/dist/icons.js +4 -0
  136. package/dist/index.d.ts +1 -0
  137. package/dist/index.js +1 -0
  138. package/dist/io/ExportPane.svelte +170 -0
  139. package/dist/io/ExportPane.svelte.d.ts +17 -0
  140. package/dist/io/decompress.d.ts +2 -1
  141. package/dist/io/decompress.js +22 -36
  142. package/dist/io/export.d.ts +9 -1
  143. package/dist/io/export.js +37 -29
  144. package/dist/io/fetch.d.ts +2 -4
  145. package/dist/io/fetch.js +0 -11
  146. package/dist/io/file-drop.d.ts +7 -0
  147. package/dist/io/file-drop.js +13 -0
  148. package/dist/io/index.d.ts +1 -0
  149. package/dist/io/index.js +3 -0
  150. package/dist/io/is-binary.d.ts +9 -0
  151. package/dist/io/is-binary.js +45 -1
  152. package/dist/io/types.d.ts +13 -0
  153. package/dist/io/url-drop.js +13 -25
  154. package/dist/isosurface/Isosurface.svelte +17 -23
  155. package/dist/isosurface/IsosurfaceControls.svelte +37 -42
  156. package/dist/isosurface/parse-vaspwave.d.ts +3 -0
  157. package/dist/isosurface/parse-vaspwave.js +138 -0
  158. package/dist/isosurface/parse.js +50 -48
  159. package/dist/labels.d.ts +6 -2
  160. package/dist/labels.js +44 -14
  161. package/dist/layout/FullscreenButton.svelte +34 -0
  162. package/dist/layout/FullscreenButton.svelte.d.ts +10 -0
  163. package/dist/layout/FullscreenToggle.svelte +12 -12
  164. package/dist/layout/InfoCard.svelte +7 -18
  165. package/dist/layout/InfoTag.svelte +1 -3
  166. package/dist/layout/NumberRangeInput.svelte +46 -0
  167. package/dist/layout/NumberRangeInput.svelte.d.ts +12 -0
  168. package/dist/layout/PropertyFilter.svelte +2 -6
  169. package/dist/layout/SettingsSection.svelte +21 -17
  170. package/dist/layout/SubpageGrid.svelte +3 -1
  171. package/dist/layout/ViewerChrome.svelte +115 -0
  172. package/dist/layout/ViewerChrome.svelte.d.ts +18 -0
  173. package/dist/layout/fullscreen.d.ts +4 -0
  174. package/dist/layout/fullscreen.js +3 -5
  175. package/dist/layout/fullscreen.svelte.d.ts +8 -0
  176. package/dist/layout/fullscreen.svelte.js +36 -0
  177. package/dist/layout/index.d.ts +4 -0
  178. package/dist/layout/index.js +4 -0
  179. package/dist/layout/json-tree/JsonNode.svelte +8 -11
  180. package/dist/layout/json-tree/JsonTree.svelte +53 -77
  181. package/dist/layout/json-tree/JsonValue.svelte +7 -15
  182. package/dist/layout/json-tree/utils.js +14 -23
  183. package/dist/math.d.ts +9 -3
  184. package/dist/math.js +37 -21
  185. package/dist/overlays/ContextMenu.svelte +4 -2
  186. package/dist/overlays/CopyButton.svelte +11 -5
  187. package/dist/overlays/DragControlTab.svelte +6 -1
  188. package/dist/overlays/DraggablePane.svelte +81 -26
  189. package/dist/overlays/DraggablePane.svelte.d.ts +1 -0
  190. package/dist/overlays/GlassChip.svelte +30 -0
  191. package/dist/overlays/GlassChip.svelte.d.ts +8 -0
  192. package/dist/overlays/InfoPaneCards.svelte +11 -23
  193. package/dist/overlays/index.d.ts +11 -0
  194. package/dist/overlays/index.js +26 -0
  195. package/dist/overlays/portal.d.ts +2 -0
  196. package/dist/overlays/portal.js +14 -0
  197. package/dist/periodic-table/PeriodicTable.svelte +115 -146
  198. package/dist/periodic-table/PeriodicTable.svelte.d.ts +5 -3
  199. package/dist/periodic-table/PeriodicTableControls.svelte +11 -63
  200. package/dist/periodic-table/TableInset.svelte +9 -4
  201. package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte +50 -71
  202. package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte.d.ts +6 -8
  203. package/dist/phase-diagram/PhaseDiagramControls.svelte +73 -96
  204. package/dist/phase-diagram/PhaseDiagramControls.svelte.d.ts +4 -3
  205. package/dist/phase-diagram/PhaseDiagramEditorPane.svelte +4 -2
  206. package/dist/phase-diagram/PhaseDiagramEditorPane.svelte.d.ts +2 -3
  207. package/dist/phase-diagram/PhaseDiagramExportPane.svelte +49 -134
  208. package/dist/phase-diagram/PhaseDiagramExportPane.svelte.d.ts +3 -4
  209. package/dist/phase-diagram/PhaseDiagramTooltip.svelte +54 -58
  210. package/dist/phase-diagram/TdbInfoPanel.svelte +21 -15
  211. package/dist/phase-diagram/build-diagram.js +3 -3
  212. package/dist/phase-diagram/colors.js +1 -1
  213. package/dist/phase-diagram/diagram-input.d.ts +1 -2
  214. package/dist/phase-diagram/parse.d.ts +2 -1
  215. package/dist/phase-diagram/parse.js +15 -15
  216. package/dist/phase-diagram/svg-to-diagram.js +21 -15
  217. package/dist/phase-diagram/utils.js +2 -3
  218. package/dist/plot/bar/BarPlot.svelte +494 -692
  219. package/dist/plot/bar/BarPlot.svelte.d.ts +9 -15
  220. package/dist/plot/bar/BarPlotControls.svelte +1 -3
  221. package/dist/plot/bar/BarPlotControls.svelte.d.ts +5 -7
  222. package/dist/plot/bar/SpacegroupBarPlot.svelte +31 -37
  223. package/dist/plot/bar/data.d.ts +5 -4
  224. package/dist/plot/bar/data.js +2 -1
  225. package/dist/plot/box/BoxPlot.svelte +361 -439
  226. package/dist/plot/box/BoxPlot.svelte.d.ts +6 -3
  227. package/dist/plot/box/BoxPlotControls.svelte.d.ts +1 -1
  228. package/dist/plot/box/Violin.svelte.d.ts +2 -1
  229. package/dist/plot/box/box-plot.d.ts +3 -2
  230. package/dist/plot/box/box-plot.js +6 -3
  231. package/dist/plot/box/kde.d.ts +2 -1
  232. package/dist/plot/box/kde.js +4 -4
  233. package/dist/plot/core/auto-place.d.ts +1 -1
  234. package/dist/plot/core/auto-place.js +5 -2
  235. package/dist/plot/core/axis-utils.d.ts +12 -7
  236. package/dist/plot/core/axis-utils.js +48 -48
  237. package/dist/plot/core/components/AxisLabel.svelte +82 -20
  238. package/dist/plot/core/components/AxisLabel.svelte.d.ts +1 -0
  239. package/dist/plot/core/components/ColorBar.svelte +106 -111
  240. package/dist/plot/core/components/ColorBar.svelte.d.ts +5 -4
  241. package/dist/plot/core/components/ColorScaleSelect.svelte +1 -1
  242. package/dist/plot/core/components/FillArea.svelte +10 -17
  243. package/dist/plot/core/components/HierarchyControls.svelte +249 -0
  244. package/dist/plot/{sunburst/SunburstControls.svelte.d.ts → core/components/HierarchyControls.svelte.d.ts} +12 -8
  245. package/dist/plot/core/components/InteractiveAxisLabel.svelte +1 -1
  246. package/dist/plot/core/components/InteractiveAxisLabel.svelte.d.ts +1 -1
  247. package/dist/plot/core/components/Line.svelte +21 -11
  248. package/dist/plot/core/components/Line.svelte.d.ts +5 -2
  249. package/dist/plot/core/components/PlotAxis.svelte +42 -18
  250. package/dist/plot/core/components/PlotAxis.svelte.d.ts +2 -1
  251. package/dist/plot/core/components/PlotControls.svelte +65 -41
  252. package/dist/plot/core/components/PlotControls.svelte.d.ts +1 -1
  253. package/dist/plot/core/components/PlotLegend.svelte +31 -35
  254. package/dist/plot/core/components/PlotMarginals.svelte +616 -0
  255. package/dist/plot/core/components/PlotMarginals.svelte.d.ts +15 -0
  256. package/dist/plot/core/components/PlotTooltip.svelte +1 -1
  257. package/dist/plot/core/components/PortalSelect.svelte +4 -8
  258. package/dist/plot/core/components/ReferenceLine.svelte +27 -24
  259. package/dist/plot/core/components/ReferenceLine3D.svelte +56 -59
  260. package/dist/plot/core/components/ReferenceLine3D.svelte.d.ts +4 -4
  261. package/dist/plot/core/components/ReferencePlane.svelte +9 -10
  262. package/dist/plot/core/components/ReferencePlane.svelte.d.ts +4 -4
  263. package/dist/plot/core/components/ZeroLines.svelte +4 -8
  264. package/dist/plot/core/components/ZoomRect.svelte +1 -2
  265. package/dist/plot/core/components/index.d.ts +1 -0
  266. package/dist/plot/core/components/index.js +1 -0
  267. package/dist/plot/core/data-cleaning-signal.d.ts +46 -0
  268. package/dist/plot/core/data-cleaning-signal.js +467 -0
  269. package/dist/plot/core/data-cleaning.d.ts +2 -42
  270. package/dist/plot/core/data-cleaning.js +29 -476
  271. package/dist/plot/core/data-transform.js +9 -5
  272. package/dist/plot/core/fill-utils.d.ts +7 -4
  273. package/dist/plot/core/fill-utils.js +34 -9
  274. package/dist/plot/core/index.d.ts +1 -0
  275. package/dist/plot/core/index.js +1 -0
  276. package/dist/plot/core/interactions.d.ts +5 -1
  277. package/dist/plot/core/interactions.js +14 -2
  278. package/dist/plot/core/layout.d.ts +3 -1
  279. package/dist/plot/core/layout.js +22 -13
  280. package/dist/plot/core/marginals.d.ts +170 -0
  281. package/dist/plot/core/marginals.js +436 -0
  282. package/dist/plot/core/pan-zoom.svelte.d.ts +35 -0
  283. package/dist/plot/core/pan-zoom.svelte.js +221 -0
  284. package/dist/plot/core/placed-tween.svelte.d.ts +21 -0
  285. package/dist/plot/core/placed-tween.svelte.js +68 -0
  286. package/dist/plot/core/reference-line.d.ts +10 -10
  287. package/dist/plot/core/reference-line.js +7 -7
  288. package/dist/plot/core/scales.d.ts +40 -25
  289. package/dist/plot/core/scales.js +49 -21
  290. package/dist/plot/core/svg.d.ts +2 -1
  291. package/dist/plot/core/types/fills.d.ts +101 -0
  292. package/dist/plot/core/types/fills.js +15 -0
  293. package/dist/plot/core/types/plot-3d.d.ts +87 -0
  294. package/dist/plot/core/types/plot-3d.js +4 -0
  295. package/dist/plot/core/types/reference-lines.d.ts +140 -0
  296. package/dist/plot/core/types/reference-lines.js +9 -0
  297. package/dist/plot/core/types.d.ts +47 -331
  298. package/dist/plot/core/types.js +23 -20
  299. package/dist/plot/core/utils/hierarchy-chart.d.ts +51 -0
  300. package/dist/plot/core/utils/hierarchy-chart.js +179 -0
  301. package/dist/plot/core/utils/hierarchy-labels.d.ts +20 -0
  302. package/dist/plot/core/utils/hierarchy-labels.js +99 -0
  303. package/dist/plot/core/utils/label-placement.d.ts +1 -1
  304. package/dist/plot/core/utils/label-placement.js +5 -5
  305. package/dist/plot/core/utils.d.ts +2 -1
  306. package/dist/plot/histogram/Histogram.svelte +373 -631
  307. package/dist/plot/histogram/Histogram.svelte.d.ts +7 -7
  308. package/dist/plot/histogram/HistogramControls.svelte +30 -28
  309. package/dist/plot/histogram/HistogramControls.svelte.d.ts +1 -1
  310. package/dist/plot/histogram/histogram.d.ts +31 -0
  311. package/dist/plot/histogram/histogram.js +70 -0
  312. package/dist/plot/histogram/index.d.ts +1 -0
  313. package/dist/plot/histogram/index.js +1 -0
  314. package/dist/plot/index.d.ts +1 -0
  315. package/dist/plot/index.js +1 -0
  316. package/dist/plot/sankey/Sankey.svelte +79 -73
  317. package/dist/plot/sankey/Sankey.svelte.d.ts +3 -3
  318. package/dist/plot/sankey/SankeyControls.svelte +22 -16
  319. package/dist/plot/sankey/sankey.js +3 -1
  320. package/dist/plot/scatter/BinnedScatterPlot.svelte +264 -112
  321. package/dist/plot/scatter/BinnedScatterPlot.svelte.d.ts +11 -4
  322. package/dist/plot/scatter/ElementScatter.svelte +4 -1
  323. package/dist/plot/scatter/ScatterPlot.svelte +623 -821
  324. package/dist/plot/scatter/ScatterPlot.svelte.d.ts +9 -15
  325. package/dist/plot/scatter/ScatterPlotControls.svelte +18 -19
  326. package/dist/plot/scatter/ScatterPlotControls.svelte.d.ts +1 -1
  327. package/dist/plot/scatter/ScatterPoint.svelte +16 -9
  328. package/dist/plot/scatter/binned-scatter-types.d.ts +6 -12
  329. package/dist/plot/scatter/binned-scatter-types.js +6 -1
  330. package/dist/plot/scatter/index.d.ts +1 -1
  331. package/dist/plot/scatter/scatter-data.d.ts +1 -1
  332. package/dist/plot/scatter/scatter-data.js +8 -7
  333. package/dist/plot/scatter-3d/ScatterPlot3D.svelte +40 -60
  334. package/dist/plot/scatter-3d/ScatterPlot3D.svelte.d.ts +6 -14
  335. package/dist/plot/scatter-3d/ScatterPlot3DControls.svelte +17 -15
  336. package/dist/plot/scatter-3d/ScatterPlot3DControls.svelte.d.ts +5 -5
  337. package/dist/plot/scatter-3d/ScatterPlot3DScene.svelte +232 -234
  338. package/dist/plot/scatter-3d/ScatterPlot3DScene.svelte.d.ts +5 -14
  339. package/dist/plot/scatter-3d/Surface3D.svelte +9 -17
  340. package/dist/plot/scatter-3d/Surface3D.svelte.d.ts +4 -3
  341. package/dist/plot/sunburst/Sunburst.svelte +256 -338
  342. package/dist/plot/sunburst/Sunburst.svelte.d.ts +4 -3
  343. package/dist/plot/sunburst/index.d.ts +1 -1
  344. package/dist/plot/sunburst/index.js +2 -1
  345. package/dist/plot/sunburst/render.d.ts +2 -2
  346. package/dist/plot/sunburst/render.js +47 -22
  347. package/dist/plot/sunburst/sunburst.d.ts +5 -1
  348. package/dist/plot/sunburst/sunburst.js +14 -4
  349. package/dist/plot/treemap/Treemap.svelte +907 -0
  350. package/dist/plot/treemap/Treemap.svelte.d.ts +84 -0
  351. package/dist/plot/treemap/index.d.ts +3 -0
  352. package/dist/plot/treemap/index.js +4 -0
  353. package/dist/plot/treemap/treemap.d.ts +26 -0
  354. package/dist/plot/treemap/treemap.js +99 -0
  355. package/dist/rdf/RdfPlot.svelte +22 -26
  356. package/dist/rdf/RdfPlot.svelte.d.ts +1 -1
  357. package/dist/rdf/calc-rdf.js +4 -8
  358. package/dist/sanitize.js +14 -3
  359. package/dist/scene/SceneCamera.svelte +62 -0
  360. package/dist/scene/SceneCamera.svelte.d.ts +19 -0
  361. package/dist/scene/bind-renderer.svelte.d.ts +2 -0
  362. package/dist/scene/bind-renderer.svelte.js +22 -0
  363. package/dist/scene/index.d.ts +4 -0
  364. package/dist/scene/index.js +5 -0
  365. package/dist/scene/props.js +52 -0
  366. package/dist/scene/types.d.ts +26 -0
  367. package/dist/scene/types.js +1 -0
  368. package/dist/settings.d.ts +33 -11
  369. package/dist/settings.js +136 -51
  370. package/dist/spectral/Bands.svelte +231 -237
  371. package/dist/spectral/Bands.svelte.d.ts +3 -2
  372. package/dist/spectral/BandsAndDos.svelte +29 -29
  373. package/dist/spectral/BrillouinBandsDos.svelte +21 -29
  374. package/dist/spectral/Dos.svelte +75 -77
  375. package/dist/spectral/Dos.svelte.d.ts +2 -1
  376. package/dist/spectral/helpers.d.ts +9 -9
  377. package/dist/spectral/helpers.js +66 -70
  378. package/dist/state.svelte.d.ts +0 -7
  379. package/dist/state.svelte.js +5 -24
  380. package/dist/structure/Arrow.svelte +6 -21
  381. package/dist/structure/ArrowInstances.svelte +160 -0
  382. package/dist/structure/ArrowInstances.svelte.d.ts +16 -0
  383. package/dist/structure/AtomLegend.svelte +115 -75
  384. package/dist/structure/AtomLegend.svelte.d.ts +1 -1
  385. package/dist/structure/Bond.svelte +31 -23
  386. package/dist/structure/CanvasTooltip.svelte +16 -3
  387. package/dist/structure/CellSelect.svelte +22 -29
  388. package/dist/structure/CellSelect.svelte.d.ts +2 -1
  389. package/dist/structure/Cylinder.svelte +24 -40
  390. package/dist/structure/InstancedAtoms.svelte +114 -0
  391. package/dist/structure/InstancedAtoms.svelte.d.ts +15 -0
  392. package/dist/structure/Lattice.svelte +85 -149
  393. package/dist/structure/SiteLabels.svelte +122 -0
  394. package/dist/structure/SiteLabels.svelte.d.ts +18 -0
  395. package/dist/structure/Structure.svelte +725 -735
  396. package/dist/structure/Structure.svelte.d.ts +8 -16
  397. package/dist/structure/StructureCarousel.svelte +630 -0
  398. package/dist/structure/StructureCarousel.svelte.d.ts +19 -0
  399. package/dist/structure/StructureControls.svelte +298 -325
  400. package/dist/structure/StructureControls.svelte.d.ts +5 -3
  401. package/dist/structure/StructureExportPane.svelte +62 -168
  402. package/dist/structure/StructureExportPane.svelte.d.ts +4 -5
  403. package/dist/structure/StructureInfoPane.svelte +77 -69
  404. package/dist/structure/StructureInfoPane.svelte.d.ts +6 -6
  405. package/dist/structure/StructureScene.svelte +758 -600
  406. package/dist/structure/StructureScene.svelte.d.ts +24 -20
  407. package/dist/structure/StructureViewport.svelte +386 -0
  408. package/dist/structure/StructureViewport.svelte.d.ts +63 -0
  409. package/dist/structure/atom-label-placement.d.ts +18 -0
  410. package/dist/structure/atom-label-placement.js +115 -0
  411. package/dist/structure/atom-properties.d.ts +3 -1
  412. package/dist/structure/atom-properties.js +115 -54
  413. package/dist/structure/bond-order-perception.js +2 -4
  414. package/dist/structure/bonding.d.ts +12 -2
  415. package/dist/structure/bonding.js +172 -73
  416. package/dist/structure/edit-history.d.ts +6 -0
  417. package/dist/structure/edit-history.js +27 -0
  418. package/dist/structure/export.d.ts +25 -4
  419. package/dist/structure/export.js +103 -153
  420. package/dist/structure/format-detect.d.ts +1 -0
  421. package/dist/structure/format-detect.js +22 -0
  422. package/dist/structure/geometry.d.ts +9 -0
  423. package/dist/structure/geometry.js +27 -0
  424. package/dist/structure/index.d.ts +18 -1
  425. package/dist/structure/index.js +15 -2
  426. package/dist/structure/measure.d.ts +1 -1
  427. package/dist/structure/measure.js +6 -12
  428. package/dist/structure/parse.d.ts +5 -3
  429. package/dist/structure/parse.js +551 -567
  430. package/dist/structure/partial-occupancy.d.ts +0 -1
  431. package/dist/structure/partial-occupancy.js +5 -3
  432. package/dist/structure/pbc.d.ts +1 -1
  433. package/dist/structure/pbc.js +204 -51
  434. package/dist/structure/polyhedra.d.ts +40 -0
  435. package/dist/structure/polyhedra.js +587 -0
  436. package/dist/structure/site.d.ts +4 -0
  437. package/dist/structure/site.js +1 -0
  438. package/dist/structure/supercell.d.ts +1 -2
  439. package/dist/structure/supercell.js +10 -15
  440. package/dist/structure/validation.js +5 -6
  441. package/dist/symmetry/SymmetryElementControls.svelte +69 -0
  442. package/dist/symmetry/SymmetryElementControls.svelte.d.ts +9 -0
  443. package/dist/symmetry/SymmetryElements.svelte +350 -0
  444. package/dist/symmetry/SymmetryElements.svelte.d.ts +24 -0
  445. package/dist/symmetry/SymmetryStats.svelte +142 -49
  446. package/dist/symmetry/WyckoffTable.svelte +58 -16
  447. package/dist/symmetry/WyckoffTable.svelte.d.ts +3 -0
  448. package/dist/symmetry/cell-transform.js +8 -14
  449. package/dist/symmetry/index.d.ts +15 -4
  450. package/dist/symmetry/index.js +308 -81
  451. package/dist/symmetry/spacegroups.d.ts +5 -1
  452. package/dist/symmetry/spacegroups.js +18 -3
  453. package/dist/symmetry/symmetry-elements.d.ts +33 -0
  454. package/dist/symmetry/symmetry-elements.js +521 -0
  455. package/dist/symmetry/wyckoff-db.d.ts +9 -0
  456. package/dist/symmetry/wyckoff-db.js +87 -0
  457. package/dist/table/HeatmapTable.svelte +1104 -378
  458. package/dist/table/HeatmapTable.svelte.d.ts +12 -2
  459. package/dist/table/ToggleMenu.svelte +5 -12
  460. package/dist/table/index.d.ts +21 -6
  461. package/dist/table/index.js +34 -21
  462. package/dist/theme/ThemeControl.svelte +16 -8
  463. package/dist/theme/index.d.ts +1 -5
  464. package/dist/theme/index.js +4 -10
  465. package/dist/theme/themes.mjs +14 -7
  466. package/dist/tooltip/KCoords.svelte +4 -1
  467. package/dist/tooltip/TooltipContent.svelte +2 -5
  468. package/dist/trajectory/Trajectory.svelte +556 -352
  469. package/dist/trajectory/Trajectory.svelte.d.ts +10 -22
  470. package/dist/trajectory/TrajectoryError.svelte +11 -8
  471. package/dist/trajectory/TrajectoryExportPane.svelte +28 -54
  472. package/dist/trajectory/TrajectoryExportPane.svelte.d.ts +4 -5
  473. package/dist/trajectory/TrajectoryInfoPane.svelte +88 -99
  474. package/dist/trajectory/TrajectoryInfoPane.svelte.d.ts +3 -2
  475. package/dist/trajectory/constants.js +6 -2
  476. package/dist/trajectory/extract.js +73 -71
  477. package/dist/trajectory/format-detect.d.ts +0 -1
  478. package/dist/trajectory/format-detect.js +13 -17
  479. package/dist/trajectory/frame-reader.d.ts +2 -1
  480. package/dist/trajectory/frame-reader.js +74 -136
  481. package/dist/trajectory/helpers.d.ts +12 -2
  482. package/dist/trajectory/helpers.js +68 -37
  483. package/dist/trajectory/index.d.ts +1 -0
  484. package/dist/trajectory/index.js +3 -0
  485. package/dist/trajectory/parse/ase.d.ts +9 -1
  486. package/dist/trajectory/parse/ase.js +47 -32
  487. package/dist/trajectory/parse/diagnostics.d.ts +3 -0
  488. package/dist/trajectory/parse/diagnostics.js +14 -0
  489. package/dist/trajectory/parse/h5-utils.d.ts +13 -0
  490. package/dist/trajectory/parse/h5-utils.js +109 -0
  491. package/dist/trajectory/parse/hdf5.d.ts +1 -1
  492. package/dist/trajectory/parse/hdf5.js +116 -106
  493. package/dist/trajectory/parse/index.d.ts +3 -4
  494. package/dist/trajectory/parse/index.js +68 -119
  495. package/dist/trajectory/parse/lammps.d.ts +0 -2
  496. package/dist/trajectory/parse/lammps.js +14 -16
  497. package/dist/trajectory/parse/pymatgen.d.ts +2 -0
  498. package/dist/trajectory/parse/pymatgen.js +74 -0
  499. package/dist/trajectory/parse/vasp.js +13 -9
  500. package/dist/trajectory/parse/vaspout-electronic.d.ts +13 -0
  501. package/dist/trajectory/parse/vaspout-electronic.js +207 -0
  502. package/dist/trajectory/parse/vaspout-h5.d.ts +5 -0
  503. package/dist/trajectory/parse/vaspout-h5.js +284 -0
  504. package/dist/trajectory/parse/xyz.d.ts +9 -21
  505. package/dist/trajectory/parse/xyz.js +38 -39
  506. package/dist/trajectory/plotting.d.ts +8 -10
  507. package/dist/trajectory/plotting.js +102 -127
  508. package/dist/utils.d.ts +4 -0
  509. package/dist/utils.js +8 -1
  510. package/dist/xrd/XrdPlot.svelte +56 -85
  511. package/dist/xrd/broadening.d.ts +2 -1
  512. package/dist/xrd/broadening.js +2 -1
  513. package/dist/xrd/calc-xrd.js +8 -15
  514. package/dist/xrd/index.d.ts +2 -2
  515. package/dist/xrd/parse.d.ts +0 -1
  516. package/dist/xrd/parse.js +56 -73
  517. package/package.json +77 -36
  518. package/dist/api/mp.d.ts +0 -6
  519. package/dist/api/mp.js +0 -22
  520. package/dist/element/data.json +0 -11864
  521. package/dist/fermi-surface/marching-cubes.d.ts +0 -2
  522. package/dist/fermi-surface/marching-cubes.js +0 -2
  523. package/dist/plot/core/hover-lock.svelte.d.ts +0 -14
  524. package/dist/plot/core/hover-lock.svelte.js +0 -45
  525. package/dist/plot/sunburst/SunburstControls.svelte +0 -200
  526. package/dist/structure/label-placement.d.ts +0 -14
  527. package/dist/structure/label-placement.js +0 -72
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
2
  import type { D3InterpolateName } from '../colors'
3
+ import { clamp01 } from '../utils'
3
4
  import {
4
5
  add_alpha,
5
6
  AXIS_COLORS,
@@ -13,17 +14,16 @@
13
14
  import type { FormulaLabelSegment } from '../composition/format'
14
15
  import { normalize_show_controls } from '../controls'
15
16
  import { sanitize_html } from '../sanitize'
16
- import { ClickFeedback, DragOverlay, Spinner } from '../feedback'
17
- import Icon from '../Icon.svelte'
17
+ import { Spinner } from '../feedback'
18
18
  import { format_num } from '../labels'
19
+ import { to_radians, type Point3D, type Vec2, type Vec3 } from '../math'
20
+ import { ColorBar } from '../plot'
19
21
  import {
20
- set_fullscreen_bg,
21
- setup_fullscreen_effect,
22
- toggle_fullscreen,
23
- } from '../layout'
24
- import { to_radians, type Point3D, type Vec3 } from '../math'
25
- import { ColorBar, PlotTooltip } from '../plot'
26
- import { centered_rect, pad_rect, rects_overlap, rect_within_rect } from '../plot/core/layout'
22
+ centered_rect,
23
+ pad_rect,
24
+ rects_overlap,
25
+ rect_within_rect,
26
+ } from '../plot/core/layout'
27
27
  import type { Rect } from '../plot/core/layout'
28
28
  import { create_pulse_animation } from '../effects.svelte'
29
29
  import { DEFAULTS } from '../settings'
@@ -40,39 +40,41 @@
40
40
  get_triangle_vertical_edges,
41
41
  TRIANGLE_VERTICES,
42
42
  } from './barycentric-coords'
43
- import ConvexHullControls from './ConvexHullControls.svelte'
44
- import ConvexHullInfoPane from './ConvexHullInfoPane.svelte'
45
- import ConvexHullTooltip from './ConvexHullTooltip.svelte'
43
+ import { create_canvas_interactions } from './canvas-interactions.svelte'
44
+ import ConvexHullChrome from './ConvexHullChrome.svelte'
46
45
  import GasPressureControls from './GasPressureControls.svelte'
47
46
  import * as helpers from './helpers'
47
+ import { create_hull_data_pipeline } from './hull-state.svelte'
48
48
  import type { BaseConvexHullProps, Hull3DProps } from './index'
49
49
  import { CONVEX_HULL_STYLE, default_controls, default_hull_config } from './index'
50
- import StructurePopup from './StructurePopup.svelte'
51
50
  import TemperatureSlider from './TemperatureSlider.svelte'
52
51
  import * as thermo from './thermodynamics'
53
52
  import type {
54
53
  ConvexHullEntry,
55
54
  ConvexHullTriangle,
56
55
  HighlightStyle,
57
- HoverData3D,
58
56
  HullFaceColorMode,
59
57
  LabelPlacement,
60
58
  } from './types'
61
59
  import { compute_hull_stability } from './helpers'
60
+ import { MAGNETIC_ORDERING_CATEGORY } from './types'
62
61
 
63
62
  let {
64
63
  entries = [],
65
64
  controls = {},
66
65
  config = {},
66
+ show_controls,
67
67
  on_point_click,
68
68
  on_point_hover,
69
69
  fullscreen = $bindable(DEFAULTS.convex_hull.ternary.fullscreen),
70
- enable_fullscreen = true,
70
+ fullscreen_toggle = true,
71
71
  enable_info_pane = true,
72
72
  wrapper = $bindable(),
73
73
  label_threshold = 50,
74
74
  show_stable = $bindable(DEFAULTS.convex_hull.ternary.show_stable),
75
75
  show_unstable = $bindable(DEFAULTS.convex_hull.ternary.show_unstable),
76
+ entry_category = MAGNETIC_ORDERING_CATEGORY,
77
+ hidden_categories = $bindable([]),
76
78
  show_hull_faces = $bindable(DEFAULTS.convex_hull.ternary.show_hull_faces),
77
79
  hull_face_opacity = $bindable(DEFAULTS.convex_hull.ternary.hull_face_opacity),
78
80
  hull_face_color_mode = $bindable(
@@ -80,11 +82,9 @@
80
82
  ),
81
83
  element_colors = vesta_hex,
82
84
  color_mode = $bindable(DEFAULTS.convex_hull.ternary.color_mode),
83
- color_scale = $bindable(
84
- DEFAULTS.convex_hull.ternary.color_scale as D3InterpolateName,
85
- ),
85
+ color_scale = $bindable(DEFAULTS.convex_hull.ternary.color_scale as D3InterpolateName),
86
86
  info_pane_open = $bindable(DEFAULTS.convex_hull.ternary.info_pane_open),
87
- legend_pane_open = $bindable(DEFAULTS.convex_hull.ternary.legend_pane_open),
87
+ controls_open = $bindable(DEFAULTS.convex_hull.ternary.legend_pane_open),
88
88
  max_hull_dist_show_phases = $bindable(
89
89
  DEFAULTS.convex_hull.ternary.max_hull_dist_show_phases,
90
90
  ),
@@ -92,9 +92,7 @@
92
92
  DEFAULTS.convex_hull.ternary.max_hull_dist_show_labels,
93
93
  ),
94
94
  show_stable_labels = $bindable(DEFAULTS.convex_hull.ternary.show_stable_labels),
95
- show_unstable_labels = $bindable(
96
- DEFAULTS.convex_hull.ternary.show_unstable_labels,
97
- ),
95
+ show_unstable_labels = $bindable(DEFAULTS.convex_hull.ternary.show_unstable_labels),
98
96
  on_file_drop,
99
97
  enable_click_selection = true,
100
98
  enable_structure_preview = true,
@@ -114,12 +112,13 @@
114
112
  children,
115
113
  tooltip,
116
114
  ...rest
117
- }: BaseConvexHullProps<ConvexHullEntry> & Hull3DProps & {
118
- highlight_style?: HighlightStyle
119
- } = $props()
115
+ }: BaseConvexHullProps<ConvexHullEntry> &
116
+ Hull3DProps & {
117
+ highlight_style?: HighlightStyle
118
+ } = $props()
120
119
 
121
120
  const merged_controls = $derived({ ...default_controls, ...controls })
122
- const controls_config = $derived(normalize_show_controls(merged_controls.show))
121
+ const controls_config = $derived(normalize_show_controls(show_controls))
123
122
  const merged_config = $derived({
124
123
  ...default_hull_config,
125
124
  ...config,
@@ -127,100 +126,43 @@
127
126
  margin: { t: 40, r: 40, b: 60, l: 60, ...config.margin },
128
127
  })
129
128
 
130
- // Temperature-dependent free energy support
131
- const { has_temp_data, available_temperatures } = $derived(
132
- helpers.analyze_temperature_data(entries),
133
- )
134
-
135
- // Initialize or reset temperature when it's undefined or no longer valid
136
- $effect(() => {
137
- if (
138
- has_temp_data &&
139
- available_temperatures.length > 0 &&
140
- (temperature === undefined || !available_temperatures.includes(temperature))
141
- ) temperature = available_temperatures[0]
142
- })
143
-
144
- // Filter entries by temperature when in temperature mode
145
- const temp_filtered_entries = $derived(
146
- has_temp_data && temperature !== undefined
147
- ? helpers.filter_entries_at_temperature(entries, temperature, {
148
- interpolate: interpolate_temperature,
149
- max_interpolation_gap,
150
- })
151
- : entries,
152
- )
153
-
154
- // Gas-dependent chemical potential support (corrections based on T, P)
155
- // Default to DEFAULT_GAS_TEMP (room temperature) when no temperature specified
156
- const {
157
- entries: gas_corrected_entries,
158
- analysis: gas_analysis,
159
- merged_config: merged_gas_config,
160
- } = $derived(
161
- helpers.get_gas_corrected_entries(
162
- temp_filtered_entries,
163
- gas_config,
164
- gas_pressures,
165
- temperature ?? helpers.DEFAULT_GAS_TEMP,
166
- ),
167
- )
168
-
169
- let { // Compute energy mode information
170
- has_precomputed_e_form,
171
- has_precomputed_hull,
172
- can_compute_e_form,
173
- can_compute_hull,
174
- energy_mode,
175
- unary_refs,
176
- } = $derived(
177
- helpers.compute_energy_mode_info(
178
- gas_corrected_entries,
179
- thermo.find_lowest_energy_unary_refs,
180
- energy_source_mode,
181
- ),
182
- )
183
-
184
- const effective_entries = $derived(
185
- helpers.get_effective_entries(
186
- gas_corrected_entries,
187
- energy_mode,
188
- unary_refs,
189
- thermo.compute_e_form_per_atom,
190
- ),
191
- )
192
-
193
- // Process convex hull data with unified PhaseData interface using effective entries
194
- const pd_data = $derived(thermo.process_hull_entries(effective_entries))
195
-
196
- // Pre-compute polymorph stats once for O(1) tooltip lookups
197
- const polymorph_stats_map = $derived(
198
- helpers.compute_all_polymorph_stats(effective_entries),
199
- )
200
-
201
- const elements = $derived.by(() => {
202
- if (pd_data.elements.length > 3) {
203
- console.error(
204
- `ConvexHull3D: Dataset contains ${pd_data.elements.length} elements, but ternary diagrams require exactly 3. Found: [${
205
- pd_data.elements.join(`, `)
206
- }]`,
207
- )
208
- return []
209
- }
210
-
211
- return pd_data.elements
129
+ // Shared reactive data pipeline (temperature → gas → energy mode → hull data → threshold)
130
+ // Explicit generic breaks the circular type inference through the all_enriched_entries thunk
131
+ const hull_data = create_hull_data_pipeline<ConvexHullEntry>({
132
+ dim: 3,
133
+ entries: () => entries,
134
+ temperature: () => temperature,
135
+ interpolate_temperature: () => interpolate_temperature,
136
+ max_interpolation_gap: () => max_interpolation_gap,
137
+ gas_config: () => gas_config,
138
+ gas_pressures: () => gas_pressures,
139
+ energy_source_mode: () => energy_source_mode,
140
+ all_enriched_entries: () => all_enriched_entries,
141
+ max_hull_dist_show_phases: () => max_hull_dist_show_phases,
142
+ show_stable: () => show_stable,
143
+ show_unstable: () => show_unstable,
144
+ entry_category: () => entry_category,
145
+ hidden_categories: () => hidden_categories,
146
+ keep_plot_entry: helpers.entry_within_hull_dist,
147
+ set_temperature: (next_temp) => (temperature = next_temp),
148
+ set_max_hull_dist_show_phases: (value) => (max_hull_dist_show_phases = value),
149
+ set_stable_entries: (value) => (stable_entries = value),
150
+ set_unstable_entries: (value) => (unstable_entries = value),
212
151
  })
152
+ const has_temp_data = $derived(hull_data.has_temp_data)
153
+ const gas_analysis = $derived(hull_data.gas_analysis)
154
+ const merged_gas_config = $derived(hull_data.merged_gas_config)
155
+ const pd_data = $derived(hull_data.pd_data)
156
+ const elements = $derived(hull_data.elements)
157
+ const plot_entries = $derived(hull_data.plot_entries)
158
+ const visible_entries = $derived(hull_data.visible_entries)
213
159
 
214
160
  // 1) Raw 3D coordinates (formation-energy z), independent of hull state
215
161
  const coords_entries = $derived.by(() => {
216
162
  if (elements.length !== 3) return []
217
163
  try {
218
164
  // Pass precomputed el_refs to avoid recomputing in error diagnostics
219
- const coords = get_ternary_3d_coordinates(
220
- pd_data.entries,
221
- elements,
222
- pd_data.el_refs,
223
- )
165
+ const coords = get_ternary_3d_coordinates(pd_data.entries, elements, pd_data.el_refs)
224
166
  return coords
225
167
  } catch (error) {
226
168
  console.error(`Error computing ternary coordinates:`, error)
@@ -250,59 +192,20 @@
250
192
  // Enrich coords with e_above_hull from cached hull model (before filtering)
251
193
  const all_enriched_entries = $derived.by(() => {
252
194
  if (coords_entries.length === 0) return []
253
- if (energy_mode !== `on-the-fly`) return coords_entries
195
+ if (hull_data.energy_mode !== `on-the-fly`) return coords_entries
254
196
  const pts = coords_entries.map(({ x, y, z }) => ({ x, y, z }))
255
197
  const raw_dists = thermo.compute_e_above_hull_for_points(pts, hull_model)
198
+ // non-finite distance (no covering hull face) -> unknown, handled by compute_hull_stability
256
199
  return coords_entries.map((entry, idx) => ({
257
- ...entry, ...compute_hull_stability(raw_dists[idx], entry.exclude_from_hull),
200
+ ...entry,
201
+ ...compute_hull_stability(raw_dists[idx], entry.exclude_from_hull),
258
202
  }))
259
203
  })
260
204
 
261
- // Auto threshold: show all for few entries, use default for many, interpolate between
262
- const max_hull_dist_in_data = $derived(
263
- helpers.calc_max_hull_dist_in_data(all_enriched_entries),
264
- )
265
- const auto_default_threshold = $derived(helpers.compute_auto_hull_dist_threshold(
266
- all_enriched_entries.length,
267
- max_hull_dist_in_data,
268
- DEFAULTS.convex_hull.ternary.max_hull_dist_show_phases,
269
- ))
270
-
271
- const next_auto_threshold = helpers.auto_threshold_reset(
272
- DEFAULTS.convex_hull.ternary.max_hull_dist_show_phases,
273
- )
274
- $effect(() => {
275
- max_hull_dist_show_phases = next_auto_threshold(
276
- entries,
277
- max_hull_dist_show_phases,
278
- auto_default_threshold,
279
- ) ?? max_hull_dist_show_phases
280
- })
281
-
282
- // Filter by threshold; visibility is a view predicate, not entry state.
283
- const plot_entries = $derived(
284
- all_enriched_entries.filter((entry) =>
285
- (entry.e_above_hull ?? 0) <= max_hull_dist_show_phases
286
- ),
287
- )
288
- const visible_entries = $derived(helpers.visible_entries(
289
- plot_entries,
290
- show_stable,
291
- show_unstable,
292
- ))
293
-
294
- $effect(() => {
295
- stable_entries = plot_entries.filter(helpers.entry_is_stable)
296
- unstable_entries = plot_entries.filter(helpers.entry_is_unstable)
297
- })
298
-
299
205
  // Canvas rendering
300
206
  let canvas: HTMLCanvasElement | undefined = undefined
301
207
  let ctx: CanvasRenderingContext2D | null = null
302
208
 
303
- // Performance optimization
304
- let frame_id = 0
305
-
306
209
  const camera_default = {
307
210
  elevation: DEFAULTS.convex_hull.ternary.camera_elevation,
308
211
  azimuth: DEFAULTS.convex_hull.ternary.camera_azimuth,
@@ -322,23 +225,11 @@
322
225
  let gizmo_active = $state(false)
323
226
 
324
227
  // Convert elevation/azimuth (degrees) to Three.js camera position + up vector.
325
- function gizmo_camera(
326
- elev_deg: number,
327
- azim_deg: number,
328
- ): { position: Vec3; up: Vec3 } {
228
+ function gizmo_camera(elev_deg: number, azim_deg: number): { position: Vec3; up: Vec3 } {
329
229
  const [elev, azim] = [to_radians(elev_deg), to_radians(azim_deg)]
330
- const [se, ce, sa, ca] = [
331
- Math.sin(elev),
332
- Math.cos(elev),
333
- Math.sin(azim),
334
- Math.cos(azim),
335
- ]
230
+ const [se, ce, sa, ca] = [Math.sin(elev), Math.cos(elev), Math.sin(azim), Math.cos(azim)]
336
231
  return {
337
- position: [
338
- -sa * se * GIZMO_CAM_DIST,
339
- -ca * se * GIZMO_CAM_DIST,
340
- ce * GIZMO_CAM_DIST,
341
- ],
232
+ position: [-sa * se * GIZMO_CAM_DIST, -ca * se * GIZMO_CAM_DIST, ce * GIZMO_CAM_DIST],
342
233
  up: [sa * ce, ca * ce, se],
343
234
  }
344
235
  }
@@ -354,7 +245,7 @@
354
245
  camera.center_x = 0
355
246
  // 0.6 matches the draw_data_points() scale factor that maps data coords to canvas pixels
356
247
  const scale = Math.min(canvas_dims.width, canvas_dims.height) * 0.6 * camera.zoom
357
- camera.center_y = Math.sqrt(3) / 12 * scale * Math.cos(to_radians(elev_deg))
248
+ camera.center_y = (Math.sqrt(3) / 12) * scale * Math.cos(to_radians(elev_deg))
358
249
  }
359
250
 
360
251
  // Sync: ConvexHull3D → Three.js gizmo camera (on main canvas drag)
@@ -374,14 +265,15 @@
374
265
  const cam = gizmo_cam_ref
375
266
  if (!cam) return
376
267
  const { x: cx, y: cy, z: cz } = cam.position
377
- const dist = Math.sqrt(cx * cx + cy * cy + cz * cz)
268
+ const dist = Math.hypot(cx, cy, cz)
378
269
  if (dist < 1e-6) return
379
270
  const elev_rad = Math.acos(Math.max(-1, Math.min(1, cz / dist)))
380
271
  const sin_elev = Math.sin(elev_rad)
381
- const azim_deg = Math.abs(sin_elev) > 1e-6
382
- ? Math.atan2(-cx / (dist * sin_elev), -cy / (dist * sin_elev)) * 180 / Math.PI
383
- : 0
384
- const elev_deg = elev_rad * 180 / Math.PI
272
+ const azim_deg =
273
+ Math.abs(sin_elev) > 1e-6
274
+ ? (Math.atan2(-cx / (dist * sin_elev), -cy / (dist * sin_elev)) * 180) / Math.PI
275
+ : 0
276
+ const elev_deg = (elev_rad * 180) / Math.PI
385
277
  camera.elevation = elev_deg
386
278
  camera.azimuth = azim_deg
387
279
  center_camera(elev_deg)
@@ -389,14 +281,15 @@
389
281
 
390
282
  // Gizmo axis colors (constant — AXIS_COLORS/NEG_AXIS_COLORS never change)
391
283
  const gizmo_axis_options = Object.fromEntries(
392
- [...AXIS_COLORS, ...NEG_AXIS_COLORS].map((
393
- [axis, color, hover_color],
394
- ) => [axis, {
395
- color,
396
- labelColor: `#111`,
397
- opacity: 0.85,
398
- hover: { color: hover_color, labelColor: `#222`, opacity: 1 },
399
- }]),
284
+ [...AXIS_COLORS, ...NEG_AXIS_COLORS].map(([axis, color, hover_color]) => [
285
+ axis,
286
+ {
287
+ color,
288
+ labelColor: `#111`,
289
+ opacity: 0.85,
290
+ hover: { color: hover_color, labelColor: `#222`, opacity: 1 },
291
+ },
292
+ ]),
400
293
  )
401
294
 
402
295
  // Extract placement from gizmo options (not a Threlte Gizmo prop)
@@ -418,43 +311,64 @@
418
311
  }
419
312
  })
420
313
 
421
- // Interaction state
422
- let is_dragging = $state(false)
423
- let drag_started = $state(false)
424
- let last_mouse = $state({ x: 0, y: 0 })
425
- let hover_data = $state.raw<HoverData3D<ConvexHullEntry> | null>(null)
426
- let copy_feedback = $state({ visible: false, position: { x: 0, y: 0 } })
427
-
428
- // Drag and drop state
429
- let drag_over = $state(false)
430
-
431
- // Structure popup state
432
- let modal_open = $state(false)
433
- let selected_structure = $state<AnyStructure | null>(null)
434
- let modal_place_right = $state(true)
435
- $effect(() => {
436
- const current_selection = helpers.current_entry(selected_entry, plot_entries)
437
- const stale_selection = selected_entry && !current_selection
438
- if (stale_selection) selected_entry = null
439
- else if (current_selection && !helpers.same_entry(current_selection, selected_entry)) {
440
- selected_entry = current_selection
441
- }
442
- const current_hover = helpers.current_entry(hover_data?.entry, plot_entries)
443
- if (hover_data?.entry && !current_hover) {
444
- hover_data = null
445
- on_point_hover?.(null)
446
- } else if (hover_data && current_hover && current_hover !== hover_data.entry) {
447
- hover_data = { ...hover_data, entry: current_hover }
448
- }
449
- if (modal_open) {
450
- const structure = current_selection && extract_structure_from_entry(current_selection)
451
- if (structure) selected_structure = structure
452
- else {
453
- modal_open = false
454
- selected_structure = null
314
+ // Shared canvas-interaction scaffold (mouse/keyboard handlers, hover/drag/popup
315
+ // state, canvas sizing, render scheduler). Rotation math + keydown actions stay local.
316
+ const interactions = create_canvas_interactions({
317
+ wheel_clamp: [0.5, 10],
318
+ fullscreen_bg_var: `--hull-3d-bg-fullscreen`,
319
+ canvas: () => canvas,
320
+ wrapper: () => wrapper,
321
+ ctx: () => ctx,
322
+ set_ctx: (context) => (ctx = context),
323
+ set_canvas_dims: (dims) => (canvas_dims = dims),
324
+ visible_entries: () => visible_entries,
325
+ plot_entries: () => plot_entries,
326
+ selected_entry: () => selected_entry,
327
+ set_selected_entry: (entry) => (selected_entry = entry),
328
+ fullscreen: () => fullscreen,
329
+ enable_click_selection: () => enable_click_selection,
330
+ enable_structure_preview: () => enable_structure_preview,
331
+ on_point_click: () => on_point_click,
332
+ on_point_hover: () => on_point_hover,
333
+ on_file_drop: () => on_file_drop,
334
+ entry_category: () => entry_category,
335
+ zoom: () => camera.zoom,
336
+ set_zoom: (zoom) => (camera.zoom = zoom),
337
+ project_point: project_3d_point,
338
+ extract_structure: extract_structure_from_entry,
339
+ render_frame,
340
+ on_drag: (dx, dy, panning) => {
341
+ if (panning) {
342
+ camera.center_x += dx
343
+ camera.center_y += dy
344
+ } else {
345
+ // Horizontal drag -> azimuth rotation around z-axis
346
+ camera.azimuth += dx * 0.3 // Positive dx (drag right) rotates clockwise
347
+ // Vertical drag -> elevation angle (full range)
348
+ camera.elevation -= dy * 0.3 // Positive dy (drag down) tilts view down
455
349
  }
456
- }
350
+ },
351
+ // Reset pan center when entering/exiting fullscreen
352
+ on_fullscreen_change: () => {
353
+ camera.center_x = 0
354
+ camera.center_y = -50
355
+ },
356
+ actions: () => ({
357
+ r: reset_camera,
358
+ t: () => {
359
+ camera.elevation = 0
360
+ camera.azimuth = 0
361
+ center_camera(0)
362
+ },
363
+ b: () => (color_mode = color_mode === `stability` ? `energy` : `stability`),
364
+ s: () => (show_stable = !show_stable),
365
+ u: () => (show_unstable = !show_unstable),
366
+ h: () => (show_hull_faces = !show_hull_faces),
367
+ l: () => (show_stable_labels = !show_stable_labels),
368
+ }),
457
369
  })
370
+ const { render_once } = interactions
371
+ const sorted_points_cache = $derived(interactions.sorted_points_cache)
458
372
 
459
373
  // Hull face color (customizable via controls)
460
374
  let hull_face_color = $state(`#4caf50`)
@@ -467,9 +381,7 @@
467
381
  let pulse_opacity = $derived(0.3 + 0.4 * pulse.unit)
468
382
 
469
383
  // Merge highlight style with defaults
470
- const merged_highlight_style = $derived(
471
- helpers.merge_highlight_style(highlight_style),
472
- )
384
+ const merged_highlight_style = $derived(helpers.merge_highlight_style(highlight_style))
473
385
 
474
386
  // Helper to check if entry is highlighted
475
387
  const is_highlighted = (entry: ConvexHullEntry): boolean =>
@@ -478,17 +390,14 @@
478
390
  // Re-render when important state changes
479
391
  $effect(() => {
480
392
  // oxfmt-ignore
481
- void [show_hull_faces, color_mode, color_scale, show_stable_labels, show_unstable_labels, max_hull_dist_show_labels, camera.elevation, camera.azimuth, camera.zoom, camera.center_x, camera.center_y, plot_entries, hull_face_color, hull_face_opacity, hull_face_color_mode, element_colors, highlighted_entries, text_color] // track reactively
393
+ void [show_hull_faces, color_mode, color_scale, show_stable_labels, show_unstable_labels, max_hull_dist_show_labels, camera.elevation, camera.azimuth, camera.zoom, camera.center_x, camera.center_y, plot_entries, visible_entries, hull_face_color, hull_face_opacity, hull_face_color_mode, element_colors, highlighted_entries, text_color] // track reactively
482
394
 
483
395
  render_once()
484
396
  })
485
397
 
486
- // Function to extract structure data from a convex hull entry
487
- function extract_structure_from_entry(
488
- entry: ConvexHullEntry,
489
- ): AnyStructure | null {
490
- const orig_entry = entries.find((ent) => ent.entry_id === entry.entry_id)
491
- return orig_entry?.structure as AnyStructure || null
398
+ // function (not const) so the create_canvas_interactions options above can reference it
399
+ function extract_structure_from_entry(entry: ConvexHullEntry): AnyStructure | null {
400
+ return helpers.extract_structure_from_entry(entries, entry)
492
401
  }
493
402
 
494
403
  const reset_camera = () => Object.assign(camera, camera_default)
@@ -496,16 +405,17 @@
496
405
  reset_camera()
497
406
  fullscreen = DEFAULTS.convex_hull.ternary.fullscreen
498
407
  info_pane_open = DEFAULTS.convex_hull.ternary.info_pane_open
499
- legend_pane_open = DEFAULTS.convex_hull.ternary.legend_pane_open
408
+ controls_open = DEFAULTS.convex_hull.ternary.legend_pane_open
500
409
  color_mode = DEFAULTS.convex_hull.ternary.color_mode
501
410
  color_scale = DEFAULTS.convex_hull.ternary.color_scale as D3InterpolateName
502
411
  show_stable = DEFAULTS.convex_hull.ternary.show_stable
503
412
  show_unstable = DEFAULTS.convex_hull.ternary.show_unstable
413
+ hidden_categories = []
504
414
  show_stable_labels = DEFAULTS.convex_hull.ternary.show_stable_labels
505
415
  show_unstable_labels = DEFAULTS.convex_hull.ternary.show_unstable_labels
506
416
  max_hull_dist_show_labels = DEFAULTS.convex_hull.ternary.max_hull_dist_show_labels
507
417
  // Use auto-computed threshold based on entry count instead of static default
508
- max_hull_dist_show_phases = auto_default_threshold
418
+ max_hull_dist_show_phases = hull_data.auto_default_threshold
509
419
  show_hull_faces = DEFAULTS.convex_hull.ternary.show_hull_faces
510
420
  hull_face_color = DEFAULTS.convex_hull.ternary.hull_face_color
511
421
  hull_face_opacity = DEFAULTS.convex_hull.ternary.hull_face_opacity
@@ -513,75 +423,6 @@
513
423
  .hull_face_color_mode as HullFaceColorMode
514
424
  }
515
425
 
516
- const handle_keydown = (event: KeyboardEvent) => {
517
- const target = event.target
518
- if (target instanceof HTMLElement && target.tagName.match(/INPUT|TEXTAREA/)) return
519
-
520
- // Stop propagation if event came from canvas to prevent wrapper's handler
521
- // from running again (both have onkeydown, causing duplicate handling)
522
- if (target === canvas) {
523
- event.stopPropagation()
524
- }
525
-
526
- if (event.key === `Escape` && modal_open) {
527
- close_structure_popup()
528
- return
529
- }
530
-
531
- // Handle Enter for keyboard accessibility - select hovered entry
532
- if (event.key === `Enter`) {
533
- const entry = hover_data?.entry
534
- if (entry) {
535
- on_point_click?.(entry)
536
- if (enable_click_selection) {
537
- selected_entry = entry
538
- if (enable_structure_preview) {
539
- const structure = extract_structure_from_entry(entry)
540
- if (structure) {
541
- selected_structure = structure
542
- modal_place_right = helpers.calculate_modal_side(wrapper)
543
- modal_open = true
544
- }
545
- }
546
- }
547
- } else if (modal_open) {
548
- close_structure_popup()
549
- }
550
- return
551
- }
552
-
553
- const actions: Record<string, () => void> = {
554
- r: reset_camera,
555
- t: () => {
556
- camera.elevation = 0
557
- camera.azimuth = 0
558
- center_camera(0)
559
- },
560
- b: () => color_mode = color_mode === `stability` ? `energy` : `stability`,
561
- s: () => show_stable = !show_stable,
562
- u: () => show_unstable = !show_unstable,
563
- h: () => show_hull_faces = !show_hull_faces,
564
- l: () => show_stable_labels = !show_stable_labels,
565
- }
566
- actions[event.key.toLowerCase()]?.()
567
- }
568
-
569
- async function handle_file_drop(event: DragEvent): Promise<void> {
570
- drag_over = false
571
- const data = await helpers.parse_hull_entries_from_drop(event)
572
- if (data) on_file_drop?.(data)
573
- }
574
-
575
- async function copy_entry_data(
576
- entry: ConvexHullEntry,
577
- position: { x: number; y: number },
578
- ) {
579
- await helpers.copy_entry_to_clipboard(entry, position, (visible, pos) => {
580
- copy_feedback.visible = visible
581
- copy_feedback.position = pos
582
- })
583
- }
584
-
585
426
  const get_point_color = (entry: ConvexHullEntry): string =>
586
427
  helpers.get_point_color_for_entry(
587
428
  entry,
@@ -592,7 +433,7 @@
592
433
 
593
434
  // Cache energy color scale per frame/setting
594
435
  const energy_color_scale = $derived.by(() =>
595
- helpers.get_energy_color_scale(color_mode, color_scale, plot_entries)
436
+ helpers.get_energy_color_scale(color_mode, color_scale, plot_entries),
596
437
  )
597
438
 
598
439
  // Convex hull statistics - compute internally and expose via bindable prop
@@ -608,10 +449,7 @@
608
449
  ): { x: number; y: number; depth: number } {
609
450
  if (!canvas) return { x: 0, y: 0, depth: 0 }
610
451
 
611
- const [elev, azim] = [
612
- (camera.elevation * Math.PI) / 180,
613
- (camera.azimuth * Math.PI) / 180,
614
- ]
452
+ const [elev, azim] = [(camera.elevation * Math.PI) / 180, (camera.azimuth * Math.PI) / 180]
615
453
  const [cos_az, sin_az, cos_el, sin_el] = [
616
454
  Math.cos(azim),
617
455
  Math.sin(azim),
@@ -666,10 +504,7 @@
666
504
  ctx.stroke()
667
505
 
668
506
  // Draw vertical edges from corners (from most negative to 0 formation energy)
669
- const vertical_edges = get_triangle_vertical_edges(
670
- e_form_min,
671
- e_form_max,
672
- )
507
+ const vertical_edges = get_triangle_vertical_edges(e_form_min, e_form_max)
673
508
  ctx.beginPath()
674
509
  for (const [v1, v2] of vertical_edges) {
675
510
  const proj1 = project_3d_point(v1.x, v1.y, v1.z)
@@ -710,15 +545,11 @@
710
545
  ctx.textAlign = `center`
711
546
  ctx.textBaseline = `middle`
712
547
 
713
- for (
714
- let idx = 0;
715
- idx < TRIANGLE_VERTICES.length && idx < elements.length;
716
- idx++
717
- ) {
548
+ for (let idx = 0; idx < TRIANGLE_VERTICES.length && idx < elements.length; idx++) {
718
549
  const [x, y] = TRIANGLE_VERTICES[idx]
719
550
  const dx = x - centroid.x
720
551
  const dy = y - centroid.y
721
- const length = Math.sqrt(dx * dx + dy * dy)
552
+ const length = Math.hypot(dx, dy)
722
553
  const distance = 0.05
723
554
 
724
555
  const label_pos = {
@@ -744,14 +575,10 @@
744
575
 
745
576
  // Find the vertex that projects to the leftmost x-position (changes with rotation)
746
577
  const projected_vertices = TRIANGLE_VERTICES.map(([vx, vy]) =>
747
- project_3d_point(vx, vy, e_mid)
578
+ project_3d_point(vx, vy, e_mid),
748
579
  )
749
580
  const leftmost_idx = projected_vertices.reduce(
750
- (
751
- min_idx,
752
- proj,
753
- idx,
754
- ) => (proj.x < projected_vertices[min_idx].x ? idx : min_idx),
581
+ (min_idx, proj, idx) => (proj.x < projected_vertices[min_idx].x ? idx : min_idx),
755
582
  0,
756
583
  )
757
584
  const [axis_x, axis_y] = TRIANGLE_VERTICES[leftmost_idx]
@@ -815,10 +642,8 @@
815
642
  let energy_face_scale: ((val: number) => string) | null = null
816
643
  let min_face_e_form = 0
817
644
  if (hull_face_color_mode === `formation_energy`) {
818
- const all_e_form = hull_faces.flatMap((tri) =>
819
- tri.vertices.map((vertex) => vertex.z)
820
- )
821
- min_face_e_form = Math.min(...all_e_form)
645
+ const all_e_form = hull_faces.flatMap((tri) => tri.vertices.map((vertex) => vertex.z))
646
+ min_face_e_form = helpers.array_min(all_e_form)
822
647
  energy_face_scale = helpers.get_energy_color_scale(
823
648
  `energy`,
824
649
  color_scale,
@@ -829,10 +654,7 @@
829
654
  }
830
655
 
831
656
  // Helper to get face color based on mode
832
- const get_face_color = (
833
- tri: typeof hull_faces[0],
834
- tri_idx: number,
835
- ): string => {
657
+ const get_face_color = (tri: (typeof hull_faces)[0], tri_idx: number): string => {
836
658
  if (hull_face_color_mode === `uniform`) {
837
659
  return hull_face_color
838
660
  }
@@ -841,12 +663,14 @@
841
663
  return energy_face_scale?.(avg_e_form - min_face_e_form) ?? hull_face_color
842
664
  }
843
665
  if (hull_face_color_mode === `dominant_element`) {
844
- // Find element vertex closest to face centroid in 2D ternary space
666
+ // Find element vertex closest to face centroid in 2D ternary space (single-pass argmin)
845
667
  const { x: cx, y: cy } = tri.centroid
846
- const dists = TRIANGLE_VERTICES.map(([tx, ty]) =>
847
- Math.hypot(cx - tx, cy - ty)
848
- )
849
- const el = elements[dists.indexOf(Math.min(...dists))]
668
+ let [closest_idx, min_dist] = [0, Infinity]
669
+ for (const [idx, [tx, ty]] of TRIANGLE_VERTICES.entries()) {
670
+ const dist = Math.hypot(cx - tx, cy - ty)
671
+ if (dist < min_dist) [closest_idx, min_dist] = [idx, dist]
672
+ }
673
+ const el = elements[closest_idx]
850
674
  return element_colors[el] ?? `#888888`
851
675
  }
852
676
  if (hull_face_color_mode === `facet_index`) {
@@ -857,11 +681,7 @@
857
681
 
858
682
  // Sort faces by depth for proper rendering
859
683
  const faces_with_depth = hull_faces.map((tri, tri_idx) => {
860
- const centroid_proj = project_3d_point(
861
- tri.centroid.x,
862
- tri.centroid.y,
863
- tri.centroid.z,
864
- )
684
+ const centroid_proj = project_3d_point(tri.centroid.x, tri.centroid.y, tri.centroid.z)
865
685
  return { tri, tri_idx, depth: centroid_proj.depth }
866
686
  })
867
687
 
@@ -886,16 +706,21 @@
886
706
  const a3 = norm_alpha?.(p3.z) ?? 0
887
707
 
888
708
  // Solve a*x + b*y + c = alpha at the three projected vertices
889
- const x1 = proj1.x, y1 = proj1.y
890
- const x2 = proj2.x, y2 = proj2.y
891
- const x3 = proj3.x, y3 = proj3.y
709
+ const x1 = proj1.x,
710
+ y1 = proj1.y
711
+ const x2 = proj2.x,
712
+ y2 = proj2.y
713
+ const x3 = proj3.x,
714
+ y3 = proj3.y
892
715
  const det = x1 * (y2 - y3) + x2 * (y3 - y1) + x3 * (y1 - y2)
893
- let coef_a = 0, coef_b = 0, coef_c = (a1 + a2 + a3) / 3
716
+ let coef_a = 0,
717
+ coef_b = 0,
718
+ coef_c = (a1 + a2 + a3) / 3
894
719
  if (Math.abs(det) > 1e-9) {
895
720
  coef_a = (a1 * (y2 - y3) + a2 * (y3 - y1) + a3 * (y1 - y2)) / det
896
721
  coef_b = (a1 * (x3 - x2) + a2 * (x1 - x3) + a3 * (x2 - x1)) / det
897
- coef_c = (a1 * (x2 * y3 - x3 * y2) + a2 * (x3 * y1 - x1 * y3) +
898
- a3 * (x1 * y2 - x2 * y1)) /
722
+ coef_c =
723
+ (a1 * (x2 * y3 - x3 * y2) + a2 * (x3 * y1 - x1 * y3) + a3 * (x1 * y2 - x2 * y1)) /
899
724
  det
900
725
  }
901
726
 
@@ -962,7 +787,7 @@
962
787
  }
963
788
 
964
789
  // Formation energy color bar helpers
965
- const e_form_range = $derived.by((): [number, number] => {
790
+ const e_form_range = $derived.by((): Vec2 => {
966
791
  const min_fe = plot_entries.length > 0 ? energy_range.min : -1
967
792
  return [min_fe, 0]
968
793
  })
@@ -972,7 +797,7 @@
972
797
  const denom = Math.max(1e-6, max_fe - min_fe)
973
798
  return (value: number) => {
974
799
  // alpha 0 at 0 eV, goes to hull_face_opacity at most negative energy
975
- const energy_fraction = Math.max(0, Math.min(1, (value - min_fe) / denom))
800
+ const energy_fraction = clamp01((value - min_fe) / denom)
976
801
  const alpha = (1 - energy_fraction) * hull_face_opacity
977
802
  return add_alpha(hull_face_color, alpha)
978
803
  }
@@ -980,59 +805,16 @@
980
805
 
981
806
  function draw_data_points(): void {
982
807
  if (!ctx || sorted_points_cache.length === 0) return
983
-
984
- for (const { entry, projected } of sorted_points_cache) {
985
- const is_stable = helpers.entry_is_stable(entry)
986
- const is_entry_highlighted = is_highlighted(entry)
987
- const color = get_point_color(entry)
988
- const size = (entry.size || (is_stable ? 6 : 4)) * canvas_dims.scale
989
- const marker = entry.marker || `circle`
990
-
991
- // Shadow
992
- const shadow_offset = Math.abs(entry.z) * 0.1 * canvas_dims.scale
993
- ctx.fillStyle = `rgba(0, 0, 0, 0.2)`
994
- const shadow_path = helpers.create_marker_path(size * 0.8, marker)
995
- ctx.save()
996
- ctx.translate(projected.x + shadow_offset, projected.y + shadow_offset)
997
- ctx.fill(shadow_path)
998
- ctx.restore()
999
-
1000
- // Highlights
1001
- if (selected_entry && entry.entry_id === selected_entry.entry_id) {
1002
- helpers.draw_selection_highlight(
1003
- ctx,
1004
- projected,
1005
- size,
1006
- canvas_dims.scale,
1007
- pulse.time,
1008
- pulse_opacity,
1009
- )
1010
- }
1011
- if (is_entry_highlighted) {
1012
- helpers.draw_highlight_effect(
1013
- ctx,
1014
- projected,
1015
- size,
1016
- canvas_dims.scale,
1017
- pulse.time,
1018
- merged_highlight_style,
1019
- )
1020
- }
1021
-
1022
- // Main point with marker symbol
1023
- ctx.fillStyle =
1024
- is_entry_highlighted && merged_highlight_style.effect === `color`
1025
- ? merged_highlight_style.color
1026
- : color
1027
- ctx.strokeStyle = is_stable ? `#ffffff` : `#000000`
1028
- ctx.lineWidth = 0.5 * canvas_dims.scale
1029
- const marker_path = helpers.create_marker_path(size, marker)
1030
- ctx.save()
1031
- ctx.translate(projected.x, projected.y)
1032
- ctx.fill(marker_path)
1033
- ctx.stroke(marker_path)
1034
- ctx.restore()
1035
- }
808
+ helpers.draw_hull_points(ctx, sorted_points_cache, {
809
+ scale: canvas_dims.scale,
810
+ shadow_factor: 0.1,
811
+ selected_entry,
812
+ is_highlighted,
813
+ get_point_color,
814
+ highlight_style: merged_highlight_style,
815
+ pulse_time: pulse.time,
816
+ pulse_opacity,
817
+ })
1036
818
  }
1037
819
 
1038
820
  const hull_label_font_size = 12
@@ -1129,18 +911,21 @@
1129
911
  ctx.textBaseline = `top`
1130
912
  const label_height = hull_label_font_size + 2
1131
913
 
1132
- const label_entries = helpers.get_composition_label_entries(
1133
- visible_entries.filter((entry) => {
1134
- if (entry.is_element) return false
1135
- const is_stable_point = helpers.entry_is_stable(entry)
1136
- return (is_stable_point && show_stable_labels) ||
1137
- (!is_stable_point && show_unstable_labels &&
1138
- (entry.e_above_hull ?? 0) <= max_hull_dist_show_labels)
1139
- }),
1140
- )
914
+ const label_entries = helpers
915
+ .get_composition_label_entries(
916
+ visible_entries.filter((entry) => {
917
+ if (entry.is_element) return false
918
+ const is_stable_point = helpers.entry_is_stable(entry)
919
+ return (
920
+ (is_stable_point && show_stable_labels) ||
921
+ (!is_stable_point &&
922
+ show_unstable_labels &&
923
+ (entry.e_above_hull ?? 0) <= max_hull_dist_show_labels)
924
+ )
925
+ }),
926
+ )
1141
927
  .sort((entry_1, entry_2) => {
1142
- const energy_diff = label_priority_energy(entry_1) -
1143
- label_priority_energy(entry_2)
928
+ const energy_diff = label_priority_energy(entry_1) - label_priority_energy(entry_2)
1144
929
  if (energy_diff !== 0) return energy_diff
1145
930
  return (entry_1.e_above_hull ?? 0) - (entry_2.e_above_hull ?? 0)
1146
931
  })
@@ -1160,17 +945,13 @@
1160
945
  const is_stable_point = helpers.entry_is_stable(entry)
1161
946
  const point_size = (entry.size || (is_stable_point ? 6 : 4)) * canvas_dims.scale
1162
947
  const text_width = measure_formula_segments(ctx, formula_segments)
1163
- const placements = get_label_placements(
1164
- projected,
1165
- point_size,
1166
- text_width,
1167
- label_height,
1168
- )
1169
- const placement = placements.find((candidate) =>
1170
- rect_within_rect(candidate.rect, canvas_rect) &&
1171
- !occupied_rects.some((occupied_rect) =>
1172
- rects_overlap(candidate.rect, occupied_rect)
1173
- )
948
+ const placements = get_label_placements(projected, point_size, text_width, label_height)
949
+ const placement = placements.find(
950
+ (candidate) =>
951
+ rect_within_rect(candidate.rect, canvas_rect) &&
952
+ !occupied_rects.some((occupied_rect) =>
953
+ rects_overlap(candidate.rect, occupied_rect),
954
+ ),
1174
955
  )
1175
956
  if (!placement) continue
1176
957
 
@@ -1216,180 +997,11 @@
1216
997
  draw_element_labels()
1217
998
  }
1218
999
 
1219
- function handle_mouse_down(event: MouseEvent) {
1220
- is_dragging = true
1221
- drag_started = false
1222
- hover_data = null
1223
- on_point_hover?.(null)
1224
- last_mouse = { x: event.clientX, y: event.clientY }
1225
- }
1226
-
1227
- const handle_mouse_move = (event: MouseEvent) => {
1228
- if (!is_dragging) return
1229
- const [dx, dy] = [event.clientX - last_mouse.x, event.clientY - last_mouse.y]
1230
-
1231
- // Mark as drag if any movement occurred
1232
- if (dx !== 0 || dy !== 0) drag_started = true
1233
-
1234
- // With Cmd/Ctrl held: pan the view instead of rotating
1235
- if (event.metaKey || event.ctrlKey) {
1236
- camera.center_x += dx
1237
- camera.center_y += dy
1238
- } else {
1239
- // Horizontal drag -> azimuth rotation around z-axis
1240
- camera.azimuth += dx * 0.3 // Positive dx (drag right) rotates clockwise
1241
-
1242
- // Vertical drag -> elevation angle (full range)
1243
- camera.elevation -= dy * 0.3 // Positive dy (drag down) tilts view down
1244
- }
1245
-
1246
- last_mouse = { x: event.clientX, y: event.clientY }
1247
- }
1248
-
1249
- const handle_wheel = (event: WheelEvent) => {
1250
- event.preventDefault()
1251
- camera.zoom = Math.max(
1252
- 0.5,
1253
- Math.min(10, camera.zoom * (event.deltaY > 0 ? 0.98 : 1.02)),
1254
- )
1255
- }
1256
-
1257
- const handle_hover = (event: MouseEvent) => {
1258
- if (is_dragging) return
1259
- const entry = find_entry_at_mouse(event)
1260
- hover_data = entry
1261
- ? { entry, position: { x: event.clientX, y: event.clientY } }
1262
- : null
1263
- on_point_hover?.(hover_data)
1264
- }
1265
-
1266
- const find_entry_at_mouse = (event: MouseEvent): ConvexHullEntry | null =>
1267
- helpers.find_hull_entry_at_mouse(
1268
- canvas,
1269
- event,
1270
- visible_entries,
1271
- (x: number, y: number, z: number) => {
1272
- const pt = project_3d_point(x, y, z)
1273
- return { x: pt.x, y: pt.y }
1274
- },
1275
- )
1276
-
1277
- const handle_click = (event: MouseEvent) => {
1278
- event.stopPropagation()
1279
- // Check if this was a drag operation (any mouse movement during drag)
1280
- const was_drag = drag_started
1281
- drag_started = false // Reset for next interaction
1282
- if (was_drag) return // Don't trigger click if this was a drag
1283
-
1284
- const entry = find_entry_at_mouse(event)
1285
- if (!entry) {
1286
- if (modal_open) close_structure_popup()
1287
- return
1288
- }
1289
-
1290
- on_point_click?.(entry)
1291
-
1292
- if (enable_click_selection) {
1293
- selected_entry = entry
1294
- if (enable_structure_preview) {
1295
- const structure = extract_structure_from_entry(entry)
1296
- if (structure) {
1297
- selected_structure = structure
1298
- modal_place_right = helpers.calculate_modal_side(wrapper)
1299
- modal_open = true
1300
- }
1301
- }
1302
- }
1303
- }
1304
-
1305
- function close_structure_popup() {
1306
- modal_open = false
1307
- selected_structure = null
1308
- selected_entry = null
1309
- }
1310
-
1311
- const handle_double_click = (event: MouseEvent) => {
1312
- const entry = find_entry_at_mouse(event)
1313
- if (entry) {
1314
- copy_entry_data(entry, {
1315
- x: event.clientX,
1316
- y: event.clientY,
1317
- })
1318
- }
1319
- }
1320
-
1321
- function render_once() {
1322
- if (!frame_id) {
1323
- frame_id = requestAnimationFrame(() => {
1324
- render_frame()
1325
- frame_id = 0
1326
- })
1327
- }
1328
- }
1329
-
1330
- function update_canvas_size() {
1331
- if (!canvas) return
1332
- const dpr = globalThis.devicePixelRatio || 1
1333
- const container = canvas.parentElement
1334
- const rect = container?.getBoundingClientRect()
1335
- const [w, h] = rect ? [rect.width, rect.height] : [400, 400]
1336
-
1337
- // Only update canvas dimensions if they actually changed
1338
- // (assigning canvas.width/height clears the canvas even if values are the same)
1339
- const new_width = Math.max(0, Math.round(w * dpr))
1340
- const new_height = Math.max(0, Math.round(h * dpr))
1341
- if (!ctx || canvas.width !== new_width || canvas.height !== new_height) {
1342
- canvas.width = new_width
1343
- canvas.height = new_height
1344
- ctx = canvas.getContext(`2d`)
1345
- if (ctx) {
1346
- ctx.setTransform(dpr, 0, 0, dpr, 0, 0)
1347
- ctx.imageSmoothingEnabled = true
1348
- ctx.imageSmoothingQuality = `high`
1349
- }
1350
- }
1351
- canvas_dims = { width: w, height: h, scale: Math.min(w, h) / 600 }
1352
- render_once()
1353
- }
1354
-
1355
1000
  // Reactive dark mode detection for canvas text color
1356
1001
  let dark_mode = $state(is_dark_mode())
1357
- $effect(() => watch_dark_mode((dark) => dark_mode = dark))
1002
+ $effect(() => watch_dark_mode((dark) => (dark_mode = dark)))
1358
1003
  const text_color = $derived(helpers.get_canvas_text_color(dark_mode))
1359
1004
 
1360
- $effect(() => {
1361
- if (!canvas) return
1362
-
1363
- // Initial setup
1364
- update_canvas_size()
1365
-
1366
- // Watch for resize events - only update canvas, don't reset camera
1367
- const resize_observer = new ResizeObserver(update_canvas_size)
1368
-
1369
- const container = canvas.parentElement
1370
- if (container) {
1371
- resize_observer.observe(container)
1372
- }
1373
-
1374
- return () => { // Cleanup on unmount
1375
- if (frame_id) cancelAnimationFrame(frame_id)
1376
- resize_observer.disconnect()
1377
- }
1378
- })
1379
-
1380
- // Fullscreen handling with camera reset
1381
- let was_fullscreen = $state(fullscreen)
1382
- $effect(() => {
1383
- setup_fullscreen_effect(fullscreen, wrapper, (entering_fullscreen) => {
1384
- if (entering_fullscreen !== was_fullscreen) {
1385
- camera.center_x = 0
1386
- camera.center_y = -50
1387
- was_fullscreen = entering_fullscreen
1388
- }
1389
- })
1390
- set_fullscreen_bg(wrapper, fullscreen, `--hull-3d-bg-fullscreen`)
1391
- })
1392
-
1393
1005
  // Performance: Cache canvas dimensions and formation energy range
1394
1006
  let canvas_dims = $state({ width: 600, height: 600, scale: 1 })
1395
1007
  const energy_range = $derived.by(() => {
@@ -1404,64 +1016,38 @@
1404
1016
  return { min, max, center: (min + max) / 2, z_scale }
1405
1017
  })
1406
1018
 
1407
- // Performance: Pre-compute and cache all point projections + depth sorting
1408
- const sorted_points_cache = $derived.by(() => {
1409
- if (!canvas || visible_entries.length === 0) return []
1410
- return visible_entries
1411
- .map((entry) => ({
1412
- entry,
1413
- projected: project_3d_point(entry.x, entry.y, entry.z),
1414
- }))
1415
- .sort((left, right) => left.projected.depth - right.projected.depth)
1416
- })
1417
-
1418
- let style = $derived(
1419
- `--hull-stable-color:${merged_config.colors?.stable || `#0072B2`};
1420
- --hull-unstable-color:${merged_config.colors?.unstable || `#E69F00`};
1421
- --hull-edge-color:${merged_config.colors?.edge || `var(--text-color, #212121)`};
1422
- --hull-text-color:${
1423
- merged_config.colors?.annotation || `var(--text-color, #212121)`
1424
- }`,
1425
- )
1019
+ let style = $derived(helpers.hull_style_css(merged_config.colors))
1426
1020
  </script>
1427
1021
 
1428
1022
  <svelte:document
1429
1023
  onfullscreenchange={() => {
1430
- fullscreen = Boolean(document.fullscreenElement)
1024
+ // tie fullscreen state to this component's own wrapper, not any fullscreen element
1025
+ fullscreen = document.fullscreenElement === wrapper
1431
1026
  }}
1432
- onmousemove={handle_mouse_move}
1433
- onmouseup={() => ([is_dragging, drag_started] = [false, false])}
1027
+ onmousemove={interactions.handle_mouse_move}
1028
+ onmouseup={interactions.handle_mouse_up}
1434
1029
  />
1435
1030
 
1436
1031
  <div
1437
1032
  {...rest}
1438
- class="convex-hull-3d {rest.class ?? ``}"
1439
- class:dragover={drag_over}
1033
+ class={[`convex-hull-3d`, rest.class]}
1034
+ class:dragover={interactions.drag_over}
1440
1035
  style={`${style}; ${rest.style ?? ``}`}
1441
1036
  data-has-selection={selected_entry !== null}
1442
- data-has-hover={hover_data !== null}
1443
- data-is-dragging={is_dragging}
1037
+ data-has-hover={interactions.hover_data !== null}
1038
+ data-is-dragging={interactions.is_dragging}
1444
1039
  bind:this={wrapper}
1445
1040
  role="application"
1446
1041
  tabindex="-1"
1447
- onkeydown={handle_keydown}
1448
- ondrop={handle_file_drop}
1449
- ondragover={(event) => {
1450
- event.preventDefault()
1451
- drag_over = true
1452
- }}
1453
- ondragleave={(event) => {
1454
- event.preventDefault()
1455
- drag_over = false
1456
- }}
1042
+ {...interactions.wrapper_handlers}
1457
1043
  aria-label="Ternary convex hull visualization"
1458
1044
  >
1459
1045
  {@render children?.({
1460
- stable_entries,
1461
- unstable_entries,
1462
- highlighted_entries,
1463
- selected_entry,
1464
- })}
1046
+ stable_entries,
1047
+ unstable_entries,
1048
+ highlighted_entries,
1049
+ selected_entry,
1050
+ })}
1465
1051
  <h3 style="position: absolute; left: 1em; top: 1ex; margin: 0; font-weight: 500">
1466
1052
  {@html sanitize_html(merged_controls.title || phase_stats?.chemical_system || ``)}
1467
1053
  </h3>
@@ -1469,12 +1055,7 @@
1469
1055
  bind:this={canvas}
1470
1056
  tabindex="0"
1471
1057
  aria-label={merged_controls.title || phase_stats?.chemical_system || `3D Convex Hull`}
1472
- onmousedown={handle_mouse_down}
1473
- onmousemove={handle_hover}
1474
- onclick={handle_click}
1475
- onkeydown={handle_keydown}
1476
- ondblclick={handle_double_click}
1477
- onwheel={handle_wheel}
1058
+ {...interactions.canvas_handlers}
1478
1059
  ></canvas>
1479
1060
 
1480
1061
  {#if entries.length === 0}
@@ -1486,14 +1067,9 @@
1486
1067
 
1487
1068
  <!-- Formation Energy Color Bar (bottom-left corner) -->
1488
1069
  {#if color_mode === `energy` && plot_entries.length > 0}
1489
- {@const hull_distances = plot_entries
1490
- .map((entry) => entry.e_above_hull)
1491
- .filter((value): value is number => typeof value === `number`)}
1492
- {@const min_energy = hull_distances.length > 0 ? Math.min(...hull_distances) : 0}
1493
- {@const max_energy = hull_distances.length > 0 ? Math.max(...hull_distances, 0.1) : 0.1}
1494
1070
  <ColorBar
1495
1071
  title="Energy above hull (eV/atom)"
1496
- range={[min_energy, max_energy]}
1072
+ range={helpers.hull_distance_range(plot_entries)}
1497
1073
  {color_scale}
1498
1074
  wrapper_style="position: absolute; bottom: 16px; left: 1em; width: 200px;"
1499
1075
  bar_style="height: 12px;"
@@ -1503,9 +1079,7 @@
1503
1079
 
1504
1080
  <!-- Formation Energy Faces Color Bar (bottom-right corner) -->
1505
1081
  <!-- Only show for uniform/formation_energy modes where face color relates to E_form -->
1506
- {#if plot_entries.length > 0 && show_hull_faces &&
1507
- (hull_face_color_mode === `uniform` ||
1508
- hull_face_color_mode === `formation_energy`)}
1082
+ {#if plot_entries.length > 0 && show_hull_faces && (hull_face_color_mode === `uniform` || hull_face_color_mode === `formation_energy`)}
1509
1083
  <ColorBar
1510
1084
  title="Formation energy (eV/atom)"
1511
1085
  color_scale_fn={e_form_color_scale_fn}
@@ -1517,89 +1091,53 @@
1517
1091
  />
1518
1092
  {/if}
1519
1093
 
1520
- <!-- Control buttons (top-right corner) -->
1521
- {#if controls_config.mode !== `never`}
1522
- <section class="control-buttons {controls_config.class}">
1523
- {#if controls_config.visible(`reset`)}
1524
- <button
1525
- type="button"
1526
- onclick={reset_all}
1527
- title="Reset view and settings"
1528
- class="reset-camera-btn"
1529
- >
1530
- <Icon icon="Reset" />
1531
- </button>
1532
- {/if}
1533
-
1534
- {#if enable_info_pane && phase_stats && controls_config.visible(`info-pane`)}
1535
- <ConvexHullInfoPane
1536
- bind:pane_open={info_pane_open}
1537
- {phase_stats}
1538
- {stable_entries}
1539
- {unstable_entries}
1540
- {show_stable}
1541
- {show_unstable}
1542
- {max_hull_dist_show_phases}
1543
- {max_hull_dist_show_labels}
1544
- {label_threshold}
1545
- toggle_props={{ class: `info-btn` }}
1546
- />
1547
- {/if}
1548
-
1549
- {#if enable_fullscreen && controls_config.visible(`fullscreen`)}
1550
- <button
1551
- type="button"
1552
- onclick={() => toggle_fullscreen(wrapper)}
1553
- title="{fullscreen ? `Exit` : `Enter`} fullscreen"
1554
- class="fullscreen-btn"
1555
- >
1556
- <Icon icon="{fullscreen ? `Exit` : ``}Fullscreen" />
1557
- </button>
1558
- {/if}
1559
-
1560
- <!-- Legend controls pane -->
1561
- {#if controls_config.visible(`controls`)}
1562
- <ConvexHullControls
1563
- bind:controls_open={legend_pane_open}
1564
- bind:color_mode
1565
- bind:color_scale
1566
- bind:show_stable
1567
- bind:show_unstable
1568
- bind:show_stable_labels
1569
- bind:show_unstable_labels
1570
- bind:max_hull_dist_show_phases
1571
- bind:max_hull_dist_show_labels
1572
- {max_hull_dist_in_data}
1573
- {stable_entries}
1574
- {unstable_entries}
1575
- {camera}
1576
- {merged_controls}
1577
- toggle_props={{ class: `legend-controls-btn` }}
1578
- {show_hull_faces}
1579
- on_hull_faces_change={(value: boolean) => show_hull_faces = value}
1580
- {hull_face_color}
1581
- on_hull_face_color_change={(value: string) => hull_face_color = value}
1582
- {hull_face_opacity}
1583
- on_hull_face_opacity_change={(value: number) => hull_face_opacity = value}
1584
- {hull_face_color_mode}
1585
- on_hull_face_color_mode_change={(value: HullFaceColorMode) =>
1586
- hull_face_color_mode = value}
1587
- bind:energy_source_mode
1588
- {has_precomputed_e_form}
1589
- {can_compute_e_form}
1590
- {has_precomputed_hull}
1591
- {can_compute_hull}
1592
- />
1593
- {/if}
1594
- </section>
1595
- {/if}
1094
+ <!-- Toolbar + tooltip/copy-feedback/drag/structure-popup chrome -->
1095
+ <ConvexHullChrome
1096
+ {interactions}
1097
+ {hull_data}
1098
+ {controls_config}
1099
+ {reset_all}
1100
+ reset_title="Reset view and settings"
1101
+ {enable_info_pane}
1102
+ {phase_stats}
1103
+ {label_threshold}
1104
+ {fullscreen}
1105
+ {fullscreen_toggle}
1106
+ {wrapper}
1107
+ {camera}
1108
+ {merged_controls}
1109
+ {stable_entries}
1110
+ {unstable_entries}
1111
+ {get_point_color}
1112
+ {merged_highlight_style}
1113
+ {is_highlighted}
1114
+ {tooltip}
1115
+ {selected_entry}
1116
+ bind:show_hull_faces
1117
+ bind:hull_face_color
1118
+ bind:hull_face_opacity
1119
+ bind:hull_face_color_mode
1120
+ bind:info_pane_open
1121
+ bind:controls_open
1122
+ bind:color_mode
1123
+ bind:color_scale
1124
+ bind:show_stable
1125
+ bind:show_unstable
1126
+ {entry_category}
1127
+ bind:hidden_categories
1128
+ bind:show_stable_labels
1129
+ bind:show_unstable_labels
1130
+ bind:max_hull_dist_show_phases
1131
+ bind:max_hull_dist_show_labels
1132
+ bind:energy_source_mode
1133
+ />
1596
1134
 
1597
1135
  <!-- Orientation gizmo (configurable placement, default top-right) -->
1598
1136
  {#if gizmo && typeof WebGLRenderingContext !== `undefined`}
1599
1137
  <div class="gizmo-wrapper {controls_config.class}" data-placement={gizmo_placement}>
1600
1138
  <Canvas
1601
1139
  createRenderer={(cvs: HTMLCanvasElement) =>
1602
- new WebGLRenderer({ canvas: cvs, alpha: true, antialias: true })}
1140
+ new WebGLRenderer({ canvas: cvs, alpha: true, antialias: true })}
1603
1141
  >
1604
1142
  <T.PerspectiveCamera
1605
1143
  makeDefault
@@ -1629,11 +1167,13 @@
1629
1167
  </div>
1630
1168
  {/if}
1631
1169
 
1632
- {#if (has_temp_data && temperature !== undefined) ||
1633
- (gas_analysis.has_gas_dependent_elements && merged_gas_config)}
1170
+ {#if (has_temp_data && temperature !== undefined) || (gas_analysis.has_gas_dependent_elements && merged_gas_config)}
1634
1171
  <div class="right-controls">
1635
1172
  {#if has_temp_data && temperature !== undefined}
1636
- <TemperatureSlider {available_temperatures} bind:temperature />
1173
+ <TemperatureSlider
1174
+ available_temperatures={hull_data.available_temperatures}
1175
+ bind:temperature
1176
+ />
1637
1177
  {/if}
1638
1178
  {#if gas_analysis.has_gas_dependent_elements && merged_gas_config}
1639
1179
  <GasPressureControls
@@ -1644,46 +1184,6 @@
1644
1184
  {/if}
1645
1185
  </div>
1646
1186
  {/if}
1647
-
1648
- <!-- Hover tooltip -->
1649
- {#if hover_data}
1650
- {@const { entry, position } = hover_data}
1651
- {@const entry_highlight = is_highlighted(entry) ? merged_highlight_style : undefined}
1652
- {@const tooltip_style =
1653
- `z-index: ${CONVEX_HULL_STYLE.z_index.tooltip}; backdrop-filter: blur(4px);
1654
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);`}
1655
- <PlotTooltip
1656
- x={position.x}
1657
- y={position.y}
1658
- offset={{ x: 10, y: -10 }}
1659
- bg_color={get_point_color(entry)}
1660
- fixed
1661
- style={tooltip_style}
1662
- >
1663
- <ConvexHullTooltip
1664
- {entry}
1665
- {polymorph_stats_map}
1666
- highlight_style={entry_highlight}
1667
- {tooltip}
1668
- />
1669
- </PlotTooltip>
1670
- {/if}
1671
-
1672
- <ClickFeedback bind:visible={copy_feedback.visible} position={copy_feedback.position} />
1673
- <DragOverlay visible={drag_over} />
1674
-
1675
- {#if modal_open && selected_structure}
1676
- <StructurePopup
1677
- structure={selected_structure}
1678
- place_right={modal_place_right}
1679
- stats={{
1680
- id: selected_entry?.entry_id,
1681
- e_above_hull: selected_entry?.e_above_hull,
1682
- e_form: selected_entry?.e_form_per_atom,
1683
- }}
1684
- onclose={close_structure_popup}
1685
- />
1686
- {/if}
1687
1187
  </div>
1688
1188
 
1689
1189
  <style>
@@ -1763,39 +1263,4 @@
1763
1263
  opacity: 1;
1764
1264
  pointer-events: auto;
1765
1265
  }
1766
- .control-buttons {
1767
- position: absolute;
1768
- top: 1ex;
1769
- right: 1ex;
1770
- display: flex;
1771
- gap: 8px;
1772
- transition: opacity 0.2s ease-in-out;
1773
- }
1774
- .control-buttons.hover-visible {
1775
- opacity: 0;
1776
- pointer-events: none;
1777
- }
1778
- .convex-hull-3d:hover .control-buttons.hover-visible,
1779
- .convex-hull-3d:focus-within .control-buttons.hover-visible {
1780
- opacity: 1;
1781
- pointer-events: auto;
1782
- }
1783
- .control-buttons.always-visible {
1784
- opacity: 1;
1785
- pointer-events: auto;
1786
- }
1787
- .control-buttons :global(button) {
1788
- background: transparent;
1789
- border: none;
1790
- padding: 4px;
1791
- cursor: pointer;
1792
- border-radius: 3px;
1793
- color: var(--text-color, currentColor);
1794
- transition: background-color 0.2s;
1795
- display: flex;
1796
- font-size: clamp(0.85em, 2cqmin, 1.3em);
1797
- }
1798
- .control-buttons :global(button):hover {
1799
- background-color: color-mix(in srgb, currentColor 8%, transparent);
1800
- }
1801
1266
  </style>