diff --git a/README.md b/README.md index 1f36dfe..5a7f73a 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ ___ 2. Blocking or non-blocking GUI. 3. Streamlined for live data, with methods for updating directly from tick data. 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: diff --git a/docs/source/docs.md b/docs/source/docs.md index e364785..8bb8acc 100644 --- a/docs/source/docs.md +++ b/docs/source/docs.md @@ -95,9 +95,9 @@ Config options for the chart. ___ ### `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` @@ -106,6 +106,12 @@ ___ 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` `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` diff --git a/lightweight_charts/chart.py b/lightweight_charts/chart.py index 9e28ace..523e773 100644 --- a/lightweight_charts/chart.py +++ b/lightweight_charts/chart.py @@ -31,7 +31,8 @@ class Line: class Chart: 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.volume_enabled = volume_enabled self.width = width @@ -179,6 +180,12 @@ class Chart: """ 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)', wick_enabled: bool = True, border_enabled: bool = True, border_up_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) 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): - c_id = self._go_return('create_sub_chart', volume_enabled, position, width, height, sync) + width: float = 0.5, height: float = 0.5, sync: Union[bool, UUID] = False): + c_id = self._go_return('create_subchart', volume_enabled, position, width, height, sync) return SubChart(self, c_id) diff --git a/lightweight_charts/js.py b/lightweight_charts/js.py index 216dd09..48950cb 100644 --- a/lightweight_charts/js.py +++ b/lightweight_charts/js.py @@ -45,7 +45,7 @@ class API: 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.js_queue = [] self.loaded = False @@ -331,7 +331,7 @@ class LWC: if self._stored('config', mode, title, right_padding): 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}.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 ''} }}}})""") + 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)', wick_enabled: bool = True, border_enabled: bool = True, border_up_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({{ {f'upColor: "{up_color}",' if up_color else ''} {f'downColor: "{down_color}",' if down_color else ''} - {f'wickVisible: {_js_bool(wick_enabled)},' if wick_enabled else ''} - {f'borderVisible: {_js_bool(border_enabled)},' if border_enabled else ''} + {f'wickVisible: {_js_bool(wick_enabled)},' if wick_enabled is not None 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'borderDownColor: "{border_down_color}",' if border_down_color else ''} {f'wickUpColor: "{wick_up_color}",' if wick_up_color else ''} @@ -524,13 +549,13 @@ class LWC: {self._js_api_code}(data{var}) }})''') - def create_sub_chart(self, volume_enabled: bool = True, position: Literal['left', 'right', 'top', 'bottom'] = 'left', - width: float = 0.5, height: float = 0.5, sync: bool | UUID = False): + def create_subchart(self, volume_enabled: bool = True, position: Literal['left', 'right', 'top', 'bottom'] = 'left', + width: float = 0.5, height: float = 0.5, sync: Union[bool, UUID] = False): subchart = SubChart(self, volume_enabled, position, width, height, sync) self._subcharts[subchart.id] = 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) self._subcharts[subchart.id] = subchart return subchart.id @@ -543,7 +568,7 @@ class SubChart(LWC): self._parent = parent 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_code = self._chart._js_api_code @@ -574,15 +599,15 @@ class SubChart(LWC): }}); ''' 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.float = "{self.position}" //chartsDiv.style.display = 'inline-block' chartsDiv.style.float = 'left' - var {self._chart_var} = {{}} - {self._chart_var}.scale= {{ + {self._chart_var} = {{}} + {self._chart_var}.scale = {{ width: {self.inner_width}, height: {self.inner_height} }} @@ -616,9 +641,9 @@ class SubChart(LWC): class PyWebViewSubChart(SubChart): - def create_sub_chart(self, volume_enabled: bool = True, position: Literal['left', 'right', 'top', 'bottom'] = 'left', - width: float = 0.5, height: float = 0.5, sync: bool | UUID = False): - return self._chart._pywebview_sub_chart(volume_enabled, position, width, height, sync, parent=self) + def create_subchart(self, volume_enabled: bool = True, position: Literal['left', 'right', 'top', 'bottom'] = 'left', + width: float = 0.5, height: float = 0.5, sync: Union[bool, UUID] = False): + 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): return super().create_line(color, width).id @@ -698,14 +723,14 @@ function makeVolumeSeries(chart) { const chartsDiv = document.createElement('div') var chart = {} - -chart.chart = makeChart(window.innerWidth, window.innerHeight, chartsDiv) -chart.series = makeCandlestickSeries(chart.chart) -chart.volumeSeries = makeVolumeSeries(chart.chart) chart.scale = { width: __INNER_WIDTH__, 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) diff --git a/lightweight_charts/pywebview.py b/lightweight_charts/pywebview.py index a670623..18ef19a 100644 --- a/lightweight_charts/pywebview.py +++ b/lightweight_charts/pywebview.py @@ -1,4 +1,4 @@ -from typing import Literal +from typing import Literal, Union from uuid import UUID import webview 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): return super().create_line(color, width).id - def create_sub_chart(self, volume_enabled: bool = True, position: Literal['left', 'right', 'top', 'bottom'] = 'left', - width: float = 0.5, height: float = 0.5, sync: bool | UUID = False): - return super()._pywebview_sub_chart(volume_enabled, position, width, height, sync) + def create_subchart(self, volume_enabled: bool = True, position: Literal['left', 'right', 'top', 'bottom'] = 'left', + width: float = 0.5, height: float = 0.5, sync: Union[bool, UUID] = False): + return super()._pywebview_subchart(volume_enabled, position, width, height, sync) def _loop(chart, controller=None): diff --git a/lightweight_charts/widgets.py b/lightweight_charts/widgets.py index 9d8a401..cd3e7a1 100644 --- a/lightweight_charts/widgets.py +++ b/lightweight_charts/widgets.py @@ -13,13 +13,13 @@ from lightweight_charts.js import 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: 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) + super().__init__(volume_enabled, inner_width=inner_width, inner_height=inner_height) self.webview.AddScriptMessageHandler('wx_msg') self._js_api_code = 'window.wx_msg.postMessage' @@ -33,19 +33,24 @@ class WxChart(LWC): def _on_js_load(self, e): self.loaded = True 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 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: self.webview = QWebEngineView(widget) except NameError: 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.page().setHtml(self._html)