implement toggleable buttons; menu items can now be changed; add horizontal line and vertical line labels
This commit is contained in:
@ -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({{
|
||||
|
||||
@ -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
@ -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)
|
||||
|
||||
@ -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
59
src/general/menu.ts
Normal 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]+' ↓';
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
|
||||
37
src/horizontal-line/axis-view.ts
Normal file
37
src/horizontal-line/axis-view.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@ -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:
|
||||
|
||||
35
src/vertical-line/axis-view.ts
Normal file
35
src/vertical-line/axis-view.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user