Charts

Activity Heatmap

Тепловая карта «день недели × время суток»: насыщенность клетки считается через color-mix от доли максимума, шкала — одна переменная акцента.

  • heatmap
  • activity
  • grid
  • color-mix

Превью

1440pxHost theme

Использовать с AI

  1. 1. Скопируйте ссылку.
  2. 2. Напишите агенту своими словами и вставьте её в предложение.
  3. 3. Агент откроет ссылку и поставит компонент из registry.

размести это в шапке: https://vibeui.ru/c/chart-021

Активность за неделю

режечаще

Единица измерения: сессий в интервале. Максимум клетки — 52.

Активность за неделю, сессий в интервале
День0:002:004:006:008:0010:0012:0014:0016:0018:0020:0022:00
Пн2114183441384429146
Вт1126213846424831167
Ср2115193644454733188
Чт1027244152495135199
Пт31262239474340272112
Сб5323915222624221811
Вс421271218212325209
Показать полную инструкцию

То, что лежит по ссылке в развёрнутом виде. Нужна, если агент не может открыть ссылку — тогда вставьте этот текст целиком.

# Установка и размещение «chart-021» (Activity Heatmap) из VibeUI

## 1. Сначала установка — не пропускай, не пересоздавай
Выполни эту команду до того, как писать код:
npx shadcn@latest add https://vibeui.ru/r/chart-021.json

Registry item: https://vibeui.ru/r/chart-021.json
Ставится в: components/vibeui/chart-021.tsx (точный путь следует алиасам components.json этого проекта).
npm-зависимости: нет.
Registry-зависимости: нет.

Ставь из реестра. Не пересоздавай по описанию, не подменяй похожим
компонентом из другой библиотеки и не переписывай под стиль проекта.

## 2. Что это
Тепловая карта «день недели × время суток»: насыщенность клетки считается через color-mix от доли максимума, шкала — одна переменная акцента.

Тепловая карта активности: семь строк дней, колонки часовых интервалов, насыщенность клетки от доли максимума, шкала «реже — чаще» и подписи часов. Данные продублированы скрытой таблицей день×час. Ноль зависимостей, один файл, клиентского JS нет.

## 3. Как использовать
import { Chart021 } from "@/components/vibeui/chart-021"

<Chart021
  title="Активность за неделю"
  unit="сессий в двухчасовом интервале"
  hours={[0, 4, 8, 12, 16, 20]}
/>

Полный список пропсов — в установленном файле.

## 4. Куда поставить
Это мелкий inline-компонент. Поставь его ровно туда, куда просил
пользователь, внутрь существующей разметки. Не создавай ради него
новую страницу, секцию или обёртку. Если на этом месте уже стоит похожий
контрол — замени его, а не добавляй второй.

Куда поставить: ___
(Эту строку заполняет пользователь. Если она осталась пустой — спроси,
а не угадывай.)

## 5. Сохранить как установлено
- локальную палитру --vibeui-chart-021-* и не подменять её токенами вашей темы (bg-card, --chart-1 и подобными)
- расчёт цвета через color-mix от доли максимума: вся шкала интенсивности держится на одной переменной акцента
- шкалу «реже — чаще» под картой: без неё насыщенность не переводится в числа
- подписи часов через одну: подписанные все, они наезжают друг на друга на узкой карте
- скрытую таблицу день×час и aria-hidden на карте — восемьдесят четыре клетки без текста недоступны
- квадратные клетки через aspect-ratio: вытянутые клетки читаются как разная величина

## 6. Можно менять
- матрицу matrix и подписи days и hours
- title — заголовок карты
- unit — единица измерения в подписи под картой
- акцент через проп accent — цвет самой насыщенной клетки
- радиус скругления и зазор между клетками

## 7. Правила
- Длина каждой строки matrix обязана совпадать с длиной hours: недостающие клетки считаются нулевыми молча.
- Двадцать четыре колонки на узком экране превращаются в полоски: для мобильных берите двухчасовые интервалы.
- Насыщенность нормируется на максимум всей матрицы, поэтому один выброс делает остальную карту бледной.
- Не выносите CSS-переменные в globals.css: компонент обязан оставаться одним файлом.

## 8. Проверка
- рендерится без ошибок в консоли;
- выглядит так же, как превью на странице VibeUI, откуда взята ссылка;
- если нет — ты изменил что-то из раздела 5, верни как было.

Для разработчика

npx shadcn@latest add https://vibeui.ru/r/chart-021.json
https://vibeui.ru/r/chart-021.json

Компонент самодостаточен: один файл, без зависимостей, собственная палитра в локальных переменных --vibeui-chart-021-*. Серверный: хуков нет. Карта — CSS-грид, число колонок приходит переменной --vibeui-chart-021-cols, поэтому шаг сетки часов меняется одним пропом. Каждая клетка получает --vibeui-chart-021-level с долей максимума, а цвет считается color-mix между акцентом и пустым фоном: вся шкала интенсивности держится на одной переменной. Строки дней собираются во фрагментах, чтобы попадать прямо в грид. Данные продублированы визуально скрытой таблицей день×час.

Исходник компонента

Тот же файл, который поставит агент. Нужен, если вы предпочитаете скопировать код руками.

import { Fragment } from "react"
import type { ComponentPropsWithoutRef, CSSProperties } from "react"

export type Chart021Props = Omit<
  ComponentPropsWithoutRef<"figure">,
  "children" | "title"
> & {
  title?: string
  days?: string[]
  hours?: number[]
  matrix?: number[][]
  unit?: string
  accent?: string
}

// Идея компонента: тепловая карта «день недели × время суток». Насыщенность
// клетки считается через color-mix от доли максимума, поэтому шкала остаётся
// одной переменной акцента. Клетки — сетка, а не таблица с рамками: сетке
// не нужны отступы, и карта не рассыпается на узкой ширине.
const STYLES = `
:where([data-vibeui-block="chart-021"]){
--vibeui-chart-021-bg:oklch(1 0 0);
--vibeui-chart-021-fg:oklch(0.22 0.014 265);
--vibeui-chart-021-muted:oklch(0.55 0.014 265);
--vibeui-chart-021-border:oklch(0.91 0.006 265);
--vibeui-chart-021-empty:oklch(0.96 0.003 265);
--vibeui-chart-021-accent:oklch(0.52 0.16 245);
--vibeui-chart-021-font:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
}
[data-vibeui-block="chart-021"]{
display:flex;flex-direction:column;gap:0.5rem;
width:100%;max-width:30rem;box-sizing:border-box;margin:0;padding:0.875rem;
background:var(--vibeui-chart-021-bg);
border:1px solid var(--vibeui-chart-021-border);border-radius:0.875rem;
color:var(--vibeui-chart-021-fg);font-family:var(--vibeui-chart-021-font);
}
[data-vibeui-block="chart-021"] [data-part="title"]{margin:0;font-size:0.875rem;font-weight:650}
[data-vibeui-block="chart-021"] [data-part="map"]{
display:grid;grid-template-columns:1.75rem repeat(var(--vibeui-chart-021-cols),1fr);
gap:2px;align-items:center;
}
[data-vibeui-block="chart-021"] [data-part="day"]{
font-size:0.6875rem;color:var(--vibeui-chart-021-muted);
}
[data-vibeui-block="chart-021"] [data-part="cell"]{
aspect-ratio:1;border-radius:2px;min-height:0.75rem;
background:color-mix(in oklab,var(--vibeui-chart-021-accent) calc(var(--vibeui-chart-021-level) * 100%),var(--vibeui-chart-021-empty));
}
[data-vibeui-block="chart-021"] [data-part="hours"]{
display:grid;grid-template-columns:1.75rem repeat(var(--vibeui-chart-021-cols),1fr);
gap:2px;margin:0;padding:0;list-style:none;
font-size:0.625rem;color:var(--vibeui-chart-021-muted);font-variant-numeric:tabular-nums;
}
[data-vibeui-block="chart-021"] [data-part="hour"]{text-align:center}
[data-vibeui-block="chart-021"] [data-part="scale"]{
display:flex;align-items:center;gap:0.3125rem;font-size:0.6875rem;color:var(--vibeui-chart-021-muted);
}
[data-vibeui-block="chart-021"] [data-part="step"]{
width:0.75rem;height:0.75rem;border-radius:2px;
background:color-mix(in oklab,var(--vibeui-chart-021-accent) calc(var(--vibeui-chart-021-level) * 100%),var(--vibeui-chart-021-empty));
}
[data-vibeui-block="chart-021"] [data-part="unit"]{margin:0;font-size:0.75rem;color:var(--vibeui-chart-021-muted)}
[data-vibeui-block="chart-021"] [data-part="data"]{
position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;
clip-path:inset(50%);white-space:nowrap;border:0;
}
@media (prefers-reduced-motion:reduce){[data-vibeui-block="chart-021"] *{animation:none!important;transition:none!important}}
`

const DEFAULT_DAYS = ["Пн", "Вт", "Ср", "Чт", "Пт", "Сб", "Вс"]
const DEFAULT_HOURS = [0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22]

const DEFAULT_MATRIX = [
  [2, 1, 1, 4, 18, 34, 41, 38, 44, 29, 14, 6],
  [1, 1, 2, 6, 21, 38, 46, 42, 48, 31, 16, 7],
  [2, 1, 1, 5, 19, 36, 44, 45, 47, 33, 18, 8],
  [1, 0, 2, 7, 24, 41, 52, 49, 51, 35, 19, 9],
  [3, 1, 2, 6, 22, 39, 47, 43, 40, 27, 21, 12],
  [5, 3, 2, 3, 9, 15, 22, 26, 24, 22, 18, 11],
  [4, 2, 1, 2, 7, 12, 18, 21, 23, 25, 20, 9],
]

/**
 * Тепловая карта активности «день недели × время суток».
 * Один файл, ноль зависимостей, собственная палитра.
 */
export function Chart021({
  title = "Активность за неделю",
  days = DEFAULT_DAYS,
  hours = DEFAULT_HOURS,
  matrix = DEFAULT_MATRIX,
  unit = "сессий в двухчасовом интервале",
  accent,
  className,
  style,
  ...props
}: Chart021Props) {
  const max = Math.max(...matrix.flat(), 1)

  const palette = {
    ...(accent ? { "--vibeui-chart-021-accent": accent } : null),
    "--vibeui-chart-021-cols": String(hours.length),
    ...style,
  } as CSSProperties

  return (
    <>
      <style href="vibeui-chart-021" precedence="medium">
        {STYLES}
      </style>
      <figure
        {...props}
        data-vibeui-block="chart-021"
        className={className}
        style={palette}
      >
        <figcaption data-part="title">{title}</figcaption>
        <div data-part="map" aria-hidden="true">
          {days.map((day, dayIndex) => (
            <Fragment key={day}>
              <span data-part="day">{day}</span>
              {hours.map((hour, hourIndex) => (
                <span
                  key={hour}
                  data-part="cell"
                  style={
                    {
                      "--vibeui-chart-021-level": (
                        (matrix[dayIndex]?.[hourIndex] ?? 0) / max
                      ).toFixed(3),
                    } as CSSProperties
                  }
                />
              ))}
            </Fragment>
          ))}
        </div>
        <ul data-part="hours" aria-hidden="true">
          <li />
          {hours.map((hour, index) => (
            <li key={hour} data-part="hour">
              {index % 2 === 0 ? hour : ""}
            </li>
          ))}
        </ul>
        <p data-part="scale">
          реже
          {[0.05, 0.3, 0.55, 0.8, 1].map((level) => (
            <span
              key={level}
              data-part="step"
              aria-hidden="true"
              style={
                { "--vibeui-chart-021-level": String(level) } as CSSProperties
              }
            />
          ))}
          чаще
        </p>
        <p data-part="unit">
          Единица измерения: {unit}. Максимум клетки — {max}.
        </p>
        <table data-part="data">
          <caption>
            {title}, {unit}
          </caption>
          <thead>
            <tr>
              <th scope="col">День</th>
              {hours.map((hour) => (
                <th key={hour} scope="col">
                  {hour}:00
                </th>
              ))}
            </tr>
          </thead>
          <tbody>
            {days.map((day, dayIndex) => (
              <tr key={day}>
                <th scope="row">{day}</th>
                {hours.map((hour, hourIndex) => (
                  <td key={hour}>{matrix[dayIndex]?.[hourIndex] ?? 0}</td>
                ))}
              </tr>
            ))}
          </tbody>
        </table>
      </figure>
    </>
  )
}