@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
package/src/charts/text/index.ts
DELETED
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Text mark renderer.
|
|
3
|
-
*
|
|
4
|
-
* Computes TextMarkLayout marks from a normalized chart spec.
|
|
5
|
-
* Positions text at data coordinates using x/y encoding channels,
|
|
6
|
-
* with content from the text encoding channel.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import type { Encoding, Mark, MarkAria, TextMarkLayout } from '@opendata-ai/openchart-core';
|
|
10
|
-
import { getRepresentativeColor } from '@opendata-ai/openchart-core';
|
|
11
|
-
import { buildSizeScale, SIZE_SCALE_DEFAULTS } from '../../compile/size-scale';
|
|
12
|
-
import { dedupeKeys, serializeKeyValue } from '../../compiler/keys';
|
|
13
|
-
import type { NormalizedChartSpec } from '../../compiler/types';
|
|
14
|
-
import type { ResolvedScales } from '../../layout/scales';
|
|
15
|
-
import type { ChartRenderer } from '../registry';
|
|
16
|
-
import { getColor, scaleValue } from '../utils';
|
|
17
|
-
|
|
18
|
-
/** Font size for text marks with no `size` encoding and no `mark.fontSize`. */
|
|
19
|
-
const DEFAULT_FONT_SIZE = 12;
|
|
20
|
-
|
|
21
|
-
const ALIGN_TO_ANCHOR = {
|
|
22
|
-
left: 'start',
|
|
23
|
-
center: 'middle',
|
|
24
|
-
right: 'end',
|
|
25
|
-
} as const satisfies Record<string, TextMarkLayout['textAnchor']>;
|
|
26
|
-
|
|
27
|
-
const BASELINE_TO_DOMINANT = {
|
|
28
|
-
top: 'hanging',
|
|
29
|
-
middle: 'central',
|
|
30
|
-
bottom: 'text-after-edge',
|
|
31
|
-
} as const satisfies Record<string, NonNullable<TextMarkLayout['dominantBaseline']>>;
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Compute text marks from spec data and resolved scales.
|
|
35
|
-
*/
|
|
36
|
-
export function computeTextMarks(
|
|
37
|
-
spec: NormalizedChartSpec,
|
|
38
|
-
scales: ResolvedScales,
|
|
39
|
-
): TextMarkLayout[] {
|
|
40
|
-
const encoding = spec.encoding as Encoding;
|
|
41
|
-
const xChannel = encoding.x;
|
|
42
|
-
const yChannel = encoding.y;
|
|
43
|
-
const textChannel = encoding.text;
|
|
44
|
-
|
|
45
|
-
if (!textChannel || !('field' in textChannel)) return [];
|
|
46
|
-
|
|
47
|
-
const marks: TextMarkLayout[] = [];
|
|
48
|
-
const colorEncoding = encoding.color && 'field' in encoding.color ? encoding.color : undefined;
|
|
49
|
-
const colorField = colorEncoding?.field;
|
|
50
|
-
const sizeEncoding = encoding.size && 'field' in encoding.size ? encoding.size : undefined;
|
|
51
|
-
|
|
52
|
-
const markDef = spec.markDef ?? {};
|
|
53
|
-
const dx = markDef.dx ?? 0;
|
|
54
|
-
const dy = markDef.dy ?? 0;
|
|
55
|
-
const isOffset = dx !== 0 || dy !== 0;
|
|
56
|
-
const textAnchor = ALIGN_TO_ANCHOR[markDef.align ?? 'center'];
|
|
57
|
-
const dominantBaseline = BASELINE_TO_DOMINANT[markDef.baseline ?? 'middle'];
|
|
58
|
-
|
|
59
|
-
// Map the `size` field onto a font-size range. Linear, not sqrt: sqrt is right
|
|
60
|
-
// for bubbles because perceived magnitude tracks *area* (∝ r²), but a glyph
|
|
61
|
-
// reads by its height, which is linear in font size.
|
|
62
|
-
//
|
|
63
|
-
// Unlike scatter/beeswarm, this channel is *not* keyed: the library ships no
|
|
64
|
-
// font-size legend, and no publication uses one. Treat it as emphasis, not as
|
|
65
|
-
// a channel a reader is expected to decode back into a value.
|
|
66
|
-
const fontSizeScale = buildSizeScale(sizeEncoding, spec.data, SIZE_SCALE_DEFAULTS.text)?.scale;
|
|
67
|
-
|
|
68
|
-
for (const row of spec.data) {
|
|
69
|
-
// Resolve x position (center of chart if no x encoding)
|
|
70
|
-
let x = 0;
|
|
71
|
-
if (xChannel && scales.x) {
|
|
72
|
-
const xVal = scaleValue(scales.x.scale, scales.x.type, row[xChannel.field]);
|
|
73
|
-
if (xVal == null) continue;
|
|
74
|
-
x = xVal;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// Resolve y position (center of chart if no y encoding)
|
|
78
|
-
let y = 0;
|
|
79
|
-
if (yChannel && scales.y) {
|
|
80
|
-
const yVal = scaleValue(scales.y.scale, scales.y.type, row[yChannel.field]);
|
|
81
|
-
if (yVal == null) continue;
|
|
82
|
-
y = yVal;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
const text = String(row[textChannel.field] ?? '');
|
|
86
|
-
if (!text) continue;
|
|
87
|
-
|
|
88
|
-
const color = getRepresentativeColor(
|
|
89
|
-
colorField
|
|
90
|
-
? getColor(scales, String(row[colorField] ?? '__default__'))
|
|
91
|
-
: getColor(scales, '__default__'),
|
|
92
|
-
);
|
|
93
|
-
|
|
94
|
-
const fallbackFontSize = markDef.fontSize ?? DEFAULT_FONT_SIZE;
|
|
95
|
-
const sizeValue = sizeEncoding ? Number(row[sizeEncoding.field]) : Number.NaN;
|
|
96
|
-
const fontSize =
|
|
97
|
-
fontSizeScale && Number.isFinite(sizeValue) ? fontSizeScale(sizeValue) : fallbackFontSize;
|
|
98
|
-
|
|
99
|
-
const aria: MarkAria = {
|
|
100
|
-
label: text,
|
|
101
|
-
};
|
|
102
|
-
|
|
103
|
-
// Bake the offset into x/y rather than emitting SVG dx/dy: the transition
|
|
104
|
-
// driver tweens the x/y attributes and rotation pivots on them, so a
|
|
105
|
-
// separate offset attribute would leave both reading the un-offset anchor.
|
|
106
|
-
marks.push({
|
|
107
|
-
type: 'textMark',
|
|
108
|
-
x: x + dx,
|
|
109
|
-
y: y + dy,
|
|
110
|
-
...(isOffset ? { anchorX: x, anchorY: y } : {}),
|
|
111
|
-
text,
|
|
112
|
-
fill: color,
|
|
113
|
-
fontSize,
|
|
114
|
-
textAnchor,
|
|
115
|
-
dominantBaseline,
|
|
116
|
-
angle:
|
|
117
|
-
encoding.angle && 'field' in encoding.angle
|
|
118
|
-
? Number(row[encoding.angle.field]) || 0
|
|
119
|
-
: undefined,
|
|
120
|
-
data: row as Record<string, unknown>,
|
|
121
|
-
aria,
|
|
122
|
-
});
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// Stamp keys: position-defining value, fall back to index
|
|
126
|
-
const posField = xChannel?.field ?? yChannel?.field;
|
|
127
|
-
const rawKeys = marks.map((m, i) => (posField ? serializeKeyValue(m.data[posField]) : String(i)));
|
|
128
|
-
const keys = dedupeKeys(rawKeys);
|
|
129
|
-
for (let i = 0; i < marks.length; i++) {
|
|
130
|
-
marks[i].key = keys[i];
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
return marks;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Text chart renderer.
|
|
138
|
-
*/
|
|
139
|
-
export const textRenderer: ChartRenderer = (spec, scales, _chartArea, _strategy, _theme) => {
|
|
140
|
-
return computeTextMarks(spec, scales) as Mark[];
|
|
141
|
-
};
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
import type { TickMarkLayout } from '@opendata-ai/openchart-core';
|
|
2
|
-
import { describe, expect, it } from 'vitest';
|
|
3
|
-
import { compileChart } from '../../../compile';
|
|
4
|
-
|
|
5
|
-
const OPTIONS = { width: 800, height: 500 };
|
|
6
|
-
|
|
7
|
-
const ROWS = [
|
|
8
|
-
{ group: '20-29', hours: 3.1 },
|
|
9
|
-
{ group: '20-29', hours: 4.7 },
|
|
10
|
-
{ group: '60+', hours: 4.1 },
|
|
11
|
-
{ group: '60+', hours: 7.0 },
|
|
12
|
-
];
|
|
13
|
-
|
|
14
|
-
function tickMarks(layout: ReturnType<typeof compileChart>): TickMarkLayout[] {
|
|
15
|
-
return layout.marks.filter((m): m is TickMarkLayout => m.type === 'tick');
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
describe('tick marks', () => {
|
|
19
|
-
it('draws vertical strokes when the value axis is x', () => {
|
|
20
|
-
const layout = compileChart(
|
|
21
|
-
{
|
|
22
|
-
mark: 'tick',
|
|
23
|
-
data: ROWS,
|
|
24
|
-
encoding: {
|
|
25
|
-
x: { field: 'hours', type: 'quantitative' },
|
|
26
|
-
y: { field: 'group', type: 'nominal' },
|
|
27
|
-
},
|
|
28
|
-
},
|
|
29
|
-
OPTIONS,
|
|
30
|
-
);
|
|
31
|
-
|
|
32
|
-
const marks = tickMarks(layout);
|
|
33
|
-
expect(marks).toHaveLength(ROWS.length);
|
|
34
|
-
// Perpendicular to the quantitative axis: values run left-to-right, so each
|
|
35
|
-
// observation is a vertical stroke. Horizontal strokes would lie along the
|
|
36
|
-
// value axis and butt end-to-end into a solid bar.
|
|
37
|
-
expect(marks.every((m) => m.orient === 'vertical')).toBe(true);
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it('draws horizontal strokes when the value axis is y', () => {
|
|
41
|
-
const layout = compileChart(
|
|
42
|
-
{
|
|
43
|
-
mark: 'tick',
|
|
44
|
-
data: ROWS,
|
|
45
|
-
encoding: {
|
|
46
|
-
x: { field: 'group', type: 'nominal' },
|
|
47
|
-
y: { field: 'hours', type: 'quantitative' },
|
|
48
|
-
},
|
|
49
|
-
},
|
|
50
|
-
OPTIONS,
|
|
51
|
-
);
|
|
52
|
-
|
|
53
|
-
expect(tickMarks(layout).every((m) => m.orient === 'horizontal')).toBe(true);
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it('separates observations within a category along the value axis', () => {
|
|
57
|
-
const layout = compileChart(
|
|
58
|
-
{
|
|
59
|
-
mark: 'tick',
|
|
60
|
-
data: ROWS,
|
|
61
|
-
encoding: {
|
|
62
|
-
x: { field: 'hours', type: 'quantitative' },
|
|
63
|
-
y: { field: 'group', type: 'nominal' },
|
|
64
|
-
},
|
|
65
|
-
},
|
|
66
|
-
OPTIONS,
|
|
67
|
-
);
|
|
68
|
-
|
|
69
|
-
const young = tickMarks(layout).filter((m) => m.data.group === '20-29');
|
|
70
|
-
expect(young).toHaveLength(2);
|
|
71
|
-
// Same row, different x: the strip plot's entire job.
|
|
72
|
-
expect(young[0].y).toBe(young[1].y);
|
|
73
|
-
expect(young[0].x).not.toBe(young[1].x);
|
|
74
|
-
});
|
|
75
|
-
});
|
package/src/charts/tick/index.ts
DELETED
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tick mark renderer.
|
|
3
|
-
*
|
|
4
|
-
* Computes TickMarkLayout marks from a normalized chart spec.
|
|
5
|
-
* Ticks are short line segments used for strip/rug plots.
|
|
6
|
-
* Each data point produces a small tick perpendicular to the data axis.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import type { Encoding, Mark, MarkAria, Rect, TickMarkLayout } from '@opendata-ai/openchart-core';
|
|
10
|
-
import { getRepresentativeColor } from '@opendata-ai/openchart-core';
|
|
11
|
-
|
|
12
|
-
import { dedupeKeys, serializeKeyValue } from '../../compiler/keys';
|
|
13
|
-
import type { NormalizedChartSpec } from '../../compiler/types';
|
|
14
|
-
import type { ResolvedScales } from '../../layout/scales';
|
|
15
|
-
import type { ChartRenderer } from '../registry';
|
|
16
|
-
import { getColor, scaleValue } from '../utils';
|
|
17
|
-
|
|
18
|
-
/** Default tick length in pixels. */
|
|
19
|
-
const DEFAULT_TICK_LENGTH = 18;
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Compute tick marks from spec data and resolved scales.
|
|
23
|
-
*
|
|
24
|
-
* Orientation is inferred from the encoding:
|
|
25
|
-
* - If x is quantitative and y is categorical (or vice versa), ticks are
|
|
26
|
-
* perpendicular to the quantitative axis.
|
|
27
|
-
* - Default: vertical ticks (short vertical lines at each x position).
|
|
28
|
-
*/
|
|
29
|
-
export function computeTickMarks(
|
|
30
|
-
spec: NormalizedChartSpec,
|
|
31
|
-
scales: ResolvedScales,
|
|
32
|
-
_chartArea: Rect,
|
|
33
|
-
): TickMarkLayout[] {
|
|
34
|
-
const encoding = spec.encoding as Encoding;
|
|
35
|
-
const xChannel = encoding.x;
|
|
36
|
-
const yChannel = encoding.y;
|
|
37
|
-
|
|
38
|
-
if (!xChannel || !yChannel || !scales.x || !scales.y) return [];
|
|
39
|
-
|
|
40
|
-
const colorEncoding = encoding.color && 'field' in encoding.color ? encoding.color : undefined;
|
|
41
|
-
const colorField = colorEncoding?.field;
|
|
42
|
-
const marks: TickMarkLayout[] = [];
|
|
43
|
-
|
|
44
|
-
// Ticks run perpendicular to the quantitative axis: a quantitative x (values
|
|
45
|
-
// spread left-to-right along a categorical row) gets vertical strokes, and a
|
|
46
|
-
// quantitative y gets horizontal ones. Drawing them parallel instead makes
|
|
47
|
-
// neighbouring observations butt end-to-end and read as one continuous bar.
|
|
48
|
-
const isQuantX = xChannel.type === 'quantitative' && yChannel.type !== 'quantitative';
|
|
49
|
-
const orient: 'horizontal' | 'vertical' = isQuantX ? 'vertical' : 'horizontal';
|
|
50
|
-
|
|
51
|
-
for (const row of spec.data) {
|
|
52
|
-
const xVal = scaleValue(scales.x.scale, scales.x.type, row[xChannel.field]);
|
|
53
|
-
const yVal = scaleValue(scales.y.scale, scales.y.type, row[yChannel.field]);
|
|
54
|
-
if (xVal == null || yVal == null) continue;
|
|
55
|
-
|
|
56
|
-
const color = getRepresentativeColor(
|
|
57
|
-
colorField
|
|
58
|
-
? getColor(scales, String(row[colorField] ?? '__default__'))
|
|
59
|
-
: getColor(scales, '__default__'),
|
|
60
|
-
);
|
|
61
|
-
|
|
62
|
-
const aria: MarkAria = {
|
|
63
|
-
label: `${row[xChannel.field]}, ${row[yChannel.field]}`,
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
marks.push({
|
|
67
|
-
type: 'tick',
|
|
68
|
-
x: xVal,
|
|
69
|
-
y: yVal,
|
|
70
|
-
length: DEFAULT_TICK_LENGTH,
|
|
71
|
-
orient,
|
|
72
|
-
stroke: color,
|
|
73
|
-
strokeWidth: 1,
|
|
74
|
-
opacity:
|
|
75
|
-
encoding.opacity && 'field' in encoding.opacity
|
|
76
|
-
? Math.max(0, Math.min(1, Number(row[encoding.opacity.field]) || 1))
|
|
77
|
-
: undefined,
|
|
78
|
-
data: row as Record<string, unknown>,
|
|
79
|
-
aria,
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// Stamp keys: position-defining value, fall back to index
|
|
84
|
-
const posField = isQuantX ? xChannel.field : yChannel.field;
|
|
85
|
-
const rawKeys = marks.map((m) => serializeKeyValue(m.data[posField]));
|
|
86
|
-
const keys = dedupeKeys(rawKeys);
|
|
87
|
-
for (let i = 0; i < marks.length; i++) {
|
|
88
|
-
marks[i].key = keys[i];
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
return marks;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Tick chart renderer.
|
|
96
|
-
*/
|
|
97
|
-
export const tickRenderer: ChartRenderer = (spec, scales, chartArea, _strategy, _theme) => {
|
|
98
|
-
return computeTickMarks(spec, scales, chartArea) as Mark[];
|
|
99
|
-
};
|
package/src/charts/utils.ts
DELETED
|
@@ -1,170 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared utilities for chart mark computation.
|
|
3
|
-
*
|
|
4
|
-
* Common helpers used across multiple chart types: scale value resolution,
|
|
5
|
-
* data grouping, color lookup, and shared constants.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import type { DataRow, GradientDef } from '@opendata-ai/openchart-core';
|
|
9
|
-
import type { ScaleBand, ScaleLinear, ScalePoint, ScaleTime } from 'd3-scale';
|
|
10
|
-
import type { D3Scale, ResolvedScales } from '../layout/scales';
|
|
11
|
-
|
|
12
|
-
// ---------------------------------------------------------------------------
|
|
13
|
-
// Constants
|
|
14
|
-
// ---------------------------------------------------------------------------
|
|
15
|
-
|
|
16
|
-
/** Default single-series color when no color encoding is present. */
|
|
17
|
-
export const DEFAULT_COLOR = '#1b7fa3';
|
|
18
|
-
|
|
19
|
-
// ---------------------------------------------------------------------------
|
|
20
|
-
// Scale helpers
|
|
21
|
-
// ---------------------------------------------------------------------------
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Resolve a data value to a pixel position using a D3 scale.
|
|
25
|
-
*
|
|
26
|
-
* Handles time scales (parsing string dates), categorical scales
|
|
27
|
-
* (point, band, ordinal - passing string values directly), and
|
|
28
|
-
* linear/log scales (coercing to number). Returns null for values
|
|
29
|
-
* that can't be resolved (null, NaN, invalid dates, or values not
|
|
30
|
-
* in a categorical scale's domain).
|
|
31
|
-
*/
|
|
32
|
-
export function scaleValue(scale: D3Scale, scaleType: string, value: unknown): number | null {
|
|
33
|
-
if (value == null) return null;
|
|
34
|
-
|
|
35
|
-
if (scaleType === 'time' || scaleType === 'utc') {
|
|
36
|
-
const date = value instanceof Date ? value : new Date(String(value));
|
|
37
|
-
if (Number.isNaN(date.getTime())) return null;
|
|
38
|
-
return (scale as ScaleTime<number, number>)(date);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// Categorical scales: pass string values directly
|
|
42
|
-
if (scaleType === 'point' || scaleType === 'band' || scaleType === 'ordinal') {
|
|
43
|
-
const result = (scale as ScalePoint<string> | ScaleBand<string>)(String(value));
|
|
44
|
-
return result ?? null;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const num = typeof value === 'number' ? value : Number(value);
|
|
48
|
-
if (!Number.isFinite(num)) return null;
|
|
49
|
-
return (scale as ScaleLinear<number, number>)(num);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// ---------------------------------------------------------------------------
|
|
53
|
-
// Data grouping
|
|
54
|
-
// ---------------------------------------------------------------------------
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Group data rows by a field value.
|
|
58
|
-
*
|
|
59
|
-
* If no field is provided, all rows are grouped under '__default__'.
|
|
60
|
-
* Returns a Map preserving insertion order.
|
|
61
|
-
*/
|
|
62
|
-
export function groupByField(data: DataRow[], field: string | undefined): Map<string, DataRow[]> {
|
|
63
|
-
const groups = new Map<string, DataRow[]>();
|
|
64
|
-
|
|
65
|
-
if (!field) {
|
|
66
|
-
groups.set('__default__', data);
|
|
67
|
-
return groups;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
for (const row of data) {
|
|
71
|
-
const key = String(row[field] ?? '__default__');
|
|
72
|
-
const existing = groups.get(key);
|
|
73
|
-
if (existing) {
|
|
74
|
-
existing.push(row);
|
|
75
|
-
} else {
|
|
76
|
-
groups.set(key, [row]);
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
return groups;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// ---------------------------------------------------------------------------
|
|
84
|
-
// Sorting
|
|
85
|
-
// ---------------------------------------------------------------------------
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Sort data rows by a field value in ascending order.
|
|
89
|
-
*
|
|
90
|
-
* Type-aware: numbers compared numerically, Date objects by timestamp,
|
|
91
|
-
* string-encoded numbers parsed and compared numerically, and everything
|
|
92
|
-
* else compared lexicographically (which also handles ISO date strings).
|
|
93
|
-
* Nulls are sorted last. Returns a new array (no mutation).
|
|
94
|
-
*/
|
|
95
|
-
export function sortByField(data: DataRow[], field: string): DataRow[] {
|
|
96
|
-
if (data.length <= 1) return [...data];
|
|
97
|
-
|
|
98
|
-
return [...data].sort((a, b) => {
|
|
99
|
-
const aVal = a[field];
|
|
100
|
-
const bVal = b[field];
|
|
101
|
-
|
|
102
|
-
// Nulls last
|
|
103
|
-
if (aVal == null && bVal == null) return 0;
|
|
104
|
-
if (aVal == null) return 1;
|
|
105
|
-
if (bVal == null) return -1;
|
|
106
|
-
|
|
107
|
-
// Both numbers
|
|
108
|
-
if (typeof aVal === 'number' && typeof bVal === 'number') {
|
|
109
|
-
return aVal - bVal;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// Both Dates
|
|
113
|
-
if (aVal instanceof Date && bVal instanceof Date) {
|
|
114
|
-
return aVal.getTime() - bVal.getTime();
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// String values: try numeric parse, then lexicographic
|
|
118
|
-
const aStr = String(aVal);
|
|
119
|
-
const bStr = String(bVal);
|
|
120
|
-
|
|
121
|
-
const aNum = Number(aStr);
|
|
122
|
-
const bNum = Number(bStr);
|
|
123
|
-
if (Number.isFinite(aNum) && Number.isFinite(bNum)) {
|
|
124
|
-
return aNum - bNum;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
return aStr.localeCompare(bStr);
|
|
128
|
-
});
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// ---------------------------------------------------------------------------
|
|
132
|
-
// Color helpers
|
|
133
|
-
// ---------------------------------------------------------------------------
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* Get the color for a series/category from the resolved color scale.
|
|
137
|
-
*
|
|
138
|
-
* For single-series charts (key === '__default__'), uses the theme's
|
|
139
|
-
* first categorical color via scales.defaultColor.
|
|
140
|
-
*/
|
|
141
|
-
export function getColor(
|
|
142
|
-
scales: ResolvedScales,
|
|
143
|
-
key: string,
|
|
144
|
-
_index?: number,
|
|
145
|
-
fallback: string = DEFAULT_COLOR,
|
|
146
|
-
): string | GradientDef {
|
|
147
|
-
if (scales.color && key !== '__default__') {
|
|
148
|
-
const colorScale = scales.color.scale as (v: string) => string;
|
|
149
|
-
return colorScale(key);
|
|
150
|
-
}
|
|
151
|
-
return scales.defaultColor ?? fallback;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* Get color from a sequential (quantitative) color scale.
|
|
156
|
-
* Maps a numeric value to a color via linear interpolation, or to a discrete
|
|
157
|
-
* class color for binned (quantile/quantize/threshold) color scales.
|
|
158
|
-
*/
|
|
159
|
-
export function getSequentialColor(
|
|
160
|
-
scales: ResolvedScales,
|
|
161
|
-
value: number,
|
|
162
|
-
fallback: string = DEFAULT_COLOR,
|
|
163
|
-
): string | GradientDef {
|
|
164
|
-
const type = scales.color?.type;
|
|
165
|
-
if (type === 'sequential' || type === 'quantile' || type === 'quantize' || type === 'threshold') {
|
|
166
|
-
const colorScale = scales.color!.scale as unknown as (v: number) => string;
|
|
167
|
-
return colorScale(value);
|
|
168
|
-
}
|
|
169
|
-
return scales.defaultColor ?? fallback;
|
|
170
|
-
}
|