@opendata-ai/openchart-engine 8.0.0-rc.8 → 8.0.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 +149 -12
- package/dist/index.js +894 -171
- package/dist/index.js.map +1 -1
- package/package.json +3 -4
- package/src/__test-fixtures__/specs.ts +0 -182
- package/src/__tests__/__snapshots__/compile-snapshot.test.ts.snap +0 -2339
- package/src/__tests__/annotation-behavior.test.ts +0 -852
- package/src/__tests__/axes.test.ts +0 -1579
- package/src/__tests__/compile-animation.test.ts +0 -161
- package/src/__tests__/compile-chart.test.ts +0 -1303
- package/src/__tests__/compile-footnotes.test.ts +0 -277
- package/src/__tests__/compile-layer.test.ts +0 -969
- package/src/__tests__/compile-snapshot.test.ts +0 -156
- package/src/__tests__/compound-labels.test.ts +0 -147
- package/src/__tests__/dimensions.test.ts +0 -918
- package/src/__tests__/encoding-sugar.test.ts +0 -390
- package/src/__tests__/facet.test.ts +0 -215
- package/src/__tests__/helpers/invariants.ts +0 -299
- package/src/__tests__/known-bugs.test.ts +0 -566
- package/src/__tests__/layout-fuzz.test.ts +0 -358
- package/src/__tests__/layout-invariants.test.ts +0 -535
- package/src/__tests__/legend.test.ts +0 -721
- package/src/__tests__/mark-keys.test.ts +0 -294
- package/src/__tests__/ranking-recipes.test.ts +0 -260
- package/src/__tests__/rotation.test.ts +0 -83
- package/src/__tests__/scales.test.ts +0 -323
- package/src/__tests__/series-search.test.ts +0 -92
- package/src/__tests__/spec-sugar.test.ts +0 -801
- package/src/__tests__/you-draw-it.test.ts +0 -160
- package/src/annotations/__tests__/compute.test.ts +0 -1672
- package/src/annotations/__tests__/geometry.test.ts +0 -66
- package/src/annotations/__tests__/placement-fuzz.test.ts +0 -256
- package/src/annotations/__tests__/placement-invariants.test.ts +0 -345
- package/src/annotations/__tests__/placement.test.ts +0 -316
- package/src/annotations/__tests__/resolve-text.test.ts +0 -788
- package/src/annotations/__tests__/rich-text.test.ts +0 -91
- package/src/annotations/__tests__/thinning.test.ts +0 -285
- package/src/annotations/collisions.ts +0 -326
- package/src/annotations/compute.ts +0 -333
- package/src/annotations/constants.ts +0 -162
- package/src/annotations/geometry.ts +0 -376
- package/src/annotations/move.ts +0 -135
- package/src/annotations/placement.ts +0 -636
- package/src/annotations/position.ts +0 -131
- package/src/annotations/resolve-range.ts +0 -117
- package/src/annotations/resolve-refline.ts +0 -157
- package/src/annotations/resolve-text.ts +0 -545
- package/src/annotations/rich-text.ts +0 -65
- package/src/annotations/thinning.ts +0 -190
- package/src/barlist/__tests__/compile-barlist.test.ts +0 -200
- package/src/barlist/compile-barlist.ts +0 -391
- package/src/barlist/types.ts +0 -28
- package/src/charts/__tests__/post-process.test.ts +0 -237
- package/src/charts/__tests__/registry.test.ts +0 -116
- package/src/charts/__tests__/utils.test.ts +0 -195
- package/src/charts/_shared/__tests__/density-filter.test.ts +0 -32
- package/src/charts/_shared/density-filter.ts +0 -26
- package/src/charts/_shared/format-label-value.ts +0 -16
- package/src/charts/bar/__tests__/compute.test.ts +0 -934
- package/src/charts/bar/__tests__/gradient-orient.test.ts +0 -127
- package/src/charts/bar/__tests__/labels.test.ts +0 -292
- package/src/charts/bar/compute.ts +0 -539
- package/src/charts/bar/index.ts +0 -60
- package/src/charts/bar/labels.ts +0 -311
- package/src/charts/beeswarm/__tests__/compute.test.ts +0 -280
- package/src/charts/beeswarm/__tests__/dodge.test.ts +0 -148
- package/src/charts/beeswarm/compute.ts +0 -203
- package/src/charts/beeswarm/dodge.ts +0 -155
- package/src/charts/beeswarm/index.ts +0 -32
- package/src/charts/builtin.ts +0 -75
- package/src/charts/calendar/__tests__/compute.test.ts +0 -417
- package/src/charts/calendar/compute.ts +0 -319
- package/src/charts/calendar/index.ts +0 -32
- package/src/charts/column/__tests__/compute.test.ts +0 -540
- package/src/charts/column/__tests__/labels.test.ts +0 -179
- package/src/charts/column/compute.ts +0 -493
- package/src/charts/column/index.ts +0 -53
- package/src/charts/column/labels.ts +0 -229
- package/src/charts/dot/__tests__/compute.test.ts +0 -380
- package/src/charts/dot/__tests__/labels.test.ts +0 -98
- package/src/charts/dot/compute.ts +0 -285
- package/src/charts/dot/index.ts +0 -62
- package/src/charts/dot/labels.ts +0 -121
- package/src/charts/line/__tests__/compute.test.ts +0 -1194
- package/src/charts/line/__tests__/curves.test.ts +0 -51
- package/src/charts/line/__tests__/labels.test.ts +0 -94
- package/src/charts/line/__tests__/mark-options.test.ts +0 -175
- package/src/charts/line/area.ts +0 -484
- package/src/charts/line/compute.ts +0 -356
- package/src/charts/line/curves.ts +0 -85
- package/src/charts/line/index.ts +0 -176
- package/src/charts/line/labels.ts +0 -177
- package/src/charts/parliament/__tests__/compute.test.ts +0 -411
- package/src/charts/parliament/compute.ts +0 -313
- package/src/charts/parliament/index.ts +0 -32
- package/src/charts/pie/__tests__/compute.test.ts +0 -384
- package/src/charts/pie/__tests__/labels.test.ts +0 -132
- package/src/charts/pie/compute.ts +0 -324
- package/src/charts/pie/index.ts +0 -49
- package/src/charts/pie/labels.ts +0 -143
- package/src/charts/post-process.ts +0 -229
- package/src/charts/range/__tests__/compute.test.ts +0 -486
- package/src/charts/range/compute.ts +0 -273
- package/src/charts/range/index.ts +0 -82
- package/src/charts/range/labels.ts +0 -169
- package/src/charts/rect/__tests__/compute.test.ts +0 -120
- package/src/charts/rect/index.ts +0 -127
- package/src/charts/registry.ts +0 -72
- package/src/charts/rule/index.ts +0 -139
- package/src/charts/scatter/__tests__/compute.test.ts +0 -488
- package/src/charts/scatter/__tests__/trendline.test.ts +0 -191
- package/src/charts/scatter/compute.ts +0 -192
- package/src/charts/scatter/index.ts +0 -46
- package/src/charts/scatter/trendline.ts +0 -100
- package/src/charts/text/__tests__/compute.test.ts +0 -307
- package/src/charts/text/index.ts +0 -141
- package/src/charts/tick/__tests__/compute.test.ts +0 -75
- package/src/charts/tick/index.ts +0 -99
- package/src/charts/utils.ts +0 -170
- package/src/charts/waffle/__tests__/compute.test.ts +0 -305
- package/src/charts/waffle/compute.ts +0 -197
- package/src/charts/waffle/index.ts +0 -31
- package/src/compile/__tests__/color-scale-range.test.ts +0 -158
- package/src/compile/__tests__/color-scale-reverse.test.ts +0 -89
- package/src/compile/__tests__/data-clip.test.ts +0 -127
- package/src/compile/__tests__/fill-patterns.test.ts +0 -154
- package/src/compile/__tests__/size-scale.test.ts +0 -108
- package/src/compile/__tests__/watermark-obstacle.test.ts +0 -93
- package/src/compile/color-scale-range.ts +0 -101
- package/src/compile/data-clip.ts +0 -48
- package/src/compile/fill-patterns.ts +0 -74
- package/src/compile/layer.ts +0 -626
- package/src/compile/size-scale.ts +0 -141
- package/src/compile/spec-sugar.ts +0 -758
- package/src/compile/watermark-obstacle.ts +0 -54
- package/src/compile/you-draw-it.ts +0 -123
- package/src/compile.ts +0 -1450
- package/src/compiler/__tests__/a11y-warnings.test.ts +0 -148
- package/src/compiler/__tests__/animation.test.ts +0 -177
- package/src/compiler/__tests__/compile.test.ts +0 -87
- package/src/compiler/__tests__/fixtures/vl-parity/axis-null.json +0 -19
- package/src/compiler/__tests__/fixtures/vl-parity/color-value.json +0 -20
- package/src/compiler/__tests__/fixtures/vl-parity/count-aggregate.json +0 -22
- package/src/compiler/__tests__/fixtures/vl-parity/data-values.json +0 -19
- package/src/compiler/__tests__/fixtures/vl-parity/layer-axis-null.json +0 -14
- package/src/compiler/__tests__/fixtures/vl-parity/layer-count.json +0 -34
- package/src/compiler/__tests__/fixtures/vl-parity/layer-data-values.json +0 -22
- package/src/compiler/__tests__/fixtures/vl-parity/layer-legend-null.json +0 -41
- package/src/compiler/__tests__/fixtures/vl-parity/layer-scheme.json +0 -38
- package/src/compiler/__tests__/fixtures/vl-parity/layer-sort.json +0 -28
- package/src/compiler/__tests__/fixtures/vl-parity/layer-theta.json +0 -28
- package/src/compiler/__tests__/fixtures/vl-parity/layer-title.json +0 -30
- package/src/compiler/__tests__/fixtures/vl-parity/layer-value-def.json +0 -29
- package/src/compiler/__tests__/fixtures/vl-parity/legend-null.json +0 -32
- package/src/compiler/__tests__/fixtures/vl-parity/scheme.json +0 -25
- package/src/compiler/__tests__/fixtures/vl-parity/sort-by-value.json +0 -19
- package/src/compiler/__tests__/fixtures/vl-parity/theta-arc.json +0 -19
- package/src/compiler/__tests__/fixtures/vl-parity/title-subtitle.json +0 -21
- package/src/compiler/__tests__/fixtures/vl-parity/width-height.json +0 -23
- package/src/compiler/__tests__/keys.test.ts +0 -83
- package/src/compiler/__tests__/normalize-highlight.test.ts +0 -103
- package/src/compiler/__tests__/normalize.test.ts +0 -399
- package/src/compiler/__tests__/sparkline-defaults.test.ts +0 -153
- package/src/compiler/__tests__/validate-did-you-mean.test.ts +0 -170
- package/src/compiler/__tests__/validate-highlight.test.ts +0 -112
- package/src/compiler/__tests__/validate-near-miss.test.ts +0 -251
- package/src/compiler/__tests__/validate.test.ts +0 -629
- package/src/compiler/__tests__/vl-parity.test.ts +0 -68
- package/src/compiler/a11y-warnings.ts +0 -110
- package/src/compiler/animation.ts +0 -142
- package/src/compiler/index.ts +0 -47
- package/src/compiler/keys.ts +0 -55
- package/src/compiler/normalize.ts +0 -734
- package/src/compiler/sparkline-defaults.ts +0 -138
- package/src/compiler/types.ts +0 -241
- package/src/compiler/validate.ts +0 -2041
- package/src/endpoint-labels/__tests__/compute.test.ts +0 -587
- package/src/endpoint-labels/compute.ts +0 -621
- package/src/endpoint-labels/constants.ts +0 -54
- package/src/endpoint-labels/format.ts +0 -60
- package/src/endpoint-labels/predict.ts +0 -139
- package/src/format/__tests__/field-format.test.ts +0 -164
- package/src/format/field-format.ts +0 -27
- package/src/geo/__tests__/compile-map.test.ts +0 -1004
- package/src/geo/compile-map.ts +0 -1341
- package/src/geo/join.ts +0 -115
- package/src/geo/projections.ts +0 -45
- package/src/geo/types.ts +0 -26
- package/src/graphs/__tests__/community.test.ts +0 -228
- package/src/graphs/__tests__/compile-graph.test.ts +0 -396
- package/src/graphs/__tests__/encoding.test.ts +0 -415
- package/src/graphs/community.ts +0 -92
- package/src/graphs/compile-graph.ts +0 -329
- package/src/graphs/encoding.ts +0 -356
- package/src/graphs/types.ts +0 -106
- package/src/index.ts +0 -177
- package/src/layout/__tests__/metrics.test.ts +0 -97
- package/src/layout/__tests__/scales-highlight.test.ts +0 -192
- package/src/layout/axes/rotation.ts +0 -152
- package/src/layout/axes/thinning.ts +0 -98
- package/src/layout/axes/ticks.ts +0 -409
- package/src/layout/axes.ts +0 -757
- package/src/layout/dimensions.ts +0 -912
- package/src/layout/facet.ts +0 -91
- package/src/layout/gridlines.ts +0 -84
- package/src/layout/metrics.ts +0 -132
- package/src/layout/plan.ts +0 -631
- package/src/layout/scales.ts +0 -1053
- package/src/layout/shared.ts +0 -44
- package/src/legend/__tests__/continuous.test.ts +0 -318
- package/src/legend/__tests__/redundancy.test.ts +0 -108
- package/src/legend/__tests__/size.test.ts +0 -348
- package/src/legend/__tests__/suppression.test.ts +0 -382
- package/src/legend/compute.ts +0 -747
- package/src/legend/continuous.ts +0 -327
- package/src/legend/size.ts +0 -407
- package/src/legend/suppression.ts +0 -226
- package/src/legend/wrap.ts +0 -117
- package/src/sankey/__tests__/compile-sankey.test.ts +0 -458
- package/src/sankey/__tests__/layout.test.ts +0 -165
- package/src/sankey/__tests__/node-label-wrap.test.ts +0 -114
- package/src/sankey/__tests__/node-sort.test.ts +0 -45
- package/src/sankey/compile-sankey.ts +0 -706
- package/src/sankey/layout.ts +0 -184
- package/src/sankey/types.ts +0 -41
- package/src/tables/__tests__/bar-column.test.ts +0 -147
- package/src/tables/__tests__/category-colors.test.ts +0 -230
- package/src/tables/__tests__/compile-table.test.ts +0 -236
- package/src/tables/__tests__/format-cells.test.ts +0 -143
- package/src/tables/__tests__/heatmap.test.ts +0 -148
- package/src/tables/__tests__/pagination.test.ts +0 -78
- package/src/tables/__tests__/search.test.ts +0 -94
- package/src/tables/__tests__/sort.test.ts +0 -107
- package/src/tables/__tests__/sparkline.test.ts +0 -122
- package/src/tables/bar-column.ts +0 -94
- package/src/tables/category-colors.ts +0 -78
- package/src/tables/compile-table.ts +0 -427
- package/src/tables/format-cells.ts +0 -115
- package/src/tables/heatmap.ts +0 -141
- package/src/tables/pagination.ts +0 -46
- package/src/tables/search.ts +0 -66
- package/src/tables/sort.ts +0 -69
- package/src/tables/sparkline.ts +0 -113
- package/src/tables/utils.ts +0 -16
- package/src/tilemap/__tests__/compile-tilemap.test.ts +0 -576
- package/src/tilemap/compile-tilemap.ts +0 -755
- package/src/tilemap/layout.ts +0 -172
- package/src/tilemap/types.ts +0 -33
- package/src/tooltips/__tests__/compute.test.ts +0 -582
- package/src/tooltips/compute.ts +0 -700
- package/src/transforms/__tests__/aggregate.test.ts +0 -159
- package/src/transforms/__tests__/bin.test.ts +0 -88
- package/src/transforms/__tests__/calculate.test.ts +0 -146
- package/src/transforms/__tests__/conditional.test.ts +0 -127
- package/src/transforms/__tests__/filter-relative.test.ts +0 -202
- package/src/transforms/__tests__/filter.test.ts +0 -59
- package/src/transforms/__tests__/fold.test.ts +0 -79
- package/src/transforms/__tests__/index.test.ts +0 -93
- package/src/transforms/__tests__/predicates.test.ts +0 -193
- package/src/transforms/__tests__/timeunit.test.ts +0 -129
- package/src/transforms/__tests__/window.test.ts +0 -286
- package/src/transforms/aggregate.ts +0 -131
- package/src/transforms/bin.ts +0 -87
- package/src/transforms/calculate.ts +0 -60
- package/src/transforms/conditional.ts +0 -46
- package/src/transforms/filter.ts +0 -122
- package/src/transforms/fold.ts +0 -49
- package/src/transforms/index.ts +0 -60
- package/src/transforms/predicates.ts +0 -123
- package/src/transforms/timeunit.ts +0 -88
- package/src/transforms/window.ts +0 -185
|
@@ -1,162 +0,0 @@
|
|
|
1
|
-
/** Default font size for annotation labels. */
|
|
2
|
-
export const DEFAULT_ANNOTATION_FONT_SIZE = 13;
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Default font weight for annotation labels. Regular, not bold: emphasis comes
|
|
6
|
-
* from inline `**bold**` spans, per the NYT/Datawrapper voice.
|
|
7
|
-
*/
|
|
8
|
-
export const DEFAULT_ANNOTATION_FONT_WEIGHT = 400;
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Weight the primary text takes when a subtitle is present and the author set no
|
|
12
|
-
* explicit `fontWeight` — the "Feb. 25 / 2015 maximum" lede + context stack.
|
|
13
|
-
*/
|
|
14
|
-
export const LEDE_FONT_WEIGHT = 700;
|
|
15
|
-
|
|
16
|
-
/** Subtitles are always regular, so they never inherit a lede weight. */
|
|
17
|
-
export const SUBTITLE_FONT_WEIGHT = 400;
|
|
18
|
-
|
|
19
|
-
/** Font stack used when the theme font isn't threaded through (SSR/legacy paths). */
|
|
20
|
-
export const FALLBACK_FONT_FAMILY = 'Inter, system-ui, sans-serif';
|
|
21
|
-
|
|
22
|
-
/** Default line height multiplier for annotation text. */
|
|
23
|
-
export const DEFAULT_LINE_HEIGHT = 1.3;
|
|
24
|
-
|
|
25
|
-
/** Default fill color for range annotations. */
|
|
26
|
-
export const DEFAULT_RANGE_FILL = '#f0c040';
|
|
27
|
-
|
|
28
|
-
/** Default opacity for range annotations. */
|
|
29
|
-
export const DEFAULT_RANGE_OPACITY = 0.15;
|
|
30
|
-
|
|
31
|
-
/** Default dash pattern for reference lines. */
|
|
32
|
-
export const DEFAULT_REFLINE_DASH = '4 3';
|
|
33
|
-
|
|
34
|
-
// Theme-aware defaults for text and stroke colors
|
|
35
|
-
export const LIGHT_TEXT_FILL = '#333333';
|
|
36
|
-
export const DARK_TEXT_FILL = '#d1d5db';
|
|
37
|
-
export const LIGHT_REFLINE_STROKE = '#888888';
|
|
38
|
-
export const DARK_REFLINE_STROKE = '#9ca3af';
|
|
39
|
-
|
|
40
|
-
// Quiet connector voice: gray hairline leaders (non-arrowed straight, drop-line).
|
|
41
|
-
// Arrowed connectors take the label's text ink instead, so the emphasis callout
|
|
42
|
-
// reads as one gesture with the words.
|
|
43
|
-
export const LIGHT_CONNECTOR_STROKE = '#8f8f8f';
|
|
44
|
-
export const DARK_CONNECTOR_STROKE = '#9aa0a6';
|
|
45
|
-
|
|
46
|
-
// Muted text fill for annotation subtitles (~60% perceived contrast vs primary)
|
|
47
|
-
export const LIGHT_MUTED_TEXT_FILL = '#6b7280';
|
|
48
|
-
export const DARK_MUTED_TEXT_FILL = '#9ca3af';
|
|
49
|
-
|
|
50
|
-
// Background fills used as the default "open ring" dot interior
|
|
51
|
-
export const LIGHT_DOT_FILL = '#ffffff';
|
|
52
|
-
export const DARK_DOT_FILL = '#0a0a0a';
|
|
53
|
-
|
|
54
|
-
// Surface fills for the text-annotation background plate (the mask that hides
|
|
55
|
-
// chart lines behind label text). `background: true` resolves to these, so
|
|
56
|
-
// authors don't hardcode '#ffffff' and get light-gray-on-white in dark mode.
|
|
57
|
-
export const LIGHT_LABEL_BACKGROUND = '#ffffff';
|
|
58
|
-
export const DARK_LABEL_BACKGROUND = '#0a0a0a';
|
|
59
|
-
|
|
60
|
-
/** Default annotation dot radius in pixels. */
|
|
61
|
-
export const DEFAULT_DOT_RADIUS = 4;
|
|
62
|
-
|
|
63
|
-
/** Default annotation dot stroke width in pixels. */
|
|
64
|
-
export const DEFAULT_DOT_STROKE_WIDTH = 1.5;
|
|
65
|
-
|
|
66
|
-
/** Vertical gap (px) between the primary annotation text and its subtitle. */
|
|
67
|
-
export const SUBTITLE_GAP = 2;
|
|
68
|
-
|
|
69
|
-
/** Subtitle font size multiplier (relative to primary annotation font size). */
|
|
70
|
-
export const SUBTITLE_FONT_SIZE_RATIO = 0.85;
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* The subtitle's resolved font size. Rounded, and shared by every caller: the
|
|
74
|
-
* auto-placement pass measures the subtitle to score candidate bounds while the
|
|
75
|
-
* resolver sets the size that actually renders. If one rounds and the other
|
|
76
|
-
* doesn't (13 * 0.85 = 11.05 vs 11), the scored bounds are a lie and the drift
|
|
77
|
-
* flows into collisions and thinning.
|
|
78
|
-
*/
|
|
79
|
-
export function subtitleFontSize(primaryFontSize: number): number {
|
|
80
|
-
return Math.round(primaryFontSize * SUBTITLE_FONT_SIZE_RATIO);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* The subtitle's baseline, given the label's. Same contract as `subtitleFontSize`,
|
|
85
|
-
* and shared for the same reason: the auto-placement pass SCORES a candidate
|
|
86
|
-
* subtitle position and the resolver STAMPS the one that renders. Two copies of
|
|
87
|
-
* this sum is two chances to disagree -- and they did, one spelling the gap
|
|
88
|
-
* `SUBTITLE_GAP` and the other a bare `+ 2`.
|
|
89
|
-
*
|
|
90
|
-
* `primaryLineHeightPx` is the line's height in PIXELS, not the ratio -- so a caller
|
|
91
|
-
* holding a ratio multiplies by the font size on the way in. Taking the two factors
|
|
92
|
-
* separately meant the one caller that already had the product had to pass a magic
|
|
93
|
-
* `1` for the font size, and a signature you have to outsmart is a signature that
|
|
94
|
-
* will eventually be outsmarted wrong.
|
|
95
|
-
*/
|
|
96
|
-
export function subtitleBaselineY(
|
|
97
|
-
labelY: number,
|
|
98
|
-
primaryLineHeightPx: number,
|
|
99
|
-
primaryLineCount: number,
|
|
100
|
-
): number {
|
|
101
|
-
return labelY + primaryLineHeightPx * primaryLineCount + SUBTITLE_GAP;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/** Gap (px) between the label bounding box and the start of its connector. */
|
|
105
|
-
export const CONNECTOR_STANDOFF = 6;
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Connectors shorter than this are suppressed: a nub between a label and the
|
|
109
|
-
* marker it's already touching reads as noise, not as a leader.
|
|
110
|
-
*
|
|
111
|
-
* Keep this well under the standoff + marker-pullback overhead (~18px), which
|
|
112
|
-
* is spent before the line is drawn at all. Setting it near or above that
|
|
113
|
-
* overhead makes the default annotation structurally unable to draw a
|
|
114
|
-
* connector, however far the author offsets the label.
|
|
115
|
-
*/
|
|
116
|
-
export const MIN_CONNECTOR_LENGTH = 8;
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Arrowhead length along the tangent. The renderer pulls the connector's line end
|
|
120
|
-
* back by exactly this much so the stroke stops at the open V instead of poking
|
|
121
|
-
* through its tip — a hand-copied duplicate on the vanilla side would silently
|
|
122
|
-
* drift the next time this changes.
|
|
123
|
-
*/
|
|
124
|
-
export const ARROWHEAD_LENGTH = 7;
|
|
125
|
-
|
|
126
|
-
/** Arrowhead half-width perpendicular to the tangent. */
|
|
127
|
-
export const ARROWHEAD_HALF_WIDTH = 3.5;
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* Is a connector of this length worth drawing?
|
|
131
|
-
*
|
|
132
|
-
* An arrowed connector doesn't stroke its full length: the renderer stops the line
|
|
133
|
-
* `ARROWHEAD_LENGTH` short and spends that budget on the head. So a bare
|
|
134
|
-
* `length >= MIN_CONNECTOR_LENGTH` test is arrow-blind — an 8.1px arrowed
|
|
135
|
-
* connector clears it, and then ships a 1.1px stub with an arrowhead stuck on the
|
|
136
|
-
* end. The minimum has to be measured against the part that actually gets stroked,
|
|
137
|
-
* which means the gate has to know about the head.
|
|
138
|
-
*
|
|
139
|
-
* Both suppression sites (`resolveTextAnnotation`, `refreshConnector`) call this
|
|
140
|
-
* instead of comparing against the constant themselves.
|
|
141
|
-
*/
|
|
142
|
-
export function connectorIsDrawable(length: number, arrow: boolean | undefined): boolean {
|
|
143
|
-
return length - (arrow ? ARROWHEAD_LENGTH : 0) >= MIN_CONNECTOR_LENGTH;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* Default label setback from the data point when using anchor directions.
|
|
148
|
-
*
|
|
149
|
-
* Sized so a bare `{ type: 'text' }` annotation lands clear of its marker and
|
|
150
|
-
* draws a real leader with no offset authoring — a minimal spec should render
|
|
151
|
-
* publication-ready. Authors tighten it with `offset`.
|
|
152
|
-
*/
|
|
153
|
-
export const ANCHOR_OFFSET = 28;
|
|
154
|
-
|
|
155
|
-
/** Vertical gap between the top of a drop-line and the top of its label box. */
|
|
156
|
-
export const DROP_LINE_TOP_GAP = 4;
|
|
157
|
-
|
|
158
|
-
/** Padding between annotation and obstacle when nudging. */
|
|
159
|
-
export const NUDGE_PADDING = 6;
|
|
160
|
-
|
|
161
|
-
/** Small inset margin so labels don't touch the SVG edge. */
|
|
162
|
-
export const CLAMP_MARGIN = 4;
|
|
@@ -1,376 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Geometry utilities for annotation text bounds, connector origins, and offsets.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import type {
|
|
6
|
-
AnnotationAnchor,
|
|
7
|
-
AnnotationOffset,
|
|
8
|
-
Rect,
|
|
9
|
-
ResolvedLabel,
|
|
10
|
-
} from '@opendata-ai/openchart-core';
|
|
11
|
-
import { estimateTextWidth } from '@opendata-ai/openchart-core';
|
|
12
|
-
import {
|
|
13
|
-
ANCHOR_OFFSET,
|
|
14
|
-
ARROWHEAD_HALF_WIDTH,
|
|
15
|
-
ARROWHEAD_LENGTH,
|
|
16
|
-
CONNECTOR_STANDOFF,
|
|
17
|
-
connectorIsDrawable,
|
|
18
|
-
DEFAULT_ANNOTATION_FONT_SIZE,
|
|
19
|
-
DEFAULT_ANNOTATION_FONT_WEIGHT,
|
|
20
|
-
DEFAULT_LINE_HEIGHT,
|
|
21
|
-
DROP_LINE_TOP_GAP,
|
|
22
|
-
} from './constants';
|
|
23
|
-
import { BOLD_SPAN_FONT_WEIGHT, parseAnnotationSpans } from './rich-text';
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Measure one run of text at a given size and weight.
|
|
27
|
-
*
|
|
28
|
-
* Deliberately takes no `fontFamily`: neither implementation could use one.
|
|
29
|
-
* `heuristicMeasure` below is family-blind, and the real canvas measurer gets its
|
|
30
|
-
* family from the `createMeasureText(fontFamily)` closure, not per call -- so a
|
|
31
|
-
* family passed here would be silently dropped while implying the opposite.
|
|
32
|
-
* `style.fontFamily` on a *resolved label* is a different thing: that one is real
|
|
33
|
-
* and gets rendered.
|
|
34
|
-
*/
|
|
35
|
-
export type AnnotationMeasureTextFn = (
|
|
36
|
-
text: string,
|
|
37
|
-
font: { fontSize: number; fontWeight: number },
|
|
38
|
-
) => number;
|
|
39
|
-
|
|
40
|
-
export const heuristicMeasure: AnnotationMeasureTextFn = (text, { fontSize, fontWeight }) =>
|
|
41
|
-
estimateTextWidth(text, fontSize, fontWeight);
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Width of one line of annotation text, `**bold**` spans included: each span is
|
|
45
|
-
* measured at its own weight and the widths are summed. Measuring the raw string
|
|
46
|
-
* would count the `**` markers and use one weight for the whole line, so bounds,
|
|
47
|
-
* collisions, placement, and connector exits would all be wrong for rich text.
|
|
48
|
-
*/
|
|
49
|
-
export function measureRichLine(
|
|
50
|
-
line: string,
|
|
51
|
-
style: { fontSize: number; fontWeight: number },
|
|
52
|
-
measure: AnnotationMeasureTextFn,
|
|
53
|
-
): number {
|
|
54
|
-
const spans = parseAnnotationSpans(line);
|
|
55
|
-
let width = 0;
|
|
56
|
-
for (const span of spans) {
|
|
57
|
-
width += measure(span.text, {
|
|
58
|
-
fontSize: style.fontSize,
|
|
59
|
-
fontWeight: span.bold ? BOLD_SPAN_FONT_WEIGHT : style.fontWeight,
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
return width;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Compute the bounding box of a text block, aware of textAnchor and multi-line layout.
|
|
67
|
-
* labelY is the first-line baseline.
|
|
68
|
-
*/
|
|
69
|
-
export function computeTextBlockBounds(
|
|
70
|
-
labelX: number,
|
|
71
|
-
labelY: number,
|
|
72
|
-
text: string,
|
|
73
|
-
style: {
|
|
74
|
-
fontSize: number;
|
|
75
|
-
fontWeight: number;
|
|
76
|
-
lineHeight: number;
|
|
77
|
-
textAnchor?: 'start' | 'middle' | 'end';
|
|
78
|
-
fontFamily?: string;
|
|
79
|
-
},
|
|
80
|
-
measure: AnnotationMeasureTextFn = heuristicMeasure,
|
|
81
|
-
): Rect {
|
|
82
|
-
const lines = text.split('\n');
|
|
83
|
-
const maxWidth = Math.max(...lines.map((line) => measureRichLine(line, style, measure)));
|
|
84
|
-
const height =
|
|
85
|
-
style.fontSize + (lines.length - 1) * style.fontSize * style.lineHeight + style.fontSize * 0.3;
|
|
86
|
-
|
|
87
|
-
let x: number;
|
|
88
|
-
if (style.textAnchor === 'middle') {
|
|
89
|
-
x = labelX - maxWidth / 2;
|
|
90
|
-
} else if (style.textAnchor === 'end') {
|
|
91
|
-
x = labelX - maxWidth;
|
|
92
|
-
} else {
|
|
93
|
-
x = labelX;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
return {
|
|
97
|
-
x,
|
|
98
|
-
y: labelY - style.fontSize,
|
|
99
|
-
width: maxWidth,
|
|
100
|
-
height,
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
export function unionRects(a: Rect, b: Rect): Rect {
|
|
105
|
-
const x = Math.min(a.x, b.x);
|
|
106
|
-
const y = Math.min(a.y, b.y);
|
|
107
|
-
return {
|
|
108
|
-
x,
|
|
109
|
-
y,
|
|
110
|
-
width: Math.max(a.x + a.width, b.x + b.width) - x,
|
|
111
|
-
height: Math.max(a.y + a.height, b.y + b.height) - y,
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* Apply anchor direction to compute label offset from data point.
|
|
117
|
-
* Returns { dx, dy } pixel offsets.
|
|
118
|
-
*/
|
|
119
|
-
export function computeAnchorOffset(
|
|
120
|
-
anchor: AnnotationAnchor | undefined,
|
|
121
|
-
_px: number,
|
|
122
|
-
py: number,
|
|
123
|
-
chartArea: Rect,
|
|
124
|
-
): { dx: number; dy: number } {
|
|
125
|
-
if (!anchor || anchor === 'auto') {
|
|
126
|
-
// Auto: place above if in the lower half, below if upper half
|
|
127
|
-
const isUpperHalf = py < chartArea.y + chartArea.height / 2;
|
|
128
|
-
return isUpperHalf
|
|
129
|
-
? { dx: ANCHOR_OFFSET, dy: ANCHOR_OFFSET } // below-right
|
|
130
|
-
: { dx: ANCHOR_OFFSET, dy: -ANCHOR_OFFSET }; // above-right
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
switch (anchor) {
|
|
134
|
-
case 'top':
|
|
135
|
-
return { dx: 0, dy: -ANCHOR_OFFSET };
|
|
136
|
-
case 'bottom':
|
|
137
|
-
return { dx: 0, dy: ANCHOR_OFFSET };
|
|
138
|
-
case 'left':
|
|
139
|
-
return { dx: -ANCHOR_OFFSET, dy: 0 };
|
|
140
|
-
case 'right':
|
|
141
|
-
return { dx: ANCHOR_OFFSET, dy: 0 };
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/** Apply user offset on top of computed anchor offset. */
|
|
146
|
-
export function applyOffset(
|
|
147
|
-
base: { dx: number; dy: number },
|
|
148
|
-
offset: AnnotationOffset | undefined,
|
|
149
|
-
): { dx: number; dy: number } {
|
|
150
|
-
if (!offset) return base;
|
|
151
|
-
return {
|
|
152
|
-
dx: base.dx + (offset.dx ?? 0),
|
|
153
|
-
dy: base.dy + (offset.dy ?? 0),
|
|
154
|
-
};
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/** Inflation applied to the label box before intersecting the connector ray. */
|
|
158
|
-
const BOX_INFLATE = 2;
|
|
159
|
-
|
|
160
|
-
/**
|
|
161
|
-
* Compute where a connector leaves the annotation's text block on its way to the
|
|
162
|
-
* target. Casts a ray from the box center toward the target, intersects the
|
|
163
|
-
* (slightly inflated) box, then advances `standoff` px along the ray so the line
|
|
164
|
-
* never touches the glyphs.
|
|
165
|
-
*
|
|
166
|
-
* Returns `null` when the target sits inside the inflated box (a connector would
|
|
167
|
-
* cross its own text) or when the standoff would overshoot the target.
|
|
168
|
-
*/
|
|
169
|
-
export function connectorExit(
|
|
170
|
-
bounds: Rect,
|
|
171
|
-
targetX: number,
|
|
172
|
-
targetY: number,
|
|
173
|
-
standoff = CONNECTOR_STANDOFF,
|
|
174
|
-
): { x: number; y: number; exit: 'horizontal' | 'vertical' } | null {
|
|
175
|
-
const box = {
|
|
176
|
-
x: bounds.x - BOX_INFLATE,
|
|
177
|
-
y: bounds.y - BOX_INFLATE,
|
|
178
|
-
width: bounds.width + BOX_INFLATE * 2,
|
|
179
|
-
height: bounds.height + BOX_INFLATE * 2,
|
|
180
|
-
};
|
|
181
|
-
const cx = box.x + box.width / 2;
|
|
182
|
-
const cy = box.y + box.height / 2;
|
|
183
|
-
const halfW = box.width / 2;
|
|
184
|
-
const halfH = box.height / 2;
|
|
185
|
-
|
|
186
|
-
const dx = targetX - cx;
|
|
187
|
-
const dy = targetY - cy;
|
|
188
|
-
|
|
189
|
-
// Target inside the inflated box: any connector would cross the text.
|
|
190
|
-
if (Math.abs(dx) <= halfW && Math.abs(dy) <= halfH) return null;
|
|
191
|
-
if (dx === 0 && dy === 0) return null;
|
|
192
|
-
|
|
193
|
-
// Parametric ray: center + t * (dx, dy). Find the smallest t that lands on an
|
|
194
|
-
// edge of the box, and which axis that edge belongs to.
|
|
195
|
-
const tx = halfW > 0 && dx !== 0 ? halfW / Math.abs(dx) : Number.POSITIVE_INFINITY;
|
|
196
|
-
const ty = halfH > 0 && dy !== 0 ? halfH / Math.abs(dy) : Number.POSITIVE_INFINITY;
|
|
197
|
-
const t = Math.min(tx, ty);
|
|
198
|
-
if (!Number.isFinite(t)) return null;
|
|
199
|
-
|
|
200
|
-
const exit: 'horizontal' | 'vertical' = tx <= ty ? 'horizontal' : 'vertical';
|
|
201
|
-
|
|
202
|
-
const edgeX = cx + dx * t;
|
|
203
|
-
const edgeY = cy + dy * t;
|
|
204
|
-
|
|
205
|
-
// Advance along the ray by the standoff distance.
|
|
206
|
-
const rayLen = Math.sqrt(dx * dx + dy * dy);
|
|
207
|
-
const ux = dx / rayLen;
|
|
208
|
-
const uy = dy / rayLen;
|
|
209
|
-
|
|
210
|
-
// Overshoot guard: the standoff must not push the origin past the target.
|
|
211
|
-
// Measure edge-to-target in pixels rather than scaling the ray by (1 - t):
|
|
212
|
-
// t is a ratio of box extents, so that form understates the clearance when the
|
|
213
|
-
// ray leaves through the long side of a wide label, and killed connectors that
|
|
214
|
-
// had ample room. Whether the line is long enough to be worth drawing is
|
|
215
|
-
// decided downstream against MIN_CONNECTOR_LENGTH, once the marker pullback is
|
|
216
|
-
// known — this guard only rejects a standoff that would overshoot the target.
|
|
217
|
-
const edgeToTarget = Math.hypot(targetX - edgeX, targetY - edgeY);
|
|
218
|
-
if (standoff >= edgeToTarget) return null;
|
|
219
|
-
|
|
220
|
-
const x = edgeX + ux * standoff;
|
|
221
|
-
const y = edgeY + uy * standoff;
|
|
222
|
-
|
|
223
|
-
return { x, y, exit };
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
/** Estimate the bounding box of an annotation label using its resolved style. */
|
|
227
|
-
export function estimateLabelBounds(
|
|
228
|
-
label: ResolvedLabel,
|
|
229
|
-
measure: AnnotationMeasureTextFn = heuristicMeasure,
|
|
230
|
-
): Rect {
|
|
231
|
-
const fontSize = label.style.fontSize ?? DEFAULT_ANNOTATION_FONT_SIZE;
|
|
232
|
-
const fontWeight = label.style.fontWeight ?? DEFAULT_ANNOTATION_FONT_WEIGHT;
|
|
233
|
-
return computeTextBlockBounds(
|
|
234
|
-
label.x,
|
|
235
|
-
label.y,
|
|
236
|
-
label.text,
|
|
237
|
-
{
|
|
238
|
-
fontSize,
|
|
239
|
-
fontWeight,
|
|
240
|
-
lineHeight: label.style.lineHeight ?? DEFAULT_LINE_HEIGHT,
|
|
241
|
-
textAnchor: label.style.textAnchor ?? 'start',
|
|
242
|
-
fontFamily: label.style.fontFamily,
|
|
243
|
-
},
|
|
244
|
-
measure,
|
|
245
|
-
);
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
/** Arrowhead triangle points: tip + two base corners. */
|
|
249
|
-
export interface ArrowheadPoints {
|
|
250
|
-
tip: { x: number; y: number };
|
|
251
|
-
baseLeft: { x: number; y: number };
|
|
252
|
-
baseRight: { x: number; y: number };
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
// Both live in `constants.ts` (the min-length gate has to reason about the
|
|
256
|
-
// arrowhead's stroke budget, and constants.ts can't import from here without a
|
|
257
|
-
// cycle). Re-exported so the renderer's import path doesn't move.
|
|
258
|
-
export { ARROWHEAD_HALF_WIDTH, ARROWHEAD_LENGTH };
|
|
259
|
-
|
|
260
|
-
/**
|
|
261
|
-
* Compute arrowhead triangle geometry at a connector endpoint.
|
|
262
|
-
* Returns the tip (at the endpoint) and two base corners perpendicular to the
|
|
263
|
-
* tangent direction.
|
|
264
|
-
*
|
|
265
|
-
* @param tipX - X coordinate of the arrowhead tip
|
|
266
|
-
* @param tipY - Y coordinate of the arrowhead tip
|
|
267
|
-
* @param tangentX - X component of the tangent direction (toward the tip)
|
|
268
|
-
* @param tangentY - Y component of the tangent direction (toward the tip)
|
|
269
|
-
* @param length - Arrow length along the tangent (default ARROWHEAD_LENGTH)
|
|
270
|
-
* @param halfWidth - Arrow half-width perpendicular to tangent (default ARROWHEAD_HALF_WIDTH)
|
|
271
|
-
*/
|
|
272
|
-
export function computeArrowheadPoints(
|
|
273
|
-
tipX: number,
|
|
274
|
-
tipY: number,
|
|
275
|
-
tangentX: number,
|
|
276
|
-
tangentY: number,
|
|
277
|
-
length = ARROWHEAD_LENGTH,
|
|
278
|
-
halfWidth = ARROWHEAD_HALF_WIDTH,
|
|
279
|
-
): ArrowheadPoints {
|
|
280
|
-
const tLen = Math.sqrt(tangentX * tangentX + tangentY * tangentY) || 1;
|
|
281
|
-
const ux = tangentX / tLen;
|
|
282
|
-
const uy = tangentY / tLen;
|
|
283
|
-
|
|
284
|
-
const baseX = tipX - ux * length;
|
|
285
|
-
const baseY = tipY - uy * length;
|
|
286
|
-
|
|
287
|
-
const px = -uy;
|
|
288
|
-
const py = ux;
|
|
289
|
-
|
|
290
|
-
return {
|
|
291
|
-
tip: { x: tipX, y: tipY },
|
|
292
|
-
baseLeft: { x: baseX + px * halfWidth, y: baseY + py * halfWidth },
|
|
293
|
-
baseRight: { x: baseX - px * halfWidth, y: baseY - py * halfWidth },
|
|
294
|
-
};
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
/**
|
|
298
|
-
* Distance the connector's data-point end is pulled back so the line stops short
|
|
299
|
-
* of the endpoint marker instead of piercing it.
|
|
300
|
-
*/
|
|
301
|
-
export function connectorPullbackGap(markerRadius: number, arrow: boolean): number {
|
|
302
|
-
return arrow ? Math.max(markerRadius + 2, 2) : Math.max(markerRadius + 3, 4);
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
/**
|
|
306
|
-
* Rebuild a connector's geometry against a (possibly moved) annotation bounds.
|
|
307
|
-
*
|
|
308
|
-
* `from` comes from the ray-box exit; `to` is recomputed by pulling back from
|
|
309
|
-
* `connector.endpoint` — never from the already-pulled-back `to`, which would
|
|
310
|
-
* shrink the line a little more on every pass.
|
|
311
|
-
*
|
|
312
|
-
* Returns `undefined` when the connector should be suppressed: the target sits
|
|
313
|
-
* inside the text block, or the resulting line is shorter than
|
|
314
|
-
* `MIN_CONNECTOR_LENGTH`.
|
|
315
|
-
*/
|
|
316
|
-
export function refreshConnector(
|
|
317
|
-
connector: NonNullable<ResolvedLabel['connector']>,
|
|
318
|
-
bounds: Rect,
|
|
319
|
-
markerRadius: number,
|
|
320
|
-
): ResolvedLabel['connector'] {
|
|
321
|
-
// `endpoint` is the un-pulled-back data point, and the only valid basis for a
|
|
322
|
-
// rebuild: pulling back from `to` (already pulled back) shrinks the line a
|
|
323
|
-
// little more on every pass. Every resolver sets it, but the field is optional
|
|
324
|
-
// on the public `ResolvedLabel` type, so a layout from another producer could
|
|
325
|
-
// omit it — bail rather than silently compound the error.
|
|
326
|
-
if (!connector.endpoint) return undefined;
|
|
327
|
-
const endpoint = connector.endpoint;
|
|
328
|
-
|
|
329
|
-
// A drop-line is not a leader: it's a vertical rule pinned to the data point's
|
|
330
|
-
// x, running from just above the label box down to the marker. Rebuilding it
|
|
331
|
-
// from a ray-box exit tilts it into a diagonal (which the renderer draws with
|
|
332
|
-
// shape-rendering: crispEdges, assuming axis alignment), and the
|
|
333
|
-
// MIN_CONNECTOR_LENGTH rule below would delete it outright once the label sits
|
|
334
|
-
// near its point.
|
|
335
|
-
//
|
|
336
|
-
// This mirrors `resolveDropLineAnnotation`'s formula exactly — `from` above the
|
|
337
|
-
// label top, `to` pulled back from the point — so a nudged drop-line keeps the
|
|
338
|
-
// same geometry it resolved with, just at the label's new position.
|
|
339
|
-
if (connector.style === 'drop-line') {
|
|
340
|
-
const gap = connectorPullbackGap(markerRadius, false);
|
|
341
|
-
return {
|
|
342
|
-
...connector,
|
|
343
|
-
from: { x: endpoint.x, y: bounds.y - DROP_LINE_TOP_GAP },
|
|
344
|
-
to: { x: endpoint.x, y: endpoint.y - gap },
|
|
345
|
-
exit: 'vertical',
|
|
346
|
-
};
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
const exit = connectorExit(bounds, endpoint.x, endpoint.y);
|
|
350
|
-
if (!exit) return undefined;
|
|
351
|
-
|
|
352
|
-
// Re-apply the author's from-end nudge BEFORE the pullback, so the pullback
|
|
353
|
-
// direction is measured from the real origin -- exactly as
|
|
354
|
-
// `resolveTextAnnotation` does on first resolve.
|
|
355
|
-
const from = {
|
|
356
|
-
x: exit.x + (connector.fromOffset?.dx ?? 0),
|
|
357
|
-
y: exit.y + (connector.fromOffset?.dy ?? 0),
|
|
358
|
-
};
|
|
359
|
-
|
|
360
|
-
const gap = connectorPullbackGap(markerRadius, connector.arrow);
|
|
361
|
-
const cdx = endpoint.x - from.x;
|
|
362
|
-
const cdy = endpoint.y - from.y;
|
|
363
|
-
const dist = Math.sqrt(cdx * cdx + cdy * cdy);
|
|
364
|
-
const to =
|
|
365
|
-
dist > 0
|
|
366
|
-
? { x: endpoint.x - (cdx / dist) * gap, y: endpoint.y - (cdy / dist) * gap }
|
|
367
|
-
: { ...endpoint };
|
|
368
|
-
|
|
369
|
-
// Min-length check runs AFTER the pullback: a near-degenerate line can flip
|
|
370
|
-
// direction once its end is pulled back, and checking first would miss it.
|
|
371
|
-
const lx = to.x - from.x;
|
|
372
|
-
const ly = to.y - from.y;
|
|
373
|
-
if (!connectorIsDrawable(Math.sqrt(lx * lx + ly * ly), connector.arrow)) return undefined;
|
|
374
|
-
|
|
375
|
-
return { ...connector, from, to, exit: exit.exit };
|
|
376
|
-
}
|
package/src/annotations/move.ts
DELETED
|
@@ -1,135 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The single owner of "where a resolved annotation sits".
|
|
3
|
-
*
|
|
4
|
-
* WHY THIS MODULE EXISTS
|
|
5
|
-
*
|
|
6
|
-
* An annotation's position is not one number, it's five that have to agree: the
|
|
7
|
-
* label origin, the subtitle origin (absolute, not relative), the label-only
|
|
8
|
-
* bounds, the label-union-subtitle block bounds, and the connector endpoints. The
|
|
9
|
-
* resolver establishes that agreement -- and then FOUR later passes re-position
|
|
10
|
-
* the block (obstacle nudge, mutual-collision nudge, clamp-to-bounds, and the
|
|
11
|
-
* auto-placement search). Every one of them has to re-establish all five.
|
|
12
|
-
*
|
|
13
|
-
* When two of those passes each did their own arithmetic, the two copies drifted:
|
|
14
|
-
* one spelled the subtitle gap `SUBTITLE_GAP`, the other spelled it `+ 2`, and a
|
|
15
|
-
* subtitled auto-placed callout rendered its second line 3px off from where the
|
|
16
|
-
* search had scored it. That is the bug this module exists to make unrepresentable.
|
|
17
|
-
*
|
|
18
|
-
* So: nothing outside this file writes annotation geometry. `moveAnnotationTo`
|
|
19
|
-
* (absolute) and `moveAnnotationBy` (delta) are the only doors, and they always
|
|
20
|
-
* leave all five in agreement.
|
|
21
|
-
*/
|
|
22
|
-
|
|
23
|
-
import type { Rect, ResolvedAnnotation, ResolvedLabel } from '@opendata-ai/openchart-core';
|
|
24
|
-
import {
|
|
25
|
-
DEFAULT_ANNOTATION_FONT_SIZE,
|
|
26
|
-
DEFAULT_LINE_HEIGHT,
|
|
27
|
-
SUBTITLE_FONT_WEIGHT,
|
|
28
|
-
subtitleFontSize,
|
|
29
|
-
} from './constants';
|
|
30
|
-
import {
|
|
31
|
-
type AnnotationMeasureTextFn,
|
|
32
|
-
computeTextBlockBounds,
|
|
33
|
-
estimateLabelBounds,
|
|
34
|
-
refreshConnector,
|
|
35
|
-
unionRects,
|
|
36
|
-
} from './geometry';
|
|
37
|
-
import { markerClearance } from './resolve-text';
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* The full block an annotation occupies: label text plus its subtitle.
|
|
41
|
-
*
|
|
42
|
-
* This is the box other annotations must avoid, and the box the connector exits.
|
|
43
|
-
* It is NOT `label.bounds` -- that one is label-only by contract, because the
|
|
44
|
-
* renderer sizes the `background` plate from it and a subtitled annotation must
|
|
45
|
-
* not get a plate sized to both lines.
|
|
46
|
-
*/
|
|
47
|
-
export function annotationBlockBounds(
|
|
48
|
-
annotation: ResolvedAnnotation,
|
|
49
|
-
label: ResolvedLabel,
|
|
50
|
-
measure: AnnotationMeasureTextFn,
|
|
51
|
-
): Rect {
|
|
52
|
-
const labelBounds = estimateLabelBounds(label, measure);
|
|
53
|
-
const sub = annotation.subtitle;
|
|
54
|
-
if (!sub) return labelBounds;
|
|
55
|
-
|
|
56
|
-
const subBounds = computeTextBlockBounds(
|
|
57
|
-
sub.x,
|
|
58
|
-
sub.y,
|
|
59
|
-
sub.text,
|
|
60
|
-
{
|
|
61
|
-
// Every resolver stamps all three, so these fallbacks are unreachable today.
|
|
62
|
-
// They still come from the constants: a hand-rolled `?? 10` here is a second
|
|
63
|
-
// definition of the subtitle's type, free to drift from the real one.
|
|
64
|
-
fontSize: sub.style.fontSize ?? subtitleFontSize(DEFAULT_ANNOTATION_FONT_SIZE),
|
|
65
|
-
fontWeight: Number(sub.style.fontWeight) || SUBTITLE_FONT_WEIGHT,
|
|
66
|
-
lineHeight: sub.style.lineHeight ?? DEFAULT_LINE_HEIGHT,
|
|
67
|
-
textAnchor: sub.style.textAnchor ?? 'start',
|
|
68
|
-
},
|
|
69
|
-
measure,
|
|
70
|
-
);
|
|
71
|
-
return unionRects(labelBounds, subBounds);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Put a resolved annotation's block at an absolute origin, keeping every piece of
|
|
76
|
-
* its geometry in agreement.
|
|
77
|
-
*
|
|
78
|
-
* The subtitle is TRANSLATED with the label rather than re-derived from a font-metric
|
|
79
|
-
* sum: the resolver already placed it correctly relative to the label, and a second
|
|
80
|
-
* derivation here is a second chance to disagree with the first.
|
|
81
|
-
*
|
|
82
|
-
* `textAnchor` is optional because only the auto-placement search chooses a side;
|
|
83
|
-
* the nudge passes slide a block that has already picked one.
|
|
84
|
-
*/
|
|
85
|
-
export function moveAnnotationTo(
|
|
86
|
-
annotation: ResolvedAnnotation,
|
|
87
|
-
label: ResolvedLabel,
|
|
88
|
-
x: number,
|
|
89
|
-
y: number,
|
|
90
|
-
measure: AnnotationMeasureTextFn,
|
|
91
|
-
textAnchor?: 'start' | 'middle' | 'end',
|
|
92
|
-
): void {
|
|
93
|
-
moveAnnotationBy(annotation, label, x - label.x, y - label.y, measure, textAnchor);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Slide a resolved annotation's block by (dx, dy), keeping every piece of its
|
|
98
|
-
* geometry in agreement. See `moveAnnotationTo`.
|
|
99
|
-
*/
|
|
100
|
-
export function moveAnnotationBy(
|
|
101
|
-
annotation: ResolvedAnnotation,
|
|
102
|
-
label: ResolvedLabel,
|
|
103
|
-
dx: number,
|
|
104
|
-
dy: number,
|
|
105
|
-
measure: AnnotationMeasureTextFn,
|
|
106
|
-
textAnchor?: 'start' | 'middle' | 'end',
|
|
107
|
-
): void {
|
|
108
|
-
const movedLabel: ResolvedLabel = { ...label, x: label.x + dx, y: label.y + dy };
|
|
109
|
-
if (textAnchor) {
|
|
110
|
-
movedLabel.style = { ...movedLabel.style, textAnchor };
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
if (annotation.subtitle) {
|
|
114
|
-
annotation.subtitle = {
|
|
115
|
-
...annotation.subtitle,
|
|
116
|
-
x: annotation.subtitle.x + dx,
|
|
117
|
-
y: annotation.subtitle.y + dy,
|
|
118
|
-
...(textAnchor ? { style: { ...annotation.subtitle.style, textAnchor } } : {}),
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
movedLabel.bounds = estimateLabelBounds(movedLabel, measure);
|
|
123
|
-
const blockBounds = annotationBlockBounds(annotation, movedLabel, measure);
|
|
124
|
-
|
|
125
|
-
if (movedLabel.connector) {
|
|
126
|
-
movedLabel.connector = refreshConnector(
|
|
127
|
-
movedLabel.connector,
|
|
128
|
-
blockBounds,
|
|
129
|
-
markerClearance(annotation.dot),
|
|
130
|
-
);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
annotation.label = movedLabel;
|
|
134
|
-
annotation.bounds = blockBounds;
|
|
135
|
-
}
|