@opendata-ai/openchart-engine 6.6.0 → 6.7.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opendata-ai/openchart-engine",
3
- "version": "6.6.0",
3
+ "version": "6.7.0",
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.6.0",
48
+ "@opendata-ai/openchart-core": "6.7.0",
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
  };
@@ -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(marks, chartArea, spec.labels.density, spec.labels.format);
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
  }
@@ -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(marks, chartArea, spec.labels.density, spec.labels.format);
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;
@@ -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) {
@@ -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
- const valuePart = lastColon >= 0 ? ariaLabel.slice(lastColon + 1).trim() : '';
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({ density: 'auto', format: '', offsets: undefined });
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({ density: 'none', format: ',.0f', offsets: undefined });
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({ density: 'endpoints', format: '', offsets: undefined });
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,
@@ -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 format are always set; offsets stays optional. */
74
- labels: Required<Pick<LabelConfig, 'density' | 'format'>> & Pick<LabelConfig, 'offsets'>;
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;