@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
|
@@ -9,19 +9,27 @@ import type { NormalizedChartSpec } from '../../compiler/types';
|
|
|
9
9
|
import { computeScales } from '../../layout/scales';
|
|
10
10
|
import { computeAnnotations } from '../compute';
|
|
11
11
|
import {
|
|
12
|
+
DARK_CONNECTOR_STROKE,
|
|
12
13
|
DARK_DOT_FILL,
|
|
14
|
+
DARK_LABEL_BACKGROUND,
|
|
13
15
|
DARK_MUTED_TEXT_FILL,
|
|
14
|
-
DARK_TEXT_FILL,
|
|
15
16
|
DEFAULT_ANNOTATION_FONT_SIZE,
|
|
17
|
+
DEFAULT_ANNOTATION_FONT_WEIGHT,
|
|
16
18
|
DEFAULT_DOT_RADIUS,
|
|
17
19
|
DEFAULT_DOT_STROKE_WIDTH,
|
|
18
20
|
DEFAULT_LINE_HEIGHT,
|
|
21
|
+
FALLBACK_FONT_FAMILY,
|
|
22
|
+
LEDE_FONT_WEIGHT,
|
|
23
|
+
LIGHT_CONNECTOR_STROKE,
|
|
19
24
|
LIGHT_DOT_FILL,
|
|
25
|
+
LIGHT_LABEL_BACKGROUND,
|
|
20
26
|
LIGHT_MUTED_TEXT_FILL,
|
|
21
|
-
|
|
27
|
+
MIN_CONNECTOR_LENGTH,
|
|
22
28
|
SUBTITLE_FONT_SIZE_RATIO,
|
|
29
|
+
SUBTITLE_FONT_WEIGHT,
|
|
23
30
|
SUBTITLE_GAP,
|
|
24
31
|
} from '../constants';
|
|
32
|
+
import { heuristicMeasure } from '../geometry';
|
|
25
33
|
|
|
26
34
|
const chartArea: Rect = { x: 50, y: 20, width: 500, height: 300 };
|
|
27
35
|
|
|
@@ -56,8 +64,79 @@ function makeSpec(annotations: Annotation[]): NormalizedChartSpec {
|
|
|
56
64
|
}
|
|
57
65
|
|
|
58
66
|
describe('text annotation: dot', () => {
|
|
59
|
-
|
|
60
|
-
|
|
67
|
+
// A connector with no arrowhead needs a terminator at the data point, so the
|
|
68
|
+
// engine resolves the default marker for it. `dot: false` opts out.
|
|
69
|
+
it('resolves the default dot when a connector is enabled and no dot key is set', () => {
|
|
70
|
+
const spec = makeSpec([{ type: 'text', x: '2020-01-01', y: 20, text: 'No dot key' }]);
|
|
71
|
+
const scales = computeScales(spec, chartArea, spec.data);
|
|
72
|
+
const annotations = computeAnnotations(spec, scales, chartArea, fullStrategy);
|
|
73
|
+
|
|
74
|
+
const dot = annotations[0].dot;
|
|
75
|
+
expect(dot).toBeDefined();
|
|
76
|
+
expect(dot!.radius).toBe(DEFAULT_DOT_RADIUS);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('resolves the default dot for an explicit straight connector', () => {
|
|
80
|
+
const spec = makeSpec([
|
|
81
|
+
{ type: 'text', x: '2020-01-01', y: 20, text: 'Straight', connector: 'straight' },
|
|
82
|
+
]);
|
|
83
|
+
const scales = computeScales(spec, chartArea, spec.data);
|
|
84
|
+
const annotations = computeAnnotations(spec, scales, chartArea, fullStrategy);
|
|
85
|
+
|
|
86
|
+
expect(annotations[0].dot).toBeDefined();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('resolves the default dot for a drop-line connector', () => {
|
|
90
|
+
const spec = makeSpec([
|
|
91
|
+
{ type: 'text', x: '2020-01-01', y: 20, text: 'Drop', connector: 'drop-line' },
|
|
92
|
+
]);
|
|
93
|
+
const scales = computeScales(spec, chartArea, spec.data);
|
|
94
|
+
const annotations = computeAnnotations(spec, scales, chartArea, fullStrategy);
|
|
95
|
+
|
|
96
|
+
expect(annotations[0].dot).toBeDefined();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('honors an explicit dot: true on a drop-line connector', () => {
|
|
100
|
+
const spec = makeSpec([
|
|
101
|
+
{ type: 'text', x: '2020-01-01', y: 20, text: 'Drop', connector: 'drop-line', dot: true },
|
|
102
|
+
]);
|
|
103
|
+
const scales = computeScales(spec, chartArea, spec.data);
|
|
104
|
+
const annotations = computeAnnotations(spec, scales, chartArea, fullStrategy);
|
|
105
|
+
|
|
106
|
+
const ann = annotations[0];
|
|
107
|
+
const px = scales.x!.scale(new Date('2020-01-01')) as number;
|
|
108
|
+
const py = scales.y!.scale(20) as number;
|
|
109
|
+
expect(ann.dot).toBeDefined();
|
|
110
|
+
expect(ann.dot!.x).toBeCloseTo(px, 5);
|
|
111
|
+
expect(ann.dot!.y).toBeCloseTo(py, 5);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// An arrowhead is already a terminator; adding a marker under it double-marks
|
|
115
|
+
// the data point.
|
|
116
|
+
it('does not resolve a default dot when the connector has an arrowhead', () => {
|
|
117
|
+
const spec = makeSpec([
|
|
118
|
+
{ type: 'text', x: '2020-01-01', y: 20, text: 'Arrowed', connector: 'curve' },
|
|
119
|
+
]);
|
|
120
|
+
const scales = computeScales(spec, chartArea, spec.data);
|
|
121
|
+
const annotations = computeAnnotations(spec, scales, chartArea, fullStrategy);
|
|
122
|
+
|
|
123
|
+
expect(annotations[0].dot).toBeUndefined();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('does not populate dot when the connector is disabled', () => {
|
|
127
|
+
const spec = makeSpec([
|
|
128
|
+
{ type: 'text', x: '2020-01-01', y: 20, text: 'Bare', connector: false },
|
|
129
|
+
]);
|
|
130
|
+
const scales = computeScales(spec, chartArea, spec.data);
|
|
131
|
+
const annotations = computeAnnotations(spec, scales, chartArea, fullStrategy);
|
|
132
|
+
|
|
133
|
+
expect(annotations[0].dot).toBeUndefined();
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('dot: false suppresses the default marker', () => {
|
|
137
|
+
const spec = makeSpec([
|
|
138
|
+
{ type: 'text', x: '2020-01-01', y: 20, text: 'No marker', dot: false },
|
|
139
|
+
]);
|
|
61
140
|
const scales = computeScales(spec, chartArea, spec.data);
|
|
62
141
|
const annotations = computeAnnotations(spec, scales, chartArea, fullStrategy);
|
|
63
142
|
|
|
@@ -74,7 +153,8 @@ describe('text annotation: dot', () => {
|
|
|
74
153
|
expect(dot!.radius).toBe(DEFAULT_DOT_RADIUS);
|
|
75
154
|
expect(dot!.strokeWidth).toBe(DEFAULT_DOT_STROKE_WIDTH);
|
|
76
155
|
expect(dot!.fill).toBe(LIGHT_DOT_FILL);
|
|
77
|
-
|
|
156
|
+
// Marker and its (non-arrowed) leader share the quiet gray.
|
|
157
|
+
expect(dot!.stroke).toBe(LIGHT_CONNECTOR_STROKE);
|
|
78
158
|
});
|
|
79
159
|
|
|
80
160
|
it('populates dot with dark-mode defaults when isDark is true', () => {
|
|
@@ -85,7 +165,7 @@ describe('text annotation: dot', () => {
|
|
|
85
165
|
const dot = annotations[0].dot;
|
|
86
166
|
expect(dot).toBeDefined();
|
|
87
167
|
expect(dot!.fill).toBe(DARK_DOT_FILL);
|
|
88
|
-
expect(dot!.stroke).toBe(
|
|
168
|
+
expect(dot!.stroke).toBe(DARK_CONNECTOR_STROKE);
|
|
89
169
|
});
|
|
90
170
|
|
|
91
171
|
it('respects user-supplied dot style overrides', () => {
|
|
@@ -121,29 +201,45 @@ describe('text annotation: dot', () => {
|
|
|
121
201
|
expect(dot!.radius).toBe(3);
|
|
122
202
|
// Other fields fall back to defaults.
|
|
123
203
|
expect(dot!.fill).toBe(LIGHT_DOT_FILL);
|
|
124
|
-
expect(dot!.stroke).toBe(
|
|
204
|
+
expect(dot!.stroke).toBe(LIGHT_CONNECTOR_STROKE);
|
|
125
205
|
expect(dot!.strokeWidth).toBe(DEFAULT_DOT_STROKE_WIDTH);
|
|
126
206
|
});
|
|
127
207
|
|
|
128
|
-
|
|
129
|
-
|
|
208
|
+
// The marker sits ON the data point; the connector stops short of it. The old
|
|
209
|
+
// behavior put the dot at the pulled-back connector tip, which offset it from
|
|
210
|
+
// the point it was marking.
|
|
211
|
+
it('dot coordinates sit exactly on the data point, not the pulled-back connector tip', () => {
|
|
212
|
+
const spec = makeSpec([
|
|
213
|
+
{
|
|
214
|
+
type: 'text',
|
|
215
|
+
x: '2020-01-01',
|
|
216
|
+
y: 20,
|
|
217
|
+
text: 'Co-render',
|
|
218
|
+
dot: true,
|
|
219
|
+
offset: { dx: 60, dy: -60 },
|
|
220
|
+
},
|
|
221
|
+
]);
|
|
130
222
|
const scales = computeScales(spec, chartArea, spec.data);
|
|
131
223
|
const annotations = computeAnnotations(spec, scales, chartArea, fullStrategy);
|
|
132
224
|
|
|
133
225
|
const resolved = annotations[0];
|
|
226
|
+
const px = scales.x!.scale(new Date('2020-01-01')) as number;
|
|
227
|
+
const py = scales.y!.scale(20) as number;
|
|
228
|
+
|
|
134
229
|
expect(resolved.dot).toBeDefined();
|
|
135
|
-
expect(resolved.
|
|
136
|
-
expect(resolved.dot!.
|
|
137
|
-
|
|
230
|
+
expect(resolved.dot!.x).toBeCloseTo(px, 5);
|
|
231
|
+
expect(resolved.dot!.y).toBeCloseTo(py, 5);
|
|
232
|
+
|
|
233
|
+
// The connector stops short of the marker.
|
|
234
|
+
const connector = resolved.label!.connector!;
|
|
235
|
+
const gap = Math.hypot(px - connector.to.x, py - connector.to.y);
|
|
236
|
+
expect(gap).toBeGreaterThan(resolved.dot!.radius);
|
|
138
237
|
});
|
|
139
238
|
|
|
140
239
|
it('dot coordinates apply user connectorOffset.to', () => {
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
chartArea,
|
|
145
|
-
fullStrategy,
|
|
146
|
-
);
|
|
240
|
+
const baseSpec = makeSpec([{ type: 'text', x: '2020-01-01', y: 20, text: 'A', dot: true }]);
|
|
241
|
+
const baseScales = computeScales(baseSpec, chartArea, baseSpec.data);
|
|
242
|
+
const withoutOffset = computeAnnotations(baseSpec, baseScales, chartArea, fullStrategy);
|
|
147
243
|
const baseDot = withoutOffset[0].dot!;
|
|
148
244
|
|
|
149
245
|
const withOffsetSpec = makeSpec([
|
|
@@ -160,12 +256,57 @@ describe('text annotation: dot', () => {
|
|
|
160
256
|
const withOffset = computeAnnotations(withOffsetSpec, scales2, chartArea, fullStrategy);
|
|
161
257
|
const offsetDot = withOffset[0].dot!;
|
|
162
258
|
|
|
163
|
-
// The user's connector offset shifts the data-side endpoint, so the
|
|
164
|
-
//
|
|
165
|
-
expect(offsetDot.x).
|
|
166
|
-
expect(offsetDot.y).
|
|
167
|
-
|
|
168
|
-
|
|
259
|
+
// The user's connector offset shifts the data-side endpoint, so the marker
|
|
260
|
+
// tracks it.
|
|
261
|
+
expect(offsetDot.x).toBeCloseTo(baseDot.x + 10, 5);
|
|
262
|
+
expect(offsetDot.y).toBeCloseTo(baseDot.y - 5, 5);
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
describe('text annotation: background', () => {
|
|
267
|
+
it('does not populate background when not specified', () => {
|
|
268
|
+
const spec = makeSpec([{ type: 'text', x: '2020-01-01', y: 20, text: 'No plate' }]);
|
|
269
|
+
const scales = computeScales(spec, chartArea, spec.data);
|
|
270
|
+
const annotations = computeAnnotations(spec, scales, chartArea, fullStrategy);
|
|
271
|
+
|
|
272
|
+
expect(annotations[0].label.background).toBeUndefined();
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it('resolves background: true to the light surface in light mode', () => {
|
|
276
|
+
const spec = makeSpec([
|
|
277
|
+
{ type: 'text', x: '2020-01-01', y: 20, text: 'Plate', background: true },
|
|
278
|
+
]);
|
|
279
|
+
const scales = computeScales(spec, chartArea, spec.data);
|
|
280
|
+
const annotations = computeAnnotations(spec, scales, chartArea, fullStrategy, false);
|
|
281
|
+
|
|
282
|
+
expect(annotations[0].label.background).toBe(LIGHT_LABEL_BACKGROUND);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
// The dark-mode regression: `background: true` must NOT stay white, or the
|
|
286
|
+
// theme's light text renders light-gray-on-white and is unreadable.
|
|
287
|
+
it('resolves background: true to the dark surface when isDark is true', () => {
|
|
288
|
+
const spec = makeSpec([
|
|
289
|
+
{ type: 'text', x: '2020-01-01', y: 20, text: 'Plate', background: true },
|
|
290
|
+
]);
|
|
291
|
+
const scales = computeScales(spec, chartArea, spec.data);
|
|
292
|
+
const annotations = computeAnnotations(spec, scales, chartArea, fullStrategy, true);
|
|
293
|
+
|
|
294
|
+
expect(annotations[0].label.background).toBe(DARK_LABEL_BACKGROUND);
|
|
295
|
+
expect(annotations[0].label.background).not.toBe(LIGHT_LABEL_BACKGROUND);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it('passes an explicit background color through unchanged in both modes', () => {
|
|
299
|
+
const spec = makeSpec([
|
|
300
|
+
{ type: 'text', x: '2020-01-01', y: 20, text: 'Plate', background: '#ff00ff' },
|
|
301
|
+
]);
|
|
302
|
+
const scales = computeScales(spec, chartArea, spec.data);
|
|
303
|
+
|
|
304
|
+
expect(
|
|
305
|
+
computeAnnotations(spec, scales, chartArea, fullStrategy, false)[0].label.background,
|
|
306
|
+
).toBe('#ff00ff');
|
|
307
|
+
expect(
|
|
308
|
+
computeAnnotations(spec, scales, chartArea, fullStrategy, true)[0].label.background,
|
|
309
|
+
).toBe('#ff00ff');
|
|
169
310
|
});
|
|
170
311
|
});
|
|
171
312
|
|
|
@@ -273,16 +414,375 @@ describe('text annotation: dot + subtitle co-resolution', () => {
|
|
|
273
414
|
text: 'Big moment',
|
|
274
415
|
subtitle: 'Adjusted for inflation',
|
|
275
416
|
dot: true,
|
|
417
|
+
offset: { dx: 60, dy: -60 },
|
|
276
418
|
},
|
|
277
419
|
]);
|
|
278
420
|
const scales = computeScales(spec, chartArea, spec.data);
|
|
279
421
|
const annotations = computeAnnotations(spec, scales, chartArea, fullStrategy);
|
|
280
422
|
|
|
281
423
|
const resolved = annotations[0];
|
|
424
|
+
const px = scales.x!.scale(new Date('2020-01-01')) as number;
|
|
425
|
+
const py = scales.y!.scale(20) as number;
|
|
426
|
+
|
|
282
427
|
expect(resolved.dot).toBeDefined();
|
|
283
428
|
expect(resolved.subtitle).toBeDefined();
|
|
284
|
-
|
|
285
|
-
expect(resolved.dot!.
|
|
286
|
-
expect(resolved.
|
|
429
|
+
expect(resolved.dot!.x).toBeCloseTo(px, 5);
|
|
430
|
+
expect(resolved.dot!.y).toBeCloseTo(py, 5);
|
|
431
|
+
expect(resolved.label!.connector).toBeDefined();
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
// The connector must clear the subtitle too, not just the primary line. The
|
|
435
|
+
// subtitle is often the wider of the two.
|
|
436
|
+
it('connector origin clears the subtitle when the subtitle is the wider line', () => {
|
|
437
|
+
const spec = makeSpec([
|
|
438
|
+
{
|
|
439
|
+
type: 'text',
|
|
440
|
+
x: '2020-01-01',
|
|
441
|
+
y: 20,
|
|
442
|
+
text: 'Short',
|
|
443
|
+
subtitle: 'A considerably longer subtitle line than the primary',
|
|
444
|
+
// Label sits below-left of the point, so the connector exits up/right
|
|
445
|
+
// and would cross the subtitle if bounds ignored it.
|
|
446
|
+
offset: { dx: 0, dy: 90 },
|
|
447
|
+
},
|
|
448
|
+
]);
|
|
449
|
+
const scales = computeScales(spec, chartArea, spec.data);
|
|
450
|
+
const annotations = computeAnnotations(spec, scales, chartArea, fullStrategy);
|
|
451
|
+
|
|
452
|
+
const ann = annotations[0];
|
|
453
|
+
const connector = ann.label!.connector!;
|
|
454
|
+
const bounds = ann.bounds!;
|
|
455
|
+
|
|
456
|
+
// The origin must sit outside the full (label ∪ subtitle) block.
|
|
457
|
+
const insideX = connector.from.x >= bounds.x && connector.from.x <= bounds.x + bounds.width;
|
|
458
|
+
const insideY = connector.from.y >= bounds.y && connector.from.y <= bounds.y + bounds.height;
|
|
459
|
+
expect(insideX && insideY).toBe(false);
|
|
460
|
+
});
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
describe('text annotation: connector suppression', () => {
|
|
464
|
+
// A label pulled back toward its point leaves no room for a leader: the
|
|
465
|
+
// standoff and marker pullback eat the whole span, and what's left is a nub
|
|
466
|
+
// touching the marker it's meant to point at. Suppress it, keep the dot.
|
|
467
|
+
// (anchor 'top' sets the label ANCHOR_OFFSET above the point; +dy drags it
|
|
468
|
+
// back down onto it.)
|
|
469
|
+
it('suppresses a connector shorter than MIN_CONNECTOR_LENGTH but keeps the dot', () => {
|
|
470
|
+
const spec = makeSpec([
|
|
471
|
+
{
|
|
472
|
+
type: 'text',
|
|
473
|
+
x: '2020-01-01',
|
|
474
|
+
y: 20,
|
|
475
|
+
text: 'Tiny leader',
|
|
476
|
+
anchor: 'top',
|
|
477
|
+
offset: { dx: 0, dy: 16 },
|
|
478
|
+
},
|
|
479
|
+
]);
|
|
480
|
+
const scales = computeScales(spec, chartArea, spec.data);
|
|
481
|
+
const annotations = computeAnnotations(spec, scales, chartArea, fullStrategy);
|
|
482
|
+
|
|
483
|
+
expect(annotations[0].label!.connector).toBeUndefined();
|
|
484
|
+
expect(annotations[0].dot).toBeDefined();
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
// The default (no offset, no anchor) must draw a leader. A bare annotation is
|
|
488
|
+
// the spec an author writes first, and floating text with no line back to the
|
|
489
|
+
// data is exactly what this redesign set out to kill.
|
|
490
|
+
it('draws a connector for a bare text annotation with no offset', () => {
|
|
491
|
+
const spec = makeSpec([{ type: 'text', x: '2020-01-01', y: 20, text: 'Default callout' }]);
|
|
492
|
+
const scales = computeScales(spec, chartArea, spec.data);
|
|
493
|
+
const annotations = computeAnnotations(spec, scales, chartArea, fullStrategy);
|
|
494
|
+
|
|
495
|
+
const connector = annotations[0].label!.connector;
|
|
496
|
+
expect(connector).toBeDefined();
|
|
497
|
+
const length = Math.hypot(
|
|
498
|
+
connector!.to.x - connector!.from.x,
|
|
499
|
+
connector!.to.y - connector!.from.y,
|
|
500
|
+
);
|
|
501
|
+
expect(length).toBeGreaterThanOrEqual(MIN_CONNECTOR_LENGTH);
|
|
502
|
+
expect(annotations[0].dot).toBeDefined();
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
it('suppresses the connector when the data point lands inside the text block', () => {
|
|
506
|
+
// Offset pulls the label back over its own point so the box straddles it.
|
|
507
|
+
// A connector here would be drawn straight through the text.
|
|
508
|
+
const spec = makeSpec([
|
|
509
|
+
{
|
|
510
|
+
type: 'text',
|
|
511
|
+
x: '2020-01-01',
|
|
512
|
+
y: 20,
|
|
513
|
+
text: 'The point sits inside this label',
|
|
514
|
+
offset: { dx: -110, dy: 4 },
|
|
515
|
+
},
|
|
516
|
+
]);
|
|
517
|
+
const scales = computeScales(spec, chartArea, spec.data);
|
|
518
|
+
const annotations = computeAnnotations(spec, scales, chartArea, fullStrategy);
|
|
519
|
+
|
|
520
|
+
expect(annotations[0].label!.connector).toBeUndefined();
|
|
521
|
+
expect(annotations[0].dot).toBeDefined();
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
it('keeps the connector once the label is far enough from the point', () => {
|
|
525
|
+
const spec = makeSpec([
|
|
526
|
+
{ type: 'text', x: '2020-01-01', y: 20, text: 'Far label', offset: { dx: 0, dy: -60 } },
|
|
527
|
+
]);
|
|
528
|
+
const scales = computeScales(spec, chartArea, spec.data);
|
|
529
|
+
const annotations = computeAnnotations(spec, scales, chartArea, fullStrategy);
|
|
530
|
+
|
|
531
|
+
const connector = annotations[0].label!.connector!;
|
|
532
|
+
expect(connector).toBeDefined();
|
|
533
|
+
const length = Math.hypot(connector.to.x - connector.from.x, connector.to.y - connector.from.y);
|
|
534
|
+
expect(length).toBeGreaterThanOrEqual(MIN_CONNECTOR_LENGTH);
|
|
535
|
+
});
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
describe('text annotation: connector config', () => {
|
|
539
|
+
it('bare "curve" string defaults arrow to true', () => {
|
|
540
|
+
const spec = makeSpec([
|
|
541
|
+
{
|
|
542
|
+
type: 'text',
|
|
543
|
+
x: '2020-01-01',
|
|
544
|
+
y: 20,
|
|
545
|
+
text: 'Curve',
|
|
546
|
+
connector: 'curve',
|
|
547
|
+
offset: { dx: 0, dy: -60 },
|
|
548
|
+
},
|
|
549
|
+
]);
|
|
550
|
+
const scales = computeScales(spec, chartArea, spec.data);
|
|
551
|
+
const annotations = computeAnnotations(spec, scales, chartArea, fullStrategy);
|
|
552
|
+
|
|
553
|
+
const c = annotations[0].label!.connector!;
|
|
554
|
+
expect(c.style).toBe('curve');
|
|
555
|
+
expect(c.arrow).toBe(true);
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
it('bare "straight" string defaults arrow to false', () => {
|
|
559
|
+
const spec = makeSpec([
|
|
560
|
+
{
|
|
561
|
+
type: 'text',
|
|
562
|
+
x: '2020-01-01',
|
|
563
|
+
y: 20,
|
|
564
|
+
text: 'Straight',
|
|
565
|
+
connector: 'straight',
|
|
566
|
+
offset: { dx: 0, dy: -60 },
|
|
567
|
+
},
|
|
568
|
+
]);
|
|
569
|
+
const scales = computeScales(spec, chartArea, spec.data);
|
|
570
|
+
const annotations = computeAnnotations(spec, scales, chartArea, fullStrategy);
|
|
571
|
+
|
|
572
|
+
const c = annotations[0].label!.connector!;
|
|
573
|
+
expect(c.style).toBe('straight');
|
|
574
|
+
expect(c.arrow).toBe(false);
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
it('boolean true defaults to straight with no arrow', () => {
|
|
578
|
+
const spec = makeSpec([
|
|
579
|
+
{
|
|
580
|
+
type: 'text',
|
|
581
|
+
x: '2020-01-01',
|
|
582
|
+
y: 20,
|
|
583
|
+
text: 'Default',
|
|
584
|
+
connector: true,
|
|
585
|
+
offset: { dx: 0, dy: -60 },
|
|
586
|
+
},
|
|
587
|
+
]);
|
|
588
|
+
const scales = computeScales(spec, chartArea, spec.data);
|
|
589
|
+
const annotations = computeAnnotations(spec, scales, chartArea, fullStrategy);
|
|
590
|
+
|
|
591
|
+
const c = annotations[0].label!.connector!;
|
|
592
|
+
expect(c.style).toBe('straight');
|
|
593
|
+
expect(c.arrow).toBe(false);
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
it('object form { type: "straight", arrow: true } enables arrow on straight', () => {
|
|
597
|
+
const spec = makeSpec([
|
|
598
|
+
{
|
|
599
|
+
type: 'text',
|
|
600
|
+
x: '2020-01-01',
|
|
601
|
+
y: 20,
|
|
602
|
+
text: 'Arrow straight',
|
|
603
|
+
connector: { type: 'straight', arrow: true },
|
|
604
|
+
offset: { dx: 0, dy: -60 },
|
|
605
|
+
},
|
|
606
|
+
]);
|
|
607
|
+
const scales = computeScales(spec, chartArea, spec.data);
|
|
608
|
+
const annotations = computeAnnotations(spec, scales, chartArea, fullStrategy);
|
|
609
|
+
|
|
610
|
+
const c = annotations[0].label!.connector!;
|
|
611
|
+
expect(c.style).toBe('straight');
|
|
612
|
+
expect(c.arrow).toBe(true);
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
it('object form { type: "curve", arrow: false } disables arrow on curve', () => {
|
|
616
|
+
const spec = makeSpec([
|
|
617
|
+
{
|
|
618
|
+
type: 'text',
|
|
619
|
+
x: '2020-01-01',
|
|
620
|
+
y: 20,
|
|
621
|
+
text: 'No arrow curve',
|
|
622
|
+
connector: { type: 'curve', arrow: false },
|
|
623
|
+
offset: { dx: 0, dy: -60 },
|
|
624
|
+
},
|
|
625
|
+
]);
|
|
626
|
+
const scales = computeScales(spec, chartArea, spec.data);
|
|
627
|
+
const annotations = computeAnnotations(spec, scales, chartArea, fullStrategy);
|
|
628
|
+
|
|
629
|
+
const c = annotations[0].label!.connector!;
|
|
630
|
+
expect(c.style).toBe('curve');
|
|
631
|
+
expect(c.arrow).toBe(false);
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
it('object form { type: "curve" } defaults arrow to true', () => {
|
|
635
|
+
const spec = makeSpec([
|
|
636
|
+
{
|
|
637
|
+
type: 'text',
|
|
638
|
+
x: '2020-01-01',
|
|
639
|
+
y: 20,
|
|
640
|
+
text: 'Default curve',
|
|
641
|
+
connector: { type: 'curve' },
|
|
642
|
+
offset: { dx: 0, dy: -60 },
|
|
643
|
+
},
|
|
644
|
+
]);
|
|
645
|
+
const scales = computeScales(spec, chartArea, spec.data);
|
|
646
|
+
const annotations = computeAnnotations(spec, scales, chartArea, fullStrategy);
|
|
647
|
+
|
|
648
|
+
const c = annotations[0].label!.connector!;
|
|
649
|
+
expect(c.style).toBe('curve');
|
|
650
|
+
expect(c.arrow).toBe(true);
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
it('connector: false disables connector entirely', () => {
|
|
654
|
+
const spec = makeSpec([
|
|
655
|
+
{ type: 'text', x: '2020-01-01', y: 20, text: 'No connector', connector: false },
|
|
656
|
+
]);
|
|
657
|
+
const scales = computeScales(spec, chartArea, spec.data);
|
|
658
|
+
const annotations = computeAnnotations(spec, scales, chartArea, fullStrategy);
|
|
659
|
+
|
|
660
|
+
expect(annotations[0].label!.connector).toBeUndefined();
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
it('drop-line string connector always has arrow: false', () => {
|
|
664
|
+
const spec = makeSpec([
|
|
665
|
+
{ type: 'text', x: '2020-01-01', y: 20, text: 'Drop line', connector: 'drop-line' },
|
|
666
|
+
]);
|
|
667
|
+
const scales = computeScales(spec, chartArea, spec.data);
|
|
668
|
+
const annotations = computeAnnotations(spec, scales, chartArea, fullStrategy);
|
|
669
|
+
|
|
670
|
+
const c = annotations[0].label!.connector!;
|
|
671
|
+
expect(c.style).toBe('drop-line');
|
|
672
|
+
expect(c.arrow).toBe(false);
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
it('object form { type: "drop-line", arrow: true } clamps arrow to false', () => {
|
|
676
|
+
const spec = makeSpec([
|
|
677
|
+
{
|
|
678
|
+
type: 'text',
|
|
679
|
+
x: '2020-01-01',
|
|
680
|
+
y: 20,
|
|
681
|
+
text: 'Drop line forced',
|
|
682
|
+
connector: { type: 'drop-line', arrow: true },
|
|
683
|
+
},
|
|
684
|
+
]);
|
|
685
|
+
const scales = computeScales(spec, chartArea, spec.data);
|
|
686
|
+
const annotations = computeAnnotations(spec, scales, chartArea, fullStrategy);
|
|
687
|
+
|
|
688
|
+
const c = annotations[0].label!.connector!;
|
|
689
|
+
expect(c.style).toBe('drop-line');
|
|
690
|
+
expect(c.arrow).toBe(false);
|
|
691
|
+
});
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
describe('text annotation: typography', () => {
|
|
695
|
+
it('defaults to the regular annotation font size and weight', () => {
|
|
696
|
+
const spec = makeSpec([{ type: 'text', x: '2020-01-01', y: 20, text: 'Plain' }]);
|
|
697
|
+
const scales = computeScales(spec, chartArea, spec.data);
|
|
698
|
+
const annotations = computeAnnotations(spec, scales, chartArea, fullStrategy);
|
|
699
|
+
|
|
700
|
+
const style = annotations[0].label!.style;
|
|
701
|
+
expect(style.fontSize).toBe(DEFAULT_ANNOTATION_FONT_SIZE);
|
|
702
|
+
expect(style.fontWeight).toBe(DEFAULT_ANNOTATION_FONT_WEIGHT);
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
// Lede rule: a subtitle turns the primary line into a lede, so it goes bold.
|
|
706
|
+
it('promotes the primary text to the lede weight when a subtitle is present', () => {
|
|
707
|
+
const spec = makeSpec([
|
|
708
|
+
{ type: 'text', x: '2020-01-01', y: 20, text: 'Feb. 25', subtitle: '2015 maximum' },
|
|
709
|
+
]);
|
|
710
|
+
const scales = computeScales(spec, chartArea, spec.data);
|
|
711
|
+
const annotations = computeAnnotations(spec, scales, chartArea, fullStrategy);
|
|
712
|
+
|
|
713
|
+
expect(annotations[0].label!.style.fontWeight).toBe(LEDE_FONT_WEIGHT);
|
|
714
|
+
// The subtitle never inherits it.
|
|
715
|
+
expect(annotations[0].subtitle!.style.fontWeight).toBe(SUBTITLE_FONT_WEIGHT);
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
it('an explicit fontWeight wins over the lede rule', () => {
|
|
719
|
+
const spec = makeSpec([
|
|
720
|
+
{
|
|
721
|
+
type: 'text',
|
|
722
|
+
x: '2020-01-01',
|
|
723
|
+
y: 20,
|
|
724
|
+
text: 'Feb. 25',
|
|
725
|
+
subtitle: '2015 maximum',
|
|
726
|
+
fontWeight: 500,
|
|
727
|
+
},
|
|
728
|
+
]);
|
|
729
|
+
const scales = computeScales(spec, chartArea, spec.data);
|
|
730
|
+
const annotations = computeAnnotations(spec, scales, chartArea, fullStrategy);
|
|
731
|
+
|
|
732
|
+
expect(annotations[0].label!.style.fontWeight).toBe(500);
|
|
733
|
+
});
|
|
734
|
+
|
|
735
|
+
it('threads the theme font family into text, range, and refline labels', () => {
|
|
736
|
+
const spec = makeSpec([
|
|
737
|
+
{ type: 'text', x: '2020-01-01', y: 20, text: 'Callout' },
|
|
738
|
+
{ type: 'range', x: ['2019-01-01', '2020-01-01'], label: 'Band' },
|
|
739
|
+
{ type: 'refline', y: 25, label: 'Target' },
|
|
740
|
+
]);
|
|
741
|
+
const scales = computeScales(spec, chartArea, spec.data);
|
|
742
|
+
const annotations = computeAnnotations(spec, {
|
|
743
|
+
scales,
|
|
744
|
+
chartArea,
|
|
745
|
+
strategy: fullStrategy,
|
|
746
|
+
isDark: false,
|
|
747
|
+
obstacles: [],
|
|
748
|
+
svg: { width: 600, height: 360 },
|
|
749
|
+
measure: heuristicMeasure,
|
|
750
|
+
fontFamily: 'Georgia, serif',
|
|
751
|
+
});
|
|
752
|
+
|
|
753
|
+
for (const ann of annotations) {
|
|
754
|
+
expect(ann.label!.style.fontFamily).toBe('Georgia, serif');
|
|
755
|
+
}
|
|
756
|
+
});
|
|
757
|
+
|
|
758
|
+
it('falls back to the default font stack when no theme font is threaded', () => {
|
|
759
|
+
const spec = makeSpec([{ type: 'text', x: '2020-01-01', y: 20, text: 'Callout' }]);
|
|
760
|
+
const scales = computeScales(spec, chartArea, spec.data);
|
|
761
|
+
const annotations = computeAnnotations(spec, scales, chartArea, fullStrategy);
|
|
762
|
+
|
|
763
|
+
expect(annotations[0].label!.style.fontFamily).toBe(FALLBACK_FONT_FAMILY);
|
|
764
|
+
});
|
|
765
|
+
});
|
|
766
|
+
|
|
767
|
+
describe('text annotation: alignment', () => {
|
|
768
|
+
// Text blocks are never centered: they align on the edge facing the point.
|
|
769
|
+
it('left-aligns multi-line text instead of centering it', () => {
|
|
770
|
+
const spec = makeSpec([
|
|
771
|
+
{ type: 'text', x: '2020-01-01', y: 20, text: 'First line\nSecond line' },
|
|
772
|
+
]);
|
|
773
|
+
const scales = computeScales(spec, chartArea, spec.data);
|
|
774
|
+
const annotations = computeAnnotations(spec, scales, chartArea, fullStrategy);
|
|
775
|
+
|
|
776
|
+
expect(annotations[0].label!.style.textAnchor).toBe('start');
|
|
777
|
+
});
|
|
778
|
+
|
|
779
|
+
it('right-aligns the block when it sits to the left of the point', () => {
|
|
780
|
+
const spec = makeSpec([
|
|
781
|
+
{ type: 'text', x: '2020-01-01', y: 20, text: 'First line\nSecond line', anchor: 'left' },
|
|
782
|
+
]);
|
|
783
|
+
const scales = computeScales(spec, chartArea, spec.data);
|
|
784
|
+
const annotations = computeAnnotations(spec, scales, chartArea, fullStrategy);
|
|
785
|
+
|
|
786
|
+
expect(annotations[0].label!.style.textAnchor).toBe('end');
|
|
287
787
|
});
|
|
288
788
|
});
|