@opendata-ai/openchart-engine 6.6.0 → 6.7.1
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 +2 -2
- package/dist/index.js +23 -7
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/__test-fixtures__/specs.ts +3 -3
- package/src/charts/bar/index.ts +7 -1
- package/src/charts/bar/labels.ts +2 -0
- package/src/charts/column/index.ts +7 -1
- package/src/charts/column/labels.ts +2 -0
- package/src/charts/dot/index.ts +1 -1
- package/src/charts/dot/labels.ts +3 -1
- package/src/compiler/__tests__/normalize.test.ts +18 -3
- package/src/compiler/normalize.ts +1 -0
- package/src/compiler/types.ts +3 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opendata-ai/openchart-engine",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.7.1",
|
|
4
4
|
"description": "Headless compiler for openchart: spec validation, data compilation, scales, and layout",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Riley Hilliard",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"typecheck": "tsc --noEmit"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"@opendata-ai/openchart-core": "6.
|
|
48
|
+
"@opendata-ai/openchart-core": "6.7.1",
|
|
49
49
|
"d3-array": "^3.2.0",
|
|
50
50
|
"d3-format": "^3.1.2",
|
|
51
51
|
"d3-interpolate": "^3.0.0",
|
|
@@ -70,7 +70,7 @@ export function makeLineSpec(): NormalizedChartSpec {
|
|
|
70
70
|
responsive: true,
|
|
71
71
|
theme: {},
|
|
72
72
|
darkMode: 'off',
|
|
73
|
-
labels: { density: 'auto', format: '' },
|
|
73
|
+
labels: { density: 'auto', format: '', prefix: '' },
|
|
74
74
|
hiddenSeries: [],
|
|
75
75
|
seriesStyles: {},
|
|
76
76
|
};
|
|
@@ -99,7 +99,7 @@ export function makeBarSpec(): NormalizedChartSpec {
|
|
|
99
99
|
responsive: true,
|
|
100
100
|
theme: {},
|
|
101
101
|
darkMode: 'off',
|
|
102
|
-
labels: { density: 'auto', format: '' },
|
|
102
|
+
labels: { density: 'auto', format: '', prefix: '' },
|
|
103
103
|
hiddenSeries: [],
|
|
104
104
|
seriesStyles: {},
|
|
105
105
|
};
|
|
@@ -130,7 +130,7 @@ export function makeScatterSpec(): NormalizedChartSpec {
|
|
|
130
130
|
responsive: true,
|
|
131
131
|
theme: {},
|
|
132
132
|
darkMode: 'off',
|
|
133
|
-
labels: { density: 'auto', format: '' },
|
|
133
|
+
labels: { density: 'auto', format: '', prefix: '' },
|
|
134
134
|
hiddenSeries: [],
|
|
135
135
|
seriesStyles: {},
|
|
136
136
|
};
|
package/src/charts/bar/index.ts
CHANGED
|
@@ -17,7 +17,13 @@ export const barRenderer: ChartRenderer = (spec, scales, chartArea, strategy, _t
|
|
|
17
17
|
const marks = computeBarMarks(spec, scales, chartArea, strategy);
|
|
18
18
|
|
|
19
19
|
// Compute and attach value labels (respects spec.labels.density)
|
|
20
|
-
const labels = computeBarLabels(
|
|
20
|
+
const labels = computeBarLabels(
|
|
21
|
+
marks,
|
|
22
|
+
chartArea,
|
|
23
|
+
spec.labels.density,
|
|
24
|
+
spec.labels.format,
|
|
25
|
+
spec.labels.prefix,
|
|
26
|
+
);
|
|
21
27
|
for (let i = 0; i < marks.length && i < labels.length; i++) {
|
|
22
28
|
marks[i].label = labels[i];
|
|
23
29
|
}
|
package/src/charts/bar/labels.ts
CHANGED
|
@@ -82,6 +82,7 @@ export function computeBarLabels(
|
|
|
82
82
|
_chartArea: { x: number; y: number; width: number; height: number },
|
|
83
83
|
density: LabelDensity = 'auto',
|
|
84
84
|
labelFormat?: string,
|
|
85
|
+
labelPrefix?: string,
|
|
85
86
|
): ResolvedLabel[] {
|
|
86
87
|
// 'none': no labels at all
|
|
87
88
|
if (density === 'none') return [];
|
|
@@ -112,6 +113,7 @@ export function computeBarLabels(
|
|
|
112
113
|
const num = parseDisplayNumber(rawValue);
|
|
113
114
|
if (!Number.isNaN(num)) valuePart = formatter(num);
|
|
114
115
|
}
|
|
116
|
+
if (labelPrefix) valuePart = labelPrefix + valuePart;
|
|
115
117
|
|
|
116
118
|
const textWidth = estimateTextWidth(valuePart, LABEL_FONT_SIZE, LABEL_FONT_WEIGHT);
|
|
117
119
|
const textHeight = LABEL_FONT_SIZE * 1.2;
|
|
@@ -17,7 +17,13 @@ export const columnRenderer: ChartRenderer = (spec, scales, chartArea, strategy,
|
|
|
17
17
|
const marks = computeColumnMarks(spec, scales, chartArea, strategy);
|
|
18
18
|
|
|
19
19
|
// Compute and attach value labels (respects spec.labels.density)
|
|
20
|
-
const labels = computeColumnLabels(
|
|
20
|
+
const labels = computeColumnLabels(
|
|
21
|
+
marks,
|
|
22
|
+
chartArea,
|
|
23
|
+
spec.labels.density,
|
|
24
|
+
spec.labels.format,
|
|
25
|
+
spec.labels.prefix,
|
|
26
|
+
);
|
|
21
27
|
for (let i = 0; i < marks.length && i < labels.length; i++) {
|
|
22
28
|
marks[i].label = labels[i];
|
|
23
29
|
}
|
|
@@ -45,6 +45,7 @@ export function computeColumnLabels(
|
|
|
45
45
|
_chartArea: { x: number; y: number; width: number; height: number },
|
|
46
46
|
density: LabelDensity = 'auto',
|
|
47
47
|
labelFormat?: string,
|
|
48
|
+
labelPrefix?: string,
|
|
48
49
|
): ResolvedLabel[] {
|
|
49
50
|
// 'none': no labels at all
|
|
50
51
|
if (density === 'none') return [];
|
|
@@ -72,6 +73,7 @@ export function computeColumnLabels(
|
|
|
72
73
|
const num = Number(rawValue.replace(/[^0-9.-]/g, ''));
|
|
73
74
|
if (!Number.isNaN(num)) valuePart = formatter(num);
|
|
74
75
|
}
|
|
76
|
+
if (labelPrefix) valuePart = labelPrefix + valuePart;
|
|
75
77
|
|
|
76
78
|
const numericValue = parseFloat(valuePart);
|
|
77
79
|
const isNegative = Number.isFinite(numericValue) && numericValue < 0;
|
package/src/charts/dot/index.ts
CHANGED
|
@@ -26,7 +26,7 @@ export const dotRenderer: ChartRenderer = (spec, scales, chartArea, strategy, _t
|
|
|
26
26
|
const pointMarks = marks.filter((m): m is PointMark => m.type === 'point');
|
|
27
27
|
|
|
28
28
|
// Compute and attach labels to point marks (respects spec.labels.density)
|
|
29
|
-
const labels = computeDotLabels(pointMarks, chartArea, spec.labels.density);
|
|
29
|
+
const labels = computeDotLabels(pointMarks, chartArea, spec.labels.density, spec.labels.prefix);
|
|
30
30
|
let labelIdx = 0;
|
|
31
31
|
for (const mark of marks) {
|
|
32
32
|
if (mark.type === 'point' && labelIdx < labels.length) {
|
package/src/charts/dot/labels.ts
CHANGED
|
@@ -40,6 +40,7 @@ export function computeDotLabels(
|
|
|
40
40
|
marks: PointMark[],
|
|
41
41
|
_chartArea: Rect,
|
|
42
42
|
density: LabelDensity = 'auto',
|
|
43
|
+
labelPrefix?: string,
|
|
43
44
|
): ResolvedLabel[] {
|
|
44
45
|
// 'none': no labels at all
|
|
45
46
|
if (density === 'none') return [];
|
|
@@ -55,8 +56,9 @@ export function computeDotLabels(
|
|
|
55
56
|
// Format is "category: value". Use the last colon to handle colons in category names.
|
|
56
57
|
const ariaLabel = mark.aria.label;
|
|
57
58
|
const lastColon = ariaLabel.lastIndexOf(':');
|
|
58
|
-
|
|
59
|
+
let valuePart = lastColon >= 0 ? ariaLabel.slice(lastColon + 1).trim() : '';
|
|
59
60
|
if (!valuePart) continue;
|
|
61
|
+
if (labelPrefix) valuePart = labelPrefix + valuePart;
|
|
60
62
|
|
|
61
63
|
const textWidth = estimateTextWidth(valuePart, LABEL_FONT_SIZE, LABEL_FONT_WEIGHT);
|
|
62
64
|
const textHeight = LABEL_FONT_SIZE * 1.2;
|
|
@@ -44,7 +44,12 @@ describe('normalizeSpec', () => {
|
|
|
44
44
|
expect(result.darkMode).toBe('off');
|
|
45
45
|
expect(result.annotations).toEqual([]);
|
|
46
46
|
expect(result.theme).toEqual({});
|
|
47
|
-
expect(result.labels).toEqual({
|
|
47
|
+
expect(result.labels).toEqual({
|
|
48
|
+
density: 'auto',
|
|
49
|
+
format: '',
|
|
50
|
+
prefix: '',
|
|
51
|
+
offsets: undefined,
|
|
52
|
+
});
|
|
48
53
|
});
|
|
49
54
|
|
|
50
55
|
it('preserves explicit values', () => {
|
|
@@ -139,7 +144,12 @@ describe('normalizeSpec', () => {
|
|
|
139
144
|
labels: { density: 'none', format: ',.0f' },
|
|
140
145
|
};
|
|
141
146
|
const result = normalizeSpec(spec) as NormalizedChartSpec;
|
|
142
|
-
expect(result.labels).toEqual({
|
|
147
|
+
expect(result.labels).toEqual({
|
|
148
|
+
density: 'none',
|
|
149
|
+
format: ',.0f',
|
|
150
|
+
prefix: '',
|
|
151
|
+
offsets: undefined,
|
|
152
|
+
});
|
|
143
153
|
});
|
|
144
154
|
|
|
145
155
|
it('fills partial label config with defaults', () => {
|
|
@@ -148,7 +158,12 @@ describe('normalizeSpec', () => {
|
|
|
148
158
|
labels: { density: 'endpoints' },
|
|
149
159
|
};
|
|
150
160
|
const result = normalizeSpec(spec) as NormalizedChartSpec;
|
|
151
|
-
expect(result.labels).toEqual({
|
|
161
|
+
expect(result.labels).toEqual({
|
|
162
|
+
density: 'endpoints',
|
|
163
|
+
format: '',
|
|
164
|
+
prefix: '',
|
|
165
|
+
offsets: undefined,
|
|
166
|
+
});
|
|
152
167
|
});
|
|
153
168
|
|
|
154
169
|
it('normalizes annotations with default styles', () => {
|
|
@@ -206,6 +206,7 @@ function normalizeChartSpec(spec: ChartSpec, warnings: string[]): NormalizedChar
|
|
|
206
206
|
labels: {
|
|
207
207
|
density: spec.labels?.density ?? 'auto',
|
|
208
208
|
format: spec.labels?.format ?? '',
|
|
209
|
+
prefix: spec.labels?.prefix ?? '',
|
|
209
210
|
offsets: spec.labels?.offsets,
|
|
210
211
|
},
|
|
211
212
|
legend: spec.legend,
|
package/src/compiler/types.ts
CHANGED
|
@@ -70,8 +70,9 @@ export interface NormalizedChartSpec {
|
|
|
70
70
|
encoding: Encoding;
|
|
71
71
|
chrome: NormalizedChrome;
|
|
72
72
|
annotations: Annotation[];
|
|
73
|
-
/** Normalized label configuration with defaults applied. density and
|
|
74
|
-
labels: Required<Pick<LabelConfig, 'density' | 'format'
|
|
73
|
+
/** Normalized label configuration with defaults applied. density, format, and prefix are always set; offsets stays optional. */
|
|
74
|
+
labels: Required<Pick<LabelConfig, 'density' | 'format' | 'prefix'>> &
|
|
75
|
+
Pick<LabelConfig, 'offsets'>;
|
|
75
76
|
/** Legend configuration (position override). */
|
|
76
77
|
legend?: LegendConfig;
|
|
77
78
|
responsive: boolean;
|