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


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

    Object.assign(parentContainer.style, {
      height: '100%',
      width: '100%',
      padding: 'var(--space-1000)',
      backgroundColor: 'var(--neutral-0)',
      borderRadius: 'var(--space-0500)',
      display: 'grid',
      rowGap: `${verticalGap}px`,
      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);

    Object.assign(headerContainer.style, {
      width: '100%',
      height: `${headerHeight}px`,
      overflow: 'auto hidden',
    });

    const footerContainer = document.createElement('div');

    Object.assign(footerContainer.style, {
      width: '100%',
      height: `${footerHeight}px`,
    });

    const chartViewContainer = document.createElement('div');

    Object.assign(chartViewContainer.style, {
      position: 'relative',
      display: 'grid',
      minWidth: '0',
      minHeight: '0',
    });

    const chartContainer = document.createElement('div');

    Object.assign(chartContainer.style, {
      position: 'relative',
      gridArea: '1 / 1',
      height: '100%',
      width: '100%',
      minWidth: '0',
      minHeight: '0',
      display: 'grid',
      columnGap: 'var(--space-0500)',
      gridTemplateColumns: 'minmax(0, 1fr)',
    });

    const chartAreaContainer = document.createElement('div');
    chartAreaContainer.tabIndex = -1;

    Object.assign(chartAreaContainer.style, {
      position: 'relative',
      height: '100%',
      minHeight: '0',
      minWidth: '0',
      outline: 'none',
      cursor: 'crosshair',
    });

    const seasonalityContainer = document.createElement('div');
    seasonalityContainer.tabIndex = -1;
    seasonalityContainer.inert = true;

    Object.assign(seasonalityContainer.style, {
      position: 'relative',
      gridArea: '1 / 1',
      minWidth: '0',
      minHeight: '0',
      visibility: 'hidden',
      pointerEvents: 'none',
      zIndex: ZIndex.Base,
      overflow: 'hidden',
      display: 'grid',
      gridTemplateRows: 'auto minmax(0, 1fr)',
      rowGap: 'var(--space-0500)',
    });

    const seasonalityControlsContainer = document.createElement('div');

    Object.assign(seasonalityControlsContainer.style, {
      minWidth: '0',
    });

    const seasonalityPlotContainer = document.createElement('div');

    Object.assign(seasonalityPlotContainer.style, {
      position: 'relative',
      minWidth: '0',
      minHeight: '0',
    });

    const toolBarContainer = document.createElement('div');

    Object.assign(toolBarContainer.style, {
      height: '100%',
      minHeight: '0',
      overflow: 'hidden',
    });

    const controlBarContainer = document.createElement('div');

    Object.assign(controlBarContainer.style, {
      position: 'absolute',
      left: '50%',
      transform: 'translateX(-50%)',
      bottom: 'var(--space-2000)',
      zIndex: ZIndex.Floating,
    });

    const drawingToolbarContainer = document.createElement('div');
    drawingToolbarContainer.classList.add(CHART_DRAWING_TOOLBAR_CONTAINER);

    Object.assign(drawingToolbarContainer.style, {
      position: 'absolute',
      inset: '0',
      zIndex: ZIndex.Floating,
      pointerEvents: 'none',
      overflow: 'hidden',
    });

    const modalContainer = document.createElement('div');
    modalContainer.classList.add(CHART_MODAL_CONTAINER_CLASSNAME);
    modalContainer.hidden = true;

    Object.assign(modalContainer.style, {
      position: 'absolute',
      inset: '0',
      zIndex: ZIndex.Modal,
    });

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

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

    const toggleToolbar = () => {
      if (toolBarContainer.isConnected) {
        toolBarContainer.remove();

        Object.assign(chartContainer.style, {
          gridTemplateColumns: 'minmax(0, 1fr)',
        });

        return false;
      }

      chartContainer.insertBefore(toolBarContainer, chartAreaContainer);

      Object.assign(chartContainer.style, {
        gridTemplateColumns: `${toolbarWidth}px minmax(0, 1fr)`,
      });

      return true;
    };

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

    Object.assign(legendContainer.style, {
      position: 'absolute',
      top: '0',
      left: '0',
      zIndex: ZIndex.Legend,
      pointerEvents: 'none',
    });

    const paneOverlayContainer = document.createElement('div');
    paneOverlayContainer.classList.add(CHART_PANE_OVERLAY_CONTAINER);

    Object.assign(paneOverlayContainer.style, {
      position: 'absolute',
      inset: '0',
      zIndex: ZIndex.Base,
      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;
  }

  const handleRangeChange = ([fromYear, toYear]: [number, number]) => {
    setRange({ fromYear, toYear });
  };

  return (
    <div className={styles.controls}>
      <div className={styles.controls_header}>
        <Dropdown selectedValue={state.scale === 'absolute' ? t('Price') : t('Percent')}>
          <SeasonalityScaleMenu selectedScale={state.scale} onClick={setScale} />
        </Dropdown>
      </div>

      <div className={styles.controls_range}>
        <RangeField
          mode="range"
          startLabel={t('From year')}
          endLabel={t('To year')}
          value={[state.fromYear, state.toYear]}
          min={state.minYear}
          max={state.maxYear}
          step={1}
          suffix=""
          size="sm"
          disabled={state.status === 'loading' || state.status === 'error'}
          onRangeChange={handleRangeChange}
        />
      </div>
    </div>
  );
}

















.controls {
  display: flex;
  flex-direction: column;
  min-width: 0;
  gap: var(--space-0500);

  &_header {
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: var(--space-0500);
    min-width: 0;
  }

  &_range {
    min-width: 0;
  }
}