Skip to content

Commit

Permalink
Merge pull request #10 from brendt/v2
Browse files Browse the repository at this point in the history
v2
  • Loading branch information
brendt authored Jun 11, 2024
2 parents ef4868c + b2cd6d4 commit ae55ab9
Show file tree
Hide file tree
Showing 9 changed files with 116 additions and 168 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
63 changes: 11 additions & 52 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 1 addition & 3 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
117 changes: 45 additions & 72 deletions src/SparkLine.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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
);
}

Expand All @@ -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;
}
Expand All @@ -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;
}
Expand All @@ -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';
Expand All @@ -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);
}
}
6 changes: 1 addition & 5 deletions src/SparkLineDay.php → src/SparkLineEntry.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,17 @@

namespace Brendt\SparkLine;

use DateTimeInterface;

final class SparkLineDay
final class SparkLineEntry
{
public function __construct(
public readonly int $count,
public readonly DateTimeInterface $day,
) {
}

public function rebase(int $base, int $max): self
{
return new self(
count: (int) floor($this->count * ($base / $max)),
day: $this->day,
);
}
}
22 changes: 13 additions & 9 deletions src/sparkLine.view.php
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
<svg width="<?= $width ?>" height="<?= $height ?>">
<?php
/** @var \Brendt\SparkLine\SparkLine $this */
?>

<svg width="<?= $this->width ?>" height="<?= $this->height ?>">
<defs>
<linearGradient id="gradient-<?= $id ?>" x1="0" x2="0" y1="1" y2="0">
<linearGradient id="gradient-<?= $this->id ?>" x1="0" x2="0" y1="1" y2="0">
<?php
foreach ($colors as $percentage => $color) {
foreach ($this->colors as $percentage => $color) {
echo <<<HTML
<stop offset="{$percentage}%" stop-color="{$color}"></stop>
HTML;
Expand All @@ -11,19 +15,19 @@
}
?>
</linearGradient>
<mask id="sparkline-<?= $id ?>" x="0" y="0" width="<?= $width ?>" height="<?= $height - 2 ?>">
<mask id="sparkline-<?= $this->id ?>" x="0" y="0" width="<?= $this->width ?>" height="<?= $this->height - 2 ?>">
<polyline
transform="translate(0, <?= $height - 2 ?>) scale(1,-1)"
points="<?= $coordinates ?>"
transform="translate(0, <?= $this->height - 2 ?>) scale(1,-1)"
points="<?= $this->getCoordinates() ?>"
fill="transparent"
stroke="<?= $colors[0] ?>"
stroke-width="<?= $strokeWidth ?>"
stroke="<?= $this->colors[0] ?>"
stroke-width="<?= $this->strokeWidth ?>"
>
</polyline>
</mask>
</defs>

<g transform="translate(0, 0)">
<rect x="0" y="0" width="<?= $width ?>" height="<?= $height ?>" style="stroke: none; fill: url(#gradient-<?= $id ?>); mask: url(#sparkline-<?= $id ?>)"></rect>
<rect x="0" y="0" width="<?= $this->width ?>" height="<?= $this->height ?>" style="stroke: none; fill: url(#gradient-<?= $this->id ?>); mask: url(#sparkline-<?= $this->id ?>)"></rect>
</g>
</svg>
5 changes: 5 additions & 0 deletions test-server.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?php

require_once __DIR__ . '/vendor/autoload.php';

passthru("php -S localhost:8080 -t tests/");
Loading

0 comments on commit ae55ab9

Please sign in to comment.