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
|
range is 10 so tiny values such as a single visit are not stretched to a
|
||||||
fractional scale).
|
fractional scale).
|
||||||
The week range is aligned to Monday 00:00 UTC and overlays up to 8 previous
|
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);
|
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
|
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,
|
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
|
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'
|
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())
|
const now = ref(Date.now())
|
||||||
let refreshInterval = null
|
let refreshInterval = null
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
@@ -52,8 +58,8 @@ const viewChart = computed(() => buildChart(viewSeries.value, now.value))
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section v-for="c in [
|
<section v-for="c in [
|
||||||
{ ylabel: 'visits', chart: visitChart, empty: 'no visits recorded yet' },
|
{ ylabel: 'visits', chart: visitChart, legend: true, empty: 'no visits recorded yet' },
|
||||||
{ ylabel: 'views', chart: viewChart, empty: 'no views recorded yet' },
|
{ ylabel: 'views', chart: viewChart, legend: false, empty: 'no views recorded yet' },
|
||||||
]" :key="c.ylabel">
|
]" :key="c.ylabel">
|
||||||
<template v-if="c.chart">
|
<template v-if="c.chart">
|
||||||
<svg class="chart" :viewBox="`${-MARGIN_L} 0 ${VIEW_W} ${VIEW_H}`"
|
<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" />
|
<path :d="c.chart.skyline" class="line" />
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<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 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>
|
||||||
</template>
|
</template>
|
||||||
<line :x1="0" :x2="CHART_W" :y1="CHART_H - 0.5" :y2="CHART_H - 0.5"
|
<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>
|
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 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>
|
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>
|
</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>
|
</template>
|
||||||
<p v-else class="empty">{{ c.empty }}</p>
|
<p v-else class="empty">{{ c.empty }}</p>
|
||||||
</section>
|
</section>
|
||||||
@@ -115,6 +129,11 @@ const viewChart = computed(() => buildChart(viewSeries.value, now.value))
|
|||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.chart .leglab {
|
||||||
|
font-size: 10px;
|
||||||
|
fill: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
.chart .minor {
|
.chart .minor {
|
||||||
stroke: var(--line);
|
stroke: var(--line);
|
||||||
stroke-width: 1;
|
stroke-width: 1;
|
||||||
@@ -159,16 +178,11 @@ const viewChart = computed(() => buildChart(viewSeries.value, now.value))
|
|||||||
stroke-linecap: round;
|
stroke-linecap: round;
|
||||||
}
|
}
|
||||||
|
|
||||||
.legend {
|
/* Past overlay weeks contrast with the current week's accent color. */
|
||||||
display: flex;
|
.chart .line.past {
|
||||||
gap: 1.2rem;
|
stroke: var(--muted);
|
||||||
margin-top: 0.4rem;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
color: var(--muted);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.legend span { color: var(--accent); }
|
|
||||||
|
|
||||||
section { margin-top: 1.8rem; }
|
section { margin-top: 1.8rem; }
|
||||||
|
|
||||||
.empty { color: var(--muted); }
|
.empty { color: var(--muted); }
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { DAY, HOUR, MIN5, WEEK, mondayUTC } from './time.js'
|
|||||||
import { formatCount } from './format.js'
|
import { formatCount } from './format.js'
|
||||||
|
|
||||||
export const CHART_W = 720
|
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 PAD_TOP = 14 // room above the highest point
|
||||||
export const MARGIN_L = 56 // y tick labels + vertical axis label
|
export const MARGIN_L = 56 // y tick labels + vertical axis label
|
||||||
export const MARGIN_B = 24 // x tick labels
|
export const MARGIN_B = 24 // x tick labels
|
||||||
|
|||||||
@@ -26,6 +26,15 @@ export function mondayUTC(t) {
|
|||||||
return (d - ((d + 3) % 7)) * DAY
|
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. */
|
/** Parse sparse timestamp buckets into a { epochMs: count } map. */
|
||||||
export function rawTimes(buckets) {
|
export function rawTimes(buckets) {
|
||||||
const raw = {}
|
const raw = {}
|
||||||
@@ -44,7 +53,9 @@ export function sumRange(raw, t0, t1) {
|
|||||||
/**
|
/**
|
||||||
* One series per overlaid week: [this week, 1 week ago, ...], at native
|
* 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
|
* 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
|
* — 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".
|
* (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).
|
* The coarser ranges use per-day rates instead (unitMinutes = 24*60).
|
||||||
@@ -67,12 +78,13 @@ export function weeklySeries(buckets) {
|
|||||||
: start + WEEK
|
: start + WEEK
|
||||||
const points = []
|
const points = []
|
||||||
for (let t = start; t < end; t += MIN5) {
|
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({
|
out.push({
|
||||||
points,
|
points,
|
||||||
label: back === 0 ? 'this week' : `${back}w ago`,
|
label: `Week ${isoWeek(start)}`,
|
||||||
opacity: Math.max(0.15, 1 - back * 0.25),
|
opacity: Math.max(0.15, 1 - back * 0.25),
|
||||||
|
past: back > 0,
|
||||||
area: back === 0,
|
area: back === 0,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user