Skip to content

Commit

Permalink
Added support for having custom colors per data item for line series
Browse files Browse the repository at this point in the history
Fixes #195 (the second part)
  • Loading branch information
timocov committed Jan 10, 2022
1 parent 005e518 commit 3410a4b
Show file tree
Hide file tree
Showing 9 changed files with 126 additions and 18 deletions.
22 changes: 16 additions & 6 deletions src/api/data-consumer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,9 @@ export interface WhitespaceData {
}

/**
* Represents a data point for a line or area series.
* A base interface for a data point of single-value series.
*/
export interface LineData {
export interface SingleValueData {
/**
* The time of the data.
*/
Expand All @@ -76,12 +76,22 @@ export interface LineData {
value: number;
}

/**
* Structure describing a single item of data for line series
*/
export interface LineData extends SingleValueData {
/**
* Optional color value for certain data item. If missed, color from options is used
*/
color?: string;
}

/**
* Structure describing a single item of data for histogram series
*/
export interface HistogramData extends LineData {
export interface HistogramData extends SingleValueData {
/**
* Optional color value for certain data item. If missed, color from HistogramSeriesOptions is used
* Optional color value for certain data item. If missed, color from options is used
*/
color?: string;
}
Expand Down Expand Up @@ -166,11 +176,11 @@ export interface SeriesDataItemTypeMap {
/**
* The types of area series data.
*/
Area: LineData | WhitespaceData;
Area: SingleValueData | WhitespaceData;
/**
* The types of baseline series data.
*/
Baseline: LineData | WhitespaceData;
Baseline: SingleValueData | WhitespaceData;
/**
* The types of line series data.
*/
Expand Down
2 changes: 1 addition & 1 deletion src/api/get-series-plot-row-creator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { BarData, CandlestickData, HistogramData, isWhitespaceData, LineData, Se
function getLineBasedSeriesPlotRow(time: TimePoint, index: TimePointIndex, item: LineData | HistogramData): Mutable<SeriesPlotRow<'Line' | 'Histogram' | 'Area' | 'Baseline'>> {
const val = item.value;

const res: Mutable<SeriesPlotRow<'Histogram'>> = { index, time, value: [val, val, val, val] };
const res: Mutable<SeriesPlotRow<'Histogram' | 'Line'>> = { index, time, value: [val, val, val, val] };

// 'color' here is public property (from API) so we can use `in` here safely
// eslint-disable-next-line no-restricted-syntax
Expand Down
8 changes: 5 additions & 3 deletions src/model/series-bar-colorer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export class SeriesBarColorer {
const seriesOptions = this._series.options();
switch (targetType) {
case 'Line':
return this._lineStyle(seriesOptions as LineStyleOptions);
return this._lineStyle(seriesOptions as LineStyleOptions, barIndex, precomputedBars);

case 'Area':
return this._areaStyle(seriesOptions as AreaStyleOptions);
Expand Down Expand Up @@ -126,10 +126,12 @@ export class SeriesBarColorer {
};
}

private _lineStyle(lineStyle: LineStyleOptions): BarColorerStyle {
private _lineStyle(lineStyle: LineStyleOptions, barIndex: TimePointIndex, precomputedBars?: PrecomputedBars): BarColorerStyle {
const currentBar = ensureNotNull(this._findBar(barIndex, precomputedBars)) as SeriesPlotRow<'Line'>;

return {
...emptyResult,
barColor: lineStyle.color,
barColor: currentBar.color ?? lineStyle.color,
};
}

Expand Down
6 changes: 5 additions & 1 deletion src/model/series-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ import { PlotRow } from './plot-data';
import { PlotList } from './plot-list';
import { SeriesType } from './series-options';

export interface LinePlotRow extends PlotRow {
readonly color?: string;
}

export interface HistogramPlotRow extends PlotRow {
readonly color?: string;
}
Expand All @@ -21,7 +25,7 @@ export interface SeriesPlotRowTypeAtTypeMap {
Candlestick: CandlestickPlotRow;
Area: PlotRow;
Baseline: PlotRow;
Line: PlotRow;
Line: LinePlotRow;
Histogram: HistogramPlotRow;
}

Expand Down
67 changes: 64 additions & 3 deletions src/renderers/line-renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { LinePoint, LineStyle, LineType, LineWidth, setLineStyle } from './draw-
import { ScaledRenderer } from './scaled-renderer';
import { walkLine } from './walk-line';

export type LineItem = TimedValue & PricedValue & LinePoint;
export type LineItem = TimedValue & PricedValue & LinePoint & { color?: string };

export interface PaneRendererLineDataBase {
lineType: LineType;
Expand Down Expand Up @@ -40,15 +40,26 @@ export abstract class PaneRendererLineBase<TData extends PaneRendererLineDataBas
ctx.strokeStyle = this._strokeStyle(ctx);
ctx.lineJoin = 'round';

ctx.beginPath();
if (this._data.items.length === 1) {
ctx.beginPath();

const point = this._data.items[0];
ctx.moveTo(point.x - this._data.barWidth / 2, point.y);
ctx.lineTo(point.x + this._data.barWidth / 2, point.y);

if (point.color !== undefined) {
ctx.strokeStyle = point.color;
}

ctx.stroke();
} else {
walkLine(ctx, this._data.items, this._data.lineType, this._data.visibleRange);
this._drawLine(ctx, this._data);
}
}

protected _drawLine(ctx: CanvasRenderingContext2D, data: TData): void {
ctx.beginPath();
walkLine(ctx, data.items, data.lineType, data.visibleRange as SeriesItemsIndexesRange);
ctx.stroke();
}

Expand All @@ -60,6 +71,56 @@ export interface PaneRendererLineData extends PaneRendererLineDataBase {
}

export class PaneRendererLine extends PaneRendererLineBase<PaneRendererLineData> {
/**
* Similar to {@link walkLine}, but supports color changes
*/
protected override _drawLine(ctx: CanvasRenderingContext2D, data: PaneRendererLineData): void {
const { items, visibleRange, lineType, lineColor } = data;
if (items.length === 0 || visibleRange === null) {
return;
}

ctx.beginPath();

const firstItem = items[visibleRange.from];
ctx.moveTo(firstItem.x, firstItem.y);

let prevStrokeStyle = firstItem.color ?? lineColor;
ctx.strokeStyle = prevStrokeStyle;

const changeColor = (color: string) => {
ctx.stroke();
ctx.beginPath();
ctx.strokeStyle = color;
prevStrokeStyle = color;
};

for (let i = visibleRange.from + 1; i < visibleRange.to; ++i) {
const currItem = items[i];
const prevItem = items[i - 1];

const currentStrokeStyle = currItem.color ?? lineColor;

if (lineType === LineType.WithSteps) {
ctx.lineTo(currItem.x, prevItem.y);

if (currentStrokeStyle !== prevStrokeStyle) {
changeColor(currentStrokeStyle);
ctx.moveTo(currItem.x, prevItem.y);
}
}

ctx.lineTo(currItem.x, currItem.y);

if (lineType !== LineType.WithSteps && currentStrokeStyle !== prevStrokeStyle) {
changeColor(currentStrokeStyle);
ctx.moveTo(currItem.x, currItem.y);
}
}

ctx.stroke();
}

protected override _strokeStyle(): CanvasRenderingContext2D['strokeStyle'] {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return this._data!.lineColor;
Expand Down
7 changes: 5 additions & 2 deletions src/views/pane/line-pane-view.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { BarPrice } from '../../model/bar';
import { ChartModel } from '../../model/chart-model';
import { Series } from '../../model/series';
import { SeriesBarColorer } from '../../model/series-bar-colorer';
import { TimePointIndex } from '../../model/time-data';
import { IPaneRenderer } from '../../renderers/ipane-renderer';
import { LineItem, PaneRendererLine, PaneRendererLineData } from '../../renderers/line-renderer';
Expand Down Expand Up @@ -38,7 +39,9 @@ export class SeriesLinePaneView extends LinePaneViewBase<'Line', LineItem> {
return this._lineRenderer;
}

protected _createRawItem(time: TimePointIndex, price: BarPrice): LineItem {
return this._createRawItemBase(time, price);
protected _createRawItem(time: TimePointIndex, price: BarPrice, colorer: SeriesBarColorer): LineItem {
const item = this._createRawItemBase(time, price) as LineItem;
item.color = colorer.barStyle(time).barColor;
return item;
}
}
13 changes: 13 additions & 0 deletions tests/e2e/graphics/test-cases/series/line-with-custom-color.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
function runTestCase(container) {
const chart = LightweightCharts.createChart(container);

const mainSeries = chart.addLineSeries();

mainSeries.setData([
{ time: 1, value: 1, color: 'red' },
{ time: 2, value: 2, color: 'green' },
{ time: 3, value: 1, color: 'blue' },
]);

chart.timeScale().fitContent();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
function runTestCase(container) {
const chart = LightweightCharts.createChart(container);

const mainSeries = chart.addLineSeries({
lineType: LightweightCharts.LineType.WithSteps,
});

mainSeries.setData([
{ time: 1, value: 1, color: 'red' },
{ time: 2, value: 2, color: 'green' },
{ time: 3, value: 1, color: 'blue' },
]);

chart.timeScale().fitContent();
}
4 changes: 2 additions & 2 deletions website/docs/series-types.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ If you'd like to change any option of a series, you could do this in different w
## Area

- **Method to create**: [`IChartApi.addAreaSeries`](/api/interfaces/IChartApi.md#addareaseries)
- **Data format**: [`LineData`](/api/interfaces/LineData.md) or [`WhitespaceData`](/api/interfaces/WhitespaceData.md)
- **Data format**: [`SingleValueData`](/api/interfaces/SingleValueData.md) or [`WhitespaceData`](/api/interfaces/WhitespaceData.md)
- **Style options**: a mix of [`SeriesOptionsCommon`](/api/interfaces/SeriesOptionsCommon.md) and [`AreaStyleOptions`](/api/interfaces/AreaStyleOptions.md)

An area chart is basically a colored area between the line connecting all data points and [the time scale](./time-scale.md):
Expand All @@ -60,7 +60,7 @@ Open & Close values are represented by tick marks, on the left & right hand side
## Baseline

- **Method to create**: [`IChartApi.addBaselineSeries`](/api/interfaces/IChartApi.md#addbaselineseries)
- **Data format**: [`LineData`](/api/interfaces/LineData.md) or [`WhitespaceData`](/api/interfaces/WhitespaceData.md)
- **Data format**: [`SingleValueData`](/api/interfaces/SingleValueData.md) or [`WhitespaceData`](/api/interfaces/WhitespaceData.md)
- **Style options**: a mix of [`SeriesOptionsCommon`](/api/interfaces/SeriesOptionsCommon.md) and [`BaselineStyleOptions`](/api/interfaces/BaselineStyleOptions.md)

A baseline is basically two colored areas (top and bottom) between the line connecting all data points and [the base value line](/api/interfaces/BaselineStyleOptions.md#basevalue):
Expand Down

0 comments on commit 3410a4b

Please sign in to comment.