@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 CHANGED
@@ -79,21 +79,11 @@ declare function exportCSV(data: Record<string, unknown>[]): string;
79
79
  /**
80
80
  * Creates a Web Worker running the force simulation.
81
81
  *
82
- * Uses the `new URL` + `import.meta.url` pattern recognized by Vite, Webpack 5,
83
- * Parcel, and esbuild. The bundler resolves the worker file path at build time
84
- * and handles the asset accordingly.
85
- *
86
- * - Vite dev (Ladle): resolves src/graph/simulation-worker.ts directly, serves
87
- * it as a native ES module worker with on-the-fly TypeScript transform.
88
- * - Production (tsup + bun build): dist/simulation-worker.js is a self-contained
89
- * IIFE produced by `bun build`. The consuming app's bundler copies it as an
90
- * asset and rewrites the URL.
91
- *
92
- * Usage:
93
- * import { createSimulationWorker } from '@opendata-ai/openchart-vanilla';
94
- * const worker = createSimulationWorker();
95
- * worker.postMessage({ type: 'init', nodes, links, width: 800, height: 600 });
96
- * worker.onmessage = (e) => console.log(e.data);
82
+ * References the built .js file via `new URL` + `import.meta.url`.
83
+ * The consuming app's bundler resolves the worker path at build time.
84
+ *
85
+ * Note: SimulationManager handles .js/.ts fallback internally. This
86
+ * helper is exported for consumers who want to manage the worker directly.
97
87
  */
98
88
  declare function createSimulationWorker(): Worker;
99
89
 
package/dist/index.js CHANGED
@@ -230,15 +230,15 @@ function csvEscape(value) {
230
230
  }
231
231
 
232
232
  // src/graph/simulation-worker-url.ts
233
- var workerUrl = new URL("./simulation-worker.ts", import.meta.url);
234
233
  function createSimulationWorker() {
235
- return new Worker(workerUrl, { type: "module" });
234
+ return new Worker(new URL("./simulation-worker.js", import.meta.url), { type: "module" });
236
235
  }
237
236
 
238
237
  // src/graph-mount.ts
239
238
  import { compileGraph } from "@opendata-ai/openchart-engine";
240
239
 
241
240
  // src/graph/canvas-renderer.ts
241
+ import { BRAND_FONT_SIZE, BRAND_MIN_WIDTH } from "@opendata-ai/openchart-core";
242
242
  var LABEL_FONT_MIN = 8;
243
243
  var LABEL_FONT_MAX = 12;
244
244
  var EDGE_ALPHA_DEFAULT = 0.35;
@@ -251,7 +251,6 @@ var GLOW_ALPHA = 0.15;
251
251
  var CULL_MARGIN = 50;
252
252
  var TWO_PI = Math.PI * 2;
253
253
  var MIN_SCREEN_RADIUS = 2.5;
254
- var BRAND_MIN_WIDTH = 120;
255
254
  function labelThreshold(zoom) {
256
255
  const t = Math.max(0, Math.min(1, (zoom - 0.2) / 1.8));
257
256
  return 1 - t;
@@ -386,7 +385,7 @@ var GraphCanvasRenderer = class {
386
385
  const padding = theme.spacing.padding;
387
386
  const x3 = w - padding;
388
387
  const y3 = h - padding;
389
- ctx.font = `600 20px ${theme.fonts.family}`;
388
+ ctx.font = `600 ${BRAND_FONT_SIZE}px ${theme.fonts.family}`;
390
389
  ctx.fillStyle = theme.colors.axis;
391
390
  ctx.globalAlpha = 0.55;
392
391
  ctx.textAlign = "right";
@@ -2172,8 +2171,8 @@ function y_default2(y3) {
2172
2171
  }
2173
2172
 
2174
2173
  // src/graph/simulation.ts
2175
- var SYNC_THRESHOLD = 200;
2176
2174
  var SYNC_MAX_TICKS = 300;
2175
+ var SYNC_TICKS_PER_BATCH = 15;
2177
2176
  function forceCluster(nodes, strength) {
2178
2177
  return (alpha) => {
2179
2178
  const cx = /* @__PURE__ */ new Map();
@@ -2208,6 +2207,7 @@ var SimulationManager = class _SimulationManager {
2208
2207
  tickCb = null;
2209
2208
  settledCb = null;
2210
2209
  destroyed = false;
2210
+ syncRafId = null;
2211
2211
  // Stored for worker->sync fallback
2212
2212
  initNodes = [];
2213
2213
  initEdges = [];
@@ -2220,7 +2220,7 @@ var SimulationManager = class _SimulationManager {
2220
2220
  */
2221
2221
  static create(nodes, edges, config) {
2222
2222
  const mgr = new _SimulationManager();
2223
- const useWorker = typeof Worker !== "undefined" && nodes.length >= SYNC_THRESHOLD;
2223
+ const useWorker = typeof Worker !== "undefined";
2224
2224
  if (useWorker) {
2225
2225
  mgr.initWorker(nodes, edges, config);
2226
2226
  } else {
@@ -2259,7 +2259,7 @@ var SimulationManager = class _SimulationManager {
2259
2259
  }
2260
2260
  }
2261
2261
  }
2262
- /** Unpin a node (free it from fixed position). */
2262
+ /** Unpin a node and reheat so forces settle it into equilibrium. */
2263
2263
  unpinNode(id) {
2264
2264
  if (this.destroyed) return;
2265
2265
  if (this.worker) {
@@ -2270,6 +2270,10 @@ var SimulationManager = class _SimulationManager {
2270
2270
  node.fx = null;
2271
2271
  node.fy = null;
2272
2272
  }
2273
+ if (this.syncSim && this.syncSim.alpha() < 0.1) {
2274
+ this.syncSim.alpha(0.1).restart();
2275
+ this.runSyncTicks();
2276
+ }
2273
2277
  }
2274
2278
  }
2275
2279
  /** Drag a node (pins it and reheats slightly). */
@@ -2292,6 +2296,10 @@ var SimulationManager = class _SimulationManager {
2292
2296
  /** Tear down the simulation and release resources. */
2293
2297
  destroy() {
2294
2298
  this.destroyed = true;
2299
+ if (this.syncRafId !== null) {
2300
+ cancelAnimationFrame(this.syncRafId);
2301
+ this.syncRafId = null;
2302
+ }
2295
2303
  if (this.worker) {
2296
2304
  this.worker.postMessage({ type: "stop" });
2297
2305
  this.worker.terminate();
@@ -2311,37 +2319,56 @@ var SimulationManager = class _SimulationManager {
2311
2319
  this.initNodes = nodes;
2312
2320
  this.initEdges = edges;
2313
2321
  this.initConfig = config;
2322
+ const initMsg = { type: "init", nodes, edges, config };
2323
+ const wireWorker = (worker) => {
2324
+ this.worker = worker;
2325
+ worker.onmessage = (event) => {
2326
+ if (this.destroyed) return;
2327
+ const msg = event.data;
2328
+ switch (msg.type) {
2329
+ case "positions":
2330
+ this.tickCb?.(msg.nodes, msg.alpha);
2331
+ break;
2332
+ case "settled":
2333
+ this.settledCb?.();
2334
+ break;
2335
+ case "error":
2336
+ console.error("[SimulationManager] Worker error:", msg.message);
2337
+ break;
2338
+ }
2339
+ };
2340
+ worker.postMessage(initMsg);
2341
+ };
2314
2342
  try {
2315
- const workerUrl2 = new URL("./simulation-worker.ts", import.meta.url);
2316
- this.worker = new Worker(workerUrl2, { type: "module" });
2343
+ const w = new Worker(new URL("./simulation-worker.js", import.meta.url), {
2344
+ type: "module"
2345
+ });
2346
+ w.onerror = () => {
2347
+ if (this.destroyed) return;
2348
+ w.terminate();
2349
+ this.worker = null;
2350
+ try {
2351
+ const w2 = new Worker(new URL("./simulation-worker.ts", import.meta.url), {
2352
+ type: "module"
2353
+ });
2354
+ w2.onerror = () => {
2355
+ if (this.destroyed) return;
2356
+ console.warn("[SimulationManager] Worker failed to load, falling back to sync");
2357
+ w2.terminate();
2358
+ this.worker = null;
2359
+ this.initSync(this.initNodes, this.initEdges, this.initConfig);
2360
+ };
2361
+ wireWorker(w2);
2362
+ } catch {
2363
+ console.warn("[SimulationManager] Worker creation failed, using sync fallback");
2364
+ this.initSync(this.initNodes, this.initEdges, this.initConfig);
2365
+ }
2366
+ };
2367
+ wireWorker(w);
2317
2368
  } catch {
2318
2369
  console.warn("[SimulationManager] Worker creation failed, using sync fallback");
2319
2370
  this.initSync(nodes, edges, config);
2320
- return;
2321
2371
  }
2322
- this.worker.onmessage = (event) => {
2323
- if (this.destroyed) return;
2324
- const msg = event.data;
2325
- switch (msg.type) {
2326
- case "positions":
2327
- this.tickCb?.(msg.nodes, msg.alpha);
2328
- break;
2329
- case "settled":
2330
- this.settledCb?.();
2331
- break;
2332
- case "error":
2333
- console.error("[SimulationManager] Worker error:", msg.message);
2334
- break;
2335
- }
2336
- };
2337
- this.worker.onerror = () => {
2338
- if (this.destroyed) return;
2339
- console.warn("[SimulationManager] Worker failed to load, falling back to sync");
2340
- this.worker?.terminate();
2341
- this.worker = null;
2342
- this.initSync(this.initNodes, this.initEdges, this.initConfig);
2343
- };
2344
- this.worker.postMessage({ type: "init", nodes, edges, config });
2345
2372
  }
2346
2373
  // -------------------------------------------------------------------------
2347
2374
  // Synchronous fallback
@@ -2374,36 +2401,52 @@ var SimulationManager = class _SimulationManager {
2374
2401
  this.runSyncTicks(true);
2375
2402
  }
2376
2403
  /**
2377
- * Run ticks synchronously and fire callbacks.
2378
- * @param deferred - When true, deliver results via microtask. Used for the
2379
- * initial run where callbacks haven't been registered yet. Subsequent
2380
- * calls (reheat, drag) fire synchronously since callbacks are already set.
2404
+ * Run simulation ticks in batches, yielding to the main thread between
2405
+ * batches via requestAnimationFrame. This prevents a multi-second freeze
2406
+ * when the sync fallback handles large graphs (1k+ nodes).
2407
+ *
2408
+ * Each batch runs SYNC_TICKS_PER_BATCH ticks, emits positions for
2409
+ * progressive rendering, then schedules the next batch.
2410
+ *
2411
+ * @param deferred - When true, start via microtask (initial run where
2412
+ * callbacks aren't wired yet). Otherwise start immediately.
2381
2413
  */
2382
2414
  runSyncTicks(deferred = false) {
2383
2415
  if (!this.syncSim || this.destroyed) return;
2384
- const sim = this.syncSim;
2385
- for (let i = 0; i < SYNC_MAX_TICKS; i++) {
2386
- sim.tick();
2387
- if (sim.alpha() < 1e-3) break;
2416
+ if (this.syncRafId !== null) {
2417
+ cancelAnimationFrame(this.syncRafId);
2418
+ this.syncRafId = null;
2388
2419
  }
2389
- const positions = this.syncNodes.map((n) => ({
2390
- id: n.id,
2391
- x: n.x ?? 0,
2392
- y: n.y ?? 0
2393
- }));
2394
- const alpha = sim.alpha();
2395
- const settled = alpha < 1e-3;
2396
- const deliver = () => {
2397
- if (this.destroyed) return;
2420
+ const sim = this.syncSim;
2421
+ let tickCount = 0;
2422
+ const runBatch = () => {
2423
+ if (this.destroyed || !this.syncSim) return;
2424
+ this.syncRafId = null;
2425
+ for (let i = 0; i < SYNC_TICKS_PER_BATCH && tickCount < SYNC_MAX_TICKS; i++, tickCount++) {
2426
+ sim.tick();
2427
+ if (sim.alpha() < 1e-3) {
2428
+ tickCount = SYNC_MAX_TICKS;
2429
+ break;
2430
+ }
2431
+ }
2432
+ const positions = this.syncNodes.map((n) => ({
2433
+ id: n.id,
2434
+ x: n.x ?? 0,
2435
+ y: n.y ?? 0
2436
+ }));
2437
+ const alpha = sim.alpha();
2438
+ const settled = alpha < 1e-3 || tickCount >= SYNC_MAX_TICKS;
2398
2439
  this.tickCb?.(positions, alpha);
2399
2440
  if (settled) {
2400
2441
  this.settledCb?.();
2442
+ } else {
2443
+ this.syncRafId = requestAnimationFrame(runBatch);
2401
2444
  }
2402
2445
  };
2403
2446
  if (deferred) {
2404
- queueMicrotask(deliver);
2447
+ queueMicrotask(runBatch);
2405
2448
  } else {
2406
- deliver();
2449
+ runBatch();
2407
2450
  }
2408
2451
  }
2409
2452
  };
@@ -2815,6 +2858,7 @@ function createGraph(container, spec, options) {
2815
2858
  linkStrength: config.linkStrength,
2816
2859
  centerForce: config.centerForce
2817
2860
  });
2861
+ let initialSettleDone = false;
2818
2862
  simulation.onTick((positions, _alpha) => {
2819
2863
  if (destroyed) return;
2820
2864
  const posMap = /* @__PURE__ */ new Map();
@@ -2837,17 +2881,17 @@ function createGraph(container, spec, options) {
2837
2881
  };
2838
2882
  });
2839
2883
  spatialIndex.rebuild(positionedNodes);
2840
- needsRender = true;
2841
- scheduleRender();
2842
- });
2843
- simulation.onSettled(() => {
2844
- if (canvas && positionedNodes.length > 0 && interactionManager && renderer) {
2884
+ if (!initialSettleDone && positionedNodes.length > 0 && interactionManager) {
2845
2885
  const { width: cw, height: ch } = getCanvasDimensions();
2846
2886
  const { transform: fitTransform } = ZoomTransform.fitBounds(positionedNodes, cw, ch);
2847
2887
  interactionManager.setTransform(fitTransform);
2848
- needsRender = true;
2849
- scheduleRender();
2850
2888
  }
2889
+ needsRender = true;
2890
+ scheduleRender();
2891
+ });
2892
+ simulation.onSettled(() => {
2893
+ if (initialSettleDone) return;
2894
+ initialSettleDone = true;
2851
2895
  });
2852
2896
  }
2853
2897
  function getCanvasDimensions() {
@@ -3214,7 +3258,7 @@ import { isLayerSpec } from "@opendata-ai/openchart-core";
3214
3258
  import { compileChart, compileLayer } from "@opendata-ai/openchart-engine";
3215
3259
 
3216
3260
  // src/svg-renderer.ts
3217
- import { estimateTextWidth } from "@opendata-ai/openchart-core";
3261
+ import { BRAND_FONT_SIZE as BRAND_FONT_SIZE2, BRAND_MIN_WIDTH as BRAND_MIN_WIDTH2, estimateTextWidth } from "@opendata-ai/openchart-core";
3218
3262
  var SVG_NS = "http://www.w3.org/2000/svg";
3219
3263
  function computeXAxisExtent(layout) {
3220
3264
  const xAxis = layout.axes.x;
@@ -4025,8 +4069,6 @@ function renderLegend(parent, legend) {
4025
4069
  }
4026
4070
  parent.appendChild(g);
4027
4071
  }
4028
- var BRAND_FONT_SIZE = 20;
4029
- var BRAND_MIN_WIDTH2 = 120;
4030
4072
  var BRAND_URL = "https://tryopendata.ai";
4031
4073
  var XLINK_NS = "http://www.w3.org/1999/xlink";
4032
4074
  function renderBrand(parent, layout) {
@@ -4052,7 +4094,7 @@ function renderBrand(parent, layout) {
4052
4094
  y: chromeY,
4053
4095
  "dominant-baseline": "hanging",
4054
4096
  "font-family": layout.theme.fonts.family,
4055
- "font-size": BRAND_FONT_SIZE,
4097
+ "font-size": BRAND_FONT_SIZE2,
4056
4098
  "text-anchor": "end",
4057
4099
  "fill-opacity": 0.55
4058
4100
  });
@@ -5851,8 +5893,8 @@ import { getBreakpoint } from "@opendata-ai/openchart-core";
5851
5893
  import { compileTable } from "@opendata-ai/openchart-engine";
5852
5894
 
5853
5895
  // src/table-renderer.ts
5896
+ import { BRAND_FONT_SIZE as BRAND_FONT_SIZE3 } from "@opendata-ai/openchart-core";
5854
5897
  var BRAND_URL2 = "https://tryopendata.ai";
5855
- var BRAND_FONT_SIZE2 = 20;
5856
5898
  function renderChromeBlock(layout, position) {
5857
5899
  const chrome = layout.chrome;
5858
5900
  if (position === "header") {
@@ -6099,7 +6141,7 @@ function renderTable(layout, container) {
6099
6141
  brandLink.href = BRAND_URL2;
6100
6142
  brandLink.target = "_blank";
6101
6143
  brandLink.rel = "noopener";
6102
- brandLink.style.cssText = `font-size: ${BRAND_FONT_SIZE2}px; font-weight: 600; color: ${brandColor}; opacity: 0.55; text-decoration: none; font-family: ${theme ? theme.fonts.family : "sans-serif"};`;
6144
+ brandLink.style.cssText = `font-size: ${BRAND_FONT_SIZE3}px; font-weight: 600; color: ${brandColor}; opacity: 0.55; text-decoration: none; font-family: ${theme ? theme.fonts.family : "sans-serif"};`;
6103
6145
  brandLink.textContent = "OpenData";
6104
6146
  brand.appendChild(brandLink);
6105
6147
  wrapper.appendChild(brand);