From aca23d3a094cb1bf86db40e42f184fbf88371d42 Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Tue, 16 Jun 2026 22:14:26 +0300 Subject: cn0 split stats.source.sh into aggregate/render/filter-album modules Pure code move: split the ~1248-line stats.source.sh (SRP+SoC) into three cohesive modules, all functions and STATS_* constants/globals moved verbatim: - stats-aggregate.source.sh: EXIF parsing, bucketing, _stats_tally, reset/collect_photo_exif_stats and the STATS_* maps. - stats-render.source.sh: stats overview page (sections, bars, leaderboard, render_stats_page, _stats_build_body, _stats_filter_link, background pickers). - stats-filter-album.source.sh: per-filter mini-albums (render_filter_pages, gallery/view/thumbnail builders, _stats_enqueue_filter_album). Updated LIB_SOURCES accordingly. No logic change (verified: all 55 functions and top-level globals preserved; full suite, shellcheck, check-generated, diff all pass). The SoC point (HTML built in bash rather than templates) is left for a follow-up (nn0/qn0). Co-Authored-By: Claude Opus 4.8 --- src/lib/stats-render.source.sh | 396 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 396 insertions(+) create mode 100644 src/lib/stats-render.source.sh (limited to 'src/lib/stats-render.source.sh') diff --git a/src/lib/stats-render.source.sh b/src/lib/stats-render.source.sh new file mode 100644 index 0000000..66ff6b4 --- /dev/null +++ b/src/lib/stats-render.source.sh @@ -0,0 +1,396 @@ +# Stats overview page rendering. Split out of stats.source.sh (task cn0) so the +# HTML/layout concern lives apart from the EXIF aggregation/bucketing (now in +# stats-aggregate.source.sh) and the per-filter mini-albums (now in +# stats-filter-album.source.sh). This module reads the STATS_* globals filled by +# collect_photo_exif_stats and turns them into the static stats overview page +# (bar charts, sections, camera leaderboard). All libs are sourced before run, +# so the STATS_* maps and the STATS_*_DIR constants defined in the sibling +# modules are available here at runtime. + +# ---------------------------------------------------------------------------- +# Rendering (task pm0) +# ---------------------------------------------------------------------------- +# render_stats_page turns the STATS_* globals filled by the aggregation above +# into a static stats.html. The page is variable-length (each category may be +# empty, sparse, or large) which does not fit the fixed field-spec template +# engine cleanly, so we follow the same approach view/details pages use for +# their dynamic EXIF table: build the whole body as an HTML string here, hand it +# to stats.tmpl through the raw context field stats_body, and let the engine wrap +# it with the shared header/footer chrome. Bars are plain CSS (width as a percent +# of the section's top bucket) so the output stays JavaScript-free. + +# Print the largest counter in the named stats array, or 0 when it is empty. +# Used to scale each section's bars relative to its own busiest bucket. +_stats_max_count() { + local -n counts_ref="$1"; shift + local key + local -i max=0 + + for key in "${!counts_ref[@]}"; do + if (( counts_ref[$key] > max )); then + max=${counts_ref[$key]} + fi + done + printf '%d' "$max" +} + +# Print the integer percentage count/total (0 when total is 0). awk keeps the +# rounding off bash integer math; STATS_TOTALS[photos] is the denominator. +_stats_percent() { + local -ri count="$1"; shift + local -ri total="$1"; shift + + if (( total <= 0 )); then + printf '0' + return + fi + awk -v c="$count" -v t="$total" 'BEGIN { printf "%.0f", 100 * c / t }' +} + +# Emit one bar-chart
  • : an already-escaped label, a CSS-width bar scaled to +# the section maximum, and the count plus its share of all photos. Callers escape +# labels themselves because some come from EXIF (camera/lens) and some are +# trusted bucket names we build internally. +_stats_bar_row() { + local -r label_html="$1"; shift + local -ri count="$1"; shift + local -ri total="$1"; shift + local -ri max="$1"; shift + local -i width=0 + + if (( max > 0 )); then + width=$(( 100 * count / max )) + fi + printf '
  • ' + printf '%s' "$label_html" + printf '' + printf '' \ + "$width" + printf '%d (%s%%)' \ + "$count" "$(_stats_percent "$count" "$total")" + printf '
  • \n' +} + +# Open a
    with an escaped heading and the
      bar container. Split from +# the row emitters so every section shares identical chrome. An optional second +# argument adds an extra CSS class to the
        (e.g. the leaderboard uses it to +# space out and separate its rows of long, wrapping camera names). +_stats_section_open() { + local -r heading="$1"; shift + local -r list_class="${1:-}" + local heading_html + local ul_class='stats-bars' + + if [ -n "$list_class" ]; then + ul_class+=" $list_class" + fi + heading_html=$(_html_escape "$heading") + printf '
        \n' + printf '

        %s

        \n' "$heading_html" + printf '
          \n' "$ul_class" +} + +_stats_section_close() { + printf '
        \n
        \n' +} + +# Wrap an escaped label in a link to its filter mini-album. Every tallied bucket +# has a pagebase recorded in STATS_FILTER_PAGEBASE during aggregation, keyed by +# "\x1f