@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opendata-ai/openchart-core",
3
- "version": "6.1.0",
3
+ "version": "6.1.2",
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
@@ -54,6 +54,8 @@ export {
54
54
  // ---------------------------------------------------------------------------
55
55
 
56
56
  export {
57
+ BRAND_FONT_SIZE,
58
+ BRAND_MIN_WIDTH,
57
59
  BRAND_RESERVE_WIDTH,
58
60
  computeChrome,
59
61
  estimateTextWidth,
@@ -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('returns zero heights when chrome is empty', () => {
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
 
@@ -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 { BRAND_RESERVE_WIDTH, estimateCharWidth, estimateTextHeight } from './text-measure';
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 elements hidden in compact mode
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: 0,
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 {
@@ -4,6 +4,8 @@
4
4
 
5
5
  export { computeChrome } from './chrome';
6
6
  export {
7
+ BRAND_FONT_SIZE,
8
+ BRAND_MIN_WIDTH,
7
9
  BRAND_RESERVE_WIDTH,
8
10
  estimateCharWidth,
9
11
  estimateTextHeight,
@@ -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
  *
@@ -43,7 +43,7 @@ export const DEFAULT_THEME: Theme = {
43
43
  },
44
44
  },
45
45
  spacing: {
46
- padding: 12,
46
+ padding: 20,
47
47
  chromeGap: 4,
48
48
  chromeToChart: 8,
49
49
  chartToFooter: 8,