@opendata-ai/openchart-core 7.2.4 → 7.3.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 +60 -11
- package/dist/index.js +440 -132
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +8 -0
- package/src/labels/collision.ts +7 -1
- package/src/labels/index.ts +1 -0
- package/src/layout/__tests__/text-measure.test.ts +7 -8
- package/src/layout/index.ts +2 -0
- package/src/layout/text-measure.ts +331 -38
- package/src/responsive/index.ts +5 -0
- package/src/responsive/metrics.ts +37 -0
- package/src/theme/defaults.ts +2 -1
- package/src/types/index.ts +1 -0
- package/src/types/layout.ts +24 -0
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -60,6 +60,8 @@ export {
|
|
|
60
60
|
COMPACT_WIDTH,
|
|
61
61
|
computeChrome,
|
|
62
62
|
estimateTextWidth,
|
|
63
|
+
heuristicMeasure,
|
|
64
|
+
resolveMeasurer,
|
|
63
65
|
wrapText,
|
|
64
66
|
} from './layout/index';
|
|
65
67
|
|
|
@@ -76,6 +78,7 @@ export type {
|
|
|
76
78
|
LabelMode,
|
|
77
79
|
LayoutStrategy,
|
|
78
80
|
LegendPosition,
|
|
81
|
+
XAxisExtentInput,
|
|
79
82
|
} from './responsive/index';
|
|
80
83
|
export {
|
|
81
84
|
AXIS_TITLE_GAP,
|
|
@@ -85,6 +88,7 @@ export {
|
|
|
85
88
|
axisTitleOffset,
|
|
86
89
|
BREAKPOINT_COMPACT_MAX,
|
|
87
90
|
BREAKPOINT_MEDIUM_MAX,
|
|
91
|
+
computeXAxisExtentFromLabels,
|
|
88
92
|
getAxisTitleOffset,
|
|
89
93
|
getBreakpoint,
|
|
90
94
|
getHeightClass,
|
|
@@ -104,6 +108,9 @@ export {
|
|
|
104
108
|
TICK_LABEL_OFFSET,
|
|
105
109
|
TOP_PAD_EXTRA_NARROW,
|
|
106
110
|
TOP_PAD_NARROW_MAX,
|
|
111
|
+
X_AXIS_BAND_HEIGHT,
|
|
112
|
+
X_AXIS_TITLE_BAND,
|
|
113
|
+
X_AXIS_TITLE_BAND_ROTATED,
|
|
107
114
|
} from './responsive/index';
|
|
108
115
|
|
|
109
116
|
// ---------------------------------------------------------------------------
|
|
@@ -120,6 +127,7 @@ export {
|
|
|
120
127
|
detectCollision,
|
|
121
128
|
EXTENDED_OFFSET_STRATEGIES,
|
|
122
129
|
OFFSET_STRATEGIES,
|
|
130
|
+
overlapArea,
|
|
123
131
|
resolveCollisions,
|
|
124
132
|
} from './labels/index';
|
|
125
133
|
|
package/src/labels/collision.ts
CHANGED
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Greedy algorithm: sort by priority, place in order, try offset
|
|
5
5
|
* positions for conflicts, demote to tooltip-only if no position works.
|
|
6
|
-
* Targeting ~60% of Infrographic quality for Phase 0.
|
|
7
6
|
*/
|
|
8
7
|
|
|
9
8
|
import { estimateTextWidth } from '../layout/text-measure';
|
|
@@ -55,6 +54,13 @@ export function detectCollision(a: Rect, b: Rect): boolean {
|
|
|
55
54
|
return a.x < b.x + b.width && a.x + a.width > b.x && a.y < b.y + b.height && a.y + a.height > b.y;
|
|
56
55
|
}
|
|
57
56
|
|
|
57
|
+
/** Compute the area of intersection between two axis-aligned rectangles. Returns 0 if disjoint. */
|
|
58
|
+
export function overlapArea(a: Rect, b: Rect): number {
|
|
59
|
+
const w = Math.min(a.x + a.width, b.x + b.width) - Math.max(a.x, b.x);
|
|
60
|
+
const h = Math.min(a.y + a.height, b.y + b.height) - Math.max(a.y, b.y);
|
|
61
|
+
return w > 0 && h > 0 ? w * h : 0;
|
|
62
|
+
}
|
|
63
|
+
|
|
58
64
|
// ---------------------------------------------------------------------------
|
|
59
65
|
// Offset strategies
|
|
60
66
|
// ---------------------------------------------------------------------------
|
package/src/labels/index.ts
CHANGED
|
@@ -7,8 +7,8 @@ describe('estimateTextWidth', () => {
|
|
|
7
7
|
});
|
|
8
8
|
|
|
9
9
|
it('scales linearly with text length', () => {
|
|
10
|
-
const w5 = estimateTextWidth('
|
|
11
|
-
const w10 = estimateTextWidth('
|
|
10
|
+
const w5 = estimateTextWidth('abcde', 14);
|
|
11
|
+
const w10 = estimateTextWidth('abcdeabcde', 14);
|
|
12
12
|
expect(w10).toBeCloseTo(w5 * 2, 1);
|
|
13
13
|
});
|
|
14
14
|
|
|
@@ -31,13 +31,12 @@ describe('estimateTextWidth', () => {
|
|
|
31
31
|
expect(width).toBeLessThan(250);
|
|
32
32
|
});
|
|
33
33
|
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
// Any future tuning of the ratio changes layout-wide text wrapping decisions,
|
|
37
|
-
// so we freeze the exact numeric output of the canonical call.
|
|
34
|
+
// Pins the per-character width table output. Any change to the tables
|
|
35
|
+
// shifts layout-wide text wrapping decisions, so we freeze the exact value.
|
|
38
36
|
it('estimateTextWidth("sample", 14, 400) returns the locked numeric value', () => {
|
|
39
|
-
//
|
|
40
|
-
|
|
37
|
+
// Per-char lookup: s(0.5) + a(0.556) + m(0.833) + p(0.556) + l(0.222) + e(0.556) = 3.223
|
|
38
|
+
// 3.223 * 14 * 1.0 = 45.122
|
|
39
|
+
expect(estimateTextWidth('sample', 14, 400)).toBeCloseTo(45.122, 2);
|
|
41
40
|
});
|
|
42
41
|
});
|
|
43
42
|
|
package/src/layout/index.ts
CHANGED
|
@@ -1,20 +1,268 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Text measurement for environments without a DOM.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Uses per-character advance-width tables measured from Inter (the default
|
|
5
|
+
* chart font) for ~2-3% accuracy on Latin text at weights 400/700.
|
|
6
|
+
* Other fonts set via theme.fonts.family still get a better-than-flat-ratio
|
|
7
|
+
* approximation but no accuracy guarantee.
|
|
8
|
+
*
|
|
9
|
+
* Tables generated by scripts/generate-char-widths.mjs — regenerate, don't
|
|
10
|
+
* hand-edit.
|
|
8
11
|
*/
|
|
9
12
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
import type { MeasureTextFn } from '../types/layout';
|
|
14
|
+
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// Per-character advance widths for Inter, normalized to fontSize = 1.
|
|
17
|
+
// Generated by scripts/generate-char-widths.mjs from Chromium canvas metrics.
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
const CHAR_WIDTHS_400: Record<string, number> = {
|
|
21
|
+
' ': 0.278,
|
|
22
|
+
'!': 0.278,
|
|
23
|
+
'"': 0.355,
|
|
24
|
+
'#': 0.556,
|
|
25
|
+
$: 0.556,
|
|
26
|
+
'%': 0.889,
|
|
27
|
+
'&': 0.667,
|
|
28
|
+
"'": 0.191,
|
|
29
|
+
'(': 0.333,
|
|
30
|
+
')': 0.333,
|
|
31
|
+
'*': 0.389,
|
|
32
|
+
'+': 0.584,
|
|
33
|
+
',': 0.278,
|
|
34
|
+
'-': 0.333,
|
|
35
|
+
'.': 0.278,
|
|
36
|
+
'/': 0.278,
|
|
37
|
+
'0': 0.556,
|
|
38
|
+
'1': 0.556,
|
|
39
|
+
'2': 0.556,
|
|
40
|
+
'3': 0.556,
|
|
41
|
+
'4': 0.556,
|
|
42
|
+
'5': 0.556,
|
|
43
|
+
'6': 0.556,
|
|
44
|
+
'7': 0.556,
|
|
45
|
+
'8': 0.556,
|
|
46
|
+
'9': 0.556,
|
|
47
|
+
':': 0.278,
|
|
48
|
+
';': 0.278,
|
|
49
|
+
'<': 0.584,
|
|
50
|
+
'=': 0.584,
|
|
51
|
+
'>': 0.584,
|
|
52
|
+
'?': 0.556,
|
|
53
|
+
'@': 1.015,
|
|
54
|
+
A: 0.667,
|
|
55
|
+
B: 0.667,
|
|
56
|
+
C: 0.722,
|
|
57
|
+
D: 0.722,
|
|
58
|
+
E: 0.667,
|
|
59
|
+
F: 0.611,
|
|
60
|
+
G: 0.778,
|
|
61
|
+
H: 0.722,
|
|
62
|
+
I: 0.278,
|
|
63
|
+
J: 0.5,
|
|
64
|
+
K: 0.667,
|
|
65
|
+
L: 0.556,
|
|
66
|
+
M: 0.833,
|
|
67
|
+
N: 0.722,
|
|
68
|
+
O: 0.778,
|
|
69
|
+
P: 0.667,
|
|
70
|
+
Q: 0.778,
|
|
71
|
+
R: 0.722,
|
|
72
|
+
S: 0.667,
|
|
73
|
+
T: 0.611,
|
|
74
|
+
U: 0.722,
|
|
75
|
+
V: 0.667,
|
|
76
|
+
W: 0.944,
|
|
77
|
+
X: 0.667,
|
|
78
|
+
Y: 0.667,
|
|
79
|
+
Z: 0.611,
|
|
80
|
+
'[': 0.278,
|
|
81
|
+
'\\': 0.278,
|
|
82
|
+
']': 0.278,
|
|
83
|
+
'^': 0.469,
|
|
84
|
+
_: 0.556,
|
|
85
|
+
'`': 0.333,
|
|
86
|
+
a: 0.556,
|
|
87
|
+
b: 0.556,
|
|
88
|
+
c: 0.5,
|
|
89
|
+
d: 0.556,
|
|
90
|
+
e: 0.556,
|
|
91
|
+
f: 0.278,
|
|
92
|
+
g: 0.556,
|
|
93
|
+
h: 0.556,
|
|
94
|
+
i: 0.222,
|
|
95
|
+
j: 0.222,
|
|
96
|
+
k: 0.5,
|
|
97
|
+
l: 0.222,
|
|
98
|
+
m: 0.833,
|
|
99
|
+
n: 0.556,
|
|
100
|
+
o: 0.556,
|
|
101
|
+
p: 0.556,
|
|
102
|
+
q: 0.556,
|
|
103
|
+
r: 0.333,
|
|
104
|
+
s: 0.5,
|
|
105
|
+
t: 0.278,
|
|
106
|
+
u: 0.556,
|
|
107
|
+
v: 0.5,
|
|
108
|
+
w: 0.722,
|
|
109
|
+
x: 0.5,
|
|
110
|
+
y: 0.5,
|
|
111
|
+
z: 0.5,
|
|
112
|
+
'{': 0.334,
|
|
113
|
+
'|': 0.26,
|
|
114
|
+
'}': 0.334,
|
|
115
|
+
'~': 0.584,
|
|
116
|
+
'₹': 0.52,
|
|
117
|
+
'₩': 1.022,
|
|
118
|
+
'¥': 0.556,
|
|
119
|
+
'¥': 1,
|
|
120
|
+
'€': 0.744,
|
|
121
|
+
'£': 0.556,
|
|
122
|
+
'¢': 0.556,
|
|
123
|
+
'§': 0.556,
|
|
124
|
+
'©': 0.737,
|
|
125
|
+
'®': 0.737,
|
|
126
|
+
'™': 1,
|
|
127
|
+
'°': 0.4,
|
|
128
|
+
'–': 0.556,
|
|
129
|
+
'—': 1,
|
|
130
|
+
'‘': 0.222,
|
|
131
|
+
'’': 0.222,
|
|
132
|
+
'“': 0.333,
|
|
133
|
+
'”': 0.333,
|
|
134
|
+
'…': 1,
|
|
135
|
+
'±': 0.549,
|
|
136
|
+
'×': 0.584,
|
|
137
|
+
'÷': 0.549,
|
|
138
|
+
};
|
|
16
139
|
|
|
17
|
-
|
|
140
|
+
const AVG_CHAR_WIDTH_400 = 0.53;
|
|
141
|
+
|
|
142
|
+
const CHAR_WIDTHS_700: Record<string, number> = {
|
|
143
|
+
' ': 0.278,
|
|
144
|
+
'!': 0.333,
|
|
145
|
+
'"': 0.474,
|
|
146
|
+
'#': 0.556,
|
|
147
|
+
$: 0.556,
|
|
148
|
+
'%': 0.889,
|
|
149
|
+
'&': 0.722,
|
|
150
|
+
"'": 0.238,
|
|
151
|
+
'(': 0.333,
|
|
152
|
+
')': 0.333,
|
|
153
|
+
'*': 0.389,
|
|
154
|
+
'+': 0.584,
|
|
155
|
+
',': 0.278,
|
|
156
|
+
'-': 0.333,
|
|
157
|
+
'.': 0.278,
|
|
158
|
+
'/': 0.278,
|
|
159
|
+
'0': 0.556,
|
|
160
|
+
'1': 0.556,
|
|
161
|
+
'2': 0.556,
|
|
162
|
+
'3': 0.556,
|
|
163
|
+
'4': 0.556,
|
|
164
|
+
'5': 0.556,
|
|
165
|
+
'6': 0.556,
|
|
166
|
+
'7': 0.556,
|
|
167
|
+
'8': 0.556,
|
|
168
|
+
'9': 0.556,
|
|
169
|
+
':': 0.333,
|
|
170
|
+
';': 0.333,
|
|
171
|
+
'<': 0.584,
|
|
172
|
+
'=': 0.584,
|
|
173
|
+
'>': 0.584,
|
|
174
|
+
'?': 0.611,
|
|
175
|
+
'@': 0.975,
|
|
176
|
+
A: 0.722,
|
|
177
|
+
B: 0.722,
|
|
178
|
+
C: 0.722,
|
|
179
|
+
D: 0.722,
|
|
180
|
+
E: 0.667,
|
|
181
|
+
F: 0.611,
|
|
182
|
+
G: 0.778,
|
|
183
|
+
H: 0.722,
|
|
184
|
+
I: 0.278,
|
|
185
|
+
J: 0.556,
|
|
186
|
+
K: 0.722,
|
|
187
|
+
L: 0.611,
|
|
188
|
+
M: 0.833,
|
|
189
|
+
N: 0.722,
|
|
190
|
+
O: 0.778,
|
|
191
|
+
P: 0.667,
|
|
192
|
+
Q: 0.778,
|
|
193
|
+
R: 0.722,
|
|
194
|
+
S: 0.667,
|
|
195
|
+
T: 0.611,
|
|
196
|
+
U: 0.722,
|
|
197
|
+
V: 0.667,
|
|
198
|
+
W: 0.944,
|
|
199
|
+
X: 0.667,
|
|
200
|
+
Y: 0.667,
|
|
201
|
+
Z: 0.611,
|
|
202
|
+
'[': 0.333,
|
|
203
|
+
'\\': 0.278,
|
|
204
|
+
']': 0.333,
|
|
205
|
+
'^': 0.584,
|
|
206
|
+
_: 0.556,
|
|
207
|
+
'`': 0.333,
|
|
208
|
+
a: 0.556,
|
|
209
|
+
b: 0.611,
|
|
210
|
+
c: 0.556,
|
|
211
|
+
d: 0.611,
|
|
212
|
+
e: 0.556,
|
|
213
|
+
f: 0.333,
|
|
214
|
+
g: 0.611,
|
|
215
|
+
h: 0.611,
|
|
216
|
+
i: 0.278,
|
|
217
|
+
j: 0.278,
|
|
218
|
+
k: 0.556,
|
|
219
|
+
l: 0.278,
|
|
220
|
+
m: 0.889,
|
|
221
|
+
n: 0.611,
|
|
222
|
+
o: 0.611,
|
|
223
|
+
p: 0.611,
|
|
224
|
+
q: 0.611,
|
|
225
|
+
r: 0.389,
|
|
226
|
+
s: 0.556,
|
|
227
|
+
t: 0.333,
|
|
228
|
+
u: 0.611,
|
|
229
|
+
v: 0.556,
|
|
230
|
+
w: 0.778,
|
|
231
|
+
x: 0.556,
|
|
232
|
+
y: 0.556,
|
|
233
|
+
z: 0.5,
|
|
234
|
+
'{': 0.389,
|
|
235
|
+
'|': 0.28,
|
|
236
|
+
'}': 0.389,
|
|
237
|
+
'~': 0.584,
|
|
238
|
+
'₹': 0.553,
|
|
239
|
+
'₩': 1.024,
|
|
240
|
+
'¥': 0.556,
|
|
241
|
+
'¥': 1,
|
|
242
|
+
'€': 0.744,
|
|
243
|
+
'£': 0.556,
|
|
244
|
+
'¢': 0.556,
|
|
245
|
+
'§': 0.556,
|
|
246
|
+
'©': 0.737,
|
|
247
|
+
'®': 0.737,
|
|
248
|
+
'™': 1,
|
|
249
|
+
'°': 0.4,
|
|
250
|
+
'–': 0.556,
|
|
251
|
+
'—': 1,
|
|
252
|
+
'‘': 0.278,
|
|
253
|
+
'’': 0.278,
|
|
254
|
+
'“': 0.5,
|
|
255
|
+
'”': 0.5,
|
|
256
|
+
'…': 1,
|
|
257
|
+
'±': 0.549,
|
|
258
|
+
'×': 0.584,
|
|
259
|
+
'÷': 0.549,
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
const AVG_CHAR_WIDTH_700 = 0.554;
|
|
263
|
+
|
|
264
|
+
// Weights < 650 scale the CHAR_WIDTHS_400 table; weights >= 650 scale
|
|
265
|
+
// CHAR_WIDTHS_700. The factor is 1.0 at each table's native weight.
|
|
18
266
|
const WEIGHT_ADJUSTMENT: Record<number, number> = {
|
|
19
267
|
100: 0.9,
|
|
20
268
|
200: 0.92,
|
|
@@ -22,48 +270,88 @@ const WEIGHT_ADJUSTMENT: Record<number, number> = {
|
|
|
22
270
|
400: 1.0,
|
|
23
271
|
500: 1.02,
|
|
24
272
|
600: 1.05,
|
|
25
|
-
700: 1.
|
|
273
|
+
700: 1.0, // uses CHAR_WIDTHS_700 directly
|
|
26
274
|
800: 1.1,
|
|
27
275
|
900: 1.12,
|
|
28
276
|
};
|
|
29
277
|
|
|
278
|
+
function resolveTable(fontWeight: number): {
|
|
279
|
+
table: Record<string, number>;
|
|
280
|
+
avg: number;
|
|
281
|
+
scale: number;
|
|
282
|
+
} {
|
|
283
|
+
if (fontWeight >= 650) {
|
|
284
|
+
return {
|
|
285
|
+
table: CHAR_WIDTHS_700,
|
|
286
|
+
avg: AVG_CHAR_WIDTH_700,
|
|
287
|
+
scale: WEIGHT_ADJUSTMENT[fontWeight] ?? 1.0,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
return {
|
|
291
|
+
table: CHAR_WIDTHS_400,
|
|
292
|
+
avg: AVG_CHAR_WIDTH_400,
|
|
293
|
+
scale: WEIGHT_ADJUSTMENT[fontWeight] ?? 1.0,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function charWidthRatio(ch: string, table: Record<string, number>, avg: number): number {
|
|
298
|
+
const direct = table[ch];
|
|
299
|
+
if (direct !== undefined) return direct;
|
|
300
|
+
const code = ch.codePointAt(0) ?? 0;
|
|
301
|
+
// Combining marks (diacritics) have zero advance width
|
|
302
|
+
if ((code >= 0x0300 && code <= 0x036f) || (code >= 0x1ab0 && code <= 0x1aff)) return 0;
|
|
303
|
+
// CJK ideographs, kana, hangul, and fullwidth forms are full-em
|
|
304
|
+
if (
|
|
305
|
+
(code >= 0x1100 && code <= 0x11ff) ||
|
|
306
|
+
(code >= 0x2e80 && code <= 0x9fff) ||
|
|
307
|
+
(code >= 0xac00 && code <= 0xd7af) ||
|
|
308
|
+
(code >= 0xf900 && code <= 0xfaff) ||
|
|
309
|
+
(code >= 0xff00 && code <= 0xff60)
|
|
310
|
+
)
|
|
311
|
+
return 1.0;
|
|
312
|
+
return avg;
|
|
313
|
+
}
|
|
314
|
+
|
|
30
315
|
/**
|
|
31
316
|
* Estimate the average character width for a given font size and weight.
|
|
32
317
|
* Used by both text width estimation and word-wrap line counting.
|
|
33
318
|
*/
|
|
34
319
|
export function estimateCharWidth(fontSize: number, fontWeight = 400): number {
|
|
35
|
-
const
|
|
36
|
-
return fontSize *
|
|
320
|
+
const { avg, scale } = resolveTable(fontWeight);
|
|
321
|
+
return fontSize * avg * scale;
|
|
37
322
|
}
|
|
38
323
|
|
|
39
324
|
/**
|
|
40
325
|
* Estimate the rendered width of a text string.
|
|
41
326
|
*
|
|
42
|
-
* Uses
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
* @param text - The text string to measure.
|
|
46
|
-
* @param fontSize - Font size in pixels.
|
|
47
|
-
* @param fontWeight - Font weight (100-900). Defaults to 400.
|
|
327
|
+
* Uses per-character advance widths measured from Inter (the default chart
|
|
328
|
+
* font). Accurate to ~2-3% for Latin text at weights 400/700; other weights
|
|
329
|
+
* are interpolated from the nearest table.
|
|
48
330
|
*/
|
|
49
331
|
export function estimateTextWidth(text: string, fontSize: number, fontWeight = 400): number {
|
|
50
|
-
const
|
|
51
|
-
|
|
332
|
+
const { table, avg, scale } = resolveTable(fontWeight);
|
|
333
|
+
|
|
334
|
+
const measureLine = (line: string): number => {
|
|
335
|
+
let sum = 0;
|
|
336
|
+
for (const ch of line) {
|
|
337
|
+
sum += charWidthRatio(ch, table, avg);
|
|
338
|
+
}
|
|
339
|
+
return sum * fontSize * scale;
|
|
340
|
+
};
|
|
341
|
+
|
|
52
342
|
if (text.includes('\n')) {
|
|
53
|
-
let
|
|
343
|
+
let maxWidth = 0;
|
|
54
344
|
for (const line of text.split('\n')) {
|
|
55
|
-
|
|
345
|
+
const w = measureLine(line);
|
|
346
|
+
if (w > maxWidth) maxWidth = w;
|
|
56
347
|
}
|
|
57
|
-
return
|
|
348
|
+
return maxWidth;
|
|
58
349
|
}
|
|
59
|
-
return text
|
|
350
|
+
return measureLine(text);
|
|
60
351
|
}
|
|
61
352
|
|
|
62
353
|
/**
|
|
63
354
|
* Width reserved for the "tryOpenData.ai" brand watermark in the bottom-right corner.
|
|
64
|
-
* Accounts for ~14 chars at font size 12 with mixed weights, plus a gap
|
|
65
|
-
* so adjacent text doesn't crowd it. Used by chrome and legend layout to avoid
|
|
66
|
-
* overlapping the brand.
|
|
67
355
|
*/
|
|
68
356
|
export const BRAND_RESERVE_WIDTH = 130;
|
|
69
357
|
|
|
@@ -73,10 +361,6 @@ export const BRAND_FONT_SIZE = 12;
|
|
|
73
361
|
/** Minimum chart width to render the brand watermark (px). */
|
|
74
362
|
export const BRAND_MIN_WIDTH = 120;
|
|
75
363
|
|
|
76
|
-
// ---------------------------------------------------------------------------
|
|
77
|
-
// Responsive breakpoints
|
|
78
|
-
// ---------------------------------------------------------------------------
|
|
79
|
-
|
|
80
364
|
/**
|
|
81
365
|
* Width threshold below which layout tightens spacing (legend gaps, chrome
|
|
82
366
|
* padding, label gaps) to maximize data area on small screens.
|
|
@@ -85,11 +369,20 @@ export const COMPACT_WIDTH = 420;
|
|
|
85
369
|
|
|
86
370
|
/**
|
|
87
371
|
* Estimate the rendered height of a text block.
|
|
88
|
-
*
|
|
89
|
-
* @param fontSize - Font size in pixels.
|
|
90
|
-
* @param lineCount - Number of lines. Defaults to 1.
|
|
91
|
-
* @param lineHeight - Line height multiplier. Defaults to 1.3.
|
|
92
372
|
*/
|
|
93
373
|
export function estimateTextHeight(fontSize: number, lineCount = 1, lineHeight = 1.3): number {
|
|
94
374
|
return fontSize * lineHeight * lineCount;
|
|
95
375
|
}
|
|
376
|
+
|
|
377
|
+
// ---------------------------------------------------------------------------
|
|
378
|
+
// Measurer resolver — bridges heuristic ↔ injected canvas measurer
|
|
379
|
+
// ---------------------------------------------------------------------------
|
|
380
|
+
|
|
381
|
+
export const heuristicMeasure: MeasureTextFn = (text, fontSize, fontWeight) => ({
|
|
382
|
+
width: estimateTextWidth(text, fontSize, fontWeight),
|
|
383
|
+
height: estimateTextHeight(fontSize),
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
export function resolveMeasurer(measureText?: MeasureTextFn): MeasureTextFn {
|
|
387
|
+
return measureText ?? heuristicMeasure;
|
|
388
|
+
}
|
package/src/responsive/index.ts
CHANGED
|
@@ -21,12 +21,14 @@ export {
|
|
|
21
21
|
HEIGHT_CRAMPED_MAX,
|
|
22
22
|
HEIGHT_SHORT_MAX,
|
|
23
23
|
} from './breakpoints';
|
|
24
|
+
export type { XAxisExtentInput } from './metrics';
|
|
24
25
|
export {
|
|
25
26
|
AXIS_TITLE_GAP,
|
|
26
27
|
AXIS_TITLE_OFFSET_COMPACT,
|
|
27
28
|
AXIS_TITLE_OFFSET_DEFAULT,
|
|
28
29
|
AXIS_TITLE_TRAILING_PAD,
|
|
29
30
|
axisTitleOffset,
|
|
31
|
+
computeXAxisExtentFromLabels,
|
|
30
32
|
getAxisTitleOffset,
|
|
31
33
|
HPAD_COMPACT_FRACTION,
|
|
32
34
|
HPAD_COMPACT_MIN,
|
|
@@ -41,4 +43,7 @@ export {
|
|
|
41
43
|
TICK_LABEL_OFFSET,
|
|
42
44
|
TOP_PAD_EXTRA_NARROW,
|
|
43
45
|
TOP_PAD_NARROW_MAX,
|
|
46
|
+
X_AXIS_BAND_HEIGHT,
|
|
47
|
+
X_AXIS_TITLE_BAND,
|
|
48
|
+
X_AXIS_TITLE_BAND_ROTATED,
|
|
44
49
|
} from './metrics';
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* LayoutStrategy is semantic (what to show), this module is metric (how much space).
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
import { estimateTextWidth } from '../layout/text-measure';
|
|
11
12
|
import { BREAKPOINT_COMPACT_MAX } from './breakpoints';
|
|
12
13
|
|
|
13
14
|
// ---------------------------------------------------------------------------
|
|
@@ -153,3 +154,39 @@ export const MAX_LEFT_LABEL_FRACTION_MEDIUM = 0.55;
|
|
|
153
154
|
|
|
154
155
|
/** Max fraction of container width reservable for left category labels (standard). */
|
|
155
156
|
export const MAX_LEFT_LABEL_FRACTION_DEFAULT = 1;
|
|
157
|
+
|
|
158
|
+
// ---------------------------------------------------------------------------
|
|
159
|
+
// X-axis extent (height below chart area)
|
|
160
|
+
// ---------------------------------------------------------------------------
|
|
161
|
+
|
|
162
|
+
export const X_AXIS_BAND_HEIGHT = 26;
|
|
163
|
+
export const X_AXIS_TITLE_BAND = 22;
|
|
164
|
+
export const X_AXIS_TITLE_BAND_ROTATED = 20;
|
|
165
|
+
|
|
166
|
+
export interface XAxisExtentInput {
|
|
167
|
+
labels: string[];
|
|
168
|
+
tickAngle?: number;
|
|
169
|
+
hasTitle: boolean;
|
|
170
|
+
tickFontSize: number;
|
|
171
|
+
tickFontWeight: number;
|
|
172
|
+
xAxisHeight?: number;
|
|
173
|
+
measure?: (text: string, fontSize: number, fontWeight: number) => number;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function computeXAxisExtentFromLabels(input: XAxisExtentInput): number {
|
|
177
|
+
const measure = input.measure ?? estimateTextWidth;
|
|
178
|
+
const baseHeight = input.xAxisHeight ?? X_AXIS_BAND_HEIGHT;
|
|
179
|
+
|
|
180
|
+
if (input.tickAngle && Math.abs(input.tickAngle) > 10) {
|
|
181
|
+
const angleRad = Math.abs(input.tickAngle) * (Math.PI / 180);
|
|
182
|
+
let maxLabelWidth = 40;
|
|
183
|
+
for (const label of input.labels) {
|
|
184
|
+
const w = measure(label, input.tickFontSize, input.tickFontWeight);
|
|
185
|
+
if (w > maxLabelWidth) maxLabelWidth = w;
|
|
186
|
+
}
|
|
187
|
+
const rotatedHeight = Math.min(maxLabelWidth * Math.sin(angleRad) + 6, 120);
|
|
188
|
+
return input.hasTitle ? rotatedHeight + X_AXIS_TITLE_BAND_ROTATED : rotatedHeight;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return input.hasTitle ? baseHeight + X_AXIS_TITLE_BAND : baseHeight;
|
|
192
|
+
}
|
package/src/theme/defaults.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { CATEGORICAL_PALETTE, DIVERGING_PALETTES, SEQUENTIAL_PALETTES } from '../colors/palettes';
|
|
13
|
+
import { X_AXIS_BAND_HEIGHT } from '../responsive/metrics';
|
|
13
14
|
import type { Theme } from '../types/theme';
|
|
14
15
|
|
|
15
16
|
/**
|
|
@@ -58,7 +59,7 @@ export const DEFAULT_THEME: Theme = {
|
|
|
58
59
|
chromeToChart: 8,
|
|
59
60
|
chartToFooter: 8,
|
|
60
61
|
axisMargin: 6,
|
|
61
|
-
xAxisHeight:
|
|
62
|
+
xAxisHeight: X_AXIS_BAND_HEIGHT,
|
|
62
63
|
// Gap (px) between the x-axis line and the TOP of the tick labels. The
|
|
63
64
|
// renderer anchors x-tick labels at their top edge (hanging baseline), so
|
|
64
65
|
// this is a literal top gap that holds regardless of font size.
|