diff --git a/config/global.ini.php b/config/global.ini.php
index 43b0605b3928b4e5a73ee81bfdd46fe6f28b3b3a..98ab17f30b0387726204ec68a33a53d8a13b8327 100644
--- a/config/global.ini.php
+++ b/config/global.ini.php
@@ -472,6 +472,17 @@ enable_auto_update = 1
 ; If set to 0 it also disables the "sent plugin update emails" feature in general and the related setting in the UI.
 enable_update_communication = 1
 
+; This controls whether the pivotBy query parameter can be used with any dimension or just subtable
+; dimensions. If set to 1, it will fetch a report with a segment for each row of the table being pivoted.
+; At present, this is very inefficient, so it is disabled by default.
+pivot_by_filter_enable_fetch_by_segment = 0
+
+; This controls the default maximum number of columns to display in a pivot table. Since a pivot table displays
+; a table's rows as columns, the number of columns can become very large, which will affect webpage layouts.
+; Set to -1 to specify no limit. Note: The pivotByColumnLimit query parameter can be used to override this default
+; on a per-request basis;
+pivot_by_filter_default_column_limit = 10
+
 [Tracker]
 ; Piwik uses first party cookies by default. If set to 1,
 ; the visit ID cookie will be set on the Piwik server domain as well
diff --git a/core/API/DocumentationGenerator.php b/core/API/DocumentationGenerator.php
index 5e8b0d1e41281ab4a0438d6d3a3da3aa35a6f903..668f97bb6064fea0561187bec506fad8cc7bce53 100644
--- a/core/API/DocumentationGenerator.php
+++ b/core/API/DocumentationGenerator.php
@@ -190,6 +190,11 @@ class DocumentationGenerator
         $aParameters['hideColumns'] = false;
         $aParameters['showColumns'] = false;
         $aParameters['filter_pattern_recursive'] = false;
+        $aParameters['pivotBy'] = false;
+        $aParameters['pivotByColumn'] = false;
+        $aParameters['pivotByColumnLimit'] = false;
+        $aParameters['disable_queued_filters'] = false;
+        $aParameters['disable_generic_filters'] = false;
 
         $moduleName = Proxy::getInstance()->getModuleNameFromClassName($class);
         $aParameters = array_merge(array('module' => 'API', 'method' => $moduleName . '.' . $methodName), $aParameters);
diff --git a/core/API/ResponseBuilder.php b/core/API/ResponseBuilder.php
index f5af55191708e2573ab8b56980406c44c73b679c..17d5d488f7ad8ae9c180230be8b6524dd612cf59 100644
--- a/core/API/ResponseBuilder.php
+++ b/core/API/ResponseBuilder.php
@@ -14,6 +14,7 @@ use Piwik\API\DataTableManipulator\LabelFilter;
 use Piwik\API\DataTableManipulator\ReportTotalsCalculator;
 use Piwik\Common;
 use Piwik\DataTable;
+use Piwik\DataTable\Filter\PivotByDimension;
 use Piwik\DataTable\Renderer;
 use Piwik\DataTable\DataTableInterface;
 use Piwik\DataTable\Filter\ColumnDelete;
@@ -157,10 +158,21 @@ class ResponseBuilder
         return Renderer::formatValueXml($message);
     }
 
-    protected function handleDataTable($datatable)
+    protected function handleDataTable(DataTableInterface $datatable)
     {
         $label = $this->getLabelFromRequest($this->request);
 
+        // handle pivot by dimension filter
+        $pivotBy = Common::getRequestVar('pivotBy', false, 'string', $this->request);
+        if (!empty($pivotBy)) {
+            $reportId = $this->apiModule . '.' . $this->apiMethod;
+            $pivotByColumn = Common::getRequestVar('pivotByColumn', false, 'string', $this->request);
+            $pivotByColumnLimit = Common::getRequestVar('pivotByColumnLimit', false, 'int', $this->request);
+
+            $datatable->filter('PivotByDimension', array($reportId, $pivotBy, $pivotByColumn, $pivotByColumnLimit,
+                PivotByDimension::isSegmentFetchingEnabledInConfig()));
+        }
+
         // if requested, flatten nested tables
         if (Common::getRequestVar('flat', '0', 'string', $this->request) == '1') {
             $flattener = new Flattener($this->apiModule, $this->apiMethod, $this->request);
diff --git a/core/Columns/Dimension.php b/core/Columns/Dimension.php
index c2b39093e4fbbdd9a5cfda56d99740450fae592e..a6f8831a71d9be60c216c64021839cfc945a7ef3 100644
--- a/core/Columns/Dimension.php
+++ b/core/Columns/Dimension.php
@@ -195,10 +195,29 @@ abstract class Dimension
      * @return Dimension|null The created instance or null if there is no Dimension for
      *                        $dimensionId or if the plugin that contains the Dimension is
      *                        not loaded.
+     * @api
      */
     public static function factory($dimensionId)
     {
         list($module, $dimension) = explode('.', $dimensionId);
         return ComponentFactory::factory($module, $dimension, __CLASS__);
     }
-}
+
+    /**
+     * Returns the name of the plugin that contains this Dimension.
+     *
+     * @return string
+     * @throws Exception if the Dimension is not located within a Plugin module.
+     * @api
+     */
+    public function getModule()
+    {
+        $id = $this->getId();
+        if (empty($id)) {
+            throw new Exception("Invalid dimension ID: '$id'.");
+        }
+
+        $parts = explode('.', $id);
+        return reset($parts);
+    }
+}
\ No newline at end of file
diff --git a/core/DataTable.php b/core/DataTable.php
index 3049dbf9669f97584edeef12a96ed76eec16df95..bc5a57cff664988a623f4b8dac84f609b690f4a2 100644
--- a/core/DataTable.php
+++ b/core/DataTable.php
@@ -452,7 +452,7 @@ class DataTable implements DataTableInterface, \IteratorAggregate, \ArrayAccess
         foreach ($this->queuedFilters as $filter) {
             $this->filter($filter['className'], $filter['parameters']);
         }
-        $this->queuedFilters = array();
+        $this->clearQueuedFilters();
     }
 
     /**
@@ -1634,6 +1634,14 @@ class DataTable implements DataTableInterface, \IteratorAggregate, \ArrayAccess
         $thisRow->sumRow($row, $copyMeta = true, $this->getMetadata(self::COLUMN_AGGREGATION_OPS_METADATA_NAME));
     }
 
+    /**
+     * Unsets all queued filters.
+     */
+    public function clearQueuedFilters()
+    {
+        $this->queuedFilters = array();
+    }
+
     /**
      * @return \ArrayIterator|Row[]
      */
@@ -1662,4 +1670,4 @@ class DataTable implements DataTableInterface, \IteratorAggregate, \ArrayAccess
     {
         $this->deleteRow($offset);
     }
-}
\ No newline at end of file
+}
diff --git a/core/DataTable/Filter/PivotByDimension.php b/core/DataTable/Filter/PivotByDimension.php
new file mode 100644
index 0000000000000000000000000000000000000000..225a33f0bbdf3434127f14143e694fba0d56fc54
--- /dev/null
+++ b/core/DataTable/Filter/PivotByDimension.php
@@ -0,0 +1,536 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ *
+ */
+namespace Piwik\DataTable\Filter;
+
+use Exception;
+use Piwik\Columns\Dimension;
+use Piwik\Common;
+use Piwik\Config;
+use Piwik\DataTable;
+use Piwik\DataTable\BaseFilter;
+use Piwik\DataTable\Row;
+use Piwik\Log;
+use Piwik\Metrics;
+use Piwik\Piwik;
+use Piwik\Plugin\Report;
+use Piwik\Plugin\Segment;
+
+/**
+ * DataTable filter that creates a pivot table from a report.
+ *
+ * A pivot table is a table that displays one metric value for two dimensions. The rows of
+ * the table represent one dimension and the columns another.
+ *
+ * This filter can pivot any report by any dimension as long as either:
+ *
+ * - the pivot-by dimension is the dimension of the report's subtable
+ * - or, the pivot-by dimension has an associated report, and the report to pivot has a dimension with
+ *   a segment
+ *
+ * Reports are pivoted by iterating over the rows of the report, fetching the pivot-by report
+ * for the current row, and setting the columns of row to the rows of the pivot-by report. For example:
+ *
+ * to pivot Referrers.getKeywords by UserCountry.City, we first loop through the Referrers.getKeywords
+ * report's rows. For each row, we take the label (which is the referrer keyword), and get the
+ * UserCountry.getCity report using the referrerKeyword=... segment. If the row's label were 'abcdefg',
+ * we would use the 'referrerKeyword==abcdefg' segment.
+ *
+ * The UserCountry.getCity report we find is the report on visits by country, but only for the visits
+ * for the specific row. We take this report's row labels and add them as columns for the Referrers.getKeywords
+ * table.
+ *
+ * Implementation details:
+ *
+ * Fetching intersected table can be done by segment or subtable. If the requested pivot by
+ * dimension is the report's subtable dimension, then the subtable is used regardless, since it
+ * is much faster than fetching by segment.
+ *
+ * Also, by default, fetching by segment is disabled in the config (see the
+ * '[General] pivot_by_filter_enable_fetch_by_segment' option).
+ */
+class PivotByDimension extends BaseFilter
+{
+    /**
+     * The pivot-by Dimension. The metadata in this class is used to determine if we can
+     * pivot the report and used to fetch intersected tables.
+     *
+     * @var Dimension
+     */
+    private $pivotByDimension;
+
+    /**
+     * The report that reports on visits by the pivot dimension. The metadata in this class
+     * is used to determine if we can pivot the report and used to fetch intersected tables
+     * by segment.
+     *
+     * @var Report
+     */
+    private $pivotDimensionReport;
+
+    /**
+     * The column that should be displayed in the pivot table. This should be a metric, eg,
+     * `'nb_visits'`, `'nb_actions'`, etc.
+     *
+     * @var string
+     */
+    private $pivotColumn;
+
+    /**
+     * The number of columns to limit the pivot table to. Applying a pivot can result in
+     * tables with many, many columns. This can cause problems when displayed in web page.
+     *
+     * A default limit of 7 is imposed if no column limit is specified in construction.
+     * If a negative value is supplied, no limiting is performed.
+     *
+     * Columns are summed and sorted before being limited so the columns w/ the most
+     * visits will be displayed and the columns w/ the least will be cut off.
+     *
+     * @var int
+     */
+    private $pivotByColumnLimit;
+
+    /**
+     * Metadata for the report being pivoted. The metadata in this class is used to
+     * determine if we can pivot the report and used to fetch intersected tables.
+     *
+     * @var Report
+     */
+    private $thisReport;
+
+    /**
+     * Metadata for the segment of the dimension of the report being pivoted. When
+     * fetching intersected tables by segment, this is the segment used.
+     *
+     * @var Segment
+     */
+    private $thisReportDimensionSegment;
+
+    /**
+     * Whether fetching by segment is enabled or not.
+     *
+     * @var bool
+     */
+    private $isFetchingBySegmentEnabled;
+
+    /**
+     * The subtable dimension of the report being pivoted. Used to determine if and
+     * how intersected tables are fetched.
+     *
+     * @var Dimension|null
+     */
+    private $subtableDimension;
+
+    /**
+     * The index value (if any) for the metric that should be displayed in the pivot
+     * table.
+     *
+     * @var int|null
+     */
+    private $metricIndexValue;
+
+    /**
+     * Constructor.
+     *
+     * @param DataTable $table The table to pivot.
+     * @param string $report The ID of the report being pivoted, eg, `'Referrers.getKeywords'`.
+     * @param string $pivotByDimension The ID of the dimension to pivot by, eg, `'Referrers.Keyword'`.
+     * @param string|false $pivotColumn The metric that should be displayed in the pivot table, eg, `'nb_visits'`.
+     *                                  If `false`, the first non-label column is used.
+     * @param false|int $pivotByColumnLimit The number of columns to limit the pivot table to.
+     * @param bool $isFetchingBySegmentEnabled Whether to allow fetching by segment.
+     * @throws Exception if pivoting the report by a dimension is unsupported.
+     */
+    public function __construct($table, $report, $pivotByDimension, $pivotColumn, $pivotByColumnLimit = false,
+                                $isFetchingBySegmentEnabled = true)
+    {
+        parent::__construct($table);
+
+        Log::debug("PivotByDimension::%s: creating with [report = %s, pivotByDimension = %s, pivotColumn = %s, "
+            . "pivotByColumnLimit = %s, isFetchingBySegmentEnabled = %s]", __FUNCTION__, $report, $pivotByDimension,
+            $pivotColumn, $pivotByColumnLimit, $isFetchingBySegmentEnabled);
+
+        $this->pivotColumn = $pivotColumn;
+        $this->pivotByColumnLimit = $pivotByColumnLimit ?: self::getDefaultColumnLimit();
+        $this->isFetchingBySegmentEnabled = $isFetchingBySegmentEnabled;
+
+        $namesToId = Metrics::getMappingFromIdToName();
+        $this->metricIndexValue = isset($namesToId[$this->pivotColumn]) ? $namesToId[$this->pivotColumn] : null;
+
+        $this->setPivotByDimension($pivotByDimension);
+        $this->setThisReportMetadata($report);
+
+        $this->checkSupportedPivot();
+    }
+
+    /**
+     * Pivots to table.
+     *
+     * @param DataTable $table The table to manipulate.
+     */
+    public function filter($table)
+    {
+        // set of all column names in the pivoted table mapped with the sum of all column
+        // values. used later in truncating and ordering the pivoted table's columns.
+        $columnSet = array();
+
+        // if no pivot column was set, use the first one found in the row
+        if (empty($this->pivotColumn)) {
+            $this->pivotColumn = $this->getNameOfFirstNonLabelColumnInTable($table);
+        }
+
+        Log::debug("PivotByDimension::%s: pivoting table with pivot column = %s", __FUNCTION__, $this->pivotColumn);
+
+        foreach ($table->getRows() as $row) {
+            $row->setColumns(array('label' => $row->getColumn('label')));
+
+            $associatedTable = $this->getIntersectedTable($table, $row);
+            if (!empty($associatedTable)) {
+                foreach ($associatedTable->getRows() as $columnRow) {
+                    $pivotTableColumn = $columnRow->getColumn('label');
+
+                    $columnValue = $this->getColumnValue($columnRow, $this->pivotColumn);
+
+                    if (isset($columnSet[$pivotTableColumn])) {
+                        $columnSet[$pivotTableColumn] += $columnValue;
+                    } else {
+                        $columnSet[$pivotTableColumn] = $columnValue;
+                    }
+
+                    $row->setColumn($pivotTableColumn, $columnValue);
+                }
+
+                Common::destroy($associatedTable);
+                unset($associatedTable);
+            }
+        }
+
+        Log::debug("PivotByDimension::%s: pivoted columns set: %s", __FUNCTION__, $columnSet);
+
+        $others = Piwik::translate('General_Others');
+        $defaultRow = $this->getPivotTableDefaultRowFromColumnSummary($columnSet, $others);
+
+        Log::debug("PivotByDimension::%s: un-prepended default row: %s", __FUNCTION__, $defaultRow);
+
+        // post process pivoted datatable
+        foreach ($table->getRows() as $row) {
+            // remove subtables from rows
+            $row->removeSubtable();
+            $row->deleteMetadata('idsubdatatable_in_db');
+
+            // use default row to ensure column ordering and add missing columns/aggregate cut-off columns
+            $orderedColumns = $defaultRow;
+            foreach ($row->getColumns() as $name => $value) {
+                if (isset($orderedColumns[$name])) {
+                    $orderedColumns[$name] = $value;
+                } else {
+                    $orderedColumns[$others] += $value;
+                }
+            }
+            $row->setColumns($orderedColumns);
+        }
+
+        $table->clearQueuedFilters(); // TODO: shouldn't clear queued filters, but we can't wait for them to be run
+                                      //       since generic filters are run before them. remove after refactoring
+                                      //       processed metrics.
+
+        // prepend numerals to columns in a queued filter (this way, disable_queued_filters can be used
+        // to get machine readable data from the API if needed)
+        $prependedColumnNames = $this->getOrderedColumnsWithPrependedNumerals($defaultRow, $others);
+
+        Log::debug("PivotByDimension::%s: prepended column name mapping: %s", __FUNCTION__, $prependedColumnNames);
+
+        $table->queueFilter(function (DataTable $table) use ($prependedColumnNames) {
+            foreach ($table->getRows() as $row) {
+                $row->setColumns(array_combine($prependedColumnNames, $row->getColumns()));
+            }
+        });
+    }
+
+    /**
+     * An intersected table is a table that describes visits by a certain dimension for the visits
+     * represented by a row in another table. This method fetches intersected tables either via
+     * subtable or by using a segment. Read the class docs for more info.
+     */
+    private function getIntersectedTable(DataTable $table, Row $row)
+    {
+        if ($this->isPivotDimensionSubtable()) {
+            return $this->loadSubtable($table, $row);
+        }
+
+        if ($this->isFetchingBySegmentEnabled) {
+            $segmentValue = $row->getColumn('label');
+            return $this->fetchIntersectedWithThisBySegment($table, $segmentValue);
+        }
+
+        // should never occur, unless checkSupportedPivot() fails to catch an unsupported pivot
+        throw new Exception("Unexpected error, cannot fetch intersected table.");
+    }
+
+    private function isPivotDimensionSubtable()
+    {
+        return self::areDimensionsEqualAndNotNull($this->subtableDimension, $this->pivotByDimension);
+    }
+
+    private function loadSubtable(DataTable $table, Row $row)
+    {
+        $idSubtable = $row->getIdSubDataTable();
+        if ($idSubtable === null) {
+            return null;
+        }
+
+        if ($row->isSubtableLoaded()) {
+            $subtable = $row->getSubtable();
+        } else {
+            $subtable = $this->thisReport->fetchSubtable($idSubtable, $this->getRequestParamOverride($table));
+        }
+
+        if ($subtable === null) { // sanity check
+            throw new Exception("Unexpected error: could not load subtable '$idSubtable'.");
+        }
+
+        return $subtable;
+    }
+
+    private function fetchIntersectedWithThisBySegment(DataTable $table, $segmentValue)
+    {
+        $segmentStr = $this->thisReportDimensionSegment->getSegment() . "==" . urlencode($segmentValue);
+
+        // TODO: segment + report API method query params should be stored in DataTable metadata so we don't have to access it here
+        $originalSegment = Common::getRequestVar('segment', false);
+        if (!empty($originalSegment)) {
+            $segmentStr = $originalSegment . ';' . $segmentStr;
+        }
+
+        Log::debug("PivotByDimension: Fetching intersected with segment '%s'", $segmentStr);
+
+        $params = array('segment' => $segmentStr) + $this->getRequestParamOverride($table);
+        return $this->pivotDimensionReport->fetch($params);
+    }
+
+    private function setPivotByDimension($pivotByDimension)
+    {
+        $this->pivotByDimension = Dimension::factory($pivotByDimension);
+        if (empty($this->pivotByDimension)) {
+            throw new Exception("Invalid dimension '$pivotByDimension'.");
+        }
+
+        $this->pivotDimensionReport = Report::getForDimension($this->pivotByDimension);
+    }
+
+    private function setThisReportMetadata($report)
+    {
+        list($module, $method) = explode('.', $report);
+
+        $this->thisReport = Report::factory($module, $method);
+        if (empty($this->thisReport)) {
+            throw new Exception("Unable to find report '$report'.");
+        }
+
+        $this->subtableDimension = $this->thisReport->getSubtableDimension();
+
+        $thisReportDimension = $this->thisReport->getDimension();
+        if ($thisReportDimension !== null) {
+            $segments = $thisReportDimension->getSegments();
+            $this->thisReportDimensionSegment = reset($segments);
+        }
+    }
+
+    private function checkSupportedPivot()
+    {
+        $reportId = $this->thisReport->getModule() . '.' . $this->thisReport->getName();
+
+        if (!$this->isFetchingBySegmentEnabled) {
+            // if fetching by segment is disabled, then there must be a subtable for the current report and
+            // subtable's dimension must be the pivot dimension
+
+            if (empty($this->subtableDimension)) {
+                throw new Exception("Unsupported pivot: report '$reportId' has no subtable dimension.");
+            }
+
+            if (!$this->isPivotDimensionSubtable()) {
+                throw new Exception("Unsupported pivot: the subtable dimension for '$reportId' does not match the "
+                                  . "requested pivotBy dimension. [subtable dimension = {$this->subtableDimension->getId()}, "
+                                  . "pivot by dimension = {$this->pivotByDimension->getId()}]");
+            }
+        } else {
+            $canFetchBySubtable = !empty($this->subtableDimension)
+                && $this->subtableDimension->getId() === $this->pivotByDimension->getId();
+            if ($canFetchBySubtable) {
+                return;
+            }
+
+            // if fetching by segment is enabled, and we cannot fetch by subtable, then there has to be a report
+            // for the pivot dimension (so we can fetch the report), and there has to be a segment for this report's
+            // dimension (so we can use it when fetching)
+
+            if (empty($this->pivotDimensionReport)) {
+                throw new Exception("Unsupported pivot: No report for pivot dimension '{$this->pivotByDimension->getId()}'"
+                                  . " (report required for fetching intersected tables by segment).");
+            }
+
+            if (empty($this->thisReportDimensionSegment)) {
+                throw new Exception("Unsupported pivot: No segment for dimension of report '$reportId'."
+                                  . " (segment required for fetching intersected tables by segment).");
+            }
+        }
+    }
+
+    /**
+     * @param $columnRow
+     * @param $pivotColumn
+     * @return false|mixed
+     */
+    private function getColumnValue(Row $columnRow, $pivotColumn)
+    {
+        $value = $columnRow->getColumn($pivotColumn);
+        if (empty($value)
+            && !empty($this->metricIndexValue)
+        ) {
+            $value = $columnRow->getColumn($this->metricIndexValue);
+        }
+        return $value;
+    }
+
+    private function getNameOfFirstNonLabelColumnInTable(DataTable $table)
+    {
+        foreach ($table->getRows() as $row) {
+            foreach ($row->getColumns() as $columnName => $ignore) {
+                if ($columnName != 'label') {
+                    return $columnName;
+                }
+            }
+        }
+    }
+
+    private function getRequestParamOverride(DataTable $table)
+    {
+        $params = array(
+            'pivotBy' => '',
+            'column' => '',
+            'flat' => 0,
+            'totals' => 0,
+            'disable_queued_filters' => 1,
+            'disable_generic_filters' => 1,
+            'showColumns' => '',
+            'hideColumns' => ''
+        );
+
+        $site = $table->getMetadata('site');
+        if (!empty($site)) {
+            $params['idSite'] = $site->getId();
+        }
+
+        $period = $table->getMetadata('period');
+        if (!empty($period)) {
+            $params['date'] = $period->getDateStart()->toString();
+            $params['period'] = $period->getLabel();
+        }
+
+        return $params;
+    }
+
+    private function getPivotTableDefaultRowFromColumnSummary($columnSet, $othersRowLabel)
+    {
+        // sort columns by sum (to ensure deterministic ordering)
+        arsort($columnSet);
+
+        // limit columns if necessary (adding aggregate Others column at end)
+        if ($this->pivotByColumnLimit > 0
+            && count($columnSet) > $this->pivotByColumnLimit
+        ) {
+            $columnSet = array_slice($columnSet, 0, $this->pivotByColumnLimit - 1, $preserveKeys = true);
+            $columnSet[$othersRowLabel] = 0;
+        }
+
+        // remove column sums from array so it can be used as a default row
+        $columnSet = array_map(function () { return false; }, $columnSet);
+
+        // make sure label column is first
+        $columnSet = array_merge(array('label' => false), $columnSet);
+
+        return $columnSet;
+    }
+
+    private function getOrderedColumnsWithPrependedNumerals($defaultRow, $othersRowLabel)
+    {
+        $nbsp = html_entity_decode('&nbsp;'); // must use decoded character otherwise sort later will fail
+                                              // (sort column will be set to decoded but columns will have &nbsp;)
+
+        $result = array();
+
+        $currentIndex = 1;
+        foreach ($defaultRow as $columnName => $ignore) {
+            if ($columnName == $othersRowLabel
+                || $columnName == 'label'
+            ) {
+                $result[] = $columnName;
+            } else {
+                $modifiedColumnName = $currentIndex . '.' . $nbsp . $columnName;
+                $result[] = $modifiedColumnName;
+
+                ++$currentIndex;
+            }
+        }
+
+        return $result;
+    }
+
+    /**
+     * Returns true if pivoting by subtable is supported for a report. Will return true if the report
+     * has a subtable dimension and if the subtable dimension is different than the report's dimension.
+     *
+     * @param Report $report
+     * @return bool
+     */
+    public static function isPivotingReportBySubtableSupported(Report $report)
+    {
+        return self::areDimensionsNotEqualAndNotNull($report->getSubtableDimension(), $report->getDimension());
+    }
+
+    /**
+     * Returns true if fetching intersected tables by segment is enabled in the INI config, false if otherwise.
+     *
+     * @return bool
+     */
+    public static function isSegmentFetchingEnabledInConfig()
+    {
+        return Config::getInstance()->General['pivot_by_filter_enable_fetch_by_segment'];
+    }
+
+    /**
+     * Returns the default maximum number of columns to allow in a pivot table from the INI config.
+     * Uses the **pivot_by_filter_default_column_limit** INI config option.
+     *
+     * @return int
+     */
+    public static function getDefaultColumnLimit()
+    {
+        return Config::getInstance()->General['pivot_by_filter_default_column_limit'];
+    }
+
+    /**
+     * @param Dimension|null $lhs
+     * @param Dimension|null $rhs
+     * @return bool
+     */
+    private static function areDimensionsEqualAndNotNull($lhs, $rhs)
+    {
+        return !empty($lhs) && !empty($rhs) && $lhs->getId() == $rhs->getId();
+    }
+
+    /**
+     * @param Dimension|null $lhs
+     * @param Dimension|null $rhs
+     * @return bool
+     */
+    private static function areDimensionsNotEqualAndNotNull($lhs, $rhs)
+    {
+        return !empty($lhs) && !empty($rhs) && $lhs->getId() != $rhs->getId();
+    }
+}
\ No newline at end of file
diff --git a/core/DataTable/Filter/Sort.php b/core/DataTable/Filter/Sort.php
index 6ba2cd1ff06c3eaaf606fcda02aae101f84004ab..683e93cffe9f766f7d5ab919a9fdbd89630d67cf 100644
--- a/core/DataTable/Filter/Sort.php
+++ b/core/DataTable/Filter/Sort.php
@@ -248,7 +248,6 @@ class Sort extends BaseFilter
                 $methodToUse = "sortString";
             }
         }
-
         $table->sort(array($this, $methodToUse), $this->columnToSort);
     }
 }
diff --git a/core/DataTable/Renderer/Csv.php b/core/DataTable/Renderer/Csv.php
index 064dbd6db14ba9cea2f2dc57140deb22d66975df..f75ef8d2151b8ee46455fc64a38d9e46d84edc2d 100644
--- a/core/DataTable/Renderer/Csv.php
+++ b/core/DataTable/Renderer/Csv.php
@@ -278,6 +278,11 @@ class Csv extends Renderer
         if ($this->translateColumnNames) {
             $columnMetrics = $this->translateColumnNames($columnMetrics);
         }
+
+        foreach ($columnMetrics as &$value) {
+            $value = $this->formatValue($value);
+        }
+
         return implode($this->separator, $columnMetrics);
     }
 
diff --git a/core/DataTable/Renderer/Xml.php b/core/DataTable/Renderer/Xml.php
index 9eb7db914b05ebd65b4509f9e034cb4ad4470548..7cd10d9367d5f7cf8522b30ad4290b215696ae4a 100644
--- a/core/DataTable/Renderer/Xml.php
+++ b/core/DataTable/Renderer/Xml.php
@@ -154,17 +154,16 @@ class Xml extends Renderer
         foreach ($array as $key => $value) {
             // based on the type of array & the key, determine how this node will look
             if ($isAssociativeArray) {
-                $keyIsInvalidXmlElement = is_numeric($key) || is_numeric($key[0]);
-                if ($keyIsInvalidXmlElement) {
-                    $prefix = "<row key=\"$key\">";
-                    $suffix = "</row>";
-                    $emptyNode = "<row key=\"$key\"/>";
-                } else if (strpos($key, '=') !== false) {
+                if (strpos($key, '=') !== false) {
                     list($keyAttributeName, $key) = explode('=', $key, 2);
 
                     $prefix = "<row $keyAttributeName=\"$key\">";
                     $suffix = "</row>";
                     $emptyNode = "<row $keyAttributeName=\"$key\">";
+                } else if (!self::isValidXmlTagName($key)) {
+                    $prefix = "<row key=\"$key\">";
+                    $suffix = "</row>";
+                    $emptyNode = "<row key=\"$key\"/>";
                 } else {
                     $prefix = "<$key>";
                     $suffix = "</$key>";
@@ -338,6 +337,8 @@ class Xml extends Renderer
      */
     protected function renderDataTable($array, $prefixLine = "")
     {
+        $columnsHaveInvalidChars = $this->areTableLabelsInvalidXmlTagNames(reset($array));
+
         $out = '';
         foreach ($array as $rowId => $row) {
             if (!is_array($row)) {
@@ -373,10 +374,13 @@ class Xml extends Renderer
                     } else {
                         $value = self::formatValueXml($value);
                     }
+
+                    list($tagStart, $tagEnd) = $this->getTagStartAndEndFor($name, $columnsHaveInvalidChars);
+
                     if (strlen($value) == 0) {
-                        $out .= $prefixLine . "\t\t<$name />\n";
+                        $out .= $prefixLine . "\t\t<$tagStart />\n";
                     } else {
-                        $out .= $prefixLine . "\t\t<$name>" . $value . "</$name>\n";
+                        $out .= $prefixLine . "\t\t<$tagStart>" . $value . "</$tagEnd>\n";
                     }
                 }
                 $out .= "\t";
@@ -399,15 +403,62 @@ class Xml extends Renderer
             $array = array('value' => $array);
         }
 
+        $columnsHaveInvalidChars = $this->areTableLabelsInvalidXmlTagNames($array);
+
         $out = '';
         foreach ($array as $keyName => $value) {
             $xmlValue = self::formatValueXml($value);
+            list($tagStart, $tagEnd) = $this->getTagStartAndEndFor($keyName, $columnsHaveInvalidChars);
             if (strlen($xmlValue) == 0) {
-                $out .= $prefixLine . "\t<$keyName />\n";
+                $out .= $prefixLine . "\t<$tagStart />\n";
             } else {
-                $out .= $prefixLine . "\t<$keyName>" . $xmlValue . "</$keyName>\n";
+                $out .= $prefixLine . "\t<$tagStart>" . $xmlValue . "</$tagEnd>\n";
             }
         }
         return $out;
     }
-}
+
+    /**
+     * Returns true if a string is a valid XML tag name, false if otherwise.
+     *
+     * @param string $str
+     * @return bool
+     */
+    private static function isValidXmlTagName($str)
+    {
+        static $validTagRegex = null;
+
+        if ($validTagRegex === null) {
+            $invalidTagChars = "!\"#$%&'()*+,\\/;<=>?@[\\]\\\\^`{|}~";
+            $invalidTagStartChars = $invalidTagChars . "\\-.0123456789";
+            $validTagRegex = "/^[^" . $invalidTagStartChars . "][^" . $invalidTagChars . "]*$/";
+        }
+
+        $result = preg_match($validTagRegex, $str);
+        return !empty($result);
+    }
+
+    private function areTableLabelsInvalidXmlTagNames($rowArray)
+    {
+        if (!empty($rowArray)) {
+            foreach ($rowArray as $name => $value) {
+                if (!self::isValidXmlTagName($name)) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    private function getTagStartAndEndFor($keyName, $columnsHaveInvalidChars)
+    {
+        if ($columnsHaveInvalidChars) {
+            $tagStart = "col name=\"" . self::formatValueXml($keyName) . "\"";
+            $tagEnd = "col";
+        } else {
+            $tagStart = $tagEnd = $keyName;
+        }
+
+        return array($tagStart, $tagEnd);
+    }
+}
\ No newline at end of file
diff --git a/core/EventDispatcher.php b/core/EventDispatcher.php
index ce0815c6baa1faf193855682a374cc523c15493e..d9d26b437763b494eeed05480c4e18e64e05c187 100644
--- a/core/EventDispatcher.php
+++ b/core/EventDispatcher.php
@@ -201,12 +201,14 @@ class EventDispatcher extends Singleton
     }
 
     /**
-     * TODO
+     * Returns the Plugin\Manager instance used by the event dispatcher.
+     *
+     * @return Plugin\Manager
      */
     private function getPluginManager()
     {
         if ($this->pluginManager === null) {
-            $this->pluginManager = \Piwik\Plugin\Manager::getInstance();
+            $this->pluginManager = Plugin\Manager::getInstance();
         }
         return $this->pluginManager;
     }
diff --git a/core/Metrics.php b/core/Metrics.php
index c88bb1329420a4e2e4ee4dff374896c48aaa976e..03b9e2e0946cabc5f909bfa49f5a805610909108 100644
--- a/core/Metrics.php
+++ b/core/Metrics.php
@@ -181,6 +181,7 @@ class Metrics
         return $names;
     }
 
+    // TODO: this method is named wrong
     public static function getMappingFromIdToName()
     {
         $idToName = array_flip(self::$mappingFromIdToName);
diff --git a/core/Plugin/ComponentFactory.php b/core/Plugin/ComponentFactory.php
index 02cb6083e1d5ea63111b14a28380c3c529616071..68415e939762840b7231d07bc203f8869f58da33 100644
--- a/core/Plugin/ComponentFactory.php
+++ b/core/Plugin/ComponentFactory.php
@@ -37,20 +37,14 @@ class ComponentFactory
     public static function factory($pluginName, $componentClassSimpleName, $componentTypeClass)
     {
         if (empty($pluginName) || empty($componentClassSimpleName)) {
+            Log::debug("ComponentFactory::%s: empty plugin name or component simple name requested (%s, %s)",
+                __FUNCTION__, $pluginName, $componentClassSimpleName);
+
             return null;
         }
 
-        $pluginManager = PluginManager::getInstance();
-
-        try {
-            if (!$pluginManager->isPluginActivated($pluginName)) {
-                return null;
-            }
-
-            $plugin = $pluginManager->getLoadedPlugin($pluginName);
-        } catch (Exception $e) {
-            Log::debug($e);
-
+        $plugin = self::getActivatedPlugin(__FUNCTION__, $pluginName);
+        if (empty($plugin)) {
             return null;
         }
 
@@ -63,6 +57,75 @@ class ComponentFactory
                 return new $class();
             }
         }
+
+        Log::debug("ComponentFactory::%s: Could not find requested component (args = ['%s', '%s', '%s']).",
+            __FUNCTION__, $pluginName, $componentClassSimpleName, $componentTypeClass);
+
+        return null;
+    }
+
+    /**
+     * Finds a component instance that satisfies a given predicate.
+     *
+     * @param string $componentTypeClass The fully qualified class name of the component type, eg,
+     *                                   `"Piwik\Plugin\Report"`.
+     * @param string $pluginName|false The name of the plugin the component is expected to belong to,
+     *                                 eg, `'UserSettings'`.
+     * @param callback $predicate
+     * @return mixed The component that satisfies $predicate or null if not found.
+     */
+    public static function getComponentIf($componentTypeClass, $pluginName, $predicate)
+    {
+        $pluginManager = PluginManager::getInstance();
+
+        // get components to search through
+        $subnamespace = $componentTypeClass::COMPONENT_SUBNAMESPACE;
+        if (empty($pluginName)) {
+            $components = $pluginManager->findMultipleComponents($subnamespace, $componentTypeClass);
+        } else {
+            $plugin = self::getActivatedPlugin(__FUNCTION__, $pluginName);
+            if (empty($plugin)) {
+                return null;
+            }
+
+            $components = $plugin->findMultipleComponents($subnamespace, $componentTypeClass);
+        }
+
+        // find component that satisfieds predicate
+        foreach ($components as $class) {
+            $component = new $class();
+            if ($predicate($component)) {
+                return $component;
+            }
+        }
+
+        Log::debug("ComponentFactory::%s: Could not find component that satisfies predicate (args = ['%s', '%s', '%s']).",
+            __FUNCTION__, $componentTypeClass, $pluginName, get_class($predicate));
+
         return null;
     }
+
+    /**
+     * @param string $function
+     * @param string $pluginName
+     * @return null|\Piwik\Plugin
+     */
+    private static function getActivatedPlugin($function, $pluginName)
+    {
+        $pluginManager = PluginManager::getInstance();
+        try {
+            if (!$pluginManager->isPluginActivated($pluginName)) {
+                Log::debug("ComponentFactory::%s: component for deactivated plugin ('%s') requested.",
+                    $function, $pluginName);
+
+                return null;
+            }
+
+            return $pluginManager->getLoadedPlugin($pluginName);
+        } catch (Exception $e) {
+            Log::debug($e);
+
+            return null;
+        }
+    }
 }
\ No newline at end of file
diff --git a/core/Plugin/Report.php b/core/Plugin/Report.php
index d9ee86e5c34071d93405e53a27114888c0d07c1b..6d950babd2b5482b440b6093b138247b23e88f8e 100644
--- a/core/Plugin/Report.php
+++ b/core/Plugin/Report.php
@@ -9,7 +9,10 @@
 namespace Piwik\Plugin;
 
 use Piwik\API\Proxy;
+use Piwik\API\Request;
 use Piwik\Cache\LanguageAwareStaticCache;
+use Piwik\Columns\Dimension;
+use Piwik\DataTable;
 use Piwik\Menu\MenuReporting;
 use Piwik\Metrics;
 use Piwik\Piwik;
@@ -575,6 +578,67 @@ class Report
         return $this->actionToLoadSubTables;
     }
 
+    /**
+     * Returns the Dimension instance of this report's subtable report.
+     *
+     * @return Dimension|null The subtable report's dimension or null if there is subtable report or
+     *                        no dimension for the subtable report.
+     * @api
+     */
+    public function getSubtableDimension()
+    {
+        if (empty($this->actionToLoadSubTables)) {
+            return null;
+        }
+
+        list($subtableReportModule, $subtableReportAction) = $this->getSubtableApiMethod();
+
+        $subtableReport = self::factory($subtableReportModule, $subtableReportAction);
+        if (empty($subtableReport)) {
+            return null;
+        }
+
+        return $subtableReport->getDimension();
+    }
+
+    /**
+     * Returns true if the report is for another report's subtable, false if otherwise.
+     *
+     * @return bool
+     */
+    public function isSubtableReport()
+    {
+        return $this->isSubtableReport;
+    }
+
+    /**
+     * Fetches the report represented by this instance.
+     *
+     * @param array $paramOverride Query parameter overrides.
+     * @return DataTable
+     * @api
+     */
+    public function fetch($paramOverride = array())
+    {
+        return Request::processRequest($this->module . '.' . $this->action, $paramOverride);
+    }
+
+    /**
+     * Fetches a subtable for the report represented by this instance.
+     *
+     * @param int $idSubtable The subtable ID.
+     * @param array $paramOverride Query parameter overrides.
+     * @return DataTable
+     * @api
+     */
+    public function fetchSubtable($idSubtable, $paramOverride = array())
+    {
+        $paramOverride = array('idSubtable' => $idSubtable) + $paramOverride;
+
+        list($module, $action) = $this->getSubtableApiMethod();
+        return Request::processRequest($module . '.' . $action, $paramOverride);
+    }
+
     /**
      * Get an instance of a specific report belonging to the given module and having the given action.
      * @param  string $module
@@ -648,4 +712,29 @@ class Report
     {
         return 'menu' . ucfirst($this->action);
     }
+
+    private function getSubtableApiMethod()
+    {
+        if (strpos($this->actionToLoadSubTables, '.') !== false) {
+            return explode('.', $this->actionToLoadSubTables);
+        } else {
+            return array($this->module, $this->actionToLoadSubTables);
+        }
+    }
+
+    /**
+     * Finds a top level report that provides stats for a specific Dimension.
+     *
+     * @param Dimension $dimension The dimension whose report we're looking for.
+     * @return Report|null The
+     * @api
+     */
+    public static function getForDimension(Dimension $dimension)
+    {
+        return ComponentFactory::getComponentIf(__CLASS__, $dimension->getModule(), function (Report $report) use ($dimension) {
+            return !$report->isSubtableReport()
+                && $report->getDimension()
+                && $report->getDimension()->getId() == $dimension->getId();
+        });
+    }
 }
diff --git a/core/Plugin/Segment.php b/core/Plugin/Segment.php
index cc3392eebc1e77cc07af142f2c45a7a32888ded4..795d4da1579a3750f2142c757152eb681f5a35f3 100644
--- a/core/Plugin/Segment.php
+++ b/core/Plugin/Segment.php
@@ -186,6 +186,16 @@ class Segment
         return $this->type;
     }
 
+    /**
+     * Returns the name of this segment as it should appear in segment expressions.
+     *
+     * @return string
+     */
+    public function getSegment()
+    {
+        return $this->segment;
+    }
+
     /**
      * Set callback which will be executed when user will call for suggested values for segment.
      *
diff --git a/core/Plugin/Visualization.php b/core/Plugin/Visualization.php
index 1c5a7a3bd84246404111796f0bfab91d2fa98139..e82a8d7f7397f3c9d1d33474c027b7bad09adf17 100644
--- a/core/Plugin/Visualization.php
+++ b/core/Plugin/Visualization.php
@@ -286,7 +286,7 @@ class Visualization extends ViewDataTable
         }
 
         if (empty($this->requestConfig->filter_sort_column)) {
-            $this->requestConfig->setDefaultSort($this->config->columns_to_display, $hasNbUniqVisitors);
+            $this->requestConfig->setDefaultSort($this->config->columns_to_display, $hasNbUniqVisitors, $columns);
         }
 
         // deal w/ table metadata
@@ -297,6 +297,13 @@ class Visualization extends ViewDataTable
                 $this->config->report_last_updated_message = $this->makePrettyArchivedOnText();
             }
         }
+
+        $pivotBy = Common::getRequestVar('pivotBy', false) ?: $this->requestConfig->pivotBy;
+        if (empty($pivotBy)
+            && $this->dataTable instanceof DataTable
+        ) {
+            $this->config->disablePivotBySubtableIfTableHasNoSubtables($this->dataTable);
+        }
     }
 
     private function applyFilters()
@@ -312,7 +319,7 @@ class Visualization extends ViewDataTable
 
         if (!in_array($this->requestConfig->filter_sort_column, $this->config->columns_to_display)) {
             $hasNbUniqVisitors = in_array('nb_uniq_visitors', $this->config->columns_to_display);
-            $this->requestConfig->setDefaultSort($this->config->columns_to_display, $hasNbUniqVisitors);
+            $this->requestConfig->setDefaultSort($this->config->columns_to_display, $hasNbUniqVisitors, $this->dataTable->getColumns());
         }
 
         if (!$this->requestConfig->areGenericFiltersDisabled()) {
diff --git a/core/ViewDataTable/Config.php b/core/ViewDataTable/Config.php
index 093f28b254851a61d1a7b20e91fb672f8ed9b72b..11ef83a1dcd3956e8c8ef2eb85f4ab0c244db744 100644
--- a/core/ViewDataTable/Config.php
+++ b/core/ViewDataTable/Config.php
@@ -9,7 +9,10 @@
 
 namespace Piwik\ViewDataTable;
 use Piwik\API\Request as ApiRequest;
+use Piwik\DataTable;
+use Piwik\DataTable\Filter\PivotByDimension;
 use Piwik\Metrics;
+use Piwik\Plugin\Report;
 use Piwik\Plugins\API\API;
 
 /**
@@ -83,7 +86,10 @@ class Config
      * The list of ViewDataTable properties that are 'Client Side Properties'.
      */
     public $clientSideProperties = array(
-        'show_limit_control'
+        'show_limit_control',
+        'pivot_by_dimension',
+        'pivot_by_column',
+        'pivot_dimension_name'
     );
 
     /**
@@ -93,6 +99,7 @@ class Config
         'show_goals',
         'show_exclude_low_population',
         'show_flatten_table',
+        'show_pivot_by_subtable',
         'show_table',
         'show_table_all_columns',
         'show_footer',
@@ -184,6 +191,28 @@ class Config
      */
     public $show_flatten_table = true;
 
+    /**
+     * Whether to show the 'Pivot by subtable' option (visible in the popup that displays after clicking
+     * the 'cog' icon).
+     */
+    public $show_pivot_by_subtable;
+
+    /**
+     * The ID of the dimension to pivot by when the 'pivot by subtable' option is clicked. Defaults
+     * to the subtable dimension of the report being displayed.
+     */
+    public $pivot_by_dimension;
+
+    /**
+     * The column to display in pivot tables. Defaults to the first non-label column if not specified.
+     */
+    public $pivot_by_column = false;
+
+    /**
+     * The human readable name of the pivot dimension.
+     */
+    public $pivot_dimension_name = false;
+
     /**
      * Controls whether the footer icon that allows users to switch to the 'normal' DataTable view
      * is shown.
@@ -447,6 +476,7 @@ class Config
         $this->report_id        = $controllerName . '.' . $controllerAction;
 
         $this->loadDocumentation();
+        $this->setShouldShowPivotBySubtable();
     }
 
     /** Load documentation from the API */
@@ -635,4 +665,33 @@ class Config
             $this->addTranslation($key, $translation);
         }
     }
+
+    private function setShouldShowPivotBySubtable()
+    {
+        $report = Report::factory($this->controllerName, $this->controllerAction);
+
+        if (empty($report)) {
+            $this->show_pivot_by_subtable = false;
+            $this->pivot_by_dimension = false;
+        } else {
+            $this->show_pivot_by_subtable =  PivotByDimension::isPivotingReportBySubtableSupported($report);
+
+            $subtableDimension = $report->getSubtableDimension();
+            if (!empty($subtableDimension)) {
+                $this->pivot_by_dimension = $subtableDimension->getId();
+                $this->pivot_dimension_name = $subtableDimension->getName();
+            }
+        }
+    }
+
+    public function disablePivotBySubtableIfTableHasNoSubtables(DataTable $table)
+    {
+        foreach ($table->getRows() as $row) {
+            if ($row->getIdSubDataTable() !== null) {
+                return;
+            }
+        }
+
+        $this->show_pivot_by_subtable = false;
+    }
 }
diff --git a/core/ViewDataTable/Request.php b/core/ViewDataTable/Request.php
index 324803aa91ec95a972708a11c72fcc8d5c8ee538..94938899443aab7eefa2bf2589eecc5d2ea0be7b 100644
--- a/core/ViewDataTable/Request.php
+++ b/core/ViewDataTable/Request.php
@@ -74,7 +74,10 @@ class Request
             'filter_column',
             'filter_pattern',
             'flat',
-            'expanded'
+            'expanded',
+            'pivotBy',
+            'pivotByColumn',
+            'pivotByColumnLimit'
         );
 
         foreach ($toSetEventually as $varToSet) {
diff --git a/core/ViewDataTable/RequestConfig.php b/core/ViewDataTable/RequestConfig.php
index ad77df882473092e9c1cbfa3733b7860c3b31786..5ea6b623f4fa9bdd9cd5a3a70a08a90355b98d7d 100644
--- a/core/ViewDataTable/RequestConfig.php
+++ b/core/ViewDataTable/RequestConfig.php
@@ -90,7 +90,10 @@ class RequestConfig
         'filter_column',
         'filter_offset',
         'flat',
-        'expanded'
+        'expanded',
+        'pivotBy',
+        'pivotByColumn',
+        'pivotByColumnLimit'
     );
 
     /**
@@ -108,7 +111,10 @@ class RequestConfig
         'disable_generic_filters',
         'disable_queued_filters',
         'flat',
-        'expanded'
+        'expanded',
+        'pivotBy',
+        'pivotByColumn',
+        'pivotByColumnLimit'
     );
 
     /**
@@ -232,6 +238,29 @@ class RequestConfig
      */
     public $idSubtable = false;
 
+    /**
+     * Dimension ID to pivot by. See {@link Piwik\DataTable\Filter\PivotByDimension} for more info.
+     *
+     * @var string
+     */
+    public $pivotBy = false;
+
+    /**
+     * The column to display in a pivot table, eg, `'nb_visits'`. See {@link Piwik\DataTable\Filter\PivotByDimension}
+     * for more info.
+     *
+     * @var string
+     */
+    public $pivotByColumn = false;
+
+    /**
+     * The maximum number of columns to display in a pivot table. See {@link Piwik\DataTable\Filter\PivotByDimension}
+     * for more info.
+     *
+     * @var int
+     */
+    public $pivotByColumnLimit = false;
+
     public function getProperties()
     {
         return get_object_vars($this);
@@ -263,7 +292,7 @@ class RequestConfig
         }
     }
 
-    public function setDefaultSort($columnsToDisplay, $hasNbUniqVisitors)
+    public function setDefaultSort($columnsToDisplay, $hasNbUniqVisitors, $actualColumns)
     {
         // default sort order to visits/visitors data
         if ($hasNbUniqVisitors && in_array('nb_uniq_visitors', $columnsToDisplay)) {
@@ -272,6 +301,16 @@ class RequestConfig
             $this->filter_sort_column = 'nb_visits';
         }
 
+        // if the default sort column does not exist, sort by the first non-label column
+        if (!in_array($this->filter_sort_column, $actualColumns)) {
+            foreach ($actualColumns as $column) {
+                if ($column != 'label') {
+                    $this->filter_sort_column = $column;
+                    break;
+                }
+            }
+        }
+
         $this->filter_sort_order = 'desc';
     }
 
diff --git a/plugins/CoreConsole/Commands/TestsRun.php b/plugins/CoreConsole/Commands/TestsRun.php
index 542e803b9e25caff6264240b07a613af618fd157..5f940acb835f22b7ed660b9544fda1eb08da4a10 100644
--- a/plugins/CoreConsole/Commands/TestsRun.php
+++ b/plugins/CoreConsole/Commands/TestsRun.php
@@ -87,7 +87,7 @@ class TestsRun extends ConsoleCommand
 
     private function executeTestFile($testFile, $options, $command, OutputInterface $output)
     {
-        $params = $testFile . " " . $options;
+        $params = $options . " " . $testFile;
         $cmd = sprintf("cd %s/tests/PHPUnit && %s %s", PIWIK_DOCUMENT_ROOT, $command, $params);
         $output->writeln('Executing command: <info>' . $cmd . '</info>');
         passthru($cmd);
diff --git a/plugins/CoreConsole/Commands/TestsSetupFixture.php b/plugins/CoreConsole/Commands/TestsSetupFixture.php
index 1d38f9f3bfb3efc327ae1b3a27bec13a34e7cc00..cc5481d7e47bdf761cf308e1f306701c09209657 100644
--- a/plugins/CoreConsole/Commands/TestsSetupFixture.php
+++ b/plugins/CoreConsole/Commands/TestsSetupFixture.php
@@ -134,7 +134,7 @@ class TestsSetupFixture extends ConsoleCommand
 
     private function createSqlDump($sqlDumpPath, OutputInterface $output)
     {
-        $output->write("<info>Creating SQL dump...</info>");
+        $output->writeln("<info>Creating SQL dump...</info>");
 
         $databaseConfig = Config::getInstance()->database;
         $dbUser = $databaseConfig['username'];
@@ -143,6 +143,7 @@ class TestsSetupFixture extends ConsoleCommand
         $dbName = $databaseConfig['dbname'];
 
         $command = "mysqldump --user='$dbUser' --password='$dbPass' --host='$dbHost' '$dbName' > '$sqlDumpPath'";
+        $output->writeln("<info>Executing $command...</info>");
         passthru($command);
 
         $this->writeSuccessMessage($output, array("SQL dump created!"));
diff --git a/plugins/CoreHome/CoreHome.php b/plugins/CoreHome/CoreHome.php
index 64cead7bcd4509307eae5a20564264b126c0299f..61fd18d54b36a898230c92173d4af11781de2878 100644
--- a/plugins/CoreHome/CoreHome.php
+++ b/plugins/CoreHome/CoreHome.php
@@ -238,5 +238,7 @@ class CoreHome extends \Piwik\Plugin
         $translationKeys[] = 'General_Default';
         $translationKeys[] = 'General_LoadingData';
         $translationKeys[] = 'General_ErrorRequest';
+        $translationKeys[] = 'CoreHome_UndoPivotBySubtable';
+        $translationKeys[] = 'CoreHome_PivotBySubtable';
     }
 }
diff --git a/plugins/CoreHome/javascripts/dataTable.js b/plugins/CoreHome/javascripts/dataTable.js
index 4ba2607c1408bbbe94dfcbf80aa8970a7c478ad2..205ec70023d0f21251c8c19e152ed75a68a8f50d 100644
--- a/plugins/CoreHome/javascripts/dataTable.js
+++ b/plugins/CoreHome/javascripts/dataTable.js
@@ -155,7 +155,9 @@ $.extend(DataTable.prototype, UIControl.prototype, {
             'columns',
             'flat',
             'include_aggregate_rows',
-            'totalRows'
+            'totalRows',
+            'pivotBy',
+            'pivotByColumn'
         ];
 
         for (var key = 0; key < filters.length; key++) {
@@ -574,7 +576,7 @@ $.extend(DataTable.prototype, UIControl.prototype, {
 
             // we change the style of the column currently used as sort column
             // adding an image and the class columnSorted to the TD
-            $("th#" + self.param.filter_sort_column + ' #thDIV', domElem).parent()
+            $('th', domElem).filter(function () { return $(this).attr('id') == self.param.filter_sort_column; })
                 .addClass('columnSorted')
                 .prepend('<div class="sortIconContainer sortIconContainer' + ImageSortClass + ' ' + imageSortClassType + '"><span class="sortIcon" width="' + imageSortWidth + '" height="' + imageSortHeight + '" /></div>');
         }
@@ -1089,6 +1091,12 @@ $.extend(DataTable.prototype, UIControl.prototype, {
                 } else {
                     str += '&expanded=1';
                 }
+                if (self.param.pivotBy) {
+                    str += '&pivotBy=' + self.param.pivotBy + '&pivotByColumnLimit=20';
+                    if (self.props.pivot_by_column) {
+                        str += '&pivotByColumn=' + self.props.pivot_by_column;
+                    }
+                }
                 if (format == 'CSV' || format == 'TSV' || format == 'RSS') {
                     str += '&translateColumnNames=1&language=' + piwik.language;
                 }
@@ -1171,15 +1179,19 @@ $.extend(DataTable.prototype, UIControl.prototype, {
         };
         $('div.tableConfiguration', domElem).hover(open, close);
 
-        var generateClickCallback = function (paramName, callbackAfterToggle) {
+        var generateClickCallback = function (paramName, callbackAfterToggle, setParamCallback) {
             return function () {
                 close();
-                self.param[paramName] = (1 - self.param[paramName]) + '';
+                if (setParamCallback) {
+                    var data = setParamCallback();
+                } else {
+                    self.param[paramName] = (1 - self.param[paramName]) + '';
+                    var data = {};
+                }
                 self.param.filter_offset = 0;
                 delete self.param.totalRows;
                 if (callbackAfterToggle) callbackAfterToggle();
                 self.reloadAjaxDataTable(true, callbackSuccess);
-                var data = {};
                 data[paramName] = self.param[paramName];
                 self.notifyWidgetParametersChange(domElem, data);
             };
@@ -1247,6 +1259,37 @@ $.extend(DataTable.prototype, UIControl.prototype, {
                 }
             }));
 
+        // handle pivot by
+        $('.dataTablePivotBySubtable', domElem)
+            .each(function () {
+                if (self.param.pivotBy
+                    && self.param.pivotBy != '0'
+                ) {
+                    $(this).html(getText('CoreHome_UndoPivotBySubtable', true));
+                    iconHighlighted = true;
+                } else {
+                    var optionLabelText = getText('CoreHome_PivotBySubtable').replace('%s', self.props.pivot_dimension_name);
+                    $(this).html(optionLabelText);
+                }
+            })
+            .click(generateClickCallback('pivotBy', null, function () {
+                if (self.param.pivotBy
+                    && self.param.pivotBy != '0'
+                ) {
+                    self.param.pivotBy = '0'; // set to '0' so it will be sent in the request and override the saved param
+                    self.param.pivotByColumn = '0';
+                } else {
+                    self.param.pivotBy = self.props.pivot_by_dimension;
+                    if (self.props.pivot_by_column) {
+                        self.param.pivotByColumn = self.props.pivot_by_column;
+                    }
+                }
+
+                // remove sorting so it will default to first column in table
+                self.param.filter_sort_column = '';
+                return {filter_sort_column: ''};
+            }));
+
         // handle highlighted icon
         if (iconHighlighted) {
             icon.addClass('highlighted');
@@ -1618,6 +1661,9 @@ $.extend(DataTable.prototype, UIControl.prototype, {
                     self.param[key] = decodeURIComponent(newParams[key]);
                 }
 
+                delete self.param.pivotBy;
+                delete self.param.pivotByColumn;
+
                 // do ajax request
                 self.reloadAjaxDataTable(true, function (newReport) {
                     var newDomElem = self.dataTableLoaded(newReport, self.workingDivId);
diff --git a/plugins/CoreHome/lang/en.json b/plugins/CoreHome/lang/en.json
index c976f2ad3a93681ef0ad271ab79fb51d5689d89e..bcaf3af402389b78e1fb66951e0ec8fac3d91e48 100644
--- a/plugins/CoreHome/lang/en.json
+++ b/plugins/CoreHome/lang/en.json
@@ -59,6 +59,8 @@
         "ViewAllPiwikVideoTutorials": "View all Piwik Video Tutorials",
         "WebAnalyticsReports": "Web Analytics Reports",
         "YouAreUsingTheLatestVersion": "You are using the latest version of Piwik!",
-        "ClickRowToExpandOrContract": "Click this row to expand or contract the subtable."
+        "ClickRowToExpandOrContract": "Click this row to expand or contract the subtable.",
+        "UndoPivotBySubtable": "This report has been pivoted %s Undo pivot",
+        "PivotBySubtable": "This report is not pivoted %s Pivot by %s"
     }
 }
\ No newline at end of file
diff --git a/plugins/CoreHome/templates/_dataTableFooter.twig b/plugins/CoreHome/templates/_dataTableFooter.twig
index 4c0a588487a3c5736ec41ec22d870e545b4265dc..0f673aa2a25e90df64164bf12afb7624f2e77354 100644
--- a/plugins/CoreHome/templates/_dataTableFooter.twig
+++ b/plugins/CoreHome/templates/_dataTableFooter.twig
@@ -97,6 +97,11 @@
                             <div class="configItem dataTableExcludeLowPopulation"></div>
                         </li>
                     {% endif %}
+                    {% if properties.show_pivot_by_subtable|default is not empty %}
+                        <li>
+                            <div class="configItem dataTablePivotBySubtable"></div>
+                        </li>
+                    {% endif %}
                 </ul>
             </div>
             {% if isPluginLoaded('Annotations') and not properties.hide_annotations_view %}
diff --git a/plugins/CoreHome/templates/_dataTableHead.twig b/plugins/CoreHome/templates/_dataTableHead.twig
index 43b87a0e69638864de5cbfbf20ecb4da639e3e99..47091b3c0fe24164e37ed174b827d1586b699a99 100644
--- a/plugins/CoreHome/templates/_dataTableHead.twig
+++ b/plugins/CoreHome/templates/_dataTableHead.twig
@@ -10,7 +10,7 @@
                        {{ properties.metrics_documentation[column]|raw }}
                    </div>
                {% endif %}
-               <div id="thDIV">{{ properties.translations[column]|default(column)|raw }}</div>
+               <div id="thDIV" class="thDIV">{{ properties.translations[column]|default(column)|raw }}</div>
            </th>
        {% endfor %}
    </tr>
diff --git a/plugins/CoreVisualizations/Visualizations/HtmlTable.php b/plugins/CoreVisualizations/Visualizations/HtmlTable.php
index c14eb33863070aa171cc06b5dbd5b0cfcecba634..63cfebe5eb73a4df381297f03ba403b6730d8258 100644
--- a/plugins/CoreVisualizations/Visualizations/HtmlTable.php
+++ b/plugins/CoreVisualizations/Visualizations/HtmlTable.php
@@ -64,7 +64,8 @@ class HtmlTable extends Visualization
                 'filter_offset' => 0,
                 'period'        => $period,
                 'showColumns'   => implode(',', $this->config->columns_to_display),
-                'columns'       => implode(',', $this->config->columns_to_display)
+                'columns'       => implode(',', $this->config->columns_to_display),
+                'pivotBy'       => ''
             ));
 
             $dataTable = $request->process();
diff --git a/plugins/CoreVisualizations/templates/_dataTableViz_htmlTable.twig b/plugins/CoreVisualizations/templates/_dataTableViz_htmlTable.twig
index 50d4981bb264257bc5303604cb9f977bbd84bf33..13c153f89c6be837ac203389b735be9f0419e62a 100644
--- a/plugins/CoreVisualizations/templates/_dataTableViz_htmlTable.twig
+++ b/plugins/CoreVisualizations/templates/_dataTableViz_htmlTable.twig
@@ -29,7 +29,7 @@
                 {% if showRow %}
                 <tr {% if rowHasSubtable %}id="{{ row.getIdSubDataTable() }}"{% endif %}
                     class="{{ row.getMetadata('css_class') }} {% if rowHasSubtable %}subDataTable{% endif %}{% if shouldHighlightRow %} highlight{% endif %}"
-                    title="{{ 'CoreHome_ClickRowToExpandOrContract'|translate }}">
+                    {% if rowHasSubtable %}title="{{ 'CoreHome_ClickRowToExpandOrContract'|translate }}"{% endif %}>
                     {% for column in properties.columns_to_display %}
                         <td>
                             {% include "@CoreHome/_dataTableCell.twig" with properties %}
diff --git a/plugins/Events/Events.php b/plugins/Events/Events.php
index af701bdf3790763b3f4bef3ffd4509b9a17a0c7e..3d335bd229f9ee955a0aa84bf98b2b5dd062e6df 100644
--- a/plugins/Events/Events.php
+++ b/plugins/Events/Events.php
@@ -10,6 +10,7 @@ namespace Piwik\Plugins\Events;
 
 use Piwik\Common;
 use Piwik\Piwik;
+use Piwik\Plugin\Report;
 use Piwik\Plugin\ViewDataTable;
 
 class Events extends \Piwik\Plugin
@@ -161,7 +162,11 @@ class Events extends \Piwik\Plugin
 
         $secondaryDimension = $this->getSecondaryDimensionFromRequest();
         $view->config->subtable_controller_action = API::getInstance()->getActionToLoadSubtables($apiMethod, $secondaryDimension);
-        $view->config->columns_to_display = array('label', 'nb_events', 'sum_event_value');
+
+        if (Common::getRequestVar('pivotBy', false) === false) {
+            $view->config->columns_to_display = array('label', 'nb_events', 'sum_event_value');
+        }
+
         $view->config->show_flatten_table = true;
         $view->config->show_table_all_columns = false;
         $view->requestConfig->filter_sort_column = 'nb_events';
@@ -171,6 +176,10 @@ class Events extends \Piwik\Plugin
         $view->config->addTranslations($this->getMetricTranslations());
         $this->addRelatedReports($view, $secondaryDimension);
         $this->addTooltipEventValue($view);
+
+        $subtableReport = Report::factory('Events', $view->config->subtable_controller_action);
+        $view->config->pivot_by_dimension = $subtableReport->getDimension()->getId();
+        $view->config->pivot_by_column = 'nb_events';
     }
 
     private function addRelatedReports($view, $secondaryDimension)
diff --git a/plugins/Events/Reports/GetAction.php b/plugins/Events/Reports/GetAction.php
index 04ed51bd214b081958f5672cdf1c498198e18083..4fa80bf4e13b0051d3953f2b139ad271f975cc2f 100644
--- a/plugins/Events/Reports/GetAction.php
+++ b/plugins/Events/Reports/GetAction.php
@@ -8,6 +8,7 @@
  */
 namespace Piwik\Plugins\Events\Reports;
 
+use Piwik\Common;
 use Piwik\Piwik;
 use Piwik\Plugins\Events\Columns\EventAction;
 
@@ -20,7 +21,11 @@ class GetAction extends Base
         $this->name          = Piwik::translate('Events_EventActions');
         $this->documentation = ''; // TODO
         $this->metrics       = array('nb_events', 'sum_event_value', 'min_event_value', 'max_event_value', 'avg_event_value', 'nb_events_with_value');
-        $this->actionToLoadSubTables = 'getNameFromActionId';
+        if (Common::getRequestVar('secondaryDimension', false) == 'eventCategory') {
+            $this->actionToLoadSubTables = 'getCategoryFromNameId';
+        } else {
+            $this->actionToLoadSubTables = 'getNameFromActionId';
+        }
         $this->order = 1;
         $this->widgetTitle  = 'Events_EventActions';
     }
diff --git a/plugins/Events/Reports/GetActionFromCategoryId.php b/plugins/Events/Reports/GetActionFromCategoryId.php
new file mode 100644
index 0000000000000000000000000000000000000000..e5de6af9cac78b8d599572da3d34f65ac756393f
--- /dev/null
+++ b/plugins/Events/Reports/GetActionFromCategoryId.php
@@ -0,0 +1,28 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ *
+ */
+namespace Piwik\Plugins\Events\Reports;
+
+use Piwik\Piwik;
+use Piwik\Plugin\Report;
+use Piwik\Plugins\Events\Columns\EventAction;
+
+/**
+ * Report metadata class for the Events.getActionFromCategoryId class.
+ */
+class GetActionFromCategoryId extends Report
+{
+    protected function init()
+    {
+        $this->category = 'Events_Events';
+        $this->processedMetrics = false;
+        $this->dimension     = new EventAction();
+        $this->name          = Piwik::translate('Events_EventActions');
+        $this->isSubtableReport = true;
+    }
+}
\ No newline at end of file
diff --git a/plugins/Events/Reports/GetActionFromNameId.php b/plugins/Events/Reports/GetActionFromNameId.php
new file mode 100644
index 0000000000000000000000000000000000000000..55bd628459ab946495ba185b3acf4911e6f52afe
--- /dev/null
+++ b/plugins/Events/Reports/GetActionFromNameId.php
@@ -0,0 +1,28 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ *
+ */
+namespace Piwik\Plugins\Events\Reports;
+
+use Piwik\Piwik;
+use Piwik\Plugin\Report;
+use Piwik\Plugins\Events\Columns\EventAction;
+
+/**
+ * Report metadata class for the Events.getActionFromNameId class.
+ */
+class GetActionFromNameId extends Report
+{
+    protected function init()
+    {
+        $this->category = 'Events_Events';
+        $this->processedMetrics = false;
+        $this->dimension     = new EventAction();
+        $this->name          = Piwik::translate('Events_EventActions');
+        $this->isSubtableReport = true;
+    }
+}
\ No newline at end of file
diff --git a/plugins/Events/Reports/GetCategory.php b/plugins/Events/Reports/GetCategory.php
index a901e5c32cce1c5d3d42a5573f45a43faf878865..9d3817cd715a33154e656a93c401baf8457e776d 100644
--- a/plugins/Events/Reports/GetCategory.php
+++ b/plugins/Events/Reports/GetCategory.php
@@ -8,6 +8,7 @@
  */
 namespace Piwik\Plugins\Events\Reports;
 
+use Piwik\Common;
 use Piwik\Piwik;
 use Piwik\Plugins\Events\Columns\EventCategory;
 
@@ -20,7 +21,11 @@ class GetCategory extends Base
         $this->name          = Piwik::translate('Events_EventCategories');
         $this->documentation = ''; // TODO
         $this->metrics       = array('nb_events', 'sum_event_value', 'min_event_value', 'max_event_value', 'avg_event_value', 'nb_events_with_value');
-        $this->actionToLoadSubTables = 'getActionFromCategoryId';
+        if (Common::getRequestVar('secondaryDimension', false) == 'eventName') {
+            $this->actionToLoadSubTables = 'getNameFromCategoryId';
+        } else {
+            $this->actionToLoadSubTables = 'getActionFromCategoryId';
+        }
         $this->order = 0;
         $this->widgetTitle  = 'Events_EventCategories';
     }
diff --git a/plugins/Events/Reports/GetCategoryFromActionId.php b/plugins/Events/Reports/GetCategoryFromActionId.php
new file mode 100644
index 0000000000000000000000000000000000000000..51f5f6918d5ae2d66776e075372a5d02c5192b3d
--- /dev/null
+++ b/plugins/Events/Reports/GetCategoryFromActionId.php
@@ -0,0 +1,28 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ *
+ */
+namespace Piwik\Plugins\Events\Reports;
+
+use Piwik\Piwik;
+use Piwik\Plugin\Report;
+use Piwik\Plugins\Events\Columns\EventCategory;
+
+/**
+ * Report metadata class for the Events.getCategoryFromActionId class.
+ */
+class GetCategoryFromActionId extends Report
+{
+    protected function init()
+    {
+        $this->category = 'Events_Events';
+        $this->processedMetrics = false;
+        $this->dimension     = new EventCategory();
+        $this->name          = Piwik::translate('Events_EventCategories');
+        $this->isSubtableReport = true;
+    }
+}
\ No newline at end of file
diff --git a/plugins/Events/Reports/GetCategoryFromNameId.php b/plugins/Events/Reports/GetCategoryFromNameId.php
new file mode 100644
index 0000000000000000000000000000000000000000..4806c97d0f00102a5103f255fb51710d8095abef
--- /dev/null
+++ b/plugins/Events/Reports/GetCategoryFromNameId.php
@@ -0,0 +1,28 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ *
+ */
+namespace Piwik\Plugins\Events\Reports;
+
+use Piwik\Piwik;
+use Piwik\Plugin\Report;
+use Piwik\Plugins\Events\Columns\EventCategory;
+
+/**
+ * Report metadata class for the Events.getCategoryFromNameId class.
+ */
+class GetCategoryFromNameId extends Report
+{
+    protected function init()
+    {
+        $this->category = 'Events_Events';
+        $this->processedMetrics = false;
+        $this->dimension     = new EventCategory();
+        $this->name          = Piwik::translate('Events_EventCategories');
+        $this->isSubtableReport = true;
+    }
+}
\ No newline at end of file
diff --git a/plugins/Events/Reports/GetName.php b/plugins/Events/Reports/GetName.php
index 886e992085a08faf285dbc056b53380cbc100a61..1f97eb7099ac6feb577cbd4233ed2eebb071d2cb 100644
--- a/plugins/Events/Reports/GetName.php
+++ b/plugins/Events/Reports/GetName.php
@@ -8,6 +8,7 @@
  */
 namespace Piwik\Plugins\Events\Reports;
 
+use Piwik\Common;
 use Piwik\Piwik;
 use Piwik\Plugins\Events\Columns\EventName;
 
@@ -20,7 +21,11 @@ class GetName extends Base
         $this->name          = Piwik::translate('Events_EventNames');
         $this->documentation = ''; // TODO
         $this->metrics       = array('nb_events', 'sum_event_value', 'min_event_value', 'max_event_value', 'avg_event_value', 'nb_events_with_value');
-        $this->actionToLoadSubTables = 'getActionFromNameId';
+        if (Common::getRequestVar('secondaryDimension', false) == 'eventCategory') {
+            $this->actionToLoadSubTables = 'getCategoryFromNameId';
+        } else {
+            $this->actionToLoadSubTables = 'getActionFromNameId';
+        }
         $this->order = 2;
         $this->widgetTitle  = 'Events_EventNames';
     }
diff --git a/plugins/Events/Reports/GetNameFromActionId.php b/plugins/Events/Reports/GetNameFromActionId.php
new file mode 100644
index 0000000000000000000000000000000000000000..7b4899b672d694728effd8d7e59550e4b8407d65
--- /dev/null
+++ b/plugins/Events/Reports/GetNameFromActionId.php
@@ -0,0 +1,28 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ *
+ */
+namespace Piwik\Plugins\Events\Reports;
+
+use Piwik\Piwik;
+use Piwik\Plugin\Report;
+use Piwik\Plugins\Events\Columns\EventName;
+
+/**
+ * Report metadata class for the Events.getNameFromActionId class.
+ */
+class GetNameFromActionId extends Report
+{
+    protected function init()
+    {
+        $this->category = 'Events_Events';
+        $this->processedMetrics = false;
+        $this->dimension     = new EventName();
+        $this->name          = Piwik::translate('Events_Names');
+        $this->isSubtableReport = true;
+    }
+}
\ No newline at end of file
diff --git a/plugins/Events/Reports/GetNameFromCategoryId.php b/plugins/Events/Reports/GetNameFromCategoryId.php
new file mode 100644
index 0000000000000000000000000000000000000000..d1f43f7931cec39bdd795be54fe254953ab93609
--- /dev/null
+++ b/plugins/Events/Reports/GetNameFromCategoryId.php
@@ -0,0 +1,28 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ *
+ */
+namespace Piwik\Plugins\Events\Reports;
+
+use Piwik\Piwik;
+use Piwik\Plugin\Report;
+use Piwik\Plugins\Events\Columns\EventName;
+
+/**
+ * Report metadata class for the Events.getNameFromCategoryId class.
+ */
+class GetNameFromCategoryId extends Report
+{
+    protected function init()
+    {
+        $this->category = 'Events_Events';
+        $this->processedMetrics = false;
+        $this->dimension     = new EventName();
+        $this->name          = Piwik::translate('Events_EventNames');
+        $this->isSubtableReport = true;
+    }
+}
\ No newline at end of file
diff --git a/tests/PHPUnit/Core/API/ApiRendererTest.php b/tests/PHPUnit/Core/API/ApiRendererTest.php
index a74db0bfe06531c6a71a0a50ccd51d0609738007..ebe1952a908c7f58e79caf33c207c254e8aab752 100644
--- a/tests/PHPUnit/Core/API/ApiRendererTest.php
+++ b/tests/PHPUnit/Core/API/ApiRendererTest.php
@@ -7,16 +7,16 @@
  */
 
 use Piwik\API\ApiRenderer;
-use Piwik\API\ApiRenderer\Json;
 
 /**
  * @group Core
+ * @group Only2
  */
 class ApiRendererTest extends PHPUnit_Framework_TestCase
 {
     public function setUp()
     {
-        \Piwik\Tests\Fixture::loadAllPlugins();
+        \Piwik\Plugin\Manager::getInstance()->loadPlugins(array('API'));
     }
 
     public function test_factory_shouldCreateAnInstance_IfValidFormatGiven()
diff --git a/tests/PHPUnit/Core/API/ResponseBuilderTest.php b/tests/PHPUnit/Core/API/ResponseBuilderTest.php
index 9b3b7e53e111127a2add1eccdbfa270327a254df..9d6d0bc7795d36f553069263f1525272bbfdf031 100644
--- a/tests/PHPUnit/Core/API/ResponseBuilderTest.php
+++ b/tests/PHPUnit/Core/API/ResponseBuilderTest.php
@@ -16,7 +16,7 @@ class ResponseBuilderTest extends PHPUnit_Framework_TestCase
 {
     public function setUp()
     {
-        \Piwik\Tests\Fixture::loadAllPlugins();
+        \Piwik\Plugin\Manager::getInstance()->loadPlugins(array('API'));
     }
 
     public function test_getResponseException_shouldFormatExceptionDependingOnFormatAndAddDebugHelp()
diff --git a/tests/PHPUnit/Core/Columns/DimensionTest.php b/tests/PHPUnit/Core/Columns/DimensionTest.php
index 477ce7114a3ebccae2a591fb96a0d74db8ac7d10..6952b3880f36c34f79f6df2ae07db9212ef9e005 100644
--- a/tests/PHPUnit/Core/Columns/DimensionTest.php
+++ b/tests/PHPUnit/Core/Columns/DimensionTest.php
@@ -171,5 +171,5 @@ namespace Piwik\Tests\Core\Columns
             $dimension = Dimension::factory("ExampleTracker.ExampleDimension");
             $this->assertInstanceOf("Piwik\\Plugins\\ExampleTracker\\Columns\\ExampleDimension", $dimension);
         }
-    }
+   }
 }
\ No newline at end of file
diff --git a/tests/PHPUnit/Core/DataTable/Filter/PivotByDimensionTest.php b/tests/PHPUnit/Core/DataTable/Filter/PivotByDimensionTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..80162b362f9efce5287284a7b5c9cf2ab5d2050e
--- /dev/null
+++ b/tests/PHPUnit/Core/DataTable/Filter/PivotByDimensionTest.php
@@ -0,0 +1,389 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+namespace Piwik\Tests\Core\DataTable\Filter;
+
+use Piwik\API\Proxy;
+use Piwik\Config;
+use Piwik\DataTable;
+use Piwik\DataTable\Filter\PivotByDimension;
+use Piwik\DataTable\Row;
+use Piwik\Plugin\Manager as PluginManager;
+use PHPUnit_Framework_TestCase;
+use Exception;
+
+/**
+ * @group Core
+ */
+class PivotByDimensionTest extends PHPUnit_Framework_TestCase
+{
+    /**
+     * The number of segment tables that have been created. Used when injecting API results to make sure each
+     * segment table is different.
+     *
+     * @var int
+     */
+    private $segmentTableCount;
+
+    /**
+     * Segment query params used to fetch intersected tables in PivotByDimension filter. Captured by mock
+     * API\Proxy class.
+     *
+     * @var array
+     */
+    public $segmentUsedToGetIntersected = array();
+
+    public function setUp()
+    {
+        $self = $this;
+
+        $proxyMock = $this->getMock('stdClass', array('call'));
+        $proxyMock->expects($this->any())->method('call')->willReturnCallback(function ($className, $methodName, $parameters) use ($self) {
+            if ($className == "\\Piwik\\Plugins\\UserCountry\\API"
+                && $methodName == 'getCity'
+            ) {
+                $self->segmentUsedToGetIntersected[] = $parameters['segment'];
+
+                return $self->getSegmentTable();
+            } else {
+                throw new Exception("Unknown API request: $className::$methodName.");
+            }
+        });
+        Proxy::setSingletonInstance($proxyMock);
+
+        $this->segmentTableCount = 0;
+
+        Config::getInstance()->setTestEnvironment();
+    }
+
+    public function tearDown()
+    {
+        PluginManager::unsetInstance();
+        Proxy::unsetInstance();
+    }
+
+    /**
+     * @expectedException Exception
+     * @expectedExceptionMessage Unsupported pivot: report 'ExampleReport.ExampleReportName' has no subtable dimension.
+     */
+    public function test_construction_ShouldFail_WhenReportHasNoSubtableAndSegmentFetchingIsDisabled()
+    {
+        $this->loadPlugins('ExampleReport', 'UserCountry');
+
+        new PivotByDimension(new DataTable(), "ExampleReport.GetExampleReport", "UserCountry.City", 'nb_visits', $columnLimit = -1, $enableFetchBySegment = false);
+    }
+
+    /**
+     * @expectedException Exception
+     * @expectedExceptionMessage Unsupported pivot: the subtable dimension for 'Referrers.Referrers_Keywords' does not match the requested pivotBy dimension.
+     */
+    public function test_construction_ShouldFail_WhenDimensionIsNotSubtableAndSegmentFetchingIsDisabled()
+    {
+        $this->loadPlugins('Referrers', 'UserCountry');
+
+        new PivotByDimension(new DataTable(), "Referrers.getKeywords", "UserCountry.City", "nb_visits", $columnLimit = -1, $enableFetchBySegment = false);
+    }
+
+    /**
+     * @expectedException Exception
+     * @expectedExceptionMessage Unsupported pivot: No segment for dimension of report 'UserSettings.UserSettings_WidgetBrowserFamilies'
+     */
+    public function test_construction_ShouldFail_WhenDimensionIsNotSubtableAndSegmentFetchingIsEnabledButThereIsNoSegment()
+    {
+        $this->loadPlugins('Referrers', 'UserSettings');
+
+        new PivotByDimension(new DataTable(), "UserSettings.getBrowserType", "Referrers.Keyword", "nb_visits");
+    }
+
+    /**
+     * @expectedException Exception
+     * @expectedExceptionMessage Invalid dimension 'ExampleTracker.InvalidDimension'
+     */
+    public function test_construction_ShouldFail_WhenDimensionDoesNotExist()
+    {
+        $this->loadPlugins('ExampleReport', 'ExampleTracker');
+
+        new PivotByDimension(new DataTable(), "ExampleReport.GetExampleReport", "ExampleTracker.InvalidDimension", 'nb_visits');
+    }
+
+    /**
+     * @expectedException Exception
+     * @expectedExceptionMessage Unsupported pivot: No report for pivot dimension 'ExampleTracker.ExampleDimension'
+     */
+    public function test_construction_ShouldFail_WhenThereIsNoReportForADimension()
+    {
+        $this->loadPlugins('ExampleReport', 'ExampleTracker');
+
+        new PivotByDimension(new DataTable(), "ExampleReport.GetExampleReport", "ExampleTracker.ExampleDimension", "nb_visits");
+    }
+
+    /**
+     * @expectedException Exception
+     * @expectedExceptionMessage Unable to find report 'ExampleReport.InvalidReport'
+     */
+    public function test_construction_ShouldFail_WhenSpecifiedReportIsNotValid()
+    {
+        $this->loadPlugins('ExampleReport', 'Referrers');
+
+        new PivotByDimension(new DataTable(), "ExampleReport.InvalidReport", "Referrers.Keyword", "nb_visits");
+    }
+
+    public function test_filter_ReturnsEmptyResult_WhenTableToFilterIsEmpty()
+    {
+        $this->loadPlugins('Referrers', 'UserCountry', 'CustomVariables');
+
+        $table = new DataTable();
+
+        $pivotFilter = new PivotByDimension($table, "Referrers.getKeywords", "Referrers.SearchEngine", 'nb_visits');
+        $pivotFilter->filter($table);
+
+        $this->assertEquals(array(), $table->getRows());
+    }
+
+    public function test_filter_CorrectlyCreatesPivotTable_WhenUsingSubtableReport()
+    {
+        $this->loadPlugins('Referrers', 'UserCountry', 'CustomVariables');
+
+        $table = $this->getTableToFilter(true);
+
+        $pivotFilter = new PivotByDimension($table, "Referrers.getKeywords", "Referrers.SearchEngine", 'nb_actions', $columnLimit = -1, $fetchBySegment = false);
+        $pivotFilter->filter($table);
+
+        $expectedRows = array(
+            array('label' => 'row 1', 'col 1' => 2, 'col 2' => false, 'col 3' => false, 'col 4' => false),
+            array('label' => 'row 2', 'col 1' => 4, 'col 2' => 6, 'col 3' => false, 'col 4' => false),
+            array('label' => 'row 3', 'col 1' => false, 'col 2' => 8, 'col 3' => 31, 'col 4' => 33)
+        );
+        $this->assertTableRowsEquals($expectedRows, $table);
+    }
+
+    public function test_filter_CorrectlyCreatesPivotTable_WhenUsingSegment()
+    {
+        $this->loadPlugins('Referrers', 'UserCountry', 'CustomVariables');
+
+        $table = $this->getTableToFilter(true);
+
+        $pivotFilter = new PivotByDimension($table, "Referrers.getKeywords", "UserCountry.City", 'nb_visits');
+        $pivotFilter->filter($table);
+
+        $expectedSegmentParams = array('referrerKeyword==row+1', 'referrerKeyword==row+2', 'referrerKeyword==row+3');
+        $this->assertEquals($expectedSegmentParams, $this->segmentUsedToGetIntersected);
+
+        $expectedRows = array(
+            array('label' => 'row 1', 'col 0' => 2, 'col 1' => false, 'col 2' => false),
+            array('label' => 'row 2', 'col 0' => 2, 'col 1' => 4, 'col 2' => false),
+            array('label' => 'row 3', 'col 0' => 2, 'col 1' => 4, 'col 2' => 6)
+        );
+        $this->assertTableRowsEquals($expectedRows, $table);
+    }
+
+    public function test_filter_UsesCorrectSegment_WhenPivotingSegmentedReport()
+    {
+        $this->loadPlugins('Referrers', 'UserCountry', 'CustomVariables');
+
+        $table = $this->getTableToFilter(true);
+
+        $_GET['segment'] = 'asegment==value';
+
+        $pivotFilter = new PivotByDimension($table, "Referrers.getKeywords", "UserCountry.City", 'nb_visits');
+        $pivotFilter->filter($table);
+
+        $expectedSegmentParams = array(
+            'asegment==value;referrerKeyword==row+1',
+            'asegment==value;referrerKeyword==row+2',
+            'asegment==value;referrerKeyword==row+3'
+        );
+        $this->assertEquals($expectedSegmentParams, $this->segmentUsedToGetIntersected);
+    }
+
+    public function test_filter_CorrectlyCreatesPivotTable_WhenPivotMetricDoesNotExistInTable()
+    {
+        $this->loadPlugins('Referrers', 'UserCountry', 'CustomVariables');
+
+        $table = $this->getTableToFilter(true);
+
+        $pivotFilter = new PivotByDimension($table, "Referrers.getKeywords", "Referrers.SearchEngine", 'invalid_metric');
+        $pivotFilter->filter($table);
+
+        $expectedRows = array(
+            array('label' => 'row 1', 'col 1' => false, 'col 2' => false, 'col 3' => false, 'col 4' => false),
+            array('label' => 'row 2', 'col 1' => false, 'col 2' => false, 'col 3' => false, 'col 4' => false),
+            array('label' => 'row 3', 'col 1' => false, 'col 2' => false, 'col 3' => false, 'col 4' => false)
+        );
+        $this->assertTableRowsEquals($expectedRows, $table);
+    }
+
+    public function test_filter_CorrectlyCreatesPivotTable_WhenSubtablesHaveNoRows()
+    {
+        $this->loadPlugins('Referrers', 'UserCountry', 'CustomVariables');
+
+        $table = $this->getTableToFilter(false);
+
+        $pivotFilter = new PivotByDimension($table, "CustomVariables.getCustomVariables", "CustomVariables.CustomVariableValue",
+            'nb_visits', $fetchBySegment = false);
+        $pivotFilter->filter($table);
+
+        $expectedRows = array(
+            array('label' => 'row 1'),
+            array('label' => 'row 2'),
+            array('label' => 'row 3')
+        );
+        $this->assertTableRowsEquals($expectedRows, $table);
+    }
+
+    public function test_filter_CorrectlyDefaultsPivotByColumn_WhenNoneProvided()
+    {
+        $this->loadPlugins('Referrers', 'UserCountry', 'CustomVariables');
+
+        $table = $this->getTableToFilter(true);
+
+        $pivotFilter = new PivotByDimension($table, "Referrers.getKeywords", "Referrers.SearchEngine", $column = false, $columnLimit = -1, $fetchBySegment = false);
+        $pivotFilter->filter($table);
+
+        $expectedRows = array(
+            array('label' => 'row 1', 'col 1' => 1, 'col 2' => false, 'col 3' => false, 'col 4' => false),
+            array('label' => 'row 2', 'col 1' => 3, 'col 2' => 5, 'col 3' => false, 'col 4' => false),
+            array('label' => 'row 3', 'col 1' => false, 'col 2' => 7, 'col 3' => 9, 'col 4' => 32)
+        );
+        $this->assertTableRowsEquals($expectedRows, $table);
+    }
+
+    public function test_filter_CorrectlyLimitsTheColumnNumber_WhenColumnLimitProvided()
+    {
+        $this->loadPlugins('Referrers', 'UserCountry', 'CustomVariables');
+
+        $table = $this->getTableToFilter(true);
+
+        $pivotFilter = new PivotByDimension($table, "Referrers.getKeywords", "Referrers.SearchEngine", $column = 'nb_visits', $columnLimit = 3, $fetchBySegment = false);
+        $pivotFilter->filter($table);
+
+        $expectedRows = array(
+            array('label' => 'row 1', 'col 2' => false, 'col 4' => false, 'General_Others' => 1),
+            array('label' => 'row 2', 'col 2' => 5, 'col 4' => false, 'General_Others' => 3),
+            array('label' => 'row 3', 'col 2' => 7, 'col 4' => 32, 'General_Others' => 9)
+        );
+        $this->assertTableRowsEquals($expectedRows, $table);
+    }
+
+    private function getTableToFilter($addSubtables = false)
+    {
+        $row1 = new Row(array(Row::COLUMNS => array(
+            'label' => 'row 1',
+            'nb_visits' => 10,
+            'nb_actions' => 15
+        )));
+        if ($addSubtables) {
+            $row1->setSubtable($this->getRow1Subtable());
+        }
+
+        $row2 = new Row(array(Row::COLUMNS => array(
+            'label' => 'row 2',
+            'nb_visits' => 13,
+            'nb_actions' => 18
+        )));
+        if ($addSubtables) {
+            $row2->setSubtable($this->getRow2Subtable());
+        }
+
+        $row3 = new Row(array(Row::COLUMNS => array(
+            'label' => 'row 3',
+            'nb_visits' => 20,
+            'nb_actions' => 25
+        )));
+        if ($addSubtables) {
+            $row3->setSubtable($this->getRow3Subtable());
+        }
+
+        $table = new DataTable();
+        $table->addRowsFromArray(array($row1, $row2, $row3));
+        return $table;
+    }
+
+    private function getRow1Subtable()
+    {
+        $table = new DataTable();
+        $table->addRowsFromArray(array(
+            new Row(array(Row::COLUMNS => array(
+                'label' => 'col 1',
+                'nb_visits' => 1,
+                'nb_actions' => 2
+            )))
+        ));
+        return $table;
+    }
+
+    private function getRow2Subtable()
+    {
+        $table = new DataTable();
+        $table->addRowsFromArray(array(
+            new Row(array(Row::COLUMNS => array(
+                'label' => 'col 1',
+                'nb_visits' => 3,
+                'nb_actions' => 4
+            ))),
+            new Row(array(Row::COLUMNS => array(
+                'label' => 'col 2',
+                'nb_visits' => 5,
+                'nb_actions' => 6
+            )))
+        ));
+        return $table;
+    }
+
+    private function getRow3Subtable()
+    {
+        $table = new DataTable();
+        $table->addRowsFromArray(array(
+            new Row(array(Row::COLUMNS => array(
+                'label' => 'col 2',
+                'nb_visits' => 7,
+                'nb_actions' => 8
+            ))),
+            new Row(array(Row::COLUMNS => array(
+                'label' => 'col 3',
+                'nb_visits' => 9,
+                'nb_actions' => 31
+            ))),
+            new Row(array(Row::COLUMNS => array(
+                'label' => 'col 4',
+                'nb_visits' => 32,
+                'nb_actions' => 33
+            )))
+        ));
+        return $table;
+    }
+
+    public function getSegmentTable()
+    {
+        ++$this->segmentTableCount;
+
+        $table = new DataTable();
+        for ($i = 0; $i != $this->segmentTableCount; ++$i) {
+            $row = new Row(array(Row::COLUMNS => array(
+                'label' => 'col ' . $i,
+                'nb_visits' => ($i + 1) * 2,
+                'nb_actions' => ($i + 1) * 3
+            )));
+            $table->addRow($row);
+        }
+        return $table;
+    }
+
+    private function assertTableRowsEquals($expectedRows, $table)
+    {
+        $renderer = new DataTable\Renderer\Php();
+        $renderer->setSerialize(false);
+        $actualRows = $renderer->render($table);
+
+        $this->assertEquals($expectedRows, $actualRows);
+    }
+
+    private function loadPlugins()
+    {
+        PluginManager::getInstance()->loadPlugins(func_get_args());
+    }
+}
\ No newline at end of file
diff --git a/tests/PHPUnit/Core/DataTable/Renderer/CSVTest.php b/tests/PHPUnit/Core/DataTable/Renderer/CSVTest.php
index 4c5c77a35513b21014b44d9da6553d3530958b86..64ffcf6a06a1eb99edfd8325c9cf49b69b9113c4 100644
--- a/tests/PHPUnit/Core/DataTable/Renderer/CSVTest.php
+++ b/tests/PHPUnit/Core/DataTable/Renderer/CSVTest.php
@@ -178,6 +178,22 @@ class DataTable_Renderer_CSVTest extends PHPUnit_Framework_TestCase
         $this->assertEquals($expected, $rendered);
     }
 
+    /**
+     * @group Core
+     */
+    public function testCSVRendererCorrectlyEscapesHeadersAndValues()
+    {
+        $dataTable = $this->_getDataTableSimpleWithCommasInCells();
+        $render = new Csv();
+        $render->setTable($dataTable);
+        $render->convertToUnicode = false;
+
+        $expected = '"col,1","col,2"
+"val""1","val"",2"';
+        $actual = $render->render();
+        $this->assertEquals($expected, $actual);
+    }
+
     /**
      * DATA OF DATATABLE_ARRAY
      * -------------------------
@@ -440,4 +456,13 @@ b,d,f,g';
 
         $this->assertEquals($expected, $render->render());
     }
-}
+
+    private function _getDataTableSimpleWithCommasInCells()
+    {
+        $table = new DataTable();
+        $table->addRowsFromSimpleArray(array(
+            array("col,1" => "val\"1", "col,2" => "val\",2")
+        ));
+        return $table;
+    }
+}
\ No newline at end of file
diff --git a/tests/PHPUnit/Core/DataTable/Renderer/XMLTest.php b/tests/PHPUnit/Core/DataTable/Renderer/XMLTest.php
index 40596e0e231d92a0976dcbd8618f931440830d3d..f8c2a93072bf6420b5c61c153851118a47de2ac9 100644
--- a/tests/PHPUnit/Core/DataTable/Renderer/XMLTest.php
+++ b/tests/PHPUnit/Core/DataTable/Renderer/XMLTest.php
@@ -221,6 +221,42 @@ class DataTable_Renderer_XMLTest extends PHPUnit_Framework_TestCase
         $this->assertEquals($expected, $render->render());
     }
 
+    /**
+     * @group Core
+     */
+    public function testXMLRendererSuccessfullyRendersWhenSimpleDataTableColumnsHaveInvalidXmlCharacters()
+    {
+        $dataTable = $this->_getDataTableSimpleWithInvalidChars();
+        $render = new Xml();
+        $render->setTable($dataTable);
+        $expected = '<?xml version="1.0" encoding="utf-8" ?>
+<result>
+	<col name="$%@(%">1</col>
+	<col name="avbs$">2</col>
+	<col name="b/">2</col>
+</result>';
+        $this->assertEquals($expected, $render->render());
+    }
+
+    /**
+     * @group Core
+     */
+    public function testXMLRendererSuccessfullyRendersWhenDataTableColumnsHaveInvalidXmlCharacters()
+    {
+        $dataTable = $this->_getDataTableWithInvalidChars();
+        $render = new Xml();
+        $render->setTable($dataTable);
+        $expected = '<?xml version="1.0" encoding="utf-8" ?>
+<result>
+	<row>
+		<col name="$%@(%">1</col>
+		<col name="avbs$">2</col>
+		<col name="b/">2</col>
+	</row>
+</result>';
+        $this->assertEquals($expected, $render->render());
+    }
+
     /**
      * DATA OF DATATABLE_ARRAY
      * -------------------------
@@ -601,4 +637,22 @@ class DataTable_Renderer_XMLTest extends PHPUnit_Framework_TestCase
 
         $this->assertEquals($expected, $render->render());
     }
+
+    private function _getDataTableSimpleWithInvalidChars()
+    {
+        $table = new DataTable\Simple();
+        $table->addRowsFromSimpleArray(
+            array("$%@(%" => 1, "avbs$" => 2, "b/" => 2)
+        );
+        return $table;
+    }
+
+    private function _getDataTableWithInvalidChars()
+    {
+        $table = new DataTable();
+        $table->addRowsFromSimpleArray(
+            array("$%@(%" => 1, "avbs$" => 2, "b/" => 2)
+        );
+        return $table;
+    }
 }
diff --git a/tests/PHPUnit/Core/Menu/MenuReportingTest.php b/tests/PHPUnit/Core/Menu/MenuReportingTest.php
index 09d0347959797aabec1ac2768676502e25cb77d5..b82c4a1aaf0e0ef6e061c22713b636c3bdb463c1 100644
--- a/tests/PHPUnit/Core/Menu/MenuReportingTest.php
+++ b/tests/PHPUnit/Core/Menu/MenuReportingTest.php
@@ -6,6 +6,7 @@
  * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
  */
 
+use Piwik\Access;
 use Piwik\Plugin\Report;
 use Piwik\Piwik;
 use Piwik\Metrics;
@@ -28,11 +29,14 @@ class Menu_ReportingTest extends PHPUnit_Framework_TestCase
     {
         PluginManager::getInstance()->unloadPlugins();
         $this->menu = MenuReporting::getInstance();
+
+        Access::setSingletonInstance(new FakeAccess());
     }
 
     public function tearDown()
     {
         MenuReporting::getInstance()->unsetInstance();
+        Access::setSingletonInstance(null);
         parent::tearDown();
     }
 
diff --git a/tests/PHPUnit/Core/Plugin/ComponentFactoryTest.php b/tests/PHPUnit/Core/Plugin/ComponentFactoryTest.php
index d41149c8cd253c5082e0403b42b3936be3e126d0..b7b0f30fc3079fcdcdf1b1ab95f86ce5b823c2bb 100644
--- a/tests/PHPUnit/Core/Plugin/ComponentFactoryTest.php
+++ b/tests/PHPUnit/Core/Plugin/ComponentFactoryTest.php
@@ -11,6 +11,7 @@ use PHPUnit_Framework_TestCase;
 use Piwik\Config;
 use Piwik\Plugin\ComponentFactory;
 use Piwik\Plugin\Manager as PluginManager;
+use Piwik\Plugin\Report;
 
 /**
  * @group Core
@@ -74,6 +75,62 @@ class ComponentFactoryTest extends PHPUnit_Framework_TestCase
         $this->assertNull($report);
     }
 
+    public function test_getComponentIf_shouldNotFindAComponentIfComponentExistsButPluginIsNotLoaded()
+    {
+        $this->unloadAllPlugins();
+
+        $report = ComponentFactory::getComponentIf(self::REPORT_CLASS_NAME, 'ExampleReport', function (Report $report) {
+            return $report->getAction() == 'getExampleReport';
+        });
+
+        $this->assertNull($report);
+    }
+
+    public function test_getComponentIf_shouldFindAComponent_ThatExists()
+    {
+        $this->loadExampleReportPlugin();
+
+        $report = ComponentFactory::getComponentIf(self::REPORT_CLASS_NAME, 'ExampleReport', function (Report $report) {
+            return $report->getAction() == 'getExampleReport';
+        });
+
+        $this->assertInstanceOf('Piwik\Plugins\ExampleReport\Reports\GetExampleReport', $report);
+    }
+
+    public function test_getComponentIf_shouldNotFindAComponent_IfPluginIsActivatedButComponentNotExists()
+    {
+        $this->loadExampleReportPlugin();
+
+        $report = ComponentFactory::getComponentIf(self::REPORT_CLASS_NAME, 'ExampleReport', function (Report $report) {
+            return false;
+        });
+
+        $this->assertNull($report);
+    }
+
+    public function test_getComponentIf_shouldNotFindAComponent_IfPluginIsLoadedButNotActivated()
+    {
+        PluginManager::getInstance()->loadPlugin('ExampleReport');
+
+        $report = ComponentFactory::getComponentIf(self::REPORT_CLASS_NAME, 'ExampleReport', function (Report $report) {
+            return $report->getAction() == 'getExampleReport';
+        });
+
+        $this->assertNull($report);
+    }
+
+    public function test_getComponentIf_shouldSearchThroughAllPlugins_IfNoPluginNameIsSupplied()
+    {
+        PluginManager::getInstance()->loadPlugins(array('ExampleReport', 'Referrers'));
+
+        $reports = array();
+        ComponentFactory::getComponentIf(self::REPORT_CLASS_NAME, null, function (Report $report) use (&$reports) {
+            $reports[] = $report;
+        });
+
+        $this->assertGreaterThan(1, count($reports));
+    }
+
     private function unloadAllPlugins()
     {
         PluginManager::getInstance()->loadPlugins(array());
diff --git a/tests/PHPUnit/Fixture.php b/tests/PHPUnit/Fixture.php
index 3fd7320094e9d3d8072dbb8a72cd4516961cdb42..f625f98001d06d85c747cecaffbf4766c04896fc 100644
--- a/tests/PHPUnit/Fixture.php
+++ b/tests/PHPUnit/Fixture.php
@@ -287,6 +287,11 @@ class Fixture extends PHPUnit_Framework_Assert
 
         $_GET = $_REQUEST = array();
         Translate::unloadEnglishTranslation();
+
+        Config::unsetInstance();
+
+        \Piwik\Config::getInstance()->Plugins; // make sure Plugins exists in a config object for next tests that use Plugin\Manager
+                                               // since Plugin\Manager uses getFromGlobalConfig which doesn't init the config object
     }
 
     public static function loadAllPlugins($testEnvironment = null, $testCaseClass = false, $extraPluginsToLoad = array())
diff --git a/tests/PHPUnit/Impl/TestRequestCollection.php b/tests/PHPUnit/Impl/TestRequestCollection.php
index 17ff280eb9cc6f5c5f3fa05667fa7688c6a44aef..68cae0ff99bf5c5187131d2b0566f4d39186e107 100644
--- a/tests/PHPUnit/Impl/TestRequestCollection.php
+++ b/tests/PHPUnit/Impl/TestRequestCollection.php
@@ -246,7 +246,6 @@ class TestRequestCollection
                     $parametersToSet['serialize'] = 1;
 
                     $exampleUrl = $apiMetadata->getExampleUrl($class, $methodName, $parametersToSet);
-                    
                     if ($exampleUrl === false) {
                         continue;
                     }
diff --git a/tests/PHPUnit/Integration/Core/ArchiveProcessingTest.php b/tests/PHPUnit/Integration/Core/ArchiveProcessingTest.php
index 34aa3aeefb1272916506c20c7c806cd6387c6a86..eaa6d6807dac83acd2c7f9a744ffe7c6cf5e9eeb 100644
--- a/tests/PHPUnit/Integration/Core/ArchiveProcessingTest.php
+++ b/tests/PHPUnit/Integration/Core/ArchiveProcessingTest.php
@@ -52,6 +52,14 @@ class Core_ArchiveProcessingTest extends DatabaseTestCase
         $pseudoMockAccess = new FakeAccess;
         FakeAccess::$superUser = true;
         Access::setSingletonInstance($pseudoMockAccess);
+
+        ArchiveTableCreator::$tablesAlreadyInstalled = null;
+    }
+
+    public function tearDown()
+    {
+        Access::setSingletonInstance(null);
+        ArchiveTableCreator::$tablesAlreadyInstalled = null;
     }
 
     /**
diff --git a/tests/PHPUnit/Integration/Core/OptionTest.php b/tests/PHPUnit/Integration/Core/OptionTest.php
index e405bae45344e1eabbf5fdcf648b693e6d9f8c48..c926e1c808140962907f7928d863dc615ba3cb71 100644
--- a/tests/PHPUnit/Integration/Core/OptionTest.php
+++ b/tests/PHPUnit/Integration/Core/OptionTest.php
@@ -9,8 +9,6 @@ use Piwik\Common;
 use Piwik\Db;
 use Piwik\Option;
 
-require_once "Option.php";
-
 /**
  * Class Core_OptionTest
  *
diff --git a/tests/PHPUnit/Integration/Core/ReportTest.php b/tests/PHPUnit/Integration/Core/ReportTest.php
index c1d0df37f1c32dcaaf7596bc098b820095b24618..f935416675973aba85d60b44076d14a6aa01a3db 100644
--- a/tests/PHPUnit/Integration/Core/ReportTest.php
+++ b/tests/PHPUnit/Integration/Core/ReportTest.php
@@ -6,11 +6,14 @@
  * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
  */
 
+use Piwik\API\Proxy;
 use Piwik\Plugin\Report;
 use Piwik\Plugins\ExampleReport\Reports\GetExampleReport;
 use Piwik\Plugins\Actions\Columns\ExitPageUrl;
 use Piwik\Piwik;
 use Piwik\Metrics;
+use Piwik\Plugins\ExampleTracker\Columns\ExampleDimension;
+use Piwik\Plugins\Referrers\Columns\Keyword;
 use Piwik\WidgetsList;
 use Piwik\Translate;
 use Piwik\Menu\MenuReporting;
@@ -28,6 +31,7 @@ class GetBasicReport extends Report
         $this->module = 'TestPlugin';
         $this->action = 'getBasicReport';
         $this->category = 'Goals_Goals';
+        $this->actionToLoadSubTables = 'invalidReport';
     }
 }
 
@@ -101,6 +105,8 @@ class Plugin_ReportTest extends DatabaseTestCase
         $this->disabledReport = new GetDisabledReport();
         $this->basicReport    = new GetBasicReport();
         $this->advancedReport = new GetAdvancedReport();
+
+        Proxy::unsetInstance();
     }
 
     public function tearDown()
@@ -338,6 +344,7 @@ class Plugin_ReportTest extends DatabaseTestCase
                     'bounce_rate' => 'General_ColumnBounceRate',
                     'conversion_rate' => 'General_ColumnConversionRate',
                 ),
+                'actionToLoadSubTables' => 'invalidReport',
                 'order' => 20
             )
         ), $reports);
@@ -422,6 +429,97 @@ class Plugin_ReportTest extends DatabaseTestCase
         }
     }
 
+    public function test_getSubtableDimension_ShouldReturnNullIfNoSubtableActionExists()
+    {
+        $report = new GetExampleReport();
+        $this->assertNull($report->getSubtableDimension());
+    }
+
+    public function test_getSubtableDimension_ShouldReturnNullIfSubtableActionIsInvalid()
+    {
+        $report = new GetBasicReport();
+        $this->assertNull($report->getSubtableDimension());
+    }
+
+    public function test_getSubtableDimension_ShouldReturnCorrectDimensionIfSubtableActionIsDefinedAndCorrect()
+    {
+        PluginManager::getInstance()->loadPlugins(array('Referrers'));
+
+        $report = Report::factory('Referrers', 'getSearchEngines');
+        $subtableDimension = $report->getSubtableDimension();
+
+        $this->assertNotNull($subtableDimension);
+        $this->assertInstanceOf("Piwik\\Plugins\\Referrers\\Columns\\Keyword", $subtableDimension);
+    }
+
+    public function test_fetch_ShouldUseCorrectApiUrl()
+    {
+        PluginManager::getInstance()->loadPlugins(array('API', 'ExampleReport'));
+
+        $proxyMock = $this->getMock('stdClass', array('call', '__construct'));
+        $proxyMock->expects($this->once())->method('call')->with(
+            '\\Piwik\\Plugins\\ExampleReport\\API', 'getExampleReport', array(
+                'idSite' => 1,
+                'date' => '2012-01-02',
+                'format' => 'original',
+                'module' => 'API',
+                'method' => 'ExampleReport.getExampleReport'
+            )
+        )->willReturn("result");
+        Proxy::setSingletonInstance($proxyMock);
+
+        $report = new GetExampleReport();
+        $result = $report->fetch(array('idSite' => 1, 'date' => '2012-01-02'));
+        $this->assertEquals("result", $result);
+    }
+
+    public function test_fetchSubtable_ShouldUseCorrectApiUrl()
+    {
+        PluginManager::getInstance()->loadPlugins(array('API', 'Referrers'));
+
+        $proxyMock = $this->getMock('stdClass', array('call', '__construct'));
+        $proxyMock->expects($this->once())->method('call')->with(
+            '\\Piwik\\Plugins\\Referrers\\API', 'getSearchEnginesFromKeywordId', array(
+                'idSubtable' => 23,
+                'idSite' => 1,
+                'date' => '2012-01-02',
+                'format' => 'original',
+                'module' => 'API',
+                'method' => 'Referrers.getSearchEnginesFromKeywordId'
+            )
+        )->willReturn("result");
+        Proxy::setSingletonInstance($proxyMock);
+
+        $report = new \Piwik\Plugins\Referrers\Reports\GetKeywords();
+        $result = $report->fetchSubtable(23, array('idSite' => 1, 'date' => '2012-01-02'));
+        $this->assertEquals("result", $result);
+    }
+
+    public function test_getForDimension_ShouldReturnCorrectInstanceTypeIfAssociatedReportExists()
+    {
+        PluginManager::getInstance()->loadPlugins(array('Referrers'));
+
+        $report = Report::getForDimension(new Keyword());
+        $this->assertInstanceOf("Piwik\\Plugins\\Referrers\\Reports\\GetKeywords", $report);
+    }
+
+    public function test_getForDimension_ShouldReturnNullIfNoReportExistsForDimension()
+    {
+        $this->loadExampleReportPlugin();
+        $this->loadMorePlugins();
+
+        $report = Report::getForDimension(new ExampleDimension());
+        $this->assertNull($report);
+    }
+
+    public function test_getForDimension_ShouldReturnNullIfReportPluginNotLoaded()
+    {
+        PluginManager::getInstance()->loadPlugins(array());
+
+        $report = Report::getForDimension(new Keyword());
+        $this->assertNull($report);
+    }
+
     private function loadExampleReportPlugin()
     {
         PluginManager::getInstance()->loadPlugins(array('ExampleReport'));
@@ -441,6 +539,4 @@ class Plugin_ReportTest extends DatabaseTestCase
     {
         Translate::reloadLanguage('en');
     }
-
-
 }
\ No newline at end of file
diff --git a/tests/PHPUnit/Integration/Core/TravisEnvironmentTest.php b/tests/PHPUnit/Integration/Core/TravisEnvironmentTest.php
index 7b6b233ef43639a343c53c5086cfef7a089af346..7549dac8a9428674ff0ac63d6e7f6fa11e12b3db 100644
--- a/tests/PHPUnit/Integration/Core/TravisEnvironmentTest.php
+++ b/tests/PHPUnit/Integration/Core/TravisEnvironmentTest.php
@@ -9,9 +9,6 @@ use Piwik\Translate;
  */
 class Core_TravisEnvironmentTest extends DatabaseTestCase
 {
-    /**
-     * @group Core
-     */
     public function testUsageOfCorrectMysqlAdapter()
     {
         $mysqlAdapter = getenv('MYSQL_ADAPTER');
diff --git a/tests/PHPUnit/Integration/PivotByQueryParamTest.php b/tests/PHPUnit/Integration/PivotByQueryParamTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..6d42fe51ba15c6a78e6a80196f9fe0082736d618
--- /dev/null
+++ b/tests/PHPUnit/Integration/PivotByQueryParamTest.php
@@ -0,0 +1,181 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link    http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+namespace Piwik\Tests\Integration;
+
+use Piwik\Config;
+use Piwik\Date;
+use Piwik\Tests\Fixtures\ManyVisitsWithMockLocationProvider;
+use Piwik\Tests\IntegrationTestCase;
+
+/**
+ * @group Integration
+ * @group PivotByQueryParamTest
+ */
+class PivotByQueryParamTest extends IntegrationTestCase
+{
+    /**
+     * @var ManyVisitsWithMockLocationProvider
+     */
+    public static $fixture = null;
+
+    public static function setUpBeforeClass()
+    {
+        parent::setUpBeforeClass();
+
+        Config::getInstance()->General['pivot_by_filter_enable_fetch_by_segment'] = 1;
+    }
+
+    public function test_PivotBySubtableDimension_CreatesCorrectPivotTable()
+    {
+        $this->assertApiResponseEqualsExpected("Referrers.getKeywords", array(
+            'idSite' => self::$fixture->idSite,
+            'date' => Date::factory(self::$fixture->dateTime)->toString(),
+            'period' => 'week',
+            'pivotBy' => 'Referrers.SearchEngine',
+            'pivotByColumn' => 'nb_visits',
+            'pivotByColumnLimit' => -1,
+            'disable_queued_filters' => 1 // test that prepending doesn't happen w/ this
+        ));
+    }
+
+    public function test_PivotBySubtableDimension_CreatesCorrectPivotTable_WhenEntireHirearchyIsNotLoaded()
+    {
+        $this->assertApiResponseEqualsExpected("Referrers.getKeywords", array(
+            'idSite' => self::$fixture->idSite,
+            'date' => Date::factory(self::$fixture->dateTime)->toString(),
+            'period' => 'week',
+            'pivotBy' => 'Referrers.SearchEngine',
+            'pivotByColumn' => '', // also test default pivot column
+            'pivotByColumnLimit' => -1,
+            'expanded' => 0
+        ));
+    }
+
+    public function test_PivotBySegment_CreatesCorrectPivotTable()
+    {
+        $this->assertApiResponseEqualsExpected("Referrers.getKeywords", array(
+            'idSite' => self::$fixture->idSite,
+            'date' => Date::factory(self::$fixture->dateTime)->toString(),
+            'period' => 'week',
+            'pivotBy' => 'UserCountry.City',
+            'pivotByColumn' => 'nb_visits',
+            'pivotByColumnLimit' => -1
+        ));
+    }
+
+    public function test_PivotBySegment_CreatesCorrectPivotTable_WhenSegmentUsedInRequest()
+    {
+        $this->assertApiResponseEqualsExpected("Referrers.getKeywords", array(
+            'idSite' => self::$fixture->idSite,
+            'date' => Date::factory(self::$fixture->dateTime)->toString(),
+            'period' => 'week',
+            'segment' => 'browserCode==FF',
+            'pivotBy' => 'UserCountry.City',
+            'pivotByColumn' => 'nb_visits',
+            'pivotByColumnLimit' => -1
+        ));
+    }
+
+    public function test_PivotByParam_PlaysNiceWithOtherQueryParams()
+    {
+        $this->assertApiResponseEqualsExpected("Referrers.getKeywords", array(
+            'idSite' => self::$fixture->idSite,
+            'date' => Date::factory(self::$fixture->dateTime)->toString(),
+            'period' => 'week',
+            'pivotBy' => 'Referrers.SearchEngine',
+            'pivotByColumn' => 'nb_visits',
+            'pivotByColumnLimit' => -1,
+            'flat' => 1,
+            'totals' => 1,
+            'disable_queued_filters' => 1,
+            'disable_generic_filters' => 1,
+            'showColumns' => 'Google,Bing'
+        ));
+    }
+
+    public function test_PivotByParam_PlaysNiceWithQueuedFilters()
+    {
+        // TODO: known issue: some segment/report relationships are more complicated; for example, UserCountry.GetCity labels are combinations
+        // of city, region & country dimensions, so the segment to get an intersected table needs all 3 of those.
+
+        $this->markTestSkipped("Not working right now.");
+
+        $this->assertApiResponseEqualsExpected("UserSettings.getBrowser", array( // should have logo metadata in output
+            'idSite' => self::$fixture->idSite,
+            'date' => Date::factory(self::$fixture->dateTime)->toString(),
+            'period' => 'week',
+            'pivotBy' => 'UserCountry.City', // testing w/ report that has no subtable report
+            'pivotByColumn' => 'nb_visits',
+            'pivotByColumnLimit' => -1
+        ));
+    }
+
+    public function test_PivotByParam_WorksWithReportWhoseSubtableIsSelf()
+    {
+        $this->assertApiResponseEqualsExpected("Actions.getPageUrls", array(
+            'idSite' => self::$fixture->idSite,
+            'date' => Date::factory(self::$fixture->dateTime)->toString(),
+            'period' => 'week',
+            'pivotBy' => 'Actions.PageUrl',
+            'pivotByColumn' => 'nb_hits',
+            'pivotByColumnLimit' => -1
+        ));
+    }
+
+    public function test_PivotByParam_WorksWithColumnLimiting()
+    {
+        $this->assertApiResponseEqualsExpected("Referrers.getKeywords", array(
+            'idSite' => self::$fixture->idSite,
+            'date' => Date::factory(self::$fixture->dateTime)->toString(),
+            'period' => 'week',
+            'pivotBy' => 'UserCountry.City',
+            'pivotByColumn' => 'nb_visits',
+            'pivotByColumnLimit' => 2
+        ));
+    }
+
+    public function test_PivotByParam_WorksWithJsonOutput()
+    {
+        $this->assertApiResponseEqualsExpected("Referrers.getKeywords", array(
+            'idSite' => self::$fixture->idSite,
+            'date' => Date::factory(self::$fixture->dateTime)->toString(),
+            'period' => 'week',
+            'format' => 'json',
+            'pivotBy' => 'UserCountry.City',
+            'pivotByColumn' => 'nb_visits',
+            'pivotByColumnLimit' => -1
+        ));
+    }
+
+    public function test_PivotByParam_WorksWithCsvOutput()
+    {
+        $this->assertApiResponseEqualsExpected("Referrers.getKeywords", array(
+            'idSite' => self::$fixture->idSite,
+            'date' => Date::factory(self::$fixture->dateTime)->toString(),
+            'period' => 'week',
+            'format' => 'csv',
+            'pivotBy' => 'UserCountry.City',
+            'pivotByColumn' => 'nb_visits',
+            'pivotByColumnLimit' => -1
+        ));
+    }
+
+    public function test_PivotByParam_PlaysNiceWithDataTableMaps()
+    {
+        $this->assertApiResponseEqualsExpected("Referrers.getKeywords", array(
+            'idSite' => 'all',
+            'date' => '2010-01-01,2010-01-07',
+            'period' => 'day',
+            'pivotBy' => 'UserCountry.City',
+            'pivotByColumn' => 'nb_visits',
+            'pivotByColumnLimit' => -1
+        ));
+    }
+}
+
+PivotByQueryParamTest::$fixture = new ManyVisitsWithMockLocationProvider();
\ No newline at end of file
diff --git a/tests/PHPUnit/Integration/expected/test_PivotByQueryParamTest_test_PivotByParam_PlaysNiceWithDataTableMaps__Referrers.getKeywords_day.xml b/tests/PHPUnit/Integration/expected/test_PivotByQueryParamTest_test_PivotByParam_PlaysNiceWithDataTableMaps__Referrers.getKeywords_day.xml
new file mode 100644
index 0000000000000000000000000000000000000000..d98cace554b06f9a86627c39a5da0b9e75d7b009
--- /dev/null
+++ b/tests/PHPUnit/Integration/expected/test_PivotByQueryParamTest_test_PivotByParam_PlaysNiceWithDataTableMaps__Referrers.getKeywords_day.xml
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<results>
+	<result idSite="1">
+		<result date="2010-01-01" />
+		<result date="2010-01-02" />
+		<result date="2010-01-03">
+			<row>
+				<col name="label">this search term</col>
+				<col name="1. Toronto, Ontario, Canada">0</col>
+				<col name="2. Yokohama, Kanagawa, Japan">1</col>
+				<col name="3. Melbourne, Victoria, Australia">2</col>
+			</row>
+			<row>
+				<col name="label">search term 2</col>
+				<col name="1. Toronto, Ontario, Canada">0</col>
+				<col name="2. Yokohama, Kanagawa, Japan">2</col>
+				<col name="3. Melbourne, Victoria, Australia">0</col>
+			</row>
+			<row>
+				<col name="label">search term 3</col>
+				<col name="1. Toronto, Ontario, Canada">2</col>
+				<col name="2. Yokohama, Kanagawa, Japan">0</col>
+				<col name="3. Melbourne, Victoria, Australia">0</col>
+			</row>
+			<row>
+				<col name="label">search term 4</col>
+				<col name="1. Toronto, Ontario, Canada">2</col>
+				<col name="2. Yokohama, Kanagawa, Japan">0</col>
+				<col name="3. Melbourne, Victoria, Australia">0</col>
+			</row>
+			<row>
+				<col name="label">that search term</col>
+				<col name="1. Toronto, Ontario, Canada">0</col>
+				<col name="2. Yokohama, Kanagawa, Japan">0</col>
+				<col name="3. Melbourne, Victoria, Australia">2</col>
+			</row>
+			<row>
+				<col name="label">search term 1</col>
+				<col name="1. Toronto, Ontario, Canada">0</col>
+				<col name="2. Yokohama, Kanagawa, Japan">1</col>
+				<col name="3. Melbourne, Victoria, Australia">0</col>
+			</row>
+		</result>
+		<result date="2010-01-04" />
+		<result date="2010-01-05" />
+		<result date="2010-01-06" />
+		<result date="2010-01-07" />
+	</result>
+</results>
\ No newline at end of file
diff --git a/tests/PHPUnit/Integration/expected/test_PivotByQueryParamTest_test_PivotByParam_PlaysNiceWithOtherQueryParams__Referrers.getKeywords_week.xml b/tests/PHPUnit/Integration/expected/test_PivotByQueryParamTest_test_PivotByParam_PlaysNiceWithOtherQueryParams__Referrers.getKeywords_week.xml
new file mode 100644
index 0000000000000000000000000000000000000000..05553ef4f85728f7bac52dc5951d9f2f62a05e7f
--- /dev/null
+++ b/tests/PHPUnit/Integration/expected/test_PivotByQueryParamTest_test_PivotByParam_PlaysNiceWithOtherQueryParams__Referrers.getKeywords_week.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<result>
+	<row>
+		<label>this search term</label>
+		<Google>1</Google>
+		<Bing>0</Bing>
+	</row>
+	<row>
+		<label>search term 2</label>
+		<Google>0</Google>
+		<Bing>0</Bing>
+	</row>
+	<row>
+		<label>search term 3</label>
+		<Google>1</Google>
+		<Bing>0</Bing>
+	</row>
+	<row>
+		<label>search term 4</label>
+		<Google>0</Google>
+		<Bing>1</Bing>
+	</row>
+	<row>
+		<label>that search term</label>
+		<Google>1</Google>
+		<Bing>0</Bing>
+	</row>
+	<row>
+		<label>search term 1</label>
+		<Google>0</Google>
+		<Bing>1</Bing>
+	</row>
+</result>
\ No newline at end of file
diff --git a/tests/PHPUnit/Integration/expected/test_PivotByQueryParamTest_test_PivotByParam_WorksWithColumnLimiting__Referrers.getKeywords_week.xml b/tests/PHPUnit/Integration/expected/test_PivotByQueryParamTest_test_PivotByParam_WorksWithColumnLimiting__Referrers.getKeywords_week.xml
new file mode 100644
index 0000000000000000000000000000000000000000..ac43686db06627a4d6ef390bc7553580055906ca
--- /dev/null
+++ b/tests/PHPUnit/Integration/expected/test_PivotByQueryParamTest_test_PivotByParam_WorksWithColumnLimiting__Referrers.getKeywords_week.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<result>
+	<row>
+		<col name="label">this search term</col>
+		<col name="1. Toronto, Ontario, Canada">0</col>
+		<col name="Others">3</col>
+	</row>
+	<row>
+		<col name="label">search term 2</col>
+		<col name="1. Toronto, Ontario, Canada">0</col>
+		<col name="Others">2</col>
+	</row>
+	<row>
+		<col name="label">search term 3</col>
+		<col name="1. Toronto, Ontario, Canada">2</col>
+		<col name="Others">0</col>
+	</row>
+	<row>
+		<col name="label">search term 4</col>
+		<col name="1. Toronto, Ontario, Canada">2</col>
+		<col name="Others">0</col>
+	</row>
+	<row>
+		<col name="label">that search term</col>
+		<col name="1. Toronto, Ontario, Canada">0</col>
+		<col name="Others">2</col>
+	</row>
+	<row>
+		<col name="label">search term 1</col>
+		<col name="1. Toronto, Ontario, Canada">0</col>
+		<col name="Others">1</col>
+	</row>
+</result>
\ No newline at end of file
diff --git a/tests/PHPUnit/Integration/expected/test_PivotByQueryParamTest_test_PivotByParam_WorksWithCsvOutput__Referrers.getKeywords_week.csv b/tests/PHPUnit/Integration/expected/test_PivotByQueryParamTest_test_PivotByParam_WorksWithCsvOutput__Referrers.getKeywords_week.csv
new file mode 100644
index 0000000000000000000000000000000000000000..a465a67a14a20fbfaaf85163f4fbd9493102d342
Binary files /dev/null and b/tests/PHPUnit/Integration/expected/test_PivotByQueryParamTest_test_PivotByParam_WorksWithCsvOutput__Referrers.getKeywords_week.csv differ
diff --git a/tests/PHPUnit/Integration/expected/test_PivotByQueryParamTest_test_PivotByParam_WorksWithJsonOutput__Referrers.getKeywords_week.json b/tests/PHPUnit/Integration/expected/test_PivotByQueryParamTest_test_PivotByParam_WorksWithJsonOutput__Referrers.getKeywords_week.json
new file mode 100644
index 0000000000000000000000000000000000000000..736901c89a03342083d13b99f8d043ba74241dbb
--- /dev/null
+++ b/tests/PHPUnit/Integration/expected/test_PivotByQueryParamTest_test_PivotByParam_WorksWithJsonOutput__Referrers.getKeywords_week.json
@@ -0,0 +1 @@
+[{"label":"this search term","1.&nbsp;Toronto, Ontario, Canada":false,"2.&nbsp;Yokohama, Kanagawa, Japan":1,"3.&nbsp;Melbourne, Victoria, Australia":2},{"label":"search term 2","1.&nbsp;Toronto, Ontario, Canada":false,"2.&nbsp;Yokohama, Kanagawa, Japan":2,"3.&nbsp;Melbourne, Victoria, Australia":false},{"label":"search term 3","1.&nbsp;Toronto, Ontario, Canada":2,"2.&nbsp;Yokohama, Kanagawa, Japan":false,"3.&nbsp;Melbourne, Victoria, Australia":false},{"label":"search term 4","1.&nbsp;Toronto, Ontario, Canada":2,"2.&nbsp;Yokohama, Kanagawa, Japan":false,"3.&nbsp;Melbourne, Victoria, Australia":false},{"label":"that search term","1.&nbsp;Toronto, Ontario, Canada":false,"2.&nbsp;Yokohama, Kanagawa, Japan":false,"3.&nbsp;Melbourne, Victoria, Australia":2},{"label":"search term 1","1.&nbsp;Toronto, Ontario, Canada":false,"2.&nbsp;Yokohama, Kanagawa, Japan":1,"3.&nbsp;Melbourne, Victoria, Australia":false}]
\ No newline at end of file
diff --git a/tests/PHPUnit/Integration/expected/test_PivotByQueryParamTest_test_PivotByParam_WorksWithReportWhoseSubtableIsSelf__Actions.getPageUrls_week.xml b/tests/PHPUnit/Integration/expected/test_PivotByQueryParamTest_test_PivotByParam_WorksWithReportWhoseSubtableIsSelf__Actions.getPageUrls_week.xml
new file mode 100644
index 0000000000000000000000000000000000000000..0b88c2ff59742c5f855defa955e558aacea2b3e9
--- /dev/null
+++ b/tests/PHPUnit/Integration/expected/test_PivotByQueryParamTest_test_PivotByParam_WorksWithReportWhoseSubtableIsSelf__Actions.getPageUrls_week.xml
@@ -0,0 +1,123 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<result>
+	<row>
+		<col name="label">0</col>
+		<col name="1. /index">1</col>
+		<col name="2. /14">0</col>
+		<col name="3. /13">0</col>
+		<col name="4. /12">0</col>
+		<col name="5. /11">0</col>
+		<col name="6. /15">0</col>
+		<col name="7. /16">0</col>
+		<col name="8. /19">0</col>
+		<col name="9. /18">0</col>
+		<col name="10. /17">0</col>
+		<col name="11. /10">0</col>
+		<col name="12. /9">0</col>
+		<col name="13. /3">1</col>
+		<col name="14. /2">1</col>
+		<col name="15. /1">1</col>
+		<col name="16. /4">0</col>
+		<col name="17. /5">0</col>
+		<col name="18. /8">0</col>
+		<col name="19. /7">0</col>
+		<col name="20. /6">0</col>
+		<col name="21. /0">1</col>
+	</row>
+	<row>
+		<col name="label">1</col>
+		<col name="1. /index">1</col>
+		<col name="2. /14">0</col>
+		<col name="3. /13">0</col>
+		<col name="4. /12">0</col>
+		<col name="5. /11">0</col>
+		<col name="6. /15">0</col>
+		<col name="7. /16">0</col>
+		<col name="8. /19">0</col>
+		<col name="9. /18">0</col>
+		<col name="10. /17">0</col>
+		<col name="11. /10">0</col>
+		<col name="12. /9">0</col>
+		<col name="13. /3">0</col>
+		<col name="14. /2">0</col>
+		<col name="15. /1">0</col>
+		<col name="16. /4">1</col>
+		<col name="17. /5">1</col>
+		<col name="18. /8">0</col>
+		<col name="19. /7">1</col>
+		<col name="20. /6">1</col>
+		<col name="21. /0">0</col>
+	</row>
+	<row>
+		<col name="label">2</col>
+		<col name="1. /index">1</col>
+		<col name="2. /14">0</col>
+		<col name="3. /13">0</col>
+		<col name="4. /12">0</col>
+		<col name="5. /11">1</col>
+		<col name="6. /15">0</col>
+		<col name="7. /16">0</col>
+		<col name="8. /19">0</col>
+		<col name="9. /18">0</col>
+		<col name="10. /17">0</col>
+		<col name="11. /10">1</col>
+		<col name="12. /9">1</col>
+		<col name="13. /3">0</col>
+		<col name="14. /2">0</col>
+		<col name="15. /1">0</col>
+		<col name="16. /4">0</col>
+		<col name="17. /5">0</col>
+		<col name="18. /8">1</col>
+		<col name="19. /7">0</col>
+		<col name="20. /6">0</col>
+		<col name="21. /0">0</col>
+	</row>
+	<row>
+		<col name="label">3</col>
+		<col name="1. /index">1</col>
+		<col name="2. /14">1</col>
+		<col name="3. /13">1</col>
+		<col name="4. /12">1</col>
+		<col name="5. /11">0</col>
+		<col name="6. /15">1</col>
+		<col name="7. /16">0</col>
+		<col name="8. /19">0</col>
+		<col name="9. /18">0</col>
+		<col name="10. /17">0</col>
+		<col name="11. /10">0</col>
+		<col name="12. /9">0</col>
+		<col name="13. /3">0</col>
+		<col name="14. /2">0</col>
+		<col name="15. /1">0</col>
+		<col name="16. /4">0</col>
+		<col name="17. /5">0</col>
+		<col name="18. /8">0</col>
+		<col name="19. /7">0</col>
+		<col name="20. /6">0</col>
+		<col name="21. /0">0</col>
+	</row>
+	<row>
+		<col name="label">4</col>
+		<col name="1. /index">1</col>
+		<col name="2. /14">0</col>
+		<col name="3. /13">0</col>
+		<col name="4. /12">0</col>
+		<col name="5. /11">0</col>
+		<col name="6. /15">0</col>
+		<col name="7. /16">1</col>
+		<col name="8. /19">1</col>
+		<col name="9. /18">1</col>
+		<col name="10. /17">1</col>
+		<col name="11. /10">0</col>
+		<col name="12. /9">0</col>
+		<col name="13. /3">0</col>
+		<col name="14. /2">0</col>
+		<col name="15. /1">0</col>
+		<col name="16. /4">0</col>
+		<col name="17. /5">0</col>
+		<col name="18. /8">0</col>
+		<col name="19. /7">0</col>
+		<col name="20. /6">0</col>
+		<col name="21. /0">0</col>
+	</row>
+</result>
\ No newline at end of file
diff --git a/tests/PHPUnit/Integration/expected/test_PivotByQueryParamTest_test_PivotBySegment_CreatesCorrectPivotTable_WhenSegmentUsedInRequest__Referrers.getKeywords_week.xml b/tests/PHPUnit/Integration/expected/test_PivotByQueryParamTest_test_PivotBySegment_CreatesCorrectPivotTable_WhenSegmentUsedInRequest__Referrers.getKeywords_week.xml
new file mode 100644
index 0000000000000000000000000000000000000000..52686d66101e456b3eca72e7552e49a15269206c
--- /dev/null
+++ b/tests/PHPUnit/Integration/expected/test_PivotByQueryParamTest_test_PivotBySegment_CreatesCorrectPivotTable_WhenSegmentUsedInRequest__Referrers.getKeywords_week.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<result>
+	<row>
+		<col name="label">that search term</col>
+		<col name="1. Melbourne, Victoria, Australia">2</col>
+	</row>
+	<row>
+		<col name="label">this search term</col>
+		<col name="1. Melbourne, Victoria, Australia">2</col>
+	</row>
+</result>
\ No newline at end of file
diff --git a/tests/PHPUnit/Integration/expected/test_PivotByQueryParamTest_test_PivotBySegment_CreatesCorrectPivotTable__Referrers.getKeywords_week.xml b/tests/PHPUnit/Integration/expected/test_PivotByQueryParamTest_test_PivotBySegment_CreatesCorrectPivotTable__Referrers.getKeywords_week.xml
new file mode 100644
index 0000000000000000000000000000000000000000..8112e991f5224325aa229f9236aaaf6392658e7b
--- /dev/null
+++ b/tests/PHPUnit/Integration/expected/test_PivotByQueryParamTest_test_PivotBySegment_CreatesCorrectPivotTable__Referrers.getKeywords_week.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<result>
+	<row>
+		<col name="label">this search term</col>
+		<col name="1. Toronto, Ontario, Canada">0</col>
+		<col name="2. Yokohama, Kanagawa, Japan">1</col>
+		<col name="3. Melbourne, Victoria, Australia">2</col>
+	</row>
+	<row>
+		<col name="label">search term 2</col>
+		<col name="1. Toronto, Ontario, Canada">0</col>
+		<col name="2. Yokohama, Kanagawa, Japan">2</col>
+		<col name="3. Melbourne, Victoria, Australia">0</col>
+	</row>
+	<row>
+		<col name="label">search term 3</col>
+		<col name="1. Toronto, Ontario, Canada">2</col>
+		<col name="2. Yokohama, Kanagawa, Japan">0</col>
+		<col name="3. Melbourne, Victoria, Australia">0</col>
+	</row>
+	<row>
+		<col name="label">search term 4</col>
+		<col name="1. Toronto, Ontario, Canada">2</col>
+		<col name="2. Yokohama, Kanagawa, Japan">0</col>
+		<col name="3. Melbourne, Victoria, Australia">0</col>
+	</row>
+	<row>
+		<col name="label">that search term</col>
+		<col name="1. Toronto, Ontario, Canada">0</col>
+		<col name="2. Yokohama, Kanagawa, Japan">0</col>
+		<col name="3. Melbourne, Victoria, Australia">2</col>
+	</row>
+	<row>
+		<col name="label">search term 1</col>
+		<col name="1. Toronto, Ontario, Canada">0</col>
+		<col name="2. Yokohama, Kanagawa, Japan">1</col>
+		<col name="3. Melbourne, Victoria, Australia">0</col>
+	</row>
+</result>
\ No newline at end of file
diff --git a/tests/PHPUnit/Integration/expected/test_PivotByQueryParamTest_test_PivotBySubtableDimension_CreatesCorrectPivotTable_WhenEntireHirearchyIsNotLoaded__Referrers.getKeywords_week.xml b/tests/PHPUnit/Integration/expected/test_PivotByQueryParamTest_test_PivotBySubtableDimension_CreatesCorrectPivotTable_WhenEntireHirearchyIsNotLoaded__Referrers.getKeywords_week.xml
new file mode 100644
index 0000000000000000000000000000000000000000..6ab56304222a894c5f55c6b2fc56ee557b38e1e7
--- /dev/null
+++ b/tests/PHPUnit/Integration/expected/test_PivotByQueryParamTest_test_PivotBySubtableDimension_CreatesCorrectPivotTable_WhenEntireHirearchyIsNotLoaded__Referrers.getKeywords_week.xml
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<result>
+	<row>
+		<col name="label">this search term</col>
+		<col name="1. Google">1</col>
+		<col name="2. Yahoo!">1</col>
+		<col name="3. Ask">1</col>
+		<col name="4. Bing">0</col>
+		<col name="5. Alexa">0</col>
+		<col name="6. Babylon">0</col>
+	</row>
+	<row>
+		<col name="label">search term 2</col>
+		<col name="1. Google">0</col>
+		<col name="2. Yahoo!">0</col>
+		<col name="3. Ask">0</col>
+		<col name="4. Bing">0</col>
+		<col name="5. Alexa">1</col>
+		<col name="6. Babylon">1</col>
+	</row>
+	<row>
+		<col name="label">search term 3</col>
+		<col name="1. Google">1</col>
+		<col name="2. Yahoo!">0</col>
+		<col name="3. Ask">1</col>
+		<col name="4. Bing">0</col>
+		<col name="5. Alexa">0</col>
+		<col name="6. Babylon">0</col>
+	</row>
+	<row>
+		<col name="label">search term 4</col>
+		<col name="1. Google">0</col>
+		<col name="2. Yahoo!">1</col>
+		<col name="3. Ask">0</col>
+		<col name="4. Bing">1</col>
+		<col name="5. Alexa">0</col>
+		<col name="6. Babylon">0</col>
+	</row>
+	<row>
+		<col name="label">that search term</col>
+		<col name="1. Google">1</col>
+		<col name="2. Yahoo!">1</col>
+		<col name="3. Ask">0</col>
+		<col name="4. Bing">0</col>
+		<col name="5. Alexa">0</col>
+		<col name="6. Babylon">0</col>
+	</row>
+	<row>
+		<col name="label">search term 1</col>
+		<col name="1. Google">0</col>
+		<col name="2. Yahoo!">0</col>
+		<col name="3. Ask">0</col>
+		<col name="4. Bing">1</col>
+		<col name="5. Alexa">0</col>
+		<col name="6. Babylon">0</col>
+	</row>
+</result>
\ No newline at end of file
diff --git a/tests/PHPUnit/Integration/expected/test_PivotByQueryParamTest_test_PivotBySubtableDimension_CreatesCorrectPivotTable__Referrers.getKeywords_week.xml b/tests/PHPUnit/Integration/expected/test_PivotByQueryParamTest_test_PivotBySubtableDimension_CreatesCorrectPivotTable__Referrers.getKeywords_week.xml
new file mode 100644
index 0000000000000000000000000000000000000000..1adf9d722a2cf3803f9eff4e97f53d9e6eb9f7f7
--- /dev/null
+++ b/tests/PHPUnit/Integration/expected/test_PivotByQueryParamTest_test_PivotBySubtableDimension_CreatesCorrectPivotTable__Referrers.getKeywords_week.xml
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<result>
+	<row>
+		<col name="label">this search term</col>
+		<col name="Google">1</col>
+		<col name="Yahoo!">1</col>
+		<col name="Ask">1</col>
+		<col name="Bing">0</col>
+		<col name="Alexa">0</col>
+		<col name="Babylon">0</col>
+	</row>
+	<row>
+		<col name="label">search term 2</col>
+		<col name="Google">0</col>
+		<col name="Yahoo!">0</col>
+		<col name="Ask">0</col>
+		<col name="Bing">0</col>
+		<col name="Alexa">1</col>
+		<col name="Babylon">1</col>
+	</row>
+	<row>
+		<col name="label">search term 3</col>
+		<col name="Google">1</col>
+		<col name="Yahoo!">0</col>
+		<col name="Ask">1</col>
+		<col name="Bing">0</col>
+		<col name="Alexa">0</col>
+		<col name="Babylon">0</col>
+	</row>
+	<row>
+		<col name="label">search term 4</col>
+		<col name="Google">0</col>
+		<col name="Yahoo!">1</col>
+		<col name="Ask">0</col>
+		<col name="Bing">1</col>
+		<col name="Alexa">0</col>
+		<col name="Babylon">0</col>
+	</row>
+	<row>
+		<col name="label">that search term</col>
+		<col name="Google">1</col>
+		<col name="Yahoo!">1</col>
+		<col name="Ask">0</col>
+		<col name="Bing">0</col>
+		<col name="Alexa">0</col>
+		<col name="Babylon">0</col>
+	</row>
+	<row>
+		<col name="label">search term 1</col>
+		<col name="Google">0</col>
+		<col name="Yahoo!">0</col>
+		<col name="Ask">0</col>
+		<col name="Bing">1</col>
+		<col name="Alexa">0</col>
+		<col name="Babylon">0</col>
+	</row>
+</result>
\ No newline at end of file
diff --git a/tests/PHPUnit/IntegrationTestCase.php b/tests/PHPUnit/IntegrationTestCase.php
index 59160c7c309af64679bd705d5ba4d48a83befc62..ae8a674ed2e636d47ed754a1dbfa94639612cbaf 100755
--- a/tests/PHPUnit/IntegrationTestCase.php
+++ b/tests/PHPUnit/IntegrationTestCase.php
@@ -274,11 +274,17 @@ abstract class IntegrationTestCase extends PHPUnit_Framework_TestCase
         list($processedFilePath, $expectedFilePath) =
             $this->getProcessedAndExpectedPaths($testName, $apiId, $format = null, $compareAgainst);
 
+        $originalGET = $_GET;
+        $_GET = $requestUrl;
+        unset($_GET['serialize']);
+
         $processedResponse = TestRequestResponse::loadFromApi($params, $requestUrl);
         if (empty($compareAgainst)) {
             $processedResponse->save($processedFilePath);
         }
 
+        $_GET = $originalGET;
+
         try {
             $expectedResponse = TestRequestResponse::loadFromFile($expectedFilePath, $params, $requestUrl);
         } catch (Exception $ex) {
@@ -395,6 +401,7 @@ abstract class IntegrationTestCase extends PHPUnit_Framework_TestCase
             'idSite' => $queryParams['idSite'],
             'date' => $queryParams['date'],
             'periods' => $queryParams['period'],
+            'format' => isset($queryParams['format']) ? $queryParams['format'] : 'xml',
             'testSuffix' => '_' . $this->getName(), // TODO: instead of using a test suffix, the whole file name should just be the test method
             'otherRequestParameters' => $queryParams
         ));