diff --git a/docs/analytics.md b/docs/analytics.md index 769a83f..48bf793 100644 --- a/docs/analytics.md +++ b/docs/analytics.md @@ -236,10 +236,14 @@ truncated at the current bucket, never drawing fake zeroes for the future); its x labels are weekday names centered at midday UTC, without vertical grid lines (day boundaries would be misleading in the viewer's timezone). The month view labels days the same lineless way — day numbers at noon UTC, -with the month name substituted for the 1st. Year is a rolling 365-day window ending at now, re-bucketed to daily points, -with boundary lines at months/years. All uses the full data reach, but keeps -at least the past 30 days so the chart never collapses to a tiny sliver when -the site is young. Below the charts: a **transition map** (all pages from +with the month name substituted for the 1st. Month, year and all are +rolling windows ending at now, aligned to UTC day boundaries at the start +so the labels span the whole range; the bucket size follows the window — +6 hours up to 31 days, daily beyond — with boundary lines at months/years +on the longer ranges. All uses the full data reach, but keeps +at least the past 30 days (identical to the month view when the site is +younger than that, bucket size included) so the chart never collapses to a +tiny sliver when the site is young. Below the charts: a **transition map** (all pages from `/_api/pages` — top-level menu items on a large-radius circular arc whose bottom point is the last item (each earlier item a bit higher), connected by an unlabeled top lane, each item's diff --git a/frontend/src/TransitionGraph.vue b/frontend/src/TransitionGraph.vue index 51abe69..16c2f1f 100644 --- a/frontend/src/TransitionGraph.vue +++ b/frontend/src/TransitionGraph.vue @@ -7,7 +7,7 @@ * so the visual scale can normalize against a one-week reference. */ import { computed, onBeforeUnmount, onMounted, ref, shallowRef, watch } from 'vue' -import { WEEK } from './analytics/time.js' +import { DAY, WEEK } from './analytics/time.js' import { formatCount } from './analytics/format.js' import { TNODE_W, @@ -26,14 +26,16 @@ const props = defineProps({ const visualScale = computed(() => { const { t0, t1 } = props.window if (t0 != null && t1 != null) return WEEK / (t1 - t0) - // 'all': scale by the actual data span. + // 'all': scale by the actual data span, but never less than the 30-day + // minimum the plot enforces, so sparse young data is not over-amplified. const times = new Set() for (const buckets of Object.values(props.data?.views || {})) { for (const k of Object.keys(buckets)) times.add(Date.parse(k)) } const arr = [...times] if (arr.length < 2) return 1 - return WEEK / (Math.max(...arr) - Math.min(...arr)) + const span = Math.max(...arr) - Math.min(...arr) + return WEEK / Math.max(span, 30 * DAY) }) const graph = computed(() => diff --git a/frontend/src/VisitorCharts.vue b/frontend/src/VisitorCharts.vue index 2c24c12..8fa29a4 100644 --- a/frontend/src/VisitorCharts.vue +++ b/frontend/src/VisitorCharts.vue @@ -4,7 +4,15 @@ */ import { computed, onMounted, onUnmounted, ref } from 'vue' import { makeSeries } from './analytics/time.js' -import { CHART_H, CHART_W, buildChart, fmtY } from './analytics/chart.js' +import { + CHART_H, + CHART_W, + MARGIN_B, + MARGIN_L, + VIEW_H, + VIEW_W, + buildChart, +} from './analytics/chart.js' const DAY_REFRESH_MS = 15000 @@ -48,41 +56,35 @@ const viewChart = computed(() => buildChart(viewSeries.value, now.value)) { ylabel: 'views', chart: viewChart, empty: 'no views recorded yet' }, ]" :key="c.ylabel">