- Fixed a bug causing the library to throw errors when used with python 3.9.

- Fixed a bug that did not size the multi pane charts correctly.
- Implementation of the ‘grid’ common method.
This commit is contained in:
louisnw
2023-05-20 01:14:01 +01:00
parent 0e709040d5
commit b60e9705cb
6 changed files with 76 additions and 33 deletions

View File

@ -21,7 +21,7 @@ ___
2. Blocking or non-blocking GUI. 2. Blocking or non-blocking GUI.
3. Streamlined for live data, with methods for updating directly from tick data. 3. Streamlined for live data, with methods for updating directly from tick data.
4. Support for PyQt and wxPython. 4. Support for PyQt and wxPython.
5. Multi-Pane Charts using `SubCharts`. 5. Multi-Pane Charts using the `SubChart` ([examples](https://lightweight-charts-python.readthedocs.io/en/latest/docs.html#subchart)).
___ ___
### 1. Display data from a csv: ### 1. Display data from a csv:

View File

@ -95,9 +95,9 @@ Config options for the chart.
___ ___
### `time_scale` ### `time_scale`
`time_visible: bool` | `seconds_visible: bool` `visible: bool` | `time_visible: bool` | `seconds_visible: bool`
Options for the time scale of the chart. Time scale options for the chart.
___ ___
### `layout` ### `layout`
@ -106,6 +106,12 @@ ___
Global layout options for the chart. Global layout options for the chart.
___ ___
### `grid`
`vert_enabled: bool` | `horz_enabled: bool` | `color: str` | `style: 'solid'/'dotted'/'dashed'/'large_dashed'/'sparse_dotted'`
Grid options for the chart.
___
### `candle_style` ### `candle_style`
`up_color: str` | `down_color: str` | `wick_enabled: bool` | `border_enabled: bool` | `border_up_color: str` | `border_down_color: str` | `wick_up_color: str` | `wick_down_color: str` `up_color: str` | `down_color: str` | `wick_enabled: bool` | `border_enabled: bool` | `border_up_color: str` | `border_down_color: str` | `wick_up_color: str` | `wick_down_color: str`

View File

@ -31,7 +31,8 @@ class Line:
class Chart: class Chart:
def __init__(self, volume_enabled: bool = True, width: int = 800, height: int = 600, x: int = None, y: int = None, 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, sub: bool = False, inner_width=1, inner_height=1): on_top: bool = False, debug: bool = False, sub: bool = False,
inner_width: float = 1.0, inner_height: float = 1.0):
self.debug = debug self.debug = debug
self.volume_enabled = volume_enabled self.volume_enabled = volume_enabled
self.width = width self.width = width
@ -179,6 +180,12 @@ class Chart:
""" """
self._go('layout', background_color, text_color, font_size, font_family) self._go('layout', background_color, text_color, font_size, font_family)
def grid(self, vert_enabled: bool = True, horz_enabled: bool = True, color: str = 'rgba(29, 30, 38, 5)', style: LINE_TYPE = 'solid'):
"""
Grid styling for the chart.
"""
self._go('grid', vert_enabled, horz_enabled, color, style)
def candle_style(self, up_color: str = 'rgba(39, 157, 130, 100)', down_color: str = 'rgba(200, 97, 100, 100)', def candle_style(self, up_color: str = 'rgba(39, 157, 130, 100)', down_color: str = 'rgba(200, 97, 100, 100)',
wick_enabled: bool = True, border_enabled: bool = True, border_up_color: str = '', wick_enabled: bool = True, border_enabled: bool = True, border_up_color: str = '',
border_down_color: str = '', wick_up_color: str = '', wick_down_color: str = ''): border_down_color: str = '', wick_up_color: str = '', wick_down_color: str = ''):
@ -231,8 +238,8 @@ class Chart:
self._go('subscribe_click', function) self._go('subscribe_click', function)
def create_subchart(self, volume_enabled: bool = True, position: Literal['left', 'right', 'top', 'bottom'] = 'left', def create_subchart(self, volume_enabled: bool = True, position: Literal['left', 'right', 'top', 'bottom'] = 'left',
width: float = 0.5, height: float = 0.5, sync: bool | UUID = False): width: float = 0.5, height: float = 0.5, sync: Union[bool, UUID] = False):
c_id = self._go_return('create_sub_chart', volume_enabled, position, width, height, sync) c_id = self._go_return('create_subchart', volume_enabled, position, width, height, sync)
return SubChart(self, c_id) return SubChart(self, c_id)

View File

@ -45,7 +45,7 @@ class API:
class LWC: class LWC:
def __init__(self, volume_enabled, inner_width=1, inner_height=1): def __init__(self, volume_enabled: bool = True, inner_width: float = 1.0, inner_height: float = 1.0):
self.id = uuid4() self.id = uuid4()
self.js_queue = [] self.js_queue = []
self.loaded = False self.loaded = False
@ -331,7 +331,7 @@ class LWC:
if self._stored('config', mode, title, right_padding): if self._stored('config', mode, title, right_padding):
return None return None
self.run_script(f'{self._chart_var}.chart.timeScale().scrollToPosition({right_padding}, false)') if right_padding else None self.run_script(f'{self._chart_var}.chart.timeScale().scrollToPosition({right_padding}, false)') if right_padding is not None else None
self.run_script(f'{self._chart_var}.series.applyOptions({{title: "{title}"}})') if title else None self.run_script(f'{self._chart_var}.series.applyOptions({{title: "{title}"}})') if title else None
self.run_script( self.run_script(
f"{self._chart_var}.chart.priceScale().applyOptions({{mode: LightweightCharts.PriceScaleMode.{_price_scale_mode(mode)}}})") if mode else None f"{self._chart_var}.chart.priceScale().applyOptions({{mode: LightweightCharts.PriceScaleMode.{_price_scale_mode(mode)}}})") if mode else None
@ -378,6 +378,31 @@ class LWC:
{f'fontFamily: "{font_family}",' if font_family else ''} {f'fontFamily: "{font_family}",' if font_family else ''}
}}}})""") }}}})""")
def grid(self, vert_enabled: bool = True, horz_enabled: bool = True, color: str = 'rgba(29, 30, 38, 5)', style: LINE_TYPE = 'solid'):
"""
Grid styling for the chart.
"""
if self._stored('grid', vert_enabled, horz_enabled, color, style):
return None
self.run_script(f"""
{self._chart_var}.chart.applyOptions({{
grid: {{
{f'''vertLines: {{
{f'visible: {_js_bool(vert_enabled)},' if vert_enabled is not None else ''}
{f'color: "{color}",' if color else ''}
{f'style: LightweightCharts.LineStyle.{_line_type(style)},' if style else ''}
}},''' if vert_enabled is not None or color or style else ''}
{f'''horzLines: {{
{f'visible: {_js_bool(horz_enabled)},' if horz_enabled is not None else ''}
{f'color: "{color}",' if color else ''}
{f'style: LightweightCharts.LineStyle.{_line_type(style)},' if style else ''}
}},''' if horz_enabled is not None or color or style else ''}
}}
}})
""")
def candle_style(self, up_color: str = 'rgba(39, 157, 130, 100)', down_color: str = 'rgba(200, 97, 100, 100)', def candle_style(self, up_color: str = 'rgba(39, 157, 130, 100)', down_color: str = 'rgba(200, 97, 100, 100)',
wick_enabled: bool = True, border_enabled: bool = True, border_up_color: str = '', wick_enabled: bool = True, border_enabled: bool = True, border_up_color: str = '',
border_down_color: str = '', wick_up_color: str = '', wick_down_color: str = ''): border_down_color: str = '', wick_up_color: str = '', wick_down_color: str = ''):
@ -392,8 +417,8 @@ class LWC:
{self._chart_var}.series.applyOptions({{ {self._chart_var}.series.applyOptions({{
{f'upColor: "{up_color}",' if up_color else ''} {f'upColor: "{up_color}",' if up_color else ''}
{f'downColor: "{down_color}",' if down_color else ''} {f'downColor: "{down_color}",' if down_color else ''}
{f'wickVisible: {_js_bool(wick_enabled)},' if wick_enabled else ''} {f'wickVisible: {_js_bool(wick_enabled)},' if wick_enabled is not None else ''}
{f'borderVisible: {_js_bool(border_enabled)},' if border_enabled else ''} {f'borderVisible: {_js_bool(border_enabled)},' if border_enabled is not None else ''}
{f'borderUpColor: "{border_up_color}",' if border_up_color else ''} {f'borderUpColor: "{border_up_color}",' if border_up_color else ''}
{f'borderDownColor: "{border_down_color}",' if border_down_color else ''} {f'borderDownColor: "{border_down_color}",' if border_down_color else ''}
{f'wickUpColor: "{wick_up_color}",' if wick_up_color else ''} {f'wickUpColor: "{wick_up_color}",' if wick_up_color else ''}
@ -524,13 +549,13 @@ class LWC:
{self._js_api_code}(data{var}) {self._js_api_code}(data{var})
}})''') }})''')
def create_sub_chart(self, volume_enabled: bool = True, position: Literal['left', 'right', 'top', 'bottom'] = 'left', def create_subchart(self, volume_enabled: bool = True, position: Literal['left', 'right', 'top', 'bottom'] = 'left',
width: float = 0.5, height: float = 0.5, sync: bool | UUID = False): width: float = 0.5, height: float = 0.5, sync: Union[bool, UUID] = False):
subchart = SubChart(self, volume_enabled, position, width, height, sync) subchart = SubChart(self, volume_enabled, position, width, height, sync)
self._subcharts[subchart.id] = subchart self._subcharts[subchart.id] = subchart
return subchart return subchart
def _pywebview_sub_chart(self, volume_enabled, position, width, height, sync, parent=None): def _pywebview_subchart(self, volume_enabled, position, width, height, sync, parent=None):
subchart = PyWebViewSubChart(self if not parent else parent, volume_enabled, position, width, height, sync) subchart = PyWebViewSubChart(self if not parent else parent, volume_enabled, position, width, height, sync)
self._subcharts[subchart.id] = subchart self._subcharts[subchart.id] = subchart
return subchart.id return subchart.id
@ -543,7 +568,7 @@ class SubChart(LWC):
self._parent = parent self._parent = parent
self._rand = self._chart._rand self._rand = self._chart._rand
self._chart_var = self._rand.generate() self._chart_var = f'window.{self._rand.generate()}'
self._js_api = self._chart._js_api self._js_api = self._chart._js_api
self._js_api_code = self._chart._js_api_code self._js_api_code = self._chart._js_api_code
@ -574,15 +599,15 @@ class SubChart(LWC):
}}); }});
''' '''
self.run_script(f''' self.run_script(f'''
var {self._chart_var}div = document.createElement('div') {self._chart_var}div = document.createElement('div')
//{self._chart_var}div.style.position = 'relative' //{self._chart_var}div.style.position = 'relative'
{self._chart_var}div.style.float = "{self.position}" {self._chart_var}div.style.float = "{self.position}"
//chartsDiv.style.display = 'inline-block' //chartsDiv.style.display = 'inline-block'
chartsDiv.style.float = 'left' chartsDiv.style.float = 'left'
var {self._chart_var} = {{}} {self._chart_var} = {{}}
{self._chart_var}.scale= {{ {self._chart_var}.scale = {{
width: {self.inner_width}, width: {self.inner_width},
height: {self.inner_height} height: {self.inner_height}
}} }}
@ -616,9 +641,9 @@ class SubChart(LWC):
class PyWebViewSubChart(SubChart): class PyWebViewSubChart(SubChart):
def create_sub_chart(self, volume_enabled: bool = True, position: Literal['left', 'right', 'top', 'bottom'] = 'left', def create_subchart(self, volume_enabled: bool = True, position: Literal['left', 'right', 'top', 'bottom'] = 'left',
width: float = 0.5, height: float = 0.5, sync: bool | UUID = False): width: float = 0.5, height: float = 0.5, sync: Union[bool, UUID] = False):
return self._chart._pywebview_sub_chart(volume_enabled, position, width, height, sync, parent=self) return self._chart._pywebview_subchart(volume_enabled, position, width, height, sync, parent=self)
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
@ -698,14 +723,14 @@ function makeVolumeSeries(chart) {
const chartsDiv = document.createElement('div') const chartsDiv = document.createElement('div')
var chart = {} var chart = {}
chart.chart = makeChart(window.innerWidth, window.innerHeight, chartsDiv)
chart.series = makeCandlestickSeries(chart.chart)
chart.volumeSeries = makeVolumeSeries(chart.chart)
chart.scale = { chart.scale = {
width: __INNER_WIDTH__, width: __INNER_WIDTH__,
height: __INNER_HEIGHT__ height: __INNER_HEIGHT__
} }
chart.chart = makeChart(window.innerWidth*chart.scale.width, window.innerHeight*chart.scale.height, chartsDiv)
chart.series = makeCandlestickSeries(chart.chart)
chart.volumeSeries = makeVolumeSeries(chart.chart)
document.body.appendChild(chartsDiv) document.body.appendChild(chartsDiv)

View File

@ -1,4 +1,4 @@
from typing import Literal from typing import Literal, Union
from uuid import UUID from uuid import UUID
import webview import webview
from multiprocessing import Queue from multiprocessing import Queue
@ -52,9 +52,9 @@ class Webview(LWC):
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
def create_sub_chart(self, volume_enabled: bool = True, position: Literal['left', 'right', 'top', 'bottom'] = 'left', def create_subchart(self, volume_enabled: bool = True, position: Literal['left', 'right', 'top', 'bottom'] = 'left',
width: float = 0.5, height: float = 0.5, sync: bool | UUID = False): width: float = 0.5, height: float = 0.5, sync: Union[bool, UUID] = False):
return super()._pywebview_sub_chart(volume_enabled, position, width, height, sync) return super()._pywebview_subchart(volume_enabled, position, width, height, sync)
def _loop(chart, controller=None): def _loop(chart, controller=None):

View File

@ -13,13 +13,13 @@ 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: bool = True, inner_width: float = 1.0, inner_height: float = 1.0):
try: try:
self.webview: wx.html2.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) super().__init__(volume_enabled, inner_width=inner_width, inner_height=inner_height)
self.webview.AddScriptMessageHandler('wx_msg') self.webview.AddScriptMessageHandler('wx_msg')
self._js_api_code = 'window.wx_msg.postMessage' self._js_api_code = 'window.wx_msg.postMessage'
@ -33,19 +33,24 @@ class WxChart(LWC):
def _on_js_load(self, e): def _on_js_load(self, e):
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) if 'SUB' in func:
c_id = args[0]
args = args[1:]
getattr(self._subcharts[c_id], func.replace('SUB', ''))(*args)
else:
getattr(self, func)(*args)
def get_webview(self): return self.webview def get_webview(self): return self.webview
class QtChart(LWC): class QtChart(LWC):
def __init__(self, widget=None, volume_enabled=True): def __init__(self, widget=None, volume_enabled: bool = True, inner_width: float = 1.0, inner_height: float = 1.0):
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) super().__init__(volume_enabled, inner_width=inner_width, inner_height=inner_height)
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)