Source code for datawrapper.charts.base

import os
import warnings
from io import StringIO
from typing import Any, Literal

import pandas as pd
from IPython.display import IFrame
from pydantic import BaseModel, ConfigDict, Field, model_validator

from datawrapper.__main__ import Datawrapper
from datawrapper.charts.models import Annotate, Describe, Publish, Transform, Visualize


[docs] class BaseChart(BaseModel): """A base class for Datawrapper charts published via its API.""" model_config = ConfigDict( populate_by_name=True, arbitrary_types_allowed=True, validate_assignment=True, validate_default=True, use_enum_values=True, json_schema_extra={ "examples": [ # Example 1: A simple chart without much configuration { "chart_type": "d3-lines", "title": "Sample Chart Title", "source_name": "Reuters", "language": "en-US", }, # Example 2: Transform as dict { "chart_type": "d3-lines", "title": "Sample Chart Title", "source_name": "Reuters", "language": "en-US", "transformations": { "transpose": False, "vertical-header": True, "horizontal-header": True, "column-order": [0, 1, 2], "column-format": [ {"column": "sales", "type": "number", "number-prepend": "$"} ], "external-data": "", "use-datawrapper-cdn": True, "upload-method": "copy", }, }, # Example 3: Transform as Transform object (using aliases) { "chart_type": "d3-lines", "title": "Sample Chart Title", "source_name": "Reuters", "language": "en-US", "transformations": { "transpose": False, "vertical-header": True, "horizontal-header": True, "column-order": [0, 1, 2], "column-format": [ {"column": "sales", "type": "number", "number-prepend": "$"} ], "external-data": "", "use-datawrapper-cdn": True, "upload-method": "copy", }, }, ] }, ) #: The type of datawrapper chart to create chart_type: Literal[ "column-chart", "d3-area", "d3-arrow-plot", "d3-bars", "d3-bars-stacked", "d3-lines", "d3-scatter-plot", "locator-map", "multiple-columns", ] = Field(alias="chart-type", description="The type of datawrapper chart to create") # # Data # #: The data to use for the chart data: pd.DataFrame | list[dict] = Field( default_factory=list[dict], description="The data to use for the chart" ) #: The metadata options for the data columns in the "Check and Describe" tab transformations: Transform | dict[str, Any] = Field(default_factory=Transform) # # Description # #: The headline that appears above the chart title: str = Field( default="", description="The headline that appears above the chart" ) #: The intro text that appears above the chart intro: str = Field( default="", description="The intro text that appears above the chart" ) #: The footnotes that appear below the chart notes: str = Field( default="", description="The footnotes that appear below the chart" ) #: The source name that appears below the chart source_name: str = Field( default="", alias="source-name", description="The source name that appears below the chart", ) #: The source URL that appears below the chart source_url: str = Field( default="", alias="source-url", description="The source URL that appears below the chart", ) #: The byline that appears below the chart byline: str = Field( default="", description="The byline that appears below the chart" ) #: The alternative text for screen readers aria_description: str = Field( default="", alias="aria-description", description="The alternative text for screen readers", ) #: Whether or not to hide the title hide_title: bool = Field( default=False, alias="hide-title", description="Whether or not to hide the title", ) # # Layout # #: The locale of the chart, which defines decimal and thousand separators as well as translations of month and weekday names. language: str = Field( default="en-US", description="The locale of the chart, which defines decimal and thousand separators as well as translations of month and weekday names.", ) #: The theme of the chart theme: str = Field(default="datawrapper", description="The theme of the chart") #: Whether the chart should automatically flip to dark mode when the user's system is in dark mode auto_dark_mode: bool = Field( default=False, alias="autoDarkMode", description="Whether the chart should automatically flip to dark mode when the user's system is in dark mode", ) #: Whether to invert colors in dark mode dark_mode_invert: bool = Field( default=True, alias="dark-mode-invert", description="Whether to invert colors in dark mode", ) #: Whether to allow data downloads get_the_data: bool = Field( default=False, alias="get-the-data", description="Whether to allow data downloads", ) #: Whether to allow PNG download download_image: bool = Field( default=False, alias="download-image", description="Whether to allow PNG download", ) #: Whether to allow PDF download download_pdf: bool = Field( default=False, alias="download-pdf", description="Whether to allow PDF download" ) #: Whether to allow SVG download download_svg: bool = Field( default=False, alias="download-svg", description="Whether to allow SVG download" ) #: Whether to allow embedding embed: bool = Field(default=False, description="Whether to allow embedding") #: Whether to attribute the chart to datawrapper force_attribution: bool = Field( default=False, alias="force-attribution", description="Whether to attribute the chart to datawrapper", ) #: Whether to show social media share buttons share_buttons: bool = Field( default=False, description="Whether to show social media share buttons" ) #: What URL to share share_url: str = Field(default="", description="What URL to share") #: Whether to show a logo logo: bool = Field(default=False, description="Whether to show a logo") #: The id of the logo to show logo_id: str = Field(default="", description="The id of the logo to show") #: A dictionary of custom tags to attach to the chart custom: dict[str, Any] = Field( default_factory=dict, description="A dictionary of custom tags to attach to the chart", ) # # Chart Settings # #: Whether to allow other users to fork this visualization forkable: bool = Field( default=True, description="Whether to allow other users to fork this visualization", ) #: The chart ID after creation (populated by create() method) chart_id: str | None = Field( default=None, description="The chart ID after creation (populated by create() method)", exclude=True, # Don't include in serialization ) # # Serialization methods for preparing data for API upload # def serialize_model(self) -> dict[str, Any]: # Create a dict with the bare minimum provided by the base chart class # This will be supplemented by subclasses tailored to individual chart types # Start with root-level metadata required by Datawrapper API dw_obj: dict[str, Any] = { "type": self.chart_type, # Note: API expects "type", not "chart-type" "title": self.title, "language": self.language, } # Only include theme if it's not empty if self.theme: dw_obj["theme"] = self.theme # Set the transformations data_section = ( self.transformations if isinstance(self.transformations, Transform) else Transform.model_validate(self.transformations) ).model_dump(by_alias=True) # Validate the Describe data describe = Describe.model_validate( { "intro": self.intro, "byline": self.byline, "source-name": self.source_name, "source-url": self.source_url, "aria-description": self.aria_description, "hide-title": self.hide_title, } ) # Validate the Annotate data annotate = Annotate.model_validate( { "notes": self.notes, } ) # Validate the Visualize data visualize = Visualize.model_validate( { "dark-mode-invert": self.dark_mode_invert, "sharing": { "enabled": self.share_buttons, "url": self.share_url, "auto": False, }, } ) # Validate the Publish data publish = Publish.model_validate( { "autoDarkMode": self.auto_dark_mode, "force-attribution": self.force_attribution, "blocks": { "get-the-data": self.get_the_data, "download-image": self.download_image, "download-pdf": self.download_pdf, "download-svg": self.download_svg, "embed": self.embed, "logo": { "id": self.logo_id, "enabled": self.logo, }, }, } ) # Create the metadata section in the proper Datawrapper order dw_obj["metadata"] = { "data": data_section, "describe": describe.model_dump(by_alias=True), "visualize": visualize.model_dump(by_alias=True), "publish": publish.model_dump(by_alias=True), "annotate": annotate.model_dump(by_alias=True), "custom": self.custom, } # Return the obj return dw_obj
[docs] def serialize_data(self) -> str | None: """Convert data to CSV string for API upload. Returns: CSV string representation of the data, or None if data is empty. """ # Check if data is empty if isinstance(self.data, pd.DataFrame): if self.data.empty: return None else: if not bool(self.data): return None # Convert to CSV if isinstance(self.data, pd.DataFrame): return self.data.to_csv(index=False, encoding="utf-8") else: # Convert list of dicts to DataFrame first, then to CSV df = pd.DataFrame(self.data) return df.to_csv(index=False, encoding="utf-8")
# # Deserialization methods for parsing API responses and input data #
[docs] @model_validator(mode="before") @classmethod def warn_on_unrecognized_fields(cls, data: dict[str, Any]) -> dict[str, Any]: """Warn users about unrecognized fields that don't match model fields or aliases. This validator checks incoming data against the model's defined fields and their aliases, issuing warnings for any keys that don't match. This helps catch typos and misunderstandings about the API without breaking initialization. """ if not isinstance(data, dict): return data # Get all valid field names and aliases for this class valid_keys = set() for field_name, field_info in cls.model_fields.items(): # Add the Python field name valid_keys.add(field_name) # Add the alias if it exists if field_info.alias: valid_keys.add(field_info.alias) # Check for unrecognized keys (excluding private attributes) unrecognized = [] for key in data.keys(): if not key.startswith("_") and key not in valid_keys: unrecognized.append(key) # Emit warnings for unrecognized fields if unrecognized: warnings.warn( f"{cls.__name__} received unrecognized field(s): {', '.join(sorted(unrecognized))}. " f"These fields will be ignored. Check for typos or refer to the documentation " f"for valid field names.", UserWarning, stacklevel=2, ) return data
[docs] @model_validator(mode="before") @classmethod def convert_column_format_dicts(cls, data: dict[str, Any]) -> dict[str, Any]: """Convert dictionary items in transformation to Transform objects.""" if not isinstance(data, dict): return data if "transformations" in data and isinstance(data["transformations"], dict): data["transformations"] = Transform.model_validate(data["transformations"]) return data
[docs] @classmethod def deserialize_data(cls, csv_data: str | pd.DataFrame) -> pd.DataFrame: """Parse CSV string from Datawrapper API into DataFrame. Args: csv_data: The CSV data from the chart data endpoint Returns: DataFrame containing the parsed CSV data """ # Use sep=None with engine='python' to auto-detect delimiter (comma or tab) if isinstance(csv_data, pd.DataFrame): return csv_data return pd.read_csv(StringIO(csv_data), sep=None, engine="python")
[docs] @classmethod def deserialize_model(cls, api_response: dict[str, Any]) -> dict[str, Any]: """Parse Datawrapper API response into model initialization data. This base implementation handles common fields. Subclasses should override to handle chart-specific visualize settings. Args: api_response: The JSON response from the chart metadata endpoint Returns: Dictionary that can be used to initialize the model (without data) """ metadata = api_response.get("metadata", {}) # Extract common fields describe = metadata.get("describe", {}) annotate = metadata.get("annotate", {}) publish = metadata.get("publish", {}) publish_blocks = publish.get("blocks", {}) publish_logo = publish_blocks.get("logo", {}) visualize = metadata.get("visualize", {}) visualize_sharing = visualize.get("sharing", {}) # Build base initialization dict without hardcoded defaults # Pydantic will apply model field defaults for any missing values result = { # Chart type and basic info "chart_type": api_response.get("type"), "title": api_response.get("title"), "theme": api_response.get("theme"), "language": api_response.get("language"), "forkable": api_response.get("forkable"), # Data transformations (but not the data itself) "transformations": Transform.model_validate(metadata.get("data", {})), # Description "intro": describe.get("intro"), "notes": annotate.get("notes"), "source_name": describe.get("source-name"), "source_url": describe.get("source-url"), "byline": describe.get("byline"), "aria_description": describe.get("aria-description"), "hide_title": describe.get("hide-title"), # Layout/Publish "auto_dark_mode": publish.get("autoDarkMode"), "dark_mode_invert": visualize.get("dark-mode-invert"), "get_the_data": publish_blocks.get("get-the-data"), "download_image": publish_blocks.get("download-image"), "download_pdf": publish_blocks.get("download-pdf"), "download_svg": publish_blocks.get("download-svg"), "embed": publish_blocks.get("embed"), "force_attribution": publish.get("force-attribution"), "share_buttons": visualize_sharing.get("enabled"), "share_url": visualize_sharing.get("url"), "logo": publish_logo.get("enabled"), "logo_id": publish_logo.get("id"), # Custom "custom": metadata.get("custom"), } # Remove None values to let Pydantic apply model defaults return {k: v for k, v in result.items() if v is not None}
# # CRUD methods for Datawrapper API # def __init__(self, **data): """Initialize the BaseChart with private attributes.""" super().__init__(**data) self._client = None def _get_client(self, access_token: str | None = None) -> Datawrapper: """Get or create a Datawrapper client instance. Args: access_token: Optional Datawrapper API access token. If not provided, will use DATAWRAPPER_ACCESS_TOKEN environment variable. Returns: An instance of the Datawrapper client. """ if self._client is not None: return self._client # Try to get access token from parameter, environment, or raise error token = access_token or os.getenv("DATAWRAPPER_ACCESS_TOKEN") if not token: raise ValueError( "No Datawrapper access token provided. " "Set DATAWRAPPER_ACCESS_TOKEN environment variable or pass access_token parameter." ) self._client = Datawrapper(access_token=token) return self._client @classmethod def _validate_chart_type(cls, chart_type: str) -> None: """Validate that the chart type matches the class's allowed types. Args: chart_type: The chart type from the API response Raises: ValueError: If chart type doesn't match the class's constraints """ if hasattr(cls, "model_fields") and "chart_type" in cls.model_fields: field_info = cls.model_fields["chart_type"] if hasattr(field_info.annotation, "__args__"): assert field_info.annotation allowed_types = field_info.annotation.__args__ if chart_type not in allowed_types: raise ValueError( f"Chart type mismatch: expected one of {allowed_types}, " f"got '{chart_type}'. Use BaseChart.get() or the correct chart class." )
[docs] @classmethod def get(cls, chart_id: str, access_token: str | None = None) -> "BaseChart": """Fetch an existing chart from the Datawrapper API. Args: chart_id: The ID of the chart to fetch access_token: Optional Datawrapper API access token. If not provided, will use DATAWRAPPER_ACCESS_TOKEN environment variable. Returns: An instance of the chart class with data populated from the API. Raises: ValueError: If no access token is available or chart type doesn't match. Exception: If the API request fails. """ # Get token from parameter or environment token = access_token or os.getenv("DATAWRAPPER_ACCESS_TOKEN") if not token: raise ValueError( "No Datawrapper access token provided. " "Set DATAWRAPPER_ACCESS_TOKEN environment variable or pass access_token parameter." ) # Create a Datawrapper client instance client = Datawrapper(access_token=token) try: # Fetch chart metadata metadata_response = client.get(f"{client._CHARTS_URL}/{chart_id}") if not isinstance(metadata_response, dict): raise ValueError( f"Unexpected response type from API: {type(metadata_response)}" ) except Exception as e: raise Exception( f"Failed to fetch chart from Datawrapper API. Error: {str(e)}" ) from e # Verify chart type matches if this is a subclass chart_type = metadata_response.get("type") assert chart_type is not None, "API response missing 'type' field" cls._validate_chart_type(chart_type) try: # Fetch chart data data_response = client.get(f"{client._CHARTS_URL}/{chart_id}/data") except Exception as e: raise Exception( f"Failed to fetch chart data from Datawrapper API. Error: {str(e)}" ) from e # Parse metadata and data separately metadata_dict = cls.deserialize_model(metadata_response) data_df = cls.deserialize_data(data_response) # Merge them parsed_data = {**metadata_dict, "data": data_df} # Create instance and set chart_id and client instance = cls(**parsed_data) instance.chart_id = chart_id instance._client = client # Return the instance return instance
[docs] def create( self, access_token: str | None = None, folder_id: int | None = None ) -> "BaseChart": """Create a new chart via the Datawrapper API. Args: access_token: Optional Datawrapper API access token. If not provided, will use DATAWRAPPER_ACCESS_TOKEN environment variable. folder_id: Optional folder ID to create the chart in. Returns: Self, to enable method chaining. The chart ID is stored in self.chart_id. Raises: ValueError: If no access token is available or API returns invalid response. Exception: If the API request fails. """ # Get the client client = self._get_client(access_token) # Get the serialized chart metadata metadata = self.serialize_model() # Use the convenience method from the client to create the chart response = client.create_chart( title=metadata["title"], chart_type=metadata["type"], theme=metadata.get("theme") or None, data=self.serialize_data(), forkable=self.forkable, language=metadata.get("language"), metadata=metadata["metadata"], folder_id=folder_id, ) # Extract and validate the chart ID if not isinstance(response, dict): raise ValueError(f"Unexpected response type from API: {type(response)}") chart_id = response.get("id") if not chart_id or not isinstance(chart_id, str): raise ValueError(f"Invalid chart ID received from API: {chart_id}") # Store the chart ID and return self for chaining self.chart_id = chart_id return self
[docs] def update(self, access_token: str | None = None) -> "BaseChart": """Update an existing chart via the Datawrapper API. Args: access_token: Optional Datawrapper API access token. If not provided, will use DATAWRAPPER_ACCESS_TOKEN environment variable. Returns: Self, to enable method chaining. Raises: ValueError: If no chart_id is set or no access token is available. Exception: If the API request fails. """ if not self.chart_id: raise ValueError( "No chart_id set. Use create() first or set chart_id manually." ) # Get the client client = self._get_client(access_token) # Get the serialized chart metadata metadata = self.serialize_model() # Use the convenience method from the client to update the chart client.update_chart( chart_id=self.chart_id, title=metadata["title"], chart_type=metadata["type"], theme=metadata.get("theme") or None, data=self.serialize_data(), language=metadata.get("language"), metadata=metadata["metadata"], ) # Return self for chaining return self
[docs] def publish( self, access_token: str | None = None, ) -> "BaseChart": """Publish the chart via the Datawrapper API. Args: access_token: Optional Datawrapper API access token. If not provided, will use DATAWRAPPER_ACCESS_TOKEN environment variable. Returns: Self, to enable method chaining. Raises: ValueError: If no chart_id is set or no access token is available. Exception: If the API request fails or publishing fails. """ if not self.chart_id: raise ValueError( "No chart_id set. Use create() first or set chart_id manually." ) # Get the client client = self._get_client(access_token) # Call the publish_chart method from the client result = client.publish_chart(chart_id=self.chart_id) # Raise an exception if publishing failed if not result: raise Exception(f"Failed to publish chart {self.chart_id}") # Return self for chaining return self
[docs] def export_png( self, *, width: int | None = None, height: int | None = None, plain: bool = False, scale: int = 1, zoom: int = 2, transparent: bool = False, border_width: int = 0, border_color: str | None = None, logo: Literal["auto", "on", "off"] = "auto", logo_id: str | None = None, dark: bool = False, ligatures: bool = True, full_vector: bool = False, download: bool = False, access_token: str | None = None, timeout: int = 30, ) -> bytes: """Export chart as PNG and return the raw bytes. Args: width: Width of visualization in pixels. If not specified, uses chart width. height: Height of visualization in pixels. If not specified, uses chart height. plain: If True, exports only the visualization without header/footer. scale: Size multiplier that changes actual dimensions (e.g., 2 = 2x size). zoom: Resolution multiplier for sharper output at same visual size (e.g., 2 = 2x DPI). transparent: If True, exports with transparent background. border_width: Margin around visualization in pixels. border_color: Color of the border (e.g., "#FFFFFF"). If not specified, uses chart background color. logo: Logo display mode: "auto", "on", or "off". logo_id: Custom logo ID (pattern: ^[a-zA-Z0-9-]+$). dark: If True, exports in dark mode. ligatures: If True (default), enables typography ligatures. full_vector: If True, exports as full vector output. download: If True, includes download headers in response. access_token: Optional Datawrapper API access token. timeout: Timeout for the API request in seconds. Returns: Raw PNG image data as bytes. Raises: ValueError: If no chart_id is set. Exception: If the API request fails. Example: >>> chart = LineChart.get(chart_id="abc123") >>> png_data = chart.export_png(zoom=3, transparent=True) >>> Path("chart.png").write_bytes(png_data) """ if not self.chart_id: raise ValueError( "No chart_id set. Use create() first or set chart_id manually." ) client = self._get_client(access_token) # Build query parameters with PNG-specific defaults params = { "unit": "px", "plain": str(plain).lower(), "scale": str(scale), "zoom": str(zoom), "transparent": str(transparent).lower(), "borderWidth": str(border_width), "logo": logo, "dark": str(dark).lower(), "ligatures": str(ligatures).lower(), "fullVector": str(full_vector).lower(), "download": str(download).lower(), } if width is not None: params["width"] = str(width) if height is not None: params["height"] = str(height) if border_color is not None: params["borderColor"] = border_color if logo_id is not None: params["logoId"] = logo_id # Make the API request response = client.get( f"{client._CHARTS_URL}/{self.chart_id}/export/png", params=params, timeout=timeout, ) # Return raw bytes if isinstance(response, bytes): return response raise ValueError(f"Unexpected response type from API: {type(response)}")
[docs] def export_pdf( self, *, width: int | None = None, height: int | None = None, plain: bool = False, unit: Literal["px", "mm", "inch"] = "px", mode: Literal["rgb", "cmyk"] = "rgb", scale: int = 1, zoom: int = 2, transparent: bool = False, border_width: int = 0, border_color: str | None = None, logo: Literal["auto", "on", "off"] = "auto", logo_id: str | None = None, dark: bool = False, ligatures: bool = True, full_vector: bool = False, download: bool = False, access_token: str | None = None, timeout: int = 30, ) -> bytes: """Export chart as PDF and return the raw bytes. Args: width: Width of visualization. If not specified, uses chart width. height: Height of visualization. If not specified, uses chart height. plain: If True, exports only the visualization without header/footer. unit: Unit for measurements: "px", "mm", or "inch". mode: Color mode: "rgb" or "cmyk". scale: Size multiplier that changes actual dimensions (e.g., 2 = 2x size). zoom: Resolution multiplier for sharper output at same visual size (e.g., 2 = 2x DPI). transparent: If True, exports with transparent background. border_width: Margin around visualization. border_color: Color of the border (e.g., "#FFFFFF"). If not specified, uses chart background color. logo: Logo display mode: "auto", "on", or "off". logo_id: Custom logo ID (pattern: ^[a-zA-Z0-9-]+$). dark: If True, exports in dark mode. ligatures: If True (default), enables typography ligatures. full_vector: If True, exports as full vector output. download: If True, includes download headers in response. access_token: Optional Datawrapper API access token. timeout: Timeout for the API request in seconds. Returns: Raw PDF document data as bytes. Raises: ValueError: If no chart_id is set or invalid parameters provided. Exception: If the API request fails. Example: >>> chart = BarChart.get(chart_id="abc123") >>> pdf_data = chart.export_pdf(unit="mm", mode="cmyk") >>> Path("chart.pdf").write_bytes(pdf_data) """ if not self.chart_id: raise ValueError( "No chart_id set. Use create() first or set chart_id manually." ) # Validate parameters if unit not in ("px", "mm", "inch"): raise ValueError(f"Invalid unit: {unit}. Must be 'px', 'mm', or 'inch'.") if mode not in ("rgb", "cmyk"): raise ValueError(f"Invalid mode: {mode}. Must be 'rgb' or 'cmyk'.") client = self._get_client(access_token) # Build query parameters params = { "unit": unit, "mode": mode, "plain": str(plain).lower(), "scale": str(scale), "zoom": str(zoom), "transparent": str(transparent).lower(), "borderWidth": str(border_width), "logo": logo, "dark": str(dark).lower(), "ligatures": str(ligatures).lower(), "fullVector": str(full_vector).lower(), "download": str(download).lower(), } if width is not None: params["width"] = str(width) if height is not None: params["height"] = str(height) if border_color is not None: params["borderColor"] = border_color if logo_id is not None: params["logoId"] = logo_id # Make the API request response = client.get( f"{client._CHARTS_URL}/{self.chart_id}/export/pdf", params=params, timeout=timeout, ) # Return raw bytes if isinstance(response, bytes): return response raise ValueError(f"Unexpected response type from API: {type(response)}")
[docs] def export_svg( self, *, width: int | None = None, height: int | None = None, plain: bool = False, scale: int = 1, zoom: int = 2, transparent: bool = False, border_width: int = 0, border_color: str | None = None, logo: Literal["auto", "on", "off"] = "auto", logo_id: str | None = None, dark: bool = False, ligatures: bool = True, full_vector: bool = False, download: bool = False, access_token: str | None = None, timeout: int = 30, ) -> bytes: """Export chart as SVG and return the raw bytes. Args: width: Width of visualization. If not specified, uses chart width. height: Height of visualization. If not specified, uses chart height. plain: If True, exports only the visualization without header/footer. scale: Size multiplier that changes actual dimensions (e.g., 2 = 2x size). zoom: Resolution multiplier for sharper output at same visual size (e.g., 2 = 2x DPI). transparent: If True, exports with transparent background. border_width: Margin around visualization in pixels. border_color: Color of the border (e.g., "#FFFFFF"). If not specified, uses chart background color. logo: Logo display mode: "auto", "on", or "off". logo_id: Custom logo ID (pattern: ^[a-zA-Z0-9-]+$). dark: If True, exports in dark mode. ligatures: If True (default), enables typography ligatures. full_vector: If True, exports as full vector output. download: If True, includes download headers in response. access_token: Optional Datawrapper API access token. timeout: Timeout for the API request in seconds. Returns: Raw SVG document data as bytes. Raises: ValueError: If no chart_id is set. Exception: If the API request fails. Example: >>> chart = ColumnChart.get(chart_id="abc123") >>> svg_data = chart.export_svg(plain=True) >>> Path("chart.svg").write_bytes(svg_data) """ if not self.chart_id: raise ValueError( "No chart_id set. Use create() first or set chart_id manually." ) client = self._get_client(access_token) # Build query parameters params = { "unit": "px", "plain": str(plain).lower(), "scale": str(scale), "zoom": str(zoom), "transparent": str(transparent).lower(), "borderWidth": str(border_width), "logo": logo, "dark": str(dark).lower(), "ligatures": str(ligatures).lower(), "fullVector": str(full_vector).lower(), "download": str(download).lower(), } if width is not None: params["width"] = str(width) if height is not None: params["height"] = str(height) if border_color is not None: params["borderColor"] = border_color if logo_id is not None: params["logoId"] = logo_id # Make the API request response = client.get( f"{client._CHARTS_URL}/{self.chart_id}/export/svg", params=params, timeout=timeout, ) # Return raw bytes if isinstance(response, bytes): return response raise ValueError(f"Unexpected response type from API: {type(response)}")
[docs] def delete(self, access_token: str | None = None) -> bool: """Delete the chart via the Datawrapper API. Args: access_token: Optional Datawrapper API access token. If not provided, will use DATAWRAPPER_ACCESS_TOKEN environment variable. Returns: True if the chart was deleted successfully. Raises: ValueError: If no chart_id is set or no access token is available. Exception: If the API request fails. """ if not self.chart_id: raise ValueError( "No chart_id set. Use create() first or set chart_id manually." ) # Get the client client = self._get_client(access_token) # Call the delete_chart method from the client result = client.delete_chart(chart_id=self.chart_id) # Clear the chart_id after successful deletion if result: self.chart_id = None return result
[docs] def duplicate(self, access_token: str | None = None) -> "BaseChart": """Duplicate the chart and create a new editable copy via the Datawrapper API. Args: access_token: Optional Datawrapper API access token. If not provided, will use DATAWRAPPER_ACCESS_TOKEN environment variable. Returns: A new BaseChart instance representing the duplicated chart. Raises: ValueError: If no chart_id is set or no access token is available. Exception: If the API request fails. """ if not self.chart_id: raise ValueError( "No chart_id set. Use create() first or set chart_id manually." ) # Get the client client = self._get_client(access_token) # Call the copy_chart method from the client response = client.copy_chart(chart_id=self.chart_id) # Extract the new chart ID if not isinstance(response, dict): raise ValueError(f"Unexpected response type from API: {type(response)}") new_chart_id = response.get("id") if not new_chart_id or not isinstance(new_chart_id, str): raise ValueError(f"Invalid chart ID received from API: {new_chart_id}") # Fetch the full chart data using the class's get method return self.__class__.get(chart_id=new_chart_id, access_token=access_token)
[docs] def fork(self, access_token: str | None = None) -> "BaseChart": """Fork the chart and create an editable copy via the Datawrapper API. Args: access_token: Optional Datawrapper API access token. If not provided, will use DATAWRAPPER_ACCESS_TOKEN environment variable. Returns: A new BaseChart instance representing the forked chart. Raises: ValueError: If no chart_id is set or no access token is available. Exception: If the API request fails. """ if not self.chart_id: raise ValueError( "No chart_id set. Use create() first or set chart_id manually." ) # Get the client client = self._get_client(access_token) # Call the fork_chart method from the client response = client.fork_chart(chart_id=self.chart_id) # Extract the new chart ID if not isinstance(response, dict): raise ValueError(f"Unexpected response type from API: {type(response)}") new_chart_id = response.get("id") if not new_chart_id or not isinstance(new_chart_id, str): raise ValueError(f"Invalid chart ID received from API: {new_chart_id}") # Fetch the full chart data using the class's get method return self.__class__.get(chart_id=new_chart_id, access_token=access_token)
[docs] def get_display_urls(self, access_token: str | None = None) -> list[dict]: """Get the URLs for the published chart, table or map. Args: access_token: Optional Datawrapper API access token. If not provided, will use DATAWRAPPER_ACCESS_TOKEN environment variable. Returns: A list of dictionaries containing the display URLs for the chart. Raises: ValueError: If no chart_id is set or no access token is available. Exception: If the API request fails. """ if not self.chart_id: raise ValueError( "No chart_id set. Use create() first or set chart_id manually." ) # Get the client client = self._get_client(access_token) # Call the get_chart_display_urls method from the client return client.get_chart_display_urls(chart_id=self.chart_id)
[docs] def get_iframe_code( self, responsive: bool = False, access_token: str | None = None ) -> str: """Get the iframe embed code for the chart, table, or map. Args: responsive: Whether to return responsive iframe code, by default False access_token: Optional Datawrapper API access token. If not provided, will use DATAWRAPPER_ACCESS_TOKEN environment variable. Returns: The iframe embed code as a string. Raises: ValueError: If no chart_id is set or no access token is available. Exception: If the API request fails. """ if not self.chart_id: raise ValueError( "No chart_id set. Use create() first or set chart_id manually." ) # Get the client client = self._get_client(access_token) # Call the get_iframe_code method from the client return client.get_iframe_code(chart_id=self.chart_id, responsive=responsive)
[docs] def display(self, access_token: str | None = None) -> IFrame: """Display the chart as an IFrame in a Jupyter notebook. Args: access_token: Optional Datawrapper API access token. If not provided, will use DATAWRAPPER_ACCESS_TOKEN environment variable. Returns: An IPython.display.IFrame object displaying the chart. Raises: ValueError: If no chart_id is set or no access token is available. Exception: If the API request fails. """ if not self.chart_id: raise ValueError( "No chart_id set. Use create() first or set chart_id manually." ) # Get the client client = self._get_client(access_token) # Call the display_chart method from the client return client.display_chart(chart_id=self.chart_id)
[docs] def get_editor_url(self) -> str: """Get the Datawrapper editor URL for this chart. Returns: The Datawrapper editor URL as a string. Raises: ValueError: If no chart_id is set. """ if not self.chart_id: raise ValueError( "No chart_id set. Use create() first or set chart_id manually." ) return f"https://app.datawrapper.de/edit/{self.chart_id}/visualize#refine"
[docs] def get_png_url(self) -> str: """Get the fallback PNG image URL for noscript tags. Returns: The PNG image URL in the format https://datawrapper.dwcdn.net/{chart_id}/full.png Raises: ValueError: If no chart_id is set. """ if not self.chart_id: raise ValueError( "No chart_id set. Use create() first or set chart_id manually." ) return f"https://datawrapper.dwcdn.net/{self.chart_id}/full.png"
[docs] def get_public_url(self) -> str: """Get the public URL for the published chart. Returns: The public URL in the format https://datawrapper.dwcdn.net/{chart_id}/ Raises: ValueError: If no chart_id is set. Note: This method returns the URL regardless of whether the chart is actually published. If the chart is not published, the URL will not be accessible. Use publish() to publish the chart before accessing this URL. """ if not self.chart_id: raise ValueError( "No chart_id set. Use create() first or set chart_id manually." ) return f"https://datawrapper.dwcdn.net/{self.chart_id}/"