- Added the chart.spinner method, which when set to `True` shows a loading spinner on the chart (a nice visual for API calls, large datasets etc). - If an empty data frame is passed to set (eg.`chart.set(pd.DataFrame())`) the volume series and candle series will be cleared. - added the `cumulative_volume` parameter to `update_from_tick`, which adds the given volume tick onto the latest bar. - Added `vert_visible` and `horz_visible` parameters to `crosshair`. - Small style improvements to the searchbox and topbar. - Fixed a bug preventing callbacks within `WxChart` and `QtChart` Thanks to @emma-uw for the following fixes and enhancements! - Methods `hide_data`, `show_data` and `price_line` can be used within Charts, Subcharts and Lines to change the visibility of data, price lines, and the price line labels. - Added the `delete` method to Line, which irreversably deletes the Line on the chart as well as its objects in JavaScript and Python. - Added the `lines` common method, which returns a list of all Line objects for the chart. - Added the `fit` method to the common methods, which uses the `fitContent()` method from Lightweight Charts. - Fixed a big which caused synced SubCharts to be out of sync upon loading. BETA: Polygon.io integration - Added the `PolygonChart` and `polygon` method, allowing for direct integration of polygon.io’s API. - This feature is still in beta, and there will be a full announcement and update once the feature is complete!
142 lines
5.0 KiB
Python
142 lines
5.0 KiB
Python
import asyncio
|
|
import time
|
|
import multiprocessing as mp
|
|
import webview
|
|
|
|
from lightweight_charts.js import LWC, CALLBACK_SCRIPT, TopBar
|
|
|
|
|
|
class CallbackAPI:
|
|
def __init__(self, emit): self.emit = emit
|
|
|
|
def callback(self, message: str):
|
|
messages = message.split('__')
|
|
name, chart_id = messages[:2]
|
|
args = messages[2:]
|
|
self.emit.put((name, chart_id, *args))
|
|
|
|
|
|
class PyWV:
|
|
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)
|
|
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
|
|
self.loop()
|
|
|
|
def loop(self):
|
|
while 1:
|
|
arg = self.queue.get()
|
|
if arg in ('start', 'show', 'hide', 'exit'):
|
|
webview.start(debug=self.debug) if arg == 'start' else getattr(self.webview, arg)()
|
|
self.exit.set() if arg in ('start', 'exit') else None
|
|
else:
|
|
try:
|
|
self.webview.evaluate_js(arg)
|
|
except KeyError:
|
|
return
|
|
|
|
def on_js_load(self):
|
|
self.loaded.set(), self.loop()
|
|
|
|
|
|
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, 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, 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.run_script(f'makeSpinner({self.id})')
|
|
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
|
|
:param block: blocks execution until the chart is closed.
|
|
"""
|
|
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 not self._exit.is_set() and self.polygon._q.empty():
|
|
time.sleep(0.05)
|
|
continue
|
|
if self._exit.is_set():
|
|
self._exit.clear()
|
|
return
|
|
value = self.polygon._q.get_nowait()
|
|
func, args = value[0], value[1:]
|
|
func(*args)
|
|
except KeyboardInterrupt:
|
|
return
|
|
|
|
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() and self.polygon._q.empty():
|
|
await asyncio.sleep(0.05)
|
|
if self._exit.is_set():
|
|
self._exit.clear()
|
|
return
|
|
elif not self._emit.empty():
|
|
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)
|
|
continue
|
|
value = self.polygon._q.get()
|
|
func, args = value[0], value[1:]
|
|
func(*args)
|
|
except KeyboardInterrupt:
|
|
return
|
|
asyncio.create_task(self.show_async(block=True))
|
|
|
|
def hide(self):
|
|
"""
|
|
Hides the chart window.\n
|
|
"""
|
|
self._q.put('hide')
|
|
|
|
def exit(self):
|
|
"""
|
|
Exits and destroys the chart window.\n
|
|
"""
|
|
self._q.put('exit')
|
|
self._exit.wait()
|
|
self._process.terminate()
|
|
del self
|