matterviz 0.3.5 → 0.3.7

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 (229) hide show
  1. package/dist/MillerIndexInput.svelte +5 -5
  2. package/dist/api/optimade.js +3 -3
  3. package/dist/brillouin/BrillouinZone.svelte +5 -2
  4. package/dist/brillouin/BrillouinZone.svelte.d.ts +1 -1
  5. package/dist/brillouin/BrillouinZoneExportPane.svelte +1 -3
  6. package/dist/brillouin/BrillouinZoneInfoPane.svelte +1 -1
  7. package/dist/brillouin/BrillouinZoneScene.svelte +5 -5
  8. package/dist/brillouin/compute.js +21 -21
  9. package/dist/brillouin/index.d.ts +1 -1
  10. package/dist/brillouin/index.js +0 -1
  11. package/dist/brillouin/types.d.ts +8 -13
  12. package/dist/chempot-diagram/ChemPotDiagram.svelte +3 -3
  13. package/dist/chempot-diagram/ChemPotDiagram2D.svelte +3 -4
  14. package/dist/chempot-diagram/ChemPotDiagram3D.svelte +33 -34
  15. package/dist/chempot-diagram/compute.js +1 -7
  16. package/dist/chempot-diagram/temperature.d.ts +1 -1
  17. package/dist/chempot-diagram/temperature.js +1 -3
  18. package/dist/chempot-diagram/types.d.ts +4 -9
  19. package/dist/colors/index.js +5 -5
  20. package/dist/composition/Composition.svelte +2 -1
  21. package/dist/composition/Formula.svelte +7 -4
  22. package/dist/composition/FormulaFilter.svelte +1 -3
  23. package/dist/composition/format.js +4 -4
  24. package/dist/composition/parse.d.ts +2 -1
  25. package/dist/composition/parse.js +61 -46
  26. package/dist/convex-hull/ConvexHull2D.svelte +62 -51
  27. package/dist/convex-hull/ConvexHull3D.svelte +101 -90
  28. package/dist/convex-hull/ConvexHull4D.svelte +70 -58
  29. package/dist/convex-hull/ConvexHullControls.svelte +24 -35
  30. package/dist/convex-hull/ConvexHullInfoPane.svelte +8 -5
  31. package/dist/convex-hull/ConvexHullInfoPane.svelte.d.ts +2 -0
  32. package/dist/convex-hull/ConvexHullStats.svelte +9 -2
  33. package/dist/convex-hull/ConvexHullStats.svelte.d.ts +2 -0
  34. package/dist/convex-hull/GasPressureControls.svelte +7 -7
  35. package/dist/convex-hull/StructurePopup.svelte +65 -30
  36. package/dist/convex-hull/StructurePopup.svelte.d.ts +6 -6
  37. package/dist/convex-hull/TemperatureSlider.svelte +8 -5
  38. package/dist/convex-hull/barycentric-coords.d.ts +2 -2
  39. package/dist/convex-hull/barycentric-coords.js +2 -2
  40. package/dist/convex-hull/gas-thermodynamics.js +2 -4
  41. package/dist/convex-hull/helpers.d.ts +13 -2
  42. package/dist/convex-hull/helpers.js +37 -16
  43. package/dist/convex-hull/index.d.ts +1 -0
  44. package/dist/convex-hull/index.js +1 -0
  45. package/dist/convex-hull/thermodynamics.d.ts +2 -1
  46. package/dist/convex-hull/thermodynamics.js +7 -7
  47. package/dist/convex-hull/types.d.ts +15 -15
  48. package/dist/effects.svelte.d.ts +12 -0
  49. package/dist/effects.svelte.js +37 -0
  50. package/dist/element/BohrAtom.svelte +4 -4
  51. package/dist/element/data.json.gz.d.ts +3 -1
  52. package/dist/element/index.d.ts +1 -1
  53. package/dist/element/index.js +0 -1
  54. package/dist/fermi-surface/FermiSurface.svelte +4 -4
  55. package/dist/fermi-surface/FermiSurface.svelte.d.ts +1 -1
  56. package/dist/fermi-surface/FermiSurfaceControls.svelte +15 -19
  57. package/dist/fermi-surface/FermiSurfaceControls.svelte.d.ts +1 -1
  58. package/dist/fermi-surface/FermiSurfaceScene.svelte +8 -6
  59. package/dist/fermi-surface/compute.js +2 -2
  60. package/dist/fermi-surface/export.js +13 -26
  61. package/dist/fermi-surface/parse.js +8 -12
  62. package/dist/fermi-surface/types.d.ts +2 -5
  63. package/dist/heatmap-matrix/HeatmapMatrix.svelte +21 -3
  64. package/dist/heatmap-matrix/index.js +6 -6
  65. package/dist/io/decompress.d.ts +2 -1
  66. package/dist/io/decompress.js +1 -1
  67. package/dist/io/export.js +1 -1
  68. package/dist/io/index.d.ts +1 -1
  69. package/dist/io/index.js +0 -1
  70. package/dist/io/url-drop.js +7 -1
  71. package/dist/isosurface/IsosurfaceControls.svelte +11 -25
  72. package/dist/isosurface/slice.js +1 -1
  73. package/dist/isosurface/types.js +12 -12
  74. package/dist/labels.d.ts +1 -1
  75. package/dist/labels.js +14 -11
  76. package/dist/layout/InfoTag.svelte +6 -4
  77. package/dist/layout/PropertyFilter.svelte +4 -2
  78. package/dist/layout/json-tree/JsonTree.svelte +22 -14
  79. package/dist/layout/json-tree/JsonValue.svelte +2 -2
  80. package/dist/layout/json-tree/types.d.ts +3 -2
  81. package/dist/layout/json-tree/types.js +0 -1
  82. package/dist/layout/json-tree/utils.d.ts +4 -4
  83. package/dist/layout/json-tree/utils.js +12 -20
  84. package/dist/marching-cubes.js +13 -15
  85. package/dist/math.d.ts +11 -1
  86. package/dist/math.js +15 -6
  87. package/dist/overlays/DragControlTab.svelte +98 -0
  88. package/dist/overlays/DragControlTab.svelte.d.ts +8 -0
  89. package/dist/overlays/DraggablePane.svelte +7 -84
  90. package/dist/overlays/index.d.ts +1 -0
  91. package/dist/overlays/index.js +1 -0
  92. package/dist/periodic-table/PeriodicTable.svelte +11 -11
  93. package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte +4 -2
  94. package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte.d.ts +1 -1
  95. package/dist/phase-diagram/PhaseDiagramControls.svelte +4 -9
  96. package/dist/phase-diagram/PhaseDiagramControls.svelte.d.ts +1 -1
  97. package/dist/phase-diagram/PhaseDiagramExportPane.svelte +2 -10
  98. package/dist/phase-diagram/PhaseDiagramTooltip.svelte +2 -3
  99. package/dist/phase-diagram/TdbInfoPanel.svelte +3 -3
  100. package/dist/phase-diagram/build-diagram.js +11 -18
  101. package/dist/phase-diagram/diagram-input.d.ts +5 -9
  102. package/dist/phase-diagram/index.d.ts +2 -2
  103. package/dist/phase-diagram/index.js +0 -2
  104. package/dist/phase-diagram/parse.d.ts +2 -2
  105. package/dist/phase-diagram/parse.js +6 -10
  106. package/dist/phase-diagram/svg-to-diagram.js +15 -15
  107. package/dist/phase-diagram/types.d.ts +5 -11
  108. package/dist/phase-diagram/utils.d.ts +2 -2
  109. package/dist/phase-diagram/utils.js +9 -11
  110. package/dist/plot/BarPlot.svelte +162 -314
  111. package/dist/plot/BarPlot.svelte.d.ts +5 -4
  112. package/dist/plot/BarPlotControls.svelte.d.ts +1 -1
  113. package/dist/plot/BinnedScatterPlot.svelte +1114 -0
  114. package/dist/plot/BinnedScatterPlot.svelte.d.ts +66 -0
  115. package/dist/plot/ColorBar.svelte +19 -17
  116. package/dist/plot/ColorBar.svelte.d.ts +1 -1
  117. package/dist/plot/FillArea.svelte +2 -4
  118. package/dist/plot/FillArea.svelte.d.ts +1 -1
  119. package/dist/plot/Histogram.svelte +167 -281
  120. package/dist/plot/Histogram.svelte.d.ts +1 -1
  121. package/dist/plot/HistogramControls.svelte.d.ts +1 -1
  122. package/dist/plot/InteractiveAxisLabel.svelte +5 -3
  123. package/dist/plot/InteractiveAxisLabel.svelte.d.ts +1 -1
  124. package/dist/plot/PlotAxis.svelte +169 -0
  125. package/dist/plot/PlotAxis.svelte.d.ts +24 -0
  126. package/dist/plot/PlotControls.svelte.d.ts +1 -1
  127. package/dist/plot/ReferenceLine3D.svelte +53 -51
  128. package/dist/plot/ReferencePlane.svelte +39 -42
  129. package/dist/plot/ScatterPlot.svelte +300 -367
  130. package/dist/plot/ScatterPlot.svelte.d.ts +8 -5
  131. package/dist/plot/ScatterPlot3D.svelte +33 -6
  132. package/dist/plot/ScatterPlot3D.svelte.d.ts +3 -2
  133. package/dist/plot/ScatterPlot3DControls.svelte +9 -9
  134. package/dist/plot/ScatterPlotControls.svelte +3 -4
  135. package/dist/plot/ScatterPoint.svelte +18 -27
  136. package/dist/plot/ScatterPoint.svelte.d.ts +4 -3
  137. package/dist/plot/Surface3D.svelte +4 -7
  138. package/dist/plot/ZeroLines.svelte +2 -1
  139. package/dist/plot/ZeroLines.svelte.d.ts +2 -1
  140. package/dist/plot/ZoomRect.svelte +2 -2
  141. package/dist/plot/ZoomRect.svelte.d.ts +3 -3
  142. package/dist/plot/adaptive-density.d.ts +69 -0
  143. package/dist/plot/adaptive-density.js +191 -0
  144. package/dist/plot/auto-place.d.ts +43 -0
  145. package/dist/plot/auto-place.js +122 -0
  146. package/dist/plot/axis-utils.js +3 -5
  147. package/dist/plot/binned-scatter-types.d.ts +59 -0
  148. package/dist/plot/binned-scatter-types.js +1 -0
  149. package/dist/plot/data-cleaning.js +1 -1
  150. package/dist/plot/data-transform.js +1 -1
  151. package/dist/plot/fill-utils.d.ts +4 -9
  152. package/dist/plot/fill-utils.js +29 -44
  153. package/dist/plot/index.d.ts +4 -0
  154. package/dist/plot/index.js +2 -0
  155. package/dist/plot/interactions.d.ts +4 -4
  156. package/dist/plot/interactions.js +4 -3
  157. package/dist/plot/layout.d.ts +20 -2
  158. package/dist/plot/layout.js +59 -16
  159. package/dist/plot/reference-line.d.ts +1 -1
  160. package/dist/plot/reference-line.js +9 -11
  161. package/dist/plot/scales.d.ts +1 -1
  162. package/dist/plot/scales.js +20 -23
  163. package/dist/plot/types.d.ts +30 -58
  164. package/dist/plot/types.js +2 -6
  165. package/dist/plot/utils/label-placement.d.ts +24 -3
  166. package/dist/plot/utils/label-placement.js +82 -12
  167. package/dist/plot/utils/series-visibility.d.ts +8 -2
  168. package/dist/plot/utils/series-visibility.js +23 -5
  169. package/dist/rdf/RdfPlot.svelte +5 -5
  170. package/dist/rdf/calc-rdf.js +3 -3
  171. package/dist/sanitize.d.ts +2 -0
  172. package/dist/sanitize.js +2 -0
  173. package/dist/spectral/Bands.svelte +1 -1
  174. package/dist/spectral/BandsAndDos.svelte +22 -16
  175. package/dist/spectral/BrillouinBandsDos.svelte +20 -16
  176. package/dist/spectral/Dos.svelte +1 -1
  177. package/dist/spectral/helpers.d.ts +4 -2
  178. package/dist/spectral/helpers.js +44 -35
  179. package/dist/spectral/index.d.ts +1 -1
  180. package/dist/spectral/index.js +0 -1
  181. package/dist/structure/AtomLegend.svelte +23 -6
  182. package/dist/structure/AtomLegend.svelte.d.ts +1 -0
  183. package/dist/structure/CanvasTooltip.svelte +9 -9
  184. package/dist/structure/CanvasTooltip.svelte.d.ts +1 -1
  185. package/dist/structure/CellSelect.svelte +14 -16
  186. package/dist/structure/Structure.svelte +317 -68
  187. package/dist/structure/Structure.svelte.d.ts +4 -2
  188. package/dist/structure/StructureControls.svelte +20 -45
  189. package/dist/structure/StructureExportPane.svelte +2 -1
  190. package/dist/structure/StructureInfoPane.svelte +10 -8
  191. package/dist/structure/StructureScene.svelte +527 -177
  192. package/dist/structure/StructureScene.svelte.d.ts +5 -2
  193. package/dist/structure/atom-properties.js +4 -4
  194. package/dist/structure/bond-order-perception.js +115 -98
  195. package/dist/structure/bonding.d.ts +27 -1
  196. package/dist/structure/bonding.js +187 -16
  197. package/dist/structure/export.js +1 -1
  198. package/dist/structure/index.d.ts +3 -2
  199. package/dist/structure/index.js +0 -2
  200. package/dist/structure/parse.js +88 -59
  201. package/dist/symmetry/WyckoffTable.svelte +7 -0
  202. package/dist/symmetry/index.js +13 -14
  203. package/dist/table/HeatmapTable.svelte +45 -66
  204. package/dist/table/HeatmapTable.svelte.d.ts +1 -1
  205. package/dist/table/ToggleMenu.svelte +19 -10
  206. package/dist/theme/themes.mjs +12 -0
  207. package/dist/tooltip/index.d.ts +1 -1
  208. package/dist/tooltip/index.js +0 -1
  209. package/dist/trajectory/Trajectory.svelte +43 -15
  210. package/dist/trajectory/TrajectoryInfoPane.svelte +2 -2
  211. package/dist/trajectory/extract.js +1 -1
  212. package/dist/trajectory/frame-reader.js +4 -4
  213. package/dist/trajectory/helpers.d.ts +5 -4
  214. package/dist/trajectory/helpers.js +9 -17
  215. package/dist/trajectory/index.d.ts +2 -2
  216. package/dist/trajectory/index.js +2 -2
  217. package/dist/trajectory/parse/ase.js +4 -4
  218. package/dist/trajectory/parse/hdf5.js +1 -1
  219. package/dist/trajectory/parse/index.js +2 -3
  220. package/dist/trajectory/parse/lammps.js +1 -1
  221. package/dist/trajectory/parse/vasp.js +1 -1
  222. package/dist/trajectory/plotting.d.ts +1 -1
  223. package/dist/trajectory/plotting.js +38 -38
  224. package/dist/trajectory/types.d.ts +1 -1
  225. package/dist/utils.d.ts +1 -0
  226. package/dist/utils.js +9 -0
  227. package/dist/xrd/calc-xrd.js +3 -4
  228. package/dist/xrd/parse.js +1 -1
  229. package/package.json +42 -22
@@ -0,0 +1,43 @@
1
+ import type { Sides } from './layout';
2
+ export declare const DECOR_GAP = 8;
3
+ type Pt = {
4
+ x: number;
5
+ y: number;
6
+ };
7
+ type Size = {
8
+ width: number;
9
+ height: number;
10
+ };
11
+ export declare const has_explicit_position: (style?: string | null) => boolean;
12
+ export declare const measured_footprint: (el: HTMLElement | null | undefined, fallback: Size) => Size;
13
+ export declare function build_obstacles_norm(series: {
14
+ points: Pt[];
15
+ draws_line?: boolean;
16
+ }[], base_w: number, base_h: number): Pt[];
17
+ export declare function clip_bar(vertical: boolean, cross: number, a: number, b: number): {
18
+ points: Pt[];
19
+ draws_line: boolean;
20
+ } | null;
21
+ export type DecorationInput = {
22
+ footprint: Size;
23
+ clearance?: number;
24
+ };
25
+ export type DecorationLayout = {
26
+ pad: Required<Sides>;
27
+ legend_outside: boolean;
28
+ legend_pos: Pt;
29
+ colorbar_outside: boolean;
30
+ colorbar_style: string;
31
+ };
32
+ export declare function place_decorations(cfg: {
33
+ base_pad: Required<Sides>;
34
+ width: number;
35
+ height: number;
36
+ obstacles_norm: Pt[];
37
+ legend?: DecorationInput | null;
38
+ colorbar?: (DecorationInput & {
39
+ horizontal?: boolean;
40
+ }) | null;
41
+ gap?: number;
42
+ }): DecorationLayout;
43
+ export {};
@@ -0,0 +1,122 @@
1
+ import { compute_element_placement, sample_series_obstacle_points } from './layout';
2
+ // Shared "move a decoration (legend/colorbar) outside the plot when interior overlap is
3
+ // unavoidable" logic, reused by every 2D plot (ScatterPlot/BarPlot/Histogram/BinnedScatterPlot).
4
+ export const DECOR_GAP = 8; // px gap between an outside decoration and the plot edge
5
+ // True when the user pinned a decoration via its style (an edge property or position:absolute),
6
+ // in which case auto-placement must leave it alone.
7
+ export const has_explicit_position = (style) => /(^|[;{]\s*)(top|bottom|left|right)\s*:|position\s*:\s*absolute/.test(style ?? ``);
8
+ // A decoration's pixel footprint: its rendered box once laid out, else `fallback` (offset dims read
9
+ // 0 before first render). Used to decide crowding before the real size is known.
10
+ export const measured_footprint = (el, fallback) => el?.offsetWidth && el?.offsetHeight
11
+ ? { width: el.offsetWidth, height: el.offsetHeight }
12
+ : fallback;
13
+ // Build the obstacle field in normalized [0,1] plot coords from one or more series. Callers pass
14
+ // points already normalized to [0,1] with y=0 at the top. Built from data (not pixel scales) so the
15
+ // crowding decision below is independent of any margins reserved for outside decorations — this
16
+ // prevents a reserve -> data-shift -> re-decide oscillation loop.
17
+ export function build_obstacles_norm(series, base_w, base_h) {
18
+ const step = 12 / Math.max(base_w, base_h, 1);
19
+ const out = [];
20
+ for (const srs of series) {
21
+ // avoid out.push(...arr): a long series would overflow the call-stack arg limit
22
+ for (const pt of sample_series_obstacle_points(srs.points, srs.draws_line ?? false, step)) {
23
+ if (isFinite(pt.x) && isFinite(pt.y))
24
+ out.push(pt);
25
+ }
26
+ }
27
+ return out;
28
+ }
29
+ // Build an obstacle segment for one bar, clipped to the visible [0,1]x[0,1] box (null if off-plot).
30
+ // Clipping is essential: when zoomed in, off-screen bars normalize to huge coords and would make
31
+ // sample_series_obstacle_points emit millions of points. `cross` is the fixed bar position; `a`/`b`
32
+ // are the span endpoints (baseline -> tip) along the other axis.
33
+ export function clip_bar(vertical, cross, a, b) {
34
+ if (!(cross >= 0 && cross <= 1))
35
+ return null;
36
+ const lo = Math.max(0, Math.min(a, b));
37
+ const hi = Math.min(1, Math.max(a, b));
38
+ if (hi < lo)
39
+ return null;
40
+ const points = vertical
41
+ ? [
42
+ { x: cross, y: lo },
43
+ { x: cross, y: hi },
44
+ ]
45
+ : [
46
+ { x: lo, y: cross },
47
+ { x: hi, y: cross },
48
+ ];
49
+ return { points, draws_line: true };
50
+ }
51
+ // A decoration is "crowded out" only when even the emptiest interior spot is at least this dense
52
+ // relative to the plot-wide average. A single clear region (e.g. one sparse quadrant) keeps the
53
+ // decoration inside; a roughly uniformly-full plot pushes it out. Using a relative ratio (not "any
54
+ // overlap") keeps the decision stable for wide decorations that merely clip a dense neighbor.
55
+ const CROWDING_RATIO = 0.5;
56
+ // True when even the best interior spot for `footprint` (px) is too dense to host the decoration
57
+ function is_crowded(obstacles, footprint, base_w, base_h, clearance) {
58
+ if (!obstacles.length || base_w <= 0 || base_h <= 0)
59
+ return false;
60
+ const fw = footprint.width / base_w;
61
+ const fh = footprint.height / base_h;
62
+ if (fw >= 1 || fh >= 1)
63
+ return true; // too big to fit inside -> outside
64
+ const best = compute_element_placement({
65
+ plot_bounds: { x: 0, y: 0, width: 1, height: 1 },
66
+ element_size: { width: fw, height: fh },
67
+ axis_clearance: clearance / Math.min(base_w, base_h),
68
+ points: obstacles,
69
+ });
70
+ const in_box = obstacles.filter((pt) => pt.x >= best.x && pt.x <= best.x + fw && pt.y >= best.y && pt.y <= best.y + fh).length;
71
+ // expected count if obstacles were spread uniformly = total * box-area fraction
72
+ return in_box > CROWDING_RATIO * obstacles.length * fw * fh;
73
+ }
74
+ // Decide which decorations must move outside (interior placement unavoidably overlaps data), the
75
+ // reserved padding, and the outside positions/styles. `base_pad` must be decoration-independent so
76
+ // the crowding decision can't see the reservation it produces.
77
+ export function place_decorations(cfg) {
78
+ const { base_pad, width, height, obstacles_norm, legend, colorbar, gap = DECOR_GAP } = cfg;
79
+ const base_w = width - base_pad.l - base_pad.r;
80
+ const base_h = height - base_pad.t - base_pad.b;
81
+ const colorbar_outside = colorbar != null &&
82
+ is_crowded(obstacles_norm, colorbar.footprint, base_w, base_h, colorbar.clearance ?? 15);
83
+ const cbar_horizontal = colorbar?.horizontal ?? false;
84
+ const colorbar_takes_right = colorbar_outside && !cbar_horizontal; // vertical colorbar -> right
85
+ const cbar_w = colorbar?.footprint.width ?? 0;
86
+ const cbar_h = colorbar?.footprint.height ?? 0;
87
+ const legend_outside = legend != null &&
88
+ is_crowded(obstacles_norm, legend.footprint, base_w, base_h, legend.clearance ?? 12);
89
+ const legend_w = legend?.footprint.width ?? 0;
90
+ const legend_h = legend?.footprint.height ?? 0;
91
+ // Put a narrow/tall legend on the right (wastes less reserved margin than a wide bottom strip);
92
+ // a wide/short legend goes below. Skip the right side if a vertical colorbar already took it.
93
+ const legend_right = legend_outside && !colorbar_takes_right && legend_h * base_w > legend_w * base_h;
94
+ const legend_bottom = legend_outside && !legend_right;
95
+ // colorbar: horizontal -> above (reserves top), vertical -> right. legend: right or bottom.
96
+ // A right legend replaces the plot's right margin (it sits flush at the edge), so reserve only its
97
+ // width + a gap on each side instead of stacking it on top of base_pad.r (which left a wide gap).
98
+ const pad = {
99
+ t: base_pad.t + (colorbar_outside && cbar_horizontal ? cbar_h + gap : 0),
100
+ l: base_pad.l,
101
+ b: base_pad.b + (legend_bottom ? legend_h + gap : 0),
102
+ r: legend_right
103
+ ? legend_w + 2 * gap
104
+ : base_pad.r + (colorbar_takes_right ? cbar_w + gap : 0),
105
+ };
106
+ const colorbar_style = !colorbar_outside
107
+ ? ``
108
+ : cbar_horizontal
109
+ ? `position: absolute; top: ${gap}px; inset-inline: 0; margin-inline: auto; width: calc(100% - ${base_pad.l + base_pad.r}px); max-width: 100%;`
110
+ : `position: absolute; right: ${gap}px; inset-block: 0; margin-block: auto; height: calc(100% - ${base_pad.t + base_pad.b}px); max-height: 100%;`;
111
+ // right: flush to the right edge, vertically centered in the plot area; bottom: centered below
112
+ const legend_pos = legend_right
113
+ ? {
114
+ x: width - legend_w - gap,
115
+ y: base_pad.t + (height - base_pad.t - base_pad.b - legend_h) / 2,
116
+ }
117
+ : {
118
+ x: base_pad.l + (width - base_pad.l - base_pad.r - legend_w) / 2,
119
+ y: height - legend_h - gap,
120
+ };
121
+ return { pad, legend_outside, legend_pos, colorbar_outside, colorbar_style };
122
+ }
@@ -1,6 +1,6 @@
1
1
  // Shared utilities for interactive axis functionality
2
- // Merge new series with preserved UI state from old series
3
- // Matches by id first (string or number), then falls back to index
2
+ // Merge new series with preserved UI state from old series.
3
+ // Matches by stable id first, then by index only for ordered id-less series.
4
4
  export function merge_series_state(old_series, new_series) {
5
5
  // Build id lookup map for O(1) matching (string or number ids)
6
6
  const by_id = new Map();
@@ -9,9 +9,7 @@ export function merge_series_state(old_series, new_series) {
9
9
  by_id.set(srs.id, srs);
10
10
  }
11
11
  return new_series.map((new_srs, idx) => {
12
- // Match by id if available (string or number), otherwise fall back to index
13
- const old_srs = (new_srs.id !== undefined && new_srs.id !== `` ? by_id.get(new_srs.id) : undefined) ??
14
- old_series[idx];
12
+ const old_srs = new_srs.id !== undefined && new_srs.id !== `` ? by_id.get(new_srs.id) : old_series[idx];
15
13
  if (!old_srs) {
16
14
  return new_srs;
17
15
  }
@@ -0,0 +1,59 @@
1
+ import type { D3ColorSchemeName, D3InterpolateName } from '../colors';
2
+ import type { DenseInternalPoint } from './adaptive-density';
3
+ import type ColorBar from './ColorBar.svelte';
4
+ import type { LabelPlacementConfig, ScaleType, ScatterHandlerProps } from './types';
5
+ import type { ComponentProps, Snippet } from 'svelte';
6
+ export type BinnedColorScaleConfig = {
7
+ type?: ScaleType;
8
+ scheme?: D3ColorSchemeName | D3InterpolateName;
9
+ value_range?: [number, number];
10
+ } | D3InterpolateName;
11
+ export type BinnedSizeScaleConfig = {
12
+ type?: ScaleType;
13
+ radius_range?: [number, number];
14
+ value_range?: [number, number];
15
+ pick_radius?: number | `auto`;
16
+ };
17
+ export type BinnedDensityConfig = {
18
+ bin_px?: number;
19
+ color_scale?: BinnedColorScaleConfig;
20
+ color_bar?: ComponentProps<typeof ColorBar> | null;
21
+ auto_point_mode?: false | {
22
+ max_points?: number;
23
+ max_points_per_px?: number;
24
+ };
25
+ bin_click?: `zoom` | `point` | `none`;
26
+ };
27
+ export type BinnedRefLine = {
28
+ x1: number;
29
+ y1: number;
30
+ x2: number;
31
+ y2: number;
32
+ color?: string;
33
+ dash?: string;
34
+ width?: number;
35
+ };
36
+ export type BinnedOverlaysConfig = {
37
+ ref_lines?: BinnedRefLine[];
38
+ };
39
+ export type BinnedPointBasePayload<Metadata extends Record<string, unknown> = Record<string, unknown>> = ScatterHandlerProps<Metadata> & {
40
+ point: DenseInternalPoint<Metadata>;
41
+ color?: string;
42
+ };
43
+ export type BinnedPointDataFn<Metadata extends Record<string, unknown> = Record<string, unknown>, PointData extends Record<string, unknown> = Record<string, unknown>> = (payload: BinnedPointBasePayload<Metadata>) => PointData | null | undefined;
44
+ export type BinnedPointPayload<Metadata extends Record<string, unknown> = Record<string, unknown>, PointData extends Record<string, unknown> = Record<string, unknown>> = BinnedPointBasePayload<Metadata> & {
45
+ point_data?: PointData;
46
+ };
47
+ export type BinnedPointTooltipPayload<Metadata extends Record<string, unknown> = Record<string, unknown>, PointData extends Record<string, unknown> = Record<string, unknown>> = BinnedPointPayload<Metadata, PointData>;
48
+ export type BinnedPointLabelPlacementConfig = Partial<LabelPlacementConfig>;
49
+ export type BinnedPointLabelsConfig<Metadata extends Record<string, unknown> = Record<string, unknown>, PointData extends Record<string, unknown> = Record<string, unknown>> = {
50
+ render?: Snippet<[BinnedPointPayload<Metadata, PointData>]>;
51
+ measure_text?: (payload: BinnedPointPayload<Metadata, PointData>) => string;
52
+ font_size?: string;
53
+ max_count?: number;
54
+ gap_px?: number;
55
+ placement?: BinnedPointLabelPlacementConfig;
56
+ leaders?: {
57
+ min_length_px?: number;
58
+ };
59
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -696,7 +696,7 @@ export function clean_xyz(x_values, y_values, z_values, config = {}) {
696
696
  const bounds_kept = [];
697
697
  for (let idx = 0; idx < filtered.x.length; idx++) {
698
698
  const primary_val = filtered[primary][idx];
699
- // Use x-axis value for dynamic bounds computation (e.g., max: (x) => x * 2)
699
+ // Use x-axis value for dynamic bounds computation (e.g., max: (x_val) => x_val * 2)
700
700
  if (is_in_bounds(primary_val, filtered.x[idx], bounds)) {
701
701
  bounds_kept.push(idx);
702
702
  }
@@ -26,7 +26,7 @@ export const prepare_legend_data = (series) => series.map((series_data, series_i
26
26
  }));
27
27
  // Create data points from series for analysis
28
28
  export const create_data_points = (series, filter_fn) => series
29
- .filter(filter_fn || ((srs) => srs.visible ?? true))
29
+ .filter(filter_fn ?? ((srs) => srs.visible ?? true))
30
30
  .flatMap(({ x: xs, y: ys }, series_idx) => {
31
31
  const length = Math.min(xs.length, ys.length);
32
32
  if (xs.length !== ys.length) {
@@ -5,24 +5,19 @@ export interface FillPathPoint {
5
5
  y1: number;
6
6
  y2: number;
7
7
  }
8
+ type FillPathArrays = Record<keyof FillPathPoint, number[]>;
8
9
  export interface InterpolatedSeries {
9
10
  x: number[];
10
11
  y_a: number[];
11
12
  y_b: number[];
12
13
  }
13
- export interface FilteredFillData {
14
- x: number[];
15
- y1: number[];
16
- y2: number[];
14
+ export interface FilteredFillData extends FillPathArrays {
17
15
  original_indices: number[];
18
16
  }
19
17
  export interface ConditionedFillData {
20
18
  segments: FillPathPoint[][];
21
19
  }
22
- export interface ClampedFillData {
23
- x: number[];
24
- y1: number[];
25
- y2: number[];
20
+ export interface ClampedFillData extends FillPathArrays {
26
21
  clamped_indices: number[];
27
22
  }
28
23
  export declare function resolve_series_ref(ref: {
@@ -47,5 +42,5 @@ export declare function apply_where_condition(x_values: readonly number[], y1_va
47
42
  export declare function clamp_for_log_scale(x_values: readonly number[], y1_values: readonly number[], y2_values: readonly number[], y_scale_type: ScaleType, x_scale_type?: ScaleType): ClampedFillData;
48
43
  export declare function generate_fill_path(data: readonly FillPathPoint[], curve_type?: FillCurveType): string;
49
44
  export declare function convert_error_band_to_fill_region(error_band: ErrorBand, series: readonly DataSeries[], default_color?: string): FillRegion | null;
50
- export declare function is_fill_gradient(fill: string | FillGradient | undefined): fill is FillGradient;
45
+ export declare const is_fill_gradient: (fill: string | FillGradient | undefined) => fill is FillGradient;
51
46
  export {};
@@ -84,46 +84,35 @@ export function resolve_boundary(boundary, series, x_values, domains) {
84
84
  if (typeof boundary === `number`) {
85
85
  return x_values.map(() => boundary);
86
86
  }
87
- switch (boundary.type) {
88
- case `series`: {
89
- const resolved = resolve_series_ref(boundary, series);
90
- if (!resolved)
91
- return null;
92
- // Interpolate to match x_values
93
- const interpolated = interpolate_series({ x: x_values, y: x_values.map(() => 0) }, { x: resolved.x, y: resolved.y }, `linear`);
94
- return interpolated.y_b;
95
- }
96
- case `constant`:
97
- return x_values.map(() => boundary.value);
98
- case `axis`: {
99
- // For axis boundaries, return the edge value of the appropriate axis domain
100
- let value;
101
- if (boundary.value !== undefined) {
102
- value = boundary.value;
103
- }
104
- else if (boundary.axis === `y2` && domains.y2_domain) {
105
- value = domains.y2_domain[0];
106
- }
107
- else {
108
- // Default: use bottom of y-domain (works for x-axis and y-axis)
109
- value = domains.y_domain[0];
110
- }
111
- return x_values.map(() => value);
112
- }
113
- case `function`:
114
- return x_values.map((curr_x) => boundary.fn(curr_x));
115
- case `data`:
116
- if (boundary.values.length === 0)
117
- return Array(x_values.length).fill(NaN);
118
- // If lengths match, use directly; otherwise interpolate
119
- if (boundary.values.length === x_values.length) {
120
- return [...boundary.values];
121
- }
122
- // Lengths don't match: truncate if values is longer, or extend last value if shorter
123
- return x_values.map((_, idx) => boundary.values[idx] ?? boundary.values[boundary.values.length - 1]);
124
- default:
87
+ if (boundary.type === `series`) {
88
+ const resolved = resolve_series_ref(boundary, series);
89
+ if (!resolved)
125
90
  return null;
91
+ // Interpolate to match x_values
92
+ const interpolated = interpolate_series({ x: x_values, y: x_values.map(() => 0) }, { x: resolved.x, y: resolved.y }, `linear`);
93
+ return interpolated.y_b;
126
94
  }
95
+ if (boundary.type === `constant`)
96
+ return x_values.map(() => boundary.value);
97
+ if (boundary.type === `axis`) {
98
+ // For axis boundaries, return the edge value of the appropriate axis domain
99
+ const value = boundary.value ??
100
+ (boundary.axis === `y2` ? domains.y2_domain?.[0] : undefined) ??
101
+ domains.y_domain[0];
102
+ return x_values.map(() => value);
103
+ }
104
+ if (boundary.type === `function`)
105
+ return x_values.map((curr_x) => boundary.fn(curr_x));
106
+ if (boundary.type === `data`) {
107
+ if (boundary.values.length === 0)
108
+ return Array(x_values.length).fill(NaN);
109
+ // If lengths match, use directly; otherwise interpolate
110
+ if (boundary.values.length === x_values.length)
111
+ return [...boundary.values];
112
+ // Lengths don't match: truncate if values is longer, or extend last value if shorter
113
+ return x_values.map((_, idx) => boundary.values[idx] ?? boundary.values[boundary.values.length - 1]);
114
+ }
115
+ return null;
127
116
  }
128
117
  export function apply_range_constraints(x_values, y1_values, y2_values, region) {
129
118
  const [x_min, x_max] = region.x_range ?? [null, null];
@@ -305,9 +294,7 @@ export function generate_fill_path(data, curve_type = `monotoneX`) {
305
294
  return area_generator(data) ?? ``;
306
295
  }
307
296
  // Helper to expand error definition to array
308
- function expand_error(err, length) {
309
- return typeof err === `number` ? Array(length).fill(err) : err;
310
- }
297
+ const expand_error = (err, length) => typeof err === `number` ? Array(length).fill(err) : err;
311
298
  // Convert an ErrorBand convenience type to a full FillRegion
312
299
  export function convert_error_band_to_fill_region(error_band, series, default_color) {
313
300
  const resolved = resolve_series_ref(error_band.series, series);
@@ -332,6 +319,4 @@ export function convert_error_band_to_fill_region(error_band, series, default_co
332
319
  };
333
320
  }
334
321
  // Type guard to check if fill is a gradient
335
- export function is_fill_gradient(fill) {
336
- return typeof fill === `object` && fill !== null && `type` in fill && `stops` in fill;
337
- }
322
+ export const is_fill_gradient = (fill) => typeof fill === `object` && fill !== null && `type` in fill && `stops` in fill;
@@ -2,6 +2,10 @@ export { default as AxisLabel } from './AxisLabel.svelte';
2
2
  export { default as BarPlot } from './BarPlot.svelte';
3
3
  export { default as BarPlotControls } from './BarPlotControls.svelte';
4
4
  export { default as ColorBar } from './ColorBar.svelte';
5
+ export { default as BinnedScatterPlot } from './BinnedScatterPlot.svelte';
6
+ export type { BinnedColorScaleConfig, BinnedDensityConfig, BinnedOverlaysConfig, BinnedPointBasePayload, BinnedPointDataFn, BinnedPointLabelPlacementConfig, BinnedPointLabelsConfig, BinnedPointPayload, BinnedPointTooltipPayload, BinnedRefLine, BinnedSizeScaleConfig, } from './binned-scatter-types';
7
+ export type { DensePointSeries } from './adaptive-density';
8
+ export { default as PlotAxis } from './PlotAxis.svelte';
5
9
  export { default as ColorScaleSelect } from './ColorScaleSelect.svelte';
6
10
  export * from './data-cleaning';
7
11
  export { default as ElementScatter } from './ElementScatter.svelte';
@@ -2,6 +2,8 @@ export { default as AxisLabel } from './AxisLabel.svelte';
2
2
  export { default as BarPlot } from './BarPlot.svelte';
3
3
  export { default as BarPlotControls } from './BarPlotControls.svelte';
4
4
  export { default as ColorBar } from './ColorBar.svelte';
5
+ export { default as BinnedScatterPlot } from './BinnedScatterPlot.svelte';
6
+ export { default as PlotAxis } from './PlotAxis.svelte';
5
7
  export { default as ColorScaleSelect } from './ColorScaleSelect.svelte';
6
8
  export * from './data-cleaning';
7
9
  export { default as ElementScatter } from './ElementScatter.svelte';
@@ -1,9 +1,9 @@
1
- import type { Vec2 } from '../math';
2
- import type { XyObj, Y2SyncConfig, Y2SyncMode } from './types';
3
- export declare function get_relative_coords(evt: MouseEvent): XyObj | null;
1
+ import type { Point2D, Vec2 } from '../math';
2
+ import type { Y2SyncConfig, Y2SyncMode } from './types';
3
+ export declare function get_relative_coords(evt: MouseEvent): Point2D | null;
4
4
  export declare function normalize_y2_sync(sync: Y2SyncConfig | Y2SyncMode | undefined): Y2SyncConfig;
5
5
  export declare function sync_y2_range(y1_range: Vec2, y2_base_range: Vec2, sync: Y2SyncConfig): Vec2;
6
- export declare function pan_range(current: Vec2, delta: number): Vec2;
6
+ export declare const pan_range: (current: Vec2, delta: number) => Vec2;
7
7
  export declare function pixels_to_data_delta(pixel_delta: number, data_range: Vec2, pixel_range: number): number;
8
8
  export declare const PINCH_ZOOM_THRESHOLD = 0.1;
9
9
  export declare function expand_range_if_needed(current: Vec2, new_range: Vec2): {
@@ -60,9 +60,10 @@ export function sync_y2_range(y1_range, y2_base_range, sync) {
60
60
  return y2_base_range;
61
61
  }
62
62
  // Shift a range by a delta amount (no bounds constraint for free panning)
63
- export function pan_range(current, delta) {
64
- return [current[0] + delta, current[1] + delta];
65
- }
63
+ export const pan_range = (current, delta) => [
64
+ current[0] + delta,
65
+ current[1] + delta,
66
+ ];
66
67
  // Convert pixel delta to data delta using current data range and pixel range
67
68
  export function pixels_to_data_delta(pixel_delta, data_range, pixel_range) {
68
69
  if (pixel_range === 0)
@@ -1,7 +1,17 @@
1
- import type { AxisConfig, Sides } from './';
1
+ import type { AxisConfig } from './types';
2
+ export type Sides = {
3
+ t?: number;
4
+ b?: number;
5
+ l?: number;
6
+ r?: number;
7
+ };
2
8
  export declare const LABEL_GAP_DEFAULT = 30;
3
9
  export declare const filter_padding: (padding: Partial<Sides> | undefined | null, defaults: Required<Sides>) => Required<Sides>;
4
10
  export declare function measure_text_width(text: string, font?: string): number;
11
+ export declare function measure_full_footprint(el: HTMLElement): {
12
+ width: number;
13
+ height: number;
14
+ };
5
15
  export interface AutoPaddingConfig {
6
16
  padding: Partial<Sides>;
7
17
  default_padding: Required<Sides>;
@@ -43,6 +53,7 @@ export interface ElementPlacementConfig {
43
53
  width: number;
44
54
  height: number;
45
55
  };
56
+ element?: HTMLElement | null;
46
57
  axis_clearance?: number;
47
58
  exclude_rects?: Rect[];
48
59
  points: {
@@ -56,5 +67,12 @@ export interface ElementPlacementResult {
56
67
  y: number;
57
68
  score: number;
58
69
  }
59
- export declare function rects_overlap(r1: Rect, r2: Rect): boolean;
70
+ export declare const rects_overlap: (r1: Rect, r2: Rect) => boolean;
71
+ export declare function sample_series_obstacle_points(pixel_points: {
72
+ x: number;
73
+ y: number;
74
+ }[], draws_line: boolean, step: number): {
75
+ x: number;
76
+ y: number;
77
+ }[];
60
78
  export declare function compute_element_placement(config: ElementPlacementConfig): ElementPlacementResult;
@@ -13,15 +13,30 @@ let measurement_canvas = null;
13
13
  export function measure_text_width(text, font = `12px sans-serif`) {
14
14
  if (typeof document === `undefined`)
15
15
  return 0;
16
- if (!measurement_canvas) {
17
- measurement_canvas = document.createElement(`canvas`);
18
- }
16
+ measurement_canvas ??= document.createElement(`canvas`);
19
17
  const ctx = measurement_canvas.getContext(`2d`);
20
18
  if (!ctx)
21
19
  return 0;
22
20
  ctx.font = font;
23
21
  return ctx.measureText(text).width;
24
22
  }
23
+ // Measure an element's full visual footprint, including descendants that overflow its
24
+ // offset box. Colorbar tick labels are position:absolute outside the bar, so
25
+ // offsetWidth/offsetHeight underestimate the space they actually occupy — which lets
26
+ // auto-placement put the colorbar where its labels overlap the axes.
27
+ export function measure_full_footprint(el) {
28
+ const root = el.getBoundingClientRect();
29
+ let right = root.right;
30
+ let bottom = root.bottom;
31
+ for (const child of el.querySelectorAll(`*`)) {
32
+ const rect = child.getBoundingClientRect();
33
+ if (rect.width === 0 && rect.height === 0)
34
+ continue;
35
+ right = Math.max(right, rect.right);
36
+ bottom = Math.max(bottom, rect.bottom);
37
+ }
38
+ return { width: right - root.left, height: bottom - root.top };
39
+ }
25
40
  // Measure the widest formatted tick label. Used for auto-padding and label placement.
26
41
  export const measure_max_tick_width = (ticks, format = ``) => ticks.length === 0
27
42
  ? 0
@@ -116,18 +131,41 @@ const DISTANCE_WEIGHT = 0.001;
116
131
  const CORNER_WEIGHT = 5.0;
117
132
  const MAX_SAMPLE_POINTS = 500;
118
133
  // Check if a point is inside a rectangle
119
- function point_in_rect(point, rect) {
120
- return (point.x >= rect.x &&
121
- point.x <= rect.x + rect.width &&
122
- point.y >= rect.y &&
123
- point.y <= rect.y + rect.height);
124
- }
134
+ const point_in_rect = (point, rect) => point.x >= rect.x &&
135
+ point.x <= rect.x + rect.width &&
136
+ point.y >= rect.y &&
137
+ point.y <= rect.y + rect.height;
125
138
  // Check if two rectangles overlap
126
- export function rects_overlap(r1, r2) {
127
- return !(r1.x + r1.width <= r2.x ||
128
- r2.x + r2.width <= r1.x ||
129
- r1.y + r1.height <= r2.y ||
130
- r2.y + r2.height <= r1.y);
139
+ export const rects_overlap = (r1, r2) => !(r1.x + r1.width <= r2.x ||
140
+ r2.x + r2.width <= r1.x ||
141
+ r1.y + r1.height <= r2.y ||
142
+ r2.y + r2.height <= r1.y);
143
+ // Build placement obstacles for one series' pixel-space polyline. Always includes the finite
144
+ // vertices; when the series draws a connecting line, also walks each segment between
145
+ // consecutive finite vertices at ~step px so legend/colorbar auto-placement avoids the line,
146
+ // not just sparse markers (e.g. a steep y=x^2 line has few markers but a long visible segment).
147
+ export function sample_series_obstacle_points(pixel_points, draws_line, step) {
148
+ const obstacles = [];
149
+ let prev = null;
150
+ for (const point of pixel_points) {
151
+ if (!isFinite(point.x) || !isFinite(point.y)) {
152
+ prev = null; // non-finite breaks the line; don't sample across the gap
153
+ continue;
154
+ }
155
+ obstacles.push(point);
156
+ if (draws_line && prev && step > 0) {
157
+ const n_samples = Math.floor(Math.hypot(point.x - prev.x, point.y - prev.y) / step);
158
+ for (let idx = 1; idx < n_samples; idx++) {
159
+ const frac = idx / n_samples;
160
+ obstacles.push({
161
+ x: prev.x + (point.x - prev.x) * frac,
162
+ y: prev.y + (point.y - prev.y) * frac,
163
+ });
164
+ }
165
+ }
166
+ prev = point;
167
+ }
168
+ return obstacles;
131
169
  }
132
170
  // Find the best placement position using continuous grid sampling
133
171
  // Scores each candidate position by:
@@ -135,10 +173,15 @@ export function rects_overlap(r1, r2) {
135
173
  // 2. Overlap with exclusion rectangles (heavy penalty)
136
174
  // 3. Distance to nearest point (tie-breaker, farther = better)
137
175
  export function compute_element_placement(config) {
138
- const { plot_bounds, element_size, axis_clearance = 40, exclude_rects = [], points, grid_resolution: raw_resolution = 10, } = config;
176
+ const { plot_bounds, element_size, element, axis_clearance = 12, exclude_rects = [], points, grid_resolution: raw_resolution = 10, } = config;
139
177
  // Ensure grid_resolution >= 2 to avoid division by zero in step calculation
140
178
  const grid_resolution = Math.max(2, raw_resolution);
141
- const { width: elem_width, height: elem_height } = element_size;
179
+ // Measure the element's full footprint (incl. descendants that overflow its box,
180
+ // such as colorbar tick labels) once it's laid out; fall back to element_size before
181
+ // first render. Centralizing this keeps every plot's auto-placement overlap-free.
182
+ const { width: elem_width, height: elem_height } = element?.offsetWidth && element?.offsetHeight
183
+ ? measure_full_footprint(element)
184
+ : element_size;
142
185
  // Calculate valid placement region (plot bounds minus axis clearance)
143
186
  const valid_x_min = plot_bounds.x + axis_clearance;
144
187
  const valid_y_min = plot_bounds.y + axis_clearance;
@@ -50,7 +50,7 @@ export interface Scene3DParams {
50
50
  z_range: [number, number];
51
51
  }
52
52
  /** Apply span constraints or use full range as fallback */
53
- export declare function span_or(span: [number | null, number | null] | undefined, range: [number, number]): [number, number];
53
+ export declare const span_or: (span: [number | null, number | null] | undefined, range: [number, number]) => [number, number];
54
54
  export declare function normalize_to_scene(value: number, [min_val, max_val]: [number, number], scene_size: number): number;
55
55
  export declare function create_to_threejs(params: Scene3DParams): (user_x: number, user_y: number, user_z: number) => {
56
56
  x: number;
@@ -11,6 +11,14 @@ const Z_INDEX_KEY_MAP = {
11
11
  'below-points': `below_points`,
12
12
  'above-all': `above_all`,
13
13
  };
14
+ const apply_span = (start_val, end_val, span) => {
15
+ if (!span)
16
+ return [start_val, end_val];
17
+ return [
18
+ span[0] !== null ? Math.max(start_val, span[0]) : start_val,
19
+ span[1] !== null ? Math.min(end_val, span[1]) : end_val,
20
+ ];
21
+ };
14
22
  // Group indexed ref_lines by z-index for ordered rendering
15
23
  export function group_ref_lines_by_z(lines) {
16
24
  const groups = {
@@ -92,14 +100,6 @@ export function resolve_line_endpoints(ref_line, { x_min, x_max, y_min, y_max, }
92
100
  const is_x_visible = (x_val) => x_val >= x_min && x_val <= x_max;
93
101
  const is_y_visible = (y_val) => y_val >= y_min && y_val <= y_max;
94
102
  // Apply span constraints (works for both x and y)
95
- const apply_span = (v1, v2, span) => {
96
- if (!span)
97
- return [v1, v2];
98
- return [
99
- span[0] !== null ? Math.max(v1, span[0]) : v1,
100
- span[1] !== null ? Math.min(v2, span[1]) : v2,
101
- ];
102
- };
103
103
  const apply_x_span = (x1, x2) => apply_span(x1, x2, ref_line.x_span);
104
104
  const apply_y_span = (y1, y2) => apply_span(y1, y2, ref_line.y_span);
105
105
  // Relative to data coordinate conversion
@@ -290,9 +290,7 @@ export function calculate_annotation_position(x1, y1, x2, y2, annotation) {
290
290
  return { x: final_x, y: final_y, text_anchor, dominant_baseline, rotation };
291
291
  }
292
292
  /** Apply span constraints or use full range as fallback */
293
- export function span_or(span, range) {
294
- return [span?.[0] ?? range[0], span?.[1] ?? range[1]];
295
- }
293
+ export const span_or = (span, range) => [span?.[0] ?? range[0], span?.[1] ?? range[1]];
296
294
  // Normalize a data value to scene coordinates (centered around 0)
297
295
  export function normalize_to_scene(value, [min_val, max_val], scene_size) {
298
296
  const range = max_val - min_val;