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.

The three metric types
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:
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
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.