Fix week overlay alignment, in-plot ISO week legend, shorter charts
- weeklySeries shifts overlaid weeks onto the current week's time axis so they overlay inside the plot instead of overflowing left; oldest weeks paint first, current week on top - Legend moved inside the visits chart's top right: current ISO week in accent, past weeks as a single muted "Week M" / "Week M–N" specimen - Past week curves use the muted color instead of faded accent - Chart height reduced ~30% (180 -> 126)
This commit is contained in:
+6
-2
@@ -231,9 +231,13 @@ labeled intervals, minor lines at fifths when integral; the minimum y-axis
|
||||
range is 10 so tiny values such as a single visit are not stretched to a
|
||||
fractional scale).
|
||||
The week range is aligned to Monday 00:00 UTC and overlays up to 8 previous
|
||||
weeks in the same accent color at decreasing opacity (the current week is
|
||||
weeks in the muted color at decreasing opacity (the current week keeps the
|
||||
accent color and is
|
||||
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
|
||||
a compact legend inside the top right of the visits chart marks the current
|
||||
ISO week in accent and the overlaid past weeks as "Week M" or "Week M–N" on
|
||||
a muted specimen. 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. Month, year and all are
|
||||
|
||||
@@ -37,6 +37,12 @@ function freqLabel(unit) {
|
||||
return unit === '5min' ? '5 min' : unit === 'hour' ? 'hourly' : 'daily'
|
||||
}
|
||||
|
||||
/** Legend label for the overlaid past weeks: "Week M" or "Week M–N". */
|
||||
function pastLabel(series) {
|
||||
const oldest = series.at(-1).label.slice(5) // strip "Week "
|
||||
return series.length > 2 ? `Week ${oldest}–${series[1].label.slice(5)}` : `Week ${oldest}`
|
||||
}
|
||||
|
||||
const now = ref(Date.now())
|
||||
let refreshInterval = null
|
||||
onMounted(() => {
|
||||
@@ -52,8 +58,8 @@ const viewChart = computed(() => buildChart(viewSeries.value, now.value))
|
||||
|
||||
<template>
|
||||
<section v-for="c in [
|
||||
{ ylabel: 'visits', chart: visitChart, empty: 'no visits recorded yet' },
|
||||
{ ylabel: 'views', chart: viewChart, empty: 'no views recorded yet' },
|
||||
{ ylabel: 'visits', chart: visitChart, legend: true, empty: 'no visits recorded yet' },
|
||||
{ ylabel: 'views', chart: viewChart, legend: false, empty: 'no views recorded yet' },
|
||||
]" :key="c.ylabel">
|
||||
<template v-if="c.chart">
|
||||
<svg class="chart" :viewBox="`${-MARGIN_L} 0 ${VIEW_W} ${VIEW_H}`"
|
||||
@@ -70,9 +76,11 @@ const viewChart = computed(() => buildChart(viewSeries.value, now.value))
|
||||
<path :d="c.chart.skyline" class="line" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<template v-for="(s, i) in c.chart.series" :key="i">
|
||||
<!-- Oldest overlay weeks first so the current week paints on top. -->
|
||||
<template v-for="(s, i) in [...c.chart.series].reverse()" :key="i">
|
||||
<path v-if="s.area" :d="s.area" class="area" />
|
||||
<path :d="s.line" class="line" :style="{ opacity: s.opacity }" />
|
||||
<path :d="s.line" class="line" :class="{ past: s.past }"
|
||||
:style="{ opacity: s.opacity }" />
|
||||
</template>
|
||||
</template>
|
||||
<line :x1="0" :x2="CHART_W" :y1="CHART_H - 0.5" :y2="CHART_H - 0.5"
|
||||
@@ -84,12 +92,18 @@ const viewChart = computed(() => buildChart(viewSeries.value, now.value))
|
||||
class="yaxis-label">{{ freqLabel(c.chart.unit) }} {{ c.ylabel }}</text>
|
||||
<text v-for="t in c.chart.xticks" :key="'x' + t.x" :x="t.x" :y="CHART_H + MARGIN_B - 8"
|
||||
text-anchor="middle" class="xlab">{{ t.label }}</text>
|
||||
<!-- Week overlay legend, top right inside the plot: current week in
|
||||
accent, one muted specimen for the whole past range. -->
|
||||
<g v-if="c.legend && c.chart.series.length > 1">
|
||||
<line :x1="CHART_W - 86" :x2="CHART_W - 66" y1="10" y2="10" class="line" />
|
||||
<text :x="CHART_W - 60" y="10" dominant-baseline="middle"
|
||||
class="leglab">{{ c.chart.series[0].label }}</text>
|
||||
<line :x1="CHART_W - 86" :x2="CHART_W - 66" y1="23" y2="23"
|
||||
class="line past" style="opacity: 0.6" />
|
||||
<text :x="CHART_W - 60" y="23" dominant-baseline="middle"
|
||||
class="leglab">{{ pastLabel(c.chart.series) }}</text>
|
||||
</g>
|
||||
</svg>
|
||||
<div v-if="c.chart.series && c.chart.series.length > 1" class="legend">
|
||||
<span v-for="(s, i) in c.chart.series" :key="i" :style="{ opacity: s.opacity }">
|
||||
● {{ s.label }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
<p v-else class="empty">{{ c.empty }}</p>
|
||||
</section>
|
||||
@@ -115,6 +129,11 @@ const viewChart = computed(() => buildChart(viewSeries.value, now.value))
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.chart .leglab {
|
||||
font-size: 10px;
|
||||
fill: var(--muted);
|
||||
}
|
||||
|
||||
.chart .minor {
|
||||
stroke: var(--line);
|
||||
stroke-width: 1;
|
||||
@@ -159,16 +178,11 @@ const viewChart = computed(() => buildChart(viewSeries.value, now.value))
|
||||
stroke-linecap: round;
|
||||
}
|
||||
|
||||
.legend {
|
||||
display: flex;
|
||||
gap: 1.2rem;
|
||||
margin-top: 0.4rem;
|
||||
font-size: 0.75rem;
|
||||
color: var(--muted);
|
||||
/* Past overlay weeks contrast with the current week's accent color. */
|
||||
.chart .line.past {
|
||||
stroke: var(--muted);
|
||||
}
|
||||
|
||||
.legend span { color: var(--accent); }
|
||||
|
||||
section { margin-top: 1.8rem; }
|
||||
|
||||
.empty { color: var(--muted); }
|
||||
|
||||
@@ -10,7 +10,7 @@ import { DAY, HOUR, MIN5, WEEK, mondayUTC } from './time.js'
|
||||
import { formatCount } from './format.js'
|
||||
|
||||
export const CHART_W = 720
|
||||
export const CHART_H = 180
|
||||
export const CHART_H = 126
|
||||
export const PAD_TOP = 14 // room above the highest point
|
||||
export const MARGIN_L = 56 // y tick labels + vertical axis label
|
||||
export const MARGIN_B = 24 // x tick labels
|
||||
|
||||
@@ -26,6 +26,15 @@ export function mondayUTC(t) {
|
||||
return (d - ((d + 3) % 7)) * DAY
|
||||
}
|
||||
|
||||
/** ISO 8601 week number of the week containing t (via its Thursday). */
|
||||
export function isoWeek(t) {
|
||||
const d = new Date(t)
|
||||
d.setUTCHours(0, 0, 0, 0)
|
||||
d.setUTCDate(d.getUTCDate() + 4 - (d.getUTCDay() || 7))
|
||||
const yearStart = Date.UTC(d.getUTCFullYear(), 0, 1)
|
||||
return Math.ceil(((d - yearStart) / DAY + 1) / 7)
|
||||
}
|
||||
|
||||
/** Parse sparse timestamp buckets into a { epochMs: count } map. */
|
||||
export function rawTimes(buckets) {
|
||||
const raw = {}
|
||||
@@ -44,7 +53,9 @@ export function sumRange(raw, t0, t1) {
|
||||
/**
|
||||
* One series per overlaid week: [this week, 1 week ago, ...], at native
|
||||
* 5-minute resolution, up to 8 weeks back (and only weeks that overlap the
|
||||
* recorded data at all). The current week is truncated at the current bucket
|
||||
* recorded data at all). Each older week's timestamps are shifted forward
|
||||
* onto the current week's axis so all curves overlay inside the plot.
|
||||
* The current week is truncated at the current bucket
|
||||
* — no fake zeroes drawn for the future. Counts are rates per hour
|
||||
* (bucket count * 12): a lone visit in a 5-minute bucket reads as "12/h".
|
||||
* The coarser ranges use per-day rates instead (unitMinutes = 24*60).
|
||||
@@ -67,12 +78,13 @@ export function weeklySeries(buckets) {
|
||||
: start + WEEK
|
||||
const points = []
|
||||
for (let t = start; t < end; t += MIN5) {
|
||||
points.push({ t, count: raw[t] || 0 })
|
||||
points.push({ t: t + back * WEEK, count: raw[t] || 0 })
|
||||
}
|
||||
out.push({
|
||||
points,
|
||||
label: back === 0 ? 'this week' : `${back}w ago`,
|
||||
label: `Week ${isoWeek(start)}`,
|
||||
opacity: Math.max(0.15, 1 - back * 0.25),
|
||||
past: back > 0,
|
||||
area: back === 0,
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user