implement toggleable buttons; menu items can now be changed; add horizontal line and vertical line labels

This commit is contained in:
louisnw
2024-05-31 17:25:55 +01:00
parent ca93ddbcb1
commit 7915863a64
13 changed files with 233 additions and 59 deletions

View File

@ -8,7 +8,7 @@ import pandas as pd
from .table import Table
from .toolbox import ToolBox
from .drawings import Box, HorizontalLine, TrendLine, TwoPointDrawing, VerticalSpan
from .drawings import Box, HorizontalLine, TrendLine, TwoPointDrawing, VerticalLine, VerticalSpan
from .topbar import TopBar
from .util import (
BulkRunScript, Pane, Events, IDGen, as_enum, jbool, js_json, TIME, NUM, FLOAT,
@ -728,10 +728,20 @@ class AbstractChart(Candlestick, Pane):
width: int = 2,
style: LINE_STYLE = 'solid'
) -> Line:
line = Line(self, '', color, style, width, False, False, False)
line._set_trend(start_time, value, start_time, value, ray=True, round=round)
# TODO
line = RayLine(self, '', color, style, width, False, False, False)
return line
def vertical_line(
self,
time: TIME,
color: str = '#1E80F0',
width: int = 2,
style: LINE_STYLE ='solid',
text: str = ''
) -> VerticalLine:
return VerticalLine(*locals().values())
def set_visible_range(self, start_time: TIME, end_time: TIME):
self.run_script(f'''
{self.id}.chart.timeScale().setVisibleRange({{

View File

@ -24,6 +24,13 @@ class Drawing(Pane):
"""
self.run_script(f'{self.id}.detach()')
def options(self, color='#1E80F0', style='solid', width=4):
self.run_script(f'''{self.id}.applyOptions({{
lineColor: '{color}',
lineStyle: {as_enum(style, LINE_STYLE)},
width: {width},
}})''')
class TwoPointDrawing(Drawing):
def __init__(
self,
@ -76,6 +83,8 @@ class HorizontalLine(Drawing):
{{
lineColor: '{color}',
lineStyle: {as_enum(style, LINE_STYLE)},
width: {width},
text: `{text}`,
}},
callbackName={f"'{self.id}'" if func else 'null'}
)
@ -103,22 +112,25 @@ class HorizontalLine(Drawing):
# self.run_script(f'{self.id}.updatePrice({price})')
self.price = price
def label(self, text: str): # TODO
self.run_script(f'{self.id}.updateLabel("{text}")')
def options(self, color='#1E80F0', style='solid', width=4, text=''):
super().options(color, style, width)
self.run_script(f'{self.id}.applyOptions({{text: `{text}`}})')
class VerticalLine(Drawing):
def __init__(self, chart, time, color, width, style, text, axis_label_visible, func):
def __init__(self, chart, time, color, width, style, text, func=None):
super().__init__(chart, func)
self.time = time
self.run_script(f'''
{self.id} = new Lib.VerticalLine(
{{time: {time}}},
{{time: {self.chart._single_datetime_format(time)}}},
{{
lineColor: '{color}',
lineStyle: {as_enum(style, LINE_STYLE)},
width: {width},
text: `{text}`,
}},
callbackName={f"'{self.id}'" if func else 'null'}
)
@ -130,8 +142,9 @@ class VerticalLine(Drawing):
# self.run_script(f'{self.id}.updatePrice({price})')
self.price = price
def label(self, text: str): # TODO
self.run_script(f'{self.id}.updateLabel("{text}")')
def options(self, color='#1E80F0', style='solid', width=4, text=''):
super().options(color, style, width)
self.run_script(f'{self.id}.applyOptions({{text: `{text}`}})')
class Box(TwoPointDrawing):
@ -188,13 +201,14 @@ class TrendLine(TwoPointDrawing):
end_value,
round,
{
"lineColor": f'"line_color"',
"lineColor": f'"{line_color}"',
"width": width,
"lineStyle": as_enum(style, LINE_STYLE)
},
func
)
# TODO reimplement/fix
class VerticalSpan(Pane):
def __init__(self, series: 'SeriesCommon', start_time: Union[TIME, tuple, list], end_time: Optional[TIME] = None,
color: str = 'rgba(252, 219, 3, 0.2)'):

File diff suppressed because one or more lines are too long

View File

@ -8,11 +8,14 @@ ALIGN = Literal['left', 'right']
class Widget(Pane):
def __init__(self, topbar, value, func: callable = None):
def __init__(self, topbar, value, func: callable = None, convert_boolean=False):
super().__init__(topbar.win)
self.value = value
def wrapper(v):
if convert_boolean:
self.value = False if v == 'false' else True
else:
self.value = v
func(topbar._chart)
@ -54,6 +57,7 @@ class MenuWidget(Widget):
{self.id} = {topbar.id}.makeMenu({list(options)}, "{default}", {jbool(separator)}, "{self.id}", "{align}")
''')
# TODO this will probably need to be fixed
def set(self, option):
if option not in self.options:
raise ValueError(f"Option {option} not in menu options ({self.options})")
@ -63,15 +67,19 @@ class MenuWidget(Widget):
''')
self.win.handlers[self.id](option)
def update_items(self, *items: str):
self.options = list(items)
self.run_script(f'{self.id}.updateMenuItems({self.options})')
class ButtonWidget(Widget):
def __init__(self, topbar, button, separator, align, func):
super().__init__(topbar, value=button, func=func)
def __init__(self, topbar, button, separator, align, toggle, func):
super().__init__(topbar, value=False, func=func, convert_boolean=toggle)
self.run_script(
f'{self.id} = {topbar.id}.makeButton("{button}", "{self.id}", {jbool(separator)}, true, "{align}")')
f'{self.id} = {topbar.id}.makeButton("{button}", "{self.id}", {jbool(separator)}, true, "{align}", {jbool(toggle)})')
def set(self, string):
self.value = string
# self.value = string
self.run_script(f'{self.id}.elem.innerText = "{string}"')
@ -112,6 +120,6 @@ class TopBar(Pane):
self._widgets[name] = TextWidget(self, initial_text, align)
def button(self, name, button_text: str, separator: bool = True,
align: ALIGN = 'left', func: callable = None):
align: ALIGN = 'left', toggle: bool = False, func: callable = None):
self._create()
self._widgets[name] = ButtonWidget(self, button_text, separator, align, func)
self._widgets[name] = ButtonWidget(self, button_text, separator, align, toggle, func)

View File

@ -29,7 +29,7 @@ export class Legend {
this.legendHandler = this.legendHandler.bind(this)
this.handler = handler;
this.ohlcEnabled = true;
this.ohlcEnabled = false;
this.percentEnabled = false
this.linesEnabled = false
this.colorBasedOnCandle = false

59
src/general/menu.ts Normal file
View File

@ -0,0 +1,59 @@
import { GlobalParams } from "./global-params";
declare const window: GlobalParams
export class Menu {
private div: HTMLDivElement;
private isOpen: boolean = false;
private widget: any;
constructor(
private makeButton: Function,
private callbackName: string,
items: string[],
activeItem: string,
separator: boolean,
align: 'right'|'left') {
this.div = document.createElement('div')
this.div.classList.add('topbar-menu');
this.widget = this.makeButton(activeItem+' ↓', null, separator, true, align)
this.updateMenuItems(items)
this.widget.elem.addEventListener('click', () => {
this.isOpen = !this.isOpen;
if (!this.isOpen) {
this.div.style.display = 'none';
return;
}
let rect = this.widget.elem.getBoundingClientRect()
this.div.style.display = 'flex'
this.div.style.flexDirection = 'column'
let center = rect.x+(rect.width/2)
this.div.style.left = center-(this.div.clientWidth/2)+'px'
this.div.style.top = rect.y+rect.height+'px'
})
document.body.appendChild(this.div)
}
updateMenuItems(items: string[]) {
this.div.innerHTML = '';
items.forEach(text => {
let button = this.makeButton(text, null, false, false)
button.elem.addEventListener('click', () => {
this.widget.elem.innerText = button.elem.innerText+' ↓'
window.callbackFunction(`${this.callbackName}_~_${button.elem.innerText}`)
this.div.style.display = 'none'
this.isOpen = false
});
button.elem.style.margin = '4px 4px'
button.elem.style.padding = '2px 2px'
this.div.appendChild(button.elem)
})
this.widget.elem.innerText = items[0]+' ↓';
}
}

View File

@ -1,5 +1,6 @@
import { GlobalParams } from "./global-params";
import { Handler } from "./handler";
import { Menu } from "./menu";
declare const window: GlobalParams
@ -85,44 +86,11 @@ export class TopBar {
return textBox
}
makeMenu(items: string[], activeItem: string, separator: boolean, callbackName: string, align='right') {
let menu = document.createElement('div')
menu.classList.add('topbar-menu');
let menuOpen = false;
items.forEach(text => {
let button = this.makeButton(text, null, false, false)
button.elem.addEventListener('click', () => {
widget.elem.innerText = button.elem.innerText+' ↓'
window.callbackFunction(`${callbackName}_~_${button.elem.innerText}`)
menu.style.display = 'none'
menuOpen = false
});
button.elem.style.margin = '4px 4px'
button.elem.style.padding = '2px 2px'
menu.appendChild(button.elem)
})
let widget =
this.makeButton(activeItem+' ↓', null, separator, true, align)
widget.elem.addEventListener('click', () => {
menuOpen = !menuOpen
if (!menuOpen) {
menu.style.display = 'none';
return;
}
let rect = widget.elem.getBoundingClientRect()
menu.style.display = 'flex'
menu.style.flexDirection = 'column'
let center = rect.x+(rect.width/2)
menu.style.left = center-(menu.clientWidth/2)+'px'
menu.style.top = rect.y+rect.height+'px'
})
document.body.appendChild(menu)
makeMenu(items: string[], activeItem: string, separator: boolean, callbackName: string, align: 'right'|'left') {
return new Menu(this.makeButton.bind(this), callbackName, items, activeItem, separator, align)
}
makeButton(defaultText: string, callbackName: string | null, separator: boolean, append=true, align='left') {
makeButton(defaultText: string, callbackName: string | null, separator: boolean, append=true, align='left', toggle=false) {
let button = document.createElement('button')
button.classList.add('topbar-button');
// button.style.color = window.pane.color
@ -137,7 +105,19 @@ export class TopBar {
}
if (callbackName) {
button.addEventListener('click', () => window.callbackFunction(`${widget.callbackName}_~_${button.innerText}`));
let handler;
if (toggle) {
let state = false;
handler = () => {
state = !state
window.callbackFunction(`${widget.callbackName}_~_${state}`)
button.style.backgroundColor = state ? 'var(--active-bg-color)' : '';
button.style.color = state ? 'var(--active-color)' : '';
}
} else {
handler = () => window.callbackFunction(`${widget.callbackName}_~_${button.innerText}`)
}
button.addEventListener('click', handler);
}
if (append) this.appendWidget(button, align, separator)
return widget

View File

@ -0,0 +1,37 @@
import { Coordinate, ISeriesPrimitiveAxisView, PriceFormatBuiltIn } from 'lightweight-charts';
import { HorizontalLine } from './horizontal-line';
export class HorizontalLineAxisView implements ISeriesPrimitiveAxisView {
_source: HorizontalLine;
_y: Coordinate | null = null;
_price: string | null = null;
constructor(source: HorizontalLine) {
this._source = source;
}
update() {
if (!this._source.series || !this._source._point) return;
this._y = this._source.series.priceToCoordinate(this._source._point.price);
const priceFormat = this._source.series.options().priceFormat as PriceFormatBuiltIn;
const precision = priceFormat.precision;
this._price = this._source._point.price.toFixed(precision).toString();
}
visible() {
return true;
}
tickVisible() {
return true;
}
coordinate() {
return this._y ?? 0;
}
text() {
return this._source._options.text || this._price || '';
}
textColor() {
return 'white';
}
backColor() {
return this._source._options.lineColor;
}
}

View File

@ -7,6 +7,7 @@ import { Drawing, InteractionState } from "../drawing/drawing";
import { DrawingOptions } from "../drawing/options";
import { HorizontalLinePaneView } from "./pane-view";
import { GlobalParams } from "../general/global-params";
import { HorizontalLineAxisView } from "./axis-view";
declare const window: GlobalParams;
@ -16,6 +17,7 @@ export class HorizontalLine extends Drawing {
_paneViews: HorizontalLinePaneView[];
_point: Point;
private _callbackName: string | null;
_priceAxisViews: HorizontalLineAxisView[];
protected _startDragPoint: Point | null = null;
@ -24,6 +26,7 @@ export class HorizontalLine extends Drawing {
this._point = point;
this._point.time = null; // time is null for horizontal lines
this._paneViews = [new HorizontalLinePaneView(this)];
this._priceAxisViews = [new HorizontalLineAxisView(this)];
this._callbackName = callbackName;
}
@ -37,6 +40,15 @@ export class HorizontalLine extends Drawing {
this.requestUpdate();
}
updateAllViews() {
this._paneViews.forEach((pw) => pw.update());
this._priceAxisViews.forEach((tw) => tw.update());
}
priceAxisViews() {
return this._priceAxisViews;
}
_moveToState(state: InteractionState) {
switch(state) {
case InteractionState.NONE:

View File

@ -0,0 +1,35 @@
import { Coordinate, ISeriesPrimitiveAxisView } from "lightweight-charts";
import { VerticalLine } from "./vertical-line";
export class VerticalLineTimeAxisView implements ISeriesPrimitiveAxisView {
_source: VerticalLine;
_x: Coordinate | null = null;
constructor(source: VerticalLine) {
this._source = source;
}
update() {
if (!this._source.chart|| !this._source._point) return;
const point = this._source._point;
const timeScale = this._source.chart.timeScale();
this._x = point.time ? timeScale.timeToCoordinate(point.time) : timeScale.logicalToCoordinate(point.logical);
}
visible() {
return !!this._source._options.text;
}
tickVisible() {
return true;
}
coordinate() {
return this._x ?? 0;
}
text() {
return this._source._options.text || '';
}
textColor() {
return "white";
}
backColor() {
return this._source._options.lineColor;
}
}

View File

@ -16,7 +16,7 @@ export class VerticalLinePaneView extends DrawingPaneView {
const point = this._source._point;
const timeScale = this._source.chart.timeScale()
const series = this._source.series;
this._point.x = timeScale.logicalToCoordinate(point.logical)
this._point.x = point.time ? timeScale.timeToCoordinate(point.time) : timeScale.logicalToCoordinate(point.logical)
this._point.y = series.priceToCoordinate(point.price);
}

View File

@ -7,6 +7,7 @@ import { Drawing, InteractionState } from "../drawing/drawing";
import { DrawingOptions } from "../drawing/options";
import { VerticalLinePaneView } from "./pane-view";
import { GlobalParams } from "../general/global-params";
import { VerticalLineTimeAxisView } from "./axis-view";
declare const window: GlobalParams;
@ -14,6 +15,7 @@ declare const window: GlobalParams;
export class VerticalLine extends Drawing {
_type = 'VerticalLine';
_paneViews: VerticalLinePaneView[];
_timeAxisViews: VerticalLineTimeAxisView[];
_point: Point;
private _callbackName: string | null;
@ -24,10 +26,27 @@ export class VerticalLine extends Drawing {
this._point = point;
this._paneViews = [new VerticalLinePaneView(this)];
this._callbackName = callbackName;
this._timeAxisViews = [new VerticalLineTimeAxisView(this)]
}
updateAllViews() {
this._paneViews.forEach(pw => pw.update());
this._timeAxisViews.forEach(tw => tw.update());
}
timeAxisViews() {
return this._timeAxisViews;
}
public updatePoints(...points: (Point | null)[]) {
for (const p of points) if (p) this._point = p;
for (const p of points) {
if (!p) continue;
if (!p.time && p.logical) {
p.time = this.series.dataByIndex(p.logical)?.time || null
}
this._point = p;
}
this.requestUpdate();
}