react-native-chart-kit
Version:
Beautiful React Native charts for dashboards, reports, and data-rich mobile apps.
108 lines (107 loc) • 4.66 kB
JavaScript
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
};
};