Merge branch 'louisnw01:main' into main

This commit is contained in:
James Baber
2023-09-14 09:25:22 -05:00
committed by GitHub
12 changed files with 104 additions and 71 deletions

View File

@ -17,6 +17,7 @@ tables
1. [`AbstractChart`](#AbstractChart) 1. [`AbstractChart`](#AbstractChart)
2. [`Line`](#Line) 2. [`Line`](#Line)
3. [`Histogram`](#Histogram)
3. [`HorizontalLine`](#HorizontalLine) 3. [`HorizontalLine`](#HorizontalLine)
4. [Charts](#charts) 4. [Charts](#charts)
5. [`Events`](./events.md) 5. [`Events`](./events.md)

View File

@ -12,18 +12,22 @@ Switchers, text boxes and buttons can be added to the top bar, and their instanc
```python ```python
chart.topbar.textbox('symbol', 'AAPL') # Declares a textbox displaying 'AAPL'. 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' 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. * `options`: The options for each switcher item.
* `default`: The initial switcher option set. * `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. * `options`: The options for each menu item.
* `default`: The initial menu option set. * `default`: The initial menu option set.
* `separator`: places a separator line to the right of the menu. * `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. * `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. * `button_text`: Text to show within the button.
* `separator`: places a separator line to the right of 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. * `func`: The event handler which will be executed upon a button click.

View File

@ -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} PRICE_SCALE_MODE(Literal['normal', 'logarithmic', 'percentage', 'index100'])
``` ```
```{py:class} ALIGN(Literal['left', 'right'])
```

View File

@ -1,5 +1,6 @@
import asyncio import asyncio
import os import os
from base64 import b64decode
from datetime import datetime from datetime import datetime
from typing import Union, Literal, List, Optional from typing import Union, Literal, List, Optional
import pandas as pd import pandas as pd
@ -887,6 +888,15 @@ class AbstractChart(Candlestick, Pane):
) -> Table: ) -> Table:
return self.win.create_table(width, height, headings, widths, alignments, position, draggable, func) 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, 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, sync: Union[str, bool] = None, scale_candles_only: bool = False,
toolbox: bool = False) -> 'AbstractChart': toolbox: bool = False) -> 'AbstractChart':

View File

@ -1,6 +1,5 @@
import asyncio import asyncio
import multiprocessing as mp import multiprocessing as mp
from base64 import b64decode
import webview import webview
from lightweight_charts import abstract from lightweight_charts import abstract
@ -148,12 +147,3 @@ class Chart(abstract.AbstractChart):
Chart._window_num = 0 Chart._window_num = 0
Chart._q = mp.Queue() Chart._q = mp.Queue()
self.is_alive = False 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])

View File

@ -13,12 +13,25 @@ if (!window.TopBar) {
this.topBar.style.borderBottom = '2px solid #3C434C' this.topBar.style.borderBottom = '2px solid #3C434C'
this.topBar.style.display = 'flex' this.topBar.style.display = 'flex'
this.topBar.style.alignItems = 'center' 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.wrapper.prepend(this.topBar)
chart.topBar = this.topBar chart.topBar = this.topBar
this.reSize = () => chart.reSize() this.reSize = () => chart.reSize()
this.reSize() this.reSize()
} }
makeSwitcher(items, activeItem, callbackName) { makeSwitcher(items, activeItem, callbackName, align='left') {
let switcherElement = document.createElement('div'); let switcherElement = document.createElement('div');
switcherElement.style.margin = '4px 12px' switcherElement.style.margin = '4px 12px'
let widget = { let widget = {
@ -60,25 +73,20 @@ if (!window.TopBar) {
activeItem = item; activeItem = item;
window.callbackFunction(`${widget.callbackName}_~_${item}`); window.callbackFunction(`${widget.callbackName}_~_${item}`);
} }
this.appendWidget(switcherElement, align, true)
this.topBar.appendChild(switcherElement)
this.makeSeparator(this.topBar)
this.reSize()
return widget return widget
} }
makeTextBoxWidget(text) { makeTextBoxWidget(text, align='left') {
let textBox = document.createElement('div') let textBox = document.createElement('div')
textBox.style.margin = '0px 18px' textBox.style.margin = '0px 18px'
textBox.style.fontSize = '16px' textBox.style.fontSize = '16px'
textBox.style.color = 'rgb(220, 220, 220)' textBox.style.color = 'rgb(220, 220, 220)'
textBox.innerText = text textBox.innerText = text
this.topBar.append(textBox) this.appendWidget(textBox, align, true)
this.makeSeparator(this.topBar)
this.reSize()
return textBox return textBox
} }
makeMenu(items, activeItem, separator, callbackName) { makeMenu(items, activeItem, separator, callbackName, align='right') {
let menu = document.createElement('div') let menu = document.createElement('div')
menu.style.position = 'absolute' menu.style.position = 'absolute'
menu.style.display = 'none' menu.style.display = 'none'
@ -102,7 +110,9 @@ if (!window.TopBar) {
button.elem.style.padding = '2px 2px' button.elem.style.padding = '2px 2px'
menu.appendChild(button.elem) menu.appendChild(button.elem)
}) })
let widget = this.makeButton(activeItem+' ↓', null, separator) let widget =
this.makeButton(activeItem+' ↓', null, separator, true, align)
widget.elem.addEventListener('click', () => { widget.elem.addEventListener('click', () => {
menuOpen = !menuOpen menuOpen = !menuOpen
if (!menuOpen) return menu.style.display = 'none' if (!menuOpen) return menu.style.display = 'none'
@ -117,11 +127,11 @@ if (!window.TopBar) {
document.body.appendChild(menu) document.body.appendChild(menu)
} }
makeButton(defaultText, callbackName, separator, append=true) { makeButton(defaultText, callbackName, separator, append=true, align='left') {
let button = document.createElement('button') let button = document.createElement('button')
button.style.border = 'none' button.style.border = 'none'
button.style.padding = '2px 5px' button.style.padding = '2px 5px'
button.style.margin = '4px 18px' button.style.margin = '4px 10px'
button.style.fontSize = '13px' button.style.fontSize = '13px'
button.style.backgroundColor = 'transparent' button.style.backgroundColor = 'transparent'
button.style.color = this.textColor button.style.color = this.textColor
@ -151,17 +161,27 @@ if (!window.TopBar) {
button.style.color = this.textColor button.style.color = this.textColor
button.style.fontWeight = 'normal' button.style.fontWeight = 'normal'
}) })
if (separator) this.makeSeparator() if (append) this.appendWidget(button, align, separator)
if (append) this.topBar.appendChild(button); this.reSize()
return widget return widget
} }
makeSeparator() { makeSeparator(align='left') {
let seperator = document.createElement('div') let seperator = document.createElement('div')
seperator.style.width = '1px' seperator.style.width = '1px'
seperator.style.height = '20px' seperator.style.height = '20px'
seperator.style.backgroundColor = '#3C434C' 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 window.TopBar = TopBar

View File

@ -85,17 +85,15 @@ if (!window.Table) {
} }
newRow(vals, id) { newRow(id) {
let row = this.table.insertRow() let row = this.table.insertRow()
row.style.cursor = 'default' 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]] = row.insertCell()
row[this.headings[i]].textContent = vals[i]
row[this.headings[i]].style.width = this.widths[i]; row[this.headings[i]].style.width = this.widths[i];
row[this.headings[i]].style.textAlign = this.alignments[i]; row[this.headings[i]].style.textAlign = this.alignments[i];
row[this.headings[i]].style.border = '1px solid rgb(70, 70, 70)' 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('mouseover', () => row.style.backgroundColor = 'rgba(60, 60, 60, 0.6)')
row.addEventListener('mouseout', () => row.style.backgroundColor = 'transparent') row.addEventListener('mouseout', () => row.style.backgroundColor = 'transparent')

View File

@ -190,8 +190,8 @@ if (!window.ToolBox) {
if (!ray) { if (!ray) {
trendLine.markers = [ trendLine.markers = [
{time: firstTime, position: 'inBar', color: '#1E80F0', shape: 'circle', size: 0.1}, {time: trendLine.from[0], position: 'inBar', color: '#1E80F0', shape: 'circle', size: 0.1},
{time: currentTime, 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) trendLine.line.setMarkers(trendLine.markers)
} }
@ -411,8 +411,8 @@ if (!window.ToolBox) {
if (!hoveringOver.ray) { if (!hoveringOver.ray) {
hoveringOver.markers = [ hoveringOver.markers = [
{time: startDate, position: 'inBar', color: '#1E80F0', shape: 'circle', size: 0.1}, {time: hoveringOver.from[0], position: 'inBar', color: '#1E80F0', shape: 'circle', size: 0.1},
{time: endDate, 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) hoveringOver.line.setMarkers(hoveringOver.markers)
} }
@ -457,8 +457,8 @@ if (!window.ToolBox) {
hoveringOver.markers = [ hoveringOver.markers = [
{time: firstTime, position: 'inBar', color: '#1E80F0', shape: 'circle', size: 0.1}, {time: hoveringOver.from[0], position: 'inBar', color: '#1E80F0', shape: 'circle', size: 0.1},
{time: currentTime, 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) hoveringOver.line.setMarkers(hoveringOver.markers)

View File

@ -22,7 +22,7 @@ class Row(dict):
self._table = table self._table = table
self.id = id self.id = id
self.meta = {} 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(): for key, val in items.items():
self[key] = val self[key] = val

View File

@ -1,9 +1,12 @@
import asyncio import asyncio
from typing import Dict from typing import Dict, Literal
from .util import jbool, Pane from .util import jbool, Pane
ALIGN = Literal['left', 'right']
class Widget(Pane): class Widget(Pane):
def __init__(self, topbar, value, func=None): def __init__(self, topbar, value, func=None):
super().__init__(topbar.win) super().__init__(topbar.win)
@ -21,9 +24,9 @@ class Widget(Pane):
class TextWidget(Widget): class TextWidget(Widget):
def __init__(self, topbar, initial_text): def __init__(self, topbar, initial_text, align):
super().__init__(topbar, value=initial_text) 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): def set(self, string):
self.value = string self.value = string
@ -31,22 +34,23 @@ class TextWidget(Widget):
class SwitcherWidget(Widget): 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) 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): 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) super().__init__(topbar, value=default, func=func)
self.run_script( self.run_script(f'''
f'{self.id} = {topbar.id}.makeMenu({list(options)}, "{default}", {jbool(separator)}, "{self.id}")') {self.id} = {topbar.id}.makeMenu({list(options)}, "{default}", {jbool(separator)}, "{self.id}", "{align}")
''')
class ButtonWidget(Widget): 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) 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): def set(self, string):
self.value = string self.value = string
@ -82,20 +86,25 @@ class TopBar(Pane):
return widget return widget
raise KeyError(f'Topbar widget "{item}" not found.') 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._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._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._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._create()
self._widgets[name] = ButtonWidget(self, button_text, separator, func) self._widgets[name] = ButtonWidget(self, button_text, separator, align, func)

View File

@ -16,7 +16,7 @@ except ImportError:
try: try:
from PySide6.QtWebEngineWidgets import QWebEngineView from PySide6.QtWebEngineWidgets import QWebEngineView
from PySide6.QtWebChannel import QWebChannel from PySide6.QtWebChannel import QWebChannel
from PySide6.QtCore import QObject, Slot from PySide6.QtCore import Qt, QObject, Slot
except ImportError: except ImportError:
QWebEngineView = None QWebEngineView = None
@ -24,11 +24,11 @@ if QWebEngineView:
class Bridge(QObject): class Bridge(QObject):
def __init__(self, chart): def __init__(self, chart):
super().__init__() super().__init__()
self.chart = chart self.win = chart.win
@Slot(str) @Slot(str)
def callback(self, message): def callback(self, message):
emit_callback(self.chart, message) emit_callback(self.win, message)
try: try:
from streamlit.components.v1 import html from streamlit.components.v1 import html
@ -78,6 +78,7 @@ class QtChart(abstract.AbstractChart):
self.web_channel.registerObject('bridge', self.bridge) self.web_channel.registerObject('bridge', self.bridge)
self.webview.page().setWebChannel(self.web_channel) self.webview.page().setWebChannel(self.web_channel)
self.webview.loadFinished.connect(self.win.on_js_load) self.webview.loadFinished.connect(self.win.on_js_load)
self.webview.setContextMenuPolicy(Qt.ContextMenuPolicy.NoContextMenu)
self._html = f''' self._html = f'''
{abstract.TEMPLATE[:85]} {abstract.TEMPLATE[:85]}
<script src="qrc:///qtwebchannel/qwebchannel.js"></script> <script src="qrc:///qtwebchannel/qwebchannel.js"></script>

View File

@ -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.17.3', version='1.0.17.5',
packages=find_packages(), packages=find_packages(),
python_requires='>=3.8', python_requires='>=3.8',
install_requires=[ install_requires=[