@opendata-ai/openchart-vanilla 6.1.2 → 6.1.4

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,9 +230,8 @@ 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
@@ -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,55 @@ 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 tsUrl = new URL(import.meta.url.replace(/\/[^/]+$/, "/simulation-worker.ts"));
2352
+ const w2 = new Worker(tsUrl, { type: "module" });
2353
+ w2.onerror = () => {
2354
+ if (this.destroyed) return;
2355
+ console.warn("[SimulationManager] Worker failed to load, falling back to sync");
2356
+ w2.terminate();
2357
+ this.worker = null;
2358
+ this.initSync(this.initNodes, this.initEdges, this.initConfig);
2359
+ };
2360
+ wireWorker(w2);
2361
+ } catch {
2362
+ console.warn("[SimulationManager] Worker creation failed, using sync fallback");
2363
+ this.initSync(this.initNodes, this.initEdges, this.initConfig);
2364
+ }
2365
+ };
2366
+ wireWorker(w);
2317
2367
  } catch {
2318
2368
  console.warn("[SimulationManager] Worker creation failed, using sync fallback");
2319
2369
  this.initSync(nodes, edges, config);
2320
- return;
2321
2370
  }
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
2371
  }
2346
2372
  // -------------------------------------------------------------------------
2347
2373
  // Synchronous fallback
@@ -2374,36 +2400,52 @@ var SimulationManager = class _SimulationManager {
2374
2400
  this.runSyncTicks(true);
2375
2401
  }
2376
2402
  /**
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.
2403
+ * Run simulation ticks in batches, yielding to the main thread between
2404
+ * batches via requestAnimationFrame. This prevents a multi-second freeze
2405
+ * when the sync fallback handles large graphs (1k+ nodes).
2406
+ *
2407
+ * Each batch runs SYNC_TICKS_PER_BATCH ticks, emits positions for
2408
+ * progressive rendering, then schedules the next batch.
2409
+ *
2410
+ * @param deferred - When true, start via microtask (initial run where
2411
+ * callbacks aren't wired yet). Otherwise start immediately.
2381
2412
  */
2382
2413
  runSyncTicks(deferred = false) {
2383
2414
  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;
2415
+ if (this.syncRafId !== null) {
2416
+ cancelAnimationFrame(this.syncRafId);
2417
+ this.syncRafId = null;
2388
2418
  }
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;
2419
+ const sim = this.syncSim;
2420
+ let tickCount = 0;
2421
+ const runBatch = () => {
2422
+ if (this.destroyed || !this.syncSim) return;
2423
+ this.syncRafId = null;
2424
+ for (let i = 0; i < SYNC_TICKS_PER_BATCH && tickCount < SYNC_MAX_TICKS; i++, tickCount++) {
2425
+ sim.tick();
2426
+ if (sim.alpha() < 1e-3) {
2427
+ tickCount = SYNC_MAX_TICKS;
2428
+ break;
2429
+ }
2430
+ }
2431
+ const positions = this.syncNodes.map((n) => ({
2432
+ id: n.id,
2433
+ x: n.x ?? 0,
2434
+ y: n.y ?? 0
2435
+ }));
2436
+ const alpha = sim.alpha();
2437
+ const settled = alpha < 1e-3 || tickCount >= SYNC_MAX_TICKS;
2398
2438
  this.tickCb?.(positions, alpha);
2399
2439
  if (settled) {
2400
2440
  this.settledCb?.();
2441
+ } else {
2442
+ this.syncRafId = requestAnimationFrame(runBatch);
2401
2443
  }
2402
2444
  };
2403
2445
  if (deferred) {
2404
- queueMicrotask(deliver);
2446
+ queueMicrotask(runBatch);
2405
2447
  } else {
2406
- deliver();
2448
+ runBatch();
2407
2449
  }
2408
2450
  }
2409
2451
  };
@@ -2815,6 +2857,7 @@ function createGraph(container, spec, options) {
2815
2857
  linkStrength: config.linkStrength,
2816
2858
  centerForce: config.centerForce
2817
2859
  });
2860
+ let initialSettleDone = false;
2818
2861
  simulation.onTick((positions, _alpha) => {
2819
2862
  if (destroyed) return;
2820
2863
  const posMap = /* @__PURE__ */ new Map();
@@ -2837,17 +2880,17 @@ function createGraph(container, spec, options) {
2837
2880
  };
2838
2881
  });
2839
2882
  spatialIndex.rebuild(positionedNodes);
2840
- needsRender = true;
2841
- scheduleRender();
2842
- });
2843
- simulation.onSettled(() => {
2844
- if (canvas && positionedNodes.length > 0 && interactionManager && renderer) {
2883
+ if (!initialSettleDone && positionedNodes.length > 0 && interactionManager) {
2845
2884
  const { width: cw, height: ch } = getCanvasDimensions();
2846
2885
  const { transform: fitTransform } = ZoomTransform.fitBounds(positionedNodes, cw, ch);
2847
2886
  interactionManager.setTransform(fitTransform);
2848
- needsRender = true;
2849
- scheduleRender();
2850
2887
  }
2888
+ needsRender = true;
2889
+ scheduleRender();
2890
+ });
2891
+ simulation.onSettled(() => {
2892
+ if (initialSettleDone) return;
2893
+ initialSettleDone = true;
2851
2894
  });
2852
2895
  }
2853
2896
  function getCanvasDimensions() {