Added source attribution table to posts' analytics page

refs https://github.com/TryGhost/Team/issues/1921
This commit is contained in:
Simon Backx 2022-09-21 15:01:31 +02:00
parent cd4e4d7003
commit ad79d45926
8 changed files with 167 additions and 108 deletions

View File

@ -6,52 +6,11 @@
grid-template-columns: 2fr 1fr;
grid-gap: 64px;
">
<div class="gh-dashboard-recents-posts gh-dashboard-list">
<div class="gh-dashboard-list-header">
<div class="gh-dashboard-list-title">Sources</div>
<div class="gh-dashboard-list-title">Free Signups</div>
<div class="gh-dashboard-list-title">Paid Conversions</div>
</div>
<div class="gh-dashboard-list-body">
{{#each this.sources as |sourceData|}}
<div class="gh-dashboard-list-item">
<div class="gh-dashboard-list-item-sub">
<span class="gh-dashboard-list-text">{{sourceData.source}}</span>
</div>
<div class="gh-dashboard-list-item-sub">
<span class="gh-dashboard-metric-minivalue">
{{#if sourceData.freeSignups}}
{{format-number sourceData.freeSignups}}
{{else}}
&mdash;
{{/if}}
</span>
</div>
<div class="gh-dashboard-list-item-sub">
<span class="gh-dashboard-metric-minivalue">
{{#if sourceData.paidConversions}}
{{format-number sourceData.paidConversions}}
{{else}}
&mdash;
{{/if}}
</span>
</div>
</div>
{{else}}
<div class="gh-dashboard-list-empty">
<p>No sources.</p>
</div>
{{/each}}
</div>
</div>
<MemberAttribution::SourceAttributionTable @sources={{this.sources}} class="gh-dashboard-recents-posts gh-dashboard-list" />
<div style="border-left: 1px solid #eceef0; padding-left: 48px;display: flex;justify-content: center;align-items: center;">
<div style="max-width: 200px;">
<EmberChart
@type='doughnut'
@data={{this.chartData}}
@options={{this.chartOptions}}
@height={{400}}
/>
<MemberAttribution::SourceAttributionChart @sources={{this.sources}} />
</div>
</div>
</div>

View File

@ -1,76 +1,15 @@
import Component from '@glimmer/component';
import {action} from '@ember/object';
import {inject as service} from '@ember/service';
import {tracked} from '@glimmer/tracking';
const CHART_COLORS = [
'#853EED',
'#CA3FED',
'#E993CC',
'#EE9696',
'#FEC7C0'
];
export default class Recents extends Component {
@service dashboardStats;
@tracked chartType = 'free';
@action
loadData() {
this.dashboardStats.loadMemberAttributionStats();
}
get chartOptions() {
return {
cutoutPercentage: 60,
borderColor: '#555',
legend: {
display: true,
position: 'top',
align: 'start',
labels: {
color: 'rgb(255, 99, 132)',
fontSize: 12,
boxWidth: 10,
padding: 3
}
}
};
}
get chartData() {
if (this.chartType === 'free') {
const sortedByFree = [...this.sources];
sortedByFree.sort((a, b) => {
return b.freeSignups - a.freeSignups;
});
return {
labels: sortedByFree.slice(0, 5).map(source => source.source),
datasets: [{
label: 'Free Signups',
data: sortedByFree.slice(0, 5).map(source => source.freeSignups),
backgroundColor: CHART_COLORS.slice(0, 5),
borderWidth: 2,
borderColor: '#fff'
}]
};
} else {
const sortedByPaid = [...this.sources];
sortedByPaid.sort((a, b) => {
return b.paidPercentage - a.paidPercentage;
});
return {
labels: sortedByPaid.slice(0, 5).map(source => source.source),
datasets: [{
label: 'Paid Conversions',
data: sortedByPaid.slice(0, 5).map(source => source.paidConversions),
backgroundColor: CHART_COLORS.slice(0, 5),
borderWidth: 2,
borderColor: '#fff'
}]
};
}
}
get sources() {
return this.dashboardStats?.memberSourceAttributionCounts || [];
}

View File

@ -0,0 +1,6 @@
<EmberChart
@type='doughnut'
@data={{this.chartData}}
@options={{this.chartOptions}}
@height={{400}}
/>

View File

@ -0,0 +1,70 @@
import Component from '@glimmer/component';
import {tracked} from '@glimmer/tracking';
const CHART_COLORS = [
'#853EED',
'#CA3FED',
'#E993CC',
'#EE9696',
'#FEC7C0'
];
export default class SourceAttributionChart extends Component {
@tracked chartType = 'free';
get sources() {
return this.args.sources;
}
get chartOptions() {
return {
cutoutPercentage: 60,
borderColor: '#555',
legend: {
display: true,
position: 'top',
align: 'start',
labels: {
color: 'rgb(255, 99, 132)',
fontSize: 12,
boxWidth: 10,
padding: 3
}
}
};
}
get chartData() {
if (this.chartType === 'free') {
const sortedByFree = [...this.sources];
sortedByFree.sort((a, b) => {
return b.freeSignups - a.freeSignups;
});
return {
labels: sortedByFree.slice(0, 5).map(source => source.source),
datasets: [{
label: 'Free Signups',
data: sortedByFree.slice(0, 5).map(source => source.freeSignups),
backgroundColor: CHART_COLORS.slice(0, 5),
borderWidth: 2,
borderColor: '#fff'
}]
};
} else {
const sortedByPaid = [...this.sources];
sortedByPaid.sort((a, b) => {
return b.paidPercentage - a.paidPercentage;
});
return {
labels: sortedByPaid.slice(0, 5).map(source => source.source),
datasets: [{
label: 'Paid Conversions',
data: sortedByPaid.slice(0, 5).map(source => source.paidConversions),
backgroundColor: CHART_COLORS.slice(0, 5),
borderWidth: 2,
borderColor: '#fff'
}]
};
}
}
}

View File

@ -0,0 +1,38 @@
<div ...attributes>
<div class="gh-dashboard-list-header">
<div class="gh-dashboard-list-title">Sources</div>
<div class="gh-dashboard-list-title">Free Signups</div>
<div class="gh-dashboard-list-title">Paid Conversions</div>
</div>
<div class="gh-dashboard-list-body">
{{#each @sources as |sourceData|}}
<div class="gh-dashboard-list-item">
<div class="gh-dashboard-list-item-sub">
<span class="gh-dashboard-list-text">{{sourceData.source}}</span>
</div>
<div class="gh-dashboard-list-item-sub">
<span class="gh-dashboard-metric-minivalue">
{{#if sourceData.freeSignups}}
{{format-number sourceData.freeSignups}}
{{else}}
&mdash;
{{/if}}
</span>
</div>
<div class="gh-dashboard-list-item-sub">
<span class="gh-dashboard-metric-minivalue">
{{#if sourceData.paidConversions}}
{{format-number sourceData.paidConversions}}
{{else}}
&mdash;
{{/if}}
</span>
</div>
</div>
{{else}}
<div class="gh-dashboard-list-empty">
<p>No sources.</p>
</div>
{{/each}}
</div>
</div>

View File

@ -1,7 +1,34 @@
import Controller from '@ember/controller';
/**
* @typedef {import('../../services/dashboard-stats').SourceAttributionCount} SourceAttributionCount
*/
export default class AnalyticsController extends Controller {
get post() {
return this.model;
}
/**
* @returns {SourceAttributionCount[]} - array of objects with source and count properties
*/
get sources() {
return [
{
source: 'Twitter',
freeSignups: 12,
paidConversions: 50
},
{
source: 'Google',
freeSignups: 9,
paidConversions: 32
},
{
source: 'Direct',
freeSignups: 2,
paidConversions: 40
}
];
}
}

View File

@ -31,9 +31,9 @@ import {tracked} from '@glimmer/tracking';
*/
/**
* @typedef SourceAttributionCounts
* @typedef SourceAttributionCount
* @type {Object}
* @property {number} source Attribution Source
* @property {string} source Attribution Source
* @property {number} freeSignups Total free members signed up for this source
* @property {number} paidConversions Total paid conversions for this source
*/
@ -233,7 +233,7 @@ export default class DashboardStatsService extends Service {
}
/**
* @type {?SourceAttributionCounts}
* @type {SourceAttributionCount[]}
*/
get memberSourceAttributionCounts() {
if (!this.memberAttributionStats) {

View File

@ -85,6 +85,26 @@
</div>
</div>
{{#if (feature 'sourceAttribution')}}
<h4 class="gh-main-section-header small bn">
Source attribution
</h4>
<div class="gh-post-analytics-box resources">
<div style="display: grid;
grid-template-columns: 2fr 1fr;
grid-gap: 64px;
">
<MemberAttribution::SourceAttributionTable @sources={{this.sources}} />
<div style="border-left: 1px solid #eceef0; padding-left: 48px;display: flex;justify-content: center;align-items: center;">
<div style="max-width: 200px;">
<MemberAttribution::SourceAttributionChart @sources={{this.sources}} />
</div>
</div>
</div>
</div>
{{/if}}
<h4 class="gh-main-section-header small bn">
Get started with analytics
</h4>