Source code for mappyng.config

"""
Configuration management for mappyng.

Style definitions, colormaps, and z-order. All default values are
imported from :mod:`mappyng.defaults`, nothing is hardcoded here.
"""

from dataclasses import dataclass, field
from typing import Dict, Any, List, Union

from . import defaults as D


[docs] @dataclass class ZOrderConfig: """Z-order for SVG layer stacking (higher = on top).""" OCEAN: int = D.Z_OCEAN RASTER: int = D.Z_RASTER GRATICULES: int = D.Z_GRATICULES SHADOW: int = D.Z_SHADOW BASEMAP: int = D.Z_BASEMAP LAYER_BACKGROUND: int = D.Z_LAYER_BACKGROUND CHOROPLETH: int = D.Z_CHOROPLETH SITUATION: int = D.Z_SITUATION PROPORTIONAL: int = D.Z_PROPORTIONAL TOPONYME: int = D.Z_TOPONYME LAYER_TOP: int = D.Z_LAYER_TOP LAYOUT: int = D.Z_LAYOUT
[docs] @dataclass class FontConfig: """Font settings.""" family: str = D.FONT_FAMILY size: int = D.FONT_SIZE weight: str = D.FONT_WEIGHT color: str = D.FONT_COLOR opacity: float = D.FONT_ALPHA
@dataclass class EdgeConfig: """Stroke settings.""" color: str = D.EDGE_COLOR width: float = D.EDGE_WIDTH
[docs] @dataclass class LegendConfig: """Legend settings.""" title: FontConfig = field(default_factory=lambda: FontConfig( size=D.LEGEND_TITLE_FONT_SIZE, weight=D.LEGEND_TITLE_FONT_WEIGHT, )) text: FontConfig = field(default_factory=FontConfig) hatch_style: str = D.LEGEND_HATCH_STYLE hatch_color: str = D.LEGEND_HATCH_COLOR spacing: float = D.LEGEND_SPACING location: str = D.LEGEND_LOCATION x: float = D.LEGEND_CONFIG_X y: float = D.LEGEND_CONFIG_Y circle_stroke: str = D.LEGEND_CIRCLE_EDGECOLOR circle_stroke_width: float = D.LEGEND_CIRCLE_LINEWIDTH circle_spacing: float = D.LEGEND_CIRCLE_SPACING title_max_chars: int = D.LEGEND_TITLE_MAX_CHARS
[docs] @dataclass class BasemapConfig: """Basemap polygon styling.""" fill_color: str = D.BASEMAP_FILL_COLOR edge: EdgeConfig = field(default_factory=lambda: EdgeConfig( color=D.BASEMAP_EDGE_COLOR, width=D.BASEMAP_EDGE_WIDTH, ))
[docs] @dataclass class ScaleConfig: """Scale bar settings.""" color: str = D.FONT_COLOR location: str = D.SCALE_BAR_LOCATION pad: float = D.SCALE_BAR_PAD
[docs] @dataclass class SourceConfig: """Source text settings.""" font: FontConfig = field(default_factory=lambda: FontConfig( size=D.SOURCE_FONT_SIZE, opacity=D.SOURCE_ALPHA, )) x: float = D.SOURCE_CONFIG_X y: float = D.SOURCE_CONFIG_Y rotation: float = D.SOURCE_ROTATION
[docs] @dataclass class StyleConfig: """Complete style configuration.""" name: str title: FontConfig = field(default_factory=FontConfig) legend: LegendConfig = field(default_factory=LegendConfig) edge: EdgeConfig = field(default_factory=EdgeConfig) basemap: BasemapConfig = field(default_factory=BasemapConfig) scale: ScaleConfig = field(default_factory=ScaleConfig) source: SourceConfig = field(default_factory=SourceConfig) z_order: ZOrderConfig = field(default_factory=ZOrderConfig) layout_edge_color: str = D.VIEWPORT_BORDER_COLOR
[docs] @dataclass class Style: """Wrapper providing dict-like access to StyleConfig.""" config: StyleConfig
[docs] def get(self, key: str, default: Any = None) -> Any: try: return self[key] except KeyError: return default
def __getitem__(self, key: str) -> Any: mapping: Dict[str, Any] = { "edge_color": self.config.edge.color, "edge_width": self.config.edge.width, "basemap_fill_color": self.config.basemap.fill_color, "basemap_edge_color": self.config.basemap.edge.color, "basemap_edge_width": self.config.basemap.edge.width, "title_color": self.config.title.color, "title_size": self.config.title.size, "title_fontweight": self.config.title.weight, "title_font_family": self.config.title.family, "legend_title_fontsize": self.config.legend.title.size, "legend_title_fontcolor": self.config.legend.title.color, "legend_title_fontweight": self.config.legend.title.weight, "legend_title_fontfamily": self.config.legend.title.family, "legend_fontsize": self.config.legend.text.size, "legend_fontcolor": self.config.legend.text.color, "legend_fontweight": self.config.legend.text.weight, "legend_fontfamily": self.config.legend.text.family, "legend_hatch_style": self.config.legend.hatch_style, "legend_hatch_color": self.config.legend.hatch_color, "scale_color": self.config.scale.color, "scale_location": self.config.scale.location, "scale_pad": self.config.scale.pad, "source_color": self.config.source.font.color, "source_alpha": self.config.source.font.opacity, "source_fontsize": self.config.source.font.size, "source_fontweight": self.config.source.font.weight, "source_fontfamily": self.config.source.font.family, "ax_layout_edge_color": self.config.layout_edge_color, } if key not in mapping: raise KeyError(f"Unknown style key: {key}") return mapping[key]
# ============================================================================ # Colormaps # ============================================================================ COLORMAP_DEFS: Dict[str, Dict[int, List[str]]] = { 'YlOrRd': { 4: ['#ffffb2', '#fecc5c', '#fd8d3c', '#e31a1c'], 5: ['#ffffb2', '#fecc5c', '#fd8d3c', '#f03b20', '#bd0026'], 6: ['#ffffb2', '#fed976', '#feb24c', '#fd8d3c', '#f03b20', '#bd0026'], 7: ['#ffffb2', '#fed976', '#feb24c', '#fd8d3c', '#fc4e2a', '#e31a1c', '#b10026'], }, 'Blues': { 4: ['#eff3ff', '#bdd7e7', '#6baed6', '#2171b5'], 5: ['#eff3ff', '#bdd7e7', '#6baed6', '#3182bd', '#08519c'], 6: ['#eff3ff', '#c6dbef', '#9ecae1', '#6baed6', '#3182bd', '#08519c'], 7: ['#eff3ff', '#c6dbef', '#9ecae1', '#6baed6', '#4292c6', '#2171b5', '#084594'], }, 'Greens': { 4: ['#edf8e9', '#bae4b3', '#74c476', '#238b45'], 5: ['#edf8e9', '#bae4b3', '#74c476', '#31a354', '#006d2c'], 6: ['#edf8e9', '#c7e9c0', '#a1d99b', '#74c476', '#31a354', '#006d2c'], 7: ['#edf8e9', '#c7e9c0', '#a1d99b', '#74c476', '#41ab5d', '#238b45', '#005a32'], }, 'Reds': { 4: ['#fee5d9', '#fcae91', '#fb6a4a', '#cb181d'], 5: ['#fee5d9', '#fcae91', '#fb6a4a', '#de2d26', '#a50f15'], 6: ['#fee5d9', '#fcbba1', '#fc9272', '#fb6a4a', '#de2d26', '#a50f15'], 7: ['#fee5d9', '#fcbba1', '#fc9272', '#fb6a4a', '#ef3b2c', '#cb181d', '#99000d'], }, 'RdYlGn': { 4: ['#d7191c', '#fdae61', '#a6d96a', '#1a9641'], 5: ['#d7191c', '#fdae61', '#ffffbf', '#a6d96a', '#1a9641'], 6: ['#d73027', '#fc8d59', '#fee08b', '#d9ef8b', '#91cf60', '#1a9850'], 7: ['#d73027', '#fc8d59', '#fee08b', '#ffffbf', '#d9ef8b', '#91cf60', '#1a9850'], }, 'RdBu': { 4: ['#ca0020', '#f4a582', '#92c5de', '#0571b0'], 5: ['#ca0020', '#f4a582', '#f7f7f7', '#92c5de', '#0571b0'], 6: ['#b2182b', '#ef8a62', '#fddbc7', '#d1e5f0', '#67a9cf', '#2166ac'], 7: ['#b2182b', '#ef8a62', '#fddbc7', '#f7f7f7', '#d1e5f0', '#67a9cf', '#2166ac'], }, 'BrBG': { 4: ['#a6611a', '#dfc27d', '#80cdc1', '#018571'], 5: ['#a6611a', '#dfc27d', '#f5f5f5', '#80cdc1', '#018571'], 6: ['#8c510a', '#d8b365', '#f6e8c3', '#c7eae5', '#5ab4ac', '#01665e'], 7: ['#8c510a', '#d8b365', '#f6e8c3', '#f5f5f5', '#c7eae5', '#5ab4ac', '#01665e'], }, } # Palettes whose colours run from one hue through a light centre to a # contrasting hue. They read the two sides apart, which suits classifications # centred on a midpoint (StdMean around the mean, factor scores around zero). DIVERGING_COLORMAPS = ('RdYlGn', 'RdBu', 'BrBG') def _build_style_from_dict(name: str, s: Dict[str, Any]) -> StyleConfig: """Build a StyleConfig from a flat style dict. Used both for presets and for user-supplied overrides. """ return StyleConfig( name=name, title=FontConfig(color=s["title_color"], size=s["title_size"], weight=s["title_weight"], family=s.get("title_font_family", D.FONT_FAMILY)), legend=LegendConfig( title=FontConfig(color=s["legend_title_color"], size=s["legend_title_size"], weight=s["legend_title_weight"], family=s.get("legend_title_font_family", D.FONT_FAMILY)), text=FontConfig(color=s["legend_text_color"], size=s["legend_text_size"]), ), edge=EdgeConfig(color=s["edge_color"], width=s["edge_width"]), basemap=BasemapConfig( fill_color=s["basemap_fill"], edge=EdgeConfig(color=s["basemap_edge_color"], width=s["basemap_edge_width"]), ), scale=ScaleConfig(color=s["scale_color"]), source=SourceConfig(font=FontConfig(color=s["source_color"], size=s["source_size"], opacity=s["source_alpha"])), layout_edge_color=s["layout_edge_color"], ) def _build_style(name: str) -> StyleConfig: """Build a StyleConfig from a defaults.STYLES preset.""" return _build_style_from_dict(name, D.STYLES[name]) STYLE_CONFIGS: Dict[str, StyleConfig] = { name: _build_style(name) for name in D.STYLES }
[docs] class ConfigManager: """Manager for style configurations and colormaps."""
[docs] @staticmethod def get_style(style_input: Union[str, Dict[str, Any]] = "classic") -> Style: """Build a Style from a preset name or a dict. Parameters ---------- style_input : str or dict - ``str``, a registered preset name (e.g. ``"classic"``, ``"modern"``). - ``dict``, custom overrides. If the dict contains a ``"base"`` key its value selects the starting preset (default ``"classic"``); all other keys override the corresponding ``STYLES`` values before building. Returns ------- Style """ if isinstance(style_input, str): if style_input not in STYLE_CONFIGS: raise ValueError( f"Unknown style: {style_input}. " f"Available: {list(STYLE_CONFIGS.keys())}" ) return Style(STYLE_CONFIGS[style_input]) if isinstance(style_input, dict): overrides = dict(style_input) base_name = overrides.pop("base", "classic") if base_name not in D.STYLES: raise ValueError( f"Unknown base style: {base_name}. " f"Available: {list(D.STYLES.keys())}" ) merged = {**D.STYLES[base_name], **overrides} # Build a one-off StyleConfig from the merged dict return Style(_build_style_from_dict(base_name, merged)) raise TypeError( f"style must be a str or dict, got {type(style_input)}" )
[docs] @staticmethod def get_colormap(name: str, num_classes: int) -> List[str]: """Return a list of hex color strings for a built-in colormap. Parameters ---------- name : str Colormap name. Available: ``"YlOrRd"``, ``"Blues"``, ``"Greens"``, ``"Reds"``, ``"RdYlGn"``. num_classes : int Number of classes (4-7). Returns ------- list of str Hex color strings, length == num_classes. Raises ------ ValueError If *name* or *num_classes* is not available. """ if name not in COLORMAP_DEFS: raise ValueError(f"Unknown colormap: {name}. Available: {list(COLORMAP_DEFS.keys())}") if num_classes not in COLORMAP_DEFS[name]: raise ValueError(f"Invalid number of classes for {name}: {num_classes}") return COLORMAP_DEFS[name][num_classes]
[docs] @staticmethod def add_style(name: str, config) -> None: """Register a custom style under *name*. Parameters ---------- name : str Style name (used as ``style=`` in :class:`~mappyng.Map`). config : StyleConfig or dict A :class:`StyleConfig` instance, or a flat dict of overrides (same format as ``STYLES`` presets). When a dict is passed it is merged on top of the ``"classic"`` preset before building the :class:`StyleConfig`. """ if isinstance(config, dict): base = {**D.STYLES["classic"], **config} config = _build_style_from_dict(name, base) elif not isinstance(config, StyleConfig): raise TypeError("config must be a StyleConfig or a dict of style overrides") STYLE_CONFIGS[name] = config