Hemaambika commited on
Commit
12ec337
·
1 Parent(s): 464a39f

update dashboard comp

Browse files
src/app/dashboard/dashboard.component.html CHANGED
@@ -1,33 +1,82 @@
1
  <div class="wrap">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  <div class="header">
3
- <div class="brand">Market / Dashboard</div>
4
- <div class="tag">Last updated: {{ lastUpdated }}</div>
 
 
 
 
 
 
 
5
  </div>
6
 
7
  <!-- 1) Important Indices -->
8
  <div class="section" id="section-indices">
9
- <h2>Important Indices (Current Price)</h2>
10
- <div class="indices">
11
- <div class="idx" *ngFor="let idx of indices; let i = index">
12
- <div class="name">{{ idx.code }}</div>
13
- <div class="row">
14
- <div class="price">{{ fmt(idx.price) }}</div>
15
- <div class="chg" [ngClass]="{ up: idx.chg >= 0, down: idx.chg < 0 }">
16
- {{ idx.chg >= 0 ? '+' : '' }}{{ idx.chg.toFixed(2) }}%
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
  </div>
18
- </div>
19
- <div class="mini">
20
- <svg preserveAspectRatio="none" viewBox="0 0 240 28" aria-hidden="true">
21
- <path [attr.d]="idx.miniPath"
22
- fill="none"
23
- [attr.stroke]="idx.isUp ? 'var(--up)' : 'var(--down)'"
24
- stroke-width="2">
25
- </path>
26
- </svg>
27
- </div>
28
- </div>
 
 
 
 
 
 
 
 
 
 
29
  </div>
30
-
31
  </div>
32
 
33
  <!-- 2) Companies under each Index (with Buy/Sell) + 3) Today Chart -->
@@ -44,7 +93,14 @@
44
 
45
  <div class="cols">
46
  <div>
47
- <div class="table-wrap">
 
 
 
 
 
 
 
48
  <table aria-label="Companies">
49
  <thead>
50
  <tr>
@@ -57,7 +113,7 @@
57
  </tr>
58
  </thead>
59
  <tbody>
60
- <tr *ngFor="let c of companiesByIndex[activeIndex]; let i = index">
61
  <td>
62
  <a href="#" (click)="onCompanyClick($event, c)" class="co">{{ c.sym }}</a>
63
  </td>
@@ -83,12 +139,7 @@
83
  <div class="chart-title">Today Chart: {{ chartSymbol || '—' }}</div>
84
  <div class="legend">1-minute simulated intraday</div>
85
  </div>
86
- <canvas #chartCanvas
87
- id="chartCanvas"
88
- width="800"
89
- height="260"
90
- aria-label="Intraday chart">
91
- </canvas>
92
  </div>
93
  <div class="note">Click a company to update the chart.</div>
94
  </div>
@@ -160,6 +211,6 @@
160
  </div>
161
 
162
  </div>
163
-
164
  </div>
165
  </div>
 
1
  <div class="wrap">
2
+
3
+ <!-- Market Overview ticker (right-to-left auto scroll) -->
4
+ <div class="section market-overview" id="section-market-overview">
5
+ <div class="mo-header" style="display:flex; justify-content:space-between; align-items:flex-start;">
6
+ <h2>Market Overview</h2>
7
+ <div class="tag" style="font-size:0.85rem; opacity:0.9;">Last updated: {{ lastUpdated }}</div>
8
+ </div>
9
+ <div class="ticker" aria-hidden="false">
10
+ <div class="ticker-track" #tickerTrack>
11
+ <div class="mcard" *ngFor="let m of marketCardsRepeat">
12
+ <h3>{{ m.title }}</h3>
13
+ <div class="mval">{{ m.value }} <span class="chg" [ngClass]="{ up: m.dir === 'up', down: m.dir === 'down', neutral: m.dir === 'neutral' }">({{ m.chg }})</span></div>
14
+ </div>
15
+ </div>
16
+ </div>
17
+ </div>
18
+
19
+ <!-- Header bar: show country pills here (removed 'Market / Dashboard' text) -->
20
  <div class="header">
21
+ <div class="country-row" style="padding:10px 12px; display:flex; align-items:center; gap:8px;">
22
+ <div class="country-list">
23
+ <ng-container *ngFor="let country of countries">
24
+ <button class="tab" *ngIf="country !== 'All'" [class.active]="selectedCountry === country" (click)="selectCountry(country)">
25
+ {{ country }}
26
+ </button>
27
+ </ng-container>
28
+ </div>
29
+ </div>
30
  </div>
31
 
32
  <!-- 1) Important Indices -->
33
  <div class="section" id="section-indices">
34
+ <h2>Global Market Indices (Live)</h2>
35
+ <div>
36
+ <!-- Country filter moved to header; removed duplicate here -->
37
+ </div>
38
+
39
+ <!-- Cards grid for indices of selected country -->
40
+ <div class="indices-grid" style="margin-top:12px;">
41
+ <!-- When India selected, show nine simple index cards (name + placeholder price) -->
42
+ <ng-container *ngIf="(selectedCountry || '').toLowerCase() === 'india'; else otherIndices">
43
+ <ng-container *ngFor="let g of displayIndices">
44
+ <div class="index-card">
45
+ <div class="ic-header">
46
+ <div class="ic-name">{{ formatIndexName(g.name) }}</div>
47
+ <div class="ic-price">{{ g.price | number:'1.2-2' }}</div>
48
+ </div>
49
+ <div class="ic-row">
50
+ <div class="ic-change" [ngClass]="{ up: g.isUp, down: !g.isUp }">
51
+ {{ g.changePct !== null && g.changePct !== undefined ? (g.changePct >= 0 ? '+' : '') + (g.changePct | number:'1.2-2') + '%' : (g.change !== null && g.change !== undefined ? (g.change >= 0 ? '+' : '') + (g.change | number:'1.2-2') : '—') }}
52
+ </div>
53
+ </div>
54
+ <svg *ngIf="g.miniPath" preserveAspectRatio="none" viewBox="0 0 240 28" aria-hidden="true" class="ic-spark">
55
+ <path [attr.d]="g.miniPath" fill="none" [attr.stroke]="g.isUp ? 'var(--up)' : 'var(--down)'" stroke-width="2"></path>
56
+ </svg>
57
  </div>
58
+ </ng-container>
59
+ </ng-container>
60
+
61
+ <ng-template #otherIndices>
62
+ <ng-container *ngFor="let g of displayIndices">
63
+ <div class="index-card">
64
+ <div class="ic-header">
65
+ <div class="ic-name">{{ formatIndexName(g.name) }}</div>
66
+ <div class="ic-price">{{ g.price | number:'1.2-2' }}</div>
67
+ </div>
68
+ <div class="ic-row">
69
+ <div class="ic-change" [ngClass]="{ up: g.isUp, down: !g.isUp }">
70
+ {{ g.changePct !== null && g.changePct !== undefined ? (g.changePct >= 0 ? '+' : '') + (g.changePct | number:'1.2-2') + '%' : (g.change !== null && g.change !== undefined ? (g.change >= 0 ? '+' : '') + (g.change | number:'1.2-2') : '—') }}
71
+ </div>
72
+ </div>
73
+ <svg *ngIf="g.miniPath" preserveAspectRatio="none" viewBox="0 0 240 28" aria-hidden="true" class="ic-spark">
74
+ <path [attr.d]="g.miniPath" fill="none" [attr.stroke]="g.isUp ? 'var(--up)' : 'var(--down)'" stroke-width="2"></path>
75
+ </svg>
76
+ </div>
77
+ </ng-container>
78
+ </ng-template>
79
  </div>
 
80
  </div>
81
 
82
  <!-- 2) Companies under each Index (with Buy/Sell) + 3) Today Chart -->
 
93
 
94
  <div class="cols">
95
  <div>
96
+ <div class="table-wrap" style="position:relative">
97
+ <div *ngIf="quotesLoading" style="position:absolute; inset:0; display:flex; align-items:center; justify-content:center; background: rgba(0,0,0,0.45); z-index:5; border-radius:8px;">
98
+ <svg width="48" height="48" viewBox="0 0 50 50" aria-hidden="true">
99
+ <circle cx="25" cy="25" r="20" stroke="#ffffff" stroke-width="4" fill="none" stroke-linecap="round" stroke-dasharray="31.4 31.4">
100
+ <animateTransform attributeName="transform" type="rotate" from="0 25 25" to="360 25 25" dur="1s" repeatCount="indefinite" />
101
+ </circle>
102
+ </svg>
103
+ </div>
104
  <table aria-label="Companies">
105
  <thead>
106
  <tr>
 
113
  </tr>
114
  </thead>
115
  <tbody>
116
+ <tr *ngFor="let c of companiesByIndex[activeIndex]; let i = index" [class.selected]="c.sym === selectedCompany">
117
  <td>
118
  <a href="#" (click)="onCompanyClick($event, c)" class="co">{{ c.sym }}</a>
119
  </td>
 
139
  <div class="chart-title">Today Chart: {{ chartSymbol || '—' }}</div>
140
  <div class="legend">1-minute simulated intraday</div>
141
  </div>
142
+ <div #chartContainer class="chart-svg" aria-label="Intraday chart" role="img"></div>
 
 
 
 
 
143
  </div>
144
  <div class="note">Click a company to update the chart.</div>
145
  </div>
 
211
  </div>
212
 
213
  </div>
214
+
215
  </div>
216
  </div>
src/app/dashboard/dashboard.component.scss CHANGED
@@ -24,7 +24,7 @@ a {
24
  text-decoration: none;
25
  }
26
 
27
- .wrap {
28
  padding: 3vw;
29
  padding-top: 8vw;
30
  }
@@ -68,6 +68,19 @@ h2 {
68
  margin: 16px 0;
69
  }
70
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71
  /* Index cards */
72
  .indices {
73
  display: grid;
@@ -87,53 +100,86 @@ h2 {
87
  }
88
  }
89
 
 
90
  .idx {
91
  background: linear-gradient(180deg, rgba(255, 255, 255, 0.02), rgba(0, 0, 0, 0));
92
  border: 1px solid var(--border);
93
- border-radius: 12px;
94
- padding: 12px;
 
95
  }
96
 
97
  .idx .name {
98
  font-weight: 600;
 
99
  }
100
 
101
  .idx .row {
102
  display: flex;
103
  align-items: baseline;
104
- gap: 10px;
105
  margin-top: 6px;
106
  }
107
 
108
- .price {
109
- font-size: 20px;
110
  font-weight: 700;
111
  }
112
 
113
- .chg {
114
- font-size: 13px;
115
  padding: 2px 8px;
116
  border-radius: 999px;
117
  background: var(--soft);
118
  }
119
 
120
- .chg.up {
121
- color: var(--up);
 
 
122
  }
123
 
124
- .chg.down {
125
- color: var(--down);
 
126
  }
127
 
128
- .mini {
129
- margin-top: 8px;
130
- height: 28px;
131
- width: 100%;
 
132
  }
133
 
134
- .mini svg {
135
- width: 100%;
136
- height: 100%;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
137
  }
138
 
139
  /* Tabs (indices -> companies) */
@@ -159,6 +205,34 @@ h2 {
159
  border-color: transparent;
160
  }
161
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
162
  .table-wrap {
163
  border: 1px solid var(--border);
164
  border-radius: 12px;
@@ -171,6 +245,17 @@ table {
171
  min-width: 620px;
172
  }
173
 
 
 
 
 
 
 
 
 
 
 
 
174
  th,
175
  td {
176
  padding: 10px 12px;
@@ -189,15 +274,14 @@ th {
189
  .btn {
190
  padding: 6px 10px;
191
  border-radius: 8px;
192
- border: 1px solid var(--border);
193
  cursor: pointer;
194
  font-weight: 600;
195
- color:white;
196
  }
197
 
198
  .btn.buy {
199
  background: green;
200
-
201
  /* border-color: rgba(18, 196, 139, 0.35);*/
202
  }
203
 
@@ -254,6 +338,13 @@ canvas {
254
  background: linear-gradient(180deg, rgba(255, 255, 255, 0.02), transparent);
255
  }
256
 
 
 
 
 
 
 
 
257
  /* Sector grid */
258
  .sector-grid {
259
  display: grid;
@@ -324,3 +415,78 @@ canvas {
324
  font-size: 12px;
325
  margin-top: 6px;
326
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
  text-decoration: none;
25
  }
26
 
27
+ .wrap {
28
  padding: 3vw;
29
  padding-top: 8vw;
30
  }
 
68
  margin: 16px 0;
69
  }
70
 
71
+ /* Ensure market overview ticker is clipped inside the rounded card */
72
+ .section.market-overview {
73
+ overflow: hidden; /* clip animated content to card */
74
+ position: relative; /* establish containing block for absolute/animated children */
75
+ }
76
+
77
+ /* Ticker container should also hide overflow so animation doesn't escape */
78
+ .ticker {
79
+ width: 100%;
80
+ overflow: hidden;
81
+ position: relative;
82
+ }
83
+
84
  /* Index cards */
85
  .indices {
86
  display: grid;
 
100
  }
101
  }
102
 
103
+ /* compact .idx used for the small preview cards (top row) */
104
  .idx {
105
  background: linear-gradient(180deg, rgba(255, 255, 255, 0.02), rgba(0, 0, 0, 0));
106
  border: 1px solid var(--border);
107
+ border-radius: 10px;
108
+ padding: 8px; /* reduced */
109
+ min-height: 70px;
110
  }
111
 
112
  .idx .name {
113
  font-weight: 600;
114
+ font-size: 13px;
115
  }
116
 
117
  .idx .row {
118
  display: flex;
119
  align-items: baseline;
120
+ gap: 8px;
121
  margin-top: 6px;
122
  }
123
 
124
+ .idx .price {
125
+ font-size: 16px;
126
  font-weight: 700;
127
  }
128
 
129
+ .idx .chg {
130
+ font-size: 12px;
131
  padding: 2px 8px;
132
  border-radius: 999px;
133
  background: var(--soft);
134
  }
135
 
136
+ .idx .mini {
137
+ margin-top: 8px;
138
+ height: 22px; /* smaller sparkline */
139
+ width: 100%;
140
  }
141
 
142
+ .idx .mini svg {
143
+ height: 100%;
144
+ width: 100%;
145
  }
146
 
147
+ /* Make the detailed indices grid use compact cards as well */
148
+ .indices-grid {
149
+ display: flex;
150
+ flex-wrap: wrap;
151
+ gap: 12px;
152
  }
153
 
154
+ @media (max-width: 1100px) {
155
+ .indices-grid {
156
+ grid-template-columns: repeat(3, 1fr);
157
+ }
158
+ }
159
+
160
+ @media (max-width: 700px) {
161
+ .indices-grid {
162
+ grid-template-columns: repeat(2, 1fr);
163
+ }
164
+ }
165
+
166
+ /* Ensure index-card component inside indices-grid stays compact */
167
+ .indices-grid .index-card {
168
+ padding: 8px;
169
+ border-radius: 10px;
170
+ min-height: 80px;
171
+ }
172
+
173
+ .indices-grid .index-card .name {
174
+ font-size: 13px;
175
+ }
176
+
177
+ .indices-grid .index-card .price {
178
+ font-size: 16px;
179
+ }
180
+
181
+ .indices-grid .index-card .mini {
182
+ height: 22px;
183
  }
184
 
185
  /* Tabs (indices -> companies) */
 
205
  border-color: transparent;
206
  }
207
 
208
+ /* Country buttons styled like tabs/pills */
209
+ .country-list {
210
+ display: flex;
211
+ gap: 8px;
212
+ align-items: center;
213
+ }
214
+
215
+ .country {
216
+ background: var(--soft);
217
+ color: var(--text);
218
+ padding: 6px 10px;
219
+ border-radius: 999px;
220
+ border: 1px solid var(--border);
221
+ cursor: pointer;
222
+ font-size: 13px;
223
+ line-height: 1;
224
+ }
225
+
226
+ .country.active {
227
+ background: var(--accent);
228
+ color: #0b1020;
229
+ border-color: transparent;
230
+ }
231
+
232
+ .tab.active, .country.active {
233
+ box-shadow: 0 4px 10px rgba(0,0,0,0.12);
234
+ }
235
+
236
  .table-wrap {
237
  border: 1px solid var(--border);
238
  border-radius: 12px;
 
245
  min-width: 620px;
246
  }
247
 
248
+ /* Highlight selected company row: pink company name */
249
+ table tr.selected td a.co {
250
+ color: #ff66b2; /* pink */
251
+ font-weight: 700;
252
+ }
253
+
254
+ /* also highlight entire row lightly */
255
+ table tr.selected {
256
+ background: rgba(255, 102, 178, 0.03);
257
+ }
258
+
259
  th,
260
  td {
261
  padding: 10px 12px;
 
274
  .btn {
275
  padding: 6px 10px;
276
  border-radius: 8px;
277
+ border: 1px solid var(--border);
278
  cursor: pointer;
279
  font-weight: 600;
280
+ color: white;
281
  }
282
 
283
  .btn.buy {
284
  background: green;
 
285
  /* border-color: rgba(18, 196, 139, 0.35);*/
286
  }
287
 
 
338
  background: linear-gradient(180deg, rgba(255, 255, 255, 0.02), transparent);
339
  }
340
 
341
+ .chart-svg {
342
+ width: 100%;
343
+ height: 260px;
344
+ border-radius: 10px;
345
+ background: linear-gradient(180deg, rgba(255,255,255,0.01), transparent);
346
+ padding: 6px;
347
+ }
348
  /* Sector grid */
349
  .sector-grid {
350
  display: grid;
 
415
  font-size: 12px;
416
  margin-top: 6px;
417
  }
418
+
419
+ /* Improve the marquee so the market overview cards run smoothly right-to-left. Use a CSS variable for duration, make the track width adapt to content and translate by -50% for seamless loop. Add will-change and hardware-accelerated transform, and ensure .mcard are non-shrinking. */
420
+ .ticker-track {
421
+ display: flex;
422
+ gap: 16px;
423
+ align-items: center;
424
+ padding: 12px 8px;
425
+ /* ensure items don't shrink and track width matches content (we duplicate items in template) */
426
+ white-space: nowrap;
427
+ will-change: transform;
428
+ --marquee-duration: 28s; /* adjust speed */
429
+ animation: marquee var(--marquee-duration) linear infinite;
430
+ }
431
+
432
+ .mcard {
433
+ flex: 0 0 auto; /* prevent shrinking/expanding */
434
+ min-width: 160px;
435
+ max-width: 260px;
436
+ padding: 8px 12px;
437
+ border-radius: 10px;
438
+ background: rgba(255,255,255,0.02);
439
+ border: 1px solid rgba(255,255,255,0.02);
440
+ }
441
+
442
+ @keyframes marquee {
443
+ 0% {
444
+ transform: translateX(0);
445
+ }
446
+
447
+ 100% {
448
+ transform: translateX(-50%);
449
+ }
450
+ }
451
+
452
+ /* Inline styles for index-card moved here */
453
+ .index-card {
454
+ background: rgba(255,255,255,0.02);
455
+ padding: 12px;
456
+ border-radius: 8px;
457
+ min-width: 260px;
458
+ margin: 6px;
459
+ }
460
+
461
+ .ic-header {
462
+ display: flex;
463
+ justify-content: space-between;
464
+ align-items: center;
465
+ }
466
+
467
+ .ic-name {
468
+ font-weight: 600;
469
+ font-size: 0.95rem;
470
+ }
471
+
472
+ .ic-price {
473
+ font-weight: 700;
474
+ }
475
+
476
+ .ic-row {
477
+ margin-top: 6px;
478
+ }
479
+
480
+ .ic-change.up {
481
+ color: var(--up);
482
+ }
483
+
484
+ .ic-change.down {
485
+ color: var(--down);
486
+ }
487
+
488
+ .ic-spark {
489
+ width: 100%;
490
+ height: 28px;
491
+ margin-top: 8px;
492
+ }
src/app/dashboard/dashboard.component.ts CHANGED
@@ -1,19 +1,26 @@
1
  import { Component, ElementRef, OnDestroy, OnInit, AfterViewInit, ViewChild } from '@angular/core';
2
  import { CommonModule } from '@angular/common';
 
 
 
 
3
 
4
  type IndexItem = { code: string; price: number; chg: number; miniPath?: string; isUp?: boolean };
5
  type Company = { sym: string; ltp: number; chg: number; high: number; low: number };
6
  type Sector = { name: string; picks: string[] };
 
 
 
7
 
8
  @Component({
9
  selector: 'app-dashboard',
10
  standalone: true,
11
- imports: [CommonModule],
12
  templateUrl: './dashboard.component.html',
13
  styleUrls: ['./dashboard.component.scss'],
14
  })
15
  export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy {
16
- // Demo data
17
  indices: IndexItem[] = [
18
  { code: 'NIFTY 50', price: 24480.55, chg: +0.35 },
19
  { code: 'BANK NIFTY', price: 51880.20, chg: -0.12 },
@@ -22,6 +29,31 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy {
22
  { code: 'NIFTY SMALLCAP', price: 17350.60, chg: -0.18 },
23
  ];
24
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  companiesByIndex: Record<string, Company[]> = {
26
  'NIFTY 50': [
27
  { sym: 'RELIANCE', ltp: 2950.30, chg: +0.85, high: 2972.0, low: 2920.0 },
@@ -64,7 +96,7 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy {
64
  { name: 'IT & Software', picks: ['TCS', 'INFY', 'HCLTECH', 'TECHM'] },
65
  { name: 'Banking & Finance', picks: ['HDFCBANK', 'ICICIBANK', 'SBIN', 'AXISBANK'] },
66
  { name: 'Energy & Materials', picks: ['RELIANCE', 'ONGC', 'TATASTEEL', 'COALINDIA'] },
67
- { name: 'Auto', picks: ['TATAMOTORS', 'MARUTI', 'M&M', 'TVSMOTOR'] },
68
  { name: 'FMCG', picks: ['ITC', 'HINDUNILVR', 'NESTLEIND', 'DABAR'] },
69
  { name: 'Telecom', picks: ['BHARTIARTL', 'VODAFONEIDE', 'TATACOMM', 'INDUSTOWER'] },
70
  ];
@@ -74,18 +106,133 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy {
74
  }
75
  activeIndex: string = this.indexCodes[0];
76
 
 
 
 
77
  gainers: Company[] = [];
78
  losers: Company[] = [];
79
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
  lastUpdated = '—';
81
- chartSymbol = 'RELIANCE';
82
 
 
 
83
  private tickHandle?: number;
 
84
 
85
  @ViewChild('chartCanvas', { static: false })
86
  private canvasRef?: ElementRef<HTMLCanvasElement>;
87
 
88
- constructor(private host: ElementRef<HTMLElement>) { }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
 
90
  ngOnInit(): void {
91
  // Initialize mini sparklines and clock
@@ -93,6 +240,47 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy {
93
  this.setClock();
94
  this.updateGainersLosers();
95
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
  // Start ticks every 3s
97
  this.tickHandle = window.setInterval(() => {
98
  this.tickIndices();
@@ -100,19 +288,556 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy {
100
  this.updateGainersLosers();
101
  this.setClock();
102
  }, 3000);
 
 
 
 
 
 
 
 
 
 
 
 
 
103
  }
104
 
105
  ngAfterViewInit(): void {
106
  // Initial chart
107
  const initial = this.companiesByIndex[this.activeIndex][0];
108
  this.chartSymbol = initial?.sym ?? '—';
109
- this.drawTodayChart(this.chartSymbol, initial?.ltp ?? 100);
 
110
  }
111
 
112
  ngOnDestroy(): void {
113
  if (this.tickHandle) {
114
  clearInterval(this.tickHandle);
115
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
116
  }
117
 
118
  // UI helpers
@@ -124,13 +849,189 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy {
124
  return Math.random();
125
  }
126
 
127
- setActiveIndex(code: string): void {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
128
  this.activeIndex = code;
 
 
 
 
 
 
 
 
 
129
  }
130
 
131
- onCompanyClick(e: Event, c: Company): void {
132
- e.preventDefault();
133
- this.drawTodayChart(c.sym, c.ltp);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
134
  }
135
 
136
  // Clock
@@ -149,10 +1050,11 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy {
149
  }
150
 
151
  private sparkPath(values: number[], w = 240, h = 28, pad = 2): string {
 
152
  const max = Math.max(...values);
153
  const min = Math.min(...values);
154
  const norm = (v: number) => h - pad - ((v - min) / Math.max(1e-6, max - min)) * (h - 2 * pad);
155
- const step = (w - 2 * pad) / (values.length - 1);
156
  let d = '';
157
  values.forEach((v, i) => {
158
  const x = pad + i * step;
@@ -213,7 +1115,179 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy {
213
  return arr;
214
  }
215
 
216
- drawTodayChart(symbol = '', base = 100): void {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
217
  this.chartSymbol = symbol;
218
  const canvas = this.canvasRef?.nativeElement;
219
  if (!canvas) return;
@@ -225,12 +1299,11 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy {
225
  const h = canvas.height;
226
  ctx.clearRect(0, 0, w, h);
227
 
228
- const values = this.makeIntraday(base, 90);
229
  const min = Math.min(...values);
230
  const max = Math.max(...values);
231
  const padX = 28;
232
  const padY = 16;
233
- const toX = (i: number) => padX + (i * (w - padX * 2)) / (values.length - 1);
234
  const toY = (v: number) => h - padY - ((v - min) / Math.max(1e-6, max - min)) * (h - padY * 2);
235
 
236
  // grid
@@ -246,11 +1319,9 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy {
246
  }
247
  ctx.globalAlpha = 1;
248
 
249
- // line
250
  const up = values[values.length - 1] >= values[0];
251
  const styles = getComputedStyle(this.host.nativeElement);
252
- const stroke =
253
- styles.getPropertyValue(up ? '--up' : '--down').trim() || (up ? '#12c48b' : '#ff5b6b');
254
 
255
  ctx.strokeStyle = stroke;
256
  ctx.lineWidth = 2;
@@ -258,8 +1329,7 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy {
258
  values.forEach((v, i) => {
259
  const x = toX(i);
260
  const y = toY(v);
261
- if (i === 0) ctx.moveTo(x, y);
262
- else ctx.lineTo(x, y);
263
  });
264
  ctx.stroke();
265
 
@@ -271,4 +1341,65 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy {
271
  ctx.arc(lastX, lastY, 3, 0, Math.PI * 2);
272
  ctx.fill();
273
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
274
  }
 
1
  import { Component, ElementRef, OnDestroy, OnInit, AfterViewInit, ViewChild } from '@angular/core';
2
  import { CommonModule } from '@angular/common';
3
+ import { HttpClientModule } from '@angular/common/http';
4
+ import { DashboardService } from './dashboard.service';
5
+ import { take } from 'rxjs/operators';
6
+ import { lastValueFrom } from 'rxjs';
7
 
8
  type IndexItem = { code: string; price: number; chg: number; miniPath?: string; isUp?: boolean };
9
  type Company = { sym: string; ltp: number; chg: number; high: number; low: number };
10
  type Sector = { name: string; picks: string[] };
11
+ type MarketCard = { title: string; value: string; chg: string; dir: 'up' | 'down' | 'neutral' };
12
+
13
+ type GlobalIndex = { id: string; name: string; country: string; region: string; price: number; change: number; changePct: number; sparkline: number[]; miniPath?: string; isUp?: boolean };
14
 
15
  @Component({
16
  selector: 'app-dashboard',
17
  standalone: true,
18
+ imports: [CommonModule, HttpClientModule], // IndexCardComponent],
19
  templateUrl: './dashboard.component.html',
20
  styleUrls: ['./dashboard.component.scss'],
21
  })
22
  export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy {
23
+ // Demo data (used as fallback until live data loads)
24
  indices: IndexItem[] = [
25
  { code: 'NIFTY 50', price: 24480.55, chg: +0.35 },
26
  { code: 'BANK NIFTY', price: 51880.20, chg: -0.12 },
 
29
  { code: 'NIFTY SMALLCAP', price: 17350.60, chg: -0.18 },
30
  ];
31
 
32
+ // Market overview cards (populated live when API responds)
33
+ // initialize placeholders to em dash so UI doesn't show raw 0 or empty strings
34
+ marketCards: MarketCard[] = [
35
+ { title: 'Gold', value: '—', chg: '—', dir: 'neutral' },
36
+ { title: 'Silver', value: '—', chg: '—', dir: 'neutral' },
37
+ { title: 'Crude Oil (Brent)', value: '—', chg: '—', dir: 'neutral' },
38
+ { title: 'Crude Oil (WTI)', value: '—', chg: '—', dir: 'neutral' },
39
+ { title: 'Natural Gas', value: '—', chg: '—', dir: 'neutral' },
40
+ { title: 'USD/INR', value: '—', chg: '—', dir: 'neutral' },
41
+ { title: 'EUR/USD', value: '—', chg: '—', dir: 'neutral' },
42
+ { title: 'GBP/USD', value: '—', chg: '—', dir: 'neutral' },
43
+ { title: 'Bitcoin', value: '—', chg: '—', dir: 'neutral' },
44
+ { title: 'Ethereum', value: '—', chg: '—', dir: 'neutral' },
45
+ { title: 'S&P 500', value: '—', chg: '—', dir: 'neutral' },
46
+ { title: 'NASDAQ', value: '—', chg: '—', dir: 'neutral' },
47
+ { title: 'DAX', value: '—', chg: '—', dir: 'neutral' },
48
+ { title: 'Nikkei', value: '—', chg: '—', dir: 'neutral' },
49
+ { title: 'Copper', value: '—', chg: '—', dir: 'neutral' }
50
+ ];
51
+
52
+ // expose duplicated list for seamless loop in template
53
+ get marketCardsRepeat(): MarketCard[] {
54
+ return [...this.marketCards, ...this.marketCards];
55
+ }
56
+
57
  companiesByIndex: Record<string, Company[]> = {
58
  'NIFTY 50': [
59
  { sym: 'RELIANCE', ltp: 2950.30, chg: +0.85, high: 2972.0, low: 2920.0 },
 
96
  { name: 'IT & Software', picks: ['TCS', 'INFY', 'HCLTECH', 'TECHM'] },
97
  { name: 'Banking & Finance', picks: ['HDFCBANK', 'ICICIBANK', 'SBIN', 'AXISBANK'] },
98
  { name: 'Energy & Materials', picks: ['RELIANCE', 'ONGC', 'TATASTEEL', 'COALINDIA'] },
99
+ { name: 'Auto', picks: ['TATAMOTORS', 'MARUTI', 'M&amp;M', 'TVSMOTOR'] },
100
  { name: 'FMCG', picks: ['ITC', 'HINDUNILVR', 'NESTLEIND', 'DABAR'] },
101
  { name: 'Telecom', picks: ['BHARTIARTL', 'VODAFONEIDE', 'TATACOMM', 'INDUSTOWER'] },
102
  ];
 
106
  }
107
  activeIndex: string = this.indexCodes[0];
108
 
109
+ // currently selected company symbol for chart highlighting
110
+ selectedCompany: string | null = null;
111
+
112
  gainers: Company[] = [];
113
  losers: Company[] = [];
114
 
115
+ // New global indices lists
116
+ globalIndices: GlobalIndex[] = [];
117
+ indiaIndices: GlobalIndex[] = [];
118
+
119
+ // All indices cache (includes India and others)
120
+ private allGlobalIndices: GlobalIndex[] = [];
121
+
122
+ // Country filter
123
+ // default countries shown immediately (will be replaced by live data when available)
124
+ countries: string[] = ['India', 'US', 'Uk', 'German', 'Sweden', 'Russia'];
125
+ selectedCountry = 'India';
126
+
127
+ // common alias map so UI labels like 'US' or 'Uk' match backend country names
128
+ private countryAliases: Record<string, string[]> = {
129
+ 'us': ['united states', 'usa', 'us', 'u.s.a', 'u.s.'],
130
+ 'uk': ['united kingdom', 'britain', 'uk', 'u.k.'],
131
+ 'german': ['germany', 'german'],
132
+ 'russia': ['russia', 'russian federation'],
133
+ 'india': ['india'],
134
+ 'sweden': ['sweden']
135
+ };
136
+
137
+ // demo list for India indices
138
+ private defaultIndiaIndexNames: string[] = [
139
+ 'Nifty 50',
140
+ 'Nifty Next 50',
141
+ 'Nifty 100',
142
+ 'Nifty 500',
143
+ 'Nifty Bank',
144
+ 'Nifty IT',
145
+ 'Nifty Pharma',
146
+ 'Nifty FMCG',
147
+ 'Nifty Midcap 100',
148
+ 'Nifty Smallcap 100'
149
+ ];
150
+
151
+ // avoid repeated fetches in quick succession
152
+ private globalFetched = false;
153
+
154
+ // last updated timestamp shown in the header
155
  lastUpdated = '—';
 
156
 
157
+ // chart symbol and timer handle
158
+ chartSymbol = 'RELIANCE';
159
  private tickHandle?: number;
160
+ public quotesIntervalHandle?: number;
161
 
162
  @ViewChild('chartCanvas', { static: false })
163
  private canvasRef?: ElementRef<HTMLCanvasElement>;
164
 
165
+ @ViewChild('chartContainer', { static: false })
166
+ private chartContainerRef?: ElementRef<HTMLDivElement>;
167
+
168
+ @ViewChild('tickerTrack', { static: false })
169
+ private tickerTrackRef?: ElementRef<HTMLDivElement>;
170
+
171
+ private marqueeAnimationId?: number;
172
+ private marqueeResizeObserver?: ResizeObserver | null = null;
173
+ private marqueeMutationObserver?: MutationObserver | null = null;
174
+ private marqueeMeasureTimeout?: any;
175
+ private lastDurationSec?: number | null = null;
176
+
177
+ // priority lists of important indices per country (used to order and pick indices from live payload)
178
+ private importantIndicesMap: Record<string, string[]> = {
179
+ 'india': [
180
+ 'Nifty 50', 'Nifty Bank', 'SENSEX', 'Nifty Next 50', 'Nifty 100', 'Nifty 500', 'Nifty IT', 'Nifty Pharma', 'Nifty FMCG', 'Nifty Midcap 100', 'Nifty Smallcap 100'
181
+ ],
182
+ 'us': ['S&P 500', 'Dow Jones', 'Nasdaq Composite', 'S&P MidCap 400', 'S&P SmallCap 600'],
183
+ 'uk': ['FTSE 100', 'FTSE 250', 'FTSE 350'],
184
+ 'german': ['DAX', 'MDAX', 'TecDAX'],
185
+ 'sweden': ['OMX Stockholm 30'],
186
+ 'russia': ['MOEX Russia', 'RTS Index']
187
+ };
188
+
189
+ private uniqueByName(list: GlobalIndex[]): GlobalIndex[] {
190
+ const seen = new Set<string>();
191
+ const out: GlobalIndex[] = [];
192
+ for (const it of list) {
193
+ const name = (it.name || '').toString().trim();
194
+ const key = name.toLowerCase();
195
+ if (!key) continue;
196
+ if (seen.has(key)) continue;
197
+ seen.add(key);
198
+ out.push(it);
199
+ }
200
+ return out;
201
+ }
202
+
203
+ // Build a prioritized list of indices for a country based on importantIndicesMap and available live entries
204
+ private buildCountryIndices(countryKey: string): GlobalIndex[] {
205
+ if (!countryKey) return [];
206
+ const key = countryKey.toLowerCase().trim();
207
+ const important = this.importantIndicesMap[key] || [];
208
+ // gather live matches (case-insensitive name match)
209
+ const live = (this.allGlobalIndices || []).filter(g => this.countryMatches(key, (g.country || '').toLowerCase()));
210
+ const byNameMap = new Map<string, GlobalIndex>();
211
+ // normalize and index live by name
212
+ live.forEach(g => {
213
+ const n = (g.name || '').toString().trim();
214
+ if (n) byNameMap.set(n.toLowerCase(), g);
215
+ });
216
+ const result: GlobalIndex[] = [];
217
+ // pick in priority order
218
+ for (const name of important) {
219
+ const found = byNameMap.get(name.toLowerCase());
220
+ if (found) result.push(found);
221
+ }
222
+ // append any other live indices for the country that were not in the important list (unique)
223
+ live.forEach(g => {
224
+ const n = (g.name || '').toString().trim().toLowerCase();
225
+ if (!result.some(r => ((r.name || '').toLowerCase() === n))) result.push(g);
226
+ });
227
+ return this.uniqueByName(result).map(g => {
228
+ // ensure miniPath computed
229
+ g.miniPath = this.sparkPath(g.sparkline || []);
230
+ g.isUp = (g.changePct || 0) >= 0;
231
+ return g;
232
+ });
233
+ }
234
+
235
+ constructor(private host: ElementRef<HTMLElement>, private market: DashboardService) { }
236
 
237
  ngOnInit(): void {
238
  // Initialize mini sparklines and clock
 
240
  this.setClock();
241
  this.updateGainersLosers();
242
 
243
+ // Load cached market overview so values appear immediately on reload if available
244
+ try {
245
+ const raw = localStorage.getItem('marketCardsCache');
246
+ if (raw) {
247
+ const cached = JSON.parse(raw) as MarketCard[];
248
+ if (Array.isArray(cached) && cached.length) this.marketCards = cached;
249
+ }
250
+ } catch {
251
+ // ignore cache errors
252
+ }
253
+
254
+ // --- Load cached companies/quotes so Companies table shows immediately after reload ---
255
+ try {
256
+ const rawComp = localStorage.getItem('companiesCacheV1');
257
+ if (rawComp) {
258
+ const parsed = JSON.parse(rawComp) as any;
259
+ if (parsed && parsed.companiesByIndex) {
260
+ this.companiesByIndex = parsed.companiesByIndex;
261
+ if (parsed.lastUpdated) this.lastUpdated = parsed.lastUpdated;
262
+ this.updateGainersLosers();
263
+ // ensure activeIndex valid
264
+ if (!this.indexCodes.includes(this.activeIndex)) {
265
+ this.activeIndex = this.indexCodes[0];
266
+ }
267
+ }
268
+ }
269
+ } catch {
270
+ // ignore
271
+ }
272
+
273
+ // Fetch live data once on init (and keep demo simulation running as fallback)
274
+ this.fetchLiveMarkets();
275
+ // fetch market overview cards independently so they display even if /getcompanies fails
276
+ this.fetchMarketCards();
277
+ this.fetchGlobalIndices();
278
+
279
+ // Ensure demo India indices present initially if backend doesn't provide them
280
+ if (this.selectedCountry && this.selectedCountry.toLowerCase().trim() === 'india' && (!this.indiaIndices || this.indiaIndices.length === 0)) {
281
+ this.populateDefaultIndiaIndices();
282
+ }
283
+
284
  // Start ticks every 3s
285
  this.tickHandle = window.setInterval(() => {
286
  this.tickIndices();
 
288
  this.updateGainersLosers();
289
  this.setClock();
290
  }, 3000);
291
+
292
+ // Start polling live quotes every 30s
293
+ this.quotesIntervalHandle = window.setInterval(() => {
294
+ this.fetchLiveQuotesForActiveIndex();
295
+ }, 30000);
296
+
297
+ // ensure selectedCompany defaults to first company in active index (from cache or demo)
298
+ const first = this.companiesByIndex[this.activeIndex]?.[0];
299
+ if (first) {
300
+ this.selectedCompany = first.sym;
301
+ // draw initial chart for selected company (try live intraday)
302
+ this.fetchAndDrawChart(first.sym, first.ltp).catch(() => { /* ignore */ });
303
+ }
304
  }
305
 
306
  ngAfterViewInit(): void {
307
  // Initial chart
308
  const initial = this.companiesByIndex[this.activeIndex][0];
309
  this.chartSymbol = initial?.sym ?? '—';
310
+ // draw SVG style area chart in container
311
+ this.renderAreaChart(initial?.sym ?? '—', initial?.ltp ?? 100);
312
  }
313
 
314
  ngOnDestroy(): void {
315
  if (this.tickHandle) {
316
  clearInterval(this.tickHandle);
317
  }
318
+ if (this.quotesIntervalHandle) {
319
+ clearInterval(this.quotesIntervalHandle);
320
+ }
321
+ if (this.marqueeAnimationId) {
322
+ cancelAnimationFrame(this.marqueeAnimationId);
323
+ this.marqueeAnimationId = undefined;
324
+ }
325
+ if (this.marqueeResizeObserver) {
326
+ try { this.marqueeResizeObserver.disconnect(); } catch (e) { /* ignore */ }
327
+ this.marqueeResizeObserver = null;
328
+ }
329
+ if (this.marqueeMutationObserver) {
330
+ try { this.marqueeMutationObserver.disconnect(); } catch (e) { /* ignore */ }
331
+ this.marqueeMutationObserver = null;
332
+ }
333
+ if (this.marqueeMeasureTimeout) {
334
+ clearTimeout(this.marqueeMeasureTimeout);
335
+ this.marqueeMeasureTimeout = undefined;
336
+ }
337
+ }
338
+
339
+ // Loading state shown by template overlay while quotes request running
340
+ quotesLoading: boolean = false;
341
+ // localStorage key for persisted companies/quotes cache
342
+ private readonly companiesCacheKey = 'companiesCacheV1';
343
+
344
+ // Called after view init to set animation duration based on content width so the marquee flows evenly
345
+ private setupMarquee(): void {
346
+ const el = this.tickerTrackRef?.nativeElement as HTMLDivElement | undefined;
347
+ if (!el) return;
348
+
349
+ // measure content width and container width; duplicated items => distance is half scrollWidth
350
+ const measureOnce = () => {
351
+ try {
352
+ const totalWidth = el.scrollWidth || 0; // width of duplicated content
353
+ const distance = Math.max(1, totalWidth / 2);
354
+ const pxPerSecond = 120; // control speed
355
+ const durationSec = Math.max(6, distance / pxPerSecond);
356
+ // avoid changing CSS var if duration unchanged (prevents animation restart)
357
+ if (this.lastDurationSec == null || Math.abs((this.lastDurationSec || 0) - durationSec) > 0.5) {
358
+ el.style.setProperty('--marquee-duration', `${Math.round(durationSec)}s`);
359
+ this.lastDurationSec = durationSec;
360
+ }
361
+ } catch (e) {
362
+ // ignore
363
+ }
364
+ };
365
+
366
+ const scheduleMeasure = () => {
367
+ if (this.marqueeMeasureTimeout) clearTimeout(this.marqueeMeasureTimeout);
368
+ this.marqueeMeasureTimeout = setTimeout(() => {
369
+ measureOnce();
370
+ this.marqueeMeasureTimeout = undefined;
371
+ }, 120);
372
+ };
373
+
374
+ // initial measurement
375
+ scheduleMeasure();
376
+
377
+ // observe size changes and DOM changes, but debounce actual measurement
378
+ try {
379
+ this.marqueeResizeObserver = new ResizeObserver(() => scheduleMeasure());
380
+ this.marqueeResizeObserver.observe(el);
381
+ if (el.parentElement) this.marqueeResizeObserver.observe(el.parentElement);
382
+ } catch (e) {
383
+ this.marqueeResizeObserver = null;
384
+ }
385
+
386
+ try {
387
+ this.marqueeMutationObserver = new MutationObserver(() => scheduleMeasure());
388
+ this.marqueeMutationObserver.observe(el, { childList: true, subtree: true });
389
+ } catch (e) {
390
+ this.marqueeMutationObserver = null;
391
+ }
392
+ }
393
+
394
+ private fetchGlobalIndices(): void {
395
+ // avoid duplicate fetch
396
+ if (this.globalFetched) return;
397
+ this.globalFetched = true;
398
+
399
+ this.market.getGlobalIndices().pipe(take(1)).subscribe({
400
+ next: (resp) => {
401
+ try {
402
+ console.log('fetchGlobalIndices resp:', resp);
403
+
404
+ const data = resp?.data || resp;
405
+ if (!Array.isArray(data)) return;
406
+ // capture marketCards from response early so we can use it when deriving indices
407
+ const respMarketCards = (resp as any)?.marketCards;
408
+ // map all returned indices
409
+ const mapped = data.map(this.mapToGlobalIndex);
410
+
411
+ // If backend returned no mapped indices but provided marketCards, derive a few common indices from marketCards
412
+ if ((!mapped || mapped.length === 0) && Array.isArray(respMarketCards) && respMarketCards.length) {
413
+ const indexNames = new Set(['S&P 500', 'NASDAQ', 'DAX', 'Nikkei', 'Nifty 50', 'Nifty Bank', 'SENSEX']);
414
+ const derived: GlobalIndex[] = [];
415
+ respMarketCards.forEach((mc: any) => {
416
+ if (indexNames.has(mc.title)) {
417
+ const price = Number(mc.price) || (typeof mc.display === 'number' ? mc.display : NaN);
418
+ const pct = mc.chgPct ?? (mc.chg && price ? (Number(mc.chg) / (price - Number(mc.chg))) * 100 : 0);
419
+ const gi: GlobalIndex = {
420
+ id: (mc.title || '').replace(/\s+/g, '_'),
421
+ name: mc.title,
422
+ country: mc.title === 'S&P 500' || mc.title === 'NASDAQ' ? 'United States' : mc.title === 'DAX' ? 'Germany' : mc.title === 'Nikkei' ? 'Japan' : 'India',
423
+ region: mc.title === 'S&P 500' || mc.title === 'NASDAQ' ? 'US' : mc.title === 'DAX' ? 'German' : mc.title === 'Nikkei' ? 'Japan' : 'India',
424
+ price: Number(isNaN(price) ? 0 : price),
425
+ change: Number(mc.chg ?? 0),
426
+ changePct: Number(pct || 0),
427
+ sparkline: this.series(Number(isNaN(price) ? Math.round(Number(mc.price) || 1000) : price), 26)
428
+ } as GlobalIndex;
429
+ gi.miniPath = this.sparkPath(gi.sparkline || []);
430
+ gi.isUp = (gi.changePct || 0) >= 0;
431
+ derived.push(gi);
432
+ }
433
+ });
434
+ // merge derived into mapped so rest of flow continues
435
+ if (derived.length) {
436
+ mapped.push(...derived);
437
+ }
438
+ }
439
+
440
+ // compute mini sparklines paths
441
+ mapped.forEach((g) => {
442
+ g.miniPath = this.sparkPath(g.sparkline || []);
443
+ g.isUp = (g.change || 0) >= 0;
444
+ });
445
+
446
+ // merge into cache (dedupe by id)
447
+ const byId = new Map<string, GlobalIndex>();
448
+ [...this.allGlobalIndices, ...mapped].forEach(i => byId.set(i.id, i));
449
+ this.allGlobalIndices = Array.from(byId.values());
450
+
451
+ // populate countries list for filter — merge live values but keep defaults if none
452
+ const uniq = Array.from(new Set(this.allGlobalIndices.map((g) => g.country).filter(Boolean)));
453
+ // always include common defaults so UI shows tabs even when backend data missing
454
+ const defaultCountries = ['India', 'US', 'Uk', 'German', 'Sweden', 'Russia'];
455
+ if (uniq.length) {
456
+ // Filter live countries: exclude those that are aliases of our defaults to avoid duplicates
457
+ const aliasesMap: Record<string, string[]> = this.countryAliases;
458
+ const isAliasOfDefault = (countryName: string) => {
459
+ if (!countryName) return false;
460
+ const lname = countryName.toLowerCase();
461
+ // check against every alias set; if any alias contains the live name, consider it a match
462
+ for (const key of Object.keys(aliasesMap)) {
463
+ const aliases = aliasesMap[key] || [];
464
+ for (const a of aliases) {
465
+ if (lname.includes(a)) return true;
466
+ }
467
+ }
468
+ return false;
469
+ };
470
+
471
+ const rest = uniq
472
+ .filter(c => c.toLowerCase() !== 'india')
473
+ .filter(c => !isAliasOfDefault(c))
474
+ .sort();
475
+ // merge defaults with any truly extra live countries
476
+ // filter out any unwanted country labels (e.g., Russia, Japan)
477
+ const hide = new Set(['russia', 'japan']);
478
+ const merged = [...defaultCountries, ...rest].filter(c => !hide.has((c || '').toLowerCase()));
479
+ this.countries = merged;
480
+ } else {
481
+ this.countries = [...defaultCountries];
482
+ }
483
+
484
+ // replace marketCards with live market overview if available in response root
485
+ if (Array.isArray(respMarketCards) && respMarketCards.length) {
486
+ this.marketCards = respMarketCards.map((m: any) => {
487
+ const display = (m.display ?? m.price);
488
+ const priceNum = Number(m.price ?? display ?? NaN);
489
+ // prefer explicit percent from backend, otherwise compute from point change and previous price
490
+ let pct = m.chgPct;
491
+ if (pct === undefined || pct === null || Number.isNaN(Number(pct))) {
492
+ const chgPoint = Number(m.chg || 0);
493
+ const prev = priceNum - chgPoint;
494
+ pct = (prev && !Number.isNaN(prev)) ? (chgPoint / prev) * 100 : 0;
495
+ }
496
+ const priceStr = (display === null || display === undefined) ? '—' : (typeof display === 'number' ? display.toLocaleString(undefined, { maximumFractionDigits: 2 }) : String(display));
497
+ const chgNum = Number(pct || 0);
498
+ return {
499
+ title: m.title,
500
+ value: priceStr,
501
+ // show percent, not raw point change
502
+ chg: (chgNum >= 0 ? '+' : '') + chgNum.toFixed(2) + '%',
503
+ dir: chgNum > 0 ? 'up' : chgNum < 0 ? 'down' : 'neutral'
504
+ } as MarketCard;
505
+ });
506
+ console.log('marketCards set from resp.marketCards:', this.marketCards);
507
+ } else {
508
+ // fallback: derive a simple market overview from some well-known indices/currencies returned in "data"
509
+ const pickTitles = new Set(['Gold', 'Silver', 'Crude Oil (Brent)', 'Crude Oil (WTI)', 'Natural Gas', 'USD/INR', 'EUR/USD', 'GBP/USD', 'Bitcoin', 'Ethereum', 'S&P 500', 'NASDAQ', 'DAX', 'Nikkei']);
510
+ const derived: MarketCard[] = [];
511
+ // if backend provided indices with matching names, map them
512
+ mapped.forEach((m: any) => {
513
+ if (pickTitles.has(m.name)) {
514
+ const priceStr = (m.price === null || m.price === undefined) ? '—' : (typeof m.price === 'number' ? m.price.toLocaleString(undefined, { maximumFractionDigits: 2 }) : String(m.price));
515
+ const chgNum = Number(m.change ?? m.changePct ?? 0);
516
+ derived.push({
517
+ title: m.name,
518
+ value: priceStr,
519
+ chg: (chgNum >= 0 ? '+' : '') + chgNum.toFixed(2) + '%',
520
+ dir: chgNum > 0 ? 'up' : chgNum < 0 ? 'down' : 'neutral'
521
+ });
522
+ }
523
+ });
524
+ // if still empty, try to pick first few indices
525
+ if (derived.length === 0) {
526
+ mapped.slice(0, 12).forEach((m: any) => {
527
+ const priceStr = (m.price === null || m.price === undefined) ? '—' : (typeof m.price === 'number' ? m.price.toLocaleString(undefined, { maximumFractionDigits: 2 }) : String(m.price));
528
+ const chgNum = Number(m.change ?? m.changePct ?? 0);
529
+ derived.push({
530
+ title: m.name,
531
+ value: priceStr,
532
+ chg: (chgNum >= 0 ? '+' : '') + chgNum.toFixed(2) + '%',
533
+ dir: chgNum > 0 ? 'up' : chgNum < 0 ? 'down' : 'neutral'
534
+ });
535
+ });
536
+ }
537
+ if (derived.length) {
538
+ this.marketCards = derived;
539
+ console.log('marketCards derived from indices:', this.marketCards);
540
+ }
541
+ }
542
+
543
+ // apply current selection so UI updates immediately with live data
544
+ this.applySelection(this.selectedCountry || 'India');
545
+ } catch (e) {
546
+ console.error('Error processing global indices', e);
547
+ }
548
+ },
549
+ error: (err) => {
550
+ console.warn('Failed to fetch global indices', err);
551
+ }
552
+ });
553
+ }
554
+
555
+ private mapToGlobalIndex(d: any): GlobalIndex {
556
+ return {
557
+ id: d.id || `${(d.name || '').replace(/\s+/g, '_')}_${Math.random().toString(36).slice(2, 8)}`,
558
+ name: d.name,
559
+ country: d.country,
560
+ region: d.region,
561
+ price: d.price,
562
+ change: d.change,
563
+ changePct: d.changePct,
564
+ sparkline: d.sparkline || [],
565
+ } as GlobalIndex;
566
+ }
567
+
568
+ // Create demo US indices list with required names so UI shows S&P 500, Dow Jones, Nasdaq, S&P MidCap 400 and S&P SmallCap 600 when 'US' selected
569
+ private createUSIndices(): GlobalIndex[] {
570
+ const names = [
571
+ 'S&P 500',
572
+ 'Dow Jones',
573
+ 'Nasdaq Composite',
574
+ 'S&P MidCap 400',
575
+ 'S&P SmallCap 600'
576
+ ];
577
+ return names.map((name, i) => {
578
+ const base = Math.round(3000 + i * 200 + (Math.random() - 0.5) * 5000);
579
+ const change = Number(((Math.random() - 0.5) * 1.5).toFixed(2));
580
+ const changePct = Number(((change / Math.max(1, base)) * 100).toFixed(2));
581
+ const spark = this.series(base, 26);
582
+ const gi: GlobalIndex = {
583
+ id: `${name.replace(/\s+/g, '_')}_${i}`,
584
+ name,
585
+ country: 'United States',
586
+ region: 'US',
587
+ price: base,
588
+ change,
589
+ changePct,
590
+ sparkline: spark,
591
+ } as GlobalIndex;
592
+ gi.miniPath = this.sparkPath(gi.sparkline || []);
593
+ gi.isUp = (gi.change || 0) >= 0;
594
+ return gi;
595
+ });
596
+ }
597
+
598
+ // Create demo UK indices (FTSE family) so UI shows FTSE 100, FTSE 250, FTSE 350, FTSE Small Cap when 'Uk' selected
599
+ private createUKIndices(): GlobalIndex[] {
600
+ const names = [
601
+ 'FTSE 100',
602
+ 'FTSE 250',
603
+ 'FTSE 350',
604
+ 'FTSE Small Cap'
605
+ ];
606
+ return names.map((name, i) => {
607
+ const base = Math.round(6000 + i * 100 + (Math.random() - 0.5) * 400);
608
+ const change = Number(((Math.random() - 0.5) * 1.2).toFixed(2));
609
+ const changePct = Number(((change / Math.max(1, base)) * 100).toFixed(2));
610
+ const spark = this.series(base, 26);
611
+ const gi: GlobalIndex = {
612
+ id: `${name.replace(/\s+/g, '_')}_${i}`,
613
+ name,
614
+ country: 'United Kingdom',
615
+ region: 'UK',
616
+ price: base,
617
+ change,
618
+ changePct,
619
+ sparkline: spark,
620
+ } as GlobalIndex;
621
+ gi.miniPath = this.sparkPath(gi.sparkline || []);
622
+ gi.isUp = (gi.change || 0) >= 0;
623
+ return gi;
624
+ });
625
+ }
626
+
627
+ // Create demo German indices (DAX family) so UI shows DAX, MDAX, TecDAX, SDAX when 'German' selected
628
+ private createGermanIndices(): GlobalIndex[] {
629
+ const names = [
630
+ 'DAX',
631
+ 'MDAX',
632
+ 'TecDAX',
633
+ 'SDAX'
634
+ ];
635
+ return names.map((name, i) => {
636
+ const base = Math.round(10000 + i * 200 + (Math.random() - 0.5) * 800);
637
+ const change = Number(((Math.random() - 0.5) * 1.2).toFixed(2));
638
+ const changePct = Number(((change / Math.max(1, base)) * 100).toFixed(2));
639
+ const spark = this.series(base, 26);
640
+ const gi: GlobalIndex = {
641
+ id: `${name.replace(/\s+/g, '_')}_${i}`,
642
+ name,
643
+ country: 'Germany',
644
+ region: 'German',
645
+ price: base,
646
+ change,
647
+ changePct,
648
+ sparkline: spark,
649
+ } as GlobalIndex;
650
+ gi.miniPath = this.sparkPath(gi.sparkline || []);
651
+ gi.isUp = (gi.change || 0) >= 0;
652
+ return gi;
653
+ });
654
+ }
655
+
656
+ // Create demo Sweden indices so UI shows OMXS30 and STOXX Sweden when 'Sweden' selected
657
+ private createSwedenIndices(): GlobalIndex[] {
658
+ const names = [
659
+ 'OMX Stockholm 30',
660
+ 'STOXX Sweden'
661
+ ];
662
+ return names.map((name, i) => {
663
+ const base = Math.round(1500 + i * 50 + (Math.random() - 0.5) * 200);
664
+ const change = Number(((Math.random() - 0.5) * 1.0).toFixed(2));
665
+ const changePct = Number(((change / Math.max(1, base)) * 100).toFixed(2));
666
+ const spark = this.series(base, 26);
667
+ const gi: GlobalIndex = {
668
+ id: `${name.replace(/\s+/g, '_')}_${i}`,
669
+ name,
670
+ country: 'Sweden',
671
+ region: 'Sweden',
672
+ price: base,
673
+ change,
674
+ changePct,
675
+ sparkline: spark,
676
+ } as GlobalIndex;
677
+ gi.miniPath = this.sparkPath(gi.sparkline || []);
678
+ gi.isUp = (gi.change || 0) >= 0;
679
+ return gi;
680
+ });
681
+ }
682
+
683
+ // Create demo Russia indices so UI shows MOEX Russia and RTS Index when 'Russia' selected
684
+ private createRussiaIndices(): GlobalIndex[] {
685
+ const names = ['MOEX Russia', 'RTS Index'];
686
+ return names.map((name, i) => {
687
+ const base = Math.round(2000 + i * 300 + (Math.random() - 0.5) * 800) * (name === 'MOEX Russia' ? 5 : 1);
688
+ const change = Number(((Math.random() - 0.5) * 1.2).toFixed(2));
689
+ const changePct = Number(((change / Math.max(1, base)) * 100).toFixed(2));
690
+ const spark = this.series(base, 26);
691
+ const gi: GlobalIndex = {
692
+ id: `${name.replace(/\s+/g, '_')}_${i}`,
693
+ name,
694
+ country: 'Russia',
695
+ region: 'Russia',
696
+ price: base,
697
+ change,
698
+ changePct,
699
+ sparkline: spark,
700
+ } as GlobalIndex;
701
+ gi.miniPath = this.sparkPath(gi.sparkline || []);
702
+ gi.isUp = (gi.change || 0) >= 0;
703
+ return gi;
704
+ });
705
+ }
706
+
707
+ // Indices to display in template: indiaIndices when India selected, otherwise globalIndices set by applySelection
708
+ get displayIndices(): GlobalIndex[] {
709
+ const sel = (this.selectedCountry || '').toLowerCase().trim();
710
+ let list: GlobalIndex[] = [];
711
+ if (sel === 'india') list = this.indiaIndices || [];
712
+ else list = this.globalIndices || [];
713
+
714
+ // Deduplicate by name (case-insensitive) and limit visible cards to a sensible number (9)
715
+ const unique = this.uniqueByName(list);
716
+ // apply small display fixes: normalize long Dow Jones name and boost MOEX Russia demo value
717
+ const adjusted = unique.slice(0, 14).map(g => {
718
+ const copy = { ...g } as GlobalIndex;
719
+ if (typeof copy.name === 'string') {
720
+ // replace long backend label if present
721
+ copy.name = copy.name.replace(/Dow Jones Industrial Average/ig, 'Dow Jones');
722
+ }
723
+ // if MOEX Russia appears and looks small (likely demo), scale it up for visibility
724
+ if (typeof copy.name === 'string' && /moex\s*russia/i.test(copy.name)) {
725
+ // scale price but avoid absurdly large values: if price < 10000, multiply
726
+ if (typeof copy.price === 'number' && copy.price > 0 && copy.price < 10000) {
727
+ copy.price = Math.round(copy.price * 5);
728
+ }
729
+ }
730
+ return copy;
731
+ });
732
+ return adjusted.slice(0, 9);
733
+ }
734
+
735
+ // Fetch live markets from backend
736
+ private fetchLiveMarkets(): void {
737
+ this.market.getCompanies().pipe(take(1)).subscribe({
738
+ next: (data) => {
739
+ console.log('fetchLiveMarkets - getCompanies response:', data);
740
+ try {
741
+ if (data.indices && Array.isArray(data.indices) && data.indices.length) {
742
+ // map backend indices to IndexItem
743
+ this.indices = data.indices.map((it: any) => ({ code: it.code, price: it.price, chg: it.chg }));
744
+ this.indices.forEach((idx) => this.refreshIndexSpark(idx));
745
+ }
746
+
747
+ if (data.companiesByIndex) {
748
+ this.companiesByIndex = data.companiesByIndex;
749
+ // Immediately fetch live quotes for active index now that companies are loaded
750
+ try {
751
+ console.log('Companies loaded for indices, fetching live quotes for', this.activeIndex);
752
+ this.fetchLiveQuotesForActiveIndex();
753
+ } catch (e) {
754
+ console.warn('fetchLiveQuotesForActiveIndex failed shortly after companies load', e);
755
+ }
756
+ }
757
+
758
+ if (data.sectors) {
759
+ this.sectors = data.sectors;
760
+ }
761
+
762
+ if (data.lastUpdated) {
763
+ this.lastUpdated = data.lastUpdated;
764
+ }
765
+
766
+ // update derived lists and chart
767
+ this.updateGainersLosers();
768
+ const initial = this.companiesByIndex[this.activeIndex]?.[0];
769
+ if (initial) this.renderAreaChart(initial.sym, initial.ltp || 100);
770
+
771
+ // if backend provided marketCards in /getcompanies payload use them
772
+ const respMarketCards = data.marketCards ?? data.data?.marketCards ?? data.results?.marketCards ?? data.market_cards ?? data.cards ?? null;
773
+ if (Array.isArray(respMarketCards) && respMarketCards.length) {
774
+ console.log('fetchLiveMarkets: found marketCards in /getcompanies payload', respMarketCards);
775
+ // apply live marketCards immediately and cache them
776
+ const mapped = respMarketCards.map((m: any) => {
777
+ const display = m.display ?? m.price ?? m.value ?? m.last ?? m.close ?? null;
778
+ const value = this.formatPrice(display);
779
+ const pctRaw = m.chgPct ?? m.chg_pct ?? m.pct ?? m.changePct ?? m.change_percent ?? m.change ?? null;
780
+ const pct = (pctRaw !== undefined && pctRaw !== null) ? Number(pctRaw) : (Number(m.chg || 0) && typeof display === 'number' ? ((Number(m.chg) / (display - Number(m.chg))) * 100) : 0);
781
+ const n = Number(pct || 0);
782
+ return {
783
+ title: m.title ?? m.name ?? (m.symbol ?? '—'),
784
+ value,
785
+ chg: isNaN(n) ? '—' : ((n >= 0 ? '+' : '') + n.toFixed(2) + '%'),
786
+ dir: isNaN(n) ? 'neutral' : (n > 0 ? 'up' : n < 0 ? 'down' : 'neutral')
787
+ } as MarketCard;
788
+ });
789
+ this.marketCards = mapped;
790
+ try { localStorage.setItem('marketCardsCache', JSON.stringify(mapped)); } catch { /* ignore */ }
791
+ } else {
792
+ console.log('fetchLiveMarkets: no marketCards found in /getcompanies payload');
793
+ }
794
+
795
+ // persist companies snapshot so table shows immediately on reload
796
+ try {
797
+ const snapshot = { companiesByIndex: this.companiesByIndex, lastUpdated: this.lastUpdated };
798
+ localStorage.setItem(this.companiesCacheKey, JSON.stringify(snapshot));
799
+ } catch { /* ignore */ }
800
+ } catch (e) {
801
+ console.error('Error processing market data', e);
802
+ }
803
+ },
804
+ error: (err) => {
805
+ console.warn('Failed to fetch live markets', err);
806
+ }
807
+ });
808
+ }
809
+
810
+ // Fetch market overview cards from backend (uses /getmarketcards)
811
+ private fetchMarketCards(): void {
812
+ this.market.getMarketCards().pipe(take(1)).subscribe({
813
+ next: (cards: any[]) => {
814
+ console.log('fetchLiveMarkets - /getmarketcards response:', cards);
815
+ try {
816
+ if (cards && Array.isArray(cards) && cards.length) {
817
+ const mapped = cards.map((c: any) => {
818
+ const display = c.display ?? c.price ?? c.value ?? c.last ?? c.close ?? null;
819
+ const value = this.formatPrice(display);
820
+ const pctRaw = c.chgPct ?? c.chg_pct ?? c.pct ?? c.changePct ?? c.change_percent ?? c.change ?? null;
821
+ const pct = (pctRaw !== undefined && pctRaw !== null) ? Number(pctRaw) : (Number(c.chg || 0) && typeof display === 'number' ? ((Number(c.chg) / (display - Number(c.chg))) * 100) : 0);
822
+ const n = Number(pct || 0);
823
+ return {
824
+ title: c.title ?? c.name ?? (c.symbol ?? '—'),
825
+ value,
826
+ chg: isNaN(n) ? '—' : ((n >= 0 ? '+' : '') + n.toFixed(2) + '%'),
827
+ dir: isNaN(n) ? 'neutral' : (n > 0 ? 'up' : n < 0 ? 'down' : 'neutral')
828
+ } as MarketCard;
829
+ });
830
+ this.marketCards = mapped;
831
+ try { localStorage.setItem('marketCardsCache', JSON.stringify(mapped)); } catch { /* ignore */ }
832
+ }
833
+ } catch (e) {
834
+ console.error('Error processing market cards', e);
835
+ }
836
+ },
837
+ error: (err) => {
838
+ console.warn('Failed to fetch market cards', err);
839
+ }
840
+ });
841
  }
842
 
843
  // UI helpers
 
849
  return Math.random();
850
  }
851
 
852
+ // Format a price for display. Treat null/undefined/empty as missing and return '—'.
853
+ private formatPrice(val: any): string {
854
+ if (val === null || val === undefined) return '—';
855
+ if (typeof val === 'number') return val.toLocaleString(undefined, { maximumFractionDigits: 2 });
856
+ if (typeof val === 'string') return val.trim() === '' ? '—' : val;
857
+ return String(val);
858
+ }
859
+
860
+ // Format index name for UI: remove bracketed suffixes like "(OMXS30)"
861
+ public formatIndexName(name: string | undefined | null): string {
862
+ if (!name) return '';
863
+ return String(name).replace(/\s*\([^)]*\)\s*/g, '').trim();
864
+ }
865
+
866
+ public setActiveIndex(code: string): void {
867
  this.activeIndex = code;
868
+ // fetch live quotes for newly active index immediately
869
+ this.fetchLiveQuotesForActiveIndex();
870
+ // update selected company to first of the newly active index
871
+ const first = this.companiesByIndex[this.activeIndex]?.[0];
872
+ if (first) {
873
+ this.selectedCompany = first.sym;
874
+ // draw initial chart for selected company (try live intraday)
875
+ this.fetchAndDrawChart(first.sym, first.ltp).catch(() => { /* ignore */ });
876
+ }
877
  }
878
 
879
+ // Build yfinance-compatible tickers for a company symbol depending on country
880
+ private buildTickersForIndex(indexCode: string): string[] {
881
+ const list = this.companiesByIndex[indexCode] || [];
882
+ const isIndia = (indexCode || '').toLowerCase().includes('nifty') || (indexCode || '').toLowerCase().includes('sensex') || this.selectedCountry.toLowerCase() === 'india';
883
+ return list.map(c => {
884
+ const sym = (c.sym || '').trim();
885
+ if (!sym) return '';
886
+ // For India, use NSE suffix if not already present
887
+ if (isIndia && !sym.includes('.') && !sym.includes(':')) return `${sym}.NS`;
888
+ return sym;
889
+ }).filter(Boolean);
890
+ }
891
+
892
+ // Fetch live quotes for companies in the active index and update table
893
+ private async fetchLiveQuotesForActiveIndex(): Promise<void> {
894
+ // show spinner in table
895
+ this.quotesLoading = true;
896
+ try {
897
+ const tickers = this.buildTickersForIndex(this.activeIndex || '');
898
+ if (!tickers || tickers.length === 0) {
899
+ this.quotesLoading = false;
900
+ return;
901
+ }
902
+
903
+ // helper: sleep
904
+ const sleep = (ms: number) => new Promise(res => setTimeout(res, ms));
905
+
906
+ // helper: fetch with retries + exponential backoff
907
+ const fetchWithRetries = async (batch: string[], maxRetries = 3, baseDelay = 500): Promise<any[] | null> => {
908
+ for (let attempt = 0; attempt < maxRetries; attempt++) {
909
+ try {
910
+ const obs = this.market.getQuotes(batch);
911
+ const res = await lastValueFrom(obs);
912
+ // treat empty result as failure (backend may return [] on error)
913
+ if (Array.isArray(res) && res.length > 0) return res;
914
+ // if got empty but this was final attempt, consider failure
915
+ } catch (err) {
916
+ // fall through to retry
917
+ console.warn('fetchWithRetry attempts Gi failed', err);
918
+ }
919
+ const delay = baseDelay * Math.pow(2, attempt);
920
+ await sleep(delay);
921
+ }
922
+ return null;
923
+ };
924
+
925
+ // batch tickers to avoid very long query strings; adjust size as needed
926
+ const batchSize = 10;
927
+ const batches: string[][] = [];
928
+ for (let i = 0; i < tickers.length; i += batchSize) {
929
+ batches.push(tickers.slice(i, i + batchSize));
930
+ }
931
+
932
+ const allQuotes: any[] = [];
933
+ for (const b of batches) {
934
+ const batchRes = await fetchWithRetries(b, 3, 400);
935
+ if (batchRes === null) {
936
+ // At least one batch failed after retries – fallback to demo ticks
937
+ console.warn('Quotes batch failed after retries, falling back to demo ticks');
938
+ this.quotesLoading = false;
939
+ // Apply one demo tick step immediately so UI updates
940
+ this.tickCompanies();
941
+ this.updateGainersLosers();
942
+ return;
943
+ }
944
+ allQuotes.push(...batchRes);
945
+ }
946
+
947
+ // If we reach here, we have fetched quotes for all batches
948
+ const list = this.companiesByIndex[this.activeIndex] || [];
949
+ const byReq = new Map<string, any>();
950
+ allQuotes.forEach((q: any) => {
951
+ if (!q) return;
952
+ const s = (q.symbol || '').toString();
953
+ byReq.set(s.toUpperCase(), q);
954
+ });
955
+
956
+ list.forEach((c) => {
957
+ const reqSym = (this.selectedCountry.toLowerCase() === 'india' && !c.sym.includes('.') && !c.sym.includes(':')) ? `${c.sym}.NS`.toUpperCase() : c.sym.toUpperCase();
958
+ const q = byReq.get(reqSym) || byReq.get(c.sym.toUpperCase());
959
+ if (!q) return;
960
+ if (q.price !== undefined && q.price !== null) c.ltp = Number(q.price);
961
+ // backend provides chgPct as percentage; use it if present
962
+ if (q.chgPct !== undefined && q.chgPct !== null) c.chg = Number(q.chgPct);
963
+ else if (q.chg !== undefined && q.chg !== null && c.ltp) {
964
+ // approximate percent from absolute change
965
+ const prev = c.ltp - Number(q.chg);
966
+ if (prev && !Number.isNaN(prev)) c.chg = (Number(q.chg) / prev) * 100;
967
+ }
968
+ if (q.high !== undefined && q.high !== null) c.high = Number(q.high);
969
+ if (q.low !== undefined && q.low !== null) c.low = Number(q.low);
970
+ });
971
+
972
+ this.updateGainersLosers();
973
+ this.lastUpdated = new Date().toLocaleString();
974
+ // persist updated quotes so reload shows latest values immediately
975
+ try {
976
+ const snapshot = { companiesByIndex: this.companiesByIndex, lastUpdated: this.lastUpdated };
977
+ localStorage.setItem(this.companiesCacheKey, JSON.stringify(snapshot));
978
+ } catch { /* ignore */ }
979
+
980
+ } catch (e) {
981
+ console.warn('fetchLiveQuotesForActiveIndex unexpected error', e);
982
+ // fallback to demo ticks
983
+ this.tickCompanies();
984
+ this.updateGainersLosers();
985
+ } finally {
986
+ this.quotesLoading = false;
987
+ }
988
+ }
989
+
990
+ // Create demo India indices with sparklines and miniPath for UI when live data not available
991
+ private populateDefaultIndiaIndices(): void {
992
+ const now = Date.now();
993
+ const items: GlobalIndex[] = this.defaultIndiaIndexNames.map((name: string, i: number) => {
994
+ const base = Math.round(20000 + i * 1000 + (Math.random() - 0.5) * 2000);
995
+ const change = Number(((Math.random() - 0.5) * 1.5).toFixed(2));
996
+ const changePct = Number((change / base * 100).toFixed(2));
997
+ const spark = this.series(base, 26);
998
+ const gi: GlobalIndex = {
999
+ id: `${name.replace(/\s+/g, '_')}_${i}`,
1000
+ name,
1001
+ country: 'India',
1002
+ region: 'India',
1003
+ price: base,
1004
+ change,
1005
+ changePct,
1006
+ sparkline: spark,
1007
+ } as GlobalIndex;
1008
+ gi.miniPath = this.sparkPath(gi.sparkline || []);
1009
+ gi.isUp = (gi.change || 0) >= 0;
1010
+ return gi;
1011
+ });
1012
+ this.indiaIndices = items;
1013
+
1014
+ // also ensure cache contains these so other logic can find India entries
1015
+ const byId = new Map<string, GlobalIndex>();
1016
+ [...this.allGlobalIndices, ...items].forEach(i => byId.set(i.id, i));
1017
+ this.allGlobalIndices = Array.from(byId.values());
1018
+ }
1019
+
1020
+ private countryMatches(selected: string, actual: string): boolean {
1021
+ if (!selected || !actual) return false;
1022
+ if (selected === actual) return true;
1023
+ // check aliases for selected label
1024
+ const aliases = this.countryAliases[selected];
1025
+ if (aliases) {
1026
+ for (const a of aliases) {
1027
+ if (actual.includes(a)) return true;
1028
+ }
1029
+ }
1030
+ // check if actual has a known alias that matches selected
1031
+ const key = Object.keys(this.countryAliases).find(k => this.countryAliases[k].some((a: string) => actual.includes(a)));
1032
+ if (key && key === selected) return true;
1033
+ // fallback: partial match
1034
+ return actual.includes(selected) || selected.includes(actual);
1035
  }
1036
 
1037
  // Clock
 
1050
  }
1051
 
1052
  private sparkPath(values: number[], w = 240, h = 28, pad = 2): string {
1053
+ if (!values || values.length === 0) return '';
1054
  const max = Math.max(...values);
1055
  const min = Math.min(...values);
1056
  const norm = (v: number) => h - pad - ((v - min) / Math.max(1e-6, max - min)) * (h - 2 * pad);
1057
+ const step = (w - 2 * pad) / (values.length - 1 || 1);
1058
  let d = '';
1059
  values.forEach((v, i) => {
1060
  const x = pad + i * step;
 
1115
  return arr;
1116
  }
1117
 
1118
+ // Compatibility wrapper for template calls delegate to SVG renderer
1119
+ public drawTodayChart(symbol = '—', base = 100): void {
1120
+ this.renderAreaChart(symbol, base);
1121
+ }
1122
+
1123
+ // Render an SVG area chart into the chart container. If `series` is provided it is used
1124
+ // as the plotted points (array of numbers). Otherwise a simulated series is generated.
1125
+ private renderAreaChart(symbol: string, base = 100, series?: number[]): void {
1126
+ const container = this.chartContainerRef?.nativeElement;
1127
+ if (!container) return;
1128
+
1129
+ // clear
1130
+ container.innerHTML = '';
1131
+
1132
+ // use device pixel width for responsiveness
1133
+ const rect = container.getBoundingClientRect();
1134
+ const width = Math.max(600, Math.round(rect.width || 700));
1135
+ const height = 260;
1136
+
1137
+ const pts = series && series.length ? series : this.makeIntraday(base, 90);
1138
+ const padLeft = 40;
1139
+ const padRight = 40;
1140
+ const padTop = 16;
1141
+ const padBottom = 36;
1142
+
1143
+ const min = Math.min(...pts);
1144
+ const max = Math.max(...pts);
1145
+
1146
+ const toX = (i: number) => padLeft + (i * (width - padLeft - padRight)) / (pts.length - 1 || 1);
1147
+ const toY = (v: number) => padTop + ((max - v) / Math.max(1e-6, max - min)) * (height - padTop - padBottom);
1148
+
1149
+ // SVG root
1150
+ const ns = 'http://www.w3.org/2000/svg';
1151
+ const svg = document.createElementNS(ns, 'svg');
1152
+ svg.setAttribute('width', String(width));
1153
+ svg.setAttribute('height', String(height));
1154
+ svg.setAttribute('viewBox', `0 0 ${width} ${height}`);
1155
+ svg.style.width = '100%';
1156
+ svg.style.height = '260px';
1157
+ svg.style.display = 'block';
1158
+
1159
+ // grid lines
1160
+ for (let g = 0; g < 4; g++) {
1161
+ const y = padTop + (g * (height - padTop - padBottom)) / 3;
1162
+ const line = document.createElementNS(ns, 'line');
1163
+ line.setAttribute('x1', String(padLeft));
1164
+ line.setAttribute('y1', String(y));
1165
+ line.setAttribute('x2', String(width - padRight));
1166
+ line.setAttribute('y2', String(y));
1167
+ line.setAttribute('stroke', '#e6ecff');
1168
+ line.setAttribute('stroke-opacity', '0.06');
1169
+ line.setAttribute('stroke-width', '1');
1170
+ svg.appendChild(line);
1171
+ }
1172
+
1173
+ // build path string
1174
+ let d = '';
1175
+ let area = '';
1176
+ pts.forEach((v, i) => {
1177
+ const x = toX(i);
1178
+ const y = toY(v);
1179
+ if (i === 0) {
1180
+ d += `M ${x} ${y}`;
1181
+ area += `M ${x} ${y}`;
1182
+ } else {
1183
+ d += ` L ${x} ${y}`;
1184
+ area += ` L ${x} ${y}`;
1185
+ }
1186
+ });
1187
+ // close area to baseline
1188
+ const lastX = toX(pts.length - 1);
1189
+ area += ` L ${lastX} ${height - padBottom} L ${padLeft} ${height - padBottom} Z`;
1190
+
1191
+ // area fill
1192
+ const areaPath = document.createElementNS(ns, 'path');
1193
+ areaPath.setAttribute('d', area);
1194
+ areaPath.setAttribute('fill', '#0f9d58');
1195
+ areaPath.setAttribute('fill-opacity', '0.08');
1196
+ svg.appendChild(areaPath);
1197
+
1198
+ // line path
1199
+ const path = document.createElementNS(ns, 'path');
1200
+ path.setAttribute('d', d);
1201
+ path.setAttribute('fill', 'none');
1202
+ path.setAttribute('stroke', '#0f9d58');
1203
+ path.setAttribute('stroke-width', '2');
1204
+ path.setAttribute('stroke-linejoin', 'round');
1205
+ path.setAttribute('stroke-linecap', 'round');
1206
+ svg.appendChild(path);
1207
+
1208
+ // last point marker
1209
+ const lastYVal = pts[pts.length - 1];
1210
+ const lastCx = toX(pts.length - 1);
1211
+ const lastCy = toY(lastYVal);
1212
+ const circ = document.createElementNS(ns, 'circle');
1213
+ circ.setAttribute('cx', String(lastCx));
1214
+ circ.setAttribute('cy', String(lastCy));
1215
+ circ.setAttribute('r', '4');
1216
+ circ.setAttribute('fill', '#0f9d58');
1217
+ svg.appendChild(circ);
1218
+
1219
+ // y-axis labels (right side)
1220
+ const labelRight = (val: number, y: number) => {
1221
+ const txt = document.createElementNS(ns, 'text');
1222
+ txt.setAttribute('x', String(width - padRight + 8));
1223
+ txt.setAttribute('y', String(y + 4));
1224
+ txt.setAttribute('fill', '#fff');
1225
+ txt.setAttribute('font-size', '12');
1226
+ txt.setAttribute('opacity', '0.9');
1227
+ txt.textContent = val.toFixed(2);
1228
+ svg.appendChild(txt);
1229
+ };
1230
+ labelRight(max, toY(max));
1231
+ labelRight(min, toY(min));
1232
+
1233
+ // x-axis labels (bottom)
1234
+ const ticks = [0, Math.floor(pts.length / 4), Math.floor(pts.length / 2), Math.floor((3 * pts.length) / 4), pts.length - 1];
1235
+ ticks.forEach((ti) => {
1236
+ const x = toX(ti);
1237
+ const txt = document.createElementNS(ns, 'text');
1238
+ txt.setAttribute('x', String(x));
1239
+ txt.setAttribute('y', String(height - 8));
1240
+ txt.setAttribute('fill', '#999');
1241
+ txt.setAttribute('font-size', '11');
1242
+ txt.setAttribute('text-anchor', 'middle');
1243
+ // fake times: show HH:MM using index
1244
+ const minutes = (ti * 5) % 60;
1245
+ const hour = 9 + Math.floor(ti / 12);
1246
+ txt.textContent = `${hour}:${String(minutes).padStart(2, '0')} AM`;
1247
+ svg.appendChild(txt);
1248
+ });
1249
+
1250
+ container.appendChild(svg);
1251
+
1252
+ // expose small tooltip on hover (basic)
1253
+ svg.addEventListener('mousemove', (ev) => {
1254
+ const rect = svg.getBoundingClientRect();
1255
+ const x = ev.clientX - rect.left;
1256
+ // find closest index
1257
+ let closest = 0;
1258
+ let bestDist = Infinity;
1259
+ for (let i = 0; i < pts.length; i++) {
1260
+ const dx = Math.abs(toX(i) - x);
1261
+ if (dx < bestDist) { bestDist = dx; closest = i; }
1262
+ }
1263
+ // simple tooltip: set title on container
1264
+ const v = pts[closest];
1265
+ svg.setAttribute('title', `Index: ${symbol}\nValue: ${v.toFixed(2)}`);
1266
+ });
1267
+ }
1268
+
1269
+ // Fetch intraday series for symbol and draw chart. Falls back to simulated when live data present
1270
+ private async fetchAndDrawChart(sym: string, base = 100): Promise<void> {
1271
+ try {
1272
+ // try backend intraday endpoint (via MarketService.getIntraday)
1273
+ const obs = this.market.getIntraday(sym, '1d', '1m');
1274
+ const resp: any = await lastValueFrom(obs);
1275
+ // expected resp: { symbol, timestamps: string[], closes: number[] }
1276
+ if (resp && Array.isArray(resp.closes) && resp.closes.length > 0) {
1277
+ this.chartSymbol = sym;
1278
+ this.drawSeriesChart(resp.closes, sym);
1279
+ return;
1280
+ }
1281
+ } catch (e) {
1282
+ // ignore and fallback
1283
+ }
1284
+ // fallback: simulate intraday
1285
+ this.chartSymbol = sym;
1286
+ this.renderAreaChart(sym, base);
1287
+ }
1288
+
1289
+ // Draw given numeric series on canvas (replaces simulated draw when live data present)
1290
+ private drawSeriesChart(values: number[], symbol = '—'): void {
1291
  this.chartSymbol = symbol;
1292
  const canvas = this.canvasRef?.nativeElement;
1293
  if (!canvas) return;
 
1299
  const h = canvas.height;
1300
  ctx.clearRect(0, 0, w, h);
1301
 
 
1302
  const min = Math.min(...values);
1303
  const max = Math.max(...values);
1304
  const padX = 28;
1305
  const padY = 16;
1306
+ const toX = (i: number) => padX + (i * (w - padX * 2)) / (values.length - 1 || 1);
1307
  const toY = (v: number) => h - padY - ((v - min) / Math.max(1e-6, max - min)) * (h - padY * 2);
1308
 
1309
  // grid
 
1319
  }
1320
  ctx.globalAlpha = 1;
1321
 
 
1322
  const up = values[values.length - 1] >= values[0];
1323
  const styles = getComputedStyle(this.host.nativeElement);
1324
+ const stroke = styles.getPropertyValue(up ? '--up' : '--down').trim() || (up ? '#12c48b' : '#ff5b6b');
 
1325
 
1326
  ctx.strokeStyle = stroke;
1327
  ctx.lineWidth = 2;
 
1329
  values.forEach((v, i) => {
1330
  const x = toX(i);
1331
  const y = toY(v);
1332
+ ctx.lineTo(x, y);
 
1333
  });
1334
  ctx.stroke();
1335
 
 
1341
  ctx.arc(lastX, lastY, 3, 0, Math.PI * 2);
1342
  ctx.fill();
1343
  }
1344
+
1345
+ public selectCountry(country: string): void {
1346
+ this.selectedCountry = country;
1347
+ // always apply selection immediately so UI updates synchronously
1348
+ this.applySelection(country);
1349
+ // if we haven't fetched live data yet, fetch (fetchGlobalIndices will re-apply selection once data arrives)
1350
+ if (!this.globalFetched) this.fetchGlobalIndices();
1351
+ }
1352
+
1353
+ // Centralized selection logic
1354
+ private applySelection(country: string): void {
1355
+ const sel = (country || '').toLowerCase().trim();
1356
+ if (!sel) {
1357
+ this.globalIndices = [];
1358
+ return;
1359
+ }
1360
+
1361
+ if (sel === 'india') {
1362
+ // Prefer prioritized live India indices; fallback to demo if none
1363
+ const built = this.buildCountryIndices('india');
1364
+ if (built && built.length) this.indiaIndices = built;
1365
+ else if (!this.indiaIndices || this.indiaIndices.length === 0) this.populateDefaultIndiaIndices();
1366
+ this.globalIndices = [];
1367
+ return;
1368
+ }
1369
+
1370
+ // try to find live entries in cache
1371
+ const matched = (this.allGlobalIndices || []).filter((g) => this.countryMatches(sel, (g.country || '').toLowerCase()) && (g.region || '').toLowerCase() !== 'india');
1372
+ if (matched && matched.length) {
1373
+ this.globalIndices = matched;
1374
+ return;
1375
+ }
1376
+
1377
+ // no live data for this country — create demo indices for known countries so UI shows names immediately
1378
+ let demo: GlobalIndex[] = [];
1379
+ if (sel === 'us') demo = this.createUSIndices();
1380
+ else if (sel === 'uk') demo = this.createUKIndices();
1381
+ else if (sel === 'german') demo = this.createGermanIndices();
1382
+ else if (sel === 'sweden') demo = this.createSwedenIndices();
1383
+ else if (sel === 'russia') demo = this.createRussiaIndices();
1384
+
1385
+ this.globalIndices = demo;
1386
+
1387
+ // merge demo into cache to improve subsequent lookups
1388
+ if (demo && demo.length) {
1389
+ const byId = new Map<string, GlobalIndex>();
1390
+ [...this.allGlobalIndices, ...demo].forEach(i => byId.set(i.id, i));
1391
+ this.allGlobalIndices = Array.from(byId.values());
1392
+ }
1393
+ }
1394
+
1395
+ public onCompanyClick(e: Event, c: Company): void {
1396
+ e.preventDefault();
1397
+ this.selectedCompany = c.sym;
1398
+ // try to fetch live intraday series and draw; fallback to simulated when not available
1399
+ this.fetchAndDrawChart(c.sym, c.ltp).then(() => {
1400
+ // highlight handled by selectedCompany binding
1401
+ }).catch(() => {
1402
+ this.renderAreaChart(c.sym, c.ltp);
1403
+ });
1404
+ }
1405
  }
src/app/dashboard/dashboard.service.ts ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Injectable } from '@angular/core';
2
+ import { HttpClient } from '@angular/common/http';
3
+ import { Observable, of } from 'rxjs';
4
+ import { map, catchError } from 'rxjs/operators';
5
+
6
+ export type YahooQuote = {
7
+ symbol: string;
8
+ regularMarketPrice?: number;
9
+ regularMarketChangePercent?: number;
10
+ regularMarketDayHigh?: number;
11
+ regularMarketDayLow?: number;
12
+ };
13
+
14
+ @Injectable({ providedIn: 'root' })
15
+ export class DashboardService {
16
+ // mirror MarketService base URL logic so dashboard can call backend endpoints directly
17
+ private readonly baseUrl =
18
+ location.hostname.endsWith('hf.space')
19
+ ? 'https://pykara-pytrade-backend.hf.space'
20
+ : 'http://127.0.0.1:5000';
21
+
22
+ private yahooBase = 'https://query1.finance.yahoo.com/v7/finance/quote';
23
+
24
+ constructor(private http: HttpClient) {}
25
+
26
+ // Backend endpoints used by dashboard component
27
+ getCompanies(): Observable<any> {
28
+ return this.http.get<any>(`${this.baseUrl}/getcompanies`).pipe(
29
+ catchError((err: any) => {
30
+ console.warn('DashboardService.getCompanies error', err);
31
+ return of({});
32
+ })
33
+ );
34
+ }
35
+
36
+ getMarketCards(): Observable<any[]> {
37
+ return this.http.get<any[]>(`${this.baseUrl}/getmarketcards`).pipe(
38
+ catchError((err: any) => {
39
+ console.warn('DashboardService.getMarketCards error', err);
40
+ return of([]);
41
+ })
42
+ );
43
+ }
44
+
45
+ getGlobalIndices(): Observable<any> {
46
+ return this.http.get<any>(`${this.baseUrl}/getglobalindices`).pipe(
47
+ catchError((err: any) => {
48
+ console.warn('DashboardService.getGlobalIndices error', err);
49
+ return of([]);
50
+ })
51
+ );
52
+ }
53
+
54
+ // Intraday series: backend expected endpoint is /getintraday (best-effort)
55
+ getIntraday(symbol: string, range = '1d', interval = '1m'): Observable<any> {
56
+ const url = `${this.baseUrl}/getintraday?symbol=${encodeURIComponent(symbol)}&range=${encodeURIComponent(range)}&interval=${encodeURIComponent(interval)}`;
57
+ return this.http.get<any>(url).pipe(
58
+ catchError((err: any) => {
59
+ console.warn('DashboardService.getIntraday error', err);
60
+ return of(null);
61
+ })
62
+ );
63
+ }
64
+
65
+ // Fetch quotes from Yahoo public API and return an array of quote objects
66
+ getQuotes(symbols: string[]): Observable<any[]> {
67
+ if (!symbols || symbols.length === 0) return of([]);
68
+ const syms = symbols.map(s => s.toUpperCase()).join(',');
69
+ const url = `${this.yahooBase}?symbols=${encodeURIComponent(syms)}`;
70
+ return this.http.get<any>(url).pipe(
71
+ map((res: any) => res?.quoteResponse?.result || []),
72
+ catchError((err: any) => {
73
+ console.warn('DashboardService.getQuotes error', err);
74
+ return of([]);
75
+ })
76
+ );
77
+ }
78
+ }