* - $graphType defines the type of graph plotted, accepted values are: 'evolution', 'verticalBar', 'pie' and '3dPie'
* - $colors accepts a comma delimited list of colors that will overwrite the default Matomo colors
* - you can also customize the width, height, font size, metric being plotted (in case the data contains multiple columns/metrics). * * See also How to embed static Image Graphs? for more information. * * @method static \Piwik\Plugins\ImageGraph\API getInstance() */ class API extends \Piwik\Plugin\API { const FILENAME_KEY = 'filename'; const TRUNCATE_KEY = 'truncate'; const WIDTH_KEY = 'width'; const HEIGHT_KEY = 'height'; const MAX_WIDTH = 2048; const MAX_HEIGHT = 2048; private static $DEFAULT_PARAMETERS = array( StaticGraph::GRAPH_TYPE_BASIC_LINE => array( self::FILENAME_KEY => 'BasicLine', self::TRUNCATE_KEY => 6, self::WIDTH_KEY => 1044, self::HEIGHT_KEY => 290, ), StaticGraph::GRAPH_TYPE_VERTICAL_BAR => array( self::FILENAME_KEY => 'BasicBar', self::TRUNCATE_KEY => 6, self::WIDTH_KEY => 1044, self::HEIGHT_KEY => 290, ), StaticGraph::GRAPH_TYPE_HORIZONTAL_BAR => array( self::FILENAME_KEY => 'HorizontalBar', self::TRUNCATE_KEY => null, // horizontal bar graphs are dynamically truncated self::WIDTH_KEY => 800, self::HEIGHT_KEY => 290, ), StaticGraph::GRAPH_TYPE_3D_PIE => array( self::FILENAME_KEY => '3DPie', self::TRUNCATE_KEY => 5, self::WIDTH_KEY => 1044, self::HEIGHT_KEY => 290, ), StaticGraph::GRAPH_TYPE_BASIC_PIE => array( self::FILENAME_KEY => 'BasicPie', self::TRUNCATE_KEY => 5, self::WIDTH_KEY => 1044, self::HEIGHT_KEY => 290, ), ); private static $DEFAULT_GRAPH_TYPE_OVERRIDE = array( 'Referrers_getReferrerType' => array( false // override if !$isMultiplePeriod => StaticGraph::GRAPH_TYPE_HORIZONTAL_BAR, ), ); const GRAPH_OUTPUT_INLINE = 0; const GRAPH_OUTPUT_FILE = 1; const GRAPH_OUTPUT_PHP = 2; const DEFAULT_ORDINATE_METRIC = 'nb_visits'; const FONT_DIR = '/plugins/ImageGraph/fonts/'; const DEFAULT_FONT = 'tahoma.ttf'; const UNICODE_FONT = 'unifont.ttf'; const DEFAULT_FONT_SIZE = 9; const DEFAULT_LEGEND_FONT_SIZE_OFFSET = 2; const DEFAULT_TEXT_COLOR = '222222'; const DEFAULT_BACKGROUND_COLOR = 'FFFFFF'; const DEFAULT_GRID_COLOR = 'CCCCCC'; // number of row evolutions to plot when no labels are specified, can be overridden using &filter_limit const DEFAULT_NB_ROW_EVOLUTIONS = 5; const MAX_NB_ROW_LABELS = 10; public function get( $idSite, $period, $date, $apiModule, $apiAction, $graphType = false, $outputType = API::GRAPH_OUTPUT_INLINE, $columns = false, $labels = false, $showLegend = true, $width = false, $height = false, $fontSize = API::DEFAULT_FONT_SIZE, $legendFontSize = false, $aliasedGraph = true, $idGoal = false, $colors = false, $textColor = API::DEFAULT_TEXT_COLOR, $backgroundColor = API::DEFAULT_BACKGROUND_COLOR, $gridColor = API::DEFAULT_GRID_COLOR, $idSubtable = false, $legendAppendMetric = true, $segment = false, $idDimension = false ) { $idSite = (int) $idSite; Piwik::checkUserHasViewAccess($idSite); // Health check - should we also test for GD2 only? if (!SettingsServer::isGdExtensionEnabled()) { throw new Exception('Error: To create graphs in Matomo, please enable GD php extension (with Freetype support) in php.ini, and restart your web server.'); } $useUnicodeFont = array( 'am', 'ar', 'el', 'fa', 'fi', 'he', 'ja', 'ka', 'ko', 'te', 'th', 'zh-cn', 'zh-tw', ); $languageLoaded = Translate::getLanguageLoaded(); $font = self::getFontPath(self::DEFAULT_FONT); if (in_array($languageLoaded, $useUnicodeFont)) { $unicodeFontPath = self::getFontPath(self::UNICODE_FONT); $font = file_exists($unicodeFontPath) ? $unicodeFontPath : $font; } // save original GET to reset after processing. Important for API-in-API-call $savedGET = $_GET; try { $apiParameters = array(); if (!empty($idGoal)) { $apiParameters = array('idGoal' => $idGoal); } if (!empty($idDimension)) { $apiParameters = array('idDimension' => $idDimension); } // Fetch the metadata for given api-action $parameters = array( 'idSite' => $idSite, 'apiModule' => $apiModule, 'apiAction' => $apiAction, 'apiParameters' => $apiParameters, 'language' => $languageLoaded, 'period' => $period, 'date' => $date, 'hideMetricsDoc' => false, 'showSubtableReports' => true ); $metadata = Request::processRequest('API.getMetadata', $parameters); if (!$metadata) { throw new Exception('Invalid API Module and/or API Action'); } $metadata = $metadata[0]; $reportHasDimension = !empty($metadata['dimension']); $constantRowsCount = !empty($metadata['constantRowsCount']); $isMultiplePeriod = Period::isMultiplePeriod($date, $period); if (!$reportHasDimension && !$isMultiplePeriod) { throw new Exception('The graph cannot be drawn for this combination of \'date\' and \'period\' parameters.'); } if (empty($legendFontSize)) { $legendFontSize = (int)$fontSize + self::DEFAULT_LEGEND_FONT_SIZE_OFFSET; } if (empty($graphType)) { if ($isMultiplePeriod) { $graphType = StaticGraph::GRAPH_TYPE_BASIC_LINE; } else { if ($constantRowsCount) { $graphType = StaticGraph::GRAPH_TYPE_VERTICAL_BAR; } else { $graphType = StaticGraph::GRAPH_TYPE_HORIZONTAL_BAR; } } $reportUniqueId = $metadata['uniqueId']; if (isset(self::$DEFAULT_GRAPH_TYPE_OVERRIDE[$reportUniqueId][$isMultiplePeriod])) { $graphType = self::$DEFAULT_GRAPH_TYPE_OVERRIDE[$reportUniqueId][$isMultiplePeriod]; } } else { $availableGraphTypes = StaticGraph::getAvailableStaticGraphTypes(); if (!in_array($graphType, $availableGraphTypes)) { throw new Exception( Piwik::translate( 'General_ExceptionInvalidStaticGraphType', array($graphType, implode(', ', $availableGraphTypes)) ) ); } } $width = (int)$width; $height = (int)$height; if (empty($width)) { $width = self::$DEFAULT_PARAMETERS[$graphType][self::WIDTH_KEY]; } if (empty($height)) { $height = self::$DEFAULT_PARAMETERS[$graphType][self::HEIGHT_KEY]; } // Cap width and height to a safe amount $width = min($width, self::MAX_WIDTH); $height = min($height, self::MAX_HEIGHT); $reportColumns = array_merge( !empty($metadata['metrics']) ? $metadata['metrics'] : array(), !empty($metadata['processedMetrics']) ? $metadata['processedMetrics'] : array(), !empty($metadata['metricsGoal']) ? $metadata['metricsGoal'] : array(), !empty($metadata['processedMetricsGoal']) ? $metadata['processedMetricsGoal'] : array() ); $ordinateColumns = array(); if (empty($columns)) { $ordinateColumns[] = empty($reportColumns[self::DEFAULT_ORDINATE_METRIC]) ? key($metadata['metrics']) : self::DEFAULT_ORDINATE_METRIC; } else { $ordinateColumns = explode(',', $columns); foreach ($ordinateColumns as $column) { if (empty($reportColumns[$column])) { throw new Exception( Piwik::translate( 'ImageGraph_ColumnOrdinateMissing', array($column, implode(',', array_keys($reportColumns))) ) ); } } } $ordinateLabels = array(); foreach ($ordinateColumns as $column) { $ordinateLabels[$column] = $reportColumns[$column]; } // sort and truncate filters $defaultFilterTruncate = self::$DEFAULT_PARAMETERS[$graphType][self::TRUNCATE_KEY]; switch ($graphType) { case StaticGraph::GRAPH_TYPE_3D_PIE: case StaticGraph::GRAPH_TYPE_BASIC_PIE: if (count($ordinateColumns) > 1) { // CpChart doesn't support multiple series on pie charts throw new Exception("Pie charts do not currently support multiple series"); } $_GET['filter_sort_column'] = reset($ordinateColumns); $this->setFilterTruncate($defaultFilterTruncate); break; case StaticGraph::GRAPH_TYPE_VERTICAL_BAR: case StaticGraph::GRAPH_TYPE_BASIC_LINE: if (!$isMultiplePeriod && !$constantRowsCount) { $this->setFilterTruncate($defaultFilterTruncate); } break; } $ordinateLogos = array(); // row evolutions if ($isMultiplePeriod && $reportHasDimension) { $plottedMetric = reset($ordinateColumns); // when no labels are specified, getRowEvolution returns the top N=filter_limit row evolutions // rows are sorted using filter_sort_column (see DataTableGenericFilter for more info) if (!$labels) { $savedFilterSortColumnValue = Common::getRequestVar('filter_sort_column', ''); $_GET['filter_sort_column'] = $plottedMetric; $savedFilterLimitValue = Common::getRequestVar('filter_limit', -1, 'int'); if ($savedFilterLimitValue == -1 || $savedFilterLimitValue > self::MAX_NB_ROW_LABELS) { $_GET['filter_limit'] = self::DEFAULT_NB_ROW_EVOLUTIONS; } } $parameters = array( 'idSite' => $idSite, 'period' => $period, 'date' => $date, 'apiModule' => $apiModule, 'apiAction' => $apiAction, 'label' => $labels, 'segment' => $segment, 'column' => $plottedMetric, 'language' => $languageLoaded, 'idGoal' => $idGoal, 'idDimension' => $idDimension, 'legendAppendMetric' => $legendAppendMetric, 'labelUseAbsoluteUrl' => false ); $processedReport = Request::processRequest('API.getRowEvolution', $parameters); //@review this test will need to be updated after evaluating the @review comment in API/API.php if (!$processedReport) { throw new Exception(Piwik::translate('General_NoDataForGraph')); } // restoring generic filter parameters if (!$labels) { $_GET['filter_sort_column'] = $savedFilterSortColumnValue; if ($savedFilterLimitValue != -1) { $_GET['filter_limit'] = $savedFilterLimitValue; } } // retrieve metric names & labels $metrics = $processedReport['metadata']['metrics']; $ordinateLabels = array(); // getRowEvolution returned more than one label if (!array_key_exists($plottedMetric, $metrics)) { $ordinateColumns = array(); $i = 0; foreach ($metrics as $metric => $info) { $ordinateColumn = $plottedMetric . '_' . $i++; $ordinateColumns[] = $metric; $ordinateLabels[$ordinateColumn] = $info['name']; if (isset($info['logo'])) { $ordinateLogo = $info['logo']; // @review pChart does not support gifs in graph legends, would it be possible to convert all plugin pictures (cookie.gif, flash.gif, ..) to png files? if (!strstr($ordinateLogo, '.gif')) { $absoluteLogoPath = self::getAbsoluteLogoPath($ordinateLogo); if (file_exists($absoluteLogoPath)) { $ordinateLogos[$ordinateColumn] = $absoluteLogoPath; } } } } } else { $ordinateLabels[$plottedMetric] = $processedReport['label'] . ' (' . $metrics[$plottedMetric]['name'] . ')'; } } else { $parameters = array( 'idSite' => $idSite, 'period' => $period, 'date' => $date, 'apiModule' => $apiModule, 'apiAction' => $apiAction, 'segment' => $segment, 'apiParameters' => false, 'idGoal' => $idGoal, 'idDimension' => $idDimension, 'language' => $languageLoaded, 'showTimer' => true, 'hideMetricsDoc' => false, 'idSubtable' => $idSubtable, 'showRawMetrics' => false ); $processedReport = Request::processRequest('API.getProcessedReport', $parameters); } // prepare abscissa and ordinate series $abscissaSeries = array(); $abscissaLogos = array(); $ordinateSeries = array(); /** @var \Piwik\DataTable\Simple|\Piwik\DataTable\Map $reportData */ $reportData = $processedReport['reportData']; $hasData = false; $hasNonZeroValue = false; if (!$isMultiplePeriod || !($reportData instanceof Map)) { $reportMetadata = $processedReport['reportMetadata']->getRows(); $i = 0; // $reportData instanceof DataTable foreach ($reportData->getRows() as $row) // Row[] { // $row instanceof Row $rowData = $row->getColumns(); // Associative Array $abscissaSeries[] = Common::unsanitizeInputValue($rowData['label']); foreach ($ordinateColumns as $column) { $parsedOrdinateValue = $this->parseOrdinateValue($rowData[$column]); $hasData = true; if ($parsedOrdinateValue != 0) { $hasNonZeroValue = true; } $ordinateSeries[$column][] = $parsedOrdinateValue; } if (isset($reportMetadata[$i])) { $rowMetadata = $reportMetadata[$i]->getColumns(); if (isset($rowMetadata['logo'])) { $absoluteLogoPath = self::getAbsoluteLogoPath($rowMetadata['logo']); if (file_exists($absoluteLogoPath)) { $abscissaLogos[$i] = $absoluteLogoPath; } } } $i++; } } else // if the report has no dimension we have multiple reports each with only one row within the reportData { // $periodsData instanceof Simple[] $periodsData = array_values($reportData->getDataTables()); $periodsCount = count($periodsData); for ($i = 0; $i < $periodsCount; $i++) { // $periodsData[$i] instanceof Simple // $rows instanceof Row[] if (empty($periodsData[$i])) { continue; } $rows = $periodsData[$i]->getRows(); if (array_key_exists(0, $rows)) { $rowData = $rows[0]->getColumns(); // associative Array foreach ($ordinateColumns as $column) { if(!isset($rowData[$column])) { continue; } $ordinateValue = $rowData[$column]; $parsedOrdinateValue = $this->parseOrdinateValue($ordinateValue); $hasData = true; if (!empty($parsedOrdinateValue)) { $hasNonZeroValue = true; } $ordinateSeries[$column][] = $parsedOrdinateValue; } } else { foreach ($ordinateColumns as $column) { $ordinateSeries[$column][] = 0; } } $rowId = $periodsData[$i]->getMetadata(DataTableFactory::TABLE_METADATA_PERIOD_INDEX)->getLocalizedShortString(); $abscissaSeries[] = Common::unsanitizeInputValue($rowId); } } if (!$hasData || !$hasNonZeroValue) { throw new Exception(Piwik::translate('General_NoDataForGraph')); } //Setup the graph $graph = StaticGraph::factory($graphType); $graph->setWidth($width); $graph->setHeight($height); $graph->setFont($font); $graph->setFontSize($fontSize); $graph->setLegendFontSize($legendFontSize); $graph->setOrdinateLabels($ordinateLabels); $graph->setShowLegend($showLegend); $graph->setAliasedGraph($aliasedGraph); $graph->setAbscissaSeries($abscissaSeries); $graph->setAbscissaLogos($abscissaLogos); $graph->setOrdinateSeries($ordinateSeries); $graph->setOrdinateLogos($ordinateLogos); $graph->setColors(!empty($colors) ? explode(',', $colors) : array()); $graph->setTextColor($textColor); $graph->setBackgroundColor($backgroundColor); $graph->setGridColor($gridColor); // when requested period is day, x-axis unit is time and all date labels can not be displayed // within requested width, force labels to be skipped every 6 days to delimit weeks if ($period == 'day' && $isMultiplePeriod) { $graph->setForceSkippedLabels(6); } // render graph $graph->renderGraph(); } catch (\Exception $e) { $graph = new \Piwik\Plugins\ImageGraph\StaticGraph\Exception(); $graph->setWidth($width); $graph->setHeight($height); $graph->setFont($font); $graph->setFontSize($fontSize); $graph->setBackgroundColor($backgroundColor); $graph->setTextColor($textColor); $graph->setException($e); $graph->renderGraph(); } // restoring get parameters $_GET = $savedGET; switch ($outputType) { case self::GRAPH_OUTPUT_FILE: if ($idGoal != '') { $idGoal = '_' . $idGoal; } if ($idDimension != '') { $idDimension = '__' . $idDimension; } $fileName = self::$DEFAULT_PARAMETERS[$graphType][self::FILENAME_KEY] . '_' . $apiModule . '_' . $apiAction . $idGoal . $idDimension . ' ' . str_replace(',', '-', $date) . ' ' . $idSite . '.png'; $fileName = str_replace(array(' ', '/'), '_', $fileName); if (!Filesystem::isValidFilename($fileName)) { throw new Exception('Error: Image graph filename ' . $fileName . ' is not valid.'); } return $graph->sendToDisk($fileName); case self::GRAPH_OUTPUT_PHP: return $graph->getRenderedImage(); case self::GRAPH_OUTPUT_INLINE: default: $graph->sendToBrowser(); exit; } } private function setFilterTruncate($default) { $_GET['filter_truncate'] = Common::getRequestVar('filter_truncate', $default, 'int'); } private static function parseOrdinateValue($ordinateValue) { $ordinateValue = @str_replace(',', '.', $ordinateValue); // convert hh:mm:ss formatted time values to number of seconds if (preg_match('/([0-9]{1,2}):([0-9]{1,2}):([0-9]{1,2})/', $ordinateValue, $matches)) { $hour = $matches[1]; $min = $matches[2]; $sec = $matches[3]; $ordinateValue = ($hour * 3600) + ($min * 60) + $sec; } // OK, only numbers from here please (strip out currency sign) $ordinateValue = preg_replace('/[^0-9.]/', '', $ordinateValue); return $ordinateValue; } private static function getFontPath($font) { return PIWIK_INCLUDE_PATH . self::FONT_DIR . $font; } protected static function getAbsoluteLogoPath($relativeLogoPath) { return PIWIK_INCLUDE_PATH . '/' . $relativeLogoPath; } }