Загрузка данных
import { Divider } from 'exchange-elements/v2';
import { Time, UTCTimestamp } from 'lightweight-charts';
import { FC, Fragment, useLayoutEffect, useRef, useState } from 'react';
import { Observable } from 'rxjs';
import { LegendModel, Ohlc } from '@core/Legend';
import { TooltipVM } from '@core/Tooltip';
import { getThemeStore } from '@src/theme';
import { OHLCConfig, TimeFormat, Timeframes, TooltipConfig } from '@src/types';
import { Defaults } from '@src/types/defaults';
import { calcTooltipPosition, DateFormat, formatDate, formatPrice, shouldShowTime, useObservable } from '@src/utils';
import styles from './index.module.scss';
export interface TooltipProps {
formatObs: Observable<{ dateFormat: DateFormat; timeFormat: TimeFormat }>;
timeframeObs: Observable<Timeframes>;
tooltipConfig: TooltipConfig;
viewModel: Observable<TooltipVM>;
ohlcConfig: OHLCConfig;
}
type LegendItem = LegendModel[number];
type LegendValue = NonNullable<LegendItem['values'][keyof Ohlc]>;
type LegendValues = Partial<Record<keyof Ohlc, LegendValue>>;
const TOOLTIP_MARGIN = 15;
const PRICE_KEYS: (keyof Ohlc)[] = ['open', 'high', 'low', 'close', 'value', 'absoluteChange', 'percentageChange'];
export const ChartTooltip: FC<TooltipProps> = ({
formatObs,
timeframeObs,
ohlcConfig,
tooltipConfig,
viewModel: vm,
}) => {
const viewModel = useObservable(vm);
const format = useObservable(formatObs);
const timeframe = useObservable(timeframeObs);
const refTooltip = useRef<HTMLDivElement | null>(null);
const [tooltipSize, setTooltipSize] = useState({ width: 0, height: 0 });
const { colors } = getThemeStore();
const { visible, position, viewport, vm: model } = viewModel ?? {};
const showTime = timeframe ? shouldShowTime(timeframe) : true;
const precision = ohlcConfig?.precision;
useLayoutEffect(() => {
const element = refTooltip.current;
if (!element || !visible) {
return;
}
setTooltipSize({
width: element.offsetWidth,
height: element.offsetHeight,
});
}, [visible]);
if (!visible || !position || !viewport) {
return null;
}
const coords = calcTooltipPosition({
position,
viewport,
tooltipSize,
margin: TOOLTIP_MARGIN,
});
const style = {
backgroundColor: colors.chartTooltipBackground,
left: `${coords.left}px`,
top: `${coords.top}px`,
};
const hasValue = (entry?: LegendValue): entry is LegendValue => {
return entry?.value !== undefined && entry.value !== null && entry.value !== '';
};
const formatEntryValue = (key: keyof Ohlc, entry: LegendValue): string => {
if (key === 'time' && typeof entry.value === 'number') {
return formatDate(
entry.value as UTCTimestamp,
format?.dateFormat ?? Defaults.dateFormat,
format?.timeFormat ?? Defaults.timeFormat,
showTime,
);
}
if (typeof entry.value === 'number' && PRICE_KEYS.includes(key)) {
return formatPrice(entry.value, precision) ?? '';
}
return String(entry.value);
};
const renderRow = (key: string, label: string, value: string) => (
<div
key={key}
className={styles.row}
>
<span className={styles.label}>{label}</span>
<span className={styles.value}>{value}</span>
</div>
);
const renderDivider = (key: string) => (
<Divider
key={key}
direction="horizontal"
pt={{ divider: { className: styles.divider } }}
/>
);
const renderIndicatorRows = (item: LegendItem) => {
return (Object.entries(item.values) as [keyof Ohlc, LegendValue][]).map(([key, entry]) =>
renderRow(`${item.name}-${String(key)}`, entry.name || String(key), formatEntryValue(key, entry)),
);
};
const renderMainRows = (values: LegendValues) => {
const rows: React.ReactNode[] = [];
if (tooltipConfig.time?.visible && hasValue(values.time)) {
rows.push(renderRow('time', tooltipConfig.time.label, formatEntryValue('time', values.time)));
}
const hasPriceBlock =
(tooltipConfig.close?.visible && hasValue(values.close)) ||
(tooltipConfig.open?.visible && hasValue(values.open)) ||
(tooltipConfig.value?.visible && hasValue(values.value));
if (hasPriceBlock) {
rows.push(renderDivider('price-divider'));
}
if (tooltipConfig.close?.visible && hasValue(values.close)) {
rows.push(renderRow('close', tooltipConfig.close.label, formatEntryValue('close', values.close)));
}
if (tooltipConfig.value?.visible && hasValue(values.value)) {
rows.push(renderRow('value', tooltipConfig.value.label, formatEntryValue('value', values.value)));
}
if (tooltipConfig.open?.visible && hasValue(values.open)) {
rows.push(renderRow('open', tooltipConfig.open.label, formatEntryValue('open', values.open)));
}
const hasDetailsBlock =
(tooltipConfig.high?.visible && hasValue(values.high)) ||
(tooltipConfig.low?.visible && hasValue(values.low)) ||
(tooltipConfig.change?.visible && (hasValue(values.absoluteChange) || hasValue(values.percentageChange)));
if (hasDetailsBlock) {
rows.push(renderDivider('details-divider'));
}
if (tooltipConfig.high?.visible && hasValue(values.high)) {
rows.push(renderRow('high', tooltipConfig.high.label, formatEntryValue('high', values.high)));
}
if (tooltipConfig.low?.visible && hasValue(values.low)) {
rows.push(renderRow('low', tooltipConfig.low.label, formatEntryValue('low', values.low)));
}
if (tooltipConfig.change?.visible) {
const absoluteChange = hasValue(values.absoluteChange)
? formatEntryValue('absoluteChange', values.absoluteChange)
: null;
const percentageChange = hasValue(values.percentageChange)
? formatEntryValue('percentageChange', values.percentageChange)
: null;
if (absoluteChange || percentageChange) {
rows.push(
renderRow(
'change',
tooltipConfig.change.label,
[absoluteChange, percentageChange && `${percentageChange}%`].filter(Boolean).join(' '),
),
);
}
}
return rows;
};
return (
<div
className={styles.tooltip}
style={style}
ref={refTooltip}
>
{model?.map((item) => (
<Fragment key={item.name}>
{item.isIndicator ? renderIndicatorRows(item) : renderMainRows(item.values)}
</Fragment>
))}
</div>
);
};