Updated MRR and count dashboard charts (#1846)
refs https://github.com/TryGhost/Team/issues/469 - Cleaned MRR stats data and label formatting - Cleaned member counts stats - total and paid
This commit is contained in:
parent
bb511c4fc5
commit
933d9b4635
|
@ -27,7 +27,7 @@
|
|||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="gh-members-chart-container">
|
||||
<div class="gh-members-chart-container {{if this.isSmall "gh-members-chart-container-small"}}">
|
||||
{{#if (not this.stats)}}
|
||||
<GhLoadingSpinner />
|
||||
{{else}}
|
||||
|
|
|
@ -3,6 +3,7 @@ import Component from '@ember/component';
|
|||
import moment from 'moment';
|
||||
import {action} from '@ember/object';
|
||||
import {computed, get} from '@ember/object';
|
||||
import {getSymbol} from 'ghost-admin/utils/currency';
|
||||
import {inject as service} from '@ember/service';
|
||||
import {task} from 'ember-concurrency';
|
||||
|
||||
|
@ -22,8 +23,16 @@ export default Component.extend({
|
|||
showSummary: true,
|
||||
showRange: true,
|
||||
chartType: '',
|
||||
chartSize: '',
|
||||
chartHeading: 'Total Members',
|
||||
|
||||
isSmall: computed('chartSize', function () {
|
||||
if (this.chartSize === 'small') {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}),
|
||||
|
||||
startDateLabel: computed('membersStats.stats', function () {
|
||||
if (!this.membersStats?.stats?.total_on_date) {
|
||||
return '';
|
||||
|
@ -67,7 +76,12 @@ export default Component.extend({
|
|||
}
|
||||
|
||||
if (this.chartStats) {
|
||||
this.setMRRChartData(this.chartStats);
|
||||
const {options, data, title, stats} = this.chartStats;
|
||||
|
||||
this.set('stats', stats);
|
||||
this.set('chartHeading', title);
|
||||
this.setChartOptions(options);
|
||||
this.setChartData(data);
|
||||
}
|
||||
this._lastNightShift = this.nightShift;
|
||||
},
|
||||
|
@ -83,57 +97,13 @@ export default Component.extend({
|
|||
|
||||
fetchStatsTask: task(function* () {
|
||||
let stats;
|
||||
if (this.chartType !== 'mrr') {
|
||||
if (!this.chartType) {
|
||||
this.set('stats', null);
|
||||
stats = yield this.membersStats.fetch();
|
||||
this.setOriginalChartData(stats);
|
||||
}
|
||||
}),
|
||||
|
||||
setMRRChartData(stats) {
|
||||
const statsForCurrency = stats && stats[0];
|
||||
if (statsForCurrency) {
|
||||
statsForCurrency.data = this.membersStats.fillDates(statsForCurrency.data) || {};
|
||||
|
||||
this.set('stats', statsForCurrency);
|
||||
|
||||
this.setChartOptions({
|
||||
rangeInDays: 30
|
||||
});
|
||||
this.set('chartHeading', 'MRR');
|
||||
this.setChartData({
|
||||
label: 'Total MRR',
|
||||
dateLabels: Object.keys(statsForCurrency.data),
|
||||
dateValues: Object.values(statsForCurrency.data).map(val => val / 100)
|
||||
});
|
||||
} else {
|
||||
this.set('stats', {});
|
||||
this.set('chartHeading', 'MRR');
|
||||
|
||||
this.setChartData({
|
||||
label: 'Total MRR',
|
||||
dateLabels: [],
|
||||
dateValues: []
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
setCountsChartData(stats) {
|
||||
if (stats) {
|
||||
this.set('stats', stats);
|
||||
|
||||
this.setChartOptions({
|
||||
rangeInDays: 30
|
||||
});
|
||||
this.set('chartHeading', 'Total Members');
|
||||
this.setChartData({
|
||||
label: 'Total Members',
|
||||
dateLabels: stats.data.map(d => d.date),
|
||||
dateValues: stats.data.map(d => d.paid)
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
setOriginalChartData(stats) {
|
||||
if (stats) {
|
||||
this.set('stats', stats);
|
||||
|
@ -172,8 +142,7 @@ export default Component.extend({
|
|||
let maxTicksAllowed = this.getTicksForRange(rangeInDays);
|
||||
|
||||
this.setChartJSDefaults();
|
||||
|
||||
this.set('chartOptions', {
|
||||
let options = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
layout: {
|
||||
|
@ -206,8 +175,8 @@ export default Component.extend({
|
|||
const labelText = data.datasets[tooltipItems.datasetIndex].label;
|
||||
let valueText = data.datasets[tooltipItems.datasetIndex].data[tooltipItems.index].toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
||||
if (this.chartType === 'mrr') {
|
||||
const currency = this.stats.currency;
|
||||
valueText = `${currency.toUpperCase()} ${valueText}`;
|
||||
const currency = getSymbol(this.stats.currency);
|
||||
valueText = `${currency}${valueText}`;
|
||||
}
|
||||
return `${labelText}: ${valueText}`;
|
||||
},
|
||||
|
@ -277,7 +246,12 @@ export default Component.extend({
|
|||
}
|
||||
}]
|
||||
}
|
||||
});
|
||||
};
|
||||
if (this.isSmall) {
|
||||
options.scales.yAxes[0].ticks.display = false;
|
||||
options.scales.xAxes[0].gridLines.display = false;
|
||||
}
|
||||
this.set('chartOptions', options);
|
||||
},
|
||||
|
||||
getTicksForRange(rangeInDays) {
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import Controller from '@ember/controller';
|
||||
import {getSymbol} from 'ghost-admin/utils/currency';
|
||||
import {inject as service} from '@ember/service';
|
||||
import {tracked} from '@glimmer/tracking';
|
||||
|
||||
|
@ -19,6 +20,12 @@ export default class DashboardController extends Controller {
|
|||
error: null,
|
||||
loading: false
|
||||
};
|
||||
@tracked
|
||||
memberCountStats = {
|
||||
data: null,
|
||||
error: null,
|
||||
loading: false
|
||||
};
|
||||
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
@ -26,16 +33,83 @@ export default class DashboardController extends Controller {
|
|||
this.loadCharts();
|
||||
}
|
||||
|
||||
loadCharts() {
|
||||
loadMRRStats() {
|
||||
this.membersStats.fetchMRR().then((stats) => {
|
||||
this.mrrStats.data = stats;
|
||||
this.events.loading = false;
|
||||
|
||||
const currencyStats = stats[0];
|
||||
if (currencyStats) {
|
||||
currencyStats.data = this.membersStats.fillDates(currencyStats.data) || {};
|
||||
const dateValues = Object.values(currencyStats.data).map(val => val / 100);
|
||||
const currentMRR = dateValues.length ? dateValues[dateValues.length - 1] : 0;
|
||||
this.mrrStats.data = {
|
||||
current: `${getSymbol(currencyStats.currency)}${currentMRR}`,
|
||||
options: {
|
||||
rangeInDays: 30
|
||||
},
|
||||
data: {
|
||||
label: 'MRR',
|
||||
dateLabels: Object.keys(currencyStats.data),
|
||||
dateValues
|
||||
},
|
||||
title: 'MRR',
|
||||
stats: currencyStats
|
||||
};
|
||||
}
|
||||
}, (error) => {
|
||||
this.mrrStats.error = error;
|
||||
this.events.loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
loadMemberCountStats() {
|
||||
this.membersStats.fetchCounts().then((stats) => {
|
||||
this.events.loading = false;
|
||||
|
||||
if (stats) {
|
||||
stats.data = this.membersStats.fillCountDates(stats.data) || {};
|
||||
const dateValues = Object.values(stats.data);
|
||||
|
||||
this.memberCountStats.data = {
|
||||
all: {
|
||||
total: dateValues.length ? dateValues[dateValues.length - 1].total : 0,
|
||||
options: {
|
||||
rangeInDays: 30
|
||||
},
|
||||
data: {
|
||||
label: 'Members',
|
||||
dateLabels: Object.keys(stats.data),
|
||||
dateValues: dateValues.map(d => d.total)
|
||||
},
|
||||
title: 'Total Members',
|
||||
stats: stats
|
||||
},
|
||||
paid: {
|
||||
total: dateValues.length ? dateValues[dateValues.length - 1].paid : 0,
|
||||
options: {
|
||||
rangeInDays: 30
|
||||
},
|
||||
data: {
|
||||
label: 'Members',
|
||||
dateLabels: Object.keys(stats.data),
|
||||
dateValues: dateValues.map(d => d.paid)
|
||||
},
|
||||
title: 'Paid Members',
|
||||
stats: stats
|
||||
}
|
||||
};
|
||||
}
|
||||
}, (error) => {
|
||||
this.mrrStats.error = error;
|
||||
this.events.loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
loadCharts() {
|
||||
this.loadMRRStats();
|
||||
this.loadMemberCountStats();
|
||||
}
|
||||
|
||||
loadEvents() {
|
||||
this.events.loading = true;
|
||||
this.membersStats.fetchTimeline().then(({events}) => {
|
||||
|
|
|
@ -66,12 +66,38 @@ export default class MembersStatsService extends Service {
|
|||
|
||||
let endDate = moment().add(1, 'hour');
|
||||
const output = {};
|
||||
|
||||
let lastVal = 0;
|
||||
while (currentRangeDate.isBefore(endDate)) {
|
||||
let dateStr = currentRangeDate.format('YYYY-MM-DD');
|
||||
const dataOnDate = data.find(d => d.date === dateStr);
|
||||
output[dateStr] = dataOnDate ? dataOnDate.value : 0;
|
||||
output[dateStr] = dataOnDate ? dataOnDate.value : lastVal;
|
||||
lastVal = output[dateStr];
|
||||
currentRangeDate = currentRangeDate.add(1, 'day');
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
fillCountDates(data) {
|
||||
let currentRangeDate = moment().subtract(29, 'days');
|
||||
|
||||
let endDate = moment().add(1, 'hour');
|
||||
const output = {};
|
||||
let lastVal = {
|
||||
paid: 0,
|
||||
free: 0,
|
||||
comped: 0,
|
||||
total: 0
|
||||
};
|
||||
while (currentRangeDate.isBefore(endDate)) {
|
||||
let dateStr = currentRangeDate.format('YYYY-MM-DD');
|
||||
const dataOnDate = data.find(d => d.date === dateStr);
|
||||
output[dateStr] = dataOnDate ? {
|
||||
paid: dataOnDate.paid,
|
||||
free: dataOnDate.free,
|
||||
comped: dataOnDate.comped,
|
||||
total: dataOnDate.paid + dataOnDate.free + dataOnDate.comped
|
||||
} : lastVal;
|
||||
lastVal = output[dateStr];
|
||||
currentRangeDate = currentRangeDate.add(1, 'day');
|
||||
}
|
||||
return output;
|
||||
|
|
|
@ -215,6 +215,13 @@ p.gh-members-list-email {
|
|||
padding: 4px 20px 11px 0;
|
||||
}
|
||||
|
||||
.gh-members-chart-container.gh-members-chart-container-small {
|
||||
height: 90px;
|
||||
width: 120px;
|
||||
margin: 0 0 0 -16px;
|
||||
padding: 4px 20px 11px 0;
|
||||
}
|
||||
|
||||
.gh-members-chart-xlabels {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
@ -277,7 +284,7 @@ p.gh-members-list-email {
|
|||
|
||||
.gh-members-chart-box.black .gh-members-chart-summary-heading {
|
||||
color: var(--lightgrey);
|
||||
}
|
||||
}
|
||||
|
||||
.gh-members-chart-box.black .gh-members-chart-header {
|
||||
border-color: var(--darkgrey);
|
||||
|
@ -457,7 +464,7 @@ p.gh-members-list-email {
|
|||
.gh-members-list-subscribed-moment {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
.gh-members-list-chevron {
|
||||
display: none;
|
||||
}
|
||||
|
@ -676,16 +683,16 @@ textarea.gh-member-details-textarea {
|
|||
.gh-member-details {
|
||||
order: 1;
|
||||
}
|
||||
|
||||
|
||||
.gh-member-feed {
|
||||
order: 4;
|
||||
}
|
||||
|
||||
|
||||
.gh-member-internal-info {
|
||||
order: 2;
|
||||
margin-top: 3.2rem;
|
||||
}
|
||||
|
||||
|
||||
.gh-member-stripe {
|
||||
order: 3;
|
||||
}
|
||||
|
@ -904,7 +911,7 @@ p.gh-members-import-errorcontext {
|
|||
/* Shadow covers */
|
||||
linear-gradient(var(--white) 30%, rgba(255,255,255,0)),
|
||||
linear-gradient(rgba(255,255,255,0), var(--white) 70%) 0 100%,
|
||||
|
||||
|
||||
/* Shadows */
|
||||
/* radial-gradient(farthest-side at 50% 0, rgba(0,0,0,.12), rgba(0,0,0,0)) -64px 0,
|
||||
radial-gradient(farthest-side at 50% 100%, rgba(0,0,0,.12), rgba(0,0,0,0)) -64px 100%; */
|
||||
|
@ -913,7 +920,7 @@ p.gh-members-import-errorcontext {
|
|||
background-repeat: no-repeat;
|
||||
background-color: var(--white);
|
||||
background-size: 100% 40px, 100% 40px, 100% 14px, 100% 14px;
|
||||
|
||||
|
||||
/* Opera doesn't support this in the shorthand */
|
||||
background-attachment: local, local, scroll, scroll;
|
||||
margin-top: 4px;
|
||||
|
@ -1292,7 +1299,7 @@ p.gh-members-import-errordetail:first-of-type {
|
|||
border-bottom-right-radius: 3px;
|
||||
padding: 0;
|
||||
margin: 32px auto;
|
||||
box-shadow:
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(0,0,0,0.02),
|
||||
0 2.8px 2.2px rgba(0, 0, 0, 0.02),
|
||||
0 6.7px 5.3px rgba(0, 0, 0, 0.028),
|
||||
|
|
|
@ -14,8 +14,13 @@
|
|||
</div>
|
||||
<div class="gh-dashboard-chart-container">
|
||||
<div class="gh-dashboard-summary">
|
||||
<div class="data">$0</div>
|
||||
<div class="growth">0.0%</div>
|
||||
{{#if this.mrrStats.data}}
|
||||
<div class="data">{{this.mrrStats.data.current}}</div>
|
||||
<div class="growth">0.0%</div>
|
||||
{{else}}
|
||||
<div class="data">$0</div>
|
||||
<div class="growth">0.0%</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{#if this.mrrStats.data}}
|
||||
<div class="gh-dashboard-chart">
|
||||
|
@ -33,12 +38,12 @@
|
|||
<div class="gh-dashboard-summary small">
|
||||
<h4 class="gh-dashboard-header">Total members</h4>
|
||||
<div class="data-container">
|
||||
<div class="data">0</div>
|
||||
<div class="data">{{this.memberCountStats.data.all.total}}</div>
|
||||
<div class="growth">0.0%</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gh-dashboard-chart small">
|
||||
No data
|
||||
<GhMembersChart @nightShift={{feature "nightShift"}} @chartSize="small" @showSummary={{false}} @chartType="all-members" @showRange={{false}} style="width:100%" @chartStats={{this.memberCountStats.data.all}} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -47,12 +52,12 @@
|
|||
<div class="gh-dashboard-summary small">
|
||||
<h4 class="gh-dashboard-header">Paid members</h4>
|
||||
<div class="data-container">
|
||||
<div class="data">0</div>
|
||||
<div class="data">{{this.memberCountStats.data.paid.total}}</div>
|
||||
<div class="growth">0.0%</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gh-dashboard-chart small nodata">
|
||||
No data
|
||||
<div class="gh-dashboard-chart small">
|
||||
<GhMembersChart @nightShift={{feature "nightShift"}} @chartSize="small" @showSummary={{false}} @chartType="paid-members" @showRange={{false}} style="width:100%" @chartStats={{this.memberCountStats.data.paid}} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
Loading…
Reference in New Issue