singhn9 commited on
Commit
7c3a5ad
·
verified ·
1 Parent(s): 663b7d1

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +329 -279
app.py CHANGED
@@ -1,29 +1,45 @@
1
  # app.py
2
- # Final gated Arc Diagram app
3
- # - Email + Code gateway (code: CHATGPTROCKS)
4
- # - Logs success & failures to session.log
5
- # - Radial outward full labels on click + connected nodes show full labels
6
- # - High-contrast colors, legend restored
7
- # - No JS f-string escaping issues (JS injected via .replace)
8
 
9
  import gradio as gr
10
- import pandas as pd
11
  import json
12
  import time
13
- import os
14
  from collections import defaultdict
15
 
16
- # ---------------------------
17
- # Config
18
- # ---------------------------
19
- import os
20
- ACCESS_CODE = os.getenv("ACCESS_CODE", "")
21
- LOG_FILE = "session.log" # will be created/used in repo root
22
- os.makedirs(os.path.dirname(LOG_FILE) or ".", exist_ok=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
 
24
- # ---------------------------
25
- # Data (same source)
26
- # ---------------------------
27
  AMCS = [
28
  "SBI MF", "ICICI Pru MF", "HDFC MF", "Nippon India MF", "Kotak MF",
29
  "UTI MF", "Axis MF", "Aditya Birla SL MF", "Mirae MF", "DSP MF"
@@ -46,7 +62,7 @@ BUY_MAP = {
46
  "Aditya Birla SL MF": ["AU Small Finance Bank"],
47
  "Mirae MF": ["Bajaj Finance", "HAL"],
48
  "DSP MF": ["Tata Motors", "Bajaj Finserv"]
49
- }
50
 
51
  SELL_MAP = {
52
  "SBI MF": ["Tata Motors"],
@@ -64,20 +80,20 @@ SELL_MAP = {
64
  COMPLETE_EXIT = {"DSP MF": ["Shriram Finance"]}
65
  FRESH_BUY = {"HDFC MF": ["Tata Elxsi"], "UTI MF": ["Adani Ports"], "Mirae MF": ["HAL"]}
66
 
 
67
  def sanitize_map(m):
68
- out = {}
69
  for k, vals in m.items():
70
- out[k] = [v for v in vals if v in COMPANIES]
71
- return out
 
72
 
73
  BUY_MAP = sanitize_map(BUY_MAP)
74
  SELL_MAP = sanitize_map(SELL_MAP)
75
  COMPLETE_EXIT = sanitize_map(COMPLETE_EXIT)
76
  FRESH_BUY = sanitize_map(FRESH_BUY)
77
 
78
- # ---------------------------
79
- # Short + full labels
80
- # ---------------------------
81
  SHORT_LABELS = {
82
  "SBI MF": "SBI", "ICICI Pru MF": "ICICI", "HDFC MF": "HDFC",
83
  "Nippon India MF": "NIPP", "Kotak MF": "KOT", "UTI MF": "UTI",
@@ -92,114 +108,145 @@ SHORT_LABELS = {
92
  "Hindalco": "Hind", "Tata Elxsi": "Elxsi",
93
  "Cummins India": "Cumm", "Vedanta": "Ved"
94
  }
95
- FULL_LABEL = {k: k for k in SHORT_LABELS}
96
 
97
- # ---------------------------
98
- # Infer AMC→AMC transfers
99
- # ---------------------------
100
- def infer_amc_transfers(buy_map, sell_map):
101
- transfers = defaultdict(int)
 
102
  c2s = defaultdict(list)
103
  c2b = defaultdict(list)
 
 
104
  for amc, comps in sell_map.items():
105
- for c in comps:
106
- c2s[c].append(amc)
107
  for amc, comps in buy_map.items():
108
- for c in comps:
109
- c2b[c].append(amc)
110
  for c in set(c2s) | set(c2b):
111
  for s in c2s[c]:
112
  for b in c2b[c]:
113
  transfers[(s, b)] += 1
 
114
  return transfers
115
 
116
- TRANSFER_COUNTS = infer_amc_transfers(BUY_MAP, SELL_MAP)
117
 
118
- # ---------------------------
119
- # Mixed ordering
120
- # ---------------------------
121
- def build_mixed_ordering(amcs, companies):
122
- mixed = []
123
- n = max(len(amcs), len(companies))
124
- for i in range(n):
125
- if i < len(amcs): mixed.append(amcs[i])
126
- if i < len(companies): mixed.append(companies[i])
127
- return mixed
128
 
129
- NODES = build_mixed_ordering(AMCS, COMPANIES)
 
 
 
 
 
 
 
 
 
 
130
  NODE_TYPE = {n: ("amc" if n in AMCS else "company") for n in NODES}
131
 
132
- # ---------------------------
133
- # Build flows
134
- # ---------------------------
135
  def build_flows():
136
- buys, sells, transfers, loops = [], [], [], []
 
 
 
 
 
137
  for amc, comps in BUY_MAP.items():
138
  for c in comps:
139
  w = 3 if c in FRESH_BUY.get(amc, []) else 1
140
  buys.append((amc, c, w))
 
 
141
  for amc, comps in SELL_MAP.items():
142
  for c in comps:
143
  w = 3 if c in COMPLETE_EXIT.get(amc, []) else 1
144
  sells.append((c, amc, w))
145
- for (s, b), w in TRANSFER_COUNTS.items():
 
 
146
  transfers.append((s, b, w))
147
- seen = set()
 
148
  for a, c, _ in buys:
149
  for c2, b, _ in sells:
150
  if c == c2:
151
- seen.add((a, c, b))
152
- loops = list(seen)
153
- return buys, sells, transfers, loops
154
 
155
- BUYS, SELLS, TRANSFERS, LOOPS = build_flows()
156
 
157
- # ---------------------------
158
- # Logging helper
159
- # ---------------------------
160
- def write_log(timestamp, email, code, ua, ok):
161
- # mask code in logs for security? we'll log full code as requested; you can mask if needed
162
- status = "OK" if ok else "FAIL"
163
- line = f"{timestamp} | {status} | email: {email} | code: {code} | ua: {ua}\n"
164
- try:
165
- with open(LOG_FILE, "a") as f:
166
- f.write(line)
167
- except Exception as e:
168
- print("Log write failed:", e)
169
 
170
- # ---------------------------
171
- # Gate callback
172
- # ---------------------------
173
- def check_access(email, code, ua):
174
- now = time.strftime("%Y-%m-%d %H:%M:%S")
175
- if (not email) or (not code):
176
- write_log(now, email or "<empty>", code or "<empty>", ua or "<ua?>", False)
177
- return gr.update(visible=True), gr.update(visible=False), "Please provide both email and code."
178
- if code.strip() == ACCESS_CODE:
179
- # success: log and reveal app
180
- write_log(now, email, code, ua or "<ua?>", True)
181
- return gr.update(visible=False), gr.update(visible=True), f"Access granted for {email}."
182
- else:
183
- write_log(now, email, code, ua or "<ua?>", False)
184
- return gr.update(visible=True), gr.update(visible=False), "Invalid code."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
185
 
186
- # ---------------------------
187
- # JS Template for arc diagram
188
- # (raw string; placeholders replaced with .replace())
189
- # ---------------------------
190
- JS_TEMPLATE = r"""
191
- <div id="arc-container" style="width:100%; height:720px;"></div>
192
- <div style="margin-top:8px;">
193
- <button id="arc-reset" style="padding:8px 12px; border-radius:6px;">Reset</button>
194
- </div>
195
 
196
- <div style="margin-top:10px; font-family:sans-serif; font-size:13px;">
197
- <b>Legend</b><br/>
198
- <span style="display:inline-block;width:12px;height:8px;background:#1a9850;margin-right:6px;"></span> BUY (green solid)<br/>
199
- <span style="display:inline-block;width:12px;height:8px;background:#d73027;margin-right:6px;border-bottom:3px dotted #d73027;"></span> SELL (red dotted)<br/>
200
- <span style="display:inline-block;width:12px;height:8px;background:#636363;margin-right:6px;"></span> TRANSFER (grey, inferred)<br/>
201
- <span style="display:inline-block;width:12px;height:8px;background:#1f9e89;margin-right:6px;"></span> LOOP (teal external arc)<br/>
202
- <div style="margin-top:6px;color:#666;font-size:12px;">Click node to expand full label and show connected nodes' labels (radial outward).</div>
 
 
 
 
 
 
 
203
  </div>
204
 
205
  <script src="https://d3js.org/d3.v7.min.js"></script>
@@ -210,285 +257,288 @@ const BUYS = __BUYS__;
210
  const SELLS = __SELLS__;
211
  const TRANSFERS = __TRANSFERS__;
212
  const LOOPS = __LOOPS__;
 
 
213
 
214
- const SHORT_LABEL_JS = __SHORT_LABEL__;
215
- const FULL_LABEL_JS = __FULL_LABEL__;
216
-
217
- // draw routine
218
- function draw() {
219
  const container = document.getElementById("arc-container");
220
  container.innerHTML = "";
221
- const w = Math.min(980, container.clientWidth || 860);
222
- const h = Math.max(540, Math.floor(w * 0.75));
223
  const svg = d3.select(container).append("svg")
224
  .attr("width","100%")
225
- .attr("height",h)
226
- .attr("viewBox",[-w/2,-h/2,w,h].join(" "));
227
 
228
- const radius = Math.min(w,h)*0.36;
229
  const n = NODES.length;
230
  function angle(i){ return (i/n)*2*Math.PI; }
231
 
232
  const pos = NODES.map((name,i)=>{
233
- const ang = angle(i) - Math.PI/2;
234
  return { name, angle: ang, x: Math.cos(ang)*radius, y: Math.sin(ang)*radius };
235
  });
236
 
237
  const index = {};
238
- NODES.forEach((nm,i)=> index[nm]=i);
239
 
240
- // group for nodes
241
- const g = svg.append("g").selectAll("g")
242
  .data(pos).enter().append("g")
243
  .attr("transform", d => `translate(${d.x},${d.y})`);
244
 
245
- g.append("circle")
246
- .attr("r", 16)
247
- .style("fill", d => NODE_TYPE[d.name] === "amc" ? "#003f5c" : "#f59e0b")
248
- .style("stroke", "#111")
249
- .style("stroke-width", 1)
250
  .style("cursor","pointer");
251
 
252
- // short labels inside
253
- g.append("text")
254
- .attr("dy", "0.35em")
255
- .style("font-size", "10px")
256
- .style("fill", "#fff")
257
- .style("text-anchor", "middle")
258
- .style("pointer-events", "none")
259
- .text(d => SHORT_LABEL_JS[d.name] || d.name);
260
-
261
- // helper bezier
262
- function arcPath(x0,y0,x1,y1,above=true){
263
  const mx=(x0+x1)/2, my=(y0+y1)/2;
264
- const len=Math.sqrt(mx*mx+my*my)||1;
265
- const ux=mx/len, uy=my/len;
266
- const offset=(above?-1:1)*Math.max(36, radius*0.9);
 
267
  const cx=mx+ux*offset, cy=my+uy*offset;
268
  return `M ${x0} ${y0} Q ${cx} ${cy} ${x1} ${y1}`;
269
  }
270
 
271
- const allW = [].concat(BUYS.map(d=>d[2]), SELLS.map(d=>d[2]), TRANSFERS.map(d=>d[2]));
 
 
 
 
272
  const sw = d3.scaleLinear().domain([1, Math.max(...allW,1)]).range([1.2,6]);
273
 
274
- // BUY arcs (top)
275
  const buyG = svg.append("g");
276
  BUYS.forEach(([a,c,w])=>{
277
- if(!(a in index) || !(c in index)) return;
278
- const s = pos[index[a]], t = pos[index[c]];
279
  buyG.append("path")
280
- .attr("d", arcPath(s.x,s.y,t.x,t.y,true))
281
  .attr("stroke","#1a9850")
282
  .attr("fill","none")
283
- .attr("stroke-width", sw(w))
284
- .attr("data-src", a)
285
- .attr("data-tgt", c)
286
- .attr("opacity", 0.92);
287
  });
288
 
289
- // SELL arcs (bottom)
290
  const sellG = svg.append("g");
291
  SELLS.forEach(([c,a,w])=>{
292
- if(!(c in index) || !(a in index)) return;
293
- const s = pos[index[c]], t = pos[index[a]];
294
  sellG.append("path")
295
- .attr("d", arcPath(s.x,s.y,t.x,t.y,false))
296
  .attr("stroke","#d73027")
 
297
  .attr("fill","none")
298
- .attr("stroke-dasharray","4,3")
299
- .attr("stroke-width", sw(w))
300
- .attr("data-src", c)
301
- .attr("data-tgt", a)
302
- .attr("opacity", 0.86);
303
  });
304
 
305
- // TRANSFERS grey chords
306
  const trG = svg.append("g");
307
  TRANSFERS.forEach(([s,b,w])=>{
308
- if(!(s in index) || !(b in index)) return;
309
- const sp = pos[index[s]], tp = pos[index[b]];
310
- const mx = (sp.x + tp.x)/2, my = (sp.y + tp.y)/2;
311
- const path = `M ${sp.x} ${sp.y} Q ${mx*0.3} ${my*0.3} ${tp.x} ${tp.y}`;
312
  trG.append("path")
313
- .attr("d", path)
314
- .attr("stroke", "#636363")
 
315
  .attr("fill","none")
316
- .attr("stroke-width", sw(w))
317
- .attr("opacity", 0.7)
318
- .attr("data-src", s)
319
- .attr("data-tgt", b);
320
  });
321
 
322
- // LOOPS outside
323
  const loopG = svg.append("g");
324
  LOOPS.forEach(([a,c,b])=>{
325
- if(!(a in index) || !(b in index)) return;
326
- const sa = pos[index[a]], sb = pos[index[b]];
327
- const mx = (sa.x + sb.x)/2, my=(sa.y+sb.y)/2;
328
- const len = Math.sqrt((sa.x-sb.x)**2 + (sa.y-sb.y)**2);
329
- const outward = Math.max(40, radius*0.28 + len*0.12);
330
- const ndx = mx, ndy = my;
331
- const nlen = Math.sqrt(ndx*ndx + ndy*ndy) || 1;
332
- const ux = ndx/nlen, uy = ndy/nlen;
333
- const cx = mx + ux*outward, cy = my + uy*outward;
334
- const path = `M ${sa.x} ${sa.y} Q ${cx} ${cy} ${sb.x} ${sb.y}`;
335
  loopG.append("path")
336
- .attr("d", path)
337
- .attr("stroke", "#1f9e89")
338
  .attr("fill","none")
339
- .attr("stroke-width", 2.8)
340
- .attr("opacity", 0.95);
341
  });
342
 
343
- // outside label layer (initially empty)
344
- const outsideLabelLayer = svg.append("g").attr("class","outside-labels");
345
-
346
- // helper to place radial outside labels
347
- function placeOutsideLabels(names){
348
- outsideLabelLayer.selectAll("*").remove();
349
- names.forEach(nm => {
350
- const p = pos[index[nm]];
351
- const offset = radius + 30;
352
- const x = p.x + Math.cos(p.angle) * offset;
353
- const y = p.y + Math.sin(p.angle) * offset;
354
- const deg = (p.angle * 180 / Math.PI);
355
- const anchor = (deg > -90 && deg < 90) ? "start" : "end";
356
- outsideLabelLayer.append("text")
357
- .attr("x", x)
358
- .attr("y", y)
359
- .attr("dy", "0.35em")
360
- .text(FULL_LABEL_JS[nm] || nm)
361
- .style("font-family","sans-serif")
362
  .style("font-size","12px")
363
- .style("fill","#0b1220")
364
- .style("text-anchor", anchor)
365
- .style("background","white");
366
  });
367
  }
368
 
369
- // highlight behavior: show full label for node and its connected nodes
370
- function highlight(nodeName){
371
- // dim non-related nodes
372
- g.selectAll("circle").style("opacity", d => d.name === nodeName ? 1.0 : 0.18);
373
- // set inside short labels to less prominent
374
- g.selectAll("text").style("opacity", d => d.name === nodeName ? 0.0 : 0.28);
 
 
 
 
 
 
 
 
 
375
 
376
- // find connected nodes (sources/targets)
377
- const connected = new Set([nodeName]);
378
  buyG.selectAll("path").each(function(){
379
- const s = this.getAttribute("data-src"), t = this.getAttribute("data-tgt");
380
- if(s===nodeName || t===nodeName){ connected.add(s); connected.add(t); }
381
  });
382
  sellG.selectAll("path").each(function(){
383
- const s = this.getAttribute("data-src"), t = this.getAttribute("data-tgt");
384
- if(s===nodeName || t===nodeName){ connected.add(s); connected.add(t); }
385
  });
386
  trG.selectAll("path").each(function(){
387
- const s = this.getAttribute("data-src"), t = this.getAttribute("data-tgt");
388
- if(s===nodeName || t===nodeName){ connected.add(s); connected.add(t); }
389
  });
390
 
391
- // show outside labels for all connected nodes (including clicked)
392
- placeOutsideLabels(Array.from(connected));
393
 
394
- // fade arcs except the relevant ones
395
- buyG.selectAll("path").style("opacity", function(){
396
- const s = this.getAttribute("data-src"), t = this.getAttribute("data-tgt");
397
- return (s===nodeName || t===nodeName) ? 0.98 : 0.06;
398
  });
399
- sellG.selectAll("path").style("opacity", function(){
400
- const s = this.getAttribute("data-src"), t = this.getAttribute("data-tgt");
401
- return (s===nodeName || t===nodeName) ? 0.98 : 0.06;
402
  });
403
- trG.selectAll("path").style("opacity", function(){
404
- const s = this.getAttribute("data-src"), t = this.getAttribute("data-tgt");
405
- return (s===nodeName || t===nodeName) ? 0.98 : 0.06;
406
  });
407
  }
408
 
409
- function reset(){
410
- g.selectAll("circle").style("opacity",1.0);
411
- g.selectAll("text").style("opacity",1.0).text(d => SHORT_LABEL_JS[d.name] || d.name);
412
- buyG.selectAll("path").style("opacity",0.92);
413
- sellG.selectAll("path").style("opacity",0.86);
414
- trG.selectAll("path").style("opacity",0.7);
415
- loopG.selectAll("path").style("opacity",0.95);
416
- outsideLabelLayer.selectAll("*").remove();
417
- }
418
 
419
- g.selectAll("circle").on("click", function(e,d){ highlight(d.name); e.stopPropagation(); });
420
- g.selectAll("text").on("click", function(e,d){ highlight(d.name); e.stopPropagation(); });
421
 
422
- document.getElementById("arc-reset").onclick = reset;
423
- svg.on("click", function(){ reset(); });
424
  }
425
 
426
- // initial draw & responsive
427
  draw();
428
  window.addEventListener("resize", draw);
429
  </script>
430
  """
431
 
432
- # ---------------------------
433
- # make html with replacements
434
- # ---------------------------
 
 
435
  def make_arc_html():
436
  html = JS_TEMPLATE
437
  html = html.replace("__NODES__", json.dumps(NODES))
438
  html = html.replace("__NODE_TYPE__", json.dumps(NODE_TYPE))
439
  html = html.replace("__BUYS__", json.dumps(BUYS))
440
  html = html.replace("__SELLS__", json.dumps(SELLS))
441
- html = html.replace("__TRANSFERS__", json.dumps(TRANSFERS))
442
  html = html.replace("__LOOPS__", json.dumps(LOOPS))
443
  html = html.replace("__SHORT_LABEL__", json.dumps(SHORT_LABELS))
444
- html = html.replace("__FULL_LABEL__", json.dumps(FULL_LABEL))
445
  return html
446
 
 
447
  ARC_HTML = make_arc_html()
448
 
449
- # ---------------------------
450
- # Gradio UI with gate
451
- # ---------------------------
452
- with gr.Blocks(title="MF Churn — Gated Arc Diagram") as demo:
453
- gr.Markdown("## Mutual Fund Churn — Access Controlled")
454
- # Gate container
455
- with gr.Row(visible=True) as gate_row:
456
- with gr.Column():
457
- gr.Markdown("**Enter your email and access code**")
458
- email_input = gr.Textbox(label="Email", placeholder="you@example.com")
459
- code_input = gr.Textbox(label="Access code", placeholder="Enter access code")
460
- ua_hidden = gr.Textbox(value="", visible=False, elem_id="ua_field")
461
- submit_btn = gr.Button("Unlock")
462
- gate_status = gr.Markdown("")
463
- # small JS to capture user-agent into hidden field
464
- gr.HTML("<script>document.getElementById('ua_field').value = navigator.userAgent || ''; </script>")
465
-
466
- # App container hidden until auth
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
467
  with gr.Column(visible=False) as app_col:
468
- gr.Markdown("### Diagram (click a node to expand labels)")
469
- arc_html = gr.HTML(ARC_HTML)
 
470
  gr.Markdown("---")
471
- gr.Markdown("### Inspect Company / AMC")
472
- select_company = gr.Dropdown(choices=COMPANIES, label="Select company")
473
  company_plot = gr.Plot()
474
  company_table = gr.DataFrame()
475
- select_amc = gr.Dropdown(choices=AMCS, label="Select AMC")
 
 
476
  amc_plot = gr.Plot()
477
  amc_table = gr.DataFrame()
478
 
479
- # Wire up gate
480
- submit_btn.click(fn=check_access,
481
- inputs=[email_input, code_input, ua_hidden],
482
- outputs=[gate_row, app_col, gate_status])
483
-
484
- # inspectors unchanged
485
- def company_cb(c):
486
- return company_trade_summary(c) if c else (None, pd.DataFrame([], columns=["Role","AMC"]))
487
- def amc_cb(a):
488
- return amc_transfer_summary(a) if a else (None, pd.DataFrame([], columns=["security","buyer_amc"]))
489
 
490
- select_company.change(fn=company_cb, inputs=[select_company], outputs=[company_plot, company_table])
491
- select_amc.change(fn=amc_cb, inputs=[select_amc], outputs=[amc_plot, amc_table])
492
 
493
  if __name__ == "__main__":
494
  demo.launch()
 
1
  # app.py
2
+ # Gated MF Arc Diagram — Final working version
3
+ # - Email + Access Code gate
4
+ # - ACCESS_CODE loaded from HF SECRETS
5
+ # - session.log in root (success + fail, masked code)
6
+ # - Fully working arc diagram (no JS issues)
 
7
 
8
  import gradio as gr
9
+ import os
10
  import json
11
  import time
12
+ import pandas as pd
13
  from collections import defaultdict
14
 
15
+ # ============================================================
16
+ # CONFIG
17
+ # ============================================================
18
+ ACCESS_CODE = os.getenv("ACCESS_CODE", "") # From HF Secrets
19
+ LOG_FILE = "session.log" # writes in repo root
20
+ if not os.path.exists(LOG_FILE):
21
+ open(LOG_FILE, "w").close() # create empty file if missing
22
+
23
+
24
+ def log_event(email, code, ua, ok):
25
+ """Append login attempt to session.log (masked code for safety)."""
26
+ timestamp = time.strftime("%Y-%m-%d %H:%M:%S")
27
+ status = "OK" if ok else "FAIL"
28
+ safe_code = code[:2] + "***" if code else "<empty>"
29
+ ua = ua or "<ua?>"
30
+
31
+ line = f"{timestamp} | {status} | email={email} | code={safe_code} | ua={ua}\n"
32
+ try:
33
+ with open(LOG_FILE, "a") as f:
34
+ f.write(line)
35
+ except Exception as e:
36
+ print("Logging failed:", e)
37
+
38
+
39
+ # ============================================================
40
+ # DATA
41
+ # ============================================================
42
 
 
 
 
43
  AMCS = [
44
  "SBI MF", "ICICI Pru MF", "HDFC MF", "Nippon India MF", "Kotak MF",
45
  "UTI MF", "Axis MF", "Aditya Birla SL MF", "Mirae MF", "DSP MF"
 
62
  "Aditya Birla SL MF": ["AU Small Finance Bank"],
63
  "Mirae MF": ["Bajaj Finance", "HAL"],
64
  "DSP MF": ["Tata Motors", "Bajaj Finserv"]
65
+ ]
66
 
67
  SELL_MAP = {
68
  "SBI MF": ["Tata Motors"],
 
80
  COMPLETE_EXIT = {"DSP MF": ["Shriram Finance"]}
81
  FRESH_BUY = {"HDFC MF": ["Tata Elxsi"], "UTI MF": ["Adani Ports"], "Mirae MF": ["HAL"]}
82
 
83
+
84
  def sanitize_map(m):
85
+ sanitized = {}
86
  for k, vals in m.items():
87
+ sanitized[k] = [v for v in vals if v in COMPANIES]
88
+ return sanitized
89
+
90
 
91
  BUY_MAP = sanitize_map(BUY_MAP)
92
  SELL_MAP = sanitize_map(SELL_MAP)
93
  COMPLETE_EXIT = sanitize_map(COMPLETE_EXIT)
94
  FRESH_BUY = sanitize_map(FRESH_BUY)
95
 
96
+ # short and full labels
 
 
97
  SHORT_LABELS = {
98
  "SBI MF": "SBI", "ICICI Pru MF": "ICICI", "HDFC MF": "HDFC",
99
  "Nippon India MF": "NIPP", "Kotak MF": "KOT", "UTI MF": "UTI",
 
108
  "Hindalco": "Hind", "Tata Elxsi": "Elxsi",
109
  "Cummins India": "Cumm", "Vedanta": "Ved"
110
  }
111
+ FULL_LABELS = {k: k for k in SHORT_LABELS}
112
 
113
+
114
+ # ============================================================
115
+ # TRANSFERS + LOOPS
116
+ # ============================================================
117
+
118
+ def infer_transfers(buy_map, sell_map):
119
  c2s = defaultdict(list)
120
  c2b = defaultdict(list)
121
+ transfers = defaultdict(int)
122
+
123
  for amc, comps in sell_map.items():
124
+ for c in comps: c2s[c].append(amc)
125
+
126
  for amc, comps in buy_map.items():
127
+ for c in comps: c2b[c].append(amc)
128
+
129
  for c in set(c2s) | set(c2b):
130
  for s in c2s[c]:
131
  for b in c2b[c]:
132
  transfers[(s, b)] += 1
133
+
134
  return transfers
135
 
 
136
 
137
+ TRANSFERS = infer_transfers(BUY_MAP, SELL_MAP)
 
 
 
 
 
 
 
 
 
138
 
139
+ # For arc ordering
140
+ def build_order(amcs, companies):
141
+ seq = []
142
+ m = max(len(amcs), len(companies))
143
+ for i in range(m):
144
+ if i < len(amcs): seq.append(amcs[i])
145
+ if i < len(companies): seq.append(companies[i])
146
+ return seq
147
+
148
+
149
+ NODES = build_order(AMCS, COMPANIES)
150
  NODE_TYPE = {n: ("amc" if n in AMCS else "company") for n in NODES}
151
 
152
+
 
 
153
  def build_flows():
154
+ buys = []
155
+ sells = []
156
+ transfers = []
157
+ loops = set()
158
+
159
+ # buys
160
  for amc, comps in BUY_MAP.items():
161
  for c in comps:
162
  w = 3 if c in FRESH_BUY.get(amc, []) else 1
163
  buys.append((amc, c, w))
164
+
165
+ # sells
166
  for amc, comps in SELL_MAP.items():
167
  for c in comps:
168
  w = 3 if c in COMPLETE_EXIT.get(amc, []) else 1
169
  sells.append((c, amc, w))
170
+
171
+ # transfers
172
+ for (s, b), w in TRANSFERS.items():
173
  transfers.append((s, b, w))
174
+
175
+ # loops
176
  for a, c, _ in buys:
177
  for c2, b, _ in sells:
178
  if c == c2:
179
+ loops.add((a, c, b))
 
 
180
 
181
+ return buys, sells, transfers, list(loops)
182
 
 
 
 
 
 
 
 
 
 
 
 
 
183
 
184
+ BUYS, SELLS, TRANSFER_LIST, LOOPS = build_flows()
185
+
186
+ # ============================================================
187
+ # Inspect functions
188
+ # ============================================================
189
+
190
+ def company_trade_summary(company):
191
+ buyers = [a for a, cs in BUY_MAP.items() if company in cs]
192
+ sellers = [a for a, cs in SELL_MAP.items() if company in cs]
193
+ fresh = [a for a, cs in FRESH_BUY.items() if company in cs]
194
+ exiters = [a for a, cs in COMPLETE_EXIT.items() if company in cs]
195
+
196
+ df = pd.DataFrame({
197
+ "Role": ["Buyer"] * len(buyers)
198
+ + ["Seller"] * len(sellers)
199
+ + ["Fresh buy"] * len(fresh)
200
+ + ["Complete exit"] * len(exiters),
201
+ "AMC": buyers + sellers + fresh + exiters
202
+ })
203
+
204
+ if df.empty:
205
+ return None, df
206
+
207
+ counts = df.groupby("Role").size().reset_index(name="Count")
208
+ fig = {
209
+ "data": [{"type": "bar", "x": counts["Role"], "y": counts["Count"]}],
210
+ "layout": {"title": f"Trades for {company}"}
211
+ }
212
+ return fig, df
213
+
214
+
215
+ def amc_transfer_summary(amc):
216
+ sold = SELL_MAP.get(amc, [])
217
+ transfers = []
218
+ for s in sold:
219
+ buyers = [a for a, cs in BUY_MAP.items() if s in cs]
220
+ for b in buyers:
221
+ transfers.append({"security": s, "buyer_amc": b})
222
+ df = pd.DataFrame(transfers)
223
+
224
+ if df.empty:
225
+ return None, df
226
+
227
+ counts = df["buyer_amc"].value_counts().reset_index()
228
+ counts.columns = ["Buyer AMC", "Count"]
229
+ fig = {
230
+ "data": [{"type": "bar", "x": counts["Buyer AMC"], "y": counts["Count"]}],
231
+ "layout": {"title": f"Inferred transfers from {amc}"}
232
+ }
233
+ return fig, df
234
 
 
 
 
 
 
 
 
 
 
235
 
236
+ # ============================================================
237
+ # JS Template (safe raw string, placeholders replaced)
238
+ # ============================================================
239
+ JS_TEMPLATE = r"""
240
+ <div id="arc-container" style="width:100%;height:720px;"></div>
241
+ <button id="arc-reset" style="margin-top:10px;padding:6px 10px;">Reset</button>
242
+
243
+ <div style="margin-top:12px;font-size:14px;font-family:sans-serif;">
244
+ <b>Legend</b><br>
245
+ <span style="color:#1a9850;font-weight:bold;">BUY</span>: green solid<br>
246
+ <span style="color:#d73027;font-weight:bold;">SELL</span>: red dotted<br>
247
+ <span style="color:#636363;font-weight:bold;">TRANSFER</span>: grey chords (inferred)<br>
248
+ <span style="color:#1f9e89;font-weight:bold;">LOOP</span>: teal external arcs<br>
249
+ <div style="margin-top:6px;font-size:12px;color:#777;">Click a node to expand full label and connected labels.</div>
250
  </div>
251
 
252
  <script src="https://d3js.org/d3.v7.min.js"></script>
 
257
  const SELLS = __SELLS__;
258
  const TRANSFERS = __TRANSFERS__;
259
  const LOOPS = __LOOPS__;
260
+ const SHORT_LABEL = __SHORT_LABEL__;
261
+ const FULL_LABEL = __FULL_LABEL__;
262
 
263
+ function draw(){
 
 
 
 
264
  const container = document.getElementById("arc-container");
265
  container.innerHTML = "";
266
+ const width = Math.min(980, container.clientWidth || 860);
267
+ const height = Math.max(540, Math.floor(width * 0.75));
268
  const svg = d3.select(container).append("svg")
269
  .attr("width","100%")
270
+ .attr("height",height)
271
+ .attr("viewBox",[-width/2, -height/2, width, height].join(" "));
272
 
273
+ const radius = Math.min(width,height)*0.36;
274
  const n = NODES.length;
275
  function angle(i){ return (i/n)*2*Math.PI; }
276
 
277
  const pos = NODES.map((name,i)=>{
278
+ const ang = angle(i)-Math.PI/2;
279
  return { name, angle: ang, x: Math.cos(ang)*radius, y: Math.sin(ang)*radius };
280
  });
281
 
282
  const index = {};
283
+ NODES.forEach((nm,i)=>index[nm]=i);
284
 
285
+ const nodeG = svg.append("g").selectAll("g")
 
286
  .data(pos).enter().append("g")
287
  .attr("transform", d => `translate(${d.x},${d.y})`);
288
 
289
+ nodeG.append("circle")
290
+ .attr("r",16)
291
+ .style("fill",d=>NODE_TYPE[d.name]==="amc"?"#003f5c":"#f59e0b")
292
+ .style("stroke","#111").style("stroke-width",1)
 
293
  .style("cursor","pointer");
294
 
295
+ nodeG.append("text")
296
+ .text(d=>SHORT_LABEL[d.name])
297
+ .attr("dy","0.35em")
298
+ .attr("fill","#fff")
299
+ .style("font-size","10px")
300
+ .style("pointer-events","none")
301
+ .style("text-anchor","middle");
302
+
303
+ function arcPath(x0,y0,x1,y1,above){
 
 
304
  const mx=(x0+x1)/2, my=(y0+y1)/2;
305
+ const dx=mx, dy=my;
306
+ const len=Math.sqrt(dx*dx+dy*dy)||1;
307
+ const ux=dx/len, uy=dy/len;
308
+ const offset=(above?-1:1)*Math.max(36,radius*0.9);
309
  const cx=mx+ux*offset, cy=my+uy*offset;
310
  return `M ${x0} ${y0} Q ${cx} ${cy} ${x1} ${y1}`;
311
  }
312
 
313
+ const allW = [].concat(
314
+ BUYS.map(d=>d[2]),
315
+ SELLS.map(d=>d[2]),
316
+ TRANSFERS.map(d=>d[2])
317
+ );
318
  const sw = d3.scaleLinear().domain([1, Math.max(...allW,1)]).range([1.2,6]);
319
 
 
320
  const buyG = svg.append("g");
321
  BUYS.forEach(([a,c,w])=>{
322
+ if(!(a in index)||!(c in index))return;
323
+ const s=pos[index[a]], t=pos[index[c]];
324
  buyG.append("path")
325
+ .attr("d",arcPath(s.x,s.y,t.x,t.y,true))
326
  .attr("stroke","#1a9850")
327
  .attr("fill","none")
328
+ .attr("stroke-width",sw(w))
329
+ .attr("data-src",a)
330
+ .attr("data-tgt",c)
331
+ .attr("opacity",0.92);
332
  });
333
 
 
334
  const sellG = svg.append("g");
335
  SELLS.forEach(([c,a,w])=>{
336
+ if(!(c in index)||!(a in index))return;
337
+ const s=pos[index[c]], t=pos[index[a]];
338
  sellG.append("path")
339
+ .attr("d",arcPath(s.x,s.y,t.x,t.y,false))
340
  .attr("stroke","#d73027")
341
+ .style("stroke-dasharray","4,3")
342
  .attr("fill","none")
343
+ .attr("stroke-width",sw(w))
344
+ .attr("data-src",c)
345
+ .attr("data-tgt",a)
346
+ .attr("opacity",0.86);
 
347
  });
348
 
 
349
  const trG = svg.append("g");
350
  TRANSFERS.forEach(([s,b,w])=>{
351
+ if(!(s in index)||!(b in index))return;
352
+ const sp=pos[index[s]], tp=pos[index[b]];
353
+ const mx=(sp.x+tp.x)/2, my=(sp.y+tp.y)/2;
 
354
  trG.append("path")
355
+ .attr("d",`M ${sp.x} ${sp.y} Q ${mx*0.3} ${my*0.3} ${tp.x} ${tp.y}`)
356
+ .attr("stroke","#636363")
357
+ .attr("stroke-width",sw(w))
358
  .attr("fill","none")
359
+ .attr("opacity",0.7)
360
+ .attr("data-src",s)
361
+ .attr("data-tgt",b);
 
362
  });
363
 
 
364
  const loopG = svg.append("g");
365
  LOOPS.forEach(([a,c,b])=>{
366
+ if(!(a in index)||!(b in index))return;
367
+ const sa=pos[index[a]], sb=pos[index[b]];
368
+ const mx=(sa.x+sb.x)/2, my=(sa.y+sb.y)/2;
369
+ const len=Math.sqrt((sa.x-sb.x)**2+(sa.y-sb.y)**2);
370
+ const outward=Math.max(40, radius*0.28+len*0.12);
371
+ const dx=mx, dy=my;
372
+ const nlen=Math.sqrt(dx*dx+dy*dy)||1;
373
+ const ux=dx/nlen, uy=dy/nlen;
374
+ const cx=mx+ux*outward, cy=my+uy*outward;
375
+ const d=`M ${sa.x} ${sa.y} Q ${cx} ${cy} ${sb.x} ${sb.y}`;
376
  loopG.append("path")
377
+ .attr("d",d)
378
+ .attr("stroke","#1f9e89")
379
  .attr("fill","none")
380
+ .attr("stroke-width",2.8)
381
+ .attr("opacity",0.95);
382
  });
383
 
384
+ const outside = svg.append("g");
385
+
386
+ function showOutside(names){
387
+ outside.selectAll("*").remove();
388
+ names.forEach(nm=>{
389
+ const p=pos[index[nm]];
390
+ const dist=radius+30;
391
+ const x=p.x + Math.cos(p.angle)*dist;
392
+ const y=p.y + Math.sin(p.angle)*dist;
393
+ const deg=(p.angle*180/Math.PI);
394
+ const anchor=(deg>-90 && deg<90)?"start":"end";
395
+ outside.append("text")
396
+ .attr("x",x)
397
+ .attr("y",y)
398
+ .attr("dy","0.35em")
399
+ .text(FULL_LABEL[nm])
 
 
 
400
  .style("font-size","12px")
401
+ .style("font-family","sans-serif")
402
+ .style("fill","#111")
403
+ .style("text-anchor",anchor);
404
  });
405
  }
406
 
407
+ function reset(){
408
+ nodeG.selectAll("circle").style("opacity",1);
409
+ nodeG.selectAll("text").style("opacity",1).text(d=>SHORT_LABEL[d.name]);
410
+ buyG.selectAll("path").style("opacity",0.92);
411
+ sellG.selectAll("path").style("opacity",0.86);
412
+ trG.selectAll("path").style("opacity",0.7);
413
+ loopG.selectAll("path").style("opacity",0.95);
414
+ outside.selectAll("*").remove();
415
+ }
416
+
417
+ function highlight(node){
418
+ nodeG.selectAll("circle").style("opacity",d=>d.name===node?1:0.18);
419
+ nodeG.selectAll("text").style("opacity",0.2);
420
+
421
+ const connected=new Set([node]);
422
 
 
 
423
  buyG.selectAll("path").each(function(){
424
+ const s=this.getAttribute("data-src"), t=this.getAttribute("data-tgt");
425
+ if(s===node||t===node){connected.add(s);connected.add(t);}
426
  });
427
  sellG.selectAll("path").each(function(){
428
+ const s=this.getAttribute("data-src"), t=this.getAttribute("data-tgt");
429
+ if(s===node||t===node){connected.add(s);connected.add(t);}
430
  });
431
  trG.selectAll("path").each(function(){
432
+ const s=this.getAttribute("data-src"), t=this.getAttribute("data-tgt");
433
+ if(s===node||t===node){connected.add(s);connected.add(t);}
434
  });
435
 
436
+ showOutside(Array.from(connected));
 
437
 
438
+ buyG.selectAll("path").style("opacity",function(){
439
+ const s=this.getAttribute("data-src"), t=this.getAttribute("data-tgt");
440
+ return (s===node||t===node)?1:0.06;
 
441
  });
442
+ sellG.selectAll("path").style("opacity",function(){
443
+ const s=this.getAttribute("data-src"), t=this.getAttribute("data-tgt");
444
+ return (s===node||t===node)?1:0.06;
445
  });
446
+ trG.selectAll("path").style("opacity",function(){
447
+ const s=this.getAttribute("data-src"), t=this.getAttribute("data-tgt");
448
+ return (s===node||t===node)?1:0.06;
449
  });
450
  }
451
 
452
+ nodeG.selectAll("circle").on("click",(e,d)=>{highlight(d.name);e.stopPropagation();});
453
+ nodeG.selectAll("text").on("click",(e,d)=>{highlight(d.name);e.stopPropagation();});
 
 
 
 
 
 
 
454
 
455
+ document.getElementById("arc-reset").onclick=reset;
 
456
 
457
+ svg.on("click", ()=>reset());
 
458
  }
459
 
 
460
  draw();
461
  window.addEventListener("resize", draw);
462
  </script>
463
  """
464
 
465
+
466
+ # ============================================================
467
+ # HTML builder
468
+ # ============================================================
469
+
470
  def make_arc_html():
471
  html = JS_TEMPLATE
472
  html = html.replace("__NODES__", json.dumps(NODES))
473
  html = html.replace("__NODE_TYPE__", json.dumps(NODE_TYPE))
474
  html = html.replace("__BUYS__", json.dumps(BUYS))
475
  html = html.replace("__SELLS__", json.dumps(SELLS))
476
+ html = html.replace("__TRANSFERS__", json.dumps(TRANSFER_LIST))
477
  html = html.replace("__LOOPS__", json.dumps(LOOPS))
478
  html = html.replace("__SHORT_LABEL__", json.dumps(SHORT_LABELS))
479
+ html = html.replace("__FULL_LABEL__", json.dumps(FULL_LABELS))
480
  return html
481
 
482
+
483
  ARC_HTML = make_arc_html()
484
 
485
+ # ============================================================
486
+ # Gate callback
487
+ # ============================================================
488
+
489
+ def gate_fn(email, code, ua):
490
+ if not email or not code:
491
+ log_event(email or "<empty>", code or "<empty>", ua, False)
492
+ return gr.update(visible=True), gr.update(visible=False), "Enter both email and access code."
493
+
494
+ if code.strip() == ACCESS_CODE:
495
+ log_event(email, code, ua, True)
496
+ return gr.update(visible=False), gr.update(visible=True), f"Access granted for {email}."
497
+ else:
498
+ log_event(email, code, ua, False)
499
+ return gr.update(visible=True), gr.update(visible=False), "Invalid access code."
500
+
501
+
502
+ # ============================================================
503
+ # UI
504
+ # ============================================================
505
+
506
+ with gr.Blocks(title="MF Churn — Secure Arc Diagram") as demo:
507
+ gr.Markdown("## 🔒 Mutual Fund Churn — Secure Access")
508
+
509
+ # Gate
510
+ with gr.Column(visible=True) as gate_col:
511
+ email_box = gr.Textbox(label="Email")
512
+ pass_box = gr.Textbox(label="Access Code", type="password")
513
+ ua_box = gr.Textbox(visible=False, elem_id="ua_box")
514
+ login_btn = gr.Button("Login")
515
+ gate_msg = gr.Markdown()
516
+ gr.HTML("<script>document.getElementById('ua_box').value = navigator.userAgent;</script>")
517
+
518
+ # App
519
  with gr.Column(visible=False) as app_col:
520
+ gr.Markdown("### Arc Diagram (Click nodes to expand labels)")
521
+ gr.HTML(ARC_HTML)
522
+
523
  gr.Markdown("---")
524
+ gr.Markdown("### Inspect Company")
525
+ select_company = gr.Dropdown(COMPANIES)
526
  company_plot = gr.Plot()
527
  company_table = gr.DataFrame()
528
+
529
+ gr.Markdown("### Inspect AMC")
530
+ select_amc = gr.Dropdown(AMCS)
531
  amc_plot = gr.Plot()
532
  amc_table = gr.DataFrame()
533
 
534
+ login_btn.click(
535
+ fn=gate_fn,
536
+ inputs=[email_box, pass_box, ua_box],
537
+ outputs=[gate_col, app_col, gate_msg]
538
+ )
 
 
 
 
 
539
 
540
+ select_company.change(company_trade_summary, select_company, [company_plot, company_table])
541
+ select_amc.change(amc_transfer_summary, select_amc, [amc_plot, amc_table])
542
 
543
  if __name__ == "__main__":
544
  demo.launch()