Componentes
[gráficas] · motion

HeatCalendar

Calendario de actividad de un solo tono (semanas x días) con entrada en ola, ripple al hover, tooltip gliding y selección por clic que totaliza un rango. Composición por partes: Grid, Tooltip y Legend.

motion
@/components/charts/heat-calendar
 menosmás
1"use client";;
2// beui.dev/charts/heat-calendar
3 
4import { cn } from "@/lib/utils";
5import { HeatCalendarContext, useHeatCalendarModel } from "./heat-calendar/context";
6import { HeatCalendarGrid } from "./heat-calendar/grid";
7import { HeatCalendarLegend } from "./heat-calendar/legend";
8import { HeatCalendarTooltip } from "./heat-calendar/tooltip";
9 
10/** Compose Grid, Tooltip and Legend, or omit children for the complete chart. */
11export function HeatCalendar({ children, className, ...props }) {
12 const model = useHeatCalendarModel(props);
13 return (
14 <HeatCalendarContext.Provider value={model}>
15 <div className={cn("w-fit max-w-full", className)}>
16 {children === undefined ? (
17 <>
18 <HeatCalendarGrid>
19 <HeatCalendarTooltip />
20 </HeatCalendarGrid>
21 <HeatCalendarLegend />
22 </>
23 ) : (
24 children
25 )}
26 </div>
27 </HeatCalendarContext.Provider>
28 );
29}
30 
31export { useHeatCalendar } from "./heat-calendar/context";
32export { HeatCalendarGrid } from "./heat-calendar/grid";
33export { HeatCalendarLegend } from "./heat-calendar/legend";
34export { HeatCalendarTooltip } from "./heat-calendar/tooltip";
35 
36/* ═══════════════════════════════════════════════════════════════════════════
37 heat-calendar/utils.js — geometría y formatos.
38 
39 IMPORTANTE: toda la aritmética de fechas es en UTC (setUTCHours / setUTCDate
40 / getUTCDay). Si vas a leer datos que vienen en clave local "YYYY-MM-DD",
41 construye el ancla desde la CADENA, no desde `new Date()`:
42 new Date(todayKey + "T00:00:00Z")
43 Así el lunes que calcula el componente y tu clave local son el mismo día en
44 cualquier huso, y celda ↔ fecha hace round-trip exacto.
45 ═══════════════════════════════════════════════════════════════════════════ */
46 
47/** Five buckets: 0 is the neutral empty cell, 1 to 4 mix the one hue in harder. */
48export const STEPS = [0, 24, 46, 70, 94];
49 
50export const EMPTY = "color-mix(in srgb, var(--foreground) 6%, transparent)";
51 
52/** Cell size and gap; every position in the grid and the tooltip derive from these. */
53export const CELL = 16;
54 
55export const GAP = 4;
56 
57export const PITCH = CELL + GAP;
58 
59export const MONTH_ROW = 12;
60 
61export const DAYS = Array.from({ length: 7 }, (_, d) => ({ id: `d${d}`, d }));
62 
63/** How far a cell rises when it is the hovered one, its neighbour, or two away. */
64export const LIFT = [1.3, 1.08, 1.03];
65 
66export const startOfDay = (d) => {
67 const x = new Date(d);
68 x.setUTCHours(0, 0, 0, 0);
69 return x;
70};
71 
72export const addDays = (d, n) => {
73 const x = new Date(d);
74 x.setUTCDate(x.getUTCDate() + n);
75 return x;
76};
77 
78/** Monday on or before `d`, so every column reads Mon to Sun, top to bottom. */
79export const mondayOf = (d) => addDays(startOfDay(d), -((d.getUTCDay() + 6) % 7));
80 
81export const fmtDay = new Intl.DateTimeFormat("es-MX", {
82 timeZone: "UTC",
83 weekday: "short",
84 month: "short",
85 day: "numeric",
86});
87 
88export const fmtMonth = new Intl.DateTimeFormat("es-MX", { timeZone: "UTC", month: "short" });
89 
90export const fmtRange = new Intl.DateTimeFormat("es-MX", { timeZone: "UTC", month: "short", day: "numeric" });
91 
[ejemplos]
01

Composición por partes

Sin `children` el componente pinta la rejilla completa. Pasando `children` armas tu propia composición: aquí la leyenda va debajo de un tooltip propio que muestra el conteo y la fecha en español.

 menosmás
02

Clic, rango y total

El clic es la interacción central: fija la celda, atenúa el resto y a partir de ahí el hover previsualiza el tramo hasta el puntero con su total; el segundo clic lo fija. `onSelectionChange` es el gancho para reaccionar (abrir un día, filtrar, etc.).

 menosmás

Uso

[documentación]

La matriz values

Es una matriz de dos dimensiones: el primer índice es la semana (columna, de izquierda a derecha) y el segundo el día de la semana, donde 0 es lunes y 6 domingo. Cada valor va de 0 a 1 y representa la intensidad relativa: 0 es la celda vacía y 1 el tope. El conteo que se muestra en el tooltip se reconstruye como round(v * maxCount), así que basta normalizar tus datos contra el pico del periodo.

Cuidado con la zona horaria

MUY importante: toda la aritmética de fechas del componente es en UTC. Si tus datos vienen en clave local "YYYY-MM-DD", construye el ancla desde la cadena y no desde `new Date()`: `endDate={new Date(todayKey + "T00:00:00Z")}`. Así el lunes que calcula la rejilla y tu clave local son el mismo día en cualquier huso, y la conversión celda ↔ fecha hace round-trip exacto.

Un solo tono, cinco niveles

A diferencia de ContributionGraph (paleta de 5 colores estilo GitHub), este componente mezcla un único `color` con transparent en porcentajes crecientes: 0%, 24%, 46%, 70% y 94%. La magnitud se lee en la fuerza de un mismo color, que es lo que hace que se vea integrado con el tema en vez de "pegado" encima.

Animación y accesibilidad

La entrada es una ola diagonal: cada celda aparece (w + d) pasos después de la esquina. El hover levanta la celda y su vecindad en un ripple corto, limitado a dispositivos con puntero real, y `prefers-reduced-motion` deja solo los desvanecidos. Cada celda es un <button> con aria-label y aria-pressed, navegable con teclado, y Escape limpia la selección.

En uso dentro de SFESS

`@/components/calendar/activity-heatmap` es el wrapper que ya lo conecta al calendario del usuario: agrupa los eventos por día, normaliza contra el pico visible, y al hacer clic mueve el mes en el store global para que EventCalendar y KanbanBoard se sincronicen. Sirve de referencia para conectar el componente a tus propios datos.

Props

PropTipoDefaultDescripción
valuesnumber[][]—Matriz [semana][día] de 0..1. week = columna, day = 0 lunes … 6 domingo.
unitstring"commits"Sustantivo del tooltip y del aria-label.
weeksnumber16Columnas (semanas) a mostrar.
maxCountnumber14La intensidad 1.0 equivale a este conteo; el conteo real es round(v * maxCount).
colorstringvar(--accent)Tono único; los 5 niveles lo mezclan con transparent.
endDateDatehoyAncla de la rejilla. Explícita = determinista (sin salto tras hidratar).
selection{ start, end }—Selección controlada (celdas {w, d}).
defaultSelection{ start, end }nullSelección inicial no controlada.
onSelectionChange(sel) => void—Cada clic. Recibe null al deseleccionar.

Acciones del componente

Search for a command to run...

Potenciá tu negocio con SFESS

Software a medida, IA integrada y ciberseguridad avanzada para empresas.

0años de ingeniería
0proyectos entregados
0% uptime
Servicios destacados
  • Consultoría SEO Avanzado & Analítica

    Auditoría técnica profunda para posicionar tu negocio en los primeros resultados de Google.

    SEO TécnicoGA4
  • Páginas Web Corporativas Complejas

    Plataformas empresariales masivas con múltiples secciones y animaciones fluidas.

    Framer MotionMulti-page
  • Integración de Inteligencia Artificial en Negocios

    Automatizamos la atención, las reservas y las ventas de tu negocio con agentes de IA.

    AI AgentsChatbots
Nuestro ecosistema
WORKENTA_OSSFESS_SOLUTIONSPRANALUZ_YOGAREMY_AI
Ver todos los servicios