diff --git a/README.md b/README.md
index d9c1a35..0949135 100644
--- a/README.md
+++ b/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/).
+
## 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)).
___
@@ -125,27 +127,27 @@ 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])
- result.append({'time': data.iloc[i]['date'], 'value': val})
- return pd.DataFrame(result)
+ 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])
+ result.append({'time': data.iloc[i]['date'], 'value': val})
+ return pd.DataFrame(result)
if __name__ == '__main__':
-
- chart = Chart()
-
- df = pd.read_csv('ohlcv.csv')
- chart.set(df)
-
- line = chart.create_line()
- sma_data = calculate_sma(df)
- line.set(sma_data)
-
- chart.show(block=True)
+ chart = Chart()
+
+ df = pd.read_csv('ohlcv.csv')
+ chart.set(df)
+
+ line = chart.create_line()
+ sma_data = calculate_sma(df)
+ 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())
```
-
+
___
diff --git a/cover.png b/cover.png
index 51cc215..291ef1b 100644
Binary files a/cover.png and b/cover.png differ
diff --git a/docs/source/conf.py b/docs/source/conf.py
index b0e782f..1ff9f68 100644
--- a/docs/source/conf.py
+++ b/docs/source/conf.py
@@ -1,7 +1,7 @@
project = 'lightweight-charts-python'
copyright = '2023, louisnw'
author = 'louisnw'
-release = '1.0.9'
+release = '1.0.10'
extensions = ["myst_parser"]
diff --git a/docs/source/docs.md b/docs/source/docs.md
index 07f4dc3..84a75db 100644
--- a/docs/source/docs.md
+++ b/docs/source/docs.md
@@ -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,34 +286,33 @@ 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)
chart2.time_scale(visible=False)
-
+
df = pd.read_csv('ohlcv.csv')
df2 = pd.read_csv('rsi.csv')
-
+
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.
`), 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()
+ async def on_search(self, searched_string): # Called when the user searches.
+ 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.set(new_data) # sets data for the Chart or SubChart in question.
+ 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()
+```
diff --git a/docs/source/index.md b/docs/source/index.md
index 4eff9bb..d4aba20 100644
--- a/docs/source/index.md
+++ b/docs/source/index.md
@@ -1,7 +1,7 @@
```{toctree}
:hidden:
:caption: Contents
-:maxdepth: 2
+:maxdepth: 3
docs
Github Repository
diff --git a/examples/2_live_data/live_data.gif b/examples/2_live_data/live_data.gif
index 9eb7e9d..500b3cf 100644
Binary files a/examples/2_live_data/live_data.gif and b/examples/2_live_data/live_data.gif differ
diff --git a/examples/3_tick_data/tick_data.gif b/examples/3_tick_data/tick_data.gif
index f18c83f..60d358e 100644
Binary files a/examples/3_tick_data/tick_data.gif and b/examples/3_tick_data/tick_data.gif differ
diff --git a/examples/6_async/async.gif b/examples/6_async/async.gif
deleted file mode 100644
index 6f68f3c..0000000
Binary files a/examples/6_async/async.gif and /dev/null differ
diff --git a/examples/6_async/async.png b/examples/6_async/async.png
deleted file mode 100644
index a58c41d..0000000
Binary files a/examples/6_async/async.png and /dev/null differ
diff --git a/examples/6_async/async.py b/examples/6_async/async.py
deleted file mode 100644
index 1d04c2d..0000000
--- a/examples/6_async/async.py
+++ /dev/null
@@ -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())
diff --git a/examples/6_async/bar_data/AAPL_1min.csv b/examples/6_callbacks/bar_data/AAPL_1min.csv
similarity index 100%
rename from examples/6_async/bar_data/AAPL_1min.csv
rename to examples/6_callbacks/bar_data/AAPL_1min.csv
diff --git a/examples/6_async/bar_data/AAPL_30min.csv b/examples/6_callbacks/bar_data/AAPL_30min.csv
similarity index 100%
rename from examples/6_async/bar_data/AAPL_30min.csv
rename to examples/6_callbacks/bar_data/AAPL_30min.csv
diff --git a/examples/6_async/bar_data/AAPL_5min.csv b/examples/6_callbacks/bar_data/AAPL_5min.csv
similarity index 100%
rename from examples/6_async/bar_data/AAPL_5min.csv
rename to examples/6_callbacks/bar_data/AAPL_5min.csv
diff --git a/examples/6_async/bar_data/GOOGL_1min.csv b/examples/6_callbacks/bar_data/GOOGL_1min.csv
similarity index 100%
rename from examples/6_async/bar_data/GOOGL_1min.csv
rename to examples/6_callbacks/bar_data/GOOGL_1min.csv
diff --git a/examples/6_async/bar_data/GOOGL_30min.csv b/examples/6_callbacks/bar_data/GOOGL_30min.csv
similarity index 100%
rename from examples/6_async/bar_data/GOOGL_30min.csv
rename to examples/6_callbacks/bar_data/GOOGL_30min.csv
diff --git a/examples/6_async/bar_data/GOOGL_5min.csv b/examples/6_callbacks/bar_data/GOOGL_5min.csv
similarity index 100%
rename from examples/6_async/bar_data/GOOGL_5min.csv
rename to examples/6_callbacks/bar_data/GOOGL_5min.csv
diff --git a/examples/6_async/bar_data/TSLA_1min.csv b/examples/6_callbacks/bar_data/TSLA_1min.csv
similarity index 100%
rename from examples/6_async/bar_data/TSLA_1min.csv
rename to examples/6_callbacks/bar_data/TSLA_1min.csv
diff --git a/examples/6_async/bar_data/TSLA_30min.csv b/examples/6_callbacks/bar_data/TSLA_30min.csv
similarity index 100%
rename from examples/6_async/bar_data/TSLA_30min.csv
rename to examples/6_callbacks/bar_data/TSLA_30min.csv
diff --git a/examples/6_async/bar_data/TSLA_5min.csv b/examples/6_callbacks/bar_data/TSLA_5min.csv
similarity index 100%
rename from examples/6_async/bar_data/TSLA_5min.csv
rename to examples/6_callbacks/bar_data/TSLA_5min.csv
diff --git a/examples/6_callbacks/callbacks.gif b/examples/6_callbacks/callbacks.gif
new file mode 100644
index 0000000..8aac4bc
Binary files /dev/null and b/examples/6_callbacks/callbacks.gif differ
diff --git a/examples/6_callbacks/callbacks.py b/examples/6_callbacks/callbacks.py
new file mode 100644
index 0000000..a27e0ba
--- /dev/null
+++ b/examples/6_callbacks/callbacks.py
@@ -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())
diff --git a/lightweight_charts/__init__.py b/lightweight_charts/__init__.py
index ea976cb..12a3ade 100644
--- a/lightweight_charts/__init__.py
+++ b/lightweight_charts/__init__.py
@@ -1,5 +1,3 @@
from .chart import Chart
from .js import LWC
-from .chartasync import ChartAsync
-
-
+from .widgets import JupyterChart
diff --git a/lightweight_charts/chart.py b/lightweight_charts/chart.py
index d3ec87f..d4f15ed 100644
--- a/lightweight_charts/chart.py
+++ b/lightweight_charts/chart.py
@@ -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
diff --git a/lightweight_charts/chartasync.py b/lightweight_charts/chartasync.py
deleted file mode 100644
index 5217584..0000000
--- a/lightweight_charts/chartasync.py
+++ /dev/null
@@ -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)
- }
-'''
diff --git a/lightweight_charts/js.py b/lightweight_charts/js.py
index 6b62828..b2725b8 100644
--- a/lightweight_charts/js.py
+++ b/lightweight_charts/js.py
@@ -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: [],
@@ -528,16 +589,10 @@ function makeChart(innerWidth, innerHeight, topBar=false) {
width: innerWidth,
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;
}}
+