diff options
Diffstat (limited to 'openbb_platform/obbject_extensions/charting/openbb_charting/charts/generic_charts.py')
-rw-r--r-- | openbb_platform/obbject_extensions/charting/openbb_charting/charts/generic_charts.py | 603 |
1 files changed, 603 insertions, 0 deletions
diff --git a/openbb_platform/obbject_extensions/charting/openbb_charting/charts/generic_charts.py b/openbb_platform/obbject_extensions/charting/openbb_charting/charts/generic_charts.py new file mode 100644 index 00000000000..066865a8d80 --- /dev/null +++ b/openbb_platform/obbject_extensions/charting/openbb_charting/charts/generic_charts.py @@ -0,0 +1,603 @@ +"""Generic Charts Module.""" + +# pylint: disable=too-many-arguments,unused-argument,too-many-locals, too-many-branches, too-many-lines, too-many-statements, use-dict-literal, broad-exception-caught, too-many-nested-blocks + +from typing import Any, Dict, List, Literal, Optional, Union + +import numpy as np +import pandas as pd +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.helpers import ( + calculate_returns, + should_share_axis, + z_score_standardization, +) +from openbb_charting.core.chart_style import ChartStyle +from openbb_charting.core.openbb_figure import OpenBBFigure +from openbb_charting.styles.colors import LARGE_CYCLER + + +def line_chart( # noqa: PLR0912 + 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, + **kwargs, +) -> Union[OpenBBFigure, Figure]: + """Create a line chart.""" + if data is None: + raise ValueError("Error: Data is a required field.") + + auto_layout = False + index = ( + data.index.name + if isinstance(data, (pd.DataFrame, pd.Series)) + else index if index is not None else x if x is not None else "date" + ) + df: pd.DataFrame = ( + basemodel_to_df(convert_to_basemodel(data), index=index) + ).dropna(how="all", axis=1) + + if df.index.name is None: + if "date" in df.columns: + df.date = df.date.apply(pd.to_datetime) + df.set_index("date", inplace=True) + else: + found_index = False + for col in df.columns: + if df[col].dtype == "object": + try: + df[col] = df[col].apply(pd.to_datetime) + index = df[col].name # type: ignore + df.set_index(col, inplace=True) + df.index.name = "date" + found_index = True + except Exception as _: # noqa: S112 + continue + if found_index is True: + break + if found_index is False: + df.set_index(df.iloc[:, 0], inplace=True) + + if "symbol" in df.columns and len(df.symbol.unique()) > 1: + df = df.pivot(columns="symbol", values=target if target else "close") + + if "symbol" not in df.columns and target in df.columns: + df = df[[target]] + + y = y.split(",") if isinstance(y, str) else y + + if y is None or same_axis is True: + y = df.columns.to_list() + auto_layout = True + + if same_axis is True: + auto_layout = False + + if returns is True: + df = df.apply(calculate_returns) + auto_layout = False + + if normalize is True: + df = df.apply(z_score_standardization) + auto_layout = False + + if layout_kwargs is None: + layout_kwargs = {} + + if scatter_kwargs is None: + scatter_kwargs = {} + + try: + fig = OpenBBFigure() + except Exception as _: + fig = OpenBBFigure(create_backend=True) + + title = f"{title}" if title else "" + xtitle = xtitle if xtitle else "" + y1title = ytitle if ytitle else "" + y2title = y2title if y2title else "" + y2 = y2 if y2 else [] + yaxis_num = 1 + yaxis = f"y{yaxis_num}" + first_y = y[0] # type: ignore[index] + second_y = None + third_y = None + add_scatter = False + + # Attempt to layout the chart automatically with multiple y-axis. + mode = scatter_kwargs.pop("mode", "lines") + hovertemplate = scatter_kwargs.pop("hovertemplate", None) + + if auto_layout is True: + # Sort columns by the difference between the max and min values. + # This is to help determine which columns should share the same y-axis. + diff = df.max(numeric_only=True) - df.min(numeric_only=True) + sorted_columns = diff.sort_values(ascending=False).index + if sorted_columns is None or len(sorted_columns) == 0: + raise ValueError("Error: expected data with numeric values.") + df = df[sorted_columns] + + for i, col in enumerate(df.columns): + + if col in y: # type: ignore[operator] + hovertemplate = ( + hovertemplate + if hovertemplate + else f"{df[col].name}: %{{y}}<extra></extra>" + ) + share_yaxis = should_share_axis(df, first_y, col, threshold=2.5) + if share_yaxis is True: + add_scatter = True + if share_yaxis is False: + yaxis_num = 2 + yaxis = f"y{yaxis_num}" + if second_y is None: + second_y = col + add_scatter = True + if second_y is not None: + add_scatter = False + share_yaxis = should_share_axis(df, col, second_y, threshold=3) + if share_yaxis is True: + add_scatter = True + if share_yaxis is False: + yaxis_num = 3 + yaxis = f"y{yaxis_num}" + third_y = col + add_scatter = True + + if add_scatter is True: + fig = fig.add_scatter( + x=df.index, + y=df[col], + name=col, + mode=mode, + line=dict(width=1, color=LARGE_CYCLER[i % len(LARGE_CYCLER)]), + hovertemplate=hovertemplate, + hoverlabel=dict(font_size=10), + yaxis=yaxis, + **scatter_kwargs, + ) + + if auto_layout is False: + color = 0 + for i, col in enumerate(y): # type: ignore[arg-type] + hovertemplate = ( + hovertemplate + if hovertemplate + else f"{df[col].name}: %{{y}}<extra></extra>" + ) + fig = fig.add_scatter( + x=df.index, + y=df[col], + name=col, + mode=mode, + line=dict(width=1, color=LARGE_CYCLER[color]), + hovertemplate=hovertemplate, + hoverlabel=dict(font_size=10), + yaxis="y1", + **scatter_kwargs, + ) + color += 1 + if y2: + second_y = y2[0] + for i, col in enumerate(y2): + hovertemplate = ( + hovertemplate + if hovertemplate + else f"{df[col].name}: %{{y}}<extra></extra>" + ) + fig = fig.add_scatter( + x=df.index, + y=df[col], + name=col, + mode=mode, + line=dict(width=1, color=LARGE_CYCLER[color]), + hovertemplate=hovertemplate, + hoverlabel=dict(font_size=10), + yaxis="y2", + **scatter_kwargs, + ) + color += 1 + + if returns is True: + y1title = "Percent" + title = f"{title} - Cumulative Returns" if title else "Cumulative Returns" + + if normalize is True: + y1title = "Z-Score" + title = f"{title} - Z-Score" if title else "Z-Score" + + if not title and target is not None: + title = f"{target.replace('_', ' ').title()}" + + fig.update_layout( + title=dict(text=title if title else None, x=0.5, font=dict(size=16)), + paper_bgcolor="rgba(0,0,0,0)", + plot_bgcolor="rgba(0,0,0,0)", + legend=dict( + orientation="h", + yanchor="bottom", + xanchor="right", + y=1.02, + x=0.95, + bgcolor="rgba(0,0,0,0)", + ), + yaxis=( + dict( + ticklen=0, + side="right", + title=dict( + text=y1title if ytitle else None, standoff=30, font=dict(size=16) + ), + tickfont=dict(size=14), + anchor="x", + showgrid=True, + mirror=True, + showline=True, + zeroline=False, + gridcolor="rgba(128,128,128,0.25)", + ) + ), + yaxis2=( + dict( + overlaying="y", + side="left", + ticklen=0, + showgrid=False, + showline=True, + zeroline=False, + mirror=True, + title=dict( + text=y2title if y2title else None, standoff=10, font=dict(size=16) + ), + tickfont=dict(size=14), + anchor="x", + ) + ), + yaxis3=( + dict( + overlaying="y", + side="left", + ticklen=0, + position=0, + showgrid=False, + showline=False, + zeroline=False, + showticklabels=True, + mirror=False, + tickfont=dict(size=12, color="rgba(128,128,128,0.75)"), + anchor="free", + ) + ), + xaxis=dict( + ticklen=0, + showgrid=True, + title=( + dict(text=xtitle, standoff=30, font=dict(size=16)) if xtitle else None + ), + zeroline=False, + showline=True, + mirror=True, + gridcolor="rgba(128,128,128,0.25)", + domain=[0.095, 0.95] if third_y else None, + ), + margin=dict(r=25, l=25) if normalize is False else None, + autosize=True, + dragmode="pan", + hovermode="x", + ) + + if df.index.name not in ("date", "timestamp"): + fig.update_xaxes(type="category") + + if layout_kwargs: + fig.update_layout( + **layout_kwargs, + ) + + return fig + + +def bar_chart( # noqa: PLR0912 + 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, + bar_kwargs: Optional[Dict[str, Any]] = None, + layout_kwargs: Optional[Dict[str, Any]] = None, + **kwargs, +) -> Union[OpenBBFigure, Figure]: + """Create a vertical 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. + """ + try: + figure = OpenBBFigure() + except Exception as _: + figure = OpenBBFigure(create_backend=True) + + figure = figure.create_subplots( + 1, + 1, + shared_xaxes=True, + vertical_spacing=0.06, + horizontal_spacing=0.01, + row_width=[1], + specs=[[{"secondary_y": True}]], + ) + + figure.update_layout(ChartStyle().plotly_template.get("layout", {})) + if colors is not None: + figure.update_layout(colorway=colors) + if bar_kwargs is None: + bar_kwargs = {} + if isinstance(data, (Data, list, dict)): + data = basemodel_to_df(convert_to_basemodel(data), index=None) + + bar_df = data.copy().set_index(x) # type: ignore + y = y.split(",") if isinstance(y, str) else y + + for item in y: + figure.add_bar( + x=bar_df.index if orientation == "v" else bar_df[item], + y=bar_df[item] if orientation == "v" else bar_df.index, + name=bar_df[item].name, + showlegend=len(y) > 1, + legendgroup=bar_df[item].name, + orientation=orientation, + hovertemplate=( + "%{fullData.name}:%{y}<extra></extra>" + if orientation == "v" + else "%{fullData.name}:%{x}<extra></extra>" + ), + width=0.95 / len(y) * 0.75 if barmode == "group" and len(y) > 1 else 0.95, + **bar_kwargs, + ) + + figure.update_layout( + title=dict(text=title if title else None, x=0.5, font=dict(size=16)), + paper_bgcolor="rgba(0,0,0,0)", + plot_bgcolor="rgba(0,0,0,0)", + legend=dict( + orientation="h", + yanchor="bottom", + xanchor="right", + y=1.02, + x=0.98, + bgcolor="rgba(0,0,0,0)", + ), + xaxis=dict( + type=xtype, + title=dict( + text=xtitle if xtitle else None, standoff=30, font=dict(size=16) + ), + ticklen=0, + showgrid=orientation == "h", + tickformat="<b>%{x}</b>", + tickfont=dict(size=12), + categoryorder="array" if orientation == "v" else None, + categoryarray=bar_df.index if orientation == "v" else None, + ), + yaxis=dict( + title=dict( + text=ytitle if ytitle else None, standoff=30, font=dict(size=16) + ), + ticklen=0, + showgrid=orientation == "v", + tickfont=dict(size=12), + side="left" if orientation == "h" else "right", + categoryorder="array" if orientation == "h" else None, + categoryarray=bar_df.index if orientation == "h" else None, + ), + margin=dict(pad=5), + barmode=barmode, + ) + if orientation == "h": + figure.update_layout( + xaxis=dict( + type="linear", + showspikes=False, + ), + yaxis=dict( + type="category", + showspikes=False, + ), + hoverlabel=dict( + font=dict(size=12), + ), + ) + if layout_kwargs: + figure.update_layout( + **layout_kwargs, + ) + return figure + + +def bar_increasing_decreasing( # pylint: disable=W0102 + keys: List[str], + values: List[Union[int, float]], + title: Optional[str] = None, + xtitle: Optional[str] = None, + ytitle: Optional[str] = None, + colors: List[str] = ["blue", "red"], + orientation: Literal["h", "v"] = "h", + barmode: Literal["group", "stack", "relative", "overlay"] = "relative", + layout_kwargs: Optional[Dict[str, Any]] = None, +) -> Union[OpenBBFigure, Figure]: + """Create a bar chart with increasing and decreasing values represented by two colors. + + Parameters + ---------- + keys : List[str] + The x-axis keys. + values : List[Any] + The y-axis values. + title : Optional[str], optional + The title of the chart, by default None. + xtitle : Optional[str], optional + The x-axis title, by default None. + ytitle : Optional[str], optional + The y-axis title, by default None. + colors : List[str], optional + The colors to use for increasing and decreasing values, by default ["blue", "red"]. + orientation : Literal["h", "v"], optional + The orientation of the bars, by default "h". + barmode : Literal["group", "stack", "relative", "overlay"], optional + The bar mode, by default "relative". + layout_kwargs : Optional[Dict[str, Any]], optional + Additional keyword arguments to apply with figure.update_layout(), by default None. + + Returns + ------- + OpenBBFigure + The OpenBBFigure object. + """ + try: + figure = OpenBBFigure() + except Exception as _: + figure = OpenBBFigure(create_backend=True) + + figure = figure.create_subplots( + 1, + 1, + shared_xaxes=False, + vertical_spacing=0.06, + horizontal_spacing=0.01, + row_width=[1], + specs=[[{"secondary_y": True}]], + ) + # figure.update_layout(ChartStyle().plotly_template.get("layout", {})) + + try: + data = pd.Series(data=values, index=keys) + increasing_data = data[data > 0] # type: ignore + decreasing_data = data[data < 0] # type: ignore + except Exception as e: + raise ValueError(f"Error: {e}") from e + + if not increasing_data.empty: + figure.add_bar( + x=increasing_data.index if orientation == "v" else increasing_data, + y=increasing_data if orientation == "v" else increasing_data.index, + marker=dict(color=colors[0]), + orientation=orientation, + showlegend=False, + width=0.95 / len(keys) * 0.75 if barmode == "group" else 0.95, + hoverinfo="y" if orientation == "v" else "x", + ) + if not decreasing_data.empty: + figure.add_bar( + x=decreasing_data.index if orientation == "v" else decreasing_data, + y=decreasing_data if orientation == "v" else decreasing_data.index, + marker=dict(color=colors[1]), + orientation=orientation, + showlegend=False, + width=0.95 / len(keys) * 0.75 if barmode == "group" else 0.95, + hoverinfo="y" if orientation == "v" else "x", + ) + + figure.update_layout( + title=dict(text=title if title else None, x=0.5, font=dict(size=20)), + hovermode="x" if orientation == "v" else "y", + hoverlabel=dict(align="left" if orientation == "h" else "auto"), + yaxis=dict( + title=dict( + text=ytitle if ytitle else None, standoff=30, font=dict(size=16) + ), + side="left" if orientation == "h" else "right", + showgrid=orientation == "v", + gridcolor="rgba(128,128,128,0.25)", + tickfont=dict(size=12), + ticklen=0, + categoryorder="array" if orientation == "h" else None, + categoryarray=keys if orientation == "h" else None, + ), + xaxis=dict( + title=dict( + text=xtitle if xtitle else None, standoff=30, font=dict(size=16) + ), + showgrid=orientation == "h", + gridcolor="rgba(128,128,128,0.25)", + tickfont=dict(size=12), + ticklen=0, + categoryorder="array" if orientation == "v" else None, + categoryarray=keys if orientation == "v" else None, + ), + margin=dict(pad=5), + ) + + if layout_kwargs: + figure.update_layout( + **layout_kwargs, + ) + + return figure |