Загрузка данных


import { Button } from 'exchange-elements/v2';

import { Observable } from 'rxjs';

import { TrashIcon } from '@components/Icon';

import { LegendModel, Ohlc, 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>;
}

type LegendItem = LegendModel[number];
type LegendValue = NonNullable<LegendItem['values'][keyof Ohlc]>;
type LegendEntry = [keyof Ohlc, LegendValue];

export const LegendComponent = ({ ohlcConfig, viewModel }: LegendProps) => {
  const model = useObservable(viewModel) ?? [];
  const showOhlc = ohlcConfig?.show;

  const getEntries = (item: LegendItem): LegendEntry[] => {
    
    return Object.entries(item.values) as LegendEntry[];
  };

  const shouldShowEntry = (item: LegendItem, key: keyof Ohlc): boolean => {
    if (item.isIndicator) {
      return true;
    }

    return Boolean(showOhlc && ohlcValuesToShowForMainSerie.includes(key));
  };

  const renderValue = (item: LegendItem, [key, value]: LegendEntry, index: number) => {
    if (!shouldShowEntry(item, key)) {
      return null;
    }

    return (
      <div
        key={`${item.name}-${index}`}
        className={styles.item}
      >
        {value.name} <span style={{ color: value.color }}>{formatDisplayText(value.value)}</span>
      </div>
    );
  };

  return (
    <section className={styles.legend}>
      {model.map((item) => (
        <div
          key={item.name}
          className={styles.row}
        >
          <div className={styles.symbol}>{item.name}</div>

          {getEntries(item).map((entry, index) => renderValue(item, entry, index))}

          {item.remove && (
            <Button
              size="sm"
              className={styles.button}
              onClick={item.remove}
              label={<TrashIcon />}
            />
          )}
        </div>
      ))}
    </section>
  );
};



import { Divider } from 'exchange-elements/v2';
import { 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, 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 = ({ formatObs, timeframeObs, ohlcConfig, tooltipConfig, viewModel: vm }: TooltipProps) => {
  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>
  );
};