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 .table import Table
|
||||||
from .toolbox import ToolBox
|
from .toolbox import ToolBox
|
||||||
|
from .drawings import HorizontalLine, TwoPointDrawing, VerticalSpan
|
||||||
from .topbar import TopBar
|
from .topbar import TopBar
|
||||||
from .util import (
|
from .util import (
|
||||||
IDGen, as_enum, jbool, Pane, Events, TIME, NUM, FLOAT,
|
Pane, Events, IDGen, as_enum, jbool, js_json, TIME, NUM, FLOAT,
|
||||||
LINE_STYLE, MARKER_POSITION, MARKER_SHAPE, CROSSHAIR_MODE, PRICE_SCALE_MODE, js_json,
|
LINE_STYLE, MARKER_POSITION, MARKER_SHAPE, CROSSHAIR_MODE,
|
||||||
marker_position, marker_shape, js_data,
|
PRICE_SCALE_MODE, marker_position, marker_shape, js_data,
|
||||||
)
|
)
|
||||||
|
|
||||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
@ -43,8 +44,16 @@ class Window:
|
|||||||
if self.loaded:
|
if self.loaded:
|
||||||
return
|
return
|
||||||
self.loaded = True
|
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):
|
def run_script(self, script: str, run_last: bool = False):
|
||||||
"""
|
"""
|
||||||
@ -54,53 +63,60 @@ class Window:
|
|||||||
raise AttributeError("script_func has not been set")
|
raise AttributeError("script_func has not been set")
|
||||||
if self.loaded:
|
if self.loaded:
|
||||||
self.script_func(script)
|
self.script_func(script)
|
||||||
return
|
elif run_last:
|
||||||
self.scripts.append(script) if not run_last else self.final_scripts.append(script)
|
self.final_scripts.append(script)
|
||||||
|
else:
|
||||||
|
self.scripts.append(script)
|
||||||
|
|
||||||
def run_script_and_get(self, script: str):
|
def run_script_and_get(self, script: str):
|
||||||
self.run_script(f'_~_~RETURN~_~_{script}')
|
self.run_script(f'_~_~RETURN~_~_{script}')
|
||||||
return self._return_q.get()
|
return self._return_q.get()
|
||||||
|
|
||||||
def create_table(
|
def create_table(
|
||||||
self,
|
self,
|
||||||
width: NUM,
|
width: NUM,
|
||||||
height: NUM,
|
height: NUM,
|
||||||
headings: tuple,
|
headings: tuple,
|
||||||
widths: Optional[tuple] = None,
|
widths: Optional[tuple] = None,
|
||||||
alignments: Optional[tuple] = None,
|
alignments: Optional[tuple] = None,
|
||||||
position: FLOAT = 'left',
|
position: FLOAT = 'left',
|
||||||
draggable: bool = False,
|
draggable: bool = False,
|
||||||
background_color: str = '#121417',
|
background_color: str = '#121417',
|
||||||
border_color: str = 'rgb(70, 70, 70)',
|
border_color: str = 'rgb(70, 70, 70)',
|
||||||
border_width: int = 1,
|
border_width: int = 1,
|
||||||
heading_text_colors: Optional[tuple] = None,
|
heading_text_colors: Optional[tuple] = None,
|
||||||
heading_background_colors: Optional[tuple] = None,
|
heading_background_colors: Optional[tuple] = None,
|
||||||
return_clicked_cells: bool = False,
|
return_clicked_cells: bool = False,
|
||||||
func: Optional[Callable] = None
|
func: Optional[Callable] = None
|
||||||
) -> 'Table':
|
) -> 'Table':
|
||||||
return Table(self, width, height, headings, widths, alignments, position, draggable,
|
return Table(*locals().values())
|
||||||
background_color, border_color, border_width, heading_text_colors,
|
|
||||||
heading_background_colors, return_clicked_cells, func)
|
|
||||||
|
|
||||||
def create_subchart(
|
def create_subchart(
|
||||||
self,
|
self,
|
||||||
position: FLOAT = 'left',
|
position: FLOAT = 'left',
|
||||||
width: float = 0.5,
|
width: float = 0.5,
|
||||||
height: float = 0.5,
|
height: float = 0.5,
|
||||||
sync_id: Optional[str] = None,
|
sync_id: Optional[str] = None,
|
||||||
scale_candles_only: bool = False,
|
scale_candles_only: bool = False,
|
||||||
sync_crosshairs_only: bool = False,
|
sync_crosshairs_only: bool = False,
|
||||||
toolbox: bool = False
|
toolbox: bool = False
|
||||||
) -> 'AbstractChart':
|
) -> '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:
|
if not sync_id:
|
||||||
return subchart
|
return subchart
|
||||||
self.run_script(f'''
|
self.run_script(f'''
|
||||||
Handler.syncCharts({subchart.id}, {sync_id}, {jbool(sync_crosshairs_only)})
|
Handler.syncCharts(
|
||||||
// TODO this should be in syncCharts
|
{subchart.id},
|
||||||
{subchart.id}.chart.timeScale().setVisibleLogicalRange(
|
{sync_id},
|
||||||
{sync_id}.chart.timeScale().getVisibleLogicalRange()
|
{jbool(sync_crosshairs_only)}
|
||||||
)
|
)
|
||||||
''', run_last=True)
|
''', run_last=True)
|
||||||
return subchart
|
return subchart
|
||||||
#TODO test func below with polygon and others
|
#TODO test func below with polygon and others
|
||||||
@ -377,95 +393,6 @@ class SeriesCommon(Pane):
|
|||||||
end_time = self._single_datetime_format(end_time) if end_time else None
|
end_time = self._single_datetime_format(end_time) if end_time else None
|
||||||
return VerticalSpan(self, start_time, end_time, color)
|
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):
|
class Line(SeriesCommon):
|
||||||
def __init__(self, chart, name, color, style, width, price_line, price_label, crosshair_marker=True):
|
def __init__(self, chart, name, color, style, width, price_line, price_label, crosshair_marker=True):
|
||||||
@ -494,20 +421,20 @@ class Line(SeriesCommon):
|
|||||||
)
|
)
|
||||||
null''')
|
null''')
|
||||||
|
|
||||||
def _set_trend(self, start_time, start_value, end_time, end_value, ray=False, round=False):
|
# def _set_trend(self, start_time, start_value, end_time, end_value, ray=False, round=False):
|
||||||
if round:
|
# if round:
|
||||||
start_time = self._single_datetime_format(start_time)
|
# start_time = self._single_datetime_format(start_time)
|
||||||
end_time = self._single_datetime_format(end_time)
|
# end_time = self._single_datetime_format(end_time)
|
||||||
else:
|
# else:
|
||||||
start_time, end_time = pd.to_datetime((start_time, end_time)).astype('int64') // 10 ** 9
|
# start_time, end_time = pd.to_datetime((start_time, end_time)).astype('int64') // 10 ** 9
|
||||||
|
|
||||||
self.run_script(f'''
|
# self.run_script(f'''
|
||||||
{self._chart.id}.chart.timeScale().applyOptions({{shiftVisibleRangeOnNewBar: false}})
|
# {self._chart.id}.chart.timeScale().applyOptions({{shiftVisibleRangeOnNewBar: false}})
|
||||||
{self.id}.series.setData(
|
# {self.id}.series.setData(
|
||||||
calculateTrendLine({start_time}, {start_value}, {end_time}, {end_value},
|
# calculateTrendLine({start_time}, {start_value}, {end_time}, {end_value},
|
||||||
{self._chart.id}, {jbool(ray)}))
|
# {self._chart.id}, {jbool(ray)}))
|
||||||
{self._chart.id}.chart.timeScale().applyOptions({{shiftVisibleRangeOnNewBar: true}})
|
# {self._chart.id}.chart.timeScale().applyOptions({{shiftVisibleRangeOnNewBar: true}})
|
||||||
''')
|
# ''')
|
||||||
|
|
||||||
def delete(self):
|
def delete(self):
|
||||||
"""
|
"""
|
||||||
@ -576,11 +503,11 @@ class Candlestick(SeriesCommon):
|
|||||||
|
|
||||||
# self.run_script(f'{self.id}.makeCandlestickSeries()')
|
# 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
|
Sets the initial data for the chart.\n
|
||||||
:param df: columns: date/time, open, high, low, close, volume (if volume enabled).
|
: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:
|
if df is None or df.empty:
|
||||||
self.run_script(f'{self.id}.series.setData([])')
|
self.run_script(f'{self.id}.series.setData([])')
|
||||||
@ -592,9 +519,8 @@ class Candlestick(SeriesCommon):
|
|||||||
self._last_bar = df.iloc[-1]
|
self._last_bar = df.iloc[-1]
|
||||||
|
|
||||||
self.run_script(f'{self.id}.series.setData({js_data(df)})')
|
self.run_script(f'{self.id}.series.setData({js_data(df)})')
|
||||||
# TODO are we not using renderdrawings then?
|
# TODO keep drawings doesnt do anything
|
||||||
# toolbox_action = 'clearDrawings' if not render_drawings else 'renderDrawings'
|
self.run_script(f"if ({self._chart.id}.toolBox) {self._chart.id}.toolBox.clearDrawings()")
|
||||||
# self.run_script(f"if ({self._chart.id}.toolBox) {self._chart.id}.toolBox.{toolbox_action}()")
|
|
||||||
if 'volume' not in df:
|
if 'volume' not in df:
|
||||||
return
|
return
|
||||||
volume = df.drop(columns=['open', 'high', 'low', 'close']).rename(columns={'volume': 'value'})
|
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}})
|
{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;
|
Updates the data from a bar;
|
||||||
if series['time'] is the same time as the last bar, the last bar will be overwritten.\n
|
if series['time'] is the same time as the last bar, the last bar will be overwritten.\n
|
||||||
@ -661,11 +587,21 @@ class Candlestick(SeriesCommon):
|
|||||||
self.update(bar, _from_tick=True)
|
self.update(bar, _from_tick=True)
|
||||||
|
|
||||||
def price_scale(
|
def price_scale(
|
||||||
self, auto_scale: bool = True, mode: PRICE_SCALE_MODE = 'normal', invert_scale: bool = False,
|
self,
|
||||||
align_labels: bool = True, scale_margin_top: float = 0.2, scale_margin_bottom: float = 0.2,
|
auto_scale: bool = True,
|
||||||
border_visible: bool = False, border_color: Optional[str] = None, text_color: Optional[str] = None,
|
mode: PRICE_SCALE_MODE = 'normal',
|
||||||
entire_text_only: bool = False, visible: bool = True, ticks_visible: bool = False, minimum_width: int = 0
|
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.run_script(f'''
|
||||||
{self.id}.series.priceScale().applyOptions({{
|
{self.id}.series.priceScale().applyOptions({{
|
||||||
autoScale: {jbool(auto_scale)},
|
autoScale: {jbool(auto_scale)},
|
||||||
@ -776,18 +712,41 @@ class AbstractChart(Candlestick, Pane):
|
|||||||
"""
|
"""
|
||||||
return self._lines.copy()
|
return self._lines.copy()
|
||||||
|
|
||||||
def trend_line(self, start_time: TIME, start_value: NUM, end_time: TIME, end_value: NUM,
|
def trend_line(
|
||||||
round: bool = False, color: str = '#1E80F0', width: int = 2,
|
self,
|
||||||
style: LINE_STYLE = 'solid',
|
start_time: TIME,
|
||||||
) -> Line:
|
start_value: NUM,
|
||||||
line = Line(self, '', color, style, width, False, False, False)
|
end_time: TIME,
|
||||||
line._set_trend(start_time, start_value, end_time, end_value, round=round)
|
end_value: NUM,
|
||||||
return line
|
round: bool = False,
|
||||||
|
color: str = '#1E80F0',
|
||||||
|
width: int = 2,
|
||||||
|
style: LINE_STYLE = 'solid',
|
||||||
|
) -> TwoPointDrawing:
|
||||||
|
return TwoPointDrawing("TrendLine", *locals().values())
|
||||||
|
|
||||||
def ray_line(self, start_time: TIME, value: NUM, round: bool = False,
|
def box(
|
||||||
color: str = '#1E80F0', width: int = 2,
|
self,
|
||||||
style: LINE_STYLE = 'solid'
|
start_time: TIME,
|
||||||
) -> Line:
|
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)
|
line = Line(self, '', color, style, width, False, False, False)
|
||||||
line._set_trend(start_time, value, start_time, value, ray=True, round=round)
|
line._set_trend(start_time, value, start_time, value, ray=True, round=round)
|
||||||
return line
|
return line
|
||||||
@ -851,11 +810,20 @@ class AbstractChart(Candlestick, Pane):
|
|||||||
}}
|
}}
|
||||||
}})""")
|
}})""")
|
||||||
|
|
||||||
def crosshair(self, mode: CROSSHAIR_MODE = 'normal', vert_visible: bool = True,
|
def crosshair(
|
||||||
vert_width: int = 1, vert_color: Optional[str] = None, vert_style: LINE_STYLE = 'large_dashed',
|
self,
|
||||||
vert_label_background_color: str = 'rgb(46, 46, 46)', horz_visible: bool = True,
|
mode: CROSSHAIR_MODE = 'normal',
|
||||||
horz_width: int = 1, horz_color: Optional[str] = None, horz_style: LINE_STYLE = 'large_dashed',
|
vert_visible: bool = True,
|
||||||
horz_label_background_color: str = 'rgb(55, 55, 55)'):
|
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.
|
Crosshair formatting for its vertical and horizontal axes.
|
||||||
"""
|
"""
|
||||||
@ -877,7 +845,8 @@ class AbstractChart(Candlestick, Pane):
|
|||||||
style: {as_enum(horz_style, LINE_STYLE)},
|
style: {as_enum(horz_style, LINE_STYLE)},
|
||||||
labelBackgroundColor: "{horz_label_background_color}"
|
labelBackgroundColor: "{horz_label_background_color}"
|
||||||
}}
|
}}
|
||||||
}}}})''')
|
}}
|
||||||
|
}})''')
|
||||||
|
|
||||||
def watermark(self, text: str, font_size: int = 44, color: str = 'rgba(180, 180, 200, 0.5)'):
|
def watermark(self, text: str, font_size: int = 44, color: str = 'rgba(180, 180, 200, 0.5)'):
|
||||||
"""
|
"""
|
||||||
@ -950,25 +919,25 @@ class AbstractChart(Candlestick, Pane):
|
|||||||
self.win.handlers[f'{modifier_key, keys}'] = func
|
self.win.handlers[f'{modifier_key, keys}'] = func
|
||||||
|
|
||||||
def create_table(
|
def create_table(
|
||||||
self,
|
self,
|
||||||
width: NUM,
|
width: NUM,
|
||||||
height: NUM,
|
height: NUM,
|
||||||
headings: tuple,
|
headings: tuple,
|
||||||
widths: Optional[tuple] = None,
|
widths: Optional[tuple] = None,
|
||||||
alignments: Optional[tuple] = None,
|
alignments: Optional[tuple] = None,
|
||||||
position: FLOAT = 'left',
|
position: FLOAT = 'left',
|
||||||
draggable: bool = False,
|
draggable: bool = False,
|
||||||
background_color: str = '#121417',
|
background_color: str = '#121417',
|
||||||
border_color: str = 'rgb(70, 70, 70)',
|
border_color: str = 'rgb(70, 70, 70)',
|
||||||
border_width: int = 1,
|
border_width: int = 1,
|
||||||
heading_text_colors: Optional[tuple] = None,
|
heading_text_colors: Optional[tuple] = None,
|
||||||
heading_background_colors: Optional[tuple] = None,
|
heading_background_colors: Optional[tuple] = None,
|
||||||
return_clicked_cells: bool = False,
|
return_clicked_cells: bool = False,
|
||||||
func: Optional[Callable] = None
|
func: Optional[Callable] = None
|
||||||
) -> Table:
|
) -> Table:
|
||||||
return self.win.create_table(width, height, headings, widths, alignments, position, draggable,
|
args = locals()
|
||||||
background_color, border_color, border_width, heading_text_colors,
|
del args['self']
|
||||||
heading_background_colors, return_clicked_cells, func)
|
return self.win.create_table(*args.values())
|
||||||
|
|
||||||
def screenshot(self) -> bytes:
|
def screenshot(self) -> bytes:
|
||||||
"""
|
"""
|
||||||
@ -984,5 +953,6 @@ class AbstractChart(Candlestick, Pane):
|
|||||||
toolbox: bool = False) -> 'AbstractChart':
|
toolbox: bool = False) -> 'AbstractChart':
|
||||||
if sync is True:
|
if sync is True:
|
||||||
sync = self.id
|
sync = self.id
|
||||||
return self.win.create_subchart(position, width, height, sync,
|
args = locals()
|
||||||
scale_candles_only, sync_crosshairs_only, toolbox)
|
del args['self']
|
||||||
|
return self.win.create_subchart(*args.values())
|
||||||
|
|||||||
@ -81,7 +81,6 @@ class PyWV:
|
|||||||
window.show()
|
window.show()
|
||||||
elif arg == 'hide':
|
elif arg == 'hide':
|
||||||
window.hide()
|
window.hide()
|
||||||
# TODO make sure setup.py requires latest pywebview now
|
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
if '_~_~RETURN~_~_' in arg:
|
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.run_script(f"{self._table.id}.deleteRow('{self.id}')")
|
||||||
self._table.pop(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):
|
class Table(Pane, dict):
|
||||||
VALUE = 'CELL__~__VALUE__~__PLACEHOLDER'
|
VALUE = 'CELL__~__VALUE__~__PLACEHOLDER'
|
||||||
|
|||||||
2
setup.py
2
setup.py
@ -10,7 +10,7 @@ setup(
|
|||||||
python_requires='>=3.8',
|
python_requires='>=3.8',
|
||||||
install_requires=[
|
install_requires=[
|
||||||
'pandas',
|
'pandas',
|
||||||
'pywebview>=4.3',
|
'pywebview>=5.0.5',
|
||||||
],
|
],
|
||||||
package_data={
|
package_data={
|
||||||
'lightweight_charts': ['js/*.js'],
|
'lightweight_charts': ['js/*.js'],
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import {
|
|||||||
} from 'lightweight-charts';
|
} from 'lightweight-charts';
|
||||||
|
|
||||||
import { Point } from '../drawing/data-source';
|
import { Point } from '../drawing/data-source';
|
||||||
import { Drawing, InteractionState } from '../drawing/drawing';
|
import { InteractionState } from '../drawing/drawing';
|
||||||
import { DrawingOptions, defaultOptions } from '../drawing/options';
|
import { DrawingOptions, defaultOptions } from '../drawing/options';
|
||||||
import { BoxPaneView } from './pane-view';
|
import { BoxPaneView } from './pane-view';
|
||||||
import { TwoPointDrawing } from '../drawing/two-point-drawing';
|
import { TwoPointDrawing } from '../drawing/two-point-drawing';
|
||||||
@ -54,13 +54,13 @@ export class Box extends TwoPointDrawing {
|
|||||||
switch(state) {
|
switch(state) {
|
||||||
case InteractionState.NONE:
|
case InteractionState.NONE:
|
||||||
document.body.style.cursor = "default";
|
document.body.style.cursor = "default";
|
||||||
this.applyOptions({showCircles: false});
|
this._hovered = false;
|
||||||
this._unsubscribe("mousedown", this._handleMouseDownInteraction);
|
this._unsubscribe("mousedown", this._handleMouseDownInteraction);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case InteractionState.HOVERING:
|
case InteractionState.HOVERING:
|
||||||
document.body.style.cursor = "pointer";
|
document.body.style.cursor = "pointer";
|
||||||
this.applyOptions({showCircles: true});
|
this._hovered = true;
|
||||||
this._unsubscribe("mouseup", this._handleMouseUpInteraction);
|
this._unsubscribe("mouseup", this._handleMouseUpInteraction);
|
||||||
this._subscribe("mousedown", this._handleMouseDownInteraction)
|
this._subscribe("mousedown", this._handleMouseDownInteraction)
|
||||||
this.chart.applyOptions({handleScroll: true});
|
this.chart.applyOptions({handleScroll: true});
|
||||||
@ -82,19 +82,19 @@ export class Box extends TwoPointDrawing {
|
|||||||
|
|
||||||
_onDrag(diff: any) {
|
_onDrag(diff: any) {
|
||||||
if (this._state == InteractionState.DRAGGING || this._state == InteractionState.DRAGGINGP1) {
|
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) {
|
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.DRAGGING) {
|
||||||
if (this._state == InteractionState.DRAGGINGP3) {
|
if (this._state == InteractionState.DRAGGINGP3) {
|
||||||
Drawing._addDiffToPoint(this._p1, diff.time, diff.logical, 0);
|
this._addDiffToPoint(this.p1, diff.logical, 0);
|
||||||
Drawing._addDiffToPoint(this._p2, 0, 0, diff.price);
|
this._addDiffToPoint(this.p2, 0, diff.price);
|
||||||
}
|
}
|
||||||
if (this._state == InteractionState.DRAGGINGP4) {
|
if (this._state == InteractionState.DRAGGINGP4) {
|
||||||
Drawing._addDiffToPoint(this._p1, 0, 0, diff.price);
|
this._addDiffToPoint(this.p1, 0, diff.price);
|
||||||
Drawing._addDiffToPoint(this._p2, diff.time, diff.logical, 0);
|
this._addDiffToPoint(this.p2, diff.logical, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,12 +2,13 @@ import { ViewPoint } from "../drawing/pane-view";
|
|||||||
import { CanvasRenderingTarget2D } from "fancy-canvas";
|
import { CanvasRenderingTarget2D } from "fancy-canvas";
|
||||||
import { TwoPointDrawingPaneRenderer } from "../drawing/pane-renderer";
|
import { TwoPointDrawingPaneRenderer } from "../drawing/pane-renderer";
|
||||||
import { BoxOptions } from "./box";
|
import { BoxOptions } from "./box";
|
||||||
|
import { setLineStyle } from "../helpers/canvas-rendering";
|
||||||
|
|
||||||
export class BoxPaneRenderer extends TwoPointDrawingPaneRenderer {
|
export class BoxPaneRenderer extends TwoPointDrawingPaneRenderer {
|
||||||
declare _options: BoxOptions;
|
declare _options: BoxOptions;
|
||||||
|
|
||||||
constructor(p1: ViewPoint, p2: ViewPoint, text1: string, text2: string, options: BoxOptions) {
|
constructor(p1: ViewPoint, p2: ViewPoint, options: BoxOptions, showCircles: boolean) {
|
||||||
super(p1, p2, text1, text2, options)
|
super(p1, p2, options, showCircles)
|
||||||
}
|
}
|
||||||
|
|
||||||
draw(target: CanvasRenderingTarget2D) {
|
draw(target: CanvasRenderingTarget2D) {
|
||||||
@ -21,6 +22,7 @@ export class BoxPaneRenderer extends TwoPointDrawingPaneRenderer {
|
|||||||
|
|
||||||
ctx.lineWidth = this._options.width;
|
ctx.lineWidth = this._options.width;
|
||||||
ctx.strokeStyle = this._options.lineColor;
|
ctx.strokeStyle = this._options.lineColor;
|
||||||
|
setLineStyle(ctx, this._options.lineStyle)
|
||||||
ctx.fillStyle = this._options.fillColor;
|
ctx.fillStyle = this._options.fillColor;
|
||||||
|
|
||||||
const mainX = Math.min(scaled.x1, scaled.x2);
|
const mainX = Math.min(scaled.x1, scaled.x2);
|
||||||
@ -31,7 +33,7 @@ export class BoxPaneRenderer extends TwoPointDrawingPaneRenderer {
|
|||||||
ctx.strokeRect(mainX, mainY, width, height);
|
ctx.strokeRect(mainX, mainY, width, height);
|
||||||
ctx.fillRect(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, mainY);
|
||||||
this._drawEndCircle(scope, mainX+width, mainY);
|
this._drawEndCircle(scope, mainX+width, mainY);
|
||||||
this._drawEndCircle(scope, mainX+width, mainY+height);
|
this._drawEndCircle(scope, mainX+width, mainY+height);
|
||||||
|
|||||||
@ -11,9 +11,8 @@ export class BoxPaneView extends TwoPointDrawingPaneView {
|
|||||||
return new BoxPaneRenderer(
|
return new BoxPaneRenderer(
|
||||||
this._p1,
|
this._p1,
|
||||||
this._p2,
|
this._p2,
|
||||||
'' + this._source._p1.price.toFixed(1),
|
|
||||||
'' + this._source._p2.price.toFixed(1),
|
|
||||||
this._source._options as BoxOptions,
|
this._source._options as BoxOptions,
|
||||||
|
this._source.hovered,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,23 +1,15 @@
|
|||||||
import {
|
import {
|
||||||
IChartApi,
|
|
||||||
ISeriesApi,
|
|
||||||
Logical,
|
Logical,
|
||||||
SeriesOptionsMap,
|
|
||||||
Time,
|
Time,
|
||||||
} from 'lightweight-charts';
|
} from 'lightweight-charts';
|
||||||
|
|
||||||
import { DrawingOptions } from './options';
|
|
||||||
|
|
||||||
export interface Point {
|
export interface Point {
|
||||||
time: Time | null;
|
time: Time | null;
|
||||||
logical: Logical;
|
logical: Logical;
|
||||||
price: number;
|
price: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DrawingDataSource {
|
export interface DiffPoint {
|
||||||
chart: IChartApi;
|
logical: number;
|
||||||
series: ISeriesApi<keyof SeriesOptionsMap>;
|
price: number;
|
||||||
options: DrawingOptions;
|
|
||||||
p1: Point;
|
|
||||||
p2: Point;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import {
|
|||||||
SeriesType,
|
SeriesType,
|
||||||
} from 'lightweight-charts';
|
} from 'lightweight-charts';
|
||||||
import { Drawing } from './drawing';
|
import { Drawing } from './drawing';
|
||||||
|
import { HorizontalLine } from '../horizontal-line/horizontal-line';
|
||||||
|
|
||||||
|
|
||||||
export class DrawingTool {
|
export class DrawingTool {
|
||||||
@ -69,9 +70,10 @@ export class DrawingTool {
|
|||||||
|
|
||||||
if (this._activeDrawing == null) {
|
if (this._activeDrawing == null) {
|
||||||
if (this._drawingType == null) return;
|
if (this._drawingType == null) return;
|
||||||
|
// TODO this line wont work for horizontals ?
|
||||||
this._activeDrawing = new this._drawingType(point, point);
|
this._activeDrawing = new this._drawingType(point, point);
|
||||||
this._series.attachPrimitive(this._activeDrawing);
|
this._series.attachPrimitive(this._activeDrawing);
|
||||||
|
if (this._drawingType == HorizontalLine) this._onClick(param);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
this._drawings.push(this._activeDrawing);
|
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 { PluginBase } from '../plugin-base';
|
||||||
import { Point } from './data-source';
|
import { DiffPoint, Point } from './data-source';
|
||||||
import { DrawingPaneView } from './pane-view';
|
|
||||||
import { DrawingOptions, defaultOptions } from './options';
|
import { DrawingOptions, defaultOptions } from './options';
|
||||||
import { convertTime } from '../helpers/time';
|
import { DrawingPaneView } from './pane-view';
|
||||||
|
|
||||||
export enum InteractionState {
|
export enum InteractionState {
|
||||||
NONE,
|
NONE,
|
||||||
@ -16,17 +20,12 @@ export enum InteractionState {
|
|||||||
DRAGGINGP4,
|
DRAGGINGP4,
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DiffPoint {
|
|
||||||
time: number | null;
|
|
||||||
logical: number;
|
|
||||||
price: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export abstract class Drawing extends PluginBase {
|
export abstract class Drawing extends PluginBase {
|
||||||
_paneViews: DrawingPaneView[] = [];
|
_paneViews: DrawingPaneView[] = [];
|
||||||
_options: DrawingOptions;
|
_options: DrawingOptions;
|
||||||
|
|
||||||
abstract _type: string;
|
abstract _type: string;
|
||||||
|
protected _points: (Point|null)[] = [];
|
||||||
|
|
||||||
protected _state: InteractionState = InteractionState.NONE;
|
protected _state: InteractionState = InteractionState.NONE;
|
||||||
|
|
||||||
@ -66,7 +65,13 @@ export abstract class Drawing extends PluginBase {
|
|||||||
this.requestUpdate();
|
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() {
|
detach() {
|
||||||
this._options.lineColor = 'transparent';
|
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) {
|
protected _subscribe(name: keyof DocumentEventMap, listener: any) {
|
||||||
document.body.addEventListener(name, listener);
|
document.body.addEventListener(name, listener);
|
||||||
this._listeners.push({name: name, listener: listener});
|
this._listeners.push({name: name, listener: listener});
|
||||||
@ -92,20 +101,19 @@ export abstract class Drawing extends PluginBase {
|
|||||||
|
|
||||||
_handleHoverInteraction(param: MouseEventParams) {
|
_handleHoverInteraction(param: MouseEventParams) {
|
||||||
this._latestHoverPoint = param.point;
|
this._latestHoverPoint = param.point;
|
||||||
if (!Drawing._mouseIsDown) {
|
if (Drawing._mouseIsDown) {
|
||||||
|
this._handleDragInteraction(param);
|
||||||
|
} else {
|
||||||
if (this._mouseIsOverDrawing(param)) {
|
if (this._mouseIsOverDrawing(param)) {
|
||||||
if (this._state != InteractionState.NONE) return;
|
if (this._state != InteractionState.NONE) return;
|
||||||
this._moveToState(InteractionState.HOVERING);
|
this._moveToState(InteractionState.HOVERING);
|
||||||
Drawing.hoveredObject = Drawing.lastHoveredObject = this;
|
Drawing.hoveredObject = Drawing.lastHoveredObject = this;
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
if (this._state == InteractionState.NONE) return;
|
if (this._state == InteractionState.NONE) return;
|
||||||
this._moveToState(InteractionState.NONE);
|
this._moveToState(InteractionState.NONE);
|
||||||
if (Drawing.hoveredObject === this) Drawing.hoveredObject = null;
|
if (Drawing.hoveredObject === this) Drawing.hoveredObject = null;
|
||||||
}
|
}
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
this._handleDragInteraction(param);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static _eventToPoint(param: MouseEventParams, series: ISeriesApi<SeriesType>) {
|
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 {
|
protected static _getDiff(p1: Point, p2: Point): DiffPoint {
|
||||||
const diff: DiffPoint = {
|
const diff: DiffPoint = {
|
||||||
time: null,
|
|
||||||
logical: p1.logical-p2.logical,
|
logical: p1.logical-p2.logical,
|
||||||
price: p1.price-p2.price,
|
price: p1.price-p2.price,
|
||||||
}
|
}
|
||||||
if (p1.time && p2.time) {
|
|
||||||
diff.time = convertTime(p1.time)-convertTime(p2.time);
|
|
||||||
}
|
|
||||||
return diff;
|
return diff;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected static _addDiffToPoint(point: Point, timeDiff: number | null, logicalDiff: number, priceDiff: number) {
|
protected _addDiffToPoint(point: Point | null, logicalDiff: number, priceDiff: number) {
|
||||||
if (timeDiff != null && point.time != null) {
|
if (!point) return;
|
||||||
point.time = (convertTime(point.time)+timeDiff)/1000 as Time;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
point.time = null;
|
|
||||||
}
|
|
||||||
point.logical = point.logical + logicalDiff as Logical;
|
point.logical = point.logical + logicalDiff as Logical;
|
||||||
point.price = point.price+priceDiff;
|
point.price = point.price+priceDiff;
|
||||||
|
point.time = this.series.dataByIndex(point.logical)?.time || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected _handleMouseDownInteraction = () => {
|
protected _handleMouseDownInteraction = () => {
|
||||||
if (Drawing._mouseIsDown) return;
|
// if (Drawing._mouseIsDown) return;
|
||||||
Drawing._mouseIsDown = true;
|
Drawing._mouseIsDown = true;
|
||||||
this._onMouseDown();
|
this._onMouseDown();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected _handleMouseUpInteraction = () => {
|
protected _handleMouseUpInteraction = () => {
|
||||||
if (!Drawing._mouseIsDown) return;
|
// if (!Drawing._mouseIsDown) return;
|
||||||
Drawing._mouseIsDown = false;
|
Drawing._mouseIsDown = false;
|
||||||
this._moveToState(InteractionState.HOVERING);
|
this._moveToState(InteractionState.HOVERING);
|
||||||
}
|
}
|
||||||
@ -174,12 +174,7 @@ export abstract class Drawing extends PluginBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected abstract _onMouseDown(): void;
|
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 _moveToState(state: InteractionState): void;
|
||||||
protected abstract _mouseIsOverDrawing(param: MouseEventParams): boolean;
|
protected abstract _mouseIsOverDrawing(param: MouseEventParams): boolean;
|
||||||
|
|
||||||
// toJSON() {
|
|
||||||
// const {series, chart, ...serialized} = this;
|
|
||||||
// return serialized;
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,18 +1,14 @@
|
|||||||
import { LineStyle } from "lightweight-charts";
|
import { LineStyle } from "lightweight-charts";
|
||||||
|
|
||||||
|
|
||||||
export interface DrawingOptions {
|
export interface DrawingOptions {
|
||||||
lineColor: string;
|
lineColor: string;
|
||||||
lineStyle: LineStyle
|
lineStyle: LineStyle
|
||||||
width: number;
|
width: number;
|
||||||
showLabels: boolean;
|
|
||||||
showCircles: boolean,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export const defaultOptions: DrawingOptions = {
|
export const defaultOptions: DrawingOptions = {
|
||||||
lineColor: 'rgb(255, 255, 255)',
|
lineColor: '#1E80F0',
|
||||||
lineStyle: LineStyle.Solid,
|
lineStyle: LineStyle.Solid,
|
||||||
width: 4,
|
width: 4,
|
||||||
showLabels: true,
|
|
||||||
showCircles: false,
|
|
||||||
};
|
};
|
||||||
@ -17,15 +17,13 @@ export abstract class DrawingPaneRenderer implements ISeriesPrimitivePaneRendere
|
|||||||
export abstract class TwoPointDrawingPaneRenderer extends DrawingPaneRenderer {
|
export abstract class TwoPointDrawingPaneRenderer extends DrawingPaneRenderer {
|
||||||
_p1: ViewPoint;
|
_p1: ViewPoint;
|
||||||
_p2: ViewPoint;
|
_p2: ViewPoint;
|
||||||
_text1: string;
|
protected _hovered: boolean;
|
||||||
_text2: string;
|
|
||||||
|
|
||||||
constructor(p1: ViewPoint, p2: ViewPoint, text1: string, text2: string, options: DrawingOptions) {
|
constructor(p1: ViewPoint, p2: ViewPoint, options: DrawingOptions, hovered: boolean) {
|
||||||
super(options);
|
super(options);
|
||||||
this._p1 = p1;
|
this._p1 = p1;
|
||||||
this._p2 = p2;
|
this._p2 = p2;
|
||||||
this._text1 = text1;
|
this._hovered = hovered;
|
||||||
this._text2 = text2;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract draw(target: CanvasRenderingTarget2D): void;
|
abstract draw(target: CanvasRenderingTarget2D): void;
|
||||||
|
|||||||
@ -33,11 +33,12 @@ export abstract class TwoPointDrawingPaneView extends DrawingPaneView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
update() {
|
update() {
|
||||||
|
if (!this._source.p1 || !this._source.p2) return;
|
||||||
const series = this._source.series;
|
const series = this._source.series;
|
||||||
const y1 = series.priceToCoordinate(this._source._p1.price);
|
const y1 = series.priceToCoordinate(this._source.p1.price);
|
||||||
const y2 = series.priceToCoordinate(this._source._p2.price);
|
const y2 = series.priceToCoordinate(this._source.p2.price);
|
||||||
const x1 = this._getX(this._source._p1);
|
const x1 = this._getX(this._source.p1);
|
||||||
const x2 = this._getX(this._source._p2);
|
const x2 = this._getX(this._source.p2);
|
||||||
this._p1 = { x: x1, y: y1 };
|
this._p1 = { x: x1, y: y1 };
|
||||||
this._p2 = { x: x2, y: y2 };
|
this._p2 = { x: x2, y: y2 };
|
||||||
if (!x1 || !x2 || !y1 || !y2) return;
|
if (!x1 || !x2 || !y1 || !y2) return;
|
||||||
@ -47,9 +48,6 @@ export abstract class TwoPointDrawingPaneView extends DrawingPaneView {
|
|||||||
|
|
||||||
_getX(p: Point) {
|
_getX(p: Point) {
|
||||||
const timeScale = this._source.chart.timeScale();
|
const timeScale = this._source.chart.timeScale();
|
||||||
if (!p.time) {
|
return timeScale.logicalToCoordinate(p.logical);
|
||||||
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 {
|
export abstract class TwoPointDrawing extends Drawing {
|
||||||
_p1: Point;
|
|
||||||
_p2: Point;
|
|
||||||
_paneViews: TwoPointDrawingPaneView[] = [];
|
_paneViews: TwoPointDrawingPaneView[] = [];
|
||||||
|
|
||||||
|
protected _hovered: boolean = false;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
p1: Point,
|
p1: Point,
|
||||||
p2: Point,
|
p2: Point,
|
||||||
options?: Partial<DrawingOptions>
|
options?: Partial<DrawingOptions>
|
||||||
) {
|
) {
|
||||||
super()
|
super()
|
||||||
this._p1 = p1;
|
this.points.push(p1);
|
||||||
this._p2 = p2;
|
this.points.push(p2);
|
||||||
this._options = {
|
this._options = {
|
||||||
...defaultOptions,
|
...defaultOptions,
|
||||||
...options,
|
...options,
|
||||||
@ -31,9 +31,8 @@ export abstract class TwoPointDrawing extends Drawing {
|
|||||||
this.updatePoints(null, point);
|
this.updatePoints(null, point);
|
||||||
}
|
}
|
||||||
|
|
||||||
public updatePoints(...points: (Point|null)[]) {
|
get p1() { return this.points[0]; }
|
||||||
this._p1 = points[0] || this._p1;
|
get p2() { return this.points[1]; }
|
||||||
this._p2 = points[1] || this._p2;
|
|
||||||
this.requestUpdate();
|
get hovered() { return this._hovered; }
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,8 @@
|
|||||||
|
import { Box } from "../box/box";
|
||||||
import { HorizontalLine } from "../horizontal-line/horizontal-line";
|
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";
|
import { Table } from "./table";
|
||||||
|
|
||||||
export interface GlobalParams extends Window {
|
export interface GlobalParams extends Window {
|
||||||
@ -10,7 +14,13 @@ export interface GlobalParams extends Window {
|
|||||||
cursor: string;
|
cursor: string;
|
||||||
Handler: any;
|
Handler: any;
|
||||||
Table: typeof Table;
|
Table: typeof Table;
|
||||||
|
|
||||||
HorizontalLine: typeof HorizontalLine;
|
HorizontalLine: typeof HorizontalLine;
|
||||||
|
TrendLine: typeof TrendLine;
|
||||||
|
Box: typeof Box;
|
||||||
|
RayLine: typeof RayLine;
|
||||||
|
VerticalLine: typeof VerticalLine;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface paneStyle {
|
interface paneStyle {
|
||||||
@ -48,7 +58,12 @@ export function globalParamInit() {
|
|||||||
}
|
}
|
||||||
window.cursor = 'default';
|
window.cursor = 'default';
|
||||||
window.Table = Table;
|
window.Table = Table;
|
||||||
|
|
||||||
window.HorizontalLine = HorizontalLine;
|
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>[] = [];
|
public _seriesList: ISeriesApi<SeriesType>[] = [];
|
||||||
|
|
||||||
|
// 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) {
|
constructor(
|
||||||
|
chartId: string,
|
||||||
|
innerWidth: number,
|
||||||
|
innerHeight: number,
|
||||||
|
position: string,
|
||||||
|
autoSize: boolean
|
||||||
|
) {
|
||||||
this.reSize = this.reSize.bind(this)
|
this.reSize = this.reSize.bind(this)
|
||||||
|
|
||||||
this.id = chartId
|
this.id = chartId
|
||||||
@ -224,8 +230,15 @@ export class Handler {
|
|||||||
return param.seriesData.get(series) || null;
|
return param.seriesData.get(series) || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const setChildRange = (timeRange: LogicalRange | null) => { if(timeRange) childChart.chart.timeScale().setVisibleLogicalRange(timeRange); }
|
const childTimeScale = childChart.chart.timeScale();
|
||||||
const setParentRange = (timeRange: LogicalRange | null) => { if(timeRange) parentChart.chart.timeScale().setVisibleLogicalRange(timeRange); }
|
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) => {
|
const setParentCrosshair = (param: MouseEventParams) => {
|
||||||
crosshairHandler(parentChart, getPoint(childChart.series, param))
|
crosshairHandler(parentChart, getPoint(childChart.series, param))
|
||||||
@ -235,7 +248,14 @@ export class Handler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let selected = parentChart
|
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', () => {
|
thisChart.wrapper.addEventListener('mouseover', () => {
|
||||||
if (selected === thisChart) return
|
if (selected === thisChart) return
|
||||||
selected = thisChart
|
selected = thisChart
|
||||||
@ -246,10 +266,28 @@ export class Handler {
|
|||||||
thisChart.chart.timeScale().subscribeVisibleLogicalRangeChange(otherRange)
|
thisChart.chart.timeScale().subscribeVisibleLogicalRangeChange(otherRange)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
addMouseOverListener(parentChart, childChart, setParentCrosshair, setChildCrosshair, setParentRange, setChildRange)
|
addMouseOverListener(
|
||||||
addMouseOverListener(childChart, parentChart, setChildCrosshair, setParentCrosshair, setChildRange, setParentRange)
|
parentChart,
|
||||||
|
childChart,
|
||||||
|
setParentCrosshair,
|
||||||
|
setChildCrosshair,
|
||||||
|
setParentRange,
|
||||||
|
setChildRange
|
||||||
|
)
|
||||||
|
addMouseOverListener(
|
||||||
|
childChart,
|
||||||
|
parentChart,
|
||||||
|
setChildCrosshair,
|
||||||
|
setParentCrosshair,
|
||||||
|
setChildRange,
|
||||||
|
setParentRange
|
||||||
|
)
|
||||||
|
|
||||||
parentChart.chart.subscribeCrosshairMove(setChildCrosshair)
|
parentChart.chart.subscribeCrosshairMove(setChildCrosshair)
|
||||||
|
|
||||||
|
const parentRange = parentTimeScale.getVisibleLogicalRange()
|
||||||
|
if (parentRange) childTimeScale.setVisibleLogicalRange(parentRange)
|
||||||
|
|
||||||
if (crosshairOnly) return;
|
if (crosshairOnly) return;
|
||||||
parentChart.chart.timeScale().subscribeVisibleLogicalRangeChange(setChildRange)
|
parentChart.chart.timeScale().subscribeVisibleLogicalRangeChange(setChildRange)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,9 +7,9 @@ import { GlobalParams } from "./global-params";
|
|||||||
import { StylePicker } from "../context-menu/style-picker";
|
import { StylePicker } from "../context-menu/style-picker";
|
||||||
import { ColorPicker } from "../context-menu/color-picker";
|
import { ColorPicker } from "../context-menu/color-picker";
|
||||||
import { IChartApi, ISeriesApi, SeriesType } from "lightweight-charts";
|
import { IChartApi, ISeriesApi, SeriesType } from "lightweight-charts";
|
||||||
import { TwoPointDrawing } from "../drawing/two-point-drawing";
|
|
||||||
import { HorizontalLine } from "../horizontal-line/horizontal-line";
|
import { HorizontalLine } from "../horizontal-line/horizontal-line";
|
||||||
import { RayLine } from "../horizontal-line/ray-line";
|
import { RayLine } from "../horizontal-line/ray-line";
|
||||||
|
import { VerticalLine } from "../vertical-line/vertical-line";
|
||||||
|
|
||||||
|
|
||||||
interface Icon {
|
interface Icon {
|
||||||
@ -67,6 +67,7 @@ export class ToolBox {
|
|||||||
this.buttons.push(this._makeToolBoxElement(HorizontalLine, 'KeyH', ToolBox.HORZ_SVG));
|
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(RayLine, 'KeyR', ToolBox.RAY_SVG));
|
||||||
this.buttons.push(this._makeToolBoxElement(Box, 'KeyB', ToolBox.BOX_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) {
|
for (const button of this.buttons) {
|
||||||
div.appendChild(button);
|
div.appendChild(button);
|
||||||
}
|
}
|
||||||
@ -122,7 +123,7 @@ export class ToolBox {
|
|||||||
this._drawingTool?.beginDrawing(this.activeIcon.type);
|
this._drawingTool?.beginDrawing(this.activeIcon.type);
|
||||||
}
|
}
|
||||||
|
|
||||||
removeActiveAndSave() {
|
removeActiveAndSave = () => {
|
||||||
window.setCursor('default');
|
window.setCursor('default');
|
||||||
if (this.activeIcon) this.activeIcon.div.classList.remove('active-toolbox-button')
|
if (this.activeIcon) this.activeIcon.div.classList.remove('active-toolbox-button')
|
||||||
this.activeIcon = null
|
this.activeIcon = null
|
||||||
@ -168,39 +169,36 @@ export class ToolBox {
|
|||||||
this._drawingTool.clearDrawings();
|
this._drawingTool.clearDrawings();
|
||||||
}
|
}
|
||||||
|
|
||||||
saveDrawings() {
|
saveDrawings = () => {
|
||||||
const drawingMeta = []
|
const drawingMeta = []
|
||||||
for (const d of this._drawingTool.drawings) {
|
for (const d of this._drawingTool.drawings) {
|
||||||
if (d instanceof TwoPointDrawing) {
|
drawingMeta.push({
|
||||||
drawingMeta.push({
|
points: d.points,
|
||||||
type: d._type,
|
options: d._options
|
||||||
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
|
|
||||||
}
|
}
|
||||||
const string = JSON.stringify(drawingMeta);
|
const string = JSON.stringify(drawingMeta);
|
||||||
window.callbackFunction(`save_drawings${this._handlerID}_~_${string}`)
|
window.callbackFunction(`save_drawings${this._handlerID}_~_${string}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
loadDrawings(drawings: any[]) { // TODO any?
|
loadDrawings(drawings: any[]) { // TODO any
|
||||||
drawings.forEach((d) => {
|
drawings.forEach((d) => {
|
||||||
const options = {
|
|
||||||
lineColor: d.color,
|
|
||||||
lineStyle: d.style,
|
|
||||||
}
|
|
||||||
switch (d.type) {
|
switch (d.type) {
|
||||||
case "Box":
|
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;
|
break;
|
||||||
case "TrendLine":
|
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;
|
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) {
|
_onDrag(diff: any) {
|
||||||
Drawing._addDiffToPoint(this._point, 0, 0, diff.price);
|
this._addDiffToPoint(this._point, 0, diff.price);
|
||||||
this.requestUpdate();
|
this.requestUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { CanvasRenderingTarget2D } from "fancy-canvas";
|
|||||||
import { DrawingOptions } from "../drawing/options";
|
import { DrawingOptions } from "../drawing/options";
|
||||||
import { DrawingPaneRenderer } from "../drawing/pane-renderer";
|
import { DrawingPaneRenderer } from "../drawing/pane-renderer";
|
||||||
import { ViewPoint } from "../drawing/pane-view";
|
import { ViewPoint } from "../drawing/pane-view";
|
||||||
|
import { setLineStyle } from "../helpers/canvas-rendering";
|
||||||
|
|
||||||
export class HorizontalLinePaneRenderer extends DrawingPaneRenderer {
|
export class HorizontalLinePaneRenderer extends DrawingPaneRenderer {
|
||||||
_point: ViewPoint = {x: null, y: null};
|
_point: ViewPoint = {x: null, y: null};
|
||||||
@ -21,6 +22,7 @@ export class HorizontalLinePaneRenderer extends DrawingPaneRenderer {
|
|||||||
|
|
||||||
ctx.lineWidth = this._options.width;
|
ctx.lineWidth = this._options.width;
|
||||||
ctx.strokeStyle = this._options.lineColor;
|
ctx.strokeStyle = this._options.lineColor;
|
||||||
|
setLineStyle(ctx, this._options.lineStyle);
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
|
|
||||||
ctx.moveTo(scaledX, scaledY);
|
ctx.moveTo(scaledX, scaledY);
|
||||||
|
|||||||
@ -16,7 +16,9 @@ export class HorizontalLinePaneView 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 = 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);
|
this._point.y = series.priceToCoordinate(point.price);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -2,8 +2,7 @@ import {
|
|||||||
DeepPartial,
|
DeepPartial,
|
||||||
MouseEventParams
|
MouseEventParams
|
||||||
} from "lightweight-charts";
|
} from "lightweight-charts";
|
||||||
import { Point } from "../drawing/data-source";
|
import { DiffPoint, Point } from "../drawing/data-source";
|
||||||
import { Drawing } from "../drawing/drawing";
|
|
||||||
import { DrawingOptions } from "../drawing/options";
|
import { DrawingOptions } from "../drawing/options";
|
||||||
import { HorizontalLine } from "./horizontal-line";
|
import { HorizontalLine } from "./horizontal-line";
|
||||||
|
|
||||||
@ -20,8 +19,8 @@ export class RayLine extends HorizontalLine {
|
|||||||
this.requestUpdate();
|
this.requestUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
_onDrag(diff: any) {
|
_onDrag(diff: DiffPoint) {
|
||||||
Drawing._addDiffToPoint(this._point, diff.time, diff.logical, diff.price);
|
this._addDiffToPoint(this._point, diff.logical, diff.price);
|
||||||
this.requestUpdate();
|
this.requestUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -3,10 +3,11 @@ import { ViewPoint } from "./pane-view";
|
|||||||
import { CanvasRenderingTarget2D } from "fancy-canvas";
|
import { CanvasRenderingTarget2D } from "fancy-canvas";
|
||||||
import { TwoPointDrawingPaneRenderer } from "../drawing/pane-renderer";
|
import { TwoPointDrawingPaneRenderer } from "../drawing/pane-renderer";
|
||||||
import { DrawingOptions } from "../drawing/options";
|
import { DrawingOptions } from "../drawing/options";
|
||||||
|
import { setLineStyle } from "../helpers/canvas-rendering";
|
||||||
|
|
||||||
export class TrendLinePaneRenderer extends TwoPointDrawingPaneRenderer {
|
export class TrendLinePaneRenderer extends TwoPointDrawingPaneRenderer {
|
||||||
constructor(p1: ViewPoint, p2: ViewPoint, text1: string, text2: string, options: DrawingOptions) {
|
constructor(p1: ViewPoint, p2: ViewPoint, options: DrawingOptions, hovered: boolean) {
|
||||||
super(p1, p2, text1, text2, options);
|
super(p1, p2, options, hovered);
|
||||||
}
|
}
|
||||||
|
|
||||||
draw(target: CanvasRenderingTarget2D) {
|
draw(target: CanvasRenderingTarget2D) {
|
||||||
@ -25,6 +26,7 @@ export class TrendLinePaneRenderer extends TwoPointDrawingPaneRenderer {
|
|||||||
|
|
||||||
ctx.lineWidth = this._options.width;
|
ctx.lineWidth = this._options.width;
|
||||||
ctx.strokeStyle = this._options.lineColor;
|
ctx.strokeStyle = this._options.lineColor;
|
||||||
|
setLineStyle(ctx, this._options.lineStyle);
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.moveTo(scaled.x1, scaled.y1);
|
ctx.moveTo(scaled.x1, scaled.y1);
|
||||||
ctx.lineTo(scaled.x2, scaled.y2);
|
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._text1, x1Scaled, y1Scaled, true);
|
||||||
// this._drawTextLabel(scope, this._text2, x2Scaled, y2Scaled, false);
|
// 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.x1, scaled.y1);
|
||||||
this._drawEndCircle(scope, scaled.x2, scaled.y2);
|
this._drawEndCircle(scope, scaled.x2, scaled.y2);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -17,9 +17,8 @@ export class TrendLinePaneView extends TwoPointDrawingPaneView {
|
|||||||
return new TrendLinePaneRenderer(
|
return new TrendLinePaneRenderer(
|
||||||
this._p1,
|
this._p1,
|
||||||
this._p2,
|
this._p2,
|
||||||
'' + this._source._p1.price.toFixed(1),
|
this._source._options,
|
||||||
'' + this._source._p2.price.toFixed(1),
|
this._source.hovered,
|
||||||
this._source._options
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -5,7 +5,7 @@ import {
|
|||||||
|
|
||||||
import { TrendLinePaneView } from './pane-view';
|
import { TrendLinePaneView } from './pane-view';
|
||||||
import { Point } from '../drawing/data-source';
|
import { Point } from '../drawing/data-source';
|
||||||
import { Drawing, InteractionState } from '../drawing/drawing';
|
import { InteractionState } from '../drawing/drawing';
|
||||||
import { DrawingOptions } from '../drawing/options';
|
import { DrawingOptions } from '../drawing/options';
|
||||||
import { TwoPointDrawing } from '../drawing/two-point-drawing';
|
import { TwoPointDrawing } from '../drawing/two-point-drawing';
|
||||||
|
|
||||||
@ -27,14 +27,14 @@ export class TrendLine extends TwoPointDrawing {
|
|||||||
|
|
||||||
case InteractionState.NONE:
|
case InteractionState.NONE:
|
||||||
document.body.style.cursor = "default";
|
document.body.style.cursor = "default";
|
||||||
this._options.showCircles = false;
|
this._hovered = false;
|
||||||
this.requestUpdate();
|
this.requestUpdate();
|
||||||
this._unsubscribe("mousedown", this._handleMouseDownInteraction);
|
this._unsubscribe("mousedown", this._handleMouseDownInteraction);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case InteractionState.HOVERING:
|
case InteractionState.HOVERING:
|
||||||
document.body.style.cursor = "pointer";
|
document.body.style.cursor = "pointer";
|
||||||
this._options.showCircles = true;
|
this._hovered = true;
|
||||||
this.requestUpdate();
|
this.requestUpdate();
|
||||||
this._subscribe("mousedown", this._handleMouseDownInteraction);
|
this._subscribe("mousedown", this._handleMouseDownInteraction);
|
||||||
this._unsubscribe("mouseup", this._handleMouseDownInteraction);
|
this._unsubscribe("mouseup", this._handleMouseDownInteraction);
|
||||||
@ -55,10 +55,10 @@ export class TrendLine extends TwoPointDrawing {
|
|||||||
|
|
||||||
_onDrag(diff: any) {
|
_onDrag(diff: any) {
|
||||||
if (this._state == InteractionState.DRAGGING || this._state == InteractionState.DRAGGINGP1) {
|
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) {
|
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": [
|
"SYM": [
|
||||||
{
|
{
|
||||||
"type": "Box",
|
"type": "Box",
|
||||||
"p1": {
|
"points":
|
||||||
"time": 1675036800,
|
[
|
||||||
"logical": 2928,
|
{
|
||||||
"price": 130.25483664317744
|
"time": 1675036800,
|
||||||
},
|
"logical": 2928,
|
||||||
"p2": {
|
"price": 130.25483664317744
|
||||||
"time": 1676332800,
|
},
|
||||||
"logical": 2939,
|
{
|
||||||
"price": 145.52389493914157
|
"time": 1676332800,
|
||||||
},
|
"logical": 2939,
|
||||||
"color": "#FFF",
|
"price": 145.52389493914157
|
||||||
"style": 0
|
}
|
||||||
|
],
|
||||||
|
"options": {
|
||||||
|
"color": "#FFF",
|
||||||
|
"style": 0
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "TrendLine",
|
"type": "TrendLine",
|
||||||
"p1": {
|
"points": [
|
||||||
"time": 1669939200,
|
{
|
||||||
"logical": 2890,
|
"time": 1669939200,
|
||||||
"price": 196.12991672005123
|
"logical": 2890,
|
||||||
},
|
"price": 196.12991672005123
|
||||||
"p2": {
|
},
|
||||||
"time": 1673222400,
|
{
|
||||||
"logical": 2914,
|
"time": 1673222400,
|
||||||
"price": 223.17796284433055
|
"logical": 2914,
|
||||||
},
|
"price": 223.17796284433055
|
||||||
"color": "#FFF",
|
}
|
||||||
"style": 0
|
],
|
||||||
|
"options": {
|
||||||
|
"color": "#FFF",
|
||||||
|
"style": 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@ -2,10 +2,41 @@ import unittest
|
|||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
from lightweight_charts import Chart
|
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):
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|||||||
@ -10,7 +10,7 @@ BARS = pd.read_csv('../examples/1_setting_data/ohlcv.csv')
|
|||||||
|
|
||||||
class Tester(unittest.TestCase):
|
class Tester(unittest.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.chart: Chart = Chart();
|
self.chart: Chart = Chart(100, 100, 800, 100);
|
||||||
|
|
||||||
def tearDown(self) -> None:
|
def tearDown(self) -> None:
|
||||||
self.chart.exit()
|
self.chart.exit()
|
||||||
|
|||||||
Reference in New Issue
Block a user