implement drawing methods, fix horizontal line bug, continue refactor

This commit is contained in:
louisnw
2024-04-14 16:29:15 +01:00
parent 3ead45f858
commit 3fdd19e3ce
33 changed files with 732 additions and 365 deletions

View File

@ -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())

View File

@ -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:

View 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

View File

@ -52,16 +52,7 @@ class Row(dict):
def delete(self): def delete(self):
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'

View File

@ -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'],

View File

@ -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);
} }
} }
} }

View File

@ -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);

View File

@ -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,
); );
} }
} }

View File

@ -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;
} }

View File

@ -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);

View File

@ -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;
// }
} }

View File

@ -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,
};

View File

@ -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;

View File

@ -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);
} }
} }

View File

@ -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; }
}
} }

View File

@ -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;
} }

View File

@ -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)
} }

View File

@ -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
} }
}) })
} }

View 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);
}

View File

@ -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();
} }

View File

@ -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);

View File

@ -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);
} }

View File

@ -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();
} }

View File

@ -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);
}); });

View File

@ -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
); );
} }
} }

View File

@ -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);
} }
} }

View 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();
});
}
}

View 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
);
}
}

View 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)}`);
}
}

View File

@ -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
}
} }
] ]
} }

View File

@ -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__':

View File

@ -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()