1: <?php
2:
3: declare(strict_types=1);
4:
5: namespace Atk4\Ui\Table\Column;
6:
7: use Atk4\Data\Field;
8: use Atk4\Data\Model;
9: use Atk4\Ui\Exception;
10: use Atk4\Ui\Table;
11:
12: /**
13: * Example seed:
14: * [ColorRating::class, [
15: * 'min' => 1,
16: * 'max' => 3,
17: * 'colors' => [
18: * '#FF0000',
19: * '#FFFF00',
20: * '#00FF00'
21: * ]
22: * ]].
23: */
24: class ColorRating extends Table\Column
25: {
26: /** @var float Minimum value of the gradient. */
27: public $min;
28: /** @var float Maximum value of the gradient. */
29: public $max;
30:
31: /** @var array Hex colors. */
32: public $colors = ['#FF0000', '#00FF00'];
33:
34: /** @var bool Define if values lesser than min have no color. */
35: public $lessThanMinNoColor = false;
36:
37: /** @var bool Define if values greater than max have no color. */
38: public $moreThanMaxNoColor = false;
39:
40: #[\Override]
41: protected function init(): void
42: {
43: parent::init();
44:
45: if ($this->min >= $this->max) {
46: throw new Exception('Min must be lower than Max');
47: }
48:
49: if (count($this->colors) < 2) {
50: throw new Exception('At least 2 colors must be set');
51: }
52: }
53:
54: #[\Override]
55: public function getTagAttributes(string $position, array $attr = []): array
56: {
57: $attr['style'] ??= '';
58: $attr['style'] .= '{$_' . $this->shortName . '_color_rating}';
59:
60: return parent::getTagAttributes($position, $attr);
61: }
62:
63: #[\Override]
64: public function getDataCellHtml(Field $field = null, array $attr = []): string
65: {
66: if ($field === null) {
67: throw new Exception('ColorRating can be used only with model field');
68: }
69:
70: return $this->getTag('body', '{$' . $field->shortName . '}', $attr);
71: }
72:
73: #[\Override]
74: public function getHtmlTags(Model $row, ?Field $field): array
75: {
76: $value = $field->get($row);
77:
78: $color = $value === null ? null : $this->getColorFromValue($value);
79:
80: if ($color === null) {
81: return parent::getHtmlTags($row, $field);
82: }
83:
84: return [
85: '_' . $this->shortName . '_color_rating' => 'background-color: ' . $color . ';',
86: ];
87: }
88:
89: private function getColorFromValue(float $value): ?string
90: {
91: if ($value < $this->min) {
92: if ($this->lessThanMinNoColor) {
93: return null;
94: }
95:
96: $value = $this->min;
97: }
98:
99: if ($value > $this->max) {
100: if ($this->moreThanMaxNoColor) {
101: return null;
102: }
103:
104: $value = $this->max;
105: }
106:
107: $colorIndex = (count($this->colors) - 1) * ($value - $this->min) / ($this->max - $this->min);
108:
109: $color = $this->interpolateColor(
110: $this->colors[floor($colorIndex)],
111: $this->colors[ceil($colorIndex)],
112: $colorIndex - floor($colorIndex)
113: );
114:
115: return $color;
116: }
117:
118: protected function interpolateColor(string $hexFrom, string $hexTo, float $value): string
119: {
120: $hexFrom = ltrim($hexFrom, '#');
121: $hexTo = ltrim($hexTo, '#');
122:
123: $fromRgb = [
124: 'r' => hexdec(substr($hexFrom, 0, 2)),
125: 'g' => hexdec(substr($hexFrom, 2, 2)),
126: 'b' => hexdec(substr($hexFrom, 4, 2)),
127: ];
128:
129: $toRgb = [
130: 'r' => hexdec(substr($hexTo, 0, 2)),
131: 'g' => hexdec(substr($hexTo, 2, 2)),
132: 'b' => hexdec(substr($hexTo, 4, 2)),
133: ];
134:
135: $rgb = [
136: 'r' => round($fromRgb['r'] + $value * ($toRgb['r'] - $fromRgb['r'])),
137: 'g' => round($fromRgb['g'] + $value * ($toRgb['g'] - $fromRgb['g'])),
138: 'b' => round($fromRgb['b'] + $value * ($toRgb['b'] - $fromRgb['b'])),
139: ];
140:
141: return '#' . sprintf('%02x%02x%02x', $rgb['r'], $rgb['g'], $rgb['b']);
142: }
143: }
144: