diff --git a/src/model/series-options.ts b/src/model/series-options.ts index a6515fcc3a..5dc5caedf6 100644 --- a/src/model/series-options.ts +++ b/src/model/series-options.ts @@ -231,6 +231,13 @@ export interface LineStyleOptions { * @defaultValue {@link LastPriceAnimationMode.Disabled} */ lastPriceAnimation: LastPriceAnimationMode; + + /** + * Connect gaps in data. + * + * @defaultValue `true` + */ + connectGaps?: boolean; } /** @@ -351,6 +358,13 @@ export interface AreaStyleOptions { * @defaultValue {@link LastPriceAnimationMode.Disabled} */ lastPriceAnimation: LastPriceAnimationMode; + + /** + * Connect gaps in data. + * + * @defaultValue `true` + */ + connectGaps?: boolean; } /** @@ -504,6 +518,13 @@ export interface BaselineStyleOptions { * @defaultValue {@link LastPriceAnimationMode.Disabled} */ lastPriceAnimation: LastPriceAnimationMode; + + /** + * Connect gaps in data. + * + * @defaultValue `true` + */ + connectGaps?: boolean; } /** diff --git a/src/model/series/area-pane-view.ts b/src/model/series/area-pane-view.ts index edeacd9483..81f71a9a91 100644 --- a/src/model/series/area-pane-view.ts +++ b/src/model/series/area-pane-view.ts @@ -53,6 +53,7 @@ export class SeriesAreaPaneView extends LinePaneViewBase<'Area', AreaFillItem & invertFilledArea: options.invertFilledArea, visibleRange: this._itemsVisibleRange, barWidth: this._model.timeScale().barSpacing(), + connectGaps: options.connectGaps as boolean, }); this._lineRenderer.setData({ @@ -63,6 +64,7 @@ export class SeriesAreaPaneView extends LinePaneViewBase<'Area', AreaFillItem & visibleRange: this._itemsVisibleRange, barWidth: this._model.timeScale().barSpacing(), pointMarkersRadius: options.pointMarkersVisible ? (options.pointMarkersRadius || options.lineWidth / 2 + 2) : undefined, + connectGaps: options.connectGaps as boolean, }); } } diff --git a/src/model/series/area-series.ts b/src/model/series/area-series.ts index f6dd72005e..3680c29318 100644 --- a/src/model/series/area-series.ts +++ b/src/model/series/area-series.ts @@ -24,6 +24,7 @@ export const areaStyleDefaults: AreaStyleOptions = { crosshairMarkerBackgroundColor: '', lastPriceAnimation: LastPriceAnimationMode.Disabled, pointMarkersVisible: false, + connectGaps: true, }; const createPaneView = (series: ISeries<'Area'>, model: IChartModelBase): IUpdatablePaneView => new SeriesAreaPaneView(series, model); export const createSeries = (): SeriesDefinition<'Area'> => { diff --git a/src/model/series/baseline-pane-view.ts b/src/model/series/baseline-pane-view.ts index 4432e26969..e29c3f1a3c 100644 --- a/src/model/series/baseline-pane-view.ts +++ b/src/model/series/baseline-pane-view.ts @@ -68,6 +68,7 @@ export class SeriesBaselinePaneView extends LinePaneViewBase<'Baseline', Baselin invertFilledArea: false, visibleRange: this._itemsVisibleRange, barWidth, + connectGaps: options.connectGaps as boolean, }); this._baselineLineRenderer.setData({ @@ -81,6 +82,7 @@ export class SeriesBaselinePaneView extends LinePaneViewBase<'Baseline', Baselin bottomCoordinate, visibleRange: this._itemsVisibleRange, barWidth, + connectGaps: options.connectGaps as boolean, }); } } diff --git a/src/model/series/baseline-series.ts b/src/model/series/baseline-series.ts index 1b0dc05e14..6024dd409d 100644 --- a/src/model/series/baseline-series.ts +++ b/src/model/series/baseline-series.ts @@ -34,6 +34,7 @@ export const baselineStyleDefaults: BaselineStyleOptions = { lastPriceAnimation: LastPriceAnimationMode.Disabled, pointMarkersVisible: false, + connectGaps: true, }; const createPaneView = (series: ISeries<'Baseline'>, model: IChartModelBase): IUpdatablePaneView => new SeriesBaselinePaneView(series, model); diff --git a/src/model/series/line-pane-view.ts b/src/model/series/line-pane-view.ts index dc836f9478..b316322b48 100644 --- a/src/model/series/line-pane-view.ts +++ b/src/model/series/line-pane-view.ts @@ -25,6 +25,7 @@ export class SeriesLinePaneView extends LinePaneViewBase<'Line', LineStrokeItem, pointMarkersRadius: options.pointMarkersVisible ? (options.pointMarkersRadius || options.lineWidth / 2 + 2) : undefined, visibleRange: this._itemsVisibleRange, barWidth: this._model.timeScale().barSpacing(), + connectGaps: options.connectGaps as boolean, }; this._renderer.setData(data); diff --git a/src/model/series/line-series.ts b/src/model/series/line-series.ts index b9fe61a11d..4477093269 100644 --- a/src/model/series/line-series.ts +++ b/src/model/series/line-series.ts @@ -20,6 +20,7 @@ export const lineStyleDefaults: LineStyleOptions = { crosshairMarkerBackgroundColor: '', lastPriceAnimation: LastPriceAnimationMode.Disabled, pointMarkersVisible: false, + connectGaps: true, }; const createPaneView = (series: ISeries<'Line'>, model: IChartModelBase): IUpdatablePaneView => new SeriesLinePaneView(series, model); diff --git a/src/renderers/area-renderer-base.ts b/src/renderers/area-renderer-base.ts index 149a8156f4..ac6bafc2b4 100644 --- a/src/renderers/area-renderer-base.ts +++ b/src/renderers/area-renderer-base.ts @@ -21,6 +21,8 @@ export interface PaneRendererAreaDataBase( +export function walkLine( renderingScope: BitmapCoordinatesRenderingScope, items: readonly TItem[], lineType: LineType, @@ -20,7 +20,8 @@ export function walkLine TStyle, finishStyledArea: (renderingScope: BitmapCoordinatesRenderingScope, style: TStyle, areaFirstItem: LinePoint, newAreaFirstItem: LinePoint) => void, - dashPatternLength: number = 0 + dashPatternLength: number = 0, + connectGaps: boolean = true ): void { if (items.length === 0 || visibleRange.from >= items.length || visibleRange.to <= 0) { return; @@ -74,11 +75,22 @@ export function walkLine 1; + + if (isGap) { + finishStyledArea(renderingScope, currentStyle, currentStyleFirstItem, prevItem); + ctx.beginPath(); + currentStyle = itemStyle; + currentStyleFirstItem = currentItem; + ctx.moveTo(currentX, currentY); + continue; + } + switch (lineType) { case LineType.Simple: { ctx.lineTo(currentX, currentY); if (shouldTrackDashOffset) { - const prevItem = items[i - 1]; const prevX = prevItem.x * horizontalPixelRatio; const prevY = prevItem.y * verticalPixelRatio; accumulatedDistance += distanceByCoordinates(prevX, prevY, currentX, currentY); @@ -86,7 +98,6 @@ export function walkLine 10 && i < 20) { + res.push({ time: currentTime }); + continue; + } + + res.push({ + time: currentTime, + value: 10 + Math.sin(i / 5) * 5, + }); + } + return res; +} + +function runTestCase(container) { + const chart = window.chart = LightweightCharts.createChart(container, { + layout: { + attributionLogo: false, + }, + timeScale: { + barSpacing: 20, + }, + }); + + const data = generateData(); + + // Line Series with connectGaps: false + const lineSeries = chart.addSeries(LightweightCharts.LineSeries, { + color: '#2196F3', + lineWidth: 2, + connectGaps: false, + title: 'Line (No Gaps)', + }); + lineSeries.setData(data); + + // Area Series with connectGaps: false (offset for visibility) + const areaSeries = chart.addSeries(LightweightCharts.AreaSeries, { + topColor: 'rgba(33, 150, 243, 0.4)', + bottomColor: 'rgba(33, 150, 243, 0.1)', + lineColor: '#2196F3', + connectGaps: false, + title: 'Area (No Gaps)', + }); + areaSeries.setData(data.map(d => d.value !== undefined ? { ...d, value: d.value + 10 } : d)); + + // Baseline Series with connectGaps: false + const baselineSeries = chart.addSeries(LightweightCharts.BaselineSeries, { + baseValue: { type: 'price', price: 30 }, + topFillColor1: 'rgba(38, 166, 154, 0.28)', + topFillColor2: 'rgba(38, 166, 154, 0.05)', + topLineColor: 'rgba(38, 166, 154, 1)', + bottomFillColor1: 'rgba(239, 83, 80, 0.05)', + bottomFillColor2: 'rgba(239, 83, 80, 0.28)', + bottomLineColor: 'rgba(239, 83, 80, 1)', + connectGaps: false, + title: 'Baseline (No Gaps)', + }); + baselineSeries.setData(data.map(d => d.value !== undefined ? { ...d, value: d.value + 20 } : d)); + + chart.timeScale().fitContent(); +}