add period comparison deltas to analytics stat cards
All checks were successful
deploy / deploy (push) Successful in 1m21s
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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user