@opendata-ai/openchart-vanilla 7.2.4 → 7.4.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 +267 -74
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/package.json +3 -3
- package/src/__tests__/font-race.test.ts +199 -0
- package/src/__tests__/known-bugs-render.test.ts +183 -0
- package/src/__tests__/svg-renderer.test.ts +6 -4
- package/src/barlist-mount.ts +58 -3
- package/src/barlist-renderer.ts +4 -1
- package/src/measure-text.ts +100 -3
- package/src/mount.ts +65 -2
- package/src/renderers/annotations.ts +24 -16
- package/src/renderers/axes.ts +19 -16
- package/src/renderers/brand.ts +8 -7
- package/src/renderers/chrome.ts +9 -9
- package/src/renderers/legend.ts +21 -13
- package/src/renderers/svg-dom.ts +1 -27
- package/src/sankey-mount.ts +62 -2
- package/src/sankey-renderer.ts +4 -1
- package/src/svg-renderer.ts +6 -4
- package/src/tilemap-mount.ts +60 -4
- package/src/tilemap-renderer.ts +23 -5
package/src/sankey-renderer.ts
CHANGED
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
BRAND_FONT_SIZE,
|
|
22
22
|
BRAND_MIN_WIDTH,
|
|
23
23
|
estimateTextWidth,
|
|
24
|
+
textAscent,
|
|
24
25
|
wrapText,
|
|
25
26
|
} from '@opendata-ai/openchart-core';
|
|
26
27
|
import { clampStaggerDelay } from '@opendata-ai/openchart-engine';
|
|
@@ -94,7 +95,9 @@ function renderChromeElement(
|
|
|
94
95
|
measureText?: MeasureTextFn,
|
|
95
96
|
): void {
|
|
96
97
|
const text = createSVGElement('text');
|
|
97
|
-
|
|
98
|
+
// element.y is the TOP of the text box; convert to the alphabetic baseline
|
|
99
|
+
// (see renderers/chrome.ts — WebKit mishandles dominant-baseline:hanging).
|
|
100
|
+
setAttrs(text, { x: element.x, y: element.y + textAscent(element.style.fontSize) });
|
|
98
101
|
applyTextStyle(text, element.style);
|
|
99
102
|
text.setAttribute('class', className);
|
|
100
103
|
text.setAttribute('data-chrome-key', chromeKey);
|
package/src/svg-renderer.ts
CHANGED
|
@@ -52,10 +52,12 @@ export function renderChartSVG(
|
|
|
52
52
|
setAttrs(svg, {
|
|
53
53
|
viewBox: `0 0 ${width} ${height}`,
|
|
54
54
|
xmlns: SVG_NS,
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
//
|
|
55
|
+
// The SVG spec default is overflow:"hidden", which clips anything a hair
|
|
56
|
+
// outside the viewBox. We now position all text on the alphabetic/central
|
|
57
|
+
// baseline (dominant-baseline:hanging was dropped because WebKit computed
|
|
58
|
+
// it from different metrics), but WebKit/iOS still reports getBBox extents
|
|
59
|
+
// with a few pixels of slack around tspans, so text touching an edge can
|
|
60
|
+
// still get clipped. overflow:visible avoids that. Chart marks are already
|
|
59
61
|
// constrained by a clipPath, so nothing bleeds out.
|
|
60
62
|
overflow: 'visible',
|
|
61
63
|
// Hint browsers to enable sub-pixel font hinting and kerning for chart text.
|
package/src/tilemap-mount.ts
CHANGED
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
type JPGExportOptions,
|
|
24
24
|
type SVGExportOptions,
|
|
25
25
|
} from './export';
|
|
26
|
-
import { createMeasureText } from './measure-text';
|
|
26
|
+
import { createMeasureText, resolveFontFamily, scheduleFontReload } from './measure-text';
|
|
27
27
|
import { observeResize } from './resize-observer';
|
|
28
28
|
import { renderTileMapSVG } from './tilemap-renderer';
|
|
29
29
|
import { createTooltipManager, type TooltipManager } from './tooltip';
|
|
@@ -118,7 +118,30 @@ export function createTileMap(
|
|
|
118
118
|
let animationCleanup: (() => void) | null = null;
|
|
119
119
|
let pendingResize = false;
|
|
120
120
|
|
|
121
|
-
|
|
121
|
+
// Set when webfonts have loaded and a recompile is owed. The next render()
|
|
122
|
+
// that recompiles flips data-oc-fonts-state to 'ready' and clears this, so
|
|
123
|
+
// the attribute stays honest when resize() defers to pendingResize during
|
|
124
|
+
// the entrance animation.
|
|
125
|
+
let fontsReloadPending = false;
|
|
126
|
+
|
|
127
|
+
// Apply the root class up front so getComputedStyle sees --oc-font-family
|
|
128
|
+
// before the text measurer is built.
|
|
129
|
+
container.classList.add('oc-tilemap-root');
|
|
130
|
+
|
|
131
|
+
// Resolve the effective font the way compile() will: compile merges
|
|
132
|
+
// { ...spec.theme, ...options.theme }, so options.theme wins over the
|
|
133
|
+
// spec-level theme; fall back to the container's computed font. Measuring a
|
|
134
|
+
// different font than gets rendered desyncs layout metrics and the reload watcher.
|
|
135
|
+
function resolveEffectiveFont(): string {
|
|
136
|
+
return (
|
|
137
|
+
options?.theme?.fonts?.family ??
|
|
138
|
+
currentSpec.theme?.fonts?.family ??
|
|
139
|
+
resolveFontFamily(container)
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
let fontFamily = resolveEffectiveFont();
|
|
143
|
+
let measureText = createMeasureText(fontFamily);
|
|
144
|
+
let renderGen = 0;
|
|
122
145
|
|
|
123
146
|
// ---------------------------------------------------------------------------
|
|
124
147
|
// Helpers
|
|
@@ -273,10 +296,28 @@ export function createTileMap(
|
|
|
273
296
|
}
|
|
274
297
|
});
|
|
275
298
|
}
|
|
299
|
+
|
|
300
|
+
renderGen += 1;
|
|
301
|
+
container.dataset.ocRenderGen = String(renderGen);
|
|
302
|
+
|
|
303
|
+
// This render recompiled with the loaded webfonts; publish 'ready' now
|
|
304
|
+
// rather than right after the fonts-ready resize() (which may have deferred
|
|
305
|
+
// to pendingResize during the entrance animation).
|
|
306
|
+
if (fontsReloadPending) {
|
|
307
|
+
fontsReloadPending = false;
|
|
308
|
+
container.dataset.ocFontsState = 'ready';
|
|
309
|
+
}
|
|
276
310
|
}
|
|
277
311
|
|
|
278
312
|
function update(newSpec: TileMapSpec): void {
|
|
279
313
|
currentSpec = newSpec;
|
|
314
|
+
// A new spec can change theme.fonts.family; rebuild the measurer so layout
|
|
315
|
+
// measures the font compile will actually render with.
|
|
316
|
+
const nextFont = resolveEffectiveFont();
|
|
317
|
+
if (nextFont !== fontFamily) {
|
|
318
|
+
fontFamily = nextFont;
|
|
319
|
+
measureText = createMeasureText(fontFamily);
|
|
320
|
+
}
|
|
280
321
|
currentLayout = compile();
|
|
281
322
|
render();
|
|
282
323
|
}
|
|
@@ -365,8 +406,8 @@ export function createTileMap(
|
|
|
365
406
|
// Initialize
|
|
366
407
|
// ---------------------------------------------------------------------------
|
|
367
408
|
|
|
368
|
-
//
|
|
369
|
-
|
|
409
|
+
// Root class was applied before the measurer was built (see above); dark
|
|
410
|
+
// mode class still applies here.
|
|
370
411
|
if (resolveDarkMode(options?.darkMode)) {
|
|
371
412
|
container.classList.add('oc-dark');
|
|
372
413
|
}
|
|
@@ -375,6 +416,21 @@ export function createTileMap(
|
|
|
375
416
|
currentLayout = compile();
|
|
376
417
|
render(true);
|
|
377
418
|
|
|
419
|
+
// Recompile once after webfonts load so labels aren't stuck measured
|
|
420
|
+
// against fallback metrics on real devices.
|
|
421
|
+
const fontsPending = scheduleFontReload(
|
|
422
|
+
fontFamily,
|
|
423
|
+
() => !destroyed,
|
|
424
|
+
() => {
|
|
425
|
+
// Mark the reload owed, then recompile. render() flips the attribute to
|
|
426
|
+
// 'ready' once it actually recompiles, including the pendingResize replay
|
|
427
|
+
// after the entrance animation finishes.
|
|
428
|
+
fontsReloadPending = true;
|
|
429
|
+
resize();
|
|
430
|
+
},
|
|
431
|
+
);
|
|
432
|
+
container.dataset.ocFontsState = fontsPending ? 'pending' : 'ready';
|
|
433
|
+
|
|
378
434
|
// Setup responsive resizing
|
|
379
435
|
if (options?.responsive !== false) {
|
|
380
436
|
disconnectResize = observeResize(container, () => {
|
package/src/tilemap-renderer.ts
CHANGED
|
@@ -11,6 +11,7 @@ import type {
|
|
|
11
11
|
TileMapLayout,
|
|
12
12
|
TileMapTileMark,
|
|
13
13
|
} from '@opendata-ai/openchart-core';
|
|
14
|
+
import { textAscent } from '@opendata-ai/openchart-core';
|
|
14
15
|
|
|
15
16
|
const SVG_NS = 'http://www.w3.org/2000/svg';
|
|
16
17
|
const XLINK_NS = 'http://www.w3.org/1999/xlink';
|
|
@@ -41,6 +42,8 @@ function setAttrs(el: SVGElement, attrs: Record<string, string | number>): void
|
|
|
41
42
|
// Chrome rendering
|
|
42
43
|
// ---------------------------------------------------------------------------
|
|
43
44
|
|
|
45
|
+
// Chrome y positions are top-edge coordinates; convert to the alphabetic
|
|
46
|
+
// baseline via textAscent() (WebKit mishandles dominant-baseline:hanging).
|
|
44
47
|
function renderChrome(parent: SVGElement, layout: TileMapLayout): void {
|
|
45
48
|
const g = createSVGElement('g');
|
|
46
49
|
g.setAttribute('class', 'oc-chrome');
|
|
@@ -50,7 +53,10 @@ function renderChrome(parent: SVGElement, layout: TileMapLayout): void {
|
|
|
50
53
|
|
|
51
54
|
if (chrome.title) {
|
|
52
55
|
const text = createSVGElement('text');
|
|
53
|
-
setAttrs(text, {
|
|
56
|
+
setAttrs(text, {
|
|
57
|
+
x: chrome.title.x,
|
|
58
|
+
y: chrome.title.y + textAscent(chrome.title.style.fontSize),
|
|
59
|
+
});
|
|
54
60
|
text.setAttribute('class', 'oc-title');
|
|
55
61
|
text.setAttribute('font-family', chrome.title.style.fontFamily);
|
|
56
62
|
text.setAttribute('font-size', String(chrome.title.style.fontSize));
|
|
@@ -62,7 +68,10 @@ function renderChrome(parent: SVGElement, layout: TileMapLayout): void {
|
|
|
62
68
|
|
|
63
69
|
if (chrome.subtitle) {
|
|
64
70
|
const text = createSVGElement('text');
|
|
65
|
-
setAttrs(text, {
|
|
71
|
+
setAttrs(text, {
|
|
72
|
+
x: chrome.subtitle.x,
|
|
73
|
+
y: chrome.subtitle.y + textAscent(chrome.subtitle.style.fontSize),
|
|
74
|
+
});
|
|
66
75
|
text.setAttribute('class', 'oc-subtitle');
|
|
67
76
|
text.setAttribute('font-family', chrome.subtitle.style.fontFamily);
|
|
68
77
|
text.setAttribute('font-size', String(chrome.subtitle.style.fontSize));
|
|
@@ -77,7 +86,10 @@ function renderChrome(parent: SVGElement, layout: TileMapLayout): void {
|
|
|
77
86
|
|
|
78
87
|
if (chrome.source) {
|
|
79
88
|
const text = createSVGElement('text');
|
|
80
|
-
setAttrs(text, {
|
|
89
|
+
setAttrs(text, {
|
|
90
|
+
x: chrome.source.x,
|
|
91
|
+
y: bottomOffset + chrome.source.y + textAscent(chrome.source.style.fontSize),
|
|
92
|
+
});
|
|
81
93
|
text.setAttribute('class', 'oc-source');
|
|
82
94
|
text.setAttribute('font-family', chrome.source.style.fontFamily);
|
|
83
95
|
text.setAttribute('font-size', String(chrome.source.style.fontSize));
|
|
@@ -92,7 +104,10 @@ function renderChrome(parent: SVGElement, layout: TileMapLayout): void {
|
|
|
92
104
|
|
|
93
105
|
if (chrome.byline) {
|
|
94
106
|
const text = createSVGElement('text');
|
|
95
|
-
setAttrs(text, {
|
|
107
|
+
setAttrs(text, {
|
|
108
|
+
x: chrome.byline.x,
|
|
109
|
+
y: bottomOffset + chrome.byline.y + textAscent(chrome.byline.style.fontSize),
|
|
110
|
+
});
|
|
96
111
|
text.setAttribute('class', 'oc-byline');
|
|
97
112
|
text.setAttribute('font-family', chrome.byline.style.fontFamily);
|
|
98
113
|
text.setAttribute('font-size', String(chrome.byline.style.fontSize));
|
|
@@ -107,7 +122,10 @@ function renderChrome(parent: SVGElement, layout: TileMapLayout): void {
|
|
|
107
122
|
|
|
108
123
|
if (chrome.footer) {
|
|
109
124
|
const text = createSVGElement('text');
|
|
110
|
-
setAttrs(text, {
|
|
125
|
+
setAttrs(text, {
|
|
126
|
+
x: chrome.footer.x,
|
|
127
|
+
y: bottomOffset + chrome.footer.y + textAscent(chrome.footer.style.fontSize),
|
|
128
|
+
});
|
|
111
129
|
text.setAttribute('class', 'oc-footer');
|
|
112
130
|
text.setAttribute('font-family', chrome.footer.style.fontFamily);
|
|
113
131
|
text.setAttribute('font-size', String(chrome.footer.style.fontSize));
|