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


import {
  CHART_DRAWING_TOOLBAR_CONTAINER,
  CHART_MODAL_CONTAINER_CLASSNAME,
  CHART_PANE_EMPTY_STATE_CONTAINER,
  CHART_PANE_LEGEND_CONTAINER,
  CHART_PANE_OVERLAY_CONTAINER,
  CHART_ROOT_CLASSNAME,
  CHART_SCROLLABLE_CLASSNAME,
} from '@src/constants';

interface CreateContainersOptions {
  parentContainer: HTMLElement;
  showBottomPanel?: boolean;
  showMenuButton?: boolean;
}

enum ZIndex {
  Chart = '0',
  Base = '1',
  Legend = '2',
  Floating = '3',
  Modal = '1000',
}

const ContainerLayoutConfig = {
  headerHeight: 36,
  footerHeight: 32,
  toolbarWidth: 42,
  verticalGap: 8,
};

/**
 * Утилита для создания DOM контейнеров
 */
export class ContainerManager {
  private static injectFont(): void {
    const existingLink = document.querySelector('link[href*="fonts.googleapis.com"]');

    if (existingLink) {
      return;
    }

    const link = document.createElement('link');
    link.rel = 'stylesheet';
    link.href = 'https://fonts.googleapis.com/css2?family=Inter:opsz,wght@14..32,100..900&display=swap';
    document.head.appendChild(link);
  }

  /**
   * Создание контейнеров для графика и UI компонентов
   */
  static createContainers({ parentContainer, showBottomPanel, showMenuButton }: CreateContainersOptions) {
    this.injectFont();

    const { headerHeight, footerHeight, toolbarWidth, verticalGap } = ContainerLayoutConfig;

    parentContainer.innerHTML = '';
    parentContainer.classList.add(CHART_ROOT_CLASSNAME);
    parentContainer.style.height = '100%';
    parentContainer.style.width = '100%';
    parentContainer.style.padding = 'var(--space-1000)';
    parentContainer.style.backgroundColor = 'var(--neutral-0)';
    parentContainer.style.borderRadius = 'var(--space-0500)';
    parentContainer.style.display = 'grid';
    parentContainer.style.rowGap = `${verticalGap}px`;
    parentContainer.style.gridTemplateRows = showBottomPanel
      ? `${headerHeight}px minmax(0, 1fr) ${footerHeight}px`
      : `${headerHeight}px minmax(0, 1fr)`;

    const headerContainer = document.createElement('div');
    headerContainer.classList.add(CHART_SCROLLABLE_CLASSNAME);
    headerContainer.style.width = '100%';
    headerContainer.style.height = `${headerHeight}px`;
    headerContainer.style.overflow = 'auto hidden';

    const footerContainer = document.createElement('div');
    footerContainer.style.width = '100%';
    footerContainer.style.height = `${footerHeight}px`;

    const chartContainer = document.createElement('div');
    const chartViewContainer = document.createElement('div');
    Object.assign(chartViewContainer.style, {
      position: 'relative',
      display: 'grid',
      minWidth: '0',
      minHeight: '0',
    });
    chartContainer.style.gridArea = '1 / 1';

    const seasonalityContainer = document.createElement('div');
    Object.assign(seasonalityContainer.style, {
      position: 'relative',
      gridArea: '1 / 1',
      minWidth: '0',
      minHeight: '0',
      visibility: 'hidden',
      pointerEvents: 'none',
      zIndex: '1',
      overflow: 'hidden',
      display: 'grid',
    });
    seasonalityContainer.tabIndex = -1;
    seasonalityContainer.inert = true;
    const seasonalityControlsContainer = document.createElement('div');
    const seasonalityPlotContainer = document.createElement('div');
    Object.assign(seasonalityPlotContainer.style, { position: 'relative', minWidth: '0', minHeight: '0' });
    seasonalityContainer.append(seasonalityControlsContainer, seasonalityPlotContainer);
    chartContainer.style.position = 'relative';
    chartContainer.style.height = '100%';
    chartContainer.style.width = '100%';
    chartContainer.style.minWidth = '0';
    chartContainer.style.minHeight = '0';
    chartContainer.style.display = 'grid';
    chartContainer.style.columnGap = 'var(--space-0500)';
    chartContainer.style.gridTemplateColumns = 'minmax(0, 1fr)';

    const chartAreaContainer = document.createElement('div');
    chartAreaContainer.style.position = 'relative';
    chartAreaContainer.style.height = '100%';
    chartAreaContainer.style.minHeight = '0';
    chartAreaContainer.style.minWidth = '0';
    chartAreaContainer.style.outline = 'none';
    chartAreaContainer.tabIndex = -1;
    chartAreaContainer.style.cursor = 'crosshair';

    const toolBarContainer = document.createElement('div');
    toolBarContainer.style.height = '100%';
    toolBarContainer.style.minHeight = '0';
    toolBarContainer.style.overflow = 'hidden';

    const controlBarContainer = document.createElement('div');
    controlBarContainer.style.position = 'absolute';
    controlBarContainer.style.left = '50%';
    controlBarContainer.style.transform = 'translateX(-50%)';
    controlBarContainer.style.bottom = 'var(--space-2000)';
    controlBarContainer.style.zIndex = ZIndex.Floating;

    const drawingToolbarContainer = document.createElement('div');
    drawingToolbarContainer.classList.add(CHART_DRAWING_TOOLBAR_CONTAINER);
    drawingToolbarContainer.style.position = 'absolute';
    drawingToolbarContainer.style.inset = '0';
    drawingToolbarContainer.style.zIndex = ZIndex.Floating;
    drawingToolbarContainer.style.pointerEvents = 'none';
    drawingToolbarContainer.style.overflow = 'hidden';

    const modalContainer = document.createElement('div');
    modalContainer.classList.add(CHART_MODAL_CONTAINER_CLASSNAME);
    modalContainer.style.position = 'absolute';
    modalContainer.style.inset = '0';
    modalContainer.style.zIndex = ZIndex.Modal;
    modalContainer.hidden = true;

    chartAreaContainer.append(controlBarContainer, drawingToolbarContainer);
    chartContainer.append(chartAreaContainer, modalContainer);
    chartViewContainer.append(chartContainer, seasonalityContainer);
    parentContainer.append(headerContainer, chartViewContainer);

    if (showBottomPanel) {
      parentContainer.append(footerContainer);
    }

    const toggleToolbar = () => {
      const mounted = toolBarContainer.isConnected;

      if (mounted) {
        toolBarContainer.remove();
        chartContainer.style.gridTemplateColumns = 'minmax(0, 1fr)';
        return false;
      }

      chartContainer.insertBefore(toolBarContainer, chartAreaContainer);
      chartContainer.style.gridTemplateColumns = `${toolbarWidth}px minmax(0, 1fr)`;
      return true;
    };

    chartContainer.insertBefore(toolBarContainer, chartAreaContainer);
    chartContainer.style.gridTemplateColumns = `${toolbarWidth}px minmax(0, 1fr)`;

    if (!showMenuButton) {
      toggleToolbar();
    }

    return {
      headerContainer,
      footerContainer,
      chartContainer,
      seasonalityContainer,
      seasonalityControlsContainer,
      seasonalityPlotContainer,
      chartAreaContainer,
      toolBarContainer,
      modalContainer,
      controlBarContainer,
      drawingToolbarContainer,
      toggleToolbar,
    };
  }

  /**
   * Очистка контейнеров
   */
  static clearContainers(parentContainer: HTMLElement): void {
    parentContainer.innerHTML = '';
  }

  static createPaneContainers() {
    const legendContainer = document.createElement('div');

    legendContainer.classList.add(CHART_PANE_LEGEND_CONTAINER);

    legendContainer.style.position = 'absolute';
    legendContainer.style.top = '0';
    legendContainer.style.left = '0';
    legendContainer.style.zIndex = ZIndex.Legend;
    legendContainer.style.pointerEvents = 'none';

    const paneOverlayContainer = document.createElement('div');
    paneOverlayContainer.classList.add(CHART_PANE_OVERLAY_CONTAINER);
    paneOverlayContainer.style.position = 'absolute';
    paneOverlayContainer.style.inset = '0';
    paneOverlayContainer.style.zIndex = ZIndex.Base;
    paneOverlayContainer.style.pointerEvents = 'none';

    const paneEmptyStateContainer = document.createElement('div');
    paneEmptyStateContainer.classList.add(CHART_PANE_EMPTY_STATE_CONTAINER);
    paneEmptyStateContainer.hidden = true;

    return {
      legendContainer,
      paneOverlayContainer,
      paneEmptyStateContainer,
    };
  }
}

















import { Dropdown } from '@src/components/Dropdown';
import { RangeField } from '@src/components/FormFields';
import { SeasonalityScaleMenu } from '@src/components/Menu/SeasonalityScaleMenu';

import { t } from '@src/translations';
import { useObservable } from '@src/utils';

import styles from './index.module.scss';

import type { SeasonalityRange, SeasonalityScale, SeasonalityState } from '@src/core/Seasonality/types';
import type { Observable } from 'rxjs';

interface SeasonalityControlsProps {
  stateObs: Observable<SeasonalityState>;
  setScale: (scale: SeasonalityScale) => void;
  setRange: (range: SeasonalityRange) => void;
}

export function SeasonalityControls({ stateObs, setScale, setRange }: SeasonalityControlsProps) {
  const state = useObservable(stateObs);
  if (!state || state.view !== 'seasonality') return null;

  return (
    <div className={styles.controls}>
      <Dropdown selectedValue={state.scale === 'absolute' ? t('Price') : t('Percent')}>
        <SeasonalityScaleMenu
          selectedScale={state.scale}
          onClick={setScale}
        />
      </Dropdown>
      <div className={styles.controls_range}>
        <RangeField
          mode="range"
          value={[state.fromYear, state.toYear]}
          min={state.minYear}
          max={state.maxYear}
          step={1}
          suffix=""
          size="sm"
          disabled={state.status === 'loading' || state.status === 'error'}
          onRangeChange={([fromYear, toYear]) => setRange({ fromYear, toYear })}
        />
      </div>
    </div>
  );
}
















.controls {
  width: 100%;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: var(--space-0500);

  &_range {
    flex: 1;
    min-width: 0;
  }
}