diff --git a/README.md b/README.md index 942717f..556fdf1 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ [![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.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) +[![Documentation](https://img.shields.io/badge/documentation-006ee3)](https://lightweight-charts-python.readthedocs.io/en/latest/common_methods.html) ![cover](https://raw.githubusercontent.com/louisnw01/lightweight-charts-python/main/cover.png) @@ -24,9 +24,10 @@ ___ 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. 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/common_methods.html#create-subchart-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. +6. [Callbacks](https://lightweight-charts-python.readthedocs.io/en/latest/callbacks.html) allowing for timeframe (1min, 5min, 30min etc.) selectors, searching, hotkeys, and more. +7. Tables for watchlists, order entry, and trade management. 7. Direct integration of market data through [Polygon.io's](https://polygon.io/?utm_source=affiliate&utm_campaign=pythonlwcharts) market data API. __Supports:__ Jupyter Notebooks, PyQt, wxPython, Streamlit, and asyncio. @@ -194,9 +195,7 @@ ___ ### 6. Callbacks: ```python -import asyncio import pandas as pd - from lightweight_charts import Chart @@ -204,49 +203,45 @@ def get_bar_data(symbol, timeframe): if symbol not in ('AAPL', 'GOOGL', 'TSLA'): print(f'No data for "{symbol}"') return pd.DataFrame() - return pd.read_csv(f'bar_data/{symbol}_{timeframe}.csv') + return pd.read_csv(f'../examples/6_callbacks/bar_data/{symbol}_{timeframe}.csv') class API: def __init__(self): self.chart = None # Changes after each callback. - async def on_search(self, searched_string): # Called when the user searches. + def on_search(self, searched_string): # Called when the user searches. new_data = get_bar_data(searched_string, self.chart.topbar['timeframe'].value) if new_data.empty: return - self.chart.topbar['corner'].set(searched_string) + self.chart.topbar['symbol'].set(searched_string) self.chart.set(new_data) - async def on_timeframe_selection(self): # Called when the user changes the timeframe. - new_data = get_bar_data(self.chart.topbar['corner'].value, self.chart.topbar['timeframe'].value) + def on_timeframe_selection(self): # Called when the user changes the timeframe. + 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) - - async def on_horizontal_line_move(self, line_id, price): + self.chart.set(new_data, True) + + def on_horizontal_line_move(self, line_id, price): print(f'Horizontal line moved to: {price}') -async def main(): +if __name__ == '__main__': api = API() chart = Chart(api=api, topbar=True, searchbox=True, toolbox=True) chart.legend(True) - chart.topbar.textbox('corner', 'TSLA') + chart.topbar.textbox('symbol', 'TSLA') chart.topbar.switcher('timeframe', api.on_timeframe_selection, '1min', '5min', '30min', default='5min') df = get_bar_data('TSLA', '5min') chart.set(df) - - chart.horizontal_line(200, interactive=True) - - await chart.show_async(block=True) + chart.horizontal_line(200, interactive=True) -if __name__ == '__main__': - asyncio.run(main()) + chart.show(block=True) ``` ![callbacks gif](https://raw.githubusercontent.com/louisnw01/lightweight-charts-python/main/examples/6_callbacks/callbacks.gif) @@ -254,7 +249,7 @@ ___
-[![Documentation](https://img.shields.io/badge/documentation-006ee3)](https://lightweight-charts-python.readthedocs.io/en/latest/docs.html) +[![Documentation](https://img.shields.io/badge/documentation-006ee3)](https://lightweight-charts-python.readthedocs.io/en/latest/common_methods.html) Inquiries: [shaders_worker_0e@icloud.com](mailto:shaders_worker_0e@icloud.com) ___ diff --git a/cover.png b/cover.png index 291ef1b..890b8c9 100644 Binary files a/cover.png and b/cover.png differ diff --git a/docs/source/callbacks.md b/docs/source/callbacks.md new file mode 100644 index 0000000..48ee7fd --- /dev/null +++ b/docs/source/callbacks.md @@ -0,0 +1,132 @@ +# Callbacks + +The `Chart` object allows for asynchronous and synchronous callbacks to be passed back to python, allowing for more sophisticated chart layouts including searching, timeframe selectors text boxes, and hotkeys using the `add_hotkey` method. + +`QtChart`and `WxChart` can also use callbacks. + +A variety of the parameters below should be passed to the Chart upon decaration. +* `api`: The class object that the fixed callbacks will always be emitted to. +* `topbar`: Adds a [TopBar](#topbar) to the `Chart` or `SubChart` and allows use of the `create_switcher` method. +* `searchbox`: Adds a search box onto the `Chart` or `SubChart` that is activated by typing. + +___ +## How to use Callbacks + +Fixed Callbacks are emitted to the class given as the `api` parameter shown above. + +Take a look at this minimal example: + +```python +class API: + def __init__(self): + self.chart = None + + def on_search(self, string): + print(f'Search Text: "{string}" | Chart/SubChart ID: "{self.chart.id}"') +``` +Upon searching in a pane, the expected output would be akin to: +``` +Search Text: "AAPL" | Chart/SubChart ID: "window.blyjagcr" +``` +The ID shown above will change depending upon which pane was used to search, due to the instance of `self.chart` dynamically updating to the latest pane which triggered the callback. +`self.chart` will update upon each callback, allowing for access to the specific pane in question. + +```{important} +* When using `show` rather than `show_async`, block should be set to `True` (`chart.show(block=True)`). +* `API` class methods can be either coroutines or normal methods. +* Non fixed callbacks (switchers, hotkeys) can be methods, coroutines, or regular functions. +``` + +There are certain callbacks which are always emitted to a specifically named method of API: +* Search callbacks: `on_search` +* Interactive Horizontal Line callbacks: `on_horizontal_line_move` + +___ + +## `TopBar` +The `TopBar` class represents the top bar shown on the chart when using callbacks: + +![topbar](https://i.imgur.com/Qu2FW9Y.png) + +This class is accessed from the `topbar` attribute of the chart object (`chart.topbar.`), after setting the topbar parameter to `True` upon declaration of the chart. + +Switchers and text boxes can be created within the top bar, and their instances can be accessed through the `topbar` dictionary. For example: + +```python +chart = Chart(api=api, topbar=True) + +chart.topbar.textbox('symbol', 'AAPL') # Declares a textbox displaying 'AAPL'. +print(chart.topbar['symbol'].value) # Prints the value within ('AAPL') + +chart.topbar['symbol'].set('MSFT') # Sets the 'symbol' textbox to 'MSFT' +print(chart.topbar['symbol'].value) # Prints the value again ('MSFT') +``` +___ + +### `switcher` +`name: str` | `method: function` | `*options: str` | `default: str` + +* `name`: the name of the switcher which can be used to access it from the `topbar` dictionary. +* `method`: The function from the `api` class given to the constructor that will receive the callback. +* `options`: The strings to be displayed within the switcher. This may be a variety of timeframes, security types, or whatever needs to be updated directly from the chart. +* `default`: The initial switcher option set. +___ + +### `textbox` +`name: str` | `initial_text: str` + +* `name`: the name of the text box which can be used to access it from the `topbar` dictionary. +* `initial_text`: The text to show within the text box. +___ + +## Callbacks Example: + +```python +import pandas as pd +from lightweight_charts import Chart + + +def get_bar_data(symbol, timeframe): + if symbol not in ('AAPL', 'GOOGL', 'TSLA'): + print(f'No data for "{symbol}"') + return pd.DataFrame() + return pd.read_csv(f'../examples/6_callbacks/bar_data/{symbol}_{timeframe}.csv') + + +class API: + def __init__(self): + self.chart = None # Changes after each callback. + + def on_search(self, searched_string): # Called when the user searches. + new_data = get_bar_data(searched_string, self.chart.topbar['timeframe'].value) + if new_data.empty: + return + self.chart.topbar['symbol'].set(searched_string) + self.chart.set(new_data) + + def on_timeframe_selection(self): # Called when the user changes the timeframe. + 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, True) + + def on_horizontal_line_move(self, line_id, price): + print(f'Horizontal line moved to: {price}') + + +if __name__ == '__main__': + api = API() + + chart = Chart(api=api, topbar=True, searchbox=True, toolbox=True) + chart.legend(True) + + chart.topbar.textbox('symbol', 'TSLA') + chart.topbar.switcher('timeframe', api.on_timeframe_selection, '1min', '5min', '30min', default='5min') + + df = get_bar_data('TSLA', '5min') + chart.set(df) + + chart.horizontal_line(200, interactive=True) + + chart.show(block=True) +``` diff --git a/docs/source/charts.md b/docs/source/charts.md new file mode 100644 index 0000000..322bd5c --- /dev/null +++ b/docs/source/charts.md @@ -0,0 +1,209 @@ +# Charts + +This page contains a reference to all chart objects that can be used within the library. They all have access to the common methods. + +___ + +## 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` | `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, 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, which can be later shown by calling `chart.show()`. +___ + +### `exit` + +Exits and destroys the chart window. + +___ + +### `show_async` +`block: bool` + +Show the chart asynchronously. +___ + +### `screenshot` +`-> bytes` + +Takes a screenshot of the chart, and returns a bytes object containing the image. For example: + +```python +if __name__ == '__main__': + chart = Chart() + df = pd.read_csv('ohlcv.csv') + chart.set(df) + chart.show() + + img = chart.screenshot() + with open('screenshot.png', 'wb') as f: + f.write(img) +``` + +```{important} +This method should be called after the chart window has loaded. +``` +___ + +## QtChart +`widget: QWidget` | `volume_enabled: bool` + +The `QtChart` object allows the use of charts within a `QMainWindow` object, and has similar functionality to the `Chart` and `ChartAsync` objects for manipulating data, configuring and styling. + +Callbacks can be recieved through the Qt event loop. +___ + +### `get_webview` + +`-> QWebEngineView` + +Returns the `QWebEngineView` object. + +___ +### Example: + +```python +import pandas as pd +from PyQt5.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget + +from lightweight_charts.widgets import QtChart + +app = QApplication([]) +window = QMainWindow() +layout = QVBoxLayout() +widget = QWidget() +widget.setLayout(layout) + +window.resize(800, 500) +layout.setContentsMargins(0, 0, 0, 0) + +chart = QtChart(widget) + +df = pd.read_csv('ohlcv.csv') +chart.set(df) + +layout.addWidget(chart.get_webview()) + +window.setCentralWidget(widget) +window.show() + +app.exec_() +``` +___ + +## WxChart +`parent: wx.Panel` | `volume_enabled: bool` + +The WxChart object allows the use of charts within a `wx.Frame` object, and has similar functionality to the `Chart` and `ChartAsync` objects for manipulating data, configuring and styling. + +Callbacks can be recieved through the Wx event loop. +___ + +### `get_webview` +`-> wx.html2.WebView` + +Returns a `wx.html2.WebView` object which can be used to for positioning and styling within wxPython. +___ + +### Example: + +```python +import wx +import pandas as pd + +from lightweight_charts.widgets import WxChart + + +class MyFrame(wx.Frame): + def __init__(self): + super().__init__(None) + self.SetSize(1000, 500) + + panel = wx.Panel(self) + sizer = wx.BoxSizer(wx.VERTICAL) + panel.SetSizer(sizer) + + chart = WxChart(panel) + + df = pd.read_csv('ohlcv.csv') + chart.set(df) + + sizer.Add(chart.get_webview(), 1, wx.EXPAND | wx.ALL) + sizer.Layout() + self.Show() + + +if __name__ == '__main__': + app = wx.App() + frame = MyFrame() + app.MainLoop() + +``` +___ + +## StreamlitChart +`parent: wx.Panel` | `volume_enabled: bool` + +The `StreamlitChart` object allows the use of charts within a Streamlit app, and has similar functionality to the `Chart` object for manipulating data, configuring and styling. + +This object only supports the displaying of **static** data, and should not be used with the `update_from_tick` or `update` methods. Every call to the chart object must occur **before** calling `load`. +___ + +### `load` + +Loads the chart into the Streamlit app. This should be called after setting, styling, and configuring the chart, as no further calls to the `StreamlitChart` will be acknowledged. +___ + +### Example: +```python +import pandas as pd +from lightweight_charts.widgets import StreamlitChart + +chart = StreamlitChart(width=900, height=600) + +df = pd.read_csv('ohlcv.csv') +chart.set(df) + +chart.load() +``` +___ + +## JupyterChart + +The `JupyterChart` object allows the use of charts within a notebook, and has similar functionality to the `Chart` object for manipulating data, configuring and styling. + +This object only supports the displaying of **static** data, and should not be used with the `update_from_tick` or `update` methods. Every call to the chart object must occur **before** calling `load`. +___ + +### `load` + +Renders the chart. This should be called after setting, styling, and configuring the chart, as no further calls to the `JupyterChart` will be acknowledged. +___ + +### Example: +```python +import pandas as pd +from lightweight_charts import JupyterChart + +chart = JupyterChart() + +df = pd.read_csv('ohlcv.csv') +chart.set(df) + +chart.load() +``` diff --git a/docs/source/common_methods.md b/docs/source/common_methods.md new file mode 100644 index 0000000..70965bf --- /dev/null +++ b/docs/source/common_methods.md @@ -0,0 +1,362 @@ +# Common Methods +The methods below can be used within all chart objects. + +___ +## `set` +`data: pd.DataFrame` `render_drawings: bool` + +Sets the initial data for the chart. 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` 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. +``` + +An empty `DataFrame` object or `None` can also be given to this method, which will erase all candle and volume data displayed on the chart. +___ + +## `update` +`series: pd.Series` + +Updates the chart data from a `pd.Series` object. The bar should contain values with labels akin to `set`. +___ + +## `update_from_tick` +`series: pd.Series` | `cumulative_volume: bool` + +Updates the chart from a tick. The series should use the labels: + +`time | price | volume` + +As before, the `time` can also be named `date`, and the `volume` can be omitted if volume is not enabled. The `time` column can also be the name of the Series object. + +```{information} +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 will be added onto the latest bar of volume data. +___ + +## `create_line` (Line) +`color: str` | `width: int` | `price_line: bool` | `price_label: bool` | `-> Line` + +Creates and returns a `Line` object, representing 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). + +Its instance should only be accessed from this method. + + +### `set` +`data: pd.DataFrame` `name: str` + +Sets the data for the line. + +When not using the `name` parameter, the columns should be named: `time | value` (Not case sensitive). + +Otherwise, the method will use the column named after the string given in `name`. This name will also be used within the legend of the chart. For example: +```python +line = chart.create_line() + +# DataFrame with columns: date | SMA 50 +df = pd.read_csv('sma50.csv') + +line.set(df, name='SMA 50') +``` + + +### `update` +`series: pd.Series` + +Updates the data for the line. + +This should be given as a Series object, with labels akin to the `line.set()` function. + + +### `delete` +Irreversibly deletes the line. + +___ + +## `lines` +`-> List[Line]` + +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` +`marker_id: str` + +Removes the marker with the given id. + +Usage: +```python +marker = chart.marker(text='hello_world') +chart.remove_marker(marker) +``` +___ + +## `horizontal_line` (HorizontalLine) +`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, and returns a `HorizontalLine` object, representing a `PriceLine` in Lightweight Charts. + +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. + + +### `update` +`price: float/int` + +Updates the price of the horizontal line. + +### `label` +`text: str` + +Updates the label of the horizontal line. + +### `delete` + +Irreversibly deletes the horizontal line. +___ + +## `remove_horizontal_line` +`price: float/int` + +Removes a horizontal line at the given price. +___ + +## `clear_markers` + +Clears the markers displayed on the data. +___ + +## `clear_horizontal_lines` + +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` + +Price scale options for the chart. +___ + +## `time_scale` +`right_offset: int` | `min_bar_spacing: float` | `visible: bool` | `time_visible: bool` | `seconds_visible: bool` | `border_visible: bool` | `border_color: str` + +Timescale options for the chart. +___ + +## `layout` +`background_color: str` | `text_color: str` | `font_size: int` | `font_family: str` + +Global layout options for the chart. +___ + +## `grid` +`vert_enabled: bool` | `horz_enabled: bool` | `color: str` | `style: 'solid'/'dotted'/'dashed'/'large_dashed'/'sparse_dotted'` + +Grid options for the chart. +___ + +## `candle_style` +`up_color: str` | `down_color: str` | `wick_enabled: bool` | `border_enabled: bool` | `border_up_color: str` | `border_down_color: str` | `wick_up_color: str` | `wick_down_color: str` + + Candle styling for each of the candle's parts (border, wick). + +```{admonition} Color Formats +:class: note + +Throughout the library, colors should be given as either: +* rgb: `rgb(100, 100, 100)` +* rgba: `rgba(100, 100, 100, 0.7)` +* hex: `#32a852` +``` +___ + +## `volume_config` +`scale_margin_top: float` | `scale_margin_bottom: float` | `up_color: str` | `down_color: str` + +Volume config options. + +```{important} +The float values given to scale the margins must be greater than 0 and less than 1. +``` +___ + +## `crosshair` +`mode` | `vert_visible: bool` | `vert_width: int` | `vert_color: str` | `vert_style: str` | `vert_label_background_color: str` | `horz_visible: bool` | `horz_width: int` | `horz_color: str` | `horz_style: str` | `horz_label_background_color: str` + +Crosshair formatting for its vertical and horizontal axes. + +`vert_style` and `horz_style` should be given as one of: `'solid'/'dotted'/'dashed'/'large_dashed'/'sparse_dotted'` +___ + +## `watermark` +`text: str` | `font_size: int` | `color: str` + +Overlays a watermark on top of the chart. +___ + +## `legend` +`visible: bool` | `ohlc: bool` | `percent: bool` | `lines: bool` | `color: str` | `font_size: int` | `font_family: str` + +Configures the legend of the chart. +___ + +## `spinner` +`visible: bool` + +Shows a loading spinner on the chart, which can be used to visualise the loading of large datasets, API calls, etc. +___ + +## `price_line` +`label_visible: bool` | `line_visible: bool` | `title: str` + +Configures the visibility of the last value price line and its label. +___ + +## `fit` + +Attempts to fit all data displayed on the chart within the viewport (`fitContent()`). +___ + +## `hide_data` + +Hides the candles on the chart. +___ + +## `show_data` + +Shows the hidden candles on the chart. +___ + +## `add_hotkey` +`modifier: 'ctrl'/'shift'/'alt'/'meta'` | `key: str/int/tuple` | `method: object` + +Adds a global hotkey to the chart window, which will execute the method or function given. + +When using a number in `key`, it should be given as an integer. If multiple key commands are needed for the same function, you can pass a tuple to `key`. For example: + +```python +def place_buy_order(key): + print(f'Buy {key} shares.') + + +def place_sell_order(key): + print(f'Sell all shares, because I pressed {key}.') + + +chart.add_hotkey('shift', (1, 2, 3), place_buy_order) +chart.add_hotkey('shift', 'X', place_sell_order) +``` + +___ + +## `create_table` +`width: int/float` | `height: int/float` | `headings: tuple[str]` | `widths: tuple[float]` | `alignments: tuple[str]` | `position: 'left'/'right'/'top'/'bottom'` | `draggable: bool` | `method: object` + +Creates and returns a [`Table`](https://lightweight-charts-python.readthedocs.io/en/latest/tables.html) object. +___ + +## `create_subchart` (SubChart) +`volume_enabled: bool` | `position: 'left'/'right'/'top'/'bottom'`, `width: float` | `height: float` | `sync: bool/str` | `-> SubChart` + +Creates and returns a `SubChart` object, placing it adjacent to the previous `Chart` or `SubChart`. This allows for the use of multiple chart panels within the same `Chart` window. Its instance should only be accessed by using this method. + +`position`: specifies how the Subchart will float. + +`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 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` should be given as a number between 0 and 1. +``` + +`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 = chart.create_subchart(position='left', width=0.5, height=0.5) + chart4 = chart.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) + chart.time_scale(visible=False) + + chart2 = chart.create_subchart(width=1, height=0.2, sync=True, volume_enabled=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) + +``` + + diff --git a/docs/source/conf.py b/docs/source/conf.py index b78e2d2..57feba5 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -1,7 +1,7 @@ project = 'lightweight-charts-python' copyright = '2023, louisnw' author = 'louisnw' -release = '1.0.14.4' +release = '1.0.15' extensions = ["myst_parser"] diff --git a/docs/source/docs.md b/docs/source/docs.md deleted file mode 100644 index 9f1dbdf..0000000 --- a/docs/source/docs.md +++ /dev/null @@ -1,773 +0,0 @@ -# Docs - -[![louisnw01 - lightweight-charts-python](https://img.shields.io/static/v1?label=louisnw01&message=lightweight-charts-python&color=057dfc&logo=github)](https://github.com/louisnw01/lightweight-charts-python "Go to GitHub repo") -[![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") -[![License](https://img.shields.io/github/license/louisnw01/lightweight-charts-python?color=9c2400)](https://github.com/louisnw01/lightweight-charts-python/blob/main/LICENSE) -[![Stars - lightweight-charts-python](https://img.shields.io/github/stars/louisnw01/lightweight-charts-python?style=social)](https://github.com/louisnw01/lightweight-charts-python) -[![Forks - lightweight-charts-python](https://img.shields.io/github/forks/louisnw01/lightweight-charts-python?style=social)](https://github.com/louisnw01/lightweight-charts-python) -___ - -## Common Methods -The methods below can be used within all chart objects. - -___ -### `set` -`data: pd.DataFrame` `render_drawings: bool` - -Sets the initial data for the chart. - -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` 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. -``` - -An empty `DataFrame` object can also be given to this method, which will erase all candle and volume data displayed on the chart. -___ - -### `update` -`series: pd.Series` - -Updates the chart data from a given bar. - -The bar should contain values with labels of the same name as the columns required for using `chart.set()`. -___ - -### `update_from_tick` -`series: pd.Series` | `cumulative_volume: bool` - -Updates the chart from a tick. - -The series should use the labels: - -`time | price | volume` - -As before, the `time` can also be named `date`, and the `volume` can be omitted if volume is not enabled. - -```{information} -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 will be added onto the latest bar of volume data. -___ - -### `create_line` -`color: str` | `width: int` | `price_line: bool` | `price_label: bool` | `-> Line` - -Creates and returns a [Line](#line) object. -___ - -### `lines` -`-> List[Line]` - -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` -`marker_id: str` - -Removes the marker with the given id. - -Usage: -```python -marker = chart.marker(text='hello_world') -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` | `interactive: bool` | `-> HorizontalLine` - -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` -`price: float/int` - -Removes a horizontal line at the given price. -___ - -### `clear_markers` - -Clears the markers displayed on the data. -___ - -### `clear_horizontal_lines` - -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` - -Price scale options for the chart. -___ - -### `time_scale` -`right_offset: int` | `min_bar_spacing: float` | `visible: bool` | `time_visible: bool` | `seconds_visible: bool` | `border_visible: bool` | `border_color: str` - -Timescale options for the chart. -___ - -### `layout` -`background_color: str` | `text_color: str` | `font_size: int` | `font_family: str` - -Global layout options for the chart. -___ - -### `grid` -`vert_enabled: bool` | `horz_enabled: bool` | `color: str` | `style: 'solid'/'dotted'/'dashed'/'large_dashed'/'sparse_dotted'` - -Grid options for the chart. -___ - -### `candle_style` -`up_color: str` | `down_color: str` | `wick_enabled: bool` | `border_enabled: bool` | `border_up_color: str` | `border_down_color: str` | `wick_up_color: str` | `wick_down_color: str` - - Candle styling for each of the candle's parts (border, wick). - -```{admonition} Color Formats -:class: note - -Throughout the library, colors should be given as either: -* rgb: `rgb(100, 100, 100)` -* rgba: `rgba(100, 100, 100, 0.7)` -* hex: `#32a852` -``` -___ - -### `volume_config` -`scale_margin_top: float` | `scale_margin_bottom: float` | `up_color: str` | `down_color: str` - -Volume config options. - -```{important} -The float values given to scale the margins must be greater than 0 and less than 1. -``` -___ - -### `crosshair` -`mode` | `vert_visible: bool` | `vert_width: int` | `vert_color: str` | `vert_style: str` | `vert_label_background_color: str` | `horz_visible: bool` | `horz_width: int` | `horz_color: str` | `horz_style: str` | `horz_label_background_color: str` - -Crosshair formatting for its vertical and horizontal axes. - -`vert_style` and `horz_style` should be given as one of: `'solid'/'dotted'/'dashed'/'large_dashed'/'sparse_dotted'` -___ - -### `watermark` -`text: str` | `font_size: int` | `color: str` - -Overlays a watermark on top of the chart. -___ - -### `legend` -`visible: bool` | `ohlc: bool` | `percent: bool` | `lines: bool` | `color: str` | `font_size: int` | `font_family: str` - -Configures the legend of the chart. -___ - -### `spinner` -`visible: bool` - -Shows a loading spinner on the chart, which can be used to visualise the loading of large datasets, API calls, etc. -___ - -### `price_line` -`label_visible: bool` | `line_visible: bool` | `title: str` - -Configures the visibility of the last value price line and its label. -___ - -### `fit` - -Attempts to fit all data displayed on the chart within the viewport (`fitContent()`). -___ - -### `hide_data` - -Hides the candles on the chart. -___ - -### `show_data` - -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)). -___ - -### `create_subchart` -`volume_enabled: bool` | `position: 'left'/'right'/'top'/'bottom'`, `width: float` | `height: float` | `sync: bool/str` | `-> SubChart` - -Creates and returns a [SubChart](#subchart) object, placing it adjacent to the declaring `Chart` or `SubChart`. - -`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 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` should be given as a number between 0 and 1. -``` - -___ - - -## 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` | `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, 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, which can be later shown by calling `chart.show()`. -___ - -### `exit` - -Exits and destroys the chart window. - -___ - -### `show_async` -`block: bool` - -Show the chart asynchronously. This should be utilised when using [Callbacks](#callbacks). - -### `screenshot` -`-> bytes` - -Takes a screenshot of the chart, and returns a bytes object containing the image. For example: - -```python -if __name__ == '__main__': - chart = Chart() - df = pd.read_csv('ohlcv.csv') - chart.set(df) - chart.show() - - img = chart.screenshot() - with open('screenshot.png', 'wb') as f: - f.write(img) -``` - -```{important} -This method should be called after the chart window has loaded. -``` -___ - -### `add_hotkey` -`modifier: 'ctrl'/'shift'/'alt'/'meta'` | `key: str/int/tuple` | `method: object` - -Adds a global hotkey to the chart window, which will execute the method or function given. - -When using a number in `key`, it should be given as an integer. If multiple key commands are needed for the same function, you can pass a tuple to `key`. For example: - -```python -def place_buy_order(key): - print(f'Buy {key} shares.') - - -def place_sell_order(key): - print(f'Sell all shares, because I pressed {key}.') - - -chart.add_hotkey('shift', (1, 2, 3), place_buy_order) -chart.add_hotkey('shift', 'X', place_sell_order) -``` - - - -___ - -## 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). - -```{important} -The `Line` object should only be accessed from the [`create_line`](#create-line) method of `Chart`. -``` -___ - -### `set` -`data: pd.DataFrame` `name: str` - -Sets the data for the line. - -When not using the `name` parameter, the columns should be named: `time | value` (Not case sensitive). - -Otherwise, the method will use the column named after the string given in `name`. This name will also be used within the legend of the chart. For example: -```python -line = chart.create_line() - -# DataFrame with columns: date | SMA 50 -df = pd.read_csv('sma50.csv') - -line.set(df, name='SMA 50') -``` -___ - -### `update` -`series: pd.Series` - -Updates the data for the line. - -This should be given as a Series object, with labels akin to the `line.set()` function. -___ - -### `delete` - -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 - -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`. Its instance should be accessed using the [create_subchart](#create-subchart) method. - -`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) - chart.time_scale(visible=False) - - chart2 = chart.create_subchart(width=1, height=0.2, sync=True, volume_enabled=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) - -``` -___ - -## Callbacks - -The `Chart` object allows for asynchronous and synchronous callbacks to be passed back to python when using the `show_async` method, allowing for more sophisticated chart layouts including searching, timeframe selectors, and text boxes. - -[`QtChart`](#qtchart) and [`WxChart`](#wxchart) can also use callbacks, however they use their respective event loops to emit callbacks rather than asyncio. - -A variety of the parameters below should be passed to the Chart upon decaration. -* `api`: The class object that the fixed callbacks will always be emitted to (see [How to use Callbacks](#how-to-use-callbacks)). -* `topbar`: Adds a [TopBar](#topbar) to the `Chart` or `SubChart` and allows use of the `create_switcher` method. -* `searchbox`: Adds a search box onto the `Chart` or `SubChart` that is activated by typing. - -___ -### How to use Callbacks - -Fixed Callbacks are emitted to the class given as the `api` parameter shown above. - -Take a look at this minimal example: - -```python -class API: - def __init__(self): - self.chart = None - - async def on_search(self, string): - print(f'Search Text: "{string}" | Chart/SubChart ID: "{self.chart.id}"') -``` -Upon searching in a pane, the expected output would be akin to: -``` -Search Text: "AAPL" | Chart/SubChart ID: "window.blyjagcr" -``` -The ID shown above will change depending upon which pane was used to search, due to the instance of `self.chart` dynamically updating to the latest pane which triggered the callback. -`self.chart` will update upon each callback, allowing for access to the specific [Common Methods](#common-methods) for the pane in question. - -```{important} -* Search callbacks will always be emitted to a method named `on_search` -* `API` class methods can be either coroutines or normal methods. -* Non fixed callbacks (switchers, hotkeys) can be methods, coroutines, or regular functions. -``` -___ - -### `TopBar` -The `TopBar` class represents the top bar shown on the chart when using callbacks: - -![topbar](https://i.imgur.com/Qu2FW9Y.png) - -This class is accessed from the `topbar` attribute of the chart object (`chart.topbar.`), after setting the topbar parameter to `True` upon declaration of the chart. - -Switchers and text boxes can be created within the top bar, and their instances can be accessed through the `topbar` dictionary. For example: - -```python -chart = Chart(api=api, topbar=True) - -chart.topbar.textbox('symbol', 'AAPL') # Declares a textbox displaying 'AAPL'. -print(chart.topbar['symbol'].value) # Prints the value within ('AAPL') - -chart.topbar['symbol'].set('MSFT') # Sets the 'symbol' textbox to 'MSFT' -print(chart.topbar['symbol'].value) # Prints the value again ('MSFT') -``` -___ - -### `switcher` -`name: str` | `method: function` | `*options: str` | `default: str` - -* `name`: the name of the switcher which can be used to access it from the `topbar` dictionary. -* `method`: The function from the `api` class given to the constructor that will receive the callback. -* `options`: The strings to be displayed within the switcher. This may be a variety of timeframes, security types, or whatever needs to be updated directly from the chart. -* `default`: The initial switcher option set. -___ - -### `textbox` -`name: str` | `initial_text: str` - -* `name`: the name of the text box which can be used to access it from the `topbar` dictionary. -* `initial_text`: The text to show within the text box. -___ - -### Callbacks Example: - -```python -import asyncio -import pandas as pd -from my_favorite_broker import get_bar_data - -from lightweight_charts import Chart - - -class API: - def __init__(self): - self.chart = None - - async def on_search(self, searched_string): # Called when the user searches. - timeframe = self.chart.topbar['timeframe'].value - new_data = await get_bar_data(searched_string, timeframe) - if not new_data: - return - self.chart.set(new_data) # sets data for the Chart or SubChart in question. - self.chart.topbar['symbol'].set(searched_string) - - async def on_timeframe(self): # Called when the user changes the timeframe. - timeframe = self.chart.topbar['timeframe'].value - symbol = self.chart.topbar['symbol'].value - new_data = await get_bar_data(symbol, timeframe) - if not new_data: - return - self.chart.set(new_data) - - -async def main(): - api = API() - - chart = Chart(api=api, topbar=True, searchbox=True) - - chart.topbar.textbox('symbol', 'TSLA') - chart.topbar.switcher('timeframe', api.on_timeframe, '1min', '5min', '30min', 'H', 'D', 'W', default='5min') - - df = pd.read_csv('ohlcv.csv') - chart.set(df) - - await chart.show_async(block=True) - - -if __name__ == '__main__': - asyncio.run(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 or Ctrl+Z: Undo - -Drawings can also be deleted by right-clicking on them, which brings up a context menu. -___ - -### `save_drawings_under` -`widget: Widget` - -Saves drawings under a specific `topbar` text widget. For example: - -```python -chart.toolbox.save_drawings_under(chart.topbar['symbol']) -``` -___ - -### `load_drawings` -`tag: str` - -Loads and displays drawings stored under the tag given. -___ -### `import_drawings` -`file_path: str` - -Imports the drawings stored at the JSON file given in `file_path`. - -___ -### `export_drawings` -`file_path: str` - -Exports all currently saved drawings to the JSON file given in `file_path`. -___ - -## QtChart -`widget: QWidget` | `volume_enabled: bool` - -The `QtChart` object allows the use of charts within a `QMainWindow` object, and has similar functionality to the `Chart` and `ChartAsync` objects for manipulating data, configuring and styling. - -Callbacks can be recieved through the Qt event loop, using an [API](#how-to-use-callbacks) class that uses **syncronous** methods instead of **asyncronous** methods. -___ - -### `get_webview` - -`-> QWebEngineView` - -Returns the `QWebEngineView` object. - -___ -### Example: - -```python -import pandas as pd -from PyQt5.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget - -from lightweight_charts.widgets import QtChart - -app = QApplication([]) -window = QMainWindow() -layout = QVBoxLayout() -widget = QWidget() -widget.setLayout(layout) - -window.resize(800, 500) -layout.setContentsMargins(0, 0, 0, 0) - -chart = QtChart(widget) - -df = pd.read_csv('ohlcv.csv') -chart.set(df) - -layout.addWidget(chart.get_webview()) - -window.setCentralWidget(widget) -window.show() - -app.exec_() -``` -___ - -## WxChart -`parent: wx.Panel` | `volume_enabled: bool` - -The WxChart object allows the use of charts within a `wx.Frame` object, and has similar functionality to the `Chart` and `ChartAsync` objects for manipulating data, configuring and styling. - -Callbacks can be recieved through the Wx event loop, using an [API](#how-to-use-callbacks) class that uses **syncronous** methods instead of **asyncronous** methods. -___ - -### `get_webview` -`-> wx.html2.WebView` - -Returns a `wx.html2.WebView` object which can be used to for positioning and styling within wxPython. -___ - -### Example: - -```python -import wx -import pandas as pd - -from lightweight_charts.widgets import WxChart - - -class MyFrame(wx.Frame): - def __init__(self): - super().__init__(None) - self.SetSize(1000, 500) - - panel = wx.Panel(self) - sizer = wx.BoxSizer(wx.VERTICAL) - panel.SetSizer(sizer) - - chart = WxChart(panel) - - df = pd.read_csv('ohlcv.csv') - chart.set(df) - - sizer.Add(chart.get_webview(), 1, wx.EXPAND | wx.ALL) - sizer.Layout() - self.Show() - - -if __name__ == '__main__': - app = wx.App() - frame = MyFrame() - app.MainLoop() - -``` -___ - -## StreamlitChart -`parent: wx.Panel` | `volume_enabled: bool` - -The `StreamlitChart` object allows the use of charts within a Streamlit app, and has similar functionality to the `Chart` object for manipulating data, configuring and styling. - -This object only supports the displaying of **static** data, and should not be used with the `update_from_tick` or `update` methods. Every call to the chart object must occur **before** calling `load`. -___ - -### `load` - -Loads the chart into the Streamlit app. This should be called after setting, styling, and configuring the chart, as no further calls to the `StreamlitChart` will be acknowledged. -___ - -### Example: -```python -import pandas as pd -from lightweight_charts.widgets import StreamlitChart - -chart = StreamlitChart(width=900, height=600) - -df = pd.read_csv('ohlcv.csv') -chart.set(df) - -chart.load() -``` -___ - -## JupyterChart - -The `JupyterChart` object allows the use of charts within a notebook, and has similar functionality to the `Chart` object for manipulating data, configuring and styling. - -This object only supports the displaying of **static** data, and should not be used with the `update_from_tick` or `update` methods. Every call to the chart object must occur **before** calling `load`. -___ - -### `load` - -Renders the chart. This should be called after setting, styling, and configuring the chart, as no further calls to the `JupyterChart` will be acknowledged. -___ - -### Example: -```python -import pandas as pd -from lightweight_charts import JupyterChart - -chart = JupyterChart() - -df = pd.read_csv('ohlcv.csv') -chart.set(df) - -chart.load() -``` diff --git a/docs/source/index.md b/docs/source/index.md index 05b6e69..d2b32fb 100644 --- a/docs/source/index.md +++ b/docs/source/index.md @@ -1,7 +1,11 @@ ```{toctree} :hidden: -docs +common_methods +charts +callbacks +toolbox +tables polygon Github Repository ``` diff --git a/docs/source/polygon.md b/docs/source/polygon.md index ba3bbdd..9492aa6 100644 --- a/docs/source/polygon.md +++ b/docs/source/polygon.md @@ -8,7 +8,7 @@ To use data from Polygon, there are certain libraries (not listed as requirement * Live data requires the `websockets` library. ___ ## `polygon` -`polygon` is a [Common Method](https://lightweight-charts-python.readthedocs.io/en/latest/docs.html#common-methods), and can be accessed from within any chart type. +`polygon` is a [Common Method](https://lightweight-charts-python.readthedocs.io/en/latest/common_methods.html), and can be accessed from within any chart type. `chart.polygon.` @@ -107,7 +107,7 @@ This object requires the `requests` library for static data, and the `websockets All data is requested within the chart window through searching and selectors. -As well as the parameters from the [Chart](https://lightweight-charts-python.readthedocs.io/en/latest/docs.html#chart) object, PolygonChart also has the parameters: +As well as the parameters from the [Chart](https://lightweight-charts-python.readthedocs.io/en/latest/charts.html#chart) object, PolygonChart also has the parameters: * `api_key`: The user's Polygon.io API key. * `num_bars`: The target number of bars to be displayed on the chart diff --git a/docs/source/tables.md b/docs/source/tables.md new file mode 100644 index 0000000..95015cd --- /dev/null +++ b/docs/source/tables.md @@ -0,0 +1,115 @@ +# Table +`width: int/float` | `height: int/float` | `headings: tuple[str]` | `widths: tuple[float]` | `alignments: tuple[str]` | `position: 'left'/'right'/'top'/'bottom'` | `draggable: bool` | `method: object` + +Tables are panes that can be used to gain further functionality from charts. They are intended to be used for watchlists, order management, or position management. It should be accessed from the `create_table` common method. + +The `Table` and `Row` objects inherit from dictionaries, and can be manipulated as such. + + + +`width`/`height`: Either given as a percentage (a `float` between 0 and 1) or as an integer representing pixel size. + +`widths`: Given as a `float` between 0 and 1. + +`position`: Used as you would when creating a `SubChart`, representing how the table will float within the window. + +`draggable`: If `True`, then the window can be dragged to any position within the window. + +`method`: If given this will be called when a row is clicked. +___ + +## `new_row` (Row) +`*values` | `id: int` | `-> Row` + +Creates a new row within the table, and returns a `Row` object. + +if `id` is passed it should be unique to all other rows. Otherwise, the `id` will be randomly generated. + + +### `background_color` +`column: str` | `color: str` + +Sets the background color of the Row cell at the given column. + +### `delete` +Deletes the row. +___ + +## `clear` +Clears and deletes all table rows. +___ + +## `format` +`column: str` | `format_str: str` + +Sets the format to be used for the given column. `table.VALUE` should be used as a placeholder for the cell value. For example: + +```python +table.format('Daily %', f'{table.VALUE} %') +table.format('PL', f'$ {table.VALUE}') +``` +___ + +## `visible` +`visible: bool` + +Sets the visibility of the Table. +___ + +## Footer +Tables can also have a footer containing a number of text boxes. To initialize this, call the `footer` attribute with the number of textboxes to be used: + +```python +table.footer(3) # Footer will be displayed, with 3 text boxes. +``` +To edit the textboxes, treat `footer` as a list: + +```python +table.footer[0] = 'Text Box 1' +table.footer[1] = 'Text Box 2' +table.footer[2] = 'Text Box 3' +``` +___ + +## Example: + +```python +import pandas as pd +from lightweight_charts import Chart + +def on_row_click(row_id): + row = table.get(row_id) + print(row) + + row['PL'] = round(row['PL']+1, 2) + row.background_color('PL', 'green' if row['PL'] > 0 else 'red') + + table.footer[1] = row['Ticker'] + +if __name__ == '__main__': + chart = Chart(width=1000, inner_width=0.7, inner_height=1) + subchart = chart.create_subchart(width=0.3, height=0.5) + df = pd.read_csv('ohlcv.csv') + chart.set(df) + subchart.set(df) + + table = chart.create_table(width=0.3, height=0.2, + headings=('Ticker', 'Quantity', 'Status', '%', 'PL'), + widths=(0.2, 0.1, 0.2, 0.2, 0.3), + alignments=('center', 'center', 'right', 'right', 'right'), + position='left', method=on_row_click) + + table.format('PL', f'£ {table.VALUE}') + table.format('%', f'{table.VALUE} %') + + table.new_row('SPY', 3, 'Submitted', 0, 0) + table.new_row('AMD', 1, 'Filled', 25.5, 105.24) + table.new_row('NVDA', 2, 'Filled', -0.5, -8.24) + + table.footer(2) + table.footer[0] = 'Selected:' + + chart.show(block=True) + +``` + diff --git a/docs/source/toolbox.md b/docs/source/toolbox.md new file mode 100644 index 0000000..62adc41 --- /dev/null +++ b/docs/source/toolbox.md @@ -0,0 +1,102 @@ +# 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 Toolbox should only be accessed from the `toolbox` attribute of the chart object. (`chart.toolbox.`) + +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 or Ctrl+Z: Undo + +Drawings can also be deleted by right-clicking on them, which brings up a context menu. +___ + +## `save_drawings_under` +`widget: Widget` + +Saves drawings under a specific `topbar` text widget. For example: + +```python +chart.toolbox.save_drawings_under(chart.topbar['symbol']) +``` +___ + +## `load_drawings` +`tag: str` + +Loads and displays drawings stored under the tag given. +___ + +## `import_drawings` +`file_path: str` + +Imports the drawings stored at the JSON file given in `file_path`. +___ + +## `export_drawings` +`file_path: str` + +Exports all currently saved drawings to the JSON file given in `file_path`. +___ + +## Example: + +To get started, create a file called `drawings.json`, which should only contain `{}`. + +```python +import pandas as pd +from lightweight_charts import Chart + + +def get_bar_data(symbol, timeframe): + if symbol not in ('AAPL', 'GOOGL', 'TSLA'): + print(f'No data for "{symbol}"') + return pd.DataFrame() + return pd.read_csv(f'bar_data/{symbol}_{timeframe}.csv') + + +class API: + def __init__(self): + self.chart: Chart = None + + def on_search(self, searched_string): + new_data = get_bar_data(searched_string, self.chart.topbar['timeframe'].value) + if new_data.empty: + return + self.chart.topbar['symbol'].set(searched_string) + self.chart.set(new_data) + self.chart.toolbox.load_drawings(searched_string) # Loads the drawings saved under the symbol. + + def on_timeframe_selection(self): + 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, render_drawings=True) # The symbol has not changed, so we want to re-render the drawings. + + +if __name__ == '__main__': + api = API() + + chart = Chart(api=api, topbar=True, searchbox=True, toolbox=True) + chart.legend(True) + + chart.topbar.textbox('symbol', 'TSLA') + chart.topbar.switcher('timeframe', api.on_timeframe_selection, '1min', '5min', '30min', default='5min') + + df = get_bar_data('TSLA', '5min') + + chart.set(df) + + chart.toolbox.import_drawings('drawings.json') # Imports the drawings saved in the JSON file. + chart.toolbox.load_drawings(chart.topbar['symbol'].value) # Loads the drawings under the default symbol. + + chart.toolbox.save_drawings_under(chart.topbar['symbol']) # Saves drawings based on the symbol. + + chart.show(block=True) + + chart.toolbox.export_drawings('drawings.json') # Exports the drawings to the JSON file. + +``` diff --git a/examples/6_callbacks/callbacks.py b/examples/6_callbacks/callbacks.py index 5c9b867..e7c4120 100644 --- a/examples/6_callbacks/callbacks.py +++ b/examples/6_callbacks/callbacks.py @@ -1,6 +1,4 @@ -import asyncio import pandas as pd - from lightweight_charts import Chart @@ -15,24 +13,24 @@ class API: def __init__(self): self.chart: Chart = None # Changes after each callback. - async def on_search(self, searched_string): # Called when the user searches. + def on_search(self, searched_string): # Called when the user searches. new_data = get_bar_data(searched_string, self.chart.topbar['timeframe'].value) if new_data.empty: return self.chart.topbar['symbol'].set(searched_string) self.chart.set(new_data) - async def on_timeframe_selection(self): # Called when the user changes the timeframe. + def on_timeframe_selection(self): # Called when the user changes the timeframe. 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, render_drawings=True) + self.chart.set(new_data, True) - async def on_horizontal_line_move(self, line_id, price): + def on_horizontal_line_move(self, line_id, price): print(f'Horizontal line moved to: {price}') -async def main(): +if __name__ == '__main__': api = API() chart = Chart(api=api, topbar=True, searchbox=True, toolbox=True) @@ -46,8 +44,4 @@ async def main(): chart.horizontal_line(200, interactive=True) - await chart.show_async(block=True) - - -if __name__ == '__main__': - asyncio.run(main()) + chart.show(block=True) \ No newline at end of file diff --git a/lightweight_charts/abstract.py b/lightweight_charts/abstract.py index 49d445d..81144eb 100644 --- a/lightweight_charts/abstract.py +++ b/lightweight_charts/abstract.py @@ -5,6 +5,7 @@ import pandas as pd from typing import Union, Literal, Dict, List +from lightweight_charts.table import Table from lightweight_charts.util import LINE_STYLE, MARKER_POSITION, MARKER_SHAPE, CROSSHAIR_MODE, _crosshair_mode, \ _line_style, \ MissingColumn, _js_bool, _price_scale_mode, PRICE_SCALE_MODE, _marker_position, _marker_shape, IDGen @@ -12,7 +13,7 @@ JS = {} current_dir = os.path.dirname(os.path.abspath(__file__)) -for file in ('pkg', 'funcs', 'callback', 'toolbox'): +for file in ('pkg', 'funcs', 'callback', 'toolbox', 'table'): with open(os.path.join(current_dir, 'js', f'{file}.js'), 'r', encoding='utf-8') as f: JS[file] = f.read() @@ -41,6 +42,7 @@
@@ -89,6 +91,8 @@ def _series_datetime_format(self, series: pd.Series, exclude_lowercase=None): self._rename(series, {exclude_lowercase.lower(): exclude_lowercase}, False) if 'date' in series.index: self._rename(series, {'date': 'time'}, False) + elif 'time' not in series.index: + series['time'] = series.name series['time'] = self._datetime_format(series['time']) return series @@ -219,6 +223,9 @@ def update(self, price): """ self._chart.run_script(f'{self.id}.updatePrice({price})') + def label(self, text: str): + self._chart.run_script(f'{self.id}.updateLabel("{text}")') + def delete(self): """ Irreversibly deletes the horizontal line. @@ -258,7 +265,7 @@ def __init__(self, chart, color, width, price_line, price_label, crosshair_marke {self._chart.id}.lines.push({self.id}) if ('legend' in {self._chart.id}) {{ {self._chart.id}.legend.makeLines({self._chart.id}) - }} + }} ''') @@ -268,6 +275,9 @@ def set(self, data: pd.DataFrame, name=''): :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. """ + if data.empty or data is None: + self.run_script(f'{self.id}.series.setData([]); {self.id}.name = "{name}"') + return df = self._df_datetime_format(data, exclude_lowercase=name) if name: if name not in data: @@ -282,7 +292,9 @@ def update(self, series: pd.Series): Updates the line data.\n :param series: labels: date/time, value """ - series = self._series_datetime_format(series) + series = self._series_datetime_format(series, exclude_lowercase=self.name) + if self.name in series.index: + series.rename({self.name: 'value'}, inplace=True) self._last_bar = series self.run_script(f'{self.id}.series.update({series.to_dict()})') @@ -413,7 +425,7 @@ def _save_drawings(self, drawings): 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, topbar: bool = False, searchbox: bool = False, toolbox: bool = False, - _js_api_code: str = '""', autosize=True, _run_script=None): + _js_api_code: str = None, autosize=True, _run_script=None): self.volume_enabled = volume_enabled self._scale_candles_only = scale_candles_only self._inner_width = inner_width @@ -433,7 +445,7 @@ def __init__(self, volume_enabled: bool = True, inner_width: float = 1.0, inner_ self._interval = None self._charts = {self.id: self} self._lines = [] - self._js_api_code = _js_api_code + self.run_script(f'window.callbackFunction = {_js_api_code}') if _js_api_code else None self._methods = {} self._return_q = None @@ -445,7 +457,7 @@ def __init__(self, volume_enabled: bool = True, inner_width: float = 1.0, inner_ 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} = makeChart({self._inner_width}, {self._inner_height}, autoSize={_js_bool(autosize)}) {self.id}.id = '{self.id}' {self.id}.wrapper.style.float = "{self._position}" ''') @@ -820,7 +832,7 @@ def screenshot(self) -> bytes: canvas.toBlob(function(blob) {{ const reader = new FileReader(); reader.onload = function(event) {{ - {self._js_api_code}(`return_~_{self.id}_~_${{event.target.result}}`) + window.callbackFunction(`return_~_{self.id}_~_${{event.target.result}}`) }}; reader.readAsDataURL(blob); }}) @@ -837,12 +849,17 @@ def add_hotkey(self, modifier_key: Literal['ctrl', 'alt', 'shift', 'meta'], keys {self.id}.commandFunctions.unshift((event) => {{ if (event.{modifier_key + 'Key'} && event.code === '{key_code}') {{ event.preventDefault() - {self.id}.callbackFunction(`{str(method)}_~_{self.id}_~_{key}`) + window.callbackFunction(`{str(method)}_~_{self.id}_~_{key}`) return true }} else return false }})''') + def create_table(self, width: Union[float, int], height: Union[float, int], headings: tuple, widths: tuple = None, alignments: tuple = None, + position: str = 'left', draggable: bool = False, method: object = None): + self._methods[str(method)] = method + return Table(self, width, height, headings, widths, alignments, position, draggable, method) + 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, dynamic_loading: bool = False, scale_candles_only: bool = False, topbar: bool = False, searchbox: bool = False, toolbox: bool = False): @@ -854,8 +871,7 @@ def create_subchart(self, volume_enabled: bool = True, position: Literal['left', class SubChart(LWC): 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) + super().__init__(volume_enabled, width, height, dynamic_loading, scale_candles_only, topbar, searchbox, toolbox, _run_script=self._chart.run_script) self._parent = parent self._position = position self._return_q = self._chart._return_q diff --git a/lightweight_charts/chart.py b/lightweight_charts/chart.py index a140835..9613581 100644 --- a/lightweight_charts/chart.py +++ b/lightweight_charts/chart.py @@ -5,6 +5,10 @@ from lightweight_charts.abstract import LWC +chart = None +num_charts = 0 + + class CallbackAPI: def __init__(self, emit_queue, return_queue): self.emit_q, self.return_q = emit_queue, return_queue @@ -17,33 +21,43 @@ def callback(self, message: str): class PyWV: - def __init__(self, q, exit, loaded, html, width, height, x, y, on_top, maximize, debug, emit_queue, return_queue): + def __init__(self, q, start: mp.Event, exit, loaded, html, width, height, x, y, on_top, maximize, debug, emit_queue, return_queue): if maximize: width, height = webview.screens[0].width, webview.screens[0].height self.queue = q self.exit = exit - self.loaded = loaded - self.debug = debug - js_api = CallbackAPI(emit_queue, return_queue) - self.webview = webview.create_window('', html=html, on_top=on_top, js_api=js_api, width=width, height=height, - x=x, y=y, background_color='#000000') - self.webview.events.loaded += self.on_js_load - self.loop() - - def loop(self): + self.callback_api = CallbackAPI(emit_queue, return_queue) + self.loaded: list = loaded + + self.windows = [] + self.create_window(html, on_top, width, height, x, y) + + start.wait() + webview.start(debug=debug) + self.exit.set() + + def create_window(self, html, on_top, width, height, x, y): + self.windows.append(webview.create_window( + '', html=html, on_top=on_top, js_api=self.callback_api, + width=width, height=height, x=x, y=y, background_color='#000000')) + self.windows[-1].events.loaded += lambda: self.loop(self.loaded[len(self.windows)-1]) + + def loop(self, loaded): + loaded.set() while 1: - arg = self.queue.get() - if arg in ('start', 'show', 'hide', 'exit'): - webview.start(debug=self.debug) if arg == 'start' else getattr(self.webview, arg)() - self.exit.set() if arg in ('start', 'exit') else None + i, arg = self.queue.get() + if i == 'create_window': + self.create_window(*arg) + elif arg in ('show', 'hide'): + getattr(self.windows[i], arg)() + elif arg == 'exit': + self.exit.set() else: try: - self.webview.evaluate_js(arg) + self.windows[i].evaluate_js(arg) except KeyError: return - def on_js_load(self): self.loaded.set(), self.loop() - class Chart(LWC): def __init__(self, volume_enabled: bool = True, width: int = 800, height: int = 600, x: int = None, y: int = None, @@ -51,14 +65,30 @@ def __init__(self, volume_enabled: bool = True, width: int = 800, height: int = 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, 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._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() + global chart, num_charts + + if chart: + self._q, self._exit, self._start, self._process = chart._q, chart._exit, chart._start, chart._process + self._emit_q, self._return_q = mp.Queue(), mp.Queue() + chart._charts[self.id] = self + self._api = chart._api + self._loaded = chart._loaded_list[num_charts] + self._q.put(('create_window', (self._html, on_top, width, height, x, y))) + else: + self._api = api + self._q, self._emit_q, self._return_q = (mp.Queue() for _ in range(3)) + self._loaded_list = [mp.Event() for _ in range(10)] + self._loaded = self._loaded_list[0] + self._exit, self._start = (mp.Event() for _ in range(2)) + self._process = mp.Process(target=PyWV, args=(self._q, self._start, self._exit, self._loaded_list, self._html, + width, height, x, y, on_top, maximize, debug, + self._emit_q, self._return_q), daemon=True) + self._process.start() + chart = self + + self.i = num_charts + num_charts += 1 + self._script_func = lambda s: self._q.put((self.i, s)) def show(self, block: bool = False): """ @@ -66,11 +96,11 @@ def show(self, block: bool = False): :param block: blocks execution until the chart is closed. """ if not self.loaded: - self._q.put('start') + self._start.set() self._loaded.wait() self._on_js_load() else: - self._q.put('show') + self._q.put((self.i, 'show')) if block: asyncio.run(self.show_async(block=True)) @@ -88,13 +118,15 @@ async def show_async(self, block=False): return elif not self._emit_q.empty(): name, chart_id, arg = self._emit_q.get() - self._api.chart = self._charts[chart_id] - if name == 'save_drawings': - self._api.chart.toolbox._save_drawings(arg) - continue - fixed_callbacks = ('on_search', 'on_horizontal_line_move') - func = self._methods[name] if name not in fixed_callbacks else getattr(self._api, name) - if hasattr(self._api.chart, 'topbar') and (widget := self._api.chart.topbar._widget_with_method(name)): + if self._api: + self._api.chart = self._charts[chart_id] + if self._api and name == 'save_drawings': + func = self._api.chart.toolbox._save_drawings + elif name in ('on_search', 'on_horizontal_line_move'): + func = getattr(self._api, name) + else: + func = self._methods[name] + if self._api and hasattr(self._api.chart, 'topbar') and (widget := self._api.chart.topbar._widget_with_method(name)): widget.value = arg await func() if asyncio.iscoroutinefunction(func) else func() else: @@ -106,18 +138,22 @@ async def show_async(self, block=False): except KeyboardInterrupt: return - def hide(self): """ Hides the chart window.\n """ - self._q.put('hide') + self._q.put((self.i, 'hide')) def exit(self): """ Exits and destroys the chart window.\n """ - self._q.put('exit') - self._exit.wait() + if not self.loaded: + global num_charts, chart + chart = None + num_charts = 0 + else: + self._q.put((self.i, 'exit')) + self._exit.wait() self._process.terminate() del self diff --git a/lightweight_charts/js/callback.js b/lightweight_charts/js/callback.js index adf9958..662305d 100644 --- a/lightweight_charts/js/callback.js +++ b/lightweight_charts/js/callback.js @@ -11,7 +11,6 @@ function makeSearchBox(chart) { searchWindow.style.padding = '5px' searchWindow.style.zIndex = '1000' searchWindow.style.alignItems = 'center' - searchWindow.style.alignItems = 'center' searchWindow.style.backgroundColor = 'rgba(30, 30, 30, 0.9)' searchWindow.style.border = '2px solid #3C434C' searchWindow.style.borderRadius = '5px' @@ -60,7 +59,7 @@ function makeSearchBox(chart) { else return false } else if (event.key === 'Enter') { - chart.callbackFunction(`on_search_~_${chart.id}_~_${sBox.value}`) + window.callbackFunction(`on_search_~_${chart.id}_~_${sBox.value}`) searchWindow.style.display = 'none' sBox.value = '' return true @@ -145,7 +144,7 @@ function makeSwitcher(chart, items, activeItem, callbackName, activeBackgroundCo element.style.color = items[index] === item ? 'activeColor' : inactiveColor }); activeItem = item; - chart.callbackFunction(`${callbackName}_~_${chart.id}_~_${item}`); + window.callbackFunction(`${callbackName}_~_${chart.id}_~_${item}`); } chart.topBar.appendChild(switcherElement) makeSeperator(chart.topBar) diff --git a/lightweight_charts/js/funcs.js b/lightweight_charts/js/funcs.js index e268c84..5b4cbf9 100644 --- a/lightweight_charts/js/funcs.js +++ b/lightweight_charts/js/funcs.js @@ -1,4 +1,4 @@ -function makeChart(callbackFunction, innerWidth, innerHeight, autoSize=true) { +function makeChart(innerWidth, innerHeight, autoSize=true) { let chart = { markers: [], horizontal_lines: [], @@ -9,7 +9,6 @@ function makeChart(callbackFunction, innerWidth, innerHeight, autoSize=true) { width: innerWidth, height: innerHeight, }, - callbackFunction: callbackFunction, candleData: [], commandFunctions: [] } @@ -64,6 +63,7 @@ function makeChart(callbackFunction, innerWidth, innerHeight, autoSize=true) { chart.wrapper.style.height = `${100*innerHeight}%` chart.wrapper.style.display = 'flex' chart.wrapper.style.flexDirection = 'column' + chart.wrapper.style.position = 'relative' chart.div.style.position = 'relative' chart.div.style.display = 'flex' @@ -119,6 +119,12 @@ if (!window.HorizontalLine) { this.line = this.chart.series.createPriceLine(this.priceLine) } + updateLabel(text) { + this.chart.series.removePriceLine(this.line) + this.priceLine.title = text + 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)) diff --git a/lightweight_charts/js/table.js b/lightweight_charts/js/table.js new file mode 100644 index 0000000..a07e951 --- /dev/null +++ b/lightweight_charts/js/table.js @@ -0,0 +1,139 @@ +if (!window.Table) { + class Table { + constructor(width, height, headings, widths, alignments, position, draggable = false, pythonMethod, chart) { + this.container = document.createElement('div') + this.pythonMethod = pythonMethod + this.chart = chart + + if (draggable) { + this.container.style.position = 'absolute' + this.container.style.cursor = 'move' + } else { + this.container.style.position = 'relative' + this.container.style.float = position + } + + this.container.style.zIndex = '2000' + this.container.style.width = width <= 1 ? width * 100 + '%' : width + 'px' + this.container.style.height = height <= 1 ? height * 100 + '%' : height + 'px' + this.container.style.display = 'flex' + this.container.style.flexDirection = 'column' + this.container.style.justifyContent = 'space-between' + + this.container.style.backgroundColor = 'rgb(45, 45, 45)' + this.container.style.borderRadius = '5px' + this.container.style.color = 'white' + this.container.style.fontSize = '12px' + this.container.style.fontVariantNumeric = 'tabular-nums' + + this.table = document.createElement('table') + this.table.style.width = '100%' + this.table.style.borderCollapse = 'collapse' + this.table.style.border = '1px solid rgb(70, 70, 70)'; + this.rows = {} + + this.headings = headings + this.widths = widths.map((width) => `${width * 100}%`) + this.alignments = alignments + + let head = this.table.createTHead() + let row = head.insertRow() + + for (let i = 0; i < this.headings.length; i++) { + let th = document.createElement('th') + th.textContent = this.headings[i] + th.style.width = this.widths[i] + th.style.textAlign = 'center' + row.appendChild(th) + th.style.border = '1px solid rgb(70, 70, 70)' + } + + this.container.appendChild(this.table) + document.getElementById('wrapper').appendChild(this.container) + + if (!draggable) return + + let offsetX, offsetY; + + this.onMouseDown = (event) => { + offsetX = event.clientX - this.container.offsetLeft; + offsetY = event.clientY - this.container.offsetTop; + + document.addEventListener('mousemove', onMouseMove); + document.addEventListener('mouseup', onMouseUp); + } + + let onMouseMove = (event) => { + this.container.style.left = (event.clientX - offsetX) + 'px'; + this.container.style.top = (event.clientY - offsetY) + 'px'; + } + + let onMouseUp = () => { + // Remove the event listeners for dragging + document.removeEventListener('mousemove', onMouseMove); + document.removeEventListener('mouseup', onMouseUp); + } + + this.container.addEventListener('mousedown', this.onMouseDown); + + + } + + newRow(vals, id) { + let row = this.table.insertRow() + row.style.cursor = 'default' + + for (let i = 0; i < vals.length; i++) { + row[this.headings[i]] = row.insertCell() + row[this.headings[i]].textContent = vals[i] + row[this.headings[i]].style.width = this.widths[i]; + row[this.headings[i]].style.textAlign = this.alignments[i]; + row[this.headings[i]].style.border = '1px solid rgb(70, 70, 70)' + + } + row.addEventListener('mouseover', () => row.style.backgroundColor = 'rgba(60, 60, 60, 0.6)') + row.addEventListener('mouseout', () => row.style.backgroundColor = 'transparent') + row.addEventListener('mousedown', () => { + row.style.backgroundColor = 'rgba(60, 60, 60)' + window.callbackFunction(`${this.pythonMethod}_~_${this.chart.id}_~_${id}`) + }) + row.addEventListener('mouseup', () => row.style.backgroundColor = 'rgba(60, 60, 60, 0.6)') + + this.rows[id] = row + } + + deleteRow(id) { + this.table.deleteRow(this.rows[id].rowIndex) + delete this.rows[id] + } + + clearRows() { + let numRows = Object.keys(this.rows).length + for (let i = 0; i < numRows; i++) + this.table.deleteRow(-1) + this.rows = {} + } + + updateCell(rowId, column, val) { + this.rows[rowId][column].textContent = val + } + + makeFooter(numBoxes) { + let footer = document.createElement('div') + footer.style.display = 'flex' + footer.style.width = '100%' + footer.style.padding = '3px 0px' + footer.style.backgroundColor = 'rgb(30, 30, 30)' + this.container.appendChild(footer) + + this.footer = [] + for (let i = 0; i < numBoxes; i++) { + this.footer.push(document.createElement('div')) + footer.appendChild(this.footer[i]) + this.footer[i].style.flex = '1' + this.footer[i].style.textAlign = 'center' + } + } + } + window.Table = Table +} diff --git a/lightweight_charts/js/toolbox.js b/lightweight_charts/js/toolbox.js index b1a8136..2fff304 100644 --- a/lightweight_charts/js/toolbox.js +++ b/lightweight_charts/js/toolbox.js @@ -184,12 +184,12 @@ if (!window.ToolBox) { trendLine.line.setData(data) if (logical) { - this.chart.chart.applyOptions({handleScroll: true}) + this.chart.chart.applyOptions({handleScroll: false}) setTimeout(() => { this.chart.chart.timeScale().setVisibleLogicalRange(logical) }, 1) setTimeout(() => { - this.chart.chart.applyOptions({handleScroll: false}) + this.chart.chart.applyOptions({handleScroll: true}) }, 50) } if (!ray) { @@ -309,6 +309,10 @@ if (!window.ToolBox) { document.body.style.cursor = this.chart.cursor hoveringOver = null contextMenu.listen(false) + if (!mouseDown) { + document.removeEventListener('mousedown', checkForClick) + document.removeEventListener('mouseup', checkForRelease) + } } }) this.chart.chart.subscribeCrosshairMove(hoverOver) @@ -327,8 +331,6 @@ if (!window.ToolBox) { 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) @@ -352,7 +354,7 @@ if (!window.ToolBox) { 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)}`); + window.callbackFunction(`on_horizontal_line_move_~_${this.chart.id}_~_${hoveringOver.id};;;${hoveringOver.price.toFixed(8)}`); } hoveringOver = null document.removeEventListener('mousedown', checkForClick) @@ -472,6 +474,8 @@ if (!window.ToolBox) { let startDate = dateToChartTime(new Date(Math.round(chartTimeToDate(item.from[0]).getTime() / this.interval) * this.interval), this.interval) let endDate = dateToChartTime(new Date(Math.round(chartTimeToDate(item.to[0]).getTime() / this.interval) * this.interval), this.interval) let data = calculateTrendLine(startDate, item.from[1], endDate, item.to[1], this.interval, this.chart, item.ray) + item.from = [data[0].time, data[0].value] + item.to = [data[data.length - 1].time, data[data.length-1].value] item.line.setData(data) }) //this.chart.chart.timeScale().setVisibleLogicalRange(logical) @@ -508,7 +512,7 @@ if (!window.ToolBox) { } return value; }); - this.chart.callbackFunction(`save_drawings_~_${this.chart.id}_~_${drawingsString}`) + window.callbackFunction(`save_drawings_~_${this.chart.id}_~_${drawingsString}`) } loadDrawings(drawings) { @@ -537,6 +541,8 @@ if (!window.ToolBox) { let startDate = dateToChartTime(new Date(Math.round(chartTimeToDate(item.from[0]).getTime() / this.interval) * this.interval), this.interval) let endDate = dateToChartTime(new Date(Math.round(chartTimeToDate(item.to[0]).getTime() / this.interval) * this.interval), this.interval) let data = calculateTrendLine(startDate, item.from[1], endDate, item.to[1], this.interval, this.chart, item.ray) + item.from = [data[0].time, data[0].value] + item.to = [data[data.length - 1].time, data[data.length-1].value] item.line.setData(data) } }) @@ -546,6 +552,5 @@ if (!window.ToolBox) { this.chart.chart.timeScale().setVisibleLogicalRange(logical) } } - window.ToolBox = ToolBox } \ No newline at end of file diff --git a/lightweight_charts/table.py b/lightweight_charts/table.py new file mode 100644 index 0000000..17e1c15 --- /dev/null +++ b/lightweight_charts/table.py @@ -0,0 +1,77 @@ +import random +from typing import Union + +from lightweight_charts.util import _js_bool + + +class Footer: + def __init__(self, table): + self._table = table + self._chart = table._chart + + def __setitem__(self, key, value): self._chart.run_script(f'{self._table.id}.footer[{key}].innerText = "{value}"') + + def __call__(self, number_of_text_boxes): self._chart.run_script(f'{self._table.id}.makeFooter({number_of_text_boxes})') + + +class Row(dict): + def __init__(self, table, id, items): + super().__init__() + self._table = table + self._chart = table._chart + self.id = id + self.meta = {} + self._table._chart.run_script(f'''{self._table.id}.newRow({list(items.values())}, '{self.id}')''') + for key, val in items.items(): + self[key] = val + + def __setitem__(self, column, value): + str_value = str(value) + if column in self._table._formatters: + str_value = self._table._formatters[column].replace(self._table.VALUE, str_value) + self._chart.run_script(f'''{self._table.id}.updateCell('{self.id}', '{column}', '{str_value}')''') + + return super().__setitem__(column, value) + + def background_color(self, column, color): + self._chart.run_script(f"{self._table.id}.rows[{self.id}]['{column}'].style.backgroundColor = '{color}'") + + def delete(self): + self._chart.run_script(f"{self._table.id}.deleteRow('{self.id}')") + self._table.pop(self.id) + +class Table(dict): + VALUE = 'CELL__~__VALUE__~__PLACEHOLDER' + + def __init__(self, chart, width, height, headings, widths=None, alignments=None, position='left', draggable=False, method=None): + super().__init__() + self._chart = chart + self.headings = headings + self._formatters = {} + self.is_shown = True + + self.id = self._chart._rand.generate() + self._chart.run_script(f''' + {self.id} = new Table({width}, {height}, {list(headings)}, {list(widths)}, {list(alignments)}, '{position}', {_js_bool(draggable)}, '{method}', {chart.id}) + ''') + self.footer = Footer(self) + + def new_row(self, *values, id=None) -> Row: + row_id = random.randint(0, 99_999_999) if not id else id + self[row_id] = Row(self, row_id, {heading: item for heading, item in zip(self.headings, values)}) + return self[row_id] + + def clear(self): self._chart.run_script(f"{self.id}.clearRows()"), super().clear() + + def get(self, __key: Union[int, str]) -> Row: return super().get(int(__key)) + + def __getitem__(self, item): return super().__getitem__(int(item)) + + def format(self, column: str, format_str: str): self._formatters[column] = format_str + + def visible(self, visible: bool): + self.is_shown = visible + self._chart.run_script(f""" + {self.id}.container.style.display = '{'block' if visible else 'none'}' + {self.id}.container.{'add' if visible else 'remove'}EventListener('mousedown', {self.id}.onMouseDown) + """) diff --git a/setup.py b/setup.py index fcb1fe9..8e78989 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setup( name='lightweight_charts', - version='1.0.14.4', + version='1.0.15', packages=find_packages(), python_requires='>=3.8', install_requires=[