AMCAnalysis / app.py
singhn9's picture
Update app.py
aee615e verified
# app.py
# Static weighted semi-layer arc diagram (L1 labels outside)
# JS block injected safely (no Python f-strings inside JS)
import gradio as gr
import pandas as pd
import json
import numpy as np
from collections import defaultdict
import plotly.graph_objects as go
# ---------------------------
# Data
# ---------------------------
AMCS = [
"SBI MF",
"ICICI Pru MF",
"HDFC MF",
"Nippon India MF",
"Kotak MF",
"UTI MF",
"Axis MF",
"Aditya Birla SL MF",
"Mirae MF",
"DSP MF"
]
# ---------------------------
# COMPANIES (deduplicated + screenshot items)
# ---------------------------
COMPANIES = [
"ACC",
"Adani Ports and SEZ",
"Adani Power",
"Aditya Birla Lifestyle Brands",
"Affle 3i",
"Angel One",
"Ashok Leyland",
"Avenue Supermarts",
"Avanti Feeds",
"Axis Bank",
"AU Small Finance Bank",
"Bajaj Finance",
"Bajaj Finserv",
"Bank Of Maharashtra",
"Berger Paints India",
"Bharti Airtel",
"Canara Bank",
"CESC",
"CEAT",
"Colgate-Palmolive (India)",
"Dalmia Bharat",
"Dixon Technologies (India)",
"Dr. Reddy's Laboratories",
"Eternal",
"Fortis Healthcare",
"Glenmark Pharmaceuticals",
"Godrej Industries",
"HCC",
"HDFC Asset Management Co",
"HDFC Bank",
"Hindalco Industries",
"Hindustan Aeronautics",
"Hindustan Unilever",
"HPCL",
"Hyundai Motor India",
"ICICI Bank",
"Infosys",
"Indian Bank",
"IREDA",
"ITC",
"Jindal Steel",
"Karur Vysya Bank",
"Kotak Mahindra Bank",
"L&T Finance",
"Larsen & Toubro",
"Mahindra & Mahindra",
"Mankind Pharma",
"Maruti Suzuki India",
"MCX",
"Muthoot Finance",
"NMDC",
"NTPC",
"One97 Communications",
"Pearl Global Industries",
"Persistent Systems",
"Praj Industries",
"Power Finance Corporation",
"Power Grid Corporation Of India",
"Premier Energies",
"Sai Silks (Kalamandir)",
"Shaily Engineering Plastics",
"Shilpa Medicare",
"Shriram Finance",
"SJS Enterprises",
"Solar Industries India",
"Steel Authority Of India",
"Sumitomo Chemical India",
"Sundaram Finance",
"Suzlon Energy",
"Tata Communications",
"Tata Consultancy Services",
"Tata Elxsi",
"Tata Motors",
"Tata Motors Passenger Vehicles",
"Tata Steel",
"Titan Company",
"Trent",
"Travel Food Services",
"Ujjivan Small Finance Bank",
"UNO Minda",
"Vedanta",
"Welspun Corp",
"Yatharth Hospital & Trauma Care",
"Zydus Lifesciences"
]
# ---------------------------
# BUY_MAP (from screenshot)
# ---------------------------
BUY_MAP = {
"SBI MF": [
"Bajaj Finance",
"Adani Power",
"Infosys"
],
"ICICI Pru MF": [
"ICICI Bank",
"HDFC Bank",
"NTPC"
],
"HDFC MF": [
"HDFC Bank",
"ICICI Bank",
"Power Grid Corporation Of India",
"Trent"
],
"Nippon India MF": [
"Trent",
"HDFC Bank",
"Colgate-Palmolive (India)",
"ITC"
],
"Kotak MF": [
"ITC",
"HDFC Bank",
"L&T Finance"
],
"UTI MF": [
"Dixon Technologies (India)",
"Bank Of Maharashtra",
"Affle 3i"
],
"Axis MF": [
"Kotak Mahindra Bank",
"AU Small Finance Bank",
"Titan Company"
],
"Aditya Birla SL MF": [
"Hindustan Unilever",
"Thyrocare Technologies",
"Mankind Pharma"
],
"Mirae MF": [
"ICICI Bank",
"Shriram Finance"
],
"DSP MF": [
"Tata Consultancy Services",
"Axis Bank"
]
}
# ---------------------------
# SELL_MAP (from screenshot)
# ---------------------------
SELL_MAP = {
"SBI MF": [
"HDFC Bank",
"ICICI Bank",
"Adani Ports and SEZ"
],
"ICICI Pru MF": [
"Bharti Airtel",
"Axis Bank",
"Larsen & Toubro",
"Maruti Suzuki India"
],
"HDFC MF": [
"NTPC",
"Tata Consultancy Services"
],
"Nippon India MF": [
"HDFC Asset Management Co",
"One97 Communications",
"Hyundai Motor India"
],
"Kotak MF": [
"Power Finance Corporation",
"Solar Industries India",
"HPCL"
],
"UTI MF": [
"Dr. Reddy's Laboratories",
"Bajaj Finance",
"Muthoot Finance",
"Avenue Supermarts"
],
"Axis MF": [
"Bajaj Finance",
"UNO Minda",
"Jindal Steel",
"Sundaram Finance"
],
"Aditya Birla SL MF": [
"Fortis Healthcare",
"Mahindra & Mahindra",
"Suzlon Energy",
"Tata Steel",
"Vedanta",
"Ashok Leyland"
],
"Mirae MF": [
"Berger Paints India",
"Godrej Industries"
],
"DSP MF": [
"Bajaj Finance",
"Tata Motors Passenger Vehicles",
"Bajaj Finserv"
]
}
# ---------------------------
# COMPLETE_EXIT (from screenshot)
# ---------------------------
COMPLETE_EXIT = {
"ICICI Pru MF": [
"Tata Elxsi",
"Avanti Feeds"
],
"HDFC MF": [
"IREDA",
"HCC"
],
"Nippon India MF": [
"Premier Energies",
"Welspun Corp",
"Zydus Lifesciences"
],
"Kotak MF": [
"Travel Food Services",
"Colgate-Palmolive (India)"
],
"UTI MF": [
"Bajaj Finserv",
"Sai Silks (Kalamandir)"
],
"Axis MF": [
"Sumitomo Chemical India",
"Adani Ports and SEZ",
"CEAT",
"ACC"
],
"Aditya Birla SL MF": [],
"Mirae MF": [
"NMDC"
],
"DSP MF": [
"Praj Industries"
]
}
# ---------------------------
# FRESH_BUY (from screenshot)
# ---------------------------
FRESH_BUY = {
"SBI MF": [
"Canara Bank",
"MCX"
],
"ICICI Pru MF": [
"Pearl Global Industries",
"Aditya Birla Lifestyle Brands",
"Angel One"
],
"HDFC MF": [
"Shriram Finance",
"Thyrocare Technologies",
"SJS Enterprises",
"Shilpa Medicare"
],
"Nippon India MF": [
"UNO Minda",
"Hindalco Industries"
],
"Kotak MF": [
"Pearl Global Industries"
],
"UTI MF": [
"Tata Communications",
"Ujjivan Small Finance Bank",
"CESC"
],
"Axis MF": [
"Adani Power",
"Steel Authority Of India",
"Shaily Engineering Plastics",
"Persistent Systems",
"Hindustan Aeronautics"
],
"Aditya Birla SL MF": [
"Eternal",
"Hindustan Aeronautics",
"Ujjivan Small Finance Bank"
],
"Mirae MF": [],
"DSP MF": [
"Eternal",
"Hindustan Aeronautics",
"Ujjivan Small Finance Bank"
]
}
def sanitize_map(m):
out = {}
for k, vals in m.items():
out[k] = [v for v in vals if v in COMPANIES]
return out
# sanitize_map call will keep only items present in COMPANIES
BUY_MAP = sanitize_map(BUY_MAP)
SELL_MAP = sanitize_map(SELL_MAP)
COMPLETE_EXIT = sanitize_map(COMPLETE_EXIT)
FRESH_BUY = sanitize_map(FRESH_BUY)
# ---------------------------
# Infer AMC->AMC transfers
# ---------------------------
def infer_amc_transfers(buy_map, sell_map):
transfers = defaultdict(int)
c2s = defaultdict(list)
c2b = defaultdict(list)
for amc, comps in sell_map.items():
for c in comps:
c2s[c].append(amc)
for amc, comps in buy_map.items():
for c in comps:
c2b[c].append(amc)
for c in set(c2s.keys()) | set(c2b.keys()):
for s in c2s[c]:
for b in c2b[c]:
transfers[(s, b)] += 1
return transfers
TRANSFER_COUNTS = infer_amc_transfers(BUY_MAP, SELL_MAP)
# ---------------------------
# Mixed ordering
# ---------------------------
def build_mixed_ordering(amcs, companies):
mixed = []
n = max(len(amcs), len(companies))
for i in range(n):
if i < len(amcs):
mixed.append(amcs[i])
if i < len(companies):
mixed.append(companies[i])
return mixed
NODES = build_mixed_ordering(AMCS, COMPANIES)
NODE_TYPE = {n: ("amc" if n in AMCS else "company") for n in NODES}
# ---------------------------
# Build flows
# ---------------------------
def build_flows():
buys = []
for amc, comps in BUY_MAP.items():
for c in comps:
w = 3 if (amc in FRESH_BUY and c in FRESH_BUY.get(amc, [])) else 1
buys.append((amc, c, w))
sells = []
for amc, comps in SELL_MAP.items():
for c in comps:
w = 3 if (amc in COMPLETE_EXIT and c in COMPLETE_EXIT.get(amc, [])) else 1
sells.append((c, amc, w))
transfers = []
for (s,b), w in TRANSFER_COUNTS.items():
transfers.append((s, b, w))
fresh_buys = []
for amc, comps in FRESH_BUY.items():
for c in comps:
fresh_buys.append((amc, c, 2)) # weight 2 or 3 as you prefer
loops = []
for a,c,w1 in buys:
for c2,b,w2 in sells:
if c == c2:
loops.append((a, c, b))
loops = list({(a,c,b) for (a,c,b) in loops})
return buys, sells, transfers, loops,fresh_buys
BUYS, SELLS, TRANSFERS, LOOPS,FRESH_BUYS_FLOW = build_flows()
# ---------------------------
# Inspector summaries
# ---------------------------
def company_trade_summary(company):
buyers = [a for a, cs in BUY_MAP.items() if company in cs]
sellers = [a for a, cs in SELL_MAP.items() if company in cs]
fresh = [a for a, cs in FRESH_BUY.items() if company in cs]
exits = [a for a, cs in COMPLETE_EXIT.items() if company in cs]
df = pd.DataFrame({
"Role":
(["Buyer"] * len(buyers))
+ (["Seller"] * len(sellers))
+ (["Fresh buy"] * len(fresh))
+ (["Complete exit"] * len(exits)),
"AMC": buyers + sellers + fresh + exits
})
if df.empty:
return go.Figure(), pd.DataFrame([], columns=["Role", "AMC"])
counts = df.groupby("Role").size().reset_index(name="Count")
fig = go.Figure(data=[go.Bar(x=counts["Role"], y=counts["Count"])])
fig.update_layout(title=f"Trades for {company}", margin=dict(l=20,r=20,t=40,b=20))
return fig, df
def amc_transfer_summary(amc):
sold = SELL_MAP.get(amc, [])
transfers = []
for s in sold:
buyers = [a for a, cs in BUY_MAP.items() if s in cs]
for b in buyers:
transfers.append({"security": s, "buyer_amc": b})
df = pd.DataFrame(transfers)
if df.empty:
return go.Figure(), pd.DataFrame([], columns=["security", "buyer_amc"])
counts = df["buyer_amc"].value_counts().reset_index()
counts.columns = ["Buyer AMC", "Count"]
fig = go.Figure(data=[go.Bar(x=counts["Buyer AMC"], y=counts["Count"])])
fig.update_layout(title=f"Inferred transfers from {amc}", margin=dict(l=20,r=20,t=40,b=20))
return fig, df
# ---------------------------------------------------------------------
# JS_TEMPLATE - single triple-quoted string (complete HTML + JS)
# ---------------------------------------------------------------------
JS_TEMPLATE = """
<div id="arc-container" style="width:100%; height:720px;"></div>
<div style="margin-top:8px;">
<button id="arc-reset" style="padding:8px 12px; border-radius:6px;">Reset</button>
</div>
<div style="margin-top:10px; font-family:sans-serif; font-size:13px;">
<b>Legend</b><br/>
<span style="display:inline-block;width:12px;height:8px;background:#2e8540;margin-right:6px;"></span> BUY (green solid)<br/>
<span style="display:inline-block;width:12px;height:8px;background:#c0392b;margin-right:6px;border-bottom:3px dotted #c0392b;"></span> SELL (red dotted)<br/>
<span style="display:inline-block;width:12px;height:8px;background:#7d7d7d;margin-right:6px;"></span> TRANSFER (grey, inferred)<br/>
<span style="display:inline-block;width:12px;height:8px;background:#9b59b6;margin-right:6px;"></span> LOOP (violet external arc)<br/>
<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>
</div>
<div id="info-box" style="margin-top:12px; padding:10px;
border:1px solid #ddd; border-radius:8px; font-family:sans-serif;
font-size:13px; background:#fbfbfb;">
<b>Click a node</b> to view details here.
</div>
<script src="https://d3js.org/d3.v7.min.js"></script>
<script>
const NODES = __NODES__;
const NODE_TYPE = __NODE_TYPE__;
const BUYS = __BUYS__;
const SELLS = __SELLS__;
const TRANSFERS = __TRANSFERS__;
const LOOPS = __LOOPS__;
const FRESH = __FRESH__;
const COMPLETE_EXIT = __COMPLETE_EXIT__;
function draw() {
const container = document.getElementById("arc-container");
// ★ FINAL FIX: always clear old SVG before drawing
container.innerHTML = "";
const w = Math.min(1200, container.clientWidth || 920);
const h = Math.max(420, Math.floor(w * 0.62));
const svg = d3.select(container)
.append("svg")
.attr("width", "100%")
.attr("height", h)
.attr("viewBox", [-w/2, -h/2, w, h].join(" "));
const radius = Math.min(w, h) * 0.36;
const n = NODES.length;
function angleFor(i){ return (i / n) * 2 * Math.PI; }
const nodePos = NODES.map((name, i) => {
const ang = angleFor(i) - Math.PI/2;
const ab = (name.length > 7 ? name.slice(0,5) + "…" : name);
return {
name: name,
abbrev: ab,
angle: ang,
x: Math.cos(ang)*radius,
y: Math.sin(ang)*radius
};
});
const nameToIndex = {};
NODES.forEach((nm, i) => nameToIndex[nm] = i);
//--------------------------------------------------------------------
// NODES — FIX #1: disable pointer events on <g>
//--------------------------------------------------------------------
const nodeCircleGroup = svg.append("g")
.attr("class", "node-circles")
.selectAll("g")
.data(nodePos)
.enter()
.append("g")
.attr("transform", d => `translate(${d.x},${d.y})`)
.style("pointer-events", "none");
//--------------------------------------------------------------------
// CIRCLES — FIX #2: re-enable pointer events
//--------------------------------------------------------------------
nodeCircleGroup.append("circle")
.attr("r", 16)
.attr("fill", d => NODE_TYPE[d.name] === "amc" ? "#2b6fa6" : "#f2c88d")
.attr("stroke", "#222")
.attr("stroke-width", 1)
.style("cursor", "pointer")
.style("pointer-events", "all");
//--------------------------------------------------------------------
// ARC drawing (unchanged)
//--------------------------------------------------------------------
function bezierPath(x0,y0,x1,y1,above=true){
const mx = (x0+x1)/2;
const my = (y0+y1)/2;
const len = Math.sqrt(mx*mx + my*my) || 1;
const ux = mx/len, uy = my/len;
const offset = (above ? -1 : 1) * Math.max(30, radius*0.9);
return `M${x0},${y0} Q${mx + ux*offset},${my + uy*offset} ${x1},${y1}`;
}
const allW = [].concat(
BUYS.map(x=>x[2]),
SELLS.map(x=>x[2]),
TRANSFERS.map(x=>x[2]),
(FRESH ? FRESH.map(x=>x[2]) : [])
);
const stroke = d3.scaleLinear()
.domain([Math.min(...allW), Math.max(...allW)])
.range([1,6]);
const buyGroup = svg.append("g").attr("class", "buys");
BUYS.forEach(b => {
const a = b[0], c = b[1], wt = b[2];
if (!(a in nameToIndex) || !(c in nameToIndex)) return;
const s = nodePos[nameToIndex[a]];
const t = nodePos[nameToIndex[c]];
buyGroup.append("path")
.attr("d", bezierPath(s.x,s.y,t.x,t.y,true))
.attr("fill", "none")
.attr("stroke", "#2e8540")
.attr("stroke-width", stroke(wt))
.attr("opacity", 0.92)
.attr("data-src", a)
.attr("data-tgt", c);
});
const sellGroup = svg.append("g").attr("class", "sells");
SELLS.forEach(s => {
const c = s[0], a = s[1], wt = s[2];
if (!(c in nameToIndex) || !(a in nameToIndex)) return;
const sp = nodePos[nameToIndex[c]];
const tp = nodePos[nameToIndex[a]];
sellGroup.append("path")
.attr("d", bezierPath(sp.x,sp.y,tp.x,tp.y,false))
.attr("fill", "none")
.attr("stroke", "#c0392b")
.attr("stroke-width", stroke(wt))
.attr("stroke-dasharray", "4,3")
.attr("opacity", 0.86)
.attr("data-src", c)
.attr("data-tgt", a);
});
const transferGroup = svg.append("g").attr("class", "transfers");
TRANSFERS.forEach(tr => {
const sname = tr[0], tname = tr[1], wt = tr[2];
if (!(sname in nameToIndex) || !(tname in nameToIndex)) return;
const sp = nodePos[nameToIndex[sname]];
const tp = nodePos[nameToIndex[tname]];
const mx = (sp.x + tp.x)/2, my = (sp.y + tp.y)/2;
transferGroup.append("path")
.attr("d", `M${sp.x},${sp.y} Q${mx*0.3},${my*0.3} ${tp.x},${tp.y}`)
.attr("fill", "none")
.attr("stroke", "#7d7d7d")
.attr("stroke-width", stroke(wt))
.attr("opacity", 0.7)
.attr("data-src", sname)
.attr("data-tgt", tname);
});
const freshGroup = svg.append("g").attr("class", "fresh");
FRESH.forEach(f => {
const a = f[0], c = f[1], wt = f[2];
if (!(a in nameToIndex) || !(c in nameToIndex)) return;
const s = nodePos[nameToIndex[a]];
const t = nodePos[nameToIndex[c]];
freshGroup.append("path")
.attr("d", bezierPath(s.x, s.y, t.x, t.y, true))
.attr("fill", "none")
.attr("stroke", "#1abc9c") // teal color
.attr("stroke-width", stroke(wt))
.attr("opacity", 0.95)
.attr("data-src", a)
.attr("data-tgt", c);
});
const loopGroup = svg.append("g").attr("class", "loops");
LOOPS.forEach(lp => {
const a = lp[0], c = lp[1], b = lp[2];
if (!(a in nameToIndex) || !(b in nameToIndex)) return;
const sa = nodePos[nameToIndex[a]];
const sb = nodePos[nameToIndex[b]];
const mx = (sa.x+sb.x)/2, my = (sa.y+sb.y)/2;
const len = Math.sqrt((sa.x - sb.x)*(sa.x - sb.x) + (sa.y - sb.y)*(sa.y - sb.y));
const outward = Math.max(40, radius*0.28 + len * 0.12);
const nlen = Math.sqrt(mx*mx + my*my) || 1;
const ux = mx / nlen, uy = my / nlen;
const cx = mx + ux * outward;
const cy = my + uy * outward;
const path = `M ${sa.x} ${sa.y} Q ${cx} ${cy} ${sb.x} ${sb.y}`;
loopGroup.append("path")
.attr("d", path)
.attr("fill", "none")
.attr("stroke", "#9b59b6")
.attr("stroke-width", 4.2)
.attr("opacity", 1)
.attr("stroke-linecap", "round")
.attr("stroke-linejoin", "round");
});
const loopPathGroup = svg.append("g").attr("class", "loop-path-highlight");
function drawLoopPathsFor(nodeName) {
loopPathGroup.selectAll("*").remove();
LOOPS.forEach(lp => {
const amcA = lp[0], company = lp[1], amcB = lp[2];
if (!(amcA in nameToIndex) || !(company in nameToIndex) || !(amcB in nameToIndex)) return;
if (nodeName !== amcA && nodeName !== company && nodeName !== amcB) return;
const pA = nodePos[nameToIndex[amcA]];
const pC = nodePos[nameToIndex[company]];
const pB = nodePos[nameToIndex[amcB]];
loopPathGroup.append("path")
.attr("d", `M${pA.x},${pA.y} L${pC.x},${pC.y} L${pB.x},${pB.y}`)
.attr("fill", "none")
.attr("stroke", "#8e44ad")
.attr("stroke-width", 4.5)
.attr("opacity", 0.98)
.attr("stroke-linecap", "round")
.attr("stroke-linejoin", "round");
});
}
//--------------------------------------------------------------------
// LABELS — FIX #3
//--------------------------------------------------------------------
const labelGroup = svg.append("g")
.attr("class", "node-labels")
.selectAll("text")
.data(nodePos)
.enter()
.append("text")
.attr("x", d => Math.cos(d.angle) * (radius + 28))
.attr("y", d => Math.sin(d.angle) * (radius + 28))
.attr("dy", "0.35em")
.style("font-family", "sans-serif")
.style("font-size", "13px")
.style("cursor", "pointer")
.style("text-anchor", d => {
const deg = (d.angle * 180 / Math.PI);
return (deg>-90 && deg<90) ? "start" : "end";
})
.style("pointer-events", "all")
.text(d => d.abbrev);
//--------------------------------------------------------------------
// CLICK / HIGHLIGHT logic (unchanged)
//--------------------------------------------------------------------
function getConnections(nodeName){
let buys = BUYS.filter(x=>x[0]===nodeName || x[1]===nodeName).map(x=>x[0]===nodeName?x[1]:x[0]);
let sells = SELLS.filter(x=>x[0]===nodeName || x[1]===nodeName).map(x=>x[0]===nodeName?x[1]:x[0]);
let transfers = TRANSFERS.filter(x=>x[0]===nodeName || x[1]===nodeName).map(x=>x[0]===nodeName?x[1]:x[0]);
let loops = LOOPS.filter(x=>x[0]===nodeName || x[2]===nodeName).map(x=>x[0]===nodeName?x[2]:x[0]);
// include FRESH edges
let fresh = FRESH.filter(x=>x[0]===nodeName || x[1]===nodeName).map(x=>x[0]===nodeName?x[1]:x[0]);
// include COMPLETE_EXIT connections:
// - if nodeName is an AMC, include companies in COMPLETE_EXIT[nodeName]
// - if nodeName is a company, include AMCs that list that company in their COMPLETE_EXIT
let exits = [];
if (COMPLETE_EXIT[nodeName]) {
// nodeName is an AMC
exits = exits.concat(COMPLETE_EXIT[nodeName]);
} else {
// nodeName may be a company; find AMCs that have it
exits = exits.concat(Object.keys(COMPLETE_EXIT).filter(amc => (COMPLETE_EXIT[amc] || []).includes(nodeName)));
}
return new Set([].concat(buys, sells, transfers, loops, fresh, exits));
}
function updateLabels(selected, connected){
labelGroup.text(d => {
if (selected && (d.name===selected || connected.has(d.name)))
return d.name;
return d.abbrev;
});
}
function setOpacityFor(nodeName){
const connected = getConnections(nodeName);
nodeCircleGroup.selectAll("circle")
.style("opacity", d => (d.name===nodeName || connected.has(d.name)) ? 1 : 0.18);
labelGroup.style("opacity", d => (d.name===nodeName || connected.has(d.name)) ? 1 : 0.28);
function isConn(p){
const src = p.getAttribute("data-src");
const tgt = p.getAttribute("data-tgt");
return src===nodeName || tgt===nodeName || connected.has(src) || connected.has(tgt);
}
buyGroup.selectAll("path").style("opacity", function(){ return isConn(this)?0.98:0.06; });
sellGroup.selectAll("path").style("opacity", function(){ return isConn(this)?0.98:0.06; });
transferGroup.selectAll("path").style("opacity", function(){ return isConn(this)?0.98:0.06; });
freshGroup.selectAll("path").style("opacity", function(){
return isConn(this) ? 0.98 : 0.06;
});
loopGroup.selectAll("path").style("opacity", 0.9);
}
function resetOpacity(){
nodeCircleGroup.selectAll("circle").style("opacity",1);
labelGroup.style("opacity",1);
buyGroup.selectAll("path").style("opacity",0.92);
sellGroup.selectAll("path").style("opacity",0.86);
transferGroup.selectAll("path").style("opacity",0.7);
loopGroup.selectAll("path").style("opacity",1);
loopPathGroup.selectAll("*").remove();
updateLabels(null, new Set());
document.getElementById("info-box").innerHTML =
"<b>Click a node</b> to view details here.";
}
function showInfo(nodeName){
const buys = BUYS.filter(x => x[0] === nodeName || x[1] === nodeName)
.map(x => x[0] === nodeName ? x[1] : x[0]);
const sells = SELLS.filter(x => x[0] === nodeName || x[1]===nodeName)
.map(x => x[0] === nodeName ? x[1] : x[0]);
const transfers = TRANSFERS.filter(x => x[0]===nodeName || x[1]===nodeName)
.map(x => x[0]===nodeName ? x[1] : x[0]);
const loops = LOOPS.filter(x => x[0]===nodeName || x[2]===nodeName)
.map(x => x[0]===nodeName ? x[2] : x[0]);
// detect Fresh Buys
const fresh = FRESH.filter(x => x[0] === nodeName || x[1] === nodeName)
.map(x => x[0] === nodeName ? x[1] : x[0]);
// detect Complete Exits:
// if nodeName is AMC, list companies they completely exited
// if nodeName is company, list AMCs who completely exited that company
let exits = [];
if (COMPLETE_EXIT[nodeName]) {
exits = exits.concat(COMPLETE_EXIT[nodeName]); // nodeName is AMC
} else {
exits = exits.concat(Object.keys(COMPLETE_EXIT).filter(amc => (COMPLETE_EXIT[amc] || []).includes(nodeName)));
}
const box = document.getElementById("info-box");
box.innerHTML = `
<div style="font-size:14px;"><b>${nodeName}</b></div>
<div style="margin-top:8px; font-size:13px;">
<b>Buys:</b> ${buys.length ? buys.join(", ") : "<span style='color:#777'>None</span>"}<br>
<b>Sells:</b> ${sells.length ? sells.join(", ") : "<span style='color:#777'>None</span>"}<br>
<b>Transfers:</b> ${transfers.length ? transfers.join(", ") : "<span style='color:#777'>None</span>"}<br>
<b>Loops:</b> ${loops.length ? loops.join(", ") : "<span style='color:#777'>None</span>"}<br>
<b>Fresh Buys:</b> ${fresh.length ? fresh.join(", ") : "<span style='color:#777'>None</span>"}<br>
<b>Complete Exits:</b> ${exits.length ? exits.join(", ") : "<span style='color:#777'>None</span>"}<br>
</div>
`;
}
function selectNode(d){
const name = d.name;
setOpacityFor(name);
showInfo(name);
const connected = getConnections(name);
updateLabels(name, connected);
drawLoopPathsFor(name);
}
//--------------------------------------------------------------------
// CLICK HANDLERS
//--------------------------------------------------------------------
nodeCircleGroup.on("click", function(event, d){
selectNode(d);
event.stopPropagation();
});
labelGroup.on("click", function(event, d){
selectNode(d);
event.stopPropagation();
});
document.getElementById("arc-reset").onclick = resetOpacity;
svg.on("click", function(event){
if (event.target === this) resetOpacity();
});
}
draw();
setTimeout(() => {
let rz;
window.addEventListener("resize", () => {
clearTimeout(rz);
rz = setTimeout(draw, 120);
});
}, 400);
</script>
"""
def make_arc_html(nodes, node_type, buys, sells, transfers, loops, fresh):
nodes_json = json.dumps(nodes)
node_type_json = json.dumps(node_type)
buys_json = json.dumps(buys)
sells_json = json.dumps(sells)
transfers_json = json.dumps(transfers)
loops_json = json.dumps(loops)
fresh_json = json.dumps(fresh)
html = (JS_TEMPLATE
.replace("__NODES__", nodes_json)
.replace("__NODE_TYPE__", node_type_json)
.replace("__BUYS__", buys_json)
.replace("__SELLS__", sells_json)
.replace("__TRANSFERS__", transfers_json)
.replace("__LOOPS__", loops_json)
.replace("__FRESH__", fresh_json)
.replace("__COMPLETE_EXIT__", json.dumps(COMPLETE_EXIT))
)
return html
initial_html = make_arc_html(NODES, NODE_TYPE, BUYS, SELLS, TRANSFERS, LOOPS, FRESH_BUYS_FLOW)
# ---------------------------------------------------------------------
# Gradio UI
# ---------------------------------------------------------------------
responsive_css = """
#arc-container { padding:0; margin:0; }
svg { font-family: sans-serif; }
"""
with gr.Blocks(css=responsive_css, title="MF Churn — Semi-layer Arc Diagram (L1 labels)") as demo:
gr.Markdown("## Mutual Fund Churn — Weighted Arcs (BUY top / SELL bottom) — labels outside (L1)")
gr.HTML(initial_html,container=False)
gr.Markdown("### Inspect Company / AMC")
# Company inspector
select_company = gr.Dropdown(choices=COMPANIES, label="Select company")
company_plot = gr.Plot()
company_table = gr.DataFrame()
# AMC inspector
select_amc = gr.Dropdown(choices=AMCS, label="Select AMC")
amc_plot = gr.Plot()
amc_table = gr.DataFrame()
def on_company(c):
fig, df = company_trade_summary(c)
return fig, df
def on_amc(a):
fig, df = amc_transfer_summary(a)
return fig, df
select_company.change(on_company, inputs=[select_company], outputs=[company_plot, company_table])
select_amc.change(on_amc, inputs=[select_amc], outputs=[amc_plot, amc_table])
if __name__ == "__main__":
demo.launch()