@opendata-ai/openchart-core 6.1.0 → 6.1.2
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.d.ts +5 -1
- package/dist/index.js +21 -2
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +2 -0
- package/src/layout/__tests__/chrome.test.ts +28 -1
- package/src/layout/chrome.ts +38 -3
- package/src/layout/index.ts +2 -0
- package/src/layout/text-measure.ts +6 -0
- package/src/theme/defaults.ts +1 -1
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest';
|
|
|
2
2
|
import { resolveTheme } from '../../theme/resolve';
|
|
3
3
|
import type { Chrome } from '../../types/spec';
|
|
4
4
|
import { computeChrome } from '../chrome';
|
|
5
|
+
import { BRAND_FONT_SIZE, estimateTextHeight } from '../text-measure';
|
|
5
6
|
|
|
6
7
|
const theme = resolveTheme();
|
|
7
8
|
|
|
@@ -14,9 +15,35 @@ describe('computeChrome', () => {
|
|
|
14
15
|
expect(result.subtitle).toBeUndefined();
|
|
15
16
|
});
|
|
16
17
|
|
|
17
|
-
it('
|
|
18
|
+
it('reserves brand height when chrome is empty but chart is wide enough', () => {
|
|
18
19
|
const result = computeChrome({}, theme, 600);
|
|
20
|
+
const pad = theme.spacing.padding;
|
|
21
|
+
const expectedBottom =
|
|
22
|
+
theme.spacing.chartToFooter + estimateTextHeight(BRAND_FONT_SIZE, 1) + pad;
|
|
19
23
|
expect(result.topHeight).toBe(0);
|
|
24
|
+
expect(result.bottomHeight).toBe(expectedBottom);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('returns zero bottom height when chrome is empty and chart is too narrow for brand', () => {
|
|
28
|
+
const result = computeChrome({}, theme, 100);
|
|
29
|
+
expect(result.topHeight).toBe(0);
|
|
30
|
+
expect(result.bottomHeight).toBe(0);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('reserves brand height in compact mode for wide charts', () => {
|
|
34
|
+
const chrome: Chrome = { title: 'Title', source: 'Source' };
|
|
35
|
+
const result = computeChrome(chrome, theme, 600, undefined, 'compact');
|
|
36
|
+
const pad = theme.spacing.padding;
|
|
37
|
+
const expectedBottom =
|
|
38
|
+
theme.spacing.chartToFooter + estimateTextHeight(BRAND_FONT_SIZE, 1) + pad;
|
|
39
|
+
expect(result.topHeight).toBeGreaterThan(0);
|
|
40
|
+
expect(result.source).toBeUndefined(); // compact hides bottom chrome text
|
|
41
|
+
expect(result.bottomHeight).toBe(expectedBottom);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('returns zero bottom height in compact mode for narrow charts', () => {
|
|
45
|
+
const chrome: Chrome = { title: 'Title', source: 'Source' };
|
|
46
|
+
const result = computeChrome(chrome, theme, 100, undefined, 'compact');
|
|
20
47
|
expect(result.bottomHeight).toBe(0);
|
|
21
48
|
});
|
|
22
49
|
|
package/src/layout/chrome.ts
CHANGED
|
@@ -22,7 +22,13 @@ import type {
|
|
|
22
22
|
} from '../types/layout';
|
|
23
23
|
import type { Chrome, ChromeText } from '../types/spec';
|
|
24
24
|
import type { ChromeDefaults, ResolvedTheme } from '../types/theme';
|
|
25
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
BRAND_FONT_SIZE,
|
|
27
|
+
BRAND_MIN_WIDTH,
|
|
28
|
+
BRAND_RESERVE_WIDTH,
|
|
29
|
+
estimateCharWidth,
|
|
30
|
+
estimateTextHeight,
|
|
31
|
+
} from './text-measure';
|
|
26
32
|
|
|
27
33
|
// ---------------------------------------------------------------------------
|
|
28
34
|
// Helpers
|
|
@@ -133,6 +139,9 @@ export function computeChrome(
|
|
|
133
139
|
padding?: number,
|
|
134
140
|
): ResolvedChrome {
|
|
135
141
|
if (!chrome || chromeMode === 'hidden') {
|
|
142
|
+
// Brand watermark is also skipped at cramped sizes (height < 200px triggers
|
|
143
|
+
// hidden mode, and those containers are typically < BRAND_MIN_WIDTH), so
|
|
144
|
+
// bottomHeight: 0 is safe here.
|
|
136
145
|
return { topHeight: 0, bottomHeight: 0 };
|
|
137
146
|
}
|
|
138
147
|
|
|
@@ -193,11 +202,17 @@ export function computeChrome(
|
|
|
193
202
|
const hasTopChrome = titleNorm || subtitleNorm;
|
|
194
203
|
const topHeight = hasTopChrome ? topY - pad + theme.spacing.chromeToChart - chromeGap : 0;
|
|
195
204
|
|
|
196
|
-
// Bottom
|
|
205
|
+
// Bottom chrome text hidden in compact mode, but brand watermark still
|
|
206
|
+
// renders for wide-enough charts. Reserve space so it doesn't overflow.
|
|
197
207
|
if (chromeMode === 'compact') {
|
|
208
|
+
let compactBottom = 0;
|
|
209
|
+
if (width >= BRAND_MIN_WIDTH) {
|
|
210
|
+
const brandHeight = estimateTextHeight(BRAND_FONT_SIZE, 1);
|
|
211
|
+
compactBottom = theme.spacing.chartToFooter + brandHeight + pad;
|
|
212
|
+
}
|
|
198
213
|
return {
|
|
199
214
|
topHeight,
|
|
200
|
-
bottomHeight:
|
|
215
|
+
bottomHeight: compactBottom,
|
|
201
216
|
...topElements,
|
|
202
217
|
};
|
|
203
218
|
}
|
|
@@ -270,8 +285,28 @@ export function computeChrome(
|
|
|
270
285
|
|
|
271
286
|
// Remove trailing gap
|
|
272
287
|
bottomHeight -= chromeGap;
|
|
288
|
+
|
|
289
|
+
// Ensure bottom height accommodates the brand watermark, which renders
|
|
290
|
+
// at the same Y as the first bottom chrome item but is taller (20px font
|
|
291
|
+
// vs 12px source). Without this, the brand overflows the SVG viewBox.
|
|
292
|
+
if (width >= BRAND_MIN_WIDTH) {
|
|
293
|
+
const brandHeight = estimateTextHeight(BRAND_FONT_SIZE, 1);
|
|
294
|
+
// firstItemY is chartToFooter (the Y offset of the first bottom item).
|
|
295
|
+
// The brand needs brandHeight below that point; bottom chrome content
|
|
296
|
+
// needs (bottomHeight - chartToFooter). Take the max.
|
|
297
|
+
const contentBelowFirstItem = bottomHeight - theme.spacing.chartToFooter;
|
|
298
|
+
if (brandHeight > contentBelowFirstItem) {
|
|
299
|
+
bottomHeight += brandHeight - contentBelowFirstItem;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
273
303
|
// Add bottom padding
|
|
274
304
|
bottomHeight += pad;
|
|
305
|
+
} else if (width >= BRAND_MIN_WIDTH) {
|
|
306
|
+
// No bottom chrome items, but brand watermark still renders.
|
|
307
|
+
// Reserve space: chartToFooter gap + brand text height + padding.
|
|
308
|
+
const brandHeight = estimateTextHeight(BRAND_FONT_SIZE, 1);
|
|
309
|
+
bottomHeight = theme.spacing.chartToFooter + brandHeight + pad;
|
|
275
310
|
}
|
|
276
311
|
|
|
277
312
|
return {
|
package/src/layout/index.ts
CHANGED
|
@@ -58,6 +58,12 @@ export function estimateTextWidth(text: string, fontSize: number, fontWeight = 4
|
|
|
58
58
|
*/
|
|
59
59
|
export const BRAND_RESERVE_WIDTH = 110;
|
|
60
60
|
|
|
61
|
+
/** Font size of the brand watermark (px). Shared between layout and renderer. */
|
|
62
|
+
export const BRAND_FONT_SIZE = 20;
|
|
63
|
+
|
|
64
|
+
/** Minimum chart width to render the brand watermark (px). */
|
|
65
|
+
export const BRAND_MIN_WIDTH = 120;
|
|
66
|
+
|
|
61
67
|
/**
|
|
62
68
|
* Estimate the rendered height of a text block.
|
|
63
69
|
*
|