singhn9 commited on
Commit
d504876
·
verified ·
1 Parent(s): 450a37e

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +510 -264
app.py CHANGED
@@ -1,18 +1,19 @@
1
  # app.py
2
- # Full Working Weighted Arc Diagram
3
- # Short labels on nodes → full labels on click
4
- # Highlight on click, dim rest
5
- # Includes: BUY arcs, SELL arcs, TRANSFER arcs, LOOPS
6
- # And Company/AMC inspector panels
7
 
8
  import gradio as gr
9
  import pandas as pd
 
 
 
10
  import json
11
  from collections import defaultdict
12
 
13
- ###############################################################
14
  # DATA
15
- ###############################################################
16
 
17
  AMCS = [
18
  "SBI MF", "ICICI Pru MF", "HDFC MF", "Nippon India MF", "Kotak MF",
@@ -51,303 +52,548 @@ SELL_MAP = {
51
  "DSP MF": ["HAL", "Shriram Finance"]
52
  }
53
 
54
- FRESH_BUY = {"HDFC MF": ["Tata Elxsi"], "UTI MF": ["Adani Ports"], "Mirae MF": ["HAL"]}
55
  COMPLETE_EXIT = {"DSP MF": ["Shriram Finance"]}
 
56
 
57
- ###############################################################
58
- # LABELS
59
- ###############################################################
60
-
61
- SHORT_LABEL = {
62
- "SBI MF": "SBI", "ICICI Pru MF": "ICICI", "HDFC MF": "HDFC",
63
- "Nippon India MF": "NIPP", "Kotak MF": "KOT", "UTI MF": "UTI",
64
- "Axis MF": "AXIS", "Aditya Birla SL MF": "ABSL", "Mirae MF": "MIR",
65
- "DSP MF": "DSP",
66
-
67
- "HDFC Bank": "HDFCB", "ICICI Bank": "ICICB", "Bajaj Finance": "BajFin",
68
- "Bajaj Finserv": "BajFsv", "Adani Ports": "AdPorts", "Tata Motors": "TataM",
69
- "Shriram Finance": "ShrFin", "HAL": "HAL", "TCS": "TCS",
70
- "AU Small Finance Bank": "AUSFB", "Pearl Global": "PearlG",
71
- "Hindalco": "Hind", "Tata Elxsi": "Elxsi", "Cummins India": "Cumm",
72
- "Vedanta": "Ved"
73
- }
74
 
75
- FULL_LABEL = {k: k for k in SHORT_LABEL}
 
 
 
76
 
77
- ###############################################################
78
- # TRANSFERS
79
- ###############################################################
80
 
81
- def infer_transfers(buy_map, sell_map):
82
- c2s = defaultdict(list)
83
- c2b = defaultdict(list)
84
  transfers = defaultdict(int)
 
 
85
 
86
- for a, cs in sell_map.items():
87
- for c in cs:
88
- c2s[c].append(a)
89
 
90
- for a, cs in buy_map.items():
91
- for c in cs:
92
- c2b[c].append(a)
93
 
94
- for c in set(c2s) | set(c2b):
95
- for s in c2s[c]:
96
- for b in c2b[c]:
97
  transfers[(s, b)] += 1
98
 
99
- return transfers
 
 
 
100
 
101
- TRANSFERS = infer_transfers(BUY_MAP, SELL_MAP)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
 
103
- ###############################################################
104
- # NODE ORDERING (interleave)
105
- ###############################################################
106
 
107
- def interleave(a, b):
108
- out = []
109
- L = max(len(a), len(b))
110
- for i in range(L):
111
- if i < len(a): out.append(a[i])
112
- if i < len(b): out.append(b[i])
113
- return out
114
 
115
- NODES = interleave(AMCS, COMPANIES)
116
- NODE_TYPE = {n: ("amc" if n in AMCS else "company") for n in NODES}
 
 
 
 
 
 
117
 
118
- ###############################################################
119
- # INSPECT PANELS
120
- ###############################################################
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
121
 
122
  def company_trade_summary(company):
123
  buyers = [a for a, cs in BUY_MAP.items() if company in cs]
124
  sellers = [a for a, cs in SELL_MAP.items() if company in cs]
 
 
125
 
126
  df = pd.DataFrame({
127
- "Role": ["Buyer"]*len(buyers) + ["Seller"]*len(sellers),
128
- "AMC": buyers + sellers
 
 
 
129
  })
130
 
131
  if df.empty:
132
- return None, df
 
 
 
 
 
 
 
 
133
 
134
- fig = {
135
- "data": [{"type": "bar", "x": df["Role"], "y": [1]*len(df)}],
136
- "layout": {"title": f"Trades for {company}"}
137
- }
138
  return fig, df
139
 
140
- ###############################################################
141
- # FULL ARC DIAGRAM HTML + JS
142
- ###############################################################
143
 
144
- ARC_HTML = """
145
- <div id="arc-container" style="width:100%; height:720px;"></div>
 
 
 
 
 
146
 
147
- <div style="margin-top:12px;font-size:14px;">
148
- <b>Legend:</b><br>
149
- • <span style='color:#2e8540;'>Green = BUY</span><br>
150
- • <span style='color:#c0392b;'>Red dotted = SELL</span><br>
151
- • <span style='color:gray;'>Gray = Transfer</span><br>
152
- • <span style='color:#227a6d;'>Teal = Loop</span><br>
153
- Short labels → Click a node to expand.
154
- </div>
155
 
156
- <script src="https://d3js.org/d3.v7.min.js"></script>
 
157
 
158
- <script>
159
- const NODES = __NODES__;
160
- const NODE_TYPE = __NODE_TYPE__;
161
- const BUYS = __BUYS__;
162
- const SELLS = __SELLS__;
163
- const TRANSFERS = __TRANSFERS__;
164
- const SHORT_LABEL = __SHORT_LABEL__;
165
- const FULL_LABEL = __FULL_LABEL__;
166
-
167
- function drawArcDiagram() {
168
- const container = document.getElementById("arc-container");
169
- container.innerHTML = "";
170
-
171
- const w = container.clientWidth;
172
- const h = 720;
173
-
174
- const svg = d3.select(container)
175
- .append("svg")
176
- .attr("width", w)
177
- .attr("height", h)
178
- .attr("viewBox", [-w/2, -h/2, w, h]);
179
-
180
- const R = Math.min(w, h) * 0.38;
181
-
182
- function angle(i) { return (i / NODES.length) * 2 * Math.PI; }
183
-
184
- const positions = NODES.map((name, i) => {
185
- const a = angle(i) - Math.PI/2;
186
- return {
187
- name,
188
- x: Math.cos(a) * R,
189
- y: Math.sin(a) * R
190
- };
191
- });
192
-
193
- const indexOf = {};
194
- NODES.forEach((n, i) => indexOf[n] = i);
195
-
196
- const nodeG = svg.append("g")
197
- .selectAll("g")
198
- .data(positions)
199
- .enter()
200
- .append("g")
201
- .attr("transform", d => `translate(${d.x},${d.y})`);
202
-
203
- nodeG.append("circle")
204
- .attr("r", 18)
205
- .attr("fill", d => NODE_TYPE[d.name] === "amc" ? "#2b6fa6" : "#f2c88d")
206
- .attr("stroke", "#222")
207
- .attr("stroke-width", 1.5)
208
- .style("cursor", "pointer");
209
-
210
- nodeG.append("text")
211
- .text(d => SHORT_LABEL[d.name])
212
- .attr("text-anchor", "middle")
213
- .attr("dy", "0.35em")
214
- .style("fill", "white")
215
- .style("font-size", "10px")
216
- .style("pointer-events", "none");
217
-
218
- function curvePath(x1,y1,x2,y2,flip=false) {
219
- const mx = (x1 + x2) / 2;
220
- const my = (y1 + y2) / 2;
221
- const dx = x2 - x1;
222
- const dy = y2 - y1;
223
- const dist = Math.sqrt(dx*dx + dy*dy);
224
- const nx = -dy / dist;
225
- const ny = dx / dist;
226
- const curve = flip ? -1 : 1;
227
- const cx = mx + nx * dist * 0.35 * curve;
228
- const cy = my + ny * dist * 0.35 * curve;
229
- return `M ${x1} ${y1} Q ${cx} ${cy} ${x2} ${y2}`;
230
- }
231
 
232
- const strokeScale = d3.scaleLinear()
233
- .domain([1, 4])
234
- .range([1.5, 6]);
235
-
236
- const edgeG = svg.append("g");
237
-
238
- BUYS.forEach(b => {
239
- const s = b[0], t = b[1], w = b[2];
240
- const a = positions[indexOf[s]];
241
- const c = positions[indexOf[t]];
242
- edgeG.append("path")
243
- .attr("d", curvePath(a.x, a.y, c.x, c.y, false))
244
- .attr("stroke", "#2e8540")
245
- .attr("stroke-width", strokeScale(w))
246
- .attr("fill", "none")
247
- .attr("opacity", 0.9)
248
- .attr("data-src", s)
249
- .attr("data-tgt", t);
250
- });
251
-
252
- SELLS.forEach(s => {
253
- const c = s[0], a = s[1], w = s[2];
254
- const sp = positions[indexOf[c]];
255
- const tp = positions[indexOf[a]];
256
- edgeG.append("path")
257
- .attr("d", curvePath(sp.x, sp.y, tp.x, tp.y, true))
258
- .attr("stroke", "#c0392b")
259
- .attr("stroke-width", strokeScale(w))
260
- .attr("stroke-dasharray", "4 3")
261
- .attr("fill", "none")
262
- .attr("opacity", 0.85)
263
- .attr("data-src", c)
264
- .attr("data-tgt", a);
265
- });
266
-
267
- TRANSFERS.forEach(([s,b,w]) => {
268
- const sp = positions[indexOf[s]];
269
- const tp = positions[indexOf[b]];
270
- edgeG.append("path")
271
- .attr("d", curvePath(sp.x, sp.y, tp.x, tp.y, false))
272
- .attr("stroke", "#777")
273
- .attr("stroke-width", strokeScale(w))
274
- .attr("fill", "none")
275
- .attr("opacity", 0.6)
276
- .attr("data-src", s)
277
- .attr("data-tgt", b);
278
- });
279
-
280
- function highlight(name) {
281
- nodeG.selectAll("circle")
282
- .style("opacity", d => d.name === name ? 1 : 0.2);
283
- nodeG.selectAll("text")
284
- .text(d => d.name === name ? FULL_LABEL[d.name] : SHORT_LABEL[d.name])
285
- .style("opacity", d => d.name === name ? 1 : 0.2);
286
-
287
- edgeG.selectAll("path")
288
- .style("opacity", function() {
289
- return (this.dataset.src === name || this.dataset.tgt === name) ? 1 : 0.05;
290
- });
291
- }
292
 
293
- function reset() {
294
- nodeG.selectAll("circle").style("opacity", 1);
295
- nodeG.selectAll("text")
296
- .text(d => SHORT_LABEL[d.name])
297
- .style("opacity", 1);
298
- edgeG.selectAll("path").style("opacity", 1);
299
- }
300
 
301
- nodeG.on("click", function(e, d) {
302
- highlight(d.name);
303
- e.stopPropagation();
304
- });
305
 
306
- svg.on("click", () => reset());
307
- }
 
308
 
309
- drawArcDiagram();
 
 
 
310
 
311
- window.addEventListener("resize", drawArcDiagram);
312
- </script>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
313
  """
314
 
315
- ###############################################################
316
- # REPLACE JS PLACEHOLDERS
317
- ###############################################################
318
-
319
- ARC_HTML = ARC_HTML.replace("__NODES__", json.dumps(NODES))
320
- ARC_HTML = ARC_HTML.replace("__NODE_TYPE__", json.dumps(NODE_TYPE))
321
- ARC_HTML = ARC_HTML.replace("__BUYS__", json.dumps([
322
- (a, c, (3 if (a in FRESH_BUY and c in FRESH_BUY[a]) else 1))
323
- for a, cs in BUY_MAP.items()
324
- for c in cs
325
- ]))
326
- ARC_HTML = ARC_HTML.replace("__SELLS__", json.dumps([
327
- (c, a, (3 if (a in COMPLETE_EXIT and c in COMPLETE_EXIT[a]) else 1))
328
- for a, cs in SELL_MAP.items()
329
- for c in cs
330
- ]))
331
- ARC_HTML = ARC_HTML.replace("__TRANSFERS__", json.dumps([
332
- (s, b, w) for (s, b), w in TRANSFERS.items()
333
- ]))
334
- ARC_HTML = ARC_HTML.replace("__SHORT_LABEL__", json.dumps(SHORT_LABEL))
335
- ARC_HTML = ARC_HTML.replace("__FULL_LABEL__", json.dumps(FULL_LABEL))
336
-
337
- ###############################################################
338
- # UI
339
- ###############################################################
340
-
341
- with gr.Blocks(title="MF Churn Arc Diagram") as demo:
342
- gr.Markdown("## Mutual Fund Churn — Arc Diagram (Full JS)")
343
- gr.HTML(ARC_HTML)
344
-
345
- gr.Markdown("### Inspect Company")
346
- dd = gr.Dropdown(COMPANIES, label="Pick a company")
347
- plot = gr.Plot()
348
- table = gr.DataFrame()
349
-
350
- dd.change(company_trade_summary, dd, [plot, table])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
351
 
352
  if __name__ == "__main__":
353
  demo.launch()
 
1
  # app.py
2
+ # Mutual Fund Churn Explorer with Gel + Wave Liquid Motion (Option D)
3
+ # D3 + Plotly hybrid layout
4
+ # Designed for Hugging Face Spaces (Gradio)
 
 
5
 
6
  import gradio as gr
7
  import pandas as pd
8
+ import networkx as nx
9
+ import plotly.graph_objects as go
10
+ import numpy as np
11
  import json
12
  from collections import defaultdict
13
 
14
+ # ============================================================
15
  # DATA
16
+ # ============================================================
17
 
18
  AMCS = [
19
  "SBI MF", "ICICI Pru MF", "HDFC MF", "Nippon India MF", "Kotak MF",
 
52
  "DSP MF": ["HAL", "Shriram Finance"]
53
  }
54
 
 
55
  COMPLETE_EXIT = {"DSP MF": ["Shriram Finance"]}
56
+ FRESH_BUY = {"HDFC MF": ["Tata Elxsi"], "UTI MF": ["Adani Ports"], "Mirae MF": ["HAL"]}
57
 
58
+ def sanitize_map(m):
59
+ out = {}
60
+ for k, vals in m.items():
61
+ out[k] = [v for v in vals if v in COMPANIES]
62
+ return out
 
 
 
 
 
 
 
 
 
 
 
 
63
 
64
+ BUY_MAP = sanitize_map(BUY_MAP)
65
+ SELL_MAP = sanitize_map(SELL_MAP)
66
+ COMPLETE_EXIT = sanitize_map(COMPLETE_EXIT)
67
+ FRESH_BUY = sanitize_map(FRESH_BUY)
68
 
69
+ # ============================================================
70
+ # GRAPH BUILDING
71
+ # ============================================================
72
 
73
+ def infer_amc_transfers(buy_map, sell_map):
 
 
74
  transfers = defaultdict(int)
75
+ comp_sellers = defaultdict(list)
76
+ comp_buyers = defaultdict(list)
77
 
78
+ for amc, comps in sell_map.items():
79
+ for c in comps:
80
+ comp_sellers[c].append(amc)
81
 
82
+ for amc, comps in buy_map.items():
83
+ for c in comps:
84
+ comp_buyers[c].append(amc)
85
 
86
+ for c in set(comp_sellers.keys()) | set(comp_buyers.keys()):
87
+ for s in comp_sellers[c]:
88
+ for b in comp_buyers[c]:
89
  transfers[(s, b)] += 1
90
 
91
+ out = []
92
+ for (s,b), w in transfers.items():
93
+ out.append((s, b, {"action":"transfer","weight":w}))
94
+ return out
95
 
96
+ transfer_edges = infer_amc_transfers(BUY_MAP, SELL_MAP)
97
+
98
+ def build_graph(include_transfers=True):
99
+ G = nx.DiGraph()
100
+
101
+ for a in AMCS:
102
+ G.add_node(a, type="amc")
103
+ for c in COMPANIES:
104
+ G.add_node(c, type="company")
105
+
106
+ # BUY/SELL edges
107
+ for amc, comps in BUY_MAP.items():
108
+ for c in comps:
109
+ G.add_edge(amc, c, weight=1, actions=["buy"])
110
+
111
+ for amc, comps in SELL_MAP.items():
112
+ for c in comps:
113
+ if G.has_edge(amc, c):
114
+ G[amc][c]["weight"] += 1
115
+ G[amc][c]["actions"].append("sell")
116
+ else:
117
+ G.add_edge(amc, c, weight=1, actions=["sell"])
118
+
119
+ # complete exits
120
+ for amc, comps in COMPLETE_EXIT.items():
121
+ for c in comps:
122
+ if G.has_edge(amc, c):
123
+ G[amc][c]["weight"] += 3
124
+ G[amc][c]["actions"].append("complete_exit")
125
+ else:
126
+ G.add_edge(amc, c, weight=3, actions=["complete_exit"])
127
+
128
+ # fresh buys
129
+ for amc, comps in FRESH_BUY.items():
130
+ for c in comps:
131
+ if G.has_edge(amc, c):
132
+ G[amc][c]["weight"] += 3
133
+ G[amc][c]["actions"].append("fresh_buy")
134
+ else:
135
+ G.add_edge(amc, c, weight=3, actions=["fresh_buy"])
136
+
137
+ # inferred transfers
138
+ if include_transfers:
139
+ for s,b,attr in transfer_edges:
140
+ if G.has_edge(s,b):
141
+ G[s][b]["weight"] += attr["weight"]
142
+ G[s][b]["actions"].append("transfer")
143
+ else:
144
+ G.add_edge(s,b, weight=attr["weight"], actions=["transfer"])
145
+
146
+ return G
147
+
148
+ # ============================================================
149
+ # BUILD FIGURE (placeholders — positions will be set by D3)
150
+ # ============================================================
151
+
152
+ def build_plotly_figure(G,
153
+ node_color_amc="#9EC5FF",
154
+ node_color_company="#FFCF9E",
155
+ edge_color_buy="#2ca02c",
156
+ edge_color_sell="#d62728",
157
+ edge_color_transfer="#888888",
158
+ edge_thickness=1.4):
159
+
160
+ node_names = []
161
+ node_x = []
162
+ node_y = []
163
+ node_colors = []
164
+ node_sizes = []
165
+
166
+ for n, d in G.nodes(data=True):
167
+ node_names.append(n)
168
+ node_x.append(0)
169
+ node_y.append(0)
170
+ if d["type"] == "amc":
171
+ node_colors.append(node_color_amc)
172
+ node_sizes.append(36)
173
+ else:
174
+ node_colors.append(node_color_company)
175
+ node_sizes.append(56)
176
+
177
+ edge_traces = []
178
+ src_idx = []
179
+ tgt_idx = []
180
+ e_colors = []
181
+ e_widths = []
182
+
183
+ for u, v, attrs in G.edges(data=True):
184
+ edge_traces.append(
185
+ go.Scatter(
186
+ x=[0,0], y=[0,0], mode="lines",
187
+ line=dict(color="#aaa", width=1),
188
+ hoverinfo="none"
189
+ )
190
+ )
191
+ src_idx.append(node_names.index(u))
192
+ tgt_idx.append(node_names.index(v))
193
+
194
+ acts = attrs.get("actions", [])
195
+ w = attrs.get("weight", 1)
196
+
197
+ if "complete_exit" in acts:
198
+ e_colors.append(edge_color_sell)
199
+ e_widths.append(edge_thickness * 3)
200
+ elif "fresh_buy" in acts:
201
+ e_colors.append(edge_color_buy)
202
+ e_widths.append(edge_thickness * 3)
203
+ elif "transfer" in acts:
204
+ e_colors.append(edge_color_transfer)
205
+ e_widths.append(edge_thickness * (1 + np.log1p(w)))
206
+ elif "sell" in acts:
207
+ e_colors.append(edge_color_sell)
208
+ e_widths.append(edge_thickness * (1 + np.log1p(w)))
209
+ else:
210
+ e_colors.append(edge_color_buy)
211
+ e_widths.append(edge_thickness * (1 + np.log1p(w)))
212
+
213
+ node_trace = go.Scatter(
214
+ x=node_x, y=node_y,
215
+ mode="markers+text",
216
+ marker=dict(color=node_colors, size=node_sizes, line=dict(width=2,color="#333")),
217
+ text=node_names,
218
+ textposition="top center",
219
+ hoverinfo="text"
220
+ )
221
+
222
+ fig = go.Figure(data=edge_traces + [node_trace])
223
+ fig.update_layout(
224
+ autosize=True,
225
+ showlegend=False,
226
+ margin=dict(l=5, r=5, t=30, b=5),
227
+ xaxis=dict(visible=False),
228
+ yaxis=dict(visible=False)
229
+ )
230
+
231
+ meta = {
232
+ "node_names": node_names,
233
+ "edge_source_index": src_idx,
234
+ "edge_target_index": tgt_idx,
235
+ "edge_colors": e_colors,
236
+ "edge_widths": e_widths,
237
+ "node_sizes": node_sizes
238
+ }
239
 
240
+ return fig, meta
 
 
241
 
 
 
 
 
 
 
 
242
 
243
+ # ============================================================
244
+ # D3 + GEL + WAVE Motion Renderer
245
+ # ============================================================
246
+
247
+ def make_network_html(fig, meta, div_id="network-plot-div"):
248
+
249
+ fig_json = json.dumps(fig.to_plotly_json())
250
+ meta_json = json.dumps(meta)
251
 
252
+ html = f"""
253
+ <div id="{div_id}" style="width:100%; height:620px;"></div>
254
+
255
+ <div style="margin-top:6px;">
256
+ <button id="{div_id}-reset" style="padding:8px 12px; border-radius:6px;">Reset</button>
257
+ <button id="{div_id}-stop" style="padding:8px 12px; margin-left:8px; border-radius:6px;">Stop Layout</button>
258
+ </div>
259
+
260
+ <script src="https://d3js.org/d3.v7.min.js"></script>
261
+ <script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
262
+
263
+ <script>
264
+ const fig = {fig_json};
265
+ const meta = {meta_json};
266
+
267
+ const container = document.getElementById("{div_id}");
268
+
269
+ Plotly.newPlot(container, fig.data, fig.layout, {{responsive:true}});
270
+
271
+ const nodeTraceIndex = fig.data.length - 1;
272
+ const edgeCount = fig.data.length - 1;
273
+
274
+ // Build nodes for D3
275
+ const nodes = meta.node_names.map((name, i) => {{
276
+ return {{
277
+ id: i,
278
+ name: name,
279
+ r: meta.node_sizes[i] || 20,
280
+ displayX: 0,
281
+ displayY: 0,
282
+ vx_smooth: 0,
283
+ vy_smooth: 0
284
+ }};
285
+ }});
286
+
287
+ // Build links
288
+ const links = meta.edge_source_index.map((s, i) => {{
289
+ return {{
290
+ source: s,
291
+ target: meta.edge_target_index[i],
292
+ color: meta.edge_colors[i],
293
+ width: meta.edge_widths[i]
294
+ }};
295
+ }});
296
+
297
+ // D3 simulation
298
+ const simulation = d3.forceSimulation(nodes)
299
+ .force("link", d3.forceLink(links).id(d => d.id).distance(150).strength(0.35))
300
+ .force("charge", d3.forceManyBody().strength(-50))
301
+ .force("collision", d3.forceCollide().radius(d => d.r * 0.9))
302
+ .force("center", d3.forceCenter(0,0))
303
+ .velocityDecay(0.55);
304
+
305
+ let tickCount = 0;
306
+ const maxTicks = 400;
307
+
308
+ simulation.on("tick", () => {{
309
+ tickCount++;
310
+
311
+ nodes.forEach(n => {{
312
+ const tx = n.x || 0;
313
+ const ty = n.y || 0;
314
+
315
+ // Gel viscosity smoothing
316
+ n.vx_smooth = n.vx_smooth * 0.82 + (tx - n.displayX) * 0.06;
317
+ n.vy_smooth = n.vy_smooth * 0.82 + (ty - n.displayY) * 0.06;
318
+
319
+ // Heavy damping
320
+ n.vx_smooth *= 0.90;
321
+ n.vy_smooth *= 0.90;
322
+
323
+ // Update display positions
324
+ n.displayX += n.vx_smooth;
325
+ n.displayY += n.vy_smooth;
326
+
327
+ // Wave pulse (gentle breathing)
328
+ const t = Date.now() * 0.001;
329
+ n.displayX += Math.sin(t + n.id) * 0.12;
330
+ n.displayY += Math.cos(t + n.id) * 0.12;
331
+ }});
332
+
333
+ // Node arrays
334
+ const xs = nodes.map(n => n.displayX);
335
+ const ys = nodes.map(n => n.displayY);
336
+
337
+ // Update node trace
338
+ Plotly.restyle(container, {{x: [xs], y: [ys]}}, [nodeTraceIndex]);
339
+
340
+ // Update edges
341
+ for (let e = 0; e < edgeCount; e++) {{
342
+ const s = meta.edge_source_index[e];
343
+ const t = meta.edge_target_index[e];
344
+
345
+ Plotly.restyle(container,
346
+ {{
347
+ x: [[nodes[s].displayX, nodes[t].displayX]],
348
+ y: [[nodes[s].displayY, nodes[t].displayY]],
349
+ "line.color": [meta.edge_colors[e]],
350
+ "line.width": [meta.edge_widths[e]]
351
+ }},
352
+ [e]
353
+ );
354
+ }}
355
+
356
+ if (simulation.alpha() < 0.02 || tickCount > maxTicks) {{
357
+ simulation.stop();
358
+ }}
359
+ }});
360
+
361
+ // STOP button
362
+ document.getElementById("{div_id}-stop").addEventListener("click", () => {{
363
+ simulation.stop();
364
+ }});
365
+
366
+ // FOCUS and RESET
367
+ const nameToIndex = {{}};
368
+ meta.node_names.forEach((n,i)=> nameToIndex[n] = i);
369
+
370
+ function focusNode(name) {{
371
+ const idx = nameToIndex[name];
372
+ const keep = new Set([idx]);
373
+
374
+ for (let e = 0; e < meta.edge_source_index.length; e++) {{
375
+ const s = meta.edge_source_index[e], t = meta.edge_target_index[e];
376
+ if (s === idx) keep.add(t);
377
+ if (t === idx) keep.add(s);
378
+ }}
379
+
380
+ const N = meta.node_names.length;
381
+ const op = Array(N).fill(0);
382
+ const colors = Array(N).fill("rgba(0,0,0,0)");
383
+
384
+ for (let i = 0; i < N; i++) {{
385
+ if (keep.has(i)) {{
386
+ op[i] = 1;
387
+ colors[i] = "black";
388
+ }}
389
+ }}
390
+
391
+ Plotly.restyle(container, {{
392
+ "marker.opacity": [op],
393
+ "textfont.color": [colors]
394
+ }}, [nodeTraceIndex]);
395
+
396
+ for (let e = 0; e < edgeCount; e++) {{
397
+ const s = meta.edge_source_index[e], t = meta.edge_target_index[e];
398
+ const show = keep.has(s) && keep.has(t);
399
+
400
+ Plotly.restyle(container, {{
401
+ "line.color": [show ? meta.edge_colors[e] : "rgba(0,0,0,0)"],
402
+ "line.width": [show ? meta.edge_widths[e] : 0.1]
403
+ }}, [e]);
404
+ }}
405
+ }}
406
+
407
+ function resetView() {{
408
+ const N = meta.node_names.length;
409
+ Plotly.restyle(container, {{
410
+ "marker.opacity": [Array(N).fill(1)],
411
+ "textfont.color": [Array(N).fill("black")]
412
+ }}, [nodeTraceIndex]);
413
+
414
+ for (let e = 0; e < edgeCount; e++) {{
415
+ Plotly.restyle(container, {{
416
+ "line.color": [meta.edge_colors[e]],
417
+ "line.width": [meta.edge_widths[e]]
418
+ }}, [e]);
419
+ }}
420
+
421
+ simulation.alpha(0.5);
422
+ simulation.restart();
423
+ }}
424
+
425
+ document.getElementById("{div_id}-reset").addEventListener("click", resetView);
426
+
427
+ container.on("plotly_click", (e) => {{
428
+ const p = e.points[0];
429
+ if (p && p.curveNumber === nodeTraceIndex) {{
430
+ const name = meta.node_names[p.pointNumber];
431
+ focusNode(name);
432
+ }}
433
+ }});
434
+ </script>
435
+ """
436
+
437
+ return html
438
+
439
+ # ============================================================
440
+ # COMPANY / AMC SUMMARY
441
+ # ============================================================
442
 
443
  def company_trade_summary(company):
444
  buyers = [a for a, cs in BUY_MAP.items() if company in cs]
445
  sellers = [a for a, cs in SELL_MAP.items() if company in cs]
446
+ fresh = [a for a, cs in FRESH_BUY.items() if company in cs]
447
+ exits = [a for a, cs in COMPLETE_EXIT.items() if company in cs]
448
 
449
  df = pd.DataFrame({
450
+ "Role": ["Buyer"] * len(buyers)
451
+ + ["Seller"] * len(sellers)
452
+ + ["Fresh buy"] * len(fresh)
453
+ + ["Complete exit"] * len(exits),
454
+ "AMC": buyers + sellers + fresh + exits
455
  })
456
 
457
  if df.empty:
458
+ return None, pd.DataFrame([], columns=["Role","AMC"])
459
+
460
+ counts = df.groupby("Role").size().reset_index(name="Count")
461
+
462
+ fig = go.Figure(go.Bar(
463
+ x=counts["Role"], y=counts["Count"],
464
+ marker_color=["green","red","orange","black"][:len(counts)]
465
+ ))
466
+ fig.update_layout(title=f"Trades for {company}", margin=dict(t=30,b=5))
467
 
 
 
 
 
468
  return fig, df
469
 
 
 
 
470
 
471
+ def amc_transfer_summary(amc):
472
+ sold = SELL_MAP.get(amc, [])
473
+ transfers = []
474
+ for s in sold:
475
+ buyers = [a for a, cs in BUY_MAP.items() if s in cs]
476
+ for b in buyers:
477
+ transfers.append({"security": s, "buyer_amc": b})
478
 
479
+ df = pd.DataFrame(transfers)
 
 
 
 
 
 
 
480
 
481
+ if df.empty:
482
+ return None, pd.DataFrame([], columns=["security","buyer_amc"])
483
 
484
+ counts = df["buyer_amc"].value_counts().reset_index()
485
+ counts.columns = ["Buyer AMC","Count"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
486
 
487
+ fig = go.Figure(go.Bar(
488
+ x=counts["Buyer AMC"], y=counts["Count"],
489
+ marker_color="gray"
490
+ ))
491
+ fig.update_layout(title=f"Inferred transfers from {amc}", margin=dict(t=30,b=5))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
492
 
493
+ return fig, df
 
 
 
 
 
 
494
 
 
 
 
 
495
 
496
+ # ============================================================
497
+ # FINAL NETWORK HTML BUILDER
498
+ # ============================================================
499
 
500
+ def build_network_html(node_color_company="#FFCF9E", node_color_amc="#9EC5FF",
501
+ edge_color_buy="#2ca02c", edge_color_sell="#d62728",
502
+ edge_color_transfer="#888888", edge_thickness=1.4,
503
+ include_transfers=True):
504
 
505
+ G = build_graph(include_transfers=include_transfers)
506
+ fig, meta = build_plotly_figure(
507
+ G,
508
+ node_color_amc=node_color_amc,
509
+ node_color_company=node_color_company,
510
+ edge_color_buy=edge_color_buy,
511
+ edge_color_sell=edge_color_sell,
512
+ edge_color_transfer=edge_color_transfer,
513
+ edge_thickness=edge_thickness
514
+ )
515
+ return make_network_html(fig, meta)
516
+
517
+ initial_html = build_network_html()
518
+
519
+
520
+ # ============================================================
521
+ # UI LAYOUT
522
+ # ============================================================
523
+
524
+ responsive_css = """
525
+ .js-plotly-plot { height:620px !important; }
526
+ @media(max-width:780px){ .js-plotly-plot{ height:600px !important; } }
527
  """
528
 
529
+ with gr.Blocks(css=responsive_css, title="MF Churn Explorer — Liquid Motion") as demo:
530
+
531
+ gr.Markdown("## Mutual Fund Churn Explorer — Liquid Gel + Wave Motion (L2 + Rhythm)")
532
+
533
+ network_html = gr.HTML(value=initial_html)
534
+
535
+ legend_html = gr.HTML("""
536
+ <div style='font-family:sans-serif;font-size:14px;margin-top:10px;line-height:1.6;'>
537
+ <b>Legend</b><br>
538
+ <div><span style="display:inline-block;width:28px;border-bottom:3px solid #2ca02c;"></span> BUY (green solid)</div>
539
+ <div><span style="display:inline-block;width:28px;border-bottom:3px dotted #d62728;"></span> SELL (red dotted)</div>
540
+ <div><span style="display:inline-block;width:28px;border-bottom:3px dashed #888;"></span> TRANSFER (grey dashed — inferred)</div>
541
+ <div><span style="display:inline-block;width:28px;border-bottom:5px solid #2ca02c;"></span> FRESH BUY</div>
542
+ <div><span style="display:inline-block;width:28px;border-bottom:5px solid #d62728;"></span> COMPLETE EXIT</div>
543
+ </div>
544
+ """)
545
+
546
+ with gr.Accordion("Customize Network", open=False):
547
+ node_color_company = gr.ColorPicker("#FFCF9E", label="Company node color")
548
+ node_color_amc = gr.ColorPicker("#9EC5FF", label="AMC node color")
549
+ edge_color_buy = gr.ColorPicker("#2ca02c", label="BUY edge color")
550
+ edge_color_sell = gr.ColorPicker("#d62728", label="SELL edge color")
551
+ edge_color_transfer = gr.ColorPicker("#888888", label="Transfer edge color")
552
+ edge_thickness = gr.Slider(0.5, 6.0, 1.4, step=0.1, label="Edge thickness")
553
+ include_transfers = gr.Checkbox(True, label="Show inferred AMC→AMC transfers")
554
+ update_btn = gr.Button("Update Graph")
555
+
556
+ gr.Markdown("### Company Summary")
557
+ select_company = gr.Dropdown(choices=COMPANIES, label="Select company")
558
+ company_plot = gr.Plot()
559
+ company_table = gr.DataFrame()
560
+
561
+ gr.Markdown("### AMC Summary (Inferred Transfers)")
562
+ select_amc = gr.Dropdown(choices=AMCS, label="Select AMC")
563
+ amc_plot = gr.Plot()
564
+ amc_table = gr.DataFrame()
565
+
566
+ # Callbacks
567
+ def update_net(c1,c2,buy,sell,trans,thick,inc):
568
+ return build_network_html(
569
+ node_color_company=c1,
570
+ node_color_amc=c2,
571
+ edge_color_buy=buy,
572
+ edge_color_sell=sell,
573
+ edge_color_transfer=trans,
574
+ edge_thickness=thick,
575
+ include_transfers=inc
576
+ )
577
+
578
+ update_btn.click(
579
+ update_net,
580
+ inputs=[node_color_company,node_color_amc,
581
+ edge_color_buy,edge_color_sell,edge_color_transfer,
582
+ edge_thickness,include_transfers],
583
+ outputs=[network_html]
584
+ )
585
+
586
+ def on_company(c):
587
+ fig,df = company_trade_summary(c)
588
+ return fig,df
589
+
590
+ def on_amc(a):
591
+ fig,df = amc_transfer_summary(a)
592
+ return fig,df
593
+
594
+ select_company.change(on_company, inputs=[select_company], outputs=[company_plot,company_table])
595
+ select_amc.change(on_amc, inputs=[select_amc], outputs=[amc_plot,amc_table])
596
+
597
 
598
  if __name__ == "__main__":
599
  demo.launch()