drawings can be placed on any series, reimplement jupyter, implement editable text boxes, allow for whitespace data within charts if they are NaN values, fix legend bug
This commit is contained in:
@ -20,6 +20,8 @@ Time can be given in the index rather than a column, and volume can be omitted i
|
|||||||
If `keep_drawings` is `True`, any drawings made using the `toolbox` will be redrawn with the new data. This is designed to be used when switching to a different timeframe of the same symbol.
|
If `keep_drawings` is `True`, any drawings made using the `toolbox` will be redrawn with the new data. This is designed to be used when switching to a different timeframe of the same symbol.
|
||||||
|
|
||||||
`None` can also be given, which will erase all candle and volume data displayed on the chart.
|
`None` can also be given, which will erase all candle and volume data displayed on the chart.
|
||||||
|
|
||||||
|
You can also add columns to color the candles (https://tradingview.github.io/lightweight-charts/tutorials/customization/data-points)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -316,6 +316,56 @@ class SeriesCommon(Pane):
|
|||||||
"""
|
"""
|
||||||
return HorizontalLine(self, price, color, width, style, text, axis_label_visible, func)
|
return HorizontalLine(self, price, color, width, style, text, axis_label_visible, func)
|
||||||
|
|
||||||
|
def trend_line(
|
||||||
|
self,
|
||||||
|
start_time: TIME,
|
||||||
|
start_value: NUM,
|
||||||
|
end_time: TIME,
|
||||||
|
end_value: NUM,
|
||||||
|
round: bool = False,
|
||||||
|
line_color: str = '#1E80F0',
|
||||||
|
width: int = 2,
|
||||||
|
style: LINE_STYLE = 'solid',
|
||||||
|
) -> TwoPointDrawing:
|
||||||
|
return TrendLine(*locals().values())
|
||||||
|
|
||||||
|
def box(
|
||||||
|
self,
|
||||||
|
start_time: TIME,
|
||||||
|
start_value: NUM,
|
||||||
|
end_time: TIME,
|
||||||
|
end_value: NUM,
|
||||||
|
round: bool = False,
|
||||||
|
color: str = '#1E80F0',
|
||||||
|
fill_color: str = 'rgba(255, 255, 255, 0.2)',
|
||||||
|
width: int = 2,
|
||||||
|
style: LINE_STYLE = 'solid',
|
||||||
|
) -> TwoPointDrawing:
|
||||||
|
return Box(*locals().values())
|
||||||
|
|
||||||
|
def ray_line(
|
||||||
|
self,
|
||||||
|
start_time: TIME,
|
||||||
|
value: NUM,
|
||||||
|
round: bool = False,
|
||||||
|
color: str = '#1E80F0',
|
||||||
|
width: int = 2,
|
||||||
|
style: LINE_STYLE = 'solid',
|
||||||
|
text: str = ''
|
||||||
|
) -> RayLine:
|
||||||
|
# TODO
|
||||||
|
return RayLine(*locals().values())
|
||||||
|
|
||||||
|
def vertical_line(
|
||||||
|
self,
|
||||||
|
time: TIME,
|
||||||
|
color: str = '#1E80F0',
|
||||||
|
width: int = 2,
|
||||||
|
style: LINE_STYLE ='solid',
|
||||||
|
text: str = ''
|
||||||
|
) -> VerticalLine:
|
||||||
|
return VerticalLine(*locals().values())
|
||||||
|
|
||||||
def clear_markers(self):
|
def clear_markers(self):
|
||||||
"""
|
"""
|
||||||
Clears the markers displayed on the data.\n
|
Clears the markers displayed on the data.\n
|
||||||
@ -494,7 +544,6 @@ class Candlestick(SeriesCommon):
|
|||||||
df = self._df_datetime_format(df)
|
df = self._df_datetime_format(df)
|
||||||
self.candle_data = df.copy()
|
self.candle_data = df.copy()
|
||||||
self._last_bar = df.iloc[-1]
|
self._last_bar = df.iloc[-1]
|
||||||
|
|
||||||
self.run_script(f'{self.id}.series.setData({js_data(df)})')
|
self.run_script(f'{self.id}.series.setData({js_data(df)})')
|
||||||
|
|
||||||
if 'volume' not in df:
|
if 'volume' not in df:
|
||||||
@ -690,56 +739,6 @@ class AbstractChart(Candlestick, Pane):
|
|||||||
"""
|
"""
|
||||||
return self._lines.copy()
|
return self._lines.copy()
|
||||||
|
|
||||||
def trend_line(
|
|
||||||
self,
|
|
||||||
start_time: TIME,
|
|
||||||
start_value: NUM,
|
|
||||||
end_time: TIME,
|
|
||||||
end_value: NUM,
|
|
||||||
round: bool = False,
|
|
||||||
line_color: str = '#1E80F0',
|
|
||||||
width: int = 2,
|
|
||||||
style: LINE_STYLE = 'solid',
|
|
||||||
) -> TwoPointDrawing:
|
|
||||||
return TrendLine(*locals().values())
|
|
||||||
|
|
||||||
def box(
|
|
||||||
self,
|
|
||||||
start_time: TIME,
|
|
||||||
start_value: NUM,
|
|
||||||
end_time: TIME,
|
|
||||||
end_value: NUM,
|
|
||||||
round: bool = False,
|
|
||||||
color: str = '#1E80F0',
|
|
||||||
fill_color: str = 'rgba(255, 255, 255, 0.2)',
|
|
||||||
width: int = 2,
|
|
||||||
style: LINE_STYLE = 'solid',
|
|
||||||
) -> TwoPointDrawing:
|
|
||||||
return Box(*locals().values())
|
|
||||||
|
|
||||||
def ray_line(
|
|
||||||
self,
|
|
||||||
start_time: TIME,
|
|
||||||
value: NUM,
|
|
||||||
round: bool = False,
|
|
||||||
color: str = '#1E80F0',
|
|
||||||
width: int = 2,
|
|
||||||
style: LINE_STYLE = 'solid',
|
|
||||||
text: str = ''
|
|
||||||
) -> RayLine:
|
|
||||||
# TODO
|
|
||||||
return RayLine(*locals().values())
|
|
||||||
|
|
||||||
def vertical_line(
|
|
||||||
self,
|
|
||||||
time: TIME,
|
|
||||||
color: str = '#1E80F0',
|
|
||||||
width: int = 2,
|
|
||||||
style: LINE_STYLE ='solid',
|
|
||||||
text: str = ''
|
|
||||||
) -> VerticalLine:
|
|
||||||
return VerticalLine(*locals().values())
|
|
||||||
|
|
||||||
def set_visible_range(self, start_time: TIME, end_time: TIME):
|
def set_visible_range(self, start_time: TIME, end_time: TIME):
|
||||||
self.run_script(f'''
|
self.run_script(f'''
|
||||||
{self.id}.chart.timeScale().setVisibleRange({{
|
{self.id}.chart.timeScale().setVisibleRange({{
|
||||||
@ -776,7 +775,13 @@ class AbstractChart(Candlestick, Pane):
|
|||||||
"""
|
"""
|
||||||
self.run_script(f"""
|
self.run_script(f"""
|
||||||
document.getElementById('container').style.backgroundColor = '{background_color}'
|
document.getElementById('container').style.backgroundColor = '{background_color}'
|
||||||
{self.id}.chart.applyOptions({{ layout: {js_json(locals())} }})""")
|
{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 ''}
|
||||||
|
}}}})""")
|
||||||
|
|
||||||
def grid(self, vert_enabled: bool = True, horz_enabled: bool = True,
|
def grid(self, vert_enabled: bool = True, horz_enabled: bool = True,
|
||||||
color: str = 'rgba(29, 30, 38, 5)', style: LINE_STYLE = 'solid'):
|
color: str = 'rgba(29, 30, 38, 5)', style: LINE_STYLE = 'solid'):
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@ -145,6 +145,12 @@ body {
|
|||||||
color: var(--color);
|
color: var(--color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.topbar-textbox-input {
|
||||||
|
background-color: var(--bg-color);
|
||||||
|
color: var(--color);
|
||||||
|
border: 1px solid var(--color);
|
||||||
|
}
|
||||||
|
|
||||||
.topbar-menu {
|
.topbar-menu {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
display: none;
|
display: none;
|
||||||
@ -214,7 +220,7 @@ body {
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
top: 10px;
|
top: 10px;
|
||||||
left: 10px;
|
left: 10px;
|
||||||
display: flex;
|
display: none;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
.legend-toggle-switch {
|
.legend-toggle-switch {
|
||||||
|
|||||||
@ -27,9 +27,12 @@ class Widget(Pane):
|
|||||||
|
|
||||||
|
|
||||||
class TextWidget(Widget):
|
class TextWidget(Widget):
|
||||||
def __init__(self, topbar, initial_text, align):
|
def __init__(self, topbar, initial_text, align, func):
|
||||||
super().__init__(topbar, value=initial_text)
|
super().__init__(topbar, value=initial_text, func=func)
|
||||||
self.run_script(f'{self.id} = {topbar.id}.makeTextBoxWidget("{initial_text}", "{align}")')
|
|
||||||
|
callback_name = f'"{self.id}"' if func else ''
|
||||||
|
|
||||||
|
self.run_script(f'{self.id} = {topbar.id}.makeTextBoxWidget("{initial_text}", "{align}", {callback_name})')
|
||||||
|
|
||||||
def set(self, string):
|
def set(self, string):
|
||||||
self.value = string
|
self.value = string
|
||||||
@ -115,9 +118,9 @@ class TopBar(Pane):
|
|||||||
self._widgets[name] = MenuWidget(self, options, default if default else options[0], separator, align, func)
|
self._widgets[name] = MenuWidget(self, options, default if default else options[0], separator, align, func)
|
||||||
|
|
||||||
def textbox(self, name: str, initial_text: str = '',
|
def textbox(self, name: str, initial_text: str = '',
|
||||||
align: ALIGN = 'left'):
|
align: ALIGN = 'left', func: callable = None):
|
||||||
self._create()
|
self._create()
|
||||||
self._widgets[name] = TextWidget(self, initial_text, align)
|
self._widgets[name] = TextWidget(self, initial_text, align, func)
|
||||||
|
|
||||||
def button(self, name, button_text: str, separator: bool = True,
|
def button(self, name, button_text: str, separator: bool = True,
|
||||||
align: ALIGN = 'left', toggle: bool = False, func: callable = None):
|
align: ALIGN = 'left', toggle: bool = False, func: callable = None):
|
||||||
|
|||||||
@ -39,7 +39,7 @@ def parse_event_message(window, string):
|
|||||||
def js_data(data: Union[pd.DataFrame, pd.Series]):
|
def js_data(data: Union[pd.DataFrame, pd.Series]):
|
||||||
if isinstance(data, pd.DataFrame):
|
if isinstance(data, pd.DataFrame):
|
||||||
d = data.to_dict(orient='records')
|
d = data.to_dict(orient='records')
|
||||||
filtered_records = [{k: v for k, v in record.items() if v is not None} for record in d]
|
filtered_records = [{k: v for k, v in record.items() if v is not None and not pd.isna(v)} for record in d]
|
||||||
else:
|
else:
|
||||||
d = data.to_dict()
|
d = data.to_dict()
|
||||||
filtered_records = {k: v for k, v in d.items()}
|
filtered_records = {k: v for k, v in d.items()}
|
||||||
|
|||||||
@ -167,19 +167,15 @@ class JupyterChart(StaticLWC):
|
|||||||
def __init__(self, width: int = 800, height=350, inner_width=1, inner_height=1, scale_candles_only: bool = False, toolbox: bool = False):
|
def __init__(self, width: int = 800, height=350, inner_width=1, inner_height=1, scale_candles_only: bool = False, toolbox: bool = False):
|
||||||
super().__init__(width, height, inner_width, inner_height, scale_candles_only, toolbox, False)
|
super().__init__(width, height, inner_width, inner_height, scale_candles_only, toolbox, False)
|
||||||
|
|
||||||
# this isn't available at the moment
|
|
||||||
|
|
||||||
raise ModuleNotFoundError('JupyterChart is unavailable in lightweight charts 2.0; please downgrade to an earlier version.')
|
|
||||||
|
|
||||||
self.run_script(f'''
|
self.run_script(f'''
|
||||||
for (var i = 0; i < document.getElementsByClassName("tv-lightweight-charts").length; i++) {{
|
for (var i = 0; i < document.getElementsByClassName("tv-lightweight-charts").length; i++) {{
|
||||||
var element = document.getElementsByClassName("tv-lightweight-charts")[i];
|
var element = document.getElementsByClassName("tv-lightweight-charts")[i];
|
||||||
element.style.overflow = "visible"
|
element.style.overflow = "visible"
|
||||||
}}
|
}}
|
||||||
document.getElementById('wrapper').style.overflow = 'hidden'
|
document.getElementById('container').style.overflow = 'hidden'
|
||||||
document.getElementById('wrapper').style.borderRadius = '10px'
|
document.getElementById('container').style.borderRadius = '10px'
|
||||||
document.getElementById('wrapper').style.width = '{self.width}px'
|
document.getElementById('container').style.width = '{self.width}px'
|
||||||
document.getElementById('wrapper').style.height = '100%'
|
document.getElementById('container').style.height = '100%'
|
||||||
''')
|
''')
|
||||||
self.run_script(f'{self.id}.chart.resize({width}, {height})')
|
self.run_script(f'{self.id}.chart.resize({width}, {height})')
|
||||||
|
|
||||||
|
|||||||
@ -106,13 +106,13 @@ export class Handler {
|
|||||||
|
|
||||||
// TODO definitely a better way to do this
|
// TODO definitely a better way to do this
|
||||||
if (this.scale.height === 0 || this.scale.width === 0) {
|
if (this.scale.height === 0 || this.scale.width === 0) {
|
||||||
this.legend.div.style.display = 'none'
|
// if (this.legend.div.style.display == 'flex') this.legend.div.style.display = 'none'
|
||||||
if (this.toolBox) {
|
if (this.toolBox) {
|
||||||
this.toolBox.div.style.display = 'none'
|
this.toolBox.div.style.display = 'none'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
this.legend.div.style.display = 'flex'
|
// this.legend.div.style.display = 'flex'
|
||||||
if (this.toolBox) {
|
if (this.toolBox) {
|
||||||
this.toolBox.div.style.display = 'flex'
|
this.toolBox.div.style.display = 'flex'
|
||||||
}
|
}
|
||||||
|
|||||||
@ -37,6 +37,7 @@ export class Legend {
|
|||||||
this.div = document.createElement('div');
|
this.div = document.createElement('div');
|
||||||
this.div.classList.add('legend');
|
this.div.classList.add('legend');
|
||||||
this.div.style.maxWidth = `${(handler.scale.width * 100) - 8}vw`
|
this.div.style.maxWidth = `${(handler.scale.width * 100) - 8}vw`
|
||||||
|
this.div.style.display = 'none';
|
||||||
|
|
||||||
this.text = document.createElement('span')
|
this.text = document.createElement('span')
|
||||||
this.text.style.lineHeight = '1.8'
|
this.text.style.lineHeight = '1.8'
|
||||||
|
|||||||
@ -145,6 +145,12 @@ body {
|
|||||||
color: var(--color);
|
color: var(--color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.topbar-textbox-input {
|
||||||
|
background-color: var(--bg-color);
|
||||||
|
color: var(--color);
|
||||||
|
border: 1px solid var(--color);
|
||||||
|
}
|
||||||
|
|
||||||
.topbar-menu {
|
.topbar-menu {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
display: none;
|
display: none;
|
||||||
@ -214,7 +220,7 @@ body {
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
top: 10px;
|
top: 10px;
|
||||||
left: 10px;
|
left: 10px;
|
||||||
display: flex;
|
display: none;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
.legend-toggle-switch {
|
.legend-toggle-switch {
|
||||||
|
|||||||
@ -78,13 +78,34 @@ export class TopBar {
|
|||||||
return widget
|
return widget
|
||||||
}
|
}
|
||||||
|
|
||||||
makeTextBoxWidget(text: string, align='left') {
|
makeTextBoxWidget(text: string, align='left', callbackName=null) {
|
||||||
|
if (callbackName) {
|
||||||
|
const textBox = document.createElement('input');
|
||||||
|
textBox.classList.add('topbar-textbox-input');
|
||||||
|
textBox.value = text
|
||||||
|
textBox.style.width = `${(textBox.value.length+2)}ch`
|
||||||
|
textBox.addEventListener('input', (e) => {
|
||||||
|
textBox.style.width = `${(textBox.value.length+2)}ch`;
|
||||||
|
});
|
||||||
|
textBox.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key == 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
textBox.blur();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
textBox.addEventListener('blur', () => {
|
||||||
|
window.callbackFunction(`${callbackName}_~_${textBox.value}`)
|
||||||
|
});
|
||||||
|
this.appendWidget(textBox, align, true)
|
||||||
|
return textBox
|
||||||
|
} else {
|
||||||
const textBox = document.createElement('div');
|
const textBox = document.createElement('div');
|
||||||
textBox.classList.add('topbar-textbox');
|
textBox.classList.add('topbar-textbox');
|
||||||
textBox.innerText = text
|
textBox.innerText = text
|
||||||
this.appendWidget(textBox, align, true)
|
this.appendWidget(textBox, align, true)
|
||||||
return textBox
|
return textBox
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
makeMenu(items: string[], activeItem: string, separator: boolean, callbackName: string, align: 'right'|'left') {
|
makeMenu(items: string[], activeItem: string, separator: boolean, callbackName: string, align: 'right'|'left') {
|
||||||
return new Menu(this.makeButton.bind(this), callbackName, items, activeItem, separator, align)
|
return new Menu(this.makeButton.bind(this), callbackName, items, activeItem, separator, align)
|
||||||
|
|||||||
Reference in New Issue
Block a user