diff --git a/datawrapper/__init__.py b/datawrapper/__init__.py index c2d0f005..02d8e604 100644 --- a/datawrapper/__init__.py +++ b/datawrapper/__init__.py @@ -20,6 +20,7 @@ BarChart, BarOverlay, BaseChart, + ChoroplethMap, ColumnChart, ColumnFormat, ConnectorLine, @@ -39,6 +40,7 @@ ScatterPlot, StackedBarChart, TextAnnotation, + Tooltip, Transform, XLineAnnotation, XRangeAnnotation, @@ -47,6 +49,9 @@ ) from datawrapper.charts.enums import ( ArrowHead, + BasemapProjection, + ColorMode, + ColorScale, ConnectorLineType, DateFormat, GridDisplay, @@ -100,6 +105,7 @@ "Describe", "BarChart", "BarOverlay", + "ChoroplethMap", "ColumnChart", "LineChart", "Line", @@ -124,7 +130,11 @@ "XLineAnnotation", "YLineAnnotation", "ConnectorLine", + "Tooltip", "ArrowHead", + "BasemapProjection", + "ColorMode", + "ColorScale", "ConnectorLineType", "DateFormat", "GridDisplay", diff --git a/datawrapper/charts/__init__.py b/datawrapper/charts/__init__.py index 53fde8d9..be45b36c 100644 --- a/datawrapper/charts/__init__.py +++ b/datawrapper/charts/__init__.py @@ -2,9 +2,11 @@ from .arrow import ArrowChart from .bar import BarChart, BarOverlay from .base import BaseChart +from .choropleth_map import ChoroplethMap, Tooltip from .column import ColumnChart from .enums import ( ArrowHead, + BasemapProjection, ConnectorLineType, DateFormat, GridDisplay, @@ -86,6 +88,7 @@ "GridFormatMixin", "GridDisplayMixin", "ArrowHead", + "BasemapProjection", "ConnectorLineType", "DateFormat", "GridDisplay", @@ -123,6 +126,7 @@ "BaseChart", "BarOverlay", "BarChart", + "ChoroplethMap", "ColumnChart", "LineChart", "Line", @@ -140,4 +144,5 @@ "MultipleColumnYRangeAnnotation", "ScatterPlot", "StackedBarChart", + "Tooltip", ) diff --git a/datawrapper/charts/base.py b/datawrapper/charts/base.py index 184fe4ca..431d75bd 100644 --- a/datawrapper/charts/base.py +++ b/datawrapper/charts/base.py @@ -79,6 +79,7 @@ class BaseChart(BaseModel): "d3-bars", "d3-bars-stacked", "d3-lines", + "d3-maps-choropleth", "d3-scatter-plot", "locator-map", "multiple-columns", diff --git a/datawrapper/charts/choropleth_map.py b/datawrapper/charts/choropleth_map.py new file mode 100644 index 00000000..3a811e06 --- /dev/null +++ b/datawrapper/charts/choropleth_map.py @@ -0,0 +1,443 @@ +from typing import Any, Literal + +import pandas as pd +from pydantic import BaseModel, ConfigDict, Field, model_serializer + +from .base import BaseChart +from .enums import BasemapProjection, ColorMode, ColorScale, DateFormat, NumberFormat +from .models import AnnotationsMixin + + +class Tooltip(BaseModel): + """Configure tooltips for a Datawrapper choropleth map.""" + + model_config = ConfigDict(populate_by_name=True, strict=True) + + #: The tooltip body template + body: str = Field( + default="", + description="The tooltip body template", + ) + + #: The tooltip title template + title: str = Field( + default="", + description="The tooltip title template", + ) + + #: Mapping of field names to custom labels + fields: dict[str, str] = Field( + default_factory=dict, + description="Mapping of field names to custom labels for tooltip display", + ) + + +class ChoroplethMap(AnnotationsMixin, BaseChart): + """A base class for the Datawrapper API's choropleth map. + + Choropleth maps visualize data across geographic regions by coloring areas + based on data values. This class provides a Pythonic interface to create + and configure choropleth maps using the Datawrapper API. + """ + + 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-maps-choropleth", + "title": "Population by Country", + "source_name": "UN Population Division", + "data": pd.DataFrame( + { + "country": ["USA", "CAN", "MEX"], + "population": [331000000, 38000000, 128000000], + } + ), + "keys_column": "country", + "values_column": "population", + "basemap": "world", + "map_key_attr": "iso-a3", + } + ] + }, + ) + + #: The type of datawrapper chart to create + chart_type: Literal["d3-maps-choropleth"] = Field( + default="d3-maps-choropleth", + alias="chart-type", + description="The type of datawrapper chart to create", + ) + + # + # Data Mapping (axes section in API) + # + + #: The column containing map keys (region identifiers) + keys_column: str | None = Field( + default=None, + alias="keys", + description="The column containing map keys (e.g., country codes, region names)", + ) + + #: The column containing values to visualize + values_column: str | None = Field( + default=None, + alias="values", + description="The column containing values to visualize on the map", + ) + + #: The type of the keys column (text or number) + key_column_type: Literal["text", "number"] | None = Field( + default=None, + description="The data type of the keys column. Use 'text' for keys with leading zeros (e.g., postal codes, region IDs)", + ) + + # + # Basemap Configuration + # + + #: The basemap ID to use (or "custom_upload" for custom basemaps) + basemap: str = Field( + default="", + description="The basemap ID (e.g., 'world', 'usa-counties') or 'custom_upload' for custom TopoJSON", + ) + + #: The map key attribute to match against your data + map_key_attr: str = Field( + default="", + alias="map-key-attr", + description="The attribute in the basemap to match against your keys column (e.g., 'iso-a3', 'fips', 'ags)", + ) + + #: The projection for custom basemaps + basemap_projection: BasemapProjection | str = Field( + default="geoAzimuthalEqualArea", + alias="basemapProjection", + description="The projection to use for custom basemaps", + ) + + #: The topology object name in custom basemaps + basemap_regions: str = Field( + default="regions", + alias="basemapRegions", + description="The name of the topology object in custom TopoJSON basemaps", + ) + + # + # Tooltips + # + + #: Tooltip configuration + tooltip: Tooltip | dict[str, Any] | None = Field( + default=None, + description="Tooltip configuration (body, title, field mappings)", + ) + + #: The format for tooltip values (use DateFormat or NumberFormat enum or custom format strings) + tooltip_number_format: DateFormat | NumberFormat | str = Field( + default="", + alias="tooltip-number-format", + description="The format for tooltip values. Use DateFormat for temporal data, NumberFormat for numeric data, or custom format strings.", + ) + + # + # Appearance + # + + #: Whether to show tooltips on hover + show_tooltips: bool = Field( + default=True, + alias="show-tooltips", + description="Whether to show tooltips when hovering over regions", + ) + + # + # Map Interaction & Display + # + + #: Whether the map is zoomable + zoomable: bool = Field( + default=False, + description="Whether users can zoom and pan the map", + ) + + #: Whether to hide regions without data + hide_empty_regions: bool = Field( + default=False, + alias="hide-empty-regions", + description="Whether to hide regions that don't have data values", + ) + + #: Whether to hide region borders + hide_borders: bool = Field( + default=False, + alias="hide-borders", + description="Whether to hide the borders between regions on the map", + ) + + # + # Color Configuration + # + + #: Color mode (gradient or buckets) + color_mode: ColorMode | str | None = Field( + default=None, + alias="color-mode", + description="Color mode: 'gradient' for continuous colors or 'buckets' for discrete intervals", + ) + + #: Number of color buckets/steps + color_steps: int | None = Field( + default=None, + alias="color-steps", + description="Number of color buckets or steps (typically 3-9)", + ) + + #: Color palette/scheme + color_palette: str | None = Field( + default=None, + alias="color-palette", + description="Color palette name (e.g., 'Blues', 'RdYlGn', 'Viridis')", + ) + + #: Color scale type + color_scale: ColorScale | str | None = Field( + default=None, + alias="color-scale", + description="Color scale type: 'linear', 'log', 'sqrt', 'quantile', or 'jenks'", + ) + + #: Minimum color (start of gradient) + color_from: str | None = Field( + default=None, + alias="color-from", + description="Start color of the gradient (hex color code, e.g., '#ffffff')", + ) + + #: Maximum color (end of gradient) + color_to: str | None = Field( + default=None, + alias="color-to", + description="End color of the gradient (hex color code, e.g., '#ff0000')", + ) + + # + # Serialization methods for preparing data for API upload + # + + @model_serializer + def serialize_model(self) -> dict: + """Serialize the model to a dictionary.""" + # Call the parent class's serialize_model method + model = super().serialize_model() + + # Build the axes section for data mapping + axes_data = {} + if self.keys_column is not None: + axes_data["keys"] = self.keys_column + if self.values_column is not None: + axes_data["values"] = self.values_column + + # Only add axes if we have keys or values + if axes_data: + model["metadata"]["axes"] = axes_data + + # Handle key_column_type by adding to metadata.data.column-format + if self.key_column_type is not None and self.keys_column is not None: + # Ensure data section exists + if "data" not in model["metadata"]: + model["metadata"]["data"] = {} + + # Get existing column-format or initialize empty dict + column_format = model["metadata"]["data"].get("column-format", {}) + + # Set the type for the keys column + if self.keys_column not in column_format: + column_format[self.keys_column] = {} + + column_format[self.keys_column]["type"] = self.key_column_type + + model["metadata"]["data"]["column-format"] = column_format + + # Build visualize section + visualize_data: dict[str, Any] = { + "show-tooltips": self.show_tooltips, + "tooltip-number-format": self.tooltip_number_format, + "zoomable": self.zoomable, + "hide-empty-regions": self.hide_empty_regions, + "hide-borders": self.hide_borders, + } + + # Add color configuration + if self.color_mode is not None: + visualize_data["color-mode"] = self.color_mode + if self.color_steps is not None: + visualize_data["color-steps"] = self.color_steps + if self.color_palette is not None: + visualize_data["color-palette"] = self.color_palette + if self.color_scale is not None: + visualize_data["color-scale"] = self.color_scale + if self.color_from is not None: + visualize_data["color-from"] = self.color_from + if self.color_to is not None: + visualize_data["color-to"] = self.color_to + + # Add colorscale object if color configuration is present + # This is necessary for Datawrapper to actually apply the colors + if self.color_from is not None and self.color_to is not None: + # Determine interpolation type based on color_scale + interpolation = "equidistant" + if self.color_scale == "quantile": + interpolation = "equidistant" + elif self.color_scale == "jenks": + interpolation = "jenks" + + # Build the colorscale configuration + colorscale_config: dict[str, Any] = { + "interpolation": interpolation, + "stops": "equidistant", + } + + # IMPORTANT: Set the mode field to match color_mode + # This is what Datawrapper uses to determine continuous vs buckets display + if self.color_mode is not None: + # Map our color_mode enum to Datawrapper's colorscale.mode + if self.color_mode == ColorMode.BUCKETS or self.color_mode == "buckets": + colorscale_config["mode"] = "discrete" + else: # gradient + colorscale_config["mode"] = "continuous" + + # Add stopCount if color_steps is specified + if self.color_steps is not None: + colorscale_config["stopCount"] = self.color_steps + + # Set the start and end colors + # Datawrapper will interpolate the colors in between + colorscale_config["colors"] = [ + {"color": self.color_from, "position": 0}, + {"color": self.color_to, "position": 1}, + ] + + visualize_data["colorscale"] = colorscale_config + + # Add basemap configuration + if self.basemap: + visualize_data["basemap"] = self.basemap + + if self.map_key_attr: + visualize_data["map-key-attr"] = self.map_key_attr + + # Add custom basemap settings if using custom upload + if self.basemap == "custom_upload": + visualize_data["basemapProjection"] = self.basemap_projection + visualize_data["basemapRegions"] = self.basemap_regions + + # Add tooltip configuration + if self.tooltip is not None: + tooltip_obj = ( + self.tooltip + if isinstance(self.tooltip, Tooltip) + else Tooltip.model_validate(self.tooltip) + ) + tooltip_dict: dict[str, Any] = { + "body": tooltip_obj.body, + "title": tooltip_obj.title, + } + # Add fields if present + if tooltip_obj.fields: + tooltip_dict["fields"] = tooltip_obj.fields + visualize_data["tooltip"] = tooltip_dict + + # Update the visualize section + model["metadata"]["visualize"].update(visualize_data) + model["metadata"]["visualize"].update(self._serialize_annotations()) + + return model + + @classmethod + def deserialize_model(cls, api_response: dict[str, Any]) -> dict[str, Any]: + """Parse Datawrapper API response including choropleth map specific fields. + + Args: + api_response: The JSON response from the chart metadata endpoint + + Returns: + Dictionary that can be used to initialize the ChoroplethMap model + """ + # Call parent to get base fields + init_data = super().deserialize_model(api_response) + + # Extract map-specific sections + metadata = api_response.get("metadata", {}) + axes = metadata.get("axes", {}) + visualize = metadata.get("visualize", {}) + + # Data mapping + if "keys" in axes: + init_data["keys_column"] = axes["keys"] + if "values" in axes: + init_data["values_column"] = axes["values"] + + # Extract key_column_type from metadata.data.column-format + data_section = metadata.get("data", {}) + column_format = data_section.get("column-format", {}) + # Check if keys_column exists and has a type defined + if "keys_column" in init_data: + keys_col = init_data["keys_column"] + if keys_col in column_format and "type" in column_format[keys_col]: + col_type = column_format[keys_col]["type"] + # Only set if it's text or number (not auto) + if col_type in ["text", "number"]: + init_data["key_column_type"] = col_type + + # Basemap configuration + if "basemap" in visualize: + init_data["basemap"] = visualize["basemap"] + if "map-key-attr" in visualize: + init_data["map_key_attr"] = visualize["map-key-attr"] + if "basemapProjection" in visualize: + init_data["basemap_projection"] = visualize["basemapProjection"] + if "basemapRegions" in visualize: + init_data["basemap_regions"] = visualize["basemapRegions"] + + # Tooltips + if "tooltip" in visualize: + tooltip_data = visualize["tooltip"] + init_data["tooltip"] = Tooltip.model_validate(tooltip_data) + if "tooltip-number-format" in visualize: + init_data["tooltip_number_format"] = visualize["tooltip-number-format"] + if "show-tooltips" in visualize: + init_data["show_tooltips"] = visualize["show-tooltips"] + + # Map interaction and display + if "zoomable" in visualize: + init_data["zoomable"] = visualize["zoomable"] + if "hide-empty-regions" in visualize: + init_data["hide_empty_regions"] = visualize["hide-empty-regions"] + if "hide-borders" in visualize: + init_data["hide_borders"] = visualize["hide-borders"] + + # Color configuration + if "color-mode" in visualize: + init_data["color_mode"] = visualize["color-mode"] + if "color-steps" in visualize: + init_data["color_steps"] = visualize["color-steps"] + if "color-palette" in visualize: + init_data["color_palette"] = visualize["color-palette"] + if "color-scale" in visualize: + init_data["color_scale"] = visualize["color-scale"] + if "color-from" in visualize: + init_data["color_from"] = visualize["color-from"] + if "color-to" in visualize: + init_data["color_to"] = visualize["color-to"] + + # Annotations + init_data.update(cls._deserialize_annotations(visualize)) + + return init_data diff --git a/datawrapper/charts/enums/__init__.py b/datawrapper/charts/enums/__init__.py index 7b611b96..073e1225 100644 --- a/datawrapper/charts/enums/__init__.py +++ b/datawrapper/charts/enums/__init__.py @@ -1,6 +1,9 @@ """Enum classes for Datawrapper chart formatting and styling options.""" from .annos import ArrowHead, ConnectorLineType, StrokeType, StrokeWidth +from .basemap_projection import BasemapProjection +from .color_mode import ColorMode +from .color_scale import ColorScale from .date_format import DateFormat from .grid_display import GridDisplay from .grid_label import GridLabelAlign, GridLabelPosition @@ -29,6 +32,9 @@ __all__ = [ "ArrowHead", + "BasemapProjection", + "ColorMode", + "ColorScale", "ConnectorLineType", "DateFormat", "GridDisplay", diff --git a/datawrapper/charts/enums/basemap_projection.py b/datawrapper/charts/enums/basemap_projection.py new file mode 100644 index 00000000..5ead98d8 --- /dev/null +++ b/datawrapper/charts/enums/basemap_projection.py @@ -0,0 +1,22 @@ +"""Basemap projection options for choropleth maps.""" + +from enum import Enum + + +class BasemapProjection(str, Enum): + """Map projection types for custom basemaps in Datawrapper.""" + + #: Azimuthal Equal Area projection + AZIMUTHAL_EQUAL_AREA = "geoAzimuthalEqualArea" + + #: Natural Equal Area projection + NATURAL_EQUAL_AREA = "geoNaturalEqualArea" + + #: Conic Equidistant projection + CONIC_EQUIDISTANT = "geoConicEquidistant" + + #: Conic Conformal projection + CONIC_CONFORMAL = "geoConicConformal" + + #: Albers USA projection (specific for US maps) + ALBERS_USA = "geoAlbersUsa" diff --git a/datawrapper/charts/enums/color_mode.py b/datawrapper/charts/enums/color_mode.py new file mode 100644 index 00000000..a64fd6e4 --- /dev/null +++ b/datawrapper/charts/enums/color_mode.py @@ -0,0 +1,13 @@ +"""Color mode options for choropleth maps.""" + +from enum import Enum + + +class ColorMode(str, Enum): + """Color mode for choropleth map coloring.""" + + #: Continuous gradient coloring + GRADIENT = "gradient" + + #: Discrete buckets/intervals coloring + BUCKETS = "buckets" diff --git a/datawrapper/charts/enums/color_scale.py b/datawrapper/charts/enums/color_scale.py new file mode 100644 index 00000000..54b0c72d --- /dev/null +++ b/datawrapper/charts/enums/color_scale.py @@ -0,0 +1,22 @@ +"""Color scale options for choropleth maps.""" + +from enum import Enum + + +class ColorScale(str, Enum): + """Color scale type for choropleth map value mapping.""" + + #: Linear scale + LINEAR = "linear" + + #: Logarithmic scale + LOG = "log" + + #: Square root scale + SQRT = "sqrt" + + #: Quantile scale (equal count in each bucket) + QUANTILE = "quantile" + + #: Jenks natural breaks optimization + JENKS = "jenks" diff --git a/tests/samples/choropleth_map/tests/samples/choropleth_map/internet_usagecountry.csv b/tests/samples/choropleth_map/tests/samples/choropleth_map/internet_usagecountry.csv new file mode 100644 index 00000000..87698a70 --- /dev/null +++ b/tests/samples/choropleth_map/tests/samples/choropleth_map/internet_usagecountry.csv @@ -0,0 +1,207 @@ +code internet usage country +ABW 88.66 Aruba +AFG 8.26 Afghanistan +AGO 12.40 Angola +ALB 63.25 Albania +AND 96.91 Andorra +ARE 91.24 United Arab Emirates +ARG 69.40 Argentina +ARM 58.25 Armenia +ATG 65.20 Antigua and Barbuda +AUS 84.56 Australia +AUT 83.93 Austria +AZE 77.00 Azerbaijan +BDI 4.87 Burundi +BEL 85.05 Belgium +BEN 6.79 Benin +BFA 11.39 Burkina Faso +BGD 14.40 Bangladesh +BGR 56.66 Bulgaria +BHR 93.48 Bahrain +BHS 78.00 Bahamas +BIH 65.07 Bosnia and Herzegovina +BLR 62.23 Belarus +BLZ 41.59 Belize +BMU 98.32 Bermuda +BOL 45.10 Bolivia +BRA 59.08 Brazil +BRB 76.11 Barbados +BRN 71.20 Brunei +BTN 39.80 Bhutan +BWA 27.50 Botswana +CAF 4.56 Central African Republic +CAN 88.47 Canada +CHE 87.48 Switzerland +CHL 64.29 Chile +CHN 50.30 China +CIV 21.00 Cote d'Ivoire +CMR 20.68 Cameroon +COD 3.80 Democratic Republic of Congo +COG 7.62 Congo +COL 55.90 Colombia +COM 7.46 Comoros +CPV 43.02 Cape Verde +CRI 59.76 Costa Rica +CUB 37.31 Cuba +CYM 77.00 Cayman Islands +CYP 71.72 Cyprus +CZE 81.30 Czech Republic +DEU 87.59 Germany +DJI 11.92 Djibouti +DMA 67.60 Dominica +DNK 96.33 Denmark +DOM 54.22 Dominican Republic +DZA 38.20 Algeria +ECU 48.94 Ecuador +EGY 37.82 Egypt +ERI 1.08 Eritrea +ESP 78.69 Spain +EST 88.41 Estonia +ETH 11.60 Ethiopia +FIN 92.65 Finland +FJI 46.33 Fiji +FRA 84.69 France +FRO 94.20 Faeroe Islands +FSM 31.50 Micronesia (country) +GAB 23.50 Gabon +GBR 92.00 United Kingdom +GEO 47.57 Georgia +GHA 23.48 Ghana +GIN 4.70 Guinea +GMB 17.12 Gambia +GNB 3.54 Guinea-Bissau +GNQ 21.32 Equatorial Guinea +GRC 66.84 Greece +GRD 53.81 Grenada +GRL 67.60 Greenland +GTM 27.10 Guatemala +GUM 73.14 Guam +GUY 38.20 Guyana +HKG 84.95 Hong Kong +HND 20.36 Honduras +HRV 69.80 Croatia +HTI 12.20 Haiti +HUN 72.83 Hungary +IDN 21.98 Indonesia +IND 26.00 India +IRL 80.12 Ireland +IRN 45.33 Iran +IRQ 17.22 Iraq +ISL 98.20 Iceland +ISR 77.35 Israel +ITA 65.57 Italy +JAM 42.22 Jamaica +JOR 53.40 Jordan +JPN 91.06 Japan +KAZ 70.83 Kazakhstan +KEN 45.62 Kenya +KGZ 30.25 Kyrgyzstan +KHM 19.00 Cambodia +KIR 13.00 Kiribati +KNA 75.70 Saint Kitts and Nevis +KOR 89.65 South Korea +KWT 82.08 Kuwait +LAO 18.20 Laos +LBN 74.00 Lebanon +LBR 5.90 Liberia +LBY 19.02 Libya +LCA 52.35 Saint Lucia +LIE 96.64 Liechtenstein +LKA 29.99 Sri Lanka +LSO 16.07 Lesotho +LTU 71.38 Lithuania +LUX 97.33 Luxembourg +LVA 79.20 Latvia +MAC 77.60 Macao +MAR 57.08 Morocco +MCO 93.36 Monaco +MDA 49.84 Moldova +MDG 4.17 Madagascar +MDV 54.46 Maldives +MEX 57.43 Mexico +MHL 19.28 Marshall Islands +MKD 70.38 Macedonia +MLI 10.34 Mali +MLT 76.18 Malta +MMR 21.80 Myanmar +MNE 68.12 Montenegro +MNG 21.44 Mongolia +MOZ 9.00 Mozambique +MRT 15.20 Mauritania +MUS 50.14 Mauritius +MWI 9.30 Malawi +MYS 71.06 Malaysia +NAM 22.31 Namibia +NCL 74.00 New Caledonia +NER 2.22 Niger +NGA 47.44 Nigeria +NIC 19.70 Nicaragua +NLD 93.10 Netherlands +NOR 96.81 Norway +NPL 17.58 Nepal +NZL 88.22 New Zealand +OMN 74.17 Oman +OWID_WRL 43.90 World +PAK 18.00 Pakistan +PAN 51.21 Panama +PER 40.90 Peru +PHL 40.70 Philippines +PNG 7.90 Papua New Guinea +POL 68.00 Poland +PRI 79.47 Puerto Rico +PRT 68.63 Portugal +PRY 48.44 Paraguay +PSE 57.42 Palestine +PYF 64.56 French Polynesia +QAT 92.88 Qatar +ROU 55.76 Romania +RUS 70.10 Russia +RWA 18.00 Rwanda +SAU 69.62 Saudi Arabia +SDN 26.61 Sudan +SEN 21.69 Senegal +SGP 82.10 Singapore +SLB 10.00 Solomon Islands +SLE 2.50 Sierra Leone +SLV 26.92 El Salvador +SOM 1.76 Somalia +SRB 65.32 Serbia +SSD 17.93 South Sudan +STP 25.82 Sao Tome and Principe +SUR 42.76 Suriname +SVK 77.63 Slovakia +SVN 73.10 Slovenia +SWE 90.61 Sweden +SWZ 30.38 Swaziland +SYC 58.12 Seychelles +SYR 29.98 Syria +TCD 2.70 Chad +TGO 7.12 Togo +THA 39.32 Thailand +TJK 18.98 Tajikistan +TKM 15.00 Turkmenistan +TLS 13.40 Timor +TON 45.00 Tonga +TTO 69.20 Trinidad and Tobago +TUN 48.52 Tunisia +TUR 53.74 Turkey +TUV 42.70 Tuvalu +TZA 5.36 Tanzania +UGA 19.22 Uganda +UKR 48.88 Ukraine +URY 64.60 Uruguay +USA 74.45 United States +UZB 42.80 Uzbekistan +VCT 51.77 Saint Vincent and the Grenadines +VEN 61.87 Venezuela +VIR 54.84 United States Virgin Islands +VNM 52.72 Vietnam +VUT 22.35 Vanuatu +WSM 25.41 Samoa +YEM 25.10 Yemen +ZAF 51.92 South Africa +ZMB 21.00 Zambia +ZWE 16.36 Zimbabwe +PRK +TWN + diff --git a/tests/samples/choropleth_map/tests/samples/choropleth_map/internet_usagecountry.json b/tests/samples/choropleth_map/tests/samples/choropleth_map/internet_usagecountry.json new file mode 100644 index 00000000..a8eef0d5 --- /dev/null +++ b/tests/samples/choropleth_map/tests/samples/choropleth_map/internet_usagecountry.json @@ -0,0 +1,218 @@ +{ + "chart": { + "crdt": { + "data": { + "publicId": "6Ue5p", + "language": "en-US", + "theme": "datawrapper", + "chartHash": "273f435208f5a92104b1b908f4e00ead", + "id": "6Ue5p", + "type": "d3-maps-choropleth", + "title": "Share of people using the internet", + "lastEditStep": 3, + "publicVersion": 0, + "deleted": false, + "forkable": true, + "isFork": false, + "metadata": { + "data": { + "changes": [], + "transpose": false, + "vertical-header": true, + "horizontal-header": true, + "column-order": [], + "column-format": { + "code": { + "type": "text" + } + }, + "external-data": "", + "upload-method": "copy", + "use-datawrapper-cdn": true + }, + "describe": { + "source-name": "Example Data", + "source-url": "", + "intro": "2015 survey estimating the share of the populations who has used the Internet in \"the last 3 months\" (via a computer, mobile phone, personal digital assistant, games machine, digital TV etc.). The bigger the square, the bigger the population of a country.", + "byline": "", + "aria-description": "", + "number-format": "-", + "number-divisor": 0, + "number-append": "", + "number-prepend": "", + "hide-title": false + }, + "visualize": { + "dark-mode-invert": true, + "highlighted-series": [], + "highlighted-values": [], + "sharing": { + "enabled": false, + "url": "", + "auto": false + }, + "basemap": "worldmap-square", + "tooltip": { + "body": "{{ internet_usage }} of individuals used the internet in 2015.", + "title": "{{ code }}", + "enabled": true, + "sticky": true, + "migrated": true + }, + "color-to": "#00768d", + "zoomable": true, + "color-from": "#c13e2e", + "color-mode": "buckets", + "colorscale": { + "mode": "discrete", + "stops": "pretty", + "colors": [ + { + "color": "#c13e2e", + "position": 0 + }, + { + "color": "#00768d", + "position": 1 + } + ], + "stopCount": 5, + "interpolation": "equidistant", + "map": {}, + "palette": 0, + "rangeMin": "", + "rangeMax": "", + "customStops": [ + null, + 20, + 40, + 60, + 80, + null, + 60, + 70, + 80 + ], + "rangeCenter": "", + "categoryOrder": [], + "categoryLabels": {} + }, + "color-scale": "sqrt", + "color-steps": 7, + "hide-borders": false, + "map-key-attr": "iso-a3", + "map-type-set": true, + "show-tooltips": true, + "text-annotations": {}, + "range-annotations": [], + "hide-empty-regions": true, + "tooltip-number-format": "0a", + "zoom-button-pos": "br", + "mapView": "crop", + "min-label-zoom": 1, + "map-padding": 0, + "map-align": "center", + "map-label-format": "0,0.[00]", + "max-map-height": 650, + "avoid-label-overlap": true, + "legends": { + "combined": { + "orientation": "horizontal" + }, + "color": { + "enabled": true, + "reverse": false, + "interactive": false, + "title": "", + "size": 170, + "labels": "ranges", + "orientation": "horizontal", + "position": "above", + "labelMin": "low", + "labelCenter": "medium", + "labelMax": "high", + "offsetX": 0, + "offsetY": 0, + "customLabels": [], + "hideItems": [], + "titleEnabled": false, + "labelFormat": "0,0.[00]" + }, + "pattern": { + "enabled": true, + "title": "", + "titleEnabled": false, + "interactive": false, + "size": 170, + "orientation": "horizontal", + "position": "above", + "useColorLegendPosition": true, + "offsetX": 0, + "offsetY": 0 + } + }, + "miniMap": { + "enabled": false, + "type": "region", + "offsetX": 1, + "offsetY": 1, + "position": "bottom-left", + "size": 120 + }, + "patterns": { + "enabled": false, + "map": {}, + "order": [], + "opacity": 50, + "colorType": "auto-contrast" + }, + "labels": { + "enabled": false, + "type": "places", + "places": [], + "max": 1 + }, + "chart-type-set": true + }, + "axes": { + "keys": "code", + "values": "internet usage" + }, + "publish": { + "embed-width": 600, + "embed-height": 514, + "blocks": { + "logo": { + "id": "", + "enabled": false + }, + "embed": false, + "download-pdf": false, + "download-svg": false, + "get-the-data": false, + "download-image": false + }, + "export-pdf": {}, + "autoDarkMode": false, + "force-attribution": false, + "chart-height": 385 + }, + "annotate": { + "notes": "" + }, + "custom": {} + }, + "utf8": false, + "createdAt": "2025-10-31T15:56:12.000Z", + "lastModifiedAt": "2025-10-31T15:56:12.000Z", + "authorId": 52021, + "workspace": "ndr" + }, + "pathsToItemArrays": [ + "metadata.visualize.text-annotations" + ] + }, + "clock": "0-7" + }, + "data": "0-0" + }