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:
Rishabh Garg 2021-02-19 11:18:01 +05:30 committed by GitHub
parent bb511c4fc5
commit 933d9b4635
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 157 additions and 71 deletions

View File

@ -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}}

View File

@ -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) {

View File

@ -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}) => {

View File

@ -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;

View File

@ -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),

View File

@ -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>