The blog analytics page: five KPI tiles, a daily views chart, the most-read posts, where their
readers came from and whether they were signed in — every figure read from the view log by
MishkaBlog.Dashboard.Analytics.
The page shows only what MishkaBlog.PostView records. There is no average read time and no bounce
rate here because nothing in the schema observes a reader leaving a post, and a tile that cannot be
measured is worse than a tile that is missing.
The read runs off the render in a start_async/3 and is repeated whenever the period changes, so
every figure and every panel describes the same window. A result is only assigned while its days
is still the selected one, so clicking through the periods quickly cannot land an older read on top
of a newer one. Until the first one lands each figure is an em dash: kpi_cards/3 has a nil
clause over the same metadata as the real one, so there is no second skeleton to keep in step.
The chart is inline SVG built from the series — line_path/2 and area_path/2 project the daily
counts onto a fixed viewBox, and vector-effect="non-scaling-stroke" keeps the stroke even once
the box has been stretched to the card's width.
Number formatting is MishkaBlogWeb.Live.Admin.Dashboard.Home.Index's — growth/2, percent/2,
move_tone/2, delta/2 and status_tone/1 — so the overview and this page colour and word the
same figures identically rather than drifting apart; counts, dates and percentages are written in
the reader's language by MishkaCmsCore.Localized.
The page opens on a notice (#analytics-roadmap) that Analytics is on the roadmap: nothing records
a view yet, so every figure here is empty or sample data.
The page is a holder's of the blog's admin:dashboard:blog:post (MishkaBlogWeb.SidebarMenu).
The Engagement tile divides comments by views, so it is drawn only for a viewer who also holds
admin:dashboard:blog:comment.
See MishkaBlog.Dashboard.Analytics for the queries.
Summary
Functions
The same line closed down to the baseline, so the area beneath it can be filled.
What the chart says for a screen reader, in one sentence.
How wide that box is, so a rule can be drawn right across it.
Month and day, e.g. "Jul 20" — the chart's axis ends and the peak marker.
The horizontal rules the chart is read against, as y positions in the viewBox.
The KPI tiles areas (MishkaBlog.Dashboard.Overview.areas/1) may see, as
{label, icon, icon_tone, value, note, note_tone}.
The line through the daily view counts, as an SVG path.
When a top-performing post went out, or that it has not.
A referrer as the page shows it: the Direct bucket in the reader's language, a host as it is.
Callback implementation for Phoenix.LiveView.render/1.
A count's share of the period's views, as a whole percentage.
The bar width for a share, floored so a small but real source still draws as a bar.
The viewBox the chart is drawn in — a fixed box the card stretches to its own width.
Functions
@spec area_path([map()], non_neg_integer()) :: String.t()
The same line closed down to the baseline, so the area beneath it can be filled.
What the chart says for a screen reader, in one sentence.
@spec chart_width() :: pos_integer()
How wide that box is, so a rule can be drawn right across it.
Month and day, e.g. "Jul 20" — the chart's axis ends and the peak marker.
@spec gridlines() :: [non_neg_integer()]
The horizontal rules the chart is read against, as y positions in the viewBox.
@spec kpi_cards(map() | nil, pos_integer(), MishkaBlog.Dashboard.Overview.areas()) :: [ {String.t(), String.t(), String.t(), String.t(), String.t(), String.t()} ]
The KPI tiles areas (MishkaBlog.Dashboard.Overview.areas/1) may see, as
{label, icon, icon_tone, value, note, note_tone}.
The nil clause is what the page renders until MishkaBlog.Dashboard.Analytics.load/2 returns.
@spec line_path([map()], non_neg_integer()) :: String.t()
The line through the daily view counts, as an SVG path.
When a top-performing post went out, or that it has not.
A referrer as the page shows it: the Direct bucket in the reader's language, a host as it is.
Callback implementation for Phoenix.LiveView.render/1.
@spec view_box() :: String.t()
The viewBox the chart is drawn in — a fixed box the card stretches to its own width.