implement drawing methods, fix horizontal line bug, continue refactor
This commit is contained in:
@ -7,11 +7,12 @@ import pandas as pd
|
||||
|
||||
from .table import Table
|
||||
from .toolbox import ToolBox
|
||||
from .drawings import HorizontalLine, TwoPointDrawing, VerticalSpan
|
||||
from .topbar import TopBar
|
||||
from .util import (
|
||||
IDGen, as_enum, jbool, Pane, Events, TIME, NUM, FLOAT,
|
||||
LINE_STYLE, MARKER_POSITION, MARKER_SHAPE, CROSSHAIR_MODE, PRICE_SCALE_MODE, js_json,
|
||||
marker_position, marker_shape, js_data,
|
||||
Pane, Events, IDGen, as_enum, jbool, js_json, TIME, NUM, FLOAT,
|
||||
LINE_STYLE, MARKER_POSITION, MARKER_SHAPE, CROSSHAIR_MODE,
|
||||
PRICE_SCALE_MODE, marker_position, marker_shape, js_data,
|
||||
)
|
||||
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
@ -43,8 +44,16 @@ class Window:
|
||||
if self.loaded:
|
||||
return
|
||||
self.loaded = True
|
||||
[self.run_script(script) for script in self.scripts]
|
||||
[self.run_script(script) for script in self.final_scripts]
|
||||
|
||||
# TODO this wont work for anything which isnt pywebview :( put it in the chart class ?
|
||||
while not self.run_script_and_get('document.readyState == "complete"'):
|
||||
continue # scary, but works
|
||||
|
||||
initial_script = ''
|
||||
self.scripts.extend(self.final_scripts)
|
||||
for script in self.scripts:
|
||||
initial_script += f'\n{script}'
|
||||
self.script_func(initial_script)
|
||||
|
||||
def run_script(self, script: str, run_last: bool = False):
|
||||
"""
|
||||
@ -54,8 +63,10 @@ class Window:
|
||||
raise AttributeError("script_func has not been set")
|
||||
if self.loaded:
|
||||
self.script_func(script)
|
||||
return
|
||||
self.scripts.append(script) if not run_last else self.final_scripts.append(script)
|
||||
elif run_last:
|
||||
self.final_scripts.append(script)
|
||||
else:
|
||||
self.scripts.append(script)
|
||||
|
||||
def run_script_and_get(self, script: str):
|
||||
self.run_script(f'_~_~RETURN~_~_{script}')
|
||||
@ -78,9 +89,7 @@ class Window:
|
||||
return_clicked_cells: bool = False,
|
||||
func: Optional[Callable] = None
|
||||
) -> 'Table':
|
||||
return Table(self, width, height, headings, widths, alignments, position, draggable,
|
||||
background_color, border_color, border_width, heading_text_colors,
|
||||
heading_background_colors, return_clicked_cells, func)
|
||||
return Table(*locals().values())
|
||||
|
||||
def create_subchart(
|
||||
self,
|
||||
@ -92,14 +101,21 @@ class Window:
|
||||
sync_crosshairs_only: bool = False,
|
||||
toolbox: bool = False
|
||||
) -> 'AbstractChart':
|
||||
subchart = AbstractChart(self, width, height, scale_candles_only, toolbox, position=position)
|
||||
subchart = AbstractChart(
|
||||
self,
|
||||
width,
|
||||
height,
|
||||
scale_candles_only,
|
||||
toolbox,
|
||||
position=position
|
||||
)
|
||||
if not sync_id:
|
||||
return subchart
|
||||
self.run_script(f'''
|
||||
Handler.syncCharts({subchart.id}, {sync_id}, {jbool(sync_crosshairs_only)})
|
||||
// TODO this should be in syncCharts
|
||||
{subchart.id}.chart.timeScale().setVisibleLogicalRange(
|
||||
{sync_id}.chart.timeScale().getVisibleLogicalRange()
|
||||
Handler.syncCharts(
|
||||
{subchart.id},
|
||||
{sync_id},
|
||||
{jbool(sync_crosshairs_only)}
|
||||
)
|
||||
''', run_last=True)
|
||||
return subchart
|
||||
@ -377,95 +393,6 @@ class SeriesCommon(Pane):
|
||||
end_time = self._single_datetime_format(end_time) if end_time else None
|
||||
return VerticalSpan(self, start_time, end_time, color)
|
||||
|
||||
# TODO drawings should be in a seperate folder, and inherbit a abstract Drawing class
|
||||
class HorizontalLine(Pane):
|
||||
def __init__(self, chart, price, color, width, style, text, axis_label_visible, func):
|
||||
super().__init__(chart.win)
|
||||
self.price = price
|
||||
self.run_script(f'''
|
||||
|
||||
{self.id} = new HorizontalLine(
|
||||
{{price: {price}}},
|
||||
{{
|
||||
lineColor: '{color}',
|
||||
lineStyle: {as_enum(style, LINE_STYLE)},
|
||||
}},
|
||||
callbackName={f"'{self.id}'" if func else 'null'}
|
||||
)
|
||||
{chart.id}.series.attachPrimitive({self.id})
|
||||
''')
|
||||
# {self.id} = new HorizontalLine(
|
||||
# {chart.id}, '{self.id}', {price}, '{color}', {width},
|
||||
# {as_enum(style, LINE_STYLE)}, {jbool(axis_label_visible)}, '{text}'
|
||||
# )''')
|
||||
if not func:
|
||||
return
|
||||
|
||||
def wrapper(p):
|
||||
self.price = float(p)
|
||||
func(chart, self)
|
||||
|
||||
async def wrapper_async(p):
|
||||
self.price = float(p)
|
||||
await func(chart, self)
|
||||
|
||||
self.win.handlers[self.id] = wrapper_async if asyncio.iscoroutinefunction(func) else wrapper
|
||||
self.run_script(f'{chart.id}.toolBox?.addNewDrawing({self.id})')
|
||||
|
||||
def update(self, price: float):
|
||||
"""
|
||||
Moves the horizontal line to the given price.
|
||||
"""
|
||||
self.run_script(f'{self.id}.updatePoints({{price: {price}}})')
|
||||
# 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 delete(self): # TODO test all methods
|
||||
"""
|
||||
Irreversibly deletes the horizontal line.
|
||||
"""
|
||||
self.run_script(f'{self.id}.detach()')
|
||||
|
||||
|
||||
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)'):
|
||||
self._chart = series._chart
|
||||
super().__init__(self._chart.win)
|
||||
start_time, end_time = pd.to_datetime(start_time), pd.to_datetime(end_time)
|
||||
self.run_script(f'''
|
||||
{self.id} = {self._chart.id}.chart.addHistogramSeries({{
|
||||
color: '{color}',
|
||||
priceFormat: {{type: 'volume'}},
|
||||
priceScaleId: 'vertical_line',
|
||||
lastValueVisible: false,
|
||||
priceLineVisible: false,
|
||||
}})
|
||||
{self.id}.priceScale('').applyOptions({{
|
||||
scaleMargins: {{top: 0, bottom: 0}}
|
||||
}})
|
||||
''')
|
||||
if end_time is None:
|
||||
if isinstance(start_time, pd.DatetimeIndex):
|
||||
data = [{'time': time.timestamp(), 'value': 1} for time in start_time]
|
||||
else:
|
||||
data = [{'time': start_time.timestamp(), 'value': 1}]
|
||||
self.run_script(f'{self.id}.setData({data})')
|
||||
else:
|
||||
self.run_script(f'''
|
||||
{self.id}.setData(calculateTrendLine(
|
||||
{start_time.timestamp()}, 1, {end_time.timestamp()}, 1, {series.id}))
|
||||
''')
|
||||
|
||||
def delete(self):
|
||||
"""
|
||||
Irreversibly deletes the vertical span.
|
||||
"""
|
||||
self.run_script(f'{self._chart.id}.chart.removeSeries({self.id})')
|
||||
|
||||
|
||||
class Line(SeriesCommon):
|
||||
def __init__(self, chart, name, color, style, width, price_line, price_label, crosshair_marker=True):
|
||||
@ -494,20 +421,20 @@ class Line(SeriesCommon):
|
||||
)
|
||||
null''')
|
||||
|
||||
def _set_trend(self, start_time, start_value, end_time, end_value, ray=False, round=False):
|
||||
if round:
|
||||
start_time = self._single_datetime_format(start_time)
|
||||
end_time = self._single_datetime_format(end_time)
|
||||
else:
|
||||
start_time, end_time = pd.to_datetime((start_time, end_time)).astype('int64') // 10 ** 9
|
||||
# def _set_trend(self, start_time, start_value, end_time, end_value, ray=False, round=False):
|
||||
# if round:
|
||||
# start_time = self._single_datetime_format(start_time)
|
||||
# end_time = self._single_datetime_format(end_time)
|
||||
# else:
|
||||
# start_time, end_time = pd.to_datetime((start_time, end_time)).astype('int64') // 10 ** 9
|
||||
|
||||
self.run_script(f'''
|
||||
{self._chart.id}.chart.timeScale().applyOptions({{shiftVisibleRangeOnNewBar: false}})
|
||||
{self.id}.series.setData(
|
||||
calculateTrendLine({start_time}, {start_value}, {end_time}, {end_value},
|
||||
{self._chart.id}, {jbool(ray)}))
|
||||
{self._chart.id}.chart.timeScale().applyOptions({{shiftVisibleRangeOnNewBar: true}})
|
||||
''')
|
||||
# self.run_script(f'''
|
||||
# {self._chart.id}.chart.timeScale().applyOptions({{shiftVisibleRangeOnNewBar: false}})
|
||||
# {self.id}.series.setData(
|
||||
# calculateTrendLine({start_time}, {start_value}, {end_time}, {end_value},
|
||||
# {self._chart.id}, {jbool(ray)}))
|
||||
# {self._chart.id}.chart.timeScale().applyOptions({{shiftVisibleRangeOnNewBar: true}})
|
||||
# ''')
|
||||
|
||||
def delete(self):
|
||||
"""
|
||||
@ -576,11 +503,11 @@ class Candlestick(SeriesCommon):
|
||||
|
||||
# self.run_script(f'{self.id}.makeCandlestickSeries()')
|
||||
|
||||
def set(self, df: Optional[pd.DataFrame] = None, render_drawings=False):
|
||||
def set(self, df: Optional[pd.DataFrame] = None, keep_drawings=False):
|
||||
"""
|
||||
Sets the initial data for the chart.\n
|
||||
:param df: columns: date/time, open, high, low, close, volume (if volume enabled).
|
||||
:param render_drawings: Re-renders any drawings made through the toolbox. Otherwise, they will be deleted.
|
||||
:param keep_drawings: keeps any drawings made through the toolbox. Otherwise, they will be deleted.
|
||||
"""
|
||||
if df is None or df.empty:
|
||||
self.run_script(f'{self.id}.series.setData([])')
|
||||
@ -592,9 +519,8 @@ class Candlestick(SeriesCommon):
|
||||
self._last_bar = df.iloc[-1]
|
||||
|
||||
self.run_script(f'{self.id}.series.setData({js_data(df)})')
|
||||
# TODO are we not using renderdrawings then?
|
||||
# toolbox_action = 'clearDrawings' if not render_drawings else 'renderDrawings'
|
||||
# self.run_script(f"if ({self._chart.id}.toolBox) {self._chart.id}.toolBox.{toolbox_action}()")
|
||||
# TODO keep drawings doesnt do anything
|
||||
self.run_script(f"if ({self._chart.id}.toolBox) {self._chart.id}.toolBox.clearDrawings()")
|
||||
if 'volume' not in df:
|
||||
return
|
||||
volume = df.drop(columns=['open', 'high', 'low', 'close']).rename(columns={'volume': 'value'})
|
||||
@ -612,7 +538,7 @@ class Candlestick(SeriesCommon):
|
||||
{self.id}.chart.priceScale("right").applyOptions({{autoScale: true}})
|
||||
''')
|
||||
|
||||
def update(self, series: pd.Series, render_drawings=False, _from_tick=False):
|
||||
def update(self, series: pd.Series, _from_tick=False):
|
||||
"""
|
||||
Updates the data from a bar;
|
||||
if series['time'] is the same time as the last bar, the last bar will be overwritten.\n
|
||||
@ -661,10 +587,20 @@ class Candlestick(SeriesCommon):
|
||||
self.update(bar, _from_tick=True)
|
||||
|
||||
def price_scale(
|
||||
self, auto_scale: bool = True, mode: PRICE_SCALE_MODE = 'normal', invert_scale: bool = False,
|
||||
align_labels: bool = True, scale_margin_top: float = 0.2, scale_margin_bottom: float = 0.2,
|
||||
border_visible: bool = False, border_color: Optional[str] = None, text_color: Optional[str] = None,
|
||||
entire_text_only: bool = False, visible: bool = True, ticks_visible: bool = False, minimum_width: int = 0
|
||||
self,
|
||||
auto_scale: bool = True,
|
||||
mode: PRICE_SCALE_MODE = 'normal',
|
||||
invert_scale: bool = False,
|
||||
align_labels: bool = True,
|
||||
scale_margin_top: float = 0.2,
|
||||
scale_margin_bottom: float = 0.2,
|
||||
border_visible: bool = False,
|
||||
border_color: Optional[str] = None,
|
||||
text_color: Optional[str] = None,
|
||||
entire_text_only: bool = False,
|
||||
visible: bool = True,
|
||||
ticks_visible: bool = False,
|
||||
minimum_width: int = 0
|
||||
):
|
||||
self.run_script(f'''
|
||||
{self.id}.series.priceScale().applyOptions({{
|
||||
@ -776,16 +712,39 @@ class AbstractChart(Candlestick, Pane):
|
||||
"""
|
||||
return self._lines.copy()
|
||||
|
||||
def trend_line(self, start_time: TIME, start_value: NUM, end_time: TIME, end_value: NUM,
|
||||
round: bool = False, color: str = '#1E80F0', width: int = 2,
|
||||
def trend_line(
|
||||
self,
|
||||
start_time: TIME,
|
||||
start_value: NUM,
|
||||
end_time: TIME,
|
||||
end_value: NUM,
|
||||
round: bool = False,
|
||||
color: str = '#1E80F0',
|
||||
width: int = 2,
|
||||
style: LINE_STYLE = 'solid',
|
||||
) -> Line:
|
||||
line = Line(self, '', color, style, width, False, False, False)
|
||||
line._set_trend(start_time, start_value, end_time, end_value, round=round)
|
||||
return line
|
||||
) -> TwoPointDrawing:
|
||||
return TwoPointDrawing("TrendLine", *locals().values())
|
||||
|
||||
def ray_line(self, start_time: TIME, value: NUM, round: bool = False,
|
||||
color: str = '#1E80F0', width: int = 2,
|
||||
def box(
|
||||
self,
|
||||
start_time: TIME,
|
||||
start_value: NUM,
|
||||
end_time: TIME,
|
||||
end_value: NUM,
|
||||
round: bool = False,
|
||||
color: str = '#1E80F0',
|
||||
width: int = 2,
|
||||
style: LINE_STYLE = 'solid',
|
||||
) -> TwoPointDrawing:
|
||||
return TwoPointDrawing("Box", *locals().values())
|
||||
|
||||
def ray_line(
|
||||
self,
|
||||
start_time: TIME,
|
||||
value: NUM,
|
||||
round: bool = False,
|
||||
color: str = '#1E80F0',
|
||||
width: int = 2,
|
||||
style: LINE_STYLE = 'solid'
|
||||
) -> Line:
|
||||
line = Line(self, '', color, style, width, False, False, False)
|
||||
@ -851,11 +810,20 @@ class AbstractChart(Candlestick, Pane):
|
||||
}}
|
||||
}})""")
|
||||
|
||||
def crosshair(self, mode: CROSSHAIR_MODE = 'normal', vert_visible: bool = True,
|
||||
vert_width: int = 1, vert_color: Optional[str] = None, vert_style: LINE_STYLE = 'large_dashed',
|
||||
vert_label_background_color: str = 'rgb(46, 46, 46)', horz_visible: bool = True,
|
||||
horz_width: int = 1, horz_color: Optional[str] = None, horz_style: LINE_STYLE = 'large_dashed',
|
||||
horz_label_background_color: str = 'rgb(55, 55, 55)'):
|
||||
def crosshair(
|
||||
self,
|
||||
mode: CROSSHAIR_MODE = 'normal',
|
||||
vert_visible: bool = True,
|
||||
vert_width: int = 1,
|
||||
vert_color: Optional[str] = None,
|
||||
vert_style: LINE_STYLE = 'large_dashed',
|
||||
vert_label_background_color: str = 'rgb(46, 46, 46)',
|
||||
horz_visible: bool = True,
|
||||
horz_width: int = 1,
|
||||
horz_color: Optional[str] = None,
|
||||
horz_style: LINE_STYLE = 'large_dashed',
|
||||
horz_label_background_color: str = 'rgb(55, 55, 55)'
|
||||
):
|
||||
"""
|
||||
Crosshair formatting for its vertical and horizontal axes.
|
||||
"""
|
||||
@ -877,7 +845,8 @@ class AbstractChart(Candlestick, Pane):
|
||||
style: {as_enum(horz_style, LINE_STYLE)},
|
||||
labelBackgroundColor: "{horz_label_background_color}"
|
||||
}}
|
||||
}}}})''')
|
||||
}}
|
||||
}})''')
|
||||
|
||||
def watermark(self, text: str, font_size: int = 44, color: str = 'rgba(180, 180, 200, 0.5)'):
|
||||
"""
|
||||
@ -966,9 +935,9 @@ class AbstractChart(Candlestick, Pane):
|
||||
return_clicked_cells: bool = False,
|
||||
func: Optional[Callable] = None
|
||||
) -> Table:
|
||||
return self.win.create_table(width, height, headings, widths, alignments, position, draggable,
|
||||
background_color, border_color, border_width, heading_text_colors,
|
||||
heading_background_colors, return_clicked_cells, func)
|
||||
args = locals()
|
||||
del args['self']
|
||||
return self.win.create_table(*args.values())
|
||||
|
||||
def screenshot(self) -> bytes:
|
||||
"""
|
||||
@ -984,5 +953,6 @@ class AbstractChart(Candlestick, Pane):
|
||||
toolbox: bool = False) -> 'AbstractChart':
|
||||
if sync is True:
|
||||
sync = self.id
|
||||
return self.win.create_subchart(position, width, height, sync,
|
||||
scale_candles_only, sync_crosshairs_only, toolbox)
|
||||
args = locals()
|
||||
del args['self']
|
||||
return self.win.create_subchart(*args.values())
|
||||
|
||||
@ -81,7 +81,6 @@ class PyWV:
|
||||
window.show()
|
||||
elif arg == 'hide':
|
||||
window.hide()
|
||||
# TODO make sure setup.py requires latest pywebview now
|
||||
else:
|
||||
try:
|
||||
if '_~_~RETURN~_~_' in arg:
|
||||
|
||||
167
lightweight_charts/drawings.py
Normal file
167
lightweight_charts/drawings.py
Normal file
@ -0,0 +1,167 @@
|
||||
import asyncio
|
||||
import json
|
||||
import pandas as pd
|
||||
|
||||
from typing import Union, Optional
|
||||
|
||||
from lightweight_charts.util import js_json
|
||||
|
||||
from .util import NUM, Pane, as_enum, LINE_STYLE, TIME
|
||||
|
||||
|
||||
class Drawing(Pane):
|
||||
def __init__(self, chart, color, width, style, func=None):
|
||||
super().__init__(chart.win)
|
||||
self.chart = chart
|
||||
|
||||
def update(self, *points):
|
||||
js_json_string = f'JSON.parse({json.dumps(points)})'
|
||||
self.run_script(f'{self.id}.updatePoints(...{js_json_string})')
|
||||
|
||||
def delete(self):
|
||||
"""
|
||||
Irreversibly deletes the drawing.
|
||||
"""
|
||||
self.run_script(f'''
|
||||
if ({self.chart.id}.toolBox) {self.chart.id}.toolBox.delete({self.id})
|
||||
else {self.id}.detach()
|
||||
''')
|
||||
|
||||
class TwoPointDrawing(Drawing):
|
||||
def __init__(
|
||||
self,
|
||||
drawing_type,
|
||||
chart,
|
||||
start_time: TIME,
|
||||
start_value: NUM,
|
||||
end_time: TIME,
|
||||
end_value: NUM,
|
||||
round: bool,
|
||||
color,
|
||||
width,
|
||||
style,
|
||||
func=None
|
||||
):
|
||||
super().__init__(chart, color, width, style, func)
|
||||
|
||||
|
||||
def make_js_point(time, price):
|
||||
return js_json({"time": time, "price": price})
|
||||
|
||||
self.run_script(f'''
|
||||
{self.id} = new {drawing_type}(
|
||||
{make_js_point(start_time, start_value)},
|
||||
{make_js_point(end_time, end_value)},
|
||||
{{
|
||||
lineColor: '{color}',
|
||||
lineStyle: {as_enum(style, LINE_STYLE)},
|
||||
}}
|
||||
)
|
||||
{chart.id}.series.attachPrimitive({self.id})
|
||||
''')
|
||||
|
||||
|
||||
class HorizontalLine(Drawing):
|
||||
def __init__(self, chart, price, color, width, style, text, axis_label_visible, func):
|
||||
super().__init__(chart, color, width, style, func)
|
||||
self.price = price
|
||||
self.run_script(f'''
|
||||
|
||||
{self.id} = new HorizontalLine(
|
||||
{{price: {price}}},
|
||||
{{
|
||||
lineColor: '{color}',
|
||||
lineStyle: {as_enum(style, LINE_STYLE)},
|
||||
}},
|
||||
callbackName={f"'{self.id}'" if func else 'null'}
|
||||
)
|
||||
{chart.id}.series.attachPrimitive({self.id})
|
||||
''')
|
||||
if not func:
|
||||
return
|
||||
|
||||
def wrapper(p):
|
||||
self.price = float(p)
|
||||
func(chart, self)
|
||||
|
||||
async def wrapper_async(p):
|
||||
self.price = float(p)
|
||||
await func(chart, self)
|
||||
|
||||
self.win.handlers[self.id] = wrapper_async if asyncio.iscoroutinefunction(func) else wrapper
|
||||
self.run_script(f'{chart.id}.toolBox?.addNewDrawing({self.id})')
|
||||
|
||||
def update(self, price: float):
|
||||
"""
|
||||
Moves the horizontal line to the given price.
|
||||
"""
|
||||
self.run_script(f'{self.id}.updatePoints({{price: {price}}})')
|
||||
# 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}")')
|
||||
|
||||
|
||||
|
||||
class VerticalLine(Drawing):
|
||||
def __init__(self, chart, time, color, width, style, text, axis_label_visible, func):
|
||||
super().__init__(chart, color, width, style, func)
|
||||
self.time = time
|
||||
self.run_script(f'''
|
||||
|
||||
{self.id} = new HorizontalLine(
|
||||
{{time: {time}}},
|
||||
{{
|
||||
lineColor: '{color}',
|
||||
lineStyle: {as_enum(style, LINE_STYLE)},
|
||||
}},
|
||||
callbackName={f"'{self.id}'" if func else 'null'}
|
||||
)
|
||||
{chart.id}.series.attachPrimitive({self.id})
|
||||
''')
|
||||
|
||||
def update(self, time: TIME):
|
||||
self.run_script(f'{self.id}.updatePoints({{time: {time}}})')
|
||||
# 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}")')
|
||||
|
||||
|
||||
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)'):
|
||||
self._chart = series._chart
|
||||
super().__init__(self._chart.win)
|
||||
start_time, end_time = pd.to_datetime(start_time), pd.to_datetime(end_time)
|
||||
self.run_script(f'''
|
||||
{self.id} = {self._chart.id}.chart.addHistogramSeries({{
|
||||
color: '{color}',
|
||||
priceFormat: {{type: 'volume'}},
|
||||
priceScaleId: 'vertical_line',
|
||||
lastValueVisible: false,
|
||||
priceLineVisible: false,
|
||||
}})
|
||||
{self.id}.priceScale('').applyOptions({{
|
||||
scaleMargins: {{top: 0, bottom: 0}}
|
||||
}})
|
||||
''')
|
||||
if end_time is None:
|
||||
if isinstance(start_time, pd.DatetimeIndex):
|
||||
data = [{'time': time.timestamp(), 'value': 1} for time in start_time]
|
||||
else:
|
||||
data = [{'time': start_time.timestamp(), 'value': 1}]
|
||||
self.run_script(f'{self.id}.setData({data})')
|
||||
else:
|
||||
self.run_script(f'''
|
||||
{self.id}.setData(calculateTrendLine(
|
||||
{start_time.timestamp()}, 1, {end_time.timestamp()}, 1, {series.id}))
|
||||
''')
|
||||
|
||||
def delete(self):
|
||||
"""
|
||||
Irreversibly deletes the vertical span.
|
||||
"""
|
||||
self.run_script(f'{self._chart.id}.chart.removeSeries({self.id})')
|
||||
File diff suppressed because one or more lines are too long
@ -53,15 +53,6 @@ class Row(dict):
|
||||
self.run_script(f"{self._table.id}.deleteRow('{self.id}')")
|
||||
self._table.pop(self.id)
|
||||
|
||||
# # TODO this might be useful in abschart
|
||||
# def _call(self, method_name: str, *args):
|
||||
# new_args = []
|
||||
# for arg in args:
|
||||
# if isinstance(arg, str):
|
||||
# arg = f"'{arg}'"
|
||||
# new_args.append(arg)
|
||||
# self.run_script(f"{self._table.id}.{method_name}({', '.join(new_args)})")
|
||||
|
||||
|
||||
class Table(Pane, dict):
|
||||
VALUE = 'CELL__~__VALUE__~__PLACEHOLDER'
|
||||
|
||||
2
setup.py
2
setup.py
@ -10,7 +10,7 @@ setup(
|
||||
python_requires='>=3.8',
|
||||
install_requires=[
|
||||
'pandas',
|
||||
'pywebview>=4.3',
|
||||
'pywebview>=5.0.5',
|
||||
],
|
||||
package_data={
|
||||
'lightweight_charts': ['js/*.js'],
|
||||
|
||||
@ -3,7 +3,7 @@ import {
|
||||
} from 'lightweight-charts';
|
||||
|
||||
import { Point } from '../drawing/data-source';
|
||||
import { Drawing, InteractionState } from '../drawing/drawing';
|
||||
import { InteractionState } from '../drawing/drawing';
|
||||
import { DrawingOptions, defaultOptions } from '../drawing/options';
|
||||
import { BoxPaneView } from './pane-view';
|
||||
import { TwoPointDrawing } from '../drawing/two-point-drawing';
|
||||
@ -54,13 +54,13 @@ export class Box extends TwoPointDrawing {
|
||||
switch(state) {
|
||||
case InteractionState.NONE:
|
||||
document.body.style.cursor = "default";
|
||||
this.applyOptions({showCircles: false});
|
||||
this._hovered = false;
|
||||
this._unsubscribe("mousedown", this._handleMouseDownInteraction);
|
||||
break;
|
||||
|
||||
case InteractionState.HOVERING:
|
||||
document.body.style.cursor = "pointer";
|
||||
this.applyOptions({showCircles: true});
|
||||
this._hovered = true;
|
||||
this._unsubscribe("mouseup", this._handleMouseUpInteraction);
|
||||
this._subscribe("mousedown", this._handleMouseDownInteraction)
|
||||
this.chart.applyOptions({handleScroll: true});
|
||||
@ -82,19 +82,19 @@ export class Box extends TwoPointDrawing {
|
||||
|
||||
_onDrag(diff: any) {
|
||||
if (this._state == InteractionState.DRAGGING || this._state == InteractionState.DRAGGINGP1) {
|
||||
Drawing._addDiffToPoint(this._p1, diff.time, diff.logical, diff.price);
|
||||
this._addDiffToPoint(this.p1, diff.logical, diff.price);
|
||||
}
|
||||
if (this._state == InteractionState.DRAGGING || this._state == InteractionState.DRAGGINGP2) {
|
||||
Drawing._addDiffToPoint(this._p2, diff.time, diff.logical, diff.price);
|
||||
this._addDiffToPoint(this.p2, diff.logical, diff.price);
|
||||
}
|
||||
if (this._state != InteractionState.DRAGGING) {
|
||||
if (this._state == InteractionState.DRAGGINGP3) {
|
||||
Drawing._addDiffToPoint(this._p1, diff.time, diff.logical, 0);
|
||||
Drawing._addDiffToPoint(this._p2, 0, 0, diff.price);
|
||||
this._addDiffToPoint(this.p1, diff.logical, 0);
|
||||
this._addDiffToPoint(this.p2, 0, diff.price);
|
||||
}
|
||||
if (this._state == InteractionState.DRAGGINGP4) {
|
||||
Drawing._addDiffToPoint(this._p1, 0, 0, diff.price);
|
||||
Drawing._addDiffToPoint(this._p2, diff.time, diff.logical, 0);
|
||||
this._addDiffToPoint(this.p1, 0, diff.price);
|
||||
this._addDiffToPoint(this.p2, diff.logical, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,12 +2,13 @@ import { ViewPoint } from "../drawing/pane-view";
|
||||
import { CanvasRenderingTarget2D } from "fancy-canvas";
|
||||
import { TwoPointDrawingPaneRenderer } from "../drawing/pane-renderer";
|
||||
import { BoxOptions } from "./box";
|
||||
import { setLineStyle } from "../helpers/canvas-rendering";
|
||||
|
||||
export class BoxPaneRenderer extends TwoPointDrawingPaneRenderer {
|
||||
declare _options: BoxOptions;
|
||||
|
||||
constructor(p1: ViewPoint, p2: ViewPoint, text1: string, text2: string, options: BoxOptions) {
|
||||
super(p1, p2, text1, text2, options)
|
||||
constructor(p1: ViewPoint, p2: ViewPoint, options: BoxOptions, showCircles: boolean) {
|
||||
super(p1, p2, options, showCircles)
|
||||
}
|
||||
|
||||
draw(target: CanvasRenderingTarget2D) {
|
||||
@ -21,6 +22,7 @@ export class BoxPaneRenderer extends TwoPointDrawingPaneRenderer {
|
||||
|
||||
ctx.lineWidth = this._options.width;
|
||||
ctx.strokeStyle = this._options.lineColor;
|
||||
setLineStyle(ctx, this._options.lineStyle)
|
||||
ctx.fillStyle = this._options.fillColor;
|
||||
|
||||
const mainX = Math.min(scaled.x1, scaled.x2);
|
||||
@ -31,7 +33,7 @@ export class BoxPaneRenderer extends TwoPointDrawingPaneRenderer {
|
||||
ctx.strokeRect(mainX, mainY, width, height);
|
||||
ctx.fillRect(mainX, mainY, width, height);
|
||||
|
||||
if (!this._options.showCircles) return;
|
||||
if (!this._hovered) return;
|
||||
this._drawEndCircle(scope, mainX, mainY);
|
||||
this._drawEndCircle(scope, mainX+width, mainY);
|
||||
this._drawEndCircle(scope, mainX+width, mainY+height);
|
||||
|
||||
@ -11,9 +11,8 @@ export class BoxPaneView extends TwoPointDrawingPaneView {
|
||||
return new BoxPaneRenderer(
|
||||
this._p1,
|
||||
this._p2,
|
||||
'' + this._source._p1.price.toFixed(1),
|
||||
'' + this._source._p2.price.toFixed(1),
|
||||
this._source._options as BoxOptions,
|
||||
this._source.hovered,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,23 +1,15 @@
|
||||
import {
|
||||
IChartApi,
|
||||
ISeriesApi,
|
||||
Logical,
|
||||
SeriesOptionsMap,
|
||||
Time,
|
||||
} from 'lightweight-charts';
|
||||
|
||||
import { DrawingOptions } from './options';
|
||||
|
||||
export interface Point {
|
||||
time: Time | null;
|
||||
logical: Logical;
|
||||
price: number;
|
||||
}
|
||||
|
||||
export interface DrawingDataSource {
|
||||
chart: IChartApi;
|
||||
series: ISeriesApi<keyof SeriesOptionsMap>;
|
||||
options: DrawingOptions;
|
||||
p1: Point;
|
||||
p2: Point;
|
||||
export interface DiffPoint {
|
||||
logical: number;
|
||||
price: number;
|
||||
}
|
||||
|
||||
@ -5,6 +5,7 @@ import {
|
||||
SeriesType,
|
||||
} from 'lightweight-charts';
|
||||
import { Drawing } from './drawing';
|
||||
import { HorizontalLine } from '../horizontal-line/horizontal-line';
|
||||
|
||||
|
||||
export class DrawingTool {
|
||||
@ -69,9 +70,10 @@ export class DrawingTool {
|
||||
|
||||
if (this._activeDrawing == null) {
|
||||
if (this._drawingType == null) return;
|
||||
|
||||
// TODO this line wont work for horizontals ?
|
||||
this._activeDrawing = new this._drawingType(point, point);
|
||||
this._series.attachPrimitive(this._activeDrawing);
|
||||
if (this._drawingType == HorizontalLine) this._onClick(param);
|
||||
}
|
||||
else {
|
||||
this._drawings.push(this._activeDrawing);
|
||||
|
||||
@ -1,10 +1,14 @@
|
||||
import { ISeriesApi, MouseEventParams, SeriesType, Time, Logical } from 'lightweight-charts';
|
||||
import {
|
||||
ISeriesApi,
|
||||
Logical,
|
||||
MouseEventParams,
|
||||
SeriesType
|
||||
} from 'lightweight-charts';
|
||||
|
||||
import { PluginBase } from '../plugin-base';
|
||||
import { Point } from './data-source';
|
||||
import { DrawingPaneView } from './pane-view';
|
||||
import { DiffPoint, Point } from './data-source';
|
||||
import { DrawingOptions, defaultOptions } from './options';
|
||||
import { convertTime } from '../helpers/time';
|
||||
import { DrawingPaneView } from './pane-view';
|
||||
|
||||
export enum InteractionState {
|
||||
NONE,
|
||||
@ -16,17 +20,12 @@ export enum InteractionState {
|
||||
DRAGGINGP4,
|
||||
}
|
||||
|
||||
interface DiffPoint {
|
||||
time: number | null;
|
||||
logical: number;
|
||||
price: number;
|
||||
}
|
||||
|
||||
export abstract class Drawing extends PluginBase {
|
||||
_paneViews: DrawingPaneView[] = [];
|
||||
_options: DrawingOptions;
|
||||
|
||||
abstract _type: string;
|
||||
protected _points: (Point|null)[] = [];
|
||||
|
||||
protected _state: InteractionState = InteractionState.NONE;
|
||||
|
||||
@ -66,7 +65,13 @@ export abstract class Drawing extends PluginBase {
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
public abstract updatePoints(...points: (Point | null)[]): void;
|
||||
public updatePoints(...points: (Point | null)[]) {
|
||||
for (let i=0; i<this.points.length; i++) {
|
||||
if (points[i] == null) continue;
|
||||
this.points[i] = points[i] as Point;
|
||||
}
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
detach() {
|
||||
this._options.lineColor = 'transparent';
|
||||
@ -78,6 +83,10 @@ export abstract class Drawing extends PluginBase {
|
||||
|
||||
}
|
||||
|
||||
get points() {
|
||||
return this._points;
|
||||
}
|
||||
|
||||
protected _subscribe(name: keyof DocumentEventMap, listener: any) {
|
||||
document.body.addEventListener(name, listener);
|
||||
this._listeners.push({name: name, listener: listener});
|
||||
@ -92,20 +101,19 @@ export abstract class Drawing extends PluginBase {
|
||||
|
||||
_handleHoverInteraction(param: MouseEventParams) {
|
||||
this._latestHoverPoint = param.point;
|
||||
if (!Drawing._mouseIsDown) {
|
||||
if (Drawing._mouseIsDown) {
|
||||
this._handleDragInteraction(param);
|
||||
} else {
|
||||
if (this._mouseIsOverDrawing(param)) {
|
||||
if (this._state != InteractionState.NONE) return;
|
||||
this._moveToState(InteractionState.HOVERING);
|
||||
Drawing.hoveredObject = Drawing.lastHoveredObject = this;
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
if (this._state == InteractionState.NONE) return;
|
||||
this._moveToState(InteractionState.NONE);
|
||||
if (Drawing.hoveredObject === this) Drawing.hoveredObject = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
this._handleDragInteraction(param);
|
||||
}
|
||||
|
||||
public static _eventToPoint(param: MouseEventParams, series: ISeriesApi<SeriesType>) {
|
||||
@ -121,35 +129,27 @@ export abstract class Drawing extends PluginBase {
|
||||
|
||||
protected static _getDiff(p1: Point, p2: Point): DiffPoint {
|
||||
const diff: DiffPoint = {
|
||||
time: null,
|
||||
logical: p1.logical-p2.logical,
|
||||
price: p1.price-p2.price,
|
||||
}
|
||||
if (p1.time && p2.time) {
|
||||
diff.time = convertTime(p1.time)-convertTime(p2.time);
|
||||
}
|
||||
return diff;
|
||||
}
|
||||
|
||||
protected static _addDiffToPoint(point: Point, timeDiff: number | null, logicalDiff: number, priceDiff: number) {
|
||||
if (timeDiff != null && point.time != null) {
|
||||
point.time = (convertTime(point.time)+timeDiff)/1000 as Time;
|
||||
}
|
||||
else {
|
||||
point.time = null;
|
||||
}
|
||||
protected _addDiffToPoint(point: Point | null, logicalDiff: number, priceDiff: number) {
|
||||
if (!point) return;
|
||||
point.logical = point.logical + logicalDiff as Logical;
|
||||
point.price = point.price+priceDiff;
|
||||
point.time = this.series.dataByIndex(point.logical)?.time || null;
|
||||
}
|
||||
|
||||
protected _handleMouseDownInteraction = () => {
|
||||
if (Drawing._mouseIsDown) return;
|
||||
// if (Drawing._mouseIsDown) return;
|
||||
Drawing._mouseIsDown = true;
|
||||
this._onMouseDown();
|
||||
}
|
||||
|
||||
protected _handleMouseUpInteraction = () => {
|
||||
if (!Drawing._mouseIsDown) return;
|
||||
// if (!Drawing._mouseIsDown) return;
|
||||
Drawing._mouseIsDown = false;
|
||||
this._moveToState(InteractionState.HOVERING);
|
||||
}
|
||||
@ -174,12 +174,7 @@ export abstract class Drawing extends PluginBase {
|
||||
}
|
||||
|
||||
protected abstract _onMouseDown(): void;
|
||||
protected abstract _onDrag(diff: any): void; // TODO any?
|
||||
protected abstract _onDrag(diff: DiffPoint): void;
|
||||
protected abstract _moveToState(state: InteractionState): void;
|
||||
protected abstract _mouseIsOverDrawing(param: MouseEventParams): boolean;
|
||||
|
||||
// toJSON() {
|
||||
// const {series, chart, ...serialized} = this;
|
||||
// return serialized;
|
||||
// }
|
||||
}
|
||||
|
||||
@ -1,18 +1,14 @@
|
||||
import { LineStyle } from "lightweight-charts";
|
||||
|
||||
|
||||
export interface DrawingOptions {
|
||||
lineColor: string;
|
||||
lineStyle: LineStyle
|
||||
width: number;
|
||||
showLabels: boolean;
|
||||
showCircles: boolean,
|
||||
}
|
||||
|
||||
|
||||
export const defaultOptions: DrawingOptions = {
|
||||
lineColor: 'rgb(255, 255, 255)',
|
||||
lineColor: '#1E80F0',
|
||||
lineStyle: LineStyle.Solid,
|
||||
width: 4,
|
||||
showLabels: true,
|
||||
showCircles: false,
|
||||
};
|
||||
@ -17,15 +17,13 @@ export abstract class DrawingPaneRenderer implements ISeriesPrimitivePaneRendere
|
||||
export abstract class TwoPointDrawingPaneRenderer extends DrawingPaneRenderer {
|
||||
_p1: ViewPoint;
|
||||
_p2: ViewPoint;
|
||||
_text1: string;
|
||||
_text2: string;
|
||||
protected _hovered: boolean;
|
||||
|
||||
constructor(p1: ViewPoint, p2: ViewPoint, text1: string, text2: string, options: DrawingOptions) {
|
||||
constructor(p1: ViewPoint, p2: ViewPoint, options: DrawingOptions, hovered: boolean) {
|
||||
super(options);
|
||||
this._p1 = p1;
|
||||
this._p2 = p2;
|
||||
this._text1 = text1;
|
||||
this._text2 = text2;
|
||||
this._hovered = hovered;
|
||||
}
|
||||
|
||||
abstract draw(target: CanvasRenderingTarget2D): void;
|
||||
|
||||
@ -33,11 +33,12 @@ export abstract class TwoPointDrawingPaneView extends DrawingPaneView {
|
||||
}
|
||||
|
||||
update() {
|
||||
if (!this._source.p1 || !this._source.p2) return;
|
||||
const series = this._source.series;
|
||||
const y1 = series.priceToCoordinate(this._source._p1.price);
|
||||
const y2 = series.priceToCoordinate(this._source._p2.price);
|
||||
const x1 = this._getX(this._source._p1);
|
||||
const x2 = this._getX(this._source._p2);
|
||||
const y1 = series.priceToCoordinate(this._source.p1.price);
|
||||
const y2 = series.priceToCoordinate(this._source.p2.price);
|
||||
const x1 = this._getX(this._source.p1);
|
||||
const x2 = this._getX(this._source.p2);
|
||||
this._p1 = { x: x1, y: y1 };
|
||||
this._p2 = { x: x2, y: y2 };
|
||||
if (!x1 || !x2 || !y1 || !y2) return;
|
||||
@ -47,9 +48,6 @@ export abstract class TwoPointDrawingPaneView extends DrawingPaneView {
|
||||
|
||||
_getX(p: Point) {
|
||||
const timeScale = this._source.chart.timeScale();
|
||||
if (!p.time) {
|
||||
return timeScale.logicalToCoordinate(p.logical);
|
||||
}
|
||||
return timeScale.timeToCoordinate(p.time);
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,18 +5,18 @@ import { TwoPointDrawingPaneView } from './pane-view';
|
||||
|
||||
|
||||
export abstract class TwoPointDrawing extends Drawing {
|
||||
_p1: Point;
|
||||
_p2: Point;
|
||||
_paneViews: TwoPointDrawingPaneView[] = [];
|
||||
|
||||
protected _hovered: boolean = false;
|
||||
|
||||
constructor(
|
||||
p1: Point,
|
||||
p2: Point,
|
||||
options?: Partial<DrawingOptions>
|
||||
) {
|
||||
super()
|
||||
this._p1 = p1;
|
||||
this._p2 = p2;
|
||||
this.points.push(p1);
|
||||
this.points.push(p2);
|
||||
this._options = {
|
||||
...defaultOptions,
|
||||
...options,
|
||||
@ -31,9 +31,8 @@ export abstract class TwoPointDrawing extends Drawing {
|
||||
this.updatePoints(null, point);
|
||||
}
|
||||
|
||||
public updatePoints(...points: (Point|null)[]) {
|
||||
this._p1 = points[0] || this._p1;
|
||||
this._p2 = points[1] || this._p2;
|
||||
this.requestUpdate();
|
||||
}
|
||||
get p1() { return this.points[0]; }
|
||||
get p2() { return this.points[1]; }
|
||||
|
||||
get hovered() { return this._hovered; }
|
||||
}
|
||||
|
||||
@ -1,4 +1,8 @@
|
||||
import { Box } from "../box/box";
|
||||
import { HorizontalLine } from "../horizontal-line/horizontal-line";
|
||||
import { RayLine } from "../horizontal-line/ray-line";
|
||||
import { TrendLine } from "../trend-line/trend-line";
|
||||
import { VerticalLine } from "../vertical-line/vertical-line";
|
||||
import { Table } from "./table";
|
||||
|
||||
export interface GlobalParams extends Window {
|
||||
@ -10,7 +14,13 @@ export interface GlobalParams extends Window {
|
||||
cursor: string;
|
||||
Handler: any;
|
||||
Table: typeof Table;
|
||||
|
||||
HorizontalLine: typeof HorizontalLine;
|
||||
TrendLine: typeof TrendLine;
|
||||
Box: typeof Box;
|
||||
RayLine: typeof RayLine;
|
||||
VerticalLine: typeof VerticalLine;
|
||||
|
||||
}
|
||||
|
||||
interface paneStyle {
|
||||
@ -48,7 +58,12 @@ export function globalParamInit() {
|
||||
}
|
||||
window.cursor = 'default';
|
||||
window.Table = Table;
|
||||
|
||||
window.HorizontalLine = HorizontalLine;
|
||||
window.TrendLine = TrendLine;
|
||||
window.Box = Box;
|
||||
window.RayLine = RayLine;
|
||||
window.VerticalLine = VerticalLine;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -52,8 +52,14 @@ export class Handler {
|
||||
|
||||
public _seriesList: ISeriesApi<SeriesType>[] = [];
|
||||
|
||||
|
||||
constructor(chartId: string, innerWidth: number, innerHeight: number, position: string, autoSize: boolean) {
|
||||
// TODO make some subcharts in the vite dev window and mess with the CSS to see if you can not need the position param. also see if you can remove resizing each time the window resizes?
|
||||
constructor(
|
||||
chartId: string,
|
||||
innerWidth: number,
|
||||
innerHeight: number,
|
||||
position: string,
|
||||
autoSize: boolean
|
||||
) {
|
||||
this.reSize = this.reSize.bind(this)
|
||||
|
||||
this.id = chartId
|
||||
@ -224,8 +230,15 @@ export class Handler {
|
||||
return param.seriesData.get(series) || null;
|
||||
}
|
||||
|
||||
const setChildRange = (timeRange: LogicalRange | null) => { if(timeRange) childChart.chart.timeScale().setVisibleLogicalRange(timeRange); }
|
||||
const setParentRange = (timeRange: LogicalRange | null) => { if(timeRange) parentChart.chart.timeScale().setVisibleLogicalRange(timeRange); }
|
||||
const childTimeScale = childChart.chart.timeScale();
|
||||
const parentTimeScale = parentChart.chart.timeScale();
|
||||
|
||||
const setChildRange = (timeRange: LogicalRange | null) => {
|
||||
if(timeRange) childTimeScale.setVisibleLogicalRange(timeRange);
|
||||
}
|
||||
const setParentRange = (timeRange: LogicalRange | null) => {
|
||||
if(timeRange) parentTimeScale.setVisibleLogicalRange(timeRange);
|
||||
}
|
||||
|
||||
const setParentCrosshair = (param: MouseEventParams) => {
|
||||
crosshairHandler(parentChart, getPoint(childChart.series, param))
|
||||
@ -235,7 +248,14 @@ export class Handler {
|
||||
}
|
||||
|
||||
let selected = parentChart
|
||||
function addMouseOverListener(thisChart: Handler, otherChart: Handler, thisCrosshair: MouseEventHandler<Time>, otherCrosshair: MouseEventHandler<Time>, thisRange: LogicalRangeChangeEventHandler, otherRange: LogicalRangeChangeEventHandler) {
|
||||
function addMouseOverListener(
|
||||
thisChart: Handler,
|
||||
otherChart: Handler,
|
||||
thisCrosshair: MouseEventHandler<Time>,
|
||||
otherCrosshair: MouseEventHandler<Time>,
|
||||
thisRange: LogicalRangeChangeEventHandler,
|
||||
otherRange: LogicalRangeChangeEventHandler)
|
||||
{
|
||||
thisChart.wrapper.addEventListener('mouseover', () => {
|
||||
if (selected === thisChart) return
|
||||
selected = thisChart
|
||||
@ -246,10 +266,28 @@ export class Handler {
|
||||
thisChart.chart.timeScale().subscribeVisibleLogicalRangeChange(otherRange)
|
||||
})
|
||||
}
|
||||
addMouseOverListener(parentChart, childChart, setParentCrosshair, setChildCrosshair, setParentRange, setChildRange)
|
||||
addMouseOverListener(childChart, parentChart, setChildCrosshair, setParentCrosshair, setChildRange, setParentRange)
|
||||
addMouseOverListener(
|
||||
parentChart,
|
||||
childChart,
|
||||
setParentCrosshair,
|
||||
setChildCrosshair,
|
||||
setParentRange,
|
||||
setChildRange
|
||||
)
|
||||
addMouseOverListener(
|
||||
childChart,
|
||||
parentChart,
|
||||
setChildCrosshair,
|
||||
setParentCrosshair,
|
||||
setChildRange,
|
||||
setParentRange
|
||||
)
|
||||
|
||||
parentChart.chart.subscribeCrosshairMove(setChildCrosshair)
|
||||
|
||||
const parentRange = parentTimeScale.getVisibleLogicalRange()
|
||||
if (parentRange) childTimeScale.setVisibleLogicalRange(parentRange)
|
||||
|
||||
if (crosshairOnly) return;
|
||||
parentChart.chart.timeScale().subscribeVisibleLogicalRangeChange(setChildRange)
|
||||
}
|
||||
|
||||
@ -7,9 +7,9 @@ import { GlobalParams } from "./global-params";
|
||||
import { StylePicker } from "../context-menu/style-picker";
|
||||
import { ColorPicker } from "../context-menu/color-picker";
|
||||
import { IChartApi, ISeriesApi, SeriesType } from "lightweight-charts";
|
||||
import { TwoPointDrawing } from "../drawing/two-point-drawing";
|
||||
import { HorizontalLine } from "../horizontal-line/horizontal-line";
|
||||
import { RayLine } from "../horizontal-line/ray-line";
|
||||
import { VerticalLine } from "../vertical-line/vertical-line";
|
||||
|
||||
|
||||
interface Icon {
|
||||
@ -67,6 +67,7 @@ export class ToolBox {
|
||||
this.buttons.push(this._makeToolBoxElement(HorizontalLine, 'KeyH', ToolBox.HORZ_SVG));
|
||||
this.buttons.push(this._makeToolBoxElement(RayLine, 'KeyR', ToolBox.RAY_SVG));
|
||||
this.buttons.push(this._makeToolBoxElement(Box, 'KeyB', ToolBox.BOX_SVG));
|
||||
this.buttons.push(this._makeToolBoxElement(VerticalLine, 'KeyV', ToolBox.RAY_SVG));
|
||||
for (const button of this.buttons) {
|
||||
div.appendChild(button);
|
||||
}
|
||||
@ -122,7 +123,7 @@ export class ToolBox {
|
||||
this._drawingTool?.beginDrawing(this.activeIcon.type);
|
||||
}
|
||||
|
||||
removeActiveAndSave() {
|
||||
removeActiveAndSave = () => {
|
||||
window.setCursor('default');
|
||||
if (this.activeIcon) this.activeIcon.div.classList.remove('active-toolbox-button')
|
||||
this.activeIcon = null
|
||||
@ -168,39 +169,36 @@ export class ToolBox {
|
||||
this._drawingTool.clearDrawings();
|
||||
}
|
||||
|
||||
saveDrawings() {
|
||||
saveDrawings = () => {
|
||||
const drawingMeta = []
|
||||
for (const d of this._drawingTool.drawings) {
|
||||
if (d instanceof TwoPointDrawing) {
|
||||
drawingMeta.push({
|
||||
type: d._type,
|
||||
p1: d._p1,
|
||||
p2: d._p2,
|
||||
color: d._options.lineColor,
|
||||
style: d._options.lineStyle, // TODO should push all options, just dont have showcircles/ non public stuff as actual options
|
||||
// would also fix the instanceOf in loadDrawings
|
||||
})
|
||||
}
|
||||
// TODO else if d instanceof Drawing
|
||||
points: d.points,
|
||||
options: d._options
|
||||
});
|
||||
}
|
||||
const string = JSON.stringify(drawingMeta);
|
||||
window.callbackFunction(`save_drawings${this._handlerID}_~_${string}`)
|
||||
}
|
||||
|
||||
loadDrawings(drawings: any[]) { // TODO any?
|
||||
loadDrawings(drawings: any[]) { // TODO any
|
||||
drawings.forEach((d) => {
|
||||
const options = {
|
||||
lineColor: d.color,
|
||||
lineStyle: d.style,
|
||||
}
|
||||
switch (d.type) {
|
||||
case "Box":
|
||||
this._drawingTool.addNewDrawing(new Box(d.p1, d.p2, options));
|
||||
this._drawingTool.addNewDrawing(new Box(d.points[0], d.points[1], d.options));
|
||||
break;
|
||||
case "TrendLine":
|
||||
this._drawingTool.addNewDrawing(new TrendLine(d.p1, d.p2, options));
|
||||
this._drawingTool.addNewDrawing(new TrendLine(d.points[0], d.points[1], d.options));
|
||||
break;
|
||||
case "HorizontalLine":
|
||||
this._drawingTool.addNewDrawing(new HorizontalLine(d.points[0], d.options));
|
||||
break;
|
||||
case "RayLine":
|
||||
this._drawingTool.addNewDrawing(new RayLine(d.points[0], d.options));
|
||||
break;
|
||||
case "VerticalLine":
|
||||
this._drawingTool.addNewDrawing(new VerticalLine(d.points[0], d.options));
|
||||
break;
|
||||
// TODO case HorizontalLine
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
14
src/helpers/canvas-rendering.ts
Normal file
14
src/helpers/canvas-rendering.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { LineStyle } from "lightweight-charts";
|
||||
|
||||
export function setLineStyle(ctx: CanvasRenderingContext2D, style: LineStyle): void {
|
||||
const dashPatterns = {
|
||||
[LineStyle.Solid]: [],
|
||||
[LineStyle.Dotted]: [ctx.lineWidth, ctx.lineWidth],
|
||||
[LineStyle.Dashed]: [2 * ctx.lineWidth, 2 * ctx.lineWidth],
|
||||
[LineStyle.LargeDashed]: [6 * ctx.lineWidth, 6 * ctx.lineWidth],
|
||||
[LineStyle.SparseDotted]: [ctx.lineWidth, 4 * ctx.lineWidth],
|
||||
};
|
||||
|
||||
const dashPattern = dashPatterns[style];
|
||||
ctx.setLineDash(dashPattern);
|
||||
}
|
||||
@ -57,7 +57,7 @@ export class HorizontalLine extends Drawing {
|
||||
}
|
||||
|
||||
_onDrag(diff: any) {
|
||||
Drawing._addDiffToPoint(this._point, 0, 0, diff.price);
|
||||
this._addDiffToPoint(this._point, 0, diff.price);
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
|
||||
@ -2,6 +2,7 @@ import { CanvasRenderingTarget2D } from "fancy-canvas";
|
||||
import { DrawingOptions } from "../drawing/options";
|
||||
import { DrawingPaneRenderer } from "../drawing/pane-renderer";
|
||||
import { ViewPoint } from "../drawing/pane-view";
|
||||
import { setLineStyle } from "../helpers/canvas-rendering";
|
||||
|
||||
export class HorizontalLinePaneRenderer extends DrawingPaneRenderer {
|
||||
_point: ViewPoint = {x: null, y: null};
|
||||
@ -21,6 +22,7 @@ export class HorizontalLinePaneRenderer extends DrawingPaneRenderer {
|
||||
|
||||
ctx.lineWidth = this._options.width;
|
||||
ctx.strokeStyle = this._options.lineColor;
|
||||
setLineStyle(ctx, this._options.lineStyle);
|
||||
ctx.beginPath();
|
||||
|
||||
ctx.moveTo(scaledX, scaledY);
|
||||
|
||||
@ -16,7 +16,9 @@ export class HorizontalLinePaneView extends DrawingPaneView {
|
||||
const point = this._source._point;
|
||||
const timeScale = this._source.chart.timeScale()
|
||||
const series = this._source.series;
|
||||
this._point.x = point.time ? timeScale.timeToCoordinate(point.time) : null;
|
||||
if (this._source._type == "RayLine") {
|
||||
this._point.x = timeScale.logicalToCoordinate(point.logical);
|
||||
}
|
||||
this._point.y = series.priceToCoordinate(point.price);
|
||||
}
|
||||
|
||||
|
||||
@ -2,8 +2,7 @@ import {
|
||||
DeepPartial,
|
||||
MouseEventParams
|
||||
} from "lightweight-charts";
|
||||
import { Point } from "../drawing/data-source";
|
||||
import { Drawing } from "../drawing/drawing";
|
||||
import { DiffPoint, Point } from "../drawing/data-source";
|
||||
import { DrawingOptions } from "../drawing/options";
|
||||
import { HorizontalLine } from "./horizontal-line";
|
||||
|
||||
@ -20,8 +19,8 @@ export class RayLine extends HorizontalLine {
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
_onDrag(diff: any) {
|
||||
Drawing._addDiffToPoint(this._point, diff.time, diff.logical, diff.price);
|
||||
_onDrag(diff: DiffPoint) {
|
||||
this._addDiffToPoint(this._point, diff.logical, diff.price);
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
|
||||
@ -3,10 +3,11 @@ import { ViewPoint } from "./pane-view";
|
||||
import { CanvasRenderingTarget2D } from "fancy-canvas";
|
||||
import { TwoPointDrawingPaneRenderer } from "../drawing/pane-renderer";
|
||||
import { DrawingOptions } from "../drawing/options";
|
||||
import { setLineStyle } from "../helpers/canvas-rendering";
|
||||
|
||||
export class TrendLinePaneRenderer extends TwoPointDrawingPaneRenderer {
|
||||
constructor(p1: ViewPoint, p2: ViewPoint, text1: string, text2: string, options: DrawingOptions) {
|
||||
super(p1, p2, text1, text2, options);
|
||||
constructor(p1: ViewPoint, p2: ViewPoint, options: DrawingOptions, hovered: boolean) {
|
||||
super(p1, p2, options, hovered);
|
||||
}
|
||||
|
||||
draw(target: CanvasRenderingTarget2D) {
|
||||
@ -25,6 +26,7 @@ export class TrendLinePaneRenderer extends TwoPointDrawingPaneRenderer {
|
||||
|
||||
ctx.lineWidth = this._options.width;
|
||||
ctx.strokeStyle = this._options.lineColor;
|
||||
setLineStyle(ctx, this._options.lineStyle);
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(scaled.x1, scaled.y1);
|
||||
ctx.lineTo(scaled.x2, scaled.y2);
|
||||
@ -32,7 +34,7 @@ export class TrendLinePaneRenderer extends TwoPointDrawingPaneRenderer {
|
||||
// this._drawTextLabel(scope, this._text1, x1Scaled, y1Scaled, true);
|
||||
// this._drawTextLabel(scope, this._text2, x2Scaled, y2Scaled, false);
|
||||
|
||||
if (!this._options.showCircles) return;
|
||||
if (!this._hovered) return;
|
||||
this._drawEndCircle(scope, scaled.x1, scaled.y1);
|
||||
this._drawEndCircle(scope, scaled.x2, scaled.y2);
|
||||
});
|
||||
|
||||
@ -17,9 +17,8 @@ export class TrendLinePaneView extends TwoPointDrawingPaneView {
|
||||
return new TrendLinePaneRenderer(
|
||||
this._p1,
|
||||
this._p2,
|
||||
'' + this._source._p1.price.toFixed(1),
|
||||
'' + this._source._p2.price.toFixed(1),
|
||||
this._source._options
|
||||
this._source._options,
|
||||
this._source.hovered,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -5,7 +5,7 @@ import {
|
||||
|
||||
import { TrendLinePaneView } from './pane-view';
|
||||
import { Point } from '../drawing/data-source';
|
||||
import { Drawing, InteractionState } from '../drawing/drawing';
|
||||
import { InteractionState } from '../drawing/drawing';
|
||||
import { DrawingOptions } from '../drawing/options';
|
||||
import { TwoPointDrawing } from '../drawing/two-point-drawing';
|
||||
|
||||
@ -27,14 +27,14 @@ export class TrendLine extends TwoPointDrawing {
|
||||
|
||||
case InteractionState.NONE:
|
||||
document.body.style.cursor = "default";
|
||||
this._options.showCircles = false;
|
||||
this._hovered = false;
|
||||
this.requestUpdate();
|
||||
this._unsubscribe("mousedown", this._handleMouseDownInteraction);
|
||||
break;
|
||||
|
||||
case InteractionState.HOVERING:
|
||||
document.body.style.cursor = "pointer";
|
||||
this._options.showCircles = true;
|
||||
this._hovered = true;
|
||||
this.requestUpdate();
|
||||
this._subscribe("mousedown", this._handleMouseDownInteraction);
|
||||
this._unsubscribe("mouseup", this._handleMouseDownInteraction);
|
||||
@ -55,10 +55,10 @@ export class TrendLine extends TwoPointDrawing {
|
||||
|
||||
_onDrag(diff: any) {
|
||||
if (this._state == InteractionState.DRAGGING || this._state == InteractionState.DRAGGINGP1) {
|
||||
Drawing._addDiffToPoint(this._p1, diff.time, diff.logical, diff.price);
|
||||
this._addDiffToPoint(this.p1, diff.logical, diff.price);
|
||||
}
|
||||
if (this._state == InteractionState.DRAGGING || this._state == InteractionState.DRAGGINGP2) {
|
||||
Drawing._addDiffToPoint(this._p2, diff.time, diff.logical, diff.price);
|
||||
this._addDiffToPoint(this.p2, diff.logical, diff.price);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
32
src/vertical-line/pane-renderer.ts
Normal file
32
src/vertical-line/pane-renderer.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import { CanvasRenderingTarget2D } from "fancy-canvas";
|
||||
import { DrawingOptions } from "../drawing/options";
|
||||
import { DrawingPaneRenderer } from "../drawing/pane-renderer";
|
||||
import { ViewPoint } from "../drawing/pane-view";
|
||||
import { setLineStyle } from "../helpers/canvas-rendering";
|
||||
|
||||
export class VerticalLinePaneRenderer extends DrawingPaneRenderer {
|
||||
_point: ViewPoint = {x: null, y: null};
|
||||
|
||||
constructor(point: ViewPoint, options: DrawingOptions) {
|
||||
super(options);
|
||||
this._point = point;
|
||||
}
|
||||
|
||||
draw(target: CanvasRenderingTarget2D) {
|
||||
target.useBitmapCoordinateSpace(scope => {
|
||||
if (this._point.x == null) return;
|
||||
const ctx = scope.context;
|
||||
const scaledX = this._point.x * scope.horizontalPixelRatio;
|
||||
|
||||
ctx.lineWidth = this._options.width;
|
||||
ctx.strokeStyle = this._options.lineColor;
|
||||
setLineStyle(ctx, this._options.lineStyle);
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(scaledX, 0);
|
||||
ctx.lineTo(scaledX, scope.bitmapSize.height);
|
||||
ctx.stroke();
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
29
src/vertical-line/pane-view.ts
Normal file
29
src/vertical-line/pane-view.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import { VerticalLinePaneRenderer } from './pane-renderer';
|
||||
import { VerticalLine } from './vertical-line';
|
||||
import { DrawingPaneView, ViewPoint } from '../drawing/pane-view';
|
||||
|
||||
|
||||
export class VerticalLinePaneView extends DrawingPaneView {
|
||||
_source: VerticalLine;
|
||||
_point: ViewPoint = {x: null, y: null};
|
||||
|
||||
constructor(source: VerticalLine) {
|
||||
super(source);
|
||||
this._source = source;
|
||||
}
|
||||
|
||||
update() {
|
||||
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.y = series.priceToCoordinate(point.price);
|
||||
}
|
||||
|
||||
renderer() {
|
||||
return new VerticalLinePaneRenderer(
|
||||
this._point,
|
||||
this._source._options
|
||||
);
|
||||
}
|
||||
}
|
||||
89
src/vertical-line/vertical-line.ts
Normal file
89
src/vertical-line/vertical-line.ts
Normal file
@ -0,0 +1,89 @@
|
||||
import {
|
||||
DeepPartial,
|
||||
MouseEventParams
|
||||
} from "lightweight-charts";
|
||||
import { Point } from "../drawing/data-source";
|
||||
import { Drawing, InteractionState } from "../drawing/drawing";
|
||||
import { DrawingOptions } from "../drawing/options";
|
||||
import { VerticalLinePaneView } from "./pane-view";
|
||||
import { GlobalParams } from "../general/global-params";
|
||||
|
||||
|
||||
declare const window: GlobalParams;
|
||||
|
||||
export class VerticalLine extends Drawing {
|
||||
_type = 'VerticalLine';
|
||||
_paneViews: VerticalLinePaneView[];
|
||||
_point: Point;
|
||||
private _callbackName: string | null;
|
||||
|
||||
protected _startDragPoint: Point | null = null;
|
||||
|
||||
constructor(point: Point, options: DeepPartial<DrawingOptions>, callbackName=null) {
|
||||
super(options)
|
||||
this._point = point;
|
||||
this._paneViews = [new VerticalLinePaneView(this)];
|
||||
this._callbackName = callbackName;
|
||||
}
|
||||
|
||||
public updatePoints(...points: (Point | null)[]) {
|
||||
for (const p of points) if (p) this._point = p;
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
_moveToState(state: InteractionState) {
|
||||
switch(state) {
|
||||
case InteractionState.NONE:
|
||||
document.body.style.cursor = "default";
|
||||
this._unsubscribe("mousedown", this._handleMouseDownInteraction);
|
||||
break;
|
||||
|
||||
case InteractionState.HOVERING:
|
||||
document.body.style.cursor = "pointer";
|
||||
this._unsubscribe("mouseup", this._childHandleMouseUpInteraction);
|
||||
this._subscribe("mousedown", this._handleMouseDownInteraction)
|
||||
this.chart.applyOptions({handleScroll: true});
|
||||
break;
|
||||
|
||||
case InteractionState.DRAGGING:
|
||||
document.body.style.cursor = "grabbing";
|
||||
this._subscribe("mouseup", this._childHandleMouseUpInteraction);
|
||||
this.chart.applyOptions({handleScroll: false});
|
||||
break;
|
||||
}
|
||||
this._state = state;
|
||||
}
|
||||
|
||||
_onDrag(diff: any) {
|
||||
this._addDiffToPoint(this._point, diff.logical, 0);
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
_mouseIsOverDrawing(param: MouseEventParams, tolerance = 4) {
|
||||
if (!param.point) return false;
|
||||
const timeScale = this.chart.timeScale()
|
||||
let x;
|
||||
if (this._point.time) {
|
||||
x = timeScale.timeToCoordinate(this._point.time);
|
||||
}
|
||||
else {
|
||||
x = timeScale.logicalToCoordinate(this._point.logical);
|
||||
}
|
||||
if (!x) return false;
|
||||
return (Math.abs(x-param.point.x) < tolerance);
|
||||
}
|
||||
|
||||
protected _onMouseDown() {
|
||||
this._startDragPoint = null;
|
||||
const hoverPoint = this._latestHoverPoint;
|
||||
if (!hoverPoint) return;
|
||||
return this._moveToState(InteractionState.DRAGGING);
|
||||
}
|
||||
|
||||
protected _childHandleMouseUpInteraction = () => {
|
||||
this._handleMouseUpInteraction();
|
||||
if (!this._callbackName) return;
|
||||
console.log(window.callbackFunction);
|
||||
window.callbackFunction(`${this._callbackName}_~_${this._point.price.toFixed(8)}`);
|
||||
}
|
||||
}
|
||||
@ -2,33 +2,42 @@
|
||||
"SYM": [
|
||||
{
|
||||
"type": "Box",
|
||||
"p1": {
|
||||
"points":
|
||||
[
|
||||
{
|
||||
"time": 1675036800,
|
||||
"logical": 2928,
|
||||
"price": 130.25483664317744
|
||||
},
|
||||
"p2": {
|
||||
{
|
||||
"time": 1676332800,
|
||||
"logical": 2939,
|
||||
"price": 145.52389493914157
|
||||
},
|
||||
}
|
||||
],
|
||||
"options": {
|
||||
"color": "#FFF",
|
||||
"style": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "TrendLine",
|
||||
"p1": {
|
||||
"points": [
|
||||
{
|
||||
"time": 1669939200,
|
||||
"logical": 2890,
|
||||
"price": 196.12991672005123
|
||||
},
|
||||
"p2": {
|
||||
{
|
||||
"time": 1673222400,
|
||||
"logical": 2914,
|
||||
"price": 223.17796284433055
|
||||
},
|
||||
}
|
||||
],
|
||||
"options": {
|
||||
"color": "#FFF",
|
||||
"style": 0
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -2,9 +2,40 @@ import unittest
|
||||
import pandas as pd
|
||||
|
||||
from lightweight_charts import Chart
|
||||
from util import BARS, Tester
|
||||
|
||||
from time import sleep
|
||||
|
||||
|
||||
class TestToolBox(unittest.TestCase):
|
||||
class TestToolBox(Tester):
|
||||
def test_create_horizontal_line(self):
|
||||
self.chart.set(BARS)
|
||||
horz_line = self.chart.horizontal_line(200, width=4)
|
||||
self.chart.show()
|
||||
result = self.chart.win.run_script_and_get(f"{horz_line.id}._options");
|
||||
self.assertTrue(result)
|
||||
self.chart.exit()
|
||||
|
||||
def test_create_trend_line(self):
|
||||
self.chart.set(BARS)
|
||||
horz_line = self.chart.trend_line(BARS.iloc[-10]['date'], 180, BARS.iloc[-3]['date'], 190)
|
||||
self.chart.show()
|
||||
result = self.chart.win.run_script_and_get(f"{horz_line.id}._options");
|
||||
self.assertTrue(result)
|
||||
self.chart.exit()
|
||||
|
||||
def test_create_box(self):
|
||||
self.chart.set(BARS)
|
||||
horz_line = self.chart.box(BARS.iloc[-10]['date'], 180, BARS.iloc[-3]['date'], 190)
|
||||
self.chart.show()
|
||||
result = self.chart.win.run_script_and_get(f"{horz_line.id}._options");
|
||||
self.assertTrue(result)
|
||||
self.chart.exit()
|
||||
|
||||
def test_create_vertical_line(self):
|
||||
...
|
||||
|
||||
def test_create_vertical_span(self):
|
||||
...
|
||||
|
||||
|
||||
|
||||
@ -10,7 +10,7 @@ BARS = pd.read_csv('../examples/1_setting_data/ohlcv.csv')
|
||||
|
||||
class Tester(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.chart: Chart = Chart();
|
||||
self.chart: Chart = Chart(100, 100, 800, 100);
|
||||
|
||||
def tearDown(self) -> None:
|
||||
self.chart.exit()
|
||||
|
||||
Reference in New Issue
Block a user