diff --git a/CHANGELOG.md b/CHANGELOG.md index f8b6537..1750f02 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,3 +2,10 @@ All notable changes to `php-sparkline` will be documented in this file. +## 2.0.0 + +- Removed `SparkLine::new()`, use `new SparkLine()` instead +- Removed `SparkLine::getPeriod()` +- Removed dependencies on `spatie/period` and `laravel/collection` +- Rename `SparkLineDay` to `SparkLineEntry` +- Allow integers to be passed directly into a new `SparkLine` instead of requiring `SparkLineEntry` objects diff --git a/README.md b/README.md index 7f82f0d..d5f6456 100644 --- a/README.md +++ b/README.md @@ -17,82 +17,41 @@ composer require brendt/php-sparkline ## Usage ```php -$sparkLine = SparkLine::new(collect([ - new SparkLineDay( - count: 1, - day: new DateTimeImmutable('2022-01-01') - ), - new SparkLineDay( - count: 2, - day: new DateTimeImmutable('2022-01-02') - ), - // … -])); +$sparkLine = new SparkLine(1, 2, 5, 10, 2)); $total = $sparkLine->getTotal(); -$period = $sparkLine->getPeriod(); // Spatie\Period + $svg = $sparkLine->make(); ``` ![](./.github/img/0.png) -To construct a sparkline, you'll have to pass in a collection of `Brendt\SparkLineDay` objects. This object takes two parameters: a `count`, and a `DateTimeInterface`. You could for example convert database entries like so: +To construct a sparkline, you'll have to pass in an array of values. -```php -$days = PostVistisPerDay::query() - ->orderByDesc('day') - ->limit(20) - ->get() - ->map(fn (SparkLineDay $row) => new SparkLineDay( - count: $row->visits, - day: Carbon::make($row->day), - )); -``` +### Customization -In many cases though, you'll want to aggregate data with an SQL query, and convert those aggregations on the fly to `SparkLineDay` objects: +You can pick any amount of colors and the sparkline will automatically generate a gradient from them: ```php -$days = DB::query() - ->from((new Post())->getTable()) - ->selectRaw('`published_at_day`, COUNT(*) as `visits`') - ->groupBy('published_at_day') - ->orderByDesc('published_at_day') - ->whereNotNull('published_at_day') - ->limit(20) - ->get() - ->map(fn (object $row) => new SparkLineDay( - count: $row->visits, - day: Carbon::make($row->published_at_day), - )); +$sparkLine = (new SparkLine($days))->withColors('#4285F4', '#31ACF2', '#2BC9F4'); ``` -### Customization +![](./.github/img/1.png) -This package offers some methods to customize the sparkline. First off, you can pick any amount of colors and the sparkline will automatically generate a gradient from them: +You can configure the stroke width: ```php -$sparkLine = SparkLine::new($days)->withColors('#4285F4', '#31ACF2', '#2BC9F4'); +$sparkLine = (new SparkLine($days))->withStrokeWidth(4); ``` -![](./.github/img/1.png) - -Next, you can configure a bunch of numbers: +As well as the dimensions (in pixels): ```php -$sparkLine = SparkLine::new($days) - ->withStrokeWidth(4) - ->withDimensions(500, 100) - ->withMaxItemAmount(100) - ->withMaxValue(20); +$sparkLine = SparkLine::new($days)->withDimensions(width: 500, height: 100); ``` ![](./.github/img/2.png) -- **`withStrokeWidth`** will determine the stroke's width -- **`withDimensions`** will determine the width and height of the rendered SVG -- **`withMaxItemAmount`** will determine how many days will be shown. If you originally passed on more days than this max, then the oldest ones will be omitted. If the max amount is set to a number that's _higher_ than the current amount of days, then the sparkline will contain empty days. By default, the amount of given days will be used. -- **`withMaxValue`** will set the maximum value of the sparkline. This is useful if you have multiple sparklines that should all have the same scale. By default, the maximum value is determined based on the given days. - ## Testing ```bash diff --git a/composer.json b/composer.json index 28ac88a..ba1593b 100644 --- a/composer.json +++ b/composer.json @@ -16,9 +16,7 @@ ], "require": { "php": "^8.1", - "illuminate/collections": "^9.43|^10.0|^11.0", - "ramsey/uuid": "^4.6", - "spatie/period": "^2.3" + "ramsey/uuid": "^4.6" }, "require-dev": { "friendsofphp/php-cs-fixer": "^3.13", diff --git a/src/SparkLine.php b/src/SparkLine.php index 777120c..49242c9 100644 --- a/src/SparkLine.php +++ b/src/SparkLine.php @@ -4,14 +4,12 @@ namespace Brendt\SparkLine; -use DateTimeImmutable; -use Illuminate\Support\Collection; use Ramsey\Uuid\Uuid; -use Spatie\Period\Period; final class SparkLine { - private Collection $days; + /** @var \Brendt\SparkLine\SparkLineEntry[] */ + private array $entries; private int $maxValue; @@ -23,40 +21,30 @@ final class SparkLine private int $strokeWidth = 2; - private array $colors = ['#c82161', '#fe2977', '#b848f5', '#b848f5']; + private array $colors; - public static function new(Collection $days): self - { - return new self($days); - } + private string $id; - public function __construct(Collection $days) + public function __construct(SparkLineEntry|int ...$entries) { - $this->days = $days - ->sortBy(fn (SparkLineDay $day) => $day->day->getTimestamp()) - ->mapWithKeys(fn (SparkLineDay $day) => [$day->day->format('Y-m-d') => $day]); + $this->id = Uuid::uuid4()->toString(); - $this->maxValue = $this->resolveMaxValueFromDays(); - $this->maxItemAmount = $this->resolveMaxItemAmountFromDays(); - } + $this->entries = array_map( + fn (SparkLineEntry|int $entry) => is_int($entry) ? new SparkLineEntry($entry) : $entry, + $entries + ); - public function getTotal(): int - { - return $this->days->sum(fn (SparkLineDay $day) => $day->count) ?? 0; + $this->maxValue = $this->resolveMaxValue($this->entries); + $this->maxItemAmount = $this->resolveMaxItemAmount($this->entries); + $this->colors = $this->resolveColors(['#c82161', '#fe2977', '#b848f5', '#b848f5']); } - public function getPeriod(): ?Period + public function getTotal(): int { - $start = $this->days->first()?->day; - $end = $this->days->last()?->day; - - if (! $start || ! $end) { - return null; - } - - return Period::make( - $start, - $end, + return array_reduce( + $this->entries, + fn (int $carry, SparkLineEntry $entry) => $carry + $entry->count, + 0 ); } @@ -83,7 +71,7 @@ public function withMaxValue(?int $maxValue): self { $clone = clone $this; - $clone->maxValue = $maxValue ?? $clone->resolveMaxValueFromDays(); + $clone->maxValue = $maxValue ?? $clone->resolveMaxValue($this->entries); return $clone; } @@ -92,7 +80,7 @@ public function withMaxItemAmount(?int $maxItemAmount): self { $clone = clone $this; - $clone->maxItemAmount = $maxItemAmount ?? $clone->resolveMaxItemAmountFromDays(); + $clone->maxItemAmount = $maxItemAmount ?? $clone->resolveMaxItemAmount($this->entries); return $clone; } @@ -101,20 +89,13 @@ public function withColors(string ...$colors): self { $clone = clone $this; - $clone->colors = $colors; + $clone->colors = $this->resolveColors($colors); return $clone; } public function make(): string { - $coordinates = $this->resolveCoordinates(); - $colors = $this->resolveColors(); - $width = $this->width; - $height = $this->height; - $strokeWidth = $this->strokeWidth; - $id = Uuid::uuid4()->toString(); - ob_start(); include __DIR__ . '/sparkLine.view.php'; @@ -131,55 +112,47 @@ public function __toString(): string return $this->make(); } - private function resolveColors(): array + private function getCoordinates(): string { - $percentageStep = floor(100 / count($this->colors)); + $divider = min($this->width, $this->maxItemAmount); + + $step = floor($this->width / $divider); + + $coordinates = []; + + foreach ($this->entries as $index => $entry) { + $coordinates[] = $index * $step . ',' . $entry->rebase($this->height - 5, $this->maxValue)->count; + } + + return implode(' ', $coordinates); + } + + private function resolveColors(array $colors): array + { + $percentageStep = floor(100 / count($colors)); $colorsWithPercentage = []; - foreach ($this->colors as $i => $color) { + foreach ($colors as $i => $color) { $colorsWithPercentage[$i * $percentageStep] = $color; } return $colorsWithPercentage; } - private function resolveMaxValueFromDays(): int + private function resolveMaxValue(array $entries): int { - if ($this->days->isEmpty()) { + if ($entries === []) { return 0; } - return $this->days - ->sortByDesc(fn (SparkLineDay $day) => $day->count) - ->first() - ->count; - } + usort($entries, fn (SparkLineEntry $a, SparkLineEntry $b) => $a->count <=> $b->count); - private function resolveMaxItemAmountFromDays(): int - { - return max($this->days->count(), 1); + return $entries[array_key_last($entries)]->count; } - private function resolveCoordinates(): string + private function resolveMaxItemAmount(array $entries): int { - $step = floor($this->width / $this->maxItemAmount); - - return collect(range(0, $this->maxItemAmount)) - ->map(fn (int $days) => (new DateTimeImmutable("-{$days} days"))->format('Y-m-d')) - ->reverse() - ->mapWithKeys(function (string $key) { - /** @var SparkLineDay|null $day */ - $day = $this->days[$key] ?? null; - - return [ - $key => $day - ? $day->rebase($this->height - 5, $this->maxValue)->count - : 1, // Default value is 1 because 0 renders too small a line - ]; - }) - ->values() - ->map(fn (int $count, int $index) => $index * $step . ',' . $count) - ->implode(' '); + return max(count($entries), 1); } } diff --git a/src/SparkLineDay.php b/src/SparkLineEntry.php similarity index 71% rename from src/SparkLineDay.php rename to src/SparkLineEntry.php index 8df8757..74a380d 100644 --- a/src/SparkLineDay.php +++ b/src/SparkLineEntry.php @@ -4,13 +4,10 @@ namespace Brendt\SparkLine; -use DateTimeInterface; - -final class SparkLineDay +final class SparkLineEntry { public function __construct( public readonly int $count, - public readonly DateTimeInterface $day, ) { } @@ -18,7 +15,6 @@ public function rebase(int $base, int $max): self { return new self( count: (int) floor($this->count * ($base / $max)), - day: $this->day, ); } } diff --git a/src/sparkLine.view.php b/src/sparkLine.view.php index 68ba49d..e7bfe92 100644 --- a/src/sparkLine.view.php +++ b/src/sparkLine.view.php @@ -1,8 +1,12 @@ - + + + - + $color) { + foreach ($this->colors as $percentage => $color) { echo << HTML; @@ -11,19 +15,19 @@ } ?> - + - + diff --git a/test-server.php b/test-server.php new file mode 100644 index 0000000..5ffee91 --- /dev/null +++ b/test-server.php @@ -0,0 +1,5 @@ +days())->make(); + $sparkLine = (new SparkLine(...$this->entries()))->make(); $this->assertStringContainsString('days()) + $sparkLine = (new SparkLine(...$this->entries())) ->withColors('red', 'green', 'blue') ->make(); @@ -50,7 +45,7 @@ public function test_colors(): void /** @test */ public function test_stroke_width(): void { - $sparkLine = SparkLine::new($this->days()) + $sparkLine = (new SparkLine(...$this->entries())) ->withStrokeWidth(50) ->make(); @@ -60,7 +55,7 @@ public function test_stroke_width(): void /** @test */ public function test_dimensions(): void { - $sparkLine = SparkLine::new($this->days()) + $sparkLine = (new SparkLine(...$this->entries())) ->withDimensions(500, 501) ->make(); @@ -68,21 +63,10 @@ public function test_dimensions(): void $this->assertStringContainsString('height="501"', $sparkLine); } - /** @test */ - public function test_get_period(): void - { - $sparkLine = SparkLine::new($this->days()); - - $this->assertTrue( - Period::fromString('[2022-01-01, 2022-01-02]') - ->equals($sparkLine->getPeriod()), - ); - } - /** @test */ public function test_get_total(): void { - $sparkLine = SparkLine::new($this->days()); + $sparkLine = (new SparkLine(...$this->entries())); $this->assertEquals(3, $sparkLine->getTotal()); } diff --git a/tests/index.php b/tests/index.php new file mode 100644 index 0000000..5f9f7db --- /dev/null +++ b/tests/index.php @@ -0,0 +1,22 @@ + rand(1, 5), + range(1, 1000) + ) +)) + ->withStrokeWidth(1) + ->withDimensions(width: 1000, height: 200); + +?> + + + + + +