diff options
author | BeezyT <timo@ezdesign.de> | 2012-09-21 13:19:53 +0400 |
---|---|---|
committer | BeezyT <timo@ezdesign.de> | 2012-09-21 13:19:53 +0400 |
commit | 5e7688106afd64a6f063dc2ef284e5578cf1adfd (patch) | |
tree | 05de26f79dc30ea681946b2165c51f583e7af92a /plugins/Transitions | |
parent | 8aedd18eb179b68a704e215581f2e56ccfb83f96 (diff) |
refs #3332 adding Transitions again
git-svn-id: http://dev.piwik.org/svn/trunk@7032 59fd770c-687e-43c8-a1e3-f5a4ff64c105
Diffstat (limited to 'plugins/Transitions')
-rw-r--r-- | plugins/Transitions/API.php | 220 | ||||
-rw-r--r-- | plugins/Transitions/Controller.php | 74 | ||||
-rw-r--r-- | plugins/Transitions/Transitions.php | 239 | ||||
-rw-r--r-- | plugins/Transitions/templates/transitions.css | 176 | ||||
-rw-r--r-- | plugins/Transitions/templates/transitions.js | 1078 | ||||
-rw-r--r-- | plugins/Transitions/templates/transitions.tpl | 43 | ||||
-rwxr-xr-x | plugins/Transitions/templates/transitions_rowaction.png | bin | 0 -> 269 bytes |
7 files changed, 1830 insertions, 0 deletions
diff --git a/plugins/Transitions/API.php b/plugins/Transitions/API.php new file mode 100644 index 0000000000..1e11063b19 --- /dev/null +++ b/plugins/Transitions/API.php @@ -0,0 +1,220 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + * @version $Id$ + * + * @category Piwik_Plugins + * @package Piwik_Transitions + */ + +/** + * @package Piwik_Transitions + */ +class Piwik_Transitions_API +{ + + static private $instance = null; + + static public function getInstance() + { + if (self::$instance == null) + { + self::$instance = new self; + } + return self::$instance; + } + + /** + * This method combines various reports (both from this and from other plugins) and + * returns a complete report. The report is used in the Transitions API to load all + * data at once. + */ + public function getFullReport($pageUrl, $idSite, $period, $date, $segment = false, $limitBeforeGrouping = false) + { + Piwik::checkUserHasViewAccess($idSite); + + $pageUrl = Piwik_Common::unsanitizeInputValue($pageUrl); + + $report = array(); + $this->addMainPageMetricsToReport($report, $pageUrl, $idSite, $period, $date, $segment); + $this->addLiveTransitionsDataToReport($report, $pageUrl, $idSite, $period, $date, $segment, $limitBeforeGrouping); + + // replace column names in the data tables + $columnNames = array( + 'label' => 'url', + Piwik_Archive::INDEX_NB_ACTIONS => 'referrals' + ); + $reportNames = array('previousPages', 'followingPages', 'outlinks', 'downloads'); + foreach ($reportNames as $reportName) + { + if (isset($report[$reportName])) + { + $report[$reportName]->filter('ReplaceColumnNames', array($columnNames)); + } + } + + return $report; + } + + /** + * Add the main metrics (pageviews, exits, bounces) to the full report. + * Data is loaded from Actions.getPageUrls using the label filter. + */ + private function addMainPageMetricsToReport(&$report, $pageUrl, $idSite, $period, $date, $segment) + { + $label = Piwik_Actions::getActionExplodedNames($pageUrl, Piwik_Tracker_Action::TYPE_ACTION_URL); + if (count($label) == 1) + { + $label = $label[0]; + } + else + { + $label = array_map('urlencode', $label); + $label = implode('>', $label); + } + + $parameters = array( + 'method' => 'Actions.getPageUrls', + 'idSite' => $idSite, + 'period' => $period, + 'date' => $date, + 'label' => $label, + 'format' => 'original', + 'serialize' => '0', + 'expanded' => '0' + ); + if (!empty($segment)) + { + $parameters['segment'] = $segment; + } + + $url = Piwik_Url::getQueryStringFromParameters($parameters); + $request = new Piwik_API_Request($url); + try + { + /** @var $dataTable Piwik_DataTable */ + $dataTable = $request->process(); + } + catch(Exception $e) + { + throw new Exception("Actions.getPageUrls returned an error: ".$e->getMessage()."\n"); + } + + if ($dataTable->getRowsCount() == 0) + { + throw new Exception("The label '$label' could not be found in Actions.getPageUrls\n"); + } + + $row = $dataTable->getFirstRow(); + + if ($row !== false) { + $report['pageMetrics'] = array( + 'pageviews' => intval($row->getColumn('nb_hits')), + 'exits' => intval($row->getColumn('exit_nb_visits')), + 'bounces' => intval($row->getColumn('entry_bounce_count')) + ); + } else { + $report['pageMetrics'] = array( + 'pageviews' => 0, + 'exits' => 0, + 'bounces' => 0 + ); + } + } + + /** + * Add transitions data to the report. + * Fake ArchiveProcessing to do the queries live. + */ + private function addLiveTransitionsDataToReport(&$report, $pageUrl, $idSite, $period, $date, + $segment, $limitBeforeGrouping) + { + // get idaction of page url + $actionsPlugin = new Piwik_Actions; + $idaction = $actionsPlugin->getIdActionFromSegment($pageUrl, 'idaction'); + + // prepare archive processing that can be reused by the archiving code + $archiveProcessing = new Piwik_ArchiveProcessing_Day(); + $archiveProcessing->setSite(new Piwik_Site($idSite)); + $archiveProcessing->setPeriod(Piwik_Period::advancedFactory($period, $date)); + $archiveProcessing->setSegment(new Piwik_Segment($segment, $idSite)); + $archiveProcessing->initForLiveUsage(); + + // launch the archiving code - but live + $transitionsArchiving = new Piwik_Transitions; + + $data = $transitionsArchiving->queryInternalReferrers($idaction, $archiveProcessing, $limitBeforeGrouping); + $report['previousPages'] = &$data['previousPages']; + $report['pageMetrics']['loops'] = intval($data['loops']); + + $data = $transitionsArchiving->queryFollowingActions($idaction, $archiveProcessing, $limitBeforeGrouping); + foreach ($data as $tableName => $table) { + $report[$tableName] = $table; + } + + $data = $transitionsArchiving->queryExternalReferrers($idaction, $archiveProcessing, $limitBeforeGrouping); + + $report['pageMetrics']['entries'] = 0; + $report['referrers'] = array(); + foreach ($data->getRows() as $row) + { + $referrerId = $row->getColumn('label'); + $visits = $row->getColumn(Piwik_Archive::INDEX_NB_VISITS); + if ($visits) + { + // load details (i.e. subtables) + $details = array(); + if ($idSubTable = $row->getIdSubDataTable()) + { + $subTable = Piwik_DataTable_Manager::getInstance()->getTable($idSubTable); + foreach ($subTable->getRows() as $subRow) + { + $details[] = array( + 'label' => $subRow->getColumn('label'), + 'referrals' => $subRow->getColumn(Piwik_Archive::INDEX_NB_VISITS) + ); + } + } + $report['referrers'][] = array( + 'label' => $this->getReferrerLabel($referrerId), + 'shortName' => Piwik_getRefererTypeFromShortName($referrerId), + 'visits' => $visits, + 'details' => $details + ); + $report['pageMetrics']['entries'] += $visits; + } + } + + // if there's no data for referrers, Piwik_API_ResponseBuilder::handleMultiDimensionalArray + // does not detect the multi dimensional array and the data is rendered differently, which + // causes an exception. + if (count($report['referrers']) == 0) + { + $report['referrers'][] = array( + 'label' => $this->getReferrerLabel(Piwik_Common::REFERER_TYPE_DIRECT_ENTRY), + 'shortName' => Piwik_getRefererTypeLabel(Piwik_Common::REFERER_TYPE_DIRECT_ENTRY), + 'visits' => 0 + ); + } + } + + private function getReferrerLabel($referrerId) { + switch ($referrerId) + { + case Piwik_Common::REFERER_TYPE_DIRECT_ENTRY: + return Piwik_Transitions_Controller::getTranslation('directEntries'); + case Piwik_Common::REFERER_TYPE_SEARCH_ENGINE: + return Piwik_Transitions_Controller::getTranslation('fromSearchEngines'); + case Piwik_Common::REFERER_TYPE_WEBSITE: + return Piwik_Transitions_Controller::getTranslation('fromWebsites'); + case Piwik_Common::REFERER_TYPE_CAMPAIGN: + return Piwik_Transitions_Controller::getTranslation('fromCampaigns'); + default: + return Piwik_Translate('General_Others'); + } + } + +}
\ No newline at end of file diff --git a/plugins/Transitions/Controller.php b/plugins/Transitions/Controller.php new file mode 100644 index 0000000000..1a32b9ef9b --- /dev/null +++ b/plugins/Transitions/Controller.php @@ -0,0 +1,74 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + * @version $Id$ + * + * @category Piwik_Plugins + * @package Piwik_Transitions + */ + +/** + * @package Piwik_Transitions + */ +class Piwik_Transitions_Controller extends Piwik_Controller +{ + + /** + * Since the metric translations are taken from different plugins, + * it makes the rest of the code easier to read and maintain when we + * use this indirection to map between the metrics and the actual + * translation keys. + */ + private static $metricTranslations = array( + 'pageviewsInline' => 'Transitions_PageviewsInline', + 'loopsInline' => 'Transitions_LoopsInline', + 'fromPreviousPages' => 'Transitions_FromPreviousPages', + 'fromPreviousPagesInline' => 'Transitions_FromPreviousPagesInline', + 'fromSearchEngines' => 'Transitions_FromSearchEngines', + 'fromSearchEnginesInline' => 'Transitions_FromSearchEnginesInline', + 'fromWebsites' => 'Transitions_FromWebsites', + 'fromWebsitesInline' => 'Transitions_FromWebsitesInline', + 'fromCampaigns' => 'Transitions_FromCampaigns', + 'fromCampaignsInline' => 'Transitions_FromCampaignsInline', + 'directEntries' => 'Transitions_DirectEntries', + 'directEntriesInline' => 'Referers_TypeDirectEntries', + 'toFollowingPages' => 'Transitions_ToFollowingPages', + 'toFollowingPagesInline' => 'Transitions_ToFollowingPagesInline', + 'downloads' => 'Actions_ColumnDownloads', + 'downloadsInline' => 'VisitsSummary_NbDownloadsDescription', + 'outlinks' => 'Actions_ColumnOutlinks', + 'outlinksInline' => 'VisitsSummary_NbOutlinksDescription', + 'exits' => 'General_ColumnExits', + 'exitsInline' => 'Transitions_ExitsInline', + 'bouncesInline' => 'Transitions_BouncesInline' + ); + + /** + * Translations that are added to JS + * (object Piwik_Transitions_Translations) + */ + private static $jsTranslations = array( + 'XOfY' => 'Transitions_XOfY', + 'XOfAllPageviews' => 'Transitions_XOfAllPageviews' + ); + + public static function getTranslation($key) + { + return Piwik_Translate(self::$metricTranslations[$key]); + } + + /** + * The main method of the plugin. + * It is triggered from the Transitions data table action. + */ + public function renderPopover() + { + $view = Piwik_View::factory('transitions'); + $view->translations = self::$metricTranslations + self::$jsTranslations; + echo $view->render(); + } + +} diff --git a/plugins/Transitions/Transitions.php b/plugins/Transitions/Transitions.php new file mode 100644 index 0000000000..94c02d84b7 --- /dev/null +++ b/plugins/Transitions/Transitions.php @@ -0,0 +1,239 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + * @version $Id$ + * + * @category Piwik_Plugins + * @package Piwik_Transitions + */ + +/** + * @package Piwik_Transitions + */ +class Piwik_Transitions extends Piwik_Plugin +{ + + private $limitBeforeGrouping = 5; + + public function getInformation() + { + return array( + 'description' => Piwik_Translate('Transitions_PluginDescription'), + 'author' => 'Piwik', + 'author_homepage' => 'http://piwik.org/', + 'version' => Piwik_Version::VERSION, + ); + } + + function getListHooksRegistered() + { + return array( + 'AssetManager.getCssFiles' => 'getCssFiles', + 'AssetManager.getJsFiles' => 'getJsFiles' + ); + } + + public function getCssFiles($notification) + { + $cssFiles = &$notification->getNotificationObject(); + $cssFiles[] = 'plugins/Transitions/templates/transitions.css'; + } + + public function getJsFiles($notification) + { + $jsFiles = &$notification->getNotificationObject(); + $jsFiles[] = 'plugins/Transitions/templates/transitions.js'; + } + + /** + * Get information about external referrers (i.e. search engines, websites & campaigns) + * + * @param $idaction + * @param Piwik_ArchiveProcessing_Day $archiveProcessing + * @return Piwik_DataTable + */ + public function queryExternalReferrers($idaction, Piwik_ArchiveProcessing_Day $archiveProcessing, + $limitBeforeGrouping = false) + { + $rankingQuery = new Piwik_RankingQuery($limitBeforeGrouping ? $limitBeforeGrouping : $this->limitBeforeGrouping); + + // we generate a single column that contains the interesting data for each referrer. + // the reason we cannot group by referer_* becomes clear when we look at search engine keywords. + // referer_url contains the url from the search engine, referer_keyword the keyword we want to + // group by. when we group by both, we don't get a single column for the keyword but instead + // one column per keyword + search engine url. this way, we could not get the top keywords using + // the ranking query. + $dimension = 'referrer_data'; + $rankingQuery->addLabelColumn('referrer_data'); + $select = ' + CASE referer_type + WHEN '.Piwik_Common::REFERER_TYPE_DIRECT_ENTRY.' THEN "" + WHEN '.Piwik_Common::REFERER_TYPE_SEARCH_ENGINE.' THEN referer_keyword + WHEN '.Piwik_Common::REFERER_TYPE_WEBSITE.' THEN referer_url + WHEN '.Piwik_Common::REFERER_TYPE_CAMPAIGN.' THEN CONCAT(referer_name, " ", referer_keyword) + END AS referrer_data, + referer_type'; + + // get one limited group per referrer type + $rankingQuery->partitionResultIntoMultipleGroups('referer_type', array( + Piwik_Common::REFERER_TYPE_DIRECT_ENTRY, + Piwik_Common::REFERER_TYPE_SEARCH_ENGINE, + Piwik_Common::REFERER_TYPE_WEBSITE, + Piwik_Common::REFERER_TYPE_CAMPAIGN + )); + + $orderBy = '`'.Piwik_Archive::INDEX_NB_VISITS.'` DESC'; + $where = 'visit_entry_idaction_url = '.intval($idaction); + + $metrics = array(Piwik_Archive::INDEX_NB_VISITS); + $data = $archiveProcessing->queryVisitsByDimension($dimension, $where, $metrics, $orderBy, + $rankingQuery, $select, $selectGeneratesLabelColumn = true); + + $referrerData = array(); + $referrerSubData = array(); + + foreach ($data as $referrerType => &$subData) + { + $referrerData[$referrerType] = array(Piwik_Archive::INDEX_NB_VISITS => 0); + if ($referrerType != Piwik_Common::REFERER_TYPE_DIRECT_ENTRY) + { + $referrerSubData[$referrerType] = array(); + } + + foreach ($subData as &$row) + { + if ($referrerType == Piwik_Common::REFERER_TYPE_SEARCH_ENGINE && empty($row['referrer_data'])) + { + $row['referrer_data'] = Piwik_Referers::LABEL_KEYWORD_NOT_DEFINED; + } + + $referrerData[$referrerType][Piwik_Archive::INDEX_NB_VISITS] += $row[Piwik_Archive::INDEX_NB_VISITS]; + + $label = $row['referrer_data']; + if ($label) + { + $referrerSubData[$referrerType][$label] = array( + Piwik_Archive::INDEX_NB_VISITS => $row[Piwik_Archive::INDEX_NB_VISITS] + ); + } + } + } + + return $archiveProcessing->getDataTableWithSubtablesFromArraysIndexedByLabel($referrerSubData, $referrerData); + } + + /** + * Get information about internal referrers (previous pages & loops, i.e. page refreshes) + * + * @param $idaction + * @param Piwik_ArchiveProcessing_Day $archiveProcessing + * @return array(previousPages:Piwik_DataTable, loops:integer) + */ + public function queryInternalReferrers($idaction, Piwik_ArchiveProcessing_Day $archiveProcessing, + $limitBeforeGrouping = false) + { + $dimension = 'idaction_url_ref'; + + $rankingQuery = new Piwik_RankingQuery($limitBeforeGrouping ? $limitBeforeGrouping : $this->limitBeforeGrouping); + $rankingQuery->addLabelColumn(array('name', 'url_prefix')); + $rankingQuery->setColumnToMarkExcludedRows('is_self'); + + $addSelect = ' + log_action.name, log_action.url_prefix, + CASE WHEN log_link_visit_action.idaction_url_ref = '.intval($idaction).' THEN 1 ELSE 0 END AS is_self'; + + $where = ' + log_link_visit_action.idaction_url = '.intval($idaction).' AND + log_action.type = '.Piwik_Tracker_Action::TYPE_ACTION_URL; + + $orderBy = '`'.Piwik_Archive::INDEX_NB_ACTIONS.'` DESC'; + + $metrics = array(Piwik_Archive::INDEX_NB_ACTIONS); + $data = $archiveProcessing->queryActionsByDimension(array($dimension), $where, $metrics, $orderBy, + $rankingQuery, $dimension, $addSelect); + + $previousPagesDataTable = new Piwik_DataTable; + foreach ($data['result'] as &$page) + { + $previousPagesDataTable->addRow(new Piwik_DataTable_Row(array( + Piwik_DataTable_Row::COLUMNS => array( + 'label' => Piwik_Tracker_Action::reconstructNormalizedUrl($page['name'], $page['url_prefix']), + Piwik_Archive::INDEX_NB_ACTIONS => intval($page[Piwik_Archive::INDEX_NB_ACTIONS]) + ) + ))); + } + + $loops = 0; + if (count($data['excludedFromLimit'])) + { + $loops = intval($data['excludedFromLimit'][0][Piwik_Archive::INDEX_NB_ACTIONS]); + } + + return array( + 'previousPages' => $previousPagesDataTable, + 'loops' => $loops + ); + } + + /** + * Get information about the following actions (following pages, outlinks, downloads) + * + * @param $idaction + * @param Piwik_ArchiveProcessing_Day $archiveProcessing + * @return array(followingPages:Piwik_DataTable, outlinks:Piwik_DataTable, downloads:Piwik_DataTable) + */ + public function queryFollowingActions($idaction, Piwik_ArchiveProcessing_Day $archiveProcessing, + $limitBeforeGrouping = false) + { + static $types = array( + Piwik_Tracker_Action::TYPE_ACTION_URL => 'followingPages', + Piwik_Tracker_Action::TYPE_OUTLINK => 'outlinks', + Piwik_Tracker_Action::TYPE_DOWNLOAD => 'downloads' + ); + + $dimension = 'idaction_url'; + + $rankingQuery = new Piwik_RankingQuery($limitBeforeGrouping ? $limitBeforeGrouping : $this->limitBeforeGrouping); + $rankingQuery->addLabelColumn(array('name', 'url_prefix')); + $rankingQuery->partitionResultIntoMultipleGroups('type', array_keys($types)); + + $addSelect = 'log_action.name, log_action.url_prefix, log_action.type'; + + $where = ' + log_link_visit_action.idaction_url_ref = '.intval($idaction).' AND + log_link_visit_action.idaction_url != '.intval($idaction); + + $orderBy = '`'.Piwik_Archive::INDEX_NB_ACTIONS.'` DESC'; + + $metrics = array(Piwik_Archive::INDEX_NB_ACTIONS); + $data = $archiveProcessing->queryActionsByDimension(array($dimension), $where, $metrics, $orderBy, + $rankingQuery, $dimension, $addSelect); + + $dataTables = array(); + foreach ($types as $type => $recordName) + { + $dataTable = new Piwik_DataTable; + if (isset($data[$type])) + { + foreach ($data[$type] as &$record) + { + $dataTable->addRow(new Piwik_DataTable_Row(array( + Piwik_DataTable_Row::COLUMNS => array( + 'label' => $type == Piwik_Tracker_Action::TYPE_ACTION_URL ? + Piwik_Tracker_Action::reconstructNormalizedUrl($record['name'], $record['url_prefix']) : + $record['name'], + Piwik_Archive::INDEX_NB_ACTIONS => intval($record[Piwik_Archive::INDEX_NB_ACTIONS]) + ) + ))); + } + } + $dataTables[$recordName] = $dataTable; + } + + return $dataTables; + } + +}
\ No newline at end of file diff --git a/plugins/Transitions/templates/transitions.css b/plugins/Transitions/templates/transitions.css new file mode 100644 index 0000000000..eda84685ea --- /dev/null +++ b/plugins/Transitions/templates/transitions.css @@ -0,0 +1,176 @@ + +#Transitions_Container { + position: relative; + z-index: 1500; + height: 550px; + text-align: left; +} + +#Transitions_Canvas_Background { + position: absolute; + z-index: 1501; +} + +#Transitions_Canvas { + position: absolute; + z-index: 1502; +} + +.Transitions_Text { + color: black; + font-size: 11px; + line-height: 14px; + position: absolute; + z-index: 1502; + font-family: Arial, Helvetica, sans-serif; + word-wrap: break-word; + text-align: left; + cursor: default; +} + +#Transitions_CenterBox { + margin: 60px 0 0 320px; + width: 208px; + height: 344px; + background: #f7f7f7; + border: 1px solid #a9a399; + border-radius:10px; + -moz-border-radius:10px; + -webkit-border-radius:10px; + -webkit-box-shadow: 0px 0px 9px 0px #999; + -moz-box-shadow: 0px 0px 9px 0px #999; + box-shadow: 0px 0px 9px 0px #999; + z-index: 1503; +} + +#Transitions_CenterBox.Transitions_Loading { + background: url(../../../themes/default/images/loading-blue.gif) no-repeat center center #f7f7f7; +} + +#Transitions_CenterBox h2 { + font-size: 12px; + line-height: 16px; + padding: 10px; + border-bottom: 1px dotted #a9a399; + font-weight: bold; + overflow: hidden; + color: #255792; +} + +.Transitions_Pageviews { + text-align: center; +} + +.Transitions_OutgoingTraffic { + text-align: right; +} + +.Transitions_CenterBoxMetrics { + padding: 15px 10px 0 10px; + display: none; + font-size: 12px; +} + +.Transitions_CenterBoxMetrics table td { + padding: 0 0 5px 0; +} + +.Transitions_CenterBoxMetrics table td.Transitions_Percentage { + padding-right: 6px; + font-weight: bold; +} + +#Transitions_CenterBox h3 { + font-weight: bold; + font-size: 12px; + margin: 15px 0 7px 0; + padding: 0; + color: #7E7363; +} + +#Transitions_Loops { + margin: 435px 0 0 320px; + width: 208px; + text-align: center; + line-height: 25px; + font-size: 12px; + display: none; + z-index: 1503; + cursor: default; +} + +.Transitions_CenterBoxMetrics p { + margin: 0 0 3px 0; + padding: 0; + cursor: default; + font-size: 12px; + line-height: 16px; +} + +.Transitions_CenterBoxMetrics p.Transitions_Margin { + margin-bottom: 11px; +} + +span.Transitions_Metric { + font-weight: bold; + cursor: default; +} + +.Transitions_TitleOfOpenGroup { + font-size: 12px; + color: #255792; + font-weight: bold; +} + +.Transitions_BoxTextLeft, +.Transitions_BoxTextRight { + width: 130px; + height: 42px; + overflow: hidden; +} + +.Transitions_BoxTextRight { + text-align: right; +} + +.Transitions_BoxTextLeft.Transitions_HasBackground, +.Transitions_BoxTextRight.Transitions_HasBackground { + background-repeat: no-repeat; + height: 18px; +} + +.Transitions_BoxTextLeft.Transitions_HasBackground { + background-position: 0 1px; + width: 150px; +} +.Transitions_BoxTextLeft.Transitions_HasBackground span { + display: block; + padding-left: 16px; +} + +.Transitions_BoxTextRight.Transitions_HasBackground { + background-position: right 1px; +} +.Transitions_BoxTextRight.Transitions_HasBackground span { + display: block; + padding-right: 17px; +} + +.Transitions_CurveTextLeft, +.Transitions_CurveTextRight { + color: #255792; + font-weight: bold; + width: 28px; + text-align: right; + cursor: default; +} + +body .piwik-tooltip.Transitions_Tooltip_Small { + font-size: 11px; + padding: 3px 5px 3px 6px; + background: white; +} + +.Transitions_SingleLine { + font-size: 12px; +}
\ No newline at end of file diff --git a/plugins/Transitions/templates/transitions.js b/plugins/Transitions/templates/transitions.js new file mode 100644 index 0000000000..69156ae1f2 --- /dev/null +++ b/plugins/Transitions/templates/transitions.js @@ -0,0 +1,1078 @@ +/*! + * Piwik - Web Analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + + +// +// TRANSITIONS ROW ACTION FOR DATA TABLES +// + +function DataTable_RowActions_Transitions(dataTable) { + this.dataTable = dataTable; + this.transitions = null; +} + +DataTable_RowActions_Transitions.prototype = new DataTable_RowAction; + +// override trigger method directly because we don't need the label +DataTable_RowActions_Transitions.prototype.trigger = function(tr, e, subTableLabel) { + var link = tr.find('> td:first > a').attr('href'); + link = $('<textarea>').html(link).val(); // remove html entities + this.openPopover(link); +}; + +DataTable_RowActions_Transitions.prototype.doOpenPopover = function(link) { + if (this.transitions === null) { + this.transitions = new Piwik_Transitions(link, this); + } else { + this.transitions.reset(link); + } + this.transitions.showPopover(); +}; + + +DataTable_RowActions_Registry.register({ + + name: 'Transitions', + + dataTableIcon: 'plugins/Transitions/templates/transitions_rowaction.png', + + createInstance: function(dataTable) { + return new DataTable_RowActions_Transitions(dataTable); + }, + + isAvailable: function(dataTableParams, tr) { + return dataTableParams.module == 'Actions' + && dataTableParams.action == 'getPageUrls' + && tr.find('> td:first > a').size() > 0; + } + +}); + + +// +// TRANSITIONS IMPLEMENTATION +// + +function Piwik_Transitions(link, rowAction) { + this.reset(link); + this.rowAction = rowAction; + + this.ajax = new Piwik_Transitions_Ajax(); + this.model = new Piwik_Transitions_Model(this.ajax); + + this.leftGroups = ['previousPages', 'searchEngines', 'websites', 'campaigns']; + this.rightGroups = ['followingPages', 'downloads', 'outlinks']; +} + +Piwik_Transitions.prototype.reset = function(link) { + this.link = link; + this.popover = null; + this.canvas = null; + this.centerBox = null; + + this.leftOpenGroup = 'previousPages'; + this.rightOpenGroup = 'followingPages'; +}; + +/** Open the popover */ +Piwik_Transitions.prototype.showPopover = function() { + var self = this; + + // initialize popover (with loading message) + var loading = $('div.loadingPiwik:first').clone(); + var box = $(document.createElement('div')).attr('id', 'Transitions_Popover').html(loading); + box.dialog({ + title: '', + modal: true, + width: '900px', + position: ['center', 'center'], + resizable: false, + autoOpen: true, + open: function(event, ui) { + $('.ui-widget-overlay').on('click.transitions',function(){ + box.dialog('close'); + }); + }, + close: function(event, ui) { + box.dialog('destroy').remove(); + piwikHelper.abortQueueAjax(); + $('.ui-widget-overlay').off('click.rowEvolution'); + } + }); + this.popover = box; + + // load the popover HTML + this.ajax.callTransitionsController('renderPopover', function(html) { + box.html(html); + box.dialog({position: ['center', 'center']}); + + var canvasDom = box.find('#Transitions_Canvas')[0]; + var canvasBgDom = box.find('#Transitions_Canvas_Background')[0]; + self.canvas = new Piwik_Transitions_Canvas(canvasDom, canvasBgDom, 850, 550); + + self.centerBox = box.find('#Transitions_CenterBox'); + + var link = Piwik_Transitions_Util.shortenUrl(self.link, true); + self.centerBox.find('h2').html(Piwik_Transitions_Util.addBreakpoints(link)); + + self.model.loadData(self.link, function() { + self.render(); + }); + }); +}; + +/** Render the popover content */ +Piwik_Transitions.prototype.render = function() { + this.renderCenterBox(); + + this.renderLeftSide(); + this.renderRightSide(); + + this.renderLoops(); +}; + +/** Render left side: referrer groups & direct entries */ +Piwik_Transitions.prototype.renderLeftSide = function() { + this.renderGroups(this.leftGroups, this.leftOpenGroup, 'left'); + this.renderEntries(); + + this.reRenderIfNeededToCenter('left'); +}; + +/** Render right side: following pages & exits */ +Piwik_Transitions.prototype.renderRightSide = function() { + this.renderGroups(this.rightGroups, this.rightOpenGroup, 'right'); + this.renderExits(); + + this.reRenderIfNeededToCenter('right'); +}; + +/** Helper method to render open and closed groups for both sides */ +Piwik_Transitions.prototype.renderGroups = function(groups, openGroup, side) { + for (var i = 0; i < groups.length; i++) { + var groupName = groups[i]; + if (groupName == openGroup) { + if (i != 0) { + this.canvas.addBoxSpacing(13, side); + } + this.renderOpenGroup(groupName, side); + } else { + this.renderClosedGroup(groupName, side); + } + } + + this.canvas.addBoxSpacing(13, side); +}; + +/** + * If one side doesn't have much information, it doesn't look good to start from y=0. + * In this case, add some spacing on top and redraw. + */ +Piwik_Transitions.prototype.reRenderIfNeededToCenter = function(side) { + var height = (side == 'left' ? this.canvas.leftBoxPositionY : this.canvas.rightBoxPositionY) - 20; + if (height < 460 && !this.reRendering) { + var yOffset = (460 - height) / 2; + this.canvas.clearSide(side); + this.canvas.addBoxSpacing(yOffset, side); + this.reRendering = true; + side == 'left' ? this.renderLeftSide() : this.renderRightSide(); + this.reRendering = false; + } +}; + +/** Render the center box with the main metrics */ +Piwik_Transitions.prototype.renderCenterBox = function() { + var box = this.centerBox; + box.removeClass('Transitions_Loading'); + + Piwik_Transitions_Util.replacePlaceholderInHtml( + box.find('.Transitions_Pageviews'), this.model.pageviews); + + var self = this; + var showMetric = function(cssClass, modelProperty) { + var el = box.find('.Transitions_' + cssClass); + Piwik_Transitions_Util.replacePlaceholderInHtml(el, self.model[modelProperty]); + self.addTooltipShowingPercentageOfAllPageviews(el, modelProperty); + }; + + showMetric('DirectEntries', 'directEntries'); + showMetric('PreviousPages', 'previousPagesNbTransitions'); + showMetric('SearchEngines', 'searchEnginesNbTransitions'); + showMetric('Websites', 'websitesNbTransitions'); + + showMetric('FollowingPages', 'followingPagesNbTransitions'); + showMetric('Outlinks', 'outlinksNbTransitions'); + showMetric('Downloads', 'downloadsNbTransitions'); + showMetric('Exits', 'exits'); + showMetric('Bounces', 'bounces'); + + box.find('.Transitions_CenterBoxMetrics').show(); +}; + +Piwik_Transitions.prototype.addTooltipShowingPercentageOfAllPageviews = function(element, metric) { + var self = this; + element.hover(function() { + var tip = Piwik_Transitions_Translations.XOfAllPageviews; + var percentage = self.model.getPercentage(metric, true); + tip = tip.replace(/%s/, '<b>' + percentage + '</b>'); + Piwik_Tooltip.show(tip, 'Transitions_Tooltip_Small'); + }, function() { + Piwik_Tooltip.hide(); + }); +}; + +/** Render the loops (i.e. page refreshes) */ +Piwik_Transitions.prototype.renderLoops = function() { + if (this.model.loops == 0) { + return; + } + + var loops = this.popover.find('#Transitions_Loops').show(); + Piwik_Transitions_Util.replacePlaceholderInHtml(loops, this.model.loops); + + this.addTooltipShowingPercentageOfAllPageviews(loops, 'loops'); + + this.canvas.renderLoops(this.model.getPercentage('loops')); +}; + +Piwik_Transitions.prototype.renderEntries = function() { + if (this.model.directEntries > 0) { + this.canvas.renderBox({ + side: 'left', + share: this.model.getPercentage('directEntries'), + gradient: this.canvas.createHorizontalGradient('#CFEDCA', '#91DE83', 'left'), + boxText: Piwik_Transitions_Translations.directEntries, + boxTextNumLines: 1, + boxTextCssClass: 'SingleLine', + smallBox: true + }); + this.canvas.addBoxSpacing(20, 'left'); + } +}; + +Piwik_Transitions.prototype.renderExits = function() { + if (this.model.exits > 0) { + this.canvas.renderBox({ + side: 'right', + share: this.model.getPercentage('exits'), + gradient: this.canvas.createHorizontalGradient('#CFEDCA', '#91DE83', 'right'), + boxText: Piwik_Transitions_Translations.exits, + boxTextNumLines: 1, + boxTextCssClass: 'SingleLine', + smallBox: true + }); + this.canvas.addBoxSpacing(20, 'right'); + } +}; + +/** Render the open group with the detailed data */ +Piwik_Transitions.prototype.renderOpenGroup = function(groupName, side) { + var self = this; + + // get data from the model + var nbTransitionsVarName = groupName + 'NbTransitions'; + var nbTransitions = self.model[nbTransitionsVarName]; + if (nbTransitions == 0) { + return; + } + + var totalShare = this.model.getPercentage(nbTransitionsVarName); + var details = self.model.getDetailsForGroup(groupName); + + // prepare gradients + var gradientItems = this.canvas.createHorizontalGradient('#E3DFD1', '#E8E4D5', side); + var gradientOthers = this.canvas.createHorizontalGradient('#F5F3EB', '#E8E4D5', side); + var gradientBackground = this.canvas.createHorizontalGradient('#FFFFFF', '#B0CAE8', side); + + // remember current offsets to reset them later for drawing the background + var boxPositionBefore, curvePositionBefore; + if (side == 'left') { + boxPositionBefore = this.canvas.leftBoxPositionY; + curvePositionBefore = this.canvas.leftCurvePositionY; + } else { + boxPositionBefore = this.canvas.rightBoxPositionY; + curvePositionBefore = this.canvas.rightCurvePositionY; + } + + // headline of the open group + var titleX, titleClass; + if (side == 'left') { + titleX = this.canvas.leftBoxBeginX + 10; + titleClass = 'BoxTextLeft'; + } else { + titleX = this.canvas.rightBoxBeginX - 1; + titleClass = 'BoxTextRight'; + } + var groupTitle = self.model.getGroupTitle(groupName); + this.canvas.renderText(groupTitle, titleX , boxPositionBefore + 11, [titleClass, 'TitleOfOpenGroup']); + this.canvas.addBoxSpacing(34, side); + + // draw detail boxes + for (var i = 0; i < details.length; i++) { + var data = details[i]; + var label = (typeof data.url != 'undefined' ? data.url : data.label); + var isOthers = (label == 'Others'); + var onClick = false; + if (!isOthers && (groupName == 'previousPages' || groupName == 'followingPages')) { + onClick = (function(url) { + return function() { self.reloadPopover(url); }; + })(label); + } + + var tooltip = Piwik_Transitions_Translations.XOfY; + tooltip = '<b>' + tooltip.replace(/%s/, data.referrals + '</b>').replace(/%s/, nbTransitions); + tooltip = this.model.getShareInGroupTooltip(tooltip, groupName); + + var fullLabel = label; + var shortened = false; + if ((groupName == 'previousPages' || groupName == 'followingPages' || groupName == 'downloads')) { + // remove http + www + domain for internal URLs + label = Piwik_Transitions_Util.shortenUrl(label, true); + shortened = true; + } else if (groupName == 'outlinks' || groupName == 'websites') { + // remove http + www + domain external URLs + label = Piwik_Transitions_Util.shortenUrl(label); + shortened = true; + } + + this.canvas.renderBox({ + side: side, + share: data.percentage / 100 * totalShare, + gradient: isOthers ? gradientOthers : gradientItems, + boxText: label, + boxTextTooltip: isOthers || !shortened ? false : fullLabel, + truncateBoxText: true, + boxTextNumLines: 3, + curveText: data.percentage + '%', + curveTextTooltip: tooltip, + onClick: onClick + }); + } + + // draw background + var boxPositionAfter, curvePositionAfter; + if (side == 'left') { + boxPositionAfter = this.canvas.leftBoxPositionY; + curvePositionAfter = this.canvas.leftCurvePositionY; + this.canvas.leftBoxPositionY = boxPositionBefore; + this.canvas.leftCurvePositionY = curvePositionBefore; + } else { + boxPositionAfter = this.canvas.rightBoxPositionY; + curvePositionAfter = this.canvas.rightCurvePositionY; + this.canvas.rightBoxPositionY = boxPositionBefore; + this.canvas.rightCurvePositionY = curvePositionBefore; + } + + this.canvas.renderBox({ + side: side, + boxHeight: boxPositionAfter - boxPositionBefore - this.canvas.boxSpacing - 2, + curveHeight: curvePositionAfter - curvePositionBefore - this.canvas.curveSpacing, + gradient: gradientBackground, + bgCanvas: true + }); + + this.canvas.addBoxSpacing(15, side); +}; + +/** Render a closed group without detailed data, only one box for the sum */ +Piwik_Transitions.prototype.renderClosedGroup = function(groupName, side) { + var self = this; + var gradient = this.canvas.createHorizontalGradient('#DDE4ED', '#9BBADE', side); + + var nbTransitionsVarName = groupName + 'NbTransitions'; + + if (self.model[nbTransitionsVarName] == 0) { + return; + } + + self.canvas.renderBox({ + side: side, + share: self.model.getPercentage(nbTransitionsVarName), + gradient: gradient, + boxText: self.model.getGroupTitle(groupName), + boxTextNumLines: 1, + boxTextCssClass: 'SingleLine', + boxIcon: 'themes/default/images/plus_blue.png', + smallBox: true, + onClick: function() { + self.openGroup(side, groupName); + } + }); +}; + +/** Reload the entire popover for a different URL */ +Piwik_Transitions.prototype.reloadPopover = function(url) { + this.popover.dialog('close'); + this.rowAction.openPopover(url); +}; + +/** Redraw the left or right sides with a different group opened */ +Piwik_Transitions.prototype.openGroup = function(side, groupName) { + + this.canvas.clearSide(side); + + if (side == 'left') { + this.leftOpenGroup = groupName; + this.renderLeftSide(); + } else { + this.rightOpenGroup = groupName; + this.renderRightSide(); + } + + this.renderLoops(); +}; + + +// -------------------------------------- +// CANVAS +// -------------------------------------- + +function Piwik_Transitions_Canvas(canvasDom, canvasBgDom, width, height) { + if (!canvasDom.getContext) { + alert('Your browser is not supported.'); + return; + } + + /** DOM element that contains the canvas */ + this.container = $(canvasDom).parent(); + /** Drawing context of the canvas */ + this.context = canvasDom.getContext('2d'); + /** Drawing context of the background canvas */ + this.bgContext = canvasBgDom.getContext('2d'); + + /** Width of the entire canvas */ + this.width = canvasDom.width = canvasBgDom.width = width; + /** Height of the entire canvas */ + this.height = canvasDom.height = canvasBgDom.height = height; + + /** Current Y positions */ + this.leftBoxPositionY = this.originalBoxPositionY = 0; + this.leftCurvePositionY = this.originalCurvePositionY = 110; + this.rightBoxPositionY = this.originalBoxPositionY; + this.rightCurvePositionY = this.originalCurvePositionY; + + /** Width of the rectangular box */ + this.boxWidth = 140; + /** Height of the rectangular box */ + this.boxHeight = 53; + /** Height of a smaller rectangular box */ + this.smallBoxHeight = 30; + /** Width of the curve that connects the boxes to the center */ + this.curveWidth = 180; + /** Line-height of the text */ + this.lineHeight = 14; + /** Spacing between rectangular boxes */ + this.boxSpacing = 7; + /** Spacing between the curves where they connect to the center */ + this.curveSpacing = 1.5; + + /** The total net height (without curve spacing) of the curves as they connect to the center */ + this.totalHeightOfConnections = 205; + + /** X positions of the left box - begin means left, end means right */ + this.leftBoxBeginX = 0; + this.leftCurveBeginX = this.leftBoxBeginX + this.boxWidth; + this.leftCurveEndX = this.leftCurveBeginX + this.curveWidth; + + /** X positions of the right box - begin means left, end means right */ + this.rightBoxEndX = this.width; + this.rightBoxBeginX = this.rightCurveEndX = this.rightBoxEndX - this.boxWidth; + this.rightCurveBeginX = this.rightCurveEndX - this.curveWidth; +} + +/** + * Helper to create horizontal gradients + * @param position left|right + */ +Piwik_Transitions_Canvas.prototype.createHorizontalGradient = function(lightColor, darkColor, position) { + var fromX, toX, fromColor, toColor; + + if (position == 'left') { + // gradient is used to fill a box on the left + fromX = this.leftBoxBeginX + 50; + toX = this.leftCurveEndX - 20; + fromColor = lightColor; + toColor = darkColor; + } else { + // gradient is used to fill a box on the right + fromX = this.rightCurveBeginX + 20; + toX = this.rightBoxEndX - 50; + fromColor = darkColor; + toColor = lightColor; + } + + var gradient = this.context.createLinearGradient(fromX, 0, toX, 0); + gradient.addColorStop(0, fromColor); + gradient.addColorStop(1, toColor); + + return gradient; +}; + +/** Render text using a div inside the container */ +Piwik_Transitions_Canvas.prototype.renderText = function(text, x, y, cssClass, onClick, icon, maxLines) { + var div = this.addDomElement('div', 'Text'); + div.html('<span>' + Piwik_Transitions_Util.addBreakpoints(text) + '</span>'); + div.css({ + left: x + 'px', + top: y + 'px' + }); + if (icon) { + div.addClass('Transitions_HasBackground'); + div.css({backgroundImage: 'url(' + icon + ')'}); + } + if (cssClass) { + if (typeof cssClass == 'object') { + for (var i = 0; i < cssClass.length; i++) { + div.addClass('Transitions_' + cssClass[i]); + } + } else { + div.addClass('Transitions_' + cssClass); + } + } + if (onClick) { + div.css('cursor', 'pointer').hover(function() { + $(this).addClass('Transitions_Hover'); + }, function() { + $(this).removeClass('Transitions_Hover'); + }).click(onClick); + } + if (maxLines) { + // truncate until span fits inside div: substitute middle part with ... + var span = div.find('span'); + var divHeight = div.innerHeight(); + var leftPart = false; + var rightPart = false; + while (divHeight < span.outerHeight()) { + if (leftPart === false) { + var middle = Math.round(text.length / 2); + leftPart = text.substring(0, middle); + rightPart = text.substring(middle, text.length); + } + leftPart = leftPart.substring(0, leftPart.length - 2); + rightPart = rightPart.substring(2, rightPart.length); + text = leftPart + '...' + rightPart; + span.html(Piwik_Transitions_Util.addBreakpoints(text)); + } + } + return div; +}; + +/** Add a DOM element inside the container (as a sibling of the canvas) */ +Piwik_Transitions_Canvas.prototype.addDomElement = function(tagName, cssClass) { + var el = $(document.createElement('div')).addClass('Transitions_' + cssClass); + this.container.append(el); + return el; +}; + +/** + * Render a box. + * This method automatically keeps track of the current position. + * + * PARAMS (pass as object): + * side: left or right + * share: of the box in the total amount of incoming transitions + * gradient: for filling the box + * boxText: to be placed inside the box (optional) + * boxTextNumLines: the number of lines to be placed in the box (optional) + * boxTextCssClass: for divs containing the texts (optional) + * boxTextTooltip: text for a tooltip this is when hovering the box text (optional) + * curveText: to be placed where the curve begins (optional) + * curveTextTooltip: text for a tooltip that is shown when hovering the curve text (optional) + * smallBox: use this.smallBoxHeight instead of this.boxHeight (optional) + * boxIcon: path to an icon that is put in front of the text (optional) + * onClick: click callback for the text in the box (optional) + * + * Only used for background: + * curveHeight: fix height in px instead of share + * boxHeight: fix box height in px + * bgCanvas: true to draw on background canvas + */ +Piwik_Transitions_Canvas.prototype.renderBox = function(params) { + var curveHeight = params.curveHeight ? params.curveHeight : + Math.round(this.totalHeightOfConnections * params.share); + curveHeight = Math.max(curveHeight, 1); + + var boxHeight = this.boxHeight; + if (params.smallBox) boxHeight = this.smallBoxHeight; + if (params.boxHeight) boxHeight = params.boxHeight; + + var context = params.bgCanvas ? this.bgContext : this.context; + + // background + context.fillStyle = params.gradient; + context.beginPath(); + if (params.side == 'left') { + this.renderLeftBoxBg(context, boxHeight, curveHeight); + } else { + this.renderRightBoxBg(context, boxHeight, curveHeight); + } + if (typeof context.endPath == 'function') { + context.endPath(); + } + + // text inside the box + if (params.boxText) { + var onClick = typeof params.onClick == 'function' ? params.onClick : false; + var boxTextLeft, boxTextTop, el; + if (params.side == 'left') { + boxTextLeft = this.leftBoxBeginX + 10; + boxTextTop = this.leftBoxPositionY + boxHeight / 2 - params.boxTextNumLines * this.lineHeight / 2; + el = this.renderText(params.boxText, boxTextLeft, boxTextTop, 'BoxTextLeft', onClick, params.boxIcon, params.boxTextNumLines); + } else { + boxTextLeft = this.rightBoxBeginX; + boxTextTop = this.rightBoxPositionY + boxHeight / 2 - params.boxTextNumLines * this.lineHeight / 2; + el = this.renderText(params.boxText, boxTextLeft, boxTextTop, 'BoxTextRight', onClick, params.boxIcon, params.boxTextNumLines); + } + if (params.boxTextCssClass) { + el.addClass('Transitions_' + params.boxTextCssClass); + } + // tooltip + if (params.boxTextTooltip) { + el.hover(function() { + var tip = Piwik_Transitions_Util.addBreakpoints(params.boxTextTooltip); + Piwik_Tooltip.show(tip, 'Transitions_Tooltip_Small', 300); + }, function() { + Piwik_Tooltip.hide(); + }); + } + } + + // text at the beginning of the curve + if (params.curveText) { + var curveTextLeft, curveTextTop; + if (params.side == 'left') { + curveTextLeft = this.leftBoxBeginX + this.boxWidth + 12; + curveTextTop = this.leftBoxPositionY + boxHeight / 2 - this.lineHeight / 2; + } else { + curveTextLeft = this.rightBoxBeginX - 35; + curveTextTop = this.rightBoxPositionY + boxHeight / 2 - this.lineHeight / 2; + } + var textDiv = this.renderText(params.curveText, curveTextLeft, curveTextTop, + params.side == 'left' ? 'CurveTextLeft' : 'CurveTextRight'); + // tooltip + if (params.curveTextTooltip) { + textDiv.hover(function() { + Piwik_Tooltip.show(params.curveTextTooltip, 'Transitions_Tooltip_Small'); + }, function() { + Piwik_Tooltip.hide(); + }); + } + } + + if (params.side == 'left') { + this.leftBoxPositionY += boxHeight + this.boxSpacing; + this.leftCurvePositionY += curveHeight + this.curveSpacing; + } else { + this.rightBoxPositionY += boxHeight + this.boxSpacing; + this.rightCurvePositionY += curveHeight + this.curveSpacing; + } +}; + +Piwik_Transitions_Canvas.prototype.renderLeftBoxBg = function(context, boxHeight, curveHeight) { + // derive coordinates for ths curve + var leftUpper = {x: this.leftCurveBeginX, y: this.leftBoxPositionY}; + var leftLower = {x: this.leftCurveBeginX, y: this.leftBoxPositionY + boxHeight}; + var rightUpper = {x: this.leftCurveEndX, y: this.leftCurvePositionY}; + var rightLower = {x: this.leftCurveEndX, y: this.leftCurvePositionY + curveHeight}; + + // derive control points for bezier curve + var center = (this.leftCurveBeginX + this.leftCurveEndX) / 2; + var cp1Upper = {x: center, y: leftUpper.y}; + var cp2Upper = {x: center, y: rightUpper.y}; + var cp1Lower = {x: center, y: rightLower.y}; + var cp2Lower = {x: center, y: leftLower.y}; + + // the flow + context.moveTo(leftUpper.x, leftUpper.y); + context.bezierCurveTo(cp1Upper.x, cp1Upper.y, cp2Upper.x, cp2Upper.y, rightUpper.x, rightUpper.y); + context.lineTo(rightLower.x, rightLower.y); + context.bezierCurveTo(cp1Lower.x, cp1Lower.y, cp2Lower.x, cp2Lower.y, leftLower.x, leftLower.y); + + // the box + context.lineTo(leftLower.x - this.boxWidth + 4, leftLower.y); + context.lineTo(leftLower.x - this.boxWidth, leftUpper.y); + context.lineTo(leftUpper.x, leftUpper.y); + context.fill(); +}; + +Piwik_Transitions_Canvas.prototype.renderRightBoxBg = function(context, boxHeight, curveHeight) { + // derive coordinates for curve + var leftUpper = {x: this.rightCurveBeginX, y: this.rightCurvePositionY}; + var leftLower = {x: this.rightCurveBeginX, y: this.rightCurvePositionY + curveHeight}; + var rightUpper = {x: this.rightCurveEndX, y: this.rightBoxPositionY}; + var rightLower = {x: this.rightCurveEndX, y: this.rightBoxPositionY + boxHeight}; + + // derive control points for bezier curve + var center = (this.rightCurveBeginX + this.rightCurveEndX) / 2; + var cp1Upper = {x: center, y: leftUpper.y}; + var cp2Upper = {x: center, y: rightUpper.y}; + var cp1Lower = {x: center, y: rightLower.y}; + var cp2Lower = {x: center, y: leftLower.y}; + + // the flow part 1 + context.moveTo(leftUpper.x, leftUpper.y); + context.bezierCurveTo(cp1Upper.x, cp1Upper.y, cp2Upper.x, cp2Upper.y, rightUpper.x, rightUpper.y); + + // the box + context.lineTo(rightUpper.x + this.boxWidth, rightUpper.y); + context.lineTo(rightLower.x + this.boxWidth - 4, rightLower.y); + context.lineTo(rightLower.x, rightLower.y); + + // the flow part 2 + context.bezierCurveTo(cp1Lower.x, cp1Lower.y, cp2Lower.x, cp2Lower.y, leftLower.x, leftLower.y); + context.lineTo(leftUpper.x, leftUpper.y); + context.fill(); +}; + +/** Add spacing after the current box */ +Piwik_Transitions_Canvas.prototype.addBoxSpacing = function(spacing, side) { + if (side == 'left') { + this.leftBoxPositionY += spacing; + } else { + this.rightBoxPositionY += spacing; + } +}; + +Piwik_Transitions_Canvas.prototype.renderLoops = function(share) { + var curveHeight = Math.round(this.totalHeightOfConnections * share); + curveHeight = Math.max(curveHeight, 1); + + // create gradient + var gradient = this.context.createLinearGradient(this.leftCurveEndX - 50, 0, this.rightCurveBeginX + 50, 0); + var light = '#F5F3EB'; + var dark = '#E8E4D5'; + gradient.addColorStop(0, dark); + gradient.addColorStop(.5, light); + gradient.addColorStop(1, dark); + + this.context.fillStyle = gradient; + + this.context.beginPath(); + + // curve from the upper left connection to the center box to the lower left connection to the text box + var point1 = {x: this.leftCurveEndX, y: this.leftCurvePositionY}; + var point2 = {x: this.leftCurveEndX, y: 460}; + + var cpLeftX = (this.leftCurveBeginX + this.leftCurveEndX) / 2 + 30; + var cp1 = {x: cpLeftX, y: point1.y}; + var cp2 = {x: cpLeftX, y: point2.y}; + + this.context.moveTo(point1.x, point1.y); + this.context.bezierCurveTo(cp1.x, cp1.y, cp2.x, cp2.y, point2.x, point2.y); + + // lower line of text box + var point3 = {x: this.rightCurveBeginX, y: point2.y}; + this.context.lineTo(point3.x, point3.y); + + // curve to upper right connection to the center box + var point4 = {x: this.rightCurveBeginX, y: this.rightCurvePositionY}; + var cpRightX = (this.rightCurveBeginX + this.rightCurveEndX) / 2 - 30; + var cp3 = {x: cpRightX, y: point3.y}; + var cp4 = {x: cpRightX, y: point4.y}; + this.context.bezierCurveTo(cp3.x, cp3.y, cp4.x, cp4.y, point4.x, point4.y); + + // line to lower right connection to the center box + var point5 = {x: point4.x, y: point4.y + curveHeight}; + this.context.lineTo(point5.x, point5.y); + + // curve to upper right connection to the text box + var point6 = {x: point5.x, y: point2.y - 25}; + cpRightX -= 30; + var cp5 = {x: cpRightX, y: point5.y}; + var cp6 = {x: cpRightX, y: point6.y}; + this.context.bezierCurveTo(cp5.x, cp5.y, cp6.x, cp6.y, point6.x, point6.y); + + // upper line of the text box + var point7 = {x: point1.x, y: point6.y}; + this.context.lineTo(point7.x, point7.y); + + // line to lower left connection to the center box + var point8 = {x: point1.x, y: point1.y + + curveHeight}; + cpLeftX += 30; + var cp7 = {x: cpLeftX, y: point7.y}; + var cp8 = {x: cpLeftX, y: point8.y}; + this.context.bezierCurveTo(cp7.x, cp7.y, cp8.x, cp8.y, point8.x, point8.y); + + this.context.fill(); + + if (typeof this.context.endPath == 'function') { + this.context.endPath(); + } + +}; + +/** Clear one side for redrawing */ +Piwik_Transitions_Canvas.prototype.clearSide = function(side) { + var x = (side == 'left' ? 0 : this.width / 2); + var y = 0; + var w = this.width / 2; + var h = this.height; + + this.context.clearRect(x, y, w, h); + this.bgContext.clearRect(x, y, w, h); + + if (side == 'left') { + this.container.find('.Transitions_BoxTextLeft').remove(); + this.container.find('.Transitions_CurveTextLeft').remove(); + this.leftBoxPositionY = this.originalBoxPositionY; + this.leftCurvePositionY = this.originalCurvePositionY; + } else { + this.container.find('.Transitions_BoxTextRight').remove(); + this.container.find('.Transitions_CurveTextRight').remove(); + this.rightBoxPositionY = this.originalBoxPositionY; + this.rightCurvePositionY = this.originalCurvePositionY; + } +}; + + +// -------------------------------------- +// MODEL +// -------------------------------------- + +function Piwik_Transitions_Model(ajax) { + this.ajax = ajax; +} + +Piwik_Transitions_Model.prototype.loadData = function(link, callback) { + var self = this; + + this.pageviews = 0; + this.exits = 0; + this.bounces = 0; + this.loops = 0; + + this.directEntries = 0; + + this.searchEnginesNbTransitions = 0; + this.searchEngines = []; + + this.websitesNbTransitions = 0; + this.websites = []; + + this.campaignsNbTransitions = 0; + this.campaigns = []; + + this.previousPagesNbTransitions = 0; + this.previousPages = []; + + this.followingPagesNbTransitions = 0; + this.followingPages = []; + + this.downloadsNbTransitions = 0; + this.downloads = []; + + this.outlinksNbTransitions = 0; + this.outlinks = []; + + this.groupTitles = { + previousPages: Piwik_Transitions_Translations.fromPreviousPages, + followingPages: Piwik_Transitions_Translations.toFollowingPages, + outlinks: Piwik_Transitions_Translations.outlinks, + downloads: Piwik_Transitions_Translations.downloads + }; + + this.shareInGroupTexts = { + previousPages: Piwik_Transitions_Translations.fromPreviousPagesInline, + followingPages: Piwik_Transitions_Translations.toFollowingPagesInline, + searchEngines: Piwik_Transitions_Translations.fromSearchEnginesInline, + websites: Piwik_Transitions_Translations.fromWebsitesInline, + campaigns: Piwik_Transitions_Translations.fromCampaignsInline, + outlinks: Piwik_Transitions_Translations.outlinksInline, + downloads: Piwik_Transitions_Translations.downloadsInline + }; + + this.ajax.callApi('Transitions.getFullReport', { + pageUrl: link, + expanded: 1 + }, + function(report) { + // load page metrics + self.pageviews = report.pageMetrics.pageviews; + self.exits = report.pageMetrics.exits; + self.bounces = report.pageMetrics.bounces; + self.loops = report.pageMetrics.loops; + + // load referrers: split direct entries and others + for (var i = 0; i < report.referrers.length; i++) { + var referrer = report.referrers[i]; + if (referrer.shortName == 'direct') { + self.directEntries = referrer.visits; + } else if (referrer.shortName == 'search') { + self.searchEnginesNbTransitions = referrer.visits; + self.searchEngines = referrer.details; + self.groupTitles.searchEngines = referrer.label; + } else if (referrer.shortName == 'website') { + self.websitesNbTransitions = referrer.visits; + self.websites = referrer.details; + self.groupTitles.websites = referrer.label; + } else if (referrer.shortName == 'campaign') { + self.campaignsNbTransitions = referrer.visits; + self.campaigns = referrer.details; + self.groupTitles.campaigns = referrer.label; + } + } + + self.loadAndSumReport(report, 'previousPages'); + self.loadAndSumReport(report, 'followingPages'); + self.loadAndSumReport(report, 'downloads'); + self.loadAndSumReport(report, 'outlinks'); + + callback(); + }); +}; + +Piwik_Transitions_Model.prototype.loadAndSumReport = function(apiData, reportName) { + var data = this[reportName] = apiData[reportName]; + var sumVarName = reportName + 'NbTransitions'; + + this[sumVarName] = 0; + for (var i = 0; i < data.length; i++) { + this[sumVarName] += data[i].referrals; + } +}; + +Piwik_Transitions_Model.prototype.getGroupTitle = function(groupName) { + if (typeof this.groupTitles[groupName] != 'undefined') { + return this.groupTitles[groupName]; + } + return groupName; +}; + +Piwik_Transitions_Model.prototype.getShareInGroupTooltip = function(share, groupName) { + var tip = this.shareInGroupTexts[groupName]; + return tip.replace(/%s/, share); +}; + +Piwik_Transitions_Model.prototype.getDetailsForGroup = function(groupName) { + return this.addPercentagesToData(this[groupName]); +}; + +Piwik_Transitions_Model.prototype.getPercentage = function(metric, formatted) { + var percentage = (this.pageviews == 0 ? 0 : this[metric] / this.pageviews); + + if (formatted) { + percentage = this.roundPercentage(percentage); + percentage += '%'; + } + + return percentage; +}; + +Piwik_Transitions_Model.prototype.addPercentagesToData = function(data) { + var total = 0; + + for (var i = 0; i < data.length; i++) { + total += parseInt(data[i].referrals, 10); + } + + for (i = 0; i < data.length; i++) { + data[i].percentage = this.roundPercentage(data[i].referrals / total); + } + + return data; +}; + +Piwik_Transitions_Model.prototype.roundPercentage = function(value) { + if (value < .1) { + return Math.round(value * 1000) / 10.0; + } else { + return Math.round(value * 100); + } +}; + + +// -------------------------------------- +// AJAX +// -------------------------------------- + +function Piwik_Transitions_Ajax() { +} + +Piwik_Transitions_Ajax.prototype.callTransitionsController = function(action, callback) { + $.post('index.php', { + module: 'Transitions', + action: action, + date: piwik.currentDateString, + idSite: piwik.idSite, + period: piwik.period + }, callback); +}; + +Piwik_Transitions_Ajax.prototype.callApi = function(method, params, callback) { + params.module = 'API'; + params.method = method; + params.date = piwik.currentDateString; + params.idSite = piwik.idSite; + params.period = piwik.period; + params.token_auth = piwik.token_auth; + params.format = 'JSON'; + if (params.period == 'range') { + params.date = piwik.startDateString + ',' + params.date; + } + + $.post('index.php', params, function(result) { + if (typeof result.result != 'undefined' && result.result == 'error') { + alert(result.message); + } else { + callback(result); + } + }, 'json'); +}; + + + +// -------------------------------------- +// STATIC UTIL FUNCTIONS +// -------------------------------------- + +Piwik_Transitions_Util = { + + /** + * Removes protocol, www and trailing slashes from a URL. + * If removeDomain is set, the domain is removed as well. + */ + shortenUrl: function(url, removeDomain) { + if (url == 'Others') { + return url; + } + url = url.replace(/http(s)?:\/\/(www\.)?/, ''); + if (removeDomain) { + var urlBackup = url; + url = url.replace(/[^\/]*/, ''); + if (url == '/') { + url = urlBackup; + } + } + url = url.replace(/\/$/, ''); + return url; + }, + + /** Add break points to string so that it can be displayed more compactly */ + addBreakpoints: function(text) { + return text.replace(/([\/&=?\.%#:])/g, '$1<wbr>'); + }, + + /** + * Replaces a %s placeholder in the HTML. + * The special feature is that it can be called multiple times, replacing the already + * replaced placeholder again. It creates a span that can be assigned a class using the + * spanClass parameter. The default class is 'Transitions_Metric'. + */ + replacePlaceholderInHtml: function(container, value, spanClass) { + var span = container.find('span'); + if (span.size() == 0) { + var html = container.html().replace(/%s/, '<span></span>'); + span = container.html(html).find('span'); + if (!spanClass) { + spanClass = 'Transitions_Metric'; + } + span.addClass(spanClass); + } + span.html(value); + } + +}; diff --git a/plugins/Transitions/templates/transitions.tpl b/plugins/Transitions/templates/transitions.tpl new file mode 100644 index 0000000000..7dd284e266 --- /dev/null +++ b/plugins/Transitions/templates/transitions.tpl @@ -0,0 +1,43 @@ + +<div id="Transitions_Container"> + <div id="Transitions_CenterBox" class="Transitions_Text Transitions_Loading"> + <h2></h2> + <div class="Transitions_CenterBoxMetrics"> + <p class="Transitions_Pageviews Transitions_Margin">{$translations.pageviewsInline|translate}</p> + + <div class="Transitions_IncomingTraffic"> + <h3>{'Transitions_IncomingTraffic'|translate}</h3> + <p class="Transitions_DirectEntries">{$translations.directEntriesInline|translate} </p> + <p class="Transitions_PreviousPages">{$translations.fromPreviousPagesInline|translate}</p> + <p class="Transitions_SearchEngines">{$translations.fromSearchEnginesInline|translate}</p> + <p class="Transitions_Websites">{$translations.fromWebsitesInline|translate}</p> + </div> + + <div class="Transitions_OutgoingTraffic"> + <h3>{'Transitions_OutgoingTraffic'|translate}</h3> + <p class="Transitions_FollowingPages">{$translations.toFollowingPagesInline|translate}</p> + <p class="Transitions_Downloads">{$translations.downloadsInline|translate}</p> + <p class="Transitions_Outlinks">{$translations.outlinksInline|translate}</p> + <p> + <span class="Transitions_Exits">{$translations.exitsInline|translate}</span>, + {'Transitions_Including'|translate}<br /> + <span class="Transitions_Bounces">{$translations.bouncesInline|translate}</span> + </p> + </div> + </div> + </div> + <div id="Transitions_Loops" class="Transitions_Text"> + {$translations.loopsInline|translate} + </div> + <canvas id="Transitions_Canvas_Background"></canvas> + <canvas id="Transitions_Canvas"></canvas> +</div> + +<script type="text/javascript"> + var Piwik_Transitions_Translations = {literal}{{/literal} + {foreach from=$translations key=internalKey item=translationKey} + "{$internalKey}": "{$translationKey|translate}", + {/foreach} + "": "" + {literal}}{/literal}; +</script>
\ No newline at end of file diff --git a/plugins/Transitions/templates/transitions_rowaction.png b/plugins/Transitions/templates/transitions_rowaction.png Binary files differnew file mode 100755 index 0000000000..5e31bbe260 --- /dev/null +++ b/plugins/Transitions/templates/transitions_rowaction.png |