From 0c3bbd3aa5c7b1a63f476f09d8b586b8561251ef Mon Sep 17 00:00:00 2001 From: Matt McKay Date: Fri, 7 Aug 2026 16:01:17 +1000 Subject: [PATCH] Label the analytics dashboard sessions, because that is what it counts The dashboard's headline number has always been sessions presented as users. The pipeline requests Metric(name='sessions') from GA4 and stored the result in a column named 'users', which surfaced here as 'Monthly Users', 'Active Users by Country' and 'Active Users (per capita) by Country'. Every figure on the page overstated the audience it claimed to measure -- sessions typically run about 1.3-1.6x users on content sites. The pipeline side landed in QuantEcon/website-dynamic#29 and is already published: map_data.json and month_data.json now carry sessions and sessions_per_capita alongside the original users fields, with identical values, so this change can be made without a flag day. See QuantEcon/website-dynamic#9 for the full reasoning. This switches the four field reads to the new names and relabels the charts and navigation. Rendered values are unchanged -- verified against the live published JSON, where sessions equals users for every row. Also adds the cross-property caveat that QuantEcon/website-dynamic#9 asks be written down next to the chart: these are visits rather than people, and sessions are reported precisely because they can be summed across the eight lecture series while user counts cannot. The per-capita chart now reads sessions_per_capita, which is null for countries with no known population rather than 0. Plotly leaves those uncoloured instead of placing them at the bottom of the colour ramp, which is what made Taiwan look like it had no readers. Section anchor ids are left alone so existing deep links keep working. --- pages/analytics_dashboard.html | 65 +++++++++++++++++++++++----------- 1 file changed, 44 insertions(+), 21 deletions(-) diff --git a/pages/analytics_dashboard.html b/pages/analytics_dashboard.html index d63ed91..2f5763f 100644 --- a/pages/analytics_dashboard.html +++ b/pages/analytics_dashboard.html @@ -47,6 +47,22 @@ text-decoration: none; } +.dashboard-note { + max-width: 800px; + margin: 0 auto 3rem auto; + color: #5a6472; + font-size: 0.95rem; + line-height: 1.6; +} + +.dashboard-note p { + margin: 0 0 0.75rem 0; +} + +.dashboard-note p:last-child { + margin-bottom: 0; +} + .chart-section { margin: 3rem 0; position: relative; @@ -82,12 +98,17 @@

Analytics Dashboard

+
+

These charts count sessions, not people. A session is a single visit, and one reader usually makes several over time, so these figures are larger than the size of the audience.

+

The totals aggregate eight QuantEcon lecture series. Sessions can be added across sites; user counts cannot, because a reader who visits three series would be counted three times, and Google Analytics offers no way to deduplicate them. That is why sessions are the figure reported here. Traffic from the Chinese, Persian and French translations is included under the corresponding English series.

+
+
@@ -137,26 +158,26 @@

Analytics Dashboard

}) .then(data => { // data is an array of objects: - // [ { country: "Australia", iso_alpha_3: "AUS", users: 123 }, ... ] + // [ { country: "Australia", iso_alpha_3: "AUS", sessions: 123 }, ... ] const isoCodes = data.map(row => row.iso_alpha_3); - const usersArr = data.map(row => row.users); + const sessionsArr = data.map(row => row.sessions); const hoverText = data.map(row => row.country); const trace = { type: 'choropleth', locations: isoCodes, - z: usersArr, + z: sessionsArr, text: hoverText, - hovertemplate: "Country: %{text}
Users: %{z}", + hovertemplate: "Country: %{text}
Sessions: %{z}", colorscale: 'Portland', colorbar: { - title: 'Users' + title: 'Sessions' } }; const layout = { - title: 'Active Users by Country (last six months)', + title: 'Sessions by Country (last six months)', geo: { showframe: false, showcoastlines: true, @@ -174,7 +195,7 @@

Analytics Dashboard

/** - * 2. Fetch and Render the Monthly Users Line Chart + * 2. Fetch and Render the Monthly Sessions Line Chart */ fetch(`${BASE_DATA_URL}/month_data.json`) .then(response => { @@ -185,26 +206,26 @@

Analytics Dashboard

}) .then(data => { // data is an array of objects: - // [ { month_formatted: "Feb 2024", users: 567 }, ... ] + // [ { month_formatted: "Feb 2024", sessions: 567 }, ... ] const xValues = data.map(row => row.month_formatted); - const yValues = data.map(row => row.users); + const yValues = data.map(row => row.sessions); const trace = { type: 'scatter', mode: 'lines+markers', x: xValues, y: yValues, - hovertemplate: "Month: %{x}
Users: %{y}", + hovertemplate: "Month: %{x}
Sessions: %{y}", line: { shape: 'linear', color: 'teal' }, marker: { size: 6 } }; const layout = { - title: 'Monthly Users', + title: 'Monthly Sessions', xaxis: { title: 'Month' }, yaxis: { - title: 'Number of Users', + title: 'Number of Sessions', range: [0, Math.max(...yValues) * 1.1] }, margin: { l: 50, r: 20, t: 50, b: 50 }, @@ -229,26 +250,28 @@

Analytics Dashboard

}) .then(data => { // data is an array of objects: - // [ { country: "Australia", iso_alpha_3: "AUS", users: 123 }, ... ] + // [ { country: "Australia", iso_alpha_3: "AUS", sessions_per_capita: 0.0000047 }, ... ] + // sessions_per_capita is null where the population is unknown; Plotly leaves + // those countries uncoloured rather than placing them at the bottom of the ramp. const isoCodes = data.map(row => row.iso_alpha_3); - const usersArr = data.map(row => row.users_per_capita); + const perCapitaArr = data.map(row => row.sessions_per_capita); const hoverText = data.map(row => row.country); const trace = { type: 'choropleth', locations: isoCodes, - z: usersArr, + z: perCapitaArr, text: hoverText, - hovertemplate: "Country: %{text}
Users (per capita): %{z}", + hovertemplate: "Country: %{text}
Sessions (per capita): %{z}", colorscale: 'Portland', colorbar: { - title: 'Users (per capita)' + title: 'Sessions (per capita)' } }; const layout = { - title: 'Active Users (per capita) by Country (last six months)', + title: 'Sessions (per capita) by Country (last six months)', geo: { showframe: false, showcoastlines: true,