- 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.
56
README.md
@ -7,16 +7,17 @@
|
||||
[](https://github.com/louisnw01/lightweight-charts-python/blob/main/LICENSE)
|
||||
[](https://lightweight-charts-python.readthedocs.io/en/latest/docs.html)
|
||||
|
||||

|
||||

|
||||
|
||||
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>
|
||||
|
||||
|
||||
## Installation
|
||||
```
|
||||
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
|
||||
@ -28,7 +29,8 @@ ___
|
||||
* wxPython
|
||||
* Streamlit
|
||||
* 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)).
|
||||
___
|
||||
|
||||
@ -127,6 +129,7 @@ from lightweight_charts import Chart
|
||||
def calculate_sma(data: pd.DataFrame, period: int = 50):
|
||||
def avg(d: pd.DataFrame):
|
||||
return d['close'].mean()
|
||||
|
||||
result = []
|
||||
for i in range(period - 1, len(data)):
|
||||
val = avg(data.iloc[i - period + 1:i])
|
||||
@ -135,7 +138,6 @@ def calculate_sma(data: pd.DataFrame, period: int = 50):
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
chart = Chart()
|
||||
|
||||
df = pd.read_csv('ohlcv.csv')
|
||||
@ -143,7 +145,7 @@ if __name__ == '__main__':
|
||||
|
||||
line = chart.create_line()
|
||||
sma_data = calculate_sma(df)
|
||||
line.set(sma_data)
|
||||
line._set(sma_data)
|
||||
|
||||
chart.show(block=True)
|
||||
|
||||
@ -188,68 +190,60 @@ if __name__ == '__main__':
|
||||

|
||||
___
|
||||
|
||||
### 6. ChartAsync:
|
||||
### 6. Callbacks:
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
import pandas as pd
|
||||
|
||||
from lightweight_charts import ChartAsync
|
||||
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.
|
||||
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()
|
||||
new_data = get_bar_data(searched_string, self.chart.topbar['timeframe'].value)
|
||||
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:
|
||||
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 = Chart(api=api, topbar=True, searchbox=True)
|
||||
chart.legend(True)
|
||||
|
||||
chart.create_switcher(api.on_timeframe_selection, '1min', '5min', '30min', default='5min')
|
||||
chart.corner_text(api.symbol)
|
||||
chart.topbar.textbox('corner', 'TSLA')
|
||||
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)
|
||||
|
||||
await chart.show(block=True)
|
||||
await chart.show_async(block=True)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
|
||||
```
|
||||

|
||||

|
||||
___
|
||||
|
||||
<div align="center">
|
||||
|
||||
BIN
cover.png
|
Before Width: | Height: | Size: 161 KiB After Width: | Height: | Size: 279 KiB |
@ -1,7 +1,7 @@
|
||||
project = 'lightweight-charts-python'
|
||||
copyright = '2023, louisnw'
|
||||
author = 'louisnw'
|
||||
release = '1.0.9'
|
||||
release = '1.0.10'
|
||||
|
||||
extensions = ["myst_parser"]
|
||||
|
||||
|
||||
@ -9,7 +9,7 @@
|
||||
___
|
||||
|
||||
## 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`
|
||||
@ -186,7 +186,8 @@ ___
|
||||
|
||||
|
||||
## 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.
|
||||
___
|
||||
@ -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
|
||||
|
||||
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:
|
||||
|
||||
```python
|
||||
import pandas as pd
|
||||
from lightweight_charts import Chart
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
chart = Chart(inner_width=0.5, inner_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')
|
||||
chart.set(df)
|
||||
chart2.set(df)
|
||||
chart3.set(df)
|
||||
chart4.set(df)
|
||||
chart2._set(df)
|
||||
chart3._set(df)
|
||||
chart4._set(df)
|
||||
|
||||
chart.show(block=True)
|
||||
|
||||
@ -278,7 +286,6 @@ import pandas as pd
|
||||
from lightweight_charts import Chart
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
chart = Chart(inner_width=1, inner_height=0.8)
|
||||
|
||||
chart2 = chart.create_subchart(width=1, height=0.2, sync=True, volume_enabled=False)
|
||||
@ -289,23 +296,23 @@ if __name__ == '__main__':
|
||||
|
||||
chart.set(df)
|
||||
line = chart2.create_line()
|
||||
line.set(df2)
|
||||
line._set(df2)
|
||||
|
||||
chart.show(block=True)
|
||||
|
||||
```
|
||||
___
|
||||
|
||||
## ChartAsync
|
||||
`api: object` | `top_bar: bool` | `search_box: bool`
|
||||
## Callbacks
|
||||
|
||||
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)).
|
||||
* `top_bar`: Adds a Top Bar 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.
|
||||
* `topbar`: Adds a [TopBar](#topbar) to the `Chart` or `SubChart` and allows use of the `create_switcher` method.
|
||||
* `searchbox`: Adds a search box onto the `Chart` or `SubChart` that is activated by typing.
|
||||
|
||||
___
|
||||
### How to use Callbacks
|
||||
@ -320,75 +327,99 @@ class API:
|
||||
self.chart = None
|
||||
|
||||
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.
|
||||
This allows access to the specific [Common Methods](#common-methods) for the pane in question.
|
||||
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.
|
||||
`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`
|
||||
```
|
||||
___
|
||||
|
||||
### `create_switcher`
|
||||
`method: function` | `*options: str` | `default: str`
|
||||
### `TopBar`
|
||||
The `TopBar` class represents the top bar shown on the chart when using callbacks:
|
||||
|
||||

|
||||
|
||||
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.
|
||||
* `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.
|
||||
___
|
||||
|
||||
### 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
|
||||
import asyncio
|
||||
import pandas as pd
|
||||
from my_favorite_broker import get_bar_data
|
||||
|
||||
from lightweight_charts import ChartAsync
|
||||
from lightweight_charts import Chart
|
||||
|
||||
|
||||
class API:
|
||||
def __init__(self):
|
||||
self.chart = None
|
||||
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()
|
||||
timeframe = self.chart.topbar['timeframe'].value
|
||||
new_data = await get_bar_data(searched_string, timeframe)
|
||||
if not new_data:
|
||||
return
|
||||
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.
|
||||
self.timeframe = timeframe
|
||||
new_data = await self.get_data()
|
||||
async def on_timeframe(self): # Called when the user changes the timeframe.
|
||||
timeframe = self.chart.topbar['timeframe'].value
|
||||
symbol = self.chart.topbar['symbol'].value
|
||||
new_data = await get_bar_data(symbol, timeframe)
|
||||
if not new_data:
|
||||
return
|
||||
self.chart.set(new_data)
|
||||
|
||||
async def get_data(self):
|
||||
data = await get_bar_data(self.symbol, self.timeframe)
|
||||
return data
|
||||
|
||||
|
||||
async def main():
|
||||
api = API()
|
||||
|
||||
chart = ChartAsync(api=api, debug=True)
|
||||
chart = Chart(api=api, topbar=True, searchbox=True)
|
||||
|
||||
chart.corner_text('TSLA')
|
||||
chart.create_switcher(api.on_timeframe, '1min', '5min', '30min', 'H', 'D', 'W', default='5min')
|
||||
chart.topbar.textbox('symbol', 'TSLA')
|
||||
chart.topbar.switcher('timeframe', api.on_timeframe, '1min', '5min', '30min', 'H', 'D', 'W', default='5min')
|
||||
|
||||
df = pd.read_csv('ohlcv.csv')
|
||||
chart.set(df)
|
||||
|
||||
await chart.show(block=True)
|
||||
await chart.show_async(block=True)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
@ -517,3 +548,29 @@ chart.set(df)
|
||||
|
||||
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()
|
||||
```
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
```{toctree}
|
||||
:hidden:
|
||||
:caption: Contents
|
||||
:maxdepth: 2
|
||||
:maxdepth: 3
|
||||
|
||||
docs
|
||||
Github Repository <https://github.com/louisnw01/lightweight-charts-python>
|
||||
|
||||
|
Before Width: | Height: | Size: 6.6 MiB After Width: | Height: | Size: 5.0 MiB |
|
Before Width: | Height: | Size: 712 KiB After Width: | Height: | Size: 349 KiB |
|
Before Width: | Height: | Size: 2.7 MiB |
|
Before Width: | Height: | Size: 504 KiB |
@ -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())
|
||||
BIN
examples/6_callbacks/callbacks.gif
Normal file
|
After Width: | Height: | Size: 475 KiB |
48
examples/6_callbacks/callbacks.py
Normal 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())
|
||||
@ -1,5 +1,3 @@
|
||||
from .chart import Chart
|
||||
from .js import LWC
|
||||
from .chartasync import ChartAsync
|
||||
|
||||
|
||||
from .widgets import JupyterChart
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
import asyncio
|
||||
import webview
|
||||
import multiprocessing as mp
|
||||
|
||||
from lightweight_charts.js import LWC
|
||||
from lightweight_charts.js import LWC, CALLBACK_SCRIPT, TopBar
|
||||
|
||||
|
||||
class CallbackAPI:
|
||||
@ -15,12 +16,12 @@ class CallbackAPI:
|
||||
|
||||
|
||||
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.exit = exit
|
||||
self.loaded = loaded
|
||||
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,
|
||||
x=x, y=y, background_color='#000000')
|
||||
self.webview.events.loaded += self.on_js_load
|
||||
@ -44,18 +45,27 @@ class PyWV:
|
||||
|
||||
class Chart(LWC):
|
||||
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):
|
||||
super().__init__(volume_enabled, inner_width, inner_height, dynamic_loading)
|
||||
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,), daemon=True)
|
||||
width, height, x, y, on_top, debug, self._emit), daemon=True)
|
||||
self._process.start()
|
||||
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):
|
||||
"""
|
||||
Shows the chart window.\n
|
||||
@ -74,6 +84,31 @@ class Chart(LWC):
|
||||
return
|
||||
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):
|
||||
"""
|
||||
Hides the chart window.\n
|
||||
|
||||
@ -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)
|
||||
}
|
||||
'''
|
||||
@ -1,6 +1,6 @@
|
||||
import pandas as pd
|
||||
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.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]
|
||||
|
||||
def _df_datetime_format(self, df: pd.DataFrame):
|
||||
df = df.copy()
|
||||
if 'date' in df.columns:
|
||||
df = df.rename(columns={'date': 'time'})
|
||||
self._set_interval(df)
|
||||
# df['time'] = df['time'].apply(self._datetime_format)
|
||||
df['time'] = self._datetime_format(df['time'])
|
||||
return df
|
||||
|
||||
def _series_datetime_format(self, series):
|
||||
series = series.copy()
|
||||
if 'date' in series.keys():
|
||||
series = series.rename({'date': '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()})')
|
||||
|
||||
|
||||
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):
|
||||
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
|
||||
@ -153,24 +205,31 @@ class LWC(SeriesCommon):
|
||||
self._script_func = None
|
||||
self._last_bar = None
|
||||
self._interval = None
|
||||
self._charts = {self.id: self}
|
||||
self._js_api_code = None
|
||||
|
||||
self._background_color = '#000000'
|
||||
self._volume_up_color = 'rgba(83,141,131,0.8)'
|
||||
self._volume_down_color = 'rgba(200,127,130,0.8)'
|
||||
|
||||
# self.polygon: PolygonAPI = PolygonAPI(self)
|
||||
|
||||
def _on_js_load(self):
|
||||
if self.loaded:
|
||||
return
|
||||
self.loaded = True
|
||||
[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.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}.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):
|
||||
"""
|
||||
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.run_script(f"""
|
||||
document.body.style.backgroundColor = '{self._background_color}'
|
||||
document.getElementById('wrapper').style.backgroundColor = '{self._background_color}'
|
||||
{self.id}.chart.applyOptions({{
|
||||
layout: {{
|
||||
background: {{
|
||||
@ -484,20 +543,28 @@ class LWC(SeriesCommon):
|
||||
}});''')
|
||||
|
||||
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):
|
||||
return SubChart(self, volume_enabled, position, width, height, sync)
|
||||
width: float = 0.5, height: float = 0.5, sync: Union[bool, str] = False,
|
||||
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):
|
||||
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)
|
||||
self._chart = parent._chart if isinstance(parent, SubChart) 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.id = f'window.{self._rand.generate()}'
|
||||
|
||||
self._create_chart()
|
||||
self.topbar = TopBar(self) if topbar else None
|
||||
self._make_search_box() if searchbox else None
|
||||
if not sync:
|
||||
return
|
||||
sync_parent_var = self._parent.id if isinstance(sync, bool) else sync
|
||||
@ -509,15 +576,9 @@ class SubChart(LWC):
|
||||
|
||||
|
||||
SCRIPT = """
|
||||
document.body.style.backgroundColor = '#000000'
|
||||
const up = 'rgba(39, 157, 130, 100)'
|
||||
const down = 'rgba(200, 97, 100, 100)'
|
||||
document.getElementById('wrapper').style.backgroundColor = '#000000'
|
||||
|
||||
const wrapper = document.createElement('div')
|
||||
wrapper.className = 'wrapper'
|
||||
document.body.appendChild(wrapper)
|
||||
|
||||
function makeChart(innerWidth, innerHeight, topBar=false) {
|
||||
function makeChart(innerWidth, innerHeight, autoSize=true) {
|
||||
let chart = {
|
||||
markers: [],
|
||||
horizontal_lines: [],
|
||||
@ -529,15 +590,9 @@ function makeChart(innerWidth, innerHeight, topBar=false) {
|
||||
height: innerHeight
|
||||
},
|
||||
}
|
||||
let topBarOffset = 0
|
||||
if (topBar) {
|
||||
makeTopBar(chart)
|
||||
topBarOffset = chart.topBar.offsetHeight
|
||||
}
|
||||
|
||||
chart.chart = LightweightCharts.createChart(chart.div, {
|
||||
width: window.innerWidth*innerWidth,
|
||||
height: (window.innerHeight*innerHeight)-topBarOffset,
|
||||
height: window.innerHeight*innerHeight,
|
||||
layout: {
|
||||
textColor: '#d1d4dc',
|
||||
background: {
|
||||
@ -565,12 +620,8 @@ function makeChart(innerWidth, innerHeight, topBar=false) {
|
||||
},
|
||||
handleScroll: {vertTouchDrag: true},
|
||||
})
|
||||
window.addEventListener('resize', function() {
|
||||
if (topBar) {
|
||||
topBarOffset = chart.topBar.offsetHeight
|
||||
}
|
||||
chart.chart.resize(window.innerWidth*innerWidth, (window.innerHeight*innerHeight)-topBarOffset)
|
||||
});
|
||||
let up = 'rgba(39, 157, 130, 100)'
|
||||
let down = 'rgba(200, 97, 100, 100)'
|
||||
chart.series = chart.chart.addCandlestickSeries({color: 'rgb(0, 120, 255)', upColor: up, borderUpColor: up, wickUpColor: up,
|
||||
downColor: down, borderDownColor: down, wickDownColor: down, lineWidth: 2,
|
||||
})
|
||||
@ -599,8 +650,18 @@ function makeChart(innerWidth, innerHeight, topBar=false) {
|
||||
|
||||
chart.div.appendChild(chart.legend)
|
||||
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
|
||||
}
|
||||
function makeHorizontalLine(chart, price, color, width, style, axisLabelVisible, text) {
|
||||
@ -635,12 +696,186 @@ HTML = f"""
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
|
||||
}}
|
||||
#wrapper {{
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="wrapper"></div>
|
||||
<script>
|
||||
{SCRIPT}
|
||||
</script>
|
||||
</body>
|
||||
</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)
|
||||
}
|
||||
'''
|
||||
|
||||
|
||||
@ -1,3 +1,6 @@
|
||||
import asyncio
|
||||
from inspect import iscoroutinefunction
|
||||
|
||||
try:
|
||||
import wx.html2
|
||||
except ImportError:
|
||||
@ -15,16 +18,18 @@ try:
|
||||
@pyqtSlot(str)
|
||||
def callback(self, message):
|
||||
_widget_message(self.chart, message)
|
||||
|
||||
except ImportError:
|
||||
pass
|
||||
try:
|
||||
from streamlit.components.v1 import html
|
||||
except ImportError:
|
||||
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
|
||||
from lightweight_charts.js import LWC, TopBar, CALLBACK_SCRIPT
|
||||
|
||||
|
||||
def _widget_message(chart, string):
|
||||
@ -32,12 +37,13 @@ def _widget_message(chart, string):
|
||||
name, chart_id = messages[:2]
|
||||
args = messages[2:]
|
||||
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):
|
||||
def __init__(self, parent, api: object = None, top_bar: bool = False, search_box: bool = False,
|
||||
volume_enabled: bool = True, inner_width: float = 1.0, inner_height: float = 1.0):
|
||||
class WxChart(LWC):
|
||||
def __init__(self, parent, volume_enabled: bool = True, inner_width: float = 1.0, inner_height: float = 1.0,
|
||||
api: object = None, topbar: bool = False, searchbox: bool = False):
|
||||
try:
|
||||
self.webview: wx.html2.WebView = wx.html2.WebView.New(parent)
|
||||
except NameError:
|
||||
@ -48,20 +54,21 @@ class WxChart(LWCAsync):
|
||||
self._script_func = self.webview.RunScript
|
||||
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.AddScriptMessageHandler('wx_msg')
|
||||
|
||||
self.webview.SetPage(self._html, '')
|
||||
self.webview.AddUserScript(ASYNC_SCRIPT)
|
||||
self._create_chart(top_bar)
|
||||
self._make_search_box() if search_box else None
|
||||
|
||||
self.webview.AddUserScript(CALLBACK_SCRIPT)
|
||||
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
|
||||
|
||||
|
||||
class QtChart(LWCAsync):
|
||||
def __init__(self, widget=None, api: object = None, top_bar: bool = False, search_box: bool = False,
|
||||
class QtChart(LWC):
|
||||
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):
|
||||
try:
|
||||
self.webview = QWebEngineView(widget)
|
||||
@ -89,22 +96,21 @@ class QtChart(LWCAsync):
|
||||
{self._html[85:]}
|
||||
'''
|
||||
self.webview.page().setHtml(self._html)
|
||||
self.run_script(ASYNC_SCRIPT)
|
||||
self._create_chart(top_bar)
|
||||
self._make_search_box() if search_box else None
|
||||
|
||||
self.run_script(CALLBACK_SCRIPT)
|
||||
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
|
||||
|
||||
|
||||
class StreamlitChart(LWC):
|
||||
class StaticLWC(LWC):
|
||||
def __init__(self, volume_enabled=True, width=None, height=None, inner_width=1, inner_height=1):
|
||||
super().__init__(volume_enabled, inner_width, inner_height)
|
||||
|
||||
self.width = width
|
||||
self.height = height
|
||||
|
||||
self._html = self._html.replace('</script>\n</body>\n</html>', '')
|
||||
self._create_chart()
|
||||
|
||||
def run_script(self, script): self._html += '\n' + script
|
||||
|
||||
@ -112,8 +118,44 @@ class StreamlitChart(LWC):
|
||||
if self.loaded:
|
||||
return
|
||||
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:
|
||||
html(f'{self._html}</script></body></html>', width=self.width, height=self.height)
|
||||
except NameError:
|
||||
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.')
|
||||
|
||||
|
||||