# `MishkaBlogWeb.Live.Admin.Dashboard.Analytics.Index`
[🔗](https://github.com/mishka-group/mishka_blog/blob/v0.0.1/lib/mishka_blog_web/live/admin/dashboard/analytics/index.ex#L5)

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.

# `area_path`

```elixir
@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`

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

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

# `chart_width`

```elixir
@spec chart_width() :: pos_integer()
```

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

# `day_label`

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

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

# `gridlines`

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

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

# `kpi_cards`

```elixir
@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`

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

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

# `published_label`

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

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

# `referrer_label`

```elixir
@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`

# `share`

```elixir
@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`

```elixir
@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`

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

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

---

*Consult [api-reference.md](api-reference.md) for complete listing*
