1<?php 2 3namespace Davaxi; 4 5use Davaxi\Sparkline\DataTrait; 6use Davaxi\Sparkline\FormatTrait; 7use Davaxi\Sparkline\Picture; 8use Davaxi\Sparkline\PointTrait; 9use Davaxi\Sparkline\StyleTrait; 10use InvalidArgumentException; 11 12/** 13 * Class Sparkline. 14 */ 15class Sparkline 16{ 17 use StyleTrait; 18 use DataTrait; 19 use FormatTrait; 20 use PointTrait; 21 22 const MIN_DATA_LENGTH = 2; 23 const FORMAT_DIMENSION = 2; 24 const HEXADECIMAL_ALIAS_LENGTH = 3; 25 const CSS_PADDING_ONE = 1; 26 const CSS_PADDING_TWO = 2; 27 const CSS_PADDING_THREE = 3; 28 const CSS_PADDING = 4; 29 30 /** 31 * @var string 32 * ex: QUERY_STRING if dedicated url 33 */ 34 protected $eTag; 35 36 /** 37 * @var int 38 */ 39 protected $expire; 40 41 /** 42 * @var string 43 */ 44 protected $filename = 'sparkline'; 45 46 /** 47 * @var resource 48 */ 49 protected $file; 50 51 /** 52 * @var array 53 */ 54 protected $server = []; 55 56 /** 57 * Sparkline constructor. 58 * 59 * @codeCoverageIgnore 60 */ 61 public function __construct() 62 { 63 if (!extension_loaded('gd')) { 64 throw new InvalidArgumentException('GD extension is not installed'); 65 } 66 } 67 68 /** 69 * @param string|null $eTag 70 */ 71 public function setETag($eTag) 72 { 73 if (null === $eTag) { 74 $this->eTag = null; 75 76 return; 77 } 78 $this->eTag = md5($eTag); 79 } 80 81 /** 82 * @param string $filename 83 * Without extension 84 */ 85 public function setFilename(string $filename) 86 { 87 $this->filename = $filename; 88 } 89 90 /** 91 * @param string|int $expire 92 * time format or string format 93 */ 94 public function setExpire($expire) 95 { 96 if (null === $expire) { 97 $this->expire = null; 98 99 return; 100 } 101 if (is_numeric($expire)) { 102 $this->expire = $expire; 103 104 return; 105 } 106 $this->expire = strtotime($expire); 107 } 108 109 public function generate() 110 { 111 list($width, $height) = $this->getNormalizedSize(); 112 113 $count = $this->getCount(); 114 115 $picture = new Picture($width, $height); 116 $picture->applyBackground($this->backgroundColor); 117 118 $lineThickness = (int)round($this->lineThickness * $this->ratioComputing); 119 $picture->applyThickness($lineThickness); 120 121 $stepCount = $this->getMaxNumberOfDataPointsAcrossSerieses(); 122 123 foreach ($this->data as $seriesIndex => $series) { 124 $seriesNormalized = $this->getNormalizedData($seriesIndex); 125 list($polygon, $line) = $this->getChartElements($seriesNormalized, $stepCount); 126 $picture->applyPolygon($polygon, $this->getFillColor($seriesIndex), $count); 127 $picture->applyLine($line, $this->getLineColor($seriesIndex)); 128 129 foreach ($this->points as $point) { 130 if ($point['series'] != $seriesIndex) { 131 continue; 132 } 133 134 $isFirst = $point['index'] === 0; 135 $lineIndex = $isFirst ? 0 : $point['index'] - 1; 136 $picture->applyDot( 137 $line[$lineIndex][$isFirst ? 0 : 2], 138 $line[$lineIndex][$isFirst ? 1 : 3], 139 $point['radius'] * $this->ratioComputing, 140 $point['color'] 141 ); 142 } 143 } 144 145 $this->file = $picture->generate($this->width, $this->height); 146 } 147 148 /** 149 * @param array $server 150 */ 151 public function setServer(array $server) 152 { 153 $this->server = $server; 154 } 155 156 /** 157 * @param string $key 158 * 159 * @return mixed|null 160 */ 161 public function getServerValue(string $key) 162 { 163 if (isset($this->server[$key])) { 164 return $this->server[$key]; 165 } 166 167 return null; 168 } 169 170 /** 171 * @return bool 172 */ 173 protected function checkNoModified(): bool 174 { 175 $httpIfNoneMatch = $this->getServerValue('HTTP_IF_NONE_MATCH'); 176 if ($this->eTag && $httpIfNoneMatch) { 177 if ($httpIfNoneMatch === $this->eTag) { 178 $serverProtocol = $this->getServerValue('SERVER_PROTOCOL'); 179 header($serverProtocol . ' 304 Not Modified', true, 304); 180 181 return true; 182 } 183 } 184 185 return false; 186 } 187 188 public function display() 189 { 190 if (!$this->file) { 191 $this->generate(); 192 } 193 194 if ($this->checkNoModified()) { 195 return; 196 } 197 198 header('Content-Type: image/png'); 199 header('Content-Disposition: inline; filename="' . $this->filename . '.png"'); 200 header('Accept-Ranges: none'); 201 if ($this->eTag) { 202 header('ETag: ' . $this->eTag); 203 } 204 if (null !== $this->expire) { 205 header('Expires: ' . gmdate('D, d M Y H:i:s T', $this->expire)); 206 } 207 imagepng($this->file); 208 } 209 210 /** 211 * @param string $savePath 212 */ 213 public function save(string $savePath) 214 { 215 if (!$this->file) { 216 $this->generate(); 217 } 218 imagepng($this->file, $savePath); 219 } 220 221 /** 222 * @return string 223 */ 224 public function toBase64(): string 225 { 226 if (!$this->file) { 227 $this->generate(); 228 } 229 ob_start(); 230 imagepng($this->file); 231 $buffer = ob_get_contents(); 232 if (ob_get_length()) { 233 ob_end_clean(); 234 } 235 236 return base64_encode($buffer); 237 } 238 239 public function destroy() 240 { 241 if ($this->file) { 242 imagedestroy($this->file); 243 } 244 $this->file = null; 245 } 246} 247