@opendata-ai/openchart-engine 7.1.4 → 7.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opendata-ai/openchart-engine",
3
- "version": "7.1.4",
3
+ "version": "7.2.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",
@@ -48,7 +48,7 @@
48
48
  "typecheck": "tsc --noEmit"
49
49
  },
50
50
  "dependencies": {
51
- "@opendata-ai/openchart-core": "7.1.4",
51
+ "@opendata-ai/openchart-core": "7.2.0",
52
52
  "d3-array": "^3.2.0",
53
53
  "d3-format": "^3.1.2",
54
54
  "d3-interpolate": "^3.0.0",
@@ -360,14 +360,14 @@ exports[`compileChart snapshot (Step 7 oracle) > clipped-domain bar chart (data
360
360
  "background": "transparent",
361
361
  "categorical": [
362
362
  "#06b6d4",
363
- "#eb7289",
364
- "#3bb974",
365
- "#ad87ed",
366
- "#e69c3a",
367
- "#4ba3f7",
368
- "#eb8656",
369
- "#8494fa",
370
- "#00b9c3",
363
+ "#ee4a73",
364
+ "#00b054",
365
+ "#a46bf5",
366
+ "#e07d00",
367
+ "#0091ff",
368
+ "#f36000",
369
+ "#6f7dff",
370
+ "#00afbf",
371
371
  ],
372
372
  "diverging": {
373
373
  "brownTeal": [
@@ -461,6 +461,8 @@ exports[`compileChart snapshot (Step 7 oracle) > clipped-domain bar chart (data
461
461
  "chromeGap": 4,
462
462
  "chromeToChart": 8,
463
463
  "padding": 20,
464
+ "xAxisHeight": 26,
465
+ "xAxisLabelPadding": 14,
464
466
  },
465
467
  },
466
468
  "tooltipDescriptors": [
@@ -790,7 +792,7 @@ exports[`compileChart snapshot (Step 7 oracle) > legend-heavy multi-series line
790
792
  "value": "40",
791
793
  },
792
794
  {
793
- "color": "#eb7289",
795
+ "color": "#ee4a73",
794
796
  "dataY": 143.375,
795
797
  "labelLines": [
796
798
  "UK",
@@ -800,7 +802,7 @@ exports[`compileChart snapshot (Step 7 oracle) > legend-heavy multi-series line
800
802
  "dataX": 657.899,
801
803
  "fill": "transparent",
802
804
  "radius": 4,
803
- "stroke": "#eb7289",
805
+ "stroke": "#ee4a73",
804
806
  "strokeWidth": 2,
805
807
  "x": 661.899,
806
808
  "y": 143.375,
@@ -810,7 +812,7 @@ exports[`compileChart snapshot (Step 7 oracle) > legend-heavy multi-series line
810
812
  "value": "35",
811
813
  },
812
814
  {
813
- "color": "#3bb974",
815
+ "color": "#00b054",
814
816
  "dataY": 242.62999999999997,
815
817
  "labelLines": [
816
818
  "FR",
@@ -820,7 +822,7 @@ exports[`compileChart snapshot (Step 7 oracle) > legend-heavy multi-series line
820
822
  "dataX": 657.899,
821
823
  "fill": "transparent",
822
824
  "radius": 4,
823
- "stroke": "#3bb974",
825
+ "stroke": "#00b054",
824
826
  "strokeWidth": 2,
825
827
  "x": 661.899,
826
828
  "y": 242.62999999999997,
@@ -830,7 +832,7 @@ exports[`compileChart snapshot (Step 7 oracle) > legend-heavy multi-series line
830
832
  "value": "22",
831
833
  },
832
834
  {
833
- "color": "#ad87ed",
835
+ "color": "#a46bf5",
834
836
  "dataY": 196.82,
835
837
  "labelLines": [
836
838
  "DE",
@@ -840,7 +842,7 @@ exports[`compileChart snapshot (Step 7 oracle) > legend-heavy multi-series line
840
842
  "dataX": 657.899,
841
843
  "fill": "transparent",
842
844
  "radius": 4,
843
- "stroke": "#ad87ed",
845
+ "stroke": "#a46bf5",
844
846
  "strokeWidth": 2,
845
847
  "x": 661.899,
846
848
  "y": 196.82,
@@ -885,19 +887,19 @@ exports[`compileChart snapshot (Step 7 oracle) > legend-heavy multi-series line
885
887
  },
886
888
  {
887
889
  "active": true,
888
- "color": "#eb7289",
890
+ "color": "#ee4a73",
889
891
  "label": "UK",
890
892
  "shape": "line",
891
893
  },
892
894
  {
893
895
  "active": true,
894
- "color": "#3bb974",
896
+ "color": "#00b054",
895
897
  "label": "FR",
896
898
  "shape": "line",
897
899
  },
898
900
  {
899
901
  "active": true,
900
- "color": "#ad87ed",
902
+ "color": "#a46bf5",
901
903
  "label": "DE",
902
904
  "shape": "line",
903
905
  },
@@ -1035,7 +1037,7 @@ exports[`compileChart snapshot (Step 7 oracle) > legend-heavy multi-series line
1035
1037
  "tooltip": {
1036
1038
  "fields": [
1037
1039
  {
1038
- "color": "#eb7289",
1040
+ "color": "#ee4a73",
1039
1041
  "label": "country",
1040
1042
  "value": "UK",
1041
1043
  },
@@ -1063,7 +1065,7 @@ exports[`compileChart snapshot (Step 7 oracle) > legend-heavy multi-series line
1063
1065
  "tooltip": {
1064
1066
  "fields": [
1065
1067
  {
1066
- "color": "#eb7289",
1068
+ "color": "#ee4a73",
1067
1069
  "label": "country",
1068
1070
  "value": "UK",
1069
1071
  },
@@ -1096,7 +1098,7 @@ exports[`compileChart snapshot (Step 7 oracle) > legend-heavy multi-series line
1096
1098
  },
1097
1099
  ],
1098
1100
  "seriesKey": "UK",
1099
- "stroke": "#eb7289",
1101
+ "stroke": "#ee4a73",
1100
1102
  "strokeDasharray": undefined,
1101
1103
  "strokeWidth": 1.5,
1102
1104
  "type": "line",
@@ -1127,7 +1129,7 @@ exports[`compileChart snapshot (Step 7 oracle) > legend-heavy multi-series line
1127
1129
  "tooltip": {
1128
1130
  "fields": [
1129
1131
  {
1130
- "color": "#3bb974",
1132
+ "color": "#00b054",
1131
1133
  "label": "country",
1132
1134
  "value": "FR",
1133
1135
  },
@@ -1155,7 +1157,7 @@ exports[`compileChart snapshot (Step 7 oracle) > legend-heavy multi-series line
1155
1157
  "tooltip": {
1156
1158
  "fields": [
1157
1159
  {
1158
- "color": "#3bb974",
1160
+ "color": "#00b054",
1159
1161
  "label": "country",
1160
1162
  "value": "FR",
1161
1163
  },
@@ -1188,7 +1190,7 @@ exports[`compileChart snapshot (Step 7 oracle) > legend-heavy multi-series line
1188
1190
  },
1189
1191
  ],
1190
1192
  "seriesKey": "FR",
1191
- "stroke": "#3bb974",
1193
+ "stroke": "#00b054",
1192
1194
  "strokeDasharray": undefined,
1193
1195
  "strokeWidth": 1.5,
1194
1196
  "type": "line",
@@ -1219,7 +1221,7 @@ exports[`compileChart snapshot (Step 7 oracle) > legend-heavy multi-series line
1219
1221
  "tooltip": {
1220
1222
  "fields": [
1221
1223
  {
1222
- "color": "#ad87ed",
1224
+ "color": "#a46bf5",
1223
1225
  "label": "country",
1224
1226
  "value": "DE",
1225
1227
  },
@@ -1247,7 +1249,7 @@ exports[`compileChart snapshot (Step 7 oracle) > legend-heavy multi-series line
1247
1249
  "tooltip": {
1248
1250
  "fields": [
1249
1251
  {
1250
- "color": "#ad87ed",
1252
+ "color": "#a46bf5",
1251
1253
  "label": "country",
1252
1254
  "value": "DE",
1253
1255
  },
@@ -1280,7 +1282,7 @@ exports[`compileChart snapshot (Step 7 oracle) > legend-heavy multi-series line
1280
1282
  },
1281
1283
  ],
1282
1284
  "seriesKey": "DE",
1283
- "stroke": "#ad87ed",
1285
+ "stroke": "#a46bf5",
1284
1286
  "strokeDasharray": undefined,
1285
1287
  "strokeWidth": 1.5,
1286
1288
  "type": "line",
@@ -1334,14 +1336,14 @@ exports[`compileChart snapshot (Step 7 oracle) > legend-heavy multi-series line
1334
1336
  "background": "transparent",
1335
1337
  "categorical": [
1336
1338
  "#06b6d4",
1337
- "#eb7289",
1338
- "#3bb974",
1339
- "#ad87ed",
1340
- "#e69c3a",
1341
- "#4ba3f7",
1342
- "#eb8656",
1343
- "#8494fa",
1344
- "#00b9c3",
1339
+ "#ee4a73",
1340
+ "#00b054",
1341
+ "#a46bf5",
1342
+ "#e07d00",
1343
+ "#0091ff",
1344
+ "#f36000",
1345
+ "#6f7dff",
1346
+ "#00afbf",
1345
1347
  ],
1346
1348
  "diverging": {
1347
1349
  "brownTeal": [
@@ -1435,6 +1437,8 @@ exports[`compileChart snapshot (Step 7 oracle) > legend-heavy multi-series line
1435
1437
  "chromeGap": 4,
1436
1438
  "chromeToChart": 8,
1437
1439
  "padding": 20,
1440
+ "xAxisHeight": 26,
1441
+ "xAxisLabelPadding": 14,
1438
1442
  },
1439
1443
  },
1440
1444
  "tooltipDescriptors": [],
@@ -1923,14 +1927,14 @@ exports[`compileChart snapshot (Step 7 oracle) > watermarked column chart with g
1923
1927
  "background": "transparent",
1924
1928
  "categorical": [
1925
1929
  "#06b6d4",
1926
- "#eb7289",
1927
- "#3bb974",
1928
- "#ad87ed",
1929
- "#e69c3a",
1930
- "#4ba3f7",
1931
- "#eb8656",
1932
- "#8494fa",
1933
- "#00b9c3",
1930
+ "#ee4a73",
1931
+ "#00b054",
1932
+ "#a46bf5",
1933
+ "#e07d00",
1934
+ "#0091ff",
1935
+ "#f36000",
1936
+ "#6f7dff",
1937
+ "#00afbf",
1934
1938
  ],
1935
1939
  "diverging": {
1936
1940
  "brownTeal": [
@@ -2024,6 +2028,8 @@ exports[`compileChart snapshot (Step 7 oracle) > watermarked column chart with g
2024
2028
  "chromeGap": 4,
2025
2029
  "chromeToChart": 8,
2026
2030
  "padding": 20,
2031
+ "xAxisHeight": 26,
2032
+ "xAxisLabelPadding": 14,
2027
2033
  },
2028
2034
  },
2029
2035
  "tooltipDescriptors": [
@@ -28,6 +28,8 @@ export const barRenderer: ChartRenderer = (spec, scales, chartArea, strategy, th
28
28
  valueField,
29
29
  spec.labels.color,
30
30
  theme.isDark,
31
+ spec.labels.fontSize,
32
+ spec.labels.suffix,
31
33
  );
32
34
  for (let i = 0; i < marks.length && i < labels.length; i++) {
33
35
  marks[i].label = labels[i];
@@ -96,7 +96,10 @@ export function computeBarLabels(
96
96
  valueField?: string,
97
97
  labelColor?: string,
98
98
  darkMode = false,
99
+ fontSize?: number,
100
+ labelSuffix?: string,
99
101
  ): ResolvedLabel[] {
102
+ const FONT_SIZE = fontSize ?? LABEL_FONT_SIZE;
100
103
  const targetMarks = filterByDensity(marks, density);
101
104
 
102
105
  const candidates: LabelCandidate[] = [];
@@ -132,9 +135,10 @@ export function computeBarLabels(
132
135
  }
133
136
  }
134
137
  if (labelPrefix) valuePart = labelPrefix + valuePart;
138
+ if (labelSuffix) valuePart = valuePart + labelSuffix;
135
139
 
136
- const textWidth = estimateTextWidth(valuePart, LABEL_FONT_SIZE, LABEL_FONT_WEIGHT);
137
- const textHeight = LABEL_FONT_SIZE * 1.2;
140
+ const textWidth = estimateTextWidth(valuePart, FONT_SIZE, LABEL_FONT_WEIGHT);
141
+ const textHeight = FONT_SIZE * 1.2;
138
142
 
139
143
  // Detect stacked bars: cornerRadius 0 indicates stacked segment
140
144
  const isStacked = mark.stackGroup !== undefined;
@@ -196,7 +200,7 @@ export function computeBarLabels(
196
200
  priority: 'data',
197
201
  style: {
198
202
  fontFamily: 'system-ui, -apple-system, sans-serif',
199
- fontSize: LABEL_FONT_SIZE,
203
+ fontSize: FONT_SIZE,
200
204
  fontWeight: LABEL_FONT_WEIGHT,
201
205
  fill,
202
206
  lineHeight: 1.2,
@@ -27,6 +27,8 @@ export const columnRenderer: ChartRenderer = (spec, scales, chartArea, strategy,
27
27
  spec.labels.prefix,
28
28
  valueField,
29
29
  spec.labels.color,
30
+ spec.labels.fontSize,
31
+ spec.labels.suffix,
30
32
  );
31
33
  for (let i = 0; i < marks.length && i < labels.length; i++) {
32
34
  marks[i].label = labels[i];
@@ -51,7 +51,10 @@ export function computeColumnLabels(
51
51
  labelPrefix?: string,
52
52
  valueField?: string,
53
53
  labelColor?: string,
54
+ fontSize?: number,
55
+ labelSuffix?: string,
54
56
  ): ResolvedLabel[] {
57
+ const FONT_SIZE = fontSize ?? LABEL_FONT_SIZE;
55
58
  const targetMarks = filterByDensity(marks, density);
56
59
 
57
60
  const formatter = buildD3Formatter(labelFormat);
@@ -83,13 +86,14 @@ export function computeColumnLabels(
83
86
  valuePart = rawValue;
84
87
  }
85
88
  }
86
- if (labelPrefix) valuePart = labelPrefix + valuePart;
87
-
88
89
  const numericValue = parseFloat(valuePart);
89
90
  const isNegative = Number.isFinite(numericValue) && numericValue < 0;
90
91
 
91
- const textWidth = estimateTextWidth(valuePart, LABEL_FONT_SIZE, LABEL_FONT_WEIGHT);
92
- const textHeight = LABEL_FONT_SIZE * 1.2;
92
+ if (labelPrefix) valuePart = labelPrefix + valuePart;
93
+ if (labelSuffix) valuePart = valuePart + labelSuffix;
94
+
95
+ const textWidth = estimateTextWidth(valuePart, FONT_SIZE, LABEL_FONT_WEIGHT);
96
+ const textHeight = FONT_SIZE * 1.2;
93
97
 
94
98
  // anchorY is the TOP of the label bounding box so the collision system's
95
99
  // AABB check (rect = { y: anchorY, height: textHeight }) is geometrically
@@ -110,7 +114,7 @@ export function computeColumnLabels(
110
114
  priority: 'data',
111
115
  style: {
112
116
  fontFamily: 'system-ui, -apple-system, sans-serif',
113
- fontSize: LABEL_FONT_SIZE,
117
+ fontSize: FONT_SIZE,
114
118
  fontWeight: LABEL_FONT_WEIGHT,
115
119
  fill: labelColor ?? getRepresentativeColor(mark.fill),
116
120
  lineHeight: 1.2,
@@ -210,8 +210,10 @@ function normalizeLabels(labels?: LabelSpec): NormalizedChartSpec['labels'] {
210
210
  density: labels.density ?? 'auto',
211
211
  format: labels.format ?? '',
212
212
  prefix: labels.prefix ?? '',
213
+ suffix: labels.suffix,
213
214
  offsets: labels.offsets,
214
215
  color: labels.color,
216
+ fontSize: labels.fontSize,
215
217
  };
216
218
  }
217
219
 
@@ -110,7 +110,7 @@ export interface NormalizedChartSpec {
110
110
  annotations: Annotation[];
111
111
  /** Normalized label configuration with defaults applied. density, format, and prefix are always set; offsets and color stay optional. */
112
112
  labels: Required<Pick<LabelConfig, 'density' | 'format' | 'prefix'>> &
113
- Pick<LabelConfig, 'offsets' | 'color'>;
113
+ Pick<LabelConfig, 'offsets' | 'color' | 'fontSize' | 'suffix'>;
114
114
  /** Legend configuration (position override). */
115
115
  legend?: LegendConfig;
116
116
  /** Right-side endpoint labels column config (multi-series line/area only). */
@@ -126,26 +126,27 @@ const TEMPORAL_SCALE_TYPES = new Set(['time', 'utc']);
126
126
  function formatTickLabel(value: unknown, resolvedScale: ResolvedScale): string {
127
127
  const axisConfig = resolvedScale.channel.axis || undefined;
128
128
  const formatStr = axisConfig?.format;
129
+ const suffix = axisConfig?.labelSuffix ?? '';
129
130
 
130
131
  if (TEMPORAL_SCALE_TYPES.has(resolvedScale.type)) {
131
132
  const temporalFmt = buildTemporalFormatter(formatStr);
132
- if (temporalFmt) return temporalFmt(value as Date);
133
+ if (temporalFmt) return temporalFmt(value as Date) + suffix;
133
134
  const useUtc = resolvedScale.type === 'utc';
134
- return formatDate(value as Date, undefined, undefined, useUtc);
135
+ return formatDate(value as Date, undefined, undefined, useUtc) + suffix;
135
136
  }
136
137
 
137
138
  if (NUMERIC_SCALE_TYPES.has(resolvedScale.type)) {
138
139
  const num = value as number;
139
140
  if (formatStr) {
140
141
  const fmt = buildD3Formatter(formatStr);
141
- if (fmt) return fmt(num);
142
+ if (fmt) return fmt(num) + suffix;
142
143
  }
143
144
  // Abbreviate large numbers for axis labels
144
- if (Math.abs(num) >= 1000) return abbreviateNumber(num);
145
- return formatNumber(num);
145
+ if (Math.abs(num) >= 1000) return abbreviateNumber(num) + suffix;
146
+ return formatNumber(num) + suffix;
146
147
  }
147
148
 
148
- return String(value);
149
+ return String(value) + suffix;
149
150
  }
150
151
 
151
152
  /**
@@ -338,7 +338,8 @@ export function computeDimensions(
338
338
  const labelHeight = Math.min(rotatedHeight, 120);
339
339
  xAxisHeight = hasXAxisLabel ? labelHeight + 20 : labelHeight;
340
340
  } else {
341
- xAxisHeight = hasXAxisLabel ? 48 : 26;
341
+ const base = theme.spacing.xAxisHeight;
342
+ xAxisHeight = hasXAxisLabel ? base + 22 : base;
342
343
  }
343
344
 
344
345
  // Resolve effective y-axis tickPosition early so margin math can account