diff --git a/lightweight_charts/js.py b/lightweight_charts/js.py index 7c1657e..5b79272 100644 --- a/lightweight_charts/js.py +++ b/lightweight_charts/js.py @@ -5,7 +5,7 @@ from typing import Dict, Union from lightweight_charts.pkg import LWC_3_5_0 from lightweight_charts.util import LINE_TYPE, POSITION, SHAPE, CROSSHAIR_MODE, _crosshair_mode, _line_type, \ - MissingColumn, _js_bool, _price_scale_mode, PRICE_SCALE_MODE, _position, _shape + MissingColumn, _js_bool, _price_scale_mode, PRICE_SCALE_MODE, _position, _shape, IDGen class Line: @@ -17,21 +17,40 @@ class Line: self.width = width def set(self, data: pd.DataFrame): + """ + Sets the line data.\n + :param data: columns: date/time, price + """ self._lwc._set_line_data(self.id, data) def update(self, series: pd.Series): """ Updates the line data.\n - :param series: columns: date/time, price + :param series: labels: date/time, price """ self._lwc._update_line_data(self.id, series) +class API: + def __init__(self): + self.click_func = None + + def onClick(self, data): + if isinstance(data['time'], int): + data['time'] = datetime.fromtimestamp(data['time']) + else: + data['time'] = datetime(data['time']['year'], data['time']['month'], data['time']['day']) + self.click_func(data) if self.click_func else None + + class LWC: def __init__(self, volume_enabled): self.js_queue = [] self.loaded = False self._html = HTML + self._rand = IDGen() + + self._js_api = API() self.volume_enabled = volume_enabled self.last_bar = None @@ -42,8 +61,7 @@ class LWC: self.volume_up_color = 'rgba(83,141,131,0.8)' self.volume_down_color = 'rgba(200,127,130,0.8)' - def _on_js_load(self): - pass + def _on_js_load(self): pass def _stored(self, func, *args, **kwargs): if self.loaded: @@ -51,8 +69,9 @@ class LWC: self.js_queue.append((func, args, kwargs)) return True - def _set_last_bar(self, bar: pd.Series): - self.last_bar = bar + def _click_func_code(self, string): self._html = self._html.replace('// __onClick__', string) + + def _set_last_bar(self, bar: pd.Series): self.last_bar = bar def _set_interval(self, df: pd.DataFrame): df['time'] = pd.to_datetime(df['time']) @@ -82,8 +101,7 @@ class LWC: string = string.strftime('%Y-%m-%d') return string - def run_script(self, script): - pass + def run_script(self, script): pass def set(self, df: pd.DataFrame): """ @@ -132,7 +150,6 @@ class LWC: self.run_script(f'chart.volumeSeries.update({volume.to_dict()})') series = series.drop(['volume']) - dictionary = series.to_dict() self.run_script(f'chart.series.update({dictionary})') @@ -176,17 +193,19 @@ class LWC: return None line = self._lines[line_id] + if not line.loaded: + var = self._rand.generate() self.run_script(f''' - let lineSeries = {{ + let lineSeries{var} = {{ color: '{line.color}', lineWidth: {line.width}, }}; - let line = {{ - series: chart.chart.addLineSeries(lineSeries), + let line{var} = {{ + series: chart.chart.addLineSeries(lineSeries{var}), id: '{line_id}', }}; - lines.push(line) + lines.push(line{var}) ''') line.loaded = True df = self._df_datetime_format(df) @@ -261,8 +280,9 @@ class LWC: if self._stored('horizontal_line', price, color, width, style, text, axis_label_visible): return None + var = self._rand.generate() self.run_script(f""" - let priceLine = {{ + let priceLine{var} = {{ price: {price}, color: '{color}', lineWidth: {width}, @@ -270,11 +290,11 @@ class LWC: axisLabelVisible: {'true' if axis_label_visible else 'false'}, title: '{text}', }}; - let line = {{ - line: chart.series.createPriceLine(priceLine), + let line{var} = {{ + line: chart.series.createPriceLine(priceLine{var}), price: {price}, }}; - horizontal_lines.push(line)""") + horizontal_lines.push(line{var})""") def remove_horizontal_line(self, price: Union[float, int]): """ @@ -458,6 +478,13 @@ class LWC: continue self.run_script(script) + def subscribe_click(self, function: object): + if self._stored('subscribe_click', function): + return None + + self._js_api.click_func = function + self.run_script('isSubscribed = true') + SCRIPT = """ @@ -606,7 +633,7 @@ function clickHandler(param) { low: prices.low, close: prices.close, } - pywebview.api.onClick(data) + // __onClick__ } chart.chart.subscribeClick(clickHandler) diff --git a/lightweight_charts/pywebview.py b/lightweight_charts/pywebview.py index 05263fd..ce8c689 100644 --- a/lightweight_charts/pywebview.py +++ b/lightweight_charts/pywebview.py @@ -1,4 +1,3 @@ -import datetime import webview from multiprocessing import Queue @@ -7,29 +6,15 @@ from lightweight_charts.js import LWC _q = Queue() _result_q = Queue() -DEBUG = True - - -class API: - def __init__(self): - self.click_func = None - - def onClick(self, data): - if isinstance(data['time'], int): - data['time'] = datetime.datetime.fromtimestamp(data['time']) - else: - data['time'] = datetime.datetime(data['time']['year'], data['time']['month'], data['time']['day']) - self.click_func(data) if self.click_func else None - class Webview(LWC): def __init__(self, chart): super().__init__(chart.volume_enabled) self.chart = chart self.started = False + self._click_func_code('pywebview.api.onClick(data)') - self.js_api = API() - self.webview = webview.create_window('', html=self._html, on_top=chart.on_top, js_api=self.js_api, + self.webview = webview.create_window('', html=self._html, on_top=chart.on_top, js_api=self._js_api, width=chart.width, height=chart.height, x=chart.x, y=chart.y) self.webview.events.loaded += self._on_js_load @@ -49,13 +34,6 @@ class Webview(LWC): else: webview.start(debug=self.chart.debug) - def subscribe_click(self, function): - if self._stored('subscribe_click', function): - return None - - self.js_api.click_func = function - self.run_script('isSubscribed = true') - def create_line(self, color: str = 'rgba(214, 237, 255, 0.6)', width: int = 2): return super().create_line(color, width).id diff --git a/lightweight_charts/util.py b/lightweight_charts/util.py index 964e70a..f7ba8d6 100644 --- a/lightweight_charts/util.py +++ b/lightweight_charts/util.py @@ -1,3 +1,5 @@ +from random import choices +from string import ascii_lowercase from typing import Literal @@ -58,4 +60,17 @@ def _js_bool(b: bool): return 'true' if b is True else 'false' if b is False els def _price_scale_mode(mode: PRICE_SCALE_MODE): - return 'IndexedTo100' if mode == 'index100' else mode.title() if mode else None \ No newline at end of file + return 'IndexedTo100' if mode == 'index100' else mode.title() if mode else None + + +class IDGen: + def __init__(self): + self.list = [] + + def generate(self): + var = ''.join(choices(ascii_lowercase, k=8)) + if var in self.list: + self.generate() + else: + self.list.append(var) + return var diff --git a/lightweight_charts/widgets.py b/lightweight_charts/widgets.py index 8ed3ad4..cf4375e 100644 --- a/lightweight_charts/widgets.py +++ b/lightweight_charts/widgets.py @@ -4,6 +4,8 @@ except ImportError: pass try: from PyQt5.QtWebEngineWidgets import QWebEngineView + from PyQt5.QtWebChannel import QWebChannel + from PyQt5.QtCore import QObject except ImportError: pass @@ -12,23 +14,23 @@ from lightweight_charts.js import LWC class WxChart(LWC): def __init__(self, parent, volume_enabled=True): - super().__init__(volume_enabled) try: - self.webview = wx.html2.WebView.New(parent) + self.webview: wx.html2.WebView = wx.html2.WebView.New(parent) except NameError: raise ModuleNotFoundError('wx.html2 was not found, and must be installed to use WxChart.') + super().__init__(volume_enabled) + + self.webview.AddScriptMessageHandler('wx_msg') + self._click_func_code('window.wx_msg.postMessage(data)') + self.webview.Bind(wx.html2.EVT_WEBVIEW_SCRIPT_MESSAGE_RECEIVED, lambda e: self._js_api.onClick(eval(e.GetString()))) + self.webview.Bind(wx.html2.EVT_WEBVIEW_LOADED, self._on_js_load) self.webview.SetPage(self._html, '') - self.second_load = False - def run_script(self, script): self.webview.RunScript(script) def _on_js_load(self, e): - if not self.second_load: - self.second_load = True - return self.loaded = True for func, args, kwargs in self.js_queue: getattr(super(), func)(*args, **kwargs) @@ -38,12 +40,13 @@ class WxChart(LWC): class QtChart(LWC): def __init__(self, widget=None, volume_enabled=True): - super().__init__(volume_enabled) try: self.webview = QWebEngineView(widget) except NameError: raise ModuleNotFoundError('QWebEngineView was not found, and must be installed to use QtChart.') + super().__init__(volume_enabled) + self.webview.loadFinished.connect(self._on_js_load) self.webview.page().setHtml(self._html) diff --git a/setup.py b/setup.py index 7cc84fb..1fa98df 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ setup( python_requires='>=3.9', install_requires=[ 'pandas', - 'pywebview==4.0.2', + 'pywebview', ], author='louisnw', license='MIT',