- Significant refactoring resulting in a 34% reduction in size of the codebase (excluding the Lightweight Charts package) and greater efficiency.

- Upgraded to Lightweight Charts v4.0.1.
- Added a ‘hover’ item to the returning dictionary from subscribe_click.
- Markers and SubCharts no longer use a UUID for identification, but an 8 character string.
This commit is contained in:
louisnw
2023-05-21 15:42:57 +01:00
parent 445d9b67d3
commit 7ea2b0ac19
10 changed files with 336 additions and 663 deletions

View File

@ -6,7 +6,7 @@
[![Documentation](https://img.shields.io/badge/documentation-006ee3)](https://lightweight-charts-python.readthedocs.io/en/latest/docs.html)
![cover](cover.png)
![cover](https://raw.githubusercontent.com/louisnw01/lightweight-charts-python/main/cover.png)
lightweight-charts-python aims to provide a simple and pythonic way to access and implement [TradingView's Lightweight Charts](https://www.tradingview.com/lightweight-charts/).
@ -42,7 +42,7 @@ if __name__ == '__main__':
chart.show(block=True)
```
![setting_data image](https://github.com/louisnw01/lightweight-charts-python/blob/main/examples/1_setting_data/setting_data.png)
![setting_data image](https://raw.githubusercontent.com/louisnw01/lightweight-charts-python/main/examples/1_setting_data/setting_data.png)
___
### 2. Updating bars in real-time:
@ -76,7 +76,7 @@ if __name__ == '__main__':
```
![live data gif](https://github.com/louisnw01/lightweight-charts-python/blob/main/examples/2_live_data/live_data.gif)
![live data gif](https://github.com/louisnw01/lightweight-charts-python/blob/main/examples/2_live_data/live_data.gif?raw=true)
___
### 3. Updating bars from tick data in real-time:
@ -106,7 +106,7 @@ if __name__ == '__main__':
sleep(0.3)
```
![tick data gif](https://github.com/louisnw01/lightweight-charts-python/blob/main/examples/3_tick_data/tick_data.gif)
![tick data gif](https://raw.githubusercontent.com/louisnw01/lightweight-charts-python/main/examples/3_tick_data/tick_data.gif)
___
### 4. Line Indicators:
@ -140,7 +140,7 @@ if __name__ == '__main__':
chart.show(block=True)
```
![line indicators image](https://github.com/louisnw01/lightweight-charts-python/blob/main/examples/4_line_indicators/line_indicators.png)
![line indicators image](https://raw.githubusercontent.com/louisnw01/lightweight-charts-python/main/examples/4_line_indicators/line_indicators.png)
___
### 5. Styling:
@ -177,7 +177,7 @@ if __name__ == '__main__':
chart.show(block=True)
```
![styling image](https://github.com/louisnw01/lightweight-charts-python/blob/main/examples/5_styling/styling.png)
![styling image](https://raw.githubusercontent.com/louisnw01/lightweight-charts-python/main/examples/5_styling/styling.png)
___
### 6. Callbacks:
@ -203,7 +203,7 @@ if __name__ == '__main__':
chart.show(block=True)
```
![callbacks gif](https://github.com/louisnw01/lightweight-charts-python/blob/main/examples/6_callbacks/callbacks.gif)
![callbacks gif](https://raw.githubusercontent.com/louisnw01/lightweight-charts-python/main/examples/6_callbacks/callbacks.gif)
___
[![Documentation](https://img.shields.io/badge/documentation-006ee3)](https://lightweight-charts-python.readthedocs.io/en/latest/docs.html)
@ -213,4 +213,3 @@ ___
_This package is an independent creation and has not been endorsed, sponsored, or approved by TradingView. The author of this package does not have any official relationship with TradingView, and the package does not represent the views or opinions of TradingView._

View File

@ -1,7 +1,7 @@
project = 'lightweight-charts-python'
copyright = '2023, louisnw'
author = 'louisnw'
release = '1.0.3'
release = '1.0.7'
extensions = ["myst_parser"]

View File

@ -6,7 +6,7 @@
[![License](https://img.shields.io/github/license/louisnw01/lightweight-charts-python?color=9c2400)](https://github.com/louisnw01/lightweight-charts-python/blob/main/LICENSE)
[![Stars - lightweight-charts-python](https://img.shields.io/github/stars/louisnw01/lightweight-charts-python?style=social)](https://github.com/louisnw01/lightweight-charts-python)
[![Forks - lightweight-charts-python](https://img.shields.io/github/forks/louisnw01/lightweight-charts-python?style=social)](https://github.com/louisnw01/lightweight-charts-python)
___
## Common Methods
These methods can be used within the `Chart`, `SubChart`, `QtChart`, and `WxChart` objects.
@ -57,17 +57,17 @@ Creates and returns a [Line](#line) object.
___
### `marker`
`time: datetime` | `position: 'above'/'below'/'inside'` | `shape: 'arrow_up'/'arrow_down'/'circle'/'square'` | `color: str` | `text: str` | `-> UUID`
`time: datetime` | `position: 'above'/'below'/'inside'` | `shape: 'arrow_up'/'arrow_down'/'circle'/'square'` | `color: str` | `text: str` | `-> str`
Adds a marker to the chart, and returns its UUID.
Adds a marker to the chart, and returns its id.
If the `time` parameter is not given, the marker will be placed at the latest bar.
___
### `remove_marker`
`m_id: UUID`
`marker_id: str`
Removes the marker with the given UUID.
Removes the marker with the given id.
Usage:
```python
@ -162,13 +162,13 @@ ___
Subscribes the given function to a chart 'click' event.
The event emits a dictionary containing the bar at the time clicked, with the keys:
The event emits a dictionary containing the bar at the time clicked, the id of the `Chart` or `SubChart`, and the hover price:
`time | open | high | low | close`
`time | open | high | low | close | id | hover`
___
### `create_subchart`
`volume_enabled: bool` | `position: 'left'/'right'/'top'/'bottom'`, `width: float` | `height: float` | `sync: bool/UUID` | `-> SubChart`
`volume_enabled: bool` | `position: 'left'/'right'/'top'/'bottom'`, `width: float` | `height: float` | `sync: bool/str` | `-> SubChart`
Creates and returns a [SubChart](#subchart) object, placing it adjacent to the declaring `Chart` or `SubChart`.
@ -176,9 +176,7 @@ Creates and returns a [SubChart](#subchart) object, placing it adjacent to the d
`height` | `width`: Specifies the size of the `SubChart`, where `1` is the width/height of the window (100%)
`sync`: If given as `True`, the `SubChart`'s time scale will follow that of the declaring `Chart` or `SubChart`. If a `UUID` object is passed, the `SubChart` will follow the panel with the given `UUID`.
Chart `UUID`'s can be accessed from the`chart.id` and `subchart.id` attributes.
`sync`: If given as `True`, the `SubChart`'s time scale will follow that of the declaring `Chart` or `SubChart`. If a `str` is passed, the `SubChart` will follow the panel with the given id. Chart ids can be accessed from the`chart.id` and `subchart.id` attributes.
```{important}
`width` and `height` must be given as a number between 0 and 1.

View File

@ -4,6 +4,7 @@
:maxdepth: 2
docs
Github Repository <https://github.com/louisnw01/lightweight-charts-python>
```
```{include} ../../README.md

View File

@ -1,34 +1,39 @@
import pandas as pd
from uuid import UUID, uuid4
from datetime import timedelta, datetime
from typing import Dict, Union, Literal
from typing import Union, Literal
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, IDGen, _valid_color
from lightweight_charts.pkg import LWC_4_0_1
from lightweight_charts.util import LINE_STYLE, MARKER_POSITION, MARKER_SHAPE, CROSSHAIR_MODE, _crosshair_mode, _line_style, \
MissingColumn, _js_bool, _price_scale_mode, PRICE_SCALE_MODE, _marker_position, _marker_shape, IDGen
class Line:
def __init__(self, lwc, line_id, color, width):
self._lwc = lwc
self.loaded = False
self.id = line_id
self.color = color
self.width = width
def __init__(self, parent, color, width):
self._parent = parent
self.id = self._parent._rand.generate()
self._parent.run_script(f'''
var {self.id} = {self._parent.id}.chart.addLineSeries({{
color: '{color}',
lineWidth: {width},
}})''')
def set(self, data: pd.DataFrame):
"""
Sets the line data.\n
:param data: columns: date/time, price
:param data: columns: date/time, value
"""
self._lwc._set_line_data(self.id, data)
df = self._parent._df_datetime_format(data)
self._parent.run_script(f'{self.id}.setData({df.to_dict("records")})')
def update(self, series: pd.Series):
"""
Updates the line data.\n
:param series: labels: date/time, price
:param series: labels: date/time, value
"""
self._lwc._update_line_data(self.id, series)
series = self._parent._series_datetime_format(series)
self._parent.run_script(f'{self.id}.update({series.to_dict()})')
def marker(self): pass
class API:
@ -40,51 +45,34 @@ class API:
if isinstance(data['time'], int):
data['time'] = datetime.fromtimestamp(data['time'])
else:
data['time'] = datetime(data['time']['year'], data['time']['month'], data['time']['day'])
data['time'] = datetime.strptime(data['time'], '%Y-%m-%d')
click_func(data) if click_func else None
class LWC:
def __init__(self, volume_enabled: bool = True, inner_width: float = 1.0, inner_height: float = 1.0):
self.id = uuid4()
self.js_queue = []
self._volume_enabled = volume_enabled
self._inner_width = inner_width
self._inner_height = inner_height
self._position = 'left'
self.loaded = False
self._rand = IDGen()
self._chart_var = 'chart'
self.id = self._rand.generate()
self._append_js = f'document.body.append({self.id}.div)'
self._scripts = []
self._script_func = None
self._html = HTML
self._last_bar = None
self._interval = None
self._background_color = '#000000'
self._volume_up_color = 'rgba(83,141,131,0.8)'
self._volume_down_color = 'rgba(200,127,130,0.8)'
self._js_api = API()
self._js_api_code = ''
self.volume_enabled = volume_enabled
self.inner_width = inner_width
self.inner_height = inner_height
self._html = HTML.replace('__INNER_WIDTH__', str(self.inner_width)).replace('__INNER_HEIGHT__', str(self.inner_height))
self.last_bar = None
self.interval = None
self._lines: Dict[UUID, Line] = {}
self._subcharts: Dict[UUID, LWC] = {self.id: self}
self.background_color = '#000000'
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 _stored(self, func, *args, **kwargs):
if self.loaded:
return False
self.js_queue.append((func, args, kwargs))
return True
def _set_last_bar(self, bar: pd.Series): self.last_bar = bar
self._js_api_code = None
def _set_interval(self, df: pd.DataFrame):
df['time'] = pd.to_datetime(df['time'])
intervals = df['time'].diff()
counts = intervals.value_counts()
self.interval = counts.index[0]
common_interval = pd.to_datetime(df['time']).diff().value_counts()
self._interval = common_interval.index[0]
def _df_datetime_format(self, df: pd.DataFrame):
if 'date' in df.columns:
@ -101,81 +89,85 @@ class LWC:
def _datetime_format(self, string):
string = pd.to_datetime(string)
if self.interval != timedelta(days=1):
if self._interval != timedelta(days=1):
string = string.timestamp()
string = self.interval.total_seconds() * (string // self.interval.total_seconds())
string = self._interval.total_seconds() * (string // self._interval.total_seconds())
else:
string = string.strftime('%Y-%m-%d')
return string
def run_script(self, script): pass
def _on_js_load(self):
self.loaded = True
for script in self._scripts:
self.run_script(script)
def _create_chart(self):
self.run_script(f'''
{self.id} = makeChart({self._inner_width}, {self._inner_height})
{self.id}.div.style.float = "{self._position}"
{self._append_js}
window.addEventListener('resize', function() {{
{self.id}.chart.resize(window.innerWidth*{self.id}.scale.width, window.innerHeight*{self.id}.scale.height)
}});
''')
def run_script(self, script):
"""
For advanced users; evaluates JavaScript within the Webview.
"""
self._script_func(script) if self.loaded else self._scripts.append(script)
def set(self, df: pd.DataFrame):
"""
Sets the initial data for the chart.\n
:param df: columns: date/time, open, high, low, close, volume (if volume enabled).
"""
if self._stored('set', df):
return None
df = self._df_datetime_format(df)
self._set_last_bar(df.iloc[-1])
bars = df
if self.volume_enabled:
if 'volume' not in df:
bars = self._df_datetime_format(df)
self._last_bar = bars.iloc[-1]
if self._volume_enabled:
if 'volume' not in bars:
raise MissingColumn("Volume enabled, but 'volume' column was not found.")
volume = df.drop(columns=['open', 'high', 'low', 'close'])
volume = volume.rename(columns={'volume': 'value'})
volume['color'] = self.volume_down_color
volume.loc[df['close'] > df['open'], 'color'] = self.volume_up_color
self.run_script(f'{self._chart_var}.volumeSeries.setData({volume.to_dict(orient="records")})')
bars = df.drop(columns=['volume'])
volume = bars.drop(columns=['open', 'high', 'low', 'close']).rename(columns={'volume': 'value'})
volume['color'] = self._volume_down_color
volume.loc[bars['close'] > bars['open'], 'color'] = self._volume_up_color
self.run_script(f'{self.id}.volumeSeries.setData({volume.to_dict(orient="records")})')
bars = bars.drop(columns=['volume'])
bars = bars.to_dict(orient='records')
self.run_script(f'{self._chart_var}.series.setData({bars})')
self.run_script(f'{self.id}.series.setData({bars})')
def update(self, series, from_tick=False):
"""
Updates the data from a bar;
if series['time'] is the same time as the last bar, the last bar will be overwritten.\n
:param series: columns: date/time, open, high, low, close, volume (if volume enabled).
:param series: labels: date/time, open, high, low, close, volume (if volume enabled).
"""
if self._stored('update', series, from_tick):
return None
series = self._series_datetime_format(series) if not from_tick else series
self._set_last_bar(series)
if self.volume_enabled:
self._last_bar = series
if self._volume_enabled:
if 'volume' not in series:
raise MissingColumn("Volume enabled, but 'volume' column was not found.")
volume = series.drop(['open', 'high', 'low', 'close'])
volume = volume.rename({'volume': 'value'})
volume['color'] = self.volume_up_color if series['close'] > series['open'] else self.volume_down_color
self.run_script(f'{self._chart_var}.volumeSeries.update({volume.to_dict()})')
volume = series.drop(['open', 'high', 'low', 'close']).rename({'volume': 'value'})
volume['color'] = self._volume_up_color if series['close'] > series['open'] else self._volume_down_color
self.run_script(f'{self.id}.volumeSeries.update({volume.to_dict()})')
series = series.drop(['volume'])
dictionary = series.to_dict()
self.run_script(f'{self._chart_var}.series.update({dictionary})')
self.run_script(f'{self.id}.series.update({series.to_dict()})')
def update_from_tick(self, series):
"""
Updates the data from a tick.\n
:param series: columns: date/time, price, volume (if volume enabled).
:param series: labels: date/time, price, volume (if volume enabled).
"""
if self._stored('update_from_tick', series):
return None
series = self._series_datetime_format(series)
bar = pd.Series()
if series['time'] == self.last_bar['time']:
bar = self.last_bar
bar['high'] = max(self.last_bar['high'], series['price'])
bar['low'] = min(self.last_bar['low'], series['price'])
if series['time'] == self._last_bar['time']:
bar = self._last_bar
bar['high'] = max(self._last_bar['high'], series['price'])
bar['low'] = min(self._last_bar['low'], series['price'])
bar['close'] = series['price']
if self.volume_enabled:
if self._volume_enabled:
if 'volume' not in series:
raise MissingColumn("Volume enabled, but 'volume' column was not found.")
bar['volume'] = series['volume']
@ -189,53 +181,11 @@ class LWC:
def create_line(self, color: str = 'rgba(214, 237, 255, 0.6)', width: int = 2):
"""
Creates and returns a Line object.)\n
:return a Line object used to set/update the line.
"""
line_id = uuid4()
self._lines[line_id] = Line(self, line_id, color, width)
return self._lines[line_id]
return Line(self, color, width)
def _set_line_data(self, line_id, df: pd.DataFrame):
if self._stored('_set_line_data', line_id, df):
return None
line = self._lines[line_id]
if not line.loaded:
var = self._rand.generate()
self.run_script(f'''
let lineSeries{var} = {{
color: '{line.color}',
lineWidth: {line.width},
}};
let line{var} = {{
series: {self._chart_var}.chart.addLineSeries(lineSeries{var}),
id: '{line_id}',
}};
lines.push(line{var})
''')
line.loaded = True
df = self._df_datetime_format(df)
self.run_script(f'''
lines.forEach(function (line) {{
if ('{line_id}' === line.id) {{
line.series.setData({df.to_dict('records')})
}}
}})''')
def _update_line_data(self, line_id, series: pd.Series):
if self._stored('_update_line_data', line_id, series):
return None
series = self._series_datetime_format(series)
self.run_script(f'''
lines.forEach(function (line) {{
if ('{line_id}' === line.id) {{
line.series.update({series.to_dict()})
}}
}})''')
def marker(self, time: datetime = None, position: POSITION = 'below', shape: SHAPE = 'arrow_up',
color: str = '#2196F3', text: str = '', m_id: UUID = None) -> UUID:
def marker(self, time: datetime = None, position: MARKER_POSITION = 'below', shape: MARKER_SHAPE = 'arrow_up',
color: str = '#2196F3', text: str = '') -> str:
"""
Creates a new marker.\n
:param time: The time that the marker will be placed at. If no time is given, it will be placed at the last bar.
@ -243,66 +193,54 @@ class LWC:
:param color: The color of the marker (rgb, rgba or hex).
:param shape: The shape of the marker.
:param text: The text to be placed with the marker.
:return: The UUID of the marker placed.
:return: The id of the marker placed.
"""
_valid_color(color)
if not m_id:
m_id = uuid4()
if self._stored('marker', time, position, shape, color, text, m_id):
return m_id
try:
time = self.last_bar['time'] if not time else self._datetime_format(time)
time = self._last_bar['time'] if not time else self._datetime_format(time)
except TypeError:
raise TypeError('Chart marker created before data was set.')
time = time if isinstance(time, float) else f"'{time}'"
marker_id = self._rand.generate()
self.run_script(f"""
markers.push({{
time: {time},
position: '{_position(position)}',
color: '{color}', shape: '{_shape(shape)}',
time: {time if isinstance(time, float) else f"'{time}'"},
position: '{_marker_position(position)}',
color: '{color}',
shape: '{_marker_shape(shape)}',
text: '{text}',
id: '{m_id}'
id: '{marker_id}'
}});
{self._chart_var}.series.setMarkers(markers)""")
return m_id
{self.id}.series.setMarkers(markers)""")
return marker_id
def remove_marker(self, m_id: UUID):
def remove_marker(self, marker_id: str):
"""
Removes the marker with the given UUID.\n
Removes the marker with the given id.\n
"""
if self._stored('remove_marker', m_id):
return None
self.run_script(f'''
markers.forEach(function (marker) {{
if ('{m_id}' === marker.id) {{
if ('{marker_id}' === marker.id) {{
markers.splice(markers.indexOf(marker), 1)
{self._chart_var}.series.setMarkers(markers)
{self.id}.series.setMarkers(markers)
}}
}});''')
def horizontal_line(self, price: Union[float, int], color: str = 'rgb(122, 146, 202)', width: int = 1,
style: LINE_TYPE = 'solid', text: str = '', axis_label_visible=True):
style: LINE_STYLE = 'solid', text: str = '', axis_label_visible=True):
"""
Creates a horizontal line at the given price.\n
"""
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{var} = {{
price: {price},
color: '{color}',
lineWidth: {width},
lineStyle: LightweightCharts.LineStyle.{_line_type(style)},
axisLabelVisible: {'true' if axis_label_visible else 'false'},
lineStyle: {_line_style(style)},
axisLabelVisible: {_js_bool(axis_label_visible)},
title: '{text}',
}};
let line{var} = {{
line: {self._chart_var}.series.createPriceLine(priceLine{var}),
line: {self.id}.series.createPriceLine(priceLine{var}),
price: {price},
}};
horizontal_lines.push(line{var})""")
@ -311,30 +249,23 @@ class LWC:
"""
Removes a horizontal line at the given price.
"""
if self._stored('remove_horizontal_line', price):
return None
self.run_script(f'''
horizontal_lines.forEach(function (line) {{
if ({price} === line.price) {{
{self._chart_var}.series.removePriceLine(line.line);
{self.id}.series.removePriceLine(line.line);
horizontal_lines.splice(horizontal_lines.indexOf(line), 1)
}}
}});''')
def config(self, mode: PRICE_SCALE_MODE = None, title: str = None, right_padding: float = None):
def config(self, mode: PRICE_SCALE_MODE = 'normal', title: str = None, right_padding: float = None):
"""
:param mode: Chart price scale mode.
:param title: Last price label text.
:param right_padding: How many bars of empty space to the right of the last bar.
"""
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 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
self.run_script(f'{self.id}.chart.timeScale().scrollToPosition({right_padding}, false)') if right_padding is not None else None
self.run_script(f'{self.id}.series.applyOptions({{title: "{title}"}})') if title else None
self.run_script(f"{self.id}.chart.priceScale().applyOptions({{mode: {_price_scale_mode(mode)}}})")
def time_scale(self, visible: bool = True, time_visible: bool = True, seconds_visible: bool = False):
"""
@ -344,18 +275,12 @@ class LWC:
:param seconds_visible: Seconds visibility control.
:return:
"""
if self._stored('time_scale', visible, time_visible, seconds_visible):
return None
time_scale_visible = f'visible: {_js_bool(visible)},'
time = f'timeVisible: {_js_bool(time_visible)},'
seconds = f'secondsVisible: {_js_bool(seconds_visible)},'
self.run_script(f'''
{self._chart_var}.chart.applyOptions({{
{self.id}.chart.applyOptions({{
timeScale: {{
{time_scale_visible if visible is not None else ''}
{time if time_visible is not None else ''}
{seconds if seconds_visible is not None else ''}
visible: {_js_bool(visible)},
timeVisible: {_js_bool(time_visible)},
secondsVisible: {_js_bool(seconds_visible)},
}}
}})''')
@ -364,44 +289,38 @@ class LWC:
"""
Global layout options for the chart.
"""
if self._stored('layout', background_color, text_color, font_size, font_family):
return None
self.background_color = background_color if background_color else self.background_color
self._background_color = background_color if background_color else self._background_color
self.run_script(f"""
document.body.style.backgroundColor = '{self.background_color}'
{self._chart_var}.chart.applyOptions({{
document.body.style.backgroundColor = '{self._background_color}'
{self.id}.chart.applyOptions({{
layout: {{
{f'backgroundColor: "{background_color}",' if background_color else ''}
background: {{
color: "{self._background_color}",
}},
{f'textColor: "{text_color}",' if text_color else ''}
{f'fontSize: {font_size},' if font_size 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'):
def grid(self, vert_enabled: bool = True, horz_enabled: bool = True, color: str = 'rgba(29, 30, 38, 5)', style: LINE_STYLE = '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({{
{self.id}.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 ''}
vertLines: {{
visible: {_js_bool(vert_enabled)},
color: "{color}",
style: {_line_style(style)},
}},
horzLines: {{
visible: {_js_bool(horz_enabled)},
color: "{color}",
style: {_line_style(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 = '',
@ -409,20 +328,16 @@ class LWC:
"""
Candle styling for each of its parts.
"""
if self._stored('candle_style', up_color, down_color, wick_enabled, border_enabled,
border_up_color, border_down_color, wick_up_color, wick_down_color):
return None
self.run_script(f"""
{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 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 ''}
{f'wickDownColor: "{wick_down_color}",' if wick_down_color else ''}
{self.id}.series.applyOptions({{
upColor: "{up_color}",
downColor: "{down_color}",
wickVisible: {_js_bool(wick_enabled)},
borderVisible: {_js_bool(border_enabled)},
{f'borderUpColor: "{border_up_color}",' if border_up_color else up_color if border_enabled else ''}
{f'borderDownColor: "{border_down_color}",' if border_down_color else down_color if border_enabled else ''}
{f'wickUpColor: "{wick_up_color}",' if wick_up_color else wick_up_color if wick_enabled else ''}
{f'wickDownColor: "{wick_down_color}",' if wick_down_color else wick_down_color if wick_enabled else ''}
}})""")
def volume_config(self, scale_margin_top: float = 0.8, scale_margin_bottom: float = 0.0,
@ -436,13 +351,10 @@ class LWC:
:param up_color: Volume color for upward direction (rgb, rgba or hex)
:param down_color: Volume color for downward direction (rgb, rgba or hex)
"""
if self._stored('volume_config', scale_margin_top, scale_margin_bottom, up_color, down_color):
return None
self.volume_up_color = up_color if up_color else self.volume_up_color
self.volume_down_color = down_color if down_color else self.volume_down_color
self._volume_up_color = up_color if up_color else self._volume_up_color
self._volume_down_color = down_color if down_color else self._volume_down_color
self.run_script(f'''
{self._chart_var}.volumeSeries.priceScale().applyOptions({{
{self.id}.volumeSeries.priceScale().applyOptions({{
scaleMargins: {{
top: {scale_margin_top},
bottom: {scale_margin_bottom},
@ -450,40 +362,35 @@ class LWC:
}})''')
def crosshair(self, mode: CROSSHAIR_MODE = 'normal', vert_width: int = 1, vert_color: str = None,
vert_style: LINE_TYPE = None, vert_label_background_color: str = None, horz_width: int = 1,
horz_color: str = None, horz_style: LINE_TYPE = None, horz_label_background_color: str = None):
vert_style: LINE_STYLE = 'dashed', vert_label_background_color: str = 'rgb(46, 46, 46)', horz_width: int = 1,
horz_color: str = None, horz_style: LINE_STYLE = 'dashed', horz_label_background_color: str = 'rgb(55, 55, 55)'):
"""
Crosshair formatting for its vertical and horizontal axes.
"""
if self._stored('crosshair', mode, vert_width, vert_color, vert_style, vert_label_background_color,
horz_width, horz_color, horz_style, horz_label_background_color):
return None
args = f"LightweightCharts.CrosshairMode.{_crosshair_mode(mode)}", \
f"{vert_width}}}", f"'{vert_color}'}}", f"LightweightCharts.LineStyle.{_line_type(vert_style)}}}",\
f"'{vert_label_background_color}'}}", \
f"{horz_width}}}", f"'{horz_color}'}}", f"LightweightCharts.LineStyle.{_line_type(horz_style)}}}",\
f"'{horz_label_background_color}'}}"
for key, arg in zip(
('mode', 'vertLine: {width', 'vertLine: {color', 'vertLine: {style', 'vertLine: {labelBackgroundColor',
'horzLine: {width', 'horzLine: {color', 'horzLine: {style', 'horzLine: {labelBackgroundColor'), args):
if 'None' in arg:
continue
self.run_script(f'''
{self._chart_var}.chart.applyOptions({{
{self.id}.chart.applyOptions({{
crosshair: {{
{key}: {arg}
mode: {_crosshair_mode(mode)},
vertLine: {{
width: {vert_width},
{f'color: "{vert_color}",' if vert_color else ''}
style: {_line_style(vert_style)},
labelBackgroundColor: "{vert_label_background_color}"
}},
horzLine: {{
width: {horz_width},
{f'color: "{horz_color}",' if horz_color else ''}
style: {_line_style(horz_style)},
labelBackgroundColor: "{horz_label_background_color}"
}}
}}}})''')
def watermark(self, text: str, font_size: int = 44, color: str = 'rgba(180, 180, 200, 0.5)'):
"""
Adds a watermark to the chart.
"""
if self._stored('watermark', text, font_size, color):
return None
self.run_script(f'''
{self._chart_var}.chart.applyOptions({{
{self.id}.chart.applyOptions({{
watermark: {{
visible: true,
fontSize: {font_size},
@ -499,18 +406,15 @@ class LWC:
"""
Configures the legend of the chart.
"""
if self._stored('legend', visible, ohlc, percent, color, font_size, font_family):
return None
if visible:
self.run_script(f'''
{f"{self._chart_var}.legend.style.color = '{color}'" if color else ''}
{f"{self._chart_var}.legend.style.fontSize = {font_size}" if font_size else ''}
{f"{self._chart_var}.legend.style.fontFamily = '{font_family}'" if font_family else ''}
{f"{self.id}.legend.style.color = '{color}'" if color else ''}
{f"{self.id}.legend.style.fontSize = {font_size}" if font_size else ''}
{f"{self.id}.legend.style.fontFamily = '{font_family}'" if font_family else ''}
{self._chart_var}.chart.subscribeCrosshairMove((param) => {{
{self.id}.chart.subscribeCrosshairMove((param) => {{
if (param.time){{
const data = param.seriesPrices.get({self._chart_var}.series);
const data = param.seriesData.get({self.id}.series);
if (!data) {{return}}
let percentMove = ((data.close-data.open)/data.open)*100
let ohlc = `open: ${{legendItemFormat(data.open)}}
@ -521,44 +425,38 @@ class LWC:
let finalString = ''
{'finalString += ohlc' if ohlc else ''}
{'finalString += percent' if percent else ''}
{self._chart_var}.legend.innerHTML = finalString
{self.id}.legend.innerHTML = finalString
}}
else {{
{self._chart_var}.legend.innerHTML = ''
{self.id}.legend.innerHTML = ''
}}
}});''')
def subscribe_click(self, function: object):
if self._stored('subscribe_click', function):
return None
self._js_api.click_funcs[str(self.id)] = function
var = self._rand.generate()
"""
Subscribes the given function to a chart click event.
The event returns a dictionary containing the bar object at the time clicked, and the price at the crosshair.
"""
self._js_api.click_funcs[self.id] = function
self.run_script(f'''
{self._chart_var}.chart.subscribeClick((param) => {{
{self.id}.chart.subscribeClick((param) => {{
if (!param.point) {{return}}
let prices{var} = param.seriesPrices.get({self._chart_var}.series);
let data{var} = {{
let prices = param.seriesData.get({self.id}.series);
let data = {{
time: param.time,
open: prices{var}.open,
high: prices{var}.high,
low: prices{var}.low,
close: prices{var}.close,
open: prices.open,
high: prices.high,
low: prices.low,
close: prices.close,
hover: {self.id}.series.coordinateToPrice(param.point.y),
id: '{self.id}'
}}
{self._js_api_code}(data{var})
{self._js_api_code}(data)
}})''')
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_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
width: float = 0.5, height: float = 0.5, sync: Union[bool, str] = False):
return SubChart(self, volume_enabled, position, width, height, sync)
class SubChart(LWC):
@ -566,207 +464,96 @@ class SubChart(LWC):
super().__init__(volume_enabled, width, height)
self._chart = parent._chart if isinstance(parent, SubChart) else parent
self._parent = parent
self._position = position
self._rand = self._chart._rand
self._chart_var = f'window.{self._rand.generate()}'
self.id = f'window.{self._rand.generate()}'
self._append_js = f'{self._parent.id}.div.parentNode.insertBefore({self.id}.div, {self._parent.id}.div.nextSibling)'
self._js_api = self._chart._js_api
self._js_api_code = self._chart._js_api_code
self.position = position
self._create_panel(sync)
def _stored(self, func, *args, **kwargs):
if self._chart.loaded:
return False
self._chart.js_queue.append((f'SUB{func}', (self.id,)+args, kwargs))
return True
self._create_chart()
if not sync:
return
sync_parent_var = self._parent.id if isinstance(sync, bool) else sync
self.run_script(f'''
{sync_parent_var}.chart.timeScale().subscribeVisibleLogicalRangeChange((timeRange) => {{
{self.id}.chart.timeScale().setVisibleLogicalRange(timeRange)
}});''')
def run_script(self, script): self._chart.run_script(script)
def _create_panel(self, sync):
if self._stored('_create_panel', sync):
return None
parent_div = 'chartsDiv' if self._parent._chart_var == 'chart' else self._parent._chart_var+'div'
sub_sync = ''
if sync:
sync_parent_var = self._chart._subcharts[sync]._chart_var if isinstance(sync, UUID) else self._parent._chart_var
sub_sync = f'''
{sync_parent_var}.chart.timeScale().subscribeVisibleLogicalRangeChange((timeRange) => {{
{self._chart_var}.chart.timeScale().setVisibleLogicalRange(timeRange)
}});
'''
self.run_script(f'''
{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'
{self._chart_var} = {{}}
{self._chart_var}.scale = {{
width: {self.inner_width},
height: {self.inner_height}
}}
{self._chart_var}.chart = makeChart(window.innerWidth*{self._chart_var}.scale.width,
window.innerHeight*{self._chart_var}.scale.height, {self._chart_var}div)
{self._chart_var}.series = makeCandlestickSeries({self._chart_var}.chart)
{self._chart_var}.volumeSeries = makeVolumeSeries({self._chart_var}.chart)
{self._chart_var}.legend = document.createElement('div')
{self._chart_var}.legend.style.position = 'absolute'
{self._chart_var}.legend.style.zIndex = 1000
{self._chart_var}.legend.style.width = '{(self.inner_width*100)-8}vw'
{self._chart_var}.legend.style.top = '10px'
{self._chart_var}.legend.style.left = '10px'
{self._chart_var}.legend.style.fontFamily = 'Monaco'
{self._chart_var}.legend.style.fontSize = '11px'
{self._chart_var}.legend.style.color = 'rgb(191, 195, 203)'
{self._chart_var}div.appendChild({self._chart_var}.legend)
{parent_div}.parentNode.insertBefore({self._chart_var}div, {parent_div}.nextSibling)
charts.push({self._chart_var})
{self._chart_var}.chart.priceScale('').applyOptions({{
scaleMargins: {{
top: 0.8,
bottom: 0,
}}
}});
{sub_sync}
''')
class PyWebViewSubChart(SubChart):
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
SCRIPT = """
document.body.style.backgroundColor = '#000000'
const markers = []
const horizontal_lines = []
const lines = []
const charts = []
const up = 'rgba(39, 157, 130, 100)'
const down = 'rgba(200, 97, 100, 100)'
function makeChart(width, height, div) {
return LightweightCharts.createChart(div, {
width: width,
height: height,
function makeChart(innerWidth, innerHeight) {
let chart = {}
chart.scale = {
width: innerWidth,
height: innerHeight
}
chart.div = document.createElement('div')
chart.chart = LightweightCharts.createChart(chart.div, {
width: window.innerWidth*innerWidth,
height: window.innerHeight*innerHeight,
layout: {
textColor: '#d1d4dc',
backgroundColor: '#000000',
fontSize: 12,
background: {
color:'#000000',
type: LightweightCharts.ColorType.Solid,
},
fontSize: 12
},
rightPriceScale: {
scaleMargins: {
top: 0.3,
bottom: 0.25,
},
},
timeScale: {
timeVisible: true,
secondsVisible: false,
scaleMargins: {top: 0.3, bottom: 0.25},
},
timeScale: {timeVisible: true, secondsVisible: false},
crosshair: {
mode: LightweightCharts.CrosshairMode.Normal,
vertLine: {
labelBackgroundColor: 'rgb(46, 46, 46)'
},
horzLine: {
labelBackgroundColor: 'rgb(55, 55, 55)'
}
},
grid: {
vertLines: {
color: 'rgba(29, 30, 38, 5)',
},
horzLines: {
color: 'rgba(29, 30, 58, 5)',
},
},
handleScroll: {
vertTouchDrag: true,
vertLines: {color: 'rgba(29, 30, 38, 5)'},
horzLines: {color: 'rgba(29, 30, 58, 5)'},
},
handleScroll: {vertTouchDrag: true},
})
}
function makeCandlestickSeries(chart){
return chart.addCandlestickSeries({
color: 'rgb(0, 120, 255)',
upColor: up,
borderUpColor: up,
wickUpColor: up,
downColor: down,
borderDownColor: down,
wickDownColor: down,
lineWidth: 2,
chart.series = chart.chart.addCandlestickSeries({color: 'rgb(0, 120, 255)', upColor: up, borderUpColor: up, wickUpColor: up,
downColor: down, borderDownColor: down, wickDownColor: down, lineWidth: 2,
})
}
function makeVolumeSeries(chart) {
return chart.addHistogramSeries({
chart.volumeSeries = chart.chart.addHistogramSeries({
color: '#26a69a',
priceFormat: {
type: 'volume',
},
priceFormat: {type: 'volume'},
priceScaleId: '',
});
})
chart.legend = document.createElement('div')
chart.legend.style.position = 'absolute'
chart.legend.style.zIndex = 1000
chart.legend.style.width = `${(chart.scale.width*100)-8}vw`
chart.legend.style.top = '10px'
chart.legend.style.left = '10px'
chart.legend.style.fontFamily = 'Monaco'
chart.legend.style.fontSize = '11px'
chart.legend.style.color = 'rgb(191, 195, 203)'
chart.div.appendChild(chart.legend)
chart.chart.priceScale('').applyOptions({
scaleMargins: {top: 0.8, bottom: 0}
});
return chart
}
const chartsDiv = document.createElement('div')
var 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)
chart.legend = document.createElement('div')
chart.legend.style.position = 'absolute'
chart.legend.style.zIndex = 1000
chart.legend.style.width = `${(chart.scale.width*100)-8}vw`
chart.legend.style.top = '10px'
chart.legend.style.left = '10px'
chart.legend.style.fontFamily = 'Monaco'
chart.legend.style.fontSize = '11px'
chart.legend.style.color = 'rgb(191, 195, 203)'
document.body.appendChild(chart.legend)
chart.chart.priceScale('').applyOptions({
scaleMargins: {
top: 0.8,
bottom: 0,
}
});
window.addEventListener('resize', function() {
let width = window.innerWidth;
let height = window.innerHeight;
chart.chart.resize(width*chart.scale.width, height*chart.scale.height)
charts.forEach(function (subchart) {{
subchart.chart.resize(width*subchart.scale.width, height*subchart.scale.height)
}});
});
function legendItemFormat(num) {
return num.toFixed(2).toString().padStart(8, ' ')
return num.toFixed(2).toString().padStart(8, ' ')
}
"""
HTML = f"""
@ -774,7 +561,7 @@ HTML = f"""
<html lang="">
<head>
<title>lightweight-charts-python</title>
<script>{LWC_3_5_0}</script>
<script>{LWC_4_0_1}</script>
<meta name="viewport" content ="width=device-width, initial-scale=1">
<style>
body {{
@ -785,9 +572,8 @@ HTML = f"""
</style>
</head>
<body>
<div id="chart"></div>
<script>
{SCRIPT}
</script>
<script>
{SCRIPT}
</script>
</body>
</html>"""

File diff suppressed because one or more lines are too long

View File

@ -1,82 +0,0 @@
from typing import Literal, Union
from uuid import UUID
import webview
from multiprocessing import Queue
from lightweight_charts.js import LWC
_q = Queue()
_result_q = Queue()
class Webview(LWC):
def __init__(self, chart):
super().__init__(chart.volume_enabled, chart.inner_width, chart.inner_height)
self.chart = chart
self.started = False
self._js_api_code = 'pywebview.api.onClick'
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
def run_script(self, script): self.webview.evaluate_js(script)
def _on_js_load(self):
self.loaded = True
while len(self.js_queue) > 0:
func, args, kwargs = self.js_queue[0]
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)
del self.js_queue[0]
_loop(self.chart, controller=self)
def show(self):
if self.loaded:
self.webview.show()
else:
webview.start(debug=self.chart.debug)
def hide(self): self.webview.hide()
def exit(self):
self.webview.destroy()
del self
def create_line(self, color: str = 'rgba(214, 237, 255, 0.6)', width: int = 2):
return super().create_line(color, width).id
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):
wv = Webview(chart) if not controller else controller
chart._result_q.put(wv.id)
while 1:
obj = wv
func, args = chart._q.get()
if 'SUB' in func:
obj = obj._subcharts[args[0]]
args = args[1:]
func = func.replace('SUB', '')
try:
result = getattr(obj, func)(*args)
except KeyError as e:
return
if func == 'show':
chart._exit.set()
elif func == 'exit':
chart._exit.set()
chart._result_q.put(result) if result is not None else None

View File

@ -21,71 +21,56 @@ class ColorError(ValueError):
return f'{self.msg}'
class IDGen(list):
def generate(self):
var = ''.join(choices(ascii_lowercase, k=8))
if var not in self:
self.append(var)
return var
self.generate()
def _valid_color(string):
if string[:3] == 'rgb' or string[:4] == 'rgba' or string[0] == '#':
return True
raise ColorError('Colors must be in the format of either rgb, rgba or hex.')
LINE_TYPE = Literal['solid', 'dotted', 'dashed', 'large_dashed', 'sparse_dotted']
def _js_bool(b: bool): return 'true' if b is True else 'false' if b is False else None
POSITION = Literal['above', 'below', 'inside']
SHAPE = Literal['arrow_up', 'arrow_down', 'circle', 'square']
LINE_STYLE = Literal['solid', 'dotted', 'dashed', 'large_dashed', 'sparse_dotted']
MARKER_POSITION = Literal['above', 'below', 'inside']
MARKER_SHAPE = Literal['arrow_up', 'arrow_down', 'circle', 'square']
CROSSHAIR_MODE = Literal['normal', 'magnet']
PRICE_SCALE_MODE = Literal['normal', 'logarithmic', 'percentage', 'index100']
def _line_type(lt: LINE_TYPE):
return {
'solid': 'Solid',
'dotted': 'Dotted',
'dashed': 'Dashed',
'large_dashed': 'LargeDashed',
'sparse_dotted': 'SparseDotted',
None: None,
}[lt]
def _line_style(line: LINE_STYLE):
js = 'LightweightCharts.LineStyle.'
return js+line[:line.index('_')].title() + line[line.index('_') + 1:].title() if '_' in line else js+line.title()
def _position(p: POSITION):
def _crosshair_mode(mode: CROSSHAIR_MODE):
return f'LightweightCharts.CrosshairMode.{mode.title()}' if mode else None
def _price_scale_mode(mode: PRICE_SCALE_MODE):
return f"LightweightCharts.PriceScaleMode.{'IndexedTo100' if mode == 'index100' else mode.title() if mode else None}"
def _marker_shape(shape: MARKER_SHAPE):
return shape[:shape.index('_')]+shape[shape.index('_')+1:].title() if '_' in shape else shape.title()
def _marker_position(p: MARKER_POSITION):
return {
'above': 'aboveBar',
'below': 'belowBar',
'inside': 'inBar',
None: None,
}[p]
def _shape(shape: SHAPE):
return {
'arrow_up': 'arrowUp',
'arrow_down': 'arrowDown',
'circle': 'Circle',
'square': 'Square',
None: None,
}[shape]
def _crosshair_mode(mode: CROSSHAIR_MODE): return mode.title() if mode else None
def _js_bool(b: bool): return 'true' if b is True else 'false' if b is False else None
def _price_scale_mode(mode: PRICE_SCALE_MODE):
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

@ -21,24 +21,16 @@ class WxChart(LWC):
super().__init__(volume_enabled, inner_width=inner_width, inner_height=inner_height)
self.webview.AddScriptMessageHandler('wx_msg')
self._script_func = self.webview.RunScript
self._js_api_code = 'window.wx_msg.postMessage'
self.webview.Bind(wx.html2.EVT_WEBVIEW_SCRIPT_MESSAGE_RECEIVED, lambda e: self._js_api.onClick(eval(e.GetString())))
self.webview.AddScriptMessageHandler('wx_msg')
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._create_chart()
def run_script(self, script): self.webview.RunScript(script)
def _on_js_load(self, e):
self.loaded = True
for func, args, kwargs in self.js_queue:
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 _on_js_load(self, e): super()._on_js_load()
def get_webview(self): return self.webview
@ -49,18 +41,13 @@ class QtChart(LWC):
self.webview = QWebEngineView(widget)
except NameError:
raise ModuleNotFoundError('QWebEngineView was not found, and must be installed to use QtChart.')
super().__init__(volume_enabled, inner_width=inner_width, inner_height=inner_height)
self._script_func = self.webview.page().runJavaScript
self.webview.loadFinished.connect(self._on_js_load)
self.webview.page().setHtml(self._html)
def run_script(self, script): self.webview.page().runJavaScript(script)
def _on_js_load(self):
self.loaded = True
for func, args, kwargs in self.js_queue:
getattr(super(), func)(*args, **kwargs)
self._create_chart()
def get_webview(self): return self.webview

View File

@ -5,7 +5,7 @@ with open('README.md', 'r', encoding='utf-8') as f:
setup(
name='lightweight_charts',
version='1.0.6',
version='1.0.7',
packages=find_packages(),
python_requires='>=3.9',
install_requires=[