@opendata-ai/openchart-core 2.9.1 → 2.10.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.d.ts +37 -6
- package/dist/index.js +77 -18
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +3 -0
- package/src/layout/chrome.ts +60 -15
- package/src/responsive/__tests__/breakpoints.test.ts +89 -1
- package/src/responsive/breakpoints.ts +89 -9
- package/src/responsive/index.ts +5 -0
- package/src/types/layout.ts +2 -0
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -66,12 +66,15 @@ export type {
|
|
|
66
66
|
AnnotationPosition,
|
|
67
67
|
AxisLabelDensity,
|
|
68
68
|
Breakpoint,
|
|
69
|
+
ChromeMode,
|
|
70
|
+
HeightClass,
|
|
69
71
|
LabelMode,
|
|
70
72
|
LayoutStrategy,
|
|
71
73
|
LegendPosition,
|
|
72
74
|
} from './responsive/index';
|
|
73
75
|
export {
|
|
74
76
|
getBreakpoint,
|
|
77
|
+
getHeightClass,
|
|
75
78
|
getLayoutStrategy,
|
|
76
79
|
} from './responsive/index';
|
|
77
80
|
|
package/src/layout/chrome.ts
CHANGED
|
@@ -3,8 +3,17 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Takes a Chrome spec + resolved theme and produces a ResolvedChrome
|
|
5
5
|
* with computed text positions, styles, and total chrome heights.
|
|
6
|
+
*
|
|
7
|
+
* Supports three chrome modes:
|
|
8
|
+
* - full: all chrome elements rendered at normal size
|
|
9
|
+
* - compact: title only, no subtitle/source/byline/footer
|
|
10
|
+
* - hidden: no chrome at all (maximizes chart area)
|
|
11
|
+
*
|
|
12
|
+
* Font sizes scale down continuously at narrow widths to keep
|
|
13
|
+
* chrome proportional to the container.
|
|
6
14
|
*/
|
|
7
15
|
|
|
16
|
+
import type { ChromeMode } from '../responsive/breakpoints';
|
|
8
17
|
import type {
|
|
9
18
|
MeasureTextFn,
|
|
10
19
|
ResolvedChrome,
|
|
@@ -28,16 +37,32 @@ function normalizeChromeText(
|
|
|
28
37
|
return { text: value.text, style: value.style, offset: value.offset };
|
|
29
38
|
}
|
|
30
39
|
|
|
31
|
-
/**
|
|
40
|
+
/**
|
|
41
|
+
* Scale a font size based on container width. Only applies to default sizes
|
|
42
|
+
* (not user overrides). Scales from 100% at >= 500px down to 72% at <= 250px.
|
|
43
|
+
*/
|
|
44
|
+
function scaleFontSize(baseFontSize: number, width: number): number {
|
|
45
|
+
if (width >= 500) return baseFontSize;
|
|
46
|
+
if (width <= 250) return Math.max(Math.round(baseFontSize * 0.72), 10);
|
|
47
|
+
const t = (width - 250) / 250;
|
|
48
|
+
return Math.max(Math.round(baseFontSize * (0.72 + t * 0.28)), 10);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Build a TextStyle from chrome defaults + optional overrides, with width-based scaling. */
|
|
32
52
|
function buildTextStyle(
|
|
33
53
|
defaults: ChromeDefaults,
|
|
34
54
|
fontFamily: string,
|
|
35
55
|
textColor: string,
|
|
56
|
+
width: number,
|
|
36
57
|
overrides?: ChromeText['style'],
|
|
37
58
|
): TextStyle {
|
|
59
|
+
const hasExplicitSize = overrides?.fontSize !== undefined;
|
|
60
|
+
const baseFontSize = overrides?.fontSize ?? defaults.fontSize;
|
|
61
|
+
const fontSize = hasExplicitSize ? baseFontSize : scaleFontSize(baseFontSize, width);
|
|
62
|
+
|
|
38
63
|
return {
|
|
39
64
|
fontFamily: overrides?.fontFamily ?? fontFamily,
|
|
40
|
-
fontSize
|
|
65
|
+
fontSize,
|
|
41
66
|
fontWeight: overrides?.fontWeight ?? defaults.fontWeight,
|
|
42
67
|
fill: overrides?.color ?? textColor ?? defaults.color,
|
|
43
68
|
lineHeight: defaults.lineHeight,
|
|
@@ -83,24 +108,28 @@ function estimateLineCount(
|
|
|
83
108
|
* @param theme - The fully resolved theme.
|
|
84
109
|
* @param width - Total available width in pixels.
|
|
85
110
|
* @param measureText - Optional real text measurement function from the adapter.
|
|
111
|
+
* @param chromeMode - Chrome display mode: full, compact (title only), or hidden.
|
|
112
|
+
* @param padding - Override padding (for scaled padding from dimensions).
|
|
86
113
|
*/
|
|
87
114
|
export function computeChrome(
|
|
88
115
|
chrome: Chrome | undefined,
|
|
89
116
|
theme: ResolvedTheme,
|
|
90
117
|
width: number,
|
|
91
118
|
measureText?: MeasureTextFn,
|
|
119
|
+
chromeMode: ChromeMode = 'full',
|
|
120
|
+
padding?: number,
|
|
92
121
|
): ResolvedChrome {
|
|
93
|
-
if (!chrome) {
|
|
122
|
+
if (!chrome || chromeMode === 'hidden') {
|
|
94
123
|
return { topHeight: 0, bottomHeight: 0 };
|
|
95
124
|
}
|
|
96
125
|
|
|
97
|
-
const
|
|
126
|
+
const pad = padding ?? theme.spacing.padding;
|
|
98
127
|
const chromeGap = theme.spacing.chromeGap;
|
|
99
|
-
const maxWidth = width -
|
|
128
|
+
const maxWidth = width - pad * 2;
|
|
100
129
|
const fontFamily = theme.fonts.family;
|
|
101
130
|
|
|
102
131
|
// Track vertical cursor for top elements
|
|
103
|
-
let topY =
|
|
132
|
+
let topY = pad;
|
|
104
133
|
const topElements: Partial<Pick<ResolvedChrome, 'title' | 'subtitle'>> = {};
|
|
105
134
|
|
|
106
135
|
// Title
|
|
@@ -110,12 +139,13 @@ export function computeChrome(
|
|
|
110
139
|
theme.chrome.title,
|
|
111
140
|
fontFamily,
|
|
112
141
|
theme.chrome.title.color,
|
|
142
|
+
width,
|
|
113
143
|
titleNorm.style,
|
|
114
144
|
);
|
|
115
145
|
const lineCount = estimateLineCount(titleNorm.text, style, maxWidth, measureText);
|
|
116
146
|
const element: ResolvedChromeElement = {
|
|
117
147
|
text: titleNorm.text,
|
|
118
|
-
x:
|
|
148
|
+
x: pad + (titleNorm.offset?.dx ?? 0),
|
|
119
149
|
y: topY + (titleNorm.offset?.dy ?? 0),
|
|
120
150
|
maxWidth,
|
|
121
151
|
style,
|
|
@@ -124,19 +154,20 @@ export function computeChrome(
|
|
|
124
154
|
topY += estimateTextHeight(style.fontSize, lineCount, style.lineHeight) + chromeGap;
|
|
125
155
|
}
|
|
126
156
|
|
|
127
|
-
// Subtitle
|
|
128
|
-
const subtitleNorm = normalizeChromeText(chrome.subtitle);
|
|
157
|
+
// Subtitle (hidden in compact mode)
|
|
158
|
+
const subtitleNorm = chromeMode === 'compact' ? null : normalizeChromeText(chrome.subtitle);
|
|
129
159
|
if (subtitleNorm) {
|
|
130
160
|
const style = buildTextStyle(
|
|
131
161
|
theme.chrome.subtitle,
|
|
132
162
|
fontFamily,
|
|
133
163
|
theme.chrome.subtitle.color,
|
|
164
|
+
width,
|
|
134
165
|
subtitleNorm.style,
|
|
135
166
|
);
|
|
136
167
|
const lineCount = estimateLineCount(subtitleNorm.text, style, maxWidth, measureText);
|
|
137
168
|
const element: ResolvedChromeElement = {
|
|
138
169
|
text: subtitleNorm.text,
|
|
139
|
-
x:
|
|
170
|
+
x: pad + (subtitleNorm.offset?.dx ?? 0),
|
|
140
171
|
y: topY + (subtitleNorm.offset?.dy ?? 0),
|
|
141
172
|
maxWidth,
|
|
142
173
|
style,
|
|
@@ -147,10 +178,18 @@ export function computeChrome(
|
|
|
147
178
|
|
|
148
179
|
// Add chromeToChart gap if there are any top elements
|
|
149
180
|
const hasTopChrome = titleNorm || subtitleNorm;
|
|
150
|
-
const topHeight = hasTopChrome ? topY -
|
|
181
|
+
const topHeight = hasTopChrome ? topY - pad + theme.spacing.chromeToChart - chromeGap : 0;
|
|
182
|
+
|
|
183
|
+
// Bottom elements hidden in compact mode
|
|
184
|
+
if (chromeMode === 'compact') {
|
|
185
|
+
return {
|
|
186
|
+
topHeight,
|
|
187
|
+
bottomHeight: 0,
|
|
188
|
+
...topElements,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
151
191
|
|
|
152
192
|
// Bottom elements: source, byline, footer
|
|
153
|
-
// We compute heights bottom-up but position them after knowing total
|
|
154
193
|
const bottomElements: Partial<Pick<ResolvedChrome, 'source' | 'byline' | 'footer'>> = {};
|
|
155
194
|
let bottomHeight = 0;
|
|
156
195
|
|
|
@@ -191,7 +230,13 @@ export function computeChrome(
|
|
|
191
230
|
bottomHeight += theme.spacing.chartToFooter;
|
|
192
231
|
|
|
193
232
|
for (const item of bottomItems) {
|
|
194
|
-
const style = buildTextStyle(
|
|
233
|
+
const style = buildTextStyle(
|
|
234
|
+
item.defaults,
|
|
235
|
+
fontFamily,
|
|
236
|
+
item.defaults.color,
|
|
237
|
+
width,
|
|
238
|
+
item.norm.style,
|
|
239
|
+
);
|
|
195
240
|
const lineCount = estimateLineCount(item.norm.text, style, maxWidth, measureText);
|
|
196
241
|
const height = estimateTextHeight(style.fontSize, lineCount, style.lineHeight);
|
|
197
242
|
|
|
@@ -199,7 +244,7 @@ export function computeChrome(
|
|
|
199
244
|
// chart area by the engine. We store offsets from bottom start.
|
|
200
245
|
bottomElements[item.key] = {
|
|
201
246
|
text: item.norm.text,
|
|
202
|
-
x:
|
|
247
|
+
x: pad + (item.norm.offset?.dx ?? 0),
|
|
203
248
|
y: bottomHeight + (item.norm.offset?.dy ?? 0), // offset from where bottom chrome starts
|
|
204
249
|
maxWidth,
|
|
205
250
|
style,
|
|
@@ -211,7 +256,7 @@ export function computeChrome(
|
|
|
211
256
|
// Remove trailing gap
|
|
212
257
|
bottomHeight -= chromeGap;
|
|
213
258
|
// Add bottom padding
|
|
214
|
-
bottomHeight +=
|
|
259
|
+
bottomHeight += pad;
|
|
215
260
|
}
|
|
216
261
|
|
|
217
262
|
return {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { getBreakpoint, getLayoutStrategy } from '../breakpoints';
|
|
2
|
+
import { getBreakpoint, getHeightClass, getLayoutStrategy } from '../breakpoints';
|
|
3
3
|
|
|
4
4
|
describe('getBreakpoint', () => {
|
|
5
5
|
it('returns compact for widths below 400', () => {
|
|
@@ -55,4 +55,92 @@ describe('getLayoutStrategy', () => {
|
|
|
55
55
|
expect(compact.legendPosition).not.toBe(full.legendPosition);
|
|
56
56
|
expect(compact.labelMode).not.toBe(full.labelMode);
|
|
57
57
|
});
|
|
58
|
+
|
|
59
|
+
it('includes chromeMode and legendMaxHeight at normal height', () => {
|
|
60
|
+
const strategy = getLayoutStrategy('full');
|
|
61
|
+
expect(strategy.chromeMode).toBe('full');
|
|
62
|
+
expect(strategy.legendMaxHeight).toBe(-1);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
// Height class detection
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
|
|
70
|
+
describe('getHeightClass', () => {
|
|
71
|
+
it('returns cramped for heights below 200', () => {
|
|
72
|
+
expect(getHeightClass(100)).toBe('cramped');
|
|
73
|
+
expect(getHeightClass(199)).toBe('cramped');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('returns short for heights 200-350', () => {
|
|
77
|
+
expect(getHeightClass(200)).toBe('short');
|
|
78
|
+
expect(getHeightClass(280)).toBe('short');
|
|
79
|
+
expect(getHeightClass(350)).toBe('short');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('returns normal for heights above 350', () => {
|
|
83
|
+
expect(getHeightClass(351)).toBe('normal');
|
|
84
|
+
expect(getHeightClass(800)).toBe('normal');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('handles edge cases', () => {
|
|
88
|
+
expect(getHeightClass(0)).toBe('cramped');
|
|
89
|
+
expect(getHeightClass(-10)).toBe('cramped');
|
|
90
|
+
expect(getHeightClass(5000)).toBe('normal');
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
// Height-aware layout strategy
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
|
|
98
|
+
describe('getLayoutStrategy with height class', () => {
|
|
99
|
+
it('normal height does not modify the width strategy', () => {
|
|
100
|
+
const withoutHeight = getLayoutStrategy('full');
|
|
101
|
+
const withNormal = getLayoutStrategy('full', 'normal');
|
|
102
|
+
expect(withoutHeight).toEqual(withNormal);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('cramped height hides chrome and labels', () => {
|
|
106
|
+
const strategy = getLayoutStrategy('full', 'cramped');
|
|
107
|
+
expect(strategy.chromeMode).toBe('hidden');
|
|
108
|
+
expect(strategy.legendMaxHeight).toBe(0);
|
|
109
|
+
expect(strategy.labelMode).toBe('none');
|
|
110
|
+
expect(strategy.annotationPosition).toBe('tooltip-only');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('cramped overrides even compact width strategy', () => {
|
|
114
|
+
const strategy = getLayoutStrategy('compact', 'cramped');
|
|
115
|
+
expect(strategy.chromeMode).toBe('hidden');
|
|
116
|
+
expect(strategy.legendMaxHeight).toBe(0);
|
|
117
|
+
expect(strategy.labelMode).toBe('none');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('short height compresses chrome and caps legend', () => {
|
|
121
|
+
const strategy = getLayoutStrategy('full', 'short');
|
|
122
|
+
expect(strategy.chromeMode).toBe('compact');
|
|
123
|
+
expect(strategy.legendMaxHeight).toBe(0.15);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('short height preserves width-based label and legend settings', () => {
|
|
127
|
+
const strategy = getLayoutStrategy('full', 'short');
|
|
128
|
+
// Width strategy for 'full' sets these; short only touches chromeMode and legendMaxHeight
|
|
129
|
+
expect(strategy.labelMode).toBe('all');
|
|
130
|
+
expect(strategy.legendPosition).toBe('right');
|
|
131
|
+
expect(strategy.axisLabelDensity).toBe('full');
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('short height preserves compact width label settings', () => {
|
|
135
|
+
const strategy = getLayoutStrategy('compact', 'short');
|
|
136
|
+
expect(strategy.labelMode).toBe('none');
|
|
137
|
+
expect(strategy.legendPosition).toBe('top');
|
|
138
|
+
expect(strategy.chromeMode).toBe('compact');
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('defaults heightClass to normal when omitted', () => {
|
|
142
|
+
const strategy = getLayoutStrategy('medium');
|
|
143
|
+
expect(strategy.chromeMode).toBe('full');
|
|
144
|
+
expect(strategy.legendMaxHeight).toBe(-1);
|
|
145
|
+
});
|
|
58
146
|
});
|
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Responsive breakpoints and layout strategies.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Width breakpoints:
|
|
5
5
|
* - compact: < 400px (mobile, small embeds)
|
|
6
6
|
* - medium: 400-700px (tablet, sidebars)
|
|
7
7
|
* - full: > 700px (desktop, full-width)
|
|
8
|
+
*
|
|
9
|
+
* Height classes:
|
|
10
|
+
* - cramped: < 200px (dashboard widgets, thumbnails)
|
|
11
|
+
* - short: 200-350px (embedded panels, short containers)
|
|
12
|
+
* - normal: > 350px (standard containers)
|
|
8
13
|
*/
|
|
9
14
|
|
|
10
15
|
// ---------------------------------------------------------------------------
|
|
11
|
-
//
|
|
16
|
+
// Width breakpoint type and detection
|
|
12
17
|
// ---------------------------------------------------------------------------
|
|
13
18
|
|
|
14
19
|
/** Responsive breakpoint based on container width. */
|
|
@@ -27,6 +32,26 @@ export function getBreakpoint(width: number): Breakpoint {
|
|
|
27
32
|
return 'full';
|
|
28
33
|
}
|
|
29
34
|
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// Height class type and detection
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
/** Height classification based on container height. */
|
|
40
|
+
export type HeightClass = 'cramped' | 'short' | 'normal';
|
|
41
|
+
|
|
42
|
+
/** Height class thresholds in pixels. */
|
|
43
|
+
export const HEIGHT_CRAMPED_MAX = 200;
|
|
44
|
+
export const HEIGHT_SHORT_MAX = 350;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Determine the height class for a given container height.
|
|
48
|
+
*/
|
|
49
|
+
export function getHeightClass(height: number): HeightClass {
|
|
50
|
+
if (height < HEIGHT_CRAMPED_MAX) return 'cramped';
|
|
51
|
+
if (height <= HEIGHT_SHORT_MAX) return 'short';
|
|
52
|
+
return 'normal';
|
|
53
|
+
}
|
|
54
|
+
|
|
30
55
|
// ---------------------------------------------------------------------------
|
|
31
56
|
// Layout strategy
|
|
32
57
|
// ---------------------------------------------------------------------------
|
|
@@ -43,9 +68,12 @@ export type AnnotationPosition = 'inline' | 'tooltip-only';
|
|
|
43
68
|
/** Axis label density (controls tick count reduction). */
|
|
44
69
|
export type AxisLabelDensity = 'full' | 'reduced' | 'minimal';
|
|
45
70
|
|
|
71
|
+
/** Chrome display mode based on available height. */
|
|
72
|
+
export type ChromeMode = 'full' | 'compact' | 'hidden';
|
|
73
|
+
|
|
46
74
|
/**
|
|
47
75
|
* Layout strategy defining how the visualization adapts to available space.
|
|
48
|
-
* Returned by getLayoutStrategy() based on
|
|
76
|
+
* Returned by getLayoutStrategy() based on width breakpoint and height class.
|
|
49
77
|
*/
|
|
50
78
|
export interface LayoutStrategy {
|
|
51
79
|
/** How data labels are displayed. */
|
|
@@ -56,16 +84,16 @@ export interface LayoutStrategy {
|
|
|
56
84
|
annotationPosition: AnnotationPosition;
|
|
57
85
|
/** Axis tick density. */
|
|
58
86
|
axisLabelDensity: AxisLabelDensity;
|
|
87
|
+
/** Chrome display mode: full, compact (title only), or hidden. */
|
|
88
|
+
chromeMode: ChromeMode;
|
|
89
|
+
/** Max fraction of container height for legend (0-1). -1 means unlimited. */
|
|
90
|
+
legendMaxHeight: number;
|
|
59
91
|
}
|
|
60
92
|
|
|
61
93
|
/**
|
|
62
|
-
* Get the layout strategy for a
|
|
63
|
-
*
|
|
64
|
-
* Compact: minimal chrome, no inline labels, legend on top, reduced axes.
|
|
65
|
-
* Medium: moderate labels, legend on top, reduced axes.
|
|
66
|
-
* Full: all labels, legend on right, full axes.
|
|
94
|
+
* Get the base layout strategy for a width breakpoint.
|
|
67
95
|
*/
|
|
68
|
-
|
|
96
|
+
function getWidthStrategy(breakpoint: Breakpoint): LayoutStrategy {
|
|
69
97
|
switch (breakpoint) {
|
|
70
98
|
case 'compact':
|
|
71
99
|
return {
|
|
@@ -73,6 +101,8 @@ export function getLayoutStrategy(breakpoint: Breakpoint): LayoutStrategy {
|
|
|
73
101
|
legendPosition: 'top',
|
|
74
102
|
annotationPosition: 'tooltip-only',
|
|
75
103
|
axisLabelDensity: 'minimal',
|
|
104
|
+
chromeMode: 'full',
|
|
105
|
+
legendMaxHeight: -1,
|
|
76
106
|
};
|
|
77
107
|
case 'medium':
|
|
78
108
|
return {
|
|
@@ -80,6 +110,8 @@ export function getLayoutStrategy(breakpoint: Breakpoint): LayoutStrategy {
|
|
|
80
110
|
legendPosition: 'top',
|
|
81
111
|
annotationPosition: 'inline',
|
|
82
112
|
axisLabelDensity: 'reduced',
|
|
113
|
+
chromeMode: 'full',
|
|
114
|
+
legendMaxHeight: -1,
|
|
83
115
|
};
|
|
84
116
|
case 'full':
|
|
85
117
|
return {
|
|
@@ -87,6 +119,54 @@ export function getLayoutStrategy(breakpoint: Breakpoint): LayoutStrategy {
|
|
|
87
119
|
legendPosition: 'right',
|
|
88
120
|
annotationPosition: 'inline',
|
|
89
121
|
axisLabelDensity: 'full',
|
|
122
|
+
chromeMode: 'full',
|
|
123
|
+
legendMaxHeight: -1,
|
|
90
124
|
};
|
|
91
125
|
}
|
|
92
126
|
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Apply height constraints to a width-based strategy.
|
|
130
|
+
* Short containers compress chrome and cap legend height.
|
|
131
|
+
* Cramped containers hide chrome and labels entirely.
|
|
132
|
+
*/
|
|
133
|
+
function applyHeightConstraints(
|
|
134
|
+
strategy: LayoutStrategy,
|
|
135
|
+
heightClass: HeightClass,
|
|
136
|
+
): LayoutStrategy {
|
|
137
|
+
if (heightClass === 'normal') return strategy;
|
|
138
|
+
|
|
139
|
+
if (heightClass === 'cramped') {
|
|
140
|
+
return {
|
|
141
|
+
...strategy,
|
|
142
|
+
chromeMode: 'hidden',
|
|
143
|
+
legendMaxHeight: 0,
|
|
144
|
+
labelMode: 'none',
|
|
145
|
+
annotationPosition: 'tooltip-only',
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// short
|
|
150
|
+
return {
|
|
151
|
+
...strategy,
|
|
152
|
+
chromeMode: 'compact',
|
|
153
|
+
legendMaxHeight: 0.15,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Get the layout strategy for a given breakpoint and height class.
|
|
159
|
+
*
|
|
160
|
+
* Compact: minimal chrome, no inline labels, legend on top, reduced axes.
|
|
161
|
+
* Medium: moderate labels, legend on top, reduced axes.
|
|
162
|
+
* Full: all labels, legend on right, full axes.
|
|
163
|
+
*
|
|
164
|
+
* Height constraints further reduce chrome and legend when container is short.
|
|
165
|
+
*/
|
|
166
|
+
export function getLayoutStrategy(
|
|
167
|
+
breakpoint: Breakpoint,
|
|
168
|
+
heightClass: HeightClass = 'normal',
|
|
169
|
+
): LayoutStrategy {
|
|
170
|
+
const base = getWidthStrategy(breakpoint);
|
|
171
|
+
return applyHeightConstraints(base, heightClass);
|
|
172
|
+
}
|
package/src/responsive/index.ts
CHANGED
|
@@ -6,6 +6,8 @@ export type {
|
|
|
6
6
|
AnnotationPosition,
|
|
7
7
|
AxisLabelDensity,
|
|
8
8
|
Breakpoint,
|
|
9
|
+
ChromeMode,
|
|
10
|
+
HeightClass,
|
|
9
11
|
LabelMode,
|
|
10
12
|
LayoutStrategy,
|
|
11
13
|
LegendPosition,
|
|
@@ -14,5 +16,8 @@ export {
|
|
|
14
16
|
BREAKPOINT_COMPACT_MAX,
|
|
15
17
|
BREAKPOINT_MEDIUM_MAX,
|
|
16
18
|
getBreakpoint,
|
|
19
|
+
getHeightClass,
|
|
17
20
|
getLayoutStrategy,
|
|
21
|
+
HEIGHT_CRAMPED_MAX,
|
|
22
|
+
HEIGHT_SHORT_MAX,
|
|
18
23
|
} from './breakpoints';
|
package/src/types/layout.ts
CHANGED
|
@@ -394,6 +394,8 @@ export interface LegendEntry {
|
|
|
394
394
|
shape: 'circle' | 'square' | 'line';
|
|
395
395
|
/** Whether this entry is currently highlighted/active. */
|
|
396
396
|
active?: boolean;
|
|
397
|
+
/** True for overflow indicator entries ("+N more"). Not interactive. */
|
|
398
|
+
overflow?: boolean;
|
|
397
399
|
}
|
|
398
400
|
|
|
399
401
|
/** Resolved legend layout with position and entries. */
|