# 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 '
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 '
\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