@opendata-ai/openchart-core 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-core",
3
- "version": "7.1.4",
3
+ "version": "7.2.0",
4
4
  "description": "Types, theme, colors, accessibility, and utilities for openchart",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Riley Hilliard",
@@ -50,6 +50,9 @@ export const ACHROMATIC_RAMP = {
50
50
  * -> sRGB pipeline. Documented OKLCH source values are the contract; if
51
51
  * the conversion math changes, regenerate from the source rather than
52
52
  * editing hex literals directly.
53
+ *
54
+ * Tuned at L≈0.65, C≈0.20 (vs prior L≈0.70, C≈0.15) for more vivid,
55
+ * saturated color on dark backgrounds where the lighter pastels read soft.
53
56
  */
54
57
  // Order interleaves hues so adjacent slots sit at least ~70° apart on the
55
58
  // OKLCH wheel. Cyan and teal differ by only ~10° and read as the same color
@@ -57,14 +60,14 @@ export const ACHROMATIC_RAMP = {
57
60
  // teal slot is pushed deep into the ramp where it won't neighbor cyan.
58
61
  export const CATEGORICAL_PALETTE = [
59
62
  '#06b6d4', // cyan, primary accent (sRGB literal, ~205°)
60
- '#eb7289', // rose — oklch(70% 0.15 10)
61
- '#3bb974', // emerald — oklch(70% 0.15 155)
62
- '#ad87ed', // violet — oklch(70% 0.15 300)
63
- '#e69c3a', // amber — oklch(75% 0.14 70)
64
- '#4ba3f7', // sky — oklch(70% 0.15 250)
65
- '#eb8656', // orange — oklch(72% 0.14 45)
66
- '#8494fa', // indigo — oklch(70% 0.15 275)
67
- '#00b9c3', // teal — oklch(70% 0.15 200)
63
+ '#ee4a73', // rose — oklch(65% 0.20 10)
64
+ '#00b054', // emerald — oklch(65% 0.20 155)
65
+ '#a46bf5', // violet — oklch(65% 0.20 300)
66
+ '#e07d00', // amber — oklch(68% 0.19 70)
67
+ '#0091ff', // sky — oklch(65% 0.20 250)
68
+ '#f36000', // orange — oklch(67% 0.20 45)
69
+ '#6f7dff', // indigo — oklch(65% 0.20 275)
70
+ '#00afbf', // teal — oklch(65% 0.20 200)
68
71
  ] as const;
69
72
 
70
73
  export type CategoricalPalette = typeof CATEGORICAL_PALETTE;
@@ -83,6 +83,26 @@ describe('resolveTheme', () => {
83
83
  expect(resolved.fonts.family).toBe(DEFAULT_THEME.fonts.family);
84
84
  expect(resolved.spacing.padding).toBe(DEFAULT_THEME.spacing.padding);
85
85
  });
86
+
87
+ it('fonts.sizes overrides propagate to chrome element fontSizes', () => {
88
+ const resolved = resolveTheme({
89
+ fonts: { sizes: { title: 40, subtitle: 20, small: 16, axisTick: 16 } },
90
+ });
91
+ expect(resolved.chrome.title.fontSize).toBe(40);
92
+ expect(resolved.chrome.subtitle.fontSize).toBe(20);
93
+ expect(resolved.chrome.source.fontSize).toBe(16);
94
+ expect(resolved.chrome.byline.fontSize).toBe(16);
95
+ expect(resolved.chrome.footer.fontSize).toBe(16);
96
+ expect(resolved.chrome.eyebrow.fontSize).toBe(DEFAULT_THEME.chrome.eyebrow.fontSize);
97
+ expect(resolved.fonts.sizes.body).toBe(DEFAULT_THEME.fonts.sizes.body);
98
+ });
99
+
100
+ it('fonts.sizes partial override only changes specified sizes', () => {
101
+ const resolved = resolveTheme({ fonts: { sizes: { title: 36 } } });
102
+ expect(resolved.chrome.title.fontSize).toBe(36);
103
+ expect(resolved.chrome.subtitle.fontSize).toBe(DEFAULT_THEME.chrome.subtitle.fontSize);
104
+ expect(resolved.chrome.source.fontSize).toBe(DEFAULT_THEME.chrome.source.fontSize);
105
+ });
86
106
  });
87
107
 
88
108
  // ---------------------------------------------------------------------------
@@ -56,6 +56,8 @@ export const DEFAULT_THEME: Theme = {
56
56
  chromeToChart: 8,
57
57
  chartToFooter: 8,
58
58
  axisMargin: 6,
59
+ xAxisHeight: 26,
60
+ xAxisLabelPadding: 14,
59
61
  },
60
62
  borderRadius: 2,
61
63
  chrome: {
@@ -72,6 +72,7 @@ function themeConfigToPartial(config: ThemeConfig): Partial<Theme> {
72
72
  const fonts: Partial<Theme['fonts']> = {};
73
73
  if (config.fonts.family) fonts.family = config.fonts.family;
74
74
  if (config.fonts.mono) fonts.mono = config.fonts.mono;
75
+ if (config.fonts.sizes) fonts.sizes = config.fonts.sizes as Theme['fonts']['sizes'];
75
76
  partial.fonts = fonts as Theme['fonts'];
76
77
  }
77
78
 
@@ -79,6 +80,9 @@ function themeConfigToPartial(config: ThemeConfig): Partial<Theme> {
79
80
  const spacing: Partial<Theme['spacing']> = {};
80
81
  if (config.spacing.padding !== undefined) spacing.padding = config.spacing.padding;
81
82
  if (config.spacing.chromeGap !== undefined) spacing.chromeGap = config.spacing.chromeGap;
83
+ if (config.spacing.xAxisHeight !== undefined) spacing.xAxisHeight = config.spacing.xAxisHeight;
84
+ if (config.spacing.xAxisLabelPadding !== undefined)
85
+ spacing.xAxisLabelPadding = config.spacing.xAxisLabelPadding;
82
86
  partial.spacing = spacing as Theme['spacing'];
83
87
  }
84
88
 
@@ -198,6 +202,30 @@ function adjustOpacity(hex: string, opacity: number): string {
198
202
  export function resolveTheme(userTheme?: ThemeConfig, base: Theme = DEFAULT_THEME): ResolvedTheme {
199
203
  let merged: Theme = userTheme ? deepMerge(base, themeConfigToPartial(userTheme)) : { ...base };
200
204
 
205
+ // Propagate fonts.sizes overrides onto the chrome element fontSizes.
206
+ // The chrome defaults hard-code fontSize independently from fonts.sizes,
207
+ // so we sync them here after merging so a single sizes override applies everywhere.
208
+ if (userTheme?.fonts?.sizes) {
209
+ const s = userTheme.fonts.sizes;
210
+ merged = {
211
+ ...merged,
212
+ chrome: {
213
+ ...merged.chrome,
214
+ ...(s.title !== undefined && {
215
+ title: { ...merged.chrome.title, fontSize: s.title },
216
+ }),
217
+ ...(s.subtitle !== undefined && {
218
+ subtitle: { ...merged.chrome.subtitle, fontSize: s.subtitle },
219
+ }),
220
+ ...(s.small !== undefined && {
221
+ source: { ...merged.chrome.source, fontSize: s.small },
222
+ byline: { ...merged.chrome.byline, fontSize: s.small },
223
+ footer: { ...merged.chrome.footer, fontSize: s.small },
224
+ }),
225
+ },
226
+ };
227
+ }
228
+
201
229
  // Auto-adapt chrome for dark backgrounds
202
230
  const dark = isDarkBackground(merged.colors.background);
203
231
  if (dark) {
package/src/types/spec.ts CHANGED
@@ -246,6 +246,8 @@ export interface AxisConfig {
246
246
  labelPadding?: number;
247
247
  /** Color override for axis tick labels and title. Useful in dual-axis charts to match axis color to its series. */
248
248
  labelColor?: string;
249
+ /** Literal string appended to every formatted tick label. e.g. "B" gives "$4.5B" when format is "$,.1~f". */
250
+ labelSuffix?: string;
249
251
  /** Secondary data field to display alongside each tick label. Renders in lighter weight/color. Only effective on categorical y-axis labels (horizontal bar charts). */
250
252
  labelField?: string;
251
253
  /**
@@ -810,6 +812,19 @@ export interface ThemeConfig {
810
812
  family?: string;
811
813
  /** Monospace font family (for tabular numbers). */
812
814
  mono?: string;
815
+ /** Font size overrides in pixels. Partial — only specified keys are overridden. */
816
+ sizes?: {
817
+ /** Chart title. Default: 26. */
818
+ title?: number;
819
+ /** Subtitle below the title. Default: 14. */
820
+ subtitle?: number;
821
+ /** Body text (tooltips, legend labels). Default: 13. */
822
+ body?: number;
823
+ /** Small text (source line, footer). Default: 11. */
824
+ small?: number;
825
+ /** Axis tick labels. Default: 11. */
826
+ axisTick?: number;
827
+ };
813
828
  };
814
829
  /** Spacing overrides in pixels. */
815
830
  spacing?: {
@@ -817,6 +832,10 @@ export interface ThemeConfig {
817
832
  padding?: number;
818
833
  /** Gap between chrome elements (title to subtitle, etc.). */
819
834
  chromeGap?: number;
835
+ /** Height reserved below chart area for x-axis tick labels. Increase when large axisTick font sizes cause label clipping. */
836
+ xAxisHeight?: number;
837
+ /** Gap in pixels between the x-axis line and tick label text. Increase when larger axisTick fonts sit too close to the axis line. */
838
+ xAxisLabelPadding?: number;
820
839
  };
821
840
  /** Border radius for chart container and tooltips. */
822
841
  borderRadius?: number;
@@ -863,10 +882,14 @@ export interface LabelConfig {
863
882
  format?: string;
864
883
  /** Literal string prepended to each formatted label value (e.g. "-" or "$"). */
865
884
  prefix?: string;
885
+ /** Literal string appended to each formatted label value (e.g. "%" or "x"). */
886
+ suffix?: string;
866
887
  /** Fixed CSS color for all labels. Overrides the default fill-derived color. */
867
888
  color?: string;
868
889
  /** Per-series pixel offsets for fine-tuning label positions, keyed by series name. */
869
890
  offsets?: Record<string, AnnotationOffset>;
891
+ /** Font size in pixels for bar/column value labels. */
892
+ fontSize?: number;
870
893
  }
871
894
 
872
895
  /** Shorthand: `false` disables all labels, `true` uses defaults, or pass a full config object. */
@@ -93,6 +93,10 @@ export interface ThemeSpacing {
93
93
  chartToFooter: number;
94
94
  /** Internal padding within the chart area (axes margins). */
95
95
  axisMargin: number;
96
+ /** Height reserved below the chart area for x-axis tick labels (and optional axis title). */
97
+ xAxisHeight: number;
98
+ /** Gap in pixels between the x-axis line and the top of non-rotated tick label text. */
99
+ xAxisLabelPadding: number;
96
100
  }
97
101
 
98
102
  // ---------------------------------------------------------------------------