3.2. Аналитика
Модуль аналитики HRoom предоставляет развернутый инструментарий для анализа данных команд, ответов на опросы и различных метрик производительности. Этот модуль позволяет HR-менеджерам и руководителям получать подробную информацию о состоянии команд и отдельных сотрудников.
Основные компоненты
- Teams - аналитика по командам с тепловыми картами метрик
- Answers - анализ ответов на опросы по метрикам и подметрикам
- Stats - агрегированная статистика по ключевым показателям
Архитектура модуля аналитики
Модуль аналитики организован по функциональному принципу и включает следующую структуру:
src/pages/Analytics/
├── components/ # Компоненты пользовательского интерфейса
│ ├── Teams/ # Компоненты для анализа команд
│ ├── Answers/ # Компоненты для анализа ответов
│ ├── EmptyStates/ # Компоненты для отображения пустых состояний
│ └── Stats.tsx # Компонент статистики
├── hooks/ # Пользовательские хуки модуля
│ ├── useAnswers.ts # Хук для работы с данными ответов
│ ├── useAnswersFiltering.ts # Хук для фильтрации ответов
│ └── useWindowSize.ts # Хук для адаптивности интерфейса
├── services/ # Сервисы для работы с данными
│ └── teamsService.ts # Сервис для получения данных команд
├── types/ # Типы TypeScript для модуля
│ └── teams.ts # Типы для команд и их метрик
├── utils/ # Утилиты и вспомогательные функции
│ ├── metricIcons.ts # Иконки для различных метрик
│ ├── sortAnswers.ts # Функции сортировки ответов
│ └── teams.ts # Утилиты для работы с данными команд
└── index.tsx # Главный компонент страницы аналитики
Анализ команд
Компонент анализа команд предоставляет инструменты для визуализации и сравнения различных метрик по командам в виде тепловых карт и списков.
Компоненты для анализа команд
TeamsList- список команд с возможностью фильтрации и сортировкиHeatmap- тепловая карта метрик по командамHeatmapGrid- сетка тепловой карты для визуализации метрикHeatmapCell- ячейка с данными метрики и визуальным представлениемMetricHeader- заголовок с названием и иконкой метрикиMetricsList- список доступных метрик для выбораTooltip- всплывающая подсказка с подробной информацией по метрике
Принцип работы тепловой карты команд
Тепловая карта визуализирует различные метрики по командам, используя цветовую градацию для отображения значений. Принцип работы включает следующие этапы:
- Получение данных - с использованием API запрашиваются данные метрик для команд за выбранный период
- Нормализация данных - значения метрик нормализуются в диапазоне от 0 до 100 для единообразного отображения
- Расчет цветовой интенсивности - для каждой ячейки тепловой карты рассчитывается цвет в зависимости от значения метрики
- Отображение тепловой карты - данные визуализируются в виде сетки, где строки соответствуют командам, а столбцы - метрикам
- Интерактивность - при наведении на ячейку отображается детальная информация в виде всплывающей подсказки
Ниже представлен пример компонента сетки тепловой карты:
// Компонент HeatmapGrid из src/pages/Analytics/components/Teams/HeatmapGrid.tsx
export const HeatmapGrid = ({ teams, metrics, data }) => {
return (
<div className="heatmap-grid">
{/* Заголовки метрик */}
<div className="metrics-headers">
{metrics.map(metric => (
<MetricHeader
key={metric.id}
name={metric.name}
icon={metric.icon}
/>
))}
</div>
{/* Данные по командам */}
{teams.map(team => (
<div className="team-row" key={team.id}>
<div className="team-name">{team.name}</div>
{metrics.map(metric => {
const value = data[team.id]?.[metric.id] || 0;
return (
<HeatmapCell
key={`${team.id}-${metric.id}`}
value={value}
teamId={team.id}
metricId={metric.id}
/>
);
})}
</div>
))}
</div>
);
};
Компонент ячейки тепловой карты
Ячейка тепловой карты (HeatmapCell) отвечает за визуализацию конкретного значения метрики. Принцип работы:
- Получение значения метрики для конкретной команды
- Определение цвета ячейки на основе значения (от красного для низких значений до зеленого для высоких)
- Отображение ячейки с соответствующим цветом и прозрачностью
- При наведении - отображение всплывающего окна с деталями
// Пример компонента HeatmapCell из src/pages/Analytics/components/Teams/HeatmapCell.tsx
export const HeatmapCell = ({ value, teamId, metricId }) => {
const [showTooltip, setShowTooltip] = useState(false);
const cellRef = useRef(null);
// Определение цвета ячейки на основе значения (от 0 до 100)
const getBackgroundColor = (value) => {
// Для значений ниже 50 - оттенки красного
if (value < 50) {
const intensity = 100 - Math.round(value * 2);
return `rgba(255, ${intensity}, ${intensity}, ${0.3 + value / 150})`;
}
// Для значений выше 50 - оттенки зеленого
else {
const intensity = Math.round((value - 50) * 2);
return `rgba(${100 - intensity}, 255, ${100 - intensity}, ${0.3 + value / 150})`;
}
};
return (
<div
className="heatmap-cell"
ref={cellRef}
style={{ backgroundColor: getBackgroundColor(value) }}
onMouseEnter={() => setShowTooltip(true)}
onMouseLeave={() => setShowTooltip(false)}
>
{value > 0 && <span className="cell-value">{value}</span>}
{showTooltip && (
<Tooltip
teamId={teamId}
metricId={metricId}
value={value}
anchorEl={cellRef.current}
/>
)}
</div>
);
};
Всплывающие подсказки
Компонент Tooltip отображает детальную информацию о метрике при наведении на ячейку. Он включает:
- Название команды и метрики
- Текущее значение метрики
- Изменение по сравнению с предыдущим периодом
- График тренда за последние периоды
- Краткие рекомендации по улучшению показателя
Фильтрация данных команд
Модуль аналитики команд предоставляет различные инструменты фильтрации:
- Фильтрация по отделу
- Фильтрация по периоду (неделя, месяц, квартал, год)
- Фильтрация по диапазону значений метрик
- Поиск по названию команды
Анализ ответов на опросы
Компонент анализа ответов позволяет исследовать ответы сотрудников на вопросы опросов, группировать их по метрикам и подметрикам, а также выявлять закономерности и тренды.
Компоненты для анализа ответов
AnswersList- список ответов с фильтрацией и группировкойAnswerCard- карточка с детальной информацией по ответуAnswerFilters- компонент с фильтрами для ответовSortControls- элементы управления сортировкойMetricGroup- группа метрик для группировки ответовSubmetricGroup- группа подметрик для более детального анализаQuestionCard- карточка с вопросом и распределением ответов
Хук useAnswers
Хук useAnswers отвечает за получение и обработку данных ответов на опросы. Он выполняет следующие функции:
- Получение ответов по идентификатору опроса
- Группировка ответов по метрикам и подметрикам
- Расчет статистических показателей (средние значения, медианы, распределения)
- Кэширование полученных данных для оптимизации производительности
- Обработка состояний загрузки и ошибок
// Пример хука useAnswers из src/pages/Analytics/hooks/useAnswers.ts
export const useAnswers = (surveyId) => {
const [answers, setAnswers] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchAnswers = async () => {
try {
setIsLoading(true);
const data = await answersService.getAnswersBySurveyId(surveyId);
setAnswers(data);
setError(null);
} catch (err) {
setError(err.message || 'Ошибка при загрузке ответов');
} finally {
setIsLoading(false);
}
};
if (surveyId) {
fetchAnswers();
}
}, [surveyId]);
// Группировка ответов по метрикам
const groupedAnswers = useMemo(() => {
return groupAnswersByMetric(answers);
}, [answers]);
// Расчет статистических показателей
const statistics = useMemo(() => {
return calculateStatistics(answers);
}, [answers]);
return {
answers,
groupedAnswers,
statistics,
isLoading,
error
};
};
Фильтрация и сортировка ответов
Модуль анализа ответов предоставляет развернутые возможности для фильтрации и сортировки данных:
Хук useAnswersFiltering
Хук useAnswersFiltering реализует логику фильтрации ответов по различным критериям:
// Упрощенный пример хука useAnswersFiltering
export const useAnswersFiltering = (answers) => {
const [filters, setFilters] = useState({
metrics: [],
submetrics: [],
dateRange: { start: null, end: null },
teams: [],
departments: [],
minScore: 0,
maxScore: 100
});
const [sortConfig, setSortConfig] = useState({
field: 'date',
direction: 'desc'
});
// Применение фильтров к ответам
const filteredAnswers = useMemo(() => {
return answers.filter(answer => {
// Фильтрация по метрикам
if (filters.metrics.length > 0 && !filters.metrics.includes(answer.metricId)) {
return false;
}
// Фильтрация по подметрикам
if (filters.submetrics.length > 0 && !filters.submetrics.includes(answer.submetricId)) {
return false;
}
// Фильтрация по дате
if (filters.dateRange.start && new Date(answer.submittedAt) < filters.dateRange.start) {
return false;
}
if (filters.dateRange.end && new Date(answer.submittedAt) > filters.dateRange.end) {
return false;
}
// Фильтрация по командам
if (filters.teams.length > 0 && !filters.teams.includes(answer.teamId)) {
return false;
}
// Фильтрация по отделам
if (filters.departments.length > 0 && !filters.departments.includes(answer.departmentId)) {
return false;
}
// Фильтрация по оценке
if (answer.score < filters.minScore || answer.score > filters.maxScore) {
return false;
}
return true;
});
}, [answers, filters]);
// Сортировка отфильтрованных ответов
const sortedAnswers = useMemo(() => {
return sortAnswers(filteredAnswers, sortConfig);
}, [filteredAnswers, sortConfig]);
// Функция установки фильтров
const setFilter = (filterType, value) => {
setFilters(prev => ({
...prev,
[filterType]: value
}));
};
// Функция установки сортировки
const setSorting = (field, direction) => {
setSortConfig({ field, direction });
};
return {
filters,
setFilter,
sortConfig,
setSorting,
filteredAnswers: sortedAnswers
};
};
Функции сортировки
Для упорядочивания ответов используются различные алгоритмы сортировки, определенные в утилите sortAnswers.ts:
// Пример функции сортировки из src/pages/Analytics/utils/sortAnswers.ts
export const sortAnswers = (answers, sortConfig) => {
const { field, direction } = sortConfig;
return [...answers].sort((a, b) => {
if (field === 'date') {
const dateA = new Date(a.submittedAt).getTime();
const dateB = new Date(b.submittedAt).getTime();
return direction === 'asc' ? dateA - dateB : dateB - dateA;
}
if (field === 'score') {
return direction === 'asc' ? a.score - b.score : b.score - a.score;
}
if (field === 'questionText') {
return direction === 'asc'
? a.questionText.localeCompare(b.questionText)
: b.questionText.localeCompare(a.questionText);
}
if (field === 'metricName') {
return direction === 'asc'
? a.metricName.localeCompare(b.metricName)
: b.metricName.localeCompare(a.metricName);
}
// Сортировка по количеству ответов
if (field === 'responsesCount') {
return direction === 'asc'
? a.responsesCount - b.responsesCount
: b.responsesCount - a.responsesCount;
}
// По умолчанию возвращаем без изменений
return 0;
});
};
Группировка ответов по метрикам
Компонент MetricGroup отвечает за группировку и отображение ответов по метрикам. Он организует ответы в иерархическую структуру:
// Пример компонента MetricGroup из src/pages/Analytics/components/Answers/MetricGroup.tsx
export const MetricGroup = ({ metric, submetrics, answers }) => {
const [expanded, setExpanded] = useState(true);
// Расчет агрегированных показателей для метрики
const averageScore = useMemo(() => {
const scores = answers.map(a => a.score);
return scores.length ?
scores.reduce((acc, score) => acc + score, 0) / scores.length :
0;
}, [answers]);
// Группировка ответов по подметрикам
const answersBySubmetric = useMemo(() => {
return submetrics.reduce((acc, submetric) => {
acc[submetric.id] = answers.filter(
answer => answer.submetricId === submetric.id
);
return acc;
}, {});
}, [submetrics, answers]);
return (
<div className="metric-group">
<div
className="metric-header"
onClick={() => setExpanded(!expanded)}
>
<div className="metric-icon" style={{ backgroundColor: metric.color }}>
{/* Иконка метрики */}
</div>
<div className="metric-title">
<h3>{metric.name}</h3>
<span className="answers-count">
{answers.length} ответов | Средний балл: {averageScore.toFixed(1)}
</span>
</div>
<div className="expand-icon">
{expanded ? '▼' : '►'}
</div>
</div>
{expanded && (
<div className="submetrics-container">
{submetrics.map(submetric => (
<SubmetricGroup
key={submetric.id}
submetric={submetric}
answers={answersBySubmetric[submetric.id] || []}
/>
))}
</div>
)}
</div>
);
};
Карточки ответов и вопросов
Компоненты AnswerCard и QuestionCard отвечают за отображение детальной информации об ответах и вопросах:
AnswerCard- отображает информацию о конкретном ответе, включая текст ответа, метрику, оценку и дополнительную информациюQuestionCard- отображает вопрос, распределение ответов и статистические показатели
// Пример компонента QuestionCard из src/pages/Analytics/components/Answers/QuestionCard.tsx
export const QuestionCard = ({ question, answers }) => {
// Распределение ответов по значениям (для вопросов с выбором оценки)
const distribution = useMemo(() => {
const dist = { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0 };
answers.forEach(answer => {
if (answer.value >= 1 && answer.value <= 5) {
dist[answer.value]++;
}
});
return dist;
}, [answers]);
// Расчет процентного соотношения оценок
const percentages = useMemo(() => {
const total = Object.values(distribution).reduce((sum, count) => sum + count, 0);
return Object.entries(distribution).reduce((acc, [score, count]) => {
acc[score] = total > 0 ? (count / total) * 100 : 0;
return acc;
}, {});
}, [distribution]);
return (
<div className="question-card">
<h4 className="question-text">{question.text}</h4>
<div className="question-stats">
<div className="responses-count">
Всего ответов: {answers.length}
</div>
<div className="average-score">
Средняя оценка: {
(answers.reduce((sum, a) => sum + a.value, 0) /
(answers.length || 1)).toFixed(1)
}
</div>
</div>
<div className="score-distribution">
{Object.entries(percentages).map(([score, percentage]) => (
<div key={score} className="score-bar-container">
<span className="score-label">{score}</span>
<div className="score-bar">
<div
className="score-bar-fill"
style={{ width: `${percentage}%` }}
></div>
</div>
<span className="score-percentage">
{percentage.toFixed(1)}%
</span>
</div>
))}
</div>
{/* Комментарии к вопросу, если есть */}
{answers.some(a => a.comment) && (
<div className="question-comments">
<h5>Комментарии:</h5>
<ul>
{answers
.filter(a => a.comment)
.map((a, idx) => (
<li key={idx}>{a.comment}</li>
))
}
</ul>
</div>
)}
</div>
);
};
Статистика и агрегированные показатели
Компонент Stats предоставляет агрегированные статистические показатели для быстрого обзора ключевых метрик:
Компонент агрегированной статистики
Компонент Stats отображает обобщенные статистические данные в виде карточек с ключевыми показателями:
// Пример компонента Stats из src/pages/Analytics/components/Stats.tsx
export const Stats = ({ data }) => {
// Вычисление общей статистики
const statistics = useMemo(() => [
{
label: 'Средний уровень вовлеченности',
value: data.engagementScore ? `${data.engagementScore.toFixed(1)}%` : 'N/A',
change: data.engagementChange,
icon: 'engagement',
color: getScoreColor(data.engagementScore)
},
{
label: 'Количество активных опросов',
value: data.activeSurveys || 0,
change: data.surveysChange,
icon: 'surveys',
color: '#6366f1'
},
{
label: 'Процент участия в опросах',
value: data.participationRate ? `${data.participationRate.toFixed(1)}%` : 'N/A',
change: data.participationChange,
icon: 'participation',
color: getScoreColor(data.participationRate)
},
{
label: 'Метрики с критическими показателями',
value: data.criticalMetrics || 0,
change: data.criticalMetricsChange,
icon: 'warning',
color: data.criticalMetrics > 0 ? '#ef4444' : '#10b981'
}
], [data]);
return (
<div className="stats-container">
{statistics.map((stat, index) => (
<StatBar
key={index}
label={stat.label}
value={stat.value}
change={stat.change}
icon={stat.icon}
color={stat.color}
/>
))}
</div>
);
};
Компонент StatBar
Компонент StatBar отображает отдельный статистический показатель с визуальным представлением и индикацией изменения:
// Пример компонента StatBar из src/pages/Analytics/components/StatBar.tsx
export const StatBar = ({ label, value, change, icon, color }) => {
// Определение стиля для индикатора изменения
const getChangeStyle = (change) => {
if (!change || change === 0) return 'neutral';
return change > 0 ? 'positive' : 'negative';
};
// Форматирование значения изменения
const formatChange = (change) => {
if (!change || change === 0) return '0%';
const prefix = change > 0 ? '+' : '';
return `${prefix}${change.toFixed(1)}%`;
};
return (
<div className="stat-bar">
<div className="stat-icon" style={{ backgroundColor: color }}>
{/* Иконка статистики */}
</div>
<div className="stat-content">
<div className="stat-label">{label}</div>
<div className="stat-value">{value}</div>
{change !== undefined && (
<div className={`stat-change ${getChangeStyle(change)}`}>
{formatChange(change)}
</div>
)}
</div>
</div>
);
};
Адаптивность интерфейса
Модуль аналитики адаптируется под различные размеры экрана с помощью хука useWindowSize, который определяет текущие размеры окна браузера и позволяет компонентам реагировать на изменения:
// Пример хука useWindowSize из src/pages/Analytics/hooks/useWindowSize.ts
export const useWindowSize = () => {
const [windowSize, setWindowSize] = useState({
width: undefined,
height: undefined,
});
useEffect(() => {
// Функция обработки изменения размера окна
function handleResize() {
setWindowSize({
width: window.innerWidth,
height: window.innerHeight,
});
}
// Установка начальных размеров
handleResize();
// Добавление обработчика события изменения размера окна
window.addEventListener('resize', handleResize);
// Очистка обработчика при размонтировании компонента
return () => window.removeEventListener('resize', handleResize);
}, []);
return windowSize;
};
Адаптивное отображение компонентов
На основе данных о размере окна компоненты аналитики адаптируют свое отображение:
- На маленьких экранах отображается упрощенный вид без тепловых карт
- Для планшетов и больших экранов используется полноценное отображение с тепловыми картами
- Количество отображаемых метрик и размер карточек меняются в зависимости от ширины экрана
Пустые состояния и обработка ошибок
Модуль аналитики включает компоненты для корректной обработки различных состояний интерфейса:
AnalyticsEmpty- компонент, отображаемый при отсутствии данных для анализа- Обработчики ошибок при загрузке данных с предложениями по решению проблемы
- Индикаторы загрузки для улучшения пользовательского опыта во время получения данных
Интеграция с API
Модуль аналитики взаимодействует с различными API-сервисами для получения данных:
metricsApi- API для получения метрик и статистикиteamsApi- API для работы с данными командanswersService- сервис для получения и анализа ответов на опросы
API-интеграции инкапсулированы в специализированные сервисы, что обеспечивает более чистую архитектуру и упрощает поддержку кода. Также реализована обработка ошибок и повторные попытки при сбоях в сетевых запросах.
Главный компонент страницы аналитики
Основной компонент Analytics объединяет все описанные выше компоненты и служит точкой входа в модуль аналитики:
// Пример основного компонента Analytics из src/pages/Analytics/index.tsx
export const Analytics = () => {
const [activeTab, setActiveTab] = useState('teams');
const [selectedSurveyId, setSelectedSurveyId] = useState(null);
const { data, isLoading, error } = useAnalyticsData();
// Обработка состояний загрузки и ошибок
if (isLoading) {
return <LoadingSpinner />;
}
if (error) {
return <ErrorMessage message={error} />;
}
if (!data) {
return <AnalyticsEmpty />;
}
return (
<div className="analytics-container">
<h1>Аналитика</h1>
<Stats data={data.statistics} />
<div className="analytics-tabs">
<div className="tab-buttons">
<button
className={activeTab === 'teams' ? 'active' : ''}
onClick={() => setActiveTab('teams')}
>
Команды
</button>
<button
className={activeTab === 'answers' ? 'active' : ''}
onClick={() => setActiveTab('answers')}
>
Ответы на опросы
</button>
</div>
<div className="tab-content">
{activeTab === 'teams' && (
<div className="teams-container">
<Heatmap
teams={data.teams}
metrics={data.metrics}
heatmapData={data.heatmapData}
/>
</div>
)}
{activeTab === 'answers' && (
<div className="answers-container">
<div className="survey-selector">
<label>Выберите опрос:</label>
<select
value={selectedSurveyId || ''}
onChange={(e) => setSelectedSurveyId(e.target.value)}
>
<option value="">Выберите опрос</option>
{data.surveys.map(survey => (
<option key={survey.id} value={survey.id}>
{survey.title}
</option>
))}
</select>
</div>
{selectedSurveyId && (
<AnswersList surveyId={selectedSurveyId} />
)}
</div>
)}
</div>
</div>
</div>
);
};
Выводы
Модуль аналитики HRoom представляет собой мощный инструмент для анализа данных команд и результатов опросов. Он предоставляет HR-менеджерам и руководителям компаний возможность глубокого анализа различных метрик и показателей, помогая выявлять как проблемные зоны, так и сильные стороны команд.
Ключевые преимущества модуля аналитики:
- Интуитивно понятная визуализация данных с помощью тепловых карт
- Гибкие инструменты фильтрации и сортировки
- Детальный анализ ответов на опросы с группировкой по метрикам
- Адаптивный дизайн для работы на различных устройствах
- Оптимизированная производительность с использованием кэширования и мемоизации
Модуль интегрируется с другими частями приложения, обеспечивая целостный подход к управлению данными о командах и опросах, и предоставляет инструменты для принятия обоснованных управленческих решений на базе полученной аналитики.