Skip to content

Commit

Permalink
v2
Browse files Browse the repository at this point in the history
  • Loading branch information
brendt committed Jun 11, 2024
1 parent ef4868c commit d3f948e
Show file tree
Hide file tree
Showing 8 changed files with 101 additions and 119 deletions.
6 changes: 2 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,15 @@ composer require brendt/php-sparkline
## Usage

```php
$sparkLine = SparkLine::new(collect([
$sparkLine = SparkLine::new(
new SparkLineDay(
count: 1,
day: new DateTimeImmutable('2022-01-01')
),
new SparkLineDay(
count: 2,
day: new DateTimeImmutable('2022-01-02')
),
// …
]));
));

$total = $sparkLine->getTotal();
$period = $sparkLine->getPeriod(); // Spatie\Period
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
public 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);
}
}
5 changes: 1 addition & 4 deletions src/SparkLineDay.php → src/SparkLineEntry.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,18 @@

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/");
38 changes: 11 additions & 27 deletions tests/SparkLineTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,40 +5,35 @@
namespace Brendt\SparkLine\Tests;

use Brendt\SparkLine\SparkLine;
use Brendt\SparkLine\SparkLineDay;
use DateTimeImmutable;
use Illuminate\Support\Collection;
use Brendt\SparkLine\SparkLineEntry;
use PHPUnit\Framework\TestCase;
use Spatie\Period\Period;

final class SparkLineTest extends TestCase
{
private function days(): Collection
private function entries(): array
{
return collect([
new SparkLineDay(
return [
new SparkLineEntry(
count: 1,
day: new DateTimeImmutable('2022-01-01')
),
new SparkLineDay(
new SparkLineEntry(
count: 2,
day: new DateTimeImmutable('2022-01-02')
),
]);
];
}

/** @test */
public function test_create_sparkline(): void
{
$sparkLine = SparkLine::new($this->days())->make();
$sparkLine = (new SparkLine(...$this->entries()))->make();

$this->assertStringContainsString('<svg', $sparkLine);
}

/** @test */
public function test_colors(): void
{
$sparkLine = SparkLine::new($this->days())
$sparkLine = (new SparkLine(...$this->entries()))
->withColors('red', 'green', 'blue')
->make();

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

Expand All @@ -60,29 +55,18 @@ 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();

$this->assertStringContainsString('width="500"', $sparkLine);
$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());
}
Expand Down
Loading

0 comments on commit d3f948e

Please sign in to comment.