@opendata-ai/openchart-engine 1.2.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 +366 -0
- package/dist/index.js +4227 -0
- package/dist/index.js.map +1 -0
- package/package.json +62 -0
- package/src/__test-fixtures__/specs.ts +124 -0
- package/src/__tests__/axes.test.ts +114 -0
- package/src/__tests__/compile-chart.test.ts +337 -0
- package/src/__tests__/dimensions.test.ts +151 -0
- package/src/__tests__/legend.test.ts +113 -0
- package/src/__tests__/scales.test.ts +109 -0
- package/src/annotations/__tests__/compute.test.ts +454 -0
- package/src/annotations/compute.ts +603 -0
- package/src/charts/__tests__/registry.test.ts +110 -0
- package/src/charts/bar/__tests__/compute.test.ts +294 -0
- package/src/charts/bar/__tests__/labels.test.ts +75 -0
- package/src/charts/bar/compute.ts +205 -0
- package/src/charts/bar/index.ts +33 -0
- package/src/charts/bar/labels.ts +132 -0
- package/src/charts/column/__tests__/compute.test.ts +277 -0
- package/src/charts/column/compute.ts +282 -0
- package/src/charts/column/index.ts +33 -0
- package/src/charts/column/labels.ts +108 -0
- package/src/charts/dot/__tests__/compute.test.ts +344 -0
- package/src/charts/dot/compute.ts +257 -0
- package/src/charts/dot/index.ts +46 -0
- package/src/charts/dot/labels.ts +97 -0
- package/src/charts/line/__tests__/compute.test.ts +437 -0
- package/src/charts/line/__tests__/labels.test.ts +93 -0
- package/src/charts/line/area.ts +288 -0
- package/src/charts/line/compute.ts +177 -0
- package/src/charts/line/index.ts +68 -0
- package/src/charts/line/labels.ts +144 -0
- package/src/charts/pie/__tests__/compute.test.ts +276 -0
- package/src/charts/pie/compute.ts +234 -0
- package/src/charts/pie/index.ts +49 -0
- package/src/charts/pie/labels.ts +142 -0
- package/src/charts/registry.ts +64 -0
- package/src/charts/scatter/__tests__/compute.test.ts +304 -0
- package/src/charts/scatter/__tests__/trendline.test.ts +191 -0
- package/src/charts/scatter/compute.ts +124 -0
- package/src/charts/scatter/index.ts +41 -0
- package/src/charts/scatter/trendline.ts +100 -0
- package/src/charts/utils.ts +120 -0
- package/src/compile.ts +368 -0
- package/src/compiler/__tests__/compile.test.ts +87 -0
- package/src/compiler/__tests__/normalize.test.ts +210 -0
- package/src/compiler/__tests__/validate.test.ts +440 -0
- package/src/compiler/index.ts +47 -0
- package/src/compiler/normalize.ts +269 -0
- package/src/compiler/types.ts +148 -0
- package/src/compiler/validate.ts +581 -0
- package/src/graphs/__tests__/community.test.ts +228 -0
- package/src/graphs/__tests__/compile-graph.test.ts +315 -0
- package/src/graphs/__tests__/encoding.test.ts +314 -0
- package/src/graphs/community.ts +92 -0
- package/src/graphs/compile-graph.ts +291 -0
- package/src/graphs/encoding.ts +302 -0
- package/src/graphs/types.ts +98 -0
- package/src/index.ts +74 -0
- package/src/layout/axes.ts +194 -0
- package/src/layout/dimensions.ts +199 -0
- package/src/layout/gridlines.ts +84 -0
- package/src/layout/scales.ts +426 -0
- package/src/legend/compute.ts +186 -0
- package/src/tables/__tests__/bar-column.test.ts +147 -0
- package/src/tables/__tests__/category-colors.test.ts +153 -0
- package/src/tables/__tests__/compile-table.test.ts +208 -0
- package/src/tables/__tests__/format-cells.test.ts +126 -0
- package/src/tables/__tests__/heatmap.test.ts +124 -0
- package/src/tables/__tests__/pagination.test.ts +78 -0
- package/src/tables/__tests__/search.test.ts +94 -0
- package/src/tables/__tests__/sort.test.ts +107 -0
- package/src/tables/__tests__/sparkline.test.ts +122 -0
- package/src/tables/bar-column.ts +94 -0
- package/src/tables/category-colors.ts +67 -0
- package/src/tables/compile-table.ts +420 -0
- package/src/tables/format-cells.ts +110 -0
- package/src/tables/heatmap.ts +121 -0
- package/src/tables/pagination.ts +46 -0
- package/src/tables/search.ts +66 -0
- package/src/tables/sort.ts +69 -0
- package/src/tables/sparkline.ts +113 -0
- package/src/tables/utils.ts +16 -0
- package/src/tooltips/__tests__/compute.test.ts +328 -0
- package/src/tooltips/compute.ts +231 -0
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
import type { Annotation, LayoutStrategy, Rect } from '@opendata-ai/openchart-core';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import type { NormalizedChartSpec } from '../../compiler/types';
|
|
4
|
+
import { computeScales } from '../../layout/scales';
|
|
5
|
+
import { computeAnnotations } from '../compute';
|
|
6
|
+
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// Fixtures
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
|
|
11
|
+
const chartArea: Rect = { x: 50, y: 20, width: 500, height: 300 };
|
|
12
|
+
|
|
13
|
+
const fullStrategy: LayoutStrategy = {
|
|
14
|
+
labelMode: 'all',
|
|
15
|
+
legendPosition: 'right',
|
|
16
|
+
annotationPosition: 'inline',
|
|
17
|
+
axisLabelDensity: 'full',
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const compactStrategy: LayoutStrategy = {
|
|
21
|
+
labelMode: 'none',
|
|
22
|
+
legendPosition: 'top',
|
|
23
|
+
annotationPosition: 'tooltip-only',
|
|
24
|
+
axisLabelDensity: 'minimal',
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
function makeSpec(annotations: Annotation[]): NormalizedChartSpec {
|
|
28
|
+
return {
|
|
29
|
+
type: 'line',
|
|
30
|
+
data: [
|
|
31
|
+
{ date: '2019-01-01', value: 10 },
|
|
32
|
+
{ date: '2020-01-01', value: 20 },
|
|
33
|
+
{ date: '2021-01-01', value: 30 },
|
|
34
|
+
{ date: '2022-01-01', value: 40 },
|
|
35
|
+
],
|
|
36
|
+
encoding: {
|
|
37
|
+
x: { field: 'date', type: 'temporal' },
|
|
38
|
+
y: { field: 'value', type: 'quantitative' },
|
|
39
|
+
},
|
|
40
|
+
chrome: {},
|
|
41
|
+
annotations,
|
|
42
|
+
responsive: true,
|
|
43
|
+
theme: {},
|
|
44
|
+
darkMode: 'off',
|
|
45
|
+
labels: { density: 'auto', format: '' },
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
// Tests
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
describe('computeAnnotations', () => {
|
|
54
|
+
describe('text annotations', () => {
|
|
55
|
+
it('resolves text annotation to pixel position', () => {
|
|
56
|
+
const spec = makeSpec([{ type: 'text', x: '2020-01-01', y: 20, text: 'Important point' }]);
|
|
57
|
+
const scales = computeScales(spec, chartArea, spec.data);
|
|
58
|
+
const annotations = computeAnnotations(spec, scales, chartArea, fullStrategy);
|
|
59
|
+
|
|
60
|
+
expect(annotations).toHaveLength(1);
|
|
61
|
+
expect(annotations[0].type).toBe('text');
|
|
62
|
+
expect(annotations[0].label).toBeDefined();
|
|
63
|
+
expect(annotations[0].label!.text).toBe('Important point');
|
|
64
|
+
expect(annotations[0].label!.visible).toBe(true);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('text annotation label has a connector to the data point', () => {
|
|
68
|
+
const spec = makeSpec([{ type: 'text', x: '2020-01-01', y: 20, text: 'Note' }]);
|
|
69
|
+
const scales = computeScales(spec, chartArea, spec.data);
|
|
70
|
+
const annotations = computeAnnotations(spec, scales, chartArea, fullStrategy);
|
|
71
|
+
|
|
72
|
+
expect(annotations[0].label!.connector).toBeDefined();
|
|
73
|
+
expect(annotations[0].label!.connector!.from).toBeDefined();
|
|
74
|
+
expect(annotations[0].label!.connector!.to).toBeDefined();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('returns null for text annotation with invalid data value', () => {
|
|
78
|
+
const spec = makeSpec([{ type: 'text', x: 'not-a-date', y: 999, text: 'Missing' }]);
|
|
79
|
+
const scales = computeScales(spec, chartArea, spec.data);
|
|
80
|
+
const annotations = computeAnnotations(spec, scales, chartArea, fullStrategy);
|
|
81
|
+
|
|
82
|
+
// Invalid date should result in null annotation (filtered out)
|
|
83
|
+
expect(annotations).toHaveLength(0);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe('range annotations', () => {
|
|
88
|
+
it('resolves x-range annotation to a rect', () => {
|
|
89
|
+
const spec = makeSpec([
|
|
90
|
+
{ type: 'range', x1: '2020-01-01', x2: '2021-01-01', label: 'Period' },
|
|
91
|
+
]);
|
|
92
|
+
const scales = computeScales(spec, chartArea, spec.data);
|
|
93
|
+
const annotations = computeAnnotations(spec, scales, chartArea, fullStrategy);
|
|
94
|
+
|
|
95
|
+
expect(annotations).toHaveLength(1);
|
|
96
|
+
expect(annotations[0].type).toBe('range');
|
|
97
|
+
expect(annotations[0].rect).toBeDefined();
|
|
98
|
+
expect(annotations[0].rect!.width).toBeGreaterThan(0);
|
|
99
|
+
// Height should span the full chart area for x-only range
|
|
100
|
+
expect(annotations[0].rect!.height).toBe(chartArea.height);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('range annotation has a label', () => {
|
|
104
|
+
const spec = makeSpec([
|
|
105
|
+
{ type: 'range', x1: '2020-01-01', x2: '2021-01-01', label: 'Recession' },
|
|
106
|
+
]);
|
|
107
|
+
const scales = computeScales(spec, chartArea, spec.data);
|
|
108
|
+
const annotations = computeAnnotations(spec, scales, chartArea, fullStrategy);
|
|
109
|
+
|
|
110
|
+
expect(annotations[0].label).toBeDefined();
|
|
111
|
+
expect(annotations[0].label!.text).toBe('Recession');
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('range has fill and opacity', () => {
|
|
115
|
+
const spec = makeSpec([
|
|
116
|
+
{
|
|
117
|
+
type: 'range',
|
|
118
|
+
x1: '2020-01-01',
|
|
119
|
+
x2: '2021-01-01',
|
|
120
|
+
fill: '#ff0000',
|
|
121
|
+
opacity: 0.2,
|
|
122
|
+
},
|
|
123
|
+
]);
|
|
124
|
+
const scales = computeScales(spec, chartArea, spec.data);
|
|
125
|
+
const annotations = computeAnnotations(spec, scales, chartArea, fullStrategy);
|
|
126
|
+
|
|
127
|
+
expect(annotations[0].fill).toBe('#ff0000');
|
|
128
|
+
expect(annotations[0].opacity).toBe(0.2);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('uses default fill and opacity when not specified', () => {
|
|
132
|
+
const spec = makeSpec([{ type: 'range', x1: '2020-01-01', x2: '2021-01-01' }]);
|
|
133
|
+
const scales = computeScales(spec, chartArea, spec.data);
|
|
134
|
+
const annotations = computeAnnotations(spec, scales, chartArea, fullStrategy);
|
|
135
|
+
|
|
136
|
+
expect(annotations[0].fill).toBeDefined();
|
|
137
|
+
expect(annotations[0].opacity).toBeDefined();
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
describe('reference line annotations', () => {
|
|
142
|
+
it('resolves horizontal refline at y value', () => {
|
|
143
|
+
const spec = makeSpec([{ type: 'refline', y: 20, label: 'Target' }]);
|
|
144
|
+
const scales = computeScales(spec, chartArea, spec.data);
|
|
145
|
+
const annotations = computeAnnotations(spec, scales, chartArea, fullStrategy);
|
|
146
|
+
|
|
147
|
+
expect(annotations).toHaveLength(1);
|
|
148
|
+
expect(annotations[0].type).toBe('refline');
|
|
149
|
+
expect(annotations[0].line).toBeDefined();
|
|
150
|
+
// Horizontal line: start.x = chartArea.x, end.x = chartArea.x + chartArea.width
|
|
151
|
+
expect(annotations[0].line!.start.x).toBe(chartArea.x);
|
|
152
|
+
expect(annotations[0].line!.end.x).toBe(chartArea.x + chartArea.width);
|
|
153
|
+
// Both y values should be the same (horizontal line)
|
|
154
|
+
expect(annotations[0].line!.start.y).toBe(annotations[0].line!.end.y);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('resolves vertical refline at x value', () => {
|
|
158
|
+
const spec = makeSpec([{ type: 'refline', x: '2020-06-01', label: 'Event' }]);
|
|
159
|
+
const scales = computeScales(spec, chartArea, spec.data);
|
|
160
|
+
const annotations = computeAnnotations(spec, scales, chartArea, fullStrategy);
|
|
161
|
+
|
|
162
|
+
expect(annotations).toHaveLength(1);
|
|
163
|
+
expect(annotations[0].line).toBeDefined();
|
|
164
|
+
// Vertical line: start.y = chartArea.y, end.y = chartArea.y + chartArea.height
|
|
165
|
+
expect(annotations[0].line!.start.y).toBe(chartArea.y);
|
|
166
|
+
expect(annotations[0].line!.end.y).toBe(chartArea.y + chartArea.height);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('refline has dashed style by default', () => {
|
|
170
|
+
const spec = makeSpec([{ type: 'refline', y: 20 }]);
|
|
171
|
+
const scales = computeScales(spec, chartArea, spec.data);
|
|
172
|
+
const annotations = computeAnnotations(spec, scales, chartArea, fullStrategy);
|
|
173
|
+
|
|
174
|
+
expect(annotations[0].strokeDasharray).toBeDefined();
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('solid refline has no dasharray', () => {
|
|
178
|
+
const spec = makeSpec([{ type: 'refline', y: 20, style: 'solid' }]);
|
|
179
|
+
const scales = computeScales(spec, chartArea, spec.data);
|
|
180
|
+
const annotations = computeAnnotations(spec, scales, chartArea, fullStrategy);
|
|
181
|
+
|
|
182
|
+
expect(annotations[0].strokeDasharray).toBeUndefined();
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('returns nothing for refline with neither x nor y', () => {
|
|
186
|
+
const spec = makeSpec([{ type: 'refline', label: 'Orphan' } as Annotation]);
|
|
187
|
+
const scales = computeScales(spec, chartArea, spec.data);
|
|
188
|
+
const annotations = computeAnnotations(spec, scales, chartArea, fullStrategy);
|
|
189
|
+
|
|
190
|
+
expect(annotations).toHaveLength(0);
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
describe('responsive behavior', () => {
|
|
195
|
+
it('returns empty annotations at compact breakpoint', () => {
|
|
196
|
+
const spec = makeSpec([
|
|
197
|
+
{ type: 'text', x: '2020-01-01', y: 20, text: 'Hidden' },
|
|
198
|
+
{ type: 'range', x1: '2020-01-01', x2: '2021-01-01' },
|
|
199
|
+
{ type: 'refline', y: 20 },
|
|
200
|
+
]);
|
|
201
|
+
const scales = computeScales(spec, chartArea, spec.data);
|
|
202
|
+
const annotations = computeAnnotations(spec, scales, chartArea, compactStrategy);
|
|
203
|
+
|
|
204
|
+
expect(annotations).toHaveLength(0);
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
describe('multiple annotations', () => {
|
|
209
|
+
it('resolves multiple mixed annotations', () => {
|
|
210
|
+
const spec = makeSpec([
|
|
211
|
+
{ type: 'text', x: '2020-01-01', y: 20, text: 'Note' },
|
|
212
|
+
{ type: 'range', x1: '2020-01-01', x2: '2021-01-01' },
|
|
213
|
+
{ type: 'refline', y: 25 },
|
|
214
|
+
]);
|
|
215
|
+
const scales = computeScales(spec, chartArea, spec.data);
|
|
216
|
+
const annotations = computeAnnotations(spec, scales, chartArea, fullStrategy);
|
|
217
|
+
|
|
218
|
+
expect(annotations).toHaveLength(3);
|
|
219
|
+
expect(annotations.map((a) => a.type)).toEqual(['text', 'range', 'refline']);
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
describe('empty annotations', () => {
|
|
224
|
+
it('returns empty array when spec has no annotations', () => {
|
|
225
|
+
const spec = makeSpec([]);
|
|
226
|
+
const scales = computeScales(spec, chartArea, spec.data);
|
|
227
|
+
const annotations = computeAnnotations(spec, scales, chartArea, fullStrategy);
|
|
228
|
+
|
|
229
|
+
expect(annotations).toHaveLength(0);
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
// -----------------------------------------------------------------
|
|
234
|
+
// Fine-grained positioning (offset, anchor, connector, zIndex)
|
|
235
|
+
// -----------------------------------------------------------------
|
|
236
|
+
|
|
237
|
+
describe('text annotation offset', () => {
|
|
238
|
+
it('applies dx/dy offset to text annotation position', () => {
|
|
239
|
+
const spec = makeSpec([
|
|
240
|
+
{
|
|
241
|
+
type: 'text',
|
|
242
|
+
x: '2020-01-01',
|
|
243
|
+
y: 20,
|
|
244
|
+
text: 'Offset label',
|
|
245
|
+
offset: { dx: 20, dy: -30 },
|
|
246
|
+
},
|
|
247
|
+
]);
|
|
248
|
+
const scales = computeScales(spec, chartArea, spec.data);
|
|
249
|
+
|
|
250
|
+
// Compute with and without offset
|
|
251
|
+
const specNoOffset = makeSpec([{ type: 'text', x: '2020-01-01', y: 20, text: 'No offset' }]);
|
|
252
|
+
const withOffset = computeAnnotations(spec, scales, chartArea, fullStrategy);
|
|
253
|
+
const withoutOffset = computeAnnotations(specNoOffset, scales, chartArea, fullStrategy);
|
|
254
|
+
|
|
255
|
+
expect(withOffset).toHaveLength(1);
|
|
256
|
+
expect(withoutOffset).toHaveLength(1);
|
|
257
|
+
|
|
258
|
+
// The offset annotation should be shifted by the dx/dy amount
|
|
259
|
+
const dx = withOffset[0].label!.x - withoutOffset[0].label!.x;
|
|
260
|
+
const dy = withOffset[0].label!.y - withoutOffset[0].label!.y;
|
|
261
|
+
expect(dx).toBe(20);
|
|
262
|
+
expect(dy).toBe(-30);
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
describe('text annotation anchor', () => {
|
|
267
|
+
it('anchor "top" places label above the data point', () => {
|
|
268
|
+
const spec = makeSpec([
|
|
269
|
+
{ type: 'text', x: '2020-01-01', y: 20, text: 'Above', anchor: 'top' },
|
|
270
|
+
]);
|
|
271
|
+
const scales = computeScales(spec, chartArea, spec.data);
|
|
272
|
+
|
|
273
|
+
const _px = scales.x?.scale(new Date('2020-01-01'));
|
|
274
|
+
const py = scales.y?.scale(20);
|
|
275
|
+
const annotations = computeAnnotations(spec, scales, chartArea, fullStrategy);
|
|
276
|
+
|
|
277
|
+
expect(annotations).toHaveLength(1);
|
|
278
|
+
// Label y should be above the data point y
|
|
279
|
+
expect(annotations[0].label!.y).toBeLessThan(py as number);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it('anchor "bottom" places label below the data point', () => {
|
|
283
|
+
const spec = makeSpec([
|
|
284
|
+
{ type: 'text', x: '2020-01-01', y: 20, text: 'Below', anchor: 'bottom' },
|
|
285
|
+
]);
|
|
286
|
+
const scales = computeScales(spec, chartArea, spec.data);
|
|
287
|
+
|
|
288
|
+
const py = scales.y?.scale(20);
|
|
289
|
+
const annotations = computeAnnotations(spec, scales, chartArea, fullStrategy);
|
|
290
|
+
|
|
291
|
+
expect(annotations).toHaveLength(1);
|
|
292
|
+
// Label y should be below the data point y
|
|
293
|
+
expect(annotations[0].label!.y).toBeGreaterThan(py as number);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it('anchor "left" places label to the left of the data point', () => {
|
|
297
|
+
const spec = makeSpec([
|
|
298
|
+
{ type: 'text', x: '2020-01-01', y: 20, text: 'Left', anchor: 'left' },
|
|
299
|
+
]);
|
|
300
|
+
const scales = computeScales(spec, chartArea, spec.data);
|
|
301
|
+
|
|
302
|
+
const px = scales.x?.scale(new Date('2020-01-01'));
|
|
303
|
+
const annotations = computeAnnotations(spec, scales, chartArea, fullStrategy);
|
|
304
|
+
|
|
305
|
+
expect(annotations).toHaveLength(1);
|
|
306
|
+
// Label x should be to the left of the data point x
|
|
307
|
+
expect(annotations[0].label!.x).toBeLessThan(px as number);
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it('anchor "right" places label to the right of the data point', () => {
|
|
311
|
+
const spec = makeSpec([
|
|
312
|
+
{ type: 'text', x: '2020-01-01', y: 20, text: 'Right', anchor: 'right' },
|
|
313
|
+
]);
|
|
314
|
+
const scales = computeScales(spec, chartArea, spec.data);
|
|
315
|
+
|
|
316
|
+
const px = scales.x?.scale(new Date('2020-01-01'));
|
|
317
|
+
const annotations = computeAnnotations(spec, scales, chartArea, fullStrategy);
|
|
318
|
+
|
|
319
|
+
expect(annotations).toHaveLength(1);
|
|
320
|
+
// Label x should be to the right of the data point x
|
|
321
|
+
expect(annotations[0].label!.x).toBeGreaterThan(px as number);
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
describe('text annotation connector', () => {
|
|
326
|
+
it('connector is present by default', () => {
|
|
327
|
+
const spec = makeSpec([{ type: 'text', x: '2020-01-01', y: 20, text: 'Has connector' }]);
|
|
328
|
+
const scales = computeScales(spec, chartArea, spec.data);
|
|
329
|
+
const annotations = computeAnnotations(spec, scales, chartArea, fullStrategy);
|
|
330
|
+
|
|
331
|
+
expect(annotations[0].label!.connector).toBeDefined();
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it('connector can be disabled', () => {
|
|
335
|
+
const spec = makeSpec([
|
|
336
|
+
{ type: 'text', x: '2020-01-01', y: 20, text: 'No connector', connector: false },
|
|
337
|
+
]);
|
|
338
|
+
const scales = computeScales(spec, chartArea, spec.data);
|
|
339
|
+
const annotations = computeAnnotations(spec, scales, chartArea, fullStrategy);
|
|
340
|
+
|
|
341
|
+
expect(annotations[0].label!.connector).toBeUndefined();
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
it('connector "to" points near the data position with a small gap', () => {
|
|
345
|
+
const spec = makeSpec([{ type: 'text', x: '2020-01-01', y: 20, text: 'Connector check' }]);
|
|
346
|
+
const scales = computeScales(spec, chartArea, spec.data);
|
|
347
|
+
|
|
348
|
+
const px = scales.x?.scale(new Date('2020-01-01')) as number;
|
|
349
|
+
const py = scales.y?.scale(20) as number;
|
|
350
|
+
const annotations = computeAnnotations(spec, scales, chartArea, fullStrategy);
|
|
351
|
+
|
|
352
|
+
const connector = annotations[0].label!.connector!;
|
|
353
|
+
// The "to" endpoint is pulled back along the connector direction by a
|
|
354
|
+
// small gap (~4px), so it won't exactly match the data point.
|
|
355
|
+
const dx = px - connector.to.x;
|
|
356
|
+
const dy = py - connector.to.y;
|
|
357
|
+
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
358
|
+
expect(dist).toBeGreaterThan(0);
|
|
359
|
+
expect(dist).toBeLessThanOrEqual(5);
|
|
360
|
+
});
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
describe('zIndex', () => {
|
|
364
|
+
it('annotations are sorted by zIndex', () => {
|
|
365
|
+
const spec = makeSpec([
|
|
366
|
+
{ type: 'text', x: '2020-01-01', y: 20, text: 'Low', zIndex: 1 },
|
|
367
|
+
{ type: 'refline', y: 25, zIndex: 10 },
|
|
368
|
+
{ type: 'range', x1: '2020-01-01', x2: '2021-01-01', zIndex: 5 },
|
|
369
|
+
]);
|
|
370
|
+
const scales = computeScales(spec, chartArea, spec.data);
|
|
371
|
+
const annotations = computeAnnotations(spec, scales, chartArea, fullStrategy);
|
|
372
|
+
|
|
373
|
+
expect(annotations).toHaveLength(3);
|
|
374
|
+
// Should be sorted: zIndex 1, 5, 10
|
|
375
|
+
expect(annotations[0].zIndex).toBe(1);
|
|
376
|
+
expect(annotations[1].zIndex).toBe(5);
|
|
377
|
+
expect(annotations[2].zIndex).toBe(10);
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
it('annotations without zIndex default to 0', () => {
|
|
381
|
+
const spec = makeSpec([
|
|
382
|
+
{ type: 'text', x: '2020-01-01', y: 20, text: 'After', zIndex: 5 },
|
|
383
|
+
{ type: 'refline', y: 25 }, // no zIndex, defaults to 0
|
|
384
|
+
]);
|
|
385
|
+
const scales = computeScales(spec, chartArea, spec.data);
|
|
386
|
+
const annotations = computeAnnotations(spec, scales, chartArea, fullStrategy);
|
|
387
|
+
|
|
388
|
+
expect(annotations).toHaveLength(2);
|
|
389
|
+
// The refline (no zIndex = 0) should come before text (zIndex 5)
|
|
390
|
+
expect(annotations[0].type).toBe('refline');
|
|
391
|
+
expect(annotations[1].type).toBe('text');
|
|
392
|
+
});
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
describe('range annotation labelOffset', () => {
|
|
396
|
+
it('applies labelOffset to range annotation label', () => {
|
|
397
|
+
const spec = makeSpec([
|
|
398
|
+
{
|
|
399
|
+
type: 'range',
|
|
400
|
+
x1: '2020-01-01',
|
|
401
|
+
x2: '2021-01-01',
|
|
402
|
+
label: 'Shifted',
|
|
403
|
+
labelOffset: { dx: 20, dy: 10 },
|
|
404
|
+
},
|
|
405
|
+
]);
|
|
406
|
+
const specNoOffset = makeSpec([
|
|
407
|
+
{
|
|
408
|
+
type: 'range',
|
|
409
|
+
x1: '2020-01-01',
|
|
410
|
+
x2: '2021-01-01',
|
|
411
|
+
label: 'Not shifted',
|
|
412
|
+
},
|
|
413
|
+
]);
|
|
414
|
+
const scales = computeScales(spec, chartArea, spec.data);
|
|
415
|
+
|
|
416
|
+
const withOffset = computeAnnotations(spec, scales, chartArea, fullStrategy);
|
|
417
|
+
const withoutOffset = computeAnnotations(specNoOffset, scales, chartArea, fullStrategy);
|
|
418
|
+
|
|
419
|
+
const dx = withOffset[0].label!.x - withoutOffset[0].label!.x;
|
|
420
|
+
const dy = withOffset[0].label!.y - withoutOffset[0].label!.y;
|
|
421
|
+
expect(dx).toBe(20);
|
|
422
|
+
expect(dy).toBe(10);
|
|
423
|
+
});
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
describe('refline annotation labelOffset', () => {
|
|
427
|
+
it('applies labelOffset to refline label', () => {
|
|
428
|
+
const spec = makeSpec([
|
|
429
|
+
{
|
|
430
|
+
type: 'refline',
|
|
431
|
+
y: 20,
|
|
432
|
+
label: 'Shifted',
|
|
433
|
+
labelOffset: { dx: 15, dy: -10 },
|
|
434
|
+
},
|
|
435
|
+
]);
|
|
436
|
+
const specNoOffset = makeSpec([
|
|
437
|
+
{
|
|
438
|
+
type: 'refline',
|
|
439
|
+
y: 20,
|
|
440
|
+
label: 'Not shifted',
|
|
441
|
+
},
|
|
442
|
+
]);
|
|
443
|
+
const scales = computeScales(spec, chartArea, spec.data);
|
|
444
|
+
|
|
445
|
+
const withOffset = computeAnnotations(spec, scales, chartArea, fullStrategy);
|
|
446
|
+
const withoutOffset = computeAnnotations(specNoOffset, scales, chartArea, fullStrategy);
|
|
447
|
+
|
|
448
|
+
const dx = withOffset[0].label!.x - withoutOffset[0].label!.x;
|
|
449
|
+
const dy = withOffset[0].label!.y - withoutOffset[0].label!.y;
|
|
450
|
+
expect(dx).toBe(15);
|
|
451
|
+
expect(dy).toBe(-10);
|
|
452
|
+
});
|
|
453
|
+
});
|
|
454
|
+
});
|