Files
beavergram/frontend/src/lib/components/presence/PresenceHeatmap.svelte
T

117 lines
2.6 KiB
Svelte

<script lang="ts">
import type { PresenceHourly } from "#lib/api/types.js";
interface Props {
hourly: PresenceHourly[];
}
const { hourly }: Props = $props();
const DAYS = ["Пн", "Вт", "Ср", "Чт", "Пт", "Сб", "Вс"];
const HOURS = Array.from({ length: 24 }, (_, hour) => hour);
const HOUR_TICKS = new Set([0, 6, 12, 18]);
interface Cell {
online: number;
total: number;
}
const grid = $derived.by(() => {
const cells: Cell[][] = DAYS.map(() =>
HOURS.map(() => ({ online: 0, total: 0 }))
);
for (const bucket of hourly) {
const date = new Date(bucket.bucket);
const day = (date.getDay() + 6) % 7;
const cell = cells[day]?.[date.getHours()];
if (cell) {
cell.online += bucket.online_samples;
cell.total += bucket.samples;
}
}
return cells;
});
function intensity(cell: Cell): number {
return cell.total > 0 ? cell.online / cell.total : 0;
}
function cellTitle(day: number, hour: number, cell: Cell): string {
const pct = cell.total > 0 ? Math.round(intensity(cell) * 100) : 0;
return `${DAYS[day]} ${hour}:00 — ${pct}% online`;
}
</script>
<div class="heatmap">
{#each grid as row, day (day)}
<div class="heat-row">
<span class="day-label">{DAYS[day]}</span>
<div class="cells">
{#each row as cell, hour (hour)}
<span
class="cell"
title={cellTitle(day, hour, cell)}
style:--i={intensity(cell)}
class:empty={cell.total === 0}
></span>
{/each}
</div>
</div>
{/each}
<div class="heat-row axis">
<span class="day-label"></span>
<div class="cells">
{#each HOURS as hour (hour)}
<span class="tick">{HOUR_TICKS.has(hour) ? hour : ""}</span>
{/each}
</div>
</div>
</div>
<style lang="scss">
.heatmap {
display: flex;
flex-direction: column;
gap: 3px;
}
.heat-row {
display: flex;
align-items: center;
gap: 0.5rem;
}
.day-label {
width: 1.5rem;
flex-shrink: 0;
font-size: 0.6875rem;
color: var(--color-text-secondary);
text-align: right;
}
.cells {
display: grid;
flex: 1;
grid-template-columns: repeat(24, 1fr);
gap: 3px;
}
.cell {
aspect-ratio: 1;
border-radius: 2px;
background-color: var(--color-primary);
opacity: calc(0.12 + 0.88 * var(--i));
&.empty {
background-color: var(--color-text-secondary);
opacity: 0.08;
}
}
.tick {
font-size: 0.625rem;
color: var(--color-text-secondary);
text-align: left;
}
</style>