@opendata-ai/openchart-vanilla 6.1.1 → 6.1.3
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 -15
- package/dist/index.js +107 -65
- package/dist/index.js.map +1 -1
- package/dist/simulation-worker.js +3 -0
- package/package.json +3 -3
- package/src/graph/canvas-renderer.ts +2 -4
- package/src/graph/simulation-worker-url.ts +5 -23
- package/src/graph/simulation-worker.ts +6 -4
- package/src/graph/simulation.ts +122 -60
- package/src/graph-mount.ts +13 -8
- package/src/svg-renderer.ts +1 -3
- package/src/table-renderer.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opendata-ai/openchart-vanilla",
|
|
3
|
-
"version": "6.1.
|
|
3
|
+
"version": "6.1.3",
|
|
4
4
|
"description": "Vanilla JS renderer for openchart: SVG charts, HTML tables, force-directed graphs",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Riley Hilliard",
|
|
@@ -50,8 +50,8 @@
|
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
52
|
"@floating-ui/dom": "^1.7.6",
|
|
53
|
-
"@opendata-ai/openchart-core": "6.1.
|
|
54
|
-
"@opendata-ai/openchart-engine": "6.1.
|
|
53
|
+
"@opendata-ai/openchart-core": "6.1.3",
|
|
54
|
+
"@opendata-ai/openchart-engine": "6.1.3",
|
|
55
55
|
"d3-force": "^3.0.0",
|
|
56
56
|
"d3-quadtree": "^3.0.1"
|
|
57
57
|
},
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* - Labels and glow skipped during active pan/zoom gestures
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
+
import { BRAND_FONT_SIZE, BRAND_MIN_WIDTH } from '@opendata-ai/openchart-core';
|
|
15
16
|
import type { GraphRenderState, PositionedEdge, PositionedNode } from './types';
|
|
16
17
|
|
|
17
18
|
// ---------------------------------------------------------------------------
|
|
@@ -33,9 +34,6 @@ const TWO_PI = Math.PI * 2;
|
|
|
33
34
|
/** Minimum node radius in screen pixels. Keeps nodes visible when zoomed out. */
|
|
34
35
|
const MIN_SCREEN_RADIUS = 2.5;
|
|
35
36
|
|
|
36
|
-
/** Minimum canvas width to render the brand watermark. */
|
|
37
|
-
const BRAND_MIN_WIDTH = 120;
|
|
38
|
-
|
|
39
37
|
// ---------------------------------------------------------------------------
|
|
40
38
|
// Helpers (exported for testing)
|
|
41
39
|
// ---------------------------------------------------------------------------
|
|
@@ -251,7 +249,7 @@ export class GraphCanvasRenderer {
|
|
|
251
249
|
const padding = theme.spacing.padding;
|
|
252
250
|
const x = w - padding;
|
|
253
251
|
const y = h - padding;
|
|
254
|
-
ctx.font = `600
|
|
252
|
+
ctx.font = `600 ${BRAND_FONT_SIZE}px ${theme.fonts.family}`;
|
|
255
253
|
ctx.fillStyle = theme.colors.axis;
|
|
256
254
|
ctx.globalAlpha = 0.55;
|
|
257
255
|
ctx.textAlign = 'right';
|
|
@@ -1,30 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Creates a Web Worker running the force simulation.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* and handles the asset accordingly.
|
|
4
|
+
* References the built .js file via `new URL` + `import.meta.url`.
|
|
5
|
+
* The consuming app's bundler resolves the worker path at build time.
|
|
7
6
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* - Production (tsup + bun build): dist/simulation-worker.js is a self-contained
|
|
11
|
-
* IIFE produced by `bun build`. The consuming app's bundler copies it as an
|
|
12
|
-
* asset and rewrites the URL.
|
|
13
|
-
*
|
|
14
|
-
* Usage:
|
|
15
|
-
* import { createSimulationWorker } from '@opendata-ai/openchart-vanilla';
|
|
16
|
-
* const worker = createSimulationWorker();
|
|
17
|
-
* worker.postMessage({ type: 'init', nodes, links, width: 800, height: 600 });
|
|
18
|
-
* worker.onmessage = (e) => console.log(e.data);
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Path that resolves in Vite dev (workspace source) to the .ts file.
|
|
23
|
-
* In production dist/, the consuming bundler resolves to simulation-worker.js
|
|
24
|
-
* which sits alongside index.js in the dist folder.
|
|
7
|
+
* Note: SimulationManager handles .js/.ts fallback internally. This
|
|
8
|
+
* helper is exported for consumers who want to manage the worker directly.
|
|
25
9
|
*/
|
|
26
|
-
const workerUrl = new URL('./simulation-worker.ts', import.meta.url);
|
|
27
|
-
|
|
28
10
|
export function createSimulationWorker(): Worker {
|
|
29
|
-
return new Worker(
|
|
11
|
+
return new Worker(new URL('./simulation-worker.js', import.meta.url), { type: 'module' });
|
|
30
12
|
}
|
|
@@ -18,10 +18,6 @@ declare const self: WorkerSelf;
|
|
|
18
18
|
* IMPORTANT: This file cannot import from workspace packages (@opendata-ai/*).
|
|
19
19
|
* All needed types are defined inline or duplicated from worker-protocol.ts.
|
|
20
20
|
* The bun build step bundles this as an isolated IIFE.
|
|
21
|
-
*
|
|
22
|
-
* The companion simulation-worker-url.ts provides createSimulationWorker()
|
|
23
|
-
* which uses `new URL('./simulation-worker.ts', import.meta.url)` for Vite dev,
|
|
24
|
-
* while production consumers load the pre-built dist/simulation-worker.js.
|
|
25
21
|
*/
|
|
26
22
|
|
|
27
23
|
import {
|
|
@@ -246,6 +242,12 @@ ctx.addEventListener('message', ((event: MessageEvent<InMessage>) => {
|
|
|
246
242
|
node.fx = null;
|
|
247
243
|
node.fy = null;
|
|
248
244
|
}
|
|
245
|
+
// Gentle reheat so the released node settles into equilibrium
|
|
246
|
+
// without destabilizing the whole graph. 0.1 is enough for
|
|
247
|
+
// local settling without triggering large-scale reorganization.
|
|
248
|
+
if (simulation && simulation.alpha() < 0.1) {
|
|
249
|
+
simulation.alpha(0.1).restart();
|
|
250
|
+
}
|
|
249
251
|
break;
|
|
250
252
|
}
|
|
251
253
|
|
package/src/graph/simulation.ts
CHANGED
|
@@ -2,11 +2,10 @@
|
|
|
2
2
|
* SimulationManager: spawns a Web Worker for the force simulation,
|
|
3
3
|
* or falls back to synchronous d3-force on the main thread.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* The sync path caps at 300 ticks to prevent blocking the main thread.
|
|
5
|
+
* The worker is always preferred when available. Synchronous fallback
|
|
6
|
+
* is only used when Web Workers are unavailable (SSR, test environments).
|
|
7
|
+
* The sync path batches ticks via requestAnimationFrame to avoid
|
|
8
|
+
* blocking the main thread.
|
|
10
9
|
*/
|
|
11
10
|
|
|
12
11
|
import {
|
|
@@ -27,8 +26,8 @@ import type { SimEdge, SimNode, WorkerOutMessage, WorkerSimulationConfig } from
|
|
|
27
26
|
// Constants
|
|
28
27
|
// ---------------------------------------------------------------------------
|
|
29
28
|
|
|
30
|
-
const SYNC_THRESHOLD = 200;
|
|
31
29
|
const SYNC_MAX_TICKS = 300;
|
|
30
|
+
const SYNC_TICKS_PER_BATCH = 15;
|
|
32
31
|
|
|
33
32
|
// ---------------------------------------------------------------------------
|
|
34
33
|
// Internal node shape for sync simulation
|
|
@@ -93,6 +92,7 @@ export class SimulationManager {
|
|
|
93
92
|
private tickCb: TickCallback | null = null;
|
|
94
93
|
private settledCb: SettledCallback | null = null;
|
|
95
94
|
private destroyed = false;
|
|
95
|
+
private syncRafId: number | null = null;
|
|
96
96
|
|
|
97
97
|
// Stored for worker->sync fallback
|
|
98
98
|
private initNodes: SimNode[] = [];
|
|
@@ -112,7 +112,7 @@ export class SimulationManager {
|
|
|
112
112
|
): SimulationManager {
|
|
113
113
|
const mgr = new SimulationManager();
|
|
114
114
|
|
|
115
|
-
const useWorker = typeof Worker !== 'undefined'
|
|
115
|
+
const useWorker = typeof Worker !== 'undefined';
|
|
116
116
|
|
|
117
117
|
if (useWorker) {
|
|
118
118
|
mgr.initWorker(nodes, edges, config);
|
|
@@ -160,7 +160,7 @@ export class SimulationManager {
|
|
|
160
160
|
}
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
-
/** Unpin a node
|
|
163
|
+
/** Unpin a node and reheat so forces settle it into equilibrium. */
|
|
164
164
|
unpinNode(id: string): void {
|
|
165
165
|
if (this.destroyed) return;
|
|
166
166
|
|
|
@@ -172,6 +172,10 @@ export class SimulationManager {
|
|
|
172
172
|
node.fx = null;
|
|
173
173
|
node.fy = null;
|
|
174
174
|
}
|
|
175
|
+
if (this.syncSim && this.syncSim.alpha() < 0.1) {
|
|
176
|
+
this.syncSim.alpha(0.1).restart();
|
|
177
|
+
this.runSyncTicks();
|
|
178
|
+
}
|
|
175
179
|
}
|
|
176
180
|
}
|
|
177
181
|
|
|
@@ -198,6 +202,11 @@ export class SimulationManager {
|
|
|
198
202
|
destroy(): void {
|
|
199
203
|
this.destroyed = true;
|
|
200
204
|
|
|
205
|
+
if (this.syncRafId !== null) {
|
|
206
|
+
cancelAnimationFrame(this.syncRafId);
|
|
207
|
+
this.syncRafId = null;
|
|
208
|
+
}
|
|
209
|
+
|
|
201
210
|
if (this.worker) {
|
|
202
211
|
this.worker.postMessage({ type: 'stop' });
|
|
203
212
|
this.worker.terminate();
|
|
@@ -223,44 +232,77 @@ export class SimulationManager {
|
|
|
223
232
|
this.initEdges = edges;
|
|
224
233
|
this.initConfig = config;
|
|
225
234
|
|
|
235
|
+
// Worker URL resolution:
|
|
236
|
+
// - Built dist/ consumers: import.meta.url points at dist/index.js,
|
|
237
|
+
// so ./simulation-worker.js resolves to dist/simulation-worker.js.
|
|
238
|
+
// - Vite dev with source aliases (Ladle): import.meta.url points at
|
|
239
|
+
// src/graph/simulation.ts, so ./simulation-worker.js doesn't exist.
|
|
240
|
+
// The .js worker fails to load, and the onerror handler retries
|
|
241
|
+
// with .ts which Vite transforms on the fly.
|
|
242
|
+
// - Vite production build: detects `new Worker(new URL(...))` and
|
|
243
|
+
// bundles the worker as a hashed .js asset.
|
|
244
|
+
const initMsg = { type: 'init' as const, nodes, edges, config };
|
|
245
|
+
const wireWorker = (worker: Worker) => {
|
|
246
|
+
this.worker = worker;
|
|
247
|
+
|
|
248
|
+
worker.onmessage = (event: MessageEvent<WorkerOutMessage>) => {
|
|
249
|
+
if (this.destroyed) return;
|
|
250
|
+
const msg = event.data;
|
|
251
|
+
|
|
252
|
+
switch (msg.type) {
|
|
253
|
+
case 'positions':
|
|
254
|
+
this.tickCb?.(msg.nodes, msg.alpha);
|
|
255
|
+
break;
|
|
256
|
+
case 'settled':
|
|
257
|
+
this.settledCb?.();
|
|
258
|
+
break;
|
|
259
|
+
case 'error':
|
|
260
|
+
console.error('[SimulationManager] Worker error:', msg.message);
|
|
261
|
+
break;
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
worker.postMessage(initMsg);
|
|
266
|
+
};
|
|
267
|
+
|
|
226
268
|
try {
|
|
227
|
-
const
|
|
228
|
-
|
|
269
|
+
const w = new Worker(new URL('./simulation-worker.js', import.meta.url), {
|
|
270
|
+
type: 'module',
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
w.onerror = () => {
|
|
274
|
+
// .js failed (likely Vite dev with source aliases). Try .ts.
|
|
275
|
+
if (this.destroyed) return;
|
|
276
|
+
w.terminate();
|
|
277
|
+
this.worker = null;
|
|
278
|
+
|
|
279
|
+
try {
|
|
280
|
+
const w2 = new Worker(new URL('./simulation-worker.ts', import.meta.url), {
|
|
281
|
+
type: 'module',
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
w2.onerror = () => {
|
|
285
|
+
// Both .js and .ts failed - fall back to sync.
|
|
286
|
+
if (this.destroyed) return;
|
|
287
|
+
console.warn('[SimulationManager] Worker failed to load, falling back to sync');
|
|
288
|
+
w2.terminate();
|
|
289
|
+
this.worker = null;
|
|
290
|
+
this.initSync(this.initNodes, this.initEdges, this.initConfig!);
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
wireWorker(w2);
|
|
294
|
+
} catch {
|
|
295
|
+
console.warn('[SimulationManager] Worker creation failed, using sync fallback');
|
|
296
|
+
this.initSync(this.initNodes, this.initEdges, this.initConfig!);
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
wireWorker(w);
|
|
229
301
|
} catch {
|
|
230
302
|
// Worker construction failed (e.g. SSR or restrictive CSP)
|
|
231
303
|
console.warn('[SimulationManager] Worker creation failed, using sync fallback');
|
|
232
304
|
this.initSync(nodes, edges, config);
|
|
233
|
-
return;
|
|
234
305
|
}
|
|
235
|
-
|
|
236
|
-
this.worker.onmessage = (event: MessageEvent<WorkerOutMessage>) => {
|
|
237
|
-
if (this.destroyed) return;
|
|
238
|
-
const msg = event.data;
|
|
239
|
-
|
|
240
|
-
switch (msg.type) {
|
|
241
|
-
case 'positions':
|
|
242
|
-
this.tickCb?.(msg.nodes, msg.alpha);
|
|
243
|
-
break;
|
|
244
|
-
case 'settled':
|
|
245
|
-
this.settledCb?.();
|
|
246
|
-
break;
|
|
247
|
-
case 'error':
|
|
248
|
-
console.error('[SimulationManager] Worker error:', msg.message);
|
|
249
|
-
break;
|
|
250
|
-
}
|
|
251
|
-
};
|
|
252
|
-
|
|
253
|
-
this.worker.onerror = () => {
|
|
254
|
-
// Worker failed to load (e.g. MIME type error in dev, missing file).
|
|
255
|
-
// Terminate and fall back to synchronous simulation.
|
|
256
|
-
if (this.destroyed) return;
|
|
257
|
-
console.warn('[SimulationManager] Worker failed to load, falling back to sync');
|
|
258
|
-
this.worker?.terminate();
|
|
259
|
-
this.worker = null;
|
|
260
|
-
this.initSync(this.initNodes, this.initEdges, this.initConfig!);
|
|
261
|
-
};
|
|
262
|
-
|
|
263
|
-
this.worker.postMessage({ type: 'init', nodes, edges, config });
|
|
264
306
|
}
|
|
265
307
|
|
|
266
308
|
// -------------------------------------------------------------------------
|
|
@@ -318,41 +360,61 @@ export class SimulationManager {
|
|
|
318
360
|
}
|
|
319
361
|
|
|
320
362
|
/**
|
|
321
|
-
* Run ticks
|
|
322
|
-
*
|
|
323
|
-
*
|
|
324
|
-
*
|
|
363
|
+
* Run simulation ticks in batches, yielding to the main thread between
|
|
364
|
+
* batches via requestAnimationFrame. This prevents a multi-second freeze
|
|
365
|
+
* when the sync fallback handles large graphs (1k+ nodes).
|
|
366
|
+
*
|
|
367
|
+
* Each batch runs SYNC_TICKS_PER_BATCH ticks, emits positions for
|
|
368
|
+
* progressive rendering, then schedules the next batch.
|
|
369
|
+
*
|
|
370
|
+
* @param deferred - When true, start via microtask (initial run where
|
|
371
|
+
* callbacks aren't wired yet). Otherwise start immediately.
|
|
325
372
|
*/
|
|
326
373
|
private runSyncTicks(deferred = false): void {
|
|
327
374
|
if (!this.syncSim || this.destroyed) return;
|
|
328
375
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
376
|
+
// Cancel any in-flight batched run (e.g. from a previous reheat)
|
|
377
|
+
if (this.syncRafId !== null) {
|
|
378
|
+
cancelAnimationFrame(this.syncRafId);
|
|
379
|
+
this.syncRafId = null;
|
|
333
380
|
}
|
|
334
381
|
|
|
335
|
-
const
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
382
|
+
const sim = this.syncSim;
|
|
383
|
+
let tickCount = 0;
|
|
384
|
+
|
|
385
|
+
const runBatch = () => {
|
|
386
|
+
if (this.destroyed || !this.syncSim) return;
|
|
387
|
+
this.syncRafId = null;
|
|
388
|
+
|
|
389
|
+
for (let i = 0; i < SYNC_TICKS_PER_BATCH && tickCount < SYNC_MAX_TICKS; i++, tickCount++) {
|
|
390
|
+
sim.tick();
|
|
391
|
+
if (sim.alpha() < 0.001) {
|
|
392
|
+
tickCount = SYNC_MAX_TICKS;
|
|
393
|
+
break;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const positions = this.syncNodes.map((n) => ({
|
|
398
|
+
id: n.id,
|
|
399
|
+
x: n.x ?? 0,
|
|
400
|
+
y: n.y ?? 0,
|
|
401
|
+
}));
|
|
402
|
+
const alpha = sim.alpha();
|
|
403
|
+
const settled = alpha < 0.001 || tickCount >= SYNC_MAX_TICKS;
|
|
342
404
|
|
|
343
|
-
const deliver = () => {
|
|
344
|
-
if (this.destroyed) return;
|
|
345
405
|
this.tickCb?.(positions, alpha);
|
|
406
|
+
|
|
346
407
|
if (settled) {
|
|
347
408
|
this.settledCb?.();
|
|
409
|
+
} else {
|
|
410
|
+
this.syncRafId = requestAnimationFrame(runBatch);
|
|
348
411
|
}
|
|
349
412
|
};
|
|
350
413
|
|
|
351
414
|
if (deferred) {
|
|
352
|
-
|
|
353
|
-
queueMicrotask(deliver);
|
|
415
|
+
queueMicrotask(runBatch);
|
|
354
416
|
} else {
|
|
355
|
-
|
|
417
|
+
runBatch();
|
|
356
418
|
}
|
|
357
419
|
}
|
|
358
420
|
}
|
package/src/graph-mount.ts
CHANGED
|
@@ -378,6 +378,8 @@ export function createGraph(
|
|
|
378
378
|
centerForce: config.centerForce,
|
|
379
379
|
});
|
|
380
380
|
|
|
381
|
+
let initialSettleDone = false;
|
|
382
|
+
|
|
381
383
|
simulation.onTick((positions, _alpha) => {
|
|
382
384
|
if (destroyed) return;
|
|
383
385
|
|
|
@@ -409,19 +411,22 @@ export function createGraph(
|
|
|
409
411
|
// Rebuild spatial index
|
|
410
412
|
spatialIndex.rebuild(positionedNodes);
|
|
411
413
|
|
|
414
|
+
// During initial simulation, continuously fit the viewport so the graph
|
|
415
|
+
// is visible and centered as it forms. This replaces the jarring single
|
|
416
|
+
// snap at settle time with a smooth progressive fit.
|
|
417
|
+
if (!initialSettleDone && positionedNodes.length > 0 && interactionManager) {
|
|
418
|
+
const { width: cw, height: ch } = getCanvasDimensions();
|
|
419
|
+
const { transform: fitTransform } = ZoomTransform.fitBounds(positionedNodes, cw, ch);
|
|
420
|
+
interactionManager.setTransform(fitTransform);
|
|
421
|
+
}
|
|
422
|
+
|
|
412
423
|
needsRender = true;
|
|
413
424
|
scheduleRender();
|
|
414
425
|
});
|
|
415
426
|
|
|
416
427
|
simulation.onSettled(() => {
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
const { width: cw, height: ch } = getCanvasDimensions();
|
|
420
|
-
const { transform: fitTransform } = ZoomTransform.fitBounds(positionedNodes, cw, ch);
|
|
421
|
-
interactionManager.setTransform(fitTransform);
|
|
422
|
-
needsRender = true;
|
|
423
|
-
scheduleRender();
|
|
424
|
-
}
|
|
428
|
+
if (initialSettleDone) return;
|
|
429
|
+
initialSettleDone = true;
|
|
425
430
|
});
|
|
426
431
|
}
|
|
427
432
|
|
package/src/svg-renderer.ts
CHANGED
|
@@ -27,7 +27,7 @@ import type {
|
|
|
27
27
|
TextStyle,
|
|
28
28
|
TickMarkLayout,
|
|
29
29
|
} from '@opendata-ai/openchart-core';
|
|
30
|
-
import { estimateTextWidth } from '@opendata-ai/openchart-core';
|
|
30
|
+
import { BRAND_FONT_SIZE, BRAND_MIN_WIDTH, estimateTextWidth } from '@opendata-ai/openchart-core';
|
|
31
31
|
|
|
32
32
|
const SVG_NS = 'http://www.w3.org/2000/svg';
|
|
33
33
|
|
|
@@ -1066,8 +1066,6 @@ function renderLegend(parent: SVGElement, legend: LegendLayout): void {
|
|
|
1066
1066
|
// Brand rendering
|
|
1067
1067
|
// ---------------------------------------------------------------------------
|
|
1068
1068
|
|
|
1069
|
-
const BRAND_FONT_SIZE = 20;
|
|
1070
|
-
const BRAND_MIN_WIDTH = 120;
|
|
1071
1069
|
const BRAND_URL = 'https://tryopendata.ai';
|
|
1072
1070
|
const XLINK_NS = 'http://www.w3.org/1999/xlink';
|
|
1073
1071
|
|
package/src/table-renderer.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import type { ResolvedColumn, TableLayout, TableRow } from '@opendata-ai/openchart-core';
|
|
10
|
+
import { BRAND_FONT_SIZE } from '@opendata-ai/openchart-core';
|
|
10
11
|
import { renderCell } from './renderers/table-cells';
|
|
11
12
|
|
|
12
13
|
// ---------------------------------------------------------------------------
|
|
@@ -14,7 +15,6 @@ import { renderCell } from './renderers/table-cells';
|
|
|
14
15
|
// ---------------------------------------------------------------------------
|
|
15
16
|
|
|
16
17
|
const BRAND_URL = 'https://tryopendata.ai';
|
|
17
|
-
const BRAND_FONT_SIZE = 20;
|
|
18
18
|
|
|
19
19
|
// ---------------------------------------------------------------------------
|
|
20
20
|
// Chrome rendering
|