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