diff --git a/analytics/analytics_package/analytics/static_site/template/index.html b/analytics/analytics_package/analytics/static_site/template/index.html index 14dde9257..95f4042ff 100644 --- a/analytics/analytics_package/analytics/static_site/template/index.html +++ b/analytics/analytics_package/analytics/static_site/template/index.html @@ -497,26 +497,52 @@

Filter Selections

document.getElementById('generated-date').textContent = meta.generated_at; } + // Returns a signed percentage as a number, or null when there is no + // usable baseline. Rounding happens at render time so callers compare + // the true sign — rounding first turns -0.04% into "-0.0", which tests + // as >= 0 and renders as a positive change. function pctChange(current, prior) { - if (prior > 0) return ((current - prior) / prior * 100).toFixed(1); - return null; + if (!(prior > 0)) return null; + const pct = (current - prior) / prior * 100; + return Number.isFinite(pct) ? pct : null; } - function statCard(value, label, change, tooltip) { + // Shared stat-card markup. Options: + // change: omit for no change row; null renders "N/A vs prior month". + // gridClass: fui-grid-item-* class; omit when the container sizes cards itself. + // tooltip: HTML shown on hover/focus of the label. + // Multi-line labels ('\n') render as line breaks. + function statCard(value, label, { change, gridClass, tooltip } = {}) { + // escapeHtml leaves newlines and quotes alone, so escape once and + // then adapt per context:
for markup, " for the attribute. + const escapedLabel = escapeHtml(label); + const labelHtml = escapedLabel.split('\n').join('
'); + const ariaLabel = escapedLabel.replace(/"/g, '"').replace(/\n/g, ' '); const tooltipHtml = tooltip ? `
${tooltip}
` : ''; + let changeHtml = ''; + if (change !== undefined) { + let changeText = 'N/A'; + if (change !== null) { + const pct = Number(change); + changeText = `${pct >= 0 ? '+' : ''}${pct.toFixed(1)}%`; + } + changeHtml = ` +
+ ${changeText} vs prior month +
+ `; + } return ` -
+
${value}
- ${escapeHtml(label)} + ${labelHtml} ${tooltipHtml}
-
- ${change === null ? 'N/A vs prior month' : `${change >= 0 ? '+' : ''}${change}% vs prior month`} -
+ ${changeHtml}
`; @@ -543,26 +569,26 @@

Filter Selections

const monthName = new Date(year, month - 1).toLocaleString('default', { month: 'long' }); document.getElementById('stats-grid').innerHTML = - statCard( - formatNumber(latest.users), 'Users', - usersChange, - `Total number of unique individuals who visited your site during ${monthName}.` - ) + - statCard( - formatNumber(sessions.current || 0), 'User Sessions', - sessionsChange, - `Total number of visits to your site during ${monthName}. A single user can have multiple sessions.` - ) + - statCard( - formatNumber(latest.pageviews), 'Pageviews', - pageviewsChange, - `Total number of pages viewed during ${monthName}. This includes repeated views of the same page.` - ) + - statCard( - engagementDisplay, 'Engagement Rate', - engagementChange, - `Percentage of sessions during ${monthName} where users actively engaged with your site (e.g., stayed longer, viewed multiple pages, or triggered events). Higher is better. Learn more.` - ); + statCard(formatNumber(latest.users), 'Users', { + change: usersChange, + gridClass: 'fui-grid-item-3', + tooltip: `Total number of unique individuals who visited your site during ${monthName}.`, + }) + + statCard(formatNumber(sessions.current || 0), 'User Sessions', { + change: sessionsChange, + gridClass: 'fui-grid-item-3', + tooltip: `Total number of visits to your site during ${monthName}. A single user can have multiple sessions.`, + }) + + statCard(formatNumber(latest.pageviews), 'Pageviews', { + change: pageviewsChange, + gridClass: 'fui-grid-item-3', + tooltip: `Total number of pages viewed during ${monthName}. This includes repeated views of the same page.`, + }) + + statCard(engagementDisplay, 'Engagement Rate', { + change: engagementChange, + gridClass: 'fui-grid-item-3', + tooltip: `Percentage of sessions during ${monthName} where users actively engaged with your site (e.g., stayed longer, viewed multiple pages, or triggered events). Higher is better. Learn more.`, + }); } function renderEventCounts(config, events, eventCharts) { @@ -588,8 +614,9 @@

Filter Selections

let chartIndex = 0; for (let i = 0; i < countCards.length; i += 2) { const pair = countCards.slice(i, i + 2); - const rowLabel = pair[0].label.split('\n')[0]; - const pairSharesLabel = pair.length > 1 && pair[1].label.split('\n')[0] === rowLabel; + const pairLabelLines = pair.map(card => card.label.split('\n')); + const rowLabel = pairLabelLines[0][0]; + const pairSharesLabel = pair.length > 1 && pairLabelLines[1][0] === rowLabel; if (pairSharesLabel) { html += `

${escapeHtml(rowLabel)}

`; } @@ -599,28 +626,17 @@

Filter Selections

const e = eventsByKey[card.event_key] || {}; const current = e.current || 0; const prior = e.prior || 0; - let change = 0; - if (prior > 0) { - change = ((current - prior) / prior * 100).toFixed(1); - } - html += ` -
-
-
${formatNumber(current)}
-
${card.label.split('\n').map(l => escapeHtml(l)).join('
')}
-
- ${change >= 0 ? '+' : ''}${change}% vs prior month -
-
-
- `; + html += statCard(formatNumber(current), card.label, { + change: pctChange(current, prior) ?? 0, + gridClass: 'fui-grid-item-6', + }); } // Chart cards row - for (const card of pair) { + for (const [j, card] of pair.entries()) { if (chartDataByKey[card.event_key] && chartDataByKey[card.event_key].length > 0) { const canvasId = `event-trend-chart-${chartIndex++}`; - const parts = card.label.split('\n'); + const parts = pairLabelLines[j]; const cardLabel = parts[0]; const chartTitle = escapeHtml(cardLabel) + ' Over Time' + (parts[1] ? ` (${escapeHtml(parts[1].replace(/[()]/g, ''))})` : ''); html += ` @@ -833,7 +849,7 @@

${escapeHtml(event.label)} ${escapeHtml(row.page || '-')} ${formatNumber(row.views || 0)} - + ${formatChange(row.change)} @@ -852,7 +868,7 @@

${escapeHtml(event.label)} ${escapeHtml(row.link || '-')} ${formatNumber(row.clicks || 0)} - + ${formatChange(row.change)} @@ -871,7 +887,7 @@

${escapeHtml(event.label)} ${escapeHtml(row.filterName || '-')} ${escapeHtml(row.filterValue || '-')} ${formatNumber(row.count || 0)} - + ${formatChange(row.change)} @@ -935,14 +951,9 @@

${escapeHtml(event.label)} .concat([['Total', total]]); statsGrid.style.display = ''; statsGrid.style.gridTemplateColumns = `repeat(${statCards.length}, 1fr)`; - statsGrid.innerHTML = statCards.map(([label, count]) => ` -
-
-
${formatNumber(count)}
-
${escapeHtml(label)}
-
-
- `).join(''); + statsGrid.innerHTML = statCards + .map(([label, count]) => statCard(formatNumber(count), label)) + .join(''); } card.style.display = ''; @@ -999,6 +1010,13 @@

${escapeHtml(event.label)} } } + // Positive/negative styling for a signed change. Works for both the + // fractions the tables carry and the percentages the cards carry. + function changeClass(change) { + if (change == null) return ''; + return change >= 0 ? 'positive' : 'negative'; + } + function formatChange(change) { if (change == null) return '-'; return (change >= 0 ? '+' : '') + (change * 100).toFixed(1) + '%';