UNPKG

react-native-chart-kit

Version:

Beautiful React Native charts for dashboards, reports, and data-rich mobile apps.

108 lines (107 loc) 4.66 kB
const daysInWeek = 7; const defaultMonthLabelHeight = 18; const defaultWeekdayLabelWidth = 28; const millisecondsInOneDay = 24 * 60 * 60 * 1000; const clampInteger = (value, min, max) => { if (!Number.isFinite(value)) { return min; } return Math.min(Math.max(Math.floor(value), min), max); }; const shiftDate = (date, numDays) => { const shiftedDate = new Date(date); shiftedDate.setDate(shiftedDate.getDate() + numDays); return shiftedDate; }; const getDayOffset = (date, weekStartsOn) => (date.getDay() - weekStartsOn + daysInWeek) % daysInWeek; const getDayIndex = (date, startDate) => Math.round((date.valueOf() - startDate.valueOf()) / millisecondsInOneDay); export const buildContributionHeatmap = ({ data, cellSize, gutterSize = 3, weekStartsOn = 0, showOutOfRangeDays = false, monthLabelHeight = defaultMonthLabelHeight, weekdayLabelWidth = defaultWeekdayLabelWidth }) => { const safeCellSize = Number.isFinite(cellSize) && cellSize > 0 ? cellSize : 0; const safeGutterSize = Number.isFinite(gutterSize) && gutterSize >= 0 ? gutterSize : 0; const safeWeekStartsOn = clampInteger(weekStartsOn, 0, daysInWeek - 1); const safeMonthLabelHeight = Number.isFinite(monthLabelHeight) && monthLabelHeight > 0 ? monthLabelHeight : 0; const safeWeekdayLabelWidth = Number.isFinite(weekdayLabelWidth) && weekdayLabelWidth > 0 ? weekdayLabelWidth : 0; const startOffset = getDayOffset(data.startDate, safeWeekStartsOn); const totalSlots = startOffset + data.days.length; const endOffset = totalSlots % daysInWeek === 0 ? 0 : daysInWeek - (totalSlots % daysInWeek); const weekCount = Math.ceil((totalSlots + endOffset) / daysInWeek); const step = safeCellSize + safeGutterSize; const firstSlotDate = shiftDate(data.startDate, -startOffset); const daysByIndex = new Map(data.days.map((day) => [day.index, day])); const cells = Array.from({ length: weekCount * daysInWeek }, (_, slotIndex) => { const weekIndex = Math.floor(slotIndex / daysInWeek); const weekdayIndex = slotIndex % daysInWeek; const date = shiftDate(firstSlotDate, slotIndex); const inRangeIndex = getDayIndex(date, data.startDate); const day = inRangeIndex >= 0 && inRangeIndex < data.days.length ? daysByIndex.get(inRangeIndex) : undefined; const outOfRange = day === undefined; const cell = { index: day?.index ?? -1, date, value: day?.value ?? null, defined: day?.defined ?? false, outOfRange, weekIndex, weekdayIndex, x: safeWeekdayLabelWidth + weekIndex * step, y: safeMonthLabelHeight + weekdayIndex * step, size: safeCellSize }; if (day?.raw !== undefined) { cell.raw = day.raw; } return cell; }).filter((cell) => showOutOfRangeDays || !cell.outOfRange); const visibleValues = cells .map((cell) => cell.value) .filter((value) => value !== null && Number.isFinite(value)); const valueMin = visibleValues.length > 0 ? Math.min(...visibleValues) : 0; const valueMax = visibleValues.length > 0 ? Math.max(...visibleValues) : 0; const monthLabels = []; const seenMonths = new Set(); data.days.forEach((day) => { if (day.date.getDate() !== 1 && day.index !== 0) { return; } const slotIndex = startOffset + day.index; const weekIndex = Math.floor(slotIndex / daysInWeek); const key = `${day.date.getFullYear()}-${day.date.getMonth()}`; if (seenMonths.has(key)) { return; } seenMonths.add(key); monthLabels.push({ key, date: day.date, monthIndex: day.date.getMonth(), x: safeWeekdayLabelWidth + weekIndex * step, y: Math.max(10, safeMonthLabelHeight - 6) }); }); const weekdayLabels = [1, 3, 5].map((weekdayIndex) => { const dayIndex = (safeWeekStartsOn + weekdayIndex) % daysInWeek; return { key: `weekday-${weekdayIndex}`, weekdayIndex, dayIndex, x: 0, y: safeMonthLabelHeight + weekdayIndex * step + safeCellSize * 0.75 }; }); return { cells, monthLabels, weekdayLabels, width: safeWeekdayLabelWidth + Math.max(0, weekCount * step - safeGutterSize), height: safeMonthLabelHeight + Math.max(0, daysInWeek * step - safeGutterSize), weekCount, valueMin, valueMax }; };