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


diff --git a/CHANGELOG.md b/CHANGELOG.md
index 79efc44dca9c2b91e56e9e02dac959f520e565e3..5f7251d6df5fc875cb14e4e6f83be6300da15fd6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,11 +1,17 @@
 # latest
 
+- Продлена ось x вправо
+- Дродаун таймфреймов теперь по умолчанию раскрыт
+- Применены стили к скролбару интервалов
 - Фикс контейнера дровингов. Теперь их контейнер не содержит `priceScale`
 - Рефактор дровингов
 - Фикс названий дровингов связанных с объёмом
 - Исправлено поведение дровинга `fixed range profile volume` 
 - `NaN` получающийся в результате деления на ноль при расчете отосительной цены заменён на `0`
 - К форматированию чисел добавлена обработка бесконечностей 
+
+# 0.1.14
+- 
 - Исправлена обработка событий клика в модальных окнах
 
 # 0.1.13
diff --git a/src/components/Accordion/index.tsx b/src/components/Accordion/index.tsx
index 0ae02ebc87e15cd5fcfcacdada20c73a419629f8..e7ece5140a804dd30a049b9e9f136090a18d0045 100644
--- a/src/components/Accordion/index.tsx
+++ b/src/components/Accordion/index.tsx
@@ -10,7 +10,7 @@ interface AccordionProps extends PropsWithChildren {
 }
 
 export function Accordion({ title, children }: AccordionProps) {
-  const [isOpenAccordion, setIsOpenAccordion] = useState(false);
+  const [isOpenAccordion, setIsOpenAccordion] = useState(true);
   const contentRef = useRef<HTMLDivElement>(null);
   const [height, setHeight] = useState(0);
 
diff --git a/src/components/Footer/index.tsx b/src/components/Footer/index.tsx
index aa0ae6206c3a7152820081ddc702b2bf3a3368fd..1fb40c1338213bc812320ddb4c0df0478e0fd04c 100644
--- a/src/components/Footer/index.tsx
+++ b/src/components/Footer/index.tsx
@@ -1,3 +1,4 @@
+import classNames from 'classnames';
 import dayjs from 'dayjs';
 
 import { Button, Tooltip } from 'exchange-elements/v2';
@@ -10,6 +11,8 @@ import { Intervals, IntervalsToTimeframe, Timeframes } from '@src/types';
 
 import { formatUtcOffset, useObservable } from '@src/utils';
 
+import coreStyles from '../../core/styles.module.scss';
+
 import styles from './index.module.scss';
 
 interface FooterProps {
@@ -32,7 +35,7 @@ export function Footer({ setInterval: setIntervalValue, intervalObs, supportedTi
 
   return (
     <footer className={styles.footer}>
-      <div className={styles.intervals}>
+      <div className={classNames(styles.intervals, coreStyles.scrollableBox)}>
         {(Object.keys(Intervals) as Intervals[])
           .filter((interval: Intervals) => {
             if (!IntervalsToTimeframe[interval]) {
diff --git a/src/core/Chart.ts b/src/core/Chart.ts
index dbf4238efd9efcdc967caa32ce07e35c59d6d956..a3f509deaa9a37b94dfe90d224754f083e800477 100644
--- a/src/core/Chart.ts
+++ b/src/core/Chart.ts
@@ -553,6 +553,8 @@ function getOptions(config: ChartConfig): DeepPartial<ChartOptions> {
       borderVisible: false,
       allowBoldLabels: false,
       rightOffset: 25,
+      shiftVisibleRangeOnNewBar: true,
+      allowShiftVisibleRangeOnWhitespaceReplacement: true,
     },
     rightPriceScale: {
       textColor: colors.chartTextPrimary,
diff --git a/src/core/Indicator.ts b/src/core/Indicator.ts
index ddb29db75fbf05cf7aaf9b32fc0463fcf859496b..2b49366cd244807b20d410600fad3fce6641aacb 100644
--- a/src/core/Indicator.ts
+++ b/src/core/Indicator.ts
@@ -71,6 +71,16 @@ export class Indicator extends DOMObject implements ISerializable<IndicatorSnaps
     this.associatedPane.setIndicator(this.id, this);
   }
 
+  public recreateSeries() {
+    this.series.forEach((s) => {
+      s.destroy();
+    });
+
+    this.series = [];
+    this.seriesMap.clear();
+    this.createSeries();
+  }
+
   public subscribeDataChange(handler: () => void): Subscription {
     this.dataChangeHandlers.add(handler);
 
diff --git a/src/core/Legend.ts b/src/core/Legend.ts
index b4aa179f6f9e190b4f02b545e7c91b68a121ff5e..408a2ec375c57bae841545a9e17cf187160146ac 100644
--- a/src/core/Legend.ts
+++ b/src/core/Legend.ts
@@ -244,6 +244,11 @@ export class Legend {
       return;
     }
 
+    if (param.seriesData.size === 0) {
+      this.updateWithLastCandle();
+      return;
+    }
+
     if (this.paneIndex() === param.paneIndex) {
       this.tooltipVisability.next(true);
     } else {
diff --git a/src/core/Pane.tsx b/src/core/Pane.tsx
index 147d21fe73e60fe3dfb4073b62f7e0faa82c3264..5a6355aae1f60989870db557892d6805e08d4237 100644
--- a/src/core/Pane.tsx
+++ b/src/core/Pane.tsx
@@ -395,6 +395,12 @@ export class Pane implements ISerializable<PaneSnapshot> {
     );
   }
 
+  private rebindIndicators(): void {
+    for (const indicator of this.indicatorsMap.value.values()) {
+      indicator.recreateSeries();
+    }
+  }
+
   private initializeMainSerie({ lwcChart, dataSource }: { lwcChart: IChartApi; dataSource: DataSource }): void {
     this.mainSerieSub = this.eventManager.subscribeSeriesSelected((nextSeries) => {
       this.mainSeries.value?.destroy();
@@ -408,6 +414,8 @@ export class Pane implements ISerializable<PaneSnapshot> {
       });
 
       this.mainSeries.next(next);
+      this.rebindIndicators();
+
       this.priceScaleControls.refresh();
     });
   }
diff --git a/src/core/PaneManager.ts b/src/core/PaneManager.ts
index 45a0c0ce6a8b9ae91a4f653ab30f75b41720561b..131ceb7c64c1b1f6b9ffc789daf0b8a33ce22377 100644
--- a/src/core/PaneManager.ts
+++ b/src/core/PaneManager.ts
@@ -24,7 +24,7 @@ interface PaneManagerParams
   panesSnapshot: PaneSnapshot[];
 }
 
-interface PriceAxisLabelsSources {
+interface PaneManagerStartParams {
   compareEntities$: Observable<Indicator[]>;
   indicatorEntities$: Observable<Indicator[]>;
 }
@@ -90,7 +90,7 @@ export class PaneManager implements ISerializable<PaneSnapshot[]> {
     this.syncPaneContainers();
   }
 
-  public start({ compareEntities$, indicatorEntities$ }: PriceAxisLabelsSources): void {
+  public start({ compareEntities$, indicatorEntities$ }: PaneManagerStartParams): void {
     this.priceAxisLabels?.destroy();
 
     this.priceAxisLabels = new PriceAxisLabels({
diff --git a/src/core/Series/BarSeriesStrategy.ts b/src/core/Series/BarSeriesStrategy.ts
index 39a084bf072dc8c6a61d23661a92c89be2e9ed30..93e754ed52839d220bc56fc499dbca9740aad76e 100644
--- a/src/core/Series/BarSeriesStrategy.ts
+++ b/src/core/Series/BarSeriesStrategy.ts
@@ -48,14 +48,7 @@ export class BarSeriesStrategy extends BaseSeries<'Bar'> implements ISeries<'Bar
       return false;
     }
 
-    return data.every(
-      (point) =>
-        typeof point.time === 'number' &&
-        typeof point.open === 'number' &&
-        typeof point.high === 'number' &&
-        typeof point.low === 'number' &&
-        typeof point.close === 'number',
-    );
+    return data.every((point) => typeof point.time === 'number');
   }
 
   protected dataSourceSubscription = (dataToSet: Candle[]): void => {
@@ -74,7 +67,7 @@ export class BarSeriesStrategy extends BaseSeries<'Bar'> implements ISeries<'Bar
     }
 
     const formattedData = this.formatData([dataToSet]);
-    this.update(formattedData[0]);
+    this.update(formattedData[0], true);
   };
 
   protected formatMainSerie(inputData: Candle[]): SeriesDataItemTypeMap<Time>['Bar'][] {
diff --git a/src/core/Series/CandlestickSeriesStrategy.ts b/src/core/Series/CandlestickSeriesStrategy.ts
index 4089ab2354b319743af3a869dd20b32c19e33327..52d41c16a90396b0fa095324e16bd1c32de5e269 100644
--- a/src/core/Series/CandlestickSeriesStrategy.ts
+++ b/src/core/Series/CandlestickSeriesStrategy.ts
@@ -55,12 +55,17 @@ export class CandlestickSeriesStrategy extends BaseSeries<'Candlestick'> impleme
         return false;
       }
       // Проверяем обязательные поля
-      if (typeof point.time !== 'number' || typeof point.close !== 'number') {
+      if (typeof point.time !== 'number') {
         return false;
       }
 
       // Если указаны OHLC, проверяем их корректность
-      if (point.open !== undefined && point.high !== undefined && point.low !== undefined) {
+      if (
+        point.open !== undefined &&
+        point.high !== undefined &&
+        point.low !== undefined &&
+        point.close !== undefined
+      ) {
         return point.high >= Math.max(point.open, point.close) && point.low <= Math.min(point.open, point.close);
       }
 
@@ -88,7 +93,7 @@ export class CandlestickSeriesStrategy extends BaseSeries<'Candlestick'> impleme
     }
 
     const formattedData = this.formatData([dataToSet]);
-    this.update(formattedData[0]);
+    this.update(formattedData[0], true);
   };
 
   protected formatMainSerie(inputData: Candle[]): SeriesDataItemTypeMap<Time>['Candlestick'][] {
diff --git a/src/core/Series/HistogramSeriesStrategy.ts b/src/core/Series/HistogramSeriesStrategy.ts
index 9060606155ec6c04c9b6c3fd0629bfc8f5606307..c877ad60c7be4bbc08a58d1eee5201cc312ebba4 100644
--- a/src/core/Series/HistogramSeriesStrategy.ts
+++ b/src/core/Series/HistogramSeriesStrategy.ts
@@ -39,9 +39,7 @@ export class HistogramSeriesStrategy extends BaseSeries<'Histogram'> implements
       return false;
     }
 
-    return data.every(
-      (point) => typeof point.time === 'number' && typeof point.volume === 'number' && !Number.isNaN(point.volume),
-    );
+    return data.every((point) => typeof point.time === 'number');
   }
 
   public getTypeName(): string {
@@ -64,7 +62,7 @@ export class HistogramSeriesStrategy extends BaseSeries<'Histogram'> implements
     }
 
     const formattedData = this.formatData([dataToSet]);
-    this.update(formattedData[0]);
+    this.update(formattedData[0], true);
   };
 
   protected formatMainSerie(inputData: Candle[]): SeriesDataItemTypeMap<Time>['Histogram'][] {
diff --git a/src/core/Series/LineSeriesStrategy.ts b/src/core/Series/LineSeriesStrategy.ts
index 8c655c960809b13c852a92ed66d030dff2a85661..74ae054a5d0965b792b3f6f97e7a05cc57c1e0f2 100644
--- a/src/core/Series/LineSeriesStrategy.ts
+++ b/src/core/Series/LineSeriesStrategy.ts
@@ -48,9 +48,7 @@ export class LineSeriesStrategy extends BaseSeries<'Line'> implements ISeries<'L
       return false;
     }
 
-    return data.every(
-      (point) => typeof point.time === 'number' && typeof point.close === 'number' && !Number.isNaN(point.close),
-    );
+    return data.every((point) => typeof point.time === 'number');
   }
 
   public getTypeName(): string {
@@ -73,7 +71,7 @@ export class LineSeriesStrategy extends BaseSeries<'Line'> implements ISeries<'L
     }
 
     const formattedData = this.formatData([dataToSet]);
-    this.update(formattedData[0]);
+    this.update(formattedData[0], true);
   };
 
   protected formatMainSerie(inputData: Candle[]): SeriesDataItemTypeMap<Time>['Line'][] {
diff --git a/src/core/SymbolSource.ts b/src/core/SymbolSource.ts
index 638b314cde1510b7d93aef1c635d07ce63e42a05..6cfaca5308f8e58b7389442d8bc2928199b958df 100644
--- a/src/core/SymbolSource.ts
+++ b/src/core/SymbolSource.ts
@@ -1,4 +1,5 @@
 import dayjs from 'dayjs';
+import { clone } from 'lodash-es';
 
 import { BehaviorSubject, firstValueFrom, Observable, Subject } from 'rxjs';
 import { filter, take } from 'rxjs/operators';
@@ -74,11 +75,6 @@ export class SymbolSource {
     return this.lastCandleSubject.value;
   }
 
-  public async ready(): Promise<void> {
-    if (this.isInitializedSubject.value) return;
-    await firstValueFrom(this.isInitializedSubject.pipe(filter(Boolean), take(1)));
-  }
-
   public destroy(): void {
     this.loadSeq += 1;
     this.loadingPromise = null;
@@ -118,8 +114,7 @@ export class SymbolSource {
 
     const tf = this.getTimeframe();
     this.realtimeCache = this.normalizeList(tf, this.realtimeCache);
-
-    const left = this.currentDataSubject.value;
+    const left = this.currentDataSubject.value.filter((v) => !!v.open);
     const right = this.realtimeCache;
 
     if (left.length === 0 || right.length === 0) {
@@ -139,9 +134,10 @@ export class SymbolSource {
         : left.concat(right);
 
     const normalizedNext = this.normalizeList(tf, next);
+    const extendedNormalizedNext = this.applyWhitespacesToFuture(normalizedNext, tf);
 
-    this.currentDataSubject.next(normalizedNext);
-    this.newestCandle = normalizedNext[normalizedNext.length - 1] ?? null;
+    this.currentDataSubject.next(extendedNormalizedNext);
+    this.newestCandle = next[next.length - 1] ?? null;
     this.realtimeCache = [];
 
     this.lastCandleSubject.next(this.newestCandle);
@@ -309,7 +305,7 @@ export class SymbolSource {
   }
 
   public async reload(tf: Timeframes): Promise<void> {
-    this.loadSeq += 1;
+    this.loadSeq = 1;
     const seq = this.loadSeq;
 
     this.isInitializedSubject.next(false);
@@ -328,11 +324,16 @@ export class SymbolSource {
         const loaded = (await this.getData(tf, this.symbol)) ?? [];
         if (seq !== this.loadSeq) return;
 
-        const normalized = this.normalizeList(tf, loaded);
+        let normalized = this.normalizeList(tf, loaded);
 
-        this.oldestCandle = normalized[0] ?? null;
         this.newestCandle = normalized[normalized.length - 1] ?? null;
 
+        if (this.loadSeq === 1) {
+          normalized = this.applyWhitespacesToFuture(normalized, tf);
+        }
+
+        this.oldestCandle = normalized[0] ?? null;
+
         this.currentDataSubject.next(normalized);
         this.lastCandleSubject.next(this.newestCandle);
 
@@ -354,6 +355,25 @@ export class SymbolSource {
     await task;
   }
 
+  private async ready(): Promise<void> {
+    if (this.isInitializedSubject.value) return;
+    await firstValueFrom(this.isInitializedSubject.pipe(filter(Boolean), take(1)));
+  }
+
+  private applyWhitespacesToFuture(data: Candle[], tf: Timeframes): Candle[] {
+    const lastTime = data[data.length - 1].time;
+    const timeDiff = data[data.length - 1].time - data[data.length - 2].time;
+    const whitespacesNeeded = 5000;
+    const res = clone(data);
+    let i = 0;
+    while (i < whitespacesNeeded) {
+      res.push({ time: lastTime + timeDiff * i } as Candle);
+      i++;
+    }
+
+    return res;
+  }
+
   private flushRealtimeBuffer(): void {
     if (this.realtimeBuffer.length === 0) return;
 
diff --git a/src/core/styles.module.scss b/src/core/styles.module.scss
index 16d5de6c80b0026b59b504d1edbe3c96422684d2..7de23753e704e4942e61710017a3be10494b84f1 100644
--- a/src/core/styles.module.scss
+++ b/src/core/styles.module.scss
@@ -1,11 +1,12 @@
 .scrollableBox {
-  scrollbar-width: thin;
-  scrollbar-color: var(--neutral-6) #00000000;
-
   &::-webkit-scrollbar {
     height: 4px;
   }
 
+  &::-webkit-scrollbar-button {
+    display: none;
+  }
+
   &::-webkit-scrollbar-track {
     background: #00000000;
     border-radius: 4px;
@@ -21,6 +22,13 @@
   }
 }
 
+@supports not selector(::-webkit-scrollbar) {
+  .scrollableBox {
+    scrollbar-width: thin;
+    scrollbar-color: var(--neutral-6) #00000000;
+  }
+}
+
 .safariFullscreen {
   position: fixed;
   top: 0;