From e369e5d1d6bf7a13d3f77eeedf10a860952eaa73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Sun, 22 Jan 2023 17:01:04 +0100 Subject: [PATCH] Interpolate colors in ColorRating table column decorator on runtime (#1975) --- demos/collection/tablecolumns.php | 1 - src/Card.php | 1 - src/HtmlTemplate/TagTree.php | 1 - src/Table/Column/ColorRating.php | 140 +++++++++------------------ src/Table/Column/Labels.php | 10 +- tests/TableColumnColorRatingTest.php | 28 +----- 6 files changed, 54 insertions(+), 127 deletions(-) diff --git a/demos/collection/tablecolumns.php b/demos/collection/tablecolumns.php index f7f021320b..e5790d9e36 100644 --- a/demos/collection/tablecolumns.php +++ b/demos/collection/tablecolumns.php @@ -90,7 +90,6 @@ protected function init(): void [ 'min' => 1, 'max' => 3, - 'steps' => 3, 'colors' => [ '#FF0000', '#FFFF00', diff --git a/src/Card.php b/src/Card.php index d1a0e98ec5..9f64645105 100644 --- a/src/Card.php +++ b/src/Card.php @@ -6,7 +6,6 @@ use Atk4\Core\Factory; use Atk4\Data\Model; -use Atk4\Ui\Js\Jquery; use Atk4\Ui\UserAction\ExecutorFactory; /** diff --git a/src/HtmlTemplate/TagTree.php b/src/HtmlTemplate/TagTree.php index 86c1be2e6c..d4a44bdfb0 100644 --- a/src/HtmlTemplate/TagTree.php +++ b/src/HtmlTemplate/TagTree.php @@ -39,7 +39,6 @@ private function __clone() public function clone(HtmlTemplate $newParentTemplate): self { $res = new static($newParentTemplate, $this->tag); - $res->children = []; foreach ($this->children as $k => $v) { $res->children[$k] = is_string($v) ? $v : clone $v; } diff --git a/src/Table/Column/ColorRating.php b/src/Table/Column/ColorRating.php index 670f4fc12c..6d2d733387 100644 --- a/src/Table/Column/ColorRating.php +++ b/src/Table/Column/ColorRating.php @@ -14,7 +14,6 @@ * [ColorRating::class, [ * 'min' => 1, * 'max' => 3, - * 'steps' => 3, * 'colors' => [ * '#FF0000', * '#FFFF00', @@ -26,18 +25,12 @@ class ColorRating extends Table\Column { /** @var float Minimum value of the gradient. */ public $min; - /** @var float Maximum value of the gradient. */ public $max; - /** @var int Step to be calculated between colors, must be greater than 1. */ - public $steps = 1; - /** @var array Hex colors ['#FF0000', '#00FF00'] from red to green. */ + /** @var array Hex colors. */ public $colors = ['#FF0000', '#00FF00']; - /** @var array Store the generated Hex color based on the number of steps. */ - protected $gradients = []; - /** @var bool Define if values lesser than min have no color. */ public $lessThanMinNoColor = false; @@ -52,78 +45,8 @@ protected function init(): void throw new Exception('Min must be lower than Max'); } - if ($this->steps === 0) { - throw new Exception('Step must be at least 1'); - } - if (count($this->colors) < 2) { - throw new Exception('Colors must be more than 1'); - } - - $this->createGradients(); - } - - private function createGradients(): void - { - $colorFrom = ''; - - foreach ($this->colors as $i => $color) { - // skip first - if ($i === 0) { - $colorFrom = $color; - - continue; - } - - // if already add remove last - // because on first iteraction of ->createGradientSingle - // will create a duplicate - if (count($this->gradients) > 0) { - array_pop($this->gradients); - } - - $this->createGradientSingle($this->gradients, $colorFrom, $color, $this->steps + 1); - $colorFrom = $color; - } - } - - private function createGradientSingle(array &$gradients, string $hexFrom, string $hexTo, int $steps): void - { - $hexFrom = ltrim($hexFrom, '#'); - $hexTo = ltrim($hexTo, '#'); - - $fromRgb = [ - 'r' => hexdec(substr($hexFrom, 0, 2)), - 'g' => hexdec(substr($hexFrom, 2, 2)), - 'b' => hexdec(substr($hexFrom, 4, 2)), - ]; - - $toRgb = [ - 'r' => hexdec(substr($hexTo, 0, 2)), - 'g' => hexdec(substr($hexTo, 2, 2)), - 'b' => hexdec(substr($hexTo, 4, 2)), - ]; - - $stepRgb = [ - 'r' => ($fromRgb['r'] - $toRgb['r']) / $steps, - 'g' => ($fromRgb['g'] - $toRgb['g']) / $steps, - 'b' => ($fromRgb['b'] - $toRgb['b']) / $steps, - ]; - - for ($i = 0; $i <= $steps; ++$i) { - $rgb = [ - 'r' => floor($fromRgb['r'] - $stepRgb['r'] * $i), - 'g' => floor($fromRgb['g'] - $stepRgb['g'] * $i), - 'b' => floor($fromRgb['b'] - $stepRgb['b'] * $i), - ]; - - $hexRgb = [ - 'r' => sprintf('%02x', $rgb['r']), - 'g' => sprintf('%02x', $rgb['g']), - 'b' => sprintf('%02x', $rgb['b']), - ]; - - $gradients[] = '#' . implode('', $hexRgb); + throw new Exception('At least 2 colors must be set'); } } @@ -147,11 +70,8 @@ public function getDataCellHtml(Field $field = null, array $attr = []): string public function getHtmlTags(Model $row, ?Field $field): array { $value = $field->get($row); - if ($value === null) { - return parent::getHtmlTags($row, $field); - } - $color = $this->getColorFromValue($value); + $color = $value === null ? null : $this->getColorFromValue($value); if ($color === null) { return parent::getHtmlTags($row, $field); @@ -164,20 +84,56 @@ public function getHtmlTags(Model $row, ?Field $field): array private function getColorFromValue(float $value): ?string { - if ($value <= $this->min) { - return $this->lessThanMinNoColor ? null : $this->gradients[0]; + if ($value < $this->min) { + if ($this->lessThanMinNoColor) { + return null; + } + + $value = $this->min; } - if ($value >= $this->max) { - return $this->moreThanMaxNoColor ? null : end($this->gradients); + if ($value > $this->max) { + if ($this->moreThanMaxNoColor) { + return null; + } + + $value = $this->max; } - $gradientsCount = count($this->gradients) - 1; - $refValue = ($value - $this->min) / ($this->max - $this->min); - $refIndex = $gradientsCount * $refValue; + $colorIndex = (count($this->colors) - 1) * ($value - $this->min) / ($this->max - $this->min); + + $color = $this->interpolateColor( + $this->colors[floor($colorIndex)], + $this->colors[ceil($colorIndex)], + $colorIndex - floor($colorIndex) + ); + + return $color; + } + + protected function interpolateColor(string $hexFrom, string $hexTo, float $value): string + { + $hexFrom = ltrim($hexFrom, '#'); + $hexTo = ltrim($hexTo, '#'); + + $fromRgb = [ + 'r' => hexdec(substr($hexFrom, 0, 2)), + 'g' => hexdec(substr($hexFrom, 2, 2)), + 'b' => hexdec(substr($hexFrom, 4, 2)), + ]; + + $toRgb = [ + 'r' => hexdec(substr($hexTo, 0, 2)), + 'g' => hexdec(substr($hexTo, 2, 2)), + 'b' => hexdec(substr($hexTo, 4, 2)), + ]; - $index = (int) floor($refIndex); + $rgb = [ + 'r' => round($fromRgb['r'] + $value * ($toRgb['r'] - $fromRgb['r'])), + 'g' => round($fromRgb['g'] + $value * ($toRgb['g'] - $fromRgb['g'])), + 'b' => round($fromRgb['b'] + $value * ($toRgb['b'] - $fromRgb['b'])), + ]; - return $this->gradients[$index]; + return '#' . sprintf('%02x%02x%02x', $rgb['r'], $rgb['g'], $rgb['b']); } } diff --git a/src/Table/Column/Labels.php b/src/Table/Column/Labels.php index 76878e8bad..a019f49bf1 100644 --- a/src/Table/Column/Labels.php +++ b/src/Table/Column/Labels.php @@ -26,20 +26,16 @@ public function getHtmlTags(Model $row, ?Field $field): array $v = $field->get($row); $v = explode(',', $v ?? ''); - $labels = []; + $labelsHtml = []; foreach ($v as $id) { - $id = trim($id); - // if field values is set, then use titles instead of IDs $id = $values[$id] ?? $id; if ($id !== '') { - $labels[] = $this->getApp()->getTag('div', ['class' => 'ui label'], $id); + $labelsHtml[] = $this->getApp()->getTag('div', ['class' => 'ui label'], $id); } } - $labels = implode('', $labels); - - return [$field->shortName => $labels]; + return [$field->shortName => implode('', $labelsHtml)]; } } diff --git a/tests/TableColumnColorRatingTest.php b/tests/TableColumnColorRatingTest.php index 91bc714eee..339969d18b 100644 --- a/tests/TableColumnColorRatingTest.php +++ b/tests/TableColumnColorRatingTest.php @@ -43,7 +43,6 @@ public function testValueGreaterThanMax(): void $rating = $this->table->addDecorator('rating', [Table\Column\ColorRating::class, [ 'min' => 0, 'max' => 2, - 'steps' => 3, 'colors' => [ '#FF0000', '#FFFF00', @@ -67,7 +66,6 @@ public function testValueGreaterThanMaxNoColor(): void $this->table->addDecorator('rating', [Table\Column\ColorRating::class, [ 'min' => 0, 'max' => 2, - 'steps' => 3, 'colors' => [ '#FF0000', '#FFFF00', @@ -87,7 +85,6 @@ public function testValueLowerThanMin(): void $rating = $this->table->addDecorator('rating', [Table\Column\ColorRating::class, [ 'min' => 4, 'max' => 10, - 'steps' => 3, 'colors' => [ '#FF0000', '#FFFF00', @@ -111,7 +108,6 @@ public function testValueLowerThanMinNoColor(): void $this->table->addDecorator('rating', [Table\Column\ColorRating::class, [ 'min' => 4, 'max' => 10, - 'steps' => 3, 'colors' => [ '#FF0000', '#FFFF00', @@ -126,13 +122,12 @@ public function testValueLowerThanMinNoColor(): void ); } - public function testExceptionMinGreaterThanMax(): void + public function testMinGreaterThanMaxException(): void { $this->expectException(Exception::class); $this->table->addDecorator('rating', [Table\Column\ColorRating::class, [ 'min' => 3, 'max' => 1, - 'steps' => 3, 'colors' => [ '#FF0000', '#FFFF00', @@ -141,13 +136,12 @@ public function testExceptionMinGreaterThanMax(): void ]]); } - public function testExceptionMinEqualsMax(): void + public function testMinEqualsMaxException(): void { $this->expectException(Exception::class); $this->table->addDecorator('rating', [Table\Column\ColorRating::class, [ 'min' => 3, 'max' => 3, - 'steps' => 3, 'colors' => [ '#FF0000', '#FFFF00', @@ -156,28 +150,12 @@ public function testExceptionMinEqualsMax(): void ]]); } - public function testExceptionZeroSteps(): void + public function testLessThan2ColorsException(): void { $this->expectException(Exception::class); $this->table->addDecorator('rating', [Table\Column\ColorRating::class, [ 'min' => 1, 'max' => 3, - 'steps' => 0, - 'colors' => [ - '#FF0000', - '#FFFF00', - '#00FF00', - ], - ]]); - } - - public function testExceptionLessThan2ColorsDefined(): void - { - $this->expectException(Exception::class); - $this->table->addDecorator('rating', [Table\Column\ColorRating::class, [ - 'min' => 1, - 'max' => 3, - 'steps' => 3, 'colors' => [ '#FF0000', ],