Загрузка данных
import { PriceScaleMode } from 'lightweight-charts';
import { Button } from '@src/components/Button';
import { Dropdown } from '@src/components/Dropdown';
import { RangeField } from '@src/components/FormFields';
import { CameraIcon, LineIcon, TableIcon } from '@src/components/Icon';
import { AverageIcon } from '@src/components/Icon/AverageIcon';
import { SnapshotMenu } from '@src/components/Menu';
import { SeasonalityScaleMenu } from '@src/components/Menu/SeasonalityScaleMenu';
import { Tooltip } from '@src/components/Tooltip';
import { t } from '@src/translations';
import { SeasonalityViewType } from '@src/types';
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>;
setDisplayMode: (displayMode: SeasonalityViewType) => void;
setScale: (scale: SeasonalityScale) => void;
setRange: (range: SeasonalityRange) => void;
setAverageVisible: (visible: boolean) => void;
copyImage?: () => Promise<void>;
}
const MAX_SCALE_LABELS = 5;
function createYearMarks(minYear: number, maxYear: number): number[] {
const span = maxYear - minYear;
if (span <= 0) return [minYear];
const intervals = Math.min(MAX_SCALE_LABELS - 1, span);
return Array.from(
{ length: intervals + 1 },
(_, index) => minYear + Math.floor((span * index) / intervals),
);
}
export function SeasonalityControls({
stateObs,
setDisplayMode,
setScale,
setRange,
setAverageVisible,
copyImage,
}: SeasonalityControlsProps) {
const state = useObservable(stateObs);
if (!state || state.status === 'idle') {
return null;
}
const selectedScaleLabel = state.scale === PriceScaleMode.Normal ? t('Price') : t('Percent');
const isRangeDisabled = state.status === 'loading' || state.status === 'error';
const canCopyImage = state.status === 'ready' && state.displayMode === SeasonalityViewType.Chart;
const marks = createYearMarks(state.minYear, state.maxYear);
const handleRangeChange = ([fromYear, toYear]: [number, number]): void => {
setRange({ fromYear, toYear });
};
return (
<div className={styles.controls}>
<div className={styles.controls_header}>
<div className={styles.controls_views}>
<Button
label={<LineIcon />}
pressed={state.displayMode === SeasonalityViewType.Chart}
onClick={() => setDisplayMode(SeasonalityViewType.Chart)}
/>
<Button
label={<TableIcon />}
pressed={state.displayMode === SeasonalityViewType.Table}
onClick={() => setDisplayMode(SeasonalityViewType.Table)}
/>
</div>
<div className={styles.controls_actions}>
<Button
label={
<span className={styles.controls_buttonContent}>
<AverageIcon />
<span>{t('Average')}</span>
</span>
}
pressed={state.showAverage}
disabled={state.status !== 'ready'}
onClick={() => setAverageVisible(!state.showAverage)}
/>
<div className={styles.controls_scale}>
<Dropdown
selectedValue={selectedScaleLabel}
renderTrigger={({ isOpen, onToggle }) => (
<Button
pressed={isOpen}
onClick={onToggle}
label={
<span className={styles.controls_buttonContent}>
<span>{selectedScaleLabel}</span>
<span
className={`${styles.controls_chevron} ${isOpen ? styles.controls_chevronOpen : ''}`}
/>
</span>
}
/>
)}
>
<SeasonalityScaleMenu selectedScale={state.scale} onClick={setScale} />
</Dropdown>
</div>
{copyImage && canCopyImage && (
<Tooltip location="bottom" label={t('Snapshot')}>
<Dropdown selectedValue={<CameraIcon />}>
<SnapshotMenu onCopyImage={copyImage} />
</Dropdown>
</Tooltip>
)}
</div>
</div>
<div className={styles.controls_range}>
<RangeField
mode="range"
value={[state.fromYear, state.toYear]}
min={state.minYear}
max={state.maxYear}
marks={marks}
step={1}
suffix=""
size="sm"
disabled={isRangeDisabled}
onRangeChange={handleRangeChange}
/>
</div>
</div>
);
}
copyImage={
config.chartCollectionPreset.showSnapshotButton
? () => this.seasonalityManager.copyImage()
: undefined
}
[data-theme='mxt'] {
--cell-background-positive: #24a249;
--cell-background-negative: #d71d38;
--cell-text-positive: #24a249;
--cell-text-negative: #d71d38;
}
[data-theme='mb'] {
--cell-background-positive: #3cae0b;
--cell-background-negative: #d71d38;
--cell-text-positive: #3cae0b;
--cell-text-negative: #d71d38;
}
[data-theme='tr'] {
--cell-background-positive: #27ef0085;
--cell-background-negative: #ff000085;
--cell-text-positive: #27ef00;
--cell-text-negative: #ff0000;
}
.seasonality {
font-variant-numeric: tabular-nums;
}
.positive {
--table-cell-background: color-mix(
in srgb,
var(--cell-background-positive) 35%,
var(--table-background, var(--neutral-0))
);
}
.negative {
--table-cell-background: color-mix(
in srgb,
var(--cell-background-negative) 35%,
var(--table-background, var(--neutral-0))
);
}
.total {
--table-cell-font-weight: 600;
}
.average {
--table-cell-background: var(--table-background, var(--neutral-0));
--table-cell-font-weight: 600;
color: var(--table-text, var(--neutral-12));
border-top: var(--borderWidth-md) solid var(--table-border, var(--neutral-7));
&.positive {
color: var(--cell-text-positive);
}
&.negative {
color: var(--cell-text-negative);
}
}