135 lines
4.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import type { BrokerPortfolio } from '../../../api/responses';
import {
buildBrokerAllocation,
type BrokerAllocationItem,
} from '../../../entities/broker-position';
const RADIUS = 44;
const CIRCUMFERENCE = 2 * Math.PI * RADIUS;
function formatMoneyValue(value: number, currency: string) {
return new Intl.NumberFormat('ru-RU', {
style: 'currency',
currency,
maximumFractionDigits: 2,
}).format(value);
}
function allocationCurrency(portfolio: BrokerPortfolio) {
return (
portfolio.totals.portfolio?.currency ||
Object.values(portfolio.totals).find((total) => total?.currency)?.currency ||
'RUB'
);
}
export function BrokerAllocationBar({
items,
title,
}: {
items: BrokerAllocationItem[];
title: string;
}) {
const positiveItems = items.filter((item) => item.value > 0);
if (positiveItems.length === 0) {
return <p className="broker-allocation-bar__empty">Нет данных для распределения</p>;
}
return (
<div className="broker-allocation-bar">
<div className="broker-allocation-bar__track" role="img" aria-label={title}>
{positiveItems.map((item) => (
<span
key={item.key}
className="broker-allocation-bar__segment"
style={{ width: `${item.percent}%`, background: item.color }}
aria-hidden="true"
/>
))}
</div>
<ul className="broker-allocation-bar__legend" aria-label={`${title}: легенда`}>
{positiveItems.map((item) => (
<li className="broker-allocation-bar__legend-item" key={item.key}>
<span
className="broker-allocation-bar__swatch"
style={{ background: item.color }}
aria-hidden="true"
/>
<span>{item.label}</span>
<strong>{item.percent.toFixed(0)}%</strong>
</li>
))}
</ul>
</div>
);
}
export function BrokerAllocationChart({ portfolio }: { portfolio: BrokerPortfolio }) {
const { sectors, negative } = buildBrokerAllocation(portfolio);
const currency = allocationCurrency(portfolio);
let remaining = CIRCUMFERENCE;
const arcs = sectors.map((sector) => {
const dashOffset = -(CIRCUMFERENCE - remaining);
const rawDashLength = (sector.percent / 100) * CIRCUMFERENCE;
const dashLength = Math.min(Math.max(rawDashLength, 0), remaining);
remaining = Math.max(0, remaining - dashLength);
return { ...sector, dashOffset, dashLength };
});
return (
<figure className="broker-allocation">
<svg role="img" aria-label="Структура брокерского портфеля" viewBox="0 0 120 120">
<title>Структура брокерского портфеля</title>
{arcs.map((sector) => (
<circle
key={sector.key}
cx="60"
cy="60"
r={RADIUS}
fill="none"
stroke={sector.color}
strokeWidth="14"
strokeDasharray={`${sector.dashLength} ${CIRCUMFERENCE - sector.dashLength}`}
strokeDashoffset={sector.dashOffset}
transform="rotate(-90 60 60)"
/>
))}
</svg>
<figcaption>
{sectors.length === 0 ? (
<p>Нет данных для распределения</p>
) : (
<ul>
{sectors.map((sector) => (
<li key={sector.key}>
<span
className="broker-allocation__swatch"
aria-hidden="true"
style={{ background: sector.color }}
/>
<span>
{sector.label}: {formatMoneyValue(sector.value, currency)} ·{' '}
{sector.percent.toFixed(1)}%
</span>
</li>
))}
</ul>
)}
{negative.length > 0 && (
<ul
className="broker-allocation__negative"
aria-label="Отрицательные значения распределения"
>
{negative.map((item) => (
<li key={item.key}>
{item.label}: отрицательное значение {formatMoneyValue(item.value, currency)}
</li>
))}
</ul>
)}
</figcaption>
</figure>
);
}