"""Charting Class implementation.""" # pylint: disable=too-many-arguments,unused-argument from typing import ( Any, Callable, ClassVar, Dict, List, Literal, Optional, Tuple, Type, Union, ) from warnings import warn import numpy as np import pandas as pd from importlib_metadata import entry_points from openbb_core.app.model.charts.chart import Chart from openbb_core.app.model.obbject import OBBject from openbb_core.app.utils import basemodel_to_df, convert_to_basemodel from openbb_core.provider.abstract.data import Data from plotly.graph_objs import Figure from openbb_charting.charts.generic_charts import bar_chart, line_chart from openbb_charting.charts.helpers import ( get_charting_functions, get_charting_functions_list, ) from openbb_charting.core.backend import Backend, create_backend, get_backend from openbb_charting.core.openbb_figure import OpenBBFigure from openbb_charting.query_params import ChartParams, IndicatorsParams class Charting: """Charting extension. Methods ------- show Display chart and save it to the OBBject. to_chart Redraw the chart and save it to the OBBject, with an optional entry point for Data. functions Return a list of Platform commands with charting functions. get_params Return the charting parameters for the function the OBBject was created from. indicators Return the list of the available technical indicators to use with the `to_chart` method and OHLC+V data. table Display an interactive table. create_line_chart Create a line chart from external data. create_bar_chart Create a bar chart, on a single x-axis with one or more values for the y-axis, from external data. toggle_chart_style Toggle the chart style, of an existing chart, between light and dark mode. """ _extension_views: ClassVar[List[Type]] = [ entry_point.load() for entry_point in entry_points(group="openbb_charting_extension") ] def __init__(self, obbject): """Initialize Charting extension.""" # pylint: disable=import-outside-toplevel from openbb_core.app.model.charts.charting_settings import ChartingSettings self._obbject: OBBject = obbject self._charting_settings = ChartingSettings( user_settings=self._obbject._user_settings, # type: ignore system_settings=self._obbject._system_settings, # type: ignore ) self._backend: Backend = self._handle_backend() self._functions: Dict[str, Callable] = self._get_functions() @classmethod def indicators(cls): """Return an instance of the IndicatorsParams class, containing all available indicators and their parameters. Without assigning to a variable, it will print the the information to the console. """ return IndicatorsParams() @classmethod def functions(cls) -> List[str]: """Return a list of the available functions.""" functions: List[str] = [] for view in cls._extension_views: functions.extend(get_charting_functions_list(view)) return functions def _get_functions(self) -> Dict[str, Callable]: """Return a dict with the available functions.""" functions: Dict[str, Callable] = {} for view in self._extension_views: functions.update(get_charting_functions(view)) return functions def _handle_backend(self) -> Backend: """Create and start the backend.""" create_backend(self._charting_settings) backend = get_backend() backend.start(debug=self._charting_settings.debug_mode) return backend def _get_chart_function(self, route: str) -> Callable: """Given a route, it returns the chart function. The module must contain the given route.""" if route is None: raise ValueError("OBBject was initialized with no function route.") adjusted_route = route.replace("/", "_")[1:] if adjusted_route not in self._functions: raise ValueError( f"Could not find the route `{adjusted_route}` in the charting functions." ) return self._functions[adjusted_route] def get_params(self) -> Union[ChartParams, None]: """Return the ChartQueryParams class for the function the OBBject was created from. Without assigning to a variable, it will print the docstring to the console. If the class is not defined, the help for the function will be returned. """ if self._obbject._route is None: # pylint: disable=protected-access raise ValueError("OBBject was initialized with no function route.") charting_function = ( self._obbject._route # pylint: disable=protected-access ).replace("/", "_")[1:] if hasattr(ChartParams, charting_function): return getattr(ChartParams, charting_function)() return help( # type: ignore self._get_chart_function( # pylint: disable=protected-access self._obbject.extra[ # pylint: disable=protected-access "metadata" ].route ) ) def _prepare_data_as_df( self, data: Optional[Union[pd.DataFrame, pd.Series]] ) -> Tuple[pd.DataFrame, bool]: """Convert supplied data to a DataFrame.""" has_data = ( isinstance(data, (Data, pd.DataFrame, pd.Series)) and not data.empty # type: ignore ) or (bool(data)) index = ( data.index.name if has_data and isinstance(data, (pd.DataFrame, pd.Series)) else None ) data_as_df: pd.DataFrame = ( basemodel_to_df(convert_to_basemodel(data), index=index) if has_data else self._obbject.to_dataframe(index=index) ) if "date" in data_as_df.columns: data_as_df = data_as_df.set_index("date") if "provider" in data_as_df.columns: data_as_df.drop(columns="provider", inplace=True) return data_as_df, has_data # pylint: disable=too-many-locals def create_line_chart( self, data: Union[ list, dict, pd.DataFrame, List[pd.DataFrame], pd.Series, List[pd.Series], np.ndarray, Data, ], index: Optional[str] = None, target: Optional[str] = None, title: Optional[str] = None, x: Optional[str] = None, xtitle: Optional[str] = None, y: Optional[Union[str, List[str]]] = None, ytitle: Optional[str] = None, y2: Optional[Union[str, List[str]]] = None, y2title: Optional[str] = None, layout_kwargs: Optional[dict] = None, scatter_kwargs: Optional[dict] = None, normalize: bool = False, returns: bool = False, same_axis: bool = False, render: bool = True, **kwargs, ) -> Union[OpenBBFigure, Figure, None]: """Create a line chart from external data and render a chart or return the OpenBBFigure. Parameters ---------- data : Union[Data, pd.DataFrame, pd.Series] Data to be plotted (OHLCV data). index : Optional[str], optional Index column, by default None target : Optional[str], optional Target column to be plotted, by default None title : Optional[str], optional Chart title, by default None x : Optional[str], optional X-axis column, by default None xtitle : Optional[str], optional X-axis title, by default None y : Optional[Union[str, List[str]]], optional Y-axis column(s), by default None If None are supplied, the layout is optimized for the contents of data. Where many units/scales are present, it will attempt to divide based on the range of values. ytitle : Optional[str], optional Y-axis title, by default None y2 : Optional[Union[str, List[str]]], optional Y2-axis column(s), by default None y2title : Optional[str], optional Y2-axis title, by default None layout_kwargs : Optional[dict], optional Additional Plotly Layout parameters for `fig.update_layout`, by default None scatter_kwargs : Optional[dict], optional Additional Plotly parameters applied on creation of each scatter plot, by default None normalize : bool, optional Normalize the data with Z-Score Standardization, by default False returns : bool, optional Convert the data to cumulative returns, by default False same_axis: bool, optional If True, forces all data onto the same Y-axis, by default False render: bool, optional If True, the chart will be rendered, by default True **kwargs: Dict[str, Any] Extra parameters to be passed to `figure.show()` """ fig = line_chart( data=data, index=index, target=target, title=title, x=x, xtitle=xtitle, y=y, ytitle=ytitle, y2=y2, y2title=y2title, layout_kwargs=layout_kwargs, scatter_kwargs=scatter_kwargs, normalize=normalize, returns=returns, same_axis=same_axis, **kwargs, ) fig = self._set_chart_style(fig) if render: return fig.show(**kwargs) return fig def create_bar_chart( self, data: Union[ list, dict, pd.DataFrame, List[pd.DataFrame], pd.Series, List[pd.Series], np.ndarray, Data, ], x: str, y: Union[str, List[str]], barmode: Literal["group", "stack", "relative", "overlay"] = "group", xtype: Literal[ "category", "multicategory", "date", "log", "linear" ] = "category", title: Optional[str] = None, xtitle: Optional[str] = None, ytitle: Optional[str] = None, orientation: Literal["h", "v"] = "v", colors: Optional[List[str]] = None, layout_kwargs: Optional[Dict[str, Any]] = None, bar_kwargs: Optional[Dict[str, Any]] = None, render: bool = True, **kwargs, ) -> Union[OpenBBFigure, Figure, None]: """Create a bar chart on a single x-axis with one or more values for the y-axis. Parameters ---------- data : Union[list, dict, pd.DataFrame, List[pd.DataFrame], pd.Series, List[pd.Series], np.ndarray, Data] Data to plot. x : str The x-axis column name. y : Union[str, List[str]] The y-axis column name(s). barmode : Literal["group", "stack", "relative", "overlay"], optional The bar mode, by default "group". xtype : Literal["category", "multicategory", "date", "log", "linear"], optional The x-axis type, by default "category". title : str, optional The title of the chart, by default None. xtitle : str, optional The x-axis title, by default None. ytitle : str, optional The y-axis title, by default None. colors: List[str], optional Manually set the colors to cycle through for each column in 'y', by default None. bar_kwargs : Dict[str, Any], optional Additional keyword arguments to apply with figure.add_bar(), by default None. layout_kwargs : Dict[str, Any], optional Additional keyword arguments to apply with figure.update_layout(), by default None. Returns ------- OpenBBFigure The OpenBBFigure object. """ fig = bar_chart( data=data, x=x, y=y, barmode=barmode, xtype=xtype, title=title, xtitle=xtitle, ytitle=ytitle, orientation=orientation, colors=colors, bar_kwargs=bar_kwargs, layout_kwargs=layout_kwargs, ) fig = self._set_chart_style(fig) if render: return fig.show(**kwargs) return fig # pylint: disable=inconsistent-return-statements def show(self, render: bool = True, **kwargs): """Display chart and save it to the OBBject.""" try: charting_function = self._get_chart_function( self._obbject._route # pylint: disable=protected-access ) kwargs["obbject_item"] = self._obbject.results kwargs["charting_settings"] = self._charting_settings # kwargs["standard_params"] = ( self._obbject._standard_params # pylint: disable=protected-access ) kwargs["extra_params"] = ( self._obbject._extra_params # pylint: disable=protected-access ) kwargs["provider"] = self._obbject.provider kwargs["extra"] = self._obbject.extra fig, content = charting_function(**kwargs) fig = self._set_chart_style(fig) content = fig.show(external=True, **kwargs).to_plotly_json() self._obbject.chart = Chart(fig=fig, content=content) if render: fig.show(**kwargs) except Exception: # pylint: disable=W0718 try: fig = self.create_line_chart(data=self._obbject.results, render=False, **kwargs) # type: ignore fig = self._set_chart_style(fig) content = fig.show(external=True, **kwargs).to_plotly_json() # type: ignore self._obbject.chart = Chart(fig=fig, content=content) if render: return fig.show(**kwargs) # type: ignore except Exception as e: raise RuntimeError( "Failed to automatically create a generic chart with the data provided." ) from e # pylint: disable=too-many-locals,inconsistent-return-statements def to_chart( self, data: Optional[ Union[ list, dict, pd.DataFrame, List[pd.DataFrame], pd.Series, List[pd.Series], np.ndarray, Data, ] ] = None, target: Optional[str] = None, index: Optional[str] = None, indicators: Optional[Dict[str, Dict[str, Any]]] = None, symbol: str = "", candles: bool = True, volume: bool = True, volume_ticks_x: int = 7, render: bool = True, **kwargs, ): """Create an OpenBBFigure with user customizations (if any) and save it to the OBBject. This function is used to populate, or re-populate, the OBBject with a chart using the data within the OBBject or external data supplied via the `data` parameter. This function modifies the original OBBject by overwriting the existing chart. Parameters ---------- data : Union[Data, pd.DataFrame, pd.Series] Data to be plotted. indicators : Dict[str, Dict[str, Any]], optional Indicators to be plotted, by default None symbol : str, optional Symbol to be plotted. This is used for labels and titles, by default "" candles : bool, optional If True, candles will be plotted, by default True volume : bool, optional If True, volume will be plotted, by default True volume_ticks_x : int, optional Volume ticks, by default 7 render : bool, optional If True, the chart will be rendered, by default True kwargs: Dict[str, Any] Extra parameters to be passed to the chart constructor. Examples -------- Plotting a time series with TA indicators >>> from openbb import obb >>> res = obb.equity.price.historical("AAPL") >>> indicators = dict( >>> sma=dict(length=[20,30,50]), >>> adx=dict(length=14), >>> rsi=dict(length=14), >>> macd=dict(fast=12, slow=26, signal=9), >>> bbands=dict(length=20, std=2), >>> stoch=dict(length=14), >>> ema=dict(length=[20,30,50]), >>> ) >>> res.charting.to_chart(**{"indicators": indicators}) Get all the available indicators >>> res = obb.equity.price.historical("AAPL") >>> indicators = res.charting.indicators() >>> indicators? """ data_as_df, has_data = self._prepare_data_as_df(data) # type: ignore if target is not None: data_as_df = data_as_df[[target]] kwargs["candles"] = candles kwargs["volume"] = volume kwargs["volume_ticks_x"] = volume_ticks_x kwargs["indicators"] = indicators if indicators else {} kwargs["symbol"] = symbol kwargs["target"] = target kwargs["index"] = index kwargs["obbject_item"] = self._obbject.results kwargs["charting_settings"] = self._charting_settings kwargs["standard_params"] = ( self._obbject._standard_params # pylint: disable=protected-access ) kwargs["extra_params"] = ( self._obbject._extra_params # pylint: disable=protected-access ) kwargs["provider"] = self._obbject.provider # pylint: disable=protected-access kwargs["extra"] = self._obbject.extra # pylint: disable=protected-access try: if has_data: self.show(data=data_as_df, render=render, **kwargs) else: self.show(**kwargs, render=render) except Exception: # pylint: disable=W0718 try: fig = self.create_line_chart(data=data_as_df, render=False, **kwargs) fig = self._set_chart_style(fig) content = fig.show(external=True, **kwargs).to_plotly_json() # type: ignore self._obbject.chart = Chart(fig=fig, content=content) if render: return fig.show(**kwargs) # type: ignore except Exception as e: # pylint: disable=W0718 raise RuntimeError( "Failed to automatically create a generic chart with the data provided." ) from e def _set_chart_style(self, figure: Figure): """Set the user preference for light or dark mode.""" style = self._charting_settings.chart_style font_color = "black" if style == "light" else "white" paper_bgcolor = "white" if style == "light" else "black" figure = figure.update_layout( dict( # pylint: disable=R1735 font_color=font_color, paper_bgcolor=paper_bgcolor ) ) return figure def toggle_chart_style(self): """Toggle the chart style between light and dark mode.""" if not hasattr(self._obbject.chart, "fig"): raise ValueError( "Error: No chart has been created. Please create a chart first." ) current = self._charting_settings.chart_style new = "light" if current == "dark" else "dark" self._charting_settings.chart_style = new figure = self._obbject.chart.fig # type: ignore[union-attr] updated_figure = self._set_chart_style(figure) self._obbject.chart.fig = updated_figure # type: ignore[union-attr] self._obbject.chart.content = updated_figure.show( # type: ignore[union-attr] external=True ).to_plotly_json() def table( self, data: Optional[Union[pd.DataFrame, pd.Series]] = None, title: str = "", ): """Display an interactive table. Parameters ---------- data : Optional[Union[pd.DataFrame, pd.Series]], optional Data to be plotted, by default None. If no data is provided the OBBject results will be used. title : str, optional Title of the table, by default "". """ data_as_df, _ = self._prepare_data_as_df(data) if isinstance(data_as_df.index, pd.RangeIndex): data_as_df.reset_index(inplace=True, drop=True) else: data_as_df.reset_index(inplace=True) if self._backend.isatty: try: self._backend.send_table( df_table=data_as_df, title=title or self._obbject._route, # pylint: disable=protected-access theme=self._charting_settings.table_style, ) except Exception as e: # pylint: disable=W0718 warn(f"Failed to show figure with backend. {e}") else: from plotly import ( # pylint:disable=import-outside-toplevel optional_imports, ) ipython_display = optional_imports.get_module("IPython.display") if ipython_display: ipython_display.display(ipython_display.HTML(data_as_df.to_html())) else: warn("IPython.display is not available.") def url( self, url: str, title: str = "", width: Optional[int] = None, height: Optional[int] = None, ): """Return the URL of the chart.""" try: self._backend.send_url(url=url, title=title, width=width, height=height) except Exception as e: # pylint: disable=W0718 warn(f"Failed to show figure with backend. {e}")