Source code for glyphx.sparkline

"""
GlyphX SparklineSeries — tiny inline charts for dashboards and tables.

A sparkline is a miniature line or bar chart, typically 60-120px wide and
20-40px tall, suitable for embedding beside KPI numbers in tables, cards,
and status dashboards.  Matplotlib has no sparkline primitive.
Plotly's require full Figure scaffolding.

    from glyphx.sparkline import sparkline_svg

    # Returns a raw SVG string — embed anywhere
    svg = sparkline_svg([1, 3, 2, 5, 4, 6], width=80, height=28, color="#2563eb")

    # Or as a Figure series for a standalone chart
    from glyphx import Figure
    from glyphx.sparkline import SparklineSeries
    fig = Figure(width=120, height=40, auto_display=False)
    fig.add(SparklineSeries([1,3,2,5,4,6], color="#2563eb"))
    fig.show()
"""
from __future__ import annotations

import math
from .series import BaseSeries
from .utils  import svg_escape


# ---------------------------------------------------------------------------
# Standalone helper — returns a raw SVG string with no Figure overhead
# ---------------------------------------------------------------------------

[docs] def sparkline_svg( data: list[float], width: int = 80, height: int = 28, color: str = "#2563eb", kind: str = "line", line_width: float = 1.5, fill: bool = True, fill_alpha: float = 0.18, show_last_dot: bool = True, padding: int = 2, ) -> str: """ Render a compact sparkline and return raw SVG markup. Args: data: Numeric values to plot. width: Pixel width of the sparkline. height: Pixel height. color: Line / bar fill color. kind: ``"line"`` or ``"bar"``. line_width: Stroke width for line sparklines. fill: Shade area under the line. fill_alpha: Fill opacity (0–1). show_last_dot: Mark the last data point with a circle. padding: Pixel margin around the chart area. Returns: Complete ``<svg>`` element as a string. """ if not data or len(data) < 2: return f'<svg width="{width}" height="{height}" xmlns="http://www.w3.org/2000/svg"></svg>' n = len(data) lo = min(data) hi = max(data) span = hi - lo or 1.0 pw = width - 2 * padding ph = height - 2 * padding def sx(i: int) -> float: return padding + i * pw / (n - 1) def sy(v: float) -> float: return padding + ph - (v - lo) / span * ph parts: list[str] = [ f'<svg width="{width}" height="{height}" ' f'xmlns="http://www.w3.org/2000/svg" ' f'viewBox="0 0 {width} {height}">' ] if kind == "bar": bw = max(1, pw / n * 0.8) y0 = sy(max(lo, 0)) for i, v in enumerate(data): bx = sx(i) by = sy(v) bh = abs(by - y0) top = min(by, y0) parts.append( f'<rect x="{bx - bw/2:.1f}" y="{top:.1f}" ' f'width="{bw:.1f}" height="{max(bh, 1):.1f}" ' f'fill="{color}" fill-opacity="0.8"/>' ) else: pts = " ".join(f"{sx(i):.2f},{sy(v):.2f}" for i, v in enumerate(data)) if fill: poly_pts = ( f"{sx(0):.2f},{sy(lo):.2f} " + pts + f" {sx(n-1):.2f},{sy(lo):.2f}" ) parts.append( f'<polygon points="{poly_pts}" ' f'fill="{color}" fill-opacity="{fill_alpha}" stroke="none"/>' ) parts.append( f'<polyline points="{pts}" fill="none" ' f'stroke="{color}" stroke-width="{line_width}" ' f'stroke-linejoin="round" stroke-linecap="round"/>' ) if show_last_dot: lx, ly = sx(n - 1), sy(data[-1]) parts.append( f'<circle cx="{lx:.2f}" cy="{ly:.2f}" r="{line_width + 1}" ' f'fill="{color}"/>' ) parts.append("</svg>") return "".join(parts)
# --------------------------------------------------------------------------- # Series wrapper for use inside a Figure # ---------------------------------------------------------------------------
[docs] class SparklineSeries(BaseSeries): """ Lightweight sparkline series. Designed for very small canvases (width ≤ 200, height ≤ 60). Omits axis lines, grid, and labels. Args: data: Numeric values. color: Line color. kind: ``"line"`` or ``"bar"``. fill: Shade area under the line. fill_alpha: Fill opacity. line_width: Stroke width. show_last_dot: Highlight the final data point. label: Legend label. """ def __init__( self, data: list[float], color: str = "#2563eb", kind: str = "line", fill: bool = True, fill_alpha: float = 0.18, line_width: float = 1.5, show_last_dot: bool = True, label: str | None = None, ) -> None: self.data = list(data) self.kind = kind self.fill = fill self.fill_alpha = fill_alpha self.line_width = float(line_width) self.show_last_dot = show_last_dot self.css_class = f"series-{id(self) % 100000}" n = len(data) super().__init__( x=list(range(n)), y=list(data), color=color, label=label, )
[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 x_vals = getattr(self, "_numeric_x", self.x) n = len(x_vals) if n < 2: return "" lo, hi = min(self.data), max(self.data) if self.kind == "bar": y0 = scale_y(max(lo, 0)) pw = (ax.width - 2 * ax.padding) / n # type: ignore bw = pw * 0.8 elements = [] for i, (x, v) in enumerate(zip(x_vals, self.data)): cx = ax.scale_x(x) # type: ignore cy = scale_y(v) bh = abs(cy - y0) top = min(cy, y0) elements.append( f'<rect x="{cx - bw/2:.1f}" y="{top:.1f}" ' f'width="{bw:.1f}" height="{max(bh, 1):.1f}" ' f'fill="{self.color}" fill-opacity="0.85"/>' ) return "\n".join(elements) pts = " ".join( f"{ax.scale_x(x):.2f},{scale_y(v):.2f}" # type: ignore for x, v in zip(x_vals, self.data) ) out: list[str] = [] if self.fill: y_base = scale_y(max(lo, 0)) x0, xn = ax.scale_x(x_vals[0]), ax.scale_x(x_vals[-1]) # type: ignore poly = f"{x0:.2f},{y_base:.2f} " + pts + f" {xn:.2f},{y_base:.2f}" out.append( f'<polygon points="{poly}" fill="{self.color}" ' f'fill-opacity="{self.fill_alpha}" stroke="none"/>' ) out.append( f'<polyline points="{pts}" fill="none" stroke="{self.color}" ' f'stroke-width="{self.line_width}" ' f'stroke-linejoin="round" stroke-linecap="round"/>' ) if self.show_last_dot: lx = ax.scale_x(x_vals[-1]) # type: ignore ly = scale_y(self.data[-1]) out.append( f'<circle cx="{lx:.2f}" cy="{ly:.2f}" ' f'r="{self.line_width + 1}" fill="{self.color}"/>' ) return "\n".join(out)