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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +141 -138
app.py CHANGED
@@ -183,7 +183,6 @@ JS_TEMPLATE = """
183
  <div style="margin-top:6px;color:#666;font-size:12px;">Note: Transfers are inferred by matching sells and buys across AMCs. Thickness shows relative weight.</div>
184
  </div>
185
 
186
- <!-- Info box -->
187
  <div id="info-box" style="margin-top:12px; padding:10px;
188
  border:1px solid #ddd; border-radius:8px; font-family:sans-serif;
189
  font-size:13px; background:#fbfbfb;">
@@ -192,31 +191,35 @@ JS_TEMPLATE = """
192
 
193
  <script src="https://d3js.org/d3.v7.min.js"></script>
194
  <script>
195
- const NODES = __NODES__ ;
196
- const NODE_TYPE = __NODE_TYPE__ ;
197
- const BUYS = __BUYS__ ;
198
- const SELLS = __SELLS__ ;
199
- const TRANSFERS = __TRANSFERS__ ;
200
- const LOOPS = __LOOPS__ ;
201
 
202
  function draw() {
203
  const container = document.getElementById("arc-container");
204
  container.innerHTML = "";
 
205
  const w = Math.min(1200, container.clientWidth || 920);
206
  const h = Math.max(420, Math.floor(w * 0.62));
207
- const svg = d3.select(container).append("svg")
 
 
208
  .attr("width", "100%")
209
  .attr("height", h)
210
  .attr("viewBox", [-w/2, -h/2, w, h].join(" "));
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,
@@ -227,57 +230,49 @@ function draw() {
227
  });
228
 
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
 
235
- group.append("circle")
236
  .attr("r", 16)
237
- .style("fill", d => NODE_TYPE[d.name] === "amc" ? "#2b6fa6" : "#f2c88d")
238
- .style("stroke", "#222")
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))
246
- .attr("dy", "0.35em")
247
- .style("font-family", "sans-serif")
248
- .style("font-size", Math.max(10, Math.min(14, radius*0.04)))
249
- .style("text-anchor", d => {
250
- const deg = (d.angle * 180 / Math.PI);
251
- return (deg > -90 && deg < 90) ? "start" : "end";
252
- })
253
- .attr("transform", d => {
254
- const deg = (d.angle * 180 / Math.PI);
255
- const flip = (deg > 90 || deg < -90) ? 180 : 0;
256
- return `rotate(${deg}) translate(${radius + 26}) rotate(${flip})`;
257
- })
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);
269
- const cx = mx + ux * offset;
270
- const cy = my + uy * offset;
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;
@@ -285,15 +280,15 @@ function draw() {
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;
@@ -301,16 +296,16 @@ function draw() {
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;
@@ -318,45 +313,82 @@ function draw() {
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;
334
  const sa = nodePos[nameToIndex[a]];
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; });
@@ -364,76 +396,46 @@ function draw() {
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
  }
438
 
439
  draw();
@@ -441,6 +443,7 @@ window.addEventListener("resize", draw);
441
  </script>
442
  """
443
 
 
444
  def make_arc_html(nodes, node_type, buys, sells, transfers, loops):
445
  # prepare JSON strings
446
  nodes_json = json.dumps(nodes)
 
183
  <div style="margin-top:6px;color:#666;font-size:12px;">Note: Transfers are inferred by matching sells and buys across AMCs. Thickness shows relative weight.</div>
184
  </div>
185
 
 
186
  <div id="info-box" style="margin-top:12px; padding:10px;
187
  border:1px solid #ddd; border-radius:8px; font-family:sans-serif;
188
  font-size:13px; background:#fbfbfb;">
 
191
 
192
  <script src="https://d3js.org/d3.v7.min.js"></script>
193
  <script>
194
+ const NODES = __NODES__;
195
+ const NODE_TYPE = __NODE_TYPE__;
196
+ const BUYS = __BUYS__;
197
+ const SELLS = __SELLS__;
198
+ const TRANSFERS = __TRANSFERS__;
199
+ const LOOPS = __LOOPS__;
200
 
201
  function draw() {
202
  const container = document.getElementById("arc-container");
203
  container.innerHTML = "";
204
+
205
  const w = Math.min(1200, container.clientWidth || 920);
206
  const h = Math.max(420, Math.floor(w * 0.62));
207
+
208
+ const svg = d3.select(container)
209
+ .append("svg")
210
  .attr("width", "100%")
211
  .attr("height", h)
212
  .attr("viewBox", [-w/2, -h/2, w, h].join(" "));
213
 
214
  const radius = Math.min(w, h) * 0.36;
215
 
216
+ // Compute node positions with abbrev
217
  const n = NODES.length;
218
+ function angleFor(i){ return (i / n) * 2 * Math.PI; }
219
+
220
+ const nodePos = NODES.map((name, i) => {
221
  const ang = angleFor(i) - Math.PI/2;
222
+ const ab = (name.length > 7 ? name.slice(0,5) + "…" : name);
223
  return {
224
  name: name,
225
  abbrev: ab,
 
230
  });
231
 
232
  const nameToIndex = {};
233
+ NODES.forEach((nm, i) => nameToIndex[nm] = i);
234
+
235
+ // --------------------------------------------------------------
236
+ // 1. Draw circles FIRST (bottom layer)
237
+ // --------------------------------------------------------------
238
+ const nodeCircleGroup = svg.append("g")
239
+ .attr("class", "node-circles")
240
+ .selectAll("g")
241
+ .data(nodePos)
242
+ .enter()
243
+ .append("g")
244
  .attr("transform", d => `translate(${d.x},${d.y})`);
245
 
246
+ nodeCircleGroup.append("circle")
247
  .attr("r", 16)
248
+ .attr("fill", d => NODE_TYPE[d.name] === "amc" ? "#2b6fa6" : "#f2c88d")
249
+ .attr("stroke", "#222")
250
+ .attr("stroke-width", 1)
251
  .style("cursor", "pointer");
252
 
253
+ // --------------------------------------------------------------
254
+ // ARC DRAWING (middle layers)
255
+ // --------------------------------------------------------------
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
256
 
257
+ function bezierPath(x0,y0,x1,y1,above=true){
258
+ const mx = (x0+x1)/2;
259
+ const my = (y0+y1)/2;
260
+ const len = Math.sqrt(mx*mx + my*my) || 1;
261
+ const ux = mx/len, uy = my/len;
 
 
262
  const offset = (above ? -1 : 1) * Math.max(30, radius*0.9);
263
+ return `M${x0},${y0} Q${mx + ux*offset},${my + uy*offset} ${x1},${y1}`;
 
 
264
  }
265
 
266
+ const allW = [].concat(
267
+ BUYS.map(x=>x[2]),
268
+ SELLS.map(x=>x[2]),
269
+ TRANSFERS.map(x=>x[2])
270
+ );
271
+ const stroke = d3.scaleLinear()
272
+ .domain([Math.min(...allW), Math.max(...allW)])
273
+ .range([1,6]);
274
 
275
+ const buyGroup = svg.append("g").attr("class", "buys");
 
276
  BUYS.forEach(b => {
277
  const a = b[0], c = b[1], wt = b[2];
278
  if (!(a in nameToIndex) || !(c in nameToIndex)) return;
 
280
  const t = nodePos[nameToIndex[c]];
281
  buyGroup.append("path")
282
  .attr("d", bezierPath(s.x,s.y,t.x,t.y,true))
283
+ .attr("fill", "none")
284
+ .attr("stroke", "#2e8540")
285
  .attr("stroke-width", stroke(wt))
286
  .attr("opacity", 0.92)
287
  .attr("data-src", a)
288
  .attr("data-tgt", c);
289
  });
290
 
291
+ const sellGroup = svg.append("g").attr("class", "sells");
292
  SELLS.forEach(s => {
293
  const c = s[0], a = s[1], wt = s[2];
294
  if (!(c in nameToIndex) || !(a in nameToIndex)) return;
 
296
  const tp = nodePos[nameToIndex[a]];
297
  sellGroup.append("path")
298
  .attr("d", bezierPath(sp.x,sp.y,tp.x,tp.y,false))
299
+ .attr("fill", "none")
300
+ .attr("stroke", "#c0392b")
301
  .attr("stroke-width", stroke(wt))
302
+ .attr("stroke-dasharray", "4,3")
303
+ .attr("opacity", 0.86)
304
  .attr("data-src", c)
305
  .attr("data-tgt", a);
306
  });
307
 
308
+ const transferGroup = svg.append("g").attr("class", "transfers");
309
  TRANSFERS.forEach(tr => {
310
  const sname = tr[0], tname = tr[1], wt = tr[2];
311
  if (!(sname in nameToIndex) || !(tname in nameToIndex)) return;
 
313
  const tp = nodePos[nameToIndex[tname]];
314
  const mx = (sp.x + tp.x)/2, my = (sp.y + tp.y)/2;
315
  transferGroup.append("path")
316
+ .attr("d", `M${sp.x},${sp.y} Q${mx*0.3},${my*0.3} ${tp.x},${tp.y}`)
317
+ .attr("fill", "none")
318
+ .attr("stroke", "#7d7d7d")
319
  .attr("stroke-width", stroke(wt))
320
+ .attr("opacity", 0.7)
321
  .attr("data-src", sname)
322
  .attr("data-tgt", tname);
323
  });
324
 
325
+ const loopGroup = svg.append("g").attr("class", "loops");
326
  LOOPS.forEach(lp => {
327
  const a = lp[0], c = lp[1], b = lp[2];
328
  if (!(a in nameToIndex) || !(b in nameToIndex)) return;
329
  const sa = nodePos[nameToIndex[a]];
330
  const sb = nodePos[nameToIndex[b]];
331
+ const mx = (sa.x+sb.x)/2, my = (sa.y+sb.y)/2;
332
+ const len = Math.sqrt(mx*mx+my*my)||1;
333
+ const offset = Math.max(40, radius*0.28 + len*0.12);
334
+ const ux = mx/len, uy = my/len;
 
 
 
335
  loopGroup.append("path")
336
+ .attr("d", `M${sa.x},${sa.y} Q${mx+ux*offset},${my+uy*offset} ${sb.x},${sb.y}`)
337
+ .attr("fill", "none")
338
+ .attr("stroke", "#227a6d")
339
  .attr("stroke-width", 2.8)
340
+ .attr("opacity", 0.95);
341
  });
342
 
343
+ // --------------------------------------------------------------
344
+ // 2. LABELS AT THE TOP (always visible)
345
+ // --------------------------------------------------------------
346
+ const labelGroup = svg.append("g")
347
+ .attr("class", "node-labels")
348
+ .selectAll("text")
349
+ .data(nodePos)
350
+ .enter()
351
+ .append("text")
352
+ .attr("x", d => Math.cos(d.angle) * (radius + 28))
353
+ .attr("y", d => Math.sin(d.angle) * (radius + 28))
354
+ .attr("dy", "0.35em")
355
+ .style("font-family", "sans-serif")
356
+ .style("font-size", "13px")
357
+ .style("cursor", "pointer")
358
+ .style("text-anchor", d => {
359
+ const deg = (d.angle * 180 / Math.PI);
360
+ return (deg>-90 && deg<90) ? "start" : "end";
361
+ })
362
+ .text(d => d.abbrev);
363
+
364
+ // --------------------------------------------------------------
365
+ // LABEL SWITCHING + OPACITY CONTROLS
366
+ // --------------------------------------------------------------
367
+ function getConnections(nodeName){
368
+ let buys = BUYS.filter(x=>x[0]===nodeName || x[1]===nodeName).map(x=>x[0]===nodeName?x[1]:x[0]);
369
+ let sells = SELLS.filter(x=>x[0]===nodeName || x[1]===nodeName).map(x=>x[0]===nodeName?x[1]:x[0]);
370
+ let transfers = TRANSFERS.filter(x=>x[0]===nodeName || x[1]===nodeName).map(x=>x[0]===nodeName?x[1]:x[0]);
371
+ let loops = LOOPS.filter(x=>x[0]===nodeName || x[2]===nodeName).map(x=>x[0]===nodeName?x[2]:x[0]);
372
+ return new Set([].concat(buys, sells, transfers, loops));
373
+ }
374
 
375
+ function updateLabels(selected, connected){
376
+ labelGroup.text(d => {
377
+ if (selected && (d.name===selected || connected.has(d.name)))
378
+ return d.name;
379
+ return d.abbrev;
380
+ });
381
+ }
382
+
383
+ function setOpacityFor(nodeName){
384
+ nodeCircleGroup.selectAll("circle")
385
+ .style("opacity", d => d.name===nodeName ? 1 : 0.18);
386
+
387
+ labelGroup.style("opacity", d => d.name===nodeName?1:0.28);
388
+
389
+ function isConn(p){
390
+ return p.getAttribute("data-src")===nodeName ||
391
+ p.getAttribute("data-tgt")===nodeName;
392
  }
393
 
394
  buyGroup.selectAll("path").style("opacity", function(){ return isConn(this)?0.98:0.06; });
 
396
  transferGroup.selectAll("path").style("opacity", function(){ return isConn(this)?0.98:0.06; });
397
  }
398
 
399
+ function resetOpacity(){
400
+ nodeCircleGroup.selectAll("circle").style("opacity",1);
401
+ labelGroup.style("opacity",1);
402
  buyGroup.selectAll("path").style("opacity",0.92);
403
  sellGroup.selectAll("path").style("opacity",0.86);
404
  transferGroup.selectAll("path").style("opacity",0.7);
405
  loopGroup.selectAll("path").style("opacity",0.95);
406
  updateLabels(null, new Set());
407
+ document.getElementById("info-box").innerHTML =
408
+ "<b>Click a node</b> to view details here.";
 
 
 
 
 
 
 
 
 
 
 
 
 
409
  }
410
 
411
+ function showInfo(name){
412
+ const con = [...getConnections(name)].sort();
413
+ document.getElementById("info-box").innerHTML =
414
+ `<b>${name}</b><br><br>
415
+ <b>Connected nodes:</b><br>
416
+ ${con.length?con.join(", "):"None"}`;
 
 
417
  }
418
 
419
+ function selectNode(d){
420
+ const name = d.name;
421
+ setOpacityFor(name);
422
+ showInfo(name);
423
+ const connected = getConnections(name);
424
+ updateLabels(name, connected);
 
 
 
 
 
425
  }
426
 
427
+ nodeCircleGroup.on("click", function(e,d){
 
 
 
 
 
 
 
 
 
428
  selectNode(d);
429
+ e.stopPropagation();
430
  });
431
+ labelGroup.on("click", function(e,d){
432
  selectNode(d);
433
+ e.stopPropagation();
434
  });
435
 
436
  document.getElementById("arc-reset").onclick = resetOpacity;
437
+ svg.on("click", function(evt){ if(evt.target.tagName==="svg") resetOpacity(); });
438
+
 
439
  }
440
 
441
  draw();
 
443
  </script>
444
  """
445
 
446
+
447
  def make_arc_html(nodes, node_type, buys, sells, transfers, loops):
448
  # prepare JSON strings
449
  nodes_json = json.dumps(nodes)