2.0 first commit
This commit is contained in:
@ -2,62 +2,32 @@ import asyncio
|
||||
import os
|
||||
from base64 import b64decode
|
||||
from datetime import datetime
|
||||
from typing import Union, Literal, List, Optional
|
||||
from typing import Callable, Union, Literal, List, Optional
|
||||
import pandas as pd
|
||||
|
||||
from .table import Table
|
||||
from .toolbox import ToolBox
|
||||
from .topbar import TopBar
|
||||
from .util import (
|
||||
IDGen, jbool, Pane, Events, TIME, NUM, FLOAT,
|
||||
LINE_STYLE, MARKER_POSITION, MARKER_SHAPE, CROSSHAIR_MODE, PRICE_SCALE_MODE,
|
||||
line_style, marker_position, marker_shape, crosshair_mode, price_scale_mode, js_data,
|
||||
IDGen, as_enum, jbool, Pane, Events, TIME, NUM, FLOAT,
|
||||
LINE_STYLE, MARKER_POSITION, MARKER_SHAPE, CROSSHAIR_MODE, PRICE_SCALE_MODE, js_json,
|
||||
marker_position, marker_shape, js_data,
|
||||
)
|
||||
|
||||
JS = {}
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
for file in ('pkg', 'funcs', 'callback', 'toolbox', 'table'):
|
||||
with open(os.path.join(current_dir, 'js', f'{file}.js'), 'r', encoding='utf-8') as f:
|
||||
JS[file] = f.read()
|
||||
|
||||
TEMPLATE = f"""
|
||||
<!DOCTYPE html>
|
||||
<html lang="">
|
||||
<head>
|
||||
<title>lightweight-charts-python</title>
|
||||
<script>{JS['pkg']}</script>
|
||||
<meta name="viewport" content ="width=device-width, initial-scale=1">
|
||||
<style>
|
||||
body {{
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu,
|
||||
Cantarell, "Helvetica Neue", sans-serif;
|
||||
}}
|
||||
#wrapper {{
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background-color: #000000;
|
||||
}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="wrapper"></div>
|
||||
<script>
|
||||
{JS['funcs']}
|
||||
{JS['table']}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
INDEX = os.path.join(current_dir, 'js', 'test.html')
|
||||
|
||||
|
||||
class Window:
|
||||
_id_gen = IDGen()
|
||||
handlers = {}
|
||||
|
||||
def __init__(self, script_func: callable = None, js_api_code: str = None, run_script: callable = None):
|
||||
def __init__(
|
||||
self,
|
||||
script_func: Optional[Callable] = None,
|
||||
js_api_code: Optional[str] = None,
|
||||
run_script: Optional[Callable] = None
|
||||
):
|
||||
self.loaded = False
|
||||
self.script_func = script_func
|
||||
self.scripts = []
|
||||
@ -80,58 +50,69 @@ class Window:
|
||||
"""
|
||||
For advanced users; evaluates JavaScript within the Webview.
|
||||
"""
|
||||
if self.script_func is None:
|
||||
raise AttributeError("script_func has not been set")
|
||||
if self.loaded:
|
||||
self.script_func(script)
|
||||
return
|
||||
self.scripts.append(script) if not run_last else self.final_scripts.append(script)
|
||||
|
||||
def run_script_and_get(self, script: str):
|
||||
self.run_script(f'_~_~RETURN~_~_{script}')
|
||||
return self._return_q.get()
|
||||
|
||||
def create_table(
|
||||
self, width: NUM, height: NUM, headings: tuple, widths: tuple = None,
|
||||
alignments: tuple = None, position: FLOAT = 'left', draggable: bool = False,
|
||||
background_color: str = '#121417', border_color: str = 'rgb(70, 70, 70)',
|
||||
border_width: int = 1, heading_text_colors: tuple = None,
|
||||
heading_background_colors: tuple = None, return_clicked_cells: bool = False,
|
||||
func: callable = None
|
||||
self,
|
||||
width: NUM,
|
||||
height: NUM,
|
||||
headings: tuple,
|
||||
widths: Optional[tuple] = None,
|
||||
alignments: Optional[tuple] = None,
|
||||
position: FLOAT = 'left',
|
||||
draggable: bool = False,
|
||||
background_color: str = '#121417',
|
||||
border_color: str = 'rgb(70, 70, 70)',
|
||||
border_width: int = 1,
|
||||
heading_text_colors: Optional[tuple] = None,
|
||||
heading_background_colors: Optional[tuple] = None,
|
||||
return_clicked_cells: bool = False,
|
||||
func: Optional[Callable] = None
|
||||
) -> 'Table':
|
||||
return Table(self, width, height, headings, widths, alignments, position, draggable,
|
||||
background_color, border_color, border_width, heading_text_colors,
|
||||
heading_background_colors, return_clicked_cells, func)
|
||||
|
||||
def create_subchart(self, position: FLOAT = 'left', width: float = 0.5, height: float = 0.5,
|
||||
sync_id: str = None, scale_candles_only: bool = False,
|
||||
sync_id: Optional[str] = None, scale_candles_only: bool = False,
|
||||
sync_crosshairs_only: bool = False, toolbox: bool = False
|
||||
) -> 'AbstractChart':
|
||||
subchart = AbstractChart(self, width, height, scale_candles_only, toolbox, position=position)
|
||||
if not sync_id:
|
||||
return subchart
|
||||
self.run_script(f'''
|
||||
syncCharts({subchart.id}, {sync_id}, {jbool(sync_crosshairs_only)})
|
||||
Handler.syncCharts({subchart.id}, {sync_id}, {jbool(sync_crosshairs_only)})
|
||||
{subchart.id}.chart.timeScale().setVisibleLogicalRange(
|
||||
{sync_id}.chart.timeScale().getVisibleLogicalRange()
|
||||
)
|
||||
''', run_last=True)
|
||||
return subchart
|
||||
|
||||
def style(self, background_color: str = '#0c0d0f', hover_background_color: str = '#3c434c',
|
||||
click_background_color: str = '#50565E',
|
||||
active_background_color: str = 'rgba(0, 122, 255, 0.7)',
|
||||
muted_background_color: str = 'rgba(0, 122, 255, 0.3)',
|
||||
border_color: str = '#3C434C', color: str = '#d8d9db', active_color: str = '#ececed'):
|
||||
self.run_script(f'''
|
||||
window.pane = {{
|
||||
backgroundColor: '{background_color}',
|
||||
hoverBackgroundColor: '{hover_background_color}',
|
||||
clickBackgroundColor: '{click_background_color}',
|
||||
activeBackgroundColor: '{active_background_color}',
|
||||
mutedBackgroundColor: '{muted_background_color}',
|
||||
borderColor: '{border_color}',
|
||||
color: '{color}',
|
||||
activeColor: '{active_color}',
|
||||
}}''')
|
||||
#TODO test func below with polygon and others
|
||||
def style(
|
||||
self,
|
||||
background_color: str = '#0c0d0f',
|
||||
hover_background_color: str = '#3c434c',
|
||||
click_background_color: str = '#50565E',
|
||||
active_background_color: str = 'rgba(0, 122, 255, 0.7)',
|
||||
muted_background_color: str = 'rgba(0, 122, 255, 0.3)',
|
||||
border_color: str = '#3C434C',
|
||||
color: str = '#d8d9db',
|
||||
active_color: str = '#ececed'
|
||||
):
|
||||
self.run_script(f'Handler.setRootStyles({js_json(locals())});')
|
||||
|
||||
|
||||
class SeriesCommon(Pane):
|
||||
def __init__(self, chart: 'AbstractChart', name: str = None):
|
||||
def __init__(self, chart: 'AbstractChart', name: str = ''):
|
||||
super().__init__(chart.win)
|
||||
self._chart = chart
|
||||
if hasattr(chart, '_interval'):
|
||||
@ -169,15 +150,13 @@ class SeriesCommon(Pane):
|
||||
self.offset = value
|
||||
break
|
||||
|
||||
self.run_script(
|
||||
f'if ({self.id}.toolBox) {self.id}.interval = {self._interval}'
|
||||
)
|
||||
|
||||
def _push_to_legend(self):
|
||||
self.run_script(f'''
|
||||
{self._chart.id}.lines.push({self.id})
|
||||
{self._chart.id}.legend.lines.push({self._chart.id}.legend.makeLineRow({self.id}))
|
||||
''')
|
||||
return
|
||||
#TODO you dont need this? All series should have a series row?
|
||||
# self.run_script(f'''
|
||||
# {self._chart.id}._seriesList.push({self.id})
|
||||
# {self._chart.id}.legend.lines.push({self._chart.id}.legend.makeSeriesRow({self.id}))
|
||||
# ''')
|
||||
|
||||
@staticmethod
|
||||
def _format_labels(data, labels, index, exclude_lowercase):
|
||||
@ -209,7 +188,7 @@ class SeriesCommon(Pane):
|
||||
series['time'] = self._single_datetime_format(series['time'])
|
||||
return series
|
||||
|
||||
def _single_datetime_format(self, arg):
|
||||
def _single_datetime_format(self, arg) -> float:
|
||||
if isinstance(arg, (str, int, float)) or not pd.api.types.is_datetime64_any_dtype(arg):
|
||||
try:
|
||||
arg = pd.to_datetime(arg, unit='ms')
|
||||
@ -218,7 +197,7 @@ class SeriesCommon(Pane):
|
||||
arg = self._interval * (arg.timestamp() // self._interval)+self.offset
|
||||
return arg
|
||||
|
||||
def set(self, df: pd.DataFrame = None, format_cols: bool = True):
|
||||
def set(self, df: Optional[pd.DataFrame] = None, format_cols: bool = True):
|
||||
if df is None or df.empty:
|
||||
self.run_script(f'{self.id}.series.setData([])')
|
||||
self.data = pd.DataFrame()
|
||||
@ -231,7 +210,7 @@ class SeriesCommon(Pane):
|
||||
df = df.rename(columns={self.name: 'value'})
|
||||
self.data = df.copy()
|
||||
self._last_bar = df.iloc[-1]
|
||||
self.run_script(f'{self.id}.data = {js_data(df)}; {self.id}.series.setData({self.id}.data); ')
|
||||
self.run_script(f'{self.id}.series.setData({js_data(df)}); ')
|
||||
|
||||
def update(self, series: pd.Series):
|
||||
series = self._series_datetime_format(series, exclude_lowercase=self.name)
|
||||
@ -241,14 +220,6 @@ class SeriesCommon(Pane):
|
||||
self.data.loc[self.data.index[-1]] = self._last_bar
|
||||
self.data = pd.concat([self.data, series.to_frame().T], ignore_index=True)
|
||||
self._last_bar = series
|
||||
bar = js_data(series)
|
||||
self.run_script(f'''
|
||||
if (stampToDate(lastBar({self.id}.data).time).getTime() === stampToDate({series['time']}).getTime()) {{
|
||||
{self.id}.data[{self.id}.data.length-1] = {bar}
|
||||
}}
|
||||
else {self.id}.data.push({bar})
|
||||
{self.id}.series.update({bar})
|
||||
''')
|
||||
self.run_script(f'{self.id}.series.update({js_data(series)})')
|
||||
|
||||
def marker_list(self, markers: list):
|
||||
@ -276,7 +247,7 @@ class SeriesCommon(Pane):
|
||||
""")
|
||||
return marker_ids
|
||||
|
||||
def marker(self, time: datetime = None, position: MARKER_POSITION = 'below',
|
||||
def marker(self, time: Optional[datetime] = None, position: MARKER_POSITION = 'below',
|
||||
shape: MARKER_SHAPE = 'arrow_up', color: str = '#2196F3', text: str = ''
|
||||
) -> str:
|
||||
"""
|
||||
@ -319,14 +290,14 @@ class SeriesCommon(Pane):
|
||||
|
||||
def horizontal_line(self, price: NUM, color: str = 'rgb(122, 146, 202)', width: int = 2,
|
||||
style: LINE_STYLE = 'solid', text: str = '', axis_label_visible: bool = True,
|
||||
func: callable = None
|
||||
func: Optional[Callable] = None
|
||||
) -> 'HorizontalLine':
|
||||
"""
|
||||
Creates a horizontal line at the given price.
|
||||
"""
|
||||
return HorizontalLine(self, price, color, width, style, text, axis_label_visible, func)
|
||||
|
||||
def remove_horizontal_line(self, price: NUM = None):
|
||||
def remove_horizontal_line(self, price: NUM):
|
||||
"""
|
||||
Removes a horizontal line at the given price.
|
||||
"""
|
||||
@ -363,10 +334,10 @@ class SeriesCommon(Pane):
|
||||
Sets the precision and minMove.\n
|
||||
:param precision: The number of decimal places.
|
||||
"""
|
||||
min_move = 1 / (10**precision)
|
||||
self.run_script(f'''
|
||||
{self.id}.precision = {precision}
|
||||
{self.id}.series.applyOptions({{
|
||||
priceFormat: {{precision: {precision}, minMove: {1 / (10 ** precision)}}}
|
||||
priceFormat: {{precision: {precision}, minMove: {min_move}}}
|
||||
}})''')
|
||||
self.num_decimals = precision
|
||||
|
||||
@ -382,8 +353,13 @@ class SeriesCommon(Pane):
|
||||
if ('volumeSeries' in {self.id}) {self.id}.volumeSeries.applyOptions({{visible: {jbool(arg)}}})
|
||||
''')
|
||||
|
||||
def vertical_span(self, start_time: Union[TIME, tuple, list], end_time: TIME = None,
|
||||
color: str = 'rgba(252, 219, 3, 0.2)', round: bool = False):
|
||||
def vertical_span(
|
||||
self,
|
||||
start_time: Union[TIME, tuple, list],
|
||||
end_time: Optional[TIME] = None,
|
||||
color: str = 'rgba(252, 219, 3, 0.2)',
|
||||
round: bool = False
|
||||
):
|
||||
"""
|
||||
Creates a vertical line or span across the chart.\n
|
||||
Start time and end time can be used together, or end_time can be
|
||||
@ -402,7 +378,7 @@ class HorizontalLine(Pane):
|
||||
self.run_script(f'''
|
||||
{self.id} = new HorizontalLine(
|
||||
{chart.id}, '{self.id}', {price}, '{color}', {width},
|
||||
{line_style(style)}, {jbool(axis_label_visible)}, '{text}'
|
||||
{as_enum(style, LINE_STYLE)}, {jbool(axis_label_visible)}, '{text}'
|
||||
)''')
|
||||
if not func:
|
||||
return
|
||||
@ -437,7 +413,7 @@ class HorizontalLine(Pane):
|
||||
|
||||
|
||||
class VerticalSpan(Pane):
|
||||
def __init__(self, series: 'SeriesCommon', start_time: Union[TIME, tuple, list], end_time: TIME = None,
|
||||
def __init__(self, series: 'SeriesCommon', start_time: Union[TIME, tuple, list], end_time: Optional[TIME] = None,
|
||||
color: str = 'rgba(252, 219, 3, 0.2)'):
|
||||
self._chart = series._chart
|
||||
super().__init__(self._chart.win)
|
||||
@ -475,31 +451,29 @@ class VerticalSpan(Pane):
|
||||
|
||||
class Line(SeriesCommon):
|
||||
def __init__(self, chart, name, color, style, width, price_line, price_label, crosshair_marker=True):
|
||||
|
||||
super().__init__(chart, name)
|
||||
self.color = color
|
||||
|
||||
self.run_script(f'''
|
||||
{self.id} = {{
|
||||
type: "line",
|
||||
series: {chart.id}.chart.addLineSeries({{
|
||||
color: '{color}',
|
||||
lineStyle: {line_style(style)},
|
||||
lineWidth: {width},
|
||||
lastValueVisible: {jbool(price_label)},
|
||||
priceLineVisible: {jbool(price_line)},
|
||||
crosshairMarkerVisible: {jbool(crosshair_marker)},
|
||||
{"""autoscaleInfoProvider: () => ({
|
||||
priceRange: {
|
||||
minValue: 1_000_000_000,
|
||||
maxValue: 0,
|
||||
},
|
||||
}),""" if chart._scale_candles_only else ''}
|
||||
}}),
|
||||
markers: [],
|
||||
horizontal_lines: [],
|
||||
name: '{name}',
|
||||
color: '{color}',
|
||||
precision: 2,
|
||||
}}
|
||||
{self.id} = {self._chart.id}.createLineSeries(
|
||||
"{name}",
|
||||
{{
|
||||
color: '{color}',
|
||||
lineStyle: {as_enum(style, LINE_STYLE)},
|
||||
lineWidth: {width},
|
||||
lastValueVisible: {jbool(price_label)},
|
||||
priceLineVisible: {jbool(price_line)},
|
||||
crosshairMarkerVisible: {jbool(crosshair_marker)},
|
||||
{"""autoscaleInfoProvider: () => ({
|
||||
priceRange: {
|
||||
minValue: 1_000_000_000,
|
||||
maxValue: 0,
|
||||
},
|
||||
}),
|
||||
""" if chart._scale_candles_only else ''}
|
||||
}}
|
||||
)
|
||||
null''')
|
||||
|
||||
def _set_trend(self, start_time, start_value, end_time, end_value, ray=False, round=False):
|
||||
@ -582,9 +556,9 @@ class Candlestick(SeriesCommon):
|
||||
|
||||
self.candle_data = pd.DataFrame()
|
||||
|
||||
self.run_script(f'{self.id}.makeCandlestickSeries()')
|
||||
# self.run_script(f'{self.id}.makeCandlestickSeries()')
|
||||
|
||||
def set(self, df: pd.DataFrame = None, render_drawings=False):
|
||||
def set(self, df: Optional[pd.DataFrame] = None, render_drawings=False):
|
||||
"""
|
||||
Sets the initial data for the chart.\n
|
||||
:param df: columns: date/time, open, high, low, close, volume (if volume enabled).
|
||||
@ -599,9 +573,10 @@ class Candlestick(SeriesCommon):
|
||||
self.candle_data = df.copy()
|
||||
self._last_bar = df.iloc[-1]
|
||||
|
||||
self.run_script(f'{self.id}.data = {js_data(df)}; {self.id}.series.setData({self.id}.data)')
|
||||
toolbox_action = 'clearDrawings' if not render_drawings else 'renderDrawings'
|
||||
self.run_script(f"if ('toolBox' in {self._chart.id}) {self._chart.id}.toolBox.{toolbox_action}()")
|
||||
self.run_script(f'{self.id}.series.setData({js_data(df)})')
|
||||
# TODO are we not using renderdrawings then?
|
||||
# toolbox_action = 'clearDrawings' if not render_drawings else 'renderDrawings'
|
||||
# self.run_script(f"if ({self._chart.id}.toolBox) {self._chart.id}.toolBox.{toolbox_action}()")
|
||||
if 'volume' not in df:
|
||||
return
|
||||
volume = df.drop(columns=['open', 'high', 'low', 'close']).rename(columns={'volume': 'value'})
|
||||
@ -630,18 +605,9 @@ class Candlestick(SeriesCommon):
|
||||
self.candle_data.loc[self.candle_data.index[-1]] = self._last_bar
|
||||
self.candle_data = pd.concat([self.candle_data, series.to_frame().T], ignore_index=True)
|
||||
self._chart.events.new_bar._emit(self)
|
||||
|
||||
self._last_bar = series
|
||||
bar = js_data(series)
|
||||
self.run_script(f'''
|
||||
if (stampToDate(lastBar({self.id}.data).time).getTime() === stampToDate({series['time']}).getTime()) {{
|
||||
{self.id}.data[{self.id}.data.length-1] = {bar}
|
||||
}}
|
||||
else {{
|
||||
{self.id}.data.push({bar})
|
||||
{f'{self.id}.toolBox.renderDrawings()' if render_drawings else ''}
|
||||
}}
|
||||
{self.id}.series.update({bar})
|
||||
''')
|
||||
self.run_script(f'{self.id}.series.update({js_data(series)})')
|
||||
if 'volume' not in series:
|
||||
return
|
||||
volume = series.drop(['open', 'high', 'low', 'close']).rename({'volume': 'value'})
|
||||
@ -656,10 +622,7 @@ class Candlestick(SeriesCommon):
|
||||
"""
|
||||
series = self._series_datetime_format(series)
|
||||
if series['time'] < self._last_bar['time']:
|
||||
raise ValueError(
|
||||
f'Trying to update tick of time "{pd.to_datetime(series["time"])}", '
|
||||
f'which occurs before the last bar time of '
|
||||
f'"{pd.to_datetime(self._last_bar["time"])}".')
|
||||
raise ValueError(f'Trying to update tick of time "{pd.to_datetime(series["time"])}", which occurs before the last bar time of "{pd.to_datetime(self._last_bar["time"])}".')
|
||||
bar = pd.Series(dtype='float64')
|
||||
if series['time'] == self._last_bar['time']:
|
||||
bar = self._last_bar
|
||||
@ -688,7 +651,7 @@ class Candlestick(SeriesCommon):
|
||||
self.run_script(f'''
|
||||
{self.id}.series.priceScale().applyOptions({{
|
||||
autoScale: {jbool(auto_scale)},
|
||||
mode: {price_scale_mode(mode)},
|
||||
mode: {as_enum(mode, PRICE_SCALE_MODE)},
|
||||
invertScale: {jbool(invert_scale)},
|
||||
alignLabels: {jbool(align_labels)},
|
||||
scaleMargins: {{top: {scale_margin_top}, bottom: {scale_margin_bottom}}},
|
||||
@ -703,29 +666,17 @@ class Candlestick(SeriesCommon):
|
||||
|
||||
def candle_style(
|
||||
self, up_color: str = 'rgba(39, 157, 130, 100)', down_color: str = 'rgba(200, 97, 100, 100)',
|
||||
wick_enabled: bool = True, border_enabled: bool = True, border_up_color: str = '',
|
||||
wick_visible: bool = True, border_visible: bool = True, border_up_color: str = '',
|
||||
border_down_color: str = '', wick_up_color: str = '', wick_down_color: str = ''):
|
||||
"""
|
||||
Candle styling for each of its parts.\n
|
||||
If only `up_color` and `down_color` are passed, they will color all parts of the candle.
|
||||
"""
|
||||
if border_enabled:
|
||||
border_up_color = border_up_color if border_up_color else up_color
|
||||
border_down_color = border_down_color if border_down_color else down_color
|
||||
if wick_enabled:
|
||||
wick_up_color = wick_up_color if wick_up_color else up_color
|
||||
wick_down_color = wick_down_color if wick_down_color else down_color
|
||||
self.run_script(f"""
|
||||
{self.id}.series.applyOptions({{
|
||||
upColor: "{up_color}",
|
||||
downColor: "{down_color}",
|
||||
wickVisible: {jbool(wick_enabled)},
|
||||
borderVisible: {jbool(border_enabled)},
|
||||
{f'borderUpColor: "{border_up_color}",' if border_enabled else ''}
|
||||
{f'borderDownColor: "{border_down_color}",' if border_enabled else ''}
|
||||
{f'wickUpColor: "{wick_up_color}",' if wick_enabled else ''}
|
||||
{f'wickDownColor: "{wick_down_color}",' if wick_enabled else ''}
|
||||
}})""")
|
||||
border_up_color = border_up_color if border_up_color else up_color
|
||||
border_down_color = border_down_color if border_down_color else down_color
|
||||
wick_up_color = wick_up_color if wick_up_color else up_color
|
||||
wick_down_color = wick_down_color if wick_down_color else down_color
|
||||
self.run_script(f"{self.id}.series.applyOptions({js_json(locals())})")
|
||||
|
||||
def volume_config(self, scale_margin_top: float = 0.8, scale_margin_bottom: float = 0.0,
|
||||
up_color='rgba(83,141,131,0.8)', down_color='rgba(200,127,130,0.8)'):
|
||||
@ -761,7 +712,7 @@ class AbstractChart(Candlestick, Pane):
|
||||
self.polygon: PolygonAPI = PolygonAPI(self)
|
||||
|
||||
self.run_script(
|
||||
f'{self.id} = new Chart("{self.id}", {width}, {height}, "{position}", {jbool(autosize)})')
|
||||
f'{self.id} = new Handler("{self.id}", {width}, {height}, "{position}", {jbool(autosize)})')
|
||||
|
||||
Candlestick.__init__(self, self)
|
||||
|
||||
@ -831,7 +782,7 @@ class AbstractChart(Candlestick, Pane):
|
||||
}})
|
||||
''')
|
||||
|
||||
def resize(self, width: float = None, height: float = None):
|
||||
def resize(self, width: Optional[float] = None, height: Optional[float] = None):
|
||||
"""
|
||||
Resizes the chart within the window.
|
||||
Dimensions should be given as a float between 0 and 1.
|
||||
@ -846,37 +797,20 @@ class AbstractChart(Candlestick, Pane):
|
||||
|
||||
def time_scale(self, right_offset: int = 0, min_bar_spacing: float = 0.5,
|
||||
visible: bool = True, time_visible: bool = True, seconds_visible: bool = False,
|
||||
border_visible: bool = True, border_color: str = None):
|
||||
border_visible: bool = True, border_color: Optional[str] = None):
|
||||
"""
|
||||
Options for the timescale of the chart.
|
||||
"""
|
||||
self.run_script(f'''
|
||||
{self.id}.chart.applyOptions({{
|
||||
timeScale: {{
|
||||
rightOffset: {right_offset},
|
||||
minBarSpacing: {min_bar_spacing},
|
||||
visible: {jbool(visible)},
|
||||
timeVisible: {jbool(time_visible)},
|
||||
secondsVisible: {jbool(seconds_visible)},
|
||||
borderVisible: {jbool(border_visible)},
|
||||
{f'borderColor: "{border_color}",' if border_color else ''}
|
||||
}}
|
||||
}})''')
|
||||
self.run_script(f'''{self.id}.chart.applyOptions({{timeScale: {js_json(locals())}}})''')
|
||||
|
||||
def layout(self, background_color: str = '#000000', text_color: str = None,
|
||||
font_size: int = None, font_family: str = None):
|
||||
def layout(self, background_color: str = '#000000', text_color: Optional[str] = None,
|
||||
font_size: Optional[int] = None, font_family: Optional[str] = None):
|
||||
"""
|
||||
Global layout options for the chart.
|
||||
"""
|
||||
self.run_script(f"""
|
||||
document.getElementById('wrapper').style.backgroundColor = '{background_color}'
|
||||
{self.id}.chart.applyOptions({{
|
||||
layout: {{
|
||||
background: {{color: "{background_color}"}},
|
||||
{f'textColor: "{text_color}",' if text_color else ''}
|
||||
{f'fontSize: {font_size},' if font_size else ''}
|
||||
{f'fontFamily: "{font_family}",' if font_family else ''}
|
||||
}}}})""")
|
||||
document.getElementById('container').style.backgroundColor = '{background_color}'
|
||||
{self.id}.chart.applyOptions({{ layout: {js_json(locals())} }})""")
|
||||
|
||||
def grid(self, vert_enabled: bool = True, horz_enabled: bool = True,
|
||||
color: str = 'rgba(29, 30, 38, 5)', style: LINE_STYLE = 'solid'):
|
||||
@ -889,20 +823,20 @@ class AbstractChart(Candlestick, Pane):
|
||||
vertLines: {{
|
||||
visible: {jbool(vert_enabled)},
|
||||
color: "{color}",
|
||||
style: {line_style(style)},
|
||||
style: {as_enum(style, LINE_STYLE)},
|
||||
}},
|
||||
horzLines: {{
|
||||
visible: {jbool(horz_enabled)},
|
||||
color: "{color}",
|
||||
style: {line_style(style)},
|
||||
style: {as_enum(style, LINE_STYLE)},
|
||||
}},
|
||||
}}
|
||||
}})""")
|
||||
|
||||
def crosshair(self, mode: CROSSHAIR_MODE = 'normal', vert_visible: bool = True,
|
||||
vert_width: int = 1, vert_color: str = None, vert_style: LINE_STYLE = 'large_dashed',
|
||||
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: str = None, horz_style: LINE_STYLE = 'large_dashed',
|
||||
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.
|
||||
@ -910,19 +844,19 @@ class AbstractChart(Candlestick, Pane):
|
||||
self.run_script(f'''
|
||||
{self.id}.chart.applyOptions({{
|
||||
crosshair: {{
|
||||
mode: {crosshair_mode(mode)},
|
||||
mode: {as_enum(mode, CROSSHAIR_MODE)},
|
||||
vertLine: {{
|
||||
visible: {jbool(vert_visible)},
|
||||
width: {vert_width},
|
||||
{f'color: "{vert_color}",' if vert_color else ''}
|
||||
style: {line_style(vert_style)},
|
||||
style: {as_enum(vert_style, LINE_STYLE)},
|
||||
labelBackgroundColor: "{vert_label_background_color}"
|
||||
}},
|
||||
horzLine: {{
|
||||
visible: {jbool(horz_visible)},
|
||||
width: {horz_width},
|
||||
{f'color: "{horz_color}",' if horz_color else ''}
|
||||
style: {line_style(horz_style)},
|
||||
style: {as_enum(horz_style, LINE_STYLE)},
|
||||
labelBackgroundColor: "{horz_label_background_color}"
|
||||
}}
|
||||
}}}})''')
|
||||
@ -935,11 +869,9 @@ class AbstractChart(Candlestick, Pane):
|
||||
{self.id}.chart.applyOptions({{
|
||||
watermark: {{
|
||||
visible: true,
|
||||
fontSize: {font_size},
|
||||
horzAlign: 'center',
|
||||
vertAlign: 'center',
|
||||
color: '{color}',
|
||||
text: '{text}',
|
||||
...{js_json(locals())}
|
||||
}}
|
||||
}})''')
|
||||
|
||||
@ -975,7 +907,7 @@ class AbstractChart(Candlestick, Pane):
|
||||
self.run_script(f"{self.id}.spinner.style.display = '{'block' if visible else 'none'}'")
|
||||
|
||||
def hotkey(self, modifier_key: Literal['ctrl', 'alt', 'shift', 'meta', None],
|
||||
keys: Union[str, tuple, int], func: callable):
|
||||
keys: Union[str, tuple, int], func: Callable):
|
||||
if not isinstance(keys, tuple):
|
||||
keys = (keys,)
|
||||
for key in keys:
|
||||
@ -1000,12 +932,21 @@ class AbstractChart(Candlestick, Pane):
|
||||
self.win.handlers[f'{modifier_key, keys}'] = func
|
||||
|
||||
def create_table(
|
||||
self, width: NUM, height: NUM, headings: tuple, widths: tuple = None,
|
||||
alignments: tuple = None, position: FLOAT = 'left', draggable: bool = False,
|
||||
background_color: str = '#121417', border_color: str = 'rgb(70, 70, 70)',
|
||||
border_width: int = 1, heading_text_colors: tuple = None,
|
||||
heading_background_colors: tuple = None, return_clicked_cells: bool = False,
|
||||
func: callable = None
|
||||
self,
|
||||
width: NUM,
|
||||
height: NUM,
|
||||
headings: tuple,
|
||||
widths: Optional[tuple] = None,
|
||||
alignments: Optional[tuple] = None,
|
||||
position: FLOAT = 'left',
|
||||
draggable: bool = False,
|
||||
background_color: str = '#121417',
|
||||
border_color: str = 'rgb(70, 70, 70)',
|
||||
border_width: int = 1,
|
||||
heading_text_colors: Optional[tuple] = None,
|
||||
heading_background_colors: Optional[tuple] = None,
|
||||
return_clicked_cells: bool = False,
|
||||
func: Optional[Callable] = None
|
||||
) -> Table:
|
||||
return self.win.create_table(width, height, headings, widths, alignments, position, draggable,
|
||||
background_color, border_color, border_width, heading_text_colors,
|
||||
@ -1016,12 +957,11 @@ class AbstractChart(Candlestick, Pane):
|
||||
Takes a screenshot. This method can only be used after the chart window is visible.
|
||||
:return: a bytes object containing a screenshot of the chart.
|
||||
"""
|
||||
self.run_script(f'_~_~RETURN~_~_{self.id}.chart.takeScreenshot().toDataURL()')
|
||||
serial_data = self.win._return_q.get()
|
||||
serial_data = self.win.run_script_and_get(f'{self.id}.chart.takeScreenshot().toDataURL()')
|
||||
return b64decode(serial_data.split(',')[1])
|
||||
|
||||
def create_subchart(self, position: FLOAT = 'left', width: float = 0.5, height: float = 0.5,
|
||||
sync: Union[str, bool] = None, scale_candles_only: bool = False,
|
||||
sync: Optional[Union[str, bool]] = None, scale_candles_only: bool = False,
|
||||
sync_crosshairs_only: bool = False,
|
||||
toolbox: bool = False) -> 'AbstractChart':
|
||||
if sync is True:
|
||||
|
||||
@ -1,37 +1,43 @@
|
||||
import asyncio
|
||||
import json
|
||||
import multiprocessing as mp
|
||||
import typing
|
||||
import webview
|
||||
from webview.errors import JavascriptException
|
||||
|
||||
from lightweight_charts import abstract
|
||||
from .util import parse_event_message, FLOAT
|
||||
|
||||
import os
|
||||
import threading
|
||||
|
||||
|
||||
class CallbackAPI:
|
||||
def __init__(self, emit_queue):
|
||||
self.emit_q = emit_queue
|
||||
self.emit_queue = emit_queue
|
||||
|
||||
def callback(self, message: str):
|
||||
self.emit_q.put(message)
|
||||
self.emit_queue.put(message)
|
||||
|
||||
|
||||
class PyWV:
|
||||
def __init__(self, q, start_ev, exit_ev, loaded, emit_queue, return_queue, html, debug,
|
||||
width, height, x, y, screen, on_top, maximize, title):
|
||||
def __init__(self, q, emit_q, return_q, loaded_event):
|
||||
self.queue = q
|
||||
self.return_queue = return_queue
|
||||
self.exit = exit_ev
|
||||
self.callback_api = CallbackAPI(emit_queue)
|
||||
self.loaded: list = loaded
|
||||
self.html = html
|
||||
self.return_queue = return_q
|
||||
self.emit_queue = emit_q
|
||||
self.loaded_event = loaded_event
|
||||
|
||||
self.windows = []
|
||||
self.create_window(width, height, x, y, screen, on_top, maximize, title)
|
||||
self.is_alive = True
|
||||
|
||||
start_ev.wait()
|
||||
webview.start(debug=debug)
|
||||
self.exit.set()
|
||||
self.callback_api = CallbackAPI(emit_q)
|
||||
self.windows: typing.List[webview.Window] = []
|
||||
self.loop()
|
||||
|
||||
def create_window(self, width, height, x, y, screen=None, on_top=False, maximize=False, title=''):
|
||||
|
||||
def create_window(
|
||||
self, width, height, x, y, screen=None, on_top=False,
|
||||
maximize=False, title=''
|
||||
):
|
||||
screen = webview.screens[screen] if screen is not None else None
|
||||
if maximize:
|
||||
if screen is None:
|
||||
@ -39,64 +45,147 @@ class PyWV:
|
||||
width, height = active_screen.width, active_screen.height
|
||||
else:
|
||||
width, height = screen.width, screen.height
|
||||
self.windows.append(webview.create_window(
|
||||
title, html=self.html, js_api=self.callback_api,
|
||||
width=width, height=height, x=x, y=y, screen=screen,
|
||||
on_top=on_top, background_color='#000000'))
|
||||
self.windows[-1].events.loaded += lambda: self.loop(self.loaded[len(self.windows)-1])
|
||||
|
||||
def loop(self, loaded):
|
||||
loaded.set()
|
||||
while 1:
|
||||
self.windows.append(webview.create_window(
|
||||
title,
|
||||
url=abstract.INDEX,
|
||||
js_api=self.callback_api,
|
||||
width=width,
|
||||
height=height,
|
||||
x=x,
|
||||
y=y,
|
||||
screen=screen,
|
||||
on_top=on_top,
|
||||
background_color='#000000')
|
||||
)
|
||||
|
||||
|
||||
def loop(self):
|
||||
self.loaded_event.set()
|
||||
while self.is_alive:
|
||||
i, arg = self.queue.get()
|
||||
|
||||
if i == 'start':
|
||||
webview.start(debug=arg, func=self.loop)
|
||||
self.is_alive = False
|
||||
self.emit_queue.put('exit')
|
||||
return
|
||||
if i == 'create_window':
|
||||
self.create_window(*arg)
|
||||
elif arg in ('show', 'hide'):
|
||||
getattr(self.windows[i], arg)()
|
||||
elif arg == 'exit':
|
||||
self.exit.set()
|
||||
continue
|
||||
|
||||
window = self.windows[i]
|
||||
if arg == 'show':
|
||||
window.show()
|
||||
elif arg == 'hide':
|
||||
window.hide()
|
||||
# TODO make sure setup.py requires latest pywebview now
|
||||
else:
|
||||
try:
|
||||
if '_~_~RETURN~_~_' in arg:
|
||||
self.return_queue.put(self.windows[i].evaluate_js(arg[14:]))
|
||||
self.return_queue.put(window.evaluate_js(arg[14:]))
|
||||
else:
|
||||
self.windows[i].evaluate_js(arg)
|
||||
except KeyError:
|
||||
window.evaluate_js(arg)
|
||||
except KeyError as e:
|
||||
return
|
||||
except JavascriptException as e:
|
||||
msg = eval(str(e))
|
||||
raise JavascriptException(f"\n\nscript -> '{arg}',\nerror -> {msg['name']}[{msg['line']}:{msg['column']}]\n{msg['message']}")
|
||||
|
||||
|
||||
class WebviewHandler():
|
||||
def __init__(self) -> None:
|
||||
self._reset()
|
||||
self.debug = False
|
||||
|
||||
def _reset(self):
|
||||
self.loaded_event = mp.Event()
|
||||
self.return_queue = mp.Queue()
|
||||
self.function_call_queue = mp.Queue()
|
||||
self.emit_queue = mp.Queue()
|
||||
self.wv_process = mp.Process(
|
||||
target=PyWV, args=(
|
||||
self.function_call_queue, self.emit_queue,
|
||||
self.return_queue, self.loaded_event
|
||||
),
|
||||
daemon=True
|
||||
)
|
||||
self.max_window_num = -1
|
||||
|
||||
def create_window(
|
||||
self, width, height, x, y, screen=None, on_top=False,
|
||||
maximize=False, title=''
|
||||
):
|
||||
self.function_call_queue.put((
|
||||
'create_window',
|
||||
(width, height, x, y, screen, on_top, maximize, title)
|
||||
))
|
||||
self.max_window_num += 1
|
||||
return self.max_window_num
|
||||
|
||||
def start(self):
|
||||
self.loaded_event.clear()
|
||||
self.wv_process.start()
|
||||
self.function_call_queue.put(('start', self.debug))
|
||||
self.loaded_event.wait()
|
||||
|
||||
def show(self, window_num):
|
||||
self.function_call_queue.put((window_num, 'show'))
|
||||
|
||||
def hide(self, window_num):
|
||||
self.function_call_queue.put((window_num, 'hide'))
|
||||
|
||||
def evaluate_js(self, window_num, script):
|
||||
self.function_call_queue.put((window_num, script))
|
||||
|
||||
def exit(self):
|
||||
if self.wv_process.is_alive():
|
||||
self.wv_process.terminate()
|
||||
self.wv_process.join()
|
||||
self._reset()
|
||||
|
||||
|
||||
class Chart(abstract.AbstractChart):
|
||||
MAX_WINDOWS = 10
|
||||
_window_num = 0
|
||||
_main_window_handlers = None
|
||||
_exit, _start = (mp.Event() for _ in range(2))
|
||||
_q, _emit_q, _return_q = (mp.Queue() for _ in range(3))
|
||||
_loaded_list = [mp.Event() for _ in range(MAX_WINDOWS)]
|
||||
WV: WebviewHandler = WebviewHandler()
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
width: int = 800,
|
||||
height: int = 600,
|
||||
x: int = None,
|
||||
y: int = None,
|
||||
title: str = '',
|
||||
screen: int = None,
|
||||
on_top: bool = False,
|
||||
maximize: bool = False,
|
||||
debug: bool = False,
|
||||
toolbox: bool = False,
|
||||
inner_width: float = 1.0,
|
||||
inner_height: float = 1.0,
|
||||
scale_candles_only: bool = False,
|
||||
position: FLOAT = 'left'
|
||||
):
|
||||
Chart.WV.debug = debug
|
||||
self._i = Chart.WV.create_window(
|
||||
width, height, x, y, screen, on_top, maximize, title
|
||||
)
|
||||
|
||||
window = abstract.Window(
|
||||
script_func=lambda s: Chart.WV.evaluate_js(self._i, s),
|
||||
js_api_code='pywebview.api.callback'
|
||||
)
|
||||
|
||||
abstract.Window._return_q = Chart.WV.return_queue
|
||||
|
||||
def __init__(self, width: int = 800, height: int = 600, x: int = None, y: int = None, title: str = '',
|
||||
screen: int = None, on_top: bool = False, maximize: bool = False, debug: bool = False,
|
||||
toolbox: bool = False, inner_width: float = 1.0, inner_height: float = 1.0,
|
||||
scale_candles_only: bool = False, position: FLOAT = 'left'):
|
||||
self._i = Chart._window_num
|
||||
self._loaded = Chart._loaded_list[self._i]
|
||||
abstract.Window._return_q = Chart._return_q
|
||||
Chart._window_num += 1
|
||||
self.is_alive = True
|
||||
|
||||
window = abstract.Window(lambda s: self._q.put((self._i, s)), 'pywebview.api.callback')
|
||||
if self._i == 0:
|
||||
if Chart._main_window_handlers is None:
|
||||
super().__init__(window, inner_width, inner_height, scale_candles_only, toolbox, position=position)
|
||||
Chart._main_window_handlers = self.win.handlers
|
||||
self._process = mp.Process(target=PyWV, args=(
|
||||
self._q, self._start, self._exit, Chart._loaded_list,
|
||||
self._emit_q, self._return_q, abstract.TEMPLATE, debug,
|
||||
width, height, x, y, screen, on_top, maximize, title
|
||||
), daemon=True)
|
||||
self._process.start()
|
||||
else:
|
||||
window.handlers = Chart._main_window_handlers
|
||||
super().__init__(window, inner_width, inner_height, scale_candles_only, toolbox, position=position)
|
||||
self._q.put(('create_window', (width, height, x, y, screen, on_top, maximize, title)))
|
||||
|
||||
def show(self, block: bool = False):
|
||||
"""
|
||||
@ -104,34 +193,31 @@ class Chart(abstract.AbstractChart):
|
||||
:param block: blocks execution until the chart is closed.
|
||||
"""
|
||||
if not self.win.loaded:
|
||||
self._start.set()
|
||||
self._loaded.wait()
|
||||
Chart.WV.start()
|
||||
self.win.on_js_load()
|
||||
else:
|
||||
self._q.put((self._i, 'show'))
|
||||
Chart.WV.show(self._i)
|
||||
if block:
|
||||
asyncio.run(self.show_async(block=True))
|
||||
asyncio.run(self.show_async())
|
||||
|
||||
async def show_async(self, block=False):
|
||||
async def show_async(self):
|
||||
self.show(block=False)
|
||||
if not block:
|
||||
asyncio.create_task(self.show_async(block=True))
|
||||
return
|
||||
try:
|
||||
from lightweight_charts import polygon
|
||||
[asyncio.create_task(self.polygon.async_set(*args)) for args in polygon._set_on_load]
|
||||
while 1:
|
||||
while self._emit_q.empty() and not self._exit.is_set():
|
||||
while Chart.WV.emit_queue.empty() and self.is_alive:
|
||||
await asyncio.sleep(0.05)
|
||||
if self._exit.is_set():
|
||||
self._exit.clear()
|
||||
self.is_alive = False
|
||||
self.exit()
|
||||
if not self.is_alive:
|
||||
return
|
||||
elif not self._emit_q.empty():
|
||||
func, args = parse_event_message(self.win, self._emit_q.get())
|
||||
response = Chart.WV.emit_queue.get()
|
||||
if response == 'exit':
|
||||
Chart.WV.exit()
|
||||
self.is_alive = False
|
||||
return
|
||||
else:
|
||||
func, args = parse_event_message(self.win, response)
|
||||
await func(*args) if asyncio.iscoroutinefunction(func) else func(*args)
|
||||
continue
|
||||
except KeyboardInterrupt:
|
||||
return
|
||||
|
||||
@ -145,12 +231,5 @@ class Chart(abstract.AbstractChart):
|
||||
"""
|
||||
Exits and destroys the chart window.\n
|
||||
"""
|
||||
self._q.put((self._i, 'exit'))
|
||||
self._exit.wait() if self.win.loaded else None
|
||||
self._process.terminate()
|
||||
|
||||
Chart._main_window_handlers = None
|
||||
Chart._window_num = 0
|
||||
Chart._q = mp.Queue()
|
||||
Chart._exit.clear(), Chart._start.clear()
|
||||
Chart.WV.exit()
|
||||
self.is_alive = False
|
||||
|
||||
1
lightweight_charts/js/bundle.js
Normal file
1
lightweight_charts/js/bundle.js
Normal file
File diff suppressed because one or more lines are too long
@ -1,277 +0,0 @@
|
||||
if (!window.TopBar) {
|
||||
class TopBar {
|
||||
constructor(chart) {
|
||||
this.makeSwitcher = this.makeSwitcher.bind(this)
|
||||
this.topBar = document.createElement('div')
|
||||
this.topBar.style.backgroundColor = pane.backgroundColor
|
||||
this.topBar.style.borderBottom = '2px solid '+pane.borderColor
|
||||
this.topBar.style.display = 'flex'
|
||||
this.topBar.style.alignItems = 'center'
|
||||
|
||||
let createTopBarContainer = (justification) => {
|
||||
let div = document.createElement('div')
|
||||
div.style.display = 'flex'
|
||||
div.style.alignItems = 'center'
|
||||
div.style.justifyContent = justification
|
||||
div.style.flexGrow = '1'
|
||||
this.topBar.appendChild(div)
|
||||
return div
|
||||
}
|
||||
this.left = createTopBarContainer('flex-start')
|
||||
this.right = createTopBarContainer('flex-end')
|
||||
|
||||
chart.wrapper.prepend(this.topBar)
|
||||
chart.topBar = this.topBar
|
||||
this.reSize = () => chart.reSize()
|
||||
this.reSize()
|
||||
}
|
||||
makeSwitcher(items, activeItem, callbackName, align='left') {
|
||||
let switcherElement = document.createElement('div');
|
||||
switcherElement.style.margin = '4px 12px'
|
||||
let widget = {
|
||||
elem: switcherElement,
|
||||
callbackName: callbackName,
|
||||
intervalElements: null,
|
||||
onItemClicked: null,
|
||||
}
|
||||
widget.intervalElements = items.map((item)=> {
|
||||
let itemEl = document.createElement('button');
|
||||
itemEl.style.border = 'none'
|
||||
itemEl.style.padding = '2px 5px'
|
||||
itemEl.style.margin = '0px 2px'
|
||||
itemEl.style.fontSize = '13px'
|
||||
itemEl.style.borderRadius = '4px'
|
||||
itemEl.style.backgroundColor = item === activeItem ? pane.activeBackgroundColor : 'transparent'
|
||||
itemEl.style.color = item === activeItem ? pane.activeColor : pane.color
|
||||
itemEl.innerText = item;
|
||||
document.body.appendChild(itemEl)
|
||||
itemEl.style.minWidth = itemEl.clientWidth + 1 + 'px'
|
||||
document.body.removeChild(itemEl)
|
||||
|
||||
itemEl.addEventListener('mouseenter', () => itemEl.style.backgroundColor = item === activeItem ? pane.activeBackgroundColor : pane.hoverBackgroundColor)
|
||||
itemEl.addEventListener('mouseleave', () => itemEl.style.backgroundColor = item === activeItem ? pane.activeBackgroundColor : 'transparent')
|
||||
itemEl.addEventListener('mousedown', () => itemEl.style.backgroundColor = item === activeItem ? pane.activeBackgroundColor : pane.clickBackgroundColor)
|
||||
itemEl.addEventListener('mouseup', () => itemEl.style.backgroundColor = item === activeItem ? pane.activeBackgroundColor : pane.hoverBackgroundColor)
|
||||
itemEl.addEventListener('click', () => widget.onItemClicked(item))
|
||||
|
||||
switcherElement.appendChild(itemEl);
|
||||
return itemEl;
|
||||
});
|
||||
widget.onItemClicked = (item)=> {
|
||||
if (item === activeItem) return
|
||||
widget.intervalElements.forEach((element, index) => {
|
||||
element.style.backgroundColor = items[index] === item ? pane.activeBackgroundColor : 'transparent'
|
||||
element.style.color = items[index] === item ? pane.activeColor : pane.color
|
||||
element.style.fontWeight = items[index] === item ? '500' : 'normal'
|
||||
})
|
||||
activeItem = item;
|
||||
window.callbackFunction(`${widget.callbackName}_~_${item}`);
|
||||
}
|
||||
this.appendWidget(switcherElement, align, true)
|
||||
return widget
|
||||
}
|
||||
makeTextBoxWidget(text, align='left') {
|
||||
let textBox = document.createElement('div')
|
||||
textBox.style.margin = '0px 18px'
|
||||
textBox.style.fontSize = '16px'
|
||||
textBox.style.color = pane.color
|
||||
textBox.innerText = text
|
||||
this.appendWidget(textBox, align, true)
|
||||
return textBox
|
||||
}
|
||||
|
||||
makeMenu(items, activeItem, separator, callbackName, align='right') {
|
||||
let menu = document.createElement('div')
|
||||
menu.style.position = 'absolute'
|
||||
menu.style.display = 'none'
|
||||
menu.style.zIndex = '100000'
|
||||
menu.style.backgroundColor = pane.backgroundColor
|
||||
menu.style.borderRadius = '2px'
|
||||
menu.style.border = '2px solid '+pane.borderColor
|
||||
menu.style.borderTop = 'none'
|
||||
menu.style.alignItems = 'flex-start'
|
||||
menu.style.maxHeight = '80%'
|
||||
menu.style.overflowY = 'auto'
|
||||
menu.style.scrollbar
|
||||
|
||||
let menuOpen = false
|
||||
items.forEach(text => {
|
||||
let button = this.makeButton(text, null, false, false)
|
||||
button.elem.addEventListener('click', () => {
|
||||
widget.elem.innerText = button.elem.innerText+' ↓'
|
||||
window.callbackFunction(`${callbackName}_~_${button.elem.innerText}`)
|
||||
menu.style.display = 'none'
|
||||
menuOpen = false
|
||||
});
|
||||
button.elem.style.margin = '4px 4px'
|
||||
button.elem.style.padding = '2px 2px'
|
||||
menu.appendChild(button.elem)
|
||||
})
|
||||
let widget =
|
||||
this.makeButton(activeItem+' ↓', null, separator, true, align)
|
||||
|
||||
widget.elem.addEventListener('click', () => {
|
||||
menuOpen = !menuOpen
|
||||
if (!menuOpen) return menu.style.display = 'none'
|
||||
let rect = widget.elem.getBoundingClientRect()
|
||||
menu.style.display = 'flex'
|
||||
menu.style.flexDirection = 'column'
|
||||
|
||||
let center = rect.x+(rect.width/2)
|
||||
menu.style.left = center-(menu.clientWidth/2)+'px'
|
||||
menu.style.top = rect.y+rect.height+'px'
|
||||
})
|
||||
document.body.appendChild(menu)
|
||||
}
|
||||
|
||||
makeButton(defaultText, callbackName, separator, append=true, align='left') {
|
||||
let button = document.createElement('button')
|
||||
button.style.border = 'none'
|
||||
button.style.padding = '2px 5px'
|
||||
button.style.margin = '4px 10px'
|
||||
button.style.fontSize = '13px'
|
||||
button.style.backgroundColor = 'transparent'
|
||||
button.style.color = pane.color
|
||||
button.style.borderRadius = '4px'
|
||||
button.innerText = defaultText;
|
||||
document.body.appendChild(button)
|
||||
button.style.minWidth = button.clientWidth+1+'px'
|
||||
document.body.removeChild(button)
|
||||
|
||||
let widget = {
|
||||
elem: button,
|
||||
callbackName: callbackName
|
||||
}
|
||||
|
||||
button.addEventListener('mouseenter', () => button.style.backgroundColor = pane.hoverBackgroundColor)
|
||||
button.addEventListener('mouseleave', () => button.style.backgroundColor = 'transparent')
|
||||
if (callbackName) {
|
||||
button.addEventListener('click', () => window.callbackFunction(`${widget.callbackName}_~_${button.innerText}`));
|
||||
}
|
||||
button.addEventListener('mousedown', () => {
|
||||
button.style.backgroundColor = pane.activeBackgroundColor
|
||||
button.style.color = pane.activeColor
|
||||
button.style.fontWeight = '500'
|
||||
})
|
||||
button.addEventListener('mouseup', () => {
|
||||
button.style.backgroundColor = pane.hoverBackgroundColor
|
||||
button.style.color = pane.color
|
||||
button.style.fontWeight = 'normal'
|
||||
})
|
||||
if (append) this.appendWidget(button, align, separator)
|
||||
return widget
|
||||
}
|
||||
|
||||
makeSeparator(align='left') {
|
||||
let seperator = document.createElement('div')
|
||||
seperator.style.width = '1px'
|
||||
seperator.style.height = '20px'
|
||||
seperator.style.backgroundColor = pane.borderColor
|
||||
let div = align === 'left' ? this.left : this.right
|
||||
div.appendChild(seperator)
|
||||
}
|
||||
|
||||
appendWidget(widget, align, separator) {
|
||||
let div = align === 'left' ? this.left : this.right
|
||||
if (separator) {
|
||||
if (align === 'left') div.appendChild(widget)
|
||||
this.makeSeparator(align)
|
||||
if (align === 'right') div.appendChild(widget)
|
||||
} else div.appendChild(widget)
|
||||
this.reSize()
|
||||
}
|
||||
}
|
||||
window.TopBar = TopBar
|
||||
}
|
||||
|
||||
function makeSearchBox(chart) {
|
||||
let searchWindow = document.createElement('div')
|
||||
searchWindow.style.position = 'absolute'
|
||||
searchWindow.style.top = '0'
|
||||
searchWindow.style.bottom = '200px'
|
||||
searchWindow.style.left = '0'
|
||||
searchWindow.style.right = '0'
|
||||
searchWindow.style.margin = 'auto'
|
||||
searchWindow.style.width = '150px'
|
||||
searchWindow.style.height = '30px'
|
||||
searchWindow.style.padding = '5px'
|
||||
searchWindow.style.zIndex = '1000'
|
||||
searchWindow.style.alignItems = 'center'
|
||||
searchWindow.style.backgroundColor = 'rgba(30, 30, 30, 0.9)'
|
||||
searchWindow.style.border = '2px solid '+pane.borderColor
|
||||
searchWindow.style.borderRadius = '5px'
|
||||
searchWindow.style.display = 'none'
|
||||
|
||||
let magnifyingGlass = document.createElement('div');
|
||||
magnifyingGlass.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="24px" height="24px" viewBox="0 0 24 24" version="1.1"><path style="fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke:lightgray;stroke-opacity:1;stroke-miterlimit:4;" d="M 15 15 L 21 21 M 10 17 C 6.132812 17 3 13.867188 3 10 C 3 6.132812 6.132812 3 10 3 C 13.867188 3 17 6.132812 17 10 C 17 13.867188 13.867188 17 10 17 Z M 10 17 "/></svg>`
|
||||
|
||||
let sBox = document.createElement('input');
|
||||
sBox.type = 'text';
|
||||
sBox.style.textAlign = 'center'
|
||||
sBox.style.width = '100px'
|
||||
sBox.style.marginLeft = '10px'
|
||||
sBox.style.backgroundColor = pane.mutedBackgroundColor
|
||||
sBox.style.color = pane.activeColor
|
||||
sBox.style.fontSize = '20px'
|
||||
sBox.style.border = 'none'
|
||||
sBox.style.outline = 'none'
|
||||
sBox.style.borderRadius = '2px'
|
||||
|
||||
searchWindow.appendChild(magnifyingGlass)
|
||||
searchWindow.appendChild(sBox)
|
||||
chart.div.appendChild(searchWindow);
|
||||
|
||||
let yPrice = null
|
||||
chart.chart.subscribeCrosshairMove((param) => {
|
||||
if (param.point) yPrice = param.point.y;
|
||||
})
|
||||
chart.commandFunctions.push((event) => {
|
||||
if (selectedChart !== chart) return false
|
||||
if (searchWindow.style.display === 'none') {
|
||||
if (/^[a-zA-Z0-9]$/.test(event.key)) {
|
||||
searchWindow.style.display = 'flex';
|
||||
sBox.focus();
|
||||
return true
|
||||
}
|
||||
else return false
|
||||
}
|
||||
else if (event.key === 'Enter' || event.key === 'Escape') {
|
||||
if (event.key === 'Enter') window.callbackFunction(`search${chart.id}_~_${sBox.value}`)
|
||||
searchWindow.style.display = 'none'
|
||||
sBox.value = ''
|
||||
return true
|
||||
}
|
||||
else return false
|
||||
})
|
||||
sBox.addEventListener('input', () => sBox.value = sBox.value.toUpperCase())
|
||||
return {
|
||||
window: searchWindow,
|
||||
box: sBox,
|
||||
}
|
||||
}
|
||||
|
||||
function makeSpinner(chart) {
|
||||
chart.spinner = document.createElement('div')
|
||||
chart.spinner.style.width = '30px'
|
||||
chart.spinner.style.height = '30px'
|
||||
chart.spinner.style.border = '4px solid rgba(255, 255, 255, 0.6)'
|
||||
chart.spinner.style.borderTop = '4px solid '+pane.activeBackgroundColor
|
||||
chart.spinner.style.borderRadius = '50%'
|
||||
chart.spinner.style.position = 'absolute'
|
||||
chart.spinner.style.top = '50%'
|
||||
chart.spinner.style.left = '50%'
|
||||
chart.spinner.style.zIndex = '1000'
|
||||
chart.spinner.style.transform = 'translate(-50%, -50%)'
|
||||
chart.spinner.style.display = 'none'
|
||||
chart.wrapper.appendChild(chart.spinner)
|
||||
let rotation = 0;
|
||||
const speed = 10;
|
||||
function animateSpinner() {
|
||||
rotation += speed
|
||||
chart.spinner.style.transform = `translate(-50%, -50%) rotate(${rotation}deg)`
|
||||
requestAnimationFrame(animateSpinner)
|
||||
}
|
||||
animateSpinner();
|
||||
}
|
||||
|
||||
|
||||
@ -1,595 +0,0 @@
|
||||
if (!window.Chart) {
|
||||
window.pane = {
|
||||
backgroundColor: '#0c0d0f',
|
||||
hoverBackgroundColor: '#3c434c',
|
||||
clickBackgroundColor: '#50565E',
|
||||
activeBackgroundColor: 'rgba(0, 122, 255, 0.7)',
|
||||
mutedBackgroundColor: 'rgba(0, 122, 255, 0.3)',
|
||||
borderColor: '#3C434C',
|
||||
color: '#d8d9db',
|
||||
activeColor: '#ececed',
|
||||
}
|
||||
|
||||
class Chart {
|
||||
constructor(chartId, innerWidth, innerHeight, position, autoSize) {
|
||||
this.makeCandlestickSeries = this.makeCandlestickSeries.bind(this)
|
||||
this.reSize = this.reSize.bind(this)
|
||||
this.id = chartId
|
||||
this.lines = []
|
||||
this.interval = null
|
||||
this.wrapper = document.createElement('div')
|
||||
this.div = document.createElement('div')
|
||||
this.scale = {
|
||||
width: innerWidth,
|
||||
height: innerHeight,
|
||||
}
|
||||
this.commandFunctions = []
|
||||
this.chart = LightweightCharts.createChart(this.div, {
|
||||
width: window.innerWidth * innerWidth,
|
||||
height: window.innerHeight * innerHeight,
|
||||
layout: {
|
||||
textColor: pane.color,
|
||||
background: {
|
||||
color: '#000000',
|
||||
type: LightweightCharts.ColorType.Solid,
|
||||
},
|
||||
fontSize: 12
|
||||
},
|
||||
rightPriceScale: {
|
||||
scaleMargins: {top: 0.3, bottom: 0.25},
|
||||
},
|
||||
timeScale: {timeVisible: true, secondsVisible: false},
|
||||
crosshair: {
|
||||
mode: LightweightCharts.CrosshairMode.Normal,
|
||||
vertLine: {
|
||||
labelBackgroundColor: 'rgb(46, 46, 46)'
|
||||
},
|
||||
horzLine: {
|
||||
labelBackgroundColor: 'rgb(55, 55, 55)'
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
vertLines: {color: 'rgba(29, 30, 38, 5)'},
|
||||
horzLines: {color: 'rgba(29, 30, 58, 5)'},
|
||||
},
|
||||
handleScroll: {vertTouchDrag: true},
|
||||
})
|
||||
this.legend = new Legend(this)
|
||||
this.wrapper.style.display = 'flex'
|
||||
this.wrapper.style.flexDirection = 'column'
|
||||
this.wrapper.style.position = 'relative'
|
||||
this.wrapper.style.float = position
|
||||
this.div.style.position = 'relative'
|
||||
// this.div.style.display = 'flex'
|
||||
this.reSize()
|
||||
this.wrapper.appendChild(this.div)
|
||||
document.getElementById('wrapper').append(this.wrapper)
|
||||
|
||||
document.addEventListener('keydown', (event) => {
|
||||
for (let i = 0; i < this.commandFunctions.length; i++) {
|
||||
if (this.commandFunctions[i](event)) break
|
||||
}
|
||||
})
|
||||
|
||||
window.selectedChart = this
|
||||
this.wrapper.addEventListener('mouseover', (e) => window.selectedChart = this)
|
||||
|
||||
if (!autoSize) return
|
||||
window.addEventListener('resize', () => this.reSize())
|
||||
}
|
||||
reSize() {
|
||||
let topBarOffset = 'topBar' in this && this.scale.height !== 0 ? this.topBar.offsetHeight : 0
|
||||
this.chart.resize(window.innerWidth * this.scale.width, (window.innerHeight * this.scale.height) - topBarOffset)
|
||||
this.wrapper.style.width = `${100 * this.scale.width}%`
|
||||
this.wrapper.style.height = `${100 * this.scale.height}%`
|
||||
|
||||
if (this.scale.height === 0 || this.scale.width === 0) {
|
||||
this.legend.div.style.display = 'none'
|
||||
if (this.toolBox) {
|
||||
this.toolBox.elem.style.display = 'none'
|
||||
}
|
||||
}
|
||||
else {
|
||||
this.legend.div.style.display = 'flex'
|
||||
if (this.toolBox) {
|
||||
this.toolBox.elem.style.display = 'flex'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
makeCandlestickSeries() {
|
||||
this.markers = []
|
||||
this.horizontal_lines = []
|
||||
this.data = []
|
||||
this.precision = 2
|
||||
let up = 'rgba(39, 157, 130, 100)'
|
||||
let down = 'rgba(200, 97, 100, 100)'
|
||||
this.series = this.chart.addCandlestickSeries({
|
||||
color: 'rgb(0, 120, 255)', upColor: up, borderUpColor: up, wickUpColor: up,
|
||||
downColor: down, borderDownColor: down, wickDownColor: down, lineWidth: 2,
|
||||
})
|
||||
this.volumeSeries = this.chart.addHistogramSeries({
|
||||
color: '#26a69a',
|
||||
priceFormat: {type: 'volume'},
|
||||
priceScaleId: 'volume_scale',
|
||||
})
|
||||
this.series.priceScale().applyOptions({
|
||||
scaleMargins: {top: 0.2, bottom: 0.2},
|
||||
});
|
||||
this.volumeSeries.priceScale().applyOptions({
|
||||
scaleMargins: {top: 0.8, bottom: 0},
|
||||
});
|
||||
}
|
||||
toJSON() {
|
||||
// Exclude the chart attribute from serialization
|
||||
const {chart, ...serialized} = this;
|
||||
return serialized;
|
||||
}
|
||||
}
|
||||
window.Chart = Chart
|
||||
|
||||
class HorizontalLine {
|
||||
constructor(chart, lineId, price, color, width, style, axisLabelVisible, text) {
|
||||
this.updatePrice = this.updatePrice.bind(this)
|
||||
this.deleteLine = this.deleteLine.bind(this)
|
||||
this.chart = chart
|
||||
this.price = price
|
||||
this.color = color
|
||||
this.id = lineId
|
||||
this.priceLine = {
|
||||
price: this.price,
|
||||
color: this.color,
|
||||
lineWidth: width,
|
||||
lineStyle: style,
|
||||
axisLabelVisible: axisLabelVisible,
|
||||
title: text,
|
||||
}
|
||||
this.line = this.chart.series.createPriceLine(this.priceLine)
|
||||
this.chart.horizontal_lines.push(this)
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
// Exclude the chart attribute from serialization
|
||||
const {chart, line, ...serialized} = this;
|
||||
return serialized;
|
||||
}
|
||||
|
||||
updatePrice(price) {
|
||||
this.chart.series.removePriceLine(this.line)
|
||||
this.price = price
|
||||
this.priceLine.price = this.price
|
||||
this.line = this.chart.series.createPriceLine(this.priceLine)
|
||||
}
|
||||
|
||||
updateLabel(text) {
|
||||
this.chart.series.removePriceLine(this.line)
|
||||
this.priceLine.title = text
|
||||
this.line = this.chart.series.createPriceLine(this.priceLine)
|
||||
}
|
||||
|
||||
updateColor(color) {
|
||||
this.chart.series.removePriceLine(this.line)
|
||||
this.color = color
|
||||
this.priceLine.color = this.color
|
||||
this.line = this.chart.series.createPriceLine(this.priceLine)
|
||||
}
|
||||
|
||||
updateStyle(style) {
|
||||
this.chart.series.removePriceLine(this.line)
|
||||
this.priceLine.lineStyle = style
|
||||
this.line = this.chart.series.createPriceLine(this.priceLine)
|
||||
}
|
||||
|
||||
deleteLine() {
|
||||
this.chart.series.removePriceLine(this.line)
|
||||
this.chart.horizontal_lines.splice(this.chart.horizontal_lines.indexOf(this))
|
||||
delete this
|
||||
}
|
||||
}
|
||||
|
||||
window.HorizontalLine = HorizontalLine
|
||||
|
||||
class Legend {
|
||||
constructor(chart) {
|
||||
this.legendHandler = this.legendHandler.bind(this)
|
||||
|
||||
this.chart = chart
|
||||
this.ohlcEnabled = false
|
||||
this.percentEnabled = false
|
||||
this.linesEnabled = false
|
||||
this.colorBasedOnCandle = false
|
||||
|
||||
this.div = document.createElement('div')
|
||||
this.div.style.position = 'absolute'
|
||||
this.div.style.zIndex = '3000'
|
||||
this.div.style.pointerEvents = 'none'
|
||||
this.div.style.top = '10px'
|
||||
this.div.style.left = '10px'
|
||||
this.div.style.display = 'flex'
|
||||
this.div.style.flexDirection = 'column'
|
||||
this.div.style.maxWidth = `${(chart.scale.width * 100) - 8}vw`
|
||||
|
||||
this.text = document.createElement('span')
|
||||
this.text.style.lineHeight = '1.8'
|
||||
this.candle = document.createElement('div')
|
||||
|
||||
this.div.appendChild(this.text)
|
||||
this.div.appendChild(this.candle)
|
||||
chart.div.appendChild(this.div)
|
||||
|
||||
this.makeLines(chart)
|
||||
|
||||
chart.chart.subscribeCrosshairMove(this.legendHandler)
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
// Exclude the chart attribute from serialization
|
||||
const {lines, chart, ...serialized} = this;
|
||||
return serialized;
|
||||
}
|
||||
|
||||
makeLines(chart) {
|
||||
this.lines = []
|
||||
if (this.linesEnabled) chart.lines.forEach(line => this.lines.push(this.makeLineRow(line)))
|
||||
}
|
||||
|
||||
makeLineRow(line) {
|
||||
let openEye = `
|
||||
<path style="fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke:${this.color};stroke-opacity:1;stroke-miterlimit:4;" d="M 21.998437 12 C 21.998437 12 18.998437 18 12 18 C 5.001562 18 2.001562 12 2.001562 12 C 2.001562 12 5.001562 6 12 6 C 18.998437 6 21.998437 12 21.998437 12 Z M 21.998437 12 " transform="matrix(0.833333,0,0,0.833333,0,0)"/>
|
||||
<path style="fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke:${this.color};stroke-opacity:1;stroke-miterlimit:4;" d="M 15 12 C 15 13.654687 13.654687 15 12 15 C 10.345312 15 9 13.654687 9 12 C 9 10.345312 10.345312 9 12 9 C 13.654687 9 15 10.345312 15 12 Z M 15 12 " transform="matrix(0.833333,0,0,0.833333,0,0)"/>\`
|
||||
`
|
||||
let closedEye = `
|
||||
<path style="fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke:${this.color};stroke-opacity:1;stroke-miterlimit:4;" d="M 20.001562 9 C 20.001562 9 19.678125 9.665625 18.998437 10.514062 M 12 14.001562 C 10.392187 14.001562 9.046875 13.589062 7.95 12.998437 M 12 14.001562 C 13.607812 14.001562 14.953125 13.589062 16.05 12.998437 M 12 14.001562 L 12 17.498437 M 3.998437 9 C 3.998437 9 4.354687 9.735937 5.104687 10.645312 M 7.95 12.998437 L 5.001562 15.998437 M 7.95 12.998437 C 6.689062 12.328125 5.751562 11.423437 5.104687 10.645312 M 16.05 12.998437 L 18.501562 15.998437 M 16.05 12.998437 C 17.38125 12.290625 18.351562 11.320312 18.998437 10.514062 M 5.104687 10.645312 L 2.001562 12 M 18.998437 10.514062 L 21.998437 12 " transform="matrix(0.833333,0,0,0.833333,0,0)"/>
|
||||
`
|
||||
|
||||
let row = document.createElement('div')
|
||||
row.style.display = 'flex'
|
||||
row.style.alignItems = 'center'
|
||||
let div = document.createElement('div')
|
||||
let toggle = document.createElement('div')
|
||||
toggle.style.borderRadius = '4px'
|
||||
toggle.style.marginLeft = '10px'
|
||||
toggle.style.pointerEvents = 'auto'
|
||||
|
||||
|
||||
let svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
||||
svg.setAttribute("width", "22");
|
||||
svg.setAttribute("height", "16");
|
||||
|
||||
let group = document.createElementNS("http://www.w3.org/2000/svg", "g");
|
||||
group.innerHTML = openEye
|
||||
|
||||
let on = true
|
||||
toggle.addEventListener('click', (event) => {
|
||||
if (on) {
|
||||
on = false
|
||||
group.innerHTML = closedEye
|
||||
line.series.applyOptions({
|
||||
visible: false
|
||||
})
|
||||
} else {
|
||||
on = true
|
||||
line.series.applyOptions({
|
||||
visible: true
|
||||
})
|
||||
group.innerHTML = openEye
|
||||
}
|
||||
})
|
||||
toggle.addEventListener('mouseover', (event) => {
|
||||
document.body.style.cursor = 'pointer'
|
||||
toggle.style.backgroundColor = 'rgba(50, 50, 50, 0.5)'
|
||||
})
|
||||
toggle.addEventListener('mouseleave', (event) => {
|
||||
document.body.style.cursor = 'default'
|
||||
toggle.style.backgroundColor = 'transparent'
|
||||
})
|
||||
svg.appendChild(group)
|
||||
toggle.appendChild(svg);
|
||||
row.appendChild(div)
|
||||
row.appendChild(toggle)
|
||||
this.div.appendChild(row)
|
||||
return {
|
||||
div: div,
|
||||
row: row,
|
||||
toggle: toggle,
|
||||
line: line,
|
||||
solid: line.color.startsWith('rgba') ? line.color.replace(/[^,]+(?=\))/, '1') : line.color
|
||||
}
|
||||
}
|
||||
|
||||
legendItemFormat(num, decimal) { return num.toFixed(decimal).toString().padStart(8, ' ') }
|
||||
|
||||
shorthandFormat(num) {
|
||||
const absNum = Math.abs(num)
|
||||
if (absNum >= 1000000) {
|
||||
return (num / 1000000).toFixed(1) + 'M';
|
||||
} else if (absNum >= 1000) {
|
||||
return (num / 1000).toFixed(1) + 'K';
|
||||
}
|
||||
return num.toString().padStart(8, ' ');
|
||||
}
|
||||
|
||||
legendHandler(param, usingPoint= false) {
|
||||
let options = this.chart.series.options()
|
||||
|
||||
if (!param.time) {
|
||||
this.candle.style.color = 'transparent'
|
||||
this.candle.innerHTML = this.candle.innerHTML.replace(options['upColor'], '').replace(options['downColor'], '')
|
||||
return
|
||||
}
|
||||
|
||||
let data, logical
|
||||
|
||||
if (usingPoint) {
|
||||
let coordinate = this.chart.chart.timeScale().timeToCoordinate(param.time)
|
||||
logical = this.chart.chart.timeScale().coordinateToLogical(coordinate)
|
||||
data = this.chart.series.dataByIndex(logical)
|
||||
}
|
||||
else {
|
||||
data = param.seriesData.get(this.chart.series);
|
||||
}
|
||||
|
||||
this.candle.style.color = ''
|
||||
let str = '<span style="line-height: 1.8;">'
|
||||
if (data) {
|
||||
if (this.ohlcEnabled) {
|
||||
str += `O ${this.legendItemFormat(data.open, this.chart.precision)} `
|
||||
str += `| H ${this.legendItemFormat(data.high, this.chart.precision)} `
|
||||
str += `| L ${this.legendItemFormat(data.low, this.chart.precision)} `
|
||||
str += `| C ${this.legendItemFormat(data.close, this.chart.precision)} `
|
||||
}
|
||||
|
||||
if (this.percentEnabled) {
|
||||
let percentMove = ((data.close - data.open) / data.open) * 100
|
||||
let color = percentMove > 0 ? options['upColor'] : options['downColor']
|
||||
let percentStr = `${percentMove >= 0 ? '+' : ''}${percentMove.toFixed(2)} %`
|
||||
|
||||
if (this.colorBasedOnCandle) {
|
||||
str += `| <span style="color: ${color};">${percentStr}</span>`
|
||||
} else {
|
||||
str += '| ' + percentStr
|
||||
}
|
||||
}
|
||||
|
||||
let volumeData;
|
||||
if (usingPoint) {
|
||||
volumeData = this.chart.volumeSeries.dataByIndex(logical)
|
||||
}
|
||||
else {
|
||||
volumeData = param.seriesData.get(this.chart.volumeSeries)
|
||||
}
|
||||
if (volumeData) {
|
||||
str += this.ohlcEnabled ? `<br>V ${this.shorthandFormat(volumeData.value)}` : ''
|
||||
}
|
||||
}
|
||||
this.candle.innerHTML = str + '</span>'
|
||||
|
||||
this.lines.forEach((line) => {
|
||||
if (!this.linesEnabled) {
|
||||
line.row.style.display = 'none'
|
||||
return
|
||||
}
|
||||
line.row.style.display = 'flex'
|
||||
|
||||
let price
|
||||
if (usingPoint) {
|
||||
price = line.line.series.dataByIndex(logical)
|
||||
}
|
||||
else {
|
||||
price = param.seriesData.get(line.line.series)
|
||||
}
|
||||
if (!price) return
|
||||
else price = price.value
|
||||
|
||||
if (line.line.type === 'histogram') {
|
||||
price = this.shorthandFormat(price)
|
||||
} else {
|
||||
price = this.legendItemFormat(price, line.line.precision)
|
||||
}
|
||||
line.div.innerHTML = `<span style="color: ${line.solid};">▨</span> ${line.line.name} : ${price}`
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
window.Legend = Legend
|
||||
}
|
||||
|
||||
function syncCharts(childChart, parentChart, crosshairOnly= false) {
|
||||
|
||||
function crosshairHandler(chart, point) {
|
||||
if (!point) {
|
||||
chart.chart.clearCrosshairPosition()
|
||||
return
|
||||
}
|
||||
chart.chart.setCrosshairPosition(point.value || point.close, point.time, chart.series);
|
||||
chart.legend.legendHandler(point, true)
|
||||
}
|
||||
|
||||
function getPoint(series, param) {
|
||||
if (!param.time) return null;
|
||||
return param.seriesData.get(series) || null;
|
||||
}
|
||||
|
||||
let setChildRange, setParentRange;
|
||||
if (crosshairOnly) {
|
||||
setChildRange = (timeRange) => { }
|
||||
setParentRange = (timeRange) => { }
|
||||
}
|
||||
else {
|
||||
setChildRange = (timeRange) => childChart.chart.timeScale().setVisibleLogicalRange(timeRange)
|
||||
setParentRange = (timeRange) => parentChart.chart.timeScale().setVisibleLogicalRange(timeRange)
|
||||
}
|
||||
|
||||
let setParentCrosshair = (param) => {
|
||||
crosshairHandler(parentChart, getPoint(childChart.series, param))
|
||||
}
|
||||
let setChildCrosshair = (param) => {
|
||||
crosshairHandler(childChart, getPoint(parentChart.series, param))
|
||||
}
|
||||
|
||||
let selected = parentChart
|
||||
function addMouseOverListener(thisChart, otherChart, thisCrosshair, otherCrosshair, thisRange, otherRange) {
|
||||
thisChart.wrapper.addEventListener('mouseover', (event) => {
|
||||
if (selected === thisChart) return
|
||||
selected = thisChart
|
||||
otherChart.chart.timeScale().unsubscribeVisibleLogicalRangeChange(thisRange)
|
||||
otherChart.chart.unsubscribeCrosshairMove(thisCrosshair)
|
||||
thisChart.chart.timeScale().subscribeVisibleLogicalRangeChange(otherRange)
|
||||
thisChart.chart.subscribeCrosshairMove(otherCrosshair)
|
||||
})
|
||||
}
|
||||
addMouseOverListener(parentChart, childChart, setParentCrosshair, setChildCrosshair, setParentRange, setChildRange)
|
||||
addMouseOverListener(childChart, parentChart, setChildCrosshair, setParentCrosshair, setChildRange, setParentRange)
|
||||
|
||||
parentChart.chart.timeScale().subscribeVisibleLogicalRangeChange(setChildRange)
|
||||
parentChart.chart.subscribeCrosshairMove(setChildCrosshair)
|
||||
}
|
||||
|
||||
function stampToDate(stampOrBusiness) {
|
||||
return new Date(stampOrBusiness*1000)
|
||||
}
|
||||
function dateToStamp(date) {
|
||||
return Math.floor(date.getTime()/1000)
|
||||
}
|
||||
|
||||
function lastBar(obj) {
|
||||
return obj[obj.length-1]
|
||||
}
|
||||
|
||||
function calculateTrendLine(startDate, startValue, endDate, endValue, chart, ray=false) {
|
||||
let reversed = false
|
||||
if (stampToDate(endDate).getTime() < stampToDate(startDate).getTime()) {
|
||||
reversed = true;
|
||||
[startDate, endDate] = [endDate, startDate];
|
||||
}
|
||||
let startIndex
|
||||
if (stampToDate(startDate).getTime() < stampToDate(chart.data[0].time).getTime()) {
|
||||
startIndex = 0
|
||||
}
|
||||
else {
|
||||
startIndex = chart.data.findIndex(item => stampToDate(item.time).getTime() === stampToDate(startDate).getTime())
|
||||
}
|
||||
|
||||
if (startIndex === -1) {
|
||||
throw new Error(`Could not calculate start index from time ${stampToDate(startDate)}.`)
|
||||
}
|
||||
let endIndex
|
||||
if (ray) {
|
||||
endIndex = chart.data.length+1000
|
||||
startValue = endValue
|
||||
}
|
||||
else {
|
||||
endIndex = chart.data.findIndex(item => stampToDate(item.time).getTime() === stampToDate(endDate).getTime())
|
||||
if (endIndex === -1) {
|
||||
let barsBetween = (endDate-lastBar(chart.data).time)/chart.interval
|
||||
endIndex = chart.data.length-1+barsBetween
|
||||
}
|
||||
}
|
||||
|
||||
let numBars = endIndex-startIndex
|
||||
const rate_of_change = (endValue - startValue) / numBars;
|
||||
const trendData = [];
|
||||
let currentDate = null
|
||||
let iPastData = 0
|
||||
for (let i = 0; i <= numBars; i++) {
|
||||
if (chart.data[startIndex+i]) {
|
||||
currentDate = chart.data[startIndex+i].time
|
||||
}
|
||||
else {
|
||||
iPastData ++
|
||||
currentDate = lastBar(chart.data).time+(iPastData*chart.interval)
|
||||
}
|
||||
|
||||
const currentValue = reversed ? startValue + rate_of_change * (numBars - i) : startValue + rate_of_change * i;
|
||||
trendData.push({ time: currentDate, value: currentValue });
|
||||
}
|
||||
return trendData;
|
||||
}
|
||||
|
||||
|
||||
if (!window.ContextMenu) {
|
||||
class ContextMenu {
|
||||
constructor() {
|
||||
this.menu = document.createElement('div')
|
||||
this.menu.style.position = 'absolute'
|
||||
this.menu.style.zIndex = '10000'
|
||||
this.menu.style.background = 'rgb(50, 50, 50)'
|
||||
this.menu.style.color = pane.activeColor
|
||||
this.menu.style.display = 'none'
|
||||
this.menu.style.borderRadius = '5px'
|
||||
this.menu.style.padding = '3px 3px'
|
||||
this.menu.style.fontSize = '13px'
|
||||
this.menu.style.cursor = 'default'
|
||||
document.body.appendChild(this.menu)
|
||||
this.hoverItem = null
|
||||
|
||||
let closeMenu = (event) => {
|
||||
if (!this.menu.contains(event.target)) {
|
||||
this.menu.style.display = 'none';
|
||||
this.listen(false)
|
||||
}
|
||||
}
|
||||
|
||||
this.onRightClick = (event) => {
|
||||
event.preventDefault();
|
||||
this.menu.style.left = event.clientX + 'px';
|
||||
this.menu.style.top = event.clientY + 'px';
|
||||
this.menu.style.display = 'block';
|
||||
document.removeEventListener('click', closeMenu)
|
||||
document.addEventListener('click', closeMenu)
|
||||
}
|
||||
}
|
||||
listen(active) {
|
||||
active ? document.addEventListener('contextmenu', this.onRightClick) : document.removeEventListener('contextmenu', this.onRightClick)
|
||||
}
|
||||
menuItem(text, action, hover=false) {
|
||||
let item = document.createElement('span')
|
||||
item.style.display = 'flex'
|
||||
item.style.alignItems = 'center'
|
||||
item.style.justifyContent = 'space-between'
|
||||
item.style.padding = '2px 10px'
|
||||
item.style.margin = '1px 0px'
|
||||
item.style.borderRadius = '3px'
|
||||
this.menu.appendChild(item)
|
||||
|
||||
let elem = document.createElement('span')
|
||||
elem.innerText = text
|
||||
elem.style.pointerEvents = 'none'
|
||||
item.appendChild(elem)
|
||||
|
||||
if (hover) {
|
||||
let arrow = document.createElement('span')
|
||||
arrow.innerText = `►`
|
||||
arrow.style.fontSize = '8px'
|
||||
arrow.style.pointerEvents = 'none'
|
||||
item.appendChild(arrow)
|
||||
}
|
||||
|
||||
item.addEventListener('mouseover', (event) => {
|
||||
item.style.backgroundColor = 'rgba(0, 122, 255, 0.3)'
|
||||
if (this.hoverItem && this.hoverItem.closeAction) this.hoverItem.closeAction()
|
||||
this.hoverItem = {elem: elem, action: action, closeAction: hover}
|
||||
})
|
||||
item.addEventListener('mouseout', (event) => item.style.backgroundColor = 'transparent')
|
||||
if (!hover) item.addEventListener('click', (event) => {action(event); this.menu.style.display = 'none'})
|
||||
else {
|
||||
let timeout
|
||||
item.addEventListener('mouseover', () => timeout = setTimeout(() => action(item.getBoundingClientRect()), 100))
|
||||
item.addEventListener('mouseout', () => clearTimeout(timeout))
|
||||
}
|
||||
}
|
||||
separator() {
|
||||
let separator = document.createElement('div')
|
||||
separator.style.width = '90%'
|
||||
separator.style.height = '1px'
|
||||
separator.style.margin = '3px 0px'
|
||||
separator.style.backgroundColor = pane.borderColor
|
||||
this.menu.appendChild(separator)
|
||||
}
|
||||
|
||||
}
|
||||
window.ContextMenu = ContextMenu
|
||||
}
|
||||
|
||||
window.callbackFunction = () => undefined;
|
||||
7
lightweight_charts/js/lightweight-charts.js
Normal file
7
lightweight_charts/js/lightweight-charts.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
228
lightweight_charts/js/styles.css
Normal file
228
lightweight_charts/js/styles.css
Normal file
@ -0,0 +1,228 @@
|
||||
:root {
|
||||
--bg-color:#0c0d0f;
|
||||
--hover-bg-color: #3c434c;
|
||||
--click-bg-color: #50565E;
|
||||
--active-bg-color: rgba(0, 122, 255, 0.7);
|
||||
--muted-bg-color: rgba(0, 122, 255, 0.3);
|
||||
--border-color: #3C434C;
|
||||
--color: #d8d9db;
|
||||
--active-color: #ececed;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: rgb(0,0,0);
|
||||
color: rgba(19, 23, 34, 1);
|
||||
overflow: hidden;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu,
|
||||
Cantarell, "Helvetica Neue", sans-serif;
|
||||
}
|
||||
|
||||
.handler {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.toolbox {
|
||||
position: absolute;
|
||||
z-index: 2000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
top: 25%;
|
||||
border: 2px solid var(--border-color);
|
||||
border-left: none;
|
||||
border-top-right-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
background-color: rgba(25, 27, 30, 0.5);
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.toolbox-button {
|
||||
margin: 3px;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
background-color: transparent;
|
||||
}
|
||||
.toolbox-button:hover {
|
||||
background-color: rgba(80, 86, 94, 0.7);
|
||||
}
|
||||
.toolbox-button:active {
|
||||
background-color: rgba(90, 106, 104, 0.7);
|
||||
}
|
||||
|
||||
.active-toolbox-button {
|
||||
background-color: var(--active-bg-color) !important;
|
||||
}
|
||||
.active-toolbox-button g {
|
||||
fill: var(--active-color);
|
||||
}
|
||||
|
||||
.context-menu {
|
||||
position: absolute;
|
||||
z-index: 1000;
|
||||
background: rgb(50, 50, 50);
|
||||
color: var(--active-color);
|
||||
display: none;
|
||||
border-radius: 5px;
|
||||
padding: 3px 3px;
|
||||
font-size: 13px;
|
||||
cursor: default;
|
||||
}
|
||||
.context-menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 2px 10px;
|
||||
margin: 1px 0px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.context-menu-item:hover {
|
||||
background-color: var(--muted-bg-color);
|
||||
}
|
||||
|
||||
.color-picker {
|
||||
max-width: 170px;
|
||||
background-color: var(--bg-color);
|
||||
position: absolute;
|
||||
z-index: 10000;
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
border: 2px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
|
||||
/* topbar-related */
|
||||
.topbar {
|
||||
background-color: var(--bg-color);
|
||||
border-bottom: 2px solid var(--border-color);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.topbar-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.topbar-button {
|
||||
border: none;
|
||||
padding: 2px 5px;
|
||||
margin: 4px 10px;
|
||||
font-size: 13px;
|
||||
border-radius: 4px;
|
||||
color: var(--color);
|
||||
background-color: transparent;
|
||||
}
|
||||
.topbar-button:hover {
|
||||
background-color: var(--hover-bg-color)
|
||||
}
|
||||
|
||||
.topbar-button:active {
|
||||
background-color: var(--click-bg-color);
|
||||
color: var(--active-color);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.switcher-button:active {
|
||||
background-color: var(--click-bg-color);
|
||||
color: var(--color);
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.active-switcher-button {
|
||||
background-color: var(--active-bg-color) !important;
|
||||
color: var(--active-color) !important;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.topbar-textbox {
|
||||
margin: 0px 18px;
|
||||
font-size: 16px;
|
||||
color: var(--color);
|
||||
}
|
||||
|
||||
.topbar-menu {
|
||||
position: absolute;
|
||||
display: none;
|
||||
z-index: 10000;
|
||||
background-color: var(--bg-color);
|
||||
border-radius: 2px;
|
||||
border: 2px solid var(--border-color);
|
||||
border-top: none;
|
||||
align-items: flex-start;
|
||||
max-height: 80%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.topbar-separator {
|
||||
width: 1px;
|
||||
height: 20px;
|
||||
background-color: var(--border-color);
|
||||
}
|
||||
|
||||
.searchbox {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 200px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin: auto;
|
||||
width: 150px;
|
||||
height: 30px;
|
||||
padding: 5px;
|
||||
z-index: 1000;
|
||||
align-items: center;
|
||||
background-color: rgba(30 ,30, 30, 0.9);
|
||||
border: 2px solid var(--border-color);
|
||||
border-radius: 5px;
|
||||
display: flex;
|
||||
|
||||
}
|
||||
.searchbox input {
|
||||
text-align: center;
|
||||
width: 100px;
|
||||
margin-left: 10px;
|
||||
background-color: var(--muted-bg-color);
|
||||
color: var(--active-color);
|
||||
font-size: 20px;
|
||||
border: none;
|
||||
outline: none;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border: 4px solid rgba(255, 255, 255, 0.6);
|
||||
border-top: 4px solid var(--active-bg-color);
|
||||
border-radius: 50%;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
z-index: 1000;
|
||||
transform: translate(-50%, -50%);
|
||||
display: none;
|
||||
}
|
||||
|
||||
.legend {
|
||||
position: absolute;
|
||||
z-index: 3000;
|
||||
pointer-events: none;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.legend-toggle-switch {
|
||||
border-radius: 4px;
|
||||
margin-left: 10px;
|
||||
pointer-events: auto;
|
||||
}
|
||||
.legend-toggle-switch:hover {
|
||||
cursor: pointer;
|
||||
background-color: rgba(50, 50, 50, 0.5);
|
||||
}
|
||||
@ -1,166 +0,0 @@
|
||||
if (!window.Table) {
|
||||
class Table {
|
||||
constructor(width, height, headings, widths, alignments, position, draggable = false,
|
||||
tableBackgroundColor, borderColor, borderWidth, textColors, backgroundColors) {
|
||||
this.container = document.createElement('div')
|
||||
this.callbackName = null
|
||||
this.borderColor = borderColor
|
||||
this.borderWidth = borderWidth
|
||||
|
||||
if (draggable) {
|
||||
this.container.style.position = 'absolute'
|
||||
this.container.style.cursor = 'move'
|
||||
} else {
|
||||
this.container.style.position = 'relative'
|
||||
this.container.style.float = position
|
||||
}
|
||||
this.container.style.zIndex = '2000'
|
||||
this.reSize(width, height)
|
||||
this.container.style.display = 'flex'
|
||||
this.container.style.flexDirection = 'column'
|
||||
// this.container.style.justifyContent = 'space-between'
|
||||
|
||||
this.container.style.borderRadius = '5px'
|
||||
this.container.style.color = 'white'
|
||||
this.container.style.fontSize = '12px'
|
||||
this.container.style.fontVariantNumeric = 'tabular-nums'
|
||||
|
||||
this.table = document.createElement('table')
|
||||
this.table.style.width = '100%'
|
||||
this.table.style.borderCollapse = 'collapse'
|
||||
this.container.style.overflow = 'hidden'
|
||||
|
||||
this.rows = {}
|
||||
|
||||
this.headings = headings
|
||||
this.widths = widths.map((width) => `${width * 100}%`)
|
||||
this.alignments = alignments
|
||||
|
||||
let head = this.table.createTHead()
|
||||
let row = head.insertRow()
|
||||
|
||||
for (let i = 0; i < this.headings.length; i++) {
|
||||
let th = document.createElement('th')
|
||||
th.textContent = this.headings[i]
|
||||
th.style.width = this.widths[i]
|
||||
th.style.letterSpacing = '0.03rem'
|
||||
th.style.padding = '0.2rem 0px'
|
||||
th.style.fontWeight = '500'
|
||||
th.style.textAlign = 'center'
|
||||
if (i !== 0) th.style.borderLeft = borderWidth+'px solid '+borderColor
|
||||
th.style.position = 'sticky'
|
||||
th.style.top = '0'
|
||||
th.style.backgroundColor = backgroundColors.length > 0 ? backgroundColors[i] : tableBackgroundColor
|
||||
th.style.color = textColors[i]
|
||||
row.appendChild(th)
|
||||
}
|
||||
|
||||
let overflowWrapper = document.createElement('div')
|
||||
overflowWrapper.style.overflowY = 'auto'
|
||||
overflowWrapper.style.overflowX = 'hidden'
|
||||
overflowWrapper.style.backgroundColor = tableBackgroundColor
|
||||
overflowWrapper.appendChild(this.table)
|
||||
this.container.appendChild(overflowWrapper)
|
||||
document.getElementById('wrapper').appendChild(this.container)
|
||||
|
||||
if (!draggable) return
|
||||
|
||||
let offsetX, offsetY;
|
||||
|
||||
this.onMouseDown = (event) => {
|
||||
offsetX = event.clientX - this.container.offsetLeft;
|
||||
offsetY = event.clientY - this.container.offsetTop;
|
||||
|
||||
document.addEventListener('mousemove', onMouseMove);
|
||||
document.addEventListener('mouseup', onMouseUp);
|
||||
}
|
||||
|
||||
let onMouseMove = (event) => {
|
||||
this.container.style.left = (event.clientX - offsetX) + 'px';
|
||||
this.container.style.top = (event.clientY - offsetY) + 'px';
|
||||
}
|
||||
|
||||
let onMouseUp = () => {
|
||||
// Remove the event listeners for dragging
|
||||
document.removeEventListener('mousemove', onMouseMove);
|
||||
document.removeEventListener('mouseup', onMouseUp);
|
||||
}
|
||||
|
||||
this.container.addEventListener('mousedown', this.onMouseDown);
|
||||
|
||||
|
||||
}
|
||||
|
||||
divToButton(div, callbackString) {
|
||||
div.addEventListener('mouseover', () => div.style.backgroundColor = 'rgba(60, 60, 60, 0.6)')
|
||||
div.addEventListener('mouseout', () => div.style.backgroundColor = 'transparent')
|
||||
div.addEventListener('mousedown', () => div.style.backgroundColor = 'rgba(60, 60, 60)')
|
||||
div.addEventListener('click', () => window.callbackFunction(callbackString))
|
||||
div.addEventListener('mouseup', () => div.style.backgroundColor = 'rgba(60, 60, 60, 0.6)')
|
||||
}
|
||||
|
||||
newRow(id, returnClickedCell=false) {
|
||||
let row = this.table.insertRow()
|
||||
row.style.cursor = 'default'
|
||||
|
||||
for (let i = 0; i < this.headings.length; i++) {
|
||||
let cell = row.insertCell()
|
||||
cell.style.width = this.widths[i];
|
||||
cell.style.textAlign = this.alignments[i];
|
||||
cell.style.border = this.borderWidth+'px solid '+this.borderColor
|
||||
if (returnClickedCell) {
|
||||
this.divToButton(cell, `${this.callbackName}_~_${id};;;${this.headings[i]}`)
|
||||
}
|
||||
row[this.headings[i]] = cell
|
||||
}
|
||||
if (!returnClickedCell) {
|
||||
this.divToButton(row, `${this.callbackName}_~_${id}`)
|
||||
}
|
||||
this.rows[id] = row
|
||||
}
|
||||
|
||||
deleteRow(id) {
|
||||
this.table.deleteRow(this.rows[id].rowIndex)
|
||||
delete this.rows[id]
|
||||
}
|
||||
|
||||
clearRows() {
|
||||
let numRows = Object.keys(this.rows).length
|
||||
for (let i = 0; i < numRows; i++)
|
||||
this.table.deleteRow(-1)
|
||||
this.rows = {}
|
||||
}
|
||||
|
||||
updateCell(rowId, column, val) {
|
||||
this.rows[rowId][column].textContent = val
|
||||
}
|
||||
|
||||
makeSection(id, type, numBoxes, func=false) {
|
||||
let section = document.createElement('div')
|
||||
section.style.display = 'flex'
|
||||
section.style.width = '100%'
|
||||
section.style.padding = '3px 0px'
|
||||
section.style.backgroundColor = 'rgb(30, 30, 30)'
|
||||
type === 'footer' ? this.container.appendChild(section) : this.container.prepend(section)
|
||||
|
||||
this[type] = []
|
||||
for (let i = 0; i < numBoxes; i++) {
|
||||
let textBox = document.createElement('div')
|
||||
section.appendChild(textBox)
|
||||
textBox.style.flex = '1'
|
||||
textBox.style.textAlign = 'center'
|
||||
if (func) {
|
||||
this.divToButton(textBox, `${id}_~_${i}`)
|
||||
textBox.style.borderRadius = '2px'
|
||||
}
|
||||
this[type].push(textBox)
|
||||
}
|
||||
}
|
||||
|
||||
reSize(width, height) {
|
||||
this.container.style.width = width <= 1 ? width * 100 + '%' : width + 'px'
|
||||
this.container.style.height = height <= 1 ? height * 100 + '%' : height + 'px'
|
||||
}
|
||||
}
|
||||
window.Table = Table
|
||||
}
|
||||
27
lightweight_charts/js/test.html
Normal file
27
lightweight_charts/js/test.html
Normal file
@ -0,0 +1,27 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="">
|
||||
<head>
|
||||
<title>lightweight-charts-python</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
<script type="importmap">
|
||||
{
|
||||
"imports": { "lightweight-charts": "./lightweight-charts.js" }
|
||||
}
|
||||
</script>
|
||||
<meta name="viewport" content ="width=device-width, initial-scale=1">
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu,
|
||||
Cantarell, "Helvetica Neue", sans-serif;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="container"></div>
|
||||
<script type="module" src="./bundle.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -1,736 +0,0 @@
|
||||
if (!window.ToolBox) {
|
||||
class ToolBox {
|
||||
constructor(chart) {
|
||||
this.onTrendSelect = this.onTrendSelect.bind(this)
|
||||
this.onHorzSelect = this.onHorzSelect.bind(this)
|
||||
this.onRaySelect = this.onRaySelect.bind(this)
|
||||
this.saveDrawings = this.saveDrawings.bind(this)
|
||||
|
||||
this.chart = chart
|
||||
this.drawings = []
|
||||
this.chart.cursor = 'default'
|
||||
this.makingDrawing = false
|
||||
this.mouseDown = false
|
||||
|
||||
this.hoverBackgroundColor = 'rgba(80, 86, 94, 0.7)'
|
||||
this.clickBackgroundColor = 'rgba(90, 106, 104, 0.7)'
|
||||
|
||||
this.elem = this.makeToolBox()
|
||||
this.subscribeHoverMove()
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
// Exclude the chart attribute from serialization
|
||||
const {chart, ...serialized} = this;
|
||||
return serialized;
|
||||
}
|
||||
|
||||
makeToolBox() {
|
||||
let toolBoxElem = document.createElement('div')
|
||||
toolBoxElem.style.position = 'absolute'
|
||||
toolBoxElem.style.zIndex = '2000'
|
||||
toolBoxElem.style.display = 'flex'
|
||||
toolBoxElem.style.alignItems = 'center'
|
||||
toolBoxElem.style.top = '25%'
|
||||
toolBoxElem.style.border = '2px solid '+pane.borderColor
|
||||
toolBoxElem.style.borderLeft = 'none'
|
||||
toolBoxElem.style.borderTopRightRadius = '4px'
|
||||
toolBoxElem.style.borderBottomRightRadius = '4px'
|
||||
toolBoxElem.style.backgroundColor = 'rgba(25, 27, 30, 0.5)'
|
||||
toolBoxElem.style.flexDirection = 'column'
|
||||
|
||||
this.chart.activeIcon = null
|
||||
|
||||
let trend = this.makeToolBoxElement(this.onTrendSelect, 'KeyT', `<rect x="3.84" y="13.67" transform="matrix(0.7071 -0.7071 0.7071 0.7071 -5.9847 14.4482)" width="21.21" height="1.56"/><path d="M23,3.17L20.17,6L23,8.83L25.83,6L23,3.17z M23,7.41L21.59,6L23,4.59L24.41,6L23,7.41z"/><path d="M6,20.17L3.17,23L6,25.83L8.83,23L6,20.17z M6,24.41L4.59,23L6,21.59L7.41,23L6,24.41z"/>`)
|
||||
let horz = this.makeToolBoxElement(this.onHorzSelect, 'KeyH', `<rect x="4" y="14" width="9" height="1"/><rect x="16" y="14" width="9" height="1"/><path d="M11.67,14.5l2.83,2.83l2.83-2.83l-2.83-2.83L11.67,14.5z M15.91,14.5l-1.41,1.41l-1.41-1.41l1.41-1.41L15.91,14.5z"/>`)
|
||||
let ray = this.makeToolBoxElement(this.onRaySelect, 'KeyR', `<rect x="8" y="14" width="17" height="1"/><path d="M3.67,14.5l2.83,2.83l2.83-2.83L6.5,11.67L3.67,14.5z M7.91,14.5L6.5,15.91L5.09,14.5l1.41-1.41L7.91,14.5z"/>`)
|
||||
// let testB = this.makeToolBoxElement(this.onTrendSelect, 'KeyB', `<rect x="8" y="6" width="12" height="1"/><rect x="9" y="22" width="11" height="1"/><path d="M3.67,6.5L6.5,9.33L9.33,6.5L6.5,3.67L3.67,6.5z M7.91,6.5L6.5,7.91L5.09,6.5L6.5,5.09L7.91,6.5z"/><path d="M19.67,6.5l2.83,2.83l2.83-2.83L22.5,3.67L19.67,6.5z M23.91,6.5L22.5,7.91L21.09,6.5l1.41-1.41L23.91,6.5z"/><path d="M19.67,22.5l2.83,2.83l2.83-2.83l-2.83-2.83L19.67,22.5z M23.91,22.5l-1.41,1.41l-1.41-1.41l1.41-1.41L23.91,22.5z"/><path d="M3.67,22.5l2.83,2.83l2.83-2.83L6.5,19.67L3.67,22.5z M7.91,22.5L6.5,23.91L5.09,22.5l1.41-1.41L7.91,22.5z"/><rect x="22" y="9" width="1" height="11"/><rect x="6" y="9" width="1" height="11"/>`)
|
||||
toolBoxElem.appendChild(trend)
|
||||
toolBoxElem.appendChild(horz)
|
||||
toolBoxElem.appendChild(ray)
|
||||
// toolBoxElem.appendChild(testB)
|
||||
|
||||
this.chart.div.append(toolBoxElem)
|
||||
|
||||
let commandZHandler = (toDelete) => {
|
||||
if (!toDelete) return
|
||||
if ('price' in toDelete && toDelete.id !== 'toolBox') return commandZHandler(this.drawings[this.drawings.indexOf(toDelete) - 1])
|
||||
this.deleteDrawing(toDelete)
|
||||
}
|
||||
this.chart.commandFunctions.push((event) => {
|
||||
if ((event.metaKey || event.ctrlKey) && event.code === 'KeyZ') {
|
||||
commandZHandler(lastBar(this.drawings))
|
||||
return true
|
||||
}
|
||||
});
|
||||
|
||||
return toolBoxElem
|
||||
}
|
||||
|
||||
makeToolBoxElement(action, keyCmd, paths) {
|
||||
let elem = document.createElement('div')
|
||||
elem.style.margin = '3px'
|
||||
elem.style.borderRadius = '4px'
|
||||
elem.style.display = 'flex'
|
||||
|
||||
let svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
||||
svg.setAttribute("width", "29");
|
||||
svg.setAttribute("height", "29");
|
||||
|
||||
let group = document.createElementNS("http://www.w3.org/2000/svg", "g");
|
||||
group.innerHTML = paths
|
||||
group.setAttribute("fill", pane.color)
|
||||
|
||||
svg.appendChild(group)
|
||||
elem.appendChild(svg);
|
||||
|
||||
let icon = {elem: elem, action: action}
|
||||
|
||||
elem.addEventListener('mouseenter', () => {
|
||||
elem.style.backgroundColor = icon === this.chart.activeIcon ? pane.activeBackgroundColor : this.hoverBackgroundColor
|
||||
})
|
||||
elem.addEventListener('mouseleave', () => {
|
||||
elem.style.backgroundColor = icon === this.chart.activeIcon ? pane.activeBackgroundColor : 'transparent'
|
||||
})
|
||||
elem.addEventListener('mousedown', () => {
|
||||
elem.style.backgroundColor = icon === this.chart.activeIcon ? pane.activeBackgroundColor : this.clickBackgroundColor
|
||||
})
|
||||
elem.addEventListener('mouseup', () => {
|
||||
elem.style.backgroundColor = icon === this.chart.activeIcon ? pane.activeBackgroundColor : 'transparent'
|
||||
})
|
||||
elem.addEventListener('click', () => {
|
||||
if (this.chart.activeIcon) {
|
||||
this.chart.activeIcon.elem.style.backgroundColor = 'transparent'
|
||||
group.setAttribute("fill", pane.color)
|
||||
document.body.style.cursor = 'crosshair'
|
||||
this.chart.cursor = 'crosshair'
|
||||
this.chart.activeIcon.action(false)
|
||||
if (this.chart.activeIcon === icon) {
|
||||
return this.chart.activeIcon = null
|
||||
}
|
||||
}
|
||||
this.chart.activeIcon = icon
|
||||
group.setAttribute("fill", pane.activeColor)
|
||||
elem.style.backgroundColor = pane.activeBackgroundColor
|
||||
document.body.style.cursor = 'crosshair'
|
||||
this.chart.cursor = 'crosshair'
|
||||
this.chart.activeIcon.action(true)
|
||||
})
|
||||
this.chart.commandFunctions.push((event) => {
|
||||
|
||||
if (this.chart !== window.selectedChart) {
|
||||
return
|
||||
}
|
||||
|
||||
if (event.altKey && event.code === keyCmd) {
|
||||
event.preventDefault()
|
||||
if (this.chart.activeIcon) {
|
||||
this.chart.activeIcon.elem.style.backgroundColor = 'transparent'
|
||||
group.setAttribute("fill", pane.color)
|
||||
document.body.style.cursor = 'crosshair'
|
||||
this.chart.cursor = 'crosshair'
|
||||
this.chart.activeIcon.action(false)
|
||||
}
|
||||
this.chart.activeIcon = icon
|
||||
group.setAttribute("fill", pane.activeColor)
|
||||
elem.style.backgroundColor = pane.activeBackgroundColor
|
||||
document.body.style.cursor = 'crosshair'
|
||||
this.chart.cursor = 'crosshair'
|
||||
this.chart.activeIcon.action(true)
|
||||
return true
|
||||
}
|
||||
})
|
||||
return elem
|
||||
}
|
||||
|
||||
removeActiveAndSave() {
|
||||
document.body.style.cursor = 'default'
|
||||
this.chart.cursor = 'default'
|
||||
this.chart.activeIcon.elem.style.backgroundColor = 'transparent'
|
||||
this.chart.activeIcon = null
|
||||
this.saveDrawings()
|
||||
}
|
||||
|
||||
onTrendSelect(toggle, ray = false) {
|
||||
let trendLine = null
|
||||
let firstTime = null
|
||||
let firstPrice = null
|
||||
let currentTime = null
|
||||
|
||||
if (!toggle) {
|
||||
return this.chart.chart.unsubscribeClick(this.clickHandler)
|
||||
}
|
||||
let crosshairHandlerTrend = (param) => {
|
||||
this.chart.chart.unsubscribeCrosshairMove(crosshairHandlerTrend)
|
||||
|
||||
if (!this.makingDrawing) return
|
||||
|
||||
currentTime = this.chart.chart.timeScale().coordinateToTime(param.point.x)
|
||||
if (!currentTime) {
|
||||
let barsToMove = param.logical - this.chart.data.length-1
|
||||
currentTime = lastBar(this.chart.data).time+(barsToMove*this.chart.interval)
|
||||
}
|
||||
let currentPrice = this.chart.series.coordinateToPrice(param.point.y)
|
||||
|
||||
if (!currentTime) return this.chart.chart.subscribeCrosshairMove(crosshairHandlerTrend)
|
||||
trendLine.calculateAndSet(firstTime, firstPrice, currentTime, currentPrice)
|
||||
|
||||
setTimeout(() => {
|
||||
this.chart.chart.subscribeCrosshairMove(crosshairHandlerTrend)
|
||||
}, 10);
|
||||
}
|
||||
|
||||
this.clickHandler = (param) => {
|
||||
if (!this.makingDrawing) {
|
||||
this.makingDrawing = true
|
||||
trendLine = new TrendLine(this.chart, 'rgb(15, 139, 237)', ray)
|
||||
firstPrice = this.chart.series.coordinateToPrice(param.point.y)
|
||||
firstTime = !ray ? this.chart.chart.timeScale().coordinateToTime(param.point.x) : lastBar(this.chart.data).time
|
||||
this.chart.chart.applyOptions({handleScroll: false})
|
||||
this.chart.chart.subscribeCrosshairMove(crosshairHandlerTrend)
|
||||
}
|
||||
else {
|
||||
this.chart.chart.applyOptions({handleScroll: true})
|
||||
this.makingDrawing = false
|
||||
trendLine.line.setMarkers([])
|
||||
this.drawings.push(trendLine)
|
||||
this.chart.chart.unsubscribeCrosshairMove(crosshairHandlerTrend)
|
||||
this.chart.chart.unsubscribeClick(this.clickHandler)
|
||||
this.removeActiveAndSave()
|
||||
}
|
||||
}
|
||||
this.chart.chart.subscribeClick(this.clickHandler)
|
||||
}
|
||||
|
||||
onHorzSelect(toggle) {
|
||||
let clickHandlerHorz = (param) => {
|
||||
let price = this.chart.series.coordinateToPrice(param.point.y)
|
||||
let lineStyle = LightweightCharts.LineStyle.Solid
|
||||
let line = new HorizontalLine(this.chart, 'toolBox', price,'red', 2, lineStyle, true)
|
||||
this.drawings.push(line)
|
||||
this.chart.chart.unsubscribeClick(clickHandlerHorz)
|
||||
this.removeActiveAndSave()
|
||||
}
|
||||
if (toggle) this.chart.chart.subscribeClick(clickHandlerHorz)
|
||||
else this.chart.chart.unsubscribeClick(clickHandlerHorz)
|
||||
}
|
||||
|
||||
onRaySelect(toggle) {
|
||||
this.onTrendSelect(toggle, true)
|
||||
}
|
||||
|
||||
subscribeHoverMove() {
|
||||
let hoveringOver = null
|
||||
let x, y
|
||||
let colorPicker = new ColorPicker(this.saveDrawings)
|
||||
let stylePicker = new StylePicker(this.saveDrawings)
|
||||
|
||||
let onClickDelete = () => this.deleteDrawing(contextMenu.drawing)
|
||||
let onClickColor = (rect) => colorPicker.openMenu(rect, contextMenu.drawing)
|
||||
let onClickStyle = (rect) => stylePicker.openMenu(rect, contextMenu.drawing)
|
||||
let contextMenu = new ContextMenu()
|
||||
contextMenu.menuItem('Color Picker', onClickColor, () =>{
|
||||
document.removeEventListener('click', colorPicker.closeMenu)
|
||||
colorPicker.container.style.display = 'none'
|
||||
})
|
||||
contextMenu.menuItem('Style', onClickStyle, () => {
|
||||
document.removeEventListener('click', stylePicker.closeMenu)
|
||||
stylePicker.container.style.display = 'none'
|
||||
})
|
||||
contextMenu.separator()
|
||||
contextMenu.menuItem('Delete Drawing', onClickDelete)
|
||||
|
||||
let hoverOver = (param) => {
|
||||
if (!param.point || this.makingDrawing) return
|
||||
this.chart.chart.unsubscribeCrosshairMove(hoverOver)
|
||||
x = param.point.x
|
||||
y = param.point.y
|
||||
|
||||
this.drawings.forEach((drawing) => {
|
||||
let boundaryConditional
|
||||
let horizontal = false
|
||||
|
||||
if ('price' in drawing) {
|
||||
horizontal = true
|
||||
let priceCoordinate = this.chart.series.priceToCoordinate(drawing.price)
|
||||
boundaryConditional = Math.abs(priceCoordinate - param.point.y) < 6
|
||||
} else {
|
||||
let trendData = param.seriesData.get(drawing.line);
|
||||
if (!trendData) return
|
||||
let priceCoordinate = this.chart.series.priceToCoordinate(trendData.value)
|
||||
let timeCoordinate = this.chart.chart.timeScale().timeToCoordinate(trendData.time)
|
||||
boundaryConditional = Math.abs(priceCoordinate - param.point.y) < 6 && Math.abs(timeCoordinate - param.point.x) < 6
|
||||
}
|
||||
|
||||
if (boundaryConditional) {
|
||||
if (hoveringOver === drawing) return
|
||||
if (!horizontal && !drawing.ray) drawing.line.setMarkers(drawing.markers)
|
||||
document.body.style.cursor = 'pointer'
|
||||
document.addEventListener('mousedown', checkForClick)
|
||||
document.addEventListener('mouseup', checkForRelease)
|
||||
hoveringOver = drawing
|
||||
contextMenu.listen(true)
|
||||
contextMenu.drawing = drawing
|
||||
} else if (hoveringOver === drawing) {
|
||||
if (!horizontal && !drawing.ray) drawing.line.setMarkers([])
|
||||
document.body.style.cursor = this.chart.cursor
|
||||
hoveringOver = null
|
||||
contextMenu.listen(false)
|
||||
if (!this.mouseDown) {
|
||||
document.removeEventListener('mousedown', checkForClick)
|
||||
document.removeEventListener('mouseup', checkForRelease)
|
||||
}
|
||||
}
|
||||
})
|
||||
this.chart.chart.subscribeCrosshairMove(hoverOver)
|
||||
}
|
||||
let originalIndex
|
||||
let originalTime
|
||||
let originalPrice
|
||||
this.mouseDown = false
|
||||
let clickedEnd = false
|
||||
let labelColor
|
||||
let checkForClick = (event) => {
|
||||
this.mouseDown = true
|
||||
document.body.style.cursor = 'grabbing'
|
||||
this.chart.chart.applyOptions({handleScroll: false})
|
||||
this.chart.chart.timeScale().applyOptions({shiftVisibleRangeOnNewBar: false})
|
||||
|
||||
this.chart.chart.unsubscribeCrosshairMove(hoverOver)
|
||||
|
||||
labelColor = this.chart.chart.options().crosshair.horzLine.labelBackgroundColor
|
||||
this.chart.chart.applyOptions({crosshair: {horzLine: {labelBackgroundColor: hoveringOver.color}}})
|
||||
if ('price' in hoveringOver) {
|
||||
originalPrice = hoveringOver.price
|
||||
this.chart.chart.subscribeCrosshairMove(crosshairHandlerHorz)
|
||||
} else if (Math.abs(this.chart.chart.timeScale().timeToCoordinate(hoveringOver.from[0]) - x) < 4 && !hoveringOver.ray) {
|
||||
clickedEnd = 'first'
|
||||
this.chart.chart.subscribeCrosshairMove(crosshairHandlerTrend)
|
||||
} else if (Math.abs(this.chart.chart.timeScale().timeToCoordinate(hoveringOver.to[0]) - x) < 4 && !hoveringOver.ray) {
|
||||
clickedEnd = 'last'
|
||||
this.chart.chart.subscribeCrosshairMove(crosshairHandlerTrend)
|
||||
} else {
|
||||
originalPrice = this.chart.series.coordinateToPrice(y)
|
||||
originalTime = this.chart.chart.timeScale().coordinateToTime(x * this.chart.scale.width)
|
||||
this.chart.chart.subscribeCrosshairMove(checkForDrag)
|
||||
}
|
||||
originalIndex = this.chart.chart.timeScale().coordinateToLogical(x)
|
||||
document.removeEventListener('mousedown', checkForClick)
|
||||
}
|
||||
let checkForRelease = (event) => {
|
||||
this.mouseDown = false
|
||||
document.body.style.cursor = this.chart.cursor
|
||||
|
||||
this.chart.chart.applyOptions({handleScroll: true})
|
||||
this.chart.chart.timeScale().applyOptions({shiftVisibleRangeOnNewBar: true})
|
||||
this.chart.chart.applyOptions({crosshair: {horzLine: {labelBackgroundColor: labelColor}}})
|
||||
if (hoveringOver && 'price' in hoveringOver && hoveringOver.id !== 'toolBox') {
|
||||
window.callbackFunction(`${hoveringOver.id}_~_${hoveringOver.price.toFixed(8)}`);
|
||||
}
|
||||
hoveringOver = null
|
||||
document.removeEventListener('mousedown', checkForClick)
|
||||
document.removeEventListener('mouseup', checkForRelease)
|
||||
this.chart.chart.subscribeCrosshairMove(hoverOver)
|
||||
this.saveDrawings()
|
||||
}
|
||||
let checkForDrag = (param) => {
|
||||
if (!param.point) return
|
||||
this.chart.chart.unsubscribeCrosshairMove(checkForDrag)
|
||||
if (!this.mouseDown) return
|
||||
|
||||
let priceAtCursor = this.chart.series.coordinateToPrice(param.point.y)
|
||||
|
||||
let priceDiff = priceAtCursor - originalPrice
|
||||
let barsToMove = param.logical - originalIndex
|
||||
|
||||
let startBarIndex = this.chart.data.findIndex(item => item.time === hoveringOver.from[0])
|
||||
let endBarIndex = this.chart.data.findIndex(item => item.time === hoveringOver.to[0])
|
||||
|
||||
let startDate
|
||||
let endBar
|
||||
if (hoveringOver.ray) {
|
||||
endBar = this.chart.data[startBarIndex + barsToMove]
|
||||
startDate = hoveringOver.to[0]
|
||||
} else {
|
||||
startDate = this.chart.data[startBarIndex + barsToMove].time
|
||||
endBar = endBarIndex === -1 ? null : this.chart.data[endBarIndex + barsToMove]
|
||||
}
|
||||
|
||||
let endDate = endBar ? endBar.time : hoveringOver.to[0] + (barsToMove * this.chart.interval)
|
||||
let startValue = hoveringOver.from[1] + priceDiff
|
||||
let endValue = hoveringOver.to[1] + priceDiff
|
||||
|
||||
hoveringOver.calculateAndSet(startDate, startValue, endDate, endValue)
|
||||
|
||||
originalIndex = param.logical
|
||||
originalPrice = priceAtCursor
|
||||
this.chart.chart.subscribeCrosshairMove(checkForDrag)
|
||||
}
|
||||
let crosshairHandlerTrend = (param) => {
|
||||
if (!param.point) return
|
||||
this.chart.chart.unsubscribeCrosshairMove(crosshairHandlerTrend)
|
||||
if (!this.mouseDown) return
|
||||
|
||||
let currentPrice = this.chart.series.coordinateToPrice(param.point.y)
|
||||
let currentTime = this.chart.chart.timeScale().coordinateToTime(param.point.x)
|
||||
|
||||
let [firstTime, firstPrice] = [null, null]
|
||||
if (clickedEnd === 'last') {
|
||||
firstTime = hoveringOver.from[0]
|
||||
firstPrice = hoveringOver.from[1]
|
||||
} else if (clickedEnd === 'first') {
|
||||
firstTime = hoveringOver.to[0]
|
||||
firstPrice = hoveringOver.to[1]
|
||||
}
|
||||
|
||||
if (!currentTime) {
|
||||
let barsToMove = param.logical - this.chart.data.length-1
|
||||
currentTime = lastBar(this.chart.data).time + (barsToMove*this.chart.interval)
|
||||
}
|
||||
|
||||
hoveringOver.calculateAndSet(firstTime, firstPrice, currentTime, currentPrice)
|
||||
|
||||
setTimeout(() => {
|
||||
this.chart.chart.subscribeCrosshairMove(crosshairHandlerTrend)
|
||||
}, 10);
|
||||
}
|
||||
let crosshairHandlerHorz = (param) => {
|
||||
if (!param.point) return
|
||||
this.chart.chart.unsubscribeCrosshairMove(crosshairHandlerHorz)
|
||||
if (!this.mouseDown) return
|
||||
hoveringOver.updatePrice(this.chart.series.coordinateToPrice(param.point.y))
|
||||
setTimeout(() => {
|
||||
this.chart.chart.subscribeCrosshairMove(crosshairHandlerHorz)
|
||||
}, 10)
|
||||
}
|
||||
this.chart.chart.subscribeCrosshairMove(hoverOver)
|
||||
}
|
||||
|
||||
renderDrawings() {
|
||||
if (this.mouseDown) return
|
||||
this.drawings.forEach((item) => {
|
||||
if ('price' in item) return
|
||||
let startDate = Math.round(item.from[0]/this.chart.interval)*this.chart.interval
|
||||
let endDate = Math.round(item.to[0]/this.chart.interval)*this.chart.interval
|
||||
item.calculateAndSet(startDate, item.from[1], endDate, item.to[1])
|
||||
})
|
||||
}
|
||||
|
||||
deleteDrawing(drawing) {
|
||||
if ('price' in drawing) {
|
||||
this.chart.series.removePriceLine(drawing.line)
|
||||
}
|
||||
else {
|
||||
let range = this.chart.chart.timeScale().getVisibleLogicalRange()
|
||||
|
||||
this.chart.chart.timeScale().applyOptions({shiftVisibleRangeOnNewBar: false})
|
||||
this.chart.chart.removeSeries(drawing.line);
|
||||
this.chart.chart.timeScale().applyOptions({shiftVisibleRangeOnNewBar: true})
|
||||
|
||||
this.chart.chart.timeScale().setVisibleLogicalRange(range)
|
||||
}
|
||||
this.drawings.splice(this.drawings.indexOf(drawing), 1)
|
||||
this.saveDrawings()
|
||||
}
|
||||
|
||||
clearDrawings() {
|
||||
this.drawings.forEach((item) => {
|
||||
if ('price' in item) this.chart.series.removePriceLine(item.line)
|
||||
else this.chart.chart.removeSeries(item.line)
|
||||
})
|
||||
this.drawings = []
|
||||
}
|
||||
|
||||
saveDrawings() {
|
||||
let drawingsString = JSON.stringify(this.drawings, (key, value) => {
|
||||
if (key === '' && Array.isArray(value)) {
|
||||
return value.filter(item => !(item && typeof item === 'object' && 'priceLine' in item && item.id !== 'toolBox'));
|
||||
} else if (key === 'line' || (value && typeof value === 'object' && 'priceLine' in value && value.id !== 'toolBox')) {
|
||||
return undefined;
|
||||
}
|
||||
return value;
|
||||
});
|
||||
window.callbackFunction(`save_drawings${this.chart.id}_~_${drawingsString}`)
|
||||
}
|
||||
|
||||
loadDrawings(drawings) {
|
||||
this.drawings = []
|
||||
drawings.forEach((item) => {
|
||||
let drawing = null
|
||||
if ('price' in item) {
|
||||
drawing = new HorizontalLine(this.chart, 'toolBox',
|
||||
item.priceLine.price, item.priceLine.color, 2,
|
||||
item.priceLine.lineStyle, item.priceLine.axisLabelVisible)
|
||||
}
|
||||
else {
|
||||
let startDate = Math.round((item.from[0]/this.chart.interval)*this.chart.interval)
|
||||
let endDate = Math.round((item.to[0]/this.chart.interval)*this.chart.interval)
|
||||
|
||||
drawing = new TrendLine(this.chart, item.color, item.ray)
|
||||
drawing.calculateAndSet(startDate, item.from[1], endDate, item.to[1])
|
||||
}
|
||||
this.drawings.push(drawing)
|
||||
})
|
||||
}
|
||||
}
|
||||
window.ToolBox = ToolBox
|
||||
|
||||
|
||||
class TrendLine {
|
||||
constructor(chart, color, ray) {
|
||||
this.calculateAndSet = this.calculateAndSet.bind(this)
|
||||
|
||||
this.line = chart.chart.addLineSeries({
|
||||
color: color,
|
||||
lineWidth: 2,
|
||||
lastValueVisible: false,
|
||||
priceLineVisible: false,
|
||||
crosshairMarkerVisible: false,
|
||||
autoscaleInfoProvider: () => ({
|
||||
priceRange: {
|
||||
minValue: 1_000_000_000,
|
||||
maxValue: 0,
|
||||
},
|
||||
}),
|
||||
})
|
||||
this.color = color
|
||||
this.markers = null
|
||||
this.data = null
|
||||
this.from = null
|
||||
this.to = null
|
||||
this.ray = ray
|
||||
this.chart = chart
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
// Exclude the chart attribute from serialization
|
||||
const {chart, ...serialized} = this;
|
||||
return serialized;
|
||||
}
|
||||
|
||||
calculateAndSet(firstTime, firstPrice, currentTime, currentPrice) {
|
||||
let data = calculateTrendLine(firstTime, firstPrice, currentTime, currentPrice, this.chart, this.ray)
|
||||
this.from = [data[0].time, data[0].value]
|
||||
this.to = [data[data.length - 1].time, data[data.length-1].value]
|
||||
|
||||
this.chart.chart.timeScale().applyOptions({shiftVisibleRangeOnNewBar: false})
|
||||
let logical = this.chart.chart.timeScale().getVisibleLogicalRange()
|
||||
|
||||
this.line.setData(data)
|
||||
|
||||
this.chart.chart.timeScale().applyOptions({shiftVisibleRangeOnNewBar: true})
|
||||
this.chart.chart.timeScale().setVisibleLogicalRange(logical)
|
||||
|
||||
if (!this.ray) {
|
||||
this.markers = [
|
||||
{time: this.from[0], position: 'inBar', color: '#1E80F0', shape: 'circle', size: 0.1},
|
||||
{time: this.to[0], position: 'inBar', color: '#1E80F0', shape: 'circle', size: 0.1}
|
||||
]
|
||||
this.line.setMarkers(this.markers)
|
||||
}
|
||||
}
|
||||
}
|
||||
window.TrendLine = TrendLine
|
||||
|
||||
class ColorPicker {
|
||||
constructor(saveDrawings) {
|
||||
this.saveDrawings = saveDrawings
|
||||
|
||||
this.container = document.createElement('div')
|
||||
this.container.style.maxWidth = '170px'
|
||||
this.container.style.backgroundColor = pane.backgroundColor
|
||||
this.container.style.position = 'absolute'
|
||||
this.container.style.zIndex = '10000'
|
||||
this.container.style.display = 'none'
|
||||
this.container.style.flexDirection = 'column'
|
||||
this.container.style.alignItems = 'center'
|
||||
this.container.style.border = '2px solid '+pane.borderColor
|
||||
this.container.style.borderRadius = '8px'
|
||||
this.container.style.cursor = 'default'
|
||||
|
||||
let colorPicker = document.createElement('div')
|
||||
colorPicker.style.margin = '10px'
|
||||
colorPicker.style.display = 'flex'
|
||||
colorPicker.style.flexWrap = 'wrap'
|
||||
|
||||
let colors = [
|
||||
'#EBB0B0','#E9CEA1','#E5DF80','#ADEB97','#A3C3EA','#D8BDED',
|
||||
'#E15F5D','#E1B45F','#E2D947','#4BE940','#639AE1','#D7A0E8',
|
||||
'#E42C2A','#E49D30','#E7D827','#3CFF0A','#3275E4','#B06CE3',
|
||||
'#F3000D','#EE9A14','#F1DA13','#2DFC0F','#1562EE','#BB00EF',
|
||||
'#B50911','#E3860E','#D2BD11','#48DE0E','#1455B4','#6E009F',
|
||||
'#7C1713','#B76B12','#8D7A13','#479C12','#165579','#51007E',
|
||||
]
|
||||
|
||||
colors.forEach((color) => colorPicker.appendChild(this.makeColorBox(color)))
|
||||
|
||||
let separator = document.createElement('div')
|
||||
separator.style.backgroundColor = pane.borderColor
|
||||
separator.style.height = '1px'
|
||||
separator.style.width = '130px'
|
||||
|
||||
let opacity = document.createElement('div')
|
||||
opacity.style.margin = '10px'
|
||||
|
||||
let opacityText = document.createElement('div')
|
||||
opacityText.style.color = 'lightgray'
|
||||
opacityText.style.fontSize = '12px'
|
||||
opacityText.innerText = 'Opacity'
|
||||
|
||||
let opacityValue = document.createElement('div')
|
||||
opacityValue.style.color = 'lightgray'
|
||||
opacityValue.style.fontSize = '12px'
|
||||
|
||||
let opacitySlider = document.createElement('input')
|
||||
opacitySlider.type = 'range'
|
||||
opacitySlider.value = this.opacity*100
|
||||
opacityValue.innerText = opacitySlider.value+'%'
|
||||
opacitySlider.oninput = () => {
|
||||
opacityValue.innerText = opacitySlider.value+'%'
|
||||
this.opacity = opacitySlider.value/100
|
||||
this.updateColor()
|
||||
}
|
||||
|
||||
opacity.appendChild(opacityText)
|
||||
opacity.appendChild(opacitySlider)
|
||||
opacity.appendChild(opacityValue)
|
||||
|
||||
this.container.appendChild(colorPicker)
|
||||
this.container.appendChild(separator)
|
||||
this.container.appendChild(opacity)
|
||||
document.getElementById('wrapper').appendChild(this.container)
|
||||
|
||||
}
|
||||
makeColorBox(color) {
|
||||
let box = document.createElement('div')
|
||||
box.style.width = '18px'
|
||||
box.style.height = '18px'
|
||||
box.style.borderRadius = '3px'
|
||||
box.style.margin = '3px'
|
||||
box.style.boxSizing = 'border-box'
|
||||
box.style.backgroundColor = color
|
||||
|
||||
box.addEventListener('mouseover', (event) => box.style.border = '2px solid lightgray')
|
||||
box.addEventListener('mouseout', (event) => box.style.border = 'none')
|
||||
|
||||
let rgbValues = this.extractRGB(color)
|
||||
|
||||
box.addEventListener('click', (event) => {
|
||||
this.rgbValues = rgbValues
|
||||
this.updateColor()
|
||||
})
|
||||
return box
|
||||
}
|
||||
extractRGB = (anyColor) => {
|
||||
let dummyElem = document.createElement('div');
|
||||
dummyElem.style.color = anyColor;
|
||||
document.body.appendChild(dummyElem);
|
||||
let computedColor = getComputedStyle(dummyElem).color;
|
||||
document.body.removeChild(dummyElem);
|
||||
let colorValues = computedColor.match(/\d+/g).map(Number);
|
||||
let isRgba = computedColor.includes('rgba');
|
||||
let opacity = isRgba ? parseFloat(computedColor.split(',')[3]) : 1
|
||||
return [colorValues[0], colorValues[1], colorValues[2], opacity]
|
||||
}
|
||||
updateColor() {
|
||||
let oColor = `rgba(${this.rgbValues[0]}, ${this.rgbValues[1]}, ${this.rgbValues[2]}, ${this.opacity})`
|
||||
if ('price' in this.drawing) this.drawing.updateColor(oColor)
|
||||
else {
|
||||
this.drawing.color = oColor
|
||||
this.drawing.line.applyOptions({color: oColor})
|
||||
}
|
||||
this.saveDrawings()
|
||||
}
|
||||
openMenu(rect, drawing) {
|
||||
this.drawing = drawing
|
||||
this.rgbValues = this.extractRGB(drawing.color)
|
||||
this.opacity = parseFloat(this.rgbValues[3])
|
||||
this.container.style.top = (rect.top-30)+'px'
|
||||
this.container.style.left = rect.right+'px'
|
||||
this.container.style.display = 'flex'
|
||||
setTimeout(() => document.addEventListener('mousedown', (event) => {
|
||||
if (!this.container.contains(event.target)) {
|
||||
this.closeMenu()
|
||||
}
|
||||
}), 10)
|
||||
}
|
||||
closeMenu(event) {
|
||||
document.removeEventListener('click', this.closeMenu)
|
||||
this.container.style.display = 'none'
|
||||
}
|
||||
}
|
||||
window.ColorPicker = ColorPicker
|
||||
class StylePicker {
|
||||
constructor(saveDrawings) {
|
||||
this.saveDrawings = saveDrawings
|
||||
|
||||
this.container = document.createElement('div')
|
||||
this.container.style.position = 'absolute'
|
||||
this.container.style.zIndex = '10000'
|
||||
this.container.style.background = 'rgb(50, 50, 50)'
|
||||
this.container.style.color = pane.activeColor
|
||||
this.container.style.display = 'none'
|
||||
this.container.style.borderRadius = '5px'
|
||||
this.container.style.padding = '3px 3px'
|
||||
this.container.style.fontSize = '13px'
|
||||
this.container.style.cursor = 'default'
|
||||
|
||||
let styles = [
|
||||
{name: 'Solid', var: LightweightCharts.LineStyle.Solid},
|
||||
{name: 'Dotted', var: LightweightCharts.LineStyle.Dotted},
|
||||
{name: 'Dashed', var: LightweightCharts.LineStyle.Dashed},
|
||||
{name: 'Large Dashed', var: LightweightCharts.LineStyle.LargeDashed},
|
||||
{name: 'Sparse Dotted', var: LightweightCharts.LineStyle.SparseDotted},
|
||||
]
|
||||
styles.forEach((style) => {
|
||||
this.container.appendChild(this.makeTextBox(style.name, style.var))
|
||||
})
|
||||
|
||||
document.getElementById('wrapper').appendChild(this.container)
|
||||
|
||||
}
|
||||
makeTextBox(text, style) {
|
||||
let item = document.createElement('span')
|
||||
item.style.display = 'flex'
|
||||
item.style.alignItems = 'center'
|
||||
item.style.justifyContent = 'space-between'
|
||||
item.style.padding = '2px 10px'
|
||||
item.style.margin = '1px 0px'
|
||||
item.style.borderRadius = '3px'
|
||||
item.innerText = text
|
||||
|
||||
item.addEventListener('mouseover', (event) => item.style.backgroundColor = pane.mutedBackgroundColor)
|
||||
item.addEventListener('mouseout', (event) => item.style.backgroundColor = 'transparent')
|
||||
|
||||
item.addEventListener('click', (event) => {
|
||||
this.style = style
|
||||
this.updateStyle()
|
||||
})
|
||||
return item
|
||||
}
|
||||
|
||||
updateStyle() {
|
||||
if ('price' in this.drawing) this.drawing.updateStyle(this.style)
|
||||
else {
|
||||
this.drawing.line.applyOptions({lineStyle: this.style})
|
||||
}
|
||||
this.saveDrawings()
|
||||
}
|
||||
openMenu(rect, drawing) {
|
||||
this.drawing = drawing
|
||||
this.container.style.top = (rect.top-30)+'px'
|
||||
this.container.style.left = rect.right+'px'
|
||||
this.container.style.display = 'block'
|
||||
setTimeout(() => document.addEventListener('mousedown', (event) => {
|
||||
if (!this.container.contains(event.target)) {
|
||||
this.closeMenu()
|
||||
}
|
||||
}), 10)
|
||||
}
|
||||
closeMenu(event) {
|
||||
document.removeEventListener('click', this.closeMenu)
|
||||
this.container.style.display = 'none'
|
||||
}
|
||||
}
|
||||
window.StylePicker = StylePicker
|
||||
}
|
||||
@ -1,5 +1,5 @@
|
||||
import random
|
||||
from typing import Union
|
||||
from typing import Union, Optional, Callable
|
||||
|
||||
from .util import jbool, Pane, NUM
|
||||
|
||||
@ -10,8 +10,8 @@ class Section(Pane):
|
||||
self._table = table
|
||||
self.type = section_type
|
||||
|
||||
def __call__(self, number_of_text_boxes: int, func: callable = None):
|
||||
if func:
|
||||
def __call__(self, number_of_text_boxes: int, func: Optional[Callable] = None):
|
||||
if func is not None:
|
||||
self.win.handlers[self.id] = lambda boxId: func(self._table, int(boxId))
|
||||
self.run_script(f'''
|
||||
{self._table.id}.makeSection("{self.id}", "{self.type}", {number_of_text_boxes}, {"true" if func else ""})
|
||||
@ -34,7 +34,8 @@ class Row(dict):
|
||||
|
||||
def __setitem__(self, column, value):
|
||||
if isinstance(column, tuple):
|
||||
return [self.__setitem__(col, val) for col, val in zip(column, value)]
|
||||
[self.__setitem__(col, val) for col, val in zip(column, value)]
|
||||
return
|
||||
original_value = value
|
||||
if column in self._table._formatters:
|
||||
value = self._table._formatters[column].replace(self._table.VALUE, str(value))
|
||||
@ -46,23 +47,42 @@ class Row(dict):
|
||||
def text_color(self, column, color): self._style('textColor', column, color)
|
||||
|
||||
def _style(self, style, column, arg):
|
||||
self.run_script(f"{self._table.id}.rows[{self.id}]['{column}'].style.{style} = '{arg}'")
|
||||
self.run_script(f"{self._table.id}.styleCell({self.id}, '{column}', '{style}', '{arg}')")
|
||||
|
||||
def delete(self):
|
||||
self.run_script(f"{self._table.id}.deleteRow('{self.id}')")
|
||||
self._table.pop(self.id)
|
||||
|
||||
# # TODO this might be useful in abschart
|
||||
# def _call(self, method_name: str, *args):
|
||||
# new_args = []
|
||||
# for arg in args:
|
||||
# if isinstance(arg, str):
|
||||
# arg = f"'{arg}'"
|
||||
# new_args.append(arg)
|
||||
# self.run_script(f"{self._table.id}.{method_name}({', '.join(new_args)})")
|
||||
|
||||
|
||||
class Table(Pane, dict):
|
||||
VALUE = 'CELL__~__VALUE__~__PLACEHOLDER'
|
||||
|
||||
def __init__(
|
||||
self, window, width: NUM, height: NUM, headings: tuple, widths: tuple = None,
|
||||
alignments: tuple = None, position='left', draggable: bool = False,
|
||||
background_color: str = '#121417', border_color: str = 'rgb(70, 70, 70)',
|
||||
border_width: int = 1, heading_text_colors: tuple = None,
|
||||
heading_background_colors: tuple = None, return_clicked_cells: bool = False,
|
||||
func: callable = None
|
||||
self,
|
||||
window,
|
||||
width: NUM,
|
||||
height: NUM,
|
||||
headings: tuple,
|
||||
widths: Optional[tuple] = None,
|
||||
alignments: Optional[tuple] = None,
|
||||
position='left',
|
||||
draggable: bool = False,
|
||||
background_color: str = '#121417',
|
||||
border_color: str = 'rgb(70, 70, 70)',
|
||||
border_width: int = 1,
|
||||
heading_text_colors: Optional[tuple] = None,
|
||||
heading_background_colors: Optional[tuple] = None,
|
||||
return_clicked_cells: bool = False,
|
||||
func: Optional[Callable] = None
|
||||
):
|
||||
dict.__init__(self)
|
||||
Pane.__init__(self, window)
|
||||
@ -75,17 +95,20 @@ class Table(Pane, dict):
|
||||
self.win.handlers[self.id] = lambda rId: func(self[rId])
|
||||
self.return_clicked_cells = return_clicked_cells
|
||||
|
||||
headings = list(headings)
|
||||
widths = list(widths) if widths else []
|
||||
alignments = list(alignments) if alignments else []
|
||||
heading_text_colors = list(heading_text_colors) if heading_text_colors else []
|
||||
heading_background_colors = list(heading_background_colors) if heading_background_colors else []
|
||||
|
||||
self.run_script(f'''
|
||||
{self.id} = new Table(
|
||||
{width}, {height}, {headings}, {widths}, {alignments}, '{position}', {jbool(draggable)},
|
||||
'{background_color}', '{border_color}', {border_width}, {heading_text_colors},
|
||||
{heading_background_colors}
|
||||
{width},
|
||||
{height},
|
||||
{list(headings)},
|
||||
{list(widths) if widths else []},
|
||||
{list(alignments) if alignments else []},
|
||||
'{position}',
|
||||
{jbool(draggable)},
|
||||
'{background_color}',
|
||||
'{border_color}',
|
||||
{border_width},
|
||||
{list(heading_text_colors) if heading_text_colors else []},
|
||||
{list(heading_background_colors) if heading_background_colors else []}
|
||||
)''')
|
||||
self.run_script(f'{self.id}.callbackName = "{self.id}"') if func else None
|
||||
self.footer = Section(self, 'footer')
|
||||
|
||||
@ -3,14 +3,12 @@ import json
|
||||
|
||||
class ToolBox:
|
||||
def __init__(self, chart):
|
||||
from lightweight_charts.abstract import JS
|
||||
self.run_script = chart.run_script
|
||||
self.id = chart.id
|
||||
self._save_under = None
|
||||
self.drawings = {}
|
||||
chart.win.handlers[f'save_drawings{self.id}'] = self._save_drawings
|
||||
self.run_script(JS['toolbox'])
|
||||
self.run_script(f'{self.id}.toolBox = new ToolBox({self.id})')
|
||||
self.run_script(f'{self.id}.createToolBox()')
|
||||
|
||||
def save_drawings_under(self, widget: 'Widget'):
|
||||
"""
|
||||
@ -24,7 +22,7 @@ class ToolBox:
|
||||
"""
|
||||
if not self.drawings.get(tag):
|
||||
return
|
||||
self.run_script(f'if ("toolBox" in {self.id}) {self.id}.toolBox.loadDrawings({json.dumps(self.drawings[tag])})')
|
||||
self.run_script(f'if ({self.id}.toolBox) {self.id}.toolBox.loadDrawings({json.dumps(self.drawings[tag])})')
|
||||
|
||||
def import_drawings(self, file_path):
|
||||
"""
|
||||
|
||||
@ -8,7 +8,7 @@ ALIGN = Literal['left', 'right']
|
||||
|
||||
|
||||
class Widget(Pane):
|
||||
def __init__(self, topbar, value, func=None):
|
||||
def __init__(self, topbar, value, func: callable = None):
|
||||
super().__init__(topbar.win)
|
||||
self.value = value
|
||||
|
||||
@ -75,10 +75,8 @@ class TopBar(Pane):
|
||||
def _create(self):
|
||||
if self._created:
|
||||
return
|
||||
from lightweight_charts.abstract import JS
|
||||
self._created = True
|
||||
self.run_script(JS['callback'])
|
||||
self.run_script(f'{self.id} = new TopBar({self._chart.id})')
|
||||
self.run_script(f'{self.id} = {self._chart.id}.createTopBar()')
|
||||
|
||||
def __getitem__(self, item):
|
||||
if widget := self._widgets.get(item):
|
||||
|
||||
@ -3,6 +3,7 @@ import json
|
||||
from datetime import datetime
|
||||
from random import choices
|
||||
from typing import Literal, Union
|
||||
from numpy import isin
|
||||
import pandas as pd
|
||||
|
||||
|
||||
@ -19,7 +20,7 @@ class Pane:
|
||||
class IDGen(list):
|
||||
ascii = 'abcdefghijklmnopqrstuvwxyz'
|
||||
|
||||
def generate(self):
|
||||
def generate(self) -> str:
|
||||
var = ''.join(choices(self.ascii, k=8))
|
||||
if var not in self:
|
||||
self.append(var)
|
||||
@ -44,6 +45,21 @@ def js_data(data: Union[pd.DataFrame, pd.Series]):
|
||||
return json.dumps(filtered_records, indent=2)
|
||||
|
||||
|
||||
def snake_to_camel(s: str):
|
||||
components = s.split('_')
|
||||
return components[0] + ''.join(x.title() for x in components[1:])
|
||||
|
||||
def js_json(d: dict):
|
||||
filtered_dict = {}
|
||||
for key, val in d.items():
|
||||
if key in ('self') or val in (None,):
|
||||
continue
|
||||
if '_' in key:
|
||||
key = snake_to_camel(key)
|
||||
filtered_dict[key] = val
|
||||
return f"JSON.parse('{json.dumps(filtered_dict)}')"
|
||||
|
||||
|
||||
def jbool(b: bool): return 'true' if b is True else 'false' if b is False else None
|
||||
|
||||
|
||||
@ -53,32 +69,27 @@ MARKER_POSITION = Literal['above', 'below', 'inside']
|
||||
|
||||
MARKER_SHAPE = Literal['arrow_up', 'arrow_down', 'circle', 'square']
|
||||
|
||||
CROSSHAIR_MODE = Literal['normal', 'magnet']
|
||||
CROSSHAIR_MODE = Literal['normal', 'magnet', 'hidden']
|
||||
|
||||
PRICE_SCALE_MODE = Literal['normal', 'logarithmic', 'percentage', 'index100']
|
||||
|
||||
TIME = Union[datetime, pd.Timestamp, str]
|
||||
TIME = Union[datetime, pd.Timestamp, str, float]
|
||||
|
||||
NUM = Union[float, int]
|
||||
|
||||
FLOAT = Literal['left', 'right', 'top', 'bottom']
|
||||
|
||||
|
||||
def line_style(line: LINE_STYLE):
|
||||
js = 'LightweightCharts.LineStyle.'
|
||||
return js+line[:line.index('_')].title() + line[line.index('_') + 1:].title() if '_' in line else js+line.title()
|
||||
|
||||
|
||||
def crosshair_mode(mode: CROSSHAIR_MODE):
|
||||
return f'LightweightCharts.CrosshairMode.{mode.title()}' if mode else None
|
||||
|
||||
|
||||
def price_scale_mode(mode: PRICE_SCALE_MODE):
|
||||
return f"LightweightCharts.PriceScaleMode.{'IndexedTo100' if mode == 'index100' else mode.title() if mode else None}"
|
||||
def as_enum(value, string_types):
|
||||
types = string_types.__args__
|
||||
return -1 if value not in types else types.index(value)
|
||||
|
||||
|
||||
def marker_shape(shape: MARKER_SHAPE):
|
||||
return shape[:shape.index('_')]+shape[shape.index('_')+1:].title() if '_' in shape else shape
|
||||
return {
|
||||
'arrow_up': 'arrowUp',
|
||||
'arrow_down': 'arrowDown',
|
||||
}.get(shape) or shape
|
||||
|
||||
|
||||
def marker_position(p: MARKER_POSITION):
|
||||
@ -86,8 +97,7 @@ def marker_position(p: MARKER_POSITION):
|
||||
'above': 'aboveBar',
|
||||
'below': 'belowBar',
|
||||
'inside': 'inBar',
|
||||
None: None,
|
||||
}[p]
|
||||
}.get(p)
|
||||
|
||||
|
||||
class Emitter:
|
||||
@ -127,12 +137,10 @@ class JSEmitter:
|
||||
class Events:
|
||||
def __init__(self, chart):
|
||||
self.new_bar = Emitter()
|
||||
from lightweight_charts.abstract import JS
|
||||
self.search = JSEmitter(chart, f'search{chart.id}',
|
||||
lambda o: chart.run_script(f'''
|
||||
{JS['callback']}
|
||||
makeSpinner({chart.id})
|
||||
{chart.id}.search = makeSearchBox({chart.id})
|
||||
Handler.makeSpinner({chart.id})
|
||||
{chart.id}.search = Handler.makeSearchBox({chart.id})
|
||||
''')
|
||||
)
|
||||
self.range_change = JSEmitter(chart, f'range_change{chart.id}',
|
||||
|
||||
Reference in New Issue
Block a user