NewWe open-sourced 50+ Laravel packages
Custom AI apps, agents and automation — Roundly ConsultingRoundly
All packages

A trend aggregates a value over time, bucketed by a unit, and returns a trends map keyed by bucket:

use App\Models\User;
use RoundlyConsulting\Metrics\Enums\Period;
use RoundlyConsulting\Metrics\Facades\Metrics;

Metrics::trend()
    ->count(User::query(), 'created_at')
    ->daily()
    ->range(Period::MonthToDate)
    ->toArray();
// result => ['trends' => ['2024-06-01' => 12.0, '2024-06-02' => 0.0, ...]]

Units

Set the unit with unit() — a Unit case or its string — or a convenience helper. The default is DAY unless you change metrics.default_unit; an unknown unit string is ignored and the current unit is kept:

use RoundlyConsulting\Metrics\Enums\Unit;

$metric->unit(Unit::Hour);   // or ->unit('HOUR')
$metric->perMinute();
$metric->hourly();
$metric->daily();            // the default (config: metrics.default_unit)
$metric->weekly();
$metric->monthly();
$metric->yearly();
ValueEnum caseHelperBucket key
MINUTEUnit::MinuteperMinute()Y-m-d H:i:00
HOURUnit::Hourhourly()Y-m-d H:00
DAYUnit::Daydaily()Y-m-d
WEEKUnit::Weekweekly()o-W — ISO year and week, e.g. 2022-52
MONTHUnit::Monthmonthly()Y-m
YEARUnit::Yearyearly()Y

Gap filling

Every bucket in the selected range is present in the output — empty buckets are filled with 0 so charts get a continuous axis. The axis starts at the bucket that holds the range start (the ISO week of January 1st for a weekly YTD, the month of the start for a monthly 90), so the bucket holding “now” is always there. Opt out to return only the buckets that have rows:

Metrics::trend()->count(User::query(), 'created_at')->daily()->withoutGapFilling()->toArray();

$metric->withoutGapFilling();          // only buckets that have rows
$metric->withoutGapFilling(false);     // re-enable filling (the default)

With the ALL range, the span is inferred from the first and last buckets that have data.

Multi-series trends

Split a trend by a dimension with groupBy(). The totals stay under trends — the ungrouped trend over the same range, i.e. the metric’s own aggregate over every row (an average of all rows, not a sum of the series’ averages) — and an additive series key holds one bucket set per dimension value, each gap-filled across the same range, so every series shares the same buckets:

Metrics::trend()->count(User::query(), 'created_at')->daily()->groupBy('plan')->toArray();
// result => [
//   'trends' => ['2024-01-01' => 30.0, ...],            // the ungrouped trend
//   'series' => [
//     'pro'  => ['2024-01-01' => 20.0, ...],
//     'free' => ['2024-01-01' => 10.0, ...],
//   ],
// ]

A NULL group and an empty-string group share the series key '', so their buckets are merged with the metric’s aggregate — summed for count and sum, the larger or smaller for max and min, a weighted average for average — rather than one overwriting the other. A boolean dimension is keyed 0 / 1 on every database.

The query’s own orderBy() is ignored: buckets always come back in date order, and an ordered query (latest()) does not break the GROUP BY on PostgreSQL or MySQL. The date column is quoted by the query grammar, so a camelCase or reserved-word name (occurredAt, order) works.

Database support

Bucketing uses a database-specific SQL date expression. MySQL, MariaDB, PostgreSQL and SQLite are supported out of the box, so the same trend runs unchanged in development and production. Week buckets are ISO-8601 on every driver — including SQLite, which has no native ISO week. Any other driver throws MissingTrendQueryExpressionException until you map an expression for it in config('metrics.trend_drivers') (see Extending).

Show your open-source love

This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.

More ways to support, including crypto

By donating, you agree to our donation terms.

Want this built into your product?

We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.