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


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>
  );
};