Skip to content

Metrics

Reshape includes a small, hand-rolled metrics system — Nova users will recognize the shape immediately: value metrics with trend comparison, time-series trends, and grouped partitions, each rendered as a dashboard card.

Metric cards on the dashboard

The three metric types

php
abstract class Metric
{
    abstract public function calculate(MetricShowRequest $request): JsonSerializable;
    abstract public function ranges(): array;

    public function id(): string          // kebab-case class name, e.g. "total-users"
    public function defaultRange(): ?string
    public function cacheFor(): ?DateTimeInterface   // null = no caching
}
  • Value — a single number for the selected range, automatically compared against the previous equivalent period (a trend delta) — total counts, sums, averages.
  • Trend — a time-series, bucketed by minute/hour/day/week/month depending on the range, with driver-aware SQL date grouping so it works identically on SQLite, MySQL, and PostgreSQL.
  • Partition — a breakdown grouped by a column, e.g. users by status.

Writing a metric

A Value metric is usually one line — the base class provides count/sum/average/max/min helpers that already know how to apply the date range:

php
class TotalUsers extends Value
{
    public function calculate(MetricShowRequest $request): ValueResult
    {
        return $this->count($request, User::class);
    }
}

Value's default ranges are ALL, 30, 60, 90 days — override ranges() on your own metric if you need TODAY/YESTERDAY/MTD/QTD/YTD style windows instead, which the base class also understands.

Registering it

php
class MetricRegistry
{
    public const METRICS = [
        TotalCustomers::class,
        TotalUsers::class,
        TotalCompanies::class,
        NewUsers::class,
        NewCompanies::class,
        UsersByStatus::class,   // a Partition example
    ];

    public static function resolve(string $id): Metric
    {
        return self::all()[$id] ?? throw new NotFoundHttpException;
    }
}

To add your own metric: write the class, add it to METRICS, done — no controller changes needed. Fetching a metric is gated by the PrivilegedMetricsIndex permission, and if a metric declares cacheFor(), results are cached per unique combination of request parameters — useful for expensive aggregations you don't need computed on every dashboard load.

In the frontend

web/src/components/metrics/*stat-card.vue (value + trend badge + a period-select for switching ranges), line-chart-card.vue and donut-chart-card.vue (trend/partition visualizations via Unovis). The dashboard composes these in routes/_dashboard/index/-components/dashboard-index.vue.