Analytics charts: translucent typical fill, last-week tail on week view
Render the seasonal typical estimate as a translucent muted fill (no stroke, same 0.6 opacity as the data) instead of a grey line, on both day and week views. Continue the fixed Monday-Monday week view with last week's curve from the current bucket to the end of the week (secondary accent, translucent fill), so the chart is useful early in the week and last week is gradually replaced by the current one. Gate the typical estimate on history spanning twice the view's full time (third day / third week on) and the last-week tail on data reaching into last week at all; the legend shows each entry only when present.
This commit is contained in:
+14
-3
@@ -366,8 +366,17 @@ 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 (the current week keeps the
|
The week range is aligned to Monday 00:00 UTC (the current week keeps the
|
||||||
accent color and is truncated at the current bucket, never drawing fake
|
accent color and is truncated at the current bucket, never drawing fake
|
||||||
zeroes for the future). Both the week and day views overlay a **"typical"
|
zeroes for the future). Since the window is fixed Monday-to-Monday, **last
|
||||||
history curve** in the muted color (`analytics/seasonal.js`, a port of
|
week's curve** continues the graph from the current bucket to the end of
|
||||||
|
the week in the secondary accent (`--accent2`, translucent fill like the
|
||||||
|
current week), so the chart shows useful data
|
||||||
|
on Monday too and last week is gradually replaced by the current week;
|
||||||
|
the tail is only drawn when the recorded data reaches into last week.
|
||||||
|
Both the week and day views overlay a **"typical"
|
||||||
|
history estimate** as a muted fill with no stroke, translucent to the same
|
||||||
|
degree as the current data — shown only once the history spans twice the
|
||||||
|
view's full time (from the third day on the day view, the third week on
|
||||||
|
the week view; `analytics/seasonal.js`, a port of
|
||||||
`seasonal.py`): the whole recorded history is densified to 5-minute bins,
|
`seasonal.py`): the whole recorded history is densified to 5-minute bins,
|
||||||
smoothed with the same Gaussian as the week view, then folded onto a weekly
|
smoothed with the same Gaussian as the week view, then folded onto a weekly
|
||||||
grid with exponential decay over age — a 7-day half-life for the average
|
grid with exponential decay over age — a 7-day half-life for the average
|
||||||
@@ -381,7 +390,9 @@ estimate as "Typical week" (future included); the day view cuts the rolling
|
|||||||
24-hour window's bins from the same estimate and labels them by the weekday
|
24-hour window's bins from the same estimate and labels them by the weekday
|
||||||
("Typical Saturday"). A compact legend inside the top right of the visits
|
("Typical Saturday"). A compact legend inside the top right of the visits
|
||||||
chart marks the current data in accent (ISO week label, or a bar specimen
|
chart marks the current data in accent (ISO week label, or a bar specimen
|
||||||
for "Last 24 hours") and the typical curve on a muted specimen. The week
|
for "Last 24 hours"), the previous week's tail on a secondary-accent line
|
||||||
|
specimen (week view only), and the typical estimate on a muted fill
|
||||||
|
specimen. The week
|
||||||
view's x labels are weekday names centered at midday UTC, without
|
view's x labels are weekday names centered at midday UTC, without
|
||||||
vertical grid
|
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
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* Visitor and page-view smoothed curves for a single shared time range.
|
* Visitor and page-view smoothed curves for a single shared time range.
|
||||||
*/
|
*/
|
||||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||||
import { makeSeries } from './analytics/time.js'
|
import { DAY, WEEK, makeSeries } from './analytics/time.js'
|
||||||
import { typicalWeek, weekBinIndex } from './analytics/seasonal.js'
|
import { typicalWeek, weekBinIndex } from './analytics/seasonal.js'
|
||||||
import {
|
import {
|
||||||
CHART_H,
|
CHART_H,
|
||||||
@@ -56,14 +56,17 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Series for the current range plus, on the day and week views, the
|
* Series for the current range plus, on the day and week views, the
|
||||||
* seasonal "typical week" history curve (all history up to now, already
|
* seasonal "typical week" history estimate (all history up to now, already
|
||||||
* smoothed). Week view: the full Monday-first week. Day view: the rolling
|
* smoothed). Week view: the full Monday-first week. Day view: the rolling
|
||||||
* window's bins looked up from the same estimate, labeled by the weekday.
|
* window's bins looked up from the same estimate, labeled by the weekday.
|
||||||
|
* The estimate only appears once the recorded history spans twice the
|
||||||
|
* view's full time — from the third day / third week on.
|
||||||
*/
|
*/
|
||||||
function withTypical(buckets) {
|
function withTypical(buckets) {
|
||||||
const input = makeSeries(buckets, props.range)
|
const input = makeSeries(buckets, props.range)
|
||||||
if (props.range !== 'day' && props.range !== 'week') return input
|
if (props.range !== 'day' && props.range !== 'week') return input
|
||||||
const estimate = typicalWeek(buckets, now.value)
|
const minHistory = props.range === 'week' ? 2 * WEEK : 2 * DAY
|
||||||
|
const estimate = typicalWeek(buckets, now.value, { minHistory })
|
||||||
if (!estimate) return input
|
if (!estimate) return input
|
||||||
if (props.range === 'week') {
|
if (props.range === 'week') {
|
||||||
return { ...input, typical: { values: [...estimate], label: 'Typical week' } }
|
return { ...input, typical: { values: [...estimate], label: 'Typical week' } }
|
||||||
@@ -77,6 +80,11 @@ function withTypical(buckets) {
|
|||||||
|
|
||||||
const visitChart = computed(() => buildChart(withTypical(props.data?.site_visits), now.value))
|
const visitChart = computed(() => buildChart(withTypical(props.data?.site_visits), now.value))
|
||||||
const viewChart = computed(() => buildChart(withTypical(allViews.value), now.value))
|
const viewChart = computed(() => buildChart(withTypical(allViews.value), now.value))
|
||||||
|
|
||||||
|
/** The previous week's tail series on the week view, if present. */
|
||||||
|
function pastSeries(chart) {
|
||||||
|
return chart.series.find((s) => s.past)
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -100,8 +108,8 @@ const viewChart = computed(() => buildChart(withTypical(allViews.value), now.val
|
|||||||
class="minor vertical" />
|
class="minor vertical" />
|
||||||
</template>
|
</template>
|
||||||
<g :clip-path="`url(#plot-${c.ylabel})`">
|
<g :clip-path="`url(#plot-${c.ylabel})`">
|
||||||
<!-- The muted "typical" history curve under the current data. -->
|
<!-- The translucent "typical" history fill under the current data. -->
|
||||||
<path v-if="c.chart.typical" :d="c.chart.typical.line" class="line past" />
|
<path v-if="c.chart.typical" :d="c.chart.typical.area" class="typical" />
|
||||||
<template v-if="c.chart.bars">
|
<template v-if="c.chart.bars">
|
||||||
<rect v-for="(b, i) in c.chart.bars" :key="'b' + i"
|
<rect v-for="(b, i) in c.chart.bars" :key="'b' + i"
|
||||||
:x="b.x" :y="b.y" :width="b.width" :height="b.height" class="bar" />
|
:x="b.x" :y="b.y" :width="b.width" :height="b.height" class="bar" />
|
||||||
@@ -109,8 +117,8 @@ const viewChart = computed(() => buildChart(withTypical(allViews.value), now.val
|
|||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<template v-for="(s, i) in c.chart.series" :key="i">
|
<template v-for="(s, i) in c.chart.series" :key="i">
|
||||||
<path v-if="s.area" :d="s.area" class="area" />
|
<path v-if="s.area" :d="s.area" class="area" :class="{ past: s.past }" />
|
||||||
<path :d="s.line" class="line" />
|
<path :d="s.line" class="line" :class="{ past: s.past }" />
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</g>
|
</g>
|
||||||
@@ -124,16 +132,26 @@ const viewChart = computed(() => buildChart(withTypical(allViews.value), now.val
|
|||||||
<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>
|
||||||
<!-- Legend, top right inside the plot: current data in accent
|
<!-- Legend, top right inside the plot: current data in accent
|
||||||
(week label, or "Last 24 hours" on the day view) and the
|
(week label, or "Last 24 hours" on the day view), the previous
|
||||||
typical history curve in muted. -->
|
week's tail in the secondary accent (week view only), then the
|
||||||
<g v-if="c.legend && c.chart.typical">
|
typical history fill as a muted specimen. -->
|
||||||
|
<g v-if="c.legend && (c.chart.typical || pastSeries(c.chart))">
|
||||||
<line :x1="CHART_W - 118" :x2="CHART_W - 98" y1="10" y2="10" class="line" />
|
<line :x1="CHART_W - 118" :x2="CHART_W - 98" y1="10" y2="10" class="line" />
|
||||||
<text :x="CHART_W - 92" y="10" dominant-baseline="middle"
|
<text :x="CHART_W - 92" y="10" dominant-baseline="middle"
|
||||||
class="leglab">{{ c.chart.bars ? 'Last 24 hours' : c.chart.series[0].label }}</text>
|
class="leglab">{{ c.chart.bars ? 'Last 24 hours' : c.chart.series[0].label }}</text>
|
||||||
<line :x1="CHART_W - 118" :x2="CHART_W - 98" y1="25" y2="25"
|
<template v-if="pastSeries(c.chart)">
|
||||||
class="line past" style="opacity: 0.6" />
|
<line :x1="CHART_W - 118" :x2="CHART_W - 98" y1="25" y2="25"
|
||||||
<text :x="CHART_W - 92" y="25" dominant-baseline="middle"
|
class="line past" />
|
||||||
class="leglab">{{ c.chart.typical.label }}</text>
|
<text :x="CHART_W - 92" y="25" dominant-baseline="middle"
|
||||||
|
class="leglab">{{ pastSeries(c.chart).label }}</text>
|
||||||
|
</template>
|
||||||
|
<template v-if="c.chart.typical">
|
||||||
|
<rect :x="CHART_W - 118" :y="pastSeries(c.chart) ? 34 : 19"
|
||||||
|
width="20" height="12" class="typical" />
|
||||||
|
<text :x="CHART_W - 92" :y="pastSeries(c.chart) ? 40 : 25"
|
||||||
|
dominant-baseline="middle"
|
||||||
|
class="leglab">{{ c.chart.typical.label }}</text>
|
||||||
|
</template>
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
</template>
|
</template>
|
||||||
@@ -213,9 +231,21 @@ const viewChart = computed(() => buildChart(withTypical(allViews.value), now.val
|
|||||||
stroke-linecap: round;
|
stroke-linecap: round;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Past overlay weeks contrast with the current week's accent color. */
|
/* The seasonal "typical" estimate is a muted fill under the current data,
|
||||||
|
translucent to the same degree, no stroke. */
|
||||||
|
.chart .typical {
|
||||||
|
fill: var(--muted);
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The previous week's tail on the week view uses the secondary accent so
|
||||||
|
only the typical fill is grey. */
|
||||||
.chart .line.past {
|
.chart .line.past {
|
||||||
stroke: var(--muted);
|
stroke: var(--accent2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart .area.past {
|
||||||
|
fill: var(--accent2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty { color: var(--muted); }
|
.empty { color: var(--muted); }
|
||||||
|
|||||||
@@ -189,13 +189,14 @@ export function buildChart(input, now = Date.now()) {
|
|||||||
const smoothed = series.map((s) =>
|
const smoothed = series.map((s) =>
|
||||||
smooth(s.points.map((p) => p.count), binMinutes, unitMinutes).map((v) => v * rate))
|
smooth(s.points.map((p) => p.count), binMinutes, unitMinutes).map((v) => v * rate))
|
||||||
// The "typical week" seasonal estimate is already smooth: one value per
|
// The "typical week" seasonal estimate is already smooth: one value per
|
||||||
// bin spanning the full week (future included), drawn in the muted color.
|
// bin spanning the full week (future included), drawn as a translucent
|
||||||
|
// muted fill under the current data.
|
||||||
const typicalRates = typical
|
const typicalRates = typical
|
||||||
? [...typical.values].map((v) => v * rate)
|
? [...typical.values].map((v) => v * rate)
|
||||||
: null
|
: null
|
||||||
// Scale from the current series plus the typical curve; both are smooth,
|
// Scale from the current series plus the typical curve; both are smooth,
|
||||||
// and neither should be clipped in normal traffic.
|
// and neither should be clipped in normal traffic.
|
||||||
const highest = Math.max(0, ...smoothed[0], ...(typicalRates || []))
|
const highest = Math.max(0, ...smoothed.flat(), ...(typicalRates || []))
|
||||||
const { max, step, minor } = yScale(highest)
|
const { max, step, minor } = yScale(highest)
|
||||||
const x = (t) => ((t - t0) / (t1 - t0)) * CHART_W
|
const x = (t) => ((t - t0) / (t1 - t0)) * CHART_W
|
||||||
const y = (v) => PAD_TOP + (1 - Math.max(0, v) / max) * (CHART_H - PAD_TOP)
|
const y = (v) => PAD_TOP + (1 - Math.max(0, v) / max) * (CHART_H - PAD_TOP)
|
||||||
@@ -210,11 +211,15 @@ export function buildChart(input, now = Date.now()) {
|
|||||||
area: s.area ? `${line}L${last.x},${CHART_H}L${first.x},${CHART_H}Z` : null,
|
area: s.area ? `${line}L${last.x},${CHART_H}L${first.x},${CHART_H}Z` : null,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
let typicalLine = null
|
let typicalFill = null
|
||||||
if (typicalRates) {
|
if (typicalRates) {
|
||||||
const binMs = (t1 - t0) / typicalRates.length
|
const binMs = (t1 - t0) / typicalRates.length
|
||||||
const pts = typicalRates.map((v, i) => ({ x: x(t0 + i * binMs), y: y(v) }))
|
const pts = typicalRates.map((v, i) => ({ x: x(t0 + i * binMs), y: y(v) }))
|
||||||
typicalLine = { line: spline(pts), label: typical.label }
|
const line = spline(pts)
|
||||||
|
typicalFill = {
|
||||||
|
area: `${line}L${pts.at(-1).x},${CHART_H}L${pts[0].x},${CHART_H}Z`,
|
||||||
|
label: typical.label,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Major (labeled) and minor (hairline) y grid ticks.
|
// Major (labeled) and minor (hairline) y grid ticks.
|
||||||
const majors = []
|
const majors = []
|
||||||
@@ -267,7 +272,7 @@ export function buildChart(input, now = Date.now()) {
|
|||||||
x: x(t), label: fmtTick(t, t1 - t0), line: true,
|
x: x(t), label: fmtTick(t, t1 - t0), line: true,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
return { max, majors, minors, series: drawn, typical: typicalLine, xticks, unit }
|
return { max, majors, minors, series: drawn, typical: typicalFill, xticks, unit }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -275,8 +280,8 @@ export function buildChart(input, now = Date.now()) {
|
|||||||
* counts; the skyline uses a projected full-bucket value for the still-open
|
* counts; the skyline uses a projected full-bucket value for the still-open
|
||||||
* final bucket. The y scale is derived from the projected skyline maximum.
|
* final bucket. The y scale is derived from the projected skyline maximum.
|
||||||
* The optional "typical day" curve (per-bin counts aligned to the window's
|
* The optional "typical day" curve (per-bin counts aligned to the window's
|
||||||
* bins, cut from the typical-week estimate) overlays the bars as a smooth
|
* bins, cut from the typical-week estimate) underlays the bars as a
|
||||||
* muted line and also feeds the y scale.
|
* translucent muted fill and also feeds the y scale.
|
||||||
*/
|
*/
|
||||||
export function buildDayChart(input, now = Date.now()) {
|
export function buildDayChart(input, now = Date.now()) {
|
||||||
const { series, t0, t1, typical } = input
|
const { series, t0, t1, typical } = input
|
||||||
@@ -327,13 +332,17 @@ export function buildDayChart(input, now = Date.now()) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let typicalLine = null
|
let typicalFill = null
|
||||||
if (typical) {
|
if (typical) {
|
||||||
const pts = points.map((p, i) => ({
|
const pts = points.map((p, i) => ({
|
||||||
x: (i + 0.5) * bucketWidth,
|
x: (i + 0.5) * bucketWidth,
|
||||||
y: y(typical.values[i] || 0),
|
y: y(typical.values[i] || 0),
|
||||||
}))
|
}))
|
||||||
typicalLine = { line: spline(pts), label: typical.label }
|
const line = spline(pts)
|
||||||
|
typicalFill = {
|
||||||
|
area: `${line}L${pts.at(-1).x},${CHART_H}L${pts[0].x},${CHART_H}Z`,
|
||||||
|
label: typical.label,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const majors = []
|
const majors = []
|
||||||
@@ -361,7 +370,7 @@ export function buildDayChart(input, now = Date.now()) {
|
|||||||
line: false,
|
line: false,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return { bars, skyline: skyline.trim(), typical: typicalLine, max, majors, minors, xticks, unit: '5min', series: [] }
|
return { bars, skyline: skyline.trim(), typical: typicalFill, max, majors, minors, xticks, unit: '5min', series: [] }
|
||||||
}
|
}
|
||||||
|
|
||||||
/** X ticks for year/all: Monday boundaries up to a quarter, UTC month
|
/** X ticks for year/all: Monday boundaries up to a quarter, UTC month
|
||||||
|
|||||||
@@ -92,13 +92,15 @@ export function weekBinIndex(t) {
|
|||||||
* (capped at MAX_HISTORY_DAYS back), smoothed with the same Gaussian the
|
* (capped at MAX_HISTORY_DAYS back), smoothed with the same Gaussian the
|
||||||
* week view uses, then folded by seasonalCurve. Returns BINS_PER_WEEK
|
* week view uses, then folded by seasonalCurve. Returns BINS_PER_WEEK
|
||||||
* counts per 5-minute bin starting Monday, or null when there is less than
|
* counts per 5-minute bin starting Monday, or null when there is less than
|
||||||
* a day of history.
|
* a day of history or the history span (first bucket to tEnd, before
|
||||||
|
* capping) is below minHistory.
|
||||||
*/
|
*/
|
||||||
export function typicalWeek(buckets, tEnd = Date.now()) {
|
export function typicalWeek(buckets, tEnd = Date.now(), { minHistory = 0 } = {}) {
|
||||||
const raw = rawTimes(buckets)
|
const raw = rawTimes(buckets)
|
||||||
const times = Object.keys(raw).map(Number)
|
const times = Object.keys(raw).map(Number)
|
||||||
if (!times.length) return null
|
if (!times.length) return null
|
||||||
const end = Math.floor(tEnd / MIN5) * MIN5
|
const end = Math.floor(tEnd / MIN5) * MIN5
|
||||||
|
if (end - Math.min(...times) < minHistory) return null
|
||||||
const start = Math.max(Math.min(...times), end - MAX_HISTORY_DAYS * DAY)
|
const start = Math.max(Math.min(...times), end - MAX_HISTORY_DAYS * DAY)
|
||||||
const n = Math.floor((end - start) / MIN5)
|
const n = Math.floor((end - start) / MIN5)
|
||||||
if (n < BINS_PER_DAY) return null
|
if (n < BINS_PER_DAY) return null
|
||||||
|
|||||||
@@ -4,7 +4,8 @@
|
|||||||
* Raw data comes as sparse 5-minute buckets; the range picks the x window
|
* Raw data comes as sparse 5-minute buckets; the range picks the x window
|
||||||
* and a coarser bucket size to keep point counts sane. The week range is
|
* and a coarser bucket size to keep point counts sane. The week range is
|
||||||
* aligned to Monday 00:00 UTC; a "typical week" seasonal estimate
|
* aligned to Monday 00:00 UTC; a "typical week" seasonal estimate
|
||||||
* (seasonal.js) is overlaid on the week and day views by the chart builder.
|
* (seasonal.js) is overlaid as a solid fill on the week and day views by
|
||||||
|
* the chart builder.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const MIN5 = 5 * 60e3
|
export const MIN5 = 5 * 60e3
|
||||||
@@ -55,8 +56,14 @@ export function sumRange(raw, t0, t1) {
|
|||||||
* bucket — no fake zeroes drawn for the future. Counts are rates per hour
|
* 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".
|
* (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).
|
||||||
* Previous weeks are no longer overlaid; the "typical week" seasonal
|
* Since the window is fixed Monday-to-Monday, the days not yet reached
|
||||||
* estimate (seasonal.js) takes their place as the history reference.
|
* would otherwise be blank early in the week: last week's curve continues
|
||||||
|
* the graph from the current bucket to the end of the week (secondary
|
||||||
|
* accent, translucent fill like the current week), gradually replaced by
|
||||||
|
* the current week as it accrues. The tail is only drawn when the data
|
||||||
|
* reaches into last week at all. The
|
||||||
|
* "typical week" seasonal estimate (seasonal.js) is the statistical
|
||||||
|
* history reference under both.
|
||||||
*/
|
*/
|
||||||
export function weeklySeries(buckets) {
|
export function weeklySeries(buckets) {
|
||||||
const raw = rawTimes(buckets)
|
const raw = rawTimes(buckets)
|
||||||
@@ -67,8 +74,19 @@ export function weeklySeries(buckets) {
|
|||||||
for (let t = thisMonday; t < end; t += MIN5) {
|
for (let t = thisMonday; t < end; t += MIN5) {
|
||||||
points.push({ t, count: raw[t] || 0 })
|
points.push({ t, count: raw[t] || 0 })
|
||||||
}
|
}
|
||||||
|
const past = []
|
||||||
|
if (Object.keys(raw).some((t) => Number(t) < thisMonday)) {
|
||||||
|
for (let t = end; t < thisMonday + WEEK; t += MIN5) {
|
||||||
|
past.push({ t, count: raw[t - WEEK] || 0 })
|
||||||
|
}
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
series: [{ points, label: `Week ${isoWeek(thisMonday)}`, opacity: 1, area: true }],
|
series: [
|
||||||
|
{ points, label: `Week ${isoWeek(thisMonday)}`, opacity: 1, area: true },
|
||||||
|
...(past.length
|
||||||
|
? [{ points: past, label: `Week ${isoWeek(thisMonday - WEEK)}`, past: true, area: true }]
|
||||||
|
: []),
|
||||||
|
],
|
||||||
t0: thisMonday,
|
t0: thisMonday,
|
||||||
t1: thisMonday + WEEK,
|
t1: thisMonday + WEEK,
|
||||||
rate: HOUR / MIN5,
|
rate: HOUR / MIN5,
|
||||||
|
|||||||
Reference in New Issue
Block a user