New Feature: Multi-Pane Charts
- Added the create_subchart method to Chart. - Added the SubChart class. - Added an inner_width and inner_height parameter to Chart. - The time_scale method can now disable the time scale completely. Bugs: - Fixed a bug which prevented markers from being placed on charts with a timescale less than a day.
This commit is contained in:
@ -51,7 +51,7 @@ The provided ticks do not need to be rounded to an interval (1 min, 5 min etc.),
|
|||||||
___
|
___
|
||||||
|
|
||||||
### `create_line`
|
### `create_line`
|
||||||
`color: str` | `width: int`
|
`color: str` | `width: int` | `-> Line`
|
||||||
|
|
||||||
Creates and returns a [Line](#line) object.
|
Creates and returns a [Line](#line) object.
|
||||||
___
|
___
|
||||||
@ -159,6 +159,24 @@ 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, with the keys:
|
||||||
|
|
||||||
`time | open | high | low | close`
|
`time | open | high | low | close`
|
||||||
|
___
|
||||||
|
|
||||||
|
### `create_subchart`
|
||||||
|
`volume_enabled: bool` | `position: 'left'/'right'/'top'/'bottom'`, `width: float` | `height: float` | `sync: bool/UUID` | `-> SubChart`
|
||||||
|
|
||||||
|
Creates and returns a [SubChart](#subchart) object, placing it adjacent to the declaring `Chart` or `SubChart`.
|
||||||
|
|
||||||
|
`position`: specifies how the `SubChart` will float within the `Chart` window.
|
||||||
|
|
||||||
|
`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.
|
||||||
|
|
||||||
|
```{important}
|
||||||
|
`width` and `height` must be given as a number between 0 and 1.
|
||||||
|
```
|
||||||
|
|
||||||
___
|
___
|
||||||
|
|
||||||
@ -209,9 +227,72 @@ ___
|
|||||||
Updates the data for the line.
|
Updates the data for the line.
|
||||||
|
|
||||||
This should be given as a Series object, with labels akin to the `line.set()` function.
|
This should be given as a Series object, with labels akin to the `line.set()` function.
|
||||||
|
|
||||||
___
|
___
|
||||||
|
|
||||||
|
## `SubChart`
|
||||||
|
|
||||||
|
The `SubChart` object allows for the use of multiple chart panels within the same `Chart` window. All of the [Common Methods](#common-methods) can be used within a `SubChart`.
|
||||||
|
|
||||||
|
`SubCharts` are arranged horizontally from left to right. When the available space is no longer sufficient, the subsequent `SubChart` will be positioned on a new row, starting from the left side.
|
||||||
|
___
|
||||||
|
|
||||||
|
### Grid of 4 Example:
|
||||||
|
```python
|
||||||
|
import pandas as pd
|
||||||
|
from lightweight_charts import Chart
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
|
||||||
|
chart = Chart(inner_width=0.5, inner_height=0.5)
|
||||||
|
|
||||||
|
chart2 = chart.create_subchart(position='right', width=0.5, height=0.5)
|
||||||
|
|
||||||
|
chart3 = chart2.create_subchart(position='left', width=0.5, height=0.5)
|
||||||
|
|
||||||
|
chart4 = chart3.create_subchart(position='right', width=0.5, height=0.5)
|
||||||
|
|
||||||
|
chart.watermark('1')
|
||||||
|
chart2.watermark('2')
|
||||||
|
chart3.watermark('3')
|
||||||
|
chart4.watermark('4')
|
||||||
|
|
||||||
|
df = pd.read_csv('ohlcv.csv')
|
||||||
|
chart.set(df)
|
||||||
|
chart2.set(df)
|
||||||
|
chart3.set(df)
|
||||||
|
chart4.set(df)
|
||||||
|
|
||||||
|
chart.show(block=True)
|
||||||
|
|
||||||
|
```
|
||||||
|
___
|
||||||
|
|
||||||
|
### Synced Line Chart Example:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import pandas as pd
|
||||||
|
from lightweight_charts import Chart
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
|
||||||
|
chart = Chart(inner_width=1, inner_height=0.8)
|
||||||
|
|
||||||
|
chart2 = chart.create_subchart(width=1, height=0.2, sync=True, volume_enabled=False)
|
||||||
|
chart2.time_scale(visible=False)
|
||||||
|
|
||||||
|
df = pd.read_csv('ohlcv.csv')
|
||||||
|
df2 = pd.read_csv('rsi.csv')
|
||||||
|
|
||||||
|
chart.set(df)
|
||||||
|
line = chart2.create_line()
|
||||||
|
line.set(df2)
|
||||||
|
|
||||||
|
chart.show(block=True)
|
||||||
|
|
||||||
|
```
|
||||||
|
___
|
||||||
|
|
||||||
|
|
||||||
## `QtChart`
|
## `QtChart`
|
||||||
`widget: QWidget` | `volume_enabled: bool`
|
`widget: QWidget` | `volume_enabled: bool`
|
||||||
|
|
||||||
@ -222,7 +303,10 @@ ___
|
|||||||
|
|
||||||
`-> QWebEngineView`
|
`-> QWebEngineView`
|
||||||
|
|
||||||
Returns the `QWebEngineView` object. For example:
|
Returns the `QWebEngineView` object.
|
||||||
|
|
||||||
|
___
|
||||||
|
### Example:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
@ -263,7 +347,10 @@ ___
|
|||||||
### `get_webview`
|
### `get_webview`
|
||||||
`-> wx.html2.WebView`
|
`-> wx.html2.WebView`
|
||||||
|
|
||||||
Returns a `wx.html2.WebView` object which can be used to for positioning and styling within wxPython. For example:
|
Returns a `wx.html2.WebView` object which can be used to for positioning and styling within wxPython.
|
||||||
|
___
|
||||||
|
|
||||||
|
### Example:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
import wx
|
import wx
|
||||||
|
|||||||
@ -4,7 +4,7 @@ from lightweight_charts import Chart
|
|||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|
||||||
chart = Chart(debug=True)
|
chart = Chart()
|
||||||
|
|
||||||
df = pd.read_csv('ohlcv.csv')
|
df = pd.read_csv('ohlcv.csv')
|
||||||
|
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import pandas as pd
|
|||||||
import multiprocessing as mp
|
import multiprocessing as mp
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Union
|
from typing import Union, Literal
|
||||||
|
|
||||||
from lightweight_charts.pywebview import _loop
|
from lightweight_charts.pywebview import _loop
|
||||||
from lightweight_charts.util import LINE_TYPE, POSITION, SHAPE, CROSSHAIR_MODE, PRICE_SCALE_MODE
|
from lightweight_charts.util import LINE_TYPE, POSITION, SHAPE, CROSSHAIR_MODE, PRICE_SCALE_MODE
|
||||||
@ -31,7 +31,7 @@ class Line:
|
|||||||
|
|
||||||
class Chart:
|
class Chart:
|
||||||
def __init__(self, volume_enabled: bool = True, width: int = 800, height: int = 600, x: int = None, y: int = None,
|
def __init__(self, volume_enabled: bool = True, width: int = 800, height: int = 600, x: int = None, y: int = None,
|
||||||
on_top: bool = False, debug: bool = False):
|
on_top: bool = False, debug: bool = False, sub: bool = False, inner_width=1, inner_height=1):
|
||||||
self.debug = debug
|
self.debug = debug
|
||||||
self.volume_enabled = volume_enabled
|
self.volume_enabled = volume_enabled
|
||||||
self.width = width
|
self.width = width
|
||||||
@ -39,16 +39,17 @@ class Chart:
|
|||||||
self.x = x
|
self.x = x
|
||||||
self.y = y
|
self.y = y
|
||||||
self.on_top = on_top
|
self.on_top = on_top
|
||||||
|
self.inner_width = inner_width
|
||||||
|
self.inner_height = inner_height
|
||||||
|
|
||||||
|
if sub:
|
||||||
|
return
|
||||||
self._q = mp.Queue()
|
self._q = mp.Queue()
|
||||||
self._result_q = mp.Queue()
|
self._result_q = mp.Queue()
|
||||||
self._exit = mp.Event()
|
self._exit = mp.Event()
|
||||||
|
self._process = mp.Process(target=_loop, args=(self,), daemon=True)
|
||||||
try:
|
self._process.start()
|
||||||
self._process = mp.Process(target=_loop, args=(self,), daemon=True)
|
self.id = self._result_q.get()
|
||||||
self._process.start()
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def _go(self, func, *args): self._q.put((func, args))
|
def _go(self, func, *args): self._q.put((func, args))
|
||||||
|
|
||||||
@ -161,14 +162,15 @@ class Chart:
|
|||||||
"""
|
"""
|
||||||
self._go('config', mode, title, right_padding)
|
self._go('config', mode, title, right_padding)
|
||||||
|
|
||||||
def time_scale(self, time_visible: bool = True, seconds_visible: bool = False):
|
def time_scale(self, visible: bool = True, time_visible: bool = True, seconds_visible: bool = False):
|
||||||
"""
|
"""
|
||||||
Options for the time scale of the chart.
|
Options for the time scale of the chart.
|
||||||
|
:param visible: Time scale visibility control.
|
||||||
:param time_visible: Time visibility control.
|
:param time_visible: Time visibility control.
|
||||||
:param seconds_visible: Seconds visibility control
|
:param seconds_visible: Seconds visibility control
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
self._go('time_scale', time_visible, seconds_visible)
|
self._go('time_scale', visible, time_visible, seconds_visible)
|
||||||
|
|
||||||
def layout(self, background_color: str = None, text_color: str = None, font_size: int = None,
|
def layout(self, background_color: str = None, text_color: str = None, font_size: int = None,
|
||||||
font_family: str = None):
|
font_family: str = None):
|
||||||
@ -227,3 +229,29 @@ class Chart:
|
|||||||
The event returns a dictionary containing the bar object at the time clicked.
|
The event returns a dictionary containing the bar object at the time clicked.
|
||||||
"""
|
"""
|
||||||
self._go('subscribe_click', function)
|
self._go('subscribe_click', function)
|
||||||
|
|
||||||
|
def create_subchart(self, volume_enabled: bool = True, position: Literal['left', 'right', 'top', 'bottom'] = 'left',
|
||||||
|
width: float = 0.5, height: float = 0.5, sync: bool | UUID = False):
|
||||||
|
c_id = self._go_return('create_sub_chart', volume_enabled, position, width, height, sync)
|
||||||
|
return SubChart(self, c_id)
|
||||||
|
|
||||||
|
|
||||||
|
class SubChart(Chart):
|
||||||
|
def __init__(self, parent, c_id):
|
||||||
|
self._parent = parent._parent if isinstance(parent, SubChart) else parent
|
||||||
|
|
||||||
|
super().__init__(sub=True)
|
||||||
|
|
||||||
|
self.id = c_id
|
||||||
|
self._q = self._parent._q
|
||||||
|
self._result_q = self._parent._result_q
|
||||||
|
|
||||||
|
def _go(self, func, *args):
|
||||||
|
func = 'SUB'+func
|
||||||
|
args = (self.id,) + args
|
||||||
|
super()._go(func, *args)
|
||||||
|
|
||||||
|
def _go_return(self, func, *args):
|
||||||
|
func = 'SUB' + func
|
||||||
|
args = (self.id,) + args
|
||||||
|
return super()._go_return(func, *args)
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
import pandas as pd
|
import pandas as pd
|
||||||
import uuid
|
from uuid import UUID, uuid4
|
||||||
from datetime import timedelta, datetime
|
from datetime import timedelta, datetime
|
||||||
from typing import Dict, Union
|
from typing import Dict, Union, Literal
|
||||||
|
|
||||||
from lightweight_charts.pkg import LWC_3_5_0
|
from lightweight_charts.pkg import LWC_3_5_0
|
||||||
from lightweight_charts.util import LINE_TYPE, POSITION, SHAPE, CROSSHAIR_MODE, _crosshair_mode, _line_type, \
|
from lightweight_charts.util import LINE_TYPE, POSITION, SHAPE, CROSSHAIR_MODE, _crosshair_mode, _line_type, \
|
||||||
MissingColumn, _js_bool, _price_scale_mode, PRICE_SCALE_MODE, _position, _shape, IDGen
|
MissingColumn, _js_bool, _price_scale_mode, PRICE_SCALE_MODE, _position, _shape, IDGen, _valid_color
|
||||||
|
|
||||||
|
|
||||||
class Line:
|
class Line:
|
||||||
@ -33,29 +33,38 @@ class Line:
|
|||||||
|
|
||||||
class API:
|
class API:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.click_func = None
|
self.click_funcs = {}
|
||||||
|
|
||||||
def onClick(self, data):
|
def onClick(self, data):
|
||||||
|
click_func = self.click_funcs[data['id']]
|
||||||
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(data['time']['year'], data['time']['month'], data['time']['day'])
|
||||||
self.click_func(data) if self.click_func else None
|
click_func(data) if click_func else None
|
||||||
|
|
||||||
|
|
||||||
class LWC:
|
class LWC:
|
||||||
def __init__(self, volume_enabled):
|
def __init__(self, volume_enabled, inner_width=1, inner_height=1):
|
||||||
|
self.id = uuid4()
|
||||||
self.js_queue = []
|
self.js_queue = []
|
||||||
self.loaded = False
|
self.loaded = False
|
||||||
self._html = HTML
|
|
||||||
self._rand = IDGen()
|
self._rand = IDGen()
|
||||||
|
self._chart_var = 'chart'
|
||||||
|
|
||||||
self._js_api = API()
|
self._js_api = API()
|
||||||
|
self._js_api_code = ''
|
||||||
|
|
||||||
self.volume_enabled = volume_enabled
|
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.last_bar = None
|
||||||
self.interval = None
|
self.interval = None
|
||||||
self._lines: Dict[uuid.UUID, Line] = {}
|
self._lines: Dict[UUID, Line] = {}
|
||||||
|
self._subcharts: Dict[UUID, LWC] = {self.id: self}
|
||||||
|
|
||||||
self.background_color = '#000000'
|
self.background_color = '#000000'
|
||||||
self.volume_up_color = 'rgba(83,141,131,0.8)'
|
self.volume_up_color = 'rgba(83,141,131,0.8)'
|
||||||
@ -69,8 +78,6 @@ class LWC:
|
|||||||
self.js_queue.append((func, args, kwargs))
|
self.js_queue.append((func, args, kwargs))
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _click_func_code(self, string): self._html = self._html.replace('// __onClick__', string)
|
|
||||||
|
|
||||||
def _set_last_bar(self, bar: pd.Series): self.last_bar = bar
|
def _set_last_bar(self, bar: pd.Series): self.last_bar = bar
|
||||||
|
|
||||||
def _set_interval(self, df: pd.DataFrame):
|
def _set_interval(self, df: pd.DataFrame):
|
||||||
@ -123,11 +130,11 @@ class LWC:
|
|||||||
volume['color'] = self.volume_down_color
|
volume['color'] = self.volume_down_color
|
||||||
volume.loc[df['close'] > df['open'], 'color'] = self.volume_up_color
|
volume.loc[df['close'] > df['open'], 'color'] = self.volume_up_color
|
||||||
|
|
||||||
self.run_script(f'chart.volumeSeries.setData({volume.to_dict(orient="records")})')
|
self.run_script(f'{self._chart_var}.volumeSeries.setData({volume.to_dict(orient="records")})')
|
||||||
bars = df.drop(columns=['volume'])
|
bars = df.drop(columns=['volume'])
|
||||||
|
|
||||||
bars = bars.to_dict(orient='records')
|
bars = bars.to_dict(orient='records')
|
||||||
self.run_script(f'chart.series.setData({bars})')
|
self.run_script(f'{self._chart_var}.series.setData({bars})')
|
||||||
|
|
||||||
def update(self, series, from_tick=False):
|
def update(self, series, from_tick=False):
|
||||||
"""
|
"""
|
||||||
@ -147,11 +154,11 @@ class LWC:
|
|||||||
volume = series.drop(['open', 'high', 'low', 'close'])
|
volume = series.drop(['open', 'high', 'low', 'close'])
|
||||||
volume = volume.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'chart.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'])
|
||||||
|
|
||||||
dictionary = series.to_dict()
|
dictionary = series.to_dict()
|
||||||
self.run_script(f'chart.series.update({dictionary})')
|
self.run_script(f'{self._chart_var}.series.update({dictionary})')
|
||||||
|
|
||||||
def update_from_tick(self, series):
|
def update_from_tick(self, series):
|
||||||
"""
|
"""
|
||||||
@ -184,14 +191,13 @@ class LWC:
|
|||||||
Creates and returns a Line object.)\n
|
Creates and returns a Line object.)\n
|
||||||
:return a Line object used to set/update the line.
|
:return a Line object used to set/update the line.
|
||||||
"""
|
"""
|
||||||
line_id = uuid.uuid4()
|
line_id = uuid4()
|
||||||
self._lines[line_id] = Line(self, line_id, color, width)
|
self._lines[line_id] = Line(self, line_id, color, width)
|
||||||
return self._lines[line_id]
|
return self._lines[line_id]
|
||||||
|
|
||||||
def _set_line_data(self, line_id, df: pd.DataFrame):
|
def _set_line_data(self, line_id, df: pd.DataFrame):
|
||||||
if self._stored('_set_line_data', line_id, df):
|
if self._stored('_set_line_data', line_id, df):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
line = self._lines[line_id]
|
line = self._lines[line_id]
|
||||||
|
|
||||||
if not line.loaded:
|
if not line.loaded:
|
||||||
@ -202,7 +208,7 @@ class LWC:
|
|||||||
lineWidth: {line.width},
|
lineWidth: {line.width},
|
||||||
}};
|
}};
|
||||||
let line{var} = {{
|
let line{var} = {{
|
||||||
series: chart.chart.addLineSeries(lineSeries{var}),
|
series: {self._chart_var}.chart.addLineSeries(lineSeries{var}),
|
||||||
id: '{line_id}',
|
id: '{line_id}',
|
||||||
}};
|
}};
|
||||||
lines.push(line{var})
|
lines.push(line{var})
|
||||||
@ -229,7 +235,7 @@ class LWC:
|
|||||||
}})''')
|
}})''')
|
||||||
|
|
||||||
def marker(self, time: datetime = None, position: POSITION = 'below', shape: SHAPE = 'arrow_up',
|
def marker(self, time: datetime = None, position: POSITION = 'below', shape: SHAPE = 'arrow_up',
|
||||||
color: str = '#2196F3', text: str = '', m_id: uuid.UUID = None) -> uuid.UUID:
|
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.
|
||||||
@ -239,38 +245,43 @@ class LWC:
|
|||||||
: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 UUID of the marker placed.
|
||||||
"""
|
"""
|
||||||
|
_valid_color(color)
|
||||||
if not m_id:
|
if not m_id:
|
||||||
m_id = uuid.uuid4()
|
m_id = uuid4()
|
||||||
if self._stored('marker', time, position, shape, color, text, m_id):
|
if self._stored('marker', time, position, shape, color, text, m_id):
|
||||||
return m_id
|
return m_id
|
||||||
|
|
||||||
time = self.last_bar['time'] if not time else self._datetime_format(time)
|
try:
|
||||||
|
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}'"
|
||||||
|
|
||||||
self.run_script(f"""
|
self.run_script(f"""
|
||||||
markers.push({{
|
markers.push({{
|
||||||
time: '{time}',
|
time: {time},
|
||||||
position: '{_position(position)}',
|
position: '{_position(position)}',
|
||||||
color: '{color}', shape: '{_shape(shape)}',
|
color: '{color}', shape: '{_shape(shape)}',
|
||||||
text: '{text}',
|
text: '{text}',
|
||||||
id: '{m_id}'
|
id: '{m_id}'
|
||||||
}});
|
}});
|
||||||
chart.series.setMarkers(markers)""")
|
{self._chart_var}.series.setMarkers(markers)""")
|
||||||
return m_id
|
return m_id
|
||||||
|
|
||||||
def remove_marker(self, m_id: uuid.UUID):
|
def remove_marker(self, m_id: UUID):
|
||||||
"""
|
"""
|
||||||
Removes the marker with the given uuid.\n
|
Removes the marker with the given UUID.\n
|
||||||
"""
|
"""
|
||||||
if self._stored('remove_marker', m_id):
|
if self._stored('remove_marker', m_id):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
self.run_script(f'''
|
self.run_script(f'''
|
||||||
markers.forEach(function (marker) {{
|
markers.forEach(function (marker) {{
|
||||||
if ('{m_id}' === marker.id) {{
|
if ('{m_id}' === marker.id) {{
|
||||||
markers.splice(markers.indexOf(marker), 1)
|
markers.splice(markers.indexOf(marker), 1)
|
||||||
chart.series.setMarkers(markers)
|
{self._chart_var}.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_TYPE = 'solid', text: str = '', axis_label_visible=True):
|
||||||
@ -291,7 +302,7 @@ class LWC:
|
|||||||
title: '{text}',
|
title: '{text}',
|
||||||
}};
|
}};
|
||||||
let line{var} = {{
|
let line{var} = {{
|
||||||
line: chart.series.createPriceLine(priceLine{var}),
|
line: {self._chart_var}.series.createPriceLine(priceLine{var}),
|
||||||
price: {price},
|
price: {price},
|
||||||
}};
|
}};
|
||||||
horizontal_lines.push(line{var})""")
|
horizontal_lines.push(line{var})""")
|
||||||
@ -306,7 +317,7 @@ class LWC:
|
|||||||
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) {{
|
||||||
chart.series.removePriceLine(line.line);
|
{self._chart_var}.series.removePriceLine(line.line);
|
||||||
horizontal_lines.splice(horizontal_lines.indexOf(line), 1)
|
horizontal_lines.splice(horizontal_lines.indexOf(line), 1)
|
||||||
}}
|
}}
|
||||||
}});''')
|
}});''')
|
||||||
@ -320,26 +331,29 @@ class LWC:
|
|||||||
if self._stored('config', mode, title, right_padding):
|
if self._stored('config', mode, title, right_padding):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
self.run_script(f'chart.chart.timeScale().scrollToPosition({right_padding}, false)') if right_padding else None
|
self.run_script(f'{self._chart_var}.chart.timeScale().scrollToPosition({right_padding}, false)') if right_padding else None
|
||||||
self.run_script(f'chart.series.applyOptions({{title: "{title}"}})') if title else None
|
self.run_script(f'{self._chart_var}.series.applyOptions({{title: "{title}"}})') if title else None
|
||||||
self.run_script(
|
self.run_script(
|
||||||
f"chart.chart.priceScale().applyOptions({{mode: LightweightCharts.PriceScaleMode.{_price_scale_mode(mode)}}})") if mode else None
|
f"{self._chart_var}.chart.priceScale().applyOptions({{mode: LightweightCharts.PriceScaleMode.{_price_scale_mode(mode)}}})") if mode else None
|
||||||
|
|
||||||
def time_scale(self, time_visible: bool = True, seconds_visible: bool = False):
|
def time_scale(self, visible: bool = True, time_visible: bool = True, seconds_visible: bool = False):
|
||||||
"""
|
"""
|
||||||
Options for the time scale of the chart.
|
Options for the time scale of the chart.
|
||||||
|
:param visible: Time scale visibility control.
|
||||||
:param time_visible: Time visibility control.
|
:param time_visible: Time visibility control.
|
||||||
:param seconds_visible: Seconds visibility control
|
:param seconds_visible: Seconds visibility control.
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
if self._stored('time_scale', time_visible, seconds_visible):
|
if self._stored('time_scale', visible, time_visible, seconds_visible):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
time_scale_visible = f'visible: {_js_bool(visible)},'
|
||||||
time = f'timeVisible: {_js_bool(time_visible)},'
|
time = f'timeVisible: {_js_bool(time_visible)},'
|
||||||
seconds = f'secondsVisible: {_js_bool(seconds_visible)}'
|
seconds = f'secondsVisible: {_js_bool(seconds_visible)},'
|
||||||
self.run_script(f'''
|
self.run_script(f'''
|
||||||
chart.chart.applyOptions({{
|
{self._chart_var}.chart.applyOptions({{
|
||||||
timeScale: {{
|
timeScale: {{
|
||||||
|
{time_scale_visible if visible is not None else ''}
|
||||||
{time if time_visible is not None else ''}
|
{time if time_visible is not None else ''}
|
||||||
{seconds if seconds_visible is not None else ''}
|
{seconds if seconds_visible is not None else ''}
|
||||||
}}
|
}}
|
||||||
@ -354,16 +368,15 @@ class LWC:
|
|||||||
return None
|
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
|
||||||
args = f"'{self.background_color}'", f"'{text_color}'", f"{font_size}", f"'{font_family}'",
|
self.run_script(f"""
|
||||||
for key, arg in zip(('backgroundColor', 'textColor', 'fontSize', 'fontFamily'), args):
|
document.body.style.backgroundColor = '{self.background_color}'
|
||||||
if not arg:
|
{self._chart_var}.chart.applyOptions({{
|
||||||
continue
|
layout: {{
|
||||||
self.run_script(f"""
|
{f'backgroundColor: "{background_color}",' if background_color else ''}
|
||||||
chart.chart.applyOptions({{
|
{f'textColor: "{text_color}",' if text_color else ''}
|
||||||
layout: {{
|
{f'fontSize: {font_size},' if font_size else ''}
|
||||||
{key}: {arg}
|
{f'fontFamily: "{font_family}",' if font_family 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 = '',
|
||||||
@ -375,20 +388,17 @@ class LWC:
|
|||||||
border_up_color, border_down_color, wick_up_color, wick_down_color):
|
border_up_color, border_down_color, wick_up_color, wick_down_color):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
params = None, 'upColor', 'downColor', 'wickVisible', 'borderVisible', 'borderUpColor', 'borderDownColor',\
|
self.run_script(f"""
|
||||||
'wickUpColor', 'wickDownColor'
|
{self._chart_var}.series.applyOptions({{
|
||||||
for param, key_arg in zip(params, locals().items()):
|
{f'upColor: "{up_color}",' if up_color else ''}
|
||||||
key, arg = key_arg
|
{f'downColor: "{down_color}",' if down_color else ''}
|
||||||
if isinstance(arg, bool):
|
{f'wickVisible: {_js_bool(wick_enabled)},' if wick_enabled else ''}
|
||||||
arg = _js_bool(arg)
|
{f'borderVisible: {_js_bool(border_enabled)},' if border_enabled else ''}
|
||||||
if key == 'self' or arg is None:
|
{f'borderUpColor: "{border_up_color}",' if border_up_color else ''}
|
||||||
continue
|
{f'borderDownColor: "{border_down_color}",' if border_down_color else ''}
|
||||||
else:
|
{f'wickUpColor: "{wick_up_color}",' if wick_up_color else ''}
|
||||||
arg = f"'{arg}'"
|
{f'wickDownColor: "{wick_down_color}",' if wick_down_color else ''}
|
||||||
self.run_script(
|
}})""")
|
||||||
f"""chart.series.applyOptions({{
|
|
||||||
{param}: {arg},
|
|
||||||
}})""")
|
|
||||||
|
|
||||||
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,
|
||||||
up_color='rgba(83,141,131,0.8)', down_color='rgba(200,127,130,0.8)'):
|
up_color='rgba(83,141,131,0.8)', down_color='rgba(200,127,130,0.8)'):
|
||||||
@ -406,13 +416,11 @@ class LWC:
|
|||||||
|
|
||||||
self.volume_up_color = up_color if up_color else self.volume_up_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.volume_down_color = down_color if down_color else self.volume_down_color
|
||||||
top = f'top: {scale_margin_top},'
|
|
||||||
bottom = f'bottom: {scale_margin_bottom},'
|
|
||||||
self.run_script(f'''
|
self.run_script(f'''
|
||||||
chart.volumeSeries.priceScale().applyOptions({{
|
{self._chart_var}.volumeSeries.priceScale().applyOptions({{
|
||||||
scaleMargins: {{
|
scaleMargins: {{
|
||||||
{top if top else ''}
|
top: {scale_margin_top},
|
||||||
{bottom if bottom else ''}
|
bottom: {scale_margin_bottom},
|
||||||
}}
|
}}
|
||||||
}})''')
|
}})''')
|
||||||
|
|
||||||
@ -431,14 +439,13 @@ class LWC:
|
|||||||
f"'{vert_label_background_color}'}}", \
|
f"'{vert_label_background_color}'}}", \
|
||||||
f"{horz_width}}}", f"'{horz_color}'}}", f"LightweightCharts.LineStyle.{_line_type(horz_style)}}}",\
|
f"{horz_width}}}", f"'{horz_color}'}}", f"LightweightCharts.LineStyle.{_line_type(horz_style)}}}",\
|
||||||
f"'{horz_label_background_color}'}}"
|
f"'{horz_label_background_color}'}}"
|
||||||
|
|
||||||
for key, arg in zip(
|
for key, arg in zip(
|
||||||
('mode', 'vertLine: {width', 'vertLine: {color', 'vertLine: {style', 'vertLine: {labelBackgroundColor',
|
('mode', 'vertLine: {width', 'vertLine: {color', 'vertLine: {style', 'vertLine: {labelBackgroundColor',
|
||||||
'horzLine: {width', 'horzLine: {color', 'horzLine: {style', 'horzLine: {labelBackgroundColor'), args):
|
'horzLine: {width', 'horzLine: {color', 'horzLine: {style', 'horzLine: {labelBackgroundColor'), args):
|
||||||
if 'None' in arg:
|
if 'None' in arg:
|
||||||
continue
|
continue
|
||||||
self.run_script(f'''
|
self.run_script(f'''
|
||||||
chart.chart.applyOptions({{
|
{self._chart_var}.chart.applyOptions({{
|
||||||
crosshair: {{
|
crosshair: {{
|
||||||
{key}: {arg}
|
{key}: {arg}
|
||||||
}}}})''')
|
}}}})''')
|
||||||
@ -451,7 +458,7 @@ class LWC:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
self.run_script(f'''
|
self.run_script(f'''
|
||||||
chart.chart.applyOptions({{
|
{self._chart_var}.chart.applyOptions({{
|
||||||
watermark: {{
|
watermark: {{
|
||||||
visible: true,
|
visible: true,
|
||||||
fontSize: {font_size},
|
fontSize: {font_size},
|
||||||
@ -470,27 +477,160 @@ class LWC:
|
|||||||
if self._stored('legend', visible, ohlc, percent, color, font_size, font_family):
|
if self._stored('legend', visible, ohlc, percent, color, font_size, font_family):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
scripts = f'legendToggle = {_js_bool(visible)}; legend.innerText = ""', f'legendOHLCVisible = {_js_bool(ohlc)}',\
|
if visible:
|
||||||
f'legendPercentVisible = {_js_bool(percent)}', f'legend.style.color = {color}', \
|
self.run_script(f'''
|
||||||
f'legend.style.fontSize = "{font_size}px"', f'legend.style.fontFamily = "{font_family}"'
|
{f"{self._chart_var}.legend.style.color = '{color}'" if color else ''}
|
||||||
for script, arg in zip(scripts, (visible, ohlc, percent, color, font_size, font_family)):
|
{f"{self._chart_var}.legend.style.fontSize = {font_size}" if font_size else ''}
|
||||||
if arg is None:
|
{f"{self._chart_var}.legend.style.fontFamily = '{font_family}'" if font_family else ''}
|
||||||
continue
|
|
||||||
self.run_script(script)
|
{self._chart_var}.chart.subscribeCrosshairMove((param) => {{
|
||||||
|
if (param.time){{
|
||||||
|
const data = param.seriesPrices.get({self._chart_var}.series);
|
||||||
|
if (!data) {{return}}
|
||||||
|
let percentMove = ((data.close-data.open)/data.open)*100
|
||||||
|
let ohlc = `open: ${{legendItemFormat(data.open)}}
|
||||||
|
| high: ${{legendItemFormat(data.high)}}
|
||||||
|
| low: ${{legendItemFormat(data.low)}}
|
||||||
|
| close: ${{legendItemFormat(data.close)}} `
|
||||||
|
let percent = `| daily: ${{percentMove >= 0 ? '+' : ''}}${{percentMove.toFixed(2)}} %`
|
||||||
|
let finalString = ''
|
||||||
|
{'finalString += ohlc' if ohlc else ''}
|
||||||
|
{'finalString += percent' if percent else ''}
|
||||||
|
{self._chart_var}.legend.innerHTML = finalString
|
||||||
|
}}
|
||||||
|
else {{
|
||||||
|
{self._chart_var}.legend.innerHTML = ''
|
||||||
|
}}
|
||||||
|
}});''')
|
||||||
|
|
||||||
def subscribe_click(self, function: object):
|
def subscribe_click(self, function: object):
|
||||||
if self._stored('subscribe_click', function):
|
if self._stored('subscribe_click', function):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
self._js_api.click_func = function
|
self._js_api.click_funcs[str(self.id)] = function
|
||||||
self.run_script('isSubscribed = true')
|
var = self._rand.generate()
|
||||||
|
self.run_script(f'''
|
||||||
|
{self._chart_var}.chart.subscribeClick((param) => {{
|
||||||
|
if (!param.point) {{return}}
|
||||||
|
let prices{var} = param.seriesPrices.get({self._chart_var}.series);
|
||||||
|
let data{var} = {{
|
||||||
|
time: param.time,
|
||||||
|
open: prices{var}.open,
|
||||||
|
high: prices{var}.high,
|
||||||
|
low: prices{var}.low,
|
||||||
|
close: prices{var}.close,
|
||||||
|
id: '{self.id}'
|
||||||
|
}}
|
||||||
|
{self._js_api_code}(data{var})
|
||||||
|
}})''')
|
||||||
|
|
||||||
|
def create_sub_chart(self, volume_enabled: bool = True, position: Literal['left', 'right', 'top', 'bottom'] = 'left',
|
||||||
|
width: float = 0.5, height: float = 0.5, sync: bool | UUID = False):
|
||||||
|
subchart = SubChart(self, volume_enabled, position, width, height, sync)
|
||||||
|
self._subcharts[subchart.id] = subchart
|
||||||
|
return subchart
|
||||||
|
|
||||||
|
def _pywebview_sub_chart(self, volume_enabled, position, width, height, sync, parent=None):
|
||||||
|
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):
|
||||||
|
def __init__(self, parent, volume_enabled, position, width, height, sync):
|
||||||
|
super().__init__(volume_enabled, width, height)
|
||||||
|
self._chart = parent._chart if isinstance(parent, SubChart) else parent
|
||||||
|
self._parent = parent
|
||||||
|
|
||||||
|
self._rand = self._chart._rand
|
||||||
|
self._chart_var = self._rand.generate()
|
||||||
|
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
|
||||||
|
|
||||||
|
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'''
|
||||||
|
var {self._chart_var}div = document.createElement('div')
|
||||||
|
//{self._chart_var}div.style.position = 'relative'
|
||||||
|
{self._chart_var}div.style.float = "{self.position}"
|
||||||
|
|
||||||
|
//chartsDiv.style.display = 'inline-block'
|
||||||
|
chartsDiv.style.float = 'left'
|
||||||
|
|
||||||
|
var {self._chart_var} = {{}}
|
||||||
|
{self._chart_var}.scale= {{
|
||||||
|
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_sub_chart(self, volume_enabled: bool = True, position: Literal['left', 'right', 'top', 'bottom'] = 'left',
|
||||||
|
width: float = 0.5, height: float = 0.5, sync: bool | UUID = False):
|
||||||
|
return self._chart._pywebview_sub_chart(volume_enabled, position, width, height, sync, parent=self)
|
||||||
|
|
||||||
|
def create_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'
|
||||||
|
|
||||||
const markers = []
|
const markers = []
|
||||||
const horizontal_lines = []
|
const horizontal_lines = []
|
||||||
const 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)'
|
||||||
@ -557,25 +697,28 @@ function makeVolumeSeries(chart) {
|
|||||||
|
|
||||||
const chartsDiv = document.createElement('div')
|
const chartsDiv = document.createElement('div')
|
||||||
|
|
||||||
|
var chart = {}
|
||||||
|
|
||||||
chart.chart = makeChart(window.innerWidth, window.innerHeight, chartsDiv)
|
chart.chart = makeChart(window.innerWidth, window.innerHeight, chartsDiv)
|
||||||
chart.series = makeCandlestickSeries(chart.chart)
|
chart.series = makeCandlestickSeries(chart.chart)
|
||||||
chart.volumeSeries = makeVolumeSeries(chart.chart)
|
chart.volumeSeries = makeVolumeSeries(chart.chart)
|
||||||
|
chart.scale = {
|
||||||
|
width: __INNER_WIDTH__,
|
||||||
|
height: __INNER_HEIGHT__
|
||||||
|
}
|
||||||
|
|
||||||
document.body.appendChild(chartsDiv)
|
document.body.appendChild(chartsDiv)
|
||||||
|
|
||||||
const legend = document.createElement('div')
|
chart.legend = document.createElement('div')
|
||||||
legend.style.display = 'block'
|
chart.legend.style.position = 'absolute'
|
||||||
legend.style.position = 'absolute'
|
chart.legend.style.zIndex = 1000
|
||||||
legend.style.zIndex = 1000
|
chart.legend.style.width = `${(chart.scale.width*100)-8}vw`
|
||||||
legend.style.width = '98vw'
|
chart.legend.style.top = '10px'
|
||||||
legend.style.top = '10px'
|
chart.legend.style.left = '10px'
|
||||||
legend.style.left = '10px'
|
chart.legend.style.fontFamily = 'Monaco'
|
||||||
legend.style.fontFamily = 'Monaco'
|
chart.legend.style.fontSize = '11px'
|
||||||
|
chart.legend.style.color = 'rgb(191, 195, 203)'
|
||||||
legend.style.fontSize = '11px'
|
document.body.appendChild(chart.legend)
|
||||||
legend.style.color = 'rgb(191, 195, 203)'
|
|
||||||
|
|
||||||
document.body.appendChild(legend)
|
|
||||||
|
|
||||||
chart.chart.priceScale('').applyOptions({
|
chart.chart.priceScale('').applyOptions({
|
||||||
scaleMargins: {
|
scaleMargins: {
|
||||||
@ -587,56 +730,18 @@ chart.chart.priceScale('').applyOptions({
|
|||||||
window.addEventListener('resize', function() {
|
window.addEventListener('resize', function() {
|
||||||
let width = window.innerWidth;
|
let width = window.innerWidth;
|
||||||
let height = window.innerHeight;
|
let height = window.innerHeight;
|
||||||
chart.chart.resize(width, height)
|
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, ' ')
|
||||||
}
|
}
|
||||||
let legendToggle = false
|
|
||||||
let legendOHLCVisible = true
|
|
||||||
let legendPercentVisible = true
|
|
||||||
chart.chart.subscribeCrosshairMove((param) => {
|
|
||||||
if (param.time){
|
|
||||||
const data = param.seriesPrices.get(chart.series);
|
|
||||||
if (!data || legendToggle === false) {return}
|
|
||||||
const percentMove = ((data.close-data.open)/data.open)*100
|
|
||||||
//legend.style.color = percentMove >= 0 ? up : down
|
|
||||||
|
|
||||||
let ohlc = `open: ${legendItemFormat(data.open)}
|
|
||||||
| high: ${legendItemFormat(data.high)}
|
|
||||||
| low: ${legendItemFormat(data.low)}
|
|
||||||
| close: ${legendItemFormat(data.close)} `
|
|
||||||
let percent = `| daily: ${percentMove >= 0 ? '+' : ''}${percentMove.toFixed(2)} %`
|
|
||||||
|
|
||||||
let finalString = ''
|
|
||||||
if (legendOHLCVisible) {
|
|
||||||
finalString += ohlc
|
|
||||||
}
|
|
||||||
if (legendPercentVisible) {
|
|
||||||
finalString += percent
|
|
||||||
}
|
|
||||||
legend.innerHTML = finalString
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
legend.innerHTML = ''
|
|
||||||
}
|
|
||||||
});
|
|
||||||
let isSubscribed = false
|
|
||||||
function clickHandler(param) {
|
|
||||||
if (!param.point || !isSubscribed) {return}
|
|
||||||
let prices = param.seriesPrices.get(chart.series);
|
|
||||||
let data = {
|
|
||||||
time: param.time,
|
|
||||||
open: prices.open,
|
|
||||||
high: prices.high,
|
|
||||||
low: prices.low,
|
|
||||||
close: prices.close,
|
|
||||||
}
|
|
||||||
// __onClick__
|
|
||||||
|
|
||||||
}
|
|
||||||
chart.chart.subscribeClick(clickHandler)
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
HTML = f"""
|
HTML = f"""
|
||||||
@ -649,6 +754,8 @@ HTML = f"""
|
|||||||
<style>
|
<style>
|
||||||
body {{
|
body {{
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
}}
|
}}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
from typing import Literal
|
||||||
|
from uuid import UUID
|
||||||
import webview
|
import webview
|
||||||
from multiprocessing import Queue
|
from multiprocessing import Queue
|
||||||
|
|
||||||
@ -9,10 +11,10 @@ _result_q = Queue()
|
|||||||
|
|
||||||
class Webview(LWC):
|
class Webview(LWC):
|
||||||
def __init__(self, chart):
|
def __init__(self, chart):
|
||||||
super().__init__(chart.volume_enabled)
|
super().__init__(chart.volume_enabled, chart.inner_width, chart.inner_height)
|
||||||
self.chart = chart
|
self.chart = chart
|
||||||
self.started = False
|
self.started = False
|
||||||
self._click_func_code('pywebview.api.onClick(data)')
|
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,
|
self.webview = webview.create_window('', html=self._html, on_top=chart.on_top, js_api=self._js_api,
|
||||||
width=chart.width, height=chart.height, x=chart.x, y=chart.y)
|
width=chart.width, height=chart.height, x=chart.x, y=chart.y)
|
||||||
@ -24,8 +26,15 @@ class Webview(LWC):
|
|||||||
self.loaded = True
|
self.loaded = True
|
||||||
while len(self.js_queue) > 0:
|
while len(self.js_queue) > 0:
|
||||||
func, args, kwargs = self.js_queue[0]
|
func, args, kwargs = self.js_queue[0]
|
||||||
getattr(self, func)(*args)
|
|
||||||
|
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]
|
del self.js_queue[0]
|
||||||
|
|
||||||
_loop(self.chart, controller=self)
|
_loop(self.chart, controller=self)
|
||||||
|
|
||||||
def show(self):
|
def show(self):
|
||||||
@ -34,27 +43,40 @@ class Webview(LWC):
|
|||||||
else:
|
else:
|
||||||
webview.start(debug=self.chart.debug)
|
webview.start(debug=self.chart.debug)
|
||||||
|
|
||||||
def create_line(self, color: str = 'rgba(214, 237, 255, 0.6)', width: int = 2):
|
|
||||||
return super().create_line(color, width).id
|
|
||||||
|
|
||||||
def hide(self): self.webview.hide()
|
def hide(self): self.webview.hide()
|
||||||
|
|
||||||
def exit(self):
|
def exit(self):
|
||||||
self.webview.destroy()
|
self.webview.destroy()
|
||||||
del self
|
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_sub_chart(self, volume_enabled: bool = True, position: Literal['left', 'right', 'top', 'bottom'] = 'left',
|
||||||
|
width: float = 0.5, height: float = 0.5, sync: bool | UUID = False):
|
||||||
|
return super()._pywebview_sub_chart(volume_enabled, position, width, height, sync)
|
||||||
|
|
||||||
|
|
||||||
def _loop(chart, controller=None):
|
def _loop(chart, controller=None):
|
||||||
wv = Webview(chart) if not controller else controller
|
wv = Webview(chart) if not controller else controller
|
||||||
|
chart._result_q.put(wv.id)
|
||||||
while 1:
|
while 1:
|
||||||
|
obj = wv
|
||||||
func, args = chart._q.get()
|
func, args = chart._q.get()
|
||||||
|
|
||||||
|
if 'SUB' in func:
|
||||||
|
obj = obj._subcharts[args[0]]
|
||||||
|
args = args[1:]
|
||||||
|
func = func.replace('SUB', '')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = getattr(wv, func)(*args)
|
result = getattr(obj, func)(*args)
|
||||||
except KeyError as e:
|
except KeyError as e:
|
||||||
return
|
return
|
||||||
if func == 'show':
|
if func == 'show':
|
||||||
chart._exit.set()
|
chart._exit.set()
|
||||||
elif func == 'exit':
|
elif func == 'exit':
|
||||||
chart._exit.set()
|
chart._exit.set()
|
||||||
|
|
||||||
chart._result_q.put(result) if result is not None else None
|
chart._result_q.put(result) if result is not None else None
|
||||||
|
|
||||||
|
|||||||
@ -12,6 +12,21 @@ class MissingColumn(KeyError):
|
|||||||
return f'{self.msg}'
|
return f'{self.msg}'
|
||||||
|
|
||||||
|
|
||||||
|
class ColorError(ValueError):
|
||||||
|
def __init__(self, message):
|
||||||
|
super().__init__(message)
|
||||||
|
self.msg = message
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f'{self.msg}'
|
||||||
|
|
||||||
|
|
||||||
|
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']
|
LINE_TYPE = Literal['solid', 'dotted', 'dashed', 'large_dashed', 'sparse_dotted']
|
||||||
|
|
||||||
POSITION = Literal['above', 'below', 'inside']
|
POSITION = Literal['above', 'below', 'inside']
|
||||||
|
|||||||
@ -22,7 +22,7 @@ class WxChart(LWC):
|
|||||||
super().__init__(volume_enabled)
|
super().__init__(volume_enabled)
|
||||||
|
|
||||||
self.webview.AddScriptMessageHandler('wx_msg')
|
self.webview.AddScriptMessageHandler('wx_msg')
|
||||||
self._click_func_code('window.wx_msg.postMessage(data)')
|
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.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)
|
||||||
|
|||||||
Reference in New Issue
Block a user