Spaces:
Sleeping
Sleeping
Update app.py
Browse files
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 |
-
|
|
|
|
|
|
|
| 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 |
-
//
|
| 215 |
const n = NODES.length;
|
| 216 |
-
function angleFor(i)
|
| 217 |
-
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 233 |
.attr("transform", d => `translate(${d.x},${d.y})`);
|
| 234 |
|
| 235 |
-
|
| 236 |
.attr("r", 16)
|
| 237 |
-
.
|
| 238 |
-
.
|
| 239 |
-
.
|
| 240 |
.style("cursor", "pointer");
|
| 241 |
|
| 242 |
-
//
|
| 243 |
-
|
| 244 |
-
|
| 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 |
-
|
| 262 |
-
|
| 263 |
-
const
|
| 264 |
-
const
|
| 265 |
-
const
|
| 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 |
-
|
| 270 |
-
const cy = my + uy * offset;
|
| 271 |
-
return `M ${x0} ${y0} Q ${cx} ${cy} ${x1} ${y1}`;
|
| 272 |
}
|
| 273 |
|
| 274 |
-
const allW = [].concat(
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 278 |
|
| 279 |
-
|
| 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
|
| 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
|
| 337 |
-
const
|
| 338 |
-
const
|
| 339 |
-
const
|
| 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
|
| 345 |
-
.attr("fill","none")
|
| 346 |
-
.attr("stroke","#227a6d")
|
| 347 |
.attr("stroke-width", 2.8)
|
| 348 |
-
.attr("opacity",0.95);
|
| 349 |
});
|
| 350 |
|
| 351 |
-
//
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 355 |
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 369 |
-
|
| 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 =
|
| 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 |
-
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
|
| 396 |
-
|
| 397 |
-
|
| 398 |
-
return d.abbrev; // otherwise short
|
| 399 |
-
});
|
| 400 |
}
|
| 401 |
|
| 402 |
-
|
| 403 |
-
|
| 404 |
-
|
| 405 |
-
|
| 406 |
-
|
| 407 |
-
|
| 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 |
-
|
| 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 |
-
|
| 427 |
});
|
| 428 |
-
|
| 429 |
selectNode(d);
|
| 430 |
-
|
| 431 |
});
|
| 432 |
|
| 433 |
document.getElementById("arc-reset").onclick = resetOpacity;
|
| 434 |
-
svg.on("click", function(
|
| 435 |
-
|
| 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)
|