From 0be88d7908197ffcf0be8848cbbe096016e1529e Mon Sep 17 00:00:00 2001 From: Elias <36868565+Prodigist@users.noreply.github.com> Date: Sat, 1 Feb 2025 09:31:17 +0000 Subject: [PATCH] Initial commit: ReactiveNotes plugin --- .editorconfig | 10 + .env | 6 + .env.example | 5 + .eslintignore | 3 + .eslintrc | 23 + .gitignore | Bin 0 -> 328 bytes .npmrc | 1 + README.md | 94 + esbuild.config.mjs | 66 + lwc-plugin-custom-primitives/.gitignore | 24 + lwc-plugin-custom-primitives/README.md | 38 + lwc-plugin-custom-primitives/compile.mjs | 116 + lwc-plugin-custom-primitives/index.html | 7 + lwc-plugin-custom-primitives/package.json | 17 + .../src/axis-pane-renderer.ts | 41 + .../src/axis-pane-view.ts | 55 + lwc-plugin-custom-primitives/src/axis-view.ts | 57 + .../src/custom-primitives.ts | 139 + .../src/data-source.ts | 20 + .../src/example/example.ts | 23 + .../src/example/index.html | 26 + .../src/helpers/assertions.ts | 33 + .../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/time.ts | 34 + lwc-plugin-custom-primitives/src/options.ts | 27 + .../src/pane-renderer.ts | 46 + lwc-plugin-custom-primitives/src/pane-view.ts | 37 + .../src/plugin-base.ts | 56 + .../src/sample-data.ts | 35 + .../src/vite-env.d.ts | 1 + .../src/vite.config.js | 13 + lwc-plugin-custom-primitives/tsconfig.json | 21 + main.tsx | 407 ++ manifest.json | 11 + package-lock.json | 6204 +++++++++++++++++ package.json | 58 + react-shim.js | 4 + src/components/ComponentRenderer.tsx | 133 + src/components/ErrorBoundary.tsx | 135 + src/components/OrderblockVisualiser.ts | 36 + src/components/RectPlugin.ts | 92 + src/components/styled.tsx | 57 + src/components/ui/card - Copy - Copy.tsx | 0 src/components/ui/card.tsx | 79 + src/components/ui/switch.tsx | 29 + src/components/ui/tabs.tsx | 52 + src/core/dynamic.tsx | 84 + src/core/registry.tsx | 47 + src/core/scope.ts | 212 + src/core/storage.ts | 109 + src/core/styles.ts | 80 + src/core/tailwind.ts | 34 + src/core/transformer.ts | 205 + src/core/useMarketData.tsx | 98 + src/hooks/useStorage.ts | 27 + src/lib/utils.ts | 7 + src/services/OrderBlockAnalysis.ts | 793 +++ src/services/marketDataService.ts | 459 ++ src/services/marketDataStorage.ts | 257 + src/types/babel-standalone.d.ts | 31 + src/types/global.d.ts | 7 + styles.css | 335 + tsconfig.json | 37 + version-bump.mjs | 14 + versions.json | 3 + 68 files changed, 11316 insertions(+) create mode 100644 .editorconfig create mode 100644 .env create mode 100644 .env.example create mode 100644 .eslintignore create mode 100644 .eslintrc create mode 100644 .gitignore create mode 100644 .npmrc create mode 100644 README.md create mode 100644 esbuild.config.mjs create mode 100644 lwc-plugin-custom-primitives/.gitignore create mode 100644 lwc-plugin-custom-primitives/README.md create mode 100644 lwc-plugin-custom-primitives/compile.mjs create mode 100644 lwc-plugin-custom-primitives/index.html create mode 100644 lwc-plugin-custom-primitives/package.json create mode 100644 lwc-plugin-custom-primitives/src/axis-pane-renderer.ts create mode 100644 lwc-plugin-custom-primitives/src/axis-pane-view.ts create mode 100644 lwc-plugin-custom-primitives/src/axis-view.ts create mode 100644 lwc-plugin-custom-primitives/src/custom-primitives.ts create mode 100644 lwc-plugin-custom-primitives/src/data-source.ts create mode 100644 lwc-plugin-custom-primitives/src/example/example.ts create mode 100644 lwc-plugin-custom-primitives/src/example/index.html create mode 100644 lwc-plugin-custom-primitives/src/helpers/assertions.ts create mode 100644 lwc-plugin-custom-primitives/src/helpers/dimensions/common.ts create mode 100644 lwc-plugin-custom-primitives/src/helpers/dimensions/crosshair-width.ts create mode 100644 lwc-plugin-custom-primitives/src/helpers/dimensions/full-width.ts create mode 100644 lwc-plugin-custom-primitives/src/helpers/dimensions/positions.ts create mode 100644 lwc-plugin-custom-primitives/src/helpers/time.ts create mode 100644 lwc-plugin-custom-primitives/src/options.ts create mode 100644 lwc-plugin-custom-primitives/src/pane-renderer.ts create mode 100644 lwc-plugin-custom-primitives/src/pane-view.ts create mode 100644 lwc-plugin-custom-primitives/src/plugin-base.ts create mode 100644 lwc-plugin-custom-primitives/src/sample-data.ts create mode 100644 lwc-plugin-custom-primitives/src/vite-env.d.ts create mode 100644 lwc-plugin-custom-primitives/src/vite.config.js create mode 100644 lwc-plugin-custom-primitives/tsconfig.json create mode 100644 main.tsx create mode 100644 manifest.json create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 react-shim.js create mode 100644 src/components/ComponentRenderer.tsx create mode 100644 src/components/ErrorBoundary.tsx create mode 100644 src/components/OrderblockVisualiser.ts create mode 100644 src/components/RectPlugin.ts create mode 100644 src/components/styled.tsx create mode 100644 src/components/ui/card - Copy - Copy.tsx create mode 100644 src/components/ui/card.tsx create mode 100644 src/components/ui/switch.tsx create mode 100644 src/components/ui/tabs.tsx create mode 100644 src/core/dynamic.tsx create mode 100644 src/core/registry.tsx create mode 100644 src/core/scope.ts create mode 100644 src/core/storage.ts create mode 100644 src/core/styles.ts create mode 100644 src/core/tailwind.ts create mode 100644 src/core/transformer.ts create mode 100644 src/core/useMarketData.tsx create mode 100644 src/hooks/useStorage.ts create mode 100644 src/lib/utils.ts create mode 100644 src/services/OrderBlockAnalysis.ts create mode 100644 src/services/marketDataService.ts create mode 100644 src/services/marketDataStorage.ts create mode 100644 src/types/babel-standalone.d.ts create mode 100644 src/types/global.d.ts create mode 100644 styles.css create mode 100644 tsconfig.json create mode 100644 version-bump.mjs create mode 100644 versions.json diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..81f3ec3 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,10 @@ +# top-most EditorConfig file +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +indent_style = tab +indent_size = 4 +tab_width = 4 diff --git a/.env b/.env new file mode 100644 index 0000000..edde2fa --- /dev/null +++ b/.env @@ -0,0 +1,6 @@ +// .env file (Do NOT commit this file) +ALPHA_VANTAGE_PRIMARY_KEY=7NMNWUG3QGNSK9J2 +ALPHA_VANTAGE_SECONDARY_KEY=9H7UI9KW24V8UXCW +MARKET_DATA_CACHE_DURATION=86400000 +TIMEZONE=US/Eastern + diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..f249cda --- /dev/null +++ b/.env.example @@ -0,0 +1,5 @@ +// .env.example file +ALPHA_VANTAGE_PRIMARY_KEY=your_primary_key_here +ALPHA_VANTAGE_SECONDARY_KEY=your_secondary_key_here +MARKET_DATA_CACHE_DURATION=86400000 +TIMEZONE=US/Eastern \ No newline at end of file diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..e019f3c --- /dev/null +++ b/.eslintignore @@ -0,0 +1,3 @@ +node_modules/ + +main.js diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..0807290 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,23 @@ +{ + "root": true, + "parser": "@typescript-eslint/parser", + "env": { "node": true }, + "plugins": [ + "@typescript-eslint" + ], + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/eslint-recommended", + "plugin:@typescript-eslint/recommended" + ], + "parserOptions": { + "sourceType": "module" + }, + "rules": { + "no-unused-vars": "off", + "@typescript-eslint/no-unused-vars": ["error", { "args": "none" }], + "@typescript-eslint/ban-ts-comment": "off", + "no-prototype-builtins": "off", + "@typescript-eslint/no-empty-function": "off" + } + } \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..7c1cda368954241d54d88723dcb735cb753d3134 GIT binary patch literal 328 zcmXw!y>0{{41}qXcn3>YB)Nj8xcpTqHcES9*N)aFz$<<(xwnruBozanhZ!H>S{Ffb z)(-*BI(vZE+$cuAID7P*qGCo0gXJwbo7Hqqf<00l&nL-u4LC2cBSBjU7D*dNLP&wL zzf}0tn@;?~LYw$K>V2gf)Kz*!mp8H@m61bs7DEf#Lsag`|GpHKw+Ws&BNe!xZW@vlYLP You can simplify the version bump process by running `npm version patch`, `npm version minor` or `npm version major` after updating `minAppVersion` manually in `manifest.json`. +> The command will bump version in `manifest.json` and `package.json`, and add the entry for the new version to `versions.json` + +## Adding your plugin to the community plugin list + +- Check the [plugin guidelines](https://docs.obsidian.md/Plugins/Releasing/Plugin+guidelines). +- Publish an initial version. +- Make sure you have a `README.md` file in the root of your repo. +- Make a pull request at https://github.com/obsidianmd/obsidian-releases to add your plugin. + +## How to use + +- Clone this repo. +- Make sure your NodeJS is at least v16 (`node --version`). +- `npm i` or `yarn` to install dependencies. +- `npm run dev` to start compilation in watch mode. + +## Manually installing the plugin + +- Copy over `main.js`, `styles.css`, `manifest.json` to your vault `VaultFolder/.obsidian/plugins/your-plugin-id/`. + +## Improve code quality with eslint (optional) +- [ESLint](https://eslint.org/) is a tool that analyzes your code to quickly find problems. You can run ESLint against your plugin to find common bugs and ways to improve your code. +- To use eslint with this project, make sure to install eslint from terminal: + - `npm install -g eslint` +- To use eslint to analyze this project use this command: + - `eslint main.ts` + - eslint will then create a report with suggestions for code improvement by file and line number. +- If your source code is in a folder, such as `src`, you can use eslint with this command to analyze all files in that folder: + - `eslint .\src\` + +## Funding URL + +You can include funding URLs where people who use your plugin can financially support it. + +The simple way is to set the `fundingUrl` field to your link in your `manifest.json` file: + +```json +{ + "fundingUrl": "https://buymeacoffee.com" +} +``` + +If you have multiple URLs, you can also do: + +```json +{ + "fundingUrl": { + "Buy Me a Coffee": "https://buymeacoffee.com", + "GitHub Sponsor": "https://github.com/sponsors", + "Patreon": "https://www.patreon.com/" + } +} +``` + +## API Documentation + +See https://github.com/obsidianmd/obsidian-api diff --git a/esbuild.config.mjs b/esbuild.config.mjs new file mode 100644 index 0000000..596aeaf --- /dev/null +++ b/esbuild.config.mjs @@ -0,0 +1,66 @@ +import esbuild from "esbuild"; +import process from "process"; +import builtins from "builtin-modules"; + +const banner = +`/* +THIS IS A GENERATED/BUNDLED FILE BY ESBUILD +if you want to view the source, please visit the github repository of this plugin +*/ +`; + +const prod = (process.argv[2] === "production"); + +const context = await esbuild.context({ + banner: { + js: banner, + }, + entryPoints: ["main.tsx"], + bundle: true, + external: [ + "obsidian", + "electron", + "@codemirror/autocomplete", + "@codemirror/collab", + "@codemirror/commands", + "@codemirror/language", + "@codemirror/lint", + "@codemirror/search", + "@codemirror/state", + "@codemirror/view", + "@lezer/common", + "@lezer/highlight", + "@lezer/lr", + ...builtins], + format: "cjs", + target: "es2020", + platform: "node", // Add this for Node.js compatibility + logLevel: "info", + sourcemap: prod ? false : "inline", + treeShaking: true, + outfile: "main.js", + minify: prod, + jsx: "transform", // Add this line + loader: { // Add loader configuration + '.tsx': 'tsx', + '.ts': 'ts', + '.jsx': 'jsx', + '.js': 'js', + '.css': 'css', // Add CSS loader + '.json': 'json' // Add JSON loader for Tailwind config + }, + define: { // Add environment definitions + 'process.env.NODE_ENV': prod ? '"production"' : '"development"', + global: 'window' + }, + inject: [ // Add React shim + './react-shim.js' + ], +}); + +if (prod) { + await context.rebuild(); + process.exit(0); +} else { + await context.watch(); +} diff --git a/lwc-plugin-custom-primitives/.gitignore b/lwc-plugin-custom-primitives/.gitignore new file mode 100644 index 0000000..a90cc13 --- /dev/null +++ b/lwc-plugin-custom-primitives/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +typings +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/lwc-plugin-custom-primitives/README.md b/lwc-plugin-custom-primitives/README.md new file mode 100644 index 0000000..211250c --- /dev/null +++ b/lwc-plugin-custom-primitives/README.md @@ -0,0 +1,38 @@ +# Custom Primitives - Lightweight Charts™ Plugin + +Description of the Plugin. + +- Developed for Lightweight Charts version: `v4.1.0` + +## Running Locally + +```shell +npm install +npm run dev +``` + +Visit `localhost:5173` in the browser. + +## Compiling + +```shell +npm run compile +``` + +Check the output in the `dist` folder. + +## Publishing To NPM + +You can configure the contents of the package's `package.json` within the +`compile.mjs` script. + +Once you have compiled the plugin (see above section) then you can publish the +package to NPM with these commands: + +```shell +cd dist +npm publish +``` + +Hint: append `--dry-run` to the end of the publish command to see the results of +the publish command without actually uploading the package to NPM. diff --git a/lwc-plugin-custom-primitives/compile.mjs b/lwc-plugin-custom-primitives/compile.mjs new file mode 100644 index 0000000..ffc34b5 --- /dev/null +++ b/lwc-plugin-custom-primitives/compile.mjs @@ -0,0 +1,116 @@ +import { dirname, resolve } from 'node:path'; +import { copyFileSync, existsSync, mkdirSync, writeFileSync } from 'node:fs'; +import { build, defineConfig } from 'vite'; +import { fileURLToPath } from 'url'; +import { generateDtsBundle } from 'dts-bundle-generator'; + +function buildPackageJson(packageName) { + /* + Define the contents of the package's package.json here. + */ + return { + name: packageName, + version: '1.0.0', + keywords: ['lwc-plugin', 'lightweight-charts'], + type: 'module', + main: `./${packageName}.umd.cjs`, + module: `./${packageName}.js`, + types: `./${packageName}.d.ts`, + exports: { + import: { + types: `./${packageName}.d.ts`, + default: `./${packageName}.js`, + }, + require: { + types: `./${packageName}.d.cts`, + default: `./${packageName}.umd.cjs`, + }, + }, + }; +} + +const __filename = fileURLToPath(import.meta.url); +const currentDir = dirname(__filename); + +const pluginFileName = 'custom-primitives'; +const pluginFile = resolve(currentDir, 'src', `${pluginFileName}.ts`); + +const pluginsToBuild = [ + { + filepath: pluginFile, + exportName: 'lwc-plugin-custom-primitives', + name: 'CustomPrimitives', + }, +]; + +const compiledFolder = resolve(currentDir, 'dist'); +if (!existsSync(compiledFolder)) { + mkdirSync(compiledFolder); +} + +const buildConfig = ({ + filepath, + name, + exportName, + formats = ['es', 'umd'], +}) => { + return defineConfig({ + publicDir: false, + build: { + outDir: `dist`, + emptyOutDir: true, + copyPublicDir: false, + lib: { + entry: filepath, + name, + formats, + fileName: exportName, + }, + rollupOptions: { + external: ['lightweight-charts', 'fancy-canvas'], + output: { + globals: { + 'lightweight-charts': 'LightweightCharts', + }, + }, + }, + }, + }); +}; + +const startTime = Date.now().valueOf(); +console.log('⚡️ Starting'); +console.log('Bundling the plugin...'); +const promises = pluginsToBuild.map(file => { + return build(buildConfig(file)); +}); +await Promise.all(promises); +console.log('Generating the package.json file...'); +pluginsToBuild.forEach(file => { + const packagePath = resolve(compiledFolder, 'package.json'); + const content = JSON.stringify( + buildPackageJson(file.exportName), + undefined, + 4 + ); + writeFileSync(packagePath, content, { encoding: 'utf-8' }); +}); +console.log('Generating the typings files...'); +pluginsToBuild.forEach(file => { + try { + const esModuleTyping = generateDtsBundle([ + { + filePath: `./typings/${pluginFileName}.d.ts`, + }, + ]); + const typingFilePath = resolve(compiledFolder, `${file.exportName}.d.ts`); + writeFileSync(typingFilePath, esModuleTyping.join('\n'), { + encoding: 'utf-8', + }); + copyFileSync(typingFilePath, resolve(compiledFolder, `${file.exportName}.d.cts`)); + } catch (e) { + console.error('Error generating typings for: ', file.exportName); + } +}); +const endTime = Date.now().valueOf(); +console.log(`🎉 Done (${endTime - startTime}ms)`); diff --git a/lwc-plugin-custom-primitives/index.html b/lwc-plugin-custom-primitives/index.html new file mode 100644 index 0000000..3f1b3a1 --- /dev/null +++ b/lwc-plugin-custom-primitives/index.html @@ -0,0 +1,7 @@ + + + + + + + diff --git a/lwc-plugin-custom-primitives/package.json b/lwc-plugin-custom-primitives/package.json new file mode 100644 index 0000000..6bf8786 --- /dev/null +++ b/lwc-plugin-custom-primitives/package.json @@ -0,0 +1,17 @@ +{ + "name": "lwc-plugin-custom-primitives", + "type": "module", + "scripts": { + "dev": "vite --config src/vite.config.js", + "compile": "tsc && node compile.mjs" + }, + "devDependencies": { + "typescript": "^5.0.4", + "vite": "^4.3.1" + }, + "dependencies": { + "dts-bundle-generator": "^8.0.1", + "fancy-canvas": "^2.1.0", + "lightweight-charts": "^4.1.0-rc2" + } +} diff --git a/lwc-plugin-custom-primitives/src/axis-pane-renderer.ts b/lwc-plugin-custom-primitives/src/axis-pane-renderer.ts new file mode 100644 index 0000000..49380b2 --- /dev/null +++ b/lwc-plugin-custom-primitives/src/axis-pane-renderer.ts @@ -0,0 +1,41 @@ +import { CanvasRenderingTarget2D } from 'fancy-canvas'; +import { ISeriesPrimitivePaneRenderer } from 'lightweight-charts'; +import { positionsBox } from './helpers/dimensions/positions'; + +export class CustomPrimitivesAxisPaneRenderer implements ISeriesPrimitivePaneRenderer { + _p1: number | null; + _p2: number | null; + _fillColor: string; + _vertical: boolean = false; + + constructor( + p1: number | null, + p2: number | null, + fillColor: string, + vertical: boolean + ) { + this._p1 = p1; + this._p2 = p2; + this._fillColor = fillColor; + this._vertical = vertical; + } + + draw(target: CanvasRenderingTarget2D) { + target.useBitmapCoordinateSpace(scope => { + if (this._p1 === null || this._p2 === null) return; + const ctx = scope.context; + ctx.globalAlpha = 0.5; + const positions = positionsBox( + this._p1, + this._p2, + this._vertical ? scope.verticalPixelRatio : scope.horizontalPixelRatio + ); + ctx.fillStyle = this._fillColor; + if (this._vertical) { + ctx.fillRect(0, positions.position, 15, positions.length); + } else { + ctx.fillRect(positions.position, 0, positions.length, 15); + } + }); + } +} diff --git a/lwc-plugin-custom-primitives/src/axis-pane-view.ts b/lwc-plugin-custom-primitives/src/axis-pane-view.ts new file mode 100644 index 0000000..a040e98 --- /dev/null +++ b/lwc-plugin-custom-primitives/src/axis-pane-view.ts @@ -0,0 +1,55 @@ +import { + Coordinate, + ISeriesPrimitivePaneView, + SeriesPrimitivePaneViewZOrder, +} from 'lightweight-charts'; +import { CustomPrimitivesAxisPaneRenderer } from './axis-pane-renderer'; +import { CustomPrimitivesDataSource } from './data-source'; + +abstract class CustomPrimitivesAxisPaneView implements ISeriesPrimitivePaneView { + _source: CustomPrimitivesDataSource; + _p1: number | null = null; + _p2: number | null = null; + _vertical: boolean = false; + + constructor(source: CustomPrimitivesDataSource, vertical: boolean) { + this._source = source; + this._vertical = vertical; + } + + abstract getPoints(): [Coordinate | null, Coordinate | null]; + + update() { + [this._p1, this._p2] = this.getPoints(); + } + + renderer() { + return new CustomPrimitivesAxisPaneRenderer( + this._p1, + this._p2, + this._source.options.fillColor, + this._vertical + ); + } + zOrder(): SeriesPrimitivePaneViewZOrder { + return 'bottom'; + } +} + +export class CustomPrimitivesPriceAxisPaneView extends CustomPrimitivesAxisPaneView { + getPoints(): [Coordinate | null, Coordinate | null] { + const series = this._source.series; + const y1 = series.priceToCoordinate(this._source.p1.price); + const y2 = series.priceToCoordinate(this._source.p2.price); + return [y1, y2]; + } +} + +export class CustomPrimitivesTimeAxisPaneView extends CustomPrimitivesAxisPaneView { + getPoints(): [Coordinate | null, Coordinate | null] { + const timeScale = this._source.chart.timeScale(); + const x1 = timeScale.timeToCoordinate(this._source.p1.time); + const x2 = timeScale.timeToCoordinate(this._source.p2.time); + return [x1, x2]; + } +} diff --git a/lwc-plugin-custom-primitives/src/axis-view.ts b/lwc-plugin-custom-primitives/src/axis-view.ts new file mode 100644 index 0000000..435f73f --- /dev/null +++ b/lwc-plugin-custom-primitives/src/axis-view.ts @@ -0,0 +1,57 @@ +import { Coordinate, ISeriesPrimitiveAxisView } from 'lightweight-charts'; +import { Point, CustomPrimitivesDataSource } from './data-source'; + +abstract class CustomPrimitivesAxisView implements ISeriesPrimitiveAxisView { + _source: CustomPrimitivesDataSource; + _p: Point; + _pos: Coordinate | null = null; + constructor(source: CustomPrimitivesDataSource, p: Point) { + this._source = source; + this._p = p; + } + abstract update(): void; + abstract text(): string; + + coordinate() { + return this._pos ?? -1; + } + + visible(): boolean { + return this._source.options.showLabels; + } + + tickVisible(): boolean { + return this._source.options.showLabels; + } + + textColor() { + return this._source.options.labelTextColor; + } + backColor() { + return this._source.options.labelColor; + } + movePoint(p: Point) { + this._p = p; + this.update(); + } +} + +export class CustomPrimitivesTimeAxisView extends CustomPrimitivesAxisView { + update() { + const timeScale = this._source.chart.timeScale(); + this._pos = timeScale.timeToCoordinate(this._p.time); + } + text() { + return this._source.options.timeLabelFormatter(this._p.time); + } +} + +export class CustomPrimitivesPriceAxisView extends CustomPrimitivesAxisView { + update() { + const series = this._source.series; + this._pos = series.priceToCoordinate(this._p.price); + } + text() { + return this._source.options.priceLabelFormatter(this._p.price); + } +} diff --git a/lwc-plugin-custom-primitives/src/custom-primitives.ts b/lwc-plugin-custom-primitives/src/custom-primitives.ts new file mode 100644 index 0000000..1dc770b --- /dev/null +++ b/lwc-plugin-custom-primitives/src/custom-primitives.ts @@ -0,0 +1,139 @@ +import { AutoscaleInfo, Logical, Time, DataChangedScope } from 'lightweight-charts'; +import { + CustomPrimitivesPriceAxisPaneView, + CustomPrimitivesTimeAxisPaneView, +} from './axis-pane-view'; +import { CustomPrimitivesPriceAxisView, CustomPrimitivesTimeAxisView } from './axis-view'; +import { Point, CustomPrimitivesDataSource } from './data-source'; +import { CustomPrimitivesOptions, defaultOptions } from './options'; +import { CustomPrimitivesPaneView } from './pane-view'; +import { PluginBase } from './plugin-base'; + +export class CustomPrimitives + extends PluginBase + implements CustomPrimitivesDataSource +{ + _options: CustomPrimitivesOptions; + _p1: Point; + _p2: Point; + _paneViews: CustomPrimitivesPaneView[]; + _timeAxisViews: CustomPrimitivesTimeAxisView[]; + _priceAxisViews: CustomPrimitivesPriceAxisView[]; + _priceAxisPaneViews: CustomPrimitivesPriceAxisPaneView[]; + _timeAxisPaneViews: CustomPrimitivesTimeAxisPaneView[]; + + constructor( + p1: Point, + p2: Point, + options: Partial = {} + ) { + super(); + this._p1 = p1; + this._p2 = p2; + this._options = { + ...defaultOptions, + ...options, + }; + this._paneViews = [new CustomPrimitivesPaneView(this)]; + this._timeAxisViews = [ + new CustomPrimitivesTimeAxisView(this, p1), + new CustomPrimitivesTimeAxisView(this, p2), + ]; + this._priceAxisViews = [ + new CustomPrimitivesPriceAxisView(this, p1), + new CustomPrimitivesPriceAxisView(this, p2), + ]; + this._priceAxisPaneViews = [new CustomPrimitivesPriceAxisPaneView(this, true)]; + this._timeAxisPaneViews = [new CustomPrimitivesTimeAxisPaneView(this, false)]; + } + + updateAllViews() { + //* Use this method to update any data required by the + //* views to draw. + this._paneViews.forEach(pw => pw.update()); + this._timeAxisViews.forEach(pw => pw.update()); + this._priceAxisViews.forEach(pw => pw.update()); + this._priceAxisPaneViews.forEach(pw => pw.update()); + this._timeAxisPaneViews.forEach(pw => pw.update()); + } + + priceAxisViews() { + //* Labels rendered on the price scale + return this._priceAxisViews; + } + + timeAxisViews() { + //* labels rendered on the time scale + return this._timeAxisViews; + } + + paneViews() { + //* rendering on the main chart pane + return this._paneViews; + } + + priceAxisPaneViews() { + //* rendering on the price scale + return this._priceAxisPaneViews; + } + + timeAxisPaneViews() { + //* rendering on the time scale + return this._timeAxisPaneViews; + } + + autoscaleInfo( + startTimePoint: Logical, + endTimePoint: Logical + ): AutoscaleInfo | null { + //* Use this method to provide autoscale information if your primitive + //* should have the ability to remain in view automatically. + if ( + this._timeCurrentlyVisible(this.p1.time, startTimePoint, endTimePoint) || + this._timeCurrentlyVisible(this.p2.time, startTimePoint, endTimePoint) + ) { + return { + priceRange: { + minValue: Math.min(this.p1.price, this.p2.price), + maxValue: Math.max(this.p1.price, this.p2.price), + }, + }; + } + return null; + } + + dataUpdated(scope: DataChangedScope): void { + //* This method will be called by PluginBase when the data on the + //* series has changed. + } + + _timeCurrentlyVisible( + time: Time, + startTimePoint: Logical, + endTimePoint: Logical + ): boolean { + const ts = this.chart.timeScale(); + const coordinate = ts.timeToCoordinate(time); + if (coordinate === null) return false; + const logical = ts.coordinateToLogical(coordinate); + if (logical === null) return false; + return logical <= endTimePoint && logical >= startTimePoint; + } + + public get options(): CustomPrimitivesOptions { + return this._options; + } + + applyOptions(options: Partial) { + this._options = { ...this._options, ...options }; + this.requestUpdate(); + } + + public get p1(): Point { + return this._p1; + } + + public get p2(): Point { + return this._p2; + } +} diff --git a/lwc-plugin-custom-primitives/src/data-source.ts b/lwc-plugin-custom-primitives/src/data-source.ts new file mode 100644 index 0000000..6c14142 --- /dev/null +++ b/lwc-plugin-custom-primitives/src/data-source.ts @@ -0,0 +1,20 @@ +import { + IChartApi, + ISeriesApi, + SeriesOptionsMap, + Time, +} from 'lightweight-charts'; +import { CustomPrimitivesOptions } from './options'; + +export interface Point { + time: Time; + price: number; +} + +export interface CustomPrimitivesDataSource { + chart: IChartApi; + series: ISeriesApi; + options: CustomPrimitivesOptions; + p1: Point; + p2: Point; +} diff --git a/lwc-plugin-custom-primitives/src/example/example.ts b/lwc-plugin-custom-primitives/src/example/example.ts new file mode 100644 index 0000000..a884dbe --- /dev/null +++ b/lwc-plugin-custom-primitives/src/example/example.ts @@ -0,0 +1,23 @@ +import { createChart } from 'lightweight-charts'; +import { generateLineData } from '../sample-data'; +import { CustomPrimitives } from '../custom-primitives'; + +const chart = ((window as unknown as any).chart = createChart('chart', { + autoSize: true, +})); + +const lineSeries = chart.addLineSeries({ + color: '#000000', +}); +const data = generateLineData(); +lineSeries.setData(data); + +const time1 = data[data.length - 50].time; +const time2 = data[data.length - 10].time; + +const primitive = new CustomPrimitives( + { price: 100, time: time1 }, + { price: 500, time: time2 } +); + +lineSeries.attachPrimitive(primitive); diff --git a/lwc-plugin-custom-primitives/src/example/index.html b/lwc-plugin-custom-primitives/src/example/index.html new file mode 100644 index 0000000..ed1334b --- /dev/null +++ b/lwc-plugin-custom-primitives/src/example/index.html @@ -0,0 +1,26 @@ + + + + + + Template Drawing Primitive Plugin Example + + + +
+ + + diff --git a/lwc-plugin-custom-primitives/src/helpers/assertions.ts b/lwc-plugin-custom-primitives/src/helpers/assertions.ts new file mode 100644 index 0000000..e68742f --- /dev/null +++ b/lwc-plugin-custom-primitives/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/lwc-plugin-custom-primitives/src/helpers/dimensions/common.ts b/lwc-plugin-custom-primitives/src/helpers/dimensions/common.ts new file mode 100644 index 0000000..394c6bb --- /dev/null +++ b/lwc-plugin-custom-primitives/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/lwc-plugin-custom-primitives/src/helpers/dimensions/crosshair-width.ts b/lwc-plugin-custom-primitives/src/helpers/dimensions/crosshair-width.ts new file mode 100644 index 0000000..9d14991 --- /dev/null +++ b/lwc-plugin-custom-primitives/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/lwc-plugin-custom-primitives/src/helpers/dimensions/full-width.ts b/lwc-plugin-custom-primitives/src/helpers/dimensions/full-width.ts new file mode 100644 index 0000000..6f4d40d --- /dev/null +++ b/lwc-plugin-custom-primitives/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/lwc-plugin-custom-primitives/src/helpers/dimensions/positions.ts b/lwc-plugin-custom-primitives/src/helpers/dimensions/positions.ts new file mode 100644 index 0000000..a021b07 --- /dev/null +++ b/lwc-plugin-custom-primitives/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/lwc-plugin-custom-primitives/src/helpers/time.ts b/lwc-plugin-custom-primitives/src/helpers/time.ts new file mode 100644 index 0000000..ed65475 --- /dev/null +++ b/lwc-plugin-custom-primitives/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/lwc-plugin-custom-primitives/src/options.ts b/lwc-plugin-custom-primitives/src/options.ts new file mode 100644 index 0000000..04fa40c --- /dev/null +++ b/lwc-plugin-custom-primitives/src/options.ts @@ -0,0 +1,27 @@ +import { Time, isBusinessDay } from 'lightweight-charts'; + +export interface CustomPrimitivesOptions { + //* Define the options for the primitive. + fillColor: string; + labelColor: string; + labelTextColor: string; + showLabels: boolean; + priceLabelFormatter: (price: number) => string; + timeLabelFormatter: (time: Time) => string; +} + +export const defaultOptions: CustomPrimitivesOptions = { + //* Define the default values for all the primitive options. + fillColor: 'rgba(200, 50, 100, 0.75)', + labelColor: 'rgba(200, 50, 100, 1)', + labelTextColor: 'white', + showLabels: true, + priceLabelFormatter: (price: number) => price.toFixed(2), + timeLabelFormatter: (time: Time) => { + 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(); + }, +} as const; diff --git a/lwc-plugin-custom-primitives/src/pane-renderer.ts b/lwc-plugin-custom-primitives/src/pane-renderer.ts new file mode 100644 index 0000000..a69c6cd --- /dev/null +++ b/lwc-plugin-custom-primitives/src/pane-renderer.ts @@ -0,0 +1,46 @@ +import { CanvasRenderingTarget2D } from 'fancy-canvas'; +import { ISeriesPrimitivePaneRenderer } from 'lightweight-charts'; +import { ViewPoint } from './pane-view'; +import { positionsBox } from './helpers/dimensions/positions'; + +export class CustomPrimitivesPaneRenderer implements ISeriesPrimitivePaneRenderer { + _p1: ViewPoint; + _p2: ViewPoint; + _fillColor: string; + + constructor(p1: ViewPoint, p2: ViewPoint, fillColor: string) { + this._p1 = p1; + this._p2 = p2; + this._fillColor = fillColor; + } + + draw(target: CanvasRenderingTarget2D) { + target.useBitmapCoordinateSpace(scope => { + if ( + this._p1.x === null || + this._p1.y === null || + this._p2.x === null || + this._p2.y === null + ) + return; + const ctx = scope.context; + const horizontalPositions = positionsBox( + this._p1.x, + this._p2.x, + scope.horizontalPixelRatio + ); + const verticalPositions = positionsBox( + this._p1.y, + this._p2.y, + scope.verticalPixelRatio + ); + ctx.fillStyle = this._fillColor; + ctx.fillRect( + horizontalPositions.position, + verticalPositions.position, + horizontalPositions.length, + verticalPositions.length + ); + }); + } +} diff --git a/lwc-plugin-custom-primitives/src/pane-view.ts b/lwc-plugin-custom-primitives/src/pane-view.ts new file mode 100644 index 0000000..810538c --- /dev/null +++ b/lwc-plugin-custom-primitives/src/pane-view.ts @@ -0,0 +1,37 @@ +import { Coordinate, ISeriesPrimitivePaneView } from 'lightweight-charts'; +import { CustomPrimitivesPaneRenderer } from './pane-renderer'; +import { CustomPrimitivesDataSource } from './data-source'; + +export interface ViewPoint { + x: Coordinate | null; + y: Coordinate | null; +} + +export class CustomPrimitivesPaneView implements ISeriesPrimitivePaneView { + _source: CustomPrimitivesDataSource; + _p1: ViewPoint = { x: null, y: null }; + _p2: ViewPoint = { x: null, y: null }; + + constructor(source: CustomPrimitivesDataSource) { + this._source = source; + } + + update() { + const series = this._source.series; + const y1 = series.priceToCoordinate(this._source.p1.price); + const y2 = series.priceToCoordinate(this._source.p2.price); + const timeScale = this._source.chart.timeScale(); + const x1 = timeScale.timeToCoordinate(this._source.p1.time); + const x2 = timeScale.timeToCoordinate(this._source.p2.time); + this._p1 = { x: x1, y: y1 }; + this._p2 = { x: x2, y: y2 }; + } + + renderer() { + return new CustomPrimitivesPaneRenderer( + this._p1, + this._p2, + this._source.options.fillColor + ); + } +} diff --git a/lwc-plugin-custom-primitives/src/plugin-base.ts b/lwc-plugin-custom-primitives/src/plugin-base.ts new file mode 100644 index 0000000..def9684 --- /dev/null +++ b/lwc-plugin-custom-primitives/src/plugin-base.ts @@ -0,0 +1,56 @@ +import { + DataChangedScope, + IChartApi, + ISeriesApi, + ISeriesPrimitive, + SeriesAttachedParameter, + SeriesOptionsMap, + Time, +} from 'lightweight-charts'; +import { ensureDefined } from './helpers/assertions'; + +//* PluginBase is a useful base to build a plugin upon which +//* already handles creating getters for the chart and series, +//* and provides a requestUpdate method. +export abstract class PluginBase implements ISeriesPrimitive