diff --git a/docs/source/reference/abstract_chart.md b/docs/source/reference/abstract_chart.md index 9a042dd..923215a 100644 --- a/docs/source/reference/abstract_chart.md +++ b/docs/source/reference/abstract_chart.md @@ -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. ``` @@ -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`). @@ -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. diff --git a/docs/source/reference/histogram.md b/docs/source/reference/histogram.md new file mode 100644 index 0000000..67971c4 --- /dev/null +++ b/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. + +``` +```` \ No newline at end of file diff --git a/docs/source/reference/index.md b/docs/source/reference/index.md index 279350c..6da662d 100644 --- a/docs/source/reference/index.md +++ b/docs/source/reference/index.md @@ -4,6 +4,7 @@ :hidden: abstract_chart line +histogram horizontal_line charts events diff --git a/docs/source/reference/line.md b/docs/source/reference/line.md index 5da6fd5..284c0d9 100644 --- a/docs/source/reference/line.md +++ b/docs/source/reference/line.md @@ -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). ___ @@ -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. diff --git a/lightweight_charts/abstract.py b/lightweight_charts/abstract.py index 4cc512e..c3dd8c3 100644 --- a/lightweight_charts/abstract.py +++ b/lightweight_charts/abstract.py @@ -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 = {} @@ -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): @@ -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: @@ -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({{ @@ -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''' @@ -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}}) @@ -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) @@ -462,8 +503,7 @@ 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: @@ -471,11 +511,12 @@ def set(self, df: pd.DataFrame = None, render_drawings=False): 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): """ @@ -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}) @@ -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): """ @@ -626,12 +666,22 @@ 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. @@ -639,20 +689,23 @@ def lines(self) -> List[Line]: 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 diff --git a/lightweight_charts/js/funcs.js b/lightweight_charts/js/funcs.js index 1397ace..41d3b7d 100644 --- a/lightweight_charts/js/funcs.js +++ b/lightweight_charts/js/funcs.js @@ -43,15 +43,13 @@ if (!window.Chart) { }, handleScroll: {vertTouchDrag: true}, }) - this.wrapper.style.width = `${100 * innerWidth}%` - this.wrapper.style.height = `${100 * innerHeight}%` 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) @@ -66,6 +64,8 @@ if (!window.Chart) { 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}%` } makeCandlestickSeries() { this.markers = [] diff --git a/lightweight_charts/util.py b/lightweight_charts/util.py index 493c736..0ca00d4 100644 --- a/lightweight_charts/util.py +++ b/lightweight_charts/util.py @@ -33,6 +33,11 @@ def parse_event_message(window, string): 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 diff --git a/setup.py b/setup.py index 990a510..14a7c8b 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setup( name='lightweight_charts', - version='1.0.17.2', + version='1.0.17.3', packages=find_packages(), python_requires='>=3.8', install_requires=[