matterviz 0.4.2 → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (244) hide show
  1. package/dist/FilePicker.svelte +40 -50
  2. package/dist/api/optimade.js +10 -7
  3. package/dist/app.css +15 -0
  4. package/dist/brillouin/BrillouinZone.svelte +18 -9
  5. package/dist/brillouin/BrillouinZoneScene.svelte +20 -29
  6. package/dist/brillouin/compute.js +10 -16
  7. package/dist/brillouin/geometry.js +2 -2
  8. package/dist/chempot-diagram/ChemPotDiagram.svelte +18 -15
  9. package/dist/chempot-diagram/ChemPotDiagram2D.svelte +17 -113
  10. package/dist/chempot-diagram/ChemPotDiagram3D.svelte +104 -264
  11. package/dist/chempot-diagram/async-compute.svelte.js +12 -13
  12. package/dist/chempot-diagram/color.d.ts +21 -1
  13. package/dist/chempot-diagram/color.js +70 -1
  14. package/dist/chempot-diagram/compute.d.ts +7 -0
  15. package/dist/chempot-diagram/compute.js +85 -26
  16. package/dist/chempot-diagram/export.js +1 -6
  17. package/dist/chempot-diagram/temperature.js +9 -10
  18. package/dist/composition/BarChart.svelte +7 -9
  19. package/dist/composition/Composition.svelte +3 -8
  20. package/dist/composition/Formula.svelte +2 -2
  21. package/dist/composition/FormulaFilter.svelte +27 -93
  22. package/dist/composition/format.js +6 -8
  23. package/dist/composition/parse.js +50 -105
  24. package/dist/constants.d.ts +0 -1
  25. package/dist/constants.js +6 -17
  26. package/dist/convex-hull/ConvexHull2D.svelte +0 -2
  27. package/dist/convex-hull/ConvexHull3D.svelte +19 -47
  28. package/dist/convex-hull/ConvexHull4D.svelte +65 -95
  29. package/dist/convex-hull/StructurePopup.svelte +1 -1
  30. package/dist/convex-hull/gas-thermodynamics.js +21 -53
  31. package/dist/convex-hull/helpers.js +1 -7
  32. package/dist/convex-hull/thermodynamics.js +19 -27
  33. package/dist/coordination/CoordinationBarPlot.svelte +12 -21
  34. package/dist/element/BohrAtom.svelte +0 -1
  35. package/dist/element/ElementTile.svelte +26 -44
  36. package/dist/element/data.d.ts +2 -0
  37. package/dist/element/data.js +5 -1
  38. package/dist/element/index.d.ts +1 -1
  39. package/dist/element/index.js +1 -1
  40. package/dist/fermi-surface/FermiSurface.svelte +22 -23
  41. package/dist/fermi-surface/FermiSurface.svelte.d.ts +2 -6
  42. package/dist/fermi-surface/FermiSurfaceScene.svelte +32 -77
  43. package/dist/fermi-surface/compute.js +22 -55
  44. package/dist/fermi-surface/parse.js +38 -70
  45. package/dist/file-viewer/JsonBrowser.svelte +1043 -0
  46. package/dist/file-viewer/JsonBrowser.svelte.d.ts +9 -0
  47. package/dist/file-viewer/PlotPanel.svelte +350 -0
  48. package/dist/file-viewer/PlotPanel.svelte.d.ts +9 -0
  49. package/dist/file-viewer/TrajectoryWithDos.svelte +30 -0
  50. package/dist/file-viewer/TrajectoryWithDos.svelte.d.ts +10 -0
  51. package/dist/file-viewer/detect.d.ts +11 -0
  52. package/dist/file-viewer/detect.js +381 -0
  53. package/dist/file-viewer/eligibility.d.ts +4 -0
  54. package/dist/file-viewer/eligibility.js +53 -0
  55. package/dist/file-viewer/host-protocol.d.ts +50 -0
  56. package/dist/file-viewer/host-transfer.d.ts +25 -0
  57. package/dist/file-viewer/host-transfer.js +42 -0
  58. package/dist/file-viewer/index.d.ts +5 -0
  59. package/dist/file-viewer/index.js +5 -0
  60. package/dist/file-viewer/main.d.ts +44 -0
  61. package/dist/file-viewer/main.js +535 -0
  62. package/dist/file-viewer/parse.d.ts +9 -0
  63. package/dist/file-viewer/parse.js +170 -0
  64. package/dist/file-viewer/plot-utils.d.ts +24 -0
  65. package/dist/file-viewer/plot-utils.js +191 -0
  66. package/dist/file-viewer/types.d.ts +4 -0
  67. package/dist/file-viewer/types.js +5 -0
  68. package/dist/heatmap-matrix/HeatmapMatrix.svelte +57 -84
  69. package/dist/heatmap-matrix/HeatmapMatrixControls.svelte.d.ts +1 -1
  70. package/dist/index.d.ts +2 -2
  71. package/dist/index.js +1 -1
  72. package/dist/io/ExportPane.svelte +1 -1
  73. package/dist/io/decompress.d.ts +2 -0
  74. package/dist/io/decompress.js +3 -2
  75. package/dist/io/export.d.ts +7 -4
  76. package/dist/io/export.js +70 -18
  77. package/dist/io/file-drop.js +60 -35
  78. package/dist/io/url-drop.d.ts +2 -0
  79. package/dist/io/url-drop.js +28 -16
  80. package/dist/isosurface/Isosurface.svelte +687 -196
  81. package/dist/isosurface/Isosurface.svelte.d.ts +7 -1
  82. package/dist/isosurface/IsosurfaceControls.svelte +564 -85
  83. package/dist/isosurface/IsosurfaceControls.svelte.d.ts +1 -1
  84. package/dist/isosurface/VolumeSlice.svelte +224 -0
  85. package/dist/isosurface/VolumeSlice.svelte.d.ts +28 -0
  86. package/dist/isosurface/coloring.d.ts +20 -0
  87. package/dist/isosurface/coloring.js +118 -0
  88. package/dist/isosurface/geometry-worker-types.d.ts +38 -0
  89. package/dist/isosurface/geometry.worker.d.ts +1 -0
  90. package/dist/isosurface/geometry.worker.js +59 -0
  91. package/dist/isosurface/grid.d.ts +10 -0
  92. package/dist/isosurface/grid.js +31 -0
  93. package/dist/isosurface/index.d.ts +4 -0
  94. package/dist/isosurface/index.js +4 -0
  95. package/dist/isosurface/profile.d.ts +12 -0
  96. package/dist/isosurface/profile.js +19 -0
  97. package/dist/isosurface/sampling.d.ts +34 -0
  98. package/dist/isosurface/sampling.js +408 -0
  99. package/dist/isosurface/slice-rendering.d.ts +13 -0
  100. package/dist/isosurface/slice-rendering.js +79 -0
  101. package/dist/isosurface/slice.d.ts +22 -2
  102. package/dist/isosurface/slice.js +200 -116
  103. package/dist/isosurface/types.d.ts +24 -1
  104. package/dist/isosurface/types.js +120 -8
  105. package/dist/json-path.d.ts +4 -0
  106. package/dist/json-path.js +89 -0
  107. package/dist/labels.js +1 -1
  108. package/dist/layout/InfoCard.svelte +1 -3
  109. package/dist/layout/SettingsSection.svelte +52 -61
  110. package/dist/layout/SettingsSection.svelte.d.ts +6 -7
  111. package/dist/layout/ViewerChrome.svelte +7 -1
  112. package/dist/layout/json-tree/JsonNode.svelte +14 -48
  113. package/dist/layout/json-tree/JsonTree.svelte +48 -95
  114. package/dist/layout/json-tree/JsonValue.svelte +3 -18
  115. package/dist/layout/json-tree/utils.d.ts +0 -3
  116. package/dist/layout/json-tree/utils.js +50 -179
  117. package/dist/marching-cubes.d.ts +8 -1
  118. package/dist/marching-cubes.js +115 -48
  119. package/dist/math.d.ts +3 -2
  120. package/dist/math.js +29 -38
  121. package/dist/overlays/ContextMenu.svelte +15 -32
  122. package/dist/overlays/DraggablePane.svelte +39 -58
  123. package/dist/overlays/DraggablePane.svelte.d.ts +1 -1
  124. package/dist/overlays/GlassChip.svelte +3 -1
  125. package/dist/periodic-table/PeriodicTableControls.svelte +26 -52
  126. package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte +18 -34
  127. package/dist/phase-diagram/build-diagram.js +1 -6
  128. package/dist/phase-diagram/svg-to-diagram.js +9 -14
  129. package/dist/plot/bar/BarPlot.svelte +38 -42
  130. package/dist/plot/bar/SpacegroupBarPlot.svelte +18 -42
  131. package/dist/plot/box/BoxPlot.svelte +27 -23
  132. package/dist/plot/core/auto-place.d.ts +1 -1
  133. package/dist/plot/core/auto-place.js +1 -1
  134. package/dist/plot/core/data-transform.d.ts +1 -2
  135. package/dist/plot/core/data-transform.js +0 -10
  136. package/dist/plot/core/layout.d.ts +12 -5
  137. package/dist/plot/core/layout.js +37 -27
  138. package/dist/plot/core/pan-zoom.svelte.d.ts +2 -4
  139. package/dist/plot/core/pan-zoom.svelte.js +34 -17
  140. package/dist/plot/core/placed-tween.svelte.d.ts +3 -0
  141. package/dist/plot/core/placed-tween.svelte.js +68 -21
  142. package/dist/plot/core/svg.js +0 -1
  143. package/dist/plot/core/utils/hierarchy-chart.d.ts +1 -0
  144. package/dist/plot/core/utils/hierarchy-chart.js +12 -0
  145. package/dist/plot/core/utils/hierarchy-labels.js +3 -2
  146. package/dist/plot/histogram/Histogram.svelte +21 -26
  147. package/dist/plot/sankey/sankey.js +13 -9
  148. package/dist/plot/scatter/BinnedScatterPlot.svelte +59 -43
  149. package/dist/plot/scatter/ScatterPlot.svelte +67 -52
  150. package/dist/plot/scatter/adaptive-density.d.ts +1 -1
  151. package/dist/plot/scatter/adaptive-density.js +36 -20
  152. package/dist/plot/sunburst/Sunburst.svelte +20 -20
  153. package/dist/plot/treemap/Treemap.svelte +175 -60
  154. package/dist/plot/treemap/Treemap.svelte.d.ts +6 -0
  155. package/dist/plot/treemap/index.d.ts +1 -0
  156. package/dist/plot/treemap/labels.d.ts +38 -0
  157. package/dist/plot/treemap/labels.js +80 -0
  158. package/dist/plot/treemap/treemap.d.ts +4 -2
  159. package/dist/plot/treemap/treemap.js +5 -2
  160. package/dist/rdf/RdfPlot.svelte +17 -35
  161. package/dist/rdf/calc-rdf.js +59 -44
  162. package/dist/rdf/index.d.ts +0 -1
  163. package/dist/sanitize.js +84 -26
  164. package/dist/scene/SceneCamera.svelte +1 -1
  165. package/dist/scene/SceneCamera.svelte.d.ts +1 -1
  166. package/dist/scene/index.d.ts +1 -2
  167. package/dist/scene/index.js +1 -1
  168. package/dist/scene/{props.js → props.svelte.js} +15 -1
  169. package/dist/settings.js +159 -482
  170. package/dist/spectral/Bands.svelte +151 -158
  171. package/dist/spectral/BrillouinBandsDos.svelte +4 -19
  172. package/dist/spectral/Dos.svelte +26 -24
  173. package/dist/spectral/helpers.js +40 -105
  174. package/dist/structure/AtomLegend.svelte +8 -13
  175. package/dist/structure/AtomLegend.svelte.d.ts +2 -3
  176. package/dist/structure/CellSelect.svelte +35 -10
  177. package/dist/structure/Structure.svelte +177 -99
  178. package/dist/structure/Structure.svelte.d.ts +1 -1
  179. package/dist/structure/StructureCarousel.svelte +10 -3
  180. package/dist/structure/StructureControls.svelte +8 -26
  181. package/dist/structure/StructureControls.svelte.d.ts +1 -1
  182. package/dist/structure/StructureInfoPane.svelte +65 -76
  183. package/dist/structure/StructureScene.svelte +75 -109
  184. package/dist/structure/StructureScene.svelte.d.ts +3 -1
  185. package/dist/structure/StructureViewport.svelte +120 -55
  186. package/dist/structure/StructureViewport.svelte.d.ts +3 -1
  187. package/dist/structure/atom-properties.d.ts +1 -1
  188. package/dist/structure/atom-properties.js +4 -3
  189. package/dist/structure/bond-order-perception.js +3 -8
  190. package/dist/structure/bonding.d.ts +0 -1
  191. package/dist/structure/bonding.js +16 -23
  192. package/dist/structure/export.js +50 -74
  193. package/dist/structure/format-detect.js +5 -7
  194. package/dist/structure/index.js +6 -12
  195. package/dist/structure/parse.js +38 -100
  196. package/dist/structure/partial-occupancy.js +7 -4
  197. package/dist/structure/pbc.js +8 -10
  198. package/dist/structure/polyhedra.js +5 -4
  199. package/dist/structure/prop-groups.d.ts +85 -0
  200. package/dist/structure/prop-groups.js +18 -0
  201. package/dist/symmetry/SymmetryStats.svelte +0 -4
  202. package/dist/symmetry/WyckoffTable.svelte +5 -4
  203. package/dist/symmetry/cell-transform.js +5 -14
  204. package/dist/symmetry/index.js +6 -9
  205. package/dist/table/HeatmapTable.svelte +61 -103
  206. package/dist/table/ToggleMenu.svelte +166 -44
  207. package/dist/table/ToggleMenu.svelte.d.ts +1 -1
  208. package/dist/theme/embedded.d.ts +18 -0
  209. package/dist/theme/embedded.js +206 -0
  210. package/dist/theme/index.d.ts +4 -0
  211. package/dist/theme/index.js +1 -2
  212. package/dist/theme/themes.mjs +31 -60
  213. package/dist/tooltip/TooltipContent.svelte +1 -1
  214. package/dist/tooltip/TooltipContent.svelte.d.ts +1 -1
  215. package/dist/tooltip/index.d.ts +8 -1
  216. package/dist/trajectory/Trajectory.svelte +140 -120
  217. package/dist/trajectory/TrajectoryError.svelte +1 -66
  218. package/dist/trajectory/TrajectoryExportPane.svelte +0 -1
  219. package/dist/trajectory/TrajectoryInfoPane.svelte +55 -80
  220. package/dist/trajectory/TrajectoryInfoPane.svelte.d.ts +2 -1
  221. package/dist/trajectory/extract.js +19 -29
  222. package/dist/trajectory/format-detect.d.ts +2 -0
  223. package/dist/trajectory/format-detect.js +7 -5
  224. package/dist/trajectory/frame-reader.js +3 -4
  225. package/dist/trajectory/helpers.js +16 -49
  226. package/dist/trajectory/index.js +2 -8
  227. package/dist/trajectory/parse/ase.js +1 -1
  228. package/dist/trajectory/parse/index.d.ts +4 -3
  229. package/dist/trajectory/parse/index.js +12 -17
  230. package/dist/trajectory/parse/vaspout-h5.d.ts +0 -1
  231. package/dist/trajectory/parse/vaspout-h5.js +1 -4
  232. package/dist/trajectory/parse/xyz.js +48 -2
  233. package/dist/trajectory/plotting.js +14 -37
  234. package/dist/xrd/XrdPlot.svelte +43 -68
  235. package/dist/xrd/calc-xrd.js +22 -58
  236. package/dist/xrd/parse.js +13 -22
  237. package/package.json +40 -8
  238. package/readme.md +6 -5
  239. package/dist/scene/types.d.ts +0 -26
  240. package/dist/tooltip/types.d.ts +0 -8
  241. package/dist/trajectory/constants.d.ts +0 -6
  242. package/dist/trajectory/constants.js +0 -11
  243. /package/dist/{scene/types.js → file-viewer/host-protocol.js} +0 -0
  244. /package/dist/{tooltip/types.js → isosurface/geometry-worker-types.js} +0 -0
@@ -232,13 +232,9 @@ function extract_boundaries(doc, format, x_scale, y_scale) {
232
232
  const dy = Math.abs(coords.y2 - coords.y1);
233
233
  if (dx < 15 && dy < 15)
234
234
  continue;
235
- // Check stroke-width to distinguish boundaries from axis lines
235
+ // Check stroke-width to distinguish boundaries from axis lines;
236
+ // only include lines with meaningful stroke
236
237
  const stroke_width = parse_stroke_width(path_el) || 1;
237
- // Axis patches (id="patch_*") are axis borders, not phase boundaries
238
- const parent_id = group.getAttribute(`id`) ?? ``;
239
- if (parent_id.startsWith(`patch_`))
240
- continue;
241
- // Only include lines with meaningful stroke
242
238
  if (stroke_width < 1)
243
239
  continue;
244
240
  add_boundary(boundaries, coords, x_scale, y_scale, epsilon);
@@ -407,7 +403,13 @@ function extract_matplotlib_labels(doc, labels) {
407
403
  if (!comment.includes(`+`))
408
404
  continue; // skip axis labels (no "+")
409
405
  // Clean LaTeX: "La$_2$NiO$_4$ + NiO" -> "La2NiO4 + NiO"
410
- const text = clean_latex(comment.trim());
406
+ const text = comment
407
+ .trim()
408
+ .replaceAll(/\$_\{(?<digits>[^}]*)\}\$/g, `$1`) // $_{10}$ -> 10
409
+ .replaceAll(/\$_(?<digit>\d)\$/g, `$1`) // $_2$ -> 2
410
+ .replaceAll(`$`, ``) // remove any remaining $
411
+ .replaceAll(/\s+/g, ` `)
412
+ .trim();
411
413
  // Get position from transform="translate(x, y)"
412
414
  const pos = parse_translate(group.querySelector(`g[transform]`)) ?? parse_translate(group);
413
415
  if (!pos)
@@ -725,13 +727,6 @@ function find_comment_text(group) {
725
727
  }
726
728
  return null;
727
729
  }
728
- // Clean LaTeX subscript notation: "La$_2$NiO$_4$" -> "La2NiO4"
729
- const clean_latex = (text) => text
730
- .replaceAll(/\$_\{(?<digits>[^}]*)\}\$/g, `$1`) // $_{10}$ -> 10
731
- .replaceAll(/\$_(?<digit>\d)\$/g, `$1`) // $_2$ -> 2
732
- .replaceAll(`$`, ``) // remove any remaining $
733
- .replaceAll(/\s+/g, ` `)
734
- .trim();
735
730
  // Parse SVG path data into absolute line segments [x1,y1,x2,y2]
736
731
  // Handles all SVG path commands (M/L/H/V/C/S/Q/T/A/Z, both absolute and relative)
737
732
  // Curves (C/S/Q/T/A) are approximated as straight lines from start to endpoint
@@ -251,9 +251,9 @@
251
251
  let ref_lines_by_z = $derived(group_ref_lines_by_z(index_ref_lines(ref_lines)))
252
252
 
253
253
  // === Categorical Normalization (string x values -> integer indices, see ./data) ===
254
- let cat_norm = $derived(normalize_categorical(series, x_axis.categories))
255
- let category_list = $derived(cat_norm.category_list)
256
- let internal_series = $derived(cat_norm.internal_series)
254
+ let { category_list, internal_series } = $derived(
255
+ normalize_categorical(series, x_axis.categories),
256
+ )
257
257
 
258
258
  let category_indices = $derived(
259
259
  category_list.length > 0 ? category_list.map((_, idx) => idx) : null,
@@ -378,10 +378,13 @@
378
378
  })
379
379
 
380
380
  let legend_element = $state<HTMLDivElement | undefined>()
381
- const legend_footprint = $derived(
382
- measured_footprint(legend_element, { width: 120, height: 60 }),
383
- )
381
+ let legend_size_revision = $state(0)
382
+ const legend_footprint = $derived.by(() => {
383
+ void legend_size_revision
384
+ return measured_footprint(legend_element, { width: 120, height: 60 })
385
+ })
384
386
  const legend_has_explicit_pos = $derived(has_explicit_position(legend?.style))
387
+ const should_show_legend = $derived(show_legend ?? series.length > 1)
385
388
 
386
389
  // Obstacle field in normalized [0,1] plot coords (y=0 at top). Each bar is modeled as a segment
387
390
  // from baseline to its tip so the legend can't hide inside a tall bar. Built from internal_series
@@ -428,7 +431,7 @@
428
431
  })
429
432
 
430
433
  // Move the legend to the bottom margin when no interior spot avoids the bars
431
- const decor = $derived.by(() =>
434
+ const decor = $derived(
432
435
  place_decorations({
433
436
  base_pad,
434
437
  width,
@@ -437,7 +440,7 @@
437
440
  // gate on legend_element (the render signal) not legend_data, whose entries can read pad
438
441
  legend:
439
442
  legend != null &&
440
- (show_legend !== undefined ? show_legend : series.length > 1) &&
443
+ should_show_legend &&
441
444
  legend_element != null &&
442
445
  !legend_has_explicit_pos
443
446
  ? { footprint: legend_footprint, clearance: legend?.axis_clearance }
@@ -472,9 +475,6 @@
472
475
  })),
473
476
  )
474
477
  const marginal_has_axis = $derived(marginal_axis_presence(show_x2, show_y2))
475
- const legend_auto_outside = $derived(decor.legend_outside)
476
- const legend_outside_x = $derived(decor.legend_pos.x)
477
- const legend_outside_y = $derived(decor.legend_pos.y)
478
478
  const chart_width = $derived(Math.max(1, width - pad.l - pad.r))
479
479
  const chart_height = $derived(Math.max(1, height - pad.t - pad.b))
480
480
 
@@ -569,7 +569,6 @@
569
569
  let tick_label_widths = $derived({
570
570
  y_max: measure_max_tick_width(ticks.y, y_axis.format ?? ``),
571
571
  y2_max: measure_max_tick_width(ticks.y2, y2_axis.format ?? ``),
572
- x2_max: measure_max_tick_width(ticks.x2, x2_axis.format ?? ``),
573
572
  })
574
573
 
575
574
  // Shared pan/zoom/touch/drag-rect interaction controller
@@ -577,7 +576,12 @@
577
576
  ranges: () => ranges.current,
578
577
  scale_type: (axis) =>
579
578
  ({ x: x_axis, x2: x2_axis, y: y_axis, y2: y2_axis })[axis].scale_type,
580
- plot_dims: () => ({ width: chart_width, height: chart_height }),
579
+ plot_bounds: () => ({
580
+ x: pad.l,
581
+ y: pad.t,
582
+ width: chart_width,
583
+ height: chart_height,
584
+ }),
581
585
  pan: () => pan,
582
586
  set_range: (axis, range) => (ranges.current[axis] = range),
583
587
  svg: () => svg_element,
@@ -703,11 +707,10 @@
703
707
  let hovered_legend_series_idx = $state<number | null>(null)
704
708
 
705
709
  // Calculate best legend placement using continuous grid sampling
706
- let legend_placement = $derived.by(() => {
707
- const should_show = show_legend !== undefined ? show_legend : series.length > 1
708
- if (!should_show || !width || !height) return null
710
+ const get_legend_placement = () => {
711
+ if (!should_show_legend || !width || !height) return null
709
712
 
710
- const result = compute_element_placement({
713
+ return compute_element_placement({
711
714
  plot_bounds: { x: pad.l, y: pad.t, width: chart_width, height: chart_height },
712
715
  element: legend_element,
713
716
  element_size: { width: 120, height: 60 }, // fallback before first render
@@ -715,17 +718,16 @@
715
718
  exclude_rects: [],
716
719
  points: bar_points_for_placement,
717
720
  })
718
-
719
- return result
720
- })
721
+ }
721
722
 
722
723
  // Tweened legend coordinates with shared placement stability gating
723
724
  const legend_tween = create_placed_tween({
724
- placement: () => legend_placement,
725
+ placement: get_legend_placement,
725
726
  dims: () => ({ width, height }),
726
727
  responsive: () => legend?.responsive ?? false,
727
728
  element: () => legend_element,
728
729
  tween: () => legend?.tween,
730
+ on_element_resize: () => (legend_size_revision += 1),
729
731
  })
730
732
 
731
733
  // Tooltip state
@@ -804,6 +806,12 @@
804
806
  on_bar_hover?.({ ...hover_info, event })
805
807
  }
806
808
 
809
+ const clear_hover = () => {
810
+ hover_info = null
811
+ change(null)
812
+ on_bar_hover?.(null)
813
+ }
814
+
807
815
  // Stack offsets (only for bar series in stacked mode, grouped by y-axis)
808
816
  let stacked_offsets = $derived(compute_stacked_offsets(internal_series, mode))
809
817
 
@@ -913,9 +921,7 @@
913
921
  onkeydown={pan_zoom.on_key_down}
914
922
  onmouseleave={() => {
915
923
  hovered = false
916
- hover_info = null
917
- change(null)
918
- on_bar_hover?.(null)
924
+ clear_hover()
919
925
  }}
920
926
  onwheel={pan_zoom.on_wheel}
921
927
  ontouchstart={pan_zoom.on_touch_start}
@@ -964,8 +970,7 @@
964
970
  on_axis_change={(key) => handle_axis_change(`x`, key)}
965
971
  />
966
972
 
967
- <!-- X2-axis (Top) -->
968
- <!-- Note: x2 axis is only supported for vertical orientation -->
973
+ <!-- X2-axis (Top): only rendered in vertical orientation -->
969
974
  {#if show_x2}
970
975
  <PlotAxis
971
976
  side="x2"
@@ -1009,8 +1014,7 @@
1009
1014
  on_axis_change={(key) => handle_axis_change(`y`, key)}
1010
1015
  />
1011
1016
 
1012
- <!-- Y2-axis (Right) -->
1013
- <!-- Note: y2 axis is only supported for vertical orientation. Implementing x2 for horizontal mode requires additional complexity. -->
1017
+ <!-- Y2-axis (Right): only rendered in vertical orientation -->
1014
1018
  {#if show_y2}
1015
1019
  <PlotAxis
1016
1020
  side="y2"
@@ -1138,11 +1142,7 @@
1138
1142
  change(hover_info)
1139
1143
  on_bar_hover?.({ ...hover_info!, event: evt })
1140
1144
  }}
1141
- onmouseleave={() => {
1142
- change(null)
1143
- hover_info = null
1144
- on_bar_hover?.(null)
1145
- }}
1145
+ onmouseleave={clear_hover}
1146
1146
  onclick={(evt) => {
1147
1147
  const pt = find_closest_point(evt, points)
1148
1148
  if (!pt) return
@@ -1304,11 +1304,7 @@
1304
1304
  aria-label={`bar ${bar_idx + 1} of ${srs.label ?? `series`}`}
1305
1305
  style:cursor={on_bar_click ? `pointer` : undefined}
1306
1306
  onmousemove={handle_bar_hover(series_idx, bar_idx, color)}
1307
- onmouseleave={() => {
1308
- hover_info = null
1309
- change(null)
1310
- on_bar_hover?.(null)
1311
- }}
1307
+ onmouseleave={clear_hover}
1312
1308
  onclick={(evt) =>
1313
1309
  on_bar_click?.({
1314
1310
  ...get_bar_data(series_idx, bar_idx, color),
@@ -1375,11 +1371,11 @@
1375
1371
  </svg>
1376
1372
 
1377
1373
  <!-- Legend -->
1378
- {#if legend && (show_legend !== undefined ? show_legend : series.length > 1)}
1374
+ {#if legend && should_show_legend}
1379
1375
  {@const legend_pos = placed_coords(
1380
- legend_auto_outside,
1381
- { x: legend_outside_x, y: legend_outside_y },
1382
- legend_placement,
1376
+ decor.legend_outside,
1377
+ decor.legend_pos,
1378
+ legend_tween.placed(),
1383
1379
  legend_tween.coords.current,
1384
1380
  { x: pad.l + 10, y: pad.t + 10 },
1385
1381
  )}
@@ -34,36 +34,21 @@
34
34
  data.map(spg.normalize_spacegroup).filter((sg): sg is number => sg !== null),
35
35
  )
36
36
 
37
- // Compute histogram of space group numbers
37
+ // Histogram of space group number counts
38
38
  const histogram = $derived.by(() => {
39
39
  const hist = new SvelteMap<number, number>()
40
-
41
- // Count occurrences
42
- for (const sg of normalized_data) {
43
- hist.set(sg, (hist.get(sg) ?? 0) + 1)
44
- }
45
-
40
+ for (const sg of normalized_data) hist.set(sg, (hist.get(sg) ?? 0) + 1)
46
41
  return hist
47
42
  })
48
43
 
49
- // Group counts by crystal system
50
- const crystal_system_stats = $derived.by(() => {
51
- const stats = new SvelteMap<CrystalSystem, { count: number; spacegroups: number[] }>()
52
-
53
- for (const system of symmetry.CRYSTAL_SYSTEMS) {
54
- stats.set(system, { count: 0, spacegroups: [] })
55
- }
56
-
44
+ // Total counts per crystal system
45
+ const crystal_system_counts = $derived.by(() => {
46
+ const counts = new SvelteMap<CrystalSystem, number>()
57
47
  for (const [sg, count] of histogram) {
58
48
  const system = spg.spacegroup_to_crystal_sys(sg)
59
- if (!system) continue
60
- const stat = stats.get(system)
61
- if (!stat) continue
62
- stat.count += count
63
- stat.spacegroups.push(sg)
49
+ if (system) counts.set(system, (counts.get(system) ?? 0) + count)
64
50
  }
65
-
66
- return stats
51
+ return counts
67
52
  })
68
53
 
69
54
  // Create sorted list of space groups for x-axis
@@ -88,28 +73,20 @@
88
73
  // Group data by crystal system
89
74
  for (const sg of sorted_spacegroups) {
90
75
  const system = spg.spacegroup_to_crystal_sys(sg)
91
- if (system) {
92
- let series = series_by_system.get(system)
93
- if (!series) {
94
- series = { x: [], y: [] }
95
- series_by_system.set(system, series)
96
- }
97
- series.x.push(sg)
98
- series.y.push(histogram.get(sg) ?? 0)
99
- }
76
+ if (!system) continue
77
+ let series = series_by_system.get(system)
78
+ if (!series) series_by_system.set(system, (series = { x: [], y: [] }))
79
+ series.x.push(sg)
80
+ series.y.push(histogram.get(sg) ?? 0)
100
81
  }
101
82
 
102
83
  // Convert to BarSeries array, maintaining order of crystal systems
103
- const result: BarSeries[] = []
104
- for (const system of symmetry.CRYSTAL_SYSTEMS) {
84
+ return symmetry.CRYSTAL_SYSTEMS.flatMap((system) => {
105
85
  const system_data = series_by_system.get(system)
106
- if (system_data) {
107
- const { x, y } = system_data
108
- const color = symmetry.CRYSTAL_SYSTEM_COLORS[system]
109
- result.push({ x, y, color, label: system, bar_width: 0.9, visible: true })
110
- }
111
- }
112
- return result
86
+ if (!system_data) return []
87
+ const color = symmetry.CRYSTAL_SYSTEM_COLORS[system]
88
+ return { ...system_data, color, label: system, bar_width: 0.9, visible: true }
89
+ })
113
90
  })
114
91
 
115
92
  // Always show full space group range (1-230)
@@ -121,8 +98,7 @@
121
98
 
122
99
  return symmetry.CRYSTAL_SYSTEMS.map((system) => {
123
100
  const [sg_start, sg_end] = symmetry.CRYSTAL_SYSTEM_RANGES[system]
124
- const stats = crystal_system_stats.get(system)
125
- const count = stats?.count ?? 0
101
+ const count = crystal_system_counts.get(system) ?? 0
126
102
  const color = symmetry.CRYSTAL_SYSTEM_COLORS[system]
127
103
  return { system, sg_start, sg_end, count, color }
128
104
  }).filter(
@@ -491,9 +491,11 @@
491
491
  })
492
492
 
493
493
  let legend_element = $state<HTMLDivElement | undefined>()
494
- const legend_footprint = $derived(
495
- measured_footprint(legend_element, { width: 120, height: 60 }),
496
- )
494
+ let legend_size_revision = $state(0)
495
+ const legend_footprint = $derived.by(() => {
496
+ void legend_size_revision
497
+ return measured_footprint(legend_element, { width: 120, height: 60 })
498
+ })
497
499
  const legend_has_explicit_pos = $derived(has_explicit_position(legend?.style))
498
500
 
499
501
  // Obstacle field in normalized [0,1] coords: each box modeled as a whisker-spanning segment
@@ -531,7 +533,7 @@
531
533
  })
532
534
 
533
535
  const should_show_legend = $derived(show_legend ?? false)
534
- const decor = $derived.by(() =>
536
+ const decor = $derived(
535
537
  place_decorations({
536
538
  base_pad,
537
539
  width,
@@ -570,9 +572,6 @@
570
572
  }),
571
573
  )
572
574
  const marginal_has_axis = $derived(marginal_axis_presence(show_x2, show_y2))
573
- const legend_auto_outside = $derived(decor.legend_outside)
574
- const legend_outside_x = $derived(decor.legend_pos.x)
575
- const legend_outside_y = $derived(decor.legend_pos.y)
576
575
  const chart_width = $derived(Math.max(1, width - pad.l - pad.r))
577
576
  const chart_height = $derived(Math.max(1, height - pad.t - pad.b))
578
577
 
@@ -645,7 +644,6 @@
645
644
  let tick_label_widths = $derived({
646
645
  y_max: measure_max_tick_width(ticks.y, y_axis.format ?? ``),
647
646
  y2_max: measure_max_tick_width(ticks.y2, y2_axis.format ?? ``),
648
- x2_max: measure_max_tick_width(ticks.x2, x2_axis.format ?? ``),
649
647
  })
650
648
 
651
649
  // Shared pan/zoom/touch/drag-rect interaction controller
@@ -653,7 +651,12 @@
653
651
  ranges: () => ranges.current,
654
652
  scale_type: (axis) =>
655
653
  ({ x: x_axis, x2: x2_axis, y: y_axis, y2: y2_axis })[axis].scale_type,
656
- plot_dims: () => ({ width: chart_width, height: chart_height }),
654
+ plot_bounds: () => ({
655
+ x: pad.l,
656
+ y: pad.t,
657
+ width: chart_width,
658
+ height: chart_height,
659
+ }),
657
660
  pan: () => pan,
658
661
  set_range: (axis, range) => (ranges.current[axis] = range),
659
662
  svg: () => svg_element,
@@ -718,7 +721,7 @@
718
721
 
719
722
  let hovered_legend_series_idx = $state<number | null>(null)
720
723
 
721
- let legend_placement = $derived.by(() => {
724
+ const get_legend_placement = () => {
722
725
  if (!should_show_legend || !width || !height) return null
723
726
  return compute_element_placement({
724
727
  plot_bounds: { x: pad.l, y: pad.t, width: chart_width, height: chart_height },
@@ -728,15 +731,16 @@
728
731
  exclude_rects: [],
729
732
  points: box_points_for_placement,
730
733
  })
731
- })
734
+ }
732
735
 
733
736
  // Tweened legend coordinates with shared placement stability gating
734
737
  const legend_tween = create_placed_tween({
735
- placement: () => legend_placement,
738
+ placement: get_legend_placement,
736
739
  dims: () => ({ width, height }),
737
740
  responsive: () => legend?.responsive ?? false,
738
741
  element: () => legend_element,
739
742
  tween: () => legend?.tween,
743
+ on_element_resize: () => (legend_size_revision += 1),
740
744
  })
741
745
 
742
746
  // === Tooltip / hover ===
@@ -788,6 +792,12 @@
788
792
  on_box_hover?.({ ...hover_info, event })
789
793
  }
790
794
 
795
+ const clear_hover = () => {
796
+ hover_info = null
797
+ change(null)
798
+ on_box_hover?.(null)
799
+ }
800
+
791
801
  // Set theme-aware background when entering fullscreen
792
802
  $effect(() => set_fullscreen_bg(wrapper, fullscreen, `--boxplot-fullscreen-bg`))
793
803
 
@@ -880,9 +890,7 @@
880
890
  onkeydown={pan_zoom.on_key_down}
881
891
  onmouseleave={() => {
882
892
  hovered = false
883
- hover_info = null
884
- change(null)
885
- on_box_hover?.(null)
893
+ clear_hover()
886
894
  }}
887
895
  onwheel={pan_zoom.on_wheel}
888
896
  ontouchstart={pan_zoom.on_touch_start}
@@ -1080,11 +1088,7 @@
1080
1088
  ? 0.25
1081
1089
  : 1}
1082
1090
  onmousemove={handle_box_hover(box_item, color)}
1083
- onmouseleave={() => {
1084
- hover_info = null
1085
- change(null)
1086
- on_box_hover?.(null)
1087
- }}
1091
+ onmouseleave={clear_hover}
1088
1092
  onclick={(evt) =>
1089
1093
  on_box_click?.({ ...get_box_data(box_item, color), event: evt })}
1090
1094
  onkeydown={(evt) => {
@@ -1215,9 +1219,9 @@
1215
1219
 
1216
1220
  {#if legend && should_show_legend}
1217
1221
  {@const legend_pos = placed_coords(
1218
- legend_auto_outside,
1219
- { x: legend_outside_x, y: legend_outside_y },
1220
- legend_placement,
1222
+ decor.legend_outside,
1223
+ decor.legend_pos,
1224
+ legend_tween.placed(),
1221
1225
  legend_tween.coords.current,
1222
1226
  { x: pad.l + 10, y: pad.t + 10 },
1223
1227
  )}
@@ -7,7 +7,7 @@ type Size = {
7
7
  width: number;
8
8
  height: number;
9
9
  };
10
- export declare const placed_coords: (auto_outside: boolean, outside: Pt, placement: Pt | null, tweened: Pt, fallback: Pt) => Pt;
10
+ export declare const placed_coords: (auto_outside: boolean, outside: Pt, placed: boolean, tweened: Pt, fallback: Pt) => Pt;
11
11
  export declare const has_explicit_position: (style?: string | null) => boolean;
12
12
  export declare const measured_footprint: (el: HTMLElement | null | undefined, fallback: Size) => Size;
13
13
  export declare function build_obstacles_norm(series: {
@@ -4,7 +4,7 @@ import { compute_element_placement, sample_series_obstacle_points, } from './lay
4
4
  const DECOR_GAP = 8; // px gap between an outside decoration and the plot edge
5
5
  // Final on-screen position for an auto-placed decoration: explicit outside placement
6
6
  // wins, then the tweened auto placement, else a static padding fallback
7
- export const placed_coords = (auto_outside, outside, placement, tweened, fallback) => (auto_outside ? outside : placement ? tweened : fallback);
7
+ export const placed_coords = (auto_outside, outside, placed, tweened, fallback) => (auto_outside ? outside : placed ? tweened : fallback);
8
8
  // True when the user pinned a decoration via its style (an edge property or position:absolute),
9
9
  // in which case auto-placement must leave it alone.
10
10
  export const has_explicit_position = (style) => /(?:^|[;{]\s*)(?:top|bottom|left|right)\s*:|position\s*:\s*absolute/.test(style ?? ``);
@@ -1,5 +1,5 @@
1
1
  import type { D3SymbolName } from '../../labels';
2
- import type { DataSeries, Point } from '..';
2
+ import type { DataSeries } from '..';
3
3
  export declare const get_series_color: (series_idx: number) => string;
4
4
  export declare const get_series_symbol: (series_idx: number) => D3SymbolName;
5
5
  export declare const extract_series_color: (series_data: DataSeries) => string;
@@ -12,5 +12,4 @@ export declare const prepare_legend_data: (series: DataSeries[]) => {
12
12
  symbol_color: string;
13
13
  };
14
14
  }[];
15
- export declare const create_data_points: (series: DataSeries[], filter_fn?: (series: DataSeries) => boolean) => Point[];
16
15
  export declare function process_prop<T>(prop: T[] | T | undefined | null, idx: number): T | undefined;
@@ -28,16 +28,6 @@ export const prepare_legend_data = (series) => series.map((series_data, series_i
28
28
  symbol_color: extract_series_color(series_data),
29
29
  },
30
30
  }));
31
- // Create data points from series for analysis
32
- export const create_data_points = (series, filter_fn) => series
33
- .filter(filter_fn ?? ((srs) => srs.visible ?? true))
34
- .flatMap(({ x: xs, y: ys }, series_idx) => {
35
- const length = Math.min(xs.length, ys.length);
36
- if (xs.length !== ys.length) {
37
- console.warn(`length mismatch in series ${series_idx}: x.length=${xs.length} vs y.length=${ys.length}`);
38
- }
39
- return xs.slice(0, length).map((x, idx) => ({ x, y: ys[idx] }));
40
- });
41
31
  // Process array or scalar properties for indexed access.
42
32
  // If prop is an array, returns the element at the given index.
43
33
  // If prop is a scalar, returns the scalar (applied to all indices).
@@ -10,10 +10,15 @@ export declare const DEFAULT_PLOT_PADDING: Required<Sides>;
10
10
  export declare function y2_axis_label_x(axis: AxisConfig, width: number, pad_r: number, max_tick_width: number): number;
11
11
  export declare const filter_padding: (padding: Partial<Sides> | undefined | null, defaults: Required<Sides>) => Required<Sides>;
12
12
  export declare function measure_text_width(text: string, font?: string): number;
13
- export declare function measure_full_footprint(el: HTMLElement): {
13
+ type Size = {
14
14
  width: number;
15
15
  height: number;
16
16
  };
17
+ type ElementFootprint = Size & {
18
+ offset_x: number;
19
+ offset_y: number;
20
+ };
21
+ export declare const full_footprint_or: (el: HTMLElement | null | undefined, fallback: Size) => ElementFootprint;
17
22
  export interface AutoPaddingConfig {
18
23
  padding: Partial<Sides>;
19
24
  default_padding: Required<Sides>;
@@ -52,10 +57,7 @@ export declare const centered_rect: (center_x: number, top_y: number, width: num
52
57
  export declare const rect_within_rect: (rect: Rect, bounds: Rect) => boolean;
53
58
  export interface ElementPlacementConfig {
54
59
  plot_bounds: Rect;
55
- element_size: {
56
- width: number;
57
- height: number;
58
- };
60
+ element_size: Size;
59
61
  element?: HTMLElement | null;
60
62
  axis_clearance?: number;
61
63
  exclude_rects?: Rect[];
@@ -70,6 +72,10 @@ export interface ElementPlacementResult {
70
72
  y: number;
71
73
  score: number;
72
74
  }
75
+ export declare const point_in_rect: (point: {
76
+ x: number;
77
+ y: number;
78
+ }, rect: Rect) => boolean;
73
79
  export declare const rects_overlap: (r1: Rect, r2: Rect) => boolean;
74
80
  export declare function sample_series_obstacle_points(pixel_points: {
75
81
  x: number;
@@ -79,3 +85,4 @@ export declare function sample_series_obstacle_points(pixel_points: {
79
85
  y: number;
80
86
  }[];
81
87
  export declare function compute_element_placement(config: ElementPlacementConfig): ElementPlacementResult;
88
+ export {};