py-trade / src /app /dashboard /dashboard.component.ts
Hemaambika's picture
update markets page
730bef4
import { Component, ElementRef, OnDestroy, OnInit, AfterViewInit, ViewChild } from '@angular/core';
import { CommonModule } from '@angular/common';
import { HttpClientModule } from '@angular/common/http';
import { DashboardService } from './dashboard.service';
import { take } from 'rxjs/operators';
import { lastValueFrom } from 'rxjs';
type IndexItem = { code: string; price: number; chg: number; miniPath?: string; isUp?: boolean };
type Company = { sym: string; ltp: number; chg: number; high: number; low: number };
type Sector = { name: string; picks: string[] };
type MarketCard = { title: string; value: string; chg: string; dir: 'up' | 'down' | 'neutral' };
type GlobalIndex = { id: string; name: string; country: string; region: string; price: number; change: number; changePct: number; sparkline: number[]; miniPath?: string; isUp?: boolean };
@Component({
selector: 'app-dashboard',
standalone: true,
imports: [CommonModule, HttpClientModule], // IndexCardComponent],
templateUrl: './dashboard.component.html',
styleUrls: ['./dashboard.component.scss'],
})
export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy {
// Demo data (used as fallback until live data loads)
indices: IndexItem[] = [
{ code: 'NIFTY 50', price: 24480.55, chg: +0.35 },
{ code: 'BANK NIFTY', price: 51880.20, chg: -0.12 },
{ code: 'SENSEX', price: 80820.40, chg: +0.28 },
{ code: 'NIFTY MIDCAP', price: 53000.10, chg: +0.45 },
{ code: 'NIFTY SMALLCAP', price: 17350.60, chg: -0.18 },
];
// Market overview cards (populated live when API responds)
// initialize placeholders to em dash so UI doesn't show raw 0 or empty strings
marketCards: MarketCard[] = [
{ title: 'Gold', value: 'β€”', chg: 'β€”', dir: 'neutral' },
{ title: 'Silver', value: 'β€”', chg: 'β€”', dir: 'neutral' },
{ title: 'Crude Oil (Brent)', value: 'β€”', chg: 'β€”', dir: 'neutral' },
{ title: 'Crude Oil (WTI)', value: 'β€”', chg: 'β€”', dir: 'neutral' },
{ title: 'Natural Gas', value: 'β€”', chg: 'β€”', dir: 'neutral' },
{ title: 'USD/INR', value: 'β€”', chg: 'β€”', dir: 'neutral' },
{ title: 'EUR/USD', value: 'β€”', chg: 'β€”', dir: 'neutral' },
{ title: 'GBP/USD', value: 'β€”', chg: 'β€”', dir: 'neutral' },
{ title: 'Bitcoin', value: 'β€”', chg: 'β€”', dir: 'neutral' },
{ title: 'Ethereum', value: 'β€”', chg: 'β€”', dir: 'neutral' },
{ title: 'S&P 500', value: 'β€”', chg: 'β€”', dir: 'neutral' },
{ title: 'NASDAQ', value: 'β€”', chg: 'β€”', dir: 'neutral' },
{ title: 'DAX', value: 'β€”', chg: 'β€”', dir: 'neutral' },
{ title: 'Nikkei', value: 'β€”', chg: 'β€”', dir: 'neutral' },
{ title: 'Copper', value: 'β€”', chg: 'β€”', dir: 'neutral' }
];
// expose duplicated list for seamless loop in template
get marketCardsRepeat(): MarketCard[] {
return [...this.marketCards, ...this.marketCards];
}
companiesByIndex: Record<string, Company[]> = {
'NIFTY 50': [
{ sym: 'RELIANCE', ltp: 2950.30, chg: +0.85, high: 2972.0, low: 2920.0 },
{ sym: 'TCS', ltp: 4175.90, chg: -0.34, high: 4210.0, low: 4152.0 },
{ sym: 'INFY', ltp: 1622.40, chg: +0.42, high: 1631.5, low: 1605.2 },
{ sym: 'HDFCBANK', ltp: 1548.10, chg: +0.12, high: 1556.0, low: 1531.5 },
{ sym: 'ICICIBANK', ltp: 1244.65, chg: -0.25, high: 1255.0, low: 1238.0 },
],
'BANK NIFTY': [
{ sym: 'SBIN', ltp: 884.30, chg: +0.70, high: 892.0, low: 876.2 },
{ sym: 'AXISBANK', ltp: 1204.70, chg: -0.31, high: 1219.0, low: 1198.0 },
{ sym: 'KOTAKBANK', ltp: 1744.50, chg: +0.15, high: 1752.0, low: 1732.0 },
{ sym: 'PNB', ltp: 119.80, chg: +1.25, high: 121.3, low: 118.4 },
{ sym: 'BANKBARODA', ltp: 278.20, chg: -0.44, high: 281.8, low: 276.5 },
],
'SENSEX': [
{ sym: 'LT', ltp: 3822.40, chg: +0.34, high: 3839.0, low: 3788.5 },
{ sym: 'ASIANPAINT', ltp: 3221.60, chg: -0.28, high: 3245.0, low: 3200.2 },
{ sym: 'ITC', ltp: 496.70, chg: +0.48, high: 499.5, low: 492.6 },
{ sym: 'HCLTECH', ltp: 1822.10, chg: +0.22, high: 1833.0, low: 1808.0 },
{ sym: 'BHARTIARTL', ltp: 1412.55, chg: -0.12, high: 1423.0, low: 1407.0 },
],
'NIFTY MIDCAP': [
{ sym: 'TATAELXSI', ltp: 9175.00, chg: +0.90, high: 9250.0, low: 9080.0 },
{ sym: 'AUBANK', ltp: 658.50, chg: -0.35, high: 666.0, low: 652.0 },
{ sym: 'INDHOTEL', ltp: 692.10, chg: +0.55, high: 699.0, low: 684.5 },
{ sym: 'TVSMOTOR', ltp: 2088.20, chg: +0.20, high: 2105.0, low: 2068.0 },
{ sym: 'ABBOTINDIA', ltp: 28200.00, chg: -0.40, high: 28450.0, low: 28000.0 },
],
'NIFTY SMALLCAP': [
{ sym: 'TANLA', ltp: 1115.10, chg: +1.12, high: 1134.0, low: 1101.0 },
{ sym: 'MAPMYINDIA', ltp: 2050.30, chg: -0.50, high: 2076.0, low: 2036.0 },
{ sym: 'KEI', ltp: 4965.00, chg: +0.44, high: 5012.0, low: 4920.0 },
{ sym: 'PNCINFRA', ltp: 475.75, chg: +0.18, high: 482.0, low: 470.0 },
{ sym: 'TARC', ltp: 128.60, chg: -0.22, high: 131.0, low: 127.8 },
],
};
// Create demo US companies per index so Companies table and sectors show US firms when 'US' selected
private createUSCompaniesByIndex(): Record<string, Company[]> {
return {
'S&P 500': [
{ sym: 'AAPL', ltp: 175.32, chg: +1.2, high: 176.5, low: 172.5 },
{ sym: 'MSFT', ltp: 345.10, chg: -0.5, high: 348.0, low: 340.2 },
{ sym: 'AMZN', ltp: 140.25, chg: +0.8, high: 142.0, low: 138.0 },
{ sym: 'GOOGL', ltp: 128.70, chg: +0.4, high: 130.0, low: 127.0 },
{ sym: 'META', ltp: 310.45, chg: -0.9, high: 316.0, low: 308.1 },
],
'Dow Jones': [
{ sym: 'MSFT', ltp: 345.10, chg: -0.5, high: 348.0, low: 340.2 },
{ sym: 'UNH', ltp: 480.22, chg: +0.3, high: 482.0, low: 475.0 },
{ sym: 'V', ltp: 230.12, chg: +0.6, high: 231.5, low: 228.0 },
{ sym: 'JPM', ltp: 140.50, chg: -0.2, high: 142.0, low: 139.0 },
{ sym: 'GS', ltp: 360.75, chg: +0.1, high: 362.0, low: 358.0 },
],
'Nasdaq Composite': [
{ sym: 'AAPL', ltp: 175.32, chg: +1.2, high: 176.5, low: 172.5 },
{ sym: 'MSFT', ltp: 345.10, chg: -0.5, high: 348.0, low: 340.2 },
{ sym: 'TSLA', ltp: 210.44, chg: +2.5, high: 215.0, low: 205.5 },
{ sym: 'NVDA', ltp: 420.55, chg: +3.1, high: 430.2, low: 410.0 },
{ sym: 'ADBE', ltp: 485.60, chg: -0.6, high: 490.0, low: 482.0 },
],
'S&P MidCap 400': [
{ sym: 'ODFL', ltp: 150.12, chg: +0.9, high: 152.0, low: 148.0 },
{ sym: 'LVS', ltp: 80.34, chg: -0.4, high: 82.0, low: 79.0 },
{ sym: 'UAL', ltp: 52.10, chg: +1.5, high: 53.0, low: 50.5 },
{ sym: 'NWL', ltp: 30.22, chg: -0.2, high: 31.0, low: 29.5 },
{ sym: 'EXPD', ltp: 115.44, chg: +0.7, high: 117.0, low: 114.0 },
],
'S&P SmallCap 600': [
{ sym: 'AEO', ltp: 25.12, chg: +0.5, high: 25.8, low: 24.5 },
{ sym: 'CNC', ltp: 45.22, chg: -0.3, high: 46.0, low: 44.0 },
{ sym: 'FLEX', ltp: 12.34, chg: +2.0, high: 12.8, low: 11.9 },
{ sym: 'BOKF', ltp: 55.66, chg: -1.1, high: 57.0, low: 55.0 },
{ sym: 'PRXL', ltp: 8.90, chg: +0.2, high: 9.2, low: 8.5 },
]
};
}
// Create demo India companies per index so Companies table and sectors show India firms when 'India' selected
private createIndiaCompaniesByIndex(): Record<string, Company[]> {
return {
'NIFTY 50': [
{ sym: 'RELIANCE', ltp: 2950.30, chg: 0.85, high: 2972.0, low: 2920.0 },
{ sym: 'TCS', ltp: 4175.90, chg: -0.34, high: 4210.0, low: 4152.0 },
{ sym: 'INFY', ltp: 1622.40, chg: 0.42, high: 1631.5, low: 1605.2 },
{ sym: 'HDFCBANK', ltp: 1548.10, chg: 0.12, high: 1556.0, low: 1531.5 },
{ sym: 'ICICIBANK', ltp: 1244.65, chg: -0.25, high: 1255.0, low: 1238.0 },
],
'BANK NIFTY': [
{ sym: 'SBIN', ltp: 884.30, chg: 0.70, high: 892.0, low: 876.2 },
{ sym: 'AXISBANK', ltp: 1204.70, chg: -0.31, high: 1219.0, low: 1198.0 },
{ sym: 'KOTAKBANK', ltp: 1744.50, chg: 0.15, high: 1752.0, low: 1732.0 },
{ sym: 'PNB', ltp: 119.80, chg: 1.25, high: 121.3, low: 118.4 },
{ sym: 'BANKBARODA', ltp: 278.20, chg: -0.44, high: 281.8, low: 276.5 },
],
'SENSEX': [
{ sym: 'LT', ltp: 3822.40, chg: 0.34, high: 3839.0, low: 3788.5 },
{ sym: 'ASIANPAINT', ltp: 3221.60, chg: -0.28, high: 3245.0, low: 3200.2 },
{ sym: 'ITC', ltp: 496.70, chg: 0.48, high: 499.5, low: 492.6 },
{ sym: 'HCLTECH', ltp: 1822.10, chg: 0.22, high: 1833.0, low: 1808.0 },
{ sym: 'BHARTIARTL', ltp: 1412.55, chg: -0.12, high: 1423.0, low: 1407.0 },
],
'NIFTY MIDCAP': [
{ sym: 'TATAELXSI', ltp: 9175.00, chg: 0.90, high: 9250.0, low: 9080.0 },
{ sym: 'AUBANK', ltp: 658.50, chg: -0.35, high: 666.0, low: 652.0 },
{ sym: 'INDHOTEL', ltp: 692.10, chg: 0.55, high: 699.0, low: 684.5 },
{ sym: 'TVSMOTOR', ltp: 2088.20, chg: 0.20, high: 2105.0, low: 2068.0 },
{ sym: 'ABBOTINDIA', ltp: 28200.00, chg: -0.40, high: 28450.0, low: 28000.0 },
],
'NIFTY SMALLCAP': [
{ sym: 'TANLA', ltp: 1115.10, chg: 1.12, high: 1134.0, low: 1101.0 },
{ sym: 'MAPMYINDIA', ltp: 2050.30, chg: -0.50, high: 2076.0, low: 2036.0 },
{ sym: 'KEI', ltp: 4965.00, chg: 0.44, high: 5012.0, low: 4920.0 },
{ sym: 'PNCINFRA', ltp: 475.75, chg: 0.18, high: 482.0, low: 470.0 },
{ sym: 'TARC', ltp: 128.60, chg: -0.22, high: 131.0, low: 127.8 },
],
};
}
// Create sector picks for US so sector grid shows US companies when 'US' selected
private createUSSectors(): Sector[] {
return [
{ name: 'Technology', picks: ['AAPL', 'MSFT', 'GOOGL', 'NVDA'] },
{ name: 'Financials', picks: ['JPM', 'GS', 'BAC', 'C'] },
{ name: 'Consumer Discretionary', picks: ['AMZN', 'TSLA', 'MCD'] },
{ name: 'Healthcare', picks: ['UNH', 'JNJ', 'PFE'] },
{ name: 'Industrials', picks: ['UNP', 'HON', 'CAT'] },
];
}
sectors: Sector[] = [
{ name: 'IT & Software', picks: ['TCS', 'INFY', 'HCLTECH', 'TECHM'] },
{ name: 'Banking & Finance', picks: ['HDFCBANK', 'ICICIBANK', 'SBIN', 'AXISBANK'] },
{ name: 'Energy & Materials', picks: ['RELIANCE', 'ONGC', 'TATASTEEL', 'COALINDIA'] },
{ name: 'Auto', picks: ['TATAMOTORS', 'MARUTI', 'M&amp;M', 'TVSMOTOR'] },
{ name: 'FMCG', picks: ['ITC', 'HINDUNILVR', 'NESTLEIND', 'DABAR'] },
{ name: 'Telecom', picks: ['BHARTIARTL', 'VODAFONEIDE', 'TATACOMM', 'INDUSTOWER'] },
];
get indexCodes(): string[] {
return Object.keys(this.companiesByIndex);
}
activeIndex: string = this.indexCodes[0];
// currently selected company symbol for chart highlighting
selectedCompany: string | null = null;
gainers: Company[] = [];
losers: Company[] = [];
// New global indices lists
globalIndices: GlobalIndex[] = [];
indiaIndices: GlobalIndex[] = [];
// All indices cache (includes India and others)
private allGlobalIndices: GlobalIndex[] = [];
// Country filter
// default countries shown immediately (will be replaced by live data when available)
countries: string[] = ['India', 'US', 'Uk', 'German', 'Sweden', 'Russia'];
selectedCountry = 'India';
// common alias map so UI labels like 'US' or 'Uk' match backend country names
private countryAliases: Record<string, string[]> = {
'us': ['united states', 'usa', 'us', 'u.s.a', 'u.s.'],
'uk': ['united kingdom', 'britain', 'uk', 'u.k.'],
'german': ['germany', 'german'],
'russia': ['russia', 'russian federation'],
'india': ['india'],
'sweden': ['sweden']
};
// demo list for India indices
private defaultIndiaIndexNames: string[] = [
'Nifty 50',
'Nifty Next 50',
'Nifty 100',
'Nifty 500',
'Nifty Bank',
'Nifty IT',
'Nifty Pharma',
'Nifty FMCG',
'Nifty Midcap 100',
'Nifty Smallcap 100'
];
// avoid repeated fetches in quick succession
private globalFetched = false;
// last updated timestamp shown in the header
lastUpdated = 'β€”';
// chart symbol and timer handle
chartSymbol = 'RELIANCE';
private tickHandle?: number;
public quotesIntervalHandle?: number;
@ViewChild('chartCanvas', { static: false })
private canvasRef?: ElementRef<HTMLCanvasElement>;
@ViewChild('chartContainer', { static: false })
private chartContainerRef?: ElementRef<HTMLDivElement>;
@ViewChild('tickerTrack', { static: false })
private tickerTrackRef?: ElementRef<HTMLDivElement>;
private marqueeAnimationId?: number;
private marqueeResizeObserver?: ResizeObserver | null = null;
private marqueeMutationObserver?: MutationObserver | null = null;
private marqueeMeasureTimeout?: any;
private lastDurationSec?: number | null = null;
// priority lists of important indices per country (used to order and pick indices from live payload)
private importantIndicesMap: Record<string, string[]> = {
'india': [
'Nifty 50', 'Nifty Bank', 'SENSEX', 'Nifty Next 50', 'Nifty 100', 'Nifty 500', 'Nifty IT', 'Nifty Pharma', 'Nifty FMCG', 'Nifty Midcap 100', 'Nifty Smallcap 100'
],
'us': ['S&P 500', 'Dow Jones', 'Nasdaq Composite', 'S&P MidCap 400', 'S&P SmallCap 600'],
'uk': ['FTSE 100', 'FTSE 250', 'FTSE 350'],
'german': ['DAX', 'MDAX', 'TecDAX'],
'sweden': ['OMX Stockholm 30'],
'russia': ['MOEX Russia', 'RTS Index']
};
private uniqueByName(list: GlobalIndex[]): GlobalIndex[] {
const seen = new Set<string>();
const out: GlobalIndex[] = [];
for (const it of list) {
const name = (it.name || '').toString().trim();
const key = name.toLowerCase();
if (!key) continue;
if (seen.has(key)) continue;
seen.add(key);
out.push(it);
}
return out;
}
// Build a prioritized list of indices for a country based on importantIndicesMap and available live entries
private buildCountryIndices(countryKey: string): GlobalIndex[] {
if (!countryKey) return [];
const key = countryKey.toLowerCase().trim();
const important = this.importantIndicesMap[key] || [];
// gather live matches (case-insensitive name match)
const live = (this.allGlobalIndices || []).filter(g => this.countryMatches(key, (g.country || '').toLowerCase()));
const byNameMap = new Map<string, GlobalIndex>();
// normalize and index live by name
live.forEach(g => {
const n = (g.name || '').toString().trim();
if (n) byNameMap.set(n.toLowerCase(), g);
});
const result: GlobalIndex[] = [];
// pick in priority order
for (const name of important) {
const found = byNameMap.get(name.toLowerCase());
if (found) result.push(found);
}
// append any other live indices for the country that were not in the important list (unique)
live.forEach(g => {
const n = (g.name || '').toString().trim().toLowerCase();
if (!result.some(r => ((r.name || '').toLowerCase() === n))) result.push(g);
});
return this.uniqueByName(result).map(g => {
// ensure miniPath computed
g.miniPath = this.sparkPath(g.sparkline || []);
g.isUp = (g.changePct || 0) >= 0;
return g;
});
}
constructor(private host: ElementRef<HTMLElement>, private market: DashboardService) { }
ngOnInit(): void {
// Initialize mini sparklines and clock
this.indices.forEach((idx) => this.refreshIndexSpark(idx));
this.setClock();
this.updateGainersLosers();
// Load cached market overview so values appear immediately on reload if available
try {
const raw = localStorage.getItem('marketCardsCache');
if (raw) {
const cached = JSON.parse(raw) as MarketCard[];
if (Array.isArray(cached) && cached.length) this.marketCards = cached;
}
} catch {
// ignore cache errors
}
// --- Load cached companies/quotes so Companies table shows immediately after reload ---
try {
const rawComp = localStorage.getItem('companiesCacheV1');
if (rawComp) {
const parsed = JSON.parse(rawComp) as any;
if (parsed && parsed.companiesByIndex) {
this.companiesByIndex = parsed.companiesByIndex;
if (parsed.lastUpdated) this.lastUpdated = parsed.lastUpdated;
this.updateGainersLosers();
// ensure activeIndex valid
if (!this.indexCodes.includes(this.activeIndex)) {
this.activeIndex = this.indexCodes[0];
}
}
}
} catch {
// ignore
}
// Fetch live data once on init (and keep demo simulation running as fallback)
this.fetchLiveMarkets();
// fetch market overview cards independently so they display even if /getcompanies fails
this.fetchMarketCards();
this.fetchGlobalIndices();
// Ensure demo India indices present initially if backend doesn't provide them
if (this.selectedCountry && this.selectedCountry.toLowerCase().trim() === 'india' && (!this.indiaIndices || this.indiaIndices.length === 0)) {
this.populateDefaultIndiaIndices();
}
// Start ticks every 3s
this.tickHandle = window.setInterval(() => {
this.tickIndices();
this.tickCompanies();
this.updateGainersLosers();
this.setClock();
}, 3000);
// Start polling live quotes every 30s
this.quotesIntervalHandle = window.setInterval(() => {
this.fetchLiveQuotesForActiveIndex();
}, 30000);
// ensure selectedCompany defaults to first company in active index (from cache or demo)
const first = this.companiesByIndex[this.activeIndex]?.[0];
if (first) {
this.selectedCompany = first.sym;
// draw initial chart for selected company (try live intraday)
this.fetchAndDrawChart(first.sym, first.ltp).catch(() => { /* ignore */ });
}
}
ngAfterViewInit(): void {
// Initial chart
const initial = this.companiesByIndex[this.activeIndex][0];
this.chartSymbol = initial?.sym ?? 'β€”';
// draw SVG style area chart in container
this.renderAreaChart(initial?.sym ?? 'β€”', initial?.ltp ?? 100);
// setup ticker marquee after view init
this.setupMarquee();
}
ngOnDestroy(): void {
if (this.tickHandle) {
clearInterval(this.tickHandle);
}
if (this.quotesIntervalHandle) {
clearInterval(this.quotesIntervalHandle);
}
if (this.marqueeAnimationId) {
cancelAnimationFrame(this.marqueeAnimationId);
this.marqueeAnimationId = undefined;
}
if (this.marqueeResizeObserver) {
try { this.marqueeResizeObserver.disconnect(); } catch (e) { /* ignore */ }
this.marqueeResizeObserver = null;
}
if (this.marqueeMutationObserver) {
try { this.marqueeMutationObserver.disconnect(); } catch (e) { /* ignore */ }
this.marqueeMutationObserver = null;
}
if (this.marqueeMeasureTimeout) {
clearTimeout(this.marqueeMeasureTimeout);
this.marqueeMeasureTimeout = undefined;
}
}
// Loading state shown by template overlay while quotes request running
quotesLoading: boolean = false;
// localStorage key for persisted companies/quotes cache
private readonly companiesCacheKey = 'companiesCacheV1';
// Called after view init to set animation duration based on content width so the marquee flows evenly
private setupMarquee(): void {
const el = this.tickerTrackRef?.nativeElement as HTMLDivElement | undefined;
if (!el) return;
// measure content width and container width; duplicated items => distance is half scrollWidth
const measureOnce = () => {
try {
const totalWidth = el.scrollWidth || 0; // width of duplicated content
const distance = Math.max(1, totalWidth / 2);
const pxPerSecond = 120; // control speed
const durationSec = Math.max(6, distance / pxPerSecond);
// avoid changing CSS var if duration unchanged (prevents animation restart)
if (this.lastDurationSec == null || Math.abs((this.lastDurationSec || 0) - durationSec) > 0.5) {
el.style.setProperty('--marquee-duration', `${Math.round(durationSec)}s`);
this.lastDurationSec = durationSec;
}
} catch (e) {
// ignore
}
};
const scheduleMeasure = () => {
if (this.marqueeMeasureTimeout) clearTimeout(this.marqueeMeasureTimeout);
this.marqueeMeasureTimeout = setTimeout(() => {
measureOnce();
this.marqueeMeasureTimeout = undefined;
}, 120);
};
// initial measurement
scheduleMeasure();
// observe size changes and DOM changes, but debounce actual measurement
try {
this.marqueeResizeObserver = new ResizeObserver(() => scheduleMeasure());
this.marqueeResizeObserver.observe(el);
if (el.parentElement) this.marqueeResizeObserver.observe(el.parentElement);
} catch (e) {
this.marqueeResizeObserver = null;
}
try {
this.marqueeMutationObserver = new MutationObserver(() => scheduleMeasure());
this.marqueeMutationObserver.observe(el, { childList: true, subtree: true });
} catch (e) {
this.marqueeMutationObserver = null;
}
}
private fetchGlobalIndices(): void {
// avoid duplicate fetch
if (this.globalFetched) return;
this.globalFetched = true;
this.market.getGlobalIndices().pipe(take(1)).subscribe({
next: (resp: any) => {
try {
console.log('fetchGlobalIndices resp:', resp);
const data = resp?.data || resp;
if (!Array.isArray(data)) return;
// capture marketCards from response early so we can use it when deriving indices
const respMarketCards = (resp as any)?.marketCards;
// map all returned indices
const mapped = data.map(this.mapToGlobalIndex);
// If backend returned no mapped indices but provided marketCards, derive a few common indices from marketCards
if ((!mapped || mapped.length === 0) && Array.isArray(respMarketCards) && respMarketCards.length) {
const indexNames = new Set(['S&P 500', 'NASDAQ', 'DAX', 'Nikkei', 'Nifty 50', 'Nifty Bank', 'SENSEX']);
const derived: GlobalIndex[] = [];
respMarketCards.forEach((mc: any) => {
if (indexNames.has(mc.title)) {
const price = Number(mc.price) || (typeof mc.display === 'number' ? mc.display : NaN);
const pct = mc.chgPct ?? (mc.chg && price ? (Number(mc.chg) / (price - Number(mc.chg))) * 100 : 0);
const gi: GlobalIndex = {
id: (mc.title || '').replace(/\s+/g, '_'),
name: mc.title,
country: mc.title === 'S&P 500' || mc.title === 'NASDAQ' ? 'United States' : mc.title === 'DAX' ? 'Germany' : mc.title === 'Nikkei' ? 'Japan' : 'India',
region: mc.title === 'S&P 500' || mc.title === 'NASDAQ' ? 'US' : mc.title === 'DAX' ? 'German' : mc.title === 'Nikkei' ? 'Japan' : 'India',
price: Number(isNaN(price) ? 0 : price),
change: Number(mc.chg ?? 0),
changePct: Number(pct || 0),
sparkline: this.series(Number(isNaN(price) ? Math.round(Number(mc.price) || 1000) : price), 26)
} as GlobalIndex;
gi.miniPath = this.sparkPath(gi.sparkline || []);
gi.isUp = (gi.changePct || 0) >= 0;
derived.push(gi);
}
});
// merge derived into mapped so rest of flow continues
if (derived.length) {
mapped.push(...derived);
}
}
// compute mini sparklines paths
mapped.forEach((g) => {
g.miniPath = this.sparkPath(g.sparkline || []);
g.isUp = (g.change || 0) >= 0;
});
// merge into cache (dedupe by id)
const byId = new Map<string, GlobalIndex>();
[...this.allGlobalIndices, ...mapped].forEach(i => byId.set(i.id, i));
this.allGlobalIndices = Array.from(byId.values());
// populate countries list for filter β€” merge live values but keep defaults if none
const uniq = Array.from(new Set(this.allGlobalIndices.map((g) => g.country).filter(Boolean)));
// always include common defaults so UI shows tabs even when backend data missing
const defaultCountries = ['India', 'US', 'Uk', 'German', 'Sweden', 'Russia'];
if (uniq.length) {
// Filter live countries: exclude those that are aliases of our defaults to avoid duplicates
const aliasesMap: Record<string, string[]> = this.countryAliases;
const isAliasOfDefault = (countryName: string) => {
if (!countryName) return false;
const lname = countryName.toLowerCase();
// check against every alias set; if any alias contains the live name, consider it a match
for (const key of Object.keys(aliasesMap)) {
const aliases = aliasesMap[key] || [];
for (const a of aliases) {
if (lname.includes(a)) return true;
}
}
return false;
};
const rest = uniq
.filter(c => c.toLowerCase() !== 'india')
.filter(c => !isAliasOfDefault(c))
.sort();
// merge defaults with any truly extra live countries
// filter out any unwanted country labels (e.g., Russia, Japan)
const hide = new Set(['russia', 'japan']);
const merged = [...defaultCountries, ...rest].filter(c => !hide.has((c || '').toLowerCase()));
this.countries = merged;
} else {
this.countries = [...defaultCountries];
}
// replace marketCards with live market overview if available in response root
if (Array.isArray(respMarketCards) && respMarketCards.length) {
this.marketCards = respMarketCards.map((m: any) => {
const display = (m.display ?? m.price);
const priceNum = Number(m.price ?? display ?? NaN);
// prefer explicit percent from backend, otherwise compute from point change and previous price
let pct = m.chgPct;
if (pct === undefined || pct === null || Number.isNaN(Number(pct))) {
const chgPoint = Number(m.chg || 0);
const prev = priceNum - chgPoint;
pct = (prev && !Number.isNaN(prev)) ? (chgPoint / prev) * 100 : 0;
}
const priceStr = (display === null || display === undefined) ? 'β€”' : (typeof display === 'number' ? display.toLocaleString(undefined, { maximumFractionDigits: 2 }) : String(display));
const chgNum = Number(pct || 0);
return {
title: m.title,
value: priceStr,
// show percent, not raw point change
chg: (chgNum >= 0 ? '+' : '') + chgNum.toFixed(2) + '%',
dir: chgNum > 0 ? 'up' : chgNum < 0 ? 'down' : 'neutral'
} as MarketCard;
});
console.log('marketCards set from resp.marketCards:', this.marketCards);
} else {
// fallback: derive a simple market overview from some well-known indices/currencies returned in "data"
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']);
const derived: MarketCard[] = [];
// if backend provided indices with matching names, map them
mapped.forEach((m: any) => {
if (pickTitles.has(m.name)) {
const priceStr = (m.price === null || m.price === undefined) ? 'β€”' : (typeof m.price === 'number' ? m.price.toLocaleString(undefined, { maximumFractionDigits: 2 }) : String(m.price));
const chgNum = Number(m.change ?? m.changePct ?? 0);
derived.push({
title: m.name,
value: priceStr,
chg: (chgNum >= 0 ? '+' : '') + chgNum.toFixed(2) + '%',
dir: chgNum > 0 ? 'up' : chgNum < 0 ? 'down' : 'neutral'
});
}
});
// if still empty, try to pick first few indices
if (derived.length === 0) {
mapped.slice(0, 12).forEach((m: any) => {
const priceStr = (m.price === null || m.price === undefined) ? 'β€”' : (typeof m.price === 'number' ? m.price.toLocaleString(undefined, { maximumFractionDigits: 2 }) : String(m.price));
const chgNum = Number(m.change ?? m.changePct ?? 0);
derived.push({
title: m.name,
value: priceStr,
chg: (chgNum >= 0 ? '+' : '') + chgNum.toFixed(2) + '%',
dir: chgNum > 0 ? 'up' : chgNum < 0 ? 'down' : 'neutral'
});
});
}
if (derived.length) {
this.marketCards = derived;
console.log('marketCards derived from indices:', this.marketCards);
}
}
// apply current selection so UI updates immediately with live data
this.applySelection(this.selectedCountry || 'India');
} catch (e) {
console.error('Error processing global indices', e);
}
},
error: (err: any) => {
console.warn('Failed to fetch global indices', err);
}
});
}
private mapToGlobalIndex(d: any): GlobalIndex {
return {
id: d.id || `${(d.name || '').replace(/\s+/g, '_')}_${Math.random().toString(36).slice(2, 8)}`,
name: d.name,
country: d.country,
region: d.region,
price: d.price,
change: d.change,
changePct: d.changePct,
sparkline: d.sparkline || [],
} as GlobalIndex;
}
// 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
private createUSIndices(): GlobalIndex[] {
const names = [
'S&P 500',
'Dow Jones',
'Nasdaq Composite',
'S&P MidCap 400',
'S&P SmallCap 600'
];
return names.map((name, i) => {
const base = Math.round(3000 + i * 200 + (Math.random() - 0.5) * 5000);
const change = Number(((Math.random() - 0.5) * 1.5).toFixed(2));
const changePct = Number(((change / Math.max(1, base)) * 100).toFixed(2));
const spark = this.series(base, 26);
const gi: GlobalIndex = {
id: `${name.replace(/\s+/g, '_')}_${i}`,
name,
country: 'United States',
region: 'US',
price: base,
change,
changePct,
sparkline: spark,
} as GlobalIndex;
gi.miniPath = this.sparkPath(gi.sparkline || []);
gi.isUp = (gi.change || 0) >= 0;
return gi;
});
}
// Create demo UK indices (FTSE family) so UI shows FTSE 100, FTSE 250, FTSE 350, FTSE Small Cap when 'Uk' selected
private createUKIndices(): GlobalIndex[] {
const names = [
'FTSE 100',
'FTSE 250',
'FTSE 350',
'FTSE Small Cap'
];
return names.map((name, i) => {
const base = Math.round(6000 + i * 100 + (Math.random() - 0.5) * 400);
const change = Number(((Math.random() - 0.5) * 1.2).toFixed(2));
const changePct = Number(((change / Math.max(1, base)) * 100).toFixed(2));
const spark = this.series(base, 26);
const gi: GlobalIndex = {
id: `${name.replace(/\s+/g, '_')}_${i}`,
name,
country: 'United Kingdom',
region: 'UK',
price: base,
change,
changePct,
sparkline: spark,
} as GlobalIndex;
gi.miniPath = this.sparkPath(gi.sparkline || []);
gi.isUp = (gi.change || 0) >= 0;
return gi;
});
}
// Create demo German indices (DAX family) so UI shows DAX, MDAX, TecDAX, SDAX when 'German' selected
private createGermanIndices(): GlobalIndex[] {
const names = [
'DAX',
'MDAX',
'TecDAX',
'SDAX'
];
return names.map((name, i) => {
const base = Math.round(10000 + i * 200 + (Math.random() - 0.5) * 800);
const change = Number(((Math.random() - 0.5) * 1.2).toFixed(2));
const changePct = Number(((change / Math.max(1, base)) * 100).toFixed(2));
const spark = this.series(base, 26);
const gi: GlobalIndex = {
id: `${name.replace(/\s+/g, '_')}_${i}`,
name,
country: 'Germany',
region: 'German',
price: base,
change,
changePct,
sparkline: spark,
} as GlobalIndex;
gi.miniPath = this.sparkPath(gi.sparkline || []);
gi.isUp = (gi.change || 0) >= 0;
return gi;
});
}
// Create demo Sweden indices so UI shows OMXS30 and STOXX Sweden when 'Sweden' selected
private createSwedenIndices(): GlobalIndex[] {
const names = [
'OMX Stockholm 30',
'STOXX Sweden'
];
return names.map((name, i) => {
const base = Math.round(1500 + i * 50 + (Math.random() - 0.5) * 200);
const change = Number(((Math.random() - 0.5) * 1.0).toFixed(2));
const changePct = Number(((change / Math.max(1, base)) * 100).toFixed(2));
const spark = this.series(base, 26);
const gi: GlobalIndex = {
id: `${name.replace(/\s+/g, '_')}_${i}`,
name,
country: 'Sweden',
region: 'Sweden',
price: base,
change,
changePct,
sparkline: spark,
} as GlobalIndex;
gi.miniPath = this.sparkPath(gi.sparkline || []);
gi.isUp = (gi.change || 0) >= 0;
return gi;
});
}
// Create demo Russia indices so UI shows MOEX Russia and RTS Index when 'Russia' selected
private createRussiaIndices(): GlobalIndex[] {
const names = ['MOEX Russia', 'RTS Index'];
return names.map((name, i) => {
const base = Math.round(2000 + i * 300 + (Math.random() - 0.5) * 800) * (name === 'MOEX Russia' ? 5 : 1);
const change = Number(((Math.random() - 0.5) * 1.2).toFixed(2));
const changePct = Number(((change / Math.max(1, base)) * 100).toFixed(2));
const spark = this.series(base, 26);
const gi: GlobalIndex = {
id: `${name.replace(/\s+/g, '_')}_${i}`,
name,
country: 'Russia',
region: 'Russia',
price: base,
change,
changePct,
sparkline: spark,
} as GlobalIndex;
gi.miniPath = this.sparkPath(gi.sparkline || []);
gi.isUp = (gi.change || 0) >= 0;
return gi;
});
}
// Helper to find a company object by symbol across all indices
private findCompanyBySymbol(sym: string): Company | undefined {
if (!sym) return undefined;
const lists = Object.values(this.companiesByIndex || {});
for (const list of lists) {
for (const c of list) {
if ((c.sym || '').toString() === sym.toString()) return c;
}
}
return undefined;
}
// Indices to display in template: indiaIndices when India selected, otherwise globalIndices set by applySelection
get displayIndices(): GlobalIndex[] {
const sel = (this.selectedCountry || '').toLowerCase().trim();
let list: GlobalIndex[] = [];
if (sel === 'india') list = this.indiaIndices || [];
else list = this.globalIndices || [];
// Deduplicate by name (case-insensitive) and limit visible cards to a sensible number (9)
const unique = this.uniqueByName(list);
// apply small display fixes: normalize long Dow Jones name and boost MOEX Russia demo value
const adjusted = unique.slice(0, 14).map(g => {
const copy = { ...g } as GlobalIndex;
if (typeof copy.name === 'string') {
// replace long backend label if present
copy.name = copy.name.replace(/Dow Jones Industrial Average/ig, 'Dow Jones');
}
// if MOEX Russia appears and looks small (likely demo), scale it up for visibility
if (typeof copy.name === 'string' && /moex\s*russia/i.test(copy.name)) {
// scale price but avoid absurdly large values: if price < 10000, multiply
if (typeof copy.price === 'number' && copy.price > 0 && copy.price < 10000) {
copy.price = Math.round(copy.price * 5);
}
}
return copy;
});
return adjusted.slice(0, 9);
}
// Fetch live markets from backend
private fetchLiveMarkets(): void {
this.market.getCompanies().pipe(take(1)).subscribe({
next: (data: any) => {
console.log('fetchLiveMarkets - getCompanies response:', data);
try {
if (data.indices && Array.isArray(data.indices) && data.indices.length) {
// map backend indices to IndexItem
this.indices = data.indices.map((it: any) => ({ code: it.code, price: it.price, chg: it.chg }));
this.indices.forEach((idx) => this.refreshIndexSpark(idx));
}
if (data.companiesByIndex) {
this.companiesByIndex = data.companiesByIndex;
// Immediately fetch live quotes for active index now that companies are loaded
try {
console.log('Companies loaded for indices, fetching live quotes for', this.activeIndex);
this.fetchLiveQuotesForActiveIndex();
} catch (e) {
console.warn('fetchLiveQuotesForActiveIndex failed shortly after companies load', e);
}
}
if (data.sectors) {
this.sectors = data.sectors;
}
if (data.lastUpdated) {
this.lastUpdated = data.lastUpdated;
}
// update derived lists and chart
this.updateGainersLosers();
const initial = this.companiesByIndex[this.activeIndex]?.[0];
if (initial) this.renderAreaChart(initial.sym, initial.ltp || 100);
// if backend provided marketCards in /getcompanies payload use them
const respMarketCards = data.marketCards ?? data.data?.marketCards ?? data.results?.marketCards ?? data.market_cards ?? data.cards ?? null;
if (Array.isArray(respMarketCards) && respMarketCards.length) {
console.log('fetchLiveMarkets: found marketCards in /getcompanies payload', respMarketCards);
// apply live marketCards immediately and cache them
const mapped = respMarketCards.map((m: any) => {
const display = m.display ?? m.price ?? m.value ?? m.last ?? m.close ?? null;
const value = this.formatPrice(display);
const pctRaw = m.chgPct ?? m.chg_pct ?? m.pct ?? m.changePct ?? m.change_percent ?? m.change ?? null;
const pct = (pctRaw !== undefined && pctRaw !== null) ? Number(pctRaw) : (Number(m.chg || 0) && typeof display === 'number' ? ((Number(m.chg) / (display - Number(m.chg))) * 100) : 0);
const n = Number(pct || 0);
return {
title: m.title ?? m.name ?? (m.symbol ?? 'β€”'),
value,
chg: isNaN(n) ? 'β€”' : ((n >= 0 ? '+' : '') + n.toFixed(2) + '%'),
dir: isNaN(n) ? 'neutral' : (n > 0 ? 'up' : n < 0 ? 'down' : 'neutral')
} as MarketCard;
});
this.marketCards = mapped;
try { localStorage.setItem('marketCardsCache', JSON.stringify(mapped)); } catch { /* ignore */ }
} else {
console.log('fetchLiveMarkets: no marketCards found in /getcompanies payload');
}
// persist companies snapshot so table shows immediately on reload
try {
const snapshot = { companiesByIndex: this.companiesByIndex, lastUpdated: this.lastUpdated };
localStorage.setItem(this.companiesCacheKey, JSON.stringify(snapshot));
} catch { /* ignore */ }
} catch (e) {
console.error('Error processing market data', e);
}
},
error: (err: any) => {
console.warn('Failed to fetch live markets', err);
}
});
}
// Fetch market overview cards from backend (uses /getmarketcards)
private fetchMarketCards(): void {
this.market.getMarketCards().pipe(take(1)).subscribe({
next: (cards: any[]) => {
console.log('fetchLiveMarkets - /getmarketcards response:', cards);
try {
if (cards && Array.isArray(cards) && cards.length) {
const mapped = cards.map((c: any) => {
const display = c.display ?? c.price ?? c.value ?? c.last ?? c.close ?? null;
const value = this.formatPrice(display);
const pctRaw = c.chgPct ?? c.chg_pct ?? c.pct ?? c.changePct ?? c.change_percent ?? c.change ?? null;
const pct = (pctRaw !== undefined && pctRaw !== null) ? Number(pctRaw) : (Number(c.chg || 0) && typeof display === 'number' ? ((Number(c.chg) / (display - Number(c.chg))) * 100) : 0);
const n = Number(pct || 0);
return {
title: c.title ?? c.name ?? (c.symbol ?? 'β€”'),
value,
chg: isNaN(n) ? 'β€”' : ((n >= 0 ? '+' : '') + n.toFixed(2) + '%'),
dir: isNaN(n) ? 'neutral' : (n > 0 ? 'up' : n < 0 ? 'down' : 'neutral')
} as MarketCard;
});
this.marketCards = mapped;
try { localStorage.setItem('marketCardsCache', JSON.stringify(mapped)); } catch { /* ignore */ }
}
} catch (e) {
console.error('Error processing market cards', e);
}
},
error: (err: any) => {
console.warn('Failed to fetch market cards', err);
}
});
}
// UI helpers
fmt(n: number): string {
return n.toFixed(2);
}
random(): number {
return Math.random();
}
// Format a price for display. Treat null/undefined/empty as missing and return 'β€”'./
private formatPrice(val: any): string {
if (val === null || val === undefined) return 'β€”';
if (typeof val === 'number') return val.toLocaleString(undefined, { maximumFractionDigits: 2 });
if (typeof val === 'string') return val.trim() === '' ? 'β€”' : val;
return String(val);
}
// Format index name for UI: remove bracketed suffixes like "(OMXS30)"
public formatIndexName(name: string | undefined | null): string {
if (!name) return '';
return String(name).replace(/\s*([^)]*)\)\s*/g, '').replace(/\s*\([^)]*\)\s*/g, '').trim();
}
public setActiveIndex(code: string): void {
this.activeIndex = code;
// fetch live quotes for newly active index immediately
this.fetchLiveQuotesForActiveIndex();
// update selected company to first of the newly active index
const first = this.companiesByIndex[this.activeIndex]?.[0];
if (first) {
this.selectedCompany = first.sym;
// draw initial chart for selected company (try live intraday)
this.fetchAndDrawChart(first.sym, first.ltp).catch(() => { /* ignore */ });
}
}
// Create demo UK companies per index
private createUKCompaniesByIndex(): Record<string, Company[]> {
return {
'FTSE 100': [
{ sym: 'HSBA', ltp: 520.10, chg: +0.8, high: 525.0, low: 515.0 },
{ sym: 'BP', ltp: 310.22, chg: -0.3, high: 315.0, low: 308.0 },
{ sym: 'GSK', ltp: 1380.50, chg: +0.2, high: 1395.0, low: 1370.0 },
{ sym: 'BARC', ltp: 190.44, chg: +1.1, high: 192.0, low: 188.0 },
{ sym: 'VOD', ltp: 120.60, chg: -0.6, high: 122.0, low: 119.0 },
],
'FTSE 250': [
{ sym: 'SMT', ltp: 45.12, chg: +0.4, high: 46.0, low: 44.0 },
{ sym: 'TPK', ltp: 8.34, chg: -0.2, high: 8.8, low: 8.0 },
]
};
}
private createUKSectors(): Sector[] {
return [
{ name: 'UK Financials', picks: ['HSBA', 'BARC', 'LLOY'] },
{ name: 'Energy', picks: ['BP', 'RDSA'] },
{ name: 'Telecom', picks: ['VOD', 'BT'] },
];
}
// Create demo German companies per index
private createGermanCompaniesByIndex(): Record<string, Company[]> {
return {
'DAX': [
{ sym: 'SAP', ltp: 110.12, chg: +0.5, high: 112.0, low: 109.0 },
{ sym: 'BMW', ltp: 85.34, chg: -0.4, high: 86.5, low: 84.0 },
{ sym: 'DAI', ltp: 75.22, chg: +0.7, high: 76.0, low: 74.0 },
{ sym: 'BAYN', ltp: 55.66, chg: -1.1, high: 57.0, low: 55.0 },
]
};
}
private createGermanSectors(): Sector[] {
return [
{ name: 'Automotive', picks: ['BMW', 'DAI', 'VOW3'] },
{ name: 'Software', picks: ['SAP', 'SOW'] },
];
}
// Create demo Sweden companies per index
private createSwedenCompaniesByIndex(): Record<string, Company[]> {
return {
'OMX Stockholm 30': [
{ sym: 'ERIC', ltp: 78.12, chg: +0.9, high: 79.5, low: 76.8 },
{ sym: 'SEB', ltp: 120.34, chg: -0.3, high: 121.8, low: 119.0 },
{ sym: 'VOLV', ltp: 150.44, chg: +0.4, high: 152.0, low: 149.0 },
]
};
}
private createSwedenSectors(): Sector[] {
return [
{ name: 'Industrial', picks: ['VOLV', 'ATCO'] },
{ name: 'Telecom', picks: ['ERIC'] },
];
}
// Create demo Russia companies per index
private createRussiaCompaniesByIndex(): Record<string, Company[]> {
return {
'MOEX Russia': [
{ sym: 'SBER', ltp: 170.12, chg: +1.2, high: 172.0, low: 168.0 },
{ sym: 'GAZP', ltp: 280.34, chg: -0.5, high: 285.0, low: 278.0 },
]
};
}
private createRussiaSectors(): Sector[] {
return [
{ name: 'Energy', picks: ['GAZP', 'LKOH'] },
{ name: 'Banks', picks: ['SBER', 'VTBR'] },
];
}
private applySelection(country: string): void {
const sel = (country || '').toLowerCase().trim();
if (!sel) {
this.globalIndices = [];
return;
}
if (sel === 'india') {
// Prefer prioritized live India indices; fallback to demo if none
const built = this.buildCountryIndices('india');
if (built && built.length) this.indiaIndices = built;
else if (!this.indiaIndices || this.indiaIndices.length === 0) this.populateDefaultIndiaIndices();
this.globalIndices = [];
// Restore India companies/sectors and reset active index so UI reflects India after switching back
this.companiesByIndex = this.createIndiaCompaniesByIndex();
this.sectors = [
{ name: 'IT & Software', picks: ['TCS', 'INFY', 'HCLTECH', 'TECHM'] },
{ name: 'Banking & Finance', picks: ['HDFCBANK', 'ICICIBANK', 'SBIN', 'AXISBANK'] },
{ name: 'Energy & Materials', picks: ['RELIANCE', 'ONGC', 'TATASTEEL', 'COALINDIA'] },
{ name: 'Auto', picks: ['TATAMOTORS', 'MARUTI', 'M&amp;M', 'TVSMOTOR'] },
{ name: 'FMCG', picks: ['ITC', 'HINDUNILVR', 'NESTLEIND', 'DABAR'] },
{ name: 'Telecom', picks: ['BHARTIARTL', 'VODAFONEIDE', 'TATACOMM', 'INDUSTOWER'] },
];
const firstKey = Object.keys(this.companiesByIndex)[0];
if (firstKey) {
this.activeIndex = firstKey;
const first = this.companiesByIndex[this.activeIndex]?.[0];
if (first) {
this.selectedCompany = first.sym;
this.renderAreaChart(first.sym, first.ltp || 100);
}
this.fetchLiveQuotesForActiveIndex().catch(() => { /* ignore */ });
}
// update derived lists
this.updateGainersLosers();
return;
}
// try to find live entries in cache
const matched = (this.allGlobalIndices || []).filter((g) => this.countryMatches(sel, (g.country || '').toLowerCase()) && (g.region || '').toLowerCase() !== 'india');
if (matched && matched.length) {
this.globalIndices = matched;
// Even when we have live index entries we may still want to populate demo companies/sectors
// for certain countries so the "Companies by Index", sector picks and toppers/losers update.
if (sel === 'us') {
this.companiesByIndex = this.createUSCompaniesByIndex();
this.sectors = this.createUSSectors();
const firstKey = Object.keys(this.companiesByIndex)[0];
if (firstKey) {
this.activeIndex = firstKey;
const first = this.companiesByIndex[this.activeIndex]?.[0];
if (first) {
this.selectedCompany = first.sym;
this.renderAreaChart(first.sym, first.ltp || 100);
}
this.fetchLiveQuotesForActiveIndex().catch(() => { /* ignore */ });
}
this.updateGainersLosers();
} else if (sel === 'uk') {
this.companiesByIndex = this.createUKCompaniesByIndex();
this.sectors = this.createUKSectors();
const firstKey = Object.keys(this.companiesByIndex)[0];
if (firstKey) {
this.activeIndex = firstKey;
const first = this.companiesByIndex[this.activeIndex]?.[0];
if (first) {
this.selectedCompany = first.sym;
this.renderAreaChart(first.sym, first.ltp || 100);
}
this.fetchLiveQuotesForActiveIndex().catch(() => { /* ignore */ });
}
this.updateGainersLosers();
} else if (sel === 'german' || sel === 'germany') {
this.companiesByIndex = this.createGermanCompaniesByIndex();
this.sectors = this.createGermanSectors();
const firstKey = Object.keys(this.companiesByIndex)[0];
if (firstKey) {
this.activeIndex = firstKey;
const first = this.companiesByIndex[this.activeIndex]?.[0];
if (first) {
this.selectedCompany = first.sym;
this.renderAreaChart(first.sym, first.ltp || 100);
}
this.fetchLiveQuotesForActiveIndex().catch(() => { /* ignore */ });
}
this.updateGainersLosers();
} else if (sel === 'sweden') {
this.companiesByIndex = this.createSwedenCompaniesByIndex();
this.sectors = this.createSwedenSectors();
const firstKey = Object.keys(this.companiesByIndex)[0];
if (firstKey) {
this.activeIndex = firstKey;
const first = this.companiesByIndex[this.activeIndex]?.[0];
if (first) {
this.selectedCompany = first.sym;
this.renderAreaChart(first.sym, first.ltp || 100);
}
this.fetchLiveQuotesForActiveIndex().catch(() => { /* ignore */ });
}
this.updateGainersLosers();
} else if (sel === 'russia') {
this.companiesByIndex = this.createRussiaCompaniesByIndex();
this.sectors = this.createRussiaSectors();
const firstKey = Object.keys(this.companiesByIndex)[0];
if (firstKey) {
this.activeIndex = firstKey;
const first = this.companiesByIndex[this.activeIndex]?.[0];
if (first) {
this.selectedCompany = first.sym;
this.renderAreaChart(first.sym, first.ltp || 100);
}
this.fetchLiveQuotesForActiveIndex().catch(() => { /* ignore */ });
}
this.updateGainersLosers();
}
// For live data we don't have a mapping of companies per index; keep existing companiesByIndex
return;
}
// no live data for this country β€” create demo indices for known countries so UI shows names immediately
let demo: GlobalIndex[] = [];
if (sel === 'us') demo = this.createUSIndices();
else if (sel === 'uk') demo = this.createUKIndices();
else if (sel === 'german') demo = this.createGermanIndices();
else if (sel === 'sweden') demo = this.createSwedenIndices();
else if (sel === 'russia') demo = this.createRussiaIndices();
this.globalIndices = demo;
// merge demo into cache to improve subsequent lookups
if (demo && demo.length) {
const byId = new Map<string, GlobalIndex>();
[...this.allGlobalIndices, ...demo].forEach(i => byId.set(i.id, i));
this.allGlobalIndices = Array.from(byId.values());
}
// --- NEW: populate demo companies & sectors for this country so related cards update ---
if (sel === 'us') {
this.companiesByIndex = this.createUSCompaniesByIndex();
this.sectors = this.createUSSectors();
// reset active index to first available and refresh chart/quotes
const firstKey = Object.keys(this.companiesByIndex)[0];
if (firstKey) {
this.activeIndex = firstKey;
const first = this.companiesByIndex[this.activeIndex]?.[0];
if (first) {
this.selectedCompany = first.sym;
this.renderAreaChart(first.sym, first.ltp || 100);
}
// try to fetch live quotes for US symbols (backend may support them)
this.fetchLiveQuotesForActiveIndex().catch(() => { /* ignore */ });
}
// update gainers/losers immediately from demo data
this.updateGainersLosers();
} else if (sel === 'uk') {
this.companiesByIndex = this.createUKCompaniesByIndex();
this.sectors = this.createUKSectors();
const firstKey = Object.keys(this.companiesByIndex)[0];
if (firstKey) {
this.activeIndex = firstKey;
const first = this.companiesByIndex[this.activeIndex]?.[0];
if (first) {
this.selectedCompany = first.sym;
this.renderAreaChart(first.sym, first.ltp || 100);
}
this.fetchLiveQuotesForActiveIndex().catch(() => { /* ignore */ });
}
this.updateGainersLosers();
} else if (sel === 'german' || sel === 'germany') {
this.companiesByIndex = this.createGermanCompaniesByIndex();
this.sectors = this.createGermanSectors();
const firstKey = Object.keys(this.companiesByIndex)[0];
if (firstKey) {
this.activeIndex = firstKey;
const first = this.companiesByIndex[this.activeIndex]?.[0];
if (first) {
this.selectedCompany = first.sym;
this.renderAreaChart(first.sym, first.ltp || 100);
}
this.fetchLiveQuotesForActiveIndex().catch(() => { /* ignore */ });
}
this.updateGainersLosers();
} else if (sel === 'sweden') {
this.companiesByIndex = this.createSwedenCompaniesByIndex();
this.sectors = this.createSwedenSectors();
const firstKey = Object.keys(this.companiesByIndex)[0];
if (firstKey) {
this.activeIndex = firstKey;
const first = this.companiesByIndex[this.activeIndex]?.[0];
if (first) {
this.selectedCompany = first.sym;
this.renderAreaChart(first.sym, first.ltp || 100);
}
this.fetchLiveQuotesForActiveIndex().catch(() => { /* ignore */ });
}
this.updateGainersLosers();
} else if (sel === 'russia') {
this.companiesByIndex = this.createRussiaCompaniesByIndex();
this.sectors = this.createRussiaSectors();
const firstKey = Object.keys(this.companiesByIndex)[0];
if (firstKey) {
this.activeIndex = firstKey;
const first = this.companiesByIndex[this.activeIndex]?.[0];
if (first) {
this.selectedCompany = first.sym;
this.renderAreaChart(first.sym, first.ltp || 100);
}
this.fetchLiveQuotesForActiveIndex().catch(() => { /* ignore */ });
}
this.updateGainersLosers();
}
}
// New: load full constituents from backend for active index (uses /getcompanies?code=)
public async loadAllConstituents(): Promise<void> {
try {
const code = this.activeIndex || this.indexCodes[0];
if (!code) return;
const resp: any = await lastValueFrom(this.market.getConstituents(code));
if (!resp) return;
// backend payload uses 'constituents' array of {symbol, company}
const rows = resp.constituents || resp.results || resp.data || [];
if (!Array.isArray(rows) || rows.length === 0) return;
// convert to Company[] using yfinance symbol mapping for India (append .NS)
const companies: Company[] = rows.map((r: any) => {
const s = (r.symbol || r.Symbol || r.symbol || '').toString().trim();
const sym = s || (r.company || r.Company || '').toString().trim();
// turn into NSE ticker if needed
const isIndia = (code || '').toLowerCase().includes('nifty') || (code || '').toLowerCase().includes('sensex') || (this.selectedCountry || '').toLowerCase() === 'india';
let symT = sym;
if (isIndia && sym && !sym.includes('.') && !sym.includes(':')) symT = `${sym}.NS`;
return { sym: symT.replace('.NS', ''), ltp: 0, chg: 0, high: 0, low: 0 } as Company;
});
// store under active code name (keep original key format)
this.companiesByIndex[code] = companies;
// fetch live quotes for new list
await this.fetchLiveQuotesForActiveIndex();
this.updateGainersLosers();
} catch (e) {
console.warn('loadAllConstituents failed', e);
}
}
// Ensure selectedCompany is valid for current activeIndex and draw chart synchronously
private ensureSelectedCompanyAndDraw(): void {
try {
const keys = Object.keys(this.companiesByIndex || {});
if (keys.length === 0) return;
if (!keys.includes(this.activeIndex)) this.activeIndex = keys[0];
const first = this.companiesByIndex[this.activeIndex]?.[0];
if (first) {
this.selectedCompany = first.sym;
this.chartSymbol = first.sym;
// Draw chart synchronously to ensure UI reflects selection immediately
// Use a short timeout so that template updates (table) have rendered
setTimeout(() => {
this.renderAreaChart(first.sym, first.ltp);
}, 20);
}
} catch (e) {
// ignore
}
}
// Fetch live quotes for companies in the active index and update table
private async fetchLiveQuotesForActiveIndex(): Promise<void> {
// show spinner in table
this.quotesLoading = true;
try {
const tickers = this.buildTickersForIndex(this.activeIndex || '');
if (!tickers || tickers.length === 0) {
this.quotesLoading = false;
return;
}
// helper: sleep
const sleep = (ms: number) => new Promise(res => setTimeout(res, ms));
// helper: fetch with retries + exponential backoff
const fetchWithRetries = async (batch: string[], maxRetries = 3, baseDelay = 500): Promise<any[] | null> => {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
const obs = this.market.getQuotes(batch);
const res = await lastValueFrom(obs);
// treat empty result as failure (backend may return [] on error)
if (Array.isArray(res) && res.length > 0) return res;
// if got empty but this was final attempt, consider failure
} catch (err) {
// fall through to retry
console.warn('fetchWithRetry attempts Gi failed', err);
}
const delay = baseDelay * Math.pow(2, attempt);
await sleep(delay);
}
return null;
};
// batch tickers to avoid very long query strings; adjust size as needed
const batchSize = 10;
const batches: string[][] = [];
for (let i = 0; i < tickers.length; i += batchSize) {
batches.push(tickers.slice(i, i + batchSize));
}
const allQuotes: any[] = [];
for (const b of batches) {
const batchRes = await fetchWithRetries(b, 3, 400);
if (batchRes === null) {
// At least one batch failed after retries – fallback to demo ticks
console.warn('Quotes batch failed after retries, falling back to demo ticks');
this.quotesLoading = false;
// Apply one demo tick step immediately so UI updates
this.tickCompanies();
this.updateGainersLosers();
return;
}
allQuotes.push(...batchRes);
}
// If we reach here, we have fetched quotes for all batches
const list = this.companiesByIndex[this.activeIndex] || [];
const byReq = new Map<string, any>();
allQuotes.forEach((q: any) => {
if (!q) return;
const s = (q.symbol || '').toString();
byReq.set(s.toUpperCase(), q);
});
list.forEach((c) => {
const reqSym = (this.selectedCountry.toLowerCase() === 'india' && !c.sym.includes('.') && !c.sym.includes(':')) ? `${c.sym}.NS`.toUpperCase() : c.sym.toUpperCase();
const q = byReq.get(reqSym) || byReq.get(c.sym.toUpperCase());
if (!q) return;
if (q.price !== undefined && q.price !== null) c.ltp = Number(q.price);
// backend provides chgPct as percentage; use it if present
if (q.chgPct !== undefined && q.chgPct !== null) c.chg = Number(q.chgPct);
else if (q.chg !== undefined && q.chg !== null && c.ltp) {
// approximate percent from absolute change
const prev = c.ltp - Number(q.chg);
if (prev && !Number.isNaN(prev)) c.chg = (Number(q.chg) / prev) * 100;
}
if (q.high !== undefined && q.high !== null) c.high = Number(q.high);
if (q.low !== undefined && q.low !== null) c.low = Number(q.low);
});
this.updateGainersLosers();
this.lastUpdated = new Date().toLocaleString();
// persist updated quotes so reload shows latest values immediately
try {
const snapshot = { companiesByIndex: this.companiesByIndex, lastUpdated: this.lastUpdated };
localStorage.setItem(this.companiesCacheKey, JSON.stringify(snapshot));
} catch { /* ignore */ }
} catch (e) {
console.warn('fetchLiveQuotesForActiveIndex unexpected error', e);
// fallback to demo ticks
this.tickCompanies();
this.updateGainersLosers();
} finally {
this.quotesLoading = false;
}
}
// Build yfinance-compatible tickers for a company symbol depending on country
private buildTickersForIndex(indexCode: string): string[] {
const list = this.companiesByIndex[indexCode] || [];
const isIndia = (indexCode || '').toLowerCase().includes('nifty') || (indexCode || '').toLowerCase().includes('sensex') || (this.selectedCountry || '').toLowerCase() === 'india';
return list.map(c => {
const sym = (c.sym || '').trim();
if (!sym) return '';
// For India, append NSE suffix if not already present
if (isIndia && !sym.includes('.') && !sym.includes(':')) return `${sym}.NS`;
return sym;
}).filter(Boolean);
}
// Create demo India indices with sparklines and miniPath for UI when live data not available
private populateDefaultIndiaIndices(): void {
const now = Date.now();
const items: GlobalIndex[] = this.defaultIndiaIndexNames.map((name: string, i: number) => {
const base = Math.round(20000 + i * 1000 + (Math.random() - 0.5) * 2000);
const change = Number(((Math.random() - 0.5) * 1.5).toFixed(2));
const changePct = Number((change / base * 100).toFixed(2));
const spark = this.series(base, 26);
const gi: GlobalIndex = {
id: `${name.replace(/\s+/g, '_')}_${i}`,
name,
country: 'India',
region: 'India',
price: base,
change,
changePct,
sparkline: spark,
} as GlobalIndex;
gi.miniPath = this.sparkPath(gi.sparkline || []);
gi.isUp = (gi.change || 0) >= 0;
return gi;
});
this.indiaIndices = items;
// also ensure cache contains these so other logic can find India entries
const byId = new Map<string, GlobalIndex>();
[...this.allGlobalIndices, ...items].forEach(i => byId.set(i.id, i));
this.allGlobalIndices = Array.from(byId.values());
}
private countryMatches(selected: string, actual: string): boolean {
if (!selected || !actual) return false;
if (selected === actual) return true;
// check aliases for selected label
const aliases = this.countryAliases[selected];
if (aliases) {
for (const a of aliases) {
if (actual.includes(a)) return true;
}
}
// check if actual has a known alias that matches selected
const key = Object.keys(this.countryAliases).find(k => this.countryAliases[k].some((a: string) => actual.includes(a)));
if (key && key === selected) return true;
// fallback: partial match
return actual.includes(selected) || selected.includes(actual);
}
// Clock
private setClock(): void {
this.lastUpdated = new Date().toLocaleString();
}
// Indices sparkline generation
private series(base: number, pts = 26): number[] {
const arr = [base];
for (let i = 1; i < pts; i++) {
const step = (Math.random() - 0.5) * (base * 0.002); // Β±0.2%
arr.push(Math.max(1, arr[i - 1] + step));
}
return arr;
}
private sparkPath(values: number[], w = 240, h = 28, pad = 2): string {
if (!values || values.length === 0) return '';
const max = Math.max(...values);
const min = Math.min(...values);
const norm = (v: number) => h - pad - ((v - min) / Math.max(1e-6, max - min)) * (h - 2 * pad);
const step = (w - 2 * pad) / (values.length - 1 || 1);
let d = '';
values.forEach((v, i) => {
const x = pad + i * step;
const y = norm(v);
d += i === 0 ? `M ${x} ${y}` : ` L ${x} ${y}`;
});
return d;
}
private refreshIndexSpark(idx: IndexItem): void {
const vals = this.series(idx.price, 26);
idx.isUp = vals[vals.length - 1] >= vals[0];
idx.miniPath = this.sparkPath(vals);
}
private tickIndices(): void {
this.indices.forEach((idx) => {
const delta = (Math.random() - 0.5) * (idx.price * 0.0008);
idx.price = Math.max(1, idx.price + delta);
idx.chg += (Math.random() - 0.5) * 0.04; // drift
this.refreshIndexSpark(idx);
});
}
// Companies random walk tick
private tickCompanies(): void {
Object.values(this.companiesByIndex).forEach((list) => {
list.forEach((c) => {
const base = c.ltp;
const move = (Math.random() - 0.5) * (base * 0.0025); // Β±0.25%
c.ltp = Math.max(1, base + move);
const ref = base / (1 + c.chg / 100);
c.chg = ((c.ltp - ref) / ref) * 100;
c.high = Math.max(c.high, c.ltp);
c.low = Math.min(c.low, c.ltp);
});
});
}
private flattenCompanies(): Company[] {
return Object.values(this.companiesByIndex).flat().map((c) => ({ ...c }));
}
private updateGainersLosers(): void {
const all = this.flattenCompanies();
const sorted = [...all].sort((a, b) => b.chg - a.chg);
this.gainers = sorted.slice(0, 5);
this.losers = sorted.slice(-5).reverse();
}
// Chart
private makeIntraday(base = 100, points = 90): number[] {
const arr = [base];
for (let i = 1; i < points; i++) {
const step = (Math.random() - 0.5) * (base * 0.004);
arr.push(Math.max(1, arr[i - 1] + step));
}
return arr;
}
// Compatibility wrapper for template calls β€” delegate to SVG renderer
public drawTodayChart(symbol = 'β€”', base = 100): void {
this.renderAreaChart(symbol, base);
}
// Render an SVG area chart into the chart container. If `series` is provided it is used
// as the plotted points (array of numbers). Otherwise a simulated series is generated.
private renderAreaChart(symbol: string, base = 100, series?: number[]): void {
const container = this.chartContainerRef?.nativeElement;
if (!container) return;
// ensure chartSymbol reflects what we're drawing
this.chartSymbol = symbol;
// clear
container.innerHTML = '';
// use device pixel width for responsiveness
const rect = container.getBoundingClientRect();
const width = Math.max(600, Math.round(rect.width || 700));
const height = 260;
const pts = series && series.length ? series : this.makeIntraday(base, 90);
const padLeft = 40;
const padRight = 40;
const padTop = 16;
const padBottom = 36;
const min = Math.min(...pts);
const max = Math.max(...pts);
const toX = (i: number) => padLeft + (i * (width - padLeft - padRight)) / (pts.length - 1 || 1);
const toY = (v: number) => padTop + ((max - v) / Math.max(1e-6, max - min)) * (height - padTop - padBottom);
// SVG root
const ns = 'http://www.w3.org/2000/svg';
const svg = document.createElementNS(ns, 'svg');
svg.setAttribute('width', String(width));
svg.setAttribute('height', String(height));
svg.setAttribute('viewBox', `0 0 ${width} ${height}`);
svg.style.width = '100%';
svg.style.height = '260px';
svg.style.display = 'block';
// grid lines
for (let g = 0; g < 4; g++) {
const y = padTop + (g * (height - padTop - padBottom)) / 3;
const line = document.createElementNS(ns, 'line');
line.setAttribute('x1', String(padLeft));
line.setAttribute('y1', String(y));
line.setAttribute('x2', String(width - padRight));
line.setAttribute('y2', String(y));
line.setAttribute('stroke', '#e6ecff');
line.setAttribute('stroke-opacity', '0.06');
line.setAttribute('stroke-width', '1');
svg.appendChild(line);
}
// build path string
let d = '';
let area = '';
pts.forEach((v, i) => {
const x = toX(i);
const y = toY(v);
if (i === 0) {
d += `M ${x} ${y}`;
area += `M ${x} ${y}`;
} else {
d += ` L ${x} ${y}`;
area += ` L ${x} ${y}`;
}
});
// close area to baseline
const lastX = toX(pts.length - 1);
area += ` L ${lastX} ${height - padBottom} L ${padLeft} ${height - padBottom} Z`;
// area fill
const areaPath = document.createElementNS(ns, 'path');
areaPath.setAttribute('d', area);
areaPath.setAttribute('fill', '#0f9d58');
areaPath.setAttribute('fill-opacity', '0.08');
svg.appendChild(areaPath);
// line path
const path = document.createElementNS(ns, 'path');
path.setAttribute('d', d);
path.setAttribute('fill', 'none');
path.setAttribute('stroke', '#0f9d58');
path.setAttribute('stroke-width', '2');
path.setAttribute('stroke-linejoin', 'round');
path.setAttribute('stroke-linecap', 'round');
svg.appendChild(path);
// last point marker
const lastYVal = pts[pts.length - 1];
const lastCx = toX(pts.length - 1);
const lastCy = toY(lastYVal);
const circ = document.createElementNS(ns, 'circle');
circ.setAttribute('cx', String(lastCx));
circ.setAttribute('cy', String(lastCy));
circ.setAttribute('r', '4');
circ.setAttribute('fill', '#0f9d58');
svg.appendChild(circ);
// y-axis labels (right side)
const labelRight = (val: number, y: number) => {
const txt = document.createElementNS(ns, 'text');
txt.setAttribute('x', String(width - padRight + 8));
txt.setAttribute('y', String(y + 4));
txt.setAttribute('fill', '#fff');
txt.setAttribute('font-size', '12');
txt.setAttribute('opacity', '0.9');
txt.textContent = val.toFixed(2);
svg.appendChild(txt);
};
labelRight(max, toY(max));
labelRight(min, toY(min));
// x-axis labels (bottom)
const ticks = [0, Math.floor(pts.length / 4), Math.floor(pts.length / 2), Math.floor((3 * pts.length) / 4), pts.length - 1];
ticks.forEach((ti) => {
const x = toX(ti);
const txt = document.createElementNS(ns, 'text');
txt.setAttribute('x', String(x));
txt.setAttribute('y', String(height - 8));
txt.setAttribute('fill', '#999');
txt.setAttribute('font-size', '11');
txt.setAttribute('text-anchor', 'middle');
// fake times: show HH:MM using index
const minutes = (ti * 5) % 60;
const hour = 9 + Math.floor(ti / 12);
txt.textContent = `${hour}:${String(minutes).padStart(2, '0')} AM`;
svg.appendChild(txt);
});
container.appendChild(svg);
// expose small tooltip on hover (basic)
svg.addEventListener('mousemove', (ev) => {
const rect = svg.getBoundingClientRect();
const x = ev.clientX - rect.left;
// find closest index
let closest = 0;
let bestDist = Infinity;
for (let i = 0; i < pts.length; i++) {
const dx = Math.abs(toX(i) - x);
if (dx < bestDist) { bestDist = dx; closest = i; }
}
// simple tooltip: set title on container
const v = pts[closest];
svg.setAttribute('title', `Index: ${symbol}\nValue: ${v.toFixed(2)}`);
});
}
// Fetch intraday series for symbol and draw chart. Falls back to simulated when live data present
private async fetchAndDrawChart(sym: string, base = 100): Promise<void> {
try {
// try backend intraday endpoint (via MarketService.getIntraday)
const obs = this.market.getIntraday(sym, '1d', '1m');
const resp: any = await lastValueFrom(obs);
// expected resp: { symbol, timestamps: string[], closes: number[] }
if (resp && Array.isArray(resp.closes) && resp.closes.length > 0) {
this.chartSymbol = sym;
this.drawSeriesChart(resp.closes, sym);
return;
}
} catch (e) {
// ignore and fallback
}
// fallback: simulate intraday
this.chartSymbol = sym;
this.renderAreaChart(sym, base);
}
// Draw given numeric series on canvas (replaces simulated draw when live data present)
private drawSeriesChart(values: number[], symbol = 'β€”'): void {
this.chartSymbol = symbol;
const canvas = this.canvasRef?.nativeElement;
if (!canvas) return;
const ctx = canvas.getContext('2d');
if (!ctx) return;
const w = canvas.width;
const h = canvas.height;
ctx.clearRect(0, 0, w, h);
const min = Math.min(...values);
const max = Math.max(...values);
const padX = 28;
const padY = 16;
const toX = (i: number) => padX + (i * (w - padX * 2)) / (values.length - 1 || 1);
const toY = (v: number) => h - padY - ((v - min) / Math.max(1e-6, max - min)) * (h - padY * 2);
// grid
ctx.globalAlpha = 0.4;
ctx.strokeStyle = 'rgba(255,255,255,0.08)';
ctx.lineWidth = 1;
for (let g = 0; g < 4; g++) {
const gy = padY + (g * (h - padY * 2)) / 3;
ctx.beginPath();
ctx.moveTo(padX, gy);
ctx.lineTo(w - padX, gy);
ctx.stroke();
}
ctx.globalAlpha = 1;
const up = values[values.length - 1] >= values[0];
const styles = getComputedStyle(this.host.nativeElement);
const stroke = styles.getPropertyValue(up ? '--up' : '--down').trim() || (up ? '#12c48b' : '#ff5b6b');
ctx.strokeStyle = stroke;
ctx.lineWidth = 2;
ctx.beginPath();
values.forEach((v, i) => {
const x = toX(i);
const y = toY(v);
ctx.lineTo(x, y);
});
ctx.stroke();
// last price marker
const lastX = toX(values.length - 1);
const lastY = toY(values[values.length - 1]);
ctx.fillStyle = stroke;
ctx.beginPath();
ctx.arc(lastX, lastY, 3, 0, Math.PI * 2);
ctx.fill();
}
public selectCountry(country: string): void {
this.selectedCountry = country;
// always apply selection immediately so UI updates synchronously
this.applySelection(country);
// if we haven't fetched live data yet, fetch (fetchGlobalIndices will re-apply selection once data arrives)
if (!this.globalFetched) this.fetchGlobalIndices();
// Ensure activeIndex points to a valid index for the newly selected companies list
const keys = Object.keys(this.companiesByIndex || {});
if (keys.length) {
if (!keys.includes(this.activeIndex)) {
this.activeIndex = keys[0];
}
const first = this.companiesByIndex[this.activeIndex]?.[0];
if (first) {
this.selectedCompany = first.sym;
this.chartSymbol = first.sym;
// draw chart for the first company (prefer live intraday)
this.fetchAndDrawChart(first.sym, first.ltp).catch(() => {
this.renderAreaChart(first.sym, first.ltp);
});
return;
}
}
// If no company available for activeIndex, try to keep current selectedCompany if present
if (this.selectedCompany) {
const comp = this.findCompanyBySymbol(this.selectedCompany);
const base = comp?.ltp ?? 100;
this.chartSymbol = this.selectedCompany;
this.fetchAndDrawChart(this.selectedCompany, base).catch(() => {
this.renderAreaChart(this.selectedCompany as string, base);
});
}
}
// expose as a public property (arrow) so Angular template type-checker recognizes it
// (removed duplicate - use the method defined later in file)
// Handle company row / button clicks from template
public onCompanyClick(e: Event, c: Company): void {
if (e && typeof e.preventDefault === 'function') e.preventDefault();
if (!c) return;
this.selectedCompany = c.sym;
// try to fetch live intraday series and draw; fallback to simulated when not available
this.fetchAndDrawChart(c.sym, c.ltp).catch(() => {
this.renderAreaChart(c.sym, c.ltp);
});
}
}