From 98f35b3c95ac0a8f4ce092e6d6d59edda017b057 Mon Sep 17 00:00:00 2001 From: Fran McDade <18710366+frano-m@users.noreply.github.com> Date: Tue, 18 Aug 2026 13:16:34 +1000 Subject: [PATCH 1/2] refactor: consolidate stat-card markup in static analytics template (#4923) Co-Authored-By: Claude Fable 5 --- .../analytics/static_site/template/index.html | 101 +++++++++--------- 1 file changed, 50 insertions(+), 51 deletions(-) diff --git a/analytics/analytics_package/analytics/static_site/template/index.html b/analytics/analytics_package/analytics/static_site/template/index.html index 14dde9257..889a9cd03 100644 --- a/analytics/analytics_package/analytics/static_site/template/index.html +++ b/analytics/analytics_package/analytics/static_site/template/index.html @@ -498,25 +498,40 @@

Filter Selections

} 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.toFixed(1) : 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 } = {}) { + const labelHtml = String(label).split('\n').map(l => escapeHtml(l)).join('
'); const tooltipHtml = tooltip ? `
${tooltip}
` : ''; + let changeHtml = ''; + if (change !== undefined) { + const changeClass = change === null ? '' : change >= 0 ? 'positive' : 'negative'; + const changeText = change === null ? 'N/A' : `${change >= 0 ? '+' : ''}${change}%`; + 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 +558,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) { @@ -599,21 +614,10 @@

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 @@ -935,14 +939,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 = ''; From 960ccc006beb43de2cb45ada1fe3c2c896e35f1a Mon Sep 17 00:00:00 2001 From: Fran McDade <18710366+frano-m@users.noreply.github.com> Date: Tue, 18 Aug 2026 14:56:15 +1000 Subject: [PATCH 2/2] fix: derive change sign before rounding and share change styling (#4923) Co-Authored-By: Claude Fable 5 --- .../analytics/static_site/template/index.html | 45 +++++++++++++------ 1 file changed, 32 insertions(+), 13 deletions(-) diff --git a/analytics/analytics_package/analytics/static_site/template/index.html b/analytics/analytics_package/analytics/static_site/template/index.html index 889a9cd03..95f4042ff 100644 --- a/analytics/analytics_package/analytics/static_site/template/index.html +++ b/analytics/analytics_package/analytics/static_site/template/index.html @@ -497,10 +497,14 @@

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 null; const pct = (current - prior) / prior * 100; - return Number.isFinite(pct) ? pct.toFixed(1) : null; + return Number.isFinite(pct) ? pct : null; } // Shared stat-card markup. Options: @@ -509,16 +513,23 @@

Filter Selections

// tooltip: HTML shown on hover/focus of the label. // Multi-line labels ('\n') render as line breaks. function statCard(value, label, { change, gridClass, tooltip } = {}) { - const labelHtml = String(label).split('\n').map(l => escapeHtml(l)).join('
'); + // 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) { - const changeClass = change === null ? '' : change >= 0 ? 'positive' : 'negative'; - const changeText = change === null ? 'N/A' : `${change >= 0 ? '+' : ''}${change}%`; + let changeText = 'N/A'; + if (change !== null) { + const pct = Number(change); + changeText = `${pct >= 0 ? '+' : ''}${pct.toFixed(1)}%`; + } changeHtml = ` -
+
${changeText} vs prior month
`; @@ -528,7 +539,7 @@

Filter Selections

${value}
- ${labelHtml} + ${labelHtml} ${tooltipHtml}
${changeHtml} @@ -603,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)}

`; } @@ -621,10 +633,10 @@

Filter Selections

} // 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 += ` @@ -837,7 +849,7 @@

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

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

${escapeHtml(event.label)} ${escapeHtml(row.filterName || '-')} ${escapeHtml(row.filterValue || '-')} ${formatNumber(row.count || 0)} - + ${formatChange(row.change)} @@ -998,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) + '%';