MishkaBlogWeb.Live.Admin.Dashboard.Analytics.Index (Mishka Blog v0.0.1)

Copy Markdown View Source

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

area_path(series, peak)

@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.

chart_label(stats)

@spec chart_label(map()) :: String.t()

What the chart says for a screen reader, in one sentence.

chart_width()

@spec chart_width() :: pos_integer()

How wide that box is, so a rule can be drawn right across it.

day_label(date)

@spec day_label(Date.t()) :: String.t()

Month and day, e.g. "Jul 20" — the chart's axis ends and the peak marker.

gridlines()

@spec gridlines() :: [non_neg_integer()]

The horizontal rules the chart is read against, as y positions in the viewBox.

kpi_cards(stats, days, areas)

@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.

line_path(series, peak)

@spec line_path([map()], non_neg_integer()) :: String.t()

The line through the daily view counts, as an SVG path.

published_label(post)

@spec published_label(map()) :: String.t()

When a top-performing post went out, or that it has not.

referrer_label(label)

@spec referrer_label(String.t()) :: String.t()

A referrer as the page shows it: the Direct bucket in the reader's language, a host as it is.

render(assigns)

Callback implementation for Phoenix.LiveView.render/1.

share(count, total)

@spec share(non_neg_integer(), non_neg_integer()) :: String.t()

A count's share of the period's views, as a whole percentage.

A source with views but under half a percent reads <1% rather than 0%, which would say it brought nobody.

share_width(count, total)

@spec share_width(non_neg_integer(), non_neg_integer()) :: String.t()

The bar width for a share, floored so a small but real source still draws as a bar.

view_box()

@spec view_box() :: String.t()

The viewBox the chart is drawn in — a fixed box the card stretches to its own width.