Skip to content

Commit

Permalink
Enhancements:
Browse files Browse the repository at this point in the history
- 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.
  • Loading branch information
louisnw01 committed Sep 4, 2023
1 parent 8532d48 commit 555573b
Show file tree
Hide file tree
Showing 8 changed files with 185 additions and 63 deletions.
18 changes: 15 additions & 3 deletions docs/source/reference/abstract_chart.md
Expand Up @@ -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:
[`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.
```
Expand All @@ -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`).
Expand All @@ -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.
Expand Down
51 changes: 51 additions & 0 deletions docs/source/reference/histogram.md
@@ -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.
```
````
1 change: 1 addition & 0 deletions docs/source/reference/index.md
Expand Up @@ -4,6 +4,7 @@
:hidden:
abstract_chart
line
histogram
horizontal_line
charts
events
Expand Down
6 changes: 3 additions & 3 deletions docs/source/reference/line.md
Expand Up @@ -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:
[`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).
___
Expand Down Expand Up @@ -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.
Expand Down
159 changes: 106 additions & 53 deletions lightweight_charts/abstract.py
Expand Up @@ -10,7 +10,7 @@
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,
line_style, marker_position, marker_shape, crosshair_mode, price_scale_mode, js_data,
)

JS = {}
Expand Down Expand Up @@ -106,11 +106,15 @@ def create_subchart(self, position: FLOAT = 'left', width: float = 0.5, height:


class SeriesCommon(Pane):
def __init__(self, chart: 'AbstractChart'):
def __init__(self, chart: 'AbstractChart', name: str = None):
super().__init__(chart.win)
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.name = name
self.num_decimals = 2

def _set_interval(self, df: pd.DataFrame):
Expand Down Expand Up @@ -155,12 +159,33 @@ def _series_datetime_format(self, series: pd.Series, exclude_lowercase=None):
return series

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)
interval_seconds = self._interval.total_seconds()
arg = interval_seconds * (arg.timestamp() // interval_seconds)
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',
shape: MARKER_SHAPE = 'arrow_up', color: str = '#2196F3', text: str = ''
) -> str:
Expand Down Expand Up @@ -349,9 +374,8 @@ def delete(self):

class Line(SeriesCommon):
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.name = name
self.run_script(f'''
{self.id} = {{
series: {chart.id}.chart.addLineSeries({{
Expand All @@ -374,7 +398,7 @@ def __init__(self, chart, name, color, style, width, price_line, price_label, cr
color: '{color}',
precision: 2,
}}
''')
null''')

def _push_to_legend(self):
self.run_script(f'''
Expand All @@ -383,39 +407,18 @@ def _push_to_legend(self):
{self._chart.id}.legend.lines.push({self._chart.id}.legend.makeLineRow({self.id}))
}}''')

def set(self, df: pd.DataFrame = None):
"""
Sets the line data.\n
:param df: If the name parameter is not used, the columns should be named: date/time, value.
"""
if df is None or df.empty:
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, round=False):
if round:
start_time = self._single_datetime_format(start_time)
end_time = self._single_datetime_format(end_time)
else:
start_time, end_time = pd.to_datetime((start_time, end_time)).astype('int64') // 10 ** 9

def _set_trend(self, start_time, start_value, end_time, end_value, ray=False):
self.run_script(f'''
{self._chart.id}.chart.timeScale().applyOptions({{shiftVisibleRangeOnNewBar: false}})
{self.id}.series.setData(
calculateTrendLine({pd.to_datetime(start_time).timestamp()}, {start_value},
{pd.to_datetime(end_time).timestamp()}, {end_value},
calculateTrendLine({start_time}, {start_value},
{end_time}, {end_value},
{self._chart._interval.total_seconds() * 1000},
{self._chart.id}, {jbool(ray)}))
{self._chart.id}.chart.timeScale().applyOptions({{shiftVisibleRangeOnNewBar: true}})
Expand All @@ -437,6 +440,44 @@ def delete(self):
''')


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):
def __init__(self, chart: 'AbstractChart'):
super().__init__(chart)
Expand All @@ -462,20 +503,20 @@ def set(self, df: pd.DataFrame = None, render_drawings=False):
self.candle_data = df.copy()
self._last_bar = df.iloc[-1]

bars = df.to_dict(orient='records')
self.run_script(f'{self.id}.candleData = {bars}; {self.id}.series.setData({self.id}.candleData)')
self.run_script(f'{self.id}.candleData = {js_data(df)}; {self.id}.series.setData({self.id}.candleData)')
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}()")
if 'volume' not in df:
return
volume = df.drop(columns=['open', 'high', 'low', 'close']).rename(columns={'volume': 'value'})
volume['color'] = self._volume_down_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:
# if line.name in df.columns:
# line.set()
for line in self._lines:
if line.name not in df.columns:
continue
line.set(df[['time', line.name]], format_cols=False)

def update(self, series: pd.Series, _from_tick=False):
"""
Expand All @@ -489,10 +530,9 @@ def update(self, series: pd.Series, _from_tick=False):
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 = series.to_dict()
bar = js_data(series)
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}
}}
else {self.id}.candleData.push({bar})
Expand All @@ -502,7 +542,7 @@ def update(self, series: pd.Series, _from_tick=False):
return
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
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):
"""
Expand Down Expand Up @@ -626,33 +666,46 @@ def create_line(
price_line: bool = True, price_label: bool = True
) -> 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[-1]._push_to_legend()
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]:
"""
Returns all lines for the chart.
"""
return self._lines.copy()

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

def ray_line(self, start_time: TIME, value: NUM,
color: str = '#1E80F0', width: int = 2, style: LINE_STYLE = 'solid'
def ray_line(self, start_time: TIME, value: NUM, round: bool = False,
color: str = '#1E80F0', width: int = 2,
style: LINE_STYLE = 'solid'
) -> Line:
line = Line(self, '', color, style, width, False, False, False)
line._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

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
Start time and end time can be used together, or end_time can be
Expand Down

0 comments on commit 555573b

Please sign in to comment.