Repository: tradingview/lightweight-charts (ts)

Issue:
LineSeries is not displayed if starting x value is out of viewport
![Screenshot 2019-07-10 at 22 19 36](https://user-images.githubusercontent.com/4056227/61005957-1cb09300-a361-11e9-9d43-0d8f3a0f38e4.png)

![Screenshot 2019-07-10 at 22 22 09](https://user-images.githubusercontent.com/4056227/61005990-2e923600-a361-11e9-8a41-1d5a269924c5.png)

You'll note the first line disappears as you zoom in and it goes just outside of the viewport, the second line will do the same when you zoom further.

Fiddle with full reference data: https://jsfiddle.net/jimeney/Lgtrhj52/9/

Steps to repeat: zoom out

Pull request description:
LineSeries is not displayed if starting x value is out of viewport
**Type of PR:** bugfix<!-- bugfix, enhancement, fix typo, etc -->

**PR checklist:**

- [x] Addresses an existing issue: fixes #116 
- [x] Includes tests
- [ ] Documentation update

**Overview of change:**

Changed series items range to draw for line-based series, fixed visible price range calculation

<!-- optional -->


Patch:
diff --git a/docs/series-basics.md b/docs/series-basics.md
index 620bc6a47..40254954a 100644
--- a/docs/series-basics.md
+++ b/docs/series-basics.md
@@ -206,23 +206,21 @@ series.applyOptions({
 
 #### Price format
 
-Four price formats are provided for displaying on the price scale:
+Three price formats are provided for displaying on the price scale:
 
 - `price` format, which is set by default, displays absolute price value as it is
 - `volume` format reduces number of digits of values over 1000, replacing zeros by letters. For example, '1000' absolute price value is shown as '1K' in a volume format.
 - `percent` format replaces absolute values with their percentage change.
-- `custom` format uses a user-defined function for price formatting that could be used in some specific cases, that are not covered by standard formatters
 
 The following options are available for setting the price format displayed by any type of series:
 
 |Name|Type|Default|Description|
 |----|----|-------|-|
-|`type`|`price` &#124; `volume` &#124; `percent` &#124; `custom` |`price`|Sets a type of price displayed by series|
+|`type`|`price` &#124; `volume` &#124; `percent`|`price`|Sets a type of price displayed by series|
 |`precision`|`number`|`2`|Specifies a number of decimal places used for price value display|
 |`minMove`|`number`|`0.01`|Sets the minimum possible step size for price value movement|
-|`formatter`|`function` &#124; `undefined`|`undefined`|Sets a formatting function that is used when the `type` is `custom`|
 
-Examples:
+Example:
 
 ```javascript
 series.applyOptions({
@@ -234,18 +232,6 @@ series.applyOptions({
 });
 ```
 
-```javascript
-series.applyOptions({
-    priceFormat: {
-        type: 'custom',
-        minMove: 0.02,
-        formatter: function(price) {
-            return '$' + price.toFixed(2);
-        },
-    }
-});
-```
-
 ### setData
 
 Allows to set/replace all existing data with new one.
diff --git a/src/api/chart-api.ts b/src/api/chart-api.ts
index e5c8330a7..040aac612 100644
--- a/src/api/chart-api.ts
+++ b/src/api/chart-api.ts
@@ -22,7 +22,6 @@ import {
 	LineSeriesPartialOptions,
 	precisionByMinMove,
 	PriceFormat,
-	PriceFormatBuiltIn,
 	SeriesType,
 } from '../model/series-options';
 import { TimePointIndex } from '../model/time-data';
@@ -48,12 +47,11 @@ import { SeriesApi } from './series-api';
 import { TimeScaleApi } from './time-scale-api';
 
 function patchPriceFormat(priceFormat?: DeepPartial<PriceFormat>): void {
-	if (priceFormat === undefined || priceFormat.type === 'custom') {
+	if (priceFormat === undefined) {
 		return;
 	}
-	const priceFormatBuiltIn = priceFormat as DeepPartial<PriceFormatBuiltIn>;
-	if (priceFormatBuiltIn.minMove !== undefined && priceFormatBuiltIn.precision === undefined) {
-		priceFormatBuiltIn.precision = precisionByMinMove(priceFormatBuiltIn.minMove);
+	if (priceFormat.minMove !== undefined && priceFormat.precision === undefined) {
+		priceFormat.precision = precisionByMinMove(priceFormat.minMove);
 	}
 }
 
diff --git a/src/formatters/percentage-formatter.ts b/src/formatters/percentage-formatter.ts
index daf487499..9a332161f 100644
--- a/src/formatters/percentage-formatter.ts
+++ b/src/formatters/percentage-formatter.ts
@@ -5,7 +5,7 @@ export class PercentageFormatter extends PriceFormatter {
 		super(priceScale);
 	}
 
-	public format(price: number): string {
-		return `${super.format(price)}%`;
+	public format(price: number, signPositive?: boolean, tailSize?: number, signNegative?: boolean): string {
+		return `${super.format(price, signPositive, tailSize, signNegative)}%`;
 	}
 }
diff --git a/src/formatters/price-formatter.ts b/src/formatters/price-formatter.ts
index 3f1fed47d..779ad161c 100644
--- a/src/formatters/price-formatter.ts
+++ b/src/formatters/price-formatter.ts
@@ -60,17 +60,37 @@ export class PriceFormatter implements IFormatter {
 		this._calculateDecimal();
 	}
 
-	public format(price: number): string {
-		// \u2212 is unicode's minus sign https://www.fileformat.info/info/unicode/char/2212/index.htm
-		// we should use it because it has the same width as plus sign +
-		const sign = price < 0 ? '\u2212' : '';
-		price = Math.abs(price);
+	// tailSize > 0 for formatting avg prices
+	public format(
+		price: number,
+		signPositive?: boolean,
+		tailSize?: number,
+		signNegative: boolean = true
+	): string {
+		// split price into integer part and fractional
+		let sign = '';
+		if (price < 0) {
+			if (signPositive === true) {
+				sign = '\u2212';
+			} else if (signNegative === false) {
+				sign = '';
+			} else {
+				sign = '−';
+			}
+			price = -price;
+		} else if (price && signPositive === true) {
+			sign = '+';
+		}
+
+		let value;
 
 		if (this._fractional) {
-			return sign + this._formatAsFractional(price);
+			value = sign + this._formatAsFractional(price, tailSize);
+		} else {
+			value = sign + this._formatAsDecimal(price, tailSize);
 		}
 
-		return sign + this._formatAsDecimal(price);
+		return value;
 	}
 
 	private _calculateDecimal(): void {
@@ -90,13 +110,14 @@ export class PriceFormatter implements IFormatter {
 		}
 	}
 
-	private _formatAsDecimal(price: number): string {
+	private _formatAsDecimal(price: number, tailSize?: number): string {
 		let base: number;
+		tailSize = tailSize || 0;
 		if (this._fractional) {
 			// if you really want to format fractional as decimal
 			base = Math.pow(10, (this._fractionalLength || 0));
 		} else {
-			base = this._priceScale / this._minMove;
+			base = Math.pow(10, tailSize) * this._priceScale / this._minMove;
 		}
 
 		let intPart = Math.floor(price);
@@ -110,30 +131,43 @@ export class PriceFormatter implements IFormatter {
 				intPart += 1;
 			}
 
-			fracString = formatterOptions.decimalSign + numberToStringWithLeadingZero(+fracPart.toFixed(this._fractionalLength) * this._minMove, fracLength);
+			fracString = formatterOptions.decimalSign + numberToStringWithLeadingZero(+fracPart.toFixed(this._fractionalLength) * this._minMove, fracLength + tailSize);
+
+			// remove ending 0 but not more then tailSize
+			fracString = this._removeEndingZeros(fracString, tailSize);
 		} else {
 			// should round int part to minmov
 			intPart = Math.round(intPart * base) / base;
 			// if minmov > 1, fractional part is always = 0
 			if (fracLength > 0) {
-				fracString = formatterOptions.decimalSign + numberToStringWithLeadingZero(0, fracLength);
+				fracString = formatterOptions.decimalSign + numberToStringWithLeadingZero(0, fracLength + tailSize);
 			}
 		}
 
 		return intPart.toFixed(0) + fracString;
 	}
 
-	private _formatAsFractional(price: number): string {
+	private _formatAsFractional(price: number, tailSize?: number): string {
 		// temporary solution - use decimal format with 2 digits
 		const base = this._priceScale / this._minMove;
 		let intPart = Math.floor(price);
-		let fracPart = Math.round(price * base) - intPart * base;
+		let fracPart = tailSize ?
+			Math.floor(price * base) - intPart * base :
+			Math.round(price * base) - intPart * base;
 
 		if (fracPart === base) {
 			fracPart = 0;
 			intPart += 1;
 		}
 
+		let tailStr = '';
+		if (tailSize) {
+			let tail = (price - intPart - fracPart / base) * base;
+			tail = Math.round(tail * Math.pow(10, tailSize));
+			tailStr = numberToStringWithLeadingZero(tail, tailSize);
+			tailStr = this._removeEndingZeros(tailStr, tailSize);
+		}
+
 		if (!this._fractionalLength) {
 			throw new Error('_fractionalLength is not calculated');
 		}
@@ -160,6 +194,18 @@ export class PriceFormatter implements IFormatter {
 			fracString = numberToStringWithLeadingZero(fracPart * this._minMove, this._fractionalLength);
 		}
 
-		return intPart.toString() + formatterOptions.decimalSignFractional + fracString;
+		return intPart.toString() + formatterOptions.decimalSignFractional + fracString + tailStr;
+	}
+
+	private _removeEndingZeros(str: string, limit: number): string {
+		for (let i = 0; i < limit; i++) {
+			if (str[str.length - 1] === '0') {
+				str = str.substr(0, str.length - 1);
+			} else {
+				break;
+			}
+		}
+
+		return str;
 	}
 }
diff --git a/src/model/localization-options.ts b/src/model/localization-options.ts
index 6796798eb..8536035e2 100644
--- a/src/model/localization-options.ts
+++ b/src/model/localization-options.ts
@@ -1,9 +1,10 @@
 import { DateFormat } from '../formatters/date-format';
 
-import { PriceFormatterFn } from './price-formatter-fn';
+import { BarPrice } from './bar';
 import { BusinessDay, UTCTimestamp } from './time-data';
 
 export type TimeFormatterFn = (time: BusinessDay | UTCTimestamp) => string;
+export type PriceFormatterFn = (priceValue: BarPrice) => string;
 
 export interface LocalizationOptions {
 	/**
diff --git a/src/model/price-formatter-fn.ts b/src/model/price-formatter-fn.ts
deleted file mode 100644
index 530f3f706..000000000
--- a/src/model/price-formatter-fn.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-import { BarPrice } from './bar';
-
-export type PriceFormatterFn = (priceValue: BarPrice) => string;
diff --git a/src/model/series-options.ts b/src/model/series-options.ts
index b7a875fb9..e9aa31834 100644
--- a/src/model/series-options.ts
+++ b/src/model/series-options.ts
@@ -2,7 +2,6 @@ import { DeepPartial } from '../helpers/strict-type-checks';
 
 import { LineStyle, LineWidth } from '../renderers/draw-line';
 
-import { PriceFormatterFn } from './price-formatter-fn';
 import { PriceScaleMargins } from './price-scale';
 
 /** Structure describing a drawing style of the candlestick chart  */
@@ -86,8 +85,7 @@ export interface HistogramStyleOptions {
  * minMove = 0.01 , precision = 3. Prices will change like 1.130, 1.140, 1.150 etc.
  * minMove = 0.05 , precision is not specified. Prices will change like 1.10, 1.15, 1.20
  */
-
-export interface PriceFormatBuiltIn {
+export interface PriceFormat {
 	/**
 	 *  Enum of possible modes of price formatting
 	 * 'price' is the most common choice; it allows customization of precision and rounding of prices
@@ -106,20 +104,6 @@ export interface PriceFormatBuiltIn {
 	minMove: number;
 }
 
-export interface PriceFormatCustom {
-	type: 'custom';
-	/**
-	 * User-defined function for price formatting that could be used for some specific cases, that could not be covered with PriceFormatBuiltIn
-	 */
-	formatter: PriceFormatterFn;
-	/**
-	 * Minimal step of the price.
-	 */
-	minMove: number;
-}
-
-export type PriceFormat = PriceFormatBuiltIn | PriceFormatCustom;
-
 export function precisionByMinMove(minMove: number): number {
 	if (minMove >= 1) {
 		return 0;
diff --git a/src/model/series.ts b/src/model/series.ts
index d5a307631..044482055 100644
--- a/src/model/series.ts
+++ b/src/model/series.ts
@@ -258,10 +258,7 @@ export class Series<T extends SeriesType = SeriesType> extends PriceDataSource i
 			});
 		}
 
-		if (options.priceFormat !== undefined) {
-			this._recreateFormatter();
-		}
-
+		this._recreateFormatter();
 		this.model().updateSource(this);
 	}
 
@@ -400,11 +397,11 @@ export class Series<T extends SeriesType = SeriesType> extends PriceDataSource i
 				barsMinMax.min === barsMinMax.max ?
 					new PriceRange(barsMinMax.min - 0.5, barsMinMax.max + 0.5) : // special case: range consists of the only point
 					new PriceRange(barsMinMax.min, barsMinMax.max) :
-				new PriceRange(-0.5, 0.5);
+				null;
 
 		if (this.seriesType() === 'Histogram') {
 			const base = (this._options as HistogramStyleOptions).base;
-			range = range.merge(new PriceRange(base, base));
+			range = range !== null ? range.merge(new PriceRange(base, base)) : new PriceRange(base - 0.5, base + 0.5);
 		}
 
 		return range;
@@ -481,10 +478,6 @@ export class Series<T extends SeriesType = SeriesType> extends PriceDataSource i
 
 	private _recreateFormatter(): void {
 		switch (this._options.priceFormat.type) {
-			case 'custom': {
-				this._formatter = { format: this._options.priceFormat.formatter };
-				break;
-			}
 			case 'volume': {
 				this._formatter = new VolumeFormatter(this._options.priceFormat.precision);
 				break;
diff --git a/src/model/time-data.ts b/src/model/time-data.ts
index 76948597c..ccc2ef2d0 100644
--- a/src/model/time-data.ts
+++ b/src/model/time-data.ts
@@ -48,8 +48,27 @@ function upperBoundItemsCompare(time: TimePointIndex, item: TimedValue): boolean
 	return time < item.time;
 }
 
-export function visibleTimedValues(items: TimedValue[], range: BarsRange): SeriesItemsIndexesRange {
-	const from = lowerbound<TimedValue, TimePointIndex>(items, range.firstBar(), lowerBoundItemsCompare);
-	const to = upperbound<TimedValue, TimePointIndex>(items, range.lastBar(), upperBoundItemsCompare);
-	return { from, to };
+export function visibleTimedValues(items: TimedValue[], range: BarsRange, extendedRange: boolean): SeriesItemsIndexesRange {
+	const firstBar = range.firstBar();
+	const lastBar = range.lastBar();
+
+	const from = lowerbound<TimedValue, TimePointIndex>(items, firstBar, lowerBoundItemsCompare);
+	const to = upperbound<TimedValue, TimePointIndex>(items, lastBar, upperBoundItemsCompare);
+
+	if (!extendedRange) {
+		return { from, to };
+	}
+
+	let extendedFrom = from;
+	let extendedTo = to;
+
+	if (from > 0 && items[from].time >= firstBar) {
+		extendedFrom = from - 1;
+	}
+
+	if (to < items.length && to > 0 && items[to - 1].time <= lastBar) {
+		extendedTo = to + 1;
+	}
+
+	return { from: extendedFrom, to: extendedTo };
 }
diff --git a/src/views/pane/bars-pane-view-base.ts b/src/views/pane/bars-pane-view-base.ts
index 4df4ad280..684df671f 100644
--- a/src/views/pane/bars-pane-view-base.ts
+++ b/src/views/pane/bars-pane-view-base.ts
@@ -15,7 +15,7 @@ import { SeriesPaneViewBase } from './series-pane-view-base';
 
 export abstract class BarsPaneViewBase<TSeriesType extends 'Bar' | 'Candlestick', ItemType extends BarCandlestickItemBase> extends SeriesPaneViewBase<TSeriesType, ItemType> {
 	public constructor(series: Series<TSeriesType>, model: ChartModel) {
-		super(series, model);
+		super(series, model, false);
 	}
 
 	protected _convertToCoordinates(priceScale: PriceScale, timeScale: TimeScale, firstValue: number): void {
diff --git a/src/views/pane/histogram-pane-view.ts b/src/views/pane/histogram-pane-view.ts
index d78643285..040d0b989 100644
--- a/src/views/pane/histogram-pane-view.ts
+++ b/src/views/pane/histogram-pane-view.ts
@@ -44,7 +44,7 @@ export class SeriesHistogramPaneView extends SeriesPaneViewBase<'Histogram', Tim
 	private _sourceIndexes: Int32Array = new Int32Array(0);
 
 	public constructor(series: Series<'Histogram'>, model: ChartModel) {
-		super(series, model);
+		super(series, model, false);
 	}
 
 	public renderer(height: number, width: number): IPaneRenderer {
@@ -114,7 +114,7 @@ export class SeriesHistogramPaneView extends SeriesPaneViewBase<'Histogram', Tim
 			timeScale.indexesToCoordinates(data.items);
 			priceScale.pointsArrayToCoordinates(data.items, firstValue);
 			data.histogramBase = histogramBase;
-			data.visibleRange = visibleTimedValues(data.items, visibleBars);
+			data.visibleRange = visibleTimedValues(data.items, visibleBars, false);
 			this._sourceIndexes[colorIndex] = data.visibleRange.from;
 		});
 
diff --git a/src/views/pane/line-pane-view-base.ts b/src/views/pane/line-pane-view-base.ts
index 38d843d13..71fcd8966 100644
--- a/src/views/pane/line-pane-view-base.ts
+++ b/src/views/pane/line-pane-view-base.ts
@@ -14,7 +14,7 @@ import { SeriesPaneViewBase } from './series-pane-view-base';
 
 export abstract class LinePaneViewBase<TSeriesType extends 'Line' | 'Area', ItemType extends PricedValue & TimedValue> extends SeriesPaneViewBase<TSeriesType, ItemType> {
 	protected constructor(series: Series<TSeriesType>, model: ChartModel) {
-		super(series, model);
+		super(series, model, true);
 	}
 
 	protected _convertToCoordinates(priceScale: PriceScale, timeScale: TimeScale, firstValue: number): void {
diff --git a/src/views/pane/series-pane-view-base.ts b/src/views/pane/series-pane-view-base.ts
index f029f426c..e82986ca3 100644
--- a/src/views/pane/series-pane-view-base.ts
+++ b/src/views/pane/series-pane-view-base.ts
@@ -9,16 +9,18 @@ import { IPaneRenderer } from '../../renderers/ipane-renderer';
 import { IUpdatablePaneView, UpdateType } from './iupdatable-pane-view';
 
 export abstract class SeriesPaneViewBase<TSeriesType extends SeriesType, ItemType extends TimedValue> implements IUpdatablePaneView {
-	protected _series: Series<TSeriesType>;
-	protected _model: ChartModel;
+	protected readonly _series: Series<TSeriesType>;
+	protected readonly _model: ChartModel;
 	protected _invalidated: boolean = true;
 	protected _dataInvalidated: boolean = true;
 	protected _items: ItemType[] = [];
 	protected _itemsVisibleRange: SeriesItemsIndexesRange | null = null;
+	private readonly _extendedVisibleRange: boolean;
 
-	public constructor(series: Series<TSeriesType>, model: ChartModel) {
+	public constructor(series: Series<TSeriesType>, model: ChartModel, extendedVisibleRange: boolean) {
 		this._series = series;
 		this._model = model;
+		this._extendedVisibleRange = extendedVisibleRange;
 	}
 
 	public update(updateType?: UpdateType): void {
@@ -70,7 +72,7 @@ export abstract class SeriesPaneViewBase<TSeriesType extends SeriesType, ItemTyp
 			return;
 		}
 
-		this._itemsVisibleRange = visibleTimedValues(this._items, visibleBars);
+		this._itemsVisibleRange = visibleTimedValues(this._items, visibleBars, this._extendedVisibleRange);
 		this._convertToCoordinates(priceScale, timeScale, firstValue);
 	}
 }


Test patch:
diff --git a/tests/e2e/graphics/test-cases/initial-options/custom-price-format.js b/tests/e2e/graphics/test-cases/initial-options/custom-price-format.js
deleted file mode 100644
index eed087393..000000000
--- a/tests/e2e/graphics/test-cases/initial-options/custom-price-format.js
+++ /dev/null
@@ -1,34 +0,0 @@
-function generateData() {
-	var res = [];
-	var time = new Date(Date.UTC(2018, 0, 1, 0, 0, 0, 0));
-	for (var i = 0; i < 500; ++i) {
-		res.push({
-			time: time.getTime() / 1000,
-			value: i,
-		});
-
-		time.setUTCDate(time.getUTCDate() + 1);
-	}
-	return res;
-}
-
-// eslint-disable-next-line no-unused-vars
-function runTestCase(container) {
-	var chart = LightweightCharts.createChart(container);
-
-	var areaSeries = chart.addAreaSeries({
-		priceFormat: {
-			type: 'custom',
-			minMove: 0.01,
-			formatter: function(price) {
-				return '\u0024' + price.toFixed(2);
-			},
-		},
-	});
-
-	areaSeries.setData(generateData());
-
-	var lineSeries = chart.addLineSeries();
-
-	lineSeries.setData(generateData());
-}
diff --git a/tests/e2e/graphics/test-cases/series/2-points-line-series.js b/tests/e2e/graphics/test-cases/series/2-points-line-series.js
new file mode 100644
index 000000000..13ebaec0b
--- /dev/null
+++ b/tests/e2e/graphics/test-cases/series/2-points-line-series.js
@@ -0,0 +1,42 @@
+function generateBar(i, target) {
+	var step = (i % 20) / 1000;
+	var base = i / 5;
+	target.open = base * (1 - step);
+	target.high = base * (1 + 2 * step);
+	target.low = base * (1 - 2 * step);
+	target.close = base * (1 + step);
+}
+
+function generateData() {
+	var res = [];
+	var time = new Date(Date.UTC(2018, 0, 1, 0, 0, 0, 0));
+	for (var i = 0; i < 500; ++i) {
+		var item = {
+			time: time.getTime() / 1000,
+		};
+		time.setUTCDate(time.getUTCDate() + 1);
+
+		generateBar(i, item);
+		res.push(item);
+	}
+	return res;
+}
+
+// eslint-disable-next-line no-unused-vars
+function runTestCase(container) {
+	var chart = LightweightCharts.createChart(container);
+
+	var mainSeries = chart.addBarSeries();
+	var lineSeries = chart.addLineSeries();
+
+	var data = generateData();
+	var lineData = [
+		{ time: data[0].time, value: data[0].close },
+		{ time: data[data.length - 1].time, value: data[data.length - 1].close },
+	];
+
+	mainSeries.setData(data);
+	lineSeries.setData(lineData);
+
+	chart.timeScale().scrollToPosition(-5);
+}
diff --git a/tests/unittests/formatters.spec.ts b/tests/unittests/formatters.spec.ts
index 2660ec92c..21cf3b258 100644
--- a/tests/unittests/formatters.spec.ts
+++ b/tests/unittests/formatters.spec.ts
@@ -55,9 +55,9 @@ describe('Formatters', () => {
 			expect(res).to.be.equal('1.500');
 		}
 		{
-			const formatter = new PriceFormatter();
-			const res = formatter.format(-1.5);
-			expect(res).to.be.equal('\u22121.50');
+			const formatter = new PriceFormatter(1000);
+			const res = formatter.format(1.5, true);
+			expect(res).to.be.equal('+1.500');
 		}
 	});
 	it('time-formatter', () => {
diff --git a/tests/unittests/timed-data.spec.ts b/tests/unittests/timed-data.spec.ts
index 67598b96a..8542793a8 100644
--- a/tests/unittests/timed-data.spec.ts
+++ b/tests/unittests/timed-data.spec.ts
@@ -7,24 +7,38 @@ import { TimePointIndex, visibleTimedValues } from '../../src/model/time-data';
 
 // TODO: add tests for marks spans
 
-function visibleTimedValuesCase(rangeFrom: number, rangeTo: number, expectedFrom: number, expectedTo: number, times: number[]): void {
-	const barsRange = new BarsRange(rangeFrom as TimePointIndex, 3 as TimePointIndex);
+function visibleTimedValuesCase(
+	rangeFrom: number,
+	rangeTo: number,
+	extendedRange: boolean,
+	expectedFrom: number,
+	expectedTo: number,
+	times: number[]
+): void {
+	const barsRange = new BarsRange(rangeFrom as TimePointIndex, rangeTo as TimePointIndex);
 	const timedData = times.map((t: number) => {
 		return { time: t as TimePointIndex, x: 0 as Coordinate };
 	});
-	const actual = visibleTimedValues(timedData, barsRange);
+	const actual = visibleTimedValues(timedData, barsRange, extendedRange);
 	const expected = { from: expectedFrom, to: expectedTo };
 	expect(actual).to.be.deep.equal(expected);
 }
 
 describe('TimedData', () => {
 	it('visibleTimedValues', () => {
-		visibleTimedValuesCase(1, 3, 0, 0, []);
-		visibleTimedValuesCase(1, 3, 0, 1, [1]);
-		visibleTimedValuesCase(1, 3, 0, 2, [1, 2, 5]);
-		visibleTimedValuesCase(1, 3, 1, 2, [-1, 2, 5]);
-		visibleTimedValuesCase(1, 3, 1, 1, [-1, 5]);
-		visibleTimedValuesCase(1, 3, 0, 0, [4, 5]);
-		visibleTimedValuesCase(1, 3, 2, 2, [-2, -1]);
+		visibleTimedValuesCase(1, 3, false, 0, 0, []);
+		visibleTimedValuesCase(1, 3, false, 0, 1, [1]);
+		visibleTimedValuesCase(1, 3, false, 0, 2, [1, 2, 5]);
+		visibleTimedValuesCase(1, 3, false, 1, 2, [-1, 2, 5]);
+		visibleTimedValuesCase(1, 3, false, 1, 1, [-1, 5]);
+		visibleTimedValuesCase(1, 3, false, 0, 0, [4, 5]);
+		visibleTimedValuesCase(1, 3, false, 2, 2, [-2, -1]);
+	});
+
+	it('visibleTimedValues with exteded range', () => {
+		visibleTimedValuesCase(1, 3, true, 0, 0, []);
+		visibleTimedValuesCase(1, 3, true, 0, 1, [1]);
+		visibleTimedValuesCase(1, 3, true, 0, 3, [1, 2, 5]);
+		visibleTimedValuesCase(1, 3, true, 1, 4, [-2, -1, 2, 5, 6]);
 	});
 });