Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/matomo-org/matomo.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBeezyT <timo@ezdesign.de>2012-09-21 13:19:53 +0400
committerBeezyT <timo@ezdesign.de>2012-09-21 13:19:53 +0400
commit5e7688106afd64a6f063dc2ef284e5578cf1adfd (patch)
tree05de26f79dc30ea681946b2165c51f583e7af92a /plugins/Transitions
parent8aedd18eb179b68a704e215581f2e56ccfb83f96 (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.php220
-rw-r--r--plugins/Transitions/Controller.php74
-rw-r--r--plugins/Transitions/Transitions.php239
-rw-r--r--plugins/Transitions/templates/transitions.css176
-rw-r--r--plugins/Transitions/templates/transitions.js1078
-rw-r--r--plugins/Transitions/templates/transitions.tpl43
-rwxr-xr-xplugins/Transitions/templates/transitions_rowaction.pngbin0 -> 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
new file mode 100755
index 0000000000..5e31bbe260
--- /dev/null
+++ b/plugins/Transitions/templates/transitions_rowaction.png
Binary files differ