diff --git a/.eslintrc.js b/.eslintrc.js index 90afb847f8..55c0bc439d 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -235,6 +235,7 @@ module.exports = { '@typescript-eslint/no-misused-promises': 'off', '@typescript-eslint/no-unnecessary-qualifier': 'off', '@typescript-eslint/no-unnecessary-type-assertion': 'off', + '@typescript-eslint/no-unsafe-argument': 'off', '@typescript-eslint/no-unsafe-call': 'off', '@typescript-eslint/no-unsafe-member-access': 'off', '@typescript-eslint/no-unsafe-return': 'off', diff --git a/website/plugins/enhanced-codeblock/theme/CodeBlock/chart.tsx b/website/plugins/enhanced-codeblock/theme/CodeBlock/chart.tsx index 864f56f59b..14ad136323 100644 --- a/website/plugins/enhanced-codeblock/theme/CodeBlock/chart.tsx +++ b/website/plugins/enhanced-codeblock/theme/CodeBlock/chart.tsx @@ -13,6 +13,7 @@ interface ChartProps { type IFrameWindow = Window & { createChart: LightweightChartsApiTypeMap[TVersion]['createChart']; + createChartEx: TVersion extends '4.1' | 'current' ? LightweightChartsApiTypeMap[TVersion]['createChartEx'] : undefined; run?: () => void; }; @@ -57,10 +58,11 @@ export function Chart(props: const injectCreateChartAndRun = async () => { try { - const { module, createChart } = await importLightweightChartsVersion[version](iframeWindow); + const { module, createChart, createChartEx } = await importLightweightChartsVersion[version](iframeWindow); Object.assign(iframeWindow, module); // Make ColorType, etc. available in the iframe iframeWindow.createChart = createChart; + iframeWindow.createChartEx = createChartEx; iframeWindow.run?.(); } catch (err: unknown) { // eslint-disable-next-line no-console diff --git a/website/plugins/enhanced-codeblock/theme/CodeBlock/import-lightweight-charts-version.ts b/website/plugins/enhanced-codeblock/theme/CodeBlock/import-lightweight-charts-version.ts index eedfa8f1e4..5f9ea77cec 100644 --- a/website/plugins/enhanced-codeblock/theme/CodeBlock/import-lightweight-charts-version.ts +++ b/website/plugins/enhanced-codeblock/theme/CodeBlock/import-lightweight-charts-version.ts @@ -25,6 +25,7 @@ export type LightweightChartsVersion = Version | 'current'; export interface LightweightChartsApiGetterResult { module: LightweightChartsApiTypeMap[T]; createChart: LightweightChartsApiTypeMap[T]['createChart']; + createChartEx: T extends '4.1' | 'current' ? LightweightChartsApiTypeMap[T]['createChartEx'] : undefined; } export type LightweightChartsApiGetters = { @@ -50,7 +51,7 @@ export const importLightweightChartsVersion: LightweightChartsApiGetters = { return result; }; - return { module, createChart }; + return { module, createChart, createChartEx: undefined }; }, '4.0': async (window: Window) => { const module = await import('lightweight-charts-4.0'); @@ -61,7 +62,7 @@ export const importLightweightChartsVersion: LightweightChartsApiGetters = { return result; }; - return { module, createChart }; + return { module, createChart, createChartEx: undefined }; }, 4.1: async (window: Window) => { const module = await import('lightweight-charts-4.1'); @@ -72,7 +73,13 @@ export const importLightweightChartsVersion: LightweightChartsApiGetters = { return result; }; - return { module, createChart }; + const createChartEx = (container: string | HTMLElement, behaviour: Parameters[1], options?: Parameters[2]) => { + const result = module.createChartEx(container, behaviour, options); + addResizeHandler(window, container as HTMLElement, result.resize.bind(result)); + return result; + }; + + return { module, createChart, createChartEx: createChartEx as typeof module.createChartEx }; }, current: async () => { const module = await import('../../../../..'); @@ -83,6 +90,12 @@ export const importLightweightChartsVersion: LightweightChartsApiGetters = { return result; }; - return { module, createChart }; + const createChartEx = (container: string | HTMLElement, behaviour: Parameters[1], options?: Parameters[2]) => { + const result = module.createChartEx(container, behaviour, options); + addResizeHandler(window, container as HTMLElement, result.resize.bind(result)); + return result; + }; + + return { module, createChart, createChartEx: createChartEx as typeof module.createChartEx }; }, }; diff --git a/website/tutorials/how_to/.eslintrc.js b/website/tutorials/how_to/.eslintrc.js index 5cd6c516c1..e5bae9c40c 100644 --- a/website/tutorials/how_to/.eslintrc.js +++ b/website/tutorials/how_to/.eslintrc.js @@ -2,5 +2,6 @@ module.exports = { globals: { document: false, createChart: false, + createChartEx: false, }, }; diff --git a/website/tutorials/how_to/horizontal-price-scale.js b/website/tutorials/how_to/horizontal-price-scale.js new file mode 100644 index 0000000000..c43a9bc393 --- /dev/null +++ b/website/tutorials/how_to/horizontal-price-scale.js @@ -0,0 +1,131 @@ +// remove-start +// Lightweight Charts™ Example: Horizontal Price Scale +// https://tradingview.github.io/lightweight-charts/tutorials/how_to/horizontal-price-scale + +// remove-end +function markWithGreaterWeight(a, b) { + return a.weight > b.weight ? a : b; +} + +// remove-line +/** @type {import('lightweight-charts').IHorzScaleBehavior} */ +class HorzScaleBehaviorPrice { + constructor() { + this._options = {}; + } + + options() { + return this._options; + } + + setOptions(options) { + this._options = options; + } + + preprocessData(data) {} + + updateFormatter(options) { + if (!this._options) { + return; + } + this._options.localization = options; + } + + createConverterToInternalObj(data) { + return price => price; + } + + key(internalItem) { + return internalItem; + } + + cacheKey(internalItem) { + return internalItem; + } + + convertHorzItemToInternal(item) { + return item; + } + + formatHorzItem(item) { + return item.toFixed(this._precision()); + } + + formatTickmark(item, localizationOptions) { + return item.time.toFixed(this._precision()); + } + + maxTickMarkWeight(marks) { + return marks.reduce(markWithGreaterWeight, marks[0]).weight; + } + + fillWeightsForPoints(sortedTimePoints, startIndex) { + const priceWeight = price => { + if (price === Math.ceil(price / 100) * 100) { + return 8; + } + if (price === Math.ceil(price / 50) * 50) { + return 7; + } + if (price === Math.ceil(price / 25) * 25) { + return 6; + } + if (price === Math.ceil(price / 10) * 10) { + return 5; + } + if (price === Math.ceil(price / 5) * 5) { + return 4; + } + if (price === Math.ceil(price)) { + return 3; + } + if (price * 2 === Math.ceil(price * 2)) { + return 1; + } + return 0; + }; + for (let index = startIndex; index < sortedTimePoints.length; ++index) { + sortedTimePoints[index].timeWeight = priceWeight( + sortedTimePoints[index].time + ); + } + } + + _precision() { + return this._options.localization.precision; + } +} + +const horzItemBehavior = new HorzScaleBehaviorPrice(); + +const chartOptions = { + layout: { + textColor: CHART_TEXT_COLOR, + background: { type: 'solid', color: CHART_BACKGROUND_COLOR }, + }, + localization: { + precision: 2, // custom option + }, +}; + +// remove-line +/** @type {import('lightweight-charts').IChartApi} */ +const chart = createChartEx( + document.getElementById('container'), + horzItemBehavior, + chartOptions +); + +const lineSeries = chart.addLineSeries({ color: LINE_LINE_COLOR }); + +const data = []; +for (let i = 0; i < 5000; i++) { + data.push({ + time: i * 0.25, + value: Math.sin(i / 100) + i / 500, + }); +} + +lineSeries.setData(data); + +chart.timeScale().fitContent(); diff --git a/website/tutorials/how_to/horizontal-price-scale.mdx b/website/tutorials/how_to/horizontal-price-scale.mdx new file mode 100644 index 0000000000..18a0760912 --- /dev/null +++ b/website/tutorials/how_to/horizontal-price-scale.mdx @@ -0,0 +1,350 @@ +--- +title: Custom horizontal scale +sidebar_label: Custom horizontal scale +description: + Customizing horizontal scale behavior with IHorzScaleBehavior interface. +pagination_prev: null +pagination_next: null +keywords: + - price + - scale + - horizontal + - options +--- + +The `IHorzScaleBehavior` interface allows you to customize the behavior of the +horizontal scale. By default, this scale uses [time](/docs/api#time) values, but +you can override it to use any other type of horizontal scale items, such as +price values. The most typical use case is the creation of Options charts. + +This guide will explain the +[`IHorzScaleBehavior`](/docs/api/interfaces/IHorzScaleBehavior) interface and +how to implement it to create a horizontal scale using price values with +customizable precision. + +## Understanding the IHorzScaleBehavior interface + +The `IHorzScaleBehavior` interface consists of several methods that you need to +implement to customize the horizontal scale behavior. Here's a breakdown of each +method and its purpose: + +### options + +``` +public options(): ChartOptionsImpl +``` + +This method returns the chart's current configuration options. These options +include various settings that control the appearance and behavior of the chart. +Implement this method to return the current options of your horizontal scale +behavior. + +### setOptions + +``` +public setOptions(options: ChartOptionsImpl): void +``` + +This method allows you to set or update the chart's configuration options. The +provided `options` parameter will contain the settings you want to apply. Use +this method to update the options when necessary. + +### preprocessData + +``` +public preprocessData(data: DataItem | DataItem[]): void +``` + +This method processes the series data before it is used by the chart. It +receives an array of data items or a single data item. You can implement this +method to preprocess or modify data as needed before it is rendered. + +### updateFormatter + +``` +public updateFormatter(options: LocalizationOptions): void +``` + +This method updates the formatter used for displaying the horizontal scale items +based on localization options. Implement this to set custom formatting settings, +such as locale-specific date or number formats. + +### createConverterToInternalObj + +``` +public createConverterToInternalObj(data: SeriesDataItemTypeMap[SeriesType][]): HorzScaleItemConverterToInternalObj +``` + +This method creates and returns a function that converts series data items into +internal horizontal scale items. Implementing this method is essential for +transforming your custom data into the format required by the chart's internal +mechanisms. + +### key + +``` +public key(internalItem: InternalHorzScaleItem | HorzScaleItem): InternalHorzScaleItemKey +``` + +This method returns a unique key for a given horizontal scale item. It's used +internally by the chart to identify and manage items uniquely. Implement this +method to provide a unique identifier for each item. + +### cacheKey + +``` +public cacheKey(internalItem: InternalHorzScaleItem): number +``` + +This method returns a cache key for a given internal horizontal scale item. This +key helps the chart to cache and retrieve items efficiently. Implement this +method to return a numeric key for caching purposes. + +### convertHorzItemToInternal + +``` +public convertHorzItemToInternal(item: HorzScaleItem): InternalHorzScaleItem +``` + +This method converts a horizontal scale item into an internal item that the +chart can use. Implementing this method ensures that your custom data type is +correctly transformed for internal use. + +### formatHorzItem + +``` +public formatHorzItem(item: InternalHorzScaleItem): string +``` + +This method formats a horizontal scale item into a display string. The returned +string will be used for displaying the item on the chart. Implement this method +to format your items in the desired way (e.g., with a specific number of decimal +places). + +### formatTickmark + +``` +public formatTickmark(item: TickMark, localizationOptions: LocalizationOptions): string +``` + +This method formats a horizontal scale tick mark into a display string. The tick +mark represents significant points on the horizontal scale. Implement this +method to customize how tick marks are displayed. + +### maxTickMarkWeight + +``` +public maxTickMarkWeight(marks: TimeMark[]): TickMarkWeightValue +``` + +This method determines the maximum weight for a set of tick marks, which +influences their display prominence. Implement this method to specify the weight +of the most significant tick mark. + +### fillWeightsForPoints + +``` +public fillWeightsForPoints(sortedTimePoints: readonly Mutable[], startIndex: number): void +``` + +This method assigns weights to the sorted time points. These weights influence +the tick marks' visual prominence. Implement this method to provide a weighting +system for your horizontal scale items. + +## Example + +Below is an example implementation of a custom horizontal scale behavior using +price values. This example also includes customizable precision for formatting +price values. + +### Implement price-based horizontal scale + +1. **Define the custom localization options interface** + +Extend the [`LocalizationOptions`](/docs/api/interfaces/LocalizationOptions) +interface to include a `precision` property. + +```ts +export interface CustomLocalizationOptions + extends LocalizationOptions { + precision: number; +} +``` + +2. **Define the type alias** + +Define a type alias for the horizontal scale item representing price values. + +```ts +export type HorzScalePriceItem = number; +``` + +3. **Implement the custom horizontal scale behavior class** + +The `HorzScaleBehaviorPrice` class implements the `IHorzScaleBehavior` +interface, with additional logic to handle the precision provided in the custom +localization options. + +```ts +function markWithGreaterWeight(a: TimeMark, b: TimeMark): TimeMark { + return a.weight > b.weight ? a : b; +} + +export class HorzScaleBehaviorPrice implements IHorzScaleBehavior { + private _options!: ChartOptionsImpl; + + public options(): ChartOptionsImpl { + return this._options; + } + + public setOptions(options: ChartOptionsImpl): void { + this._options = options; + } + + public preprocessData( + data: DataItem | DataItem[] + ): void { + // un-needed in this example because we do not require any additional + // data processing for this scale. + // The method is still required to be implemented in the class. + } + + public updateFormatter(options: CustomLocalizationOptions): void { + if (!this._options) { + return; + } + this._options.localization = options; + } + + public createConverterToInternalObj( + data: SeriesDataItemTypeMap[SeriesType][] + ): HorzScaleItemConverterToInternalObj { + return (price: number) => price as unknown as InternalHorzScaleItem; + } + + public key( + internalItem: InternalHorzScaleItem | HorzScalePriceItem + ): InternalHorzScaleItemKey { + return internalItem as InternalHorzScaleItemKey; + } + + public cacheKey(internalItem: InternalHorzScaleItem): number { + return internalItem as unknown as number; + } + + public convertHorzItemToInternal( + item: HorzScalePriceItem + ): InternalHorzScaleItem { + return item as unknown as InternalHorzScaleItem; + } + + public formatHorzItem(item: InternalHorzScaleItem): string { + return (item as unknown as number).toFixed(this._precision()); + } + + public formatTickmark( + item: TickMark, + localizationOptions: LocalizationOptions + ): string { + return (item.time as unknown as number).toFixed(this._precision()); + } + + public maxTickMarkWeight(marks: TimeMark[]): TickMarkWeightValue { + return marks.reduce(markWithGreaterWeight, marks[0]).weight; + } + + public fillWeightsForPoints( + sortedTimePoints: readonly Mutable[], + startIndex: number + ): void { + const priceWeight = (price: number) => { + if (price === Math.ceil(price / 100) * 100) { + return 8; + } + if (price === Math.ceil(price / 50) * 50) { + return 7; + } + if (price === Math.ceil(price / 25) * 25) { + return 6; + } + if (price === Math.ceil(price / 10) * 10) { + return 5; + } + if (price === Math.ceil(price / 5) * 5) { + return 4; + } + if (price === Math.ceil(price)) { + return 3; + } + if (price * 2 === Math.ceil(price * 2)) { + return 1; + } + return 0; + }; + for (let index = startIndex; index < sortedTimePoints.length; ++index) { + sortedTimePoints[index].timeWeight = priceWeight( + sortedTimePoints[index].time as unknown as number + ); + } + } + + private _precision(): number { + return (this._options.localization as CustomLocalizationOptions).precision; + } +} +``` + +This class provides additional precision control through localization options, +allowing formatted price values to use a specific number of decimal places. + +### Customize horizontal scale behavior + +To use the custom horizontal scale behavior, instantiate the +`HorzScaleBehaviorPrice` class and pass it to +[`createChartEx`](/docs/api#createchartex). + +You can pass the custom option for `precision` within the `localization` +property of the chart options. + +```js +const horzItemBehavior = new HorzScaleBehaviorPrice(); +const chart = LightweightCharts.createChartEx(container, horzItemBehavior, { + localization: { + precision: 2, // custom option + }, +}); +const s1 = chart.addLineSeries(); +const data = []; +for (let i = 0; i < 5000; i++) { + data.push({ + time: i * 0.25, + value: Math.sin(i / 100), + }); +} +s1.setData(data); +``` + +### Conclusion + +The `IHorzScaleBehavior` interface provides a powerful way to customize the +horizontal scale behavior in Lightweight Charts™. By implementing this +interface, you can define how the horizontal scale should interpret and display +custom data types, such as price values. The provided example demonstrates how +to implement a horizontal scale with customizable precision, allowing for +tailored display formats to fit your specific requirements. + +### Full example + +import UsageGuidePartial from '../_usage-guide-partial.mdx'; +import CodeBlock from '@theme/CodeBlock'; +import code from '!!raw-loader!./horizontal-price-scale.js'; + +} +> + {code} +