Enhancements:

- added the `create_histogram` method and the `Histogram` object.
- added the `round` parameter to `trend_line` and `ray_line`
- chart.set can now be given line data.

Bug Fixes:
- `NaN` values can now be given when setting data, and will leave a blank space in the data.
- `resize` will now change the chart wrapper’s size as well as the chart itself.
This commit is contained in:
louisnw
2023-09-04 20:29:15 +01:00
parent 8532d48e5d
commit 555573b54b
8 changed files with 185 additions and 63 deletions

View File

@ -59,7 +59,19 @@ ___
Creates and returns a Line object, representing a `LineSeries` object in Lightweight Charts and can be used to create indicators. As well as the methods described below, the `Line` object also has access to: Creates and returns a Line object, representing a `LineSeries` object in Lightweight Charts and can be used to create indicators. As well as the methods described below, the `Line` object also has access to:
[`marker`](#marker), [`horizontal_line`](#AbstractChart.horizontal_line) [`hide_data`](#hide_data), [`show_data`](#show_data) and[`price_line`](#price_line). [`marker`](#marker), [`horizontal_line`](#AbstractChart.horizontal_line), [`hide_data`](#hide_data), [`show_data`](#show_data) and [`price_line`](#price_line).
Its instance should only be accessed from this method.
```
___
```{py:method} create_histogram(name: str, color: COLOR, price_line: bool, price_label: bool, scale_margin_top: float, scale_margin_bottom: float) -> Histogram
Creates and returns a Histogram object, representing a `HistogramSeries` object in Lightweight Charts and can be used to create indicators. As well as the methods described below, the object also has access to:
[`horizontal_line`](#AbstractChart.horizontal_line), [`hide_data`](#hide_data), [`show_data`](#show_data) and [`price_line`](#price_line).
Its instance should only be accessed from this method. Its instance should only be accessed from this method.
``` ```
@ -76,7 +88,7 @@ ___
```{py:method} trend_line(start_time: str | datetime, start_value: NUM, end_time: str | datetime, end_value: NUM, color: COLOR, width: int, style: LINE_STYLE) -> Line ```{py:method} trend_line(start_time: str | datetime, start_value: NUM, end_time: str | datetime, end_value: NUM, color: COLOR, width: int, style: LINE_STYLE, round: bool) -> Line
Creates a trend line, drawn from the first point (`start_time`, `start_value`) to the last point (`end_time`, `end_value`). Creates a trend line, drawn from the first point (`start_time`, `start_value`) to the last point (`end_time`, `end_value`).
@ -85,7 +97,7 @@ ___
```{py:method} ray_line(start_time: str | datetime, value: NUM, color: COLOR, width: int, style: LINE_STYLE) -> Line ```{py:method} ray_line(start_time: str | datetime, value: NUM, color: COLOR, width: int, style: LINE_STYLE, round: bool) -> Line
Creates a ray line, drawn from the first point (`start_time`, `value`) and onwards. Creates a ray line, drawn from the first point (`start_time`, `value`) and onwards.

View File

@ -0,0 +1,51 @@
# `Histogram`
````{py:class} Histogram(name: str, color: COLOR, style: LINE_STYLE, width: int, price_line: bool, price_label: bool)
The `Histogram` object represents a `HistogramSeries` object in Lightweight Charts and can be used to create indicators. As well as the methods described below, the `Line` object also has access to:
[`horizontal_line`](#AbstractChart.horizontal_line), [`hide_data`](#hide_data), [`show_data`](#show_data) and [`price_line`](#price_line).
Its instance should only be accessed from [`create_histogram`](#AbstractChart.create_histogram).
___
```{py:method} set(data: pd.DataFrame)
Sets the data for the histogram.
When a name has not been set upon declaration, the columns should be named: `time | value` (Not case sensitive).
The column containing the data should be named after the string given in the `name`.
A `color` column can be used within the dataframe to specify the color of individual bars.
```
___
```{py:method} update(series: pd.Series)
Updates the data for the histogram.
This should be given as a Series object, with labels akin to the `histogram.set` method.
```
___
```{py:method} scale(scale_margin_top: float, scale_margin_bottom: float)
Scales the margins of the histogram, as used within [`volume_config`](#AbstractChart.volume_config).
```
___
```{py:method} delete()
Irreversibly deletes the histogram.
```
````

View File

@ -4,6 +4,7 @@
:hidden: :hidden:
abstract_chart abstract_chart
line line
histogram
horizontal_line horizontal_line
charts charts
events events

View File

@ -5,9 +5,9 @@
The `Line` object represents a `LineSeries` object in Lightweight Charts and can be used to create indicators. As well as the methods described below, the `Line` object also has access to: The `Line` object represents a `LineSeries` object in Lightweight Charts and can be used to create indicators. As well as the methods described below, the `Line` object also has access to:
[`marker`](#marker), [`horizontal_line`](#AbstractChart.horizontal_line) [`hide_data`](#hide_data), [`show_data`](#show_data) and [`price_line`](#price_line). [`marker`](#marker), [`horizontal_line`](#AbstractChart.horizontal_line), [`hide_data`](#hide_data), [`show_data`](#show_data) and [`price_line`](#price_line).
Its instance should only be accessed from [create_line](#AbstractChart.create_line). Its instance should only be accessed from [`create_line`](#AbstractChart.create_line).
___ ___
@ -36,7 +36,7 @@ This should be given as a Series object, with labels akin to the `line.set()` fu
___ ___
```{py:method} line.delete() ```{py:method} delete()
Irreversibly deletes the line. Irreversibly deletes the line.

View File

@ -10,7 +10,7 @@ from .topbar import TopBar
from .util import ( from .util import (
IDGen, jbool, Pane, Events, TIME, NUM, FLOAT, 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,
line_style, marker_position, marker_shape, crosshair_mode, price_scale_mode, line_style, marker_position, marker_shape, crosshair_mode, price_scale_mode, js_data,
) )
JS = {} JS = {}
@ -106,11 +106,15 @@ class Window:
class SeriesCommon(Pane): class SeriesCommon(Pane):
def __init__(self, chart: 'AbstractChart'): def __init__(self, chart: 'AbstractChart', name: str = None):
super().__init__(chart.win) super().__init__(chart.win)
self._chart = chart self._chart = chart
self._interval = pd.Timedelta(seconds=1) if hasattr(chart, '_interval'):
self._interval = chart._interval
else:
self._interval = pd.Timedelta(seconds=1)
self._last_bar = None self._last_bar = None
self.name = name
self.num_decimals = 2 self.num_decimals = 2
def _set_interval(self, df: pd.DataFrame): def _set_interval(self, df: pd.DataFrame):
@ -155,12 +159,33 @@ class SeriesCommon(Pane):
return series return series
def _single_datetime_format(self, arg): def _single_datetime_format(self, arg):
if isinstance(arg, str) or not pd.api.types.is_datetime64_any_dtype(arg): if isinstance(arg, (str, int, float)) or not pd.api.types.is_datetime64_any_dtype(arg):
arg = pd.to_datetime(arg) arg = pd.to_datetime(arg)
interval_seconds = self._interval.total_seconds() interval_seconds = self._interval.total_seconds()
arg = interval_seconds * (arg.timestamp() // interval_seconds) arg = interval_seconds * (arg.timestamp() // interval_seconds)
return arg return arg
def set(self, df: pd.DataFrame = None, format_cols: bool = True):
if df is None or df.empty:
self.run_script(f'{self.id}.series.setData([])')
return
if format_cols:
df = self._df_datetime_format(df, exclude_lowercase=self.name)
if self.name:
if self.name not in df:
raise NameError(f'No column named "{self.name}".')
df = df.rename(columns={self.name: 'value'})
self._last_bar = df.iloc[-1]
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)
if self.name in series.index:
series.rename({self.name: 'value'}, inplace=True)
self._last_bar = series
self.run_script(f'{self.id}.series.update({js_data(series)})')
def marker(self, time: datetime = None, position: MARKER_POSITION = 'below', def marker(self, time: datetime = None, position: MARKER_POSITION = 'below',
shape: MARKER_SHAPE = 'arrow_up', color: str = '#2196F3', text: str = '' shape: MARKER_SHAPE = 'arrow_up', color: str = '#2196F3', text: str = ''
) -> str: ) -> str:
@ -349,9 +374,8 @@ class VerticalSpan(Pane):
class Line(SeriesCommon): class Line(SeriesCommon):
def __init__(self, chart, name, color, style, width, price_line, price_label, crosshair_marker=True): def __init__(self, chart, name, color, style, width, price_line, price_label, crosshair_marker=True):
super().__init__(chart) super().__init__(chart, name)
self.color = color self.color = color
self.name = name
self.run_script(f''' self.run_script(f'''
{self.id} = {{ {self.id} = {{
series: {chart.id}.chart.addLineSeries({{ series: {chart.id}.chart.addLineSeries({{
@ -374,7 +398,7 @@ class Line(SeriesCommon):
color: '{color}', color: '{color}',
precision: 2, precision: 2,
}} }}
''') null''')
def _push_to_legend(self): def _push_to_legend(self):
self.run_script(f''' self.run_script(f'''
@ -383,39 +407,18 @@ class Line(SeriesCommon):
{self._chart.id}.legend.lines.push({self._chart.id}.legend.makeLineRow({self.id})) {self._chart.id}.legend.lines.push({self._chart.id}.legend.makeLineRow({self.id}))
}}''') }}''')
def set(self, df: pd.DataFrame = None): def _set_trend(self, start_time, start_value, end_time, end_value, ray=False, round=False):
""" if round:
Sets the line data.\n start_time = self._single_datetime_format(start_time)
:param df: If the name parameter is not used, the columns should be named: date/time, value. end_time = self._single_datetime_format(end_time)
""" else:
if df is None or df.empty: start_time, end_time = pd.to_datetime((start_time, end_time)).astype('int64') // 10 ** 9
self.run_script(f'{self.id}.series.setData([])')
return
df = self._df_datetime_format(df, exclude_lowercase=self.name)
if self.name:
if self.name not in df:
raise NameError(f'No column named "{self.name}".')
df = df.rename(columns={self.name: 'value'})
self._last_bar = df.iloc[-1]
self.run_script(f'{self.id}.series.setData({df.to_dict("records")})')
def update(self, series: pd.Series):
"""
Updates the line data.\n
:param series: labels: date/time, value
"""
series = self._series_datetime_format(series, exclude_lowercase=self.name)
if self.name in series.index:
series.rename({self.name: 'value'}, inplace=True)
self._last_bar = series
self.run_script(f'{self.id}.series.update({series.to_dict()})')
def _set_trend(self, start_time, start_value, end_time, end_value, ray=False):
self.run_script(f''' self.run_script(f'''
{self._chart.id}.chart.timeScale().applyOptions({{shiftVisibleRangeOnNewBar: false}}) {self._chart.id}.chart.timeScale().applyOptions({{shiftVisibleRangeOnNewBar: false}})
{self.id}.series.setData( {self.id}.series.setData(
calculateTrendLine({pd.to_datetime(start_time).timestamp()}, {start_value}, calculateTrendLine({start_time}, {start_value},
{pd.to_datetime(end_time).timestamp()}, {end_value}, {end_time}, {end_value},
{self._chart._interval.total_seconds() * 1000}, {self._chart._interval.total_seconds() * 1000},
{self._chart.id}, {jbool(ray)})) {self._chart.id}, {jbool(ray)}))
{self._chart.id}.chart.timeScale().applyOptions({{shiftVisibleRangeOnNewBar: true}}) {self._chart.id}.chart.timeScale().applyOptions({{shiftVisibleRangeOnNewBar: true}})
@ -437,6 +440,44 @@ class Line(SeriesCommon):
''') ''')
class Histogram(SeriesCommon):
def __init__(self, chart, name, color, price_line, price_label, scale_margin_top, scale_margin_bottom):
super().__init__(chart, name)
self.color = color
self.run_script(f'''
{self.id} = {{
series: {chart.id}.chart.addHistogramSeries({{
color: '{color}',
lastValueVisible: {jbool(price_label)},
priceLineVisible: {jbool(price_line)},
priceScaleId: '{self.id}'
}}),
markers: [],
horizontal_lines: [],
name: '{name}',
color: '{color}',
precision: 2,
}}
{self.id}.series.priceScale().applyOptions({{
scaleMargins: {{top:{scale_margin_top}, bottom: {scale_margin_bottom}}}
}})''')
def delete(self):
"""
Irreversibly deletes the histogram.
"""
self.run_script(f'''
{self._chart.id}.chart.removeSeries({self.id}.series)
delete {self.id}
''')
def scale(self, scale_margin_top: float = 0.0, scale_margin_bottom: float = 0.0):
self.run_script(f'''
{self.id}.series.priceScale().applyOptions({{
scaleMargins: {{top: {scale_margin_top}, bottom: {scale_margin_bottom}}}
}})''')
class Candlestick(SeriesCommon): class Candlestick(SeriesCommon):
def __init__(self, chart: 'AbstractChart'): def __init__(self, chart: 'AbstractChart'):
super().__init__(chart) super().__init__(chart)
@ -462,8 +503,7 @@ class Candlestick(SeriesCommon):
self.candle_data = df.copy() self.candle_data = df.copy()
self._last_bar = df.iloc[-1] self._last_bar = df.iloc[-1]
bars = df.to_dict(orient='records') self.run_script(f'{self.id}.candleData = {js_data(df)}; {self.id}.series.setData({self.id}.candleData)')
self.run_script(f'{self.id}.candleData = {bars}; {self.id}.series.setData({self.id}.candleData)')
toolbox_action = 'clearDrawings' if not render_drawings else 'renderDrawings' 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"if ('toolBox' in {self._chart.id}) {self._chart.id}.toolBox.{toolbox_action}()")
if 'volume' not in df: if 'volume' not in df:
@ -471,11 +511,12 @@ class Candlestick(SeriesCommon):
volume = df.drop(columns=['open', 'high', 'low', 'close']).rename(columns={'volume': 'value'}) volume = df.drop(columns=['open', 'high', 'low', 'close']).rename(columns={'volume': 'value'})
volume['color'] = self._volume_down_color volume['color'] = self._volume_down_color
volume.loc[df['close'] > df['open'], 'color'] = self._volume_up_color volume.loc[df['close'] > df['open'], 'color'] = self._volume_up_color
self.run_script(f'{self.id}.volumeSeries.setData({volume.to_dict(orient="records")})') self.run_script(f'{self.id}.volumeSeries.setData({js_data(volume)})')
# for line in self._lines: for line in self._lines:
# if line.name in df.columns: if line.name not in df.columns:
# line.set() continue
line.set(df[['time', line.name]], format_cols=False)
def update(self, series: pd.Series, _from_tick=False): def update(self, series: pd.Series, _from_tick=False):
""" """
@ -489,10 +530,9 @@ class Candlestick(SeriesCommon):
self.candle_data = pd.concat([self.candle_data, series.to_frame().T], ignore_index=True) self.candle_data = pd.concat([self.candle_data, series.to_frame().T], ignore_index=True)
self._chart.events.new_bar._emit(self) self._chart.events.new_bar._emit(self)
self._last_bar = series self._last_bar = series
bar = js_data(series)
bar = series.to_dict()
self.run_script(f''' self.run_script(f'''
if (stampToDate(lastBar({self.id}.candleData).time).getTime() === stampToDate({bar['time']}).getTime()) {{ if (stampToDate(lastBar({self.id}.candleData).time).getTime() === stampToDate({series['time']}).getTime()) {{
{self.id}.candleData[{self.id}.candleData.length-1] = {bar} {self.id}.candleData[{self.id}.candleData.length-1] = {bar}
}} }}
else {self.id}.candleData.push({bar}) else {self.id}.candleData.push({bar})
@ -502,7 +542,7 @@ class Candlestick(SeriesCommon):
return return
volume = series.drop(['open', 'high', 'low', 'close']).rename({'volume': 'value'}) volume = series.drop(['open', 'high', 'low', 'close']).rename({'volume': 'value'})
volume['color'] = self._volume_up_color if series['close'] > series['open'] else self._volume_down_color volume['color'] = self._volume_up_color if series['close'] > series['open'] else self._volume_down_color
self.run_script(f'{self.id}.volumeSeries.update({volume.to_dict()})') self.run_script(f'{self.id}.volumeSeries.update({js_data(volume)})')
def update_from_tick(self, series: pd.Series, cumulative_volume: bool = False): def update_from_tick(self, series: pd.Series, cumulative_volume: bool = False):
""" """
@ -626,12 +666,22 @@ class AbstractChart(Candlestick, Pane):
price_line: bool = True, price_label: bool = True price_line: bool = True, price_label: bool = True
) -> Line: ) -> Line:
""" """
Creates and returns a Line object.)\n Creates and returns a Line object.
""" """
self._lines.append(Line(self, name, color, style, width, price_line, price_label)) self._lines.append(Line(self, name, color, style, width, price_line, price_label))
self._lines[-1]._push_to_legend() self._lines[-1]._push_to_legend()
return self._lines[-1] return self._lines[-1]
def create_histogram(
self, name: str = '', color: str = 'rgba(214, 237, 255, 0.6)',
price_line: bool = True, price_label: bool = True,
scale_margin_top: float = 0.0, scale_margin_bottom: float = 0.0
) -> Histogram:
"""
Creates and returns a Histogram object.
"""
return Histogram(self, name, color, price_line, price_label, scale_margin_top, scale_margin_bottom)
def lines(self) -> List[Line]: def lines(self) -> List[Line]:
""" """
Returns all lines for the chart. Returns all lines for the chart.
@ -639,20 +689,23 @@ class AbstractChart(Candlestick, Pane):
return self._lines.copy() return self._lines.copy()
def trend_line(self, start_time: TIME, start_value: NUM, end_time: TIME, end_value: NUM, def trend_line(self, start_time: TIME, start_value: NUM, end_time: TIME, end_value: NUM,
color: str = '#1E80F0', width: int = 2, style: LINE_STYLE = 'solid' round: bool = False, color: str = '#1E80F0', width: int = 2,
style: LINE_STYLE = 'solid',
) -> Line: ) -> Line:
line = Line(self, '', color, style, width, False, False, False) line = Line(self, '', color, style, width, False, False, False)
line._set_trend(start_time, start_value, end_time, end_value) line._set_trend(start_time, start_value, end_time, end_value, round=round)
return line return line
def ray_line(self, start_time: TIME, value: NUM, def ray_line(self, start_time: TIME, value: NUM, round: bool = False,
color: str = '#1E80F0', width: int = 2, style: LINE_STYLE = 'solid' color: str = '#1E80F0', width: int = 2,
style: LINE_STYLE = 'solid'
) -> Line: ) -> Line:
line = Line(self, '', color, style, width, False, False, False) line = Line(self, '', color, style, width, False, False, False)
line._set_trend(start_time, value, start_time, value, ray=True) line._set_trend(start_time, value, start_time, value, ray=True, round=round)
return line return line
def vertical_span(self, start_time: Union[TIME, tuple, list], end_time: TIME = None, color: str = 'rgba(252, 219, 3, 0.2)'): def vertical_span(self, start_time: Union[TIME, tuple, list], end_time: TIME = None,
color: str = 'rgba(252, 219, 3, 0.2)'):
""" """
Creates a vertical line or span across the chart.\n Creates a vertical line or span across the chart.\n
Start time and end time can be used together, or end_time can be Start time and end time can be used together, or end_time can be

View File

@ -43,15 +43,13 @@ if (!window.Chart) {
}, },
handleScroll: {vertTouchDrag: true}, handleScroll: {vertTouchDrag: true},
}) })
this.wrapper.style.width = `${100 * innerWidth}%`
this.wrapper.style.height = `${100 * innerHeight}%`
this.wrapper.style.display = 'flex' this.wrapper.style.display = 'flex'
this.wrapper.style.flexDirection = 'column' this.wrapper.style.flexDirection = 'column'
this.wrapper.style.position = 'relative' this.wrapper.style.position = 'relative'
this.wrapper.style.float = position this.wrapper.style.float = position
this.div.style.position = 'relative' this.div.style.position = 'relative'
this.div.style.display = 'flex' this.div.style.display = 'flex'
this.reSize()
this.wrapper.appendChild(this.div) this.wrapper.appendChild(this.div)
document.getElementById('wrapper').append(this.wrapper) document.getElementById('wrapper').append(this.wrapper)
@ -66,6 +64,8 @@ if (!window.Chart) {
reSize() { reSize() {
let topBarOffset = 'topBar' in this && this.scale.height !== 0 ? this.topBar.offsetHeight : 0 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.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}%`
} }
makeCandlestickSeries() { makeCandlestickSeries() {
this.markers = [] this.markers = []

View File

@ -33,6 +33,11 @@ def parse_event_message(window, string):
return func, args return func, args
def js_data(data: Union[pd.DataFrame, pd.Series]):
orient = 'columns' if isinstance(data, pd.Series) else 'records'
return data.to_json(orient=orient, default_handler=lambda x: 'null' if pd.isna(x) else x)
def jbool(b: bool): return 'true' if b is True else 'false' if b is False else None def jbool(b: bool): return 'true' if b is True else 'false' if b is False else None

View File

@ -5,7 +5,7 @@ with open('README.md', 'r', encoding='utf-8') as f:
setup( setup(
name='lightweight_charts', name='lightweight_charts',
version='1.0.17.2', version='1.0.17.3',
packages=find_packages(), packages=find_packages(),
python_requires='>=3.8', python_requires='>=3.8',
install_requires=[ install_requires=[