singhn9 commited on
Commit
fa19ccc
·
verified ·
1 Parent(s): 9370b77

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +91 -107
app.py CHANGED
@@ -211,12 +211,12 @@ function draw() {
211
 
212
  const radius = Math.min(w, h) * 0.36;
213
 
214
- // node positions around circle (add abbrev)
215
  const n = NODES.length;
216
  function angleFor(i) { return (i / n) * 2 * Math.PI; }
217
  const nodePos = NODES.map((name,i) => {
218
- const ang = angleFor(i) - Math.PI/2; // start at top
219
- const ab = (name.length > 7 ? name.slice(0,5) + "…" : name);
220
  return {
221
  name: name,
222
  abbrev: ab,
@@ -229,7 +229,6 @@ function draw() {
229
  const nameToIndex = {};
230
  NODES.forEach((nm,i)=> nameToIndex[nm]=i);
231
 
232
- // small node circles and labels outside
233
  const group = svg.append("g").selectAll("g").data(nodePos).enter().append("g")
234
  .attr("transform", d => `translate(${d.x},${d.y})`);
235
 
@@ -240,6 +239,7 @@ function draw() {
240
  .style("stroke-width", 1)
241
  .style("cursor", "pointer");
242
 
 
243
  group.append("text")
244
  .attr("x", d => Math.cos(d.angle) * (radius + 26))
245
  .attr("y", d => Math.sin(d.angle) * (radius + 26))
@@ -258,12 +258,11 @@ function draw() {
258
  .style("cursor","pointer")
259
  .text(d => d.abbrev);
260
 
261
- // bezier helper
262
  function bezierPath(x0,y0,x1,y1,above=true) {
263
  const mx = (x0 + x1)/2;
264
  const my = (y0 + y1)/2;
265
- const dx = mx;
266
- const dy = my;
267
  const len = Math.sqrt(dx*dx + dy*dy) || 1;
268
  const ux = dx/len, uy = dy/len;
269
  const offset = (above ? -1 : 1) * Math.max(30, radius*0.9);
@@ -272,80 +271,63 @@ function draw() {
272
  return `M ${x0} ${y0} Q ${cx} ${cy} ${x1} ${y1}`;
273
  }
274
 
275
- // stroke width scale
276
  const allW = [].concat(BUYS.map(d=>d[2]), SELLS.map(d=>d[2]), TRANSFERS.map(d=>d[2]));
277
  const wmin = Math.min(...(allW.length?allW:[1]));
278
  const wmax = Math.max(...(allW.length?allW:[1]));
279
  const stroke = d3.scaleLinear().domain([wmin, Math.max(wmax,1)]).range([1.0, 6.0]);
280
 
281
- // buys (top)
282
- const buyGroup = svg.append("g").attr("class","buys");
283
  BUYS.forEach(b => {
284
  const a = b[0], c = b[1], wt = b[2];
285
  if (!(a in nameToIndex) || !(c in nameToIndex)) return;
286
  const s = nodePos[nameToIndex[a]];
287
  const t = nodePos[nameToIndex[c]];
288
- const path = bezierPath(s.x,s.y,t.x,t.y,true);
289
  buyGroup.append("path")
290
- .attr("d", path)
291
  .attr("fill","none")
292
  .attr("stroke","#2e8540")
293
  .attr("stroke-width", stroke(wt))
294
- .attr("stroke-linecap","round")
295
  .attr("opacity", 0.92)
296
  .attr("data-src", a)
297
- .attr("data-tgt", c)
298
- .on("mouseover", function() { d3.select(this).attr("opacity",1); })
299
- .on("mouseout", function() { d3.select(this).attr("opacity",0.92); });
300
  });
301
 
302
- // sells (bottom)
303
- const sellGroup = svg.append("g").attr("class","sells");
304
  SELLS.forEach(s => {
305
  const c = s[0], a = s[1], wt = s[2];
306
  if (!(c in nameToIndex) || !(a in nameToIndex)) return;
307
  const sp = nodePos[nameToIndex[c]];
308
  const tp = nodePos[nameToIndex[a]];
309
- const path = bezierPath(sp.x,sp.y,tp.x,tp.y,false);
310
  sellGroup.append("path")
311
- .attr("d", path)
312
  .attr("fill","none")
313
  .attr("stroke","#c0392b")
314
  .attr("stroke-width", stroke(wt))
315
- .attr("stroke-linecap","round")
316
  .attr("stroke-dasharray","4,3")
317
  .attr("opacity",0.86)
318
  .attr("data-src", c)
319
- .attr("data-tgt", a)
320
- .on("mouseover", function() { d3.select(this).attr("opacity",1); })
321
- .on("mouseout", function() { d3.select(this).attr("opacity",0.86); });
322
  });
323
 
324
- // transfers (grey chords)
325
- const transferGroup = svg.append("g").attr("class","transfers");
326
  TRANSFERS.forEach(tr => {
327
  const sname = tr[0], tname = tr[1], wt = tr[2];
328
  if (!(sname in nameToIndex) || !(tname in nameToIndex)) return;
329
  const sp = nodePos[nameToIndex[sname]];
330
  const tp = nodePos[nameToIndex[tname]];
331
- const mx = (sp.x + tp.x)/2;
332
- const my = (sp.y + tp.y)/2;
333
- const cx = mx * 0.3, cy = my * 0.3;
334
- const path = `M ${sp.x} ${sp.y} Q ${cx} ${cy} ${tp.x} ${tp.y}`;
335
  transferGroup.append("path")
336
- .attr("d", path)
337
  .attr("fill","none")
338
  .attr("stroke","#7d7d7d")
339
  .attr("stroke-width", stroke(wt))
340
  .attr("opacity",0.7)
341
  .attr("data-src", sname)
342
- .attr("data-tgt", tname)
343
- .on("mouseover", function() { d3.select(this).attr("opacity",1); })
344
- .on("mouseout", function() { d3.select(this).attr("opacity",0.7); });
345
  });
346
 
347
- // loops (external arcs)
348
- const loopGroup = svg.append("g").attr("class","loops");
349
  LOOPS.forEach(lp => {
350
  const a = lp[0], c = lp[1], b = lp[2];
351
  if (!(a in nameToIndex) || !(b in nameToIndex)) return;
@@ -353,101 +335,103 @@ function draw() {
353
  const sb = nodePos[nameToIndex[b]];
354
  const mx = (sa.x + sb.x)/2;
355
  const my = (sa.y + sb.y)/2;
356
- const len = Math.sqrt((sa.x - sb.x)*(sa.x - sb.x) + (sa.y - sb.y)*(sa.y - sb.y));
357
- const outward = Math.max(40, radius*0.28 + len * 0.12);
358
- const ndx = mx, ndy = my;
359
- const nlen = Math.sqrt(ndx*ndx + ndy*ndy) || 1;
360
- const ux = ndx/nlen, uy = ndy/nlen;
361
- const cx = mx + ux * outward;
362
- const cy = my + uy * outward;
363
- const path = `M ${sa.x} ${sa.y} Q ${cx} ${cy} ${sb.x} ${sb.y}`;
364
  loopGroup.append("path")
365
- .attr("d", path)
366
- .attr("fill", "none")
367
- .attr("stroke", "#227a6d")
368
  .attr("stroke-width", 2.8)
369
- .attr("opacity",0.95)
370
- .on("mouseover", function() { d3.select(this).attr("opacity",1); })
371
- .on("mouseout", function() { d3.select(this).attr("opacity",0.95); });
372
  });
373
 
374
- // interactivity: focus on node
375
  function setOpacityFor(nodeName) {
376
- group.selectAll("circle").style("opacity", d => (d.name === nodeName ? 1.0 : 0.18));
377
- group.selectAll("text").style("opacity", d => (d.name === nodeName ? 1.0 : 0.28));
378
- buyGroup.selectAll("path").style("opacity", function() {
379
- const src = this.getAttribute("data-src");
380
- const tgt = this.getAttribute("data-tgt");
381
- return (src === nodeName || tgt === nodeName) ? 0.98 : 0.06;
382
- });
383
- sellGroup.selectAll("path").style("opacity", function() {
384
- const src = this.getAttribute("data-src");
385
- const tgt = this.getAttribute("data-tgt");
386
- return (src === nodeName || tgt === nodeName) ? 0.98 : 0.06;
387
- });
388
- transferGroup.selectAll("path").style("opacity", function() {
389
- const src = this.getAttribute("data-src");
390
- const tgt = this.getAttribute("data-tgt");
391
- return (src === nodeName || tgt === nodeName) ? 0.98 : 0.06;
392
- });
393
- loopGroup.selectAll("path").style("opacity", 0.95);
394
  }
395
 
396
  function resetOpacity() {
397
- group.selectAll("circle").style("opacity", 1.0);
398
- group.selectAll("text").style("opacity", 1.0);
399
- buyGroup.selectAll("path").style("opacity", 0.92);
400
- sellGroup.selectAll("path").style("opacity", 0.86);
401
- transferGroup.selectAll("path").style("opacity", 0.7);
402
- loopGroup.selectAll("path").style("opacity", 0.95);
403
- const box = document.getElementById("info-box");
404
- if (box) box.innerHTML = "<b>Click a node</b> to view details here.";
405
  }
406
 
407
- // get connections for info box
408
  function getConnections(nodeName) {
409
- let buys = BUYS.filter(x => x[0] === nodeName || x[1] === nodeName)
410
- .map(x => (x[0] === nodeName ? x[1] : x[0]));
411
- let sells = SELLS.filter(x => x[0] === nodeName || x[1] === nodeName)
412
- .map(x => (x[0] === nodeName ? x[1] : x[0]));
413
- let transfers = TRANSFERS.filter(x => x[0] === nodeName || x[1] === nodeName)
414
- .map(x => (x[0] === nodeName ? x[1] : x[0]));
415
- let loops = LOOPS.filter(x => x[0] === nodeName || x[2] === nodeName)
416
- .map(x => (x[0] === nodeName ? x[2] : x[0]));
417
- // unique and sorted
418
- function uniq(arr){ return Array.from(new Set(arr)).sort(); }
419
- return {buys: uniq(buys), sells: uniq(sells), transfers: uniq(transfers), loops: uniq(loops)};
 
 
 
 
 
 
 
 
 
420
  }
421
 
 
422
  function showInfo(nodeName) {
423
- const box = document.getElementById("info-box");
424
  const con = getConnections(nodeName);
 
425
  box.innerHTML = `
426
- <div style="font-size:14px;"><b>${nodeName}</b></div><div style="height:6px"></div>
427
- <div style="font-size:12px"><b>Connected nodes</b></div>
428
- <div style="margin-top:6px; font-size:12px;">
429
- <div><b>Buys:</b> ${con.buys.length ? con.buys.join(", ") : "<span style='color:#666'>None</span>"}</div>
430
- <div><b>Sells:</b> ${con.sells.length ? con.sells.join(", ") : "<span style='color:#666'>None</span>"}</div>
431
- <div><b>Transfers:</b> ${con.transfers.length ? con.transfers.join(", ") : "<span style='color:#666'>None</span>"}</div>
432
- <div><b>Loops:</b> ${con.loops.length ? con.loops.join(", ") : "<span style='color:#666'>None</span>"}</div>
433
- </div>
434
  `;
435
  }
436
 
437
  // click handlers
438
- group.selectAll("circle").style("cursor","pointer").on("click", function(e,d) {
439
- setOpacityFor(d.name);
440
- showInfo(d.name);
441
- if (e && e.stopPropagation) e.stopPropagation();
 
 
 
 
 
 
 
442
  });
443
- group.selectAll("text").style("cursor","pointer").on("click", function(e,d) {
444
- setOpacityFor(d.name);
445
- showInfo(d.name);
446
- if (e && e.stopPropagation) e.stopPropagation();
447
  });
448
 
449
  document.getElementById("arc-reset").onclick = resetOpacity;
450
- svg.on("click", function(event) {
451
  if (event.target.tagName === "svg") resetOpacity();
452
  });
453
  }
 
211
 
212
  const radius = Math.min(w, h) * 0.36;
213
 
214
+ // nodes with abbrev
215
  const n = NODES.length;
216
  function angleFor(i) { return (i / n) * 2 * Math.PI; }
217
  const nodePos = NODES.map((name,i) => {
218
+ const ang = angleFor(i) - Math.PI/2;
219
+ const ab = (name.length > 7 ? name.slice(0,5)+"…" : name);
220
  return {
221
  name: name,
222
  abbrev: ab,
 
229
  const nameToIndex = {};
230
  NODES.forEach((nm,i)=> nameToIndex[nm]=i);
231
 
 
232
  const group = svg.append("g").selectAll("g").data(nodePos).enter().append("g")
233
  .attr("transform", d => `translate(${d.x},${d.y})`);
234
 
 
239
  .style("stroke-width", 1)
240
  .style("cursor", "pointer");
241
 
242
+ // short label on load
243
  group.append("text")
244
  .attr("x", d => Math.cos(d.angle) * (radius + 26))
245
  .attr("y", d => Math.sin(d.angle) * (radius + 26))
 
258
  .style("cursor","pointer")
259
  .text(d => d.abbrev);
260
 
261
+ // helper curves
262
  function bezierPath(x0,y0,x1,y1,above=true) {
263
  const mx = (x0 + x1)/2;
264
  const my = (y0 + y1)/2;
265
+ const dx = mx, dy = my;
 
266
  const len = Math.sqrt(dx*dx + dy*dy) || 1;
267
  const ux = dx/len, uy = dy/len;
268
  const offset = (above ? -1 : 1) * Math.max(30, radius*0.9);
 
271
  return `M ${x0} ${y0} Q ${cx} ${cy} ${x1} ${y1}`;
272
  }
273
 
 
274
  const allW = [].concat(BUYS.map(d=>d[2]), SELLS.map(d=>d[2]), TRANSFERS.map(d=>d[2]));
275
  const wmin = Math.min(...(allW.length?allW:[1]));
276
  const wmax = Math.max(...(allW.length?allW:[1]));
277
  const stroke = d3.scaleLinear().domain([wmin, Math.max(wmax,1)]).range([1.0, 6.0]);
278
 
279
+ // groups
280
+ const buyGroup = svg.append("g");
281
  BUYS.forEach(b => {
282
  const a = b[0], c = b[1], wt = b[2];
283
  if (!(a in nameToIndex) || !(c in nameToIndex)) return;
284
  const s = nodePos[nameToIndex[a]];
285
  const t = nodePos[nameToIndex[c]];
 
286
  buyGroup.append("path")
287
+ .attr("d", bezierPath(s.x,s.y,t.x,t.y,true))
288
  .attr("fill","none")
289
  .attr("stroke","#2e8540")
290
  .attr("stroke-width", stroke(wt))
 
291
  .attr("opacity", 0.92)
292
  .attr("data-src", a)
293
+ .attr("data-tgt", c);
 
 
294
  });
295
 
296
+ const sellGroup = svg.append("g");
 
297
  SELLS.forEach(s => {
298
  const c = s[0], a = s[1], wt = s[2];
299
  if (!(c in nameToIndex) || !(a in nameToIndex)) return;
300
  const sp = nodePos[nameToIndex[c]];
301
  const tp = nodePos[nameToIndex[a]];
 
302
  sellGroup.append("path")
303
+ .attr("d", bezierPath(sp.x,sp.y,tp.x,tp.y,false))
304
  .attr("fill","none")
305
  .attr("stroke","#c0392b")
306
  .attr("stroke-width", stroke(wt))
 
307
  .attr("stroke-dasharray","4,3")
308
  .attr("opacity",0.86)
309
  .attr("data-src", c)
310
+ .attr("data-tgt", a);
 
 
311
  });
312
 
313
+ const transferGroup = svg.append("g");
 
314
  TRANSFERS.forEach(tr => {
315
  const sname = tr[0], tname = tr[1], wt = tr[2];
316
  if (!(sname in nameToIndex) || !(tname in nameToIndex)) return;
317
  const sp = nodePos[nameToIndex[sname]];
318
  const tp = nodePos[nameToIndex[tname]];
319
+ const mx = (sp.x + tp.x)/2, my = (sp.y + tp.y)/2;
 
 
 
320
  transferGroup.append("path")
321
+ .attr("d", `M ${sp.x} ${sp.y} Q ${mx*0.3} ${my*0.3} ${tp.x} ${tp.y}`)
322
  .attr("fill","none")
323
  .attr("stroke","#7d7d7d")
324
  .attr("stroke-width", stroke(wt))
325
  .attr("opacity",0.7)
326
  .attr("data-src", sname)
327
+ .attr("data-tgt", tname);
 
 
328
  });
329
 
330
+ const loopGroup = svg.append("g");
 
331
  LOOPS.forEach(lp => {
332
  const a = lp[0], c = lp[1], b = lp[2];
333
  if (!(a in nameToIndex) || !(b in nameToIndex)) return;
 
335
  const sb = nodePos[nameToIndex[b]];
336
  const mx = (sa.x + sb.x)/2;
337
  const my = (sa.y + sb.y)/2;
338
+ const len = Math.sqrt((sa.x-sb.x)**2 + (sa.y-sb.y)**2);
339
+ const outward = Math.max(40, radius*0.28 + len*0.12);
340
+ const nlen = Math.sqrt(mx*mx + my*my)||1;
341
+ const ux = mx/nlen, uy = my/nlen;
342
+ const cx = mx + ux*outward, cy = my + uy*outward;
 
 
 
343
  loopGroup.append("path")
344
+ .attr("d", `M ${sa.x} ${sa.y} Q ${cx} ${cy} ${sb.x} ${sb.y}`)
345
+ .attr("fill","none")
346
+ .attr("stroke","#227a6d")
347
  .attr("stroke-width", 2.8)
348
+ .attr("opacity",0.95);
 
 
349
  });
350
 
351
+ // opacity control
352
  function setOpacityFor(nodeName) {
353
+ group.selectAll("circle").style("opacity", d => d.name===nodeName ? 1 : 0.18);
354
+ group.selectAll("text").style("opacity", d => d.name===nodeName ? 1 : 0.28);
355
+
356
+ function isConn(path) {
357
+ const src = path.getAttribute("data-src");
358
+ const tgt = path.getAttribute("data-tgt");
359
+ return (src===nodeName || tgt===nodeName);
360
+ }
361
+
362
+ buyGroup.selectAll("path").style("opacity", function(){ return isConn(this)?0.98:0.06; });
363
+ sellGroup.selectAll("path").style("opacity", function(){ return isConn(this)?0.98:0.06; });
364
+ transferGroup.selectAll("path").style("opacity", function(){ return isConn(this)?0.98:0.06; });
 
 
 
 
 
 
365
  }
366
 
367
  function resetOpacity() {
368
+ group.selectAll("circle").style("opacity",1);
369
+ group.selectAll("text").style("opacity",1);
370
+ buyGroup.selectAll("path").style("opacity",0.92);
371
+ sellGroup.selectAll("path").style("opacity",0.86);
372
+ transferGroup.selectAll("path").style("opacity",0.7);
373
+ loopGroup.selectAll("path").style("opacity",0.95);
374
+ updateLabels(null, new Set());
375
+ document.getElementById("info-box").innerHTML = "<b>Click a node</b> to view details here.";
376
  }
377
 
378
+ // detect connections for label switching
379
  function getConnections(nodeName) {
380
+ let buys = BUYS.filter(x => x[0]===nodeName || x[1]===nodeName)
381
+ .map(x => (x[0]===nodeName ? x[1] : x[0]));
382
+ let sells = SELLS.filter(x => x[0]===nodeName || x[1]===nodeName)
383
+ .map(x => (x[0]===nodeName ? x[1] : x[0]));
384
+ let transfers = TRANSFERS.filter(x => x[0]===nodeName || x[1]===nodeName)
385
+ .map(x => (x[0]===nodeName ? x[1] : x[0]));
386
+ let loops = LOOPS.filter(x => x[0]===nodeName || x[2]===nodeName)
387
+ .map(x => (x[0]===nodeName ? x[2] : x[0]));
388
+
389
+ return new Set([].concat(buys, sells, transfers, loops));
390
+ }
391
+
392
+ // dynamic label switching
393
+ function updateLabels(selected, connectedSet) {
394
+ group.selectAll("text").text(d => {
395
+ if (selected && (d.name === selected || connectedSet.has(d.name))) {
396
+ return d.name; // show full name
397
+ }
398
+ return d.abbrev; // otherwise short
399
+ });
400
  }
401
 
402
+ // info box updates
403
  function showInfo(nodeName) {
 
404
  const con = getConnections(nodeName);
405
+ const box = document.getElementById("info-box");
406
  box.innerHTML = `
407
+ <div style="font-size:14px;"><b>${nodeName}</b></div>
408
+ <div style="margin-top:6px; font-size:12px;"><b>Connected nodes</b></div>
409
+ <div style="margin-top:6px; font-size:12px;">
410
+ <div><b>Buys:</b> ${[...con].join(", ") || "<span style='color:#777'>None</span>"}</div>
411
+ </div>
 
 
 
412
  `;
413
  }
414
 
415
  // click handlers
416
+ function selectNode(d) {
417
+ const nodeName = d.name;
418
+ setOpacityFor(nodeName);
419
+ showInfo(nodeName);
420
+ const connected = getConnections(nodeName);
421
+ updateLabels(nodeName, connected);
422
+ }
423
+
424
+ group.selectAll("circle").on("click", function(e,d){
425
+ selectNode(d);
426
+ if (e.stopPropagation) e.stopPropagation();
427
  });
428
+ group.selectAll("text").on("click", function(e,d){
429
+ selectNode(d);
430
+ if (e.stopPropagation) e.stopPropagation();
 
431
  });
432
 
433
  document.getElementById("arc-reset").onclick = resetOpacity;
434
+ svg.on("click", function(event){
435
  if (event.target.tagName === "svg") resetOpacity();
436
  });
437
  }