From 88ff247f36743b96d4ef08d7d2fea1846f8011e1 Mon Sep 17 00:00:00 2001 From: Jamie Schouten Date: Wed, 22 Jan 2025 22:13:23 +0100 Subject: [PATCH] Add grouped bar chart (#14) --- README.md | 50 ++-- .../{simple-bar-chart.php => bar-chart.php} | 0 ...curved-chart.php => curved-line-chart.php} | 0 examples/grouped-bar-chart.php | 140 +++++++++++ .../{simple-line-chart.php => line-chart.php} | 0 .../{simple-bar-chart.svg => bar-chart.svg} | 0 ...curved-chart.svg => curved-line-chart.svg} | 0 examples/output/grouped-bar-chart.svg | 37 +++ .../{simple-line-chart.svg => line-chart.svg} | 0 ...ed-bar-chart.svg => stacked-bar-chart.svg} | 0 ...ple-step-chart.svg => step-line-chart.svg} | 0 ...ed-bar-chart.php => stacked-bar-chart.php} | 0 ...ple-step-chart.php => step-line-chart.php} | 0 src/Bar/Bar.php | 15 +- src/Bar/BarGroup.php | 102 ++++++++ src/SVG/Fragment.php | 4 +- tests/Unit/GroupedBarChartTest.php | 234 ++++++++++++++++++ 17 files changed, 557 insertions(+), 25 deletions(-) rename examples/{simple-bar-chart.php => bar-chart.php} (100%) rename examples/{simple-curved-chart.php => curved-line-chart.php} (100%) create mode 100644 examples/grouped-bar-chart.php rename examples/{simple-line-chart.php => line-chart.php} (100%) rename examples/output/{simple-bar-chart.svg => bar-chart.svg} (100%) rename examples/output/{simple-curved-chart.svg => curved-line-chart.svg} (100%) create mode 100644 examples/output/grouped-bar-chart.svg rename examples/output/{simple-line-chart.svg => line-chart.svg} (100%) rename examples/output/{simple-stacked-bar-chart.svg => stacked-bar-chart.svg} (100%) rename examples/output/{simple-step-chart.svg => step-line-chart.svg} (100%) rename examples/{simple-stacked-bar-chart.php => stacked-bar-chart.php} (100%) rename examples/{simple-step-chart.php => step-line-chart.php} (100%) create mode 100644 src/Bar/BarGroup.php create mode 100644 tests/Unit/GroupedBarChartTest.php diff --git a/README.md b/README.md index 869bb71..f99b311 100644 --- a/README.md +++ b/README.md @@ -23,31 +23,47 @@ composer require maantje/charts Below are some examples of the types of charts you can create using this library. Click on the links to view the source code for each example. -### Simple line chart -![alt text](./examples/output/simple-line-chart.svg) -[View source](./examples/simple-line-chart.php) - -### Curved line chart -![alt text](./examples/output/simple-curved-chart.svg) -[View source](./examples/simple-curved-chart.php) +- [Simple Line Chart](#simple-line-chart) +- [Curved Line Chart](#curved-line-chart) +- [Step Line Chart](#step-line-chart) +- [Bar Chart](#bar-chart) +- [Stacked Bar Chart](#stacked-bar-chart) +- [Grouped Bar Chart](#grouped-bar-chart) +- [Advanced Line Chart](#advanced-line-chart) +- [Advanced Bar Chart](#advanced-bar-chart) +- [Mixed Chart](#mixed-chart) +- [Pie Chart](#pie-chart) + + +### Simple Line Chart +![alt text](./examples/output/line-chart.svg) +[View source](./examples/line-chart.php) + +### Curved Line Chart +![alt text](./examples/output/curved-line-chart.svg) +[View source](./examples/curved-line-chart.php) ### Step line chart -![alt text](./examples/output/simple-step-chart.svg) -[View source](./examples/simple-step-chart.php) +![alt text](./examples/output/step-line-chart.svg) +[View source](./examples/step-line-chart.php) + +### Bar Chart +![alt text](./examples/output/bar-chart.svg) +[View source](./examples/bar-chart.php) -### Simple bar chart -![alt text](./examples/output/simple-bar-chart.svg) -[View source](./examples/simple-bar-chart.php) +### Stacked Bar Chart +![alt text](./examples/output/stacked-bar-chart.svg) +[View source](./examples/stacked-bar-chart.php) -### Simple stacked chart -![alt text](./examples/output/simple-stacked-bar-chart.svg) -[View source](./examples/simple-stacked-bar-chart.php) +### Grouped Bar Chart +![alt text](./examples/output/grouped-bar-chart.svg) +[View source](./examples/grouped-bar-chart.php) -### Advanced line charts +### Advanced Line Chart ![alt text](./examples/output/advanced-line-chart.svg) [View source](./examples/advanced-line-chart.php) -### Advanced bar chart +### Advanced Bar Chart ![alt text](./examples/output/advanced-bar-chart.svg) [View source](./examples/advanced-bar-chart.php) diff --git a/examples/simple-bar-chart.php b/examples/bar-chart.php similarity index 100% rename from examples/simple-bar-chart.php rename to examples/bar-chart.php diff --git a/examples/simple-curved-chart.php b/examples/curved-line-chart.php similarity index 100% rename from examples/simple-curved-chart.php rename to examples/curved-line-chart.php diff --git a/examples/grouped-bar-chart.php b/examples/grouped-bar-chart.php new file mode 100644 index 0000000..c7afd25 --- /dev/null +++ b/examples/grouped-bar-chart.php @@ -0,0 +1,140 @@ +render(); diff --git a/examples/simple-line-chart.php b/examples/line-chart.php similarity index 100% rename from examples/simple-line-chart.php rename to examples/line-chart.php diff --git a/examples/output/simple-bar-chart.svg b/examples/output/bar-chart.svg similarity index 100% rename from examples/output/simple-bar-chart.svg rename to examples/output/bar-chart.svg diff --git a/examples/output/simple-curved-chart.svg b/examples/output/curved-line-chart.svg similarity index 100% rename from examples/output/simple-curved-chart.svg rename to examples/output/curved-line-chart.svg diff --git a/examples/output/grouped-bar-chart.svg b/examples/output/grouped-bar-chart.svg new file mode 100644 index 0000000..1a39ec8 --- /dev/null +++ b/examples/output/grouped-bar-chart.svg @@ -0,0 +1,37 @@ + + + 02004006008001,000 + + + + +101 +251 +389 +457 +601 + +January +73 +223 +347 +509 +653 + +February +113 +281 +401 +541 +733 + +March +193 +311 +457 +613 +809 + +April + + \ No newline at end of file diff --git a/examples/output/simple-line-chart.svg b/examples/output/line-chart.svg similarity index 100% rename from examples/output/simple-line-chart.svg rename to examples/output/line-chart.svg diff --git a/examples/output/simple-stacked-bar-chart.svg b/examples/output/stacked-bar-chart.svg similarity index 100% rename from examples/output/simple-stacked-bar-chart.svg rename to examples/output/stacked-bar-chart.svg diff --git a/examples/output/simple-step-chart.svg b/examples/output/step-line-chart.svg similarity index 100% rename from examples/output/simple-step-chart.svg rename to examples/output/step-line-chart.svg diff --git a/examples/simple-stacked-bar-chart.php b/examples/stacked-bar-chart.php similarity index 100% rename from examples/simple-stacked-bar-chart.php rename to examples/stacked-bar-chart.php diff --git a/examples/simple-step-chart.php b/examples/step-line-chart.php similarity index 100% rename from examples/simple-step-chart.php rename to examples/step-line-chart.php diff --git a/src/Bar/Bar.php b/src/Bar/Bar.php index 0310a1c..9b7c712 100644 --- a/src/Bar/Bar.php +++ b/src/Bar/Bar.php @@ -10,13 +10,14 @@ class Bar implements BarContract { public function __construct( - public string $name, - public float $value, + public ?string $name = null, + public float $value = 0, public ?string $yAxis = null, public string $color = '#3498db', public ?float $width = 100, public string $labelColor = '#333', - public int $labelMarginY = 30 + public int $labelMarginY = 30, + public ?int $radius = null, ) {} public function render(Chart $chart, float $x, float $maxBarWidth): string @@ -37,9 +38,11 @@ public function render(Chart $chart, float $x, float $maxBarWidth): string width: $width, height: $chart->bottom() - $y, fill: $this->color, - title: $this->value + rx: $this->radius ?? 0, + ry: $this->radius ?? 0, + title: $this->value, ), - new Text( + $this->name ? new Text( content: $this->name, x: $labelX, y: $chart->bottom() + $this->labelMarginY, @@ -47,7 +50,7 @@ public function render(Chart $chart, float $x, float $maxBarWidth): string fontSize: $chart->fontSize, fill: $this->labelColor, textAnchor: 'middle' - ), + ) : null, ]); } diff --git a/src/Bar/BarGroup.php b/src/Bar/BarGroup.php new file mode 100644 index 0000000..12be59c --- /dev/null +++ b/src/Bar/BarGroup.php @@ -0,0 +1,102 @@ +radius)) { + return; + } + + foreach ($this->bars as $bar) { + if (is_null($bar->radius)) { + $bar->radius = $this->radius; + } + } + } + + public function maxValue(): float + { + if (count($this->bars) === 0) { + return 0; + } + + return max(array_map(fn (BarContract $bar) => $bar->value(), $this->bars)); + } + + public function minValue(): float + { + if (count($this->bars) === 0) { + return 0; + } + + return min(array_map(fn (BarContract $bar) => $bar->value(), $this->bars)); + } + + public function render(Chart $chart, float $x, float $maxGroupWidth): string + { + $numBars = count($this->bars); + $barWidth = 0; + + if ($numBars > 0) { + $barWidth = min($maxGroupWidth, $this->width) / $numBars; + } + + $labelX = $x + ($maxGroupWidth / 2); + $startX = $x; + $x += ($maxGroupWidth / 2) - (($barWidth + $this->margin) * $numBars / 2); + + return new Fragment([ + new Line( + x1: $startX, + y1: $chart->bottom(), + x2: $startX, + y2: $chart->bottom() + 10, + ), + ...array_map(function (BarContract $bar) use ($barWidth, &$x, $chart) { + $svg = $bar->render($chart, $x + $this->margin, $barWidth); + $x += $this->margin + $barWidth; + + return $svg; + }, $this->bars), + new Line( + x1: $startX + $maxGroupWidth, + y1: $chart->bottom(), + x2: $startX + $maxGroupWidth, + y2: $chart->bottom() + 10, + ), + new Text( + content: $this->name, + x: $labelX, + y: $chart->bottom() + $this->labelMarginY, + fontFamily: $chart->fontFamily, + fontSize: $this->fontSize ?? $chart->fontSize, + fill: $this->labelColor, + textAnchor: 'middle' + ), + ]); + } + + public function value(): float + { + return $this->maxValue(); + } +} diff --git a/src/SVG/Fragment.php b/src/SVG/Fragment.php index 2387e0c..a2e5017 100644 --- a/src/SVG/Fragment.php +++ b/src/SVG/Fragment.php @@ -7,7 +7,7 @@ readonly class Fragment implements Stringable { /** - * @param string[] $children + * @param (string|null)[] $children */ public function __construct(public array $children) { @@ -16,6 +16,6 @@ public function __construct(public array $children) public function __toString(): string { - return implode(PHP_EOL, $this->children); + return implode(PHP_EOL, array_filter($this->children, fn (?string $item) => $item !== null)); } } diff --git a/tests/Unit/GroupedBarChartTest.php b/tests/Unit/GroupedBarChartTest.php new file mode 100644 index 0000000..baf8071 --- /dev/null +++ b/tests/Unit/GroupedBarChartTest.php @@ -0,0 +1,234 @@ +render(); + + expect(pretty($chart->render()))->toBe(<<<'SVG' + + + + + 0 + 200 + 400 + 600 + 800 + 1,000 + + + + + + + + + + + + 101 + + + 251 + + + 389 + + + 457 + + + 601 + + + January + + + 73 + + + 223 + + + 347 + + + 509 + + + 653 + + + February + + + 113 + + + 281 + + + 401 + + + 541 + + + 733 + + + March + +SVG + ); +}); + +it('renders empty grouped bar chart', function () { + $chart = new Chart( + yAxis: new YAxis( + minValue: 0, + maxValue: 1000, + ), + series: [ + new Bars( + bars: [ + new BarGroup( + name: 'January', + bars: [], + ), + ], + ), + ], + ); + + expect(pretty($chart->render()))->toBe(<<<'SVG' + + + + + 0 + 200 + 400 + 600 + 800 + 1,000 + + + + + + + + + + + + January + +SVG + ); +});