Skip to content

Make leaderboard rendering pixel-perfect and support full rankings - #255

Merged
ChuckBuilds merged 3 commits into
mainfrom
claude/nfl-leaderboard-display-q1kul0
Aug 5, 2026
Merged

Make leaderboard rendering pixel-perfect and support full rankings#255
ChuckBuilds merged 3 commits into
mainfrom
claude/nfl-leaderboard-display-q1kul0

Conversation

@ChuckBuilds

Copy link
Copy Markdown
Owner

Pull Request

Summary

The leaderboard read as blurry on the panel and stopped partway through the list. This makes text and logos pixel-perfect on the LED grid, fixes a layout bug where the measured strip width disagreed with what was drawn, and lets top_teams: 0 show every team a league returns — with a startup warning when the list is longer than the display-duration budget can reach.

Type of change

  • Bug fix in an existing plugin
  • New plugin (also fill out the SUBMISSION.md checklist below)
  • New feature for an existing plugin
  • Documentation only
  • Repo-wide change (registry script, hook, top-level docs)

Plugin(s) affected

ledmatrix-leaderboard (1.2.3 → 1.3.0)

Related issues

N/A — reported directly.

What was wrong

1. Anti-aliasing (the blur). Press Start 2P only rasterises without anti-aliasing at multiples of 8, but the renderer loaded it at 6/10/12/14 and left PIL's default font mode on. Measured on a 10-team NFL strip:

distinct lit colours partially-lit text pixels
before 39 35%
after 2 0%

On a 1:1 LED matrix each partially-lit pixel is a dim LED, not a smooth edge. Text now draws with fontmode = "1" — the idiom news and baseball-scoreboard already use — and default sizes snap to the font's pixel grid (8px on a 32px panel, 16px on 64px).

2. Logos. They were scaled to int(height * 1.2) and pasted at a negative y, so the top and bottom of every logo was cropped off the panel; they were also forced into a square box regardless of source aspect ratio, and LANCZOS left a ring of semi-transparent edge pixels that lit as a halo. Logos now fit inside the panel, keep their aspect ratio, and have their alpha thresholded to hard on/off.

3. Layout. Measuring and drawing were two passes over the same data that budgeted differently — the width pass always counted a team logo while the draw pass skipped missing ones, and the draw pass advanced 10px further per league than the measure pass. The strip came out either padded with dead space (1322px allocated for 839px of content in the missing-logo case) or clipped short of the last team. Both passes now share one resolved layout.

4. Showing the full rankings. top_teams accepts 0 to mean every team the league returns. A long list also needs time to scroll: the display controller allows min(plugin cap, core cap) seconds and then moves on mid-scroll, which is what makes a long list appear to cut off at an arbitrary team. The core's display.dynamic_duration.max_duration_seconds defaults to 180s and is usually the binding one — all 32 NFL teams is ~3,200px, about 240s at the default 15 px/s. The plugin now reads the real cap and warns at startup naming the limiting setting and roughly how much of the list will not be reached. The README documents all three caps.

Also seeds self.scroll_speed on the time-based path, where get_info() would otherwise raise AttributeError on the default config.

New config (global.appearance)

pixel_perfect_text, crisp_logos, text_outline, logo_scale, font_size — all with defaults matching the new behaviour, fine-tuning keys marked x-advanced. pixel_perfect_text: false restores the old softer look.

Test plan

  • Loaded the plugin in LEDMatrix on real hardware
  • Loaded the plugin in LEDMatrix emulator mode (EMULATOR=true python3 run.py)
  • Rendered the plugin against a core checkout and eyeballed the PNGs at 64×32, 128×32, 128×64 and 256×32
  • Verified the web UI configuration form against the schema (schema validates as Draft-07; every new key has default, description and constraints)
  • N/A — repo-wide / docs-only change

Additional automated coverage:

  • New test_pixel_perfect.py (15 tests, all passing) — asserts zero anti-aliased text pixels at three panel heights, font sizes on the pixel grid, nothing drawn past the panel edge, logo alpha thresholding and aspect preservation, and that measured width matches drawn content both with and without logos.
  • New test/harness.json + test/fixtures/mock.json — the core safety harness previously only ever rendered the no-data fallback here because CI has no network. With the fixture it renders the real drawing path; check_plugin.py PASSes at all 8 sizes.
  • scripts/check_module_collisions.py: OK across 43 plugins.

Not verified on real hardware — I don't have a panel in this environment, so the crispness claims rest on pixel measurements and rendered PNGs rather than a photo of the matrix.

Required for plugin changes

  • Bumped version in plugins/<id>/manifest.json (1.2.3 → 1.3.0, with a versions changelog entry)
  • class_name in manifest.json matches the actual class in manager.py exactly
  • entry_point matches the real file
  • Updated the plugin's README.md (new Appearance section and a "How many teams are shown" section covering the duration caps)
  • config_schema.json is the source of truth for the web UI form — every new option has a default, description, and constraints
  • Pre-commit hook ran successfully (plugins.json synced to 1.3.0)

Checklist

  • My commits follow the message convention in CONTRIBUTING.md
  • I read CONTRIBUTING.md and CODE_OF_CONDUCT.md
  • I've not committed any secrets

Notes for reviewer

  • Font size is a visible change. On a 32px panel the rank number goes from 14px to 8px and the abbreviation from 12px to 8px, because 8 and 16 are the only crisp sizes for Press Start 2P below 24. A 32px panel could instead use 16px and still be crisp, just bolder and ~6% wider per team — happy to switch if you prefer the larger look. global.appearance.font_size lets users pick either way (values are snapped to the grid).
  • top_teams minimum changed from 5 to 0 in the schema for every league, so 0 can mean "all". Existing configs are unaffected.
  • I did not add golden images. The harness renders one frame at scroll position 0, which shows only the league logo, and exact-match comparison across Pillow versions on a LANCZOS-resized logo looked more likely to produce CI flakes than to catch drift. The property assertions in test_pixel_perfect.py cover the same ground more robustly.
  • The duration-cap warning reads the core's config through plugin_manager.config_manager / cache_manager.config_manager (the same resolution order BasePlugin.global_config uses) because this plugin reassigns self.global_config to its own config slice. It falls back to the core's documented 180s default if neither manager is reachable.

Generated by Claude Code

The leaderboard read as blurry on the panel and stopped partway through
the list. Three separate causes:

Anti-aliasing. Press Start 2P only rasterises without anti-aliasing at
multiples of 8, but the renderer loaded it at 6/10/12/14 and left PIL's
default font mode on. Measured on a 10-team NFL strip, 35% of lit text
pixels were partially-lit greys across 39 distinct colours — on a 1:1
LED matrix each of those is a dim LED, not a smooth edge. Text now
draws with fontmode "1" (the idiom the news and baseball plugins
already use) and default sizes snap to the font's pixel grid, giving
exactly two colours and zero anti-aliased pixels.

Logos. They were scaled to 120% of the panel height and pasted at a
negative offset, so the top and bottom of every logo was cropped, then
squashed into a square box regardless of source aspect ratio. LANCZOS
also left a ring of semi-transparent edge pixels that lit as a halo.
Logos now fit inside the panel, keep their aspect ratio, and have their
alpha thresholded.

Layout. Measuring and drawing were two passes over the same data that
budgeted differently — the width pass always counted a team logo while
the draw pass skipped missing ones, and the draw pass advanced 10px
further per league than the measure pass. The strip came out either
padded with dead space or clipped short of the last team. Both now
share one resolved layout, so measured width is drawn width.

For showing the full rankings: top_teams accepts 0 to mean every team a
league returns. A long list also needs the time to scroll — the display
controller allows min(plugin cap, core cap) seconds and then moves on
mid-scroll, which is what makes a long list appear to cut off at an
arbitrary team. The plugin now computes whether the content fits that
budget and warns at startup naming the limiting cap and how much of the
list will not be reached; the README documents all three caps.

Also seeds self.scroll_speed on the time-based path, where get_info()
would otherwise raise AttributeError on the default config.

Adds test_pixel_perfect.py (15 tests) covering anti-aliasing, grid
snapping, panel bounds, logo alpha/aspect, and measure-vs-draw width,
plus a test/harness.json fixture so the core safety harness renders the
real drawing path instead of only the no-data fallback.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WexvwNDtWLVymGVqKD7BGk
@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@ChuckBuilds, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 20 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 64976b14-1d98-48ba-98b4-de05f8d697a6

📥 Commits

Reviewing files that changed from the base of the PR and between 2c35486 and b1fc220.

📒 Files selected for processing (10)
  • plugins.json
  • plugins/ledmatrix-leaderboard/README.md
  • plugins/ledmatrix-leaderboard/config_schema.json
  • plugins/ledmatrix-leaderboard/data_fetcher.py
  • plugins/ledmatrix-leaderboard/image_renderer.py
  • plugins/ledmatrix-leaderboard/manager.py
  • plugins/ledmatrix-leaderboard/manifest.json
  • plugins/ledmatrix-leaderboard/test/fixtures/mock.json
  • plugins/ledmatrix-leaderboard/test/harness.json
  • plugins/ledmatrix-leaderboard/test_pixel_perfect.py

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

The manifest gate requires the newly-added versions[] entry to use
ledmatrix_min_version; ledmatrix_min is the deprecated spelling and is
not what the store and loader read. Older entries are left as they are.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WexvwNDtWLVymGVqKD7BGk
@codacy-production

codacy-production Bot commented Aug 5, 2026

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 81 complexity

Metric Results
Complexity 81

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

The except/continue added for the duration-cap lookup discarded the
exception with no trace, so a persistently broken config manager would
show only as the plugin quietly using the default cap. Logs at debug
with the traceback, matching how BasePlugin.global_config handles the
same failure.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WexvwNDtWLVymGVqKD7BGk
@ChuckBuilds
ChuckBuilds merged commit 72ef29c into main Aug 5, 2026
4 checks passed
@ChuckBuilds
ChuckBuilds deleted the claude/nfl-leaderboard-display-q1kul0 branch August 5, 2026 22:26
ChuckBuilds added a commit that referenced this pull request Aug 5, 2026
Follows the same three defects fixed in the leaderboard (#255). news
already had this -- it is where the fontmode idiom came from -- so it is
untouched here.

Anti-aliasing. PIL anti-aliases text by default, and on a 1:1 LED matrix
a partially-lit pixel is a visibly dim LED rather than a smooth edge.
None of these three plugins set fontmode, so all their text carried the
fringe. Each now routes every draw surface -- including the scratch
canvases used only to measure text, so metrics cannot disagree with what
is drawn -- through a _pixel_draw() helper that sets fontmode "1".

How much that mattered differed per plugin, measured on the string
"AAPL +1.2% Earnings":

- stock-news derives its font size from the panel height when one is not
  configured, and height//3 lands on 10 for a 32px panel. Press Start 2P
  only rasterises cleanly at multiples of 8, so 57% of the lit pixels
  were partial. Now 0%.
- odds-ticker and ledmatrix-stocks default their element sizes to 8,
  already on the grid, so their defaults were crisp by luck rather than
  by construction -- any size a user set in the customization UI was
  not. At a configured 12: 41% partial before, 0% after.

Font grid. stock-news' auto size is now snapped to the font's grid, so
the 32px panel gets 8 instead of 10 and glyph pixels map 1:1 rather than
alternating 1px and 2px strokes. Only that case changes; 48/64/96px
panels already resolved to 16. An explicit font_size is honoured as
typed.

Logos. odds-ticker sized team logos to int(height * 1.2) and then
centred them with (height - logo_size) // 2, which is -3 on a 32px
panel -- so the top and bottom of every logo was cropped off the panel,
the same bug the leaderboard had. Logos now fit inside the panel.


Claude-Session: https://claude.ai/code/session_01WexvwNDtWLVymGVqKD7BGk

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant