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
@@ -26,6 +26,7 @@
26
26
  hierarchy_legend_items,
27
27
  is_activation_key,
28
28
  node_handler_props,
29
+ observe_height,
29
30
  pointer_pos,
30
31
  prune_muted_ids,
31
32
  safe_hierarchy_layout,
@@ -42,6 +43,15 @@
42
43
  import type { Rect, Sides } from '../core/layout'
43
44
  import { arrow_nav_target } from '../sunburst/render'
44
45
  import type { PositionedArc } from '../sunburst/sunburst'
46
+ import {
47
+ normalize_treemap_label_lines,
48
+ place_treemap_label,
49
+ safe_font_size,
50
+ type TreemapLabelFit,
51
+ type TreemapLabelFormatter,
52
+ type TreemapLabelLine,
53
+ type TreemapLabelPlacement,
54
+ } from './labels'
45
55
  import type { TreemapNode, TreemapNodeHandlerProps } from './treemap'
46
56
  import { lerp_rects, tile_rects } from './treemap'
47
57
  import { DEFAULTS } from '../../settings'
@@ -52,12 +62,16 @@
52
62
  import { Tween, type TweenOptions } from 'svelte/motion'
53
63
  import { SvelteSet } from 'svelte/reactivity'
54
64
 
55
- const DEFAULT_PADDING: Required<Sides> = { t: 10, b: 10, l: 10, r: 10 }
65
+ // no outer inset by default: cells tile flush with the container (pass
66
+ // `padding` to reserve chart-edge space, e.g. for host-drawn annotations)
67
+ const DEFAULT_PADDING: Required<Sides> = { t: 0, b: 0, l: 0, r: 0 }
56
68
 
57
69
  let {
58
70
  data = $bindable([]),
59
71
  value_mode = $bindable(DEFAULTS.treemap.value_mode),
60
- sort = `none`,
72
+ // descending (unlike Sunburst's input-order default): squarified tiling
73
+ // reads best with the largest cell top-left and smallest bottom-right
74
+ sort = `descending`,
61
75
  level_lighten = 0,
62
76
  min_fraction = $bindable(DEFAULTS.treemap.min_fraction),
63
77
  other_label = `Other`,
@@ -67,6 +81,11 @@
67
81
  padding_outer = $bindable(DEFAULTS.treemap.padding_outer),
68
82
  show_labels = $bindable(DEFAULTS.treemap.show_labels),
69
83
  label_text = $bindable(DEFAULTS.treemap.label_text),
84
+ label_formatter,
85
+ label_fit = `shrink`,
86
+ label_min_font_size = 6,
87
+ label_max_font_size,
88
+ parent_label_font_size = 14,
70
89
  zoom_on_click = $bindable(DEFAULTS.treemap.zoom_on_click),
71
90
  zoom_root_id = $bindable(null),
72
91
  show_breadcrumbs = $bindable(DEFAULTS.treemap.show_breadcrumbs),
@@ -102,7 +121,7 @@
102
121
  Omit<BasePlotProps, `change`> & {
103
122
  data?: TreemapNode<Metadata> | TreemapNode<Metadata>[]
104
123
  value_mode?: SunburstValueMode
105
- sort?: SunburstSort
124
+ sort?: SunburstSort // default 'descending' (largest top-left); 'none' keeps input order
106
125
  level_lighten?: number
107
126
  // Aggregate sibling cells below this fraction of the total into one 'Other'
108
127
  // cell per parent (only when >= 2 qualify); 0 disables
@@ -114,6 +133,13 @@
114
133
  padding_outer?: number // px inset of children within their parent (plotly marker.pad)
115
134
  show_labels?: boolean
116
135
  label_text?: SunburstLabelText // what labels display (plotly textinfo equivalent)
136
+ // Structured multiline labels. Unlike cell_content, this keeps built-in
137
+ // hover/focus/click and tooltip behavior on the underlying cell.
138
+ label_formatter?: TreemapLabelFormatter<Metadata>
139
+ label_fit?: TreemapLabelFit // shrink-to-fit (default), hide, or clip at max size
140
+ label_min_font_size?: number // px floor used by shrink mode
141
+ label_max_font_size?: number // px ceiling for leaf/cutoff labels
142
+ parent_label_font_size?: number // px size/ceiling for branch header labels
117
143
  zoom_on_click?: boolean
118
144
  zoom_root_id?: string | number | null // id of the cell the view is rooted on
119
145
  show_breadcrumbs?: boolean // clickable ancestor trail when zoomed
@@ -167,9 +193,9 @@
167
193
  let pad = $derived(filter_padding(padding, DEFAULT_PADDING))
168
194
  let inner_width = $derived(Math.max(0, width - pad.l - pad.r))
169
195
  let avail_height = $derived(Math.max(0, height - pad.t - pad.b))
170
- // measured height of the bottom colorbar, reserved from the chart so it never
171
- // overlaps the cells; reset to 0 when hidden (effect below); capped at half the
172
- // area so a bad measurement can't collapse the chart
196
+ // measured height of the bottom colorbar (via observe_height, which resets it
197
+ // to 0 on unmount), reserved from the chart so it never overlaps the cells;
198
+ // capped at half the area so a bad measurement can't collapse the chart
173
199
  let colorbar_height = $state(0)
174
200
  let colorbar_reserve = $derived(
175
201
  colorbar_height > 0 ? Math.min(colorbar_height + 16, avail_height / 2) : 0,
@@ -289,11 +315,6 @@
289
315
  )
290
316
  const cell_color = (arc: PositionedArc<Metadata>): string =>
291
317
  metric?.colors[arc.node_idx] ?? arc.color
292
- // release the colorbar's reserved chart space when it's not rendered
293
- $effect(() => {
294
- if (!metric || colorbar == null) colorbar_height = 0
295
- })
296
-
297
318
  // Hovered cell + its ancestors/descendants stay fully opaque, others dim
298
319
  let cell_dim = $derived(compute_node_dim(layout.arcs, muted_ids, hovered_idx))
299
320
 
@@ -424,6 +445,12 @@
424
445
  let cells_clickable = $derived(Boolean(on_node_click) || zoom_on_click)
425
446
 
426
447
  let label_font = $derived(svg_label_font(svg_element))
448
+ // leading "<n>px" of the CSS font shorthand (e.g. "11px sans-serif")
449
+ let label_font_size = $derived.by(() => {
450
+ const leading_num = Number(label_font.match(/^[\d.]+/)?.[0])
451
+ return safe_font_size(leading_num, 11)
452
+ })
453
+ let resolved_parent_label_font_size = $derived(safe_font_size(parent_label_font_size, 14))
427
454
  const cached_text_width = make_cached_text_width()
428
455
 
429
456
  // Per-cell label text, measured width, fill/label colors and aria string -
@@ -440,38 +467,95 @@
440
467
  )
441
468
 
442
469
  const LABEL_MARGIN = 6 // px clearance between label text and cell edges
443
- // Label text + placement for a cell; null = no variant fits, hide the label.
470
+ const label_clip_id = (idx: number) => `treemap-label-clip-${uid}-${idx}`
471
+ const font_for_line = (line: TreemapLabelLine, font_size: number): string => {
472
+ const sized_font = label_font.replace(/(?:\d+(?:\.\d+)?|\.\d+)px/, `${font_size}px`)
473
+ return line.font_weight == null ? sized_font : `${line.font_weight} ${sized_font}`
474
+ }
475
+ const measure_label_line = (line: TreemapLabelLine, font_size: number): number => {
476
+ const measured_width = cached_text_width(line.text, font_for_line(line, font_size))
477
+ // Canvas text metrics can be unavailable during SSR and in lightweight DOM
478
+ // environments. A conservative fallback keeps fitting deterministic there.
479
+ return measured_width > 0 ? measured_width : line.text.length * font_size * 0.6
480
+ }
481
+
482
+ // Formatter output is layout-independent, so resolve it once per node instead
483
+ // of on every frame of a zoom tween. Without a formatter, fit modes use the
484
+ // richest built-in variant (compound text when configured).
485
+ // The depth-0 root never renders a cell, so the formatter is never invoked
486
+ // for it (for array data it's synthetic and e.g. carries no metadata).
487
+ let label_lines = $derived(
488
+ layout.arcs.map((arc, idx) =>
489
+ arc.depth === 0
490
+ ? []
491
+ : normalize_treemap_label_lines(
492
+ label_formatter ? label_formatter(arc) : cell_info[idx].variants[0]?.text,
493
+ ),
494
+ ),
495
+ )
496
+
497
+ // Label text + placement for a cell; null means hide the label.
444
498
  // Branch cells label their header strip (top-left); leaves (and branches at the
445
499
  // depth cutoff, which render as plain cells) center their label, rotating 90°
446
- // in thin-but-tall cells like the icicle shape does. Richest label variant
447
- // that fits wins: extended -> label -> label_short.
448
- function label_attrs(
500
+ // in thin-but-tall cells like the icicle shape does.
501
+ function place_label(
449
502
  arc: PositionedArc<Metadata>,
450
503
  rect: Rect,
451
- ): { x: number; y: number; text: string; transform?: string; header: boolean } | null {
452
- const { variants } = cell_info[arc.node_idx]
453
- const { x, y, width: cell_w, height: cell_h } = rect
504
+ ): TreemapLabelPlacement | null {
454
505
  // Branches with visible children only ever label their header strip: a
455
506
  // centered label would paint over the descendant cells that cover the rest
456
507
  // of the cell, so when the strip is missing/too thin the label is dropped
457
508
  const has_visible_children = !arc.is_leaf && arc.depth < depth_cutoff
458
- if (has_visible_children) {
459
- if (padding_top < 12) return null
460
- const fitting = variants.find((entry) => entry.width <= cell_w - 2 * LABEL_MARGIN)
461
- if (!fitting) return null
462
- return { x: x + LABEL_MARGIN, y: y + padding_top / 2, text: fitting.text, header: true }
463
- }
464
- const fits = (text_w: number, along: number, across: number) =>
465
- text_w <= along - 2 * LABEL_MARGIN && across >= 12
466
- const [cx, cy] = [x + cell_w / 2, y + cell_h / 2]
467
- for (const { text, width: text_w } of variants) {
468
- if (fits(text_w, cell_w, cell_h)) return { x: cx, y: cy, text, header: false }
469
- if (fits(text_w, cell_h, cell_w)) {
470
- return { x: cx, y: cy, text, transform: `rotate(-90, ${cx}, ${cy})`, header: false }
509
+
510
+ const place = (lines: TreemapLabelLine[]) =>
511
+ place_treemap_label({
512
+ rect,
513
+ lines,
514
+ header: has_visible_children,
515
+ fit: label_fit,
516
+ min_font_size: label_min_font_size,
517
+ max_font_size: has_visible_children
518
+ ? resolved_parent_label_font_size
519
+ : (label_max_font_size ?? label_font_size),
520
+ padding_top,
521
+ margin: LABEL_MARGIN,
522
+ measure_line: (line, font_size) =>
523
+ measure_label_line(
524
+ has_visible_children && line.font_weight == null
525
+ ? { ...line, font_weight: 600 }
526
+ : line,
527
+ font_size,
528
+ ),
529
+ })
530
+
531
+ if (!label_formatter && label_fit === `hide`) {
532
+ for (const { text } of cell_info[arc.node_idx].variants) {
533
+ const placement = place([{ text }])
534
+ if (placement) return placement
471
535
  }
536
+ return null
537
+ }
538
+ return place(label_lines[arc.node_idx])
539
+ }
540
+
541
+ // hide mode never overflows (unfitting labels return null), so clipPaths are
542
+ // only rendered/applied for the shrink/clip modes (with or without formatter)
543
+ let clip_labels = $derived(label_fit !== `hide`)
544
+ const label_clip_rect = (rect: Rect, header: boolean): Rect => {
545
+ const inset = 1
546
+ const clip_height = header ? Math.min(rect.height, padding_top) : rect.height
547
+ return {
548
+ x: rect.x + inset,
549
+ y: rect.y + inset,
550
+ width: Math.max(0, rect.width - 2 * inset),
551
+ height: Math.max(0, clip_height - 2 * inset),
472
552
  }
473
- return null
474
553
  }
554
+ // Defs and visible text share these placements, avoiding duplicate text
555
+ // measurement and fitting work on every frame of a zoom tween.
556
+ let label_placements = $derived(
557
+ new Map(visible_idxs.map((idx) => [idx, place_label(layout.arcs[idx], rects[idx])])),
558
+ )
475
559
 
476
560
  // Legend: one item per depth-1 category, toggling mutes (dims) rather than removes.
477
561
  let depth1_arcs = $derived(layout.arcs.filter((arc) => arc.depth === 1))
@@ -591,6 +675,16 @@
591
675
  <pattern id={hatch_pattern_id} patternUnits="userSpaceOnUse" width="8" height="8">
592
676
  <path class="hatch-pattern-line" d="M-1,1 l2,-2 M0,8 l8,-8 M7,9 l2,-2" />
593
677
  </pattern>
678
+ {#if show_labels && !cell_content && clip_labels}
679
+ {#each visible_idxs as idx (idx)}
680
+ {@const label = label_placements.get(idx)}
681
+ {#if label}
682
+ <clipPath id={label_clip_id(idx)}>
683
+ <rect {...label_clip_rect(rects[idx], label.header)} />
684
+ </clipPath>
685
+ {/if}
686
+ {/each}
687
+ {/if}
594
688
  </defs>
595
689
  <!-- Hover/click delegation sits on the chart group (not the cells group) so
596
690
  labels - which carry the same data-treemap-node-idx and are selectable text -
@@ -650,21 +744,39 @@
650
744
  {#if show_labels && !cell_content}
651
745
  <g class="cell-labels">
652
746
  {#each visible_idxs as idx (idx)}
653
- {@const lbl = label_attrs(layout.arcs[idx], rects[idx])}
747
+ {@const lbl = label_placements.get(idx)}
654
748
  {#if lbl}
655
- <text
656
- class="cell-label"
657
- class:header={lbl.header}
658
- data-treemap-node-idx={idx}
659
- x={lbl.x}
660
- y={lbl.y}
661
- transform={lbl.transform}
662
- fill={cell_info[idx].label_fill}
663
- fill-opacity={cell_dim[idx].label_opacity}
664
- style:cursor={cells_clickable ? `pointer` : `text`}
665
- >
666
- {lbl.text}
667
- </text>
749
+ <!-- Keep the clip on this untransformed wrapper. Applying it to
750
+ rotated text rotates the clipping region and crops the wrong area. -->
751
+ <g clip-path={clip_labels ? `url(#${label_clip_id(idx)})` : undefined}>
752
+ <text
753
+ class="cell-label"
754
+ class:header={lbl.header}
755
+ data-treemap-node-idx={idx}
756
+ x={lbl.x}
757
+ y={lbl.lines[0].y}
758
+ dominant-baseline={lbl.dominant_baseline}
759
+ transform={lbl.transform}
760
+ fill={cell_info[idx].label_fill}
761
+ fill-opacity={cell_dim[idx].label_opacity}
762
+ font-size={lbl.font_size}
763
+ style:cursor={cells_clickable ? `pointer` : `text`}
764
+ >
765
+ {#each lbl.lines as line}
766
+ <tspan
767
+ class={line.class}
768
+ x={lbl.x}
769
+ y={line.y}
770
+ font-size={lbl.font_size * (line.font_scale ?? 1)}
771
+ font-weight={line.font_weight}
772
+ opacity={line.opacity}
773
+ fill={line.fill}
774
+ >
775
+ {line.text}
776
+ </tspan>
777
+ {/each}
778
+ </text>
779
+ </g>
668
780
  {/if}
669
781
  {/each}
670
782
  </g>
@@ -716,17 +828,19 @@
716
828
  {/if}
717
829
 
718
830
  {#if metric && colorbar != null}
719
- <div
720
- bind:clientHeight={colorbar_height}
721
- style="position: absolute; bottom: var(--treemap-colorbar-bottom, 8px); left: 50%; transform: translateX(-50%); width: var(--treemap-colorbar-width, 40%); min-width: 120px; pointer-events: auto;"
722
- >
723
- <ColorBar
724
- {color_scale}
725
- range={metric.range}
726
- {...colorbar}
727
- wrapper_style={`width: 100%; ${colorbar?.wrapper_style ?? ``}`}
728
- />
729
- </div>
831
+ <!-- positioning + height measurement live on ColorBar's own root (via rest
832
+ props + attachment) instead of an extra wrapper div -->
833
+ <ColorBar
834
+ {color_scale}
835
+ range={metric.range}
836
+ {...colorbar}
837
+ wrapper_style="{colorbar?.orientation === `vertical`
838
+ ? `--cbar-height: var(--treemap-colorbar-height, 150px);`
839
+ : ``} {colorbar?.wrapper_style ?? ``}"
840
+ style="position: absolute; bottom: var(--treemap-colorbar-bottom, 8px); left: 50%; transform: translateX(-50%); width: var(--treemap-colorbar-width, 40%); min-width: 120px; pointer-events: auto; {colorbar?.style ??
841
+ ``}"
842
+ {@attach observe_height((px) => (colorbar_height = px))}
843
+ />
730
844
  {/if}
731
845
 
732
846
  {@render children?.({ height, width, fullscreen })}
@@ -745,7 +859,9 @@
745
859
  flex: var(--treemap-flex, 1 1 auto);
746
860
  display: var(--treemap-display, flex);
747
861
  flex-direction: column;
748
- background: var(--treemap-bg, var(--plot-bg));
862
+ /* no bg shading by default: the cells are the chart; set --treemap-bg to
863
+ add a panel background */
864
+ background: var(--treemap-bg, transparent);
749
865
  border-radius: var(--treemap-border-radius, var(--border-radius, 3pt));
750
866
  }
751
867
  .treemap.fullscreen {
@@ -787,7 +903,7 @@
787
903
  background: var(--treemap-btn-bg, light-dark(#e3e6ea, #33383f));
788
904
  color: inherit;
789
905
  border: none;
790
- padding: 2px 14px 2px 12px;
906
+ padding: var(--treemap-breadcrumbs-padding, 0 14px 0 12px);
791
907
  cursor: pointer;
792
908
  font: inherit;
793
909
  clip-path: polygon(
@@ -894,7 +1010,6 @@
894
1010
  }
895
1011
  .cell-label {
896
1012
  text-anchor: middle;
897
- dominant-baseline: central;
898
1013
  /* selectable so labels can be copied; clicks/hover still reach the underlying
899
1014
  cell via data-treemap-node-idx + delegation on the chart group */
900
1015
  -webkit-user-select: text;
@@ -4,6 +4,7 @@ import type { BasePlotProps, LegendConfig, SunburstLabelText, SunburstSort, Sunb
4
4
  import { ColorBar } from '..';
5
5
  import type { Rect, Sides } from '../core/layout';
6
6
  import type { PositionedArc } from '../sunburst/sunburst';
7
+ import { type TreemapLabelFit, type TreemapLabelFormatter } from './labels';
7
8
  import type { TreemapNode, TreemapNodeHandlerProps } from './treemap';
8
9
  import type { ComponentProps, Snippet } from 'svelte';
9
10
  import type { HTMLAttributes } from 'svelte/elements';
@@ -22,6 +23,11 @@ declare function $$render<Metadata extends Record<string, unknown> = Record<stri
22
23
  padding_outer?: number;
23
24
  show_labels?: boolean;
24
25
  label_text?: SunburstLabelText;
26
+ label_formatter?: TreemapLabelFormatter<Metadata>;
27
+ label_fit?: TreemapLabelFit;
28
+ label_min_font_size?: number;
29
+ label_max_font_size?: number;
30
+ parent_label_font_size?: number;
25
31
  zoom_on_click?: boolean;
26
32
  zoom_root_id?: string | number | null;
27
33
  show_breadcrumbs?: boolean;
@@ -1,3 +1,4 @@
1
1
  export * from './treemap';
2
+ export type { TreemapLabelFit, TreemapLabelFormatter, TreemapLabelLine } from './labels';
2
3
  export { default as Treemap } from './Treemap.svelte';
3
4
  export { default as TreemapControls } from '../core/components/HierarchyControls.svelte';
@@ -0,0 +1,38 @@
1
+ import type { Rect } from '../core/layout';
2
+ import type { TreemapArc } from './treemap';
3
+ export type TreemapLabelFit = `hide` | `shrink` | `clip`;
4
+ export interface TreemapLabelLine {
5
+ text: string;
6
+ class?: string;
7
+ font_scale?: number;
8
+ font_weight?: string | number;
9
+ opacity?: number;
10
+ fill?: string;
11
+ }
12
+ type TreemapLabelContent = string | TreemapLabelLine | null | undefined;
13
+ export type TreemapLabelFormatter<Metadata extends Record<string, unknown> = Record<string, unknown>> = (arc: TreemapArc<Metadata>) => TreemapLabelContent | readonly TreemapLabelContent[];
14
+ export interface TreemapLabelPlacement {
15
+ x: number;
16
+ lines: (TreemapLabelLine & {
17
+ y: number;
18
+ })[];
19
+ font_size: number;
20
+ header: boolean;
21
+ dominant_baseline: `central`;
22
+ transform?: string;
23
+ }
24
+ interface TreemapLabelPlacementOptions {
25
+ rect: Rect;
26
+ lines: readonly TreemapLabelLine[];
27
+ header: boolean;
28
+ fit: TreemapLabelFit;
29
+ min_font_size: number;
30
+ max_font_size: number;
31
+ padding_top: number;
32
+ margin: number;
33
+ measure_line: (line: TreemapLabelLine, font_size: number) => number;
34
+ }
35
+ export declare const safe_font_size: (font_size: number, fallback: number) => number;
36
+ export declare function normalize_treemap_label_lines(content: ReturnType<TreemapLabelFormatter>): TreemapLabelLine[];
37
+ export declare function place_treemap_label({ rect, lines, header, fit, min_font_size, max_font_size, padding_top, margin, measure_line, }: TreemapLabelPlacementOptions): TreemapLabelPlacement | null;
38
+ export {};
@@ -0,0 +1,80 @@
1
+ const LINE_HEIGHT = 1.1;
2
+ const MIN_FONT_SIZE = 0.5;
3
+ const MIN_FONT_SCALE = 0.05;
4
+ // Coerce a user-provided font size to a usable px value (0.5px floor guards
5
+ // against zero/negative sizes; non-finite input falls back)
6
+ export const safe_font_size = (font_size, fallback) => Number.isFinite(font_size) ? Math.max(MIN_FONT_SIZE, font_size) : fallback;
7
+ export function normalize_treemap_label_lines(content) {
8
+ if (content == null)
9
+ return [];
10
+ const lines = Array.isArray(content) ? content : [content];
11
+ return lines.flatMap((line) => {
12
+ const normalized = typeof line === `string` ? { text: line } : line;
13
+ return normalized?.text ? [{ ...normalized }] : [];
14
+ });
15
+ }
16
+ const line_scale = (line) => Number.isFinite(line.font_scale) ? Math.max(MIN_FONT_SCALE, line.font_scale ?? 1) : 1;
17
+ function block_metrics(lines, font_size, measure_line) {
18
+ let width = 0;
19
+ let height = 0;
20
+ for (const line of lines) {
21
+ const line_font_size = font_size * line_scale(line);
22
+ width = Math.max(width, measure_line(line, line_font_size));
23
+ height += line_font_size * LINE_HEIGHT;
24
+ }
25
+ return { width, height };
26
+ }
27
+ export function place_treemap_label({ rect, lines, header, fit, min_font_size, max_font_size, padding_top, margin, measure_line, }) {
28
+ if (lines.length === 0)
29
+ return null;
30
+ const safe_max_font_size = safe_font_size(max_font_size, 11);
31
+ const safe_min_font_size = Math.min(safe_max_font_size, safe_font_size(min_font_size, MIN_FONT_SIZE));
32
+ const header_height = Math.min(rect.height, padding_top);
33
+ const available_width = Math.max(0, rect.width - 2 * margin);
34
+ const label_height = header ? header_height : rect.height;
35
+ const available_height = Math.max(0, label_height - 2 * (header ? 1 : margin));
36
+ // Single guard for all degenerate geometry (zero/negative/NaN rects, missing
37
+ // header strips, margin-swallowed slivers): negated > 0 so NaN also bails
38
+ // instead of emitting NaN SVG coordinates, same idiom as tile_rects.
39
+ if (!(available_width > 0) || !(available_height > 0))
40
+ return null;
41
+ const max_metrics = block_metrics(lines, safe_max_font_size, measure_line);
42
+ const fit_ratio = (block_width, block_height) => block_width > 0 && block_height > 0
43
+ ? Math.min(1, available_width / block_width, available_height / block_height)
44
+ : 0;
45
+ const horizontal_ratio = fit_ratio(max_metrics.width, max_metrics.height);
46
+ // rotated 90°: the block's width runs along the cell's height (swapped args)
47
+ const vertical_ratio = header ? 0 : fit_ratio(max_metrics.height, max_metrics.width);
48
+ const rotated = vertical_ratio > horizontal_ratio;
49
+ const best_ratio = Math.max(horizontal_ratio, vertical_ratio);
50
+ if (fit === `hide` && best_ratio < 1)
51
+ return null;
52
+ const font_size = fit === `shrink`
53
+ ? Math.max(safe_min_font_size, safe_max_font_size * best_ratio)
54
+ : safe_max_font_size;
55
+ // text metrics scale linearly with font size, so the block height at the
56
+ // final size derives from the max-size measurement — no re-measuring (which
57
+ // would miss the width cache on every frame of a zoom tween in shrink mode,
58
+ // since the fitted size varies continuously with the animated rect)
59
+ const block_height = max_metrics.height * (font_size / safe_max_font_size);
60
+ const center_x = rect.x + rect.width / 2;
61
+ const center_y = rect.y + label_height / 2;
62
+ const x = header ? rect.x + margin : center_x;
63
+ let line_top = center_y - block_height / 2;
64
+ const placed_lines = lines.map((line) => {
65
+ const font_scale = line_scale(line);
66
+ const line_height = font_size * font_scale * LINE_HEIGHT;
67
+ line_top += line_height / 2;
68
+ const y = line_top;
69
+ line_top += line_height / 2;
70
+ return { ...line, font_scale, y };
71
+ });
72
+ return {
73
+ x,
74
+ lines: placed_lines,
75
+ font_size,
76
+ header,
77
+ dominant_baseline: `central`,
78
+ ...(rotated ? { transform: `rotate(-90, ${center_x}, ${center_y})` } : {}),
79
+ };
80
+ }
@@ -1,9 +1,11 @@
1
1
  import type { Rect } from '../core/layout';
2
- import type { PositionedArc, SunburstLayoutOptions, SunburstNode, SunburstNodeHandlerProps } from '../sunburst/sunburst';
2
+ import type { PositionedArc, SunburstLayoutOptions, SunburstNode, SunburstNodeHandlerProps, SunburstSort } from '../sunburst/sunburst';
3
3
  export type TreemapNode<Metadata = Record<string, unknown>> = SunburstNode<Metadata>;
4
4
  export type TreemapArc<Metadata = Record<string, unknown>> = PositionedArc<Metadata>;
5
5
  export type TreemapNodeHandlerProps<Metadata = Record<string, unknown>> = SunburstNodeHandlerProps<Metadata>;
6
- export type TreemapLayoutOptions = SunburstLayoutOptions;
6
+ export interface TreemapLayoutOptions extends Omit<SunburstLayoutOptions, `sort`> {
7
+ sort?: SunburstSort;
8
+ }
7
9
  export interface TilePadding {
8
10
  padding_inner: number;
9
11
  padding_top: number;
@@ -92,8 +92,11 @@ export function lerp_rects(prev, next, t) {
92
92
  // re-tiles without recomputing tree semantics).
93
93
  export function compute_treemap_layout(data, size, opts = {}) {
94
94
  // padding fallbacks derive from DEFAULTS.treemap to prevent drift with the component
95
- const { padding_inner = DEFAULTS.treemap.padding_inner, padding_top = DEFAULTS.treemap.padding_top, padding_outer = DEFAULTS.treemap.padding_outer, ...tree_opts } = opts;
96
- const { arcs, root, max_depth } = compute_sunburst_layout(data, tree_opts);
95
+ const { padding_inner = DEFAULTS.treemap.padding_inner, padding_top = DEFAULTS.treemap.padding_top, padding_outer = DEFAULTS.treemap.padding_outer,
96
+ // descending (matching the Treemap component): squarified tiling reads best
97
+ // with the largest cell top-left and smallest bottom-right
98
+ sort = `descending`, ...tree_opts } = opts;
99
+ const { arcs, root, max_depth } = compute_sunburst_layout(data, { sort, ...tree_opts });
97
100
  const rects = tile_rects(arcs, 0, size, { padding_inner, padding_top, padding_outer });
98
101
  return { arcs, rects, root, max_depth };
99
102
  }
@@ -21,7 +21,7 @@
21
21
  y_axis = {},
22
22
  cutoff = 15,
23
23
  n_bins = 75,
24
- pbc = [true, true, true],
24
+ pbc,
25
25
  enable_drop = false,
26
26
  on_file_drop,
27
27
  loading = $bindable(false),
@@ -87,41 +87,23 @@
87
87
  }
88
88
 
89
89
  const entries = $derived.by(() => {
90
- const result: RdfEntry[] = []
90
+ const result: RdfEntry[] = patterns
91
+ ? Array.isArray(patterns)
92
+ ? [...patterns]
93
+ : [patterns]
94
+ : []
91
95
 
92
- // Add patterns
93
- if (patterns) {
94
- if (Array.isArray(patterns)) result.push(...patterns)
95
- else result.push(patterns)
96
- }
97
-
98
- // Add structures
99
- const struct_list: { struct: Crystal; label: string }[] = []
100
- if (structures) {
101
- if (Array.isArray(structures)) {
102
- structures.forEach((struct, idx) =>
103
- struct_list.push({
104
- struct,
105
- label: format_structure_label(struct, `Crystal ${idx + 1}`),
106
- }),
107
- )
108
- } else if (is_crystal(structures)) {
109
- struct_list.push({
110
- struct: structures,
111
- label: format_structure_label(structures, ``),
112
- })
113
- } else {
114
- Object.entries(structures).forEach(([label, struct]) =>
115
- struct_list.push({ struct, label: format_structure_label(struct, label) }),
116
- )
117
- }
118
- }
119
- drag_dropped.forEach((struct, idx) =>
120
- struct_list.push({
121
- struct,
122
- label: format_structure_label(struct, `Dropped ${idx + 1}`),
123
- }),
124
- )
96
+ // Normalize structures prop (single, array, or dict) plus dropped files to labeled list
97
+ const struct_list = [
98
+ ...(!structures
99
+ ? []
100
+ : Array.isArray(structures)
101
+ ? structures.map((struct, idx) => ({ struct, base: `Crystal ${idx + 1}` }))
102
+ : is_crystal(structures)
103
+ ? [{ struct: structures, base: `` }]
104
+ : Object.entries(structures).map(([base, struct]) => ({ struct, base }))),
105
+ ...drag_dropped.map((struct, idx) => ({ struct, base: `Dropped ${idx + 1}` })),
106
+ ].map(({ struct, base }) => ({ struct, label: format_structure_label(struct, base) }))
125
107
 
126
108
  for (const { struct, label } of struct_list) {
127
109
  if (mode === `element_pairs`) {