@opendata-ai/openchart-engine 7.2.4 → 7.4.0
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/index.d.ts +88 -39
- package/dist/index.js +4134 -2930
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/__snapshots__/compile-snapshot.test.ts.snap +143 -78
- package/src/__tests__/axes.test.ts +129 -1
- package/src/__tests__/compile-chart.test.ts +6 -0
- package/src/__tests__/compile-layer.test.ts +49 -0
- package/src/__tests__/dimensions.test.ts +70 -2
- package/src/__tests__/helpers/invariants.ts +265 -0
- package/src/__tests__/known-bugs.test.ts +113 -0
- package/src/__tests__/layout-fuzz.test.ts +358 -0
- package/src/__tests__/layout-invariants.test.ts +535 -0
- package/src/__tests__/legend.test.ts +37 -0
- package/src/annotations/__tests__/compute.test.ts +8 -15
- package/src/annotations/__tests__/placement-fuzz.test.ts +256 -0
- package/src/annotations/__tests__/placement-invariants.test.ts +345 -0
- package/src/annotations/__tests__/placement.test.ts +316 -0
- package/src/annotations/collisions.ts +19 -7
- package/src/annotations/compute.ts +257 -16
- package/src/annotations/geometry.ts +73 -15
- package/src/annotations/placement.ts +588 -0
- package/src/annotations/resolve-text.ts +58 -10
- package/src/charts/__tests__/post-process.test.ts +3 -3
- package/src/charts/bar/__tests__/compute.test.ts +131 -45
- package/src/charts/bar/compute.ts +44 -4
- package/src/charts/bar/index.ts +9 -2
- package/src/charts/bar/labels.ts +77 -26
- package/src/charts/column/labels.ts +29 -16
- package/src/charts/post-process.ts +21 -7
- package/src/charts/registry.ts +4 -2
- package/src/compile/layer.ts +31 -9
- package/src/compile.ts +88 -149
- package/src/index.ts +9 -0
- package/src/layout/axes/rotation.ts +36 -0
- package/src/layout/axes.ts +130 -8
- package/src/layout/dimensions.ts +80 -98
- package/src/layout/plan.ts +550 -0
- package/src/layout/scales.ts +22 -0
- package/src/layout/shared.ts +44 -0
- package/src/legend/compute.ts +315 -78
- package/src/legend/wrap.ts +11 -7
- package/src/sankey/__tests__/node-label-wrap.test.ts +2 -2
- package/src/tables/__tests__/heatmap.test.ts +24 -20
- package/src/tables/heatmap.ts +25 -9
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
import type { Rect } from '@opendata-ai/openchart-core';
|
|
2
|
+
import { overlapArea } from '@opendata-ai/openchart-core';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
import {
|
|
5
|
+
findBestPlacement,
|
|
6
|
+
isAutoPlacement,
|
|
7
|
+
type PlacementObstacle,
|
|
8
|
+
samplePolylineObstacles,
|
|
9
|
+
} from '../placement';
|
|
10
|
+
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// samplePolylineObstacles
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
describe('samplePolylineObstacles', () => {
|
|
16
|
+
it('returns empty array for empty input', () => {
|
|
17
|
+
expect(samplePolylineObstacles([], 'line')).toEqual([]);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('returns single padded rect for a single point', () => {
|
|
21
|
+
const pad = 3;
|
|
22
|
+
const result = samplePolylineObstacles([{ x: 50, y: 100 }], 'line', 24, pad);
|
|
23
|
+
expect(result).toHaveLength(1);
|
|
24
|
+
expect(result[0]).toEqual({
|
|
25
|
+
x: 50 - pad,
|
|
26
|
+
y: 100 - pad,
|
|
27
|
+
width: pad * 2,
|
|
28
|
+
height: pad * 2,
|
|
29
|
+
kind: 'line',
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('returns single vertical rect when all points share the same x', () => {
|
|
34
|
+
const pad = 3;
|
|
35
|
+
const points = [
|
|
36
|
+
{ x: 100, y: 10 },
|
|
37
|
+
{ x: 100, y: 50 },
|
|
38
|
+
{ x: 100, y: 30 },
|
|
39
|
+
];
|
|
40
|
+
const result = samplePolylineObstacles(points, 'line', 24, pad);
|
|
41
|
+
expect(result).toHaveLength(1);
|
|
42
|
+
expect(result[0].x).toBe(100 - pad);
|
|
43
|
+
expect(result[0].y).toBe(10 - pad);
|
|
44
|
+
expect(result[0].width).toBe(pad * 2);
|
|
45
|
+
expect(result[0].height).toBe(50 - 10 + pad * 2);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('produces gap-free coverage for a steep sine wave', () => {
|
|
49
|
+
// Generate a sine wave with enough amplitude to cross many buckets vertically
|
|
50
|
+
const bucketWidth = 20;
|
|
51
|
+
const points: Array<{ x: number; y: number }> = [];
|
|
52
|
+
for (let i = 0; i <= 200; i++) {
|
|
53
|
+
const x = i;
|
|
54
|
+
const y = Math.sin(x * 0.1) * 100;
|
|
55
|
+
points.push({ x, y });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const obstacles = samplePolylineObstacles(points, 'line', bucketWidth, 0);
|
|
59
|
+
expect(obstacles.length).toBeGreaterThan(0);
|
|
60
|
+
|
|
61
|
+
// The union of obstacle x-ranges should cover the entire polyline x-extent
|
|
62
|
+
const xMin = Math.min(...points.map((p) => p.x));
|
|
63
|
+
const xMax = Math.max(...points.map((p) => p.x));
|
|
64
|
+
|
|
65
|
+
// Sort obstacles by x and verify continuous coverage
|
|
66
|
+
const sorted = [...obstacles].sort((a, b) => a.x - b.x);
|
|
67
|
+
expect(sorted[0].x).toBeLessThanOrEqual(xMin);
|
|
68
|
+
const lastObs = sorted[sorted.length - 1];
|
|
69
|
+
expect(lastObs.x + lastObs.width).toBeGreaterThanOrEqual(xMax);
|
|
70
|
+
|
|
71
|
+
// Check no gaps between consecutive obstacles
|
|
72
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
73
|
+
const prevEnd = sorted[i - 1].x + sorted[i - 1].width;
|
|
74
|
+
expect(sorted[i].x).toBeLessThanOrEqual(prevEnd + 0.001);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('captures min/max y within each bucket for monotone increasing data', () => {
|
|
79
|
+
const bucketWidth = 50;
|
|
80
|
+
const pad = 0;
|
|
81
|
+
// Monotone increasing: y = x
|
|
82
|
+
const points: Array<{ x: number; y: number }> = [];
|
|
83
|
+
for (let i = 0; i <= 200; i += 10) {
|
|
84
|
+
points.push({ x: i, y: i });
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const obstacles = samplePolylineObstacles(points, 'line', bucketWidth, pad);
|
|
88
|
+
|
|
89
|
+
for (const obs of obstacles) {
|
|
90
|
+
// Find all points within this bucket's x-range
|
|
91
|
+
const bucketXStart = obs.x;
|
|
92
|
+
const bucketXEnd = obs.x + obs.width;
|
|
93
|
+
const inBucket = points.filter((p) => p.x >= bucketXStart && p.x < bucketXEnd);
|
|
94
|
+
|
|
95
|
+
if (inBucket.length === 0) continue;
|
|
96
|
+
|
|
97
|
+
const minY = Math.min(...inBucket.map((p) => p.y));
|
|
98
|
+
const maxY = Math.max(...inBucket.map((p) => p.y));
|
|
99
|
+
|
|
100
|
+
// The obstacle's y range should contain the points' y range within this bucket
|
|
101
|
+
// (it may be larger due to interpolation at bucket edges, but should not be smaller)
|
|
102
|
+
expect(obs.y).toBeLessThanOrEqual(minY);
|
|
103
|
+
expect(obs.y + obs.height).toBeGreaterThanOrEqual(maxY);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('bucket width parameter controls granularity', () => {
|
|
108
|
+
const points = [
|
|
109
|
+
{ x: 0, y: 0 },
|
|
110
|
+
{ x: 100, y: 100 },
|
|
111
|
+
];
|
|
112
|
+
|
|
113
|
+
const narrow = samplePolylineObstacles(points, 'line', 10, 0);
|
|
114
|
+
const wide = samplePolylineObstacles(points, 'line', 50, 0);
|
|
115
|
+
|
|
116
|
+
// Narrower buckets should produce more obstacles
|
|
117
|
+
expect(narrow.length).toBeGreaterThan(wide.length);
|
|
118
|
+
// Each obstacle's width matches the bucket width
|
|
119
|
+
for (const obs of narrow) expect(obs.width).toBe(10);
|
|
120
|
+
for (const obs of wide) expect(obs.width).toBe(50);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// ---------------------------------------------------------------------------
|
|
125
|
+
// overlapArea (from @opendata-ai/openchart-core)
|
|
126
|
+
// ---------------------------------------------------------------------------
|
|
127
|
+
|
|
128
|
+
describe('overlapArea', () => {
|
|
129
|
+
it('returns 0 for disjoint rects', () => {
|
|
130
|
+
const a: Rect = { x: 0, y: 0, width: 10, height: 10 };
|
|
131
|
+
const b: Rect = { x: 20, y: 20, width: 10, height: 10 };
|
|
132
|
+
expect(overlapArea(a, b)).toBe(0);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('returns correct area for partial overlap', () => {
|
|
136
|
+
const a: Rect = { x: 0, y: 0, width: 10, height: 10 };
|
|
137
|
+
const b: Rect = { x: 5, y: 5, width: 10, height: 10 };
|
|
138
|
+
// Overlap region: x=[5,10], y=[5,10] => 5*5 = 25
|
|
139
|
+
expect(overlapArea(a, b)).toBe(25);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('returns smaller rects area when one rect is fully contained', () => {
|
|
143
|
+
const outer: Rect = { x: 0, y: 0, width: 100, height: 100 };
|
|
144
|
+
const inner: Rect = { x: 10, y: 10, width: 20, height: 30 };
|
|
145
|
+
expect(overlapArea(outer, inner)).toBe(20 * 30);
|
|
146
|
+
// Symmetric
|
|
147
|
+
expect(overlapArea(inner, outer)).toBe(20 * 30);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('returns full area for identical rects', () => {
|
|
151
|
+
const r: Rect = { x: 5, y: 5, width: 40, height: 60 };
|
|
152
|
+
expect(overlapArea(r, r)).toBe(40 * 60);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// ---------------------------------------------------------------------------
|
|
157
|
+
// Scorer (via findBestPlacement)
|
|
158
|
+
// ---------------------------------------------------------------------------
|
|
159
|
+
|
|
160
|
+
describe('findBestPlacement scorer', () => {
|
|
161
|
+
// A large chart area and SVG rect so nothing clips
|
|
162
|
+
const chartArea: Rect = { x: 0, y: 0, width: 800, height: 600 };
|
|
163
|
+
const svgRect: Rect = { x: 0, y: 0, width: 800, height: 600 };
|
|
164
|
+
const style = { fontSize: 12, fontWeight: 400, lineHeight: 1.3 };
|
|
165
|
+
|
|
166
|
+
it('picks NE (direction index 1) first for a lower-half anchor on an empty chart', () => {
|
|
167
|
+
// Anchor in the lower half of the chart
|
|
168
|
+
const result = findBestPlacement(
|
|
169
|
+
400,
|
|
170
|
+
400,
|
|
171
|
+
'Test',
|
|
172
|
+
style,
|
|
173
|
+
undefined,
|
|
174
|
+
undefined,
|
|
175
|
+
[],
|
|
176
|
+
chartArea,
|
|
177
|
+
svgRect,
|
|
178
|
+
undefined,
|
|
179
|
+
true,
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
// LOWER_HALF_ORDER = [1, 0, 7, 2, 3, 4, 5, 6] => first tried is NE (index 1)
|
|
183
|
+
// On empty chart, the first direction tried gets the lowest tiebreak and wins
|
|
184
|
+
// (all candidates score close to 0 for obstacles/clipping, tiebreak resolves it)
|
|
185
|
+
expect(result.debug).toBeDefined();
|
|
186
|
+
// The best candidate should be direction index 1 (NE)
|
|
187
|
+
const best = result.debug!.reduce((a, b) => (a.total <= b.total ? a : b));
|
|
188
|
+
expect(best.direction).toBe('NE');
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('picks SE (direction index 3) first for an upper-half anchor on an empty chart', () => {
|
|
192
|
+
// Anchor in the upper half of the chart
|
|
193
|
+
const result = findBestPlacement(
|
|
194
|
+
400,
|
|
195
|
+
100,
|
|
196
|
+
'Test',
|
|
197
|
+
style,
|
|
198
|
+
undefined,
|
|
199
|
+
undefined,
|
|
200
|
+
[],
|
|
201
|
+
chartArea,
|
|
202
|
+
svgRect,
|
|
203
|
+
undefined,
|
|
204
|
+
true,
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
// UPPER_HALF_ORDER = [3, 4, 5, 2, 1, 0, 7, 6] => first tried is SE (index 3)
|
|
208
|
+
const best = result.debug!.reduce((a, b) => (a.total <= b.total ? a : b));
|
|
209
|
+
expect(best.direction).toBe('SE');
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('avoids the N direction when blocked by a hard obstacle', () => {
|
|
213
|
+
// Place a large hard obstacle directly above the anchor so that the N direction
|
|
214
|
+
// would overlap it heavily
|
|
215
|
+
const anchorX = 400;
|
|
216
|
+
const anchorY = 400;
|
|
217
|
+
const obstacle: PlacementObstacle = {
|
|
218
|
+
x: anchorX - 100,
|
|
219
|
+
y: anchorY - 80,
|
|
220
|
+
width: 200,
|
|
221
|
+
height: 60,
|
|
222
|
+
kind: 'line',
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
const result = findBestPlacement(
|
|
226
|
+
anchorX,
|
|
227
|
+
anchorY,
|
|
228
|
+
'Test label',
|
|
229
|
+
style,
|
|
230
|
+
undefined,
|
|
231
|
+
undefined,
|
|
232
|
+
[obstacle],
|
|
233
|
+
chartArea,
|
|
234
|
+
svgRect,
|
|
235
|
+
undefined,
|
|
236
|
+
true,
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
// The winning direction should NOT be N (index 0), because the obstacle blocks it
|
|
240
|
+
const best = result.debug!.reduce((a, b) => (a.total <= b.total ? a : b));
|
|
241
|
+
expect(best.direction).not.toBe('N');
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('penalizes area-fill overlap less than line overlap of equal area', () => {
|
|
245
|
+
const anchorX = 400;
|
|
246
|
+
const anchorY = 300;
|
|
247
|
+
|
|
248
|
+
// Surround the anchor with obstacles on all sides so every candidate
|
|
249
|
+
// overlaps at least one. The winning candidate will necessarily overlap,
|
|
250
|
+
// making the obstacle kind weight the differentiating factor.
|
|
251
|
+
function makeWall(kind: 'line' | 'area-fill'): PlacementObstacle[] {
|
|
252
|
+
return [{ x: anchorX - 200, y: anchorY - 200, width: 400, height: 400, kind }];
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const lineResult = findBestPlacement(
|
|
256
|
+
anchorX,
|
|
257
|
+
anchorY,
|
|
258
|
+
'Test',
|
|
259
|
+
style,
|
|
260
|
+
undefined,
|
|
261
|
+
undefined,
|
|
262
|
+
makeWall('line'),
|
|
263
|
+
chartArea,
|
|
264
|
+
svgRect,
|
|
265
|
+
undefined,
|
|
266
|
+
true,
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
const areaResult = findBestPlacement(
|
|
270
|
+
anchorX,
|
|
271
|
+
anchorY,
|
|
272
|
+
'Test',
|
|
273
|
+
style,
|
|
274
|
+
undefined,
|
|
275
|
+
undefined,
|
|
276
|
+
makeWall('area-fill'),
|
|
277
|
+
chartArea,
|
|
278
|
+
svgRect,
|
|
279
|
+
undefined,
|
|
280
|
+
true,
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
// Both winners overlap their obstacle, but area-fill weight (1.5) is much
|
|
284
|
+
// lower than line weight (20), so the area-fill score should be smaller.
|
|
285
|
+
expect(areaResult.score).toBeLessThan(lineResult.score);
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
// ---------------------------------------------------------------------------
|
|
290
|
+
// isAutoPlacement
|
|
291
|
+
// ---------------------------------------------------------------------------
|
|
292
|
+
|
|
293
|
+
describe('isAutoPlacement', () => {
|
|
294
|
+
it('returns true for plain annotation without offset/anchor/drop-line', () => {
|
|
295
|
+
expect(isAutoPlacement({})).toBe(true);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it('returns false when offset is set', () => {
|
|
299
|
+
expect(isAutoPlacement({ offset: { dx: 10 } })).toBe(false);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it('returns false when anchor is not auto', () => {
|
|
303
|
+
expect(isAutoPlacement({ anchor: 'top' })).toBe(false);
|
|
304
|
+
expect(isAutoPlacement({ anchor: 'left' })).toBe(false);
|
|
305
|
+
expect(isAutoPlacement({ anchor: 'right' })).toBe(false);
|
|
306
|
+
expect(isAutoPlacement({ anchor: 'bottom' })).toBe(false);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it('returns false when connector is drop-line', () => {
|
|
310
|
+
expect(isAutoPlacement({ connector: 'drop-line' })).toBe(false);
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it('returns true when anchor is explicitly auto', () => {
|
|
314
|
+
expect(isAutoPlacement({ anchor: 'auto' })).toBe(true);
|
|
315
|
+
});
|
|
316
|
+
});
|
|
@@ -13,7 +13,12 @@ import { detectCollision } from '@opendata-ai/openchart-core';
|
|
|
13
13
|
import type { NormalizedChartSpec } from '../compiler/types';
|
|
14
14
|
import type { ResolvedScales } from '../layout/scales';
|
|
15
15
|
import { CLAMP_MARGIN, NUDGE_PADDING } from './constants';
|
|
16
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
type AnnotationMeasureTextFn,
|
|
18
|
+
estimateLabelBounds,
|
|
19
|
+
heuristicMeasure,
|
|
20
|
+
recomputeConnector,
|
|
21
|
+
} from './geometry';
|
|
17
22
|
import { resolvePosition } from './position';
|
|
18
23
|
|
|
19
24
|
/**
|
|
@@ -61,10 +66,11 @@ export function nudgeAnnotationFromObstacles(
|
|
|
61
66
|
scales: ResolvedScales,
|
|
62
67
|
chartArea: Rect,
|
|
63
68
|
obstacles: Rect[],
|
|
69
|
+
measure: AnnotationMeasureTextFn = heuristicMeasure,
|
|
64
70
|
): boolean {
|
|
65
71
|
if (annotation.type !== 'text' || !annotation.label) return false;
|
|
66
72
|
|
|
67
|
-
const labelBounds = estimateLabelBounds(annotation.label);
|
|
73
|
+
const labelBounds = estimateLabelBounds(annotation.label, measure);
|
|
68
74
|
const collidingObs = obstacles.filter(
|
|
69
75
|
(obs) => obs.width > 0 && obs.height > 0 && detectCollision(labelBounds, obs),
|
|
70
76
|
);
|
|
@@ -90,7 +96,7 @@ export function nudgeAnnotationFromObstacles(
|
|
|
90
96
|
connector: recomputeConnector({ ...annotation.label, x: newLabelX, y: newLabelY }, px, py),
|
|
91
97
|
};
|
|
92
98
|
|
|
93
|
-
const candidateBounds = estimateLabelBounds(candidateLabel);
|
|
99
|
+
const candidateBounds = estimateLabelBounds(candidateLabel, measure);
|
|
94
100
|
|
|
95
101
|
// Check no collisions with any obstacle
|
|
96
102
|
const stillCollides = obstacles.some(
|
|
@@ -114,6 +120,7 @@ export function nudgeAnnotationFromObstacles(
|
|
|
114
120
|
|
|
115
121
|
if (inBounds) {
|
|
116
122
|
annotation.label = candidateLabel;
|
|
123
|
+
annotation.label.bounds = candidateBounds;
|
|
117
124
|
return true;
|
|
118
125
|
}
|
|
119
126
|
}
|
|
@@ -134,6 +141,7 @@ export function resolveAnnotationCollisions(
|
|
|
134
141
|
originalSpecs: NormalizedChartSpec['annotations'],
|
|
135
142
|
scales: ResolvedScales,
|
|
136
143
|
chartArea: Rect,
|
|
144
|
+
measure: AnnotationMeasureTextFn = heuristicMeasure,
|
|
137
145
|
): void {
|
|
138
146
|
const placedBounds: Rect[] = [];
|
|
139
147
|
|
|
@@ -143,7 +151,7 @@ export function resolveAnnotationCollisions(
|
|
|
143
151
|
continue;
|
|
144
152
|
}
|
|
145
153
|
|
|
146
|
-
const bounds = estimateLabelBounds(annotation.label);
|
|
154
|
+
const bounds = estimateLabelBounds(annotation.label, measure);
|
|
147
155
|
|
|
148
156
|
// Check against all previously placed annotation labels
|
|
149
157
|
const collidingBounds = placedBounds.filter(
|
|
@@ -171,7 +179,7 @@ export function resolveAnnotationCollisions(
|
|
|
171
179
|
x: newLabelX,
|
|
172
180
|
y: newLabelY,
|
|
173
181
|
};
|
|
174
|
-
const candidateBounds = estimateLabelBounds(candidateLabel);
|
|
182
|
+
const candidateBounds = estimateLabelBounds(candidateLabel, measure);
|
|
175
183
|
|
|
176
184
|
// Check no collisions with any placed label
|
|
177
185
|
const stillCollides = placedBounds.some(
|
|
@@ -193,6 +201,7 @@ export function resolveAnnotationCollisions(
|
|
|
193
201
|
...annotation.label,
|
|
194
202
|
x: newLabelX,
|
|
195
203
|
y: newLabelY,
|
|
204
|
+
bounds: candidateBounds,
|
|
196
205
|
connector: recomputeConnector(
|
|
197
206
|
{ ...annotation.label, x: newLabelX, y: newLabelY },
|
|
198
207
|
px,
|
|
@@ -207,7 +216,7 @@ export function resolveAnnotationCollisions(
|
|
|
207
216
|
}
|
|
208
217
|
|
|
209
218
|
// Add this annotation's final bounds to the placed list
|
|
210
|
-
placedBounds.push(estimateLabelBounds(annotation.label));
|
|
219
|
+
placedBounds.push(estimateLabelBounds(annotation.label, measure));
|
|
211
220
|
}
|
|
212
221
|
}
|
|
213
222
|
|
|
@@ -221,11 +230,12 @@ export function clampAnnotationsToBounds(
|
|
|
221
230
|
annotations: ResolvedAnnotation[],
|
|
222
231
|
svgWidth: number,
|
|
223
232
|
svgHeight: number,
|
|
233
|
+
measure: AnnotationMeasureTextFn = heuristicMeasure,
|
|
224
234
|
): void {
|
|
225
235
|
for (const annotation of annotations) {
|
|
226
236
|
if (annotation.type !== 'text' || !annotation.label) continue;
|
|
227
237
|
|
|
228
|
-
const bounds = estimateLabelBounds(annotation.label);
|
|
238
|
+
const bounds = estimateLabelBounds(annotation.label, measure);
|
|
229
239
|
let dx = 0;
|
|
230
240
|
let dy = 0;
|
|
231
241
|
|
|
@@ -252,10 +262,12 @@ export function clampAnnotationsToBounds(
|
|
|
252
262
|
const newY = annotation.label.y + dy;
|
|
253
263
|
|
|
254
264
|
const connector = annotation.label.connector;
|
|
265
|
+
const newBounds = estimateLabelBounds({ ...annotation.label, x: newX, y: newY }, measure);
|
|
255
266
|
annotation.label = {
|
|
256
267
|
...annotation.label,
|
|
257
268
|
x: newX,
|
|
258
269
|
y: newY,
|
|
270
|
+
bounds: newBounds,
|
|
259
271
|
connector: connector
|
|
260
272
|
? recomputeConnector(
|
|
261
273
|
{ ...annotation.label, x: newX, y: newY },
|