Загрузка данных
import { Divider } from 'exchange-elements/v2';
import { Time, UTCTimestamp } from 'lightweight-charts';
import { 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, shouldShowTime, useObservable } from '@src/utils';
import { DateFormat, formatDate, formatDisplayText } from '@src/utils/formatter';
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;
}
const TOOLTIP_MARGIN = 15;
export function ChartTooltip({ formatObs, timeframeObs, tooltipConfig, viewModel: vm }: TooltipProps) {
const viewModel = useObservable(vm);
const format = useObservable<{ dateFormat: DateFormat; timeFormat: TimeFormat }>(formatObs);
const timeframe = useObservable<Timeframes>(timeframeObs);
const refTooltip = useRef<HTMLDivElement | null>(null);
const [tooltipSize, setTooltipSize] = useState({ width: 0, height: 0 });
const { colors } = getThemeStore();
const { visible, position, viewport, vm: vmLegend } = viewModel ?? {};
const showTime = timeframe ? shouldShowTime(timeframe) : true;
useLayoutEffect(() => {
const element = refTooltip.current;
if (!element || !visible) {
return;
}
setTooltipSize({
width: element.offsetWidth,
height: element.offsetHeight,
});
}, [visible, vmLegend]);
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 renderRow = (key: string, label: string | undefined, value: unknown) => {
const text = formatDisplayText(value);
if (!label || !text) {
return null;
}
return (
<div
key={key}
className={styles.row}
>
<span className={styles.label}>{label}</span>
<span className={styles.value}>{text}</span>
</div>
);
};
const getEntries = (values: LegendModel[number]['values']) => {
return values instanceof Map ? Array.from(values.entries()) : Object.entries(values);
};
return (
<div
className={styles.tooltip}
style={style}
ref={refTooltip}
>
{vmLegend?.map((indicator, index) => {
if (indicator.isIndicator) {
return getEntries(indicator.values).map(([key, entry]) => {
if (!entry) {
return null;
}
return renderRow(`${indicator.name}-${String(key)}-${index}`, entry.name || String(indicator.name), entry.value);
});
}
const entries = Object.fromEntries(getEntries(indicator.values)) as Partial<
Record<keyof Ohlc, { value: number | string | Time; color: string; name: string }>
>;
return (
<Fragment key={`${indicator.name}-${index}`}>
{tooltipConfig.time?.visible &&
entries.time?.value &&
renderRow(
`${indicator.name}-time`,
tooltipConfig.time.label,
formatDate(
entries.time.value as UTCTimestamp,
format?.dateFormat ?? Defaults.dateFormat,
format?.timeFormat ?? Defaults.timeFormat,
showTime,
),
)}
{((tooltipConfig.close?.visible && entries.close?.value) ||
(tooltipConfig.open?.visible && entries.open?.value) ||
(tooltipConfig.value?.visible && entries.value?.value)) && (
<Divider
direction="horizontal"
pt={{ divider: { className: styles.divider } }}
/>
)}
{tooltipConfig.close?.visible &&
renderRow(`${indicator.name}-close`, tooltipConfig.close.label, entries.close?.value)}
{tooltipConfig.value?.visible &&
renderRow(`${indicator.name}-value`, tooltipConfig.value.label, entries.value?.value)}
{tooltipConfig.open?.visible &&
renderRow(`${indicator.name}-open`, tooltipConfig.open.label, entries.open?.value)}
{((tooltipConfig.high?.visible && entries.high?.value) ||
(tooltipConfig.low?.visible && entries.low?.value) ||
(tooltipConfig.change?.visible && entries.absoluteChange?.value) ||
(tooltipConfig.change?.visible && entries.percentageChange?.value)) && (
<Divider
direction="horizontal"
pt={{ divider: { className: styles.divider } }}
/>
)}
{tooltipConfig.high?.visible &&
renderRow(`${indicator.name}-high`, tooltipConfig.high.label, entries.high?.value)}
{tooltipConfig.low?.visible &&
renderRow(`${indicator.name}-low`, tooltipConfig.low.label, entries.low?.value)}
{tooltipConfig.change?.visible &&
renderRow(`${indicator.name}-absolute-change`, 'Абсолютное изменение', entries.absoluteChange?.value)}
{tooltipConfig.change?.visible &&
renderRow(
`${indicator.name}-percentage-change`,
'Относительное изменение',
entries.percentageChange?.value,
)}
</Fragment>
);
})}
</div>
);
}
import { Button } from 'exchange-elements/v2';
import { Observable } from 'rxjs';
import { GearIcon, TrashIcon } from '@components/Icon';
import { LegendModel, ohlcValuesToShowForMainSerie } from '@core/Legend';
import { OHLCConfig } from '@src/types';
import { formatDisplayText, useObservable } from '@src/utils';
import styles from './index.module.scss';
export interface LegendProps {
ohlcConfig?: OHLCConfig;
viewModel: Observable<LegendModel>;
}
const mainSerieKeys = new Set(ohlcValuesToShowForMainSerie);
function getEntries(values: LegendModel[number]['values']) {
return values instanceof Map ? Array.from(values.entries()) : Object.entries(values);
}
export const LegendComponent = ({ ohlcConfig, viewModel }: LegendProps) => {
const model = useObservable(viewModel);
const showMainSerieValues = Boolean(ohlcConfig?.show);
return (
<section className={styles.legend}>
{model?.map((item, index) => (
<div
key={`legend-${item.name}-${index}`}
className={styles.row}
>
<div className={styles.symbol}>{item.name}</div>
{getEntries(item.values).map(([key, value]) => {
if (!item.isIndicator && (!showMainSerieValues || !mainSerieKeys.has(key))) {
return null;
}
if (!value) {
return null;
}
const text = formatDisplayText(value.value);
if (!text) {
return null;
}
return (
<div
key={`${item.name}-${key}`}
className={styles.item}
>
{value.name && <span>{value.name}</span>}
<span style={{ color: value.color }}>{text}</span>
</div>
);
})}
{item.settings && (
<Button
size="sm"
className={styles.button}
onClick={item.settings}
label={<GearIcon />}
/>
)}
{item.remove && (
<Button
size="sm"
className={styles.button}
onClick={item.remove}
label={<TrashIcon />}
/>
)}
</div>
))}
</section>
);
};