mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-27 04:43:12 +03:00
1ad2c05d37
no issue - new linting rules that needed fixing: - calling `super` in lifecycle hooks - no usage of String prototype extensions
365 lines
12 KiB
JavaScript
365 lines
12 KiB
JavaScript
/* global Chart */
|
|
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';
|
|
|
|
const DATE_FORMAT = 'D MMM YYYY';
|
|
|
|
export default Component.extend({
|
|
ajax: service(),
|
|
membersStats: service(),
|
|
|
|
// public attrs
|
|
nightShift: false,
|
|
lineColor: '#14b8ff',
|
|
|
|
stats: null,
|
|
tagName: '',
|
|
chartStats: null,
|
|
chartData: null,
|
|
chartOptions: null,
|
|
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 '';
|
|
}
|
|
|
|
let firstDate = Object.keys(this.membersStats.stats.total_on_date)[0];
|
|
return moment(firstDate).format(DATE_FORMAT);
|
|
}),
|
|
|
|
selectedRange: computed('membersStats.days', function () {
|
|
const availableRanges = this.availableRanges;
|
|
return availableRanges.findBy('days', this.membersStats.days);
|
|
}),
|
|
|
|
availableRanges: computed(function () {
|
|
return [{
|
|
name: '30 days',
|
|
days: '30'
|
|
}, {
|
|
name: '90 days',
|
|
days: '90'
|
|
}, {
|
|
name: '365 days',
|
|
days: '365'
|
|
}, {
|
|
name: 'All time',
|
|
days: 'all-time'
|
|
}];
|
|
}),
|
|
|
|
// Lifecycle ---------------------------------------------------------------
|
|
|
|
init() {
|
|
this._super(...arguments);
|
|
this.setChartJSDefaults();
|
|
},
|
|
|
|
didReceiveAttrs() {
|
|
this._super(...arguments);
|
|
|
|
if (this.chartStats) {
|
|
const {options, data, title, stats} = this.chartStats;
|
|
|
|
this.set('stats', stats);
|
|
this.set('chartHeading', title);
|
|
this.setChartData(data);
|
|
this.setChartOptions(options);
|
|
}
|
|
|
|
if (this._lastNightShift !== undefined && this.nightShift !== this._lastNightShift) {
|
|
const {options = {}} = this.chartStats;
|
|
|
|
this.setChartOptions(options);
|
|
}
|
|
this._lastNightShift = this.nightShift;
|
|
},
|
|
|
|
// Actions -----------------------------------------------------------------
|
|
|
|
changeDateRange: action(function (range) {
|
|
this.membersStats.days = get(range, 'days');
|
|
this.fetchStatsTask.perform();
|
|
}),
|
|
|
|
// Tasks -------------------------------------------------------------------
|
|
|
|
fetchStatsTask: task(function* () {
|
|
let stats;
|
|
if (!this.chartType) {
|
|
this.set('stats', null);
|
|
stats = yield this.membersStats.fetch();
|
|
this.setOriginalChartData(stats);
|
|
}
|
|
}),
|
|
|
|
setOriginalChartData(stats) {
|
|
if (stats) {
|
|
this.set('stats', stats);
|
|
|
|
this.setChartOptions({
|
|
rangeInDays: Object.keys(stats.total_on_date).length
|
|
});
|
|
|
|
this.setChartData({
|
|
dateLabels: Object.keys(stats.total_on_date),
|
|
dateValues: Object.values(stats.total_on_date)
|
|
});
|
|
}
|
|
},
|
|
|
|
// Internal ----------------------------------------------------------------
|
|
|
|
setChartData({dateLabels, dateValues, label = 'Total Members'}) {
|
|
let backgroundColors = this.lineColor;
|
|
|
|
if (this.chartType === 'open-rate') {
|
|
backgroundColors = dateLabels.map((val) => {
|
|
if (val) {
|
|
return this.lineColor;
|
|
} else {
|
|
return (this.nightShift ? '#7C8B9A' : '#CED4D9');
|
|
}
|
|
});
|
|
}
|
|
|
|
this.set('chartData', {
|
|
labels: dateLabels,
|
|
datasets: [{
|
|
label: label,
|
|
cubicInterpolationMode: 'monotone',
|
|
data: dateValues,
|
|
fill: false,
|
|
backgroundColor: backgroundColors,
|
|
pointRadius: 0,
|
|
pointHitRadius: 10,
|
|
borderColor: this.lineColor,
|
|
borderJoinStyle: 'miter',
|
|
maxBarThickness: 20,
|
|
minBarLength: 2
|
|
}]
|
|
});
|
|
},
|
|
|
|
setChartOptions({rangeInDays}) {
|
|
let maxTicksAllowed = this.isSmall ? 3 : this.getTicksForRange(rangeInDays);
|
|
|
|
if (this.chartType === 'open-rate') {
|
|
maxTicksAllowed = 0;
|
|
}
|
|
|
|
this.setChartJSDefaults();
|
|
let options = {
|
|
responsive: true,
|
|
responsiveAnimationDuration: 5,
|
|
maintainAspectRatio: false,
|
|
layout: {
|
|
padding: {
|
|
top: (this.isSmall ? 20 : 5), // Needed otherwise the top dot is cut
|
|
right: 10,
|
|
bottom: (this.isSmall ? 20 : 5),
|
|
left: 10
|
|
}
|
|
},
|
|
title: {
|
|
display: false
|
|
},
|
|
tooltips: {
|
|
intersect: false,
|
|
mode: 'index',
|
|
displayColors: false,
|
|
backgroundColor: '#15171A',
|
|
xPadding: 7,
|
|
yPadding: 7,
|
|
cornerRadius: 5,
|
|
caretSize: 7,
|
|
caretPadding: 5,
|
|
bodyFontSize: 12.5,
|
|
titleFontSize: 12,
|
|
titleFontStyle: 'normal',
|
|
titleFontColor: 'rgba(255, 255, 255, 0.7)',
|
|
titleMarginBottom: 3,
|
|
filter: (tooltipItems, data) => {
|
|
if (this.chartType === 'open-rate') {
|
|
let label = data.labels[tooltipItems.index];
|
|
if (label === '') {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
},
|
|
callbacks: {
|
|
label: (tooltipItems, data) => {
|
|
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 = getSymbol(this.stats.currency);
|
|
valueText = `${currency}${valueText}`;
|
|
}
|
|
if (this.chartType === 'open-rate') {
|
|
valueText = `${valueText}%`;
|
|
}
|
|
return `${labelText}: ${valueText}`;
|
|
},
|
|
title: (tooltipItems) => {
|
|
if (this.chartType === 'open-rate') {
|
|
if (tooltipItems.length) {
|
|
return tooltipItems[0].xLabel;
|
|
} else {
|
|
return '';
|
|
}
|
|
}
|
|
return moment(tooltipItems[0].xLabel).format(DATE_FORMAT);
|
|
}
|
|
}
|
|
},
|
|
hover: {
|
|
mode: 'index',
|
|
intersect: false,
|
|
animationDuration: 120
|
|
},
|
|
legend: {
|
|
display: false
|
|
},
|
|
scales: {
|
|
xAxes: [{
|
|
labelString: 'Date',
|
|
gridLines: {
|
|
drawTicks: false,
|
|
color: (this.nightShift ? '#333F44' : '#DDE1E5'),
|
|
zeroLineColor: (this.nightShift ? '#333F44' : '#DDE1E5')
|
|
},
|
|
ticks: {
|
|
display: false,
|
|
maxRotation: 0,
|
|
minRotation: 0,
|
|
padding: 6,
|
|
autoSkip: false,
|
|
fontColor: '#626D79',
|
|
maxTicksLimit: 10,
|
|
callback: (value, index, values) => {
|
|
let step = (values.length - 1) / (maxTicksAllowed);
|
|
let steps = [];
|
|
for (let i = 0; i < maxTicksAllowed; i++) {
|
|
steps.push(Math.ceil(i * step));
|
|
}
|
|
|
|
if (index === 0) {
|
|
return value;
|
|
}
|
|
if (index === (values.length - 1) && this.chartType !== 'open-rate') {
|
|
return 'Today';
|
|
}
|
|
|
|
if (steps.includes(index)) {
|
|
return '';
|
|
}
|
|
}
|
|
}
|
|
}],
|
|
yAxes: [{
|
|
gridLines: {
|
|
drawTicks: false,
|
|
display: false,
|
|
drawBorder: false
|
|
},
|
|
ticks: {
|
|
maxTicksLimit: 5,
|
|
fontColor: '#7C8B9A',
|
|
padding: 8,
|
|
precision: 0,
|
|
callback: (value) => {
|
|
const currency = this.chartType === 'mrr' ? getSymbol(this.stats.currency) : '';
|
|
if (parseInt(value) >= 1000) {
|
|
return currency + value.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
|
} else {
|
|
return currency + value;
|
|
}
|
|
}
|
|
}
|
|
}]
|
|
}
|
|
};
|
|
|
|
if (this.chartType === 'mrr' || this.chartType === 'all-members') {
|
|
const chartData = this.get('chartData').datasets[0].data;
|
|
let allZeros = true;
|
|
for (let i = 0; i < chartData.length; i++) {
|
|
const element = chartData[i];
|
|
if (element !== 0) {
|
|
allZeros = false;
|
|
break;
|
|
}
|
|
}
|
|
if (allZeros) {
|
|
options.scales.yAxes[0].ticks.suggestedMin = 0;
|
|
options.scales.yAxes[0].ticks.suggestedMax = 100;
|
|
}
|
|
}
|
|
|
|
if (this.chartType === 'open-rate') {
|
|
options.scales.yAxes[0].ticks.suggestedMin = 0;
|
|
}
|
|
|
|
if (this.isSmall) {
|
|
options.scales.yAxes[0].ticks.display = false;
|
|
options.scales.xAxes[0].gridLines.display = true;
|
|
}
|
|
this.set('chartOptions', options);
|
|
},
|
|
|
|
getTicksForRange(rangeInDays) {
|
|
if (rangeInDays <= 30) {
|
|
return 5;
|
|
} else if (rangeInDays <= 90) {
|
|
return 10;
|
|
} else {
|
|
return 15;
|
|
}
|
|
},
|
|
|
|
setChartJSDefaults() {
|
|
Chart.defaults.LineWithLine = Chart.defaults.line;
|
|
Chart.controllers.LineWithLine = Chart.controllers.line.extend({
|
|
draw: function (ease) {
|
|
Chart.controllers.line.prototype.draw.call(this, ease);
|
|
|
|
if (this.chart.tooltip._active && this.chart.tooltip._active.length) {
|
|
let activePoint = this.chart.tooltip._active[0];
|
|
let ctx = this.chart.ctx;
|
|
let x = activePoint.tooltipPosition().x;
|
|
let topY = this.chart.scales['y-axis-0'].top;
|
|
let bottomY = this.chart.scales['y-axis-0'].bottom;
|
|
|
|
// draw line
|
|
ctx.save();
|
|
ctx.beginPath();
|
|
ctx.moveTo(x, topY);
|
|
ctx.lineTo(x, bottomY);
|
|
ctx.lineWidth = 1;
|
|
ctx.strokeStyle = (this.nightShift ? 'rgba(62, 176, 239, 0.65)' : 'rgba(62, 176, 239, 0.1)');
|
|
ctx.stroke();
|
|
ctx.restore();
|
|
}
|
|
}
|
|
});
|
|
}
|
|
});
|