topsecrettraders commited on
Commit
ce0cce3
·
verified ·
1 Parent(s): 7ac5d89

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +554 -578
app.py CHANGED
@@ -50,12 +50,8 @@ def home():
50
  def get_dates():
51
  """
52
  Fetch distinct dates.
53
- Increased limit to 100,000 to scan through dense 10s data
54
- to find older dates.
55
  """
56
- # 10s data is dense. 1 day ~ 3750 minutes ~ 22500 rows.
57
- # 100k rows gives ~4-5 days history scan.
58
- data = sb_get("/history_buckets_upstox", {"select": "minute", "order": "minute.desc", "limit": "100000"})
59
  dates = set()
60
  for row in data:
61
  if row.get('minute'):
@@ -211,7 +207,7 @@ def fetch_series(
211
 
212
 
213
  # ==========================================
214
- # 5. FRONTEND TEMPLATE (High Aesthetic)
215
  # ==========================================
216
 
217
  HTML_TEMPLATE = """
@@ -219,266 +215,212 @@ HTML_TEMPLATE = """
219
  <html lang="en">
220
  <head>
221
  <meta charset="UTF-8">
222
- <title>DepthChain PRO</title>
223
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
224
- <!-- Fonts -->
225
- <link rel="preconnect" href="https://fonts.googleapis.com">
226
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@500&display=swap" rel="stylesheet">
227
  <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
228
-
229
  <style>
230
- /* --- DESIGN SYSTEM --- */
231
  :root {
232
- --bg-app: #050505;
233
- --bg-panel: #111111;
234
- --bg-card: #0A0A0A;
235
- --border: #222222;
236
- --border-hover: #333333;
237
-
238
- --text-main: #EAEAEA;
239
- --text-muted: #666666;
240
- --text-accent: #3399FF;
241
-
242
- --accent: #007AFF;
243
- --accent-glow: rgba(0, 122, 255, 0.2);
244
- --danger: #FF3B30;
245
- --success: #34C759;
246
- --warning: #FFCC00;
247
-
248
- --font-main: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
249
- --font-mono: 'JetBrains Mono', monospace;
250
-
251
- --radius: 8px;
252
- --header-h: 52px;
253
  }
254
-
255
- * { box-sizing: border-box; margin: 0; padding: 0; user-select: none; outline: none; }
256
-
257
- /* SCROLLBARS */
258
- ::-webkit-scrollbar { width: 6px; height: 6px; }
259
- ::-webkit-scrollbar-track { background: transparent; }
260
- ::-webkit-scrollbar-thumb { background: #333; border-radius: 3px; }
261
- ::-webkit-scrollbar-thumb:hover { background: #555; }
262
-
263
  body {
264
- background: var(--bg-app); color: var(--text-main); font-family: var(--font-main);
265
- height: 100vh; overflow: hidden; display: flex; flex-direction: column;
266
  }
267
 
268
- /* --- HEADER --- */
269
- header {
270
- height: var(--header-h); background: rgba(17, 17, 17, 0.8);
271
- backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);
272
- border-bottom: 1px solid var(--border);
273
- display: flex; align-items: center; justify-content: space-between;
274
- padding: 0 16px; z-index: 100;
275
  }
276
-
277
- .logo { font-weight: 600; font-size: 14px; letter-spacing: -0.02em; color: var(--text-main); }
278
  .logo span { color: var(--accent); }
279
-
280
- .controls { display: flex; gap: 12px; align-items: center; }
281
-
282
- /* Custom Inputs */
283
- select, button {
284
- background: #1A1A1A; border: 1px solid var(--border); color: var(--text-main);
285
- padding: 0 12px; height: 28px; border-radius: 6px; font-size: 12px;
286
- font-family: var(--font-main); transition: all 0.2s ease; cursor: pointer;
287
  }
288
- select:hover, button:hover { border-color: var(--border-hover); background: #222; }
289
- select:focus { border-color: var(--accent); }
290
-
291
- /* Segmented Control for Layout */
292
- .segmented {
293
- background: #1A1A1A; border: 1px solid var(--border);
294
- border-radius: 6px; padding: 2px; display: flex; gap: 2px; height: 28px;
 
 
 
 
 
 
295
  }
296
- .seg-btn {
297
- background: transparent; border: none; color: var(--text-muted);
298
- width: 32px; height: 100%; border-radius: 4px; font-weight: 600; font-size: 11px;
299
- display: flex; align-items: center; justify-content: center; padding: 0;
300
  }
301
- .seg-btn:hover { color: var(--text-main); background: rgba(255,255,255,0.05); }
302
- .seg-btn.active { background: #333; color: var(--text-main); box-shadow: 0 1px 2px rgba(0,0,0,0.5); }
303
 
304
- .btn-blue { color: var(--accent); font-weight: 500; border-color: var(--border); }
305
- .btn-blue:hover { border-color: var(--accent); background: var(--accent-glow); }
306
 
307
- .btn-red { color: var(--danger); opacity: 0.7; }
308
- .btn-red:hover { opacity: 1; border-color: var(--danger); }
309
-
310
- /* Mode Switcher */
311
- .mode-switch {
312
- font-size: 11px; font-weight: 600; padding: 0 10px; display: flex; align-items: center; gap: 6px;
313
- }
314
- .mode-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--text-muted); transition: 0.3s; }
315
- .mode-switch.live .mode-dot { background: var(--success); box-shadow: 0 0 6px var(--success); }
316
- .mode-switch.live { border-color: var(--success); color: #fff; background: rgba(52, 199, 89, 0.1); }
317
-
318
- /* --- MAIN LAYOUT --- */
319
- .grid-container {
320
- flex: 1; display: flex; overflow: hidden; /* No scroll on container */
321
- padding: 0; gap: 0;
322
- }
323
-
324
  .column {
325
- flex: 1;
326
- display: flex; flex-direction: column;
327
- border-right: 1px solid var(--border);
328
- overflow-y: auto; /* Independent Scroll */
329
- overflow-x: hidden;
330
- min-width: 0; /* Prevents flex blowout */
331
  }
332
- .column:last-child { border-right: none; }
333
 
334
  /* --- CHART CARD --- */
335
  .chart-card {
336
- background: var(--bg-card);
337
- border-bottom: 1px solid var(--border);
338
- display: flex; flex-direction: column;
339
- position: relative;
340
- transition: height 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
341
  }
342
-
343
- .card-head {
344
- height: 36px; display: flex; align-items: center; justify-content: space-between;
345
- padding: 0 12px; font-size: 11px; border-bottom: 1px solid rgba(255,255,255,0.03);
346
- flex-shrink: 0;
347
  }
348
-
349
- .card-info { display: flex; align-items: center; gap: 8px; font-weight: 500; }
350
- .ci-symbol { color: var(--text-main); font-weight: 600; }
351
- .ci-meta { color: var(--text-muted); font-family: var(--font-mono); }
352
-
353
- .card-tools { display: flex; gap: 4px; opacity: 0; transition: 0.2s; }
354
- .chart-card:hover .card-tools { opacity: 1; }
355
-
356
- .tool-btn {
357
- width: 24px; height: 24px; border: none; background: transparent;
358
- color: var(--text-muted); border-radius: 4px; display: flex; align-items: center; justify-content: center;
359
  }
360
- .tool-btn:hover { color: var(--text-main); background: rgba(255,255,255,0.1); }
361
-
362
- .card-body { flex: 1; position: relative; overflow: hidden; }
363
- .card-overlay { position: absolute; inset: 0; display:flex; justify-content: center; align-items: center; z-index: 10; pointer-events: none;}
364
 
365
- /* Empty State */
366
- .empty-placeholder {
367
- display: flex; flex-direction: column; align-items: center; justify-content: center;
368
- height: 100%; width: 100%; color: var(--text-muted); gap: 12px;
369
- cursor: pointer; transition: 0.2s;
370
- }
371
- .empty-placeholder:hover { background: rgba(255,255,255,0.02); color: var(--text-main); }
372
- .add-icon {
373
- width: 32px; height: 32px; border-radius: 50%; border: 1px dashed var(--text-muted);
374
- display: flex; align-items: center; justify-content: center; font-size: 16px;
375
- }
376
- .empty-placeholder:hover .add-icon { border-color: var(--text-main); border-style: solid; }
377
-
378
- /* Add Button in Column */
379
- .col-add {
380
- height: 40px; display: flex; align-items: center; justify-content: center;
381
- border-top: 1px solid var(--border); color: var(--text-muted);
382
- font-size: 11px; font-weight: 500; cursor: pointer; flex-shrink: 0;
383
- background: var(--bg-panel); transition: 0.2s;
384
- }
385
- .col-add:hover { background: #1a1a1a; color: var(--text-main); }
386
 
387
  /* --- MODAL --- */
388
- .modal-backdrop {
389
- position: fixed; inset: 0; background: rgba(0,0,0,0.6); backdrop-filter: blur(4px);
390
- z-index: 200; display: none; align-items: center; justify-content: center;
 
391
  }
392
  .modal {
393
- background: #111; border: 1px solid var(--border); border-radius: 12px;
394
- width: 420px; box-shadow: 0 24px 48px rgba(0,0,0,0.8);
395
- display: flex; flex-direction: column; overflow: hidden;
396
- animation: modalPop 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275);
397
  }
398
- @keyframes modalPop { from { transform: scale(0.95); opacity: 0; } to { transform: scale(1); opacity: 1; } }
399
-
400
- .m-head { padding: 16px; border-bottom: 1px solid var(--border); display: flex; justify-content: space-between; align-items: center; font-weight: 600; font-size: 13px; }
401
- .m-body { padding: 16px; display: flex; flex-direction: column; gap: 16px; max-height: 70vh; overflow-y: auto; }
402
- .m-foot { padding: 12px 16px; border-top: 1px solid var(--border); display: flex; justify-content: flex-end; gap: 8px; background: #0e0e0e; }
403
-
404
- .f-group label { display: block; font-size: 10px; text-transform: uppercase; color: var(--text-muted); margin-bottom: 6px; font-weight: 600; }
405
- .f-row { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
406
- .inp { width: 100%; background: #050505; border: 1px solid var(--border); padding: 6px 10px; border-radius: 6px; color: #fff; font-size: 12px; }
407
- .inp:focus { border-color: var(--accent); }
408
 
409
  /* Series List */
410
- .s-item { display: grid; grid-template-columns: 24px 1fr 1fr 40px 24px; gap: 8px; align-items: center; margin-bottom: 6px; }
411
- .s-color { padding: 0; width: 24px; height: 24px; border: none; border-radius: 4px; overflow: hidden; cursor: pointer; }
412
 
413
  /* Toast */
414
  .toast {
415
- position: fixed; bottom: 24px; left: 50%; transform: translateX(-50%) translateY(20px);
416
- background: #222; border: 1px solid var(--border); padding: 8px 16px; border-radius: 20px;
417
- font-size: 12px; font-weight: 500; color: #fff; opacity: 0; pointer-events: none; transition: 0.3s;
418
- box-shadow: 0 10px 30px rgba(0,0,0,0.5); z-index: 500; display: flex; align-items: center; gap: 8px;
419
  }
420
- .toast.visible { opacity: 1; transform: translateX(-50%) translateY(0); }
421
- .t-icon { color: var(--success); }
422
 
423
  </style>
424
  </head>
425
  <body>
426
 
427
- <header>
 
428
  <div class="logo">DepthChain <span>PRO</span></div>
429
 
430
  <div class="controls">
431
- <select id="globalDate" onchange="App.onDateChange()"></select>
432
-
433
- <button id="modeBtn" class="mode-switch" onclick="App.toggleMode()">
434
- <div class="mode-dot"></div> <span id="modeText">HISTORICAL</span>
435
- </button>
436
-
437
- <div style="width:1px; height:16px; background:var(--border); margin:0 4px;"></div>
438
-
439
- <div class="segmented">
440
- <button class="seg-btn" onclick="App.setLayout(1)">1</button>
441
- <button class="seg-btn" onclick="App.setLayout(2)">2</button>
442
- <button class="seg-btn active" onclick="App.setLayout(3)">3</button>
443
- <button class="seg-btn" onclick="App.setLayout(4)">4</button>
 
 
 
 
 
 
 
 
444
  </div>
445
 
446
- <button class="btn-blue" onclick="ConfigModal.openDefaults()">Settings</button>
447
- <button class="btn-red" onclick="App.reset()">Reset</button>
448
  </div>
449
- </header>
450
 
451
- <div class="grid-container" id="grid">
452
- <!-- Columns and charts injected here -->
 
453
  </div>
454
 
455
- <!-- Config Modal -->
456
- <div class="modal-backdrop" id="modalOverlay">
457
  <div class="modal">
458
- <div class="m-head">
459
- <span id="mTitle">Configure Chart</span>
460
- <button class="tool-btn" onclick="ConfigModal.close()">✕</button>
461
  </div>
462
- <div class="m-body">
 
 
 
 
463
  <div id="cfgForm">
464
- <div class="f-row">
465
- <div class="f-group">
466
- <label>Symbol</label>
467
- <select id="cfgRoot" class="inp" onchange="ConfigModal.onRootChange()"></select>
468
  </div>
469
- <div class="f-group">
470
  <label>Expiry</label>
471
- <select id="cfgExp" class="inp" onchange="ConfigModal.onExpChange()"></select>
472
  </div>
473
  </div>
474
- <div class="f-group">
 
475
  <label>Instrument</label>
476
- <select id="cfgInst" class="inp"></select>
477
  </div>
478
- <div class="f-row">
479
- <div class="f-group">
 
480
  <label>Timeframe</label>
481
- <select id="cfgTF" class="inp">
482
  <option value="1min">1 Minute</option>
483
  <option value="3min">3 Minutes</option>
484
  <option value="5min">5 Minutes</option>
@@ -487,66 +429,77 @@ HTML_TEMPLATE = """
487
  <option value="1hour">1 Hour</option>
488
  </select>
489
  </div>
490
- <div class="f-group">
491
- <label>Range (Start - End)</label>
492
- <div style="display:flex; gap:4px;">
493
- <input type="time" id="cfgStart" value="09:15" class="inp">
494
- <input type="time" id="cfgEnd" value="15:30" class="inp">
495
  </div>
496
  </div>
497
  </div>
 
 
498
 
499
- <div style="height:1px; background:var(--border); margin:10px 0;"></div>
500
-
501
- <div class="f-group">
502
- <label>Series ($P, $B, $S, $V, $OI)</label>
503
- <div id="cfgSeries"></div>
504
- <button class="inp" style="margin-top:4px; text-align:center;" onclick="ConfigModal.addRow()">+ Add Plot Line</button>
505
  </div>
506
  </div>
507
  </div>
508
- <div class="m-foot">
509
- <button class="inp" style="width:auto;" onclick="ConfigModal.close()">Cancel</button>
510
- <button class="inp" style="width:auto; background:var(--accent); border-color:var(--accent);" onclick="ConfigModal.save()">Save</button>
511
  </div>
512
  </div>
513
  </div>
514
 
515
- <div class="toast" id="toast">
516
- <span class="t-icon">✓</span> <span id="toastMsg">Saved</span>
 
 
 
 
 
 
 
 
 
 
517
  </div>
518
 
 
 
519
  <script>
 
520
  /**
521
- * --- APP CONTROLLER ---
522
- * Handles Layout, DOM Caching (Instant Switch), and Global State
523
  */
524
- const App = {
525
  state: {
526
  date: '',
527
- mode: 'HIST',
528
- columns: 3,
529
- layout: [[], [], []], // Array of arrays of IDs
530
- charts: {} // ID -> { config: {}, instance: ChartObj, interval: null }
531
  },
532
- domCache: {}, // Map<ID, HTMLElement> - Keeps chart elements alive in memory
533
 
534
  defaults: [
535
- { formula: '$P', color: '#007AFF', label: 'Price', axis: 'left' },
536
- { formula: '$B', color: '#34C759', label: 'Buy', axis: 'right' },
537
- { formula: '$S', color: '#FF3B30', label: 'Sell', axis: 'right' }
538
  ],
539
 
540
  async init() {
 
541
  await this.loadDates();
542
- this.loadState();
543
 
544
- // Auto-Live Check
545
- const today = new Date().toISOString().split('T')[0];
546
- if(this.state.date === today && this.state.mode !== 'LIVE') this.toggleMode();
547
-
548
- this.renderGrid();
549
- this.startLoops();
550
  },
551
 
552
  async loadDates() {
@@ -556,527 +509,550 @@ const App = {
556
  const sel = document.getElementById('globalDate');
557
  sel.innerHTML = dates.map(d => `<option value="${d}">${d}</option>`).join('');
558
  if(dates.length) this.state.date = dates[0];
559
- } catch(e) { console.error(e); }
560
  },
561
 
562
- loadState() {
563
- try {
564
- const raw = localStorage.getItem('dc_pro_v5');
565
- if(raw) {
566
- const s = JSON.parse(raw);
567
- this.state.date = s.date || this.state.date;
568
- document.getElementById('globalDate').value = this.state.date;
569
- this.state.columns = s.columns || 3;
570
-
571
- // Restore Mode
572
- if(s.mode === 'LIVE') {
573
- this.state.mode = 'LIVE';
574
- this.updateModeUI();
575
- }
576
 
577
- // Restore Layout & Configs
578
- this.state.layout = [];
579
- s.layout.forEach((colIds, cIdx) => {
580
- this.state.layout[cIdx] = [];
581
- colIds.forEach(item => {
582
- const id = item.id;
583
- this.state.layout[cIdx].push(id);
584
- this.state.charts[id] = { config: item.config, instance: null, interval: null };
585
- });
 
 
 
 
 
 
 
 
 
 
586
  });
587
- } else {
588
- this.resetLayoutState();
589
- }
590
- } catch(e) { this.resetLayoutState(); }
591
- },
592
 
593
- resetLayoutState() {
594
- this.state.layout = [[],[],[]];
595
- this.state.columns = 3;
596
- // Init 3x2 empty
597
- for(let i=0; i<3; i++) {
598
- this.addChartToState(i);
599
- this.addChartToState(i);
600
  }
601
  },
602
 
603
- saveState() {
604
- const minLayout = this.state.layout.map(col => col.map(id => ({
605
- id: id, config: this.state.charts[id].config
606
- })));
607
- localStorage.setItem('dc_pro_v5', JSON.stringify({
 
 
 
 
 
608
  date: this.state.date,
609
  mode: this.state.mode,
610
  columns: this.state.columns,
611
- layout: minLayout
612
- }));
 
 
613
  },
614
 
615
- addChartToState(colIdx) {
616
- const id = 'c_' + Date.now() + Math.random().toString(36).substr(2,4);
617
- this.state.charts[id] = { config: {}, instance: null, interval: null };
618
- if(!this.state.layout[colIdx]) this.state.layout[colIdx] = [];
619
- this.state.layout[colIdx].push(id);
620
- return id;
621
  },
622
 
623
- // --- RENDERING (The Magic Part) ---
624
- setLayout(n) {
625
- if(n === this.state.columns) return;
626
-
627
- // Flatten all charts
628
- const allIds = this.state.layout.flat();
629
- this.state.columns = n;
630
-
631
- // Update UI Tabs
632
- document.querySelectorAll('.seg-btn').forEach((b, i) => {
633
- b.classList.toggle('active', (i+1) === n);
634
- });
635
-
636
- // Redistribute
637
- this.state.layout = Array.from({length: n}, () => []);
638
- allIds.forEach((id, i) => {
639
- this.state.layout[i % n].push(id);
640
- });
641
 
 
 
 
 
642
  this.renderGrid();
643
- this.saveState();
644
  },
645
 
646
  renderGrid() {
647
- const container = document.getElementById('grid');
648
- // We do NOT clear innerHTML because we want to preserve DOM nodes (Canvas context)
649
- // Instead, we diff or just append cached nodes to new columns.
650
-
651
- container.innerHTML = ''; // Reset container structure
652
 
653
- for(let i=0; i<this.state.columns; i++) {
654
  const colDiv = document.createElement('div');
655
  colDiv.className = 'column';
656
  colDiv.id = `col-${i}`;
657
-
658
- const ids = this.state.layout[i] || [];
659
- ids.forEach(id => {
660
- const el = this.getOrBuilderChartDOM(id);
 
661
  colDiv.appendChild(el);
 
 
662
  });
663
 
664
  // Add button
665
- const btn = document.createElement('div');
666
- btn.className = 'col-add';
667
- btn.innerHTML = '+ Add Chart';
668
- btn.onclick = () => this.addChartBtn(i);
669
- colDiv.appendChild(btn);
670
 
671
  container.appendChild(colDiv);
672
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
673
 
674
- this.calcHeights();
675
- // Trigger resize for charts to fit new widths
676
- setTimeout(() => Object.values(this.state.charts).forEach(c => c.instance && c.instance.resize()), 50);
 
 
 
 
 
 
 
 
 
677
  },
678
 
679
- getOrBuilderChartDOM(id) {
680
- if(this.domCache[id]) return this.domCache[id];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
681
 
682
- // Build New
683
  const div = document.createElement('div');
684
  div.className = 'chart-card';
685
- div.id = id;
686
  div.innerHTML = `
687
- <div class="card-head">
688
- <div class="card-info">
689
- <div class="mode-dot" id="dot-${id}"></div>
690
- <span class="ci-symbol" id="sym-${id}">Empty</span>
691
- <span class="ci-meta" id="meta-${id}"></span>
692
  </div>
693
- <div class="card-tools">
694
- <button class="tool-btn" onclick="ConfigModal.open('${id}')">⚙</button>
695
- <button class="tool-btn" onclick="App.removeChart('${id}')">✕</button>
696
  </div>
697
  </div>
698
  <div class="card-body" id="body-${id}">
699
- <div class="empty-placeholder" onclick="ConfigModal.open('${id}')">
700
- <div class="add-icon">+</div>
701
- <span>Configure Chart</span>
702
  </div>
703
  </div>
704
  `;
705
-
706
- this.domCache[id] = div;
707
-
708
- // If config exists, plot it (restore)
709
- const cfg = this.state.charts[id].config;
710
- if(cfg && cfg.root) ChartLogic.plot(id);
711
-
712
  return div;
713
  },
714
 
715
- addChartBtn(colIdx) {
716
- const id = this.addChartToState(colIdx);
717
- this.renderGrid(); // Re-render to place it
718
- this.saveState();
719
- },
720
-
721
- removeChart(id) {
722
- ChartLogic.stopLive(id);
723
- if(this.state.charts[id].instance) this.state.charts[id].instance.destroy();
724
- delete this.state.charts[id];
725
- delete this.domCache[id];
726
-
727
- // Remove from layout array
728
- this.state.layout = this.state.layout.map(col => col.filter(x => x !== id));
729
- this.renderGrid();
730
- this.saveState();
731
- },
732
-
733
- calcHeights() {
734
- const headerH = 52;
735
- const totalH = window.innerHeight - headerH;
736
-
737
- this.state.layout.forEach((ids, colIdx) => {
738
- const count = ids.length;
739
- if(count === 0) return;
740
-
741
- // Max 4 visible rule
742
- let hPx = totalH / count;
743
- if(count > 4) hPx = totalH / 4; // Scroll logic kicks in via CSS
744
-
745
- ids.forEach(id => {
746
- if(this.domCache[id]) {
747
- this.domCache[id].style.height = `${hPx}px`;
748
- }
749
- });
750
- });
751
- },
752
-
753
- // --- GLOBAL ACTIONS ---
754
  onDateChange() {
755
  this.state.date = document.getElementById('globalDate').value;
756
- const today = new Date().toISOString().split('T')[0];
757
- if(this.state.date === today && this.state.mode !== 'LIVE') this.toggleMode();
758
-
759
- this.saveState();
760
- // Refresh all
761
  Object.keys(this.state.charts).forEach(id => ChartLogic.plot(id));
762
- this.startLoops();
763
- },
764
-
765
- toggleMode() {
766
- this.state.mode = (this.state.mode === 'LIVE') ? 'HIST' : 'LIVE';
767
- this.updateModeUI();
768
- this.saveState();
769
- this.startLoops();
770
- },
771
-
772
- updateModeUI() {
773
- const btn = document.getElementById('modeBtn');
774
- const txt = document.getElementById('modeText');
775
- if(this.state.mode === 'LIVE') {
776
- btn.classList.add('live');
777
- txt.innerText = 'LIVE';
778
- } else {
779
- btn.classList.remove('live');
780
- txt.innerText = 'HISTORICAL';
781
- }
782
  },
783
 
784
- startLoops() {
785
- const isLive = (this.state.mode === 'LIVE');
 
 
786
  Object.keys(this.state.charts).forEach(id => {
787
- if(isLive) ChartLogic.startLive(id);
788
  else ChartLogic.stopLive(id);
789
  });
790
- },
791
-
792
- reset() {
793
- if(confirm("Reset Everything?")) {
794
- localStorage.removeItem('dc_pro_v5');
795
- location.reload();
796
- }
797
- },
798
-
799
- toast(msg) {
800
- const t = document.getElementById('toast');
801
- document.getElementById('toastMsg').innerText = msg;
802
- t.classList.add('visible');
803
- setTimeout(() => t.classList.remove('visible'), 2000);
804
  }
805
  };
806
 
807
- window.onresize = () => App.calcHeights();
808
-
809
  /**
810
- * --- CHART LOGIC ---
811
  */
812
  const ChartLogic = {
 
813
  async plot(id, isUpdate=false) {
814
- const c = App.state.charts[id];
815
- const cfg = c.config;
816
- if(!cfg || !cfg.root) return;
817
-
818
- // UI Updates
819
- const card = App.domCache[id];
820
- if(!card) return; // Should not happen
821
-
822
- card.querySelector('#sym-'+id).innerText = cfg.root;
823
- card.querySelector('#meta-'+id).innerText = `${cfg.expiry} • ${cfg.instrument} • ${cfg.timeframe}`;
824
 
825
- const dot = card.querySelector('#dot-'+id);
826
- dot.style.background = isUpdate ? 'var(--warning)' : (App.state.mode==='LIVE' ? 'var(--success)' : 'var(--accent)');
 
 
 
 
 
 
 
 
827
 
828
- // Canvas Setup
829
- const body = card.querySelector('#body-'+id);
830
- if(!body.querySelector('canvas')) {
831
- body.innerHTML = `<div style="position:relative; width:100%; height:100%"><canvas id="can-${id}"></canvas></div>`;
832
  }
833
 
834
- let endTime = cfg.end;
835
- if(App.state.mode === 'LIVE') {
836
- const n = new Date();
837
- endTime = `${String(n.getHours()).padStart(2,'0')}:${String(n.getMinutes()).padStart(2,'0')}`;
838
- }
839
 
840
  try {
841
  const res = await fetch('/api/fetch_series', {
842
  method: 'POST',
843
- headers: {'Content-Type':'application/json'},
844
  body: JSON.stringify({
845
- date: App.state.date, root: cfg.root, expiry: cfg.expiry,
846
- instrument: cfg.instrument, timeframe: cfg.timeframe,
847
- start_time: cfg.start, end_time: endTime
 
 
 
 
848
  })
849
  });
850
  const data = await res.json();
851
-
852
- if(data.error || !data.labels || !data.labels.length) {
853
- if(App.state.mode !== 'LIVE') {
854
- body.innerHTML = `<div class="empty-placeholder" onclick="ConfigModal.open('${id}')"><span>No Data</span></div>`;
855
- if(c.instance) c.instance.destroy();
 
 
 
 
 
 
 
856
  }
857
- return;
858
  }
859
 
860
- // Restore live dot color
861
- if(App.state.mode==='LIVE') dot.style.background = 'var(--success)';
862
 
 
863
  const datasets = cfg.series.map(s => {
864
- const vals = data.labels.map((_, i) => {
865
- const P=data.P[i]||0, V=data.V[i]||0, OI=data.OI[i]||0, B=data.B[i]||0, S=data.S[i]||0;
866
- let f = s.formula.replace(/\$OI/g,OI).replace(/\$P/g,P).replace(/\$V/g,V).replace(/\$B/g,B).replace(/\$S/g,S);
867
- try { return new Function('return '+f)(); } catch{ return null; }
 
 
 
868
  });
 
869
  return {
870
- label: s.label, data: vals, borderColor: s.color, backgroundColor: s.color,
871
- borderWidth: 1.5, pointRadius: 0, hoverRadius: 4, tension: 0.1,
872
- yAxisID: s.axis==='left'?'y':'y1'
 
 
 
 
 
 
873
  };
874
  });
875
 
876
  this.renderCanvas(id, data.labels, datasets);
877
 
878
- } catch(e) { console.error(e); }
 
 
879
  },
880
 
881
  renderCanvas(id, labels, datasets) {
882
- const ctx = document.getElementById(`can-${id}`);
883
- if(!ctx) return;
884
- const c = App.state.charts[id];
885
-
886
- if(c.instance) {
887
- c.instance.data.labels = labels;
888
- c.instance.data.datasets = datasets;
889
- c.instance.update('none');
 
 
890
  } else {
 
891
  const hasRight = datasets.some(d => d.yAxisID === 'y1');
892
- c.instance = new Chart(ctx.getContext('2d'), {
893
  type: 'line',
894
  data: { labels, datasets },
895
  options: {
896
- responsive: true, maintainAspectRatio: false, animation: false,
 
 
897
  interaction: { mode: 'index', intersect: false },
898
- plugins: {
899
- legend: { display:false },
900
- tooltip: { backgroundColor:'rgba(10,10,10,0.95)', titleColor:'#fff', bodyColor:'#ccc', borderColor:'#333', borderWidth:1 }
901
  },
902
  scales: {
903
- x: { grid:{color:'#222', tickLength:0}, ticks:{color:'#666', maxTicksLimit:8} },
904
- y: { type:'linear', display:true, position:'left', grid:{color:'#222'}, ticks:{color:'#666'} },
905
- y1: { type:'linear', display:hasRight, position:'right', grid:{display:false}, ticks:{color:'#666'} }
906
  }
907
  }
908
  });
909
  }
910
  },
911
 
 
912
  startLive(id) {
913
- this.stopLive(id);
914
- const c = App.state.charts[id];
915
- if(!c.config.root) return;
 
 
 
 
916
 
 
917
  this.plot(id);
918
- c.interval = setInterval(() => this.plot(id, true), 15000);
 
 
 
 
919
  },
920
 
921
  stopLive(id) {
922
- const c = App.state.charts[id];
923
- if(c && c.interval) { clearInterval(c.interval); c.interval = null; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
924
  }
925
  };
926
 
927
  /**
928
- * --- CONFIG MODAL ---
929
  */
930
  const ConfigModal = {
931
- currId: null,
932
  isGlobal: false,
933
 
934
  async open(id) {
935
- this.currId = id;
936
  this.isGlobal = false;
937
- document.getElementById('mTitle').innerText = 'Configure Chart';
938
- document.getElementById('modalOverlay').style.display = 'flex';
939
  document.getElementById('cfgForm').style.display = 'block';
 
940
 
941
- const cfg = App.state.charts[id].config;
942
- await this.loadRoots(cfg.root);
943
 
944
- document.getElementById('cfgTF').value = cfg.timeframe || '1min';
945
- document.getElementById('cfgStart').value = cfg.start || '09:15';
946
- document.getElementById('cfgEnd').value = cfg.end || '15:30';
947
-
948
- this.renderSeries(cfg.series || App.defaults);
949
 
950
- if(cfg.root) {
951
- document.getElementById('cfgRoot').value = cfg.root;
952
- await this.onRootChange(cfg.expiry, cfg.instrument);
 
 
 
 
 
 
 
 
 
953
  }
954
  },
955
 
956
  openDefaults() {
957
  this.isGlobal = true;
958
- document.getElementById('mTitle').innerText = 'Global Defaults';
959
- document.getElementById('modalOverlay').style.display = 'flex';
960
- this.renderSeries(App.defaults);
 
 
 
 
 
961
  },
962
 
963
- close() { document.getElementById('modalOverlay').style.display = 'none'; },
 
 
964
 
965
- async loadRoots(pre) {
966
- const sel = document.getElementById('cfgRoot');
967
- sel.innerHTML = '<option>Loading...</option>';
968
  try {
969
- const d = App.state.date;
970
- const res = await fetch(`/api/roots?date=${d}`);
971
- const data = await res.json();
972
- sel.innerHTML = `<option value="">-- Select --</option>` + data.map(r=>`<option>${r}</option>`).join('');
973
- if(pre && data.includes(pre)) sel.value = pre;
974
- } catch(e){ sel.innerHTML = '<option>Error</option>'; }
975
  },
976
 
977
  async onRootChange(preExp, preInst) {
978
  const root = document.getElementById('cfgRoot').value;
979
- const d = App.state.date;
980
  const eSel = document.getElementById('cfgExp');
981
  eSel.innerHTML = '<option>Loading...</option>';
982
-
983
- const res = await fetch(`/api/expiries?date=${d}&root=${root}`);
984
- const data = await res.json();
985
- eSel.innerHTML = `<option value="">-- Select --</option>` + data.map(e=>`<option>${e}</option>`).join('');
986
 
987
- if(preExp && data.includes(preExp)) {
 
 
 
 
988
  eSel.value = preExp;
989
  await this.onExpChange(preInst);
990
  } else {
991
- // Auto
992
- const auto = await fetch(`/api/auto_config?date=${d}&root=${root}`).then(r=>r.json());
993
- if(auto.current) {
994
- eSel.value = auto.current.expiry;
995
- await this.onExpChange(auto.current.instrument);
996
- }
 
997
  }
998
  },
999
 
1000
  async onExpChange(preInst) {
1001
  const root = document.getElementById('cfgRoot').value;
1002
  const exp = document.getElementById('cfgExp').value;
1003
- const d = App.state.date;
1004
  const iSel = document.getElementById('cfgInst');
 
1005
  iSel.innerHTML = '<option>Loading...</option>';
 
 
 
1006
 
1007
- const res = await fetch(`/api/instruments?date=${d}&root=${root}&expiry=${exp}`);
1008
- const data = await res.json();
1009
- iSel.innerHTML = data.map(i=>`<option>${i}</option>`).join('');
1010
- if(preInst && data.includes(preInst)) iSel.value = preInst;
1011
  },
1012
 
1013
  renderSeries(list) {
1014
- const div = document.getElementById('cfgSeries');
1015
- div.innerHTML = '';
1016
  list.forEach(s => {
1017
- const el = document.createElement('div');
1018
- el.className = 's-item';
1019
- el.innerHTML = `
1020
- <input type="color" class="s-color" value="${s.color}">
1021
- <input type="text" class="inp" value="${s.formula}" placeholder="$P">
1022
- <input type="text" class="inp" value="${s.label}" placeholder="Label">
1023
- <select class="inp" style="padding:0 4px;"><option value="left" ${s.axis==='left'?'selected':''}>L</option><option value="right" ${s.axis==='right'?'selected':''}>R</option></select>
1024
- <button class="tool-btn" style="color:var(--danger)" onclick="this.parentElement.remove()"></button>
 
 
 
1025
  `;
1026
- div.appendChild(el);
1027
  });
1028
  },
1029
 
1030
- addRow() {
1031
- const div = document.getElementById('cfgSeries');
1032
- const el = document.createElement('div');
1033
- el.className = 's-item';
1034
- el.innerHTML = `
1035
- <input type="color" class="s-color" value="#ffffff">
1036
- <input type="text" class="inp" value="$P">
1037
- <input type="text" class="inp" value="New">
1038
- <select class="inp" style="padding:0 4px;"><option value="left">L</option><option value="right">R</option></select>
1039
- <button class="tool-btn" style="color:var(--danger)" onclick="this.parentElement.remove()">✕</button>
1040
  `;
1041
- div.appendChild(el);
1042
  },
1043
 
1044
  save() {
1045
- const rows = document.querySelectorAll('#cfgSeries .s-item');
1046
- const series = Array.from(rows).map(r => ({
 
1047
  color: r.querySelector('.s-color').value,
1048
- formula: r.querySelector('input[type=text]').value, // first text input
1049
- label: r.querySelectorAll('input[type=text]')[1].value, // second
1050
- axis: r.querySelector('select').value
1051
  }));
1052
 
1053
  if(this.isGlobal) {
1054
- App.defaults = series;
 
1055
  } else {
1056
  const root = document.getElementById('cfgRoot').value;
1057
  const exp = document.getElementById('cfgExp').value;
1058
  const inst = document.getElementById('cfgInst').value;
1059
 
1060
- if(!root || !exp || !inst) { App.toast('Details incomplete'); return; }
1061
 
1062
  const config = {
1063
  root, expiry: exp, instrument: inst,
1064
  timeframe: document.getElementById('cfgTF').value,
1065
  start: document.getElementById('cfgStart').value,
1066
  end: document.getElementById('cfgEnd').value,
1067
- series: series
1068
  };
1069
-
1070
- App.state.charts[this.currId].config = config;
1071
- App.saveState();
1072
- ChartLogic.plot(this.currId);
1073
- if(App.state.mode === 'LIVE') ChartLogic.startLive(this.currId);
1074
  }
1075
-
1076
- App.toast('Configuration Saved');
1077
  this.close();
1078
  }
1079
  };
 
 
 
 
1080
  </script>
1081
  </body>
1082
  </html>
 
50
  def get_dates():
51
  """
52
  Fetch distinct dates.
 
 
53
  """
54
+ data = sb_get("/history_buckets_upstox", {"select": "minute", "order": "minute.desc", "limit": "50000"})
 
 
55
  dates = set()
56
  for row in data:
57
  if row.get('minute'):
 
207
 
208
 
209
  # ==========================================
210
+ # 5. FRONTEND TEMPLATE (NEW UI)
211
  # ==========================================
212
 
213
  HTML_TEMPLATE = """
 
215
  <html lang="en">
216
  <head>
217
  <meta charset="UTF-8">
218
+ <title>DepthChain Live Pro</title>
219
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
220
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;700&display=swap" rel="stylesheet">
 
 
221
  <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
 
222
  <style>
223
+ /* --- THEME --- */
224
  :root {
225
+ --bg-body: #0d1117;
226
+ --bg-header: #161b22;
227
+ --bg-card: #010409;
228
+ --bg-panel: #21262d;
229
+ --border: #30363d;
230
+ --text-primary: #e6edf3;
231
+ --text-secondary: #8b949e;
232
+ --accent: #2f81f7;
233
+ --success: #238636;
234
+ --danger: #da3633;
235
+ --warning: #d29922;
 
 
 
 
 
 
 
 
 
 
236
  }
237
+ * { box-sizing: border-box; margin: 0; padding: 0; }
 
 
 
 
 
 
 
 
238
  body {
239
+ background: var(--bg-body); color: var(--text-primary);
240
+ font-family: 'Inter', sans-serif; display: flex; flex-direction: column; height: 100vh; overflow: hidden;
241
  }
242
 
243
+ /* --- TOP BAR --- */
244
+ .top-bar {
245
+ height: 50px; background: var(--bg-header); border-bottom: 1px solid var(--border);
246
+ display: flex; align-items: center; justify-content: space-between; padding: 0 15px; flex-shrink: 0;
 
 
 
247
  }
248
+ .logo { font-weight: 800; font-size: 16px; margin-right: 20px; color: var(--text-primary); }
 
249
  .logo span { color: var(--accent); }
250
+
251
+ .controls { display: flex; gap: 10px; align-items: center; }
252
+
253
+ select, button, input {
254
+ background: var(--bg-panel); border: 1px solid var(--border); color: var(--text-primary);
255
+ padding: 5px 10px; border-radius: 4px; font-size: 12px; font-family: inherit; outline: none; cursor: pointer;
 
 
256
  }
257
+ select:focus, input:focus { border-color: var(--accent); }
258
+ button:hover { background: var(--border); }
259
+ .btn-accent { background: var(--accent); color: white; border: none; font-weight: 600; }
260
+ .btn-accent:hover { background: #58a6ff; }
261
+ .btn-danger { color: var(--danger); border-color: var(--border); }
262
+ .btn-danger:hover { background: rgba(218, 54, 51, 0.1); border-color: var(--danger); }
263
+
264
+ .layout-popover { position: relative; }
265
+ .layout-menu {
266
+ position: absolute; top: 35px; right: 0; background: var(--bg-header);
267
+ border: 1px solid var(--border); border-radius: 6px; padding: 10px;
268
+ display: none; grid-template-columns: 1fr; gap: 5px; z-index: 100; box-shadow: 0 4px 12px rgba(0,0,0,0.5);
269
+ width: 150px;
270
  }
271
+ .layout-menu.active { display: grid; }
272
+ .layout-option {
273
+ padding: 8px; text-align: center; border: 1px solid var(--border); border-radius: 4px; cursor: pointer;
 
274
  }
275
+ .layout-option:hover { background: var(--bg-panel); border-color: var(--text-secondary); }
 
276
 
277
+ /* --- MAIN GRID --- */
278
+ .main-content { flex: 1; overflow: hidden; display: flex; padding: 5px; gap: 5px; }
279
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
280
  .column {
281
+ flex: 1; display: flex; flex-direction: column; gap: 10px; overflow-y: auto;
282
+ padding: 5px; min-width: 300px;
283
+ }
284
+ .column-add-btn {
285
+ padding: 10px; border: 1px dashed var(--border); color: var(--text-secondary);
286
+ text-align: center; border-radius: 6px; cursor: pointer; margin-top: auto; font-size: 12px;
287
  }
288
+ .column-add-btn:hover { border-color: var(--text-primary); color: var(--text-primary); }
289
 
290
  /* --- CHART CARD --- */
291
  .chart-card {
292
+ background: var(--bg-card); border: 1px solid var(--border); border-radius: 6px;
293
+ display: flex; flex-direction: column; height: 400px; min-height: 400px; position: relative;
 
 
 
294
  }
295
+ .card-header {
296
+ height: 35px; border-bottom: 1px solid var(--border); display: flex; align-items: center; justify-content: space-between;
297
+ padding: 0 10px; background: var(--bg-header); font-size: 12px;
 
 
298
  }
299
+ .card-title { font-weight: 600; display: flex; gap: 8px; align-items: center; }
300
+ .card-meta { color: var(--text-secondary); font-family: 'JetBrains Mono', monospace; font-size: 11px; }
301
+ .card-actions { display: flex; gap: 5px; }
302
+ .icon-btn { padding: 2px 6px; font-size: 14px; border: none; background: transparent; color: var(--text-secondary); }
303
+ .icon-btn:hover { color: var(--text-primary); background: var(--border); }
304
+
305
+ .card-body { flex: 1; position: relative; padding: 5px; overflow: hidden; }
306
+ .empty-state {
307
+ display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%;
308
+ color: var(--text-secondary); font-size: 13px; gap: 10px;
 
309
  }
 
 
 
 
310
 
311
+ /* --- STATUS DOTS --- */
312
+ .status-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; background: var(--text-secondary); }
313
+ .status-dot.live { background: var(--success); box-shadow: 0 0 5px var(--success); animation: pulse 2s infinite; }
314
+ .status-dot.retry { background: var(--warning); }
315
+ .status-dot.hist { background: var(--accent); }
316
+ @keyframes pulse { 0% { opacity: 1; } 50% { opacity: 0.5; } 100% { opacity: 1; } }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
317
 
318
  /* --- MODAL --- */
319
+ .modal-overlay {
320
+ position: fixed; top: 0; left: 0; width: 100%; height: 100%;
321
+ background: rgba(0,0,0,0.7); backdrop-filter: blur(2px); z-index: 500;
322
+ display: none; justify-content: center; align-items: center;
323
  }
324
  .modal {
325
+ background: var(--bg-header); border: 1px solid var(--border); border-radius: 8px;
326
+ width: 500px; max-width: 95%; max-height: 90vh; display: flex; flex-direction: column;
327
+ box-shadow: 0 20px 40px rgba(0,0,0,0.5);
 
328
  }
329
+ .modal-head { padding: 15px; border-bottom: 1px solid var(--border); font-weight: 700; display: flex; justify-content: space-between; }
330
+ .modal-body { padding: 20px; overflow-y: auto; }
331
+ .modal-foot { padding: 15px; border-top: 1px solid var(--border); text-align: right; }
332
+
333
+ .form-group { margin-bottom: 15px; }
334
+ .form-group label { display: block; margin-bottom: 5px; color: var(--text-secondary); font-size: 11px; text-transform: uppercase; }
335
+ .form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
 
 
 
336
 
337
  /* Series List */
338
+ .series-row { display: grid; grid-template-columns: 30px 1fr 1fr 60px 20px; gap: 5px; margin-bottom: 5px; align-items: center; }
339
+ .series-row input[type="color"] { padding: 0; height: 30px; border: none; background: none; }
340
 
341
  /* Toast */
342
  .toast {
343
+ position: fixed; bottom: 20px; right: 20px; background: var(--accent); color: white;
344
+ padding: 10px 20px; border-radius: 4px; font-weight: 600; font-size: 13px;
345
+ transform: translateY(100px); transition: transform 0.3s; z-index: 1000;
 
346
  }
347
+ .toast.show { transform: translateY(0); }
 
348
 
349
  </style>
350
  </head>
351
  <body>
352
 
353
+ <!-- HEADER -->
354
+ <div class="top-bar">
355
  <div class="logo">DepthChain <span>PRO</span></div>
356
 
357
  <div class="controls">
358
+ <!-- Date Selection -->
359
+ <select id="globalDate" onchange="GlobalApp.onDateChange()"></select>
360
+
361
+ <!-- Mode Selection -->
362
+ <select id="globalMode" onchange="GlobalApp.onModeChange()">
363
+ <option value="HIST">Historical</option>
364
+ <option value="LIVE">Live (Auto Refresh)</option>
365
+ </select>
366
+
367
+ <div style="width: 1px; height: 20px; background: var(--border); margin: 0 10px;"></div>
368
+
369
+ <!-- Layout Selector -->
370
+ <div class="layout-popover">
371
+ <button onclick="document.getElementById('layoutMenu').classList.toggle('active')">
372
+ Layout / Columns ▼
373
+ </button>
374
+ <div class="layout-menu" id="layoutMenu">
375
+ <div class="layout-option" onclick="GlobalApp.setLayout(1)">1 Column</div>
376
+ <div class="layout-option" onclick="GlobalApp.setLayout(2)">2 Columns</div>
377
+ <div class="layout-option" onclick="GlobalApp.setLayout(3)">3 Columns</div>
378
+ </div>
379
  </div>
380
 
381
+ <button onclick="ConfigModal.openDefaults()">Global Defaults</button>
382
+ <button class="btn-danger" onclick="GlobalApp.resetApp()">Reset App</button>
383
  </div>
384
+ </div>
385
 
386
+ <!-- GRID CONTAINER -->
387
+ <div class="main-content" id="gridContainer">
388
+ <!-- Columns injected via JS -->
389
  </div>
390
 
391
+ <!-- MODALS -->
392
+ <div class="modal-overlay" id="configModalOverlay">
393
  <div class="modal">
394
+ <div class="modal-head">
395
+ <span id="cfgModalTitle">Configure Chart</span>
396
+ <button class="icon-btn" onclick="ConfigModal.close()">✕</button>
397
  </div>
398
+ <div class="modal-body">
399
+ <!-- Loading State -->
400
+ <div id="cfgLoader" style="display:none; text-align:center; padding:20px;">Loading...</div>
401
+
402
+ <!-- Config Form -->
403
  <div id="cfgForm">
404
+ <div class="form-row">
405
+ <div class="form-group">
406
+ <label>Symbol (Root)</label>
407
+ <select id="cfgRoot" onchange="ConfigModal.onRootChange()"></select>
408
  </div>
409
+ <div class="form-group">
410
  <label>Expiry</label>
411
+ <select id="cfgExp" onchange="ConfigModal.onExpChange()"></select>
412
  </div>
413
  </div>
414
+
415
+ <div class="form-group">
416
  <label>Instrument</label>
417
+ <select id="cfgInst"></select>
418
  </div>
419
+
420
+ <div class="form-row">
421
+ <div class="form-group">
422
  <label>Timeframe</label>
423
+ <select id="cfgTF">
424
  <option value="1min">1 Minute</option>
425
  <option value="3min">3 Minutes</option>
426
  <option value="5min">5 Minutes</option>
 
429
  <option value="1hour">1 Hour</option>
430
  </select>
431
  </div>
432
+ <div class="form-group">
433
+ <label>Time Range</label>
434
+ <div style="display:flex; gap:5px;">
435
+ <input type="time" id="cfgStart" value="09:15">
436
+ <input type="time" id="cfgEnd" value="15:30">
437
  </div>
438
  </div>
439
  </div>
440
+
441
+ <hr style="border:0; border-top:1px solid var(--border); margin:15px 0;">
442
 
443
+ <div class="form-group">
444
+ <label>Plot Lines (Variables: $P, $B, $S, $V, $OI)</label>
445
+ <div id="cfgSeriesList"></div>
446
+ <button style="width:100%; margin-top:5px;" onclick="ConfigModal.addSeriesRow()">+ Add Line</button>
 
 
447
  </div>
448
  </div>
449
  </div>
450
+ <div class="modal-foot">
451
+ <button onclick="ConfigModal.close()">Cancel</button>
452
+ <button class="btn-accent" onclick="ConfigModal.save()">Save Configuration</button>
453
  </div>
454
  </div>
455
  </div>
456
 
457
+ <!-- RESTORE SESSION POPUP -->
458
+ <div class="modal-overlay" id="restoreModal" style="display:none;">
459
+ <div class="modal" style="width: 350px;">
460
+ <div class="modal-head">Restore Session?</div>
461
+ <div class="modal-body">
462
+ We found a saved layout from your previous visit.
463
+ </div>
464
+ <div class="modal-foot">
465
+ <button class="btn-danger" onclick="GlobalApp.initNew()">Start Fresh</button>
466
+ <button class="btn-accent" onclick="GlobalApp.loadFromStorage()">Restore</button>
467
+ </div>
468
+ </div>
469
  </div>
470
 
471
+ <div id="toast" class="toast">Settings Saved</div>
472
+
473
  <script>
474
+
475
  /**
476
+ * --- GLOBAL STATE MANAGER ---
 
477
  */
478
+ const GlobalApp = {
479
  state: {
480
  date: '',
481
+ mode: 'HIST', // HIST or LIVE
482
+ columns: 1,
483
+ layout: [[], [], []], // 3 potential columns of chart IDs
484
+ charts: {} // ID -> { config: {}, instance: ChartJS, interval: null }
485
  },
 
486
 
487
  defaults: [
488
+ { formula: '$P', color: '#58a6ff', label: 'Price', axis: 'left' },
489
+ { formula: '$B', color: '#238636', label: 'Buy Qty', axis: 'right' },
490
+ { formula: '$S', color: '#da3633', label: 'Sell Qty', axis: 'right' }
491
  ],
492
 
493
  async init() {
494
+ // Load dates first
495
  await this.loadDates();
 
496
 
497
+ // Check local storage
498
+ if (localStorage.getItem('dc_state')) {
499
+ document.getElementById('restoreModal').style.display = 'flex';
500
+ } else {
501
+ this.initNew();
502
+ }
503
  },
504
 
505
  async loadDates() {
 
509
  const sel = document.getElementById('globalDate');
510
  sel.innerHTML = dates.map(d => `<option value="${d}">${d}</option>`).join('');
511
  if(dates.length) this.state.date = dates[0];
512
+ } catch(e) { console.error("Dates error", e); }
513
  },
514
 
515
+ initNew() {
516
+ document.getElementById('restoreModal').style.display = 'none';
517
+ this.setLayout(2); // Default 2 cols
518
+ this.addChart(0); // 1 chart in col 1
519
+ this.addChart(1); // 1 chart in col 2
520
+ },
 
 
 
 
 
 
 
 
521
 
522
+ loadFromStorage() {
523
+ try {
524
+ const raw = localStorage.getItem('dc_state');
525
+ const saved = JSON.parse(raw);
526
+
527
+ document.getElementById('restoreModal').style.display = 'none';
528
+ document.getElementById('globalDate').value = saved.date;
529
+ document.getElementById('globalMode').value = saved.mode;
530
+
531
+ this.state.date = saved.date;
532
+ this.state.mode = saved.mode;
533
+
534
+ // Rebuild Layout
535
+ this.setLayout(saved.columns);
536
+
537
+ // Rebuild Charts
538
+ saved.layout.forEach((colCharts, colIdx) => {
539
+ colCharts.forEach(chartData => {
540
+ this.addChart(colIdx, chartData.id, chartData.config);
541
  });
542
+ });
 
 
 
 
543
 
544
+ } catch(e) {
545
+ console.error("Load failed", e);
546
+ this.initNew();
 
 
 
 
547
  }
548
  },
549
 
550
+ saveToStorage() {
551
+ const minimalLayout = [[], [], []];
552
+ this.state.layout.forEach((col, idx) => {
553
+ minimalLayout[idx] = col.map(id => ({
554
+ id: id,
555
+ config: this.state.charts[id].config
556
+ }));
557
+ });
558
+
559
+ const dump = {
560
  date: this.state.date,
561
  mode: this.state.mode,
562
  columns: this.state.columns,
563
+ layout: minimalLayout
564
+ };
565
+ localStorage.setItem('dc_state', JSON.stringify(dump));
566
+ this.showToast("Layout Saved");
567
  },
568
 
569
+ resetApp() {
570
+ if(!confirm("Clear all settings and reset?")) return;
571
+ localStorage.removeItem('dc_state');
572
+ location.reload();
 
 
573
  },
574
 
575
+ showToast(msg) {
576
+ const t = document.getElementById('toast');
577
+ t.textContent = msg;
578
+ t.classList.add('show');
579
+ setTimeout(() => t.classList.remove('show'), 2000);
580
+ },
 
 
 
 
 
 
 
 
 
 
 
 
581
 
582
+ // --- LAYOUT LOGIC ---
583
+ setLayout(cols) {
584
+ this.state.columns = cols;
585
+ document.getElementById('layoutMenu').classList.remove('active');
586
  this.renderGrid();
 
587
  },
588
 
589
  renderGrid() {
590
+ const container = document.getElementById('gridContainer');
591
+ container.innerHTML = '';
 
 
 
592
 
593
+ for (let i = 0; i < this.state.columns; i++) {
594
  const colDiv = document.createElement('div');
595
  colDiv.className = 'column';
596
  colDiv.id = `col-${i}`;
597
+
598
+ // Append existing charts for this column from state
599
+ const chartIds = this.state.layout[i] || [];
600
+ chartIds.forEach(id => {
601
+ const el = this.createChartElement(id);
602
  colDiv.appendChild(el);
603
+ // Re-initialize chart if needed (canvas context is lost on re-append)
604
+ setTimeout(() => ChartLogic.render(id), 0);
605
  });
606
 
607
  // Add button
608
+ const addBtn = document.createElement('div');
609
+ addBtn.className = 'column-add-btn';
610
+ addBtn.innerHTML = '+ Add Chart';
611
+ addBtn.onclick = () => this.addChart(i);
612
+ colDiv.appendChild(addBtn);
613
 
614
  container.appendChild(colDiv);
615
  }
616
+ },
617
+
618
+ addChart(colIdx, forceId = null, forceConfig = null) {
619
+ const id = forceId || 'c_' + Date.now() + Math.random().toString(36).substr(2,5);
620
+
621
+ // Init State
622
+ this.state.charts[id] = {
623
+ col: colIdx,
624
+ config: forceConfig || {}, // Empty config means "Not Configured"
625
+ instance: null,
626
+ interval: null,
627
+ lastFetch: null
628
+ };
629
+
630
+ // Add to Layout Arrays
631
+ if (!this.state.layout[colIdx]) this.state.layout[colIdx] = [];
632
+ // prevent duplicate push if restoring
633
+ if (!this.state.layout[colIdx].includes(id)) {
634
+ this.state.layout[colIdx].push(id);
635
+ }
636
 
637
+ // Render UI
638
+ const colDiv = document.getElementById(`col-${colIdx}`);
639
+ const btn = colDiv.querySelector('.column-add-btn');
640
+ const el = this.createChartElement(id);
641
+ colDiv.insertBefore(el, btn);
642
+
643
+ // If config exists, try to plot
644
+ if (forceConfig && forceConfig.root) {
645
+ ChartLogic.plot(id);
646
+ }
647
+
648
+ this.saveToStorage();
649
  },
650
 
651
+ removeChart(id) {
652
+ if(!confirm("Remove chart?")) return;
653
+
654
+ // Cleanup Intervals
655
+ ChartLogic.stopLive(id);
656
+ if(this.state.charts[id].instance) this.state.charts[id].instance.destroy();
657
+
658
+ // Remove from Layout
659
+ const colIdx = this.state.charts[id].col;
660
+ this.state.layout[colIdx] = this.state.layout[colIdx].filter(x => x !== id);
661
+ delete this.state.charts[id];
662
+
663
+ // Remove DOM
664
+ document.getElementById(`card-${id}`).remove();
665
+ this.saveToStorage();
666
+ },
667
 
668
+ createChartElement(id) {
669
  const div = document.createElement('div');
670
  div.className = 'chart-card';
671
+ div.id = `card-${id}`;
672
  div.innerHTML = `
673
+ <div class="card-header">
674
+ <div class="card-title" id="title-${id}">
675
+ <span class="status-dot"></span>
676
+ <span>New Chart</span>
 
677
  </div>
678
+ <div class="card-actions">
679
+ <button class="icon-btn" onclick="ConfigModal.open('${id}')">⚙</button>
680
+ <button class="icon-btn" onclick="GlobalApp.removeChart('${id}')">✕</button>
681
  </div>
682
  </div>
683
  <div class="card-body" id="body-${id}">
684
+ <div class="empty-state">
685
+ <p>Not Configured</p>
686
+ <button onclick="ConfigModal.open('${id}')">Configure</button>
687
  </div>
688
  </div>
689
  `;
 
 
 
 
 
 
 
690
  return div;
691
  },
692
 
693
+ // --- GLOBAL CONTROLS ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
694
  onDateChange() {
695
  this.state.date = document.getElementById('globalDate').value;
696
+ this.saveToStorage();
697
+ // Reload all charts
 
 
 
698
  Object.keys(this.state.charts).forEach(id => ChartLogic.plot(id));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
699
  },
700
 
701
+ onModeChange() {
702
+ this.state.mode = document.getElementById('globalMode').value;
703
+ this.saveToStorage();
704
+ // Toggle intervals
705
  Object.keys(this.state.charts).forEach(id => {
706
+ if(this.state.mode === 'LIVE') ChartLogic.startLive(id);
707
  else ChartLogic.stopLive(id);
708
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
709
  }
710
  };
711
 
 
 
712
  /**
713
+ * --- CHART LOGIC & DATA ---
714
  */
715
  const ChartLogic = {
716
+
717
  async plot(id, isUpdate=false) {
718
+ const chartData = GlobalApp.state.charts[id];
719
+ const cfg = chartData.config;
 
 
 
 
 
 
 
 
720
 
721
+ if (!cfg || !cfg.root) return; // Not configured
722
+
723
+ // UI Update
724
+ const titleEl = document.querySelector(`#card-${id} .card-title`);
725
+ const dotClass = GlobalApp.state.mode === 'LIVE' ? 'live' : 'hist';
726
+ titleEl.innerHTML = `
727
+ <span class="status-dot ${dotClass}"></span>
728
+ ${cfg.root} <span style="color:var(--accent);">${cfg.expiry}</span>
729
+ <span class="card-meta">${cfg.instrument} | ${cfg.timeframe}</span>
730
+ `;
731
 
732
+ // Ensure Canvas
733
+ const body = document.getElementById(`body-${id}`);
734
+ if (!body.querySelector('canvas')) {
735
+ body.innerHTML = `<canvas id="canvas-${id}"></canvas>`;
736
  }
737
 
738
+ // Prepare Status Indicator for Live
739
+ if(isUpdate) titleEl.querySelector('.status-dot').classList.add('retry');
 
 
 
740
 
741
  try {
742
  const res = await fetch('/api/fetch_series', {
743
  method: 'POST',
744
+ headers: {'Content-Type': 'application/json'},
745
  body: JSON.stringify({
746
+ date: GlobalApp.state.date,
747
+ root: cfg.root,
748
+ expiry: cfg.expiry,
749
+ instrument: cfg.instrument,
750
+ timeframe: cfg.timeframe,
751
+ start_time: cfg.start,
752
+ end_time: cfg.end
753
  })
754
  });
755
  const data = await res.json();
756
+
757
+ // Handle "Live but no data yet" or "Empty"
758
+ if(data.error || !data.labels || data.labels.length === 0) {
759
+ if(GlobalApp.state.mode === 'LIVE') {
760
+ // Keep old data, just indicate retry
761
+ titleEl.querySelector('.status-dot').className = 'status-dot retry';
762
+ return;
763
+ } else {
764
+ // Historical empty
765
+ if(chartData.instance) chartData.instance.destroy();
766
+ body.innerHTML = '<div class="empty-state">No Data Found</div>';
767
+ return;
768
  }
 
769
  }
770
 
771
+ // Success: Update Dot
772
+ titleEl.querySelector('.status-dot').className = `status-dot ${dotClass}`;
773
 
774
+ // Compute Datasets
775
  const datasets = cfg.series.map(s => {
776
+ const computed = data.labels.map((_, i) => {
777
+ const P = data.P[i] || 0, V = data.V[i] || 0, OI = data.OI[i] || 0;
778
+ const B = data.B[i] || 0, S = data.S[i] || 0;
779
+ let f = s.formula
780
+ .replace(/\$OI/g, OI).replace(/\$P/g, P).replace(/\$V/g, V)
781
+ .replace(/\$B/g, B).replace(/\$S/g, S);
782
+ try { return new Function('return ' + f)(); } catch { return null; }
783
  });
784
+
785
  return {
786
+ label: s.label,
787
+ data: computed,
788
+ borderColor: s.color,
789
+ backgroundColor: s.color,
790
+ borderWidth: 1.5,
791
+ pointRadius: 0,
792
+ pointHoverRadius: 4,
793
+ yAxisID: s.axis === 'left' ? 'y' : 'y1',
794
+ tension: 0.1
795
  };
796
  });
797
 
798
  this.renderCanvas(id, data.labels, datasets);
799
 
800
+ } catch (e) {
801
+ console.error("Plot Error", e);
802
+ }
803
  },
804
 
805
  renderCanvas(id, labels, datasets) {
806
+ const ctx = document.getElementById(`canvas-${id}`);
807
+ if (!ctx) return;
808
+
809
+ const chartData = GlobalApp.state.charts[id];
810
+
811
+ // If chart exists, update data (prevent flicker)
812
+ if (chartData.instance) {
813
+ chartData.instance.data.labels = labels;
814
+ chartData.instance.data.datasets = datasets;
815
+ chartData.instance.update('none'); // 'none' mode for performance
816
  } else {
817
+ // Create New
818
  const hasRight = datasets.some(d => d.yAxisID === 'y1');
819
+ chartData.instance = new Chart(ctx.getContext('2d'), {
820
  type: 'line',
821
  data: { labels, datasets },
822
  options: {
823
+ responsive: true,
824
+ maintainAspectRatio: false,
825
+ animation: false,
826
  interaction: { mode: 'index', intersect: false },
827
+ plugins: {
828
+ legend: { labels: { color: '#8b949e', font: {size:10} } },
829
+ tooltip: { backgroundColor: '#161b22', titleColor:'#e6edf3', bodyColor:'#e6edf3', borderColor:'#30363d', borderWidth:1 }
830
  },
831
  scales: {
832
+ x: { grid: { color: '#30363d', tickLength: 0 }, ticks: { color: '#8b949e', maxTicksLimit: 10 } },
833
+ y: { type: 'linear', display: true, position: 'left', grid: { color: '#30363d' }, ticks: { color: '#8b949e' } },
834
+ y1: { type: 'linear', display: hasRight, position: 'right', grid: { display: false }, ticks: { color: '#8b949e' } }
835
  }
836
  }
837
  });
838
  }
839
  },
840
 
841
+ // --- LIVE MODE ---
842
  startLive(id) {
843
+ this.stopLive(id); // Safety clear
844
+ // Check if date is today (simple check)
845
+ const selected = GlobalApp.state.date;
846
+ const today = new Date().toISOString().split('T')[0];
847
+
848
+ // In real app, might compare accurately. Assuming user selects correct date or backend handles it.
849
+ // We just poll regardless if Mode is Live.
850
 
851
+ // Initial Plot
852
  this.plot(id);
853
+
854
+ // Interval
855
+ GlobalApp.state.charts[id].interval = setInterval(() => {
856
+ this.plot(id, true);
857
+ }, 15000); // 15 seconds
858
  },
859
 
860
  stopLive(id) {
861
+ const c = GlobalApp.state.charts[id];
862
+ if(c && c.interval) {
863
+ clearInterval(c.interval);
864
+ c.interval = null;
865
+ }
866
+ },
867
+
868
+ // Re-render chart instance if DOM moved
869
+ render(id) {
870
+ const c = GlobalApp.state.charts[id];
871
+ if(c.config && c.config.root) {
872
+ if(c.instance) c.instance.destroy();
873
+ c.instance = null;
874
+ this.plot(id);
875
+ if(GlobalApp.state.mode === 'LIVE') this.startLive(id);
876
+ }
877
  }
878
  };
879
 
880
  /**
881
+ * --- CONFIGURATION MODAL ---
882
  */
883
  const ConfigModal = {
884
+ currentId: null,
885
  isGlobal: false,
886
 
887
  async open(id) {
888
+ this.currentId = id;
889
  this.isGlobal = false;
890
+ document.getElementById('cfgModalTitle').textContent = "Configure Chart";
891
+ document.getElementById('configModalOverlay').style.display = 'flex';
892
  document.getElementById('cfgForm').style.display = 'block';
893
+ document.getElementById('cfgLoader').style.display = 'none';
894
 
895
+ // Pre-fill
896
+ const c = GlobalApp.state.charts[id].config;
897
 
898
+ // Load Roots
899
+ await this.loadRoots(c.root);
 
 
 
900
 
901
+ document.getElementById('cfgTF').value = c.timeframe || '1min';
902
+ document.getElementById('cfgStart').value = c.start || '09:15';
903
+ document.getElementById('cfgEnd').value = c.end || '15:30';
904
+
905
+ // Series
906
+ const series = c.series && c.series.length ? c.series : GlobalApp.defaults;
907
+ this.renderSeries(series);
908
+
909
+ // If saved state exists, trigger cascades
910
+ if(c.root) {
911
+ document.getElementById('cfgRoot').value = c.root;
912
+ await this.onRootChange(c.expiry, c.instrument);
913
  }
914
  },
915
 
916
  openDefaults() {
917
  this.isGlobal = true;
918
+ document.getElementById('cfgModalTitle').textContent = "Global Default Series";
919
+ document.getElementById('configModalOverlay').style.display = 'flex';
920
+ // Hide specific fields
921
+ document.getElementById('cfgForm').style.display = 'block';
922
+ // Actually, we just want series editing.
923
+ // Quick hack: hide form rows, show series only.
924
+ // Ideally separate modal, but re-using for brevity.
925
+ this.renderSeries(GlobalApp.defaults);
926
  },
927
 
928
+ close() {
929
+ document.getElementById('configModalOverlay').style.display = 'none';
930
+ },
931
 
932
+ async loadRoots(preSelect) {
933
+ const rSel = document.getElementById('cfgRoot');
934
+ rSel.innerHTML = '<option>Loading...</option>';
935
  try {
936
+ const date = GlobalApp.state.date;
937
+ const res = await fetch(`/api/roots?date=${date}`);
938
+ const roots = await res.json();
939
+ rSel.innerHTML = `<option value="">-- Select --</option>` + roots.map(r => `<option>${r}</option>`).join('');
940
+ if(preSelect && roots.includes(preSelect)) rSel.value = preSelect;
941
+ } catch(e) { rSel.innerHTML = '<option>Error</option>'; }
942
  },
943
 
944
  async onRootChange(preExp, preInst) {
945
  const root = document.getElementById('cfgRoot').value;
946
+ const date = GlobalApp.state.date;
947
  const eSel = document.getElementById('cfgExp');
948
  eSel.innerHTML = '<option>Loading...</option>';
 
 
 
 
949
 
950
+ const res = await fetch(`/api/expiries?date=${date}&root=${root}`);
951
+ const exps = await res.json();
952
+ eSel.innerHTML = `<option value="">-- Select --</option>` + exps.map(e => `<option>${e}</option>`).join('');
953
+
954
+ if(preExp && exps.includes(preExp)) {
955
  eSel.value = preExp;
956
  await this.onExpChange(preInst);
957
  } else {
958
+ // Auto select current?
959
+ // Fetch auto config for smart defaults
960
+ const auto = await fetch(`/api/auto_config?date=${date}&root=${root}`).then(r=>r.json());
961
+ if(auto.current) {
962
+ eSel.value = auto.current.expiry;
963
+ await this.onExpChange(auto.current.instrument);
964
+ }
965
  }
966
  },
967
 
968
  async onExpChange(preInst) {
969
  const root = document.getElementById('cfgRoot').value;
970
  const exp = document.getElementById('cfgExp').value;
971
+ const date = GlobalApp.state.date;
972
  const iSel = document.getElementById('cfgInst');
973
+
974
  iSel.innerHTML = '<option>Loading...</option>';
975
+ const res = await fetch(`/api/instruments?date=${date}&root=${root}&expiry=${exp}`);
976
+ const insts = await res.json();
977
+ iSel.innerHTML = `<option value="">-- Select --</option>` + insts.map(i => `<option>${i}</option>`).join('');
978
 
979
+ if(preInst && insts.includes(preInst)) iSel.value = preInst;
980
+ else if(insts.length > 0) iSel.value = insts[0]; // Select first default
 
 
981
  },
982
 
983
  renderSeries(list) {
984
+ const container = document.getElementById('cfgSeriesList');
985
+ container.innerHTML = '';
986
  list.forEach(s => {
987
+ const div = document.createElement('div');
988
+ div.className = 'series-row';
989
+ div.innerHTML = `
990
+ <input type="color" value="${s.color}" class="s-color">
991
+ <input type="text" value="${s.formula}" class="s-formula" placeholder="$P">
992
+ <input type="text" value="${s.label}" class="s-label" placeholder="Label">
993
+ <select class="s-axis">
994
+ <option value="left" ${s.axis==='left'?'selected':''}>L</option>
995
+ <option value="right" ${s.axis==='right'?'selected':''}>R</option>
996
+ </select>
997
+ <button class="btn-danger" style="padding:0;" onclick="this.parentElement.remove()">✕</button>
998
  `;
999
+ container.appendChild(div);
1000
  });
1001
  },
1002
 
1003
+ addSeriesRow() {
1004
+ const container = document.getElementById('cfgSeriesList');
1005
+ const div = document.createElement('div');
1006
+ div.className = 'series-row';
1007
+ div.innerHTML = `
1008
+ <input type="color" value="#ffffff" class="s-color">
1009
+ <input type="text" value="$P" class="s-formula">
1010
+ <input type="text" value="New" class="s-label">
1011
+ <select class="s-axis"><option value="left">L</option><option value="right">R</option></select>
1012
+ <button class="btn-danger" style="padding:0;" onclick="this.parentElement.remove()">✕</button>
1013
  `;
1014
+ container.appendChild(div);
1015
  },
1016
 
1017
  save() {
1018
+ // Collect Series
1019
+ const rows = document.querySelectorAll('#cfgSeriesList .series-row');
1020
+ const newSeries = Array.from(rows).map(r => ({
1021
  color: r.querySelector('.s-color').value,
1022
+ formula: r.querySelector('.s-formula').value,
1023
+ label: r.querySelector('.s-label').value,
1024
+ axis: r.querySelector('.s-axis').value
1025
  }));
1026
 
1027
  if(this.isGlobal) {
1028
+ GlobalApp.defaults = newSeries;
1029
+ GlobalApp.showToast("Defaults Updated");
1030
  } else {
1031
  const root = document.getElementById('cfgRoot').value;
1032
  const exp = document.getElementById('cfgExp').value;
1033
  const inst = document.getElementById('cfgInst').value;
1034
 
1035
+ if(!root || !exp || !inst) { alert("Select Symbol details"); return; }
1036
 
1037
  const config = {
1038
  root, expiry: exp, instrument: inst,
1039
  timeframe: document.getElementById('cfgTF').value,
1040
  start: document.getElementById('cfgStart').value,
1041
  end: document.getElementById('cfgEnd').value,
1042
+ series: newSeries
1043
  };
1044
+
1045
+ GlobalApp.state.charts[this.currentId].config = config;
1046
+ GlobalApp.saveToStorage();
1047
+ ChartLogic.render(this.currentId); // Re-plot
 
1048
  }
 
 
1049
  this.close();
1050
  }
1051
  };
1052
+
1053
+ // Start App
1054
+ window.onload = () => GlobalApp.init();
1055
+
1056
  </script>
1057
  </body>
1058
  </html>