@opendata-ai/openchart-core 2.6.0 → 2.8.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": "2.6.0",
3
+ "version": "2.8.0",
4
4
  "description": "Types, theme, colors, accessibility, and utilities for openchart",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Riley Hilliard",
package/src/index.ts CHANGED
@@ -92,6 +92,7 @@ export { resolveCollisions } from './labels/index';
92
92
  export type { DateGranularity } from './locale/index';
93
93
  export {
94
94
  abbreviateNumber,
95
+ buildD3Formatter,
95
96
  formatDate,
96
97
  formatNumber,
97
98
  } from './locale/index';
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, it } from 'vitest';
2
- import { abbreviateNumber, formatDate, formatNumber } from '../format';
2
+ import { abbreviateNumber, buildD3Formatter, formatDate, formatNumber } from '../format';
3
3
 
4
4
  describe('formatNumber', () => {
5
5
  it('formats integers with commas', () => {
@@ -59,6 +59,45 @@ describe('abbreviateNumber', () => {
59
59
  });
60
60
  });
61
61
 
62
+ describe('buildD3Formatter', () => {
63
+ it('returns a formatter for a valid d3 format string', () => {
64
+ const fmt = buildD3Formatter('$,.0f');
65
+ expect(fmt).not.toBeNull();
66
+ expect(fmt!(1234)).toBe('$1,234');
67
+ });
68
+
69
+ it('handles tilde trim modifier', () => {
70
+ const fmt = buildD3Formatter('$,.2~f');
71
+ expect(fmt).not.toBeNull();
72
+ expect(fmt!(3.1)).toBe('$3.1');
73
+ expect(fmt!(3.75)).toBe('$3.75');
74
+ });
75
+
76
+ it('handles literal alpha suffix after d3 format', () => {
77
+ const fmt = buildD3Formatter('$,.2~fT');
78
+ expect(fmt).not.toBeNull();
79
+ expect(fmt!(3.75)).toBe('$3.75T');
80
+ });
81
+
82
+ it('handles non-alpha suffix like %', () => {
83
+ const fmt = buildD3Formatter('.0f%');
84
+ expect(fmt).not.toBeNull();
85
+ expect(fmt!(50)).toBe('50%');
86
+ });
87
+
88
+ it('returns null for undefined input', () => {
89
+ expect(buildD3Formatter(undefined)).toBeNull();
90
+ });
91
+
92
+ it('returns null for empty string', () => {
93
+ expect(buildD3Formatter('')).toBeNull();
94
+ });
95
+
96
+ it('returns null for completely invalid format', () => {
97
+ expect(buildD3Formatter('not-a-format!!!')).toBeNull();
98
+ });
99
+ });
100
+
62
101
  describe('formatDate', () => {
63
102
  it('formats a Date object', () => {
64
103
  const result = formatDate(new Date('2020-06-15'), undefined, 'day');
@@ -69,6 +69,49 @@ export function abbreviateNumber(value: number): string {
69
69
  return formatNumber(value);
70
70
  }
71
71
 
72
+ // ---------------------------------------------------------------------------
73
+ // d3-format with suffix support
74
+ // ---------------------------------------------------------------------------
75
+
76
+ /**
77
+ * Regex that matches a valid d3-format specifier (with optional ~ trim flag)
78
+ * followed by a literal suffix. The first capture group is the d3 format part,
79
+ * the second is the trailing literal suffix (e.g. "T", "pp", etc.).
80
+ *
81
+ * Examples:
82
+ * - "$,.2~fT" -> ["$,.2~f", "T"]
83
+ * - ".0f%" -> [".0f", "%"] (% here is literal, not d3 percent type)
84
+ * - "$,.0f" -> no match (valid d3 format, no suffix)
85
+ */
86
+ const D3_FORMAT_SUFFIX_RE = /^(.*~?[efgsrdxXobcnp%])(.+)$/;
87
+
88
+ /**
89
+ * Build a number formatter from a d3-format string, with support for a
90
+ * trailing literal suffix that d3-format itself would reject.
91
+ *
92
+ * Returns null if the format string is falsy or unparseable.
93
+ */
94
+ export function buildD3Formatter(formatStr: string | undefined): ((v: number) => string) | null {
95
+ if (!formatStr) return null;
96
+
97
+ try {
98
+ return d3Format(formatStr);
99
+ } catch {
100
+ // If d3-format rejects it, try stripping a trailing literal suffix
101
+ const m = formatStr.match(D3_FORMAT_SUFFIX_RE);
102
+ if (m) {
103
+ try {
104
+ const fmt = d3Format(m[1]);
105
+ const suffix = m[2];
106
+ return (v: number) => fmt(v) + suffix;
107
+ } catch {
108
+ // Unparseable even after suffix stripping
109
+ }
110
+ }
111
+ }
112
+ return null;
113
+ }
114
+
72
115
  // ---------------------------------------------------------------------------
73
116
  // Date formatting
74
117
  // ---------------------------------------------------------------------------
@@ -3,4 +3,4 @@
3
3
  */
4
4
 
5
5
  export type { DateGranularity } from './format';
6
- export { abbreviateNumber, formatDate, formatNumber } from './format';
6
+ export { abbreviateNumber, buildD3Formatter, formatDate, formatNumber } from './format';
@@ -58,4 +58,19 @@ describe('resolveTheme', () => {
58
58
  const resolved = resolveTheme(undefined, customBase);
59
59
  expect(resolved.borderRadius).toBe(12);
60
60
  });
61
+
62
+ it('accepts flat string[] as shorthand for categorical colors', () => {
63
+ const colors = ['#ff0000', '#94a3b8', '#94a3b8'];
64
+ const resolved = resolveTheme({ colors });
65
+ expect(resolved.colors.categorical).toEqual(colors);
66
+ // Other color fields preserved from defaults
67
+ expect(resolved.colors.background).toBe(DEFAULT_THEME.colors.background);
68
+ expect(resolved.colors.text).toBe(DEFAULT_THEME.colors.text);
69
+ });
70
+
71
+ it('flat color array does not clobber non-color theme fields', () => {
72
+ const resolved = resolveTheme({ colors: ['#ff0000'] });
73
+ expect(resolved.fonts.family).toBe(DEFAULT_THEME.fonts.family);
74
+ expect(resolved.spacing.padding).toBe(DEFAULT_THEME.spacing.padding);
75
+ });
61
76
  });
@@ -79,6 +79,7 @@ export type {
79
79
  AnnotationOffset,
80
80
  AxisConfig,
81
81
  ChartSpec,
82
+ ChartSpecOverride,
82
83
  ChartSpecWithoutData,
83
84
  ChartType,
84
85
  Chrome,
package/src/types/spec.ts CHANGED
@@ -8,8 +8,8 @@
8
8
  * with editorial extensions for chrome, annotations, responsive, and dark mode.
9
9
  */
10
10
 
11
- // Re-import for use in LegendConfig (avoids circular by importing from sibling)
12
- import type { LegendPosition } from '../responsive/breakpoints';
11
+ // Re-import for use in LegendConfig and overrides (avoids circular by importing from sibling)
12
+ import type { Breakpoint, LegendPosition } from '../responsive/breakpoints';
13
13
  import type { ColumnConfig } from './table';
14
14
 
15
15
  // ---------------------------------------------------------------------------
@@ -395,6 +395,8 @@ export interface LegendConfig {
395
395
  position?: LegendPosition;
396
396
  /** Pixel offset for fine-tuning legend position. */
397
397
  offset?: AnnotationOffset;
398
+ /** Whether to show the legend. Defaults to true. Set to false to hide. */
399
+ show?: boolean;
398
400
  }
399
401
 
400
402
  // ---------------------------------------------------------------------------
@@ -416,6 +418,24 @@ export interface SeriesStyle {
416
418
  opacity?: number;
417
419
  }
418
420
 
421
+ /**
422
+ * Breakpoint-conditional overrides for chart specs.
423
+ *
424
+ * Allows specifying different chrome, labels, legend, or annotations
425
+ * per breakpoint. Merged shallowly into the base spec at compile time
426
+ * when the container matches the breakpoint.
427
+ */
428
+ export interface ChartSpecOverride {
429
+ /** Override editorial chrome at this breakpoint. */
430
+ chrome?: Chrome;
431
+ /** Override label configuration at this breakpoint. */
432
+ labels?: LabelConfig;
433
+ /** Override legend configuration at this breakpoint. */
434
+ legend?: LegendConfig;
435
+ /** Override annotations at this breakpoint. */
436
+ annotations?: Annotation[];
437
+ }
438
+
419
439
  /**
420
440
  * Chart specification: the primary input for standard chart types.
421
441
  *
@@ -448,6 +468,12 @@ export interface ChartSpec {
448
468
  hiddenSeries?: string[];
449
469
  /** Per-series visual overrides, keyed by series name (the color field value). */
450
470
  seriesStyles?: Record<string, SeriesStyle>;
471
+ /**
472
+ * Breakpoint-conditional overrides. Keys are breakpoint names.
473
+ * At compile time, if the container matches a breakpoint, its overrides
474
+ * are shallow-merged into the spec before layout computation.
475
+ */
476
+ overrides?: Partial<Record<Breakpoint, ChartSpecOverride>>;
451
477
  }
452
478
 
453
479
  /**