@opendata-ai/openchart-engine 8.0.0-rc.9 → 8.1.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 +175 -12
- package/dist/index.js +917 -168
- package/dist/index.js.map +1 -1
- package/package.json +3 -4
- package/src/__test-fixtures__/specs.ts +0 -182
- package/src/__tests__/__snapshots__/compile-snapshot.test.ts.snap +0 -2401
- package/src/__tests__/annotation-behavior.test.ts +0 -852
- package/src/__tests__/axes.test.ts +0 -1579
- package/src/__tests__/compile-animation.test.ts +0 -161
- package/src/__tests__/compile-chart.test.ts +0 -1303
- package/src/__tests__/compile-footnotes.test.ts +0 -277
- package/src/__tests__/compile-layer.test.ts +0 -969
- package/src/__tests__/compile-snapshot.test.ts +0 -156
- package/src/__tests__/compound-labels.test.ts +0 -147
- package/src/__tests__/dimensions.test.ts +0 -918
- package/src/__tests__/encoding-sugar.test.ts +0 -390
- package/src/__tests__/facet.test.ts +0 -215
- package/src/__tests__/helpers/invariants.ts +0 -299
- package/src/__tests__/known-bugs.test.ts +0 -566
- package/src/__tests__/layout-fuzz.test.ts +0 -358
- package/src/__tests__/layout-invariants.test.ts +0 -535
- package/src/__tests__/legend.test.ts +0 -721
- package/src/__tests__/mark-keys.test.ts +0 -294
- package/src/__tests__/ranking-recipes.test.ts +0 -260
- package/src/__tests__/rotation.test.ts +0 -83
- package/src/__tests__/scales.test.ts +0 -323
- package/src/__tests__/series-search.test.ts +0 -92
- package/src/__tests__/spec-sugar.test.ts +0 -801
- package/src/__tests__/you-draw-it.test.ts +0 -160
- package/src/annotations/__tests__/compute.test.ts +0 -1672
- package/src/annotations/__tests__/geometry.test.ts +0 -66
- package/src/annotations/__tests__/placement-fuzz.test.ts +0 -256
- package/src/annotations/__tests__/placement-invariants.test.ts +0 -345
- package/src/annotations/__tests__/placement.test.ts +0 -316
- package/src/annotations/__tests__/resolve-text.test.ts +0 -788
- package/src/annotations/__tests__/rich-text.test.ts +0 -91
- package/src/annotations/__tests__/thinning.test.ts +0 -285
- package/src/annotations/collisions.ts +0 -326
- package/src/annotations/compute.ts +0 -333
- package/src/annotations/constants.ts +0 -162
- package/src/annotations/geometry.ts +0 -376
- package/src/annotations/move.ts +0 -135
- package/src/annotations/placement.ts +0 -636
- package/src/annotations/position.ts +0 -131
- package/src/annotations/resolve-range.ts +0 -117
- package/src/annotations/resolve-refline.ts +0 -157
- package/src/annotations/resolve-text.ts +0 -545
- package/src/annotations/rich-text.ts +0 -65
- package/src/annotations/thinning.ts +0 -190
- package/src/barlist/__tests__/compile-barlist.test.ts +0 -200
- package/src/barlist/compile-barlist.ts +0 -391
- package/src/barlist/types.ts +0 -28
- package/src/charts/__tests__/post-process.test.ts +0 -237
- package/src/charts/__tests__/registry.test.ts +0 -116
- package/src/charts/__tests__/utils.test.ts +0 -195
- package/src/charts/_shared/__tests__/density-filter.test.ts +0 -32
- package/src/charts/_shared/density-filter.ts +0 -26
- package/src/charts/_shared/format-label-value.ts +0 -16
- package/src/charts/bar/__tests__/compute.test.ts +0 -934
- package/src/charts/bar/__tests__/gradient-orient.test.ts +0 -127
- package/src/charts/bar/__tests__/labels.test.ts +0 -292
- package/src/charts/bar/compute.ts +0 -539
- package/src/charts/bar/index.ts +0 -60
- package/src/charts/bar/labels.ts +0 -311
- package/src/charts/beeswarm/__tests__/compute.test.ts +0 -280
- package/src/charts/beeswarm/__tests__/dodge.test.ts +0 -148
- package/src/charts/beeswarm/compute.ts +0 -203
- package/src/charts/beeswarm/dodge.ts +0 -155
- package/src/charts/beeswarm/index.ts +0 -32
- package/src/charts/builtin.ts +0 -75
- package/src/charts/calendar/__tests__/compute.test.ts +0 -417
- package/src/charts/calendar/compute.ts +0 -319
- package/src/charts/calendar/index.ts +0 -32
- package/src/charts/column/__tests__/compute.test.ts +0 -540
- package/src/charts/column/__tests__/labels.test.ts +0 -179
- package/src/charts/column/compute.ts +0 -493
- package/src/charts/column/index.ts +0 -53
- package/src/charts/column/labels.ts +0 -229
- package/src/charts/dot/__tests__/compute.test.ts +0 -380
- package/src/charts/dot/__tests__/labels.test.ts +0 -98
- package/src/charts/dot/compute.ts +0 -285
- package/src/charts/dot/index.ts +0 -62
- package/src/charts/dot/labels.ts +0 -121
- package/src/charts/line/__tests__/compute.test.ts +0 -1194
- package/src/charts/line/__tests__/curves.test.ts +0 -51
- package/src/charts/line/__tests__/labels.test.ts +0 -94
- package/src/charts/line/__tests__/mark-options.test.ts +0 -175
- package/src/charts/line/area.ts +0 -484
- package/src/charts/line/compute.ts +0 -356
- package/src/charts/line/curves.ts +0 -85
- package/src/charts/line/index.ts +0 -176
- package/src/charts/line/labels.ts +0 -177
- package/src/charts/parliament/__tests__/compute.test.ts +0 -411
- package/src/charts/parliament/compute.ts +0 -313
- package/src/charts/parliament/index.ts +0 -32
- package/src/charts/pie/__tests__/compute.test.ts +0 -384
- package/src/charts/pie/__tests__/labels.test.ts +0 -132
- package/src/charts/pie/compute.ts +0 -324
- package/src/charts/pie/index.ts +0 -49
- package/src/charts/pie/labels.ts +0 -143
- package/src/charts/post-process.ts +0 -229
- package/src/charts/range/__tests__/compute.test.ts +0 -486
- package/src/charts/range/compute.ts +0 -273
- package/src/charts/range/index.ts +0 -82
- package/src/charts/range/labels.ts +0 -169
- package/src/charts/rect/__tests__/compute.test.ts +0 -120
- package/src/charts/rect/index.ts +0 -127
- package/src/charts/registry.ts +0 -72
- package/src/charts/rule/index.ts +0 -139
- package/src/charts/scatter/__tests__/compute.test.ts +0 -488
- package/src/charts/scatter/__tests__/trendline.test.ts +0 -191
- package/src/charts/scatter/compute.ts +0 -192
- package/src/charts/scatter/index.ts +0 -46
- package/src/charts/scatter/trendline.ts +0 -100
- package/src/charts/text/__tests__/compute.test.ts +0 -307
- package/src/charts/text/index.ts +0 -141
- package/src/charts/tick/__tests__/compute.test.ts +0 -75
- package/src/charts/tick/index.ts +0 -99
- package/src/charts/utils.ts +0 -170
- package/src/charts/waffle/__tests__/compute.test.ts +0 -305
- package/src/charts/waffle/compute.ts +0 -197
- package/src/charts/waffle/index.ts +0 -31
- package/src/compile/__tests__/color-scale-range.test.ts +0 -158
- package/src/compile/__tests__/color-scale-reverse.test.ts +0 -89
- package/src/compile/__tests__/data-clip.test.ts +0 -127
- package/src/compile/__tests__/fill-patterns.test.ts +0 -154
- package/src/compile/__tests__/size-scale.test.ts +0 -108
- package/src/compile/__tests__/watermark-obstacle.test.ts +0 -93
- package/src/compile/color-scale-range.ts +0 -101
- package/src/compile/data-clip.ts +0 -48
- package/src/compile/fill-patterns.ts +0 -74
- package/src/compile/layer.ts +0 -630
- package/src/compile/size-scale.ts +0 -141
- package/src/compile/spec-sugar.ts +0 -758
- package/src/compile/watermark-obstacle.ts +0 -54
- package/src/compile/you-draw-it.ts +0 -123
- package/src/compile.ts +0 -1503
- package/src/compiler/__tests__/a11y-warnings.test.ts +0 -148
- package/src/compiler/__tests__/animation.test.ts +0 -177
- package/src/compiler/__tests__/compile.test.ts +0 -87
- package/src/compiler/__tests__/fixtures/vl-parity/axis-null.json +0 -19
- package/src/compiler/__tests__/fixtures/vl-parity/color-value.json +0 -20
- package/src/compiler/__tests__/fixtures/vl-parity/count-aggregate.json +0 -22
- package/src/compiler/__tests__/fixtures/vl-parity/data-values.json +0 -19
- package/src/compiler/__tests__/fixtures/vl-parity/layer-axis-null.json +0 -14
- package/src/compiler/__tests__/fixtures/vl-parity/layer-count.json +0 -34
- package/src/compiler/__tests__/fixtures/vl-parity/layer-data-values.json +0 -22
- package/src/compiler/__tests__/fixtures/vl-parity/layer-legend-null.json +0 -41
- package/src/compiler/__tests__/fixtures/vl-parity/layer-scheme.json +0 -38
- package/src/compiler/__tests__/fixtures/vl-parity/layer-sort.json +0 -28
- package/src/compiler/__tests__/fixtures/vl-parity/layer-theta.json +0 -28
- package/src/compiler/__tests__/fixtures/vl-parity/layer-title.json +0 -30
- package/src/compiler/__tests__/fixtures/vl-parity/layer-value-def.json +0 -29
- package/src/compiler/__tests__/fixtures/vl-parity/legend-null.json +0 -32
- package/src/compiler/__tests__/fixtures/vl-parity/scheme.json +0 -25
- package/src/compiler/__tests__/fixtures/vl-parity/sort-by-value.json +0 -19
- package/src/compiler/__tests__/fixtures/vl-parity/theta-arc.json +0 -19
- package/src/compiler/__tests__/fixtures/vl-parity/title-subtitle.json +0 -21
- package/src/compiler/__tests__/fixtures/vl-parity/width-height.json +0 -23
- package/src/compiler/__tests__/keys.test.ts +0 -83
- package/src/compiler/__tests__/normalize-highlight.test.ts +0 -103
- package/src/compiler/__tests__/normalize.test.ts +0 -399
- package/src/compiler/__tests__/sparkline-defaults.test.ts +0 -153
- package/src/compiler/__tests__/validate-did-you-mean.test.ts +0 -170
- package/src/compiler/__tests__/validate-highlight.test.ts +0 -112
- package/src/compiler/__tests__/validate-near-miss.test.ts +0 -251
- package/src/compiler/__tests__/validate.test.ts +0 -629
- package/src/compiler/__tests__/vl-parity.test.ts +0 -68
- package/src/compiler/a11y-warnings.ts +0 -110
- package/src/compiler/animation.ts +0 -142
- package/src/compiler/index.ts +0 -47
- package/src/compiler/keys.ts +0 -55
- package/src/compiler/normalize.ts +0 -734
- package/src/compiler/sparkline-defaults.ts +0 -138
- package/src/compiler/types.ts +0 -241
- package/src/compiler/validate.ts +0 -2041
- package/src/endpoint-labels/__tests__/compute.test.ts +0 -587
- package/src/endpoint-labels/compute.ts +0 -621
- package/src/endpoint-labels/constants.ts +0 -54
- package/src/endpoint-labels/format.ts +0 -60
- package/src/endpoint-labels/predict.ts +0 -139
- package/src/format/__tests__/field-format.test.ts +0 -164
- package/src/format/field-format.ts +0 -27
- package/src/geo/__tests__/compile-map.test.ts +0 -1128
- package/src/geo/compile-map.ts +0 -1376
- package/src/geo/join.ts +0 -115
- package/src/geo/projections.ts +0 -65
- package/src/geo/types.ts +0 -26
- package/src/graphs/__tests__/community.test.ts +0 -228
- package/src/graphs/__tests__/compile-graph.test.ts +0 -396
- package/src/graphs/__tests__/encoding.test.ts +0 -415
- package/src/graphs/community.ts +0 -92
- package/src/graphs/compile-graph.ts +0 -329
- package/src/graphs/encoding.ts +0 -356
- package/src/graphs/types.ts +0 -106
- package/src/index.ts +0 -177
- package/src/layout/__tests__/metrics.test.ts +0 -97
- package/src/layout/__tests__/scales-highlight.test.ts +0 -192
- package/src/layout/axes/rotation.ts +0 -152
- package/src/layout/axes/thinning.ts +0 -98
- package/src/layout/axes/ticks.ts +0 -409
- package/src/layout/axes.ts +0 -757
- package/src/layout/dimensions.ts +0 -912
- package/src/layout/facet.ts +0 -91
- package/src/layout/gridlines.ts +0 -84
- package/src/layout/metrics.ts +0 -132
- package/src/layout/plan.ts +0 -631
- package/src/layout/scales.ts +0 -1053
- package/src/layout/shared.ts +0 -44
- package/src/legend/__tests__/continuous.test.ts +0 -318
- package/src/legend/__tests__/redundancy.test.ts +0 -108
- package/src/legend/__tests__/size.test.ts +0 -348
- package/src/legend/__tests__/suppression.test.ts +0 -382
- package/src/legend/compute.ts +0 -748
- package/src/legend/continuous.ts +0 -327
- package/src/legend/size.ts +0 -407
- package/src/legend/suppression.ts +0 -226
- package/src/legend/wrap.ts +0 -117
- package/src/sankey/__tests__/compile-sankey.test.ts +0 -458
- package/src/sankey/__tests__/layout.test.ts +0 -165
- package/src/sankey/__tests__/node-label-wrap.test.ts +0 -114
- package/src/sankey/__tests__/node-sort.test.ts +0 -45
- package/src/sankey/compile-sankey.ts +0 -706
- package/src/sankey/layout.ts +0 -184
- package/src/sankey/types.ts +0 -41
- package/src/tables/__tests__/bar-column.test.ts +0 -147
- package/src/tables/__tests__/category-colors.test.ts +0 -230
- package/src/tables/__tests__/compile-table.test.ts +0 -236
- package/src/tables/__tests__/format-cells.test.ts +0 -143
- package/src/tables/__tests__/heatmap.test.ts +0 -148
- package/src/tables/__tests__/pagination.test.ts +0 -78
- package/src/tables/__tests__/search.test.ts +0 -94
- package/src/tables/__tests__/sort.test.ts +0 -107
- package/src/tables/__tests__/sparkline.test.ts +0 -122
- package/src/tables/bar-column.ts +0 -94
- package/src/tables/category-colors.ts +0 -78
- package/src/tables/compile-table.ts +0 -427
- package/src/tables/format-cells.ts +0 -115
- package/src/tables/heatmap.ts +0 -141
- package/src/tables/pagination.ts +0 -46
- package/src/tables/search.ts +0 -66
- package/src/tables/sort.ts +0 -69
- package/src/tables/sparkline.ts +0 -113
- package/src/tables/utils.ts +0 -16
- package/src/tilemap/__tests__/compile-tilemap.test.ts +0 -576
- package/src/tilemap/compile-tilemap.ts +0 -755
- package/src/tilemap/layout.ts +0 -172
- package/src/tilemap/types.ts +0 -33
- package/src/tooltips/__tests__/compute.test.ts +0 -582
- package/src/tooltips/compute.ts +0 -700
- package/src/transforms/__tests__/aggregate.test.ts +0 -159
- package/src/transforms/__tests__/bin.test.ts +0 -88
- package/src/transforms/__tests__/calculate.test.ts +0 -146
- package/src/transforms/__tests__/conditional.test.ts +0 -127
- package/src/transforms/__tests__/filter-relative.test.ts +0 -202
- package/src/transforms/__tests__/filter.test.ts +0 -59
- package/src/transforms/__tests__/fold.test.ts +0 -79
- package/src/transforms/__tests__/index.test.ts +0 -93
- package/src/transforms/__tests__/predicates.test.ts +0 -193
- package/src/transforms/__tests__/timeunit.test.ts +0 -129
- package/src/transforms/__tests__/window.test.ts +0 -286
- package/src/transforms/aggregate.ts +0 -131
- package/src/transforms/bin.ts +0 -87
- package/src/transforms/calculate.ts +0 -60
- package/src/transforms/conditional.ts +0 -46
- package/src/transforms/filter.ts +0 -122
- package/src/transforms/fold.ts +0 -49
- package/src/transforms/index.ts +0 -60
- package/src/transforms/predicates.ts +0 -123
- package/src/transforms/timeunit.ts +0 -88
- package/src/transforms/window.ts +0 -185
|
@@ -1,53 +0,0 @@
|
|
|
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 { resolveFieldFormatter } from '../../format/field-format';
|
|
9
|
-
import type { ChartRenderer } from '../registry';
|
|
10
|
-
import { computeColumnMarks } from './compute';
|
|
11
|
-
import { computeColumnLabels } from './labels';
|
|
12
|
-
|
|
13
|
-
// ---------------------------------------------------------------------------
|
|
14
|
-
// Column chart renderer
|
|
15
|
-
// ---------------------------------------------------------------------------
|
|
16
|
-
|
|
17
|
-
export const columnRenderer: ChartRenderer = (spec, scales, chartArea, strategy, theme) => {
|
|
18
|
-
const marks = computeColumnMarks(spec, scales, chartArea, strategy);
|
|
19
|
-
|
|
20
|
-
// Compute and attach value labels (respects spec.labels.density)
|
|
21
|
-
const valueField =
|
|
22
|
-
spec.encoding?.y && 'field' in spec.encoding.y ? spec.encoding.y.field : undefined;
|
|
23
|
-
const labelFormatter = resolveFieldFormatter({
|
|
24
|
-
surfaceFormat: spec.labels.format,
|
|
25
|
-
channelFormat:
|
|
26
|
-
spec.encoding?.y && 'format' in spec.encoding.y ? spec.encoding.y.format : undefined,
|
|
27
|
-
values: valueField ? spec.data.map((r) => r[valueField]) : [],
|
|
28
|
-
});
|
|
29
|
-
const labels = computeColumnLabels(
|
|
30
|
-
marks,
|
|
31
|
-
chartArea,
|
|
32
|
-
spec.labels.density,
|
|
33
|
-
labelFormatter,
|
|
34
|
-
spec.labels.prefix,
|
|
35
|
-
valueField,
|
|
36
|
-
spec.labels.color,
|
|
37
|
-
theme.isDark,
|
|
38
|
-
spec.labels.fontSize,
|
|
39
|
-
spec.labels.suffix,
|
|
40
|
-
);
|
|
41
|
-
for (let i = 0; i < marks.length && i < labels.length; i++) {
|
|
42
|
-
marks[i].label = labels[i];
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
return marks as Mark[];
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
// ---------------------------------------------------------------------------
|
|
49
|
-
// Public exports
|
|
50
|
-
// ---------------------------------------------------------------------------
|
|
51
|
-
|
|
52
|
-
export { computeColumnMarks } from './compute';
|
|
53
|
-
export { computeColumnLabels } from './labels';
|
|
@@ -1,229 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Column chart label computation.
|
|
3
|
-
*
|
|
4
|
-
* Produces value labels positioned above each column (for positive values)
|
|
5
|
-
* or below (for negative values). Stacked segments are the exception: the
|
|
6
|
-
* space above a segment's top edge belongs to the next segment up, not to the
|
|
7
|
-
* background, so a stacked label is centered *inside* its own segment and
|
|
8
|
-
* takes a contrasting fill (mirrors computeBarLabels).
|
|
9
|
-
*
|
|
10
|
-
* Respects the spec's label density setting:
|
|
11
|
-
* - 'all': consider every column's label (no density pre-filter), but still
|
|
12
|
-
* run collision resolution so colliding losers are hidden rather than drawn
|
|
13
|
-
* on top of each other
|
|
14
|
-
* - 'auto': existing behavior (density pre-filter + collision detection)
|
|
15
|
-
* - 'endpoints': first and last columns only
|
|
16
|
-
* - 'none': return empty array
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
import type {
|
|
20
|
-
LabelCandidate,
|
|
21
|
-
LabelDensity,
|
|
22
|
-
NumberFormatter,
|
|
23
|
-
RectMark,
|
|
24
|
-
ResolvedLabel,
|
|
25
|
-
} from '@opendata-ai/openchart-core';
|
|
26
|
-
import {
|
|
27
|
-
estimateTextWidth,
|
|
28
|
-
getRepresentativeColor,
|
|
29
|
-
pickLabelColor,
|
|
30
|
-
resolveCollisions,
|
|
31
|
-
textAscent,
|
|
32
|
-
} from '@opendata-ai/openchart-core';
|
|
33
|
-
import { filterByDensity } from '../_shared/density-filter';
|
|
34
|
-
import { formatLabelValue } from '../_shared/format-label-value';
|
|
35
|
-
|
|
36
|
-
// ---------------------------------------------------------------------------
|
|
37
|
-
// Constants
|
|
38
|
-
// ---------------------------------------------------------------------------
|
|
39
|
-
|
|
40
|
-
const LABEL_FONT_SIZE = 10;
|
|
41
|
-
const LABEL_FONT_WEIGHT = 600;
|
|
42
|
-
const LABEL_OFFSET_Y = 8;
|
|
43
|
-
/** Vertical padding inside a stacked segment, above and below the label. */
|
|
44
|
-
const LABEL_PADDING_Y = 2;
|
|
45
|
-
|
|
46
|
-
// ---------------------------------------------------------------------------
|
|
47
|
-
// Public API
|
|
48
|
-
// ---------------------------------------------------------------------------
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Compute value labels for column marks.
|
|
52
|
-
*
|
|
53
|
-
* For each column, the value is placed centered above the column top.
|
|
54
|
-
*/
|
|
55
|
-
export function computeColumnLabels(
|
|
56
|
-
marks: RectMark[],
|
|
57
|
-
_chartArea: { x: number; y: number; width: number; height: number },
|
|
58
|
-
density: LabelDensity = 'auto',
|
|
59
|
-
labelFormatter?: NumberFormatter | null,
|
|
60
|
-
labelPrefix?: string,
|
|
61
|
-
valueField?: string,
|
|
62
|
-
labelColor?: string,
|
|
63
|
-
darkMode = false,
|
|
64
|
-
fontSize?: number,
|
|
65
|
-
labelSuffix?: string,
|
|
66
|
-
): ResolvedLabel[] {
|
|
67
|
-
const FONT_SIZE = fontSize ?? LABEL_FONT_SIZE;
|
|
68
|
-
const targetMarks = filterByDensity(marks, density);
|
|
69
|
-
|
|
70
|
-
const formatter = labelFormatter ?? null;
|
|
71
|
-
|
|
72
|
-
const candidates: LabelCandidate[] = [];
|
|
73
|
-
// Track whether each candidate fits inside its stacked segment. Non-stacked
|
|
74
|
-
// columns label into open space above the bar, so they always fit.
|
|
75
|
-
const fitsInSegment: boolean[] = [];
|
|
76
|
-
|
|
77
|
-
for (const mark of targetMarks) {
|
|
78
|
-
// Get the original numeric value from the data row when possible,
|
|
79
|
-
// falling back to parsing the aria label (which may lose precision
|
|
80
|
-
// due to abbreviation rounding, e.g. 1955 → "2K" → 2000).
|
|
81
|
-
let valuePart: string;
|
|
82
|
-
const rawNum = valueField != null ? Number(mark.data[valueField]) : NaN;
|
|
83
|
-
|
|
84
|
-
if (formatter && Number.isFinite(rawNum)) {
|
|
85
|
-
valuePart = formatter(rawNum);
|
|
86
|
-
} else if (Number.isFinite(rawNum)) {
|
|
87
|
-
valuePart = formatLabelValue(rawNum);
|
|
88
|
-
} else {
|
|
89
|
-
// Fallback: extract from aria label
|
|
90
|
-
const ariaLabel = mark.aria.label;
|
|
91
|
-
if (!ariaLabel) continue;
|
|
92
|
-
const lastColon = ariaLabel.lastIndexOf(':');
|
|
93
|
-
const rawValue = lastColon >= 0 ? ariaLabel.slice(lastColon + 1).trim() : '';
|
|
94
|
-
if (!rawValue) continue;
|
|
95
|
-
if (formatter) {
|
|
96
|
-
const num = Number(rawValue.replace(/[^0-9.-]/g, ''));
|
|
97
|
-
valuePart = !Number.isNaN(num) ? formatter(num) : rawValue;
|
|
98
|
-
} else {
|
|
99
|
-
valuePart = rawValue;
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
const numericValue = parseFloat(valuePart);
|
|
103
|
-
const isNegative = Number.isFinite(numericValue) && numericValue < 0;
|
|
104
|
-
|
|
105
|
-
if (labelPrefix) valuePart = labelPrefix + valuePart;
|
|
106
|
-
if (labelSuffix) valuePart = valuePart + labelSuffix;
|
|
107
|
-
|
|
108
|
-
const textWidth = estimateTextWidth(valuePart, FONT_SIZE, LABEL_FONT_WEIGHT);
|
|
109
|
-
const textHeight = FONT_SIZE * 1.2;
|
|
110
|
-
|
|
111
|
-
// A stacked segment has another segment sitting directly on top of it, so
|
|
112
|
-
// the "float above the top edge" placement used for plain columns would
|
|
113
|
-
// drop the label into its neighbour — and tint it with this segment's
|
|
114
|
-
// color, guaranteeing colored-text-on-colored-fill. Center it inside its
|
|
115
|
-
// own segment instead and pick a fill that contrasts with that segment.
|
|
116
|
-
const isStacked = mark.stackGroup !== undefined;
|
|
117
|
-
|
|
118
|
-
// anchorY is the TOP of the label bounding box so the collision system's
|
|
119
|
-
// AABB check (rect = { y: anchorY, height: textHeight }) is geometrically
|
|
120
|
-
// correct. Collision math stays in top-coordinate space; the ascent shift
|
|
121
|
-
// to the alphabetic baseline is applied only at emission (see below).
|
|
122
|
-
// Stacked: top = segment center - textHeight / 2, text sits within
|
|
123
|
-
// Positive bar: top = barTop - LABEL_OFFSET_Y - textHeight, text floats above
|
|
124
|
-
// Negative bar: top = barBottom + LABEL_OFFSET_Y, text hangs below
|
|
125
|
-
const anchorX = mark.x + mark.width / 2;
|
|
126
|
-
let anchorY: number;
|
|
127
|
-
let fill: string;
|
|
128
|
-
|
|
129
|
-
if (isStacked) {
|
|
130
|
-
anchorY = mark.y + mark.height / 2 - textHeight / 2;
|
|
131
|
-
fill = labelColor ?? pickLabelColor(getRepresentativeColor(mark.fill), darkMode);
|
|
132
|
-
} else {
|
|
133
|
-
anchorY = isNegative
|
|
134
|
-
? mark.y + mark.height + LABEL_OFFSET_Y
|
|
135
|
-
: mark.y - LABEL_OFFSET_Y - textHeight;
|
|
136
|
-
fill = labelColor ?? getRepresentativeColor(mark.fill);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// A label only belongs inside a stacked segment if the segment is tall
|
|
140
|
-
// enough to hold it; short segments (Nuclear at ~4%) would otherwise
|
|
141
|
-
// overflow into their neighbours.
|
|
142
|
-
fitsInSegment.push(!(isStacked && textHeight > mark.height - 2 * LABEL_PADDING_Y));
|
|
143
|
-
|
|
144
|
-
candidates.push({
|
|
145
|
-
text: valuePart,
|
|
146
|
-
anchorX,
|
|
147
|
-
anchorY,
|
|
148
|
-
width: textWidth,
|
|
149
|
-
height: textHeight,
|
|
150
|
-
priority: 'data',
|
|
151
|
-
style: {
|
|
152
|
-
fontFamily: 'system-ui, -apple-system, sans-serif',
|
|
153
|
-
fontSize: FONT_SIZE,
|
|
154
|
-
fontWeight: LABEL_FONT_WEIGHT,
|
|
155
|
-
fill,
|
|
156
|
-
lineHeight: 1.2,
|
|
157
|
-
textAnchor: 'middle',
|
|
158
|
-
fontVariant: 'tabular-nums',
|
|
159
|
-
},
|
|
160
|
-
});
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
if (candidates.length === 0) return [];
|
|
164
|
-
|
|
165
|
-
// Collision resolution keeps y in top-edge coordinates; renderers position
|
|
166
|
-
// text on the alphabetic baseline, so shift every emitted y down by the
|
|
167
|
-
// ascent instead of relying on dominant-baseline:hanging (WebKit computes
|
|
168
|
-
// hanging from different font metrics than Blink, clipping labels on iOS).
|
|
169
|
-
const ascent = textAscent(FONT_SIZE);
|
|
170
|
-
|
|
171
|
-
// Every density that reaches here ('all' and 'auto') pre-hides candidates
|
|
172
|
-
// whose stacked segment is too short to hold them, then runs collision
|
|
173
|
-
// resolution. 'all' just skips the density pre-filter above so every mark is
|
|
174
|
-
// a candidate; colliding losers are still hidden by resolveCollisions rather
|
|
175
|
-
// than stacked on top of each other.
|
|
176
|
-
const fittingCandidates: LabelCandidate[] = [];
|
|
177
|
-
const unfittingIndices = new Set<number>();
|
|
178
|
-
for (let i = 0; i < candidates.length; i++) {
|
|
179
|
-
if (fitsInSegment[i] === false) {
|
|
180
|
-
unfittingIndices.add(i);
|
|
181
|
-
} else {
|
|
182
|
-
// Carry the original candidate index so we can zip the resolved output
|
|
183
|
-
// back to it regardless of any reordering resolveCollisions performs.
|
|
184
|
-
fittingCandidates.push({ ...candidates[i], index: i });
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// resolveCollisions may reorder its output (it sorts by priority), so key the
|
|
189
|
-
// result by the source index we carried rather than by position.
|
|
190
|
-
const resolvedByOrig = new Map<number, ResolvedLabel>();
|
|
191
|
-
for (const r of resolveCollisions(fittingCandidates)) {
|
|
192
|
-
if (r.index !== undefined) resolvedByOrig.set(r.index, r);
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// Re-emit hidden labels rather than dropping them: the renderer zips
|
|
196
|
-
// marks[i].label = labels[i], so the output must stay index-aligned.
|
|
197
|
-
const results: ResolvedLabel[] = [];
|
|
198
|
-
for (let i = 0; i < candidates.length; i++) {
|
|
199
|
-
if (unfittingIndices.has(i)) {
|
|
200
|
-
results.push({
|
|
201
|
-
text: candidates[i].text,
|
|
202
|
-
x: candidates[i].anchorX,
|
|
203
|
-
y: candidates[i].anchorY + ascent,
|
|
204
|
-
style: candidates[i].style,
|
|
205
|
-
visible: false,
|
|
206
|
-
});
|
|
207
|
-
continue;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
const r = resolvedByOrig.get(i);
|
|
211
|
-
if (!r) continue;
|
|
212
|
-
// Drop the internal zip index so it doesn't leak into the label output.
|
|
213
|
-
const { index: _index, ...rest } = r;
|
|
214
|
-
results.push({
|
|
215
|
-
...rest,
|
|
216
|
-
y: r.y + ascent,
|
|
217
|
-
// The connector originates at the label; shift its start to the baseline
|
|
218
|
-
// too so the leader stays glued to the shifted text.
|
|
219
|
-
connector: r.connector
|
|
220
|
-
? {
|
|
221
|
-
...r.connector,
|
|
222
|
-
from: { ...r.connector.from, y: r.connector.from.y + ascent },
|
|
223
|
-
}
|
|
224
|
-
: undefined,
|
|
225
|
-
});
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
return results;
|
|
229
|
-
}
|
|
@@ -1,380 +0,0 @@
|
|
|
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
|
-
markType: 'circle',
|
|
24
|
-
markDef: { type: 'circle' },
|
|
25
|
-
data: [
|
|
26
|
-
{ country: 'USA', score: 85 },
|
|
27
|
-
{ country: 'UK', score: 72 },
|
|
28
|
-
{ country: 'Japan', score: 90 },
|
|
29
|
-
{ country: 'France', score: 68 },
|
|
30
|
-
],
|
|
31
|
-
encoding: {
|
|
32
|
-
x: { field: 'score', type: 'quantitative' },
|
|
33
|
-
y: { field: 'country', type: 'nominal' },
|
|
34
|
-
},
|
|
35
|
-
chrome: {},
|
|
36
|
-
annotations: [],
|
|
37
|
-
responsive: true,
|
|
38
|
-
theme: {},
|
|
39
|
-
darkMode: 'off',
|
|
40
|
-
labels: { density: 'auto', format: '' },
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function makeColoredDotSpec(): NormalizedChartSpec {
|
|
45
|
-
return {
|
|
46
|
-
markType: 'circle',
|
|
47
|
-
markDef: { type: 'circle' },
|
|
48
|
-
data: [
|
|
49
|
-
{ item: 'Revenue', value: 120, status: 'good' },
|
|
50
|
-
{ item: 'Costs', value: 80, status: 'neutral' },
|
|
51
|
-
{ item: 'Profit', value: 40, status: 'good' },
|
|
52
|
-
{ item: 'Debt', value: -10, status: 'bad' },
|
|
53
|
-
],
|
|
54
|
-
encoding: {
|
|
55
|
-
x: { field: 'value', type: 'quantitative' },
|
|
56
|
-
y: { field: 'item', type: 'nominal' },
|
|
57
|
-
color: { field: 'status', type: 'nominal' },
|
|
58
|
-
},
|
|
59
|
-
chrome: {},
|
|
60
|
-
annotations: [],
|
|
61
|
-
responsive: true,
|
|
62
|
-
theme: {},
|
|
63
|
-
darkMode: 'off',
|
|
64
|
-
labels: { density: 'auto', format: '' },
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// ---------------------------------------------------------------------------
|
|
69
|
-
// computeDotMarks tests
|
|
70
|
-
// ---------------------------------------------------------------------------
|
|
71
|
-
|
|
72
|
-
describe('computeDotMarks', () => {
|
|
73
|
-
describe('simple dot plot', () => {
|
|
74
|
-
it('produces PointMarks and RectMarks (stems)', () => {
|
|
75
|
-
const spec = makeSimpleDotSpec();
|
|
76
|
-
const scales = computeScales(spec, chartArea, spec.data);
|
|
77
|
-
const marks = computeDotMarks(spec, scales, chartArea, fullStrategy);
|
|
78
|
-
|
|
79
|
-
const points = marks.filter((m): m is PointMark => m.type === 'point');
|
|
80
|
-
const stems = marks.filter((m): m is RectMark => m.type === 'rect');
|
|
81
|
-
|
|
82
|
-
expect(points).toHaveLength(4);
|
|
83
|
-
expect(stems).toHaveLength(4);
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
it('dot positions reflect data values (higher score = further right)', () => {
|
|
87
|
-
const spec = makeSimpleDotSpec();
|
|
88
|
-
const scales = computeScales(spec, chartArea, spec.data);
|
|
89
|
-
const marks = computeDotMarks(spec, scales, chartArea, fullStrategy);
|
|
90
|
-
|
|
91
|
-
const points = marks.filter((m): m is PointMark => m.type === 'point');
|
|
92
|
-
const japan = points.find((m) => m.aria.label.includes('Japan'))!;
|
|
93
|
-
const france = points.find((m) => m.aria.label.includes('France'))!;
|
|
94
|
-
|
|
95
|
-
// Japan (90) should be further right than France (68)
|
|
96
|
-
expect(japan.cx).toBeGreaterThan(france.cx);
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
it('stems connect baseline to dot positions', () => {
|
|
100
|
-
const spec = makeSimpleDotSpec();
|
|
101
|
-
const scales = computeScales(spec, chartArea, spec.data);
|
|
102
|
-
const marks = computeDotMarks(spec, scales, chartArea, fullStrategy);
|
|
103
|
-
|
|
104
|
-
const stems = marks.filter((m): m is RectMark => m.type === 'rect');
|
|
105
|
-
for (const stem of stems) {
|
|
106
|
-
expect(stem.width).toBeGreaterThan(0);
|
|
107
|
-
expect(stem.height).toBe(2); // STEM_WIDTH
|
|
108
|
-
}
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
it('dots are vertically centered within their band', () => {
|
|
112
|
-
const spec = makeSimpleDotSpec();
|
|
113
|
-
const scales = computeScales(spec, chartArea, spec.data);
|
|
114
|
-
const marks = computeDotMarks(spec, scales, chartArea, fullStrategy);
|
|
115
|
-
|
|
116
|
-
const points = marks.filter((m): m is PointMark => m.type === 'point');
|
|
117
|
-
// All dots should be at different y positions (different categories)
|
|
118
|
-
const yPositions = points.map((p) => p.cy);
|
|
119
|
-
const uniqueY = new Set(yPositions);
|
|
120
|
-
expect(uniqueY.size).toBe(4);
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
it('dots have consistent radius', () => {
|
|
124
|
-
const spec = makeSimpleDotSpec();
|
|
125
|
-
const scales = computeScales(spec, chartArea, spec.data);
|
|
126
|
-
const marks = computeDotMarks(spec, scales, chartArea, fullStrategy);
|
|
127
|
-
|
|
128
|
-
const points = marks.filter((m): m is PointMark => m.type === 'point');
|
|
129
|
-
const radii = new Set(points.map((p) => p.r));
|
|
130
|
-
expect(radii.size).toBe(1);
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
it('each dot has an aria label with category and value', () => {
|
|
134
|
-
const spec = makeSimpleDotSpec();
|
|
135
|
-
const scales = computeScales(spec, chartArea, spec.data);
|
|
136
|
-
const marks = computeDotMarks(spec, scales, chartArea, fullStrategy);
|
|
137
|
-
|
|
138
|
-
const points = marks.filter((m): m is PointMark => m.type === 'point');
|
|
139
|
-
expect(points[0].aria.label).toContain('USA');
|
|
140
|
-
expect(points[0].aria.label).toContain('85');
|
|
141
|
-
});
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
describe('dumbbell (multi-series)', () => {
|
|
145
|
-
function makeDumbbellSpec(): NormalizedChartSpec {
|
|
146
|
-
return {
|
|
147
|
-
markType: 'circle',
|
|
148
|
-
markDef: { type: 'circle' },
|
|
149
|
-
data: [
|
|
150
|
-
{ country: 'USA', rate: 78, gender: 'Male' },
|
|
151
|
-
{ country: 'USA', rate: 82, gender: 'Female' },
|
|
152
|
-
{ country: 'Japan', rate: 85, gender: 'Male' },
|
|
153
|
-
{ country: 'Japan', rate: 90, gender: 'Female' },
|
|
154
|
-
{ country: 'Brazil', rate: 60, gender: 'Male' },
|
|
155
|
-
{ country: 'Brazil', rate: 65, gender: 'Female' },
|
|
156
|
-
],
|
|
157
|
-
encoding: {
|
|
158
|
-
x: { field: 'rate', type: 'quantitative' },
|
|
159
|
-
y: { field: 'country', type: 'nominal' },
|
|
160
|
-
color: { field: 'gender', type: 'nominal' },
|
|
161
|
-
},
|
|
162
|
-
chrome: {},
|
|
163
|
-
annotations: [],
|
|
164
|
-
responsive: true,
|
|
165
|
-
theme: {},
|
|
166
|
-
darkMode: 'off',
|
|
167
|
-
labels: { density: 'auto', format: '' },
|
|
168
|
-
};
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
it('renders connecting bars and dots for multi-series', () => {
|
|
172
|
-
const spec = makeDumbbellSpec();
|
|
173
|
-
const scales = computeScales(spec, chartArea, spec.data);
|
|
174
|
-
const marks = computeDotMarks(spec, scales, chartArea, fullStrategy);
|
|
175
|
-
|
|
176
|
-
const points = marks.filter((m): m is PointMark => m.type === 'point');
|
|
177
|
-
const bars = marks.filter((m): m is RectMark => m.type === 'rect');
|
|
178
|
-
|
|
179
|
-
// 6 data rows = 6 dots
|
|
180
|
-
expect(points).toHaveLength(6);
|
|
181
|
-
// 3 categories = 3 connecting bars
|
|
182
|
-
expect(bars).toHaveLength(3);
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
it('connecting bars span from min to max value per category', () => {
|
|
186
|
-
const spec = makeDumbbellSpec();
|
|
187
|
-
const scales = computeScales(spec, chartArea, spec.data);
|
|
188
|
-
const marks = computeDotMarks(spec, scales, chartArea, fullStrategy);
|
|
189
|
-
|
|
190
|
-
const bars = marks.filter((m): m is RectMark => m.type === 'rect');
|
|
191
|
-
for (const bar of bars) {
|
|
192
|
-
expect(bar.width).toBeGreaterThan(0);
|
|
193
|
-
}
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
it('connecting bars use stem color (gray)', () => {
|
|
197
|
-
const spec = makeDumbbellSpec();
|
|
198
|
-
const scales = computeScales(spec, chartArea, spec.data);
|
|
199
|
-
const marks = computeDotMarks(spec, scales, chartArea, fullStrategy);
|
|
200
|
-
|
|
201
|
-
const bars = marks.filter((m): m is RectMark => m.type === 'rect');
|
|
202
|
-
for (const bar of bars) {
|
|
203
|
-
expect(bar.fill).toBe('#cccccc');
|
|
204
|
-
}
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
it('dots within same category share the same cy position', () => {
|
|
208
|
-
const spec = makeDumbbellSpec();
|
|
209
|
-
const scales = computeScales(spec, chartArea, spec.data);
|
|
210
|
-
const marks = computeDotMarks(spec, scales, chartArea, fullStrategy);
|
|
211
|
-
|
|
212
|
-
const points = marks.filter((m): m is PointMark => m.type === 'point');
|
|
213
|
-
const usaDots = points.filter((m) => m.aria.label.includes('USA'));
|
|
214
|
-
expect(usaDots).toHaveLength(2);
|
|
215
|
-
expect(usaDots[0].cy).toBe(usaDots[1].cy);
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
it('dots for different series have different fill colors', () => {
|
|
219
|
-
const spec = makeDumbbellSpec();
|
|
220
|
-
const scales = computeScales(spec, chartArea, spec.data);
|
|
221
|
-
const marks = computeDotMarks(spec, scales, chartArea, fullStrategy);
|
|
222
|
-
|
|
223
|
-
const points = marks.filter((m): m is PointMark => m.type === 'point');
|
|
224
|
-
const maleDot = points.find((m) => m.aria.label.includes('Male'))!;
|
|
225
|
-
const femaleDot = points.find((m) => m.aria.label.includes('Female'))!;
|
|
226
|
-
expect(maleDot.fill).not.toBe(femaleDot.fill);
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
it('connecting bars render before dots (lower z-index)', () => {
|
|
230
|
-
const spec = makeDumbbellSpec();
|
|
231
|
-
const scales = computeScales(spec, chartArea, spec.data);
|
|
232
|
-
const marks = computeDotMarks(spec, scales, chartArea, fullStrategy);
|
|
233
|
-
|
|
234
|
-
// First mark should be a rect (connecting bar renders before dots)
|
|
235
|
-
expect(marks[0].type).toBe('rect');
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
it('aria labels include category and series name', () => {
|
|
239
|
-
const spec = makeDumbbellSpec();
|
|
240
|
-
const scales = computeScales(spec, chartArea, spec.data);
|
|
241
|
-
const marks = computeDotMarks(spec, scales, chartArea, fullStrategy);
|
|
242
|
-
|
|
243
|
-
const points = marks.filter((m): m is PointMark => m.type === 'point');
|
|
244
|
-
const usaMale = points.find(
|
|
245
|
-
(m) => m.aria.label.includes('USA') && m.aria.label.includes('Male'),
|
|
246
|
-
);
|
|
247
|
-
expect(usaMale).toBeDefined();
|
|
248
|
-
expect(usaMale!.aria.label).toContain('78');
|
|
249
|
-
});
|
|
250
|
-
});
|
|
251
|
-
|
|
252
|
-
describe('negative values', () => {
|
|
253
|
-
it('handles negative values (stem extends left of baseline)', () => {
|
|
254
|
-
const spec = makeColoredDotSpec();
|
|
255
|
-
const scales = computeScales(spec, chartArea, spec.data);
|
|
256
|
-
const marks = computeDotMarks(spec, scales, chartArea, fullStrategy);
|
|
257
|
-
|
|
258
|
-
const points = marks.filter((m): m is PointMark => m.type === 'point');
|
|
259
|
-
const debtDot = points.find((m) => m.data.item === 'Debt')!;
|
|
260
|
-
const revenueDot = points.find((m) => m.data.item === 'Revenue')!;
|
|
261
|
-
|
|
262
|
-
// Debt (-10) should be to the left of Revenue (120)
|
|
263
|
-
expect(debtDot.cx).toBeLessThan(revenueDot.cx);
|
|
264
|
-
});
|
|
265
|
-
});
|
|
266
|
-
|
|
267
|
-
describe('baseline clamping', () => {
|
|
268
|
-
it('clamps stems to the plot area when domain does not include zero', () => {
|
|
269
|
-
const spec: NormalizedChartSpec = {
|
|
270
|
-
markType: 'circle',
|
|
271
|
-
markDef: { type: 'circle' },
|
|
272
|
-
data: [
|
|
273
|
-
{ country: 'USA', score: 50 },
|
|
274
|
-
{ country: 'UK', score: 80 },
|
|
275
|
-
],
|
|
276
|
-
encoding: {
|
|
277
|
-
x: { field: 'score', type: 'quantitative', scale: { domain: [40, 100] } },
|
|
278
|
-
y: { field: 'country', type: 'nominal' },
|
|
279
|
-
},
|
|
280
|
-
chrome: {},
|
|
281
|
-
annotations: [],
|
|
282
|
-
responsive: true,
|
|
283
|
-
theme: {},
|
|
284
|
-
darkMode: 'off',
|
|
285
|
-
labels: { density: 'auto', format: '' },
|
|
286
|
-
};
|
|
287
|
-
const scales = computeScales(spec, chartArea, spec.data);
|
|
288
|
-
const marks = computeDotMarks(spec, scales, chartArea, fullStrategy);
|
|
289
|
-
|
|
290
|
-
const stems = marks.filter((m): m is RectMark => m.type === 'rect');
|
|
291
|
-
for (const stem of stems) {
|
|
292
|
-
expect(stem.x).toBeGreaterThanOrEqual(chartArea.x);
|
|
293
|
-
expect(stem.x + stem.width).toBeLessThanOrEqual(chartArea.x + chartArea.width);
|
|
294
|
-
}
|
|
295
|
-
});
|
|
296
|
-
});
|
|
297
|
-
|
|
298
|
-
describe('edge cases', () => {
|
|
299
|
-
it('returns empty array when no x encoding', () => {
|
|
300
|
-
const spec: NormalizedChartSpec = {
|
|
301
|
-
markType: 'circle',
|
|
302
|
-
markDef: { type: 'circle' },
|
|
303
|
-
data: [{ country: 'USA', score: 85 }],
|
|
304
|
-
encoding: {
|
|
305
|
-
y: { field: 'country', type: 'nominal' },
|
|
306
|
-
},
|
|
307
|
-
chrome: {},
|
|
308
|
-
annotations: [],
|
|
309
|
-
responsive: true,
|
|
310
|
-
theme: {},
|
|
311
|
-
darkMode: 'off',
|
|
312
|
-
labels: { density: 'auto', format: '' },
|
|
313
|
-
};
|
|
314
|
-
const scales = computeScales(spec, chartArea, spec.data);
|
|
315
|
-
const marks = computeDotMarks(spec, scales, chartArea, fullStrategy);
|
|
316
|
-
expect(marks).toHaveLength(0);
|
|
317
|
-
});
|
|
318
|
-
|
|
319
|
-
it('returns empty array for empty data', () => {
|
|
320
|
-
const spec: NormalizedChartSpec = {
|
|
321
|
-
markType: 'circle',
|
|
322
|
-
markDef: { type: 'circle' },
|
|
323
|
-
data: [],
|
|
324
|
-
encoding: {
|
|
325
|
-
x: { field: 'score', type: 'quantitative' },
|
|
326
|
-
y: { field: 'country', type: 'nominal' },
|
|
327
|
-
},
|
|
328
|
-
chrome: {},
|
|
329
|
-
annotations: [],
|
|
330
|
-
responsive: true,
|
|
331
|
-
theme: {},
|
|
332
|
-
darkMode: 'off',
|
|
333
|
-
labels: { density: 'auto', format: '' },
|
|
334
|
-
};
|
|
335
|
-
const scales = computeScales(spec, chartArea, spec.data);
|
|
336
|
-
const marks = computeDotMarks(spec, scales, chartArea, fullStrategy);
|
|
337
|
-
expect(marks).toHaveLength(0);
|
|
338
|
-
});
|
|
339
|
-
});
|
|
340
|
-
});
|
|
341
|
-
|
|
342
|
-
// ---------------------------------------------------------------------------
|
|
343
|
-
// computeDotLabels tests
|
|
344
|
-
// ---------------------------------------------------------------------------
|
|
345
|
-
|
|
346
|
-
describe('computeDotLabels', () => {
|
|
347
|
-
it('produces labels for each dot mark', () => {
|
|
348
|
-
const spec = makeSimpleDotSpec();
|
|
349
|
-
const scales = computeScales(spec, chartArea, spec.data);
|
|
350
|
-
const allMarks = computeDotMarks(spec, scales, chartArea, fullStrategy);
|
|
351
|
-
const points = allMarks.filter((m): m is PointMark => m.type === 'point');
|
|
352
|
-
const labels = computeDotLabels(points, chartArea);
|
|
353
|
-
|
|
354
|
-
expect(labels).toHaveLength(points.length);
|
|
355
|
-
});
|
|
356
|
-
|
|
357
|
-
it('labels are positioned to the right of dots', () => {
|
|
358
|
-
const spec = makeSimpleDotSpec();
|
|
359
|
-
const scales = computeScales(spec, chartArea, spec.data);
|
|
360
|
-
const allMarks = computeDotMarks(spec, scales, chartArea, fullStrategy);
|
|
361
|
-
const points = allMarks.filter((m): m is PointMark => m.type === 'point');
|
|
362
|
-
const labels = computeDotLabels(points, chartArea);
|
|
363
|
-
|
|
364
|
-
for (let i = 0; i < labels.length; i++) {
|
|
365
|
-
expect(labels[i].x).toBeGreaterThan(points[i].cx);
|
|
366
|
-
}
|
|
367
|
-
});
|
|
368
|
-
|
|
369
|
-
it('labels contain the numeric values', () => {
|
|
370
|
-
const spec = makeSimpleDotSpec();
|
|
371
|
-
const scales = computeScales(spec, chartArea, spec.data);
|
|
372
|
-
const allMarks = computeDotMarks(spec, scales, chartArea, fullStrategy);
|
|
373
|
-
const points = allMarks.filter((m): m is PointMark => m.type === 'point');
|
|
374
|
-
const labels = computeDotLabels(points, chartArea);
|
|
375
|
-
|
|
376
|
-
const texts = labels.map((l) => l.text);
|
|
377
|
-
expect(texts).toContain('85');
|
|
378
|
-
expect(texts).toContain('90');
|
|
379
|
-
});
|
|
380
|
-
});
|