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
@@ -31,23 +31,35 @@ export function measure_text_width(text, font = `12px sans-serif`) {
31
31
  ctx.font = font;
32
32
  return ctx.measureText(text).width;
33
33
  }
34
- // Measure an element's full visual footprint, including descendants that overflow its
35
- // offset box. Colorbar tick labels are position:absolute outside the bar, so
36
- // offsetWidth/offsetHeight underestimate the space they actually occupy — which lets
37
- // auto-placement put the colorbar where its labels overlap the axes.
38
- export function measure_full_footprint(el) {
34
+ function measure_full_footprint(el) {
39
35
  const root = el.getBoundingClientRect();
36
+ let left = root.left;
37
+ let top = root.top;
40
38
  let right = root.right;
41
39
  let bottom = root.bottom;
42
40
  for (const child of el.querySelectorAll(`*`)) {
43
41
  const rect = child.getBoundingClientRect();
44
42
  if (rect.width === 0 && rect.height === 0)
45
43
  continue;
44
+ left = Math.min(left, rect.left);
45
+ top = Math.min(top, rect.top);
46
46
  right = Math.max(right, rect.right);
47
47
  bottom = Math.max(bottom, rect.bottom);
48
48
  }
49
- return { width: right - root.left, height: bottom - root.top };
49
+ return {
50
+ width: right - left,
51
+ height: bottom - top,
52
+ offset_x: left - root.left,
53
+ offset_y: top - root.top,
54
+ };
50
55
  }
56
+ // Full footprint once the element is laid out, else `fallback` (offset dims read 0
57
+ // before first render). NOT interchangeable with auto-place's measured_footprint:
58
+ // that returns the offset box, which underestimates elements with overflowing
59
+ // absolutely-positioned descendants like colorbar tick labels.
60
+ export const full_footprint_or = (el, fallback) => el?.offsetWidth && el?.offsetHeight
61
+ ? measure_full_footprint(el)
62
+ : { ...fallback, offset_x: 0, offset_y: 0 };
51
63
  // Measure the widest formatted tick label. Used for auto-padding and label placement.
52
64
  export const measure_max_tick_width = (ticks, format = ``) => ticks.length === 0
53
65
  ? 0
@@ -148,7 +160,7 @@ const DISTANCE_WEIGHT = 0.001;
148
160
  const CORNER_WEIGHT = 5.0;
149
161
  const MAX_SAMPLE_POINTS = 500;
150
162
  // Check if a point is inside a rectangle
151
- const point_in_rect = (point, rect) => point.x >= rect.x &&
163
+ export const point_in_rect = (point, rect) => point.x >= rect.x &&
152
164
  point.x <= rect.x + rect.width &&
153
165
  point.y >= rect.y &&
154
166
  point.y <= rect.y + rect.height;
@@ -195,14 +207,16 @@ export function compute_element_placement(config) {
195
207
  // Measure the element's full footprint (incl. descendants that overflow its box,
196
208
  // such as colorbar tick labels) once it's laid out; fall back to element_size before
197
209
  // first render. Centralizing this keeps every plot's auto-placement overlap-free.
198
- const { width: elem_width, height: elem_height } = element?.offsetWidth && element?.offsetHeight
199
- ? measure_full_footprint(element)
200
- : element_size;
210
+ const { width: elem_width, height: elem_height, offset_x, offset_y, } = full_footprint_or(element, element_size);
201
211
  // Calculate valid placement region (plot bounds minus axis clearance)
202
- const valid_x_min = plot_bounds.x + axis_clearance;
203
- const valid_y_min = plot_bounds.y + axis_clearance;
204
- const valid_x_max = plot_bounds.x + plot_bounds.width - axis_clearance - elem_width;
205
- const valid_y_max = plot_bounds.y + plot_bounds.height - axis_clearance - elem_height;
212
+ const plot_left = plot_bounds.x + axis_clearance;
213
+ const plot_right = plot_bounds.x + plot_bounds.width - axis_clearance;
214
+ const plot_top = plot_bounds.y + axis_clearance;
215
+ const plot_bottom = plot_bounds.y + plot_bounds.height - axis_clearance;
216
+ const valid_x_min = plot_left - offset_x;
217
+ const valid_y_min = plot_top - offset_y;
218
+ const valid_x_max = plot_right - offset_x - elem_width;
219
+ const valid_y_max = plot_bottom - offset_y - elem_height;
206
220
  // Handle case where element is too large for the valid region
207
221
  const effective_x_min = Math.min(valid_x_min, valid_x_max);
208
222
  const effective_x_max = Math.max(valid_x_min, valid_x_max);
@@ -225,18 +239,14 @@ export function compute_element_placement(config) {
225
239
  ? (effective_y_max - effective_y_min) / (grid_resolution - 1)
226
240
  : 0;
227
241
  // Precompute plot corners (constant across all candidates)
228
- const plot_left = valid_x_min;
229
- const plot_right = plot_bounds.x + plot_bounds.width - axis_clearance;
230
- const plot_top = valid_y_min;
231
- const plot_bottom = plot_bounds.y + plot_bounds.height - axis_clearance;
232
242
  const max_corner_dist = euclidean_dist([plot_left, plot_top], [plot_right, plot_bottom]);
233
243
  for (let grid_x = 0; grid_x < grid_resolution; grid_x++) {
234
244
  for (let grid_y = 0; grid_y < grid_resolution; grid_y++) {
235
245
  const cand_x = effective_x_min + grid_x * x_step;
236
246
  const cand_y = effective_y_min + grid_y * y_step;
237
247
  const cand_rect = {
238
- x: cand_x,
239
- y: cand_y,
248
+ x: cand_x + offset_x,
249
+ y: cand_y + offset_y,
240
250
  width: elem_width,
241
251
  height: elem_height,
242
252
  };
@@ -250,8 +260,8 @@ export function compute_element_placement(config) {
250
260
  // Count points overlapping this candidate position
251
261
  let overlap_count = 0;
252
262
  let min_distance_sq = Infinity;
253
- const center_x = cand_x + elem_width / 2;
254
- const center_y = cand_y + elem_height / 2;
263
+ const center_x = cand_rect.x + elem_width / 2;
264
+ const center_y = cand_rect.y + elem_height / 2;
255
265
  for (const point of sampled_points) {
256
266
  if (point_in_rect(point, cand_rect)) {
257
267
  overlap_count++;
@@ -271,12 +281,12 @@ export function compute_element_placement(config) {
271
281
  // Corner preference: use element's actual corner (not center) for distance
272
282
  // This ensures a wide element at the left edge gets proper corner credit
273
283
  // (measured footprint, same as cand_rect — not the element_size fallback)
274
- const elem_right = cand_x + elem_width;
275
- const elem_bottom = cand_y + elem_height;
284
+ const elem_right = cand_rect.x + elem_width;
285
+ const elem_bottom = cand_rect.y + elem_height;
276
286
  // Distance from element's matching corner to each plot corner
277
- const min_corner_dist = Math.min(euclidean_dist([cand_x, cand_y], [plot_left, plot_top]), // top-left
278
- euclidean_dist([elem_right, cand_y], [plot_right, plot_top]), // top-right
279
- euclidean_dist([cand_x, elem_bottom], [plot_left, plot_bottom]), // bottom-left
287
+ const min_corner_dist = Math.min(euclidean_dist([cand_rect.x, cand_rect.y], [plot_left, plot_top]), // top-left
288
+ euclidean_dist([elem_right, cand_rect.y], [plot_right, plot_top]), // top-right
289
+ euclidean_dist([cand_rect.x, elem_bottom], [plot_left, plot_bottom]), // bottom-left
280
290
  euclidean_dist([elem_right, elem_bottom], [plot_right, plot_bottom]));
281
291
  // Higher bonus for positions closer to corners (0 = at corner, 1 = far from all)
282
292
  const corner_bonus = max_corner_dist > 0 ? (1 - min_corner_dist / max_corner_dist) * CORNER_WEIGHT : 0;
@@ -1,13 +1,11 @@
1
1
  import type { Point2D, Vec2 } from '../../math';
2
+ import { type Rect } from './layout';
2
3
  import type { AxisRanges, PanConfig, ScaleType } from './types';
3
4
  type Axis = `x` | `x2` | `y` | `y2`;
4
5
  export interface PanZoomOptions {
5
6
  ranges: () => AxisRanges;
6
7
  scale_type: (axis: Axis) => ScaleType | undefined;
7
- plot_dims: () => {
8
- width: number;
9
- height: number;
10
- };
8
+ plot_bounds: () => Rect;
11
9
  pan: () => PanConfig | undefined;
12
10
  set_range: (axis: Axis, range: Vec2) => void;
13
11
  svg: () => SVGElement | null;
@@ -3,17 +3,18 @@
3
3
  // per-component policy (which axis props to write on rect-zoom, reset semantics)
4
4
  // stays in the component via the on_rect_zoom/on_reset/set_range callbacks.
5
5
  import { MIN_TOUCH_DISTANCE_PIXELS, pan_range_by_pixels, PINCH_ZOOM_THRESHOLD, remove_drag_listeners, snapshot_ranges, zoom_range_by_factor, } from './interactions';
6
+ import { point_in_rect } from './layout';
6
7
  const AXES = [`x`, `x2`, `y`, `y2`];
7
8
  export function create_pan_zoom(opts) {
8
9
  // Rect-zoom drag state
9
- let drag_state = $state({ start: null, current: null, bounds: null });
10
+ let drag_state = $state(null);
10
11
  // Pan state
11
12
  let is_focused = $state(false);
12
13
  let shift_held = $state(false);
13
14
  let pan_drag_state = $state(null);
14
15
  let touch_state = $state(null);
15
16
  const cancel_rect_drag = () => {
16
- drag_state = { start: null, current: null, bounds: null };
17
+ drag_state = null;
17
18
  if (typeof window !== `undefined`) {
18
19
  window.removeEventListener(`mousemove`, on_window_mouse_move);
19
20
  window.removeEventListener(`mouseup`, on_window_mouse_up);
@@ -21,7 +22,7 @@ export function create_pan_zoom(opts) {
21
22
  }
22
23
  };
23
24
  const on_window_mouse_move = (evt) => {
24
- if (!drag_state.start || !drag_state.bounds)
25
+ if (!drag_state)
25
26
  return;
26
27
  const coords = {
27
28
  x: evt.clientX - drag_state.bounds.left,
@@ -35,7 +36,7 @@ export function create_pan_zoom(opts) {
35
36
  opts.on_drag_move?.(coords, inside_svg);
36
37
  };
37
38
  const on_window_mouse_up = () => {
38
- if (drag_state.start && drag_state.current) {
39
+ if (drag_state) {
39
40
  // Ignore minuscule drag rects (e.g. accidental clicks)
40
41
  const dx = Math.abs(drag_state.start.x - drag_state.current.x);
41
42
  const dy = Math.abs(drag_state.start.y - drag_state.current.y);
@@ -47,7 +48,7 @@ export function create_pan_zoom(opts) {
47
48
  // Pan/zoom all four axes from an interaction-start snapshot, each in its own
48
49
  // scale's transform space (log axes pan by a constant factor, linear by a shift)
49
50
  const pan_all_axes = (init, dx_px, dy_px) => {
50
- const dims = opts.plot_dims();
51
+ const dims = opts.plot_bounds();
51
52
  for (const axis of AXES) {
52
53
  const horizontal = axis === `x` || axis === `x2`;
53
54
  opts.set_range(axis, pan_range_by_pixels(init[`initial_${axis}_range`], horizontal ? dx_px : dy_px, horizontal ? dims.width : dims.height, opts.scale_type(axis)));
@@ -72,9 +73,17 @@ export function create_pan_zoom(opts) {
72
73
  window.removeEventListener(`mouseup`, on_pan_end);
73
74
  };
74
75
  const on_mouse_down = (evt) => {
76
+ if (evt.button !== 0)
77
+ return;
75
78
  const svg = opts.svg();
76
79
  if (!svg)
77
80
  return;
81
+ // The SVG also contains axes, tick labels, and titles. Only the padded data
82
+ // rectangle may start a drag interaction.
83
+ const svg_bounds = svg.getBoundingClientRect();
84
+ const coords = { x: evt.clientX - svg_bounds.left, y: evt.clientY - svg_bounds.top };
85
+ if (!point_in_rect(coords, opts.plot_bounds()))
86
+ return;
78
87
  // Shift+drag pans (when enabled); plain drag draws the zoom rect
79
88
  const pan_enabled = opts.pan()?.enabled !== false;
80
89
  if (pan_enabled && evt.shiftKey) {
@@ -89,9 +98,7 @@ export function create_pan_zoom(opts) {
89
98
  return;
90
99
  }
91
100
  // Cache bounds at drag start so window mousemove can compute relative coords
92
- const bounds = svg.getBoundingClientRect();
93
- const coords = { x: evt.clientX - bounds.left, y: evt.clientY - bounds.top };
94
- drag_state = { start: coords, current: coords, bounds };
101
+ drag_state = { start: coords, current: coords, bounds: svg_bounds };
95
102
  window.addEventListener(`mousemove`, on_window_mouse_move);
96
103
  window.addEventListener(`mouseup`, on_window_mouse_up);
97
104
  document.body.style.cursor = `crosshair`;
@@ -104,7 +111,7 @@ export function create_pan_zoom(opts) {
104
111
  if (pan_cfg?.enabled === false || !is_focused || !shift_held)
105
112
  return;
106
113
  evt.preventDefault();
107
- const dims = opts.plot_dims();
114
+ const dims = opts.plot_bounds();
108
115
  const sensitivity = pan_cfg?.wheel_sensitivity ?? 1;
109
116
  const ranges = opts.ranges();
110
117
  // Pan along the dominant wheel direction
@@ -125,12 +132,22 @@ export function create_pan_zoom(opts) {
125
132
  const touch_enabled = pan_cfg?.enabled !== false && pan_cfg?.touch_enabled !== false;
126
133
  if (!touch_enabled || evt.touches.length !== 2)
127
134
  return;
135
+ const svg_bounds = opts.svg()?.getBoundingClientRect();
136
+ if (!svg_bounds)
137
+ return;
138
+ const plot_bounds = opts.plot_bounds();
139
+ const start_touches = Array.from(evt.touches).map((touch) => ({
140
+ x: touch.clientX,
141
+ y: touch.clientY,
142
+ }));
143
+ // Every contact must start in the data rectangle; touching an axis or label
144
+ // must not arm a gesture that later moves into the plot.
145
+ const starts_outside_plot = start_touches.some((touch) => !point_in_rect({ x: touch.x - svg_bounds.left, y: touch.y - svg_bounds.top }, plot_bounds));
146
+ if (starts_outside_plot)
147
+ return;
128
148
  evt.preventDefault();
129
149
  touch_state = {
130
- start_touches: Array.from(evt.touches).map((touch) => ({
131
- x: touch.clientX,
132
- y: touch.clientY,
133
- })),
150
+ start_touches,
134
151
  ...snapshot_ranges(opts.ranges()),
135
152
  };
136
153
  };
@@ -168,7 +185,7 @@ export function create_pan_zoom(opts) {
168
185
  const on_key_down = (evt) => {
169
186
  if (evt.target !== evt.currentTarget)
170
187
  return;
171
- if (evt.key === `Escape` && drag_state.start)
188
+ if (evt.key === `Escape` && drag_state)
172
189
  cancel_rect_drag();
173
190
  if ([`Enter`, ` `].includes(evt.key)) {
174
191
  evt.preventDefault();
@@ -177,10 +194,10 @@ export function create_pan_zoom(opts) {
177
194
  };
178
195
  return {
179
196
  get drag_start() {
180
- return drag_state.start;
197
+ return drag_state?.start ?? null;
181
198
  },
182
199
  get drag_current() {
183
- return drag_state.current;
200
+ return drag_state?.current ?? null;
184
201
  },
185
202
  get is_pan_dragging() {
186
203
  return pan_drag_state !== null;
@@ -213,7 +230,7 @@ export function create_pan_zoom(opts) {
213
230
  // stuck cursor). Safe during SSR teardown, where window/document don't exist.
214
231
  destroy: () => {
215
232
  remove_drag_listeners([on_window_mouse_move, on_pan_move], [on_window_mouse_up, on_pan_end]);
216
- drag_state = { start: null, current: null, bounds: null };
233
+ drag_state = null;
217
234
  pan_drag_state = null;
218
235
  touch_state = null;
219
236
  },
@@ -14,8 +14,11 @@ export declare function create_placed_tween(opts: {
14
14
  tween?: () => TweenOptions<Point> | undefined;
15
15
  suspended?: () => boolean;
16
16
  manual_position?: () => Point | null;
17
+ on_element_resize?: () => void;
18
+ placement_revision?: () => unknown;
17
19
  }): {
18
20
  coords: Tween<Point>;
21
+ placed: () => boolean;
19
22
  set_locked: (locked: boolean) => void;
20
23
  };
21
24
  export {};
@@ -5,9 +5,9 @@ import { Tween } from 'svelte/motion';
5
5
  const HOVER_DEBOUNCE_MS = 300;
6
6
  // Tweened position for an auto-placed plot element with the stability gating shared
7
7
  // by BarPlot/BoxPlot/Histogram/ScatterPlot legends: snap (no animation) on first
8
- // placement, follow container resizes, stay put while hovered (debounced hover lock),
9
- // and track placement continuously only when `responsive`. Creates $effects, so it
10
- // must be called during component init.
8
+ // placement, follow container/element resizes, stay put while hovered (debounced
9
+ // hover lock), and track placement continuously only when `responsive`. Creates
10
+ // $effects, so it must be called during component init.
11
11
  export function create_placed_tween(opts) {
12
12
  // Hover lock: placement updates pause while the element is hovered; release is
13
13
  // debounced so brief mouse-outs don't cause jumps
@@ -22,16 +22,43 @@ export function create_placed_tween(opts) {
22
22
  else
23
23
  unlock_timeout = setTimeout(() => (hover_locked = false), HOVER_DEBOUNCE_MS);
24
24
  };
25
- // Plain (untracked) flags: only read/written inside the effect below, and outcomes
26
- // don't depend on re-running when they flip
25
+ // Previous plot dimensions stay plain; they only classify the current effect run.
27
26
  let prev_dims = null;
28
- let has_initial_placement = false;
27
+ let element_size_revision = $state(0);
28
+ let prev_element_size_revision = 0;
29
+ let previous_element;
30
+ let previous_placement_revision;
31
+ let placed = $state(false);
29
32
  const coords = new Tween({ x: 0, y: 0 }, untrack(() => ({ duration: 400, ...opts.tween?.() })));
30
33
  // Clear pending unlock timeout to prevent state updates after unmount
31
34
  $effect(() => () => {
32
35
  if (unlock_timeout)
33
36
  clearTimeout(unlock_timeout);
34
37
  });
38
+ // Decoration content (title, orientation, tick labels) can resize without the
39
+ // plot dimensions changing. Observe it so frozen placements are recomputed.
40
+ $effect(() => {
41
+ const element = opts.element() ?? null;
42
+ if (previous_element !== undefined && previous_element !== element) {
43
+ element_size_revision += 1;
44
+ if (!element)
45
+ placed = false;
46
+ }
47
+ previous_element = element;
48
+ if (!element || typeof ResizeObserver === `undefined`)
49
+ return undefined;
50
+ const observed_elements = new WeakSet();
51
+ const observer = new ResizeObserver((entries) => {
52
+ const size_changed = entries.some(({ target }) => observed_elements.has(target));
53
+ for (const { target } of entries)
54
+ observed_elements.add(target);
55
+ if (size_changed)
56
+ element_size_revision += 1;
57
+ });
58
+ for (const target of [element, ...element.querySelectorAll(`*`)])
59
+ observer.observe(target);
60
+ return () => observer.disconnect();
61
+ });
35
62
  // Update position with stability checks
36
63
  $effect(() => {
37
64
  const { width, height } = opts.dims();
@@ -43,26 +70,46 @@ export function create_placed_tween(opts) {
43
70
  if (manual) {
44
71
  // Immediate update (no animation) for manually positioned elements
45
72
  void coords.set({ x: manual.x, y: manual.y }, { duration: 0 });
46
- return;
73
+ if (opts.element())
74
+ placed = true;
47
75
  }
48
- const placement = opts.placement();
49
- if (!placement)
76
+ // Leave resize revisions pending so unlock recomputes the latest placement.
77
+ if (hover_locked)
50
78
  return;
51
79
  // Track dimensions for resize detection
52
80
  const dims_changed = !prev_dims || prev_dims.width !== width || prev_dims.height !== height;
53
81
  if (dims_changed)
54
82
  prev_dims = { width, height };
55
- // Only update if: resize occurred, OR (not hover-locked AND (responsive OR not
56
- // yet initially placed))
57
- const should_update = dims_changed || (!hover_locked && (opts.responsive() || !has_initial_placement));
58
- if (should_update) {
59
- void coords.set({ x: placement.x, y: placement.y },
60
- // Skip animation on initial placement to avoid jump from (0, 0)
61
- has_initial_placement ? undefined : { duration: 0 });
62
- // Only lock position after the element has an actual measured size
63
- if (opts.element())
64
- has_initial_placement = true;
65
- }
83
+ const element_size_changed = prev_element_size_revision !== element_size_revision;
84
+ prev_element_size_revision = element_size_revision;
85
+ if (element_size_changed)
86
+ opts.on_element_resize?.();
87
+ if (manual)
88
+ return;
89
+ const placement_revision = opts.placement_revision?.();
90
+ const placement_invalidated = !Object.is(previous_placement_revision, placement_revision);
91
+ previous_placement_revision = placement_revision;
92
+ const responsive = opts.responsive();
93
+ // A non-responsive tween tracks `placed` once so it immediately reruns and
94
+ // unsubscribes from the expensive placement chain after its first placement.
95
+ const has_initial_placement = responsive ? untrack(() => placed) : placed;
96
+ // Skip expensive DOM placement before evaluating it: non-responsive
97
+ // elements stay fixed after their initial placement until the plot resizes.
98
+ if (!dims_changed &&
99
+ !element_size_changed &&
100
+ !placement_invalidated &&
101
+ !responsive &&
102
+ has_initial_placement)
103
+ return;
104
+ const placement = opts.placement();
105
+ if (!placement)
106
+ return;
107
+ void coords.set({ x: placement.x, y: placement.y },
108
+ // Skip animation on initial placement to avoid jump from (0, 0)
109
+ has_initial_placement ? undefined : { duration: 0 });
110
+ // Only lock position after the element has an actual measured size
111
+ if (opts.element())
112
+ placed = true;
66
113
  });
67
- return { coords, set_locked };
114
+ return { coords, placed: () => placed, set_locked };
68
115
  }
@@ -29,7 +29,6 @@ export function violin_path(grid_px, half_offsets_px, center, side, orient) {
29
29
  }
30
30
  return `M${pts.map(([x_pos, y_pos]) => `${x_pos},${y_pos}`).join(`L`)}Z`;
31
31
  }
32
- //
33
32
  // Generate SVG path for a bar with rounded corners on the "free" end (away from axis).
34
33
  // For vertical bars, rounds top corners. For horizontal bars, rounds right corners.
35
34
  export function bar_path(x, y, w, h, r, vertical = true) {
@@ -36,6 +36,7 @@ export declare function compute_node_infos<Metadata>(arcs: readonly PositionedAr
36
36
  contrast: (fill: string) => string;
37
37
  clickable?: (arc: PositionedArc<Metadata>) => boolean;
38
38
  }): HierarchyNodeInfo[];
39
+ export declare const observe_height: (on_height: (px: number) => void) => (element: Element) => () => void;
39
40
  export declare function svg_label_font(svg_element: SVGSVGElement | null): string;
40
41
  export declare const pointer_pos: (event: MouseEvent | FocusEvent | undefined, svg_element: SVGSVGElement | null) => {
41
42
  x: number;
@@ -145,6 +145,18 @@ export function compute_node_infos(arcs, opts) {
145
145
  };
146
146
  });
147
147
  }
148
+ // Attachment factory reporting an element's rendered height (immediately, on
149
+ // resize, and 0 on unmount) — used to reserve chart space below the cells/arcs
150
+ // for the colorbar without an extra measuring wrapper div.
151
+ export const observe_height = (on_height) => (element) => {
152
+ const observer = new ResizeObserver(() => on_height(element.clientHeight));
153
+ observer.observe(element);
154
+ on_height(element.clientHeight);
155
+ return () => {
156
+ observer.disconnect();
157
+ on_height(0); // release the reserved space when the element unmounts
158
+ };
159
+ };
148
160
  // The font node labels actually render in (respects the chart's font-size CSS
149
161
  // var instead of assuming 11px), for canvas-measured label fitting
150
162
  export function svg_label_font(svg_element) {
@@ -88,12 +88,13 @@ const EXPORT_INLINE_STYLES = [
88
88
  `font-weight`,
89
89
  `opacity`,
90
90
  ];
91
+ const EXPORT_OPTIONS = { viewbox_padding: `stroke` };
91
92
  export function export_hierarchy_chart(svg_element, base_filename, format) {
92
93
  if (!svg_element)
93
94
  return;
94
95
  const filename = `${base_filename}.${format}`;
95
96
  if (format === `svg`)
96
- export_svg_as_svg(svg_element, filename, EXPORT_INLINE_STYLES);
97
+ export_svg_as_svg(svg_element, filename, EXPORT_INLINE_STYLES, EXPORT_OPTIONS);
97
98
  else
98
- export_svg_as_png(svg_element, filename, 150, EXPORT_INLINE_STYLES);
99
+ export_svg_as_png(svg_element, filename, 150, EXPORT_INLINE_STYLES, EXPORT_OPTIONS);
99
100
  }
@@ -398,10 +398,13 @@
398
398
  base_pad = new_pad
399
399
  })
400
400
 
401
- const legend_footprint = $derived(
402
- measured_footprint(legend_element, { width: 120, height: 60 }),
403
- )
401
+ let legend_size_revision = $state(0)
402
+ const legend_footprint = $derived.by(() => {
403
+ void legend_size_revision
404
+ return measured_footprint(legend_element, { width: 120, height: 60 })
405
+ })
404
406
  const legend_has_explicit_pos = $derived(has_explicit_position(legend?.style))
407
+ const should_show_legend = $derived(show_legend && legend != null && series.length > 1)
405
408
 
406
409
  // Obstacle field in normalized [0,1] plot coords (y=0 at top). Each filled bar is modeled as a
407
410
  // vertical segment (top -> baseline) so the legend can't hide inside a tall bar. Built from
@@ -431,7 +434,7 @@
431
434
  })
432
435
 
433
436
  // Move the legend to the bottom margin when no interior spot avoids the bars
434
- const decor = $derived.by(() =>
437
+ const decor = $derived(
435
438
  place_decorations({
436
439
  base_pad,
437
440
  width,
@@ -439,11 +442,7 @@
439
442
  obstacles_norm,
440
443
  // gate on legend_element (the render signal) not legend_data, whose entries can read pad
441
444
  legend:
442
- show_legend &&
443
- legend != null &&
444
- series.length > 1 &&
445
- legend_element != null &&
446
- !legend_has_explicit_pos
445
+ should_show_legend && legend_element != null && !legend_has_explicit_pos
447
446
  ? { footprint: legend_footprint, clearance: legend?.axis_clearance }
448
447
  : null,
449
448
  }),
@@ -472,9 +471,6 @@
472
471
  const marginal_has_axis = $derived(
473
472
  marginal_axis_presence(x2_series.length > 0, y2_series.length > 0),
474
473
  )
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
474
 
479
475
  // Scales and data (x/x2 share the horizontal pixel span, y/y2 the inverted vertical one)
480
476
  let scales = $derived(
@@ -531,7 +527,6 @@
531
527
  // Cache measured tick-label widths so expensive text measurement only runs
532
528
  // when tick values/format change, not on every template rerender.
533
529
  let tick_label_widths = $derived({
534
- x2_max: measure_max_tick_width(ticks.x2, final_x2_axis.format ?? ``),
535
530
  y_max: measure_max_tick_width(ticks.y, final_y_axis.format ?? ``),
536
531
  y2_max: measure_max_tick_width(ticks.y2, final_y2_axis.format ?? ``),
537
532
  })
@@ -562,14 +557,13 @@
562
557
  })
563
558
 
564
559
  // Calculate best legend placement using continuous grid sampling
565
- let legend_placement = $derived.by(() => {
566
- const should_place = show_legend && legend != null && series.length > 1
567
- if (!should_place || !width || !height) return null
560
+ const get_legend_placement = () => {
561
+ if (!should_show_legend || !width || !height) return null
568
562
 
569
563
  const plot_width = width - pad.l - pad.r
570
564
  const plot_height = height - pad.t - pad.b
571
565
 
572
- const result = compute_element_placement({
566
+ return compute_element_placement({
573
567
  plot_bounds: { x: pad.l, y: pad.t, width: plot_width, height: plot_height },
574
568
  element: legend_element,
575
569
  element_size: { width: 120, height: 60 }, // fallback before first render
@@ -577,17 +571,16 @@
577
571
  exclude_rects: [],
578
572
  points: hist_points_for_placement,
579
573
  })
580
-
581
- return result
582
- })
574
+ }
583
575
 
584
576
  // Tweened legend coordinates with shared placement stability gating
585
577
  const legend_tween = create_placed_tween({
586
- placement: () => legend_placement,
578
+ placement: get_legend_placement,
587
579
  dims: () => ({ width, height }),
588
580
  responsive: () => legend?.responsive ?? false,
589
581
  element: () => legend_element,
590
582
  tween: () => legend?.tween,
583
+ on_element_resize: () => (legend_size_revision += 1),
591
584
  })
592
585
 
593
586
  // Shared pan/zoom/touch/drag-rect interaction controller
@@ -597,7 +590,9 @@
597
590
  ({ x: final_x_axis, x2: final_x2_axis, y: final_y_axis, y2: final_y2_axis })[axis]
598
591
  .scale_type,
599
592
  // Clamp to at least 1 to avoid Infinity deltas when padding equals container size
600
- plot_dims: () => ({
593
+ plot_bounds: () => ({
594
+ x: pad.l,
595
+ y: pad.t,
601
596
  width: Math.max(1, width - pad.l - pad.r),
602
597
  height: Math.max(1, height - pad.t - pad.b),
603
598
  }),
@@ -1042,11 +1037,11 @@
1042
1037
  />
1043
1038
  {/if}
1044
1039
 
1045
- {#if show_legend && legend != null && series.length > 1}
1040
+ {#if should_show_legend && legend}
1046
1041
  {@const legend_pos = placed_coords(
1047
- legend_auto_outside,
1048
- { x: legend_outside_x, y: legend_outside_y },
1049
- legend_placement,
1042
+ decor.legend_outside,
1043
+ decor.legend_pos,
1044
+ legend_tween.placed(),
1050
1045
  legend_tween.coords.current,
1051
1046
  { x: pad.l + 10, y: pad.t + 10 },
1052
1047
  )}
@@ -20,11 +20,6 @@ function resolve_node_ref(ref, id_to_idx, n_nodes) {
20
20
  }
21
21
  throw new Error(`Sankey link references unknown node: ${JSON.stringify(ref)}`);
22
22
  }
23
- function resolve_numeric_node_ref(ref, n_nodes) {
24
- if (Number.isInteger(ref) && ref >= 0 && ref < n_nodes)
25
- return ref;
26
- throw new Error(`Sankey link references unknown node: ${JSON.stringify(ref)}`);
27
- }
28
23
  // Build a SankeyData object from parallel flat arrays (e.g. the plotly/matbench
29
24
  // `link.source`/`link.target`/`link.value` + `node.label` format).
30
25
  export function sankey_from_links(source, target, value, labels) {
@@ -107,9 +102,18 @@ export function compute_sankey_layout(data, opts) {
107
102
  }));
108
103
  // Resolve a source/target ref to a node index: id/label lookup when any link uses
109
104
  // non-numeric refs, otherwise a cheap numeric-range check (no map built).
110
- const resolve_ref = (ref) => needs_ref_lookup
111
- ? resolve_node_ref(ref, id_to_idx, data.nodes.length)
112
- : resolve_numeric_node_ref(ref, data.nodes.length);
105
+ const resolve_ref = (ref) => {
106
+ if (needs_ref_lookup) {
107
+ return resolve_node_ref(ref, id_to_idx, data.nodes.length);
108
+ }
109
+ if (typeof ref === `number` &&
110
+ Number.isInteger(ref) &&
111
+ ref >= 0 &&
112
+ ref < data.nodes.length) {
113
+ return ref;
114
+ }
115
+ throw new Error(`Sankey link references unknown node: ${JSON.stringify(ref)}`);
116
+ };
113
117
  const link_copies = data.links.map((link, idx) => ({
114
118
  link_idx: idx,
115
119
  color: link.color,
@@ -185,5 +189,5 @@ export function compute_sankey_layout(data, opts) {
185
189
  }
186
190
  return graph;
187
191
  }
188
- // Map a node alignment string to the d3-sankey alignment function (exposed for tests)
192
+ // Map a node alignment string to the d3-sankey alignment function
189
193
  export const sankey_align_fn = (align) => ALIGN_FNS[align];