{"action":"created","comment":{"url":"https://api.github.com/repos/piwik/piwik/pulls/comments/35641697","id":35641697,"diff_hunk":"@@ -947,64 +948,6 @@ public static function getPrettyDate($date, $period)\n         return self::getCalendarPrettyDate(Period\\Factory::build($period, Date::factory($date)));\n     }\n \n-    /**\n-     * Calculates the evolution from one value to another and returns HTML displaying\n-     * the evolution percent. The HTML includes an up/down arrow and is colored red, black or\n-     * green depending on whether the evolution is negative, 0 or positive.\n-     *\n-     * No HTML is returned if the current value and evolution percent are both 0.\n-     *\n-     * @param string $date The date of the current value.\n-     * @param int $currentValue The value to calculate evolution to.\n-     * @param string $pastDate The date of past value.\n-     * @param int $pastValue The value in the past to calculate evolution from.\n-     * @return string|false The HTML or `false` if the evolution is 0 and the current value is 0.\n-     * @api\n-     */\n-    protected function getEvolutionHtml($date, $currentValue, $pastDate, $pastValue)","path":"core/Plugin/Controller.php","position":35,"original_position":35,"commit_id":"bab2c1a31d13632979cf2c0fddfdc1b765b80451","original_commit_id":"d34d066d45ff3f7705613c06d4d4d0924e53791c","user":{"login":"tsteur","id":273120,"avatar_url":"https://avatars.githubusercontent.com/u/273120?v=3","gravatar_id":"","url":"https://api.github.com/users/tsteur","html_url":"https://github.com/tsteur","followers_url":"https://api.github.com/users/tsteur/followers","following_url":"https://api.github.com/users/tsteur/following{/other_user}","gists_url":"https://api.github.com/users/tsteur/gists{/gist_id}","starred_url":"https://api.github.com/users/tsteur/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/tsteur/subscriptions","organizations_url":"https://api.github.com/users/tsteur/orgs","repos_url":"https://api.github.com/users/tsteur/repos","events_url":"https://api.github.com/users/tsteur/events{/privacy}","received_events_url":"https://api.github.com/users/tsteur/received_events","type":"User","site_admin":false},"body":"Was only used in Referrers\\Controller. Now implemented by `Sparklines` visualization","created_at":"2015-07-28T12:40:12Z","updated_at":"2015-07-28T12:40:12Z","html_url":"https://github.com/piwik/piwik/pull/8442#discussion_r35641697","pull_request_url":"https://api.github.com/repos/piwik/piwik/pulls/8442","_links":{"self":{"href":"https://api.github.com/repos/piwik/piwik/pulls/comments/35641697"},"html":{"href":"https://github.com/piwik/piwik/pull/8442#discussion_r35641697"},"pull_request":{"href":"https://api.github.com/repos/piwik/piwik/pulls/8442"}}},"pull_request":{"url":"https://api.github.com/repos/piwik/piwik/pulls/8442","id":41005267,"html_url":"https://github.com/piwik/piwik/pull/8442","diff_url":"https://github.com/piwik/piwik/pull/8442.diff","patch_url":"https://github.com/piwik/piwik/pull/8442.patch","issue_url":"https://api.github.com/repos/piwik/piwik/issues/8442","number":8442,"state":"open","locked":false,"title":"Provide an API for plugins to display (multiple) reports in a page","user":{"login":"tsteur","id":273120,"avatar_url":"https://avatars.githubusercontent.com/u/273120?v=3","gravatar_id":"","url":"https://api.github.com/users/tsteur","html_url":"https://github.com/tsteur","followers_url":"https://api.github.com/users/tsteur/followers","following_url":"https://api.github.com/users/tsteur/following{/other_user}","gists_url":"https://api.github.com/users/tsteur/gists{/gist_id}","starred_url":"https://api.github.com/users/tsteur/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/tsteur/subscriptions","organizations_url":"https://api.github.com/users/tsteur/orgs","repos_url":"https://api.github.com/users/tsteur/repos","events_url":"https://api.github.com/users/tsteur/events{/privacy}","received_events_url":"https://api.github.com/users/tsteur/received_events","type":"User","site_admin":false},"body":"fixes #7822\r\n\r\nThis PR brings many changes and it was not really possible to do smaller PRs as it all affected each other:\r\n\r\n* New Widgets API\r\n* New API methods `API.getWidgetMetadata()`, `API.getReportPagesMetadata()`\r\n* Many new classes that will be API and many breaking changes see `CHANGELOG.md` \r\n* We render the page layout in browser, widgets/reports on any page are now requested in parallel\r\n* New Sparklines visualization\r\n* Any report / plugin can add sparklines to other reports\r\n* One can customize pages (partially)\r\n* Order of reports / widgets are no longer hard coded, they are determined by categories and subcategories\r\n* Widgets & Dashboard menu changed. It should be more intuitive to find a report (similar to the actual reporting menu)\r\n* Url for reporting pages changed: Instead of `...#/idSite=...` we now have `...#?idSite=` which is more correct (as those are query parameters) and easier to use as it is supported by Angular.\r\n\r\nOverall I'd say we got rid of some duplicated code and different UI behaviour. Data is reused where possible so that the order of reports and widgets etc is more consistent and when updated in one position, no update somewhere else is needed. Eg the menu items in \"Widgets & Dashboard\" selector were completely different to the position of reports in the UI as we forgot to update it.\r\n\r\n\r\n\r\n## Pages\r\n\r\nWe got rid of many controllers and actions that were responsible to render reporting pages. From now on any page is constructed pased on widgets. A page is identifed by a `categoryId` (eg 'General_Actions') and a `subcategoryId` (eg 'General_Pages'). \r\n\r\n### On initial reporting page load\r\n\r\nThere is a new API `API.getReportPagesMetadata` that returns all reporting pages including the widgets on each page and the structure of each page looks like this:\r\n\r\n```js\r\n{\r\n\tuniqueId: \"General_Actions.General_Downloads\",\r\n\tcategory: {\r\n\t\tid: \"General_Actions\",\r\n\t\tname: \"Actions\",\r\n\t\torder: 10\r\n\t},\r\n\tsubcategory: {\r\n\t\tid: \"General_Downloads\",\r\n\t\tname: \"Downloads\",\r\n\t\torder: 35\r\n\t},\r\n\twidgets: [\r\n\t\t{\r\n\t\t\tname: \"Downloads\",\r\n\t\t\tmodule: \"Actions\",\r\n\t\t\taction: \"getDownloads\",\r\n\t\t\torder: 109,\r\n\t\t\tparameters: {\r\n\t\t\t\tmodule: \"Actions\",\r\n\t\t\t\taction: \"getDownloads\"\r\n\t\t\t},\r\n\t\t\tuniqueId: \"widgetActionsgetDownloads\",\r\n\t\t\tviewDataTable: \"table\",\r\n\t\t\tisReport: true\r\n\t\t}\r\n\t]\r\n}\r\n```\r\n\r\nIt defines a page for `Actions => Downloads` and only one widget shall be shown within that page.\r\n\r\nThe frontend fetches the data of all pages and creates the reporting menu based on this information as well as the currently selected reporting page. It's quite a lot of data to load but with gzipped it's not that much. On my local server it takes about 300ms to generate and load this data, on a fast server it should be even faster. We only load this list on the initial page load and as long as one doesn't change the date or idSite this list does not need to be reloaded. The good thing is, from then on we have the information of all pages. Meaning when clicking on a menu item, we can build the new page very fast and at least the headlines of the reports will be visible immediately.\r\n\r\nHaving everything based on the above API will make it possible to have a very flexible UI that can be entirely changed eg by a measurable type like \"Mobile app\".\r\n\r\nTo build this list, the system looks for all `Widget` classes provided by plugins, it triggers an event `Widget.addWidgetConfigs` and it asks each report to add widgets.\r\n\r\n### Reporting menu\r\n\r\nAs you may notice for each page there is a `category` and `subcategory`. Based on this we can build the menu. Each `category` represents a main menu item, and each `subcategory` represents a submenu item of a particular main menu item. They are sorted depending on the specified `order`. The `id` of a category or a subcategory will be used in the URL. Eg `category=General_Actions&subcategory=General_Downloads` will be used to render that page. If there's no categroy or subcategory given, we cannot render a page. If both are not given, we select the inital page which is the page having the lowest category and subcategory order (in our case currently \"Dashboard\" page). \r\n\r\nFor the rendering responsible is the new angular directive `<div piwik-reporting-menu/>`. As soon as someone clicks on a menu item, the URL will be changed and a new page will be rendered immediately because AngularJS triggers a `$locationChangeSuccess` event. \r\n\r\nA typical URL for a reporting page looks like this:\r\n\r\n```\r\nhttp://apache.piwik/index.php?module=CoreHome&action=index&idSite=1&period=day&date=2015-07-02#?idSite=1&period=day&date=2015-07-02&category=General_Actions&subcategory=General_Pages\r\n```\r\n\r\n\r\n### Reporting page\r\n\r\nThe new angular directive `<div piwik-reporting-page/>` is listening to changes in the URL. As soon as there is a change it renders the requested page depending on the `category` and `subcategory` URL parameter. As mentioned the data for each page is already present so we can replace the old page immediately with the new one. Each widget containing the actual data still needs a bit of time to render. As the widgets can now be rendered in parallel (eg some pages have 5 or more widgets on one page) it should be faster to load a particular page.\r\n\r\nThe reporting page directive is responsible for rendering a particular page. Eg if an evolution and sparkline widget is defined for a page, it will make sure to show these widgets on the top. If there's only one widget it will show that widget with full width (eg \"Action pages\") unless specified differently via CSS. If there's more than one widget, it will put these widgets automatically into two columns and also take care of proper headline spacing etc.\r\n\r\nIt is no longer possible to add any menu items via `ReportingMenu::add(...)`. The reporting menu and reporting pages are fully based on the API `API.getReportPagesMetadata`. If one wants to have something shown in a reporting page, one needs to create a widget (eg via a Report or via `Widget` class).\r\n\r\n\r\n### Widgets within a page\r\n\r\nEach widget that is defined by a widget class or by any report will be shown on a reporting page as long as it defines a `categoryId` and `subcategoryId`. If a widget does not define a `subcategoryId` it won't be visible in a reporting page but it would be still possible to put it on a dashboard or to export it unless `$widget->setIsNotWidgetizable()` is called. `setIsNotWidgetizable()` makes it eg possible to put a widget on a reporting page but not have it available in the list of exportable widgets. This is useful eg for `Manage Goals` and many other widgets.\r\n\r\nThe order of each widget defines the position of a widget within a page. If a widget is based on a report that has a related report, and this related report would be visible on that page again (eg `Devices.getOs` and `Devices.getOsFamilies` are related reports and they would be both visible on one page), then we show only one of those reports (the one having the lower order). \r\n\r\n\r\n#### New widget API\r\n\r\nInstead of having one `Widgets` class per plugin that defines multiple widgets we now have one `Widget` class per widget and it is basically configured like this:\r\n\r\n```php\r\nclass GetVisitorProfilePopup extends \\Piwik\\Plugin\\Widget\r\n{\r\n    public static function configure(WidgetConfig $config)\r\n    {\r\n        $config->setCategory('Live!');\r\n        $config->setName('Live_VisitorProfile');\r\n        $config->setOrder(25);\r\n        $config->disable();\r\n    }\r\n\r\n    public function render()\r\n    {\r\n    }\r\n}\r\n```\r\n\r\nThey can be located in `Widgets` directory and we can now generate more than one widget (as it is one file per widget). Internally we work with `WidgetConfig` which is a rather stupid data object. The actual widget is only needed to render it. Dependencies can be defined in `__construct`, the dependencies will be only created if needed to keep gathering widget config information fast. To also keep widget generation fast and cachable we always work with translation keys (eg `->setName('Live_VisitorProfile')` instead of `->setName(Piwik::translate('Live_VisitorProfile'))`).\r\n\r\n`WidgetConfig` can be also used in a `Widget.addWidgetConfigs` event:\r\n\r\n```php\r\n$config = new WidgetConfig();\r\n$config->setCategory('Live!');\r\n$config->setName('UserCountryMap_RealTimeMap');\r\n$config->setModule('UserCountryMap');\r\n$config->setAction('realtimeMap');\r\n$config->setOrder(5);\r\n$widgetConfigs[] = $config;\r\n\r\n// instead of \r\nWidgetsList::add('Live!', Piwik::translate('UserCountryMap_RealTimeMap'), 'UserCountryMap', 'realtimeMap', array(), $order = 5);\r\n\r\n```\r\n\r\nIf one wants to generate a widget from a report all one has to do is to define a `subcategoryId` like this:\r\n\r\n```php\r\n    protected function init()\r\n    {\r\n        $this->categoryId    = 'General_Actions';\r\n        $this->subcategoryId = 'General_Pages';\r\n    }\r\n    // this will create a default widget (using default visualization) for this report and it will be displayed in a reporting page having the menu item \"Actions -> Pages\"\r\n    \r\n```\r\n\r\nIt is the same as doing this manually:\r\n\r\n```php\r\n    public function configureWidgets(WidgetsList $widgetsList, ReportWidgetFactory $factory)\r\n    {\r\n        $widgetsList->addWidgetConfig($factory->createWidget());\r\n\r\n        // $factory->createWidget() will created a `WidgetConfig` based on all information that is already present in the report (Report name, category, order, subcategory, module, action, ...)\r\n    }\r\n    \r\n```\r\n\r\nMany widgets can be generated from one report and they can be customized. Eg \r\n\r\n```php\r\n    public function configureWidgets(WidgetsList $widgetsList, ReportWidgetFactory $factory)\r\n    {\r\n        $widgetsList->addWidgetConfig(\r\n            $factory->createWidget()\r\n                ->setName('VisitsSummary_WidgetLastVisits')\r\n                ->forceViewDataTable(Evolution::ID)\r\n                ->setAction('getEvolutionGraph')\r\n                ->setOrder(5)\r\n        );\r\n\r\n        $widgetsList->addWidgetConfig(\r\n            $factory->createWidget()\r\n                ->setName('VisitsSummary_WidgetVisits')\r\n                ->forceViewDataTable(Sparklines::ID)\r\n                ->setOrder(10)\r\n        );\r\n    }\r\n```\r\n\r\n##### Widget middleware parameters\r\n\r\nThere are widgets that should be only shown if an archives contains specific data. For example the \"Goals Conversion Overview\" widget should be only shown if there are actually conversions for the current selected date/period. As mentioned earlier when requesting the initial page we collect all information about all pages and all widgets. Loading this list would be very slow if we had to archive data and fetch reports in order to build this list. Also as we only load this list initially, a widget might not be added to this list if at the point of the initial page load there are no conversions, but at a later point there might be conversions. Also the list of available widgets wouldn't be cachable if it would depend on archived data since we'd have to invalidate the cache whenever new data is tracked or archived. \r\n\r\nTo have the gathering of all widgets fast I introduced \"Middleware parameters\". I named it this way as it is kinda used similar in other frameworks like Slim and Laravel. If a widget should be only shown based on archived data, one can set middleware parameters like this:\r\n\r\n```php\r\n$config->setMiddlewareParameters(array('module' => 'Goals', 'action' => 'hasConversions'));\r\n```\r\n\r\nWhenever a page is requested that contains this widget, we will first perform a request to the specified URL (idSite, period, date is added automatically). If the response contains a JSON `true` we will display the widget, otherwise not. If one clicks on the same page again, we will again perform this check. Meaning even if initially there are no conversions, if one clicks on the link again the widget might be shown if there were conversions meanwhile.\r\n\r\n\r\n#### Widget container\r\n\r\nSometimes the structure of the UI might be a bit more complex. As mentioned before: As soon as there is more than one widget on a page, they are shown in two columns. If you want to have two independent widgets below each other (and not next to each other) you need to group them as done for example in the `getContinent` report:\r\n\r\n```php\r\npublic function configureWidgets(WidgetsList $widgetsList, ReportWidgetFactory $factory)\r\n    {\r\n    $containerId = 'Continent';\r\n    $container = $factory->createContainerWidget($containerId);\r\n    $widgetsList->addWidgetConfig($container);\r\n\r\n    $container->addWidgetConfig($factory->createWidget());\r\n\r\n    $widget = $factory->createWidget()->setAction('getDistinctCountries');\r\n    $container->addWidgetConfig($containerId, $widget);\r\n}\r\n \r\n```\r\n\r\nAny other report can add reports to this container via the `$containerId` as well:\r\n\r\n```php\r\npublic function configureWidgets(WidgetsList $widgetsList, ReportWidgetFactory $factory)\r\n    {\r\n    $widget = $factory->createWidget();\r\n    $widgetsList->addToContainerWidget($containerId = 'Container', $widget);\r\n}\r\n```\r\n\r\nContainer can as well have a layout in case they are not supposed to be shown below each other. For example you could set a layout `ByDimension` which will show only one of the widgets at a time on the right side and a menu to select a widget on the left side.\r\n\r\n```php\r\n$container->setLayout(CoreHome::WIDGET_CONTAINER_LAYOUT_BY_DIMENSION);\r\n```\r\n\r\nWidget containers can contain any number of widgets and even widget container itself.\r\n\r\n### Changing the order of categories and subcategories\r\n \r\nAs seen earlier in the output a category and subcategory have an `order` to find the correct position:\r\n\r\n\r\n```js\r\n\tcategory: {\r\n\t\tid: \"General_Actions\",\r\n\t\tname: \"Actions\",\r\n\t\torder: 10\r\n\t},\r\n\tsubcategory: {\r\n\t\tid: \"General_Downloads\",\r\n\t\tname: \"Downloads\",\r\n\t\torder: 35\r\n\t},\r\n```\r\n\r\nA report consists of a category and optionally a subcategory. The order of the category / subcategory is now used to sort reports (eg for `API.getReportMetadata`). This was hard coded in the past.\r\n\r\nA widget conists of a category and optionally a subcategory as well. We use this to sort eg the output of `API.getReportPagesMetadata` and `API.getWidgetMetadata`. Meaning the lower the order of the category, the further left the menu item for that category will be shown. Same for subcategories.\r\n\r\nBy default each category and subcategory have an order of `99`. It is possible to customize this behavior though by defining a category class:\r\n\r\n```php\r\nclass VisitorsCategory extends Category\r\n{\r\n    protected $id = 'General_Visitors';\r\n    protected $order = 5;\r\n}\r\n```\r\n\r\nor by defining a subcategory class:\r\n\r\n```php\r\nclass VisitorsOverviewSubcategory extends Subcategory\r\n{\r\n    protected $categoryId = 'General_Visitors';\r\n    protected $id = 'General_Overview';\r\n    protected $order = 2;\r\n\r\n}\r\n```\r\n\r\nThose classes can be placed in a plugin in a `Categories` folder. I did this for all of our plugins to make sure we have the same order in reporting menu as before. A plugin developer would not have to take care of this as those menu items would be just shown after our core menu items (because orderId is `99` by default).\r\n\r\n## Sparklines Visualization\r\n\r\nThe new sparklines visualization shows one or multiple sparklines. Each sparkline item consists of a sparkline graph, at least one metric and one description. Optionally it can show an evolution beside each item. For more documentation see the `Sparklines` and `Sparklines\\Config` PHP docs. An example usage in a report looks like this:\r\n\r\n\r\n```php\r\n    public function configureWidgets(WidgetsList $widgetsList, ReportWidgetFactory $factory)\r\n    {\r\n        $widgetsList->addWidgetConfig(\r\n            $factory->createWidget()->forceViewDataTable(Sparklines::ID)\r\n        );\r\n    }\r\n    \r\n    public function configureView(ViewDataTable $view)\r\n    {\r\n        if ($view->isViewDataTableId(Sparklines::ID)) {\r\n        \t$view->config->addSparklineMetric(array('nb_visits_returning'));\r\n        \t$view->config->addSparklineMetric(array('avg_time_on_site_returning'));\r\n        \t$view->config->addSparklineMetric(array('nb_actions_returning', 'nb_actions'));\r\n           \t$view->config->addTranslations(array(\r\n           \t    'nb_visits_returning' => 'returning visits',\r\n           \t    'avg_time_on_site_returning' => 'avg time on site',\r\n           \t    'nb_actions_returning' => 'returning actions: %s', // %s can be used optionally, by default value is shown first before the description,\r\n           \t    'nb_actions' => 'of %s total actions'\r\n           \t));\r\n        }\r\n    }\r\n    \r\n```\r\n\r\nAny report can listen to the `ViewDataTable.configure` event and add more sparklines.\r\n\r\n```php\r\n    public function getListHooksRegistered()\r\n    {\r\n        return array(\r\n            'ViewDataTable.configure' => 'configureViewDataTable',\r\n        );\r\n    }\r\n    \r\n    public function configureViewDataTable(ViewDataTable $view)\r\n    {\r\n        if ($view->requestConfig->apiMethodToRequestDataTable === 'Goals.get' && \r\n        \t$view->isViewDataTableId(Sparklines::ID)) {\r\n        \t$view->config->addSparklineMetric(array('nb_visits_returning'));\r\n           \t$view->config->addTranslations(array(\r\n           \t    'nb_visits_returning' => 'returning visits'\r\n           \t));\r\n        }\r\n    }\r\n```\r\n\r\n\r\n\r\n## New angular directives\r\n\r\nBesides the mentioned directives `piwik-reporting-menu` and `piwik-reporting-page` there are following new directives:\r\n\r\n* `<div piwik-activity-indicator loading=\"true/false\"/>` Shows our general loading message and the typical spinning wheel.\r\n* `<div piwik-dashboard dashboard-id=\"5\"/>` Shows the dashboard having the specified ID.\r\n* `<div piwik-popover-handler/>` If present on any page will listen to the popover URL parameter and open or close a popover. Only needed on a page initially.\r\n* `<div piwik-widget=\"widget\" widetized=\"true/false\"/>` renders any kind of widget as returned by the widget metadata API.\r\n* `<div piwik-widget-container=\"containerWidget\"/>` renders a container widget as returned by the widget metadata API. All widgets within this container are rendered below each other.\r\n* `<div piwik-widget-by-dimension-container=\"containerWidget\"/>` renders a container widget as returned by the widget metadata API. It shows a menu for each widget on the left and the select widget on the right.\r\n* `<div piwik-widget-loader=\"{module: '', action: '', ...}\"/>` similar to ng-include can request any action and shows the returned response of this action within this `<div/>`.\r\n\r\nA reporting page looks pretty much like this:\r\n\r\n```html\r\n<div piwik-popover-handler/>\r\n<div piwik-reporting-menu/>\r\n<div piwik-reporting-page>\r\n\t<div piwik-widget><div piwik-widget-loader></div>\r\n\t<div piwik-widget>\r\n\t\t<div piwik-widget-container>\r\n\t\t\t<div piwik-widget><div piwik-widget-loader></div>\r\n\t\t\t<div piwik-widget><div piwik-widget-loader></div>\r\n\t\t\t<div piwik-widget><div piwik-widget-loader></div>\r\n\t\t</div>\r\n\t</div>\r\n\t<div piwik-widget><div piwik-widget-loader></div>\r\n\t<div piwik-widget><div piwik-widget-loader></div>\r\n</div>\r\n```\r\n\r\n\r\nFurthermore we got kinda rid of the `broadcast` JS object. It is only used by Piwik Overlays now. It's still used to get params though. However, it is no longer used to fetch pages, to handle the history etc. Instead we use Angulars builtin `$location` where possible. This is the first step towards using Angulars builtin `$routing` but to actually use it, we need to do more work.\r\n\r\n## Other new API's\r\n\r\nThere's as well a new API method `API.getWidgetMetadata`. It is only kinda similar to `API.getReportPagesMetadata`. \r\n\r\n* `API.getReportPagesMetadata` returns information about all reporting UI pages. They are grouped by `category` and `subcategory` and only widgets will be shown in this list, if they have actually both a category and a subcategory defined. A widget that has no subcategory defined won't be displayed in a reporting page.\r\n* `API.getWidgetMetadata` is a simple list of all available widgets that can be added to the dashboard or exported via an iframe etc. It also contains widgets that have no `subcategory` defined. A `category` is mandatory though. \r\n\r\nIf a widget should be shown in a reporting page but not added to the widget metadata, one can call `$widgetConfig->setIsNotWidgetizable()`. \r\n\r\nIf a widget shall be not shown in a reporting page but widgetizable one should simply not set a `subcategoryId`\r\n\r\n## Small overview of new classes\r\n\r\nThe diff looks really big but it is not that much actually. New are mainly the following classes. Otherwise there are more or less only changes in the plugin to use the new page layout rendering mechanism instead of the old controller logic.\r\n\r\n* `Piwik\\Category\\CategoryList` Holds a list of all available categories\r\n* `Piwik\\Category\\Category` Defines a single category. A category holds a list of subcategories\r\n* `Piwik\\Category\\Subcategory` (API) Defines a single subcategory. \r\n* `Piwik\\Plugin\\Categories` Finds all categories and subcategories implemented by plugins\r\n\r\n* `Piwik\\Widget\\Widget` (API) Defines a standalone widget\r\n* `Piwik\\Widget\\WidgetConfig` (API) Data object that defines a widget configuration.\r\n* `Piwik\\Widget\\WidgetContainerConfig` (API) Data object that defines a widget  container configuration.\r\n* `Piwik\\Widget\\WidgetsList` (API) Holds a list of all configured widgets and widget container\r\n* `Piwik\\Plugin\\Widgets` Finds all widgets and widget configs implemented by plugins\r\n\r\n* `Piwik\\Report\\ReportWidgetConfig` (API) Data object for a widget configuration that is based on a report or created by a report. Allows you to set eg a viewDataTable to render etc\r\n* `Piwik\\Report\\ReportWidgetFactory` (API) Factory that lets you easily created widgets from a report\r\n* `Piwik\\Plugin\\Reports` Finds a specific report or all reports implemented by plugins\r\n\r\n### Code organization\r\n\r\nI made a few changes to the code organization. Eg `Widgets` is no longer under `Piwik\\Plugin\\Widget`, it is under `Piwik\\Widget\\Widget` and everything under this namespace contains so far only code related to Widgets (not really any other dependencies). All category related classes are under `Piwik\\Category`. They do not really know anything about Piwik and certainly not know anything about Widgets or Reports. \r\n\r\nI moved some code that finds all components (reports/widgets/categories) that are defined by plugins to `Piwik\\Plugin`. For example moved `Piwik\\Plugin\\Report::factory()` and `Piwik\\Plugin\\Report::getAllReports()` to a different class named `Piwik\\Plugin\\Reports`. Same for `Piwik\\Plugin\\Categories` and `Piwik\\Plugin\\Widgets`. For example:\r\n\r\n* Piwik\\Plugin\\Categories::getAllCategories()\r\n* Piwik\\Plugin\\Categories::getAllSubcategories()\r\n* Piwik\\Plugin\\Widgets::getAllWidgets()\r\n* Piwik\\Plugin\\Widgets::getAllWidgetConfigs()\r\n* Piwik\\Plugin\\Widgets::getAllWidgetContainerConfigs()\r\n\r\nThey are under `Piwik\\Plugin` as they find only concrete Report, Widget, Category, ... classes implemented by plugins. It would be nice to move most other classes at some point out of `Piwik\\Plugin` and eg have instead of a `Piwik\\Plugin\\Report` a `Piwik\\Report\\Report` class.\r\n\r\n## Better widgets list\r\n\r\nSee following image. We make sure it is more intuitive to find a widget by reusing the structure of the menu. As soon as a subcategory contains 3 or more widgets, we add a new menu item to the list. Eg `Visitors - Times` or `Ecommerce - Products`, otherwise they are listed in the related category eg `Visitors`.\r\n\r\n![widgetlist](https://cloud.githubusercontent.com/assets/273120/8931342/dea3da56-3535-11e5-952f-9ce8b058dacb.png)\r\n\r\n## Todo\r\n\r\n* We no longer have different naming for an exported widget and for the headline within a reporting page. They use the same naming for consistency, simplicity and because it is easier to implement and understand. We will probably need to adjust some namings of some widgets.\r\n* There might be still a view `TODO` in the code but these can be ignored\r\n","created_at":"2015-07-28T12:25:12Z","updated_at":"2015-07-28T12:40:12Z","closed_at":null,"merged_at":null,"merge_commit_sha":"350ba5741b01ca214f529cadc4a7d29886d1d4ec","assignee":null,"milestone":{"url":"https://api.github.com/repos/piwik/piwik/milestones/75","html_url":"https://github.com/piwik/piwik/milestones/3.0.0","labels_url":"https://api.github.com/repos/piwik/piwik/milestones/75/labels","id":887285,"number":75,"title":"3.0.0","description":"","creator":{"login":"mattab","id":466765,"avatar_url":"https://avatars.githubusercontent.com/u/466765?v=3","gravatar_id":"","url":"https://api.github.com/users/mattab","html_url":"https://github.com/mattab","followers_url":"https://api.github.com/users/mattab/followers","following_url":"https://api.github.com/users/mattab/following{/other_user}","gists_url":"https://api.github.com/users/mattab/gists{/gist_id}","starred_url":"https://api.github.com/users/mattab/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mattab/subscriptions","organizations_url":"https://api.github.com/users/mattab/orgs","repos_url":"https://api.github.com/users/mattab/repos","events_url":"https://api.github.com/users/mattab/events{/privacy}","received_events_url":"https://api.github.com/users/mattab/received_events","type":"User","site_admin":false},"open_issues":71,"closed_issues":4,"state":"open","created_at":"2014-12-02T02:19:24Z","updated_at":"2015-07-28T12:25:13Z","due_on":"2015-11-30T11:00:00Z","closed_at":null},"commits_url":"https://api.github.com/repos/piwik/piwik/pulls/8442/commits","review_comments_url":"https://api.github.com/repos/piwik/piwik/pulls/8442/comments","review_comment_url":"https://api.github.com/repos/piwik/piwik/pulls/comments{/number}","comments_url":"https://api.github.com/repos/piwik/piwik/issues/8442/comments","statuses_url":"https://api.github.com/repos/piwik/piwik/statuses/bab2c1a31d13632979cf2c0fddfdc1b765b80451","head":{"label":"piwik:7822_4","ref":"7822_4","sha":"bab2c1a31d13632979cf2c0fddfdc1b765b80451","user":{"login":"piwik","id":698038,"avatar_url":"https://avatars.githubusercontent.com/u/698038?v=3","gravatar_id":"","url":"https://api.github.com/users/piwik","html_url":"https://github.com/piwik","followers_url":"https://api.github.com/users/piwik/followers","following_url":"https://api.github.com/users/piwik/following{/other_user}","gists_url":"https://api.github.com/users/piwik/gists{/gist_id}","starred_url":"https://api.github.com/users/piwik/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/piwik/subscriptions","organizations_url":"https://api.github.com/users/piwik/orgs","repos_url":"https://api.github.com/users/piwik/repos","events_url":"https://api.github.com/users/piwik/events{/privacy}","received_events_url":"https://api.github.com/users/piwik/received_events","type":"Organization","site_admin":false},"repo":{"id":1548202,"name":"piwik","full_name":"piwik/piwik","owner":{"login":"piwik","id":698038,"avatar_url":"https://avatars.githubusercontent.com/u/698038?v=3","gravatar_id":"","url":"https://api.github.com/users/piwik","html_url":"https://github.com/piwik","followers_url":"https://api.github.com/users/piwik/followers","following_url":"https://api.github.com/users/piwik/following{/other_user}","gists_url":"https://api.github.com/users/piwik/gists{/gist_id}","starred_url":"https://api.github.com/users/piwik/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/piwik/subscriptions","organizations_url":"https://api.github.com/users/piwik/orgs","repos_url":"https://api.github.com/users/piwik/repos","events_url":"https://api.github.com/users/piwik/events{/privacy}","received_events_url":"https://api.github.com/users/piwik/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/piwik/piwik","description":"Liberating Web Analytics. Star us on Github? +1. Piwik is the leading open alternative to Google Analytics that gives you full control over your data. Piwik lets you easily collect data from websites, apps & IoT and visualise this data and extract insights. Privacy is built-in. We love Pull Requests! ","fork":false,"url":"https://api.github.com/repos/piwik/piwik","forks_url":"https://api.github.com/repos/piwik/piwik/forks","keys_url":"https://api.github.com/repos/piwik/piwik/keys{/key_id}","collaborators_url":"https://api.github.com/repos/piwik/piwik/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/piwik/piwik/teams","hooks_url":"https://api.github.com/repos/piwik/piwik/hooks","issue_events_url":"https://api.github.com/repos/piwik/piwik/issues/events{/number}","events_url":"https://api.github.com/repos/piwik/piwik/events","assignees_url":"https://api.github.com/repos/piwik/piwik/assignees{/user}","branches_url":"https://api.github.com/repos/piwik/piwik/branches{/branch}","tags_url":"https://api.github.com/repos/piwik/piwik/tags","blobs_url":"https://api.github.com/repos/piwik/piwik/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/piwik/piwik/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/piwik/piwik/git/refs{/sha}","trees_url":"https://api.github.com/repos/piwik/piwik/git/trees{/sha}","statuses_url":"https://api.github.com/repos/piwik/piwik/statuses/{sha}","languages_url":"https://api.github.com/repos/piwik/piwik/languages","stargazers_url":"https://api.github.com/repos/piwik/piwik/stargazers","contributors_url":"https://api.github.com/repos/piwik/piwik/contributors","subscribers_url":"https://api.github.com/repos/piwik/piwik/subscribers","subscription_url":"https://api.github.com/repos/piwik/piwik/subscription","commits_url":"https://api.github.com/repos/piwik/piwik/commits{/sha}","git_commits_url":"https://api.github.com/repos/piwik/piwik/git/commits{/sha}","comments_url":"https://api.github.com/repos/piwik/piwik/comments{/number}","issue_comment_url":"https://api.github.com/repos/piwik/piwik/issues/comments{/number}","contents_url":"https://api.github.com/repos/piwik/piwik/contents/{+path}","compare_url":"https://api.github.com/repos/piwik/piwik/compare/{base}...{head}","merges_url":"https://api.github.com/repos/piwik/piwik/merges","archive_url":"https://api.github.com/repos/piwik/piwik/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/piwik/piwik/downloads","issues_url":"https://api.github.com/repos/piwik/piwik/issues{/number}","pulls_url":"https://api.github.com/repos/piwik/piwik/pulls{/number}","milestones_url":"https://api.github.com/repos/piwik/piwik/milestones{/number}","notifications_url":"https://api.github.com/repos/piwik/piwik/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/piwik/piwik/labels{/name}","releases_url":"https://api.github.com/repos/piwik/piwik/releases{/id}","created_at":"2011-03-30T21:18:17Z","updated_at":"2015-07-28T12:38:38Z","pushed_at":"2015-07-28T12:27:21Z","git_url":"git://github.com/piwik/piwik.git","ssh_url":"git@github.com:piwik/piwik.git","clone_url":"https://github.com/piwik/piwik.git","svn_url":"https://github.com/piwik/piwik","homepage":"http://piwik.org/","size":340416,"stargazers_count":4178,"watchers_count":4178,"language":"PHP","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":782,"mirror_url":null,"open_issues_count":1073,"forks":782,"open_issues":1073,"watchers":4178,"default_branch":"master"}},"base":{"label":"piwik:3.0","ref":"3.0","sha":"187133c16aa1c8e2b58a2db2f195f77760e87d1e","user":{"login":"piwik","id":698038,"avatar_url":"https://avatars.githubusercontent.com/u/698038?v=3","gravatar_id":"","url":"https://api.github.com/users/piwik","html_url":"https://github.com/piwik","followers_url":"https://api.github.com/users/piwik/followers","following_url":"https://api.github.com/users/piwik/following{/other_user}","gists_url":"https://api.github.com/users/piwik/gists{/gist_id}","starred_url":"https://api.github.com/users/piwik/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/piwik/subscriptions","organizations_url":"https://api.github.com/users/piwik/orgs","repos_url":"https://api.github.com/users/piwik/repos","events_url":"https://api.github.com/users/piwik/events{/privacy}","received_events_url":"https://api.github.com/users/piwik/received_events","type":"Organization","site_admin":false},"repo":{"id":1548202,"name":"piwik","full_name":"piwik/piwik","owner":{"login":"piwik","id":698038,"avatar_url":"https://avatars.githubusercontent.com/u/698038?v=3","gravatar_id":"","url":"https://api.github.com/users/piwik","html_url":"https://github.com/piwik","followers_url":"https://api.github.com/users/piwik/followers","following_url":"https://api.github.com/users/piwik/following{/other_user}","gists_url":"https://api.github.com/users/piwik/gists{/gist_id}","starred_url":"https://api.github.com/users/piwik/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/piwik/subscriptions","organizations_url":"https://api.github.com/users/piwik/orgs","repos_url":"https://api.github.com/users/piwik/repos","events_url":"https://api.github.com/users/piwik/events{/privacy}","received_events_url":"https://api.github.com/users/piwik/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/piwik/piwik","description":"Liberating Web Analytics. Star us on Github? +1. Piwik is the leading open alternative to Google Analytics that gives you full control over your data. Piwik lets you easily collect data from websites, apps & IoT and visualise this data and extract insights. Privacy is built-in. We love Pull Requests! ","fork":false,"url":"https://api.github.com/repos/piwik/piwik","forks_url":"https://api.github.com/repos/piwik/piwik/forks","keys_url":"https://api.github.com/repos/piwik/piwik/keys{/key_id}","collaborators_url":"https://api.github.com/repos/piwik/piwik/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/piwik/piwik/teams","hooks_url":"https://api.github.com/repos/piwik/piwik/hooks","issue_events_url":"https://api.github.com/repos/piwik/piwik/issues/events{/number}","events_url":"https://api.github.com/repos/piwik/piwik/events","assignees_url":"https://api.github.com/repos/piwik/piwik/assignees{/user}","branches_url":"https://api.github.com/repos/piwik/piwik/branches{/branch}","tags_url":"https://api.github.com/repos/piwik/piwik/tags","blobs_url":"https://api.github.com/repos/piwik/piwik/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/piwik/piwik/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/piwik/piwik/git/refs{/sha}","trees_url":"https://api.github.com/repos/piwik/piwik/git/trees{/sha}","statuses_url":"https://api.github.com/repos/piwik/piwik/statuses/{sha}","languages_url":"https://api.github.com/repos/piwik/piwik/languages","stargazers_url":"https://api.github.com/repos/piwik/piwik/stargazers","contributors_url":"https://api.github.com/repos/piwik/piwik/contributors","subscribers_url":"https://api.github.com/repos/piwik/piwik/subscribers","subscription_url":"https://api.github.com/repos/piwik/piwik/subscription","commits_url":"https://api.github.com/repos/piwik/piwik/commits{/sha}","git_commits_url":"https://api.github.com/repos/piwik/piwik/git/commits{/sha}","comments_url":"https://api.github.com/repos/piwik/piwik/comments{/number}","issue_comment_url":"https://api.github.com/repos/piwik/piwik/issues/comments{/number}","contents_url":"https://api.github.com/repos/piwik/piwik/contents/{+path}","compare_url":"https://api.github.com/repos/piwik/piwik/compare/{base}...{head}","merges_url":"https://api.github.com/repos/piwik/piwik/merges","archive_url":"https://api.github.com/repos/piwik/piwik/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/piwik/piwik/downloads","issues_url":"https://api.github.com/repos/piwik/piwik/issues{/number}","pulls_url":"https://api.github.com/repos/piwik/piwik/pulls{/number}","milestones_url":"https://api.github.com/repos/piwik/piwik/milestones{/number}","notifications_url":"https://api.github.com/repos/piwik/piwik/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/piwik/piwik/labels{/name}","releases_url":"https://api.github.com/repos/piwik/piwik/releases{/id}","created_at":"2011-03-30T21:18:17Z","updated_at":"2015-07-28T12:38:38Z","pushed_at":"2015-07-28T12:27:21Z","git_url":"git://github.com/piwik/piwik.git","ssh_url":"git@github.com:piwik/piwik.git","clone_url":"https://github.com/piwik/piwik.git","svn_url":"https://github.com/piwik/piwik","homepage":"http://piwik.org/","size":340416,"stargazers_count":4178,"watchers_count":4178,"language":"PHP","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":782,"mirror_url":null,"open_issues_count":1073,"forks":782,"open_issues":1073,"watchers":4178,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/piwik/piwik/pulls/8442"},"html":{"href":"https://github.com/piwik/piwik/pull/8442"},"issue":{"href":"https://api.github.com/repos/piwik/piwik/issues/8442"},"comments":{"href":"https://api.github.com/repos/piwik/piwik/issues/8442/comments"},"review_comments":{"href":"https://api.github.com/repos/piwik/piwik/pulls/8442/comments"},"review_comment":{"href":"https://api.github.com/repos/piwik/piwik/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/piwik/piwik/pulls/8442/commits"},"statuses":{"href":"https://api.github.com/repos/piwik/piwik/statuses/bab2c1a31d13632979cf2c0fddfdc1b765b80451"}}}}