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.
@/components/charts/heat-calendar1"use client";;2// beui.dev/charts/heat-calendar3 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 children25 )}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 / setUTCDate40 / 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 en44 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 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.
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.).
Uso
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
Acciones del componente
Search for a command to run...
Software a medida, IA integrada y ciberseguridad avanzada para empresas.
Consultoría SEO Avanzado & Analítica
Auditoría técnica profunda para posicionar tu negocio en los primeros resultados de Google.
SEO TécnicoGA4Páginas Web Corporativas Complejas
Plataformas empresariales masivas con múltiples secciones y animaciones fluidas.
Framer MotionMulti-pageIntegración de Inteligencia Artificial en Negocios
Automatizamos la atención, las reservas y las ventas de tu negocio con agentes de IA.
AI AgentsChatbots