add period comparison deltas to analytics stat cards
All checks were successful
deploy / deploy (push) Successful in 1m21s

Each stat card now shows the percentage change vs the equivalent
previous period (e.g. 30d compares last 30 days vs 30 days before).
Handles zero-baseline with "new" label and caps extreme deltas at
>999%. Seed data extended to 2 years for meaningful 12m comparisons.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
jamey
2026-02-23 01:01:25 +00:00
parent 08fcd60eb6
commit 6eda1de1bc
5 changed files with 244 additions and 30 deletions

View File

@@ -516,10 +516,83 @@ const AnalyticsInit = {
}
}
// Analytics: chart tooltip on hover/tap
const ChartTooltip = {
mounted() { this._setup() },
updated() { this._setup() },
_setup() {
// Re-query after LiveView patches
this.tooltip = this.el.querySelector("[data-tooltip]")
this.bars = this.el.querySelector("[data-bars]")
if (!this.tooltip || !this.bars) return
// Clean up previous listeners if re-setting up
if (this._cleanup) this._cleanup()
const onMove = (e) => {
const clientX = e.touches ? e.touches[0].clientX : e.clientX
const bar = this._barAt(clientX)
if (bar) this._show(bar, clientX)
}
const onLeave = () => this._hide()
const onDocTap = (e) => {
if (!this.el.contains(e.target)) this._hide()
}
this.bars.addEventListener("mousemove", onMove)
this.bars.addEventListener("mouseleave", onLeave)
this.bars.addEventListener("touchstart", onMove, { passive: true })
document.addEventListener("touchstart", onDocTap, { passive: true })
this._cleanup = () => {
this.bars.removeEventListener("mousemove", onMove)
this.bars.removeEventListener("mouseleave", onLeave)
this.bars.removeEventListener("touchstart", onMove)
document.removeEventListener("touchstart", onDocTap)
}
},
destroyed() {
if (this._cleanup) this._cleanup()
},
_barAt(clientX) {
const children = this.bars.children
for (let i = 0; i < children.length; i++) {
const rect = children[i].getBoundingClientRect()
if (clientX >= rect.left && clientX <= rect.right) return children[i]
}
return null
},
_show(bar, clientX) {
const label = bar.dataset.label
const visitors = bar.dataset.visitors
if (!label) return
this.tooltip.textContent = `${label}: ${visitors} visitor${visitors === "1" ? "" : "s"}`
this.tooltip.style.display = "block"
// Position: centered on cursor, clamped to chart bounds
const chartRect = this.el.getBoundingClientRect()
const tipWidth = this.tooltip.offsetWidth
let left = clientX - chartRect.left - tipWidth / 2
left = Math.max(0, Math.min(left, chartRect.width - tipWidth))
this.tooltip.style.left = left + "px"
},
_hide() {
if (this.tooltip) this.tooltip.style.display = "none"
}
}
const csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
const liveSocket = new LiveSocket("/live", Socket, {
params: {_csrf_token: csrfToken},
hooks: {...colocatedHooks, ColorSync, Lightbox, CartPersist, CartDrawer, ProductImageScroll, SearchModal, CollectionFilters, CardRadioScroll, AnalyticsInit},
hooks: {...colocatedHooks, ColorSync, Lightbox, CartPersist, CartDrawer, ProductImageScroll, SearchModal, CollectionFilters, CardRadioScroll, AnalyticsInit, ChartTooltip},
})
// Show progress bar on live navigation and form submits