import { EmptyState, SectionHeading, usePaneNoticeFooter } from "../../../components";
import { CompositeChart, pricePointsToResolvedSeries } from "../../../components/chart/composite";
import { CompanyLogo } from "../../../components/company-logo";
import { PriceReturnStrip } from "../../../components/price-performance";
import { t } from "../../../i18n";
import { useFxRatesMap } from "../../../market-data/hooks";
import { formatMarketPriceWithCurrency, formatSignedMarketPrice, liveQuoteFormatOptions } from "../../../market-data/market/format";
import { exchangeShortName, marketStateColor, marketStateLabel } from "../../../market-data/market/status";
import { appendQuoteToPriceReturnHistory, buildPriceReturnFields } from "../../../market-data/performance";
import { useViewport } from "../../../react/input";
import { useAppSelector } from "../../../state/app/context";
import { colors, priceColor } from "../../../theme/colors";
import { appendLiveQuotePoint, hasUnknownBondHistoryBasis } from "../../../time-series/chart-data";
import type { TickerFinancials } from "../../../types/financials";
import type { TickerRecord } from "../../../types/ticker";
import { Box, ScrollBox, Text, TextAttributes, useUiCapabilities } from "../../../ui";
import { selectEffectiveExchangeRates } from "../../../utils/exchange-rate-map";
import { resolveExchangeTimeZone } from "../../../utils/exchanges";
import { convertCurrency, displayWidth, formatPercentRaw, truncateToDisplayWidth } from "../../../utils/format";
import { CompactRangeBar, FundamentalsGrid, PositionTable, QuoteBook } from "./overview/components";
import { buildOverviewStats, buildPositionRows } from "./overview/model";
import { describeFundamentalMarketCap } from "../../../utils/market-capitalization";
import { liveFiftyTwoWeekRange, liveMarketCapitalization } from "../portfolio-list/live-valuation";
interface OverviewTabProps {
width?: number;
focused?: boolean;
ticker: TickerRecord | null;
financials: TickerFinancials | null;
/** A click on the price chart opens the full chart. */
onOpenChart?: () => void;
}
export function OverviewTab(props: OverviewTabProps) {
if (!props.ticker) return ;
return ;
}
function ResolvedOverviewTab({ width, focused = false, ticker, financials, onOpenChart }: OverviewTabProps & { ticker: TickerRecord }) {
const baseCurrency = useAppSelector((state) => state.config.baseCurrency);
const exchangeRatesState = useAppSelector((state) => state.exchangeRates);
const { width: termWidth } = useViewport();
const { fractionalViewport = false, nativePaneChrome } = useUiCapabilities();
const quote = financials?.quote;
const fundamentals = financials?.fundamentals;
const capitalization = liveMarketCapitalization(quote, fundamentals);
const profile = financials?.profile;
const instrumentType = quote?.instrumentType?.trim()
|| financials?.quoteMetadata?.instrumentType?.trim()
|| ticker.metadata.assetCategory;
const exchangeRates = useFxRatesMap([
baseCurrency,
ticker.metadata.currency,
quote?.currency,
capitalization?.currency,
...ticker.metadata.positions.map((position) => position.currency),
]);
const effectiveExchangeRates = selectEffectiveExchangeRates(exchangeRates, exchangeRatesState);
const quoteCurrency = quote?.currency ?? ticker.metadata.currency ?? baseCurrency;
const toBase = (value: number, fromCurrency: string) =>
convertCurrency(value, fromCurrency, baseCurrency, effectiveExchangeRates);
const sector = ticker.metadata.sector ?? profile?.sector;
const industry = ticker.metadata.industry ?? profile?.industry;
const description = profile?.description?.trim();
const listingVenue = exchangeShortName(
quote?.listingExchangeName ?? quote?.exchangeName,
quote?.listingExchangeFullName ?? quote?.fullExchangeName,
);
const routingVenue = exchangeShortName(quote?.routingExchangeName, quote?.routingExchangeFullName);
const metadataParts = [
routingVenue && routingVenue !== listingVenue ? `route ${routingVenue}` : "",
].filter((part) => part.length > 0);
const contentWidth = Math.max((width || Math.floor(termWidth * 0.5)) - (fractionalViewport ? 2 : 4), 20);
const chartWidth = contentWidth;
const hasHistory = (financials?.priceHistory?.length ?? 0) > 2;
const unknownHistoryBasis = hasUnknownBondHistoryBasis(quote, ticker.metadata.assetCategory, financials?.quoteMetadata?.instrumentType);
const historyQuote = unknownHistoryBasis ? undefined : quote;
const chartHistory = appendLiveQuotePoint(financials?.priceHistory ?? [], historyQuote);
const chartDelta = (chartHistory.at(-1)?.close ?? 0) - (chartHistory[0]?.close ?? 0);
const chartTimeZone = resolveExchangeTimeZone(
ticker.metadata.exchange || quote?.listingExchangeName || quote?.exchangeName,
);
const priceSeries = pricePointsToResolvedSeries(chartHistory, {
id: `${ticker.metadata.ticker}:price`,
label: `${ticker.metadata.ticker} Price`,
color: priceColor(chartDelta),
unit: unknownHistoryBasis ? "unknown" : quoteCurrency,
style: "area",
axis: "right",
panelId: "price",
timeBasis: chartTimeZone ? { kind: "market", timeZone: chartTimeZone } : undefined,
});
usePaneNoticeFooter({
registrationId: "overview-notices",
notices: [
...(!quote && financials ? [t("Current quote unavailable. Other research data is still available.")] : []),
...(priceSeries.warning ? [priceSeries.warning] : []),
...(fundamentals?.unavailableFields?.includes("enterpriseValue")
? [t("Enterprise value unavailable: the source observation failed validation.")] : []),
...(capitalization?.provenance.kind === "fundamentals" && !capitalization.live
? [`Market cap: ${describeFundamentalMarketCap(capitalization.provenance)}.`] : []),
],
focused,
});
const hasBidAsk = quote?.bid != null || quote?.ask != null;
const quoteBookInline = hasBidAsk && contentWidth >= 68;
const quoteBookWidth = quoteBookInline ? Math.min(32, Math.max(24, Math.floor(contentWidth * 0.3))) : Math.min(contentWidth, 32);
const quoteSummaryWidth = quoteBookInline ? Math.max(20, contentWidth - quoteBookWidth - 2) : contentWidth;
// Price, change and ranges keep the instrument's decimals on every streamed tick.
const moneyOptions = liveQuoteFormatOptions(quote, quote?.currency, ticker.metadata.assetCategory, financials?.quoteMetadata?.instrumentType);
const quotePriceText = quote ? formatMarketPriceWithCurrency(quote.price, quote.currency, moneyOptions) : "";
const quoteChangeText = quote ? formatSignedMarketPrice(quote.change, moneyOptions) : "";
const quotePercentText = quote ? `(${formatPercentRaw(quote.changePercent)})` : "";
const quoteTextWidth = Math.max(1, quoteSummaryWidth - (nativePaneChrome ? 6 : 0));
const stackQuoteChange = displayWidth(quoteChangeText) + 1 + displayWidth(quotePercentText) > quoteTextWidth;
const stackQuoteSummary = displayWidth(quotePriceText) + 3 + displayWidth(quoteChangeText) + displayWidth(quotePercentText) > quoteTextWidth;
// The pane title already names the ticker, so the line leads with the company.
const companyName = ticker.metadata.name || quote?.name || ticker.metadata.ticker;
const marketStateText = quote?.marketState ? t(marketStateLabel(quote.marketState)) : "";
const companyNameWidth = Math.max(8, quoteSummaryWidth - (nativePaneChrome ? 6 : 0)
- (listingVenue ? listingVenue.length + 3 : 0)
- (marketStateText ? marketStateText.length + 1 : 0));
const hasDayRange = quote?.low != null && quote?.high != null && quote.high > quote.low;
const yearRange = liveFiftyTwoWeekRange(quote);
const hasYearRange = yearRange != null;
const rangeInline = contentWidth >= 70 && hasDayRange && hasYearRange;
const rangeWidth = rangeInline
? Math.floor((contentWidth - 2) / 2)
: contentWidth;
const rangeMarkerColor = quote ? priceColor(quote.change) : colors.textDim;
const stats = buildOverviewStats({
quote,
fundamentals,
quoteCurrency,
baseCurrency,
toBase,
marketCapExchangeRates: effectiveExchangeRates,
});
const performanceFields = buildPriceReturnFields(
appendQuoteToPriceReturnHistory(financials?.priceHistory ?? [], historyQuote),
);
const hasPerformance = performanceFields.some((field) => field.value != null || field.unavailableReason);
const positionRows = buildPositionRows({
ticker,
quote,
quoteCurrency,
baseCurrency,
toBase,
});
return (
{truncateToDisplayWidth(companyName, companyNameWidth)}
{listingVenue && (
{" "}({listingVenue})
)}
{quote?.marketState && (
{" "}{t(marketStateLabel(quote.marketState))}
)}
{quote && (
{quotePriceText}
{quoteChangeText}
{quotePercentText}
)}
{quote && (quote.marketState === "PRE" || quote.marketState === "PREPRE") && quote.preMarketPrice != null && (
{t("Pre-Market")}:
{formatMarketPriceWithCurrency(quote.preMarketPrice, quote.currency, moneyOptions)}
{formatSignedMarketPrice(quote.preMarketChange, moneyOptions)} ({formatPercentRaw(quote.preMarketChangePercent)})
)}
{quote && (quote.marketState === "POST" || quote.marketState === "POSTPOST") && quote.postMarketPrice != null && (
{t("After-Hours")}:
{formatMarketPriceWithCurrency(quote.postMarketPrice, quote.currency, moneyOptions)}
{formatSignedMarketPrice(quote.postMarketChange, moneyOptions)} ({formatPercentRaw(quote.postMarketChangePercent)})
)}
{metadataParts.length > 0 && (
{metadataParts.join(" | ")}
)}
{quote && hasBidAsk && (
)}
{(hasDayRange || hasYearRange) && quote && (
{hasDayRange && (
)}
{yearRange && (
)}
)}
{hasHistory && (
{
if ((event?.button ?? 0) === 0) onOpenChart();
}
: undefined}
cursor={onOpenChart ? "pointer" : undefined}
data-gloom-interactive={onOpenChart ? "true" : undefined}
data-gloom-role="overview-chart"
data-gloom-label={onOpenChart ? "Open chart" : undefined}
>
)}
{hasPerformance && (
)}
{stats.length > 0 && (
)}
{positionRows.length > 0 && (
)}
{/* Sector / Industry / Type */}
{(sector || industry || instrumentType) && (
{instrumentType && (
{t("Type")}:
{instrumentType}
)}
{sector && (
{t("Sector")}:
{sector}
)}
{industry && (
{t("Industry")}:
{industry}
)}
)}
{/* ISIN */}
{ticker.metadata.isin && (
{t("ISIN")}:
{ticker.metadata.isin}
)}
{/* Description — last, collapsed */}
{description && (
{description}
)}
);
}