Spaces:
Sleeping
Sleeping
| # 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() | |