Source code for glyphx.grouped_bar

"""
GlyphX GroupedBarSeries — side-by-side bars for one value per group per category.
"""
from __future__ import annotations
import numpy as np
from .series import BaseSeries
from .utils import svg_escape, _format_tick


[docs] class GroupedBarSeries(BaseSeries): """ Grouped bar chart: for each category on the X-axis, draws one bar per group side-by-side, each with a distinct color. Args: groups: Category labels shown on the X-axis (outer grouping). categories: Series / group names (inner grouping — one bar per name). values: 2-D list ``values[group_i][cat_j]``. group_colors: Color per category (inner group). Auto-assigned if None. bar_width: Fraction of the available slot used by all bars together (0–1). label: Legend label (unused; individual category labels shown instead). """ def __init__( self, groups: list, categories: list, values: list[list[float]], group_colors: list[str] | None = None, bar_width: float = 0.8, label: str | None = None, ) -> None: from .themes import themes as _themes default_colors = _themes["default"]["colors"] self.groups = list(groups) self.categories = list(categories) self.values = values # [n_groups][n_cats] self.group_colors = (group_colors or default_colors)[:len(categories)] self.bar_width = float(bar_width) all_y = [v for row in values for v in row] y_min = min(0, min(all_y)) y_max = max(all_y) # Use 1-indexed numeric x for the groups n = len(groups) super().__init__( x=list(range(1, n + 1)), y=[y_min, y_max], color=self.group_colors[0], label=label, ) # Let render_grid use category names instead of raw numbers self._x_categories = list(groups) self._numeric_x = list(range(1, n + 1)) self.css_class = f"series-{id(self) % 100000}"
[docs] def to_svg(self, ax: object, use_y2: bool = False) -> str: scale_y = ax.scale_y2 if use_y2 else ax.scale_y # type: ignore n_groups = len(self.groups) n_cats = len(self.categories) if n_groups == 0 or n_cats == 0: return "" # Pixel width for the whole group slot (distance between group centres) if n_groups > 1: px_slot = ax.scale_x(2) - ax.scale_x(1) # type: ignore else: px_slot = (ax.width - 2 * ax.padding) * 0.8 # type: ignore px_total = px_slot * self.bar_width px_bar = px_total / n_cats y0 = scale_y(0) elements: list[str] = [] for gi, gname in enumerate(self.groups): cx_group = ax.scale_x(gi + 1) # type: ignore group centre pixel for ci, (cat, color) in enumerate(zip(self.categories, self.group_colors)): val = self.values[gi][ci] cy = scale_y(val) h = abs(cy - y0) top = min(cy, y0) # Bar centre x within the group slot offset = (ci - (n_cats - 1) / 2) * px_bar bar_cx = cx_group + offset tooltip = ( f'data-x="{svg_escape(str(gname))}" ' f'data-label="{svg_escape(str(cat))}" ' f'data-value="{svg_escape(_format_tick(val))}"' ) elements.append( f'<rect class="glyphx-point {self.css_class}" ' f'x="{bar_cx - px_bar / 2:.1f}" y="{top:.1f}" ' f'width="{px_bar * 0.92:.1f}" height="{max(h, 1):.1f}" ' f'fill="{color}" stroke="#00000022" {tooltip}/>' ) # Category color legend (right-side inline) font = ax.theme.get("font", "sans-serif") # type: ignore tc = ax.theme.get("text_color", "#000") # type: ignore lx = ax.width - ax.padding - 120 # type: ignore for ci, (cat, color) in enumerate(zip(self.categories, self.group_colors)): ly = ax.padding + ci * 18 # type: ignore elements.append( f'<rect x="{lx}" y="{ly}" width="12" height="12" fill="{color}"/>' ) elements.append( f'<text x="{lx + 16}" y="{ly + 10}" font-size="11" ' f'font-family="{font}" fill="{tc}">{svg_escape(str(cat))}</text>' ) return "\n".join(elements)