From 86da4820bc2d81126122496ae6c7bca1b98b216a Mon Sep 17 00:00:00 2001 From: Mark Silverwood Date: Tue, 29 Aug 2023 23:44:59 +0100 Subject: [PATCH 1/3] =?UTF-8?q?add=20plugin=20examples=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .eslintignore | 2 + .github/workflows/deploy.yml | 9 + plugin-examples/.gitignore | 17 + plugin-examples/README.md | 96 ++++ plugin-examples/build-website.mjs | 72 +++ plugin-examples/compile.mjs | 165 ++++++ plugin-examples/index.html | 9 + plugin-examples/package.json | 20 + .../delta-brushable/example.ts | 98 ++++ .../delta-brushable/index.html | 31 ++ plugin-examples/src/examples-base.css | 69 +++ plugin-examples/src/helpers/assertions.ts | 33 ++ plugin-examples/src/helpers/closest-index.ts | 44 ++ plugin-examples/src/helpers/delegate.ts | 67 +++ .../src/helpers/dimensions/candles.ts | 48 ++ .../src/helpers/dimensions/columns.ts | 237 ++++++++ .../src/helpers/dimensions/common.ts | 6 + .../src/helpers/dimensions/crosshair-width.ts | 23 + .../src/helpers/dimensions/full-width.ts | 29 + .../src/helpers/dimensions/positions.ts | 48 ++ .../src/helpers/min-max-in-range.ts | 69 +++ plugin-examples/src/helpers/simple-clone.ts | 7 + plugin-examples/src/helpers/time.ts | 34 ++ plugin-examples/src/index.html | 59 ++ .../plugins/anchored-text/anchored-text.ts | 107 ++++ .../plugins/anchored-text/example/example.ts | 28 + .../plugins/anchored-text/example/index.html | 19 + .../src/plugins/anchored-text/package.json | 5 + .../background-shade-series.ts | 45 ++ .../example/example.ts | 20 + .../example/index.html | 22 + .../background-shade-series/options.ts | 21 + .../background-shade-series/package.json | 5 + .../background-shade-series/renderer.ts | 111 ++++ .../plugins/bands-indicator/band-indicator.ts | 214 ++++++++ .../bands-indicator/example/example.ts | 14 + .../bands-indicator/example/index.html | 21 + .../box-whisker-series/box-whisker-series.ts | 44 ++ .../box-whisker-series/example/example.ts | 20 + .../box-whisker-series/example/index.html | 24 + .../src/plugins/box-whisker-series/options.ts | 19 + .../plugins/box-whisker-series/renderer.ts | 304 ++++++++++ .../plugins/box-whisker-series/sample-data.ts | 86 +++ .../brushable-area-series.ts | 43 ++ .../src/plugins/brushable-area-series/data.ts | 8 + .../brushable-area-series/example/example.ts | 123 +++++ .../brushable-area-series/example/index.html | 23 + .../plugins/brushable-area-series/options.ts | 39 ++ .../plugins/brushable-area-series/renderer.ts | 162 ++++++ .../delta-tooltip/crosshair-line-pane.ts | 94 ++++ .../delta-tooltip/delta-tooltip-pane.ts | 400 ++++++++++++++ .../plugins/delta-tooltip/delta-tooltip.ts | 284 ++++++++++ .../plugins/delta-tooltip/example/example.ts | 39 ++ .../plugins/delta-tooltip/example/index.html | 28 + .../delta-tooltip/multi-touch-chart-events.ts | 257 +++++++++ .../expiring-price-alerts/example/example.ts | 113 ++++ .../expiring-price-alerts/example/index.html | 19 + .../expiring-price-alerts.ts | 210 +++++++ .../plugins/expiring-price-alerts/icons.ts | 13 + .../iexpiring-price-alerts.ts | 17 + .../plugins/expiring-price-alerts/options.ts | 17 + .../expiring-price-alerts/primitive.ts | 118 ++++ .../plugins/expiring-price-alerts/renderer.ts | 81 +++ .../expiring-price-alerts/sample-data.ts | 94 ++++ .../src/plugins/grouped-bars-series/data.ts | 8 + .../grouped-bars-series/example/example.ts | 25 + .../grouped-bars-series/example/index.html | 21 + .../grouped-bars-series.ts | 46 ++ .../plugins/grouped-bars-series/options.ts | 19 + .../plugins/grouped-bars-series/renderer.ts | 121 ++++ .../plugins/heatmap-series/bell-curve-data.ts | 54 ++ .../src/plugins/heatmap-series/data.ts | 17 + .../plugins/heatmap-series/example/example.ts | 41 ++ .../heatmap-series/example/example2.html | 22 + .../heatmap-series/example/example2.ts | 63 +++ .../plugins/heatmap-series/example/index.html | 20 + .../plugins/heatmap-series/heatmap-series.ts | 53 ++ .../src/plugins/heatmap-series/options.ts | 26 + .../src/plugins/heatmap-series/renderer.ts | 122 +++++ .../heatmap-series/sample-heatmap-data.ts | 169 ++++++ .../example/example.ts | 16 + .../example/index.html | 20 + .../highlight-bar-crosshair.ts | 180 ++++++ .../src/plugins/hlc-area-series/data.ts | 10 + .../hlc-area-series/example/example.ts | 16 + .../hlc-area-series/example/index.html | 21 + .../hlc-area-series/hlc-area-series.ts | 43 ++ .../src/plugins/hlc-area-series/options.ts | 27 + .../src/plugins/hlc-area-series/renderer.ts | 127 +++++ .../image-watermark/example/example.ts | 37 ++ .../plugins/image-watermark/example/image.svg | 39 ++ .../image-watermark/example/index.html | 26 + .../image-watermark/image-watermark.ts | 163 ++++++ .../src/plugins/lollipop-series/data.ts | 8 + .../lollipop-series/example/example.ts | 17 + .../lollipop-series/example/index.html | 20 + .../lollipop-series/lollipop-series.ts | 44 ++ .../src/plugins/lollipop-series/options.ts | 13 + .../src/plugins/lollipop-series/renderer.ts | 108 ++++ .../overlay-price-scale/example/example.ts | 24 + .../overlay-price-scale/example/index.html | 20 + .../overlay-price-scale.ts | 194 +++++++ .../partial-price-line/example/example.ts | 38 ++ .../partial-price-line/example/index.html | 20 + .../partial-price-line/partial-price-line.ts | 121 ++++ plugin-examples/src/plugins/plugin-base.ts | 49 ++ .../rectangle-drawing-tool/example/example.ts | 20 + .../rectangle-drawing-tool/example/index.html | 40 ++ .../rectangle-drawing-tool.ts | 517 ++++++++++++++++++ .../plugins/rounded-candles-series/data.ts | 10 + .../rounded-candles-series/example/example.ts | 32 ++ .../rounded-candles-series/example/index.html | 19 + .../plugins/rounded-candles-series/helpers.ts | 27 + .../rounded-candles-series/renderer.ts | 157 ++++++ .../rounded-candles-series.ts | 72 +++ .../session-highlighting/example/example.ts | 34 ++ .../session-highlighting/example/index.html | 22 + .../session-highlighting.ts | 151 +++++ .../src/plugins/stacked-area-series/data.ts | 8 + .../stacked-area-series/example/example.ts | 24 + .../stacked-area-series/example/index.html | 20 + .../plugins/stacked-area-series/options.ts | 26 + .../plugins/stacked-area-series/renderer.ts | 205 +++++++ .../stacked-area-series.ts | 49 ++ .../src/plugins/stacked-bars-series/data.ts | 8 + .../stacked-bars-series/example/example.ts | 20 + .../stacked-bars-series/example/index.html | 23 + .../plugins/stacked-bars-series/options.ts | 19 + .../plugins/stacked-bars-series/renderer.ts | 112 ++++ .../stacked-bars-series.ts | 49 ++ .../src/plugins/tooltip/example/example.ts | 65 +++ .../src/plugins/tooltip/example/index.html | 38 ++ .../src/plugins/tooltip/tooltip-element.ts | 209 +++++++ .../src/plugins/tooltip/tooltip.ts | 258 +++++++++ .../src/plugins/trend-line/example/example.ts | 23 + .../src/plugins/trend-line/example/index.html | 20 + .../src/plugins/trend-line/trend-line.ts | 187 +++++++ .../plugins/user-price-alerts/constants.ts | 43 ++ .../user-price-alerts/example/example.ts | 62 +++ .../user-price-alerts/example/index.html | 29 + .../user-price-alerts/irenderer-data.ts | 37 ++ .../src/plugins/user-price-alerts/mouse.ts | 110 ++++ .../user-price-alerts/pane-renderer.ts | 320 +++++++++++ .../plugins/user-price-alerts/pane-view.ts | 29 + .../price-scale-pane-renderer.ts | 48 ++ .../user-price-alerts/renderer-base.ts | 11 + .../src/plugins/user-price-alerts/state.ts | 80 +++ .../user-price-alerts/user-price-alerts.ts | 226 ++++++++ .../user-price-lines/example/example.ts | 13 + .../user-price-lines/example/index.html | 21 + .../user-price-lines/user-price-lines.ts | 293 ++++++++++ .../plugins/vertical-line/example/example.ts | 24 + .../plugins/vertical-line/example/index.html | 20 + .../plugins/vertical-line/vertical-line.ts | 145 +++++ .../plugins/volume-profile/example/example.ts | 28 + .../plugins/volume-profile/example/index.html | 21 + .../plugins/volume-profile/volume-profile.ts | 198 +++++++ plugin-examples/src/sample-data.ts | 157 ++++++ plugin-examples/src/style.css | 62 +++ plugin-examples/src/vite-env.d.ts | 1 + plugin-examples/src/vite.config.js | 25 + plugin-examples/tsconfig.json | 21 + website/docs/plugins/intro.md | 6 + 163 files changed, 11325 insertions(+) create mode 100644 plugin-examples/.gitignore create mode 100644 plugin-examples/README.md create mode 100644 plugin-examples/build-website.mjs create mode 100644 plugin-examples/compile.mjs create mode 100644 plugin-examples/index.html create mode 100644 plugin-examples/package.json create mode 100644 plugin-examples/src/combined-examples/delta-brushable/example.ts create mode 100644 plugin-examples/src/combined-examples/delta-brushable/index.html create mode 100644 plugin-examples/src/examples-base.css create mode 100644 plugin-examples/src/helpers/assertions.ts create mode 100644 plugin-examples/src/helpers/closest-index.ts create mode 100644 plugin-examples/src/helpers/delegate.ts create mode 100644 plugin-examples/src/helpers/dimensions/candles.ts create mode 100644 plugin-examples/src/helpers/dimensions/columns.ts create mode 100644 plugin-examples/src/helpers/dimensions/common.ts create mode 100644 plugin-examples/src/helpers/dimensions/crosshair-width.ts create mode 100644 plugin-examples/src/helpers/dimensions/full-width.ts create mode 100644 plugin-examples/src/helpers/dimensions/positions.ts create mode 100644 plugin-examples/src/helpers/min-max-in-range.ts create mode 100644 plugin-examples/src/helpers/simple-clone.ts create mode 100644 plugin-examples/src/helpers/time.ts create mode 100644 plugin-examples/src/index.html create mode 100644 plugin-examples/src/plugins/anchored-text/anchored-text.ts create mode 100644 plugin-examples/src/plugins/anchored-text/example/example.ts create mode 100644 plugin-examples/src/plugins/anchored-text/example/index.html create mode 100644 plugin-examples/src/plugins/anchored-text/package.json create mode 100644 plugin-examples/src/plugins/background-shade-series/background-shade-series.ts create mode 100644 plugin-examples/src/plugins/background-shade-series/example/example.ts create mode 100644 plugin-examples/src/plugins/background-shade-series/example/index.html create mode 100644 plugin-examples/src/plugins/background-shade-series/options.ts create mode 100644 plugin-examples/src/plugins/background-shade-series/package.json create mode 100644 plugin-examples/src/plugins/background-shade-series/renderer.ts create mode 100644 plugin-examples/src/plugins/bands-indicator/band-indicator.ts create mode 100644 plugin-examples/src/plugins/bands-indicator/example/example.ts create mode 100644 plugin-examples/src/plugins/bands-indicator/example/index.html create mode 100644 plugin-examples/src/plugins/box-whisker-series/box-whisker-series.ts create mode 100644 plugin-examples/src/plugins/box-whisker-series/example/example.ts create mode 100644 plugin-examples/src/plugins/box-whisker-series/example/index.html create mode 100644 plugin-examples/src/plugins/box-whisker-series/options.ts create mode 100644 plugin-examples/src/plugins/box-whisker-series/renderer.ts create mode 100644 plugin-examples/src/plugins/box-whisker-series/sample-data.ts create mode 100644 plugin-examples/src/plugins/brushable-area-series/brushable-area-series.ts create mode 100644 plugin-examples/src/plugins/brushable-area-series/data.ts create mode 100644 plugin-examples/src/plugins/brushable-area-series/example/example.ts create mode 100644 plugin-examples/src/plugins/brushable-area-series/example/index.html create mode 100644 plugin-examples/src/plugins/brushable-area-series/options.ts create mode 100644 plugin-examples/src/plugins/brushable-area-series/renderer.ts create mode 100644 plugin-examples/src/plugins/delta-tooltip/crosshair-line-pane.ts create mode 100644 plugin-examples/src/plugins/delta-tooltip/delta-tooltip-pane.ts create mode 100644 plugin-examples/src/plugins/delta-tooltip/delta-tooltip.ts create mode 100644 plugin-examples/src/plugins/delta-tooltip/example/example.ts create mode 100644 plugin-examples/src/plugins/delta-tooltip/example/index.html create mode 100644 plugin-examples/src/plugins/delta-tooltip/multi-touch-chart-events.ts create mode 100644 plugin-examples/src/plugins/expiring-price-alerts/example/example.ts create mode 100644 plugin-examples/src/plugins/expiring-price-alerts/example/index.html create mode 100644 plugin-examples/src/plugins/expiring-price-alerts/expiring-price-alerts.ts create mode 100644 plugin-examples/src/plugins/expiring-price-alerts/icons.ts create mode 100644 plugin-examples/src/plugins/expiring-price-alerts/iexpiring-price-alerts.ts create mode 100644 plugin-examples/src/plugins/expiring-price-alerts/options.ts create mode 100644 plugin-examples/src/plugins/expiring-price-alerts/primitive.ts create mode 100644 plugin-examples/src/plugins/expiring-price-alerts/renderer.ts create mode 100644 plugin-examples/src/plugins/expiring-price-alerts/sample-data.ts create mode 100644 plugin-examples/src/plugins/grouped-bars-series/data.ts create mode 100644 plugin-examples/src/plugins/grouped-bars-series/example/example.ts create mode 100644 plugin-examples/src/plugins/grouped-bars-series/example/index.html create mode 100644 plugin-examples/src/plugins/grouped-bars-series/grouped-bars-series.ts create mode 100644 plugin-examples/src/plugins/grouped-bars-series/options.ts create mode 100644 plugin-examples/src/plugins/grouped-bars-series/renderer.ts create mode 100644 plugin-examples/src/plugins/heatmap-series/bell-curve-data.ts create mode 100644 plugin-examples/src/plugins/heatmap-series/data.ts create mode 100644 plugin-examples/src/plugins/heatmap-series/example/example.ts create mode 100644 plugin-examples/src/plugins/heatmap-series/example/example2.html create mode 100644 plugin-examples/src/plugins/heatmap-series/example/example2.ts create mode 100644 plugin-examples/src/plugins/heatmap-series/example/index.html create mode 100644 plugin-examples/src/plugins/heatmap-series/heatmap-series.ts create mode 100644 plugin-examples/src/plugins/heatmap-series/options.ts create mode 100644 plugin-examples/src/plugins/heatmap-series/renderer.ts create mode 100644 plugin-examples/src/plugins/heatmap-series/sample-heatmap-data.ts create mode 100644 plugin-examples/src/plugins/highlight-bar-crosshair/example/example.ts create mode 100644 plugin-examples/src/plugins/highlight-bar-crosshair/example/index.html create mode 100644 plugin-examples/src/plugins/highlight-bar-crosshair/highlight-bar-crosshair.ts create mode 100644 plugin-examples/src/plugins/hlc-area-series/data.ts create mode 100644 plugin-examples/src/plugins/hlc-area-series/example/example.ts create mode 100644 plugin-examples/src/plugins/hlc-area-series/example/index.html create mode 100644 plugin-examples/src/plugins/hlc-area-series/hlc-area-series.ts create mode 100644 plugin-examples/src/plugins/hlc-area-series/options.ts create mode 100644 plugin-examples/src/plugins/hlc-area-series/renderer.ts create mode 100644 plugin-examples/src/plugins/image-watermark/example/example.ts create mode 100644 plugin-examples/src/plugins/image-watermark/example/image.svg create mode 100644 plugin-examples/src/plugins/image-watermark/example/index.html create mode 100644 plugin-examples/src/plugins/image-watermark/image-watermark.ts create mode 100644 plugin-examples/src/plugins/lollipop-series/data.ts create mode 100644 plugin-examples/src/plugins/lollipop-series/example/example.ts create mode 100644 plugin-examples/src/plugins/lollipop-series/example/index.html create mode 100644 plugin-examples/src/plugins/lollipop-series/lollipop-series.ts create mode 100644 plugin-examples/src/plugins/lollipop-series/options.ts create mode 100644 plugin-examples/src/plugins/lollipop-series/renderer.ts create mode 100644 plugin-examples/src/plugins/overlay-price-scale/example/example.ts create mode 100644 plugin-examples/src/plugins/overlay-price-scale/example/index.html create mode 100644 plugin-examples/src/plugins/overlay-price-scale/overlay-price-scale.ts create mode 100644 plugin-examples/src/plugins/partial-price-line/example/example.ts create mode 100644 plugin-examples/src/plugins/partial-price-line/example/index.html create mode 100644 plugin-examples/src/plugins/partial-price-line/partial-price-line.ts create mode 100644 plugin-examples/src/plugins/plugin-base.ts create mode 100644 plugin-examples/src/plugins/rectangle-drawing-tool/example/example.ts create mode 100644 plugin-examples/src/plugins/rectangle-drawing-tool/example/index.html create mode 100644 plugin-examples/src/plugins/rectangle-drawing-tool/rectangle-drawing-tool.ts create mode 100644 plugin-examples/src/plugins/rounded-candles-series/data.ts create mode 100644 plugin-examples/src/plugins/rounded-candles-series/example/example.ts create mode 100644 plugin-examples/src/plugins/rounded-candles-series/example/index.html create mode 100644 plugin-examples/src/plugins/rounded-candles-series/helpers.ts create mode 100644 plugin-examples/src/plugins/rounded-candles-series/renderer.ts create mode 100644 plugin-examples/src/plugins/rounded-candles-series/rounded-candles-series.ts create mode 100644 plugin-examples/src/plugins/session-highlighting/example/example.ts create mode 100644 plugin-examples/src/plugins/session-highlighting/example/index.html create mode 100644 plugin-examples/src/plugins/session-highlighting/session-highlighting.ts create mode 100644 plugin-examples/src/plugins/stacked-area-series/data.ts create mode 100644 plugin-examples/src/plugins/stacked-area-series/example/example.ts create mode 100644 plugin-examples/src/plugins/stacked-area-series/example/index.html create mode 100644 plugin-examples/src/plugins/stacked-area-series/options.ts create mode 100644 plugin-examples/src/plugins/stacked-area-series/renderer.ts create mode 100644 plugin-examples/src/plugins/stacked-area-series/stacked-area-series.ts create mode 100644 plugin-examples/src/plugins/stacked-bars-series/data.ts create mode 100644 plugin-examples/src/plugins/stacked-bars-series/example/example.ts create mode 100644 plugin-examples/src/plugins/stacked-bars-series/example/index.html create mode 100644 plugin-examples/src/plugins/stacked-bars-series/options.ts create mode 100644 plugin-examples/src/plugins/stacked-bars-series/renderer.ts create mode 100644 plugin-examples/src/plugins/stacked-bars-series/stacked-bars-series.ts create mode 100644 plugin-examples/src/plugins/tooltip/example/example.ts create mode 100644 plugin-examples/src/plugins/tooltip/example/index.html create mode 100644 plugin-examples/src/plugins/tooltip/tooltip-element.ts create mode 100644 plugin-examples/src/plugins/tooltip/tooltip.ts create mode 100644 plugin-examples/src/plugins/trend-line/example/example.ts create mode 100644 plugin-examples/src/plugins/trend-line/example/index.html create mode 100644 plugin-examples/src/plugins/trend-line/trend-line.ts create mode 100644 plugin-examples/src/plugins/user-price-alerts/constants.ts create mode 100644 plugin-examples/src/plugins/user-price-alerts/example/example.ts create mode 100644 plugin-examples/src/plugins/user-price-alerts/example/index.html create mode 100644 plugin-examples/src/plugins/user-price-alerts/irenderer-data.ts create mode 100644 plugin-examples/src/plugins/user-price-alerts/mouse.ts create mode 100644 plugin-examples/src/plugins/user-price-alerts/pane-renderer.ts create mode 100644 plugin-examples/src/plugins/user-price-alerts/pane-view.ts create mode 100644 plugin-examples/src/plugins/user-price-alerts/price-scale-pane-renderer.ts create mode 100644 plugin-examples/src/plugins/user-price-alerts/renderer-base.ts create mode 100644 plugin-examples/src/plugins/user-price-alerts/state.ts create mode 100644 plugin-examples/src/plugins/user-price-alerts/user-price-alerts.ts create mode 100644 plugin-examples/src/plugins/user-price-lines/example/example.ts create mode 100644 plugin-examples/src/plugins/user-price-lines/example/index.html create mode 100644 plugin-examples/src/plugins/user-price-lines/user-price-lines.ts create mode 100644 plugin-examples/src/plugins/vertical-line/example/example.ts create mode 100644 plugin-examples/src/plugins/vertical-line/example/index.html create mode 100644 plugin-examples/src/plugins/vertical-line/vertical-line.ts create mode 100644 plugin-examples/src/plugins/volume-profile/example/example.ts create mode 100644 plugin-examples/src/plugins/volume-profile/example/index.html create mode 100644 plugin-examples/src/plugins/volume-profile/volume-profile.ts create mode 100644 plugin-examples/src/sample-data.ts create mode 100644 plugin-examples/src/style.css create mode 100644 plugin-examples/src/vite-env.d.ts create mode 100644 plugin-examples/src/vite.config.js create mode 100644 plugin-examples/tsconfig.json diff --git a/.eslintignore b/.eslintignore index 517b6298d6..5db9c3a96d 100644 --- a/.eslintignore +++ b/.eslintignore @@ -13,3 +13,5 @@ /website/docs/api/** /website/versioned_docs/**/api/** /website/build/** + +/plugin-examples diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 0584405a63..87c6a17013 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -36,6 +36,15 @@ jobs: - name: Build documentation website working-directory: ./website run: npm run build + - name: Install plugin example dependencies + working-directory: ./plugin-examples + run: npm install + - name: Build plugin examples website + working-directory: ./plugin-examples + run: npm run build:examples:site + - name: Build plugin examples website + working-directory: ./plugin-examples + run: npm run build:examples:site - name: Deploy uses: peaceiris/actions-gh-pages@v3 diff --git a/plugin-examples/.gitignore b/plugin-examples/.gitignore new file mode 100644 index 0000000000..e8076a4b1d --- /dev/null +++ b/plugin-examples/.gitignore @@ -0,0 +1,17 @@ +node_modules +dist +compiled +typings +website +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/plugin-examples/README.md b/plugin-examples/README.md new file mode 100644 index 0000000000..b7d530c76e --- /dev/null +++ b/plugin-examples/README.md @@ -0,0 +1,96 @@ +# Lightweight Charts™ Plugin Examples + +This folder contains a collection of example plugins designed to extend the +functionality of Lightweight Charts™ and inspire the development of your own +plugins. + +**Disclaimer:** These plugins are provided as-is, and are primarily intended as +proof-of-concept examples and starting points. They have not been fully +optimized for production and may not receive updates or improvements over time. + +We believe in the power of community collaboration, and we warmly welcome any +pull requests (PRs) aimed at enhancing and fixing the existing examples. +Additionally, we encourage you to create your own plugins and share them with +the community. We would be delighted to showcase the best plugins our users +create in this readme document. + +✨ If you have something cool to share or if you need assistance, please don't +hesitate to get in touch. + +🚀 Need a starting point for your plugin idea? Check out +[create-lwc-plugin](https://github.com/tradingview/create-lwc-plugin) package. + +📊 You can view a demo page of the plugins within this repo at his link: +[Plugin Examples](https://tradingview.github.io/lightweight-charts/plugin-examples) + +## Learning More + +- [Documentation for Plugins](https://tradingview.github.io/lightweight-charts/docs/next/plugins/intro) +- [Learn more about Lightweight Charts™](https://www.tradingview.com/lightweight-charts/) + +## Running Locally + +To run this repo locally, follow these steps: + +1. Clone the repo to your local machine +2. First build the library + + ```shell + npm install + npm run build:prod + ``` + +3. Switch to the Plugin Examples Folder, install and start the development server + + ```shell + cd plugin-examples + npm install + npm run dev + ``` + +4. Visit `localhost:5173` in the browser. + +## Compiling the Examples + +```shell +npm run compile +``` + +Check the output in the `compiled` folder. + +## Using an Example + +Once you have compiled the examples then simply copy that folder into your +project and import the JS module in your code. + +1. Copy the compiled plugin folder into your project, example: + `plugins/background-shade-series` (from `compiled/background-shade-series`) +2. Within your project, you can import the class as follows: + + ```js + import { BackgroundShadeSeries } from '../plugins/background-shade-series/background-shade-series'; + + // ... + + const backgroundShadeSeriesPlugin = new BackgroundShadeSeries(); + const myCustomSeries = chart.addCustomSeries(backgroundShadeSeriesPlugin, { + lowValue: 0, + highValue: 1000, + }); + ``` + +## Creating your own Plugin + +[create-lwc-plugin](https://github.com/tradingview/create-lwc-plugin) is an npm +package designed to simplify the process of creating a new plugin for +Lightweight Charts™. With this generator, you can quickly scaffold a project +from a template for either + +- a Drawing primitive plugin, or +- a Custom series plugin. + +You can get started with this simple command: + +```shell +npm create lwc-plugin@latest +``` diff --git a/plugin-examples/build-website.mjs b/plugin-examples/build-website.mjs new file mode 100644 index 0000000000..b76a161d22 --- /dev/null +++ b/plugin-examples/build-website.mjs @@ -0,0 +1,72 @@ +import { + existsSync, + mkdirSync, + readdirSync, + statSync, + readFileSync, + writeFileSync, + rmSync, +} from 'node:fs'; +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const currentDir = dirname(__filename); + +const distFolder = resolve(currentDir, 'dist'); +const distSrcFolder = resolve(currentDir, 'dist', 'src'); +const websiteFolder = resolve(currentDir, 'website'); +const docsWebsiteFolder = resolve(currentDir, '..', 'website', 'build', 'plugin-examples'); + +function emptyDir(dir) { + if (!existsSync(dir)) { + return; + } + for (const file of readdirSync(dir)) { + rmSync(resolve(dir, file), { recursive: true, force: true }); + } +} + +function copy(src, dest, contentReplacer) { + const stat = statSync(src); + if (stat.isDirectory()) { + copyDir(src, dest, contentReplacer); + } else { + const content = readFileSync(src).toString(); + writeFileSync(dest, contentReplacer ? contentReplacer(content) : content); + } +} + +function copyDir(srcDir, destDir, contentReplacer) { + mkdirSync(destDir, { recursive: true }); + for (const file of readdirSync(srcDir)) { + const srcFile = resolve(srcDir, file); + const destFile = resolve(destDir, file); + if (file !== 'src') { + copy(srcFile, destFile, contentReplacer); + } + } +} + +function contentReplacer(content) { + return content + .replace(/("|')\.\.\/assets/g, '$1./assets') + .replace(/\/\.\.\/assets/g, '/assets'); +} + +emptyDir(websiteFolder); +copyDir(distFolder, websiteFolder, contentReplacer, 'src'); +copyDir(distSrcFolder, websiteFolder, contentReplacer); +copy( + resolve(websiteFolder, 'index.html'), + resolve(websiteFolder, 'index.html'), + content => { + return content.replace( + '', + '\n' + ); + } +); + +// Copy into the documentation site build +copyDir(websiteFolder, docsWebsiteFolder, content => content); diff --git a/plugin-examples/compile.mjs b/plugin-examples/compile.mjs new file mode 100644 index 0000000000..8e5dd5ecd6 --- /dev/null +++ b/plugin-examples/compile.mjs @@ -0,0 +1,165 @@ +/* eslint-disable no-console */ +import { dirname, resolve, basename, join, extname } from 'node:path'; +import { + existsSync, + mkdirSync, + readdirSync, + statSync, + writeFileSync, +} from 'node:fs'; +import { build, defineConfig } from 'vite'; +import { fileURLToPath } from 'url'; +import { generateDtsBundle } from 'dts-bundle-generator'; + +function findPluginFiles(folderPath, recursive) { + const pathNames = readdirSync(folderPath); + const matchingFiles = []; + + pathNames.forEach(pathName => { + const fullPath = join(folderPath, pathName); + const stats = statSync(fullPath); + + if (recursive && stats.isDirectory() && pathName === basename(fullPath)) { + const innerFiles = findPluginFiles(fullPath, false); + matchingFiles.push(...innerFiles); + } else if ( + stats.isFile() && + pathName === `${basename(folderPath)}${extname(pathName)}` + ) { + matchingFiles.push([fullPath, basename(folderPath)]); + } + }); + + return matchingFiles; +} + +function convertKebabToCamel(kebabCaseString) { + const words = kebabCaseString.split('-'); + const camelCaseWords = words.map((word, index) => { + if (index === 0) { + return word.charAt(0).toUpperCase() + word.slice(1); + } + return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(); + }); + + return camelCaseWords.join(''); +} + +const __filename = fileURLToPath(import.meta.url); +const currentDir = dirname(__filename); + +const pluginsFolder = resolve(currentDir, 'src', 'plugins'); +const pluginFiles = findPluginFiles(pluginsFolder, true); + +const filesToBuild = pluginFiles.map(([filepath, exportName]) => { + return { + filepath, + exportName, + name: convertKebabToCamel(exportName), + }; +}); + +const compiledFolder = resolve(currentDir, 'compiled'); +if (!existsSync(compiledFolder)) { + mkdirSync(compiledFolder); +} + +const buildConfig = ({ + filepath, + name, + exportName, + formats = ['es', 'umd'], +}) => { + return defineConfig({ + publicDir: false, + build: { + outDir: `compiled/${exportName}`, + emptyOutDir: true, + copyPublicDir: false, + lib: { + entry: filepath, + name, + formats, + fileName: exportName, + }, + rollupOptions: { + external: ['lightweight-charts', 'fancy-canvas'], + output: { + globals: { + 'lightweight-charts': 'LightweightCharts', + }, + }, + }, + }, + }); +}; + +function buildPackageJson(exportName) { + return { + name: exportName, + type: 'module', + main: `./${exportName}.umd.cjs`, + module: `./${exportName}.js`, + exports: { + '.': { + import: `./${exportName}.js`, + require: `./${exportName}.umd.cjs`, + types: `./${exportName}.d.ts`, + }, + }, + }; +} + +const compile = async () => { + const startTime = Date.now().valueOf(); + console.log('⚡️ Starting'); + console.log('Bundling the plugins...'); + const promises = filesToBuild.map(file => { + return build(buildConfig(file)); + }); + await Promise.all(promises); + console.log('Generating the package.json files...'); + filesToBuild.forEach(file => { + const packagePath = resolve( + compiledFolder, + file.exportName, + 'package.json' + ); + const content = JSON.stringify( + buildPackageJson(file.exportName), + undefined, + 4 + ); + writeFileSync(packagePath, content, { encoding: 'utf-8' }); + }); + console.log('Generating the typings files...'); + filesToBuild.forEach(file => { + try { + const esModuleTyping = generateDtsBundle([ + { + filePath: `./typings/plugins/${file.exportName}/${file.exportName}.d.ts`, + // output: { + // umdModuleName: file.name, + // }, + }, + ]); + const typingFilePath = resolve( + compiledFolder, + file.exportName, + `${file.exportName}.d.ts` + ); + writeFileSync(typingFilePath, esModuleTyping.join('\n'), { + encoding: 'utf-8', + }); + } catch (e) { + console.error('Error generating typings for: ', file.exportName); + } + }); + const endTime = Date.now().valueOf(); + console.log(`🎉 Done (${endTime - startTime}ms)`); +}; + +(async () => { + await compile(); + process.exit(0); +})(); diff --git a/plugin-examples/index.html b/plugin-examples/index.html new file mode 100644 index 0000000000..343797abfa --- /dev/null +++ b/plugin-examples/index.html @@ -0,0 +1,9 @@ + + + + + + +

List of example previews are here

+ + diff --git a/plugin-examples/package.json b/plugin-examples/package.json new file mode 100644 index 0000000000..06e3494809 --- /dev/null +++ b/plugin-examples/package.json @@ -0,0 +1,20 @@ +{ + "name": "lightweight-charts-plugin-examples", + "type": "module", + "scripts": { + "build": "tsc", + "compile": "tsc && node compile.mjs", + "dev": "vite --config src/vite.config.js", + "build:examples:site": "vite build --config src/vite.config.js && node build-website.mjs" + }, + "devDependencies": { + "typescript": "^5.0.4", + "vite": "^4.3.1" + }, + "dependencies": { + "dts-bundle-generator": "^8.0.1", + "fancy-canvas": "^2.1.0", + "globby": "^13.1.4", + "lightweight-charts": "file:.." + } +} diff --git a/plugin-examples/src/combined-examples/delta-brushable/example.ts b/plugin-examples/src/combined-examples/delta-brushable/example.ts new file mode 100644 index 0000000000..256195d050 --- /dev/null +++ b/plugin-examples/src/combined-examples/delta-brushable/example.ts @@ -0,0 +1,98 @@ +import { WhitespaceData, createChart } from 'lightweight-charts'; +import { BrushableAreaSeries } from '../../plugins/brushable-area-series/brushable-area-series'; +import { BrushableAreaData } from '../../plugins/brushable-area-series/data'; +import { BrushableAreaStyle } from '../../plugins/brushable-area-series/options'; +import { DeltaTooltipPrimitive } from '../../plugins/delta-tooltip/delta-tooltip'; +import { generateLineData } from '../../sample-data'; + +const chart = ((window as unknown as any).chart = createChart('chart', { + autoSize: true, + grid: { + vertLines: { + visible: false, + }, + horzLines: { + visible: false, + }, + }, + timeScale: { + borderVisible: false, + }, + rightPriceScale: { + borderVisible: false, + }, + handleScale: false, + handleScroll: false, +})); + +const greenStyle: Partial = { + lineColor: 'rgb(4,153,129)', + topColor: 'rgba(4,153,129, 0.4)', + bottomColor: 'rgba(4,153,129, 0)', + lineWidth: 3, +}; + +const redStyle: Partial = { + lineColor: 'rgb(239,83,80)', + topColor: 'rgba(239,83,80, 0.4)', + bottomColor: 'rgba(239,83,80, 0)', + lineWidth: 3, +}; + +const fadeStyle: Partial = { + lineColor: 'rgb(40,98,255, 0.2)', + topColor: 'rgba(40,98,255, 0.05)', + bottomColor: 'rgba(40,98,255, 0)', +}; + +const baseStyle: Partial = { + lineColor: 'rgb(40,98,255)', + topColor: 'rgba(40,98,255, 0.4)', + bottomColor: 'rgba(40,98,255, 0)', +}; + +const customSeriesView = new BrushableAreaSeries(); +const brushAreaSeries = chart.addCustomSeries(customSeriesView, { + /* Options */ + ...baseStyle, + priceLineVisible: false, +}); + +const data: (BrushableAreaData | WhitespaceData)[] = generateLineData(); +brushAreaSeries.setData(data); + +const tooltipPrimitive = new DeltaTooltipPrimitive({ + lineColor: 'rgba(0, 0, 0, 0.2)', +}); + +brushAreaSeries.attachPrimitive(tooltipPrimitive); + +chart.timeScale().fitContent(); + +tooltipPrimitive.activeRange().subscribe(activeRange => { + if (activeRange === null) { + brushAreaSeries.applyOptions({ + brushRanges: [], + ...baseStyle, + }); + return; + } + brushAreaSeries.applyOptions({ + brushRanges: [ + { + range: { + from: activeRange.from, + to: activeRange.to, + }, + style: activeRange.positive ? greenStyle : redStyle, + }, + ], + ...fadeStyle, + }); +}); + +window.addEventListener('resize', () => { + requestAnimationFrame(() => { + chart.timeScale().fitContent(); + }); +}); diff --git a/plugin-examples/src/combined-examples/delta-brushable/index.html b/plugin-examples/src/combined-examples/delta-brushable/index.html new file mode 100644 index 0000000000..2766601e9f --- /dev/null +++ b/plugin-examples/src/combined-examples/delta-brushable/index.html @@ -0,0 +1,31 @@ + + + + + + + Lightweight Charts - Delta Tooltip Plugin & Brushable Area Series Example + + + + + +
+
+

Delta Tooltip and Brushable Area Series

+

HINT: Use multi-touch, or click and drag

+

+ An example showcasing two plugins being used together. The delta tooltip + is used to show the difference between two points (defined by + multi-touch or click and drag), while the brushable area series plugin + will adjust the colours of the area plot to accentuate the effect. +

+
+ + + diff --git a/plugin-examples/src/examples-base.css b/plugin-examples/src/examples-base.css new file mode 100644 index 0000000000..0bd0d6b76d --- /dev/null +++ b/plugin-examples/src/examples-base.css @@ -0,0 +1,69 @@ +:root { + font-family: -apple-system, BlinkMacSystemFont, 'Trebuchet MS', Roboto, Ubuntu, + sans-serif; + line-height: 1.5; + font-weight: 400; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + -webkit-text-size-adjust: 100%; +} +body { + background-color: rgba(248, 249, 253, 1); + color: rgba(19, 23, 34, 1); +} +#chart { + height: 300px; + background-color: rgba(240, 243, 250, 1); + border-radius: 5px; + overflow: hidden; +} +.column, +#chart, +#description { + margin-inline: auto; + max-width: 600px; +} + +.hint-message { + color: rgba(120, 123, 134, 1); +} + +button { + all: initial; + font-family: -apple-system, BlinkMacSystemFont, 'Trebuchet MS', Roboto, Ubuntu, + sans-serif; + font-size: 16px; + font-style: normal; + font-weight: 510; + line-height: 24px; /* 150% */ + letter-spacing: -0.32px; + padding: 8px 24px; + color: #fff; + background-color: rgba(41, 98, 255, 1); + border-radius: 8px; + cursor: pointer; +} + +button:hover { + background-color: rgba(30, 83, 229, 1); +} + +button:active { + background-color: rgba(24, 72, 204, 1); +} + +button.grey { + color: rgba(19, 23, 34, 1); + background-color: rgba(240, 243, 250, 1); +} + +button.grey:hover { + background-color: rgba(224, 227, 235, 1); +} + +button.grey:active { + background-color: rgba(209, 212, 220, 1); +} diff --git a/plugin-examples/src/helpers/assertions.ts b/plugin-examples/src/helpers/assertions.ts new file mode 100644 index 0000000000..e68742f3cf --- /dev/null +++ b/plugin-examples/src/helpers/assertions.ts @@ -0,0 +1,33 @@ +/** + * Ensures that value is defined. + * Throws if the value is undefined, returns the original value otherwise. + * + * @param value - The value, or undefined. + * @returns The passed value, if it is not undefined + */ +export function ensureDefined(value: undefined): never; +export function ensureDefined(value: T | undefined): T; +export function ensureDefined(value: T | undefined): T { + if (value === undefined) { + throw new Error('Value is undefined'); + } + + return value; +} + +/** + * Ensures that value is not null. + * Throws if the value is null, returns the original value otherwise. + * + * @param value - The value, or null. + * @returns The passed value, if it is not null + */ +export function ensureNotNull(value: null): never; +export function ensureNotNull(value: T | null): T; +export function ensureNotNull(value: T | null): T { + if (value === null) { + throw new Error('Value is null'); + } + + return value; +} diff --git a/plugin-examples/src/helpers/closest-index.ts b/plugin-examples/src/helpers/closest-index.ts new file mode 100644 index 0000000000..2bd72d2b1c --- /dev/null +++ b/plugin-examples/src/helpers/closest-index.ts @@ -0,0 +1,44 @@ +export type SearchDirection = 'left' | 'right'; +export class ClosestTimeIndexFinder { + private numbers: T[]; + private cache: Map; + + constructor(sortedNumbers: T[]) { + this.numbers = sortedNumbers; + this.cache = new Map(); + } + + public findClosestIndex(target: number, direction: SearchDirection): number { + const cacheKey = `${target}:${direction}`; + if (this.cache.has(cacheKey)) { + return this.cache.get(cacheKey) as number; + } + + const closestIndex = this._performSearch(target, direction); + + this.cache.set(cacheKey, closestIndex); + return closestIndex; + } + + private _performSearch(target: number, direction: SearchDirection): number { + let low = 0; + let high = this.numbers.length - 1; + + if (target <= this.numbers[0].time) return 0; + if (target >= this.numbers[high].time) return high; + + while (low <= high) { + const mid = Math.floor((low + high) / 2); + const num = this.numbers[mid].time; + + if (num === target) { + return mid; + } else if (num > target) { + high = mid - 1; + } else { + low = mid + 1; + } + } + return direction === 'left' ? low : high; + } +} diff --git a/plugin-examples/src/helpers/delegate.ts b/plugin-examples/src/helpers/delegate.ts new file mode 100644 index 0000000000..d169449d3e --- /dev/null +++ b/plugin-examples/src/helpers/delegate.ts @@ -0,0 +1,67 @@ +export type Callback = (param1: T1) => void; + +export interface ISubscription { + subscribe( + callback: Callback, + linkedObject?: unknown, + singleshot?: boolean + ): void; + unsubscribe(callback: Callback): void; + unsubscribeAll(linkedObject: unknown): void; +} + +interface Listener { + callback: Callback; + linkedObject?: unknown; + singleshot: boolean; +} + +export class Delegate implements ISubscription { + private _listeners: Listener[] = []; + + public subscribe( + callback: Callback, + linkedObject?: unknown, + singleshot?: boolean + ): void { + const listener: Listener = { + callback, + linkedObject, + singleshot: singleshot === true, + }; + this._listeners.push(listener); + } + + public unsubscribe(callback: Callback): void { + const index = this._listeners.findIndex( + (listener: Listener) => callback === listener.callback + ); + if (index > -1) { + this._listeners.splice(index, 1); + } + } + + public unsubscribeAll(linkedObject: unknown): void { + this._listeners = this._listeners.filter( + (listener: Listener) => listener.linkedObject !== linkedObject + ); + } + + public fire(param1: T1): void { + const listenersSnapshot = [...this._listeners]; + this._listeners = this._listeners.filter( + (listener: Listener) => !listener.singleshot + ); + listenersSnapshot.forEach((listener: Listener) => + listener.callback(param1) + ); + } + + public hasListeners(): boolean { + return this._listeners.length > 0; + } + + public destroy(): void { + this._listeners = []; + } +} diff --git a/plugin-examples/src/helpers/dimensions/candles.ts b/plugin-examples/src/helpers/dimensions/candles.ts new file mode 100644 index 0000000000..4a22b0797a --- /dev/null +++ b/plugin-examples/src/helpers/dimensions/candles.ts @@ -0,0 +1,48 @@ +function optimalCandlestickWidth( + barSpacing: number, + pixelRatio: number +): number { + const barSpacingSpecialCaseFrom = 2.5; + const barSpacingSpecialCaseTo = 4; + const barSpacingSpecialCaseCoeff = 3; + if ( + barSpacing >= barSpacingSpecialCaseFrom && + barSpacing <= barSpacingSpecialCaseTo + ) { + return Math.floor(barSpacingSpecialCaseCoeff * pixelRatio); + } + // coeff should be 1 on small barspacing and go to 0.8 while groing bar spacing + const barSpacingReducingCoeff = 0.2; + const coeff = + 1 - + (barSpacingReducingCoeff * + Math.atan( + Math.max(barSpacingSpecialCaseTo, barSpacing) - barSpacingSpecialCaseTo + )) / + (Math.PI * 0.5); + const res = Math.floor(barSpacing * coeff * pixelRatio); + const scaledBarSpacing = Math.floor(barSpacing * pixelRatio); + const optimal = Math.min(res, scaledBarSpacing); + return Math.max(Math.floor(pixelRatio), optimal); +} + +/** + * Calculates the candlestick width that the library would use for the current + * bar spacing. + * @param barSpacing bar spacing in media coordinates + * @param horizontalPixelRatio - horizontal pixel ratio + * @returns The width (in bitmap coordinates) that the chart would use to draw a candle body + */ +export function candlestickWidth( + barSpacing: number, + horizontalPixelRatio: number +): number { + let width = optimalCandlestickWidth(barSpacing, horizontalPixelRatio); + if (width >= 2) { + const wickWidth = Math.floor(horizontalPixelRatio); + if (wickWidth % 2 !== width % 2) { + width--; + } + } + return width; +} diff --git a/plugin-examples/src/helpers/dimensions/columns.ts b/plugin-examples/src/helpers/dimensions/columns.ts new file mode 100644 index 0000000000..de3d12859c --- /dev/null +++ b/plugin-examples/src/helpers/dimensions/columns.ts @@ -0,0 +1,237 @@ +const alignToMinimalWidthLimit = 4; +const showSpacingMinimalBarWidth = 1; + +/** + * Spacing gap between columns. + * @param barSpacingMedia - spacing between bars (media coordinate) + * @param horizontalPixelRatio - horizontal pixel ratio + * @returns Spacing gap between columns (in Bitmap coordinates) + */ +function columnSpacing(barSpacingMedia: number, horizontalPixelRatio: number) { + return Math.ceil(barSpacingMedia * horizontalPixelRatio) <= + showSpacingMinimalBarWidth + ? 0 + : Math.max(1, Math.floor(horizontalPixelRatio)); +} + +/** + * Desired width for columns. This may not be the final width because + * it may be adjusted later to ensure all columns on screen have a + * consistent width and gap. + * @param barSpacingMedia - spacing between bars (media coordinate) + * @param horizontalPixelRatio - horizontal pixel ratio + * @param spacing - Spacing gap between columns (in Bitmap coordinates). (optional, provide if you have already calculated it) + * @returns Desired width for column bars (in Bitmap coordinates) + */ +function desiredColumnWidth( + barSpacingMedia: number, + horizontalPixelRatio: number, + spacing?: number +) { + return ( + Math.round(barSpacingMedia * horizontalPixelRatio) - + (spacing ?? columnSpacing(barSpacingMedia, horizontalPixelRatio)) + ); +} + +interface ColumnCommon { + /** Spacing gap between columns */ + spacing: number; + /** Shift columns left by one pixel */ + shiftLeft: boolean; + /** Half width of a column */ + columnHalfWidthBitmap: number; + /** horizontal pixel ratio */ + horizontalPixelRatio: number; +} + +/** + * Calculated values which are common to all the columns on the screen, and + * are required to calculate the individual positions. + * @param barSpacingMedia - spacing between bars (media coordinate) + * @param horizontalPixelRatio - horizontal pixel ratio + * @returns calculated values for subsequent column calculations + */ +function columnCommon( + barSpacingMedia: number, + horizontalPixelRatio: number +): ColumnCommon { + const spacing = columnSpacing(barSpacingMedia, horizontalPixelRatio); + const columnWidthBitmap = desiredColumnWidth( + barSpacingMedia, + horizontalPixelRatio, + spacing + ); + const shiftLeft = columnWidthBitmap % 2 === 0; + const columnHalfWidthBitmap = (columnWidthBitmap - (shiftLeft ? 0 : 1)) / 2; + return { + spacing, + shiftLeft, + columnHalfWidthBitmap, + horizontalPixelRatio, + }; +} + +export interface ColumnPosition { + left: number; + right: number; + shiftLeft: boolean; +} + +/** + * Calculate the position for a column. These values can be later adjusted + * by a second pass which corrects widths, and shifts columns. + * @param xMedia - column x position (center) in media coordinates + * @param columnData - precalculated common values (returned by `columnCommon`) + * @param previousPosition - result from this function for the previous bar. + * @returns initial column position + */ +function calculateColumnPosition( + xMedia: number, + columnData: ColumnCommon, + previousPosition: ColumnPosition | undefined +): ColumnPosition { + const xBitmapUnRounded = xMedia * columnData.horizontalPixelRatio; + const xBitmap = Math.round(xBitmapUnRounded); + const xPositions: ColumnPosition = { + left: xBitmap - columnData.columnHalfWidthBitmap, + right: + xBitmap + + columnData.columnHalfWidthBitmap - + (columnData.shiftLeft ? 1 : 0), + shiftLeft: xBitmap > xBitmapUnRounded, + }; + const expectedAlignmentShift = columnData.spacing + 1; + if (previousPosition) { + if (xPositions.left - previousPosition.right !== expectedAlignmentShift) { + // need to adjust alignment + if (previousPosition.shiftLeft) { + previousPosition.right = xPositions.left - expectedAlignmentShift; + } else { + xPositions.left = previousPosition.right + expectedAlignmentShift; + } + } + } + return xPositions; +} + +function fixPositionsAndReturnSmallestWidth( + positions: ColumnPosition[], + initialMinWidth: number +): number { + return positions.reduce((smallest: number, position: ColumnPosition) => { + if (position.right < position.left) { + position.right = position.left; + } + const width = position.right - position.left + 1; + return Math.min(smallest, width); + }, initialMinWidth); +} + +function fixAlignmentForNarrowColumns( + positions: ColumnPosition[], + minColumnWidth: number +) { + return positions.map((position: ColumnPosition) => { + const width = position.right - position.left + 1; + if (width <= minColumnWidth) return position; + if (position.shiftLeft) { + position.right -= 1; + } else { + position.left += 1; + } + return position; + }); +} + +/** + * Calculates the column positions and widths for the x positions. + * This function creates a new array. You may get faster performance using the + * `calculateColumnPositionsInPlace` function instead + * @param xMediaPositions - x positions for the bars in media coordinates + * @param barSpacingMedia - spacing between bars in media coordinates + * @param horizontalPixelRatio - horizontal pixel ratio + * @returns Positions for the columns + */ +export function calculateColumnPositions( + xMediaPositions: number[], + barSpacingMedia: number, + horizontalPixelRatio: number +): ColumnPosition[] { + const common = columnCommon(barSpacingMedia, horizontalPixelRatio); + const positions = new Array(xMediaPositions.length); + let previous: ColumnPosition | undefined = undefined; + for (let i = 0; i < xMediaPositions.length; i++) { + positions[i] = calculateColumnPosition( + xMediaPositions[i], + common, + previous + ); + previous = positions[i]; + } + const initialMinWidth = Math.ceil(barSpacingMedia * horizontalPixelRatio); + const minColumnWidth = fixPositionsAndReturnSmallestWidth( + positions, + initialMinWidth + ); + if (common.spacing > 0 && minColumnWidth < alignToMinimalWidthLimit) { + return fixAlignmentForNarrowColumns(positions, minColumnWidth); + } + return positions; +} + +export interface ColumnPositionItem { + x: number; + column?: ColumnPosition; +} + +/** + * Calculates the column positions and widths for bars using the existing the + * array of items. + * @param items - bar items which include an `x` property, and will be mutated to contain a column property + * @param barSpacingMedia - bar spacing in media coordinates + * @param horizontalPixelRatio - horizontal pixel ratio + * @param startIndex - start index for visible bars within the items array + * @param endIndex - end index for visible bars within the items array + */ +export function calculateColumnPositionsInPlace( + items: ColumnPositionItem[], + barSpacingMedia: number, + horizontalPixelRatio: number, + startIndex: number, + endIndex: number +): void { + const common = columnCommon(barSpacingMedia, horizontalPixelRatio); + let previous: ColumnPosition | undefined = undefined; + for (let i = startIndex; i < Math.min(endIndex, items.length); i++) { + items[i].column = calculateColumnPosition(items[i].x, common, previous); + previous = items[i].column; + } + const minColumnWidth = (items as ColumnPositionItem[]).reduce( + (smallest: number, item: ColumnPositionItem, index: number) => { + if (!item.column || index < startIndex || index > endIndex) + return smallest; + if (item.column.right < item.column.left) { + item.column.right = item.column.left; + } + const width = item.column.right - item.column.left + 1; + return Math.min(smallest, width); + }, + Math.ceil(barSpacingMedia * horizontalPixelRatio) + ); + if (common.spacing > 0 && minColumnWidth < alignToMinimalWidthLimit) { + (items as ColumnPositionItem[]).forEach( + (item: ColumnPositionItem, index: number) => { + if (!item.column || index < startIndex || index > endIndex) return; + const width = item.column.right - item.column.left + 1; + if (width <= minColumnWidth) return item; + if (item.column.shiftLeft) { + item.column.right -= 1; + } else { + item.column.left += 1; + } + return item.column; + } + ); + } +} diff --git a/plugin-examples/src/helpers/dimensions/common.ts b/plugin-examples/src/helpers/dimensions/common.ts new file mode 100644 index 0000000000..394c6bb9ad --- /dev/null +++ b/plugin-examples/src/helpers/dimensions/common.ts @@ -0,0 +1,6 @@ +export interface BitmapPositionLength { + /** coordinate for use with a bitmap rendering scope */ + position: number; + /** length for use with a bitmap rendering scope */ + length: number; +} diff --git a/plugin-examples/src/helpers/dimensions/crosshair-width.ts b/plugin-examples/src/helpers/dimensions/crosshair-width.ts new file mode 100644 index 0000000000..9d14991aaf --- /dev/null +++ b/plugin-examples/src/helpers/dimensions/crosshair-width.ts @@ -0,0 +1,23 @@ +/** + * Default grid / crosshair line width in Bitmap sizing + * @param horizontalPixelRatio - horizontal pixel ratio + * @returns default grid / crosshair line width in Bitmap sizing + */ +export function gridAndCrosshairBitmapWidth( + horizontalPixelRatio: number +): number { + return Math.max(1, Math.floor(horizontalPixelRatio)); +} + +/** + * Default grid / crosshair line width in Media sizing + * @param horizontalPixelRatio - horizontal pixel ratio + * @returns default grid / crosshair line width in Media sizing + */ +export function gridAndCrosshairMediaWidth( + horizontalPixelRatio: number +): number { + return ( + gridAndCrosshairBitmapWidth(horizontalPixelRatio) / horizontalPixelRatio + ); +} diff --git a/plugin-examples/src/helpers/dimensions/full-width.ts b/plugin-examples/src/helpers/dimensions/full-width.ts new file mode 100644 index 0000000000..6f4d40dd41 --- /dev/null +++ b/plugin-examples/src/helpers/dimensions/full-width.ts @@ -0,0 +1,29 @@ +import { BitmapPositionLength } from './common'; + +/** + * Calculates the position and width which will completely full the space for the bar. + * Useful if you want to draw something that will not have any gaps between surrounding bars. + * @param xMedia - x coordinate of the bar defined in media sizing + * @param halfBarSpacingMedia - half the width of the current barSpacing (un-rounded) + * @param horizontalPixelRatio - horizontal pixel ratio + * @returns position and width which will completely full the space for the bar + */ +export function fullBarWidth( + xMedia: number, + halfBarSpacingMedia: number, + horizontalPixelRatio: number +): BitmapPositionLength { + const fullWidthLeftMedia = xMedia - halfBarSpacingMedia; + const fullWidthRightMedia = xMedia + halfBarSpacingMedia; + const fullWidthLeftBitmap = Math.round( + fullWidthLeftMedia * horizontalPixelRatio + ); + const fullWidthRightBitmap = Math.round( + fullWidthRightMedia * horizontalPixelRatio + ); + const fullWidthBitmap = fullWidthRightBitmap - fullWidthLeftBitmap; + return { + position: fullWidthLeftBitmap, + length: fullWidthBitmap, + }; +} diff --git a/plugin-examples/src/helpers/dimensions/positions.ts b/plugin-examples/src/helpers/dimensions/positions.ts new file mode 100644 index 0000000000..a021b072b0 --- /dev/null +++ b/plugin-examples/src/helpers/dimensions/positions.ts @@ -0,0 +1,48 @@ +import { BitmapPositionLength } from './common'; + +function centreOffset(lineBitmapWidth: number): number { + return Math.floor(lineBitmapWidth * 0.5); +} + +/** + * Calculates the bitmap position for an item with a desired length (height or width), and centred according to + * an position coordinate defined in media sizing. + * @param positionMedia - position coordinate for the bar (in media coordinates) + * @param pixelRatio - pixel ratio. Either horizontal for x positions, or vertical for y positions + * @param desiredWidthMedia - desired width (in media coordinates) + * @returns Position of of the start point and length dimension. + */ +export function positionsLine( + positionMedia: number, + pixelRatio: number, + desiredWidthMedia: number = 1, + widthIsBitmap?: boolean +): BitmapPositionLength { + const scaledPosition = Math.round(pixelRatio * positionMedia); + const lineBitmapWidth = widthIsBitmap + ? desiredWidthMedia + : Math.round(desiredWidthMedia * pixelRatio); + const offset = centreOffset(lineBitmapWidth); + const position = scaledPosition - offset; + return { position, length: lineBitmapWidth }; +} + +/** + * Determines the bitmap position and length for a dimension of a shape to be drawn. + * @param position1Media - media coordinate for the first point + * @param position2Media - media coordinate for the second point + * @param pixelRatio - pixel ratio for the corresponding axis (vertical or horizontal) + * @returns Position of of the start point and length dimension. + */ +export function positionsBox( + position1Media: number, + position2Media: number, + pixelRatio: number +): BitmapPositionLength { + const scaledPosition1 = Math.round(pixelRatio * position1Media); + const scaledPosition2 = Math.round(pixelRatio * position2Media); + return { + position: Math.min(scaledPosition1, scaledPosition2), + length: Math.abs(scaledPosition2 - scaledPosition1) + 1, + }; +} diff --git a/plugin-examples/src/helpers/min-max-in-range.ts b/plugin-examples/src/helpers/min-max-in-range.ts new file mode 100644 index 0000000000..3400075845 --- /dev/null +++ b/plugin-examples/src/helpers/min-max-in-range.ts @@ -0,0 +1,69 @@ +interface UpperLowerData { + upper: number; + lower: number; +} +export class UpperLowerInRange { + private _arr: T[]; + private _chunkSize: number; + private _cache: Map; + + constructor(arr: T[], chunkSize = 10) { + this._arr = arr; + this._chunkSize = chunkSize; + this._cache = new Map(); + } + + public getMinMax(startIndex: number, endIndex: number): UpperLowerData { + const cacheKey = `${startIndex}:${endIndex}`; + if (cacheKey in this._cache) { + return this._cache.get(cacheKey) as T; + } + + const result: UpperLowerData = { + lower: Infinity, + upper: -Infinity, + }; + // Check if we have precalculated min and max values for any of the chunks. + const startChunkIndex = Math.floor(startIndex / this._chunkSize); + const endChunkIndex = Math.floor(endIndex / this._chunkSize); + for ( + let chunkIndex = startChunkIndex; + chunkIndex <= endChunkIndex; + chunkIndex++ + ) { + const chunkStart = chunkIndex * this._chunkSize; + const chunkEnd = Math.min( + (chunkIndex + 1) * this._chunkSize - 1, + this._arr.length - 1 + ); + const chunkCacheKey = `${chunkStart}:${chunkEnd}`; + + if (chunkCacheKey in this._cache.keys()) { + const item = this._cache.get(cacheKey) as T; + this._check(item, result); + } else { + const chunkResult: UpperLowerData = { + lower: Infinity, + upper: -Infinity, + }; + for (let i = chunkStart; i <= chunkEnd; i++) { + this._check(this._arr[i], chunkResult); + } + this._cache.set(chunkCacheKey, chunkResult); + this._check(chunkResult, result); + } + } + + this._cache.set(cacheKey, result); + return result; + } + + private _check(item: UpperLowerData, state: UpperLowerData) { + if (item.lower < state.lower) { + state.lower = item.lower; + } + if (item.upper > state.upper) { + state.upper = item.upper; + } + } +} diff --git a/plugin-examples/src/helpers/simple-clone.ts b/plugin-examples/src/helpers/simple-clone.ts new file mode 100644 index 0000000000..8f9a77e5ee --- /dev/null +++ b/plugin-examples/src/helpers/simple-clone.ts @@ -0,0 +1,7 @@ +type Mutable = { + -readonly [K in keyof T]: T[K] +} + +export function cloneReadonly(obj: T): Mutable { + return JSON.parse(JSON.stringify(obj)); +} diff --git a/plugin-examples/src/helpers/time.ts b/plugin-examples/src/helpers/time.ts new file mode 100644 index 0000000000..ed654758a1 --- /dev/null +++ b/plugin-examples/src/helpers/time.ts @@ -0,0 +1,34 @@ +import { Time, isUTCTimestamp, isBusinessDay } from 'lightweight-charts'; + +export function convertTime(t: Time): number { + if (isUTCTimestamp(t)) return t * 1000; + if (isBusinessDay(t)) return new Date(t.year, t.month, t.day).valueOf(); + const [year, month, day] = t.split('-').map(parseInt); + return new Date(year, month, day).valueOf(); +} + +export function displayTime(time: Time): string { + if (typeof time == 'string') return time; + const date = isBusinessDay(time) + ? new Date(time.year, time.month, time.day) + : new Date(time * 1000); + return date.toLocaleDateString(); +} + +export function formattedDateAndTime(timestamp: number | undefined): [string, string] { + if (!timestamp) return ['', '']; + const dateObj = new Date(timestamp); + + // Format date string + const year = dateObj.getFullYear(); + const month = dateObj.toLocaleString('default', { month: 'short' }); + const date = dateObj.getDate().toString().padStart(2, '0'); + const formattedDate = `${date} ${month} ${year}`; + + // Format time string + const hours = dateObj.getHours().toString().padStart(2, '0'); + const minutes = dateObj.getMinutes().toString().padStart(2, '0'); + const formattedTime = `${hours}:${minutes}`; + + return [formattedDate, formattedTime]; +} diff --git a/plugin-examples/src/index.html b/plugin-examples/src/index.html new file mode 100644 index 0000000000..fe66c61b1e --- /dev/null +++ b/plugin-examples/src/index.html @@ -0,0 +1,59 @@ + + + + + + Lightweight Charts - Plugin Examples + + + + +

+ Lightweight Charts™ + Plugin Examples +

+

+

+

+

Combined Examples

+ +

Custom Series

+ +

Primitives

+ + + diff --git a/plugin-examples/src/plugins/anchored-text/anchored-text.ts b/plugin-examples/src/plugins/anchored-text/anchored-text.ts new file mode 100644 index 0000000000..65b22c3591 --- /dev/null +++ b/plugin-examples/src/plugins/anchored-text/anchored-text.ts @@ -0,0 +1,107 @@ +import { CanvasRenderingTarget2D } from 'fancy-canvas'; +import { + ISeriesPrimitive, + ISeriesPrimitivePaneRenderer, + ISeriesPrimitivePaneView, + SeriesAttachedParameter, + Time, +} from 'lightweight-charts'; + +interface AnchoredTextOptions { + vertAlign: 'top' | 'middle' | 'bottom'; + horzAlign: 'left' | 'middle' | 'right'; + text: string; + lineHeight: number; + font: string; + color: string; +} + +class AnchoredTextRenderer implements ISeriesPrimitivePaneRenderer { + _data: AnchoredTextOptions; + + constructor(options: AnchoredTextOptions) { + this._data = options; + } + + draw(target: CanvasRenderingTarget2D) { + target.useMediaCoordinateSpace(scope => { + const ctx = scope.context; + ctx.save(); + ctx.font = this._data.font; + const textWidth = ctx.measureText(this._data.text).width; + const horzMargin = 20; + let x = horzMargin; + const width = scope.mediaSize.width; + const height = scope.mediaSize.height; + switch (this._data.horzAlign) { + case 'right': { + x = width - horzMargin - textWidth; + break; + } + case 'middle': { + x = width / 2 - textWidth / 2; + break; + } + } + const vertMargin = 10; + const lineHeight = this._data.lineHeight; + let y = vertMargin + lineHeight; + switch (this._data.vertAlign) { + case 'middle': { + y = height / 2 + lineHeight / 2; + break; + } + case 'bottom': { + y = height - vertMargin; + break; + } + } + ctx.fillStyle = this._data.color; + ctx.fillText(this._data.text, x, y); + ctx.restore(); + }); + } +} + +class AnchoredTextPaneView implements ISeriesPrimitivePaneView { + private _source: AnchoredText; + constructor(source: AnchoredText) { + this._source = source; + } + update() {} + renderer() { + return new AnchoredTextRenderer(this._source._data); + } +} + +export class AnchoredText implements ISeriesPrimitive