diff --git a/.circleci/config.yml b/.circleci/config.yml index f59c32bc95..947e675644 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -42,11 +42,11 @@ aliases: executors: node16-executor: docker: - - image: circleci/node:16 + - image: cimg/node:16.16 node16-browsers-executor: docker: - - image: circleci/node:16-browsers + - image: cimg/node:16.16-browsers commands: checkout-with-deps: @@ -74,7 +74,10 @@ commands: echo 'export TESTS_REPORT_FILE="test-results/graphics/results.xml"' >> $BASH_ENV echo 'export DEVICE_PIXEL_RATIO=<< parameters.devicePixelRatio >>' >> $BASH_ENV echo 'export PRODUCTION_BUILD=<< parameters.productionBuild >>' >> $BASH_ENV - - run: scripts/run-graphics-tests.sh + - run: + command: scripts/run-graphics-tests.sh + no_output_timeout: 20m + - store_test_results: path: test-results/ - store_artifacts: @@ -101,10 +104,10 @@ jobs: executor: node16-executor steps: - checkout-with-deps + - run: npm run build:prod # make sure that the project is compiled successfully with composite projects # so we don't have cyclic deps between projects and wrong imports - run: npm run tsc-verify - - run: npm run build:prod - persist_to_workspace: root: ./ paths: @@ -127,6 +130,8 @@ jobs: executor: node16-executor steps: - checkout-with-deps + - attach_workspace: + at: ./ - run: npm run lint:eslint lint-dts: @@ -215,6 +220,19 @@ jobs: - store_test_results: path: test-results/ + interactions: + executor: node16-browsers-executor + environment: + NO_SANDBOX: "true" + TESTS_REPORT_FILE: "test-results/interactions/results.xml" + steps: + - checkout-with-deps + - attach_workspace: + at: ./ + - run: scripts/run-interactions-tests.sh + - store_test_results: + path: test-results/ + size-limit: executor: node16-executor steps: @@ -230,8 +248,7 @@ jobs: executor: node16-executor steps: - checkout-with-deps - - run: npm run tsc - - run: npm run bundle-dts + - run: npm run build:prod - run: name: "Install dependencies and build website" command: | @@ -290,10 +307,7 @@ workflows: filters: *default-filters requires: - install-deps - - lint-eslint: - filters: *default-filters - requires: - - install-deps + - install-deps-website - lint-markdown: filters: *default-filters requires: @@ -335,17 +349,18 @@ workflows: filters: *default-filters requires: - build - - lint-dts: + - interactions: filters: *default-filters requires: - build - - website: - jobs: - - install-deps: + - lint-dts: filters: *default-filters - - install-deps-website: + requires: + - build + - lint-eslint: filters: *default-filters + requires: + - build - build-docusaurus-website: filters: *default-filters requires: diff --git a/.eslintrc.js b/.eslintrc.js index e2885f1def..61ceceb2a3 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,21 +1,11 @@ function getNamingConventionRules(additionalDefaultFormats = []) { return [ { selector: 'default', format: ['camelCase', ...additionalDefaultFormats], leadingUnderscore: 'forbid', trailingUnderscore: 'forbid' }, - { selector: 'variable', format: ['camelCase', 'UPPER_CASE', ...additionalDefaultFormats] }, - // { - // selector: 'variable', - // types: ['boolean'], - // format: ['PascalCase'], - // prefix: ['is', 'should', 'has', 'can', 'did', 'will', 'show', 'enable', 'need'], - // }, - { selector: 'typeLike', format: ['PascalCase'] }, { selector: 'enumMember', format: ['PascalCase'] }, - { selector: 'memberLike', modifiers: ['private'], leadingUnderscore: 'require', format: ['camelCase'] }, { selector: 'memberLike', modifiers: ['protected'], leadingUnderscore: 'require', format: ['camelCase'] }, - { selector: 'property', format: ['PascalCase'], @@ -24,8 +14,6 @@ function getNamingConventionRules(additionalDefaultFormats = []) { regex: '^(Area|Baseline|Bar|Candlestick|Histogram|Line)$', }, }, - - // { selector: 'typeParameter', format: ['PascalCase'], prefix: ['T', 'U'] }, ]; } @@ -59,6 +47,7 @@ module.exports = { 'prefer-arrow', 'unicorn', 'jsdoc', + 'eslint-plugin-react', ], settings: { jsdoc: { @@ -73,7 +62,7 @@ module.exports = { 'plugin:react/recommended', ], parserOptions: { - ecmaVersion: 2019, + ecmaVersion: 2020, sourceType: 'module', }, overrides: [ @@ -94,6 +83,7 @@ module.exports = { }, rules: { 'react/prop-types': 'off', + 'import/no-default-export': 'off', }, }, ], @@ -180,8 +170,8 @@ module.exports = { 'no-undef': 'off', 'no-unused-vars': 'off', indent: ['error', 4], - 'unicorn/filename-case': 'off', + 'react/prop-types': 'off', }, }, { @@ -195,7 +185,7 @@ module.exports = { }, overrides: [ { - files: ['website/**/*.tsx'], + files: ['website/**/*.ts', 'website/**/*.tsx'], parserOptions: { project: 'website/tsconfig.json', sourceType: 'module', diff --git a/.gitignore b/.gitignore index a91d1f7234..69426ed6e6 100644 --- a/.gitignore +++ b/.gitignore @@ -27,8 +27,10 @@ debug.html /website/.docusaurus /website/.cache-loader /website/.previous-typings-cache/*.d.ts +/website/versions.d.ts !/src/typings/**/*.d.ts # graphics tests out data /tests/e2e/graphics/.gendata/ +tests/e2e/coverage/.gendata diff --git a/.vscode/launch.json b/.vscode/launch.json index 522a265c74..54bf3dc70d 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -41,6 +41,16 @@ "${input:testStandalonePath}" ], "internalConsoleOptions": "openOnSessionStart" + }, + { + "type": "node", + "request": "launch", + "name": "Interaction tests", + "program": "${workspaceFolder}/tests/e2e/interactions/runner.js", + "args": [ + "${input:testStandalonePath}" + ], + "internalConsoleOptions": "openOnSessionStart" } ], "inputs": [ diff --git a/.vscode/settings.json b/.vscode/settings.json index 0c39654774..d54dee2f42 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,6 +3,7 @@ "dist/**": true, "lib/**": true, "tests/e2e/graphics/.gendata/**": true, + "tests/e2e/coverage/.gendata/**": true, "website/.docusaurus/**": true, "website/build/**": true, "website/docs/api/**": true, diff --git a/BUILDING.md b/BUILDING.md index f8d80f10ff..3fdf49af26 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -1,6 +1,8 @@ # Building Lightweight Charts -The minimal supported version of [NodeJS](https://nodejs.org/) for development is 16.13. +The minimal supported version of [NodeJS](https://nodejs.org/) for development is 16.16. + +**Note:** you need to run `npm install` in both the root directory and the `website` directory before you can run the lint tests. ## Compiling @@ -19,6 +21,8 @@ The minimal supported version of [NodeJS](https://nodejs.org/) for development i - `npm run lint` - runs lint for the code - `npm run test` - runs unit-tests +There are several included e2e tests available which can be run individually. Please have a read through the following document for further information: [/tests/README.md](./tests/README.md) + ## Tips - You can use the following command to make sure that your local copy passes all (almost) available checks: @@ -34,6 +38,7 @@ The minimal supported version of [NodeJS](https://nodejs.org/) for development i 1. Run `npm run docusaurus docs:version MAJ:MIN` in `website` folder to create new versioned docs. Note that there is not patch version in docs, only major and minor parts. 1. (optional) Remove docs for the oldest version (see ). +1. Handle the new version in `import-lightweight-charts-version.ts`: add a package reference for that version to `website/package.json` (e.g. `"lightweight-charts-MAJ.MIN": "npm:lightweight-charts@~MAJ.MIN.0"`) and a import of that package in a matching case statement. 1. Bump `lightweight-charts` package version in `website/package.json` file. 1. Add all created files to git and commit changes. Note that at this step the website cannot work since it uses unpublished so far version. It will be fixed in the next steps. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6503c56b08..cedc4f2300 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -32,7 +32,7 @@ When logging a bug, please be sure to include the following: ### Tests 1. Every pull request should have an adequate tests whenever it's possible (we have several [type of tests](./tests/), so you can find what works best for your changes). -1. If your changes affect paining, then your changes should contain a test case (or test cases) for [graphics tests](./tests/e2e/graphics). +1. If your changes affect painting, then your changes should contain a test case (or test cases) for [graphics tests](./tests/e2e/graphics). 1. Your pull request should pass CI (except checks marked as "not required" - in this case a reviewer should pay attention to job's artifacts). ### Git commit messages diff --git a/README.md b/README.md index 3e255f0446..93af1d5ce8 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,22 @@ +# About this fork + +This is a fork of TradingView's [Lightweight Charts](https://github.com/tradingview/lightweight-charts). +I needed certain features that weren't included yet (and maybe never) in the official version of LWC. +Specifically, I have an application that is very much like TradingView.com where each window can contain multiple charts, and each chart can have multiple indicator panes. + +[John Wallace](https://github.com/john-wallace-dev) +March 2022 + +## Multiple Panes + +Allows multiple panes below the main chart. I use them to display interactive indicators below the main chart. Implemented by [ntf](https://github.com/ntf) to satisfy requests from [Lightweight Charts issue 50](https://github.com/tradingview/lightweight-charts/issues/50). + +## Setting Crosshair Position + +Allows you to programmatically set the crosshair, which is useful when you want to synchronize the crosshair between multiple charts on one screen. Implemented by [trior](https://github.com/triorr) to satisfy requests from [Lightweight Charts issue 438](https://github.com/tradingview/lightweight-charts/issues/438). + +## From Main Branch +
@@ -16,7 +35,7 @@ -[Demos][demo-url] | [Documentation](https://tradingview.github.io/lightweight-charts/) | [Discord community](https://discord.gg/UC7cGkvn4U) +[Demos][demo-url] | [Documentation](https://tradingview.github.io/lightweight-charts/) | [Discord community](https://discord.gg/UC7cGkvn4U) | [Reddit](https://www.reddit.com/r/TradingView/) TradingView Lightweight Charts are one of the smallest and fastest financial HTML5 charts. diff --git a/package.json b/package.json index ada66e4a35..dd66b812f3 100644 --- a/package.json +++ b/package.json @@ -30,61 +30,62 @@ "charts" ], "engines": { - "node": ">=16.13.0" + "node": ">=16.16.0" }, "dependencies": { "fancy-canvas": "0.2.2" }, "devDependencies": { - "@rollup/plugin-node-resolve": "~13.1.3", - "@rollup/plugin-replace": "~3.1.0", - "@size-limit/file": "~7.0.5", - "@types/chai": "~4.3.0", - "@types/mocha": "~9.1.0", - "@types/node": "~16.11.26", - "@types/pixelmatch": "~5.2.1", - "@types/pngjs": "~6.0.0", - "@typescript-eslint/eslint-plugin": "~4.32.0", - "@typescript-eslint/eslint-plugin-tslint": "~4.32.0", - "@typescript-eslint/parser": "~4.32.0", - "bytes": "~3.1.1", + "@rollup/plugin-node-resolve": "~13.3.0", + "@rollup/plugin-replace": "~4.0.0", + "@size-limit/file": "~8.0.0", + "@types/chai": "~4.3.3", + "@types/mocha": "~9.1.1", + "@types/node": "~16.11.47", + "@types/pixelmatch": "~5.2.4", + "@types/pngjs": "~6.0.1", + "@typescript-eslint/eslint-plugin": "~5.33.0", + "@typescript-eslint/eslint-plugin-tslint": "~5.33.0", + "@typescript-eslint/parser": "~5.33.0", + "bytes": "~3.1.2", "chai": "~4.3.6", "chai-exclude": "~2.1.0", - "cross-env": "~7.0.0", - "dts-bundle-generator": "~6.5.0", + "cross-env": "~7.0.3", + "dts-bundle-generator": "~6.12.0", "eslint": "~7.32.0", - "eslint-plugin-deprecation": "~1.3.0", - "eslint-plugin-import": "~2.25.4", - "eslint-plugin-jsdoc": "~37.9.2", - "eslint-plugin-markdown": "~2.2.1", - "eslint-plugin-mdx": "~1.16.0", - "eslint-plugin-prefer-arrow": "~1.2.1", - "eslint-plugin-tsdoc": "~0.2.14", + "eslint-plugin-deprecation": "~1.3.2", + "eslint-plugin-import": "~2.26.0", + "eslint-plugin-jsdoc": "~39.3.6", + "eslint-plugin-markdown": "~3.0.0", + "eslint-plugin-mdx": "~1.17.0", + "eslint-plugin-prefer-arrow": "~1.2.3", + "eslint-plugin-react": "~7.30.1", + "eslint-plugin-tsdoc": "~0.2.16", "eslint-plugin-unicorn": "~40.1.0", - "eslint-plugin-react": "~7.28.0", - "express": "~4.17.2", - "glob": "~7.2.0", - "markdown-it": "~12.3.2", - "markdown-it-anchor": "~8.4.1", - "markdownlint-cli": "~0.31.1", - "mocha": "~9.2.0", + "express": "~4.18.1", + "glob": "~8.0.3", + "markdown-it": "~13.0.1", + "markdown-it-anchor": "~8.6.4", + "markdownlint-cli": "~0.32.1", + "mocha": "~10.0.0", "npm-run-all": "~4.1.5", - "pixelmatch": "~5.2.1", + "pixelmatch": "~5.3.0", "pngjs": "~6.0.0", - "puppeteer": "~13.3.2", + "puppeteer": "~16.1.1", "rimraf": "~3.0.2", - "rollup": "~2.67.3", + "rollup": "~2.77.2", "rollup-plugin-terser": "~7.0.2", - "size-limit": "~7.0.5", - "ts-node": "~10.5.0", + "size-limit": "~8.0.0", + "ts-node": "~10.9.1", "ts-transformer-properties-rename": "~0.13.0", "ts-transformer-strip-const-enums": "~1.0.1", - "tslib": "2.3.1", + "tslib": "2.4.0", "tslint": "6.1.3", "tslint-eslint-rules": "~5.4.0", "tslint-microsoft-contrib": "~6.2.0", "ttypescript": "~1.5.13", - "typescript": "4.6.2" + "typescript": "4.7.3", + "yargs": "~17.6.0" }, "scripts": { "postinstall": "npm run install-hooks", @@ -93,7 +94,7 @@ "bundle-dts": "tsc --noEmit --allowJs dts-config.js && dts-bundle-generator --config dts-config.js", "tsc": "ttsc -p tsconfig.prod.json", "tsc-watch": "npm run tsc -- --watch --preserveWatchOutput", - "tsc-verify": "tsc -b tsconfig.composite.json", + "tsc-verify": "node website/scripts/generate-versions-dts.js && tsc -b tsconfig.composite.json", "lint": "npm-run-all -p lint:**", "lint:eslint": "eslint --format=unix ./", "lint:md": "markdownlint -i \"**/node_modules/**\" -i \"**/website/docs/api/**\" -i \"**/website/versioned_docs/**/api/**\" \"**/*.md\"", diff --git a/scripts/run-interactions-tests.sh b/scripts/run-interactions-tests.sh new file mode 100755 index 0000000000..94485e7147 --- /dev/null +++ b/scripts/run-interactions-tests.sh @@ -0,0 +1,8 @@ +#!/bin/bash +set -e +echo "Preparing" + +npm run build + +echo "Interactions tests" +node ./tests/e2e/interactions/runner.js ./dist/lightweight-charts.standalone.development.js diff --git a/src/api/chart-api.ts b/src/api/chart-api.ts index 0a3094fb7a..96d4a42366 100644 --- a/src/api/chart-api.ts +++ b/src/api/chart-api.ts @@ -9,27 +9,23 @@ import { ChartOptions, ChartOptionsInternal } from '../model/chart-model'; import { Series } from '../model/series'; import { SeriesPlotRow } from '../model/series-data'; import { - AreaSeriesOptions, AreaSeriesPartialOptions, - BarSeriesOptions, BarSeriesPartialOptions, - BaselineSeriesOptions, BaselineSeriesPartialOptions, - CandlestickSeriesOptions, CandlestickSeriesPartialOptions, fillUpDownCandlesticksColors, - HistogramSeriesOptions, HistogramSeriesPartialOptions, - LineSeriesOptions, LineSeriesPartialOptions, precisionByMinMove, PriceFormat, PriceFormatBuiltIn, + SeriesOptionsMap, + SeriesPartialOptionsMap, + SeriesStyleOptionsMap, SeriesType, } from '../model/series-options'; import { Logical, Time } from '../model/time-data'; -import { CandlestickSeriesApi } from './candlestick-series-api'; import { DataUpdatesConsumer, isFulfilledData, SeriesDataItemTypeMap } from './data-consumer'; import { DataLayer, DataUpdateResponse, SeriesChanges } from './data-layer'; import { getSeriesDataCreator } from './get-series-data-creator'; @@ -65,7 +61,10 @@ function migrateHandleScaleScrollOptions(options: DeepPartial): vo if (isBoolean(options.handleScale)) { const handleScale = options.handleScale; options.handleScale = { - axisDoubleClickReset: handleScale, + axisDoubleClickReset: { + time: handleScale, + price: handleScale, + }, axisPressedMouseMove: { time: handleScale, price: handleScale, @@ -73,12 +72,20 @@ function migrateHandleScaleScrollOptions(options: DeepPartial): vo mouseWheel: handleScale, pinch: handleScale, }; - } else if (options.handleScale !== undefined && isBoolean(options.handleScale.axisPressedMouseMove)) { - const axisPressedMouseMove = options.handleScale.axisPressedMouseMove; - options.handleScale.axisPressedMouseMove = { - time: axisPressedMouseMove, - price: axisPressedMouseMove, - }; + } else if (options.handleScale !== undefined) { + const { axisPressedMouseMove, axisDoubleClickReset } = options.handleScale; + if (isBoolean(axisPressedMouseMove)) { + options.handleScale.axisPressedMouseMove = { + time: axisPressedMouseMove, + price: axisPressedMouseMove, + }; + } + if (isBoolean(axisDoubleClickReset)) { + options.handleScale.axisDoubleClickReset = { + time: axisDoubleClickReset, + price: axisDoubleClickReset, + }; + } } const handleScroll = options.handleScroll; @@ -158,84 +165,30 @@ export class ChartApi implements IChartApi, DataUpdatesConsumer { this._chartWidget.resize(width, height, forceRepaint); } - public addAreaSeries(options: AreaSeriesPartialOptions = {}): ISeriesApi<'Area'> { - patchPriceFormat(options.priceFormat); - - const strictOptions = merge(clone(seriesOptionsDefaults), areaStyleDefaults, options) as AreaSeriesOptions; - const series = this._chartWidget.model().createSeries('Area', strictOptions); - - const res = new SeriesApi<'Area'>(series, this, this); - this._seriesMap.set(res, series); - this._seriesMapReversed.set(series, res); - - return res; + public addAreaSeries(options?: AreaSeriesPartialOptions): ISeriesApi<'Area'> { + return this._addSeriesImpl('Area', areaStyleDefaults, options); } - public addBaselineSeries(options: BaselineSeriesPartialOptions = {}): ISeriesApi<'Baseline'> { - patchPriceFormat(options.priceFormat); - - // to avoid assigning fields to defaults we have to clone them - const strictOptions = merge(clone(seriesOptionsDefaults), clone(baselineStyleDefaults), options) as BaselineSeriesOptions; - const series = this._chartWidget.model().createSeries('Baseline', strictOptions); - - const res = new SeriesApi<'Baseline'>(series, this, this); - this._seriesMap.set(res, series); - this._seriesMapReversed.set(series, res); - - return res; + public addBaselineSeries(options?: BaselineSeriesPartialOptions): ISeriesApi<'Baseline'> { + return this._addSeriesImpl('Baseline', baselineStyleDefaults, options); } - public addBarSeries(options: BarSeriesPartialOptions = {}): ISeriesApi<'Bar'> { - patchPriceFormat(options.priceFormat); - - const strictOptions = merge(clone(seriesOptionsDefaults), barStyleDefaults, options) as BarSeriesOptions; - const series = this._chartWidget.model().createSeries('Bar', strictOptions); - - const res = new SeriesApi<'Bar'>(series, this, this); - this._seriesMap.set(res, series); - this._seriesMapReversed.set(series, res); - - return res; + public addBarSeries(options?: BarSeriesPartialOptions): ISeriesApi<'Bar'> { + return this._addSeriesImpl('Bar', barStyleDefaults, options); } public addCandlestickSeries(options: CandlestickSeriesPartialOptions = {}): ISeriesApi<'Candlestick'> { fillUpDownCandlesticksColors(options); - patchPriceFormat(options.priceFormat); - - const strictOptions = merge(clone(seriesOptionsDefaults), candlestickStyleDefaults, options) as CandlestickSeriesOptions; - const series = this._chartWidget.model().createSeries('Candlestick', strictOptions); - - const res = new CandlestickSeriesApi(series, this, this); - this._seriesMap.set(res, series); - this._seriesMapReversed.set(series, res); - return res; + return this._addSeriesImpl('Candlestick', candlestickStyleDefaults, options); } - public addHistogramSeries(options: HistogramSeriesPartialOptions = {}): ISeriesApi<'Histogram'> { - patchPriceFormat(options.priceFormat); - - const strictOptions = merge(clone(seriesOptionsDefaults), histogramStyleDefaults, options) as HistogramSeriesOptions; - const series = this._chartWidget.model().createSeries('Histogram', strictOptions); - - const res = new SeriesApi<'Histogram'>(series, this, this); - this._seriesMap.set(res, series); - this._seriesMapReversed.set(series, res); - - return res; + public addHistogramSeries(options?: HistogramSeriesPartialOptions): ISeriesApi<'Histogram'> { + return this._addSeriesImpl('Histogram', histogramStyleDefaults, options); } - public addLineSeries(options: LineSeriesPartialOptions = {}): ISeriesApi<'Line'> { - patchPriceFormat(options.priceFormat); - - const strictOptions = merge(clone(seriesOptionsDefaults), lineStyleDefaults, options) as LineSeriesOptions; - const series = this._chartWidget.model().createSeries('Line', strictOptions); - - const res = new SeriesApi<'Line'>(series, this, this); - this._seriesMap.set(res, series); - this._seriesMapReversed.set(series, res); - - return res; + public addLineSeries(options?: LineSeriesPartialOptions): ISeriesApi<'Line'> { + return this._addSeriesImpl('Line', lineStyleDefaults, options); } public removeSeries(seriesApi: SeriesApi): void { @@ -275,6 +228,10 @@ export class ChartApi implements IChartApi, DataUpdatesConsumer { this._crosshairMovedDelegate.unsubscribe(handler); } + public setCrossHair(x: number, y: number, visible: boolean): void { + this._chartWidget.paneWidgets()[0].setCrossHair(x, y, visible); + } + public priceScale(priceScaleId: string): IPriceScaleApi { return new PriceScaleApi(this._chartWidget, priceScaleId); } @@ -307,6 +264,23 @@ export class ChartApi implements IChartApi, DataUpdatesConsumer { return this._chartWidget.paneWidgets().map((paneWidget: PaneWidget) => paneWidget.getPaneCell()); } + private _addSeriesImpl( + type: TSeries, + styleDefaults: SeriesStyleOptionsMap[TSeries], + options: SeriesPartialOptionsMap[TSeries] = {} + ): ISeriesApi { + patchPriceFormat(options.priceFormat); + + const strictOptions = merge(clone(seriesOptionsDefaults), clone(styleDefaults), options) as SeriesOptionsMap[TSeries]; + const series = this._chartWidget.model().createSeries(type, strictOptions); + + const res = new SeriesApi(series, this, this); + this._seriesMap.set(res, series); + this._seriesMapReversed.set(series, res); + + return res; + } + private _sendUpdateToChart(update: DataUpdateResponse): void { const model = this._chartWidget.model(); diff --git a/src/api/data-consumer.ts b/src/api/data-consumer.ts index 4e6085029c..43af539dab 100644 --- a/src/api/data-consumer.ts +++ b/src/api/data-consumer.ts @@ -82,6 +82,61 @@ export interface HistogramData extends SingleValueData { color?: string; } +/** + * Structure describing a single item of data for area series + */ +export interface AreaData extends SingleValueData { + /** + * Optional line color value for certain data item. If missed, color from options is used + */ + lineColor?: string; + + /** + * Optional top color value for certain data item. If missed, color from options is used + */ + topColor?: string; + + /** + * Optional bottom color value for certain data item. If missed, color from options is used + */ + bottomColor?: string; +} + +/** + * Structure describing a single item of data for baseline series + */ +export interface BaselineData extends SingleValueData { + /** + * Optional top area top fill color value for certain data item. If missed, color from options is used + */ + topFillColor1?: string; + + /** + * Optional top area bottom fill color value for certain data item. If missed, color from options is used + */ + topFillColor2?: string; + + /** + * Optional top area line color value for certain data item. If missed, color from options is used + */ + topLineColor?: string; + + /** + * Optional bottom area top fill color value for certain data item. If missed, color from options is used + */ + bottomFillColor1?: string; + + /** + * Optional bottom area bottom fill color value for certain data item. If missed, color from options is used + */ + bottomFillColor2?: string; + + /** + * Optional bottom area line color value for certain data item. If missed, color from options is used + */ + bottomLineColor?: string; +} + /** * Represents a bar with a {@link Time} and open, high, low, and close prices. */ @@ -162,11 +217,11 @@ export interface SeriesDataItemTypeMap { /** * The types of area series data. */ - Area: SingleValueData | WhitespaceData; + Area: AreaData | WhitespaceData; /** * The types of baseline series data. */ - Baseline: SingleValueData | WhitespaceData; + Baseline: BaselineData | WhitespaceData; /** * The types of line series data. */ diff --git a/src/api/data-layer.ts b/src/api/data-layer.ts index bec1a28828..1fc36a5b63 100644 --- a/src/api/data-layer.ts +++ b/src/api/data-layer.ts @@ -260,7 +260,7 @@ export class DataLayer { } } - let seriesRows: (SeriesPlotRow | WhitespacePlotRow)[] = []; + let seriesRows: (SeriesPlotRow | WhitespacePlotRow)[] = []; if (data.length !== 0) { const extendedData = data as SeriesDataItemWithOriginalTime[]; diff --git a/src/api/data-validators.ts b/src/api/data-validators.ts index 931475d6cf..8ae6428a30 100644 --- a/src/api/data-validators.ts +++ b/src/api/data-validators.ts @@ -1,6 +1,6 @@ import { assert } from '../helpers/assertions'; -import { PriceLineOptions } from '../model/price-line-options'; +import { CreatePriceLineOptions } from '../model/price-line-options'; import { SeriesMarker } from '../model/series-markers'; import { SeriesType } from '../model/series-options'; import { Time } from '../model/time-data'; @@ -8,7 +8,7 @@ import { Time } from '../model/time-data'; import { isFulfilledData, SeriesDataItemTypeMap } from './data-consumer'; import { convertTime } from './data-layer'; -export function checkPriceLineOptions(options: PriceLineOptions): void { +export function checkPriceLineOptions(options: CreatePriceLineOptions): void { if (process.env.NODE_ENV === 'production') { return; } diff --git a/src/api/get-series-data-creator.ts b/src/api/get-series-data-creator.ts index a0996ca71a..26448936c7 100644 --- a/src/api/get-series-data-creator.ts +++ b/src/api/get-series-data-creator.ts @@ -1,6 +1,8 @@ import { PlotRow, PlotRowValueIndex } from '../model/plot-data'; import { + AreaPlotRow, BarPlotRow, + BaselinePlotRow, CandlestickPlotRow, LinePlotRow, SeriesPlotRow, @@ -9,7 +11,9 @@ import { SeriesType } from '../model/series-options'; import { Time } from '../model/time-data'; import { + AreaData, BarData, + BaselineData, CandlestickData, LineData, OhlcData, @@ -38,6 +42,54 @@ function lineData(plotRow: LinePlotRow): LineData { return result; } +function areaData(plotRow: AreaPlotRow): AreaData { + const result: AreaData = singleValueData(plotRow); + + if (plotRow.lineColor !== undefined) { + result.lineColor = plotRow.lineColor; + } + + if (plotRow.topColor !== undefined) { + result.topColor = plotRow.topColor; + } + + if (plotRow.bottomColor !== undefined) { + result.bottomColor = plotRow.bottomColor; + } + + return result; +} + +function baselineData(plotRow: BaselinePlotRow): BaselineData { + const result: BaselineData = singleValueData(plotRow); + + if (plotRow.topLineColor !== undefined) { + result.topLineColor = plotRow.topLineColor; + } + + if (plotRow.bottomLineColor !== undefined) { + result.bottomLineColor = plotRow.bottomLineColor; + } + + if (plotRow.topFillColor1 !== undefined) { + result.topFillColor1 = plotRow.topFillColor1; + } + + if (plotRow.topFillColor2 !== undefined) { + result.topFillColor2 = plotRow.topFillColor2; + } + + if (plotRow.bottomFillColor1 !== undefined) { + result.bottomFillColor1 = plotRow.bottomFillColor1; + } + + if (plotRow.bottomFillColor2 !== undefined) { + result.bottomFillColor2 = plotRow.bottomFillColor2; + } + + return result; +} + function ohlcData(plotRow: PlotRow): OhlcData { return { open: plotRow.value[PlotRowValueIndex.Open], @@ -78,9 +130,9 @@ function candlestickData(plotRow: CandlestickPlotRow): CandlestickData { } const seriesPlotRowToDataMap: SeriesPlotRowToDataMap = { - Area: singleValueData, + Area: areaData, Line: lineData, - Baseline: singleValueData, + Baseline: baselineData, Histogram: lineData, Bar: barData, Candlestick: candlestickData, diff --git a/src/api/get-series-plot-row-creator.ts b/src/api/get-series-plot-row-creator.ts index 73c94d727f..accb37b002 100644 --- a/src/api/get-series-plot-row-creator.ts +++ b/src/api/get-series-plot-row-creator.ts @@ -3,33 +3,76 @@ import { SeriesPlotRow } from '../model/series-data'; import { SeriesType } from '../model/series-options'; import { OriginalTime, TimePoint, TimePointIndex } from '../model/time-data'; -import { BarData, CandlestickData, HistogramData, isWhitespaceData, LineData, SeriesDataItemTypeMap } from './data-consumer'; - -function getLineBasedSeriesPlotRow(time: TimePoint, index: TimePointIndex, item: LineData | HistogramData, originalTime: OriginalTime): Mutable> { - const val = item.value; - return { index, time, value: [val, val, val, val], originalTime }; -} +import { AreaData, BarData, BaselineData, CandlestickData, HistogramData, isWhitespaceData, LineData, SeriesDataItemTypeMap } from './data-consumer'; function getColoredLineBasedSeriesPlotRow(time: TimePoint, index: TimePointIndex, item: LineData | HistogramData, originalTime: OriginalTime): Mutable> { const val = item.value; const res: Mutable> = { index, time, value: [val, val, val, val], originalTime }; - // '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) { + if (item.color !== undefined) { res.color = item.color; } return res; } +function getAreaSeriesPlotRow(time: TimePoint, index: TimePointIndex, item: AreaData, originalTime: OriginalTime): Mutable> { + const val = item.value; + + const res: Mutable> = { index, time, value: [val, val, val, val], originalTime }; + + if (item.lineColor !== undefined) { + res.lineColor = item.lineColor; + } + + if (item.topColor !== undefined) { + res.topColor = item.topColor; + } + + if (item.bottomColor !== undefined) { + res.bottomColor = item.bottomColor; + } + + return res; +} + +function getBaselineSeriesPlotRow(time: TimePoint, index: TimePointIndex, item: BaselineData, originalTime: OriginalTime): Mutable> { + const val = item.value; + + const res: Mutable> = { index, time, value: [val, val, val, val], originalTime }; + + if (item.topLineColor !== undefined) { + res.topLineColor = item.topLineColor; + } + + if (item.bottomLineColor !== undefined) { + res.bottomLineColor = item.bottomLineColor; + } + + if (item.topFillColor1 !== undefined) { + res.topFillColor1 = item.topFillColor1; + } + + if (item.topFillColor2 !== undefined) { + res.topFillColor2 = item.topFillColor2; + } + + if (item.bottomFillColor1 !== undefined) { + res.bottomFillColor1 = item.bottomFillColor1; + } + + if (item.bottomFillColor2 !== undefined) { + res.bottomFillColor2 = item.bottomFillColor2; + } + + return res; +} + function getBarSeriesPlotRow(time: TimePoint, index: TimePointIndex, item: BarData, originalTime: OriginalTime): Mutable> { const res: Mutable> = { index, time, value: [item.open, item.high, item.low, item.close], originalTime }; - // '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) { + if (item.color !== undefined) { res.color = item.color; } @@ -38,22 +81,15 @@ function getBarSeriesPlotRow(time: TimePoint, index: TimePointIndex, item: BarDa function getCandlestickSeriesPlotRow(time: TimePoint, index: TimePointIndex, item: CandlestickData, originalTime: OriginalTime): Mutable> { const res: Mutable> = { index, time, value: [item.open, item.high, item.low, item.close], originalTime }; - - // '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) { + if (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) { + if (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) { + if (item.wickColor !== undefined) { res.wickColor = item.wickColor; } @@ -66,17 +102,11 @@ export function isSeriesPlotRow(row: SeriesPlotRow | WhitespacePlotRow): row is return (row as Partial).value !== undefined; } -// we want to have compile-time checks that the type of the functions is correct -// but due contravariance we cannot easily use type of values of the SeriesItemValueFnMap map itself -// so let's use TimedSeriesItemValueFn for shut up the compiler in seriesItemValueFn -// we need to be sure (and we're sure actually) that stored data has correct type for it's according series object type SeriesItemValueFnMap = { - [T in keyof SeriesDataItemTypeMap]: (time: TimePoint, index: TimePointIndex, item: SeriesDataItemTypeMap[T], originalTime: OriginalTime) => Mutable; + [T in keyof SeriesDataItemTypeMap]: (time: TimePoint, index: TimePointIndex, item: SeriesDataItemTypeMap[T], originalTime: OriginalTime) => Mutable | WhitespacePlotRow>; }; -export type TimedSeriesItemValueFn = (time: TimePoint, index: TimePointIndex, item: SeriesDataItemTypeMap[SeriesType], originalTime: OriginalTime) => Mutable; - -function wrapWhitespaceData(createPlotRowFn: (typeof getLineBasedSeriesPlotRow) | (typeof getBarSeriesPlotRow) | (typeof getCandlestickSeriesPlotRow)): TimedSeriesItemValueFn { +function wrapWhitespaceData(createPlotRowFn: (typeof getBaselineSeriesPlotRow) | (typeof getBarSeriesPlotRow) | (typeof getCandlestickSeriesPlotRow)): SeriesItemValueFnMap[TSeriesType] { return (time: TimePoint, index: TimePointIndex, bar: SeriesDataItemTypeMap[SeriesType], originalTime: OriginalTime) => { if (isWhitespaceData(bar)) { return { time, index, originalTime }; @@ -89,12 +119,12 @@ function wrapWhitespaceData(createPlotRowFn: (typeof getLineBasedSeriesPlotRow) const seriesPlotRowFnMap: SeriesItemValueFnMap = { Candlestick: wrapWhitespaceData(getCandlestickSeriesPlotRow), Bar: wrapWhitespaceData(getBarSeriesPlotRow), - Area: wrapWhitespaceData(getLineBasedSeriesPlotRow), - Baseline: wrapWhitespaceData(getLineBasedSeriesPlotRow), + Area: wrapWhitespaceData(getAreaSeriesPlotRow), + Baseline: wrapWhitespaceData(getBaselineSeriesPlotRow), Histogram: wrapWhitespaceData(getColoredLineBasedSeriesPlotRow), Line: wrapWhitespaceData(getColoredLineBasedSeriesPlotRow), }; -export function getSeriesPlotRowCreator(seriesType: SeriesType): TimedSeriesItemValueFn { - return seriesPlotRowFnMap[seriesType] as TimedSeriesItemValueFn; +export function getSeriesPlotRowCreator(seriesType: TSeriesType): SeriesItemValueFnMap[TSeriesType] { + return seriesPlotRowFnMap[seriesType]; } diff --git a/src/api/ichart-api.ts b/src/api/ichart-api.ts index 60ce2fc75d..f17428d487 100644 --- a/src/api/ichart-api.ts +++ b/src/api/ichart-api.ts @@ -209,7 +209,7 @@ export interface IChartApi { * console.log(`Crosshair moved to ${param.point.x}, ${param.point.y}. The time is ${param.time}.`); * } * - * chart.subscribeClick(myCrosshairMoveHandler); + * chart.subscribeCrosshairMove(myCrosshairMoveHandler); * ``` */ subscribeCrosshairMove(handler: MouseEventHandler): void; @@ -225,6 +225,15 @@ export interface IChartApi { */ unsubscribeCrosshairMove(handler: MouseEventHandler): void; + /** + * Move the crosshair to the specified position. + * + * @param x - horizontal pixel coordinate + * @param y - vertical pixel coordinate + * @param visible - true for the crosshair to be visible, false for invisible + */ + setCrossHair(x: number, y: number, visible: boolean): void; + /** * Returns API to manipulate a price scale. * diff --git a/src/api/iseries-api.ts b/src/api/iseries-api.ts index 0aa1f59914..dbe2d1d70a 100644 --- a/src/api/iseries-api.ts +++ b/src/api/iseries-api.ts @@ -3,7 +3,7 @@ import { IPriceFormatter } from '../formatters/iprice-formatter'; import { BarPrice } from '../model/bar'; import { Coordinate } from '../model/coordinate'; import { MismatchDirection } from '../model/plot-list'; -import { PriceLineOptions } from '../model/price-line-options'; +import { CreatePriceLineOptions } from '../model/price-line-options'; import { SeriesMarker } from '../model/series-markers'; import { SeriesOptionsMap, @@ -222,7 +222,7 @@ export interface ISeriesApi { /** * Creates a new price line * - * @param options - Any subset of options. + * @param options - Any subset of options, however `price` is required. * @example * ```js * const priceLine = series.createPriceLine({ @@ -235,7 +235,7 @@ export interface ISeriesApi { * }); * ``` */ - createPriceLine(options: PriceLineOptions): IPriceLine; + createPriceLine(options: CreatePriceLineOptions): IPriceLine; /** * Removes the price line that was created before. diff --git a/src/api/options/chart-options-defaults.ts b/src/api/options/chart-options-defaults.ts index 557fa87952..4c6c6d19ea 100644 --- a/src/api/options/chart-options-defaults.ts +++ b/src/api/options/chart-options-defaults.ts @@ -47,7 +47,10 @@ export const chartOptionsDefaults: ChartOptionsInternal = { time: true, price: true, }, - axisDoubleClickReset: true, + axisDoubleClickReset: { + time: true, + price: true, + }, mouseWheel: true, pinch: true, }, diff --git a/src/api/options/crosshair-options-defaults.ts b/src/api/options/crosshair-options-defaults.ts index 7d04145b19..ccd985acec 100644 --- a/src/api/options/crosshair-options-defaults.ts +++ b/src/api/options/crosshair-options-defaults.ts @@ -3,20 +3,20 @@ import { LineStyle } from '../../renderers/draw-line'; export const crosshairOptionsDefaults: CrosshairOptions = { vertLine: { - color: '#758696', + color: '#9598A1', width: 1, style: LineStyle.LargeDashed, visible: true, labelVisible: true, - labelBackgroundColor: '#4c525e', + labelBackgroundColor: '#131722', }, horzLine: { - color: '#758696', + color: '#9598A1', width: 1, style: LineStyle.LargeDashed, visible: true, labelVisible: true, - labelBackgroundColor: '#4c525e', + labelBackgroundColor: '#131722', }, mode: CrosshairMode.Magnet, }; diff --git a/src/api/options/layout-options-defaults.ts b/src/api/options/layout-options-defaults.ts index 79b9fe79fb..e878cecf9c 100644 --- a/src/api/options/layout-options-defaults.ts +++ b/src/api/options/layout-options-defaults.ts @@ -8,6 +8,6 @@ export const layoutOptionsDefaults: LayoutOptions = { color: '#FFFFFF', }, textColor: '#191919', - fontSize: 11, + fontSize: 12, fontFamily: defaultFontFamily, }; diff --git a/src/api/options/price-scale-options-defaults.ts b/src/api/options/price-scale-options-defaults.ts index d3b22a1d86..c7fc83fee9 100644 --- a/src/api/options/price-scale-options-defaults.ts +++ b/src/api/options/price-scale-options-defaults.ts @@ -9,7 +9,7 @@ export const priceScaleOptionsDefaults: PriceScaleOptions = { borderColor: '#2B2B43', entireTextOnly: false, visible: false, - ticksVisible: true, + ticksVisible: false, scaleMargins: { bottom: 0.1, top: 0.2, diff --git a/src/api/options/series-options-defaults.ts b/src/api/options/series-options-defaults.ts index 93aacf3ab7..5cee840180 100644 --- a/src/api/options/series-options-defaults.ts +++ b/src/api/options/series-options-defaults.ts @@ -39,6 +39,7 @@ export const lineStyleDefaults: LineStyleOptions = { crosshairMarkerVisible: true, crosshairMarkerRadius: 4, crosshairMarkerBorderColor: '', + crosshairMarkerBorderWidth: 2, crosshairMarkerBackgroundColor: '', lastPriceAnimation: LastPriceAnimationMode.Disabled, }; @@ -46,6 +47,7 @@ export const lineStyleDefaults: LineStyleOptions = { export const areaStyleDefaults: AreaStyleOptions = { topColor: 'rgba( 46, 220, 135, 0.4)', bottomColor: 'rgba( 40, 221, 100, 0)', + invertFilledArea: false, lineColor: '#33D778', lineStyle: LineStyle.Solid, lineWidth: 3, @@ -53,6 +55,7 @@ export const areaStyleDefaults: AreaStyleOptions = { crosshairMarkerVisible: true, crosshairMarkerRadius: 4, crosshairMarkerBorderColor: '', + crosshairMarkerBorderWidth: 2, crosshairMarkerBackgroundColor: '', lastPriceAnimation: LastPriceAnimationMode.Disabled, }; @@ -78,6 +81,7 @@ export const baselineStyleDefaults: BaselineStyleOptions = { crosshairMarkerVisible: true, crosshairMarkerRadius: 4, crosshairMarkerBorderColor: '', + crosshairMarkerBorderWidth: 2, crosshairMarkerBackgroundColor: '', lastPriceAnimation: LastPriceAnimationMode.Disabled, diff --git a/src/api/options/time-scale-options-defaults.ts b/src/api/options/time-scale-options-defaults.ts index 473e1362bd..3863d67298 100644 --- a/src/api/options/time-scale-options-defaults.ts +++ b/src/api/options/time-scale-options-defaults.ts @@ -14,5 +14,5 @@ export const timeScaleOptionsDefaults: TimeScaleOptions = { timeVisible: false, secondsVisible: true, shiftVisibleRangeOnNewBar: true, - ticksVisible: true, + ticksVisible: false, }; diff --git a/src/api/series-api.ts b/src/api/series-api.ts index 017785bb47..3118106e63 100644 --- a/src/api/series-api.ts +++ b/src/api/series-api.ts @@ -6,7 +6,7 @@ import { clone, merge } from '../helpers/strict-type-checks'; import { BarPrice } from '../model/bar'; import { Coordinate } from '../model/coordinate'; import { MismatchDirection } from '../model/plot-list'; -import { PriceLineOptions } from '../model/price-line-options'; +import { CreatePriceLineOptions, PriceLineOptions } from '../model/price-line-options'; import { RangeImpl } from '../model/range-impl'; import { Series } from '../model/series'; import { SeriesMarker } from '../model/series-markers'; @@ -170,7 +170,7 @@ export class SeriesApi implements ISeriesApi MouseEventParamsImpl; +const windowsChrome = isChromiumBased() && isWindows(); + export class ChartWidget implements IDestroyable { private readonly _options: ChartOptionsInternal; private _paneWidgets: PaneWidget[] = []; @@ -71,8 +74,9 @@ export class ChartWidget implements IDestroyable { this._element.appendChild(this._tableElement); this._onWheelBound = this._onMousewheel.bind(this); - this._element.addEventListener('wheel', this._onWheelBound, { passive: false }); - + if (shouldSubscribeMouseWheel(this._options)) { + this._setMouseWheelEventListener(true); + } this._model = new ChartModel( this._invalidateHandler.bind(this), this._options @@ -131,7 +135,7 @@ export class ChartWidget implements IDestroyable { } public destroy(): void { - this._element.removeEventListener('wheel', this._onWheelBound); + this._setMouseWheelEventListener(false); if (this._drawRafId !== 0) { window.cancelAnimationFrame(this._drawRafId); } @@ -181,7 +185,7 @@ export class ChartWidget implements IDestroyable { this._tableElement.style.width = widthStr; if (forceRepaint) { - this._drawImpl(new InvalidateMask(InvalidationLevel.Full)); + this._drawImpl(InvalidateMask.full(), performance.now()); } else { this._model.fullUpdate(); } @@ -189,7 +193,7 @@ export class ChartWidget implements IDestroyable { public paint(invalidateMask?: InvalidateMask): void { if (invalidateMask === undefined) { - invalidateMask = new InvalidateMask(InvalidationLevel.Full); + invalidateMask = InvalidateMask.full(); } for (let i = 0; i < this._paneWidgets.length; i++) { @@ -202,10 +206,18 @@ export class ChartWidget implements IDestroyable { } public applyOptions(options: DeepPartial): void { + const currentlyHasMouseWheelListener = shouldSubscribeMouseWheel(this._options); + // we don't need to merge options here because it's done in chart model // and since both model and widget share the same object it will be done automatically for widget as well // not ideal solution for sure, but it work's for now ¯\_(ツ)_/¯ this._model.applyOptions(options); + + const shouldHaveMouseWheelListener = shouldSubscribeMouseWheel(this._options); + if (shouldHaveMouseWheelListener !== currentlyHasMouseWheelListener) { + this._setMouseWheelEventListener(shouldHaveMouseWheelListener); + } + this._updateTimeAxisVisibility(); const width = options.width || this._width; @@ -224,7 +236,7 @@ export class ChartWidget implements IDestroyable { public takeScreenshot(): HTMLCanvasElement { if (this._invalidateMask !== null) { - this._drawImpl(this._invalidateMask); + this._drawImpl(this._invalidateMask, performance.now()); this._invalidateMask = null; } // calculate target size @@ -414,31 +426,49 @@ export class ChartWidget implements IDestroyable { } } - private _onMousewheel(event: WheelEvent): void { - let deltaX = event.deltaX / 100; - let deltaY = -(event.deltaY / 100); - - if ((deltaX === 0 || !this._options.handleScroll.mouseWheel) && - (deltaY === 0 || !this._options.handleScale.mouseWheel)) { + private _setMouseWheelEventListener(add: boolean): void { + if (add) { + this._element.addEventListener('wheel', this._onWheelBound, { passive: false }); return; } + this._element.removeEventListener('wheel', this._onWheelBound); + } - if (event.cancelable) { - event.preventDefault(); - } - + private _determineWheelSpeedAdjustment(event: WheelEvent): number { switch (event.deltaMode) { case event.DOM_DELTA_PAGE: // one screen at time scroll mode - deltaX *= 120; - deltaY *= 120; - break; - + return 120; case event.DOM_DELTA_LINE: // one line at time scroll mode - deltaX *= 32; - deltaY *= 32; - break; + return 32; + } + + if (!windowsChrome) { + return 1; + } + + // Chromium on Windows has a bug where the scroll speed isn't correctly + // adjusted for high density displays. We need to correct for this so that + // scroll speed is consistent between browsers. + // https://bugs.chromium.org/p/chromium/issues/detail?id=1001735 + // https://bugs.chromium.org/p/chromium/issues/detail?id=1207308 + return (1 / window.devicePixelRatio); + } + + private _onMousewheel(event: WheelEvent): void { + if ((event.deltaX === 0 || !this._options.handleScroll.mouseWheel) && + (event.deltaY === 0 || !this._options.handleScale.mouseWheel)) { + return; + } + + const scrollSpeedAdjustment = this._determineWheelSpeedAdjustment(event); + + const deltaX = scrollSpeedAdjustment * event.deltaX / 100; + const deltaY = -(scrollSpeedAdjustment * event.deltaY / 100); + + if (event.cancelable) { + event.preventDefault(); } if (deltaY !== 0 && this._options.handleScale.mouseWheel) { @@ -452,7 +482,7 @@ export class ChartWidget implements IDestroyable { } } - private _drawImpl(invalidateMask: InvalidateMask): void { + private _drawImpl(invalidateMask: InvalidateMask, time: number): void { const invalidationType = invalidateMask.fullInvalidation(); // actions for full invalidation ONLY (not shared with light) @@ -466,7 +496,7 @@ export class ChartWidget implements IDestroyable { invalidationType === InvalidationLevel.Light ) { this._applyMomentaryAutoScale(invalidateMask); - this._applyTimeScaleInvalidations(invalidateMask); + this._applyTimeScaleInvalidations(invalidateMask, time); this._timeAxisWidget.update(); this._paneWidgets.forEach((pane: PaneWidget) => { @@ -483,7 +513,7 @@ export class ChartWidget implements IDestroyable { this._updateGui(); this._applyMomentaryAutoScale(this._invalidateMask); - this._applyTimeScaleInvalidations(this._invalidateMask); + this._applyTimeScaleInvalidations(this._invalidateMask, time); invalidateMask = this._invalidateMask; this._invalidateMask = null; @@ -493,10 +523,9 @@ export class ChartWidget implements IDestroyable { this.paint(invalidateMask); } - private _applyTimeScaleInvalidations(invalidateMask: InvalidateMask): void { - const timeScaleInvalidations = invalidateMask.timeScaleInvalidations(); - for (const tsInvalidation of timeScaleInvalidations) { - this._applyTimeScaleInvalidation(tsInvalidation); + private _applyTimeScaleInvalidations(invalidateMask: InvalidateMask, time: number): void { + for (const tsInvalidation of invalidateMask.timeScaleInvalidations()) { + this._applyTimeScaleInvalidation(tsInvalidation, time); } } @@ -509,7 +538,7 @@ export class ChartWidget implements IDestroyable { } } - private _applyTimeScaleInvalidation(invalidation: TimeScaleInvalidation): void { + private _applyTimeScaleInvalidation(invalidation: TimeScaleInvalidation, time: number): void { const timeScale = this._model.timeScale(); switch (invalidation.type) { case TimeScaleInvalidationType.FitContent: @@ -527,6 +556,11 @@ export class ChartWidget implements IDestroyable { case TimeScaleInvalidationType.Reset: timeScale.restoreDefault(); break; + case TimeScaleInvalidationType.Animation: + if (!invalidation.value.finished(time)) { + timeScale.setRightOffset(invalidation.value.getPosition(time)); + } + break; } } @@ -539,14 +573,21 @@ export class ChartWidget implements IDestroyable { if (!this._drawPlanned) { this._drawPlanned = true; - this._drawRafId = window.requestAnimationFrame(() => { + this._drawRafId = window.requestAnimationFrame((time: number) => { this._drawPlanned = false; this._drawRafId = 0; if (this._invalidateMask !== null) { const mask = this._invalidateMask; this._invalidateMask = null; - this._drawImpl(mask); + this._drawImpl(mask, time); + + for (const tsInvalidation of mask.timeScaleInvalidations()) { + if (tsInvalidation.type === TimeScaleInvalidationType.Animation && !tsInvalidation.value.finished(time)) { + this.model().setTimeScaleAnimation(tsInvalidation.value); + break; + } + } } }); } @@ -686,3 +727,7 @@ function disableSelection(element: HTMLElement): void { // eslint-disable-next-line @typescript-eslint/no-explicit-any,@typescript-eslint/no-unsafe-member-access (element.style as any).webkitTapHighlightColor = 'transparent'; } + +function shouldSubscribeMouseWheel(options: ChartOptionsInternal): boolean { + return Boolean(options.handleScroll.mouseWheel || options.handleScale.mouseWheel); +} diff --git a/src/gui/labels-image-cache.ts b/src/gui/labels-image-cache.ts deleted file mode 100644 index 2820543e65..0000000000 --- a/src/gui/labels-image-cache.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { createPreconfiguredCanvas, getCanvasDevicePixelRatio, getContext2D, Size } from '../gui/canvas-utils'; - -import { ensureDefined } from '../helpers/assertions'; -import { drawScaled } from '../helpers/canvas-helpers'; -import { IDestroyable } from '../helpers/idestroyable'; -import { makeFont } from '../helpers/make-font'; -import { ceiledEven } from '../helpers/mathex'; - -import { TextWidthCache } from '../model/text-width-cache'; - -const MAX_COUNT = 200; - -interface Item { - text: string; - textWidth: number; - width: number; - height: number; - canvas: HTMLCanvasElement; -} - -export class LabelsImageCache implements IDestroyable { - private _textWidthCache: TextWidthCache = new TextWidthCache(MAX_COUNT); - private _fontSize: number = 0; - private _color: string = ''; - private _font: string = ''; - private _keys: string[] = []; - private _hash: Map = new Map(); - - public constructor(fontSize: number, color: string, fontFamily?: string, fontStyle?: string) { - this._fontSize = fontSize; - this._color = color; - this._font = makeFont(fontSize, fontFamily, fontStyle); - } - - public destroy(): void { - this._textWidthCache.reset(); - this._keys = []; - this._hash.clear(); - } - - public paintTo(ctx: CanvasRenderingContext2D, text: string, x: number, y: number, align: string): void { - const label = this._getLabelImage(ctx, text); - if (align !== 'left') { - const pixelRatio = getCanvasDevicePixelRatio(ctx.canvas); - x -= Math.floor(label.textWidth * pixelRatio); - } - - y -= Math.floor(label.height / 2); - - ctx.drawImage( - label.canvas, - x, y, - label.width, label.height - ); - } - - private _getLabelImage(ctx: CanvasRenderingContext2D, text: string): Item { - let item: Item; - if (this._hash.has(text)) { - // Cache hit! - item = ensureDefined(this._hash.get(text)); - } else { - if (this._keys.length >= MAX_COUNT) { - const key = ensureDefined(this._keys.shift()); - this._hash.delete(key); - } - - const pixelRatio = getCanvasDevicePixelRatio(ctx.canvas); - - const margin = Math.ceil(this._fontSize / 4.5); - const baselineOffset = Math.round(this._fontSize / 10); - const textWidth = Math.ceil(this._textWidthCache.measureText(ctx, text)); - const width = ceiledEven(Math.round(textWidth + margin * 2)); - const height = ceiledEven(this._fontSize + margin * 2); - const canvas = createPreconfiguredCanvas(document, new Size(width, height)); - - // Allocate new - item = { - text: text, - textWidth: Math.round(Math.max(1, textWidth)), - width: Math.ceil(width * pixelRatio), - height: Math.ceil(height * pixelRatio), - canvas: canvas, - }; - - if (textWidth !== 0) { - this._keys.push(item.text); - this._hash.set(item.text, item); - } - - ctx = getContext2D(item.canvas); - drawScaled(ctx, pixelRatio, () => { - ctx.font = this._font; - ctx.fillStyle = this._color; - ctx.fillText(text, 0, height - margin - baselineOffset); - }); - } - - return item; - } -} diff --git a/src/gui/mouse-event-handler.ts b/src/gui/mouse-event-handler.ts index 94b38585fc..eb104627cb 100644 --- a/src/gui/mouse-event-handler.ts +++ b/src/gui/mouse-event-handler.ts @@ -613,9 +613,15 @@ export class MouseEventHandler implements IDestroyable { if (!this._handler.mouseDownOutsideEvent) { return; } + + if (event.composed && this._target.contains(event.composedPath()[0] as Element)) { + return; + } + if (event.target && this._target.contains(event.target as Element)) { return; } + this._handler.mouseDownOutsideEvent(); }; diff --git a/src/gui/pane-widget.ts b/src/gui/pane-widget.ts index 394e4f9c80..fc1fc396d7 100644 --- a/src/gui/pane-widget.ts +++ b/src/gui/pane-widget.ts @@ -11,6 +11,7 @@ import { Coordinate } from '../model/coordinate'; import { IDataSource } from '../model/idata-source'; import { InvalidationLevel } from '../model/invalidate-mask'; import { IPriceDataSource } from '../model/iprice-data-source'; +import { KineticAnimation } from '../model/kinetic-animation'; import { Pane, PaneInfo } from '../model/pane'; import { Point } from '../model/point'; import { TimePointIndex } from '../model/time-data'; @@ -19,11 +20,10 @@ import { IPaneView } from '../views/pane/ipane-view'; import { createBoundCanvas, getContext2D, Size } from './canvas-utils'; import { ChartWidget } from './chart-widget'; -import { KineticAnimation } from './kinetic-animation'; -import { MouseEventHandler, MouseEventHandlerMouseEvent, MouseEventHandlers, MouseEventHandlerTouchEvent, Position, TouchMouseEvent } from './mouse-event-handler'; +import { MouseEventHandler, MouseEventHandlerEventBase, MouseEventHandlerMouseEvent, MouseEventHandlers, MouseEventHandlerTouchEvent, Position, TouchMouseEvent } from './mouse-event-handler'; import { PriceAxisWidget, PriceAxisWidgetSide } from './price-axis-widget'; -const enum Constants { +const enum KineticScrollConstants { MinScrollSpeed = 0.2, MaxScrollSpeed = 7, DumpingCoeff = 0.997, @@ -94,7 +94,9 @@ export class PaneWidget implements IDestroyable, MouseEventHandlers { private _startTrackPoint: Point | null = null; private _exitTrackingModeOnNextTry: boolean = false; private _initCrosshairPosition: Point | null = null; + private _scrollXAnimation: KineticAnimation | null = null; + private _isSettingSize: boolean = false; public constructor(chart: ChartWidget, state: Pane) { @@ -277,14 +279,7 @@ export class PaneWidget implements IDestroyable, MouseEventHandlers { } this._onMouseEvent(); - const x = event.localX; - const y = event.localY; - - if (this._clicked.hasListeners()) { - const currentTime = this._model().crosshairSource().appliedIndex(); - const paneIndex = this._model().getPaneIndex(ensureNotNull(this._state)); - this._clicked.fire(currentTime, { x, y, paneIndex }); - } + this._fireClickedDelegate(event); } public pressedMouseMoveEvent(event: MouseEventHandlerMouseEvent): void { @@ -304,6 +299,13 @@ export class PaneWidget implements IDestroyable, MouseEventHandlers { this._endScroll(event); } + public tapEvent(event: MouseEventHandlerTouchEvent): void { + if (this._state === null) { + return; + } + this._fireClickedDelegate(event); + } + public longTapEvent(event: MouseEventHandlerTouchEvent): void { this._longTap = true; @@ -329,7 +331,7 @@ export class PaneWidget implements IDestroyable, MouseEventHandlers { public pinchStartEvent(): void { this._prevPinchScale = 1; - this._terminateKineticAnimation(); + this._model().stopTimeScaleAnimation(); } public pinchEvent(middlePoint: Position, scale: number): void { @@ -506,6 +508,29 @@ export class PaneWidget implements IDestroyable, MouseEventHandlers { return this._rightPriceAxisWidget; } + public setCrossHair(x: number, y: number, visible: boolean): void { + if (!this._state) { + return; + } + if (visible) { + const xCoord = x as Coordinate; + const yCoord = y as Coordinate; + + // if (!mobileTouch) { + this._setCrosshairPositionNoFire(xCoord, yCoord); + // } + } else { + this._state.model().setHoveredSource(null); + // if (!isMobile) { + this._clearCrosshairPosition(); + // } + } + } + + private _setCrosshairPositionNoFire(x: Coordinate, y: Coordinate): void { + this._model().setAndSaveCurrentPosition(this._correctXCoord(x), this._correctYCoord(y), ensureNotNull(this._state), false); + } + private _onStateDestroyed(): void { if (this._state !== null) { this._state.onDestroyed().unsubscribeAll(this); @@ -514,6 +539,16 @@ export class PaneWidget implements IDestroyable, MouseEventHandlers { this._state = null; } + private _fireClickedDelegate(event: MouseEventHandlerEventBase): void { + const x = event.localX; + const y = event.localY; + + if (this._clicked.hasListeners()) { + const paneIndex = this._model().getPaneIndex(ensureNotNull(this._state)); + this._clicked.fire(this._model().timeScale().coordinateToIndex(x), { x, y, paneIndex }); + } + } + private _drawBackground(ctx: CanvasRenderingContext2D, pixelRatio: number): void { drawScaled(ctx, pixelRatio, () => { const model = this._model(); @@ -627,11 +662,11 @@ export class PaneWidget implements IDestroyable, MouseEventHandlers { } const rendererOptionsProvider = chart.model().rendererOptionsProvider(); if (leftAxisVisible && this._leftPriceAxisWidget === null) { - this._leftPriceAxisWidget = new PriceAxisWidget(this, chart.options().layout, rendererOptionsProvider, 'left'); + this._leftPriceAxisWidget = new PriceAxisWidget(this, chart.options(), rendererOptionsProvider, 'left'); this._leftAxisCell.appendChild(this._leftPriceAxisWidget.getElement()); } if (rightAxisVisible && this._rightPriceAxisWidget === null) { - this._rightPriceAxisWidget = new PriceAxisWidget(this, chart.options().layout, rendererOptionsProvider, 'right'); + this._rightPriceAxisWidget = new PriceAxisWidget(this, chart.options(), rendererOptionsProvider, 'right'); this._rightAxisCell.appendChild(this._rightPriceAxisWidget.getElement()); } } @@ -675,66 +710,30 @@ export class PaneWidget implements IDestroyable, MouseEventHandlers { return this._chart.model(); } - private _finishScroll(): void { - const model = this._model(); - const state = this.state(); - const priceScale = state.defaultPriceScale(); - - model.endScrollPrice(state, priceScale); - model.endScrollTime(); - this._startScrollingPos = null; - this._isScrolling = false; - } - private _endScroll(event: TouchMouseEvent): void { if (!this._isScrolling) { return; } - const startAnimationTime = performance.now(); - - if (this._scrollXAnimation !== null) { - this._scrollXAnimation.start(event.localX, startAnimationTime); - } - - if ((this._scrollXAnimation === null || this._scrollXAnimation.finished(startAnimationTime))) { - // animation is not needed - this._finishScroll(); - return; - } - const model = this._model(); - const timeScale = model.timeScale(); + const state = this.state(); - const scrollXAnimation = this._scrollXAnimation; + model.endScrollPrice(state, state.defaultPriceScale()); - const animationFn = () => { - if ((scrollXAnimation.terminated())) { - // animation terminated, see _terminateKineticAnimation - return; - } + this._startScrollingPos = null; + this._isScrolling = false; + model.endScrollTime(); - const now = performance.now(); + if (this._scrollXAnimation !== null) { + const startAnimationTime = performance.now(); + const timeScale = model.timeScale(); - let xAnimationFinished = scrollXAnimation.finished(now); - if (!scrollXAnimation.terminated()) { - const prevRightOffset = timeScale.rightOffset(); - model.scrollTimeTo(scrollXAnimation.getPosition(now)); - if (prevRightOffset === timeScale.rightOffset()) { - xAnimationFinished = true; - this._scrollXAnimation = null; - } - } + this._scrollXAnimation.start(timeScale.rightOffset() as Coordinate, startAnimationTime); - if (xAnimationFinished) { - this._finishScroll(); - return; + if (!this._scrollXAnimation.finished(startAnimationTime)) { + model.setTimeScaleAnimation(this._scrollXAnimation); } - - requestAnimationFrame(animationFn); - }; - - requestAnimationFrame(animationFn); + } } private _onMouseEvent(): void { @@ -746,7 +745,7 @@ export class PaneWidget implements IDestroyable, MouseEventHandlers { return; } - this._terminateKineticAnimation(); + this._model().stopTimeScaleAnimation(); if (document.activeElement !== document.body && document.activeElement !== document.documentElement) { // If any focusable element except the page itself is focused, remove the focus @@ -773,8 +772,9 @@ export class PaneWidget implements IDestroyable, MouseEventHandlers { } const model = this._model(); + const timeScale = model.timeScale(); - if (model.timeScale().isEmpty()) { + if (timeScale.isEmpty()) { return; } @@ -802,29 +802,22 @@ export class PaneWidget implements IDestroyable, MouseEventHandlers { }; } - if (this._scrollXAnimation !== null) { - this._scrollXAnimation.addPosition(event.localX, now); - } - if ( this._startScrollingPos !== null && !this._isScrolling && (this._startScrollingPos.x !== event.clientX || this._startScrollingPos.y !== event.clientY) ) { - if ( - this._scrollXAnimation === null && ( - event.isTouch && kineticScrollOptions.touch || - !event.isTouch && kineticScrollOptions.mouse - ) - ) { + if (event.isTouch && kineticScrollOptions.touch || !event.isTouch && kineticScrollOptions.mouse) { + const barSpacing = timeScale.barSpacing(); this._scrollXAnimation = new KineticAnimation( - Constants.MinScrollSpeed, - Constants.MaxScrollSpeed, - Constants.DumpingCoeff, - Constants.ScrollMinMove + KineticScrollConstants.MinScrollSpeed / barSpacing, + KineticScrollConstants.MaxScrollSpeed / barSpacing, + KineticScrollConstants.DumpingCoeff, + KineticScrollConstants.ScrollMinMove / barSpacing ); - this._scrollXAnimation.addPosition(this._startScrollingPos.localX, this._startScrollingPos.timestamp); - this._scrollXAnimation.addPosition(event.localX, now); + this._scrollXAnimation.addPosition(timeScale.rightOffset() as Coordinate, this._startScrollingPos.timestamp); + } else { + this._scrollXAnimation = null; } if (!priceScale.isEmpty()) { @@ -842,22 +835,10 @@ export class PaneWidget implements IDestroyable, MouseEventHandlers { } model.scrollTimeTo(event.localX); - } - } - - private _terminateKineticAnimation(): void { - const now = performance.now(); - const xAnimationFinished = this._scrollXAnimation === null || this._scrollXAnimation.finished(now); - if (this._scrollXAnimation !== null) { - if (!xAnimationFinished) { - this._finishScroll(); + if (this._scrollXAnimation !== null) { + this._scrollXAnimation.addPosition(timeScale.rightOffset() as Coordinate, now); } } - - if (this._scrollXAnimation !== null) { - this._scrollXAnimation.terminate(); - this._scrollXAnimation = null; - } } private readonly _canvasConfiguredHandler = () => { diff --git a/src/gui/price-axis-widget.ts b/src/gui/price-axis-widget.ts index 0af72f4388..e91653a746 100644 --- a/src/gui/price-axis-widget.ts +++ b/src/gui/price-axis-widget.ts @@ -5,20 +5,20 @@ import { clearRect, clearRectWithGradient, drawScaled } from '../helpers/canvas- import { IDestroyable } from '../helpers/idestroyable'; import { makeFont } from '../helpers/make-font'; +import { ChartOptionsInternal } from '../model/chart-model'; import { Coordinate } from '../model/coordinate'; import { IDataSource } from '../model/idata-source'; import { InvalidationLevel } from '../model/invalidate-mask'; import { IPriceDataSource } from '../model/iprice-data-source'; import { LayoutOptions } from '../model/layout-options'; import { PriceScalePosition } from '../model/pane'; -import { PriceScale } from '../model/price-scale'; +import { PriceMark, PriceScale } from '../model/price-scale'; import { TextWidthCache } from '../model/text-width-cache'; import { PriceAxisViewRendererOptions } from '../renderers/iprice-axis-view-renderer'; import { PriceAxisRendererOptionsProvider } from '../renderers/price-axis-renderer-options-provider'; import { IPriceAxisView } from '../views/price-axis/iprice-axis-view'; import { createBoundCanvas, getContext2D, Size } from './canvas-utils'; -import { LabelsImageCache } from './labels-image-cache'; import { MouseEventHandler, MouseEventHandlers, TouchMouseEvent } from './mouse-event-handler'; import { PaneWidget } from './pane-widget'; @@ -35,9 +35,14 @@ const enum Constants { type IPriceAxisViewArray = readonly IPriceAxisView[]; +const enum Constants { + LabelOffset = 5, +} + export class PriceAxisWidget implements IDestroyable { private readonly _pane: PaneWidget; - private readonly _options: LayoutOptions; + private readonly _options: Readonly; + private readonly _layoutOptions: Readonly; private readonly _rendererOptionsProvider: PriceAxisRendererOptionsProvider; private readonly _isLeft: boolean; @@ -52,17 +57,16 @@ export class PriceAxisWidget implements IDestroyable { private _mouseEventHandler: MouseEventHandler; private _mousedown: boolean = false; - private readonly _widthCache: TextWidthCache = new TextWidthCache(50); - private _tickMarksCache: LabelsImageCache = new LabelsImageCache(11, '#000'); + private readonly _widthCache: TextWidthCache = new TextWidthCache(200); - private _color: string | null = null; private _font: string | null = null; private _prevOptimalWidth: number = 0; private _isSettingSize: boolean = false; - public constructor(pane: PaneWidget, options: LayoutOptions, rendererOptionsProvider: PriceAxisRendererOptionsProvider, side: PriceAxisWidgetSide) { + public constructor(pane: PaneWidget, options: Readonly, rendererOptionsProvider: PriceAxisRendererOptionsProvider, side: PriceAxisWidgetSide) { this._pane = pane; this._options = options; + this._layoutOptions = options.layout; this._rendererOptionsProvider = rendererOptionsProvider; this._isLeft = side === 'left'; @@ -126,41 +130,20 @@ export class PriceAxisWidget implements IDestroyable { } this._priceScale = null; - - this._tickMarksCache.destroy(); } public getElement(): HTMLElement { return this._cell; } - public lineColor(): string { - return ensureNotNull(this._priceScale).options().borderColor; - } - - public textColor(): string { - return this._options.textColor; - } - public fontSize(): number { - return this._options.fontSize; - } - - public baseFont(): string { - return makeFont(this.fontSize(), this._options.fontFamily); + return this._layoutOptions.fontSize; } public rendererOptions(): Readonly { const options = this._rendererOptionsProvider.options(); - - const isColorChanged = this._color !== options.color; const isFontChanged = this._font !== options.font; - if (isColorChanged || isFontChanged) { - this._recreateTickMarksCache(options); - this._color = options.color; - } - if (isFontChanged) { this._widthCache.reset(); this._font = options.font; @@ -180,7 +163,7 @@ export class PriceAxisWidget implements IDestroyable { const ctx = getContext2D(this._canvasBinding.canvas); const tickMarks = this._priceScale.marks(); - ctx.font = this.baseFont(); + ctx.font = this._baseFont(); if (tickMarks.length > 0) { tickMarkMaxWidth = Math.max( @@ -216,9 +199,11 @@ export class PriceAxisWidget implements IDestroyable { rendererOptions.tickLength + rendererOptions.paddingInner + rendererOptions.paddingOuter + + Constants.LabelOffset + resultTickMarksMaxWidth ); - // make it even + + // make it even, remove this after migration to perfect fancy canvas res += res % 2; return res; } @@ -303,7 +288,7 @@ export class PriceAxisWidget implements IDestroyable { } private _mouseDownEvent(e: TouchMouseEvent): void { - if (this._priceScale === null || this._priceScale.isEmpty() || !this._pane.chart().options().handleScale.axisPressedMouseMove.price) { + if (this._priceScale === null || this._priceScale.isEmpty() || !this._options.handleScale.axisPressedMouseMove.price) { return; } @@ -314,7 +299,7 @@ export class PriceAxisWidget implements IDestroyable { } private _pressedMouseMoveEvent(e: TouchMouseEvent): void { - if (this._priceScale === null || !this._pane.chart().options().handleScale.axisPressedMouseMove.price) { + if (this._priceScale === null || !this._options.handleScale.axisPressedMouseMove.price) { return; } @@ -325,7 +310,7 @@ export class PriceAxisWidget implements IDestroyable { } private _mouseDownOutsideEvent(): void { - if (this._priceScale === null || !this._pane.chart().options().handleScale.axisPressedMouseMove.price) { + if (this._priceScale === null || !this._options.handleScale.axisPressedMouseMove.price) { return; } @@ -340,7 +325,7 @@ export class PriceAxisWidget implements IDestroyable { } private _mouseUpEvent(e: TouchMouseEvent): void { - if (this._priceScale === null || !this._pane.chart().options().handleScale.axisPressedMouseMove.price) { + if (this._priceScale === null || !this._options.handleScale.axisPressedMouseMove.price) { return; } const model = this._pane.chart().model(); @@ -350,7 +335,7 @@ export class PriceAxisWidget implements IDestroyable { } private _mouseDoubleClickEvent(e: TouchMouseEvent): void { - if (this._pane.chart().options().handleScale.axisDoubleClickReset) { + if (this._options.handleScale.axisDoubleClickReset.price) { this.reset(); } } @@ -417,7 +402,7 @@ export class PriceAxisWidget implements IDestroyable { } ctx.save(); - ctx.fillStyle = this.lineColor(); + ctx.fillStyle = this._priceScale.options().borderColor; const borderSize = Math.max(1, Math.floor(this.rendererOptions().borderSize * pixelRatio)); @@ -441,26 +426,26 @@ export class PriceAxisWidget implements IDestroyable { ctx.save(); - ctx.strokeStyle = this.lineColor(); + const priceScaleOptions = this._priceScale.options(); + + ctx.strokeStyle = priceScaleOptions.borderColor; - ctx.font = this.baseFont(); - ctx.fillStyle = this.lineColor(); + ctx.font = this._baseFont(); + ctx.fillStyle = priceScaleOptions.borderColor; const rendererOptions = this.rendererOptions(); const tickMarkLeftX = this._isLeft ? - Math.floor((this._size.w - rendererOptions.tickLength) * pixelRatio - rendererOptions.borderSize * pixelRatio) : - Math.floor(rendererOptions.borderSize * pixelRatio); + Math.floor((this._size.w - rendererOptions.tickLength) * pixelRatio) : + 0; const textLeftX = this._isLeft ? Math.round(tickMarkLeftX - rendererOptions.paddingInner * pixelRatio) : Math.round(tickMarkLeftX + rendererOptions.tickLength * pixelRatio + rendererOptions.paddingInner * pixelRatio); - const textAlign = this._isLeft ? 'right' : 'left'; const tickHeight = Math.max(1, Math.floor(pixelRatio)); const tickOffset = Math.floor(pixelRatio * 0.5); - const options = this._priceScale.options(); - if (options.borderVisible && options.ticksVisible) { + if (priceScaleOptions.borderVisible && priceScaleOptions.ticksVisible) { const tickLength = Math.round(rendererOptions.tickLength * pixelRatio); ctx.beginPath(); for (const tickMark of tickMarks) { @@ -470,10 +455,18 @@ export class PriceAxisWidget implements IDestroyable { ctx.fill(); } - ctx.fillStyle = this.textColor(); - for (const tickMark of tickMarks) { - this._tickMarksCache.paintTo(ctx, tickMark.label, textLeftX, Math.round(tickMark.coord * pixelRatio), textAlign); - } + ctx.fillStyle = priceScaleOptions.textColor ?? this._layoutOptions.textColor; + ctx.textAlign = this._isLeft ? 'right' : 'left'; + ctx.textBaseline = 'middle'; + + const yMidCorrections = tickMarks.map((mark: PriceMark) => this._widthCache.yMidCorrection(ctx, mark.label)); + + drawScaled(ctx, pixelRatio, () => { + for (let i = tickMarks.length; i--;) { + const tickMark = tickMarks[i]; + ctx.fillText(tickMark.label, textLeftX / pixelRatio, tickMark.coord + yMidCorrections[i]); + } + }); ctx.restore(); } @@ -524,6 +517,21 @@ export class PriceAxisWidget implements IDestroyable { // crosshair individually updateForSources(orderedSources); + views.forEach((view: IPriceAxisView) => view.setFixedCoordinate(view.coordinate())); + + const options = this._priceScale.options(); + if (!options.alignLabels) { + return; + } + + this._fixLabelOverlap(views, rendererOptions, center); + } + + private _fixLabelOverlap(views: IPriceAxisView[], rendererOptions: Readonly, center: number): void { + if (this._size === null) { + return; + } + // split into two parts const top = views.filter((view: IPriceAxisView) => view.coordinate() <= center); const bottom = views.filter((view: IPriceAxisView) => view.coordinate() > center); @@ -538,11 +546,16 @@ export class PriceAxisWidget implements IDestroyable { bottom.sort((l: IPriceAxisView, r: IPriceAxisView) => l.coordinate() - r.coordinate()); - views.forEach((view: IPriceAxisView) => view.setFixedCoordinate(view.coordinate())); + for (const view of views) { + const halfHeight = Math.floor(view.height(rendererOptions) / 2); + const coordinate = view.coordinate(); + if (coordinate > -halfHeight && coordinate < halfHeight) { + view.setFixedCoordinate(halfHeight); + } - const options = this._priceScale.options(); - if (!options.alignLabels) { - return; + if (coordinate > (this._size.h - halfHeight) && coordinate < this._size.h + halfHeight) { + view.setFixedCoordinate(this._size.h - halfHeight); + } } for (let i = 1; i < top.length; i++) { @@ -643,18 +656,7 @@ export class PriceAxisWidget implements IDestroyable { this._prevOptimalWidth = width; } - private _recreateTickMarksCache(options: PriceAxisViewRendererOptions): void { - this._tickMarksCache.destroy(); - - this._tickMarksCache = new LabelsImageCache( - options.fontSize, - options.color, - options.fontFamily - ); - } - private readonly _canvasConfiguredHandler = () => { - this._recreateTickMarksCache(this._rendererOptionsProvider.options()); if (!this._isSettingSize) { this._pane.chart().model().lightUpdate(); } @@ -667,4 +669,8 @@ export class PriceAxisWidget implements IDestroyable { this._pane.chart().model().lightUpdate(); }; + + private _baseFont(): string { + return makeFont(this._layoutOptions.fontSize, this._layoutOptions.fontFamily); + } } diff --git a/src/gui/time-axis-widget.ts b/src/gui/time-axis-widget.ts index 4d06dcb6ed..70c9140650 100644 --- a/src/gui/time-axis-widget.ts +++ b/src/gui/time-axis-widget.ts @@ -21,7 +21,7 @@ import { PriceAxisStub, PriceAxisStubParams } from './price-axis-stub'; const enum Constants { BorderSize = 1, - TickLength = 3, + TickLength = 5, } const enum CursorType { @@ -193,7 +193,7 @@ export class TimeAxisWidget implements MouseEventHandlers, IDestroyable { } public mouseDoubleClickEvent(): void { - if (this._chart.options().handleScale.axisDoubleClickReset) { + if (this._chart.options().handleScale.axisDoubleClickReset.time) { this._chart.model().resetTimeScale(); } } @@ -251,7 +251,8 @@ export class TimeAxisWidget implements MouseEventHandlers, IDestroyable { rendererOptions.tickLength + rendererOptions.fontSize + rendererOptions.paddingTop + - rendererOptions.paddingBottom + rendererOptions.paddingBottom + + rendererOptions.labelBottomOffset ); } @@ -338,14 +339,13 @@ export class TimeAxisWidget implements MouseEventHandlers, IDestroyable { rendererOptions.borderSize + rendererOptions.tickLength + rendererOptions.paddingTop + - rendererOptions.fontSize - - rendererOptions.baselineOffset + rendererOptions.fontSize / 2 ); ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; ctx.fillStyle = this._lineColor(); - const borderSize = Math.floor(this._getRendererOptions().borderSize * pixelRatio); const tickWidth = Math.max(1, Math.floor(pixelRatio)); const tickOffset = Math.floor(pixelRatio * 0.5); @@ -355,7 +355,7 @@ export class TimeAxisWidget implements MouseEventHandlers, IDestroyable { const tickLen = Math.round(rendererOptions.tickLength * pixelRatio); for (let index = tickMarks.length; index--;) { const x = Math.round(tickMarks[index].coord * pixelRatio); - ctx.rect(x - tickOffset, borderSize, tickWidth, tickLen); + ctx.rect(x - tickOffset, 0, tickWidth, tickLen); } ctx.fill(); @@ -441,6 +441,7 @@ export class TimeAxisWidget implements MouseEventHandlers, IDestroyable { fontSize: NaN, font: '', widthCache: new TextWidthCache(), + labelBottomOffset: 0, }; } @@ -451,10 +452,11 @@ export class TimeAxisWidget implements MouseEventHandlers, IDestroyable { const fontSize = this._fontSize(); rendererOptions.fontSize = fontSize; rendererOptions.font = newFont; - rendererOptions.paddingTop = Math.ceil(fontSize / 2.5); - rendererOptions.paddingBottom = rendererOptions.paddingTop; - rendererOptions.paddingHorizontal = Math.ceil(fontSize / 2); - rendererOptions.baselineOffset = Math.round(this._fontSize() / 5); + rendererOptions.paddingTop = 3 * fontSize / 12; + rendererOptions.paddingBottom = 3 * fontSize / 12; + rendererOptions.paddingHorizontal = 9 * fontSize / 12; + rendererOptions.baselineOffset = 0; + rendererOptions.labelBottomOffset = 4 * fontSize / 12; rendererOptions.widthCache.reset(); } diff --git a/src/helpers/browsers.ts b/src/helpers/browsers.ts index 6c35a2091a..6c0ab390a0 100644 --- a/src/helpers/browsers.ts +++ b/src/helpers/browsers.ts @@ -22,3 +22,23 @@ export function isChrome(): boolean { return window.chrome !== undefined; } +// Determine whether the browser is running on windows. +export function isWindows(): boolean { + // more accurate if available + if ( + navigator?.userAgentData?.platform + ) { + return navigator.userAgentData.platform === 'Windows'; + } + return navigator.userAgent.toLowerCase().indexOf('win') >= 0; +} + +// Determine whether the browser is Chromium based. +export function isChromiumBased(): boolean { + if (!navigator.userAgentData) { return false; } + return navigator.userAgentData.brands.some( + (brand: UADataBrand) => { + return brand.brand.includes('Chromium'); + } + ); +} diff --git a/src/helpers/canvas-helpers.ts b/src/helpers/canvas-helpers.ts index 3a44ae19fb..260ae45b74 100644 --- a/src/helpers/canvas-helpers.ts +++ b/src/helpers/canvas-helpers.ts @@ -51,6 +51,121 @@ export function clearRect(ctx: CanvasRenderingContext2D, x: number, y: number, w ctx.restore(); } +export type TopBottomRadii = [number, number]; +export type LeftTopRightTopRightBottomLeftBottomRadii = [number, number, number, number]; +export type DrawRoundRectRadii = number | TopBottomRadii | LeftTopRightTopRightBottomLeftBottomRadii; + +function changeBorderRadius(borderRadius: DrawRoundRectRadii, offset: number): typeof borderRadius { + if (Array.isArray(borderRadius)) { + return borderRadius.map((x: number) => x === 0 ? x : x + offset) as typeof borderRadius; + } + return borderRadius + offset; +} + +export function drawRoundRect( + // eslint:disable-next-line:max-params + ctx: CanvasRenderingContext2D, + x: number, + y: number, + w: number, + h: number, + radii: DrawRoundRectRadii +): void { + let radiusLeftTop: number; + let radiusRightTop: number; + let radiusRightBottom: number; + let radiusLeftBottom: number; + + if (!Array.isArray(radii)) { + const oneRadius = Math.max(0, radii); + radiusLeftTop = oneRadius; + radiusRightTop = oneRadius; + radiusRightBottom = oneRadius; + radiusLeftBottom = oneRadius; + } else if (radii.length === 2) { + const cornerRadius1 = Math.max(0, radii[0]); + const cornerRadius2 = Math.max(0, radii[1]); + radiusLeftTop = cornerRadius1; + radiusRightTop = cornerRadius1; + radiusRightBottom = cornerRadius2; + radiusLeftBottom = cornerRadius2; + } else if (radii.length === 4) { + radiusLeftTop = Math.max(0, radii[0]); + radiusRightTop = Math.max(0, radii[1]); + radiusRightBottom = Math.max(0, radii[2]); + radiusLeftBottom = Math.max(0, radii[3]); + } else { + throw new Error(`Wrong border radius - it should be like css border radius`); + } + + ctx.beginPath(); + ctx.moveTo(x + radiusLeftTop, y); + ctx.lineTo(x + w - radiusRightTop, y); + if (radiusRightTop !== 0) { + ctx.arcTo(x + w, y, x + w, y + radiusRightTop, radiusRightTop); + } + + ctx.lineTo(x + w, y + h - radiusRightBottom); + if (radiusRightBottom !== 0) { + ctx.arcTo(x + w, y + h, x + w - radiusRightBottom, y + h, radiusRightBottom); + } + + ctx.lineTo(x + radiusLeftBottom, y + h); + if (radiusLeftBottom !== 0) { + ctx.arcTo(x, y + h, x, y + h - radiusLeftBottom, radiusLeftBottom); + } + + ctx.lineTo(x, y + radiusLeftTop); + if (radiusLeftTop !== 0) { + ctx.arcTo(x, y, x + radiusLeftTop, y, radiusLeftTop); + } +} + +// eslint-disable-next-line max-params +export function drawRoundRectWithInnerBorder( + ctx: CanvasRenderingContext2D, + left: number, + top: number, + width: number, + height: number, + backgroundColor: string, + borderWidth: number = 0, + borderRadius: DrawRoundRectRadii = 0, + borderColor: string = '' +): void { + ctx.save(); + + if (!borderWidth || !borderColor || borderColor === backgroundColor) { + drawRoundRect(ctx, left, top, width, height, borderRadius); + ctx.fillStyle = backgroundColor; + ctx.fill(); + ctx.restore(); + return; + } + + const halfBorderWidth = borderWidth / 2; + + // Draw body + if (backgroundColor !== 'transparent') { + const innerRadii = changeBorderRadius(borderRadius, - borderWidth); + drawRoundRect(ctx, left + borderWidth, top + borderWidth, width - borderWidth * 2, height - borderWidth * 2, innerRadii); + + ctx.fillStyle = backgroundColor; + ctx.fill(); + } + + // Draw border + if (borderColor !== 'transparent') { + const outerRadii = changeBorderRadius(borderRadius, - halfBorderWidth); + drawRoundRect(ctx, left + halfBorderWidth, top + halfBorderWidth, width - borderWidth, height - borderWidth, outerRadii); + + ctx.lineWidth = borderWidth; + ctx.strokeStyle = borderColor; + ctx.closePath(); + ctx.stroke(); + } +} + // eslint-disable-next-line max-params export function clearRectWithGradient(ctx: CanvasRenderingContext2D, x: number, y: number, w: number, h: number, topColor: string, bottomColor: string): void { ctx.save(); diff --git a/src/helpers/strict-type-checks.ts b/src/helpers/strict-type-checks.ts index 56ac22daf9..620657353e 100644 --- a/src/helpers/strict-type-checks.ts +++ b/src/helpers/strict-type-checks.ts @@ -21,6 +21,7 @@ export function merge(dst: Record, ...sources: Record[ if ('object' !== typeof src[i] || dst[i] === undefined) { dst[i] = src[i]; } else { + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument merge(dst[i], src[i]); } } diff --git a/src/model/chart-model.ts b/src/model/chart-model.ts index 8fa5ff5ef6..902803480b 100644 --- a/src/model/chart-model.ts +++ b/src/model/chart-model.ts @@ -14,7 +14,7 @@ import { Coordinate } from './coordinate'; import { Crosshair, CrosshairOptions } from './crosshair'; import { DefaultPriceScaleId, isDefaultPriceScale } from './default-price-scale'; import { GridOptions } from './grid'; -import { InvalidateMask, InvalidationLevel } from './invalidate-mask'; +import { InvalidateMask, InvalidationLevel, ITimeScaleAnimation } from './invalidate-mask'; import { IPriceDataSource } from './iprice-data-source'; import { ColorType, LayoutOptions } from './layout-options'; import { LocalizationOptions } from './localization-options'; @@ -90,10 +90,8 @@ export interface HandleScaleOptions { /** * Enable resetting scaling by double-clicking the left mouse button. - * - * @defaultValue `true` */ - axisDoubleClickReset: boolean; + axisDoubleClickReset: AxisDoubleClickOptions | boolean; } /** @@ -116,10 +114,13 @@ export interface KineticScrollOptions { } type HandleScaleOptionsInternal = - Omit + Omit & { /** @public */ axisPressedMouseMove: AxisPressedMouseMoveOptions; + + /** @public */ + axisDoubleClickReset: AxisDoubleClickOptions; }; /** @@ -141,6 +142,25 @@ export interface AxisPressedMouseMoveOptions { price: boolean; } +/** + * Represents options for how the time and price axes react to mouse double click. + */ +export interface AxisDoubleClickOptions { + /** + * Enable resetting scaling the time axis by double-clicking the left mouse button. + * + * @defaultValue `true` + */ + time: boolean; + + /** + * Enable reseting scaling the price axis by by double-clicking the left mouse button. + * + * @defaultValue `true` + */ + price: boolean; +} + export interface HoveredObject { hitTestData?: unknown; externalId?: string; @@ -201,7 +221,7 @@ export const enum TrackingModeExitMode { */ export interface TrackingModeOptions { // eslint-disable-next-line tsdoc/syntax - /** @inheritdoc TrackingModeExitMode + /** @inheritDoc TrackingModeExitMode * * @defaultValue {@link TrackingModeExitMode.OnNextTap} */ @@ -331,7 +351,6 @@ export class ChartModel implements IDestroyable { private _serieses: Series[] = []; private _width: number = 0; - private _initialTimeScrollPos: number | null = null; private _hoveredSource: HoveredSource | null = null; private readonly _priceScalesOptionsChanged: Delegate = new Delegate(); private _crosshairMoved: Delegate = new Delegate(); @@ -361,11 +380,11 @@ export class ChartModel implements IDestroyable { } public fullUpdate(): void { - this._invalidate(new InvalidateMask(InvalidationLevel.Full)); + this._invalidate(InvalidateMask.full()); } public lightUpdate(): void { - this._invalidate(new InvalidateMask(InvalidationLevel.Light)); + this._invalidate(InvalidateMask.light()); } public cursorUpdate(): void { @@ -518,7 +537,7 @@ export class ChartModel implements IDestroyable { // if autoscale option is true, it is ok, just recalculate by invalidation mask // if autoscale option is false, autoscale anyway on the first draw // also there is a scenario when autoscale is true in constructor and false later on applyOptions - const mask = new InvalidateMask(InvalidationLevel.Full); + const mask = InvalidateMask.full(); mask.invalidatePane(actualIndex, { level: InvalidationLevel.None, autoScale: true, @@ -669,34 +688,24 @@ export class ChartModel implements IDestroyable { } public startScrollTime(x: Coordinate): void { - this._initialTimeScrollPos = x; this._timeScale.startScroll(x); } - public scrollTimeTo(x: Coordinate): boolean { - let res = false; - if (this._initialTimeScrollPos !== null && Math.abs(x - this._initialTimeScrollPos) > 20) { - this._initialTimeScrollPos = null; - res = true; - } - + public scrollTimeTo(x: Coordinate): void { this._timeScale.scrollTo(x); this.recalculateAllPanes(); - return res; } public endScrollTime(): void { this._timeScale.endScroll(); this.lightUpdate(); - - this._initialTimeScrollPos = null; } public serieses(): readonly Series[] { return this._serieses; } - public setAndSaveCurrentPosition(x: Coordinate, y: Coordinate, pane: Pane): void { + public setAndSaveCurrentPosition(x: Coordinate, y: Coordinate, pane: Pane, fire: boolean = true): void { this._crosshair.saveOriginCoord(x, y); let price = NaN; let index = this._timeScale.coordinateToIndex(x); @@ -717,7 +726,10 @@ export class ChartModel implements IDestroyable { this.cursorUpdate(); const paneIndex = this.getPaneIndex(pane); - this._crosshairMoved.fire(this._crosshair.appliedIndex(), { x, y, paneIndex }); + + if (fire) { + this._crosshairMoved.fire(this._crosshair.appliedIndex(), { x, y, paneIndex }); + } } public clearCurrentPosition(): void { @@ -859,35 +871,47 @@ export class ChartModel implements IDestroyable { } public fitContent(): void { - const mask = new InvalidateMask(InvalidationLevel.Light); + const mask = InvalidateMask.light(); mask.setFitContent(); this._invalidate(mask); } public setTargetLogicalRange(range: LogicalRange): void { - const mask = new InvalidateMask(InvalidationLevel.Light); + const mask = InvalidateMask.light(); mask.applyRange(range); this._invalidate(mask); } public resetTimeScale(): void { - const mask = new InvalidateMask(InvalidationLevel.Light); + const mask = InvalidateMask.light(); mask.resetTimeScale(); this._invalidate(mask); } public setBarSpacing(spacing: number): void { - const mask = new InvalidateMask(InvalidationLevel.Light); + const mask = InvalidateMask.light(); mask.setBarSpacing(spacing); this._invalidate(mask); } public setRightOffset(offset: number): void { - const mask = new InvalidateMask(InvalidationLevel.Light); + const mask = InvalidateMask.light(); mask.setRightOffset(offset); this._invalidate(mask); } + public setTimeScaleAnimation(animation: ITimeScaleAnimation): void { + const mask = InvalidateMask.light(); + mask.setTimeScaleAnimation(animation); + this._invalidate(mask); + } + + public stopTimeScaleAnimation(): void { + const mask = InvalidateMask.light(); + mask.stopTimeScaleAnimation(); + this._invalidate(mask); + } + public defaultVisiblePriceScaleId(): string { return this._options.rightPriceScale.visible ? DefaultPriceScaleId.Right : DefaultPriceScaleId.Left; } diff --git a/src/model/invalidate-mask.ts b/src/model/invalidate-mask.ts index a8e3f80f3a..88230c208e 100644 --- a/src/model/invalidate-mask.ts +++ b/src/model/invalidate-mask.ts @@ -27,6 +27,8 @@ export const enum TimeScaleInvalidationType { ApplyBarSpacing, ApplyRightOffset, Reset, + Animation, + StopAnimation, } export interface TimeScaleApplyRangeInvalidation { @@ -52,12 +54,27 @@ export interface TimeScaleResetInvalidation { type: TimeScaleInvalidationType.Reset; } +export interface ITimeScaleAnimation { + getPosition(time: number): number; + finished(time: number): boolean; +} +export interface StartTimeScaleAnimationInvalidation { + type: TimeScaleInvalidationType.Animation; + value: ITimeScaleAnimation; +} + +export interface StopTimeScaleAnimationInvalidation { + type: TimeScaleInvalidationType.StopAnimation; +} + export type TimeScaleInvalidation = | TimeScaleApplyRangeInvalidation | TimeScaleFitContentInvalidation | TimeScaleApplyRightOffsetInvalidation | TimeScaleApplyBarSpacingInvalidation - | TimeScaleResetInvalidation; + | TimeScaleResetInvalidation + | StartTimeScaleAnimationInvalidation + | StopTimeScaleAnimationInvalidation; export class InvalidateMask { private _invalidatedPanes: Map = new Map(); @@ -92,25 +109,40 @@ export class InvalidateMask { } public setFitContent(): void { + this.stopTimeScaleAnimation(); // modifies both bar spacing and right offset this._timeScaleInvalidations = [{ type: TimeScaleInvalidationType.FitContent }]; } public applyRange(range: LogicalRange): void { + this.stopTimeScaleAnimation(); // modifies both bar spacing and right offset this._timeScaleInvalidations = [{ type: TimeScaleInvalidationType.ApplyRange, value: range }]; } + public setTimeScaleAnimation(animation: ITimeScaleAnimation): void { + this._removeTimeScaleAnimation(); + this._timeScaleInvalidations.push({ type: TimeScaleInvalidationType.Animation, value: animation }); + } + + public stopTimeScaleAnimation(): void { + this._removeTimeScaleAnimation(); + this._timeScaleInvalidations.push({ type: TimeScaleInvalidationType.StopAnimation }); + } + public resetTimeScale(): void { + this.stopTimeScaleAnimation(); // modifies both bar spacing and right offset this._timeScaleInvalidations = [{ type: TimeScaleInvalidationType.Reset }]; } public setBarSpacing(barSpacing: number): void { + this.stopTimeScaleAnimation(); this._timeScaleInvalidations.push({ type: TimeScaleInvalidationType.ApplyBarSpacing, value: barSpacing }); } public setRightOffset(offset: number): void { + this.stopTimeScaleAnimation(); this._timeScaleInvalidations.push({ type: TimeScaleInvalidationType.ApplyRightOffset, value: offset }); } @@ -129,6 +161,14 @@ export class InvalidateMask { }); } + public static light(): InvalidateMask { + return new InvalidateMask(InvalidationLevel.Light); + } + + public static full(): InvalidateMask { + return new InvalidateMask(InvalidationLevel.Full); + } + private _applyTimeScaleInvalidation(invalidation: TimeScaleInvalidation): void { switch (invalidation.type) { case TimeScaleInvalidationType.FitContent: @@ -146,6 +186,18 @@ export class InvalidateMask { case TimeScaleInvalidationType.Reset: this.resetTimeScale(); break; + case TimeScaleInvalidationType.Animation: + this.setTimeScaleAnimation(invalidation.value); + break; + case TimeScaleInvalidationType.StopAnimation: + this._removeTimeScaleAnimation(); + } + } + + private _removeTimeScaleAnimation(): void { + const index = this._timeScaleInvalidations.findIndex((inv: TimeScaleInvalidation) => inv.type === TimeScaleInvalidationType.Animation); + if (index !== -1) { + this._timeScaleInvalidations.splice(index, 1); } } } diff --git a/src/gui/kinetic-animation.ts b/src/model/kinetic-animation.ts similarity index 96% rename from src/gui/kinetic-animation.ts rename to src/model/kinetic-animation.ts index c32f818e11..e0bd6a0aa5 100644 --- a/src/gui/kinetic-animation.ts +++ b/src/model/kinetic-animation.ts @@ -36,8 +36,6 @@ export class KineticAnimation { private _durationMsecs: number = 0; private _speedPxPerMsec: number = 0; - private _terminated: boolean = false; - private readonly _minMove: number; private readonly _minSpeed: number; private readonly _maxSpeed: number; @@ -136,14 +134,6 @@ export class KineticAnimation { return this._animationStartPosition === null || this._progressDuration(time) === this._durationMsecs; } - public terminated(): boolean { - return this._terminated; - } - - public terminate(): void { - this._terminated = true; - } - private _progressDuration(time: number): number { const startPosition = ensureNotNull(this._animationStartPosition); const progress = time - startPosition.time; diff --git a/src/model/price-line-options.ts b/src/model/price-line-options.ts index c46e0ea72d..a66d06480f 100644 --- a/src/model/price-line-options.ts +++ b/src/model/price-line-options.ts @@ -47,3 +47,10 @@ export interface PriceLineOptions { */ title: string; } + +/** + * Price line options for the {@link ISeriesApi.createPriceLine} method. + * + * `price` is required, while the rest of the options are optional. + */ +export type CreatePriceLineOptions = Partial & Pick; diff --git a/src/model/price-scale.ts b/src/model/price-scale.ts index 7450454677..14a5324b8b 100644 --- a/src/model/price-scale.ts +++ b/src/model/price-scale.ts @@ -147,6 +147,14 @@ export interface PriceScaleOptions { */ borderColor: string; + /** + * Price scale text color. + * If not provided {@link LayoutOptions.textColor} is used. + * + * @defaultValue `undefined` + */ + textColor?: string; + /** * Show top and bottom corner labels only if entire text is visible. * @@ -164,7 +172,7 @@ export interface PriceScaleOptions { /** * Draw small horizontal line on price axis labels. * - * @defaultValue `true` + * @defaultValue `false` */ ticksVisible: boolean; } diff --git a/src/model/series-bar-colorer.ts b/src/model/series-bar-colorer.ts index ac2cca0933..e9310a850e 100644 --- a/src/model/series-bar-colorer.ts +++ b/src/model/series-bar-colorer.ts @@ -10,6 +10,8 @@ import { CandlestickStyleOptions, HistogramStyleOptions, LineStyleOptions, + SeriesOptionsMap, + SeriesType, } from './series-options'; import { TimePointIndex } from './time-data'; @@ -18,79 +20,86 @@ export interface PrecomputedBars { previousValue?: SeriesPlotRow; } -export interface BarColorerStyle { +export interface CommonBarColorerStyle { barColor: string; - barBorderColor: string; // Used in Candlesticks - barWickColor: string; // Used in Candlesticks } -const emptyResult: BarColorerStyle = { - barColor: '', - barBorderColor: '', - barWickColor: '', -}; +export interface LineStrokeColorerStyle { + lineColor: string; +} -export class SeriesBarColorer { - private _series: Series; +export interface LineBarColorerStyle extends CommonBarColorerStyle, LineStrokeColorerStyle { +} - public constructor(series: Series) { - this._series = series; - } +export interface HistogramBarColorerStyle extends CommonBarColorerStyle { +} +export interface AreaFillColorerStyle { + topColor: string; + bottomColor: string; +} +export interface AreaBarColorerStyle extends CommonBarColorerStyle, AreaFillColorerStyle, LineStrokeColorerStyle { +} - public barStyle(barIndex: TimePointIndex, precomputedBars?: PrecomputedBars): BarColorerStyle { - // precomputedBars: {value: [Array BarValues], previousValue: [Array BarValues] | undefined} - // Used to avoid binary search if bars are already known +export interface BaselineStrokeColorerStyle { + topLineColor: string; + bottomLineColor: string; +} - const targetType = this._series.seriesType(); - const seriesOptions = this._series.options(); - switch (targetType) { - case 'Line': - return this._lineStyle(seriesOptions as LineStyleOptions, barIndex, precomputedBars); +export interface BaselineFillColorerStyle { + topFillColor1: string; + topFillColor2: string; + bottomFillColor2: string; + bottomFillColor1: string; +} - case 'Area': - return this._areaStyle(seriesOptions as AreaStyleOptions); +export interface BaselineBarColorerStyle extends CommonBarColorerStyle, BaselineStrokeColorerStyle, BaselineFillColorerStyle { +} - case 'Baseline': - return this._baselineStyle(seriesOptions as BaselineStyleOptions, barIndex, precomputedBars); +export interface BarColorerStyle extends CommonBarColorerStyle { +} - case 'Bar': - return this._barStyle(seriesOptions as BarStyleOptions, barIndex, precomputedBars); +export interface CandlesticksColorerStyle extends CommonBarColorerStyle { + barBorderColor: string; + barWickColor: string; +} - case 'Candlestick': - return this._candleStyle(seriesOptions as CandlestickStyleOptions, barIndex, precomputedBars); +export interface BarStylesMap { + Bar: BarColorerStyle; + Candlestick: CandlesticksColorerStyle; + Area: AreaBarColorerStyle; + Baseline: BaselineBarColorerStyle; + Line: LineBarColorerStyle; + Histogram: HistogramBarColorerStyle; +} - case 'Histogram': - return this._histogramStyle(seriesOptions as HistogramStyleOptions, barIndex, precomputedBars); - } +type FindBarFn = (barIndex: TimePointIndex, precomputedBars?: PrecomputedBars) => SeriesPlotRow | null; - throw new Error('Unknown chart style'); - } +type StyleGetterFn = ( + findBar: FindBarFn, + barStyle: ReturnType['options']>, + barIndex: TimePointIndex, + precomputedBars?: PrecomputedBars +) => BarStylesMap[T]; - private _barStyle(barStyle: BarStyleOptions, barIndex: TimePointIndex, precomputedBars?: PrecomputedBars): BarColorerStyle { - const result: BarColorerStyle = { ...emptyResult }; +type BarStylesFnMap = { + [T in keyof SeriesOptionsMap]: StyleGetterFn; +}; +const barStyleFnMap: BarStylesFnMap = { + // eslint-disable-next-line @typescript-eslint/naming-convention + Bar: (findBar: FindBarFn, barStyle: BarStyleOptions, barIndex: TimePointIndex, precomputedBars?: PrecomputedBars): BarColorerStyle => { const upColor = barStyle.upColor; const downColor = barStyle.downColor; - const borderUpColor = upColor; - const borderDownColor = downColor; - const currentBar = ensureNotNull(this._findBar(barIndex, precomputedBars)) as SeriesPlotRow<'Bar'>; + const currentBar = ensureNotNull(findBar(barIndex, precomputedBars)) as SeriesPlotRow<'Bar'>; const isUp = ensure(currentBar.value[PlotRowValueIndex.Open]) <= ensure(currentBar.value[PlotRowValueIndex.Close]); - 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; - } - - private _candleStyle(candlestickStyle: CandlestickStyleOptions, barIndex: TimePointIndex, precomputedBars?: PrecomputedBars): BarColorerStyle { - const result: BarColorerStyle = { ...emptyResult }; - + return { + barColor: currentBar.color ?? (isUp ? upColor : downColor), + }; + }, + // eslint-disable-next-line @typescript-eslint/naming-convention + Candlestick: (findBar: FindBarFn, candlestickStyle: CandlestickStyleOptions, barIndex: TimePointIndex, precomputedBars?: PrecomputedBars): CandlesticksColorerStyle => { const upColor = candlestickStyle.upColor; const downColor = candlestickStyle.downColor; const borderUpColor = candlestickStyle.borderUpColor; @@ -99,54 +108,78 @@ export class SeriesBarColorer { const wickUpColor = candlestickStyle.wickUpColor; const wickDownColor = candlestickStyle.wickDownColor; - const currentBar = ensureNotNull(this._findBar(barIndex, precomputedBars)) as SeriesPlotRow<'Candlestick'>; + const currentBar = ensureNotNull(findBar(barIndex, precomputedBars)) as SeriesPlotRow<'Candlestick'>; const isUp = ensure(currentBar.value[PlotRowValueIndex.Open]) <= ensure(currentBar.value[PlotRowValueIndex.Close]); - result.barColor = currentBar.color ?? (isUp ? upColor : downColor); - result.barBorderColor = currentBar.borderColor ?? (isUp ? borderUpColor : borderDownColor); - result.barWickColor = currentBar.wickColor ?? (isUp ? wickUpColor : wickDownColor); - - return result; - } - - private _areaStyle(areaStyle: AreaStyleOptions): BarColorerStyle { return { - ...emptyResult, - barColor: areaStyle.lineColor, + barColor: currentBar.color ?? (isUp ? upColor : downColor), + barBorderColor: currentBar.borderColor ?? (isUp ? borderUpColor : borderDownColor), + barWickColor: currentBar.wickColor ?? (isUp ? wickUpColor : wickDownColor), }; - } - - private _baselineStyle(baselineStyle: BaselineStyleOptions, barIndex: TimePointIndex, precomputedBars?: PrecomputedBars): BarColorerStyle { - const currentBar = ensureNotNull(this._findBar(barIndex, precomputedBars)) as SeriesPlotRow<'Baseline'>; + }, + // eslint-disable-next-line @typescript-eslint/naming-convention + Area: (findBar: FindBarFn, areaStyle: AreaStyleOptions, barIndex: TimePointIndex, precomputedBars?: PrecomputedBars): AreaBarColorerStyle => { + const currentBar = ensureNotNull(findBar(barIndex, precomputedBars)) as SeriesPlotRow<'Area'>; + return { + barColor: currentBar.lineColor ?? areaStyle.lineColor, + lineColor: currentBar.lineColor ?? areaStyle.lineColor, + topColor: currentBar.topColor ?? areaStyle.topColor, + bottomColor: currentBar.bottomColor ?? areaStyle.bottomColor, + }; + }, + // eslint-disable-next-line @typescript-eslint/naming-convention + Baseline: (findBar: FindBarFn, baselineStyle: BaselineStyleOptions, barIndex: TimePointIndex, precomputedBars?: PrecomputedBars): BaselineBarColorerStyle => { + const currentBar = ensureNotNull(findBar(barIndex, precomputedBars)) as SeriesPlotRow<'Baseline'>; const isAboveBaseline = currentBar.value[PlotRowValueIndex.Close] >= baselineStyle.baseValue.price; return { - ...emptyResult, barColor: isAboveBaseline ? baselineStyle.topLineColor : baselineStyle.bottomLineColor, + topLineColor: currentBar.topLineColor ?? baselineStyle.topLineColor, + bottomLineColor: currentBar.bottomLineColor ?? baselineStyle.bottomLineColor, + topFillColor1: currentBar.topFillColor1 ?? baselineStyle.topFillColor1, + topFillColor2: currentBar.topFillColor2 ?? baselineStyle.topFillColor2, + bottomFillColor1: currentBar.bottomFillColor1 ?? baselineStyle.bottomFillColor1, + bottomFillColor2: currentBar.bottomFillColor2 ?? baselineStyle.bottomFillColor2, }; - } - - private _lineStyle(lineStyle: LineStyleOptions, barIndex: TimePointIndex, precomputedBars?: PrecomputedBars): BarColorerStyle { - const currentBar = ensureNotNull(this._findBar(barIndex, precomputedBars)) as SeriesPlotRow<'Line'>; + }, + // eslint-disable-next-line @typescript-eslint/naming-convention + Line: (findBar: FindBarFn, lineStyle: LineStyleOptions, barIndex: TimePointIndex, precomputedBars?: PrecomputedBars): LineBarColorerStyle => { + const currentBar = ensureNotNull(findBar(barIndex, precomputedBars)) as SeriesPlotRow<'Line'>; return { - ...emptyResult, barColor: currentBar.color ?? lineStyle.color, + lineColor: currentBar.color ?? lineStyle.color, + }; + }, + // eslint-disable-next-line @typescript-eslint/naming-convention + Histogram: (findBar: FindBarFn, histogramStyle: HistogramStyleOptions, barIndex: TimePointIndex, precomputedBars?: PrecomputedBars): HistogramBarColorerStyle => { + const currentBar = ensureNotNull(findBar(barIndex, precomputedBars)) as SeriesPlotRow<'Histogram'>; + return { + barColor: currentBar.color ?? histogramStyle.color, }; + }, +}; + +export class SeriesBarColorer { + private _series: Series; + private readonly _styleGetter: typeof barStyleFnMap[T]; + + public constructor(series: Series) { + this._series = series; + this._styleGetter = barStyleFnMap[series.seriesType()]; } - private _histogramStyle(histogramStyle: HistogramStyleOptions, barIndex: TimePointIndex, precomputedBars?: PrecomputedBars): BarColorerStyle { - const result: BarColorerStyle = { ...emptyResult }; - const currentBar = ensureNotNull(this._findBar(barIndex, precomputedBars)) as SeriesPlotRow<'Histogram'>; - result.barColor = currentBar.color !== undefined ? currentBar.color : histogramStyle.color; - return result; + public barStyle(barIndex: TimePointIndex, precomputedBars?: PrecomputedBars): BarStylesMap[T] { + // precomputedBars: {value: [Array BarValues], previousValue: [Array BarValues] | undefined} + // Used to avoid binary search if bars are already known + return this._styleGetter(this._findBar, this._series.options(), barIndex, precomputedBars); } - private _findBar(barIndex: TimePointIndex, precomputedBars?: PrecomputedBars): SeriesPlotRow | null { + private _findBar = (barIndex: TimePointIndex, precomputedBars?: PrecomputedBars): SeriesPlotRow | null => { if (precomputedBars !== undefined) { return precomputedBars.value; } return this._series.bars().valueAt(barIndex); - } + }; } diff --git a/src/model/series-data.ts b/src/model/series-data.ts index ebf67f98c8..d9d1f06f37 100644 --- a/src/model/series-data.ts +++ b/src/model/series-data.ts @@ -6,6 +6,21 @@ export interface LinePlotRow extends PlotRow { readonly color?: string; } +export interface AreaPlotRow extends PlotRow { + lineColor?: string; + topColor?: string; + bottomColor?: string; +} + +export interface BaselinePlotRow extends PlotRow { + topFillColor1?: string; + topFillColor2?: string; + topLineColor?: string; + bottomFillColor1?: string; + bottomFillColor2?: string; + bottomLineColor?: string; +} + export interface HistogramPlotRow extends PlotRow { readonly color?: string; } @@ -23,8 +38,8 @@ export interface CandlestickPlotRow extends PlotRow { export interface SeriesPlotRowTypeAtTypeMap { Bar: BarPlotRow; Candlestick: CandlestickPlotRow; - Area: PlotRow; - Baseline: PlotRow; + Area: AreaPlotRow; + Baseline: BaselinePlotRow; Line: LinePlotRow; Histogram: HistogramPlotRow; } diff --git a/src/model/series-options.ts b/src/model/series-options.ts index 580f6ea3fe..f4df143b6c 100644 --- a/src/model/series-options.ts +++ b/src/model/series-options.ts @@ -198,6 +198,12 @@ export interface LineStyleOptions { * @defaultValue `''` */ crosshairMarkerBackgroundColor: string; + /** + * Crosshair marker border width in pixels. + * + * @defaultValue `2` + */ + crosshairMarkerBorderWidth: number; /** * Last price animation mode. @@ -225,6 +231,13 @@ export interface AreaStyleOptions { */ bottomColor: string; + /** + * Invert the filled area. Fills the area above the line if set to true. + * + * @defaultValue `false` + */ + invertFilledArea: boolean; + /** * Line color. * @@ -277,6 +290,12 @@ export interface AreaStyleOptions { * @defaultValue `''` */ crosshairMarkerBackgroundColor: string; + /** + * Crosshair marker border width in pixels. + * + * @defaultValue `2` + */ + crosshairMarkerBorderWidth: number; /** * Last price animation mode. @@ -398,6 +417,12 @@ export interface BaselineStyleOptions { * @defaultValue `''` */ crosshairMarkerBackgroundColor: string; + /** + * Crosshair marker border width in pixels. + * + * @defaultValue `2` + */ + crosshairMarkerBorderWidth: number; /** * Last price animation mode. @@ -787,6 +812,38 @@ export type LineSeriesOptions = SeriesOptions; */ export type LineSeriesPartialOptions = SeriesPartialOptions; +/** + * Represents the type of style options for each series type. + * + * For example a bar series has style options represented by {@link BarStyleOptions}. + */ +export interface SeriesStyleOptionsMap { + /** + * The type of bar style options. + */ + Bar: BarStyleOptions; + /** + * The type of candlestick style options. + */ + Candlestick: CandlestickStyleOptions; + /** + * The type of area style options. + */ + Area: AreaStyleOptions; + /** + * The type of baseline style options. + */ + Baseline: BaselineStyleOptions; + /** + * The type of line style options. + */ + Line: LineStyleOptions; + /** + * The type of histogram style options. + */ + Histogram: HistogramStyleOptions; +} + /** * Represents the type of options for each series type. * diff --git a/src/model/series.ts b/src/model/series.ts index 0cb733e5d8..2b31ca9f52 100644 --- a/src/model/series.ts +++ b/src/model/series.ts @@ -74,6 +74,7 @@ export interface MarkerData { price: BarPrice; radius: number; borderColor: string | null; + borderWidth: number; backgroundColor: string; } @@ -105,7 +106,7 @@ export class Series extends PriceDataSource i private readonly _baseHorizontalLineView: SeriesHorizontalBaseLinePaneView = new SeriesHorizontalBaseLinePaneView(this); private _paneView!: IUpdatablePaneView; private readonly _lastPriceAnimationPaneView: SeriesLastPriceAnimationPaneView | null = null; - private _barColorerCache: SeriesBarColorer | null = null; + private _barColorerCache: SeriesBarColorer | null = null; private readonly _options: SeriesOptionsInternal; private _markers: readonly SeriesMarker[] = []; private _indexedMarkers: InternalSeriesMarker[] = []; @@ -198,7 +199,7 @@ export class Series extends PriceDataSource i }; } - public barColorer(): SeriesBarColorer { + public barColorer(): SeriesBarColorer { if (this._barColorerCache !== null) { return this._barColorerCache; } @@ -459,8 +460,9 @@ export class Series extends PriceDataSource i const price = bar.value[PlotRowValueIndex.Close] as BarPrice; const radius = this._markerRadius(); const borderColor = this._markerBorderColor(); + const borderWidth = this._markerBorderWidth(); const backgroundColor = this._markerBackgroundColor(index); - return { price, radius, borderColor, backgroundColor }; + return { price, radius, borderColor, borderWidth, backgroundColor }; } public title(): string { @@ -526,6 +528,17 @@ export class Series extends PriceDataSource i return null; } + private _markerBorderWidth(): number { + switch (this._seriesType) { + case 'Line': + case 'Area': + case 'Baseline': + return (this._options as (LineStyleOptions | AreaStyleOptions | BaselineStyleOptions)).crosshairMarkerBorderWidth; + } + + return 0; + } + private _markerBackgroundColor(index: TimePointIndex): string { switch (this._seriesType) { case 'Line': diff --git a/src/model/text-width-cache.ts b/src/model/text-width-cache.ts index 143a2ce8d2..86e7a39f46 100644 --- a/src/model/text-width-cache.ts +++ b/src/model/text-width-cache.ts @@ -1,71 +1,69 @@ -export type CanvasCtxLike = Pick; +import { ensureDefined } from '../helpers/assertions'; + +export type CanvasCtxLike = Pick; const defaultReplacementRe = /[2-9]/g; export class TextWidthCache { - private _cache: Map = new Map(); - /** A "cyclic buffer" of cache keys */ - private _keys: (string | undefined)[]; - /** Current index in the "cyclic buffer" */ - private _keysIndex: number = 0; + private readonly _maxSize: number; + private _actualSize: number = 0; + private _usageTick: number = 1; + private _oldestTick: number = 1; + private _tick2Labels: Record = {}; + private _cache: Map = new Map(); public constructor(size: number = 50) { - // A trick to keep array PACKED_ELEMENTS - this._keys = Array.from(new Array(size)); + this._maxSize = size; } public reset(): void { + this._actualSize = 0; this._cache.clear(); - this._keys.fill(undefined); - // We don't care where exactly the _keysIndex points, - // so there's no point in resetting it + this._usageTick = 1; + this._oldestTick = 1; + this._tick2Labels = {}; } public measureText(ctx: CanvasCtxLike, text: string, optimizationReplacementRe?: RegExp): number { - const re = optimizationReplacementRe || defaultReplacementRe; - const cacheString = String(text).replace(re, '0'); - - let width = this._cache.get(cacheString); - - if (width === undefined) { - width = ctx.measureText(cacheString).width; + return this._getMetrics(ctx, text, optimizationReplacementRe).width; + } - if (width === 0 && text.length !== 0) { - // measureText can return 0 in FF depending on a canvas size, don't cache it - return 0; - } + public yMidCorrection(ctx: CanvasCtxLike, text: string, optimizationReplacementRe?: RegExp): number { + const metrics = this._getMetrics(ctx, text, optimizationReplacementRe); + // if actualBoundingBoxAscent/actualBoundingBoxDescent are not supported we use 0 as a fallback + return ((metrics.actualBoundingBoxAscent || 0) - (metrics.actualBoundingBoxDescent || 0)) / 2; + } - // A cyclic buffer is used to keep track of the cache keys and to delete - // the oldest one before a new one is inserted. - // ├──────┬──────┬──────┬──────┤ - // │ foo │ bar │ │ │ - // ├──────┴──────┴──────┴──────┤ - // ↑ index + private _getMetrics(ctx: CanvasCtxLike, text: string, optimizationReplacementRe?: RegExp): TextMetrics { + const re = optimizationReplacementRe || defaultReplacementRe; + const cacheString = String(text).replace(re, '0'); - // Eventually, the index reach the end of an array and roll-over to 0. - // ├──────┬──────┬──────┬──────┤ - // │ foo │ bar │ baz │ quux │ - // ├──────┴──────┴──────┴──────┤ - // ↑ index = 0 + if (this._cache.has(cacheString)) { + return ensureDefined(this._cache.get(cacheString)).metrics; + } - // After that the oldest value will be overwritten. - // ├──────┬──────┬──────┬──────┤ - // │ WOOT │ bar │ baz │ quux │ - // ├──────┴──────┴──────┴──────┤ - // ↑ index = 1 + if (this._actualSize === this._maxSize) { + const oldestValue = this._tick2Labels[this._oldestTick]; + delete this._tick2Labels[this._oldestTick]; + this._cache.delete(oldestValue); + this._oldestTick++; + this._actualSize--; + } - const oldestKey = this._keys[this._keysIndex]; - if (oldestKey !== undefined) { - this._cache.delete(oldestKey); - } - // Set a newest key in place of the just deleted one - this._keys[this._keysIndex] = cacheString; - // Advance the index so it always points the oldest value - this._keysIndex = (this._keysIndex + 1) % this._keys.length; + ctx.save(); + ctx.textBaseline = 'middle'; + const metrics = ctx.measureText(cacheString); + ctx.restore(); - this._cache.set(cacheString, width); + if (metrics.width === 0 && !!text.length) { + // measureText can return 0 in FF depending on a canvas size, don't cache it + return metrics; } - return width; + this._cache.set(cacheString, { metrics: metrics, tick: this._usageTick }); + this._tick2Labels[this._usageTick] = cacheString; + this._actualSize++; + this._usageTick++; + return metrics; } } diff --git a/src/model/time-scale.ts b/src/model/time-scale.ts index 492aee5f9e..a81d02dfba 100644 --- a/src/model/time-scale.ts +++ b/src/model/time-scale.ts @@ -199,7 +199,7 @@ export interface TimeScaleOptions { /** * Draw small vertical line on time axis labels. * - * @defaultValue `true` + * @defaultValue `false` */ ticksVisible: boolean; } @@ -670,17 +670,15 @@ export class TimeScale { const source = this._rightOffset; const animationStart = performance.now(); - const animationFn = () => { - const animationProgress = (performance.now() - animationStart) / animationDuration; - const finishAnimation = animationProgress >= 1; - const rightOffset = finishAnimation ? offset : source + (offset - source) * animationProgress; - this.setRightOffset(rightOffset); - if (!finishAnimation) { - setTimeout(animationFn, 20); - } - }; - animationFn(); + this._model.setTimeScaleAnimation({ + finished: (time: number) => (time - animationStart) / animationDuration >= 1, + getPosition: (time: number) => { + const animationProgress = (time - animationStart) / animationDuration; + const finishAnimation = animationProgress >= 1; + return finishAnimation ? offset : source + (offset - source) * animationProgress; + }, + }); } public update(newPoints: readonly TimeScalePoint[], firstChangedPointIndex: number): void { @@ -753,7 +751,7 @@ export class TimeScale { && !handleScroll.mouseWheel && !handleScroll.pressedMouseMove && !handleScroll.vertTouchDrag - && !handleScale.axisDoubleClickReset + && !handleScale.axisDoubleClickReset.time && !handleScale.axisPressedMouseMove.time && !handleScale.mouseWheel && !handleScale.pinch; diff --git a/src/renderers/area-renderer-base.ts b/src/renderers/area-renderer-base.ts new file mode 100644 index 0000000000..21e2fa23d9 --- /dev/null +++ b/src/renderers/area-renderer-base.ts @@ -0,0 +1,68 @@ +import { Coordinate } from '../model/coordinate'; +import { PricedValue } from '../model/price-scale'; +import { SeriesItemsIndexesRange, TimedValue } from '../model/time-data'; + +import { LinePoint, LineStyle, LineType, LineWidth, setLineStyle } from './draw-line'; +import { ScaledRenderer } from './scaled-renderer'; +import { walkLine } from './walk-line'; + +export type AreaFillItemBase = TimedValue & PricedValue & LinePoint; +export interface PaneRendererAreaDataBase { + items: TItem[]; + lineType: LineType; + lineWidth: LineWidth; + lineStyle: LineStyle; + + bottom: Coordinate; + baseLevelCoordinate: Coordinate; + + barWidth: number; + + visibleRange: SeriesItemsIndexesRange | null; +} + +function finishStyledArea( + baseLevelCoordinate: Coordinate, + ctx: CanvasRenderingContext2D, + style: CanvasRenderingContext2D['fillStyle'], + areaFirstItem: LinePoint, + newAreaFirstItem: LinePoint +): void { + ctx.lineTo(newAreaFirstItem.x, baseLevelCoordinate); + ctx.lineTo(areaFirstItem.x, baseLevelCoordinate); + ctx.closePath(); + ctx.fillStyle = style; + ctx.fill(); +} + +export abstract class PaneRendererAreaBase extends ScaledRenderer { + protected _data: TData | null = null; + + public setData(data: TData): void { + this._data = data; + } + + protected _drawImpl(ctx: CanvasRenderingContext2D): void { + if (this._data === null) { + return; + } + + const { items, visibleRange, barWidth, lineWidth, lineStyle, lineType, baseLevelCoordinate } = this._data; + + if (visibleRange === null) { + return; + } + + ctx.lineCap = 'butt'; + ctx.lineJoin = 'round'; + ctx.lineWidth = lineWidth; + setLineStyle(ctx, lineStyle); + + // walk lines with width=1 to have more accurate gradient's filling + ctx.lineWidth = 1; + + walkLine(ctx, items, lineType, visibleRange, barWidth, this._fillStyle.bind(this), finishStyledArea.bind(null, baseLevelCoordinate)); + } + + protected abstract _fillStyle(ctx: CanvasRenderingContext2D, item: TData['items'][0]): CanvasRenderingContext2D['fillStyle']; +} diff --git a/src/renderers/area-renderer.ts b/src/renderers/area-renderer.ts index aeac3842a9..05f709389f 100644 --- a/src/renderers/area-renderer.ts +++ b/src/renderers/area-renderer.ts @@ -1,87 +1,42 @@ import { Coordinate } from '../model/coordinate'; -import { SeriesItemsIndexesRange } from '../model/time-data'; +import { AreaFillColorerStyle } from '../model/series-bar-colorer'; -import { LineStyle, LineType, LineWidth, setLineStyle } from './draw-line'; -import { LineItem } from './line-renderer'; -import { ScaledRenderer } from './scaled-renderer'; -import { walkLine } from './walk-line'; +import { AreaFillItemBase, PaneRendererAreaBase, PaneRendererAreaDataBase } from './area-renderer-base'; -export interface PaneRendererAreaDataBase { - items: LineItem[]; - lineType: LineType; - lineWidth: LineWidth; - lineStyle: LineStyle; +export type AreaFillItem = AreaFillItemBase & AreaFillColorerStyle; +export interface PaneRendererAreaData extends PaneRendererAreaDataBase { +} +interface AreaFillCache extends Record { + fillStyle: CanvasRenderingContext2D['fillStyle']; bottom: Coordinate; - baseLevelCoordinate: Coordinate; - - barWidth: number; - - visibleRange: SeriesItemsIndexesRange | null; } -export abstract class PaneRendererAreaBase extends ScaledRenderer { - protected _data: TData | null = null; - - public setData(data: TData): void { - this._data = data; - } - - protected _drawImpl(ctx: CanvasRenderingContext2D): void { - if (this._data === null || this._data.items.length === 0 || this._data.visibleRange === null) { - return; - } - - ctx.lineCap = 'butt'; - ctx.lineJoin = 'round'; - ctx.lineWidth = this._data.lineWidth; - setLineStyle(ctx, this._data.lineStyle); - - // walk lines with width=1 to have more accurate gradient's filling - ctx.lineWidth = 1; - - ctx.beginPath(); +export class PaneRendererArea extends PaneRendererAreaBase { + private _fillCache: AreaFillCache | null = null; - if (this._data.items.length === 1) { - const point = this._data.items[0]; - const halfBarWidth = this._data.barWidth / 2; - ctx.moveTo(point.x - halfBarWidth, this._data.baseLevelCoordinate); - ctx.lineTo(point.x - halfBarWidth, point.y); - ctx.lineTo(point.x + halfBarWidth, point.y); - ctx.lineTo(point.x + halfBarWidth, this._data.baseLevelCoordinate); - } else { - ctx.moveTo(this._data.items[this._data.visibleRange.from].x, this._data.baseLevelCoordinate); - ctx.lineTo(this._data.items[this._data.visibleRange.from].x, this._data.items[this._data.visibleRange.from].y); + protected override _fillStyle(ctx: CanvasRenderingContext2D, item: AreaFillItem): CanvasRenderingContext2D['fillStyle'] { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const data = this._data!; - walkLine(ctx, this._data.items, this._data.lineType, this._data.visibleRange); + const { topColor, bottomColor } = item; + const bottom = data.bottom; - if (this._data.visibleRange.to > this._data.visibleRange.from) { - ctx.lineTo(this._data.items[this._data.visibleRange.to - 1].x, this._data.baseLevelCoordinate); - ctx.lineTo(this._data.items[this._data.visibleRange.from].x, this._data.baseLevelCoordinate); - } + if ( + this._fillCache !== null && + this._fillCache.topColor === topColor && + this._fillCache.bottomColor === bottomColor && + this._fillCache.bottom === bottom + ) { + return this._fillCache.fillStyle; } - ctx.closePath(); - - ctx.fillStyle = this._fillStyle(ctx); - ctx.fill(); - } - - protected abstract _fillStyle(ctx: CanvasRenderingContext2D): CanvasRenderingContext2D['fillStyle']; -} -export interface PaneRendererAreaData extends PaneRendererAreaDataBase { - topColor: string; - bottomColor: string; -} + const fillStyle = ctx.createLinearGradient(0, 0, 0, bottom); + fillStyle.addColorStop(0, topColor); + fillStyle.addColorStop(1, bottomColor); -export class PaneRendererArea extends PaneRendererAreaBase { - protected override _fillStyle(ctx: CanvasRenderingContext2D): CanvasRenderingContext2D['fillStyle'] { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const data = this._data!; + this._fillCache = { topColor, bottomColor, fillStyle, bottom }; - const gradient = ctx.createLinearGradient(0, 0, 0, data.bottom); - gradient.addColorStop(0, data.topColor); - gradient.addColorStop(1, data.bottomColor); - return gradient; + return fillStyle; } } diff --git a/src/renderers/bars-renderer.ts b/src/renderers/bars-renderer.ts index 55a9d06485..cb236fc971 100644 --- a/src/renderers/bars-renderer.ts +++ b/src/renderers/bars-renderer.ts @@ -1,6 +1,7 @@ import { ensureNotNull } from '../helpers/assertions'; import { BarCoordinates, BarPrices } from '../model/bar'; +import { BarColorerStyle } from '../model/series-bar-colorer'; import { SeriesItemsIndexesRange, TimedValue } from '../model/time-data'; import { IPaneRenderer } from './ipane-renderer'; @@ -8,8 +9,7 @@ import { optimalBarWidth } from './optimal-bar-width'; export type BarCandlestickItemBase = TimedValue & BarPrices & BarCoordinates; -export interface BarItem extends BarCandlestickItemBase { - color: string; +export interface BarItem extends BarCandlestickItemBase, BarColorerStyle { } export interface PaneRendererBarsData { @@ -56,9 +56,9 @@ export class PaneRendererBars implements IPaneRenderer { const drawOpenClose = this._barLineWidth <= this._barWidth && this._data.barSpacing >= Math.floor(1.5 * pixelRatio); for (let i = this._data.visibleRange.from; i < this._data.visibleRange.to; ++i) { const bar = this._data.bars[i]; - if (prevColor !== bar.color) { - ctx.fillStyle = bar.color; - prevColor = bar.color; + if (prevColor !== bar.barColor) { + ctx.fillStyle = bar.barColor; + prevColor = bar.barColor; } const bodyWidthHalf = Math.floor(this._barLineWidth * 0.5); diff --git a/src/renderers/baseline-renderer-area.ts b/src/renderers/baseline-renderer-area.ts new file mode 100644 index 0000000000..70c83e6bc1 --- /dev/null +++ b/src/renderers/baseline-renderer-area.ts @@ -0,0 +1,59 @@ +import { clamp } from '../helpers/mathex'; + +import { Coordinate } from '../model/coordinate'; +import { BaselineFillColorerStyle } from '../model/series-bar-colorer'; + +import { AreaFillItemBase, PaneRendererAreaBase, PaneRendererAreaDataBase } from './area-renderer-base'; + +export type BaselineFillItem = AreaFillItemBase & BaselineFillColorerStyle; +export interface PaneRendererBaselineData extends PaneRendererAreaDataBase { +} + +interface BaselineFillCache extends Record { + fillStyle: CanvasRenderingContext2D['fillStyle']; + baseLevelCoordinate: Coordinate; + bottom: Coordinate; +} +export class PaneRendererBaselineArea extends PaneRendererAreaBase { + private _fillCache: BaselineFillCache | null = null; + + protected override _fillStyle(ctx: CanvasRenderingContext2D, item: BaselineFillItem): CanvasRenderingContext2D['fillStyle'] { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const data = this._data!; + + const { topFillColor1, topFillColor2, bottomFillColor1, bottomFillColor2 } = item; + const { baseLevelCoordinate, bottom } = data; + + if ( + this._fillCache !== null && + this._fillCache.topFillColor1 === topFillColor1 && + this._fillCache.topFillColor2 === topFillColor2 && + this._fillCache.bottomFillColor1 === bottomFillColor1 && + this._fillCache.bottomFillColor2 === bottomFillColor2 && + this._fillCache.baseLevelCoordinate === baseLevelCoordinate && + this._fillCache.bottom === bottom + ) { + return this._fillCache.fillStyle; + } + + const fillStyle = ctx.createLinearGradient(0, 0, 0, bottom); + const baselinePercent = clamp(baseLevelCoordinate / bottom, 0, 1); + + fillStyle.addColorStop(0, topFillColor1); + fillStyle.addColorStop(baselinePercent, topFillColor2); + fillStyle.addColorStop(baselinePercent, bottomFillColor1); + fillStyle.addColorStop(1, bottomFillColor2); + + this._fillCache = { + topFillColor1, + topFillColor2, + bottomFillColor1, + bottomFillColor2, + fillStyle, + baseLevelCoordinate, + bottom, + }; + + return fillStyle; + } +} diff --git a/src/renderers/baseline-renderer-line.ts b/src/renderers/baseline-renderer-line.ts new file mode 100644 index 0000000000..6d49db77c9 --- /dev/null +++ b/src/renderers/baseline-renderer-line.ts @@ -0,0 +1,58 @@ +import { clamp } from '../helpers/mathex'; + +import { Coordinate } from '../model/coordinate'; +import { BaselineStrokeColorerStyle } from '../model/series-bar-colorer'; + +import { LineItemBase as LineStrokeItemBase, PaneRendererLineBase, PaneRendererLineDataBase } from './line-renderer-base'; + +export type BaselineStrokeItem = LineStrokeItemBase & BaselineStrokeColorerStyle; +export interface PaneRendererBaselineLineData extends PaneRendererLineDataBase { + baseLevelCoordinate: Coordinate; + bottom: Coordinate; +} + +interface BaselineStrokeCache extends Record { + strokeStyle: CanvasRenderingContext2D['strokeStyle']; + baseLevelCoordinate: Coordinate; + bottom: Coordinate; +} + +export class PaneRendererBaselineLine extends PaneRendererLineBase { + private _strokeCache: BaselineStrokeCache | null = null; + + protected override _strokeStyle(ctx: CanvasRenderingContext2D, item: BaselineStrokeItem): CanvasRenderingContext2D['strokeStyle'] { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const data = this._data!; + + const { topLineColor, bottomLineColor } = item; + const { baseLevelCoordinate, bottom } = data; + + if ( + this._strokeCache !== null && + this._strokeCache.topLineColor === topLineColor && + this._strokeCache.bottomLineColor === bottomLineColor && + this._strokeCache.baseLevelCoordinate === baseLevelCoordinate && + this._strokeCache.bottom === bottom + ) { + return this._strokeCache.strokeStyle; + } + + const strokeStyle = ctx.createLinearGradient(0, 0, 0, bottom); + const baselinePercent = clamp(baseLevelCoordinate / bottom, 0, 1); + + strokeStyle.addColorStop(0, topLineColor); + strokeStyle.addColorStop(baselinePercent, topLineColor); + strokeStyle.addColorStop(baselinePercent, bottomLineColor); + strokeStyle.addColorStop(1, bottomLineColor); + + this._strokeCache = { + topLineColor, + bottomLineColor, + strokeStyle, + baseLevelCoordinate, + bottom, + }; + + return strokeStyle; + } +} diff --git a/src/renderers/baseline-renderer.ts b/src/renderers/baseline-renderer.ts deleted file mode 100644 index 6b3e2a6b6d..0000000000 --- a/src/renderers/baseline-renderer.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { clamp } from '../helpers/mathex'; - -import { Coordinate } from '../model/coordinate'; - -import { PaneRendererAreaBase, PaneRendererAreaDataBase } from './area-renderer'; -import { PaneRendererLineBase, PaneRendererLineDataBase } from './line-renderer'; - -export interface PaneRendererBaselineData extends PaneRendererAreaDataBase { - topFillColor1: string; - topFillColor2: string; - - bottomFillColor1: string; - bottomFillColor2: string; -} - -export class PaneRendererBaselineArea extends PaneRendererAreaBase { - protected override _fillStyle(ctx: CanvasRenderingContext2D): CanvasRenderingContext2D['fillStyle'] { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const data = this._data!; - - const gradient = ctx.createLinearGradient(0, 0, 0, data.bottom); - const baselinePercent = clamp(data.baseLevelCoordinate / data.bottom, 0, 1); - - gradient.addColorStop(0, data.topFillColor1); - gradient.addColorStop(baselinePercent, data.topFillColor2); - gradient.addColorStop(baselinePercent, data.bottomFillColor1); - gradient.addColorStop(1, data.bottomFillColor2); - - return gradient; - } -} - -export interface PaneRendererBaselineLineData extends PaneRendererLineDataBase { - topColor: string; - bottomColor: string; - - baseLevelCoordinate: Coordinate; - bottom: Coordinate; -} - -export class PaneRendererBaselineLine extends PaneRendererLineBase { - protected override _strokeStyle(ctx: CanvasRenderingContext2D): CanvasRenderingContext2D['strokeStyle'] { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const data = this._data!; - - const gradient = ctx.createLinearGradient(0, 0, 0, data.bottom); - const baselinePercent = clamp(data.baseLevelCoordinate / data.bottom, 0, 1); - - gradient.addColorStop(0, data.topColor); - gradient.addColorStop(baselinePercent, data.topColor); - gradient.addColorStop(baselinePercent, data.bottomColor); - gradient.addColorStop(1, data.bottomColor); - - return gradient; - } -} diff --git a/src/renderers/candlesticks-renderer.ts b/src/renderers/candlesticks-renderer.ts index 77665c3002..58e8273c74 100644 --- a/src/renderers/candlesticks-renderer.ts +++ b/src/renderers/candlesticks-renderer.ts @@ -1,15 +1,13 @@ import { fillRectInnerBorder } from '../helpers/canvas-helpers'; +import { CandlesticksColorerStyle } from '../model/series-bar-colorer'; import { SeriesItemsIndexesRange } from '../model/time-data'; import { BarCandlestickItemBase } from './bars-renderer'; import { IPaneRenderer } from './ipane-renderer'; import { optimalCandlestickWidth } from './optimal-bar-width'; -export interface CandlestickItem extends BarCandlestickItemBase { - color: string; - borderColor: string; - wickColor: string; +export interface CandlestickItem extends BarCandlestickItemBase, CandlesticksColorerStyle { } export interface PaneRendererCandlesticksData { @@ -86,9 +84,9 @@ export class PaneRendererCandlesticks implements IPaneRenderer { for (let i = visibleRange.from; i < visibleRange.to; i++) { const bar = bars[i]; - if (bar.wickColor !== prevWickColor) { - ctx.fillStyle = bar.wickColor; - prevWickColor = bar.wickColor; + if (bar.barWickColor !== prevWickColor) { + ctx.fillStyle = bar.barWickColor; + prevWickColor = bar.barWickColor; } const top = Math.round(Math.min(bar.openY, bar.closeY) * pixelRatio); @@ -138,9 +136,9 @@ export class PaneRendererCandlesticks implements IPaneRenderer { for (let i = visibleRange.from; i < visibleRange.to; i++) { const bar = bars[i]; - if (bar.borderColor !== prevBorderColor) { - ctx.fillStyle = bar.borderColor; - prevBorderColor = bar.borderColor; + if (bar.barBorderColor !== prevBorderColor) { + ctx.fillStyle = bar.barBorderColor; + prevBorderColor = bar.barBorderColor; } let left = Math.round(bar.x * pixelRatio) - Math.floor(this._barWidth * 0.5); @@ -182,8 +180,8 @@ export class PaneRendererCandlesticks implements IPaneRenderer { let left = Math.round(bar.x * pixelRatio) - Math.floor(this._barWidth * 0.5); let right = left + this._barWidth - 1; - if (bar.color !== prevBarColor) { - const barColor = bar.color; + if (bar.barColor !== prevBarColor) { + const barColor = bar.barColor; ctx.fillStyle = barColor; prevBarColor = barColor; } diff --git a/src/renderers/histogram-renderer.ts b/src/renderers/histogram-renderer.ts index 0be7e2724e..505677d0fb 100644 --- a/src/renderers/histogram-renderer.ts +++ b/src/renderers/histogram-renderer.ts @@ -7,7 +7,7 @@ const showSpacingMinimalBarWidth = 1; const alignToMinimalWidthLimit = 4; export interface HistogramItem extends PricedValue, TimedValue { - color: string; + barColor: string; } export interface PaneRendererHistogramData { @@ -53,7 +53,7 @@ export class PaneRendererHistogram implements IPaneRenderer { const item = this._data.items[i]; const current = this._precalculatedCache[i - this._data.visibleRange.from]; const y = Math.round(item.y * pixelRatio); - ctx.fillStyle = item.color; + ctx.fillStyle = item.barColor; let top: number; let bottom: number; diff --git a/src/renderers/iprice-axis-view-renderer.ts b/src/renderers/iprice-axis-view-renderer.ts index 60e1a31e04..8682429ea9 100644 --- a/src/renderers/iprice-axis-view-renderer.ts +++ b/src/renderers/iprice-axis-view-renderer.ts @@ -7,6 +7,8 @@ export interface PriceAxisViewRendererCommonData { color: string; coordinate: number; fixedCoordinate?: number; + additionalPaddingTop: number; + additionalPaddingBottom: number; } export interface PriceAxisViewRendererData { @@ -15,7 +17,10 @@ export interface PriceAxisViewRendererData { tickVisible: boolean; moveTextToInvisibleTick: boolean; borderColor: string; + color: string; lineWidth?: LineWidth; + borderVisible: boolean; + separatorVisible: boolean; } export interface PriceAxisViewRendererOptions { @@ -24,6 +29,7 @@ export interface PriceAxisViewRendererOptions { font: string; fontFamily: string; color: string; + paneBackgroundColor: string; fontSize: number; paddingBottom: number; paddingInner: number; diff --git a/src/renderers/itime-axis-view-renderer.ts b/src/renderers/itime-axis-view-renderer.ts index 6e66ba52cf..ecc5a89698 100644 --- a/src/renderers/itime-axis-view-renderer.ts +++ b/src/renderers/itime-axis-view-renderer.ts @@ -10,6 +10,7 @@ export interface TimeAxisViewRendererOptions { tickLength: number; paddingHorizontal: number; widthCache: TextWidthCache; + labelBottomOffset: number; } export interface ITimeAxisViewRenderer { diff --git a/src/renderers/line-renderer-base.ts b/src/renderers/line-renderer-base.ts new file mode 100644 index 0000000000..55584da2fc --- /dev/null +++ b/src/renderers/line-renderer-base.ts @@ -0,0 +1,57 @@ +import { PricedValue } from '../model/price-scale'; +import { SeriesItemsIndexesRange, TimedValue } from '../model/time-data'; + +import { LinePoint, LineStyle, LineType, LineWidth, setLineStyle } from './draw-line'; +import { ScaledRenderer } from './scaled-renderer'; +import { walkLine } from './walk-line'; + +export type LineItemBase = TimedValue & PricedValue & LinePoint; + +export interface PaneRendererLineDataBase { + lineType: LineType; + + items: TItem[]; + + barWidth: number; + + lineWidth: LineWidth; + lineStyle: LineStyle; + + visibleRange: SeriesItemsIndexesRange | null; +} + +function finishStyledArea(ctx: CanvasRenderingContext2D, style: CanvasRenderingContext2D['strokeStyle']): void { + ctx.strokeStyle = style; + ctx.stroke(); +} + +export abstract class PaneRendererLineBase extends ScaledRenderer { + protected _data: TData | null = null; + + public setData(data: TData): void { + this._data = data; + } + + protected _drawImpl(ctx: CanvasRenderingContext2D): void { + if (this._data === null) { + return; + } + + const { items, visibleRange, barWidth, lineType, lineWidth, lineStyle } = this._data; + + if (visibleRange === null) { + return; + } + + ctx.lineCap = 'butt'; + ctx.lineWidth = lineWidth; + + setLineStyle(ctx, lineStyle); + + ctx.lineJoin = 'round'; + + walkLine(ctx, items, lineType, visibleRange, barWidth, this._strokeStyle.bind(this), finishStyledArea); + } + + protected abstract _strokeStyle(ctx: CanvasRenderingContext2D, item: TData['items'][0]): CanvasRenderingContext2D['strokeStyle']; +} diff --git a/src/renderers/line-renderer.ts b/src/renderers/line-renderer.ts index 39cc1614da..e03f5d9296 100644 --- a/src/renderers/line-renderer.ts +++ b/src/renderers/line-renderer.ts @@ -1,136 +1,13 @@ -import { PricedValue } from '../model/price-scale'; -import { SeriesItemsIndexesRange, TimedValue } from '../model/time-data'; +import { LineStrokeColorerStyle } from '../model/series-bar-colorer'; -import { LinePoint, LineStyle, LineType, LineWidth, setLineStyle } from './draw-line'; -import { ScaledRenderer } from './scaled-renderer'; -import { getControlPoints, walkLine } from './walk-line'; +import { LineItemBase, PaneRendererLineBase, PaneRendererLineDataBase } from './line-renderer-base'; -export type LineItem = TimedValue & PricedValue & LinePoint & { color?: string }; - -export interface PaneRendererLineDataBase { - lineType: LineType; - - items: LineItem[]; - - barWidth: number; - - lineWidth: LineWidth; - lineStyle: LineStyle; - - visibleRange: SeriesItemsIndexesRange | null; -} - -export abstract class PaneRendererLineBase extends ScaledRenderer { - protected _data: TData | null = null; - - public setData(data: TData): void { - this._data = data; - } - - protected _drawImpl(ctx: CanvasRenderingContext2D): void { - if (this._data === null || this._data.items.length === 0 || this._data.visibleRange === null) { - return; - } - - ctx.lineCap = 'butt'; - ctx.lineWidth = this._data.lineWidth; - - setLineStyle(ctx, this._data.lineStyle); - - ctx.strokeStyle = this._strokeStyle(ctx); - ctx.lineJoin = 'round'; - - 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 { - 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(); - } - - protected abstract _strokeStyle(ctx: CanvasRenderingContext2D): CanvasRenderingContext2D['strokeStyle']; -} - -export interface PaneRendererLineData extends PaneRendererLineDataBase { - lineColor: string; +export type LineStrokeItem = LineItemBase & LineStrokeColorerStyle; +export interface PaneRendererLineData extends PaneRendererLineDataBase { } export class PaneRendererLine extends PaneRendererLineBase { - /** - * 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 currentStrokeStyle = currItem.color ?? lineColor; - - switch (lineType) { - case LineType.Simple: - ctx.lineTo(currItem.x, currItem.y); - break; - case LineType.WithSteps: - ctx.lineTo(currItem.x, items[i - 1].y); - - if (currentStrokeStyle !== prevStrokeStyle) { - changeColor(currentStrokeStyle); - ctx.lineTo(currItem.x, items[i - 1].y); - } - - ctx.lineTo(currItem.x, currItem.y); - break; - case LineType.Curved: { - const [cp1, cp2] = getControlPoints(items, i - 1, i); - ctx.bezierCurveTo(cp1.x, cp1.y, cp2.x, cp2.y, currItem.x, currItem.y); - break; - } - } - - 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; + protected override _strokeStyle(ctx: CanvasRenderingContext2D, item: LineStrokeItem): CanvasRenderingContext2D['strokeStyle'] { + return item.lineColor; } } diff --git a/src/renderers/marks-renderer.ts b/src/renderers/marks-renderer.ts index 805b7ae3a7..a45b9fedad 100644 --- a/src/renderers/marks-renderer.ts +++ b/src/renderers/marks-renderer.ts @@ -1,11 +1,12 @@ import { SeriesItemsIndexesRange } from '../model/time-data'; -import { LineItem } from './line-renderer'; +import { LineItemBase } from './line-renderer-base'; import { ScaledRenderer } from './scaled-renderer'; export interface MarksRendererData { - items: LineItem[]; + items: LineItemBase[]; lineColor: string; + lineWidth: number; backColor: string; radius: number; visibleRange: SeriesItemsIndexesRange | null; @@ -38,8 +39,10 @@ export class PaneRendererMarks extends ScaledRenderer { ctx.fill(); }; - ctx.fillStyle = data.backColor; - draw(data.radius + 2); + if (data.lineWidth > 0) { + ctx.fillStyle = data.backColor; + draw(data.radius + data.lineWidth); + } ctx.fillStyle = data.lineColor; draw(data.radius); diff --git a/src/renderers/price-axis-renderer-options-provider.ts b/src/renderers/price-axis-renderer-options-provider.ts index b6d8327768..cd8aeb4117 100644 --- a/src/renderers/price-axis-renderer-options-provider.ts +++ b/src/renderers/price-axis-renderer-options-provider.ts @@ -6,7 +6,7 @@ import { PriceAxisViewRendererOptions } from './iprice-axis-view-renderer'; const enum RendererConstants { BorderSize = 1, - TickLength = 4, + TickLength = 5, } export class PriceAxisRendererOptionsProvider { @@ -19,6 +19,7 @@ export class PriceAxisRendererOptionsProvider { font: '', fontFamily: '', color: '', + paneBackgroundColor: '', paddingBottom: 0, paddingInner: 0, paddingOuter: 0, @@ -40,17 +41,15 @@ export class PriceAxisRendererOptionsProvider { rendererOptions.fontSize = currentFontSize; rendererOptions.fontFamily = currentFontFamily; rendererOptions.font = makeFont(currentFontSize, currentFontFamily); - rendererOptions.paddingTop = Math.floor(currentFontSize / 3.5); + rendererOptions.paddingTop = 2.5 / 12 * currentFontSize; // 2.5 px for 12px font rendererOptions.paddingBottom = rendererOptions.paddingTop; - rendererOptions.paddingInner = Math.max( - Math.ceil(currentFontSize / 2 - rendererOptions.tickLength / 2), - 0 - ); - rendererOptions.paddingOuter = Math.ceil(currentFontSize / 2 + rendererOptions.tickLength / 2); - rendererOptions.baselineOffset = Math.round(currentFontSize / 10); + rendererOptions.paddingInner = currentFontSize / 12 * rendererOptions.tickLength; + rendererOptions.paddingOuter = currentFontSize / 12 * rendererOptions.tickLength; + rendererOptions.baselineOffset = 0; } rendererOptions.color = this._textColor(); + rendererOptions.paneBackgroundColor = this._paneBackgroundColor(); return this._rendererOptions; } @@ -59,6 +58,10 @@ export class PriceAxisRendererOptionsProvider { return this._chartModel.options().layout.textColor; } + private _paneBackgroundColor(): string { + return this._chartModel.backgroundTopColor(); + } + private _fontSize(): number { return this._chartModel.options().layout.fontSize; } diff --git a/src/renderers/price-axis-view-renderer.ts b/src/renderers/price-axis-view-renderer.ts index 115dbc627d..60cf94e5e6 100644 --- a/src/renderers/price-axis-view-renderer.ts +++ b/src/renderers/price-axis-view-renderer.ts @@ -1,4 +1,4 @@ -import { drawScaled } from '../helpers/canvas-helpers'; +import { drawRoundRectWithInnerBorder, drawScaled } from '../helpers/canvas-helpers'; import { TextWidthCache } from '../model/text-width-cache'; @@ -9,6 +9,24 @@ import { PriceAxisViewRendererOptions, } from './iprice-axis-view-renderer'; +interface Geometry { + alignRight: boolean; + yTop: number; + yMid: number; + yBottom: number; + totalWidthScaled: number; + totalHeightScaled: number; + radius: number; + horzBorderScaled: number; + xOutside: number; + xInside: number; + xTick: number; + xText: number; + tickHeight: number; + rightScaled: number; + textMidCorrection: number; +} + export class PriceAxisViewRenderer implements IPriceAxisViewRenderer { private _data!: PriceAxisViewRendererData; private _commonData!: PriceAxisViewRendererCommonData; @@ -33,102 +51,67 @@ export class PriceAxisViewRenderer implements IPriceAxisViewRenderer { if (!this._data.visible) { return; } - ctx.font = rendererOptions.font; - const tickSize = (this._data.tickVisible || !this._data.moveTextToInvisibleTick) ? rendererOptions.tickLength : 0; - const horzBorder = rendererOptions.borderSize; - const paddingTop = rendererOptions.paddingTop; - const paddingBottom = rendererOptions.paddingBottom; - const paddingInner = rendererOptions.paddingInner; - const paddingOuter = rendererOptions.paddingOuter; - const text = this._data.text; - const textWidth = Math.ceil(textWidthCache.measureText(ctx, text)); - const baselineOffset = rendererOptions.baselineOffset; - const totalHeight = rendererOptions.fontSize + paddingTop + paddingBottom; - const halfHeigth = Math.ceil(totalHeight * 0.5); - const totalWidth = horzBorder + textWidth + paddingInner + paddingOuter + tickSize; + const geometry = this._calculateGeometry(ctx, rendererOptions, textWidthCache, width, align, pixelRatio); - let yMid = this._commonData.coordinate; - if (this._commonData.fixedCoordinate) { - yMid = this._commonData.fixedCoordinate; - } - - yMid = Math.round(yMid); - - const yTop = yMid - halfHeigth; - const yBottom = yTop + totalHeight; - - const alignRight = align === 'right'; - - const xInside = alignRight ? width : 0; - const rightScaled = Math.ceil(width * pixelRatio); - - let xOutside = xInside; - let xTick: number; - let xText: number; + const textColor = this._data.color || this._commonData.color; + const backgroundColor = (this._commonData.background); ctx.fillStyle = this._commonData.background; - ctx.lineWidth = 1; - ctx.lineCap = 'butt'; - - if (text) { - if (alignRight) { - // 2 1 - // - // 6 5 - // - // 3 4 - xOutside = xInside - totalWidth; - xTick = xInside - tickSize; - xText = xOutside + paddingOuter; - } else { - // 1 2 - // - // 6 5 - // - // 4 3 - xOutside = xInside + totalWidth; - xTick = xInside + tickSize; - xText = xInside + horzBorder + tickSize + paddingInner; - } - - const tickHeight = Math.max(1, Math.floor(pixelRatio)); - - const horzBorderScaled = Math.max(1, Math.floor(horzBorder * pixelRatio)); - const xInsideScaled = alignRight ? rightScaled : 0; - const yTopScaled = Math.round(yTop * pixelRatio); - const xOutsideScaled = Math.round(xOutside * pixelRatio); - const yMidScaled = Math.round(yMid * pixelRatio) - Math.floor(pixelRatio * 0.5); - const yBottomScaled = yMidScaled + tickHeight + (yMidScaled - yTopScaled); - const xTickScaled = Math.round(xTick * pixelRatio); - - ctx.save(); - - ctx.beginPath(); - ctx.moveTo(xInsideScaled, yTopScaled); - ctx.lineTo(xOutsideScaled, yTopScaled); - ctx.lineTo(xOutsideScaled, yBottomScaled); - ctx.lineTo(xInsideScaled, yBottomScaled); - ctx.fill(); + if (this._data.text) { + const drawLabelBody = (labelBackgroundColor: string, labelBorderColor?: string): void => { + if (geometry.alignRight) { + drawRoundRectWithInnerBorder( + ctx, + geometry.xOutside, + geometry.yTop, + geometry.totalWidthScaled, + geometry.totalHeightScaled, + labelBackgroundColor, + geometry.horzBorderScaled, + [geometry.radius, 0, 0, geometry.radius], + labelBorderColor + ); + } else { + drawRoundRectWithInnerBorder( + ctx, + geometry.xInside, + geometry.yTop, + geometry.totalWidthScaled, + geometry.totalHeightScaled, + labelBackgroundColor, + geometry.horzBorderScaled, + [0, geometry.radius, geometry.radius, 0], + labelBorderColor + ); + } + }; // draw border - ctx.fillStyle = this._data.borderColor; - ctx.fillRect(alignRight ? rightScaled - horzBorderScaled : 0, yTopScaled, horzBorderScaled, yBottomScaled - yTopScaled); - + // draw label background + drawLabelBody(backgroundColor, 'transparent'); + // draw tick if (this._data.tickVisible) { - ctx.fillStyle = this._commonData.color; - ctx.fillRect(xInsideScaled, yMidScaled, xTickScaled - xInsideScaled, tickHeight); + ctx.fillStyle = textColor; + ctx.fillRect(geometry.xInside, geometry.yMid, geometry.xTick - geometry.xInside, geometry.tickHeight); } + // draw label border above the tick + drawLabelBody('transparent', backgroundColor); - ctx.textAlign = 'left'; - ctx.fillStyle = this._commonData.color; + // draw separator + if (this._data.borderVisible) { + ctx.fillStyle = rendererOptions.paneBackgroundColor; + ctx.fillRect(geometry.alignRight ? geometry.rightScaled - geometry.horzBorderScaled : 0, geometry.yTop, geometry.horzBorderScaled, geometry.yBottom - geometry.yTop); + } + ctx.save(); + ctx.translate(geometry.xText, (geometry.yTop + geometry.yBottom) / 2 + geometry.textMidCorrection); drawScaled(ctx, pixelRatio, () => { - ctx.fillText(text, xText, yBottom - paddingBottom - baselineOffset); + ctx.fillStyle = textColor; + ctx.fillText(this._data.text, 0, 0); }); - ctx.restore(); } } @@ -140,4 +123,101 @@ export class PriceAxisViewRenderer implements IPriceAxisViewRenderer { return rendererOptions.fontSize + rendererOptions.paddingTop + rendererOptions.paddingBottom; } + + private _calculateGeometry( + ctx: CanvasRenderingContext2D, + rendererOptions: PriceAxisViewRendererOptions, + textWidthCache: TextWidthCache, + width: number, + align: 'left' | 'right', + pixelRatio: number + ): Geometry { + const tickSize = (this._data.tickVisible || !this._data.moveTextToInvisibleTick) ? rendererOptions.tickLength : 0; + const horzBorder = rendererOptions.borderSize; + const paddingTop = rendererOptions.paddingTop + this._commonData.additionalPaddingTop; + const paddingBottom = rendererOptions.paddingBottom + this._commonData.additionalPaddingBottom; + const paddingInner = rendererOptions.paddingInner; + const paddingOuter = rendererOptions.paddingOuter; + const text = this._data.text; + const actualTextHeight = rendererOptions.fontSize; + const textMidCorrection = textWidthCache.yMidCorrection(ctx, text) * pixelRatio; + + const textWidth = Math.ceil(textWidthCache.measureText(ctx, text)); + + const totalHeight = actualTextHeight + paddingTop + paddingBottom; + + const totalWidth = horzBorder + paddingInner + paddingOuter + textWidth + tickSize; + + const tickHeight = Math.max(1, Math.floor(pixelRatio)); + let totalHeightScaled = Math.round(totalHeight * pixelRatio); + if (totalHeightScaled % 2 !== tickHeight % 2) { + totalHeightScaled += 1; + } + const horzBorderScaled = this._data.separatorVisible ? Math.max(1, Math.floor(horzBorder * pixelRatio)) : 0; + const totalWidthScaled = Math.round(totalWidth * pixelRatio); + // tick overlaps scale border + const tickSizeScaled = Math.round(tickSize * pixelRatio); + const widthScaled = Math.ceil(width * pixelRatio); + const paddingInnerScaled = Math.ceil(paddingInner * pixelRatio); + + let yMid = this._commonData.coordinate; + if (this._commonData.fixedCoordinate) { + yMid = this._commonData.fixedCoordinate; + } + + yMid = Math.round(yMid * pixelRatio) - Math.floor(pixelRatio * 0.5); + const yTop = Math.floor(yMid + tickHeight / 2 - totalHeightScaled / 2); + const yBottom = yTop + totalHeightScaled; + + const alignRight = align === 'right'; + + const xInside = alignRight ? widthScaled - horzBorderScaled : horzBorderScaled; + const rightScaled = widthScaled; + + let xOutside = xInside; + let xTick: number; + let xText: number; + + const radius = 2 * pixelRatio; + + ctx.textAlign = alignRight ? 'right' : 'left'; + ctx.textBaseline = 'middle'; + + if (alignRight) { + // 2 1 + // + // 6 5 + // + // 3 4 + xOutside = xInside - totalWidthScaled; + xTick = xInside - tickSizeScaled; + xText = xInside - tickSizeScaled - paddingInnerScaled - 1; + } else { + // 1 2 + // + // 6 5 + // + // 4 3 + xOutside = xInside + totalWidthScaled; + xTick = xInside + tickSizeScaled; + xText = xInside + tickSizeScaled + paddingInnerScaled; + } + return { + alignRight, + yTop, + yMid, + yBottom, + totalWidthScaled, + totalHeightScaled, + radius, + horzBorderScaled, + xOutside, + xInside, + xTick, + xText, + tickHeight, + rightScaled, + textMidCorrection, + }; + } } diff --git a/src/renderers/time-axis-view-renderer.ts b/src/renderers/time-axis-view-renderer.ts index 2a2773f139..4c7d7dcfae 100644 --- a/src/renderers/time-axis-view-renderer.ts +++ b/src/renderers/time-axis-view-renderer.ts @@ -15,6 +15,8 @@ export interface TimeAxisViewRendererData { const optimizationReplacementRe = /[1-9]/g; +const radius = 2; + export class TimeAxisViewRenderer implements ITimeAxisViewRenderer { private _data: TimeAxisViewRendererData | null; @@ -58,9 +60,10 @@ export class TimeAxisViewRenderer implements ITimeAxisViewRenderer { const x2 = x1 + labelWidth; const y1 = 0; - const y2 = ( + const y2 = Math.ceil( y1 + rendererOptions.borderSize + + rendererOptions.tickLength + rendererOptions.paddingTop + rendererOptions.fontSize + rendererOptions.paddingBottom @@ -72,12 +75,20 @@ export class TimeAxisViewRenderer implements ITimeAxisViewRenderer { const y1scaled = Math.round(y1 * pixelRatio); const x2scaled = Math.round(x2 * pixelRatio); const y2scaled = Math.round(y2 * pixelRatio); - ctx.fillRect(x1scaled, y1scaled, x2scaled - x1scaled, y2scaled - y1scaled); + const radiusScaled = Math.round(radius * pixelRatio); + ctx.beginPath(); + ctx.moveTo(x1scaled, y1scaled); + ctx.lineTo(x1scaled, y2scaled - radiusScaled); + ctx.arcTo(x1scaled, y2scaled, x1scaled + radiusScaled, y2scaled, radiusScaled); + ctx.lineTo(x2scaled - radiusScaled, y2scaled); + ctx.arcTo(x2scaled, y2scaled, x2scaled, y2scaled - radiusScaled, radiusScaled); + ctx.lineTo(x2scaled, y1scaled); + ctx.fill(); if (this._data.tickVisible) { const tickX = Math.round(this._data.coordinate * pixelRatio); const tickTop = y1scaled; - const tickBottom = Math.round((tickTop + rendererOptions.borderSize + rendererOptions.tickLength) * pixelRatio); + const tickBottom = Math.round((tickTop + rendererOptions.tickLength) * pixelRatio); ctx.fillStyle = this._data.color; const tickWidth = Math.max(1, Math.floor(pixelRatio)); @@ -85,13 +96,21 @@ export class TimeAxisViewRenderer implements ITimeAxisViewRenderer { ctx.fillRect(tickX - tickOffset, tickTop, tickWidth, tickBottom - tickTop); } - const yText = y2 - rendererOptions.baselineOffset - rendererOptions.paddingBottom; + const yText = + y1 + + rendererOptions.borderSize + + rendererOptions.tickLength + + rendererOptions.paddingTop + + rendererOptions.fontSize / 2; + ctx.textAlign = 'left'; + ctx.textBaseline = 'middle'; ctx.fillStyle = this._data.color; - drawScaled(ctx, pixelRatio, () => { - ctx.fillText(ensureNotNull(this._data).text, x1 + horzMargin, yText); - }); + const textYCorrection = rendererOptions.widthCache.yMidCorrection(ctx, 'Apr0'); + + ctx.translate((x1 + horzMargin) * pixelRatio, (yText + textYCorrection) * pixelRatio); + drawScaled(ctx, pixelRatio, () => ctx.fillText(ensureNotNull(this._data).text, 0, 0)); ctx.restore(); } diff --git a/src/renderers/walk-line.ts b/src/renderers/walk-line.ts index 9caaee1a24..2d82b5f31f 100644 --- a/src/renderers/walk-line.ts +++ b/src/renderers/walk-line.ts @@ -3,41 +3,88 @@ import { SeriesItemsIndexesRange } from '../model/time-data'; import { LinePoint, LineType } from './draw-line'; -/** - * BEWARE: The method must be called after beginPath and before stroke/fill/closePath/etc - */ -export function walkLine( +// eslint-disable-next-line max-params, complexity +export function walkLine( ctx: CanvasRenderingContext2D, - points: readonly LinePoint[], + items: readonly TItem[], lineType: LineType, - visibleRange: SeriesItemsIndexesRange + visibleRange: SeriesItemsIndexesRange, + barWidth: number, + // the values returned by styleGetter are compared using the operator !==, + // so if styleGetter returns objects, then styleGetter should return the same object for equal styles + styleGetter: (ctx: CanvasRenderingContext2D, item: TItem) => TStyle, + finishStyledArea: (ctx: CanvasRenderingContext2D, style: TStyle, areaFirstItem: LinePoint, newAreaFirstItem: LinePoint) => void ): void { - if (points.length === 0) { + if (items.length === 0 || visibleRange.from >= items.length) { + return; + } + + const firstItem = items[visibleRange.from]; + let currentStyle = styleGetter(ctx, firstItem); + let currentStyleFirstItem = firstItem; + + if (visibleRange.to - visibleRange.from < 2) { + const halfBarWidth = barWidth / 2; + + ctx.beginPath(); + + const item1: LinePoint = { x: firstItem.x - halfBarWidth as Coordinate, y: firstItem.y }; + const item2: LinePoint = { x: firstItem.x + halfBarWidth as Coordinate, y: firstItem.y }; + + ctx.moveTo(item1.x, item1.y); + ctx.lineTo(item2.x, item2.y); + + finishStyledArea(ctx, currentStyle, item1, item2); + return; } - const x = points[visibleRange.from].x as number; - const y = points[visibleRange.from].y as number; - ctx.moveTo(x, y); + const changeStyle = (newStyle: TStyle, currentItem: TItem) => { + finishStyledArea(ctx, currentStyle, currentStyleFirstItem, currentItem); + + ctx.beginPath(); + currentStyle = newStyle; + currentStyleFirstItem = currentItem; + }; + + let currentItem = currentStyleFirstItem; + + ctx.beginPath(); + ctx.moveTo(firstItem.x, firstItem.y); for (let i = visibleRange.from + 1; i < visibleRange.to; ++i) { - const currItem = points[i]; + currentItem = items[i]; + const itemStyle = styleGetter(ctx, currentItem); switch (lineType) { case LineType.Simple: - ctx.lineTo(currItem.x, currItem.y); + ctx.lineTo(currentItem.x, currentItem.y); break; - case LineType.WithSteps: { - ctx.lineTo(currItem.x, points[i - 1].y); - ctx.lineTo(currItem.x, currItem.y); + case LineType.WithSteps: + ctx.lineTo(currentItem.x, items[i - 1].y); + + if (itemStyle !== currentStyle) { + changeStyle(itemStyle, currentItem); + ctx.lineTo(currentItem.x, items[i - 1].y); + } + + ctx.lineTo(currentItem.x, currentItem.y); break; - } case LineType.Curved: { - const [cp1, cp2] = getControlPoints(points, i - 1, i); - ctx.bezierCurveTo(cp1.x, cp1.y, cp2.x, cp2.y, currItem.x, currItem.y); + const [cp1, cp2] = getControlPoints(items, i - 1, i); + ctx.bezierCurveTo(cp1.x, cp1.y, cp2.x, cp2.y, currentItem.x, currentItem.y); break; } } + + if (lineType !== LineType.WithSteps && itemStyle !== currentStyle) { + changeStyle(itemStyle, currentItem); + ctx.moveTo(currentItem.x, currentItem.y); + } + } + + if (currentStyleFirstItem !== currentItem || currentStyleFirstItem === currentItem && lineType === LineType.WithSteps) { + finishStyledArea(ctx, currentStyle, currentStyleFirstItem, currentItem); } } diff --git a/src/typings/dom-not-standarted/index.d.ts b/src/typings/dom-not-standarted/index.d.ts index 79ad64fcdf..1c503aaaf0 100644 --- a/src/typings/dom-not-standarted/index.d.ts +++ b/src/typings/dom-not-standarted/index.d.ts @@ -7,6 +7,23 @@ interface UIEvent { sourceCapabilities?: InputDeviceCapabilities; } +/** + * Navigator userAgentData + * https://developer.mozilla.org/en-US/docs/Web/API/NavigatorUAData + * More reliable way of determining chromium browsers. + * Note: This is a partial type definition for the low entropy properties. + */ +interface UADataBrand { + brand: string; version: string; +} +interface Navigator { + userAgentData?: { + brands: UADataBrand[]; + platform: string; + mobile: boolean; + }; +} + interface Window { chrome: unknown; } diff --git a/src/views/pane/area-pane-view.ts b/src/views/pane/area-pane-view.ts index d4ecd41af4..ebe3d2b61d 100644 --- a/src/views/pane/area-pane-view.ts +++ b/src/views/pane/area-pane-view.ts @@ -2,16 +2,16 @@ import { BarPrice } from '../../model/bar'; import { ChartModel } from '../../model/chart-model'; import { Coordinate } from '../../model/coordinate'; import { Series } from '../../model/series'; +import { SeriesBarColorer } from '../../model/series-bar-colorer'; import { TimePointIndex } from '../../model/time-data'; -import { PaneRendererArea } from '../../renderers/area-renderer'; +import { AreaFillItem, PaneRendererArea } from '../../renderers/area-renderer'; import { CompositeRenderer } from '../../renderers/composite-renderer'; -import { IPaneRenderer } from '../../renderers/ipane-renderer'; -import { LineItem, PaneRendererLine } from '../../renderers/line-renderer'; +import { LineStrokeItem, PaneRendererLine } from '../../renderers/line-renderer'; import { LinePaneViewBase } from './line-pane-view-base'; -export class SeriesAreaPaneView extends LinePaneViewBase<'Area', LineItem> { - private readonly _renderer: CompositeRenderer = new CompositeRenderer(); +export class SeriesAreaPaneView extends LinePaneViewBase<'Area', AreaFillItem & LineStrokeItem, CompositeRenderer> { + protected readonly _renderer: CompositeRenderer = new CompositeRenderer(); private readonly _areaRenderer: PaneRendererArea = new PaneRendererArea(); private readonly _lineRenderer: PaneRendererLine = new PaneRendererLine(); @@ -20,23 +20,24 @@ export class SeriesAreaPaneView extends LinePaneViewBase<'Area', LineItem> { this._renderer.setRenderers([this._areaRenderer, this._lineRenderer]); } - public renderer(height: number, width: number): IPaneRenderer | null { - if (!this._series.visible()) { - return null; - } + protected _createRawItem(time: TimePointIndex, price: BarPrice, colorer: SeriesBarColorer<'Area'>): AreaFillItem & LineStrokeItem { + return { + ...this._createRawItemBase(time, price), + ...colorer.barStyle(time), + }; + } + protected _prepareRendererData(width: number, height: number): void { const areaStyleProperties = this._series.options(); - this._makeValid(); + const baseLevelCoordinate = (areaStyleProperties.invertFilledArea ? 0 : height) as Coordinate; this._areaRenderer.setData({ lineType: areaStyleProperties.lineType, items: this._items, lineStyle: areaStyleProperties.lineStyle, lineWidth: areaStyleProperties.lineWidth, - topColor: areaStyleProperties.topColor, - bottomColor: areaStyleProperties.bottomColor, - baseLevelCoordinate: height as Coordinate, + baseLevelCoordinate, bottom: height as Coordinate, visibleRange: this._itemsVisibleRange, barWidth: this._model.timeScale().barSpacing(), @@ -45,17 +46,10 @@ export class SeriesAreaPaneView extends LinePaneViewBase<'Area', LineItem> { this._lineRenderer.setData({ lineType: areaStyleProperties.lineType, items: this._items, - lineColor: areaStyleProperties.lineColor, lineStyle: areaStyleProperties.lineStyle, lineWidth: areaStyleProperties.lineWidth, visibleRange: this._itemsVisibleRange, barWidth: this._model.timeScale().barSpacing(), }); - - return this._renderer; - } - - protected _createRawItem(time: TimePointIndex, price: BarPrice): LineItem { - return this._createRawItemBase(time, price); } } diff --git a/src/views/pane/bars-pane-view-base.ts b/src/views/pane/bars-pane-view-base.ts index 72d9152aa1..07a8fc317a 100644 --- a/src/views/pane/bars-pane-view-base.ts +++ b/src/views/pane/bars-pane-view-base.ts @@ -11,10 +11,11 @@ import { SeriesPlotRow } from '../../model/series-data'; import { TimePointIndex } from '../../model/time-data'; import { TimeScale } from '../../model/time-scale'; import { BarCandlestickItemBase } from '../../renderers/bars-renderer'; +import { IPaneRenderer } from '../../renderers/ipane-renderer'; import { SeriesPaneViewBase } from './series-pane-view-base'; -export abstract class BarsPaneViewBase extends SeriesPaneViewBase { +export abstract class BarsPaneViewBase extends SeriesPaneViewBase { public constructor(series: Series, model: ChartModel) { super(series, model, false); } @@ -24,9 +25,9 @@ export abstract class BarsPaneViewBase): ItemType; - protected _createDefaultItem(time: TimePointIndex, bar: SeriesPlotRow, colorer: SeriesBarColorer): BarCandlestickItemBase { + protected _createDefaultItem(time: TimePointIndex, bar: SeriesPlotRow, colorer: SeriesBarColorer): BarCandlestickItemBase { return { time: time, open: bar.value[PlotRowValueIndex.Open] as BarPrice, diff --git a/src/views/pane/bars-pane-view.ts b/src/views/pane/bars-pane-view.ts index 9a4b793523..9938415186 100644 --- a/src/views/pane/bars-pane-view.ts +++ b/src/views/pane/bars-pane-view.ts @@ -4,46 +4,29 @@ import { TimePointIndex } from '../../model/time-data'; import { BarItem, PaneRendererBars, - PaneRendererBarsData, } from '../../renderers/bars-renderer'; -import { IPaneRenderer } from '../../renderers/ipane-renderer'; import { BarsPaneViewBase } from './bars-pane-view-base'; -export class SeriesBarsPaneView extends BarsPaneViewBase<'Bar', BarItem> { - private readonly _renderer: PaneRendererBars = new PaneRendererBars(); +export class SeriesBarsPaneView extends BarsPaneViewBase<'Bar', BarItem, PaneRendererBars> { + protected readonly _renderer: PaneRendererBars = new PaneRendererBars(); - public renderer(height: number, width: number): IPaneRenderer | null { - if (!this._series.visible()) { - return null; - } + protected _createRawItem(time: TimePointIndex, bar: SeriesPlotRow, colorer: SeriesBarColorer<'Bar'>): BarItem { + return { + ...this._createDefaultItem(time, bar, colorer), + ...colorer.barStyle(time), + }; + } + protected _prepareRendererData(): void { const barStyleProps = this._series.options(); - this._makeValid(); - const data: PaneRendererBarsData = { + this._renderer.setData({ bars: this._items, barSpacing: this._model.timeScale().barSpacing(), openVisible: barStyleProps.openVisible, thinBars: barStyleProps.thinBars, visibleRange: this._itemsVisibleRange, - }; - - this._renderer.setData(data); - - return this._renderer; - } - - protected _updateOptions(): void { - this._items.forEach((item: BarItem) => { - item.color = this._series.barColorer().barStyle(item.time).barColor; }); } - - protected _createRawItem(time: TimePointIndex, bar: SeriesPlotRow, colorer: SeriesBarColorer): BarItem { - return { - ...this._createDefaultItem(time, bar, colorer), - color: colorer.barStyle(time).barColor, - }; - } } diff --git a/src/views/pane/baseline-pane-view.ts b/src/views/pane/baseline-pane-view.ts index 5599ea6b0a..5fd0648693 100644 --- a/src/views/pane/baseline-pane-view.ts +++ b/src/views/pane/baseline-pane-view.ts @@ -2,49 +2,45 @@ import { BarPrice } from '../../model/bar'; import { ChartModel } from '../../model/chart-model'; import { Coordinate } from '../../model/coordinate'; import { Series } from '../../model/series'; +import { SeriesBarColorer } from '../../model/series-bar-colorer'; import { TimePointIndex } from '../../model/time-data'; -import { PaneRendererBaselineArea, PaneRendererBaselineLine } from '../../renderers/baseline-renderer'; +import { BaselineFillItem, PaneRendererBaselineArea } from '../../renderers/baseline-renderer-area'; +import { BaselineStrokeItem, PaneRendererBaselineLine } from '../../renderers/baseline-renderer-line'; import { CompositeRenderer } from '../../renderers/composite-renderer'; -import { IPaneRenderer } from '../../renderers/ipane-renderer'; -import { LineItem } from '../../renderers/line-renderer'; import { LinePaneViewBase } from './line-pane-view-base'; -export class SeriesBaselinePaneView extends LinePaneViewBase<'Baseline', LineItem> { +export class SeriesBaselinePaneView extends LinePaneViewBase<'Baseline', BaselineFillItem & BaselineStrokeItem, CompositeRenderer> { + protected readonly _renderer: CompositeRenderer = new CompositeRenderer(); private readonly _baselineAreaRenderer: PaneRendererBaselineArea = new PaneRendererBaselineArea(); private readonly _baselineLineRenderer: PaneRendererBaselineLine = new PaneRendererBaselineLine(); - private readonly _compositeRenderer: CompositeRenderer = new CompositeRenderer(); public constructor(series: Series<'Baseline'>, model: ChartModel) { super(series, model); - this._compositeRenderer.setRenderers([this._baselineAreaRenderer, this._baselineLineRenderer]); + this._renderer.setRenderers([this._baselineAreaRenderer, this._baselineLineRenderer]); } - public renderer(height: number, width: number): IPaneRenderer | null { - if (!this._series.visible()) { - return null; - } + protected _createRawItem(time: TimePointIndex, price: BarPrice, colorer: SeriesBarColorer<'Baseline'>): BaselineFillItem & BaselineStrokeItem { + return { + ...this._createRawItemBase(time, price), + ...colorer.barStyle(time), + }; + } + protected _prepareRendererData(width: number, height: number): void { const firstValue = this._series.firstValue(); if (firstValue === null) { - return null; + return; } const baselineProps = this._series.options(); - this._makeValid(); - const baseLevelCoordinate = this._series.priceScale().priceToCoordinate(baselineProps.baseValue.price, firstValue.value); const barWidth = this._model.timeScale().barSpacing(); this._baselineAreaRenderer.setData({ items: this._items, - topFillColor1: baselineProps.topFillColor1, - topFillColor2: baselineProps.topFillColor2, - bottomFillColor1: baselineProps.bottomFillColor1, - bottomFillColor2: baselineProps.bottomFillColor2, - lineWidth: baselineProps.lineWidth, lineStyle: baselineProps.lineStyle, lineType: baselineProps.lineType, @@ -59,9 +55,6 @@ export class SeriesBaselinePaneView extends LinePaneViewBase<'Baseline', LineIte this._baselineLineRenderer.setData({ items: this._items, - topColor: baselineProps.topLineColor, - bottomColor: baselineProps.bottomLineColor, - lineWidth: baselineProps.lineWidth, lineStyle: baselineProps.lineStyle, lineType: baselineProps.lineType, @@ -72,11 +65,5 @@ export class SeriesBaselinePaneView extends LinePaneViewBase<'Baseline', LineIte visibleRange: this._itemsVisibleRange, barWidth, }); - - return this._compositeRenderer; - } - - protected _createRawItem(time: TimePointIndex, price: BarPrice): LineItem { - return this._createRawItemBase(time, price); } } diff --git a/src/views/pane/candlesticks-pane-view.ts b/src/views/pane/candlesticks-pane-view.ts index bfb51f177f..6b4c2ce3e4 100644 --- a/src/views/pane/candlesticks-pane-view.ts +++ b/src/views/pane/candlesticks-pane-view.ts @@ -4,52 +4,29 @@ import { TimePointIndex } from '../../model/time-data'; import { CandlestickItem, PaneRendererCandlesticks, - PaneRendererCandlesticksData, } from '../../renderers/candlesticks-renderer'; -import { IPaneRenderer } from '../../renderers/ipane-renderer'; import { BarsPaneViewBase } from './bars-pane-view-base'; -export class SeriesCandlesticksPaneView extends BarsPaneViewBase<'Candlestick', CandlestickItem> { - private readonly _renderer: PaneRendererCandlesticks = new PaneRendererCandlesticks(); +export class SeriesCandlesticksPaneView extends BarsPaneViewBase<'Candlestick', CandlestickItem, PaneRendererCandlesticks> { + protected readonly _renderer: PaneRendererCandlesticks = new PaneRendererCandlesticks(); - public renderer(height: number, width: number): IPaneRenderer | null { - if (!this._series.visible()) { - return null; - } + protected _createRawItem(time: TimePointIndex, bar: SeriesPlotRow, colorer: SeriesBarColorer<'Candlestick'>): CandlestickItem { + return { + ...this._createDefaultItem(time, bar, colorer), + ...colorer.barStyle(time), + }; + } + protected _prepareRendererData(): void { const candlestickStyleProps = this._series.options(); - this._makeValid(); - const data: PaneRendererCandlesticksData = { + this._renderer.setData({ bars: this._items, barSpacing: this._model.timeScale().barSpacing(), wickVisible: candlestickStyleProps.wickVisible, borderVisible: candlestickStyleProps.borderVisible, visibleRange: this._itemsVisibleRange, - }; - - this._renderer.setData(data); - - return this._renderer; - } - - protected _updateOptions(): void { - this._items.forEach((item: CandlestickItem) => { - const style = this._series.barColorer().barStyle(item.time); - item.color = style.barColor; - item.wickColor = style.barWickColor; - item.borderColor = style.barBorderColor; }); } - - protected _createRawItem(time: TimePointIndex, bar: SeriesPlotRow, colorer: SeriesBarColorer): CandlestickItem { - const style = colorer.barStyle(time); - return { - ...this._createDefaultItem(time, bar, colorer), - color: style.barColor, - wickColor: style.barWickColor, - borderColor: style.barBorderColor, - }; - } } diff --git a/src/views/pane/crosshair-marks-pane-view.ts b/src/views/pane/crosshair-marks-pane-view.ts index 3400986813..dca240e587 100644 --- a/src/views/pane/crosshair-marks-pane-view.ts +++ b/src/views/pane/crosshair-marks-pane-view.ts @@ -25,6 +25,7 @@ function createEmptyMarkerData(): MarksRendererData { lineColor: '', backColor: '', radius: 0, + lineWidth: 0, visibleRange: null, }; } @@ -94,6 +95,7 @@ export class CrosshairMarksPaneView implements IUpdatablePaneView { const firstValue = ensureNotNull(s.firstValue()); data.lineColor = seriesData.backgroundColor; data.radius = seriesData.radius; + data.lineWidth = seriesData.borderWidth; data.items[0].price = seriesData.price; data.items[0].y = s.priceScale().priceToCoordinate(seriesData.price, firstValue.value); data.backColor = seriesData.borderColor ?? this._chartModel.backgroundColorAtYPercentFromTop(data.items[0].y / height); diff --git a/src/views/pane/histogram-pane-view.ts b/src/views/pane/histogram-pane-view.ts index 72f016311c..54d11417b5 100644 --- a/src/views/pane/histogram-pane-view.ts +++ b/src/views/pane/histogram-pane-view.ts @@ -1,108 +1,30 @@ import { ensureNotNull } from '../../helpers/assertions'; import { BarPrice } from '../../model/bar'; -import { ChartModel } from '../../model/chart-model'; -import { Coordinate } from '../../model/coordinate'; -import { PlotRowValueIndex } from '../../model/plot-data'; -import { PriceScale } from '../../model/price-scale'; -import { Series } from '../../model/series'; -import { TimedValue, TimePointIndex, visibleTimedValues } from '../../model/time-data'; -import { TimeScale } from '../../model/time-scale'; -import { CompositeRenderer } from '../../renderers/composite-renderer'; +import { SeriesBarColorer } from '../../model/series-bar-colorer'; +import { TimePointIndex } from '../../model/time-data'; import { HistogramItem, PaneRendererHistogram, PaneRendererHistogramData } from '../../renderers/histogram-renderer'; -import { IPaneRenderer } from '../../renderers/ipane-renderer'; -import { SeriesPaneViewBase } from './series-pane-view-base'; +import { LinePaneViewBase } from './line-pane-view-base'; -function createEmptyHistogramData(barSpacing: number): PaneRendererHistogramData { - return { - items: [], - barSpacing, - histogramBase: NaN, - visibleRange: null, - }; -} - -function createRawItem(time: TimePointIndex, price: BarPrice, color: string): HistogramItem { - return { - time: time, - price: price, - x: NaN as Coordinate, - y: NaN as Coordinate, - color, - }; -} - -export class SeriesHistogramPaneView extends SeriesPaneViewBase<'Histogram', TimedValue> { - private _compositeRenderer: CompositeRenderer = new CompositeRenderer(); - private _histogramData: PaneRendererHistogramData = createEmptyHistogramData(0); - private _renderer: PaneRendererHistogram; - - public constructor(series: Series<'Histogram'>, model: ChartModel) { - super(series, model, false); - this._renderer = new PaneRendererHistogram(); - } - - public renderer(height: number, width: number): IPaneRenderer | null { - if (!this._series.visible()) { - return null; - } - - this._makeValid(); - return this._compositeRenderer; - } - - protected _fillRawPoints(): void { - const barSpacing = this._model.timeScale().barSpacing(); +export class SeriesHistogramPaneView extends LinePaneViewBase<'Histogram', HistogramItem, PaneRendererHistogram> { + protected readonly _renderer: PaneRendererHistogram = new PaneRendererHistogram(); - this._histogramData = createEmptyHistogramData(barSpacing); - - let targetIndex = 0; - let itemIndex = 0; - - const defaultColor = this._series.options().color; - - for (const row of this._series.bars().rows()) { - const value = row.value[PlotRowValueIndex.Close] as BarPrice; - - const color = row.color !== undefined ? row.color : defaultColor; - const item = createRawItem(row.index, value, color); - targetIndex++; - if (targetIndex < this._histogramData.items.length) { - this._histogramData.items[targetIndex] = item; - } else { - this._histogramData.items.push(item); - } - this._items[itemIndex++] = { time: row.index, x: 0 as Coordinate }; - } - - this._renderer.setData(this._histogramData); - this._compositeRenderer.setRenderers([this._renderer]); + protected _createRawItem(time: TimePointIndex, price: BarPrice, colorer: SeriesBarColorer<'Histogram'>): HistogramItem { + return { + ...this._createRawItemBase(time, price), + ...colorer.barStyle(time), + }; } - protected _updateOptions(): void {} - - protected override _clearVisibleRange(): void { - super._clearVisibleRange(); - - this._histogramData.visibleRange = null; - } - - protected _convertToCoordinates(priceScale: PriceScale, timeScale: TimeScale, firstValue: number): void { - if (this._itemsVisibleRange === null) { - return; - } - - const barSpacing = timeScale.barSpacing(); - const visibleBars = ensureNotNull(timeScale.visibleStrictRange()); - const histogramBase = priceScale.priceToCoordinate(this._series.options().base, firstValue); + protected _prepareRendererData(): void { + const data: PaneRendererHistogramData = { + items: this._items, + barSpacing: this._model.timeScale().barSpacing(), + visibleRange: this._itemsVisibleRange, + histogramBase: this._series.priceScale().priceToCoordinate(this._series.options().base, ensureNotNull(this._series.firstValue()).value), + }; - timeScale.indexesToCoordinates(this._histogramData.items); - priceScale.pointsArrayToCoordinates(this._histogramData.items, firstValue); - this._histogramData.histogramBase = histogramBase; - this._histogramData.visibleRange = visibleTimedValues(this._histogramData.items, visibleBars, false); - this._histogramData.barSpacing = barSpacing; - // need this to update cache - this._renderer.setData(this._histogramData); + this._renderer.setData(data); } } diff --git a/src/views/pane/line-pane-view-base.ts b/src/views/pane/line-pane-view-base.ts index ea4007932b..f5d1312f51 100644 --- a/src/views/pane/line-pane-view-base.ts +++ b/src/views/pane/line-pane-view-base.ts @@ -10,11 +10,16 @@ import { SeriesBarColorer } from '../../model/series-bar-colorer'; import { SeriesPlotRow } from '../../model/series-data'; import { TimedValue, TimePointIndex } from '../../model/time-data'; import { TimeScale } from '../../model/time-scale'; +import { IPaneRenderer } from '../../renderers/ipane-renderer'; import { SeriesPaneViewBase } from './series-pane-view-base'; -export abstract class LinePaneViewBase extends SeriesPaneViewBase { - protected constructor(series: Series, model: ChartModel) { +export abstract class LinePaneViewBase< + TSeriesType extends 'Line' | 'Area' | 'Baseline' | 'Histogram', + ItemType extends PricedValue & TimedValue, + TRenderer extends IPaneRenderer +> extends SeriesPaneViewBase { + public constructor(series: Series, model: ChartModel) { super(series, model, true); } @@ -23,7 +28,7 @@ export abstract class LinePaneViewBase): ItemType; protected _createRawItemBase(time: TimePointIndex, price: BarPrice): PricedValue & TimedValue { return { @@ -34,8 +39,6 @@ export abstract class LinePaneViewBase) => { diff --git a/src/views/pane/line-pane-view.ts b/src/views/pane/line-pane-view.ts index 7036ee7792..6a1d730025 100644 --- a/src/views/pane/line-pane-view.ts +++ b/src/views/pane/line-pane-view.ts @@ -1,32 +1,25 @@ 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'; +import { LineStrokeItem, PaneRendererLine, PaneRendererLineData } from '../../renderers/line-renderer'; import { LinePaneViewBase } from './line-pane-view-base'; -export class SeriesLinePaneView extends LinePaneViewBase<'Line', LineItem> { - private readonly _lineRenderer: PaneRendererLine = new PaneRendererLine(); +export class SeriesLinePaneView extends LinePaneViewBase<'Line', LineStrokeItem, PaneRendererLine> { + protected readonly _renderer: PaneRendererLine = new PaneRendererLine(); - // eslint-disable-next-line no-useless-constructor - public constructor(series: Series<'Line'>, model: ChartModel) { - super(series, model); + protected _createRawItem(time: TimePointIndex, price: BarPrice, colorer: SeriesBarColorer<'Line'>): LineStrokeItem { + return { + ...this._createRawItemBase(time, price), + ...colorer.barStyle(time), + }; } - public renderer(height: number, width: number): IPaneRenderer | null { - if (!this._series.visible()) { - return null; - } - + protected _prepareRendererData(): void { const lineStyleProps = this._series.options(); - this._makeValid(); const data: PaneRendererLineData = { items: this._items, - lineColor: lineStyleProps.color, lineStyle: lineStyleProps.lineStyle, lineType: lineStyleProps.lineType, lineWidth: lineStyleProps.lineWidth, @@ -34,20 +27,6 @@ export class SeriesLinePaneView extends LinePaneViewBase<'Line', LineItem> { barWidth: this._model.timeScale().barSpacing(), }; - this._lineRenderer.setData(data); - - return this._lineRenderer; - } - - protected override _updateOptions(): void { - this._items.forEach((item: LineItem) => { - item.color = this._series.barColorer().barStyle(item.time).barColor; - }); - } - - 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; + this._renderer.setData(data); } } diff --git a/src/views/pane/series-pane-view-base.ts b/src/views/pane/series-pane-view-base.ts index 60c176690b..6d8a35cfe6 100644 --- a/src/views/pane/series-pane-view-base.ts +++ b/src/views/pane/series-pane-view-base.ts @@ -8,7 +8,7 @@ import { IPaneRenderer } from '../../renderers/ipane-renderer'; import { IUpdatablePaneView, UpdateType } from './iupdatable-pane-view'; -export abstract class SeriesPaneViewBase implements IUpdatablePaneView { +export abstract class SeriesPaneViewBase implements IUpdatablePaneView { protected readonly _series: Series; protected readonly _model: ChartModel; protected _invalidated: boolean = true; @@ -16,6 +16,7 @@ export abstract class SeriesPaneViewBase, model: ChartModel, extendedVisibleRange: boolean) { @@ -34,28 +35,24 @@ export abstract class SeriesPaneViewBase ({ + ...item, + ...this._series.barColorer().barStyle(item.time), + })); + } protected abstract _convertToCoordinates(priceScale: PriceScale, timeScale: TimeScale, firstValue: number): void; @@ -63,7 +60,26 @@ export abstract class SeriesPaneViewBase ({ - ...item, - color: colors[index % colors.length], - })); -} - -// eslint-disable-next-line no-unused-vars -function runTestCase(container) { - const chart = LightweightCharts.createChart(container, { - leftPriceScale: { - visible: true, - mode: LightweightCharts.PriceScaleMode.Logarithmic, - }, - rightPriceScale: { - visible: true, - mode: LightweightCharts.PriceScaleMode.Percentage, - }, - timeScale: { - timeVisible: true, - }, - watermark: { - visible: true, - color: 'red', - text: 'Watermark', - fontSize: 24, - fontFamily: 'Roboto', - fontStyle: 'italic', - }, - kineticScroll: { - mouse: true, - }, - layout: { - background: { - type: LightweightCharts.ColorType.VerticalGradient, - topColor: '#FFFFFF', - bottomColor: '#AAFFAA', - }, - }, - }); - - const data = generateLineData(); - const areaSeries = chart.addAreaSeries({ - priceFormat: { - type: 'custom', - minMove: 0.02, - formatter: price => '$' + price.toFixed(2), - }, - }); - areaSeries.setData(data); - - const seriesToRemove = chart.addAreaSeries({ - priceScaleId: 'overlay-id', - priceFormat: { - type: 'volume', - }, - lastPriceAnimation: LightweightCharts.LastPriceAnimationMode.Continuous, - }); - seriesToRemove.setData(generateLineData()); - - const candlestickSeries = chart.addCandlestickSeries({ priceScaleId: 'left' }); - candlestickSeries.setData(generateBars()); - - const barSeries = chart.addBarSeries({ - title: 'Initial title', - priceFormat: { - type: 'percent', - }, - }); - barSeries.setData(generateBars()); - barSeries.applyOptions({ priceScaleId: 'left' }); - - const histogramSeries = chart.addHistogramSeries({ - color: '#ff0000', - autoscaleInfoProvider: original => original(), - }); - - histogramSeries.setData(generateHistogramData()); - - const lineSeries = chart.addLineSeries({ - lineWidth: 1, - color: '#ff0000', - priceFormat: { - type: 'volume', - }, - lastPriceAnimation: LightweightCharts.LastPriceAnimationMode.OnDataUpdate, - }); - - lineSeries.setData(generateLineData()); - - const baselineSeries = chart.addBaselineSeries(); - baselineSeries.setData(generateLineData()); - - areaSeries.createPriceLine({ - price: 10, - color: 'red', - lineWidth: 1, - lineStyle: LightweightCharts.LineStyle.Solid, - }); - - areaSeries.createPriceLine({ - price: 20, - color: '#00FF00', - lineWidth: 2, - lineStyle: LightweightCharts.LineStyle.Dotted, - }); - - areaSeries.createPriceLine({ - price: 30, - color: 'rgb(0,0,255)', - lineWidth: 3, - lineStyle: LightweightCharts.LineStyle.Dashed, - }); - - const priceLineToRemove = areaSeries.createPriceLine({ - price: 40, - color: 'rgba(255,0,0,0.5)', - lineWidth: 4, - lineStyle: LightweightCharts.LineStyle.LargeDashed, - }); - - const priceLine1 = areaSeries.createPriceLine({ - price: 50, - color: '#f0f', - lineWidth: 4, - lineStyle: LightweightCharts.LineStyle.SparseDotted, - }); - - areaSeries.setMarkers([ - { time: data[data.length - 7].time, position: 'belowBar', color: 'rgb(255, 0, 0)', shape: 'arrowUp', text: 'test' }, - { time: data[data.length - 5].time, position: 'aboveBar', color: 'rgba(255, 255, 0, 1)', shape: 'arrowDown', text: 'test' }, - { time: data[data.length - 3].time, position: 'inBar', color: '#f0f', shape: 'circle', text: 'test' }, - { time: data[data.length - 1].time, position: 'belowBar', color: '#fff00a', shape: 'square', text: 'test', size: 2 }, - ]); - - // apply overlay price scales while create series - // time formatter - - chart.timeScale().fitContent(); - - chart.timeScale().subscribeVisibleTimeRangeChange(console.log); - chart.timeScale().subscribeVisibleLogicalRangeChange(console.log); - chart.subscribeCrosshairMove(console.log); - chart.subscribeClick(console.log); - - return new Promise(resolve => { - setTimeout(() => { - chart.timeScale().scrollToRealTime(); - - chart.priceScale('overlay-id').applyOptions({}); - - chart.removeSeries(seriesToRemove); - areaSeries.removePriceLine(priceLineToRemove); - - chart.takeScreenshot(); - - chart.resize(700, 700); - - chart.applyOptions({ - leftPriceScale: { - mode: LightweightCharts.PriceScaleMode.IndexedTo100, - }, - rightPriceScale: { - mode: LightweightCharts.PriceScaleMode.Normal, - invertScale: true, - alignLabels: false, - }, - localization: { - dateFormat: 'yyyy MM dd', - }, - }); - - chart.priceScale('left').width(); - - // move series to left price scale - lineSeries.applyOptions({ priceScaleId: 'left' }); - - // set new series data - const newData = generateBars(520, 1); - barSeries.setData(newData); - barSeries.update({ - ...newData[newData.length - 1], - close: newData[newData.length - 1].close - 10, - }); - barSeries.update({ - ...newData[newData.length - 1], - time: newData[newData.length - 1].time + 3600, - }); - - chart.timeScale().getVisibleRange(); - chart.timeScale().setVisibleRange({ - from: newData[0].time, - to: newData[newData.length - 1].time, - }); - - barSeries.barsInLogicalRange(chart.timeScale().getVisibleLogicalRange()); - chart.timeScale().applyOptions({ fixLeftEdge: true }); - - priceLine1.applyOptions({}); - - setTimeout(() => { - chart.timeScale().unsubscribeVisibleTimeRangeChange(console.log); - chart.timeScale().unsubscribeVisibleLogicalRangeChange(console.log); - chart.unsubscribeCrosshairMove(console.log); - chart.unsubscribeClick(console.log); - - resolve(() => chart.remove()); - }, 500); - }, 500); - }); -} diff --git a/tests/e2e/coverage/coverage-test-cases.ts b/tests/e2e/coverage/coverage-test-cases.ts new file mode 100644 index 0000000000..35e01a6f61 --- /dev/null +++ b/tests/e2e/coverage/coverage-test-cases.ts @@ -0,0 +1,276 @@ +/// + +import * as fs from 'fs'; +import * as path from 'path'; + +import { expect } from 'chai'; +import { describe, it } from 'mocha'; +import puppeteer, { + Browser, + HTTPResponse, + launch as launchPuppeteer, + Page, +} from 'puppeteer'; + +import { TestCase } from '../helpers/get-test-cases'; +import { Interaction, performInteractions } from '../helpers/perform-interactions'; + +import { expectedCoverage, threshold } from './coverage-config'; +import { getTestCases } from './helpers/get-coverage-test-cases'; + +const dummyContent = fs.readFileSync( + path.join(__dirname, 'helpers', 'test-page-dummy.html'), + { encoding: 'utf-8' } +); + +function generatePageContent( + standaloneBundlePath: string, + testCaseCode: string +): string { + return dummyContent + .replace('PATH_TO_STANDALONE_MODULE', standaloneBundlePath) + .replace('TEST_CASE_SCRIPT', testCaseCode); +} + +const testStandalonePathEnvKey = 'TEST_STANDALONE_PATH'; + +const testStandalonePath: string = process.env[testStandalonePathEnvKey] || ''; + +interface InternalWindow { + interactions: Interaction[]; + finishedSetup: Promise<() => void>; + afterInteractions: () => void; +} + +function rmRf(dir: string): void { + if (!fs.existsSync(dir)) { + return; + } + + fs.readdirSync(dir).forEach((file: string) => { + const filePath = path.join(dir, file); + if (fs.lstatSync(filePath).isDirectory()) { + rmRf(filePath); + } else { + fs.unlinkSync(filePath); + } + }); + + fs.rmdirSync(dir); +} + +function generateAndSaveCoverageFile(coveredJs: string): void { + // Create output directory + const outDir = path.resolve(process.env.CMP_OUT_DIR || path.join(__dirname, '.gendata')); + rmRf(outDir); + fs.mkdirSync(outDir, { recursive: true }); + + try { + const filePath = path.join(outDir, 'covered.js'); + fs.writeFileSync(filePath, coveredJs); + console.info('\nGenerated `covered.js` file for the coverage test.\n'); + console.info(filePath); + } catch (error: unknown) { + console.warn('Unable to save `covered.js` file for the coverage test.'); + console.error(error); + } +} + +async function getCoverageResult(page: Page): Promise { + const coverageEntries = await page.coverage.stopJSCoverage(); + const getFileNameFromUrl = (url: string): string => url.split('/').at(-1) ?? ''; + + for (const entry of coverageEntries) { + const fileName = getFileNameFromUrl(entry.url); + if (fileName === 'test.js') { return entry; } + } + return null; +} + +type CoverageTestResults = Record; +interface Range { + start: number; + end: number; +} + +interface CoverageResult { + usedBytes: number; + totalBytes: number; + coverageFile: string; +} + +function mergeRanges(ranges: Range[]): Range[] { + ranges.sort((a: Range, b: Range) => { + if (a.start === b.start) { + return a.end - b.end; + } + return a.start - b.start; + }); + const merged: Range[] = []; + for (const range of ranges) { + const last = merged.at(-1); + if (!last || last.end < range.start) { + merged.push(range); + } else { + last.end = Math.max(last.end, range.end); + } + } + return merged; +} + +function consolidateCoverageResults(testResults: CoverageTestResults): CoverageResult { + const coveredRanges: Range[] = []; + let testScriptCode = ''; + for (const [, coverageResult] of Object.entries(testResults)) { + coveredRanges.push(...coverageResult.ranges); + if (!testScriptCode) { + // Every test is against the same library file, + // therefore this will be the same for each coverageResult + testScriptCode = coverageResult.text; + } + } + const totalBytes = testScriptCode.length; + const mergedRanges = mergeRanges(coveredRanges); + let usedBytes = 0; + let coverageFile = ''; + for (const range of mergedRanges) { + usedBytes += (range.end - range.start); + coverageFile += testScriptCode.slice(range.start, range.end) + '\n'; + } + return { + usedBytes, + totalBytes, + coverageFile, + }; +} + +describe('Coverage tests', (): void => { + const puppeteerOptions: Parameters[0] = {}; + if (process.env.NO_SANDBOX) { + puppeteerOptions.args = ['--no-sandbox', '--disable-setuid-sandbox']; + } + + let browser: Browser; + + before(async () => { + expect( + testStandalonePath, + `path to test standalone module must be passed via ${testStandalonePathEnvKey} env var` + ).to.have.length.greaterThan(0); + + // note that we cannot use launchPuppeteer here as soon it wrong typing in puppeteer + // see https://github.com/puppeteer/puppeteer/issues/7529 + const browserPromise = puppeteer.launch(puppeteerOptions); + browser = await browserPromise; + }); + + let testCaseCount = 0; + const coverageResults: CoverageTestResults = {}; + + const runTestCase = (testCase: TestCase) => { + testCaseCount += 1; + it(testCase.name, async () => { + const pageContent = generatePageContent( + testStandalonePath, + testCase.caseContent + ); + + const page = await browser.newPage(); + await page.coverage.startJSCoverage(); + await page.setViewport({ width: 600, height: 600 }); + + const errors: string[] = []; + page.on('pageerror', (error: Error) => { + errors.push(error.message); + }); + + page.on('response', (response: HTTPResponse) => { + if (!response.ok()) { + errors.push( + `Network error: ${response.url()} status=${response.status()}` + ); + } + }); + + await page.setContent(pageContent, { waitUntil: 'load' }); + + await page.evaluate(() => { + return (window as unknown as InternalWindow).finishedSetup; + }); + + const interactionsToPerform = await page.evaluate(() => { + return (window as unknown as InternalWindow).interactions; + }); + + await performInteractions(page, interactionsToPerform); + + await page.evaluate(() => { + return new Promise((resolve: () => void) => { + (window as unknown as InternalWindow).afterInteractions(); + window.requestAnimationFrame(() => { + setTimeout(resolve, 50); + }); + }); + }); + + if (errors.length !== 0) { + throw new Error(`Page has errors:\n${errors.join('\n')}`); + } + + expect(errors.length).to.be.equal( + 0, + 'There should not be any errors thrown within the test page.' + ); + + const result = await getCoverageResult(page); + expect(result).not.to.be.equal(null); + if (result) { + coverageResults[testCase.name] = result; + } + }); + }; + + const testCaseGroups = getTestCases(); + + for (const groupName of Object.keys(testCaseGroups)) { + if (groupName.length === 0) { + for (const testCase of testCaseGroups[groupName]) { + runTestCase(testCase); + } + } else { + describe(groupName, () => { + for (const testCase of testCaseGroups[groupName]) { + runTestCase(testCase); + } + }); + } + } + + it('number of test cases', () => { + // we need to have at least 1 test to check it + expect(testCaseCount).to.be.greaterThan( + 0, + 'there should be at least 1 test case' + ); + }); + + after(async () => { + await browser.close(); + + const consolidatedResult = consolidateCoverageResults(coverageResults); + expect(consolidatedResult.usedBytes).to.be.lessThanOrEqual(consolidatedResult.totalBytes, 'Used bytes should be less than or equal to Total bytes.'); + expect(consolidatedResult.usedBytes).to.be.greaterThan(0, 'Used bytes should be more than zero.'); + + if (process.env.GENERATE_COVERAGE_FILE === 'true') { + generateAndSaveCoverageFile(consolidatedResult.coverageFile); + } + + const currentCoverage = parseFloat(((consolidatedResult.usedBytes / consolidatedResult.totalBytes) * 100).toFixed(2)); + expect(currentCoverage).to.be.closeTo(expectedCoverage, threshold, `Please either update config to pass the test or improve coverage`); + console.log(`Current coverage is ${currentCoverage.toFixed(2)}% (${formatChange(currentCoverage - expectedCoverage)}%)`); + }); +}); + +function formatChange(change: number): string { + return change < 0 ? change.toFixed(1) : `+${change.toFixed(1)}`; +} diff --git a/tests/e2e/coverage/coverage.spec.ts b/tests/e2e/coverage/coverage.spec.ts deleted file mode 100644 index 22998ed042..0000000000 --- a/tests/e2e/coverage/coverage.spec.ts +++ /dev/null @@ -1,339 +0,0 @@ -/// - -import * as fs from 'fs'; -import * as path from 'path'; - -import { expect } from 'chai'; -import { describe, it } from 'mocha'; -import puppeteer, { - BoundingBox, - Browser, - ConsoleMessage, - ElementHandle, - HTTPResponse, - launch as launchPuppeteer, - Page, -} from 'puppeteer'; - -import { expectedCoverage, threshold } from './coverage-config'; - -const coverageScript = fs.readFileSync(path.join(__dirname, 'coverage-script.js'), { encoding: 'utf-8' }); - -const testStandalonePathEnvKey = 'TEST_STANDALONE_PATH'; - -const testStandalonePath: string = process.env[testStandalonePathEnvKey] || ''; - -interface MouseWheelEventOptions { - type: 'mouseWheel'; - x: number; - y: number; - deltaX: number; - deltaY: number; -} - -interface InternalPuppeteerClient { - // see https://github.com/ChromeDevTools/devtools-protocol/blob/20413fc82dea0d45a598715970293b4787296673/json/browser_protocol.json#L7822-L7898 - // see https://github.com/puppeteer/puppeteer/issues/4119 - send(event: 'Input.dispatchMouseEvent', options: MouseWheelEventOptions): Promise; -} - -async function doMouseScrolls(element: ElementHandle): Promise { - // eslint-disable-next-line @typescript-eslint/no-explicit-any,@typescript-eslint/no-unsafe-member-access - const client: InternalPuppeteerClient = (element as any)._client; - - await client.send('Input.dispatchMouseEvent', { - type: 'mouseWheel', - x: 0, - y: 0, - deltaX: 10.0, - deltaY: 0, - }); - - await client.send('Input.dispatchMouseEvent', { - type: 'mouseWheel', - x: 0, - y: 0, - deltaX: 0, - deltaY: 10.0, - }); - - await client.send('Input.dispatchMouseEvent', { - type: 'mouseWheel', - x: 0, - y: 0, - deltaX: -10.0, - deltaY: 0, - }); - - await client.send('Input.dispatchMouseEvent', { - type: 'mouseWheel', - x: 0, - y: 0, - deltaX: 0, - deltaY: -10.0, - }); - - await client.send('Input.dispatchMouseEvent', { - type: 'mouseWheel', - x: 0, - y: 0, - deltaX: 10.0, - deltaY: 10.0, - }); - - await client.send('Input.dispatchMouseEvent', { - type: 'mouseWheel', - x: 0, - y: 0, - deltaX: -10.0, - deltaY: -10.0, - }); -} - -async function doZoomInZoomOut(page: Page): Promise { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const prevViewport = page.viewport()!; - await page.setViewport({ - ...prevViewport, - deviceScaleFactor: 2, - }); - - await page.setViewport(prevViewport); -} - -async function doVerticalDrag(page: Page, element: ElementHandle): Promise { - const elBox = await element.boundingBox() as BoundingBox; - - const elMiddleX = elBox.x + elBox.width / 2; - const elMiddleY = elBox.y + elBox.height / 2; - - // move mouse to the middle of element - await page.mouse.move(elMiddleX, elMiddleY); - - await page.mouse.down({ button: 'left' }); - await page.mouse.move(elMiddleX, elMiddleY - 20); - await page.mouse.move(elMiddleX, elMiddleY + 40); - await page.mouse.up({ button: 'left' }); -} - -async function doHorizontalDrag(page: Page, element: ElementHandle): Promise { - const elBox = await element.boundingBox() as BoundingBox; - - const elMiddleX = elBox.x + elBox.width / 2; - const elMiddleY = elBox.y + elBox.height / 2; - - // move mouse to the middle of element - await page.mouse.move(elMiddleX, elMiddleY); - - await page.mouse.down({ button: 'left' }); - await page.mouse.move(elMiddleX - 20, elMiddleY); - await page.mouse.move(elMiddleX + 40, elMiddleY); - await page.mouse.up({ button: 'left' }); -} - -async function doKineticAnimation(page: Page, element: ElementHandle): Promise { - const elBox = await element.boundingBox() as BoundingBox; - - const elMiddleX = elBox.x + elBox.width / 2; - const elMiddleY = elBox.y + elBox.height / 2; - - // move mouse to the middle of element - await page.mouse.move(elMiddleX, elMiddleY); - - await page.mouse.down({ button: 'left' }); - await page.waitForTimeout(50); - await page.mouse.move(elMiddleX - 40, elMiddleY); - await page.mouse.move(elMiddleX - 55, elMiddleY); - await page.mouse.move(elMiddleX - 105, elMiddleY); - await page.mouse.move(elMiddleX - 155, elMiddleY); - await page.mouse.move(elMiddleX - 205, elMiddleY); - await page.mouse.move(elMiddleX - 255, elMiddleY); - await page.mouse.up({ button: 'left' }); - - await page.waitForTimeout(200); - - // stop animation - await page.mouse.down({ button: 'left' }); - await page.mouse.up({ button: 'left' }); -} - -async function doUserInteractions(page: Page): Promise { - const chartContainer = await page.$('#container') as ElementHandle; - const chartBox = await chartContainer.boundingBox() as BoundingBox; - - // move cursor to the middle of the chart - await page.mouse.move(chartBox.width / 2, chartBox.height / 2); - - const leftPriceAxis = (await chartContainer.$$('tr:nth-of-type(1) td:nth-of-type(1) div canvas'))[0]; - const paneWidget = (await chartContainer.$$('tr:nth-of-type(1) td:nth-of-type(2) div canvas'))[0]; - const rightPriceAxis = (await chartContainer.$$('tr:nth-of-type(1) td:nth-of-type(3) div canvas'))[0]; - const timeAxis = (await chartContainer.$$('tr:nth-of-type(2) td:nth-of-type(2) div canvas'))[0]; - - // mouse scroll - await doMouseScrolls(chartContainer); - - // outside click - await page.mouse.click(chartBox.x + chartBox.width + 20, chartBox.y + chartBox.height + 50, { button: 'left' }); - - // change viewport zoom - await doZoomInZoomOut(page); - - // drag price scale - await doVerticalDrag(page, leftPriceAxis); - await doVerticalDrag(page, rightPriceAxis); - - // drag time scale - await doHorizontalDrag(page, timeAxis); - - // drag pane - await doVerticalDrag(page, paneWidget); - await doVerticalDrag(page, paneWidget); - - // clicks on scales - await leftPriceAxis.click({ button: 'left' }); - await leftPriceAxis.click({ button: 'left', clickCount: 2 }); - - await rightPriceAxis.click({ button: 'left' }); - await rightPriceAxis.click({ button: 'left', clickCount: 2 }); - - await timeAxis.click({ button: 'left' }); - await timeAxis.click({ button: 'left', clickCount: 2 }); - - await doKineticAnimation(page, timeAxis); -} - -interface CoverageResult { - usedBytes: number; - totalBytes: number; -} - -interface InternalWindow { - finishTestCasePromise: Promise<() => void>; -} - -async function getCoverageResult(page: Page): Promise> { - const coverageEntries = await page.coverage.stopJSCoverage(); - - const result = new Map(); - - for (const entry of coverageEntries) { - let entryRes = result.get(entry.url); - if (entryRes === undefined) { - entryRes = { - totalBytes: 0, - usedBytes: 0, - }; - - result.set(entry.url, entryRes); - } - - entryRes.totalBytes += entry.text.length; - - for (const range of entry.ranges) { - entryRes.usedBytes += range.end - range.start; - } - - result.set(entry.url, entryRes); - } - - return result; -} - -describe('Coverage tests', () => { - const puppeteerOptions: Parameters[0] = {}; - if (process.env.NO_SANDBOX) { - puppeteerOptions.args = ['--no-sandbox', '--disable-setuid-sandbox']; - } - - let browser: Browser; - - before(async () => { - expect(testStandalonePath, `path to test standalone module must be passed via ${testStandalonePathEnvKey} env var`) - .to.have.length.greaterThan(0); - - // note that we cannot use launchPuppeteer here as soon it wrong typing in puppeteer - // see https://github.com/puppeteer/puppeteer/issues/7529 - const browserPromise = puppeteer.launch(puppeteerOptions); - browser = await browserPromise; - }); - - async function runTest(onError: (errorMsg: string) => void): Promise { - const page = await browser.newPage(); - await page.coverage.startJSCoverage(); - - page.on('pageerror', (error: Error) => { - onError(`Page error: ${error.message}`); - }); - - page.on('console', (message: ConsoleMessage) => { - const type = message.type(); - if (type === 'error' || type === 'assert') { - onError(`Console ${type}: ${message.text()}`); - } - }); - - page.on('response', (response: HTTPResponse) => { - if (!response.ok()) { - onError(`Network error: ${response.url()} status=${response.status()}`); - } - }); - - await page.setContent(` - - - - - - Test case page - - - -
- - - - - - - - `); - - // first, wait until test case is ready - await page.evaluate(() => { - return (window as unknown as InternalWindow).finishTestCasePromise; - }); - - // now let's do some user's interactions - await doUserInteractions(page); - - // finish test case - await page.evaluate(() => { - return (window as unknown as InternalWindow).finishTestCasePromise.then((finishTestCase: () => void) => finishTestCase()); - }); - - const result = await getCoverageResult(page); - const libraryRes = result.get(testStandalonePath) as CoverageResult; - expect(libraryRes).not.to.be.equal(undefined); - - const currentCoverage = parseFloat((libraryRes.usedBytes / libraryRes.totalBytes * 100).toFixed(1)); - expect(currentCoverage).to.be.closeTo(expectedCoverage, threshold, `Please either update config to pass the test or improve coverage`); - - console.log(`Current coverage is ${currentCoverage.toFixed(1)}% (${formatChange(currentCoverage - expectedCoverage)}%)`); - } - - it(`should have coverage around ${expectedCoverage.toFixed(1)}% (±${threshold.toFixed(1)}%)`, async () => { - return new Promise((resolve: () => void, reject: () => void) => { - runTest(reject).then(resolve).catch(reject); - }); - }); - - after(async () => { - await browser.close(); - }); -}); - -function formatChange(change: number): string { - return change < 0 ? change.toFixed(1) : `+${change.toFixed(1)}`; -} diff --git a/tests/e2e/coverage/helpers/get-coverage-test-cases.ts b/tests/e2e/coverage/helpers/get-coverage-test-cases.ts new file mode 100644 index 0000000000..ec28ed117f --- /dev/null +++ b/tests/e2e/coverage/helpers/get-coverage-test-cases.ts @@ -0,0 +1,11 @@ +/// + +import * as path from 'path'; + +import { getTestCases as getTestCasesImpl, TestCase } from '../../helpers/get-test-cases'; + +const testCasesDir = path.join(__dirname, '..', 'test-cases'); + +export function getTestCases(): Record { + return getTestCasesImpl(testCasesDir); +} diff --git a/tests/e2e/coverage/helpers/test-page-dummy.html b/tests/e2e/coverage/helpers/test-page-dummy.html new file mode 100644 index 0000000000..fa99ddb1a6 --- /dev/null +++ b/tests/e2e/coverage/helpers/test-page-dummy.html @@ -0,0 +1,76 @@ + + + + + + Test case page + + + +
+ + + + + + + + diff --git a/tests/e2e/coverage/runner.js b/tests/e2e/coverage/runner.js index 0d17d63f8e..7fe8e45496 100644 --- a/tests/e2e/coverage/runner.js +++ b/tests/e2e/coverage/runner.js @@ -53,7 +53,7 @@ function runMocha(closeServer) { } mocha.diff(mochaConfig.diff); - mocha.addFile(path.resolve(__dirname, './coverage.spec.ts')); + mocha.addFile(path.resolve(__dirname, './coverage-test-cases.ts')); mocha.run(failures => { if (closeServer !== null) { diff --git a/tests/e2e/coverage/test-cases/.eslintrc.js b/tests/e2e/coverage/test-cases/.eslintrc.js new file mode 100644 index 0000000000..9537067fab --- /dev/null +++ b/tests/e2e/coverage/test-cases/.eslintrc.js @@ -0,0 +1,17 @@ +/* eslint-env node */ + +module.exports = { + env: { + browser: true, + node: false, + }, + rules: { + 'no-unused-vars': ['error', { varsIgnorePattern: '^(beforeInteractions|afterInteractions|interactionsToPerform)$', args: 'none' }], + }, + globals: { + LightweightCharts: false, + generateLineData: false, + generateHistogramData: false, + generateBars: false, + }, +}; diff --git a/tests/e2e/coverage/test-cases/chart/create-string.js b/tests/e2e/coverage/test-cases/chart/create-string.js new file mode 100644 index 0000000000..ef80be4b42 --- /dev/null +++ b/tests/e2e/coverage/test-cases/chart/create-string.js @@ -0,0 +1,12 @@ +function interactionsToPerform() { + return []; +} + +function beforeInteractions() { + LightweightCharts.createChart('container'); + return Promise.resolve(); +} + +function afterInteractions() { + return Promise.resolve(); +} diff --git a/tests/e2e/coverage/test-cases/chart/crosshair.js b/tests/e2e/coverage/test-cases/chart/crosshair.js new file mode 100644 index 0000000000..3a82a88266 --- /dev/null +++ b/tests/e2e/coverage/test-cases/chart/crosshair.js @@ -0,0 +1,58 @@ +function interactionsToPerform() { + return [ + { action: 'moveMouseCenter', target: 'container' }, + { action: 'moveMouseTopLeft', target: 'container' }, + { action: 'moveMouseCenter', target: 'container' }, + ]; +} + +let chart; + +function beforeInteractions(container) { + chart = LightweightCharts.createChart(container, { + crosshair: { + mode: LightweightCharts.CrosshairMode.Magnet, + vertLine: { + labelVisible: false, + visible: false, + }, + horzLine: { + labelVisible: false, + visible: false, + }, + }, + }); + + const mainSeries = chart.addCandlestickSeries(); + mainSeries.setData(generateBars()); + + const lineSeries = chart.addLineSeries({ + crosshairMarkerBorderColor: 'orange', + crosshairMarkerBackgroundColor: 'orange', + crosshairMarkerRadius: 6, + }); + lineSeries.setData(generateLineData()); + + return new Promise(resolve => { + requestAnimationFrame(resolve); + }); +} + +function afterInteractions() { + chart.applyOptions({ + crosshair: { + mode: LightweightCharts.CrosshairMode.Normal, + vertLine: { + labelVisible: true, + visible: true, + }, + horzLine: { + labelVisible: true, + visible: true, + }, + }, + }); + return new Promise(resolve => { + requestAnimationFrame(resolve); + }); +} diff --git a/tests/e2e/coverage/test-cases/chart/handle-scale.js b/tests/e2e/coverage/test-cases/chart/handle-scale.js new file mode 100644 index 0000000000..3a402259c9 --- /dev/null +++ b/tests/e2e/coverage/test-cases/chart/handle-scale.js @@ -0,0 +1,37 @@ +function simpleData() { + return [ + { time: 1663740000, value: 10 }, + { time: 1663750000, value: 20 }, + { time: 1663760000, value: 30 }, + ]; +} + +function interactionsToPerform() { + return [ + { action: 'scrollUpRight', target: 'pane' }, + { action: 'scrollDownLeft', target: 'pane' }, + ]; +} + +let chart; + +function beforeInteractions(container) { + chart = LightweightCharts.createChart(container, { + handleScale: false, + }); + + const mainSeries = chart.addLineSeries(); + + mainSeries.setData(simpleData()); + + return Promise.resolve(); +} + +function afterInteractions() { + chart.applyOptions({ + handleScale: { + axisPressedMouseMove: true, + }, + }); + return Promise.resolve(); +} diff --git a/tests/e2e/coverage/test-cases/chart/handle-scroll.js b/tests/e2e/coverage/test-cases/chart/handle-scroll.js new file mode 100644 index 0000000000..1700d6a3f6 --- /dev/null +++ b/tests/e2e/coverage/test-cases/chart/handle-scroll.js @@ -0,0 +1,30 @@ +function simpleData() { + return [ + { time: 1663740000, value: 10 }, + { time: 1663750000, value: 20 }, + { time: 1663760000, value: 30 }, + ]; +} + +function interactionsToPerform() { + return [ + { action: 'scrollUpRight', target: 'pane' }, + { action: 'scrollDownLeft', target: 'pane' }, + ]; +} + +function beforeInteractions(container) { + const chart = LightweightCharts.createChart(container, { + handleScroll: false, + }); + + const mainSeries = chart.addLineSeries(); + + mainSeries.setData(simpleData()); + + return Promise.resolve(); +} + +function afterInteractions() { + return Promise.resolve(); +} diff --git a/tests/e2e/coverage/test-cases/chart/kinetic-scroll.js b/tests/e2e/coverage/test-cases/chart/kinetic-scroll.js new file mode 100644 index 0000000000..497af98f94 --- /dev/null +++ b/tests/e2e/coverage/test-cases/chart/kinetic-scroll.js @@ -0,0 +1,29 @@ +function simpleData() { + return [ + { time: 1663740000, value: 10 }, + { time: 1663750000, value: 20 }, + { time: 1663760000, value: 30 }, + ]; +} + +function interactionsToPerform() { + return [{ action: 'kineticAnimation', target: 'pane' }]; +} + +function beforeInteractions(container) { + const chart = LightweightCharts.createChart(container, { + kineticScroll: { + mouse: true, + }, + }); + + const mainSeries = chart.addLineSeries(); + + mainSeries.setData(simpleData()); + + return Promise.resolve(); +} + +function afterInteractions() { + return Promise.resolve(); +} diff --git a/tests/e2e/coverage/test-cases/chart/layout.js b/tests/e2e/coverage/test-cases/chart/layout.js new file mode 100644 index 0000000000..8f5982db3e --- /dev/null +++ b/tests/e2e/coverage/test-cases/chart/layout.js @@ -0,0 +1,44 @@ +function simpleData() { + return [ + { time: 1663740000, value: 10 }, + { time: 1663750000, value: 20 }, + { time: 1663760000, value: 30 }, + ]; +} + +function interactionsToPerform() { + return []; +} + +let chart; + +function beforeInteractions(container) { + chart = LightweightCharts.createChart(container, { + layout: { + background: { + type: LightweightCharts.ColorType.VerticalGradient, + topColor: '#FFFFFF', + bottomColor: '#AAFFAA', + }, + }, + }); + + const mainSeries = chart.addLineSeries(); + + mainSeries.setData(simpleData()); + + return Promise.resolve(); +} + +function afterInteractions() { + chart.applyOptions({ + layout: { + background: { + type: LightweightCharts.ColorType.Solid, + color: 'transparent', + }, + fontFamily: undefined, + }, + }); + return Promise.resolve(); +} diff --git a/tests/e2e/coverage/test-cases/chart/localization.js b/tests/e2e/coverage/test-cases/chart/localization.js new file mode 100644 index 0000000000..2e50e52efe --- /dev/null +++ b/tests/e2e/coverage/test-cases/chart/localization.js @@ -0,0 +1,33 @@ +function interactionsToPerform() { + return []; +} + +function beforeInteractions(container) { + const chart = LightweightCharts.createChart(container, { + localization: { + locale: 'en-GB-u-ca-islamic', + }, + }); + + const mainSeries = chart.addAreaSeries(); + + mainSeries.setData(generateLineData()); + + chart.options(); + + chart.applyOptions({ + localization: { + dateFormat: 'yyyy MM dd', + priceFormatter: p => `£${p.toFixed(2)}`, + timeFormatter: t => `${t.toString()}`, + }, + }); + + return new Promise(resolve => { + requestAnimationFrame(resolve); + }); +} + +function afterInteractions() { + return Promise.resolve(); +} diff --git a/tests/e2e/coverage/test-cases/chart/options.js b/tests/e2e/coverage/test-cases/chart/options.js new file mode 100644 index 0000000000..16ed464670 --- /dev/null +++ b/tests/e2e/coverage/test-cases/chart/options.js @@ -0,0 +1,27 @@ +function interactionsToPerform() { + return []; +} + +function beforeInteractions(container) { + const chart = LightweightCharts.createChart(container); + + const mainSeries = chart.addAreaSeries(); + + mainSeries.setData(generateLineData()); + + chart.options(); + + chart.applyOptions({ + localization: { + dateFormat: 'yyyy MM dd', + }, + }); + + return new Promise(resolve => { + requestAnimationFrame(resolve); + }); +} + +function afterInteractions() { + return Promise.resolve(); +} diff --git a/tests/e2e/coverage/test-cases/chart/remove.js b/tests/e2e/coverage/test-cases/chart/remove.js new file mode 100644 index 0000000000..aa6d610db6 --- /dev/null +++ b/tests/e2e/coverage/test-cases/chart/remove.js @@ -0,0 +1,16 @@ +function interactionsToPerform() { + return []; +} + +let chart; + +function beforeInteractions(container) { + chart = LightweightCharts.createChart(container); + return Promise.resolve(); +} + +function afterInteractions() { + chart.remove(); + chart = null; + return Promise.resolve(); +} diff --git a/tests/e2e/coverage/test-cases/chart/resize.js b/tests/e2e/coverage/test-cases/chart/resize.js new file mode 100644 index 0000000000..42c0231083 --- /dev/null +++ b/tests/e2e/coverage/test-cases/chart/resize.js @@ -0,0 +1,46 @@ +function interactionsToPerform() { + return []; +} + +async function awaitNewFrame() { + return new Promise(resolve => { + requestAnimationFrame(resolve); + }); +} + +let chart; + +function beforeInteractions(container) { + chart = LightweightCharts.createChart(container); + + const mainSeries = chart.addHistogramSeries(); + + mainSeries.setData(generateHistogramData()); + + return new Promise(resolve => { + requestAnimationFrame(resolve); + }); +} + +async function afterInteractions() { + chart.resize(750, 100); + + chart.applyOptions({ + timeScale: { + barSpacing: 12, + minBarSpacing: 2, + rightOffset: 4, + fixRightEdge: true, + fixLeftEdge: true, + }, + }); + chart.timeScale().fitContent(); + + await awaitNewFrame(); + + chart.resize(450, 200); + + return new Promise(resolve => { + requestAnimationFrame(resolve); + }); +} diff --git a/tests/e2e/coverage/test-cases/chart/screenshot.js b/tests/e2e/coverage/test-cases/chart/screenshot.js new file mode 100644 index 0000000000..8d8b11fd87 --- /dev/null +++ b/tests/e2e/coverage/test-cases/chart/screenshot.js @@ -0,0 +1,22 @@ +function interactionsToPerform() { + return []; +} + +let chart; + +function beforeInteractions(container) { + chart = LightweightCharts.createChart(container); + + const mainSeries = chart.addHistogramSeries(); + + mainSeries.setData(generateHistogramData()); + + return new Promise(resolve => { + requestAnimationFrame(resolve); + }); +} + +function afterInteractions() { + chart.takeScreenshot(); + return Promise.resolve(); +} diff --git a/tests/e2e/coverage/test-cases/chart/watermark.js b/tests/e2e/coverage/test-cases/chart/watermark.js new file mode 100644 index 0000000000..17f5285bf0 --- /dev/null +++ b/tests/e2e/coverage/test-cases/chart/watermark.js @@ -0,0 +1,53 @@ +function simpleData() { + return [ + { time: 1663740000, value: 10 }, + { time: 1663750000, value: 20 }, + { time: 1663760000, value: 30 }, + ]; +} + +function interactionsToPerform() { + return []; +} + +let chart; + +function beforeInteractions(container) { + chart = LightweightCharts.createChart(container, { + watermark: { + visible: true, + color: 'red', + text: 'Watermark', + fontSize: 24, + fontStyle: 'italic', + }, + }); + + const mainSeries = chart.addLineSeries(); + + mainSeries.setData(simpleData()); + + return Promise.resolve(); +} + +function afterInteractions() { + chart.applyOptions({ + watermark: { + fontFamily: 'Roboto', + horzAlign: 'left', + vertAlign: 'top', + }, + }); + + return new Promise(resolve => { + requestAnimationFrame(() => { + chart.applyOptions({ + watermark: { + horzAlign: 'right', + vertAlign: 'bottom', + }, + }); + }); + requestAnimationFrame(resolve); + }); +} diff --git a/tests/e2e/coverage/test-cases/exports.js b/tests/e2e/coverage/test-cases/exports.js new file mode 100644 index 0000000000..4c858947b6 --- /dev/null +++ b/tests/e2e/coverage/test-cases/exports.js @@ -0,0 +1,16 @@ +function interactionsToPerform() { + return []; +} + +function beforeInteractions() { + console.log(LightweightCharts.TrackingModeExitMode); + console.log(LightweightCharts.MismatchDirection); + console.log(LightweightCharts.PriceLineSource); + console.log(LightweightCharts.TickMarkType); + console.log(LightweightCharts.version()); + return Promise.resolve(); +} + +function afterInteractions() { + return Promise.resolve(); +} diff --git a/tests/e2e/coverage/test-cases/interactions/interactions.js b/tests/e2e/coverage/test-cases/interactions/interactions.js new file mode 100644 index 0000000000..558819e1d9 --- /dev/null +++ b/tests/e2e/coverage/test-cases/interactions/interactions.js @@ -0,0 +1,105 @@ +function interactionsToPerform() { + return [ + { action: 'moveMouseCenter', target: 'container' }, + { action: 'scrollLeft', target: 'pane' }, + { action: 'scrollUp', target: 'pane' }, + { action: 'scrollDown', target: 'pane' }, + { action: 'scrollRight', target: 'pane' }, + { action: 'scrollUpRight', target: 'pane' }, + { action: 'scrollDownLeft', target: 'pane' }, + { action: 'outsideClick', target: 'container' }, + { action: 'viewportZoomInOut' }, + { action: 'verticalDrag', target: 'leftpricescale' }, + { action: 'verticalDrag', target: 'rightpricescale' }, + { action: 'horizontalDrag', target: 'timescale' }, + { action: 'verticalDrag', target: 'pane' }, + { action: 'horizontalDrag', target: 'pane' }, + { action: 'click', target: 'leftpricescale' }, + { action: 'doubleClick', target: 'leftpricescale' }, + { action: 'click', target: 'rightpricescale' }, + { action: 'doubleClick', target: 'rightpricescale' }, + { action: 'click', target: 'timescale' }, + { action: 'doubleClick', target: 'timescale' }, + { action: 'tap', target: 'container' }, + { action: 'pinchZoomIn', target: 'container' }, + { action: 'pinchZoomOut', target: 'container' }, + { action: 'swipeTouchVertical', target: 'leftpricescale' }, + { action: 'swipeTouchVertical', target: 'pane' }, + { action: 'swipeTouchHorizontal', target: 'timescale' }, + { action: 'swipeTouchDiagonal', target: 'container' }, + { action: 'longTouch', target: 'container' }, + { action: 'kineticAnimation', target: 'timescale' }, + ]; +} + +let chart; + +function beforeInteractions(container) { + chart = LightweightCharts.createChart(container, { + leftPriceScale: { + visible: true, + mode: LightweightCharts.PriceScaleMode.Logarithmic, + }, + rightPriceScale: { + visible: true, + mode: LightweightCharts.PriceScaleMode.Percentage, + }, + timeScale: { + timeVisible: true, + }, + }); + + const lineSeries = chart.addLineSeries(); + lineSeries.setData(generateLineData()); + + const candlestickSeries = chart.addCandlestickSeries({ + priceScaleId: 'left', + }); + chart.priceScale('left').applyOptions({ + autoScale: false, + }); + candlestickSeries.setData(generateBars()); + + const histogramSeries = chart.addHistogramSeries({ + priceScaleId: 'overlay-1', + }); + histogramSeries.setData(generateHistogramData()); + + const barSeries = chart.addBarSeries({ + priceScaleId: 'overlay-2', + }); + barSeries.setData(generateBars()); + + const areaSeries = chart.addAreaSeries({ + priceScaleId: 'overlay-3', + }); + areaSeries.setData(generateLineData()); + + const baselineSeries = chart.addBaselineSeries({ + priceScaleId: 'overlay-4', + }); + baselineSeries.setData(generateLineData()); + + chart.timeScale().subscribeVisibleTimeRangeChange(console.log); + chart.timeScale().subscribeVisibleLogicalRangeChange(console.log); + chart.timeScale().subscribeSizeChange(console.log); + chart.subscribeCrosshairMove(console.log); + chart.subscribeClick(console.log); + + return new Promise(resolve => { + requestAnimationFrame(resolve); + }); +} + +function afterInteractions() { + return new Promise(resolve => { + requestAnimationFrame(() => { + chart.timeScale().unsubscribeVisibleTimeRangeChange(console.log); + chart.timeScale().unsubscribeVisibleLogicalRangeChange(console.log); + chart.timeScale().unsubscribeSizeChange(console.log); + chart.unsubscribeCrosshairMove(console.log); + chart.unsubscribeClick(console.log); + resolve(); + }); + }); +} diff --git a/tests/e2e/coverage/test-cases/interactions/tracking-mode.js b/tests/e2e/coverage/test-cases/interactions/tracking-mode.js new file mode 100644 index 0000000000..cc901cd6d7 --- /dev/null +++ b/tests/e2e/coverage/test-cases/interactions/tracking-mode.js @@ -0,0 +1,32 @@ +function interactionsToPerform() { + return [ + { action: 'longTouch', target: 'pane' }, + { action: 'swipeTouchDiagonal', target: 'pane' }, + { action: 'tap', target: 'container' }, + { action: 'swipeTouchDiagonal', target: 'container' }, + { action: 'longTouch', target: 'pane' }, + ]; +} + +let chart; + +function beforeInteractions(container) { + chart = LightweightCharts.createChart(container, { + trackingMode: { + exitMode: LightweightCharts.TrackingModeExitMode.OnNextTap, + }, + }); + + const mainSeries = chart.addHistogramSeries(); + + mainSeries.setData(generateHistogramData()); + + return new Promise(resolve => { + requestAnimationFrame(resolve); + }); +} + +function afterInteractions() { + chart.takeScreenshot(); + return Promise.resolve(); +} diff --git a/tests/e2e/coverage/test-cases/price-scale/change-scales.js b/tests/e2e/coverage/test-cases/price-scale/change-scales.js new file mode 100644 index 0000000000..0601557ec5 --- /dev/null +++ b/tests/e2e/coverage/test-cases/price-scale/change-scales.js @@ -0,0 +1,49 @@ +function interactionsToPerform() { + return []; +} + +async function awaitNewFrame() { + return new Promise(resolve => { + requestAnimationFrame(resolve); + }); +} + +let mainSeries; +let chart; + +async function beforeInteractions(container) { + chart = LightweightCharts.createChart(container); + + mainSeries = chart.addLineSeries(); + mainSeries.setData(generateLineData()); + + await awaitNewFrame(); + chart.priceScale('right').applyOptions({ + mode: LightweightCharts.PriceScaleMode.Percentage, + invertScale: true, + }); + + await awaitNewFrame(); + chart.priceScale('right').applyOptions({ + mode: LightweightCharts.PriceScaleMode.IndexedTo100, + invertScale: false, + }); + + await awaitNewFrame(); + chart.priceScale('right').applyOptions({ + mode: LightweightCharts.PriceScaleMode.Logarithmic, + }); + + await awaitNewFrame(); + chart.priceScale('right').applyOptions({ + mode: LightweightCharts.PriceScaleMode.Normal, + }); + + return Promise.resolve(); +} + +function afterInteractions() { + return new Promise(resolve => { + requestAnimationFrame(resolve); + }); +} diff --git a/tests/e2e/coverage/test-cases/price-scale/log-scale.js b/tests/e2e/coverage/test-cases/price-scale/log-scale.js new file mode 100644 index 0000000000..9f294f359d --- /dev/null +++ b/tests/e2e/coverage/test-cases/price-scale/log-scale.js @@ -0,0 +1,32 @@ +function interactionsToPerform() { + return [ + { action: 'scrollUp', target: 'pane' }, + { action: 'scrollDown', target: 'pane' }, + { action: 'scrollDown', target: 'pane' }, + ]; +} + +let mainSeries; + +function beforeInteractions(container) { + const chart = LightweightCharts.createChart(container); + + mainSeries = chart.addLineSeries(); + const priceScale = chart.priceScale('right'); + priceScale.applyOptions({ + mode: LightweightCharts.PriceScaleMode.Logarithmic, + invertScale: true, + }); + + mainSeries.setData(generateLineData()); + + return Promise.resolve(); +} + +function afterInteractions() { + mainSeries.coordinateToPrice(300); + mainSeries.priceToCoordinate(300); + return new Promise(resolve => { + requestAnimationFrame(resolve); + }); +} diff --git a/tests/e2e/coverage/test-cases/price-scale/options.js b/tests/e2e/coverage/test-cases/price-scale/options.js new file mode 100644 index 0000000000..7e02b156d3 --- /dev/null +++ b/tests/e2e/coverage/test-cases/price-scale/options.js @@ -0,0 +1,79 @@ +function interactionsToPerform() { + return []; +} + +async function awaitNewFrame() { + return new Promise(resolve => { + requestAnimationFrame(resolve); + }); +} + +async function beforeInteractions(container) { + const chart = LightweightCharts.createChart(container, { + leftPriceScale: { + visible: true, + mode: LightweightCharts.PriceScaleMode.Logarithmic, + }, + rightPriceScale: { + visible: true, + mode: LightweightCharts.PriceScaleMode.Percentage, + }, + }); + + const areaSeries = chart.addAreaSeries(); + + areaSeries.setData(generateLineData()); + + const lineSeries = chart.addLineSeries({ priceScaleId: 'left' }); + lineSeries.setData(generateLineData()); + + await awaitNewFrame(); + + chart.priceScale('left').width(); + chart.priceScale('right').width(); + chart.priceScale('right').applyOptions({}); + + await awaitNewFrame(); + + chart.applyOptions({ + leftPriceScale: { + mode: LightweightCharts.PriceScaleMode.IndexedTo100, + }, + rightPriceScale: { + mode: LightweightCharts.PriceScaleMode.Normal, + invertScale: true, + alignLabels: false, + }, + }); + + chart.priceScale('right').applyOptions({ + borderVisible: false, + ticksVisible: false, + mode: LightweightCharts.PriceScaleMode.Logarithmic, + }); + + await awaitNewFrame(); + + lineSeries.applyOptions({ priceScaleId: 'right' }); + areaSeries.applyOptions({ priceScaleId: 'left' }); + + chart.priceScale('right').applyOptions({ + borderVisible: true, + ticksVisible: true, + }); + + areaSeries.coordinateToPrice(10); + areaSeries.priceToCoordinate(200); + + try { + chart.priceScale(); + } catch { + console.log('expected error'); + } + + return Promise.resolve(); +} + +function afterInteractions() { + return Promise.resolve(); +} diff --git a/tests/e2e/coverage/test-cases/price-scale/percentage-scale.js b/tests/e2e/coverage/test-cases/price-scale/percentage-scale.js new file mode 100644 index 0000000000..b80e47664b --- /dev/null +++ b/tests/e2e/coverage/test-cases/price-scale/percentage-scale.js @@ -0,0 +1,31 @@ +function interactionsToPerform() { + return [ + { action: 'scrollUp', target: 'pane' }, + { action: 'scrollDown', target: 'pane' }, + { action: 'scrollDown', target: 'pane' }, + ]; +} + +let mainSeries; + +function beforeInteractions(container) { + const chart = LightweightCharts.createChart(container); + + mainSeries = chart.addLineSeries(); + chart.priceScale('right').applyOptions({ + mode: LightweightCharts.PriceScaleMode.Percentage, + invertScale: true, + }); + + mainSeries.setData(generateLineData()); + + return Promise.resolve(); +} + +function afterInteractions() { + mainSeries.coordinateToPrice(300); + mainSeries.priceToCoordinate(300); + return new Promise(resolve => { + requestAnimationFrame(resolve); + }); +} diff --git a/tests/e2e/coverage/test-cases/series/area-inverted.js b/tests/e2e/coverage/test-cases/series/area-inverted.js new file mode 100644 index 0000000000..ee2cccf418 --- /dev/null +++ b/tests/e2e/coverage/test-cases/series/area-inverted.js @@ -0,0 +1,31 @@ +function interactionsToPerform() { + return []; +} + +async function awaitNewFrame() { + return new Promise(resolve => { + requestAnimationFrame(resolve); + }); +} + +async function beforeInteractions(container) { + const chart = LightweightCharts.createChart(container); + + const mainSeries = chart.addAreaSeries({ + invertFilledArea: true, + }); + + mainSeries.setData(generateLineData()); + + await awaitNewFrame(); + + mainSeries.applyOptions({ + invertFilledArea: false, + }); + + return Promise.resolve(); +} + +function afterInteractions() { + return Promise.resolve(); +} diff --git a/tests/e2e/coverage/test-cases/series/area-series.js b/tests/e2e/coverage/test-cases/series/area-series.js new file mode 100644 index 0000000000..ee8b25033d --- /dev/null +++ b/tests/e2e/coverage/test-cases/series/area-series.js @@ -0,0 +1,36 @@ +function interactionsToPerform() { + return []; +} + +function testSeriesApi(series) { + series.options(); + series.coordinateToPrice(300); + series.priceToCoordinate(300); + series.priceScale(); + series.applyOptions({ + priceFormatter: a => a.toFixed(2), + }); + series.priceFormatter(); + series.seriesType(); + series.markers(); + series.dataByIndex(10); + series.dataByIndex(-5); + series.dataByIndex(-5, LightweightCharts.MismatchDirection.NearestRight); + series.dataByIndex(1500, LightweightCharts.MismatchDirection.NearestLeft); + series.dataByIndex(1500, LightweightCharts.MismatchDirection.None); +} + +function beforeInteractions(container) { + const chart = LightweightCharts.createChart(container); + + const mainSeries = chart.addAreaSeries(); + + mainSeries.setData(generateLineData()); + + testSeriesApi(mainSeries); + return Promise.resolve(); +} + +function afterInteractions() { + return Promise.resolve(); +} diff --git a/tests/e2e/coverage/test-cases/series/bar-series.js b/tests/e2e/coverage/test-cases/series/bar-series.js new file mode 100644 index 0000000000..34b35319f6 --- /dev/null +++ b/tests/e2e/coverage/test-cases/series/bar-series.js @@ -0,0 +1,37 @@ +function interactionsToPerform() { + return []; +} + +function testSeriesApi(series) { + series.options(); + series.coordinateToPrice(300); + series.priceToCoordinate(300); + series.priceScale(); + series.applyOptions({ + priceFormatter: a => a.toFixed(2), + }); + series.priceFormatter(); + series.seriesType(); + series.markers(); + series.dataByIndex(10); + series.dataByIndex(-5); + series.dataByIndex(-5, LightweightCharts.MismatchDirection.NearestRight); + series.dataByIndex(1500, LightweightCharts.MismatchDirection.NearestLeft); + series.dataByIndex(1500, LightweightCharts.MismatchDirection.None); +} + +function beforeInteractions(container) { + const chart = LightweightCharts.createChart(container); + + const mainSeries = chart.addBarSeries(); + + mainSeries.setData(generateBars()); + + testSeriesApi(mainSeries); + + return Promise.resolve(); +} + +function afterInteractions() { + return Promise.resolve(); +} diff --git a/tests/e2e/coverage/test-cases/series/baseline-series.js b/tests/e2e/coverage/test-cases/series/baseline-series.js new file mode 100644 index 0000000000..e2ae086da9 --- /dev/null +++ b/tests/e2e/coverage/test-cases/series/baseline-series.js @@ -0,0 +1,37 @@ +function interactionsToPerform() { + return []; +} + +function testSeriesApi(series) { + series.options(); + series.coordinateToPrice(300); + series.priceToCoordinate(300); + series.priceScale(); + series.applyOptions({ + priceFormatter: a => a.toFixed(2), + }); + series.priceFormatter(); + series.seriesType(); + series.markers(); + series.dataByIndex(10); + series.dataByIndex(-5); + series.dataByIndex(-5, LightweightCharts.MismatchDirection.NearestRight); + series.dataByIndex(1500, LightweightCharts.MismatchDirection.NearestLeft); + series.dataByIndex(1500, LightweightCharts.MismatchDirection.None); +} + +function beforeInteractions(container) { + const chart = LightweightCharts.createChart(container); + + const mainSeries = chart.addBaselineSeries(); + + mainSeries.setData(generateLineData()); + + testSeriesApi(mainSeries); + + return Promise.resolve(); +} + +function afterInteractions() { + return Promise.resolve(); +} diff --git a/tests/e2e/coverage/test-cases/series/candlestick-series.js b/tests/e2e/coverage/test-cases/series/candlestick-series.js new file mode 100644 index 0000000000..d80b7cddef --- /dev/null +++ b/tests/e2e/coverage/test-cases/series/candlestick-series.js @@ -0,0 +1,37 @@ +function interactionsToPerform() { + return []; +} + +function testSeriesApi(series) { + series.options(); + series.coordinateToPrice(300); + series.priceToCoordinate(300); + series.priceScale(); + series.applyOptions({ + priceFormatter: a => a.toFixed(2), + }); + series.priceFormatter(); + series.seriesType(); + series.markers(); + series.dataByIndex(10); + series.dataByIndex(-5); + series.dataByIndex(-5, LightweightCharts.MismatchDirection.NearestRight); + series.dataByIndex(1500, LightweightCharts.MismatchDirection.NearestLeft); + series.dataByIndex(1500, LightweightCharts.MismatchDirection.None); +} + +function beforeInteractions(container) { + const chart = LightweightCharts.createChart(container); + + const mainSeries = chart.addCandlestickSeries(); + + mainSeries.setData(generateBars()); + + testSeriesApi(mainSeries); + + return Promise.resolve(); +} + +function afterInteractions() { + return Promise.resolve(); +} diff --git a/tests/e2e/coverage/test-cases/series/histogram-series.js b/tests/e2e/coverage/test-cases/series/histogram-series.js new file mode 100644 index 0000000000..4b87c7d37a --- /dev/null +++ b/tests/e2e/coverage/test-cases/series/histogram-series.js @@ -0,0 +1,39 @@ +function interactionsToPerform() { + return []; +} + +function testSeriesApi(series) { + series.options(); + series.coordinateToPrice(300); + series.priceToCoordinate(300); + series.priceScale(); + series.applyOptions({ + priceFormatter: a => a.toFixed(2), + }); + series.priceFormatter(); + series.seriesType(); + series.markers(); + series.dataByIndex(10); + series.dataByIndex(-5); + series.dataByIndex(-5, LightweightCharts.MismatchDirection.NearestRight); + series.dataByIndex(1500, LightweightCharts.MismatchDirection.NearestLeft); + series.dataByIndex(1500, LightweightCharts.MismatchDirection.None); +} + +function beforeInteractions(container) { + const chart = LightweightCharts.createChart(container); + + const mainSeries = chart.addHistogramSeries({ + autoscaleInfoProvider: original => original(), + }); + + mainSeries.setData(generateHistogramData()); + + testSeriesApi(mainSeries); + + return Promise.resolve(); +} + +function afterInteractions() { + return Promise.resolve(); +} diff --git a/tests/e2e/coverage/test-cases/series/line-series.js b/tests/e2e/coverage/test-cases/series/line-series.js new file mode 100644 index 0000000000..cd23cb32d3 --- /dev/null +++ b/tests/e2e/coverage/test-cases/series/line-series.js @@ -0,0 +1,50 @@ +function interactionsToPerform() { + return []; +} + +function testSeriesApi(series) { + series.options(); + series.coordinateToPrice(300); + series.priceToCoordinate(300); + series.priceScale(); + series.applyOptions({ + priceFormatter: a => a.toFixed(2), + }); + series.priceFormatter(); + series.seriesType(); + series.markers(); + series.dataByIndex(10); + series.dataByIndex(-5); + series.dataByIndex(-5, LightweightCharts.MismatchDirection.NearestRight); + series.dataByIndex(1500, LightweightCharts.MismatchDirection.NearestLeft); + series.dataByIndex(1500, LightweightCharts.MismatchDirection.None); +} + +let mainSeries; + +function beforeInteractions(container) { + const chart = LightweightCharts.createChart(container); + + mainSeries = chart.addLineSeries(); + + mainSeries.setData(generateLineData()); + + // Cover edge case + chart.timeScale().setVisibleRange({ + from: 0, + to: 0.8, + }); + + testSeriesApi(mainSeries); + return Promise.resolve(); +} + +function afterInteractions() { + mainSeries.applyOptions({ lineType: LightweightCharts.LineType.WithSteps }); + return new Promise(resolve => { + requestAnimationFrame(() => { + mainSeries.applyOptions({ lineType: LightweightCharts.LineType.Curved }); + requestAnimationFrame(resolve); + }); + }); +} diff --git a/tests/e2e/coverage/test-cases/series/markers.js b/tests/e2e/coverage/test-cases/series/markers.js new file mode 100644 index 0000000000..533c988510 --- /dev/null +++ b/tests/e2e/coverage/test-cases/series/markers.js @@ -0,0 +1,34 @@ +function interactionsToPerform() { + return [ + { action: 'moveMouseCenter', target: 'container' }, + { action: 'moveMouseTopLeft', target: 'container' }, + { action: 'moveMouseCenter', target: 'container' }, + ]; +} + +let mainSeries; + +function beforeInteractions(container) { + const chart = LightweightCharts.createChart(container); + + mainSeries = chart.addBaselineSeries(); + + const data = generateLineData(); + mainSeries.setData(data); + + mainSeries.setMarkers([ + { time: data[data.length - 7].time, position: 'belowBar', color: 'rgb(255, 0, 0)', shape: 'arrowUp', text: 'test' }, + { time: data[data.length - 5].time, position: 'aboveBar', color: 'rgba(255, 255, 0, 1)', shape: 'arrowDown', text: 'test' }, + { time: data[data.length - 3].time, position: 'inBar', color: '#f0f', shape: 'circle', text: 'test' }, + { time: data[data.length - 1].time, position: 'belowBar', color: '#fff00a', shape: 'square', text: 'test', size: 2 }, + ]); + + mainSeries.markers(); + + return Promise.resolve(); +} + +function afterInteractions() { + mainSeries.setMarkers([]); + return Promise.resolve(); +} diff --git a/tests/e2e/coverage/test-cases/series/overlay-series.js b/tests/e2e/coverage/test-cases/series/overlay-series.js new file mode 100644 index 0000000000..728658b664 --- /dev/null +++ b/tests/e2e/coverage/test-cases/series/overlay-series.js @@ -0,0 +1,32 @@ +function interactionsToPerform() { + return []; +} + +let chart; +let overlaySeries; + +function beforeInteractions(container) { + chart = LightweightCharts.createChart(container); + + const mainSeries = chart.addLineSeries(); + + mainSeries.setData(generateLineData()); + + overlaySeries = chart.addAreaSeries({ + priceScaleId: 'overlay-id', + priceFormat: { + type: 'volume', + }, + lastPriceAnimation: LightweightCharts.LastPriceAnimationMode.Continuous, + }); + overlaySeries.setData(generateLineData()); + + chart.priceScale('overlay-id').width(); + + return Promise.resolve(); +} + +function afterInteractions() { + chart.removeSeries(overlaySeries); + return Promise.resolve(); +} diff --git a/tests/e2e/coverage/test-cases/series/price-format.js b/tests/e2e/coverage/test-cases/series/price-format.js new file mode 100644 index 0000000000..41ced5af4e --- /dev/null +++ b/tests/e2e/coverage/test-cases/series/price-format.js @@ -0,0 +1,55 @@ +function interactionsToPerform() { + return []; +} + +function priceFormatter(price) { + return '£' + price.toFixed(2); +} + +let mainSeries; + +function beforeInteractions(container) { + const chart = LightweightCharts.createChart(container); + + mainSeries = chart.addLineSeries({ + priceFormat: { + type: 'custom', + minMove: 0.02, + formatter: priceFormatter, + }, + }); + + mainSeries.setData(generateLineData()); + + const overlaySeries = chart.addAreaSeries({ + priceScaleId: 'overlay-id', + priceFormat: { + type: 'volume', + }, + }); + overlaySeries.setData(generateLineData()); + + // Should be a volume, therefore test the various states for the formatter. + overlaySeries.priceFormatter().format(1); + overlaySeries.priceFormatter().format(0.001); + overlaySeries.priceFormatter().format(1234); + overlaySeries.priceFormatter().format(1234567); + overlaySeries.priceFormatter().format(1234567890); + + return new Promise(resolve => { + requestAnimationFrame(resolve); + }); +} + +function afterInteractions() { + mainSeries.applyOptions({ + priceFormat: { + type: 'price', + minMove: 1, + precision: undefined, + }, + }); + return new Promise(resolve => { + requestAnimationFrame(resolve); + }); +} diff --git a/tests/e2e/coverage/test-cases/series/price-line.js b/tests/e2e/coverage/test-cases/series/price-line.js new file mode 100644 index 0000000000..f5330b83fa --- /dev/null +++ b/tests/e2e/coverage/test-cases/series/price-line.js @@ -0,0 +1,63 @@ +function interactionsToPerform() { + return []; +} + +let mainSeries; +let priceLineToRemove; +let priceLine1; + +function beforeInteractions(container) { + const chart = LightweightCharts.createChart(container); + + mainSeries = chart.addBarSeries(); + + mainSeries.setData(generateBars()); + + mainSeries.createPriceLine({ + price: 10, + color: 'red', + lineWidth: 1, + lineStyle: LightweightCharts.LineStyle.Solid, + }); + + mainSeries.createPriceLine({ + price: 20, + color: '#00FF00', + lineWidth: 2, + lineStyle: LightweightCharts.LineStyle.Dotted, + }); + + mainSeries.createPriceLine({ + price: 30, + color: 'rgb(0,0,255)', + lineWidth: 3, + lineStyle: LightweightCharts.LineStyle.Dashed, + }); + + priceLineToRemove = mainSeries.createPriceLine({ + price: 40, + color: 'rgba(255,0,0,0.5)', + lineWidth: 4, + lineStyle: LightweightCharts.LineStyle.LargeDashed, + }); + + priceLine1 = mainSeries.createPriceLine({ + price: 50, + color: '#f0f', + lineWidth: 4, + lineStyle: LightweightCharts.LineStyle.SparseDotted, + }); + + priceLine1.options(); + + return new Promise(resolve => { + requestAnimationFrame(resolve); + }); +} + +function afterInteractions() { + mainSeries.removePriceLine(priceLineToRemove); + priceLine1.applyOptions({}); + + return Promise.resolve(); +} diff --git a/tests/e2e/coverage/test-cases/series/remove-series.js b/tests/e2e/coverage/test-cases/series/remove-series.js new file mode 100644 index 0000000000..b465b28751 --- /dev/null +++ b/tests/e2e/coverage/test-cases/series/remove-series.js @@ -0,0 +1,21 @@ +function interactionsToPerform() { + return []; +} + +let series; +let chart; + +function beforeInteractions(container) { + chart = LightweightCharts.createChart(container); + + series = chart.addAreaSeries(); + + series.setData(generateLineData()); + + return Promise.resolve(); +} + +function afterInteractions() { + chart.removeSeries(series); + return Promise.resolve(); +} diff --git a/tests/e2e/coverage/test-cases/series/styling.js b/tests/e2e/coverage/test-cases/series/styling.js new file mode 100644 index 0000000000..9b648d8730 --- /dev/null +++ b/tests/e2e/coverage/test-cases/series/styling.js @@ -0,0 +1,61 @@ +function interactionsToPerform() { + return []; +} + +async function awaitNewFrame() { + return new Promise(resolve => { + requestAnimationFrame(resolve); + }); +} + +let candlestickSeries; + +async function beforeInteractions(container) { + const chart = LightweightCharts.createChart(container, { + leftPriceScale: { + visible: true, + }, + rightPriceScale: { + visible: true, + }, + timeScale: { + timeVisible: true, + }, + }); + + const lineSeries = chart.addLineSeries({ + lineWidth: 2, + color: '#ff0000', + lineType: LightweightCharts.LineType.Simple, + }); + lineSeries.setData(generateLineData()); + + candlestickSeries = chart.addCandlestickSeries({ priceScaleId: 'left', wickColor: 'blue', borderColor: 'green' }); + const candleStickData = generateBars().map((bar, index) => { + if (index > 5) { return bar; } + return { ...bar, color: 'orange', wickColor: 'orange', borderColor: 'orange' }; + }); + candlestickSeries.setData(candleStickData); + + await awaitNewFrame(); + lineSeries.applyOptions({ lineType: LightweightCharts.LineType.Curved }); + + await awaitNewFrame(); + lineSeries.applyOptions({ lineType: LightweightCharts.LineType.WithSteps }); + + return Promise.resolve(); +} + +function afterInteractions() { + candlestickSeries.applyOptions({ + upColor: 'transparent', + downColor: 'rgba(-10, 0, 260, -0.1)', // incorrect rgba strings to test correction function + borderUpColor: 'rgba(0, 0, 0, 2)', + borderDownColor: 'black', + wickUpColor: 'black', + wickDownColor: 'black', + }); + return new Promise(resolve => { + requestAnimationFrame(resolve); + }); +} diff --git a/tests/e2e/coverage/test-cases/series/title.js b/tests/e2e/coverage/test-cases/series/title.js new file mode 100644 index 0000000000..642800f21e --- /dev/null +++ b/tests/e2e/coverage/test-cases/series/title.js @@ -0,0 +1,30 @@ +function interactionsToPerform() { + return []; +} + +let mainSeries; + +function beforeInteractions(container) { + const chart = LightweightCharts.createChart(container); + + mainSeries = chart.addBarSeries({ + title: 'Initial title', + priceFormat: { + type: 'percent', + }, + }); + + mainSeries.setData(generateBars()); + return new Promise(resolve => { + requestAnimationFrame(resolve); + }); +} + +function afterInteractions() { + mainSeries.applyOptions({ + title: 'Updated title', + }); + return new Promise(resolve => { + requestAnimationFrame(resolve); + }); +} diff --git a/tests/e2e/coverage/test-cases/series/update-data.js b/tests/e2e/coverage/test-cases/series/update-data.js new file mode 100644 index 0000000000..97c2461eda --- /dev/null +++ b/tests/e2e/coverage/test-cases/series/update-data.js @@ -0,0 +1,92 @@ +function interactionsToPerform() { + return []; +} + +async function awaitNewFrame() { + return new Promise(resolve => { + requestAnimationFrame(resolve); + }); +} + +let chart; +let lineSeries; +let leftSeries; +let lineData; +let barData; +let lastTime; + +function beforeInteractions(container) { + chart = LightweightCharts.createChart(container); + + lineSeries = chart.addLineSeries({ + lastPriceAnimation: LightweightCharts.LastPriceAnimationMode.Continuous, + }); + + lineData = generateLineData(); + lastTime = new Date(lineData[lineData.length - 1].time); + lineSeries.setData(lineData); + + leftSeries = chart.addCandlestickSeries({ + priceScaleId: 'left', + lastPriceAnimation: LightweightCharts.LastPriceAnimationMode.OnDataUpdate, + }); + + barData = generateBars(); + leftSeries.setData(barData); + + chart.timeScale().fitContent(); + + return Promise.resolve(); +} + +async function afterInteractions() { + lastTime.setUTCDate(lastTime.getUTCDate() + 1); + lineSeries.update({ + time: lastTime.toISOString().slice(0, 10), + value: 0.012, + }); + + lastTime.setUTCDate(lastTime.getUTCDate() + 1); + lineSeries.update({ + time: lastTime.toISOString().slice(0, 10), + value: 2411, + }); + + await awaitNewFrame(); + leftSeries.applyOptions({ + lastPriceAnimation: LightweightCharts.LastPriceAnimationMode.Disabled, + }); + + lastTime.setUTCDate(lastTime.getUTCDate() + 1); + lineSeries.update({ + time: lastTime.toISOString().slice(0, 10), + value: 1234, + }); + + await awaitNewFrame(); + chart.timeScale().scrollToRealTime(); + + lastTime.setUTCDate(lastTime.getUTCDate() + 1); + lineSeries.update({ + time: lastTime.toISOString().slice(0, 10), + value: 1234567, + }); + + await awaitNewFrame(); + + lastTime.setUTCDate(lastTime.getUTCDate() + 1); + lineSeries.update({ + time: lastTime.toISOString().slice(0, 10), + value: 12345678912, + }); + + leftSeries.update({ + ...barData[barData.length - 1], + close: barData[barData.length - 1].close - 10, + }); + leftSeries.update({ + ...barData[barData.length - 1], + time: barData[barData.length - 1].time + 3600, + }); + return Promise.resolve(); +} diff --git a/tests/e2e/coverage/test-cases/time-scale/bar-width.js b/tests/e2e/coverage/test-cases/time-scale/bar-width.js new file mode 100644 index 0000000000..5d7c1fbbac --- /dev/null +++ b/tests/e2e/coverage/test-cases/time-scale/bar-width.js @@ -0,0 +1,38 @@ +function interactionsToPerform() { + return [ + { action: 'scrollUp', target: 'pane' }, + { action: 'scrollUp', target: 'pane' }, + { action: 'scrollDown', target: 'pane' }, + { action: 'scrollDown', target: 'pane' }, + { action: 'scrollDown', target: 'pane' }, + { action: 'scrollDown', target: 'pane' }, + ]; +} + +let chart; + +function beforeInteractions(container) { + chart = LightweightCharts.createChart(container, { + timeScale: { + rightOffset: 10, + barSpacing: 3, + minBarSpacing: 2, + }, + }); + + const mainSeries = chart.addHistogramSeries(); + mainSeries.setData(generateHistogramData()); + + return new Promise(resolve => { + requestAnimationFrame(resolve); + }); +} + +function afterInteractions() { + chart.timeScale().applyOptions({ + minBarSpacing: 12, + }); + return new Promise(resolve => { + requestAnimationFrame(resolve); + }); +} diff --git a/tests/e2e/coverage/test-cases/time-scale/general.js b/tests/e2e/coverage/test-cases/time-scale/general.js new file mode 100644 index 0000000000..98ee4f2145 --- /dev/null +++ b/tests/e2e/coverage/test-cases/time-scale/general.js @@ -0,0 +1,106 @@ +function interactionsToPerform() { + return []; +} + +let chart; +let mainSeries; +let timeScale; +let data; + +function beforeInteractions(container) { + chart = LightweightCharts.createChart(container); + + mainSeries = chart.addCandlestickSeries(); + + data = generateBars(); + mainSeries.setData(data); + + timeScale = chart.timeScale(); + + timeScale.fitContent(); + timeScale.options(); + + const logical = timeScale.coordinateToLogical(300); + timeScale.logicalToCoordinate(logical); + + const time = timeScale.coordinateToTime(300); + timeScale.timeToCoordinate(time); + + timeScale.width(); + timeScale.height(); + + return Promise.resolve(); +} + +async function awaitNewFrame() { + return new Promise(resolve => { + requestAnimationFrame(resolve); + }); +} + +async function delay(time) { + return new Promise(resolve => { + setTimeout(resolve, time); + }); +} + +async function afterInteractions() { + timeScale.getVisibleRange(); + timeScale.setVisibleRange({ + from: data[0].time, + to: data[data.length - 1].time, + }); + + mainSeries.barsInLogicalRange(chart.timeScale().getVisibleLogicalRange()); + + timeScale.applyOptions({ fixLeftEdge: true }); + + timeScale.applyOptions({ + timeVisible: true, + secondsVisible: true, + }); + + await awaitNewFrame(); + + // set timeout + const pos = timeScale.scrollPosition(); + timeScale.scrollToPosition(pos - 20, false); + await awaitNewFrame(); + await delay(50); + timeScale.scrollToPosition(pos - 10, true); + + await awaitNewFrame(); + await delay(50); + + timeScale.scrollToRealTime(); + + await awaitNewFrame(); + await delay(50); + + chart.applyOptions({ + layout: { + fontFamily: undefined, + }, + timeScale: { + barSpacing: 12, + minBarSpacing: 2, + rightOffset: 4, + fixRightEdge: true, + fixLeftEdge: true, + }, + }); + + await awaitNewFrame(); + await delay(50); + + timeScale.resetTimeScale(); + + await awaitNewFrame(); + + chart.timeScale().applyOptions({ lockVisibleTimeRangeOnResize: true }); + chart.resize(200, 200); + + await awaitNewFrame(); + + return Promise.resolve(); +} diff --git a/tests/e2e/coverage/test-cases/time-scale/hidden.js b/tests/e2e/coverage/test-cases/time-scale/hidden.js new file mode 100644 index 0000000000..7000863d06 --- /dev/null +++ b/tests/e2e/coverage/test-cases/time-scale/hidden.js @@ -0,0 +1,32 @@ +function interactionsToPerform() { + return []; +} + +let chart; + +function beforeInteractions(container) { + chart = LightweightCharts.createChart(container, { + timeScale: { + visible: false, + }, + }); + + const mainSeries = chart.addCandlestickSeries(); + + mainSeries.setData(generateBars()); + + return new Promise(resolve => { + requestAnimationFrame(resolve); + }); +} + +function afterInteractions() { + chart.applyOptions({ + timeScale: { + visible: true, + }, + }); + return new Promise(resolve => { + requestAnimationFrame(resolve); + }); +} diff --git a/tests/e2e/coverage/test-cases/time-scale/seconds.js b/tests/e2e/coverage/test-cases/time-scale/seconds.js new file mode 100644 index 0000000000..635a56bf16 --- /dev/null +++ b/tests/e2e/coverage/test-cases/time-scale/seconds.js @@ -0,0 +1,40 @@ +function simpleData() { + return [ + { time: 1663740000, value: 10 }, + { time: 1663740005, value: 20 }, + { time: 1663740010, value: 30 }, + ]; +} + +function interactionsToPerform() { + return []; +} + +let chart; + +function beforeInteractions(container) { + chart = LightweightCharts.createChart(container, { + timeScale: { + secondsVisible: true, + }, + }); + + const mainSeries = chart.addLineSeries(); + + mainSeries.setData(simpleData()); + + return new Promise(resolve => { + requestAnimationFrame(resolve); + }); +} + +function afterInteractions() { + chart.applyOptions({ + timeScale: { + timeVisible: true, + }, + }); + return new Promise(resolve => { + requestAnimationFrame(resolve); + }); +} diff --git a/tests/e2e/graphics/README.md b/tests/e2e/graphics/README.md index fa03cd5e10..6e6bf7e81f 100644 --- a/tests/e2e/graphics/README.md +++ b/tests/e2e/graphics/README.md @@ -46,3 +46,14 @@ If file is local then local server will be runner to serve that file (see [serve Let's say you run your tests in that way - `./runner.js ./golden/standalone/module.js ./test/standalone/module.js`. After that in `.gendata/test-case-name/1.golden.html` you can find a HTML page. To open this page properly you can run `./tests/e2e/serve-static-files.js golden.js:./golden/standalone/module.js test.js:./test/standalone/module.js` and then open that page in the browser to debug. + +1. The following environmental variables can be used to adjust the test: + + - `PRODUCTION_BUILD`: Set to true if testing a Production build + - `DEVICE_PIXEL_RATIO`: Device pixel ratio to simulate during the test (number) + +1. You can set Mocha options from the command line arguments: + +```bash +node ./tests/e2e/graphics/runner.js ./path/to/golden/standalone/module.js ./path/to/test/standalone/module.js --bail --grep "add-series" +``` diff --git a/tests/e2e/graphics/graphics-test-cases.ts b/tests/e2e/graphics/graphics-test-cases.ts index f02517bb6f..3ad135c185 100644 --- a/tests/e2e/graphics/graphics-test-cases.ts +++ b/tests/e2e/graphics/graphics-test-cases.ts @@ -114,8 +114,16 @@ describe(`Graphics tests with devicePixelRatio=${devicePixelRatioStr} (${buildMo }); function registerTestCases(testCases: TestCase[], screenshoter: Screenshoter, outDir: string): void { + const attempts: Record = {}; + testCases.forEach((testCase: TestCase) => { + attempts[testCase.name] = 0; + }); + for (const testCase of testCases) { it(testCase.name, async () => { + const previousAttempts = attempts[testCase.name]; + attempts[testCase.name] += 1; + const testCaseOutDir = path.join(outDir, testCase.name); rmRf(testCaseOutDir); fs.mkdirSync(testCaseOutDir, { recursive: true }); @@ -130,12 +138,22 @@ function registerTestCases(testCases: TestCase[], screenshoter: Screenshoter, ou writeTestDataItem('1.golden.html', goldenPageContent); writeTestDataItem('2.test.html', testPageContent); + const errors: string[] = []; + const failedPages: string[] = []; + // run in parallel to increase speed const goldenScreenshotPromise = screenshoter.generateScreenshot(goldenPageContent); - const testScreenshotPromise = screenshoter.generateScreenshot(testPageContent); - const errors: string[] = []; - const failedPages: string[] = []; + if (previousAttempts) { + try { + // If a test has previously failed then attempt to run the tests in series (one at a time). + await goldenScreenshotPromise; + } catch { + // error will be caught again below and handled correctly there. + } + } + + const testScreenshotPromise = screenshoter.generateScreenshot(testPageContent); let goldenScreenshot: PNG | null = null; try { diff --git a/tests/e2e/graphics/helpers/screenshoter.ts b/tests/e2e/graphics/helpers/screenshoter.ts index ec10bf668c..1f10927a2b 100644 --- a/tests/e2e/graphics/helpers/screenshoter.ts +++ b/tests/e2e/graphics/helpers/screenshoter.ts @@ -9,6 +9,9 @@ import puppeteer, { Page, } from 'puppeteer'; +import { MouseEventParams } from '../../../../src/api/ichart-api'; +import { TestCaseWindow } from './testcase-window-type'; + const viewportWidth = 600; const viewportHeight = 600; @@ -67,19 +70,42 @@ export class Screenshoter { // wait for test case is ready await page.evaluate(() => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any,@typescript-eslint/no-unsafe-return,@typescript-eslint/no-unsafe-member-access - return (window as any).testCaseReady; + return (window as unknown as TestCaseWindow).testCaseReady; + }); + + // move mouse to top-left corner + await page.mouse.move(0, 0); + + const waitForMouseMove = page.evaluate(() => { + if ((window as unknown as TestCaseWindow).ignoreMouseMove) { return Promise.resolve(); } + return new Promise((resolve: (value: number[]) => void) => { + const chart = (window as unknown as TestCaseWindow).chart; + if (!chart) { + throw new Error('window variable `chart` is required unless `ignoreMouseMove` is set to true'); + } + chart.subscribeCrosshairMove((param: MouseEventParams) => { + const point = param.point; + if (!point) { return; } + if (point.x > 0 && point.y > 0) { + requestAnimationFrame(() => resolve([point.x, point.y] as number[])); + } + }); + }); }); // to avoid random cursor position await page.mouse.move(viewportWidth / 2, viewportHeight / 2); + await waitForMouseMove; + // let's wait until the next af to make sure that everything is repainted await page.evaluate(() => { return new Promise((resolve: () => void) => { window.requestAnimationFrame(() => { // and a little more time after af :) - setTimeout(resolve, 50); + // Note: This timeout value isn't part of the test and is only + // included to improve the reliability of the test. + setTimeout(resolve, 250); }); }); }); diff --git a/tests/e2e/graphics/helpers/testcase-window-type.ts b/tests/e2e/graphics/helpers/testcase-window-type.ts new file mode 100644 index 0000000000..ffef21d9a9 --- /dev/null +++ b/tests/e2e/graphics/helpers/testcase-window-type.ts @@ -0,0 +1,7 @@ +import { IChartApi } from '../../../../src/api/ichart-api'; + +export interface TestCaseWindow extends Window { + testCaseReady: void | Promise; + chart?: IChartApi; + ignoreMouseMove?: boolean; +} diff --git a/tests/e2e/graphics/runner.js b/tests/e2e/graphics/runner.js index 309f28a1ce..677149f73a 100755 --- a/tests/e2e/graphics/runner.js +++ b/tests/e2e/graphics/runner.js @@ -3,6 +3,9 @@ const fs = require('fs'); const path = require('path'); +const yargs = require('yargs/yargs'); +const argv = yargs(process.argv.slice(4)).argv; + const Mocha = require('mocha'); const serveLocalFiles = require('../serve-local-files').serveLocalFiles; @@ -16,7 +19,7 @@ mochaConfig.require.forEach(module => { require(module); }); -if (process.argv.length !== 4) { +if (process.argv.length < 4) { console.log('Usage: runner PATH_TO_GOLDEN_STANDALONE_MODULE PATH_TO_TEST_STANDALONE_MODULE'); process.exit(1); } @@ -48,11 +51,18 @@ process.env.TEST_STANDALONE_PATH = testStandalonePath; function runMocha(closeServer) { console.log('Running tests...'); + + /** @type Partial */ + const mochaOptions = Object.fromEntries( + Object.entries(argv).filter(entry => !['_', '$0'].includes(entry[0])) + ); + const mocha = new Mocha({ timeout: 20000, slow: 10000, reporter: mochaConfig.reporter, reporterOptions: mochaConfig._reporterOptions, + ...mochaOptions, }); if (mochaConfig.checkLeaks) { diff --git a/tests/e2e/graphics/test-cases/add-series-after-time.js b/tests/e2e/graphics/test-cases/add-series-after-time.js index a03d4bf11d..f55808c0a6 100644 --- a/tests/e2e/graphics/test-cases/add-series-after-time.js +++ b/tests/e2e/graphics/test-cases/add-series-after-time.js @@ -13,7 +13,9 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container, { + height: 500, width: 600, + }); return new Promise(resolve => { setTimeout(() => { diff --git a/tests/e2e/graphics/test-cases/api/getting-series-price-scale.js b/tests/e2e/graphics/test-cases/api/getting-series-price-scale.js index 6b9274f00e..424d6d1f86 100644 --- a/tests/e2e/graphics/test-cases/api/getting-series-price-scale.js +++ b/tests/e2e/graphics/test-cases/api/getting-series-price-scale.js @@ -1,5 +1,5 @@ function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const series = chart.addAreaSeries(); series.setData([ { time: '1990-04-24', value: 0 }, diff --git a/tests/e2e/graphics/test-cases/api/price-scale-width.js b/tests/e2e/graphics/test-cases/api/price-scale-width.js index a2a96df5ac..e3a729bd99 100644 --- a/tests/e2e/graphics/test-cases/api/price-scale-width.js +++ b/tests/e2e/graphics/test-cases/api/price-scale-width.js @@ -1,5 +1,5 @@ function runTestCase(container) { - const chart = LightweightCharts.createChart(container, { + const chart = window.chart = LightweightCharts.createChart(container, { rightPriceScale: { visible: true, }, 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 83d1bb7f43..cbb6558cd9 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,5 +1,9 @@ function compareAreaData(obj1, obj2) { - return obj1.time === obj2.time && obj1.value === obj2.value; + return obj1.time === obj2.time + && obj1.value === obj2.value + && obj1.lineColor === obj2.lineColor + && obj1.topColor === obj2.topColor + && obj1.bottomColor === obj2.bottomColor; } function compareLineData(obj1, obj2) { @@ -16,6 +20,17 @@ function compareCandlestickData(obj1, obj2) { && obj1.wickColor === obj2.wickColor; } +function compareBaselineData(obj1, obj2) { + return obj1.time === obj2.time + && obj1.value === obj2.value + && obj1.topFillColor1 === obj2.topFillColor1 + && obj1.topFillColor2 === obj2.topFillColor2 + && obj1.topLineColor === obj2.topLineColor + && obj1.bottomFillColor1 === obj2.bottomFillColor1 + && obj1.bottomFillColor2 === obj2.bottomFillColor2 + && obj1.bottomLineColor === obj2.bottomLineColor; +} + function generateCandlestickData() { const result = []; const time = new Date(Date.UTC(2018, 0, 1, 0, 0, 0, 0)); @@ -38,95 +53,102 @@ function generateCandlestickData() { return result; } -function runTestCase(container) { - const chart = LightweightCharts.createChart(container); +function checkSeries(series, data, compareItemsFn) { + const originalData = data.map(item => ({ ...item })); + const seriesType = series.seriesType(); - 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' }, - ]); + series.setData(data); 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 })}` + series.dataByIndex(-1) === null, + `${seriesType} series should return null for logical index (-1) that is outside of data` ); + 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' })}` + compareAreaData(series.dataByIndex(-1, LightweightCharts.MismatchDirection.NearestRight), series.dataByIndex(0)), + `${seriesType} series should return nearest right item if mismatch direction is MismatchDirection.NearestRight` ); - 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 })}` + series.dataByIndex(originalData.length) === null, + `${seriesType} should return null for logical index (${originalData.length}) that is outside of data` ); 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', - })}` + compareAreaData(series.dataByIndex(originalData.length, LightweightCharts.MismatchDirection.NearestLeft), series.dataByIndex(originalData.length - 1)), + `${seriesType} series should return nearest left item if mismatch direction is MismatchDirection.NearestLeft` + ); + + for (let i = 0; i < originalData.length; ++i) { + const item = series.dataByIndex(i); + const expectedItem = originalData[i]; + console.assert( + compareItemsFn(item, expectedItem), + `incorrect ${seriesType} series item at index ${i}: ${JSON.stringify(item)} !== ${JSON.stringify(expectedItem)}` + ); + } +} + +function runTestCase(container) { + const chart = window.chart = LightweightCharts.createChart(container); + + const lineSeries = chart.addLineSeries(); + checkSeries( + lineSeries, + [ + { 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' }, + ], + compareLineData + ); + + chart.removeSeries(lineSeries); + + const candlestickSeries = chart.addCandlestickSeries(); + + checkSeries( + candlestickSeries, + generateCandlestickData(), + compareCandlestickData ); 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 }, - { time: '1990-04-28', value: 3 }, - { time: '1990-04-29', value: 4 }, - { time: '1990-04-30', value: 5 }, - ]); - - 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( - compareAreaData(areaSeries.dataByIndex(-1, LightweightCharts.MismatchDirection.NearestRight), areaSeries.dataByIndex(0)), - 'should return nearest right item if mismatch direction is MismatchDirection.NearestRight' + + checkSeries( + areaSeries, + [ + { time: '1990-04-24', value: 0 }, + { time: '1990-04-25', value: 1 }, + { time: '1990-04-27', value: 2 }, + { time: '1990-04-28', value: 3, lineColor: '#FF0000', topColor: '#00FF00', bottomColor: '#0000FF' }, + { time: '1990-04-29', value: 4, lineColor: '#FF0000', topColor: '#00FF00', bottomColor: '#0000FF' }, + { time: '1990-04-30', value: 5, lineColor: '#FF0000', topColor: '#00FF00', bottomColor: '#0000FF' }, + ], + compareAreaData ); - 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( - compareAreaData(areaSeries.dataByIndex(6, LightweightCharts.MismatchDirection.NearestLeft), areaSeries.dataByIndex(5)), - 'should return nearest left item if mismatch direction is MismatchDirection.NearestLeft' + chart.removeSeries(areaSeries); + + const baselineSeries = chart.addBaselineSeries(); + + checkSeries( + baselineSeries, + [ + { time: '1990-04-24', value: 0 }, + { time: '1990-04-25', value: 1 }, + { time: '1990-04-27', value: 2 }, + { time: '1990-04-28', value: 3, topFillColor1: '#FFFFFF', topFillColor2: '#000000' }, + { time: '1990-04-29', value: 4, bottomFillColor1: '#FFFFFF', bottomFillColor2: '#000000' }, + { time: '1990-04-30', value: 5, topLineColor: '#FF0000', bottomLineColor: '#00FF00' }, + ], + compareBaselineData ); - chart.applyOptions({ - watermark: { - color: 'red', - visible: true, - text: `${areaSeries.dataByIndex(3).time}`, - }, - }); + chart.removeSeries(baselineSeries); } diff --git a/tests/e2e/graphics/test-cases/api/series-markers.js b/tests/e2e/graphics/test-cases/api/series-markers.js index ce10b387ad..cb849df6ec 100644 --- a/tests/e2e/graphics/test-cases/api/series-markers.js +++ b/tests/e2e/graphics/test-cases/api/series-markers.js @@ -3,7 +3,7 @@ function compare(markers, seriesApiMarkers) { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const series = chart.addAreaSeries(); series.setData([ { time: '1990-04-24', value: 0 }, diff --git a/tests/e2e/graphics/test-cases/api/subscribe-crosshair-move.js b/tests/e2e/graphics/test-cases/api/subscribe-crosshair-move.js index ab07faf320..4d9b8d5686 100644 --- a/tests/e2e/graphics/test-cases/api/subscribe-crosshair-move.js +++ b/tests/e2e/graphics/test-cases/api/subscribe-crosshair-move.js @@ -1,5 +1,5 @@ function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const series = chart.addAreaSeries(); series.setData([ { time: '1990-04-24', value: 0 }, diff --git a/tests/e2e/graphics/test-cases/api/time-scale-coordinate-to-logical.js b/tests/e2e/graphics/test-cases/api/time-scale-coordinate-to-logical.js index 07c2eab017..4f7ee6a9d2 100644 --- a/tests/e2e/graphics/test-cases/api/time-scale-coordinate-to-logical.js +++ b/tests/e2e/graphics/test-cases/api/time-scale-coordinate-to-logical.js @@ -1,6 +1,9 @@ +// Ignore the mouse movement check because height of chart is too short +window.ignoreMouseMove = true; + function runTestCase(container) { const width = 400; - const chart = LightweightCharts.createChart(container, { + const chart = window.chart = LightweightCharts.createChart(container, { width: width, height: 200, rightPriceScale: { diff --git a/tests/e2e/graphics/test-cases/api/time-scale-coordinate-to-time.js b/tests/e2e/graphics/test-cases/api/time-scale-coordinate-to-time.js index b58553dd5d..5f94065354 100644 --- a/tests/e2e/graphics/test-cases/api/time-scale-coordinate-to-time.js +++ b/tests/e2e/graphics/test-cases/api/time-scale-coordinate-to-time.js @@ -1,6 +1,9 @@ +// Ignore the mouse movement check because height of chart is too short +window.ignoreMouseMove = true; + function runTestCase(container) { const width = 400; - const chart = LightweightCharts.createChart(container, { + const chart = window.chart = LightweightCharts.createChart(container, { width: width, height: 200, rightPriceScale: { diff --git a/tests/e2e/graphics/test-cases/api/time-scale-logical-to-coordinate.js b/tests/e2e/graphics/test-cases/api/time-scale-logical-to-coordinate.js index 3470412b8c..7196df4f6d 100644 --- a/tests/e2e/graphics/test-cases/api/time-scale-logical-to-coordinate.js +++ b/tests/e2e/graphics/test-cases/api/time-scale-logical-to-coordinate.js @@ -2,9 +2,12 @@ function inRange(from, value, to) { return !isNaN(value) && from < value && value < to; } +// Ignore the mouse movement check because height of chart is too short +window.ignoreMouseMove = true; + function runTestCase(container) { const width = 400; - const chart = LightweightCharts.createChart(container, { + const chart = window.chart = LightweightCharts.createChart(container, { width: width, height: 200, rightPriceScale: { diff --git a/tests/e2e/graphics/test-cases/api/time-scale-time-to-coordinate.js b/tests/e2e/graphics/test-cases/api/time-scale-time-to-coordinate.js index c24feb7194..fd27cf2a77 100644 --- a/tests/e2e/graphics/test-cases/api/time-scale-time-to-coordinate.js +++ b/tests/e2e/graphics/test-cases/api/time-scale-time-to-coordinate.js @@ -2,9 +2,12 @@ function inRange(from, value, to) { return !isNaN(value) && from < value && value < to; } +// Ignore the mouse movement check because height of chart is too short +window.ignoreMouseMove = true; + function runTestCase(container) { const width = 400; - const chart = LightweightCharts.createChart(container, { + const chart = window.chart = LightweightCharts.createChart(container, { width: width, height: 200, rightPriceScale: { diff --git a/tests/e2e/graphics/test-cases/applying-options/apply-do-not-draw-price-ticks.js b/tests/e2e/graphics/test-cases/applying-options/apply-do-not-draw-price-ticks.js index f0bfe15127..ee0d317990 100644 --- a/tests/e2e/graphics/test-cases/applying-options/apply-do-not-draw-price-ticks.js +++ b/tests/e2e/graphics/test-cases/applying-options/apply-do-not-draw-price-ticks.js @@ -13,9 +13,9 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container, { + const chart = window.chart = LightweightCharts.createChart(container, { rightPriceScale: { - ticksVisible: false, + ticksVisible: true, }, }); @@ -27,7 +27,7 @@ function runTestCase(container) { setTimeout(() => { chart.applyOptions({ rightPriceScale: { - ticksVisible: true, + ticksVisible: false, }, }); diff --git a/tests/e2e/graphics/test-cases/applying-options/change-bar-colors.js b/tests/e2e/graphics/test-cases/applying-options/change-bar-colors.js index 95ef957e61..7c37f3eab2 100644 --- a/tests/e2e/graphics/test-cases/applying-options/change-bar-colors.js +++ b/tests/e2e/graphics/test-cases/applying-options/change-bar-colors.js @@ -39,7 +39,7 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container, { + const chart = window.chart = LightweightCharts.createChart(container, { timeScale: { barSpacing: 40, timeVisible: true, diff --git a/tests/e2e/graphics/test-cases/applying-options/change-candlestick-colors.js b/tests/e2e/graphics/test-cases/applying-options/change-candlestick-colors.js index d06f378200..f3d760299e 100644 --- a/tests/e2e/graphics/test-cases/applying-options/change-candlestick-colors.js +++ b/tests/e2e/graphics/test-cases/applying-options/change-candlestick-colors.js @@ -39,7 +39,7 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container, { + const chart = window.chart = LightweightCharts.createChart(container, { timeScale: { barSpacing: 40, timeVisible: true, diff --git a/tests/e2e/graphics/test-cases/applying-options/empty-time-scale-options.js b/tests/e2e/graphics/test-cases/applying-options/empty-time-scale-options.js index b77a66dedd..c4f6f449da 100644 --- a/tests/e2e/graphics/test-cases/applying-options/empty-time-scale-options.js +++ b/tests/e2e/graphics/test-cases/applying-options/empty-time-scale-options.js @@ -23,7 +23,7 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const mainSeries = chart.addCandlestickSeries(); diff --git a/tests/e2e/graphics/test-cases/applying-options/fix-both-edges-then-scroll.js b/tests/e2e/graphics/test-cases/applying-options/fix-both-edges-then-scroll.js index 66a6699ef3..efab161ad8 100644 --- a/tests/e2e/graphics/test-cases/applying-options/fix-both-edges-then-scroll.js +++ b/tests/e2e/graphics/test-cases/applying-options/fix-both-edges-then-scroll.js @@ -13,7 +13,7 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const mainSeries = chart.addLineSeries(); mainSeries.setData(generateData()); diff --git a/tests/e2e/graphics/test-cases/applying-options/fix-both-edges-time-scale-labels.js b/tests/e2e/graphics/test-cases/applying-options/fix-both-edges-time-scale-labels.js index 804759bf99..493c7f2c8c 100644 --- a/tests/e2e/graphics/test-cases/applying-options/fix-both-edges-time-scale-labels.js +++ b/tests/e2e/graphics/test-cases/applying-options/fix-both-edges-time-scale-labels.js @@ -30,7 +30,7 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container, { + const chart = window.chart = LightweightCharts.createChart(container, { timeScale: { fixLeftEdge: true, fixRightEdge: true, diff --git a/tests/e2e/graphics/test-cases/applying-options/fix-both-edges.js b/tests/e2e/graphics/test-cases/applying-options/fix-both-edges.js index 51cb3a6c69..c0267b3aa8 100644 --- a/tests/e2e/graphics/test-cases/applying-options/fix-both-edges.js +++ b/tests/e2e/graphics/test-cases/applying-options/fix-both-edges.js @@ -23,7 +23,7 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container, { + const chart = window.chart = LightweightCharts.createChart(container, { timeScale: { fixLeftEdge: true, fixRightEdge: true, diff --git a/tests/e2e/graphics/test-cases/applying-options/fix-left-edge.js b/tests/e2e/graphics/test-cases/applying-options/fix-left-edge.js index 9dcb9db280..239e5cf09f 100644 --- a/tests/e2e/graphics/test-cases/applying-options/fix-left-edge.js +++ b/tests/e2e/graphics/test-cases/applying-options/fix-left-edge.js @@ -13,7 +13,7 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const mainSeries = chart.addLineSeries(); mainSeries.setData(generateData()); diff --git a/tests/e2e/graphics/test-cases/applying-options/increase-min-bar-spacing.js b/tests/e2e/graphics/test-cases/applying-options/increase-min-bar-spacing.js index fdb84b328c..63b10113b0 100644 --- a/tests/e2e/graphics/test-cases/applying-options/increase-min-bar-spacing.js +++ b/tests/e2e/graphics/test-cases/applying-options/increase-min-bar-spacing.js @@ -23,7 +23,7 @@ function generateData(startValue) { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container, { + const chart = window.chart = LightweightCharts.createChart(container, { timeScale: { minBarSpacing: 0.001, }, diff --git a/tests/e2e/graphics/test-cases/applying-options/make-series-hidden.js b/tests/e2e/graphics/test-cases/applying-options/make-series-hidden.js index d69acf4ad0..447b9f627d 100644 --- a/tests/e2e/graphics/test-cases/applying-options/make-series-hidden.js +++ b/tests/e2e/graphics/test-cases/applying-options/make-series-hidden.js @@ -14,7 +14,7 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container, { + const chart = window.chart = LightweightCharts.createChart(container, { rightPriceScale: { mode: LightweightCharts.PriceScaleMode.IndexedTo100, }, diff --git a/tests/e2e/graphics/test-cases/applying-options/make-series-visible.js b/tests/e2e/graphics/test-cases/applying-options/make-series-visible.js index 620025bfc2..933f8ebea9 100644 --- a/tests/e2e/graphics/test-cases/applying-options/make-series-visible.js +++ b/tests/e2e/graphics/test-cases/applying-options/make-series-visible.js @@ -14,7 +14,7 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container, { + const chart = window.chart = LightweightCharts.createChart(container, { rightPriceScale: { mode: LightweightCharts.PriceScaleMode.IndexedTo100, }, diff --git a/tests/e2e/graphics/test-cases/applying-options/move-price-scale.js b/tests/e2e/graphics/test-cases/applying-options/move-price-scale.js index 60c213ca99..3ce6a77f14 100644 --- a/tests/e2e/graphics/test-cases/applying-options/move-price-scale.js +++ b/tests/e2e/graphics/test-cases/applying-options/move-price-scale.js @@ -13,7 +13,7 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container, { + const chart = window.chart = LightweightCharts.createChart(container, { rightPriceScale: { visible: true }, leftPriceScale: { visible: false }, }); diff --git a/tests/e2e/graphics/test-cases/applying-options/move-series-from-right-to-left.js b/tests/e2e/graphics/test-cases/applying-options/move-series-from-right-to-left.js index 9a36127f45..93e3dc4c5b 100644 --- a/tests/e2e/graphics/test-cases/applying-options/move-series-from-right-to-left.js +++ b/tests/e2e/graphics/test-cases/applying-options/move-series-from-right-to-left.js @@ -14,7 +14,7 @@ function generateData(offset) { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container, { + const chart = window.chart = LightweightCharts.createChart(container, { leftPriceScale: { visible: true, }, diff --git a/tests/e2e/graphics/test-cases/applying-options/move-series-to-overlay.js b/tests/e2e/graphics/test-cases/applying-options/move-series-to-overlay.js index 623a6cf8fe..60475d9fe0 100644 --- a/tests/e2e/graphics/test-cases/applying-options/move-series-to-overlay.js +++ b/tests/e2e/graphics/test-cases/applying-options/move-series-to-overlay.js @@ -14,7 +14,7 @@ function generateData(offset) { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const firstSeries = chart.addLineSeries({ lineWidth: 1, diff --git a/tests/e2e/graphics/test-cases/applying-options/reduce-min-bar-spacing.js b/tests/e2e/graphics/test-cases/applying-options/reduce-min-bar-spacing.js index 93bd5905c4..e84799cfc4 100644 --- a/tests/e2e/graphics/test-cases/applying-options/reduce-min-bar-spacing.js +++ b/tests/e2e/graphics/test-cases/applying-options/reduce-min-bar-spacing.js @@ -23,7 +23,7 @@ function generateData(startValue) { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const mainSeries = chart.addBarSeries(); diff --git a/tests/e2e/graphics/test-cases/applying-options/scale-margins-for-overlay-series.js b/tests/e2e/graphics/test-cases/applying-options/scale-margins-for-overlay-series.js index ba401ea07f..59a13d1eb6 100644 --- a/tests/e2e/graphics/test-cases/applying-options/scale-margins-for-overlay-series.js +++ b/tests/e2e/graphics/test-cases/applying-options/scale-margins-for-overlay-series.js @@ -14,7 +14,7 @@ function generateData(valueOffset) { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const firstSeries = chart.addLineSeries({ color: 'blue', diff --git a/tests/e2e/graphics/test-cases/applying-options/scroll-to-future-then-fix-right-edge.js b/tests/e2e/graphics/test-cases/applying-options/scroll-to-future-then-fix-right-edge.js index b846970a1a..f1df90b373 100644 --- a/tests/e2e/graphics/test-cases/applying-options/scroll-to-future-then-fix-right-edge.js +++ b/tests/e2e/graphics/test-cases/applying-options/scroll-to-future-then-fix-right-edge.js @@ -13,7 +13,7 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const mainSeries = chart.addLineSeries(); mainSeries.setData(generateData()); diff --git a/tests/e2e/graphics/test-cases/applying-options/scroll-to-past-then-fix-right-edge.js b/tests/e2e/graphics/test-cases/applying-options/scroll-to-past-then-fix-right-edge.js index 8720fe7eb1..86ae0ab972 100644 --- a/tests/e2e/graphics/test-cases/applying-options/scroll-to-past-then-fix-right-edge.js +++ b/tests/e2e/graphics/test-cases/applying-options/scroll-to-past-then-fix-right-edge.js @@ -13,7 +13,7 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const mainSeries = chart.addLineSeries(); mainSeries.setData(generateData()); diff --git a/tests/e2e/graphics/test-cases/applying-options/series-price-format.js b/tests/e2e/graphics/test-cases/applying-options/series-price-format.js index b2b770fa6a..211d457294 100644 --- a/tests/e2e/graphics/test-cases/applying-options/series-price-format.js +++ b/tests/e2e/graphics/test-cases/applying-options/series-price-format.js @@ -13,7 +13,7 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const mainSeries = chart.addLineSeries({ priceFormat: { diff --git a/tests/e2e/graphics/test-cases/applying-options/series-title.js b/tests/e2e/graphics/test-cases/applying-options/series-title.js index 1701e3ec1e..b79dc1a9bb 100644 --- a/tests/e2e/graphics/test-cases/applying-options/series-title.js +++ b/tests/e2e/graphics/test-cases/applying-options/series-title.js @@ -14,7 +14,7 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const firstSeries = chart.addLineSeries({ title: 'Initial title', diff --git a/tests/e2e/graphics/test-cases/applying-options/unfix-right-edge-then-scroll-to-future.js b/tests/e2e/graphics/test-cases/applying-options/unfix-right-edge-then-scroll-to-future.js index 8448ad8a56..5ba22a33ae 100644 --- a/tests/e2e/graphics/test-cases/applying-options/unfix-right-edge-then-scroll-to-future.js +++ b/tests/e2e/graphics/test-cases/applying-options/unfix-right-edge-then-scroll-to-future.js @@ -13,7 +13,7 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const mainSeries = chart.addLineSeries(); mainSeries.setData(generateData()); diff --git a/tests/e2e/graphics/test-cases/applying-options/update-chart-width.js b/tests/e2e/graphics/test-cases/applying-options/update-chart-width.js index 8f9da9adb4..37c36b317b 100644 --- a/tests/e2e/graphics/test-cases/applying-options/update-chart-width.js +++ b/tests/e2e/graphics/test-cases/applying-options/update-chart-width.js @@ -24,7 +24,7 @@ function generateData() { function runTestCase(container) { const box = container.getBoundingClientRect(); - const chart = LightweightCharts.createChart(container, { + const chart = window.chart = LightweightCharts.createChart(container, { width: 2000, height: box.height, timeScale: { diff --git a/tests/e2e/graphics/test-cases/applying-options/watermark.js b/tests/e2e/graphics/test-cases/applying-options/watermark.js index 9d92f6afd1..b09403a39a 100644 --- a/tests/e2e/graphics/test-cases/applying-options/watermark.js +++ b/tests/e2e/graphics/test-cases/applying-options/watermark.js @@ -23,7 +23,7 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const mainSeries = chart.addCandlestickSeries(); diff --git a/tests/e2e/graphics/test-cases/applying-options/zoom-in-then-fix-both-edges.js b/tests/e2e/graphics/test-cases/applying-options/zoom-in-then-fix-both-edges.js index 761c900edf..5e76280f00 100644 --- a/tests/e2e/graphics/test-cases/applying-options/zoom-in-then-fix-both-edges.js +++ b/tests/e2e/graphics/test-cases/applying-options/zoom-in-then-fix-both-edges.js @@ -13,7 +13,7 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const mainSeries = chart.addLineSeries(); mainSeries.setData(generateData()); diff --git a/tests/e2e/graphics/test-cases/applying-options/zoom-out-then-fix-both-edges.js b/tests/e2e/graphics/test-cases/applying-options/zoom-out-then-fix-both-edges.js index 0ed1a94a5c..61af24b82f 100644 --- a/tests/e2e/graphics/test-cases/applying-options/zoom-out-then-fix-both-edges.js +++ b/tests/e2e/graphics/test-cases/applying-options/zoom-out-then-fix-both-edges.js @@ -13,7 +13,7 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const mainSeries = chart.addLineSeries(); mainSeries.setData(generateData()); diff --git a/tests/e2e/graphics/test-cases/correct-price-range-in-autoscale.js b/tests/e2e/graphics/test-cases/correct-price-range-in-autoscale.js index f99ab8ad06..bf6658b7d9 100644 --- a/tests/e2e/graphics/test-cases/correct-price-range-in-autoscale.js +++ b/tests/e2e/graphics/test-cases/correct-price-range-in-autoscale.js @@ -15,7 +15,7 @@ function generateData(valueOffset, daysStep) { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const firstSeries = chart.addLineSeries(); const secondSeries = chart.addLineSeries(); diff --git a/tests/e2e/graphics/test-cases/data-validation.js b/tests/e2e/graphics/test-cases/data-validation.js index 68a8eed6dd..b46203d274 100644 --- a/tests/e2e/graphics/test-cases/data-validation.js +++ b/tests/e2e/graphics/test-cases/data-validation.js @@ -1,5 +1,9 @@ function runTestCase(container) { if (window.BUILD_MODE === 'production') { + // Ignore the mouse movement check because we don't run this test on production + window.ignoreMouseMove = true; + + // don't run this test on production build. return; } @@ -10,7 +14,7 @@ function runTestCase(container) { // passed } - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const lineSeries = chart.addLineSeries(); const barSeries = chart.addBarSeries(); diff --git a/tests/e2e/graphics/test-cases/degenerative-horizontal-series-with-integer-min-tick/indexed-to-100-scale.js b/tests/e2e/graphics/test-cases/degenerative-horizontal-series-with-integer-min-tick/indexed-to-100-scale.js index af9297cacc..866916fb46 100644 --- a/tests/e2e/graphics/test-cases/degenerative-horizontal-series-with-integer-min-tick/indexed-to-100-scale.js +++ b/tests/e2e/graphics/test-cases/degenerative-horizontal-series-with-integer-min-tick/indexed-to-100-scale.js @@ -16,7 +16,7 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container, { + const chart = window.chart = LightweightCharts.createChart(container, { rightPriceScale: { mode: LightweightCharts.PriceScaleMode.IndexedTo100, }, diff --git a/tests/e2e/graphics/test-cases/degenerative-horizontal-series-with-integer-min-tick/normal-scale.js b/tests/e2e/graphics/test-cases/degenerative-horizontal-series-with-integer-min-tick/normal-scale.js index f5d6fad0cf..1c9c116cf4 100644 --- a/tests/e2e/graphics/test-cases/degenerative-horizontal-series-with-integer-min-tick/normal-scale.js +++ b/tests/e2e/graphics/test-cases/degenerative-horizontal-series-with-integer-min-tick/normal-scale.js @@ -16,7 +16,7 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container, { + const chart = window.chart = LightweightCharts.createChart(container, { rightPriceScale: { mode: LightweightCharts.PriceScaleMode.Normal, }, diff --git a/tests/e2e/graphics/test-cases/degenerative-horizontal-series-with-integer-min-tick/percentage-scale.js b/tests/e2e/graphics/test-cases/degenerative-horizontal-series-with-integer-min-tick/percentage-scale.js index 12e163fc9d..a7e3aee282 100644 --- a/tests/e2e/graphics/test-cases/degenerative-horizontal-series-with-integer-min-tick/percentage-scale.js +++ b/tests/e2e/graphics/test-cases/degenerative-horizontal-series-with-integer-min-tick/percentage-scale.js @@ -16,7 +16,7 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container, { + const chart = window.chart = LightweightCharts.createChart(container, { rightPriceScale: { mode: LightweightCharts.PriceScaleMode.Percentage, }, diff --git a/tests/e2e/graphics/test-cases/fit-content-with-few-data.js b/tests/e2e/graphics/test-cases/fit-content-with-few-data.js index 744b7fc45f..4873cfebe0 100644 --- a/tests/e2e/graphics/test-cases/fit-content-with-few-data.js +++ b/tests/e2e/graphics/test-cases/fit-content-with-few-data.js @@ -14,7 +14,7 @@ function generateData(valueOffset, count) { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const series = chart.addLineSeries(); diff --git a/tests/e2e/graphics/test-cases/fit-content-with-lot-data.js b/tests/e2e/graphics/test-cases/fit-content-with-lot-data.js index 405d85592c..3b6bfca8aa 100644 --- a/tests/e2e/graphics/test-cases/fit-content-with-lot-data.js +++ b/tests/e2e/graphics/test-cases/fit-content-with-lot-data.js @@ -14,7 +14,7 @@ function generateData(valueOffset, count) { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container, { + const chart = window.chart = LightweightCharts.createChart(container, { leftPriceScale: { visible: true, ticksVisible: false, diff --git a/tests/e2e/graphics/test-cases/incorrect-autoscale-when-prices-are-small.js b/tests/e2e/graphics/test-cases/incorrect-autoscale-when-prices-are-small.js index c8991f8f56..4a6a719f70 100644 --- a/tests/e2e/graphics/test-cases/incorrect-autoscale-when-prices-are-small.js +++ b/tests/e2e/graphics/test-cases/incorrect-autoscale-when-prices-are-small.js @@ -18,7 +18,7 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const firstSeries = chart.addLineSeries(); const secondSeries = chart.addLineSeries(); diff --git a/tests/e2e/graphics/test-cases/initial-options/base-line-style.js b/tests/e2e/graphics/test-cases/initial-options/base-line-style.js index b4930d2c61..2a23d23f99 100644 --- a/tests/e2e/graphics/test-cases/initial-options/base-line-style.js +++ b/tests/e2e/graphics/test-cases/initial-options/base-line-style.js @@ -14,7 +14,7 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container, { + const chart = window.chart = LightweightCharts.createChart(container, { rightPriceScale: { mode: LightweightCharts.PriceScaleMode.Percentage, }, diff --git a/tests/e2e/graphics/test-cases/initial-options/chart-width-and-height.js b/tests/e2e/graphics/test-cases/initial-options/chart-width-and-height.js index 17fef0592e..6c425081d1 100644 --- a/tests/e2e/graphics/test-cases/initial-options/chart-width-and-height.js +++ b/tests/e2e/graphics/test-cases/initial-options/chart-width-and-height.js @@ -13,6 +13,24 @@ function generateData() { return res; } +// Ignore the mouse movement check because we are generating multiple charts +window.ignoreMouseMove = true; + +const chartOptionsToHideCrosshair = { + vertLine: { + visible: false, + labelVisible: false, + }, + horzLine: { + visible: false, + labelVisible: false, + }, +}; + +const seriesOptionsToHideCrosshair = { + crosshairMarkerVisible: false, +}; + function runTestCase(container) { const configs = [{}, { width: 500 }, { height: 100 }, { width: 500, height: 100 }]; @@ -27,8 +45,8 @@ function runTestCase(container) { container.appendChild(box); - const chart = LightweightCharts.createChart(box, config); - const mainSeries = chart.addAreaSeries(); + const chart = LightweightCharts.createChart(box, { ...config, ...chartOptionsToHideCrosshair }); + const mainSeries = chart.addAreaSeries(seriesOptionsToHideCrosshair); mainSeries.setData(generateData()); }); diff --git a/tests/e2e/graphics/test-cases/initial-options/crosshair-label-color-with-transparency.js b/tests/e2e/graphics/test-cases/initial-options/crosshair-label-color-with-transparency.js index 4c28d50d6f..663cb016f3 100644 --- a/tests/e2e/graphics/test-cases/initial-options/crosshair-label-color-with-transparency.js +++ b/tests/e2e/graphics/test-cases/initial-options/crosshair-label-color-with-transparency.js @@ -13,7 +13,7 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container, { + const chart = window.chart = LightweightCharts.createChart(container, { crosshair: { vertLine: { labelBackgroundColor: 'rgba(123, 123, 123, 0.5)', diff --git a/tests/e2e/graphics/test-cases/initial-options/crosshair-label-color.js b/tests/e2e/graphics/test-cases/initial-options/crosshair-label-color.js index 40db51d957..377c7aa095 100644 --- a/tests/e2e/graphics/test-cases/initial-options/crosshair-label-color.js +++ b/tests/e2e/graphics/test-cases/initial-options/crosshair-label-color.js @@ -13,7 +13,7 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container, { + const chart = window.chart = LightweightCharts.createChart(container, { crosshair: { vertLine: { labelBackgroundColor: '#ffffff', diff --git a/tests/e2e/graphics/test-cases/initial-options/crosshair.js b/tests/e2e/graphics/test-cases/initial-options/crosshair.js index 36ad2bf920..b7f8b76726 100644 --- a/tests/e2e/graphics/test-cases/initial-options/crosshair.js +++ b/tests/e2e/graphics/test-cases/initial-options/crosshair.js @@ -13,7 +13,7 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container, { + const chart = window.chart = LightweightCharts.createChart(container, { crosshair: { vertLine: { color: '#ff0000', diff --git a/tests/e2e/graphics/test-cases/initial-options/custom-date-format.js b/tests/e2e/graphics/test-cases/initial-options/custom-date-format.js index 60a98a090d..5765403da1 100644 --- a/tests/e2e/graphics/test-cases/initial-options/custom-date-format.js +++ b/tests/e2e/graphics/test-cases/initial-options/custom-date-format.js @@ -13,7 +13,7 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container, { + const chart = window.chart = LightweightCharts.createChart(container, { localization: { dateFormat: 'Year: yy (MM-yyyy)', }, diff --git a/tests/e2e/graphics/test-cases/initial-options/custom-price-format-with-extending-axis.js b/tests/e2e/graphics/test-cases/initial-options/custom-price-format-with-extending-axis.js index 9fb7da8d14..dcabc34ea2 100644 --- a/tests/e2e/graphics/test-cases/initial-options/custom-price-format-with-extending-axis.js +++ b/tests/e2e/graphics/test-cases/initial-options/custom-price-format-with-extending-axis.js @@ -19,7 +19,7 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const series = chart.addLineSeries({ priceFormat: { diff --git a/tests/e2e/graphics/test-cases/initial-options/custom-price-format.js b/tests/e2e/graphics/test-cases/initial-options/custom-price-format.js index 438c8984b9..073ba98a7d 100644 --- a/tests/e2e/graphics/test-cases/initial-options/custom-price-format.js +++ b/tests/e2e/graphics/test-cases/initial-options/custom-price-format.js @@ -13,7 +13,7 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const areaSeries = chart.addAreaSeries({ priceFormat: { diff --git a/tests/e2e/graphics/test-cases/initial-options/date-format.js b/tests/e2e/graphics/test-cases/initial-options/date-format.js index 11f86a0f24..04122350f3 100644 --- a/tests/e2e/graphics/test-cases/initial-options/date-format.js +++ b/tests/e2e/graphics/test-cases/initial-options/date-format.js @@ -13,7 +13,7 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container, { + const chart = window.chart = LightweightCharts.createChart(container, { localization: { dateFormat: 'MMM dd, yyyy', }, diff --git a/tests/e2e/graphics/test-cases/initial-options/do-not-draw-price-ticks.js b/tests/e2e/graphics/test-cases/initial-options/draw-price-ticks.js similarity index 80% rename from tests/e2e/graphics/test-cases/initial-options/do-not-draw-price-ticks.js rename to tests/e2e/graphics/test-cases/initial-options/draw-price-ticks.js index e9b2a61783..f306e7ec33 100644 --- a/tests/e2e/graphics/test-cases/initial-options/do-not-draw-price-ticks.js +++ b/tests/e2e/graphics/test-cases/initial-options/draw-price-ticks.js @@ -13,9 +13,9 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container, { + const chart = window.chart = LightweightCharts.createChart(container, { rightPriceScale: { - ticksVisible: false, + ticksVisible: true, }, }); diff --git a/tests/e2e/graphics/test-cases/initial-options/do-not-draw-time-ticks.js b/tests/e2e/graphics/test-cases/initial-options/draw-time-ticks.js similarity index 80% rename from tests/e2e/graphics/test-cases/initial-options/do-not-draw-time-ticks.js rename to tests/e2e/graphics/test-cases/initial-options/draw-time-ticks.js index 11fc732ebc..95ede3e2fb 100644 --- a/tests/e2e/graphics/test-cases/initial-options/do-not-draw-time-ticks.js +++ b/tests/e2e/graphics/test-cases/initial-options/draw-time-ticks.js @@ -13,9 +13,9 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container, { + const chart = window.chart = LightweightCharts.createChart(container, { timeScale: { - ticksVisible: false, + ticksVisible: true, }, }); diff --git a/tests/e2e/graphics/test-cases/initial-options/fat-bars.js b/tests/e2e/graphics/test-cases/initial-options/fat-bars.js index 2725158368..d398be6482 100644 --- a/tests/e2e/graphics/test-cases/initial-options/fat-bars.js +++ b/tests/e2e/graphics/test-cases/initial-options/fat-bars.js @@ -23,7 +23,7 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container, { + const chart = window.chart = LightweightCharts.createChart(container, { timeScale: { barSpacing: 20, }, diff --git a/tests/e2e/graphics/test-cases/initial-options/fix-left-edge-and-update-data.js b/tests/e2e/graphics/test-cases/initial-options/fix-left-edge-and-update-data.js index 84fa28a28f..a16827bfa6 100644 --- a/tests/e2e/graphics/test-cases/initial-options/fix-left-edge-and-update-data.js +++ b/tests/e2e/graphics/test-cases/initial-options/fix-left-edge-and-update-data.js @@ -1,5 +1,5 @@ function runTestCase(container) { - const chart = LightweightCharts.createChart(container, { + const chart = window.chart = LightweightCharts.createChart(container, { timeScale: { fixLeftEdge: true, }, diff --git a/tests/e2e/graphics/test-cases/initial-options/fix-left-edge.js b/tests/e2e/graphics/test-cases/initial-options/fix-left-edge.js index fd07280771..8e14486acb 100644 --- a/tests/e2e/graphics/test-cases/initial-options/fix-left-edge.js +++ b/tests/e2e/graphics/test-cases/initial-options/fix-left-edge.js @@ -13,7 +13,7 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container, { + const chart = window.chart = LightweightCharts.createChart(container, { timeScale: { fixLeftEdge: true, }, diff --git a/tests/e2e/graphics/test-cases/initial-options/gradient-background.js b/tests/e2e/graphics/test-cases/initial-options/gradient-background.js index 6ca494e1b5..570e3a47a8 100644 --- a/tests/e2e/graphics/test-cases/initial-options/gradient-background.js +++ b/tests/e2e/graphics/test-cases/initial-options/gradient-background.js @@ -14,7 +14,7 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container, { + const chart = window.chart = LightweightCharts.createChart(container, { layout: { background: { type: LightweightCharts.ColorType.VerticalGradient, diff --git a/tests/e2e/graphics/test-cases/initial-options/invalid-bar-spacing.js b/tests/e2e/graphics/test-cases/initial-options/invalid-bar-spacing.js index c0f6fafbbc..71620b4321 100644 --- a/tests/e2e/graphics/test-cases/initial-options/invalid-bar-spacing.js +++ b/tests/e2e/graphics/test-cases/initial-options/invalid-bar-spacing.js @@ -23,7 +23,7 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container, { + const chart = window.chart = LightweightCharts.createChart(container, { timeScale: { barSpacing: 1000, }, diff --git a/tests/e2e/graphics/test-cases/initial-options/left-price-scale.js b/tests/e2e/graphics/test-cases/initial-options/left-price-scale.js index 55a576c62d..038c785f6e 100644 --- a/tests/e2e/graphics/test-cases/initial-options/left-price-scale.js +++ b/tests/e2e/graphics/test-cases/initial-options/left-price-scale.js @@ -14,7 +14,7 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container, { + const chart = window.chart = LightweightCharts.createChart(container, { rightPriceScale: { visible: false }, leftPriceScale: { visible: true }, }); diff --git a/tests/e2e/graphics/test-cases/initial-options/log-price-scale-mode.js b/tests/e2e/graphics/test-cases/initial-options/log-price-scale-mode.js index a62ee8c2a9..ba65b9f65f 100644 --- a/tests/e2e/graphics/test-cases/initial-options/log-price-scale-mode.js +++ b/tests/e2e/graphics/test-cases/initial-options/log-price-scale-mode.js @@ -13,7 +13,7 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container, { + const chart = window.chart = LightweightCharts.createChart(container, { rightPriceScale: { mode: LightweightCharts.PriceScaleMode.Logarithmic, }, diff --git a/tests/e2e/graphics/test-cases/initial-options/min-visible-bars.js b/tests/e2e/graphics/test-cases/initial-options/min-visible-bars.js index cae2aa6936..201f101f86 100644 --- a/tests/e2e/graphics/test-cases/initial-options/min-visible-bars.js +++ b/tests/e2e/graphics/test-cases/initial-options/min-visible-bars.js @@ -23,7 +23,7 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container, { + const chart = window.chart = LightweightCharts.createChart(container, { timeScale: { barSpacing: 1000000, rightOffset: 100000, diff --git a/tests/e2e/graphics/test-cases/initial-options/no-autoscale.js b/tests/e2e/graphics/test-cases/initial-options/no-autoscale.js index f9842facef..129b83163e 100644 --- a/tests/e2e/graphics/test-cases/initial-options/no-autoscale.js +++ b/tests/e2e/graphics/test-cases/initial-options/no-autoscale.js @@ -13,7 +13,7 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container, { + const chart = window.chart = LightweightCharts.createChart(container, { rightPriceScale: { autoScale: false, }, diff --git a/tests/e2e/graphics/test-cases/initial-options/no-base-line.js b/tests/e2e/graphics/test-cases/initial-options/no-base-line.js index fd408fd460..f065085576 100644 --- a/tests/e2e/graphics/test-cases/initial-options/no-base-line.js +++ b/tests/e2e/graphics/test-cases/initial-options/no-base-line.js @@ -14,7 +14,7 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container, { + const chart = window.chart = LightweightCharts.createChart(container, { rightPriceScale: { mode: LightweightCharts.PriceScaleMode.Percentage, }, diff --git a/tests/e2e/graphics/test-cases/initial-options/no-price-line.js b/tests/e2e/graphics/test-cases/initial-options/no-price-line.js index ec6842577b..45d36a428a 100644 --- a/tests/e2e/graphics/test-cases/initial-options/no-price-line.js +++ b/tests/e2e/graphics/test-cases/initial-options/no-price-line.js @@ -14,7 +14,7 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container, { + const chart = window.chart = LightweightCharts.createChart(container, { rightPriceScale: { mode: LightweightCharts.PriceScaleMode.Percentage, }, diff --git a/tests/e2e/graphics/test-cases/initial-options/no-price-scale.js b/tests/e2e/graphics/test-cases/initial-options/no-price-scale.js index 6b3b85b758..6b92d4d774 100644 --- a/tests/e2e/graphics/test-cases/initial-options/no-price-scale.js +++ b/tests/e2e/graphics/test-cases/initial-options/no-price-scale.js @@ -14,7 +14,7 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container, { + const chart = window.chart = LightweightCharts.createChart(container, { rightPriceScale: { visible: false }, leftPriceScale: { visible: false }, }); diff --git a/tests/e2e/graphics/test-cases/initial-options/non-auto-scale-price-scale.js b/tests/e2e/graphics/test-cases/initial-options/non-auto-scale-price-scale.js index 60472f5251..efe4f71f09 100644 --- a/tests/e2e/graphics/test-cases/initial-options/non-auto-scale-price-scale.js +++ b/tests/e2e/graphics/test-cases/initial-options/non-auto-scale-price-scale.js @@ -13,7 +13,7 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container, { + const chart = window.chart = LightweightCharts.createChart(container, { rightPriceScale: { autoScale: false, }, diff --git a/tests/e2e/graphics/test-cases/initial-options/price-format.js b/tests/e2e/graphics/test-cases/initial-options/price-format.js index 5110d2bc0a..a78bd62fc2 100644 --- a/tests/e2e/graphics/test-cases/initial-options/price-format.js +++ b/tests/e2e/graphics/test-cases/initial-options/price-format.js @@ -13,7 +13,7 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); // see issue#55 const mainSeries = chart.addLineSeries({ diff --git a/tests/e2e/graphics/test-cases/initial-options/price-line-source-default.js b/tests/e2e/graphics/test-cases/initial-options/price-line-source-default.js index 98936cee28..af4a3a17e5 100644 --- a/tests/e2e/graphics/test-cases/initial-options/price-line-source-default.js +++ b/tests/e2e/graphics/test-cases/initial-options/price-line-source-default.js @@ -13,7 +13,7 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const series = chart.addLineSeries(); diff --git a/tests/e2e/graphics/test-cases/initial-options/price-line-source-last-visible.js b/tests/e2e/graphics/test-cases/initial-options/price-line-source-last-visible.js index 12022a4061..f2dbad82c0 100644 --- a/tests/e2e/graphics/test-cases/initial-options/price-line-source-last-visible.js +++ b/tests/e2e/graphics/test-cases/initial-options/price-line-source-last-visible.js @@ -13,7 +13,7 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const series = chart.addLineSeries({ priceLineSource: LightweightCharts.PriceLineSource.LastVisible, diff --git a/tests/e2e/graphics/test-cases/initial-options/price-line-style.js b/tests/e2e/graphics/test-cases/initial-options/price-line-style.js index 0f88abd256..3530480058 100644 --- a/tests/e2e/graphics/test-cases/initial-options/price-line-style.js +++ b/tests/e2e/graphics/test-cases/initial-options/price-line-style.js @@ -14,7 +14,7 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container, { + const chart = window.chart = LightweightCharts.createChart(container, { rightPriceScale: { mode: LightweightCharts.PriceScaleMode.Percentage, }, diff --git a/tests/e2e/graphics/test-cases/initial-options/price-scale-entire-text-only.js b/tests/e2e/graphics/test-cases/initial-options/price-scale-entire-text-only.js index f84d3a2db4..0558b4ad6b 100644 --- a/tests/e2e/graphics/test-cases/initial-options/price-scale-entire-text-only.js +++ b/tests/e2e/graphics/test-cases/initial-options/price-scale-entire-text-only.js @@ -1,5 +1,8 @@ +// Ignore the mouse movement check because height of chart is too short +window.ignoreMouseMove = true; + function runTestCase(container) { - const chart = LightweightCharts.createChart(container, { + const chart = window.chart = LightweightCharts.createChart(container, { width: 600, height: 300, rightPriceScale: { diff --git a/tests/e2e/graphics/test-cases/initial-options/small-candlesticks.js b/tests/e2e/graphics/test-cases/initial-options/small-candlesticks.js index 9b8961f501..0b3b3bc3a7 100644 --- a/tests/e2e/graphics/test-cases/initial-options/small-candlesticks.js +++ b/tests/e2e/graphics/test-cases/initial-options/small-candlesticks.js @@ -23,7 +23,7 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container, { + const chart = window.chart = LightweightCharts.createChart(container, { timeScale: { barSpacing: 20, }, diff --git a/tests/e2e/graphics/test-cases/initial-options/tick-marks-formatter-2.js b/tests/e2e/graphics/test-cases/initial-options/tick-marks-formatter-2.js index 5f71ecbc20..d600db6d00 100644 --- a/tests/e2e/graphics/test-cases/initial-options/tick-marks-formatter-2.js +++ b/tests/e2e/graphics/test-cases/initial-options/tick-marks-formatter-2.js @@ -19,7 +19,7 @@ function getData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container, { + const chart = window.chart = LightweightCharts.createChart(container, { timeScale: { tickMarkFormatter: (time, tickMarkType, locale) => time, // return time as is }, diff --git a/tests/e2e/graphics/test-cases/initial-options/tick-marks-formatter.js b/tests/e2e/graphics/test-cases/initial-options/tick-marks-formatter.js index 6ff5cef6ce..e8205ab4dd 100644 --- a/tests/e2e/graphics/test-cases/initial-options/tick-marks-formatter.js +++ b/tests/e2e/graphics/test-cases/initial-options/tick-marks-formatter.js @@ -18,7 +18,7 @@ function getData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container, { + const chart = window.chart = LightweightCharts.createChart(container, { timeScale: { timeVisible: true, secondsVisible: true, diff --git a/tests/e2e/graphics/test-cases/initial-options/time-formatter-2.js b/tests/e2e/graphics/test-cases/initial-options/time-formatter-2.js index fdb6239ec3..4bca045be7 100644 --- a/tests/e2e/graphics/test-cases/initial-options/time-formatter-2.js +++ b/tests/e2e/graphics/test-cases/initial-options/time-formatter-2.js @@ -19,7 +19,7 @@ function getData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container, { + const chart = window.chart = LightweightCharts.createChart(container, { localization: { timeFormatter: time => time, // return time as is }, diff --git a/tests/e2e/graphics/test-cases/initial-options/time-formatter.js b/tests/e2e/graphics/test-cases/initial-options/time-formatter.js index d90a02d954..83cc300a14 100644 --- a/tests/e2e/graphics/test-cases/initial-options/time-formatter.js +++ b/tests/e2e/graphics/test-cases/initial-options/time-formatter.js @@ -14,7 +14,7 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container, { + const chart = window.chart = LightweightCharts.createChart(container, { localization: { timeFormatter: businessDayOrTimestamp => { if (LightweightCharts.isBusinessDay(businessDayOrTimestamp)) { diff --git a/tests/e2e/graphics/test-cases/initial-options/time-scale.js b/tests/e2e/graphics/test-cases/initial-options/time-scale.js index b57700bb44..f061477f75 100644 --- a/tests/e2e/graphics/test-cases/initial-options/time-scale.js +++ b/tests/e2e/graphics/test-cases/initial-options/time-scale.js @@ -23,7 +23,7 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container, { + const chart = window.chart = LightweightCharts.createChart(container, { timeScale: { rightOffset: 10, barSpacing: 12, diff --git a/tests/e2e/graphics/test-cases/initial-options/transparent-background.js b/tests/e2e/graphics/test-cases/initial-options/transparent-background.js index c0368b1cb1..dc880ba7d0 100644 --- a/tests/e2e/graphics/test-cases/initial-options/transparent-background.js +++ b/tests/e2e/graphics/test-cases/initial-options/transparent-background.js @@ -14,7 +14,7 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container, { + const chart = window.chart = LightweightCharts.createChart(container, { layout: { background: { type: LightweightCharts.ColorType.Solid, diff --git a/tests/e2e/graphics/test-cases/initial-options/watermark.js b/tests/e2e/graphics/test-cases/initial-options/watermark.js index 7ea44bc946..55068364b1 100644 --- a/tests/e2e/graphics/test-cases/initial-options/watermark.js +++ b/tests/e2e/graphics/test-cases/initial-options/watermark.js @@ -13,7 +13,7 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container, { + const chart = window.chart = LightweightCharts.createChart(container, { watermark: { visible: true, color: 'red', diff --git a/tests/e2e/graphics/test-cases/initial-options/zero-precision.js b/tests/e2e/graphics/test-cases/initial-options/zero-precision.js index ab687419e4..f225aa4a19 100644 --- a/tests/e2e/graphics/test-cases/initial-options/zero-precision.js +++ b/tests/e2e/graphics/test-cases/initial-options/zero-precision.js @@ -13,7 +13,7 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); // see issue#59 const mainSeries = chart.addLineSeries({ diff --git a/tests/e2e/graphics/test-cases/logical-range/bars-in-gap.js b/tests/e2e/graphics/test-cases/logical-range/bars-in-gap.js index bb6d0beed2..0cc4d7e8d8 100644 --- a/tests/e2e/graphics/test-cases/logical-range/bars-in-gap.js +++ b/tests/e2e/graphics/test-cases/logical-range/bars-in-gap.js @@ -14,7 +14,7 @@ function generateData(count) { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const mainSeries = chart.addLineSeries(); const series = chart.addLineSeries(); diff --git a/tests/e2e/graphics/test-cases/logical-range/bars-in-range.js b/tests/e2e/graphics/test-cases/logical-range/bars-in-range.js index aaa51305cc..946679e5dd 100644 --- a/tests/e2e/graphics/test-cases/logical-range/bars-in-range.js +++ b/tests/e2e/graphics/test-cases/logical-range/bars-in-range.js @@ -14,7 +14,7 @@ function generateData(count) { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const series = chart.addLineSeries(); diff --git a/tests/e2e/graphics/test-cases/logical-range/subscribe-visible-logical-range-change.js b/tests/e2e/graphics/test-cases/logical-range/subscribe-visible-logical-range-change.js index b381678cb3..980385d310 100644 --- a/tests/e2e/graphics/test-cases/logical-range/subscribe-visible-logical-range-change.js +++ b/tests/e2e/graphics/test-cases/logical-range/subscribe-visible-logical-range-change.js @@ -14,7 +14,7 @@ function generateData(count) { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const series = chart.addLineSeries(); diff --git a/tests/e2e/graphics/test-cases/months-chart.js b/tests/e2e/graphics/test-cases/months-chart.js index d18d1833b0..e51b0f9673 100644 --- a/tests/e2e/graphics/test-cases/months-chart.js +++ b/tests/e2e/graphics/test-cases/months-chart.js @@ -14,7 +14,7 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const firstSeries = chart.addLineSeries(); firstSeries.setData(generateData()); diff --git a/tests/e2e/graphics/test-cases/price-scale/logarithmic-scale-on-extra-small-values.js b/tests/e2e/graphics/test-cases/price-scale/logarithmic-scale-on-extra-small-values.js index 792a0b5906..1816bf3155 100644 --- a/tests/e2e/graphics/test-cases/price-scale/logarithmic-scale-on-extra-small-values.js +++ b/tests/e2e/graphics/test-cases/price-scale/logarithmic-scale-on-extra-small-values.js @@ -11,7 +11,7 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container, { + const chart = window.chart = LightweightCharts.createChart(container, { rightPriceScale: { mode: LightweightCharts.PriceScaleMode.Logarithmic, }, diff --git a/tests/e2e/graphics/test-cases/price-scale/logarithmic-scale-on-small-values.js b/tests/e2e/graphics/test-cases/price-scale/logarithmic-scale-on-small-values.js index b022d7c96a..f4f752a402 100644 --- a/tests/e2e/graphics/test-cases/price-scale/logarithmic-scale-on-small-values.js +++ b/tests/e2e/graphics/test-cases/price-scale/logarithmic-scale-on-small-values.js @@ -11,7 +11,7 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container, { + const chart = window.chart = LightweightCharts.createChart(container, { rightPriceScale: { mode: LightweightCharts.PriceScaleMode.Logarithmic, }, diff --git a/tests/e2e/graphics/test-cases/price-scale/no-empty-mark-on-price-scale.js b/tests/e2e/graphics/test-cases/price-scale/no-empty-mark-on-price-scale.js index 504214ce4f..a2257f524b 100644 --- a/tests/e2e/graphics/test-cases/price-scale/no-empty-mark-on-price-scale.js +++ b/tests/e2e/graphics/test-cases/price-scale/no-empty-mark-on-price-scale.js @@ -1,5 +1,5 @@ function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const series = chart.addLineSeries(); diff --git a/tests/e2e/graphics/test-cases/price-scale/series-label-is-fully-visible-at-edge-of-scale.js b/tests/e2e/graphics/test-cases/price-scale/series-label-is-fully-visible-at-edge-of-scale.js new file mode 100644 index 0000000000..af82aa9bdd --- /dev/null +++ b/tests/e2e/graphics/test-cases/price-scale/series-label-is-fully-visible-at-edge-of-scale.js @@ -0,0 +1,336 @@ +function runTestCase(container) { + const chart = window.chart = LightweightCharts.createChart(container); + + const areaSeries = chart.addAreaSeries({ + symbol: 'AAPL', + lineWidth: 2, + }); + + areaSeries.priceScale().applyOptions({ + scaleMargins: { + top: 0, + bottom: 0.9999, + }, + }); + + const volumeSeries = chart.addHistogramSeries({ + priceFormat: { + type: 'volume', + }, + priceLineVisible: false, + priceScaleId: '', + }); + + volumeSeries.priceScale().applyOptions({ + scaleMargins: { + top: 0.85, + bottom: 0, + }, + }); + + areaSeries.setData([ + { time: '2018-10-19', value: 219.31 }, + { time: '2018-10-22', value: 220.65 }, + { time: '2018-10-23', value: 222.73 }, + { time: '2018-10-24', value: 215.09 }, + { time: '2018-10-25', value: 219.80 }, + { time: '2018-10-26', value: 216.30 }, + { time: '2018-10-29', value: 212.24 }, + { time: '2018-10-30', value: 213.30 }, + { time: '2018-10-31', value: 218.86 }, + { time: '2018-11-01', value: 222.22 }, + { time: '2018-11-02', value: 207.48 }, + { time: '2018-11-05', value: 201.59 }, + { time: '2018-11-06', value: 203.77 }, + { time: '2018-11-07', value: 209.95 }, + { time: '2018-11-08', value: 208.49 }, + { time: '2018-11-09', value: 204.47 }, + { time: '2018-11-12', value: 194.17 }, + { time: '2018-11-13', value: 192.23 }, + { time: '2018-11-14', value: 186.80 }, + { time: '2018-11-15', value: 191.41 }, + { time: '2018-11-16', value: 193.53 }, + { time: '2018-11-19', value: 185.86 }, + { time: '2018-11-20', value: 176.98 }, + { time: '2018-11-21', value: 176.78 }, + { time: '2018-11-23', value: 172.29 }, + { time: '2018-11-26', value: 174.62 }, + { time: '2018-11-27', value: 174.24 }, + { time: '2018-11-28', value: 180.94 }, + { time: '2018-11-29', value: 179.55 }, + { time: '2018-11-30', value: 178.58 }, + { time: '2018-12-03', value: 184.82 }, + { time: '2018-12-04', value: 176.69 }, + { time: '2018-12-06', value: 174.72 }, + { time: '2018-12-07', value: 168.49 }, + { time: '2018-12-10', value: 169.60 }, + { time: '2018-12-11', value: 168.63 }, + { time: '2018-12-12', value: 169.10 }, + { time: '2018-12-13', value: 170.95 }, + { time: '2018-12-14', value: 165.48 }, + { time: '2018-12-17', value: 163.94 }, + { time: '2018-12-18', value: 166.07 }, + { time: '2018-12-19', value: 160.89 }, + { time: '2018-12-20', value: 156.83 }, + { time: '2018-12-21', value: 150.73 }, + { time: '2018-12-24', value: 146.83 }, + { time: '2018-12-26', value: 157.17 }, + { time: '2018-12-27', value: 156.15 }, + { time: '2018-12-28', value: 156.23 }, + { time: '2018-12-31', value: 157.74 }, + { time: '2019-01-02', value: 157.92 }, + { time: '2019-01-03', value: 142.19 }, + { time: '2019-01-04', value: 148.26 }, + { time: '2019-01-07', value: 147.93 }, + { time: '2019-01-08', value: 150.75 }, + { time: '2019-01-09', value: 153.31 }, + { time: '2019-01-10', value: 153.80 }, + { time: '2019-01-11', value: 152.29 }, + { time: '2019-01-14', value: 150.00 }, + { time: '2019-01-15', value: 153.07 }, + { time: '2019-01-16', value: 154.94 }, + { time: '2019-01-17', value: 155.86 }, + { time: '2019-01-18', value: 156.82 }, + { time: '2019-01-22', value: 153.30 }, + { time: '2019-01-23', value: 153.92 }, + { time: '2019-01-24', value: 152.70 }, + { time: '2019-01-25', value: 157.76 }, + { time: '2019-01-28', value: 156.30 }, + { time: '2019-01-29', value: 154.68 }, + { time: '2019-01-30', value: 165.25 }, + { time: '2019-01-31', value: 166.44 }, + { time: '2019-02-01', value: 166.52 }, + { time: '2019-02-04', value: 171.25 }, + { time: '2019-02-05', value: 174.18 }, + { time: '2019-02-06', value: 174.24 }, + { time: '2019-02-07', value: 170.94 }, + { time: '2019-02-08', value: 170.41 }, + { time: '2019-02-11', value: 169.43 }, + { time: '2019-02-12', value: 170.89 }, + { time: '2019-02-13', value: 170.18 }, + { time: '2019-02-14', value: 170.80 }, + { time: '2019-02-15', value: 170.42 }, + { time: '2019-02-19', value: 170.93 }, + { time: '2019-02-20', value: 172.03 }, + { time: '2019-02-21', value: 171.06 }, + { time: '2019-02-22', value: 172.97 }, + { time: '2019-02-25', value: 174.23 }, + { time: '2019-02-26', value: 174.33 }, + { time: '2019-02-27', value: 174.87 }, + { time: '2019-02-28', value: 173.15 }, + { time: '2019-03-01', value: 174.97 }, + { time: '2019-03-04', value: 175.85 }, + { time: '2019-03-05', value: 175.53 }, + { time: '2019-03-06', value: 174.52 }, + { time: '2019-03-07', value: 172.50 }, + { time: '2019-03-08', value: 172.91 }, + { time: '2019-03-11', value: 178.90 }, + { time: '2019-03-12', value: 180.91 }, + { time: '2019-03-13', value: 181.71 }, + { time: '2019-03-14', value: 183.73 }, + { time: '2019-03-15', value: 186.12 }, + { time: '2019-03-18', value: 188.02 }, + { time: '2019-03-19', value: 186.53 }, + { time: '2019-03-20', value: 188.16 }, + { time: '2019-03-21', value: 195.09 }, + { time: '2019-03-22', value: 191.05 }, + { time: '2019-03-25', value: 188.74 }, + { time: '2019-03-26', value: 186.79 }, + { time: '2019-03-27', value: 188.47 }, + { time: '2019-03-28', value: 188.72 }, + { time: '2019-03-29', value: 189.95 }, + { time: '2019-04-01', value: 191.24 }, + { time: '2019-04-02', value: 194.02 }, + { time: '2019-04-03', value: 195.35 }, + { time: '2019-04-04', value: 195.69 }, + { time: '2019-04-05', value: 197.00 }, + { time: '2019-04-08', value: 200.10 }, + { time: '2019-04-09', value: 199.50 }, + { time: '2019-04-10', value: 200.62 }, + { time: '2019-04-11', value: 198.95 }, + { time: '2019-04-12', value: 198.87 }, + { time: '2019-04-15', value: 199.23 }, + { time: '2019-04-16', value: 199.25 }, + { time: '2019-04-17', value: 203.13 }, + { time: '2019-04-18', value: 203.86 }, + { time: '2019-04-22', value: 204.53 }, + { time: '2019-04-23', value: 207.48 }, + { time: '2019-04-24', value: 207.16 }, + { time: '2019-04-25', value: 205.28 }, + { time: '2019-04-26', value: 204.30 }, + { time: '2019-04-29', value: 204.61 }, + { time: '2019-04-30', value: 200.67 }, + { time: '2019-05-01', value: 210.52 }, + { time: '2019-05-02', value: 209.15 }, + { time: '2019-05-03', value: 211.75 }, + { time: '2019-05-06', value: 208.48 }, + { time: '2019-05-07', value: 202.86 }, + { time: '2019-05-08', value: 202.90 }, + { time: '2019-05-09', value: 200.72 }, + { time: '2019-05-10', value: 197.18 }, + { time: '2019-05-13', value: 185.72 }, + { time: '2019-05-14', value: 188.66 }, + { time: '2019-05-15', value: 190.92 }, + { time: '2019-05-16', value: 190.08 }, + { time: '2019-05-17', value: 189.00 }, + { time: '2019-05-20', value: 183.09 }, + { time: '2019-05-21', value: 186.60 }, + { time: '2019-05-22', value: 182.78 }, + { time: '2019-05-23', value: 179.66 }, + { time: '2019-05-24', value: 178.97 }, + { time: '2019-05-28', value: 178.67 }, + ]); + + volumeSeries.setData([ + { time: '2018-10-19', value: 33078726.00 }, + { time: '2018-10-22', value: 28792082.00 }, + { time: '2018-10-23', value: 38767846.00 }, + { time: '2018-10-24', value: 40925163.00 }, + { time: '2018-10-25', value: 29855768.00 }, + { time: '2018-10-26', value: 47258375.00 }, + { time: '2018-10-29', value: 45935520.00 }, + { time: '2018-10-30', value: 36659990.00 }, + { time: '2018-10-31', value: 38358933.00 }, + { time: '2018-11-01', value: 58323180.00 }, + { time: '2018-11-02', value: 91328654.00 }, + { time: '2018-11-05', value: 66163669.00 }, + { time: '2018-11-06', value: 31882881.00 }, + { time: '2018-11-07', value: 33424434.00 }, + { time: '2018-11-08', value: 25362636.00 }, + { time: '2018-11-09', value: 34365750.00 }, + { time: '2018-11-12', value: 51135518.00 }, + { time: '2018-11-13', value: 46882936.00 }, + { time: '2018-11-14', value: 60800957.00 }, + { time: '2018-11-15', value: 46478801.00 }, + { time: '2018-11-16', value: 36928253.00 }, + { time: '2018-11-19', value: 41920872.00 }, + { time: '2018-11-20', value: 67825247.00 }, + { time: '2018-11-21', value: 31124210.00 }, + { time: '2018-11-23', value: 23623972.00 }, + { time: '2018-11-26', value: 44998520.00 }, + { time: '2018-11-27', value: 41387377.00 }, + { time: '2018-11-28', value: 46062539.00 }, + { time: '2018-11-29', value: 41769992.00 }, + { time: '2018-11-30', value: 39531549.00 }, + { time: '2018-12-03', value: 40798002.00 }, + { time: '2018-12-04', value: 41344282.00 }, + { time: '2018-12-06', value: 43098410.00 }, + { time: '2018-12-07', value: 42281631.00 }, + { time: '2018-12-10', value: 62025994.00 }, + { time: '2018-12-11', value: 47281665.00 }, + { time: '2018-12-12', value: 35627674.00 }, + { time: '2018-12-13', value: 31897827.00 }, + { time: '2018-12-14', value: 40703710.00 }, + { time: '2018-12-17', value: 44287922.00 }, + { time: '2018-12-18', value: 33841518.00 }, + { time: '2018-12-19', value: 49047297.00 }, + { time: '2018-12-20', value: 64772960.00 }, + { time: '2018-12-21', value: 95744384.00 }, + { time: '2018-12-24', value: 37169232.00 }, + { time: '2018-12-26', value: 58582544.00 }, + { time: '2018-12-27', value: 53117065.00 }, + { time: '2018-12-28', value: 42291424.00 }, + { time: '2018-12-31', value: 35003466.00 }, + { time: '2019-01-02', value: 37039737.00 }, + { time: '2019-01-03', value: 91312195.00 }, + { time: '2019-01-04', value: 58607070.00 }, + { time: '2019-01-07', value: 54777764.00 }, + { time: '2019-01-08', value: 41025314.00 }, + { time: '2019-01-09', value: 45099081.00 }, + { time: '2019-01-10', value: 35780670.00 }, + { time: '2019-01-11', value: 27023241.00 }, + { time: '2019-01-14', value: 32439186.00 }, + { time: '2019-01-15', value: 28710324.00 }, + { time: '2019-01-16', value: 30569706.00 }, + { time: '2019-01-17', value: 29821160.00 }, + { time: '2019-01-18', value: 33751023.00 }, + { time: '2019-01-22', value: 30393970.00 }, + { time: '2019-01-23', value: 23130570.00 }, + { time: '2019-01-24', value: 25441549.00 }, + { time: '2019-01-25', value: 33547893.00 }, + { time: '2019-01-28', value: 26192058.00 }, + { time: '2019-01-29', value: 41587239.00 }, + { time: '2019-01-30', value: 61109780.00 }, + { time: '2019-01-31', value: 40739649.00 }, + { time: '2019-02-01', value: 32668138.00 }, + { time: '2019-02-04', value: 31495582.00 }, + { time: '2019-02-05', value: 36101628.00 }, + { time: '2019-02-06', value: 28239591.00 }, + { time: '2019-02-07', value: 31741690.00 }, + { time: '2019-02-08', value: 23819966.00 }, + { time: '2019-02-11', value: 20993425.00 }, + { time: '2019-02-12', value: 22283523.00 }, + { time: '2019-02-13', value: 22490233.00 }, + { time: '2019-02-14', value: 21835747.00 }, + { time: '2019-02-15', value: 24626814.00 }, + { time: '2019-02-19', value: 18972826.00 }, + { time: '2019-02-20', value: 26114362.00 }, + { time: '2019-02-21', value: 17249670.00 }, + { time: '2019-02-22', value: 18913154.00 }, + { time: '2019-02-25', value: 21873358.00 }, + { time: '2019-02-26', value: 17070211.00 }, + { time: '2019-02-27', value: 27835389.00 }, + { time: '2019-02-28', value: 28215416.00 }, + { time: '2019-03-01', value: 25886167.00 }, + { time: '2019-03-04', value: 27436203.00 }, + { time: '2019-03-05', value: 19737419.00 }, + { time: '2019-03-06', value: 20810384.00 }, + { time: '2019-03-07', value: 24796374.00 }, + { time: '2019-03-08', value: 23999358.00 }, + { time: '2019-03-11', value: 32011034.00 }, + { time: '2019-03-12', value: 32467584.00 }, + { time: '2019-03-13', value: 31032524.00 }, + { time: '2019-03-14', value: 23579508.00 }, + { time: '2019-03-15', value: 39042912.00 }, + { time: '2019-03-18', value: 26219832.00 }, + { time: '2019-03-19', value: 31646369.00 }, + { time: '2019-03-20', value: 31035231.00 }, + { time: '2019-03-21', value: 51034237.00 }, + { time: '2019-03-22', value: 42407666.00 }, + { time: '2019-03-25', value: 43845293.00 }, + { time: '2019-03-26', value: 49800538.00 }, + { time: '2019-03-27', value: 29848427.00 }, + { time: '2019-03-28', value: 20780363.00 }, + { time: '2019-03-29', value: 23563961.00 }, + { time: '2019-04-01', value: 27861964.00 }, + { time: '2019-04-02', value: 22765732.00 }, + { time: '2019-04-03', value: 23271830.00 }, + { time: '2019-04-04', value: 19114275.00 }, + { time: '2019-04-05', value: 18526644.00 }, + { time: '2019-04-08', value: 25881697.00 }, + { time: '2019-04-09', value: 35768237.00 }, + { time: '2019-04-10', value: 21695288.00 }, + { time: '2019-04-11', value: 20900808.00 }, + { time: '2019-04-12', value: 27760668.00 }, + { time: '2019-04-15', value: 17536646.00 }, + { time: '2019-04-16', value: 25696385.00 }, + { time: '2019-04-17', value: 28906780.00 }, + { time: '2019-04-18', value: 24195766.00 }, + { time: '2019-04-22', value: 19439545.00 }, + { time: '2019-04-23', value: 23322991.00 }, + { time: '2019-04-24', value: 17540609.00 }, + { time: '2019-04-25', value: 18543206.00 }, + { time: '2019-04-26', value: 18649102.00 }, + { time: '2019-04-29', value: 22204716.00 }, + { time: '2019-04-30', value: 46534923.00 }, + { time: '2019-05-01', value: 64827328.00 }, + { time: '2019-05-02', value: 31996324.00 }, + { time: '2019-05-03', value: 20892378.00 }, + { time: '2019-05-06', value: 32443113.00 }, + { time: '2019-05-07', value: 38763698.00 }, + { time: '2019-05-08', value: 26339504.00 }, + { time: '2019-05-09', value: 34908607.00 }, + { time: '2019-05-10', value: 41208712.00 }, + { time: '2019-05-13', value: 57430623.00 }, + { time: '2019-05-14', value: 36529677.00 }, + { time: '2019-05-15', value: 26544718.00 }, + { time: '2019-05-16', value: 33031364.00 }, + { time: '2019-05-17', value: 32879090.00 }, + { time: '2019-05-20', value: 38690198.00 }, + { time: '2019-05-21', value: 28364848.00 }, + { time: '2019-05-22', value: 29748556.00 }, + { time: '2019-05-23', value: 36217464.00 }, + { time: '2019-05-24', value: 23714686.00 }, + { time: '2019-05-28', value: 164013.00 }, + ]); +} diff --git a/tests/e2e/graphics/test-cases/remove-series-extended-time-scale.js b/tests/e2e/graphics/test-cases/remove-series-extended-time-scale.js index 548c65d00c..7b9d756acb 100644 --- a/tests/e2e/graphics/test-cases/remove-series-extended-time-scale.js +++ b/tests/e2e/graphics/test-cases/remove-series-extended-time-scale.js @@ -13,7 +13,7 @@ function generateData(step, startDay) { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const mainSeries = chart.addLineSeries({ color: '#0000ff', diff --git a/tests/e2e/graphics/test-cases/series-markers/marker-in-gap-from-left.js b/tests/e2e/graphics/test-cases/series-markers/marker-in-gap-from-left.js index a934d0fb84..88d760ca1e 100644 --- a/tests/e2e/graphics/test-cases/series-markers/marker-in-gap-from-left.js +++ b/tests/e2e/graphics/test-cases/series-markers/marker-in-gap-from-left.js @@ -1,5 +1,5 @@ function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const lineSeries = chart.addLineSeries(); diff --git a/tests/e2e/graphics/test-cases/series-markers/marker-in-gap.js b/tests/e2e/graphics/test-cases/series-markers/marker-in-gap.js index 00bbecaa9f..723f368051 100644 --- a/tests/e2e/graphics/test-cases/series-markers/marker-in-gap.js +++ b/tests/e2e/graphics/test-cases/series-markers/marker-in-gap.js @@ -1,5 +1,5 @@ function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const lineSeries = chart.addLineSeries(); diff --git a/tests/e2e/graphics/test-cases/series-markers/series-arrow-markers.js b/tests/e2e/graphics/test-cases/series-markers/series-arrow-markers.js index 8f3c948d37..dc8e6a5fd3 100644 --- a/tests/e2e/graphics/test-cases/series-markers/series-arrow-markers.js +++ b/tests/e2e/graphics/test-cases/series-markers/series-arrow-markers.js @@ -23,7 +23,7 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const mainSeries = chart.addBarSeries(); diff --git a/tests/e2e/graphics/test-cases/series-markers/series-circle-markers.js b/tests/e2e/graphics/test-cases/series-markers/series-circle-markers.js index f61fbf0a06..123b9b625e 100644 --- a/tests/e2e/graphics/test-cases/series-markers/series-circle-markers.js +++ b/tests/e2e/graphics/test-cases/series-markers/series-circle-markers.js @@ -23,7 +23,7 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const mainSeries = chart.addBarSeries(); diff --git a/tests/e2e/graphics/test-cases/series-markers/series-markers-aligned.js b/tests/e2e/graphics/test-cases/series-markers/series-markers-aligned.js index 5b40d7904a..e5f38600e7 100644 --- a/tests/e2e/graphics/test-cases/series-markers/series-markers-aligned.js +++ b/tests/e2e/graphics/test-cases/series-markers/series-markers-aligned.js @@ -1,5 +1,5 @@ function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const line = chart.addLineSeries(); line.setData([ diff --git a/tests/e2e/graphics/test-cases/series-markers/series-markers-max-bar-spacing.js b/tests/e2e/graphics/test-cases/series-markers/series-markers-max-bar-spacing.js index 9d1d1b5c15..8b4beeb4aa 100644 --- a/tests/e2e/graphics/test-cases/series-markers/series-markers-max-bar-spacing.js +++ b/tests/e2e/graphics/test-cases/series-markers/series-markers-max-bar-spacing.js @@ -13,7 +13,7 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const mainSeries = chart.addLineSeries(); diff --git a/tests/e2e/graphics/test-cases/series-markers/series-markers-min-bar-spacing.js b/tests/e2e/graphics/test-cases/series-markers/series-markers-min-bar-spacing.js index 580b5f9f3b..d3dbf1b598 100644 --- a/tests/e2e/graphics/test-cases/series-markers/series-markers-min-bar-spacing.js +++ b/tests/e2e/graphics/test-cases/series-markers/series-markers-min-bar-spacing.js @@ -13,7 +13,7 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const mainSeries = chart.addLineSeries(); diff --git a/tests/e2e/graphics/test-cases/series-markers/series-markers-object-business-day.js b/tests/e2e/graphics/test-cases/series-markers/series-markers-object-business-day.js index d264ba3bac..a7951dc03d 100644 --- a/tests/e2e/graphics/test-cases/series-markers/series-markers-object-business-day.js +++ b/tests/e2e/graphics/test-cases/series-markers/series-markers-object-business-day.js @@ -1,5 +1,5 @@ function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const line = chart.addLineSeries(); line.setData([ diff --git a/tests/e2e/graphics/test-cases/series-markers/series-markers-out-of-visible-range.js b/tests/e2e/graphics/test-cases/series-markers/series-markers-out-of-visible-range.js index 6795a05c8c..8b9e2e4405 100644 --- a/tests/e2e/graphics/test-cases/series-markers/series-markers-out-of-visible-range.js +++ b/tests/e2e/graphics/test-cases/series-markers/series-markers-out-of-visible-range.js @@ -13,7 +13,7 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const mainSeries = chart.addLineSeries(); diff --git a/tests/e2e/graphics/test-cases/series-markers/series-markers-re-aligned.js b/tests/e2e/graphics/test-cases/series-markers/series-markers-re-aligned.js index ac17568c1a..8755dcfda3 100644 --- a/tests/e2e/graphics/test-cases/series-markers/series-markers-re-aligned.js +++ b/tests/e2e/graphics/test-cases/series-markers/series-markers-re-aligned.js @@ -1,5 +1,5 @@ function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const line = chart.addLineSeries(); line.setData([ diff --git a/tests/e2e/graphics/test-cases/series-markers/series-markers-update.js b/tests/e2e/graphics/test-cases/series-markers/series-markers-update.js index 982e955840..f1311c5762 100644 --- a/tests/e2e/graphics/test-cases/series-markers/series-markers-update.js +++ b/tests/e2e/graphics/test-cases/series-markers/series-markers-update.js @@ -13,7 +13,7 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const mainSeries = chart.addLineSeries(); diff --git a/tests/e2e/graphics/test-cases/series-markers/series-markers-with-text.js b/tests/e2e/graphics/test-cases/series-markers/series-markers-with-text.js index 226b76a144..2d2861eacd 100644 --- a/tests/e2e/graphics/test-cases/series-markers/series-markers-with-text.js +++ b/tests/e2e/graphics/test-cases/series-markers/series-markers-with-text.js @@ -23,7 +23,7 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart( + const chart = window.chart = LightweightCharts.createChart( container, { layout: { diff --git a/tests/e2e/graphics/test-cases/series-markers/series-square-markers.js b/tests/e2e/graphics/test-cases/series-markers/series-square-markers.js index 8f5a77d252..e11a9c52fa 100644 --- a/tests/e2e/graphics/test-cases/series-markers/series-square-markers.js +++ b/tests/e2e/graphics/test-cases/series-markers/series-square-markers.js @@ -23,7 +23,7 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const mainSeries = chart.addBarSeries(); diff --git a/tests/e2e/graphics/test-cases/series-markers/set-markers-before-series-data.js b/tests/e2e/graphics/test-cases/series-markers/set-markers-before-series-data.js index 2c610a0f42..57f20c8c21 100644 --- a/tests/e2e/graphics/test-cases/series-markers/set-markers-before-series-data.js +++ b/tests/e2e/graphics/test-cases/series-markers/set-markers-before-series-data.js @@ -1,5 +1,5 @@ function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const mainSeries = chart.addLineSeries(); diff --git a/tests/e2e/graphics/test-cases/series/2-baseline-series.js b/tests/e2e/graphics/test-cases/series/2-baseline-series.js index 0c6499fa7c..9e55616237 100644 --- a/tests/e2e/graphics/test-cases/series/2-baseline-series.js +++ b/tests/e2e/graphics/test-cases/series/2-baseline-series.js @@ -40,7 +40,7 @@ function generateData(valueOffset) { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const firstSeries = chart.addBaselineSeries({ baseValue: { diff --git a/tests/e2e/graphics/test-cases/series/2-points-line-series.js b/tests/e2e/graphics/test-cases/series/2-points-line-series.js index 87f50e7def..fa2bd31529 100644 --- a/tests/e2e/graphics/test-cases/series/2-points-line-series.js +++ b/tests/e2e/graphics/test-cases/series/2-points-line-series.js @@ -23,7 +23,7 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const mainSeries = chart.addBarSeries(); const lineSeries = chart.addLineSeries(); diff --git a/tests/e2e/graphics/test-cases/series/add-series-after-volume.js b/tests/e2e/graphics/test-cases/series/add-series-after-volume.js index 39df0a5466..38ea32ae51 100644 --- a/tests/e2e/graphics/test-cases/series/add-series-after-volume.js +++ b/tests/e2e/graphics/test-cases/series/add-series-after-volume.js @@ -2,7 +2,7 @@ // https://github.com/tradingview/lightweight-charts/issues/110 function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const areaSeries = chart.addAreaSeries(); // or any other series type const volumeSeries = chart.addHistogramSeries(); diff --git a/tests/e2e/graphics/test-cases/series/alternate-histogram-items-with-gaps.js b/tests/e2e/graphics/test-cases/series/alternate-histogram-items-with-gaps.js index ee58ab408d..ad795fbf1c 100644 --- a/tests/e2e/graphics/test-cases/series/alternate-histogram-items-with-gaps.js +++ b/tests/e2e/graphics/test-cases/series/alternate-histogram-items-with-gaps.js @@ -15,7 +15,7 @@ function generateData(step) { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container, { + const chart = window.chart = LightweightCharts.createChart(container, { timeScale: { barSpacing: 100, }, diff --git a/tests/e2e/graphics/test-cases/series/area-inverted-after-delay.js b/tests/e2e/graphics/test-cases/series/area-inverted-after-delay.js new file mode 100644 index 0000000000..81ceef9ced --- /dev/null +++ b/tests/e2e/graphics/test-cases/series/area-inverted-after-delay.js @@ -0,0 +1,32 @@ +function generateData() { + const res = []; + const time = new Date(Date.UTC(2018, 0, 1, 0, 0, 0, 0)); + for (let i = 0; i < 500; ++i) { + res.push({ + time: time.getTime() / 1000, + value: i, + }); + + time.setUTCDate(time.getUTCDate() + 1); + } + return res; +} + +function runTestCase(container) { + const chart = window.chart = LightweightCharts.createChart(container); + + const mainSeries = chart.addAreaSeries({ + invertFilledArea: false, + }); + + mainSeries.setData(generateData()); + + return new Promise(resolve => { + requestAnimationFrame(() => { + mainSeries.applyOptions({ + invertFilledArea: true, + }); + requestAnimationFrame(resolve); + }); + }); +} diff --git a/tests/e2e/graphics/test-cases/series/area-inverted.js b/tests/e2e/graphics/test-cases/series/area-inverted.js new file mode 100644 index 0000000000..4c49ef7361 --- /dev/null +++ b/tests/e2e/graphics/test-cases/series/area-inverted.js @@ -0,0 +1,23 @@ +function generateData() { + const res = []; + const time = new Date(Date.UTC(2018, 0, 1, 0, 0, 0, 0)); + for (let i = 0; i < 500; ++i) { + res.push({ + time: time.getTime() / 1000, + value: i, + }); + + time.setUTCDate(time.getUTCDate() + 1); + } + return res; +} + +function runTestCase(container) { + const chart = window.chart = LightweightCharts.createChart(container); + + const mainSeries = chart.addAreaSeries({ + invertFilledArea: true, + }); + + mainSeries.setData(generateData()); +} diff --git a/tests/e2e/graphics/test-cases/series/area-out-of-viewport.js b/tests/e2e/graphics/test-cases/series/area-out-of-viewport.js index b648699774..251e7ad34f 100644 --- a/tests/e2e/graphics/test-cases/series/area-out-of-viewport.js +++ b/tests/e2e/graphics/test-cases/series/area-out-of-viewport.js @@ -1,5 +1,5 @@ function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const areaSeries = chart.addAreaSeries(); areaSeries.setData([ diff --git a/tests/e2e/graphics/test-cases/series/area-with-custom-colors.js b/tests/e2e/graphics/test-cases/series/area-with-custom-colors.js new file mode 100644 index 0000000000..f6a9751be6 --- /dev/null +++ b/tests/e2e/graphics/test-cases/series/area-with-custom-colors.js @@ -0,0 +1,34 @@ +function generateBar(i, target) { + const step = (i % 20) / 5000; + const base = i / 5; + target.value = base * (1 - step); + + if ((i % 10) > 4) { + target.lineColor = 'yellow'; + target.topColor = 'black'; + target.bottomColor = '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); + + generateBar(i, item); + res.push(item); + } + return res; +} + +function runTestCase(container) { + const chart = window.chart = LightweightCharts.createChart(container); + + const mainSeries = chart.addAreaSeries(); + + mainSeries.setData(generateData()); +} diff --git a/tests/e2e/graphics/test-cases/series/area-with-whitespaces.js b/tests/e2e/graphics/test-cases/series/area-with-whitespaces.js index 17dabbc521..0122d42a77 100644 --- a/tests/e2e/graphics/test-cases/series/area-with-whitespaces.js +++ b/tests/e2e/graphics/test-cases/series/area-with-whitespaces.js @@ -17,7 +17,7 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const mainSeries = chart.addAreaSeries(); diff --git a/tests/e2e/graphics/test-cases/series/area.js b/tests/e2e/graphics/test-cases/series/area.js index d1761352fe..2af6cc3f8e 100644 --- a/tests/e2e/graphics/test-cases/series/area.js +++ b/tests/e2e/graphics/test-cases/series/area.js @@ -13,7 +13,7 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const mainSeries = chart.addAreaSeries(); diff --git a/tests/e2e/graphics/test-cases/series/bar-semitransparent.js b/tests/e2e/graphics/test-cases/series/bar-semitransparent.js index 8d35d7910e..7436ef716b 100644 --- a/tests/e2e/graphics/test-cases/series/bar-semitransparent.js +++ b/tests/e2e/graphics/test-cases/series/bar-semitransparent.js @@ -23,7 +23,7 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const mainSeries = chart.addBarSeries({ upColor: 'rgba(0, 250, 0, 0.3)', diff --git a/tests/e2e/graphics/test-cases/series/bar-with-custom-colors.js b/tests/e2e/graphics/test-cases/series/bar-with-custom-colors.js index 403d545bf4..716ccacd5d 100644 --- a/tests/e2e/graphics/test-cases/series/bar-with-custom-colors.js +++ b/tests/e2e/graphics/test-cases/series/bar-with-custom-colors.js @@ -27,7 +27,7 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const mainSeries = chart.addBarSeries(); diff --git a/tests/e2e/graphics/test-cases/series/bar.js b/tests/e2e/graphics/test-cases/series/bar.js index 1883e9ac46..23585510e7 100644 --- a/tests/e2e/graphics/test-cases/series/bar.js +++ b/tests/e2e/graphics/test-cases/series/bar.js @@ -23,7 +23,7 @@ function generateData(startValue) { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const mainSeries = chart.addBarSeries(); diff --git a/tests/e2e/graphics/test-cases/series/bars-with-whitespaces.js b/tests/e2e/graphics/test-cases/series/bars-with-whitespaces.js index acd095dd66..8cc2ea6676 100644 --- a/tests/e2e/graphics/test-cases/series/bars-with-whitespaces.js +++ b/tests/e2e/graphics/test-cases/series/bars-with-whitespaces.js @@ -25,7 +25,7 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const mainSeries = chart.addBarSeries(); diff --git a/tests/e2e/graphics/test-cases/series/baseline-with-custom-colors.js b/tests/e2e/graphics/test-cases/series/baseline-with-custom-colors.js new file mode 100644 index 0000000000..784ab3c1a8 --- /dev/null +++ b/tests/e2e/graphics/test-cases/series/baseline-with-custom-colors.js @@ -0,0 +1,43 @@ +function generateBar(i, target) { + const step = (i % 20) / 5000; + const base = i / 5; + target.value = base * (1 - step); + + if ((i % 10) > 4) { + target.topFillColor1 = 'red'; + target.topFillColor2 = 'rgba(255, 0, 0, 0)'; + } + + if ((i % 10) > 6) { + target.bottomFillColor1 = 'yellow'; + target.bottomFillColor2 = 'rgba(255, 255, 0, 0)'; + } + + if ((i % 10) > 5) { + target.topLineColor = 'blue'; + target.bottomLineColor = 'green'; + } +} + +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); + + generateBar(i, item); + res.push(item); + } + return res; +} + +function runTestCase(container) { + const chart = window.chart = LightweightCharts.createChart(container); + + const mainSeries = chart.addBaselineSeries({ baseValue: { type: 'price', price: 88 } }); + + mainSeries.setData(generateData()); +} diff --git a/tests/e2e/graphics/test-cases/series/baseline.js b/tests/e2e/graphics/test-cases/series/baseline.js index af494b40c2..570d0083da 100644 --- a/tests/e2e/graphics/test-cases/series/baseline.js +++ b/tests/e2e/graphics/test-cases/series/baseline.js @@ -40,7 +40,7 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const mainSeries = chart.addBaselineSeries({ lineWidth: 1, diff --git a/tests/e2e/graphics/test-cases/series/candlesticks-semitransparent.js b/tests/e2e/graphics/test-cases/series/candlesticks-semitransparent.js index e1087742ee..688b541ac2 100644 --- a/tests/e2e/graphics/test-cases/series/candlesticks-semitransparent.js +++ b/tests/e2e/graphics/test-cases/series/candlesticks-semitransparent.js @@ -23,7 +23,7 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const mainSeries = chart.addCandlestickSeries({ borderColor: 'rgba(0, 0, 255, 0.2)', diff --git a/tests/e2e/graphics/test-cases/series/candlesticks-with-custom-colors.js b/tests/e2e/graphics/test-cases/series/candlesticks-with-custom-colors.js index 7db7e78b62..c09f64dac8 100644 --- a/tests/e2e/graphics/test-cases/series/candlesticks-with-custom-colors.js +++ b/tests/e2e/graphics/test-cases/series/candlesticks-with-custom-colors.js @@ -29,7 +29,7 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const mainSeries = chart.addCandlestickSeries(); diff --git a/tests/e2e/graphics/test-cases/series/candlesticks-with-huge-range.js b/tests/e2e/graphics/test-cases/series/candlesticks-with-huge-range.js index 0b89090a6d..569027543d 100644 --- a/tests/e2e/graphics/test-cases/series/candlesticks-with-huge-range.js +++ b/tests/e2e/graphics/test-cases/series/candlesticks-with-huge-range.js @@ -23,7 +23,7 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const mainSeries = chart.addCandlestickSeries(); diff --git a/tests/e2e/graphics/test-cases/series/candlesticks.js b/tests/e2e/graphics/test-cases/series/candlesticks.js index 1318bfb311..d7d1c3e262 100644 --- a/tests/e2e/graphics/test-cases/series/candlesticks.js +++ b/tests/e2e/graphics/test-cases/series/candlesticks.js @@ -23,7 +23,7 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const mainSeries = chart.addCandlestickSeries(); diff --git a/tests/e2e/graphics/test-cases/series/candlestics-with-whitespaces.js b/tests/e2e/graphics/test-cases/series/candlestics-with-whitespaces.js index f737a6db11..d18da12306 100644 --- a/tests/e2e/graphics/test-cases/series/candlestics-with-whitespaces.js +++ b/tests/e2e/graphics/test-cases/series/candlestics-with-whitespaces.js @@ -25,7 +25,7 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const mainSeries = chart.addCandlestickSeries(); diff --git a/tests/e2e/graphics/test-cases/series/change-colors-via-set-data-in-histogram.js b/tests/e2e/graphics/test-cases/series/change-colors-via-set-data-in-histogram.js index be8d07f2f3..961c6cd253 100644 --- a/tests/e2e/graphics/test-cases/series/change-colors-via-set-data-in-histogram.js +++ b/tests/e2e/graphics/test-cases/series/change-colors-via-set-data-in-histogram.js @@ -14,7 +14,7 @@ function generateData(colors) { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const mainSeries = chart.addHistogramSeries(); diff --git a/tests/e2e/graphics/test-cases/series/crosshair-marker-border-width.js b/tests/e2e/graphics/test-cases/series/crosshair-marker-border-width.js new file mode 100644 index 0000000000..2b5acb2fa3 --- /dev/null +++ b/tests/e2e/graphics/test-cases/series/crosshair-marker-border-width.js @@ -0,0 +1,35 @@ +function generateData(step) { + const res = []; + const time = new Date(Date.UTC(2018, 0, 1, 0, 0, 0, 0)); + let value = step > 0 ? 0 : 500; + for (let i = 0; i < 500; ++i) { + res.push({ + time: time.getTime() / 1000, + value: value, + }); + + value += step; + + time.setUTCDate(time.getUTCDate() + 1); + } + return res; +} + +function runTestCase(container) { + const chart = window.chart = LightweightCharts.createChart(container); + + const areaSeries = chart.addAreaSeries({ + crosshairMarkerBorderWidth: 5, + crosshairMarkerBorderColor: 'yellow', + crosshairMarkerBackgroundColor: 'red', + }); + + const lineSeries = chart.addLineSeries({ + crosshairMarkerBorderWidth: 1, + crosshairMarkerBorderColor: 'blue', + crosshairMarkerBackgroundColor: 'green', + }); + + areaSeries.setData(generateData(1)); + lineSeries.setData(generateData(-1)); +} diff --git a/tests/e2e/graphics/test-cases/series/crosshair-marker-colors-change-to-default.js b/tests/e2e/graphics/test-cases/series/crosshair-marker-colors-change-to-default.js index daf4e0dc07..4c491b716e 100644 --- a/tests/e2e/graphics/test-cases/series/crosshair-marker-colors-change-to-default.js +++ b/tests/e2e/graphics/test-cases/series/crosshair-marker-colors-change-to-default.js @@ -16,7 +16,7 @@ function generateData(step) { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const areaSeries = chart.addAreaSeries({ crosshairMarkerBorderColor: '#ff00ff', diff --git a/tests/e2e/graphics/test-cases/series/crosshair-marker-colors-change.js b/tests/e2e/graphics/test-cases/series/crosshair-marker-colors-change.js index 8517cc235a..cae8a5633c 100644 --- a/tests/e2e/graphics/test-cases/series/crosshair-marker-colors-change.js +++ b/tests/e2e/graphics/test-cases/series/crosshair-marker-colors-change.js @@ -16,7 +16,7 @@ function generateData(step) { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const areaSeries = chart.addAreaSeries(); const lineSeries = chart.addLineSeries(); diff --git a/tests/e2e/graphics/test-cases/series/crosshair-marker-colors.js b/tests/e2e/graphics/test-cases/series/crosshair-marker-colors.js index f9de4e78a9..ac90cfdde8 100644 --- a/tests/e2e/graphics/test-cases/series/crosshair-marker-colors.js +++ b/tests/e2e/graphics/test-cases/series/crosshair-marker-colors.js @@ -16,7 +16,7 @@ function generateData(step) { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const areaSeries = chart.addAreaSeries({ crosshairMarkerBorderColor: 'yellow', diff --git a/tests/e2e/graphics/test-cases/series/curved-line-2-points.js b/tests/e2e/graphics/test-cases/series/curved-line-2-points.js index 2d373fe07f..6c3a6567b6 100644 --- a/tests/e2e/graphics/test-cases/series/curved-line-2-points.js +++ b/tests/e2e/graphics/test-cases/series/curved-line-2-points.js @@ -1,5 +1,5 @@ function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const lineSeries = chart.addLineSeries({ lineType: LightweightCharts.LineType.Curved, diff --git a/tests/e2e/graphics/test-cases/series/curved-line-3-points.js b/tests/e2e/graphics/test-cases/series/curved-line-3-points.js index 52e72641e4..36f9c4dc65 100644 --- a/tests/e2e/graphics/test-cases/series/curved-line-3-points.js +++ b/tests/e2e/graphics/test-cases/series/curved-line-3-points.js @@ -1,5 +1,5 @@ function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const lineSeries = chart.addLineSeries({ lineType: LightweightCharts.LineType.Curved, diff --git a/tests/e2e/graphics/test-cases/series/curved-line-area.js b/tests/e2e/graphics/test-cases/series/curved-line-area.js index 7bd0ded7d4..1667f060da 100644 --- a/tests/e2e/graphics/test-cases/series/curved-line-area.js +++ b/tests/e2e/graphics/test-cases/series/curved-line-area.js @@ -15,7 +15,7 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const areaSeries = chart.addAreaSeries({ lineType: LightweightCharts.LineType.Curved, diff --git a/tests/e2e/graphics/test-cases/series/curved-line-baseline.js b/tests/e2e/graphics/test-cases/series/curved-line-baseline.js index c49ace1e24..afa7a12c9b 100644 --- a/tests/e2e/graphics/test-cases/series/curved-line-baseline.js +++ b/tests/e2e/graphics/test-cases/series/curved-line-baseline.js @@ -15,7 +15,7 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const baselineSeries = chart.addBaselineSeries({ lineType: LightweightCharts.LineType.Curved, diff --git a/tests/e2e/graphics/test-cases/series/curved-line-colored-items.js b/tests/e2e/graphics/test-cases/series/curved-line-colored-items.js index 17e7806c60..c184059ac1 100644 --- a/tests/e2e/graphics/test-cases/series/curved-line-colored-items.js +++ b/tests/e2e/graphics/test-cases/series/curved-line-colored-items.js @@ -18,7 +18,7 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const lineSeries = chart.addLineSeries({ lineType: LightweightCharts.LineType.Curved, diff --git a/tests/e2e/graphics/test-cases/series/hidden-series-and-autoscale.js b/tests/e2e/graphics/test-cases/series/hidden-series-and-autoscale.js index b44f2a44d0..77eabee7ef 100644 --- a/tests/e2e/graphics/test-cases/series/hidden-series-and-autoscale.js +++ b/tests/e2e/graphics/test-cases/series/hidden-series-and-autoscale.js @@ -14,7 +14,7 @@ function generateData(mul = 1) { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const lineSeries = chart.addLineSeries({ visible: false }); lineSeries.setData(generateData()); diff --git a/tests/e2e/graphics/test-cases/series/histogram-add-new-color-on-update.js b/tests/e2e/graphics/test-cases/series/histogram-add-new-color-on-update.js index 64752d3c1e..ed03319f82 100644 --- a/tests/e2e/graphics/test-cases/series/histogram-add-new-color-on-update.js +++ b/tests/e2e/graphics/test-cases/series/histogram-add-new-color-on-update.js @@ -20,7 +20,7 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const mainSeries = chart.addHistogramSeries({ lineWidth: 1, diff --git a/tests/e2e/graphics/test-cases/series/histogram-change-default-color.js b/tests/e2e/graphics/test-cases/series/histogram-change-default-color.js index 0fb20dc171..cbb7351bf9 100644 --- a/tests/e2e/graphics/test-cases/series/histogram-change-default-color.js +++ b/tests/e2e/graphics/test-cases/series/histogram-change-default-color.js @@ -1,5 +1,5 @@ function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const series = chart.addHistogramSeries({ color: 'blue', diff --git a/tests/e2e/graphics/test-cases/series/histogram-out-of-range.js b/tests/e2e/graphics/test-cases/series/histogram-out-of-range.js index 81bc67e6ee..73d042f6b3 100644 --- a/tests/e2e/graphics/test-cases/series/histogram-out-of-range.js +++ b/tests/e2e/graphics/test-cases/series/histogram-out-of-range.js @@ -1,7 +1,7 @@ // see https://github.com/tradingview/lightweight-charts/issues/133 function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const series = chart.addCandlestickSeries(); series.setData([ diff --git a/tests/e2e/graphics/test-cases/series/histogram-update-without-set-data.js b/tests/e2e/graphics/test-cases/series/histogram-update-without-set-data.js index 4d4a3061a5..acd60555fa 100644 --- a/tests/e2e/graphics/test-cases/series/histogram-update-without-set-data.js +++ b/tests/e2e/graphics/test-cases/series/histogram-update-without-set-data.js @@ -2,7 +2,7 @@ // https://github.com/tradingview/lightweight-charts/issues/110 function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const areaSeries = chart.addAreaSeries(); const volumeSeries = chart.addHistogramSeries(); diff --git a/tests/e2e/graphics/test-cases/series/histogram-with-whitespaces.js b/tests/e2e/graphics/test-cases/series/histogram-with-whitespaces.js index e17d001b34..dbfc2b29a4 100644 --- a/tests/e2e/graphics/test-cases/series/histogram-with-whitespaces.js +++ b/tests/e2e/graphics/test-cases/series/histogram-with-whitespaces.js @@ -17,7 +17,7 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const mainSeries = chart.addHistogramSeries(); diff --git a/tests/e2e/graphics/test-cases/series/histogram.js b/tests/e2e/graphics/test-cases/series/histogram.js index 952f57f760..be871f5586 100644 --- a/tests/e2e/graphics/test-cases/series/histogram.js +++ b/tests/e2e/graphics/test-cases/series/histogram.js @@ -20,7 +20,7 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const mainSeries = chart.addHistogramSeries({ lineWidth: 1, diff --git a/tests/e2e/graphics/test-cases/series/line-dotted.js b/tests/e2e/graphics/test-cases/series/line-dotted.js index d3b257b821..7137310698 100644 --- a/tests/e2e/graphics/test-cases/series/line-dotted.js +++ b/tests/e2e/graphics/test-cases/series/line-dotted.js @@ -13,7 +13,7 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const mainSeries = chart.addLineSeries({ lineWidth: 1, diff --git a/tests/e2e/graphics/test-cases/series/line-overlap.js b/tests/e2e/graphics/test-cases/series/line-overlap.js index 66249d4cb3..e0169a4c44 100644 --- a/tests/e2e/graphics/test-cases/series/line-overlap.js +++ b/tests/e2e/graphics/test-cases/series/line-overlap.js @@ -13,7 +13,7 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const mainSeries = chart.addLineSeries({ lineWidth: 1, diff --git a/tests/e2e/graphics/test-cases/series/line-with-custom-color-change-color-later.js b/tests/e2e/graphics/test-cases/series/line-with-custom-color-change-color-later.js index 1a136b2361..2e6cdf98fc 100644 --- a/tests/e2e/graphics/test-cases/series/line-with-custom-color-change-color-later.js +++ b/tests/e2e/graphics/test-cases/series/line-with-custom-color-change-color-later.js @@ -1,5 +1,5 @@ function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const mainSeries = chart.addLineSeries(); diff --git a/tests/e2e/graphics/test-cases/series/line-with-custom-color.js b/tests/e2e/graphics/test-cases/series/line-with-custom-color.js index 9627442aaa..21b4782c0d 100644 --- a/tests/e2e/graphics/test-cases/series/line-with-custom-color.js +++ b/tests/e2e/graphics/test-cases/series/line-with-custom-color.js @@ -1,5 +1,5 @@ function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const mainSeries = chart.addLineSeries(); diff --git a/tests/e2e/graphics/test-cases/series/line-with-steps-with-custom-color.js b/tests/e2e/graphics/test-cases/series/line-with-steps-with-custom-color.js index 04518f23a2..e263ead396 100644 --- a/tests/e2e/graphics/test-cases/series/line-with-steps-with-custom-color.js +++ b/tests/e2e/graphics/test-cases/series/line-with-steps-with-custom-color.js @@ -1,5 +1,5 @@ function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const mainSeries = chart.addLineSeries({ lineType: LightweightCharts.LineType.WithSteps, diff --git a/tests/e2e/graphics/test-cases/series/line-with-whitespaces.js b/tests/e2e/graphics/test-cases/series/line-with-whitespaces.js index fe08019fa7..f7cd6a5a0d 100644 --- a/tests/e2e/graphics/test-cases/series/line-with-whitespaces.js +++ b/tests/e2e/graphics/test-cases/series/line-with-whitespaces.js @@ -17,7 +17,7 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const mainSeries = chart.addLineSeries(); diff --git a/tests/e2e/graphics/test-cases/series/line.js b/tests/e2e/graphics/test-cases/series/line.js index df1c7da7eb..ef7aca38e1 100644 --- a/tests/e2e/graphics/test-cases/series/line.js +++ b/tests/e2e/graphics/test-cases/series/line.js @@ -13,7 +13,7 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const mainSeries = chart.addLineSeries({ lineWidth: 1, diff --git a/tests/e2e/graphics/test-cases/series/no-last-price-animation-on-adding-whitespace.js b/tests/e2e/graphics/test-cases/series/no-last-price-animation-on-adding-whitespace.js index e033c095d6..7c726ec578 100644 --- a/tests/e2e/graphics/test-cases/series/no-last-price-animation-on-adding-whitespace.js +++ b/tests/e2e/graphics/test-cases/series/no-last-price-animation-on-adding-whitespace.js @@ -1,5 +1,5 @@ function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const series = chart.addLineSeries({ lastPriceAnimation: LightweightCharts.LastPriceAnimationMode.OnDataUpdate, diff --git a/tests/e2e/graphics/test-cases/series/no-last-price-animation-on-first-set-data-after-reseting-data.js b/tests/e2e/graphics/test-cases/series/no-last-price-animation-on-first-set-data-after-reseting-data.js index bb121b9e3b..1c7f6d0771 100644 --- a/tests/e2e/graphics/test-cases/series/no-last-price-animation-on-first-set-data-after-reseting-data.js +++ b/tests/e2e/graphics/test-cases/series/no-last-price-animation-on-first-set-data-after-reseting-data.js @@ -1,5 +1,5 @@ function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const series = chart.addLineSeries({ lastPriceAnimation: LightweightCharts.LastPriceAnimationMode.OnDataUpdate, diff --git a/tests/e2e/graphics/test-cases/series/no-last-price-animation-on-first-set-data.js b/tests/e2e/graphics/test-cases/series/no-last-price-animation-on-first-set-data.js index 04ee7b94c8..d467005349 100644 --- a/tests/e2e/graphics/test-cases/series/no-last-price-animation-on-first-set-data.js +++ b/tests/e2e/graphics/test-cases/series/no-last-price-animation-on-first-set-data.js @@ -1,5 +1,5 @@ function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const series = chart.addLineSeries({ lastPriceAnimation: LightweightCharts.LastPriceAnimationMode.OnDataUpdate, diff --git a/tests/e2e/graphics/test-cases/series/no-last-price-animation-on-loading-history-data.js b/tests/e2e/graphics/test-cases/series/no-last-price-animation-on-loading-history-data.js index 07864f7251..d2d67f167d 100644 --- a/tests/e2e/graphics/test-cases/series/no-last-price-animation-on-loading-history-data.js +++ b/tests/e2e/graphics/test-cases/series/no-last-price-animation-on-loading-history-data.js @@ -1,5 +1,5 @@ function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const series = chart.addLineSeries({ lastPriceAnimation: LightweightCharts.LastPriceAnimationMode.OnDataUpdate, diff --git a/tests/e2e/graphics/test-cases/series/no-visible-points-in-the-middle.js b/tests/e2e/graphics/test-cases/series/no-visible-points-in-the-middle.js index 3635a10098..6c7a3d29b4 100644 --- a/tests/e2e/graphics/test-cases/series/no-visible-points-in-the-middle.js +++ b/tests/e2e/graphics/test-cases/series/no-visible-points-in-the-middle.js @@ -19,7 +19,7 @@ function generateData(priceOffset) { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container, { + const chart = window.chart = LightweightCharts.createChart(container, { timeScale: { rightOffset: 7, barSpacing: 50, diff --git a/tests/e2e/graphics/test-cases/series/overlay-series-title-only-overlay-price-scale.js b/tests/e2e/graphics/test-cases/series/overlay-series-title-only-overlay-price-scale.js index c958b9e23c..217446ed42 100644 --- a/tests/e2e/graphics/test-cases/series/overlay-series-title-only-overlay-price-scale.js +++ b/tests/e2e/graphics/test-cases/series/overlay-series-title-only-overlay-price-scale.js @@ -13,7 +13,7 @@ function generateData(func) { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const line1 = chart.addLineSeries({ priceScaleId: 'overlay', diff --git a/tests/e2e/graphics/test-cases/series/overlay-series-title-only-overlay-stacked.js b/tests/e2e/graphics/test-cases/series/overlay-series-title-only-overlay-stacked.js index b79dcf8f3c..495fa077f6 100644 --- a/tests/e2e/graphics/test-cases/series/overlay-series-title-only-overlay-stacked.js +++ b/tests/e2e/graphics/test-cases/series/overlay-series-title-only-overlay-stacked.js @@ -13,7 +13,7 @@ function generateData(func, shouldAddValue) { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); chart.priceScale('right').applyOptions({ visible: false, diff --git a/tests/e2e/graphics/test-cases/series/overlay-series-title.js b/tests/e2e/graphics/test-cases/series/overlay-series-title.js index d90f2448ac..a3ed2a990e 100644 --- a/tests/e2e/graphics/test-cases/series/overlay-series-title.js +++ b/tests/e2e/graphics/test-cases/series/overlay-series-title.js @@ -13,7 +13,7 @@ function generateData(func) { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const mainSeries = chart.addLineSeries({ color: '#ff0000', diff --git a/tests/e2e/graphics/test-cases/series/override-autoscale-fixed-range.js b/tests/e2e/graphics/test-cases/series/override-autoscale-fixed-range.js index 07eb048257..48b0dd8f2d 100644 --- a/tests/e2e/graphics/test-cases/series/override-autoscale-fixed-range.js +++ b/tests/e2e/graphics/test-cases/series/override-autoscale-fixed-range.js @@ -13,7 +13,7 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container, { + const chart = window.chart = LightweightCharts.createChart(container, { rightPriceScale: { scaleMargins: { bottom: 0, diff --git a/tests/e2e/graphics/test-cases/series/price-line-apply-options.js b/tests/e2e/graphics/test-cases/series/price-line-apply-options.js index d2cde33da2..bf22b283a9 100644 --- a/tests/e2e/graphics/test-cases/series/price-line-apply-options.js +++ b/tests/e2e/graphics/test-cases/series/price-line-apply-options.js @@ -13,7 +13,7 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const series = chart.addLineSeries(); series.setData(generateData()); diff --git a/tests/e2e/graphics/test-cases/series/price-line-line-visibility.js b/tests/e2e/graphics/test-cases/series/price-line-line-visibility.js index f946e7a54e..6e4632f769 100644 --- a/tests/e2e/graphics/test-cases/series/price-line-line-visibility.js +++ b/tests/e2e/graphics/test-cases/series/price-line-line-visibility.js @@ -13,7 +13,7 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const series = chart.addLineSeries(); series.setData(generateData()); diff --git a/tests/e2e/graphics/test-cases/series/price-line-overlapping-series-title.js b/tests/e2e/graphics/test-cases/series/price-line-overlapping-series-title.js index 76892b47c4..3c5dcea5cf 100644 --- a/tests/e2e/graphics/test-cases/series/price-line-overlapping-series-title.js +++ b/tests/e2e/graphics/test-cases/series/price-line-overlapping-series-title.js @@ -1,5 +1,5 @@ function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const series = chart.addLineSeries({ title: 'TITLE' }); diff --git a/tests/e2e/graphics/test-cases/series/price-line-with-percentage-scale-mode.js b/tests/e2e/graphics/test-cases/series/price-line-with-percentage-scale-mode.js index bb498a69e5..8918efdc32 100644 --- a/tests/e2e/graphics/test-cases/series/price-line-with-percentage-scale-mode.js +++ b/tests/e2e/graphics/test-cases/series/price-line-with-percentage-scale-mode.js @@ -13,7 +13,7 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container, { + const chart = window.chart = LightweightCharts.createChart(container, { rightPriceScale: { mode: LightweightCharts.PriceScaleMode.Percentage, }, diff --git a/tests/e2e/graphics/test-cases/series/price-lines-align-labels.js b/tests/e2e/graphics/test-cases/series/price-lines-align-labels.js index 0ab956419d..67a84127be 100644 --- a/tests/e2e/graphics/test-cases/series/price-lines-align-labels.js +++ b/tests/e2e/graphics/test-cases/series/price-lines-align-labels.js @@ -13,7 +13,7 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container, { + const chart = window.chart = LightweightCharts.createChart(container, { rightPriceScale: { mode: LightweightCharts.PriceScaleMode.Percentage, alignLabels: true, diff --git a/tests/e2e/graphics/test-cases/series/price-lines-do-not-align-labels.js b/tests/e2e/graphics/test-cases/series/price-lines-do-not-align-labels.js index f18a8ef258..b3f0ca0d7c 100644 --- a/tests/e2e/graphics/test-cases/series/price-lines-do-not-align-labels.js +++ b/tests/e2e/graphics/test-cases/series/price-lines-do-not-align-labels.js @@ -13,7 +13,7 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container, { + const chart = window.chart = LightweightCharts.createChart(container, { rightPriceScale: { mode: LightweightCharts.PriceScaleMode.Percentage, alignLabels: false, diff --git a/tests/e2e/graphics/test-cases/series/price-lines-on-two-axis.js b/tests/e2e/graphics/test-cases/series/price-lines-on-two-axis.js index 9b4f7e60ef..d4a932481e 100644 --- a/tests/e2e/graphics/test-cases/series/price-lines-on-two-axis.js +++ b/tests/e2e/graphics/test-cases/series/price-lines-on-two-axis.js @@ -13,7 +13,7 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container, { + const chart = window.chart = LightweightCharts.createChart(container, { leftPriceScale: { visible: true, borderColor: '#EFF2F5', diff --git a/tests/e2e/graphics/test-cases/series/price-lines-remove.js b/tests/e2e/graphics/test-cases/series/price-lines-remove.js index 51b8d19f5e..a83347cd83 100644 --- a/tests/e2e/graphics/test-cases/series/price-lines-remove.js +++ b/tests/e2e/graphics/test-cases/series/price-lines-remove.js @@ -13,7 +13,7 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const series = chart.addLineSeries(); series.setData(generateData()); diff --git a/tests/e2e/graphics/test-cases/series/price-lines.js b/tests/e2e/graphics/test-cases/series/price-lines.js index e9dfbde054..a7810839ec 100644 --- a/tests/e2e/graphics/test-cases/series/price-lines.js +++ b/tests/e2e/graphics/test-cases/series/price-lines.js @@ -13,7 +13,7 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const series = chart.addLineSeries(); series.setData(generateData()); diff --git a/tests/e2e/graphics/test-cases/series/series-price-formatter.js b/tests/e2e/graphics/test-cases/series/series-price-formatter.js index 945c92ecd2..f7a8a7a70e 100644 --- a/tests/e2e/graphics/test-cases/series/series-price-formatter.js +++ b/tests/e2e/graphics/test-cases/series/series-price-formatter.js @@ -13,7 +13,7 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const lineSeries = chart.addLineSeries({ priceFormat: { diff --git a/tests/e2e/graphics/test-cases/series/series-type.js b/tests/e2e/graphics/test-cases/series/series-type.js index 600623876e..faa13fb3fc 100644 --- a/tests/e2e/graphics/test-cases/series/series-type.js +++ b/tests/e2e/graphics/test-cases/series/series-type.js @@ -1,5 +1,5 @@ function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const line = chart.addLineSeries(); const area = chart.addAreaSeries(); const candlestick = chart.addCandlestickSeries(); diff --git a/tests/e2e/graphics/test-cases/series/series-visibility.js b/tests/e2e/graphics/test-cases/series/series-visibility.js index 892526812e..bacd76bccf 100644 --- a/tests/e2e/graphics/test-cases/series/series-visibility.js +++ b/tests/e2e/graphics/test-cases/series/series-visibility.js @@ -33,7 +33,7 @@ function generateBarData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container, { + const chart = window.chart = LightweightCharts.createChart(container, { rightPriceScale: { mode: LightweightCharts.PriceScaleMode.IndexedTo100, }, diff --git a/tests/e2e/graphics/test-cases/series/set-empty-data-to-histogram.js b/tests/e2e/graphics/test-cases/series/set-empty-data-to-histogram.js index c7eaaae0df..57dc91b395 100644 --- a/tests/e2e/graphics/test-cases/series/set-empty-data-to-histogram.js +++ b/tests/e2e/graphics/test-cases/series/set-empty-data-to-histogram.js @@ -22,7 +22,7 @@ function generateColoredData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const areaSeries = chart.addAreaSeries(); const volumeSeries = chart.addHistogramSeries(); diff --git a/tests/e2e/graphics/test-cases/series/setting-same-data-after-time.js b/tests/e2e/graphics/test-cases/series/setting-same-data-after-time.js index a35dfbd98e..e847d8b9a6 100644 --- a/tests/e2e/graphics/test-cases/series/setting-same-data-after-time.js +++ b/tests/e2e/graphics/test-cases/series/setting-same-data-after-time.js @@ -3,7 +3,7 @@ function copy(data) { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const lineSeries = chart.addLineSeries(); const data = [ diff --git a/tests/e2e/graphics/test-cases/series/several-non-regular-series.js b/tests/e2e/graphics/test-cases/series/several-non-regular-series.js index 512e5bb6f3..1fb4e3c386 100644 --- a/tests/e2e/graphics/test-cases/series/several-non-regular-series.js +++ b/tests/e2e/graphics/test-cases/series/several-non-regular-series.js @@ -1,5 +1,5 @@ function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const lineSeries1 = chart.addLineSeries(); const lineSeries2 = chart.addLineSeries(); diff --git a/tests/e2e/graphics/test-cases/series/single-point-area.js b/tests/e2e/graphics/test-cases/series/single-point-area.js index 4a7e858df9..2834801910 100644 --- a/tests/e2e/graphics/test-cases/series/single-point-area.js +++ b/tests/e2e/graphics/test-cases/series/single-point-area.js @@ -1,5 +1,5 @@ function runTestCase(container) { - const chart = LightweightCharts.createChart(container, { + const chart = window.chart = LightweightCharts.createChart(container, { timeScale: { rightOffset: 7, barSpacing: 50, diff --git a/tests/e2e/graphics/test-cases/series/single-point-line.js b/tests/e2e/graphics/test-cases/series/single-point-line.js index 7692b51a14..90233646bf 100644 --- a/tests/e2e/graphics/test-cases/series/single-point-line.js +++ b/tests/e2e/graphics/test-cases/series/single-point-line.js @@ -1,5 +1,5 @@ function runTestCase(container) { - const chart = LightweightCharts.createChart(container, { + const chart = window.chart = LightweightCharts.createChart(container, { timeScale: { rightOffset: 7, barSpacing: 50, diff --git a/tests/e2e/graphics/test-cases/series/single-visible-point-line-first-bar.js b/tests/e2e/graphics/test-cases/series/single-visible-point-line-first-bar.js index 8549835f52..5bc9ff5e08 100644 --- a/tests/e2e/graphics/test-cases/series/single-visible-point-line-first-bar.js +++ b/tests/e2e/graphics/test-cases/series/single-visible-point-line-first-bar.js @@ -19,7 +19,7 @@ function generateData(priceOffset) { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container, { + const chart = window.chart = LightweightCharts.createChart(container, { timeScale: { rightOffset: 7, barSpacing: 50, diff --git a/tests/e2e/graphics/test-cases/series/single-visible-point-line-last-bar.js b/tests/e2e/graphics/test-cases/series/single-visible-point-line-last-bar.js index ee1cf9cc87..f4ef7070f4 100644 --- a/tests/e2e/graphics/test-cases/series/single-visible-point-line-last-bar.js +++ b/tests/e2e/graphics/test-cases/series/single-visible-point-line-last-bar.js @@ -19,7 +19,7 @@ function generateData(priceOffset) { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container, { + const chart = window.chart = LightweightCharts.createChart(container, { timeScale: { rightOffset: 7, barSpacing: 50, diff --git a/tests/e2e/graphics/test-cases/series/step-line-area.js b/tests/e2e/graphics/test-cases/series/step-line-area.js index f1648cb251..9a4874beaa 100644 --- a/tests/e2e/graphics/test-cases/series/step-line-area.js +++ b/tests/e2e/graphics/test-cases/series/step-line-area.js @@ -13,7 +13,7 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const mainSeries = chart.addAreaSeries({ lineType: LightweightCharts.LineType.WithSteps, diff --git a/tests/e2e/graphics/test-cases/series/step-line-baseline.js b/tests/e2e/graphics/test-cases/series/step-line-baseline.js index 99aed3b547..d0d7f0f326 100644 --- a/tests/e2e/graphics/test-cases/series/step-line-baseline.js +++ b/tests/e2e/graphics/test-cases/series/step-line-baseline.js @@ -15,7 +15,7 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const baselineSeries = chart.addBaselineSeries({ lineType: LightweightCharts.LineType.WithSteps, diff --git a/tests/e2e/graphics/test-cases/series/step-line.js b/tests/e2e/graphics/test-cases/series/step-line.js index bd13c9c683..466a421fb4 100644 --- a/tests/e2e/graphics/test-cases/series/step-line.js +++ b/tests/e2e/graphics/test-cases/series/step-line.js @@ -13,7 +13,7 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const mainSeries = chart.addLineSeries({ lineWidth: 1, diff --git a/tests/e2e/graphics/test-cases/series/two-series-one-not-autoscaled.js b/tests/e2e/graphics/test-cases/series/two-series-one-not-autoscaled.js index b9d6415acb..50a50ddc19 100644 --- a/tests/e2e/graphics/test-cases/series/two-series-one-not-autoscaled.js +++ b/tests/e2e/graphics/test-cases/series/two-series-one-not-autoscaled.js @@ -14,7 +14,7 @@ function generateData(amplitude) { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container, { + const chart = window.chart = LightweightCharts.createChart(container, { rightPriceScale: { scaleMargins: { bottom: 0, diff --git a/tests/e2e/graphics/test-cases/series/update-baseline-base-value.js b/tests/e2e/graphics/test-cases/series/update-baseline-base-value.js index a91c476db9..e69280172f 100644 --- a/tests/e2e/graphics/test-cases/series/update-baseline-base-value.js +++ b/tests/e2e/graphics/test-cases/series/update-baseline-base-value.js @@ -40,7 +40,7 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const mainSeries = chart.addBaselineSeries({ lineWidth: 1, diff --git a/tests/e2e/graphics/test-cases/series/update-removed-series-data.js b/tests/e2e/graphics/test-cases/series/update-removed-series-data.js index abdc62a454..b5557d6718 100644 --- a/tests/e2e/graphics/test-cases/series/update-removed-series-data.js +++ b/tests/e2e/graphics/test-cases/series/update-removed-series-data.js @@ -1,3 +1,6 @@ +// Ignore the mouse movement check because height of chart is too short +window.ignoreMouseMove = true; + function runTestCase(container) { const data1 = [ { @@ -29,7 +32,7 @@ function runTestCase(container) { }, ]; - const chart = LightweightCharts.createChart(container, { + const chart = window.chart = LightweightCharts.createChart(container, { width: 600, height: 300, timeScale: { diff --git a/tests/e2e/graphics/test-cases/series/whitespace-updates.js b/tests/e2e/graphics/test-cases/series/whitespace-updates.js index 9b7b4099dc..c21ebe8473 100644 --- a/tests/e2e/graphics/test-cases/series/whitespace-updates.js +++ b/tests/e2e/graphics/test-cases/series/whitespace-updates.js @@ -1,5 +1,5 @@ function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const mainSeries = chart.addCandlestickSeries(); diff --git a/tests/e2e/graphics/test-cases/set-price-line-label.js b/tests/e2e/graphics/test-cases/set-price-line-label.js index df2a7956de..a0098b1e7d 100644 --- a/tests/e2e/graphics/test-cases/set-price-line-label.js +++ b/tests/e2e/graphics/test-cases/set-price-line-label.js @@ -13,7 +13,7 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const mainSeries = chart.addLineSeries(); diff --git a/tests/e2e/graphics/test-cases/take-screenshot.js b/tests/e2e/graphics/test-cases/take-screenshot.js index 8293c6a9eb..4bdadc42fb 100644 --- a/tests/e2e/graphics/test-cases/take-screenshot.js +++ b/tests/e2e/graphics/test-cases/take-screenshot.js @@ -43,8 +43,11 @@ function generateDataHist() { return res; } +// Ignore the mouse movement check because we are covering the chart. +window.ignoreMouseMove = true; + function runTestCase(container) { - const chart = LightweightCharts.createChart(container, { + const chart = window.chart = LightweightCharts.createChart(container, { timeScale: { barSpacing: 20, }, diff --git a/tests/e2e/graphics/test-cases/time-scale/add-data-to-left-two-series.js b/tests/e2e/graphics/test-cases/time-scale/add-data-to-left-two-series.js index a1fac554bf..6893635ebf 100644 --- a/tests/e2e/graphics/test-cases/time-scale/add-data-to-left-two-series.js +++ b/tests/e2e/graphics/test-cases/time-scale/add-data-to-left-two-series.js @@ -37,7 +37,7 @@ let areaSeries2 = null; const ONE_DAY_IN_SEC = 24 * 60 * 60; function createChart(container) { - chart = LightweightCharts.createChart(container); + chart = window.chart = LightweightCharts.createChart(container); } function createFirstSeries() { diff --git a/tests/e2e/graphics/test-cases/time-scale/add-data-to-left.js b/tests/e2e/graphics/test-cases/time-scale/add-data-to-left.js index 871dccf004..5a16f19f75 100644 --- a/tests/e2e/graphics/test-cases/time-scale/add-data-to-left.js +++ b/tests/e2e/graphics/test-cases/time-scale/add-data-to-left.js @@ -35,7 +35,7 @@ let areaSeries = null; const ONE_DAY_IN_SEC = 24 * 60 * 60; function createChart(container) { - chart = LightweightCharts.createChart(container); + chart = window.chart = LightweightCharts.createChart(container); } function createOneSeries() { diff --git a/tests/e2e/graphics/test-cases/time-scale/add-data-to-right-two-series.js b/tests/e2e/graphics/test-cases/time-scale/add-data-to-right-two-series.js index cf379f8fa1..e8f74df487 100644 --- a/tests/e2e/graphics/test-cases/time-scale/add-data-to-right-two-series.js +++ b/tests/e2e/graphics/test-cases/time-scale/add-data-to-right-two-series.js @@ -37,7 +37,7 @@ let areaSeries2 = null; const ONE_DAY_IN_SEC = 24 * 60 * 60; function createChart(container) { - chart = LightweightCharts.createChart(container); + chart = window.chart = LightweightCharts.createChart(container); } function createFirstSeries() { diff --git a/tests/e2e/graphics/test-cases/time-scale/add-data-to-right.js b/tests/e2e/graphics/test-cases/time-scale/add-data-to-right.js index a458eef662..c2593a5a41 100644 --- a/tests/e2e/graphics/test-cases/time-scale/add-data-to-right.js +++ b/tests/e2e/graphics/test-cases/time-scale/add-data-to-right.js @@ -35,7 +35,7 @@ let areaSeries = null; const ONE_DAY_IN_SEC = 24 * 60 * 60; function createChart(container) { - chart = LightweightCharts.createChart(container); + chart = window.chart = LightweightCharts.createChart(container); } function createOneSeries() { diff --git a/tests/e2e/graphics/test-cases/time-scale/do-not-shift-on-new-data-from-right.js b/tests/e2e/graphics/test-cases/time-scale/do-not-shift-on-new-data-from-right.js index cac4564265..9142ae1984 100644 --- a/tests/e2e/graphics/test-cases/time-scale/do-not-shift-on-new-data-from-right.js +++ b/tests/e2e/graphics/test-cases/time-scale/do-not-shift-on-new-data-from-right.js @@ -13,7 +13,7 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container, { + const chart = window.chart = LightweightCharts.createChart(container, { timeScale: { rightOffset: 10, shiftVisibleRangeOnNewBar: false, diff --git a/tests/e2e/graphics/test-cases/time-scale/fit-content-pricescale-changes.js b/tests/e2e/graphics/test-cases/time-scale/fit-content-pricescale-changes.js index 7e13231854..15c1fca7cc 100644 --- a/tests/e2e/graphics/test-cases/time-scale/fit-content-pricescale-changes.js +++ b/tests/e2e/graphics/test-cases/time-scale/fit-content-pricescale-changes.js @@ -13,8 +13,11 @@ function generateData(down) { return res; } +// Ignore the mouse movement check because height of chart is too short +window.ignoreMouseMove = true; + function runTestCase(container) { - const chart = LightweightCharts.createChart(container, { + const chart = window.chart = LightweightCharts.createChart(container, { width: 600, height: 300, }); diff --git a/tests/e2e/graphics/test-cases/time-scale/hidden-time-scale-size.js b/tests/e2e/graphics/test-cases/time-scale/hidden-time-scale-size.js index da97ad7069..f776c9c532 100644 --- a/tests/e2e/graphics/test-cases/time-scale/hidden-time-scale-size.js +++ b/tests/e2e/graphics/test-cases/time-scale/hidden-time-scale-size.js @@ -1,5 +1,5 @@ async function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const timeScale = chart.timeScale(); diff --git a/tests/e2e/graphics/test-cases/time-scale/lock-visible-range-on-resize-and-fixing-edges.js b/tests/e2e/graphics/test-cases/time-scale/lock-visible-range-on-resize-and-fixing-edges.js index 72ec4adf81..fc206e8137 100644 --- a/tests/e2e/graphics/test-cases/time-scale/lock-visible-range-on-resize-and-fixing-edges.js +++ b/tests/e2e/graphics/test-cases/time-scale/lock-visible-range-on-resize-and-fixing-edges.js @@ -14,7 +14,7 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container, { + const chart = window.chart = LightweightCharts.createChart(container, { timeScale: { fixLeftEdge: true, fixRightEdge: true, diff --git a/tests/e2e/graphics/test-cases/time-scale/min-bar-spacing.js b/tests/e2e/graphics/test-cases/time-scale/min-bar-spacing.js index 261a7c2d78..94c68f6f72 100644 --- a/tests/e2e/graphics/test-cases/time-scale/min-bar-spacing.js +++ b/tests/e2e/graphics/test-cases/time-scale/min-bar-spacing.js @@ -23,7 +23,7 @@ function generateData(startValue) { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container, { + const chart = window.chart = LightweightCharts.createChart(container, { timeScale: { minBarSpacing: 0.001, }, diff --git a/tests/e2e/graphics/test-cases/time-scale/realign-partially-hidden-time-scale-mark-on-left.js b/tests/e2e/graphics/test-cases/time-scale/realign-partially-hidden-time-scale-mark-on-left.js index 0c1a73fc3c..3d7b0bec55 100644 --- a/tests/e2e/graphics/test-cases/time-scale/realign-partially-hidden-time-scale-mark-on-left.js +++ b/tests/e2e/graphics/test-cases/time-scale/realign-partially-hidden-time-scale-mark-on-left.js @@ -69,7 +69,7 @@ function getData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container, { + const chart = window.chart = LightweightCharts.createChart(container, { handleScale: false, handleScroll: false, timeScale: { diff --git a/tests/e2e/graphics/test-cases/time-scale/realign-partially-hidden-time-scale-mark-on-right.js b/tests/e2e/graphics/test-cases/time-scale/realign-partially-hidden-time-scale-mark-on-right.js index 61b9e33109..bbebb96e9e 100644 --- a/tests/e2e/graphics/test-cases/time-scale/realign-partially-hidden-time-scale-mark-on-right.js +++ b/tests/e2e/graphics/test-cases/time-scale/realign-partially-hidden-time-scale-mark-on-right.js @@ -68,8 +68,11 @@ function getData() { ]; } +// Ignore the mouse movement check because height of chart is too short +window.ignoreMouseMove = true; + function runTestCase(container) { - const chart = LightweightCharts.createChart(container, { + const chart = window.chart = LightweightCharts.createChart(container, { height: 200, handleScale: false, handleScroll: false, diff --git a/tests/e2e/graphics/test-cases/time-scale/realign-partially-hidden-time-scale-marks.js b/tests/e2e/graphics/test-cases/time-scale/realign-partially-hidden-time-scale-marks.js index dc4c5cbb60..4384466533 100644 --- a/tests/e2e/graphics/test-cases/time-scale/realign-partially-hidden-time-scale-marks.js +++ b/tests/e2e/graphics/test-cases/time-scale/realign-partially-hidden-time-scale-marks.js @@ -1,5 +1,5 @@ function runTestCase(container) { - const chart = LightweightCharts.createChart(container, { + const chart = window.chart = LightweightCharts.createChart(container, { handleScroll: false, handleScale: false, }); diff --git a/tests/e2e/graphics/test-cases/time-scale/reset-time-scale-after-set-visible-range.js b/tests/e2e/graphics/test-cases/time-scale/reset-time-scale-after-set-visible-range.js index 31a2d8b571..f9c34ff965 100644 --- a/tests/e2e/graphics/test-cases/time-scale/reset-time-scale-after-set-visible-range.js +++ b/tests/e2e/graphics/test-cases/time-scale/reset-time-scale-after-set-visible-range.js @@ -13,7 +13,7 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const mainSeries = chart.addLineSeries(); diff --git a/tests/e2e/graphics/test-cases/time-scale/scroll-to-position-after-set-visible-range.js b/tests/e2e/graphics/test-cases/time-scale/scroll-to-position-after-set-visible-range.js index a792615ef2..629f2f4fec 100644 --- a/tests/e2e/graphics/test-cases/time-scale/scroll-to-position-after-set-visible-range.js +++ b/tests/e2e/graphics/test-cases/time-scale/scroll-to-position-after-set-visible-range.js @@ -13,7 +13,7 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const mainSeries = chart.addLineSeries(); diff --git a/tests/e2e/graphics/test-cases/time-scale/scroll-to-position-with-no-data.js b/tests/e2e/graphics/test-cases/time-scale/scroll-to-position-with-no-data.js index 9b8351e303..a789ec7ca2 100644 --- a/tests/e2e/graphics/test-cases/time-scale/scroll-to-position-with-no-data.js +++ b/tests/e2e/graphics/test-cases/time-scale/scroll-to-position-with-no-data.js @@ -13,7 +13,7 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); chart.timeScale().scrollToPosition(-100000); // to force subscribe and emit event diff --git a/tests/e2e/graphics/test-cases/time-scale/set-and-clear-series.js b/tests/e2e/graphics/test-cases/time-scale/set-and-clear-series.js index b7a6960b7b..f3ec60688d 100644 --- a/tests/e2e/graphics/test-cases/time-scale/set-and-clear-series.js +++ b/tests/e2e/graphics/test-cases/time-scale/set-and-clear-series.js @@ -5,7 +5,7 @@ function runTestCase(container) { { time: 1609632000, value: 700 }, ]; - const chart = LightweightCharts.createChart(container, { + const chart = window.chart = LightweightCharts.createChart(container, { timeScale: { barSpacing: 40, }, diff --git a/tests/e2e/graphics/test-cases/time-scale/set-right-offset-after-set-visible-range.js b/tests/e2e/graphics/test-cases/time-scale/set-right-offset-after-set-visible-range.js index 951539af6a..14ca506fd2 100644 --- a/tests/e2e/graphics/test-cases/time-scale/set-right-offset-after-set-visible-range.js +++ b/tests/e2e/graphics/test-cases/time-scale/set-right-offset-after-set-visible-range.js @@ -13,7 +13,7 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const mainSeries = chart.addLineSeries(); diff --git a/tests/e2e/graphics/test-cases/time-scale/set-visible-logical-range-with-no-data.js b/tests/e2e/graphics/test-cases/time-scale/set-visible-logical-range-with-no-data.js index db1030498e..c44527ff3a 100644 --- a/tests/e2e/graphics/test-cases/time-scale/set-visible-logical-range-with-no-data.js +++ b/tests/e2e/graphics/test-cases/time-scale/set-visible-logical-range-with-no-data.js @@ -13,7 +13,7 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); chart.timeScale().setVisibleLogicalRange({ from: -1001000, to: -1000000 }); // to force subscribe and emit event diff --git a/tests/e2e/graphics/test-cases/time-scale/set-visible-range-after-time.js b/tests/e2e/graphics/test-cases/time-scale/set-visible-range-after-time.js index 973a8053a1..410fceefde 100644 --- a/tests/e2e/graphics/test-cases/time-scale/set-visible-range-after-time.js +++ b/tests/e2e/graphics/test-cases/time-scale/set-visible-range-after-time.js @@ -13,7 +13,7 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const mainSeries = chart.addLineSeries(); diff --git a/tests/e2e/graphics/test-cases/time-scale/set-visible-range-with-small-range.js b/tests/e2e/graphics/test-cases/time-scale/set-visible-range-with-small-range.js index d79575091d..c2f072ba7c 100644 --- a/tests/e2e/graphics/test-cases/time-scale/set-visible-range-with-small-range.js +++ b/tests/e2e/graphics/test-cases/time-scale/set-visible-range-with-small-range.js @@ -13,7 +13,7 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const mainSeries = chart.addLineSeries(); diff --git a/tests/e2e/graphics/test-cases/time-scale/set-visible-range-with-two-series.js b/tests/e2e/graphics/test-cases/time-scale/set-visible-range-with-two-series.js index a80a12f7cd..03de458471 100644 --- a/tests/e2e/graphics/test-cases/time-scale/set-visible-range-with-two-series.js +++ b/tests/e2e/graphics/test-cases/time-scale/set-visible-range-with-two-series.js @@ -1,5 +1,5 @@ function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const line1 = chart.addLineSeries(); line1.setData([ diff --git a/tests/e2e/graphics/test-cases/time-scale/set-visible-range.js b/tests/e2e/graphics/test-cases/time-scale/set-visible-range.js index d5bc1f2719..1f890880bd 100644 --- a/tests/e2e/graphics/test-cases/time-scale/set-visible-range.js +++ b/tests/e2e/graphics/test-cases/time-scale/set-visible-range.js @@ -13,7 +13,7 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const mainSeries = chart.addLineSeries(); diff --git a/tests/e2e/graphics/test-cases/time-scale/time-scale-size-on-changing-chart-size.js b/tests/e2e/graphics/test-cases/time-scale/time-scale-size-on-changing-chart-size.js index 0e1fed30ab..8e545a3d5f 100644 --- a/tests/e2e/graphics/test-cases/time-scale/time-scale-size-on-changing-chart-size.js +++ b/tests/e2e/graphics/test-cases/time-scale/time-scale-size-on-changing-chart-size.js @@ -1,5 +1,8 @@ +// Ignore the mouse movement check because height of chart is too thin +window.ignoreMouseMove = true; + async function runTestCase(container) { - const chart = LightweightCharts.createChart(container, { width: 150 }); + const chart = window.chart = LightweightCharts.createChart(container, { width: 150 }); const timeScale = chart.timeScale(); diff --git a/tests/e2e/graphics/test-cases/time-scale/time-scale-size-with-hidden-price-scale.js b/tests/e2e/graphics/test-cases/time-scale/time-scale-size-with-hidden-price-scale.js index 42fab8f281..e8cad5a169 100644 --- a/tests/e2e/graphics/test-cases/time-scale/time-scale-size-with-hidden-price-scale.js +++ b/tests/e2e/graphics/test-cases/time-scale/time-scale-size-with-hidden-price-scale.js @@ -1,5 +1,5 @@ async function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const timeScale = chart.timeScale(); diff --git a/tests/e2e/graphics/test-cases/transparent-color.js b/tests/e2e/graphics/test-cases/transparent-color.js index 6e16199f87..243cd5b383 100644 --- a/tests/e2e/graphics/test-cases/transparent-color.js +++ b/tests/e2e/graphics/test-cases/transparent-color.js @@ -13,7 +13,7 @@ function generateData() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const mainSeries = chart.addHistogramSeries({ color: 'transparent', diff --git a/tests/e2e/graphics/test-cases/two-scales/basic.js b/tests/e2e/graphics/test-cases/two-scales/basic.js index abc4fe5203..1740ca4e5a 100644 --- a/tests/e2e/graphics/test-cases/two-scales/basic.js +++ b/tests/e2e/graphics/test-cases/two-scales/basic.js @@ -46,7 +46,7 @@ function generateDataHist() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container, { + const chart = window.chart = LightweightCharts.createChart(container, { leftPriceScale: { visible: true, scaleMargins: { diff --git a/tests/e2e/graphics/test-cases/two-scales/empty-price-scale-id.js b/tests/e2e/graphics/test-cases/two-scales/empty-price-scale-id.js index 9b95cde6e6..5a60749963 100644 --- a/tests/e2e/graphics/test-cases/two-scales/empty-price-scale-id.js +++ b/tests/e2e/graphics/test-cases/two-scales/empty-price-scale-id.js @@ -46,7 +46,7 @@ function generateDataLine(offset) { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const mainSeries = chart.addBarSeries({ borderColor: 'rgba(0, 0, 255, 0.2)', diff --git a/tests/e2e/graphics/test-cases/two-scales/left-percent.js b/tests/e2e/graphics/test-cases/two-scales/left-percent.js index 4bf82fe3f7..8cec26592f 100644 --- a/tests/e2e/graphics/test-cases/two-scales/left-percent.js +++ b/tests/e2e/graphics/test-cases/two-scales/left-percent.js @@ -46,7 +46,7 @@ function generateDataHist() { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container, { + const chart = window.chart = LightweightCharts.createChart(container, { leftPriceScale: { visible: true, scaleMargins: { diff --git a/tests/e2e/graphics/test-cases/two-scales/price-scale-text-colors.js b/tests/e2e/graphics/test-cases/two-scales/price-scale-text-colors.js new file mode 100644 index 0000000000..332bdeb8a7 --- /dev/null +++ b/tests/e2e/graphics/test-cases/two-scales/price-scale-text-colors.js @@ -0,0 +1,31 @@ +function generateData(offset) { + const res = []; + const time = new Date(Date.UTC(2018, 0, 1, 0, 0, 0, 0)); + for (let i = 0; i < 500; ++i) { + res.push({ + time: time.getTime() / 1000, + value: Math.cos(i + offset), + }); + time.setUTCDate(time.getUTCDate() + 1); + } + return res; +} + +function runTestCase(container) { + const chart = window.chart = LightweightCharts.createChart(container, { + leftPriceScale: { + visible: true, + textColor: 'blue', + }, + rightPriceScale: { + visible: true, + textColor: 'red', + }, + }); + + const series1 = chart.addLineSeries({ color: 'red', priceScaleId: 'right' }); + const series2 = chart.addLineSeries({ color: 'blue', priceScaleId: 'left' }); + + series1.setData(generateData(0)); + series2.setData(generateData(Math.PI)); +} diff --git a/tests/e2e/graphics/test-cases/two-scales/two-groups-of-overlays.js b/tests/e2e/graphics/test-cases/two-scales/two-groups-of-overlays.js index b6e076366a..cfe7c13bab 100644 --- a/tests/e2e/graphics/test-cases/two-scales/two-groups-of-overlays.js +++ b/tests/e2e/graphics/test-cases/two-scales/two-groups-of-overlays.js @@ -46,7 +46,7 @@ function generateDataLine(offset) { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const mainSeries = chart.addBarSeries({ borderColor: 'rgba(0, 0, 255, 0.2)', diff --git a/tests/e2e/graphics/test-cases/two-scales/two-overlays-share-scale.js b/tests/e2e/graphics/test-cases/two-scales/two-overlays-share-scale.js index 541513f699..92a6d027b9 100644 --- a/tests/e2e/graphics/test-cases/two-scales/two-overlays-share-scale.js +++ b/tests/e2e/graphics/test-cases/two-scales/two-overlays-share-scale.js @@ -46,7 +46,7 @@ function generateDataLine(offset) { } function runTestCase(container) { - const chart = LightweightCharts.createChart(container); + const chart = window.chart = LightweightCharts.createChart(container); const mainSeries = chart.addBarSeries({ borderColor: 'rgba(0, 0, 255, 0.2)', diff --git a/tests/e2e/helpers/get-test-cases.ts b/tests/e2e/helpers/get-test-cases.ts new file mode 100644 index 0000000000..062fc781c5 --- /dev/null +++ b/tests/e2e/helpers/get-test-cases.ts @@ -0,0 +1,58 @@ +/// + +import * as fs from 'fs'; +import * as path from 'path'; + +export interface TestCase { + name: string; + caseContent: string; +} + +function extractTestCaseName(fileName: string): string | null { + const match = /^([^.].+)\.js$/.exec(path.basename(fileName)); + return match && match[1]; +} + +function isTestCaseFile(filePath: string): boolean { + return fs.lstatSync(filePath).isFile() && extractTestCaseName(filePath) !== null; +} + +interface TestCasesGroupInfo { + name: string; + path: string; +} + +function getTestCaseGroups(testCasesDir: string): TestCasesGroupInfo[] { + return [ + { + name: '', + path: testCasesDir, + }, + ...fs.readdirSync(testCasesDir) + .filter((filePath: string) => fs.lstatSync(path.join(testCasesDir, filePath)).isDirectory()) + .map((filePath: string) => { + return { + name: filePath, + path: path.join(testCasesDir, filePath), + }; + }), + ]; +} + +export function getTestCases(testCasesDir: string): Record { + const result: Record = {}; + + for (const group of getTestCaseGroups(testCasesDir)) { + result[group.name] = fs.readdirSync(group.path) + .map((filePath: string) => path.join(group.path, filePath)) + .filter(isTestCaseFile) + .map((testCaseFile: string) => { + return { + name: extractTestCaseName(testCaseFile) as string, + caseContent: fs.readFileSync(testCaseFile, { encoding: 'utf-8' }), + }; + }); + } + + return result; +} diff --git a/tests/e2e/helpers/mouse-drag-actions.ts b/tests/e2e/helpers/mouse-drag-actions.ts new file mode 100644 index 0000000000..e462effa03 --- /dev/null +++ b/tests/e2e/helpers/mouse-drag-actions.ts @@ -0,0 +1,67 @@ +import { BoundingBox, ElementHandle, Page } from 'puppeteer'; + +import { pageTimeout } from './page-timeout'; + +export async function doVerticalDrag( + page: Page, + element: ElementHandle +): Promise { + const elBox = (await element.boundingBox()) as BoundingBox; + + const elMiddleX = elBox.x + elBox.width / 2; + const elMiddleY = elBox.y + elBox.height / 2; + + // move mouse to the middle of element + await page.mouse.move(elMiddleX, elMiddleY); + + await page.mouse.down({ button: 'left' }); + await page.mouse.move(elMiddleX, elMiddleY - 20); + await page.mouse.move(elMiddleX, elMiddleY + 40); + await page.mouse.up({ button: 'left' }); +} + +export async function doHorizontalDrag( + page: Page, + element: ElementHandle +): Promise { + const elBox = (await element.boundingBox()) as BoundingBox; + + const elMiddleX = elBox.x + elBox.width / 2; + const elMiddleY = elBox.y + elBox.height / 2; + + // move mouse to the middle of element + await page.mouse.move(elMiddleX, elMiddleY); + + await page.mouse.down({ button: 'left' }); + await page.mouse.move(elMiddleX - 20, elMiddleY); + await page.mouse.move(elMiddleX + 40, elMiddleY); + await page.mouse.up({ button: 'left' }); +} + +export async function doKineticAnimation( + page: Page, + element: ElementHandle +): Promise { + const elBox = (await element.boundingBox()) as BoundingBox; + + const elMiddleX = elBox.x + elBox.width / 2; + const elMiddleY = elBox.y + elBox.height / 2; + + // move mouse to the middle of element + await page.mouse.move(elMiddleX, elMiddleY); + + await page.mouse.down({ button: 'left' }); + await pageTimeout(page, 50); + await page.mouse.move(elMiddleX - 40, elMiddleY); + await page.mouse.move(elMiddleX - 55, elMiddleY); + await page.mouse.move(elMiddleX - 105, elMiddleY); + await page.mouse.move(elMiddleX - 155, elMiddleY); + await page.mouse.move(elMiddleX - 205, elMiddleY); + await page.mouse.move(elMiddleX - 255, elMiddleY); + await page.mouse.up({ button: 'left' }); + + await pageTimeout(page, 200); + // stop animation + await page.mouse.down({ button: 'left' }); + await page.mouse.up({ button: 'left' }); +} diff --git a/tests/e2e/helpers/mouse-scroll-actions.ts b/tests/e2e/helpers/mouse-scroll-actions.ts new file mode 100644 index 0000000000..dda6c5a0f9 --- /dev/null +++ b/tests/e2e/helpers/mouse-scroll-actions.ts @@ -0,0 +1,48 @@ +import { ElementHandle, Page } from 'puppeteer'; + +export async function centerMouseOnElement( + page: Page, + element: ElementHandle +): Promise { + const boundingBox = await element.boundingBox(); + if (!boundingBox) { + throw new Error('Unable to get boundingBox for element.'); + } + + // move mouse to center of element + await page.mouse.move( + boundingBox.x + boundingBox.width / 2, + boundingBox.y + boundingBox.height / 2 + ); +} + +interface MouseScrollDelta { + x?: number; + y?: number; +} + +export async function doMouseScroll( + deltas: MouseScrollDelta, + page: Page +): Promise { + await page.mouse.wheel({ deltaX: deltas.x || 0, deltaY: deltas.y || 0 }); +} + +export async function doMouseScrolls( + page: Page, + element: ElementHandle +): Promise { + await centerMouseOnElement(page, element); + + await doMouseScroll({ x: 10.0 }, page); + + await doMouseScroll({ y: 10.0 }, page); + + await doMouseScroll({ x: -10.0 }, page); + + await doMouseScroll({ y: -10.0 }, page); + + await doMouseScroll({ x: 10.0, y: 10.0 }, page); + + await doMouseScroll({ x: -10.0, y: -10.0 }, page); +} diff --git a/tests/e2e/helpers/page-timeout.ts b/tests/e2e/helpers/page-timeout.ts new file mode 100644 index 0000000000..d57ef781bc --- /dev/null +++ b/tests/e2e/helpers/page-timeout.ts @@ -0,0 +1,11 @@ +import { Page } from 'puppeteer'; + +// await a setTimeout delay evaluated within page context +export async function pageTimeout(page: Page, delay: number): Promise { + return page.evaluate( + (ms: number) => new Promise( + (resolve: () => void) => setTimeout(resolve, ms) + ), + delay + ); +} diff --git a/tests/e2e/helpers/perform-interactions.ts b/tests/e2e/helpers/perform-interactions.ts new file mode 100644 index 0000000000..1f4783404b --- /dev/null +++ b/tests/e2e/helpers/perform-interactions.ts @@ -0,0 +1,218 @@ +import { ElementHandle, Page } from 'puppeteer'; + +import { + doHorizontalDrag, + doKineticAnimation, + doVerticalDrag, +} from './mouse-drag-actions'; +import { doMouseScroll } from './mouse-scroll-actions'; +import { doLongTouch, doPinchZoomTouch, doSwipeTouch } from './touch-actions'; +import { doZoomInZoomOut } from './zoom-action'; + +export type InteractionAction = + | 'scrollLeft' + | 'scrollRight' + | 'scrollUp' + | 'scrollDown' + | 'scrollUpRight' + | 'scrollDownLeft' + | 'click' + | 'doubleClick' + | 'outsideClick' + | 'viewportZoomInOut' + | 'verticalDrag' + | 'horizontalDrag' + | 'tap' + | 'longTouch' + | 'pinchZoomIn' + | 'pinchZoomOut' + | 'swipeTouchVertical' + | 'swipeTouchHorizontal' + | 'swipeTouchDiagonal' + | 'kineticAnimation' + | 'moveMouseCenter' + | 'moveMouseTopLeft'; +export type InteractionTarget = + | 'container' + | 'timescale' + | 'leftpricescale' + | 'rightpricescale' + | 'pane'; + +export interface Interaction { + action: InteractionAction; + target?: InteractionTarget; +} + +// eslint-disable-next-line complexity +async function performAction( + action: InteractionAction, + page: Page, + target: ElementHandle +): Promise { + switch (action) { + case 'scrollLeft': + await doMouseScroll({ x: -10.0 }, page); + break; + case 'scrollRight': + await doMouseScroll({ x: 10.0 }, page); + break; + case 'scrollDown': + await doMouseScroll({ y: 10.0 }, page); + break; + case 'scrollUp': + await doMouseScroll({ y: -10.0 }, page); + break; + case 'scrollUpRight': + await doMouseScroll({ y: 10.0, x: 10.0 }, page); + break; + case 'scrollDownLeft': + await doMouseScroll({ y: -10.0, x: -10.0 }, page); + break; + case 'click': + await target.click({ button: 'left' }); + break; + case 'doubleClick': + await target.click({ button: 'left', clickCount: 2 }); + break; + case 'outsideClick': + { + const boundingBox = await target.boundingBox(); + if (boundingBox) { + await page.mouse.click( + boundingBox.x + boundingBox.width + 20, + boundingBox.y + boundingBox.height + 50, + { button: 'left' } + ); + } + } + break; + case 'viewportZoomInOut': + await doZoomInZoomOut(page); + break; + case 'verticalDrag': + await doVerticalDrag(page, target); + break; + case 'horizontalDrag': + await doHorizontalDrag(page, target); + break; + + case 'tap': + { + const boundingBox = await target.boundingBox(); + if (boundingBox) { + await page.touchscreen.tap( + boundingBox.x + boundingBox.width / 2, + boundingBox.y + boundingBox.height / 2 + ); + } + } + break; + case 'longTouch': + await doLongTouch(page, target, 500); + break; + case 'pinchZoomIn': + { + const devToolsSession = await page.target().createCDPSession(); + await doPinchZoomTouch(devToolsSession, target, true); + } + break; + case 'pinchZoomOut': + { + const devToolsSession = await page.target().createCDPSession(); + await doPinchZoomTouch(devToolsSession, target); + } + break; + case 'swipeTouchHorizontal': + { + const devToolsSession = await page.target().createCDPSession(); + await doSwipeTouch(devToolsSession, target, { horizontal: true }); + } + break; + case 'swipeTouchVertical': + { + const devToolsSession = await page.target().createCDPSession(); + await doSwipeTouch(devToolsSession, target, { vertical: true }); + } + break; + case 'swipeTouchDiagonal': + { + const devToolsSession = await page.target().createCDPSession(); + await doSwipeTouch(devToolsSession, target, { + vertical: true, + horizontal: true, + }); + } + break; + case 'kineticAnimation': + await doKineticAnimation(page, target); + break; + case 'moveMouseCenter': + { + const boundingBox = await target.boundingBox(); + if (boundingBox) { + await page.mouse.move(boundingBox.width / 2, boundingBox.height / 2); + } + } + break; + case 'moveMouseTopLeft': + { + const boundingBox = await target.boundingBox(); + if (boundingBox) { + await page.mouse.move(0, 0); + } + } + break; + default: { + const exhaustiveCheck: never = action; + throw new Error(exhaustiveCheck); + } + } +} + +export async function performInteractions( + page: Page, + interactionsToPerform: Interaction[] +): Promise { + const chartContainer = (await page.$('#container')) as ElementHandle; + const leftPriceAxis = ( + await chartContainer.$$('tr:nth-of-type(1) td:nth-of-type(1) div canvas') + )[0]; + const paneWidget = ( + await chartContainer.$$('tr:nth-of-type(1) td:nth-of-type(2) div canvas') + )[0]; + const rightPriceAxis = ( + await chartContainer.$$('tr:nth-of-type(1) td:nth-of-type(3) div canvas') + )[0]; + const timeAxis = ( + await chartContainer.$$('tr:nth-of-type(2) td:nth-of-type(2) div canvas') + )[0]; + + for (const interaction of interactionsToPerform) { + let target: ElementHandle; + switch (interaction.target) { + case undefined: + case 'container': + target = chartContainer; + break; + case 'leftpricescale': + target = leftPriceAxis; + break; + case 'rightpricescale': + target = rightPriceAxis; + break; + case 'timescale': + target = timeAxis; + break; + case 'pane': + target = paneWidget; + break; + default: { + const exhaustiveCheck: never = interaction.target; + throw new Error(exhaustiveCheck); + } + } + + await performAction(interaction.action, page, target); + } +} diff --git a/tests/e2e/helpers/touch-actions.ts b/tests/e2e/helpers/touch-actions.ts new file mode 100644 index 0000000000..431d6d2b65 --- /dev/null +++ b/tests/e2e/helpers/touch-actions.ts @@ -0,0 +1,89 @@ +import { BoundingBox, ElementHandle, Page, type CDPSession } from 'puppeteer'; + +import { pageTimeout } from './page-timeout'; + +// Simulate a long touch action in a single position +export async function doLongTouch(page: Page, element: ElementHandle, duration: number): Promise { + const elBox = (await element.boundingBox()) as BoundingBox; + + const elCenterX = elBox.x + elBox.width / 2; + const elCenterY = elBox.y + elBox.height / 2; + + const client = await page.target().createCDPSession(); + + await client.send('Input.dispatchTouchEvent', { + type: 'touchStart', + touchPoints: [ + { x: elCenterX, y: elCenterY }, + ], + }); + await pageTimeout(page, duration); + return client.send('Input.dispatchTouchEvent', { + type: 'touchEnd', + touchPoints: [ + { x: elCenterX, y: elCenterY }, + ], + }); +} + +// Simulate a touch swipe gesture +export async function doSwipeTouch( + devToolsSession: CDPSession, + element: ElementHandle, + { + horizontal = false, + vertical = false, + }: { horizontal?: boolean; vertical?: boolean } +): Promise { + const elBox = (await element.boundingBox()) as BoundingBox; + + const elCenterX = elBox.x + elBox.width / 2; + const elCenterY = elBox.y + elBox.height / 2; + const xStep = horizontal ? elBox.width / 8 : 0; + const yStep = vertical ? elBox.height / 8 : 0; + + for (let i = 2; i > 0; i--) { + const type = i === 2 ? 'touchStart' : 'touchMove'; + await devToolsSession.send('Input.dispatchTouchEvent', { + type, + touchPoints: [{ x: elCenterX - i * xStep, y: elCenterY - i * yStep }], + }); + } + return devToolsSession.send('Input.dispatchTouchEvent', { + type: 'touchEnd', + touchPoints: [{ x: elCenterX - xStep, y: elCenterY - yStep }], + }); +} + +// Perform a pinch or zoom touch gesture within the specified element. +export async function doPinchZoomTouch( + devToolsSession: CDPSession, + element: ElementHandle, + zoom?: boolean +): Promise { + const elBox = (await element.boundingBox()) as BoundingBox; + + const sign = zoom ? -1 : 1; + const elCenterX = elBox.x + elBox.width / 2; + const elCenterY = elBox.y + elBox.height / 2; + const xStep = (sign * elBox.width) / 8; + const yStep = (sign * elBox.height) / 8; + + for (let i = 2; i > 0; i--) { + const type = i === 2 ? 'touchStart' : 'touchMove'; + await devToolsSession.send('Input.dispatchTouchEvent', { + type, + touchPoints: [ + { x: elCenterX - i * xStep, y: elCenterY - i * yStep }, + { x: elCenterX + i * xStep, y: elCenterY + i * xStep }, + ], + }); + } + return devToolsSession.send('Input.dispatchTouchEvent', { + type: 'touchEnd', + touchPoints: [ + { x: elCenterX - xStep, y: elCenterY - yStep }, + { x: elCenterX + xStep, y: elCenterY + xStep }, + ], + }); +} diff --git a/tests/e2e/helpers/zoom-action.ts b/tests/e2e/helpers/zoom-action.ts new file mode 100644 index 0000000000..b4ab74b566 --- /dev/null +++ b/tests/e2e/helpers/zoom-action.ts @@ -0,0 +1,12 @@ +import { Page } from 'puppeteer'; + +export async function doZoomInZoomOut(page: Page): Promise { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const prevViewport = page.viewport()!; + await page.setViewport({ + ...prevViewport, + deviceScaleFactor: 2, + }); + + await page.setViewport(prevViewport); +} diff --git a/tests/e2e/interactions/helpers/get-interaction-test-cases.ts b/tests/e2e/interactions/helpers/get-interaction-test-cases.ts new file mode 100644 index 0000000000..ec28ed117f --- /dev/null +++ b/tests/e2e/interactions/helpers/get-interaction-test-cases.ts @@ -0,0 +1,11 @@ +/// + +import * as path from 'path'; + +import { getTestCases as getTestCasesImpl, TestCase } from '../../helpers/get-test-cases'; + +const testCasesDir = path.join(__dirname, '..', 'test-cases'); + +export function getTestCases(): Record { + return getTestCasesImpl(testCasesDir); +} diff --git a/tests/e2e/interactions/helpers/test-page-dummy.html b/tests/e2e/interactions/helpers/test-page-dummy.html new file mode 100644 index 0000000000..f7b010d1e8 --- /dev/null +++ b/tests/e2e/interactions/helpers/test-page-dummy.html @@ -0,0 +1,23 @@ + + + + + + Test case page + + + +
+ + + + + + + + diff --git a/tests/e2e/interactions/interactions-test-cases.ts b/tests/e2e/interactions/interactions-test-cases.ts new file mode 100644 index 0000000000..696af87f4f --- /dev/null +++ b/tests/e2e/interactions/interactions-test-cases.ts @@ -0,0 +1,151 @@ +/// + +import * as fs from 'fs'; +import * as path from 'path'; + +import { expect } from 'chai'; +import { describe, it } from 'mocha'; +import puppeteer, { + Browser, + HTTPResponse, + launch as launchPuppeteer, +} from 'puppeteer'; + +import { TestCase } from '../helpers/get-test-cases'; +import { Interaction, performInteractions } from '../helpers/perform-interactions'; + +import { getTestCases } from './helpers/get-interaction-test-cases'; + +const dummyContent = fs.readFileSync( + path.join(__dirname, 'helpers', 'test-page-dummy.html'), + { encoding: 'utf-8' } +); + +function generatePageContent( + standaloneBundlePath: string, + testCaseCode: string +): string { + return dummyContent + .replace('PATH_TO_STANDALONE_MODULE', standaloneBundlePath) + .replace('TEST_CASE_SCRIPT', testCaseCode); +} + +const testStandalonePathEnvKey = 'TEST_STANDALONE_PATH'; + +const testStandalonePath: string = process.env[testStandalonePathEnvKey] || ''; + +interface InternalWindow { + interactions: Interaction[]; + finishedSetup: Promise<() => void>; + afterInteractions: () => void; +} + +describe('Interactions tests', function(): void { + // this tests are unstable sometimes. + this.retries(5); + + const puppeteerOptions: Parameters[0] = {}; + if (process.env.NO_SANDBOX) { + puppeteerOptions.args = ['--no-sandbox', '--disable-setuid-sandbox']; + } + + let browser: Browser; + + before(async () => { + expect( + testStandalonePath, + `path to test standalone module must be passed via ${testStandalonePathEnvKey} env var` + ).to.have.length.greaterThan(0); + + // note that we cannot use launchPuppeteer here as soon it wrong typing in puppeteer + // see https://github.com/puppeteer/puppeteer/issues/7529 + const browserPromise = puppeteer.launch(puppeteerOptions); + browser = await browserPromise; + }); + + let testCaseCount = 0; + + const runTestCase = (testCase: TestCase) => { + testCaseCount += 1; + it(testCase.name, async () => { + const pageContent = generatePageContent( + testStandalonePath, + testCase.caseContent + ); + + const page = await browser.newPage(); + await page.setViewport({ width: 600, height: 600 }); + + const errors: string[] = []; + page.on('pageerror', (error: Error) => { + errors.push(error.message); + }); + + page.on('response', (response: HTTPResponse) => { + if (!response.ok()) { + errors.push( + `Network error: ${response.url()} status=${response.status()}` + ); + } + }); + + await page.setContent(pageContent, { waitUntil: 'load' }); + + await page.evaluate(() => { + return (window as unknown as InternalWindow).finishedSetup; + }); + + const interactionsToPerform = await page.evaluate(() => { + return (window as unknown as InternalWindow).interactions; + }); + + await performInteractions(page, interactionsToPerform); + + await page.evaluate(() => { + return new Promise((resolve: () => void) => { + (window as unknown as InternalWindow).afterInteractions(); + window.requestAnimationFrame(() => { + setTimeout(resolve, 50); + }); + }); + }); + + if (errors.length !== 0) { + throw new Error(`Page has errors:\n${errors.join('\n')}`); + } + + expect(errors.length).to.be.equal( + 0, + 'There should not be any errors thrown within the test page.' + ); + }); + }; + + const testCaseGroups = getTestCases(); + + for (const groupName of Object.keys(testCaseGroups)) { + if (groupName.length === 0) { + for (const testCase of testCaseGroups[groupName]) { + runTestCase(testCase); + } + } else { + describe(groupName, () => { + for (const testCase of testCaseGroups[groupName]) { + runTestCase(testCase); + } + }); + } + } + + it('number of test cases', () => { + // we need to have at least 1 test to check it + expect(testCaseCount).to.be.greaterThan( + 0, + 'there should be at least 1 test case' + ); + }); + + after(async () => { + await browser.close(); + }); +}); diff --git a/tests/e2e/interactions/runner.js b/tests/e2e/interactions/runner.js new file mode 100644 index 0000000000..a2c5289e97 --- /dev/null +++ b/tests/e2e/interactions/runner.js @@ -0,0 +1,71 @@ +#!/usr/bin/env node + +const fs = require('fs'); +const path = require('path'); + +const Mocha = require('mocha'); + +const serveLocalFiles = require('../serve-local-files').serveLocalFiles; + +const mochaConfig = require('../../../.mocharc.js'); + +// override tsconfig +process.env.TS_NODE_PROJECT = path.resolve(__dirname, '../tsconfig.composite.json'); + +mochaConfig.require.forEach(module => { + require(module); +}); + +if (process.argv.length !== 3) { + console.log('Usage: runner PATH_TO_TEST_STANDALONE_MODULE'); + process.exit(1); +} + +const startTime = Date.now(); + +let testStandalonePath = process.argv[2]; + +const hostname = 'localhost'; +const port = 34567; +const httpServerPrefix = `http://${hostname}:${port}/`; + +const filesToServe = new Map(); + +if (fs.existsSync(testStandalonePath)) { + const fileNameToServe = 'test.js'; + filesToServe.set(fileNameToServe, path.resolve(testStandalonePath)); + testStandalonePath = `${httpServerPrefix}${fileNameToServe}`; +} + +process.env.TEST_STANDALONE_PATH = testStandalonePath; + +function runMocha(closeServer) { + console.log('Running tests...'); + const mocha = new Mocha({ + timeout: 20000, + slow: 10000, + reporter: mochaConfig.reporter, + reporterOptions: mochaConfig._reporterOptions, + }); + + if (mochaConfig.checkLeaks) { + mocha.checkLeaks(); + } + + mocha.diff(mochaConfig.diff); + mocha.addFile(path.resolve(__dirname, './interactions-test-cases.ts')); + + mocha.run(failures => { + if (closeServer !== null) { + closeServer(); + } + + const timeInSecs = (Date.now() - startTime) / 1000; + console.log(`Done in ${timeInSecs.toFixed(2)}s with ${failures} error(s)`); + + process.exitCode = failures !== 0 ? 1 : 0; + }); +} + +serveLocalFiles(filesToServe, port, hostname) + .then(runMocha); diff --git a/tests/e2e/interactions/test-cases/.eslintrc.js b/tests/e2e/interactions/test-cases/.eslintrc.js new file mode 100644 index 0000000000..2459a86d2d --- /dev/null +++ b/tests/e2e/interactions/test-cases/.eslintrc.js @@ -0,0 +1,14 @@ +/* eslint-env node */ + +module.exports = { + env: { + browser: true, + node: false, + }, + rules: { + 'no-unused-vars': ['error', { varsIgnorePattern: '^(beforeInteractions|afterInteractions|interactionsToPerform)$', args: 'none' }], + }, + globals: { + LightweightCharts: false, + }, +}; diff --git a/tests/e2e/interactions/test-cases/mouse-wheel/default-scroll-handler.js b/tests/e2e/interactions/test-cases/mouse-wheel/default-scroll-handler.js new file mode 100644 index 0000000000..89e6acfa32 --- /dev/null +++ b/tests/e2e/interactions/test-cases/mouse-wheel/default-scroll-handler.js @@ -0,0 +1,47 @@ +function generateData() { + const res = []; + const time = new Date(Date.UTC(2018, 0, 1, 0, 0, 0, 0)); + for (let i = 0; i < 500; ++i) { + res.push({ + time: time.getTime() / 1000, + value: i, + }); + + time.setUTCDate(time.getUTCDate() + 1); + } + return res; +} + +function interactionsToPerform() { + return [{ action: 'scrollLeft' }, { action: 'scrollDown' }]; +} + +let chart; +let startRange; + +function beforeInteractions(container) { + chart = LightweightCharts.createChart(container); + + const mainSeries = chart.addLineSeries(); + + mainSeries.setData(generateData()); + + return new Promise(resolve => { + requestAnimationFrame(() => { + startRange = chart.timeScale().getVisibleLogicalRange(); + resolve(); + }); + }); +} + +function afterInteractions() { + const endRange = chart.timeScale().getVisibleLogicalRange(); + + const pass = Boolean(startRange.from !== endRange.from && startRange.to !== endRange.to); + + if (!pass) { + throw new Error('Expected visible logical range to have changed.'); + } + + return Promise.resolve(); +} diff --git a/tests/e2e/interactions/test-cases/mouse-wheel/disabled-scroll-handler.js b/tests/e2e/interactions/test-cases/mouse-wheel/disabled-scroll-handler.js new file mode 100644 index 0000000000..9db6b65735 --- /dev/null +++ b/tests/e2e/interactions/test-cases/mouse-wheel/disabled-scroll-handler.js @@ -0,0 +1,54 @@ +function generateData() { + const res = []; + const time = new Date(Date.UTC(2018, 0, 1, 0, 0, 0, 0)); + for (let i = 0; i < 500; ++i) { + res.push({ + time: time.getTime() / 1000, + value: i, + }); + + time.setUTCDate(time.getUTCDate() + 1); + } + return res; +} + +function interactionsToPerform() { + return [{ action: 'scrollLeft' }, { action: 'scrollDown' }]; +} + +let chart; +let startRange; + +function beforeInteractions(container) { + chart = LightweightCharts.createChart(container, { + handleScroll: { + mouseWheel: false, + }, + handleScale: { + mouseWheel: false, + }, + }); + + const mainSeries = chart.addLineSeries(); + + mainSeries.setData(generateData()); + + return new Promise(resolve => { + requestAnimationFrame(() => { + startRange = chart.timeScale().getVisibleLogicalRange(); + resolve(); + }); + }); +} + +function afterInteractions() { + const endRange = chart.timeScale().getVisibleLogicalRange(); + + const pass = Boolean(startRange.from === endRange.from && startRange.to === endRange.to); + + if (!pass) { + throw new Error('Expected visible logical range to be unchanged.'); + } + + return Promise.resolve(); +} diff --git a/tests/e2e/interactions/test-cases/mouse-wheel/enabled-scroll-handler.js b/tests/e2e/interactions/test-cases/mouse-wheel/enabled-scroll-handler.js new file mode 100644 index 0000000000..5dc1d24e55 --- /dev/null +++ b/tests/e2e/interactions/test-cases/mouse-wheel/enabled-scroll-handler.js @@ -0,0 +1,54 @@ +function generateData() { + const res = []; + const time = new Date(Date.UTC(2018, 0, 1, 0, 0, 0, 0)); + for (let i = 0; i < 500; ++i) { + res.push({ + time: time.getTime() / 1000, + value: i, + }); + + time.setUTCDate(time.getUTCDate() + 1); + } + return res; +} + +function interactionsToPerform() { + return [{ action: 'scrollLeft' }, { action: 'scrollDown' }]; +} + +let chart; +let startRange; + +function beforeInteractions(container) { + chart = LightweightCharts.createChart(container, { + handleScroll: { + mouseWheel: true, + }, + handleScale: { + mouseWheel: true, + }, + }); + + const mainSeries = chart.addLineSeries(); + + mainSeries.setData(generateData()); + + return new Promise(resolve => { + requestAnimationFrame(() => { + startRange = chart.timeScale().getVisibleLogicalRange(); + resolve(); + }); + }); +} + +function afterInteractions() { + const endRange = chart.timeScale().getVisibleLogicalRange(); + + const pass = Boolean(startRange.from !== endRange.from && startRange.to !== endRange.to); + + if (!pass) { + throw new Error('Expected visible logical range to have changed.'); + } + + return Promise.resolve(); +} diff --git a/tests/e2e/interactions/test-cases/mouse-wheel/reenabled-scroll-handler.js b/tests/e2e/interactions/test-cases/mouse-wheel/reenabled-scroll-handler.js new file mode 100644 index 0000000000..cf2fcbec27 --- /dev/null +++ b/tests/e2e/interactions/test-cases/mouse-wheel/reenabled-scroll-handler.js @@ -0,0 +1,62 @@ +function generateData() { + const res = []; + const time = new Date(Date.UTC(2018, 0, 1, 0, 0, 0, 0)); + for (let i = 0; i < 500; ++i) { + res.push({ + time: time.getTime() / 1000, + value: i, + }); + + time.setUTCDate(time.getUTCDate() + 1); + } + return res; +} + +function interactionsToPerform() { + return [{ action: 'scrollLeft' }, { action: 'scrollDown' }]; +} + +let chart; +let startRange; + +function beforeInteractions(container) { + chart = LightweightCharts.createChart(container, { + handleScroll: { + mouseWheel: false, + }, + handleScale: { + mouseWheel: false, + }, + }); + + const mainSeries = chart.addLineSeries(); + + mainSeries.setData(generateData()); + + return new Promise(resolve => { + requestAnimationFrame(() => { + chart.applyOptions({ + handleScroll: { + mouseWheel: true, + }, + handleScale: { + mouseWheel: true, + }, + }); + startRange = chart.timeScale().getVisibleLogicalRange(); + resolve(); + }); + }); +} + +function afterInteractions() { + const endRange = chart.timeScale().getVisibleLogicalRange(); + + const pass = Boolean(startRange.from !== endRange.from && startRange.to !== endRange.to); + + if (!pass) { + throw new Error('Expected visible logical range to have changed.'); + } + + return Promise.resolve(); +} diff --git a/tests/e2e/memleaks/memleaks-test-cases.ts b/tests/e2e/memleaks/memleaks-test-cases.ts index 4a084f199b..06b9d6c152 100644 --- a/tests/e2e/memleaks/memleaks-test-cases.ts +++ b/tests/e2e/memleaks/memleaks-test-cases.ts @@ -5,7 +5,7 @@ import * as path from 'path'; import { expect } from 'chai'; import { describe, it } from 'mocha'; -import puppeteer, { Browser, Frame, HTTPResponse, JSHandle, launch as launchPuppeteer } from 'puppeteer'; +import puppeteer, { Browser, HTTPResponse, JSHandle, launch as launchPuppeteer, Page } from 'puppeteer'; import { getTestCases } from './helpers/get-test-cases'; @@ -15,17 +15,16 @@ function generatePageContent(standaloneBundlePath: string, testCaseCode: string) return dummyContent .replace('PATH_TO_STANDALONE_MODULE', standaloneBundlePath) .replace('TEST_CASE_SCRIPT', testCaseCode) - ; + ; } const testStandalonePathEnvKey = 'TEST_STANDALONE_PATH'; const testStandalonePath: string = process.env[testStandalonePathEnvKey] || ''; -async function getReferencesCount(frame: Frame, prototypeReference: JSHandle): Promise { - const context = await frame.executionContext(); - const activeRefsHandle = await context.queryObjects(prototypeReference); - const activeRefsCount = await (await activeRefsHandle?.getProperty('length'))?.jsonValue(); +async function getReferencesCount(page: Page, prototypeReference: JSHandle): Promise { + const activeRefsHandle = await page.queryObjects(prototypeReference); + const activeRefsCount = await (await activeRefsHandle?.getProperty('length'))?.jsonValue(); await activeRefsHandle.dispose(); @@ -38,7 +37,47 @@ function promisleep(ms: number): Promise { }); } -describe('Memleaks tests', () => { +/** + * Request garbage collection on the page. + * **Note:** This is only a request and the page will still decide + * when best to perform this action. + */ +async function requestGarbageCollection(page: Page): Promise { + const client = await page.target().createCDPSession(); + await client.send('HeapProfiler.enable'); + return client.send('HeapProfiler.collectGarbage'); +} + +// Poll the references count on the page until the condition +// is satisfied for a specific prototype. +async function pollReferencesCount( + page: Page, + prototype: JSHandle, + condition: (currentCount: number) => boolean, + timeout: number, + actionName?: string +): Promise { + const start = performance.now(); + let referencesCount = 0; + let done = false; + do { + const duration = performance.now() - start; + if (duration > timeout) { + throw new Error(`${actionName ? `${actionName}: ` : ''}Timeout exceeded waiting for references count to meet desired condition.`); + } + referencesCount = await getReferencesCount(page, prototype); + done = condition(referencesCount); + if (!done) { + await promisleep(50); + } + } while (!done); + return referencesCount; +} + +describe('Memleaks tests', function(): void { + // this tests are unstable sometimes. + this.retries(5); + const puppeteerOptions: Parameters[0] = {}; if (process.env.NO_SANDBOX) { puppeteerOptions.args = ['--no-sandbox', '--disable-setuid-sandbox']; @@ -90,12 +129,9 @@ describe('Memleaks tests', () => { return Promise.resolve(CanvasRenderingContext2D.prototype); }; - const frame = page.mainFrame(); - const context = await frame.executionContext(); + const prototype = await page.evaluateHandle(getCanvasPrototype); - const prototype = await context.evaluateHandle(getCanvasPrototype); - - const referencesCountBefore = await getReferencesCount(frame, prototype); + const referencesCountBefore = await getReferencesCount(page, prototype); await page.setContent(pageContent, { waitUntil: 'load' }); @@ -103,6 +139,15 @@ describe('Memleaks tests', () => { throw new Error(`Page has errors:\n${errors.join('\n')}`); } + // Wait until at least one canvas element has been created. + await pollReferencesCount( + page, + prototype, + (count: number) => count > referencesCountBefore, + 2500, + 'Creation' + ); + // now remove chart await page.evaluate(() => { @@ -113,12 +158,18 @@ describe('Memleaks tests', () => { delete (window as any).chart; }); - // IMPORTANT: This timeout is important + await requestGarbageCollection(page); + + // Wait until all the created canvas elements have been garbage collected. // Browser could keep references to DOM elements several milliseconds after its actual removing // So we have to wait to be sure all is clear - await promisleep(100); - - const referencesCountAfter = await getReferencesCount(frame, prototype); + const referencesCountAfter = await pollReferencesCount( + page, + prototype, + (count: number) => count <= referencesCountBefore, + 5000, + 'Garbage Collection' + ); expect(referencesCountAfter).to.be.equal(referencesCountBefore, 'There should not be extra references after removing a chart'); }); diff --git a/tests/e2e/tsconfig.composite.json b/tests/e2e/tsconfig.composite.json index b4c16e15bf..6a9fb5865c 100644 --- a/tests/e2e/tsconfig.composite.json +++ b/tests/e2e/tsconfig.composite.json @@ -13,6 +13,9 @@ "./coverage/**/*.ts", "./graphics/graphics-test-cases.ts", "./graphics/helpers/**/*.ts", - "./memleaks/**/*.ts" + "./memleaks/**/*.ts", + "./interactions/**/*.ts", + "./helpers/**/*.ts", + "../../src/**/*.ts" ] } diff --git a/tests/unittests/get-series-data-creator.spec.ts b/tests/unittests/get-series-data-creator.spec.ts index 1e5e4113e0..db0c975b2d 100644 --- a/tests/unittests/get-series-data-creator.spec.ts +++ b/tests/unittests/get-series-data-creator.spec.ts @@ -3,7 +3,7 @@ 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 { AreaPlotRow, BarPlotRow, BaselinePlotRow, CandlestickPlotRow, HistogramPlotRow, LinePlotRow } from '../../src/model/series-data'; import { OriginalTime, TimePointIndex, UTCTimestamp } from '../../src/model/time-data'; const plotRow: PlotRow = { @@ -20,8 +20,27 @@ const linePlotRows: LinePlotRow[] = [ }, plotRow, ]; -const areaPlotRows: PlotRow = plotRow; -const baselinePlotRows: PlotRow = plotRow; +const areaPlotRows: AreaPlotRow[] = [ + { + ...plotRow, + lineColor: '#FF0000', + topColor: '#00FF00', + bottomColor: '#0000FF', + }, + plotRow, +]; +const baselinePlotRows: BaselinePlotRow[] = [ + { + ...plotRow, + topFillColor1: '#000001', + topFillColor2: '#000002', + topLineColor: '#000003', + bottomFillColor1: '#000004', + bottomFillColor2: '#000005', + bottomLineColor: '#000006', + }, + plotRow, +]; const histogramPlotRow: HistogramPlotRow[] = [ { ...plotRow, @@ -72,14 +91,32 @@ describe('getSeriesDataCreator', () => { }); it('Area', () => { - expect(getSeriesDataCreator('Area')(areaPlotRows)).to.deep.equal({ + expect(getSeriesDataCreator('Area')(areaPlotRows[0])).to.deep.equal({ + value: 4, + time: 1649931070, + lineColor: '#FF0000', + topColor: '#00FF00', + bottomColor: '#0000FF', + }); + expect(getSeriesDataCreator('Area')(areaPlotRows[1])).to.deep.equal({ value: 4, time: 1649931070, }); }); it('Baseline', () => { - expect(getSeriesDataCreator('Baseline')(baselinePlotRows)).to.deep.equal({ + expect(getSeriesDataCreator('Baseline')(baselinePlotRows[0])).to.deep.equal({ + value: 4, + time: 1649931070, + topFillColor1: '#000001', + topFillColor2: '#000002', + topLineColor: '#000003', + bottomFillColor1: '#000004', + bottomFillColor2: '#000005', + bottomLineColor: '#000006', + }); + + expect(getSeriesDataCreator('Baseline')(baselinePlotRows[1])).to.deep.equal({ value: 4, time: 1649931070, }); diff --git a/tests/unittests/get-series-plot-row-creator.spec.ts b/tests/unittests/get-series-plot-row-creator.spec.ts new file mode 100644 index 0000000000..18d55fd935 --- /dev/null +++ b/tests/unittests/get-series-plot-row-creator.spec.ts @@ -0,0 +1,145 @@ +import { expect } from 'chai'; +import { describe, it } from 'mocha'; + +import { getSeriesPlotRowCreator } from '../../src/api/get-series-plot-row-creator'; +import { OriginalTime, TimePointIndex, UTCTimestamp } from '../../src/model/time-data'; + +describe('getSeriesPlotRowCreator', () => { + it('Line', () => { + expect(getSeriesPlotRowCreator('Line')( + { timestamp: 1649931070 as UTCTimestamp }, + 0 as TimePointIndex, + { + value: 4, + time: 1649931070 as UTCTimestamp, + color: '#FF0000', + }, + 1649931070 as unknown as OriginalTime + )).to.deep.equal({ + index: 0 as TimePointIndex, + time: { timestamp: 1649931070 as UTCTimestamp }, + value: [4, 4, 4, 4], + originalTime: 1649931070 as unknown as OriginalTime, + color: '#FF0000', + }); + }); + + it('Area', () => { + expect(getSeriesPlotRowCreator('Area')( + { timestamp: 1649931070 as UTCTimestamp }, + 0 as TimePointIndex, + { + value: 4, + time: 1649931070 as UTCTimestamp, + lineColor: '#FF0000', + topColor: '#00FF00', + bottomColor: '#0000FF', + }, + 1649931070 as unknown as OriginalTime + )).to.deep.equal({ + index: 0 as TimePointIndex, + time: { timestamp: 1649931070 as UTCTimestamp }, + value: [4, 4, 4, 4], + originalTime: 1649931070 as unknown as OriginalTime, + lineColor: '#FF0000', + topColor: '#00FF00', + bottomColor: '#0000FF', + }); + }); + + it('Baseline', () => { + expect(getSeriesPlotRowCreator('Baseline')( + { timestamp: 1649931070 as UTCTimestamp }, + 0 as TimePointIndex, + { + value: 4, + time: 1649931070 as UTCTimestamp, + topFillColor1: '#000001', + topFillColor2: '#000002', + topLineColor: '#000003', + bottomFillColor1: '#000004', + bottomFillColor2: '#000005', + bottomLineColor: '#000006', + }, + 1649931070 as unknown as OriginalTime + )).to.deep.equal({ + index: 0 as TimePointIndex, + time: { timestamp: 1649931070 as UTCTimestamp }, + value: [4, 4, 4, 4], + originalTime: 1649931070 as unknown as OriginalTime, + topFillColor1: '#000001', + topFillColor2: '#000002', + topLineColor: '#000003', + bottomFillColor1: '#000004', + bottomFillColor2: '#000005', + bottomLineColor: '#000006', + }); + }); + + it('Histogram', () => { + expect(getSeriesPlotRowCreator('Histogram')( + { timestamp: 1649931070 as UTCTimestamp }, + 0 as TimePointIndex, + { + value: 4, + time: 1649931070 as UTCTimestamp, + color: '#FF0000', + }, + 1649931070 as unknown as OriginalTime + )).to.deep.equal({ + index: 0 as TimePointIndex, + time: { timestamp: 1649931070 as UTCTimestamp }, + value: [4, 4, 4, 4], + originalTime: 1649931070 as unknown as OriginalTime, + color: '#FF0000', + }); + }); + + it('Bar', () => { + expect(getSeriesPlotRowCreator('Bar')( + { timestamp: 1649931070 as UTCTimestamp }, + 0 as TimePointIndex, + { + open: 1, + high: 3, + low: 0, + close: 2, + time: 1649931070 as UTCTimestamp, + color: '#FF0000', + }, + 1649931070 as unknown as OriginalTime + )).to.deep.equal({ + index: 0 as TimePointIndex, + time: { timestamp: 1649931070 as UTCTimestamp }, + value: [1, 3, 0, 2], + originalTime: 1649931070 as unknown as OriginalTime, + color: '#FF0000', + }); + }); + + it('Candlestick', () => { + expect(getSeriesPlotRowCreator('Candlestick')( + { timestamp: 1649931070 as UTCTimestamp }, + 0 as TimePointIndex, + { + open: 1, + high: 3, + low: 0, + close: 2, + time: 1649931070 as UTCTimestamp, + color: '#FF0000', + borderColor: '#00FF00', + wickColor: '#0000FF', + }, + 1649931070 as unknown as OriginalTime + )).to.deep.equal({ + index: 0 as TimePointIndex, + time: { timestamp: 1649931070 as UTCTimestamp }, + value: [1, 3, 0, 2], + originalTime: 1649931070 as unknown as OriginalTime, + color: '#FF0000', + borderColor: '#00FF00', + wickColor: '#0000FF', + }); + }); +}); diff --git a/tests/unittests/text-width-cache.spec.ts b/tests/unittests/text-width-cache.spec.ts index 402d25fcb8..814c714fac 100644 --- a/tests/unittests/text-width-cache.spec.ts +++ b/tests/unittests/text-width-cache.spec.ts @@ -6,11 +6,17 @@ import { CanvasCtxLike, TextWidthCache } from '../../src/model/text-width-cache' class FakeCtx implements CanvasCtxLike { public readonly invocations: string[] = []; + public textBaseline: CanvasTextBaseline = 'alphabetic'; + public measureText(text: string): TextMetrics { this.invocations.push(text); return { width: this._impl(text) } as unknown as TextMetrics; } + public save(): void {} + + public restore(): void {} + protected _impl(text: string): number { let fakeWidth = 0; for (let i = 0; i < text.length; i++) { diff --git a/tsconfig.composite.json b/tsconfig.composite.json index e68f1431fe..5f3b32416d 100644 --- a/tsconfig.composite.json +++ b/tsconfig.composite.json @@ -1,7 +1,8 @@ { "references": [ { "path": "./src/tsconfig.composite.json" }, - { "path": "./tests/tsconfig.composite.json" } + { "path": "./tests/tsconfig.composite.json" }, + { "path": "./website/tsconfig.json" }, ], "include": [] } diff --git a/website/.eslintrc.js b/website/.eslintrc.js new file mode 100644 index 0000000000..f05e4a42ea --- /dev/null +++ b/website/.eslintrc.js @@ -0,0 +1,19 @@ +module.exports = { + globals: { + CHART_BACKGROUND_COLOR: true, + CHART_BACKGROUND_RGB_COLOR: true, + LINE_LINE_COLOR: true, + AREA_TOP_COLOR: true, + AREA_BOTTOM_COLOR: true, + BAR_UP_COLOR: true, + BAR_DOWN_COLOR: true, + BASELINE_TOP_LINE_COLOR: true, + BASELINE_TOP_FILL_COLOR1: true, + BASELINE_TOP_FILL_COLOR2: true, + BASELINE_BOTTOM_LINE_COLOR: true, + BASELINE_BOTTOM_FILL_COLOR1: true, + BASELINE_BOTTOM_FILL_COLOR2: true, + HISTOGRAM_COLOR: true, + CHART_TEXT_COLOR: true, + }, +}; diff --git a/website/README.md b/website/README.md index effee65bc1..24d5bec8d2 100644 --- a/website/README.md +++ b/website/README.md @@ -28,6 +28,16 @@ _Note_: API documentation will not be generated unless you have already built th This command generates static content in the `build` directory. +## Serve Build Locally + +```console +npm run serve +``` + +_Note_: Embedded `.html` examples won't display correctly when using this command but will work correctly when hosted online. + +This command serves the built website locally. + ## Deployment ```console diff --git a/website/docs/intro.md b/website/docs/intro.md index 0e084059ba..ae195cf0f6 100644 --- a/website/docs/intro.md +++ b/website/docs/intro.md @@ -44,8 +44,8 @@ import { createChart } from 'lightweight-charts'; // ... // somewhere in your code -const firstChart = createChart(firstContainer); -const secondChart = createChart(secondContainer); +const firstChart = createChart(document.getElementById('firstContainer')); +const secondChart = createChart(document.getElementById('secondContainer')); ``` The result of this function is a [`IChartApi`](/api/interfaces/IChartApi.md) object, which you need to use to work with a chart instance. @@ -92,12 +92,13 @@ Note that regardless of the series type, the API calls are the same (the type of To set the data (or to replace all data items) to a series you need to use [`ISeriesApi.setData`](/api/interfaces/ISeriesApi.md#setdata) method: -```js -import { createChart } from 'lightweight-charts'; - -const chart = createChart(container); - -const areaSeries = chart.addAreaSeries(); +```js chart replaceThemeConstants +const chartOptions = { layout: { textColor: CHART_TEXT_COLOR, background: { type: 'solid', color: CHART_BACKGROUND_COLOR } } }; +const chart = createChart(document.getElementById('container'), chartOptions); +const areaSeries = chart.addAreaSeries({ + lineColor: LINE_LINE_COLOR, topColor: AREA_TOP_COLOR, + bottomColor: AREA_BOTTOM_COLOR, +}); areaSeries.setData([ { time: '2018-12-22', value: 32.51 }, { time: '2018-12-23', value: 31.11 }, @@ -111,7 +112,10 @@ areaSeries.setData([ { time: '2018-12-31', value: 22.67 }, ]); -const candlestickSeries = chart.addCandlestickSeries(); +const candlestickSeries = chart.addCandlestickSeries({ + upColor: BAR_UP_COLOR, downColor: BAR_DOWN_COLOR, borderVisible: false, + wickUpColor: BAR_UP_COLOR, wickDownColor: BAR_DOWN_COLOR, +}); candlestickSeries.setData([ { time: '2018-12-22', open: 75.16, high: 82.84, low: 36.16, close: 45.72 }, { time: '2018-12-23', open: 45.12, high: 53.90, low: 45.12, close: 48.09 }, @@ -124,11 +128,9 @@ candlestickSeries.setData([ { time: '2018-12-30', open: 106.33, high: 110.20, low: 90.39, close: 98.10 }, { time: '2018-12-31', open: 109.87, high: 114.69, low: 85.66, close: 111.26 }, ]); -``` -It's pretty easy, isn't it? That's it, your chart is ready to be displayed on the page: - -![First simple chart](/img/first-chart.png "First simple chart") +chart.timeScale().fitContent(); +``` ### Updating the data in a series diff --git a/website/docs/migrations/from-v3-to-v4.md b/website/docs/migrations/from-v3-to-v4.md index 723090f487..f86f01dee2 100644 --- a/website/docs/migrations/from-v3-to-v4.md +++ b/website/docs/migrations/from-v3-to-v4.md @@ -95,6 +95,8 @@ const chart = createChart({ }); ``` +Also this option is off by default. + ## The type of outbound time values has been changed Affected API: diff --git a/website/docs/series-types.md b/website/docs/series-types.md index 512df9b4a4..7b5bee1173 100644 --- a/website/docs/series-types.md +++ b/website/docs/series-types.md @@ -42,7 +42,17 @@ If you'd like to change any option of a series, you could do this in different w An area chart is basically a colored area between the line connecting all data points and [the time scale](./time-scale.md): -![Area chart example](/img/area-series.png "Area chart example") +```js chart replaceThemeConstants +const chartOptions = { layout: { textColor: CHART_TEXT_COLOR, background: { type: 'solid', color: CHART_BACKGROUND_COLOR } } }; +const chart = createChart(document.getElementById('container'), chartOptions); +const areaSeries = chart.addAreaSeries({ lineColor: LINE_LINE_COLOR, topColor: AREA_TOP_COLOR, bottomColor: AREA_BOTTOM_COLOR }); + +const data = [{ value: 0, time: 1642425322 }, { value: 8, time: 1642511722 }, { value: 10, time: 1642598122 }, { value: 20, time: 1642684522 }, { value: 3, time: 1642770922 }, { value: 43, time: 1642857322 }, { value: 41, time: 1642943722 }, { value: 43, time: 1643030122 }, { value: 56, time: 1643116522 }, { value: 46, time: 1643202922 }]; + +areaSeries.setData(data); + +chart.timeScale().fitContent(); +``` ## Bar @@ -55,7 +65,17 @@ A bar chart shows price movements in the form of bars. Vertical line length of a bar is limited by the highest and lowest price values. Open & Close values are represented by tick marks, on the left & right hand side of the bar respectively: -![Bar chart example](/img/bar-series.png "Bar chart example") +```js chart replaceThemeConstants +const chartOptions = { layout: { textColor: CHART_TEXT_COLOR, background: { type: 'solid', color: CHART_BACKGROUND_COLOR } } }; +const chart = createChart(document.getElementById('container'), chartOptions); +const barSeries = chart.addBarSeries({ upColor: BAR_UP_COLOR, downColor: BAR_DOWN_COLOR }); + +const data = [{ open: 10, high: 10.63, low: 9.49, close: 9.55, time: 1642427876 }, { open: 9.55, high: 10.30, low: 9.42, close: 9.94, time: 1642514276 }, { open: 9.94, high: 10.17, low: 9.92, close: 9.78, time: 1642600676 }, { open: 9.78, high: 10.59, low: 9.18, close: 9.51, time: 1642687076 }, { open: 9.51, high: 10.46, low: 9.10, close: 10.17, time: 1642773476 }, { open: 10.17, high: 10.96, low: 10.16, close: 10.47, time: 1642859876 }, { open: 10.47, high: 11.39, low: 10.40, close: 10.81, time: 1642946276 }, { open: 10.81, high: 11.60, low: 10.30, close: 10.75, time: 1643032676 }, { open: 10.75, high: 11.60, low: 10.49, close: 10.93, time: 1643119076 }, { open: 10.93, high: 11.53, low: 10.76, close: 10.96, time: 1643205476 }]; + +barSeries.setData(data); + +chart.timeScale().fitContent(); +``` ## Baseline @@ -65,7 +85,17 @@ Open & Close values are represented by tick marks, on the left & right hand side 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): -![Baseline chart example](/img/baseline-series.png) +```js chart replaceThemeConstants +const chartOptions = { layout: { textColor: CHART_TEXT_COLOR, background: { type: 'solid', color: CHART_BACKGROUND_COLOR } } }; +const chart = createChart(document.getElementById('container'), chartOptions); +const baselineSeries = chart.addBaselineSeries({ baseValue: { type: 'price', price: 25 }, topLineColor: BASELINE_TOP_LINE_COLOR, topFillColor1: BASELINE_TOP_FILL_COLOR1, topFillColor2: BASELINE_TOP_FILL_COLOR2, bottomLineColor: BASELINE_BOTTOM_LINE_COLOR, bottomFillColor1: BASELINE_BOTTOM_FILL_COLOR1, bottomFillColor2: BASELINE_BOTTOM_FILL_COLOR2 }); + +const data = [{ value: 1, time: 1642425322 }, { value: 8, time: 1642511722 }, { value: 10, time: 1642598122 }, { value: 20, time: 1642684522 }, { value: 3, time: 1642770922 }, { value: 43, time: 1642857322 }, { value: 41, time: 1642943722 }, { value: 43, time: 1643030122 }, { value: 56, time: 1643116522 }, { value: 46, time: 1643202922 }]; + +baselineSeries.setData(data); + +chart.timeScale().fitContent(); +``` ## Candlestick @@ -76,7 +106,17 @@ A baseline is basically two colored areas (top and bottom) between the line conn A candlestick chart shows price movements in the form of candlesticks. On the candlestick chart, open & close values form a solid body of a candle while wicks show high & low values for a candlestick's time interval: -![Candlestick chart example](/img/candlestick-series.png "Candlestick chart example") +```js chart replaceThemeConstants +const chartOptions = { layout: { textColor: CHART_TEXT_COLOR, background: { type: 'solid', color: CHART_BACKGROUND_COLOR } } }; +const chart = createChart(document.getElementById('container'), chartOptions); +const candlestickSeries = chart.addCandlestickSeries({ upColor: BAR_UP_COLOR, downColor: BAR_DOWN_COLOR, borderVisible: false, wickUpColor: BAR_UP_COLOR, wickDownColor: BAR_DOWN_COLOR }); + +const data = [{ open: 10, high: 10.63, low: 9.49, close: 9.55, time: 1642427876 }, { open: 9.55, high: 10.30, low: 9.42, close: 9.94, time: 1642514276 }, { open: 9.94, high: 10.17, low: 9.92, close: 9.78, time: 1642600676 }, { open: 9.78, high: 10.59, low: 9.18, close: 9.51, time: 1642687076 }, { open: 9.51, high: 10.46, low: 9.10, close: 10.17, time: 1642773476 }, { open: 10.17, high: 10.96, low: 10.16, close: 10.47, time: 1642859876 }, { open: 10.47, high: 11.39, low: 10.40, close: 10.81, time: 1642946276 }, { open: 10.81, high: 11.60, low: 10.30, close: 10.75, time: 1643032676 }, { open: 10.75, high: 11.60, low: 10.49, close: 10.93, time: 1643119076 }, { open: 10.93, high: 11.53, low: 10.76, close: 10.96, time: 1643205476 }]; + +candlestickSeries.setData(data); + +chart.timeScale().fitContent(); +``` ## Histogram @@ -87,7 +127,17 @@ On the candlestick chart, open & close values form a solid body of a candle whil A histogram series is a graphical representation of the value distribution. Histogram creates intervals (columns) and counts how many values fall into each column: -![Histogram example](/img/histogram-series.png "Histogram chart example") +```js chart replaceThemeConstants +const chartOptions = { layout: { textColor: CHART_TEXT_COLOR, background: { type: 'solid', color: CHART_BACKGROUND_COLOR } } }; +const chart = createChart(document.getElementById('container'), chartOptions); +const histogramSeries = chart.addHistogramSeries({ color: HISTOGRAM_COLOR }); + +const data = [{ value: 1, time: 1642425322 }, { value: 8, time: 1642511722 }, { value: 10, time: 1642598122 }, { value: 20, time: 1642684522 }, { value: 3, time: 1642770922, color: 'red' }, { value: 43, time: 1642857322 }, { value: 41, time: 1642943722, color: 'red' }, { value: 43, time: 1643030122 }, { value: 56, time: 1643116522 }, { value: 46, time: 1643202922, color: 'red' }]; + +histogramSeries.setData(data); + +chart.timeScale().fitContent(); +``` ## Line @@ -97,116 +147,14 @@ Histogram creates intervals (columns) and counts how many values fall into each A line chart is a type of chart that displays information as series of the data points connected by straight line segments: -![Line chart example](/img/line-series.png "Line chart example") - - +const data = [{ value: 0, time: 1642425322 }, { value: 8, time: 1642511722 }, { value: 10, time: 1642598122 }, { value: 20, time: 1642684522 }, { value: 3, time: 1642770922 }, { value: 43, time: 1642857322 }, { value: 41, time: 1642943722 }, { value: 43, time: 1643030122 }, { value: 56, time: 1643116522 }, { value: 46, time: 1643202922 }]; + +lineSeries.setData(data); + +chart.timeScale().fitContent(); +``` diff --git a/website/docusaurus.config.js b/website/docusaurus.config.js index 707afbe112..dd0fabcb5a 100644 --- a/website/docusaurus.config.js +++ b/website/docusaurus.config.js @@ -9,7 +9,7 @@ const https = require('https'); const lightCodeTheme = require('prism-react-renderer/themes/github'); const darkCodeTheme = require('prism-react-renderer/themes/dracula'); const { default: pluginDocusaurus } = require('docusaurus-plugin-typedoc'); -const { default: logger } = require('@docusaurus/logger'); +const logger = require('@docusaurus/logger'); const versions = require('./versions.json'); const sizeLimits = require('../.size-limit'); @@ -55,7 +55,7 @@ function downloadFile(urlString, filePath) { const url = new URL(urlString); const request = https.get(url, response => { - if (response.statusCode >= 300 && response.statusCode < 400 && response.headers.location !== undefined) { + if (response.statusCode && response.statusCode >= 300 && response.statusCode < 400 && response.headers.location !== undefined) { // handling redirect url.pathname = response.headers.location; downloadFile(url.toString(), filePath).then(resolve, reject); @@ -112,7 +112,7 @@ async function downloadTypingsFromUnpkg(version) { return typingsFilePath; } -/** @type {Partial & import('typedoc/dist/index').TypeDocOptions} */ +/** @type {Partial & import('typedoc').TypeDocOptions} */ const commonDocusaurusPluginTypedocConfig = { readme: 'none', disableSources: true, @@ -299,11 +299,38 @@ async function getConfig() { theme: lightCodeTheme, darkTheme: darkCodeTheme, additionalLanguages: ['ruby', 'swift', 'kotlin', 'groovy'], + magicComments: [ + { + className: 'theme-code-block-highlighted-line', + line: 'highlight-next-line', + block: { start: 'highlight-start', end: 'highlight-end' }, + }, + { + // Lightly fades the code lines (useful for boilerplate sections of code) + className: 'code-block-fade-line', + block: { start: 'highlight-fade-start', end: 'highlight-fade-end' }, + line: 'highlight-fade', + }, + { + // Hides code lines but can be reveal using toggle and css + className: 'code-block-hide-line', + block: { start: 'hide-start', end: 'hide-end' }, + line: 'hide-line', + }, + { + // Hides code lines and can't be reveal using toggle and css. + // Will still be included in copied code. + // Useful for type comments and header notices. + className: 'code-block-remove-line', + block: { start: 'remove-start', end: 'remove-end' }, + line: 'remove-line', + }, + ], }, algolia: { appId: '7Q5A441YPA', // Public API key: it is safe to commit it - apiKey: 'b6417716804e66012544fd5904e208c8', + apiKey: 'c8a8aaeb7ef3fbcce40bada2196e2bcb', indexName: 'lightweight-charts', contextualSearch: true, }, @@ -322,8 +349,7 @@ async function getConfig() { ], [ 'docusaurus-plugin-typedoc', - // @ts-ignore - /** @type {Partial & import('typedoc/dist/index').TypeDocOptions} */ + /** @type {Partial & import('typedoc').TypeDocOptions} */ ({ ...commonDocusaurusPluginTypedocConfig, id: 'current-api', @@ -333,6 +359,7 @@ async function getConfig() { }), ], ...versions.map(typedocPluginForVersion), + './plugins/enhanced-codeblock', ], }; diff --git a/website/package.json b/website/package.json index 1716caaec1..ed76c5363e 100644 --- a/website/package.json +++ b/website/package.json @@ -3,7 +3,7 @@ "scripts": { "docusaurus": "docusaurus", "start": "cross-env TYPEDOC_WATCH=true docusaurus start", - "build": "docusaurus build", + "build": "node scripts/generate-versions-dts.js && docusaurus build", "swizzle": "docusaurus swizzle", "deploy": "docusaurus deploy", "clear": "docusaurus clear", @@ -12,20 +12,24 @@ "write-heading-ids": "docusaurus write-heading-ids" }, "dependencies": { - "@docusaurus/core": "2.0.0-beta.17", - "@docusaurus/module-type-aliases": "2.0.0-beta.17", - "@docusaurus/preset-classic": "2.0.0-beta.17", - "@docusaurus/theme-search-algolia": "2.0.0-beta.17", - "@tsconfig/docusaurus": "~1.0.4", + "@docusaurus/core": "2.1.0", + "@docusaurus/module-type-aliases": "2.1.0", + "@docusaurus/preset-classic": "2.1.0", + "@docusaurus/theme-search-algolia": "2.1.0", + "@tsconfig/docusaurus": "~1.0.6", + "@types/react": "~17.0.39", "cross-env": "~7.0.3", - "docusaurus-plugin-typedoc": "0.17.2", + "docusaurus-plugin-typedoc": "0.17.5", "lightweight-charts": "~3.8.0", - "prism-react-renderer": "~1.3.1", + "lightweight-charts-local": "file:..", + "lightweight-charts-3.8": "npm:lightweight-charts@~3.8.0", + "prism-react-renderer": "~1.3.5", "raw-loader": "~4.0.2", - "react": "~17.0.1", - "react-dom": "~17.0.1", - "typedoc": "~0.22.13", - "typedoc-plugin-markdown": "3.11.14", - "typescript": "4.6.2" + "react": "~17.0.2", + "react-dom": "~17.0.2", + "typedoc": "~0.23.14", + "typedoc-plugin-markdown": "3.13.6", + "typescript": "4.7.3", + "vue": "^3.2.39" } } diff --git a/website/plugins/enhanced-codeblock/index.js b/website/plugins/enhanced-codeblock/index.js new file mode 100644 index 0000000000..344fe5d645 --- /dev/null +++ b/website/plugins/enhanced-codeblock/index.js @@ -0,0 +1,9 @@ +const path = require('path'); + +module.exports = function chartCodeBlockPlugin(context, options) { + return { + name: 'enhanced-codeblock', + + getThemePath: () => path.resolve(__dirname, './theme'), + }; +}; diff --git a/website/plugins/enhanced-codeblock/theme/CodeBlock/chart.tsx b/website/plugins/enhanced-codeblock/theme/CodeBlock/chart.tsx new file mode 100644 index 0000000000..d7f76536d3 --- /dev/null +++ b/website/plugins/enhanced-codeblock/theme/CodeBlock/chart.tsx @@ -0,0 +1,94 @@ +import { type PropVersionMetadata } from '@docusaurus/plugin-content-docs'; +import { useDocsPreferredVersion } from '@docusaurus/theme-common'; +import * as React from 'react'; + +import versions from '../../../../versions.json'; +import { importLightweightChartsVersion, LightweightChartsApiTypeMap } from './import-lightweight-charts-version'; +import styles from './styles.module.css'; + +interface ChartProps { + script: string; +} + +type IFrameWindow = Window & { + createChart: LightweightChartsApiTypeMap[TVersion]['createChart']; + run?: () => void; +}; + +function getSrcDocWithScript(script: string): string { + return ` + +
+ + `; +} + +export function Chart(props: ChartProps): JSX.Element { + const { script } = props; + const { preferredVersion } = useDocsPreferredVersion() as { preferredVersion: (PropVersionMetadata & { name: string }) | null }; + const currentVersion = versions && versions.length > 0 ? versions[0] : ''; + const version = (preferredVersion?.name ?? currentVersion ?? 'current') as TVersion; + const srcDoc = getSrcDocWithScript(script); + const ref = React.useRef(null); + + React.useEffect( + () => { + const iframeElement = ref.current; + const iframeWindow = iframeElement?.contentWindow as IFrameWindow; + const iframeDocument = iframeElement?.contentDocument; + + if (iframeElement === null || !iframeWindow || !iframeDocument) { + return; + } + + const injectCreateChartAndRun = async () => { + try { + const { module, createChart } = await importLightweightChartsVersion[version](iframeWindow); + + Object.assign(iframeWindow, module); // Make ColorType, etc. available in the iframe + iframeWindow.createChart = createChart; + iframeWindow.run?.(); + } catch (err: unknown) { + // eslint-disable-next-line no-console + console.error(err); + } + }; + + if (iframeWindow.run !== undefined) { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + injectCreateChartAndRun(); + } else { + const iframeLoadListener = () => { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + injectCreateChartAndRun(); + iframeElement.removeEventListener('load', iframeLoadListener); + }; + + iframeElement.addEventListener('load', iframeLoadListener); + } + }, + [srcDoc] + ); + + return ( + + + View in a new window + + +## Next steps + +In the next step, we will change the colors for the candlestick series. + +## Download + +You can download the HTML file for example at this stage here in case you've encountered a problem or would like to start the next step from this point. + +## Complete code + +import CodeBlock from '@theme/CodeBlock'; +import code from '!!raw-loader!./assets/step2.html'; +import InstantDetails from '@site/src/components/InstantDetails' + + + +Click here to reveal the complete code for the example at this stage of the guide. + +{code} + diff --git a/website/tutorials/customization/conclusion.mdx b/website/tutorials/customization/conclusion.mdx new file mode 100644 index 0000000000..a9283e2362 --- /dev/null +++ b/website/tutorials/customization/conclusion.mdx @@ -0,0 +1,24 @@ +--- +sidebar_position: 11 +title: Conclusion +pagination_title: Conclusion +sidebar_label: Conclusion +description: Conclusion to the customization tutorial +keywords: + - customization + - appearance + - styling +pagination_prev: customization/finishing-touches +pagination_next: null +--- + +Thank you for taking the time to complete this tutorial, and we hope that you've learnt how to customise your charts to meet your needs and more importantly where to find the available options in the documentation and how to apply them to your chart. + +We look forward to seeing what you create. + +We welcome any and all feedback on this tutorial and library in general. You can reach us via the following channels: + +- [Discord](https://discord.gg/UC7cGkvn4U) +- [GitHub Discussions](https://github.com/tradingview/lightweight-charts/discussions) + +and if you've spotted any errors or bugs then please post an issue on our [GitHub page](https://github.com/tradingview/lightweight-charts/issues). diff --git a/website/tutorials/customization/creating-a-chart.mdx b/website/tutorials/customization/creating-a-chart.mdx new file mode 100644 index 0000000000..6f7a4c60fa --- /dev/null +++ b/website/tutorials/customization/creating-a-chart.mdx @@ -0,0 +1,148 @@ +--- +sidebar_position: 1 +title: First steps +pagination_title: First steps +sidebar_label: First steps +description: In this section, we will be creating a simple chart. +keywords: + - customization + - appearance + - styling +pagination_prev: customization/intro +pagination_next: customization/chart-colors +--- + +:::tip + +If you haven't already, please read the [Introduction](intro). At the bottom of that page, you will find a starting file containing everything you need to get up and running with this tutorial (including some sample data to display on the chart). + +::: + +Our first task will be to create a chart and get it visible on the page. A more detailed explanation of these steps is available [here](/docs). + +## Adding the Lightweight Charts script + +For this example, we will be loading the Lightweight Charts library using the standalone version hosted on a CDN server. This approach allows us to just include the script tag within our HTML file and not be concerned about spinning up a web server to host our files, and thus open the HTML file directly on our computer. + +We can add the script tag to the HTML page by including the following code within the `` element of the page. In this case, we will insert the code between the `` and `<style>` elements. + +```html +<title>Lightweight Charts Customization Tutorial + + + + + +``` + +and then add the following `
` element within the container element used for the chart. + +```html + +``` + +## Result + +🎉 Congrats! At this point you should have the final chart which looks like this: + + + + View in a new window + + +## Download + +You can download the HTML file for example at this stage here in case you've encountered a problem or would like to start the next step from this point. + +## Complete code + +import CodeBlock from '@theme/CodeBlock'; +import code from '!!raw-loader!./assets/step10.html'; +import InstantDetails from '@site/src/components/InstantDetails' + + + +Click here to reveal the complete code for the example at this stage of the guide. + +{code} + diff --git a/website/tutorials/customization/intro.mdx b/website/tutorials/customization/intro.mdx new file mode 100644 index 0000000000..91acaa92c1 --- /dev/null +++ b/website/tutorials/customization/intro.mdx @@ -0,0 +1,91 @@ +--- +sidebar_position: 0 +sidebar_label: Introduction +pagination_title: Introduction +title: Customizing the Chart +description: This tutorial provides an introduction to customizing Lightweight Chart's appearance and functionality. +keywords: + - customization + - appearance + - styling +pagination_prev: null +pagination_next: customization/creating-a-chart +--- + +# Customization - Introduction + +This tutorial provides an introduction to customizing Lightweight Chart's appearance and functionality. It is not meant as an exhaustive tutorial but rather as a guided tour on how and where to apply options within the API to adjust specific parts of the chart. Along the way, we will provide links to the API documentation which outline the full set of options available for each part of the chart. It is highly recommended that you explore these API links to discover the wide range of possible customization and feature flags contained within Lightweight Charts. + +## What we will be building + +Before we get started, let us have a look at what we will be building in this tutorial. + + + + + View in a new window + + +## Topics to be covered + +The following topics will be covered within the tutorial: + +- Styling the main chart +- Styling a series +- Setting a custom price formatter +- Adjusting the Price Scale +- Adjusting the Time Scale +- Customising the Crosshair +- Adding a second series +- Customising the appearance of a few data points +- Adding a simple attribution message +- Setting a different font + +## Prerequisite knowledge + +The tutorial requires basic knowledge of: + +- Javascript +- HTML +- CSS + +:::tip + +The tutorial will assume that you've already read the [Getting Started](/docs) section even though we may repeat a few aspects from that guide. + +::: + +## Terminology + +- **Data Series (aka data/dataset):** A collection of data points representing a specific metric over time. +- **Series Type:** A series type specifies how to draw the data on the chart. For example, a line series type will plot the data series on the chart as a series of the data points connected by straight line segments. Available series types: [Series types | Lightweight Charts](/docs/series-types) +- **Series:** A combination of a specified series type and a data series. +- **Price Scale:** Price Scale (or price axis) is a vertical scale that mostly maps prices to coordinates and vice versa. +- **Time Scale:** Time scale (or time axis) is a horizontal scale at the bottom of the chart that displays the time of bars. +- **Crosshair:** Thin vertical and horizontal lines centered on a data point in the chart. + +## How to set up the example so you can follow along + +This guide makes use of a single HTML file which you can download to your computer and run in the browser without the need for any build steps or web servers. The only thing required is an active internet connection such that the Lightweight Charts library can be downloaded from the CDN. + +Provided below is the 'starting point' file for the guide which is a simple HTML page scaffolded out with a single div element (`#container`) and a JS function to generate the sample data set. **At this point, you won't see anything on the page until we add the chart in the next step.** + +You can either: + +- + Download the file + and then edit and run the example on your computer, +- or [open this JSFiddle](https://jsfiddle.net/TradingView/5h76xeqk/) and then edit and run the example within the browser. + + +:::tip + +At the end of each section will be a link to download the example at that stage of the guide, and a full code block. + +::: diff --git a/website/tutorials/customization/price-format.mdx b/website/tutorials/customization/price-format.mdx new file mode 100644 index 0000000000..1167809c6e --- /dev/null +++ b/website/tutorials/customization/price-format.mdx @@ -0,0 +1,95 @@ +--- +sidebar_position: 4 +title: Price format +pagination_title: Price format +sidebar_label: Price format +description: In this section, we will be replacing the default price formatter function with our implementation. +keywords: + - customization + - appearance + - styling +pagination_prev: customization/series +pagination_next: customization/price-scale +--- + +import IterativeGuideWarning from './_iterative-guide-warning-partial.mdx'; + + + +In this section, we will be replacing the default price formatter function with our implementation. Currently, the prices on the chart are been shown with two decimal places and without a currency symbol. Let's implement a formatter which will format the number correctly based on their current locale and present it as the Euro currency. + +## Price Formatter functions + +To provide a price formatter, we need to create a function which accepts a `number` as the input and returns a `string` as the output. A simple price formatter which takes a number and returns the number (as a string) formatted with two decimal points would look as follows: + +```js +const myPriceFormatter = p => p.toFixed(2); +``` + +We can make use of the built-in functionality provided by the browser to build a more versatile price formatter by using the [Intl.NumberFormat API](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat). + +```js +// Get the current users primary locale +const currentLocale = window.navigator.languages[0]; +// Create a number format using Intl.NumberFormat +const myPriceFormatter = Intl.NumberFormat(currentLocale, { + style: 'currency', + currency: 'EUR', // Currency for data points +}).format; +``` + +First, we are grabbing the primary locale for the current user which will pass into the `Intl.NumberFormat` constructor. The constructor takes a second argument for options where we can specify the `style` and `currency` properties. The instance created contains a `format` method which we can then pass into Lightweight Charts 9as shown below). + +## Setting the price formatter + +We can set the default price formatter to be used throughout the chart by passing our own custom price formatter function into the `priceformatter` property of the `localization` property ([LocalizationOptions](/docs/api/interfaces/LocalizationOptions)) within the chart options. + +```js +// Apply the custom priceFormatter to the chart +chart.applyOptions({ + localization: { + priceFormatter: myPriceFormatter, + }, +}); +``` + +Price values displayed on the vertical price scale will now be displayed as Euros. + +Price formatters can also be applied to each series individually by adjusting the [priceformat](/docs/api/interfaces/SeriesOptionsCommon#priceformat) property of the series options. + +## Built-in price formatting + +Lightweight Charts includes a few options to adjust the built-in price formatting which can be see here: [PriceFormatBuiltIn](/docs/api/interfaces/PriceFormatBuiltIn). + +## Result + +At this point we should have a chart like this: + + + + View in a new window + + +## Next steps + +In the next step, we will be making some adjustments to the functionality of the vertical price scale. + +## Download + +You can download the HTML file for example at this stage here in case you've encountered a problem or would like to start the next step from this point. + +## Complete code + +import CodeBlock from '@theme/CodeBlock'; +import code from '!!raw-loader!./assets/step4.html'; +import InstantDetails from '@site/src/components/InstantDetails' + + + +Click here to reveal the complete code for the example at this stage of the guide. + +{code} + diff --git a/website/tutorials/customization/price-scale.mdx b/website/tutorials/customization/price-scale.mdx new file mode 100644 index 0000000000..9dac45ad00 --- /dev/null +++ b/website/tutorials/customization/price-scale.mdx @@ -0,0 +1,86 @@ +--- +sidebar_position: 5 +title: Price scale +pagination_title: Price scale +sidebar_label: Price scale +description: In this section, we are going to adjust the functionality of the vertical price scale. +keywords: + - customization + - appearance + - styling +pagination_prev: customization/price-format +pagination_next: customization/time-scale +--- + +import IterativeGuideWarning from './_iterative-guide-warning-partial.mdx'; + + + +In this section, we are going to adjust the functionality of the vertical price scale. Currently, the price scale will auto-scale to fit the visible data on the chart. You can observe this behaviour by scrolling the chart to the right such that some of the data points are no longer visible. We will disable this feature such that the price scale can only be manually adjusted by the user (by clicking and dragging on the scale). + +:::tip + +A chart can have more than one price scale and their options can be individually adjusted, however in this example we only have one price scale. + +::: + +## Adjusting settings for the price scale + +We can get the current [IPriceScaleApi](/docs/api/interfaces/IPriceScaleApi) instance for the chart by evoking the `priceScale()` method on the candlestick series reference. + +Once again, we can use the `applyOptions()` method on this API instance to adjust it's options. You can add the following code to the example at any point after the `mainSeries` reference has been created, so let us place it at the end of the script. The options available are shown here: [PriceScaleOptions](/docs/api/interfaces/PriceScaleOptions). + +```js +// Adjust the options for the priceScale of the mainSeries +mainSeries.priceScale().applyOptions({ + autoScale: false, // disables auto scaling based on visible content + scaleMargins: { + top: 0.1, + bottom: 0.2, + }, +}); +``` + +As discussed above we are disabling the `autoScale` feature on this price scale. We are additionally adjusting the `scaleMargins` which are used when the chart is first rendered to determine the 'zoom' and position of the scale. The scale margins set the proportion of the chart to be empty above and below the data points currently visible. + +## Result + +At this point, we should have a chart like the one presented below. Play around with scrolling the data left and right to see the change of behaviour versus the previous step. + +### Before + + + +### After + + + + View in a new window + + +## Next steps + +In the next step, we will be adjusting the settings on the horizontal time scale. + +## Download + +You can download the HTML file for example at this stage here in case you've encountered a problem or would like to start the next step from this point. + +## Complete code + +import CodeBlock from '@theme/CodeBlock'; +import code from '!!raw-loader!./assets/step5.html'; +import InstantDetails from '@site/src/components/InstantDetails' + + + +Click here to reveal the complete code for the example at this stage of the guide. + +{code} + diff --git a/website/tutorials/customization/second-series.mdx b/website/tutorials/customization/second-series.mdx new file mode 100644 index 0000000000..82bf9ca6e8 --- /dev/null +++ b/website/tutorials/customization/second-series.mdx @@ -0,0 +1,111 @@ +--- +sidebar_position: 8 +title: Adding a second series +pagination_title: Adding a second series +sidebar_label: Adding a second series +description: In this section, we will be adding an area series to the chart with a subtle vertical gradient. +keywords: + - customization + - appearance + - styling +pagination_prev: customization/crosshair +pagination_next: customization/data-points +--- + +import IterativeGuideWarning from './_iterative-guide-warning-partial.mdx'; + + + +In this section, we will be adding an [area series](/docs/series-types#area) to the chart with a subtle vertical gradient. It's purpose is solely for aesthetic reasons (only to make the chart more visually appealing). However, it will teach us a few key points about the differences between different series types and the visual stacking order. + +## Preparing the data for the area series + +The data structure required for the area series isn't the same as for the candlestick series. The area series is expecting each data point to have the following properties: `time` and `value`. Whereas the candlestick data points don't have a `value` property but rather the following properties: `open`, `close`, `high`, `low`, and `time`. + +We can create a copy of the candlestick data and transform it in a single step by using a `map` higher-order function ([more info](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map)). We will set the `value` for the area series as the midpoint between the `open` and `close` values of the candlestick data points. + +```js +// Generate sample data to use within a candlestick series +const candleStickData = generateCandlestickData(); + +// highlight-start +// Convert the candlestick data for use with a line series +const lineData = candleStickData.map(datapoint => ({ + time: datapoint.time, + value: (datapoint.close + datapoint.open) / 2, +})); +// highlight-end +``` + +## Adding the area series and setting it's options + +We can add the area series as we did with the candlestick series by calling the `addAreaSeries()` method on the chart instance. We will pass the options for the series as the first argument to `addAreaSeries()` method instead of separately calling `applyOptions()` at a later stage. + +:::caution + +Make sure to add this code **before** the `addCandlestickSeries()` call already in the code because we want it to appear below the candlesticks (as explained in the next section). + +::: + +```js +// Convert the candlestick data for use with a line series +const lineData = candleStickData.map(datapoint => ({ + time: datapoint.time, + value: (datapoint.close + datapoint.open) / 2, +})); + +// highlight-start +// Add an area series to the chart, +// Adding this before we add the candlestick chart +// so that it will appear beneath the candlesticks +const areaSeries = chart.addAreaSeries({ + lastValueVisible: false, // hide the last value marker for this series + crosshairMarkerVisible: false, // hide the crosshair marker for this series + lineColor: 'transparent', // hide the line + topColor: 'rgba(56, 33, 110,0.6)', + bottomColor: 'rgba(56, 33, 110, 0.1)', +}); +// Set the data for the Area Series +areaSeries.setData(lineData); +// highlight-end + +// Create the Main Series (Candlesticks) +const mainSeries = chart.addCandlestickSeries(); +``` + +## Visual stacking order of series + +When adding multiple series to a single chart, it is important to take note of the order in which they are added because that will determine the visual stacking order (when they overlap). The first series added will appear at the bottom of the stack and each series added will be placed on top of the stack. Thus in the current example, we want the area series to appear below the candlestick series so we will make sure to first add the area series and then the candlestick series. + +## Result + +At this point we should have a chart like this: + + + + View in a new window + + +## Next steps + +In the next step, we will look at how to adjust the colour of individual candlesticks on our chart. + +## Download + +You can download the HTML file for example at this stage here in case you've encountered a problem or would like to start the next step from this point. + +## Complete code + +import CodeBlock from '@theme/CodeBlock'; +import code from '!!raw-loader!./assets/step8.html'; +import InstantDetails from '@site/src/components/InstantDetails' + + + +Click here to reveal the complete code for the example at this stage of the guide. + +{code} + diff --git a/website/tutorials/customization/series.mdx b/website/tutorials/customization/series.mdx new file mode 100644 index 0000000000..e49e79739b --- /dev/null +++ b/website/tutorials/customization/series.mdx @@ -0,0 +1,77 @@ +--- +sidebar_position: 3 +title: Series colors +pagination_title: Series colors +sidebar_label: Series colors +description: In this section, we will be customizing the visual styling of the candlestick series. +keywords: + - customization + - appearance + - styling +pagination_prev: customization/chart-colors +pagination_next: customization/price-format +--- + +import IterativeGuideWarning from './_iterative-guide-warning-partial.mdx'; + + + +In this section, we will be customizing the visual styling of the candlestick series. + +We can add our custom options to the series by using the `applyOptions` method on the ISeriesApi instance for the candlestick series. In other words, we can call the `applyOptions` method on the `mainSeries` variable (which was returned when we evoked `addCandlestickSeries()` earlier). + +The available options for the candlestick series is a combination of the following interfaces: [CandlestickStyleOptions](/docs/api/interfaces/CandlestickStyleOptions) and [SeriesOptionsCommon](/docs/api/interfaces/SeriesOptionsCommon). + +## Setting custom colors for the candlestick series + +We are going to set the colors such that upward candles will be a light blue and downward candles will be a vibrant red. The color for the body of the candle is determined by the `upColor` and `downColor` properties, whilst the wick colors are determined by `wickUpColor` and `wickDownColor`. We will additionally disable the border on the candlestick for this example. + +We can apply these options at any point in the code after we have created the candlestick series, and in this case, we will place the code below the `setData()` call (but it would still work if was placed before). + +```js +mainSeries.setData(candleStickData); + +// highlight-start +// Changing the Candlestick colors +mainSeries.applyOptions({ + wickUpColor: 'rgb(54, 116, 217)', + upColor: 'rgb(54, 116, 217)', + wickDownColor: 'rgb(225, 50, 85)', + downColor: 'rgb(225, 50, 85)', + borderVisible: false, +}); +// highlight-end +``` + +## Result + +At this point we should have a chart like this: + + + + View in a new window + + +## Next steps + +In the next step, we will set a price formatter so we can customize the formatting of numbers on the chart. + +## Download + +You can download the HTML file for example at this stage here in case you've encountered a problem or would like to start the next step from this point. + +## Complete code + +import CodeBlock from '@theme/CodeBlock'; +import code from '!!raw-loader!./assets/step3.html'; +import InstantDetails from '@site/src/components/InstantDetails' + + + +Click here to reveal the complete code for the example at this stage of the guide. + +{code} + diff --git a/website/tutorials/customization/time-scale.mdx b/website/tutorials/customization/time-scale.mdx new file mode 100644 index 0000000000..86a8a5afe7 --- /dev/null +++ b/website/tutorials/customization/time-scale.mdx @@ -0,0 +1,88 @@ +--- +sidebar_position: 6 +title: Time scale +pagination_title: Time scale +sidebar_label: Time scale +description: In the previous step, we adjusted the vertical price scale, now let us adjust the horizontal time scale. +keywords: + - customization + - appearance + - styling +pagination_prev: customization/price-scale +pagination_next: customization/crosshair +--- + +import IterativeGuideWarning from './_iterative-guide-warning-partial.mdx'; + + + +In the previous step, we adjusted the vertical price scale, now let us adjust the horizontal time scale. We previously adjusted the border color of this scale and now we are going to adjust the starting 'zoom' level. + +We will be adjusting the [`barSpacing`](/docs/api/interfaces/TimeScaleOptions#barspacing) option on the time scale which is used when the chart is first rendered to determine the horizontal 'zoom' level. The property sets 'The space between bars in pixels.', where a larger number will result in wider bars and fewer bars visible on the chart. The default value is `6` and we will be increasing it to `10` which will effectively 'zoom in' for the time scale. + +## Adjusting settings for the time scale + +We can get the [ITimeScaleApi](/docs/api/interfaces/ITimeScaleApi) instance for the chart by evoking the `timeScale()` method on the chart reference. + +Just as with the previous step, we can use the `applyOptions()` method on this API instance to adjust it's options. You can add the following code to the example at any point after the `chart` reference has been created, but we will place it directly below where we set the border color for the time scale. The options available are shown here: [TimeScaleOptions](/docs/api/interfaces/TimeScaleOptions). + +```js +// Setting the border color for the horizontal axis +chart.timeScale().applyOptions({ + borderColor: '#71649C', +}); + +// highlight-start +// Adjust the starting bar width (essentially the horizontal zoom) +chart.timeScale().applyOptions({ + barSpacing: 10, +}); +// highlight-end +``` + +The `applyOptions()` calls on the time scale were purposely split into two to show that it is possible to apply the options individually if that leads to cleaner code to read. It is also possible to apply both options in a single step as shown below: + +```js +// Example of applying both properties in a single call +chart.timeScale().applyOptions({ + borderColor: '#71649C', + barSpacing: 10, +}); +``` + +## Auto fitting all the content + +It is possible to auto fit all the content into the visable area of the chart by calling the [`fitContent()`](/docs/api/interfaces/ITimeScaleApi#fitcontent) method on the time scale instance, for example: `chart.timeScale().fitContent();` + +## Result + +At this point we should have a chart like this (noting the wider candlestick bars): + + + + View in a new window + + +## Next steps + +In the next step, we will be adjusting the visual style and behaviour of the crosshair. + +## Download + +You can download the HTML file for example at this stage here in case you've encountered a problem or would like to start the next step from this point. + +## Complete code + +import CodeBlock from '@theme/CodeBlock'; +import code from '!!raw-loader!./assets/step6.html'; +import InstantDetails from '@site/src/components/InstantDetails' + + + +Click here to reveal the complete code for the example at this stage of the guide. + +{code} + diff --git a/website/tutorials/how_to/.eslintrc.js b/website/tutorials/how_to/.eslintrc.js new file mode 100644 index 0000000000..5cd6c516c1 --- /dev/null +++ b/website/tutorials/how_to/.eslintrc.js @@ -0,0 +1,6 @@ +module.exports = { + globals: { + document: false, + createChart: false, + }, +}; diff --git a/website/tutorials/how_to/_usage-guide-partial.mdx b/website/tutorials/how_to/_usage-guide-partial.mdx new file mode 100644 index 0000000000..746ed3e043 --- /dev/null +++ b/website/tutorials/how_to/_usage-guide-partial.mdx @@ -0,0 +1,16 @@ +
+How to use the code sample +The code presented below requires: + +- That `createChart` has already been imported. See [Getting Started](/docs#creating-a-chart) for more information, +- and that there is an html div element on the page with an `id` of `container`. + +Here is an example skeleton setup: [Code Sandbox](https://codesandbox.io/s/lightweight-charts-skeleton-n67pm6). +You can paste the provided code below the `// REPLACE EVERYTHING BELOW HERE` comment. + +:::tip + +Some code may be hidden to improve readability. Toggle the checkbox above the code block to reveal all the code. + +::: +
diff --git a/website/tutorials/how_to/inverted-price-scale.js b/website/tutorials/how_to/inverted-price-scale.js new file mode 100644 index 0000000000..e077441af7 --- /dev/null +++ b/website/tutorials/how_to/inverted-price-scale.js @@ -0,0 +1,187 @@ +// remove-start +// Lightweight Charts Example: Inverted Price Scale +// https://tradingview.github.io/lightweight-charts/tutorials/how_to/inverted-price-scale + +// remove-end +const chartOptions = { + layout: { + textColor: CHART_TEXT_COLOR, + background: { type: 'solid', color: CHART_BACKGROUND_COLOR }, + }, +}; +// remove-line +/** @type {import('lightweight-charts').IChartApi} */ +const chart = createChart(document.getElementById('container'), chartOptions); + +chart.applyOptions({ + rightPriceScale: { + scaleMargins: { + top: 0.1, + bottom: 0.1, + }, + // highlight-start + invertScale: true, + // highlight-end + }, +}); + +const lineSeries = chart.addLineSeries({ color: LINE_LINE_COLOR }); + +const data = [ + { time: '2016-07-18', value: 661.47 }, + // hide-start + { time: '2016-07-25', value: 623.83 }, + { time: '2016-08-01', value: 592.47 }, + { time: '2016-08-08', value: 568.76 }, + { time: '2016-08-15', value: 577.55 }, + { time: '2016-08-22', value: 573.20 }, + { time: '2016-08-29', value: 603.72 }, + { time: '2016-09-05', value: 606.32 }, + { time: '2016-09-12', value: 608.00 }, + { time: '2016-09-19', value: 598.98 }, + { time: '2016-09-26', value: 608.60 }, + { time: '2016-10-03', value: 613.06 }, + { time: '2016-10-10', value: 638.97 }, + { time: '2016-10-17', value: 648.74 }, + { time: '2016-10-24', value: 697.23 }, + { time: '2016-10-31', value: 709.93 }, + { time: '2016-11-07', value: 700.38 }, + { time: '2016-11-14', value: 727.09 }, + { time: '2016-11-21', value: 727.32 }, + { time: '2016-11-28', value: 762.00 }, + { time: '2016-12-05', value: 768.97 }, + { time: '2016-12-12', value: 788.67 }, + { time: '2016-12-19', value: 890.67 }, + { time: '2016-12-26', value: 997.75 }, + { time: '2017-01-02', value: 909.75 }, + { time: '2017-01-09', value: 821.86 }, + { time: '2017-01-16', value: 923.76 }, + { time: '2017-01-23', value: 912.01 }, + { time: '2017-01-30', value: 1011.07 }, + { time: '2017-02-06', value: 1000.73 }, + { time: '2017-02-13', value: 1051.80 }, + { time: '2017-02-20', value: 1179.05 }, + { time: '2017-02-27', value: 1273.00 }, + { time: '2017-03-06', value: 1226.62 }, + { time: '2017-03-13', value: 1017.97 }, + { time: '2017-03-20', value: 960.00 }, + { time: '2017-03-27', value: 1078.01 }, + { time: '2017-04-03', value: 1206.20 }, + { time: '2017-04-10', value: 1162.31 }, + { time: '2017-04-17', value: 1241.99 }, + { time: '2017-04-24', value: 1350.21 }, + { time: '2017-05-01', value: 1554.01 }, + { time: '2017-05-08', value: 1784.00 }, + { time: '2017-05-15', value: 2017.55 }, + { time: '2017-05-22', value: 2178.81 }, + { time: '2017-05-29', value: 2530.27 }, + { time: '2017-06-05', value: 2954.22 }, + { time: '2017-06-12', value: 2516.98 }, + { time: '2017-06-19', value: 2502.03 }, + { time: '2017-06-26', value: 2504.37 }, + { time: '2017-07-03', value: 2502.28 }, + { time: '2017-07-10', value: 1917.63 }, + { time: '2017-07-17', value: 2749.02 }, + { time: '2017-07-24', value: 2742.37 }, + { time: '2017-07-31', value: 3222.75 }, + { time: '2017-08-07', value: 4053.87 }, + { time: '2017-08-14', value: 4058.68 }, + { time: '2017-08-21', value: 4337.68 }, + { time: '2017-08-28', value: 4606.26 }, + { time: '2017-09-04', value: 4226.22 }, + { time: '2017-09-11', value: 3662.99 }, + { time: '2017-09-18', value: 3664.22 }, + { time: '2017-09-25', value: 4377.22 }, + { time: '2017-10-02', value: 4597.98 }, + { time: '2017-10-09', value: 5679.70 }, + { time: '2017-10-16', value: 5969.00 }, + { time: '2017-10-23', value: 6137.37 }, + { time: '2017-10-30', value: 7372.72 }, + { time: '2017-11-06', value: 5870.37 }, + { time: '2017-11-13', value: 8016.58 }, + { time: '2017-11-20', value: 9271.06 }, + { time: '2017-11-27', value: 11250.00 }, + { time: '2017-12-04', value: 14691.00 }, + { time: '2017-12-11', value: 18953.00 }, + { time: '2017-12-18', value: 14157.87 }, + { time: '2017-12-25', value: 13880.00 }, + { time: '2018-01-01', value: 16124.02 }, + { time: '2018-01-08', value: 13647.99 }, + { time: '2018-01-15', value: 11558.87 }, + { time: '2018-01-22', value: 11685.58 }, + { time: '2018-01-29', value: 8191.00 }, + { time: '2018-02-05', value: 8067.00 }, + { time: '2018-02-12', value: 10421.06 }, + { time: '2018-02-19', value: 9590.04 }, + { time: '2018-02-26', value: 11463.27 }, + { time: '2018-03-05', value: 9535.04 }, + { time: '2018-03-12', value: 8188.24 }, + { time: '2018-03-19', value: 8453.90 }, + { time: '2018-03-26', value: 6813.52 }, + { time: '2018-04-02', value: 7027.26 }, + { time: '2018-04-09', value: 8354.22 }, + { time: '2018-04-16', value: 8789.96 }, + { time: '2018-04-23', value: 9393.99 }, + { time: '2018-04-30', value: 9623.54 }, + { time: '2018-05-07', value: 8696.58 }, + { time: '2018-05-14', value: 8518.48 }, + { time: '2018-05-21', value: 7347.39 }, + { time: '2018-05-28', value: 7703.67 }, + { time: '2018-06-04', value: 6781.17 }, + { time: '2018-06-11', value: 6453.41 }, + { time: '2018-06-18', value: 6153.40 }, + { time: '2018-06-25', value: 6349.99 }, + { time: '2018-07-02', value: 6706.60 }, + { time: '2018-07-09', value: 6349.30 }, + { time: '2018-07-16', value: 7396.60 }, + { time: '2018-07-23', value: 8216.74 }, + { time: '2018-07-30', value: 7032.61 }, + { time: '2018-08-06', value: 6310.82 }, + { time: '2018-08-13', value: 6481.99 }, + { time: '2018-08-20', value: 6700.46 }, + { time: '2018-08-27', value: 7290.31 }, + { time: '2018-09-03', value: 6236.04 }, + { time: '2018-09-10', value: 6499.98 }, + { time: '2018-09-17', value: 6702.22 }, + { time: '2018-09-24', value: 6597.81 }, + { time: '2018-10-01', value: 6577.63 }, + { time: '2018-10-08', value: 6183.00 }, + { time: '2018-10-15', value: 6413.38 }, + { time: '2018-10-22', value: 6405.57 }, + { time: '2018-10-29', value: 6421.76 }, + { time: '2018-11-05', value: 6357.54 }, + { time: '2018-11-12', value: 5559.26 }, + { time: '2018-11-19', value: 3938.89 }, + { time: '2018-11-26', value: 4102.05 }, + { time: '2018-12-03', value: 3529.75 }, + { time: '2018-12-10', value: 3193.78 }, + { time: '2018-12-17', value: 3943.83 }, + { time: '2018-12-24', value: 3835.79 }, + { time: '2018-12-31', value: 4040.71 }, + { time: '2019-01-07', value: 3515.95 }, + { time: '2019-01-14', value: 3536.72 }, + { time: '2019-01-21', value: 3533.23 }, + { time: '2019-01-28', value: 3414.82 }, + { time: '2019-02-04', value: 3650.37 }, + { time: '2019-02-11', value: 3625.60 }, + { time: '2019-02-18', value: 3730.68 }, + { time: '2019-02-25', value: 3789.52 }, + { time: '2019-03-04', value: 3897.92 }, + { time: '2019-03-11', value: 3965.50 }, + { time: '2019-03-18', value: 3969.99 }, + { time: '2019-03-25', value: 4096.08 }, + { time: '2019-04-01', value: 5190.85 }, + { time: '2019-04-08', value: 5162.72 }, + { time: '2019-04-15', value: 5295.65 }, + { time: '2019-04-22', value: 5160.98 }, + { time: '2019-04-29', value: 5709.32 }, + { time: '2019-05-06', value: 6974.35 }, + { time: '2019-05-13', value: 8200.00 }, + { time: '2019-05-20', value: 8733.26 }, + { time: '2019-05-27', value: 8702.43 }, + // hide-end +]; + +lineSeries.setData(data); + +chart.timeScale().fitContent(); diff --git a/website/tutorials/how_to/inverted-price-scale.mdx b/website/tutorials/how_to/inverted-price-scale.mdx new file mode 100644 index 0000000000..d4af973445 --- /dev/null +++ b/website/tutorials/how_to/inverted-price-scale.mdx @@ -0,0 +1,54 @@ +--- +title: Inverted Price Scale +sidebar_label: Inverted Price Scale +description: How to invert a price scale. +pagination_prev: null +pagination_next: null +keywords: + - price scale + - Inverted + - example +--- + +This example shows how to invert a price scale. Usually, the price scale will +map the range of numbers from small to large along the vertical axis from bottom +to top. Inverting the price scale will change this such that the values map from +top to bottom. + +## How to + +Set the [`invertScale`](/docs/api/interfaces/PriceScaleOptions#invertscale) property +on the [priceScale options](/docs/api/interfaces/PriceScaleOptions) to `true`. + +```js +chart.applyOptions({ + rightPriceScale: { + invertScale: true, + }, +}); + +// or (for a specific price scale) +const priceScale = chart.priceScale(); +priceScale.applyOptions({ + invertScale: true, +}); +``` + +You can see a full [working example](#full-example) below. + +## Resources +- [invertScale](/docs/api/interfaces/PriceScaleOptions#invertscale) +- [Price Scales](/docs/price-scale) - General introduction to Price Scales. + +## Full example + +import UsageGuidePartial from "./_usage-guide-partial.mdx"; + + + +import CodeBlock from "@theme/CodeBlock"; +import code from "!!raw-loader!./inverted-price-scale.js"; + + + {code} + diff --git a/website/tutorials/how_to/legend-3line.js b/website/tutorials/how_to/legend-3line.js new file mode 100644 index 0000000000..71c88a4b7b --- /dev/null +++ b/website/tutorials/how_to/legend-3line.js @@ -0,0 +1,238 @@ +// remove-start +// Lightweight Charts Example: Legend 3 Lines +// https://tradingview.github.io/lightweight-charts/tutorials/how_to/legends + +// remove-end +const chartOptions = { + layout: { + textColor: CHART_TEXT_COLOR, + background: { type: 'solid', color: CHART_BACKGROUND_COLOR }, + }, +}; +// remove-line +/** @type {import('lightweight-charts').IChartApi} */ +const chart = createChart(document.getElementById('container'), chartOptions); + +chart.applyOptions({ + rightPriceScale: { + scaleMargins: { + top: 0.4, // leave some space for the legend + bottom: 0.15, + }, + }, + crosshair: { + // hide the horizontal crosshair line + horzLine: { + visible: false, + labelVisible: false, + }, + }, + // hide the grid lines + grid: { + vertLines: { + visible: false, + }, + horzLines: { + visible: false, + }, + }, +}); + +const areaSeries = chart.addAreaSeries({ + topColor: AREA_TOP_COLOR, + bottomColor: AREA_BOTTOM_COLOR, + lineColor: LINE_LINE_COLOR, + lineWidth: 2, + crossHairMarkerVisible: false, +}); + +const data = [ + { time: '2018-10-19', value: 26.19 }, + // hide-start + { time: '2018-10-22', value: 25.87 }, + { time: '2018-10-23', value: 25.83 }, + { time: '2018-10-24', value: 25.78 }, + { time: '2018-10-25', value: 25.82 }, + { time: '2018-10-26', value: 25.81 }, + { time: '2018-10-29', value: 25.82 }, + { time: '2018-10-30', value: 25.71 }, + { time: '2018-10-31', value: 25.82 }, + { time: '2018-11-01', value: 25.72 }, + { time: '2018-11-02', value: 25.74 }, + { time: '2018-11-05', value: 25.81 }, + { time: '2018-11-06', value: 25.75 }, + { time: '2018-11-07', value: 25.73 }, + { time: '2018-11-08', value: 25.75 }, + { time: '2018-11-09', value: 25.75 }, + { time: '2018-11-12', value: 25.76 }, + { time: '2018-11-13', value: 25.8 }, + { time: '2018-11-14', value: 25.77 }, + { time: '2018-11-15', value: 25.75 }, + { time: '2018-11-16', value: 25.75 }, + { time: '2018-11-19', value: 25.75 }, + { time: '2018-11-20', value: 25.72 }, + { time: '2018-11-21', value: 25.78 }, + { time: '2018-11-23', value: 25.72 }, + { time: '2018-11-26', value: 25.78 }, + { time: '2018-11-27', value: 25.85 }, + { time: '2018-11-28', value: 25.85 }, + { time: '2018-11-29', value: 25.55 }, + { time: '2018-11-30', value: 25.41 }, + { time: '2018-12-03', value: 25.41 }, + { time: '2018-12-04', value: 25.42 }, + { time: '2018-12-06', value: 25.33 }, + { time: '2018-12-07', value: 25.39 }, + { time: '2018-12-10', value: 25.32 }, + { time: '2018-12-11', value: 25.48 }, + { time: '2018-12-12', value: 25.39 }, + { time: '2018-12-13', value: 25.45 }, + { time: '2018-12-14', value: 25.52 }, + { time: '2018-12-17', value: 25.38 }, + { time: '2018-12-18', value: 25.36 }, + { time: '2018-12-19', value: 25.65 }, + { time: '2018-12-20', value: 25.7 }, + { time: '2018-12-21', value: 25.66 }, + { time: '2018-12-24', value: 25.66 }, + { time: '2018-12-26', value: 25.65 }, + { time: '2018-12-27', value: 25.66 }, + { time: '2018-12-28', value: 25.68 }, + { time: '2018-12-31', value: 25.77 }, + { time: '2019-01-02', value: 25.72 }, + { time: '2019-01-03', value: 25.69 }, + { time: '2019-01-04', value: 25.71 }, + { time: '2019-01-07', value: 25.72 }, + { time: '2019-01-08', value: 25.72 }, + { time: '2019-01-09', value: 25.66 }, + { time: '2019-01-10', value: 25.85 }, + { time: '2019-01-11', value: 25.92 }, + { time: '2019-01-14', value: 25.94 }, + { time: '2019-01-15', value: 25.95 }, + { time: '2019-01-16', value: 26.0 }, + { time: '2019-01-17', value: 25.99 }, + { time: '2019-01-18', value: 25.6 }, + { time: '2019-01-22', value: 25.81 }, + { time: '2019-01-23', value: 25.7 }, + { time: '2019-01-24', value: 25.74 }, + { time: '2019-01-25', value: 25.8 }, + { time: '2019-01-28', value: 25.83 }, + { time: '2019-01-29', value: 25.7 }, + { time: '2019-01-30', value: 25.78 }, + { time: '2019-01-31', value: 25.35 }, + { time: '2019-02-01', value: 25.6 }, + { time: '2019-02-04', value: 25.65 }, + { time: '2019-02-05', value: 25.73 }, + { time: '2019-02-06', value: 25.71 }, + { time: '2019-02-07', value: 25.71 }, + { time: '2019-02-08', value: 25.72 }, + { time: '2019-02-11', value: 25.76 }, + { time: '2019-02-12', value: 25.84 }, + { time: '2019-02-13', value: 25.85 }, + { time: '2019-02-14', value: 25.87 }, + { time: '2019-02-15', value: 25.89 }, + { time: '2019-02-19', value: 25.9 }, + { time: '2019-02-20', value: 25.92 }, + { time: '2019-02-21', value: 25.96 }, + { time: '2019-02-22', value: 26.0 }, + { time: '2019-02-25', value: 25.93 }, + { time: '2019-02-26', value: 25.92 }, + { time: '2019-02-27', value: 25.67 }, + { time: '2019-02-28', value: 25.79 }, + { time: '2019-03-01', value: 25.86 }, + { time: '2019-03-04', value: 25.94 }, + { time: '2019-03-05', value: 26.02 }, + { time: '2019-03-06', value: 25.95 }, + { time: '2019-03-07', value: 25.89 }, + { time: '2019-03-08', value: 25.94 }, + { time: '2019-03-11', value: 25.91 }, + { time: '2019-03-12', value: 25.92 }, + { time: '2019-03-13', value: 26.0 }, + { time: '2019-03-14', value: 26.05 }, + { time: '2019-03-15', value: 26.11 }, + { time: '2019-03-18', value: 26.1 }, + { time: '2019-03-19', value: 25.98 }, + { time: '2019-03-20', value: 26.11 }, + { time: '2019-03-21', value: 26.12 }, + { time: '2019-03-22', value: 25.88 }, + { time: '2019-03-25', value: 25.85 }, + { time: '2019-03-26', value: 25.72 }, + { time: '2019-03-27', value: 25.73 }, + { time: '2019-03-28', value: 25.8 }, + { time: '2019-03-29', value: 25.77 }, + { time: '2019-04-01', value: 26.06 }, + { time: '2019-04-02', value: 25.93 }, + { time: '2019-04-03', value: 25.95 }, + { time: '2019-04-04', value: 26.06 }, + { time: '2019-04-05', value: 26.16 }, + { time: '2019-04-08', value: 26.12 }, + { time: '2019-04-09', value: 26.07 }, + { time: '2019-04-10', value: 26.13 }, + { time: '2019-04-11', value: 26.04 }, + { time: '2019-04-12', value: 26.04 }, + { time: '2019-04-15', value: 26.05 }, + { time: '2019-04-16', value: 26.01 }, + { time: '2019-04-17', value: 26.09 }, + { time: '2019-04-18', value: 26.0 }, + { time: '2019-04-22', value: 26.0 }, + { time: '2019-04-23', value: 26.06 }, + { time: '2019-04-24', value: 26.0 }, + { time: '2019-04-25', value: 25.81 }, + { time: '2019-04-26', value: 25.88 }, + { time: '2019-04-29', value: 25.91 }, + { time: '2019-04-30', value: 25.9 }, + { time: '2019-05-01', value: 26.02 }, + { time: '2019-05-02', value: 25.97 }, + { time: '2019-05-03', value: 26.02 }, + { time: '2019-05-06', value: 26.03 }, + { time: '2019-05-07', value: 26.04 }, + { time: '2019-05-08', value: 26.05 }, + { time: '2019-05-09', value: 26.05 }, + { time: '2019-05-10', value: 26.08 }, + { time: '2019-05-13', value: 26.05 }, + { time: '2019-05-14', value: 26.01 }, + { time: '2019-05-15', value: 26.03 }, + { time: '2019-05-16', value: 26.14 }, + { time: '2019-05-17', value: 26.09 }, + { time: '2019-05-20', value: 26.01 }, + { time: '2019-05-21', value: 26.12 }, + { time: '2019-05-22', value: 26.15 }, + { time: '2019-05-23', value: 26.18 }, + { time: '2019-05-24', value: 26.16 }, + { time: '2019-05-28', value: 26.23 }, + // hide-end +]; + +areaSeries.setData(data); + +const symbolName = 'AEROSPACE'; + +const container = document.getElementById('container'); + +const legend = document.createElement('div'); +legend.style = `position: absolute; left: 12px; top: 12px; z-index: 1; font-size: 14px; font-family: sans-serif; line-height: 18px; font-weight: 300;`; +legend.style.color = CHART_TEXT_COLOR; +container.appendChild(legend); + +const getLastBar = () => data[data.length - 1]; +const buildDateString = time => `${time.year} - ${time.month} - ${time.day}`; +const formatPrice = price => (Math.round(price * 100) / 100).toFixed(2); +const setTooltipHtml = (name, date, price) => { + legend.innerHTML = `
${name}
${price}
${date}
`; +}; + +const updateLegend = param => { + const validCrosshairPoint = !( + param === undefined || param.time === undefined || param.point.x < 0 || param.point.y < 0 + ); + const bar = validCrosshairPoint ? param : getLastBar(); + const time = bar.time; + const date = buildDateString(time); + const price = validCrosshairPoint ? param.seriesPrices.get(areaSeries) : bar.value; + const formattedPrice = formatPrice(price); + setTooltipHtml(symbolName, date, formattedPrice); +}; + +chart.subscribeCrosshairMove(updateLegend); + +updateLegend(undefined); + +chart.timeScale().fitContent(); diff --git a/website/tutorials/how_to/legend.js b/website/tutorials/how_to/legend.js new file mode 100644 index 0000000000..67c5caebe3 --- /dev/null +++ b/website/tutorials/how_to/legend.js @@ -0,0 +1,226 @@ +// remove-start +// Lightweight Charts Example: Legend +// https://tradingview.github.io/lightweight-charts/tutorials/how_to/legends + +// remove-end +const chartOptions = { + layout: { + textColor: CHART_TEXT_COLOR, + background: { type: 'solid', color: CHART_BACKGROUND_COLOR }, + }, +}; +// remove-line +/** @type {import('lightweight-charts').IChartApi} */ +const chart = createChart(document.getElementById('container'), chartOptions); + +chart.applyOptions({ + rightPriceScale: { + scaleMargins: { + top: 0.3, // leave some space for the legend + bottom: 0.25, + }, + }, + crosshair: { + // hide the horizontal crosshair line + horzLine: { + visible: false, + labelVisible: false, + }, + }, + // hide the grid lines + grid: { + vertLines: { + visible: false, + }, + horzLines: { + visible: false, + }, + }, +}); + +const areaSeries = chart.addAreaSeries({ + topColor: AREA_TOP_COLOR, + bottomColor: AREA_BOTTOM_COLOR, + lineColor: LINE_LINE_COLOR, + lineWidth: 2, + crossHairMarkerVisible: false, +}); + +areaSeries.setData([ + { time: '2018-10-19', value: 26.19 }, + // hide-start + { time: '2018-10-22', value: 25.87 }, + { time: '2018-10-23', value: 25.83 }, + { time: '2018-10-24', value: 25.78 }, + { time: '2018-10-25', value: 25.82 }, + { time: '2018-10-26', value: 25.81 }, + { time: '2018-10-29', value: 25.82 }, + { time: '2018-10-30', value: 25.71 }, + { time: '2018-10-31', value: 25.82 }, + { time: '2018-11-01', value: 25.72 }, + { time: '2018-11-02', value: 25.74 }, + { time: '2018-11-05', value: 25.81 }, + { time: '2018-11-06', value: 25.75 }, + { time: '2018-11-07', value: 25.73 }, + { time: '2018-11-08', value: 25.75 }, + { time: '2018-11-09', value: 25.75 }, + { time: '2018-11-12', value: 25.76 }, + { time: '2018-11-13', value: 25.8 }, + { time: '2018-11-14', value: 25.77 }, + { time: '2018-11-15', value: 25.75 }, + { time: '2018-11-16', value: 25.75 }, + { time: '2018-11-19', value: 25.75 }, + { time: '2018-11-20', value: 25.72 }, + { time: '2018-11-21', value: 25.78 }, + { time: '2018-11-23', value: 25.72 }, + { time: '2018-11-26', value: 25.78 }, + { time: '2018-11-27', value: 25.85 }, + { time: '2018-11-28', value: 25.85 }, + { time: '2018-11-29', value: 25.55 }, + { time: '2018-11-30', value: 25.41 }, + { time: '2018-12-03', value: 25.41 }, + { time: '2018-12-04', value: 25.42 }, + { time: '2018-12-06', value: 25.33 }, + { time: '2018-12-07', value: 25.39 }, + { time: '2018-12-10', value: 25.32 }, + { time: '2018-12-11', value: 25.48 }, + { time: '2018-12-12', value: 25.39 }, + { time: '2018-12-13', value: 25.45 }, + { time: '2018-12-14', value: 25.52 }, + { time: '2018-12-17', value: 25.38 }, + { time: '2018-12-18', value: 25.36 }, + { time: '2018-12-19', value: 25.65 }, + { time: '2018-12-20', value: 25.7 }, + { time: '2018-12-21', value: 25.66 }, + { time: '2018-12-24', value: 25.66 }, + { time: '2018-12-26', value: 25.65 }, + { time: '2018-12-27', value: 25.66 }, + { time: '2018-12-28', value: 25.68 }, + { time: '2018-12-31', value: 25.77 }, + { time: '2019-01-02', value: 25.72 }, + { time: '2019-01-03', value: 25.69 }, + { time: '2019-01-04', value: 25.71 }, + { time: '2019-01-07', value: 25.72 }, + { time: '2019-01-08', value: 25.72 }, + { time: '2019-01-09', value: 25.66 }, + { time: '2019-01-10', value: 25.85 }, + { time: '2019-01-11', value: 25.92 }, + { time: '2019-01-14', value: 25.94 }, + { time: '2019-01-15', value: 25.95 }, + { time: '2019-01-16', value: 26.0 }, + { time: '2019-01-17', value: 25.99 }, + { time: '2019-01-18', value: 25.6 }, + { time: '2019-01-22', value: 25.81 }, + { time: '2019-01-23', value: 25.7 }, + { time: '2019-01-24', value: 25.74 }, + { time: '2019-01-25', value: 25.8 }, + { time: '2019-01-28', value: 25.83 }, + { time: '2019-01-29', value: 25.7 }, + { time: '2019-01-30', value: 25.78 }, + { time: '2019-01-31', value: 25.35 }, + { time: '2019-02-01', value: 25.6 }, + { time: '2019-02-04', value: 25.65 }, + { time: '2019-02-05', value: 25.73 }, + { time: '2019-02-06', value: 25.71 }, + { time: '2019-02-07', value: 25.71 }, + { time: '2019-02-08', value: 25.72 }, + { time: '2019-02-11', value: 25.76 }, + { time: '2019-02-12', value: 25.84 }, + { time: '2019-02-13', value: 25.85 }, + { time: '2019-02-14', value: 25.87 }, + { time: '2019-02-15', value: 25.89 }, + { time: '2019-02-19', value: 25.9 }, + { time: '2019-02-20', value: 25.92 }, + { time: '2019-02-21', value: 25.96 }, + { time: '2019-02-22', value: 26.0 }, + { time: '2019-02-25', value: 25.93 }, + { time: '2019-02-26', value: 25.92 }, + { time: '2019-02-27', value: 25.67 }, + { time: '2019-02-28', value: 25.79 }, + { time: '2019-03-01', value: 25.86 }, + { time: '2019-03-04', value: 25.94 }, + { time: '2019-03-05', value: 26.02 }, + { time: '2019-03-06', value: 25.95 }, + { time: '2019-03-07', value: 25.89 }, + { time: '2019-03-08', value: 25.94 }, + { time: '2019-03-11', value: 25.91 }, + { time: '2019-03-12', value: 25.92 }, + { time: '2019-03-13', value: 26.0 }, + { time: '2019-03-14', value: 26.05 }, + { time: '2019-03-15', value: 26.11 }, + { time: '2019-03-18', value: 26.1 }, + { time: '2019-03-19', value: 25.98 }, + { time: '2019-03-20', value: 26.11 }, + { time: '2019-03-21', value: 26.12 }, + { time: '2019-03-22', value: 25.88 }, + { time: '2019-03-25', value: 25.85 }, + { time: '2019-03-26', value: 25.72 }, + { time: '2019-03-27', value: 25.73 }, + { time: '2019-03-28', value: 25.8 }, + { time: '2019-03-29', value: 25.77 }, + { time: '2019-04-01', value: 26.06 }, + { time: '2019-04-02', value: 25.93 }, + { time: '2019-04-03', value: 25.95 }, + { time: '2019-04-04', value: 26.06 }, + { time: '2019-04-05', value: 26.16 }, + { time: '2019-04-08', value: 26.12 }, + { time: '2019-04-09', value: 26.07 }, + { time: '2019-04-10', value: 26.13 }, + { time: '2019-04-11', value: 26.04 }, + { time: '2019-04-12', value: 26.04 }, + { time: '2019-04-15', value: 26.05 }, + { time: '2019-04-16', value: 26.01 }, + { time: '2019-04-17', value: 26.09 }, + { time: '2019-04-18', value: 26.0 }, + { time: '2019-04-22', value: 26.0 }, + { time: '2019-04-23', value: 26.06 }, + { time: '2019-04-24', value: 26.0 }, + { time: '2019-04-25', value: 25.81 }, + { time: '2019-04-26', value: 25.88 }, + { time: '2019-04-29', value: 25.91 }, + { time: '2019-04-30', value: 25.9 }, + { time: '2019-05-01', value: 26.02 }, + { time: '2019-05-02', value: 25.97 }, + { time: '2019-05-03', value: 26.02 }, + { time: '2019-05-06', value: 26.03 }, + { time: '2019-05-07', value: 26.04 }, + { time: '2019-05-08', value: 26.05 }, + { time: '2019-05-09', value: 26.05 }, + { time: '2019-05-10', value: 26.08 }, + { time: '2019-05-13', value: 26.05 }, + { time: '2019-05-14', value: 26.01 }, + { time: '2019-05-15', value: 26.03 }, + { time: '2019-05-16', value: 26.14 }, + { time: '2019-05-17', value: 26.09 }, + { time: '2019-05-20', value: 26.01 }, + { time: '2019-05-21', value: 26.12 }, + { time: '2019-05-22', value: 26.15 }, + { time: '2019-05-23', value: 26.18 }, + { time: '2019-05-24', value: 26.16 }, + { time: '2019-05-28', value: 26.23 }, + // hide-end +]); + +const symbolName = 'ETC USD 7D VWAP'; + +const container = document.getElementById('container'); + +const legend = document.createElement('div'); +legend.style = `position: absolute; left: 12px; top: 12px; z-index: 1; font-size: 14px; font-family: sans-serif; line-height: 18px; font-weight: 300;`; +container.appendChild(legend); + +const firstRow = document.createElement('div'); +firstRow.innerHTML = symbolName; +firstRow.style.color = CHART_TEXT_COLOR; +legend.appendChild(firstRow); + +chart.subscribeCrosshairMove(param => { + let priceFormatted = ''; + if (param.time) { + const price = param.seriesPrices.get(areaSeries); + priceFormatted = price.toFixed(2); + } + firstRow.innerHTML = `${symbolName} ${priceFormatted}`; +}); + +chart.timeScale().fitContent(); diff --git a/website/tutorials/how_to/legends.mdx b/website/tutorials/how_to/legends.mdx new file mode 100644 index 0000000000..7056027d42 --- /dev/null +++ b/website/tutorials/how_to/legends.mdx @@ -0,0 +1,73 @@ +--- +title: Legends +sidebar_label: Legends +description: Examples on how to add a legend to your chart. +pagination_prev: null +pagination_next: null +keywords: + - example + - legend +--- + +Lightweight charts doesn't include a built-in legend feature, however it is something which can be added +to your chart by following the examples presented below. + +## How to + +In order to add a legend to the chart we need to create and position an `html` into the desired position above +the chart. We can then subscribe to the crosshairMove events ([subscribeCrosshairMove](/docs/api/interfaces/IChartApi#subscribecrosshairmove)) provided by the [`IChartApi`](/docs/api/interfaces/IChartApi) instance, and manually +update the content within our `html` legend element. + +```js +chart.subscribeCrosshairMove(param => { + let priceFormatted = ''; + if (param.time) { + const price = param.seriesPrices.get(areaSeries); + priceFormatted = price.toFixed(2); + } + // legend is a html element which has already been created + legend.innerHTML = `${symbolName} ${priceFormatted}`; +}); +``` + +The process of creating the legend html element and positioning can be seen within the examples below. +Essentially, we create a new div element within the container div (holding the chart) and then position +and style it using `css`. + +You can see full [working examples](#examples) below. + +## Resources + +- [subscribeCrosshairMove](/docs/api/interfaces/IChartApi#subscribecrosshairmove) +- [MouseEventParams Interface](/docs/api/interfaces/MouseEventParams) +- [MouseEventhandler](/docs/api#mouseeventhandler) + +Below are a few external resources related to creating and styling html elements: + +- [createElement](https://developer.mozilla.org/en-US/docs/Web/API/Document/createElement) +- [innerHTML](https://developer.mozilla.org/en-US/docs/Web/API/Element/innerHTML) +- [style property](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/style) + +## Examples + +import UsageGuidePartial from "./_usage-guide-partial.mdx"; + + + +import CodeBlock from "@theme/CodeBlock"; + +### Simple Legend Example + +import codeLegend from "!!raw-loader!./legend.js"; + + + {codeLegend} + + +### 3 Line Legend Example + +import code3Line from "!!raw-loader!./legend-3line.js"; + + + {code3Line} + diff --git a/website/tutorials/how_to/no-time-scale.js b/website/tutorials/how_to/no-time-scale.js new file mode 100644 index 0000000000..0d97724901 --- /dev/null +++ b/website/tutorials/how_to/no-time-scale.js @@ -0,0 +1,43 @@ +// remove-start +// Lightweight Charts Example: No Time Scale +// https://tradingview.github.io/lightweight-charts/tutorials/how_to/example + +// remove-end +const chartOptions = { + layout: { + textColor: CHART_TEXT_COLOR, + background: { type: 'solid', color: CHART_BACKGROUND_COLOR }, + }, +}; +// remove-line +/** @type {import('lightweight-charts').IChartApi} */ +const chart = createChart(document.getElementById('container'), chartOptions); + +// highlight-start +chart.applyOptions({ + timeScale: { + visible: false, + }, +}); +// highlight-end + +const lineSeries = chart.addLineSeries({ color: LINE_LINE_COLOR }); + +const data = [ + { value: 0, time: 1642425322 }, + // hide-start + { value: 8, time: 1642511722 }, + { value: 10, time: 1642598122 }, + { value: 20, time: 1642684522 }, + { value: 3, time: 1642770922 }, + { value: 43, time: 1642857322 }, + { value: 41, time: 1642943722 }, + { value: 43, time: 1643030122 }, + { value: 56, time: 1643116522 }, + { value: 46, time: 1643202922 }, + // hide-end +]; + +lineSeries.setData(data); + +chart.timeScale().fitContent(); diff --git a/website/tutorials/how_to/price-and-volume.js b/website/tutorials/how_to/price-and-volume.js new file mode 100644 index 0000000000..2e3d065a03 --- /dev/null +++ b/website/tutorials/how_to/price-and-volume.js @@ -0,0 +1,360 @@ +// remove-start +// Lightweight Charts Example: Price and Volume +// https://tradingview.github.io/lightweight-charts/tutorials/how_to/price-and-volume + +// remove-end +const chartOptions = { + layout: { + textColor: CHART_TEXT_COLOR, + background: { type: 'solid', color: CHART_BACKGROUND_COLOR }, + }, + // highlight-start + rightPriceScale: { + // positioning the price scale for the area series + scaleMargins: { + top: 0.1, + bottom: 0.4, + }, + borderVisible: false, + }, + // highlight-end +}; +// remove-line +/** @type {import('lightweight-charts').IChartApi} */ +const chart = createChart(document.getElementById('container'), chartOptions); + +const areaSeries = chart.addAreaSeries({ + topColor: AREA_TOP_COLOR, + bottomColor: AREA_BOTTOM_COLOR, + lineColor: LINE_LINE_COLOR, + lineWidth: 2, +}); + +const volumeSeries = chart.addHistogramSeries({ + color: BAR_UP_COLOR, + // highlight-start + priceFormat: { + type: 'volume', + }, + priceScaleId: '', // set as an overlay by setting a blank priceScaleId + // set the positioning of the volume series + scaleMargins: { + top: 0.7, // highest point of the series will be 70% away from the top + bottom: 0, + }, + // highlight-end +}); + +areaSeries.setData([ + { time: '2018-10-19', value: 54.90 }, + // hide-start + { time: '2018-10-22', value: 54.98 }, + { time: '2018-10-23', value: 57.21 }, + { time: '2018-10-24', value: 57.42 }, + { time: '2018-10-25', value: 56.43 }, + { time: '2018-10-26', value: 55.51 }, + { time: '2018-10-29', value: 56.48 }, + { time: '2018-10-30', value: 58.18 }, + { time: '2018-10-31', value: 57.09 }, + { time: '2018-11-01', value: 56.05 }, + { time: '2018-11-02', value: 56.63 }, + { time: '2018-11-05', value: 57.21 }, + { time: '2018-11-06', value: 57.21 }, + { time: '2018-11-07', value: 57.65 }, + { time: '2018-11-08', value: 58.27 }, + { time: '2018-11-09', value: 58.46 }, + { time: '2018-11-12', value: 58.72 }, + { time: '2018-11-13', value: 58.66 }, + { time: '2018-11-14', value: 58.94 }, + { time: '2018-11-15', value: 59.08 }, + { time: '2018-11-16', value: 60.21 }, + { time: '2018-11-19', value: 60.62 }, + { time: '2018-11-20', value: 59.46 }, + { time: '2018-11-21', value: 59.16 }, + { time: '2018-11-23', value: 58.64 }, + { time: '2018-11-26', value: 59.17 }, + { time: '2018-11-27', value: 60.65 }, + { time: '2018-11-28', value: 60.06 }, + { time: '2018-11-29', value: 59.45 }, + { time: '2018-11-30', value: 60.30 }, + { time: '2018-12-03', value: 58.16 }, + { time: '2018-12-04', value: 58.09 }, + { time: '2018-12-06', value: 58.08 }, + { time: '2018-12-07', value: 57.68 }, + { time: '2018-12-10', value: 58.27 }, + { time: '2018-12-11', value: 58.85 }, + { time: '2018-12-12', value: 57.25 }, + { time: '2018-12-13', value: 57.09 }, + { time: '2018-12-14', value: 57.08 }, + { time: '2018-12-17', value: 55.95 }, + { time: '2018-12-18', value: 55.65 }, + { time: '2018-12-19', value: 55.86 }, + { time: '2018-12-20', value: 55.07 }, + { time: '2018-12-21', value: 54.92 }, + { time: '2018-12-24', value: 53.05 }, + { time: '2018-12-26', value: 54.44 }, + { time: '2018-12-27', value: 55.15 }, + { time: '2018-12-28', value: 55.27 }, + { time: '2018-12-31', value: 56.22 }, + { time: '2019-01-02', value: 56.02 }, + { time: '2019-01-03', value: 56.22 }, + { time: '2019-01-04', value: 56.36 }, + { time: '2019-01-07', value: 56.72 }, + { time: '2019-01-08', value: 58.38 }, + { time: '2019-01-09', value: 57.05 }, + { time: '2019-01-10', value: 57.60 }, + { time: '2019-01-11', value: 58.02 }, + { time: '2019-01-14', value: 58.03 }, + { time: '2019-01-15', value: 58.10 }, + { time: '2019-01-16', value: 57.08 }, + { time: '2019-01-17', value: 56.83 }, + { time: '2019-01-18', value: 57.09 }, + { time: '2019-01-22', value: 56.99 }, + { time: '2019-01-23', value: 57.76 }, + { time: '2019-01-24', value: 57.07 }, + { time: '2019-01-25', value: 56.40 }, + { time: '2019-01-28', value: 55.07 }, + { time: '2019-01-29', value: 53.28 }, + { time: '2019-01-30', value: 54.00 }, + { time: '2019-01-31', value: 55.06 }, + { time: '2019-02-01', value: 54.55 }, + { time: '2019-02-04', value: 54.04 }, + { time: '2019-02-05', value: 54.14 }, + { time: '2019-02-06', value: 53.79 }, + { time: '2019-02-07', value: 53.57 }, + { time: '2019-02-08', value: 53.95 }, + { time: '2019-02-11', value: 54.05 }, + { time: '2019-02-12', value: 54.42 }, + { time: '2019-02-13', value: 54.48 }, + { time: '2019-02-14', value: 54.03 }, + { time: '2019-02-15', value: 55.16 }, + { time: '2019-02-19', value: 55.44 }, + { time: '2019-02-20', value: 55.76 }, + { time: '2019-02-21', value: 56.15 }, + { time: '2019-02-22', value: 56.92 }, + { time: '2019-02-25', value: 56.78 }, + { time: '2019-02-26', value: 56.64 }, + { time: '2019-02-27', value: 56.72 }, + { time: '2019-02-28', value: 56.92 }, + { time: '2019-03-01', value: 56.96 }, + { time: '2019-03-04', value: 56.24 }, + { time: '2019-03-05', value: 56.08 }, + { time: '2019-03-06', value: 55.68 }, + { time: '2019-03-07', value: 56.30 }, + { time: '2019-03-08', value: 56.53 }, + { time: '2019-03-11', value: 57.58 }, + { time: '2019-03-12', value: 57.43 }, + { time: '2019-03-13', value: 57.66 }, + { time: '2019-03-14', value: 57.95 }, + { time: '2019-03-15', value: 58.39 }, + { time: '2019-03-18', value: 58.07 }, + { time: '2019-03-19', value: 57.50 }, + { time: '2019-03-20', value: 57.67 }, + { time: '2019-03-21', value: 58.29 }, + { time: '2019-03-22', value: 59.76 }, + { time: '2019-03-25', value: 60.08 }, + { time: '2019-03-26', value: 60.63 }, + { time: '2019-03-27', value: 60.88 }, + { time: '2019-03-28', value: 59.08 }, + { time: '2019-03-29', value: 59.13 }, + { time: '2019-04-01', value: 59.09 }, + { time: '2019-04-02', value: 58.53 }, + { time: '2019-04-03', value: 58.87 }, + { time: '2019-04-04', value: 58.99 }, + { time: '2019-04-05', value: 59.09 }, + { time: '2019-04-08', value: 59.13 }, + { time: '2019-04-09', value: 58.40 }, + { time: '2019-04-10', value: 58.61 }, + { time: '2019-04-11', value: 58.56 }, + { time: '2019-04-12', value: 58.74 }, + { time: '2019-04-15', value: 58.71 }, + { time: '2019-04-16', value: 58.79 }, + { time: '2019-04-17', value: 57.78 }, + { time: '2019-04-18', value: 58.04 }, + { time: '2019-04-22', value: 58.37 }, + { time: '2019-04-23', value: 57.15 }, + { time: '2019-04-24', value: 57.08 }, + { time: '2019-04-25', value: 55.85 }, + { time: '2019-04-26', value: 56.58 }, + { time: '2019-04-29', value: 56.84 }, + { time: '2019-04-30', value: 57.19 }, + { time: '2019-05-01', value: 56.52 }, + { time: '2019-05-02', value: 56.99 }, + { time: '2019-05-03', value: 57.24 }, + { time: '2019-05-06', value: 56.91 }, + { time: '2019-05-07', value: 56.63 }, + { time: '2019-05-08', value: 56.38 }, + { time: '2019-05-09', value: 56.48 }, + { time: '2019-05-10', value: 56.91 }, + { time: '2019-05-13', value: 56.75 }, + { time: '2019-05-14', value: 56.55 }, + { time: '2019-05-15', value: 56.81 }, + { time: '2019-05-16', value: 57.38 }, + { time: '2019-05-17', value: 58.09 }, + { time: '2019-05-20', value: 59.01 }, + { time: '2019-05-21', value: 59.50 }, + { time: '2019-05-22', value: 59.25 }, + { time: '2019-05-23', value: 58.87 }, + { time: '2019-05-24', value: 59.32 }, + { time: '2019-05-28', value: 59.57 }, + // hide-end +]); + +// setting the data for the volume series. +// note: we are defining each bars color as part of the data +volumeSeries.setData([ + { time: '2018-10-19', value: 19103293.00, color: BAR_UP_COLOR }, + // hide-start + { time: '2018-10-22', value: 21737523.00, color: BAR_UP_COLOR }, + { time: '2018-10-23', value: 29328713.00, color: BAR_UP_COLOR }, + { time: '2018-10-24', value: 37435638.00, color: BAR_UP_COLOR }, + { time: '2018-10-25', value: 25269995.00, color: BAR_DOWN_COLOR }, + { time: '2018-10-26', value: 24973311.00, color: BAR_DOWN_COLOR }, + { time: '2018-10-29', value: 22103692.00, color: BAR_UP_COLOR }, + { time: '2018-10-30', value: 25231199.00, color: BAR_UP_COLOR }, + { time: '2018-10-31', value: 24214427.00, color: BAR_DOWN_COLOR }, + { time: '2018-11-01', value: 22533201.00, color: BAR_DOWN_COLOR }, + { time: '2018-11-02', value: 14734412.00, color: BAR_UP_COLOR }, + { time: '2018-11-05', value: 12733842.00, color: BAR_UP_COLOR }, + { time: '2018-11-06', value: 12371207.00, color: BAR_UP_COLOR }, + { time: '2018-11-07', value: 14891287.00, color: BAR_UP_COLOR }, + { time: '2018-11-08', value: 12482392.00, color: BAR_UP_COLOR }, + { time: '2018-11-09', value: 17365762.00, color: BAR_UP_COLOR }, + { time: '2018-11-12', value: 13236769.00, color: BAR_UP_COLOR }, + { time: '2018-11-13', value: 13047907.00, color: BAR_DOWN_COLOR }, + { time: '2018-11-14', value: 18288710.00, color: BAR_UP_COLOR }, + { time: '2018-11-15', value: 17147123.00, color: BAR_UP_COLOR }, + { time: '2018-11-16', value: 19470986.00, color: BAR_UP_COLOR }, + { time: '2018-11-19', value: 18405731.00, color: BAR_UP_COLOR }, + { time: '2018-11-20', value: 22028957.00, color: BAR_DOWN_COLOR }, + { time: '2018-11-21', value: 18482233.00, color: BAR_DOWN_COLOR }, + { time: '2018-11-23', value: 7009050.00, color: BAR_DOWN_COLOR }, + { time: '2018-11-26', value: 12308876.00, color: BAR_UP_COLOR }, + { time: '2018-11-27', value: 14118867.00, color: BAR_UP_COLOR }, + { time: '2018-11-28', value: 18662989.00, color: BAR_DOWN_COLOR }, + { time: '2018-11-29', value: 14763658.00, color: BAR_DOWN_COLOR }, + { time: '2018-11-30', value: 31142818.00, color: BAR_UP_COLOR }, + { time: '2018-12-03', value: 27795428.00, color: BAR_DOWN_COLOR }, + { time: '2018-12-04', value: 21727411.00, color: BAR_DOWN_COLOR }, + { time: '2018-12-06', value: 26880429.00, color: BAR_DOWN_COLOR }, + { time: '2018-12-07', value: 16948126.00, color: BAR_DOWN_COLOR }, + { time: '2018-12-10', value: 16603356.00, color: BAR_UP_COLOR }, + { time: '2018-12-11', value: 14991438.00, color: BAR_UP_COLOR }, + { time: '2018-12-12', value: 18892182.00, color: BAR_DOWN_COLOR }, + { time: '2018-12-13', value: 15454706.00, color: BAR_DOWN_COLOR }, + { time: '2018-12-14', value: 13960870.00, color: BAR_DOWN_COLOR }, + { time: '2018-12-17', value: 18902523.00, color: BAR_DOWN_COLOR }, + { time: '2018-12-18', value: 18895777.00, color: BAR_DOWN_COLOR }, + { time: '2018-12-19', value: 20968473.00, color: BAR_UP_COLOR }, + { time: '2018-12-20', value: 26897008.00, color: BAR_DOWN_COLOR }, + { time: '2018-12-21', value: 55413082.00, color: BAR_DOWN_COLOR }, + { time: '2018-12-24', value: 15077207.00, color: BAR_DOWN_COLOR }, + { time: '2018-12-26', value: 17970539.00, color: BAR_UP_COLOR }, + { time: '2018-12-27', value: 17530977.00, color: BAR_UP_COLOR }, + { time: '2018-12-28', value: 14771641.00, color: BAR_UP_COLOR }, + { time: '2018-12-31', value: 15331758.00, color: BAR_UP_COLOR }, + { time: '2019-01-02', value: 13969691.00, color: BAR_DOWN_COLOR }, + { time: '2019-01-03', value: 19245411.00, color: BAR_UP_COLOR }, + { time: '2019-01-04', value: 17035848.00, color: BAR_UP_COLOR }, + { time: '2019-01-07', value: 16348982.00, color: BAR_UP_COLOR }, + { time: '2019-01-08', value: 21425008.00, color: BAR_UP_COLOR }, + { time: '2019-01-09', value: 18136000.00, color: BAR_DOWN_COLOR }, + { time: '2019-01-10', value: 14259910.00, color: BAR_UP_COLOR }, + { time: '2019-01-11', value: 15801548.00, color: BAR_UP_COLOR }, + { time: '2019-01-14', value: 11342293.00, color: BAR_UP_COLOR }, + { time: '2019-01-15', value: 10074386.00, color: BAR_UP_COLOR }, + { time: '2019-01-16', value: 13411691.00, color: BAR_DOWN_COLOR }, + { time: '2019-01-17', value: 15223854.00, color: BAR_DOWN_COLOR }, + { time: '2019-01-18', value: 16802516.00, color: BAR_UP_COLOR }, + { time: '2019-01-22', value: 18284771.00, color: BAR_DOWN_COLOR }, + { time: '2019-01-23', value: 15109007.00, color: BAR_UP_COLOR }, + { time: '2019-01-24', value: 12494109.00, color: BAR_DOWN_COLOR }, + { time: '2019-01-25', value: 17806822.00, color: BAR_DOWN_COLOR }, + { time: '2019-01-28', value: 25955718.00, color: BAR_DOWN_COLOR }, + { time: '2019-01-29', value: 33789235.00, color: BAR_DOWN_COLOR }, + { time: '2019-01-30', value: 27260036.00, color: BAR_UP_COLOR }, + { time: '2019-01-31', value: 28585447.00, color: BAR_UP_COLOR }, + { time: '2019-02-01', value: 13778392.00, color: BAR_DOWN_COLOR }, + { time: '2019-02-04', value: 15818901.00, color: BAR_DOWN_COLOR }, + { time: '2019-02-05', value: 14124794.00, color: BAR_UP_COLOR }, + { time: '2019-02-06', value: 11391442.00, color: BAR_DOWN_COLOR }, + { time: '2019-02-07', value: 12436168.00, color: BAR_DOWN_COLOR }, + { time: '2019-02-08', value: 12011657.00, color: BAR_UP_COLOR }, + { time: '2019-02-11', value: 9802798.00, color: BAR_UP_COLOR }, + { time: '2019-02-12', value: 11227550.00, color: BAR_UP_COLOR }, + { time: '2019-02-13', value: 11884803.00, color: BAR_UP_COLOR }, + { time: '2019-02-14', value: 11190094.00, color: BAR_DOWN_COLOR }, + { time: '2019-02-15', value: 15719416.00, color: BAR_UP_COLOR }, + { time: '2019-02-19', value: 12272877.00, color: BAR_UP_COLOR }, + { time: '2019-02-20', value: 11379006.00, color: BAR_UP_COLOR }, + { time: '2019-02-21', value: 14680547.00, color: BAR_UP_COLOR }, + { time: '2019-02-22', value: 12534431.00, color: BAR_UP_COLOR }, + { time: '2019-02-25', value: 15051182.00, color: BAR_DOWN_COLOR }, + { time: '2019-02-26', value: 12005571.00, color: BAR_DOWN_COLOR }, + { time: '2019-02-27', value: 8962776.00, color: BAR_UP_COLOR }, + { time: '2019-02-28', value: 15742971.00, color: BAR_UP_COLOR }, + { time: '2019-03-01', value: 10942737.00, color: BAR_UP_COLOR }, + { time: '2019-03-04', value: 13674737.00, color: BAR_DOWN_COLOR }, + { time: '2019-03-05', value: 15749545.00, color: BAR_DOWN_COLOR }, + { time: '2019-03-06', value: 13935530.00, color: BAR_DOWN_COLOR }, + { time: '2019-03-07', value: 12644171.00, color: BAR_UP_COLOR }, + { time: '2019-03-08', value: 10646710.00, color: BAR_UP_COLOR }, + { time: '2019-03-11', value: 13627431.00, color: BAR_UP_COLOR }, + { time: '2019-03-12', value: 12812980.00, color: BAR_DOWN_COLOR }, + { time: '2019-03-13', value: 14168350.00, color: BAR_UP_COLOR }, + { time: '2019-03-14', value: 12148349.00, color: BAR_UP_COLOR }, + { time: '2019-03-15', value: 23715337.00, color: BAR_UP_COLOR }, + { time: '2019-03-18', value: 12168133.00, color: BAR_DOWN_COLOR }, + { time: '2019-03-19', value: 13462686.00, color: BAR_DOWN_COLOR }, + { time: '2019-03-20', value: 11903104.00, color: BAR_UP_COLOR }, + { time: '2019-03-21', value: 10920129.00, color: BAR_UP_COLOR }, + { time: '2019-03-22', value: 25125385.00, color: BAR_UP_COLOR }, + { time: '2019-03-25', value: 15463411.00, color: BAR_UP_COLOR }, + { time: '2019-03-26', value: 12316901.00, color: BAR_UP_COLOR }, + { time: '2019-03-27', value: 13290298.00, color: BAR_UP_COLOR }, + { time: '2019-03-28', value: 20547060.00, color: BAR_DOWN_COLOR }, + { time: '2019-03-29', value: 17283871.00, color: BAR_UP_COLOR }, + { time: '2019-04-01', value: 16331140.00, color: BAR_DOWN_COLOR }, + { time: '2019-04-02', value: 11408146.00, color: BAR_DOWN_COLOR }, + { time: '2019-04-03', value: 15491724.00, color: BAR_UP_COLOR }, + { time: '2019-04-04', value: 8776028.00, color: BAR_UP_COLOR }, + { time: '2019-04-05', value: 11497780.00, color: BAR_UP_COLOR }, + { time: '2019-04-08', value: 11680538.00, color: BAR_UP_COLOR }, + { time: '2019-04-09', value: 10414416.00, color: BAR_DOWN_COLOR }, + { time: '2019-04-10', value: 8782061.00, color: BAR_UP_COLOR }, + { time: '2019-04-11', value: 9219930.00, color: BAR_DOWN_COLOR }, + { time: '2019-04-12', value: 10847504.00, color: BAR_UP_COLOR }, + { time: '2019-04-15', value: 7741472.00, color: BAR_DOWN_COLOR }, + { time: '2019-04-16', value: 10239261.00, color: BAR_UP_COLOR }, + { time: '2019-04-17', value: 15498037.00, color: BAR_DOWN_COLOR }, + { time: '2019-04-18', value: 13189013.00, color: BAR_UP_COLOR }, + { time: '2019-04-22', value: 11950365.00, color: BAR_UP_COLOR }, + { time: '2019-04-23', value: 23488682.00, color: BAR_DOWN_COLOR }, + { time: '2019-04-24', value: 13227084.00, color: BAR_DOWN_COLOR }, + { time: '2019-04-25', value: 17425466.00, color: BAR_DOWN_COLOR }, + { time: '2019-04-26', value: 16329727.00, color: BAR_UP_COLOR }, + { time: '2019-04-29', value: 13984965.00, color: BAR_UP_COLOR }, + { time: '2019-04-30', value: 15469002.00, color: BAR_UP_COLOR }, + { time: '2019-05-01', value: 11627436.00, color: BAR_DOWN_COLOR }, + { time: '2019-05-02', value: 14435436.00, color: BAR_UP_COLOR }, + { time: '2019-05-03', value: 9388228.00, color: BAR_UP_COLOR }, + { time: '2019-05-06', value: 10066145.00, color: BAR_DOWN_COLOR }, + { time: '2019-05-07', value: 12963827.00, color: BAR_DOWN_COLOR }, + { time: '2019-05-08', value: 12086743.00, color: BAR_DOWN_COLOR }, + { time: '2019-05-09', value: 14835326.00, color: BAR_UP_COLOR }, + { time: '2019-05-10', value: 10707335.00, color: BAR_UP_COLOR }, + { time: '2019-05-13', value: 13759350.00, color: BAR_DOWN_COLOR }, + { time: '2019-05-14', value: 12776175.00, color: BAR_DOWN_COLOR }, + { time: '2019-05-15', value: 10806379.00, color: BAR_UP_COLOR }, + { time: '2019-05-16', value: 11695064.00, color: BAR_UP_COLOR }, + { time: '2019-05-17', value: 14436662.00, color: BAR_UP_COLOR }, + { time: '2019-05-20', value: 20910590.00, color: BAR_UP_COLOR }, + { time: '2019-05-21', value: 14016315.00, color: BAR_UP_COLOR }, + { time: '2019-05-22', value: 11487448.00, color: BAR_DOWN_COLOR }, + { time: '2019-05-23', value: 11707083.00, color: BAR_DOWN_COLOR }, + { time: '2019-05-24', value: 8755506.00, color: BAR_UP_COLOR }, + { time: '2019-05-28', value: 3097125.00, color: BAR_UP_COLOR }, + // hide-end +]); + +chart.timeScale().fitContent(); diff --git a/website/tutorials/how_to/price-and-volume.mdx b/website/tutorials/how_to/price-and-volume.mdx new file mode 100644 index 0000000000..5428dd6201 --- /dev/null +++ b/website/tutorials/how_to/price-and-volume.mdx @@ -0,0 +1,93 @@ +--- +title: Price and volume on a single chart +sidebar_label: Price and Volume +description: An example of how to include both price and volume series on a single chart. +pagination_prev: null +pagination_next: null +keywords: + - example +--- + +This example shows how to include a volume study on your chart. + +## How to add a volume histogram + +An additional series can be added to a chart as an 'overlay' by setting the series' +[`priceScaleId`](/docs/api/interfaces/SeriesOptionsCommon#pricescaleid) to `''`. +An overlay doesn't make use of either the left or right price scale, and it's positioning +is controlled by setting the [`scaleMargins`](/docs/api/interfaces/PriceScaleOptions#scalemargins) +property on the series options. + +```js +const volumeSeries = chart.addHistogramSeries({ + priceFormat: { + type: 'volume', + }, + priceScaleId: '', // set as an overlay by setting a blank priceScaleId + // set the positioning of the volume series + scaleMargins: { + top: 0.7, // highest point of the series will be 70% away from the top + bottom: 0, + }, +}); +``` + +We are using the [Histogram](/docs/series-types#histogram) series type to draw the volume bars. +We can set the `priceFormat` option to `'volume'` to have the values display correctly within +the crosshair line label. + +We adjust the position of the overlay series to the bottom 30% of the chart by +setting the [`scaleMargins`](/docs/api/interfaces/PriceScaleOptions#scalemargins) properties as follows: + +```js +series.applyOptions({ + scaleMargins: { + top: 0.7, // highest point of the series will be 70% away from the top + bottom: 0, // lowest point will be at the very bottom. + }, +}); +``` + +Similarly, we can set the position of the main series using the same approach. By setting +the `bottom` margin value to `0.4` we can ensure that the two series don't overlap each other. + +```js +mainSeries.applyOptions({ + scaleMargins: { + top: 0.1, // highest point of the series will be 10% away from the top + bottom: 0.4, // lowest point will be 40% away from the bottom + }, +}); +``` + +We can control the color of the histogram bars by directly specifying color inside +the data set. + +```js +histogramSeries.setData([ + { time: '2018-10-19', value: 19103293.0, color: 'green' }, + { time: '2018-10-20', value: 20345000.0, color: 'red' }, +]); +``` + +You can see a full [working example](#full-example) below. + +## Resources + +- [OverlayPriceScale Options](/docs/api#overlaypricescaleoptions) +- [Histogram Series Type](/docs/series-types#histogram) +- [PriceFormat Types](/docs/api/interfaces/PriceFormatBuiltIn#type) +- [Scale Margins](/docs/api/interfaces/PriceScaleOptions#scalemargins) + +## Full example + +import UsageGuidePartial from "./_usage-guide-partial.mdx"; + + + +import CodeBlock from "@theme/CodeBlock"; +import code from "!!raw-loader!./price-and-volume.js"; + + + {code} + diff --git a/website/tutorials/how_to/price-line.js b/website/tutorials/how_to/price-line.js new file mode 100644 index 0000000000..0058aea743 --- /dev/null +++ b/website/tutorials/how_to/price-line.js @@ -0,0 +1,441 @@ +// remove-start +// Lightweight Charts Example: Price Lines +// https://tradingview.github.io/lightweight-charts/tutorials/how_to/price-line + +// remove-end +const chartOptions = { + layout: { + textColor: CHART_TEXT_COLOR, + background: { type: 'solid', color: CHART_BACKGROUND_COLOR }, + }, +}; +// remove-line +/** @type {import('lightweight-charts').IChartApi} */ +const chart = createChart(document.getElementById('container'), chartOptions); + +const series = chart.addLineSeries({ + color: LINE_LINE_COLOR, + lineWidth: 2, + // highlight-start + // disabling built-in price lines + lastValueVisible: false, + priceLineVisible: false, + // highlight-end +}); + +const data = [ + { time: { year: 2018, month: 1, day: 1 }, value: 27.58405298746434 }, + // hide-start + { time: { year: 2018, month: 1, day: 2 }, value: 31.74088841431117 }, + { time: { year: 2018, month: 1, day: 3 }, value: 35.892978753808926 }, + { time: { year: 2018, month: 1, day: 4 }, value: 39.63642029045179 }, + { time: { year: 2018, month: 1, day: 5 }, value: 40.79167357702531 }, + { time: { year: 2018, month: 1, day: 6 }, value: 47.691740220947764 }, + { time: { year: 2018, month: 1, day: 7 }, value: 49.377161099825415 }, + { time: { year: 2018, month: 1, day: 8 }, value: 52.47379203136591 }, + { time: { year: 2018, month: 1, day: 9 }, value: 50.40209743179448 }, + { time: { year: 2018, month: 1, day: 10 }, value: 61.47316837848548 }, + { time: { year: 2018, month: 1, day: 11 }, value: 58.22831552141069 }, + { time: { year: 2018, month: 1, day: 12 }, value: 59.36868132891698 }, + { time: { year: 2018, month: 1, day: 13 }, value: 62.10845687168416 }, + { time: { year: 2018, month: 1, day: 14 }, value: 51.259701958506724 }, + { time: { year: 2018, month: 1, day: 15 }, value: 56.247578870411644 }, + { time: { year: 2018, month: 1, day: 16 }, value: 55.483307642385164 }, + { time: { year: 2018, month: 1, day: 17 }, value: 55.85295564734231 }, + { time: { year: 2018, month: 1, day: 18 }, value: 48.3138216778343 }, + { time: { year: 2018, month: 1, day: 19 }, value: 53.071901176203866 }, + { time: { year: 2018, month: 1, day: 20 }, value: 50.873781097281885 }, + { time: { year: 2018, month: 1, day: 21 }, value: 49.7840315054249 }, + { time: { year: 2018, month: 1, day: 22 }, value: 52.34956807336156 }, + { time: { year: 2018, month: 1, day: 23 }, value: 53.79112543285674 }, + { time: { year: 2018, month: 1, day: 24 }, value: 53.984887985424805 }, + { time: { year: 2018, month: 1, day: 25 }, value: 58.56902893497121 }, + { time: { year: 2018, month: 1, day: 26 }, value: 54.76191372282466 }, + { time: { year: 2018, month: 1, day: 27 }, value: 63.38042554684846 }, + { time: { year: 2018, month: 1, day: 28 }, value: 55.452618512103065 }, + { time: { year: 2018, month: 1, day: 29 }, value: 65.60820758942769 }, + { time: { year: 2018, month: 1, day: 30 }, value: 56.82795136583009 }, + { time: { year: 2018, month: 1, day: 31 }, value: 70.3148022984224 }, + { time: { year: 2018, month: 2, day: 1 }, value: 65.86230944167264 }, + { time: { year: 2018, month: 2, day: 2 }, value: 72.05467846676524 }, + { time: { year: 2018, month: 2, day: 3 }, value: 72.99238887850564 }, + { time: { year: 2018, month: 2, day: 4 }, value: 67.03373730222785 }, + { time: { year: 2018, month: 2, day: 5 }, value: 69.97670934736414 }, + { time: { year: 2018, month: 2, day: 6 }, value: 73.08910595492105 }, + { time: { year: 2018, month: 2, day: 7 }, value: 81.43976528732057 }, + { time: { year: 2018, month: 2, day: 8 }, value: 73.62230936920984 }, + { time: { year: 2018, month: 2, day: 9 }, value: 82.15522801870938 }, + { time: { year: 2018, month: 2, day: 10 }, value: 77.99384538574678 }, + { time: { year: 2018, month: 2, day: 11 }, value: 85.62489628897463 }, + { time: { year: 2018, month: 2, day: 12 }, value: 86.93090666568217 }, + { time: { year: 2018, month: 2, day: 13 }, value: 75.99689788850394 }, + { time: { year: 2018, month: 2, day: 14 }, value: 88.46418548355727 }, + { time: { year: 2018, month: 2, day: 15 }, value: 86.20760396539865 }, + { time: { year: 2018, month: 2, day: 16 }, value: 81.88757639758437 }, + { time: { year: 2018, month: 2, day: 17 }, value: 79.58151786389108 }, + { time: { year: 2018, month: 2, day: 18 }, value: 80.96845249711073 }, + { time: { year: 2018, month: 2, day: 19 }, value: 73.54901807055447 }, + { time: { year: 2018, month: 2, day: 20 }, value: 75.65626118347262 }, + { time: { year: 2018, month: 2, day: 21 }, value: 78.41307347680399 }, + { time: { year: 2018, month: 2, day: 22 }, value: 74.60352602043042 }, + { time: { year: 2018, month: 2, day: 23 }, value: 72.28241570381236 }, + { time: { year: 2018, month: 2, day: 24 }, value: 72.24427397962566 }, + { time: { year: 2018, month: 2, day: 25 }, value: 64.80996965592134 }, + { time: { year: 2018, month: 2, day: 26 }, value: 67.37511361319652 }, + { time: { year: 2018, month: 2, day: 27 }, value: 65.5449131917524 }, + { time: { year: 2018, month: 2, day: 28 }, value: 65.4802711362433 }, + { time: { year: 2018, month: 3, day: 1 }, value: 62.207767815581086 }, + { time: { year: 2018, month: 3, day: 2 }, value: 59.78884720470812 }, + { time: { year: 2018, month: 3, day: 3 }, value: 67.51846586137782 }, + { time: { year: 2018, month: 3, day: 4 }, value: 68.752834400291 }, + { time: { year: 2018, month: 3, day: 5 }, value: 66.63416073573323 }, + { time: { year: 2018, month: 3, day: 6 }, value: 65.58601621691751 }, + { time: { year: 2018, month: 3, day: 7 }, value: 57.33498792621458 }, + { time: { year: 2018, month: 3, day: 8 }, value: 56.93436946311955 }, + { time: { year: 2018, month: 3, day: 9 }, value: 58.31144672902557 }, + { time: { year: 2018, month: 3, day: 10 }, value: 59.96407207657268 }, + { time: { year: 2018, month: 3, day: 11 }, value: 55.7861486424976 }, + { time: { year: 2018, month: 3, day: 12 }, value: 52.91803500214551 }, + { time: { year: 2018, month: 3, day: 13 }, value: 54.491591573038306 }, + { time: { year: 2018, month: 3, day: 14 }, value: 51.924409342593385 }, + { time: { year: 2018, month: 3, day: 15 }, value: 41.90263950118436 }, + { time: { year: 2018, month: 3, day: 16 }, value: 40.514436076485694 }, + { time: { year: 2018, month: 3, day: 17 }, value: 41.065887666854486 }, + { time: { year: 2018, month: 3, day: 18 }, value: 40.44445534031683 }, + { time: { year: 2018, month: 3, day: 19 }, value: 42.13922977216152 }, + { time: { year: 2018, month: 3, day: 20 }, value: 42.317162952084495 }, + { time: { year: 2018, month: 3, day: 21 }, value: 39.02881877743751 }, + { time: { year: 2018, month: 3, day: 22 }, value: 39.81917993955704 }, + { time: { year: 2018, month: 3, day: 23 }, value: 36.753197056053374 }, + { time: { year: 2018, month: 3, day: 24 }, value: 37.02203306330588 }, + { time: { year: 2018, month: 3, day: 25 }, value: 36.36014042161194 }, + { time: { year: 2018, month: 3, day: 26 }, value: 33.56275879100148 }, + { time: { year: 2018, month: 3, day: 27 }, value: 34.39112540787079 }, + { time: { year: 2018, month: 3, day: 28 }, value: 30.57170225544929 }, + { time: { year: 2018, month: 3, day: 29 }, value: 33.56826040802756 }, + { time: { year: 2018, month: 3, day: 30 }, value: 32.89895543218274 }, + { time: { year: 2018, month: 3, day: 31 }, value: 31.015658561825738 }, + { time: { year: 2018, month: 4, day: 1 }, value: 33.189179815787455 }, + { time: { year: 2018, month: 4, day: 2 }, value: 29.530756945582162 }, + { time: { year: 2018, month: 4, day: 3 }, value: 29.250978140719916 }, + { time: { year: 2018, month: 4, day: 4 }, value: 27.89635178919736 }, + { time: { year: 2018, month: 4, day: 5 }, value: 26.995427160624686 }, + { time: { year: 2018, month: 4, day: 6 }, value: 25.89631885043023 }, + { time: { year: 2018, month: 4, day: 7 }, value: 28.71812492475548 }, + { time: { year: 2018, month: 4, day: 8 }, value: 23.56476085365468 }, + { time: { year: 2018, month: 4, day: 9 }, value: 23.8550787876547 }, + { time: { year: 2018, month: 4, day: 10 }, value: 23.27046572075657 }, + { time: { year: 2018, month: 4, day: 11 }, value: 25.73099630369951 }, + { time: { year: 2018, month: 4, day: 12 }, value: 23.398760738394646 }, + { time: { year: 2018, month: 4, day: 13 }, value: 22.97970501784193 }, + { time: { year: 2018, month: 4, day: 14 }, value: 22.145587244500526 }, + { time: { year: 2018, month: 4, day: 15 }, value: 20.622578956226192 }, + { time: { year: 2018, month: 4, day: 16 }, value: 21.99297423796017 }, + { time: { year: 2018, month: 4, day: 17 }, value: 20.756081302371527 }, + { time: { year: 2018, month: 4, day: 18 }, value: 18.1983338834302 }, + { time: { year: 2018, month: 4, day: 19 }, value: 16.419404563645198 }, + { time: { year: 2018, month: 4, day: 20 }, value: 18.38192363882247 }, + { time: { year: 2018, month: 4, day: 21 }, value: 17.41452255210322 }, + { time: { year: 2018, month: 4, day: 22 }, value: 17.39866711593896 }, + { time: { year: 2018, month: 4, day: 23 }, value: 14.859371316920733 }, + { time: { year: 2018, month: 4, day: 24 }, value: 14.49488592297959 }, + { time: { year: 2018, month: 4, day: 25 }, value: 14.183858665721397 }, + { time: { year: 2018, month: 4, day: 26 }, value: 12.754187935636056 }, + { time: { year: 2018, month: 4, day: 27 }, value: 14.467536059608742 }, + { time: { year: 2018, month: 4, day: 28 }, value: 14.72962730689032 }, + { time: { year: 2018, month: 4, day: 29 }, value: 16.591675981296518 }, + { time: { year: 2018, month: 4, day: 30 }, value: 17.560385679284135 }, + { time: { year: 2018, month: 5, day: 1 }, value: 22.386250317504064 }, + { time: { year: 2018, month: 5, day: 2 }, value: 23.697029442697385 }, + { time: { year: 2018, month: 5, day: 3 }, value: 23.453148128592442 }, + { time: { year: 2018, month: 5, day: 4 }, value: 23.164700105036882 }, + { time: { year: 2018, month: 5, day: 5 }, value: 23.919034678006323 }, + { time: { year: 2018, month: 5, day: 6 }, value: 25.18353870528509 }, + { time: { year: 2018, month: 5, day: 7 }, value: 26.982824465076394 }, + { time: { year: 2018, month: 5, day: 8 }, value: 29.122512185000712 }, + { time: { year: 2018, month: 5, day: 9 }, value: 29.60160406576696 }, + { time: { year: 2018, month: 5, day: 10 }, value: 30.845749384528016 }, + { time: { year: 2018, month: 5, day: 11 }, value: 33.03296938636202 }, + { time: { year: 2018, month: 5, day: 12 }, value: 33.784707082446104 }, + { time: { year: 2018, month: 5, day: 13 }, value: 36.08545410406137 }, + { time: { year: 2018, month: 5, day: 14 }, value: 36.80597815439238 }, + { time: { year: 2018, month: 5, day: 15 }, value: 34.56062188644443 }, + { time: { year: 2018, month: 5, day: 16 }, value: 36.52666657165473 }, + { time: { year: 2018, month: 5, day: 17 }, value: 34.76705735297833 }, + { time: { year: 2018, month: 5, day: 18 }, value: 39.01598033743525 }, + { time: { year: 2018, month: 5, day: 19 }, value: 37.60979560604685 }, + { time: { year: 2018, month: 5, day: 20 }, value: 42.403895121650436 }, + { time: { year: 2018, month: 5, day: 21 }, value: 45.55998014298455 }, + { time: { year: 2018, month: 5, day: 22 }, value: 39.76704752577289 }, + { time: { year: 2018, month: 5, day: 23 }, value: 41.83196178430985 }, + { time: { year: 2018, month: 5, day: 24 }, value: 46.99399105885453 }, + { time: { year: 2018, month: 5, day: 25 }, value: 48.553566318637984 }, + { time: { year: 2018, month: 5, day: 26 }, value: 48.918166891087594 }, + { time: { year: 2018, month: 5, day: 27 }, value: 42.95391588314584 }, + { time: { year: 2018, month: 5, day: 28 }, value: 51.267649950057546 }, + { time: { year: 2018, month: 5, day: 29 }, value: 44.86104374986037 }, + { time: { year: 2018, month: 5, day: 30 }, value: 46.7183564731453 }, + { time: { year: 2018, month: 5, day: 31 }, value: 52.753001379260766 }, + { time: { year: 2018, month: 6, day: 1 }, value: 56.04835638442964 }, + { time: { year: 2018, month: 6, day: 2 }, value: 54.82119793390652 }, + { time: { year: 2018, month: 6, day: 3 }, value: 57.718938021257685 }, + { time: { year: 2018, month: 6, day: 4 }, value: 53.9914459224653 }, + { time: { year: 2018, month: 6, day: 5 }, value: 59.33050624063286 }, + { time: { year: 2018, month: 6, day: 6 }, value: 50.79074268713266 }, + { time: { year: 2018, month: 6, day: 7 }, value: 53.15657316197036 }, + { time: { year: 2018, month: 6, day: 8 }, value: 59.43675134021954 }, + { time: { year: 2018, month: 6, day: 9 }, value: 63.28437595760727 }, + { time: { year: 2018, month: 6, day: 10 }, value: 58.07099287435428 }, + { time: { year: 2018, month: 6, day: 11 }, value: 56.68728978119396 }, + { time: { year: 2018, month: 6, day: 12 }, value: 57.05079700873323 }, + { time: { year: 2018, month: 6, day: 13 }, value: 65.65087785161663 }, + { time: { year: 2018, month: 6, day: 14 }, value: 59.20877581396441 }, + { time: { year: 2018, month: 6, day: 15 }, value: 64.30200099280857 }, + { time: { year: 2018, month: 6, day: 16 }, value: 68.60774992100288 }, + { time: { year: 2018, month: 6, day: 17 }, value: 67.16628680993453 }, + { time: { year: 2018, month: 6, day: 18 }, value: 62.89644591741811 }, + { time: { year: 2018, month: 6, day: 19 }, value: 61.42888198118138 }, + { time: { year: 2018, month: 6, day: 20 }, value: 64.89813095653885 }, + { time: { year: 2018, month: 6, day: 21 }, value: 72.72701573552945 }, + { time: { year: 2018, month: 6, day: 22 }, value: 67.85006296129148 }, + { time: { year: 2018, month: 6, day: 23 }, value: 74.8567814889 }, + { time: { year: 2018, month: 6, day: 24 }, value: 66.24228046316296 }, + { time: { year: 2018, month: 6, day: 25 }, value: 68.09192660329269 }, + { time: { year: 2018, month: 6, day: 26 }, value: 75.30075953672075 }, + { time: { year: 2018, month: 6, day: 27 }, value: 68.7478054620306 }, + { time: { year: 2018, month: 6, day: 28 }, value: 76.2285023801364 }, + { time: { year: 2018, month: 6, day: 29 }, value: 82.945167109736 }, + { time: { year: 2018, month: 6, day: 30 }, value: 76.91811779365663 }, + { time: { year: 2018, month: 7, day: 1 }, value: 73.4904875247808 }, + { time: { year: 2018, month: 7, day: 2 }, value: 78.37882382739707 }, + { time: { year: 2018, month: 7, day: 3 }, value: 77.00224598364632 }, + { time: { year: 2018, month: 7, day: 4 }, value: 74.96345662377028 }, + { time: { year: 2018, month: 7, day: 5 }, value: 85.54303380001907 }, + { time: { year: 2018, month: 7, day: 6 }, value: 74.23916926437794 }, + { time: { year: 2018, month: 7, day: 7 }, value: 86.38109732705232 }, + { time: { year: 2018, month: 7, day: 8 }, value: 88.36203657839357 }, + { time: { year: 2018, month: 7, day: 9 }, value: 81.63303116061893 }, + { time: { year: 2018, month: 7, day: 10 }, value: 78.05188105610182 }, + { time: { year: 2018, month: 7, day: 11 }, value: 93.73294498110195 }, + { time: { year: 2018, month: 7, day: 12 }, value: 86.3226018109303 }, + { time: { year: 2018, month: 7, day: 13 }, value: 83.18571530136985 }, + { time: { year: 2018, month: 7, day: 14 }, value: 92.45097319531271 }, + { time: { year: 2018, month: 7, day: 15 }, value: 82.61971871486392 }, + { time: { year: 2018, month: 7, day: 16 }, value: 84.39935112744469 }, + { time: { year: 2018, month: 7, day: 17 }, value: 86.84420907417798 }, + { time: { year: 2018, month: 7, day: 18 }, value: 81.50766784637338 }, + { time: { year: 2018, month: 7, day: 19 }, value: 88.74841709228694 }, + { time: { year: 2018, month: 7, day: 20 }, value: 85.84190975050336 }, + { time: { year: 2018, month: 7, day: 21 }, value: 86.9594938103096 }, + { time: { year: 2018, month: 7, day: 22 }, value: 83.72572564120199 }, + { time: { year: 2018, month: 7, day: 23 }, value: 83.42754326770913 }, + { time: { year: 2018, month: 7, day: 24 }, value: 80.40755818165856 }, + { time: { year: 2018, month: 7, day: 25 }, value: 81.40515523172276 }, + { time: { year: 2018, month: 7, day: 26 }, value: 88.6671912474792 }, + { time: { year: 2018, month: 7, day: 27 }, value: 83.98197434091429 }, + { time: { year: 2018, month: 7, day: 28 }, value: 79.93747419607003 }, + { time: { year: 2018, month: 7, day: 29 }, value: 88.74666581089701 }, + { time: { year: 2018, month: 7, day: 30 }, value: 88.4887222568271 }, + { time: { year: 2018, month: 7, day: 31 }, value: 91.70282162754224 }, + { time: { year: 2018, month: 8, day: 1 }, value: 81.82327312829118 }, + { time: { year: 2018, month: 8, day: 2 }, value: 89.56625546634567 }, + { time: { year: 2018, month: 8, day: 3 }, value: 83.60742160062637 }, + { time: { year: 2018, month: 8, day: 4 }, value: 92.16271144958857 }, + { time: { year: 2018, month: 8, day: 5 }, value: 92.93451790057534 }, + { time: { year: 2018, month: 8, day: 6 }, value: 97.10629615852636 }, + { time: { year: 2018, month: 8, day: 7 }, value: 93.18989484413193 }, + { time: { year: 2018, month: 8, day: 8 }, value: 99.52744238602173 }, + { time: { year: 2018, month: 8, day: 9 }, value: 90.84973685659028 }, + { time: { year: 2018, month: 8, day: 10 }, value: 98.37517814040118 }, + { time: { year: 2018, month: 8, day: 11 }, value: 90.13525425607658 }, + { time: { year: 2018, month: 8, day: 12 }, value: 95.55125798221395 }, + { time: { year: 2018, month: 8, day: 13 }, value: 82.77346455876308 }, + { time: { year: 2018, month: 8, day: 14 }, value: 83.24854499361042 }, + { time: { year: 2018, month: 8, day: 15 }, value: 95.41999231944423 }, + { time: { year: 2018, month: 8, day: 16 }, value: 93.80228527345439 }, + { time: { year: 2018, month: 8, day: 17 }, value: 95.6453232668047 }, + { time: { year: 2018, month: 8, day: 18 }, value: 85.15427147925779 }, + { time: { year: 2018, month: 8, day: 19 }, value: 84.96447951638784 }, + { time: { year: 2018, month: 8, day: 20 }, value: 95.92739640648459 }, + { time: { year: 2018, month: 8, day: 21 }, value: 96.44143870862634 }, + { time: { year: 2018, month: 8, day: 22 }, value: 101.23323393804354 }, + { time: { year: 2018, month: 8, day: 23 }, value: 92.12092707164649 }, + { time: { year: 2018, month: 8, day: 24 }, value: 101.74117610271144 }, + { time: { year: 2018, month: 8, day: 25 }, value: 96.38311956379351 }, + { time: { year: 2018, month: 8, day: 26 }, value: 98.18485692799554 }, + { time: { year: 2018, month: 8, day: 27 }, value: 102.60080903938159 }, + { time: { year: 2018, month: 8, day: 28 }, value: 97.48394132428021 }, + { time: { year: 2018, month: 8, day: 29 }, value: 101.41501247525068 }, + { time: { year: 2018, month: 8, day: 30 }, value: 94.9501205245301 }, + { time: { year: 2018, month: 8, day: 31 }, value: 89.11429564465982 }, + { time: { year: 2018, month: 9, day: 1 }, value: 104.1842141132897 }, + { time: { year: 2018, month: 9, day: 2 }, value: 97.36185743859737 }, + { time: { year: 2018, month: 9, day: 3 }, value: 97.34376526297034 }, + { time: { year: 2018, month: 9, day: 4 }, value: 103.73609636859413 }, + { time: { year: 2018, month: 9, day: 5 }, value: 86.89420264148593 }, + { time: { year: 2018, month: 9, day: 6 }, value: 90.99445484839778 }, + { time: { year: 2018, month: 9, day: 7 }, value: 92.0197876339847 }, + { time: { year: 2018, month: 9, day: 8 }, value: 87.35412911434453 }, + { time: { year: 2018, month: 9, day: 9 }, value: 97.40224787139312 }, + { time: { year: 2018, month: 9, day: 10 }, value: 98.14732375673768 }, + { time: { year: 2018, month: 9, day: 11 }, value: 101.5147062059064 }, + { time: { year: 2018, month: 9, day: 12 }, value: 93.11950437404803 }, + { time: { year: 2018, month: 9, day: 13 }, value: 98.41765784798642 }, + { time: { year: 2018, month: 9, day: 14 }, value: 89.08499357950659 }, + { time: { year: 2018, month: 9, day: 15 }, value: 96.29213559344964 }, + { time: { year: 2018, month: 9, day: 16 }, value: 103.57528341068684 }, + { time: { year: 2018, month: 9, day: 17 }, value: 98.94258099235431 }, + { time: { year: 2018, month: 9, day: 18 }, value: 92.43383394007822 }, + { time: { year: 2018, month: 9, day: 19 }, value: 105.39681697822706 }, + { time: { year: 2018, month: 9, day: 20 }, value: 100.52663985092036 }, + { time: { year: 2018, month: 9, day: 21 }, value: 98.84754340440189 }, + { time: { year: 2018, month: 9, day: 22 }, value: 93.78502517034752 }, + { time: { year: 2018, month: 9, day: 23 }, value: 95.51435402626828 }, + { time: { year: 2018, month: 9, day: 24 }, value: 91.94633821567461 }, + { time: { year: 2018, month: 9, day: 25 }, value: 98.18484857755837 }, + { time: { year: 2018, month: 9, day: 26 }, value: 102.51694320185617 }, + { time: { year: 2018, month: 9, day: 27 }, value: 97.40549024974396 }, + { time: { year: 2018, month: 9, day: 28 }, value: 103.49718807374374 }, + { time: { year: 2018, month: 9, day: 29 }, value: 108.24441490057781 }, + { time: { year: 2018, month: 9, day: 30 }, value: 103.24675412744978 }, + { time: { year: 2018, month: 10, day: 1 }, value: 97.05089568637521 }, + { time: { year: 2018, month: 10, day: 2 }, value: 91.85875309835458 }, + { time: { year: 2018, month: 10, day: 3 }, value: 72.24590653541385 }, + { time: { year: 2018, month: 10, day: 4 }, value: 70.73707674373722 }, + { time: { year: 2018, month: 10, day: 5 }, value: 61.2106378263602 }, + { time: { year: 2018, month: 10, day: 6 }, value: 62.35889407826063 }, + { time: { year: 2018, month: 10, day: 7 }, value: 56.311930696659 }, + { time: { year: 2018, month: 10, day: 8 }, value: 51.462743547904374 }, + { time: { year: 2018, month: 10, day: 9 }, value: 47.99928302521288 }, + { time: { year: 2018, month: 10, day: 10 }, value: 42.735011616511976 }, + { time: { year: 2018, month: 10, day: 11 }, value: 35.82291867003256 }, + { time: { year: 2018, month: 10, day: 12 }, value: 28.706141122035454 }, + { time: { year: 2018, month: 10, day: 13 }, value: 24.289344698634036 }, + { time: { year: 2018, month: 10, day: 14 }, value: 22.56513000253429 }, + { time: { year: 2018, month: 10, day: 15 }, value: 18.862530899060324 }, + { time: { year: 2018, month: 10, day: 16 }, value: 18.164416367748263 }, + { time: { year: 2018, month: 10, day: 17 }, value: 16.25993836760582 }, + { time: { year: 2018, month: 10, day: 18 }, value: 15.849033589978859 }, + { time: { year: 2018, month: 10, day: 19 }, value: 12.581184324950792 }, + { time: { year: 2018, month: 10, day: 20 }, value: 12.46960767886934 }, + { time: { year: 2018, month: 10, day: 21 }, value: 13.203284995561251 }, + { time: { year: 2018, month: 10, day: 22 }, value: 12.751819244602629 }, + { time: { year: 2018, month: 10, day: 23 }, value: 13.815126496529205 }, + { time: { year: 2018, month: 10, day: 24 }, value: 14.44156419046133 }, + { time: { year: 2018, month: 10, day: 25 }, value: 12.030505290672643 }, + { time: { year: 2018, month: 10, day: 26 }, value: 13.426535837756518 }, + { time: { year: 2018, month: 10, day: 27 }, value: 13.141003741491893 }, + { time: { year: 2018, month: 10, day: 28 }, value: 12.216411608284261 }, + { time: { year: 2018, month: 10, day: 29 }, value: 12.437867813477077 }, + { time: { year: 2018, month: 10, day: 30 }, value: 12.228521667932771 }, + { time: { year: 2018, month: 10, day: 31 }, value: 13.587126038913974 }, + { time: { year: 2018, month: 11, day: 1 }, value: 12.016792589137491 }, + { time: { year: 2018, month: 11, day: 2 }, value: 13.01948020515645 }, + { time: { year: 2018, month: 11, day: 3 }, value: 12.70475528902004 }, + { time: { year: 2018, month: 11, day: 4 }, value: 13.018454073452016 }, + { time: { year: 2018, month: 11, day: 5 }, value: 12.505487262036313 }, + { time: { year: 2018, month: 11, day: 6 }, value: 13.467522897553604 }, + { time: { year: 2018, month: 11, day: 7 }, value: 13.748796553616549 }, + { time: { year: 2018, month: 11, day: 8 }, value: 11.974873751121669 }, + { time: { year: 2018, month: 11, day: 9 }, value: 11.8316362912944 }, + { time: { year: 2018, month: 11, day: 10 }, value: 13.864291857325023 }, + { time: { year: 2018, month: 11, day: 11 }, value: 12.071675684436165 }, + { time: { year: 2018, month: 11, day: 12 }, value: 12.314581956701367 }, + { time: { year: 2018, month: 11, day: 13 }, value: 13.223445358310986 }, + { time: { year: 2018, month: 11, day: 14 }, value: 12.346191421746546 }, + { time: { year: 2018, month: 11, day: 15 }, value: 12.0232072259563 }, + { time: { year: 2018, month: 11, day: 16 }, value: 13.367039701380252 }, + { time: { year: 2018, month: 11, day: 17 }, value: 12.232635114201212 }, + { time: { year: 2018, month: 11, day: 18 }, value: 13.348220671014012 }, + { time: { year: 2018, month: 11, day: 19 }, value: 13.508314659502604 }, + { time: { year: 2018, month: 11, day: 20 }, value: 12.630893498655155 }, + { time: { year: 2018, month: 11, day: 21 }, value: 12.632952849963768 }, + { time: { year: 2018, month: 11, day: 22 }, value: 11.645073051089117 }, + { time: { year: 2018, month: 11, day: 23 }, value: 13.845637677048611 }, + { time: { year: 2018, month: 11, day: 24 }, value: 12.567519871595463 }, + { time: { year: 2018, month: 11, day: 25 }, value: 13.927270152127294 }, + { time: { year: 2018, month: 11, day: 26 }, value: 12.179362670863737 }, + { time: { year: 2018, month: 11, day: 27 }, value: 12.471835488646303 }, + { time: { year: 2018, month: 11, day: 28 }, value: 11.71761488042106 }, + { time: { year: 2018, month: 11, day: 29 }, value: 12.181312973409113 }, + { time: { year: 2018, month: 11, day: 30 }, value: 12.662272671374286 }, + { time: { year: 2018, month: 12, day: 1 }, value: 13.859774226603497 }, + { time: { year: 2018, month: 12, day: 2 }, value: 11.643237368800426 }, + { time: { year: 2018, month: 12, day: 3 }, value: 11.646083130428282 }, + { time: { year: 2018, month: 12, day: 4 }, value: 13.3486102643562 }, + { time: { year: 2018, month: 12, day: 5 }, value: 13.421817450001853 }, + { time: { year: 2018, month: 12, day: 6 }, value: 13.734844672048157 }, + { time: { year: 2018, month: 12, day: 7 }, value: 11.808371821628475 }, + { time: { year: 2018, month: 12, day: 8 }, value: 13.906976670383472 }, + { time: { year: 2018, month: 12, day: 9 }, value: 13.161627469585584 }, + { time: { year: 2018, month: 12, day: 10 }, value: 13.642368164712535 }, + { time: { year: 2018, month: 12, day: 11 }, value: 12.755167209621261 }, + { time: { year: 2018, month: 12, day: 12 }, value: 12.13947468588139 }, + { time: { year: 2018, month: 12, day: 13 }, value: 13.68979129854326 }, + { time: { year: 2018, month: 12, day: 14 }, value: 11.812050924695251 }, + { time: { year: 2018, month: 12, day: 15 }, value: 11.71992287051065 }, + { time: { year: 2018, month: 12, day: 16 }, value: 13.003823991463284 }, + { time: { year: 2018, month: 12, day: 17 }, value: 13.124946877570311 }, + { time: { year: 2018, month: 12, day: 18 }, value: 11.844736927132542 }, + { time: { year: 2018, month: 12, day: 19 }, value: 11.770961792864846 }, + { time: { year: 2018, month: 12, day: 20 }, value: 10.926179837275598 }, + { time: { year: 2018, month: 12, day: 21 }, value: 11.155771630851676 }, + { time: { year: 2018, month: 12, day: 22 }, value: 11.63008791780728 }, + { time: { year: 2018, month: 12, day: 23 }, value: 10.216268005840389 }, + { time: { year: 2018, month: 12, day: 24 }, value: 13.611694182717626 }, + { time: { year: 2018, month: 12, day: 25 }, value: 17.47706580052263 }, + { time: { year: 2018, month: 12, day: 26 }, value: 20.900697260373697 }, + { time: { year: 2018, month: 12, day: 27 }, value: 20.44940301651778 }, + { time: { year: 2018, month: 12, day: 28 }, value: 23.47128311932538 }, + { time: { year: 2018, month: 12, day: 29 }, value: 28.63506505828928 }, + { time: { year: 2018, month: 12, day: 30 }, value: 29.567577074788517 }, + // hide-end +]; +series.setData(data); + +let minimumPrice = data[0].value; +let maximumPrice = minimumPrice; +for (let i = 1; i < data.length; i++) { + const price = data[i].value; + if (price > maximumPrice) { + maximumPrice = price; + } + if (price < minimumPrice) { + minimumPrice = price; + } +} +const avgPrice = (maximumPrice + minimumPrice) / 2; + +// highlight-start +const lineWidth = 2; +const minPriceLine = { + price: minimumPrice, + color: BAR_DOWN_COLOR, + lineWidth: lineWidth, + lineStyle: 2, // LineStyle.Dashed + axisLabelVisible: true, + title: 'min price', +}; +const avgPriceLine = { + price: avgPrice, + color: CHART_TEXT_COLOR, + lineWidth: lineWidth, + lineStyle: 1, // LineStyle.Dotted + axisLabelVisible: true, + title: 'ave price', +}; +const maxPriceLine = { + price: maximumPrice, + color: BAR_UP_COLOR, + lineWidth: lineWidth, + lineStyle: 2, // LineStyle.Dashed + axisLabelVisible: true, + title: 'max price', +}; + +series.createPriceLine(minPriceLine); +series.createPriceLine(avgPriceLine); +series.createPriceLine(maxPriceLine); +// highlight-end + +chart.timeScale().fitContent(); diff --git a/website/tutorials/how_to/price-line.mdx b/website/tutorials/how_to/price-line.mdx new file mode 100644 index 0000000000..61b303f14d --- /dev/null +++ b/website/tutorials/how_to/price-line.mdx @@ -0,0 +1,66 @@ +--- +title: Add Price Line +sidebar_label: Price Lines +description: An example of how to add price lines to a chart. +pagination_prev: null +pagination_next: null +keywords: + - price line +--- + +A price line is a horizontal line drawn across the width of the chart at a specific price value. +This example shows how to add price lines to your chart. + +## Short answer + +A price line can be added to a chart by using the +[`createPriceLine`](/docs/api/interfaces/ISeriesApi#createpriceline) method on an existing series +([ISeriesApi](/docs/api/interfaces/ISeriesApi)) instance. + +```js +const myPriceLine = { + price: 1234, + color: BAR_UP_COLOR, + lineWidth: lineWidth, + lineStyle: 2, // LineStyle.Dashed + axisLabelVisible: true, + title: 'my label', +}; + +series.createPriceLine(myPriceLine); +``` + +You can see a full [working example](#full-example) below. + +## Tips + +You may wish to disable the default price line drawn by Lightweight Charts +for the last value in the series (and it's label). You can do this by adjusting the +series options as follows: + +```js +series.applyOptions({ + lastValueVisible: false, + priceLineVisible: false, +}); +``` + +## Resources + +You can view the related APIs here: +- [createPriceLine](/docs/api/interfaces/ISeriesApi#createpriceline) - Method to create a new Price Line. +- [PriceLineOptions](/docs/api/interfaces/PriceLineOptions) - Price Line options. +- [LineStyle](/docs/api/enums/LineStyle) - Available line styles. + +## Full example + +import UsageGuidePartial from "./_usage-guide-partial.mdx"; + + + +import CodeBlock from "@theme/CodeBlock"; +import code from "!!raw-loader!./price-line.js"; + + + {code} + diff --git a/website/tutorials/how_to/series-markers.js b/website/tutorials/how_to/series-markers.js new file mode 100644 index 0000000000..f8910bdc9f --- /dev/null +++ b/website/tutorials/how_to/series-markers.js @@ -0,0 +1,775 @@ +// remove-start +// Lightweight Charts Example: Series Markers +// https://tradingview.github.io/lightweight-charts/tutorials/how_to/series-markers + +// remove-end +const chartOptions = { + layout: { + textColor: CHART_TEXT_COLOR, + background: { type: 'solid', color: CHART_BACKGROUND_COLOR }, + }, +}; +// remove-line +/** @type {import('lightweight-charts').IChartApi} */ +const chart = createChart(document.getElementById('container'), chartOptions); + +const series = chart.addCandlestickSeries({ + upColor: BAR_UP_COLOR, downColor: BAR_DOWN_COLOR, borderVisible: false, + wickUpColor: BAR_UP_COLOR, wickDownColor: BAR_DOWN_COLOR, +}); + +const data = [ + { + time: { year: 2018, month: 9, day: 22 }, + open: 29.630237296336794, + high: 35.36950035097501, + low: 26.21522501353531, + close: 30.734997177569916, + }, + // hide-start + { + time: { year: 2018, month: 9, day: 23 }, + open: 32.267626500691215, + high: 34.452661663723774, + low: 26.096868360824704, + close: 29.573918833457004, + }, + { + time: { year: 2018, month: 9, day: 24 }, + open: 27.33996760497746, + high: 35.8060364835531, + low: 27.33996760497746, + close: 33.06283432964511, + }, + { + time: { year: 2018, month: 9, day: 25 }, + open: 31.1654368745013, + high: 31.97284477478497, + low: 26.766743287285593, + close: 27.364979322283386, + }, + { + time: { year: 2018, month: 9, day: 26 }, + open: 29.5901452337888, + high: 32.147919593347474, + low: 27.53289219709677, + close: 29.202912415085272, + }, + { + time: { year: 2018, month: 9, day: 27 }, + open: 27.561741523265923, + high: 35.11649043301526, + low: 25.20035866163233, + close: 31.14520649627546, + }, + { + time: { year: 2018, month: 9, day: 28 }, + open: 31.925975006823798, + high: 31.925975006823798, + low: 28.998500720406675, + close: 29.87723790403876, + }, + { + time: { year: 2018, month: 9, day: 29 }, + open: 30.826956088992475, + high: 34.79463130873015, + low: 25.291546123273097, + close: 28.994812708315987, + }, + { + time: { year: 2018, month: 9, day: 30 }, + open: 31.202920145287838, + high: 33.19178819590413, + low: 23.94419012923956, + close: 31.47253745770869, + }, + { + time: { year: 2018, month: 10, day: 1 }, + open: 26.927794164758666, + high: 34.6744456778885, + low: 26.927794164758666, + close: 31.091122539737423, + }, + { + time: { year: 2018, month: 10, day: 2 }, + open: 26.452041173938298, + high: 34.527917622572154, + low: 26.452041173938298, + close: 27.65703395829094, + }, + { + time: { year: 2018, month: 10, day: 3 }, + open: 27.74629982387605, + high: 29.300441707649835, + low: 23.761300216231263, + close: 29.182874625005628, + }, + { + time: { year: 2018, month: 10, day: 4 }, + open: 30.41599722290526, + high: 31.942643078777103, + low: 27.09925359459428, + close: 30.918477883682872, + }, + { + time: { year: 2018, month: 10, day: 5 }, + open: 25.76549797105683, + high: 33.4650523853759, + low: 25.76549797105683, + close: 28.15984801386293, + }, + { + time: { year: 2018, month: 10, day: 6 }, + open: 27.543404135965382, + high: 30.7227783000902, + low: 25.749951838020884, + close: 29.150903848724184, + }, + { + time: { year: 2018, month: 10, day: 7 }, + open: 29.34759861812077, + high: 31.08503530472835, + low: 23.395022079647823, + close: 25.00923131079722, + }, + { + time: { year: 2018, month: 10, day: 8 }, + open: 27.00266154335036, + high: 29.51599687178633, + low: 23.46749249241176, + close: 28.702932483799707, + }, + { + time: { year: 2018, month: 10, day: 9 }, + open: 25.569958099853594, + high: 27.669071502065417, + low: 25.569958099853594, + close: 25.626920473922613, + }, + { + time: { year: 2018, month: 10, day: 10 }, + open: 24.886919828178304, + high: 27.167620185117006, + low: 23.71595991386752, + close: 23.71595991386752, + }, + { + time: { year: 2018, month: 10, day: 11 }, + open: 26.14124249813686, + high: 29.5638477987916, + low: 20.82341105699825, + close: 25.563138238511257, + }, + { + time: { year: 2018, month: 10, day: 12 }, + open: 22.26412127509447, + high: 27.637685003390743, + low: 20.838507431464958, + close: 22.450517792778047, + }, + { + time: { year: 2018, month: 10, day: 13 }, + open: 25.75099239090953, + high: 28.12000626118839, + low: 21.929748303510852, + close: 22.63015682488669, + }, + { + time: { year: 2018, month: 10, day: 14 }, + open: 25.428132591291497, + high: 25.999229490809693, + low: 22.266121337091555, + close: 23.51047528528147, + }, + { + time: { year: 2018, month: 10, day: 15 }, + open: 25.07416967939059, + high: 25.50535192500713, + low: 21.96666570325133, + close: 21.96666570325133, + }, + { + time: { year: 2018, month: 10, day: 16 }, + open: 24.957206161449307, + high: 26.679727314857256, + low: 20.196753994637245, + close: 21.523347810451863, + }, + { + time: { year: 2018, month: 10, day: 17 }, + open: 23.705184745772733, + high: 26.754094837621004, + low: 18.724184302695104, + close: 20.160857555541725, + }, + { + time: { year: 2018, month: 10, day: 18 }, + open: 21.95610851644136, + high: 22.914889536420105, + low: 19.567733140100472, + close: 22.914889536420105, + }, + { + time: { year: 2018, month: 10, day: 19 }, + open: 23.216357873687972, + high: 25.44815512734246, + low: 19.54787451276509, + close: 20.76851802225937, + }, + { + time: { year: 2018, month: 10, day: 20 }, + open: 19.6289025950405, + high: 24.290702755740412, + low: 19.041541929894358, + close: 22.48608548162324, + }, + { + time: { year: 2018, month: 10, day: 21 }, + open: 23.599000037544915, + high: 26.839019853462844, + low: 20.884129956680898, + close: 22.01878871761756, + }, + { + time: { year: 2018, month: 10, day: 22 }, + open: 24.618502768742008, + high: 28.00099352255492, + low: 23.061935629399088, + close: 23.061935629399088, + }, + { + time: { year: 2018, month: 10, day: 23 }, + open: 23.840701995876866, + high: 28.494382608429564, + low: 23.840701995876866, + close: 25.321841131665526, + }, + { + time: { year: 2018, month: 10, day: 24 }, + open: 27.764925733189372, + high: 31.05550601484776, + low: 22.810929726970702, + close: 30.02406259204889, + }, + { + time: { year: 2018, month: 10, day: 25 }, + open: 29.703149280184604, + high: 34.0185175501095, + low: 26.82967654698301, + close: 32.06834171351323, + }, + { + time: { year: 2018, month: 10, day: 26 }, + open: 29.0251492427822, + high: 36.89478162439007, + low: 28.3502671011196, + close: 32.822663125409356, + }, + { + time: { year: 2018, month: 10, day: 27 }, + open: 35.040777462643284, + high: 35.12524316379231, + low: 26.805156020579663, + close: 34.23626219571325, + }, + { + time: { year: 2018, month: 10, day: 28 }, + open: 31.21349419519032, + high: 35.73068910379853, + low: 31.064101813812698, + close: 34.75020857236565, + }, + { + time: { year: 2018, month: 10, day: 29 }, + open: 32.34914826794689, + high: 42.381605482695505, + low: 30.176750284055878, + close: 39.24138147444552, + }, + { + time: { year: 2018, month: 10, day: 30 }, + open: 38.84583808993371, + high: 41.75165839362154, + low: 33.37106955991806, + close: 35.93904098275507, + }, + { + time: { year: 2018, month: 10, day: 31 }, + open: 37.070183005323564, + high: 44.84460203857022, + low: 35.23671284121251, + close: 36.329972003600034, + }, + { + time: { year: 2018, month: 11, day: 1 }, + open: 43.31997309164893, + high: 48.43216497187469, + low: 38.30881963355285, + close: 41.554948540677586, + }, + { + time: { year: 2018, month: 11, day: 2 }, + open: 41.33946811092929, + high: 46.65347243834853, + low: 37.472215586661335, + close: 39.26832265482503, + }, + { + time: { year: 2018, month: 11, day: 3 }, + open: 44.76468593661226, + high: 44.76468593661226, + low: 40.039672147314235, + close: 43.42106786288436, + }, + { + time: { year: 2018, month: 11, day: 4 }, + open: 49.13160326887013, + high: 49.13160326887013, + low: 40.93648693038296, + close: 42.17817698294767, + }, + { + time: { year: 2018, month: 11, day: 5 }, + open: 50.46706012970579, + high: 54.38104598422352, + low: 38.159930155343616, + close: 47.5899156640143, + }, + { + time: { year: 2018, month: 11, day: 6 }, + open: 48.25899506613569, + high: 48.25899506613569, + low: 45.63208604138365, + close: 45.63208604138365, + }, + { + time: { year: 2018, month: 11, day: 7 }, + open: 52.45484210527629, + high: 57.55979771849961, + low: 45.23447676016779, + close: 46.01127464234881, + }, + { + time: { year: 2018, month: 11, day: 8 }, + open: 53.228216675179624, + high: 54.07804814570622, + low: 40.61161433961706, + close: 47.689867390699014, + }, + { + time: { year: 2018, month: 11, day: 9 }, + open: 46.193099316212816, + high: 56.190537353078824, + low: 45.01246323828753, + close: 49.14012661656766, + }, + { + time: { year: 2018, month: 11, day: 10 }, + open: 50.409245396927986, + high: 52.3082002787041, + low: 41.764144138886394, + close: 52.3082002787041, + }, + { + time: { year: 2018, month: 11, day: 11 }, + open: 48.58146178816203, + high: 52.653922195022126, + low: 47.34031788474959, + close: 47.34031788474959, + }, + { + time: { year: 2018, month: 11, day: 12 }, + open: 46.80040325283692, + high: 56.709349494076804, + low: 45.81605691554122, + close: 45.81605691554122, + }, + { + time: { year: 2018, month: 11, day: 13 }, + open: 46.042722425788355, + high: 58.476056411825695, + low: 46.042722425788355, + close: 51.2300776481609, + }, + { + time: { year: 2018, month: 11, day: 14 }, + open: 53.909068487588385, + high: 60.240990154306715, + low: 45.230741063278664, + close: 51.34529637385427, + }, + { + time: { year: 2018, month: 11, day: 15 }, + open: 53.739609857086606, + high: 53.739609857086606, + low: 44.38017019990068, + close: 47.595960698697894, + }, + { + time: { year: 2018, month: 11, day: 16 }, + open: 52.52688238296145, + high: 60.9220040817774, + low: 44.27700764117003, + close: 55.27309771985698, + }, + { + time: { year: 2018, month: 11, day: 17 }, + open: 54.46100795908005, + high: 57.57937841117058, + low: 49.50543170388487, + close: 49.50543170388487, + }, + { + time: { year: 2018, month: 11, day: 18 }, + open: 51.12284024600029, + high: 57.646718858433026, + low: 48.73280269653226, + close: 51.35457902694444, + }, + { + time: { year: 2018, month: 11, day: 19 }, + open: 53.536130807863266, + high: 53.536130807863266, + low: 51.29649965636722, + close: 52.99088526565045, + }, + { + time: { year: 2018, month: 11, day: 20 }, + open: 50.92761950009885, + high: 57.70671943558014, + low: 46.45030483558741, + close: 52.229112575743066, + }, + { + time: { year: 2018, month: 11, day: 21 }, + open: 49.30035068900293, + high: 58.67691694734525, + low: 44.63563165197862, + close: 58.67691694734525, + }, + { + time: { year: 2018, month: 11, day: 22 }, + open: 54.230476484061036, + high: 59.03831193868438, + low: 50.77849134047791, + close: 59.03831193868438, + }, + { + time: { year: 2018, month: 11, day: 23 }, + open: 57.282420985156854, + high: 60.4869735007396, + low: 44.14116488798797, + close: 57.93461310007337, + }, + { + time: { year: 2018, month: 11, day: 24 }, + open: 54.86833150125539, + high: 64.25102812467448, + low: 52.36616043331222, + close: 52.36616043331222, + }, + { + time: { year: 2018, month: 11, day: 25 }, + open: 51.689239380620386, + high: 64.29747922654688, + low: 50.71498529572432, + close: 60.518206306602394, + }, + { + time: { year: 2018, month: 11, day: 26 }, + open: 55.74863310659164, + high: 60.816819055612584, + low: 46.11238607935206, + close: 59.23044859881929, + }, + { + time: { year: 2018, month: 11, day: 27 }, + open: 52.57406222528308, + high: 64.2058753841427, + low: 48.163404012323305, + close: 60.593847809696896, + }, + { + time: { year: 2018, month: 11, day: 28 }, + open: 57.50710740029724, + high: 60.12123058977347, + low: 49.61839271711267, + close: 53.29152711098895, + }, + { + time: { year: 2018, month: 11, day: 29 }, + open: 57.33581828303538, + high: 58.92432332528284, + low: 53.27790061455899, + close: 57.02787118731709, + }, + { + time: { year: 2018, month: 11, day: 30 }, + open: 57.527445314328595, + high: 67.63249690962569, + low: 49.603261485289146, + close: 54.589123556483656, + }, + { + time: { year: 2018, month: 12, day: 1 }, + open: 59.98835793934424, + high: 65.51917884840141, + low: 52.32535994476165, + close: 62.127135611086565, + }, + { + time: { year: 2018, month: 12, day: 2 }, + open: 52.509550731662536, + high: 58.49971806419494, + low: 52.509550731662536, + close: 54.759948868082255, + }, + { + time: { year: 2018, month: 12, day: 3 }, + open: 58.08470541982317, + high: 62.74987556918568, + low: 47.85627992158991, + close: 58.690428071336406, + }, + { + time: { year: 2018, month: 12, day: 4 }, + open: 58.28482939034761, + high: 69.16675825892361, + low: 57.41588944088662, + close: 57.74515245619454, + }, + { + time: { year: 2018, month: 12, day: 5 }, + open: 60.004299871302464, + high: 65.82447121605708, + low: 53.13330527599658, + close: 57.64488004774012, + }, + { + time: { year: 2018, month: 12, day: 6 }, + open: 61.92746155137417, + high: 64.36944842979646, + low: 49.470442234694225, + close: 59.94404434023895, + }, + { + time: { year: 2018, month: 12, day: 7 }, + open: 63.72235832229121, + high: 66.33649390307095, + low: 49.91822946887207, + close: 63.56396375320479, + }, + { + time: { year: 2018, month: 12, day: 8 }, + open: 56.64594047326664, + high: 65.3730920902599, + low: 52.604389283975664, + close: 60.71684658387917, + }, + { + time: { year: 2018, month: 12, day: 9 }, + open: 58.89798885700999, + high: 68.04578543284373, + low: 58.89798885700999, + close: 63.36111469854223, + }, + { + time: { year: 2018, month: 12, day: 10 }, + open: 58.869685789579826, + high: 70.99828637845869, + low: 52.36901833289119, + close: 63.15473262144694, + }, + { + time: { year: 2018, month: 12, day: 11 }, + open: 57.61362492091653, + high: 66.41975632948531, + low: 50.827182111530895, + close: 61.770769489947064, + }, + { + time: { year: 2018, month: 12, day: 12 }, + open: 57.869332957269656, + high: 66.28374056429257, + low: 57.05028878520954, + close: 63.87762958979595, + }, + { + time: { year: 2018, month: 12, day: 13 }, + open: 68.14347595614306, + high: 73.46304446829079, + low: 50.83319311788897, + close: 66.9144140431443, + }, + { + time: { year: 2018, month: 12, day: 14 }, + open: 56.95907344942102, + high: 68.81432823196859, + low: 56.95907344942102, + close: 60.69722290026252, + }, + { + time: { year: 2018, month: 12, day: 15 }, + open: 69.14662166493828, + high: 69.14662166493828, + low: 58.59143795311565, + close: 66.25235616866007, + }, + { + time: { year: 2018, month: 12, day: 16 }, + open: 64.0373004661208, + high: 72.91321850066319, + low: 52.079104978168345, + close: 65.92678310822487, + }, + { + time: { year: 2018, month: 12, day: 17 }, + open: 68.81814300123497, + high: 69.51927964796873, + low: 62.70935477415118, + close: 65.64565364397754, + }, + { + time: { year: 2018, month: 12, day: 18 }, + open: 63.47554821643351, + high: 73.6284398311906, + low: 58.996882824636856, + close: 58.996882824636856, + }, + { + time: { year: 2018, month: 12, day: 19 }, + open: 69.97765183896102, + high: 69.97765183896102, + low: 58.73355952507237, + close: 58.73355952507237, + }, + { + time: { year: 2018, month: 12, day: 20 }, + open: 63.22638756186111, + high: 65.67137242291682, + low: 59.9542779777421, + close: 61.20003065016431, + }, + { + time: { year: 2018, month: 12, day: 21 }, + open: 59.690029086102506, + high: 78.08665559197297, + low: 54.862707942292275, + close: 70.58935191024504, + }, + { + time: { year: 2018, month: 12, day: 22 }, + open: 66.29092355620301, + high: 71.82667261213395, + low: 65.28001993201676, + close: 71.82667261213395, + }, + { + time: { year: 2018, month: 12, day: 23 }, + open: 60.92645998120027, + high: 74.21283998861118, + low: 57.331119016099116, + close: 60.36728842356329, + }, + { + time: { year: 2018, month: 12, day: 24 }, + open: 60.211957192084036, + high: 72.37883919241614, + low: 60.211957192084036, + close: 72.37883919241614, + }, + { + time: { year: 2018, month: 12, day: 25 }, + open: 64.80282266865653, + high: 71.00204457933133, + low: 54.58446926152339, + close: 69.9468262738086, + }, + { + time: { year: 2018, month: 12, day: 26 }, + open: 66.28091239894763, + high: 81.00843300529249, + low: 54.56212171317677, + close: 69.58528111643206, + }, + { + time: { year: 2018, month: 12, day: 27 }, + open: 66.38479296949795, + high: 79.97207476893692, + low: 59.738742243860464, + close: 73.77893045661807, + }, + { + time: { year: 2018, month: 12, day: 28 }, + open: 73.80105714462456, + high: 73.80105714462456, + low: 59.95172576316864, + close: 73.49823170047799, + }, + { + time: { year: 2018, month: 12, day: 29 }, + open: 75.65816205696441, + high: 75.65816205696441, + low: 63.710206287837266, + close: 63.710206287837266, + }, + { + time: { year: 2018, month: 12, day: 30 }, + open: 70.43199072631421, + high: 80.48229715762909, + low: 62.65542750589909, + close: 63.42588929424237, + }, + { + time: { year: 2018, month: 12, day: 31 }, + open: 74.18101512382138, + high: 79.0918171034821, + low: 57.80109358134577, + close: 72.91361896511863, + }, + // hide-end +]; +series.setData(data); + +// determining the dates for the 'buy' and 'sell' markers added below. +const datesForMarkers = [data[data.length - 39], data[data.length - 19]]; +let indexOfMinPrice = 0; +for (let i = 1; i < datesForMarkers.length; i++) { + if (datesForMarkers[i].high < datesForMarkers[indexOfMinPrice].high) { + indexOfMinPrice = i; + } +} + +// highlight-start +const markers = [ + { + time: data[data.length - 48].time, + position: 'aboveBar', + color: '#f68410', + shape: 'circle', + text: 'D', + }, +]; +for (let i = 0; i < datesForMarkers.length; i++) { + if (i !== indexOfMinPrice) { + markers.push({ + time: datesForMarkers[i].time, + position: 'aboveBar', + color: '#e91e63', + shape: 'arrowDown', + text: 'Sell @ ' + Math.floor(datesForMarkers[i].high + 2), + }); + } else { + markers.push({ + time: datesForMarkers[i].time, + position: 'belowBar', + color: '#2196F3', + shape: 'arrowUp', + text: 'Buy @ ' + Math.floor(datesForMarkers[i].low - 2), + }); + } +} +series.setMarkers(markers); +// highlight-end + +chart.timeScale().fitContent(); diff --git a/website/tutorials/how_to/series-markers.mdx b/website/tutorials/how_to/series-markers.mdx new file mode 100644 index 0000000000..96c3830aad --- /dev/null +++ b/website/tutorials/how_to/series-markers.mdx @@ -0,0 +1,64 @@ +--- +title: Add Series Markers +sidebar_label: Series Markers +description: An example of how to add markers to a series on the chart. +pagination_prev: null +pagination_next: null +keywords: + - series + - markers +--- + +A series marker is an annotation which can be drawn on the chart at a specific point. +It can be used to draw attention to specific events within the data set. +This example shows how to add series markers to your chart. + +## Short answer + +You can add markers to a series by passing an array of [`seriesMarker`](/docs/api/interfaces/SeriesMarker) +objects to the [`setMarkers`](/docs/api/interfaces/ISeriesApi#setmarkers) method on +an [`ISeriesApi`](/docs/api/interfaces/ISeriesApi) instance. + +```js +const markers = [ + { + time: { year: 2018, month: 12, day: 23 }, + position: 'aboveBar', + color: '#f68410', + shape: 'circle', + text: 'A', + }, +]; +series.setMarkers(markers); +``` + +You can see a full [working example](#full-example) below. + +## Further information + +A series marker is an annotation which can be attached to a specific data point within a series. +We don't need to specify a vertical price value but rather only the `time` property since the +marker will determine it's vertical position from the data points values (such as `high` and +`low` in the case of candlestick data) and the specified `position` property ([SeriesMarkerPosition](/docs/api#seriesmarkerposition)). + +## Resources + +You can view the related APIs here: +- [SeriesMarker](/docs/api/interfaces/SeriesMarker) - Series Marker interface. +- [SeriesMarkerPosition](/docs/api#seriesmarkerposition) - Positions that can be set for the marker. +- [SeriesMarkerShape](/docs/api#seriesmarkershape) - Shapes that can be set for the marker. +- [setMarkers](/docs/api/interfaces/ISeriesApi#setmarkers) - Method for adding markers to a series. +- [Time Types](/docs/api#time) - Different time formats available to use. + +## Full example + +import UsageGuidePartial from "./_usage-guide-partial.mdx"; + + + +import CodeBlock from "@theme/CodeBlock"; +import code from "!!raw-loader!./series-markers.js"; + + + {code} + diff --git a/website/tutorials/how_to/tooltip-floating.js b/website/tutorials/how_to/tooltip-floating.js new file mode 100644 index 0000000000..9156b6dd13 --- /dev/null +++ b/website/tutorials/how_to/tooltip-floating.js @@ -0,0 +1,275 @@ +// remove-start +// Lightweight Charts Example: Floating Tooltip +// https://tradingview.github.io/lightweight-charts/tutorials/how_to/tooltips + +// remove-end +const chartOptions = { + layout: { + textColor: CHART_TEXT_COLOR, + background: { type: 'solid', color: CHART_BACKGROUND_COLOR }, + }, +}; +// remove-line +/** @type {import('lightweight-charts').IChartApi} */ +const chart = createChart(document.getElementById('container'), chartOptions); + +chart.applyOptions({ + rightPriceScale: { + scaleMargins: { + top: 0.3, // leave some space for the legend + bottom: 0.25, + }, + }, + crosshair: { + // hide the horizontal crosshair line + horzLine: { + visible: false, + labelVisible: false, + }, + // hide the vertical crosshair label + vertLine: { + labelVisible: false, + }, + }, + // hide the grid lines + grid: { + vertLines: { + visible: false, + }, + horzLines: { + visible: false, + }, + }, +}); + +const series = chart.addAreaSeries({ + topColor: AREA_TOP_COLOR, + bottomColor: AREA_BOTTOM_COLOR, + lineColor: LINE_LINE_COLOR, + lineWidth: 2, + crossHairMarkerVisible: false, +}); + +series.setData([ + { time: '2018-10-19', value: 26.19 }, + // hide-start + { time: '2018-10-22', value: 25.87 }, + { time: '2018-10-23', value: 25.83 }, + { time: '2018-10-24', value: 25.78 }, + { time: '2018-10-25', value: 25.82 }, + { time: '2018-10-26', value: 25.81 }, + { time: '2018-10-29', value: 25.82 }, + { time: '2018-10-30', value: 25.71 }, + { time: '2018-10-31', value: 25.82 }, + { time: '2018-11-01', value: 25.72 }, + { time: '2018-11-02', value: 25.74 }, + { time: '2018-11-05', value: 25.81 }, + { time: '2018-11-06', value: 25.75 }, + { time: '2018-11-07', value: 25.73 }, + { time: '2018-11-08', value: 25.75 }, + { time: '2018-11-09', value: 25.75 }, + { time: '2018-11-12', value: 25.76 }, + { time: '2018-11-13', value: 25.8 }, + { time: '2018-11-14', value: 25.77 }, + { time: '2018-11-15', value: 25.75 }, + { time: '2018-11-16', value: 25.75 }, + { time: '2018-11-19', value: 25.75 }, + { time: '2018-11-20', value: 25.72 }, + { time: '2018-11-21', value: 25.78 }, + { time: '2018-11-23', value: 25.72 }, + { time: '2018-11-26', value: 25.78 }, + { time: '2018-11-27', value: 25.85 }, + { time: '2018-11-28', value: 25.85 }, + { time: '2018-11-29', value: 25.55 }, + { time: '2018-11-30', value: 25.41 }, + { time: '2018-12-03', value: 25.41 }, + { time: '2018-12-04', value: 25.42 }, + { time: '2018-12-06', value: 25.33 }, + { time: '2018-12-07', value: 25.39 }, + { time: '2018-12-10', value: 25.32 }, + { time: '2018-12-11', value: 25.48 }, + { time: '2018-12-12', value: 25.39 }, + { time: '2018-12-13', value: 25.45 }, + { time: '2018-12-14', value: 25.52 }, + { time: '2018-12-17', value: 25.38 }, + { time: '2018-12-18', value: 25.36 }, + { time: '2018-12-19', value: 25.65 }, + { time: '2018-12-20', value: 25.7 }, + { time: '2018-12-21', value: 25.66 }, + { time: '2018-12-24', value: 25.66 }, + { time: '2018-12-26', value: 25.65 }, + { time: '2018-12-27', value: 25.66 }, + { time: '2018-12-28', value: 25.68 }, + { time: '2018-12-31', value: 25.77 }, + { time: '2019-01-02', value: 25.72 }, + { time: '2019-01-03', value: 25.69 }, + { time: '2019-01-04', value: 25.71 }, + { time: '2019-01-07', value: 25.72 }, + { time: '2019-01-08', value: 25.72 }, + { time: '2019-01-09', value: 25.66 }, + { time: '2019-01-10', value: 25.85 }, + { time: '2019-01-11', value: 25.92 }, + { time: '2019-01-14', value: 25.94 }, + { time: '2019-01-15', value: 25.95 }, + { time: '2019-01-16', value: 26.0 }, + { time: '2019-01-17', value: 25.99 }, + { time: '2019-01-18', value: 25.6 }, + { time: '2019-01-22', value: 25.81 }, + { time: '2019-01-23', value: 25.7 }, + { time: '2019-01-24', value: 25.74 }, + { time: '2019-01-25', value: 25.8 }, + { time: '2019-01-28', value: 25.83 }, + { time: '2019-01-29', value: 25.7 }, + { time: '2019-01-30', value: 25.78 }, + { time: '2019-01-31', value: 25.35 }, + { time: '2019-02-01', value: 25.6 }, + { time: '2019-02-04', value: 25.65 }, + { time: '2019-02-05', value: 25.73 }, + { time: '2019-02-06', value: 25.71 }, + { time: '2019-02-07', value: 25.71 }, + { time: '2019-02-08', value: 25.72 }, + { time: '2019-02-11', value: 25.76 }, + { time: '2019-02-12', value: 25.84 }, + { time: '2019-02-13', value: 25.85 }, + { time: '2019-02-14', value: 25.87 }, + { time: '2019-02-15', value: 25.89 }, + { time: '2019-02-19', value: 25.9 }, + { time: '2019-02-20', value: 25.92 }, + { time: '2019-02-21', value: 25.96 }, + { time: '2019-02-22', value: 26.0 }, + { time: '2019-02-25', value: 25.93 }, + { time: '2019-02-26', value: 25.92 }, + { time: '2019-02-27', value: 25.67 }, + { time: '2019-02-28', value: 25.79 }, + { time: '2019-03-01', value: 25.86 }, + { time: '2019-03-04', value: 25.94 }, + { time: '2019-03-05', value: 26.02 }, + { time: '2019-03-06', value: 25.95 }, + { time: '2019-03-07', value: 25.89 }, + { time: '2019-03-08', value: 25.94 }, + { time: '2019-03-11', value: 25.91 }, + { time: '2019-03-12', value: 25.92 }, + { time: '2019-03-13', value: 26.0 }, + { time: '2019-03-14', value: 26.05 }, + { time: '2019-03-15', value: 26.11 }, + { time: '2019-03-18', value: 26.1 }, + { time: '2019-03-19', value: 25.98 }, + { time: '2019-03-20', value: 26.11 }, + { time: '2019-03-21', value: 26.12 }, + { time: '2019-03-22', value: 25.88 }, + { time: '2019-03-25', value: 25.85 }, + { time: '2019-03-26', value: 25.72 }, + { time: '2019-03-27', value: 25.73 }, + { time: '2019-03-28', value: 25.8 }, + { time: '2019-03-29', value: 25.77 }, + { time: '2019-04-01', value: 26.06 }, + { time: '2019-04-02', value: 25.93 }, + { time: '2019-04-03', value: 25.95 }, + { time: '2019-04-04', value: 26.06 }, + { time: '2019-04-05', value: 26.16 }, + { time: '2019-04-08', value: 26.12 }, + { time: '2019-04-09', value: 26.07 }, + { time: '2019-04-10', value: 26.13 }, + { time: '2019-04-11', value: 26.04 }, + { time: '2019-04-12', value: 26.04 }, + { time: '2019-04-15', value: 26.05 }, + { time: '2019-04-16', value: 26.01 }, + { time: '2019-04-17', value: 26.09 }, + { time: '2019-04-18', value: 26.0 }, + { time: '2019-04-22', value: 26.0 }, + { time: '2019-04-23', value: 26.06 }, + { time: '2019-04-24', value: 26.0 }, + { time: '2019-04-25', value: 25.81 }, + { time: '2019-04-26', value: 25.88 }, + { time: '2019-04-29', value: 25.91 }, + { time: '2019-04-30', value: 25.9 }, + { time: '2019-05-01', value: 26.02 }, + { time: '2019-05-02', value: 25.97 }, + { time: '2019-05-03', value: 26.02 }, + { time: '2019-05-06', value: 26.03 }, + { time: '2019-05-07', value: 26.04 }, + { time: '2019-05-08', value: 26.05 }, + { time: '2019-05-09', value: 26.05 }, + { time: '2019-05-10', value: 26.08 }, + { time: '2019-05-13', value: 26.05 }, + { time: '2019-05-14', value: 26.01 }, + { time: '2019-05-15', value: 26.03 }, + { time: '2019-05-16', value: 26.14 }, + { time: '2019-05-17', value: 26.09 }, + { time: '2019-05-20', value: 26.01 }, + { time: '2019-05-21', value: 26.12 }, + { time: '2019-05-22', value: 26.15 }, + { time: '2019-05-23', value: 26.18 }, + { time: '2019-05-24', value: 26.16 }, + { time: '2019-05-28', value: 26.23 }, + // hide-end +]); + +// const symbolName = 'ETC USD 7D VWAP'; + +const container = document.getElementById('container'); + +function dateToString(date) { + return `${date.year} - ${date.month} - ${date.day}`; +} + +const toolTipWidth = 80; +const toolTipHeight = 80; +const toolTipMargin = 15; + +// Create and style the tooltip html element +const toolTip = document.createElement('div'); +toolTip.style = `width: 96px; height: 80px; position: absolute; display: none; padding: 8px; box-sizing: border-box; font-size: 12px; text-align: left; z-index: 1000; top: 12px; left: 12px; pointer-events: none; border: 1px solid; border-radius: 2px;font-family: 'Trebuchet MS', Roboto, Ubuntu, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;`; +toolTip.style.background = CHART_BACKGROUND_COLOR; +toolTip.style.color = CHART_TEXT_COLOR; +toolTip.style.borderColor = LINE_LINE_COLOR; +container.appendChild(toolTip); + +// update tooltip +chart.subscribeCrosshairMove(param => { + if ( + param.point === undefined || + !param.time || + param.point.x < 0 || + param.point.x > container.clientWidth || + param.point.y < 0 || + param.point.y > container.clientHeight + ) { + toolTip.style.display = 'none'; + } else { + const dateStr = dateToString(param.time); + toolTip.style.display = 'block'; + const price = param.seriesPrices.get(series); + toolTip.innerHTML = `
Apple Inc.
+ ${Math.round(100 * price) / 100} +
+ ${dateStr} +
`; + + // highlight-start + const coordinate = series.priceToCoordinate(price); + let shiftedCoordinate = param.point.x - 50; + if (coordinate === null) { + return; + } + shiftedCoordinate = Math.max( + 0, + Math.min(container.clientWidth - toolTipWidth, shiftedCoordinate) + ); + const coordinateY = + coordinate - toolTipHeight - toolTipMargin > 0 + ? coordinate - toolTipHeight - toolTipMargin + : Math.max( + 0, + Math.min( + container.clientHeight - toolTipHeight - toolTipMargin, + coordinate + toolTipMargin + ) + ); + toolTip.style.left = shiftedCoordinate + 'px'; + toolTip.style.top = coordinateY + 'px'; + // highlight-end + } +}); + +chart.timeScale().fitContent(); diff --git a/website/tutorials/how_to/tooltip-magnifier.js b/website/tutorials/how_to/tooltip-magnifier.js new file mode 100644 index 0000000000..25c2e5ecd1 --- /dev/null +++ b/website/tutorials/how_to/tooltip-magnifier.js @@ -0,0 +1,426 @@ +// remove-start +// Lightweight Charts Example: Magnifier Tooltip +// https://tradingview.github.io/lightweight-charts/tutorials/how_to/tooltips + +// remove-end +const chartOptions = { + layout: { + textColor: CHART_TEXT_COLOR, + background: { type: 'solid', color: CHART_BACKGROUND_COLOR }, + }, +}; +// remove-line +/** @type {import('lightweight-charts').IChartApi} */ +const chart = createChart(document.getElementById('container'), chartOptions); + +chart.applyOptions({ + leftPriceScale: { + scaleMargins: { + top: 0.3, // leave some space for the legend + bottom: 0.25, + }, + visible: true, + borderVisible: false, + }, + rightPriceScale: { + visible: false, + }, + timeScale: { + borderVisible: false, + }, + crosshair: { + horzLine: { + visible: false, + labelVisible: false, + }, + vertLine: { + visible: true, + style: 0, + width: 2, + color: 'rgba(32, 38, 46, 0.1)', + labelVisible: false, + }, + }, + // hide the grid lines + grid: { + vertLines: { + visible: false, + }, + horzLines: { + visible: false, + }, + }, +}); + +const series = chart.addAreaSeries({ + topColor: BASELINE_BOTTOM_FILL_COLOR1, + bottomColor: BASELINE_BOTTOM_FILL_COLOR2, + lineColor: BASELINE_BOTTOM_LINE_COLOR, + lineWidth: 2, + crossHairMarkerVisible: false, + priceLineVisible: false, + lastValueVisible: false, +}); + +series.setData([ + { time: '2018-03-28', value: 154 }, + // hide-start + { time: '2018-03-29', value: 154.2 }, + { time: '2018-03-30', value: 155.60001 }, + { time: '2018-04-02', value: 156.25 }, + { time: '2018-04-03', value: 156.25 }, + { time: '2018-04-04', value: 156.05 }, + { time: '2018-04-05', value: 158.05 }, + { time: '2018-04-06', value: 157.3 }, + { time: '2018-04-09', value: 144 }, + { time: '2018-04-10', value: 144.8 }, + { time: '2018-04-11', value: 143.5 }, + { time: '2018-04-12', value: 147.05 }, + { time: '2018-04-13', value: 144.85001 }, + { time: '2018-04-16', value: 145.39999 }, + { time: '2018-04-17', value: 147.3 }, + { time: '2018-04-18', value: 153.55 }, + { time: '2018-04-19', value: 150.95 }, + { time: '2018-04-20', value: 149.39999 }, + { time: '2018-04-23', value: 148.5 }, + { time: '2018-04-24', value: 146.60001 }, + { time: '2018-04-25', value: 144.45 }, + { time: '2018-04-26', value: 145.60001 }, + { time: '2018-04-27', value: 144.3 }, + { time: '2018-04-30', value: 144 }, + { time: '2018-05-02', value: 147.3 }, + { time: '2018-05-03', value: 144.2 }, + { time: '2018-05-04', value: 141.64999 }, + { time: '2018-05-07', value: 141.89999 }, + { time: '2018-05-08', value: 140.39999 }, + { time: '2018-05-10', value: 139.8 }, + { time: '2018-05-11', value: 137.5 }, + { time: '2018-05-14', value: 136.14999 }, + { time: '2018-05-15', value: 138 }, + { time: '2018-05-16', value: 137.95 }, + { time: '2018-05-17', value: 137.95 }, + { time: '2018-05-18', value: 136.75 }, + { time: '2018-05-21', value: 136.2 }, + { time: '2018-05-22', value: 135 }, + { time: '2018-05-23', value: 132.55 }, + { time: '2018-05-24', value: 132.7 }, + { time: '2018-05-25', value: 132.2 }, + { time: '2018-05-28', value: 131.55 }, + { time: '2018-05-29', value: 131.85001 }, + { time: '2018-05-30', value: 139.85001 }, + { time: '2018-05-31', value: 140.8 }, + { time: '2018-06-01', value: 139.64999 }, + { time: '2018-06-04', value: 137.10001 }, + { time: '2018-06-05', value: 139.25 }, + { time: '2018-06-06', value: 141.3 }, + { time: '2018-06-07', value: 145 }, + { time: '2018-06-08', value: 143.89999 }, + { time: '2018-06-11', value: 142.7 }, + { time: '2018-06-13', value: 144.05 }, + { time: '2018-06-14', value: 143.55 }, + { time: '2018-06-15', value: 140.5 }, + { time: '2018-06-18', value: 139.64999 }, + { time: '2018-06-19', value: 138 }, + { time: '2018-06-20', value: 141 }, + { time: '2018-06-21', value: 140.55 }, + { time: '2018-06-22', value: 140.55 }, + { time: '2018-06-25', value: 140.75 }, + { time: '2018-06-26', value: 140.64999 }, + { time: '2018-06-27', value: 141.14999 }, + { time: '2018-06-28', value: 139.8 }, + { time: '2018-06-29', value: 139.8 }, + { time: '2018-07-02', value: 140.14999 }, + { time: '2018-07-03', value: 143.05 }, + { time: '2018-07-04', value: 140 }, + { time: '2018-07-05', value: 129.2 }, + { time: '2018-07-06', value: 129.55 }, + { time: '2018-07-09', value: 128.3 }, + { time: '2018-07-10', value: 126.55 }, + { time: '2018-07-11', value: 124.3 }, + { time: '2018-07-12', value: 124.8 }, + { time: '2018-07-13', value: 123.25 }, + { time: '2018-07-16', value: 123 }, + { time: '2018-07-17', value: 124.25 }, + { time: '2018-07-18', value: 123 }, + { time: '2018-07-19', value: 121 }, + { time: '2018-07-20', value: 121.45 }, + { time: '2018-07-23', value: 123.8 }, + { time: '2018-07-24', value: 122.15 }, + { time: '2018-07-25', value: 121.3 }, + { time: '2018-07-26', value: 122.7 }, + { time: '2018-07-27', value: 122.2 }, + { time: '2018-07-30', value: 121.7 }, + { time: '2018-07-31', value: 122.95 }, + { time: '2018-08-01', value: 121 }, + { time: '2018-08-02', value: 116 }, + { time: '2018-08-03', value: 118.1 }, + { time: '2018-08-06', value: 114.3 }, + { time: '2018-08-07', value: 114.25 }, + { time: '2018-08-08', value: 111.85 }, + { time: '2018-08-09', value: 109.7 }, + { time: '2018-08-10', value: 105 }, + { time: '2018-08-13', value: 105.75 }, + { time: '2018-08-14', value: 107.25 }, + { time: '2018-08-15', value: 107.4 }, + { time: '2018-08-16', value: 110.5 }, + { time: '2018-08-17', value: 109 }, + { time: '2018-08-20', value: 108.95 }, + { time: '2018-08-21', value: 108.35 }, + { time: '2018-08-22', value: 108.6 }, + { time: '2018-08-23', value: 105.65 }, + { time: '2018-08-24', value: 104.45 }, + { time: '2018-08-27', value: 106.2 }, + { time: '2018-08-28', value: 106.55 }, + { time: '2018-08-29', value: 111.8 }, + { time: '2018-08-30', value: 115.5 }, + { time: '2018-08-31', value: 115.5 }, + { time: '2018-09-03', value: 114.55 }, + { time: '2018-09-04', value: 112.75 }, + { time: '2018-09-05', value: 111.5 }, + { time: '2018-09-06', value: 108.1 }, + { time: '2018-09-07', value: 108.55 }, + { time: '2018-09-10', value: 106.5 }, + { time: '2018-09-11', value: 105.1 }, + { time: '2018-09-12', value: 107.2 }, + { time: '2018-09-13', value: 107.1 }, + { time: '2018-09-14', value: 105.75 }, + { time: '2018-09-17', value: 106.05 }, + { time: '2018-09-18', value: 109.15 }, + { time: '2018-09-19', value: 111.4 }, + { time: '2018-09-20', value: 111 }, + { time: '2018-09-21', value: 111 }, + { time: '2018-09-24', value: 108.95 }, + { time: '2018-09-25', value: 106.65 }, + { time: '2018-09-26', value: 106.7 }, + { time: '2018-09-27', value: 107.05 }, + { time: '2018-09-28', value: 106.55 }, + { time: '2018-10-01', value: 106.2 }, + { time: '2018-10-02', value: 106.4 }, + { time: '2018-10-03', value: 107.1 }, + { time: '2018-10-04', value: 106.4 }, + { time: '2018-10-05', value: 104.65 }, + { time: '2018-10-08', value: 102.65 }, + { time: '2018-10-09', value: 102.1 }, + { time: '2018-10-10', value: 102.15 }, + { time: '2018-10-11', value: 100.9 }, + { time: '2018-10-12', value: 102 }, + { time: '2018-10-15', value: 100.15 }, + { time: '2018-10-16', value: 99 }, + { time: '2018-10-17', value: 98 }, + { time: '2018-10-18', value: 96 }, + { time: '2018-10-19', value: 95.5 }, + { time: '2018-10-22', value: 92.400002 }, + { time: '2018-10-23', value: 92.300003 }, + { time: '2018-10-24', value: 92.900002 }, + { time: '2018-10-25', value: 91.849998 }, + { time: '2018-10-26', value: 91.599998 }, + { time: '2018-10-29', value: 94.050003 }, + { time: '2018-10-30', value: 98.25 }, + { time: '2018-10-31', value: 97.25 }, + { time: '2018-11-01', value: 96.879997 }, + { time: '2018-11-02', value: 101.78 }, + { time: '2018-11-06', value: 102.4 }, + { time: '2018-11-07', value: 100.6 }, + { time: '2018-11-08', value: 98.5 }, + { time: '2018-11-09', value: 95.139999 }, + { time: '2018-11-12', value: 96.900002 }, + { time: '2018-11-13', value: 97.400002 }, + { time: '2018-11-14', value: 103.3 }, + { time: '2018-11-15', value: 102.74 }, + { time: '2018-11-16', value: 101.2 }, + { time: '2018-11-19', value: 98.720001 }, + { time: '2018-11-20', value: 102.2 }, + { time: '2018-11-21', value: 108.8 }, + { time: '2018-11-22', value: 109.9 }, + { time: '2018-11-23', value: 113.74 }, + { time: '2018-11-26', value: 110.9 }, + { time: '2018-11-27', value: 113.28 }, + { time: '2018-11-28', value: 113.6 }, + { time: '2018-11-29', value: 113.14 }, + { time: '2018-11-30', value: 114.4 }, + { time: '2018-12-03', value: 111.84 }, + { time: '2018-12-04', value: 106.7 }, + { time: '2018-12-05', value: 107.8 }, + { time: '2018-12-06', value: 106.44 }, + { time: '2018-12-07', value: 103.7 }, + { time: '2018-12-10', value: 101.02 }, + { time: '2018-12-11', value: 102.72 }, + { time: '2018-12-12', value: 101.8 }, + { time: '2018-12-13', value: 102 }, + { time: '2018-12-14', value: 102.1 }, + { time: '2018-12-17', value: 101.04 }, + { time: '2018-12-18', value: 101.6 }, + { time: '2018-12-19', value: 102 }, + { time: '2018-12-20', value: 102.02 }, + { time: '2018-12-21', value: 102.2 }, + { time: '2018-12-24', value: 102.1 }, + { time: '2018-12-25', value: 100.8 }, + { time: '2018-12-26', value: 101.02 }, + { time: '2018-12-27', value: 100.5 }, + { time: '2018-12-28', value: 101 }, + { time: '2019-01-03', value: 101.5 }, + { time: '2019-01-04', value: 101.1 }, + { time: '2019-01-08', value: 101.1 }, + { time: '2019-01-09', value: 102.06 }, + { time: '2019-01-10', value: 105.14 }, + { time: '2019-01-11', value: 104.66 }, + { time: '2019-01-14', value: 106.22 }, + { time: '2019-01-15', value: 106.98 }, + { time: '2019-01-16', value: 108.4 }, + { time: '2019-01-17', value: 109.06 }, + { time: '2019-01-18', value: 107 }, + { time: '2019-01-21', value: 105.8 }, + { time: '2019-01-22', value: 105.24 }, + { time: '2019-01-23', value: 105.4 }, + { time: '2019-01-24', value: 105.38 }, + { time: '2019-01-25', value: 105.7 }, + { time: '2019-01-28', value: 105.8 }, + { time: '2019-01-29', value: 106.1 }, + { time: '2019-01-30', value: 108.6 }, + { time: '2019-01-31', value: 107.92 }, + { time: '2019-02-01', value: 105.22 }, + { time: '2019-02-04', value: 102.44 }, + { time: '2019-02-05', value: 102.26 }, + { time: '2019-02-06', value: 102 }, + { time: '2019-02-07', value: 101.62 }, + { time: '2019-02-08', value: 101.9 }, + { time: '2019-02-11', value: 101.78 }, + { time: '2019-02-12', value: 102.7 }, + { time: '2019-02-13', value: 100.14 }, + { time: '2019-02-14', value: 101.36 }, + { time: '2019-02-15', value: 101.62 }, + { time: '2019-02-18', value: 100.74 }, + { time: '2019-02-19', value: 100 }, + { time: '2019-02-20', value: 100.04 }, + { time: '2019-02-21', value: 100 }, + { time: '2019-02-22', value: 100.12 }, + { time: '2019-02-25', value: 100.04 }, + { time: '2019-02-26', value: 98.980003 }, + { time: '2019-02-27', value: 97.699997 }, + { time: '2019-02-28', value: 97.099998 }, + { time: '2019-03-01', value: 96.760002 }, + { time: '2019-03-04', value: 98.360001 }, + { time: '2019-03-05', value: 96.260002 }, + { time: '2019-03-06', value: 98.120003 }, + { time: '2019-03-07', value: 99.68 }, + { time: '2019-03-11', value: 102.1 }, + { time: '2019-03-12', value: 100.22 }, + { time: '2019-03-13', value: 100.5 }, + { time: '2019-03-14', value: 99.660004 }, + { time: '2019-03-15', value: 100.08 }, + { time: '2019-03-18', value: 99.919998 }, + { time: '2019-03-19', value: 99.459999 }, + { time: '2019-03-20', value: 98.220001 }, + { time: '2019-03-21', value: 97.300003 }, + { time: '2019-03-22', value: 97.660004 }, + { time: '2019-03-25', value: 97 }, + { time: '2019-03-26', value: 97.019997 }, + { time: '2019-03-27', value: 96.099998 }, + { time: '2019-03-28', value: 96.699997 }, + { time: '2019-03-29', value: 96.300003 }, + { time: '2019-04-01', value: 97.779999 }, + { time: '2019-04-02', value: 98.360001 }, + { time: '2019-04-03', value: 98.5 }, + { time: '2019-04-04', value: 98.360001 }, + { time: '2019-04-05', value: 99.540001 }, + { time: '2019-04-08', value: 98.580002 }, + { time: '2019-04-09', value: 97.239998 }, + { time: '2019-04-10', value: 97.32 }, + { time: '2019-04-11', value: 98.400002 }, + { time: '2019-04-12', value: 98.220001 }, + { time: '2019-04-15', value: 98.720001 }, + { time: '2019-04-16', value: 99.120003 }, + { time: '2019-04-17', value: 98.620003 }, + { time: '2019-04-18', value: 98 }, + { time: '2019-04-19', value: 97.599998 }, + { time: '2019-04-22', value: 97.599998 }, + { time: '2019-04-23', value: 96.800003 }, + { time: '2019-04-24', value: 96.32 }, + { time: '2019-04-25', value: 96.339996 }, + { time: '2019-04-26', value: 97.120003 }, + { time: '2019-04-29', value: 96.980003 }, + { time: '2019-04-30', value: 96.32 }, + { time: '2019-05-02', value: 96.860001 }, + { time: '2019-05-03', value: 96.699997 }, + { time: '2019-05-06', value: 94.940002 }, + { time: '2019-05-07', value: 94.5 }, + { time: '2019-05-08', value: 94.239998 }, + { time: '2019-05-10', value: 92.900002 }, + { time: '2019-05-13', value: 91.279999 }, + { time: '2019-05-14', value: 91.599998 }, + { time: '2019-05-15', value: 90.080002 }, + { time: '2019-05-16', value: 91.68 }, + { time: '2019-05-17', value: 91.959999 }, + { time: '2019-05-20', value: 91.080002 }, + { time: '2019-05-21', value: 90.760002 }, + { time: '2019-05-22', value: 91 }, + { time: '2019-05-23', value: 90.739998 }, + { time: '2019-05-24', value: 91 }, + { time: '2019-05-27', value: 91.199997 }, + { time: '2019-05-28', value: 90.68 }, + { time: '2019-05-29', value: 91.120003 }, + { time: '2019-05-30', value: 90.419998 }, + { time: '2019-05-31', value: 93.800003 }, + { time: '2019-06-03', value: 96.800003 }, + { time: '2019-06-04', value: 96.34 }, + { time: '2019-06-05', value: 95.94 }, + // hide-end +]); + +// const symbolName = 'ETC USD 7D VWAP'; + +const container = document.getElementById('container'); + +function dateToString(date) { + return `${date.year} - ${date.month} - ${date.day}`; +} + +const toolTipWidth = 80; +const priceScaleWidth = 50; +const toolTipMargin = 15; + +// Create and style the tooltip html element +const toolTip = document.createElement('div'); +toolTip.style = `width: 96px; height: 300px; position: absolute; display: none; padding: 8px; box-sizing: border-box; font-size: 12px; text-align: left; z-index: 1000; top: 12px; left: 12px; pointer-events: none; border-radius: 4px 4px 0px 0px; border-bottom: none; box-shadow: 0 2px 5px 0 rgba(117, 134, 150, 0.45);font-family: 'Trebuchet MS', Roboto, Ubuntu, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;`; +toolTip.style.background = `rgba(${CHART_BACKGROUND_RGB_COLOR}, 0.25)`; +toolTip.style.color = CHART_TEXT_COLOR; +toolTip.style.borderColor = BASELINE_BOTTOM_LINE_COLOR; +container.appendChild(toolTip); + +// update tooltip +chart.subscribeCrosshairMove(param => { + if ( + param.point === undefined || + !param.time || + param.point.x < 0 || + param.point.x > container.clientWidth || + param.point.y < 0 || + param.point.y > container.clientHeight + ) { + toolTip.style.display = 'none'; + } else { + const dateStr = dateToString(param.time); + toolTip.style.display = 'block'; + const price = param.seriesPrices.get(series); + toolTip.innerHTML = `
⬤ ABC Inc.
+ ${Math.round(100 * price) / 100} +
+ ${dateStr} +
`; + + // highlight-start + let left = param.point.x; + + if (left > container.clientWidth - toolTipWidth - toolTipMargin) { + left = container.clientWidth - toolTipWidth; + } else if (left < toolTipWidth / 2) { + left = priceScaleWidth; + } + + toolTip.style.left = left + 'px'; + toolTip.style.top = 0 + 'px'; + // highlight-end + } +}); + +chart.timeScale().fitContent(); diff --git a/website/tutorials/how_to/tooltip-tracking.js b/website/tutorials/how_to/tooltip-tracking.js new file mode 100644 index 0000000000..c494a91982 --- /dev/null +++ b/website/tutorials/how_to/tooltip-tracking.js @@ -0,0 +1,266 @@ +// remove-start +// Lightweight Charts Example: Tracking Tooltip +// https://tradingview.github.io/lightweight-charts/tutorials/how_to/tooltips + +// remove-end +const chartOptions = { + layout: { + textColor: CHART_TEXT_COLOR, + background: { type: 'solid', color: CHART_BACKGROUND_COLOR }, + }, +}; +// remove-line +/** @type {import('lightweight-charts').IChartApi} */ +const chart = createChart(document.getElementById('container'), chartOptions); + +chart.applyOptions({ + rightPriceScale: { + scaleMargins: { + top: 0.3, // leave some space for the legend + bottom: 0.25, + }, + }, + crosshair: { + // hide the horizontal crosshair line + horzLine: { + visible: false, + labelVisible: false, + }, + // hide the vertical crosshair label + vertLine: { + labelVisible: false, + }, + }, + // hide the grid lines + grid: { + vertLines: { + visible: false, + }, + horzLines: { + visible: false, + }, + }, +}); + +const series = chart.addAreaSeries({ + topColor: BASELINE_TOP_FILL_COLOR1, + bottomColor: BASELINE_TOP_FILL_COLOR2, + lineColor: BASELINE_TOP_LINE_COLOR, + lineWidth: 2, + crossHairMarkerVisible: false, +}); + +series.setData([ + { time: '2016-07-18', value: 98.66 }, + // hide-start + { time: '2016-07-25', value: 104.21 }, + { time: '2016-08-01', value: 107.48 }, + { time: '2016-08-08', value: 108.18 }, + { time: '2016-08-15', value: 109.36 }, + { time: '2016-08-22', value: 106.94 }, + { time: '2016-08-29', value: 107.73 }, + { time: '2016-09-05', value: 103.13 }, + { time: '2016-09-12', value: 114.92 }, + { time: '2016-09-19', value: 112.71 }, + { time: '2016-09-26', value: 113.05 }, + { time: '2016-10-03', value: 114.06 }, + { time: '2016-10-10', value: 117.63 }, + { time: '2016-10-17', value: 116.6 }, + { time: '2016-10-24', value: 113.72 }, + { time: '2016-10-31', value: 108.84 }, + { time: '2016-11-07', value: 108.43 }, + { time: '2016-11-14', value: 110.06 }, + { time: '2016-11-21', value: 111.79 }, + { time: '2016-11-28', value: 109.9 }, + { time: '2016-12-05', value: 113.95 }, + { time: '2016-12-12', value: 115.97 }, + { time: '2016-12-19', value: 116.52 }, + { time: '2016-12-26', value: 115.82 }, + { time: '2017-01-02', value: 117.91 }, + { time: '2017-01-09', value: 119.04 }, + { time: '2017-01-16', value: 120.0 }, + { time: '2017-01-23', value: 121.95 }, + { time: '2017-01-30', value: 129.08 }, + { time: '2017-02-06', value: 132.12 }, + { time: '2017-02-13', value: 135.72 }, + { time: '2017-02-20', value: 136.66 }, + { time: '2017-02-27', value: 139.78 }, + { time: '2017-03-06', value: 139.14 }, + { time: '2017-03-13', value: 139.99 }, + { time: '2017-03-20', value: 140.64 }, + { time: '2017-03-27', value: 143.66 }, + { time: '2017-04-03', value: 143.34 }, + { time: '2017-04-10', value: 141.05 }, + { time: '2017-04-17', value: 142.27 }, + { time: '2017-04-24', value: 143.65 }, + { time: '2017-05-01', value: 148.96 }, + { time: '2017-05-08', value: 156.1 }, + { time: '2017-05-15', value: 153.06 }, + { time: '2017-05-22', value: 153.61 }, + { time: '2017-05-29', value: 155.45 }, + { time: '2017-06-05', value: 148.98 }, + { time: '2017-06-12', value: 142.27 }, + { time: '2017-06-19', value: 146.28 }, + { time: '2017-06-26', value: 144.02 }, + { time: '2017-07-03', value: 144.18 }, + { time: '2017-07-10', value: 149.04 }, + { time: '2017-07-17', value: 150.27 }, + { time: '2017-07-24', value: 149.5 }, + { time: '2017-07-31', value: 156.39 }, + { time: '2017-08-07', value: 157.48 }, + { time: '2017-08-14', value: 157.5 }, + { time: '2017-08-21', value: 159.86 }, + { time: '2017-08-28', value: 164.05 }, + { time: '2017-09-04', value: 158.63 }, + { time: '2017-09-11', value: 159.88 }, + { time: '2017-09-18', value: 151.89 }, + { time: '2017-09-25', value: 154.12 }, + { time: '2017-10-02', value: 155.3 }, + { time: '2017-10-09', value: 156.99 }, + { time: '2017-10-16', value: 156.25 }, + { time: '2017-10-23', value: 163.05 }, + { time: '2017-10-30', value: 172.5 }, + { time: '2017-11-06', value: 174.67 }, + { time: '2017-11-13', value: 170.15 }, + { time: '2017-11-20', value: 174.97 }, + { time: '2017-11-27', value: 171.05 }, + { time: '2017-12-04', value: 169.37 }, + { time: '2017-12-11', value: 173.97 }, + { time: '2017-12-18', value: 175.01 }, + { time: '2017-12-25', value: 169.23 }, + { time: '2018-01-01', value: 175.0 }, + { time: '2018-01-08', value: 177.09 }, + { time: '2018-01-15', value: 178.46 }, + { time: '2018-01-22', value: 171.51 }, + { time: '2018-01-29', value: 160.5 }, + { time: '2018-02-05', value: 156.41 }, + { time: '2018-02-12', value: 172.43 }, + { time: '2018-02-19', value: 175.5 }, + { time: '2018-02-26', value: 176.21 }, + { time: '2018-03-05', value: 179.98 }, + { time: '2018-03-12', value: 178.02 }, + { time: '2018-03-19', value: 164.94 }, + { time: '2018-03-26', value: 167.78 }, + { time: '2018-04-02', value: 168.38 }, + { time: '2018-04-09', value: 174.73 }, + { time: '2018-04-16', value: 165.72 }, + { time: '2018-04-23', value: 162.32 }, + { time: '2018-04-30', value: 183.83 }, + { time: '2018-05-07', value: 188.59 }, + { time: '2018-05-14', value: 186.31 }, + { time: '2018-05-21', value: 188.58 }, + { time: '2018-05-28', value: 190.24 }, + { time: '2018-06-04', value: 191.7 }, + { time: '2018-06-11', value: 188.84 }, + { time: '2018-06-18', value: 184.92 }, + { time: '2018-06-25', value: 185.11 }, + { time: '2018-07-02', value: 187.97 }, + { time: '2018-07-09', value: 191.33 }, + { time: '2018-07-16', value: 191.44 }, + { time: '2018-07-23', value: 190.98 }, + { time: '2018-07-30', value: 207.99 }, + { time: '2018-08-06', value: 207.53 }, + { time: '2018-08-13', value: 217.58 }, + { time: '2018-08-20', value: 216.16 }, + { time: '2018-08-27', value: 227.63 }, + { time: '2018-09-03', value: 221.3 }, + { time: '2018-09-10', value: 223.84 }, + { time: '2018-09-17', value: 217.66 }, + { time: '2018-09-24', value: 225.74 }, + { time: '2018-10-01', value: 224.29 }, + { time: '2018-10-08', value: 222.11 }, + { time: '2018-10-15', value: 219.31 }, + { time: '2018-10-22', value: 216.3 }, + { time: '2018-10-29', value: 207.48 }, + { time: '2018-11-05', value: 204.47 }, + { time: '2018-11-12', value: 193.53 }, + { time: '2018-11-19', value: 172.29 }, + { time: '2018-11-26', value: 178.58 }, + { time: '2018-12-03', value: 168.49 }, + { time: '2018-12-10', value: 165.48 }, + { time: '2018-12-17', value: 150.73 }, + { time: '2018-12-24', value: 156.23 }, + { time: '2018-12-31', value: 148.26 }, + { time: '2019-01-07', value: 152.29 }, + { time: '2019-01-14', value: 156.82 }, + { time: '2019-01-21', value: 157.76 }, + { time: '2019-01-28', value: 166.52 }, + { time: '2019-02-04', value: 170.41 }, + { time: '2019-02-11', value: 170.42 }, + { time: '2019-02-18', value: 172.97 }, + { time: '2019-02-25', value: 174.97 }, + { time: '2019-03-04', value: 172.91 }, + { time: '2019-03-11', value: 186.12 }, + { time: '2019-03-18', value: 191.05 }, + { time: '2019-03-25', value: 189.95 }, + { time: '2019-04-01', value: 197.0 }, + { time: '2019-04-08', value: 198.87 }, + { time: '2019-04-15', value: 203.86 }, + { time: '2019-04-22', value: 204.3 }, + { time: '2019-04-29', value: 211.75 }, + { time: '2019-05-06', value: 197.18 }, + { time: '2019-05-13', value: 189.0 }, + { time: '2019-05-20', value: 178.97 }, + { time: '2019-05-27', value: 179.03 }, + // hide-end +]); + +// const symbolName = 'ETC USD 7D VWAP'; + +const container = document.getElementById('container'); + +function dateToString(date) { + return `${date.year} - ${date.month} - ${date.day}`; +} + +const toolTipWidth = 80; +const toolTipHeight = 80; +const toolTipMargin = 15; + +// Create and style the tooltip html element +const toolTip = document.createElement('div'); +toolTip.style = `width: 96px; height: 80px; position: absolute; display: none; padding: 8px; box-sizing: border-box; font-size: 12px; text-align: left; z-index: 1000; top: 12px; left: 12px; pointer-events: none; border: 1px solid; border-radius: 2px;font-family: 'Trebuchet MS', Roboto, Ubuntu, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;`; +toolTip.style.background = CHART_BACKGROUND_COLOR; +toolTip.style.color = CHART_TEXT_COLOR; +toolTip.style.borderColor = BASELINE_TOP_LINE_COLOR; +container.appendChild(toolTip); + +// update tooltip +chart.subscribeCrosshairMove(param => { + if ( + param.point === undefined || + !param.time || + param.point.x < 0 || + param.point.x > container.clientWidth || + param.point.y < 0 || + param.point.y > container.clientHeight + ) { + toolTip.style.display = 'none'; + } else { + const dateStr = dateToString(param.time); + toolTip.style.display = 'block'; + const price = param.seriesPrices.get(series); + toolTip.innerHTML = `
ABC Inc.
+ ${Math.round(100 * price) / 100} +
+ ${dateStr} +
`; + + // highlight-start + const y = param.point.y; + let left = param.point.x + toolTipMargin; + if (left > container.clientWidth - toolTipWidth) { + left = param.point.x - toolTipMargin - toolTipWidth; + } + + let top = y + toolTipMargin; + if (top > container.clientHeight - toolTipHeight) { + top = y - toolTipHeight - toolTipMargin; + } + toolTip.style.left = left + 'px'; + toolTip.style.top = top + 'px'; + // highlight-end + } +}); + +chart.timeScale().fitContent(); diff --git a/website/tutorials/how_to/tooltips.mdx b/website/tutorials/how_to/tooltips.mdx new file mode 100644 index 0000000000..5b350dd75a --- /dev/null +++ b/website/tutorials/how_to/tooltips.mdx @@ -0,0 +1,122 @@ +--- +title: Tooltips +sidebar_label: Tooltips +description: Examples on how to implement a tooltip for your chart. +pagination_prev: null +pagination_next: null +keywords: + - example + - tooltip +--- + +Lightweight charts doesn't include a built-in tooltip feature, however it is something which can be added +to your chart by following the examples presented below. + +## How to + +In order to add a tooltip to the chart we need to create and position an `html` into the desired position above +the chart. We can then subscribe to the crosshairMove events ([subscribeCrosshairMove](/docs/api/interfaces/IChartApi#subscribecrosshairmove)) provided by the [`IChartApi`](/docs/api/interfaces/IChartApi) instance, and manually +update the content within our `html` tooltip element and change it's position. + +```js +chart.subscribeCrosshairMove(param => { + if ( + param.point === undefined || + !param.time || + param.point.x < 0 || + param.point.y < 0 + ) { + toolTip.style.display = 'none'; + } else { + const dateStr = dateToString(param.time); + toolTip.style.display = 'block'; + const price = param.seriesPrices.get(series); + toolTip.innerHTML = `
${price.toFixed(2)}
`; + + // Position tooltip according to mouse cursor position + toolTip.style.left = param.point.x + 'px'; + toolTip.style.top = param.point.y + 'px'; + } +}); +``` + +The process of creating the tooltip html element and positioning can be seen within the examples below. +Essentially, we create a new div element within the container div (holding the chart) and then position +and style it using `css`. + +You can see full [working examples](#examples) below. + +### Getting the mouse cursors position + +The parameter object ([MouseEventParams Interface](/docs/api/interfaces/MouseEventParams)) passed to the +crosshairMove handler function ([MouseEventhandler](/docs/api#mouseeventhandler)) contains a +[point](/docs/api/interfaces/Point) property which gives the current mouse cursor position relative to +the top left corner of the chart. + +### Getting the data points position + +It is possible to convert a price value into it's current vertical position on the chart by using +the [priceToCoordinate](/docs/api/interfaces/ISeriesApi#pricetocoordinate) method on the series' instance. +This along with the `param.point.x` can be used to determine the position of the data point. + +```js +chart.subscribeCrosshairMove(param => { + const x = param.point.x; + const price = param.seriesPrices.get(series); + // where series is the ISeriesApi instance that we are interested in. + const y = series.priceToCoordinate(price); + console.log(`The data point is at position: ${x}, ${y}`); +}); +``` + +## Resources + +- [subscribeCrosshairMove](/docs/api/interfaces/IChartApi#subscribecrosshairmove) +- [MouseEventParams Interface](/docs/api/interfaces/MouseEventParams) +- [MouseEventhandler](/docs/api#mouseeventhandler) +- [priceToCoordinate](/docs/api/interfaces/ISeriesApi#pricetocoordinate) + +Below are a few external resources related to creating and styling html elements: + +- [createElement](https://developer.mozilla.org/en-US/docs/Web/API/Document/createElement) +- [innerHTML](https://developer.mozilla.org/en-US/docs/Web/API/Element/innerHTML) +- [style property](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/style) + +## Examples + +import UsageGuidePartial from "./_usage-guide-partial.mdx"; + + + +import CodeBlock from "@theme/CodeBlock"; + +### Floating Tooltip + +The floating tooltip in this example will position itself next to the current datapoint. + +import floatingCode from "!!raw-loader!./tooltip-floating.js"; + + + {floatingCode} + + +### Tracking Tooltip + +The tracking tooltip will position itself next to the user's cursor. + +import trackingCode from "!!raw-loader!./tooltip-tracking.js"; + + + {trackingCode} + + +### Magnifier Tooltip + +The magnifier tooltip will position itself along the top edge of the chart while following +the user's cursor along the horizontal time axis. + +import magnifierCode from "!!raw-loader!./tooltip-magnifier.js"; + + + {magnifierCode} + diff --git a/website/tutorials/how_to/two-price-scales.js b/website/tutorials/how_to/two-price-scales.js new file mode 100644 index 0000000000..c10342bfd6 --- /dev/null +++ b/website/tutorials/how_to/two-price-scales.js @@ -0,0 +1,859 @@ +// remove-start +// Lightweight Charts Example: Two Price Scales +// https://tradingview.github.io/lightweight-charts/tutorials/how_to/two-price-scales + +// remove-end +const chartOptions = { + // highlight-start + rightPriceScale: { + visible: true, + }, + leftPriceScale: { + visible: true, + }, + // highlight-end + layout: { + textColor: CHART_TEXT_COLOR, + background: { type: 'solid', color: CHART_BACKGROUND_COLOR }, + }, + // highlight-start + crosshair: { + mode: 0, // CrosshairMode.Normal + }, + // highlight-end +}; +// remove-line +/** @type {import('lightweight-charts').IChartApi} */ +const chart = createChart(document.getElementById('container'), chartOptions); +chart + .addLineSeries({ + color: LINE_LINE_COLOR, + lineWidth: 2, + }) + .setData([ + { time: { year: 2018, month: 9, day: 22 }, value: 25.531816900940186 }, + // hide-start + { time: { year: 2018, month: 9, day: 23 }, value: 26.350850429478125 }, + { time: { year: 2018, month: 9, day: 24 }, value: 25.05218443850655 }, + { time: { year: 2018, month: 9, day: 25 }, value: 25.41022485894306 }, + { time: { year: 2018, month: 9, day: 26 }, value: 25.134847363259958 }, + { time: { year: 2018, month: 9, day: 27 }, value: 24.239250761300525 }, + { time: { year: 2018, month: 9, day: 28 }, value: 28.8673009313941 }, + { time: { year: 2018, month: 9, day: 29 }, value: 27.028082380884264 }, + { time: { year: 2018, month: 9, day: 30 }, value: 27.181577793470662 }, + { time: { year: 2018, month: 10, day: 1 }, value: 28.658957209998505 }, + { time: { year: 2018, month: 10, day: 2 }, value: 30.418392247817536 }, + { time: { year: 2018, month: 10, day: 3 }, value: 26.41825183552505 }, + { time: { year: 2018, month: 10, day: 4 }, value: 30.0951233353539 }, + { time: { year: 2018, month: 10, day: 5 }, value: 30.30985059775389 }, + { time: { year: 2018, month: 10, day: 6 }, value: 30.71612555943148 }, + { time: { year: 2018, month: 10, day: 7 }, value: 28.222424591003268 }, + { time: { year: 2018, month: 10, day: 8 }, value: 31.01149570947896 }, + { time: { year: 2018, month: 10, day: 9 }, value: 30.390225881550307 }, + { time: { year: 2018, month: 10, day: 10 }, value: 29.451733557312163 }, + { time: { year: 2018, month: 10, day: 11 }, value: 34.14376900459634 }, + { time: { year: 2018, month: 10, day: 12 }, value: 30.223333215683407 }, + { time: { year: 2018, month: 10, day: 13 }, value: 35.1548736041708 }, + { time: { year: 2018, month: 10, day: 14 }, value: 37.795223779011096 }, + { time: { year: 2018, month: 10, day: 15 }, value: 38.95966228546306 }, + { time: { year: 2018, month: 10, day: 16 }, value: 35.59132526195566 }, + { time: { year: 2018, month: 10, day: 17 }, value: 38.42249768195307 }, + { time: { year: 2018, month: 10, day: 18 }, value: 40.82520015585623 }, + { time: { year: 2018, month: 10, day: 19 }, value: 37.401446370157814 }, + { time: { year: 2018, month: 10, day: 20 }, value: 44.14728329801845 }, + { time: { year: 2018, month: 10, day: 21 }, value: 43.908512951087765 }, + { time: { year: 2018, month: 10, day: 22 }, value: 47.139711966410914 }, + { time: { year: 2018, month: 10, day: 23 }, value: 43.78495537329606 }, + { time: { year: 2018, month: 10, day: 24 }, value: 46.37910782721347 }, + { time: { year: 2018, month: 10, day: 25 }, value: 48.280192310089234 }, + { time: { year: 2018, month: 10, day: 26 }, value: 49.63767420501933 }, + { time: { year: 2018, month: 10, day: 27 }, value: 43.05752682224708 }, + { time: { year: 2018, month: 10, day: 28 }, value: 48.32708061157758 }, + { time: { year: 2018, month: 10, day: 29 }, value: 53.39600337663517 }, + { time: { year: 2018, month: 10, day: 30 }, value: 46.711006266435355 }, + { time: { year: 2018, month: 10, day: 31 }, value: 54.13809826985657 }, + { time: { year: 2018, month: 11, day: 1 }, value: 55.79021790616995 }, + { time: { year: 2018, month: 11, day: 2 }, value: 49.2873885580548 }, + { time: { year: 2018, month: 11, day: 3 }, value: 56.97009522871737 }, + { time: { year: 2018, month: 11, day: 4 }, value: 50.823930530973975 }, + { time: { year: 2018, month: 11, day: 5 }, value: 54.960060077375076 }, + { time: { year: 2018, month: 11, day: 6 }, value: 62.0222568413422 }, + { time: { year: 2018, month: 11, day: 7 }, value: 58.20081640960216 }, + { time: { year: 2018, month: 11, day: 8 }, value: 65.13004590769961 }, + { time: { year: 2018, month: 11, day: 9 }, value: 57.78891076252559 }, + { time: { year: 2018, month: 11, day: 10 }, value: 58.792896124952186 }, + { time: { year: 2018, month: 11, day: 11 }, value: 61.87890147945707 }, + { time: { year: 2018, month: 11, day: 12 }, value: 60.93156560716248 }, + { time: { year: 2018, month: 11, day: 13 }, value: 57.85928164082374 }, + { time: { year: 2018, month: 11, day: 14 }, value: 70.95139577968187 }, + { time: { year: 2018, month: 11, day: 15 }, value: 71.59735270974251 }, + { time: { year: 2018, month: 11, day: 16 }, value: 68.6730845432055 }, + { time: { year: 2018, month: 11, day: 17 }, value: 70.1298800651962 }, + { time: { year: 2018, month: 11, day: 18 }, value: 68.82963709012753 }, + { time: { year: 2018, month: 11, day: 19 }, value: 70.66316240517193 }, + { time: { year: 2018, month: 11, day: 20 }, value: 67.83320577283186 }, + { time: { year: 2018, month: 11, day: 21 }, value: 75.08486799785067 }, + { time: { year: 2018, month: 11, day: 22 }, value: 72.87979342888752 }, + { time: { year: 2018, month: 11, day: 23 }, value: 78.84973566116827 }, + { time: { year: 2018, month: 11, day: 24 }, value: 77.59573370643601 }, + { time: { year: 2018, month: 11, day: 25 }, value: 74.74726921909757 }, + { time: { year: 2018, month: 11, day: 26 }, value: 69.68128245039887 }, + { time: { year: 2018, month: 11, day: 27 }, value: 84.2489916330028 }, + { time: { year: 2018, month: 11, day: 28 }, value: 85.49947753269504 }, + { time: { year: 2018, month: 11, day: 29 }, value: 79.8486264148003 }, + { time: { year: 2018, month: 11, day: 30 }, value: 87.69287202402485 }, + { time: { year: 2018, month: 12, day: 1 }, value: 78.01690218289475 }, + { time: { year: 2018, month: 12, day: 2 }, value: 90.03789034980372 }, + { time: { year: 2018, month: 12, day: 3 }, value: 80.8840602849401 }, + { time: { year: 2018, month: 12, day: 4 }, value: 76.88131503723805 }, + { time: { year: 2018, month: 12, day: 5 }, value: 80.31060219295262 }, + { time: { year: 2018, month: 12, day: 6 }, value: 93.94619117220051 }, + { time: { year: 2018, month: 12, day: 7 }, value: 94.87133202008548 }, + { time: { year: 2018, month: 12, day: 8 }, value: 82.60328626838404 }, + { time: { year: 2018, month: 12, day: 9 }, value: 97.16768398118845 }, + { time: { year: 2018, month: 12, day: 10 }, value: 86.28219316727935 }, + { time: { year: 2018, month: 12, day: 11 }, value: 88.98768390749808 }, + { time: { year: 2018, month: 12, day: 12 }, value: 86.9799632094888 }, + { time: { year: 2018, month: 12, day: 13 }, value: 94.84612878449812 }, + { time: { year: 2018, month: 12, day: 14 }, value: 102.1160216124386 }, + { time: { year: 2018, month: 12, day: 15 }, value: 87.0646295567293 }, + { time: { year: 2018, month: 12, day: 16 }, value: 88.48802912330535 }, + { time: { year: 2018, month: 12, day: 17 }, value: 89.68490260440238 }, + { time: { year: 2018, month: 12, day: 18 }, value: 86.66224375818467 }, + { time: { year: 2018, month: 12, day: 19 }, value: 88.05916917094234 }, + { time: { year: 2018, month: 12, day: 20 }, value: 78.96513176162487 }, + { time: { year: 2018, month: 12, day: 21 }, value: 90.54239307317953 }, + { time: { year: 2018, month: 12, day: 22 }, value: 92.40550159209458 }, + { time: { year: 2018, month: 12, day: 23 }, value: 82.47365747958841 }, + { time: { year: 2018, month: 12, day: 24 }, value: 91.55327647717618 }, + { time: { year: 2018, month: 12, day: 25 }, value: 89.34790162747024 }, + { time: { year: 2018, month: 12, day: 26 }, value: 85.68927849920716 }, + { time: { year: 2018, month: 12, day: 27 }, value: 85.86795553966918 }, + { time: { year: 2018, month: 12, day: 28 }, value: 90.55358282944198 }, + { time: { year: 2018, month: 12, day: 29 }, value: 91.28939932554621 }, + { time: { year: 2018, month: 12, day: 30 }, value: 100.90495261248472 }, + { time: { year: 2018, month: 12, day: 31 }, value: 98.99348823473713 }, + // hide-end + ]); + +const candlestickSeries = chart.addCandlestickSeries({ + // highlight-start + priceScaleId: 'left', + // highlight-end + upColor: BAR_UP_COLOR, downColor: BAR_DOWN_COLOR, borderVisible: false, + wickUpColor: BAR_UP_COLOR, wickDownColor: BAR_DOWN_COLOR, +}); + +candlestickSeries.setData([ + { + close: 108.9974612905403, + high: 121.20998259466148, + low: 96.65376292551082, + open: 104.5614412226746, + time: { year: 2018, month: 9, day: 22 }, + }, + // hide-start + { + close: 110.46815600023501, + high: 111.3650273696516, + low: 82.65543461471314, + open: 110.16538466099634, + time: { year: 2018, month: 9, day: 23 }, + }, + { + close: 90.62131977740425, + high: 109.19838270252615, + low: 82.30106956144076, + open: 105.03104735287424, + time: { year: 2018, month: 9, day: 24 }, + }, + { + close: 96.80120024431532, + high: 101.92074283374939, + low: 89.25819769856513, + open: 89.25819769856513, + time: { year: 2018, month: 9, day: 25 }, + }, + { + close: 94.87113928076117, + high: 104.12503365679314, + low: 85.42405005240111, + open: 104.12503365679314, + time: { year: 2018, month: 9, day: 26 }, + }, + { + close: 100.76494087674855, + high: 102.60508417239113, + low: 80.76268547064865, + open: 92.93299948659636, + time: { year: 2018, month: 9, day: 27 }, + }, + { + close: 101.45855928883597, + high: 110.26727325817173, + low: 91.10348900896837, + open: 93.4362185148034, + time: { year: 2018, month: 9, day: 28 }, + }, + { + close: 91.68871815146144, + high: 103.73263644407702, + low: 73.5329263610334, + open: 92.96467883926464, + time: { year: 2018, month: 9, day: 29 }, + }, + { + close: 89.39633140354033, + high: 101.06569518834237, + low: 81.71149885084162, + open: 83.08248758612376, + time: { year: 2018, month: 9, day: 30 }, + }, + { + close: 93.09498513675378, + high: 93.09498513675378, + low: 76.81138276012628, + open: 91.97280452613565, + time: { year: 2018, month: 10, day: 1 }, + }, + { + close: 89.26733004009083, + high: 89.26733004009083, + low: 76.27851645958225, + open: 85.83860311023625, + time: { year: 2018, month: 10, day: 2 }, + }, + { + close: 89.66035763006947, + high: 89.66035763006947, + low: 67.63677527399729, + open: 77.79584976144585, + time: { year: 2018, month: 10, day: 3 }, + }, + { + close: 89.59687813884807, + high: 97.05916949817754, + low: 72.9823390058379, + open: 77.37388423995716, + time: { year: 2018, month: 10, day: 4 }, + }, + { + close: 83.6425403120788, + high: 91.72593377862522, + low: 65.27208271740422, + open: 85.92711686401718, + time: { year: 2018, month: 10, day: 5 }, + }, + { + close: 82.99053929200655, + high: 93.4482645370556, + low: 65.98920655616067, + open: 71.8940788814462, + time: { year: 2018, month: 10, day: 6 }, + }, + { + close: 73.09595957510754, + high: 86.65935598412881, + low: 62.710852488609326, + open: 80.78945254092527, + time: { year: 2018, month: 10, day: 7 }, + }, + { + close: 80.12127692112905, + high: 87.49034842093855, + low: 60.09987438133361, + open: 70.2408873110499, + time: { year: 2018, month: 10, day: 8 }, + }, + { + close: 77.60439116240829, + high: 83.20908153481327, + low: 68.56836128158209, + open: 75.83440719565763, + time: { year: 2018, month: 10, day: 9 }, + }, + { + close: 73.8952889203428, + high: 81.89987377779327, + low: 57.8891283348631, + open: 66.51904017615065, + time: { year: 2018, month: 10, day: 10 }, + }, + { + close: 75.08452506029613, + high: 75.08452506029613, + low: 59.208194031968155, + open: 72.14475369395771, + time: { year: 2018, month: 10, day: 11 }, + }, + { + close: 73.08898607472305, + high: 73.08898607472305, + low: 63.05632280826725, + open: 66.93050765469924, + time: { year: 2018, month: 10, day: 12 }, + }, + { + close: 58.993371469509704, + high: 73.08872095153116, + low: 53.52204433018574, + open: 66.12840939191403, + time: { year: 2018, month: 10, day: 13 }, + }, + { + close: 57.150755012485, + high: 74.57414896810235, + low: 52.6552427480398, + open: 68.50876667562338, + time: { year: 2018, month: 10, day: 14 }, + }, + { + close: 58.03147289822832, + high: 69.62445357159157, + low: 53.8260018823565, + open: 61.62298899368165, + time: { year: 2018, month: 10, day: 15 }, + }, + { + close: 60.6852855399041, + high: 69.02095441024431, + low: 54.00939224622043, + open: 64.81901552322648, + time: { year: 2018, month: 10, day: 16 }, + }, + { + close: 57.508820449565285, + high: 63.82926565242872, + low: 54.07370975509682, + open: 54.07370975509682, + time: { year: 2018, month: 10, day: 17 }, + }, + { + close: 51.60796818909221, + high: 64.88712939579875, + low: 51.60796818909221, + open: 53.489226476218434, + time: { year: 2018, month: 10, day: 18 }, + }, + { + close: 55.139520748382864, + high: 59.161320710177925, + low: 52.228139922762765, + open: 52.228139922762765, + time: { year: 2018, month: 10, day: 19 }, + }, + { + close: 47.47868992247237, + high: 58.0836625917653, + low: 46.43213518526832, + open: 47.59258635788406, + time: { year: 2018, month: 10, day: 20 }, + }, + { + close: 47.22596723150508, + high: 51.55468175560989, + low: 45.22654435521185, + open: 47.452459556200054, + time: { year: 2018, month: 10, day: 21 }, + }, + { + close: 53.39724151191295, + high: 58.256006746786035, + low: 46.40105667413804, + open: 53.41548527476272, + time: { year: 2018, month: 10, day: 22 }, + }, + { + close: 45.015877277800854, + high: 51.2955426978105, + low: 40.26534748806228, + open: 43.90158660063875, + time: { year: 2018, month: 10, day: 23 }, + }, + { + close: 49.307312373439665, + high: 49.93643636637581, + low: 43.20705757075934, + open: 45.672934511555795, + time: { year: 2018, month: 10, day: 24 }, + }, + { + close: 45.15418019295631, + high: 48.59676744409583, + low: 37.628047445918504, + open: 40.33862825788268, + time: { year: 2018, month: 10, day: 25 }, + }, + { + close: 43.2972018283068, + high: 43.2972018283068, + low: 36.335943004352245, + open: 42.605991542720965, + time: { year: 2018, month: 10, day: 26 }, + }, + { + close: 39.1153643552137, + high: 44.311406627923844, + low: 35.31457011784855, + open: 42.00000202357808, + time: { year: 2018, month: 10, day: 27 }, + }, + { + close: 36.06960076896885, + high: 42.89041111567749, + low: 33.58326637182405, + open: 37.40942817102858, + time: { year: 2018, month: 10, day: 28 }, + }, + { + close: 35.8981036950969, + high: 42.19793419602979, + low: 33.62190962880232, + open: 36.87246325249825, + time: { year: 2018, month: 10, day: 29 }, + }, + { + close: 31.378205119641457, + high: 37.33501102832602, + low: 31.30137162225214, + open: 35.651275660713694, + time: { year: 2018, month: 10, day: 30 }, + }, + { + close: 33.574536057730576, + high: 37.30152570719593, + low: 27.369689193426243, + open: 34.330180925654936, + time: { year: 2018, month: 10, day: 31 }, + }, + { + close: 28.91735096504839, + high: 32.62196350117741, + low: 27.072564759401843, + open: 29.398552328599372, + time: { year: 2018, month: 11, day: 1 }, + }, + { + close: 28.44143154172122, + high: 31.042019861166594, + low: 23.383320830495375, + open: 27.275885037308072, + time: { year: 2018, month: 11, day: 2 }, + }, + { + close: 25.92162714418916, + high: 30.57604443170622, + low: 25.418671641150752, + open: 26.775196275924657, + time: { year: 2018, month: 11, day: 3 }, + }, + { + close: 26.376994016637433, + high: 28.198647836523744, + low: 21.492969733673334, + open: 26.27980943059139, + time: { year: 2018, month: 11, day: 4 }, + }, + { + close: 28.60834088674494, + high: 28.60834088674494, + low: 21.89002840571941, + open: 25.376464895884993, + time: { year: 2018, month: 11, day: 5 }, + }, + { + close: 31.103861067101136, + high: 31.103861067101136, + low: 24.39227668420513, + open: 28.994785427089838, + time: { year: 2018, month: 11, day: 6 }, + }, + { + close: 28.6308138310307, + high: 35.430817482769164, + low: 24.069515410358232, + open: 27.109407394069084, + time: { year: 2018, month: 11, day: 7 }, + }, + { + close: 29.468889521733466, + high: 37.54082586961352, + low: 27.90833873315644, + open: 33.16901271715577, + time: { year: 2018, month: 11, day: 8 }, + }, + { + close: 35.887823124204296, + high: 39.21804237580939, + low: 30.951078044055627, + open: 30.951078044055627, + time: { year: 2018, month: 11, day: 9 }, + }, + { + close: 34.361137347097575, + high: 35.27083445807796, + low: 27.825889562160082, + open: 34.86040182980157, + time: { year: 2018, month: 11, day: 10 }, + }, + { + close: 36.61336645243868, + high: 40.31460537175622, + low: 33.96383400053921, + open: 39.70037560142739, + time: { year: 2018, month: 11, day: 11 }, + }, + { + close: 41.321693986803055, + high: 44.45481986667003, + low: 35.67563772228475, + open: 38.67059795413642, + time: { year: 2018, month: 11, day: 12 }, + }, + { + close: 40.15038854039306, + high: 41.50912000191902, + low: 32.610637769394444, + open: 41.50912000191902, + time: { year: 2018, month: 11, day: 13 }, + }, + { + close: 44.092601065208015, + high: 44.092601065208015, + low: 37.778306506100726, + open: 38.99045898154136, + time: { year: 2018, month: 11, day: 14 }, + }, + { + close: 41.42426637839382, + high: 44.72189614841937, + low: 41.42426637839382, + open: 44.72189614841937, + time: { year: 2018, month: 11, day: 15 }, + }, + { + close: 41.19513795258408, + high: 49.08084695291049, + low: 36.24282165100056, + open: 44.909248500660254, + time: { year: 2018, month: 11, day: 16 }, + }, + { + close: 44.24935708161703, + high: 53.028535501565486, + low: 40.32056743060158, + open: 46.198546801109984, + time: { year: 2018, month: 11, day: 17 }, + }, + { + close: 43.18555863372182, + high: 52.34250206788521, + low: 43.18555863372182, + open: 49.58135271619679, + time: { year: 2018, month: 11, day: 18 }, + }, + { + close: 50.8568887039091, + high: 52.60441957102357, + low: 39.917719271944364, + open: 48.197532365645806, + time: { year: 2018, month: 11, day: 19 }, + }, + { + close: 48.79128595974164, + high: 52.45087789296739, + low: 46.80633073487828, + open: 52.45087789296739, + time: { year: 2018, month: 11, day: 20 }, + }, + { + close: 46.97300046781947, + high: 55.80689868049132, + low: 46.97300046781947, + open: 55.80689868049132, + time: { year: 2018, month: 11, day: 21 }, + }, + { + close: 55.58129861112469, + high: 55.58129861112469, + low: 49.087279242343996, + open: 53.16719476594961, + time: { year: 2018, month: 11, day: 22 }, + }, + { + close: 50.058979311730226, + high: 62.55333249171461, + low: 50.058979311730226, + open: 52.628489607588826, + time: { year: 2018, month: 11, day: 23 }, + }, + { + close: 51.193155229085995, + high: 59.08949991997865, + low: 51.193155229085995, + open: 55.352594157474755, + time: { year: 2018, month: 11, day: 24 }, + }, + { + close: 60.099338327208436, + high: 66.93510126958154, + low: 55.27299867222781, + open: 61.05897620044226, + time: { year: 2018, month: 11, day: 25 }, + }, + { + close: 58.3802630890727, + high: 71.50922937699602, + low: 50.95210438359451, + open: 62.4679688326532, + time: { year: 2018, month: 11, day: 26 }, + }, + { + close: 57.875350873413225, + high: 65.59903214448208, + low: 57.875350873413225, + open: 62.747405667247016, + time: { year: 2018, month: 11, day: 27 }, + }, + { + close: 61.231150731698605, + high: 66.3829902228434, + low: 61.231150731698605, + open: 65.01028486919516, + time: { year: 2018, month: 11, day: 28 }, + }, + { + close: 64.9698540874806, + high: 77.09783903299783, + low: 58.455031795628194, + open: 58.455031795628194, + time: { year: 2018, month: 11, day: 29 }, + }, + { + close: 72.40978471883417, + high: 72.40978471883417, + low: 53.05804901549206, + open: 65.907298021202, + time: { year: 2018, month: 11, day: 30 }, + }, + { + close: 74.60745731538934, + high: 78.33742117000789, + low: 54.42067144918077, + open: 73.20930147914103, + time: { year: 2018, month: 12, day: 1 }, + }, + { + close: 64.10866184869924, + high: 76.14506447001202, + low: 61.5224432669736, + open: 69.33984127682314, + time: { year: 2018, month: 12, day: 2 }, + }, + { + close: 65.92038759928786, + high: 76.98479070362022, + low: 65.92038759928786, + open: 69.32298264631615, + time: { year: 2018, month: 12, day: 3 }, + }, + { + close: 68.23682161095334, + high: 77.6723729460968, + low: 68.23682161095334, + open: 74.39471534484744, + time: { year: 2018, month: 12, day: 4 }, + }, + { + close: 67.45035792565862, + high: 83.53728553118547, + low: 67.45035792565862, + open: 74.79418570077237, + time: { year: 2018, month: 12, day: 5 }, + }, + { + close: 79.26722967320323, + high: 79.26722967320323, + low: 68.40654829383521, + open: 68.40654829383521, + time: { year: 2018, month: 12, day: 6 }, + }, + { + close: 74.95305464030587, + high: 81.65884414224071, + low: 64.08761481290135, + open: 81.65884414224071, + time: { year: 2018, month: 12, day: 7 }, + }, + { + close: 86.30802154315482, + high: 91.21953112925642, + low: 65.46112304993535, + open: 77.82514647663533, + time: { year: 2018, month: 12, day: 8 }, + }, + { + close: 82.67218208289492, + high: 92.45833377442081, + low: 76.80728739647081, + open: 87.18916937056241, + time: { year: 2018, month: 12, day: 9 }, + }, + { + close: 73.86125805398967, + high: 83.68952750914036, + low: 73.86125805398967, + open: 75.76119064173785, + time: { year: 2018, month: 12, day: 10 }, + }, + { + close: 79.00109311074502, + high: 88.74271558831151, + low: 69.00900811612337, + open: 88.74271558831151, + time: { year: 2018, month: 12, day: 11 }, + }, + { + close: 80.98779620162513, + high: 97.07429720104427, + low: 73.76970378608283, + open: 88.57288529720472, + time: { year: 2018, month: 12, day: 12 }, + }, + { + close: 87.83619759370346, + high: 91.29759438607132, + low: 74.00740214639268, + open: 87.51853658661781, + time: { year: 2018, month: 12, day: 13 }, + }, + { + close: 87.50134898892377, + high: 102.95635188637507, + low: 81.0513723219724, + open: 94.74009794290814, + time: { year: 2018, month: 12, day: 14 }, + }, + { + close: 92.40159548029843, + high: 103.24363067710844, + low: 75.44605394394573, + open: 95.99903495559444, + time: { year: 2018, month: 12, day: 15 }, + }, + { + close: 87.43619322092951, + high: 99.39349139000474, + low: 80.24624983473528, + open: 99.39349139000474, + time: { year: 2018, month: 12, day: 16 }, + }, + { + close: 84.42724177432086, + high: 95.57485075893881, + low: 84.42724177432086, + open: 90.71070399095831, + time: { year: 2018, month: 12, day: 17 }, + }, + { + close: 96.04408990868804, + high: 101.02158248010466, + low: 94.38544669520195, + open: 101.02158248010466, + time: { year: 2018, month: 12, day: 18 }, + }, + { + close: 97.2120815653703, + high: 103.35830035963959, + low: 78.72594316029567, + open: 86.98009038330306, + time: { year: 2018, month: 12, day: 19 }, + }, + { + close: 105.23366706522204, + high: 106.56174456393981, + low: 94.6658899187065, + open: 106.56174456393981, + time: { year: 2018, month: 12, day: 20 }, + }, + { + close: 89.53750874231946, + high: 112.27917367188074, + low: 87.13586952228918, + open: 96.0857985693989, + time: { year: 2018, month: 12, day: 21 }, + }, + { + close: 88.55787263435407, + high: 112.62138454627025, + low: 80.42783344898223, + open: 88.34340019789849, + time: { year: 2018, month: 12, day: 22 }, + }, + { + close: 86.00639650371167, + high: 110.94532193307279, + low: 74.78703575498496, + open: 92.4275741143068, + time: { year: 2018, month: 12, day: 23 }, + }, + { + close: 90.45119640254379, + high: 92.51500970997435, + low: 82.69475430846728, + open: 86.21662699549296, + time: { year: 2018, month: 12, day: 24 }, + }, + { + close: 93.38124264891343, + high: 93.38124264891343, + low: 84.5674956907938, + open: 87.54923273867136, + time: { year: 2018, month: 12, day: 25 }, + }, + { + close: 87.88725775527871, + high: 90.14253631595105, + low: 77.28638555494503, + open: 83.93199044032968, + time: { year: 2018, month: 12, day: 26 }, + }, + { + close: 71.77940077333062, + high: 89.25710176370582, + low: 67.74278646676306, + open: 78.5346198695072, + time: { year: 2018, month: 12, day: 27 }, + }, + { + close: 72.08757207606054, + high: 79.36518615067514, + low: 69.18787486704672, + open: 69.18787486704672, + time: { year: 2018, month: 12, day: 28 }, + }, + { + close: 73.87977927793119, + high: 77.62891475860795, + low: 70.42293039125319, + open: 70.42293039125319, + time: { year: 2018, month: 12, day: 29 }, + }, + { + close: 74.86330345366132, + high: 75.88473282167168, + low: 62.89549355427313, + open: 74.86554252962132, + time: { year: 2018, month: 12, day: 30 }, + }, + { + close: 71.00787215611817, + high: 71.00787215611817, + low: 57.29681995441558, + open: 60.04464694823929, + time: { year: 2018, month: 12, day: 31 }, + }, + // hide-end +]); + +chart.timeScale().fitContent(); diff --git a/website/tutorials/how_to/two-price-scales.mdx b/website/tutorials/how_to/two-price-scales.mdx new file mode 100644 index 0000000000..0fd6226186 --- /dev/null +++ b/website/tutorials/how_to/two-price-scales.mdx @@ -0,0 +1,78 @@ +--- +title: Two Price Scales +sidebar_label: Two Price Scales +description: An example of how to add two price scales to a chart. +pagination_prev: null +pagination_next: null +keywords: + - price + - scale +--- + +It is possible to have two price scales visible on a Lightweight Chart, +namely one on the right side (default) and another on the left. This example +shows how to configure your chart to contain two price scales. + +## Short answer + +Ensure that `rightPriceScale` and `leftPriceScale` has the `visibility` property +set to `true` within the [chart options](/docs/api/interfaces/ChartOptions#leftpricescale). + +```js +chart.applyOptions({ + rightPriceScale: { + visible: true, + }, + leftPriceScale: { + visible: true, + }, +}); +``` + +and assign the `priceScaleId` property on the [series options](/docs/api/interfaces/SeriesOptionsCommon#pricescaleid) +for the series which you would like to use the left scale. Note that by default a +series will use the right scale thus we don't need to set that property on the other series. + +```js +const leftSeries = chart.addCandlestickSeries( + { priceScaleId: 'left' } +); +``` + +You can see a full [working example](#full-example) below. + +## Tips + +By default the crosshair will snap to the data points of the first series. +You may prefer to set the [crosshair mode](/docs/api/enums/CrosshairMode) to +`normal` so that you get a crosshair which allows sits directly beneath your cursor. + +```js +chart.applyOptions({ + crosshair: { + mode: 0, // CrosshairMode.Normal + }, +}); +``` + +## Resources + +You can learn more about price scales here: [Price scale](/docs/price-scale) + +and view the related APIs here: +- [Chart Options](/docs/api/interfaces/ChartOptions#leftpricescale) +- [PriceScaleOptions](/docs/api/interfaces/PriceScaleOptions) +- [SeriesOptionsCommon priceScaleId](/docs/api/interfaces/SeriesOptionsCommon#pricescaleid) + +## Full example + +import UsageGuidePartial from "./_usage-guide-partial.mdx"; + + + +import CodeBlock from "@theme/CodeBlock"; +import code from "!!raw-loader!./two-price-scales.js"; + + + {code} + diff --git a/website/tutorials/how_to/watermark-advanced.js b/website/tutorials/how_to/watermark-advanced.js new file mode 100644 index 0000000000..d1f4b06ad6 --- /dev/null +++ b/website/tutorials/how_to/watermark-advanced.js @@ -0,0 +1,57 @@ +// remove-start +// Lightweight Charts Example: Watermark Advanced +// https://tradingview.github.io/lightweight-charts/tutorials/how_to/watermark + +// remove-end +const chartOptions = { + layout: { + textColor: CHART_TEXT_COLOR, + // set chart background color to transparent so we can see the elements below + // highlight-next-line + background: { type: 'solid', color: 'transparent' }, + }, +}; +// remove-line +/** @type {import('lightweight-charts').IChartApi} */ +const chart = createChart(document.getElementById('container'), chartOptions); + +// highlight-start +const container = document.getElementById('container'); +const background = document.createElement('div'); +// place below the chart +background.style.zIndex = -1; +background.style.position = 'absolute'; +// set size and position to match container +background.style.inset = '0px'; +background.style.backgroundImage = `url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyOTIiIGhlaWdodD0iMTI4IiB2aWV3Qm94PSIwIDAgMjkyIDEyOCI+PHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBkPSJtMTgyLjkzIDcuNi42My0uMzdhNjQuMSA2NC4xIDAgMCAwIDIuNDMtNS4zMWw0Ljc3LTEuMzlhNjQuNjggNjQuNjggMCAwIDEtNC43MiAxMC41NGMuMzggMTAuNDUtMy45MyAyMS4xNS0xMS4xIDI5LjM3LTExLjY2IDEzLjQxLTI2Ljk4IDE1Ljk3LTQzLjU3IDEzLjc4bDEuMDctLjk4YTIxLjEgMjEuMSAwIDAgMCAzLjcyLTQuMDUgNDguMzcgNDguMzcgMCAwIDEtMTEuMDQgMi44NGMtMTAuNjUtNS41NC0yMS42NC0xNC45NC0yNC4yNy0yNy4yNyA5LjE5LTE3IDI4Ljk1LTI0LjAxIDQ3LjM5LTE5Ljk0YTIyLjU3IDIyLjU3IDAgMCAwIDUuODYgOS4wMmMtLjEyLTEuOTItLjEtMy44NC0uMS01Ljc2bC4wMS0xLjc4YzQuOCAyLjk2IDkuNjYgNS44NSAxNS41MiA1LjcgNC4wOC0uMSA4LjQtMS41MiAxMy40LTQuNFptLTIyLjU1IDIzLjI4YTguNDggOC40OCAwIDAgMC0xMi40NS0uMzNsLTcuOS03LjI2QTguNiA4LjYgMCAwIDAgMTMyIDEyYy02LjE0IDAtMTAuMjUgNi42My03LjcgMTIuMDlsLTEzLjAyIDEyLjE5Yy00LjEtNC45Ny01LjY4LTkuMy02LjE3LTEwLjk0IDguMzYtMTMuNzIgMjQuNDYtMjAuMTggNDAuMTUtMTcuMDcgMi45MyA2LjkgOC4zOCAxMC43MiAxNC43NyAxMy45NmwtLjMzLTEuMTRjLS43NC0yLjU2LTEuNDctNS4xLTEuNjItNy43OCA3LjA1IDMuNDUgMTQuNiAzLjM1IDIxLjc2LjMxLTQuNzYgNy4yNy0xMS4xMyAxNC4yMi0xOS40NiAxNy4yNlptLTIyLjU2LTQuMTkgOC4wMyA3LjM4QTguNiA4LjYgMCAwIDAgMTU0IDQ1YTguNiA4LjYgMCAwIDAgOC4yNS0xMC41NWM3Ljk5LTMuMDggMTQuMzctOS4zOCAxOS4yOC0xNi4yMy0zLjQ3IDE5LjQ3LTIxLjk2IDM0LjYxLTQxLjkgMzIuOTggMS43Ny0yLjg0IDIuNDktNi4wNiAzLjIxLTkuMjhsLjM1LTEuNTZjLTUuNDcgMy43Ny0xMC42NyA2LjM4LTE3LjM3IDcuNTJhNDkuOSA0OS45IDAgMCAxLTExLjg1LTguNjVsMTIuODMtMTJhOC41OCA4LjU4IDAgMCAwIDExLjAyLS41NFpNMTMyIDE2YTQuNSA0LjUgMCAxIDAgMCA5IDQuNSA0LjUgMCAwIDAgMC05Wm0xNy41IDIwLjVhNC41IDQuNSAwIDEgMSA5IDAgNC41IDQuNSAwIDAgMS05IDBaTTIxLjYzIDcxLjhhMi4zMyAyLjMzIDAgMCAxIDIuMzMgMi4zNCAyLjM0IDIuMzQgMCAwIDEtMi4zMyAyLjM3IDIuMzggMi4zOCAwIDAgMS0yLjM3LTIuMzcgMi4zOCAyLjM4IDAgMCAxIDIuMzctMi4zM1ptMS43NiA4LjJ2MTZoLTMuNTJWODBoMy41MlptLTYuNDYgMTZIMi43OFY3My4yOGgzLjc1djE5LjE0aDEwLjRWOTZabTI2LjM5LTEuMDlWODBIMzkuOHYyLjE0YTYuMjYgNi4yNiAwIDAgMC01LjEyLTIuNDZjLTQuMzIgMC03LjY4IDMuNTgtNy42OCA4LjEgMCA0LjU0IDMuMzYgOC4xMiA3LjY4IDguMTIgMi4yIDAgNC4xNi0xLjA4IDUuMTItMi41djEuNDhjMCAzLjIzLTIuMTggNS00LjgzIDVhNy4wMyA3LjAzIDAgMCAxLTUuMzItMi4zNGwtMi4xNCAyLjUyYzEuNTcgMS43NiA0LjM1IDIuOTUgNy40OSAyLjk1IDQuNzMgMCA4LjMyLTIuNTMgOC4zMi04LjFabS0xMi43Ny03LjEzYTQuNyA0LjcgMCAwIDEgNC43Ny00LjkgNC43IDQuNyAwIDAgMSA0Ljc3IDQuOSA0LjcgNC43IDAgMCAxLTQuNzcgNC45IDQuNyA0LjcgMCAwIDEtNC43Ny00LjlaTTUxLjU4IDk2aC0zLjUyVjcyaDMuNTJ2MTAuMThjLjk2LTEuNiAyLjc4LTIuNSA0Ljg2LTIuNSAzLjcxIDAgNi4xMSAyLjYyIDYuMTEgNi42OVY5NmgtMy41MnYtOS4wNmMwLTIuNTItMS4yOC00LjA2LTMuMzMtNC4wNi0yLjMzIDAtNC4xMiAxLjgyLTQuMTIgNS4yNVY5NlptMjQuODYtLjJ2LTMuMTNjLS41Mi4yLTEuMjIuMzItMS45LjMyLTEuODIgMC0yLjY4LS43My0yLjY4LTIuNzJ2LTcuMTNoNC41OFY4MGgtNC41OHYtNC40NWgtMy41MlY4MGgtMy4zM3YzLjE0aDMuMzN2Ny43YzAgMy42MiAyLjQgNS4zMiA1LjQ3IDUuMzIgMS4wOSAwIDEuOTItLjEzIDIuNjMtLjM1Wm0yMC4zLjJIOTMuNGwtMy41Mi0xMC4zN0w4Ni4zOSA5NmgtMy4zMmwtNS4zOC0xNmgzLjcybDMuNDUgMTEgMy42OC0xMWgyLjY5bDMuNjUgMTEgMy40OS0xMWgzLjc0bC01LjM4IDE2Wm02Ljc2LThjMCA0Ljg2IDMuNDkgOC4zMiA4LjM1IDguMzIgMy4zNiAwIDUuODYtMS40NCA3LjMtMy43MWwtMi43LTEuOTJhNS4wMyA1LjAzIDAgMCAxLTQuNTcgMi40M2MtMi42NSAwLTQuNzctMS43My00LjkzLTQuMzVoMTIuNThjLjAzLS41MS4wMy0uOC4wMy0xLjE1IDAtNS4xNi0zLjUyLTcuOTQtNy43MS03Ljk0QTguMTIgOC4xMiAwIDAgMCAxMDMuNSA4OFptOC4yMi01LjM0YzIuMDUgMCAzLjkgMS4yNCA0LjI5IDMuNTVoLTguOWMuNDgtMi4zNyAyLjU2LTMuNTUgNC42MS0zLjU1Wm0xMy4yMi0xMC44NWEyLjMzIDIuMzMgMCAwIDEgMi4zNCAyLjMzIDIuMzQgMi4zNCAwIDAgMS0yLjM0IDIuMzcgMi4zOCAyLjM4IDAgMCAxLTIuMzctMi4zNyAyLjM4IDIuMzggMCAwIDEgMi4zNy0yLjMzWm0yMS43IDIzLjFWODBoLTMuNTN2Mi4xNGE2LjI2IDYuMjYgMCAwIDAtNS4xMi0yLjQ2Yy00LjMyIDAtNy42OCAzLjU4LTcuNjggOC4xIDAgNC41NCAzLjM2IDguMTIgNy42OCA4LjEyIDIuMiAwIDQuMTYtMS4wOCA1LjEyLTIuNXYxLjQ4YzAgMy4yMy0yLjE4IDUtNC44MyA1YTcuMDMgNy4wMyAwIDAgMS01LjMxLTIuMzRsLTIuMTUgMi41MmMxLjU3IDEuNzYgNC4zNiAyLjk1IDcuNSAyLjk1IDQuNzMgMCA4LjMxLTIuNTMgOC4zMS04LjFaTTEyNi43IDk2aC0zLjUyVjgwaDMuNTJ2MTZabTcuMTYtOC4yMmE0LjcgNC43IDAgMCAxIDQuNzctNC45IDQuNyA0LjcgMCAwIDEgNC43NyA0LjkgNC43IDQuNyAwIDAgMS00Ljc3IDQuOSA0LjcgNC43IDAgMCAxLTQuNzctNC45Wk0xNTQuOSA5NmgtMy41MlY3MmgzLjUydjEwLjE4Yy45Ni0xLjYgMi43OC0yLjUgNC44Ni0yLjUgMy43MSAwIDYuMTEgMi42MiA2LjExIDYuNjlWOTZoLTMuNTJ2LTkuMDZjMC0yLjUyLTEuMjgtNC4wNi0zLjMyLTQuMDYtMi4zNCAwLTQuMTMgMS44Mi00LjEzIDUuMjVWOTZabTI0Ljg2LS4ydi0zLjEzYy0uNTEuMi0xLjIyLjMyLTEuODkuMzItMS44MiAwLTIuNjktLjczLTIuNjktMi43MnYtNy4xM2g0LjU4VjgwaC00LjU4di00LjQ1aC0zLjUyVjgwaC0zLjMzdjMuMTRoMy4zM3Y3LjdjMCAzLjYyIDIuNCA1LjMyIDUuNDcgNS4zMiAxLjEgMCAxLjkyLS4xMyAyLjYzLS4zNVptMjEuNTkuNThhMTEuNjcgMTEuNjcgMCAwIDEtMTEuNzUtMTEuNzRjMC02LjU2IDUuMjItMTEuNzQgMTEuNzUtMTEuNzQgNC40NSAwIDguMjIgMi4yNyAxMC4yNCA1Ljc2bC0zLjIzIDEuODVhNy45NCA3Ljk0IDAgMCAwLTcuMDEtNCA3Ljk2IDcuOTYgMCAwIDAtNy45NyA4LjEzIDcuOTYgNy45NiAwIDAgMCA3Ljk3IDguMTMgNy45NCA3Ljk0IDAgMCAwIDctNGwzLjI0IDEuODVhMTEuNjYgMTEuNjYgMCAwIDEtMTAuMjQgNS43NlptMTMuNC0uMzhoMy41MnYtNy44N2MwLTMuNDMgMS44LTUuMjUgNC4xMy01LjI1IDIuMDUgMCAzLjMzIDEuNTQgMy4zMyA0LjA2Vjk2aDMuNTJ2LTkuNjNjMC00LjA3LTIuNC02LjY5LTYuMTEtNi42OS0yLjA4IDAtMy45LjktNC44NyAyLjVWNzJoLTMuNTJ2MjRabTI1LjU2LjMyYy00LjM4IDAtNy43LTMuNzQtNy43LTguMzJzMy4zMi04LjMyIDcuNy04LjMyYzIuMyAwIDQuMjMgMS4xOCA1LjEyIDIuNDZWODBoMy41MnYxNmgtMy41MnYtMi4xNGE2LjM4IDYuMzggMCAwIDEtNS4xMiAyLjQ2Wm0uNjQtMy4yYzIuODUgMCA0Ljc3LTIuMjQgNC43Ny01LjEycy0xLjkyLTUuMTItNC43Ny01LjEyYy0yLjg0IDAtNC43NiAyLjI0LTQuNzYgNS4xMnMxLjkxIDUuMTIgNC43NiA1LjEyWk0yNTMuNzEgOTZoMy41MnYtNy44YzAtMy4yIDEuODMtNC45IDMuODQtNC45LjY0IDAgMS4xNS4xIDEuNzYuMjh2LTMuNjFjLS40OC0uMS0uOTMtLjEzLTEuMzctLjEzYTQuNSA0LjUgMCAwIDAtNC4yMyAzVjgwaC0zLjUydjE2Wm0yMS43My0zLjMzdjMuMTRjLS43LjIyLTEuNTQuMzUtMi42My4zNS0zLjA3IDAtNS40Ny0xLjctNS40Ny01LjMxdi03LjcxaC0zLjMzVjgwaDMuMzN2LTQuNDVoMy41MlY4MGg0LjU4djMuMTRoLTQuNTh2Ny4xM2MwIDEuOTkuODYgMi43MiAyLjY5IDIuNzIuNjcgMCAxLjM3LS4xMyAxLjg5LS4zMlptMTQuMjEtMS4zMWMwLTIuNjItMS42Ni00LjAzLTQuNDgtNC44NmwtMS42My0uNDhjLTEuNTctLjQ1LTEuOTItMS4xMi0xLjkyLTEuOSAwLS45NSAxLjA5LTEuNSAyLjE1LTEuNSAxLjMgMCAyLjMzLjY0IDMuMDQgMS42NGwyLjQzLTEuODZjLTEuMTItMS43Ni0zLjAxLTIuNzItNS40MS0yLjcyLTMuMiAwLTUuNyAxLjczLTUuNzMgNC41OC0uMDMgMi4zNiAxLjQxIDQuMTIgNC4yIDQuOWwxLjQuMzhjMS45Mi41NyAyLjQ3IDEuMTIgMi40NyAyLjA0IDAgMS4xMi0xLjA2IDEuNy0yLjMgMS43LTEuNjQgMC0zLjItLjgtMy44NS0yLjJsLTIuNTkgMS44NWMxLjE1IDIuMjcgMy41OCAzLjM5IDYuNDMgMy4zOSAzLjMgMCA1LjgtMS44OSA1LjgtNC45NlptLTE0My4zOCAyMS40YzAgLjQ2LS4zNy44NC0uODMuODRhLjg2Ljg2IDAgMCAxLS44Ny0uODVjMC0uNDYuMzktLjg1Ljg3LS44NS40NiAwIC44My4zOS44My44NVptLS4yOSAxMS4yNGgtMS4xMnYtOGgxLjEydjhabS01Mi4wMi4xNmE0LjA0IDQuMDQgMCAwIDAgMy45OC00LjE2IDQuMDQgNC4wNCAwIDAgMC0zLjk4LTQuMTZjLTEuMjQgMC0yLjM5LjY0LTIuOTYgMS41VjExMmgtMS4xMnYxMkg5MXYtMS4zNGMuNTcuODYgMS43MiAxLjUgMi45NiAxLjVabS0uMTItMS4wNGMtMS43NCAwLTIuOTQtMS40LTIuOTQtMy4xMiAwLTEuNzMgMS4yLTMuMTIgMi45NC0zLjEyIDEuNzUgMCAyLjk1IDEuNCAyLjk1IDMuMTIgMCAxLjczLTEuMiAzLjEyLTIuOTUgMy4xMlptNy45IDQuMjIgNS4zLTExLjM0aC0xLjI2bC0yLjkzIDYuMzUtMi45My02LjM1aC0xLjI0bDMuNTUgNy42LTEuNzYgMy43NGgxLjI2Wk0xMTUuMyAxMjRoLTEuMnYtMTAuMmgtMy42OHYtMS4xNmg4LjU2djEuMTVoLTMuNjhWMTI0Wm0zLjgyIDBoMS4xMnYtNC4wMmMwLTIuMDQgMS4yMy0yLjk0IDIuMjItMi45NC4yNCAwIC40NS4wMy42Ny4xMXYtMS4xN2EyLjQ0IDIuNDQgMCAwIDAtMi45IDEuNjZWMTE2aC0xLjExdjhabTExLjcyLTEuMzRhMy42NCAzLjY0IDAgMCAxLTIuOTYgMS41IDQuMDQgNC4wNCAwIDAgMS0zLjk4LTQuMTYgNC4wNCA0LjA0IDAgMCAxIDMuOTgtNC4xNmMxLjIzIDAgMi4zOS42NCAyLjk2IDEuNVYxMTZoMS4xMnY4aC0xLjEydi0xLjM0Wm0tNS44LTIuNjZjMCAxLjczIDEuMiAzLjEyIDIuOTUgMy4xMiAxLjc1IDAgMi45NS0xLjQgMi45NS0zLjEyIDAtMS43My0xLjItMy4xMi0yLjk1LTMuMTItMS43NCAwLTIuOTQgMS40LTIuOTQgMy4xMlptMTIuOTggNC4xNmMxLjIzIDAgMi4zOS0uNjQgMi45Ni0xLjVWMTI0aDEuMTJ2LTEySDE0MXY1LjM0YTMuNjQgMy42NCAwIDAgMC0yLjk2LTEuNSA0LjA0IDQuMDQgMCAwIDAtMy45OCA0LjE2IDQuMDQgNC4wNCAwIDAgMCAzLjk4IDQuMTZabS4xMS0xLjA0Yy0xLjc0IDAtMi45NC0xLjQtMi45NC0zLjEyIDAtMS43MyAxLjItMy4xMiAyLjk0LTMuMTIgMS43NSAwIDIuOTUgMS40IDIuOTUgMy4xMiAwIDEuNzMtMS4yIDMuMTItMi45NSAzLjEyWm0xMC42Ljg4aDEuMTF2LTMuOThjMC0xLjk5IDEuMS0zLjE0IDIuNS0zLjE0IDEuMTkgMCAyLjAyLjg2IDIuMDIgMi4yN1YxMjRoMS4xMnYtNWMwLTEuOTYtMS4yNy0zLjE2LTMuMDEtMy4xNi0xLjA0IDAtMi4wNS40NS0yLjYzIDEuNVYxMTZoLTEuMTF2OFptMTYuNzEtLjQyYzAgMi42MS0xLjcyIDMuOTItMy45NSAzLjkyLTEuODQgMC0zLjE3LS44My0zLjc3LTEuNzRsLjg4LS43NWEzLjQgMy40IDAgMCAwIDIuOSAxLjQ1YzEuMzcgMCAyLjgyLS44MyAyLjgyLTIuOTR2LTEuMDJjLS41Ny44Ni0xLjcgMS41LTIuOTIgMS41YTMuOTQgMy45NCAwIDAgMS0zLjk2LTQuMDggMy45NCAzLjk0IDAgMCAxIDMuOTYtNC4wOGMxLjIzIDAgMi4zNS42NCAyLjkyIDEuNVYxMTZoMS4xMnY3LjU4Wm0tNi44NC0zLjY2YzAgMS43MyAxLjE2IDMuMDQgMi45IDMuMDQgMS43NSAwIDIuOTItMS4zMSAyLjkyLTMuMDRzLTEuMTctMy4wNC0yLjkxLTMuMDRjLTEuNzUgMC0yLjkxIDEuMzEtMi45MSAzLjA0Wm0xMy41NSA0LjA4IDQuODgtMTEuMzZoLTEuMzVsLTQuMDMgOS4zOC00LjAzLTkuMzhoLTEuMzZsNC45IDExLjM2aC45OVptNy44NC0xMS4yNWMwIC40Ny0uMzcuODUtLjgzLjg1YS44Ni44NiAwIDAgMS0uODYtLjg1YzAtLjQ2LjM4LS44NS44Ni0uODUuNDcgMCAuODMuMzkuODMuODVabS0uMjggMTEuMjVoLTEuMTN2LThoMS4xM3Y4Wm02LjIuMTZhMy45IDMuOSAwIDAgMCAzLjU2LTEuOTVsLS45MS0uNmEyLjc4IDIuNzggMCAwIDEtMi42NCAxLjUxIDIuODcgMi44NyAwIDAgMS0yLjk2LTIuOTNoNi43NXYtLjNjLS4wMi0yLjU2LTEuNjgtNC4wNS0zLjc2LTQuMDVhNC4wNSA0LjA1IDAgMCAwLTQuMTUgNC4xNmMwIDIuMyAxLjYgNC4xNiA0LjEyIDQuMTZabS0uMDEtNy4yOGMxLjM0IDAgMi40NS44OCAyLjY0IDIuMzJoLTUuNDlhMi44NCAyLjg0IDAgMCAxIDIuODUtMi4zMlptMTMuNTUgNy4xMmgtLjkzbC0yLjEtNi4xLTIuMTQgNi4xaC0uOTJsLTIuNzQtOGgxLjE1bDIuMDggNi4wOCAyLjExLTYuMDhoLjg3bDIuMTEgNi4wOCAyLjA4LTYuMDhoMS4xN2wtMi43NCA4WiIgZmlsbD0iY3VycmVudENvbG9yIj48L3BhdGg+PC9zdmc+")`; +background.style.backgroundRepeat = 'no-repeat'; +background.style.backgroundPosition = 'center'; +background.style.opacity = '0.5'; +container.appendChild(background); +// highlight-end + +const lineSeries = chart.addAreaSeries({ + topColor: AREA_TOP_COLOR, + bottomColor: AREA_BOTTOM_COLOR, + lineColor: LINE_LINE_COLOR, + lineWidth: 2, +}); + +const data = [ + { value: 0, time: 1642425322 }, + // hide-start + { value: 8, time: 1642511722 }, + { value: 10, time: 1642598122 }, + { value: 20, time: 1642684522 }, + { value: 3, time: 1642770922 }, + { value: 43, time: 1642857322 }, + { value: 41, time: 1642943722 }, + { value: 43, time: 1643030122 }, + { value: 56, time: 1643116522 }, + { value: 46, time: 1643202922 }, + // hide-end +]; + +lineSeries.setData(data); + +chart.timeScale().fitContent(); diff --git a/website/tutorials/how_to/watermark-simple.js b/website/tutorials/how_to/watermark-simple.js new file mode 100644 index 0000000000..5687e385f6 --- /dev/null +++ b/website/tutorials/how_to/watermark-simple.js @@ -0,0 +1,53 @@ +// remove-start +// Lightweight Charts Example: Watermark Simple +// https://tradingview.github.io/lightweight-charts/tutorials/how_to/watermark + +// remove-end +const chartOptions = { + layout: { + textColor: CHART_TEXT_COLOR, + background: { type: 'solid', color: CHART_BACKGROUND_COLOR }, + }, +}; +// remove-line +/** @type {import('lightweight-charts').IChartApi} */ +const chart = createChart(document.getElementById('container'), chartOptions); + +// highlight-start +chart.applyOptions({ + watermark: { + visible: true, + fontSize: 24, + horzAlign: 'center', + vertAlign: 'center', + color: 'rgba(171, 71, 188, 0.5)', + text: 'Watermark Example', + }, +}); +// highlight-end + +const lineSeries = chart.addAreaSeries({ + topColor: AREA_TOP_COLOR, + bottomColor: AREA_BOTTOM_COLOR, + lineColor: LINE_LINE_COLOR, + lineWidth: 2, +}); + +const data = [ + { value: 0, time: 1642425322 }, + // hide-start + { value: 8, time: 1642511722 }, + { value: 10, time: 1642598122 }, + { value: 20, time: 1642684522 }, + { value: 3, time: 1642770922 }, + { value: 43, time: 1642857322 }, + { value: 41, time: 1642943722 }, + { value: 43, time: 1643030122 }, + { value: 56, time: 1643116522 }, + { value: 46, time: 1643202922 }, + // hide-end +]; + +lineSeries.setData(data); + +chart.timeScale().fitContent(); diff --git a/website/tutorials/how_to/watermark.mdx b/website/tutorials/how_to/watermark.mdx new file mode 100644 index 0000000000..0af569fca8 --- /dev/null +++ b/website/tutorials/how_to/watermark.mdx @@ -0,0 +1,106 @@ +--- +title: Watermark +sidebar_label: Watermark +description: Examples of how to add a watermark to your chart. +pagination_prev: null +pagination_next: null +keywords: + - watermark + - example +--- + +Lightweight charts has a built-in feature for displaying simple text watermarks on your chart. +This example shows how to configure and add this simple text watermark to your chart. +If you are looking to add a more complex watermark then have a look at the [advanced watermark example](#advanced-watermark-example) +included below. + +## Short answer + +A simple text watermark can be configured and added by specifying the [`watermark`](/docs/api/interfaces/ChartOptions#watermark) property within +the chart options as follows: + +```js +chart.applyOptions({ + watermark: { + visible: true, + fontSize: 24, + horzAlign: 'center', + vertAlign: 'center', + color: 'rgba(171, 71, 188, 0.5)', + text: 'Watermark Example', + }, +}); +``` + +The options available for the watermark are: [Watermark Options](/docs/api/interfaces/WatermarkOptions). + +To have the watermark appear, you need to set `visible` to `true` and ensure that the `text` property isn't empty. + +You can see full [working examples](#examples) below. + +## Resources + +- [Watermark Options](/docs/api/interfaces/WatermarkOptions) + +## Examples + +import UsageGuidePartial from "./_usage-guide-partial.mdx"; + + + +import CodeBlock from "@theme/CodeBlock"; + +### Simple Watermark Example + +import codeSimple from "!!raw-loader!./watermark-simple.js"; + + + {codeSimple} + + +### Advanced Watermark Example + +If a simple text watermark doesn't meet your requirements then you can use the following tips +to rather create your own custom watermark using `html` and `css`. + +We will first set the `background` color of the chart to `transparent` so that we can +place our custom watermark underneath the chart and still see it. + +```js +chart.applyOptions({ + layout: { + // set chart background color to transparent so we can see the elements below + // highlight-next-line + background: { type: 'solid', color: 'transparent' }, + }, +}); +``` + +Next we will create a `div` element, and attach it as a child of the `container` element which is holding the chart. + +By setting the `zIndex` value for this div to be negative it will appear beneath the chart. + +We will position the div using `display: absolute` and by setting `inset: 0px` the div will fill the container. + +You can then style the div to meet your specific needs. + +```js +const container = document.getElementById('container'); +const background = document.createElement('div'); +// place below the chart +background.style.zIndex = -1; +background.style.position = 'absolute'; +// set size and position to match container +background.style.inset = '0px'; +background.style.backgroundImage = `url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyOTIiIGhlaWdodD0iMTI4IiB2aWV3Qm94PSIwIDAgMjkyIDEyOCI+PHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBkPSJtMTgyLjkzIDcuNi42My0uMzdhNjQuMSA2NC4xIDAgMCAwIDIuNDMtNS4zMWw0Ljc3LTEuMzlhNjQuNjggNjQuNjggMCAwIDEtNC43MiAxMC41NGMuMzggMTAuNDUtMy45MyAyMS4xNS0xMS4xIDI5LjM3LTExLjY2IDEzLjQxLTI2Ljk4IDE1Ljk3LTQzLjU3IDEzLjc4bDEuMDctLjk4YTIxLjEgMjEuMSAwIDAgMCAzLjcyLTQuMDUgNDguMzcgNDguMzcgMCAwIDEtMTEuMDQgMi44NGMtMTAuNjUtNS41NC0yMS42NC0xNC45NC0yNC4yNy0yNy4yNyA5LjE5LTE3IDI4Ljk1LTI0LjAxIDQ3LjM5LTE5Ljk0YTIyLjU3IDIyLjU3IDAgMCAwIDUuODYgOS4wMmMtLjEyLTEuOTItLjEtMy44NC0uMS01Ljc2bC4wMS0xLjc4YzQuOCAyLjk2IDkuNjYgNS44NSAxNS41MiA1LjcgNC4wOC0uMSA4LjQtMS41MiAxMy40LTQuNFptLTIyLjU1IDIzLjI4YTguNDggOC40OCAwIDAgMC0xMi40NS0uMzNsLTcuOS03LjI2QTguNiA4LjYgMCAwIDAgMTMyIDEyYy02LjE0IDAtMTAuMjUgNi42My03LjcgMTIuMDlsLTEzLjAyIDEyLjE5Yy00LjEtNC45Ny01LjY4LTkuMy02LjE3LTEwLjk0IDguMzYtMTMuNzIgMjQuNDYtMjAuMTggNDAuMTUtMTcuMDcgMi45MyA2LjkgOC4zOCAxMC43MiAxNC43NyAxMy45NmwtLjMzLTEuMTRjLS43NC0yLjU2LTEuNDctNS4xLTEuNjItNy43OCA3LjA1IDMuNDUgMTQuNiAzLjM1IDIxLjc2LjMxLTQuNzYgNy4yNy0xMS4xMyAxNC4yMi0xOS40NiAxNy4yNlptLTIyLjU2LTQuMTkgOC4wMyA3LjM4QTguNiA4LjYgMCAwIDAgMTU0IDQ1YTguNiA4LjYgMCAwIDAgOC4yNS0xMC41NWM3Ljk5LTMuMDggMTQuMzctOS4zOCAxOS4yOC0xNi4yMy0zLjQ3IDE5LjQ3LTIxLjk2IDM0LjYxLTQxLjkgMzIuOTggMS43Ny0yLjg0IDIuNDktNi4wNiAzLjIxLTkuMjhsLjM1LTEuNTZjLTUuNDcgMy43Ny0xMC42NyA2LjM4LTE3LjM3IDcuNTJhNDkuOSA0OS45IDAgMCAxLTExLjg1LTguNjVsMTIuODMtMTJhOC41OCA4LjU4IDAgMCAwIDExLjAyLS41NFpNMTMyIDE2YTQuNSA0LjUgMCAxIDAgMCA5IDQuNSA0LjUgMCAwIDAgMC05Wm0xNy41IDIwLjVhNC41IDQuNSAwIDEgMSA5IDAgNC41IDQuNSAwIDAgMS05IDBaTTIxLjYzIDcxLjhhMi4zMyAyLjMzIDAgMCAxIDIuMzMgMi4zNCAyLjM0IDIuMzQgMCAwIDEtMi4zMyAyLjM3IDIuMzggMi4zOCAwIDAgMS0yLjM3LTIuMzcgMi4zOCAyLjM4IDAgMCAxIDIuMzctMi4zM1ptMS43NiA4LjJ2MTZoLTMuNTJWODBoMy41MlptLTYuNDYgMTZIMi43OFY3My4yOGgzLjc1djE5LjE0aDEwLjRWOTZabTI2LjM5LTEuMDlWODBIMzkuOHYyLjE0YTYuMjYgNi4yNiAwIDAgMC01LjEyLTIuNDZjLTQuMzIgMC03LjY4IDMuNTgtNy42OCA4LjEgMCA0LjU0IDMuMzYgOC4xMiA3LjY4IDguMTIgMi4yIDAgNC4xNi0xLjA4IDUuMTItMi41djEuNDhjMCAzLjIzLTIuMTggNS00LjgzIDVhNy4wMyA3LjAzIDAgMCAxLTUuMzItMi4zNGwtMi4xNCAyLjUyYzEuNTcgMS43NiA0LjM1IDIuOTUgNy40OSAyLjk1IDQuNzMgMCA4LjMyLTIuNTMgOC4zMi04LjFabS0xMi43Ny03LjEzYTQuNyA0LjcgMCAwIDEgNC43Ny00LjkgNC43IDQuNyAwIDAgMSA0Ljc3IDQuOSA0LjcgNC43IDAgMCAxLTQuNzcgNC45IDQuNyA0LjcgMCAwIDEtNC43Ny00LjlaTTUxLjU4IDk2aC0zLjUyVjcyaDMuNTJ2MTAuMThjLjk2LTEuNiAyLjc4LTIuNSA0Ljg2LTIuNSAzLjcxIDAgNi4xMSAyLjYyIDYuMTEgNi42OVY5NmgtMy41MnYtOS4wNmMwLTIuNTItMS4yOC00LjA2LTMuMzMtNC4wNi0yLjMzIDAtNC4xMiAxLjgyLTQuMTIgNS4yNVY5NlptMjQuODYtLjJ2LTMuMTNjLS41Mi4yLTEuMjIuMzItMS45LjMyLTEuODIgMC0yLjY4LS43My0yLjY4LTIuNzJ2LTcuMTNoNC41OFY4MGgtNC41OHYtNC40NWgtMy41MlY4MGgtMy4zM3YzLjE0aDMuMzN2Ny43YzAgMy42MiAyLjQgNS4zMiA1LjQ3IDUuMzIgMS4wOSAwIDEuOTItLjEzIDIuNjMtLjM1Wm0yMC4zLjJIOTMuNGwtMy41Mi0xMC4zN0w4Ni4zOSA5NmgtMy4zMmwtNS4zOC0xNmgzLjcybDMuNDUgMTEgMy42OC0xMWgyLjY5bDMuNjUgMTEgMy40OS0xMWgzLjc0bC01LjM4IDE2Wm02Ljc2LThjMCA0Ljg2IDMuNDkgOC4zMiA4LjM1IDguMzIgMy4zNiAwIDUuODYtMS40NCA3LjMtMy43MWwtMi43LTEuOTJhNS4wMyA1LjAzIDAgMCAxLTQuNTcgMi40M2MtMi42NSAwLTQuNzctMS43My00LjkzLTQuMzVoMTIuNThjLjAzLS41MS4wMy0uOC4wMy0xLjE1IDAtNS4xNi0zLjUyLTcuOTQtNy43MS03Ljk0QTguMTIgOC4xMiAwIDAgMCAxMDMuNSA4OFptOC4yMi01LjM0YzIuMDUgMCAzLjkgMS4yNCA0LjI5IDMuNTVoLTguOWMuNDgtMi4zNyAyLjU2LTMuNTUgNC42MS0zLjU1Wm0xMy4yMi0xMC44NWEyLjMzIDIuMzMgMCAwIDEgMi4zNCAyLjMzIDIuMzQgMi4zNCAwIDAgMS0yLjM0IDIuMzcgMi4zOCAyLjM4IDAgMCAxLTIuMzctMi4zNyAyLjM4IDIuMzggMCAwIDEgMi4zNy0yLjMzWm0yMS43IDIzLjFWODBoLTMuNTN2Mi4xNGE2LjI2IDYuMjYgMCAwIDAtNS4xMi0yLjQ2Yy00LjMyIDAtNy42OCAzLjU4LTcuNjggOC4xIDAgNC41NCAzLjM2IDguMTIgNy42OCA4LjEyIDIuMiAwIDQuMTYtMS4wOCA1LjEyLTIuNXYxLjQ4YzAgMy4yMy0yLjE4IDUtNC44MyA1YTcuMDMgNy4wMyAwIDAgMS01LjMxLTIuMzRsLTIuMTUgMi41MmMxLjU3IDEuNzYgNC4zNiAyLjk1IDcuNSAyLjk1IDQuNzMgMCA4LjMxLTIuNTMgOC4zMS04LjFaTTEyNi43IDk2aC0zLjUyVjgwaDMuNTJ2MTZabTcuMTYtOC4yMmE0LjcgNC43IDAgMCAxIDQuNzctNC45IDQuNyA0LjcgMCAwIDEgNC43NyA0LjkgNC43IDQuNyAwIDAgMS00Ljc3IDQuOSA0LjcgNC43IDAgMCAxLTQuNzctNC45Wk0xNTQuOSA5NmgtMy41MlY3MmgzLjUydjEwLjE4Yy45Ni0xLjYgMi43OC0yLjUgNC44Ni0yLjUgMy43MSAwIDYuMTEgMi42MiA2LjExIDYuNjlWOTZoLTMuNTJ2LTkuMDZjMC0yLjUyLTEuMjgtNC4wNi0zLjMyLTQuMDYtMi4zNCAwLTQuMTMgMS44Mi00LjEzIDUuMjVWOTZabTI0Ljg2LS4ydi0zLjEzYy0uNTEuMi0xLjIyLjMyLTEuODkuMzItMS44MiAwLTIuNjktLjczLTIuNjktMi43MnYtNy4xM2g0LjU4VjgwaC00LjU4di00LjQ1aC0zLjUyVjgwaC0zLjMzdjMuMTRoMy4zM3Y3LjdjMCAzLjYyIDIuNCA1LjMyIDUuNDcgNS4zMiAxLjEgMCAxLjkyLS4xMyAyLjYzLS4zNVptMjEuNTkuNThhMTEuNjcgMTEuNjcgMCAwIDEtMTEuNzUtMTEuNzRjMC02LjU2IDUuMjItMTEuNzQgMTEuNzUtMTEuNzQgNC40NSAwIDguMjIgMi4yNyAxMC4yNCA1Ljc2bC0zLjIzIDEuODVhNy45NCA3Ljk0IDAgMCAwLTcuMDEtNCA3Ljk2IDcuOTYgMCAwIDAtNy45NyA4LjEzIDcuOTYgNy45NiAwIDAgMCA3Ljk3IDguMTMgNy45NCA3Ljk0IDAgMCAwIDctNGwzLjI0IDEuODVhMTEuNjYgMTEuNjYgMCAwIDEtMTAuMjQgNS43NlptMTMuNC0uMzhoMy41MnYtNy44N2MwLTMuNDMgMS44LTUuMjUgNC4xMy01LjI1IDIuMDUgMCAzLjMzIDEuNTQgMy4zMyA0LjA2Vjk2aDMuNTJ2LTkuNjNjMC00LjA3LTIuNC02LjY5LTYuMTEtNi42OS0yLjA4IDAtMy45LjktNC44NyAyLjVWNzJoLTMuNTJ2MjRabTI1LjU2LjMyYy00LjM4IDAtNy43LTMuNzQtNy43LTguMzJzMy4zMi04LjMyIDcuNy04LjMyYzIuMyAwIDQuMjMgMS4xOCA1LjEyIDIuNDZWODBoMy41MnYxNmgtMy41MnYtMi4xNGE2LjM4IDYuMzggMCAwIDEtNS4xMiAyLjQ2Wm0uNjQtMy4yYzIuODUgMCA0Ljc3LTIuMjQgNC43Ny01LjEycy0xLjkyLTUuMTItNC43Ny01LjEyYy0yLjg0IDAtNC43NiAyLjI0LTQuNzYgNS4xMnMxLjkxIDUuMTIgNC43NiA1LjEyWk0yNTMuNzEgOTZoMy41MnYtNy44YzAtMy4yIDEuODMtNC45IDMuODQtNC45LjY0IDAgMS4xNS4xIDEuNzYuMjh2LTMuNjFjLS40OC0uMS0uOTMtLjEzLTEuMzctLjEzYTQuNSA0LjUgMCAwIDAtNC4yMyAzVjgwaC0zLjUydjE2Wm0yMS43My0zLjMzdjMuMTRjLS43LjIyLTEuNTQuMzUtMi42My4zNS0zLjA3IDAtNS40Ny0xLjctNS40Ny01LjMxdi03LjcxaC0zLjMzVjgwaDMuMzN2LTQuNDVoMy41MlY4MGg0LjU4djMuMTRoLTQuNTh2Ny4xM2MwIDEuOTkuODYgMi43MiAyLjY5IDIuNzIuNjcgMCAxLjM3LS4xMyAxLjg5LS4zMlptMTQuMjEtMS4zMWMwLTIuNjItMS42Ni00LjAzLTQuNDgtNC44NmwtMS42My0uNDhjLTEuNTctLjQ1LTEuOTItMS4xMi0xLjkyLTEuOSAwLS45NSAxLjA5LTEuNSAyLjE1LTEuNSAxLjMgMCAyLjMzLjY0IDMuMDQgMS42NGwyLjQzLTEuODZjLTEuMTItMS43Ni0zLjAxLTIuNzItNS40MS0yLjcyLTMuMiAwLTUuNyAxLjczLTUuNzMgNC41OC0uMDMgMi4zNiAxLjQxIDQuMTIgNC4yIDQuOWwxLjQuMzhjMS45Mi41NyAyLjQ3IDEuMTIgMi40NyAyLjA0IDAgMS4xMi0xLjA2IDEuNy0yLjMgMS43LTEuNjQgMC0zLjItLjgtMy44NS0yLjJsLTIuNTkgMS44NWMxLjE1IDIuMjcgMy41OCAzLjM5IDYuNDMgMy4zOSAzLjMgMCA1LjgtMS44OSA1LjgtNC45NlptLTE0My4zOCAyMS40YzAgLjQ2LS4zNy44NC0uODMuODRhLjg2Ljg2IDAgMCAxLS44Ny0uODVjMC0uNDYuMzktLjg1Ljg3LS44NS40NiAwIC44My4zOS44My44NVptLS4yOSAxMS4yNGgtMS4xMnYtOGgxLjEydjhabS01Mi4wMi4xNmE0LjA0IDQuMDQgMCAwIDAgMy45OC00LjE2IDQuMDQgNC4wNCAwIDAgMC0zLjk4LTQuMTZjLTEuMjQgMC0yLjM5LjY0LTIuOTYgMS41VjExMmgtMS4xMnYxMkg5MXYtMS4zNGMuNTcuODYgMS43MiAxLjUgMi45NiAxLjVabS0uMTItMS4wNGMtMS43NCAwLTIuOTQtMS40LTIuOTQtMy4xMiAwLTEuNzMgMS4yLTMuMTIgMi45NC0zLjEyIDEuNzUgMCAyLjk1IDEuNCAyLjk1IDMuMTIgMCAxLjczLTEuMiAzLjEyLTIuOTUgMy4xMlptNy45IDQuMjIgNS4zLTExLjM0aC0xLjI2bC0yLjkzIDYuMzUtMi45My02LjM1aC0xLjI0bDMuNTUgNy42LTEuNzYgMy43NGgxLjI2Wk0xMTUuMyAxMjRoLTEuMnYtMTAuMmgtMy42OHYtMS4xNmg4LjU2djEuMTVoLTMuNjhWMTI0Wm0zLjgyIDBoMS4xMnYtNC4wMmMwLTIuMDQgMS4yMy0yLjk0IDIuMjItMi45NC4yNCAwIC40NS4wMy42Ny4xMXYtMS4xN2EyLjQ0IDIuNDQgMCAwIDAtMi45IDEuNjZWMTE2aC0xLjExdjhabTExLjcyLTEuMzRhMy42NCAzLjY0IDAgMCAxLTIuOTYgMS41IDQuMDQgNC4wNCAwIDAgMS0zLjk4LTQuMTYgNC4wNCA0LjA0IDAgMCAxIDMuOTgtNC4xNmMxLjIzIDAgMi4zOS42NCAyLjk2IDEuNVYxMTZoMS4xMnY4aC0xLjEydi0xLjM0Wm0tNS44LTIuNjZjMCAxLjczIDEuMiAzLjEyIDIuOTUgMy4xMiAxLjc1IDAgMi45NS0xLjQgMi45NS0zLjEyIDAtMS43My0xLjItMy4xMi0yLjk1LTMuMTItMS43NCAwLTIuOTQgMS40LTIuOTQgMy4xMlptMTIuOTggNC4xNmMxLjIzIDAgMi4zOS0uNjQgMi45Ni0xLjVWMTI0aDEuMTJ2LTEySDE0MXY1LjM0YTMuNjQgMy42NCAwIDAgMC0yLjk2LTEuNSA0LjA0IDQuMDQgMCAwIDAtMy45OCA0LjE2IDQuMDQgNC4wNCAwIDAgMCAzLjk4IDQuMTZabS4xMS0xLjA0Yy0xLjc0IDAtMi45NC0xLjQtMi45NC0zLjEyIDAtMS43MyAxLjItMy4xMiAyLjk0LTMuMTIgMS43NSAwIDIuOTUgMS40IDIuOTUgMy4xMiAwIDEuNzMtMS4yIDMuMTItMi45NSAzLjEyWm0xMC42Ljg4aDEuMTF2LTMuOThjMC0xLjk5IDEuMS0zLjE0IDIuNS0zLjE0IDEuMTkgMCAyLjAyLjg2IDIuMDIgMi4yN1YxMjRoMS4xMnYtNWMwLTEuOTYtMS4yNy0zLjE2LTMuMDEtMy4xNi0xLjA0IDAtMi4wNS40NS0yLjYzIDEuNVYxMTZoLTEuMTF2OFptMTYuNzEtLjQyYzAgMi42MS0xLjcyIDMuOTItMy45NSAzLjkyLTEuODQgMC0zLjE3LS44My0zLjc3LTEuNzRsLjg4LS43NWEzLjQgMy40IDAgMCAwIDIuOSAxLjQ1YzEuMzcgMCAyLjgyLS44MyAyLjgyLTIuOTR2LTEuMDJjLS41Ny44Ni0xLjcgMS41LTIuOTIgMS41YTMuOTQgMy45NCAwIDAgMS0zLjk2LTQuMDggMy45NCAzLjk0IDAgMCAxIDMuOTYtNC4wOGMxLjIzIDAgMi4zNS42NCAyLjkyIDEuNVYxMTZoMS4xMnY3LjU4Wm0tNi44NC0zLjY2YzAgMS43MyAxLjE2IDMuMDQgMi45IDMuMDQgMS43NSAwIDIuOTItMS4zMSAyLjkyLTMuMDRzLTEuMTctMy4wNC0yLjkxLTMuMDRjLTEuNzUgMC0yLjkxIDEuMzEtMi45MSAzLjA0Wm0xMy41NSA0LjA4IDQuODgtMTEuMzZoLTEuMzVsLTQuMDMgOS4zOC00LjAzLTkuMzhoLTEuMzZsNC45IDExLjM2aC45OVptNy44NC0xMS4yNWMwIC40Ny0uMzcuODUtLjgzLjg1YS44Ni44NiAwIDAgMS0uODYtLjg1YzAtLjQ2LjM4LS44NS44Ni0uODUuNDcgMCAuODMuMzkuODMuODVabS0uMjggMTEuMjVoLTEuMTN2LThoMS4xM3Y4Wm02LjIuMTZhMy45IDMuOSAwIDAgMCAzLjU2LTEuOTVsLS45MS0uNmEyLjc4IDIuNzggMCAwIDEtMi42NCAxLjUxIDIuODcgMi44NyAwIDAgMS0yLjk2LTIuOTNoNi43NXYtLjNjLS4wMi0yLjU2LTEuNjgtNC4wNS0zLjc2LTQuMDVhNC4wNSA0LjA1IDAgMCAwLTQuMTUgNC4xNmMwIDIuMyAxLjYgNC4xNiA0LjEyIDQuMTZabS0uMDEtNy4yOGMxLjM0IDAgMi40NS44OCAyLjY0IDIuMzJoLTUuNDlhMi44NCAyLjg0IDAgMCAxIDIuODUtMi4zMlptMTMuNTUgNy4xMmgtLjkzbC0yLjEtNi4xLTIuMTQgNi4xaC0uOTJsLTIuNzQtOGgxLjE1bDIuMDggNi4wOCAyLjExLTYuMDhoLjg3bDIuMTEgNi4wOCAyLjA4LTYuMDhoMS4xN2wtMi43NCA4WiIgZmlsbD0iY3VycmVudENvbG9yIj48L3BhdGg+PC9zdmc+")`; +background.style.backgroundRepeat = 'no-repeat'; +background.style.backgroundPosition = 'center'; +background.style.opacity = '0.5'; +container.appendChild(background); +``` + +import codeAdvanced from "!!raw-loader!./watermark-advanced.js"; + + + {codeAdvanced} + diff --git a/website/tutorials/index.mdx b/website/tutorials/index.mdx index cc0959a482..f6b6fab0d7 100644 --- a/website/tutorials/index.mdx +++ b/website/tutorials/index.mdx @@ -1,23 +1,99 @@ --- +sidebar_position: 0 pagination_next: null --- +import CardLinkList from "@site/src/components/CardLinkList"; +import Shapes from "@site/src/img/shapes.svg"; +import ReactLogo from "@site/src/img/react.svg"; +import VuejsLogo from "@site/src/img/vuejs.svg"; +import WebComponentsLogo from "@site/src/img/webcomponents.svg"; + # Tutorials -:::caution -These tutorials are for the latest published version of Lightweight Charts. -::: +import VersionWarningAdmonition from "@site/src/components/VersionWarningAdmonition"; + + + + + + +## Guides + +, + description: "Customizing appearance & features", + }, + ]} +/> + +## Framework integrations -:::info This section contains some tutorials how to use Lightweight Charts with some popular frameworks. -If you think that a tutorial is missing feel free to ask [in the discussions](https://github.com/tradingview/lightweight-charts/discussions) or submit your own. + +, + description: "Integration guide for React", + }, + { + href: "/tutorials/vuejs/wrapper", + title: "Vue.js", + image: , + description: "Integration guide for Vue.js", + }, + { + href: "/tutorials/webcomponents/custom-element", + title: "Web Components", + image: , + description: "Web components custom element", + }, + ]} +/> + +:::info + +If you think that a tutorial is missing feel free to ask [in the discussions](https://github.com/tradingview/lightweight-charts/discussions) +or submit your own. + ::: -import DocCardList from '@theme/DocCardList'; -import { useDocsSidebar } from '@docusaurus/theme-common'; +## How To / Examples + +A collection of code examples showcasing the various capabilities of the library, and how to implement common additional features. + +import { useDocsSidebar } from "@docusaurus/theme-common/internal"; +export const ExamplesList = () => { + const examplesCategory = useDocsSidebar().items.find( + item => item.type === "category" && item.label === "How To / Examples" + ); + const examples = examplesCategory.items.filter(doc => doc.type === "link"); + return ( + + ); +}; - x.docId !== 'index') -} /> + + +:::tip + +More examples can be viewed on the [Lightweight Charts product page](https://www.tradingview.com/lightweight-charts/). + +::: diff --git a/website/tutorials/react/01-simple.mdx b/website/tutorials/react/01-simple.mdx index 7d8c462dce..3e817938ae 100644 --- a/website/tutorials/react/01-simple.mdx +++ b/website/tutorials/react/01-simple.mdx @@ -37,18 +37,24 @@ This will create a web page accessible by default on . The example _React component_ on this page may not fit your requirements completely. Creating a general purpose declarative wrapper for Lightweight Charts' imperative API is a challenge, but hopefully you can adapt this example to your use case. +:::info + +For this example we are using props to set chart colors based on the current theme (light or dark). In your real code it might be a better idea to use a [Context](https://reactjs.org/docs/context.html#when-to-use-context). +::: + +import { ThemedChart } from '@site/src/components/tutorials/themed-chart-colors-wrapper'; import CodeBlock from '@theme/CodeBlock'; -import code from '!!raw-loader!./_simple-react-example'; +import code from '!!raw-loader!@site/src/components/tutorials/simple-react-example'; -{code} +{code} and you'll have a reusable component that could then be enhanced, tweaked to meet your needs, adding properties and even functionalities. If you've successfully followed all the steps you should see something similar to -import { App } from './_simple-react-example'; +import { App } from '@site/src/components/tutorials/simple-react-example'; import styles from '@site/src/pages/chart.module.css';
- +
diff --git a/website/tutorials/react/02-advanced.mdx b/website/tutorials/react/02-advanced.mdx index 18cc8dfedf..9c1eecfc56 100644 --- a/website/tutorials/react/02-advanced.mdx +++ b/website/tutorials/react/02-advanced.mdx @@ -120,10 +120,10 @@ The same technique will be used within the Series component to handle this time Moreover those 2 "main" components will "expose" whatever functions the user wants from the internal reference object at a higher level, meaning once those references are accessible any other component would then be able to act on either the Chart or any Series. -Here's a squeleton of what the final structure would be like: +Here's a skeleton of what the final structure would be like: ```js -import React, { useEffect, useImperativeHandle, useRef } from 'react'; +import React, { useEffect, useImperativeHandle, useRef, createContext, forwardRef } from 'react'; const Context = createContext(); @@ -212,19 +212,25 @@ ChildComponent.displayName = 'ChildComponent'; By considering all the above you could end up with Chart/Series components looking like the following +:::info + +For this example we are using props to set chart colors based on the current theme (light or dark). In your real code it might be a better idea to use a [Context](https://reactjs.org/docs/context.html#when-to-use-context). +::: + +import { ThemedChart } from '@site/src/components/tutorials/themed-chart-colors-wrapper'; import CodeBlock from '@theme/CodeBlock'; -import Code from '!!raw-loader!./_advanced-react-example'; +import code from '!!raw-loader!@site/src/components/tutorials/advanced-react-example'; -{Code} +{code} The code above will produce a line series. Given a `series1` reference is created to be passed to the Series component you could reuse that object via `series1.current.[any function applicable on Series]`. For instance and as shown below `series1.current.update(new data)` is used upon clicking on the button. -import { App } from './_advanced-react-example'; +import { App } from '@site/src/components/tutorials/advanced-react-example'; import styles from '@site/src/pages/chart.module.css';
- +
diff --git a/website/tutorials/vuejs/01-wrapper.mdx b/website/tutorials/vuejs/01-wrapper.mdx new file mode 100644 index 0000000000..9dc15489c1 --- /dev/null +++ b/website/tutorials/vuejs/01-wrapper.mdx @@ -0,0 +1,351 @@ +--- +title: Vue.js - Wrapper Component +description: + A simple example of how to use Lightweight Charts within the Vue.js framework. +pagination_prev: null +pagination_next: null +keywords: + - vue + - vue.js + - example +--- + +# Vue.js - Wrapper Component + +:::info + +The following describes a relatively simple example that only allows for a +single [series](/docs/series-types) to be rendered. This example can be used as +a starting point and could be tweaked further using our extensive +[API](/docs/api). + +**Please note: this example is intended to be used with Vue.js 3** + +::: + +This guide will focus on the key concepts required to get Lightweight Charts +running within a Vue component. Please note this guide is not intended as a +complete step-by-step tutorial. The example Vue components can be found at the +[bottom](#complete-sample-code) of this guide. + +If you are new to Vue.js then please have a look at the +[official Vue.js tutorials](https://vuejs.org/guide/introduction.html) before +proceeding further with this example. + +## About the example wrapper component + +The example Vue wrapper component has the following features. + +The ability to: + +- specify the series type via a component attribute, +- specify the series data via a component property, +- control the chart, series, time scale, and price scale options via properties, +- enable automatic resizing of the chart when the browser is resized. + +The example may not fit your requirements completely. Creating a general-purpose +declarative wrapper for Lightweight Charts' imperative API is a challenge, but +hopefully, you can adapt this example to your use case. + +### Component showcase + +Presented below is the finished wrapper component which is discussed throughout +this guide. The interactive buttons beneath the chart are showcasing how to +interact with the component and that code is provided below as well (within the +example app component). + +import BrowserOnly from '@docusaurus/BrowserOnly'; + +Loading...
}> + {() => { + require('./assets/web-component.js'); + return ; + }} + + +### Vue API styles + +Vue components can be authored in two different +[API styles](https://vuejs.org/guide/introduction.html#api-styles): _Options +API_ and _Composition API_. + +This example will make use of the **Composition API**, but complete code +examples for both APIs will be presented at the end of the tutorial. + +The Vue component could be used within any Vue setup, you can learn more on the +Vue documentation site: +[Ways of Vue](https://vuejs.org/guide/extras/ways-of-using-vue.html) + +## Integrating Lightweight Charts with Vue + +### Avoid using `Refs` for storing API instances + +The preferred way to store a reference to the created chart +([IChartApi](/docs/api/interfaces/IChartApi) instance), or any other of the +library's instances, is to make use of a plain JS variable or class field +instead of using Vue's [`ref`](https://vuejs.org/api/reactivity-core.html#ref) +functionality. + +When Vue wraps an object in a reference object, it modifies the object (to +enable reactivity) in such a way that it interferes with the internal logic of +the Lightweight Chart. This can lead to unexpected behaviour. If you really need +to use a [`ref`](https://vuejs.org/api/reactivity-core.html#ref) then please +consider using +[`shallowRef`](https://vuejs.org/api/reactivity-advanced.html#shallowref) +instead. + +We can instead create a variable to hold these instances outside of any vue +hooks (such as +[`onMounted`](https://vuejs.org/api/composition-api-lifecycle.html#onmounted), +[`watch`](https://vuejs.org/api/reactivity-core.html#watch)) within the body of +the script. + +```html + +``` + +### Use the `onMounted` lifecycle hook to create the chart + +Lightweight Charts requires an html element to use as its container, you can +create a simple div element within the component's `template` and ask Vue to +create a reference to that element by adding the `ref="chartContainer"` +attribute to the div element and the corresponding variable within the script +section: `const chartContainer = ref();` + +The ideal time to create the chart is during the `mounted` lifecycle hook +provided by the Vue component. The container div will be created and ready for +use at this stage. Within the +[`onMounted`](https://vuejs.org/api/composition-api-lifecycle.html#onmounted) +hook we can call Lightweight Charts' [`createChart`](/docs/api#createchart) +constructor and pass it the value of the container reference (which is the div +element). + +:::tip + +Remember to also clean up when the component is unmounted +([`onUnmounted`](https://vuejs.org/api/composition-api-lifecycle.html#onunmounted) +hook) by calling the [`remove`](/docs/api/interfaces/IChartApi#remove) method on +the saved chart instance. + +::: + +```html + + + +``` + +### Providing option properties + +A simple way to provide customisation of the chart to the component's consumers +is to create component properties for the options you wish to be customised. +Lightweight Charts has a variety of customisation options which can be applied +through the [`applyOptions`](/docs/api/interfaces/IChartApi#applyOptions) method +on an Api instance (such as [IChartApi](/docs/api/interfaces/IChartApi), +[ISeriesApi](/docs/api/interfaces/ISeriesApi), +[IPriceScaleApi](/docs/api/interfaces/IPriceScaleApi), and +[ITimeScaleApi](/docs/api/interfaces/ITimeScaleApi)). + +We can define properties for use as the components API as follows: + +```html + +``` + +These properties can be used during the creation of Api instances, for example: + +```js +chart = createChart(chartContainer.value, props.chartOptions); +``` + +We can instruct Vue to +[`watch`](https://vuejs.org/api/reactivity-core.html#watch) these properties for +changes and allow us to provide code to react to these changes. Using this +mechanism, we can provide a direct mapping between the options properties and +the `applyOptions` methods on the instance. This allows the consumer of the +component to apply changes to the current options at any point during the +lifecycle of the chart. + +:::info + +Please note: the current options aren't reset when applying the new options, and +the new options can be a partial object. Thus it is possible to change one +option at a time while still keeping the current options. + +::: + +```js +watch( + () => props.chartOptions, + newOptions => { + if (!chart) { + return; + } + chart.applyOptions(newOptions); + } +); + +watch( + () => props.priceScaleOptions, + newOptions => { + if (!chart) { + return; + } + chart.priceScale().applyOptions(newOptions); + } +); +``` + +### Exposing the chart instance or additional methods + +There may be cases where you want to provide access to the chart instance, or +provide useful methods, to the consumer of the component. This can be achieved +with the +[`defineExpose`](https://vuejs.org/api/sfc-script-setup.html#defineexpose) hook +provided by Vue. + +```js +import { defineExpose } from 'vue'; + +// A simple method to call `fitContent` on the time scale +const fitContent = () => { + if (!chart) { + return; + } + chart.timeScale().fitContent(); +}; + +// Expose the chart instance via a method +const getChart = () => chart; + +defineExpose({ fitContent, getChart }); +``` + +The consumer of the component can create a reference to a specific instance of +the component and use the reference's value to evoke one of the exposed methods. + +```html + + +``` + +## Complete Sample Code + +Presented below is the complete component source code for the Vue components. We +have also provided a sample Vue App component which showcases how to make use of +these components within a typical Vue application. + +You can view a complete Vue project using these components at this +[StackBlitz example](https://stackblitz.com/edit/vitejs-vite-r4bbai?file=src/App.vue). + +### Composition API + +The following code block contains the source code for the sample Vue component +using the Composition API. + +

Download file

+ +import CodeBlock from '@theme/CodeBlock'; +import InstantDetails from '@site/src/components/InstantDetails'; +import compositionCode from '!!raw-loader!./assets/composition-api.vue'; + + + Click here to reveal the code. + {compositionCode} + + +### Options API + +The following code block contains the source code for the sample Vue component +using the Options API. + +

Download file

+ +import optionsCode from '!!raw-loader!./assets/options-api.vue'; + + + Click here to reveal the code. + {optionsCode} + + +### Example Vue App Component + +The following code block contains the source code for a sample Vue Application +component which makes use of the Vue components shown above. It showcases a few +ways to control and interact with the component. + +

Download file

+ +import appCode from '!!raw-loader!./assets/app.vue'; + + + Click here to reveal the code. + {appCode} + diff --git a/website/tutorials/vuejs/_category_.yml b/website/tutorials/vuejs/_category_.yml new file mode 100644 index 0000000000..3944463aad --- /dev/null +++ b/website/tutorials/vuejs/_category_.yml @@ -0,0 +1 @@ +label: "Vue.js" diff --git a/website/tutorials/vuejs/assets/.eslintrc.js b/website/tutorials/vuejs/assets/.eslintrc.js new file mode 100644 index 0000000000..ea162df0bf --- /dev/null +++ b/website/tutorials/vuejs/assets/.eslintrc.js @@ -0,0 +1,6 @@ +module.exports = { + globals: { + document: false, + window: false, + }, +}; diff --git a/website/tutorials/vuejs/assets/app.vue b/website/tutorials/vuejs/assets/app.vue new file mode 100644 index 0000000000..ec5d13d7c6 --- /dev/null +++ b/website/tutorials/vuejs/assets/app.vue @@ -0,0 +1,174 @@ + + + + diff --git a/website/tutorials/vuejs/assets/composition-api.vue b/website/tutorials/vuejs/assets/composition-api.vue new file mode 100644 index 0000000000..44391e12ba --- /dev/null +++ b/website/tutorials/vuejs/assets/composition-api.vue @@ -0,0 +1,188 @@ + + + + + diff --git a/website/tutorials/vuejs/assets/options-api.vue b/website/tutorials/vuejs/assets/options-api.vue new file mode 100644 index 0000000000..ed8480f7eb --- /dev/null +++ b/website/tutorials/vuejs/assets/options-api.vue @@ -0,0 +1,157 @@ + + + + + diff --git a/website/tutorials/vuejs/assets/web-component.js b/website/tutorials/vuejs/assets/web-component.js new file mode 100644 index 0000000000..72547abbb4 --- /dev/null +++ b/website/tutorials/vuejs/assets/web-component.js @@ -0,0 +1,380 @@ +/* + * This is the Web Component version of the App Vue component + * + * This WC is used as the on-page example for the Vue tutorial, and includes + * some specific logic for reacting to the current docusaurus theme and adjusting the + * chart colours as required. + */ +import { defineCustomElement } from 'vue/dist/vue.esm-bundler'; +import { createChart, ColorType } from 'lightweight-charts'; +import { themeColors } from '../../../theme-colors'; + +let series; +let chart; + +function getChartSeriesConstructorName(type) { + return `add${type.charAt(0).toUpperCase() + type.slice(1)}Series`; +} + +const addSeriesAndData = (type, seriesOptions, data) => { + const seriesConstructor = getChartSeriesConstructorName(type); + series = chart[seriesConstructor](seriesOptions); + series.setData(data); +}; + +const resizeHandler = container => { + if (!chart || !container) { + return; + } + const dimensions = container.getBoundingClientRect(); + chart.resize(dimensions.width, dimensions.height); +}; + +const LWChart = { + props: { + type: { + type: String, + default: 'line', + }, + data: { + type: Array, + required: true, + }, + autosize: { + default: true, + type: Boolean, + }, + chartOptions: { + type: Object, + }, + seriesOptions: { + type: Object, + }, + timeScaleOptions: { + type: Object, + }, + priceScaleOptions: { + type: Object, + }, + }, + template: `
`, + mounted() { + chart = createChart(this.$refs.lightweightChart, this.chartOptions); + addSeriesAndData(this.type, this.seriesOptions, this.data); + + if (this.priceScaleOptions) { + chart.priceScale().applyOptions(this.priceScaleOptions); + } + + if (this.timeScaleOptions) { + chart.timeScale().applyOptions(this.timeScaleOptions); + } + + chart.timeScale().fitContent(); + + if (this.autosize) { + window.addEventListener('resize', () => + resizeHandler(this.$refs.lightweightChart) + ); + } + }, + unmounted() { + if (chart) { + chart.remove(); + chart = null; + } + if (series) { + series = null; + } + }, + watch: { + autosize(enabled) { + if (!enabled) { + window.removeEventListener('resize', () => + resizeHandler(this.$refs.lightweightChart) + ); + return; + } + window.addEventListener('resize', () => + resizeHandler(this.$refs.lightweightChart) + ); + }, + type() { + if (series && chart) { + chart.removeSeries(series); + } + addSeriesAndData(this.type, this.seriesOptions, this.data); + }, + data(newData) { + if (!series) { + return; + } + series.setData(newData); + }, + chartOptions(newOptions) { + if (!chart) { + return; + } + chart.applyOptions(newOptions); + }, + seriesOptions(newOptions) { + if (!series) { + return; + } + series.applyOptions(newOptions); + }, + priceScaleOptions(newOptions) { + if (!chart) { + return; + } + chart.priceScale().applyOptions(newOptions); + }, + timeScaleOptions(newOptions) { + if (!chart) { + return; + } + chart.timeScale().applyOptions(newOptions); + }, + }, + methods: { + fitContent() { + if (!chart) { + return; + } + chart.timeScale().fitContent(); + }, + getChart: () => chart, + }, + expose: ['fitContent', 'getChart'], +}; + +function generateSampleData(ohlc) { + const randomFactor = 25 + Math.random() * 25; + const samplePoint = i => + i * + (0.5 + + Math.sin(i / 10) * 0.2 + + Math.sin(i / 20) * 0.4 + + Math.sin(i / randomFactor) * 0.8 + + Math.sin(i / 500) * 0.5) + + 200; + + const res = []; + const date = new Date(Date.UTC(2018, 0, 1, 0, 0, 0, 0)); + const numberOfPoints = ohlc ? 100 : 500; + for (let i = 0; i < numberOfPoints; ++i) { + const time = date.getTime() / 1000; + const value = samplePoint(i); + if (ohlc) { + const randomRanges = [ + -1 * Math.random(), + Math.random(), + Math.random(), + ].map(j => j * 10); + const sign = Math.sin(Math.random() - 0.5); + res.push({ + time, + low: value + randomRanges[0], + high: value + randomRanges[1], + open: value + sign * randomRanges[2], + close: samplePoint(i + 1), + }); + } else { + res.push({ + time, + value, + }); + } + + date.setUTCDate(date.getUTCDate() + 1); + } + + return res; +} + +function randomShade() { + return Math.round(Math.random() * 255); +} + +function randomColor(alpha = 1) { + return `rgba(${randomShade()}, ${randomShade()}, ${randomShade()}, ${alpha})`; +} + +const colorsTypeMap = { + area: [ + ['topColor', 0.4], + ['bottomColor', 0], + ['lineColor', 1], + ], + bar: [ + ['upColor', 1], + ['downColor', 1], + ], + baseline: [ + ['topFillColor1', 0.28], + ['topFillColor2', 0.05], + ['topLineColor', 1], + ['bottomFillColor1', 0.28], + ['bottomFillColor2', 0.05], + ['bottomLineColor', 1], + ], + candlestick: [ + ['upColor', 1], + ['downColor', 1], + ['borderUpColor', 1], + ['borderDownColor', 1], + ['wickUpColor', 1], + ['wickDownColor', 1], + ], + histogram: [['color', 1]], + line: [['color', 1]], +}; + +const checkPageTheme = () => + document.documentElement.getAttribute('data-theme') === 'dark'; + +const VueExample = defineCustomElement({ + components: { + LWChart, + }, + data: () => ({ + chartOptions: { + layout: { + background: { + color: 'transparent', + type: ColorType.Solid, + }, + }, + }, + dataset: generateSampleData(false), + seriesOptions: {}, + chartType: 'area', + }), + template: ` +
+ +
+ + + + `, + styles: [ + ` + button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.5em 1em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: var(--hero-button-background-color-active, #e9e9e9); + color: var(--hero-button-text-color, #e9e9e9); + cursor: pointer; + transition: border-color 0.25s; + margin-left: 0.5em; + } + button:hover { + border-color: #3179F5; + background-color: var(--hero-button-background-color-hover); + color: var(--hero-button-text-color-hover-active); + } + button:focus, + button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; + } + + .chart-container { + height: var(--lwchart-height, 300px); + } + + .lw-chart { + height: 100%; + } + `, + ], + mounted() { + this.changeChartTheme(checkPageTheme()); + + if (window.MutationObserver) { + const callback = _ => { + this.changeChartTheme(checkPageTheme()); + }; + this.observer = new window.MutationObserver(callback); + this.observer.observe(document.documentElement, { attributes: true }); + } + }, + unmounted() { + if (this.observer) { + this.observer.disconnect(); + } + }, + methods: { + changeColors() { + const options = {}; + const colorsToSet = colorsTypeMap[this.chartType]; + colorsToSet.forEach(c => { + options[c[0]] = randomColor(c[1]); + }); + this.seriesOptions = options; + }, + changeData() { + const candlestickTypeData = ['candlestick', 'bar'].includes( + this.chartType + ); + const newData = generateSampleData(candlestickTypeData); + this.dataset = newData; + if (this.chartType === 'baseline') { + const average = + newData.reduce((s, c) => s + c.value, 0) / newData.length; + this.seriesOptions = { + baseValue: { type: 'price', price: average }, + }; + } + }, + changeType() { + const types = [ + 'line', + 'area', + 'baseline', + 'histogram', + 'candlestick', + 'bar', + ].filter(t => t !== this.chartType); + const randIndex = Math.round(Math.random() * (types.length - 1)); + this.chartType = types[randIndex]; + this.changeData(); + + // call a method on the component. + this.$refs.lwChart.fitContent(); + }, + changeChartTheme(isDark) { + const theme = isDark ? themeColors.DARK : themeColors.LIGHT; + const gridColor = isDark ? '#424F53' : '#D6DCDE'; + this.chartOptions = { + layout: { + textColor: theme.CHART_TEXT_COLOR, + background: { + color: theme.CHART_BACKGROUND_COLOR, + }, + }, + grid: { + vertLines: { + color: gridColor, + }, + horzLines: { + color: gridColor, + }, + }, + }; + }, + }, +}); + +window.customElements.define('vue-example', VueExample); diff --git a/website/tutorials/webcomponents/01-custom-element.mdx b/website/tutorials/webcomponents/01-custom-element.mdx new file mode 100644 index 0000000000..0234080ab8 --- /dev/null +++ b/website/tutorials/webcomponents/01-custom-element.mdx @@ -0,0 +1,560 @@ +--- +title: Web Components - Custom Element +description: + A simple example of how to use Lightweight Charts within the Web component + custom element. +pagination_prev: null +pagination_next: null +keywords: + - web component + - custom element + - example +--- + +# Web Components - Custom Element + +:::info + +The following describes a relatively simple example that only allows for a +single [series](/docs/series-types) to be rendered. This example can be used as +a starting point, and could be tweaked further using our extensive +[API](/docs/api). + +::: + +This guide will focus on the key concepts required to get Lightweight Charts +running within a Vanilla JS web component (using +[custom elements](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_custom_elements)). +Please note this guide is not intended as a complete step-by-step tutorial. The +example web component custom element can be found at the +[bottom](#complete-sample-code) of this guide. + +If you are new to Web Components then please have a look at the following +resources before proceeding further with this example. + +- [MDN: Using Custom Elements](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_custom_elements) +- [Custom Elements Best Practices](https://web.dev/custom-elements-best-practices/) +- [Open Web Components](https://open-wc.org) + +## About the example custom element + +The example Web Components custom element has the following features. + +The ability to: + +- specify the series type via a component attribute, +- specify the series data via a component property, +- control the chart, series, time scale, and price scale options via properties, +- enable automatic resizing of the chart when the browser is resized. + +The example may not fit your requirements completely. Creating a general-purpose +declarative wrapper for Lightweight Charts' imperative API is a challenge, but +hopefully, you can adapt this example to your use case. + +### Component showcase + +Presented below is the finished wrapper custom element which is discussed +throughout this guide. The interactive buttons beneath the chart are showcasing +how to interact with the component and that code is provided below as well +(within the example app custom element). + +import BrowserOnly from '@docusaurus/BrowserOnly'; + +Loading...
}> + {() => { + require('./assets/wc-example.js'); + return ; + }} + + +## Creating the chart + +Web Components are a suite of different technologies which allow you to +encapsulate functionality within custom elements. +[Custom elements](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_custom_elements) +make use of the standard web languages `html`, `css`, and `js` which means that +there aren't many specific changes, or extra knowledge, required to get +Lightweight Charts working within a custom element. + +The process of creating a chart is essentially the same as when using the +library normally, except that we are encapsulating all the `html`, `css`, and +`js` code specific to the chart within our custom element. + +Starting with a simple boilerplate custom element, as shown below: + +```js +(function() { + class LightweightChartWC extends HTMLElement { + connectedCallback() { + this.attachShadow({ mode: 'open' }); + } + + disconnectedCallback() {} + } + + // Register our custom element with a specific tag name. + window.customElements.define('lightweight-chart', LightweightChartWC); +})(); +``` + +The first step is to define the `html` for the custom element. For Lightweight +Charts, all we need to do is create a `div` element to act as our container +element. You can create the html by cloning a `template` (as seen in our usage +example below) or by imperatively using the DOM JS api as shown below: + +```js +// hide-start +class LightweightChartWC extends HTMLElement { + // ... + // hide-end + // Within the class definition + connectedCallback() { + // Create the div container for the chart + const container = document.createElement('div'); + container.setAttribute('class', 'chart-container'); + + this.shadowRoot.append(container); + } + // hide-line +} +``` + +Next we will want to define some basic styles to ensure that the container +element fills the available space and that the element can be hidden using the +`hidden` attribute. + +```js +// Outside of the Class definition +const elementStyles = ` + :host { + display: block; + } + :host[hidden] { + display: none; + } + .chart-container { + height: 100%; + width: 100%; + } +`; + +// ... + +// hide-start +class LightweightChartWC extends HTMLElement { + // ... + // hide-end + // Within the class definition + connectedCallback() { + // highlight-fade-start + // Create the div container for the chart + const container = document.createElement('div'); + container.setAttribute('class', 'chart-container'); + // highlight-fade-end + // create the stylesheet for the custom element + const style = document.createElement('style'); + style.textContent = elementStyles; + this.shadowRoot.append(style, container); + } + // hide-line +} +``` + +Finally, we can now create the chart using Lightweight Charts. Depending on your +build process, you may either need to import Lightweight Charts, or access it +from the global scope (if loaded as a standalone script). To create the chart, +we call the [`createChart`](/docs/api#createchart) constructor function, passing +our container element as the first argument. The returned variable will be a +[`IChartApi`](/docs/api/interfaces/IChartApi) instance which we can use as shown +in the API documentation. The IChartApi instance provides all the required +functionality to create series, assign data, and more. See our +[Getting started](/docs) guide for a quick example. + +```js +// hide-start +class LightweightChartWC extends HTMLElement { + // ... + // hide-end + connectedCallback() { + // highlight-fade-start + // Create the div container for the chart + const container = document.createElement('div'); + container.setAttribute('class', 'chart-container'); + + // create the stylesheet for the custom element + const style = document.createElement('style'); + style.textContent = elementStyles; + this.shadowRoot.append(style, container); + // highlight-fade-end + + // Create the Lightweight Chart + this.chart = createChart(container); + } + // hide-line +} +``` + +## Attributes and properties + +Whilst we could encapsulate everything required to create a chart within the +custom element, generally we wish to allow further customisation of the chart to +the consumers of the custom element. Attributes and properties are a great way +to provide this 'API' to the consumer. + +As a general rule of thumb, it is better to use attributes for options which are +defined using simple values (number, string, boolean), and properties for rich +data types. + +In our example, we will be using attributes for the series type option (type) +and the autosize flag which enables automatic resizing of the chart when the +window is resized. We will be using properties to provide the interfaces for +setting the series data, and options for the chart. Additionally, the IChartApi +instance will be accessable via the `chart` property such that the consumer has +full access to the entire API provided by Lightweight Charts. + +### Attributes + +Attributes for the custom element can be set directly on the custom element +(within the html), or via javascript as seen for the properties in the next +section. + +```html + +``` + +Attributes can be set and read from within the custom element's definition as +follows: + +```js +// read `type` attribute +const type = this.getAttribute('type'); + +// set `type` attribute +this.setAttribute('type', 'line'); +``` + +It is recommended that attributes be mirrored as properties on the custom +element (and reflected such that any changes appear on the html as well). This +can be achieved as follows: + +```js +// hide-start +class LightweightChartWC extends HTMLElement { + // ... + // hide-end + // Within the class definition + set type(value) { + this.setAttribute('type', value || 'line'); + } + + get type() { + return this.getAttribute('type') || 'line'; + } + // hide-line +} +``` + +We can observe any changes to an attribute by defining the static +`observedAttributes` getter on the custom element and the +`attributeChangedCallback` method on the class definition. + +```js +class LightweightChartWC extends HTMLElement { + // Attributes to observe. When changes occur, `attributeChangedCallback` is called. + static get observedAttributes() { + return ['type', 'autosize']; + } + + /** + * `attributeChangedCallback()` is called when any of the attributes in the + * `observedAttributes` array are changed. + */ + attributeChangedCallback(name, _oldValue, newValue) { + if (!this.chart) { + return; + } + const hasValue = newValue !== null; + switch (name) { + case 'type': + // handle the changed attribute + break; + case 'autosize': + // handle the changed attribute + break; + } + } +} +``` + +### Properties + +Properties for the custom element are read and set through javascript on a +reference to a custom element's instance. This instance can be created using +standard DOM methods such as `querySelector`. + +```js +// Get a reference to an instance of the custom element on the page +const myChartElement = document.querySelector('lightweight-chart'); + +// read the data property +const currentData = myChartElement.data; + +// set the seriesOptions property +myChartElement.seriesOptions = { + color: 'blue', +}; +``` + +We can define setters and getters for the properties if we need more control +over the property instead of it being just a value. + +```js +// hide-start +class LightweightChartWC extends HTMLElement { + // ... + // hide-end + // Within the class definition + set options(value) { + if (!this.chart) { + return; + } + this.chart.applyOptions(value); + } + + get options() { + if (!this.chart) { + return null; + } + return this.chart.options(); + } + // hide-line +} +``` + +As mentioned earlier, it is recommended that any API which accepts complex (or +rich data) beyond a simple string, number, or boolean value should be property. +However, since properties can only be set via javascript there may be cases +where it would be preferable to define these values within the html markup. We +can provide an attribute interface for these properties which can be used to +define the initial values, then remove those attributes from the markup and +ignore any further changes to those attributes. + +```js +// hide-line +class LightweightChartWC extends HTMLElement { + /** + * Any data properties which are provided as JSON string values + * when the component is attached to the DOM will be used as the + * initial values for those properties. + * + * Note: once the component is attached, then any changes to these + * attributes will be ignored (not observed), and should rather be + * set using the property directly. + */ + _tryLoadInitialProperty(name) { + if (this.hasAttribute(name)) { + const valueString = this.getAttribute(name); + let value; + try { + value = JSON.parse(valueString); + } catch (error) { + console.error( + `Unable to read attribute ${name}'s value during initialisation.` + ); + return; + } + // change kebab case attribute name to camel case. + const propertyName = name + .split('-') + .map((text, index) => { + if (index < 1) { + return text; + } + return `${text.charAt(0).toUpperCase()}${text.slice(1)}`; + }) + .join(''); + this[propertyName] = value; + this.removeAttribute(name); + } + } + + connectedCallback() { + // ... + + // Read initial values using attributes and then clear the attributes + // since we don't want to 'reflect' data properties onto the elements + // attributes. + const richDataProperties = [ + 'options', + 'series-options', + 'pricescale-options', + 'timescale-options', + ]; + richDataProperties.forEach(propertyName => { + this._tryLoadInitialProperty(propertyName); + }); + } + // hide-line +} +``` + +These attributes can be used to define the initial values for the properties as +follows (using JSON notation): + +```html + +``` + +## Accessing the chart instance or additional methods + +The IChartApi instance will be accessible via the `chart` property on the custom +element. This can be used by the consumer of the custom element to fully control +the Lightweight Chart within the element. + +```js +// Get a reference to an instance of the custom element on the page +const myChartElement = document.querySelector('lightweight-chart'); + +const chartApi = myChartElement.chart; + +// For example, call the `fitContent` method on the time scale +chartApi.timeScale().fitContent(); +``` + +## Using a Custom Element + +Custom elements can be used just like any other normal html element after the +custom element has been defined and registered. The example custom element will +define and register itself (using +`window.customElements.define('lightweight-chart', LightweightChartWC);`) when +the script is loaded and executed, so all you need to do is include the script +tag on the page. + +Depending on your build step for your page, you may either need to import +Lightweight Charts via an import statement, or access the library via the global +variable defined when using the standalone script version. + +```js +// if using esm version (installed via npm): +// import { createChart } from 'lightweight-charts'; + +// If using standalone version (loaded via a script tag): +const { createChart } = LightweightCharts; +``` + +Similarily, the custom element can either be loaded via an 'side-effect' import +statement: + +```js +// side-effect import statement (use within a module js file) +import './lw-chart.js'; +``` + + or via a script tag: + + ```html + +``` + +Once the custom element script has been loaded and executed then you can use the +custom element anywhere that you can use normal html, including within other +frameworks like React, Vue, and Angular. See +[Custom Elements Everywhere](https://custom-elements-everywhere.com) for more +information. + +### Standalone script example html file + +If you are loading the Lightweight Charts library via the standalone script +version then you can also load the custom element via a script tag (see above +section for more info) and construct your html page as follows: + +```html + + + + + + Web component Example + + + + + + + + + + +``` + +## Complete Sample Code + +Presented below is the complete custom element source code for the Web +component. We have also provided a sample custom element component which +showcases how to make use of these components within a typical html page. + +### Wrapper Custom Element + +The following code block contains the source code for the wrapper custom +element. + +

+ + Download file + +

+ +import CodeBlock from '@theme/CodeBlock'; +import InstantDetails from '@site/src/components/InstantDetails'; +import wrapperCode from '!!raw-loader!./assets/lw-chart.js'; + + + Click here to reveal the code. + {wrapperCode} + + +### Example Usage Custom Element + +The following code block contains the source code for the custom element +showcasing how to use the above custom element. + +

+ + Download file + +

+ +import exampleCode from '!!raw-loader!./assets/wc-example.js'; + + + Click here to reveal the code. + {exampleCode} + diff --git a/website/tutorials/webcomponents/_category_.yml b/website/tutorials/webcomponents/_category_.yml new file mode 100644 index 0000000000..d98818aa6d --- /dev/null +++ b/website/tutorials/webcomponents/_category_.yml @@ -0,0 +1 @@ +label: "Web Components" diff --git a/website/tutorials/webcomponents/assets/.eslintrc.js b/website/tutorials/webcomponents/assets/.eslintrc.js new file mode 100644 index 0000000000..9d646774d0 --- /dev/null +++ b/website/tutorials/webcomponents/assets/.eslintrc.js @@ -0,0 +1,11 @@ +module.exports = { + globals: { + document: false, + window: false, + HTMLElement: false, + }, + rules: { + 'no-console': 'off', + 'no-prototype-builtins': 'off', + }, +}; diff --git a/website/tutorials/webcomponents/assets/lw-chart.js b/website/tutorials/webcomponents/assets/lw-chart.js new file mode 100644 index 0000000000..15da917810 --- /dev/null +++ b/website/tutorials/webcomponents/assets/lw-chart.js @@ -0,0 +1,280 @@ +// if using esm version (installed via npm): +import { createChart } from 'lightweight-charts'; + +// If using standalone version (loaded via a script tag): +// const { createChart } = LightweightCharts; + +(function() { + // Styles for the custom element + const elementStyles = ` + :host { + display: block; + } + :host[hidden] { + display: none; + } + .chart-container { + height: 100%; + width: 100%; + } + `; + + // Class definition for the custom element + class LightweightChartWC extends HTMLElement { + // Attributes to observe. When changes occur, `attributeChangedCallback` is called. + static get observedAttributes() { + return ['type', 'autosize']; + } + + // Helper function to get the series constructor name from a chart type + // eg. 'line' -> 'addLineSeries' + static getChartSeriesConstructorName(type) { + return `add${type.charAt(0).toUpperCase() + type.slice(1)}Series`; + } + + constructor() { + super(); + this.chart = undefined; + this.series = undefined; + this.__data = []; + this._resizeEventHandler = () => this._resizeHandler(); + } + + /** + * `connectedCallback()` fires when the element is inserted into the DOM. + */ + connectedCallback() { + this.attachShadow({ mode: 'open' }); + + /** + * Attributes you may want to set, but should only change if + * not already specified. + */ + // if (!this.hasAttribute('tabindex')) + // this.setAttribute('tabindex', -1); + + // A user may set a property on an _instance_ of an element, + // before its prototype has been connected to this class. + // The `_upgradeProperty()` method will check for any instance properties + // and run them through the proper class setters. + this._upgradeProperty('type'); + this._upgradeProperty('autosize'); + + // We load the data attribute before creating the chart + // so the `setTypeAndData` method can have an initial value. + this._tryLoadInitialProperty('data'); + + // Create the div container for the chart + const container = document.createElement('div'); + container.setAttribute('class', 'chart-container'); + // create the stylesheet for the custom element + const style = document.createElement('style'); + style.textContent = elementStyles; + this.shadowRoot.append(style, container); + + // Create the Lightweight Chart + this.chart = createChart(container); + this.setTypeAndData(); + + // Read initial values using attributes and then clear the attributes + // since we don't want to 'reflect' data properties onto the elements + // attributes. + const richDataProperties = [ + 'options', + 'series-options', + 'pricescale-options', + 'timescale-options', + ]; + richDataProperties.forEach(propertyName => { + this._tryLoadInitialProperty(propertyName); + }); + + if (this.autosize) { + window.addEventListener('resize', this._resizeEventHandler); + } + } + + /** + * Any data properties which are provided as JSON string values + * when the component is attached to the DOM will be used as the + * initial values for those properties. + * + * Note: once the component is attached, then any changes to these + * attributes will be ignored (not observed), and should rather be + * set using the property directly. + */ + _tryLoadInitialProperty(name) { + if (this.hasAttribute(name)) { + const valueString = this.getAttribute(name); + let value; + try { + value = JSON.parse(valueString); + } catch (error) { + console.error( + `Unable to read attribute ${name}'s value during initialisation.` + ); + return; + } + // change kebab case attribute name to camel case. + const propertyName = name + .split('-') + .map((text, index) => { + if (index < 1) {return text;} + return `${text.charAt(0).toUpperCase()}${text.slice(1)}`; + }) + .join(''); + this[propertyName] = value; + this.removeAttribute(name); + } + } + + // Create a chart series (according to the 'type' attribute) and set it's data. + setTypeAndData() { + if (this.series && this.chart) { + this.chart.removeSeries(this.series); + } + this.series = + this.chart[ + LightweightChartWC.getChartSeriesConstructorName(this.type) + ](); + this.series.setData(this.data); + } + + _upgradeProperty(prop) { + if (this.hasOwnProperty(prop)) { + const value = this[prop]; + delete this[prop]; + this[prop] = value; + } + } + + /** + * `disconnectedCallback()` fires when the element is removed from the DOM. + * It's a good place to do clean up work like releasing references and + * removing event listeners. + */ + disconnectedCallback() { + if (this.chart) { + this.chart.remove(); + this.chart = null; + } + window.removeEventListener('resize', this._resizeEventHandler); + } + + /** + * Reflected Properties + * + * These Properties and their corresponding attributes should mirror one another. + */ + set type(value) { + this.setAttribute('type', value || 'line'); + } + + get type() { + return this.getAttribute('type') || 'line'; + } + + set autosize(value) { + const autosize = Boolean(value); + if (autosize) {this.setAttribute('autosize', '');} else {this.removeAttribute('autosize');} + } + + get autosize() { + return this.hasAttribute('autosize'); + } + + /** + * Rich Data Properties + * + * These Properties are not reflected to a corresponding attribute. + */ + set data(value) { + let newData = value; + if (typeof newData !== 'object' || !Array.isArray(newData)) { + newData = []; + console.warn('Lightweight Charts: Data should be an array'); + } + this.__data = newData; + if (this.series) { + this.series.setData(this.__data); + } + } + + get data() { + return this.__data; + } + + set options(value) { + if (!this.chart) {return;} + this.chart.applyOptions(value); + } + + get options() { + if (!this.chart) {return null;} + return this.chart.options(); + } + + set seriesOptions(value) { + if (!this.series) {return;} + this.series.applyOptions(value); + } + + get seriesOptions() { + if (!this.series) {return null;} + return this.series.options(); + } + + set priceScaleOptions(value) { + if (!this.chart) {return;} + this.chart.priceScale().applyOptions(value); + } + + get priceScaleOptions() { + if (!this.series) {return null;} + return this.chart.priceScale().options(); + } + + set timeScaleOptions(value) { + if (!this.chart) {return;} + this.chart.timeScale().applyOptions(value); + } + + get timeScaleOptions() { + if (!this.series) {return null;} + return this.chart.timeScale().options(); + } + + /** + * `attributeChangedCallback()` is called when any of the attributes in the + * `observedAttributes` array are changed. + */ + attributeChangedCallback(name, _oldValue, newValue) { + if (!this.chart) {return;} + const hasValue = newValue !== null; + switch (name) { + case 'type': + this.data = []; + this.setTypeAndData(); + break; + case 'autosize': + if (hasValue) { + window.addEventListener('resize', () => this._resizeEventHandler); + // call once when added to an existing element + this._resizeEventHandler(); + } else { + window.removeEventListener('resize', this._resizeEventHandler); + } + break; + } + } + + _resizeHandler() { + const container = this.shadowRoot.querySelector('div.chart-container'); + if (!this.chart || !container) {return;} + const dimensions = container.getBoundingClientRect(); + this.chart.resize(dimensions.width, dimensions.height); + } + } + + window.customElements.define('lightweight-chart', LightweightChartWC); +})(); diff --git a/website/tutorials/webcomponents/assets/wc-example.js b/website/tutorials/webcomponents/assets/wc-example.js new file mode 100644 index 0000000000..9798808fd8 --- /dev/null +++ b/website/tutorials/webcomponents/assets/wc-example.js @@ -0,0 +1,291 @@ +import './lw-chart.js'; +import { themeColors } from '../../../theme-colors'; + +(function() { + const template = document.createElement('template'); + template.innerHTML = ` + +
+
+ +
+
+ + + +
+
+ `; + + function generateSampleData(ohlc) { + const randomFactor = 25 + Math.random() * 25; + const samplePoint = i => + i * + (0.5 + + Math.sin(i / 10) * 0.2 + + Math.sin(i / 20) * 0.4 + + Math.sin(i / randomFactor) * 0.8 + + Math.sin(i / 500) * 0.5) + + 200; + + const res = []; + const date = new Date(Date.UTC(2018, 0, 1, 0, 0, 0, 0)); + const numberOfPoints = ohlc ? 100 : 500; + for (let i = 0; i < numberOfPoints; ++i) { + const time = date.getTime() / 1000; + const value = samplePoint(i); + if (ohlc) { + const randomRanges = [ + -1 * Math.random(), + Math.random(), + Math.random(), + ].map(j => j * 10); + const sign = Math.sin(Math.random() - 0.5); + res.push({ + time, + low: value + randomRanges[0], + high: value + randomRanges[1], + open: value + sign * randomRanges[2], + close: samplePoint(i + 1), + }); + } else { + res.push({ + time, + value, + }); + } + + date.setUTCDate(date.getUTCDate() + 1); + } + + return res; + } + + const randomShade = () => Math.round(Math.random() * 255); + + const randomColor = (alpha = 1) => + `rgba(${randomShade()}, ${randomShade()}, ${randomShade()}, ${alpha})`; + + const colorsTypeMap = { + area: [ + ['topColor', 0.4], + ['bottomColor', 0], + ['lineColor', 1], + ], + bar: [ + ['upColor', 1], + ['downColor', 1], + ], + baseline: [ + ['topFillColor1', 0.28], + ['topFillColor2', 0.05], + ['topLineColor', 1], + ['bottomFillColor1', 0.28], + ['bottomFillColor2', 0.05], + ['bottomLineColor', 1], + ], + candlestick: [ + ['upColor', 1], + ['downColor', 1], + ['borderUpColor', 1], + ['borderDownColor', 1], + ['wickUpColor', 1], + ['wickDownColor', 1], + ], + histogram: [['color', 1]], + line: [['color', 1]], + }; + + const checkPageTheme = () => + document.documentElement.getAttribute('data-theme') === 'dark'; + + class LightweightChartExampleWC extends HTMLElement { + constructor() { + super(); + this.chartElement = undefined; + } + + connectedCallback() { + this.attachShadow({ mode: 'open' }); + this.shadowRoot.appendChild(template.content.cloneNode(true)); + + this.changeChartTheme(checkPageTheme()); + + if (window.MutationObserver) { + const callback = _ => { + this.changeChartTheme(checkPageTheme()); + }; + this.observer = new window.MutationObserver(callback); + this.observer.observe(document.documentElement, { attributes: true }); + } + + this.chartElement = this.shadowRoot.querySelector('#example-chart'); + this._changeData(); + + this.addButtonClickHandlers(); + this.chartElement.chart.timeScale().fitContent(); + } + + addButtonClickHandlers() { + this.changeColours = () => this._changeColours(); + this.changeType = () => this._changeType(); + this.changeData = () => this._changeData(); + this.shadowRoot + .querySelector('#change-colours-button') + .addEventListener('click', this.changeColours); + this.shadowRoot + .querySelector('#change-type-button') + .addEventListener('click', this.changeType); + this.shadowRoot + .querySelector('#change-data-button') + .addEventListener('click', this.changeData); + } + + removeButtonClickHandlers() { + if (this.changeColours) { + this.shadowRoot + .querySelector('#change-colours-button') + .removeEventListener('click', this.changeColours); + } + if (this.changeType) { + this.shadowRoot + .querySelector('#change-type-button') + .removeEventListener('click', this.changeType); + } + if (this.changeData) { + this.shadowRoot + .querySelector('#change-data-button') + .removeEventListener('click', this.changeData); + } + } + + _changeColours() { + if (!this.chartElement) { + return; + } + const options = {}; + const colorsToSet = colorsTypeMap[this.chartElement.type]; + colorsToSet.forEach(c => { + options[c[0]] = randomColor(c[1]); + }); + this.chartElement.seriesOptions = options; + } + + _changeData() { + if (!this.chartElement) { + return; + } + const candlestickTypeData = ['candlestick', 'bar'].includes( + this.chartElement.type + ); + const newData = generateSampleData(candlestickTypeData); + this.chartElement.data = newData; + if (this.chartElement.type === 'baseline') { + const average = + newData.reduce((s, c) => s + c.value, 0) / newData.length; + this.chartElement.seriesOptions = { + baseValue: { type: 'price', price: average }, + }; + } + } + + _changeType() { + if (!this.chartElement) { + return; + } + const types = [ + 'line', + 'area', + 'baseline', + 'histogram', + 'candlestick', + 'bar', + ].filter(t => t !== this.chartElement.type); + const randIndex = Math.round(Math.random() * (types.length - 1)); + this.chartElement.type = types[randIndex]; + this._changeData(); + + // call a method on the component. + this.chartElement.chart.timeScale().fitContent(); + } + + disconnectedCallback() {} + + changeChartTheme(isDark) { + if (!this.chartElement) { + return; + } + const theme = isDark ? themeColors.DARK : themeColors.LIGHT; + const gridColor = isDark ? '#424F53' : '#D6DCDE'; + this.chartElement.options = { + layout: { + textColor: theme.CHART_TEXT_COLOR, + background: { + color: theme.CHART_BACKGROUND_COLOR, + }, + }, + grid: { + vertLines: { + color: gridColor, + }, + horzLines: { + color: gridColor, + }, + }, + }; + } + } + + window.customElements.define( + 'lightweight-chart-example', + LightweightChartExampleWC + ); +})(); diff --git a/website/versioned_docs/version-3.8/intro.md b/website/versioned_docs/version-3.8/intro.md index 6ae68a00ae..2e386cddd8 100644 --- a/website/versioned_docs/version-3.8/intro.md +++ b/website/versioned_docs/version-3.8/intro.md @@ -82,9 +82,7 @@ Note that regardless of the series type, the API calls are the same (the type of To set the data (or to replace all data items) to a series you need to use [`ISeriesApi.setData`](/api/interfaces/ISeriesApi.md#setdata) method: -```js -import { createChart } from 'lightweight-charts'; - +```js chart const chart = createChart(container); const areaSeries = chart.addAreaSeries(); @@ -114,11 +112,9 @@ candlestickSeries.setData([ { time: '2018-12-30', open: 106.33, high: 110.20, low: 90.39, close: 98.10 }, { time: '2018-12-31', open: 109.87, high: 114.69, low: 85.66, close: 111.26 }, ]); -``` - -It's pretty easy, isn't it? That's it, your chart is ready to be displayed on the page: -![First simple chart](/img/first-chart.png "First simple chart") +chart.timeScale().fitContent(); +``` ### Updating the data in a series diff --git a/website/versioned_docs/version-3.8/series-types.md b/website/versioned_docs/version-3.8/series-types.md index 512df9b4a4..7b5bee1173 100644 --- a/website/versioned_docs/version-3.8/series-types.md +++ b/website/versioned_docs/version-3.8/series-types.md @@ -42,7 +42,17 @@ If you'd like to change any option of a series, you could do this in different w An area chart is basically a colored area between the line connecting all data points and [the time scale](./time-scale.md): -![Area chart example](/img/area-series.png "Area chart example") +```js chart replaceThemeConstants +const chartOptions = { layout: { textColor: CHART_TEXT_COLOR, background: { type: 'solid', color: CHART_BACKGROUND_COLOR } } }; +const chart = createChart(document.getElementById('container'), chartOptions); +const areaSeries = chart.addAreaSeries({ lineColor: LINE_LINE_COLOR, topColor: AREA_TOP_COLOR, bottomColor: AREA_BOTTOM_COLOR }); + +const data = [{ value: 0, time: 1642425322 }, { value: 8, time: 1642511722 }, { value: 10, time: 1642598122 }, { value: 20, time: 1642684522 }, { value: 3, time: 1642770922 }, { value: 43, time: 1642857322 }, { value: 41, time: 1642943722 }, { value: 43, time: 1643030122 }, { value: 56, time: 1643116522 }, { value: 46, time: 1643202922 }]; + +areaSeries.setData(data); + +chart.timeScale().fitContent(); +``` ## Bar @@ -55,7 +65,17 @@ A bar chart shows price movements in the form of bars. Vertical line length of a bar is limited by the highest and lowest price values. Open & Close values are represented by tick marks, on the left & right hand side of the bar respectively: -![Bar chart example](/img/bar-series.png "Bar chart example") +```js chart replaceThemeConstants +const chartOptions = { layout: { textColor: CHART_TEXT_COLOR, background: { type: 'solid', color: CHART_BACKGROUND_COLOR } } }; +const chart = createChart(document.getElementById('container'), chartOptions); +const barSeries = chart.addBarSeries({ upColor: BAR_UP_COLOR, downColor: BAR_DOWN_COLOR }); + +const data = [{ open: 10, high: 10.63, low: 9.49, close: 9.55, time: 1642427876 }, { open: 9.55, high: 10.30, low: 9.42, close: 9.94, time: 1642514276 }, { open: 9.94, high: 10.17, low: 9.92, close: 9.78, time: 1642600676 }, { open: 9.78, high: 10.59, low: 9.18, close: 9.51, time: 1642687076 }, { open: 9.51, high: 10.46, low: 9.10, close: 10.17, time: 1642773476 }, { open: 10.17, high: 10.96, low: 10.16, close: 10.47, time: 1642859876 }, { open: 10.47, high: 11.39, low: 10.40, close: 10.81, time: 1642946276 }, { open: 10.81, high: 11.60, low: 10.30, close: 10.75, time: 1643032676 }, { open: 10.75, high: 11.60, low: 10.49, close: 10.93, time: 1643119076 }, { open: 10.93, high: 11.53, low: 10.76, close: 10.96, time: 1643205476 }]; + +barSeries.setData(data); + +chart.timeScale().fitContent(); +``` ## Baseline @@ -65,7 +85,17 @@ Open & Close values are represented by tick marks, on the left & right hand side 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): -![Baseline chart example](/img/baseline-series.png) +```js chart replaceThemeConstants +const chartOptions = { layout: { textColor: CHART_TEXT_COLOR, background: { type: 'solid', color: CHART_BACKGROUND_COLOR } } }; +const chart = createChart(document.getElementById('container'), chartOptions); +const baselineSeries = chart.addBaselineSeries({ baseValue: { type: 'price', price: 25 }, topLineColor: BASELINE_TOP_LINE_COLOR, topFillColor1: BASELINE_TOP_FILL_COLOR1, topFillColor2: BASELINE_TOP_FILL_COLOR2, bottomLineColor: BASELINE_BOTTOM_LINE_COLOR, bottomFillColor1: BASELINE_BOTTOM_FILL_COLOR1, bottomFillColor2: BASELINE_BOTTOM_FILL_COLOR2 }); + +const data = [{ value: 1, time: 1642425322 }, { value: 8, time: 1642511722 }, { value: 10, time: 1642598122 }, { value: 20, time: 1642684522 }, { value: 3, time: 1642770922 }, { value: 43, time: 1642857322 }, { value: 41, time: 1642943722 }, { value: 43, time: 1643030122 }, { value: 56, time: 1643116522 }, { value: 46, time: 1643202922 }]; + +baselineSeries.setData(data); + +chart.timeScale().fitContent(); +``` ## Candlestick @@ -76,7 +106,17 @@ A baseline is basically two colored areas (top and bottom) between the line conn A candlestick chart shows price movements in the form of candlesticks. On the candlestick chart, open & close values form a solid body of a candle while wicks show high & low values for a candlestick's time interval: -![Candlestick chart example](/img/candlestick-series.png "Candlestick chart example") +```js chart replaceThemeConstants +const chartOptions = { layout: { textColor: CHART_TEXT_COLOR, background: { type: 'solid', color: CHART_BACKGROUND_COLOR } } }; +const chart = createChart(document.getElementById('container'), chartOptions); +const candlestickSeries = chart.addCandlestickSeries({ upColor: BAR_UP_COLOR, downColor: BAR_DOWN_COLOR, borderVisible: false, wickUpColor: BAR_UP_COLOR, wickDownColor: BAR_DOWN_COLOR }); + +const data = [{ open: 10, high: 10.63, low: 9.49, close: 9.55, time: 1642427876 }, { open: 9.55, high: 10.30, low: 9.42, close: 9.94, time: 1642514276 }, { open: 9.94, high: 10.17, low: 9.92, close: 9.78, time: 1642600676 }, { open: 9.78, high: 10.59, low: 9.18, close: 9.51, time: 1642687076 }, { open: 9.51, high: 10.46, low: 9.10, close: 10.17, time: 1642773476 }, { open: 10.17, high: 10.96, low: 10.16, close: 10.47, time: 1642859876 }, { open: 10.47, high: 11.39, low: 10.40, close: 10.81, time: 1642946276 }, { open: 10.81, high: 11.60, low: 10.30, close: 10.75, time: 1643032676 }, { open: 10.75, high: 11.60, low: 10.49, close: 10.93, time: 1643119076 }, { open: 10.93, high: 11.53, low: 10.76, close: 10.96, time: 1643205476 }]; + +candlestickSeries.setData(data); + +chart.timeScale().fitContent(); +``` ## Histogram @@ -87,7 +127,17 @@ On the candlestick chart, open & close values form a solid body of a candle whil A histogram series is a graphical representation of the value distribution. Histogram creates intervals (columns) and counts how many values fall into each column: -![Histogram example](/img/histogram-series.png "Histogram chart example") +```js chart replaceThemeConstants +const chartOptions = { layout: { textColor: CHART_TEXT_COLOR, background: { type: 'solid', color: CHART_BACKGROUND_COLOR } } }; +const chart = createChart(document.getElementById('container'), chartOptions); +const histogramSeries = chart.addHistogramSeries({ color: HISTOGRAM_COLOR }); + +const data = [{ value: 1, time: 1642425322 }, { value: 8, time: 1642511722 }, { value: 10, time: 1642598122 }, { value: 20, time: 1642684522 }, { value: 3, time: 1642770922, color: 'red' }, { value: 43, time: 1642857322 }, { value: 41, time: 1642943722, color: 'red' }, { value: 43, time: 1643030122 }, { value: 56, time: 1643116522 }, { value: 46, time: 1643202922, color: 'red' }]; + +histogramSeries.setData(data); + +chart.timeScale().fitContent(); +``` ## Line @@ -97,116 +147,14 @@ Histogram creates intervals (columns) and counts how many values fall into each A line chart is a type of chart that displays information as series of the data points connected by straight line segments: -![Line chart example](/img/line-series.png "Line chart example") - - +const data = [{ value: 0, time: 1642425322 }, { value: 8, time: 1642511722 }, { value: 10, time: 1642598122 }, { value: 20, time: 1642684522 }, { value: 3, time: 1642770922 }, { value: 43, time: 1642857322 }, { value: 41, time: 1642943722 }, { value: 43, time: 1643030122 }, { value: 56, time: 1643116522 }, { value: 46, time: 1643202922 }]; + +lineSeries.setData(data); + +chart.timeScale().fitContent(); +```