Skip to content

Commit

Permalink
Enhancements:
Browse files Browse the repository at this point in the history
- added the `align` parameter to all topbar widgets, allowing for individual widgets to be placed either on the left or right hand side of the topbar.
- Disabled QtChart’s context menu by default.

Bug Fixes:
- The `screenshot` method now works for subcharts.
- Tables will still render if a `NoneType` is passed to them as a value.
- Qt and Wx charts no longer throw an error when using the toolbox.
  • Loading branch information
louisnw01 committed Sep 14, 2023
1 parent 8f65a7f commit b1f007f
Show file tree
Hide file tree
Showing 12 changed files with 104 additions and 71 deletions.
1 change: 1 addition & 0 deletions docs/source/reference/index.md
Expand Up @@ -17,6 +17,7 @@ tables

1. [`AbstractChart`](#AbstractChart)
2. [`Line`](#Line)
3. [`Histogram`](#Histogram)
3. [`HorizontalLine`](#HorizontalLine)
4. [Charts](#charts)
5. [`Events`](./events.md)
Expand Down
21 changes: 11 additions & 10 deletions docs/source/reference/topbar.md
Expand Up @@ -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.
Expand All @@ -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.
Expand All @@ -44,19 +47,17 @@ ___
```{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.
```
___
```{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.
Expand Down
3 changes: 3 additions & 0 deletions docs/source/reference/typing.md
Expand Up @@ -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'])
```




Expand Down
10 changes: 10 additions & 0 deletions 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
Expand Down Expand Up @@ -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':
Expand Down
10 changes: 0 additions & 10 deletions 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
Expand Down Expand Up @@ -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])
54 changes: 37 additions & 17 deletions lightweight_charts/js/callback.js
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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'
Expand All @@ -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'
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
6 changes: 2 additions & 4 deletions lightweight_charts/js/table.js
Expand Up @@ -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')
Expand Down
12 changes: 6 additions & 6 deletions lightweight_charts/js/toolbox.js
Expand Up @@ -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)
}
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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)

Expand Down
2 changes: 1 addition & 1 deletion lightweight_charts/table.py
Expand Up @@ -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

Expand Down
47 changes: 28 additions & 19 deletions 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)
Expand All @@ -21,32 +24,33 @@ 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
self.run_script(f'{self.id}.innerText = "{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
Expand Down Expand Up @@ -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)
self._widgets[name] = ButtonWidget(self, button_text, separator, align, func)

0 comments on commit b1f007f

Please sign in to comment.