Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -225,7 +225,6 @@ function draw() {
|
|
| 225 |
|
| 226 |
const radius = Math.min(w, h) * 0.36;
|
| 227 |
|
| 228 |
-
// Compute node positions with abbrev
|
| 229 |
const n = NODES.length;
|
| 230 |
function angleFor(i){ return (i / n) * 2 * Math.PI; }
|
| 231 |
|
|
@@ -244,27 +243,32 @@ function draw() {
|
|
| 244 |
const nameToIndex = {};
|
| 245 |
NODES.forEach((nm, i) => nameToIndex[nm] = i);
|
| 246 |
|
| 247 |
-
|
| 248 |
-
// 1
|
| 249 |
-
|
| 250 |
const nodeCircleGroup = svg.append("g")
|
| 251 |
.attr("class", "node-circles")
|
| 252 |
.selectAll("g")
|
| 253 |
.data(nodePos)
|
| 254 |
.enter()
|
| 255 |
.append("g")
|
| 256 |
-
.attr("transform", d => `translate(${d.x},${d.y})`)
|
|
|
|
| 257 |
|
|
|
|
|
|
|
|
|
|
| 258 |
nodeCircleGroup.append("circle")
|
| 259 |
.attr("r", 16)
|
| 260 |
.attr("fill", d => NODE_TYPE[d.name] === "amc" ? "#2b6fa6" : "#f2c88d")
|
| 261 |
.attr("stroke", "#222")
|
| 262 |
.attr("stroke-width", 1)
|
| 263 |
-
.style("cursor", "pointer")
|
|
|
|
| 264 |
|
| 265 |
-
|
| 266 |
-
// ARC
|
| 267 |
-
|
| 268 |
function bezierPath(x0,y0,x1,y1,above=true){
|
| 269 |
const mx = (x0+x1)/2;
|
| 270 |
const my = (y0+y1)/2;
|
|
@@ -333,8 +337,6 @@ function draw() {
|
|
| 333 |
.attr("data-tgt", tname);
|
| 334 |
});
|
| 335 |
|
| 336 |
-
|
| 337 |
-
// original loop arcs (keep logic but style to violet and slightly thicker)
|
| 338 |
const loopGroup = svg.append("g").attr("class", "loops");
|
| 339 |
LOOPS.forEach(lp => {
|
| 340 |
const a = lp[0], c = lp[1], b = lp[2];
|
|
@@ -352,16 +354,13 @@ function draw() {
|
|
| 352 |
loopGroup.append("path")
|
| 353 |
.attr("d", path)
|
| 354 |
.attr("fill", "none")
|
| 355 |
-
.attr("stroke", "#9b59b6")
|
| 356 |
.attr("stroke-width", 4.2)
|
| 357 |
.attr("opacity", 1)
|
| 358 |
.attr("stroke-linecap", "round")
|
| 359 |
.attr("stroke-linejoin", "round");
|
| 360 |
});
|
| 361 |
|
| 362 |
-
// --------------------------------------------------------------
|
| 363 |
-
// Overlay group for dynamic highlighted loop paths (drawn on click)
|
| 364 |
-
// --------------------------------------------------------------
|
| 365 |
const loopPathGroup = svg.append("g").attr("class", "loop-path-highlight");
|
| 366 |
|
| 367 |
function drawLoopPathsFor(nodeName) {
|
|
@@ -373,11 +372,10 @@ function draw() {
|
|
| 373 |
const pA = nodePos[nameToIndex[amcA]];
|
| 374 |
const pC = nodePos[nameToIndex[company]];
|
| 375 |
const pB = nodePos[nameToIndex[amcB]];
|
| 376 |
-
// draw two-segment polyline that follows straight segments A->C and C->B
|
| 377 |
loopPathGroup.append("path")
|
| 378 |
.attr("d", `M${pA.x},${pA.y} L${pC.x},${pC.y} L${pB.x},${pB.y}`)
|
| 379 |
.attr("fill", "none")
|
| 380 |
-
.attr("stroke", "#8e44ad")
|
| 381 |
.attr("stroke-width", 4.5)
|
| 382 |
.attr("opacity", 0.98)
|
| 383 |
.attr("stroke-linecap", "round")
|
|
@@ -385,9 +383,9 @@ function draw() {
|
|
| 385 |
});
|
| 386 |
}
|
| 387 |
|
| 388 |
-
|
| 389 |
-
//
|
| 390 |
-
|
| 391 |
const labelGroup = svg.append("g")
|
| 392 |
.attr("class", "node-labels")
|
| 393 |
.selectAll("text")
|
|
@@ -404,11 +402,12 @@ function draw() {
|
|
| 404 |
const deg = (d.angle * 180 / Math.PI);
|
| 405 |
return (deg>-90 && deg<90) ? "start" : "end";
|
| 406 |
})
|
|
|
|
| 407 |
.text(d => d.abbrev);
|
| 408 |
|
| 409 |
-
|
| 410 |
-
//
|
| 411 |
-
|
| 412 |
function getConnections(nodeName){
|
| 413 |
let buys = BUYS.filter(x=>x[0]===nodeName || x[1]===nodeName).map(x=>x[0]===nodeName?x[1]:x[0]);
|
| 414 |
let sells = SELLS.filter(x=>x[0]===nodeName || x[1]===nodeName).map(x=>x[0]===nodeName?x[1]:x[0]);
|
|
@@ -443,16 +442,7 @@ function draw() {
|
|
| 443 |
sellGroup.selectAll("path").style("opacity", function(){ return isConn(this)?0.98:0.06; });
|
| 444 |
transferGroup.selectAll("path").style("opacity", function(){ return isConn(this)?0.98:0.06; });
|
| 445 |
|
| 446 |
-
|
| 447 |
-
loopGroup.selectAll("path").style("opacity", function(){
|
| 448 |
-
// loops don't have data-src/data-tgt; approximate by checking endpoints
|
| 449 |
-
try {
|
| 450 |
-
const dstr = this.getAttribute("d") || "";
|
| 451 |
-
// simple heuristic: if path contains nodeName coordinate string, highlight it
|
| 452 |
-
// (we'll just keep them at 0.9 for readability)
|
| 453 |
-
return 0.9;
|
| 454 |
-
} catch(e){ return 0.9; }
|
| 455 |
-
});
|
| 456 |
}
|
| 457 |
|
| 458 |
function resetOpacity(){
|
|
@@ -463,7 +453,7 @@ function draw() {
|
|
| 463 |
transferGroup.selectAll("path").style("opacity",0.7);
|
| 464 |
loopGroup.selectAll("path").style("opacity",1);
|
| 465 |
|
| 466 |
-
loopPathGroup.selectAll("*").remove();
|
| 467 |
updateLabels(null, new Set());
|
| 468 |
document.getElementById("info-box").innerHTML =
|
| 469 |
"<b>Click a node</b> to view details here.";
|
|
@@ -497,35 +487,44 @@ function draw() {
|
|
| 497 |
showInfo(name);
|
| 498 |
const connected = getConnections(name);
|
| 499 |
updateLabels(name, connected);
|
| 500 |
-
drawLoopPathsFor(name);
|
| 501 |
}
|
| 502 |
|
|
|
|
|
|
|
|
|
|
| 503 |
nodeCircleGroup.on("click", function(event, d){
|
| 504 |
selectNode(d);
|
| 505 |
event.stopPropagation();
|
| 506 |
});
|
|
|
|
| 507 |
labelGroup.on("click", function(event, d){
|
| 508 |
selectNode(d);
|
| 509 |
event.stopPropagation();
|
| 510 |
});
|
| 511 |
|
| 512 |
-
|
| 513 |
document.getElementById("arc-reset").onclick = resetOpacity;
|
| 514 |
-
svg.on("click", function(evt){
|
| 515 |
-
// Only reset when clicking empty background, not circles, not labels, not paths
|
| 516 |
-
if (evt.target === this) {
|
| 517 |
-
resetOpacity();
|
| 518 |
-
}
|
| 519 |
-
});
|
| 520 |
|
|
|
|
|
|
|
|
|
|
| 521 |
|
| 522 |
}
|
| 523 |
|
| 524 |
draw();
|
| 525 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 526 |
</script>
|
| 527 |
"""
|
| 528 |
|
|
|
|
| 529 |
def make_arc_html(nodes, node_type, buys, sells, transfers, loops):
|
| 530 |
nodes_json = json.dumps(nodes)
|
| 531 |
node_type_json = json.dumps(node_type)
|
|
|
|
| 225 |
|
| 226 |
const radius = Math.min(w, h) * 0.36;
|
| 227 |
|
|
|
|
| 228 |
const n = NODES.length;
|
| 229 |
function angleFor(i){ return (i / n) * 2 * Math.PI; }
|
| 230 |
|
|
|
|
| 243 |
const nameToIndex = {};
|
| 244 |
NODES.forEach((nm, i) => nameToIndex[nm] = i);
|
| 245 |
|
| 246 |
+
//--------------------------------------------------------------------
|
| 247 |
+
// NODES — FIX #1: disable pointer events on <g>
|
| 248 |
+
//--------------------------------------------------------------------
|
| 249 |
const nodeCircleGroup = svg.append("g")
|
| 250 |
.attr("class", "node-circles")
|
| 251 |
.selectAll("g")
|
| 252 |
.data(nodePos)
|
| 253 |
.enter()
|
| 254 |
.append("g")
|
| 255 |
+
.attr("transform", d => `translate(${d.x},${d.y})`)
|
| 256 |
+
.style("pointer-events", "none"); // ★ FIX ADDED
|
| 257 |
|
| 258 |
+
//--------------------------------------------------------------------
|
| 259 |
+
// CIRCLES — FIX #2: reenable pointer events on <circle>
|
| 260 |
+
//--------------------------------------------------------------------
|
| 261 |
nodeCircleGroup.append("circle")
|
| 262 |
.attr("r", 16)
|
| 263 |
.attr("fill", d => NODE_TYPE[d.name] === "amc" ? "#2b6fa6" : "#f2c88d")
|
| 264 |
.attr("stroke", "#222")
|
| 265 |
.attr("stroke-width", 1)
|
| 266 |
+
.style("cursor", "pointer")
|
| 267 |
+
.style("pointer-events", "all"); // ★ FIX ADDED
|
| 268 |
|
| 269 |
+
//--------------------------------------------------------------------
|
| 270 |
+
// ARC drawing (unchanged)
|
| 271 |
+
//--------------------------------------------------------------------
|
| 272 |
function bezierPath(x0,y0,x1,y1,above=true){
|
| 273 |
const mx = (x0+x1)/2;
|
| 274 |
const my = (y0+y1)/2;
|
|
|
|
| 337 |
.attr("data-tgt", tname);
|
| 338 |
});
|
| 339 |
|
|
|
|
|
|
|
| 340 |
const loopGroup = svg.append("g").attr("class", "loops");
|
| 341 |
LOOPS.forEach(lp => {
|
| 342 |
const a = lp[0], c = lp[1], b = lp[2];
|
|
|
|
| 354 |
loopGroup.append("path")
|
| 355 |
.attr("d", path)
|
| 356 |
.attr("fill", "none")
|
| 357 |
+
.attr("stroke", "#9b59b6")
|
| 358 |
.attr("stroke-width", 4.2)
|
| 359 |
.attr("opacity", 1)
|
| 360 |
.attr("stroke-linecap", "round")
|
| 361 |
.attr("stroke-linejoin", "round");
|
| 362 |
});
|
| 363 |
|
|
|
|
|
|
|
|
|
|
| 364 |
const loopPathGroup = svg.append("g").attr("class", "loop-path-highlight");
|
| 365 |
|
| 366 |
function drawLoopPathsFor(nodeName) {
|
|
|
|
| 372 |
const pA = nodePos[nameToIndex[amcA]];
|
| 373 |
const pC = nodePos[nameToIndex[company]];
|
| 374 |
const pB = nodePos[nameToIndex[amcB]];
|
|
|
|
| 375 |
loopPathGroup.append("path")
|
| 376 |
.attr("d", `M${pA.x},${pA.y} L${pC.x},${pC.y} L${pB.x},${pB.y}`)
|
| 377 |
.attr("fill", "none")
|
| 378 |
+
.attr("stroke", "#8e44ad")
|
| 379 |
.attr("stroke-width", 4.5)
|
| 380 |
.attr("opacity", 0.98)
|
| 381 |
.attr("stroke-linecap", "round")
|
|
|
|
| 383 |
});
|
| 384 |
}
|
| 385 |
|
| 386 |
+
//--------------------------------------------------------------------
|
| 387 |
+
// LABELS — FIX #3: enable pointer events
|
| 388 |
+
//--------------------------------------------------------------------
|
| 389 |
const labelGroup = svg.append("g")
|
| 390 |
.attr("class", "node-labels")
|
| 391 |
.selectAll("text")
|
|
|
|
| 402 |
const deg = (d.angle * 180 / Math.PI);
|
| 403 |
return (deg>-90 && deg<90) ? "start" : "end";
|
| 404 |
})
|
| 405 |
+
.style("pointer-events", "all") // ★ FIX ADDED
|
| 406 |
.text(d => d.abbrev);
|
| 407 |
|
| 408 |
+
//--------------------------------------------------------------------
|
| 409 |
+
// CLICK / HIGHLIGHT logic
|
| 410 |
+
//--------------------------------------------------------------------
|
| 411 |
function getConnections(nodeName){
|
| 412 |
let buys = BUYS.filter(x=>x[0]===nodeName || x[1]===nodeName).map(x=>x[0]===nodeName?x[1]:x[0]);
|
| 413 |
let sells = SELLS.filter(x=>x[0]===nodeName || x[1]===nodeName).map(x=>x[0]===nodeName?x[1]:x[0]);
|
|
|
|
| 442 |
sellGroup.selectAll("path").style("opacity", function(){ return isConn(this)?0.98:0.06; });
|
| 443 |
transferGroup.selectAll("path").style("opacity", function(){ return isConn(this)?0.98:0.06; });
|
| 444 |
|
| 445 |
+
loopGroup.selectAll("path").style("opacity", 0.9);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 446 |
}
|
| 447 |
|
| 448 |
function resetOpacity(){
|
|
|
|
| 453 |
transferGroup.selectAll("path").style("opacity",0.7);
|
| 454 |
loopGroup.selectAll("path").style("opacity",1);
|
| 455 |
|
| 456 |
+
loopPathGroup.selectAll("*").remove();
|
| 457 |
updateLabels(null, new Set());
|
| 458 |
document.getElementById("info-box").innerHTML =
|
| 459 |
"<b>Click a node</b> to view details here.";
|
|
|
|
| 487 |
showInfo(name);
|
| 488 |
const connected = getConnections(name);
|
| 489 |
updateLabels(name, connected);
|
| 490 |
+
drawLoopPathsFor(name);
|
| 491 |
}
|
| 492 |
|
| 493 |
+
//--------------------------------------------------------------------
|
| 494 |
+
// CLICK HANDLERS — working now
|
| 495 |
+
//--------------------------------------------------------------------
|
| 496 |
nodeCircleGroup.on("click", function(event, d){
|
| 497 |
selectNode(d);
|
| 498 |
event.stopPropagation();
|
| 499 |
});
|
| 500 |
+
|
| 501 |
labelGroup.on("click", function(event, d){
|
| 502 |
selectNode(d);
|
| 503 |
event.stopPropagation();
|
| 504 |
});
|
| 505 |
|
|
|
|
| 506 |
document.getElementById("arc-reset").onclick = resetOpacity;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 507 |
|
| 508 |
+
svg.on("click", function(event){
|
| 509 |
+
if (event.target === this) resetOpacity();
|
| 510 |
+
});
|
| 511 |
|
| 512 |
}
|
| 513 |
|
| 514 |
draw();
|
| 515 |
+
|
| 516 |
+
//-----------------------------------------------------------
|
| 517 |
+
// RESIZE DEBOUNCE (ANTI-DOUBLE-CLICK ISSUE)
|
| 518 |
+
//-----------------------------------------------------------
|
| 519 |
+
let rz;
|
| 520 |
+
window.addEventListener("resize", () => {
|
| 521 |
+
clearTimeout(rz);
|
| 522 |
+
rz = setTimeout(draw, 120);
|
| 523 |
+
});
|
| 524 |
</script>
|
| 525 |
"""
|
| 526 |
|
| 527 |
+
|
| 528 |
def make_arc_html(nodes, node_type, buys, sells, transfers, loops):
|
| 529 |
nodes_json = json.dumps(nodes)
|
| 530 |
node_type_json = json.dumps(node_type)
|