matterviz 0.3.2 → 0.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (281) hide show
  1. package/dist/EmptyState.svelte +10 -2
  2. package/dist/FilePicker.svelte +123 -82
  3. package/dist/Icon.svelte +18 -12
  4. package/dist/MillerIndexInput.svelte +27 -21
  5. package/dist/api/optimade.js +6 -6
  6. package/dist/app.css +216 -207
  7. package/dist/brillouin/BrillouinZone.svelte +292 -149
  8. package/dist/brillouin/BrillouinZone.svelte.d.ts +1 -1
  9. package/dist/brillouin/BrillouinZoneControls.svelte +32 -5
  10. package/dist/brillouin/BrillouinZoneExportPane.svelte +69 -42
  11. package/dist/brillouin/BrillouinZoneExportPane.svelte.d.ts +1 -1
  12. package/dist/brillouin/BrillouinZoneInfoPane.svelte +99 -68
  13. package/dist/brillouin/BrillouinZoneScene.svelte +275 -163
  14. package/dist/brillouin/BrillouinZoneScene.svelte.d.ts +1 -1
  15. package/dist/brillouin/BrillouinZoneTooltip.svelte +17 -7
  16. package/dist/brillouin/compute.js +11 -6
  17. package/dist/chempot-diagram/ChemPotDiagram.svelte +162 -27
  18. package/dist/chempot-diagram/ChemPotDiagram2D.svelte +451 -281
  19. package/dist/chempot-diagram/ChemPotDiagram3D.svelte +2148 -1642
  20. package/dist/chempot-diagram/ChemPotScene3D.svelte +8 -5
  21. package/dist/chempot-diagram/async-compute.svelte.d.ts +3 -0
  22. package/dist/chempot-diagram/async-compute.svelte.js +77 -0
  23. package/dist/chempot-diagram/chempot-worker.d.ts +1 -0
  24. package/dist/chempot-diagram/chempot-worker.js +11 -0
  25. package/dist/chempot-diagram/color.js +1 -2
  26. package/dist/chempot-diagram/compute.d.ts +10 -0
  27. package/dist/chempot-diagram/compute.js +250 -88
  28. package/dist/chempot-diagram/index.d.ts +2 -1
  29. package/dist/chempot-diagram/index.js +2 -1
  30. package/dist/chempot-diagram/temperature.js +8 -9
  31. package/dist/chempot-diagram/types.d.ts +3 -0
  32. package/dist/chempot-diagram/types.js +1 -0
  33. package/dist/colors/index.d.ts +1 -1
  34. package/dist/colors/index.js +5 -3
  35. package/dist/composition/BarChart.svelte +128 -55
  36. package/dist/composition/BubbleChart.svelte +102 -49
  37. package/dist/composition/Composition.svelte +100 -79
  38. package/dist/composition/Formula.svelte +108 -62
  39. package/dist/composition/FormulaFilter.svelte +665 -537
  40. package/dist/composition/PieChart.svelte +183 -108
  41. package/dist/composition/format.d.ts +5 -0
  42. package/dist/composition/format.js +20 -3
  43. package/dist/composition/parse.js +14 -9
  44. package/dist/convex-hull/ConvexHull.svelte +93 -40
  45. package/dist/convex-hull/ConvexHull.svelte.d.ts +1 -1
  46. package/dist/convex-hull/ConvexHull2D.svelte +549 -360
  47. package/dist/convex-hull/ConvexHull2D.svelte.d.ts +1 -1
  48. package/dist/convex-hull/ConvexHull3D.svelte +1296 -827
  49. package/dist/convex-hull/ConvexHull3D.svelte.d.ts +1 -1
  50. package/dist/convex-hull/ConvexHull4D.svelte +1004 -688
  51. package/dist/convex-hull/ConvexHull4D.svelte.d.ts +1 -1
  52. package/dist/convex-hull/ConvexHullControls.svelte +115 -28
  53. package/dist/convex-hull/ConvexHullControls.svelte.d.ts +1 -1
  54. package/dist/convex-hull/ConvexHullInfoPane.svelte +29 -3
  55. package/dist/convex-hull/ConvexHullStats.svelte +425 -328
  56. package/dist/convex-hull/ConvexHullTooltip.svelte +40 -16
  57. package/dist/convex-hull/GasPressureControls.svelte +104 -61
  58. package/dist/convex-hull/StructurePopup.svelte +25 -4
  59. package/dist/convex-hull/TemperatureSlider.svelte +45 -25
  60. package/dist/convex-hull/barycentric-coords.js +13 -7
  61. package/dist/convex-hull/demo-temperature.js +8 -4
  62. package/dist/convex-hull/gas-thermodynamics.js +17 -12
  63. package/dist/convex-hull/helpers.d.ts +9 -0
  64. package/dist/convex-hull/helpers.js +77 -34
  65. package/dist/convex-hull/thermodynamics.js +61 -56
  66. package/dist/convex-hull/types.d.ts +9 -14
  67. package/dist/convex-hull/types.js +0 -17
  68. package/dist/coordination/CoordinationBarPlot.svelte +227 -154
  69. package/dist/element/BohrAtom.svelte +55 -12
  70. package/dist/element/ElementHeading.svelte +7 -2
  71. package/dist/element/ElementPhoto.svelte +15 -9
  72. package/dist/element/ElementStats.svelte +10 -4
  73. package/dist/element/ElementTile.svelte +137 -73
  74. package/dist/element/Nucleus.svelte +39 -11
  75. package/dist/element/data.js +1 -1
  76. package/dist/feedback/ClickFeedback.svelte +16 -5
  77. package/dist/feedback/DragOverlay.svelte +10 -2
  78. package/dist/feedback/Spinner.svelte +4 -2
  79. package/dist/feedback/StatusMessage.svelte +8 -2
  80. package/dist/fermi-surface/FermiSlice.svelte +118 -88
  81. package/dist/fermi-surface/FermiSurface.svelte +328 -187
  82. package/dist/fermi-surface/FermiSurface.svelte.d.ts +1 -1
  83. package/dist/fermi-surface/FermiSurfaceControls.svelte +113 -46
  84. package/dist/fermi-surface/FermiSurfaceControls.svelte.d.ts +1 -1
  85. package/dist/fermi-surface/FermiSurfaceScene.svelte +535 -342
  86. package/dist/fermi-surface/FermiSurfaceScene.svelte.d.ts +1 -1
  87. package/dist/fermi-surface/FermiSurfaceTooltip.svelte +14 -5
  88. package/dist/fermi-surface/compute.js +16 -20
  89. package/dist/fermi-surface/parse.js +24 -14
  90. package/dist/fermi-surface/symmetry.js +2 -7
  91. package/dist/fermi-surface/types.d.ts +3 -5
  92. package/dist/heatmap-matrix/HeatmapMatrix.svelte +1019 -765
  93. package/dist/heatmap-matrix/HeatmapMatrix.svelte.d.ts +1 -1
  94. package/dist/heatmap-matrix/HeatmapMatrixControls.svelte +76 -22
  95. package/dist/heatmap-matrix/HeatmapMatrixControls.svelte.d.ts +2 -3
  96. package/dist/icons.js +47 -0
  97. package/dist/index.d.ts +2 -1
  98. package/dist/index.js +2 -1
  99. package/dist/io/decompress.js +1 -1
  100. package/dist/io/export.d.ts +3 -0
  101. package/dist/io/export.js +129 -143
  102. package/dist/io/is-binary.js +2 -3
  103. package/dist/io/url-drop.js +1 -2
  104. package/dist/isosurface/Isosurface.svelte +202 -148
  105. package/dist/isosurface/IsosurfaceControls.svelte +46 -28
  106. package/dist/isosurface/parse.js +34 -29
  107. package/dist/isosurface/slice.js +5 -10
  108. package/dist/isosurface/types.d.ts +2 -1
  109. package/dist/isosurface/types.js +61 -12
  110. package/dist/labels.js +11 -8
  111. package/dist/layout/FullscreenToggle.svelte +11 -2
  112. package/dist/layout/InfoCard.svelte +38 -6
  113. package/dist/layout/InfoTag.svelte +63 -32
  114. package/dist/layout/PropertyFilter.svelte +82 -37
  115. package/dist/layout/SettingsSection.svelte +85 -55
  116. package/dist/layout/SubpageGrid.svelte +10 -2
  117. package/dist/layout/json-tree/JsonNode.svelte +183 -138
  118. package/dist/layout/json-tree/JsonTree.svelte +499 -413
  119. package/dist/layout/json-tree/JsonValue.svelte +127 -99
  120. package/dist/layout/json-tree/utils.js +4 -2
  121. package/dist/marching-cubes.js +25 -2
  122. package/dist/math.d.ts +13 -17
  123. package/dist/math.js +133 -67
  124. package/dist/overlays/ContextMenu.svelte +65 -40
  125. package/dist/overlays/DraggablePane.svelte +211 -139
  126. package/dist/periodic-table/PeriodicTable.svelte +278 -145
  127. package/dist/periodic-table/PeriodicTableControls.svelte +178 -128
  128. package/dist/periodic-table/PropertySelect.svelte +25 -7
  129. package/dist/periodic-table/TableInset.svelte +8 -3
  130. package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte +446 -309
  131. package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte.d.ts +1 -1
  132. package/dist/phase-diagram/PhaseDiagramControls.svelte +102 -43
  133. package/dist/phase-diagram/PhaseDiagramControls.svelte.d.ts +1 -1
  134. package/dist/phase-diagram/PhaseDiagramEditorPane.svelte +63 -40
  135. package/dist/phase-diagram/PhaseDiagramExportPane.svelte +71 -28
  136. package/dist/phase-diagram/PhaseDiagramExportPane.svelte.d.ts +1 -1
  137. package/dist/phase-diagram/PhaseDiagramTooltip.svelte +158 -101
  138. package/dist/phase-diagram/TdbInfoPanel.svelte +28 -4
  139. package/dist/phase-diagram/build-diagram.js +9 -9
  140. package/dist/phase-diagram/colors.js +1 -3
  141. package/dist/phase-diagram/parse.js +10 -9
  142. package/dist/phase-diagram/svg-to-diagram.js +53 -49
  143. package/dist/phase-diagram/utils.d.ts +1 -0
  144. package/dist/phase-diagram/utils.js +80 -25
  145. package/dist/plot/AxisLabel.svelte +28 -3
  146. package/dist/plot/BarPlot.svelte +1182 -734
  147. package/dist/plot/BarPlot.svelte.d.ts +2 -2
  148. package/dist/plot/BarPlotControls.svelte +31 -5
  149. package/dist/plot/BarPlotControls.svelte.d.ts +1 -1
  150. package/dist/plot/ColorBar.svelte +479 -329
  151. package/dist/plot/ColorScaleSelect.svelte +27 -6
  152. package/dist/plot/ElementScatter.svelte +36 -15
  153. package/dist/plot/FillArea.svelte +152 -95
  154. package/dist/plot/Histogram.svelte +934 -571
  155. package/dist/plot/Histogram.svelte.d.ts +1 -1
  156. package/dist/plot/HistogramControls.svelte +53 -9
  157. package/dist/plot/HistogramControls.svelte.d.ts +1 -1
  158. package/dist/plot/InteractiveAxisLabel.svelte +34 -11
  159. package/dist/plot/InteractiveAxisLabel.svelte.d.ts +1 -1
  160. package/dist/plot/Line.svelte +63 -28
  161. package/dist/plot/PlotControls.svelte +157 -114
  162. package/dist/plot/PlotControls.svelte.d.ts +1 -1
  163. package/dist/plot/PlotLegend.svelte +174 -91
  164. package/dist/plot/PlotTooltip.svelte +45 -6
  165. package/dist/plot/PortalSelect.svelte +175 -147
  166. package/dist/plot/ReferenceLine.svelte +76 -22
  167. package/dist/plot/ReferenceLine3D.svelte +132 -107
  168. package/dist/plot/ReferencePlane.svelte +146 -121
  169. package/dist/plot/ScatterPlot.svelte +1681 -1091
  170. package/dist/plot/ScatterPlot.svelte.d.ts +2 -2
  171. package/dist/plot/ScatterPlot3D.svelte +256 -131
  172. package/dist/plot/ScatterPlot3D.svelte.d.ts +2 -2
  173. package/dist/plot/ScatterPlot3DControls.svelte +113 -63
  174. package/dist/plot/ScatterPlot3DControls.svelte.d.ts +2 -1
  175. package/dist/plot/ScatterPlot3DScene.svelte +608 -403
  176. package/dist/plot/ScatterPlot3DScene.svelte.d.ts +2 -2
  177. package/dist/plot/ScatterPlotControls.svelte +65 -25
  178. package/dist/plot/ScatterPlotControls.svelte.d.ts +1 -1
  179. package/dist/plot/ScatterPoint.svelte +98 -26
  180. package/dist/plot/ScatterPoint.svelte.d.ts +1 -0
  181. package/dist/plot/SpacegroupBarPlot.svelte +142 -85
  182. package/dist/plot/Surface3D.svelte +159 -108
  183. package/dist/plot/ZeroLines.svelte +55 -3
  184. package/dist/plot/ZoomRect.svelte +4 -2
  185. package/dist/plot/axis-utils.js +1 -3
  186. package/dist/plot/data-cleaning.js +12 -28
  187. package/dist/plot/data-transform.js +2 -1
  188. package/dist/plot/fill-utils.js +2 -0
  189. package/dist/plot/layout.d.ts +4 -1
  190. package/dist/plot/layout.js +33 -14
  191. package/dist/plot/reference-line.d.ts +2 -2
  192. package/dist/plot/reference-line.js +7 -5
  193. package/dist/plot/scales.js +24 -36
  194. package/dist/plot/types.d.ts +11 -23
  195. package/dist/plot/types.js +6 -11
  196. package/dist/plot/utils/label-placement.d.ts +32 -15
  197. package/dist/plot/utils/label-placement.js +227 -66
  198. package/dist/plot/utils/series-visibility.js +2 -3
  199. package/dist/rdf/RdfPlot.svelte +143 -91
  200. package/dist/rdf/calc-rdf.js +4 -5
  201. package/dist/sanitize.d.ts +4 -0
  202. package/dist/sanitize.js +107 -0
  203. package/dist/settings.d.ts +18 -6
  204. package/dist/settings.js +46 -16
  205. package/dist/spectral/Bands.svelte +632 -453
  206. package/dist/spectral/BandsAndDos.svelte +90 -49
  207. package/dist/spectral/BrillouinBandsDos.svelte +151 -93
  208. package/dist/spectral/Dos.svelte +389 -258
  209. package/dist/spectral/helpers.js +55 -43
  210. package/dist/state.svelte.d.ts +1 -1
  211. package/dist/state.svelte.js +3 -2
  212. package/dist/structure/Arrow.svelte +59 -20
  213. package/dist/structure/AtomLegend.svelte +215 -134
  214. package/dist/structure/Bond.svelte +73 -47
  215. package/dist/structure/CanvasTooltip.svelte +10 -2
  216. package/dist/structure/CellSelect.svelte +72 -45
  217. package/dist/structure/Cylinder.svelte +33 -17
  218. package/dist/structure/Lattice.svelte +88 -33
  219. package/dist/structure/Structure.svelte +1063 -797
  220. package/dist/structure/Structure.svelte.d.ts +1 -1
  221. package/dist/structure/StructureControls.svelte +349 -118
  222. package/dist/structure/StructureExportPane.svelte +124 -89
  223. package/dist/structure/StructureExportPane.svelte.d.ts +1 -1
  224. package/dist/structure/StructureInfoPane.svelte +304 -237
  225. package/dist/structure/StructureScene.svelte +879 -443
  226. package/dist/structure/StructureScene.svelte.d.ts +15 -7
  227. package/dist/structure/atom-properties.js +8 -8
  228. package/dist/structure/bonding.js +6 -7
  229. package/dist/structure/export.js +14 -29
  230. package/dist/structure/ferrox-wasm.js +1 -1
  231. package/dist/structure/index.d.ts +13 -3
  232. package/dist/structure/index.js +83 -23
  233. package/dist/structure/measure.d.ts +2 -2
  234. package/dist/structure/measure.js +4 -44
  235. package/dist/structure/parse.js +113 -141
  236. package/dist/structure/partial-occupancy.js +7 -10
  237. package/dist/structure/pbc.d.ts +1 -0
  238. package/dist/structure/pbc.js +16 -6
  239. package/dist/structure/supercell.d.ts +2 -2
  240. package/dist/structure/supercell.js +12 -22
  241. package/dist/structure/validation.js +1 -2
  242. package/dist/symmetry/SymmetryStats.svelte +84 -41
  243. package/dist/symmetry/WyckoffTable.svelte +26 -6
  244. package/dist/symmetry/cell-transform.js +5 -3
  245. package/dist/symmetry/index.js +8 -7
  246. package/dist/symmetry/spacegroups.js +148 -148
  247. package/dist/table/HeatmapTable.svelte +790 -554
  248. package/dist/table/HeatmapTable.svelte.d.ts +1 -1
  249. package/dist/table/ToggleMenu.svelte +125 -92
  250. package/dist/table/index.js +2 -4
  251. package/dist/theme/ThemeControl.svelte +21 -12
  252. package/dist/time.js +4 -1
  253. package/dist/tooltip/TooltipContent.svelte +33 -8
  254. package/dist/trajectory/Trajectory.svelte +758 -558
  255. package/dist/trajectory/TrajectoryError.svelte +14 -3
  256. package/dist/trajectory/TrajectoryExportPane.svelte +137 -83
  257. package/dist/trajectory/TrajectoryInfoPane.svelte +272 -143
  258. package/dist/trajectory/extract.js +10 -26
  259. package/dist/trajectory/format-detect.js +5 -5
  260. package/dist/trajectory/frame-reader.d.ts +1 -1
  261. package/dist/trajectory/frame-reader.js +5 -12
  262. package/dist/trajectory/helpers.d.ts +0 -1
  263. package/dist/trajectory/helpers.js +2 -17
  264. package/dist/trajectory/index.js +14 -12
  265. package/dist/trajectory/parse/ase.js +5 -4
  266. package/dist/trajectory/parse/hdf5.js +26 -18
  267. package/dist/trajectory/parse/index.js +13 -18
  268. package/dist/trajectory/parse/lammps.js +17 -7
  269. package/dist/trajectory/parse/vasp.js +5 -2
  270. package/dist/trajectory/parse/xyz.js +8 -7
  271. package/dist/trajectory/plotting.js +13 -8
  272. package/dist/utils.d.ts +1 -0
  273. package/dist/utils.js +13 -0
  274. package/dist/xrd/XrdPlot.svelte +337 -247
  275. package/dist/xrd/broadening.js +14 -9
  276. package/dist/xrd/calc-xrd.js +12 -18
  277. package/dist/xrd/parse.d.ts +1 -1
  278. package/dist/xrd/parse.js +17 -17
  279. package/package.json +99 -103
  280. package/readme.md +1 -1
  281. /package/dist/theme/{themes.js → themes.mjs} +0 -0
@@ -48,7 +48,10 @@ export function normalize_value(value) {
48
48
  return 0;
49
49
  }
50
50
  // Normalize a point tuple
51
- export const normalize_point = (point) => [normalize_value(point[0]), normalize_value(point[1])];
51
+ export const normalize_point = (point) => [
52
+ normalize_value(point[0]),
53
+ normalize_value(point[1]),
54
+ ];
52
55
  // Clip a line segment to a rectangle using Liang-Barsky algorithm
53
56
  // Returns clipped [x1, y1, x2, y2] or null if segment is entirely outside
54
57
  function clip_segment_to_rect(p1x, p1y, p2x, p2y, x_min, x_max, y_min, y_max) {
@@ -81,7 +84,7 @@ function clip_segment_to_rect(p1x, p1y, p2x, p2y, x_min, x_max, y_min, y_max) {
81
84
  }
82
85
  // Compute the screen coordinates for a reference line
83
86
  // Returns [x1, y1, x2, y2] in pixel coordinates, or null if line is not visible
84
- export function resolve_line_endpoints(ref_line, { x_min, x_max, y_min, y_max }, { x_scale, x2_scale, y_scale, y2_scale }) {
87
+ export function resolve_line_endpoints(ref_line, { x_min, x_max, y_min, y_max, }, { x_scale, x2_scale, y_scale, y2_scale, }) {
85
88
  // Determine which scales to use based on axis assignment
86
89
  const active_x_scale = ref_line.x_axis === `x2` && x2_scale ? x2_scale : x_scale;
87
90
  const active_y_scale = ref_line.y_axis === `y2` && y2_scale ? y2_scale : y_scale;
@@ -243,7 +246,7 @@ export function calculate_annotation_position(x1, y1, x2, y2, annotation) {
243
246
  if (side === `above` || side === `below`) {
244
247
  // In SVG, y increases downward. Flip sign if 'above' and perpendicular points down (ny > 0),
245
248
  // or if 'below' and perpendicular points up (ny <= 0), to ensure offset is in correct direction
246
- const sign = (side === `above`) === (ny > 0) ? -1 : 1;
249
+ const sign = (side === `above`) === ny > 0 ? -1 : 1;
247
250
  perp_x = sign * nx * gap;
248
251
  perp_y = sign * ny * gap;
249
252
  }
@@ -267,8 +270,7 @@ export function calculate_annotation_position(x1, y1, x2, y2, annotation) {
267
270
  text_anchor = `start`; // text starts at gap point, extends right
268
271
  }
269
272
  else {
270
- text_anchor = ({ start: `start`, end: `end`, center: `middle` }[position] ??
271
- `middle`);
273
+ text_anchor = ({ start: `start`, end: `end`, center: `middle` }[position] ?? `middle`);
272
274
  }
273
275
  const dominant_baseline = ({
274
276
  above: `auto`,
@@ -1,5 +1,5 @@
1
1
  import * as math from '../math';
2
- import { get_arcsinh_threshold, get_scale_type_name, is_time_scale, } from './types';
2
+ import { get_arcsinh_threshold, get_scale_type_name, is_time_scale } from './types';
3
3
  import { extent, range } from 'd3-array';
4
4
  import { scaleLinear, scaleLog, scaleSequential, scaleSequentialLog, scaleTime, } from 'd3-scale';
5
5
  import * as d3_sc from 'd3-scale-chromatic';
@@ -98,8 +98,8 @@ export function generate_arcsinh_ticks(min, max, threshold = 1, count = 10) {
98
98
  if (hi <= 0) {
99
99
  // Negative range: mirror the positive logic
100
100
  return generate_positive_arcsinh_ticks(-hi, -lo, safe_threshold, count)
101
- .map((t) => -t)
102
- .reverse();
101
+ .map((tick) => -tick)
102
+ .toReversed();
103
103
  }
104
104
  // Mixed range: symmetric ticks around zero (includes_zero is always true here)
105
105
  // For very small counts, we prioritize zero as the most meaningful tick
@@ -110,7 +110,7 @@ export function generate_arcsinh_ticks(min, max, threshold = 1, count = 10) {
110
110
  ticks.push(...pos_ticks.filter((t) => t > 0));
111
111
  // Add negative ticks (mirror of positive)
112
112
  const neg_ticks = generate_positive_arcsinh_ticks(0, -lo, safe_threshold, half_count);
113
- ticks.push(...neg_ticks.filter((t) => t > 0).map((t) => -t));
113
+ ticks.push(...neg_ticks.filter((tick) => tick > 0).map((tick) => -tick));
114
114
  // For small counts where half_count is 0 or 1, ensure at least some boundary coverage
115
115
  if (half_count <= 1 && count >= 2) {
116
116
  // Add boundaries if not already present and we have room
@@ -185,7 +185,9 @@ export function create_scale(scale_type, domain, range) {
185
185
  const [min_val, max_val] = domain;
186
186
  const type_name = get_scale_type_name(scale_type);
187
187
  if (type_name === `log`) {
188
- return scaleLog().domain([Math.max(min_val, math.LOG_EPS), max_val]).range(range);
188
+ return scaleLog()
189
+ .domain([Math.max(min_val, math.LOG_EPS), max_val])
190
+ .range(range);
189
191
  }
190
192
  if (type_name === `arcsinh`) {
191
193
  const threshold = get_arcsinh_threshold(scale_type);
@@ -221,9 +223,10 @@ options = {}) {
221
223
  const time_scale = scaleTime().domain([new Date(min_val), new Date(max_val)]);
222
224
  let count = 10; // default
223
225
  if (typeof ticks_option === `number`) {
224
- count = ticks_option < 0
225
- ? Math.ceil((max_val - min_val) / Math.abs(ticks_option) / 86_400_000) // milliseconds per day
226
- : ticks_option;
226
+ count =
227
+ ticks_option < 0
228
+ ? Math.ceil((max_val - min_val) / Math.abs(ticks_option) / 86_400_000) // milliseconds per day
229
+ : ticks_option;
227
230
  }
228
231
  else if (typeof ticks_option === `string`) {
229
232
  count = ticks_option === `day` ? 30 : ticks_option === `month` ? 12 : 10;
@@ -248,9 +251,7 @@ options = {}) {
248
251
  // Arcsinh scale ticks
249
252
  if (type_name === `arcsinh`) {
250
253
  const threshold = get_arcsinh_threshold(scale_type);
251
- const tick_count = typeof ticks_option === `number` && ticks_option > 0
252
- ? ticks_option
253
- : default_count;
254
+ const tick_count = typeof ticks_option === `number` && ticks_option > 0 ? ticks_option : default_count;
254
255
  return generate_arcsinh_ticks(min_val, max_val, threshold, tick_count);
255
256
  }
256
257
  // Linear scale with interval (negative number indicates interval)
@@ -260,9 +261,7 @@ options = {}) {
260
261
  return range(start, max_val + interval * interval_padding, interval);
261
262
  }
262
263
  // Default ticks using scale function
263
- const tick_count = typeof ticks_option === `number` && ticks_option > 0
264
- ? ticks_option
265
- : default_count;
264
+ const tick_count = typeof ticks_option === `number` && ticks_option > 0 ? ticks_option : default_count;
266
265
  const ticks = scale_fn.ticks(tick_count);
267
266
  return ticks.map(Number);
268
267
  }
@@ -274,9 +273,7 @@ export function calculate_domain(values, scale_type = `linear`) {
274
273
  const type_name = get_scale_type_name(scale_type);
275
274
  // Only log scale needs domain clamping to positive values
276
275
  // Arcsinh and linear can handle any values
277
- return type_name === `log`
278
- ? [Math.max(min_val, math.LOG_EPS), max_val]
279
- : [min_val, max_val];
276
+ return type_name === `log` ? [Math.max(min_val, math.LOG_EPS), max_val] : [min_val, max_val];
280
277
  }
281
278
  // Advanced domain calculation with padding and nice boundaries (from ScatterPlot)
282
279
  export function get_nice_data_range(points, get_value, range, scale_type, padding_factor, is_time = false) {
@@ -376,15 +373,11 @@ export function generate_log_ticks(min, max, ticks_option) {
376
373
  const max_power = Math.ceil(Math.log10(max));
377
374
  // For very wide ranges, extend the range to include more ticks
378
375
  const range_size = max_power - min_power;
379
- const extended_min_power = range_size <= 2
380
- ? min_power - 1
381
- : min_power - Math.max(1, Math.floor(range_size / 4));
376
+ const extended_min_power = range_size <= 2 ? min_power - 1 : min_power - Math.max(1, Math.floor(range_size / 4));
382
377
  const extended_max_power = range_size <= 2 ? max_power + 1 : max_power;
383
- const powers = range(extended_min_power, extended_max_power + 1).map((p) => Math.pow(10, p));
378
+ const powers = range(extended_min_power, extended_max_power + 1).map((power) => Math.pow(10, power));
384
379
  // For narrow ranges, include intermediate values
385
- if (max_power - min_power < 3 &&
386
- typeof ticks_option === `number` &&
387
- ticks_option > 5) {
380
+ if (max_power - min_power < 3 && typeof ticks_option === `number` && ticks_option > 5) {
388
381
  const detailed_ticks = [];
389
382
  powers.forEach((power) => {
390
383
  detailed_ticks.push(power);
@@ -393,9 +386,9 @@ export function generate_log_ticks(min, max, ticks_option) {
393
386
  if (power * 5 <= Math.pow(10, extended_max_power))
394
387
  detailed_ticks.push(power * 5);
395
388
  });
396
- return detailed_ticks.filter((t) => t >= min && t <= max);
389
+ return detailed_ticks.filter((tick) => tick >= min && tick <= max);
397
390
  }
398
- return powers.filter((p) => p >= min && p <= max);
391
+ return powers.filter((power) => power >= min && power <= max);
399
392
  }
400
393
  // Get custom label for a tick value if provided, otherwise return null
401
394
  export function get_tick_label(tick_value, ticks_option) {
@@ -406,18 +399,13 @@ export function get_tick_label(tick_value, ticks_option) {
406
399
  }
407
400
  // Create a color scale function from configuration
408
401
  export function create_color_scale(color_scale_config, auto_color_range) {
409
- const scheme = typeof color_scale_config === `string`
410
- ? color_scale_config
411
- : color_scale_config.scheme;
402
+ const scheme = typeof color_scale_config === `string` ? color_scale_config : color_scale_config.scheme;
412
403
  const interpolator = (typeof d3_sc[scheme] === `function`
413
404
  ? d3_sc[scheme]
414
405
  : d3_sc.interpolateViridis);
415
- const [min_val, max_val] = (typeof color_scale_config === `string`
416
- ? undefined
417
- : color_scale_config.value_range) ?? auto_color_range;
418
- const scale_type = typeof color_scale_config === `string`
419
- ? undefined
420
- : color_scale_config.type;
406
+ const [min_val, max_val] = (typeof color_scale_config === `string` ? undefined : color_scale_config.value_range) ??
407
+ auto_color_range;
408
+ const scale_type = typeof color_scale_config === `string` ? undefined : color_scale_config.type;
421
409
  const type_name = get_scale_type_name(scale_type);
422
410
  if (type_name === `log`) {
423
411
  return scaleSequentialLog(interpolator).domain([
@@ -465,7 +453,7 @@ function create_arcsinh_color_scale(interpolator, initial_domain, threshold) {
465
453
  export function create_size_scale(config, all_size_values) {
466
454
  const [min_radius, max_radius] = config.radius_range ?? [2, 10];
467
455
  const auto_range = all_size_values.length > 0
468
- ? extent(all_size_values.filter((v) => v !== null))
456
+ ? extent(all_size_values.filter((val) => val !== null))
469
457
  : [0, 1];
470
458
  const [min_val, max_val] = config.value_range ?? auto_range;
471
459
  const safe_min = min_val ?? 0;
@@ -1,10 +1,8 @@
1
1
  import type { D3SymbolName } from '../labels';
2
2
  import type { Vec2, Vec3 } from '../math';
3
3
  import type DraggablePane from '../overlays/DraggablePane.svelte';
4
- import type { SimulationNodeDatum } from 'd3-force';
5
4
  import type { ComponentProps, Snippet } from 'svelte';
6
5
  import type { HTMLAttributes } from 'svelte/elements';
7
- import type ColorBar from './ColorBar.svelte';
8
6
  import type PlotLegend from './PlotLegend.svelte';
9
7
  import type { TicksOption } from './scales';
10
8
  export interface TweenedOptions<T> {
@@ -200,33 +198,23 @@ export type QuadrantCounts = {
200
198
  bottom_left: number;
201
199
  bottom_right: number;
202
200
  };
203
- export interface LabelNode<Metadata = Record<string, unknown>> extends SimulationNodeDatum {
204
- id: string;
205
- anchor_x: number;
206
- anchor_y: number;
207
- point_node: InternalPoint<Metadata>;
208
- label_width: number;
209
- label_height: number;
201
+ export interface LabelPlacementWeights {
202
+ overlap?: number;
203
+ marker?: number;
204
+ leader_cross?: number;
205
+ leader_text?: number;
206
+ distance?: number;
207
+ bounds?: number;
210
208
  }
211
209
  export interface LabelPlacementConfig {
212
- collision_strength: number;
213
- link_strength: number;
214
- link_distance: number;
215
- placement_ticks: number;
216
- link_distance_range?: [number | null, number | null];
210
+ sa_iterations?: number;
211
+ weights?: LabelPlacementWeights;
212
+ leader_line_threshold?: number;
213
+ max_labels?: number;
217
214
  }
218
215
  export type HoverConfig = {
219
216
  threshold_px: number;
220
217
  };
221
- export interface AnchorNode extends SimulationNodeDatum {
222
- id: string;
223
- fx: number;
224
- fy: number;
225
- point_radius: number;
226
- show_color_bar?: boolean;
227
- color_bar?: ComponentProps<typeof ColorBar> | null;
228
- label_placement_config?: Partial<LabelPlacementConfig>;
229
- }
230
218
  export type LegendConfig = Omit<ComponentProps<typeof PlotLegend>, `series_data` | `on_drag_start` | `on_drag` | `on_drag_end`> & {
231
219
  margin?: number | Sides;
232
220
  tween?: TweenedOptions<XyObj>;
@@ -1,7 +1,7 @@
1
1
  // Type guard for select value narrowing (avoids unsafe casts)
2
- const SCALE_TYPE_NAMES = [`linear`, `log`, `arcsinh`, `time`];
2
+ const SCALE_TYPE_NAMES = new Set([`linear`, `log`, `arcsinh`, `time`]);
3
3
  export function is_scale_type_name(val) {
4
- return SCALE_TYPE_NAMES.includes(val);
4
+ return SCALE_TYPE_NAMES.has(val);
5
5
  }
6
6
  // Helper to normalize ScaleType to base type name
7
7
  export function get_scale_type_name(scale_type) {
@@ -33,9 +33,9 @@ export function is_time_scale(scale_type, format) {
33
33
  return format?.startsWith(`%`) ?? false;
34
34
  }
35
35
  // Type guard for select value narrowing (avoids unsafe casts)
36
- const Y2_SYNC_MODES = [`none`, `synced`, `align`];
36
+ const Y2_SYNC_MODES = new Set([`none`, `synced`, `align`]);
37
37
  export function is_y2_sync_mode(val) {
38
- return Y2_SYNC_MODES.includes(val);
38
+ return Y2_SYNC_MODES.has(val);
39
39
  }
40
40
  export const LINE_TYPES = [`solid`, `dashed`, `dotted`];
41
41
  // Define grid cell identifiers
@@ -50,15 +50,10 @@ export const CELLS_3X3 = [
50
50
  `bottom-center`,
51
51
  `bottom-right`,
52
52
  ];
53
- export const CORNER_CELLS = [
54
- `top-left`,
55
- `top-right`,
56
- `bottom-left`,
57
- `bottom-right`,
58
- ];
53
+ export const CORNER_CELLS = [`top-left`, `top-right`, `bottom-left`, `bottom-right`];
59
54
  // Default grid line style (SSOT for all plot components)
60
55
  export const DEFAULT_GRID_STYLE = {
61
- 'stroke': `var(--border-color, gray)`,
56
+ stroke: `var(--border-color, gray)`,
62
57
  'stroke-dasharray': `4`,
63
58
  'stroke-width': `1`,
64
59
  };
@@ -1,21 +1,38 @@
1
1
  import type { AxisConfig, DataSeries, XyObj } from '..';
2
2
  import type { PlotScaleFn } from '../scales';
3
- import type { LabelPlacementConfig } from '../types';
4
- type ScaleFn = PlotScaleFn;
5
- export interface AnchorNode {
6
- id: string;
7
- fx: number;
8
- fy: number;
9
- point_radius: number;
3
+ import type { LabelPlacementConfig, LabelPlacementWeights } from '../types';
4
+ export interface Rect {
5
+ x: number;
6
+ y: number;
7
+ w: number;
8
+ h: number;
10
9
  }
11
- export declare function compute_label_positions(filtered_series: DataSeries[], config: LabelPlacementConfig & {
12
- max_labels?: number;
13
- charge_strength?: number;
14
- charge_distance_max?: number;
15
- }, scales: {
16
- x_scale_fn: ScaleFn;
17
- y_scale_fn: ScaleFn;
18
- y2_scale_fn: ScaleFn;
10
+ export interface PlotBounds {
11
+ min_x: number;
12
+ min_y: number;
13
+ max_x: number;
14
+ max_y: number;
15
+ }
16
+ interface AnchorInfo {
17
+ x: number;
18
+ y: number;
19
+ radius: number;
20
+ }
21
+ interface LabelState extends Rect {
22
+ anchor_idx: number;
23
+ }
24
+ export declare function parse_font_size(size_str?: string): number;
25
+ export declare function rect_overlap_area(a: Rect, b: Rect): number;
26
+ export declare function rect_circle_overlap(rect: Rect, cx: number, cy: number, radius: number): number;
27
+ export declare function segments_intersect(ax1: number, ay1: number, ax2: number, ay2: number, bx1: number, by1: number, bx2: number, by2: number): boolean;
28
+ export declare function segment_rect_intersects(sx1: number, sy1: number, sx2: number, sy2: number, rect: Rect): boolean;
29
+ export declare function rect_out_of_bounds_area(rect: Rect, bounds: PlotBounds): number;
30
+ export declare function generate_candidates(ax: number, ay: number, point_radius: number, label_w: number, label_h: number, gap: number): XyObj[];
31
+ export declare function compute_delta_energy(labels: LabelState[], anchors: AnchorInfo[], changed_idx: number, old_state: LabelState, new_state: LabelState, weights: Required<LabelPlacementWeights>, bounds: PlotBounds): number;
32
+ export declare function compute_label_positions(filtered_series: DataSeries[], config: LabelPlacementConfig, scales: {
33
+ x_scale_fn: PlotScaleFn;
34
+ y_scale_fn: PlotScaleFn;
35
+ y2_scale_fn: PlotScaleFn;
19
36
  x_axis: AxisConfig;
20
37
  }, bounds: {
21
38
  width: number;
@@ -1,21 +1,148 @@
1
1
  import { is_time_scale } from '../types';
2
- import { forceCollide, forceLink, forceManyBody, forceSimulation } from 'd3-force';
3
- const is_label_node = (node) => `label_width` in node;
4
- function parse_font_size(size_str) {
2
+ const DEFAULT_WEIGHTS = {
3
+ overlap: 30,
4
+ marker: 100,
5
+ leader_cross: 10,
6
+ leader_text: 8,
7
+ distance: 0.5,
8
+ bounds: 100,
9
+ };
10
+ export function parse_font_size(size_str) {
5
11
  if (!size_str)
6
12
  return 12;
7
- const match = size_str.match(/^(\d+(?:\.\d+)?)(px|em|rem)?$/);
13
+ const match = /^(\d+(?:\.\d+)?)(px|em|rem)?$/.exec(size_str);
8
14
  if (!match)
9
15
  return 12;
10
16
  const value = parseFloat(match[1]);
11
17
  return match[2] === `em` || match[2] === `rem` ? value * 16 : value;
12
18
  }
19
+ // === Geometry helpers ===
20
+ export function rect_overlap_area(a, b) {
21
+ const ox = Math.max(0, Math.min(a.x + a.w, b.x + b.w) - Math.max(a.x, b.x));
22
+ const oy = Math.max(0, Math.min(a.y + a.h, b.y + b.h) - Math.max(a.y, b.y));
23
+ return ox * oy;
24
+ }
25
+ export function rect_circle_overlap(rect, cx, cy, radius) {
26
+ // Inflate rect by radius to create an exclusion zone around the marker
27
+ const left = rect.x - radius;
28
+ const top = rect.y - radius;
29
+ const right = rect.x + rect.w + radius;
30
+ const bottom = rect.y + rect.h + radius;
31
+ if (cx < left || cx > right || cy < top || cy > bottom)
32
+ return 0;
33
+ // Penalty proportional to how deep the marker center is inside the exclusion zone
34
+ const dx = Math.min(cx - left, right - cx);
35
+ const dy = Math.min(cy - top, bottom - cy);
36
+ return Math.min(dx, dy) + radius;
37
+ }
38
+ export function segments_intersect(ax1, ay1, ax2, ay2, bx1, by1, bx2, by2) {
39
+ const d1x = ax2 - ax1, d1y = ay2 - ay1;
40
+ const d2x = bx2 - bx1, d2y = by2 - by1;
41
+ const cross = d1x * d2y - d1y * d2x;
42
+ if (Math.abs(cross) < 1e-10)
43
+ return false;
44
+ const t_val = ((bx1 - ax1) * d2y - (by1 - ay1) * d2x) / cross;
45
+ const u_val = ((bx1 - ax1) * d1y - (by1 - ay1) * d1x) / cross;
46
+ return t_val > 0 && t_val < 1 && u_val > 0 && u_val < 1;
47
+ }
48
+ export function segment_rect_intersects(sx1, sy1, sx2, sy2, rect) {
49
+ const rx = rect.x, ry = rect.y, rx2 = rx + rect.w, ry2 = ry + rect.h;
50
+ return (segments_intersect(sx1, sy1, sx2, sy2, rx, ry, rx2, ry) ||
51
+ segments_intersect(sx1, sy1, sx2, sy2, rx2, ry, rx2, ry2) ||
52
+ segments_intersect(sx1, sy1, sx2, sy2, rx, ry2, rx2, ry2) ||
53
+ segments_intersect(sx1, sy1, sx2, sy2, rx, ry, rx, ry2));
54
+ }
55
+ export function rect_out_of_bounds_area(rect, bounds) {
56
+ let penalty = 0;
57
+ if (rect.x < bounds.min_x)
58
+ penalty += (bounds.min_x - rect.x) * rect.h;
59
+ if (rect.y < bounds.min_y)
60
+ penalty += (bounds.min_y - rect.y) * rect.w;
61
+ if (rect.x + rect.w > bounds.max_x)
62
+ penalty += (rect.x + rect.w - bounds.max_x) * rect.h;
63
+ if (rect.y + rect.h > bounds.max_y)
64
+ penalty += (rect.y + rect.h - bounds.max_y) * rect.w;
65
+ return penalty;
66
+ }
67
+ // 8 candidate positions around anchor: R, TR, T, TL, L, BL, B, BR
68
+ // Positions are top-left corner of the label bounding box.
69
+ // All positions keep a full `offset` gap from the marker edge.
70
+ export function generate_candidates(ax, ay, point_radius, label_w, label_h, gap) {
71
+ const offset = point_radius + gap;
72
+ return [
73
+ { x: ax + offset, y: ay - label_h + offset / 2 }, // R (baseline just below center)
74
+ { x: ax + offset, y: ay - label_h - offset / 2 }, // TR
75
+ { x: ax - label_w / 2, y: ay - label_h - offset }, // T
76
+ { x: ax - label_w - offset, y: ay - label_h - offset / 2 }, // TL
77
+ { x: ax - label_w - offset, y: ay - label_h + offset / 2 }, // L (baseline just below center)
78
+ { x: ax - label_w - offset, y: ay + offset / 2 }, // BL
79
+ { x: ax - label_w / 2, y: ay + offset }, // B
80
+ { x: ax + offset, y: ay + offset / 2 }, // BR
81
+ ];
82
+ }
83
+ // Compute energy delta when only label at `changed_idx` moves
84
+ export function compute_delta_energy(labels, anchors, changed_idx, old_state, new_state, weights, bounds) {
85
+ let delta = 0;
86
+ const anchor = anchors[new_state.anchor_idx];
87
+ const old_cx = old_state.x + old_state.w / 2, old_cy = old_state.y + old_state.h / 2;
88
+ const new_cx = new_state.x + new_state.w / 2, new_cy = new_state.y + new_state.h / 2;
89
+ // Distance penalty change
90
+ delta +=
91
+ weights.distance *
92
+ (Math.hypot(new_cx - anchor.x, new_cy - anchor.y) -
93
+ Math.hypot(old_cx - anchor.x, old_cy - anchor.y));
94
+ // Bounds penalty change
95
+ delta +=
96
+ weights.bounds *
97
+ (rect_out_of_bounds_area(new_state, bounds) - rect_out_of_bounds_area(old_state, bounds));
98
+ // Marker overlap change (all markers)
99
+ for (const marker of anchors) {
100
+ delta +=
101
+ weights.marker *
102
+ (rect_circle_overlap(new_state, marker.x, marker.y, marker.radius) -
103
+ rect_circle_overlap(old_state, marker.x, marker.y, marker.radius));
104
+ }
105
+ // Pairwise interactions with all other labels
106
+ for (let jdx = 0; jdx < labels.length; jdx++) {
107
+ if (jdx === changed_idx)
108
+ continue;
109
+ const other = labels[jdx];
110
+ const anchor_j = anchors[other.anchor_idx];
111
+ const other_cx = other.x + other.w / 2, other_cy = other.y + other.h / 2;
112
+ // Label-label overlap delta
113
+ delta +=
114
+ weights.overlap *
115
+ (rect_overlap_area(new_state, other) - rect_overlap_area(old_state, other));
116
+ // Leader line crossing delta (changed label's leader vs other's leader)
117
+ const old_cross = segments_intersect(anchor.x, anchor.y, old_cx, old_cy, anchor_j.x, anchor_j.y, other_cx, other_cy);
118
+ const new_cross = segments_intersect(anchor.x, anchor.y, new_cx, new_cy, anchor_j.x, anchor_j.y, other_cx, other_cy);
119
+ if (new_cross !== old_cross)
120
+ delta += (new_cross ? 1 : -1) * weights.leader_cross;
121
+ // Changed label's leader crossing other label's rect
122
+ const old_text = segment_rect_intersects(anchor.x, anchor.y, old_cx, old_cy, other);
123
+ const new_text = segment_rect_intersects(anchor.x, anchor.y, new_cx, new_cy, other);
124
+ if (new_text !== old_text)
125
+ delta += (new_text ? 1 : -1) * weights.leader_text;
126
+ // Other label's leader crossing changed label's rect
127
+ const old_other = segment_rect_intersects(anchor_j.x, anchor_j.y, other_cx, other_cy, old_state);
128
+ const new_other = segment_rect_intersects(anchor_j.x, anchor_j.y, other_cx, other_cy, new_state);
129
+ if (new_other !== old_other)
130
+ delta += (new_other ? 1 : -1) * weights.leader_text;
131
+ }
132
+ return delta;
133
+ }
134
+ // === Main export ===
13
135
  export function compute_label_positions(filtered_series, config, scales, bounds) {
14
- const { width, height, pad } = bounds;
15
136
  const { x_scale_fn, y_scale_fn, y2_scale_fn, x_axis } = scales;
16
- const label_nodes = [];
17
- const anchor_nodes = [];
18
- const links = [];
137
+ const { width, height, pad } = bounds;
138
+ const plot_bounds = {
139
+ min_x: pad.l,
140
+ min_y: pad.t,
141
+ max_x: width - pad.r,
142
+ max_y: height - pad.b,
143
+ };
144
+ // Collect all label data in a single pass
145
+ const label_infos = [];
19
146
  for (const series of filtered_series) {
20
147
  for (const pt of series.filtered_data ?? []) {
21
148
  if (!pt.point_label?.auto_placement || !pt.point_label.text)
@@ -24,72 +151,106 @@ export function compute_label_positions(filtered_series, config, scales, bounds)
24
151
  ? x_scale_fn(new Date(pt.x))
25
152
  : x_scale_fn(pt.x);
26
153
  const ay = (series.y_axis === `y2` ? y2_scale_fn : y_scale_fn)(pt.y);
27
- const id = `${pt.series_idx}-${pt.point_idx}`;
28
154
  const font_size = parse_font_size(pt.point_label.font_size);
29
- const w = pt.point_label.text.length * font_size * 0.6 + 10;
30
- const h = font_size * 1.2;
31
- const r = pt.point_style?.radius ?? 3;
32
- label_nodes.push({
33
- id,
34
- anchor_x: ax,
35
- anchor_y: ay,
36
- point_node: pt,
37
- label_width: w,
38
- label_height: h,
39
- x: ax + (pt.point_label.offset?.x ?? 5),
40
- y: ay + (pt.point_label.offset?.y ?? r + h / 2 + 3),
155
+ const label_w = pt.point_label.text.length * font_size * 0.6 + 10;
156
+ const label_h = font_size * 1.2;
157
+ const radius = pt.point_style?.radius ?? 3;
158
+ label_infos.push({
159
+ id: `${pt.series_idx}-${pt.point_idx}`,
160
+ anchor: { x: ax, y: ay, radius },
161
+ width: label_w,
162
+ height: label_h,
163
+ candidates: generate_candidates(ax, ay, radius, label_w, label_h, 4),
41
164
  });
42
- anchor_nodes.push({ id: `anchor-${id}`, fx: ax, fy: ay, point_radius: r });
43
- links.push({ source: id, target: `anchor-${id}` });
44
165
  }
45
166
  }
46
- if (label_nodes.length === 0)
167
+ const num_labels = label_infos.length;
168
+ if (num_labels === 0)
47
169
  return {};
48
- if (config.max_labels && label_nodes.length > config.max_labels) {
49
- return Object.fromEntries(label_nodes.map((n) => [n.id, { x: n.x ?? 0, y: n.y ?? 0 }]));
170
+ // Fallback: too many labels, just offset to the right with bounds clamping
171
+ if (config.max_labels && num_labels > config.max_labels) {
172
+ return Object.fromEntries(label_infos.map((info) => [
173
+ info.id,
174
+ {
175
+ x: Math.min(Math.max(info.anchor.x + 5, plot_bounds.min_x), plot_bounds.max_x - info.width),
176
+ y: Math.min(Math.max(info.anchor.y, plot_bounds.min_y), plot_bounds.max_y - info.height),
177
+ },
178
+ ]));
50
179
  }
51
- const sim = forceSimulation([...label_nodes, ...anchor_nodes])
52
- .force(`link`, forceLink(links)
53
- .id((node) => node.id)
54
- .distance(config.link_distance)
55
- .strength(config.link_strength))
56
- .force(`collide`, forceCollide().radius((node) => {
57
- if (is_label_node(node)) {
58
- return Math.sqrt(node.label_width ** 2 + node.label_height ** 2) / 2 + 2;
59
- }
60
- return node.point_radius + 2;
61
- }).strength(config.collision_strength))
62
- .force(`charge`, forceManyBody().strength((node) => {
63
- if (is_label_node(node))
64
- return 0;
65
- return node.point_radius !== undefined && node.fx !== undefined
66
- ? -(config.charge_strength ?? 50)
67
- : 0;
68
- }).distanceMax(config.charge_distance_max ?? 30));
69
- sim.stop().tick(config.placement_ticks);
70
- const [min_dist, max_dist] = config.link_distance_range ?? [null, null];
71
- const result = {};
72
- for (const node of label_nodes) {
73
- const node_x = node.x ?? 0;
74
- const node_y = node.y ?? 0;
75
- let x = Math.max(pad.l + node.label_width / 2, Math.min(width - pad.r - node.label_width / 2, node_x));
76
- let y = Math.max(pad.t + node.label_height / 2, Math.min(height - pad.b - node.label_height / 2, node_y));
77
- if (min_dist || max_dist) {
78
- const dx = x - node.anchor_x;
79
- const dy = y - node.anchor_y;
80
- const dist = Math.hypot(dx, dy);
81
- if (max_dist && dist > max_dist) {
82
- const s = max_dist / dist;
83
- x = node.anchor_x + dx * s;
84
- y = node.anchor_y + dy * s;
180
+ const weights = { ...DEFAULT_WEIGHTS, ...config.weights };
181
+ const anchors = label_infos.map((info) => info.anchor);
182
+ // Greedy initialization: pick best candidate per label
183
+ const labels = [];
184
+ for (let idx = 0; idx < num_labels; idx++) {
185
+ const { candidates, width: lw, height: lh, anchor } = label_infos[idx];
186
+ let best_candidate = candidates[0];
187
+ let best_score = Infinity;
188
+ for (const candidate of candidates) {
189
+ const test_rect = { x: candidate.x, y: candidate.y, w: lw, h: lh };
190
+ let score = weights.bounds * rect_out_of_bounds_area(test_rect, plot_bounds);
191
+ for (const placed of labels) {
192
+ score += weights.overlap * rect_overlap_area(test_rect, placed);
193
+ }
194
+ for (const marker of anchors) {
195
+ score +=
196
+ weights.marker * rect_circle_overlap(test_rect, marker.x, marker.y, marker.radius);
85
197
  }
86
- else if (min_dist && dist > 0 && dist < min_dist) {
87
- const s = min_dist / dist;
88
- x = node.anchor_x + dx * s;
89
- y = node.anchor_y + dy * s;
198
+ score +=
199
+ weights.distance *
200
+ Math.hypot(candidate.x + lw / 2 - anchor.x, candidate.y + lh / 2 - anchor.y);
201
+ if (score < best_score) {
202
+ best_score = score;
203
+ best_candidate = candidate;
90
204
  }
91
205
  }
92
- result[node.id] = { x, y };
206
+ labels.push({ x: best_candidate.x, y: best_candidate.y, w: lw, h: lh, anchor_idx: idx });
207
+ }
208
+ // Simulated annealing
209
+ const sa_iterations = config.sa_iterations ?? 2000;
210
+ const total_steps = sa_iterations * num_labels;
211
+ const cooling_rate = 1 / total_steps;
212
+ // Seeded pseudo-random for deterministic results
213
+ let rng_state = 42;
214
+ const next_random = () => {
215
+ rng_state = (rng_state * 1664525 + 1013904223) & 0x7fffffff;
216
+ return rng_state / 0x7fffffff;
217
+ };
218
+ // Reusable scratch objects to avoid allocations in the hot loop
219
+ const old_scratch = { x: 0, y: 0, w: 0, h: 0, anchor_idx: 0 };
220
+ const new_scratch = { x: 0, y: 0, w: 0, h: 0, anchor_idx: 0 };
221
+ const copy_state = (dst, src) => {
222
+ dst.x = src.x;
223
+ dst.y = src.y;
224
+ dst.w = src.w;
225
+ dst.h = src.h;
226
+ dst.anchor_idx = src.anchor_idx;
227
+ };
228
+ for (let step = 0; step < total_steps; step++) {
229
+ const temperature = Math.max(0.001, 1.0 - step * cooling_rate);
230
+ const label_idx = Math.floor(next_random() * num_labels);
231
+ const current = labels[label_idx];
232
+ copy_state(old_scratch, current);
233
+ copy_state(new_scratch, current);
234
+ // 70% try a candidate position, 30% small perturbation
235
+ if (next_random() < 0.7) {
236
+ const candidate = label_infos[label_idx].candidates[Math.floor(next_random() * label_infos[label_idx].candidates.length)];
237
+ new_scratch.x = candidate.x;
238
+ new_scratch.y = candidate.y;
239
+ }
240
+ else {
241
+ const max_shift = 30 * temperature + 5;
242
+ new_scratch.x += (next_random() - 0.5) * 2 * max_shift;
243
+ new_scratch.y += (next_random() - 0.5) * 2 * max_shift;
244
+ }
245
+ const delta = compute_delta_energy(labels, anchors, label_idx, old_scratch, new_scratch, weights, plot_bounds);
246
+ if (delta < 0 || next_random() < Math.exp(-delta / (temperature * 10 + 0.1))) {
247
+ current.x = new_scratch.x;
248
+ current.y = new_scratch.y;
249
+ }
93
250
  }
94
- return result;
251
+ // Return label center positions (matching existing API)
252
+ return Object.fromEntries(labels.map((label, idx) => [
253
+ label_infos[idx].id,
254
+ { x: label.x + label.w / 2, y: label.y + label.h / 2 },
255
+ ]));
95
256
  }