Source code for datawrapper.charts.bar

from typing import Any, Literal

import pandas as pd
from pydantic import BaseModel, ConfigDict, Field, field_validator

from .base import BaseChart
from .enums import DateFormat, NumberFormat, ReplaceFlagsType, ValueLabelAlignment
from .models import AnnotationsMixin
from .serializers import ColorCategory, CustomRange, ReplaceFlags


class BarOverlay(BaseModel):
    """A base class for the Datawrapper API's 'bar-overlay' attribute."""

    model_config = ConfigDict(
        populate_by_name=True,
        strict=True,
        json_schema_extra={
            "examples": [
                {
                    "to_column": "column",
                }
            ]
        },
    )

    #: The type of overlay
    type: Literal["value", "range"] = Field(
        default="value", description="The type of overlay"
    )

    #: The title of the overlay. Defaults to the column name.
    title: str = Field(
        default="", description="The title of the overlay. Defaults to the column name."
    )

    #: The column to draw ranges to or to label, depending on the type
    to_column: str = Field(
        min_length=1,
        alias="to",
        description="The column to draw ranges to or to label, depending on the type",
    )

    #: The column to draw ranges from
    from_column: str = Field(
        default="--zero-baseline--",  # This is datawrapper's default string value for zero
        alias="from",
        description="The column to draw ranges from",
    )

    #: The color of the overlay
    color: str = Field(default="#4682b4", description="The color of the overlay")

    #: The opacity of the overlay
    opacity: float = Field(default=0.6, description="The opacity of the overlay")

    #: The pattern of the overlay when the type is 'range'
    pattern: Literal["solid", "diagonal-up", "diagonal-down"] = Field(
        default="solid",
        description="The pattern of the overlay when the type is 'range'",
    )

    #: Whether or not to show the overlay in the color key
    show_in_color_key: bool = Field(
        default=True,
        alias="showInColorKey",
        description="Whether or not to show the overlay in the color key",
    )

    #: Whether or not to show the overlay directly on the bar
    label_directly: bool = Field(
        default=True,
        alias="labelDirectly",
        description="Whether or not to show the overlay directly on the bar",
    )


[docs] class BarChart(AnnotationsMixin, BaseChart): """A base class for the Datawrapper API's bar chart.""" model_config = ConfigDict( populate_by_name=True, strict=True, validate_assignment=True, validate_default=True, use_enum_values=True, json_schema_extra={ "examples": [ { "chart-type": "d3-bars", "title": "European countries with lowest & highest voter turnout", "source_name": "Parties & Elections, 2024", "source_url": "https://parties-and-elections.eu/", "highlighted-series": [ "Malta (2022)", "Turkey (2023)", "Belgium (2024)", "Romania (2020)", "Bulgaria (2024)", "Albania (2021)", ], "custom-range": [0, 100], "background": True, "sort-bars": True, "tick-position": "top", "data": pd.DataFrame( { "Country": [ "Malta (2022)", "Turkey (2023)", "Belgium (2024)", "Romania (2020)", "Bulgaria (2024)", "Albania (2021)", "United Kingdom (2024)", "Germany (2021)", "Sweden (2022)", "Spain (2023)", "France (2024)", ], "turnout": [ "85.6", "87.0", "88.5", "33.2", "33.4", "46.3", "60.0", "76.4", "83.8", "66.0", "66.7", ], } ), } ] }, ) #: The type of datawrapper chart to create chart_type: Literal["d3-bars"] = Field( default="d3-bars", alias="chart-type", description="The type of datawrapper chart to create", ) # # Labels # #: The column with the labels for the bars label_column: str = Field( default="", alias="label-column", description="The column with the labels for the bars", ) #: On which side are the labels aligned label_alignment: Literal["left", "right"] = Field( default="left", alias="label-alignment", description="On which side are the labels aligned", ) #: Whether to move labels to a separate line block_labels: bool = Field( default=False, alias="block-labels", description="Whether to move labels to a separate line", ) #: Whether or not to show value labels with the bars show_value_labels: bool = Field( default=True, alias="show-value-labels", description="Whether or not to show value labels with the bars", ) #: The alignment of the value labels value_label_alignment: ValueLabelAlignment | str = Field( default="left", alias="value-label-alignment", description="The alignment of the value labels", ) #: The format of the value labels (use DateFormat or NumberFormat enum or custom format strings) value_label_format: DateFormat | NumberFormat | str = Field( default="", alias="value-label-format", description="The format of the value labels. Use DateFormat for temporal data, NumberFormat for numeric data, or provide custom format strings.", ) #: Whether to swap labels and values swap_labels: bool = Field( default=False, alias="swap-labels", description="Whether to swap labels and values", ) #: Whether to replace country codes with flag replace_flags: ReplaceFlagsType | str = Field( default="off", alias="replace-flags", description="Whether to replace country codes with flag", ) #: Whether to show the color key show_color_key: bool = Field( default=False, alias="show-color-key", description="Whether to show the color key", ) #: Whether to stack the color key stack_color_legend: bool = Field( default=False, alias="stack-color-legend", description="Whether to stack the color key", ) #: A list of column to exclude from the color key exclude_from_color_key: list[str] = Field( default_factory=list, alias="exclude-from-color-key", description="A list of column to exclude from the color key", ) # # Horizontal axis # #: The column with the value for the bars bar_column: str = Field( default="", alias="bar-column", description="The column with the value for the bars", ) #: The custom range for the x axis custom_range: list[Any] | tuple[Any, Any] = Field( default_factory=lambda: ["", ""], alias="custom-range", description="The custom range for the x axis", ) #: Whether or not to show the x grid force_grid: bool = Field( default=False, alias="force-grid", description="Whether or not to show the x grid", ) #: Set custom grid lines custom_grid_lines: list[Any] = Field( default_factory=list, alias="custom-grid-lines", description="Set custom grid lines", ) #: The position of the ticks tick_position: Literal["top", "bottom"] = Field( default="top", alias="tick-position", description="The position of the ticks" ) #: The format of the axis labels (use DateFormat or NumberFormat enum or custom format strings) axis_label_format: DateFormat | NumberFormat | str = Field( default="", alias="axis-label-format", description="The format of the axis labels. Use DateFormat for temporal data, NumberFormat for numeric data, or provide custom format strings.", ) # # Appearance # #: The default color for the chart (palette index or hex string) base_color: str | int = Field( default=0, alias="base-color", description="The default color for the chart (palette index or hex string)", ) #: The name of the column with the color for the bars color_column: str = Field( default="", alias="color-column", description="The name of the column with the color for the bars", ) #: A mapping of layer names to colors color_category: dict[str, str] = Field( default_factory=dict, alias="color-category", description="A mapping of layer names to colors", ) #: Dictionary mapping category names to their display labels in the color legend category_labels: dict[str, str] = Field( default_factory=dict, alias="category-labels", description="Dictionary mapping category names to their display labels in the color legend", ) #: List defining the order in which categories appear in the chart and legend category_order: list[str] = Field( default_factory=list, alias="category-order", description="List defining the order in which categories appear in the chart and legend", ) #: Draw a separating line between bars rules: bool = Field( default=False, alias="rules", description="Draw a separating line between bars" ) #: Make the bars thicker thick_bars: bool = Field( default=False, alias="thick", description="Make the bars thicker" ) #: Fill the background of the bar's full potential with a light color background: bool = Field( default=False, alias="background", description="Fill the background of the bar's full potential with a light color", ) # # Sorting and grouping # #: Whether to sort the bars sort_bars: bool = Field( default=False, alias="sort-bars", description="Whether to sort the bars" ) #: Whether to reverse the sort order reverse_order: bool = Field( default=False, alias="reverse-order", description="Whether to reverse the sort order", ) #: The column to use for grouping bars groups_column: str | None = Field( default=None, alias="groups-column", description="The column to use for grouping bars", ) #: Whether to show the group labels show_group_labels: bool = Field( default=True, alias="show-group-labels", description="Whether to show the group labels", ) #: Whether to show the value labels show_category_labels: bool = Field( default=True, alias="show-category-labels", description="Whether to show the value labels", ) # # Overlays # #: A list of bar overlays overlays: list[BarOverlay | dict[str, Any]] = Field( default_factory=list, description="A list of bar overlays", ) # # Annotations # #: A list of the highlighted series highlighted_series: list[str] = Field( default_factory=list, alias="highlighted-series", description="A list of the highlighted series", )
[docs] @field_validator("replace_flags") @classmethod def validate_replace_flags( cls, v: ReplaceFlagsType | str ) -> ReplaceFlagsType | str: """Validate that replace_flags is a valid ReplaceFlagsType value.""" if isinstance(v, str): valid_values = [e.value for e in ReplaceFlagsType] if v not in valid_values: raise ValueError( f"Invalid replace_flags: {v}. Must be one of {valid_values}" ) return v
[docs] def serialize_model(self) -> dict: """Serialize the model to a dictionary.""" # Call the parent class's serialize_model method model = super().serialize_model() # Add chart specific properties model["metadata"]["visualize"].update( { # Labels "label-alignment": self.label_alignment, "block-labels": self.block_labels, "show-value-labels": self.show_value_labels, "value-label-alignment": self.value_label_alignment, "value-label-format": self.value_label_format, "swap-labels": self.swap_labels, "replace-flags": ReplaceFlags.serialize(self.replace_flags), "show-color-key": self.show_color_key, "stack-color-legend": self.stack_color_legend, # Horizontal axis "custom-range": CustomRange.serialize(self.custom_range), "force-grid": self.force_grid, "custom-grid-lines": ",".join(str(t) for t in self.custom_grid_lines), "tick-position": self.tick_position, "axis-label-format": self.axis_label_format, # Appearance "base-color": self.base_color, "color-category": ColorCategory.serialize( self.color_category, self.category_labels, self.category_order, self.exclude_from_color_key, ), "color-by-column": bool(self.color_category), "rules": self.rules, "thick": self.thick_bars, "background": self.background, # Sorting and grouping "sort-bars": self.sort_bars, "reverse-order": self.reverse_order, "group-by-column": self.groups_column is not None and self.groups_column != "", "show-group-labels": self.show_group_labels, "show-category-labels": self.show_category_labels, # Overlays "overlays": [], # Annotations "highlighted-series": self.highlighted_series, } ) # Add annotations model["metadata"]["visualize"].update(self._serialize_annotations()) # Add the overlays, if any for overlay_obj in self.overlays: # If the overlay is a dictionary, validate it and convert it to a BarOverlay object if isinstance(overlay_obj, dict): overlay_dict = BarOverlay.model_validate(overlay_obj).model_dump( by_alias=True ) # If the overlay is a BarOverlay object, convert it to a dictionary elif isinstance(overlay_obj, BarOverlay): overlay_dict = overlay_obj.model_dump(by_alias=True) # If the overlay is neither, raise an error else: raise ValueError("Overlays must be BarOverlay objects or dicts") # Add the overlay to the list of overlays model["metadata"]["visualize"]["overlays"].append(overlay_dict) # Add axes configuration to metadata axes_config = { "colors": self.color_column or self.label_column, "bars": self.bar_column, "labels": self.label_column, } # Only add groups if it's set if self.groups_column: axes_config["groups"] = self.groups_column model["metadata"]["axes"] = axes_config # Return the serialized data return model
[docs] @classmethod def deserialize_model(cls, api_response: dict[str, Any]) -> dict[str, Any]: """Parse Datawrapper API response including bar chart specific fields. Args: api_response: The JSON response from the chart metadata endpoint Returns: Dictionary that can be used to initialize the BarChart model """ # Call parent to get base fields init_data = super().deserialize_model(api_response) # Extract bar-specific sections metadata = api_response.get("metadata", {}) visualize = metadata.get("visualize", {}) axes = metadata.get("axes", {}) # Labels if "labels" in axes: init_data["label_column"] = axes["labels"] if "label-alignment" in visualize: init_data["label_alignment"] = visualize["label-alignment"] if "block-labels" in visualize: init_data["block_labels"] = visualize["block-labels"] if "show-value-labels" in visualize: init_data["show_value_labels"] = visualize["show-value-labels"] if "value-label-alignment" in visualize: init_data["value_label_alignment"] = visualize["value-label-alignment"] if "value-label-format" in visualize: init_data["value_label_format"] = visualize["value-label-format"] if "swap-labels" in visualize: init_data["swap_labels"] = visualize["swap-labels"] # Replace flags if "replace-flags" in visualize: init_data["replace_flags"] = ReplaceFlags.deserialize( visualize["replace-flags"] ) if "show-color-key" in visualize: init_data["show_color_key"] = visualize["show-color-key"] if "stack-color-legend" in visualize: init_data["stack_color_legend"] = visualize["stack-color-legend"] # Horizontal axis if "bars" in axes: init_data["bar_column"] = axes["bars"] init_data["custom_range"] = CustomRange.deserialize( visualize.get("custom-range") ) if "force-grid" in visualize: init_data["force_grid"] = visualize["force-grid"] # Parse custom grid lines (comes as comma-separated string) if "custom-grid-lines" in visualize: grid_lines_str = visualize["custom-grid-lines"] if grid_lines_str: init_data["custom_grid_lines"] = [ float(x.strip()) if x.strip() else x.strip() for x in grid_lines_str.split(",") ] if "tick-position" in visualize: init_data["tick_position"] = visualize["tick-position"] if "axis-label-format" in visualize: init_data["axis_label_format"] = visualize["axis-label-format"] # Appearance if "base-color" in visualize: init_data["base_color"] = visualize["base-color"] if "colors" in axes: init_data["color_column"] = axes["colors"] # Parse color-category using utility init_data.update(ColorCategory.deserialize(visualize.get("color-category"))) if "rules" in visualize: init_data["rules"] = visualize["rules"] if "thick" in visualize: init_data["thick_bars"] = visualize["thick"] if "background" in visualize: init_data["background"] = visualize["background"] # Sorting and grouping if "sort-bars" in visualize: init_data["sort_bars"] = visualize["sort-bars"] if "reverse-order" in visualize: init_data["reverse_order"] = visualize["reverse-order"] if "groups" in axes: init_data["groups_column"] = axes["groups"] if "show-group-labels" in visualize: init_data["show_group_labels"] = visualize["show-group-labels"] if "show-category-labels" in visualize: init_data["show_category_labels"] = visualize["show-category-labels"] # Overlays (can be dict with UUID keys or list of BarOverlay objects) if "overlays" in visualize: overlays_data = visualize["overlays"] # Handle both dict (with UUID keys) and list formats if isinstance(overlays_data, dict): init_data["overlays"] = [ BarOverlay.model_validate(overlay) for overlay in overlays_data.values() ] elif isinstance(overlays_data, list): init_data["overlays"] = [ BarOverlay.model_validate(overlay) for overlay in overlays_data ] # Annotations if "highlighted-series" in visualize: init_data["highlighted_series"] = visualize["highlighted-series"] init_data.update(cls._deserialize_annotations(visualize)) return init_data