Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 98 additions & 21 deletions analytics/analytics_package/analytics/static_site/template/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -354,9 +354,14 @@ <h3 class="fui-card-header-title fui-heading-xsmall">Filter Selections</h3>
</main>

<script>
// Sets the page's Chart.js defaults.
// The --fui-font-family stack (analytics.css) without Inter.
const FALLBACK_FONT_FAMILY = "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif";
const INTER_STYLESHEET = document.querySelector('link[href^="https://fonts.googleapis.com/css2"]');

// Sets the page's Chart.js defaults. Charts draw in the fallback font
// until useInterInCharts switches them to Inter.
function applyChartDefaults() {
Chart.defaults.font.family = "'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif";
Chart.defaults.font.family = FALLBACK_FONT_FAMILY;
Chart.defaults.color = cssVar('--fui-ink-light');
// Match the .fui-tooltip style (see the analytics.css usage notes).
Object.assign(Chart.defaults.plugins.tooltip, {
Expand All @@ -367,6 +372,32 @@ <h3 class="fui-card-header-title fui-heading-xsmall">Filter Selections</h3>
});
}

// True once the Inter stylesheet is active (its onload sets media to
// "all") and the face is loaded. The media check matters: with no
// Inter @font-face defined yet, fonts.check also returns true.
function isInterLoaded() {
return INTER_STYLESHEET.media === 'all' && document.fonts.check('400 12px Inter');
}

// Resolves once the Inter stylesheet is active. The onload attribute
// runs before this load listener, so media is "all" by then. Never
// settles if the stylesheet fails; the charts keep the fallback font.
function interStylesheetLoaded() {
return new Promise((resolve) => {
if (INTER_STYLESHEET.media === 'all') return resolve();
INTER_STYLESHEET.addEventListener('load', resolve);
});
}

// Switches the charts to the page's font, Inter, once it has loaded.
// The new font family string also gives Chart.js fresh label
// measurements; with an unchanged string, update() would reuse the
// fallback font's widths.
function useInterInCharts() {
Chart.defaults.font.family = cssVar('--fui-font-family');
Object.values(Chart.instances).forEach((chart) => chart.update('none'));
}

// Reads a theme color from analytics.css, or the page's build-time overrides.
function cssVar(name) {
return getComputedStyle(document.documentElement).getPropertyValue(name).trim();
Expand Down Expand Up @@ -506,8 +537,6 @@ <h3 class="fui-card-header-title fui-heading-xsmall">Filter Selections</h3>
renderMeta(meta);
const chronological = [...traffic].reverse();
renderStats(traffic, config, events, meta);
renderTrafficChart(chronological);
renderPageviewsChart(chronological);
renderPagesTable(pageviews);
renderOutboundTable(outbound);
renderFiltersTable(filters);
Expand All @@ -521,7 +550,9 @@ <h3 class="fui-card-header-title fui-heading-xsmall">Filter Selections</h3>
}
renderSearchQueries(searchQueries);
renderFileDownloadEvents(fileDownloadEvents);
renderEventCounts(config, events, eventCharts);
const chartDataByKey = eventChartDataByKey(eventCharts);
renderEventCounts(config, events, chartDataByKey);
resolveChartData({ chronological, chartDataByKey });
await renderEventStats(events, config);
} catch (error) {
console.error('Error loading data:', error);
Expand Down Expand Up @@ -659,15 +690,8 @@ <h3 class="fui-card-header-title fui-heading-xsmall">Filter Selections</h3>
});
}

function renderEventCounts(config, events, eventCharts) {
const grid = document.getElementById('event-counts-grid');
const countCards = config.event_counts;
if (!countCards || countCards.length === 0 || !events) return;

const eventsByKey = {};
for (const e of events) eventsByKey[e.event_name] = e;

// Build lookup from event_key to monthly data from eventCharts
// Maps each event_key to its monthly data from event_charts.json.
function eventChartDataByKey(eventCharts) {
const chartDataByKey = {};
if (eventCharts && eventCharts.charts) {
for (const chart of eventCharts.charts) {
Expand All @@ -676,6 +700,18 @@ <h3 class="fui-card-header-title fui-heading-xsmall">Filter Selections</h3>
}
}
}
return chartDataByKey;
}

// Renders the key metric cards, with an empty canvas for each trend
// chart that renderEventTrendCharts draws once Chart.js has loaded.
function renderEventCounts(config, events, chartDataByKey) {
const grid = document.getElementById('event-counts-grid');
const countCards = config.event_counts;
if (!countCards || countCards.length === 0 || !events) return;

const eventsByKey = {};
for (const e of events) eventsByKey[e.event_name] = e;

// Group cards into pairs by row label, then emit: heading, stats row, charts row
let html = '';
Expand Down Expand Up @@ -728,11 +764,13 @@ <h3 class="fui-card-header-title fui-heading-xsmall">${chartTitle}</h3>
grid.innerHTML = html;
document.getElementById('key-metrics-heading').style.display = '';
document.getElementById('key-metric-detail-heading').style.display = '';
}

// Render Chart.js instances for each trend chart
grid.querySelectorAll('canvas[data-event-key]').forEach(canvas => {
// Draws the trend charts into the canvases renderEventCounts rendered.
function renderEventTrendCharts(chartDataByKey) {
document.querySelectorAll('#event-counts-grid canvas[data-event-key]').forEach(canvas => {
const key = canvas.dataset.eventKey;
const data = chartDataByKey[key] || [];
const data = chartDataByKey[key];
const months = data.map(d => d.month);
const counts = data.map(d => d.count);

Expand Down Expand Up @@ -1113,11 +1151,50 @@ <h3 class="fui-card-header-title fui-heading-xsmall">${escapeHtml(event.label)}

// Chart.js loads with defer so it doesn't block first paint, which
// means it runs after this script. DOMContentLoaded fires once
// deferred scripts have run.
document.addEventListener('DOMContentLoaded', () => {
applyChartDefaults();
loadData();
// deferred scripts have run. Rejects if the Chart.js script failed to
// load: the charts are left out and the rest of the page renders.
const chartJsReady = new Promise((resolve, reject) => {
document.addEventListener('DOMContentLoaded', () => {
if (typeof Chart === 'undefined') {
reject(new Error('Chart.js failed to load, so the charts are not drawn'));
return;
}
applyChartDefaults();
// Canvas text doesn't swap fonts by itself. If Inter is already
// here (say, cached), the charts start in it; otherwise they
// draw in the fallback font and redraw once Inter loads.
if (isInterLoaded()) {
useInterInCharts();
} else {
interStylesheetLoaded()
.then(() => document.fonts.load('400 12px Inter'))
// On rejection the font file failed to load (a network
// failure, already in the console): the charts keep
// the fallback font, like the page's text.
.then(useInterInCharts, () => {});
}
resolve();
});
});

// Resolved by loadData with the charts' data. Never settles if the
// data fails to load, which loadData reports on the page.
let resolveChartData;
const chartDataReady = new Promise((resolve) => { resolveChartData = resolve; });

// Draws the charts once Chart.js and their data are both ready. Kept
// out of loadData so a Chart.js failure leaves only the charts
// missing, and attached now so that failure is reported once.
chartJsReady
.then(() => chartDataReady)
.then(({ chronological, chartDataByKey }) => {
renderTrafficChart(chronological);
renderPageviewsChart(chronological);
renderEventTrendCharts(chartDataByKey);
});

// Starts the data fetches now, alongside the Chart.js download.
loadData();
</script>
</body>
</html>
2 changes: 1 addition & 1 deletion analytics/analytics_package/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "analytics"
version = "5.1.3"
version = "5.1.4"
requires-python = ">=3.12"
dependencies = [
"pandas>=3,<4",
Expand Down
2 changes: 1 addition & 1 deletion analytics/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

119 changes: 98 additions & 21 deletions gh-pages/anvil-explorer/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -355,9 +355,14 @@ <h3 class="fui-card-header-title fui-heading-xsmall">Filter Selections</h3>
</main>

<script>
// Sets the page's Chart.js defaults.
// The --fui-font-family stack (analytics.css) without Inter.
const FALLBACK_FONT_FAMILY = "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif";
const INTER_STYLESHEET = document.querySelector('link[href^="https://fonts.googleapis.com/css2"]');

// Sets the page's Chart.js defaults. Charts draw in the fallback font
// until useInterInCharts switches them to Inter.
function applyChartDefaults() {
Chart.defaults.font.family = "'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif";
Chart.defaults.font.family = FALLBACK_FONT_FAMILY;
Chart.defaults.color = cssVar('--fui-ink-light');
// Match the .fui-tooltip style (see the analytics.css usage notes).
Object.assign(Chart.defaults.plugins.tooltip, {
Expand All @@ -368,6 +373,32 @@ <h3 class="fui-card-header-title fui-heading-xsmall">Filter Selections</h3>
});
}

// True once the Inter stylesheet is active (its onload sets media to
// "all") and the face is loaded. The media check matters: with no
// Inter @font-face defined yet, fonts.check also returns true.
function isInterLoaded() {
return INTER_STYLESHEET.media === 'all' && document.fonts.check('400 12px Inter');
}

// Resolves once the Inter stylesheet is active. The onload attribute
// runs before this load listener, so media is "all" by then. Never
// settles if the stylesheet fails; the charts keep the fallback font.
function interStylesheetLoaded() {
return new Promise((resolve) => {
if (INTER_STYLESHEET.media === 'all') return resolve();
INTER_STYLESHEET.addEventListener('load', resolve);
});
}

// Switches the charts to the page's font, Inter, once it has loaded.
// The new font family string also gives Chart.js fresh label
// measurements; with an unchanged string, update() would reuse the
// fallback font's widths.
function useInterInCharts() {
Chart.defaults.font.family = cssVar('--fui-font-family');
Object.values(Chart.instances).forEach((chart) => chart.update('none'));
}

// Reads a theme color from analytics.css, or the page's build-time overrides.
function cssVar(name) {
return getComputedStyle(document.documentElement).getPropertyValue(name).trim();
Expand Down Expand Up @@ -507,8 +538,6 @@ <h3 class="fui-card-header-title fui-heading-xsmall">Filter Selections</h3>
renderMeta(meta);
const chronological = [...traffic].reverse();
renderStats(traffic, config, events, meta);
renderTrafficChart(chronological);
renderPageviewsChart(chronological);
renderPagesTable(pageviews);
renderOutboundTable(outbound);
renderFiltersTable(filters);
Expand All @@ -522,7 +551,9 @@ <h3 class="fui-card-header-title fui-heading-xsmall">Filter Selections</h3>
}
renderSearchQueries(searchQueries);
renderFileDownloadEvents(fileDownloadEvents);
renderEventCounts(config, events, eventCharts);
const chartDataByKey = eventChartDataByKey(eventCharts);
renderEventCounts(config, events, chartDataByKey);
resolveChartData({ chronological, chartDataByKey });
await renderEventStats(events, config);
} catch (error) {
console.error('Error loading data:', error);
Expand Down Expand Up @@ -660,15 +691,8 @@ <h3 class="fui-card-header-title fui-heading-xsmall">Filter Selections</h3>
});
}

function renderEventCounts(config, events, eventCharts) {
const grid = document.getElementById('event-counts-grid');
const countCards = config.event_counts;
if (!countCards || countCards.length === 0 || !events) return;

const eventsByKey = {};
for (const e of events) eventsByKey[e.event_name] = e;

// Build lookup from event_key to monthly data from eventCharts
// Maps each event_key to its monthly data from event_charts.json.
function eventChartDataByKey(eventCharts) {
const chartDataByKey = {};
if (eventCharts && eventCharts.charts) {
for (const chart of eventCharts.charts) {
Expand All @@ -677,6 +701,18 @@ <h3 class="fui-card-header-title fui-heading-xsmall">Filter Selections</h3>
}
}
}
return chartDataByKey;
}

// Renders the key metric cards, with an empty canvas for each trend
// chart that renderEventTrendCharts draws once Chart.js has loaded.
function renderEventCounts(config, events, chartDataByKey) {
const grid = document.getElementById('event-counts-grid');
const countCards = config.event_counts;
if (!countCards || countCards.length === 0 || !events) return;

const eventsByKey = {};
for (const e of events) eventsByKey[e.event_name] = e;

// Group cards into pairs by row label, then emit: heading, stats row, charts row
let html = '';
Expand Down Expand Up @@ -729,11 +765,13 @@ <h3 class="fui-card-header-title fui-heading-xsmall">${chartTitle}</h3>
grid.innerHTML = html;
document.getElementById('key-metrics-heading').style.display = '';
document.getElementById('key-metric-detail-heading').style.display = '';
}

// Render Chart.js instances for each trend chart
grid.querySelectorAll('canvas[data-event-key]').forEach(canvas => {
// Draws the trend charts into the canvases renderEventCounts rendered.
function renderEventTrendCharts(chartDataByKey) {
document.querySelectorAll('#event-counts-grid canvas[data-event-key]').forEach(canvas => {
const key = canvas.dataset.eventKey;
const data = chartDataByKey[key] || [];
const data = chartDataByKey[key];
const months = data.map(d => d.month);
const counts = data.map(d => d.count);

Expand Down Expand Up @@ -1114,11 +1152,50 @@ <h3 class="fui-card-header-title fui-heading-xsmall">${escapeHtml(event.label)}

// Chart.js loads with defer so it doesn't block first paint, which
// means it runs after this script. DOMContentLoaded fires once
// deferred scripts have run.
document.addEventListener('DOMContentLoaded', () => {
applyChartDefaults();
loadData();
// deferred scripts have run. Rejects if the Chart.js script failed to
// load: the charts are left out and the rest of the page renders.
const chartJsReady = new Promise((resolve, reject) => {
document.addEventListener('DOMContentLoaded', () => {
if (typeof Chart === 'undefined') {
reject(new Error('Chart.js failed to load, so the charts are not drawn'));
return;
}
applyChartDefaults();
// Canvas text doesn't swap fonts by itself. If Inter is already
// here (say, cached), the charts start in it; otherwise they
// draw in the fallback font and redraw once Inter loads.
if (isInterLoaded()) {
useInterInCharts();
} else {
interStylesheetLoaded()
.then(() => document.fonts.load('400 12px Inter'))
// On rejection the font file failed to load (a network
// failure, already in the console): the charts keep
// the fallback font, like the page's text.
.then(useInterInCharts, () => {});
}
resolve();
});
});

// Resolved by loadData with the charts' data. Never settles if the
// data fails to load, which loadData reports on the page.
let resolveChartData;
const chartDataReady = new Promise((resolve) => { resolveChartData = resolve; });

// Draws the charts once Chart.js and their data are both ready. Kept
// out of loadData so a Chart.js failure leaves only the charts
// missing, and attached now so that failure is reported once.
chartJsReady
.then(() => chartDataReady)
.then(({ chronological, chartDataByKey }) => {
renderTrafficChart(chronological);
renderPageviewsChart(chronological);
renderEventTrendCharts(chartDataByKey);
});

// Starts the data fetches now, alongside the Chart.js download.
loadData();
</script>
</body>
</html>
Loading
Loading