Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -85,7 +85,7 @@ def infer_amc_transfers(buy_map, sell_map):
|
|
| 85 |
TRANSFER_COUNTS = infer_amc_transfers(BUY_MAP, SELL_MAP)
|
| 86 |
|
| 87 |
# ---------------------------
|
| 88 |
-
# Mixed ordering
|
| 89 |
# ---------------------------
|
| 90 |
def build_mixed_ordering(amcs, companies):
|
| 91 |
mixed = []
|
|
@@ -109,46 +109,55 @@ def build_flows():
|
|
| 109 |
for c in comps:
|
| 110 |
w = 3 if (amc in FRESH_BUY and c in FRESH_BUY.get(amc, [])) else 1
|
| 111 |
buys.append((amc, c, w))
|
|
|
|
| 112 |
sells = []
|
| 113 |
for amc, comps in SELL_MAP.items():
|
| 114 |
for c in comps:
|
| 115 |
w = 3 if (amc in COMPLETE_EXIT and c in COMPLETE_EXIT.get(amc, [])) else 1
|
| 116 |
sells.append((c, amc, w))
|
|
|
|
| 117 |
transfers = []
|
| 118 |
for (s,b), w in TRANSFER_COUNTS.items():
|
| 119 |
transfers.append((s, b, w))
|
|
|
|
| 120 |
loops = []
|
| 121 |
-
# loops: a -> c -> b (buy from a into c, sell from b of c)
|
| 122 |
for a,c,w1 in buys:
|
| 123 |
for c2,b,w2 in sells:
|
| 124 |
if c == c2:
|
| 125 |
loops.append((a, c, b))
|
| 126 |
-
|
| 127 |
loops = list({(a,c,b) for (a,c,b) in loops})
|
| 128 |
return buys, sells, transfers, loops
|
| 129 |
|
| 130 |
BUYS, SELLS, TRANSFERS, LOOPS = build_flows()
|
| 131 |
|
| 132 |
# ---------------------------
|
| 133 |
-
# Inspector summaries
|
| 134 |
# ---------------------------
|
| 135 |
def company_trade_summary(company):
|
| 136 |
buyers = [a for a, cs in BUY_MAP.items() if company in cs]
|
| 137 |
sellers = [a for a, cs in SELL_MAP.items() if company in cs]
|
| 138 |
fresh = [a for a, cs in FRESH_BUY.items() if company in cs]
|
| 139 |
exits = [a for a, cs in COMPLETE_EXIT.items() if company in cs]
|
|
|
|
| 140 |
df = pd.DataFrame({
|
| 141 |
-
"Role":
|
| 142 |
-
|
|
|
|
|
|
|
|
|
|
| 143 |
"AMC": buyers + sellers + fresh + exits
|
| 144 |
})
|
|
|
|
| 145 |
if df.empty:
|
| 146 |
return go.Figure(), pd.DataFrame([], columns=["Role", "AMC"])
|
|
|
|
| 147 |
counts = df.groupby("Role").size().reset_index(name="Count")
|
| 148 |
-
fig = go.Figure(data=[go.Bar(x=counts["Role"]
|
| 149 |
fig.update_layout(title=f"Trades for {company}", margin=dict(l=20,r=20,t=40,b=20))
|
| 150 |
return fig, df
|
| 151 |
|
|
|
|
| 152 |
def amc_transfer_summary(amc):
|
| 153 |
sold = SELL_MAP.get(amc, [])
|
| 154 |
transfers = []
|
|
@@ -156,18 +165,21 @@ def amc_transfer_summary(amc):
|
|
| 156 |
buyers = [a for a, cs in BUY_MAP.items() if s in cs]
|
| 157 |
for b in buyers:
|
| 158 |
transfers.append({"security": s, "buyer_amc": b})
|
|
|
|
| 159 |
df = pd.DataFrame(transfers)
|
| 160 |
if df.empty:
|
| 161 |
return go.Figure(), pd.DataFrame([], columns=["security", "buyer_amc"])
|
|
|
|
| 162 |
counts = df["buyer_amc"].value_counts().reset_index()
|
| 163 |
counts.columns = ["Buyer AMC", "Count"]
|
| 164 |
-
fig = go.Figure(data=[go.Bar(x=counts["Buyer AMC"]
|
| 165 |
fig.update_layout(title=f"Inferred transfers from {amc}", margin=dict(l=20,r=20,t=40,b=20))
|
| 166 |
return fig, df
|
| 167 |
|
| 168 |
-
|
| 169 |
-
#
|
| 170 |
-
#
|
|
|
|
| 171 |
JS_TEMPLATE = """
|
| 172 |
<div id="arc-container" style="width:100%; height:720px;"></div>
|
| 173 |
<div style="margin-top:8px;">
|
|
@@ -179,12 +191,12 @@ JS_TEMPLATE = """
|
|
| 179 |
<span style="display:inline-block;width:12px;height:8px;background:#2e8540;margin-right:6px;"></span> BUY (green solid)<br/>
|
| 180 |
<span style="display:inline-block;width:12px;height:8px;background:#c0392b;margin-right:6px;border-bottom:3px dotted #c0392b;"></span> SELL (red dotted)<br/>
|
| 181 |
<span style="display:inline-block;width:12px;height:8px;background:#7d7d7d;margin-right:6px;"></span> TRANSFER (grey, inferred)<br/>
|
| 182 |
-
<span style="display:inline-block;width:12px;height:8px;background:#
|
| 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;">
|
| 189 |
<b>Click a node</b> to view details here.
|
| 190 |
</div>
|
|
@@ -253,7 +265,6 @@ function draw() {
|
|
| 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;
|
|
@@ -322,6 +333,8 @@ function draw() {
|
|
| 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];
|
|
@@ -329,19 +342,49 @@ function draw() {
|
|
| 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(
|
| 333 |
-
const
|
| 334 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
| 335 |
loopGroup.append("path")
|
| 336 |
.attr("d", path)
|
| 337 |
.attr("fill", "none")
|
| 338 |
-
.attr("stroke", "#9b59b6")
|
| 339 |
-
.attr("stroke-width", 4.
|
| 340 |
.attr("opacity", 1)
|
| 341 |
-
.attr("stroke-linecap","round")
|
| 342 |
-
.attr("stroke-linejoin","round");
|
| 343 |
});
|
| 344 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 345 |
// --------------------------------------------------------------
|
| 346 |
// 2. LABELS AT THE TOP (always visible)
|
| 347 |
// --------------------------------------------------------------
|
|
@@ -382,34 +425,35 @@ function draw() {
|
|
| 382 |
});
|
| 383 |
}
|
| 384 |
|
| 385 |
-
function setOpacityFor(nodeName)
|
| 386 |
const connected = getConnections(nodeName);
|
| 387 |
-
|
| 388 |
-
// highlight circles
|
| 389 |
nodeCircleGroup.selectAll("circle")
|
| 390 |
-
.style("opacity", d =>
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
|
| 396 |
-
|
| 397 |
-
|
| 398 |
-
|
| 399 |
-
// highlight arcs
|
| 400 |
-
function isConn(path){
|
| 401 |
-
const src = path.getAttribute("data-src");
|
| 402 |
-
const tgt = path.getAttribute("data-tgt");
|
| 403 |
-
return src === nodeName || tgt === nodeName ||
|
| 404 |
-
connected.has(src) || connected.has(tgt);
|
| 405 |
}
|
| 406 |
-
|
| 407 |
buyGroup.selectAll("path").style("opacity", function(){ return isConn(this)?0.98:0.06; });
|
| 408 |
sellGroup.selectAll("path").style("opacity", function(){ return isConn(this)?0.98:0.06; });
|
| 409 |
transferGroup.selectAll("path").style("opacity", function(){ return isConn(this)?0.98:0.06; });
|
| 410 |
-
loopGroup.selectAll("path").style("opacity",0.95); // loops unchanged
|
| 411 |
-
}
|
| 412 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 413 |
|
| 414 |
function resetOpacity(){
|
| 415 |
nodeCircleGroup.selectAll("circle").style("opacity",1);
|
|
@@ -417,27 +461,25 @@ function draw() {
|
|
| 417 |
buyGroup.selectAll("path").style("opacity",0.92);
|
| 418 |
sellGroup.selectAll("path").style("opacity",0.86);
|
| 419 |
transferGroup.selectAll("path").style("opacity",0.7);
|
| 420 |
-
loopGroup.selectAll("path").style("opacity",
|
|
|
|
|
|
|
| 421 |
updateLabels(null, new Set());
|
| 422 |
document.getElementById("info-box").innerHTML =
|
| 423 |
"<b>Click a node</b> to view details here.";
|
| 424 |
}
|
| 425 |
|
| 426 |
-
function showInfo(nodeName)
|
| 427 |
const buys = BUYS.filter(x => x[0] === nodeName || x[1] === nodeName)
|
| 428 |
.map(x => x[0] === nodeName ? x[1] : x[0]);
|
| 429 |
-
|
| 430 |
const sells = SELLS.filter(x => x[0] === nodeName || x[1] === nodeName)
|
| 431 |
.map(x => x[0] === nodeName ? x[1] : x[0]);
|
| 432 |
-
|
| 433 |
const transfers = TRANSFERS.filter(x => x[0] === nodeName || x[1] === nodeName)
|
| 434 |
.map(x => x[0] === nodeName ? x[1] : x[0]);
|
| 435 |
-
|
| 436 |
const loops = LOOPS.filter(x => x[0] === nodeName || x[2] === nodeName)
|
| 437 |
.map(x => x[0] === nodeName ? x[2] : x[0]);
|
| 438 |
-
|
| 439 |
const box = document.getElementById("info-box");
|
| 440 |
-
|
| 441 |
box.innerHTML = `
|
| 442 |
<div style="font-size:14px;"><b>${nodeName}</b></div>
|
| 443 |
<div style="margin-top:8px; font-size:13px;">
|
|
@@ -449,26 +491,26 @@ function draw() {
|
|
| 449 |
`;
|
| 450 |
}
|
| 451 |
|
| 452 |
-
|
| 453 |
function selectNode(d){
|
| 454 |
const name = d.name;
|
| 455 |
setOpacityFor(name);
|
| 456 |
showInfo(name);
|
| 457 |
const connected = getConnections(name);
|
| 458 |
updateLabels(name, connected);
|
|
|
|
| 459 |
}
|
| 460 |
|
| 461 |
nodeCircleGroup.on("click", function(e,d){
|
| 462 |
selectNode(d);
|
| 463 |
-
e.stopPropagation();
|
| 464 |
});
|
| 465 |
labelGroup.on("click", function(e,d){
|
| 466 |
selectNode(d);
|
| 467 |
-
e.stopPropagation();
|
| 468 |
});
|
| 469 |
|
| 470 |
document.getElementById("arc-reset").onclick = resetOpacity;
|
| 471 |
-
svg.on("click", function(evt){ if(evt.target.tagName==="svg") resetOpacity(); });
|
| 472 |
|
| 473 |
}
|
| 474 |
|
|
@@ -477,9 +519,7 @@ window.addEventListener("resize", draw);
|
|
| 477 |
</script>
|
| 478 |
"""
|
| 479 |
|
| 480 |
-
|
| 481 |
def make_arc_html(nodes, node_type, buys, sells, transfers, loops):
|
| 482 |
-
# prepare JSON strings
|
| 483 |
nodes_json = json.dumps(nodes)
|
| 484 |
node_type_json = json.dumps(node_type)
|
| 485 |
buys_json = json.dumps(buys)
|
|
@@ -496,12 +536,10 @@ def make_arc_html(nodes, node_type, buys, sells, transfers, loops):
|
|
| 496 |
|
| 497 |
initial_html = make_arc_html(NODES, NODE_TYPE, BUYS, SELLS, TRANSFERS, LOOPS)
|
| 498 |
|
| 499 |
-
#
|
| 500 |
-
# Gradio UI
|
| 501 |
-
# ---------------------------
|
| 502 |
-
# ---------------------------
|
| 503 |
# Gradio UI
|
| 504 |
-
#
|
|
|
|
| 505 |
responsive_css = """
|
| 506 |
#arc-container { padding:0; margin:0; }
|
| 507 |
svg { font-family: sans-serif; }
|
|
@@ -512,11 +550,13 @@ with gr.Blocks(css=responsive_css, title="MF Churn — Semi-layer Arc Diagram (L
|
|
| 512 |
gr.HTML(initial_html)
|
| 513 |
|
| 514 |
gr.Markdown("### Inspect Company / AMC")
|
| 515 |
-
|
|
|
|
| 516 |
select_company = gr.Dropdown(choices=COMPANIES, label="Select company")
|
| 517 |
company_plot = gr.Plot()
|
| 518 |
company_table = gr.DataFrame()
|
| 519 |
-
|
|
|
|
| 520 |
select_amc = gr.Dropdown(choices=AMCS, label="Select AMC")
|
| 521 |
amc_plot = gr.Plot()
|
| 522 |
amc_table = gr.DataFrame()
|
|
|
|
| 85 |
TRANSFER_COUNTS = infer_amc_transfers(BUY_MAP, SELL_MAP)
|
| 86 |
|
| 87 |
# ---------------------------
|
| 88 |
+
# Mixed ordering
|
| 89 |
# ---------------------------
|
| 90 |
def build_mixed_ordering(amcs, companies):
|
| 91 |
mixed = []
|
|
|
|
| 109 |
for c in comps:
|
| 110 |
w = 3 if (amc in FRESH_BUY and c in FRESH_BUY.get(amc, [])) else 1
|
| 111 |
buys.append((amc, c, w))
|
| 112 |
+
|
| 113 |
sells = []
|
| 114 |
for amc, comps in SELL_MAP.items():
|
| 115 |
for c in comps:
|
| 116 |
w = 3 if (amc in COMPLETE_EXIT and c in COMPLETE_EXIT.get(amc, [])) else 1
|
| 117 |
sells.append((c, amc, w))
|
| 118 |
+
|
| 119 |
transfers = []
|
| 120 |
for (s,b), w in TRANSFER_COUNTS.items():
|
| 121 |
transfers.append((s, b, w))
|
| 122 |
+
|
| 123 |
loops = []
|
|
|
|
| 124 |
for a,c,w1 in buys:
|
| 125 |
for c2,b,w2 in sells:
|
| 126 |
if c == c2:
|
| 127 |
loops.append((a, c, b))
|
| 128 |
+
|
| 129 |
loops = list({(a,c,b) for (a,c,b) in loops})
|
| 130 |
return buys, sells, transfers, loops
|
| 131 |
|
| 132 |
BUYS, SELLS, TRANSFERS, LOOPS = build_flows()
|
| 133 |
|
| 134 |
# ---------------------------
|
| 135 |
+
# Inspector summaries
|
| 136 |
# ---------------------------
|
| 137 |
def company_trade_summary(company):
|
| 138 |
buyers = [a for a, cs in BUY_MAP.items() if company in cs]
|
| 139 |
sellers = [a for a, cs in SELL_MAP.items() if company in cs]
|
| 140 |
fresh = [a for a, cs in FRESH_BUY.items() if company in cs]
|
| 141 |
exits = [a for a, cs in COMPLETE_EXIT.items() if company in cs]
|
| 142 |
+
|
| 143 |
df = pd.DataFrame({
|
| 144 |
+
"Role":
|
| 145 |
+
(["Buyer"] * len(buyers))
|
| 146 |
+
+ (["Seller"] * len(sellers))
|
| 147 |
+
+ (["Fresh buy"] * len(fresh))
|
| 148 |
+
+ (["Complete exit"] * len(exits)),
|
| 149 |
"AMC": buyers + sellers + fresh + exits
|
| 150 |
})
|
| 151 |
+
|
| 152 |
if df.empty:
|
| 153 |
return go.Figure(), pd.DataFrame([], columns=["Role", "AMC"])
|
| 154 |
+
|
| 155 |
counts = df.groupby("Role").size().reset_index(name="Count")
|
| 156 |
+
fig = go.Figure(data=[go.Bar(x=counts["Role"], y=counts["Count"])])
|
| 157 |
fig.update_layout(title=f"Trades for {company}", margin=dict(l=20,r=20,t=40,b=20))
|
| 158 |
return fig, df
|
| 159 |
|
| 160 |
+
|
| 161 |
def amc_transfer_summary(amc):
|
| 162 |
sold = SELL_MAP.get(amc, [])
|
| 163 |
transfers = []
|
|
|
|
| 165 |
buyers = [a for a, cs in BUY_MAP.items() if s in cs]
|
| 166 |
for b in buyers:
|
| 167 |
transfers.append({"security": s, "buyer_amc": b})
|
| 168 |
+
|
| 169 |
df = pd.DataFrame(transfers)
|
| 170 |
if df.empty:
|
| 171 |
return go.Figure(), pd.DataFrame([], columns=["security", "buyer_amc"])
|
| 172 |
+
|
| 173 |
counts = df["buyer_amc"].value_counts().reset_index()
|
| 174 |
counts.columns = ["Buyer AMC", "Count"]
|
| 175 |
+
fig = go.Figure(data=[go.Bar(x=counts["Buyer AMC"], y=counts["Count"])])
|
| 176 |
fig.update_layout(title=f"Inferred transfers from {amc}", margin=dict(l=20,r=20,t=40,b=20))
|
| 177 |
return fig, df
|
| 178 |
|
| 179 |
+
|
| 180 |
+
# ---------------------------------------------------------------------
|
| 181 |
+
# JS_TEMPLATE - single triple-quoted string (complete HTML + JS)
|
| 182 |
+
# ---------------------------------------------------------------------
|
| 183 |
JS_TEMPLATE = """
|
| 184 |
<div id="arc-container" style="width:100%; height:720px;"></div>
|
| 185 |
<div style="margin-top:8px;">
|
|
|
|
| 191 |
<span style="display:inline-block;width:12px;height:8px;background:#2e8540;margin-right:6px;"></span> BUY (green solid)<br/>
|
| 192 |
<span style="display:inline-block;width:12px;height:8px;background:#c0392b;margin-right:6px;border-bottom:3px dotted #c0392b;"></span> SELL (red dotted)<br/>
|
| 193 |
<span style="display:inline-block;width:12px;height:8px;background:#7d7d7d;margin-right:6px;"></span> TRANSFER (grey, inferred)<br/>
|
| 194 |
+
<span style="display:inline-block;width:12px;height:8px;background:#9b59b6;margin-right:6px;"></span> LOOP (violet external arc)<br/>
|
| 195 |
<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>
|
| 196 |
</div>
|
| 197 |
|
| 198 |
+
<div id="info-box" style="margin-top:12px; padding:10px;
|
| 199 |
+
border:1px solid #ddd; border-radius:8px; font-family:sans-serif;
|
| 200 |
font-size:13px; background:#fbfbfb;">
|
| 201 |
<b>Click a node</b> to view details here.
|
| 202 |
</div>
|
|
|
|
| 265 |
// --------------------------------------------------------------
|
| 266 |
// ARC DRAWING (middle layers)
|
| 267 |
// --------------------------------------------------------------
|
|
|
|
| 268 |
function bezierPath(x0,y0,x1,y1,above=true){
|
| 269 |
const mx = (x0+x1)/2;
|
| 270 |
const my = (y0+y1)/2;
|
|
|
|
| 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];
|
|
|
|
| 342 |
const sa = nodePos[nameToIndex[a]];
|
| 343 |
const sb = nodePos[nameToIndex[b]];
|
| 344 |
const mx = (sa.x+sb.x)/2, my = (sa.y+sb.y)/2;
|
| 345 |
+
const len = Math.sqrt((sa.x - sb.x)*(sa.x - sb.x) + (sa.y - sb.y)*(sa.y - sb.y));
|
| 346 |
+
const outward = Math.max(40, radius*0.28 + len * 0.12);
|
| 347 |
+
const nlen = Math.sqrt(mx*mx + my*my) || 1;
|
| 348 |
+
const ux = mx / nlen, uy = my / nlen;
|
| 349 |
+
const cx = mx + ux * outward;
|
| 350 |
+
const cy = my + uy * outward;
|
| 351 |
+
const path = `M ${sa.x} ${sa.y} Q ${cx} ${cy} ${sb.x} ${sb.y}`;
|
| 352 |
loopGroup.append("path")
|
| 353 |
.attr("d", path)
|
| 354 |
.attr("fill", "none")
|
| 355 |
+
.attr("stroke", "#9b59b6") // violet to stand out
|
| 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) {
|
| 368 |
+
loopPathGroup.selectAll("*").remove();
|
| 369 |
+
LOOPS.forEach(lp => {
|
| 370 |
+
const amcA = lp[0], company = lp[1], amcB = lp[2];
|
| 371 |
+
if (!(amcA in nameToIndex) || !(company in nameToIndex) || !(amcB in nameToIndex)) return;
|
| 372 |
+
if (nodeName !== amcA && nodeName !== company && nodeName !== amcB) return;
|
| 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") // slightly different violet for highlight
|
| 381 |
+
.attr("stroke-width", 4.5)
|
| 382 |
+
.attr("opacity", 0.98)
|
| 383 |
+
.attr("stroke-linecap", "round")
|
| 384 |
+
.attr("stroke-linejoin", "round");
|
| 385 |
+
});
|
| 386 |
+
}
|
| 387 |
+
|
| 388 |
// --------------------------------------------------------------
|
| 389 |
// 2. LABELS AT THE TOP (always visible)
|
| 390 |
// --------------------------------------------------------------
|
|
|
|
| 425 |
});
|
| 426 |
}
|
| 427 |
|
| 428 |
+
function setOpacityFor(nodeName){
|
| 429 |
const connected = getConnections(nodeName);
|
| 430 |
+
|
|
|
|
| 431 |
nodeCircleGroup.selectAll("circle")
|
| 432 |
+
.style("opacity", d => (d.name===nodeName || connected.has(d.name)) ? 1 : 0.18);
|
| 433 |
+
|
| 434 |
+
labelGroup.style("opacity", d => (d.name===nodeName || connected.has(d.name)) ? 1 : 0.28);
|
| 435 |
+
|
| 436 |
+
function isConn(p){
|
| 437 |
+
const src = p.getAttribute("data-src");
|
| 438 |
+
const tgt = p.getAttribute("data-tgt");
|
| 439 |
+
return src===nodeName || tgt===nodeName || connected.has(src) || connected.has(tgt);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 440 |
}
|
| 441 |
+
|
| 442 |
buyGroup.selectAll("path").style("opacity", function(){ return isConn(this)?0.98:0.06; });
|
| 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 |
+
// keep loopGroup visible but slightly dim non-relevant ones
|
| 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(){
|
| 459 |
nodeCircleGroup.selectAll("circle").style("opacity",1);
|
|
|
|
| 461 |
buyGroup.selectAll("path").style("opacity",0.92);
|
| 462 |
sellGroup.selectAll("path").style("opacity",0.86);
|
| 463 |
transferGroup.selectAll("path").style("opacity",0.7);
|
| 464 |
+
loopGroup.selectAll("path").style("opacity",1);
|
| 465 |
+
|
| 466 |
+
loopPathGroup.selectAll("*").remove(); // clear highlight overlay
|
| 467 |
updateLabels(null, new Set());
|
| 468 |
document.getElementById("info-box").innerHTML =
|
| 469 |
"<b>Click a node</b> to view details here.";
|
| 470 |
}
|
| 471 |
|
| 472 |
+
function showInfo(nodeName){
|
| 473 |
const buys = BUYS.filter(x => x[0] === nodeName || x[1] === nodeName)
|
| 474 |
.map(x => x[0] === nodeName ? x[1] : x[0]);
|
|
|
|
| 475 |
const sells = SELLS.filter(x => x[0] === nodeName || x[1] === nodeName)
|
| 476 |
.map(x => x[0] === nodeName ? x[1] : x[0]);
|
|
|
|
| 477 |
const transfers = TRANSFERS.filter(x => x[0] === nodeName || x[1] === nodeName)
|
| 478 |
.map(x => x[0] === nodeName ? x[1] : x[0]);
|
|
|
|
| 479 |
const loops = LOOPS.filter(x => x[0] === nodeName || x[2] === nodeName)
|
| 480 |
.map(x => x[0] === nodeName ? x[2] : x[0]);
|
| 481 |
+
|
| 482 |
const box = document.getElementById("info-box");
|
|
|
|
| 483 |
box.innerHTML = `
|
| 484 |
<div style="font-size:14px;"><b>${nodeName}</b></div>
|
| 485 |
<div style="margin-top:8px; font-size:13px;">
|
|
|
|
| 491 |
`;
|
| 492 |
}
|
| 493 |
|
|
|
|
| 494 |
function selectNode(d){
|
| 495 |
const name = d.name;
|
| 496 |
setOpacityFor(name);
|
| 497 |
showInfo(name);
|
| 498 |
const connected = getConnections(name);
|
| 499 |
updateLabels(name, connected);
|
| 500 |
+
drawLoopPathsFor(name); // draw overlay highlights for loops involving this node
|
| 501 |
}
|
| 502 |
|
| 503 |
nodeCircleGroup.on("click", function(e,d){
|
| 504 |
selectNode(d);
|
| 505 |
+
if (e && e.stopPropagation) e.stopPropagation();
|
| 506 |
});
|
| 507 |
labelGroup.on("click", function(e,d){
|
| 508 |
selectNode(d);
|
| 509 |
+
if (e && e.stopPropagation) e.stopPropagation();
|
| 510 |
});
|
| 511 |
|
| 512 |
document.getElementById("arc-reset").onclick = resetOpacity;
|
| 513 |
+
svg.on("click", function(evt){ if(evt.target.tagName === "svg") resetOpacity(); });
|
| 514 |
|
| 515 |
}
|
| 516 |
|
|
|
|
| 519 |
</script>
|
| 520 |
"""
|
| 521 |
|
|
|
|
| 522 |
def make_arc_html(nodes, node_type, buys, sells, transfers, loops):
|
|
|
|
| 523 |
nodes_json = json.dumps(nodes)
|
| 524 |
node_type_json = json.dumps(node_type)
|
| 525 |
buys_json = json.dumps(buys)
|
|
|
|
| 536 |
|
| 537 |
initial_html = make_arc_html(NODES, NODE_TYPE, BUYS, SELLS, TRANSFERS, LOOPS)
|
| 538 |
|
| 539 |
+
# ---------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
| 540 |
# Gradio UI
|
| 541 |
+
# ---------------------------------------------------------------------
|
| 542 |
+
|
| 543 |
responsive_css = """
|
| 544 |
#arc-container { padding:0; margin:0; }
|
| 545 |
svg { font-family: sans-serif; }
|
|
|
|
| 550 |
gr.HTML(initial_html)
|
| 551 |
|
| 552 |
gr.Markdown("### Inspect Company / AMC")
|
| 553 |
+
|
| 554 |
+
# Company inspector
|
| 555 |
select_company = gr.Dropdown(choices=COMPANIES, label="Select company")
|
| 556 |
company_plot = gr.Plot()
|
| 557 |
company_table = gr.DataFrame()
|
| 558 |
+
|
| 559 |
+
# AMC inspector
|
| 560 |
select_amc = gr.Dropdown(choices=AMCS, label="Select AMC")
|
| 561 |
amc_plot = gr.Plot()
|
| 562 |
amc_table = gr.DataFrame()
|