Skip to content

Commit d3f948e

Browse files
committed
v2
1 parent ef4868c commit d3f948e

8 files changed

+101
-119
lines changed

README.md

+2-4
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,15 @@ composer require brendt/php-sparkline
1717
## Usage
1818

1919
```php
20-
$sparkLine = SparkLine::new(collect([
20+
$sparkLine = SparkLine::new(
2121
new SparkLineDay(
2222
count: 1,
23-
day: new DateTimeImmutable('2022-01-01')
2423
),
2524
new SparkLineDay(
2625
count: 2,
27-
day: new DateTimeImmutable('2022-01-02')
2826
),
2927
// …
30-
]));
28+
));
3129

3230
$total = $sparkLine->getTotal();
3331
$period = $sparkLine->getPeriod(); // Spatie\Period

composer.json

+1-3
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,7 @@
1616
],
1717
"require": {
1818
"php": "^8.1",
19-
"illuminate/collections": "^9.43|^10.0|^11.0",
20-
"ramsey/uuid": "^4.6",
21-
"spatie/period": "^2.3"
19+
"ramsey/uuid": "^4.6"
2220
},
2321
"require-dev": {
2422
"friendsofphp/php-cs-fixer": "^3.13",

src/SparkLine.php

+45-72
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,12 @@
44

55
namespace Brendt\SparkLine;
66

7-
use DateTimeImmutable;
8-
use Illuminate\Support\Collection;
97
use Ramsey\Uuid\Uuid;
10-
use Spatie\Period\Period;
118

129
final class SparkLine
1310
{
14-
private Collection $days;
11+
/** @var \Brendt\SparkLine\SparkLineEntry[] */
12+
private array $entries;
1513

1614
private int $maxValue;
1715

@@ -23,40 +21,30 @@ final class SparkLine
2321

2422
private int $strokeWidth = 2;
2523

26-
private array $colors = ['#c82161', '#fe2977', '#b848f5', '#b848f5'];
24+
private array $colors;
2725

28-
public static function new(Collection $days): self
29-
{
30-
return new self($days);
31-
}
26+
private string $id;
3227

33-
public function __construct(Collection $days)
28+
public function __construct(SparkLineEntry|int ...$entries)
3429
{
35-
$this->days = $days
36-
->sortBy(fn (SparkLineDay $day) => $day->day->getTimestamp())
37-
->mapWithKeys(fn (SparkLineDay $day) => [$day->day->format('Y-m-d') => $day]);
30+
$this->id = Uuid::uuid4()->toString();
3831

39-
$this->maxValue = $this->resolveMaxValueFromDays();
40-
$this->maxItemAmount = $this->resolveMaxItemAmountFromDays();
41-
}
32+
$this->entries = array_map(
33+
fn (SparkLineEntry|int $entry) => is_int($entry) ? new SparkLineEntry($entry) : $entry,
34+
$entries
35+
);
4236

43-
public function getTotal(): int
44-
{
45-
return $this->days->sum(fn (SparkLineDay $day) => $day->count) ?? 0;
37+
$this->maxValue = $this->resolveMaxValue($this->entries);
38+
$this->maxItemAmount = $this->resolveMaxItemAmount($this->entries);
39+
$this->colors = $this->resolveColors(['#c82161', '#fe2977', '#b848f5', '#b848f5']);
4640
}
4741

48-
public function getPeriod(): ?Period
42+
public function getTotal(): int
4943
{
50-
$start = $this->days->first()?->day;
51-
$end = $this->days->last()?->day;
52-
53-
if (! $start || ! $end) {
54-
return null;
55-
}
56-
57-
return Period::make(
58-
$start,
59-
$end,
44+
return array_reduce(
45+
$this->entries,
46+
fn (int $carry, SparkLineEntry $entry) => $carry + $entry->count,
47+
0
6048
);
6149
}
6250

@@ -83,7 +71,7 @@ public function withMaxValue(?int $maxValue): self
8371
{
8472
$clone = clone $this;
8573

86-
$clone->maxValue = $maxValue ?? $clone->resolveMaxValueFromDays();
74+
$clone->maxValue = $maxValue ?? $clone->resolveMaxValue($this->entries);
8775

8876
return $clone;
8977
}
@@ -92,7 +80,7 @@ public function withMaxItemAmount(?int $maxItemAmount): self
9280
{
9381
$clone = clone $this;
9482

95-
$clone->maxItemAmount = $maxItemAmount ?? $clone->resolveMaxItemAmountFromDays();
83+
$clone->maxItemAmount = $maxItemAmount ?? $clone->resolveMaxItemAmount($this->entries);
9684

9785
return $clone;
9886
}
@@ -101,20 +89,13 @@ public function withColors(string ...$colors): self
10189
{
10290
$clone = clone $this;
10391

104-
$clone->colors = $colors;
92+
$clone->colors = $this->resolveColors($colors);
10593

10694
return $clone;
10795
}
10896

10997
public function make(): string
11098
{
111-
$coordinates = $this->resolveCoordinates();
112-
$colors = $this->resolveColors();
113-
$width = $this->width;
114-
$height = $this->height;
115-
$strokeWidth = $this->strokeWidth;
116-
$id = Uuid::uuid4()->toString();
117-
11899
ob_start();
119100

120101
include __DIR__ . '/sparkLine.view.php';
@@ -131,55 +112,47 @@ public function __toString(): string
131112
return $this->make();
132113
}
133114

134-
private function resolveColors(): array
115+
public function getCoordinates(): string
135116
{
136-
$percentageStep = floor(100 / count($this->colors));
117+
$divider = min($this->width, $this->maxItemAmount);
118+
119+
$step = floor($this->width / $divider);
120+
121+
$coordinates = [];
122+
123+
foreach ($this->entries as $index => $entry) {
124+
$coordinates[] = $index * $step . ',' . $entry->rebase($this->height - 5, $this->maxValue)->count;
125+
}
126+
127+
return implode(' ', $coordinates);
128+
}
129+
130+
private function resolveColors(array $colors): array
131+
{
132+
$percentageStep = floor(100 / count($colors));
137133

138134
$colorsWithPercentage = [];
139135

140-
foreach ($this->colors as $i => $color) {
136+
foreach ($colors as $i => $color) {
141137
$colorsWithPercentage[$i * $percentageStep] = $color;
142138
}
143139

144140
return $colorsWithPercentage;
145141
}
146142

147-
private function resolveMaxValueFromDays(): int
143+
private function resolveMaxValue(array $entries): int
148144
{
149-
if ($this->days->isEmpty()) {
145+
if ($entries === []) {
150146
return 0;
151147
}
152148

153-
return $this->days
154-
->sortByDesc(fn (SparkLineDay $day) => $day->count)
155-
->first()
156-
->count;
157-
}
149+
usort($entries, fn (SparkLineEntry $a, SparkLineEntry $b) => $a->count <=> $b->count);
158150

159-
private function resolveMaxItemAmountFromDays(): int
160-
{
161-
return max($this->days->count(), 1);
151+
return $entries[array_key_last($entries)]->count;
162152
}
163153

164-
private function resolveCoordinates(): string
154+
private function resolveMaxItemAmount(array $entries): int
165155
{
166-
$step = floor($this->width / $this->maxItemAmount);
167-
168-
return collect(range(0, $this->maxItemAmount))
169-
->map(fn (int $days) => (new DateTimeImmutable("-{$days} days"))->format('Y-m-d'))
170-
->reverse()
171-
->mapWithKeys(function (string $key) {
172-
/** @var SparkLineDay|null $day */
173-
$day = $this->days[$key] ?? null;
174-
175-
return [
176-
$key => $day
177-
? $day->rebase($this->height - 5, $this->maxValue)->count
178-
: 1, // Default value is 1 because 0 renders too small a line
179-
];
180-
})
181-
->values()
182-
->map(fn (int $count, int $index) => $index * $step . ',' . $count)
183-
->implode(' ');
156+
return max(count($entries), 1);
184157
}
185158
}

src/SparkLineDay.php src/SparkLineEntry.php

+1-4
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,18 @@
44

55
namespace Brendt\SparkLine;
66

7-
use DateTimeInterface;
87

9-
final class SparkLineDay
8+
final class SparkLineEntry
109
{
1110
public function __construct(
1211
public readonly int $count,
13-
public readonly DateTimeInterface $day,
1412
) {
1513
}
1614

1715
public function rebase(int $base, int $max): self
1816
{
1917
return new self(
2018
count: (int) floor($this->count * ($base / $max)),
21-
day: $this->day,
2219
);
2320
}
2421
}

src/sparkLine.view.php

+13-9
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
1-
<svg width="<?= $width ?>" height="<?= $height ?>">
1+
<?php
2+
/** @var \Brendt\SparkLine\SparkLine $this */
3+
?>
4+
5+
<svg width="<?= $this->width ?>" height="<?= $this->height ?>">
26
<defs>
3-
<linearGradient id="gradient-<?= $id ?>" x1="0" x2="0" y1="1" y2="0">
7+
<linearGradient id="gradient-<?= $this->id ?>" x1="0" x2="0" y1="1" y2="0">
48
<?php
5-
foreach ($colors as $percentage => $color) {
9+
foreach ($this->colors as $percentage => $color) {
610
echo <<<HTML
711
<stop offset="{$percentage}%" stop-color="{$color}"></stop>
812
HTML;
@@ -11,19 +15,19 @@
1115
}
1216
?>
1317
</linearGradient>
14-
<mask id="sparkline-<?= $id ?>" x="0" y="0" width="<?= $width ?>" height="<?= $height - 2 ?>">
18+
<mask id="sparkline-<?= $this->id ?>" x="0" y="0" width="<?= $this->width ?>" height="<?= $this->height - 2 ?>">
1519
<polyline
16-
transform="translate(0, <?= $height - 2 ?>) scale(1,-1)"
17-
points="<?= $coordinates ?>"
20+
transform="translate(0, <?= $this->height - 2 ?>) scale(1,-1)"
21+
points="<?= $this->getCoordinates() ?>"
1822
fill="transparent"
19-
stroke="<?= $colors[0] ?>"
20-
stroke-width="<?= $strokeWidth ?>"
23+
stroke="<?= $this->colors[0] ?>"
24+
stroke-width="<?= $this->strokeWidth ?>"
2125
>
2226
</polyline>
2327
</mask>
2428
</defs>
2529

2630
<g transform="translate(0, 0)">
27-
<rect x="0" y="0" width="<?= $width ?>" height="<?= $height ?>" style="stroke: none; fill: url(#gradient-<?= $id ?>); mask: url(#sparkline-<?= $id ?>)"></rect>
31+
<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>
2832
</g>
2933
</svg>

test-server.php

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<?php
2+
3+
require_once __DIR__ . '/vendor/autoload.php';
4+
5+
passthru("php -S localhost:8080 -t tests/");

tests/SparkLineTest.php

+11-27
Original file line numberDiff line numberDiff line change
@@ -5,40 +5,35 @@
55
namespace Brendt\SparkLine\Tests;
66

77
use Brendt\SparkLine\SparkLine;
8-
use Brendt\SparkLine\SparkLineDay;
9-
use DateTimeImmutable;
10-
use Illuminate\Support\Collection;
8+
use Brendt\SparkLine\SparkLineEntry;
119
use PHPUnit\Framework\TestCase;
12-
use Spatie\Period\Period;
1310

1411
final class SparkLineTest extends TestCase
1512
{
16-
private function days(): Collection
13+
private function entries(): array
1714
{
18-
return collect([
19-
new SparkLineDay(
15+
return [
16+
new SparkLineEntry(
2017
count: 1,
21-
day: new DateTimeImmutable('2022-01-01')
2218
),
23-
new SparkLineDay(
19+
new SparkLineEntry(
2420
count: 2,
25-
day: new DateTimeImmutable('2022-01-02')
2621
),
27-
]);
22+
];
2823
}
2924

3025
/** @test */
3126
public function test_create_sparkline(): void
3227
{
33-
$sparkLine = SparkLine::new($this->days())->make();
28+
$sparkLine = (new SparkLine(...$this->entries()))->make();
3429

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

3833
/** @test */
3934
public function test_colors(): void
4035
{
41-
$sparkLine = SparkLine::new($this->days())
36+
$sparkLine = (new SparkLine(...$this->entries()))
4237
->withColors('red', 'green', 'blue')
4338
->make();
4439

@@ -50,7 +45,7 @@ public function test_colors(): void
5045
/** @test */
5146
public function test_stroke_width(): void
5247
{
53-
$sparkLine = SparkLine::new($this->days())
48+
$sparkLine = (new SparkLine(...$this->entries()))
5449
->withStrokeWidth(50)
5550
->make();
5651

@@ -60,29 +55,18 @@ public function test_stroke_width(): void
6055
/** @test */
6156
public function test_dimensions(): void
6257
{
63-
$sparkLine = SparkLine::new($this->days())
58+
$sparkLine = (new SparkLine(...$this->entries()))
6459
->withDimensions(500, 501)
6560
->make();
6661

6762
$this->assertStringContainsString('width="500"', $sparkLine);
6863
$this->assertStringContainsString('height="501"', $sparkLine);
6964
}
7065

71-
/** @test */
72-
public function test_get_period(): void
73-
{
74-
$sparkLine = SparkLine::new($this->days());
75-
76-
$this->assertTrue(
77-
Period::fromString('[2022-01-01, 2022-01-02]')
78-
->equals($sparkLine->getPeriod()),
79-
);
80-
}
81-
8266
/** @test */
8367
public function test_get_total(): void
8468
{
85-
$sparkLine = SparkLine::new($this->days());
69+
$sparkLine = (new SparkLine(...$this->entries()));
8670

8771
$this->assertEquals(3, $sparkLine->getTotal());
8872
}

0 commit comments

Comments
 (0)