New stats pages!
- supports grouping by day, week, and month - new event-driven UI easier to maintain - uses impala styles for extra pretty! TODO - rehabilitate csv table - polish style for side notes - bring back field selection menu - bring back overview page
This commit is contained in:
Родитель
6f06067d21
Коммит
f717424d79
|
@ -1,6 +1,5 @@
|
|||
{% set show_contributions = false %}
|
||||
<div class="secondary-item-list report-menu">
|
||||
<h3>{{ _('Add-on Statistics') }}</h3>
|
||||
<nav id='side-nav'>
|
||||
<ul>
|
||||
{% for item in report_tree %}
|
||||
{% if item.name != 'contributions' or show_contributions %}
|
||||
|
@ -10,21 +9,21 @@
|
|||
<li>
|
||||
{% endif %}
|
||||
<a href="{{ base_url + item.url }}">{{ item.title }}</a>
|
||||
{% if item.children %}
|
||||
<ul>
|
||||
{% for child in item.children %}
|
||||
{% if child.name == report %}
|
||||
<li class="selected">
|
||||
{% else %}
|
||||
<li>
|
||||
{% endif %}
|
||||
<a href="{{ base_url + child.url }}">{{ child.title }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if item.children %}
|
||||
<ul>
|
||||
{% for child in item.children %}
|
||||
{% if child.name == report %}
|
||||
<li class="selected">
|
||||
{% else %}
|
||||
<li>
|
||||
{% endif %}
|
||||
<a href="{{ base_url + child.url }}">{{ child.title }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
|
|
|
@ -37,7 +37,7 @@
|
|||
{% block stats_note %}
|
||||
<aside class="highlight">
|
||||
{% trans slug=addon.slug, id=addon.id %}
|
||||
<h3>Tracking external sources</h3>
|
||||
<h2>Tracking external sources</h2>
|
||||
<p>
|
||||
If you link to your add-on's details page or directly to its file from an
|
||||
external site, such as your blog or website, you can append a parameter to be
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
{% extends "base.html" %}
|
||||
{% extends "impala/base.html" %}
|
||||
|
||||
{% set range = view.range %}
|
||||
|
||||
{% block bodyclass %}statistics{% endblock %}
|
||||
|
||||
{% block extrahead %}
|
||||
<link rel="stylesheet" href="{{ media('css/legacy/stats.css') }}"/>
|
||||
{{ css('zamboni/stats') }}
|
||||
<link rel="stylesheet"
|
||||
href="{{ media('css/zamboni/jquery-ui/custom-1.7.2.css') }}">
|
||||
{% endblock %}
|
||||
|
@ -14,9 +16,9 @@
|
|||
{% endblock %}
|
||||
|
||||
{% block navbar %}
|
||||
{{ breadcrumbs([(addon.type_url(), amo.ADDON_TYPES[addon.type]),
|
||||
(addon.get_url_path(), addon.name),
|
||||
(link, _('Statistics'))]) }}
|
||||
{{ impala_breadcrumbs([(addon.type_url(), amo.ADDON_TYPES[addon.type]),
|
||||
(addon.get_url_path(), addon.name),
|
||||
(link, _('Statistics'))]) }}
|
||||
{# TODO: Replace above line with this --> once we serve the extension home. { breadcrumbs([(addon.type.get_url_path(), amo.ADDON_TYPES[addon.type_id])]) } #}
|
||||
|
||||
|
||||
|
@ -41,18 +43,14 @@
|
|||
<a href="#">{{ _('Custom') }}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<hgroup>
|
||||
<h2 class="addon"{{ addon.name|locale_html }}>
|
||||
<img src="{{ addon.icon_url }}" class="icon"/>
|
||||
<span>
|
||||
{# L10n: {0} is an add-on name #}
|
||||
{{ _('Statistics for {0}')|f(addon.name) }}
|
||||
</span>
|
||||
</h2>
|
||||
{# L10n: {0} is an add-on author #}
|
||||
<h4 class="author">{{ _('by {0}')|f(users_list(addon.listed_authors))|xssafe }}</h4>
|
||||
</hgroup>
|
||||
<header>
|
||||
<hgroup>
|
||||
<h1 class="addon"{{ addon.name|locale_html }}>
|
||||
{# L10n: {0} is an add-on name #}
|
||||
{{ _('Statistics for {0}')|f(addon.name) }}
|
||||
</h1>
|
||||
</hgroup>
|
||||
</header>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
@ -92,19 +90,8 @@
|
|||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="featured">
|
||||
<div class="featured-inner chart">
|
||||
<div class="listing-header">
|
||||
{% block chart_menu %}
|
||||
<ul><li class="selected"> </li></ul>
|
||||
{% endblock %}
|
||||
</div>
|
||||
<div class="featured-body" id="head-chart" style="background:#fff;height:256px"
|
||||
{% block chart_config %}
|
||||
data-series="{{ series_fields }}"
|
||||
{% endblock %}
|
||||
>
|
||||
</div>
|
||||
<div class="island chart c">
|
||||
<div id="head-chart" style="background:#fff;height:384px">
|
||||
</div>
|
||||
</div>
|
||||
{% block stats %}
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
.amo-header {
|
||||
font-family: @head-sans;
|
||||
margin-bottom: 35px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#masthead {
|
||||
|
|
|
@ -143,6 +143,15 @@ header + .island {
|
|||
line-height: 16px;
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
ul ul {
|
||||
margin-bottom: 0;
|
||||
li {
|
||||
border-top: 0;
|
||||
a {
|
||||
padding-left: 1em;
|
||||
}
|
||||
}
|
||||
}
|
||||
li {
|
||||
border: 1px solid @border-black;
|
||||
border-width: 0 0 1px 0;
|
||||
|
@ -240,7 +249,8 @@ header + .island {
|
|||
.s-users #side-nav .s-users a,
|
||||
.s-downloads #side-nav .s-downloads a,
|
||||
.s-rating #side-nav .s-rating a,
|
||||
.s-created #side-nav .s-created a {
|
||||
.s-created #side-nav .s-created a,
|
||||
#side-nav .selected {
|
||||
background: #ecf5fe;
|
||||
color: #333;
|
||||
font-weight: bold;
|
||||
|
@ -249,7 +259,8 @@ header + .island {
|
|||
.s-users #side-nav .s-users a:after,
|
||||
.s-downloads #side-nav .s-downloads a:after,
|
||||
.s-rating #side-nav .s-rating a:after,
|
||||
.s-created #side-nav .s-created a:after {
|
||||
.s-created #side-nav .s-created a:after,
|
||||
#side-nav .selected a:after {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
|
|
|
@ -126,6 +126,15 @@ div.statbox {
|
|||
}
|
||||
}
|
||||
|
||||
.statistics #page {
|
||||
max-width: 1280px;
|
||||
width: auto;
|
||||
min-width: 960px;
|
||||
padding-bottom: 500px;
|
||||
padding-left: 20px;
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
||||
.criteria li.divider:before {
|
||||
font-weight: normal;
|
||||
font-size: 160%;
|
||||
|
@ -469,6 +478,7 @@ table.stats-aggregate tbody span.change.minus {
|
|||
}
|
||||
#head-chart {
|
||||
border-radius: 1em;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.loadmessage {
|
||||
|
|
|
@ -1,515 +0,0 @@
|
|||
/* Clearfix! */
|
||||
.statbox:after {
|
||||
content: ".";
|
||||
display: block;
|
||||
clear: both;
|
||||
height: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.statbox .pagination {
|
||||
display: block;
|
||||
width: 98%;
|
||||
margin: 0;
|
||||
padding: .5em 1%;
|
||||
border-top: 1px solid #A5BFCE;
|
||||
-moz-border-radius: 0 0 3px 3px;
|
||||
background: -moz-linear-gradient(#DAF0F6, #FDFEFE) repeat scroll 0 0 transparent
|
||||
}
|
||||
|
||||
/**
|
||||
* Undo default table style
|
||||
**/
|
||||
|
||||
table, tbody, thead, th, tr, td,
|
||||
thead tr th, tbody tr td,
|
||||
thead th, tbody tr {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
thead tr th, tbody tr td {
|
||||
border-top: 0;
|
||||
}
|
||||
|
||||
|
||||
table {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.tabular {
|
||||
margin: 1em;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.csv-table td {
|
||||
min-width: 100px;
|
||||
white-space:nowrap;
|
||||
}
|
||||
|
||||
/**
|
||||
* common styles
|
||||
**/
|
||||
|
||||
.statbox .listing-header {
|
||||
-moz-border-radius:0 0 4px 4px;
|
||||
background-color:#F0F8FC;
|
||||
background-image: -moz-linear-gradient(#daf0f6,#fdfefe);
|
||||
border:0;
|
||||
border-top:1px solid #A5BFCE;
|
||||
line-height:2.5em;
|
||||
overflow:hidden;
|
||||
padding:0.1em 0 0.1em 0.25em;
|
||||
}
|
||||
|
||||
table tbody tr {
|
||||
border-top: 1px dotted #B5D9E5;
|
||||
}
|
||||
|
||||
.featured-inner {
|
||||
background: -moz-linear-gradient(#ffffff, #eff8fb);
|
||||
}
|
||||
|
||||
div.statbox {
|
||||
border: 1px solid #C9E8F3;
|
||||
-moz-border-radius: 5px;
|
||||
-webkit-border-radius: 5px;
|
||||
border-radius: 5px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.secondary > aside,
|
||||
.secondary > div {
|
||||
margin-bottom: 2em;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rules for date criteria selection
|
||||
**/
|
||||
|
||||
.criteria {
|
||||
background: -moz-linear-gradient(#fdfefe,#daf0f6);
|
||||
border: .1em solid #abc4d2;
|
||||
-moz-border-radius: 4px;
|
||||
}
|
||||
.criteria ul {
|
||||
line-height: 2.5em;
|
||||
}
|
||||
.criteria.custom form {
|
||||
line-height: 2.5em;
|
||||
margin: .3em 1em;
|
||||
}
|
||||
.criteria.custom form input {
|
||||
z-index: 10;
|
||||
position: relative;
|
||||
}
|
||||
.criteria.custom {
|
||||
position: relative;
|
||||
margin-bottom: 1em;
|
||||
display: none;
|
||||
}
|
||||
.criteria.range {
|
||||
float: right;
|
||||
position: relative;
|
||||
top: -2px;
|
||||
z-index: 1000;
|
||||
}
|
||||
.criteria li {
|
||||
float: left;
|
||||
font-weight: bold;
|
||||
}
|
||||
.criteria li:first-child {
|
||||
padding-left: 1.4em;
|
||||
}
|
||||
.criteria li.divider:before {
|
||||
font-weight: normal;
|
||||
font-size: 160%;
|
||||
color: #b8cdd9;
|
||||
content: "|";
|
||||
}
|
||||
.criteria li a {
|
||||
-moz-border-radius:3px 3px 3px 3px;
|
||||
border:1px solid transparent;
|
||||
color:#003595;
|
||||
font-weight:bold;
|
||||
padding:0.3em 0.8em;
|
||||
text-decoration:none;
|
||||
white-space:nowrap;
|
||||
}
|
||||
.criteria li a:active,
|
||||
.criteria li a:active,
|
||||
.criteria li.selected a {
|
||||
-moz-border-radius:3px 3px 3px 3px;
|
||||
color:#fff;
|
||||
background:#003595;
|
||||
}
|
||||
|
||||
/* creates the inner triangle */
|
||||
.criteria.custom:before {
|
||||
font-weight: normal;
|
||||
content: "\00a0";
|
||||
display: block; /* reduce the damage in FF3.0 */
|
||||
position: absolute;
|
||||
width: 0;
|
||||
height: 0;
|
||||
top: -38px; /* value = - border-top-width - border-bottom-width */
|
||||
right: 25px; /* controls horizontal position */
|
||||
border: 20px solid transparent;
|
||||
border-bottom-color: #abc4d2;
|
||||
border-width: 19px 14px; /* vary these values to change the angle of the vertex */
|
||||
border-style: solid;
|
||||
}
|
||||
|
||||
/* creates the outer triangle */
|
||||
.criteria.custom:after {
|
||||
content: "\00a0";
|
||||
display: block; /* reduce the damage in FF3.0 */
|
||||
position: absolute;
|
||||
width: 0;
|
||||
height: 0;
|
||||
top: -37px; /* value = - border-top-width - border-bottom-width */
|
||||
right: 24px; /* value = (:before right) + (:before border-right) - (:after border-right) */
|
||||
border: 13px solid transparent;
|
||||
border-bottom-color: #fdfdfe;
|
||||
border-width: 20px 15px; /* vary these values to change the angle of the vertex */
|
||||
border-style: solid;
|
||||
}
|
||||
|
||||
/* @end */
|
||||
|
||||
/**
|
||||
* Three-up stats
|
||||
**/
|
||||
|
||||
.two-up > div {
|
||||
float: left;
|
||||
width: 50%;
|
||||
margin: 10px 0;
|
||||
text-align:center;
|
||||
}
|
||||
.two-up > div > div {
|
||||
border-left: 1px dotted #aaa;
|
||||
}
|
||||
.two-up div a {
|
||||
font-weight: bold;
|
||||
font-size: 140%;
|
||||
}
|
||||
.two-up div small {
|
||||
font-size: 110%;
|
||||
}
|
||||
|
||||
.listing-header ul.chart_legend {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.listing-header ul.chart_legend li {
|
||||
margin: 0 .5em;
|
||||
line-height: 2.5em;
|
||||
display: block;
|
||||
float: right;
|
||||
}
|
||||
|
||||
/**
|
||||
* Toplists
|
||||
**/
|
||||
|
||||
.seriesdot {
|
||||
display: block;
|
||||
float: left;
|
||||
position: relative;
|
||||
top: .7em;
|
||||
margin-right: .5em;
|
||||
-moz-border-radius: 4px;
|
||||
height: 8px;
|
||||
width: 8px;
|
||||
}
|
||||
.listing-header .seriesdot {
|
||||
border: 2px solid #fff;
|
||||
-moz-border-radius: 6px;
|
||||
margin: 0 .2em 0 .5em;
|
||||
top: .8em;
|
||||
}
|
||||
.toplists {
|
||||
margin-bottom: 2em;
|
||||
overflow: hidden;
|
||||
}
|
||||
.toplist {
|
||||
float: left;
|
||||
width: 31%;
|
||||
padding: 0;
|
||||
margin-left: 3.5%;
|
||||
}
|
||||
.toplist:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
.toplist .statbox {
|
||||
padding: 1em;
|
||||
margin: 0;
|
||||
}
|
||||
.toplist h3 {
|
||||
margin: 0 0 .2em 0;
|
||||
}
|
||||
#toplist1 {
|
||||
width: 210px;
|
||||
margin: .5em auto 0 auto;
|
||||
}
|
||||
.toplist tr:first-child {
|
||||
border-top: 0;
|
||||
}
|
||||
.toplist table {
|
||||
width: 100%;
|
||||
height: 160px;
|
||||
margin: .7em 0;
|
||||
}
|
||||
.toplist td {
|
||||
text-align: right;
|
||||
padding: 0;
|
||||
line-height: 2em;
|
||||
}
|
||||
.toplist td:last-child {
|
||||
text-align: left;
|
||||
font-size: 90%;
|
||||
width:40px;
|
||||
padding-left: .3em;
|
||||
}
|
||||
.toplist td:first-child {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rules for Report Menu
|
||||
**/
|
||||
|
||||
.report-menu nav {
|
||||
display: block;
|
||||
}
|
||||
.report-menu h3 {
|
||||
margin-top: 0;
|
||||
}
|
||||
.report-menu ul li {
|
||||
font-size: 110%;
|
||||
}
|
||||
.report-menu ul li ul li {
|
||||
font-size: 90%;
|
||||
}
|
||||
.report-menu ul li ul li a {
|
||||
margin: .4em 0;
|
||||
}
|
||||
.report-menu ul li a {
|
||||
margin: .4em 0;
|
||||
}
|
||||
.report-menu ul ul {
|
||||
margin-left: 25px;
|
||||
}
|
||||
.report-menu ul li.selected > a {
|
||||
position: relative;
|
||||
color: #333;
|
||||
}
|
||||
.report-menu ul li.selected > a:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* Makes the triangles for selected reports */
|
||||
.report-menu ul li.selected > a:before {
|
||||
content: "\00a0";
|
||||
display: block; /* reduce the damage in FF3.0 */
|
||||
position: absolute;
|
||||
width: 0;
|
||||
height: 0;
|
||||
top: 20%; /* value = - border-top-width - border-bottom-width */
|
||||
left: 10px; /* value = (:before right) + (:before border-right) - (:after border-right) */
|
||||
border: 5px solid transparent;
|
||||
border-left-color: #333;
|
||||
border-style: solid;
|
||||
}
|
||||
.report-menu ul li ul li.selected > a:before {
|
||||
border-width: 4px;
|
||||
}
|
||||
|
||||
/* @end */
|
||||
|
||||
/**
|
||||
* bar-chart tables
|
||||
**/
|
||||
|
||||
.csv-table tbody {
|
||||
display: none;
|
||||
}
|
||||
.csv-table tbody.selected {
|
||||
display: table-row-group;
|
||||
}
|
||||
|
||||
table.csv-table {
|
||||
width: 100%;
|
||||
}
|
||||
table.csv-table td,
|
||||
table.csv-table th {
|
||||
text-align: right;
|
||||
}
|
||||
table.csv-table td.bar div {
|
||||
text-align: right;
|
||||
margin: 8px 0;
|
||||
line-height: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
table.csv-table td.bar,
|
||||
table.csv-table th.bar {
|
||||
border-left: 1px solid #8db9c7;
|
||||
padding: 0;
|
||||
}
|
||||
table.csv-table td:first-child,
|
||||
table.csv-table th:first-child {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
div.piechart {
|
||||
height: 200px;
|
||||
}
|
||||
div.piechart .highcharts-container {
|
||||
margin: 8px auto 0 auto;
|
||||
}
|
||||
|
||||
/**
|
||||
* sidestats
|
||||
**/
|
||||
|
||||
aside.highlight {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.highlight dd {
|
||||
font-size: .8em;
|
||||
}
|
||||
|
||||
#export_data {
|
||||
font: 13px "helvetica neue",arial,helvetica,sans-serif;
|
||||
margin-left:1em;
|
||||
}
|
||||
/**
|
||||
* Big table
|
||||
**/
|
||||
|
||||
table.stats-aggregate {
|
||||
width: 100%;
|
||||
margin-top: .5em;
|
||||
}
|
||||
|
||||
table.stats-aggregate thead th {
|
||||
text-align: right;
|
||||
padding-right: 72px;
|
||||
line-height: 90%;
|
||||
}
|
||||
|
||||
table.stats-aggregate thead th:first-child {
|
||||
text-align: left;
|
||||
padding-right: inherit;
|
||||
}
|
||||
|
||||
table.stats-aggregate tbody td {
|
||||
line-height: 120%;
|
||||
font-size: 140%;
|
||||
}
|
||||
|
||||
table.stats-aggregate tbody td.value {
|
||||
padding: 5px 0 3 0px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
table.stats-aggregate tbody td.label {
|
||||
padding-left: 2px;
|
||||
}
|
||||
|
||||
table.stats-aggregate tbody span.change {
|
||||
display: block;
|
||||
float: right;
|
||||
width: 54px;
|
||||
padding-left: .5em;
|
||||
text-align: left;
|
||||
font-size: 80%;
|
||||
padding-top: 2px;
|
||||
}
|
||||
|
||||
table.stats-aggregate tbody span.change.plus {
|
||||
color: #00774d;
|
||||
}
|
||||
table.stats-aggregate tbody span.change.minus {
|
||||
color: #850000;
|
||||
}
|
||||
|
||||
.loading, .loaded {
|
||||
position:relative;
|
||||
}
|
||||
.loading:after {
|
||||
-moz-transition: opacity .5s;
|
||||
content: "\00a0";
|
||||
display: block;
|
||||
position: absolute;
|
||||
background: #040204 url("../../img/zamboni/loading.gif") no-repeat center center;;
|
||||
opacity: .4;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 1000;
|
||||
top: 0;
|
||||
}
|
||||
.loaded:after {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.loadmessage {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: -2.5em;
|
||||
height: 2em;
|
||||
margin: 0 auto;
|
||||
text-align: center;
|
||||
pointer-events: none;
|
||||
-moz-transition: top .5s;
|
||||
-webkit-transition: top .5s;
|
||||
transition: top .5s;
|
||||
z-index:9000;
|
||||
}
|
||||
|
||||
.loadmessage span {
|
||||
background: url("../../img/zamboni/loading-small.gif") no-repeat 1em center;
|
||||
background-color: #000;
|
||||
padding: .5em 1em;
|
||||
padding-left: 36px;
|
||||
line-height: 2em;
|
||||
color: #fff;
|
||||
opacity: .75;
|
||||
-moz-border-radius: 0 0 .75em .75em;
|
||||
-webkit-border-radius: 0 0 .75em .75em;
|
||||
border-radius: 0 0 .75em .75em;
|
||||
}
|
||||
|
||||
.loadmessage.on {
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.loadmessage.off {
|
||||
top: -2.5em;
|
||||
}
|
||||
|
||||
/* Field Menu */
|
||||
#fieldMenu, #fieldList {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
#fieldList label {
|
||||
display: block;
|
||||
white-space: nowrap;
|
||||
padding-right: 2em;
|
||||
}
|
||||
#fieldList label:hover {
|
||||
background: #ccf;
|
||||
}
|
||||
#fieldMenu button {
|
||||
width: 100%;
|
||||
}
|
||||
#fieldList {
|
||||
max-height: 300px;
|
||||
min-width: 160px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
|
@ -0,0 +1,176 @@
|
|||
(function () {
|
||||
var $win = $(window),
|
||||
baseConfig = {
|
||||
chart: {
|
||||
renderTo: 'head-chart',
|
||||
zoomType: 'x',
|
||||
},
|
||||
credits: { enabled: false },
|
||||
title: {
|
||||
text: null
|
||||
},
|
||||
xAxis: {
|
||||
type: 'datetime',
|
||||
maxZoom: 7 * 24 * 3600000, // seven days
|
||||
title: {
|
||||
text: null
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
title: {
|
||||
text: null
|
||||
},
|
||||
labels: {
|
||||
formatter: function() {
|
||||
return Highcharts.numberFormat(this.value, 0);
|
||||
}
|
||||
},
|
||||
min: 0,
|
||||
startOnTick: false,
|
||||
showFirstLabel: false
|
||||
},
|
||||
legend: {
|
||||
enabled: true
|
||||
},
|
||||
tooltip: { },
|
||||
plotOptions: {
|
||||
line: {
|
||||
lineWidth: 1,
|
||||
animation: false,
|
||||
shadow: false,
|
||||
marker: {
|
||||
enabled: false,
|
||||
states: {
|
||||
hover: {
|
||||
enabled: true,
|
||||
radius: 5
|
||||
}
|
||||
}
|
||||
},
|
||||
states: {
|
||||
hover: {
|
||||
lineWidth: 2
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var chart;
|
||||
// which unit do we use for a given metric?
|
||||
var metricTypes = {
|
||||
"usage" : "users",
|
||||
"apps" : "users",
|
||||
"locales" : "users",
|
||||
"os" : "users",
|
||||
"versions" : "users",
|
||||
"statuses" : "users",
|
||||
"downloads" : "downloads",
|
||||
"sources" : "downloads"
|
||||
};
|
||||
|
||||
$win.bind("dataready", function(e, obj) {
|
||||
var view = obj.view,
|
||||
metric = view.metric,
|
||||
group = view.group,
|
||||
range = z.date.normalizeRange(view.range),
|
||||
start = range.start,
|
||||
end = range.end,
|
||||
fields = obj.fields ? obj.fields.slice(0,5) : ['count'],
|
||||
data = obj.data,
|
||||
series = {},
|
||||
t, row, i, field, val;
|
||||
|
||||
// Initialize the empty series object.
|
||||
_.each(fields, function(f) { series[f] = []; });
|
||||
|
||||
// Transmute the data into something Highcharts understands.
|
||||
if (group == 'month') {
|
||||
_.each(data, function(row, t) {
|
||||
for (i = 0; i < fields.length; i++) {
|
||||
field = fields[i];
|
||||
val = parseFloat(z.StatsManager.getField(row, field));
|
||||
if (val != val) val = null;
|
||||
series[field].push({
|
||||
'x' : parseInt(t, 10),
|
||||
'y' : val
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
var step = z.date.millis('1 day');
|
||||
if (group == 'week') {
|
||||
step = z.date.millis('7 days');
|
||||
while((new Date(start)).getDay() > 0) {
|
||||
start += z.date.millis('1 day');
|
||||
}
|
||||
}
|
||||
for (t = start; t < end; t += step) {
|
||||
row = data[t];
|
||||
for (i = 0; i < fields.length; i++) {
|
||||
field = fields[i];
|
||||
val = parseFloat(z.StatsManager.getField(row, field));
|
||||
if (val != val) val = null;
|
||||
series[field].push({
|
||||
'x' : t,
|
||||
'y' : val
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Populate the chart config object.
|
||||
var chartData = [], id
|
||||
for (i = 0; i < fields.length; i++) {
|
||||
field = fields[i];
|
||||
id = field.split("|").slice(-1)[0];
|
||||
chartData.push({
|
||||
'type' : 'line',
|
||||
'name' : z.StatsManager.getPrettyName(view.metric, id),
|
||||
'id' : id,
|
||||
'data' : series[field]
|
||||
});
|
||||
}
|
||||
|
||||
// Generate the tooltip function for this chart.
|
||||
// both x and y axis can be displayed differently.
|
||||
var tooltipFormatter = (function(){
|
||||
var xFormatter,
|
||||
yFormatter;
|
||||
function dayFormatter(d) { return Highcharts.dateFormat('%a, %b %e, %Y', new Date(d)); }
|
||||
function weekFormatter(d) { return "Week of " + Highcharts.dateFormat('%b %e, %Y', new Date(d)); }
|
||||
function monthFormatter(d) { return Highcharts.dateFormat('%B %Y', new Date(d)); }
|
||||
function downloadFormatter(n) { return Highcharts.numberFormat(n, 0) + ' downloads'; }
|
||||
function userFormatter(n) { return Highcharts.numberFormat(n, 0) + ' users'; }
|
||||
if (group == "week") {
|
||||
xFormatter = weekFormatter;
|
||||
} else if (group == "month") {
|
||||
xFormatter = monthFormatter;
|
||||
} else {
|
||||
xFormatter = dayFormatter;
|
||||
}
|
||||
if (metricTypes[metric] == "users") {
|
||||
yFormatter = userFormatter;
|
||||
} else {
|
||||
yFormatter = downloadFormatter;
|
||||
}
|
||||
return function() {
|
||||
return "<b>" + this.series.name + "</b><br>" +
|
||||
xFormatter(this.x) + "<br>" +
|
||||
yFormatter(this.y);
|
||||
}
|
||||
})();
|
||||
|
||||
// Set up the new chart's configuration.
|
||||
var newConfig = $.extend(baseConfig, { series: chartData });
|
||||
if (fields.length == 1) {
|
||||
newConfig.legend.enabled = false;
|
||||
newConfig.chart.margin = [50, 50, 50, 80]
|
||||
}
|
||||
newConfig.tooltip.formatter = tooltipFormatter;
|
||||
|
||||
|
||||
if (chart) chart.destroy();
|
||||
chart = new Highcharts.Chart(newConfig);
|
||||
});
|
||||
})();
|
|
@ -0,0 +1,75 @@
|
|||
(function (){
|
||||
"use strict";
|
||||
|
||||
var $rangeSelector = $(".criteria.range ul"),
|
||||
$customRangeForm = $("div.custom.criteria");
|
||||
|
||||
$.datepicker.setDefaults({showAnim: ''});
|
||||
$("#date-range-start").datepicker();
|
||||
$("#date-range-end").datepicker();
|
||||
|
||||
$rangeSelector.click(function(e) {
|
||||
var $target = $(e.target).parent();
|
||||
var newRange = $target.attr("data-range");
|
||||
|
||||
if (newRange) {
|
||||
$rangeSelector.children("li.selected").removeClass("selected");
|
||||
$target.addClass("selected");
|
||||
|
||||
if (newRange == "custom") {
|
||||
$customRangeForm.removeClass("hidden").slideDown('fast');
|
||||
} else {
|
||||
$target.trigger('changeview', {range: newRange});
|
||||
$customRangeForm.slideUp('fast');
|
||||
}
|
||||
}
|
||||
e.preventDefault();
|
||||
});
|
||||
$(window).bind('changeview', function(e, newState) {
|
||||
function populateCustomRange() {
|
||||
var nRange = z.date.normalizeRange(newState.range);
|
||||
$("#date-range-start").val(
|
||||
z.date.datepicker_format(
|
||||
new Date(nRange.start)
|
||||
)
|
||||
);
|
||||
$("#date-range-end").val(
|
||||
z.date.datepicker_format(
|
||||
new Date(nRange.end)
|
||||
)
|
||||
);
|
||||
$rangeSelector.children("li.selected").removeClass("selected");
|
||||
$('[data-range="custom"]').addClass("selected");
|
||||
$customRangeForm.removeClass("hidden").slideDown('fast');
|
||||
}
|
||||
|
||||
if (newState && newState.range) {
|
||||
if (!newState.range.custom) {
|
||||
var newRange = newState.range,
|
||||
$rangeEl = $('[data-range="' + newRange + '"]');
|
||||
if ($rangeEl.length) {
|
||||
$rangeSelector.children("li.selected").removeClass("selected");
|
||||
$rangeEl.addClass("selected");
|
||||
return;
|
||||
} else {
|
||||
populateCustomRange();
|
||||
}
|
||||
} else {
|
||||
populateCustomRange();
|
||||
}
|
||||
}
|
||||
});
|
||||
$("#date-range-form").submit(function(e) {
|
||||
e.preventDefault();
|
||||
var start = new Date($("#date-range-start").val()),
|
||||
end = new Date($("#date-range-end").val()),
|
||||
newRange = {
|
||||
custom: true,
|
||||
start: z.date.date(start),
|
||||
end: z.date.date(end)
|
||||
};
|
||||
|
||||
$rangeSelector.trigger('changeview', {range: newRange});
|
||||
return false;
|
||||
});
|
||||
})();
|
|
@ -0,0 +1,78 @@
|
|||
// date management helpers
|
||||
|
||||
z.date = (function() {
|
||||
var _millis = {
|
||||
"day" : 1000 * 60 * 60 * 24,
|
||||
"week" : 1000 * 60 * 60 * 24 * 7
|
||||
};
|
||||
|
||||
// Returns the number of milliseconds for a given duration.
|
||||
// millis("1 day")
|
||||
// > 86400000
|
||||
// millis("2 days")
|
||||
// > 172800000
|
||||
function millis(str) {
|
||||
var tokens = str.split(/\s+/);
|
||||
n = parseInt(tokens[0]);
|
||||
if (!tokens[1]) throw "Invalid duration string";
|
||||
unit = tokens[1].replace(/s$/,'').toLowerCase();
|
||||
if (!_millis[ unit ]) throw "Invalid time unit";
|
||||
return n * _millis[ unit ];
|
||||
};
|
||||
|
||||
// pads a number with a preceding zero.
|
||||
// pad2(2)
|
||||
// > "02"
|
||||
// pad2(20)
|
||||
// > "20"
|
||||
function pad2(n) {
|
||||
var str = n.toString();
|
||||
return ('0' + str).substr(-2);
|
||||
};
|
||||
|
||||
// Takes a date object and converts it to a time-less
|
||||
// representation of today's date.
|
||||
function date(d) {
|
||||
return Date.parse(date_string(d, '-'));
|
||||
};
|
||||
function date_string(d, del) {
|
||||
del = del || '-';
|
||||
return [d.getFullYear(), pad2(d.getMonth()+1), pad2(d.getDate())].join(del);
|
||||
};
|
||||
function datepicker_format(d) {
|
||||
return [pad2(d.getMonth()+1), pad2(d.getDate()), d.getFullYear()].join('/');
|
||||
};
|
||||
|
||||
// Truncates the current time off today's date.
|
||||
function today() {
|
||||
var d = new Date();
|
||||
return date(d);
|
||||
};
|
||||
|
||||
// returns a millisecond timestamp for a specified duration in the past.
|
||||
function ago(str, times) {
|
||||
times = (times !== undefined) ? times : 1;
|
||||
return today() - millis(str) * times;
|
||||
};
|
||||
|
||||
// takes a range object and normalizes it to have a `start` and `end` property.
|
||||
function normalizeRange(range) {
|
||||
var ret = {};
|
||||
if (typeof range == "string") {
|
||||
ret.start = ago(range);
|
||||
ret.end = today();
|
||||
} else if (typeof range == "object") {
|
||||
ret.start = range.start;
|
||||
ret.end = range.end;
|
||||
} else {
|
||||
throw "Invalid range values found."
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
return {
|
||||
'ago': ago, 'date': date, 'date_string': date_string,
|
||||
'datepicker_format': datepicker_format, 'millis': millis, 'pad2': pad2,
|
||||
'today': today, 'normalizeRange': normalizeRange
|
||||
};
|
||||
})();
|
|
@ -0,0 +1,399 @@
|
|||
function dbg() {
|
||||
if(window.console && (typeof window.console.log == 'function')) {
|
||||
window.console.log(Array.prototype.slice.apply(arguments));
|
||||
}
|
||||
}
|
||||
|
||||
z.hasPushState = (typeof history.replaceState === "function");
|
||||
|
||||
z.StatsManager = (function() {
|
||||
"use strict";
|
||||
|
||||
// The version of the stats localStorage we are using.
|
||||
// If you increment this number, you cache-bust everyone!
|
||||
var STATS_VERSION = 21;
|
||||
|
||||
var storage = z.Storage("stats"),
|
||||
storageCache = z.Storage("statscache"),
|
||||
dataStore = {},
|
||||
currentView = {},
|
||||
addonId = parseInt($(".primary").attr("data-addon_id"), 10),
|
||||
baseURL = $(".primary").attr("data-base_url"),
|
||||
pendingFetches = 0,
|
||||
writeInterval = false;
|
||||
|
||||
// It's a bummer, but we need to know which metrics have breakdown fields.
|
||||
// check by saying `if (metric in breakdownMetrics)`
|
||||
var breakdownMetrics = {
|
||||
"apps": true,
|
||||
"locales": true,
|
||||
"os": true,
|
||||
"sources": true,
|
||||
"versions": true,
|
||||
"statuses": true
|
||||
};
|
||||
|
||||
// is a metric an average or a sum?
|
||||
var metricTypes = {
|
||||
"usage" : "mean",
|
||||
"apps" : "mean",
|
||||
"locales" : "mean",
|
||||
"os" : "mean",
|
||||
"versions" : "mean",
|
||||
"statuses" : "mean",
|
||||
"downloads" : "sum",
|
||||
"sources" : "sum"
|
||||
};
|
||||
|
||||
// Initialize from localStorage when dom is ready.
|
||||
$(function() {
|
||||
dbg("looking for local data");
|
||||
if (verifyLocalStorage()) {
|
||||
var cacheObject = storageCache.get(addonId);
|
||||
if (cacheObject) {
|
||||
dbg("found local data, loading...");
|
||||
cacheObject = JSON.parse(cacheObject);
|
||||
if (cacheObject) {
|
||||
dataStore = cacheObject;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// These functions deal with our localStorage cache.
|
||||
|
||||
function writeLocalStorage() {
|
||||
dbg("saving local data");
|
||||
storageCache.set(addonId, JSON.stringify(dataStore));
|
||||
storage.set("version", STATS_VERSION);
|
||||
dbg("saved local data");
|
||||
}
|
||||
|
||||
function clearLocalStorage() {
|
||||
storageCache.remove(addonId);
|
||||
dbg("cleared local data");
|
||||
}
|
||||
|
||||
function verifyLocalStorage() {
|
||||
if (storage.get("version") == STATS_VERSION) {
|
||||
return true;
|
||||
} else {
|
||||
dbg("wrong offline data verion");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
document.onbeforeunload = writeLocalStorage;
|
||||
|
||||
|
||||
// Runs when 'changeview' event is detected.
|
||||
function processView(e, newView) {
|
||||
// Update our internal view state.
|
||||
currentView = $.extend(currentView, newView);
|
||||
|
||||
// Fetch the data from the server or storage, and notify other components.
|
||||
getDataRange(currentView, function(data) {
|
||||
$(window).trigger("dataready", {
|
||||
'view': currentView,
|
||||
'fields': getAvailableFields(currentView),
|
||||
'data': data
|
||||
});
|
||||
});
|
||||
}
|
||||
$(window).bind('changeview', processView);
|
||||
|
||||
|
||||
// Returns a list of field names for a given data set.
|
||||
function getAvailableFields(view) {
|
||||
var metric = view.metric,
|
||||
range = z.date.normalizeRange(view.range),
|
||||
start = range.start,
|
||||
end = range.end,
|
||||
ds,
|
||||
row,
|
||||
numRows = 0,
|
||||
step = z.date.millis("1 day"),
|
||||
fields = {};
|
||||
|
||||
// Non-breakdwon metrics only have one field.
|
||||
if (!(metric in breakdownMetrics)) return false;
|
||||
|
||||
ds = dataStore[metric];
|
||||
if (!ds) throw "Expected metric with valid data!";
|
||||
|
||||
// Locate all unique fields.
|
||||
for (var i=start; i<end; i+= step) {
|
||||
if (ds[i]) {
|
||||
row = (metric == 'apps') ? collapseVersions(ds[i], 1) : ds[i];
|
||||
_.each(row.data, function(v, k) {
|
||||
fields[k] = fields[k] ? fields[k] + v : v;
|
||||
});
|
||||
_.extend(fields, row.data);
|
||||
}
|
||||
}
|
||||
|
||||
// sort the fields, make them proper field identifiers, and return.
|
||||
return _.map(
|
||||
_.sortBy(
|
||||
_.keys(fields),
|
||||
function (f) {
|
||||
return -fields[f];
|
||||
}
|
||||
),
|
||||
function(f) {
|
||||
return "data|" + f;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// getDataRange: ensures we have all the data from the server we need,
|
||||
// and queues up requests to the server if the requested data is outside
|
||||
// the range currently stored locally. Once all server requests return,
|
||||
// we move on.
|
||||
function getDataRange(view, callback) {
|
||||
dbg("enter getDataRange");
|
||||
var range = z.date.normalizeRange(view.range),
|
||||
metric = view.metric,
|
||||
ds,
|
||||
needed = 0;
|
||||
|
||||
function finished() {
|
||||
needed--;
|
||||
dbg(pendingFetches, " fetches pending");
|
||||
if (needed < 1) {
|
||||
var ret = {}, i, row,
|
||||
step = z.date.millis("1 day");
|
||||
ds = dataStore[metric];
|
||||
for (var i=range.start; i<range.end; i+= step) {
|
||||
if (ds[i]) {
|
||||
ret[i] = (metric == 'apps') ? collapseVersions(ds[i], 1) : ds[i];
|
||||
}
|
||||
}
|
||||
ret = groupData(ret, view);
|
||||
callback.call(this, ret);
|
||||
}
|
||||
}
|
||||
|
||||
if (dataStore[metric]) {
|
||||
ds = dataStore[metric];
|
||||
dbg("range", range.start, range.end)
|
||||
if (ds.maxdate < range.end) {
|
||||
needed++;
|
||||
fetchData(metric, ds.maxdate, range.end, finished);
|
||||
}
|
||||
if (ds.mindate > range.start) {
|
||||
needed++;
|
||||
fetchData(metric, range.start, ds.mindate, finished);
|
||||
}
|
||||
if (ds.mindate <= range.start && ds.maxdate >= range.end) {
|
||||
dbg("all data found locally");
|
||||
finished();
|
||||
}
|
||||
} else {
|
||||
dbg("metric not found");
|
||||
needed++;
|
||||
fetchData(metric, range.start, range.end, finished);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Aggregate data based on our view's `group` setting.
|
||||
function groupData(data, view) {
|
||||
var metric = view.metric,
|
||||
range = z.date.normalizeRange(view.range),
|
||||
group = view.group || 'day',
|
||||
groupedData = {};
|
||||
// if grouping is by day, do nothing.
|
||||
if (group == 'day') return data;
|
||||
var groupKey = false,
|
||||
groupVal = false,
|
||||
groupCount = 0,
|
||||
d, row;
|
||||
// big loop!
|
||||
for (var i=range.start; i<range.end; i+= z.date.millis('1 day')) {
|
||||
d = new Date(i);
|
||||
row = data[i];
|
||||
// Here's where grouping points are caluculated.
|
||||
if ((group == 'week' && d.getDay() == 0) || (group == 'month' && d.getDate() == 1)) {
|
||||
// we drop the some days of data from the result set
|
||||
// if they are not a complete grouping.
|
||||
if (groupKey && groupVal) {
|
||||
// average `count` for mean metrics
|
||||
if (metricTypes[metric] == 'mean') {
|
||||
groupVal['count'] /= groupCount;
|
||||
}
|
||||
if (metric in breakdownMetrics) {
|
||||
_.each(groupVal.data, function(val, field) {
|
||||
// average for mean metrics.
|
||||
if (metricTypes[metric] == 'mean') {
|
||||
groupVal.data[field] /= groupCount;
|
||||
}
|
||||
});
|
||||
}
|
||||
groupedData[groupKey] = groupVal;
|
||||
}
|
||||
// set the new group date to the current iteration.
|
||||
groupKey = i;
|
||||
// reset our aggregates.
|
||||
groupCount = 0;
|
||||
groupVal = {
|
||||
date: z.date.date_string(new Date(groupKey), '-'),
|
||||
count: 0,
|
||||
data: {}
|
||||
};
|
||||
}
|
||||
// add the current row to our aggregates.
|
||||
if (row && groupVal) {
|
||||
groupVal.count += row.count;
|
||||
if (metric in breakdownMetrics) {
|
||||
_.each(row.data, function(val, field) {
|
||||
if (!groupVal.data[field]) {
|
||||
groupVal.data[field] = 0;
|
||||
}
|
||||
groupVal.data[field] += val;
|
||||
});
|
||||
}
|
||||
}
|
||||
groupCount++;
|
||||
}
|
||||
return groupedData;
|
||||
}
|
||||
|
||||
|
||||
// The beef. Negotiates with the server for data.
|
||||
function fetchData(metric, start, end, callback) {
|
||||
var seriesStart = start;
|
||||
var seriesEnd = end;
|
||||
|
||||
pendingFetches++;
|
||||
|
||||
var seriesURLStart = Highcharts.dateFormat('%Y%m%d', seriesStart),
|
||||
seriesURLEnd = Highcharts.dateFormat('%Y%m%d', seriesEnd),
|
||||
seriesURL = baseURL + ([metric,'day',seriesURLStart,seriesURLEnd]).join('-') + '.json';
|
||||
|
||||
dbg("GET", seriesURLStart, seriesURLEnd);
|
||||
|
||||
$.ajax({ url: seriesURL,
|
||||
dataType: 'text',
|
||||
success: fetchHandler});
|
||||
|
||||
function fetchHandler(raw_data, status, xhr) {
|
||||
var maxdate = 0,
|
||||
mindate = z.date.today();
|
||||
|
||||
if (xhr.status == 200) {
|
||||
|
||||
if (!dataStore[metric]) {
|
||||
dataStore[metric] = {};
|
||||
dataStore[metric].mindate = z.date.today();
|
||||
dataStore[metric].maxdate = 0;
|
||||
}
|
||||
|
||||
var ds = dataStore[metric], data;
|
||||
|
||||
data = JSON.parse(raw_data);
|
||||
|
||||
var i, datekey;
|
||||
for (i=0; i<data.length; i++) {
|
||||
datekey = parseInt(Date.parse(data[i].date), 10);
|
||||
maxdate = Math.max(datekey, maxdate);
|
||||
mindate = Math.min(datekey, mindate);
|
||||
ds[datekey] = data[i];
|
||||
}
|
||||
ds.maxdate = Math.max(parseInt(maxdate, 10), parseInt(ds.maxdate, 10));
|
||||
ds.mindate = Math.min(parseInt(mindate, 10), parseInt(ds.mindate, 10));
|
||||
pendingFetches--;
|
||||
callback.call(this, true);
|
||||
clearTimeout(writeInterval);
|
||||
writeInterval = setTimeout(writeLocalStorage, 1000);
|
||||
|
||||
} else if (xhr.status == 202) { //Handle a successful fetch but with no reponse
|
||||
|
||||
var retry_delay = 30000;
|
||||
|
||||
if (xhr.getResponseHeader("Retry-After")) {
|
||||
retry_delay = parseInt(xhr.getResponseHeader("Retry-After"), 10) * 1000;
|
||||
}
|
||||
|
||||
setTimeout(function () {
|
||||
AMO.fetchData(metric, start, end, callback);
|
||||
}, retry_delay);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Rounds application version strings to a given precision.
|
||||
// Passing `0` will truncate versions entirely.
|
||||
function collapseVersions(row, precision) {
|
||||
var out = {
|
||||
count : row.count,
|
||||
date : row.date,
|
||||
end : row.end
|
||||
},
|
||||
set,
|
||||
ver,
|
||||
key,
|
||||
apps = row.data,
|
||||
ret = {};
|
||||
|
||||
for (var i in apps) {
|
||||
if (apps.hasOwnProperty(i)) {
|
||||
set = apps[i];
|
||||
for (ver in set) {
|
||||
key = i + '_' + ver.split('.').slice(0,precision).join('.');
|
||||
if (!(key in ret)) {
|
||||
ret[key] = 0;
|
||||
}
|
||||
var v = parseFloat(set[ver]);
|
||||
ret[key] += v;
|
||||
}
|
||||
}
|
||||
}
|
||||
out.data = ret;
|
||||
return out;
|
||||
}
|
||||
|
||||
|
||||
// Takes a data row and a field identifier and returns the value.
|
||||
function getField(row, field) {
|
||||
var parts = field.split('|'),
|
||||
val = row;
|
||||
|
||||
if (!val) return null;
|
||||
for (var i = 0; i < parts.length; i++) {
|
||||
val = val[parts[i]];
|
||||
if (!val) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return val;
|
||||
}
|
||||
|
||||
|
||||
function getPrettyName(metric, field) {
|
||||
var parts = field.split('_');
|
||||
var key = parts[0];
|
||||
parts = parts.slice(1);
|
||||
|
||||
if (metric in csv_keys) {
|
||||
if (key in csv_keys[metric]) {
|
||||
return csv_keys[metric][key] + ' ' + parts.join(' ');
|
||||
}
|
||||
}
|
||||
return field;
|
||||
}
|
||||
|
||||
|
||||
// Expose some functionality to the z.StatsManager api.
|
||||
return {
|
||||
'fetchData' : fetchData,
|
||||
'dataStore' : dataStore,
|
||||
'getPrettyName' : getPrettyName,
|
||||
'getField' : getField
|
||||
};
|
||||
})();
|
|
@ -0,0 +1,21 @@
|
|||
$(function() {
|
||||
var initView = {
|
||||
metric: $('.primary').attr('data-report'),
|
||||
range: '365 days', //$('.primary').attr('data-range'),
|
||||
group: 'month'
|
||||
};
|
||||
|
||||
$(window).trigger('changeview', initView);
|
||||
});
|
||||
|
||||
$(window).bind("changeview", function(e, view) {
|
||||
var queryParams;
|
||||
if (view.range) {
|
||||
if (typeof view == "string") {
|
||||
queryparams = "last=" + view.split(/\s+/)[0];
|
||||
history.replaceState(view, document.title, '?' + queryparams);
|
||||
} else if (typeof view == "object") {
|
||||
|
||||
}
|
||||
}
|
||||
})
|
18
settings.py
18
settings.py
|
@ -476,6 +476,9 @@ MINIFY_BUNDLES = {
|
|||
'css/impala/apps.less',
|
||||
'css/impala/formset.less',
|
||||
),
|
||||
'zamboni/stats': (
|
||||
'css/impala/stats.less',
|
||||
),
|
||||
'zamboni/discovery-pane': (
|
||||
'css/zamboni/discovery-pane.css',
|
||||
'css/impala/promos.less',
|
||||
|
@ -722,11 +725,16 @@ MINIFY_BUNDLES = {
|
|||
'zamboni/stats': (
|
||||
'js/lib/jquery-datepicker.js',
|
||||
'js/lib/highcharts.src.js',
|
||||
'js/zamboni/stats/csv_keys.js',
|
||||
'js/zamboni/stats/helpers.js',
|
||||
'js/zamboni/stats/stats_manager.js',
|
||||
'js/zamboni/stats/stats_tables.js',
|
||||
'js/zamboni/stats/stats.js',
|
||||
'js/impala/stats/csv_keys.js',
|
||||
# 'js/zamboni/stats/helpers.js',
|
||||
# 'js/zamboni/stats/stats_manager.js',
|
||||
# 'js/zamboni/stats/stats_tables.js',
|
||||
# 'js/zamboni/stats/stats.js',
|
||||
'js/impala/stats/dateutils.js',
|
||||
'js/impala/stats/manager.js',
|
||||
'js/impala/stats/controls.js',
|
||||
'js/impala/stats/chart.js',
|
||||
'js/impala/stats/stats.js',
|
||||
),
|
||||
'zamboni/admin': (
|
||||
'js/zamboni/admin.js',
|
||||
|
|
Загрузка…
Ссылка в новой задаче