@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.
Files changed (85) hide show
  1. package/dist/index.d.ts +366 -0
  2. package/dist/index.js +4227 -0
  3. package/dist/index.js.map +1 -0
  4. package/package.json +62 -0
  5. package/src/__test-fixtures__/specs.ts +124 -0
  6. package/src/__tests__/axes.test.ts +114 -0
  7. package/src/__tests__/compile-chart.test.ts +337 -0
  8. package/src/__tests__/dimensions.test.ts +151 -0
  9. package/src/__tests__/legend.test.ts +113 -0
  10. package/src/__tests__/scales.test.ts +109 -0
  11. package/src/annotations/__tests__/compute.test.ts +454 -0
  12. package/src/annotations/compute.ts +603 -0
  13. package/src/charts/__tests__/registry.test.ts +110 -0
  14. package/src/charts/bar/__tests__/compute.test.ts +294 -0
  15. package/src/charts/bar/__tests__/labels.test.ts +75 -0
  16. package/src/charts/bar/compute.ts +205 -0
  17. package/src/charts/bar/index.ts +33 -0
  18. package/src/charts/bar/labels.ts +132 -0
  19. package/src/charts/column/__tests__/compute.test.ts +277 -0
  20. package/src/charts/column/compute.ts +282 -0
  21. package/src/charts/column/index.ts +33 -0
  22. package/src/charts/column/labels.ts +108 -0
  23. package/src/charts/dot/__tests__/compute.test.ts +344 -0
  24. package/src/charts/dot/compute.ts +257 -0
  25. package/src/charts/dot/index.ts +46 -0
  26. package/src/charts/dot/labels.ts +97 -0
  27. package/src/charts/line/__tests__/compute.test.ts +437 -0
  28. package/src/charts/line/__tests__/labels.test.ts +93 -0
  29. package/src/charts/line/area.ts +288 -0
  30. package/src/charts/line/compute.ts +177 -0
  31. package/src/charts/line/index.ts +68 -0
  32. package/src/charts/line/labels.ts +144 -0
  33. package/src/charts/pie/__tests__/compute.test.ts +276 -0
  34. package/src/charts/pie/compute.ts +234 -0
  35. package/src/charts/pie/index.ts +49 -0
  36. package/src/charts/pie/labels.ts +142 -0
  37. package/src/charts/registry.ts +64 -0
  38. package/src/charts/scatter/__tests__/compute.test.ts +304 -0
  39. package/src/charts/scatter/__tests__/trendline.test.ts +191 -0
  40. package/src/charts/scatter/compute.ts +124 -0
  41. package/src/charts/scatter/index.ts +41 -0
  42. package/src/charts/scatter/trendline.ts +100 -0
  43. package/src/charts/utils.ts +120 -0
  44. package/src/compile.ts +368 -0
  45. package/src/compiler/__tests__/compile.test.ts +87 -0
  46. package/src/compiler/__tests__/normalize.test.ts +210 -0
  47. package/src/compiler/__tests__/validate.test.ts +440 -0
  48. package/src/compiler/index.ts +47 -0
  49. package/src/compiler/normalize.ts +269 -0
  50. package/src/compiler/types.ts +148 -0
  51. package/src/compiler/validate.ts +581 -0
  52. package/src/graphs/__tests__/community.test.ts +228 -0
  53. package/src/graphs/__tests__/compile-graph.test.ts +315 -0
  54. package/src/graphs/__tests__/encoding.test.ts +314 -0
  55. package/src/graphs/community.ts +92 -0
  56. package/src/graphs/compile-graph.ts +291 -0
  57. package/src/graphs/encoding.ts +302 -0
  58. package/src/graphs/types.ts +98 -0
  59. package/src/index.ts +74 -0
  60. package/src/layout/axes.ts +194 -0
  61. package/src/layout/dimensions.ts +199 -0
  62. package/src/layout/gridlines.ts +84 -0
  63. package/src/layout/scales.ts +426 -0
  64. package/src/legend/compute.ts +186 -0
  65. package/src/tables/__tests__/bar-column.test.ts +147 -0
  66. package/src/tables/__tests__/category-colors.test.ts +153 -0
  67. package/src/tables/__tests__/compile-table.test.ts +208 -0
  68. package/src/tables/__tests__/format-cells.test.ts +126 -0
  69. package/src/tables/__tests__/heatmap.test.ts +124 -0
  70. package/src/tables/__tests__/pagination.test.ts +78 -0
  71. package/src/tables/__tests__/search.test.ts +94 -0
  72. package/src/tables/__tests__/sort.test.ts +107 -0
  73. package/src/tables/__tests__/sparkline.test.ts +122 -0
  74. package/src/tables/bar-column.ts +94 -0
  75. package/src/tables/category-colors.ts +67 -0
  76. package/src/tables/compile-table.ts +420 -0
  77. package/src/tables/format-cells.ts +110 -0
  78. package/src/tables/heatmap.ts +121 -0
  79. package/src/tables/pagination.ts +46 -0
  80. package/src/tables/search.ts +66 -0
  81. package/src/tables/sort.ts +69 -0
  82. package/src/tables/sparkline.ts +113 -0
  83. package/src/tables/utils.ts +16 -0
  84. package/src/tooltips/__tests__/compute.test.ts +328 -0
  85. package/src/tooltips/compute.ts +231 -0
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Column chart module (vertical bars).
3
+ *
4
+ * Exports the column chart renderer and computation functions.
5
+ */
6
+
7
+ import type { Mark } from '@opendata-ai/openchart-core';
8
+ import type { ChartRenderer } from '../registry';
9
+ import { computeColumnMarks } from './compute';
10
+ import { computeColumnLabels } from './labels';
11
+
12
+ // ---------------------------------------------------------------------------
13
+ // Column chart renderer
14
+ // ---------------------------------------------------------------------------
15
+
16
+ export const columnRenderer: ChartRenderer = (spec, scales, chartArea, strategy, _theme) => {
17
+ const marks = computeColumnMarks(spec, scales, chartArea, strategy);
18
+
19
+ // Compute and attach value labels (respects spec.labels.density)
20
+ const labels = computeColumnLabels(marks, chartArea, spec.labels.density);
21
+ for (let i = 0; i < marks.length && i < labels.length; i++) {
22
+ marks[i].label = labels[i];
23
+ }
24
+
25
+ return marks as Mark[];
26
+ };
27
+
28
+ // ---------------------------------------------------------------------------
29
+ // Public exports
30
+ // ---------------------------------------------------------------------------
31
+
32
+ export { computeColumnMarks } from './compute';
33
+ export { computeColumnLabels } from './labels';
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Column chart label computation.
3
+ *
4
+ * Produces value labels positioned above each column (for positive values)
5
+ * or below (for negative values).
6
+ *
7
+ * Respects the spec's label density setting:
8
+ * - 'all': show every label, skip collision detection
9
+ * - 'auto': existing behavior (collision detection)
10
+ * - 'endpoints': first and last columns only
11
+ * - 'none': return empty array
12
+ */
13
+
14
+ import type {
15
+ LabelCandidate,
16
+ LabelDensity,
17
+ RectMark,
18
+ ResolvedLabel,
19
+ } from '@opendata-ai/openchart-core';
20
+ import { estimateTextWidth, resolveCollisions } from '@opendata-ai/openchart-core';
21
+
22
+ // ---------------------------------------------------------------------------
23
+ // Constants
24
+ // ---------------------------------------------------------------------------
25
+
26
+ const LABEL_FONT_SIZE = 10;
27
+ const LABEL_FONT_WEIGHT = 600;
28
+ const LABEL_OFFSET_Y = 6;
29
+
30
+ // ---------------------------------------------------------------------------
31
+ // Public API
32
+ // ---------------------------------------------------------------------------
33
+
34
+ /**
35
+ * Compute value labels for column marks.
36
+ *
37
+ * For each column, the value is placed centered above the column top.
38
+ */
39
+ export function computeColumnLabels(
40
+ marks: RectMark[],
41
+ _chartArea: { x: number; y: number; width: number; height: number },
42
+ density: LabelDensity = 'auto',
43
+ ): ResolvedLabel[] {
44
+ // 'none': no labels at all
45
+ if (density === 'none') return [];
46
+
47
+ // Filter marks for 'endpoints' density
48
+ const targetMarks =
49
+ density === 'endpoints' && marks.length > 1 ? [marks[0], marks[marks.length - 1]] : marks;
50
+
51
+ const candidates: LabelCandidate[] = [];
52
+
53
+ for (const mark of targetMarks) {
54
+ // Extract the display value from the aria label.
55
+ // Format is "category: value" or "category, group: value".
56
+ // Use the last colon to split, which handles colons in category names.
57
+ const ariaLabel = mark.aria.label;
58
+ const lastColon = ariaLabel.lastIndexOf(':');
59
+ const valuePart = lastColon >= 0 ? ariaLabel.slice(lastColon + 1).trim() : '';
60
+ if (!valuePart) continue;
61
+
62
+ const numericValue = parseFloat(valuePart);
63
+ const isNegative = Number.isFinite(numericValue) && numericValue < 0;
64
+
65
+ const textWidth = estimateTextWidth(valuePart, LABEL_FONT_SIZE, LABEL_FONT_WEIGHT);
66
+ const textHeight = LABEL_FONT_SIZE * 1.2;
67
+
68
+ // For positive values, place label above the column top.
69
+ // For negative values, place label below the column bottom.
70
+ const anchorX = mark.x + mark.width / 2 - textWidth / 2;
71
+ const anchorY = isNegative
72
+ ? mark.y + mark.height + LABEL_OFFSET_Y
73
+ : mark.y - LABEL_OFFSET_Y - textHeight;
74
+
75
+ candidates.push({
76
+ text: valuePart,
77
+ anchorX,
78
+ anchorY,
79
+ width: textWidth,
80
+ height: textHeight,
81
+ priority: 'data',
82
+ style: {
83
+ fontFamily: 'system-ui, -apple-system, sans-serif',
84
+ fontSize: LABEL_FONT_SIZE,
85
+ fontWeight: LABEL_FONT_WEIGHT,
86
+ fill: mark.fill,
87
+ lineHeight: 1.2,
88
+ textAnchor: 'middle',
89
+ dominantBaseline: isNegative ? 'hanging' : 'auto',
90
+ },
91
+ });
92
+ }
93
+
94
+ if (candidates.length === 0) return [];
95
+
96
+ // 'all': skip collision detection, mark everything visible
97
+ if (density === 'all') {
98
+ return candidates.map((c) => ({
99
+ text: c.text,
100
+ x: c.anchorX,
101
+ y: c.anchorY,
102
+ style: c.style,
103
+ visible: true,
104
+ }));
105
+ }
106
+
107
+ return resolveCollisions(candidates);
108
+ }
@@ -0,0 +1,344 @@
1
+ import type { LayoutStrategy, PointMark, Rect, RectMark } 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 { computeDotMarks } from '../compute';
6
+ import { computeDotLabels } from '../labels';
7
+
8
+ // ---------------------------------------------------------------------------
9
+ // Shared fixtures
10
+ // ---------------------------------------------------------------------------
11
+
12
+ const chartArea: Rect = { x: 80, y: 20, width: 500, height: 300 };
13
+
14
+ const fullStrategy: LayoutStrategy = {
15
+ labelMode: 'all',
16
+ legendPosition: 'right',
17
+ annotationPosition: 'inline',
18
+ axisLabelDensity: 'full',
19
+ };
20
+
21
+ function makeSimpleDotSpec(): NormalizedChartSpec {
22
+ return {
23
+ type: 'dot',
24
+ data: [
25
+ { country: 'USA', score: 85 },
26
+ { country: 'UK', score: 72 },
27
+ { country: 'Japan', score: 90 },
28
+ { country: 'France', score: 68 },
29
+ ],
30
+ encoding: {
31
+ x: { field: 'score', type: 'quantitative' },
32
+ y: { field: 'country', type: 'nominal' },
33
+ },
34
+ chrome: {},
35
+ annotations: [],
36
+ responsive: true,
37
+ theme: {},
38
+ darkMode: 'off',
39
+ labels: { density: 'auto', format: '' },
40
+ };
41
+ }
42
+
43
+ function makeColoredDotSpec(): NormalizedChartSpec {
44
+ return {
45
+ type: 'dot',
46
+ data: [
47
+ { item: 'Revenue', value: 120, status: 'good' },
48
+ { item: 'Costs', value: 80, status: 'neutral' },
49
+ { item: 'Profit', value: 40, status: 'good' },
50
+ { item: 'Debt', value: -10, status: 'bad' },
51
+ ],
52
+ encoding: {
53
+ x: { field: 'value', type: 'quantitative' },
54
+ y: { field: 'item', type: 'nominal' },
55
+ color: { field: 'status', type: 'nominal' },
56
+ },
57
+ chrome: {},
58
+ annotations: [],
59
+ responsive: true,
60
+ theme: {},
61
+ darkMode: 'off',
62
+ labels: { density: 'auto', format: '' },
63
+ };
64
+ }
65
+
66
+ // ---------------------------------------------------------------------------
67
+ // computeDotMarks tests
68
+ // ---------------------------------------------------------------------------
69
+
70
+ describe('computeDotMarks', () => {
71
+ describe('simple dot plot', () => {
72
+ it('produces PointMarks and RectMarks (stems)', () => {
73
+ const spec = makeSimpleDotSpec();
74
+ const scales = computeScales(spec, chartArea, spec.data);
75
+ const marks = computeDotMarks(spec, scales, chartArea, fullStrategy);
76
+
77
+ const points = marks.filter((m): m is PointMark => m.type === 'point');
78
+ const stems = marks.filter((m): m is RectMark => m.type === 'rect');
79
+
80
+ expect(points).toHaveLength(4);
81
+ expect(stems).toHaveLength(4);
82
+ });
83
+
84
+ it('dot positions reflect data values (higher score = further right)', () => {
85
+ const spec = makeSimpleDotSpec();
86
+ const scales = computeScales(spec, chartArea, spec.data);
87
+ const marks = computeDotMarks(spec, scales, chartArea, fullStrategy);
88
+
89
+ const points = marks.filter((m): m is PointMark => m.type === 'point');
90
+ const japan = points.find((m) => m.aria.label.includes('Japan'))!;
91
+ const france = points.find((m) => m.aria.label.includes('France'))!;
92
+
93
+ // Japan (90) should be further right than France (68)
94
+ expect(japan.cx).toBeGreaterThan(france.cx);
95
+ });
96
+
97
+ it('stems connect baseline to dot positions', () => {
98
+ const spec = makeSimpleDotSpec();
99
+ const scales = computeScales(spec, chartArea, spec.data);
100
+ const marks = computeDotMarks(spec, scales, chartArea, fullStrategy);
101
+
102
+ const stems = marks.filter((m): m is RectMark => m.type === 'rect');
103
+ for (const stem of stems) {
104
+ expect(stem.width).toBeGreaterThan(0);
105
+ expect(stem.height).toBe(2); // STEM_WIDTH
106
+ }
107
+ });
108
+
109
+ it('dots are vertically centered within their band', () => {
110
+ const spec = makeSimpleDotSpec();
111
+ const scales = computeScales(spec, chartArea, spec.data);
112
+ const marks = computeDotMarks(spec, scales, chartArea, fullStrategy);
113
+
114
+ const points = marks.filter((m): m is PointMark => m.type === 'point');
115
+ // All dots should be at different y positions (different categories)
116
+ const yPositions = points.map((p) => p.cy);
117
+ const uniqueY = new Set(yPositions);
118
+ expect(uniqueY.size).toBe(4);
119
+ });
120
+
121
+ it('dots have consistent radius', () => {
122
+ const spec = makeSimpleDotSpec();
123
+ const scales = computeScales(spec, chartArea, spec.data);
124
+ const marks = computeDotMarks(spec, scales, chartArea, fullStrategy);
125
+
126
+ const points = marks.filter((m): m is PointMark => m.type === 'point');
127
+ const radii = new Set(points.map((p) => p.r));
128
+ expect(radii.size).toBe(1);
129
+ });
130
+
131
+ it('each dot has an aria label with category and value', () => {
132
+ const spec = makeSimpleDotSpec();
133
+ const scales = computeScales(spec, chartArea, spec.data);
134
+ const marks = computeDotMarks(spec, scales, chartArea, fullStrategy);
135
+
136
+ const points = marks.filter((m): m is PointMark => m.type === 'point');
137
+ expect(points[0].aria.label).toContain('USA');
138
+ expect(points[0].aria.label).toContain('85');
139
+ });
140
+ });
141
+
142
+ describe('dumbbell (multi-series)', () => {
143
+ function makeDumbbellSpec(): NormalizedChartSpec {
144
+ return {
145
+ type: 'dot',
146
+ data: [
147
+ { country: 'USA', rate: 78, gender: 'Male' },
148
+ { country: 'USA', rate: 82, gender: 'Female' },
149
+ { country: 'Japan', rate: 85, gender: 'Male' },
150
+ { country: 'Japan', rate: 90, gender: 'Female' },
151
+ { country: 'Brazil', rate: 60, gender: 'Male' },
152
+ { country: 'Brazil', rate: 65, gender: 'Female' },
153
+ ],
154
+ encoding: {
155
+ x: { field: 'rate', type: 'quantitative' },
156
+ y: { field: 'country', type: 'nominal' },
157
+ color: { field: 'gender', type: 'nominal' },
158
+ },
159
+ chrome: {},
160
+ annotations: [],
161
+ responsive: true,
162
+ theme: {},
163
+ darkMode: 'off',
164
+ labels: { density: 'auto', format: '' },
165
+ };
166
+ }
167
+
168
+ it('renders connecting bars and dots for multi-series', () => {
169
+ const spec = makeDumbbellSpec();
170
+ const scales = computeScales(spec, chartArea, spec.data);
171
+ const marks = computeDotMarks(spec, scales, chartArea, fullStrategy);
172
+
173
+ const points = marks.filter((m): m is PointMark => m.type === 'point');
174
+ const bars = marks.filter((m): m is RectMark => m.type === 'rect');
175
+
176
+ // 6 data rows = 6 dots
177
+ expect(points).toHaveLength(6);
178
+ // 3 categories = 3 connecting bars
179
+ expect(bars).toHaveLength(3);
180
+ });
181
+
182
+ it('connecting bars span from min to max value per category', () => {
183
+ const spec = makeDumbbellSpec();
184
+ const scales = computeScales(spec, chartArea, spec.data);
185
+ const marks = computeDotMarks(spec, scales, chartArea, fullStrategy);
186
+
187
+ const bars = marks.filter((m): m is RectMark => m.type === 'rect');
188
+ for (const bar of bars) {
189
+ expect(bar.width).toBeGreaterThan(0);
190
+ }
191
+ });
192
+
193
+ it('connecting bars use stem color (gray)', () => {
194
+ const spec = makeDumbbellSpec();
195
+ const scales = computeScales(spec, chartArea, spec.data);
196
+ const marks = computeDotMarks(spec, scales, chartArea, fullStrategy);
197
+
198
+ const bars = marks.filter((m): m is RectMark => m.type === 'rect');
199
+ for (const bar of bars) {
200
+ expect(bar.fill).toBe('#cccccc');
201
+ }
202
+ });
203
+
204
+ it('dots within same category share the same cy position', () => {
205
+ const spec = makeDumbbellSpec();
206
+ const scales = computeScales(spec, chartArea, spec.data);
207
+ const marks = computeDotMarks(spec, scales, chartArea, fullStrategy);
208
+
209
+ const points = marks.filter((m): m is PointMark => m.type === 'point');
210
+ const usaDots = points.filter((m) => m.aria.label.includes('USA'));
211
+ expect(usaDots).toHaveLength(2);
212
+ expect(usaDots[0].cy).toBe(usaDots[1].cy);
213
+ });
214
+
215
+ it('dots for different series have different fill colors', () => {
216
+ const spec = makeDumbbellSpec();
217
+ const scales = computeScales(spec, chartArea, spec.data);
218
+ const marks = computeDotMarks(spec, scales, chartArea, fullStrategy);
219
+
220
+ const points = marks.filter((m): m is PointMark => m.type === 'point');
221
+ const maleDot = points.find((m) => m.aria.label.includes('Male'))!;
222
+ const femaleDot = points.find((m) => m.aria.label.includes('Female'))!;
223
+ expect(maleDot.fill).not.toBe(femaleDot.fill);
224
+ });
225
+
226
+ it('connecting bars render before dots (lower z-index)', () => {
227
+ const spec = makeDumbbellSpec();
228
+ const scales = computeScales(spec, chartArea, spec.data);
229
+ const marks = computeDotMarks(spec, scales, chartArea, fullStrategy);
230
+
231
+ // First mark should be a rect (connecting bar renders before dots)
232
+ expect(marks[0].type).toBe('rect');
233
+ });
234
+
235
+ it('aria labels include category and series name', () => {
236
+ const spec = makeDumbbellSpec();
237
+ const scales = computeScales(spec, chartArea, spec.data);
238
+ const marks = computeDotMarks(spec, scales, chartArea, fullStrategy);
239
+
240
+ const points = marks.filter((m): m is PointMark => m.type === 'point');
241
+ const usaMale = points.find(
242
+ (m) => m.aria.label.includes('USA') && m.aria.label.includes('Male'),
243
+ );
244
+ expect(usaMale).toBeDefined();
245
+ expect(usaMale!.aria.label).toContain('78');
246
+ });
247
+ });
248
+
249
+ describe('negative values', () => {
250
+ it('handles negative values (stem extends left of baseline)', () => {
251
+ const spec = makeColoredDotSpec();
252
+ const scales = computeScales(spec, chartArea, spec.data);
253
+ const marks = computeDotMarks(spec, scales, chartArea, fullStrategy);
254
+
255
+ const points = marks.filter((m): m is PointMark => m.type === 'point');
256
+ const debtDot = points.find((m) => m.data.item === 'Debt')!;
257
+ const revenueDot = points.find((m) => m.data.item === 'Revenue')!;
258
+
259
+ // Debt (-10) should be to the left of Revenue (120)
260
+ expect(debtDot.cx).toBeLessThan(revenueDot.cx);
261
+ });
262
+ });
263
+
264
+ describe('edge cases', () => {
265
+ it('returns empty array when no x encoding', () => {
266
+ const spec: NormalizedChartSpec = {
267
+ type: 'dot',
268
+ data: [{ country: 'USA', score: 85 }],
269
+ encoding: {
270
+ y: { field: 'country', type: 'nominal' },
271
+ },
272
+ chrome: {},
273
+ annotations: [],
274
+ responsive: true,
275
+ theme: {},
276
+ darkMode: 'off',
277
+ labels: { density: 'auto', format: '' },
278
+ };
279
+ const scales = computeScales(spec, chartArea, spec.data);
280
+ const marks = computeDotMarks(spec, scales, chartArea, fullStrategy);
281
+ expect(marks).toHaveLength(0);
282
+ });
283
+
284
+ it('returns empty array for empty data', () => {
285
+ const spec: NormalizedChartSpec = {
286
+ type: 'dot',
287
+ data: [],
288
+ encoding: {
289
+ x: { field: 'score', type: 'quantitative' },
290
+ y: { field: 'country', type: 'nominal' },
291
+ },
292
+ chrome: {},
293
+ annotations: [],
294
+ responsive: true,
295
+ theme: {},
296
+ darkMode: 'off',
297
+ labels: { density: 'auto', format: '' },
298
+ };
299
+ const scales = computeScales(spec, chartArea, spec.data);
300
+ const marks = computeDotMarks(spec, scales, chartArea, fullStrategy);
301
+ expect(marks).toHaveLength(0);
302
+ });
303
+ });
304
+ });
305
+
306
+ // ---------------------------------------------------------------------------
307
+ // computeDotLabels tests
308
+ // ---------------------------------------------------------------------------
309
+
310
+ describe('computeDotLabels', () => {
311
+ it('produces labels for each dot mark', () => {
312
+ const spec = makeSimpleDotSpec();
313
+ const scales = computeScales(spec, chartArea, spec.data);
314
+ const allMarks = computeDotMarks(spec, scales, chartArea, fullStrategy);
315
+ const points = allMarks.filter((m): m is PointMark => m.type === 'point');
316
+ const labels = computeDotLabels(points, chartArea);
317
+
318
+ expect(labels).toHaveLength(points.length);
319
+ });
320
+
321
+ it('labels are positioned to the right of dots', () => {
322
+ const spec = makeSimpleDotSpec();
323
+ const scales = computeScales(spec, chartArea, spec.data);
324
+ const allMarks = computeDotMarks(spec, scales, chartArea, fullStrategy);
325
+ const points = allMarks.filter((m): m is PointMark => m.type === 'point');
326
+ const labels = computeDotLabels(points, chartArea);
327
+
328
+ for (let i = 0; i < labels.length; i++) {
329
+ expect(labels[i].x).toBeGreaterThan(points[i].cx);
330
+ }
331
+ });
332
+
333
+ it('labels contain the numeric values', () => {
334
+ const spec = makeSimpleDotSpec();
335
+ const scales = computeScales(spec, chartArea, spec.data);
336
+ const allMarks = computeDotMarks(spec, scales, chartArea, fullStrategy);
337
+ const points = allMarks.filter((m): m is PointMark => m.type === 'point');
338
+ const labels = computeDotLabels(points, chartArea);
339
+
340
+ const texts = labels.map((l) => l.text);
341
+ expect(texts).toContain('85');
342
+ expect(texts).toContain('90');
343
+ });
344
+ });