Skip to content

Commit

Permalink
Fixes/Enhancements
Browse files Browse the repository at this point in the history
- Removed bar data from exported drawings, which should reduce the file size of exported drawings. (Make sure to back up any drawing files before running the new version!)
- Drawings can now be deleted through each’s right click context menu.
- Added the new ‘hotkey’ method, which will execute the given method or function when the key command is pressed.
- Non-fixed callbacks no longer need to be placed in the API class.
  • Loading branch information
louisnw01 committed Jul 24, 2023
1 parent eaec61d commit ca3122b
Show file tree
Hide file tree
Showing 9 changed files with 170 additions and 71 deletions.
2 changes: 1 addition & 1 deletion docs/source/conf.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
project = 'lightweight-charts-python'
copyright = '2023, louisnw'
author = 'louisnw'
release = '1.0.14.2'
release = '1.0.14.4'

extensions = ["myst_parser"]

Expand Down
32 changes: 29 additions & 3 deletions docs/source/docs.md
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,29 @@ if __name__ == '__main__':
```{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)
```



___

Expand Down Expand Up @@ -436,19 +459,19 @@ ___

## Callbacks

The `Chart` object allows for asyncronous 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.
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 callbacks will be emitted to (see [How to use Callbacks](#how-to-use-callbacks)).
* `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

Callbacks are emitted to the class given as the `api` parameter shown above.
Fixed Callbacks are emitted to the class given as the `api` parameter shown above.

Take a look at this minimal example:

Expand All @@ -470,6 +493,7 @@ The ID shown above will change depending upon which pane was used to search, due
```{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.
```
___

Expand Down Expand Up @@ -569,6 +593,8 @@ The following hotkeys can also be used when the Toolbox is enabled:
* 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`
Expand Down
25 changes: 21 additions & 4 deletions lightweight_charts/abstract.py
Original file line number Diff line number Diff line change
Expand Up @@ -332,9 +332,9 @@ class SwitcherWidget(Widget):
def __init__(self, topbar, method, *options, default):
super().__init__(topbar)
self.value = default
self._method = method.__name__
self._method = str(method)
self._chart.run_script(f'''
makeSwitcher({self._chart.id}, {list(options)}, '{default}', '{method.__name__}',
makeSwitcher({self._chart.id}, {list(options)}, '{default}', '{self._method}',
'{topbar.active_background_color}', '{topbar.active_text_color}', '{topbar.text_color}', '{topbar.hover_color}')
reSize({self._chart.id})
''')
Expand All @@ -356,6 +356,7 @@ def __init__(self, chart):
def __getitem__(self, item): return self._widgets.get(item)

def switcher(self, name, method, *options, default=None):
self._chart._methods[str(method)] = method
self._widgets[name] = SwitcherWidget(self, method, *options, default=default if default else options[0])

def textbox(self, name, initial_text=''): self._widgets[name] = TextWidget(self, initial_text)
Expand Down Expand Up @@ -401,7 +402,7 @@ def export_drawings(self, file_path):
Exports the current list of drawings to the given file path.
"""
with open(file_path, 'w+') as f:
json.dump(self._saved_drawings, f)
json.dump(self._saved_drawings, f, indent=4)

def _save_drawings(self, drawings):
if not self._save_under:
Expand Down Expand Up @@ -433,6 +434,7 @@ def __init__(self, volume_enabled: bool = True, inner_width: float = 1.0, inner_
self._charts = {self.id: self}
self._lines = []
self._js_api_code = _js_api_code
self._methods = {}
self._return_q = None

self._background_color = '#000000'
Expand Down Expand Up @@ -818,14 +820,29 @@ 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}}`)
{self._js_api_code}(`return_~_{self.id}_~_${{event.target.result}}`)
}};
reader.readAsDataURL(blob);
}})
''')
serial_data = self._return_q.get()
return b64decode(serial_data.split(',')[1])

def add_hotkey(self, modifier_key: Literal['ctrl', 'alt', 'shift', 'meta'], keys: Union[str, tuple, int], method):
self._methods[str(method)] = method
if not isinstance(keys, tuple): keys = (keys,)
for key in keys:
key_code = 'Key' + key.upper() if isinstance(key, str) else 'Digit' + str(key)
self.run_script(f'''
{self.id}.commandFunctions.unshift((event) => {{
if (event.{modifier_key + 'Key'} && event.code === '{key_code}') {{
event.preventDefault()
{self.id}.callbackFunction(`{str(method)}_~_{self.id}_~_{key}`)
return true
}}
else return false
}})''')

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):
Expand Down
9 changes: 5 additions & 4 deletions lightweight_charts/chart.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ def __init__(self, emit_queue, return_queue):
self.emit_q, self.return_q = emit_queue, return_queue

def callback(self, message: str):
messages = message.split('__')
messages = message.split('_~_')
name, chart_id = messages[:2]
args = messages[2:]
self.return_q.put(*args) if name == 'return' else self.emit_q.put((name, chart_id, *args))
Expand Down Expand Up @@ -92,12 +92,13 @@ async def show_async(self, block=False):
if name == 'save_drawings':
self._api.chart.toolbox._save_drawings(arg)
continue
method = getattr(self._api, name)
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)):
widget.value = arg
await method() if asyncio.iscoroutinefunction(method) else method()
await func() if asyncio.iscoroutinefunction(func) else func()
else:
await method(*arg.split(';;;')) if asyncio.iscoroutinefunction(method) else method(arg)
await func(*arg.split(';;;')) if asyncio.iscoroutinefunction(func) else func(*arg.split(';;;'))
continue
value = self.polygon._q.get()
func, args = value[0], value[1:]
Expand Down
4 changes: 2 additions & 2 deletions lightweight_charts/js/callback.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ function makeSearchBox(chart) {
else return false
}
else if (event.key === 'Enter') {
chart.callbackFunction(`on_search__${chart.id}__${sBox.value}`)
chart.callbackFunction(`on_search_~_${chart.id}_~_${sBox.value}`)
searchWindow.style.display = 'none'
sBox.value = ''
return true
Expand Down Expand Up @@ -145,7 +145,7 @@ function makeSwitcher(chart, items, activeItem, callbackName, activeBackgroundCo
element.style.color = items[index] === item ? 'activeColor' : inactiveColor
});
activeItem = item;
chart.callbackFunction(`${callbackName}__${chart.id}__${item}`);
chart.callbackFunction(`${callbackName}_~_${chart.id}_~_${item}`);
}
chart.topBar.appendChild(switcherElement)
makeSeperator(chart.topBar)
Expand Down
46 changes: 46 additions & 0 deletions lightweight_charts/js/funcs.js
Original file line number Diff line number Diff line change
Expand Up @@ -367,3 +367,49 @@ function calculateTrendLine(startDate, startValue, endDate, endValue, interval,
}
return trendData;
}


if (!window.ContextMenu) {
class ContextMenu {
constructor() {
this.menu = document.createElement('div')
this.menu.style.position = 'absolute'
this.menu.style.zIndex = '10000'
this.menu.style.background = 'rgb(50, 50, 50)'
this.menu.style.color = 'lightgrey'
this.menu.style.display = 'none'
this.menu.style.borderRadius = '5px'
this.menu.style.padding = '3px 3px'
this.menu.style.fontSize = '14px'
this.menu.style.cursor = 'default'
document.body.appendChild(this.menu)

let closeMenu = (event) => {
if (!this.menu.contains(event.target)) this.menu.style.display = 'none';
}

this.onRightClick = (event) => {
event.preventDefault();
this.menu.style.left = event.clientX + 'px';
this.menu.style.top = event.clientY + 'px';
this.menu.style.display = 'block';
document.removeEventListener('click', closeMenu)
document.addEventListener('click', closeMenu)
}
}
listen(active) {
active ? document.addEventListener('contextmenu', this.onRightClick) : document.removeEventListener('contextmenu', this.onRightClick)
}
menuItem(text, action) {
let elem = document.createElement('div')
elem.innerText = text
elem.style.padding = '0px 10px'
elem.style.borderRadius = '3px'
this.menu.appendChild(elem)
elem.addEventListener('mouseover', (event) => elem.style.backgroundColor = 'rgba(0, 122, 255, 0.3)')
elem.addEventListener('mouseout', (event) => elem.style.backgroundColor = 'transparent')
elem.addEventListener('click', (event) => {action(); this.menu.style.display = 'none'})
}
}
window.ContextMenu = ContextMenu
}
Loading

0 comments on commit ca3122b

Please sign in to comment.