PageRenderTime 50ms CodeModel.GetById 20ms RepoModel.GetById 1ms app.codeStats 0ms

/plugins/ImageGraph/StaticGraph/GridGraph.php

https://github.com/CodeYellowBV/piwik
PHP | 485 lines | 322 code | 62 blank | 101 comment | 37 complexity | 3d510df7b96ba36852c11f7fd720ee74 MD5 | raw file
Possible License(s): LGPL-3.0, JSON, MIT, GPL-3.0, LGPL-2.1, GPL-2.0, AGPL-1.0, BSD-2-Clause, BSD-3-Clause
  1. <?php
  2. /**
  3. * Piwik - free/libre analytics platform
  4. *
  5. * @link http://piwik.org
  6. * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
  7. *
  8. */
  9. namespace Piwik\Plugins\ImageGraph\StaticGraph;
  10. use Piwik\Plugins\ImageGraph\StaticGraph;
  11. /**
  12. *
  13. */
  14. abstract class GridGraph extends StaticGraph
  15. {
  16. const GRAPHIC_COLOR_KEY = 'GRAPHIC_COLOR';
  17. const TRUNCATION_TEXT = '...';
  18. const DEFAULT_TICK_ALPHA = 20;
  19. const DEFAULT_SERIE_WEIGHT = 0.5;
  20. const LEFT_GRID_MARGIN = 4;
  21. const BOTTOM_GRID_MARGIN = 10;
  22. const TOP_GRID_MARGIN_HORIZONTAL_GRAPH = 1;
  23. const RIGHT_GRID_MARGIN_HORIZONTAL_GRAPH = 4;
  24. const OUTER_TICK_WIDTH = 5;
  25. const INNER_TICK_WIDTH = 0;
  26. const LABEL_SPACE_VERTICAL_GRAPH = 7;
  27. const HORIZONTAL_LEGEND_TOP_MARGIN = 5;
  28. const HORIZONTAL_LEGEND_LEFT_MARGIN = 10;
  29. const HORIZONTAL_LEGEND_BOTTOM_MARGIN = 10;
  30. const VERTICAL_LEGEND_TOP_MARGIN = 8;
  31. const VERTICAL_LEGEND_LEFT_MARGIN = 6;
  32. const VERTICAL_LEGEND_MAX_WIDTH_PCT = 0.70;
  33. const LEGEND_LINE_BULLET_WIDTH = 14;
  34. const LEGEND_BOX_BULLET_WIDTH = 5;
  35. const LEGEND_BULLET_RIGHT_PADDING = 5;
  36. const LEGEND_ITEM_HORIZONTAL_INTERSTICE = 6;
  37. const LEGEND_ITEM_VERTICAL_INTERSTICE_OFFSET = 4;
  38. const LEGEND_SHADOW_OPACITY = 25;
  39. const LEGEND_VERTICAL_SHADOW_PADDING = 3;
  40. const LEGEND_HORIZONTAL_SHADOW_PADDING = 2;
  41. const PCHART_HARD_CODED_VERTICAL_LEGEND_INTERSTICE = 5;
  42. protected function getDefaultColors()
  43. {
  44. return array(
  45. self::GRAPHIC_COLOR_KEY . '1' => '5170AE',
  46. self::GRAPHIC_COLOR_KEY . '2' => 'F29007',
  47. self::GRAPHIC_COLOR_KEY . '3' => 'CC3399',
  48. self::GRAPHIC_COLOR_KEY . '4' => '9933CC',
  49. self::GRAPHIC_COLOR_KEY . '5' => '80A033',
  50. self::GRAPHIC_COLOR_KEY . '6' => '246AD2'
  51. );
  52. }
  53. protected function initGridChart(
  54. $displayVerticalGridLines,
  55. $bulletType,
  56. $horizontalGraph,
  57. $showTicks,
  58. $verticalLegend
  59. )
  60. {
  61. $this->initpData();
  62. $colorIndex = 1;
  63. foreach ($this->ordinateSeries as $column => $data) {
  64. $this->pData->setSerieWeight($column, self::DEFAULT_SERIE_WEIGHT);
  65. $graphicColor = $this->colors[self::GRAPHIC_COLOR_KEY . $colorIndex++];
  66. $this->pData->setPalette($column, $graphicColor);
  67. }
  68. $this->initpImage();
  69. // graph area coordinates
  70. $topLeftXValue = $this->getGridLeftMargin($horizontalGraph, $withLabel = true);
  71. $topLeftYValue = $this->getGridTopMargin($horizontalGraph, $verticalLegend);
  72. $bottomRightXValue = $this->width - $this->getGridRightMargin($horizontalGraph);
  73. $bottomRightYValue = $this->getGraphBottom($horizontalGraph);
  74. $this->drawBackground();
  75. $this->pImage->setGraphArea(
  76. $topLeftXValue,
  77. $topLeftYValue,
  78. $bottomRightXValue,
  79. $bottomRightYValue
  80. );
  81. // determine how many labels need to be skipped
  82. $skippedLabels = 0;
  83. if (!$horizontalGraph) {
  84. list($abscissaMaxWidth, $abscissaMaxHeight) = $this->getMaximumTextWidthHeight($this->abscissaSeries);
  85. $graphWidth = $bottomRightXValue - $topLeftXValue;
  86. $maxNumOfLabels = floor($graphWidth / ($abscissaMaxWidth + self::LABEL_SPACE_VERTICAL_GRAPH));
  87. $abscissaSeriesCount = count($this->abscissaSeries);
  88. if ($maxNumOfLabels < $abscissaSeriesCount) {
  89. for ($candidateSkippedLabels = 1; $candidateSkippedLabels < $abscissaSeriesCount; $candidateSkippedLabels++) {
  90. $numberOfSegments = $abscissaSeriesCount / ($candidateSkippedLabels + 1);
  91. $numberOfCompleteSegments = floor($numberOfSegments);
  92. $numberOfLabels = $numberOfCompleteSegments;
  93. if ($numberOfSegments > $numberOfCompleteSegments) {
  94. $numberOfLabels++;
  95. }
  96. if ($numberOfLabels <= $maxNumOfLabels) {
  97. $skippedLabels = $candidateSkippedLabels;
  98. break;
  99. }
  100. }
  101. }
  102. if ($this->forceSkippedLabels
  103. && $skippedLabels
  104. && $skippedLabels < $this->forceSkippedLabels
  105. && $abscissaSeriesCount > $this->forceSkippedLabels + 1
  106. ) {
  107. $skippedLabels = $this->forceSkippedLabels;
  108. }
  109. }
  110. $ordinateAxisLength =
  111. $horizontalGraph ? $bottomRightXValue - $topLeftXValue : $this->getGraphHeight($horizontalGraph, $verticalLegend);
  112. $maxOrdinateValue = 0;
  113. foreach ($this->ordinateSeries as $column => $data) {
  114. $currentMax = $this->pData->getMax($column);
  115. if ($currentMax > $maxOrdinateValue) {
  116. $maxOrdinateValue = $currentMax;
  117. }
  118. }
  119. // rounding top scale value to the next multiple of 10
  120. if ($maxOrdinateValue > 10) {
  121. $modTen = $maxOrdinateValue % 10;
  122. if ($modTen) $maxOrdinateValue += 10 - $modTen;
  123. }
  124. $gridColor = $this->gridColor;
  125. $this->pImage->drawScale(
  126. array(
  127. 'Mode' => SCALE_MODE_MANUAL,
  128. 'GridTicks' => 0,
  129. 'LabelSkip' => $skippedLabels,
  130. 'DrawXLines' => $displayVerticalGridLines,
  131. 'Factors' => array(ceil($maxOrdinateValue / 2)),
  132. 'MinDivHeight' => $ordinateAxisLength / 2,
  133. 'AxisAlpha' => 0,
  134. 'SkippedAxisAlpha' => 0,
  135. 'TickAlpha' => $showTicks ? self::DEFAULT_TICK_ALPHA : 0,
  136. 'InnerTickWidth' => self::INNER_TICK_WIDTH,
  137. 'OuterTickWidth' => self::OUTER_TICK_WIDTH,
  138. 'GridR' => $gridColor['R'],
  139. 'GridG' => $gridColor['G'],
  140. 'GridB' => $gridColor['B'],
  141. 'GridAlpha' => 100,
  142. 'ManualScale' => array(
  143. 0 => array(
  144. 'Min' => 0,
  145. 'Max' => $maxOrdinateValue
  146. )
  147. ),
  148. 'Pos' => $horizontalGraph ? SCALE_POS_TOPBOTTOM : SCALE_POS_LEFTRIGHT,
  149. )
  150. );
  151. if ($this->showLegend) {
  152. switch ($bulletType) {
  153. case LEGEND_FAMILY_LINE:
  154. $bulletWidth = self::LEGEND_LINE_BULLET_WIDTH;
  155. // measured using a picture editing software
  156. $iconOffsetAboveLabelSymmetryAxis = -2;
  157. break;
  158. case LEGEND_FAMILY_BOX:
  159. $bulletWidth = self::LEGEND_BOX_BULLET_WIDTH;
  160. // measured using a picture editing software
  161. $iconOffsetAboveLabelSymmetryAxis = 3;
  162. break;
  163. }
  164. // pChart requires two coordinates to draw the legend $legendTopLeftXValue & $legendTopLeftYValue
  165. // $legendTopLeftXValue = legend's left padding
  166. $legendTopLeftXValue = $topLeftXValue + ($verticalLegend ? self::VERTICAL_LEGEND_LEFT_MARGIN : self::HORIZONTAL_LEGEND_LEFT_MARGIN);
  167. // $legendTopLeftYValue = y coordinate of the top edge of the legend's icons
  168. // Caution :
  169. // - pChart will silently add some value (see $paddingAddedByPChart) to $legendTopLeftYValue depending on multiple criterias
  170. // - pChart will not take into account the size of the text. Setting $legendTopLeftYValue = 0 will crop the legend's labels
  171. // The following section of code determines the value of $legendTopLeftYValue while taking into account the following paremeters :
  172. // - whether legend items have icons
  173. // - whether icons are bigger than the legend's labels
  174. // - how much colored shadow padding is required
  175. list($maxLogoWidth, $maxLogoHeight) = self::getMaxLogoSize(array_values($this->ordinateLogos));
  176. if ($maxLogoHeight >= $this->legendFontSize) {
  177. $heightOfTextAboveBulletTop = 0;
  178. $paddingCreatedByLogo = $maxLogoHeight - $this->legendFontSize;
  179. $effectiveShadowPadding = $paddingCreatedByLogo < self::LEGEND_VERTICAL_SHADOW_PADDING * 2 ? self::LEGEND_VERTICAL_SHADOW_PADDING - ($paddingCreatedByLogo / 2) : 0;
  180. } else {
  181. if ($maxLogoHeight) {
  182. // measured using a picture editing software
  183. $iconOffsetAboveLabelSymmetryAxis = 5;
  184. }
  185. $heightOfTextAboveBulletTop = $this->legendFontSize / 2 - $iconOffsetAboveLabelSymmetryAxis;
  186. $effectiveShadowPadding = self::LEGEND_VERTICAL_SHADOW_PADDING;
  187. }
  188. $effectiveLegendItemVerticalInterstice = $this->legendFontSize + self::LEGEND_ITEM_VERTICAL_INTERSTICE_OFFSET;
  189. $effectiveLegendItemHorizontalInterstice = self::LEGEND_ITEM_HORIZONTAL_INTERSTICE + self::LEGEND_HORIZONTAL_SHADOW_PADDING;
  190. $legendTopMargin = $verticalLegend ? self::VERTICAL_LEGEND_TOP_MARGIN : self::HORIZONTAL_LEGEND_TOP_MARGIN;
  191. $requiredPaddingAboveItemBullet = $legendTopMargin + $heightOfTextAboveBulletTop + $effectiveShadowPadding;
  192. $paddingAddedByPChart = 0;
  193. if ($verticalLegend) {
  194. if ($maxLogoHeight) {
  195. // see line 1691 of pDraw.class.php
  196. if ($maxLogoHeight < $effectiveLegendItemVerticalInterstice) {
  197. $paddingAddedByPChart = ($effectiveLegendItemVerticalInterstice / 2) - ($maxLogoHeight / 2);
  198. }
  199. } else {
  200. // see line 1711 of pDraw.class.php ($Y+$IconAreaHeight/2)
  201. $paddingAddedByPChart = $effectiveLegendItemVerticalInterstice / 2;
  202. }
  203. }
  204. $legendTopLeftYValue = $paddingAddedByPChart < $requiredPaddingAboveItemBullet ? $requiredPaddingAboveItemBullet - $paddingAddedByPChart : 0;
  205. // add colored background to each legend item
  206. if (count($this->ordinateLabels) > 1) {
  207. $currentPosition = $verticalLegend ? $legendTopMargin : $legendTopLeftXValue;
  208. $colorIndex = 1;
  209. foreach ($this->ordinateLabels as $metricCode => &$label) {
  210. $color = $this->colors[self::GRAPHIC_COLOR_KEY . $colorIndex++];
  211. $paddedBulletWidth = $bulletWidth;
  212. if (isset($this->ordinateLogos[$metricCode])) {
  213. $paddedBulletWidth = $maxLogoWidth;
  214. }
  215. $paddedBulletWidth += self::LEGEND_BULLET_RIGHT_PADDING;
  216. // truncate labels if required
  217. if ($verticalLegend) {
  218. $label = $this->truncateLabel($label, ($this->width * self::VERTICAL_LEGEND_MAX_WIDTH_PCT) - $legendTopLeftXValue - $paddedBulletWidth, $this->legendFontSize);
  219. $this->pData->setSerieDescription($metricCode, $label);
  220. }
  221. $rectangleTopLeftXValue = ($verticalLegend ? $legendTopLeftXValue : $currentPosition) + $paddedBulletWidth - self::LEGEND_HORIZONTAL_SHADOW_PADDING;
  222. $rectangleTopLeftYValue = $verticalLegend ? $currentPosition : $legendTopMargin;
  223. list($labelWidth, $labelHeight) = $this->getTextWidthHeight($label, $this->legendFontSize);
  224. $legendItemWidth = $paddedBulletWidth + $labelWidth + $effectiveLegendItemHorizontalInterstice;
  225. $rectangleBottomRightXValue = $rectangleTopLeftXValue + $labelWidth + (self::LEGEND_HORIZONTAL_SHADOW_PADDING * 2);
  226. $legendItemHeight = max($maxLogoHeight, $this->legendFontSize) + ($effectiveShadowPadding * 2);
  227. $rectangleBottomRightYValue = $rectangleTopLeftYValue + $legendItemHeight;
  228. $this->pImage->drawFilledRectangle(
  229. $rectangleTopLeftXValue,
  230. $rectangleTopLeftYValue,
  231. $rectangleBottomRightXValue,
  232. $rectangleBottomRightYValue,
  233. array(
  234. 'Alpha' => self::LEGEND_SHADOW_OPACITY,
  235. 'R' => $color['R'],
  236. 'G' => $color['G'],
  237. 'B' => $color['B'],
  238. )
  239. );
  240. if ($verticalLegend) {
  241. $currentPositionIncrement = max($maxLogoHeight, $effectiveLegendItemVerticalInterstice, $this->legendFontSize) + self::PCHART_HARD_CODED_VERTICAL_LEGEND_INTERSTICE;
  242. } else {
  243. $currentPositionIncrement = $legendItemWidth;
  244. }
  245. $currentPosition += $currentPositionIncrement;
  246. }
  247. }
  248. // draw legend
  249. $legendColor = $this->textColor;
  250. $this->pImage->drawLegend(
  251. $legendTopLeftXValue,
  252. $legendTopLeftYValue,
  253. array(
  254. 'Style' => LEGEND_NOBORDER,
  255. 'FontSize' => $this->legendFontSize,
  256. 'BoxWidth' => $bulletWidth,
  257. 'XSpacing' => $effectiveLegendItemHorizontalInterstice, // not effective when vertical
  258. 'Mode' => $verticalLegend ? LEGEND_VERTICAL : LEGEND_HORIZONTAL,
  259. 'BoxHeight' => $verticalLegend ? $effectiveLegendItemVerticalInterstice : null,
  260. 'Family' => $bulletType,
  261. 'FontR' => $legendColor['R'],
  262. 'FontG' => $legendColor['G'],
  263. 'FontB' => $legendColor['B'],
  264. )
  265. );
  266. }
  267. }
  268. protected static function getMaxLogoSize($logoPaths)
  269. {
  270. $maxLogoWidth = 0;
  271. $maxLogoHeight = 0;
  272. foreach ($logoPaths as $logoPath) {
  273. list($logoWidth, $logoHeight) = self::getLogoSize($logoPath);
  274. if ($logoWidth > $maxLogoWidth) {
  275. $maxLogoWidth = $logoWidth;
  276. }
  277. if ($logoHeight > $maxLogoHeight) {
  278. $maxLogoHeight = $logoHeight;
  279. }
  280. }
  281. return array($maxLogoWidth, $maxLogoHeight);
  282. }
  283. protected static function getLogoSize($logoPath)
  284. {
  285. $pathInfo = getimagesize($logoPath);
  286. return array($pathInfo[0], $pathInfo[1]);
  287. }
  288. protected function getGridLeftMargin($horizontalGraph, $withLabel)
  289. {
  290. $gridLeftMargin = self::LEFT_GRID_MARGIN + self::OUTER_TICK_WIDTH;
  291. if ($withLabel) {
  292. list($maxTextWidth, $maxTextHeight) = $this->getMaximumTextWidthHeight($horizontalGraph ? $this->abscissaSeries : $this->ordinateSeries);
  293. $gridLeftMargin += $maxTextWidth;
  294. }
  295. return $gridLeftMargin;
  296. }
  297. protected function getGridTopMargin($horizontalGraph, $verticalLegend)
  298. {
  299. list($ordinateMaxWidth, $ordinateMaxHeight) = $this->getMaximumTextWidthHeight($this->ordinateSeries);
  300. if ($horizontalGraph) {
  301. $topMargin = $ordinateMaxHeight + self::TOP_GRID_MARGIN_HORIZONTAL_GRAPH + self::OUTER_TICK_WIDTH;
  302. } else {
  303. $topMargin = $ordinateMaxHeight / 2;
  304. }
  305. if ($this->showLegend && !$verticalLegend) {
  306. $topMargin += $this->getHorizontalLegendHeight();
  307. }
  308. return $topMargin;
  309. }
  310. private function getHorizontalLegendHeight()
  311. {
  312. list($maxMetricLegendWidth, $maxMetricLegendHeight) =
  313. $this->getMaximumTextWidthHeight(array_values($this->ordinateLabels), $this->legendFontSize);
  314. return $maxMetricLegendHeight + self::HORIZONTAL_LEGEND_BOTTOM_MARGIN + self::HORIZONTAL_LEGEND_TOP_MARGIN;
  315. }
  316. protected function getGraphHeight($horizontalGraph, $verticalLegend)
  317. {
  318. return $this->getGraphBottom($horizontalGraph) - $this->getGridTopMargin($horizontalGraph, $verticalLegend);
  319. }
  320. private function getGridBottomMargin($horizontalGraph)
  321. {
  322. $gridBottomMargin = self::BOTTOM_GRID_MARGIN;
  323. if (!$horizontalGraph) {
  324. list($abscissaMaxWidth, $abscissaMaxHeight) = $this->getMaximumTextWidthHeight($this->abscissaSeries);
  325. $gridBottomMargin += $abscissaMaxHeight;
  326. }
  327. return $gridBottomMargin;
  328. }
  329. protected function getGridRightMargin($horizontalGraph)
  330. {
  331. if ($horizontalGraph) {
  332. // in horizontal graphs, metric values are displayed on the far right of the bar
  333. list($ordinateMaxWidth, $ordinateMaxHeight) = $this->getMaximumTextWidthHeight($this->ordinateSeries);
  334. return self::RIGHT_GRID_MARGIN_HORIZONTAL_GRAPH + $ordinateMaxWidth;
  335. } else {
  336. return 0;
  337. }
  338. }
  339. protected function getGraphBottom($horizontalGraph)
  340. {
  341. return $this->height - $this->getGridBottomMargin($horizontalGraph);
  342. }
  343. protected function truncateLabel($label, $labelWidthLimit, $fontSize = false)
  344. {
  345. list($truncationTextWidth, $truncationTextHeight) = $this->getTextWidthHeight(self::TRUNCATION_TEXT, $fontSize);
  346. list($labelWidth, $labelHeight) = $this->getTextWidthHeight($label, $fontSize);
  347. if ($labelWidth > $labelWidthLimit) {
  348. $averageCharWidth = $labelWidth / strlen($label);
  349. $charsToKeep = floor(($labelWidthLimit - $truncationTextWidth) / $averageCharWidth);
  350. $label = substr($label, 0, $charsToKeep) . self::TRUNCATION_TEXT;
  351. }
  352. return $label;
  353. }
  354. // display min & max values
  355. // can not currently be used because pChart's label design is not flexible enough
  356. // e.g: it is not possible to remove the box border & the square icon
  357. // it would require modifying pChart code base which we try to avoid
  358. // see http://dev.piwik.org/trac/ticket/3396
  359. // protected function displayMinMaxValues()
  360. // {
  361. // if($displayMinMax)
  362. // {
  363. // // when plotting multiple metrics, display min & max on both series
  364. // // to fix: in vertical bars, labels are hidden when multiple metrics are plotted, hence the restriction on count($this->ordinateSeries) == 1
  365. // if($this->multipleMetrics && count($this->ordinateSeries) == 1)
  366. // {
  367. // $colorIndex = 1;
  368. // foreach($this->ordinateSeries as $column => $data)
  369. // {
  370. // $color = $this->colors[self::GRAPHIC_COLOR_KEY . $colorIndex++];
  371. //
  372. // $this->pImage->writeLabel(
  373. // $column,
  374. // self::locateMinMaxValue($data),
  375. // $Format = array(
  376. // 'NoTitle' => true,
  377. // 'DrawPoint' => false,
  378. // 'DrawSerieColor' => true,
  379. // 'TitleMode' => LABEL_TITLE_NOBACKGROUND,
  380. // 'GradientStartR' => $color['R'],
  381. // 'GradientStartG' => $color['G'],
  382. // 'GradientStartB' => $color['B'],
  383. // 'GradientEndR' => 255,
  384. // 'GradientEndG' => 255,
  385. // 'GradientEndB' => 255,
  386. // 'BoxWidth' => 0,
  387. // 'VerticalMargin' => 9,
  388. // 'HorizontalMargin' => 7,
  389. // )
  390. // );
  391. // }
  392. // }
  393. // else
  394. // {
  395. // // display only one min & max label
  396. // }
  397. // }
  398. // }
  399. // protected static function locateMinMaxValue($data)
  400. // {
  401. // $firstValue = $data[0];
  402. // $minValue = $firstValue;
  403. // $minValueIndex = 0;
  404. // $maxValue = $firstValue;
  405. // $maxValueIndex = 0;
  406. // foreach($data as $index => $value)
  407. // {
  408. // if($value > $maxValue)
  409. // {
  410. // $maxValue = $value;
  411. // $maxValueIndex = $index;
  412. // }
  413. //
  414. // if($value < $minValue)
  415. // {
  416. // $minValue = $value;
  417. // $minValueIndex = $index;
  418. // }
  419. // }
  420. //
  421. // return array($minValueIndex, $maxValueIndex);
  422. // }
  423. }