├── test_venv
├── bin
│ ├── python3
│ ├── python3.9
│ ├── python
│ ├── pip
│ ├── pip3
│ ├── pip3.9
│ ├── activate.csh
│ ├── activate
│ ├── activate.fish
│ └── Activate.ps1
└── pyvenv.cfg
├── lightweight_charts_v5
├── frontend
│ ├── src
│ │ ├── react-app-env.d.ts
│ │ ├── index.tsx
│ │ ├── plugins
│ │ │ └── StaticRectanglePlugin.ts
│ │ └── LightweightChartsComponent.tsx
│ ├── .prettierrc
│ ├── build
│ │ ├── asset-manifest.json
│ │ ├── index.html
│ │ └── static
│ │ │ └── js
│ │ │ └── main.477bf26b.js.LICENSE.txt
│ ├── tsconfig.json
│ ├── public
│ │ └── index.html
│ └── package.json
└── __init__.py
├── Screenshot_1.png
├── Screenshot_2.png
├── Screenshot_3.png
├── requirements.txt
├── MANIFEST.in
├── demo
├── minimal_demo.py
├── yield_curve.py
├── multi_demo.py
├── chart_demo.py
├── chart_themes.py
└── indicators.py
├── CHANGELOG.md
├── .devcontainer
└── devcontainer.json
├── setup.py
├── DEVELOP_AND_DEPLOY.txt
├── LICENSE
├── .gitignore
├── e2e
├── test_template.py
└── e2e_utils.py
├── README.md
├── CLAUDE.md
└── streamlit-component-development-workflow.svg
/test_venv/bin/python3:
--------------------------------------------------------------------------------
1 | python
--------------------------------------------------------------------------------
/test_venv/bin/python3.9:
--------------------------------------------------------------------------------
1 | python
--------------------------------------------------------------------------------
/test_venv/bin/python:
--------------------------------------------------------------------------------
1 | /opt/homebrew/anaconda3/bin/python
--------------------------------------------------------------------------------
/lightweight_charts_v5/frontend/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/Screenshot_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/locupleto/streamlit-lightweight-charts-v5/HEAD/Screenshot_1.png
--------------------------------------------------------------------------------
/Screenshot_2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/locupleto/streamlit-lightweight-charts-v5/HEAD/Screenshot_2.png
--------------------------------------------------------------------------------
/Screenshot_3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/locupleto/streamlit-lightweight-charts-v5/HEAD/Screenshot_3.png
--------------------------------------------------------------------------------
/test_venv/pyvenv.cfg:
--------------------------------------------------------------------------------
1 | home = /opt/homebrew/anaconda3/bin
2 | include-system-site-packages = false
3 | version = 3.9.7
4 |
--------------------------------------------------------------------------------
/lightweight_charts_v5/frontend/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "endOfLine": "lf",
3 | "semi": false,
4 | "trailingComma": "es5"
5 | }
6 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | numpy>=2.2.3
2 | pandas>=2.2.3
3 | streamlit>=1.43.2
4 | yfinance>=0.2.54
5 | streamlit-lightweight-charts-v5
6 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | recursive-include lightweight_charts_v5/frontend/build *
2 | recursive-exclude lightweight_charts_v5/frontend/node_modules *
3 |
--------------------------------------------------------------------------------
/lightweight_charts_v5/frontend/build/asset-manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "files": {
3 | "main.js": "./static/js/main.477bf26b.js",
4 | "index.html": "./index.html",
5 | "main.477bf26b.js.map": "./static/js/main.477bf26b.js.map"
6 | },
7 | "entrypoints": [
8 | "static/js/main.477bf26b.js"
9 | ]
10 | }
--------------------------------------------------------------------------------
/test_venv/bin/pip:
--------------------------------------------------------------------------------
1 | #!/Volumes/Work/development/projects/git/streamlit-lightweight-charts-v5/test_venv/bin/python
2 | # -*- coding: utf-8 -*-
3 | import re
4 | import sys
5 | from pip._internal.cli.main import main
6 | if __name__ == '__main__':
7 | sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
8 | sys.exit(main())
9 |
--------------------------------------------------------------------------------
/test_venv/bin/pip3:
--------------------------------------------------------------------------------
1 | #!/Volumes/Work/development/projects/git/streamlit-lightweight-charts-v5/test_venv/bin/python
2 | # -*- coding: utf-8 -*-
3 | import re
4 | import sys
5 | from pip._internal.cli.main import main
6 | if __name__ == '__main__':
7 | sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
8 | sys.exit(main())
9 |
--------------------------------------------------------------------------------
/test_venv/bin/pip3.9:
--------------------------------------------------------------------------------
1 | #!/Volumes/Work/development/projects/git/streamlit-lightweight-charts-v5/test_venv/bin/python
2 | # -*- coding: utf-8 -*-
3 | import re
4 | import sys
5 | from pip._internal.cli.main import main
6 | if __name__ == '__main__':
7 | sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
8 | sys.exit(main())
9 |
--------------------------------------------------------------------------------
/lightweight_charts_v5/frontend/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import ReactDOM from "react-dom"
3 | import LightweightChartsComponent from "./LightweightChartsComponent"
4 |
5 | ReactDOM.render(
6 |
7 |
8 | ,
9 | document.getElementById("root")
10 | )
11 |
--------------------------------------------------------------------------------
/lightweight_charts_v5/frontend/build/index.html:
--------------------------------------------------------------------------------
1 |
Streamlit Component
--------------------------------------------------------------------------------
/lightweight_charts_v5/frontend/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "esModuleInterop": true,
8 | "allowSyntheticDefaultImports": true,
9 | "strict": true,
10 | "forceConsistentCasingInFileNames": true,
11 | "module": "esnext",
12 | "moduleResolution": "node",
13 | "resolveJsonModule": true,
14 | "isolatedModules": true,
15 | "noEmit": true,
16 | "jsx": "react"
17 | },
18 | "include": ["src"]
19 | }
20 |
--------------------------------------------------------------------------------
/demo/minimal_demo.py:
--------------------------------------------------------------------------------
1 | import streamlit as st
2 | from lightweight_charts_v5 import lightweight_charts_v5_component
3 | import yfinance as yf
4 |
5 | # Load stock data
6 | ticker = "AAPL"
7 | data = yf.download(ticker, period="100d", interval="1d", auto_adjust=False)
8 |
9 | # Convert data to Lightweight Charts format, ensuring values are proper floats
10 | chart_data = [
11 | {"time": str(date.date()), "value": float(row["Close"].iloc[0])}
12 | for date, row in data.iterrows()
13 | ]
14 |
15 | # Streamlit app
16 | st.title(f"{ticker} Stock Price Chart")
17 |
18 | # Render the chart
19 | lightweight_charts_v5_component(
20 | name=f"{ticker} Chart",
21 | charts=[{
22 | "chart": {"layout": {"background": {"color": "#FFFFFF"}}},
23 | "series": [{
24 | "type": "Line",
25 | "data": chart_data,
26 | "options": {"color": "#2962FF"}
27 | }],
28 | "height": 400
29 | }],
30 | height=400
31 | )
--------------------------------------------------------------------------------
/test_venv/bin/activate.csh:
--------------------------------------------------------------------------------
1 | # This file must be used with "source bin/activate.csh" *from csh*.
2 | # You cannot run it directly.
3 | # Created by Davide Di Blasi .
4 | # Ported to Python 3.3 venv by Andrew Svetlov
5 |
6 | alias deactivate 'test $?_OLD_VIRTUAL_PATH != 0 && setenv PATH "$_OLD_VIRTUAL_PATH" && unset _OLD_VIRTUAL_PATH; rehash; test $?_OLD_VIRTUAL_PROMPT != 0 && set prompt="$_OLD_VIRTUAL_PROMPT" && unset _OLD_VIRTUAL_PROMPT; unsetenv VIRTUAL_ENV; test "\!:*" != "nondestructive" && unalias deactivate'
7 |
8 | # Unset irrelevant variables.
9 | deactivate nondestructive
10 |
11 | setenv VIRTUAL_ENV "/Volumes/Work/development/projects/git/streamlit-lightweight-charts-v5/test_venv"
12 |
13 | set _OLD_VIRTUAL_PATH="$PATH"
14 | setenv PATH "$VIRTUAL_ENV/bin:$PATH"
15 |
16 |
17 | set _OLD_VIRTUAL_PROMPT="$prompt"
18 |
19 | if (! "$?VIRTUAL_ENV_DISABLE_PROMPT") then
20 | set prompt = "(test_venv) $prompt"
21 | endif
22 |
23 | alias pydoc python -m pydoc
24 |
25 | rehash
26 |
--------------------------------------------------------------------------------
/lightweight_charts_v5/frontend/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Streamlit Component
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/lightweight_charts_v5/frontend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "lightweight-charts-v5",
3 | "version": "0.1.7",
4 | "private": true,
5 | "dependencies": {
6 | "lightweight-charts": "^5.0.2",
7 | "react": "^16.13.1",
8 | "react-dom": "^16.13.1",
9 | "streamlit-component-lib": "^2.0.0"
10 | },
11 | "scripts": {
12 | "start": "react-scripts start",
13 | "build": "react-scripts build",
14 | "test": "react-scripts test",
15 | "eject": "react-scripts eject"
16 | },
17 | "eslintConfig": {
18 | "extends": "react-app"
19 | },
20 | "browserslist": {
21 | "production": [
22 | ">0.2%",
23 | "not dead",
24 | "not op_mini all"
25 | ],
26 | "development": [
27 | "last 1 chrome version",
28 | "last 1 firefox version",
29 | "last 1 safari version"
30 | ]
31 | },
32 | "homepage": ".",
33 | "devDependencies": {
34 | "@types/node": "^12.0.0",
35 | "@types/react": "^16.9.0",
36 | "@types/react-dom": "^16.9.0",
37 | "react-scripts": "^5.0.1",
38 | "typescript": "^4.2.0"
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## 0.1.7 (2025-06-20)
4 |
5 | ### Security
6 | - **CRITICAL**: Removed vulnerable "build" package containing js-yaml < 3.13.1 (code injection vulnerability)
7 | - **HIGH**: Eliminated timespan package RegEx DoS vulnerability (no patch available)
8 | - **CRITICAL**: Fixed uglify-js RegEx DoS vulnerability
9 | - Resolved multiple dependency security issues without breaking functionality
10 |
11 | ### Changed
12 | - Streamlined dependencies by removing unnecessary "build" package
13 | - Component functionality fully preserved and tested with demo applications
14 | - Updated all version numbers to maintain consistency across project files
15 |
16 | ## 0.1.6 (2025-04-12)
17 |
18 | ### Fixed
19 | - Fixed window resize detection issue that was preventing charts from resizing properly
20 | - Eliminated flickering during resize operations
21 | - Improved resize handling with requestAnimationFrame for smoother performance
22 | - Reduced MIN_RESIZE_INTERVAL from 1000ms to 500ms for more responsive resizing
23 |
24 | ### Changed
25 | - Simplified ResizeObserver implementation for more reliable resize detection
26 | - Improved debounce mechanism in handleResize function
--------------------------------------------------------------------------------
/.devcontainer/devcontainer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Python 3",
3 | // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
4 | "image": "mcr.microsoft.com/devcontainers/python:1-3.11-bullseye",
5 | "customizations": {
6 | "codespaces": {
7 | "openFiles": [
8 | "README.md",
9 | "demo/chart_demo.py"
10 | ]
11 | },
12 | "vscode": {
13 | "settings": {},
14 | "extensions": [
15 | "ms-python.python",
16 | "ms-python.vscode-pylance"
17 | ]
18 | }
19 | },
20 | "updateContentCommand": "[ -f packages.txt ] && sudo apt update && sudo apt upgrade -y && sudo xargs apt install -y = 0.63",
27 | ],
28 | extras_require={
29 | "devel": [
30 | "wheel",
31 | "pytest==7.4.0",
32 | "playwright==1.48.0",
33 | "requests==2.31.0",
34 | "pytest-playwright-snapshot==1.0",
35 | "pytest-rerunfailures==12.0",
36 | ],
37 | "demo": [
38 | "yfinance",
39 | "numpy",
40 | ]
41 | }
42 | )
--------------------------------------------------------------------------------
/DEVELOP_AND_DEPLOY.txt:
--------------------------------------------------------------------------------
1 | Notes on development-loop for this project
2 | ==========================================
3 |
4 | - Keep two shells available in VSCode
5 | a) Python Debug Console for the Python Streamlit test project
6 | b) zsh for running the development server and re-building ts-code
7 |
8 | Typescript development iteration (zsh)
9 | --------------------------------------
10 | - project_root: /Volumes/Work/development/projects/git/streamlit-lightweight-charts-v5/
11 | - cd {project_root}/lightweight_charts_v5/frontend
12 | - use restart.sh to re-build all ts-code and restart the server
13 | (basically: npm run build && sleep 3 && npm start)
14 |
15 | Python-side streamlit demo_chart.py
16 | -----------------------------------
17 | - cd {project_root}
18 | - stop streamlit debugger if running
19 | - pip install -e .
20 | - Cmd-Shift D to run streamlit again in the debugger
21 |
22 | New Release
23 | ----------------
24 | # Remove previous build artifacts
25 | rm -rf build/ dist/ *.egg-info/
26 |
27 | # Build the frontend
28 | cd lightweight_charts_v5/frontend
29 | npm install
30 | npm run build
31 | cd ../..
32 |
33 | # Build the Python package
34 | python -m pip install --upgrade build
35 | python -m build
36 |
37 | Deploy to PyPi
38 | --------------
39 | - python setup.py sdist bdist_wheel
40 | - (pip install twine)
41 | - twine upload dist/* (paste token from Documents/Licenses/PyPI-Token.txt)
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 | Copyright (c) 2025 Urban Ottosson
3 |
4 | Permission is hereby granted, free of charge, to any person obtaining a copy
5 | of this software and associated documentation files (the "Software"), to deal
6 | in the Software without restriction, including without limitation the rights
7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | copies of the Software, and to permit persons to whom the Software is
9 | furnished to do so, subject to the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be included in all
12 | copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20 | SOFTWARE.
21 |
22 | ---
23 |
24 | ### **Attribution Notice**
25 | This project integrates the [TradingView Lightweight Charts library](https://github.com/tradingview/lightweight-charts),
26 | which is licensed under the Apache License 2.0. See the original license in `third_party/LICENSE_TRADINGVIEW.txt`.
--------------------------------------------------------------------------------
/lightweight_charts_v5/frontend/build/static/js/main.477bf26b.js.LICENSE.txt:
--------------------------------------------------------------------------------
1 | /*
2 | object-assign
3 | (c) Sindre Sorhus
4 | @license MIT
5 | */
6 |
7 | /*!
8 | * @license
9 | * TradingView Lightweight Charts™ v5.0.3
10 | * Copyright (c) 2025 TradingView, Inc.
11 | * Licensed under Apache License 2.0 https://www.apache.org/licenses/LICENSE-2.0
12 | */
13 |
14 | /** @license React v0.19.1
15 | * scheduler.production.min.js
16 | *
17 | * Copyright (c) Facebook, Inc. and its affiliates.
18 | *
19 | * This source code is licensed under the MIT license found in the
20 | * LICENSE file in the root directory of this source tree.
21 | */
22 |
23 | /** @license React v16.13.1
24 | * react-is.production.min.js
25 | *
26 | * Copyright (c) Facebook, Inc. and its affiliates.
27 | *
28 | * This source code is licensed under the MIT license found in the
29 | * LICENSE file in the root directory of this source tree.
30 | */
31 |
32 | /** @license React v16.14.0
33 | * react-dom.production.min.js
34 | *
35 | * Copyright (c) Facebook, Inc. and its affiliates.
36 | *
37 | * This source code is licensed under the MIT license found in the
38 | * LICENSE file in the root directory of this source tree.
39 | */
40 |
41 | /** @license React v16.14.0
42 | * react-jsx-runtime.production.min.js
43 | *
44 | * Copyright (c) Facebook, Inc. and its affiliates.
45 | *
46 | * This source code is licensed under the MIT license found in the
47 | * LICENSE file in the root directory of this source tree.
48 | */
49 |
50 | /** @license React v16.14.0
51 | * react.production.min.js
52 | *
53 | * Copyright (c) Facebook, Inc. and its affiliates.
54 | *
55 | * This source code is licensed under the MIT license found in the
56 | * LICENSE file in the root directory of this source tree.
57 | */
58 |
--------------------------------------------------------------------------------
/test_venv/bin/activate:
--------------------------------------------------------------------------------
1 | # This file must be used with "source bin/activate" *from bash*
2 | # you cannot run it directly
3 |
4 | deactivate () {
5 | # reset old environment variables
6 | if [ -n "${_OLD_VIRTUAL_PATH:-}" ] ; then
7 | PATH="${_OLD_VIRTUAL_PATH:-}"
8 | export PATH
9 | unset _OLD_VIRTUAL_PATH
10 | fi
11 | if [ -n "${_OLD_VIRTUAL_PYTHONHOME:-}" ] ; then
12 | PYTHONHOME="${_OLD_VIRTUAL_PYTHONHOME:-}"
13 | export PYTHONHOME
14 | unset _OLD_VIRTUAL_PYTHONHOME
15 | fi
16 |
17 | # This should detect bash and zsh, which have a hash command that must
18 | # be called to get it to forget past commands. Without forgetting
19 | # past commands the $PATH changes we made may not be respected
20 | if [ -n "${BASH:-}" -o -n "${ZSH_VERSION:-}" ] ; then
21 | hash -r 2> /dev/null
22 | fi
23 |
24 | if [ -n "${_OLD_VIRTUAL_PS1:-}" ] ; then
25 | PS1="${_OLD_VIRTUAL_PS1:-}"
26 | export PS1
27 | unset _OLD_VIRTUAL_PS1
28 | fi
29 |
30 | unset VIRTUAL_ENV
31 | if [ ! "${1:-}" = "nondestructive" ] ; then
32 | # Self destruct!
33 | unset -f deactivate
34 | fi
35 | }
36 |
37 | # unset irrelevant variables
38 | deactivate nondestructive
39 |
40 | VIRTUAL_ENV="/Volumes/Work/development/projects/git/streamlit-lightweight-charts-v5/test_venv"
41 | export VIRTUAL_ENV
42 |
43 | _OLD_VIRTUAL_PATH="$PATH"
44 | PATH="$VIRTUAL_ENV/bin:$PATH"
45 | export PATH
46 |
47 | # unset PYTHONHOME if set
48 | # this will fail if PYTHONHOME is set to the empty string (which is bad anyway)
49 | # could use `if (set -u; : $PYTHONHOME) ;` in bash
50 | if [ -n "${PYTHONHOME:-}" ] ; then
51 | _OLD_VIRTUAL_PYTHONHOME="${PYTHONHOME:-}"
52 | unset PYTHONHOME
53 | fi
54 |
55 | if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT:-}" ] ; then
56 | _OLD_VIRTUAL_PS1="${PS1:-}"
57 | PS1="(test_venv) ${PS1:-}"
58 | export PS1
59 | fi
60 |
61 | # This should detect bash and zsh, which have a hash command that must
62 | # be called to get it to forget past commands. Without forgetting
63 | # past commands the $PATH changes we made may not be respected
64 | if [ -n "${BASH:-}" -o -n "${ZSH_VERSION:-}" ] ; then
65 | hash -r 2> /dev/null
66 | fi
67 |
--------------------------------------------------------------------------------
/test_venv/bin/activate.fish:
--------------------------------------------------------------------------------
1 | # This file must be used with "source /bin/activate.fish" *from fish*
2 | # (https://fishshell.com/); you cannot run it directly.
3 |
4 | function deactivate -d "Exit virtual environment and return to normal shell environment"
5 | # reset old environment variables
6 | if test -n "$_OLD_VIRTUAL_PATH"
7 | set -gx PATH $_OLD_VIRTUAL_PATH
8 | set -e _OLD_VIRTUAL_PATH
9 | end
10 | if test -n "$_OLD_VIRTUAL_PYTHONHOME"
11 | set -gx PYTHONHOME $_OLD_VIRTUAL_PYTHONHOME
12 | set -e _OLD_VIRTUAL_PYTHONHOME
13 | end
14 |
15 | if test -n "$_OLD_FISH_PROMPT_OVERRIDE"
16 | functions -e fish_prompt
17 | set -e _OLD_FISH_PROMPT_OVERRIDE
18 | functions -c _old_fish_prompt fish_prompt
19 | functions -e _old_fish_prompt
20 | end
21 |
22 | set -e VIRTUAL_ENV
23 | if test "$argv[1]" != "nondestructive"
24 | # Self-destruct!
25 | functions -e deactivate
26 | end
27 | end
28 |
29 | # Unset irrelevant variables.
30 | deactivate nondestructive
31 |
32 | set -gx VIRTUAL_ENV "/Volumes/Work/development/projects/git/streamlit-lightweight-charts-v5/test_venv"
33 |
34 | set -gx _OLD_VIRTUAL_PATH $PATH
35 | set -gx PATH "$VIRTUAL_ENV/bin" $PATH
36 |
37 | # Unset PYTHONHOME if set.
38 | if set -q PYTHONHOME
39 | set -gx _OLD_VIRTUAL_PYTHONHOME $PYTHONHOME
40 | set -e PYTHONHOME
41 | end
42 |
43 | if test -z "$VIRTUAL_ENV_DISABLE_PROMPT"
44 | # fish uses a function instead of an env var to generate the prompt.
45 |
46 | # Save the current fish_prompt function as the function _old_fish_prompt.
47 | functions -c fish_prompt _old_fish_prompt
48 |
49 | # With the original prompt function renamed, we can override with our own.
50 | function fish_prompt
51 | # Save the return status of the last command.
52 | set -l old_status $status
53 |
54 | # Output the venv prompt; color taken from the blue of the Python logo.
55 | printf "%s%s%s" (set_color 4B8BBE) "(test_venv) " (set_color normal)
56 |
57 | # Restore the return status of the previous command.
58 | echo "exit $old_status" | .
59 | # Output the original/"old" prompt.
60 | _old_fish_prompt
61 | end
62 |
63 | set -gx _OLD_FISH_PROMPT_OVERRIDE "$VIRTUAL_ENV"
64 | end
65 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | venv/
2 |
3 | # Byte-compiled / optimized / DLL files
4 | __pycache__/
5 | *.py[cod]
6 | *.class
7 |
8 | # C extensions
9 | *.so
10 |
11 | # Distribution / packaging
12 | .Python
13 | develop-eggs/
14 | dist/
15 | downloads/
16 | eggs/
17 | .eggs/
18 | lib/
19 | lib64/
20 | parts/
21 | sdist/
22 | var/
23 | wheels/
24 | *.egg-info/
25 | .installed.cfg
26 | *.egg
27 |
28 | # PyInstaller
29 | # Usually these files are written by a python script from a template
30 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
31 | *.manifest
32 | *.spec
33 |
34 | # Installer logs
35 | pip-log.txt
36 | pip-delete-this-directory.txt
37 |
38 | # Unit test / coverage reports
39 | htmlcov/
40 | .tox/
41 | .nox/
42 | .coverage
43 | .coverage.*
44 | .cache
45 | nosetests.xml
46 | coverage.xml
47 | *.cover
48 | *.py,cover
49 | .hypothesis/
50 | .pytest_cache/
51 |
52 | # Translations
53 | *.mo
54 | *.pot
55 |
56 | # Django stuff:
57 | *.log
58 | local_settings.py
59 | db.sqlite3
60 |
61 | # Flask stuff:
62 | instance/
63 | .webassets-cache
64 |
65 | # Scrapy stuff:
66 | .scrapy
67 |
68 | # Sphinx documentation
69 | docs/_build/
70 |
71 | # PyBuilder
72 | target/
73 |
74 | # Jupyter Notebook
75 | .ipynb_checkpoints
76 |
77 | # IPython
78 | profile_default/
79 | ipython_config.py
80 |
81 | # pyenv
82 | .python-version
83 |
84 | # Celery stuff
85 | celerybeat-schedule
86 | celerybeat.pid
87 |
88 | # SageMath parsed files
89 | *.sage.py
90 |
91 | # Environments
92 | .env
93 | .venv
94 | env/
95 | venv/
96 | ENV/
97 | env.bak/
98 | venv.bak/
99 |
100 | # Spyder project settings
101 | .spyderproject
102 | .spyproject
103 |
104 | # Rope project settings
105 | .ropeproject
106 |
107 | # mkdocs documentation
108 | /site
109 |
110 | # mypy
111 | .mypy_cache/
112 | .dmypy.json
113 | dmypy.json
114 |
115 | # Pyre type checker
116 | .pyre/
117 |
118 | # IDEs
119 | .vscode/
120 | .idea/
121 |
122 | # macOS
123 | .DS_Store
124 |
125 | # config files (may contain secrets)
126 | .config/
127 |
128 | # chat histories
129 | trading_chat_history/
130 | developer_chat_history/
131 | research_chat_history/
132 | web_chat_history/
133 | chat_chat_history/
134 |
135 | # documents (research assistant)
136 | research_documents/
137 | developer_docs/
138 |
139 | # Wikipedia stock index constituents
140 | market_indexes/
141 | optimization_results
142 |
143 | # nodejs stuff...
144 | my_component/frontend/node_modules/.bin/
145 | node_modules
146 |
--------------------------------------------------------------------------------
/demo/yield_curve.py:
--------------------------------------------------------------------------------
1 | from typing import List, Dict, Any
2 |
3 | def get_sample_yield_curves() -> List[Dict[str, Any]]:
4 | import pandas as pd
5 |
6 | """Returns sample yield curve data"""
7 | curve1 = [
8 | {"time": 1, "value": 5.378},
9 | {"time": 2, "value": 5.372},
10 | {"time": 3, "value": 5.271},
11 | {"time": 6, "value": 5.094},
12 | {"time": 12, "value": 4.739},
13 | {"time": 24, "value": 4.237},
14 | {"time": 36, "value": 4.036},
15 | {"time": 60, "value": 3.887},
16 | {"time": 84, "value": 3.921},
17 | {"time": 120, "value": 4.007},
18 | {"time": 240, "value": 4.366},
19 | {"time": 360, "value": 4.29},
20 | ]
21 |
22 | curve2 = [
23 | {"time": 1, "value": 5.381},
24 | {"time": 2, "value": 5.393},
25 | {"time": 3, "value": 5.425},
26 | {"time": 6, "value": 5.494},
27 | {"time": 12, "value": 5.377},
28 | {"time": 24, "value": 4.883},
29 | {"time": 36, "value": 4.554},
30 | {"time": 60, "value": 4.241},
31 | {"time": 84, "value": 4.172},
32 | {"time": 120, "value": 4.084},
33 | {"time": 240, "value": 4.365},
34 | {"time": 360, "value": 4.176},
35 | ]
36 | return [curve1, curve2]
37 |
38 | def get_yield_curve_config(theme: dict) -> Dict[str, Any]:
39 | """Creates yield curve chart configuration"""
40 | curves = get_sample_yield_curves()
41 |
42 | # Extract title font settings from theme correctly
43 | title_font_family = theme.get("titleOptions", {}).get("fontFamily", "inherit")
44 | title_font_size = theme.get("titleOptions", {}).get("fontSize", 14)
45 | title_font_style = theme.get("titleOptions", {}).get("fontStyle", "normal")
46 |
47 | return {
48 | "chart": {
49 | "layout": theme["layout"],
50 | "grid": {
51 | "vertLines": {"visible": False},
52 | "horzLines": {"visible": False},
53 | },
54 | "yieldCurve": {
55 | "baseResolution": 12,
56 | "minimumTimeRange": 10,
57 | "startTimeRange": 3,
58 | },
59 | "handleScroll": False,
60 | "handleScale": False,
61 | "fontFamily": "inherit",
62 | "titleFontFamily": title_font_family,
63 | "titleFontSize": title_font_size,
64 | "titleFontStyle": title_font_style,
65 | },
66 | "series": [
67 | {
68 | "type": "Line",
69 | "data": curves[0],
70 | "options": {
71 | "lineType": 2,
72 | "color": "#26c6da",
73 | "pointMarkersVisible": True,
74 | "lineWidth": 2,
75 | }
76 | },
77 | {
78 | "type": "Line",
79 | "data": curves[1],
80 | "options": {
81 | "lineType": 2,
82 | "color": "rgb(164, 89, 209)",
83 | "pointMarkersVisible": True,
84 | "lineWidth": 1,
85 | }
86 | }
87 | ],
88 | "height": 400,
89 | "title": "Yield Curve Comparison",
90 | }
--------------------------------------------------------------------------------
/e2e/test_template.py:
--------------------------------------------------------------------------------
1 | from pathlib import Path
2 |
3 | import pytest
4 |
5 | from playwright.sync_api import Page, expect
6 |
7 | from e2e_utils import StreamlitRunner
8 |
9 | ROOT_DIRECTORY = Path(__file__).parent.parent.absolute()
10 | BASIC_EXAMPLE_FILE = ROOT_DIRECTORY / "my_component" / "example.py"
11 |
12 | @pytest.fixture(autouse=True, scope="module")
13 | def streamlit_app():
14 | with StreamlitRunner(BASIC_EXAMPLE_FILE) as runner:
15 | yield runner
16 |
17 |
18 | @pytest.fixture(autouse=True, scope="function")
19 | def go_to_app(page: Page, streamlit_app: StreamlitRunner):
20 | page.goto(streamlit_app.server_url)
21 | # Wait for app to load
22 | page.get_by_role("img", name="Running...").is_hidden()
23 |
24 |
25 | def test_should_render_template(page: Page):
26 | frame_0 = page.frame_locator(
27 | 'iframe[title="my_component\\.my_component"]'
28 | ).nth(0)
29 | frame_1 = page.frame_locator(
30 | 'iframe[title="my_component\\.my_component"]'
31 | ).nth(1)
32 |
33 | st_markdown_0 = page.get_by_role('paragraph').nth(0)
34 | st_markdown_1 = page.get_by_role('paragraph').nth(1)
35 |
36 | expect(st_markdown_0).to_contain_text("You've clicked 0 times!")
37 |
38 | frame_0.get_by_role("button", name="Click me!").click()
39 |
40 | expect(st_markdown_0).to_contain_text("You've clicked 1 times!")
41 | expect(st_markdown_1).to_contain_text("You've clicked 0 times!")
42 |
43 | frame_1.get_by_role("button", name="Click me!").click()
44 | frame_1.get_by_role("button", name="Click me!").click()
45 |
46 | expect(st_markdown_0).to_contain_text("You've clicked 1 times!")
47 | expect(st_markdown_1).to_contain_text("You've clicked 2 times!")
48 |
49 | page.get_by_label("Enter a name").click()
50 | page.get_by_label("Enter a name").fill("World")
51 | page.get_by_label("Enter a name").press("Enter")
52 |
53 | expect(frame_1.get_by_text("Hello, World!")).to_be_visible()
54 |
55 | frame_1.get_by_role("button", name="Click me!").click()
56 |
57 | expect(st_markdown_0).to_contain_text("You've clicked 1 times!")
58 | expect(st_markdown_1).to_contain_text("You've clicked 3 times!")
59 |
60 |
61 | def test_should_change_iframe_height(page: Page):
62 | frame = page.frame_locator('iframe[title="my_component\\.my_component"]').nth(1)
63 |
64 | expect(frame.get_by_text("Hello, Streamlit!")).to_be_visible()
65 |
66 | locator = page.locator('iframe[title="my_component\\.my_component"]').nth(1)
67 |
68 | page.wait_for_timeout(1000)
69 | init_frame_height = locator.bounding_box()['height']
70 | assert init_frame_height != 0
71 |
72 | page.get_by_label("Enter a name").click()
73 |
74 | page.get_by_label("Enter a name").fill(35 * "Streamlit ")
75 | page.get_by_label("Enter a name").press("Enter")
76 |
77 | expect(frame.get_by_text("Streamlit Streamlit Streamlit")).to_be_visible()
78 |
79 | page.wait_for_timeout(1000)
80 | frame_height = locator.bounding_box()['height']
81 | assert frame_height > init_frame_height
82 |
83 | page.set_viewport_size({"width": 150, "height": 150})
84 |
85 | expect(frame.get_by_text("Streamlit Streamlit Streamlit")).not_to_be_in_viewport()
86 |
87 | page.wait_for_timeout(1000)
88 | frame_height_after_viewport_change = locator.bounding_box()['height']
89 | assert frame_height_after_viewport_change > frame_height
90 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Streamlit Lightweight Charts v5
2 |
3 | A Streamlit component that integrates TradingView's Lightweight Charts v5 library, providing interactive financial charts with multi-pane support for technical analysis.
4 |
5 | ## Overview
6 |
7 | Streamlit Lightweight Charts v5 is built around version 5 of the TradingView Lightweight Charts library, which introduces powerful multi-pane capabilities perfect for technical analysis. This component allows you to create great looking financial charts with multiple indicators stacked vertically, similar to popular trading platforms.
8 |
9 | Key features:
10 |
11 | - Multi-pane chart layouts for price and indicators
12 | - Customizable themes and styles
13 | - Add your own favourite standalone technical indicators (RSI, MACD, Williams %R etc.)
14 | - Use overlay indicators (Moving Averages, AVWAP, Pivot Points...)
15 | - Support for drawing Rectangles for e.g. Support / Resistance areas from code
16 | - Yield curve charts
17 | - Supports Screenshots
18 |
19 | 
20 |
21 | 
22 |
23 | 
24 |
25 | ## Installation
26 |
27 | ```bash
28 | python3 -m venv venv
29 | source venv/bin/activate
30 | pip install streamlit-lightweight-charts-v5
31 | pip install streamlit yfinance numpy # for running demos
32 | ```
33 |
34 | ## Quick Start
35 |
36 | ```python
37 | import streamlit as st
38 | from lightweight_charts_v5 import lightweight_charts_v5_component
39 | import yfinance as yf
40 |
41 | # Load stock data
42 | ticker = "AAPL"
43 | data = yf.download(ticker, period="100d", interval="1d", auto_adjust=False)
44 |
45 | # Convert data to Lightweight Charts format, ensuring values are proper floats
46 | chart_data = [
47 | {"time": str(date.date()), "value": float(row["Close"].iloc[0])}
48 | for date, row in data.iterrows()
49 | ]
50 |
51 | # Streamlit app
52 | st.title(f"{ticker} Stock Price Line Chart")
53 |
54 | # Render the chart
55 | lightweight_charts_v5_component(
56 | name=f"{ticker} Chart",
57 | charts=[{
58 | "chart": {"layout": {"background": {"color": "#FFFFFF"}}},
59 | "series": [{
60 | "type": "Line",
61 | "data": chart_data,
62 | "options": {"color": "#2962FF"}
63 | }],
64 | "height": 400
65 | }],
66 | height=400
67 | )
68 | ```
69 |
70 | ## Demos
71 |
72 | The repository includes a `demo/` directory with two example scripts that showcase how to use the component.
73 |
74 | - `minimal_demo.py`: A minimal example using Yahoo Finance stock data
75 | - `chart_demo.py`: A slightly more advanced example with multiple indicators
76 | - `chart_themes.py`: Theme customization examples for the chart_demo module.
77 | - `indicators.py`: Example indicators for the chart_demo module.
78 | - `yield_curve.py`: Yield curve example chart for the chart_demo module.
79 |
80 | You can find the demo files in the [GitHub repository](https://github.com/locupleto/streamlit-lightweight-charts-v5/tree/main/demo).
81 |
82 | ## Running the Demo Applications
83 |
84 | To test the two demo scripts, run them using **Streamlit**:
85 |
86 | ```bash
87 | streamlit run demo/minimal_demo.py # Minimal example
88 | streamlit run demo/chart_demo.py # Full demo with indicators
89 | ```
90 |
91 | ## License
92 |
93 | This project is licensed under the MIT License.
94 |
--------------------------------------------------------------------------------
/demo/multi_demo.py:
--------------------------------------------------------------------------------
1 | import streamlit as st
2 | import yfinance as yf
3 | import pandas as pd
4 | from lightweight_charts_v5 import lightweight_charts_v5_component
5 | from indicators import PriceIndicator
6 | from chart_demo import HANDWRITTEN_FONTS
7 |
8 | LARGE_US_STOCKS = [
9 | {"symbol": "AAPL", "name": "Apple Inc."},
10 | {"symbol": "MSFT", "name": "Microsoft Corporation"},
11 | {"symbol": "AMZN", "name": "Amazon.com Inc."},
12 | {"symbol": "GOOGL", "name": "Alphabet Inc."},
13 | {"symbol": "META", "name": "Meta Platforms Inc."},
14 | {"symbol": "NVDA", "name": "NVIDIA Corporation"}
15 | ]
16 |
17 | def run_multi_chart_demo(theme, selected_theme_name):
18 | """Run the multi-chart demo with the provided theme"""
19 | col1, col2, col3, col4, col5 = st.columns([1, 1, 1, 1, 1])
20 |
21 | with col1:
22 | history_options = ["1 Month", "3 Months", "6 Months", "1 Year"]
23 | selected_history = st.selectbox("History Length", history_options, index=1)
24 |
25 | # Parse history length
26 | if "Month" in selected_history:
27 | months = int(selected_history.split()[0])
28 | period = f"{months}mo"
29 | else:
30 | years = int(selected_history.split()[0])
31 | period = f"{years}y"
32 |
33 | # Create rows for the grid
34 | for i in range(0, len(LARGE_US_STOCKS), 2):
35 | cols = st.columns(2)
36 |
37 | # First column
38 | with cols[0]:
39 | display_chart(LARGE_US_STOCKS[i], period, theme, selected_theme_name)
40 |
41 | # Second column (if available)
42 | if i + 1 < len(LARGE_US_STOCKS):
43 | with cols[1]:
44 | display_chart(LARGE_US_STOCKS[i + 1], period, theme, selected_theme_name)
45 |
46 | def display_chart(symbol_info, period, theme, selected_theme_name):
47 | try:
48 | # Download data
49 | ticker = yf.Ticker(symbol_info["symbol"])
50 | df = ticker.history(period=period)
51 |
52 | # Reset index to make date a column
53 | df = df.reset_index()
54 |
55 | # Rename columns to match expected format
56 | df = df.rename(columns={
57 | 'Date': 'date',
58 | 'Open': 'open',
59 | 'High': 'high',
60 | 'Low': 'low',
61 | 'Close': 'close',
62 | 'Volume': 'volume'
63 | })
64 |
65 | # Format date exactly like chart_demo
66 | df['date'] = df['date'].dt.strftime('%Y-%m-%d')
67 |
68 | # Create title
69 | title = f"{symbol_info['symbol']} - {symbol_info['name']}"
70 |
71 | # Create indicator with Area style instead of Line
72 | indicator = PriceIndicator(
73 | df=df,
74 | height=250,
75 | title=title,
76 | style="Area", # Changed from "Line" to "Area"
77 | theme=theme
78 | )
79 |
80 | # Calculate indicator
81 | indicator.calculate()
82 |
83 | # Get chart configuration
84 | chart_config = indicator.pane()
85 |
86 | # Render chart
87 | lightweight_charts_v5_component(
88 | name=symbol_info["symbol"],
89 | charts=[chart_config],
90 | height=chart_config["height"],
91 | zoom_level=250,
92 | take_screenshot=False,
93 | configure_time_scale=False,
94 | fonts=HANDWRITTEN_FONTS if selected_theme_name == "Custom" else None,
95 | key=f"chart_{symbol_info['symbol']}"
96 | )
97 |
98 | except Exception as e:
99 | st.error(f"Error displaying chart for {symbol_info['symbol']}: {str(e)}")
--------------------------------------------------------------------------------
/lightweight_charts_v5/__init__.py:
--------------------------------------------------------------------------------
1 | # __init__.py
2 |
3 | import os
4 | import socket
5 | from typing import List, Dict, Any, Optional, Union
6 | import streamlit.components.v1 as components
7 |
8 | COMPONENT_NAME = "lightweight_charts_v5_component"
9 | __version__ = "0.1.7"
10 | _RELEASE = False # Keep this False for development flexibility
11 |
12 | # Function to check if dev server is running
13 | def _is_dev_server_running():
14 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
15 | try:
16 | # Set a short timeout to avoid hanging
17 | s.settimeout(0.5)
18 | s.connect(('localhost', 3001))
19 | # Try to receive data to confirm it's actually the dev server
20 | data = s.recv(1024)
21 | s.close()
22 | return len(data) > 0
23 | except:
24 | return False
25 |
26 | # Use dev server if it's running and we're in dev mode, otherwise use build
27 | if not _RELEASE and _is_dev_server_running():
28 | _component_func = components.declare_component(
29 | COMPONENT_NAME,
30 | url="http://localhost:3001",
31 | )
32 | else:
33 | parent_dir = os.path.dirname(os.path.abspath(__file__))
34 | build_dir = os.path.join(parent_dir, "frontend/build")
35 | _component_func = components.declare_component(COMPONENT_NAME, path=build_dir)
36 |
37 | def lightweight_charts_v5_component(name, data=None,
38 | charts=None,
39 | height: int = 400,
40 | take_screenshot: bool = False,
41 | zoom_level: int = 200,
42 | fonts: List[str] = None,
43 | configure_time_scale: bool = False,
44 | key=None):
45 | """
46 | Create a new instance of the component.
47 |
48 | Parameters
49 | ----------
50 | name: str
51 | A label or title.
52 | data: list of dict
53 | Data for a single pane chart (if not using multiple panes).
54 | charts: list of dict
55 | A list of pane configuration dictionaries (for multiple panes).
56 |
57 | Each series in a chart can include rectangles with the following format:
58 | {
59 | "startTime": "2023-01-01", # Time for the starting point
60 | "startPrice": 100.0, # Price for the starting point
61 | "endTime": "2023-01-15", # Time for the ending point
62 | "endPrice": 120.0, # Price for the ending point
63 | "fillColor": "rgba(255, 0, 0, 0.2)", # Fill color with opacity
64 | "borderColor": "rgba(255, 0, 0, 1)", # Optional border color
65 | "borderWidth": 1, # Optional border width
66 | "opacity": 0.5 # Optional opacity (overrides the one in fillColor)
67 | }
68 |
69 | height: int
70 | Overall chart height (if using single pane or as a fallback).
71 | take_screenshot: bool
72 | If True, triggers a screenshot of the chart
73 | zoom_level: int
74 | Number of bars to show in the initial view (default: 200).
75 | fonts: List[str]
76 | List of optional google fonts that will be downloaded for use.
77 | configure_time_scale: bool
78 | If True, applies additional time scale configuration that helps with
79 | multi-chart layouts with really small charts but may cause issues with
80 | screenshot functionality. Default is False.
81 | key: str or None
82 | Optional key.
83 |
84 | Returns
85 | -------
86 | dict or int
87 | If take_screenshot is True, returns a dict containing the screenshot data.
88 | Otherwise returns the component's default return value (int).
89 | """
90 | # Use different defaults based on screenshot mode
91 | default_value = None if take_screenshot else 0
92 |
93 | # If charts configuration is provided, pass that.
94 | # Otherwise, pass data and height.
95 | if charts is not None:
96 | return _component_func(
97 | name=name,
98 | charts=charts,
99 | height=height,
100 | take_screenshot=take_screenshot,
101 | zoom_level=zoom_level,
102 | fonts=fonts,
103 | key=key,
104 | configure_time_scale=configure_time_scale,
105 | default=default_value
106 | )
107 | else:
108 | return _component_func(
109 | name=name,
110 | data=data,
111 | height=height,
112 | take_screenshot=take_screenshot,
113 | zoom_level=zoom_level,
114 | key=key,
115 | configure_time_scale=configure_time_scale,
116 | default=default_value
117 | )
--------------------------------------------------------------------------------
/lightweight_charts_v5/frontend/src/plugins/StaticRectanglePlugin.ts:
--------------------------------------------------------------------------------
1 | // StaticRectanglePlugin.ts
2 | import { CanvasRenderingTarget2D } from 'fancy-canvas';
3 | import {
4 | Coordinate,
5 | IChartApi,
6 | ISeriesApi,
7 | IPrimitivePaneRenderer,
8 | IPrimitivePaneView,
9 | ISeriesPrimitive,
10 | SeriesType,
11 | Time,
12 | } from 'lightweight-charts';
13 |
14 | // Define the rectangle properties that will be passed from Python
15 | export interface RectangleOptions {
16 | startTime: Time;
17 | startPrice: number;
18 | endTime: Time;
19 | endPrice: number;
20 | fillColor: string;
21 | borderColor?: string;
22 | borderWidth?: number;
23 | opacity?: number;
24 | zOrder?: 'top' | 'bottom'; // Just 'top' or 'bottom'
25 | }
26 |
27 | class RectanglePaneRenderer implements IPrimitivePaneRenderer {
28 | _p1: { x: Coordinate | null; y: Coordinate | null };
29 | _p2: { x: Coordinate | null; y: Coordinate | null };
30 | _fillColor: string;
31 | _borderColor: string | undefined;
32 | _borderWidth: number;
33 | _opacity: number;
34 |
35 | constructor(
36 | p1: { x: Coordinate | null; y: Coordinate | null },
37 | p2: { x: Coordinate | null; y: Coordinate | null },
38 | fillColor: string,
39 | borderColor?: string,
40 | borderWidth?: number,
41 | opacity?: number
42 | ) {
43 | this._p1 = p1;
44 | this._p2 = p2;
45 | this._fillColor = fillColor;
46 | this._borderColor = borderColor;
47 | this._borderWidth = borderWidth || 0;
48 | this._opacity = opacity !== undefined ? opacity : 0.75;
49 | }
50 |
51 | draw(target: CanvasRenderingTarget2D) {
52 | target.useBitmapCoordinateSpace(scope => {
53 | if (
54 | this._p1.x === null ||
55 | this._p1.y === null ||
56 | this._p2.x === null ||
57 | this._p2.y === null
58 | ) {
59 | return;
60 | }
61 |
62 | const ctx = scope.context;
63 |
64 | // Calculate positions with proper scaling
65 | const x1 = Math.round(this._p1.x * scope.horizontalPixelRatio);
66 | const y1 = Math.round(this._p1.y * scope.verticalPixelRatio);
67 | const x2 = Math.round(this._p2.x * scope.horizontalPixelRatio);
68 | const y2 = Math.round(this._p2.y * scope.verticalPixelRatio);
69 |
70 | // Calculate rectangle dimensions
71 | const left = Math.min(x1, x2);
72 | const top = Math.min(y1, y2);
73 | const width = Math.abs(x2 - x1);
74 | const height = Math.abs(y2 - y1);
75 |
76 | // Save current context state
77 | ctx.save();
78 |
79 | // Set global alpha for opacity
80 | ctx.globalAlpha = this._opacity;
81 |
82 | // Fill rectangle
83 | ctx.fillStyle = this._fillColor;
84 | ctx.fillRect(left, top, width, height);
85 |
86 | // Only draw border if borderWidth is greater than 0
87 | if (this._borderColor && this._borderWidth > 0) {
88 | const borderWidth = this._borderWidth * Math.max(scope.horizontalPixelRatio, scope.verticalPixelRatio);
89 | ctx.strokeStyle = this._borderColor;
90 | ctx.lineWidth = borderWidth;
91 |
92 | // Adjust the stroke rectangle to account for the border width
93 | const offset = borderWidth / 2;
94 | ctx.strokeRect(
95 | left + offset,
96 | top + offset,
97 | width - borderWidth,
98 | height - borderWidth
99 | );
100 | }
101 |
102 | // Restore context state
103 | ctx.restore();
104 | });
105 | }
106 | }
107 |
108 | class StaticRectanglePaneView implements IPrimitivePaneView {
109 | _chart: IChartApi;
110 | _series: ISeriesApi;
111 | _options: RectangleOptions;
112 | _p1: { x: Coordinate | null; y: Coordinate | null } = { x: null, y: null };
113 | _p2: { x: Coordinate | null; y: Coordinate | null } = { x: null, y: null };
114 |
115 | constructor(chart: IChartApi, series: ISeriesApi, options: RectangleOptions) {
116 | this._chart = chart;
117 | this._series = series;
118 | this._options = options;
119 | }
120 |
121 | update() {
122 | // Get price coordinates
123 | const y1 = this._series.priceToCoordinate(this._options.startPrice);
124 | const y2 = this._series.priceToCoordinate(this._options.endPrice);
125 |
126 | // Get time coordinates
127 | const timeScale = this._chart.timeScale();
128 | const x1 = timeScale.timeToCoordinate(this._options.startTime);
129 | const x2 = timeScale.timeToCoordinate(this._options.endTime);
130 |
131 | // Update points
132 | this._p1 = { x: x1, y: y1 };
133 | this._p2 = { x: x2, y: y2 };
134 | }
135 |
136 | zOrder() {
137 | return this._options.zOrder || 'bottom'; // Default to bottom if not specified
138 | }
139 |
140 | renderer() {
141 | // Make sure coordinates are updated before rendering
142 | this.update();
143 |
144 | return new RectanglePaneRenderer(
145 | this._p1,
146 | this._p2,
147 | this._options.fillColor,
148 | this._options.borderColor,
149 | this._options.borderWidth,
150 | this._options.opacity
151 | );
152 | }
153 | }
154 |
155 | // This class implements ISeriesPrimitive