@opendata-ai/openchart-engine 7.11.0 → 8.0.0-rc.2
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 +123 -14
- package/dist/index.js +16166 -10027
- package/dist/index.js.map +1 -1
- package/package.json +11 -4
- package/src/__tests__/__snapshots__/compile-snapshot.test.ts.snap +170 -56
- package/src/__tests__/annotation-behavior.test.ts +721 -0
- package/src/__tests__/axes.test.ts +50 -3
- package/src/__tests__/compile-chart.test.ts +30 -0
- package/src/__tests__/compile-footnotes.test.ts +277 -0
- package/src/__tests__/compile-layer.test.ts +120 -1
- package/src/__tests__/helpers/invariants.ts +54 -20
- package/src/__tests__/known-bugs.test.ts +455 -2
- package/src/__tests__/legend.test.ts +69 -4
- package/src/__tests__/ranking-recipes.test.ts +260 -0
- package/src/__tests__/scales.test.ts +9 -3
- package/src/__tests__/series-search.test.ts +92 -0
- package/src/__tests__/spec-sugar.test.ts +801 -0
- package/src/__tests__/you-draw-it.test.ts +160 -0
- package/src/annotations/__tests__/compute.test.ts +207 -92
- package/src/annotations/__tests__/geometry.test.ts +66 -0
- package/src/annotations/__tests__/resolve-text.test.ts +527 -27
- package/src/annotations/__tests__/rich-text.test.ts +91 -0
- package/src/annotations/__tests__/thinning.test.ts +148 -0
- package/src/annotations/collisions.ts +130 -84
- package/src/annotations/compute.ts +83 -85
- package/src/annotations/constants.ts +116 -6
- package/src/annotations/geometry.ts +222 -77
- package/src/annotations/move.ts +135 -0
- package/src/annotations/placement.ts +62 -14
- package/src/annotations/resolve-range.ts +2 -0
- package/src/annotations/resolve-refline.ts +2 -0
- package/src/annotations/resolve-text.ts +312 -111
- package/src/annotations/rich-text.ts +65 -0
- package/src/annotations/thinning.ts +84 -10
- package/src/barlist/compile-barlist.ts +16 -6
- package/src/charts/_shared/format-label-value.ts +9 -8
- package/src/charts/bar/__tests__/compute.test.ts +57 -10
- package/src/charts/bar/__tests__/labels.test.ts +5 -4
- package/src/charts/bar/compute.ts +2 -6
- package/src/charts/bar/index.ts +8 -1
- package/src/charts/bar/labels.ts +3 -3
- package/src/charts/beeswarm/__tests__/compute.test.ts +280 -0
- package/src/charts/beeswarm/__tests__/dodge.test.ts +148 -0
- package/src/charts/beeswarm/compute.ts +203 -0
- package/src/charts/beeswarm/dodge.ts +155 -0
- package/src/charts/beeswarm/index.ts +32 -0
- package/src/charts/builtin.ts +12 -1
- package/src/charts/calendar/__tests__/compute.test.ts +417 -0
- package/src/charts/calendar/compute.ts +319 -0
- package/src/charts/calendar/index.ts +32 -0
- package/src/charts/column/__tests__/compute.test.ts +21 -7
- package/src/charts/column/__tests__/labels.test.ts +76 -1
- package/src/charts/column/compute.ts +2 -6
- package/src/charts/column/index.ts +10 -2
- package/src/charts/column/labels.ts +95 -21
- package/src/charts/dot/index.ts +8 -1
- package/src/charts/dot/labels.ts +3 -3
- package/src/charts/line/__tests__/compute.test.ts +15 -19
- package/src/charts/line/area.ts +8 -23
- package/src/charts/line/compute.ts +32 -2
- package/src/charts/parliament/__tests__/compute.test.ts +411 -0
- package/src/charts/parliament/compute.ts +313 -0
- package/src/charts/parliament/index.ts +32 -0
- package/src/charts/pie/__tests__/compute.test.ts +103 -0
- package/src/charts/pie/compute.ts +63 -8
- package/src/charts/pie/labels.ts +1 -0
- package/src/charts/range/__tests__/compute.test.ts +486 -0
- package/src/charts/range/compute.ts +273 -0
- package/src/charts/range/index.ts +82 -0
- package/src/charts/range/labels.ts +169 -0
- package/src/charts/rect/__tests__/compute.test.ts +120 -0
- package/src/charts/rect/index.ts +127 -0
- package/src/charts/scatter/compute.ts +7 -21
- package/src/charts/text/__tests__/compute.test.ts +307 -0
- package/src/charts/text/index.ts +44 -7
- package/src/charts/tick/__tests__/compute.test.ts +75 -0
- package/src/charts/tick/index.ts +7 -4
- package/src/charts/utils.ts +5 -3
- package/src/charts/waffle/__tests__/compute.test.ts +305 -0
- package/src/charts/waffle/compute.ts +197 -0
- package/src/charts/waffle/index.ts +31 -0
- package/src/compile/__tests__/color-scale-reverse.test.ts +89 -0
- package/src/compile/__tests__/data-clip.test.ts +68 -0
- package/src/compile/__tests__/fill-patterns.test.ts +154 -0
- package/src/compile/__tests__/size-scale.test.ts +108 -0
- package/src/compile/color-scale-range.ts +54 -33
- package/src/compile/data-clip.ts +21 -6
- package/src/compile/fill-patterns.ts +74 -0
- package/src/compile/layer.ts +117 -21
- package/src/compile/size-scale.ts +139 -0
- package/src/compile/spec-sugar.ts +758 -0
- package/src/compile/you-draw-it.ts +123 -0
- package/src/compile.ts +310 -103
- package/src/compiler/__tests__/a11y-warnings.test.ts +148 -0
- package/src/compiler/__tests__/fixtures/vl-parity/axis-null.json +19 -0
- package/src/compiler/__tests__/fixtures/vl-parity/color-value.json +20 -0
- package/src/compiler/__tests__/fixtures/vl-parity/count-aggregate.json +22 -0
- package/src/compiler/__tests__/fixtures/vl-parity/data-values.json +19 -0
- package/src/compiler/__tests__/fixtures/vl-parity/layer-axis-null.json +14 -0
- package/src/compiler/__tests__/fixtures/vl-parity/layer-count.json +34 -0
- package/src/compiler/__tests__/fixtures/vl-parity/layer-data-values.json +22 -0
- package/src/compiler/__tests__/fixtures/vl-parity/layer-legend-null.json +41 -0
- package/src/compiler/__tests__/fixtures/vl-parity/layer-scheme.json +38 -0
- package/src/compiler/__tests__/fixtures/vl-parity/layer-sort.json +28 -0
- package/src/compiler/__tests__/fixtures/vl-parity/layer-theta.json +28 -0
- package/src/compiler/__tests__/fixtures/vl-parity/layer-title.json +30 -0
- package/src/compiler/__tests__/fixtures/vl-parity/layer-value-def.json +29 -0
- package/src/compiler/__tests__/fixtures/vl-parity/legend-null.json +32 -0
- package/src/compiler/__tests__/fixtures/vl-parity/scheme.json +25 -0
- package/src/compiler/__tests__/fixtures/vl-parity/sort-by-value.json +19 -0
- package/src/compiler/__tests__/fixtures/vl-parity/theta-arc.json +19 -0
- package/src/compiler/__tests__/fixtures/vl-parity/title-subtitle.json +21 -0
- package/src/compiler/__tests__/fixtures/vl-parity/width-height.json +23 -0
- package/src/compiler/__tests__/keys.test.ts +3 -9
- package/src/compiler/__tests__/normalize.test.ts +65 -1
- package/src/compiler/__tests__/validate-did-you-mean.test.ts +86 -0
- package/src/compiler/__tests__/validate-near-miss.test.ts +89 -0
- package/src/compiler/__tests__/validate.test.ts +156 -0
- package/src/compiler/__tests__/vl-parity.test.ts +68 -0
- package/src/compiler/a11y-warnings.ts +110 -0
- package/src/compiler/keys.ts +5 -4
- package/src/compiler/normalize.ts +150 -7
- package/src/compiler/types.ts +11 -0
- package/src/compiler/validate.ts +664 -18
- package/src/endpoint-labels/__tests__/compute.test.ts +88 -0
- package/src/endpoint-labels/compute.ts +40 -12
- package/src/endpoint-labels/format.ts +45 -15
- package/src/endpoint-labels/predict.ts +50 -36
- package/src/format/__tests__/field-format.test.ts +164 -0
- package/src/format/field-format.ts +27 -0
- package/src/geo/__tests__/compile-map.test.ts +621 -0
- package/src/geo/compile-map.ts +856 -0
- package/src/geo/join.ts +115 -0
- package/src/geo/projections.ts +45 -0
- package/src/geo/types.ts +24 -0
- package/src/graphs/__tests__/compile-graph.test.ts +25 -0
- package/src/graphs/compile-graph.ts +27 -18
- package/src/index.ts +20 -2
- package/src/layout/axes/ticks.ts +59 -10
- package/src/layout/axes.ts +35 -6
- package/src/layout/dimensions.ts +139 -83
- package/src/layout/facet.ts +14 -3
- package/src/layout/plan.ts +83 -33
- package/src/layout/scales.ts +132 -15
- package/src/legend/__tests__/continuous.test.ts +318 -0
- package/src/legend/__tests__/size.test.ts +348 -0
- package/src/legend/__tests__/suppression.test.ts +17 -1
- package/src/legend/compute.ts +119 -13
- package/src/legend/continuous.ts +327 -0
- package/src/legend/size.ts +407 -0
- package/src/legend/suppression.ts +8 -7
- package/src/sankey/compile-sankey.ts +31 -19
- package/src/tables/format-cells.ts +16 -7
- package/src/tilemap/__tests__/compile-tilemap.test.ts +39 -0
- package/src/tilemap/compile-tilemap.ts +24 -7
- package/src/tooltips/__tests__/compute.test.ts +2 -2
- package/src/tooltips/compute.ts +422 -41
- package/src/transforms/__tests__/conditional.test.ts +18 -0
- package/src/transforms/aggregate.ts +2 -1
|
@@ -24,9 +24,10 @@ import {
|
|
|
24
24
|
nudgeAnnotationFromObstacles,
|
|
25
25
|
resolveAnnotationCollisions,
|
|
26
26
|
} from './collisions';
|
|
27
|
-
import {
|
|
27
|
+
import { SUBTITLE_FONT_WEIGHT, subtitleFontSize } from './constants';
|
|
28
28
|
import type { AnnotationMeasureTextFn } from './geometry';
|
|
29
29
|
import { heuristicMeasure } from './geometry';
|
|
30
|
+
import { moveAnnotationTo } from './move';
|
|
30
31
|
import {
|
|
31
32
|
findBestPlacement,
|
|
32
33
|
isAutoPlacement,
|
|
@@ -36,7 +37,11 @@ import {
|
|
|
36
37
|
import { resolvePosition } from './position';
|
|
37
38
|
import { resolveRangeAnnotation } from './resolve-range';
|
|
38
39
|
import { resolveRefLineAnnotation } from './resolve-refline';
|
|
39
|
-
import {
|
|
40
|
+
import {
|
|
41
|
+
makeAnnotationLabelStyle,
|
|
42
|
+
resolveLedeFontWeight,
|
|
43
|
+
resolveTextAnnotation,
|
|
44
|
+
} from './resolve-text';
|
|
40
45
|
|
|
41
46
|
export interface AnnotationContext {
|
|
42
47
|
scales: ResolvedScales;
|
|
@@ -46,6 +51,8 @@ export interface AnnotationContext {
|
|
|
46
51
|
obstacles: PlacementObstacle[];
|
|
47
52
|
svg: { width: number; height: number };
|
|
48
53
|
measure: AnnotationMeasureTextFn;
|
|
54
|
+
/** Theme font stack, so annotation text is measured and rendered in the same face. */
|
|
55
|
+
fontFamily?: string;
|
|
49
56
|
debugPlacement?: boolean;
|
|
50
57
|
/** When true, annotations that overlap are demoted to footnote markers instead of being hidden. */
|
|
51
58
|
autoThin?: boolean;
|
|
@@ -122,7 +129,7 @@ export function computeAnnotations(
|
|
|
122
129
|
}> = [];
|
|
123
130
|
|
|
124
131
|
// ---- Pass 1: resolve explicit annotations and queue auto ones ----
|
|
125
|
-
for (const annotation of spec.annotations) {
|
|
132
|
+
for (const [specIndex, annotation] of spec.annotations.entries()) {
|
|
126
133
|
if (isCompact && annotation.responsive !== false) {
|
|
127
134
|
continue;
|
|
128
135
|
}
|
|
@@ -136,6 +143,7 @@ export function computeAnnotations(
|
|
|
136
143
|
ctx.chartArea,
|
|
137
144
|
ctx.isDark,
|
|
138
145
|
ctx.measure,
|
|
146
|
+
ctx.fontFamily,
|
|
139
147
|
);
|
|
140
148
|
if (resolved && ctx.svg.width > 0 && ctx.svg.height > 0 && isAutoPlacement(annotation)) {
|
|
141
149
|
const px = resolvePosition(annotation.x, ctx.scales.x);
|
|
@@ -148,16 +156,30 @@ export function computeAnnotations(
|
|
|
148
156
|
resolved,
|
|
149
157
|
index: annotations.length,
|
|
150
158
|
});
|
|
159
|
+
// Same contract as the push below -- this branch `continue`s past it.
|
|
160
|
+
resolved.specIndex = specIndex;
|
|
151
161
|
annotations.push(resolved);
|
|
152
162
|
continue;
|
|
153
163
|
}
|
|
154
164
|
}
|
|
155
165
|
break;
|
|
156
166
|
case 'range':
|
|
157
|
-
resolved = resolveRangeAnnotation(
|
|
167
|
+
resolved = resolveRangeAnnotation(
|
|
168
|
+
annotation,
|
|
169
|
+
ctx.scales,
|
|
170
|
+
ctx.chartArea,
|
|
171
|
+
ctx.isDark,
|
|
172
|
+
ctx.fontFamily,
|
|
173
|
+
);
|
|
158
174
|
break;
|
|
159
175
|
case 'refline':
|
|
160
|
-
resolved = resolveRefLineAnnotation(
|
|
176
|
+
resolved = resolveRefLineAnnotation(
|
|
177
|
+
annotation,
|
|
178
|
+
ctx.scales,
|
|
179
|
+
ctx.chartArea,
|
|
180
|
+
ctx.isDark,
|
|
181
|
+
ctx.fontFamily,
|
|
182
|
+
);
|
|
161
183
|
break;
|
|
162
184
|
}
|
|
163
185
|
|
|
@@ -172,6 +194,10 @@ export function computeAnnotations(
|
|
|
172
194
|
ctx.measure,
|
|
173
195
|
);
|
|
174
196
|
}
|
|
197
|
+
// The resolved array is filtered (an out-of-domain annotation resolves to
|
|
198
|
+
// nothing), so carry the spec index along -- it's the only stable way back
|
|
199
|
+
// to the authored annotation from here on.
|
|
200
|
+
resolved.specIndex = specIndex;
|
|
175
201
|
annotations.push(resolved);
|
|
176
202
|
}
|
|
177
203
|
}
|
|
@@ -189,18 +215,23 @@ export function computeAnnotations(
|
|
|
189
215
|
|
|
190
216
|
for (const entry of autoQueue) {
|
|
191
217
|
const { annotation, anchorX, anchorY, resolved } = entry;
|
|
218
|
+
// Must mirror resolveTextAnnotation exactly (lede weight, theme font):
|
|
219
|
+
// findBestPlacement measures with this style, and the resolved annotation
|
|
220
|
+
// renders with the other one. Any drift and the scored bounds are a lie.
|
|
192
221
|
const labelStyle = makeAnnotationLabelStyle(
|
|
193
222
|
annotation.fontSize,
|
|
194
|
-
annotation
|
|
223
|
+
resolveLedeFontWeight(annotation),
|
|
195
224
|
undefined,
|
|
196
225
|
ctx.isDark,
|
|
226
|
+
ctx.fontFamily,
|
|
197
227
|
);
|
|
198
228
|
|
|
199
229
|
const subtitleStyle = annotation.subtitle
|
|
200
230
|
? {
|
|
201
|
-
fontSize: labelStyle.fontSize
|
|
202
|
-
fontWeight:
|
|
231
|
+
fontSize: subtitleFontSize(labelStyle.fontSize),
|
|
232
|
+
fontWeight: SUBTITLE_FONT_WEIGHT,
|
|
203
233
|
lineHeight: labelStyle.lineHeight,
|
|
234
|
+
fontFamily: labelStyle.fontFamily,
|
|
204
235
|
}
|
|
205
236
|
: undefined;
|
|
206
237
|
|
|
@@ -212,6 +243,7 @@ export function computeAnnotations(
|
|
|
212
243
|
fontSize: labelStyle.fontSize,
|
|
213
244
|
fontWeight: Number(labelStyle.fontWeight) || 400,
|
|
214
245
|
lineHeight: labelStyle.lineHeight,
|
|
246
|
+
fontFamily: labelStyle.fontFamily,
|
|
215
247
|
},
|
|
216
248
|
annotation.subtitle,
|
|
217
249
|
subtitleStyle,
|
|
@@ -222,46 +254,29 @@ export function computeAnnotations(
|
|
|
222
254
|
ctx.debugPlacement,
|
|
223
255
|
);
|
|
224
256
|
|
|
225
|
-
//
|
|
257
|
+
// Commit the placement. `moveAnnotationTo` owns every piece of the block's
|
|
258
|
+
// geometry -- label, subtitle, both bounds caches, connector -- so this pass
|
|
259
|
+
// does not get to invent its own copy of any of them. It used to, and the
|
|
260
|
+
// copies drifted: this one re-derived the subtitle's baseline from a font-metric
|
|
261
|
+
// sum spelled `+ 2` while the resolver spelled it `SUBTITLE_GAP`, so a subtitled
|
|
262
|
+
// callout rendered 3px off from where the search had actually scored it.
|
|
226
263
|
if (resolved.label) {
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
if (resolved.label.connector) {
|
|
236
|
-
const box = result.bounds;
|
|
237
|
-
const cx = box.x + box.width / 2;
|
|
238
|
-
const cy = box.y + box.height / 2;
|
|
239
|
-
const isCurve = resolved.label.connector.style === 'curve';
|
|
240
|
-
if (isCurve) {
|
|
241
|
-
resolved.label.connector.from = { x: box.x + box.width, y: cy };
|
|
242
|
-
} else {
|
|
243
|
-
const halfW = box.width / 2 || 1;
|
|
244
|
-
const halfH = box.height / 2 || 1;
|
|
245
|
-
const ndx = (anchorX - cx) / halfW;
|
|
246
|
-
const ndy = (anchorY - cy) / halfH;
|
|
247
|
-
if (Math.abs(ndy) >= Math.abs(ndx)) {
|
|
248
|
-
resolved.label.connector.from = {
|
|
249
|
-
x: cx,
|
|
250
|
-
y: ndy < 0 ? box.y : box.y + box.height,
|
|
251
|
-
};
|
|
252
|
-
} else {
|
|
253
|
-
resolved.label.connector.from = {
|
|
254
|
-
x: ndx < 0 ? box.x : box.x + box.width,
|
|
255
|
-
y: cy,
|
|
256
|
-
};
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
}
|
|
264
|
+
moveAnnotationTo(
|
|
265
|
+
resolved,
|
|
266
|
+
resolved.label,
|
|
267
|
+
result.labelX,
|
|
268
|
+
result.labelY,
|
|
269
|
+
ctx.measure,
|
|
270
|
+
result.textAnchor,
|
|
271
|
+
);
|
|
260
272
|
}
|
|
261
|
-
resolved.bounds = result.bounds;
|
|
262
273
|
|
|
263
|
-
// Add this annotation's bounds as obstacle for subsequent auto annotations
|
|
264
|
-
|
|
274
|
+
// Add this annotation's bounds as obstacle for subsequent auto annotations.
|
|
275
|
+
// Read it back off the annotation rather than reusing `result.bounds`: the
|
|
276
|
+
// mover is the authority on the block's extent, and `result.bounds` is the
|
|
277
|
+
// search's own estimate of it. One of them has to be the source of truth.
|
|
278
|
+
const placedBounds = resolved.bounds ?? result.bounds;
|
|
279
|
+
workingObstacles.push({ ...placedBounds, kind: 'annotation' } as PlacementObstacle);
|
|
265
280
|
}
|
|
266
281
|
} else if (autoQueue.length > 0) {
|
|
267
282
|
// Fallback: no SVG dimensions, use legacy nudge for auto annotations
|
|
@@ -282,46 +297,29 @@ export function computeAnnotations(
|
|
|
282
297
|
// Resolve annotation-to-annotation collisions for non-auto-placed annotations.
|
|
283
298
|
// When auto placement ran, those annotations already avoid each other via the
|
|
284
299
|
// obstacle-accumulation loop above, so only explicitly placed ones need this.
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
resolveAnnotationCollisions(
|
|
309
|
-
explicitAnnotations,
|
|
310
|
-
explicitSpecs,
|
|
311
|
-
ctx.scales,
|
|
312
|
-
ctx.chartArea,
|
|
313
|
-
ctx.measure,
|
|
314
|
-
);
|
|
315
|
-
}
|
|
316
|
-
} else {
|
|
317
|
-
resolveAnnotationCollisions(
|
|
318
|
-
annotations,
|
|
319
|
-
spec.annotations,
|
|
320
|
-
ctx.scales,
|
|
321
|
-
ctx.chartArea,
|
|
322
|
-
ctx.measure,
|
|
323
|
-
);
|
|
324
|
-
}
|
|
300
|
+
//
|
|
301
|
+
// There used to be a hand-rolled walk here that built a *compacted* pair of
|
|
302
|
+
// arrays (explicit annotations + their specs) and passed those. It was the
|
|
303
|
+
// second representation of the same list, and once `resolveAnnotationCollisions`
|
|
304
|
+
// started keying on the stamped `specIndex` — a GLOBAL index — the compacted
|
|
305
|
+
// spec array no longer matched it. Every lookup was off by the number of auto
|
|
306
|
+
// annotations, and the `type === 'text'` guard swallowed the resulting
|
|
307
|
+
// `undefined`, so collision avoidance silently did nothing the moment a single
|
|
308
|
+
// auto-placed annotation was present. Which is the *encouraged* authoring style:
|
|
309
|
+
// a bare `{ type: 'text' }` is auto-placed by definition.
|
|
310
|
+
//
|
|
311
|
+
// So don't rebuild the list. Pass the real one, and let the pass skip the
|
|
312
|
+
// auto-placed entries itself — `specIndex` already tells it which spec each
|
|
313
|
+
// resolved annotation came from.
|
|
314
|
+
const autoIndices = new Set(autoQueue.map((q) => q.index));
|
|
315
|
+
resolveAnnotationCollisions(
|
|
316
|
+
annotations,
|
|
317
|
+
spec.annotations,
|
|
318
|
+
ctx.scales,
|
|
319
|
+
ctx.chartArea,
|
|
320
|
+
ctx.measure,
|
|
321
|
+
autoQueue.length > 0 ? (i) => autoIndices.has(i) : undefined,
|
|
322
|
+
);
|
|
325
323
|
|
|
326
324
|
// Clamp labels that overflow the SVG boundary back inside
|
|
327
325
|
if (ctx.svg.width > 0 && ctx.svg.height > 0) {
|
|
@@ -1,9 +1,24 @@
|
|
|
1
1
|
/** Default font size for annotation labels. */
|
|
2
|
-
export const DEFAULT_ANNOTATION_FONT_SIZE =
|
|
2
|
+
export const DEFAULT_ANNOTATION_FONT_SIZE = 13;
|
|
3
3
|
|
|
4
|
-
/**
|
|
4
|
+
/**
|
|
5
|
+
* Default font weight for annotation labels. Regular, not bold: emphasis comes
|
|
6
|
+
* from inline `**bold**` spans, per the NYT/Datawrapper voice.
|
|
7
|
+
*/
|
|
5
8
|
export const DEFAULT_ANNOTATION_FONT_WEIGHT = 400;
|
|
6
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
|
+
|
|
7
22
|
/** Default line height multiplier for annotation text. */
|
|
8
23
|
export const DEFAULT_LINE_HEIGHT = 1.3;
|
|
9
24
|
|
|
@@ -22,6 +37,12 @@ export const DARK_TEXT_FILL = '#d1d5db';
|
|
|
22
37
|
export const LIGHT_REFLINE_STROKE = '#888888';
|
|
23
38
|
export const DARK_REFLINE_STROKE = '#9ca3af';
|
|
24
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
|
+
|
|
25
46
|
// Muted text fill for annotation subtitles (~60% perceived contrast vs primary)
|
|
26
47
|
export const LIGHT_MUTED_TEXT_FILL = '#6b7280';
|
|
27
48
|
export const DARK_MUTED_TEXT_FILL = '#9ca3af';
|
|
@@ -30,11 +51,17 @@ export const DARK_MUTED_TEXT_FILL = '#9ca3af';
|
|
|
30
51
|
export const LIGHT_DOT_FILL = '#ffffff';
|
|
31
52
|
export const DARK_DOT_FILL = '#0a0a0a';
|
|
32
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
|
+
|
|
33
60
|
/** Default annotation dot radius in pixels. */
|
|
34
|
-
export const DEFAULT_DOT_RADIUS =
|
|
61
|
+
export const DEFAULT_DOT_RADIUS = 4;
|
|
35
62
|
|
|
36
63
|
/** Default annotation dot stroke width in pixels. */
|
|
37
|
-
export const DEFAULT_DOT_STROKE_WIDTH =
|
|
64
|
+
export const DEFAULT_DOT_STROKE_WIDTH = 1.5;
|
|
38
65
|
|
|
39
66
|
/** Vertical gap (px) between the primary annotation text and its subtitle. */
|
|
40
67
|
export const SUBTITLE_GAP = 2;
|
|
@@ -42,8 +69,91 @@ export const SUBTITLE_GAP = 2;
|
|
|
42
69
|
/** Subtitle font size multiplier (relative to primary annotation font size). */
|
|
43
70
|
export const SUBTITLE_FONT_SIZE_RATIO = 0.85;
|
|
44
71
|
|
|
45
|
-
/**
|
|
46
|
-
|
|
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;
|
|
47
157
|
|
|
48
158
|
/** Padding between annotation and obstacle when nudging. */
|
|
49
159
|
export const NUDGE_PADDING = 6;
|