matterviz 0.3.2 → 0.3.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.
- package/dist/EmptyState.svelte +10 -2
- package/dist/FilePicker.svelte +123 -82
- package/dist/Icon.svelte +18 -12
- package/dist/MillerIndexInput.svelte +27 -21
- package/dist/api/optimade.js +6 -6
- package/dist/app.css +216 -207
- package/dist/brillouin/BrillouinZone.svelte +292 -149
- package/dist/brillouin/BrillouinZone.svelte.d.ts +1 -1
- package/dist/brillouin/BrillouinZoneControls.svelte +32 -5
- package/dist/brillouin/BrillouinZoneExportPane.svelte +69 -42
- package/dist/brillouin/BrillouinZoneExportPane.svelte.d.ts +1 -1
- package/dist/brillouin/BrillouinZoneInfoPane.svelte +99 -68
- package/dist/brillouin/BrillouinZoneScene.svelte +275 -163
- package/dist/brillouin/BrillouinZoneScene.svelte.d.ts +1 -1
- package/dist/brillouin/BrillouinZoneTooltip.svelte +17 -7
- package/dist/brillouin/compute.js +11 -6
- package/dist/chempot-diagram/ChemPotDiagram.svelte +162 -27
- package/dist/chempot-diagram/ChemPotDiagram2D.svelte +451 -281
- package/dist/chempot-diagram/ChemPotDiagram3D.svelte +2148 -1642
- package/dist/chempot-diagram/ChemPotScene3D.svelte +8 -5
- package/dist/chempot-diagram/async-compute.svelte.d.ts +3 -0
- package/dist/chempot-diagram/async-compute.svelte.js +77 -0
- package/dist/chempot-diagram/chempot-worker.d.ts +1 -0
- package/dist/chempot-diagram/chempot-worker.js +11 -0
- package/dist/chempot-diagram/color.js +1 -2
- package/dist/chempot-diagram/compute.d.ts +10 -0
- package/dist/chempot-diagram/compute.js +250 -88
- package/dist/chempot-diagram/index.d.ts +2 -1
- package/dist/chempot-diagram/index.js +2 -1
- package/dist/chempot-diagram/temperature.js +8 -9
- package/dist/chempot-diagram/types.d.ts +3 -0
- package/dist/chempot-diagram/types.js +1 -0
- package/dist/colors/index.d.ts +1 -1
- package/dist/colors/index.js +5 -3
- package/dist/composition/BarChart.svelte +128 -55
- package/dist/composition/BubbleChart.svelte +102 -49
- package/dist/composition/Composition.svelte +100 -79
- package/dist/composition/Formula.svelte +108 -62
- package/dist/composition/FormulaFilter.svelte +665 -537
- package/dist/composition/PieChart.svelte +183 -108
- package/dist/composition/format.d.ts +5 -0
- package/dist/composition/format.js +20 -3
- package/dist/composition/parse.js +14 -9
- package/dist/convex-hull/ConvexHull.svelte +93 -40
- package/dist/convex-hull/ConvexHull.svelte.d.ts +1 -1
- package/dist/convex-hull/ConvexHull2D.svelte +549 -360
- package/dist/convex-hull/ConvexHull2D.svelte.d.ts +1 -1
- package/dist/convex-hull/ConvexHull3D.svelte +1296 -827
- package/dist/convex-hull/ConvexHull3D.svelte.d.ts +1 -1
- package/dist/convex-hull/ConvexHull4D.svelte +1004 -688
- package/dist/convex-hull/ConvexHull4D.svelte.d.ts +1 -1
- package/dist/convex-hull/ConvexHullControls.svelte +115 -28
- package/dist/convex-hull/ConvexHullControls.svelte.d.ts +1 -1
- package/dist/convex-hull/ConvexHullInfoPane.svelte +29 -3
- package/dist/convex-hull/ConvexHullStats.svelte +425 -328
- package/dist/convex-hull/ConvexHullTooltip.svelte +40 -16
- package/dist/convex-hull/GasPressureControls.svelte +104 -61
- package/dist/convex-hull/StructurePopup.svelte +25 -4
- package/dist/convex-hull/TemperatureSlider.svelte +45 -25
- package/dist/convex-hull/barycentric-coords.js +13 -7
- package/dist/convex-hull/demo-temperature.js +8 -4
- package/dist/convex-hull/gas-thermodynamics.js +17 -12
- package/dist/convex-hull/helpers.d.ts +9 -0
- package/dist/convex-hull/helpers.js +77 -34
- package/dist/convex-hull/thermodynamics.js +61 -56
- package/dist/convex-hull/types.d.ts +9 -14
- package/dist/convex-hull/types.js +0 -17
- package/dist/coordination/CoordinationBarPlot.svelte +227 -154
- package/dist/element/BohrAtom.svelte +55 -12
- package/dist/element/ElementHeading.svelte +7 -2
- package/dist/element/ElementPhoto.svelte +15 -9
- package/dist/element/ElementStats.svelte +10 -4
- package/dist/element/ElementTile.svelte +137 -73
- package/dist/element/Nucleus.svelte +39 -11
- package/dist/feedback/ClickFeedback.svelte +16 -5
- package/dist/feedback/DragOverlay.svelte +10 -2
- package/dist/feedback/Spinner.svelte +4 -2
- package/dist/feedback/StatusMessage.svelte +8 -2
- package/dist/fermi-surface/FermiSlice.svelte +118 -88
- package/dist/fermi-surface/FermiSurface.svelte +328 -187
- package/dist/fermi-surface/FermiSurface.svelte.d.ts +1 -1
- package/dist/fermi-surface/FermiSurfaceControls.svelte +113 -46
- package/dist/fermi-surface/FermiSurfaceControls.svelte.d.ts +1 -1
- package/dist/fermi-surface/FermiSurfaceScene.svelte +535 -342
- package/dist/fermi-surface/FermiSurfaceScene.svelte.d.ts +1 -1
- package/dist/fermi-surface/FermiSurfaceTooltip.svelte +14 -5
- package/dist/fermi-surface/compute.js +16 -20
- package/dist/fermi-surface/parse.js +24 -14
- package/dist/fermi-surface/symmetry.js +2 -7
- package/dist/fermi-surface/types.d.ts +3 -5
- package/dist/heatmap-matrix/HeatmapMatrix.svelte +1019 -765
- package/dist/heatmap-matrix/HeatmapMatrix.svelte.d.ts +1 -1
- package/dist/heatmap-matrix/HeatmapMatrixControls.svelte +76 -22
- package/dist/heatmap-matrix/HeatmapMatrixControls.svelte.d.ts +2 -3
- package/dist/icons.js +47 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/io/decompress.js +1 -1
- package/dist/io/export.d.ts +3 -0
- package/dist/io/export.js +129 -143
- package/dist/io/is-binary.js +2 -3
- package/dist/io/url-drop.js +1 -2
- package/dist/isosurface/Isosurface.svelte +202 -148
- package/dist/isosurface/IsosurfaceControls.svelte +46 -28
- package/dist/isosurface/parse.js +34 -29
- package/dist/isosurface/slice.js +5 -10
- package/dist/isosurface/types.d.ts +2 -1
- package/dist/isosurface/types.js +61 -12
- package/dist/labels.js +11 -8
- package/dist/layout/FullscreenToggle.svelte +11 -2
- package/dist/layout/InfoCard.svelte +38 -6
- package/dist/layout/InfoTag.svelte +63 -32
- package/dist/layout/PropertyFilter.svelte +82 -37
- package/dist/layout/SettingsSection.svelte +85 -55
- package/dist/layout/SubpageGrid.svelte +10 -2
- package/dist/layout/json-tree/JsonNode.svelte +183 -138
- package/dist/layout/json-tree/JsonTree.svelte +499 -413
- package/dist/layout/json-tree/JsonValue.svelte +127 -99
- package/dist/layout/json-tree/utils.js +4 -2
- package/dist/marching-cubes.js +25 -2
- package/dist/math.d.ts +13 -17
- package/dist/math.js +133 -67
- package/dist/overlays/ContextMenu.svelte +65 -40
- package/dist/overlays/DraggablePane.svelte +211 -139
- package/dist/periodic-table/PeriodicTable.svelte +278 -145
- package/dist/periodic-table/PeriodicTableControls.svelte +178 -128
- package/dist/periodic-table/PropertySelect.svelte +25 -7
- package/dist/periodic-table/TableInset.svelte +8 -3
- package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte +446 -309
- package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte.d.ts +1 -1
- package/dist/phase-diagram/PhaseDiagramControls.svelte +102 -43
- package/dist/phase-diagram/PhaseDiagramControls.svelte.d.ts +1 -1
- package/dist/phase-diagram/PhaseDiagramEditorPane.svelte +63 -40
- package/dist/phase-diagram/PhaseDiagramExportPane.svelte +71 -28
- package/dist/phase-diagram/PhaseDiagramExportPane.svelte.d.ts +1 -1
- package/dist/phase-diagram/PhaseDiagramTooltip.svelte +158 -101
- package/dist/phase-diagram/TdbInfoPanel.svelte +28 -4
- package/dist/phase-diagram/build-diagram.js +9 -9
- package/dist/phase-diagram/colors.js +1 -3
- package/dist/phase-diagram/parse.js +10 -9
- package/dist/phase-diagram/svg-to-diagram.js +53 -49
- package/dist/phase-diagram/utils.d.ts +1 -0
- package/dist/phase-diagram/utils.js +80 -25
- package/dist/plot/AxisLabel.svelte +28 -3
- package/dist/plot/BarPlot.svelte +1182 -734
- package/dist/plot/BarPlot.svelte.d.ts +2 -2
- package/dist/plot/BarPlotControls.svelte +31 -5
- package/dist/plot/BarPlotControls.svelte.d.ts +1 -1
- package/dist/plot/ColorBar.svelte +479 -329
- package/dist/plot/ColorScaleSelect.svelte +27 -6
- package/dist/plot/ElementScatter.svelte +36 -15
- package/dist/plot/FillArea.svelte +152 -95
- package/dist/plot/Histogram.svelte +934 -571
- package/dist/plot/Histogram.svelte.d.ts +1 -1
- package/dist/plot/HistogramControls.svelte +53 -9
- package/dist/plot/HistogramControls.svelte.d.ts +1 -1
- package/dist/plot/InteractiveAxisLabel.svelte +34 -11
- package/dist/plot/InteractiveAxisLabel.svelte.d.ts +1 -1
- package/dist/plot/Line.svelte +63 -28
- package/dist/plot/PlotControls.svelte +157 -114
- package/dist/plot/PlotControls.svelte.d.ts +1 -1
- package/dist/plot/PlotLegend.svelte +174 -91
- package/dist/plot/PlotTooltip.svelte +45 -6
- package/dist/plot/PortalSelect.svelte +175 -147
- package/dist/plot/ReferenceLine.svelte +76 -22
- package/dist/plot/ReferenceLine3D.svelte +132 -107
- package/dist/plot/ReferencePlane.svelte +146 -121
- package/dist/plot/ScatterPlot.svelte +1681 -1091
- package/dist/plot/ScatterPlot.svelte.d.ts +2 -2
- package/dist/plot/ScatterPlot3D.svelte +256 -131
- package/dist/plot/ScatterPlot3D.svelte.d.ts +2 -2
- package/dist/plot/ScatterPlot3DControls.svelte +113 -63
- package/dist/plot/ScatterPlot3DControls.svelte.d.ts +2 -1
- package/dist/plot/ScatterPlot3DScene.svelte +608 -403
- package/dist/plot/ScatterPlot3DScene.svelte.d.ts +2 -2
- package/dist/plot/ScatterPlotControls.svelte +65 -25
- package/dist/plot/ScatterPlotControls.svelte.d.ts +1 -1
- package/dist/plot/ScatterPoint.svelte +98 -26
- package/dist/plot/ScatterPoint.svelte.d.ts +1 -0
- package/dist/plot/SpacegroupBarPlot.svelte +142 -85
- package/dist/plot/Surface3D.svelte +159 -108
- package/dist/plot/ZeroLines.svelte +55 -3
- package/dist/plot/ZoomRect.svelte +4 -2
- package/dist/plot/axis-utils.js +1 -3
- package/dist/plot/data-cleaning.js +12 -28
- package/dist/plot/data-transform.js +2 -1
- package/dist/plot/fill-utils.js +2 -0
- package/dist/plot/layout.d.ts +4 -1
- package/dist/plot/layout.js +33 -14
- package/dist/plot/reference-line.d.ts +2 -2
- package/dist/plot/reference-line.js +7 -5
- package/dist/plot/scales.js +24 -36
- package/dist/plot/types.d.ts +11 -23
- package/dist/plot/types.js +6 -11
- package/dist/plot/utils/label-placement.d.ts +32 -15
- package/dist/plot/utils/label-placement.js +227 -66
- package/dist/plot/utils/series-visibility.js +2 -3
- package/dist/rdf/RdfPlot.svelte +143 -91
- package/dist/rdf/calc-rdf.js +4 -5
- package/dist/sanitize.d.ts +4 -0
- package/dist/sanitize.js +107 -0
- package/dist/settings.d.ts +18 -6
- package/dist/settings.js +46 -16
- package/dist/spectral/Bands.svelte +632 -453
- package/dist/spectral/BandsAndDos.svelte +90 -49
- package/dist/spectral/BrillouinBandsDos.svelte +151 -93
- package/dist/spectral/Dos.svelte +389 -258
- package/dist/spectral/helpers.js +55 -43
- package/dist/state.svelte.d.ts +1 -1
- package/dist/state.svelte.js +3 -2
- package/dist/structure/Arrow.svelte +59 -20
- package/dist/structure/AtomLegend.svelte +215 -134
- package/dist/structure/Bond.svelte +73 -47
- package/dist/structure/CanvasTooltip.svelte +10 -2
- package/dist/structure/CellSelect.svelte +72 -45
- package/dist/structure/Cylinder.svelte +33 -17
- package/dist/structure/Lattice.svelte +88 -33
- package/dist/structure/Structure.svelte +1063 -797
- package/dist/structure/Structure.svelte.d.ts +1 -1
- package/dist/structure/StructureControls.svelte +349 -118
- package/dist/structure/StructureExportPane.svelte +124 -89
- package/dist/structure/StructureExportPane.svelte.d.ts +1 -1
- package/dist/structure/StructureInfoPane.svelte +304 -237
- package/dist/structure/StructureScene.svelte +879 -443
- package/dist/structure/StructureScene.svelte.d.ts +15 -7
- package/dist/structure/atom-properties.js +8 -8
- package/dist/structure/bonding.js +6 -7
- package/dist/structure/export.js +14 -29
- package/dist/structure/ferrox-wasm.js +1 -1
- package/dist/structure/index.d.ts +13 -3
- package/dist/structure/index.js +83 -23
- package/dist/structure/measure.d.ts +2 -2
- package/dist/structure/measure.js +4 -44
- package/dist/structure/parse.js +113 -141
- package/dist/structure/partial-occupancy.js +7 -10
- package/dist/structure/pbc.d.ts +1 -0
- package/dist/structure/pbc.js +16 -6
- package/dist/structure/supercell.d.ts +2 -2
- package/dist/structure/supercell.js +12 -22
- package/dist/structure/validation.js +1 -2
- package/dist/symmetry/SymmetryStats.svelte +84 -41
- package/dist/symmetry/WyckoffTable.svelte +26 -6
- package/dist/symmetry/cell-transform.js +5 -3
- package/dist/symmetry/index.js +8 -7
- package/dist/symmetry/spacegroups.js +148 -148
- package/dist/table/HeatmapTable.svelte +790 -554
- package/dist/table/HeatmapTable.svelte.d.ts +1 -1
- package/dist/table/ToggleMenu.svelte +125 -92
- package/dist/table/index.js +2 -4
- package/dist/theme/ThemeControl.svelte +21 -12
- package/dist/time.js +4 -1
- package/dist/tooltip/TooltipContent.svelte +33 -8
- package/dist/trajectory/Trajectory.svelte +758 -558
- package/dist/trajectory/TrajectoryError.svelte +14 -3
- package/dist/trajectory/TrajectoryExportPane.svelte +137 -83
- package/dist/trajectory/TrajectoryInfoPane.svelte +272 -143
- package/dist/trajectory/extract.js +10 -26
- package/dist/trajectory/format-detect.js +5 -5
- package/dist/trajectory/frame-reader.d.ts +1 -1
- package/dist/trajectory/frame-reader.js +5 -12
- package/dist/trajectory/helpers.d.ts +0 -1
- package/dist/trajectory/helpers.js +2 -17
- package/dist/trajectory/index.js +14 -12
- package/dist/trajectory/parse/ase.js +5 -4
- package/dist/trajectory/parse/hdf5.js +26 -18
- package/dist/trajectory/parse/index.js +13 -18
- package/dist/trajectory/parse/lammps.js +17 -7
- package/dist/trajectory/parse/vasp.js +5 -2
- package/dist/trajectory/parse/xyz.js +8 -7
- package/dist/trajectory/plotting.js +13 -8
- package/dist/utils.d.ts +1 -0
- package/dist/utils.js +13 -0
- package/dist/xrd/XrdPlot.svelte +337 -247
- package/dist/xrd/broadening.js +14 -9
- package/dist/xrd/calc-xrd.js +12 -18
- package/dist/xrd/parse.d.ts +1 -1
- package/dist/xrd/parse.js +17 -17
- package/package.json +99 -103
- package/readme.md +1 -1
- /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) => [
|
|
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`) ===
|
|
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`,
|
package/dist/plot/scales.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as math from '../math';
|
|
2
|
-
import { get_arcsinh_threshold, get_scale_type_name, is_time_scale
|
|
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((
|
|
102
|
-
.
|
|
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((
|
|
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()
|
|
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 =
|
|
225
|
-
|
|
226
|
-
|
|
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((
|
|
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((
|
|
389
|
+
return detailed_ticks.filter((tick) => tick >= min && tick <= max);
|
|
397
390
|
}
|
|
398
|
-
return powers.filter((
|
|
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
|
-
|
|
417
|
-
|
|
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((
|
|
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;
|
package/dist/plot/types.d.ts
CHANGED
|
@@ -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
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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>;
|
package/dist/plot/types.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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 =
|
|
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
|
|
17
|
-
const
|
|
18
|
-
|
|
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
|
|
30
|
-
const
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
id
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
167
|
+
const num_labels = label_infos.length;
|
|
168
|
+
if (num_labels === 0)
|
|
47
169
|
return {};
|
|
48
|
-
|
|
49
|
-
|
|
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
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|