- 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) [![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/). 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) 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: ### 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: ### 3. Updating bars from tick data in real-time:
@ -106,7 +106,7 @@ if __name__ == '__main__':
sleep(0.3) 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: ### 4. Line Indicators:
@ -140,7 +140,7 @@ if __name__ == '__main__':
chart.show(block=True) 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: ### 5. Styling:
@ -177,7 +177,7 @@ if __name__ == '__main__':
chart.show(block=True) 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: ### 6. Callbacks:
@ -203,7 +203,7 @@ if __name__ == '__main__':
chart.show(block=True) 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) [![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._ _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' project = 'lightweight-charts-python'
copyright = '2023, louisnw' copyright = '2023, louisnw'
author = 'louisnw' author = 'louisnw'
release = '1.0.3' release = '1.0.7'
extensions = ["myst_parser"] 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) [![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) [![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) [![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 ## Common Methods
These methods can be used within the `Chart`, `SubChart`, `QtChart`, and `WxChart` objects. 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` ### `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. If the `time` parameter is not given, the marker will be placed at the latest bar.
___ ___
### `remove_marker` ### `remove_marker`
`m_id: UUID` `marker_id: str`
Removes the marker with the given UUID. Removes the marker with the given id.
Usage: Usage:
```python ```python
@ -162,13 +162,13 @@ ___
Subscribes the given function to a chart 'click' event. 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` ### `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`. 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%) `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`. `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.
Chart `UUID`'s can be accessed from the`chart.id` and `subchart.id` attributes.
```{important} ```{important}
`width` and `height` must be given as a number between 0 and 1. `width` and `height` must be given as a number between 0 and 1.

View File

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

View File

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

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}' 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): def _valid_color(string):
if string[:3] == 'rgb' or string[:4] == 'rgba' or string[0] == '#': if string[:3] == 'rgb' or string[:4] == 'rgba' or string[0] == '#':
return True return True
raise ColorError('Colors must be in the format of either rgb, rgba or hex.') 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'] CROSSHAIR_MODE = Literal['normal', 'magnet']
PRICE_SCALE_MODE = Literal['normal', 'logarithmic', 'percentage', 'index100'] PRICE_SCALE_MODE = Literal['normal', 'logarithmic', 'percentage', 'index100']
def _line_type(lt: LINE_TYPE): def _line_style(line: LINE_STYLE):
return { js = 'LightweightCharts.LineStyle.'
'solid': 'Solid', return js+line[:line.index('_')].title() + line[line.index('_') + 1:].title() if '_' in line else js+line.title()
'dotted': 'Dotted',
'dashed': 'Dashed',
'large_dashed': 'LargeDashed',
'sparse_dotted': 'SparseDotted',
None: None,
}[lt]
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 { return {
'above': 'aboveBar', 'above': 'aboveBar',
'below': 'belowBar', 'below': 'belowBar',
'inside': 'inBar', 'inside': 'inBar',
None: None, None: None,
}[p] }[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) 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._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.Bind(wx.html2.EVT_WEBVIEW_LOADED, self._on_js_load)
self.webview.SetPage(self._html, '') self.webview.SetPage(self._html, '')
self._create_chart()
def run_script(self, script): self.webview.RunScript(script) def _on_js_load(self, e): super()._on_js_load()
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 get_webview(self): return self.webview def get_webview(self): return self.webview
@ -49,18 +41,13 @@ class QtChart(LWC):
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, inner_width=inner_width, inner_height=inner_height) 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.loadFinished.connect(self._on_js_load)
self.webview.page().setHtml(self._html) self.webview.page().setHtml(self._html)
self._create_chart()
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)
def get_webview(self): return self.webview def get_webview(self): return self.webview

View File

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