diff --git a/docs/source/reference/index.md b/docs/source/reference/index.md index 6da662d..c0c38e3 100644 --- a/docs/source/reference/index.md +++ b/docs/source/reference/index.md @@ -17,6 +17,7 @@ tables 1. [`AbstractChart`](#AbstractChart) 2. [`Line`](#Line) +3. [`Histogram`](#Histogram) 3. [`HorizontalLine`](#HorizontalLine) 4. [Charts](#charts) 5. [`Events`](./events.md) diff --git a/docs/source/reference/topbar.md b/docs/source/reference/topbar.md index fcfbb8a..dc85b6d 100644 --- a/docs/source/reference/topbar.md +++ b/docs/source/reference/topbar.md @@ -12,18 +12,22 @@ Switchers, text boxes and buttons can be added to the top bar, and their instanc ```python chart.topbar.textbox('symbol', 'AAPL') # Declares a textbox displaying 'AAPL'. -print(chart.topbar['symbol'].value) # Prints the value within ('AAPL') +print(chart.topbar['symbol'].value) # Prints the value within 'symbol' -> 'AAPL' chart.topbar['symbol'].set('MSFT') # Sets the 'symbol' textbox to 'MSFT' -print(chart.topbar['symbol'].value) # Prints the value again ('MSFT') +print(chart.topbar['symbol'].value) # Prints the value again -> 'MSFT' ``` + +Topbar widgets share common parameters: +* `name`: The name of the widget which can be used to access it from the `topbar` dictionary. +* `align`: The alignment of the widget (either `'left'` or `'right'` which determines which side of the topbar the widget will be placed upon. + ___ -```{py:method} switcher(name: str, options: tuple: default: str, func: callable) +```{py:method} switcher(name: str, options: tuple: default: str, align: ALIGN, func: callable) -* `name`: the name of the switcher which can be used to access it from the `topbar` dictionary. * `options`: The options for each switcher item. * `default`: The initial switcher option set. @@ -32,9 +36,8 @@ ___ -```{py:method} menu(name: str, options: tuple: default: str, separator: bool, func: callable) +```{py:method} menu(name: str, options: tuple: default: str, separator: bool, align: ALIGN, func: callable) -* `name`: the name of the menu which can be used to access it from the `topbar` dictionary. * `options`: The options for each menu item. * `default`: The initial menu option set. * `separator`: places a separator line to the right of the menu. @@ -44,9 +47,8 @@ ___ -```{py:method} textbox(name: str, initial_text: str) +```{py:method} textbox(name: str, initial_text: str, align: ALIGN) -* `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. ``` @@ -54,9 +56,8 @@ ___ -```{py:method} button(name: str, button_text: str, separator: bool, func: callable) +```{py:method} button(name: str, button_text: str, separator: bool, align: ALIGN, func: callable) -* `name`: the name of the text box to access it from the `topbar` dictionary. * `button_text`: Text to show within the button. * `separator`: places a separator line to the right of the button. * `func`: The event handler which will be executed upon a button click. diff --git a/docs/source/reference/typing.md b/docs/source/reference/typing.md index f0b3653..99f3cc1 100644 --- a/docs/source/reference/typing.md +++ b/docs/source/reference/typing.md @@ -33,6 +33,9 @@ Throughout the library, colors should be given as either rgb (`rgb(100, 100, 100 ```{py:class} PRICE_SCALE_MODE(Literal['normal', 'logarithmic', 'percentage', 'index100']) ``` +```{py:class} ALIGN(Literal['left', 'right']) +``` + diff --git a/lightweight_charts/abstract.py b/lightweight_charts/abstract.py index c3dd8c3..e46537c 100644 --- a/lightweight_charts/abstract.py +++ b/lightweight_charts/abstract.py @@ -1,5 +1,6 @@ import asyncio import os +from base64 import b64decode from datetime import datetime from typing import Union, Literal, List import pandas as pd @@ -871,6 +872,15 @@ def create_table(self, width: NUM, height: NUM, ) -> Table: return self.win.create_table(width, height, headings, widths, alignments, position, draggable, func) + def screenshot(self) -> bytes: + """ + Takes a screenshot. This method can only be used after the chart window is visible. + :return: a bytes object containing a screenshot of the chart. + """ + self.run_script(f'_~_~RETURN~_~_{self.id}.chart.takeScreenshot().toDataURL()') + serial_data = self.win._return_q.get() + return b64decode(serial_data.split(',')[1]) + def create_subchart(self, position: FLOAT = 'left', width: float = 0.5, height: float = 0.5, sync: Union[str, bool] = None, scale_candles_only: bool = False, toolbox: bool = False) -> 'AbstractChart': diff --git a/lightweight_charts/chart.py b/lightweight_charts/chart.py index aa0fecd..25564ed 100644 --- a/lightweight_charts/chart.py +++ b/lightweight_charts/chart.py @@ -1,6 +1,5 @@ import asyncio import multiprocessing as mp -from base64 import b64decode import webview from lightweight_charts import abstract @@ -148,12 +147,3 @@ def exit(self): Chart._window_num = 0 Chart._q = mp.Queue() self.is_alive = False - - def screenshot(self) -> bytes: - """ - Takes a screenshot. This method can only be used after the chart window is visible. - :return: a bytes object containing a screenshot of the chart. - """ - self.run_script(f'_~_~RETURN~_~_{self.id}.chart.takeScreenshot().toDataURL()') - serial_data = self.win._return_q.get() - return b64decode(serial_data.split(',')[1]) diff --git a/lightweight_charts/js/callback.js b/lightweight_charts/js/callback.js index c0e7775..f3bfd10 100644 --- a/lightweight_charts/js/callback.js +++ b/lightweight_charts/js/callback.js @@ -13,12 +13,25 @@ if (!window.TopBar) { this.topBar.style.borderBottom = '2px solid #3C434C' this.topBar.style.display = 'flex' this.topBar.style.alignItems = 'center' + + let createTopBarContainer = (justification) => { + let div = document.createElement('div') + div.style.display = 'flex' + div.style.alignItems = 'center' + div.style.justifyContent = justification + div.style.flexGrow = '1' + this.topBar.appendChild(div) + return div + } + this.left = createTopBarContainer('flex-start') + this.right = createTopBarContainer('flex-end') + chart.wrapper.prepend(this.topBar) chart.topBar = this.topBar this.reSize = () => chart.reSize() this.reSize() } - makeSwitcher(items, activeItem, callbackName) { + makeSwitcher(items, activeItem, callbackName, align='left') { let switcherElement = document.createElement('div'); switcherElement.style.margin = '4px 12px' let widget = { @@ -60,25 +73,20 @@ if (!window.TopBar) { activeItem = item; window.callbackFunction(`${widget.callbackName}_~_${item}`); } - - this.topBar.appendChild(switcherElement) - this.makeSeparator(this.topBar) - this.reSize() + this.appendWidget(switcherElement, align, true) return widget } - makeTextBoxWidget(text) { + makeTextBoxWidget(text, align='left') { let textBox = document.createElement('div') textBox.style.margin = '0px 18px' textBox.style.fontSize = '16px' textBox.style.color = 'rgb(220, 220, 220)' textBox.innerText = text - this.topBar.append(textBox) - this.makeSeparator(this.topBar) - this.reSize() + this.appendWidget(textBox, align, true) return textBox } - makeMenu(items, activeItem, separator, callbackName) { + makeMenu(items, activeItem, separator, callbackName, align='right') { let menu = document.createElement('div') menu.style.position = 'absolute' menu.style.display = 'none' @@ -102,7 +110,9 @@ if (!window.TopBar) { button.elem.style.padding = '2px 2px' menu.appendChild(button.elem) }) - let widget = this.makeButton(activeItem+' ↓', null, separator) + let widget = + this.makeButton(activeItem+' ↓', null, separator, true, align) + widget.elem.addEventListener('click', () => { menuOpen = !menuOpen if (!menuOpen) return menu.style.display = 'none' @@ -117,11 +127,11 @@ if (!window.TopBar) { document.body.appendChild(menu) } - makeButton(defaultText, callbackName, separator, append=true) { + makeButton(defaultText, callbackName, separator, append=true, align='left') { let button = document.createElement('button') button.style.border = 'none' button.style.padding = '2px 5px' - button.style.margin = '4px 18px' + button.style.margin = '4px 10px' button.style.fontSize = '13px' button.style.backgroundColor = 'transparent' button.style.color = this.textColor @@ -151,17 +161,27 @@ if (!window.TopBar) { button.style.color = this.textColor button.style.fontWeight = 'normal' }) - if (separator) this.makeSeparator() - if (append) this.topBar.appendChild(button); this.reSize() + if (append) this.appendWidget(button, align, separator) return widget } - makeSeparator() { + makeSeparator(align='left') { let seperator = document.createElement('div') seperator.style.width = '1px' seperator.style.height = '20px' seperator.style.backgroundColor = '#3C434C' - this.topBar.appendChild(seperator) + let div = align === 'left' ? this.left : this.right + div.appendChild(seperator) + } + + appendWidget(widget, align, separator) { + let div = align === 'left' ? this.left : this.right + if (separator) { + if (align === 'left') div.appendChild(widget) + this.makeSeparator(align) + if (align === 'right') div.appendChild(widget) + } else div.appendChild(widget) + this.reSize() } } window.TopBar = TopBar diff --git a/lightweight_charts/js/table.js b/lightweight_charts/js/table.js index 28ba5cc..b1ce890 100644 --- a/lightweight_charts/js/table.js +++ b/lightweight_charts/js/table.js @@ -85,17 +85,15 @@ if (!window.Table) { } - newRow(vals, id) { + newRow(id) { let row = this.table.insertRow() row.style.cursor = 'default' - for (let i = 0; i < vals.length; i++) { + for (let i = 0; i < this.headings.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') diff --git a/lightweight_charts/js/toolbox.js b/lightweight_charts/js/toolbox.js index ea29529..e4181c5 100644 --- a/lightweight_charts/js/toolbox.js +++ b/lightweight_charts/js/toolbox.js @@ -190,8 +190,8 @@ if (!window.ToolBox) { if (!ray) { trendLine.markers = [ - {time: firstTime, position: 'inBar', color: '#1E80F0', shape: 'circle', size: 0.1}, - {time: currentTime, position: 'inBar', color: '#1E80F0', shape: 'circle', size: 0.1} + {time: trendLine.from[0], position: 'inBar', color: '#1E80F0', shape: 'circle', size: 0.1}, + {time: trendLine.to[0], position: 'inBar', color: '#1E80F0', shape: 'circle', size: 0.1} ] trendLine.line.setMarkers(trendLine.markers) } @@ -411,8 +411,8 @@ if (!window.ToolBox) { if (!hoveringOver.ray) { hoveringOver.markers = [ - {time: startDate, position: 'inBar', color: '#1E80F0', shape: 'circle', size: 0.1}, - {time: endDate, position: 'inBar', color: '#1E80F0', shape: 'circle', size: 0.1} + {time: hoveringOver.from[0], position: 'inBar', color: '#1E80F0', shape: 'circle', size: 0.1}, + {time: hoveringOver.to[0], position: 'inBar', color: '#1E80F0', shape: 'circle', size: 0.1} ] hoveringOver.line.setMarkers(hoveringOver.markers) } @@ -457,8 +457,8 @@ if (!window.ToolBox) { hoveringOver.markers = [ - {time: firstTime, position: 'inBar', color: '#1E80F0', shape: 'circle', size: 0.1}, - {time: currentTime, position: 'inBar', color: '#1E80F0', shape: 'circle', size: 0.1} + {time: hoveringOver.from[0], position: 'inBar', color: '#1E80F0', shape: 'circle', size: 0.1}, + {time: hoveringOver.to[0], position: 'inBar', color: '#1E80F0', shape: 'circle', size: 0.1} ] hoveringOver.line.setMarkers(hoveringOver.markers) diff --git a/lightweight_charts/table.py b/lightweight_charts/table.py index 83886e3..9b04868 100644 --- a/lightweight_charts/table.py +++ b/lightweight_charts/table.py @@ -22,7 +22,7 @@ def __init__(self, table, id, items): self._table = table self.id = id self.meta = {} - self.run_script(f'{self._table.id}.newRow({list(items.values())}, "{self.id}")') + self.run_script(f'{self._table.id}.newRow("{self.id}")') for key, val in items.items(): self[key] = val diff --git a/lightweight_charts/topbar.py b/lightweight_charts/topbar.py index 1eaeb20..d321882 100644 --- a/lightweight_charts/topbar.py +++ b/lightweight_charts/topbar.py @@ -1,9 +1,12 @@ import asyncio -from typing import Dict +from typing import Dict, Literal from .util import jbool, Pane +ALIGN = Literal['left', 'right'] + + class Widget(Pane): def __init__(self, topbar, value, func=None): super().__init__(topbar.win) @@ -21,9 +24,9 @@ async def async_wrapper(v): class TextWidget(Widget): - def __init__(self, topbar, initial_text): + def __init__(self, topbar, initial_text, align): super().__init__(topbar, value=initial_text) - self.run_script(f'{self.id} = {topbar.id}.makeTextBoxWidget("{initial_text}")') + self.run_script(f'{self.id} = {topbar.id}.makeTextBoxWidget("{initial_text}", "{align}")') def set(self, string): self.value = string @@ -31,22 +34,23 @@ def set(self, string): class SwitcherWidget(Widget): - def __init__(self, topbar, options, default, func): + def __init__(self, topbar, options, default, align, func): super().__init__(topbar, value=default, func=func) - self.run_script(f'{self.id} = {topbar.id}.makeSwitcher({list(options)}, "{default}", "{self.id}")') + self.run_script(f'{self.id} = {topbar.id}.makeSwitcher({list(options)}, "{default}", "{self.id}", "{align}")') class MenuWidget(Widget): - def __init__(self, topbar, options, default, separator, func): + def __init__(self, topbar, options, default, separator, align, func): super().__init__(topbar, value=default, func=func) - self.run_script( - f'{self.id} = {topbar.id}.makeMenu({list(options)}, "{default}", {jbool(separator)}, "{self.id}")') + self.run_script(f''' + {self.id} = {topbar.id}.makeMenu({list(options)}, "{default}", {jbool(separator)}, "{self.id}", "{align}") + ''') class ButtonWidget(Widget): - def __init__(self, topbar, button, separator, func): + def __init__(self, topbar, button, separator, align, func): super().__init__(topbar, value=button, func=func) - self.run_script(f'{self.id} = {topbar.id}.makeButton("{button}", "{self.id}", {jbool(separator)})') + self.run_script(f'{self.id} = {topbar.id}.makeButton("{button}", "{self.id}", {jbool(separator)}, "{align}")') def set(self, string): self.value = string @@ -82,20 +86,25 @@ def __getitem__(self, item): return widget raise KeyError(f'Topbar widget "{item}" not found.') - def get(self, widget_name): return self._widgets.get(widget_name) + def get(self, widget_name): + return self._widgets.get(widget_name) - def switcher(self, name, options: tuple, default: str = None, func: callable = None): + def switcher(self, name, options: tuple, default: str = None, + align: ALIGN = 'left', func: callable = None): self._create() - self._widgets[name] = SwitcherWidget(self, options, default if default else options[0], func) + self._widgets[name] = SwitcherWidget(self, options, default if default else options[0], align, func) - def menu(self, name, options: tuple, default: str = None, separator: bool = True, func: callable = None): + def menu(self, name, options: tuple, default: str = None, separator: bool = True, + align: ALIGN = 'left', func: callable = None): self._create() - self._widgets[name] = MenuWidget(self, options, default if default else options[0], separator, func) + self._widgets[name] = MenuWidget(self, options, default if default else options[0], separator, align, func) - def textbox(self, name: str, initial_text: str = ''): + def textbox(self, name: str, initial_text: str = '', + align: ALIGN = 'left'): self._create() - self._widgets[name] = TextWidget(self, initial_text) + self._widgets[name] = TextWidget(self, initial_text, align) - def button(self, name, button_text: str, separator: bool = True, func: callable = None): + def button(self, name, button_text: str, separator: bool = True, + align: ALIGN = 'left', func: callable = None): self._create() - self._widgets[name] = ButtonWidget(self, button_text, separator, func) \ No newline at end of file + self._widgets[name] = ButtonWidget(self, button_text, separator, align, func) diff --git a/lightweight_charts/widgets.py b/lightweight_charts/widgets.py index 29143c9..7d7dd42 100644 --- a/lightweight_charts/widgets.py +++ b/lightweight_charts/widgets.py @@ -16,7 +16,7 @@ try: from PySide6.QtWebEngineWidgets import QWebEngineView from PySide6.QtWebChannel import QWebChannel - from PySide6.QtCore import QObject, Slot + from PySide6.QtCore import Qt, QObject, Slot except ImportError: QWebEngineView = None @@ -24,11 +24,11 @@ class Bridge(QObject): def __init__(self, chart): super().__init__() - self.chart = chart + self.win = chart.win @Slot(str) def callback(self, message): - emit_callback(self.chart, message) + emit_callback(self.win, message) try: from streamlit.components.v1 import html @@ -78,6 +78,7 @@ def __init__(self, widget=None, inner_width: float = 1.0, inner_height: float = self.web_channel.registerObject('bridge', self.bridge) self.webview.page().setWebChannel(self.web_channel) self.webview.loadFinished.connect(self.win.on_js_load) + self.webview.setContextMenuPolicy(Qt.ContextMenuPolicy.NoContextMenu) self._html = f''' {abstract.TEMPLATE[:85]} diff --git a/setup.py b/setup.py index 14a7c8b..c2ff4ef 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setup( name='lightweight_charts', - version='1.0.17.3', + version='1.0.17.5', packages=find_packages(), python_requires='>=3.8', install_requires=[