/
zemars
/
it-dashboard
Обзор
Документация
Войти
/
zemars
/
it-dashboard
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
develop
frontend/src/components/WorkloadHeatmap/WorkloadHeatmap.jsx
153 строки
5 KB
Дмитрий
fix(docs): актуализировать risk_log и docstring init_db
26 май 2026, 22:07
26 май 2026, 22:07
9981d43
Код
Авторство
О чём код?
// Автор: Дмитрий Жещинский — тепловая карта загрузки. // Кастомная SVG-таблица (готового heatmap у Recharts нет): ось X — даты, // ось Y — исполнители, цвет = число активных тикетов. Tooltip — нативный. import { useMemo, useState } from 'react' function colorFor(value, maxValue) { if (!value) return '#f5f7fb' if (maxValue <= 0) return '#f5f7fb' const ratio = Math.min(1, value / maxValue) // от светло-синего к тёмно-красному через жёлтый if (ratio < 0.33) { // голубой const t = ratio / 0.33 const r = Math.round(220 - t * 100) const g = Math.round(235 - t * 30) const b = 250 return `rgb(${r},${g},${b})` } if (ratio < 0.66) { // жёлто-оранжевый const t = (ratio - 0.33) / 0.33 return `rgb(${245},${Math.round(180 - t * 40)},${Math.round(80 - t * 40)})` } // красный const t = (ratio - 0.66) / 0.34 return `rgb(${220 - Math.round(t * 30)},${60 - Math.round(t * 30)},${40})` } export default function WorkloadHeatmap({ items, summary, loading }) { const [hover, setHover] = useState(null) const { dates, assignees, lookup, maxValue } = useMemo(() => { if (!items || items.length === 0) { return { dates: [], assignees: [], lookup: {}, maxValue: 0 } } const dateSet = new Set() const assigneeSet = new Set() const lk = {} let maxv = 0 for (const it of items) { dateSet.add(it.date) assigneeSet.add(it.assignee) lk[`${it.assignee}|${it.date}`] = it.active_tickets if (it.active_tickets > maxv) maxv = it.active_tickets } return { dates: [...dateSet].sort(), assignees: [...assigneeSet].sort(), lookup: lk, maxValue: maxv, } }, [items]) if (loading) { return ( <div className="card"> <h3 className="section-title">Загрузка исполнителей</h3> <div className="empty-state">Загрузка данных…</div> </div> ) } if (!items || items.length === 0) { return ( <div className="card"> <h3 className="section-title">Загрузка исполнителей</h3> <div className="empty-state">Нет данных</div> </div> ) } const cellW = 18 const cellH = 24 const leftPad = 130 const topPad = 30 const width = leftPad + dates.length * cellW const height = topPad + assignees.length * cellH + 10 return ( <div className="card"> <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12 }}> <h3 className="section-title" style={{ margin: 0 }}>Загрузка исполнителей</h3> {summary && ( <div style={{ fontSize: 12, color: 'var(--color-text-muted)' }}> <span>среднее: <b>{summary.avg_load.toFixed(1)}</b></span> <span style={{ marginLeft: 14 }}>пик: <b>{summary.max_load}</b></span> <span style={{ marginLeft: 14 }}>разброс (σ): <b>{summary.load_stddev.toFixed(2)}</b></span> </div> )} </div> <div style={{ overflowX: 'auto' }}> <svg width={Math.max(width, 600)} height={height} style={{ fontSize: 11 }}> {/* Подписи дат (каждая 3-я) */} {dates.map((d, i) => ( i % 3 === 0 ? ( <text key={d} x={leftPad + i * cellW + cellW / 2} y={topPad - 8} textAnchor="middle" fill="#6b7385" > {d.slice(5)} </text> ) : null ))} {/* Подписи исполнителей */} {assignees.map((a, i) => ( <text key={a} x={leftPad - 6} y={topPad + i * cellH + cellH / 2 + 4} textAnchor="end" fill="#1c2536" > {a} </text> ))} {/* Ячейки */} {assignees.map((a, ai) => dates.map((d, di) => { const v = lookup[`${a}|${d}`] || 0 const color = colorFor(v, maxValue) return ( <rect key={`${a}-${d}`} x={leftPad + di * cellW} y={topPad + ai * cellH} width={cellW - 2} height={cellH - 2} fill={color} stroke="#fff" onMouseEnter={() => setHover({ a, d, v })} onMouseLeave={() => setHover(null)} > <title> {a} • {d} • {v} активных </title> </rect> ) }) )} </svg> </div> {hover && ( <div style={{ marginTop: 6, fontSize: 12, color: 'var(--color-text-muted)' }}> <b>{hover.a}</b> · {hover.d} · <b>{hover.v}</b> активных тикетов </div> )} </div> ) }