fix: analytics charts now show y-axis units & tick labels

5 tick labels (0/25/50/75/100% of max) appear on every analytics
chart's y-axis — line_chart macro accepts unit ("s") or as_pct=True
for percentage charts, plus an optional axis_label rendered top-left
above the highest tick. Daily-views bar chart gets the same treatment
inline (unit "views").

Per chart:
  Daily views        bar  → "views" axis, integer ticks
  Session Duration   line → "s" unit, "seconds" axis label
  Engagement         line → 0-100% formatting
  Bounce Rate        line → 0-100% formatting
  External Referrer  line → "visits" axis, integer ticks

viewBox widens from 560→610 to reserve ~50px on the left for the
y-tick labels; SVG scales to container so no CSS change is needed.
This commit is contained in:
russell@unturf.com 2026-05-15 09:35:01 -04:00
parent 4cbd7e4b20
commit 14ebda23a0
No known key found for this signature in database

View file

@ -6,29 +6,43 @@
{% block content -%}
{% macro line_chart(data, max_value, label, color) %}
{# y-axis tick label — `as_pct` formats v as a percentage (0..1 → 0%..100%),
otherwise show v rounded as integer with `unit` suffix appended. #}
{% macro y_tick_label(v, unit, as_pct) -%}
{%- if as_pct -%}{{ (v * 100) | round(0) | int }}%{%- else -%}{{ v | round(0) | int }}{{ unit }}{%- endif -%}
{%- endmacro %}
{% macro line_chart(data, max_value, label, color, unit="", as_pct=False, axis_label="") %}
{% if data and max_value > 0 %}
<div class="analytics-chart-svg">
<svg viewBox="0 0 560 160" role="img" aria-label="{{ label }}">
{% for pct in [25, 50, 75] %}
<line x1="0" y1="{{ 140 - (pct / 100 * 130) }}" x2="556" y2="{{ 140 - (pct / 100 * 130) }}"
stroke="var(--border-light, #e9ecef)" stroke-width="1" />
<svg viewBox="0 0 610 170" role="img" aria-label="{{ label }}">
{# y-axis label (top-left, above the highest tick) #}
{% if axis_label %}
<text x="50" y="8" text-anchor="start"
font-size="9" fill="var(--text-muted, #999)">{{ axis_label }}</text>
{% endif %}
{# gridlines + y-axis tick labels (0/25/50/75/100% of max) #}
{% for pct in [0, 25, 50, 75, 100] %}
{% set y_pos = 140 - (pct / 100 * 130) %}
<line x1="50" y1="{{ y_pos }}" x2="606" y2="{{ y_pos }}"
stroke="{{ 'var(--border-color, #dee2e6)' if pct == 0 else 'var(--border-light, #e9ecef)' }}" stroke-width="1" />
<text x="46" y="{{ y_pos }}" text-anchor="end" dominant-baseline="middle"
font-size="9" fill="var(--text-muted, #999)">{{ y_tick_label((pct / 100) * max_value, unit, as_pct) }}</text>
{% endfor %}
<line x1="0" y1="140" x2="556" y2="140" stroke="var(--border-color, #dee2e6)" stroke-width="1" />
<polygon points="{% for b in data %}{{ loop.index0 * 20 + 8 }},{{ 140 - (b.value / max_value * 130) }} {% endfor %}{{ (data|length - 1) * 20 + 8 }},140 8,140"
<polygon points="{% for b in data %}{{ loop.index0 * 20 + 58 }},{{ 140 - (b.value / max_value * 130) }} {% endfor %}{{ (data|length - 1) * 20 + 58 }},140 58,140"
fill="{{ color }}" opacity="0.15" />
<polyline
points="{% for b in data %}{{ loop.index0 * 20 + 8 }},{{ 140 - (b.value / max_value * 130) }} {% endfor %}"
points="{% for b in data %}{{ loop.index0 * 20 + 58 }},{{ 140 - (b.value / max_value * 130) }} {% endfor %}"
fill="none" stroke="{{ color }}" stroke-width="2" stroke-linejoin="round" stroke-linecap="round" />
{% for b in data %}
<circle cx="{{ loop.index0 * 20 + 8 }}" cy="{{ 140 - (b.value / max_value * 130) }}"
<circle cx="{{ loop.index0 * 20 + 58 }}" cy="{{ 140 - (b.value / max_value * 130) }}"
r="3" fill="{{ color }}">
<title>{{ b.label }}: {{ b.tooltip }}</title>
</circle>
{% endfor %}
{% for b in data %}
{% if loop.index0 % 7 == 0 %}
<text x="{{ loop.index0 * 20 + 8 }}" y="155" text-anchor="middle"
<text x="{{ loop.index0 * 20 + 58 }}" y="155" text-anchor="middle"
font-size="9" fill="var(--text-muted, #999)">{{ b.label }}</text>
{% endif %}
{% endfor %}
@ -73,17 +87,22 @@
{% if daily_views and daily_views_max > 0 %}
<h4>Daily Views (last 28 days)</h4>
<div class="analytics-chart-svg">
<svg viewBox="0 0 560 160" role="img" aria-label="Daily views bar chart">
{# Gridlines #}
{% for pct in [25, 50, 75] %}
<line x1="0" y1="{{ 140 - (pct / 100 * 130) }}" x2="556" y2="{{ 140 - (pct / 100 * 130) }}"
stroke="var(--border-light, #e9ecef)" stroke-width="1" />
<svg viewBox="0 0 610 170" role="img" aria-label="Daily views bar chart">
{# y-axis label #}
<text x="50" y="8" text-anchor="start"
font-size="9" fill="var(--text-muted, #999)">views</text>
{# Gridlines + y-axis tick labels #}
{% for pct in [0, 25, 50, 75, 100] %}
{% set y_pos = 140 - (pct / 100 * 130) %}
<line x1="50" y1="{{ y_pos }}" x2="606" y2="{{ y_pos }}"
stroke="{{ 'var(--border-color, #dee2e6)' if pct == 0 else 'var(--border-light, #e9ecef)' }}" stroke-width="1" />
<text x="46" y="{{ y_pos }}" text-anchor="end" dominant-baseline="middle"
font-size="9" fill="var(--text-muted, #999)">{{ ((pct / 100) * daily_views_max) | round(0) | int }}</text>
{% endfor %}
<line x1="0" y1="140" x2="556" y2="140" stroke="var(--border-color, #dee2e6)" stroke-width="1" />
{# Bars #}
{% for b in daily_views %}
{% set bar_h = (b.count / daily_views_max * 130) if daily_views_max > 0 else 0 %}
<rect x="{{ loop.index0 * 20 }}" y="{{ 140 - bar_h }}" width="16" height="{{ bar_h }}"
<rect x="{{ loop.index0 * 20 + 50 }}" y="{{ 140 - bar_h }}" width="16" height="{{ bar_h }}"
rx="2" fill="var(--green-color, #a3c765)">
<title>{{ b.label }}: {{ b.count }} view{{ 's' if b.count != 1 else '' }}</title>
</rect>
@ -91,7 +110,7 @@
{# Date labels every 7th bar #}
{% for b in daily_views %}
{% if loop.index0 % 7 == 0 %}
<text x="{{ loop.index0 * 20 + 8 }}" y="155" text-anchor="middle"
<text x="{{ loop.index0 * 20 + 58 }}" y="155" text-anchor="middle"
font-size="9" fill="var(--text-muted, #999)">{{ b.label }}</text>
{% endif %}
{% endfor %}
@ -103,19 +122,19 @@
{% if daily_session_duration_max > 0 %}
<h4>Session Duration Trend (last 28 days)</h4>
<p class="analytics-hint">Average time visitors spend per session.</p>
{{ line_chart(daily_session_duration, daily_session_duration_max, "Session duration trend", "var(--blue-color, #98b6fa)") }}
{{ line_chart(daily_session_duration, daily_session_duration_max, "Session duration trend", "var(--blue-color, #98b6fa)", unit="s", axis_label="seconds") }}
{% endif %}
{% if daily_engagement %}
<h4>Engagement Trend (last 28 days)</h4>
<p class="analytics-hint">Active interaction time as a fraction of total session time.</p>
{{ line_chart(daily_engagement, 1.0, "Engagement trend", "var(--green-color, #a3c765)") }}
{{ line_chart(daily_engagement, 1.0, "Engagement trend", "var(--green-color, #a3c765)", as_pct=True, axis_label="%") }}
{% endif %}
{% if daily_bounce %}
<h4>Bounce Rate Trend (last 28 days)</h4>
<p class="analytics-hint">Sessions under 7 seconds visible. Lower is better.</p>
{{ line_chart(daily_bounce, 1.0, "Bounce rate trend", "var(--red-color, #bc2131)") }}
{{ line_chart(daily_bounce, 1.0, "Bounce rate trend", "var(--red-color, #bc2131)", as_pct=True, axis_label="%") }}
{% endif %}
{# --- Ring Consumed breakdown --- #}
@ -429,7 +448,7 @@
{% if daily_referrers_max > 0 %}
<h4>External Referrer Trend (last 28 days)</h4>
<p class="analytics-hint">Daily count of visits from external referrers (excludes direct and internal traffic).</p>
{{ line_chart(daily_referrers, daily_referrers_max, "External referrer trend", "var(--orange-color, #e8a838)") }}
{{ line_chart(daily_referrers, daily_referrers_max, "External referrer trend", "var(--orange-color, #e8a838)", axis_label="visits") }}
{% endif %}
{# --- Top Referrer Domains (21d) --- #}