@intact-ebi/cytoscape-lumina 1.0.1 → 1.1.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/README.md CHANGED
@@ -107,6 +107,8 @@ Options are split by who uses them:
107
107
  | --- | --- | --- |
108
108
  | `weightExponent` | `2` | Stress weights are `w_ij = 1 / d_ij ^ weightExponent`; `2` is classic stress majorization. |
109
109
 
110
+ Stress shortest paths use weighted Dijkstra. Computed edge lengths (`idealLength` plus both virtual-node radii) must be finite and non-negative; invalid lengths throw a `RangeError`.
111
+
110
112
  ### Structure detection
111
113
 
112
114
  | Option | Default | Description |
@@ -143,7 +145,7 @@ Set `stepByStep: true`, keep a reference to the layout, and run it:
143
145
  const layout = cy.layout({ name: 'lumina', stepByStep: true });
144
146
  layout.run();
145
147
 
146
- layout.listSteps(); // print and return all captured steps
148
+ const steps = layout.listSteps(); // return all captured steps
147
149
  layout.goToStep(3); // jump to a step
148
150
  layout.nextStep();
149
151
  layout.prevStep();
@@ -1,3 +1,4 @@
1
+ import FastPriorityQueue from 'fastpriorityqueue';
1
2
  export const DEFAULT_PARAMS = {
2
3
  layoutAlgorithm: 'stress',
3
4
  // shared solver
@@ -110,6 +111,52 @@ class VEdge {
110
111
  };
111
112
  }
112
113
  }
114
+ /** Dense stress distances from repeated weighted Dijkstra. */
115
+ function computeStressDistances(nodes, edges, idealLength) {
116
+ const numNodes = nodes.length;
117
+ const nodeIndexMap = new Map();
118
+ nodes.forEach((node, index) => nodeIndexMap.set(node.id, index));
119
+ const adjacency = Array.from({ length: numNodes }, () => []);
120
+ edges.forEach(edge => {
121
+ if (!edge.source || !edge.target) {
122
+ return;
123
+ }
124
+ const source = nodeIndexMap.get(edge.source.id);
125
+ const target = nodeIndexMap.get(edge.target.id);
126
+ if (source === undefined || target === undefined) {
127
+ return;
128
+ }
129
+ const distance = idealLength + edge.source.radius + edge.target.radius;
130
+ if (distance < 0 || !Number.isFinite(distance)) {
131
+ throw new RangeError('cytoscape-lumina: stress edge lengths must be finite and non-negative');
132
+ }
133
+ adjacency[source].push({ node: target, distance });
134
+ adjacency[target].push({ node: source, distance });
135
+ });
136
+ const distances = Array.from({ length: numNodes }, (_, index) => {
137
+ const row = new Array(numNodes).fill(Infinity);
138
+ row[index] = 0;
139
+ return row;
140
+ });
141
+ for (let source = 0; source < numNodes; source++) {
142
+ const row = distances[source];
143
+ const heap = new FastPriorityQueue((a, b) => a.distance < b.distance || (a.distance === b.distance && a.node < b.node));
144
+ heap.add({ node: source, distance: 0 });
145
+ let current;
146
+ while ((current = heap.poll()) !== undefined) {
147
+ if (current.distance !== row[current.node])
148
+ continue;
149
+ for (const neighbor of adjacency[current.node]) {
150
+ const candidate = current.distance + neighbor.distance;
151
+ if (candidate < row[neighbor.node]) {
152
+ row[neighbor.node] = candidate;
153
+ heap.add({ node: neighbor.node, distance: candidate });
154
+ }
155
+ }
156
+ }
157
+ }
158
+ return distances;
159
+ }
113
160
  /**
114
161
  * Register this layout as a Cytoscape.js layout extension.
115
162
  *
@@ -203,18 +250,8 @@ LuminaLayout.prototype.captureStep = function (stepName, description, metadata)
203
250
  };
204
251
  this.steps.push(step);
205
252
  this.currentStepIndex = this.steps.length - 1;
206
- console.log(`[Step ${step.stepNumber}] ${stepName}: ${description}`);
253
+ console.debug(`[Step ${step.stepNumber}] ${stepName}: ${description}`);
207
254
  };
208
- /////////////// Check whether two node arrays are identical, ignoring order
209
- function areNodesEqual(arr1, arr2) {
210
- // 1. Different lengths mean they are definitely different
211
- if (arr1.length !== arr2.length)
212
- return false;
213
- // 2. Extract all IDs and put them into a Set
214
- const ids1 = new Set(arr1.map((node) => node.id()));
215
- // 3. Check whether every ID in arr2 exists in the Set
216
- return arr2.every(node => ids1.has(node.id()));
217
- }
218
255
  ////////////////sum length of all edges ////////////////
219
256
  function totalEdgeLength(edges) {
220
257
  let total = 0;
@@ -293,6 +330,17 @@ function layoutRectangular(nodes, center, dirVector, rowSpacing = 60, colSpacing
293
330
  }
294
331
  LuminaLayout.prototype.identifyStructures = function (nodes) {
295
332
  const params = this.params;
333
+ // Topology stays fixed during detection; structure markers are filtered dynamically.
334
+ const neighborhoodCache = new Map();
335
+ const neighborsOf = (node) => {
336
+ const id = node.id();
337
+ let neighbors = neighborhoodCache.get(id);
338
+ if (neighbors === undefined) {
339
+ neighbors = node.neighborhood().nodes();
340
+ neighborhoodCache.set(id, neighbors);
341
+ }
342
+ return neighbors;
343
+ };
296
344
  // 0. Reset all markers
297
345
  nodes.data('structType', 'Normal');
298
346
  // nodes.data('structColor', '#999999');
@@ -304,7 +352,7 @@ LuminaLayout.prototype.identifyStructures = function (nodes) {
304
352
  ///////////////////////////// 2. detecting Cycle - by DFS algorithm /////////////////
305
353
  const allCycles = []; // each sub-array represents an independent cycle
306
354
  if (1) {
307
- const normalNodes = nodes.toArray().filter((n) => n.data('structType') === 'Normal' && n.neighborhood().nodes().length >= 2);
355
+ const normalNodes = nodes.toArray().filter((n) => n.data('structType') === 'Normal' && neighborsOf(n).length >= 2);
308
356
  const seenCycles = new Set();
309
357
  // To avoid duplicate cycles (like A-B-C and B-C-A), we sort and stringify for a check
310
358
  normalNodes.forEach((startNode, startIndex) => {
@@ -312,7 +360,7 @@ LuminaLayout.prototype.identifyStructures = function (nodes) {
312
360
  // This is a massive optimization to prevent finding the same cycle N times
313
361
  const startId = startNode.id();
314
362
  const findCycles = (u, parent, path) => {
315
- const neighbors = u.neighborhood().nodes().toArray().filter((n) => n.data('structType') === 'Normal');
363
+ const neighbors = neighborsOf(u).toArray().filter((n) => n.data('structType') === 'Normal');
316
364
  if (neighbors.length < 8) { // If a node has too many neighbors, it is unlikely to be part of a cycle; this mainly improves efficiency and prevents the search from getting stuck here
317
365
  for (const v of neighbors) {
318
366
  const vId = v.id();
@@ -393,11 +441,10 @@ LuminaLayout.prototype.identifyStructures = function (nodes) {
393
441
  });
394
442
  circleIndex++;
395
443
  });
396
- console.log("num of cycles: ", filteredCycles.length);
397
444
  const chains = [];
398
445
  const processedNodeIds = new Set(); // avoid processing nodes more than once
399
446
  // 2. Find all leaf nodes (Normal type with degree 1)
400
- const leafNodes = nodes.filter((n) => n.data('structType') === 'Normal' && n.neighborhood().nodes().length === 1);
447
+ const leafNodes = nodes.filter((n) => n.data('structType') === 'Normal' && neighborsOf(n).length === 1);
401
448
  let chainId = 0;
402
449
  leafNodes.forEach((leaf) => {
403
450
  if (processedNodeIds.has(leaf.id()))
@@ -410,8 +457,7 @@ LuminaLayout.prototype.identifyStructures = function (nodes) {
410
457
  currentChainNodes.push(currentNode);
411
458
  processedNodeIds.add(currentNode.id());
412
459
  // Find the next neighbor
413
- const neighbors = currentNode
414
- .neighborhood('node')
460
+ const neighbors = neighborsOf(currentNode)
415
461
  .filter((n) => n.id() !== currentNode.id() &&
416
462
  n.data('structType') === 'Normal' &&
417
463
  !processedNodeIds.has(n.id()));
@@ -420,8 +466,7 @@ LuminaLayout.prototype.identifyStructures = function (nodes) {
420
466
  // 2. The neighbor must not have a high degree (if degree > 2, a branching point has been reached and the chain ends)
421
467
  if (neighbors.length === 1) {
422
468
  const nextNode = neighbors[0];
423
- const nextNodeNeighborCount = nextNode
424
- .neighborhood('node')
469
+ const nextNodeNeighborCount = neighborsOf(nextNode)
425
470
  .filter((n) => n.id() !== nextNode.id())
426
471
  .length;
427
472
  if (nextNodeNeighborCount > 2) {
@@ -475,11 +520,11 @@ LuminaLayout.prototype.identifyStructures = function (nodes) {
475
520
  ///////////////////////////////// Star structure ///////////
476
521
  let starIndex = 0;
477
522
  nodes.forEach((node) => {
478
- const neighbors = node.neighborhood().nodes();
523
+ const neighbors = neighborsOf(node);
479
524
  // Number of distinct neighbors
480
525
  const neighborCount = neighbors.length;
481
526
  // Check whether the neighbors consist only of distinct neighbor nodes
482
- const leafNeighbors = neighbors.filter((n) => n.neighborhood().nodes().length === 1);
527
+ const leafNeighbors = neighbors.filter((n) => neighborsOf(n).length === 1);
483
528
  if (leafNeighbors.length >= params.minStarLeaves && neighborCount >= params.minStarLeaves) {
484
529
  if (node.data('structType') !== 'Cycle' && node.data('structType') !== 'Chain') {
485
530
  const groupId = 'Star_' + starIndex;
@@ -508,56 +553,57 @@ LuminaLayout.prototype.identifyStructures = function (nodes) {
508
553
  }
509
554
  });
510
555
  /////////////////////////////// Find parallel/diamond structures ///////////////////////////////////
511
- // let diamonds = [];
512
- let parallelId = 0;
513
- // Whether any two nodes have common neighbors
514
- for (let i = 0; i < nodes.length; i++) {
515
- let nodeVecParallel = [];
516
- const u = nodes[i];
517
- const u1 = u.neighborhood().nodes();
518
- if (u.data('structType') === 'Normal') {
519
- for (let j = 0; j < nodes.length; j++) {
520
- if (i === j)
521
- continue;
522
- const v = nodes[j];
523
- if (v.data('structType') === 'Normal') {
524
- const v1 = v.neighborhood().nodes();
525
- if (areNodesEqual(v1, u1) && v1.length >= params.minParallelNeighbors && u1.length >= params.minParallelNeighbors) {
526
- if (nodeVecParallel.length === 0) {
527
- nodeVecParallel.push(u.id());
528
- }
529
- nodeVecParallel.push(v.id());
530
- }
531
- }
532
- }
556
+ const nodesById = new Map();
557
+ const parallelGroups = new Map();
558
+ nodes.forEach((node) => {
559
+ nodesById.set(node.id(), node);
560
+ if (node.data("structType") !== "Normal") {
561
+ return;
533
562
  }
534
- if (nodeVecParallel.length >= 2) {
535
- nodeVecParallel.forEach(v => {
536
- nodes.forEach((node) => {
537
- if (node.id() == v) {
538
- node.addClass('substructure-parallel');
539
- node.data('structType', 'Parallel');
540
- // node.data('structColor', '#50C878');
541
- node.data('groupId', 'Parallel' + parallelId);
542
- node.data('structs', {
543
- ...node.data('structs'), Parallel: {
544
- color: '#50C878', groupId: `Parallel_${parallelId}`
545
- }
546
- });
547
- }
548
- });
549
- });
550
- u1.forEach((v1) => {
551
- nodes.forEach((node) => {
552
- if (node.id() == v1.id()) {
553
- node.data('parallelGroupIdVec', [...node.data('parallelGroupIdVec'), 'Parallel' + parallelId]);
554
- }
555
- });
556
- });
557
- parallelId++;
563
+ const neighbors = neighborsOf(node);
564
+ if (!(neighbors.length >= params.minParallelNeighbors)) {
565
+ return;
566
+ }
567
+ // Encode a sorted copy so IDs cannot collide and cached neighbor order stays intact.
568
+ const signature = JSON.stringify(neighbors.map((neighbor) => neighbor.id()).sort());
569
+ const group = parallelGroups.get(signature);
570
+ if (group) {
571
+ group.push(node);
572
+ }
573
+ else {
574
+ parallelGroups.set(signature, [node]);
558
575
  }
576
+ });
577
+ // Map insertion order preserves group numbering by the first eligible member.
578
+ let parallelId = 0;
579
+ for (const group of parallelGroups.values()) {
580
+ if (group.length < 2)
581
+ continue;
582
+ const groupId = "Parallel" + parallelId;
583
+ group.forEach((node) => {
584
+ node.addClass("substructure-parallel");
585
+ node.data("structType", "Parallel");
586
+ node.data("groupId", groupId);
587
+ node.data("structs", {
588
+ ...node.data("structs"),
589
+ Parallel: {
590
+ color: "#50C878",
591
+ groupId: `Parallel_${parallelId}`,
592
+ },
593
+ });
594
+ });
595
+ neighborsOf(group[0]).forEach((neighbor) => {
596
+ // Neighborhoods may include nodes outside the supplied collection.
597
+ const node = nodesById.get(neighbor.id());
598
+ if (node) {
599
+ node.data("parallelGroupIdVec", [
600
+ ...node.data("parallelGroupIdVec"),
601
+ groupId,
602
+ ]);
603
+ }
604
+ });
605
+ parallelId++;
559
606
  }
560
- console.log("num of chains: ", chains.length);
561
607
  };
562
608
  LuminaLayout.prototype.run = function () {
563
609
  this.stopped = false;
@@ -583,7 +629,6 @@ LuminaLayout.prototype.run = function () {
583
629
  components.forEach((component, index) => {
584
630
  this.vnodes = [];
585
631
  this.vedges = [];
586
- console.log(`independent net: ${index + 1}`);
587
632
  // 1. get user defined boundingBox,if not then default cy container
588
633
  const bb = this.boundingBox || this.cy.extent();
589
634
  const width = bb.x2 - bb.x1;
@@ -612,8 +657,6 @@ LuminaLayout.prototype.run = function () {
612
657
  });
613
658
  });
614
659
  }
615
- console.log("num of nodes: " + nodes.length);
616
- console.log("num of edges: " + edgeSet.length);
617
660
  // Capture initial state
618
661
  this.captureStep('Initial', 'Initial node positions before any layout', {
619
662
  nodeCount: nodes.length, edgeCount: edgeSet.length
@@ -1089,6 +1132,7 @@ LuminaLayout.prototype.run = function () {
1089
1132
  numOfCollision = 0;
1090
1133
  const padding = params.collisionPadding;
1091
1134
  let iter = 0;
1135
+ let overlapLimitReached = false;
1092
1136
  while (colisionFlag) {
1093
1137
  if (this.stopped) {
1094
1138
  break;
@@ -1131,12 +1175,17 @@ LuminaLayout.prototype.run = function () {
1131
1175
  }
1132
1176
  // [Optimization 3] Safety valve: exit directly if the iteration count becomes too large (typically when nodes are extremely dense)
1133
1177
  if (numOfCollision > 10000) {
1134
- console.warn("avoid dead loop");
1178
+ overlapLimitReached = true;
1135
1179
  break;
1136
1180
  }
1137
- // this.captureStep('Anti-collision', 'Eliminate all collisions ' + iter, {iterations});
1138
1181
  }
1139
- this.captureStep('Anti-collision', 'Eliminate all collisions', { iterations });
1182
+ this.captureStep("Anti-collision", overlapLimitReached
1183
+ ? "Overlap removal stopped at safety limit"
1184
+ : "Eliminate all collisions", {
1185
+ iterations,
1186
+ overlapIterations: iter,
1187
+ overlapLimitReached,
1188
+ });
1140
1189
  if (1) {
1141
1190
  numOfCollision = 0;
1142
1191
  for (let i = 0; i < this.vnodes.length; i++) {
@@ -1169,8 +1218,7 @@ LuminaLayout.prototype.run = function () {
1169
1218
  if (numNodes === 0)
1170
1219
  return;
1171
1220
  // Index mapping: ID -> array index, for matrix operations
1172
- const nodeIndexMap = new Map();
1173
- this.vnodes.forEach((v, i) => nodeIndexMap.set(v.id, i));
1221
+ // The distance helper builds the index alongside its weighted adjacency.
1174
1222
  // Extract the adjacency list for reuse during initialization
1175
1223
  const adj = new Map();
1176
1224
  this.vedges.forEach((e) => {
@@ -1184,34 +1232,10 @@ LuminaLayout.prototype.run = function () {
1184
1232
  adj.get(e.target.id).push(e.source);
1185
1233
  });
1186
1234
  // -------------------------------------------------------------
1187
- // [Preprocessing 1] Calculate the shortest-path distance matrix between every pair of nodes in the graph (APSP - Floyd Warshall)
1235
+ // [Preprocessing 1] Calculate all-pairs shortest paths using weighted Dijkstra
1188
1236
  // -------------------------------------------------------------
1189
- const distMatrix = Array.from({ length: numNodes }, () => new Array(numNodes).fill(Infinity));
1237
+ const distMatrix = computeStressDistances(this.vnodes, this.vedges, idealLength);
1190
1238
  const weightMatrix = Array.from({ length: numNodes }, () => new Array(numNodes).fill(0));
1191
- for (let i = 0; i < numNodes; i++)
1192
- distMatrix[i][i] = 0;
1193
- // Assign the ideal distance baseline based on real edges
1194
- this.vedges.forEach((e) => {
1195
- if (!e.source || !e.target)
1196
- return;
1197
- const u = nodeIndexMap.get(e.source.id);
1198
- const v = nodeIndexMap.get(e.target.id);
1199
- if (u !== undefined && v !== undefined) {
1200
- const idealDist = idealLength + e.source.radius + e.target.radius;
1201
- distMatrix[u][v] = Math.min(distMatrix[u][v], idealDist);
1202
- distMatrix[v][u] = Math.min(distMatrix[v][u], idealDist);
1203
- }
1204
- });
1205
- // Use Floyd-Warshall to compute shortest paths between all node pairs
1206
- for (let k = 0; k < numNodes; k++) {
1207
- for (let i = 0; i < numNodes; i++) {
1208
- for (let j = 0; j < numNodes; j++) {
1209
- if (distMatrix[i][k] + distMatrix[k][j] < distMatrix[i][j]) {
1210
- distMatrix[i][j] = distMatrix[i][k] + distMatrix[k][j];
1211
- }
1212
- }
1213
- }
1214
- }
1215
1239
  // Calculate the weight matrix W_ij = 1 / (d_ij ^ weightExponent)
1216
1240
  for (let i = 0; i < numNodes; i++) {
1217
1241
  for (let j = 0; j < numNodes; j++) {
@@ -1321,6 +1345,7 @@ LuminaLayout.prototype.run = function () {
1321
1345
  let numOfCollision = 0;
1322
1346
  const padding = params.collisionPadding;
1323
1347
  let overlapIter = 0;
1348
+ let overlapLimitReached = false;
1324
1349
  while (colisionFlag) {
1325
1350
  if (this.stopped) {
1326
1351
  break;
@@ -1354,11 +1379,17 @@ LuminaLayout.prototype.run = function () {
1354
1379
  }
1355
1380
  }
1356
1381
  if (numOfCollision > 10000 || overlapIter > params.maxOverlapIterations) {
1357
- console.warn("Avoid dead loop in overlap removal");
1382
+ overlapLimitReached = true;
1358
1383
  break;
1359
1384
  }
1360
1385
  }
1361
- this.captureStep('Anti-collision', 'Eliminate all collisions', { iterations });
1386
+ this.captureStep("Anti-collision", overlapLimitReached
1387
+ ? "Overlap removal stopped at safety limit"
1388
+ : "Eliminate all collisions", {
1389
+ iterations,
1390
+ overlapIterations: overlapIter,
1391
+ overlapLimitReached,
1392
+ });
1362
1393
  }
1363
1394
  this.captureStep('Virtual Node Layout', 'Stress Majorization layout applied to virtual nodes', { iterations });
1364
1395
  }
@@ -1670,34 +1701,6 @@ LuminaLayout.prototype.run = function () {
1670
1701
  nodes[2].position().y = 90;
1671
1702
  }
1672
1703
  rotateNetworkToMinimumBoundingBox(nodes);
1673
- if (1) {
1674
- let maxDis = 0;
1675
- let maxS = 0;
1676
- let maxT = 0;
1677
- let minDist = 10e10;
1678
- let minS = 0;
1679
- let minT = 0;
1680
- let avgDis = 0;
1681
- edgeSet.forEach((e) => {
1682
- const s = e.source();
1683
- const t = e.target();
1684
- const distance = Math.sqrt(Math.pow(s.position().x - t.position().x, 2) + Math.pow(s.position().y - t.position().y, 2));
1685
- if (maxDis < distance) {
1686
- maxDis = distance;
1687
- maxS = s.id();
1688
- maxT = t.id();
1689
- }
1690
- if (minDist > distance) {
1691
- minDist = distance;
1692
- minS = s.id();
1693
- minT = t.id();
1694
- }
1695
- avgDis += distance;
1696
- });
1697
- console.log("maxDis:", maxDis, " ", maxS, "->", maxT);
1698
- console.log("minDist:", minDist, " ", minS, "->", minT);
1699
- console.log("avgDis:", avgDis / edgeSet.length);
1700
- }
1701
1704
  const bb2 = nodes.boundingBox();
1702
1705
  networkInfos.push({
1703
1706
  component: component,
@@ -2289,7 +2292,7 @@ LuminaLayout.prototype.goToStep = function (stepIndex) {
2289
2292
  });
2290
2293
  // Visualize virtual nodes if they exist at this step
2291
2294
  this.visualizeVirtualNodes(step);
2292
- console.log(`[Step ${step.stepNumber}/${this.steps.length - 1}] ${step.stepName}: ${step.description}`);
2295
+ console.debug(`[Step ${step.stepNumber}/${this.steps.length - 1}] ${step.stepName}: ${step.description}`);
2293
2296
  this.cy.fit(null, 50);
2294
2297
  return this;
2295
2298
  };
@@ -2301,7 +2304,7 @@ LuminaLayout.prototype.nextStep = function () {
2301
2304
  return this.goToStep(this.currentStepIndex + 1);
2302
2305
  }
2303
2306
  else {
2304
- console.log('Already at the last step');
2307
+ console.debug("Already at the last step");
2305
2308
  return this;
2306
2309
  }
2307
2310
  };
@@ -2313,7 +2316,7 @@ LuminaLayout.prototype.prevStep = function () {
2313
2316
  return this.goToStep(this.currentStepIndex - 1);
2314
2317
  }
2315
2318
  else {
2316
- console.log('Already at the first step');
2319
+ console.debug("Already at the first step");
2317
2320
  return this;
2318
2321
  }
2319
2322
  };
@@ -2322,14 +2325,9 @@ LuminaLayout.prototype.prevStep = function () {
2322
2325
  */
2323
2326
  LuminaLayout.prototype.listSteps = function () {
2324
2327
  if (!this.params.stepByStep || this.steps.length === 0) {
2325
- console.log('No steps available');
2328
+ console.debug("No steps available");
2326
2329
  return [];
2327
2330
  }
2328
- console.log(`Total steps: ${this.steps.length}`);
2329
- this.steps.forEach((step, index) => {
2330
- const current = index === this.currentStepIndex ? ' ← CURRENT' : '';
2331
- console.log(` [${index}] ${step.stepName}: ${step.description}${current}`);
2332
- });
2333
2331
  return this.steps.map((s) => ({
2334
2332
  stepNumber: s.stepNumber,
2335
2333
  stepName: s.stepName,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intact-ebi/cytoscape-lumina",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
4
4
  "description": "LUMINA: a structure-aware Cytoscape.js layout that detects cycles, chains, stars and parallel groups and lays them out with force-directed or stress-majorization solvers.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -26,8 +26,9 @@
26
26
  "registry": "https://registry.npmjs.org/"
27
27
  },
28
28
  "scripts": {
29
- "build": "tsc && cp demo/favicon.ico dist/favicon.ico && cp demo/index.html dist/index.html && cp node_modules/cytoscape/dist/cytoscape.min.js dist/ && if [ -d demo/data ]; then cp -r demo/data dist/data; fi && touch dist/.nojekyll",
30
- "dev": "npm run build && concurrently \"tsc --watch\" \"node watch.cjs\" \"http-server dist -p 8080 -c-1\"",
29
+ "build": "npm run build:lib && npm run build:browser && cp demo/favicon.ico dist/favicon.ico && cp demo/index.html dist/index.html && cp node_modules/cytoscape/dist/cytoscape.min.js dist/ && if [ -d demo/data ]; then cp -r demo/data dist/data; fi && touch dist/.nojekyll",
30
+ "build:browser": "esbuild src/index.ts --bundle --format=esm --platform=browser --target=es2020 --external:cytoscape --outfile=dist/lumina-browser.js",
31
+ "dev": "npm run build && concurrently \"tsc --watch\" \"npm run build:browser -- --watch\" \"node watch.cjs\" \"http-server dist -p 8080 -c-1\"",
31
32
  "deploy": "./deploy.sh",
32
33
  "build:lib": "tsc",
33
34
  "local-publish": "npm run build:lib && yalc push",
@@ -37,6 +38,7 @@
37
38
  "devDependencies": {
38
39
  "concurrently": "^9.2.4",
39
40
  "cytoscape": "^3.33.1",
41
+ "esbuild": "^0.28.2",
40
42
  "gh-pages": "^6.3.0",
41
43
  "http-server": "^14.1.1",
42
44
  "typescript": "^5.9.3",
@@ -65,5 +67,8 @@
65
67
  },
66
68
  "allowScripts": {
67
69
  "fsevents@2.3.3": true
70
+ },
71
+ "dependencies": {
72
+ "fastpriorityqueue": "^0.8.0"
68
73
  }
69
- }
74
+ }