@opendata-ai/openchart-engine 2.7.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-engine",
3
- "version": "2.7.0",
3
+ "version": "2.8.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": "2.7.0",
48
+ "@opendata-ai/openchart-core": "2.8.0",
49
49
  "d3-array": "^3.2.0",
50
50
  "d3-format": "^3.1.2",
51
51
  "d3-interpolate": "^3.0.0",
@@ -509,4 +509,82 @@ describe('compileGraph', () => {
509
509
  // Rotated labels should shrink the chart area height
510
510
  expect(layoutRotated.area.height).toBeLessThan(layoutNormal.area.height);
511
511
  });
512
+
513
+ it('hides legend when legend.show is false', () => {
514
+ const spec = {
515
+ ...lineSpec,
516
+ legend: { show: false },
517
+ };
518
+ const layout = compileChart(spec, { width: 600, height: 400 });
519
+ expect(layout.legend.entries).toHaveLength(0);
520
+ expect(layout.legend.bounds.width).toBe(0);
521
+ expect(layout.legend.bounds.height).toBe(0);
522
+ });
523
+
524
+ it('applies compact breakpoint overrides to chrome', () => {
525
+ const spec = {
526
+ ...lineSpec,
527
+ chrome: {
528
+ title: 'Full width title with extra context',
529
+ subtitle: 'Long subtitle with methodology details',
530
+ },
531
+ overrides: {
532
+ compact: {
533
+ chrome: {
534
+ title: 'Short title',
535
+ subtitle: 'Short sub',
536
+ },
537
+ },
538
+ },
539
+ };
540
+
541
+ // At compact width (< 400), overrides should apply
542
+ const compactLayout = compileChart(spec, { width: 350, height: 400 });
543
+ expect(compactLayout.chrome.title!.text).toBe('Short title');
544
+ expect(compactLayout.chrome.subtitle!.text).toBe('Short sub');
545
+
546
+ // At full width, base spec should apply
547
+ const fullLayout = compileChart(spec, { width: 800, height: 400 });
548
+ expect(fullLayout.chrome.title!.text).toBe('Full width title with extra context');
549
+ expect(fullLayout.chrome.subtitle!.text).toBe('Long subtitle with methodology details');
550
+ });
551
+
552
+ it('applies medium breakpoint overrides', () => {
553
+ const spec = {
554
+ ...lineSpec,
555
+ chrome: { title: 'Full title' },
556
+ overrides: {
557
+ medium: {
558
+ chrome: { title: 'Medium title' },
559
+ },
560
+ },
561
+ };
562
+
563
+ // At medium width (400-700), override should apply
564
+ const mediumLayout = compileChart(spec, { width: 500, height: 400 });
565
+ expect(mediumLayout.chrome.title!.text).toBe('Medium title');
566
+
567
+ // At full width, base spec should apply
568
+ const fullLayout = compileChart(spec, { width: 800, height: 400 });
569
+ expect(fullLayout.chrome.title!.text).toBe('Full title');
570
+ });
571
+
572
+ it('applies breakpoint override for legend show', () => {
573
+ const spec = {
574
+ ...lineSpec,
575
+ overrides: {
576
+ compact: {
577
+ legend: { show: false },
578
+ },
579
+ },
580
+ };
581
+
582
+ // At compact, legend hidden
583
+ const compactLayout = compileChart(spec, { width: 350, height: 400 });
584
+ expect(compactLayout.legend.entries).toHaveLength(0);
585
+
586
+ // At full, legend shown
587
+ const fullLayout = compileChart(spec, { width: 800, height: 400 });
588
+ expect(fullLayout.legend.entries.length).toBeGreaterThan(0);
589
+ });
512
590
  });
@@ -82,6 +82,30 @@ describe('computeLegend', () => {
82
82
  expect(legend.bounds.height).toBeGreaterThan(0);
83
83
  });
84
84
 
85
+ it('returns empty entries when show is false', () => {
86
+ const specHidden: NormalizedChartSpec = {
87
+ ...specWithColor,
88
+ legend: { show: false },
89
+ hiddenSeries: [],
90
+ seriesStyles: {},
91
+ };
92
+ const legend = computeLegend(specHidden, fullStrategy, theme, chartArea);
93
+ expect(legend.entries).toHaveLength(0);
94
+ expect(legend.bounds.width).toBe(0);
95
+ expect(legend.bounds.height).toBe(0);
96
+ });
97
+
98
+ it('still shows legend when show is true', () => {
99
+ const specShown: NormalizedChartSpec = {
100
+ ...specWithColor,
101
+ legend: { show: true },
102
+ hiddenSeries: [],
103
+ seriesStyles: {},
104
+ };
105
+ const legend = computeLegend(specShown, fullStrategy, theme, chartArea);
106
+ expect(legend.entries).toHaveLength(3);
107
+ });
108
+
85
109
  it('uses correct swatch shape for chart type', () => {
86
110
  const lineLegend = computeLegend(specWithColor, fullStrategy, theme, chartArea);
87
111
  expect(lineLegend.entries[0].shape).toBe('line');
package/src/compile.ts CHANGED
@@ -159,7 +159,58 @@ export function compileChart(spec: unknown, options: CompileOptions): ChartLayou
159
159
  throw new Error('compileChart received a graph spec. Use compileGraph instead.');
160
160
  }
161
161
 
162
- const chartSpec = normalized as NormalizedChartSpec;
162
+ let chartSpec = normalized as NormalizedChartSpec;
163
+
164
+ // Responsive strategy
165
+ const breakpoint = getBreakpoint(options.width);
166
+ const strategy = getLayoutStrategy(breakpoint);
167
+
168
+ // Apply breakpoint-conditional overrides from the original spec
169
+ const rawSpec = spec as Record<string, unknown>;
170
+ const overrides = rawSpec.overrides as
171
+ | Partial<
172
+ Record<
173
+ string,
174
+ { chrome?: unknown; labels?: unknown; legend?: unknown; annotations?: unknown }
175
+ >
176
+ >
177
+ | undefined;
178
+ if (overrides?.[breakpoint]) {
179
+ const bp = overrides[breakpoint]!;
180
+ if (bp.chrome) {
181
+ chartSpec = {
182
+ ...chartSpec,
183
+ chrome: {
184
+ ...chartSpec.chrome,
185
+ ...(bp.chrome as NormalizedChartSpec['chrome']),
186
+ },
187
+ };
188
+ }
189
+ if (bp.labels) {
190
+ chartSpec = {
191
+ ...chartSpec,
192
+ labels: {
193
+ ...chartSpec.labels,
194
+ ...(bp.labels as NormalizedChartSpec['labels']),
195
+ },
196
+ };
197
+ }
198
+ if (bp.legend) {
199
+ chartSpec = {
200
+ ...chartSpec,
201
+ legend: {
202
+ ...chartSpec.legend,
203
+ ...(bp.legend as NormalizedChartSpec['legend']),
204
+ },
205
+ };
206
+ }
207
+ if (bp.annotations) {
208
+ chartSpec = {
209
+ ...chartSpec,
210
+ annotations: bp.annotations as NormalizedChartSpec['annotations'],
211
+ };
212
+ }
213
+ }
163
214
 
164
215
  // Resolve theme: merge spec-level theme with options-level overrides
165
216
  const mergedThemeConfig = options.theme
@@ -170,10 +221,6 @@ export function compileChart(spec: unknown, options: CompileOptions): ChartLayou
170
221
  theme = adaptTheme(theme);
171
222
  }
172
223
 
173
- // Responsive strategy
174
- const breakpoint = getBreakpoint(options.width);
175
- const strategy = getLayoutStrategy(breakpoint);
176
-
177
224
  // Compute legend first (needs to reserve space)
178
225
  const preliminaryArea: Rect = {
179
226
  x: 0,
@@ -87,6 +87,25 @@ export function computeLegend(
87
87
  theme: ResolvedTheme,
88
88
  chartArea: Rect,
89
89
  ): LegendLayout {
90
+ // Legend explicitly hidden via show: false
91
+ if (spec.legend?.show === false) {
92
+ return {
93
+ position: 'top',
94
+ entries: [],
95
+ bounds: { x: 0, y: 0, width: 0, height: 0 },
96
+ labelStyle: {
97
+ fontFamily: theme.fonts.family,
98
+ fontSize: theme.fonts.sizes.small,
99
+ fontWeight: theme.fonts.weights.normal,
100
+ fill: theme.colors.text,
101
+ lineHeight: 1.3,
102
+ },
103
+ swatchSize: SWATCH_SIZE,
104
+ swatchGap: SWATCH_GAP,
105
+ entryGap: ENTRY_GAP,
106
+ };
107
+ }
108
+
90
109
  const entries = extractColorEntries(spec, theme);
91
110
 
92
111
  const labelStyle: TextStyle = {