- 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:
@ -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)
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user