Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Yield Curve and Options Chart Types, Up/Down Markers Plugin #1674

Draft
wants to merge 19 commits into
base: v5-candidate
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 26 additions & 2 deletions .size-limit.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export default [
{
name: 'ESM',
path: 'dist/lightweight-charts.production.mjs',
limit: '45.00 KB',
limit: '47.00 KB',
import: '*',
ignore: ['fancy-canvas'],
brotli: true,
Expand All @@ -30,7 +30,23 @@ export default [
brotli: true,
},
{
name: 'ESM Standalone',
name: 'ESM createYieldCurveChart',
path: 'dist/lightweight-charts.production.mjs',
limit: '45.00 KB',
import: '{ createYieldCurveChart }',
ignore: ['fancy-canvas'],
brotli: true,
},
{
name: 'ESM createOptionsChart',
path: 'dist/lightweight-charts.production.mjs',
limit: '45.00 KB',
import: '{ createOptionsChart }',
ignore: ['fancy-canvas'],
brotli: true,
},
{
name: 'Standalone-ESM',
path: 'dist/lightweight-charts.standalone.production.mjs',
limit: '50.00 KB',
import: '*',
Expand Down Expand Up @@ -58,4 +74,12 @@ export default [
limit: '2.00 KB',
brotli: true,
},
{
name: 'Plugin: UpDownMarkersPrimitive',
path: 'dist/lightweight-charts.production.mjs',
limit: '2.50 KB',
import: '{ UpDownMarkersPrimitive }',
ignore: ['fancy-canvas'],
brotli: true,
},
];
7 changes: 4 additions & 3 deletions src/api/chart-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,8 @@ function toInternalOptions<HorzScaleItem>(options: DeepPartial<ChartOptionsImpl<
export type IPriceScaleApiProvider<HorzScaleItem> = Pick<IChartApiBase<HorzScaleItem>, 'priceScale'>;

export class ChartApi<HorzScaleItem> implements IChartApiBase<HorzScaleItem>, DataUpdatesConsumer<SeriesType, HorzScaleItem> {
protected readonly _horzScaleBehavior: IHorzScaleBehavior<HorzScaleItem>;

private _chartWidget: ChartWidget<HorzScaleItem>;
private _dataLayer: DataLayer<HorzScaleItem>;
private readonly _seriesMap: Map<SeriesApi<SeriesType, HorzScaleItem>, Series<SeriesType>> = new Map();
Expand All @@ -128,7 +130,6 @@ export class ChartApi<HorzScaleItem> implements IChartApiBase<HorzScaleItem>, Da

private readonly _timeScaleApi: TimeScaleApi<HorzScaleItem>;
private readonly _panes: WeakMap<Pane, PaneApi<HorzScaleItem>> = new WeakMap();
private readonly _horzScaleBehavior: IHorzScaleBehavior<HorzScaleItem>;

public constructor(container: HTMLElement, horzScaleBehavior: IHorzScaleBehavior<HorzScaleItem>, options?: DeepPartial<ChartOptionsImpl<HorzScaleItem>>) {
this._dataLayer = new DataLayer<HorzScaleItem>(horzScaleBehavior);
Expand Down Expand Up @@ -261,8 +262,8 @@ export class ChartApi<HorzScaleItem> implements IChartApiBase<HorzScaleItem>, Da
this._sendUpdateToChart(this._dataLayer.setSeriesData(series, data));
}

public updateData<TSeriesType extends SeriesType>(series: Series<TSeriesType>, data: SeriesDataItemTypeMap<HorzScaleItem>[TSeriesType]): void {
this._sendUpdateToChart(this._dataLayer.updateSeriesData(series, data));
public updateData<TSeriesType extends SeriesType>(series: Series<TSeriesType>, data: SeriesDataItemTypeMap<HorzScaleItem>[TSeriesType], historicalUpdate: boolean): void {
this._sendUpdateToChart(this._dataLayer.updateSeriesData(series, data, historicalUpdate));
}

public subscribeClick(handler: MouseEventHandler<HorzScaleItem>): void {
Expand Down
19 changes: 10 additions & 9 deletions src/api/create-chart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,15 @@ import { IHorzScaleBehavior } from '../model/ihorz-scale-behavior';
import { ChartApi } from './chart-api';
import { IChartApiBase } from './ichart-api';

export function fetchHtmlElement(container: string | HTMLElement): HTMLElement {
if (isString(container)) {
const element = document.getElementById(container);
assert(element !== null, `Cannot find element in DOM with id=${container}`);
return element;
}
return container;
}

/**
* This function is the main entry point of the Lightweight Charting Library. If you are using time values
* for the horizontal scale then it is recommended that you rather use the {@link createChart} function.
Expand All @@ -26,15 +35,7 @@ export function createChartEx<HorzScaleItem, THorzScaleBehavior extends IHorzSca
horzScaleBehavior: THorzScaleBehavior,
options?: DeepPartial<ReturnType<THorzScaleBehavior['options']>>
): IChartApiBase<HorzScaleItem> {
let htmlElement: HTMLElement;
if (isString(container)) {
const element = document.getElementById(container);
assert(element !== null, `Cannot find element in DOM with id=${container}`);
htmlElement = element;
} else {
htmlElement = container;
}

const htmlElement = fetchHtmlElement(container);
const res = new ChartApi<HorzScaleItem>(htmlElement, horzScaleBehavior, options);
horzScaleBehavior.setOptions(res.options());
return res;
Expand Down
25 changes: 25 additions & 0 deletions src/api/create-options-chart.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { DeepPartial } from '../helpers/strict-type-checks';

import { HorzScaleBehaviorPrice } from '../model/horz-scale-behavior-price/horz-scale-behaviour-price';
import { PriceChartOptions } from '../model/horz-scale-behavior-price/options';

import { createChartEx } from './create-chart';
import { IChartApiBase } from './ichart-api';

/**
* Creates an 'options' chart with price values on the horizontal scale.
*
* This function is used to create a specialized chart type where the horizontal scale
* represents price values instead of time. It's particularly useful for visualizing
* option chains, price distributions, or any data where price is the primary x-axis metric.
*
* @param container - The DOM element or its id where the chart will be rendered.
* @param options - Optional configuration options for the price chart.
* @returns An instance of IChartApiBase configured for price-based horizontal scaling.
*/
export function createOptionsChart(
container: string | HTMLElement,
options?: DeepPartial<PriceChartOptions>
): IChartApiBase<number> {
return createChartEx(container, new HorzScaleBehaviorPrice(), options);
}
31 changes: 31 additions & 0 deletions src/api/create-yield-curve-chart.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { DeepPartial } from '../helpers/strict-type-checks';

import {
YieldCurveChartOptions,
} from '../model/yield-curve-horz-scale-behavior/yield-curve-chart-options';

import { fetchHtmlElement } from './create-chart';
import { IYieldCurveChartApi } from './iyield-chart-api';
import { YieldChartApi } from './yield-chart-api';

/**
* Creates a yield curve chart with the specified options.
*
* A yield curve chart differs from the default chart type
* in the following ways:
* - Horizontal scale is linearly spaced, and defined in monthly
* time duration units
* - Whitespace is ignored for the crosshair and grid lines
*
* @param container - ID of HTML element or element itself
* @param options - The yield chart options.
* @returns An interface to the created chart
*/
export function createYieldCurveChart(
container: string | HTMLElement,
options?: DeepPartial<YieldCurveChartOptions>
): IYieldCurveChartApi {
const htmlElement = fetchHtmlElement(container);
const chartApi = new YieldChartApi(htmlElement, options);
return chartApi;
}
4 changes: 3 additions & 1 deletion src/api/iseries-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,8 @@ export interface ISeriesApi<
*
* @param bar - A single data item to be added. Time of the new item must be greater or equal to the latest existing time point.
* If the new item's time is equal to the last existing item's time, then the existing item is replaced with the new one.
* @param historicalUpdate - If true, allows updating an existing data point that is not the latest bar. Default is false.
* Updating older data using `historicalUpdate` will be slower than updating the most recent data point.
* @example Updating line series data
* ```js
* lineSeries.update({
Expand All @@ -176,7 +178,7 @@ export interface ISeriesApi<
* });
* ```
*/
update(bar: TData): void;
update(bar: TData, historicalUpdate?: boolean): void;

/**
* Returns a bar data by provided logical index.
Expand Down
5 changes: 5 additions & 0 deletions src/api/iseries-primitive-api.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Time } from '../model/horz-scale-behavior-time/types';
import { IHorzScaleBehavior } from '../model/ihorz-scale-behavior';
import { ISeriesPrimitiveBase } from '../model/iseries-primitive';
import { SeriesOptionsMap, SeriesType } from '../model/series-options';

Expand All @@ -25,6 +26,10 @@ export interface SeriesAttachedParameter<
* Request an update (redraw the chart)
*/
requestUpdate: () => void;
/**
* Horizontal Scale Behaviour for the chart.
*/
horzScaleBehavior: IHorzScaleBehavior<HorzScaleItem>;
}

/**
Expand Down
6 changes: 6 additions & 0 deletions src/api/iyield-chart-api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { IChartApiBase } from './ichart-api';

/**
* The main interface of a single yield curve chart.
*/
export interface IYieldCurveChartApi extends IChartApiBase<number> {}
1 change: 1 addition & 0 deletions src/api/options/time-scale-options-defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,5 @@ export const timeScaleOptionsDefaults: HorzScaleOptions = {
uniformDistribution: false,
minimumHeight: 0,
allowBoldLabels: true,
ignoreWhitespaceIndices: false,
};
7 changes: 7 additions & 0 deletions src/api/options/yield-curve-chart-options-defaults.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { YieldCurveOptions } from '../../model/yield-curve-horz-scale-behavior/yield-curve-chart-options';

export const yieldChartOptionsDefaults: YieldCurveOptions = {
baseResolution: 1,
minimumTimeRange: 120,
startTimeRange: 0,
};
5 changes: 3 additions & 2 deletions src/api/series-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,10 +155,10 @@ export class SeriesApi<
this._onDataChanged('full');
}

public update(bar: TData): void {
public update(bar: TData, historicalUpdate: boolean = false): void {
checkSeriesValuesType(this._series.seriesType(), [bar]);

this._dataUpdatesConsumer.updateData(this._series, bar);
this._dataUpdatesConsumer.updateData(this._series, bar, historicalUpdate);
this._onDataChanged('update');
}

Expand Down Expand Up @@ -239,6 +239,7 @@ export class SeriesApi<
chart: this._chartApi,
series: this,
requestUpdate: () => this._series.model().fullUpdate(),
horzScaleBehavior: this._horzScaleBehavior,
});
}
}
Expand Down
128 changes: 128 additions & 0 deletions src/api/yield-chart-api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import { DeepPartial, merge } from '../helpers/strict-type-checks';

import { AreaData, LineData, WhitespaceData } from '../model/data-consumer';
import {
AreaSeriesOptions,
AreaStyleOptions,
LineSeriesOptions,
LineStyleOptions,
SeriesOptionsCommon,
} from '../model/series-options';
import { YieldCurveChartOptions, YieldCurveOptions } from '../model/yield-curve-horz-scale-behavior/yield-curve-chart-options';
import { YieldCurveHorzScaleBehavior } from '../model/yield-curve-horz-scale-behavior/yield-curve-horz-scale-behavior';

import { ChartApi } from './chart-api';
import { ISeriesApi } from './iseries-api';
import { IYieldCurveChartApi } from './iyield-chart-api';
import { yieldChartOptionsDefaults } from './options/yield-curve-chart-options-defaults';

interface WhitespaceState {
start: number;
end: number;
resolution: number;
}

function generateWhitespaceData({
start,
end,
resolution,
}: WhitespaceState): WhitespaceData<number>[] {
return Array.from(
{ length: Math.floor((end - start) / resolution) + 1 },
// eslint-disable-next-line quote-props
(item: unknown, i: number) => ({ 'time': start + i * resolution })
);
}

function buildWhitespaceState(
options: YieldCurveOptions,
lastIndex: number
): WhitespaceState {
return {
start: Math.max(0, options.startTimeRange),
end: Math.max(0, options.minimumTimeRange, lastIndex || 0),
resolution: Math.max(1, options.baseResolution),
};
}

const generateWhitespaceHash = ({
start,
end,
resolution,
}: WhitespaceState): string => `${start}~${end}~${resolution}`;

const defaultOptions: DeepPartial<YieldCurveChartOptions> = {
yieldCurve: yieldChartOptionsDefaults,
// and add sensible default options for yield charts which
// are different from the usual defaults.
timeScale: {
ignoreWhitespaceIndices: true,
},
leftPriceScale: {
visible: true,
},
rightPriceScale: {
visible: false,
},
localization: {
priceFormatter: (value: number): string => {
return value.toFixed(3) + '%';
},
},
};

const lineStyleDefaultOptionOverrides: DeepPartial<LineStyleOptions & SeriesOptionsCommon & AreaStyleOptions> = {
lastValueVisible: false,
priceLineVisible: false,
};

export class YieldChartApi extends ChartApi<number> implements IYieldCurveChartApi {
public constructor(container: HTMLElement, options?: DeepPartial<YieldCurveChartOptions>) {
const fullOptions = merge(
defaultOptions,
options || {}
) as YieldCurveChartOptions;
const horzBehaviour = new YieldCurveHorzScaleBehavior();
super(container, horzBehaviour, fullOptions);
horzBehaviour.setOptions(this.options() as YieldCurveChartOptions);
this._initWhitespaceSeries();
}

public override addLineSeries(options?: DeepPartial<LineStyleOptions & SeriesOptionsCommon> | undefined, paneIndex?: number | undefined): ISeriesApi<'Line', number, WhitespaceData<number> | LineData<number>, LineSeriesOptions, DeepPartial<LineStyleOptions & SeriesOptionsCommon>> {
const optionOverrides = {
...lineStyleDefaultOptionOverrides,
...options,
};
return super.addLineSeries(optionOverrides, paneIndex);
}

public override addAreaSeries(options?: DeepPartial<AreaStyleOptions & SeriesOptionsCommon> | undefined, paneIndex?: number | undefined): ISeriesApi<'Area', number, AreaData<number> | WhitespaceData<number>, AreaSeriesOptions, DeepPartial<AreaStyleOptions & SeriesOptionsCommon>> {
const optionOverrides = {
...lineStyleDefaultOptionOverrides,
...options,
};
return super.addAreaSeries(optionOverrides, paneIndex);
}

private _initWhitespaceSeries(): void {
const horzBehaviour = this._horzScaleBehavior as YieldCurveHorzScaleBehavior;
const whiteSpaceSeries = this.addLineSeries();

let currentWhitespaceHash: string;
function updateWhitespace(lastIndex: number): void {
const newWhitespaceState = buildWhitespaceState(
horzBehaviour.options().yieldCurve,
lastIndex
);
const newWhitespaceHash = generateWhitespaceHash(newWhitespaceState);

if (newWhitespaceHash !== currentWhitespaceHash) {
currentWhitespaceHash = newWhitespaceHash;
whiteSpaceSeries.setData(generateWhitespaceData(newWhitespaceState));
}
}

updateWhitespace(0);
horzBehaviour.whitespaceInvalidated().subscribe(updateWhitespace);
}
}
4 changes: 4 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ export const customSeriesDefaultOptions: CustomSeriesOptions = {
};

export { createChart, createChartEx, defaultHorzScaleBehavior } from './api/create-chart';
export { createYieldCurveChart } from './api/create-yield-curve-chart';
export { createOptionsChart } from './api/create-options-chart';

export { UpDownMarkersPrimitive } from './plugins/up-down-markers-plugin/primitive';

/*
Plugins
Expand Down
Loading
Loading