Skip to content

feat: inline sparklines and delta indicators in HTML report score cards #281

@decko

Description

@decko

Problem

Every raki run produces a point-in-time snapshot. The first question is always "did this get better or worse?" but the report has no historical context.

Solution

Add inline sparklines and directional deltas to each metric score card in the HTML report, pulled from .raki/history.jsonl at render time.

Acceptance criteria

  • HTML score cards show inline SVG sparkline (last N runs, default N=10)
  • Directional delta badge: ▲ improving / ▼ declining / = stable
  • Manifest-filtered: only history entries matching current run's manifest
  • Judge-config filtered for analytical metrics (via config_hash)
  • Direction threshold: delta > 2% = improving, < -2% = declining, else stable
  • Direction respects higher_is_better from METRIC_METADATA — for lower-is-better metrics (rework_cycles, cost), a decrease is ▲ improving
  • No history → score card renders normally without sparkline (no empty containers)
  • <3 history entries → show dots without connecting line, no delta badge
  • CLI print_summary() shows optional delta per metric: 0.38 (▲ vs last run)
  • raki report re-render also shows sparklines from current history
  • JSON report unchanged — no trend data embedded
  • Sparkline container has min-width; degrades to delta-only on narrow viewports
  • Tests: sparkline rendering with mock history, no-history graceful degradation, manifest filtering, judge-config filtering
  • Towncrier fragment

Implementation Plan

Task 1: Build sparkline data helper

File: src/raki/report/sparkline.py (new)

Write failing tests in tests/test_sparkline.py:

  • test_build_sparkline_data_returns_values — 10 history entries → list of 10 values per metric
  • test_build_sparkline_data_filters_by_manifest — only matching manifest entries included
  • test_build_sparkline_data_empty_history — returns empty dict
  • test_build_sparkline_data_caps_at_n — 20 entries, N=10 → last 10 only
  • test_build_direction_improving — delta > 2% for higher-is-better metric → "improving"
  • test_build_direction_lower_is_better — decrease in rework_cycles → "improving" (not declining)
  • test_build_direction_declining — delta < -2% → "declining"
  • test_build_direction_stable — delta within ±2% → "stable"
  • test_build_direction_insufficient_data — <3 entries → None

Implement:

@dataclass
class SparklineData:
    values: list[float]     # last N values for this metric
    direction: str | None   # "improving" / "declining" / "stable" / None
    current: float | None   # most recent value

def build_sparkline_data(
    entries: list[HistoryEntry],
    manifest_filter: str | None = None,
    max_points: int = 10,
) -> dict[str, SparklineData]:
    # Uses is_higher_is_better() from report/diff.py for direction logic

Task 2: Pass sparkline data to HTML template

File: src/raki/report/html_report.py

Modify write_html_report() signature to accept optional sparklines: dict[str, SparklineData] | None.

Pass to the template as sparklines context variable.

Task 3: Render sparklines in score cards

File: src/raki/report/templates/report.html.j2

Write failing tests:

  • test_sparkline_svg_rendered_when_data_exists — sparkline SVG present in HTML
  • test_sparkline_hidden_when_no_data — no sparkline elements in HTML
  • test_delta_badge_improving — "improving" badge rendered
  • test_delta_badge_hidden_below_3_points — <3 data points → no badge

Inside each score card, after the metric value, conditionally render:

{% if sparklines and name in sparklines %}
{% set spark = sparklines[name] %}
<div class="sparkline-container">
  <svg class="sparkline-svg" viewBox="0 0 100 28">
    {% for val in spark.values %}
    <circle cx="{{ loop.index0 * (100 / (spark.values|length - 1)) }}" cy="{{ 26 - (val * 24) }}" r="{{ 2.5 if loop.last else 1.5 }}" fill="{{ 'var(--blue)' if loop.last else 'var(--text-muted)' }}" opacity="{{ 1 if loop.last else 0.4 }}"/>
    {% endfor %}
  </svg>
  {% if spark.direction %}
  <span class="delta-badge delta-{{ spark.direction }}">{{ '▲' if spark.direction == 'improving' else '▼' if spark.direction == 'declining' else '=' }} {{ spark.direction }}</span>
  {% endif %}
</div>
{% endif %}

Task 4: Add sparkline CSS

Add to template <style>: sparkline-container, sparkline-svg, delta-badge classes (from mockup #281).

Task 5: Wire history loading in CLI

File: src/raki/cli.py

In the run command, after generating the report:

  1. Load history via load_history() (already done for history writing)
  2. Call build_sparkline_data(entries, manifest_filter=manifest_name)
  3. Pass to write_html_report(report, ..., sparklines=sparklines)

In the report re-render command:

  1. Load history from default path
  2. Build sparkline data
  3. Pass to renderer

Task 6: CLI delta in print_summary

File: src/raki/report/cli_summary.py

Modify print_summary() to accept optional sparklines dict. For each metric, if sparkline data exists and has direction, append (▲ vs last run) to the metric line.

Task 7: Towncrier fragment

changes/281.feature: Add inline sparklines and directional delta indicators (▲ improving / ▼ declining / = stable) to HTML report score cards, pulled from evaluation history.

Verification

uv run pytest tests/test_sparkline.py -v
uv run pytest tests/test_report_html.py -v -k "sparkline or delta_badge"
uv run pytest tests/ -v -m "not slow"

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions