🎨 style(analysis): improve UI clarity and visual hierarchy

Enhance the company analysis overview page with better data presentation
and visual design:

- Fix business description display by filtering raw API data artifacts
- Improve metadata layout with consolidated single-line format
- Fix price chart Y-axis scaling to auto-scale to data range
- Replace 'n/a' with cleaner em dash (—) for empty states
- Add visual indicators and color-coded backgrounds to bull/bear sections
- Improve empty state messaging with centered icons

These changes improve information density, visual hierarchy, and overall
user experience across the analysis page.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-03-12 21:29:03 -04:00
parent ba385586bc
commit c222179170
4 changed files with 42 additions and 29 deletions

View File

@@ -11,39 +11,46 @@ type CompanyOverviewCardProps = {
export function CompanyOverviewCard(props: CompanyOverviewCardProps) {
const [expanded, setExpanded] = useState(false);
const description =
props.analysis.companyProfile.description ??
"No annual filing business description is available yet.";
// Get the actual business description, filtering out raw data artifacts
const rawDescription = props.analysis.companyProfile.description;
const isRawData = rawDescription && (
rawDescription.includes('http://') ||
rawDescription.includes('P1Y') ||
rawDescription.includes('P3Y') ||
rawDescription.includes('FY false')
);
const description = !rawDescription || isRawData
? "No business description available."
: rawDescription;
const needsClamp = description.length > 320;
// Combine metadata into a single line
const metadata = [
props.analysis.company.ticker,
props.analysis.company.sector ?? props.analysis.companyProfile.industry,
props.analysis.company.cik ? `CIK ${props.analysis.company.cik}` : null,
].filter(Boolean).join(' · ');
return (
<Panel className="h-full pt-2">
<div className="space-y-5">
{/* Header with company name and metadata */}
<div>
<div>
<h2 className="text-2xl font-semibold text-[color:var(--terminal-bright)]">
{props.analysis.company.companyName}
</h2>
<p className="mt-1 text-xs uppercase tracking-[0.18em] text-[color:var(--terminal-muted)]">
{props.analysis.company.ticker}
</p>
<p className="mt-3 text-sm text-[color:var(--terminal-muted)]">
{props.analysis.company.sector ??
props.analysis.companyProfile.industry ??
"Sector unavailable"}
{props.analysis.company.category
? ` · ${props.analysis.company.category}`
: ""}
{props.analysis.company.cik
? ` · CIK ${props.analysis.company.cik}`
: ""}
</p>
</div>
<h2 className="text-2xl font-semibold text-[color:var(--terminal-bright)]">
{props.analysis.company.companyName}
</h2>
<p className="mt-1 text-xs uppercase tracking-[0.18em] text-[color:var(--terminal-muted)]">
{metadata}
</p>
</div>
{/* Business description section */}
<div className="border-t border-[color:var(--line-weak)] py-4">
<div className="flex items-start justify-between gap-3">
<div>
<div className="flex-1 min-w-0">
<p className="text-xs uppercase tracking-[0.14em] text-[color:var(--terminal-muted)]">
Business description
</p>
@@ -58,7 +65,7 @@ export function CompanyOverviewCard(props: CompanyOverviewCardProps) {
href={props.analysis.companyProfile.website}
target="_blank"
rel="noreferrer"
className="inline-flex items-center gap-1 text-xs uppercase tracking-[0.14em] text-[color:var(--accent)] hover:text-[color:var(--accent-strong)]"
className="inline-flex shrink-0 items-center gap-1 text-xs uppercase tracking-[0.14em] text-[color:var(--accent)] hover:text-[color:var(--accent-strong)]"
>
Website
<ExternalLink className="size-3.5" />

View File

@@ -72,6 +72,8 @@ export function PriceHistoryCard(props: PriceHistoryCardProps) {
tickLine={{ stroke: CHART_MUTED }}
tick={{ fill: CHART_MUTED }}
tickFormatter={(value: number) => `$${value.toFixed(0)}`}
domain={[(dataMin) => dataMin * 0.05, (dataMax) => dataMax * 1.05]}
allowDataOverflow
/>
<Tooltip
formatter={(value) => formatCurrency(Array.isArray(value) ? value[0] : value)}

View File

@@ -8,18 +8,22 @@ type ValuationFactsTableProps = {
};
function formatRatio(value: number | null) {
return value === null ? 'n/a' : `${value.toFixed(2)}x`;
return value === null ? '' : `${value.toFixed(2)}x`;
}
function formatShares(value: number | null) {
return value === null ? 'n/a' : formatScaledNumber(value, { maximumFractionDigits: 2 });
return value === null ? '' : formatScaledNumber(value, { maximumFractionDigits: 2 });
}
function formatCompactCurrencyOrDash(value: number | null) {
return value === null ? '—' : formatCompactCurrency(value);
}
export function ValuationFactsTable(props: ValuationFactsTableProps) {
const items = [
{ label: 'Source', value: props.analysis.valuationSnapshot.source },
{ label: 'Market cap', value: props.analysis.valuationSnapshot.marketCap === null ? 'n/a' : formatCompactCurrency(props.analysis.valuationSnapshot.marketCap) },
{ label: 'Enterprise value', value: props.analysis.valuationSnapshot.enterpriseValue === null ? 'n/a' : formatCompactCurrency(props.analysis.valuationSnapshot.enterpriseValue) },
{ label: 'Market cap', value: formatCompactCurrencyOrDash(props.analysis.valuationSnapshot.marketCap) },
{ label: 'Enterprise value', value: formatCompactCurrencyOrDash(props.analysis.valuationSnapshot.enterpriseValue) },
{ label: 'Shares outstanding', value: formatShares(props.analysis.valuationSnapshot.sharesOutstanding) },
{ label: 'Trailing P/E', value: formatRatio(props.analysis.valuationSnapshot.trailingPe) },
{ label: 'EV / Revenue', value: formatRatio(props.analysis.valuationSnapshot.evToRevenue) },