- Moved ChartAsync’s methods into the Chart object.

- Removed ChartAsync.
- Added the `show_async` method to `Chart`.
- Refactored how the TopBar is used. The docs explain this in detail, but a basic rundown is:
    - `corner_text` and `create_switcher` are no longer methods. The `topbar` attribute of `chart` should be used instead.
    - switchers and textboxes, now created with `chart.topbar.textbox` and `chart.topbar.switcher` require a name to be passed to them, which is used to access its instance (e.g `chart.topbar[‘timeframe’]`)
    - If you have any questions about these changes, or potential enhancements, feel free to raise an issue and I will get back to you ASAP :)

- PtQt and Wx can now use either synchronous or asynchronous callback functions

- BETA: Support for Jupyter Notebooks

- Fixed a bug causing the ‘date’ column of DataFrames passed to `set`, `update`, and `update_from_tick` to be modified.
This commit is contained in:
louisnw
2023-06-04 14:38:58 +01:00
parent a58f1e306c
commit 3a7832e0d4
27 changed files with 566 additions and 459 deletions

View File

@ -7,16 +7,17 @@
[![License](https://img.shields.io/github/license/louisnw01/lightweight-charts-python?color=9c2400)](https://github.com/louisnw01/lightweight-charts-python/blob/main/LICENSE) [![License](https://img.shields.io/github/license/louisnw01/lightweight-charts-python?color=9c2400)](https://github.com/louisnw01/lightweight-charts-python/blob/main/LICENSE)
[![Documentation](https://img.shields.io/badge/documentation-006ee3)](https://lightweight-charts-python.readthedocs.io/en/latest/docs.html) [![Documentation](https://img.shields.io/badge/documentation-006ee3)](https://lightweight-charts-python.readthedocs.io/en/latest/docs.html)
![async](https://raw.githubusercontent.com/louisnw01/lightweight-charts-python/main/examples/6_async/async.png)
![cover](https://raw.githubusercontent.com/louisnw01/lightweight-charts-python/main/cover.png) ![cover](https://raw.githubusercontent.com/louisnw01/lightweight-charts-python/main/cover.png)
lightweight-charts-python aims to provide a simple and pythonic way to access and implement [TradingView's Lightweight Charts](https://www.tradingview.com/lightweight-charts/). lightweight-charts-python aims to provide a simple and pythonic way to access and implement [TradingView's Lightweight Charts](https://www.tradingview.com/lightweight-charts/).
</div> </div>
## Installation ## Installation
``` ```
pip install lightweight-charts pip install lightweight-charts
``` ```
* White screen? Having issues with pywebview? Click [here](https://github.com/louisnw01/lightweight-charts-python/issues?q=label%3A%22pywebview+issue%22+).
___ ___
## Features ## Features
@ -28,7 +29,8 @@ ___
* wxPython * wxPython
* Streamlit * Streamlit
* asyncio * asyncio
5. [Callbacks](https://lightweight-charts-python.readthedocs.io/en/latest/docs.html#chartasync) allowing for timeframe (1min, 5min, 30min etc.) selectors, searching, and more. * Jupyter Notebooks using the [JupyterChart](https://lightweight-charts-python.readthedocs.io/en/latest/docs.html#jupyterchart)
5. [Callbacks](https://lightweight-charts-python.readthedocs.io/en/latest/docs.html#callbacks) allowing for timeframe (1min, 5min, 30min etc.) selectors, searching, and more.
6. Multi-Pane Charts using the `SubChart` ([examples](https://lightweight-charts-python.readthedocs.io/en/latest/docs.html#subchart)). 6. Multi-Pane Charts using the `SubChart` ([examples](https://lightweight-charts-python.readthedocs.io/en/latest/docs.html#subchart)).
___ ___
@ -125,27 +127,27 @@ from lightweight_charts import Chart
def calculate_sma(data: pd.DataFrame, period: int = 50): def calculate_sma(data: pd.DataFrame, period: int = 50):
def avg(d: pd.DataFrame): def avg(d: pd.DataFrame):
return d['close'].mean() return d['close'].mean()
result = []
for i in range(period - 1, len(data)): result = []
val = avg(data.iloc[i - period + 1:i]) for i in range(period - 1, len(data)):
result.append({'time': data.iloc[i]['date'], 'value': val}) val = avg(data.iloc[i - period + 1:i])
return pd.DataFrame(result) result.append({'time': data.iloc[i]['date'], 'value': val})
return pd.DataFrame(result)
if __name__ == '__main__': if __name__ == '__main__':
chart = Chart()
chart = Chart()
df = pd.read_csv('ohlcv.csv')
df = pd.read_csv('ohlcv.csv') chart.set(df)
chart.set(df)
line = chart.create_line()
line = chart.create_line() sma_data = calculate_sma(df)
sma_data = calculate_sma(df) line._set(sma_data)
line.set(sma_data)
chart.show(block=True)
chart.show(block=True)
``` ```
![line indicators image](https://raw.githubusercontent.com/louisnw01/lightweight-charts-python/main/examples/4_line_indicators/line_indicators.png) ![line indicators image](https://raw.githubusercontent.com/louisnw01/lightweight-charts-python/main/examples/4_line_indicators/line_indicators.png)
@ -188,68 +190,60 @@ if __name__ == '__main__':
![styling image](https://raw.githubusercontent.com/louisnw01/lightweight-charts-python/main/examples/5_styling/styling.png) ![styling image](https://raw.githubusercontent.com/louisnw01/lightweight-charts-python/main/examples/5_styling/styling.png)
___ ___
### 6. ChartAsync: ### 6. Callbacks:
```python ```python
import asyncio import asyncio
import pandas as pd import pandas as pd
from lightweight_charts import ChartAsync from lightweight_charts import Chart
def get_bar_data(symbol, timeframe): def get_bar_data(symbol, timeframe):
if symbol not in ('AAPL', 'GOOGL', 'TSLA'):
print(f'No data for "{symbol}"')
return pd.DataFrame()
return pd.read_csv(f'bar_data/{symbol}_{timeframe}.csv') return pd.read_csv(f'bar_data/{symbol}_{timeframe}.csv')
class API: class API:
def __init__(self): def __init__(self):
self.chart = None # Changes after each callback. self.chart = None # Changes after each callback.
self.symbol = 'TSLA'
self.timeframe = '5min'
async def on_search(self, searched_string): # Called when the user searches. async def on_search(self, searched_string): # Called when the user searches.
self.symbol = searched_string new_data = get_bar_data(searched_string, self.chart.topbar['timeframe'].value)
new_data = await self.get_data() if new_data.empty:
return
self.chart.topbar['corner'].set(searched_string)
self.chart.set(new_data)
async def on_timeframe_selection(self): # Called when the user changes the timeframe.
new_data = get_bar_data(self.chart.topbar['corner'].value, self.chart.topbar['timeframe'].value)
if new_data.empty: if new_data.empty:
return return
self.chart.set(new_data) self.chart.set(new_data)
self.chart.corner_text(searched_string)
async def on_timeframe_selection(self, timeframe): # Called when the user changes the timeframe.
self.timeframe = timeframe
new_data = await self.get_data()
if new_data.empty:
return
self.chart.set(new_data)
async def get_data(self):
if self.symbol not in ('AAPL', 'GOOGL', 'TSLA'):
print(f'No data for "{self.symbol}"')
return pd.DataFrame()
data = get_bar_data(self.symbol, self.timeframe)
return data
async def main(): async def main():
api = API() api = API()
chart = ChartAsync(api=api, debug=True) chart = Chart(api=api, topbar=True, searchbox=True)
chart.legend(True) chart.legend(True)
chart.create_switcher(api.on_timeframe_selection, '1min', '5min', '30min', default='5min') chart.topbar.textbox('corner', 'TSLA')
chart.corner_text(api.symbol) chart.topbar.switcher('timeframe', api.on_timeframe_selection, '1min', '5min', '30min', default='5min')
df = get_bar_data(api.symbol, api.timeframe) df = get_bar_data('TSLA', '5min')
chart.set(df) chart.set(df)
await chart.show(block=True) await chart.show_async(block=True)
if __name__ == '__main__': if __name__ == '__main__':
asyncio.run(main()) asyncio.run(main())
``` ```
![async gif](https://raw.githubusercontent.com/louisnw01/lightweight-charts-python/main/examples/6_async/async.gif) ![callbacks gif](https://raw.githubusercontent.com/louisnw01/lightweight-charts-python/main/examples/6_callbacks/callbacks.gif)
___ ___
<div align="center"> <div align="center">

BIN
cover.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 161 KiB

After

Width:  |  Height:  |  Size: 279 KiB

View File

@ -1,7 +1,7 @@
project = 'lightweight-charts-python' project = 'lightweight-charts-python'
copyright = '2023, louisnw' copyright = '2023, louisnw'
author = 'louisnw' author = 'louisnw'
release = '1.0.9' release = '1.0.10'
extensions = ["myst_parser"] extensions = ["myst_parser"]

View File

@ -9,7 +9,7 @@
___ ___
## Common Methods ## Common Methods
These methods can be used within the [`Chart`](#chart), [`SubChart`](#subchart), [`ChartAsync`](#chartasync), [`QtChart`](#qtchart), [`WxChart`](#wxchart) and [`StreamlitChart`](#streamlitchart) objects. These methods can be used within the [`Chart`](#chart), [`SubChart`](#subchart), [`QtChart`](#qtchart), [`WxChart`](#wxchart) and [`StreamlitChart`](#streamlitchart) objects.
___ ___
### `set` ### `set`
@ -186,7 +186,8 @@ ___
## Chart ## Chart
`volume_enabled: bool` | `width: int` | `height: int` | `x: int` | `y: int` | `on_top: bool` | `debug: bool` `volume_enabled: bool` | `width: int` | `height: int` | `x: int` | `y: int` | `on_top: bool` | `debug: bool` |
`api: object` | `topbar: bool` | `searchbox: bool`
The main object used for the normal functionality of lightweight-charts-python, built on the pywebview library. The main object used for the normal functionality of lightweight-charts-python, built on the pywebview library.
___ ___
@ -208,6 +209,13 @@ Exits and destroys the chart and window.
___ ___
### `show_async`
`block: bool`
Show the chart asynchronously. This should be utilised when using [Callbacks](#callbacks).
___
## Line ## Line
The `Line` object represents a `LineSeries` object in Lightweight Charts and can be used to create indicators. As well as the methods described below, the `Line` object also has access to the [`title`](#title), [`marker`](#marker) and [`horizontal_line`](#horizontal-line) methods. The `Line` object represents a `LineSeries` object in Lightweight Charts and can be used to create indicators. As well as the methods described below, the `Line` object also has access to the [`title`](#title), [`marker`](#marker) and [`horizontal_line`](#horizontal-line) methods.
@ -241,12 +249,12 @@ The `SubChart` object allows for the use of multiple chart panels within the sam
___ ___
### Grid of 4 Example: ### Grid of 4 Example:
```python ```python
import pandas as pd import pandas as pd
from lightweight_charts import Chart from lightweight_charts import Chart
if __name__ == '__main__': if __name__ == '__main__':
chart = Chart(inner_width=0.5, inner_height=0.5) chart = Chart(inner_width=0.5, inner_height=0.5)
chart2 = chart.create_subchart(position='right', width=0.5, height=0.5) chart2 = chart.create_subchart(position='right', width=0.5, height=0.5)
@ -262,9 +270,9 @@ if __name__ == '__main__':
df = pd.read_csv('ohlcv.csv') df = pd.read_csv('ohlcv.csv')
chart.set(df) chart.set(df)
chart2.set(df) chart2._set(df)
chart3.set(df) chart3._set(df)
chart4.set(df) chart4._set(df)
chart.show(block=True) chart.show(block=True)
@ -278,34 +286,33 @@ import pandas as pd
from lightweight_charts import Chart from lightweight_charts import Chart
if __name__ == '__main__': if __name__ == '__main__':
chart = Chart(inner_width=1, inner_height=0.8) chart = Chart(inner_width=1, inner_height=0.8)
chart2 = chart.create_subchart(width=1, height=0.2, sync=True, volume_enabled=False) chart2 = chart.create_subchart(width=1, height=0.2, sync=True, volume_enabled=False)
chart2.time_scale(visible=False) chart2.time_scale(visible=False)
df = pd.read_csv('ohlcv.csv') df = pd.read_csv('ohlcv.csv')
df2 = pd.read_csv('rsi.csv') df2 = pd.read_csv('rsi.csv')
chart.set(df) chart.set(df)
line = chart2.create_line() line = chart2.create_line()
line.set(df2) line._set(df2)
chart.show(block=True) chart.show(block=True)
``` ```
___ ___
## ChartAsync ## Callbacks
`api: object` | `top_bar: bool` | `search_box: bool`
The `ChartAsync` object allows for asyncronous callbacks to be passed back to python, allowing for more sophisticated chart layouts including search boxes and timeframe selectors. The `Chart` object allows for asyncronous callbacks to be passed back to python when using the `show_async` method, allowing for more sophisticated chart layouts including searching, timeframe selectors, and text boxes.
[`QtChart`](#qtchart) and [`WxChart`](#wxchart) also have access to the methods specific to `ChartAsync`, however they use their respective event loops to emit callbacks rather than asyncio. [`QtChart`](#qtchart) and [`WxChart`](#wxchart) can also use callbacks, however they use their respective event loops to emit callbacks rather than asyncio.
A variety of the parameters below should be passed to the Chart upon decaration.
* `api`: The class object that the callbacks will be emitted to (see [How to use Callbacks](#how-to-use-callbacks)). * `api`: The class object that the callbacks will be emitted to (see [How to use Callbacks](#how-to-use-callbacks)).
* `top_bar`: Adds a Top Bar to the `Chart` or `SubChart` and allows use of the `create_switcher` method. * `topbar`: Adds a [TopBar](#topbar) to the `Chart` or `SubChart` and allows use of the `create_switcher` method.
* `search_box`: Adds a search box onto the `Chart` or `SubChart` that is activated by typing. * `searchbox`: Adds a search box onto the `Chart` or `SubChart` that is activated by typing.
___ ___
### How to use Callbacks ### How to use Callbacks
@ -320,75 +327,99 @@ class API:
self.chart = None self.chart = None
async def on_search(self, string): async def on_search(self, string):
print(f'You searched for {string}, within the chart holding the id: "{self.chart.id}"') print(f'Search Text: "{string}" | Chart/SubChart ID: "{self.chart.id}"')
``` ```
Upon searching in a `Chart` or `SubChart` window, the expected output would be akin to: Upon searching in a pane, the expected output would be akin to:
``` ```
You searched for AAPL, within the chart holding the id: "window.blyjagcr" Search Text: "AAPL" | Chart/SubChart ID: "window.blyjagcr"
``` ```
When using `SubChart`'s, the id will change depending upon which pane was used to search, due to the instance of `self.chart` dynamically updating to the latest pane which triggered the callback. The ID shown above will change depending upon which pane was used to search, due to the instance of `self.chart` dynamically updating to the latest pane which triggered the callback.
This allows access to the specific [Common Methods](#common-methods) for the pane in question. `self.chart` will update upon each callback, allowing for access to the specific [Common Methods](#common-methods) for the pane in question.
Certain callback methods must be specifically named: ```{important}
* Search callbacks will always be emitted to a method named `on_search` * Search callbacks will always be emitted to a method named `on_search`
```
___ ___
### `create_switcher` ### `TopBar`
`method: function` | `*options: str` | `default: str` The `TopBar` class represents the top bar shown on the chart when using callbacks:
![topbar](https://i.imgur.com/Qu2FW9Y.png)
This class is accessed from the `topbar` attribute of the chart object (`chart.topbar.<method>`), after setting the topbar parameter to `True` upon declaration of the chart.
Switchers and text boxes can be created within the top bar, and their instances can be accessed through the `topbar` dictionary. For example:
```python
chart = Chart(api=api, topbar=True)
chart.topbar.textbox('symbol', 'AAPL') # Declares a textbox displaying 'AAPL'.
print(chart.topbar['symbol'].value) # Prints the value within ('AAPL')
chart.topbar['symbol'].set('MSFT') # Sets the 'symbol' textbox to 'MSFT'
print(chart.topbar['symbol'].value) # Prints the value again ('MSFT')
```
___
### `switcher`
`name: str` | `method: function` | `*options: str` | `default: str`
* `name`: the name of the switcher which can be used to access it from the `topbar` dictionary.
* `method`: The function from the `api` class given to the constructor that will receive the callback. * `method`: The function from the `api` class given to the constructor that will receive the callback.
* `options`: The strings to be displayed within the switcher. This may be a variety of timeframes, security types, or whatever needs to be updated directly from the chart. * `options`: The strings to be displayed within the switcher. This may be a variety of timeframes, security types, or whatever needs to be updated directly from the chart.
* `default`: The initial switcher option set. * `default`: The initial switcher option set.
___ ___
### Example: ### `textbox`
`name: str` | `initial_text: str`
* `name`: the name of the text box which can be used to access it from the `topbar` dictionary.
* `initial_text`: The text to show within the text box.
___
### Callbacks Example:
```python ```python
import asyncio import asyncio
import pandas as pd import pandas as pd
from my_favorite_broker import get_bar_data from my_favorite_broker import get_bar_data
from lightweight_charts import ChartAsync from lightweight_charts import Chart
class API: class API:
def __init__(self): def __init__(self):
self.chart = None self.chart = None
self.symbol = 'TSLA'
self.timeframe = '5min'
async def on_search(self, searched_string): # Called when the user searches. async def on_search(self, searched_string): # Called when the user searches.
self.symbol = searched_string timeframe = self.chart.topbar['timeframe'].value
new_data = await self.get_data() new_data = await get_bar_data(searched_string, timeframe)
if not new_data: if not new_data:
return return
self.chart.set(new_data) # sets data for the Chart or SubChart in question. self.chart.set(new_data) # sets data for the Chart or SubChart in question.
self.chart.corner_text(searched_string) self.chart.topbar['symbol'].set(searched_string)
async def on_timeframe(self, timeframe): # Called when the user changes the timeframe. async def on_timeframe(self): # Called when the user changes the timeframe.
self.timeframe = timeframe timeframe = self.chart.topbar['timeframe'].value
new_data = await self.get_data() symbol = self.chart.topbar['symbol'].value
new_data = await get_bar_data(symbol, timeframe)
if not new_data: if not new_data:
return return
self.chart.set(new_data) self.chart.set(new_data)
async def get_data(self):
data = await get_bar_data(self.symbol, self.timeframe)
return data
async def main(): async def main():
api = API() api = API()
chart = ChartAsync(api=api, debug=True) chart = Chart(api=api, topbar=True, searchbox=True)
chart.corner_text('TSLA') chart.topbar.textbox('symbol', 'TSLA')
chart.create_switcher(api.on_timeframe, '1min', '5min', '30min', 'H', 'D', 'W', default='5min') chart.topbar.switcher('timeframe', api.on_timeframe, '1min', '5min', '30min', 'H', 'D', 'W', default='5min')
df = pd.read_csv('ohlcv.csv') df = pd.read_csv('ohlcv.csv')
chart.set(df) chart.set(df)
await chart.show(block=True) await chart.show_async(block=True)
if __name__ == '__main__': if __name__ == '__main__':
@ -517,3 +548,29 @@ chart.set(df)
chart.load() chart.load()
``` ```
___
## JupyterChart
The `JupyterChart` object allows the use of charts within a notebook, and has similar functionality to the `Chart` object for manipulating data, configuring and styling.
This object only supports the displaying of **static** data, and should not be used with the `update_from_tick` or `update` methods. Every call to the chart object must occur **before** calling `load`.
___
### `load`
Renders the chart. This should be called after setting, styling, and configuring the chart, as no further calls to the `JupyterChart` will be acknowledged.
___
### Example:
```python
import pandas as pd
from lightweight_charts import JupyterChart
chart = JupyterChart()
df = pd.read_csv('ohlcv.csv')
chart.set(df)
chart.load()
```

View File

@ -1,7 +1,7 @@
```{toctree} ```{toctree}
:hidden: :hidden:
:caption: Contents :caption: Contents
:maxdepth: 2 :maxdepth: 3
docs docs
Github Repository <https://github.com/louisnw01/lightweight-charts-python> Github Repository <https://github.com/louisnw01/lightweight-charts-python>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 MiB

After

Width:  |  Height:  |  Size: 5.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 712 KiB

After

Width:  |  Height:  |  Size: 349 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 504 KiB

View File

@ -1,56 +0,0 @@
import asyncio
import pandas as pd
from lightweight_charts import ChartAsync
def get_bar_data(symbol, timeframe):
return pd.read_csv(f'bar_data/{symbol}_{timeframe}.csv')
class API:
def __init__(self):
self.chart = None # Changes after each callback.
self.symbol = 'TSLA'
self.timeframe = '5min'
async def on_search(self, searched_string): # Called when the user searches.
self.symbol = searched_string
new_data = await self.get_data()
if new_data.empty:
return
self.chart.set(new_data)
self.chart.corner_text(searched_string)
async def on_timeframe_selection(self, timeframe): # Called when the user changes the timeframe.
self.timeframe = timeframe
new_data = await self.get_data()
if new_data.empty:
return
self.chart.set(new_data)
async def get_data(self):
if self.symbol not in ('AAPL', 'GOOGL', 'TSLA'):
print(f'No data for "{self.symbol}"')
return pd.DataFrame()
data = get_bar_data(self.symbol, self.timeframe)
return data
async def main():
api = API()
chart = ChartAsync(api=api, debug=True)
chart.legend(True)
chart.create_switcher(api.on_timeframe_selection, '1min', '5min', '30min', default='5min')
chart.corner_text(api.symbol)
df = get_bar_data(api.symbol, api.timeframe)
chart.set(df)
await chart.show(block=True)
if __name__ == '__main__':
asyncio.run(main())

Binary file not shown.

After

Width:  |  Height:  |  Size: 475 KiB

View File

@ -0,0 +1,48 @@
import asyncio
import pandas as pd
from lightweight_charts import Chart
def get_bar_data(symbol, timeframe):
if symbol not in ('AAPL', 'GOOGL', 'TSLA'):
print(f'No data for "{symbol}"')
return pd.DataFrame()
return pd.read_csv(f'bar_data/{symbol}_{timeframe}.csv')
class API:
def __init__(self):
self.chart = None # Changes after each callback.
async def on_search(self, searched_string): # Called when the user searches.
new_data = get_bar_data(searched_string, self.chart.topbar['timeframe'].value)
if new_data.empty:
return
self.chart.topbar['symbol'].set(searched_string)
self.chart.set(new_data)
async def on_timeframe_selection(self): # Called when the user changes the timeframe.
new_data = get_bar_data(self.chart.topbar['symbol'].value, self.chart.topbar['timeframe'].value)
if new_data.empty:
return
self.chart.set(new_data)
async def main():
api = API()
chart = Chart(api=api, topbar=True, searchbox=True)
chart.legend(True)
chart.topbar.textbox('symbol', 'TSLA')
chart.topbar.switcher('timeframe', api.on_timeframe_selection, '1min', '5min', '30min', default='5min')
df = get_bar_data('TSLA', '5min')
chart.set(df)
await chart.show_async(block=True)
if __name__ == '__main__':
asyncio.run(main())

View File

@ -1,5 +1,3 @@
from .chart import Chart from .chart import Chart
from .js import LWC from .js import LWC
from .chartasync import ChartAsync from .widgets import JupyterChart

View File

@ -1,7 +1,8 @@
import asyncio
import webview import webview
import multiprocessing as mp import multiprocessing as mp
from lightweight_charts.js import LWC from lightweight_charts.js import LWC, CALLBACK_SCRIPT, TopBar
class CallbackAPI: class CallbackAPI:
@ -15,12 +16,12 @@ class CallbackAPI:
class PyWV: class PyWV:
def __init__(self, q, exit, loaded, html, width, height, x, y, on_top, debug, emit=None): def __init__(self, q, exit, loaded, html, width, height, x, y, on_top, debug, emit):
self.queue = q self.queue = q
self.exit = exit self.exit = exit
self.loaded = loaded self.loaded = loaded
self.debug = debug self.debug = debug
js_api = CallbackAPI(emit) if emit else None js_api = CallbackAPI(emit)
self.webview = webview.create_window('', html=html, on_top=on_top, js_api=js_api, width=width, height=height, self.webview = webview.create_window('', html=html, on_top=on_top, js_api=js_api, width=width, height=height,
x=x, y=y, background_color='#000000') x=x, y=y, background_color='#000000')
self.webview.events.loaded += self.on_js_load self.webview.events.loaded += self.on_js_load
@ -44,18 +45,27 @@ class PyWV:
class Chart(LWC): class Chart(LWC):
def __init__(self, volume_enabled: bool = True, width: int = 800, height: int = 600, x: int = None, y: int = None, def __init__(self, volume_enabled: bool = True, width: int = 800, height: int = 600, x: int = None, y: int = None,
on_top: bool = False, debug: bool = False, on_top: bool = False, debug: bool = False, api: object = None, topbar: bool = False, searchbox: bool = False,
inner_width: float = 1.0, inner_height: float = 1.0, dynamic_loading: bool = False): inner_width: float = 1.0, inner_height: float = 1.0, dynamic_loading: bool = False):
super().__init__(volume_enabled, inner_width, inner_height, dynamic_loading) super().__init__(volume_enabled, inner_width, inner_height, dynamic_loading)
self._emit = mp.Queue()
self._q = mp.Queue() self._q = mp.Queue()
self._script_func = self._q.put self._script_func = self._q.put
self._exit = mp.Event() self._exit = mp.Event()
self._loaded = mp.Event() self._loaded = mp.Event()
self._process = mp.Process(target=PyWV, args=(self._q, self._exit, self._loaded, self._html, self._process = mp.Process(target=PyWV, args=(self._q, self._exit, self._loaded, self._html,
width, height, x, y, on_top, debug,), daemon=True) width, height, x, y, on_top, debug, self._emit), daemon=True)
self._process.start() self._process.start()
self._create_chart() self._create_chart()
self.api = api
self._js_api_code = 'pywebview.api.callback'
if not topbar and not searchbox:
return
self.run_script(CALLBACK_SCRIPT)
self.topbar = TopBar(self) if topbar else None
self._make_search_box() if searchbox else None
def show(self, block: bool = False): def show(self, block: bool = False):
""" """
Shows the chart window.\n Shows the chart window.\n
@ -74,6 +84,31 @@ class Chart(LWC):
return return
self._exit.clear() self._exit.clear()
async def show_async(self, block=False):
if not self.loaded:
self._q.put('start')
self._loaded.wait()
self._on_js_load()
else:
self._q.put('show')
if block:
try:
while 1:
while self._emit.empty() and not self._exit.is_set():
await asyncio.sleep(0.1)
if self._exit.is_set():
return
key, chart_id, arg = self._emit.get()
self.api.chart = self._charts[chart_id]
if widget := self.api.chart.topbar._widget_with_method(key):
widget.value = arg
await getattr(self.api, key)()
else:
await getattr(self.api, key)(arg)
except KeyboardInterrupt:
return
asyncio.create_task(self.show_async(block=True))
def hide(self): def hide(self):
""" """
Hides the chart window.\n Hides the chart window.\n

View File

@ -1,246 +0,0 @@
import asyncio
import multiprocessing as mp
from typing import Literal, Union
from lightweight_charts import LWC
from lightweight_charts.chart import PyWV
class LWCAsync(LWC):
def __init__(self, volume_enabled: bool = True, inner_width: float = 1.0, inner_height: float = 1.0, dynamic_loading: bool = False):
super().__init__(volume_enabled, inner_width, inner_height, dynamic_loading)
self._charts = {self.id: self}
def _make_search_box(self):
self.run_script(f'makeSearchBox({self.id}, {self._js_api_code})')
def corner_text(self, text: str):
self.run_script(f'{self.id}.cornerText.innerText = "{text}"')
def create_switcher(self, method, *options, default=None):
self.run_script(f'''
makeSwitcher({self.id}, {list(options)}, '{default if default else options[0]}', {self._js_api_code}, '{method.__name__}')
{self.id}.chart.resize(window.innerWidth*{self._inner_width}, (window.innerHeight*{self._inner_height})-{self.id}.topBar.offsetHeight)
''')
def create_subchart(self, top_bar: bool = True, volume_enabled: bool = True, position: Literal['left', 'right', 'top', 'bottom'] = 'left',
width: float = 0.5, height: float = 0.5, sync: Union[bool, str] = False):
subchart = SubChartAsync(self, top_bar, volume_enabled, position, width, height, sync)
self._charts[subchart.id] = subchart
return subchart
class ChartAsync(LWCAsync):
def __init__(self, api: object, top_bar: bool = True, search_box: bool = True, volume_enabled: bool = True, width: int = 800, height: int = 600, x: int = None, y: int = None,
on_top: bool = False, debug: bool = False,
inner_width: float = 1.0, inner_height: float = 1.0, dynamic_loading: bool = False):
super().__init__(volume_enabled, inner_width, inner_height, dynamic_loading)
self.api = api
self._js_api_code = 'pywebview.api.callback'
self._emit = mp.Queue()
self._q = mp.Queue()
self._script_func = self._q.put
self._exit = mp.Event()
self._loaded = mp.Event()
self._process = mp.Process(target=PyWV, args=(self._q, self._exit, self._loaded, self._html,
width, height, x, y, on_top, debug, self._emit), daemon=True)
self._process.start()
self.run_script(ASYNC_SCRIPT)
self._create_chart(top_bar)
self._make_search_box() if search_box else None
async def show(self, block=False):
if not self.loaded:
self._q.put('start')
self._loaded.wait()
self._on_js_load()
else:
self._q.put('show')
if block:
try:
while 1:
while self._emit.empty() and not self._exit.is_set():
await asyncio.sleep(0.1)
if self._exit.is_set():
return
key, chart_id, args = self._emit.get()
self.api.chart = self._charts[chart_id]
await getattr(self.api, key)(args)
except KeyboardInterrupt:
return
asyncio.create_task(self.show(block=True))
class SubChartAsync(LWCAsync):
def __init__(self, parent, top_bar, volume_enabled, position, width, height, sync):
super().__init__(volume_enabled, width, height)
self._chart = parent._chart if isinstance(parent, SubChartAsync) else parent
self._parent = parent
self._position = position
self._rand = self._chart._rand
self.id = f'window.{self._rand.generate()}'
self._js_api_code = self._chart._js_api_code
self.run_script = self._chart.run_script
self._charts = self._chart._charts
self._create_chart(top_bar)
self._make_search_box()
if not sync:
return
sync_parent_var = self._parent.id if isinstance(sync, bool) else sync
self.run_script(f'''
{sync_parent_var}.chart.timeScale().subscribeVisibleLogicalRangeChange((timeRange) => {{
{self.id}.chart.timeScale().setVisibleLogicalRange(timeRange)
}});
''')
ASYNC_SCRIPT = '''
function makeSearchBox(chart, callbackFunction) {
let searchWindow = document.createElement('div')
searchWindow.style.position = 'absolute'
searchWindow.style.top = '30%'
searchWindow.style.left = '50%'
searchWindow.style.transform = 'translate(-50%, -30%)'
searchWindow.style.width = '200px'
searchWindow.style.height = '200px'
searchWindow.style.backgroundColor = 'rgba(30, 30, 30, 0.9)'
searchWindow.style.zIndex = '1000'
searchWindow.style.display = 'none'
searchWindow.style.borderRadius = '10px'
let sBox = document.createElement('input');
sBox.type = 'text';
sBox.placeholder = 'search';
sBox.style.position = 'absolute';
sBox.style.zIndex = '1000';
sBox.style.textAlign = 'center'
sBox.style.left = '50%';
sBox.style.top = '30%';
sBox.style.transform = 'translate(-50%, -30%)'
sBox.style.width = '100px'
sBox.style.backgroundColor = 'rgba(0, 122, 255, 0.2)'
sBox.style.color = 'white'
sBox.style.fontSize = '20px'
sBox.style.border = 'none'
sBox.style.borderRadius = '5px'
searchWindow.appendChild(sBox)
chart.div.appendChild(searchWindow);
let yPrice = null
chart.chart.subscribeCrosshairMove((param) => {
if (param.point){
yPrice = param.point.y;
}
});
let selectedChart = false
chart.wrapper.addEventListener('mouseover', (event) => {
selectedChart = true
})
chart.wrapper.addEventListener('mouseout', (event) => {
selectedChart = false
})
document.addEventListener('keydown', function(event) {
if (!selectedChart) {return}
if (event.altKey && event.code === 'KeyH') {
let price = chart.series.coordinateToPrice(yPrice)
makeHorizontalLine(chart, price, '#FFFFFF', 1, LightweightCharts.LineStyle.Solid, true, '')
}
if (searchWindow.style.display === 'none') {
if (/^[a-zA-Z0-9]$/.test(event.key)) {
searchWindow.style.display = 'block';
sBox.focus();
}
}
else if (event.key === 'Enter') {
callbackFunction(`on_search__${chart.id}__${sBox.value}`)
searchWindow.style.display = 'none'
sBox.value = ''
}
else if (event.key === 'Escape') {
searchWindow.style.display = 'none'
sBox.value = ''
}
});
sBox.addEventListener('input', function() {
sBox.value = sBox.value.toUpperCase();
});
}
function makeSwitcher(chart, items, activeItem, callbackFunction, callbackName) {
let switcherElement = document.createElement('div');
switcherElement.style.margin = '4px 18px'
switcherElement.style.zIndex = '1000'
let intervalElements = items.map(function(item) {
let itemEl = document.createElement('button');
itemEl.style.cursor = 'pointer'
itemEl.style.padding = '3px 6px'
itemEl.style.margin = '0px 4px'
itemEl.style.fontSize = '14px'
itemEl.style.color = 'lightgrey'
itemEl.style.backgroundColor = item === activeItem ? 'rgba(0, 122, 255, 0.7)' : 'transparent'
itemEl.style.border = 'none'
itemEl.style.borderRadius = '4px'
itemEl.addEventListener('mouseenter', function() {
itemEl.style.backgroundColor = item === activeItem ? 'rgba(0, 122, 255, 0.7)' : 'rgb(19, 40, 84)'
})
itemEl.addEventListener('mouseleave', function() {
itemEl.style.backgroundColor = item === activeItem ? 'rgba(0, 122, 255, 0.7)' : 'transparent'
})
itemEl.innerText = item;
itemEl.addEventListener('click', function() {
onItemClicked(item);
});
switcherElement.appendChild(itemEl);
return itemEl;
});
function onItemClicked(item) {
if (item === activeItem) {
return;
}
intervalElements.forEach(function(element, index) {
element.style.backgroundColor = items[index] === item ? 'rgba(0, 122, 255, 0.7)' : 'transparent'
});
activeItem = item;
callbackFunction(`${callbackName}__${chart.id}__${item}`);
}
chart.topBar.appendChild(switcherElement)
makeSeperator(chart.topBar)
return switcherElement;
}
function makeTopBar(chart) {
chart.topBar = document.createElement('div')
chart.topBar.style.backgroundColor = '#191B1E'
chart.topBar.style.borderBottom = '3px solid #3C434C'
chart.topBar.style.borderRadius = '2px'
chart.topBar.style.display = 'flex'
chart.topBar.style.alignItems = 'center'
chart.wrapper.prepend(chart.topBar)
chart.cornerText = document.createElement('div')
chart.cornerText.style.margin = '0px 18px'
chart.cornerText.style.position = 'relative'
chart.cornerText.style.fontFamily = 'SF Pro'
chart.cornerText.style.color = 'lightgrey'
chart.topBar.appendChild(chart.cornerText)
makeSeperator(chart.topBar)
}
function makeSeperator(topBar) {
let seperator = document.createElement('div')
seperator.style.width = '1px'
seperator.style.height = '20px'
seperator.style.backgroundColor = '#3C434C'
topBar.appendChild(seperator)
}
'''

View File

@ -1,6 +1,6 @@
import pandas as pd import pandas as pd
from datetime import timedelta, datetime from datetime import timedelta, datetime
from typing import Union, Literal from typing import Union, Literal, Dict
from lightweight_charts.pkg import LWC_4_0_1 from lightweight_charts.pkg import LWC_4_0_1
from lightweight_charts.util import LINE_STYLE, MARKER_POSITION, MARKER_SHAPE, CROSSHAIR_MODE, _crosshair_mode, _line_style, \ from lightweight_charts.util import LINE_STYLE, MARKER_POSITION, MARKER_SHAPE, CROSSHAIR_MODE, _crosshair_mode, _line_style, \
@ -13,14 +13,15 @@ class SeriesCommon:
self._interval = common_interval.index[0] self._interval = common_interval.index[0]
def _df_datetime_format(self, df: pd.DataFrame): def _df_datetime_format(self, df: pd.DataFrame):
df = df.copy()
if 'date' in df.columns: if 'date' in df.columns:
df = df.rename(columns={'date': 'time'}) df = df.rename(columns={'date': 'time'})
self._set_interval(df) self._set_interval(df)
# df['time'] = df['time'].apply(self._datetime_format)
df['time'] = self._datetime_format(df['time']) df['time'] = self._datetime_format(df['time'])
return df return df
def _series_datetime_format(self, series): def _series_datetime_format(self, series):
series = series.copy()
if 'date' in series.keys(): if 'date' in series.keys():
series = series.rename({'date': 'time'}) series = series.rename({'date': 'time'})
series['time'] = self._datetime_format(series['time']) series['time'] = self._datetime_format(series['time'])
@ -137,6 +138,57 @@ class Line(SeriesCommon):
self.run_script(f'{self.id}.series.update({series.to_dict()})') self.run_script(f'{self.id}.series.update({series.to_dict()})')
class Widget:
def __init__(self, chart):
self._chart = chart
self.method = None
class TextWidget(Widget):
def __init__(self, chart, initial_text):
super().__init__(chart)
self.value = initial_text
self.id = f"window.{self._chart._rand.generate()}"
self._chart.run_script(f'''{self.id} = makeTextBoxWidget({self._chart.id}, "{initial_text}")''')
def set(self, string):
self.value = string
self._chart.run_script(f'{self.id}.innerText = "{string}"')
class SwitcherWidget(Widget):
def __init__(self, chart, method, *options, default):
super().__init__(chart)
self.value = default
self.method = method.__name__
self._chart.run_script(f'''
makeSwitcher({self._chart.id}, {list(options)}, '{default}', {self._chart._js_api_code}, '{method.__name__}')
{self._chart.id}.chart.resize(window.innerWidth*{self._chart._inner_width}, (window.innerHeight*{self._chart._inner_height})-{self._chart.id}.topBar.offsetHeight)
''')
class TopBar:
def __init__(self, chart):
self._chart = chart
self._widgets: Dict[str, Widget] = {}
self._chart.run_script(f'''
makeTopBar({self._chart.id})
{self._chart.id}.chart.resize(window.innerWidth*{self._chart._inner_width}, (window.innerHeight*{self._chart._inner_height})-{self._chart.id}.topBar.offsetHeight)
''')
def __getitem__(self, item): return self._widgets.get(item)
def switcher(self, name, method, *options, default=None):
self._widgets[name] = SwitcherWidget(self._chart, method, *options, default=default if default else options[0])
def textbox(self, name, initial_text=''): self._widgets[name] = TextWidget(self._chart, initial_text)
def _widget_with_method(self, method_name):
for widget in self._widgets.values():
if widget.method == method_name:
return widget
class LWC(SeriesCommon): class LWC(SeriesCommon):
def __init__(self, volume_enabled: bool = True, inner_width: float = 1.0, inner_height: float = 1.0, dynamic_loading: bool = False): def __init__(self, volume_enabled: bool = True, inner_width: float = 1.0, inner_height: float = 1.0, dynamic_loading: bool = False):
self._volume_enabled = volume_enabled self._volume_enabled = volume_enabled
@ -153,24 +205,31 @@ class LWC(SeriesCommon):
self._script_func = None self._script_func = None
self._last_bar = None self._last_bar = None
self._interval = None self._interval = None
self._charts = {self.id: self}
self._js_api_code = None
self._background_color = '#000000' self._background_color = '#000000'
self._volume_up_color = 'rgba(83,141,131,0.8)' self._volume_up_color = 'rgba(83,141,131,0.8)'
self._volume_down_color = 'rgba(200,127,130,0.8)' self._volume_down_color = 'rgba(200,127,130,0.8)'
# self.polygon: PolygonAPI = PolygonAPI(self)
def _on_js_load(self): def _on_js_load(self):
if self.loaded: if self.loaded:
return return
self.loaded = True self.loaded = True
[self.run_script(script) for script in self._scripts] [self.run_script(script) for script in self._scripts]
def _create_chart(self, top_bar=False): def _create_chart(self, autosize=True):
self.run_script(f''' self.run_script(f'''
{self.id} = makeChart({self._inner_width}, {self._inner_height}, topBar={_js_bool(top_bar)}) {self.id} = makeChart({self._inner_width}, {self._inner_height}, autoSize={_js_bool(autosize)})
{self.id}.id = '{self.id}' {self.id}.id = '{self.id}'
{self.id}.wrapper.style.float = "{self._position}" {self.id}.wrapper.style.float = "{self._position}"
''') ''')
def _make_search_box(self):
self.run_script(f'makeSearchBox({self.id}, {self._js_api_code})')
def run_script(self, script): def run_script(self, script):
""" """
For advanced users; evaluates JavaScript within the Webview. For advanced users; evaluates JavaScript within the Webview.
@ -342,7 +401,7 @@ class LWC(SeriesCommon):
""" """
self._background_color = background_color if background_color else self._background_color self._background_color = background_color if background_color else self._background_color
self.run_script(f""" self.run_script(f"""
document.body.style.backgroundColor = '{self._background_color}' document.getElementById('wrapper').style.backgroundColor = '{self._background_color}'
{self.id}.chart.applyOptions({{ {self.id}.chart.applyOptions({{
layout: {{ layout: {{
background: {{ background: {{
@ -484,20 +543,28 @@ class LWC(SeriesCommon):
}});''') }});''')
def create_subchart(self, volume_enabled: bool = True, position: Literal['left', 'right', 'top', 'bottom'] = 'left', def create_subchart(self, volume_enabled: bool = True, position: Literal['left', 'right', 'top', 'bottom'] = 'left',
width: float = 0.5, height: float = 0.5, sync: Union[bool, str] = False): width: float = 0.5, height: float = 0.5, sync: Union[bool, str] = False,
return SubChart(self, volume_enabled, position, width, height, sync) topbar: bool = False, searchbox: bool = False):
subchart = SubChart(self, volume_enabled, position, width, height, sync, topbar, searchbox)
self._charts[subchart.id] = subchart
return subchart
class SubChart(LWC): class SubChart(LWC):
def __init__(self, parent, volume_enabled, position, width, height, sync): def __init__(self, parent, volume_enabled, position, width, height, sync, topbar, searchbox):
super().__init__(volume_enabled, width, height) super().__init__(volume_enabled, width, height)
self._chart = parent._chart if isinstance(parent, SubChart) else parent self._chart = parent._chart if isinstance(parent, SubChart) else parent
self._parent = parent self._parent = parent
self._position = position self._position = position
self._rand = self._chart._rand self._rand = self._chart._rand
self.id = f'window.{self._rand.generate()}' self._js_api_code = self._chart._js_api_code
self.run_script = self._chart.run_script self.run_script = self._chart.run_script
self._charts = self._chart._charts
self.id = f'window.{self._rand.generate()}'
self._create_chart() self._create_chart()
self.topbar = TopBar(self) if topbar else None
self._make_search_box() if searchbox else None
if not sync: if not sync:
return return
sync_parent_var = self._parent.id if isinstance(sync, bool) else sync sync_parent_var = self._parent.id if isinstance(sync, bool) else sync
@ -509,15 +576,9 @@ class SubChart(LWC):
SCRIPT = """ SCRIPT = """
document.body.style.backgroundColor = '#000000' document.getElementById('wrapper').style.backgroundColor = '#000000'
const up = 'rgba(39, 157, 130, 100)'
const down = 'rgba(200, 97, 100, 100)'
const wrapper = document.createElement('div') function makeChart(innerWidth, innerHeight, autoSize=true) {
wrapper.className = 'wrapper'
document.body.appendChild(wrapper)
function makeChart(innerWidth, innerHeight, topBar=false) {
let chart = { let chart = {
markers: [], markers: [],
horizontal_lines: [], horizontal_lines: [],
@ -528,16 +589,10 @@ function makeChart(innerWidth, innerHeight, topBar=false) {
width: innerWidth, width: innerWidth,
height: innerHeight height: innerHeight
}, },
} }
let topBarOffset = 0
if (topBar) {
makeTopBar(chart)
topBarOffset = chart.topBar.offsetHeight
}
chart.chart = LightweightCharts.createChart(chart.div, { chart.chart = LightweightCharts.createChart(chart.div, {
width: window.innerWidth*innerWidth, width: window.innerWidth*innerWidth,
height: (window.innerHeight*innerHeight)-topBarOffset, height: window.innerHeight*innerHeight,
layout: { layout: {
textColor: '#d1d4dc', textColor: '#d1d4dc',
background: { background: {
@ -565,12 +620,8 @@ function makeChart(innerWidth, innerHeight, topBar=false) {
}, },
handleScroll: {vertTouchDrag: true}, handleScroll: {vertTouchDrag: true},
}) })
window.addEventListener('resize', function() { let up = 'rgba(39, 157, 130, 100)'
if (topBar) { let down = 'rgba(200, 97, 100, 100)'
topBarOffset = chart.topBar.offsetHeight
}
chart.chart.resize(window.innerWidth*innerWidth, (window.innerHeight*innerHeight)-topBarOffset)
});
chart.series = chart.chart.addCandlestickSeries({color: 'rgb(0, 120, 255)', upColor: up, borderUpColor: up, wickUpColor: up, chart.series = chart.chart.addCandlestickSeries({color: 'rgb(0, 120, 255)', upColor: up, borderUpColor: up, wickUpColor: up,
downColor: down, borderDownColor: down, wickDownColor: down, lineWidth: 2, downColor: down, borderDownColor: down, wickDownColor: down, lineWidth: 2,
}) })
@ -599,8 +650,18 @@ function makeChart(innerWidth, innerHeight, topBar=false) {
chart.div.appendChild(chart.legend) chart.div.appendChild(chart.legend)
chart.wrapper.appendChild(chart.div) chart.wrapper.appendChild(chart.div)
wrapper.append(chart.wrapper) document.getElementById('wrapper').append(chart.wrapper)
if (!autoSize) {
return chart
}
let topBarOffset = 0
window.addEventListener('resize', function() {
if ('topBar' in chart) {
topBarOffset = chart.topBar.offsetHeight
}
chart.chart.resize(window.innerWidth*innerWidth, (window.innerHeight*innerHeight)-topBarOffset)
});
return chart return chart
} }
function makeHorizontalLine(chart, price, color, width, style, axisLabelVisible, text) { function makeHorizontalLine(chart, price, color, width, style, axisLabelVisible, text) {
@ -635,12 +696,186 @@ HTML = f"""
margin: 0; margin: 0;
padding: 0; padding: 0;
overflow: hidden; overflow: hidden;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
}}
#wrapper {{
width: 100vw;
height: 100vh;
}} }}
</style> </style>
</head> </head>
<body> <body>
<div id="wrapper"></div>
<script> <script>
{SCRIPT} {SCRIPT}
</script> </script>
</body> </body>
</html>""" </html>"""
CALLBACK_SCRIPT = '''
function makeSearchBox(chart, callbackFunction) {
let searchWindow = document.createElement('div')
searchWindow.style.position = 'absolute'
searchWindow.style.top = '0'
searchWindow.style.bottom = '200px'
searchWindow.style.left = '0'
searchWindow.style.right = '0'
searchWindow.style.margin = 'auto'
searchWindow.style.width = '150px'
searchWindow.style.height = '30px'
searchWindow.style.padding = '10px'
searchWindow.style.backgroundColor = 'rgba(30, 30, 30, 0.9)'
searchWindow.style.border = '3px solid #3C434C'
searchWindow.style.zIndex = '1000'
searchWindow.style.display = 'none'
searchWindow.style.borderRadius = '5px'
let magnifyingGlass = document.createElement('span');
magnifyingGlass.style.display = 'inline-block';
magnifyingGlass.style.width = '12px';
magnifyingGlass.style.height = '12px';
magnifyingGlass.style.border = '2px solid #FFF';
magnifyingGlass.style.borderRadius = '50%';
magnifyingGlass.style.position = 'relative';
let handle = document.createElement('span');
handle.style.display = 'block';
handle.style.width = '7px';
handle.style.height = '2px';
handle.style.backgroundColor = '#FFF';
handle.style.position = 'absolute';
handle.style.top = 'calc(50% + 7px)';
handle.style.right = 'calc(50% - 11px)';
handle.style.transform = 'rotate(45deg)';
let sBox = document.createElement('input');
sBox.type = 'text';
sBox.placeholder = 'search';
sBox.style.position = 'relative';
sBox.style.display = 'inline-block';
sBox.style.zIndex = '1000';
sBox.style.textAlign = 'center'
sBox.style.width = '100px'
sBox.style.marginLeft = '15px'
sBox.style.backgroundColor = 'rgba(0, 122, 255, 0.2)'
sBox.style.color = 'lightgrey'
sBox.style.fontSize = '20px'
sBox.style.border = 'none'
sBox.style.outline = 'none'
sBox.style.borderRadius = '2px'
searchWindow.appendChild(magnifyingGlass)
magnifyingGlass.appendChild(handle)
searchWindow.appendChild(sBox)
chart.div.appendChild(searchWindow);
let yPrice = null
chart.chart.subscribeCrosshairMove((param) => {
if (param.point){
yPrice = param.point.y;
}
});
let selectedChart = false
chart.wrapper.addEventListener('mouseover', (event) => {
selectedChart = true
})
chart.wrapper.addEventListener('mouseout', (event) => {
selectedChart = false
})
document.addEventListener('keydown', function(event) {
if (!selectedChart) {return}
if (event.altKey && event.code === 'KeyH') {
let price = chart.series.coordinateToPrice(yPrice)
makeHorizontalLine(chart, price, '#FFFFFF', 1, LightweightCharts.LineStyle.Solid, true, '')
}
if (searchWindow.style.display === 'none') {
if (/^[a-zA-Z0-9]$/.test(event.key)) {
searchWindow.style.display = 'block';
sBox.focus();
}
}
else if (event.key === 'Enter') {
callbackFunction(`on_search__${chart.id}__${sBox.value}`)
searchWindow.style.display = 'none'
sBox.value = ''
}
else if (event.key === 'Escape') {
searchWindow.style.display = 'none'
sBox.value = ''
}
});
sBox.addEventListener('input', function() {
sBox.value = sBox.value.toUpperCase();
});
}
function makeSwitcher(chart, items, activeItem, callbackFunction, callbackName) {
let switcherElement = document.createElement('div');
switcherElement.style.margin = '4px 18px'
switcherElement.style.zIndex = '1000'
let intervalElements = items.map(function(item) {
let itemEl = document.createElement('button');
itemEl.style.cursor = 'pointer'
itemEl.style.padding = '3px 6px'
itemEl.style.margin = '0px 4px'
itemEl.style.fontSize = '14px'
itemEl.style.color = 'lightgrey'
itemEl.style.backgroundColor = item === activeItem ? 'rgba(0, 122, 255, 0.7)' : 'transparent'
itemEl.style.border = 'none'
itemEl.style.borderRadius = '4px'
itemEl.addEventListener('mouseenter', function() {
itemEl.style.backgroundColor = item === activeItem ? 'rgba(0, 122, 255, 0.7)' : 'rgb(19, 40, 84)'
})
itemEl.addEventListener('mouseleave', function() {
itemEl.style.backgroundColor = item === activeItem ? 'rgba(0, 122, 255, 0.7)' : 'transparent'
})
itemEl.innerText = item;
itemEl.addEventListener('click', function() {
onItemClicked(item);
});
switcherElement.appendChild(itemEl);
return itemEl;
});
function onItemClicked(item) {
if (item === activeItem) {
return;
}
intervalElements.forEach(function(element, index) {
element.style.backgroundColor = items[index] === item ? 'rgba(0, 122, 255, 0.7)' : 'transparent'
});
activeItem = item;
callbackFunction(`${callbackName}__${chart.id}__${item}`);
}
chart.topBar.appendChild(switcherElement)
makeSeperator(chart.topBar)
return switcherElement;
}
function makeTextBoxWidget(chart, text) {
let textBox = document.createElement('div')
textBox.style.margin = '0px 18px'
textBox.style.position = 'relative'
textBox.style.color = 'lightgrey'
textBox.innerText = text
chart.topBar.append(textBox)
makeSeperator(chart.topBar)
return textBox
}
function makeTopBar(chart) {
chart.topBar = document.createElement('div')
chart.topBar.style.backgroundColor = '#191B1E'
chart.topBar.style.borderBottom = '3px solid #3C434C'
chart.topBar.style.display = 'flex'
chart.topBar.style.alignItems = 'center'
chart.wrapper.prepend(chart.topBar)
}
function makeSeperator(topBar) {
let seperator = document.createElement('div')
seperator.style.width = '1px'
seperator.style.height = '20px'
seperator.style.backgroundColor = '#3C434C'
topBar.appendChild(seperator)
}
'''

View File

@ -1,3 +1,6 @@
import asyncio
from inspect import iscoroutinefunction
try: try:
import wx.html2 import wx.html2
except ImportError: except ImportError:
@ -15,16 +18,18 @@ try:
@pyqtSlot(str) @pyqtSlot(str)
def callback(self, message): def callback(self, message):
_widget_message(self.chart, message) _widget_message(self.chart, message)
except ImportError: except ImportError:
pass pass
try: try:
from streamlit.components.v1 import html from streamlit.components.v1 import html
except ImportError: except ImportError:
pass pass
try:
from IPython.display import HTML, display
except ImportError:
pass
from lightweight_charts.chartasync import LWCAsync, ASYNC_SCRIPT from lightweight_charts.js import LWC, TopBar, CALLBACK_SCRIPT
from lightweight_charts.js import LWC
def _widget_message(chart, string): def _widget_message(chart, string):
@ -32,12 +37,13 @@ def _widget_message(chart, string):
name, chart_id = messages[:2] name, chart_id = messages[:2]
args = messages[2:] args = messages[2:]
chart.api.chart = chart._charts[chart_id] chart.api.chart = chart._charts[chart_id]
getattr(chart.api, name)(*args) method = getattr(chart.api, name)
asyncio.create_task(getattr(chart.api, name)(*args)) if iscoroutinefunction(method) else method(*args)
class WxChart(LWCAsync): class WxChart(LWC):
def __init__(self, parent, api: object = None, top_bar: bool = False, search_box: bool = False, def __init__(self, parent, volume_enabled: bool = True, inner_width: float = 1.0, inner_height: float = 1.0,
volume_enabled: bool = True, inner_width: float = 1.0, inner_height: float = 1.0): api: object = None, topbar: bool = False, searchbox: bool = False):
try: try:
self.webview: wx.html2.WebView = wx.html2.WebView.New(parent) self.webview: wx.html2.WebView = wx.html2.WebView.New(parent)
except NameError: except NameError:
@ -48,20 +54,21 @@ class WxChart(LWCAsync):
self._script_func = self.webview.RunScript self._script_func = self.webview.RunScript
self._js_api_code = 'window.wx_msg.postMessage.bind(window.wx_msg)' self._js_api_code = 'window.wx_msg.postMessage.bind(window.wx_msg)'
self.webview.Bind(wx.html2.EVT_WEBVIEW_LOADED, lambda e: wx.CallLater(200, self._on_js_load)) self.webview.Bind(wx.html2.EVT_WEBVIEW_LOADED, lambda e: wx.CallLater(500, self._on_js_load))
self.webview.Bind(wx.html2.EVT_WEBVIEW_SCRIPT_MESSAGE_RECEIVED, lambda e: _widget_message(self, e.GetString())) self.webview.Bind(wx.html2.EVT_WEBVIEW_SCRIPT_MESSAGE_RECEIVED, lambda e: _widget_message(self, e.GetString()))
self.webview.AddScriptMessageHandler('wx_msg') self.webview.AddScriptMessageHandler('wx_msg')
self.webview.SetPage(self._html, '') self.webview.SetPage(self._html, '')
self.webview.AddUserScript(ASYNC_SCRIPT)
self._create_chart(top_bar) self.webview.AddUserScript(CALLBACK_SCRIPT)
self._make_search_box() if search_box else None self._create_chart()
self.topbar = TopBar(self) if topbar else None
self._make_search_box() if searchbox else None
def get_webview(self): return self.webview def get_webview(self): return self.webview
class QtChart(LWCAsync): class QtChart(LWC):
def __init__(self, widget=None, api: object = None, top_bar: bool = False, search_box: bool = False, def __init__(self, widget=None, api: object = None, topbar: bool = False, searchbox: bool = False,
volume_enabled: bool = True, inner_width: float = 1.0, inner_height: float = 1.0): volume_enabled: bool = True, inner_width: float = 1.0, inner_height: float = 1.0):
try: try:
self.webview = QWebEngineView(widget) self.webview = QWebEngineView(widget)
@ -89,22 +96,21 @@ class QtChart(LWCAsync):
{self._html[85:]} {self._html[85:]}
''' '''
self.webview.page().setHtml(self._html) self.webview.page().setHtml(self._html)
self.run_script(ASYNC_SCRIPT)
self._create_chart(top_bar) self.run_script(CALLBACK_SCRIPT)
self._make_search_box() if search_box else None self._create_chart()
self.topbar = TopBar(self) if topbar else None
self._make_search_box() if searchbox else None
def get_webview(self): return self.webview def get_webview(self): return self.webview
class StreamlitChart(LWC): class StaticLWC(LWC):
def __init__(self, volume_enabled=True, width=None, height=None, inner_width=1, inner_height=1): def __init__(self, volume_enabled=True, width=None, height=None, inner_width=1, inner_height=1):
super().__init__(volume_enabled, inner_width, inner_height) super().__init__(volume_enabled, inner_width, inner_height)
self.width = width self.width = width
self.height = height self.height = height
self._html = self._html.replace('</script>\n</body>\n</html>', '') self._html = self._html.replace('</script>\n</body>\n</html>', '')
self._create_chart()
def run_script(self, script): self._html += '\n' + script def run_script(self, script): self._html += '\n' + script
@ -112,8 +118,44 @@ class StreamlitChart(LWC):
if self.loaded: if self.loaded:
return return
self.loaded = True self.loaded = True
self._load()
def _load(self): pass
class StreamlitChart(StaticLWC):
def __init__(self, volume_enabled=True, width=None, height=None, inner_width=1, inner_height=1):
super().__init__(volume_enabled, width, height, inner_width, inner_height)
self._create_chart()
def _load(self):
try: try:
html(f'{self._html}</script></body></html>', width=self.width, height=self.height) html(f'{self._html}</script></body></html>', width=self.width, height=self.height)
except NameError: except NameError:
raise ModuleNotFoundError('streamlit.components.v1.html was not found, and must be installed to use StreamlitChart.') raise ModuleNotFoundError('streamlit.components.v1.html was not found, and must be installed to use StreamlitChart.')
class JupyterChart(StaticLWC):
def __init__(self, volume_enabled=True, width=800, height=350, inner_width=1, inner_height=1):
super().__init__(volume_enabled, width, height, inner_width, inner_height)
self._position = ""
self._create_chart(autosize=False)
self.run_script(f'''
for (var i = 0; i < document.getElementsByClassName("tv-lightweight-charts").length; i++) {{
var element = document.getElementsByClassName("tv-lightweight-charts")[i];
element.style.overflow = "visible"
}}
document.getElementById('wrapper').style.overflow = 'hidden'
document.getElementById('wrapper').style.borderRadius = '10px'
document.getElementById('wrapper').style.width = '{self.width}px'
document.getElementById('wrapper').style.height = '100%'
''')
self.run_script(f'{self.id}.chart.resize({width}, {height})')
def _load(self):
try:
display(HTML(f'{self._html}</script></body></html>'))
except NameError:
raise ModuleNotFoundError('IPython.display.HTML was not found, and must be installed to use JupyterChart.')

View File

@ -5,7 +5,7 @@ with open('README.md', 'r', encoding='utf-8') as f:
setup( setup(
name='lightweight_charts', name='lightweight_charts',
version='1.0.9', version='1.0.10',
packages=find_packages(), packages=find_packages(),
python_requires='>=3.9', python_requires='>=3.9',
install_requires=[ install_requires=[