Source code for report_creator.charts

from __future__ import annotations

# Standard library imports
from abc import ABC, abstractmethod  # Use ABC for abstract base classes
from typing import Any

# Third-party imports
import numpy as np
import pandas as pd
import plotly.express as px
import plotly.graph_objs as go

# Loguru for logging
from loguru import logger
from pandas.api.types import is_datetime64_any_dtype

# Internal imports
from .base import Base
from .theming import preferred_fonts, report_creator_colors
from .utilities import (
    _strip_whitespace,  # _generate_anchor_id, _gfm_markdown_to_html not used here
)


class PxBase(Base, ABC):
    """
    Abstract Base Class for Plotly Express chart components.

    This class provides common functionality for chart components built using
    Plotly Express. It handles optional labels (typically used for chart titles)
    and includes static methods to apply consistent styling and keyword argument
    processing to Plotly Figure objects.

    Derived classes must implement the `to_html` method to generate the
    specific chart HTML.

    Args:
        label (Optional[str], optional): An optional label for the chart,
            which is often used as the chart's title. If provided, it can be
            automatically formatted and set as the title by `apply_common_kwargs`.
            Defaults to None.
    """

    def __init__(self, label: str | None = None):
        super().__init__(label=label)

    @abstractmethod
    def to_html(self) -> str:
        """
        Abstract method that must be implemented by derived chart classes.
        It should render the chart component to an HTML string.

        Returns:
            str: The HTML representation of the chart.
        """
        pass

    @staticmethod
    def apply_common_fig_options(fig: go.Figure) -> None:
        """
        Applies common layout options to a Plotly Figure object for consistent styling.

        These options currently include:
        - Setting a preferred font family for the entire figure.
        - Enabling autosize for the figure.
        - Removing the lasso and select tools from the Plotly modebar, as they are
          less commonly used in static reports.
        - Styling x-axis titles with the preferred font.
        - Setting x-axis tick labels to a 90-degree angle for better readability
          if they are long or numerous.

        Args:
            fig (go.Figure): The Plotly Figure object to which the common styling
                options will be applied. The figure is modified in place.
        """
        fig.update_layout(
            font_family=preferred_fonts[0] if preferred_fonts else "sans-serif",
            autosize=True,
            # Remove less commonly used tools from the modebar
            modebar_remove=["lasso2d", "select2d"],
            title_font={"weight": "bold", "family": preferred_fonts[0] if preferred_fonts else "sans-serif"},
        )
        fig.update_xaxes(
            title_font_family=preferred_fonts[0] if preferred_fonts else "sans-serif",
            tickangle=90,  # Rotates x-axis labels to prevent overlap
        )

    @staticmethod
    def apply_common_kwargs(kwargs: dict[str, Any], label: str | None = None) -> None:
        """
        Applies common keyword arguments to a kwargs dictionary, primarily for
        setting a chart title based on the provided `label`.

        If a `label` is provided and the `kwargs` dictionary does not already
        contain a "title" key, this method will format the `label` (by wrapping
        long text using HTML `<br>` tags) and add it to `kwargs` as "title".
        This ensures that charts have titles derived from their labels unless
        a title is explicitly overridden in `**kwargs`.

        Args:
            kwargs (dict[str, Any]): The dictionary of keyword arguments intended for
                a Plotly Express function. This dictionary is modified in place.
            label (Optional[str], optional): The label for the chart. If provided,
                it's used to generate a title. Defaults to None.
        """

        def _format_title_with_line_breaks(text: str, max_words_per_line: int = 5) -> str:
            """
            Formats a string by inserting HTML <br> tags to wrap lines,
            aiming to break lines after a specified number of words.
            """
            if not text:  # Handle empty or None text
                return ""
            words = str(text).split()  # Ensure text is string
            if max_words_per_line <= 0:
                max_words_per_line = 1
            return "<br>".join(
                [" ".join(words[i : i + max_words_per_line]) for i in range(0, len(words), max_words_per_line)]
            )

        if label and "title" not in kwargs:
            kwargs["title"] = _format_title_with_line_breaks(label)


##############################

# Charting Components

##############################


[docs] class Bar(PxBase): """ Displays a bar chart using Plotly Express. This component generates a bar chart from a Pandas DataFrame. It allows specifying the x and y axes, an optional dimension for coloring bars (grouping), and a label that is typically used as the chart title. Additional keyword arguments (`**kwargs`) are passed directly to the `plotly.express.bar` function, allowing for extensive customization. Args: df (pd.DataFrame): The DataFrame containing the data for the chart. x (str): The name of the column in `df` to be used for the x-axis. y (str): The name of the column in `df` to be used for the y-axis (representing bar heights). dimension (Optional[str], optional): The name of the column in `df` to be used for color-coding the bars. If provided, this will map unique values in the `dimension` column to different colors. Defaults to None. label (Optional[str], optional): An optional label for the chart, which is used to generate the chart title if no 'title' is provided in `**kwargs`. Defaults to None. **kwargs (Any): Additional keyword arguments passed directly to `plotly.express.bar()`. This can include arguments like `orientation`, `barmode`, `color_discrete_map`, etc. Raises: ValueError: If specified `x`, `y`, or `dimension` columns are not in `df`. """ def __init__( self, df: pd.DataFrame, x: str, y: str, *, dimension: str | None = None, label: str | None = None, **kwargs: Any, ): super().__init__(label=label) self.df = df self.x_col = x self.y_col = y self.dimension_col = dimension self.kwargs = kwargs if self.x_col not in self.df.columns: raise ValueError( f"X-axis column '{self.x_col}' not found in DataFrame columns: {self.df.columns.tolist()}." ) if self.y_col not in self.df.columns: raise ValueError( f"Y-axis column '{self.y_col}' not found in DataFrame columns: {self.df.columns.tolist()}." ) if self.dimension_col: if self.dimension_col not in self.df.columns: raise ValueError( f"Dimension column '{self.dimension_col}' not found in DataFrame columns: {self.df.columns.tolist()}." ) if "color" not in self.kwargs: self.kwargs["color"] = self.dimension_col PxBase.apply_common_kwargs(self.kwargs, label=self.label) logger.info( f"Bar chart: x='{self.x_col}', y='{self.y_col}', dimension='{self.dimension_col}', label='{self.label}'" )
[docs] @_strip_whitespace def to_html(self) -> str: """Generates the HTML representation of the bar chart.""" fig = px.bar(self.df, x=self.x_col, y=self.y_col, **self.kwargs) PxBase.apply_common_fig_options(fig) if "bargap" not in self.kwargs: # Apply default bargap if not user-specified fig.update_layout(bargap=0.1) html_fig = fig.to_html( include_plotlyjs=False, full_html=False, config={"responsive": True, "displayModeBar": True}, ) return "<section>" + html_fig + "</section>"
[docs] class Line(PxBase): """ Displays a line chart using Plotly Express. Plots one or more y-axis variables against an x-axis variable from a DataFrame. An optional `dimension` column can color-code and differentiate symbols for lines. Defaults to spline lines with markers. Args: df (pd.DataFrame): DataFrame for the chart. x (str): Column name for the x-axis. y (Union[str, list[str]]): Column name or list of column names for the y-axis. dimension (Optional[str], optional): Column name for color-coding and symbol differentiation. Defaults to None. label (Optional[str], optional): Chart label, used for title generation. Defaults to None. **kwargs (Any): Additional arguments for `plotly.express.line()`. Raises: ValueError: If specified `x`, `y`, or `dimension` columns are not in `df`, or if `y` is not a string or list of strings. """ def __init__( self, df: pd.DataFrame, x: str, y: str | list[str], *, dimension: str | None = None, label: str | None = None, **kwargs: Any, ): super().__init__(label=label) self.df = df self.x_col = x self.y_cols = y self.dimension_col = dimension self.kwargs = kwargs if self.x_col not in self.df.columns: raise ValueError( f"X-axis column '{self.x_col}' not found in DataFrame columns: {self.df.columns.tolist()}." ) self._validate_y_columns() if self.dimension_col: if self.dimension_col not in self.df.columns: raise ValueError( f"Dimension column '{self.dimension_col}' not found in DataFrame columns: {self.df.columns.tolist()}." ) if "color" not in self.kwargs: self.kwargs["color"] = self.dimension_col if "symbol" not in self.kwargs: self.kwargs["symbol"] = self.dimension_col if "line_shape" not in self.kwargs: self.kwargs["line_shape"] = "spline" if "markers" not in self.kwargs: self.kwargs["markers"] = True PxBase.apply_common_kwargs(self.kwargs, label=self.label) logger.info( f"Line chart: x='{self.x_col}', y(s)='{self.y_cols}', dimension='{self.dimension_col}', label='{self.label}'" ) def _validate_y_columns(self) -> None: """Validates that the specified y-axis column(s) exist in the DataFrame.""" if isinstance(self.y_cols, list): for y_col_name in self.y_cols: if y_col_name not in self.df.columns: raise ValueError( f"Y-axis column '{y_col_name}' not found in DataFrame columns: {self.df.columns.tolist()}." ) elif isinstance(self.y_cols, str): if self.y_cols not in self.df.columns: raise ValueError( f"Y-axis column '{self.y_cols}' not found in DataFrame columns: {self.df.columns.tolist()}." ) else: raise ValueError(f"Parameter 'y' must be a string or a list of strings, got {type(self.y_cols).__name__}.")
[docs] @_strip_whitespace def to_html(self) -> str: """Generates the HTML representation of the line chart.""" fig = px.line(self.df, x=self.x_col, y=self.y_cols, **self.kwargs) PxBase.apply_common_fig_options(fig) html_fig = fig.to_html( include_plotlyjs=False, full_html=False, config={"responsive": True, "displayModeBar": True}, ) return "<section>" + html_fig + "</section>"
[docs] class Pie(PxBase): """ Displays a pie chart (or donut chart if `hole` is specified) using Plotly Express. Visualizes parts of a whole, where `values` determine slice sizes and `names` provide slice labels. Defaults to a donut chart appearance (hole=0.4). Args: df (pd.DataFrame): DataFrame for the chart. values (str): Column name for slice values. names (str): Column name for slice names/labels. label (Optional[str], optional): Chart label, used for title generation. Defaults to None. **kwargs (Any): Additional arguments for `plotly.express.pie()`. Example: `hole=0` for a pie chart, `color_discrete_sequence`. Raises: ValueError: If specified `values` or `names` columns are not in `df`. """ def __init__( self, df: pd.DataFrame, values: str, names: str, *, label: str | None = None, **kwargs: Any, ): super().__init__(label=label) self.df = df self.values_col = values self.names_col = names self.kwargs = kwargs if self.values_col not in self.df.columns: raise ValueError( f"Values column '{self.values_col}' not found in DataFrame columns: {self.df.columns.tolist()}." ) if self.names_col not in self.df.columns: raise ValueError( f"Names column '{self.names_col}' not found in DataFrame columns: {self.df.columns.tolist()}." ) PxBase.apply_common_kwargs(self.kwargs, label=self.label) if "hole" not in self.kwargs: self.kwargs["hole"] = 0.4 # Default to donut logger.info(f"Pie chart: values='{self.values_col}', names='{self.names_col}', label='{self.label}'")
[docs] @_strip_whitespace def to_html(self) -> str: """Generates the HTML representation of the pie chart.""" fig = px.pie(self.df, values=self.values_col, names=self.names_col, **self.kwargs) fig.update_traces(textposition="inside", textinfo="percent+label") fig.update_layout(uniformtext_minsize=10, uniformtext_mode="hide") PxBase.apply_common_fig_options(fig) html_fig = fig.to_html( include_plotlyjs=False, full_html=False, config={"responsive": True, "displayModeBar": True}, ) return "<section>" + html_fig + "</section>"
[docs] class Radar(PxBase): """ Displays a radar chart (spider/star chart) using Plotly's graph objects. Each row in the input DataFrame represents a trace (entity), and each column an angular axis (category). The DataFrame index names the traces. Args: df (pd.DataFrame): DataFrame for the chart. - Index: Names for radar traces (must be unique, non-null). - Columns: Angular axes/categories. - Values: Magnitudes for each category per trace. label (Optional[str], optional): Chart label, used for title generation. Defaults to None. lock_minimum_to_zero (Optional[bool], optional): If True, radial axis starts at zero. Default False (axis minimum based on data). filled (Optional[bool], optional): If True, areas enclosed by traces are filled. Defaults to False. **kwargs (Any): Additional arguments. - `trace_kwargs` (dict): Passed to `go.Scatterpolar` traces. - Others used by `PxBase.apply_common_kwargs` (e.g., `title`, `height`). Raises: ValueError: If `df` is not a Pandas DataFrame, or if its index is invalid (empty, not unique, or contains NaNs). """ def __init__( self, df: pd.DataFrame, *, label: str | None = None, lock_minimum_to_zero: bool | None = False, filled: bool | None = False, **kwargs: Any, ): super().__init__(label=label) if not isinstance(df, pd.DataFrame): raise ValueError("Input 'df' must be a Pandas DataFrame.") if df.index is None or len(df.index) == 0: raise ValueError("DataFrame must have a non-empty index for Radar chart traces.") if not df.index.is_unique: raise ValueError("DataFrame index must be unique for Radar chart traces.") if df.index.hasnans: # Checks for NaNs in the index raise ValueError("DataFrame index must not contain NaNs for Radar chart traces.") self.df = df self.filled = filled self.kwargs = kwargs self.min_value = 0.0 if lock_minimum_to_zero else float(df.min(numeric_only=True).min(skipna=True)) self.max_value = float(df.max(numeric_only=True).max(skipna=True)) PxBase.apply_common_kwargs(self.kwargs, label=self.label) logger.info( f"Radar chart: {len(self.df)} traces, {len(self.df.columns)} categories. " f"Radial range: [{self.min_value}, {self.max_value}]. Filled: {self.filled}. Label: '{self.label}'" )
[docs] @_strip_whitespace def to_html(self) -> str: """Generates the HTML representation of the radar chart.""" theta_categories = self.df.columns.tolist() theta_closed_loop = theta_categories + theta_categories[:1] # Close the loop data = [] trace_specific_kwargs = self.kwargs.get("trace_kwargs", {}) # Optimize by avoiding repeated go.Scatterpolar instantiation and add_trace calls # Construct raw dictionaries instead. # Use numpy for faster array manipulation and iteration values = self.df.to_numpy() # Concatenate the first column to the end to close the loop r_closed_loop_values = np.hstack((values, values[:, :1])) r_data = r_closed_loop_values.tolist() trace_names = self.df.index.astype(str).tolist() for trace_name, r_closed_loop in zip(trace_names, r_data): # noqa: B905 trace = { "type": "scatterpolar", "r": r_closed_loop, "theta": theta_closed_loop, "fill": "toself" if self.filled else None, "name": trace_name, **trace_specific_kwargs, } data.append(trace) fig = go.Figure(data=data) fig.update_layout( polar={"radialaxis": {"visible": True, "range": [self.min_value, self.max_value]}}, height=self.kwargs.get("height", 600), # Allow height override title=self.kwargs.get("title"), # Title applied via PxBase ) PxBase.apply_common_fig_options(fig) html_fig = fig.to_html( include_plotlyjs=False, full_html=False, config={"responsive": True, "displayModeBar": True}, ) return "<section>" + html_fig + "</section>"
[docs] class Scatter(PxBase): """ Displays a scatter plot using Plotly Express. Shows the relationship between two numerical variables (`x` and `y`). An optional `dimension` can color-code and differentiate symbols. Marginal plots (histogram, box, etc.) can be added. Args: df (pd.DataFrame): DataFrame for the chart. x (str): Column name for the x-axis. y (str): Column name for the y-axis. dimension (Optional[str], optional): Column name for color-coding and symbol differentiation. Defaults to None. label (Optional[str], optional): Chart label, used for title generation. Defaults to None. marginal (Optional[str], optional): Adds marginal plots. Valid options: "histogram", "violin", "box", "rug". Defaults to None. **kwargs (Any): Additional arguments for `plotly.express.scatter()`. Raises: ValueError: If specified `x`, `y`, or `dimension` columns are not in `df`, or if `marginal` type is invalid. """ def __init__( self, df: pd.DataFrame, x: str, y: str, dimension: str | None = None, *, label: str | None = None, marginal: str | None = None, **kwargs: Any, ): super().__init__(label=label) self.df = df self.x_col = x self.y_col = y self.dimension_col = dimension self.kwargs = kwargs if self.x_col not in self.df.columns: raise ValueError( f"X-axis column '{self.x_col}' not found in DataFrame columns: {self.df.columns.tolist()}." ) if self.y_col not in self.df.columns: raise ValueError( f"Y-axis column '{self.y_col}' not found in DataFrame columns: {self.df.columns.tolist()}." ) VALID_MARGINALS = ["histogram", "violin", "box", "rug"] if marginal: if marginal not in VALID_MARGINALS: raise ValueError(f"Invalid 'marginal' type '{marginal}'. Must be one of {VALID_MARGINALS}.") self.kwargs["marginal_x"] = marginal self.kwargs["marginal_y"] = marginal if self.dimension_col: if self.dimension_col not in self.df.columns: raise ValueError( f"Dimension column '{self.dimension_col}' not found in DataFrame columns: {self.df.columns.tolist()}." ) if "color" not in self.kwargs: self.kwargs["color"] = self.dimension_col if "symbol" not in self.kwargs: self.kwargs["symbol"] = self.dimension_col PxBase.apply_common_kwargs(self.kwargs, label=self.label) logger.info( f"Scatter plot: x='{self.x_col}', y='{self.y_col}', dimension='{self.dimension_col}', " f"marginal='{marginal}', label='{self.label}'" )
[docs] @_strip_whitespace def to_html(self) -> str: """Generates the HTML representation of the scatter plot.""" fig = px.scatter(self.df, x=self.x_col, y=self.y_col, **self.kwargs) PxBase.apply_common_fig_options(fig) html_fig = fig.to_html( include_plotlyjs=False, full_html=False, config={"responsive": True, "displayModeBar": True}, ) return "<section>" + html_fig + "</section>"
[docs] class Box(PxBase): """ Displays a box plot using Plotly Express. Shows data distribution. `y` is the variable plotted. `dimension` (as `x`) groups data into multiple box plots. For wide-form data, plots can be generated for each numerical column if `y` is None. Args: df (pd.DataFrame): DataFrame for the chart. y (Optional[str], optional): Column name for the y-axis (variable for distribution). Can be None for wide-form data. Defaults to None. dimension (Optional[str], optional): Column name for x-axis grouping (categories). If provided, passed as `x` to `px.box`, and also used for `color` if `color` is not in `**kwargs`. Defaults to None. label (Optional[str], optional): Chart label, used for title generation. Defaults to None. **kwargs (Any): Additional arguments for `plotly.express.box()`. Raises: ValueError: If specified `y` or `dimension` columns are not in `df`. """ def __init__( self, df: pd.DataFrame, y: str | None = None, dimension: str | None = None, *, label: str | None = None, **kwargs: Any, ): super().__init__(label=label) self.df = df self.y_col = y self.dimension_col = dimension self.kwargs = kwargs if self.y_col and self.y_col not in self.df.columns: raise ValueError( f"Y-axis column '{self.y_col}' not found in DataFrame columns: {self.df.columns.tolist()}." ) if self.dimension_col: if self.dimension_col not in self.df.columns: raise ValueError( f"Dimension column '{self.dimension_col}' not found in DataFrame columns: {self.df.columns.tolist()}." ) if "x" not in self.kwargs: self.kwargs["x"] = self.dimension_col if "color" not in self.kwargs: self.kwargs["color"] = self.dimension_col PxBase.apply_common_kwargs(self.kwargs, label=self.label) logger.info(f"Box plot: y='{self.y_col}', dimension (as x)='{self.dimension_col}', label='{self.label}'")
[docs] @_strip_whitespace def to_html(self) -> str: """Generates the HTML representation of the box plot.""" fig = px.box(self.df, y=self.y_col, **self.kwargs) PxBase.apply_common_fig_options(fig) if "boxpoints" not in self.kwargs: # Show outliers by default fig.update_traces(boxpoints="outliers") html_fig = fig.to_html( include_plotlyjs=False, full_html=False, config={"responsive": True, "displayModeBar": True}, ) return "<section>" + html_fig + "</section>"
[docs] class Histogram(PxBase): """ Displays a histogram using Plotly Express. Visualizes the distribution of a single numerical variable (`x`). An optional `dimension` can color-code bars. Args: df (pd.DataFrame): DataFrame for the chart. x (str): Column name for the x-axis (numerical variable for distribution). dimension (Optional[str], optional): Column name for color-coding bars. Defaults to None. label (Optional[str], optional): Chart label, used for title generation. Defaults to None. **kwargs (Any): Additional arguments for `plotly.express.histogram()`. Raises: ValueError: If specified `x` or `dimension` columns are not in `df`. """ def __init__( self, df: pd.DataFrame, x: str, dimension: str | None = None, *, label: str | None = None, **kwargs: Any, ): super().__init__(label=label) self.df = df self.x_col = x self.dimension_col = dimension self.kwargs = kwargs if self.x_col not in self.df.columns: raise ValueError( f"X-axis column '{self.x_col}' not found in DataFrame columns: {self.df.columns.tolist()}." ) if self.dimension_col: if self.dimension_col not in self.df.columns: raise ValueError( f"Dimension column '{self.dimension_col}' not found in DataFrame columns: {self.df.columns.tolist()}." ) if "color" not in self.kwargs: self.kwargs["color"] = self.dimension_col PxBase.apply_common_kwargs(self.kwargs, label=self.label) logger.info(f"Histogram: x='{self.x_col}', dimension='{self.dimension_col}', label='{self.label}'")
[docs] @_strip_whitespace def to_html(self) -> str: """Generates the HTML representation of the histogram.""" fig = px.histogram(self.df, x=self.x_col, **self.kwargs) if "bargap" not in self.kwargs: # Default bargap for histograms fig.update_layout(bargap=0.1) PxBase.apply_common_fig_options(fig) html_fig = fig.to_html( include_plotlyjs=False, full_html=False, config={"responsive": True, "displayModeBar": True}, ) return "<section>" + html_fig + "</section>"
[docs] class Timeline(PxBase): """ Displays a Gantt/timeline chart. Renders tasks as horizontal bars on a date axis. Tasks appear in top-to-bottom order matching the DataFrame row order. Each data point is represented as a horizontal bar with a start and end point specified as dates. Args: df (pd.DataFrame): DataFrame containing the task data. task (str): Column name for task labels (y-axis). start (str): Column name for task start datetimes. Must be datetime dtype; use ``pd.to_datetime()`` to convert string columns. finish (str): Column name for task finish datetimes. Must be datetime dtype. dimension (Optional[str]): Column name for color-coding tasks (e.g. team or phase). Defaults to None. label (Optional[str]): Chart title. Defaults to None. **kwargs: Additional keyword arguments passed to ``plotly.express.timeline()``. Raises: ValueError: If any required column is missing from ``df``. ValueError: If ``start`` or ``finish`` columns are not datetime dtype. ValueError: If ``dimension`` column is specified but not found in ``df``. Example: .. code-block:: python df = pd.DataFrame({ "task": ["Design", "Development", "Testing", "Deploy"], "start": pd.to_datetime(["2024-01-01", "2024-01-15", "2024-02-01", "2024-02-15"]), "finish": pd.to_datetime(["2024-01-14", "2024-01-31", "2024-02-14", "2024-02-28"]), "team": ["Engineering", "Engineering", "QA", "DevOps"], }) rc.Timeline(df, task="task", start="start", finish="finish", dimension="team", label="Q1 Project Plan") """ def __init__( self, df: pd.DataFrame, task: str, start: str, finish: str, *, dimension: str | None = None, label: str | None = None, **kwargs: Any, ): super().__init__(label=label) self.df = df self.task_col = task self.start_col = start self.finish_col = finish self.dimension_col = dimension self.kwargs = kwargs if self.task_col not in self.df.columns: raise ValueError( f"Task column '{self.task_col}' not found in DataFrame columns: {self.df.columns.tolist()}." ) if self.start_col not in self.df.columns: raise ValueError( f"Start column '{self.start_col}' not found in DataFrame columns: {self.df.columns.tolist()}." ) if self.finish_col not in self.df.columns: raise ValueError( f"Finish column '{self.finish_col}' not found in DataFrame columns: {self.df.columns.tolist()}." ) if not is_datetime64_any_dtype(self.df[self.start_col]): raise ValueError( f"Start column '{self.start_col}' must contain datetime values. Use pd.to_datetime() to convert." ) if not is_datetime64_any_dtype(self.df[self.finish_col]): raise ValueError( f"Finish column '{self.finish_col}' must contain datetime values. Use pd.to_datetime() to convert." ) if self.dimension_col: if self.dimension_col not in self.df.columns: raise ValueError( f"Dimension column '{self.dimension_col}' not found in DataFrame columns: {self.df.columns.tolist()}." ) if "color" not in self.kwargs: self.kwargs["color"] = self.dimension_col PxBase.apply_common_kwargs(self.kwargs, label=self.label) logger.info( f"Timeline: task='{self.task_col}', start='{self.start_col}', " f"finish='{self.finish_col}', dimension='{self.dimension_col}', label='{self.label}'" )
[docs] @_strip_whitespace def to_html(self) -> str: """Generates the HTML representation of the timeline (Gantt) chart.""" fig = px.timeline( self.df, x_start=self.start_col, x_end=self.finish_col, y=self.task_col, **self.kwargs, ) fig.update_yaxes(autorange="reversed") # First task at top (conventional Gantt order) PxBase.apply_common_fig_options(fig) html_fig = fig.to_html( include_plotlyjs=False, full_html=False, config={"responsive": True, "displayModeBar": True}, ) return "<section>" + html_fig + "</section>"
##############################
[docs] class Gauge(Base): """ Displays a radial gauge chart for visualizing a single value within a range. The `Gauge` component is ideal for tracking progress towards a goal or representing a metric that sits on a scale (e.g., speed, temperature, completion percentage). Args: value (float): The current value to be displayed on the gauge. min_value (float, optional): The minimum value of the gauge scale. Defaults to 0. max_value (float, optional): The maximum value of the gauge scale. Defaults to 100. title (Optional[str], optional): A label for the gauge. Defaults to None. unit (Optional[str], optional): A unit of measurement for the value. Defaults to None. color (Optional[str], optional): The color of the gauge bar. If None, the library's default color palette is used. Example: .. code-block:: python import report_creator as rc rc.Gauge(value=75.5, title="CPU Usage", unit="%") """ def __init__( self, value: float, min_value: float = 0, max_value: float = 100, title: str | None = None, unit: str | None = None, color: str | None = None, ): super().__init__(label=title) self.value = value self.min_value = min_value self.max_value = max_value self.title = title self.unit = unit self.color = color or report_creator_colors[0]
[docs] def to_html(self) -> str: """Generates the HTML representation of the gauge chart.""" fig = go.Figure( go.Indicator( mode="gauge+number", value=self.value, title={"text": self.title, "font": {"size": 16}} if self.title else None, number={"suffix": self.unit, "font": {"size": 24}} if self.unit else {"font": {"size": 24}}, gauge={ "axis": {"range": [self.min_value, self.max_value]}, "bar": {"color": self.color}, "bgcolor": "white", "borderwidth": 2, "bordercolor": "#eee", }, ) ) fig.update_layout( font_family=preferred_fonts[0] if preferred_fonts else "sans-serif", autosize=True, margin={"l": 20, "r": 20, "t": 50, "b": 20}, height=250, ) html_fig = fig.to_html( include_plotlyjs=False, full_html=False, config={"responsive": True, "displayModeBar": False}, ) return html_fig
##############################
[docs] class Bullet(Base): """ Displays a bullet chart for visualizing a single value against a target and ranges. Bullet charts are a variation of bar charts designed to replace dashboard gauges and meters. They are more space-efficient and provide more context, such as performance ranges (e.g., poor, satisfactory, good) and a target marker. Args: value (float): The current value to be displayed. target (float): The target value to reach. min_value (float, optional): The minimum value of the scale. Defaults to 0. max_value (float, optional): The maximum value of the scale. Defaults to 100. title (Optional[str], optional): A label for the bullet chart. Defaults to None. unit (Optional[str], optional): A unit of measurement for the value. Defaults to None. color (Optional[str], optional): The color of the main value bar. Defaults to the library's default color palette. Example: .. code-block:: python import report_creator as rc rc.Bullet(value=85, target=90, title="Sales Performance", unit="%") """ def __init__( self, value: float, target: float, min_value: float = 0, max_value: float = 100, title: str | None = None, unit: str | None = None, color: str | None = None, ): super().__init__(label=title) self.value = value self.target = target self.min_value = min_value self.max_value = max_value self.title = title self.unit = unit self.color = color or report_creator_colors[0]
[docs] def to_html(self) -> str: """Generates the HTML representation of the bullet chart.""" fig = go.Figure( go.Indicator( mode="number+gauge+delta", value=self.value, delta={"reference": self.target, "position": "right"}, title={"text": self.title, "font": {"size": 16}} if self.title else None, number={"suffix": self.unit, "font": {"size": 24}} if self.unit else {"font": {"size": 24}}, gauge={ "shape": "bullet", "axis": {"range": [self.min_value, self.max_value]}, "threshold": { "line": {"color": "black", "width": 2}, "thickness": 0.75, "value": self.target, }, "bar": {"color": self.color, "thickness": 0.5}, "bgcolor": "white", "borderwidth": 1, "bordercolor": "#eee", }, ) ) fig.update_layout( font_family=preferred_fonts[0] if preferred_fonts else "sans-serif", autosize=True, margin={"l": 20, "r": 20, "t": 50, "b": 20}, height=150, ) html_fig = fig.to_html( include_plotlyjs=False, full_html=False, config={"responsive": True, "displayModeBar": False}, ) return "<section>" + html_fig + "</section>"