Fixes/Enhancements
- 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.
This commit is contained in:
@ -1,7 +1,7 @@
|
|||||||
project = 'lightweight-charts-python'
|
project = 'lightweight-charts-python'
|
||||||
copyright = '2023, louisnw'
|
copyright = '2023, louisnw'
|
||||||
author = 'louisnw'
|
author = 'louisnw'
|
||||||
release = '1.0.14.2'
|
release = '1.0.14.4'
|
||||||
|
|
||||||
extensions = ["myst_parser"]
|
extensions = ["myst_parser"]
|
||||||
|
|
||||||
|
|||||||
@ -307,6 +307,29 @@ if __name__ == '__main__':
|
|||||||
```{important}
|
```{important}
|
||||||
This method should be called after the chart window has loaded.
|
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)
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
___
|
___
|
||||||
|
|
||||||
@ -436,19 +459,19 @@ ___
|
|||||||
|
|
||||||
## Callbacks
|
## 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.
|
[`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.
|
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.
|
* `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.
|
* `searchbox`: Adds a search box onto the `Chart` or `SubChart` that is activated by typing.
|
||||||
|
|
||||||
___
|
___
|
||||||
### How to use Callbacks
|
### 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:
|
Take a look at this minimal example:
|
||||||
|
|
||||||
@ -470,6 +493,7 @@ The ID shown above will change depending upon which pane was used to search, due
|
|||||||
```{important}
|
```{important}
|
||||||
* Search callbacks will always be emitted to a method named `on_search`
|
* Search callbacks will always be emitted to a method named `on_search`
|
||||||
* `API` class methods can be either coroutines or normal methods.
|
* `API` class methods can be either coroutines or normal methods.
|
||||||
|
* Non fixed callbacks (switchers, hotkeys) can be methods, coroutines, or regular functions.
|
||||||
```
|
```
|
||||||
___
|
___
|
||||||
|
|
||||||
@ -569,6 +593,8 @@ The following hotkeys can also be used when the Toolbox is enabled:
|
|||||||
* Alt+H: Horizontal Line
|
* Alt+H: Horizontal Line
|
||||||
* Alt+R: Ray Line
|
* Alt+R: Ray Line
|
||||||
* Meta+Z or Ctrl+Z: Undo
|
* 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`
|
### `save_drawings_under`
|
||||||
|
|||||||
@ -332,9 +332,9 @@ class SwitcherWidget(Widget):
|
|||||||
def __init__(self, topbar, method, *options, default):
|
def __init__(self, topbar, method, *options, default):
|
||||||
super().__init__(topbar)
|
super().__init__(topbar)
|
||||||
self.value = default
|
self.value = default
|
||||||
self._method = method.__name__
|
self._method = str(method)
|
||||||
self._chart.run_script(f'''
|
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}')
|
'{topbar.active_background_color}', '{topbar.active_text_color}', '{topbar.text_color}', '{topbar.hover_color}')
|
||||||
reSize({self._chart.id})
|
reSize({self._chart.id})
|
||||||
''')
|
''')
|
||||||
@ -356,6 +356,7 @@ class TopBar:
|
|||||||
def __getitem__(self, item): return self._widgets.get(item)
|
def __getitem__(self, item): return self._widgets.get(item)
|
||||||
|
|
||||||
def switcher(self, name, method, *options, default=None):
|
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])
|
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)
|
def textbox(self, name, initial_text=''): self._widgets[name] = TextWidget(self, initial_text)
|
||||||
@ -401,7 +402,7 @@ class ToolBox:
|
|||||||
Exports the current list of drawings to the given file path.
|
Exports the current list of drawings to the given file path.
|
||||||
"""
|
"""
|
||||||
with open(file_path, 'w+') as f:
|
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):
|
def _save_drawings(self, drawings):
|
||||||
if not self._save_under:
|
if not self._save_under:
|
||||||
@ -433,6 +434,7 @@ class LWC(SeriesCommon):
|
|||||||
self._charts = {self.id: self}
|
self._charts = {self.id: self}
|
||||||
self._lines = []
|
self._lines = []
|
||||||
self._js_api_code = _js_api_code
|
self._js_api_code = _js_api_code
|
||||||
|
self._methods = {}
|
||||||
self._return_q = None
|
self._return_q = None
|
||||||
|
|
||||||
self._background_color = '#000000'
|
self._background_color = '#000000'
|
||||||
@ -818,7 +820,7 @@ class LWC(SeriesCommon):
|
|||||||
canvas.toBlob(function(blob) {{
|
canvas.toBlob(function(blob) {{
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onload = function(event) {{
|
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);
|
reader.readAsDataURL(blob);
|
||||||
}})
|
}})
|
||||||
@ -826,6 +828,21 @@ class LWC(SeriesCommon):
|
|||||||
serial_data = self._return_q.get()
|
serial_data = self._return_q.get()
|
||||||
return b64decode(serial_data.split(',')[1])
|
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',
|
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,
|
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):
|
scale_candles_only: bool = False, topbar: bool = False, searchbox: bool = False, toolbox: bool = False):
|
||||||
|
|||||||
@ -10,7 +10,7 @@ class CallbackAPI:
|
|||||||
self.emit_q, self.return_q = emit_queue, return_queue
|
self.emit_q, self.return_q = emit_queue, return_queue
|
||||||
|
|
||||||
def callback(self, message: str):
|
def callback(self, message: str):
|
||||||
messages = message.split('__')
|
messages = message.split('_~_')
|
||||||
name, chart_id = messages[:2]
|
name, chart_id = messages[:2]
|
||||||
args = messages[2:]
|
args = messages[2:]
|
||||||
self.return_q.put(*args) if name == 'return' else self.emit_q.put((name, chart_id, *args))
|
self.return_q.put(*args) if name == 'return' else self.emit_q.put((name, chart_id, *args))
|
||||||
@ -92,12 +92,13 @@ class Chart(LWC):
|
|||||||
if name == 'save_drawings':
|
if name == 'save_drawings':
|
||||||
self._api.chart.toolbox._save_drawings(arg)
|
self._api.chart.toolbox._save_drawings(arg)
|
||||||
continue
|
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)):
|
if hasattr(self._api.chart, 'topbar') and (widget := self._api.chart.topbar._widget_with_method(name)):
|
||||||
widget.value = arg
|
widget.value = arg
|
||||||
await method() if asyncio.iscoroutinefunction(method) else method()
|
await func() if asyncio.iscoroutinefunction(func) else func()
|
||||||
else:
|
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
|
continue
|
||||||
value = self.polygon._q.get()
|
value = self.polygon._q.get()
|
||||||
func, args = value[0], value[1:]
|
func, args = value[0], value[1:]
|
||||||
|
|||||||
@ -60,7 +60,7 @@ function makeSearchBox(chart) {
|
|||||||
else return false
|
else return false
|
||||||
}
|
}
|
||||||
else if (event.key === 'Enter') {
|
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'
|
searchWindow.style.display = 'none'
|
||||||
sBox.value = ''
|
sBox.value = ''
|
||||||
return true
|
return true
|
||||||
@ -145,7 +145,7 @@ function makeSwitcher(chart, items, activeItem, callbackName, activeBackgroundCo
|
|||||||
element.style.color = items[index] === item ? 'activeColor' : inactiveColor
|
element.style.color = items[index] === item ? 'activeColor' : inactiveColor
|
||||||
});
|
});
|
||||||
activeItem = item;
|
activeItem = item;
|
||||||
chart.callbackFunction(`${callbackName}__${chart.id}__${item}`);
|
chart.callbackFunction(`${callbackName}_~_${chart.id}_~_${item}`);
|
||||||
}
|
}
|
||||||
chart.topBar.appendChild(switcherElement)
|
chart.topBar.appendChild(switcherElement)
|
||||||
makeSeperator(chart.topBar)
|
makeSeperator(chart.topBar)
|
||||||
|
|||||||
@ -367,3 +367,49 @@ function calculateTrendLine(startDate, startValue, endDate, endValue, interval,
|
|||||||
}
|
}
|
||||||
return trendData;
|
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
|
||||||
|
}
|
||||||
|
|||||||
@ -57,17 +57,8 @@ if (!window.ToolBox) {
|
|||||||
|
|
||||||
let commandZHandler = (toDelete) => {
|
let commandZHandler = (toDelete) => {
|
||||||
if (!toDelete) return
|
if (!toDelete) return
|
||||||
if ('price' in toDelete) {
|
if ('price' in toDelete && toDelete.id !== 'toolBox') return commandZHandler(this.drawings[this.drawings.indexOf(toDelete) - 1])
|
||||||
if (toDelete.id !== 'toolBox') return commandZHandler(this.drawings[this.drawings.indexOf(toDelete) - 1])
|
this.deleteDrawing(toDelete)
|
||||||
this.chart.series.removePriceLine(toDelete.line)
|
|
||||||
} else {
|
|
||||||
let logical
|
|
||||||
if (toDelete.ray) logical = this.chart.chart.timeScale().getVisibleLogicalRange()
|
|
||||||
this.chart.chart.removeSeries(toDelete.line);
|
|
||||||
if (toDelete.ray) this.chart.chart.timeScale().setVisibleLogicalRange(logical)
|
|
||||||
}
|
|
||||||
this.drawings.splice(this.drawings.indexOf(toDelete))
|
|
||||||
this.saveDrawings()
|
|
||||||
}
|
}
|
||||||
this.chart.commandFunctions.push((event) => {
|
this.chart.commandFunctions.push((event) => {
|
||||||
if ((event.metaKey || event.ctrlKey) && event.code === 'KeyZ') {
|
if ((event.metaKey || event.ctrlKey) && event.code === 'KeyZ') {
|
||||||
@ -184,15 +175,13 @@ if (!window.ToolBox) {
|
|||||||
|
|
||||||
|
|
||||||
if (!currentTime) return this.chart.chart.subscribeCrosshairMove(crosshairHandlerTrend)
|
if (!currentTime) return this.chart.chart.subscribeCrosshairMove(crosshairHandlerTrend)
|
||||||
trendLine.data = calculateTrendLine(firstTime, firstPrice, currentTime, currentPrice, this.interval, this.chart, ray)
|
let data = calculateTrendLine(firstTime, firstPrice, currentTime, currentPrice, this.interval, this.chart, ray)
|
||||||
trendLine.from = trendLine.data[0].time
|
trendLine.from = [data[0].time, data[0].value]
|
||||||
trendLine.to = trendLine.data[trendLine.data.length - 1].time
|
trendLine.to = [data[data.length - 1].time, data[data.length-1].value]
|
||||||
|
|
||||||
|
|
||||||
if (ray) logical = this.chart.chart.timeScale().getVisibleLogicalRange()
|
if (ray) logical = this.chart.chart.timeScale().getVisibleLogicalRange()
|
||||||
|
|
||||||
|
trendLine.line.setData(data)
|
||||||
trendLine.line.setData(trendLine.data)
|
|
||||||
|
|
||||||
if (logical) {
|
if (logical) {
|
||||||
this.chart.chart.applyOptions({handleScroll: true})
|
this.chart.chart.applyOptions({handleScroll: true})
|
||||||
@ -278,6 +267,11 @@ if (!window.ToolBox) {
|
|||||||
subscribeHoverMove() {
|
subscribeHoverMove() {
|
||||||
let hoveringOver = null
|
let hoveringOver = null
|
||||||
let x, y
|
let x, y
|
||||||
|
|
||||||
|
let onClickDelete = () => this.deleteDrawing(contextMenu.drawing)
|
||||||
|
let contextMenu = new ContextMenu()
|
||||||
|
contextMenu.menuItem('Delete Drawing', onClickDelete)
|
||||||
|
|
||||||
let hoverOver = (param) => {
|
let hoverOver = (param) => {
|
||||||
if (!param.point || this.makingDrawing) return
|
if (!param.point || this.makingDrawing) return
|
||||||
this.chart.chart.unsubscribeCrosshairMove(hoverOver)
|
this.chart.chart.unsubscribeCrosshairMove(hoverOver)
|
||||||
@ -308,10 +302,13 @@ if (!window.ToolBox) {
|
|||||||
document.addEventListener('mousedown', checkForClick)
|
document.addEventListener('mousedown', checkForClick)
|
||||||
document.addEventListener('mouseup', checkForRelease)
|
document.addEventListener('mouseup', checkForRelease)
|
||||||
hoveringOver = drawing
|
hoveringOver = drawing
|
||||||
|
contextMenu.listen(true)
|
||||||
|
contextMenu.drawing = drawing
|
||||||
} else if (hoveringOver === drawing) {
|
} else if (hoveringOver === drawing) {
|
||||||
if (!horizontal && !drawing.ray) drawing.line.setMarkers([])
|
if (!horizontal && !drawing.ray) drawing.line.setMarkers([])
|
||||||
document.body.style.cursor = this.chart.cursor
|
document.body.style.cursor = this.chart.cursor
|
||||||
hoveringOver = null
|
hoveringOver = null
|
||||||
|
contextMenu.listen(false)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
this.chart.chart.subscribeCrosshairMove(hoverOver)
|
this.chart.chart.subscribeCrosshairMove(hoverOver)
|
||||||
@ -335,10 +332,10 @@ if (!window.ToolBox) {
|
|||||||
if ('price' in hoveringOver) {
|
if ('price' in hoveringOver) {
|
||||||
originalPrice = hoveringOver.price
|
originalPrice = hoveringOver.price
|
||||||
this.chart.chart.subscribeCrosshairMove(crosshairHandlerHorz)
|
this.chart.chart.subscribeCrosshairMove(crosshairHandlerHorz)
|
||||||
} else if (Math.abs(this.chart.chart.timeScale().timeToCoordinate(hoveringOver.data[0].time) - x) < 4 && !hoveringOver.ray) {
|
} else if (Math.abs(this.chart.chart.timeScale().timeToCoordinate(hoveringOver.from[0]) - x) < 4 && !hoveringOver.ray) {
|
||||||
clickedEnd = 'first'
|
clickedEnd = 'first'
|
||||||
this.chart.chart.subscribeCrosshairMove(crosshairHandlerTrend)
|
this.chart.chart.subscribeCrosshairMove(crosshairHandlerTrend)
|
||||||
} else if (Math.abs(this.chart.chart.timeScale().timeToCoordinate(hoveringOver.data[hoveringOver.data.length - 1].time) - x) < 4 && !hoveringOver.ray) {
|
} else if (Math.abs(this.chart.chart.timeScale().timeToCoordinate(hoveringOver.to[0]) - x) < 4 && !hoveringOver.ray) {
|
||||||
clickedEnd = 'last'
|
clickedEnd = 'last'
|
||||||
this.chart.chart.subscribeCrosshairMove(crosshairHandlerTrend)
|
this.chart.chart.subscribeCrosshairMove(crosshairHandlerTrend)
|
||||||
} else {
|
} else {
|
||||||
@ -355,7 +352,7 @@ if (!window.ToolBox) {
|
|||||||
|
|
||||||
this.chart.chart.applyOptions({handleScroll: true})
|
this.chart.chart.applyOptions({handleScroll: true})
|
||||||
if (hoveringOver && 'price' in hoveringOver && hoveringOver.id !== 'toolBox') {
|
if (hoveringOver && 'price' in hoveringOver && hoveringOver.id !== 'toolBox') {
|
||||||
this.chart.callbackFunction(`on_horizontal_line_move__${this.chart.id}__${hoveringOver.id};;;${hoveringOver.price.toFixed(8)}`);
|
this.chart.callbackFunction(`on_horizontal_line_move_~_${this.chart.id}_~_${hoveringOver.id};;;${hoveringOver.price.toFixed(8)}`);
|
||||||
}
|
}
|
||||||
hoveringOver = null
|
hoveringOver = null
|
||||||
document.removeEventListener('mousedown', checkForClick)
|
document.removeEventListener('mousedown', checkForClick)
|
||||||
@ -373,32 +370,31 @@ if (!window.ToolBox) {
|
|||||||
let priceDiff = priceAtCursor - originalPrice
|
let priceDiff = priceAtCursor - originalPrice
|
||||||
let barsToMove = param.logical - originalIndex
|
let barsToMove = param.logical - originalIndex
|
||||||
|
|
||||||
let startBarIndex = this.chart.candleData.findIndex(item => chartTimeToDate(item.time).getTime() === chartTimeToDate(hoveringOver.data[0].time).getTime())
|
let startBarIndex = this.chart.candleData.findIndex(item => chartTimeToDate(item.time).getTime() === chartTimeToDate(hoveringOver.from[0]).getTime())
|
||||||
let endBarIndex = this.chart.candleData.findIndex(item => chartTimeToDate(item.time).getTime() === chartTimeToDate(hoveringOver.data[hoveringOver.data.length - 1].time).getTime())
|
let endBarIndex = this.chart.candleData.findIndex(item => chartTimeToDate(item.time).getTime() === chartTimeToDate(hoveringOver.to[0]).getTime())
|
||||||
|
|
||||||
let startBar
|
let startDate
|
||||||
let endBar
|
let endBar
|
||||||
if (hoveringOver.ray) {
|
if (hoveringOver.ray) {
|
||||||
endBar = this.chart.candleData[startBarIndex + barsToMove]
|
endBar = this.chart.candleData[startBarIndex + barsToMove]
|
||||||
startBar = hoveringOver.data[hoveringOver.data.length - 1]
|
startDate = hoveringOver.to[0]
|
||||||
} else {
|
} else {
|
||||||
startBar = this.chart.candleData[startBarIndex + barsToMove]
|
startDate = this.chart.candleData[startBarIndex + barsToMove].time
|
||||||
endBar = endBarIndex === -1 ? null : this.chart.candleData[endBarIndex + barsToMove]
|
endBar = endBarIndex === -1 ? null : this.chart.candleData[endBarIndex + barsToMove]
|
||||||
}
|
}
|
||||||
|
|
||||||
let endDate = endBar ? endBar.time : dateToChartTime(new Date(chartTimeToDate(hoveringOver.data[hoveringOver.data.length - 1].time).getTime() + (barsToMove * this.interval)), this.interval)
|
let endDate = endBar ? endBar.time : dateToChartTime(new Date(chartTimeToDate(hoveringOver.to[0]).getTime() + (barsToMove * this.interval)), this.interval)
|
||||||
let startDate = startBar.time
|
let startValue = hoveringOver.from[1] + priceDiff
|
||||||
let startValue = hoveringOver.data[0].value + priceDiff
|
let endValue = hoveringOver.to[1] + priceDiff
|
||||||
let endValue = hoveringOver.data[hoveringOver.data.length - 1].value + priceDiff
|
let data = calculateTrendLine(startDate, startValue, endDate, endValue, this.interval, this.chart, hoveringOver.ray)
|
||||||
hoveringOver.data = calculateTrendLine(startDate, startValue, endDate, endValue, this.interval, this.chart, hoveringOver.ray)
|
|
||||||
|
|
||||||
let logical
|
let logical
|
||||||
if (chartTimeToDate(hoveringOver.data[hoveringOver.data.length - 1].time).getTime() >= chartTimeToDate(this.chart.candleData[this.chart.candleData.length - 1].time).getTime()) {
|
if (chartTimeToDate(data[data.length - 1].time).getTime() >= chartTimeToDate(this.chart.candleData[this.chart.candleData.length - 1].time).getTime()) {
|
||||||
logical = this.chart.chart.timeScale().getVisibleLogicalRange()
|
logical = this.chart.chart.timeScale().getVisibleLogicalRange()
|
||||||
}
|
}
|
||||||
hoveringOver.from = hoveringOver.data[0].time
|
hoveringOver.from = [data[0].time, data[0].value]
|
||||||
hoveringOver.to = hoveringOver.data[hoveringOver.data.length - 1].time
|
hoveringOver.to = [data[data.length - 1].time, data[data.length - 1].value]
|
||||||
hoveringOver.line.setData(hoveringOver.data)
|
hoveringOver.line.setData(data)
|
||||||
if (logical) this.chart.chart.timeScale().setVisibleLogicalRange(logical)
|
if (logical) this.chart.chart.timeScale().setVisibleLogicalRange(logical)
|
||||||
|
|
||||||
if (!hoveringOver.ray) {
|
if (!hoveringOver.ray) {
|
||||||
@ -423,11 +419,11 @@ if (!window.ToolBox) {
|
|||||||
|
|
||||||
let [firstTime, firstPrice] = [null, null]
|
let [firstTime, firstPrice] = [null, null]
|
||||||
if (clickedEnd === 'last') {
|
if (clickedEnd === 'last') {
|
||||||
firstTime = hoveringOver.data[0].time
|
firstTime = hoveringOver.from[0]
|
||||||
firstPrice = hoveringOver.data[0].value
|
firstPrice = hoveringOver.from[1]
|
||||||
} else if (clickedEnd === 'first') {
|
} else if (clickedEnd === 'first') {
|
||||||
firstTime = hoveringOver.data[hoveringOver.data.length - 1].time
|
firstTime = hoveringOver.to[0]
|
||||||
firstPrice = hoveringOver.data[hoveringOver.data.length - 1].value
|
firstPrice = hoveringOver.to[1]
|
||||||
}
|
}
|
||||||
|
|
||||||
let logical
|
let logical
|
||||||
@ -440,11 +436,11 @@ if (!window.ToolBox) {
|
|||||||
logical = this.chart.chart.timeScale().getVisibleLogicalRange()
|
logical = this.chart.chart.timeScale().getVisibleLogicalRange()
|
||||||
}
|
}
|
||||||
|
|
||||||
hoveringOver.data = calculateTrendLine(firstTime, firstPrice, currentTime, currentPrice, this.interval, this.chart)
|
let data = calculateTrendLine(firstTime, firstPrice, currentTime, currentPrice, this.interval, this.chart)
|
||||||
hoveringOver.line.setData(hoveringOver.data)
|
hoveringOver.line.setData(data)
|
||||||
|
|
||||||
hoveringOver.from = hoveringOver.data[0].time
|
hoveringOver.from = [data[0].time, data[0].value]
|
||||||
hoveringOver.to = hoveringOver.data[hoveringOver.data.length - 1].time
|
hoveringOver.to = [data[data.length - 1].time, data[data.length - 1].value]
|
||||||
if (logical) this.chart.chart.timeScale().setVisibleLogicalRange(logical)
|
if (logical) this.chart.chart.timeScale().setVisibleLogicalRange(logical)
|
||||||
|
|
||||||
hoveringOver.markers = [
|
hoveringOver.markers = [
|
||||||
@ -473,15 +469,28 @@ if (!window.ToolBox) {
|
|||||||
//let logical = this.chart.chart.timeScale().getVisibleLogicalRange()
|
//let logical = this.chart.chart.timeScale().getVisibleLogicalRange()
|
||||||
this.drawings.forEach((item) => {
|
this.drawings.forEach((item) => {
|
||||||
if ('price' in item) return
|
if ('price' in item) return
|
||||||
let startDate = dateToChartTime(new Date(Math.round(chartTimeToDate(item.from).getTime() / this.interval) * this.interval), this.interval)
|
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).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.data[0].value, endDate, item.data[item.data.length - 1].value, this.interval, this.chart, item.ray)
|
let data = calculateTrendLine(startDate, item.from[1], endDate, item.to[1], this.interval, this.chart, item.ray)
|
||||||
if (data.length !== 0) item.data = data
|
|
||||||
item.line.setData(data)
|
item.line.setData(data)
|
||||||
})
|
})
|
||||||
//this.chart.chart.timeScale().setVisibleLogicalRange(logical)
|
//this.chart.chart.timeScale().setVisibleLogicalRange(logical)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
deleteDrawing(drawing) {
|
||||||
|
if ('price' in drawing) {
|
||||||
|
this.chart.series.removePriceLine(drawing.line)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
let logical
|
||||||
|
if (drawing.ray) logical = this.chart.chart.timeScale().getVisibleLogicalRange()
|
||||||
|
this.chart.chart.removeSeries(drawing.line);
|
||||||
|
if (drawing.ray) this.chart.chart.timeScale().setVisibleLogicalRange(logical)
|
||||||
|
}
|
||||||
|
this.drawings.splice(this.drawings.indexOf(drawing), 1)
|
||||||
|
this.saveDrawings()
|
||||||
|
}
|
||||||
|
|
||||||
clearDrawings() {
|
clearDrawings() {
|
||||||
this.drawings.forEach((item) => {
|
this.drawings.forEach((item) => {
|
||||||
if ('price' in item) this.chart.series.removePriceLine(item.line)
|
if ('price' in item) this.chart.series.removePriceLine(item.line)
|
||||||
@ -499,7 +508,7 @@ if (!window.ToolBox) {
|
|||||||
}
|
}
|
||||||
return value;
|
return value;
|
||||||
});
|
});
|
||||||
this.chart.callbackFunction(`save_drawings__${this.chart.id}__${drawingsString}`)
|
this.chart.callbackFunction(`save_drawings_~_${this.chart.id}_~_${drawingsString}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
loadDrawings(drawings) {
|
loadDrawings(drawings) {
|
||||||
@ -525,10 +534,9 @@ if (!window.ToolBox) {
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
let startDate = dateToChartTime(new Date(Math.round(chartTimeToDate(item.from).getTime() / this.interval) * this.interval), this.interval)
|
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).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.data[0].value, endDate, item.data[item.data.length - 1].value, this.interval, this.chart, item.ray)
|
let data = calculateTrendLine(startDate, item.from[1], endDate, item.to[1], this.interval, this.chart, item.ray)
|
||||||
if (data.length !== 0) item.data = data
|
|
||||||
item.line.setData(data)
|
item.line.setData(data)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@ -33,16 +33,17 @@ from lightweight_charts.abstract import LWC, JS
|
|||||||
|
|
||||||
|
|
||||||
def _widget_message(chart, string):
|
def _widget_message(chart, string):
|
||||||
messages = string.split('__')
|
messages = string.split('_~_')
|
||||||
name, chart_id = messages[:2]
|
name, chart_id = messages[:2]
|
||||||
arg = messages[2]
|
arg = messages[2]
|
||||||
chart.api.chart = chart._charts[chart_id]
|
chart.api.chart = chart._charts[chart_id]
|
||||||
method = getattr(chart.api, name)
|
fixed_callbacks = ('on_search', 'on_horizontal_line_move')
|
||||||
if widget := chart.api.chart.topbar._widget_with_method(name):
|
func = chart._methods[name] if name not in fixed_callbacks else getattr(chart._api, name)
|
||||||
|
if hasattr(chart._api.chart, 'topbar') and (widget := chart._api.chart.topbar._widget_with_method(name)):
|
||||||
widget.value = arg
|
widget.value = arg
|
||||||
asyncio.create_task(getattr(chart.api, name)()) if iscoroutinefunction(method) else method()
|
asyncio.create_task(func()) if asyncio.iscoroutinefunction(func) else func()
|
||||||
else:
|
else:
|
||||||
asyncio.create_task(getattr(chart.api, name)(arg)) if iscoroutinefunction(method) else method(arg)
|
asyncio.create_task(func(*arg.split(';;;'))) if asyncio.iscoroutinefunction(func) else func(*arg.split(';;;'))
|
||||||
|
|
||||||
|
|
||||||
class WxChart(LWC):
|
class WxChart(LWC):
|
||||||
|
|||||||
2
setup.py
2
setup.py
@ -5,7 +5,7 @@ with open('README.md', 'r', encoding='utf-8') as f:
|
|||||||
|
|
||||||
setup(
|
setup(
|
||||||
name='lightweight_charts',
|
name='lightweight_charts',
|
||||||
version='1.0.14.2',
|
version='1.0.14.4',
|
||||||
packages=find_packages(),
|
packages=find_packages(),
|
||||||
python_requires='>=3.8',
|
python_requires='>=3.8',
|
||||||
install_requires=[
|
install_requires=[
|
||||||
|
|||||||
Reference in New Issue
Block a user