├── 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 | ![Screenshot](https://github.com/locupleto/streamlit-lightweight-charts-v5/raw/main/Screenshot_1.png) 20 | 21 | ![Screenshot](https://github.com/locupleto/streamlit-lightweight-charts-v5/raw/main/Screenshot_2.png) 22 | 23 | ![Screenshot](https://github.com/locupleto/streamlit-lightweight-charts-v5/raw/main/Screenshot_3.png) 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