matterviz 0.3.2 → 0.3.4

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 (281) hide show
  1. package/dist/EmptyState.svelte +10 -2
  2. package/dist/FilePicker.svelte +123 -82
  3. package/dist/Icon.svelte +18 -12
  4. package/dist/MillerIndexInput.svelte +27 -21
  5. package/dist/api/optimade.js +6 -6
  6. package/dist/app.css +216 -207
  7. package/dist/brillouin/BrillouinZone.svelte +292 -149
  8. package/dist/brillouin/BrillouinZone.svelte.d.ts +1 -1
  9. package/dist/brillouin/BrillouinZoneControls.svelte +32 -5
  10. package/dist/brillouin/BrillouinZoneExportPane.svelte +69 -42
  11. package/dist/brillouin/BrillouinZoneExportPane.svelte.d.ts +1 -1
  12. package/dist/brillouin/BrillouinZoneInfoPane.svelte +99 -68
  13. package/dist/brillouin/BrillouinZoneScene.svelte +275 -163
  14. package/dist/brillouin/BrillouinZoneScene.svelte.d.ts +1 -1
  15. package/dist/brillouin/BrillouinZoneTooltip.svelte +17 -7
  16. package/dist/brillouin/compute.js +11 -6
  17. package/dist/chempot-diagram/ChemPotDiagram.svelte +162 -27
  18. package/dist/chempot-diagram/ChemPotDiagram2D.svelte +451 -281
  19. package/dist/chempot-diagram/ChemPotDiagram3D.svelte +2148 -1642
  20. package/dist/chempot-diagram/ChemPotScene3D.svelte +8 -5
  21. package/dist/chempot-diagram/async-compute.svelte.d.ts +3 -0
  22. package/dist/chempot-diagram/async-compute.svelte.js +77 -0
  23. package/dist/chempot-diagram/chempot-worker.d.ts +1 -0
  24. package/dist/chempot-diagram/chempot-worker.js +11 -0
  25. package/dist/chempot-diagram/color.js +1 -2
  26. package/dist/chempot-diagram/compute.d.ts +10 -0
  27. package/dist/chempot-diagram/compute.js +250 -88
  28. package/dist/chempot-diagram/index.d.ts +2 -1
  29. package/dist/chempot-diagram/index.js +2 -1
  30. package/dist/chempot-diagram/temperature.js +8 -9
  31. package/dist/chempot-diagram/types.d.ts +3 -0
  32. package/dist/chempot-diagram/types.js +1 -0
  33. package/dist/colors/index.d.ts +1 -1
  34. package/dist/colors/index.js +5 -3
  35. package/dist/composition/BarChart.svelte +128 -55
  36. package/dist/composition/BubbleChart.svelte +102 -49
  37. package/dist/composition/Composition.svelte +100 -79
  38. package/dist/composition/Formula.svelte +108 -62
  39. package/dist/composition/FormulaFilter.svelte +665 -537
  40. package/dist/composition/PieChart.svelte +183 -108
  41. package/dist/composition/format.d.ts +5 -0
  42. package/dist/composition/format.js +20 -3
  43. package/dist/composition/parse.js +14 -9
  44. package/dist/convex-hull/ConvexHull.svelte +93 -40
  45. package/dist/convex-hull/ConvexHull.svelte.d.ts +1 -1
  46. package/dist/convex-hull/ConvexHull2D.svelte +549 -360
  47. package/dist/convex-hull/ConvexHull2D.svelte.d.ts +1 -1
  48. package/dist/convex-hull/ConvexHull3D.svelte +1296 -827
  49. package/dist/convex-hull/ConvexHull3D.svelte.d.ts +1 -1
  50. package/dist/convex-hull/ConvexHull4D.svelte +1004 -688
  51. package/dist/convex-hull/ConvexHull4D.svelte.d.ts +1 -1
  52. package/dist/convex-hull/ConvexHullControls.svelte +115 -28
  53. package/dist/convex-hull/ConvexHullControls.svelte.d.ts +1 -1
  54. package/dist/convex-hull/ConvexHullInfoPane.svelte +29 -3
  55. package/dist/convex-hull/ConvexHullStats.svelte +425 -328
  56. package/dist/convex-hull/ConvexHullTooltip.svelte +40 -16
  57. package/dist/convex-hull/GasPressureControls.svelte +104 -61
  58. package/dist/convex-hull/StructurePopup.svelte +25 -4
  59. package/dist/convex-hull/TemperatureSlider.svelte +45 -25
  60. package/dist/convex-hull/barycentric-coords.js +13 -7
  61. package/dist/convex-hull/demo-temperature.js +8 -4
  62. package/dist/convex-hull/gas-thermodynamics.js +17 -12
  63. package/dist/convex-hull/helpers.d.ts +9 -0
  64. package/dist/convex-hull/helpers.js +77 -34
  65. package/dist/convex-hull/thermodynamics.js +61 -56
  66. package/dist/convex-hull/types.d.ts +9 -14
  67. package/dist/convex-hull/types.js +0 -17
  68. package/dist/coordination/CoordinationBarPlot.svelte +227 -154
  69. package/dist/element/BohrAtom.svelte +55 -12
  70. package/dist/element/ElementHeading.svelte +7 -2
  71. package/dist/element/ElementPhoto.svelte +15 -9
  72. package/dist/element/ElementStats.svelte +10 -4
  73. package/dist/element/ElementTile.svelte +137 -73
  74. package/dist/element/Nucleus.svelte +39 -11
  75. package/dist/element/data.js +1 -1
  76. package/dist/feedback/ClickFeedback.svelte +16 -5
  77. package/dist/feedback/DragOverlay.svelte +10 -2
  78. package/dist/feedback/Spinner.svelte +4 -2
  79. package/dist/feedback/StatusMessage.svelte +8 -2
  80. package/dist/fermi-surface/FermiSlice.svelte +118 -88
  81. package/dist/fermi-surface/FermiSurface.svelte +328 -187
  82. package/dist/fermi-surface/FermiSurface.svelte.d.ts +1 -1
  83. package/dist/fermi-surface/FermiSurfaceControls.svelte +113 -46
  84. package/dist/fermi-surface/FermiSurfaceControls.svelte.d.ts +1 -1
  85. package/dist/fermi-surface/FermiSurfaceScene.svelte +535 -342
  86. package/dist/fermi-surface/FermiSurfaceScene.svelte.d.ts +1 -1
  87. package/dist/fermi-surface/FermiSurfaceTooltip.svelte +14 -5
  88. package/dist/fermi-surface/compute.js +16 -20
  89. package/dist/fermi-surface/parse.js +24 -14
  90. package/dist/fermi-surface/symmetry.js +2 -7
  91. package/dist/fermi-surface/types.d.ts +3 -5
  92. package/dist/heatmap-matrix/HeatmapMatrix.svelte +1019 -765
  93. package/dist/heatmap-matrix/HeatmapMatrix.svelte.d.ts +1 -1
  94. package/dist/heatmap-matrix/HeatmapMatrixControls.svelte +76 -22
  95. package/dist/heatmap-matrix/HeatmapMatrixControls.svelte.d.ts +2 -3
  96. package/dist/icons.js +47 -0
  97. package/dist/index.d.ts +2 -1
  98. package/dist/index.js +2 -1
  99. package/dist/io/decompress.js +1 -1
  100. package/dist/io/export.d.ts +3 -0
  101. package/dist/io/export.js +129 -143
  102. package/dist/io/is-binary.js +2 -3
  103. package/dist/io/url-drop.js +1 -2
  104. package/dist/isosurface/Isosurface.svelte +202 -148
  105. package/dist/isosurface/IsosurfaceControls.svelte +46 -28
  106. package/dist/isosurface/parse.js +34 -29
  107. package/dist/isosurface/slice.js +5 -10
  108. package/dist/isosurface/types.d.ts +2 -1
  109. package/dist/isosurface/types.js +61 -12
  110. package/dist/labels.js +11 -8
  111. package/dist/layout/FullscreenToggle.svelte +11 -2
  112. package/dist/layout/InfoCard.svelte +38 -6
  113. package/dist/layout/InfoTag.svelte +63 -32
  114. package/dist/layout/PropertyFilter.svelte +82 -37
  115. package/dist/layout/SettingsSection.svelte +85 -55
  116. package/dist/layout/SubpageGrid.svelte +10 -2
  117. package/dist/layout/json-tree/JsonNode.svelte +183 -138
  118. package/dist/layout/json-tree/JsonTree.svelte +499 -413
  119. package/dist/layout/json-tree/JsonValue.svelte +127 -99
  120. package/dist/layout/json-tree/utils.js +4 -2
  121. package/dist/marching-cubes.js +25 -2
  122. package/dist/math.d.ts +13 -17
  123. package/dist/math.js +133 -67
  124. package/dist/overlays/ContextMenu.svelte +65 -40
  125. package/dist/overlays/DraggablePane.svelte +211 -139
  126. package/dist/periodic-table/PeriodicTable.svelte +278 -145
  127. package/dist/periodic-table/PeriodicTableControls.svelte +178 -128
  128. package/dist/periodic-table/PropertySelect.svelte +25 -7
  129. package/dist/periodic-table/TableInset.svelte +8 -3
  130. package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte +446 -309
  131. package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte.d.ts +1 -1
  132. package/dist/phase-diagram/PhaseDiagramControls.svelte +102 -43
  133. package/dist/phase-diagram/PhaseDiagramControls.svelte.d.ts +1 -1
  134. package/dist/phase-diagram/PhaseDiagramEditorPane.svelte +63 -40
  135. package/dist/phase-diagram/PhaseDiagramExportPane.svelte +71 -28
  136. package/dist/phase-diagram/PhaseDiagramExportPane.svelte.d.ts +1 -1
  137. package/dist/phase-diagram/PhaseDiagramTooltip.svelte +158 -101
  138. package/dist/phase-diagram/TdbInfoPanel.svelte +28 -4
  139. package/dist/phase-diagram/build-diagram.js +9 -9
  140. package/dist/phase-diagram/colors.js +1 -3
  141. package/dist/phase-diagram/parse.js +10 -9
  142. package/dist/phase-diagram/svg-to-diagram.js +53 -49
  143. package/dist/phase-diagram/utils.d.ts +1 -0
  144. package/dist/phase-diagram/utils.js +80 -25
  145. package/dist/plot/AxisLabel.svelte +28 -3
  146. package/dist/plot/BarPlot.svelte +1182 -734
  147. package/dist/plot/BarPlot.svelte.d.ts +2 -2
  148. package/dist/plot/BarPlotControls.svelte +31 -5
  149. package/dist/plot/BarPlotControls.svelte.d.ts +1 -1
  150. package/dist/plot/ColorBar.svelte +479 -329
  151. package/dist/plot/ColorScaleSelect.svelte +27 -6
  152. package/dist/plot/ElementScatter.svelte +36 -15
  153. package/dist/plot/FillArea.svelte +152 -95
  154. package/dist/plot/Histogram.svelte +934 -571
  155. package/dist/plot/Histogram.svelte.d.ts +1 -1
  156. package/dist/plot/HistogramControls.svelte +53 -9
  157. package/dist/plot/HistogramControls.svelte.d.ts +1 -1
  158. package/dist/plot/InteractiveAxisLabel.svelte +34 -11
  159. package/dist/plot/InteractiveAxisLabel.svelte.d.ts +1 -1
  160. package/dist/plot/Line.svelte +63 -28
  161. package/dist/plot/PlotControls.svelte +157 -114
  162. package/dist/plot/PlotControls.svelte.d.ts +1 -1
  163. package/dist/plot/PlotLegend.svelte +174 -91
  164. package/dist/plot/PlotTooltip.svelte +45 -6
  165. package/dist/plot/PortalSelect.svelte +175 -147
  166. package/dist/plot/ReferenceLine.svelte +76 -22
  167. package/dist/plot/ReferenceLine3D.svelte +132 -107
  168. package/dist/plot/ReferencePlane.svelte +146 -121
  169. package/dist/plot/ScatterPlot.svelte +1681 -1091
  170. package/dist/plot/ScatterPlot.svelte.d.ts +2 -2
  171. package/dist/plot/ScatterPlot3D.svelte +256 -131
  172. package/dist/plot/ScatterPlot3D.svelte.d.ts +2 -2
  173. package/dist/plot/ScatterPlot3DControls.svelte +113 -63
  174. package/dist/plot/ScatterPlot3DControls.svelte.d.ts +2 -1
  175. package/dist/plot/ScatterPlot3DScene.svelte +608 -403
  176. package/dist/plot/ScatterPlot3DScene.svelte.d.ts +2 -2
  177. package/dist/plot/ScatterPlotControls.svelte +65 -25
  178. package/dist/plot/ScatterPlotControls.svelte.d.ts +1 -1
  179. package/dist/plot/ScatterPoint.svelte +98 -26
  180. package/dist/plot/ScatterPoint.svelte.d.ts +1 -0
  181. package/dist/plot/SpacegroupBarPlot.svelte +142 -85
  182. package/dist/plot/Surface3D.svelte +159 -108
  183. package/dist/plot/ZeroLines.svelte +55 -3
  184. package/dist/plot/ZoomRect.svelte +4 -2
  185. package/dist/plot/axis-utils.js +1 -3
  186. package/dist/plot/data-cleaning.js +12 -28
  187. package/dist/plot/data-transform.js +2 -1
  188. package/dist/plot/fill-utils.js +2 -0
  189. package/dist/plot/layout.d.ts +4 -1
  190. package/dist/plot/layout.js +33 -14
  191. package/dist/plot/reference-line.d.ts +2 -2
  192. package/dist/plot/reference-line.js +7 -5
  193. package/dist/plot/scales.js +24 -36
  194. package/dist/plot/types.d.ts +11 -23
  195. package/dist/plot/types.js +6 -11
  196. package/dist/plot/utils/label-placement.d.ts +32 -15
  197. package/dist/plot/utils/label-placement.js +227 -66
  198. package/dist/plot/utils/series-visibility.js +2 -3
  199. package/dist/rdf/RdfPlot.svelte +143 -91
  200. package/dist/rdf/calc-rdf.js +4 -5
  201. package/dist/sanitize.d.ts +4 -0
  202. package/dist/sanitize.js +107 -0
  203. package/dist/settings.d.ts +18 -6
  204. package/dist/settings.js +46 -16
  205. package/dist/spectral/Bands.svelte +632 -453
  206. package/dist/spectral/BandsAndDos.svelte +90 -49
  207. package/dist/spectral/BrillouinBandsDos.svelte +151 -93
  208. package/dist/spectral/Dos.svelte +389 -258
  209. package/dist/spectral/helpers.js +55 -43
  210. package/dist/state.svelte.d.ts +1 -1
  211. package/dist/state.svelte.js +3 -2
  212. package/dist/structure/Arrow.svelte +59 -20
  213. package/dist/structure/AtomLegend.svelte +215 -134
  214. package/dist/structure/Bond.svelte +73 -47
  215. package/dist/structure/CanvasTooltip.svelte +10 -2
  216. package/dist/structure/CellSelect.svelte +72 -45
  217. package/dist/structure/Cylinder.svelte +33 -17
  218. package/dist/structure/Lattice.svelte +88 -33
  219. package/dist/structure/Structure.svelte +1063 -797
  220. package/dist/structure/Structure.svelte.d.ts +1 -1
  221. package/dist/structure/StructureControls.svelte +349 -118
  222. package/dist/structure/StructureExportPane.svelte +124 -89
  223. package/dist/structure/StructureExportPane.svelte.d.ts +1 -1
  224. package/dist/structure/StructureInfoPane.svelte +304 -237
  225. package/dist/structure/StructureScene.svelte +879 -443
  226. package/dist/structure/StructureScene.svelte.d.ts +15 -7
  227. package/dist/structure/atom-properties.js +8 -8
  228. package/dist/structure/bonding.js +6 -7
  229. package/dist/structure/export.js +14 -29
  230. package/dist/structure/ferrox-wasm.js +1 -1
  231. package/dist/structure/index.d.ts +13 -3
  232. package/dist/structure/index.js +83 -23
  233. package/dist/structure/measure.d.ts +2 -2
  234. package/dist/structure/measure.js +4 -44
  235. package/dist/structure/parse.js +113 -141
  236. package/dist/structure/partial-occupancy.js +7 -10
  237. package/dist/structure/pbc.d.ts +1 -0
  238. package/dist/structure/pbc.js +16 -6
  239. package/dist/structure/supercell.d.ts +2 -2
  240. package/dist/structure/supercell.js +12 -22
  241. package/dist/structure/validation.js +1 -2
  242. package/dist/symmetry/SymmetryStats.svelte +84 -41
  243. package/dist/symmetry/WyckoffTable.svelte +26 -6
  244. package/dist/symmetry/cell-transform.js +5 -3
  245. package/dist/symmetry/index.js +8 -7
  246. package/dist/symmetry/spacegroups.js +148 -148
  247. package/dist/table/HeatmapTable.svelte +790 -554
  248. package/dist/table/HeatmapTable.svelte.d.ts +1 -1
  249. package/dist/table/ToggleMenu.svelte +125 -92
  250. package/dist/table/index.js +2 -4
  251. package/dist/theme/ThemeControl.svelte +21 -12
  252. package/dist/time.js +4 -1
  253. package/dist/tooltip/TooltipContent.svelte +33 -8
  254. package/dist/trajectory/Trajectory.svelte +758 -558
  255. package/dist/trajectory/TrajectoryError.svelte +14 -3
  256. package/dist/trajectory/TrajectoryExportPane.svelte +137 -83
  257. package/dist/trajectory/TrajectoryInfoPane.svelte +272 -143
  258. package/dist/trajectory/extract.js +10 -26
  259. package/dist/trajectory/format-detect.js +5 -5
  260. package/dist/trajectory/frame-reader.d.ts +1 -1
  261. package/dist/trajectory/frame-reader.js +5 -12
  262. package/dist/trajectory/helpers.d.ts +0 -1
  263. package/dist/trajectory/helpers.js +2 -17
  264. package/dist/trajectory/index.js +14 -12
  265. package/dist/trajectory/parse/ase.js +5 -4
  266. package/dist/trajectory/parse/hdf5.js +26 -18
  267. package/dist/trajectory/parse/index.js +13 -18
  268. package/dist/trajectory/parse/lammps.js +17 -7
  269. package/dist/trajectory/parse/vasp.js +5 -2
  270. package/dist/trajectory/parse/xyz.js +8 -7
  271. package/dist/trajectory/plotting.js +13 -8
  272. package/dist/utils.d.ts +1 -0
  273. package/dist/utils.js +13 -0
  274. package/dist/xrd/XrdPlot.svelte +337 -247
  275. package/dist/xrd/broadening.js +14 -9
  276. package/dist/xrd/calc-xrd.js +12 -18
  277. package/dist/xrd/parse.d.ts +1 -1
  278. package/dist/xrd/parse.js +17 -17
  279. package/package.json +99 -103
  280. package/readme.md +1 -1
  281. /package/dist/theme/{themes.js → themes.mjs} +0 -0
@@ -1,338 +1,475 @@
1
- <script lang="ts">import { format_num } from '../labels';
2
- import { FullscreenToggle, set_fullscreen_bg, setup_fullscreen_effect, } from '../layout';
3
- import { compute_bounding_box_2d, polygon_centroid } from '../math';
4
- import { constrain_tooltip_position } from '../plot/layout';
5
- import { scaleLinear } from 'd3-scale';
6
- import { build_diagram } from './build-diagram';
7
- import PhaseDiagramControls from './PhaseDiagramControls.svelte';
8
- import PhaseDiagramEditorPane from './PhaseDiagramEditorPane.svelte';
9
- import PhaseDiagramExportPane from './PhaseDiagramExportPane.svelte';
10
- import PhaseDiagramTooltip from './PhaseDiagramTooltip.svelte';
11
- import { parse_phase_diagram_svg } from './svg-to-diagram';
12
- import { calculate_lever_rule, calculate_vertical_lever_rule, compute_label_properties, convert_temp, find_phase_at_point, format_composition, format_formula_svg, format_hover_info_text, format_label_svg, generate_boundary_path, generate_region_path, get_multi_phase_gradient, get_phase_color, merge_phase_diagram_config, PHASE_COLOR_RGB, transform_vertices, } from './utils';
13
- let { data, config = $bindable({}), on_phase_hover, fullscreen = $bindable(false), wrapper = $bindable(), hovered_region = $bindable(null), show_boundaries = $bindable(true), show_labels = $bindable(true), show_special_points = $bindable(true), show_grid = $bindable(true), show_component_labels = $bindable(true), fullscreen_toggle = true, enable_export = true, show_controls = true, display_temp_unit = $bindable(), controls_open = $bindable(false), controls_props = {}, export_pane_open = $bindable(false), png_dpi = $bindable(150), export_filename = `phase-diagram`, lever_rule_mode = $bindable(`horizontal`), diagram_input = $bindable(null), editor_open = $bindable(false), x_axis = $bindable({}), y_axis = $bindable({}), tooltip, children, ...rest } = $props();
14
- // Shared icon/toggle styling for controls and export panes
15
- const pane_icon_style = `width: 14px; height: 14px`;
16
- const pane_toggle_props = { style: `padding: 0` };
17
- // Rebuild diagram data when diagram_input changes ($derived auto-recomputes)
18
- const rebuilt_data = $derived.by(() => {
19
- if (!diagram_input)
20
- return null;
1
+ <script lang="ts">
2
+ import { format_num } from '../labels'
3
+ import {
4
+ FullscreenToggle,
5
+ set_fullscreen_bg,
6
+ setup_fullscreen_effect,
7
+ } from '../layout'
8
+ import { sanitize_svg } from '../sanitize'
9
+ import { compute_bounding_box_2d, polygon_centroid, type Vec2 } from '../math'
10
+ import type { AxisConfig } from '../plot'
11
+ import { constrain_tooltip_position } from '../plot/layout'
12
+ import { scaleLinear } from 'd3-scale'
13
+ import type { ComponentProps, Snippet } from 'svelte'
14
+ import type { HTMLAttributes } from 'svelte/elements'
15
+ import { build_diagram } from './build-diagram'
16
+ import type { DiagramInput } from './diagram-input'
17
+ import PhaseDiagramControls from './PhaseDiagramControls.svelte'
18
+ import PhaseDiagramEditorPane from './PhaseDiagramEditorPane.svelte'
19
+ import PhaseDiagramExportPane from './PhaseDiagramExportPane.svelte'
20
+ import PhaseDiagramTooltip from './PhaseDiagramTooltip.svelte'
21
+ import { parse_phase_diagram_svg } from './svg-to-diagram'
22
+ import type {
23
+ LeverRuleMode,
24
+ PhaseDiagramConfig,
25
+ PhaseDiagramData,
26
+ PhaseDiagramTooltipConfig,
27
+ PhaseHoverInfo,
28
+ PhaseRegion,
29
+ TempUnit,
30
+ } from './types'
31
+ import {
32
+ calculate_lever_rule,
33
+ calculate_vertical_lever_rule,
34
+ compute_label_properties,
35
+ compute_x_domain,
36
+ convert_temp,
37
+ find_phase_at_point,
38
+ format_composition,
39
+ format_formula_svg,
40
+ format_hover_info_text,
41
+ format_label_svg,
42
+ generate_boundary_path,
43
+ generate_region_path,
44
+ get_multi_phase_gradient,
45
+ get_phase_color,
46
+ merge_phase_diagram_config,
47
+ PHASE_COLOR_RGB,
48
+ transform_vertices,
49
+ } from './utils'
50
+
51
+ type Props = HTMLAttributes<HTMLDivElement> & {
52
+ data: PhaseDiagramData
53
+ config?: Partial<PhaseDiagramConfig>
54
+ // Hover callback
55
+ on_phase_hover?: (info: PhaseHoverInfo | null) => void
56
+ // Bindable state
57
+ fullscreen?: boolean
58
+ wrapper?: HTMLDivElement
59
+ hovered_region?: PhaseRegion | null
60
+ // Display options
61
+ show_boundaries?: boolean
62
+ show_labels?: boolean
63
+ show_special_points?: boolean
64
+ show_grid?: boolean
65
+ show_component_labels?: boolean
66
+ fullscreen_toggle?: boolean
67
+ enable_export?: boolean
68
+ show_controls?: boolean
69
+ // Temperature display unit (can differ from data.temperature_unit)
70
+ display_temp_unit?: `K` | `°C` | `°F`
71
+ // Controls pane
72
+ controls_open?: boolean
73
+ controls_props?: Partial<ComponentProps<typeof PhaseDiagramControls>>
74
+ // Export options
75
+ export_pane_open?: boolean
76
+ png_dpi?: number
77
+ export_filename?: string
78
+ // Lever rule mode (horizontal = composition tie-line, vertical = temperature tie-line)
79
+ lever_rule_mode?: LeverRuleMode
80
+ // Diagram input editor (for SVG drop editing)
81
+ diagram_input?: DiagramInput | null
82
+ editor_open?: boolean
83
+ // Axis configuration
84
+ x_axis?: AxisConfig
85
+ y_axis?: AxisConfig
86
+ // Custom tooltip - can be a snippet (replaces default), config object (adds prefix/suffix),
87
+ // or false to disable tooltip entirely
88
+ tooltip?: Snippet<[PhaseHoverInfo]> | PhaseDiagramTooltipConfig | false
89
+ children?: Snippet<
90
+ [{ width: number; height: number; fullscreen: boolean }]
91
+ >
92
+ }
93
+
94
+ let {
95
+ data,
96
+ config = $bindable({}),
97
+ on_phase_hover,
98
+ fullscreen = $bindable(false),
99
+ wrapper = $bindable(),
100
+ hovered_region = $bindable(null),
101
+ show_boundaries = $bindable(true),
102
+ show_labels = $bindable(true),
103
+ show_special_points = $bindable(true),
104
+ show_grid = $bindable(true),
105
+ show_component_labels = $bindable(true),
106
+ fullscreen_toggle = true,
107
+ enable_export = true,
108
+ show_controls = true,
109
+ display_temp_unit = $bindable(),
110
+ controls_open = $bindable(false),
111
+ controls_props = {},
112
+ export_pane_open = $bindable(false),
113
+ png_dpi = $bindable(150),
114
+ export_filename = `phase-diagram`,
115
+ lever_rule_mode = $bindable(`horizontal`),
116
+ diagram_input = $bindable<DiagramInput | null>(null),
117
+ editor_open = $bindable(false),
118
+ x_axis = $bindable({}),
119
+ y_axis = $bindable({}),
120
+ tooltip,
121
+ children,
122
+ ...rest
123
+ }: Props = $props()
124
+
125
+ // Shared icon/toggle styling for controls and export panes
126
+ const pane_icon_style = `width: 14px; height: 14px`
127
+ const pane_toggle_props = { style: `padding: 0` }
128
+
129
+ // Rebuild diagram data when diagram_input changes ($derived auto-recomputes)
130
+ const rebuilt_data = $derived.by(() => {
131
+ if (!diagram_input) return null
21
132
  try {
22
- return build_diagram(diagram_input);
133
+ return build_diagram(diagram_input)
134
+ } catch (error) {
135
+ console.warn(`Failed to rebuild diagram from input:`, error)
136
+ return null
23
137
  }
24
- catch (err) {
25
- console.warn(`Failed to rebuild diagram from input:`, err);
26
- return null;
27
- }
28
- });
29
- // Override from direct PhaseDiagramData edits in the editor pane
30
- let data_override = $state(null);
31
- // Clear data_override when source data changes (e.g. new SVG dropped or data prop updated)
32
- $effect(() => {
33
- if (diagram_input || data)
34
- data_override = null;
35
- });
36
- // Use editor override first (clears rebuilt_data path), then rebuilt, then data prop
37
- const effective_data = $derived(data_override ?? rebuilt_data ?? data);
38
- // Handle SVG file drop directly on the component
39
- function handle_svg_drop(event) {
40
- event.preventDefault();
41
- const file = event.dataTransfer?.files[0];
138
+ })
139
+
140
+ // Override from direct PhaseDiagramData edits in the editor pane
141
+ let data_override = $state<PhaseDiagramData | null>(null)
142
+
143
+ // Clear data_override when source data changes (e.g. new SVG dropped or data prop updated)
144
+ $effect(() => {
145
+ if (diagram_input || data) data_override = null
146
+ })
147
+
148
+ // Use editor override first (clears rebuilt_data path), then rebuilt, then data prop
149
+ const effective_data = $derived(data_override ?? rebuilt_data ?? data)
150
+
151
+ // Handle SVG file drop directly on the component
152
+ function handle_svg_drop(event: DragEvent) {
153
+ event.preventDefault()
154
+ const file = event.dataTransfer?.files[0]
42
155
  if (!file || (!file.name.endsWith(`.svg`) && file.type !== `image/svg+xml`)) {
43
- return;
44
- }
45
- const reader = new FileReader();
46
- reader.onload = () => {
47
- try {
48
- diagram_input = parse_phase_diagram_svg(reader.result);
49
- }
50
- catch (err) {
51
- console.error(`Failed to parse dropped SVG:`, err);
52
- }
53
- };
54
- reader.readAsText(file);
55
- }
56
- // Merge config with centralized defaults using shared helper
57
- const merged_config = $derived(merge_phase_diagram_config(config));
58
- // Dimensions - use container size directly, no fallback to avoid layout shift
59
- let width = $state(0);
60
- let height = $state(0);
61
- // Margin from config
62
- const margin = $derived(merged_config.margin);
63
- // Pre-computed plot edges to avoid repeated calculations
64
- const left = $derived(margin.l);
65
- const right = $derived(width - margin.r);
66
- const top = $derived(margin.t);
67
- const bottom = $derived(height - margin.b);
68
- const plot_width = $derived(right - left);
69
- const plot_height = $derived(bottom - top);
70
- // Compute x domain from data extent, x_axis.range override, or default [0, 1]
71
- // Auto-extends to 0/1 when edge regions contain a pure component
72
- const x_domain = $derived.by(() => {
73
- const lo = x_axis.range?.[0];
74
- const hi = x_axis.range?.[1];
75
- if (lo != null && hi != null)
76
- return [lo, hi];
77
- if (effective_data) {
78
- // Loop-based min/max to avoid stack overflow with large datasets
79
- let data_min = Infinity;
80
- let data_max = -Infinity;
81
- const update = (val) => {
82
- if (val < data_min)
83
- data_min = val;
84
- if (val > data_max)
85
- data_max = val;
86
- };
87
- for (const region of effective_data.regions) {
88
- for (const vertex of region.vertices)
89
- update(vertex[0]);
90
- }
91
- for (const boundary of effective_data.boundaries) {
92
- for (const point of boundary.points)
93
- update(point[0]);
94
- }
95
- for (const special_point of effective_data.special_points ?? []) {
96
- update(special_point.position[0]);
97
- }
98
- if (data_min <= data_max) {
99
- let x_min = lo ?? data_min;
100
- let x_max = hi ?? data_max;
101
- // Auto-extend to 0/1 when edge regions contain a pure component AND the
102
- // data already nearly reaches the boundary. This prevents extending a
103
- // section diagram (e.g. 0.3–0.7) to the full [0, 1] range.
104
- // Word boundary regex avoids matching substrings (e.g. "Fe" won't match "Fe3C")
105
- const comp_at_edge = (comp, x_val) => {
106
- const re = new RegExp(`\\b${comp.replace(/[.*+?^${}()|[\]\\]/g, `\\$&`)}\\b`);
107
- return effective_data.regions.some((region) => re.test(region.name) &&
108
- region.vertices.some((vertex) => Math.abs(vertex[0] - x_val) < 1e-6));
109
- };
110
- if (lo == null && x_min < 0.05 &&
111
- effective_data.components[0] &&
112
- comp_at_edge(effective_data.components[0], x_min)) {
113
- x_min = 0;
114
- }
115
- if (hi == null && x_max > 0.95 &&
116
- effective_data.components[1] &&
117
- comp_at_edge(effective_data.components[1], x_max)) {
118
- x_max = 1;
119
- }
120
- return [x_min, x_max];
121
- }
156
+ return
122
157
  }
123
- return [lo ?? 0, hi ?? 1];
124
- });
125
- // Scales
126
- const x_scale = $derived(scaleLinear().domain(x_domain).range([left, right]));
127
- // Temperature units (guard for initial render when data may be undefined)
128
- const data_temp_unit = $derived((effective_data?.temperature_unit ?? `K`));
129
- const temp_unit = $derived(display_temp_unit ?? data_temp_unit);
130
- const temp_range = $derived(effective_data?.temperature_range ?? [0, 1000]);
131
- // Convert temperature range for display
132
- const display_temp_range = $derived([
158
+ const reader = new FileReader()
159
+ reader.addEventListener(`load`, () => {
160
+ try {
161
+ diagram_input = parse_phase_diagram_svg(reader.result as string)
162
+ } catch (error) {
163
+ console.error(`Failed to parse dropped SVG:`, error)
164
+ }
165
+ })
166
+ reader.readAsText(file)
167
+ }
168
+
169
+ // Merge config with centralized defaults using shared helper
170
+ const merged_config = $derived(merge_phase_diagram_config(config))
171
+
172
+ // Dimensions - use container size directly, no fallback to avoid layout shift
173
+ let width = $state(0)
174
+ let height = $state(0)
175
+
176
+ // Margin from config
177
+ const margin = $derived(merged_config.margin)
178
+
179
+ // Pre-computed plot edges to avoid repeated calculations
180
+ const left = $derived(margin.l)
181
+ const right = $derived(width - margin.r)
182
+ const top = $derived(margin.t)
183
+ const bottom = $derived(height - margin.b)
184
+ const plot_width = $derived(right - left)
185
+ const plot_height = $derived(bottom - top)
186
+
187
+ // Compute x domain from data extent, x_axis.range override, or default [0, 1]
188
+ // Auto-extends to 0/1 when edge regions contain a pure component
189
+ const x_domain = $derived(compute_x_domain(x_axis.range, effective_data))
190
+
191
+ // Scales
192
+ const x_scale = $derived(scaleLinear().domain(x_domain).range([left, right]))
193
+
194
+ // Temperature units (guard for initial render when data may be undefined)
195
+ const data_temp_unit = $derived<TempUnit>(
196
+ (effective_data?.temperature_unit ?? `K`) as TempUnit,
197
+ )
198
+ const temp_unit = $derived<TempUnit>(display_temp_unit ?? data_temp_unit)
199
+ const temp_range = $derived(effective_data?.temperature_range ?? [0, 1000])
200
+
201
+ // Convert temperature range for display
202
+ const display_temp_range = $derived<Vec2>([
133
203
  convert_temp(temp_range[0], data_temp_unit, temp_unit),
134
204
  convert_temp(temp_range[1], data_temp_unit, temp_unit),
135
- ]);
136
- // y_scale maps data temperatures to SVG coordinates
137
- // We keep this in data units so region vertices render correctly
138
- const y_scale = $derived(scaleLinear().domain(temp_range).range([bottom, top]));
139
- // y_scale_display maps display temperatures (after unit conversion) to SVG
140
- // Used for axis labels and ticks
141
- const y_scale_display = $derived(scaleLinear().domain(display_temp_range).range([bottom, top]));
142
- // Generate tick values using d3 scale's built-in ticks method
143
- const x_ticks = $derived(x_scale.ticks(typeof x_axis.ticks === `number` ? x_axis.ticks : 5));
144
- // Use display scale for y ticks so they show converted temperatures
145
- const y_ticks = $derived(y_scale_display.ticks(typeof y_axis.ticks === `number` ? y_axis.ticks : 6));
146
- // Transform regions to SVG coordinates
147
- const transformed_regions = $derived((effective_data?.regions ?? []).map((region) => {
148
- const svg_vertices = transform_vertices(region.vertices, x_scale, y_scale);
149
- const { width, height } = compute_bounding_box_2d(svg_vertices);
150
- const label_props = compute_label_properties(region.name, { width, height }, merged_config.font_size);
151
- // Get gradient stops for multi-phase regions (2+, supports 3+ phases)
152
- const gradient = get_multi_phase_gradient(region.name);
153
- const x_coords = svg_vertices.map(([vx]) => vx);
154
- return {
205
+ ])
206
+
207
+ // y_scale maps data temperatures to SVG coordinates
208
+ // We keep this in data units so region vertices render correctly
209
+ const y_scale = $derived(
210
+ scaleLinear().domain(temp_range).range([bottom, top]),
211
+ )
212
+
213
+ // y_scale_display maps display temperatures (after unit conversion) to SVG
214
+ // Used for axis labels and ticks
215
+ const y_scale_display = $derived(
216
+ scaleLinear().domain(display_temp_range).range([bottom, top]),
217
+ )
218
+
219
+ // Generate tick values using d3 scale's built-in ticks method
220
+ const x_ticks = $derived(
221
+ x_scale.ticks(typeof x_axis.ticks === `number` ? x_axis.ticks : 5),
222
+ )
223
+ // Use display scale for y ticks so they show converted temperatures
224
+ const y_ticks = $derived(
225
+ y_scale_display.ticks(typeof y_axis.ticks === `number` ? y_axis.ticks : 6),
226
+ )
227
+
228
+ // Transform regions to SVG coordinates
229
+ const transformed_regions = $derived(
230
+ (effective_data?.regions ?? []).map((region) => {
231
+ const svg_vertices = transform_vertices(region.vertices, x_scale, y_scale)
232
+ const { width, height } = compute_bounding_box_2d(svg_vertices)
233
+ const label_props = compute_label_properties(
234
+ region.name,
235
+ { width, height },
236
+ merged_config.font_size,
237
+ )
238
+ // Get gradient stops for multi-phase regions (2+, supports 3+ phases)
239
+ const gradient = get_multi_phase_gradient(region.name)
240
+ const x_coords = svg_vertices.map(([vx]) => vx)
241
+ return {
155
242
  ...region,
156
243
  svg_path: generate_region_path(svg_vertices),
157
244
  label_pos: region.label_position
158
- ? [x_scale(region.label_position[0]), y_scale(region.label_position[1])]
159
- : polygon_centroid(svg_vertices),
245
+ ? [x_scale(region.label_position[0]), y_scale(region.label_position[1])]
246
+ : polygon_centroid(svg_vertices),
160
247
  label_rotation: label_props.rotation,
161
248
  label_lines: label_props.lines,
162
249
  label_scale: label_props.scale,
163
250
  gradient,
164
251
  x_min: Math.min(...x_coords),
165
252
  x_max: Math.max(...x_coords),
166
- };
167
- }));
168
- // Transform boundaries to SVG coordinates
169
- const transformed_boundaries = $derived((effective_data?.boundaries ?? []).map((boundary) => ({
170
- ...boundary,
171
- svg_path: generate_boundary_path(transform_vertices(boundary.points, x_scale, y_scale)),
172
- })));
173
- // Transform special points to SVG coordinates
174
- const transformed_special_points = $derived((effective_data?.special_points ?? []).map((point) => ({
175
- ...point,
176
- svg_x: x_scale(point.position[0]),
177
- svg_y: y_scale(point.position[1]),
178
- })));
179
- // Hover state
180
- let hover_info = $state(null);
181
- // Locked tooltip state (click to lock, click again to unlock)
182
- let locked_hover_info = $state(null);
183
- // Clear hover state helper (used in multiple places)
184
- function clear_hover() {
185
- hover_info = null;
186
- hovered_region = null;
187
- on_phase_hover?.(null);
188
- }
189
- // Handle click to lock/unlock tooltip
190
- function handle_click() {
253
+ }
254
+ }),
255
+ )
256
+
257
+ // Transform boundaries to SVG coordinates
258
+ const transformed_boundaries = $derived(
259
+ (effective_data?.boundaries ?? []).map((boundary) => ({
260
+ ...boundary,
261
+ svg_path: generate_boundary_path(
262
+ transform_vertices(boundary.points, x_scale, y_scale),
263
+ ),
264
+ })),
265
+ )
266
+
267
+ // Transform special points to SVG coordinates
268
+ const transformed_special_points = $derived(
269
+ (effective_data?.special_points ?? []).map((point) => ({
270
+ ...point,
271
+ svg_x: x_scale(point.position[0]),
272
+ svg_y: y_scale(point.position[1]),
273
+ })),
274
+ )
275
+
276
+ // Hover state
277
+ let hover_info = $state<PhaseHoverInfo | null>(null)
278
+ // Locked tooltip state (click to lock, click again to unlock)
279
+ let locked_hover_info = $state<PhaseHoverInfo | null>(null)
280
+
281
+ // Clear hover state helper (used in multiple places)
282
+ function clear_hover() {
283
+ hover_info = null
284
+ hovered_region = null
285
+ on_phase_hover?.(null)
286
+ }
287
+
288
+ // Handle click to lock/unlock tooltip
289
+ function handle_click() {
191
290
  if (locked_hover_info) {
192
- // Unlock if already locked
193
- locked_hover_info = null;
194
- }
195
- else if (hover_info) {
196
- // Lock current hover info
197
- locked_hover_info = { ...hover_info };
291
+ // Unlock if already locked
292
+ locked_hover_info = null
293
+ } else if (hover_info) {
294
+ // Lock current hover info
295
+ locked_hover_info = { ...hover_info }
198
296
  }
199
- }
200
- // Effective hover info - locked takes precedence
201
- const effective_hover_info = $derived(locked_hover_info ?? hover_info);
202
- // Copy feedback state
203
- let copy_feedback_visible = $state(false);
204
- let copy_feedback_pos = $state({ x: 0, y: 0 });
205
- let copy_feedback_timeout;
206
- // Handle double-click to copy tooltip data
207
- async function handle_double_click(event) {
208
- if (!hover_info)
209
- return;
297
+ }
298
+
299
+ // Effective hover info - locked takes precedence
300
+ const effective_hover_info = $derived(locked_hover_info ?? hover_info)
301
+
302
+ // Copy feedback state
303
+ let copy_feedback_visible = $state(false)
304
+ let copy_feedback_pos = $state({ x: 0, y: 0 })
305
+ let copy_feedback_timeout: ReturnType<typeof setTimeout> | undefined
306
+
307
+ // Handle double-click to copy tooltip data
308
+ async function handle_double_click(event: MouseEvent) {
309
+ if (!hover_info) return
210
310
  try {
211
- await navigator.clipboard.writeText(format_hover_info_text(hover_info, temp_unit, comp_unit, component_a, component_b, data_temp_unit, lever_rule_mode));
212
- if (copy_feedback_timeout)
213
- clearTimeout(copy_feedback_timeout);
214
- copy_feedback_pos = { x: event.clientX, y: event.clientY };
215
- copy_feedback_visible = true;
216
- copy_feedback_timeout = setTimeout(() => {
217
- copy_feedback_visible = false;
218
- copy_feedback_timeout = undefined;
219
- }, 1500);
311
+ await navigator.clipboard.writeText(
312
+ format_hover_info_text(
313
+ hover_info,
314
+ temp_unit,
315
+ comp_unit,
316
+ component_a,
317
+ component_b,
318
+ data_temp_unit,
319
+ lever_rule_mode,
320
+ ),
321
+ )
322
+ if (copy_feedback_timeout) clearTimeout(copy_feedback_timeout)
323
+ copy_feedback_pos = { x: event.clientX, y: event.clientY }
324
+ copy_feedback_visible = true
325
+ copy_feedback_timeout = setTimeout(() => {
326
+ copy_feedback_visible = false
327
+ copy_feedback_timeout = undefined
328
+ }, 1500)
329
+ } catch (error) {
330
+ console.error(`Failed to copy phase data:`, error)
220
331
  }
221
- catch (err) {
222
- console.error(`Failed to copy phase data:`, err);
223
- }
224
- }
225
- // Tooltip element reference for measuring actual size
226
- let tooltip_el = $state(null);
227
- // Tooltip positioning using shared utility (uses effective_hover_info for locked state)
228
- const tooltip_pos = $derived.by(() => {
229
- const info = effective_hover_info;
230
- if (!info)
231
- return { x: 0, y: 0 };
232
- return constrain_tooltip_position(info.position.x, info.position.y, tooltip_el?.offsetWidth ?? 200, tooltip_el?.offsetHeight ?? 150, globalThis.innerWidth ?? 1000, globalThis.innerHeight ?? 800, { offset: 15 });
233
- });
234
- // Find nearest special point within threshold (in SVG pixels)
235
- function find_nearby_special_point(svg_x, svg_y, threshold = 20) {
236
- let nearest = null;
237
- let min_dist = threshold;
332
+ }
333
+
334
+ // Tooltip element reference for measuring actual size
335
+ let tooltip_el = $state<HTMLDivElement | null>(null)
336
+
337
+ // Tooltip positioning using shared utility (uses effective_hover_info for locked state)
338
+ const tooltip_pos = $derived.by(() => {
339
+ const info = effective_hover_info
340
+ if (!info) return { x: 0, y: 0 }
341
+ return constrain_tooltip_position(
342
+ info.position.x,
343
+ info.position.y,
344
+ tooltip_el?.offsetWidth ?? 200,
345
+ tooltip_el?.offsetHeight ?? 150,
346
+ globalThis.innerWidth ?? 1000,
347
+ globalThis.innerHeight ?? 800,
348
+ { offset: 15 },
349
+ )
350
+ })
351
+
352
+ // Find nearest special point within threshold (in SVG pixels)
353
+ function find_nearby_special_point(
354
+ svg_x: number,
355
+ svg_y: number,
356
+ threshold: number = 20,
357
+ ) {
358
+ let nearest: (typeof transformed_special_points)[0] | null = null
359
+ let min_dist = threshold
238
360
  for (const point of transformed_special_points) {
239
- const dist = Math.hypot(point.svg_x - svg_x, point.svg_y - svg_y);
240
- if (dist < min_dist) {
241
- min_dist = dist;
242
- nearest = point;
243
- }
361
+ const dist = Math.hypot(point.svg_x - svg_x, point.svg_y - svg_y)
362
+ if (dist < min_dist) {
363
+ min_dist = dist
364
+ nearest = point
365
+ }
244
366
  }
245
- return nearest;
246
- }
247
- // Pointer move handler (unified mouse/touch via Pointer Events API)
248
- function handle_pointer_move(event) {
249
- const svg = event.currentTarget;
250
- const rect = svg.getBoundingClientRect();
251
- const svg_x = event.clientX - rect.left;
252
- const svg_y = event.clientY - rect.top;
367
+ return nearest
368
+ }
369
+
370
+ // Pointer move handler (unified mouse/touch via Pointer Events API)
371
+ function handle_pointer_move(event: PointerEvent) {
372
+ const svg = event.currentTarget as SVGElement
373
+ const rect = svg.getBoundingClientRect()
374
+ const svg_x = event.clientX - rect.left
375
+ const svg_y = event.clientY - rect.top
376
+
253
377
  // Check if within plot area
254
- if (svg_x < left || svg_x > right || svg_y < top || svg_y > bottom ||
255
- !effective_data) {
256
- clear_hover();
257
- return;
378
+ if (
379
+ svg_x < left || svg_x > right || svg_y < top || svg_y > bottom ||
380
+ !effective_data
381
+ ) {
382
+ clear_hover()
383
+ return
258
384
  }
385
+
259
386
  // Convert to data coordinates and find phase
260
- const composition = x_scale.invert(svg_x);
261
- const temperature = y_scale.invert(svg_y);
262
- const region = find_phase_at_point(composition, temperature, effective_data);
387
+ const composition = x_scale.invert(svg_x)
388
+ const temperature = y_scale.invert(svg_y)
389
+ const region = find_phase_at_point(composition, temperature, effective_data)
390
+
263
391
  // Check for nearby special point
264
392
  const nearby_special = show_special_points
265
- ? find_nearby_special_point(svg_x, svg_y)
266
- : null;
393
+ ? find_nearby_special_point(svg_x, svg_y)
394
+ : null
395
+
267
396
  if (region) {
268
- hovered_region = region;
269
- hover_info = {
270
- region,
271
- composition,
272
- temperature,
273
- position: { x: event.clientX, y: event.clientY },
274
- lever_rule: calculate_lever_rule(region, composition, temperature) ||
275
- undefined,
276
- vertical_lever_rule: calculate_vertical_lever_rule(region, composition, temperature) ||
277
- undefined,
278
- special_point: nearby_special || undefined,
279
- };
280
- on_phase_hover?.(hover_info);
281
- }
282
- else {
283
- clear_hover();
397
+ hovered_region = region
398
+ hover_info = {
399
+ region,
400
+ composition,
401
+ temperature,
402
+ position: { x: event.clientX, y: event.clientY },
403
+ lever_rule: calculate_lever_rule(region, composition, temperature) ||
404
+ undefined,
405
+ vertical_lever_rule:
406
+ calculate_vertical_lever_rule(region, composition, temperature) ||
407
+ undefined,
408
+ special_point: nearby_special || undefined,
409
+ }
410
+ on_phase_hover?.(hover_info)
411
+ } else {
412
+ clear_hover()
284
413
  }
285
- }
286
- function handle_pointer_leave(event) {
414
+ }
415
+
416
+ function handle_pointer_leave(event: PointerEvent) {
287
417
  // Don't clear on touch lift (allows reading tooltip) or when locked
288
- if (event.pointerType === `touch` || locked_hover_info)
289
- return;
290
- clear_hover();
291
- }
292
- // Document-level keyboard shortcuts
293
- function handle_doc_keydown(event) {
418
+ if (event.pointerType === `touch` || locked_hover_info) return
419
+ clear_hover()
420
+ }
421
+
422
+ // Document-level keyboard shortcuts
423
+ function handle_doc_keydown(event: KeyboardEvent) {
294
424
  if ((event.ctrlKey || event.metaKey) && event.shiftKey && event.key === `E`) {
295
- event.preventDefault();
296
- export_pane_open = !export_pane_open;
425
+ event.preventDefault()
426
+ export_pane_open = !export_pane_open
427
+ } else if (event.key === `Escape` && locked_hover_info) {
428
+ locked_hover_info = null
297
429
  }
298
- else if (event.key === `Escape` && locked_hover_info) {
299
- locked_hover_info = null;
300
- }
301
- }
302
- // SVG keyboard handler (Enter/Space to toggle lock)
303
- function handle_svg_keydown(event) {
430
+ }
431
+
432
+ // SVG keyboard handler (Enter/Space to toggle lock)
433
+ function handle_svg_keydown(event: KeyboardEvent) {
304
434
  if (event.key === `Enter` || event.key === ` `) {
305
- event.preventDefault();
306
- handle_click();
435
+ event.preventDefault()
436
+ handle_click()
307
437
  }
308
- }
309
- // Fullscreen handling
310
- $effect(() => {
311
- setup_fullscreen_effect(fullscreen, wrapper);
312
- set_fullscreen_bg(wrapper, fullscreen, `--phase-diagram-bg-fullscreen`);
313
- });
314
- // Cleanup timeout on unmount to prevent memory leaks
315
- $effect(() => {
438
+ }
439
+
440
+ // Fullscreen handling
441
+ $effect(() => {
442
+ setup_fullscreen_effect(fullscreen, wrapper)
443
+ set_fullscreen_bg(wrapper, fullscreen, `--phase-diagram-bg-fullscreen`)
444
+ })
445
+
446
+ // Cleanup timeout on unmount to prevent memory leaks
447
+ $effect(() => {
316
448
  return () => {
317
- if (copy_feedback_timeout)
318
- clearTimeout(copy_feedback_timeout);
319
- };
320
- });
321
- // Component labels (guard for initial render when data may be undefined)
322
- const component_a = $derived(effective_data?.components?.[0] ?? ``);
323
- const component_b = $derived(effective_data?.components?.[1] ?? ``);
324
- const comp_unit = $derived(effective_data?.composition_unit ?? `at%`);
325
- // Pseudo-binary support: format compound names with subscripts when enabled
326
- const use_subscripts = $derived(effective_data?.pseudo_binary?.use_subscripts ?? true);
327
- // Formatted component labels for SVG axis labels (with tspan subscripts if compound)
328
- const component_a_svg = $derived(format_formula_svg(component_a, use_subscripts));
329
- const component_b_svg = $derived(format_formula_svg(component_b, use_subscripts));
330
- // Default x-axis label as a single string (avoids mixing plain text with {@html})
331
- const default_x_axis_label = $derived.by(() => {
332
- const prefix = comp_unit === `fraction` ? `x ` : ``;
333
- const unit = comp_unit === `fraction` ? `mole fraction` : comp_unit;
334
- return `${prefix}${component_b_svg} (${unit})`;
335
- });
449
+ if (copy_feedback_timeout) clearTimeout(copy_feedback_timeout)
450
+ }
451
+ })
452
+
453
+ // Component labels (guard for initial render when data may be undefined)
454
+ const component_a = $derived(effective_data?.components?.[0] ?? ``)
455
+ const component_b = $derived(effective_data?.components?.[1] ?? ``)
456
+ const comp_unit = $derived(effective_data?.composition_unit ?? `at%`)
457
+
458
+ // Pseudo-binary support: format compound names with subscripts when enabled
459
+ const use_subscripts = $derived(
460
+ effective_data?.pseudo_binary?.use_subscripts ?? true,
461
+ )
462
+
463
+ // Formatted component labels for SVG axis labels (with tspan subscripts if compound)
464
+ const component_a_svg = $derived(format_formula_svg(component_a, use_subscripts))
465
+ const component_b_svg = $derived(format_formula_svg(component_b, use_subscripts))
466
+
467
+ // Default x-axis label as a single string (avoids mixing plain text with {@html})
468
+ const default_x_axis_label = $derived.by(() => {
469
+ const prefix = comp_unit === `fraction` ? `x ` : ``
470
+ const unit = comp_unit === `fraction` ? `mole fraction` : comp_unit
471
+ return `${prefix}${component_b_svg} (${unit})`
472
+ })
336
473
  </script>
337
474
 
338
475
  <!-- Grid lines snippet for DRY rendering -->
@@ -568,7 +705,7 @@ const default_x_axis_label = $derived.by(() => {
568
705
  font-weight="500"
569
706
  class="region-label"
570
707
  >
571
- {@html format_label_svg(line, use_subscripts)}
708
+ {@html sanitize_svg(format_label_svg(line, use_subscripts))}
572
709
  </text>
573
710
  {/each}
574
711
  </g>
@@ -686,11 +823,11 @@ const default_x_axis_label = $derived.by(() => {
686
823
  font-size={merged_config.font_size + 2}
687
824
  >
688
825
  {#if x_axis.label}
689
- {@html x_axis.label}
826
+ {@html sanitize_svg(x_axis.label)}
690
827
  {:else if effective_data?.x_axis_label}
691
- {@html effective_data.x_axis_label}
828
+ {@html sanitize_svg(effective_data.x_axis_label)}
692
829
  {:else}
693
- {@html default_x_axis_label}
830
+ {@html sanitize_svg(default_x_axis_label)}
694
831
  {/if}
695
832
  </text>
696
833
  </g>
@@ -729,9 +866,9 @@ const default_x_axis_label = $derived.by(() => {
729
866
  font-size={merged_config.font_size + 2}
730
867
  >
731
868
  {#if y_axis.label}
732
- {@html y_axis.label}
869
+ {@html sanitize_svg(y_axis.label)}
733
870
  {:else if effective_data?.y_axis_label}
734
- {@html effective_data.y_axis_label}
871
+ {@html sanitize_svg(effective_data.y_axis_label)}
735
872
  {:else}
736
873
  Temperature ({temp_unit})
737
874
  {/if}
@@ -748,7 +885,7 @@ const default_x_axis_label = $derived.by(() => {
748
885
  font-size={merged_config.font_size + 2}
749
886
  font-weight="bold"
750
887
  >
751
- {@html component_a_svg}
888
+ {@html sanitize_svg(component_a_svg)}
752
889
  </text>
753
890
  <text
754
891
  x={right}
@@ -758,7 +895,7 @@ const default_x_axis_label = $derived.by(() => {
758
895
  font-size={merged_config.font_size + 2}
759
896
  font-weight="bold"
760
897
  >
761
- {@html component_b_svg}
898
+ {@html sanitize_svg(component_b_svg)}
762
899
  </text>
763
900
  {/if}
764
901
  </svg>