- Fixed an issue which caused JavaScript variables of the same name to be declared twice.

- Refactoring to allow the widget classes to use the subscribe_click method.
This commit is contained in:
louisnw
2023-05-17 12:45:54 +01:00
parent 993fbe8ed8
commit 88c8a266ec
5 changed files with 75 additions and 52 deletions

View File

@ -5,7 +5,7 @@ from typing import Dict, Union
from lightweight_charts.pkg import LWC_3_5_0 from lightweight_charts.pkg import LWC_3_5_0
from lightweight_charts.util import LINE_TYPE, POSITION, SHAPE, CROSSHAIR_MODE, _crosshair_mode, _line_type, \ 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: class Line:
@ -17,21 +17,40 @@ class Line:
self.width = width self.width = width
def set(self, data: pd.DataFrame): 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) self._lwc._set_line_data(self.id, data)
def update(self, series: pd.Series): def update(self, series: pd.Series):
""" """
Updates the line data.\n 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) 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: class LWC:
def __init__(self, volume_enabled): def __init__(self, volume_enabled):
self.js_queue = [] self.js_queue = []
self.loaded = False self.loaded = False
self._html = HTML self._html = HTML
self._rand = IDGen()
self._js_api = API()
self.volume_enabled = volume_enabled self.volume_enabled = volume_enabled
self.last_bar = None self.last_bar = None
@ -42,8 +61,7 @@ class LWC:
self.volume_up_color = 'rgba(83,141,131,0.8)' self.volume_up_color = 'rgba(83,141,131,0.8)'
self.volume_down_color = 'rgba(200,127,130,0.8)' self.volume_down_color = 'rgba(200,127,130,0.8)'
def _on_js_load(self): def _on_js_load(self): pass
pass
def _stored(self, func, *args, **kwargs): def _stored(self, func, *args, **kwargs):
if self.loaded: if self.loaded:
@ -51,8 +69,9 @@ class LWC:
self.js_queue.append((func, args, kwargs)) self.js_queue.append((func, args, kwargs))
return True return True
def _set_last_bar(self, bar: pd.Series): def _click_func_code(self, string): self._html = self._html.replace('// __onClick__', string)
self.last_bar = bar
def _set_last_bar(self, bar: pd.Series): self.last_bar = bar
def _set_interval(self, df: pd.DataFrame): def _set_interval(self, df: pd.DataFrame):
df['time'] = pd.to_datetime(df['time']) df['time'] = pd.to_datetime(df['time'])
@ -82,8 +101,7 @@ class LWC:
string = string.strftime('%Y-%m-%d') string = string.strftime('%Y-%m-%d')
return string return string
def run_script(self, script): def run_script(self, script): pass
pass
def set(self, df: pd.DataFrame): def set(self, df: pd.DataFrame):
""" """
@ -132,7 +150,6 @@ class LWC:
self.run_script(f'chart.volumeSeries.update({volume.to_dict()})') self.run_script(f'chart.volumeSeries.update({volume.to_dict()})')
series = series.drop(['volume']) series = series.drop(['volume'])
dictionary = series.to_dict() dictionary = series.to_dict()
self.run_script(f'chart.series.update({dictionary})') self.run_script(f'chart.series.update({dictionary})')
@ -176,17 +193,19 @@ class LWC:
return None return None
line = self._lines[line_id] line = self._lines[line_id]
if not line.loaded: if not line.loaded:
var = self._rand.generate()
self.run_script(f''' self.run_script(f'''
let lineSeries = {{ let lineSeries{var} = {{
color: '{line.color}', color: '{line.color}',
lineWidth: {line.width}, lineWidth: {line.width},
}}; }};
let line = {{ let line{var} = {{
series: chart.chart.addLineSeries(lineSeries), series: chart.chart.addLineSeries(lineSeries{var}),
id: '{line_id}', id: '{line_id}',
}}; }};
lines.push(line) lines.push(line{var})
''') ''')
line.loaded = True line.loaded = True
df = self._df_datetime_format(df) 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): if self._stored('horizontal_line', price, color, width, style, text, axis_label_visible):
return None return None
var = self._rand.generate()
self.run_script(f""" self.run_script(f"""
let priceLine = {{ let priceLine{var} = {{
price: {price}, price: {price},
color: '{color}', color: '{color}',
lineWidth: {width}, lineWidth: {width},
@ -270,11 +290,11 @@ class LWC:
axisLabelVisible: {'true' if axis_label_visible else 'false'}, axisLabelVisible: {'true' if axis_label_visible else 'false'},
title: '{text}', title: '{text}',
}}; }};
let line = {{ let line{var} = {{
line: chart.series.createPriceLine(priceLine), line: chart.series.createPriceLine(priceLine{var}),
price: {price}, price: {price},
}}; }};
horizontal_lines.push(line)""") horizontal_lines.push(line{var})""")
def remove_horizontal_line(self, price: Union[float, int]): def remove_horizontal_line(self, price: Union[float, int]):
""" """
@ -458,6 +478,13 @@ class LWC:
continue continue
self.run_script(script) 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 = """ SCRIPT = """
@ -606,7 +633,7 @@ function clickHandler(param) {
low: prices.low, low: prices.low,
close: prices.close, close: prices.close,
} }
pywebview.api.onClick(data) // __onClick__
} }
chart.chart.subscribeClick(clickHandler) chart.chart.subscribeClick(clickHandler)

View File

@ -1,4 +1,3 @@
import datetime
import webview import webview
from multiprocessing import Queue from multiprocessing import Queue
@ -7,29 +6,15 @@ from lightweight_charts.js import LWC
_q = Queue() _q = Queue()
_result_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): class Webview(LWC):
def __init__(self, chart): def __init__(self, chart):
super().__init__(chart.volume_enabled) super().__init__(chart.volume_enabled)
self.chart = chart self.chart = chart
self.started = False 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) width=chart.width, height=chart.height, x=chart.x, y=chart.y)
self.webview.events.loaded += self._on_js_load self.webview.events.loaded += self._on_js_load
@ -49,13 +34,6 @@ class Webview(LWC):
else: else:
webview.start(debug=self.chart.debug) 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): def create_line(self, color: str = 'rgba(214, 237, 255, 0.6)', width: int = 2):
return super().create_line(color, width).id return super().create_line(color, width).id

View File

@ -1,3 +1,5 @@
from random import choices
from string import ascii_lowercase
from typing import Literal 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): def _price_scale_mode(mode: PRICE_SCALE_MODE):
return 'IndexedTo100' if mode == 'index100' else mode.title() if mode else None 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

View File

@ -4,6 +4,8 @@ except ImportError:
pass pass
try: try:
from PyQt5.QtWebEngineWidgets import QWebEngineView from PyQt5.QtWebEngineWidgets import QWebEngineView
from PyQt5.QtWebChannel import QWebChannel
from PyQt5.QtCore import QObject
except ImportError: except ImportError:
pass pass
@ -12,23 +14,23 @@ from lightweight_charts.js import LWC
class WxChart(LWC): class WxChart(LWC):
def __init__(self, parent, volume_enabled=True): def __init__(self, parent, volume_enabled=True):
super().__init__(volume_enabled)
try: try:
self.webview = wx.html2.WebView.New(parent) self.webview: wx.html2.WebView = wx.html2.WebView.New(parent)
except NameError: except NameError:
raise ModuleNotFoundError('wx.html2 was not found, and must be installed to use WxChart.') 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.Bind(wx.html2.EVT_WEBVIEW_LOADED, self._on_js_load)
self.webview.SetPage(self._html, '') self.webview.SetPage(self._html, '')
self.second_load = False
def run_script(self, script): self.webview.RunScript(script) def run_script(self, script): self.webview.RunScript(script)
def _on_js_load(self, e): def _on_js_load(self, e):
if not self.second_load:
self.second_load = True
return
self.loaded = True self.loaded = True
for func, args, kwargs in self.js_queue: for func, args, kwargs in self.js_queue:
getattr(super(), func)(*args, **kwargs) getattr(super(), func)(*args, **kwargs)
@ -38,12 +40,13 @@ class WxChart(LWC):
class QtChart(LWC): class QtChart(LWC):
def __init__(self, widget=None, volume_enabled=True): def __init__(self, widget=None, volume_enabled=True):
super().__init__(volume_enabled)
try: try:
self.webview = QWebEngineView(widget) self.webview = QWebEngineView(widget)
except NameError: except NameError:
raise ModuleNotFoundError('QWebEngineView was not found, and must be installed to use QtChart.') 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.loadFinished.connect(self._on_js_load)
self.webview.page().setHtml(self._html) self.webview.page().setHtml(self._html)

View File

@ -10,7 +10,7 @@ setup(
python_requires='>=3.9', python_requires='>=3.9',
install_requires=[ install_requires=[
'pandas', 'pandas',
'pywebview==4.0.2', 'pywebview',
], ],
author='louisnw', author='louisnw',
license='MIT', license='MIT',