@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/dist/index.js +3410 -252
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/compile-chart.test.ts +78 -0
- package/src/__tests__/legend.test.ts +24 -0
- package/src/compile.ts +52 -5
- package/src/legend/compute.ts +19 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opendata-ai/openchart-engine",
|
|
3
|
-
"version": "2.
|
|
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.
|
|
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
|
-
|
|
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,
|
package/src/legend/compute.ts
CHANGED
|
@@ -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 = {
|