# app.py # Static weighted semi-layer arc diagram (L1 labels outside) # With short labels by default & full label on click import gradio as gr import pandas as pd import json import numpy as np from collections import defaultdict # --------------------------- # 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 = [ "HDFC Bank", "ICICI Bank", "Bajaj Finance", "Bajaj Finserv", "Adani Ports", "Tata Motors", "Shriram Finance", "HAL", "TCS", "AU Small Finance Bank", "Pearl Global", "Hindalco", "Tata Elxsi", "Cummins India", "Vedanta" ] BUY_MAP = { "SBI MF": ["Bajaj Finance", "AU Small Finance Bank"], "ICICI Pru MF": ["HDFC Bank"], "HDFC MF": ["Tata Elxsi", "TCS"], "Nippon India MF": ["Hindalco"], "Kotak MF": ["Bajaj Finance"], "UTI MF": ["Adani Ports", "Shriram Finance"], "Axis MF": ["Tata Motors", "Shriram Finance"], "Aditya Birla SL MF": ["AU Small Finance Bank"], "Mirae MF": ["Bajaj Finance", "HAL"], "DSP MF": ["Tata Motors", "Bajaj Finserv"] } SELL_MAP = { "SBI MF": ["Tata Motors"], "ICICI Pru MF": ["Bajaj Finance", "Adani Ports"], "HDFC MF": ["HDFC Bank"], "Nippon India MF": ["Hindalco"], "Kotak MF": ["AU Small Finance Bank"], "UTI MF": ["Hindalco", "TCS"], "Axis MF": ["TCS"], "Aditya Birla SL MF": ["Adani Ports"], "Mirae MF": ["TCS"], "DSP MF": ["HAL", "Shriram Finance"] } COMPLETE_EXIT = {"DSP MF": ["Shriram Finance"]} FRESH_BUY = {"HDFC MF": ["Tata Elxsi"], "UTI MF": ["Adani Ports"], "Mirae MF": ["HAL"]} def sanitize_map(m): out = {} for k, vals in m.items(): out[k] = [v for v in vals if v in COMPANIES] return out BUY_MAP = sanitize_map(BUY_MAP) SELL_MAP = sanitize_map(SELL_MAP) COMPLETE_EXIT = sanitize_map(COMPLETE_EXIT) FRESH_BUY = sanitize_map(FRESH_BUY) # --------------------------- # Label maps (NEW) # --------------------------- SHORT_LABEL = { "SBI MF": "SBI", "ICICI Pru MF": "ICICI", "HDFC MF": "HDFC", "Nippon India MF": "NIP", "Kotak MF": "KOTAK", "UTI MF": "UTI", "Axis MF": "AXIS", "Aditya Birla SL MF": "ABSL", "Mirae MF": "MIRAE", "DSP MF": "DSP", "HDFC Bank": "HDFC Bk", "ICICI Bank": "ICICI Bk", "Bajaj Finance": "Bajaj Fin", "Bajaj Finserv": "Bajaj Fsrv", "Adani Ports": "AdaniPt", "Tata Motors": "TataMot", "Shriram Finance": "Shriram", "HAL": "HAL", "TCS": "TCS", "AU Small Finance Bank": "AU SFB", "Pearl Global": "PearlG", "Hindalco": "Hindalco", "Tata Elxsi": "Elxsi", "Cummins India": "Cummins", "Vedanta": "Vedanta" } FULL_LABEL = {k: k for k in SHORT_LABEL} # --------------------------- # 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) | set(c2b): 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 to reduce crossings # --------------------------- 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, sells, transfers, loops = [], [], [], [] 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)) 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)) for (s,b),w in TRANSFER_COUNTS.items(): transfers.append((s,b,w)) for a,c,_ in buys: for c2,b,_ 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 BUYS, SELLS, TRANSFERS, LOOPS = build_flows() # --------------------------- # Inspect panels # --------------------------- 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 None, df counts = df.groupby("Role").size().reset_index(name="Count") fig = {"data":[{"type":"bar","x":counts["Role"].tolist(),"y":counts["Count"].tolist()}], "layout":{"title":f"Trades for {company}"}} 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 None,df counts=df["buyer_amc"].value_counts().reset_index() counts.columns=["Buyer AMC","Count"] fig={"data":[{"type":"bar","x":counts["Buyer AMC"].tolist(),"y":counts["Count"].tolist()}], "layout":{"title":f"Inferred transfers from {amc}"}} return fig,df # --------------------------- # HTML template: safe, no f-strings # --------------------------- JS_TEMPLATE = """
Legend
BUY = green solid
SELL = red dotted
TRANSFER = grey
LOOP = teal external arc
Labels: short by default. Clicking a node shows full name.
""" # --------------------------- # Build final HTML # --------------------------- def make_arc_html(nodes, node_type, buys, sells, transfers, loops): html = JS_TEMPLATE html = html.replace("__NODES__", json.dumps(nodes)) html = html.replace("__NODE_TYPE__", json.dumps(node_type)) html = html.replace("__BUYS__", json.dumps(buys)) html = html.replace("__SELLS__", json.dumps(sells)) html = html.replace("__TRANSFERS__", json.dumps(transfers)) html = html.replace("__LOOPS__", json.dumps(loops)) html = html.replace("__SHORT_LABEL__", json.dumps(SHORT_LABEL)) html = html.replace("__FULL_LABEL__", json.dumps(FULL_LABEL)) return html initial_html = make_arc_html(NODES, NODE_TYPE, BUYS, SELLS, TRANSFERS, LOOPS) # --------------------------- # Gradio UI # --------------------------- with gr.Blocks(title="MF Churn — Arc Diagram (Short→Full Label on Click)") as demo: gr.Markdown("## Mutual Fund Churn — Weighted Arc Diagram (Short labels → Full label on click)") gr.HTML(initial_html) gr.Markdown("### Inspect Company / AMC") select_company = gr.Dropdown(COMPANIES, label="Select company") company_plot = gr.Plot() company_table = gr.DataFrame() select_amc = gr.Dropdown(AMCS, label="Select AMC") amc_plot = gr.Plot() amc_table = gr.DataFrame() select_company.change(company_trade_summary, inputs=[select_company], outputs=[company_plot, company_table]) select_amc.change(amc_transfer_summary, inputs=[select_amc], outputs=[amc_plot, amc_table]) if __name__ == "__main__": demo.launch()