Use last 24h/7d/30d/365d/all analytics data. Previously some fields were unfiltered and weekly view was based on calendar weeks.
This commit is contained in:
@@ -5,7 +5,13 @@
|
||||
// visit/crawler tables. Read-only.
|
||||
// See docs/analytics.md for the data format.
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { RANGES } from './analytics/time.js'
|
||||
import {
|
||||
RANGES,
|
||||
rangeWindow,
|
||||
filterRecordsByRange,
|
||||
filterTransitionsByRange,
|
||||
filterViewsByRange,
|
||||
} from './analytics/time.js'
|
||||
import {
|
||||
calcReadStats,
|
||||
calcTotalViews,
|
||||
@@ -90,8 +96,25 @@ onUnmounted(() => {
|
||||
}
|
||||
})
|
||||
|
||||
const visits = computed(() => data.value?.visits || [])
|
||||
const totalViews = computed(() => calcTotalViews(data.value?.views))
|
||||
const window = computed(() => rangeWindow(range.value))
|
||||
|
||||
// All non-chart stats follow the selected range; the charts keep their own
|
||||
// range-specific x windows (week overlays previous weeks aligned to Monday).
|
||||
const rangeData = computed(() => {
|
||||
if (!data.value) return null
|
||||
const { t0, t1 } = window.value
|
||||
return {
|
||||
...data.value,
|
||||
transitions: filterTransitionsByRange(data.value.transitions, t0, t1),
|
||||
views: filterViewsByRange(data.value.views, t0, t1),
|
||||
visits: filterRecordsByRange(data.value.visits, t0, t1),
|
||||
crawlers: filterRecordsByRange(data.value.crawlers, t0, t1),
|
||||
abuse: filterRecordsByRange(data.value.abuse, t0, t1),
|
||||
}
|
||||
})
|
||||
|
||||
const visits = computed(() => rangeData.value?.visits || [])
|
||||
const totalViews = computed(() => calcTotalViews(rangeData.value?.views))
|
||||
const readStats = computed(() => calcReadStats(visits.value))
|
||||
|
||||
// Keep the URL shareable when the range changes.
|
||||
@@ -103,9 +126,9 @@ watch(range, (r) => {
|
||||
|
||||
const clients = computed(() => data.value?.clients || {})
|
||||
const visitRows = computed(() => formatVisitRows(visits.value, clients.value, pageTree.value, now.value))
|
||||
const crawlers = computed(() => data.value?.crawlers || [])
|
||||
const crawlers = computed(() => rangeData.value?.crawlers || [])
|
||||
const crawlerRows = computed(() => formatCrawlerRows(crawlers.value, clients.value, pageTree.value, now.value))
|
||||
const abuseRows = computed(() => formatAbuseRows(data.value?.abuse || [], clients.value, now.value))
|
||||
const abuseRows = computed(() => formatAbuseRows(rangeData.value?.abuse || [], clients.value, now.value))
|
||||
|
||||
</script>
|
||||
|
||||
@@ -133,7 +156,7 @@ const abuseRows = computed(() => formatAbuseRows(data.value?.abuse || [], client
|
||||
</section>
|
||||
|
||||
<VisitorCharts :data="data" :range="range" />
|
||||
<TransitionGraph :data="data" :range="range" :page-tree="pageTree" />
|
||||
<TransitionGraph :data="rangeData" :window="window" :page-tree="pageTree" />
|
||||
|
||||
<section>
|
||||
<h2>Recent visits</h2>
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<script setup>
|
||||
/**
|
||||
* Radial transition map filtered to the selected time range.
|
||||
* Radial transition map for a pre-filtered time range.
|
||||
*
|
||||
* Transitions are stored per 5-minute bucket (from -> to -> bucket ->
|
||||
* count), so the graph sums the buckets falling inside the selected
|
||||
* range, exactly like the charts and per-page views do.
|
||||
* The parent filters transitions, views and visits to the selected range
|
||||
* before passing them in; `window` carries the absolute [t0, t1) window
|
||||
* so the visual scale can normalize against a one-week reference.
|
||||
*/
|
||||
import { computed, onBeforeUnmount, onMounted, ref, shallowRef, watch } from 'vue'
|
||||
import { rangeWindow, WEEK } from './analytics/time.js'
|
||||
import { WEEK } from './analytics/time.js'
|
||||
import { formatCount } from './analytics/format.js'
|
||||
import {
|
||||
TNODE_W,
|
||||
@@ -15,21 +15,16 @@ import {
|
||||
BEAD_R,
|
||||
BEAD_SPEED,
|
||||
buildTransitionGraph,
|
||||
filterTransitionsByRange,
|
||||
filterViewsByRange,
|
||||
filterVisitsByRange,
|
||||
} from './analytics/transitions.js'
|
||||
|
||||
const props = defineProps({
|
||||
data: { type: Object, default: null },
|
||||
range: { type: String, required: true },
|
||||
window: { type: Object, required: true },
|
||||
pageTree: { type: Array, default: null },
|
||||
})
|
||||
|
||||
const window = computed(() => rangeWindow(props.range))
|
||||
|
||||
const visualScale = computed(() => {
|
||||
const { t0, t1 } = window.value
|
||||
const { t0, t1 } = props.window
|
||||
if (t0 != null && t1 != null) return WEEK / (t1 - t0)
|
||||
// 'all': scale by the actual data span.
|
||||
const times = new Set()
|
||||
@@ -41,22 +36,9 @@ const visualScale = computed(() => {
|
||||
return WEEK / (Math.max(...arr) - Math.min(...arr))
|
||||
})
|
||||
|
||||
const filteredData = computed(() => {
|
||||
if (!props.data) return null
|
||||
const { t0, t1 } = window.value
|
||||
return {
|
||||
transitions: filterTransitionsByRange(props.data.transitions, t0, t1),
|
||||
views: filterViewsByRange(props.data.views, t0, t1),
|
||||
}
|
||||
})
|
||||
|
||||
const filteredVisits = computed(() =>
|
||||
filterVisitsByRange(props.data?.visits, window.value.t0, window.value.t1),
|
||||
)
|
||||
|
||||
const graph = computed(() =>
|
||||
filteredData.value
|
||||
? buildTransitionGraph(filteredData.value, props.pageTree, filteredVisits.value, visualScale.value)
|
||||
props.data
|
||||
? buildTransitionGraph(props.data, props.pageTree, props.data.visits || [], visualScale.value)
|
||||
: null,
|
||||
)
|
||||
|
||||
|
||||
@@ -152,14 +152,16 @@ export function makeSeries(buckets, rangeKey) {
|
||||
|
||||
/**
|
||||
* Absolute UTC time window for a given range key. Used to filter visits,
|
||||
* transitions and views to the same period the charts are showing.
|
||||
* transitions and views for the non-chart stats on the analytics page.
|
||||
* The week range is a rolling 7 days ending at now; the charts instead
|
||||
* align to Monday 00:00 UTC and overlay previous weeks, so their x window
|
||||
* differs from the stats range on purpose.
|
||||
* Returns { t0, t1 } where null means unbounded.
|
||||
*/
|
||||
export function rangeWindow(rangeKey) {
|
||||
const now = Date.now()
|
||||
if (rangeKey === 'week') {
|
||||
const start = mondayUTC(now)
|
||||
return { t0: start, t1: start + WEEK }
|
||||
return { t0: now - WEEK, t1: now }
|
||||
}
|
||||
if (rangeKey === 'all') {
|
||||
return { t0: null, t1: null }
|
||||
@@ -168,3 +170,49 @@ export function rangeWindow(rangeKey) {
|
||||
const t1 = Math.floor(now / bucket) * bucket + bucket
|
||||
return { t0: t1 - span, t1 }
|
||||
}
|
||||
|
||||
/**
|
||||
* Sum the bucketed transition matrix (from -> to -> bucket ISO -> count)
|
||||
* into a plain from -> to -> count matrix for the window [t0, t1).
|
||||
*/
|
||||
export function filterTransitionsByRange(transitions, t0, t1) {
|
||||
const out = {}
|
||||
for (const [fr, tos] of Object.entries(transitions || {})) {
|
||||
for (const [to, buckets] of Object.entries(tos)) {
|
||||
let n = 0
|
||||
for (const [k, c] of Object.entries(buckets)) {
|
||||
const t = Date.parse(k)
|
||||
if ((t0 == null || t >= t0) && (t1 == null || t < t1)) n += c
|
||||
}
|
||||
if (n) {
|
||||
out[fr] = out[fr] || {}
|
||||
out[fr][to] = n
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
/** Keep only the 5-minute view buckets that fall inside [t0, t1). */
|
||||
export function filterViewsByRange(views, t0, t1) {
|
||||
const filtered = {}
|
||||
for (const [path, buckets] of Object.entries(views || {})) {
|
||||
const out = {}
|
||||
for (const [k, c] of Object.entries(buckets)) {
|
||||
const t = Date.parse(k)
|
||||
if ((t0 == null || t >= t0) && (t1 == null || t < t1)) out[k] = c
|
||||
}
|
||||
if (Object.keys(out).length) filtered[path] = out
|
||||
}
|
||||
return filtered
|
||||
}
|
||||
|
||||
/** Keep only records whose start time falls inside [t0, t1). */
|
||||
export function filterRecordsByRange(records, t0, t1) {
|
||||
const out = []
|
||||
for (const r of records || []) {
|
||||
const t = Date.parse(r.start)
|
||||
if ((t0 == null || t >= t0) && (t1 == null || t < t1)) out.push(r)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -678,52 +678,6 @@ function buildInternalEdges(pairs, byPath, visualScale = 1) {
|
||||
return { edges, flows }
|
||||
}
|
||||
|
||||
/**
|
||||
* Sum the bucketed transition matrix (from -> to -> bucket ISO -> count)
|
||||
* into a plain from -> to -> count matrix for the window [t0, t1).
|
||||
*/
|
||||
export function filterTransitionsByRange(transitions, t0, t1) {
|
||||
const out = {}
|
||||
for (const [fr, tos] of Object.entries(transitions || {})) {
|
||||
for (const [to, buckets] of Object.entries(tos)) {
|
||||
let n = 0
|
||||
for (const [k, c] of Object.entries(buckets)) {
|
||||
const t = Date.parse(k)
|
||||
if ((t0 == null || t >= t0) && (t1 == null || t < t1)) n += c
|
||||
}
|
||||
if (n) {
|
||||
out[fr] = out[fr] || {}
|
||||
out[fr][to] = n
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
/** Keep only the 5-minute view buckets that fall inside [t0, t1). */
|
||||
export function filterViewsByRange(views, t0, t1) {
|
||||
const filtered = {}
|
||||
for (const [path, buckets] of Object.entries(views || {})) {
|
||||
const out = {}
|
||||
for (const [k, c] of Object.entries(buckets)) {
|
||||
const t = Date.parse(k)
|
||||
if ((t0 == null || t >= t0) && (t1 == null || t < t1)) out[k] = c
|
||||
}
|
||||
if (Object.keys(out).length) filtered[path] = out
|
||||
}
|
||||
return filtered
|
||||
}
|
||||
|
||||
/** Keep only visits whose start time falls inside [t0, t1). */
|
||||
export function filterVisitsByRange(visits, t0, t1) {
|
||||
const out = []
|
||||
for (const v of visits || []) {
|
||||
const t = Date.parse(v.start)
|
||||
if ((t0 == null || t >= t0) && (t1 == null || t < t1)) out.push(v)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
const UTM_PRIORITY = ['utm_campaign', 'utm_source']
|
||||
const UTM_FALLBACK = ['utm_medium', 'utm_content', 'utm_term', 'utm_id']
|
||||
|
||||
|
||||
Reference in New Issue
Block a user