Загрузка данных
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;
}
}