Merge branch 'louisnw01:main' into main
This commit is contained in:
@ -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':
|
||||
|
||||
@ -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])
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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>
|
||||
|
||||
Reference in New Issue
Block a user