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 .table import Table
from .toolbox import ToolBox 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 .topbar import TopBar
from .util import ( from .util import (
BulkRunScript, Pane, Events, IDGen, as_enum, jbool, js_json, TIME, NUM, FLOAT, BulkRunScript, Pane, Events, IDGen, as_enum, jbool, js_json, TIME, NUM, FLOAT,
@ -728,10 +728,20 @@ class AbstractChart(Candlestick, Pane):
width: int = 2, width: int = 2,
style: LINE_STYLE = 'solid' style: LINE_STYLE = 'solid'
) -> Line: ) -> Line:
line = Line(self, '', color, style, width, False, False, False) # TODO
line._set_trend(start_time, value, start_time, value, ray=True, round=round) line = RayLine(self, '', color, style, width, False, False, False)
return line 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): def set_visible_range(self, start_time: TIME, end_time: TIME):
self.run_script(f''' self.run_script(f'''
{self.id}.chart.timeScale().setVisibleRange({{ {self.id}.chart.timeScale().setVisibleRange({{

View File

@ -24,6 +24,13 @@ class Drawing(Pane):
""" """
self.run_script(f'{self.id}.detach()') 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): class TwoPointDrawing(Drawing):
def __init__( def __init__(
self, self,
@ -76,6 +83,8 @@ class HorizontalLine(Drawing):
{{ {{
lineColor: '{color}', lineColor: '{color}',
lineStyle: {as_enum(style, LINE_STYLE)}, lineStyle: {as_enum(style, LINE_STYLE)},
width: {width},
text: `{text}`,
}}, }},
callbackName={f"'{self.id}'" if func else 'null'} callbackName={f"'{self.id}'" if func else 'null'}
) )
@ -103,22 +112,25 @@ class HorizontalLine(Drawing):
# self.run_script(f'{self.id}.updatePrice({price})') # self.run_script(f'{self.id}.updatePrice({price})')
self.price = price self.price = price
def label(self, text: str): # TODO def options(self, color='#1E80F0', style='solid', width=4, text=''):
self.run_script(f'{self.id}.updateLabel("{text}")') super().options(color, style, width)
self.run_script(f'{self.id}.applyOptions({{text: `{text}`}})')
class VerticalLine(Drawing): 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) super().__init__(chart, func)
self.time = time self.time = time
self.run_script(f''' self.run_script(f'''
{self.id} = new Lib.VerticalLine( {self.id} = new Lib.VerticalLine(
{{time: {time}}}, {{time: {self.chart._single_datetime_format(time)}}},
{{ {{
lineColor: '{color}', lineColor: '{color}',
lineStyle: {as_enum(style, LINE_STYLE)}, lineStyle: {as_enum(style, LINE_STYLE)},
width: {width},
text: `{text}`,
}}, }},
callbackName={f"'{self.id}'" if func else 'null'} callbackName={f"'{self.id}'" if func else 'null'}
) )
@ -130,8 +142,9 @@ class VerticalLine(Drawing):
# self.run_script(f'{self.id}.updatePrice({price})') # self.run_script(f'{self.id}.updatePrice({price})')
self.price = price self.price = price
def label(self, text: str): # TODO def options(self, color='#1E80F0', style='solid', width=4, text=''):
self.run_script(f'{self.id}.updateLabel("{text}")') super().options(color, style, width)
self.run_script(f'{self.id}.applyOptions({{text: `{text}`}})')
class Box(TwoPointDrawing): class Box(TwoPointDrawing):
@ -188,13 +201,14 @@ class TrendLine(TwoPointDrawing):
end_value, end_value,
round, round,
{ {
"lineColor": f'"line_color"', "lineColor": f'"{line_color}"',
"width": width, "width": width,
"lineStyle": as_enum(style, LINE_STYLE) "lineStyle": as_enum(style, LINE_STYLE)
}, },
func func
) )
# TODO reimplement/fix
class VerticalSpan(Pane): class VerticalSpan(Pane):
def __init__(self, series: 'SeriesCommon', start_time: Union[TIME, tuple, list], end_time: Optional[TIME] = None, def __init__(self, series: 'SeriesCommon', start_time: Union[TIME, tuple, list], end_time: Optional[TIME] = None,
color: str = 'rgba(252, 219, 3, 0.2)'): 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): 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) super().__init__(topbar.win)
self.value = value self.value = value
def wrapper(v): def wrapper(v):
if convert_boolean:
self.value = False if v == 'false' else True
else:
self.value = v self.value = v
func(topbar._chart) func(topbar._chart)
@ -54,6 +57,7 @@ class MenuWidget(Widget):
{self.id} = {topbar.id}.makeMenu({list(options)}, "{default}", {jbool(separator)}, "{self.id}", "{align}") {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): def set(self, option):
if option not in self.options: if option not in self.options:
raise ValueError(f"Option {option} not in menu options ({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) 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): class ButtonWidget(Widget):
def __init__(self, topbar, button, separator, align, func): def __init__(self, topbar, button, separator, align, toggle, func):
super().__init__(topbar, value=button, func=func) super().__init__(topbar, value=False, func=func, convert_boolean=toggle)
self.run_script( 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): def set(self, string):
self.value = string # self.value = string
self.run_script(f'{self.id}.elem.innerText = "{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) self._widgets[name] = TextWidget(self, initial_text, align)
def button(self, name, button_text: str, separator: bool = True, 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._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.legendHandler = this.legendHandler.bind(this)
this.handler = handler; this.handler = handler;
this.ohlcEnabled = true; this.ohlcEnabled = false;
this.percentEnabled = false this.percentEnabled = false
this.linesEnabled = false this.linesEnabled = false
this.colorBasedOnCandle = 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 { GlobalParams } from "./global-params";
import { Handler } from "./handler"; import { Handler } from "./handler";
import { Menu } from "./menu";
declare const window: GlobalParams declare const window: GlobalParams
@ -85,44 +86,11 @@ export class TopBar {
return textBox return textBox
} }
makeMenu(items: string[], activeItem: string, separator: boolean, callbackName: string, align='right') { makeMenu(items: string[], activeItem: string, separator: boolean, callbackName: string, align: 'right'|'left') {
let menu = document.createElement('div') return new Menu(this.makeButton.bind(this), callbackName, items, activeItem, separator, align)
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)
} }
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') let button = document.createElement('button')
button.classList.add('topbar-button'); button.classList.add('topbar-button');
// button.style.color = window.pane.color // button.style.color = window.pane.color
@ -137,7 +105,19 @@ export class TopBar {
} }
if (callbackName) { 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) if (append) this.appendWidget(button, align, separator)
return widget 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 { DrawingOptions } from "../drawing/options";
import { HorizontalLinePaneView } from "./pane-view"; import { HorizontalLinePaneView } from "./pane-view";
import { GlobalParams } from "../general/global-params"; import { GlobalParams } from "../general/global-params";
import { HorizontalLineAxisView } from "./axis-view";
declare const window: GlobalParams; declare const window: GlobalParams;
@ -16,6 +17,7 @@ export class HorizontalLine extends Drawing {
_paneViews: HorizontalLinePaneView[]; _paneViews: HorizontalLinePaneView[];
_point: Point; _point: Point;
private _callbackName: string | null; private _callbackName: string | null;
_priceAxisViews: HorizontalLineAxisView[];
protected _startDragPoint: Point | null = null; protected _startDragPoint: Point | null = null;
@ -24,6 +26,7 @@ export class HorizontalLine extends Drawing {
this._point = point; this._point = point;
this._point.time = null; // time is null for horizontal lines this._point.time = null; // time is null for horizontal lines
this._paneViews = [new HorizontalLinePaneView(this)]; this._paneViews = [new HorizontalLinePaneView(this)];
this._priceAxisViews = [new HorizontalLineAxisView(this)];
this._callbackName = callbackName; this._callbackName = callbackName;
} }
@ -37,6 +40,15 @@ export class HorizontalLine extends Drawing {
this.requestUpdate(); this.requestUpdate();
} }
updateAllViews() {
this._paneViews.forEach((pw) => pw.update());
this._priceAxisViews.forEach((tw) => tw.update());
}
priceAxisViews() {
return this._priceAxisViews;
}
_moveToState(state: InteractionState) { _moveToState(state: InteractionState) {
switch(state) { switch(state) {
case InteractionState.NONE: 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 point = this._source._point;
const timeScale = this._source.chart.timeScale() const timeScale = this._source.chart.timeScale()
const series = this._source.series; 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); 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 { DrawingOptions } from "../drawing/options";
import { VerticalLinePaneView } from "./pane-view"; import { VerticalLinePaneView } from "./pane-view";
import { GlobalParams } from "../general/global-params"; import { GlobalParams } from "../general/global-params";
import { VerticalLineTimeAxisView } from "./axis-view";
declare const window: GlobalParams; declare const window: GlobalParams;
@ -14,6 +15,7 @@ declare const window: GlobalParams;
export class VerticalLine extends Drawing { export class VerticalLine extends Drawing {
_type = 'VerticalLine'; _type = 'VerticalLine';
_paneViews: VerticalLinePaneView[]; _paneViews: VerticalLinePaneView[];
_timeAxisViews: VerticalLineTimeAxisView[];
_point: Point; _point: Point;
private _callbackName: string | null; private _callbackName: string | null;
@ -24,10 +26,27 @@ export class VerticalLine extends Drawing {
this._point = point; this._point = point;
this._paneViews = [new VerticalLinePaneView(this)]; this._paneViews = [new VerticalLinePaneView(this)];
this._callbackName = callbackName; 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)[]) { 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(); this.requestUpdate();
} }