Repository: tradingview/lightweight-charts (ts)

Issue:
The "color" property in MouseEventParams.seriesData for a Line series is not defined, even though it was passed through SeriesApi.setData
**Lightweight Charts Version:** 4.0.0

**Steps/code to reproduce:**

1. Create Line series;
2. Set LineData with color property;
3. Subscribe to corshair move;
4. Hover over the chart;

**Actual behavior:**

MouseEventParams.seriesData doesn't contain 'color' propery

**Expected behavior:**

MouseEventParams.seriesData should contain 'color' property


Pull request description:
Add 'color' property to MouseEventParams.seriesData for Line series
**Type of PR:** bugfix

**PR checklist:**

- [ ] Addresses an existing issue: fixes #1057
- [ ] Includes tests
- [ ] Documentation update

**Overview of change:**
Add 'color' property to MouseEventParams.seriesData for Line series


Patch:
diff --git a/.size-limit.js b/.size-limit.js
index ebeac178d..489fff200 100644
--- a/.size-limit.js
+++ b/.size-limit.js
@@ -4,11 +4,11 @@ module.exports = [
 	{
 		name: 'ESM',
 		path: 'dist/lightweight-charts.esm.production.js',
-		limit: '43 KB',
+		limit: '43.02 KB',
 	},
 	{
 		name: 'Standalone',
 		path: 'dist/lightweight-charts.standalone.production.js',
-		limit: '43.55 KB',
+		limit: '43.58 KB',
 	},
 ];
diff --git a/src/api/get-series-data-creator.ts b/src/api/get-series-data-creator.ts
index 7f365d554..a0996ca71 100644
--- a/src/api/get-series-data-creator.ts
+++ b/src/api/get-series-data-creator.ts
@@ -1,30 +1,44 @@
 import { PlotRow, PlotRowValueIndex } from '../model/plot-data';
-import { HistogramPlotRow, SeriesPlotRow } from '../model/series-data';
+import {
+	BarPlotRow,
+	CandlestickPlotRow,
+	LinePlotRow,
+	SeriesPlotRow,
+} from '../model/series-data';
 import { SeriesType } from '../model/series-options';
 import { Time } from '../model/time-data';
 
-import { BarData, HistogramData, LineData, SeriesDataItemTypeMap } from './data-consumer';
+import {
+	BarData,
+	CandlestickData,
+	LineData,
+	OhlcData,
+	SeriesDataItemTypeMap,
+	SingleValueData,
+} from './data-consumer';
 
 type SeriesPlotRowToDataMap = {
 	[T in keyof SeriesDataItemTypeMap]: (plotRow: SeriesPlotRow) => SeriesDataItemTypeMap[T];
 };
 
-function lineData(plotRow: PlotRow): LineData {
+function singleValueData(plotRow: PlotRow): SingleValueData {
 	return {
 		value: plotRow.value[PlotRowValueIndex.Close],
 		time: plotRow.originalTime as unknown as Time,
 	};
 }
 
-function histogramData(plotRow: HistogramPlotRow): HistogramData {
-	return {
-		value: plotRow.value[PlotRowValueIndex.Close],
-		color: plotRow.color,
-		time: plotRow.originalTime as unknown as Time,
-	};
+function lineData(plotRow: LinePlotRow): LineData {
+	const result: LineData = singleValueData(plotRow);
+
+	if (plotRow.color !== undefined) {
+		result.color = plotRow.color;
+	}
+
+	return result;
 }
 
-function barData(plotRow: PlotRow): BarData {
+function ohlcData(plotRow: PlotRow): OhlcData {
 	return {
 		open: plotRow.value[PlotRowValueIndex.Open],
 		high: plotRow.value[PlotRowValueIndex.High],
@@ -34,13 +48,42 @@ function barData(plotRow: PlotRow): BarData {
 	};
 }
 
+function barData(plotRow: BarPlotRow): BarData {
+	const result: BarData = ohlcData(plotRow);
+
+	if (plotRow.color !== undefined) {
+		result.color = plotRow.color;
+	}
+
+	return result;
+}
+
+function candlestickData(plotRow: CandlestickPlotRow): CandlestickData {
+	const result: CandlestickData = ohlcData(plotRow);
+	const { color, borderColor, wickColor } = plotRow;
+
+	if (color !== undefined) {
+		result.color = color;
+	}
+
+	if (borderColor !== undefined) {
+		result.borderColor = borderColor;
+	}
+
+	if (wickColor !== undefined) {
+		result.wickColor = wickColor;
+	}
+
+	return result;
+}
+
 const seriesPlotRowToDataMap: SeriesPlotRowToDataMap = {
-	Area: lineData,
+	Area: singleValueData,
 	Line: lineData,
-	Baseline: lineData,
-	Histogram: histogramData,
+	Baseline: singleValueData,
+	Histogram: lineData,
 	Bar: barData,
-	Candlestick: barData,
+	Candlestick: candlestickData,
 };
 
 export function getSeriesDataCreator<TSeriesType extends SeriesType>(seriesType: TSeriesType): (plotRow: SeriesPlotRow<TSeriesType>) => SeriesDataItemTypeMap[TSeriesType] {


Test patch:
diff --git a/tests/e2e/graphics/test-cases/api/series-data-by-index.js b/tests/e2e/graphics/test-cases/api/series-data-by-index.js
index 6a1d33778..83d1bb7f4 100644
--- a/tests/e2e/graphics/test-cases/api/series-data-by-index.js
+++ b/tests/e2e/graphics/test-cases/api/series-data-by-index.js
@@ -1,11 +1,105 @@
-function compare(obj1, obj2) {
+function compareAreaData(obj1, obj2) {
 	return obj1.time === obj2.time && obj1.value === obj2.value;
 }
 
+function compareLineData(obj1, obj2) {
+	return obj1.time === obj2.time
+		&& obj1.value === obj2.value
+		&& obj1.color === obj2.color;
+}
+
+function compareCandlestickData(obj1, obj2) {
+	return obj1.time === obj2.time
+		&& obj1.value === obj2.value
+		&& obj1.color === obj2.color
+		&& obj1.borderColor === obj2.borderColor
+		&& obj1.wickColor === obj2.wickColor;
+}
+
+function generateCandlestickData() {
+	const result = [];
+	const time = new Date(Date.UTC(2018, 0, 1, 0, 0, 0, 0));
+
+	for (let i = 0; i < 10; i++) {
+		result.push({
+			time: time.getTime() / 1000,
+			open: 4 + i,
+			high: 9 + i,
+			low: i,
+			close: 7 + i,
+			color: i < 5 ? undefined : 'red',
+			borderColor: i === 7 ? 'blue' : undefined,
+			wickColor: i > 3 ? 'green' : undefined,
+		});
+
+		time.setUTCDate(time.getUTCDate() + 1);
+	}
+
+	return result;
+}
+
 function runTestCase(container) {
 	const chart = LightweightCharts.createChart(container);
-	const series = chart.addAreaSeries();
-	series.setData([
+
+	const lineSeries = chart.addLineSeries();
+	lineSeries.setData([
+		{ time: '1990-04-24', value: 0 },
+		{ time: '1990-04-25', value: 1 },
+		{ time: '1990-04-26', value: 2 },
+		{ time: '1990-04-27', value: 3 },
+		{ time: '1990-04-28', value: 4, color: 'green' },
+		{ time: '1990-04-29', value: 5, color: 'red' },
+	]);
+
+	console.assert(
+		compareLineData(lineSeries.dataByIndex(0), { time: '1990-04-24', value: 0 }),
+		`objects should be equal: ${JSON.stringify(lineSeries.dataByIndex(0))} !== ${JSON.stringify({ time: '1990-04-24', value: 0 })}`
+	);
+	console.assert(
+		compareLineData(lineSeries.dataByIndex(5), { time: '1990-04-29', value: 5, color: 'red' }),
+		`objects should be equal: ${JSON.stringify(lineSeries.dataByIndex(5))} !== ${JSON.stringify({ time: '1990-04-29', value: 5, color: 'red' })}`
+	);
+
+	chart.removeSeries(lineSeries);
+
+	const candlestickSeries = chart.addCandlestickSeries();
+	candlestickSeries.setData(generateCandlestickData());
+
+	console.assert(
+		compareCandlestickData(candlestickSeries.dataByIndex(0), { time: 1514764800, open: 4, high: 9, low: 0, close: 7 }),
+		`objects should be equal: ${JSON.stringify(candlestickSeries.dataByIndex(0))} !== ${JSON.stringify({ time: 1514764800, open: 4, high: 9, low: 0, close: 7 })}`
+	);
+
+	console.assert(
+		compareCandlestickData(
+			candlestickSeries.dataByIndex(7),
+			{
+				time: 1515369600,
+				open: 11,
+				high: 16,
+				low: 7,
+				close: 14,
+				color: 'red',
+				borderColor: 'blue',
+				wickColor: 'green',
+			}
+		),
+		`objects should be equal: ${JSON.stringify(candlestickSeries.dataByIndex(7))} !== ${JSON.stringify({
+			time: 1515369600,
+			open: 11,
+			high: 16,
+			low: 7,
+			close: 14,
+			color: 'red',
+			borderColor: 'blue',
+			wickColor: 'green',
+		})}`
+	);
+
+	chart.removeSeries(candlestickSeries);
+
+	const areaSeries = chart.addAreaSeries();
+	areaSeries.setData([
 		{ time: '1990-04-24', value: 0 },
 		{ time: '1990-04-25', value: 1 },
 		{ time: '1990-04-27', value: 2 },
@@ -14,17 +108,17 @@ function runTestCase(container) {
 		{ time: '1990-04-30', value: 5 },
 	]);
 
-	console.assert(series.dataByIndex(-1) === null, 'should return null for logical index that is outside of data');
-	console.assert(compare(series.dataByIndex(0), { time: '1990-04-24', value: 0 }), 'incorrect item at index 0: ' + JSON.stringify(series.dataByIndex(0)));
+	console.assert(areaSeries.dataByIndex(-1) === null, 'should return null for logical index that is outside of data');
+	console.assert(compareAreaData(areaSeries.dataByIndex(0), { time: '1990-04-24', value: 0 }), 'incorrect item at index 0: ' + JSON.stringify(areaSeries.dataByIndex(0)));
 	console.assert(
-		compare(series.dataByIndex(-1, LightweightCharts.MismatchDirection.NearestRight), series.dataByIndex(0)),
+		compareAreaData(areaSeries.dataByIndex(-1, LightweightCharts.MismatchDirection.NearestRight), areaSeries.dataByIndex(0)),
 		'should return nearest right item if mismatch direction is MismatchDirection.NearestRight'
 	);
 
-	console.assert(series.dataByIndex(6) === null, 'should return null for logical index that is outside of data');
-	console.assert(compare(series.dataByIndex(5), { time: '1990-04-30', value: 5 }), 'incorrect item at index 5: ' + JSON.stringify(series.dataByIndex(5)));
+	console.assert(areaSeries.dataByIndex(6) === null, 'should return null for logical index that is outside of data');
+	console.assert(compareAreaData(areaSeries.dataByIndex(5), { time: '1990-04-30', value: 5 }), 'incorrect item at index 5: ' + JSON.stringify(areaSeries.dataByIndex(5)));
 	console.assert(
-		compare(series.dataByIndex(6, LightweightCharts.MismatchDirection.NearestLeft), series.dataByIndex(5)),
+		compareAreaData(areaSeries.dataByIndex(6, LightweightCharts.MismatchDirection.NearestLeft), areaSeries.dataByIndex(5)),
 		'should return nearest left item if mismatch direction is MismatchDirection.NearestLeft'
 	);
 
@@ -32,7 +126,7 @@ function runTestCase(container) {
 		watermark: {
 			color: 'red',
 			visible: true,
-			text: `${series.dataByIndex(3).time}`,
+			text: `${areaSeries.dataByIndex(3).time}`,
 		},
 	});
 }
diff --git a/tests/unittests/get-series-data-creator.spec.ts b/tests/unittests/get-series-data-creator.spec.ts
new file mode 100644
index 000000000..1e5e4113e
--- /dev/null
+++ b/tests/unittests/get-series-data-creator.spec.ts
@@ -0,0 +1,161 @@
+import { expect } from 'chai';
+import { describe, it } from 'mocha';
+
+import { getSeriesDataCreator } from '../../src/api/get-series-data-creator';
+import { PlotRow } from '../../src/model/plot-data';
+import { BarPlotRow, CandlestickPlotRow, HistogramPlotRow, LinePlotRow } from '../../src/model/series-data';
+import { OriginalTime, TimePointIndex, UTCTimestamp } from '../../src/model/time-data';
+
+const plotRow: PlotRow = {
+	index: 0 as TimePointIndex,
+	time: { timestamp: 1649931070 as UTCTimestamp },
+	value: [1, 2, 3, 4],
+	originalTime: 1649931070 as unknown as OriginalTime,
+};
+
+const linePlotRows: LinePlotRow[] = [
+	{
+		...plotRow,
+		color: '#FF0000',
+	},
+	plotRow,
+];
+const areaPlotRows: PlotRow = plotRow;
+const baselinePlotRows: PlotRow = plotRow;
+const histogramPlotRow: HistogramPlotRow[] = [
+	{
+		...plotRow,
+		color: '#00FF00',
+	},
+	plotRow,
+];
+const barPlotRow: BarPlotRow[] = [
+	{
+		...plotRow,
+		color: '#0000FF',
+	},
+	plotRow,
+];
+const candlestickPlotRows: CandlestickPlotRow[] = [
+	{
+		...plotRow,
+		color: '#0000FF',
+	},
+	plotRow,
+	{
+		...plotRow,
+		borderColor: '#FFFF00',
+	},
+	{
+		...plotRow,
+		wickColor: '#FF00FF',
+	},
+	{
+		...plotRow,
+		color: '#FF0000',
+		borderColor: '#00FF00',
+		wickColor: '#0000FF',
+	},
+];
+
+describe('getSeriesDataCreator', () => {
+	it('Line', () => {
+		expect(getSeriesDataCreator('Line')(linePlotRows[0])).to.deep.equal({
+			value: 4,
+			time: 1649931070,
+			color: '#FF0000',
+		});
+		expect(getSeriesDataCreator('Line')(linePlotRows[1])).to.deep.equal({
+			value: 4,
+			time: 1649931070,
+		});
+	});
+
+	it('Area', () => {
+		expect(getSeriesDataCreator('Area')(areaPlotRows)).to.deep.equal({
+			value: 4,
+			time: 1649931070,
+		});
+	});
+
+	it('Baseline', () => {
+		expect(getSeriesDataCreator('Baseline')(baselinePlotRows)).to.deep.equal({
+			value: 4,
+			time: 1649931070,
+		});
+	});
+
+	it('Histogram', () => {
+		expect(getSeriesDataCreator('Histogram')(histogramPlotRow[0])).to.deep.equal({
+			value: 4,
+			time: 1649931070,
+			color: '#00FF00',
+		});
+		expect(getSeriesDataCreator('Histogram')(histogramPlotRow[1])).to.deep.equal({
+			value: 4,
+			time: 1649931070,
+		});
+	});
+
+	it('Bar', () => {
+		expect(getSeriesDataCreator('Bar')(barPlotRow[0])).to.deep.equal({
+			open: 1,
+			high: 2,
+			low: 3,
+			close: 4,
+			time: 1649931070,
+			color: '#0000FF',
+		});
+		expect(getSeriesDataCreator('Bar')(barPlotRow[1])).to.deep.equal({
+			open: 1,
+			high: 2,
+			low: 3,
+			close: 4,
+			time: 1649931070,
+		});
+	});
+
+	it('Candlestick', () => {
+		expect(getSeriesDataCreator('Candlestick')(candlestickPlotRows[0])).to.deep.equal({
+			open: 1,
+			high: 2,
+			low: 3,
+			close: 4,
+			time: 1649931070,
+			color: '#0000FF',
+		});
+		expect(getSeriesDataCreator('Candlestick')(candlestickPlotRows[1])).to.deep.equal({
+			open: 1,
+			high: 2,
+			low: 3,
+			close: 4,
+			time: 1649931070,
+		});
+		expect(getSeriesDataCreator('Candlestick')(candlestickPlotRows[2])).to.deep.equal({
+			open: 1,
+			high: 2,
+			low: 3,
+			close: 4,
+			time: 1649931070,
+			borderColor: '#FFFF00',
+		});
+		expect(getSeriesDataCreator('Candlestick')(candlestickPlotRows[3])).to.deep.equal({
+			open: 1,
+			high: 2,
+			low: 3,
+			close: 4,
+			time: 1649931070,
+			wickColor: '#FF00FF',
+		});
+		expect(getSeriesDataCreator('Candlestick')(candlestickPlotRows[4])).to.deep.equal({
+			open: 1,
+			high: 2,
+			low: 3,
+			close: 4,
+			time: 1649931070,
+			color: '#FF0000',
+			borderColor: '#00FF00',
+			wickColor: '#0000FF',
+		});
+	});
+});