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


import type { PriceScaleMode } from 'lightweight-charts';

import type { ChartViewType } from './chartView';

export type SeasonalityScale = PriceScaleMode.Normal | PriceScaleMode.Percentage;

export interface SeasonalityRange {
  fromYear: number;
  toYear: number;
}

export interface SeasonalityState extends SeasonalityRange {
  view: ChartViewType;
  status: 'idle' | 'loading' | 'ready' | 'empty' | 'error';
  scale: SeasonalityScale;
  symbolId: string;
  minYear: number;
  maxYear: number;
  error: string | null;
}

export interface SeasonalityPoint {
  date: string;
  chartDate: string;
  price: number;
  percentage: number | null;
}

export interface SeasonalityYear {
  year: number;
  baselineDate: string;
  baselinePrice: number;
  points: SeasonalityPoint[];
}

export interface DailyClose {
  date: string;
  close: number;
}

















export enum ChartViewType {
  Chart = 'chart',
  Seasonality = 'seasonality',
}

















import { PriceScaleMode } from 'lightweight-charts';

import { ChartViewType } from '@src/types/chartView';

import type { SeasonalityState } from '@src/types/seasonality';

export const DEFAULT_SEASONALITY_YEARS = 5;

export function createInitialSeasonalityState(): SeasonalityState {
  const year = new Date().getUTCFullYear();
  const fromYear = year - DEFAULT_SEASONALITY_YEARS + 1;

  return {
    view: ChartViewType.Chart,
    status: 'idle',
    scale: PriceScaleMode.Normal,
    symbolId: '',
    error: null,
    minYear: fromYear,
    maxYear: year,
    fromYear,
    toYear: year,
  };
}

















import { MenuList, type MenuOption } from '@src/components/Menu/MenuList';
import { t } from '@src/translations';
import { ChartViewType } from '@src/types/chartView';

interface ChartViewMenuProps {
  selectedView: ChartViewType;
  onClick: (view: ChartViewType) => void;
}

export function ChartViewMenu({ selectedView, onClick }: ChartViewMenuProps) {
  const options: MenuOption<ChartViewType>[] = [
    {
      value: ChartViewType.Chart,
      label: t('Chart'),
    },
    {
      value: ChartViewType.Seasonality,
      label: t('Seasonality'),
    },
  ];

  return (
    <MenuList
      mode="single"
      value={selectedView}
      options={options}
      onClick={onClick}
    />
  );
}

















import { PriceScaleMode } from 'lightweight-charts';

import { MenuList, type MenuOption } from '@src/components/Menu/MenuList';
import { t } from '@src/translations';

import type { SeasonalityScale } from '@src/types/seasonality';

interface SeasonalityScaleMenuProps {
  selectedScale: SeasonalityScale;
  onClick: (scale: SeasonalityScale) => void;
}

export function SeasonalityScaleMenu({ selectedScale, onClick }: SeasonalityScaleMenuProps) {
  const options: MenuOption<SeasonalityScale>[] = [
    {
      value: PriceScaleMode.Normal,
      label: t('Price'),
    },
    {
      value: PriceScaleMode.Percentage,
      label: t('Percent'),
    },
  ];

  return (
    <MenuList
      mode="single"
      value={selectedScale}
      options={options}
      onClick={onClick}
    />
  );
}

















import { PriceScaleMode } from 'lightweight-charts';

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 { ChartViewType } from '@src/types/chartView';
import { useObservable } from '@src/utils';

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

import type { SeasonalityRange, SeasonalityScale, SeasonalityState } from '@src/types/seasonality';
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 !== ChartViewType.Seasonality) {
    return null;
  }

  const selectedScaleLabel = state.scale === PriceScaleMode.Normal
    ? t('Price')
    : t('Percent');

  const isRangeDisabled = state.status === 'loading' || state.status === 'error';

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

  return (
    <div className={styles.controls}>
      <div className={styles.controls_header}>
        <Dropdown selectedValue={selectedScaleLabel}>
          <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={isRangeDisabled}
          onRangeChange={handleRangeChange}
        />
      </div>
    </div>
  );
}

















  private setChartView = (view: ChartViewType): void => {
    if (view === ChartViewType.Chart) {
      this.backToChart();
      return;
    }

    void this.showSeasonality();
  };















  const isSeasonality = seasonalityState.view === ChartViewType.Seasonality;







  setChartView?: (view: ChartViewType) => void;