NEW FEATURE: Trendlines, Rays and the Toolbox

- Added `trend_line` and `ray_line` to the Common Methods.

- Added the `toolbox` parameter to chart declaration. This allows horizontal lines, trend lines and rays to be drawn on the chart using hotkeys and buttons.
    - cmd-Z will delete the last drawing.
    - Drawings can be moved by clicking and dragging.

- Added the `render_drawings` parameter to `set`, which will keep and re-render the drawings displayed on the chart (useful for multiple timeframes!)

Horizontal Lines
- The `horizontal_line` method now returns a HorizontalLine object, containing the methods `update` and `delete`.
- Added the `interactive` parameter to `horizontal_line`, allowing for callbacks to be emitted to the `on_horizontal_line_move` callback method when the line is dragged to a new price (stop losses, limit orders, etc.).

Enhancements:
- added the `precision` method to the Common Methods, allowing for the number of decimal places shown on the price scale to be declared.
- Lines displayed on legends now have toggle switches, allowing for their visibility to be controlled directly within the chart window.
- when using `set`, the column names can now be capitalised, and the `date` column can be the index.

Changes:
- Merged the `title` method into the `price_line` method.
This commit is contained in:
louisnw
2023-07-16 20:54:32 +01:00
parent 7850821c6a
commit e4459208d2
14 changed files with 1092 additions and 262 deletions

View File

@ -3,7 +3,7 @@
# lightweight-charts-python
[![PyPi Release](https://img.shields.io/pypi/v/lightweight-charts?color=32a852&label=PyPi)](https://pypi.org/project/lightweight-charts/)
[![Made with Python](https://img.shields.io/badge/Python-3.9+-c7a002?logo=python&logoColor=white)](https://python.org "Go to Python homepage")
[![Made with Python](https://img.shields.io/badge/Python-3.8+-c7a002?logo=python&logoColor=white)](https://python.org "Go to Python homepage")
[![License](https://img.shields.io/github/license/louisnw01/lightweight-charts-python?color=9c2400)](https://github.com/louisnw01/lightweight-charts-python/blob/main/LICENSE)
[![Documentation](https://img.shields.io/badge/documentation-006ee3)](https://lightweight-charts-python.readthedocs.io/en/latest/docs.html)
@ -24,10 +24,12 @@ ___
1. Simple and easy to use.
2. Blocking or non-blocking GUI.
3. Streamlined for live data, with methods for updating directly from tick data.
4. __Supports:__ Jupyter Notebooks, PyQt, wxPython, Streamlit, and asyncio.
5. [Callbacks](https://lightweight-charts-python.readthedocs.io/en/latest/docs.html#callbacks) allowing for timeframe (1min, 5min, 30min etc.) selectors, searching, and more.
6. Multi-Pane Charts using the [`SubChart`](https://lightweight-charts-python.readthedocs.io/en/latest/docs.html#subchart).
4. Multi-Pane Charts using the [`SubChart`](https://lightweight-charts-python.readthedocs.io/en/latest/docs.html#subchart).
5. The Toolbox, allowing for trendlines, rays and horizontal lines to be drawn directly onto charts.
6. [Callbacks](https://lightweight-charts-python.readthedocs.io/en/latest/docs.html#callbacks) allowing for timeframe (1min, 5min, 30min etc.) selectors, searching, and more.
7. Direct integration of market data through [Polygon.io's](https://polygon.io) market data API.
__Supports:__ Jupyter Notebooks, PyQt, wxPython, Streamlit, and asyncio.
___
### 1. Display data from a csv:
@ -129,19 +131,20 @@ def calculate_sma(data: pd.DataFrame, period: int = 50):
result = []
for i in range(period - 1, len(data)):
val = avg(data.iloc[i - period + 1:i])
result.append({'time': data.iloc[i]['date'], 'value': val})
result.append({'time': data.iloc[i]['date'], f'SMA {period}': val})
return pd.DataFrame(result)
if __name__ == '__main__':
chart = Chart()
chart.legend(visible=True)
df = pd.read_csv('ohlcv.csv')
chart.set(df)
line = chart.create_line()
sma_data = calculate_sma(df)
line._set(sma_data)
sma_data = calculate_sma(df, period=50)
line.set(sma_data, name='SMA 50')
chart.show(block=True)
@ -219,11 +222,14 @@ class API:
return
self.chart.set(new_data)
async def on_horizontal_line_move(self, line_id, price):
print(f'Horizontal line moved to: {price}')
async def main():
api = API()
chart = Chart(api=api, topbar=True, searchbox=True)
chart = Chart(api=api, topbar=True, searchbox=True, toolbox=True)
chart.legend(True)
chart.topbar.textbox('corner', 'TSLA')
@ -232,6 +238,8 @@ async def main():
df = get_bar_data('TSLA', '5min')
chart.set(df)
chart.horizontal_line(200, interactive=True)
await chart.show_async(block=True)
@ -245,6 +253,8 @@ ___
<div align="center">
[![Documentation](https://img.shields.io/badge/documentation-006ee3)](https://lightweight-charts-python.readthedocs.io/en/latest/docs.html)
Inquiries: [shaders_worker_0e@icloud.com](mailto:shaders_worker_0e@icloud.com)
___
_This package is an independent creation and has not been endorsed, sponsored, or approved by TradingView. The author of this package does not have any official relationship with TradingView, and the package does not represent the views or opinions of TradingView._

View File

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

View File

@ -9,11 +9,11 @@
___
## Common Methods
These methods can be used within the [`Chart`](#chart), [`SubChart`](#subchart), [`QtChart`](#qtchart), [`WxChart`](#wxchart) and [`StreamlitChart`](#streamlitchart) objects.
The methods below can be used within all chart objects.
___
### `set`
`data: pd.DataFrame`
`data: pd.DataFrame` `render_drawings: bool`
Sets the initial data for the chart.
@ -21,7 +21,11 @@ The data must be given as a DataFrame, with the columns:
`time | open | high | low | close | volume`
The `time` column can also be named `date`, and the `volume` column can be omitted if volume is not enabled.
The `time` column can also be named `date` or be the index, and the `volume` column can be omitted if volume is not enabled.
Column names are not case sensitive.
If `render_drawings` is `True`, any drawings made using the `toolbox` will be redrawn with the new data. This is designed to be used when switching to a different timeframe of the same symbol.
```{important}
the `time` column must have rows all of the same timezone and locale. This is particularly noticeable for data which crosses over daylight saving hours on data with intervals of less than 1 day. Errors are likely to be raised if they are not converted beforehand.
@ -53,7 +57,7 @@ As before, the `time` can also be named `date`, and the `volume` can be omitted
The provided ticks do not need to be rounded to an interval (1 min, 5 min etc.), as the library handles this automatically.```````
```
If `cumulative_volume` is used, the volume data given to this method will be added onto the latest bar of volume data.
If `cumulative_volume` is used, the volume data given will be added onto the latest bar of volume data.
___
### `create_line`
@ -65,14 +69,29 @@ ___
### `lines`
`-> List[Line]`
Returns a list of all Line objects for the chart or subchart.
Returns a list of all lines for the chart or subchart.
___
### `trend_line`
`start_time: str/datetime` | `start_value: float/int` | `end_time: str/datetime` | `end_value: float/int` | `color: str` | `width: int` | `-> Line`
Creates a trend line, drawn from the first point (`start_time`, `start_value`) to the last point (`end_time`, `end_value`).
___
### `ray_line`
`start_time: str/datetime` | `value: float/int` | `color: str` | `width: int` | `-> Line`
Creates a ray line, drawn from the first point (`start_time`, `value`) and onwards.
___
### `marker`
`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 id.
If the `time` parameter is not given, the marker will be placed at the latest bar.
When using multiple markers, they should be placed in chronological order or display bugs may be present.
___
### `remove_marker`
@ -88,9 +107,11 @@ chart.remove_marker(marker)
___
### `horizontal_line`
`price: float/int` | `color: str` | `width: int` | `style: 'solid'/'dotted'/'dashed'/'large_dashed'/'sparse_dotted'` | `text: str` | `axis_label_visible: bool`
`price: float/int` | `color: str` | `width: int` | `style: 'solid'/'dotted'/'dashed'/'large_dashed'/'sparse_dotted'` | `text: str` | `axis_label_visible: bool` | `interactive: bool` | `-> HorizontalLine`
Places a horizontal line at the given price.
Places a horizontal line at the given price, and returns a HorizontalLine object.
If `interactive` is set to `True`, this horizontal line can be edited on the chart. Upon its movement a callback will also be emitted to an `on_horizontal_line_move` method, containing its ID and price. The toolbox should be enabled during its usage. It is designed to be used to update an order (limit, stop, etc.) directly on the chart.
___
### `remove_horizontal_line`
@ -109,6 +130,12 @@ ___
Clears the horizontal lines displayed on the data.
___
### `precision`
`precision: int`
Sets the precision of the chart based on the given number of decimal places.
___
### `price_scale`
`mode: 'normal'/'logarithmic'/'percentage'/'index100'` | `align_labels: bool` | `border_visible: bool` | `border_color: str` | `text_color: str` | `entire_text_only: bool` | `ticks_visible: bool` | `scale_margin_top: float` | `scale_margin_bottom: float`
@ -172,12 +199,6 @@ ___
Overlays a watermark on top of the chart.
___
### `title`
`title: str`
Sets the title label for the chart.
___
### `legend`
`visible: bool` | `ohlc: bool` | `percent: bool` | `lines: bool` | `color: str` | `font_size: int` | `font_family: str`
@ -191,7 +212,7 @@ Shows a loading spinner on the chart, which can be used to visualise the loading
___
### `price_line`
`label_visible: bool` | `line_visible: bool`
`label_visible: bool` | `line_visible: bool` | `title: str`
Configures the visibility of the last value price line and its label.
___
@ -212,7 +233,7 @@ Shows the hidden candles on the chart.
___
### `polygon`
Used to access Polygon.io's API (see [here](https://lightweight-charts-python.readthedocs.io/en/latest/polygon.html))
Used to access Polygon.io's API (see [here](https://lightweight-charts-python.readthedocs.io/en/latest/polygon.html)).
___
### `create_subchart`
@ -227,7 +248,7 @@ Creates and returns a [SubChart](#subchart) object, placing it adjacent to the d
`sync`: If given as `True`, the `SubChart`'s timescale and crosshair will follow that of the declaring `Chart` or `SubChart`. If a `str` is passed, the `SubChart` will follow the panel with the given id. Chart ids can be accessed from the`chart.id` and `subchart.id` attributes.
```{important}
`width` and `height` must be given as a number between 0 and 1.
`width` and `height` should be given as a number between 0 and 1.
```
___
@ -235,25 +256,29 @@ ___
## Chart
`volume_enabled: bool` | `width: int` | `height: int` | `x: int` | `y: int` | `on_top: bool` | `maximize: bool` | `debug: bool` |
`api: object` | `topbar: bool` | `searchbox: bool`
`api: object` | `topbar: bool` | `searchbox: bool` | `toolbox: bool`
The main object used for the normal functionality of lightweight-charts-python, built on the pywebview library.
```{important}
The `Chart` object should be defined within an `if __name__ == '__main__'` block.
```
___
### `show`
`block: bool`
Shows the chart window. If `block` is enabled, the method will block code execution until the window is closed.
Shows the chart window, blocking until the chart has loaded. If `block` is enabled, the method will block code execution until the window is closed.
___
### `hide`
Hides the chart window, and can be later shown by calling `chart.show()`.
Hides the chart window, which can be later shown by calling `chart.show()`.
___
### `exit`
Exits and destroys the chart and window.
Exits and destroys the chart window.
___
@ -280,7 +305,7 @@ if __name__ == '__main__':
```
```{important}
This method must be called after the chart window is open.
This method should be called after the chart window has loaded.
```
___
@ -288,11 +313,10 @@ ___
## Line
The `Line` object represents a `LineSeries` object in Lightweight Charts and can be used to create indicators. As well as the methods described below, the `Line` object also has access to:
[`title`](#title), [`marker`](#marker), [`horizontal_line`](#horizontal-line) [`hide_data`](#hide-data), [`show_data`](#show-data) and[`price_line`](#price-line) methods.
[`title`](#title), [`marker`](#marker), [`horizontal_line`](#horizontal-line) [`hide_data`](#hide-data), [`show_data`](#show-data) and[`price_line`](#price-line).
```{important}
The `line` object should only be accessed from the [`create_line`](#create-line) method of `Chart`.
The `Line` object should only be accessed from the [`create_line`](#create-line) method of `Chart`.
```
___
@ -324,7 +348,28 @@ ___
### `delete`
Irreversibly deletes the line on the chart as well as the Line object.
Irreversibly deletes the line.
___
## HorizontalLine
The `HorizontalLine` object represents a `PriceLine` in Lightweight Charts.
```{important}
The `HorizontalLine` object should only be accessed from the [`horizontal_line`](#horizontal-line) Common Method.
```
___
### `update`
`price: float/int`
Updates the price of the horizontal line.
___
### `delete`
Irreversibly deletes the horizontal line.
___
## SubChart
@ -513,6 +558,19 @@ if __name__ == '__main__':
```
___
## Toolbox
The Toolbox allows for trendlines, ray lines and horizontal lines to be drawn and edited directly on the chart.
It can be used within any Chart object, and is enabled by setting the `toolbox` parameter to `True` upon Chart declaration.
The following hotkeys can also be used when the Toolbox is enabled:
* Alt+T: Trendline
* Alt+H: Horizontal Line
* Alt+R: Ray Line
* Meta+Z: Undo
___
## QtChart
`widget: QWidget` | `volume_enabled: bool`

View File

@ -8,19 +8,20 @@ def calculate_sma(data: pd.DataFrame, period: int = 50):
result = []
for i in range(period - 1, len(data)):
val = avg(data.iloc[i - period + 1:i])
result.append({'time': data.iloc[i]['date'], 'value': val})
result.append({'time': data.iloc[i]['date'], f'SMA {period}': val})
return pd.DataFrame(result)
if __name__ == '__main__':
chart = Chart()
chart = Chart(debug=True)
chart.legend(visible=True)
df = pd.read_csv('ohlcv.csv')
chart.set(df)
line = chart.create_line()
sma_data = calculate_sma(df)
line.set(sma_data)
sma_data = calculate_sma(df, period=50)
line.set(sma_data, name='SMA 50')
chart.show(block=True)

View File

@ -13,7 +13,7 @@ def get_bar_data(symbol, timeframe):
class API:
def __init__(self):
self.chart = None # Changes after each callback.
self.chart: Chart = None # Changes after each callback.
async def on_search(self, searched_string): # Called when the user searches.
new_data = get_bar_data(searched_string, self.chart.topbar['timeframe'].value)
@ -26,13 +26,16 @@ class API:
new_data = get_bar_data(self.chart.topbar['symbol'].value, self.chart.topbar['timeframe'].value)
if new_data.empty:
return
self.chart.set(new_data)
self.chart.set(new_data, render_drawings=True)
async def on_horizontal_line_move(self, line_id, price):
print(f'Horizontal line moved to: {price}')
async def main():
api = API()
chart = Chart(api=api, topbar=True, searchbox=True)
chart = Chart(api=api, topbar=True, searchbox=True, toolbox=True)
chart.legend(True)
chart.topbar.textbox('symbol', 'TSLA')
@ -41,6 +44,8 @@ async def main():
df = get_bar_data('TSLA', '5min')
chart.set(df)
chart.horizontal_line(200, interactive=True)
await chart.show_async(block=True)

View File

@ -11,7 +11,7 @@ from lightweight_charts.util import LINE_STYLE, MARKER_POSITION, MARKER_SHAPE, C
JS = {}
current_dir = os.path.dirname(os.path.abspath(__file__))
for file in ('pkg', 'funcs', 'callback'):
for file in ('pkg', 'funcs', 'callback', 'toolbox'):
with open(os.path.join(current_dir, 'js', f'{file}.js'), 'r', encoding='utf-8') as f:
JS[file] = f.read()
@ -59,10 +59,15 @@ class SeriesCommon:
}}
''')
def _df_datetime_format(self, df: pd.DataFrame):
def _df_datetime_format(self, df: pd.DataFrame, exclude_lowercase=None):
df = df.copy()
df.columns = df.columns.str.lower()
if exclude_lowercase:
df[exclude_lowercase] = df[exclude_lowercase.lower()]
if 'date' in df.columns:
df = df.rename(columns={'date': 'time'})
elif 'time' not in df.columns:
df['time'] = df.index
self._set_interval(df)
df['time'] = self._datetime_format(df['time'])
return df
@ -76,11 +81,14 @@ class SeriesCommon:
def _datetime_format(self, arg: Union[pd.Series, str]):
arg = pd.to_datetime(arg)
if self._interval != timedelta(days=1):
arg = arg.astype('int64') // 10 ** 9 if isinstance(arg, pd.Series) else arg.timestamp()
arg = self._interval.total_seconds() * (arg // self._interval.total_seconds())
if self._interval < timedelta(days=1):
if isinstance(arg, pd.Series):
arg = arg.astype('int64') // 10 ** 9
else:
arg = self._interval.total_seconds() * (arg.timestamp() // self._interval.total_seconds())
else:
arg = arg.dt.strftime('%Y-%m-%d') if isinstance(arg, pd.Series) else arg.strftime('%Y-%m-%d')
return arg
def marker(self, time: datetime = None, position: MARKER_POSITION = 'below', shape: MARKER_SHAPE = 'arrow_up',
@ -123,28 +131,21 @@ class SeriesCommon:
}}
}});''')
def horizontal_line(self, price: Union[float, int], color: str = 'rgb(122, 146, 202)', width: int = 1,
style: LINE_STYLE = 'solid', text: str = '', axis_label_visible=True):
def horizontal_line(self, price: Union[float, int], color: str = 'rgb(122, 146, 202)', width: int = 2,
style: LINE_STYLE = 'solid', text: str = '', axis_label_visible: bool = True, interactive: bool = False) -> 'HorizontalLine':
"""
Creates a horizontal line at the given price.\n
"""
line_id = self._rand.generate()
self.run_script(f"""
makeHorizontalLine({self.id}, '{line_id}', {price}, '{color}', {width}, {_line_style(style)}, {_js_bool(axis_label_visible)}, '{text}')
""")
return line_id
return HorizontalLine(self, price, color, width, style, text, axis_label_visible, interactive)
def remove_horizontal_line(self, price: Union[float, int]):
def remove_horizontal_line(self, price: Union[float, int] = None):
"""
Removes a horizontal line at the given price.
"""
self.run_script(f'''
{self.id}.horizontal_lines.forEach(function (line) {{
if ({price} === line.price) {{
{self.id}.series.removePriceLine(line.line);
{self.id}.horizontal_lines.splice({self.id}.horizontal_lines.indexOf(line), 1)
}}
}});''')
if ({price} === line.price) line.deleteLine()
}})''')
def clear_markers(self):
"""
@ -161,13 +162,22 @@ class SeriesCommon:
{self.id}.horizontal_lines = [];
''')
def title(self, title: str): self.run_script(f'{self.id}.series.applyOptions({{title: "{title}"}})')
def price_line(self, label_visible: bool = True, line_visible: bool = True):
def price_line(self, label_visible: bool = True, line_visible: bool = True, title: str = ''):
self.run_script(f'''
{self.id}.series.applyOptions({{
lastValueVisible: {_js_bool(label_visible)},
priceLineVisible: {_js_bool(line_visible)},
title: '{title}',
}})''')
def precision(self, precision: int):
"""
Sets the precision and minMove.\n
:param precision: The number of decimal places.
"""
self.run_script(f'''
{self.id}.series.applyOptions({{
priceFormat: {{precision: {precision}, minMove: {1 / (10 ** precision)}}}
}})''')
def hide_data(self): self._toggle_data(False)
@ -180,47 +190,79 @@ class SeriesCommon:
{f'{self.id}.volumeSeries.applyOptions({{visible: {_js_bool(arg)}}})' if hasattr(self, 'volume_enabled') and self.volume_enabled else ''}
''')
class HorizontalLine:
def __init__(self, chart, price, color, width, style, text, axis_label_visible, interactive):
self._chart = chart
self.id = self._chart._rand.generate()
self._chart.run_script(f'''
{self.id} = new HorizontalLine({self._chart.id}, '{self.id}', {price}, '{color}', {width}, {_line_style(style)}, {_js_bool(axis_label_visible)}, '{text}')
''')
if not interactive: return
self._chart.run_script(f'if ("toolBox" in {self._chart.id}) {self._chart.id}.toolBox.drawings.push({self.id})')
def update(self, price):
"""
Moves the horizontal line to the given price.
"""
self._chart.run_script(f'{self.id}.updatePrice({price})')
def delete(self):
"""
Irreversibly deletes the horizontal line.
"""
self._chart.run_script(f'{self.id}.deleteLine()')
del self
class Line(SeriesCommon):
def __init__(self, chart, color, width, price_line, price_label):
def __init__(self, chart, color, width, price_line, price_label, crosshair_marker=True):
self.color = color
self.name = ''
self._chart = chart
self._rand = chart._rand
self.id = f'window.{self._rand.generate()}'
self.id = self._rand.generate()
self.run_script = self._chart.run_script
self.run_script(f'''
{self.id} = {{
series: {self._chart.id}.chart.addLineSeries({{
color: '{color}',
lineWidth: {width},
lastValueVisible: {_js_bool(price_label)},
priceLineVisible: {_js_bool(price_line)},
{"""autoscaleInfoProvider: () => ({
priceRange: {
minValue: 1_000_000_000,
maxValue: 0,
},
}),""" if self._chart._scale_candles_only else ''}
}}),
markers: [],
horizontal_lines: [],
}}''')
{self.id} = {{
series: {self._chart.id}.chart.addLineSeries({{
color: '{color}',
lineWidth: {width},
lastValueVisible: {_js_bool(price_label)},
priceLineVisible: {_js_bool(price_line)},
crosshairMarkerVisible: {_js_bool(crosshair_marker)},
{"""autoscaleInfoProvider: () => ({
priceRange: {
minValue: 1_000_000_000,
maxValue: 0,
},
}),""" if self._chart._scale_candles_only else ''}
}}),
markers: [],
horizontal_lines: [],
name: '',
color: '{color}',
}}
{self._chart.id}.lines.push({self.id})
if ('legend' in {self._chart.id}) {{
{self._chart.id}.legend.makeLines({self._chart.id})
}}
''')
def set(self, data: pd.DataFrame, name=None):
def set(self, data: pd.DataFrame, name=''):
"""
Sets the line data.\n
:param data: If the name parameter is not used, the columns should be named: date/time, value.
:param name: The column of the DataFrame to use as the line value. When used, the Line will be named after this column.
"""
df = self._df_datetime_format(data)
df = self._df_datetime_format(data, exclude_lowercase=name)
if name:
if name not in data:
raise NameError(f'No column named "{name}".')
self.name = name
df = df.rename(columns={name: 'value'})
self._last_bar = df.iloc[-1]
self.run_script(f'{self.id}.series.setData({df.to_dict("records")})')
self.run_script(f'{self.id}.series.setData({df.to_dict("records")}); {self.id}.name = "{name}"')
def update(self, series: pd.Series):
"""
@ -231,11 +273,23 @@ class Line(SeriesCommon):
self._last_bar = series
self.run_script(f'{self.id}.series.update({series.to_dict()})')
def _set_trend(self, start_time, start_value, end_time, end_value, ray=False):
def time_format(time_val):
time_val = self._chart._datetime_format(time_val)
return f"'{time_val}'" if isinstance(time_val, str) else time_val
self.run_script(f'''
let logical
if ({_js_bool(ray)}) logical = {self._chart.id}.chart.timeScale().getVisibleLogicalRange()
{self.id}.series.setData(calculateTrendLine({time_format(start_time)}, {start_value}, {time_format(end_time)}, {end_value},
{self._chart._interval.total_seconds()*1000}, {self._chart.id}, {_js_bool(ray)}))
if (logical) {self._chart.id}.chart.timeScale().setVisibleLogicalRange(logical)
''')
def delete(self):
"""
Irreversibly deletes the line, as well as the object that contains the line.
"""
self._chart._lines.remove(self)
self._chart._lines.remove(self) if self in self._chart._lines else None
self.run_script(f'''
{self._chart.id}.chart.removeSeries({self.id}.series)
delete {self.id}
@ -253,7 +307,7 @@ class TextWidget(Widget):
def __init__(self, topbar, initial_text):
super().__init__(topbar)
self.value = initial_text
self.id = f"window.{self._chart._rand.generate()}"
self.id = self._chart._rand.generate()
self._chart.run_script(f'''{self.id} = makeTextBoxWidget({self._chart.id}, "{initial_text}")''')
def set(self, string):
@ -267,9 +321,9 @@ class SwitcherWidget(Widget):
self.value = default
self._method = method.__name__
self._chart.run_script(f'''
makeSwitcher({self._chart.id}, {list(options)}, '{default}', {self._chart._js_api_code}, '{method.__name__}',
makeSwitcher({self._chart.id}, {list(options)}, '{default}', '{method.__name__}',
'{topbar.active_background_color}', '{topbar.active_text_color}', '{topbar.text_color}', '{topbar.hover_color}')
{self._chart.id}.chart.resize(window.innerWidth*{self._chart._inner_width}, (window.innerHeight*{self._chart._inner_height})-{self._chart.id}.topBar.offsetHeight)
reSize({self._chart.id})
''')
@ -279,8 +333,7 @@ class TopBar:
self._widgets: Dict[str, Widget] = {}
self._chart.run_script(f'''
makeTopBar({self._chart.id})
{self._chart.id}.chart.resize(window.innerWidth*{self._chart._inner_width},
(window.innerHeight*{self._chart._inner_height})-{self._chart.id}.topBar.offsetHeight)
reSize({self._chart.id})
''')
self.active_background_color = 'rgba(0, 122, 255, 0.7)'
self.active_text_color = 'rgb(240, 240, 240)'
@ -302,15 +355,17 @@ class TopBar:
class LWC(SeriesCommon):
def __init__(self, volume_enabled: bool = True, inner_width: float = 1.0, inner_height: float = 1.0, dynamic_loading: bool = False,
scale_candles_only: bool = False):
scale_candles_only: bool = False, topbar: bool = False, searchbox: bool = False, toolbox: bool = False,
_js_api_code: str = '""', autosize=True, _run_script=None):
self.volume_enabled = volume_enabled
self._scale_candles_only = scale_candles_only
self._inner_width = inner_width
self._inner_height = inner_height
self._dynamic_loading = dynamic_loading
if _run_script:
self.run_script = _run_script
self._rand = IDGen()
self.id = f'window.{self._rand.generate()}'
self.id = self._rand.generate()
self._position = 'left'
self.loaded = False
self._html = HTML
@ -321,7 +376,7 @@ class LWC(SeriesCommon):
self._interval = None
self._charts = {self.id: self}
self._lines = []
self._js_api_code = None
self._js_api_code = _js_api_code
self._return_q = None
self._background_color = '#000000'
@ -331,6 +386,21 @@ class LWC(SeriesCommon):
from lightweight_charts.polygon import PolygonAPI
self.polygon: PolygonAPI = PolygonAPI(self)
self.run_script(f'''
{self.id} = makeChart({self._js_api_code}, {self._inner_width}, {self._inner_height}, autoSize={_js_bool(autosize)})
{self.id}.id = '{self.id}'
{self.id}.wrapper.style.float = "{self._position}"
''')
if toolbox:
self.run_script(JS['toolbox'])
self.run_script(f'{self.id}.toolBox = new ToolBox({self.id})')
if not topbar and not searchbox:
return
self.run_script(JS['callback'])
self.run_script(f'makeSpinner({self.id})')
self.topbar = TopBar(self) if topbar else None
self.run_script(f'{self.id}.search = makeSearchBox({self.id})') if searchbox else None
def _on_js_load(self):
if self.loaded:
return
@ -338,16 +408,6 @@ class LWC(SeriesCommon):
[self.run_script(script) for script in self._scripts]
[self.run_script(script) for script in self._final_scripts]
def _create_chart(self, autosize=True):
self.run_script(f'''
{self.id} = makeChart({self._inner_width}, {self._inner_height}, autoSize={_js_bool(autosize)})
{self.id}.id = '{self.id}'
{self.id}.wrapper.style.float = "{self._position}"
''')
def _make_search_box(self):
self.run_script(f'{self.id}.search = makeSearchBox({self.id}, {self._js_api_code})')
def run_script(self, script, run_last=False):
"""
For advanced users; evaluates JavaScript within the Webview.
@ -357,14 +417,17 @@ class LWC(SeriesCommon):
return
self._scripts.append(script) if not run_last else self._final_scripts.append(script)
def set(self, df: pd.DataFrame):
def set(self, df: pd.DataFrame = None, render_drawings=False):
"""
Sets the initial data for the chart.\n
:param df: columns: date/time, open, high, low, close, volume (if volume enabled).
:param render_drawings: Re-renders any drawings made through the toolbox. Otherwise, they will be deleted.
"""
if df.empty:
if df.empty or df is None:
self.run_script(f'{self.id}.series.setData([])')
self.run_script(f'{self.id}.volumeSeries.setData([])')
# self.run_script(f"if ('toolBox' in {self.id}) {self.id}.toolBox.{render_or_clear}()")
return
bars = self._df_datetime_format(df)
self._last_bar = bars.iloc[-1]
@ -416,6 +479,7 @@ class LWC(SeriesCommon):
}}, 50);
}});
''') if self._dynamic_loading else self.run_script(f'{self.id}.candleData = {bars}; {self.id}.series.setData({self.id}.candleData)')
self.run_script(f"if ('toolBox' in {self.id}) {self.id}.toolBox.{'clearDrawings' if not render_drawings else 'renderDrawings'}()")
def fit(self):
"""
@ -446,7 +510,6 @@ class LWC(SeriesCommon):
let barsInfo = {self.id}.series.barsInLogicalRange(logicalRange);
if ({self.id}.candleData[{self.id}.candleData.length-1].time === {bar['time']}) {{
{self.id}.shownData[{self.id}.shownData.length-1] = {bar}
{self.id}.candleData[{self.id}.candleData.length-1] = {bar}
}}
@ -461,7 +524,15 @@ class LWC(SeriesCommon):
{self.id}.candleData.push({bar})
}}
{self.id}.series.update({self.id}.shownData[{self.id}.shownData.length-1])
''') if self._dynamic_loading else self.run_script(f'{self.id}.series.update({bar})')
''') if self._dynamic_loading else self.run_script(f'''
if (chartTimeToDate({self.id}.candleData[{self.id}.candleData.length-1].time).getTime() === chartTimeToDate({bar['time']}).getTime()) {{
{self.id}.candleData[{self.id}.candleData.length-1] = {bar}
}}
else {{
{self.id}.candleData.push({bar})
}}
{self.id}.series.update({bar})
''')
def update_from_tick(self, series, cumulative_volume=False):
"""
@ -504,6 +575,16 @@ class LWC(SeriesCommon):
"""
return self._lines.copy()
def trend_line(self, start_time, start_value, end_time, end_value, color: str = '#1E80F0', width: int = 2) -> Line:
line = Line(self, color, width, price_line=False, price_label=False, crosshair_marker=False)
line._set_trend(start_time, start_value, end_time, end_value, ray=False)
return line
def ray_line(self, start_time, value, color: str = '#1E80F0', width: int = 2) -> Line:
line = Line(self, color, width, price_line=False, price_label=False, crosshair_marker=False)
line._set_trend(start_time, value, start_time, value, ray=True)
return line
def price_scale(self, mode: PRICE_SCALE_MODE = 'normal', align_labels: bool = True, border_visible: bool = False,
border_color: str = None, text_color: str = None, entire_text_only: bool = False,
ticks_visible: bool = False, scale_margin_top: float = 0.2, scale_margin_bottom: float = 0.2):
@ -657,48 +738,16 @@ class LWC(SeriesCommon):
}}
}})''')
def legend(self, visible: bool = False, ohlc: bool = True, percent: bool = True, lines: bool = True, color: str = None,
font_size: int = None, font_family: str = None):
def legend(self, visible: bool = False, ohlc: bool = True, percent: bool = True, lines: bool = True, color: str = 'rgb(191, 195, 203)',
font_size: int = 11, font_family: str = 'Monaco'):
"""
Configures the legend of the chart.
"""
if not visible:
return
lines_code = ''
for i, line in enumerate(self._lines):
lines_code += f'''finalString += `<span style="color: {line.color};">▨</span>{f' {line.name}'} :
${{legendItemFormat(param.seriesData.get({line.id}.series).value)}}<br>`;'''
self.run_script(f'''
{f"{self.id}.legend.style.color = '{color}'" if color else ''}
{f"{self.id}.legend.style.fontSize = {font_size}" if font_size else ''}
{f"{self.id}.legend.style.fontFamily = '{font_family}'" if font_family else ''}
legendItemFormat = (num) => num.toFixed(2).toString().padStart(8, ' ')
{self.id}.chart.subscribeCrosshairMove((param) => {{
if (param.time){{
let data = param.seriesData.get({self.id}.series);
let finalString = '<span style="line-height: 1.8;">'
if (data) {{
let ohlc = `O ${{legendItemFormat(data.open)}}
| H ${{legendItemFormat(data.high)}}
| L ${{legendItemFormat(data.low)}}
| C ${{legendItemFormat(data.close)}} `
let percentMove = ((data.close-data.open)/data.open)*100
let percent = `| ${{percentMove >= 0 ? '+' : ''}}${{percentMove.toFixed(2)}} %`
{'finalString += ohlc' if ohlc else ''}
{'finalString += percent' if percent else ''}
{'finalString += "<br>"' if ohlc or percent else ''}
}}
{lines_code if lines else ''}
{self.id}.legend.innerHTML = finalString+'</span>'
}}
else {{
{self.id}.legend.innerHTML = ''
}}
}});''')
{self.id}.legend = new Legend({self.id}, {_js_bool(ohlc)}, {_js_bool(percent)}, {_js_bool(lines)}, '{color}', {font_size}, '{font_family}')
''')
def spinner(self, visible): self.run_script(f"{self.id}.spinner.style.display = '{'block' if visible else 'none'}'")
@ -721,30 +770,24 @@ class LWC(SeriesCommon):
return b64decode(serial_data.split(',')[1])
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, str] = False,
topbar: bool = False, searchbox: bool = False):
subchart = SubChart(self, volume_enabled, position, width, height, sync, topbar, searchbox)
width: float = 0.5, height: float = 0.5, sync: Union[bool, str] = False, dynamic_loading: bool = False,
scale_candles_only: bool = False, topbar: bool = False, searchbox: bool = False, toolbox: bool = False):
subchart = SubChart(self, volume_enabled, position, width, height, sync, dynamic_loading, scale_candles_only, topbar, searchbox, toolbox)
self._charts[subchart.id] = subchart
return subchart
class SubChart(LWC):
def __init__(self, parent, volume_enabled, position, width, height, sync, topbar, searchbox):
super().__init__(volume_enabled, width, height)
def __init__(self, parent, volume_enabled, position, width, height, sync, dynamic_loading, scale_candles_only, topbar, searchbox, toolbox):
self._chart = parent._chart if isinstance(parent, SubChart) else parent
super().__init__(volume_enabled, width, height, dynamic_loading, scale_candles_only, topbar, searchbox, toolbox,
self._chart._js_api_code, _run_script=self._chart.run_script)
self._parent = parent
self._position = position
self._rand = self._chart._rand
self._js_api_code = self._chart._js_api_code
self._return_q = self._chart._return_q
self.run_script = self._chart.run_script
self._charts = self._chart._charts
self.id = f'window.{self._rand.generate()}'
self.polygon = self._chart.polygon._subchart(self)
self._create_chart()
self.topbar = TopBar(self) if topbar else None
self._make_search_box() if searchbox else None
if not sync:
return
sync_parent_id = self._parent.id if isinstance(sync, bool) else sync
@ -757,6 +800,3 @@ class SubChart(LWC):
self.run_script(f'''
{self.id}.chart.timeScale().setVisibleLogicalRange({sync_parent_id}.chart.timeScale().getVisibleLogicalRange())
''', run_last=True)

View File

@ -2,7 +2,7 @@ import asyncio
import multiprocessing as mp
import webview
from lightweight_charts.abstract import LWC, JS, TopBar
from lightweight_charts.abstract import LWC
class CallbackAPI:
@ -48,25 +48,17 @@ class PyWV:
class Chart(LWC):
def __init__(self, volume_enabled: bool = True, width: int = 800, height: int = 600, x: int = None, y: int = None,
on_top: bool = False, maximize: bool = False, debug: bool = False,
api: object = None, topbar: bool = False, searchbox: bool = False,
api: object = None, topbar: bool = False, searchbox: bool = False, toolbox: bool = False,
inner_width: float = 1.0, inner_height: float = 1.0, dynamic_loading: bool = False, scale_candles_only: bool = False):
super().__init__(volume_enabled, inner_width, inner_height, dynamic_loading, scale_candles_only)
super().__init__(volume_enabled, inner_width, inner_height, dynamic_loading, scale_candles_only, topbar, searchbox, toolbox, 'pywebview.api.callback')
self._q, self._emit_q, self._return_q = (mp.Queue() for _ in range(3))
self._exit, self._loaded = mp.Event(), mp.Event()
self._script_func = self._q.put
self._api = api
self._js_api_code = 'pywebview.api.callback'
self._process = mp.Process(target=PyWV, args=(self._q, self._exit, self._loaded, self._html,
width, height, x, y, on_top, maximize, debug,
self._emit_q, self._return_q), daemon=True)
self._process.start()
self._create_chart()
if not topbar and not searchbox:
return
self.run_script(JS['callback'])
self.run_script(f'makeSpinner({self.id})')
self.topbar = TopBar(self) if topbar else None
self._make_search_box() if searchbox else None
def show(self, block: bool = False):
"""
@ -101,7 +93,7 @@ class Chart(LWC):
widget.value = arg
await getattr(self._api, key)()
else:
await getattr(self._api, key)(arg)
await getattr(self._api, key)(*arg.split(';;;'))
continue
value = self.polygon._q.get()
func, args = value[0], value[1:]

View File

@ -1,4 +1,4 @@
function makeSearchBox(chart, callbackFunction) {
function makeSearchBox(chart) {
let searchWindow = document.createElement('div')
searchWindow.style.position = 'absolute'
searchWindow.style.top = '0'
@ -49,39 +49,28 @@ function makeSearchBox(chart, callbackFunction) {
chart.wrapper.addEventListener('mouseout', (event) => {
selectedChart = false
})
document.addEventListener('keydown', function(event) {
if (!selectedChart) {return}
if (event.altKey && event.code === 'KeyH') {
let price = chart.series.coordinateToPrice(yPrice)
let colorList = [
'rgba(228, 0, 16, 0.7)',
'rgba(255, 133, 34, 0.7)',
'rgba(164, 59, 176, 0.7)',
'rgba(129, 59, 102, 0.7)',
'rgba(91, 20, 248, 0.7)',
'rgba(32, 86, 249, 0.7)',
]
let color = colorList[Math.floor(Math.random()*colorList.length)]
makeHorizontalLine(chart, 0, price, color, 2, LightweightCharts.LineStyle.Solid, true, '')
}
chart.commandFunctions.push((event) => {
if (searchWindow.style.display === 'none') {
if (/^[a-zA-Z0-9]$/.test(event.key)) {
searchWindow.style.display = 'flex';
sBox.focus();
return true
}
else return false
}
else if (event.key === 'Enter') {
callbackFunction(`on_search__${chart.id}__${sBox.value}`)
chart.callbackFunction(`on_search__${chart.id}__${sBox.value}`)
searchWindow.style.display = 'none'
sBox.value = ''
return true
}
else if (event.key === 'Escape') {
searchWindow.style.display = 'none'
sBox.value = ''
return true
}
});
else return false
})
sBox.addEventListener('input', function() {
sBox.value = sBox.value.toUpperCase();
});
@ -115,7 +104,7 @@ function makeSpinner(chart) {
animateSpinner();
}
function makeSwitcher(chart, items, activeItem, callbackFunction, callbackName, activeBackgroundColor, activeColor, inactiveColor, hoverColor) {
function makeSwitcher(chart, items, activeItem, callbackName, activeBackgroundColor, activeColor, inactiveColor, hoverColor) {
let switcherElement = document.createElement('div');
switcherElement.style.margin = '4px 14px'
switcherElement.style.zIndex = '1000'
@ -155,7 +144,7 @@ function makeSwitcher(chart, items, activeItem, callbackFunction, callbackName,
element.style.color = items[index] === item ? 'activeColor' : inactiveColor
});
activeItem = item;
callbackFunction(`${callbackName}__${chart.id}__${item}`);
chart.callbackFunction(`${callbackName}__${chart.id}__${item}`);
}
chart.topBar.appendChild(switcherElement)
makeSeperator(chart.topBar)

View File

@ -1,14 +1,17 @@
function makeChart(innerWidth, innerHeight, autoSize=true) {
function makeChart(callbackFunction, innerWidth, innerHeight, autoSize=true) {
let chart = {
markers: [],
horizontal_lines: [],
lines: [],
wrapper: document.createElement('div'),
div: document.createElement('div'),
legend: document.createElement('div'),
scale: {
width: innerWidth,
height: innerHeight
height: innerHeight,
},
callbackFunction: callbackFunction,
candleData: [],
commandFunctions: []
}
chart.chart = LightweightCharts.createChart(chart.div, {
width: window.innerWidth*innerWidth,
@ -57,15 +60,6 @@ function makeChart(innerWidth, innerHeight, autoSize=true) {
chart.volumeSeries.priceScale().applyOptions({
scaleMargins: {top: 0.8, bottom: 0},
});
chart.legend.style.position = 'absolute'
chart.legend.style.zIndex = '1000'
chart.legend.style.width = `${(chart.scale.width*100)-8}vw`
chart.legend.style.top = '10px'
chart.legend.style.left = '10px'
chart.legend.style.fontFamily = 'Monaco'
chart.legend.style.fontSize = '11px'
chart.legend.style.color = 'rgb(191, 195, 203)'
chart.wrapper.style.width = `${100*innerWidth}%`
chart.wrapper.style.height = `${100*innerHeight}%`
chart.wrapper.style.display = 'flex'
@ -73,35 +67,190 @@ function makeChart(innerWidth, innerHeight, autoSize=true) {
chart.div.style.position = 'relative'
chart.div.style.display = 'flex'
chart.div.appendChild(chart.legend)
chart.wrapper.appendChild(chart.div)
document.getElementById('wrapper').append(chart.wrapper)
if (!autoSize) return chart
document.addEventListener('keydown', (event) => {
for (let i=0; i<chart.commandFunctions.length; i++) {
if (chart.commandFunctions[i](event)) break
}
})
let topBarOffset = 0
window.addEventListener('resize', function() {
if ('topBar' in chart) topBarOffset = chart.topBar.offsetHeight
chart.chart.resize(window.innerWidth*innerWidth, (window.innerHeight*innerHeight)-topBarOffset)
});
if (!autoSize) return chart
window.addEventListener('resize', () => reSize(chart))
return chart
}
function makeHorizontalLine(chart, lineId, price, color, width, style, axisLabelVisible, text) {
let priceLine = {
price: price,
color: color,
lineWidth: width,
lineStyle: style,
axisLabelVisible: axisLabelVisible,
title: text,
};
let line = {
line: chart.series.createPriceLine(priceLine),
price: price,
id: lineId,
};
chart.horizontal_lines.push(line)
function reSize(chart) {
let topBarOffset = 'topBar' in chart ? chart.topBar.offsetHeight : 0
chart.chart.resize(window.innerWidth*chart.scale.width, (window.innerHeight*chart.scale.height)-topBarOffset)
}
if (!window.HorizontalLine) {
class HorizontalLine {
constructor(chart, lineId, price, color, width, style, axisLabelVisible, text) {
this.updatePrice = this.updatePrice.bind(this)
this.deleteLine = this.deleteLine.bind(this)
this.chart = chart
this.price = price
this.id = lineId
this.priceLine = {
price: this.price,
color: color,
lineWidth: width,
lineStyle: style,
axisLabelVisible: axisLabelVisible,
title: text,
}
this.line = this.chart.series.createPriceLine(this.priceLine)
this.chart.horizontal_lines.push(this)
}
updatePrice(price) {
this.chart.series.removePriceLine(this.line)
this.price = price
this.priceLine.price = this.price
this.line = this.chart.series.createPriceLine(this.priceLine)
}
deleteLine() {
this.chart.series.removePriceLine(this.line)
this.chart.horizontal_lines.splice(this.chart.horizontal_lines.indexOf(this))
delete this
}
}
window.HorizontalLine = HorizontalLine
class Legend {
constructor(chart, ohlcEnabled = true, percentEnabled = true, linesEnabled = true,
color = 'rgb(191, 195, 203)', fontSize = '11', fontFamily = 'Monaco') {
this.div = document.createElement('div')
this.div.style.position = 'absolute'
this.div.style.zIndex = '3000'
this.div.style.top = '10px'
this.div.style.left = '10px'
this.div.style.display = 'flex'
this.div.style.flexDirection = 'column'
this.div.style.width = `${(chart.scale.width * 100) - 8}vw`
this.div.style.color = color
this.div.style.fontSize = fontSize + 'px'
this.div.style.fontFamily = fontFamily
this.candle = document.createElement('div')
this.div.appendChild(this.candle)
chart.div.appendChild(this.div)
this.color = color
this.linesEnabled = linesEnabled
this.makeLines(chart)
let legendItemFormat = (num) => num.toFixed(2).toString().padStart(8, ' ')
chart.chart.subscribeCrosshairMove((param) => {
if (param.time) {
let data = param.seriesData.get(chart.series);
let finalString = '<span style="line-height: 1.8;">'
if (data) {
this.candle.style.color = ''
let ohlc = `O ${legendItemFormat(data.open)}
| H ${legendItemFormat(data.high)}
| L ${legendItemFormat(data.low)}
| C ${legendItemFormat(data.close)} `
let percentMove = ((data.close - data.open) / data.open) * 100
let percent = `| ${percentMove >= 0 ? '+' : ''}${percentMove.toFixed(2)} %`
finalString += ohlcEnabled ? ohlc : ''
finalString += percentEnabled ? percent : ''
}
this.candle.innerHTML = finalString + '</span>'
this.lines.forEach((line) => {
if (!param.seriesData.get(line.line.series)) return
let price = legendItemFormat(param.seriesData.get(line.line.series).value)
line.div.innerHTML = `<span style="color: ${line.line.color};">▨</span> ${line.line.name} : ${price}`
})
} else {
this.candle.style.color = 'transparent'
}
});
}
makeLines(chart) {
this.lines = []
if (this.linesEnabled) {
chart.lines.forEach((line) => {
this.lines.push(this.makeLineRow(line))
})
}
}
makeLineRow(line) {
let openEye = `
<path style="fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke:${this.color};stroke-opacity:1;stroke-miterlimit:4;" d="M 21.998437 12 C 21.998437 12 18.998437 18 12 18 C 5.001562 18 2.001562 12 2.001562 12 C 2.001562 12 5.001562 6 12 6 C 18.998437 6 21.998437 12 21.998437 12 Z M 21.998437 12 " transform="matrix(0.833333,0,0,0.833333,0,0)"/>
<path style="fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke:${this.color};stroke-opacity:1;stroke-miterlimit:4;" d="M 15 12 C 15 13.654687 13.654687 15 12 15 C 10.345312 15 9 13.654687 9 12 C 9 10.345312 10.345312 9 12 9 C 13.654687 9 15 10.345312 15 12 Z M 15 12 " transform="matrix(0.833333,0,0,0.833333,0,0)"/>\`
`
let closedEye = `
<path style="fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke:${this.color};stroke-opacity:1;stroke-miterlimit:4;" d="M 20.001562 9 C 20.001562 9 19.678125 9.665625 18.998437 10.514062 M 12 14.001562 C 10.392187 14.001562 9.046875 13.589062 7.95 12.998437 M 12 14.001562 C 13.607812 14.001562 14.953125 13.589062 16.05 12.998437 M 12 14.001562 L 12 17.498437 M 3.998437 9 C 3.998437 9 4.354687 9.735937 5.104687 10.645312 M 7.95 12.998437 L 5.001562 15.998437 M 7.95 12.998437 C 6.689062 12.328125 5.751562 11.423437 5.104687 10.645312 M 16.05 12.998437 L 18.501562 15.998437 M 16.05 12.998437 C 17.38125 12.290625 18.351562 11.320312 18.998437 10.514062 M 5.104687 10.645312 L 2.001562 12 M 18.998437 10.514062 L 21.998437 12 " transform="matrix(0.833333,0,0,0.833333,0,0)"/>
`
let row = document.createElement('div')
row.style.display = 'flex'
row.style.alignItems = 'center'
let div = document.createElement('div')
let toggle = document.createElement('div')
toggle.style.borderRadius = '4px'
toggle.style.marginLeft = '10px'
let svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
svg.setAttribute("width", "22");
svg.setAttribute("height", "16");
let group = document.createElementNS("http://www.w3.org/2000/svg", "g");
group.innerHTML = openEye
let on = true
toggle.addEventListener('click', (event) => {
if (on) {
on = false
group.innerHTML = closedEye
line.series.applyOptions({
visible: false
})
} else {
on = true
line.series.applyOptions({
visible: true
})
group.innerHTML = openEye
}
})
toggle.addEventListener('mouseover', (event) => {
document.body.style.cursor = 'pointer'
toggle.style.backgroundColor = 'rgba(50, 50, 50, 0.5)'
})
toggle.addEventListener('mouseleave', (event) => {
document.body.style.cursor = 'default'
toggle.style.backgroundColor = 'transparent'
})
svg.appendChild(group)
toggle.appendChild(svg);
row.appendChild(div)
row.appendChild(toggle)
this.div.appendChild(row)
return {
div: div,
row: row,
toggle: toggle,
line: line,
}
}
}
window.Legend = Legend
}
function syncCrosshairs(childChart, parentChart) {
function crosshairHandler (e, thisChart, otherChart, otherHandler) {
@ -140,3 +289,108 @@ function syncCrosshairs(childChart, parentChart) {
parentChart.subscribeCrosshairMove(parentCrosshairHandler)
childChart.subscribeCrosshairMove(childCrosshairHandler)
}
function chartTimeToDate(stampOrBusiness) {
if (typeof stampOrBusiness === 'number') {
stampOrBusiness = new Date(stampOrBusiness*1000)
}
else if (typeof stampOrBusiness === 'string') {
let [year, month, day] = stampOrBusiness.split('-').map(Number)
stampOrBusiness = new Date(Date.UTC(year, month-1, day))
}
else {
stampOrBusiness = new Date(Date.UTC(stampOrBusiness.year, stampOrBusiness.month - 1, stampOrBusiness.day))
}
return stampOrBusiness
}
function dateToChartTime(date, interval) {
if (interval >= 24*60*60*1000) {
return {day: date.getUTCDate(), month: date.getUTCMonth()+1, year: date.getUTCFullYear()}
}
return Math.floor(date.getTime()/1000)
}
function calculateTrendLine(startDate, startValue, endDate, endValue, interval, chart, ray=false) {
let reversed = false
if (chartTimeToDate(endDate).getTime() < chartTimeToDate(startDate).getTime()) {
reversed = true;
[startDate, endDate] = [endDate, startDate];
}
let startIndex
if (chartTimeToDate(startDate).getTime() < chartTimeToDate(chart.candleData[0].time).getTime()) {
startIndex = 0
}
else {
startIndex = chart.candleData.findIndex(item => chartTimeToDate(item.time).getTime() === chartTimeToDate(startDate).getTime())
}
if (startIndex === -1) {
return []
}
let endIndex
if (ray) {
endIndex = chart.candleData.length+1000
startValue = endValue
}
else {
endIndex = chart.candleData.findIndex(item => chartTimeToDate(item.time).getTime() === chartTimeToDate(endDate).getTime())
if (endIndex === -1) {
let barsBetween = (chartTimeToDate(endDate)-chartTimeToDate(chart.candleData[chart.candleData.length-1].time))/interval
endIndex = chart.candleData.length-1+barsBetween
}
}
let numBars = endIndex-startIndex
const rate_of_change = (endValue - startValue) / numBars;
const trendData = [];
let currentDate = null
let iPastData = 0
for (let i = 0; i <= numBars; i++) {
if (chart.candleData[startIndex+i]) {
currentDate = chart.candleData[startIndex+i].time
}
else {
iPastData ++
currentDate = dateToChartTime(new Date(chartTimeToDate(chart.candleData[chart.candleData.length-1].time).getTime()+(iPastData*interval)), interval)
}
const currentValue = reversed ? startValue + rate_of_change * (numBars - i) : startValue + rate_of_change * i;
trendData.push({ time: currentDate, value: currentValue });
}
return trendData;
}
/*
let customMenu = document.createElement('div')
customMenu.style.position = 'absolute'
customMenu.style.zIndex = '10000'
customMenu.style.background = 'rgba(25, 25, 25, 0.7)'
customMenu.style.color = 'lightgrey'
customMenu.style.display = 'none'
customMenu.style.borderRadius = '5px'
customMenu.style.padding = '5px 10px'
document.body.appendChild(customMenu)
function menuItem(text) {
let elem = document.createElement('div')
elem.innerText = text
customMenu.appendChild(elem)
}
menuItem('Delete drawings')
menuItem('Hide all indicators')
menuItem('Save Chart State')
let closeMenu = (event) => {if (!customMenu.contains(event.target)) customMenu.style.display = 'none';}
document.addEventListener('contextmenu', function (event) {
event.preventDefault(); // Prevent default right-click menu
customMenu.style.left = event.clientX + 'px';
customMenu.style.top = event.clientY + 'px';
customMenu.style.display = 'block';
document.removeEventListener('click', closeMenu)
document.addEventListener('click', closeMenu)
});
*/

View File

@ -0,0 +1,484 @@
if (!window.ToolBox) {
class ToolBox {
constructor(chart) {
this.onTrendSelect = this.onTrendSelect.bind(this)
this.onHorzSelect = this.onHorzSelect.bind(this)
this.onRaySelect = this.onRaySelect.bind(this)
this.chart = chart
this.drawings = []
this.chart.cursor = 'default'
this.makingDrawing = false
this.interval = 24 * 60 * 60 * 1000
this.activeBackgroundColor = 'rgba(0, 122, 255, 0.7)'
this.activeIconColor = 'rgb(240, 240, 240)'
this.iconColor = 'lightgrey'
this.backgroundColor = 'transparent'
this.hoverColor = 'rgba(60, 60, 60, 0.7)'
this.elem = this.makeToolBox()
this.subscribeHoverMove()
}
makeToolBox() {
let toolBoxElem = document.createElement('div')
toolBoxElem.style.position = 'absolute'
toolBoxElem.style.zIndex = '2000'
toolBoxElem.style.display = 'flex'
toolBoxElem.style.alignItems = 'center'
toolBoxElem.style.top = '25%'
toolBoxElem.style.borderRight = '2px solid #3C434C'
toolBoxElem.style.borderTop = '2px solid #3C434C'
toolBoxElem.style.borderBottom = '2px solid #3C434C'
toolBoxElem.style.borderTopRightRadius = '4px'
toolBoxElem.style.borderBottomRightRadius = '4px'
toolBoxElem.style.backgroundColor = 'rgba(25, 27, 30, 0.5)'
toolBoxElem.style.flexDirection = 'column'
this.chart.activeIcon = null
let trend = this.makeToolBoxElement(this.onTrendSelect, 'KeyT', `<rect x="3.84" y="13.67" transform="matrix(0.7071 -0.7071 0.7071 0.7071 -5.9847 14.4482)" width="21.21" height="1.56"/><path d="M23,3.17L20.17,6L23,8.83L25.83,6L23,3.17z M23,7.41L21.59,6L23,4.59L24.41,6L23,7.41z"/><path d="M6,20.17L3.17,23L6,25.83L8.83,23L6,20.17z M6,24.41L4.59,23L6,21.59L7.41,23L6,24.41z"/>`)
let horz = this.makeToolBoxElement(this.onHorzSelect, 'KeyH', `<rect x="4" y="14" width="9" height="1"/><rect x="16" y="14" width="9" height="1"/><path d="M11.67,14.5l2.83,2.83l2.83-2.83l-2.83-2.83L11.67,14.5z M15.91,14.5l-1.41,1.41l-1.41-1.41l1.41-1.41L15.91,14.5z"/>`)
let ray = this.makeToolBoxElement(this.onRaySelect, 'KeyR', `<rect x="8" y="14" width="17" height="1"/><path d="M3.67,14.5l2.83,2.83l2.83-2.83L6.5,11.67L3.67,14.5z M7.91,14.5L6.5,15.91L5.09,14.5l1.41-1.41L7.91,14.5z"/>`)
//let testB = this.makeToolBoxElement(this.onTrendSelect, `<rect x="8" y="6" width="12" height="1"/><rect x="9" y="22" width="11" height="1"/><path d="M3.67,6.5L6.5,9.33L9.33,6.5L6.5,3.67L3.67,6.5z M7.91,6.5L6.5,7.91L5.09,6.5L6.5,5.09L7.91,6.5z"/><path d="M19.67,6.5l2.83,2.83l2.83-2.83L22.5,3.67L19.67,6.5z M23.91,6.5L22.5,7.91L21.09,6.5l1.41-1.41L23.91,6.5z"/><path d="M19.67,22.5l2.83,2.83l2.83-2.83l-2.83-2.83L19.67,22.5z M23.91,22.5l-1.41,1.41l-1.41-1.41l1.41-1.41L23.91,22.5z"/><path d="M3.67,22.5l2.83,2.83l2.83-2.83L6.5,19.67L3.67,22.5z M7.91,22.5L6.5,23.91L5.09,22.5l1.41-1.41L7.91,22.5z"/><rect x="22" y="9" width="1" height="11"/><rect x="6" y="9" width="1" height="11"/>`)
toolBoxElem.appendChild(trend)
toolBoxElem.appendChild(horz)
toolBoxElem.appendChild(ray)
//toolBoxElem.appendChild(testB)
this.chart.div.append(toolBoxElem)
let commandZHandler = (toDelete) => {
if (!toDelete) return
if ('price' in toDelete) {
if (toDelete.id !== 'toolBox') return commandZHandler(this.drawings[this.drawings.indexOf(toDelete) - 1])
this.chart.series.removePriceLine(toDelete.line)
} else {
let logical
if (toDelete.ray) logical = this.chart.chart.timeScale().getVisibleLogicalRange()
this.chart.chart.removeSeries(toDelete.line);
if (toDelete.ray) this.chart.chart.timeScale().setVisibleLogicalRange(logical)
}
this.drawings.splice(this.drawings.length - 1)
}
this.chart.commandFunctions.push((event) => {
if (event.metaKey && event.code === 'KeyZ') {
commandZHandler(this.drawings[this.drawings.length - 1])
return true
}
});
return toolBoxElem
}
makeToolBoxElement(action, keyCmd, paths) {
let elem = document.createElement('div')
elem.style.margin = '3px'
elem.style.borderRadius = '4px'
elem.style.display = 'flex'
let svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
svg.setAttribute("width", "29");
svg.setAttribute("height", "29");
let group = document.createElementNS("http://www.w3.org/2000/svg", "g");
group.innerHTML = paths
group.setAttribute("fill", this.iconColor)
svg.appendChild(group)
elem.appendChild(svg);
elem.addEventListener('mouseenter', () => {
elem.style.backgroundColor = elem === this.chart.activeIcon ? this.activeBackgroundColor : this.hoverColor
document.body.style.cursor = 'pointer'
})
elem.addEventListener('mouseleave', () => {
elem.style.backgroundColor = elem === this.chart.activeIcon ? this.activeBackgroundColor : this.backgroundColor
document.body.style.cursor = this.chart.cursor
})
elem.addEventListener('click', () => {
if (this.chart.activeIcon) {
this.chart.activeIcon.style.backgroundColor = this.backgroundColor
group.setAttribute("fill", this.iconColor)
document.body.style.cursor = 'crosshair'
this.chart.cursor = 'crosshair'
action(false)
}
if (this.chart.activeIcon === elem) {
this.chart.activeIcon = null
return
}
this.chart.activeIcon = elem
group.setAttribute("fill", this.activeIconColor)
elem.style.backgroundColor = this.activeBackgroundColor
document.body.style.cursor = 'crosshair'
this.chart.cursor = 'crosshair'
action(true)
})
this.chart.commandFunctions.push((event) => {
if (event.altKey && event.code === keyCmd) {
if (this.chart.activeIcon) {
this.chart.activeIcon.style.backgroundColor = this.backgroundColor
group.setAttribute("fill", this.iconColor)
document.body.style.cursor = 'crosshair'
this.chart.cursor = 'crosshair'
action(false)
}
this.chart.activeIcon = elem
group.setAttribute("fill", this.activeIconColor)
elem.style.backgroundColor = this.activeBackgroundColor
document.body.style.cursor = 'crosshair'
this.chart.cursor = 'crosshair'
action(true)
return true
}
})
return elem
}
onTrendSelect(toggle, ray = false) {
let trendLine = {
line: null,
markers: null,
data: null,
from: null,
to: null,
ray: ray,
}
let firstTime = null
let firstPrice = null
let currentTime = null
if (!toggle) {
this.chart.chart.unsubscribeClick(this.chart.clickHandler)
return
}
let crosshairHandlerTrend = (param) => {
this.chart.chart.unsubscribeCrosshairMove(crosshairHandlerTrend)
if (!this.makingDrawing) return
let logical
let lastCandleTime = this.chart.candleData[this.chart.candleData.length - 1].time
currentTime = this.chart.chart.timeScale().coordinateToTime(param.point.x)
if (!currentTime) {
let barsToMove = param.logical - this.chart.chart.timeScale().coordinateToLogical(this.chart.chart.timeScale().timeToCoordinate(lastCandleTime))
logical = barsToMove <= 0 ? null : this.chart.chart.timeScale().getVisibleLogicalRange()
currentTime = dateToChartTime(new Date(chartTimeToDate(this.chart.candleData[this.chart.candleData.length - 1].time).getTime() + (barsToMove * this.interval)), this.interval)
} else if (chartTimeToDate(lastCandleTime).getTime() <= chartTimeToDate(currentTime).getTime()) {
logical = this.chart.chart.timeScale().getVisibleLogicalRange()
}
let currentPrice = this.chart.series.coordinateToPrice(param.point.y)
if (!currentTime) return this.chart.chart.subscribeCrosshairMove(crosshairHandlerTrend)
trendLine.data = calculateTrendLine(firstTime, firstPrice, currentTime, currentPrice, this.interval, this.chart, ray)
trendLine.from = trendLine.data[0].time
trendLine.to = trendLine.data[trendLine.data.length - 1].time
if (ray) logical = this.chart.chart.timeScale().getVisibleLogicalRange()
trendLine.line.setData(trendLine.data)
if (logical) {
this.chart.chart.applyOptions({handleScroll: true})
setTimeout(() => {
this.chart.chart.timeScale().setVisibleLogicalRange(logical)
}, 1)
setTimeout(() => {
this.chart.chart.applyOptions({handleScroll: false})
}, 50)
}
if (!ray) {
trendLine.markers = [
{time: firstTime, position: 'inBar', color: '#1E80F0', shape: 'circle', size: 0.1},
{time: currentTime, position: 'inBar', color: '#1E80F0', shape: 'circle', size: 0.1}
]
trendLine.line.setMarkers(trendLine.markers)
}
setTimeout(() => {
this.chart.chart.subscribeCrosshairMove(crosshairHandlerTrend)
}, 10);
}
this.chart.clickHandler = (param) => {
if (!this.makingDrawing) {
this.makingDrawing = true
trendLine.line = this.chart.chart.addLineSeries({
lineWidth: 2,
lastValueVisible: false,
priceLineVisible: false,
crosshairMarkerVisible: false,
autoscaleInfoProvider: () => ({
priceRange: {
minValue: 1_000_000_000,
maxValue: 0,
},
}),
})
firstPrice = this.chart.series.coordinateToPrice(param.point.y)
firstTime = !ray ? this.chart.chart.timeScale().coordinateToTime(param.point.x) : this.chart.candleData[this.chart.candleData.length - 1].time
this.chart.chart.applyOptions({
handleScroll: false
})
this.chart.chart.subscribeCrosshairMove(crosshairHandlerTrend)
} else {
this.chart.chart.applyOptions({
handleScroll: true
})
this.makingDrawing = false
trendLine.line.setMarkers([])
this.drawings.push(trendLine)
this.chart.chart.unsubscribeCrosshairMove(crosshairHandlerTrend)
this.chart.chart.unsubscribeClick(this.chart.clickHandler)
document.body.style.cursor = 'default'
this.chart.cursor = 'default'
this.chart.activeIcon.style.backgroundColor = this.backgroundColor
this.chart.activeIcon = null
}
}
this.chart.chart.subscribeClick(this.chart.clickHandler)
}
onHorzSelect(toggle) {
let clickHandlerHorz = (param) => {
let price = this.chart.series.coordinateToPrice(param.point.y)
let lineStyle = LightweightCharts.LineStyle.Solid
let line = new HorizontalLine(this.chart, 'toolBox', price, null, 2, lineStyle, true)
this.drawings.push(line)
this.chart.chart.unsubscribeClick(clickHandlerHorz)
document.body.style.cursor = 'default'
this.chart.cursor = 'default'
this.chart.activeIcon.style.backgroundColor = this.backgroundColor
this.chart.activeIcon = null
}
this.chart.chart.subscribeClick(clickHandlerHorz)
}
onRaySelect(toggle) {
this.onTrendSelect(toggle, true)
}
subscribeHoverMove() {
let hoveringOver = null
let x, y
let hoverOver = (param) => {
if (!param.point || this.makingDrawing) return
this.chart.chart.unsubscribeCrosshairMove(hoverOver)
x = param.point.x
y = param.point.y
this.drawings.forEach((drawing) => {
let boundaryConditional
let horizontal = false
if ('price' in drawing) {
horizontal = true
let priceCoordinate = this.chart.series.priceToCoordinate(drawing.price)
boundaryConditional = Math.abs(priceCoordinate - param.point.y) < 6
} else {
let trendData = param.seriesData.get(drawing.line);
if (!trendData) return
let priceCoordinate = this.chart.series.priceToCoordinate(trendData.value)
let timeCoordinate = this.chart.chart.timeScale().timeToCoordinate(trendData.time)
boundaryConditional = Math.abs(priceCoordinate - param.point.y) < 6 && Math.abs(timeCoordinate - param.point.x) < 6
}
if (boundaryConditional) {
if (hoveringOver === drawing) return
if (!horizontal && !drawing.ray) drawing.line.setMarkers(drawing.markers)
document.body.style.cursor = 'pointer'
document.addEventListener('mousedown', checkForClick)
document.addEventListener('mouseup', checkForRelease)
hoveringOver = drawing
} else if (hoveringOver === drawing) {
if (!horizontal && !drawing.ray) drawing.line.setMarkers([])
document.body.style.cursor = this.chart.cursor
hoveringOver = null
}
})
this.chart.chart.subscribeCrosshairMove(hoverOver)
}
let originalIndex
let originalTime
let originalPrice
let mouseDown = false
let clickedEnd = false
let checkForClick = (event) => {
//if (!hoveringOver) return
mouseDown = true
document.body.style.cursor = 'grabbing'
this.chart.chart.applyOptions({
handleScroll: false
})
this.chart.chart.unsubscribeCrosshairMove(hoverOver)
// let [x, y] = [event.clientX, event.clientY]
// if ('topBar' in this.chart) y = y - this.chart.topBar.offsetHeight
if ('price' in hoveringOver) {
originalPrice = hoveringOver.price
this.chart.chart.subscribeCrosshairMove(crosshairHandlerHorz)
} else if (Math.abs(this.chart.chart.timeScale().timeToCoordinate(hoveringOver.data[0].time) - x) < 4 && !hoveringOver.ray) {
clickedEnd = 'first'
this.chart.chart.subscribeCrosshairMove(crosshairHandlerTrend)
} else if (Math.abs(this.chart.chart.timeScale().timeToCoordinate(hoveringOver.data[hoveringOver.data.length - 1].time) - x) < 4 && !hoveringOver.ray) {
clickedEnd = 'last'
this.chart.chart.subscribeCrosshairMove(crosshairHandlerTrend)
} else {
originalPrice = this.chart.series.coordinateToPrice(y)
originalTime = this.chart.chart.timeScale().coordinateToTime(x * this.chart.scale.width)
this.chart.chart.subscribeCrosshairMove(checkForDrag)
}
originalIndex = this.chart.chart.timeScale().coordinateToLogical(x)
this.chart.chart.unsubscribeClick(checkForClick)
}
let checkForRelease = (event) => {
mouseDown = false
document.body.style.cursor = 'pointer'
this.chart.chart.applyOptions({handleScroll: true})
if (hoveringOver && 'price' in hoveringOver && hoveringOver.id !== 'toolBox') {
this.chart.callbackFunction(`on_horizontal_line_move__${this.chart.id}__${hoveringOver.id};;;${hoveringOver.price.toFixed(8)}`);
}
hoveringOver = null
document.removeEventListener('mousedown', checkForClick)
document.removeEventListener('mouseup', checkForRelease)
this.chart.chart.subscribeCrosshairMove(hoverOver)
}
let checkForDrag = (param) => {
if (!param.point) return
this.chart.chart.unsubscribeCrosshairMove(checkForDrag)
if (!mouseDown) return
let priceAtCursor = this.chart.series.coordinateToPrice(param.point.y)
let priceDiff = priceAtCursor - originalPrice
let barsToMove = param.logical - originalIndex
let startBarIndex = this.chart.candleData.findIndex(item => chartTimeToDate(item.time).getTime() === chartTimeToDate(hoveringOver.data[0].time).getTime())
let endBarIndex = this.chart.candleData.findIndex(item => chartTimeToDate(item.time).getTime() === chartTimeToDate(hoveringOver.data[hoveringOver.data.length - 1].time).getTime())
let startBar
let endBar
if (hoveringOver.ray) {
endBar = this.chart.candleData[startBarIndex + barsToMove]
startBar = hoveringOver.data[hoveringOver.data.length - 1]
} else {
startBar = this.chart.candleData[startBarIndex + barsToMove]
endBar = endBarIndex === -1 ? null : this.chart.candleData[endBarIndex + barsToMove]
}
let endDate = endBar ? endBar.time : dateToChartTime(new Date(chartTimeToDate(hoveringOver.data[hoveringOver.data.length - 1].time).getTime() + (barsToMove * this.interval)), this.interval)
let startDate = startBar.time
let startValue = hoveringOver.data[0].value + priceDiff
let endValue = hoveringOver.data[hoveringOver.data.length - 1].value + priceDiff
hoveringOver.data = calculateTrendLine(startDate, startValue, endDate, endValue, this.interval, this.chart, hoveringOver.ray)
let logical
if (chartTimeToDate(hoveringOver.data[hoveringOver.data.length - 1].time).getTime() >= chartTimeToDate(this.chart.candleData[this.chart.candleData.length - 1].time).getTime()) {
logical = this.chart.chart.timeScale().getVisibleLogicalRange()
}
hoveringOver.from = hoveringOver.data[0].time
hoveringOver.to = hoveringOver.data[hoveringOver.data.length - 1].time
hoveringOver.line.setData(hoveringOver.data)
if (logical) this.chart.chart.timeScale().setVisibleLogicalRange(logical)
if (!hoveringOver.ray) {
hoveringOver.markers = [
{time: startDate, position: 'inBar', color: '#1E80F0', shape: 'circle', size: 0.1},
{time: endDate, position: 'inBar', color: '#1E80F0', shape: 'circle', size: 0.1}
]
hoveringOver.line.setMarkers(hoveringOver.markers)
}
originalIndex = param.logical
originalPrice = priceAtCursor
this.chart.chart.subscribeCrosshairMove(checkForDrag)
}
let crosshairHandlerTrend = (param) => {
if (!param.point) return
this.chart.chart.unsubscribeCrosshairMove(crosshairHandlerTrend)
if (!mouseDown) return
let currentPrice = this.chart.series.coordinateToPrice(param.point.y)
let currentTime = this.chart.chart.timeScale().coordinateToTime(param.point.x)
let [firstTime, firstPrice] = [null, null]
if (clickedEnd === 'last') {
firstTime = hoveringOver.data[0].time
firstPrice = hoveringOver.data[0].value
} else if (clickedEnd === 'first') {
firstTime = hoveringOver.data[hoveringOver.data.length - 1].time
firstPrice = hoveringOver.data[hoveringOver.data.length - 1].value
}
let logical
let lastCandleTime = this.chart.candleData[this.chart.candleData.length - 1].time
if (!currentTime) {
let barsToMove = param.logical - this.chart.chart.timeScale().coordinateToLogical(this.chart.chart.timeScale().timeToCoordinate(lastCandleTime))
logical = barsToMove <= 0 ? null : this.chart.chart.timeScale().getVisibleLogicalRange()
currentTime = dateToChartTime(new Date(chartTimeToDate(this.chart.candleData[this.chart.candleData.length - 1].time).getTime() + (barsToMove * this.interval)), this.interval)
} else if (chartTimeToDate(lastCandleTime).getTime() <= chartTimeToDate(currentTime).getTime()) {
logical = this.chart.chart.timeScale().getVisibleLogicalRange()
}
hoveringOver.data = calculateTrendLine(firstTime, firstPrice, currentTime, currentPrice, this.interval, this.chart)
hoveringOver.line.setData(hoveringOver.data)
hoveringOver.from = hoveringOver.data[0].time
hoveringOver.to = hoveringOver.data[hoveringOver.data.length - 1].time
if (logical) this.chart.chart.timeScale().setVisibleLogicalRange(logical)
hoveringOver.markers = [
{time: firstTime, position: 'inBar', color: '#1E80F0', shape: 'circle', size: 0.1},
{time: currentTime, position: 'inBar', color: '#1E80F0', shape: 'circle', size: 0.1}
]
hoveringOver.line.setMarkers(hoveringOver.markers)
setTimeout(() => {
this.chart.chart.subscribeCrosshairMove(crosshairHandlerTrend)
}, 10);
}
let crosshairHandlerHorz = (param) => {
if (!param.point) return
this.chart.chart.unsubscribeCrosshairMove(crosshairHandlerHorz)
if (!mouseDown) return
hoveringOver.updatePrice(this.chart.series.coordinateToPrice(param.point.y))
setTimeout(() => {
this.chart.chart.subscribeCrosshairMove(crosshairHandlerHorz)
}, 10)
}
this.chart.chart.subscribeCrosshairMove(hoverOver)
}
renderDrawings() {
//let logical = this.chart.chart.timeScale().getVisibleLogicalRange()
this.drawings.forEach((item) => {
if ('price' in item) return
let startDate = dateToChartTime(new Date(Math.round(chartTimeToDate(item.from).getTime() / this.interval) * this.interval), this.interval)
let endDate = dateToChartTime(new Date(Math.round(chartTimeToDate(item.to).getTime() / this.interval) * this.interval), this.interval)
let data = calculateTrendLine(startDate, item.data[0].value, endDate, item.data[item.data.length - 1].value, this.interval, this.chart, item.ray)
if (data.length !== 0) item.data = data
item.line.setData(data)
})
//this.chart.chart.timeScale().setVisibleLogicalRange(logical)
}
clearDrawings() {
this.drawings.forEach((item) => {
if ('price' in item) this.chart.series.removePriceLine(item.line)
else this.chart.chart.removeSeries(item.line)
})
this.drawings = []
}
}
window.ToolBox = ToolBox
}

View File

@ -49,6 +49,7 @@ class PolygonAPI:
self._using_live_data = False
self._using_live = {'stocks': False, 'options': False, 'indices': False, 'crypto': False, 'forex': False}
self._ws = {'stocks': None, 'options': None, 'indices': None, 'crypto': None, 'forex': None}
self._tickers = {}
def log(self, info: bool):
@ -156,7 +157,9 @@ class PolygonAPI:
columns.append('v')
df = df[columns].rename(columns=rename)
df['time'] = pd.to_datetime(df['time'], unit='ms')
chart.set(df)
chart.set(df, render_drawings=self._tickers.get(chart) == ticker)
self._tickers[chart] = ticker
if not live:
return True
@ -300,10 +303,10 @@ class PolygonChart(Chart):
def __init__(self, api_key: str, live: bool = False, num_bars: int = 200, end_date: str = 'now', limit: int = 5_000,
timeframe_options: tuple = ('1min', '5min', '30min', 'D', 'W'),
security_options: tuple = ('Stock', 'Option', 'Index', 'Forex', 'Crypto'),
width: int = 800, height: int = 600, x: int = None, y: int = None,
toolbox: bool = True, width: int = 800, height: int = 600, x: int = None, y: int = None,
on_top: bool = False, maximize: bool = False, debug: bool = False):
super().__init__(volume_enabled=True, width=width, height=height, x=x, y=y, on_top=on_top, maximize=maximize, debug=debug,
api=self, topbar=True, searchbox=True)
api=self, topbar=True, searchbox=True, toolbox=toolbox)
self.chart = self
self.num_bars = num_bars
self.end_date = end_date
@ -337,7 +340,7 @@ class PolygonChart(Chart):
def _polygon(self, symbol):
self.spinner(True)
self.set(pd.DataFrame())
self.set(pd.DataFrame(), True)
self.crosshair(vert_visible=False, horz_visible=False)
mult, span = _convert_timeframe(self.topbar['timeframe'].value)

View File

@ -27,7 +27,7 @@ class IDGen(list):
var = ''.join(choices(ascii_lowercase, k=8))
if var not in self:
self.append(var)
return var
return f'window.{var}'
self.generate()

View File

@ -29,7 +29,7 @@ try:
except ImportError:
HTML = None
from lightweight_charts.abstract import LWC, TopBar, JS
from lightweight_charts.abstract import LWC, JS
def _widget_message(chart, string):
@ -47,40 +47,41 @@ def _widget_message(chart, string):
class WxChart(LWC):
def __init__(self, parent, volume_enabled: bool = True, inner_width: float = 1.0, inner_height: float = 1.0,
scale_candles_only: bool = False, api: object = None, topbar: bool = False, searchbox: bool = False):
scale_candles_only: bool = False, api: object = None, topbar: bool = False, searchbox: bool = False,
toolbox: bool = False):
if wx is None:
raise ModuleNotFoundError('wx.html2 was not found, and must be installed to use WxChart.')
self.webview: wx.html2.WebView = wx.html2.WebView.New(parent)
super().__init__(volume_enabled, inner_width=inner_width, inner_height=inner_height, scale_candles_only=scale_candles_only)
super().__init__(volume_enabled, inner_width=inner_width, inner_height=inner_height,
scale_candles_only=scale_candles_only, topbar=topbar, searchbox=searchbox, toolbox=toolbox,
_js_api_code='window.wx_msg.postMessage.bind(window.wx_msg)')
self.api = api
self._script_func = self.webview.RunScript
self._js_api_code = 'window.wx_msg.postMessage.bind(window.wx_msg)'
self.webview.Bind(wx.html2.EVT_WEBVIEW_LOADED, lambda e: wx.CallLater(500, self._on_js_load))
self.webview.Bind(wx.html2.EVT_WEBVIEW_SCRIPT_MESSAGE_RECEIVED, lambda e: _widget_message(self, e.GetString()))
self.webview.AddScriptMessageHandler('wx_msg')
self.webview.SetPage(self._html, '')
self.webview.AddUserScript(JS['callback'])
self._create_chart()
self.topbar = TopBar(self) if topbar else None
self._make_search_box() if searchbox else None
self.webview.AddUserScript(JS['callback']) if topbar or searchbox else None
self.webview.AddUserScript(JS['toolbox']) if toolbox else None
def get_webview(self): return self.webview
class QtChart(LWC):
def __init__(self, widget=None, api: object = None, topbar: bool = False, searchbox: bool = False,
volume_enabled: bool = True, inner_width: float = 1.0, inner_height: float = 1.0, scale_candles_only: bool = False):
def __init__(self, widget=None, volume_enabled: bool = True, inner_width: float = 1.0, inner_height: float = 1.0,
scale_candles_only: bool = False, api: object = None, topbar: bool = False, searchbox: bool = False,
toolbox: bool = False):
if QWebEngineView is None:
raise ModuleNotFoundError('QWebEngineView was not found, and must be installed to use QtChart.')
self.webview = QWebEngineView(widget)
super().__init__(volume_enabled, inner_width=inner_width, inner_height=inner_height, scale_candles_only=scale_candles_only)
super().__init__(volume_enabled, inner_width=inner_width, inner_height=inner_height,
scale_candles_only=scale_candles_only, topbar=topbar, searchbox=searchbox, toolbox=toolbox,
_js_api_code='window.pythonObject.callback')
self.api = api
self._script_func = self.webview.page().runJavaScript
self._js_api_code = 'window.pythonObject.callback'
self.web_channel = QWebChannel()
self.bridge = Bridge(self)
@ -100,17 +101,12 @@ class QtChart(LWC):
'''
self.webview.page().setHtml(self._html)
self.run_script(JS['callback'])
self._create_chart()
self.topbar = TopBar(self) if topbar else None
self._make_search_box() if searchbox else None
def get_webview(self): return self.webview
class StaticLWC(LWC):
def __init__(self, volume_enabled=True, width=None, height=None, inner_width=1, inner_height=1, scale_candles_only: bool = False):
super().__init__(volume_enabled, inner_width, inner_height, scale_candles_only=scale_candles_only)
def __init__(self, volume_enabled=True, width=None, height=None, inner_width=1, inner_height=1, scale_candles_only: bool = False, toolbox=False, autosize=True):
super().__init__(volume_enabled, inner_width, inner_height, scale_candles_only=scale_candles_only, toolbox=toolbox, autosize=autosize)
self.width = width
self.height = height
self._html = self._html.replace('</script>\n</body>\n</html>', '')
@ -133,9 +129,8 @@ class StaticLWC(LWC):
class StreamlitChart(StaticLWC):
def __init__(self, volume_enabled=True, width=None, height=None, inner_width=1, inner_height=1, scale_candles_only: bool = False):
super().__init__(volume_enabled, width, height, inner_width, inner_height, scale_candles_only)
self._create_chart()
def __init__(self, volume_enabled=True, width=None, height=None, inner_width=1, inner_height=1, scale_candles_only: bool = False, toolbox: bool = False):
super().__init__(volume_enabled, width, height, inner_width, inner_height, scale_candles_only, toolbox)
def _load(self):
if html is None:
@ -144,11 +139,10 @@ class StreamlitChart(StaticLWC):
class JupyterChart(StaticLWC):
def __init__(self, volume_enabled=True, width=800, height=350, inner_width=1, inner_height=1, scale_candles_only: bool = False):
super().__init__(volume_enabled, width, height, inner_width, inner_height, scale_candles_only)
def __init__(self, volume_enabled=True, width: int = 800, height=350, inner_width=1, inner_height=1, scale_candles_only: bool = False, toolbox: bool = False):
super().__init__(volume_enabled, width, height, inner_width, inner_height, scale_candles_only, toolbox, autosize=False)
self._position = ""
self._create_chart(autosize=False)
self.run_script(f'''
for (var i = 0; i < document.getElementsByClassName("tv-lightweight-charts").length; i++) {{
var element = document.getElementsByClassName("tv-lightweight-charts")[i];

View File

@ -5,9 +5,9 @@ with open('README.md', 'r', encoding='utf-8') as f:
setup(
name='lightweight_charts',
version='1.0.13.4',
version='1.0.14',
packages=find_packages(),
python_requires='>=3.9',
python_requires='>=3.8',
install_requires=[
'pandas',
'pywebview',