Skip to content

Commit

Permalink
Added support for having custom colors per data item for candlestick …
Browse files Browse the repository at this point in the history
…and bar series

Fixes #195 (the first part)
  • Loading branch information
timocov committed Jan 10, 2022
1 parent 45cef7b commit 005e518
Show file tree
Hide file tree
Showing 7 changed files with 166 additions and 19 deletions.
32 changes: 30 additions & 2 deletions src/api/data-consumer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ export interface HistogramData extends LineData {
/**
* Represents a bar with a {@link Time} and open, high, low, and close prices.
*/
export interface BarData {
export interface OhlcData {
/**
* The bar time.
*/
Expand All @@ -113,6 +113,34 @@ export interface BarData {
close: number;
}

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

/**
* Structure describing a single item of data for candlestick series
*/
export interface CandlestickData extends OhlcData {
/**
* Optional color value for certain data item. If missed, color from options is used
*/
color?: string;
/**
* Optional border color value for certain data item. If missed, color from options is used
*/
borderColor?: string;
/**
* Optional wick color value for certain data item. If missed, color from options is used
*/
wickColor?: string;
}

export function isWhitespaceData(data: SeriesDataItemTypeMap[SeriesType]): data is WhitespaceData {
return (data as Partial<BarData>).open === undefined && (data as Partial<LineData>).value === undefined;
}
Expand All @@ -134,7 +162,7 @@ export interface SeriesDataItemTypeMap {
/**
* The types of candlestick series data.
*/
Candlestick: BarData | WhitespaceData;
Candlestick: CandlestickData | WhitespaceData;
/**
* The types of area series data.
*/
Expand Down
46 changes: 39 additions & 7 deletions src/api/get-series-plot-row-creator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import { SeriesPlotRow } from '../model/series-data';
import { SeriesType } from '../model/series-options';
import { TimePoint, TimePointIndex } from '../model/time-data';

import { BarData, HistogramData, isWhitespaceData, LineData, SeriesDataItemTypeMap } from './data-consumer';
import { BarData, CandlestickData, HistogramData, isWhitespaceData, LineData, SeriesDataItemTypeMap } from './data-consumer';

function getLineBasedSeriesPlotRow(time: TimePoint, index: TimePointIndex, item: LineData | HistogramData): Mutable<SeriesPlotRow<'Line' | 'Histogram'>> {
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] };
Expand All @@ -19,8 +19,40 @@ function getLineBasedSeriesPlotRow(time: TimePoint, index: TimePointIndex, item:
return res;
}

function getOHLCBasedSeriesPlotRow(time: TimePoint, index: TimePointIndex, item: BarData): Mutable<SeriesPlotRow> {
return { index, time, value: [item.open, item.high, item.low, item.close] };
function getBarSeriesPlotRow(time: TimePoint, index: TimePointIndex, item: BarData): Mutable<SeriesPlotRow<'Bar'>> {
const res: Mutable<SeriesPlotRow<'Bar'>> = { index, time, value: [item.open, item.high, item.low, item.close] };

// 'color' here is public property (from API) so we can use `in` here safely
// eslint-disable-next-line no-restricted-syntax
if ('color' in item && item.color !== undefined) {
res.color = item.color;
}

return res;
}

function getCandlestickSeriesPlotRow(time: TimePoint, index: TimePointIndex, item: CandlestickData): Mutable<SeriesPlotRow<'Candlestick'>> {
const res: Mutable<SeriesPlotRow<'Candlestick'>> = { index, time, value: [item.open, item.high, item.low, item.close] };

// 'color' here is public property (from API) so we can use `in` here safely
// eslint-disable-next-line no-restricted-syntax
if ('color' in item && item.color !== undefined) {
res.color = item.color;
}

// 'borderColor' here is public property (from API) so we can use `in` here safely
// eslint-disable-next-line no-restricted-syntax
if ('borderColor' in item && item.borderColor !== undefined) {
res.borderColor = item.borderColor;
}

// 'wickColor' here is public property (from API) so we can use `in` here safely
// eslint-disable-next-line no-restricted-syntax
if ('wickColor' in item && item.wickColor !== undefined) {
res.wickColor = item.wickColor;
}

return res;
}

export type WhitespacePlotRow = Omit<PlotRow, 'value'>;
Expand All @@ -39,7 +71,7 @@ type SeriesItemValueFnMap = {

export type TimedSeriesItemValueFn = (time: TimePoint, index: TimePointIndex, item: SeriesDataItemTypeMap[SeriesType]) => Mutable<SeriesPlotRow | WhitespacePlotRow>;

function wrapWhitespaceData(createPlotRowFn: (typeof getLineBasedSeriesPlotRow) | (typeof getOHLCBasedSeriesPlotRow)): TimedSeriesItemValueFn {
function wrapWhitespaceData(createPlotRowFn: (typeof getLineBasedSeriesPlotRow) | (typeof getBarSeriesPlotRow) | (typeof getCandlestickSeriesPlotRow)): TimedSeriesItemValueFn {
return (time: TimePoint, index: TimePointIndex, bar: SeriesDataItemTypeMap[SeriesType]) => {
if (isWhitespaceData(bar)) {
return { time, index };
Expand All @@ -50,8 +82,8 @@ function wrapWhitespaceData(createPlotRowFn: (typeof getLineBasedSeriesPlotRow)
}

const seriesPlotRowFnMap: SeriesItemValueFnMap = {
Candlestick: wrapWhitespaceData(getOHLCBasedSeriesPlotRow),
Bar: wrapWhitespaceData(getOHLCBasedSeriesPlotRow),
Candlestick: wrapWhitespaceData(getCandlestickSeriesPlotRow),
Bar: wrapWhitespaceData(getBarSeriesPlotRow),
Area: wrapWhitespaceData(getLineBasedSeriesPlotRow),
Baseline: wrapWhitespaceData(getLineBasedSeriesPlotRow),
Histogram: wrapWhitespaceData(getLineBasedSeriesPlotRow),
Expand Down
19 changes: 12 additions & 7 deletions src/model/series-bar-colorer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,11 +74,16 @@ export class SeriesBarColorer {
const borderUpColor = upColor;
const borderDownColor = downColor;

const currentBar = ensureNotNull(this._findBar(barIndex, precomputedBars));
const currentBar = ensureNotNull(this._findBar(barIndex, precomputedBars)) as SeriesPlotRow<'Bar'>;
const isUp = ensure(currentBar.value[PlotRowValueIndex.Open]) <= ensure(currentBar.value[PlotRowValueIndex.Close]);

result.barColor = isUp ? upColor : downColor;
result.barBorderColor = isUp ? borderUpColor : borderDownColor;
if (currentBar.color !== undefined) {
result.barColor = currentBar.color;
result.barBorderColor = currentBar.color;
} else {
result.barColor = isUp ? upColor : downColor;
result.barBorderColor = isUp ? borderUpColor : borderDownColor;
}

return result;
}
Expand All @@ -94,12 +99,12 @@ export class SeriesBarColorer {
const wickUpColor = candlestickStyle.wickUpColor;
const wickDownColor = candlestickStyle.wickDownColor;

const currentBar = ensureNotNull(this._findBar(barIndex, precomputedBars));
const currentBar = ensureNotNull(this._findBar(barIndex, precomputedBars)) as SeriesPlotRow<'Candlestick'>;
const isUp = ensure(currentBar.value[PlotRowValueIndex.Open]) <= ensure(currentBar.value[PlotRowValueIndex.Close]);

result.barColor = isUp ? upColor : downColor;
result.barBorderColor = isUp ? borderUpColor : borderDownColor;
result.barWickColor = isUp ? wickUpColor : wickDownColor;
result.barColor = currentBar.color ?? (isUp ? upColor : downColor);
result.barBorderColor = currentBar.borderColor ?? (isUp ? borderUpColor : borderDownColor);
result.barWickColor = currentBar.wickColor ?? (isUp ? wickUpColor : wickDownColor);

return result;
}
Expand Down
14 changes: 12 additions & 2 deletions src/model/series-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,19 @@ export interface HistogramPlotRow extends PlotRow {
readonly color?: string;
}

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

export interface CandlestickPlotRow extends PlotRow {
readonly color?: string;
readonly borderColor?: string;
readonly wickColor?: string;
}

export interface SeriesPlotRowTypeAtTypeMap {
Bar: PlotRow;
Candlestick: PlotRow;
Bar: BarPlotRow;
Candlestick: CandlestickPlotRow;
Area: PlotRow;
Baseline: PlotRow;
Line: PlotRow;
Expand Down
35 changes: 35 additions & 0 deletions tests/e2e/graphics/test-cases/series/bar-with-custom-colors.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
function generateCandle(i, target) {
const step = (i % 20) / 5000;
const 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);

if (i % 2 === 0) {
target.color = 'yellow';
}
}

function generateData() {
const res = [];
const time = new Date(Date.UTC(2018, 0, 1, 0, 0, 0, 0));
for (let i = 0; i < 500; ++i) {
const item = {
time: time.getTime() / 1000,
};
time.setUTCDate(time.getUTCDate() + 1);

generateCandle(i, item);
res.push(item);
}
return res;
}

function runTestCase(container) {
const chart = LightweightCharts.createChart(container);

const mainSeries = chart.addBarSeries();

mainSeries.setData(generateData());
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
function generateCandle(i, target) {
const step = (i % 20) / 5000;
const 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);

if (i % 2 === 0) {
target.color = 'yellow';
target.borderColor = 'black';
target.wickColor = 'blue';
}
}

function generateData() {
const res = [];
const time = new Date(Date.UTC(2018, 0, 1, 0, 0, 0, 0));
for (let i = 0; i < 500; ++i) {
const item = {
time: time.getTime() / 1000,
};
time.setUTCDate(time.getUTCDate() + 1);

generateCandle(i, item);
res.push(item);
}
return res;
}

function runTestCase(container) {
const chart = LightweightCharts.createChart(container);

const mainSeries = chart.addCandlestickSeries();

mainSeries.setData(generateData());
}
2 changes: 1 addition & 1 deletion website/docs/series-types.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ A baseline is basically two colored areas (top and bottom) between the line conn
## Candlestick

- **Method to create**: [`IChartApi.addCandlestickSeries`](/api/interfaces/IChartApi.md#addcandlestickseries)
- **Data format**: [`BarData`](/api/interfaces/BarData.md) or [`WhitespaceData`](/api/interfaces/WhitespaceData.md)
- **Data format**: [`CandlestickData`](/api/interfaces/CandlestickData.md) or [`WhitespaceData`](/api/interfaces/WhitespaceData.md)
- **Style options**: a mix of [`SeriesOptionsCommon`](/api/interfaces/SeriesOptionsCommon.md) and [`CandlestickStyleOptions`](/api/interfaces/CandlestickStyleOptions.md)

A candlestick chart shows price movements in the form of candlesticks.
Expand Down

0 comments on commit 005e518

Please sign in to comment.