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)
2. [`Line`](#Line)
3. [`Histogram`](#Histogram)
3. [`HorizontalLine`](#HorizontalLine)
4. [Charts](#charts)
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
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.

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} ALIGN(Literal['left', 'right'])
```

View File

@ -1,5 +1,6 @@
import asyncio
import os
from base64 import b64decode
from datetime import datetime
from typing import Union, Literal, List, Optional
import pandas as pd
@ -887,6 +888,15 @@ class AbstractChart(Candlestick, Pane):
) -> 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':

View File

@ -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 @@ class Chart(abstract.AbstractChart):
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])

View File

@ -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

View File

@ -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')

View File

@ -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)

View File

@ -22,7 +22,7 @@ class Row(dict):
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

View File

@ -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 @@ class Widget(Pane):
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 @@ class TextWidget(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)
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 @@ class TopBar(Pane):
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)
self._widgets[name] = ButtonWidget(self, button_text, separator, align, func)

View File

@ -16,7 +16,7 @@ except ImportError:
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 @@ if QWebEngineView:
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 @@ class QtChart(abstract.AbstractChart):
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]}
<script src="qrc:///qtwebchannel/qwebchannel.js"></script>

View File

@ -5,7 +5,7 @@ with open('README.md', 'r', encoding='utf-8') as f:
setup(
name='lightweight_charts',
version='1.0.17.3',
version='1.0.17.5',
packages=find_packages(),
python_requires='>=3.8',
install_requires=[