Added technical details to stats (#20898)

[ANAL-1](https://linear.app/tryghost/issue/ANAL-32/add-stats-kpis-charts)

The technical details section in Stats contains only the browser breakdown ATM. This PR adds the rest (devices, operating systems) and fixes a couple of minor UI details on the rest of the charts
This commit is contained in:
Peter Zimon 2024-09-02 16:05:09 +02:00 committed by GitHub
parent 47d1a3c451
commit 397342a910
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 179 additions and 54 deletions

View File

@ -60,16 +60,17 @@ export default class KpisComponent extends Component {
params={params}
options={{
grid: {
left: 0,
right: 0,
top: 10,
left: '0%',
right: '0%',
top: '10%',
bottom: 0,
containLabel: true
},
xAxis: {
type: 'time',
min: startDate.toISOString(),
max: endDate.toISOString(),
// min: startDate.toISOString(),
// max: endDate.toISOString(),
boundaryGap: ['0%', '0.5%'],
axisLabel: {
formatter: chartDays <= 7 ? '{ee}' : '{dd} {MMM}'
},
@ -87,8 +88,7 @@ export default class KpisComponent extends Component {
lineStyle: {
color: '#DDE1E5'
}
},
boundaryGap: false
}
},
yAxis: {
splitLine: {
@ -109,7 +109,10 @@ export default class KpisComponent extends Component {
type: 'line',
z: 1
},
extraCssText: 'box-shadow: 0px 100px 80px 0px rgba(0, 0, 0, 0.07), 0px 41.778px 33.422px 0px rgba(0, 0, 0, 0.05), 0px 22.336px 17.869px 0px rgba(0, 0, 0, 0.04), 0px 12.522px 10.017px 0px rgba(0, 0, 0, 0.04), 0px 6.65px 5.32px 0px rgba(0, 0, 0, 0.03), 0px 2.767px 2.214px 0px rgba(0, 0, 0, 0.02);'
extraCssText: 'box-shadow: 0px 100px 80px 0px rgba(0, 0, 0, 0.07), 0px 41.778px 33.422px 0px rgba(0, 0, 0, 0.05), 0px 22.336px 17.869px 0px rgba(0, 0, 0, 0.04), 0px 12.522px 10.017px 0px rgba(0, 0, 0, 0.04), 0px 6.65px 5.32px 0px rgba(0, 0, 0, 0.03), 0px 2.767px 2.214px 0px rgba(0, 0, 0, 0.02);',
formatter: function (fparams) {
return `<div><div>${moment(fparams[0].value[0]).format('DD MMM, YYYY')}</div><div><span style="display: inline-block; margin-right: 16px; font-weight: 600;">Pageviews</span> ${fparams[0].value[1]}</div></div>`;
}
},
series: [
{
@ -148,7 +151,7 @@ export default class KpisComponent extends Component {
symbol: 'circle',
symbolSize: 10,
z: 8,
smooth: true,
smooth: false,
name: props.selected,
data: (data ?? []).map(row => [
String(row[INDEX]),

View File

@ -28,45 +28,137 @@ export default class KpisComponent extends Component {
site_uuid: this.config.stats.id,
date_from: startDate.format('YYYY-MM-DD'),
date_to: endDate.format('YYYY-MM-DD'),
member_status: audience.length === 0 ? null : audience.join(',')
member_status: audience.length === 0 ? null : audience.join(','),
limit: 5
};
let endpoint;
switch (props.selected) {
case 'browsers':
endpoint = `${this.config.stats.endpoint}/v0/pipes/top_browsers.json`;
break;
default:
endpoint = `${this.config.stats.endpoint}/v0/pipes/top_devices.json`;
}
const {data, meta, error, loading} = useQuery({
endpoint: `${this.config.stats.endpoint}/v0/pipes/top_browsers.json`,
endpoint: endpoint,
token: this.config.stats.token,
params
});
const colorPalette = ['#B78AFB', '#7FDE8A', '#FBCE75', '#F97DB7', '#6ED0FB'];
let transformedData;
let indexBy;
let tableHead;
switch (props.selected) {
case 'browsers':
transformedData = (data ?? []).map((item, index) => ({
name: item.browser.charAt(0).toUpperCase() + item.browser.slice(1),
value: item.hits,
color: colorPalette[index % colorPalette.length]
}));
indexBy = 'browser';
tableHead = 'Browser';
break;
default:
transformedData = (data ?? []).map((item, index) => ({
name: item.device.charAt(0).toUpperCase() + item.device.slice(1),
value: item.hits,
color: colorPalette[index % colorPalette.length]
}));
indexBy = 'device';
tableHead = 'Device';
}
return (
<DonutChart
data={data}
meta={meta}
loading={loading}
error={error}
index="browser"
categories={['hits']}
colorPalette={['#B78AFB', '#7FDE8A', '#FBCE75', '#F97DB7', '#6ED0FB']}
backgroundColor="transparent"
fontSize="13px"
textColor="#AEB7C1"
showLegend={true}
height="280px"
params={params}
options={{
tooltip: {
trigger: 'axis',
backgroundColor: '#fff',
textStyle: {
color: '#15171A'
},
axisPointer: {
type: 'line',
z: 1
},
extraCssText: 'border: none !important; box-shadow: 0px 100px 80px 0px rgba(0, 0, 0, 0.07), 0px 41.778px 33.422px 0px rgba(0, 0, 0, 0.05), 0px 22.336px 17.869px 0px rgba(0, 0, 0, 0.04), 0px 12.522px 10.017px 0px rgba(0, 0, 0, 0.04), 0px 6.65px 5.32px 0px rgba(0, 0, 0, 0.03), 0px 2.767px 2.214px 0px rgba(0, 0, 0, 0.02);'
}
}}
/>
<div className="gh-stats-piechart-container">
<table>
<thead>
<tr>
<th>{tableHead}</th>
<th>Hits</th>
</tr>
</thead>
<tbody>
{transformedData.map((item, index) => (
<tr key={index}>
<td>
<span style={{backgroundColor: item.color, display: 'inline-block', width: '10px', height: '10px', marginRight: '5px', borderRadius: '2px'}}></span>
{item.name}
</td>
<td>{item.value}</td>
</tr>
))}
</tbody>
</table>
<div className="gh-stats-piechart">
<DonutChart
data={data}
meta={meta}
loading={loading}
error={error}
index={indexBy}
categories={['hits']}
colorPalette={colorPalette}
backgroundColor="transparent"
fontSize="13px"
textColor="#AEB7C1"
showLegend={true}
params={params}
height="210px"
options={{
color: colorPalette,
tooltip: {
show: true,
trigger: 'item',
backgroundColor: '#fff',
textStyle: {
color: '#15171A'
},
extraCssText: 'border: none !important; box-shadow: 0px 100px 80px 0px rgba(0, 0, 0, 0.07), 0px 41.778px 33.422px 0px rgba(0, 0, 0, 0.05), 0px 22.336px 17.869px 0px rgba(0, 0, 0, 0.04), 0px 12.522px 10.017px 0px rgba(0, 0, 0, 0.04), 0px 6.65px 5.32px 0px rgba(0, 0, 0, 0.03), 0px 2.767px 2.214px 0px rgba(0, 0, 0, 0.02);',
formatter: function (fparams) {
return `<span style="background-color: ${fparams.color}; display: inline-block; width: 10px; height: 10px; margin-right: 5px; border-radius: 2px;"></span> ${fparams.name}: ${fparams.value}`;
}
},
legend: {
show: false,
orient: 'vertical',
left: 'left',
textStyle: {
color: '#AEB7C1'
}
},
series: [
{
animation: true,
name: 'Browser',
type: 'pie',
radius: ['60%', '90%'],
center: ['50%', '50%'], // Adjusted to align the chart to the top
data: transformedData,
label: {
show: false,
formatter: '{b}: {c}'
},
labelLine: {
lineStyle: {
color: '#DDE1E5'
},
smooth: 0.2,
length: 10,
length2: 20
},
padding: 0
}
]
}}
/>
</div>
</div>
);
};
}

View File

@ -27,7 +27,8 @@ export default class TopLocations extends Component {
site_uuid: this.config.stats.id,
date_from: startDate.format('YYYY-MM-DD'),
date_to: endDate.format('YYYY-MM-DD'),
member_status: audience.length === 0 ? null : audience.join(',')
member_status: audience.length === 0 ? null : audience.join(','),
limit: 6
};
const {data, meta, error, loading} = useQuery({
@ -45,7 +46,6 @@ export default class TopLocations extends Component {
index="location"
categories={['hits']}
colorPalette={['#E8D9FF']}
height="300px"
/>
);
};

View File

@ -29,11 +29,12 @@ export default class TopPages extends Component {
site_uuid: this.config.stats.id,
date_from: startDate.format('YYYY-MM-DD'),
date_to: endDate.format('YYYY-MM-DD'),
member_status: audience.length === 0 ? null : audience.join(',')
member_status: audience.length === 0 ? null : audience.join(','),
limit: 6
};
const {data, meta, error, loading} = useQuery({
endpoint: `${this.config.stats.endpoint}/v0/pipes/top_locations.json`,
endpoint: `${this.config.stats.endpoint}/v0/pipes/top_pages.json`,
token: this.config.stats.token,
params
});

View File

@ -35,7 +35,8 @@ export default class TopPages extends Component {
const {data, meta, error, loading} = useQuery({
endpoint: `${this.config.stats.endpoint}/v0/pipes/top_sources.json`,
token: this.config.stats.token,
params
params,
limit: 6
});
return (

View File

@ -26,7 +26,7 @@ export default class KpisOverview extends Component {
@task
*fetchData() {
try {
const response = yield fetch(`${this.config.stats.endpoint}/v0/pipes/kpis.json`, {
const response = yield fetch(`${this.config.stats.endpoint}/v0/pipes/kpis.json?site_uuid=${this.config.stats.id}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
@ -39,6 +39,7 @@ export default class KpisOverview extends Component {
}
const rawData = yield response.json();
this.totals = this.processData(rawData.data);
} catch (error) {
// console.error('Error fetching KPI data:', error);

View File

@ -9,10 +9,10 @@
@label="Browsers" />
</button>
<button type="button" class="gh-stats-tab {{if this.osTabSelected 'is-selected'}}" {{on "click" this.changeTabToOSs}}>
{{!-- <button type="button" class="gh-stats-tab {{if this.osTabSelected 'is-selected'}}" {{on "click" this.changeTabToOSs}}>
<Stats::Parts::Metric
@label="Operating systems" />
</button>
</button> --}}
</div>
<Stats::Charts::Technical @chartDays={{@chartDays}} @audience={{@audience}} @selected={{this.selected}} />

View File

@ -73,7 +73,7 @@
content: "";
position: absolute;
width: 100%;
height: 1px;
height: 2px;
bottom: -1px;
left: 0;
right: 0;
@ -107,4 +107,31 @@
letter-spacing: -0.05em;
font-weight: 600;
line-height: 1;
}
.gh-stats-piechart-container {
display: flex;
gap: 16px;
align-items: flex-start;
width: 100%;
}
.gh-stats-piechart-container table {
margin-top: 5px;
width: 100%;
margin-bottom: 0;
}
.gh-stats-piechart-container .table td,
.gh-stats-piechart-container.table th,
.gh-stats-piechart-container table td,
.gh-stats-piechart-container table th {
padding: 6px 6px 6px 0px;
}
.gh-stats-piechart {
width: 100%;
height: 100%;
flex-grow: 1;
margin-right: -20px;
}

View File

@ -37,27 +37,27 @@
<section class="view-container gh-stats">
<section class="gh-stats-container">
<Stats::KpisOverview @chartDays={{this.days}} @audience={{this.audience}} />
<Stats::KpisOverview @chartDays={{this.chartDays}} @audience={{this.audience}} />
</section>
<section class="gh-stats-grid cols-2">
<div class="gh-stats-container">
<h5 class="gh-stats-metric-label">Top posts & pages</h5>
<Stats::Charts::TopPages @chartDays={{this.days}} @audience={{this.audience}} />
<Stats::Charts::TopPages @chartDays={{this.chartDays}} @audience={{this.audience}} />
</div>
<div class="gh-stats-container">
<h5 class="gh-stats-metric-label">Sources</h5>
<Stats::Charts::TopSources @chartDays={{this.days}} @audience={{this.audience}} />
<Stats::Charts::TopSources @chartDays={{this.chartDays}} @audience={{this.audience}} />
</div>
</section>
<section class="gh-stats-grid cols-2">
<div class="gh-stats-container">
<h5 class="gh-stats-metric-label">Top locations</h5>
<Stats::Charts::TopLocations @chartDays={{this.days}} @audience={{this.audience}} />
<Stats::Charts::TopLocations @chartDays={{this.chartDays}} @audience={{this.audience}} />
</div>
<div class="gh-stats-container">
<Stats::TechnicalOverview @chartDays={{this.days}} @audience={{this.audience}} />
<Stats::TechnicalOverview @chartDays={{this.chartDays}} @audience={{this.audience}} />
</div>
</section>