import React from 'react'; import { DAYS_IN_WEEK, MILLISECONDS_IN_ONE_DAY, DAY_LABELS, MONTH_LABELS } from './constants.js'; import { shiftDate, getBeginningTimeForDate, convertToDate, getRange, } from "../../utils"; import {CalendarHeatmapProp} from "./CalendarHeatmapProp"; const SQUARE_SIZE = 10; const MONTH_LABEL_GUTTER_SIZE = 4; const CSS_PSEUDO_NAMESPACE = 'react-calendar-heatmap-'; function CalendarHeatmap({ values, startDate, endDate, gutterSize, horizontal, showMonthLabels, showWeekdayLabels, showOutOfRangeDays, tooltipDataAttrs, onClick, onMouseOver, onMouseLeave, transformDayElement }: CalendarHeatmapProp) { const cachedValues = {}; for (const i in values) { const value = values[i]; const date = convertToDate(value.date); const index = Math.floor((date.getTime() - getStartDateWithEmptyDays().getTime()) / MILLISECONDS_IN_ONE_DAY); cachedValues[index] = { date: value.date, event: value.event, className: classForValue(value.id), title: titleForValue(value.id), tooltipDataAttrs: getTooltipDataAttrsForValue(value.id), }; } function getDateDifferenceInDays() { const timeDiff = getEndDate().getTime() - convertToDate(startDate).getTime(); return Math.ceil(timeDiff / MILLISECONDS_IN_ONE_DAY); } function classForValue(value) { return value ? 'color-filled' : 'color-empty'; } function titleForValue(value) { return value; } function getSquareSizeWithGutter() { return SQUARE_SIZE + gutterSize; } function getMonthLabelSize() { if (!showMonthLabels) { return 0; } if (horizontal) { return SQUARE_SIZE + MONTH_LABEL_GUTTER_SIZE; } return 2 * (SQUARE_SIZE + MONTH_LABEL_GUTTER_SIZE); } function getWeekdayLabelSize() { if (!showWeekdayLabels) { return 0; } if (horizontal) { return 30; } return SQUARE_SIZE * 1.5; } function getStartDate() { return shiftDate(getEndDate(), -getDateDifferenceInDays() + 1); // +1 because endDate is inclusive } function getEndDate(): Date { return getBeginningTimeForDate(convertToDate(endDate)); } function getStartDateWithEmptyDays() { return shiftDate(getStartDate(), -getNumEmptyDaysAtStart()); } function getNumEmptyDaysAtStart() { return getStartDate().getDay(); } function getNumEmptyDaysAtEnd() { return DAYS_IN_WEEK - 1 - getEndDate().getDay(); } function getWeekCount() { const numDaysRoundedToWeek = getDateDifferenceInDays() + getNumEmptyDaysAtStart() + getNumEmptyDaysAtEnd(); return Math.ceil(numDaysRoundedToWeek / DAYS_IN_WEEK); } function getWeekWidth() { return DAYS_IN_WEEK * getSquareSizeWithGutter(); } function getWidth() { return ( getWeekCount() * getSquareSizeWithGutter() - (gutterSize - getWeekdayLabelSize()) ); } function getHeight() { return ( getWeekWidth() + (getMonthLabelSize() - gutterSize) + getWeekdayLabelSize() ); } function getValueForIndex(index) { if (cachedValues[index]) { console.log(index, cachedValues); return cachedValues[index].event; } return null; } function getClassNameForIndex(index) { if (cachedValues[index]) { return cachedValues[index].className; } return classForValue(null); } function getTitleForIndex(index) { if (cachedValues[index]) { return cachedValues[index].title; } return titleForValue ? titleForValue(null) : null; } function getTooltipDataAttrsForIndex(index) { if (cachedValues[index]) { return cachedValues[index].tooltipDataAttrs; } return getTooltipDataAttrsForValue({ date: null, count: null }); } function getTooltipDataAttrsForValue(value) { if (typeof tooltipDataAttrs === 'function') { return tooltipDataAttrs(value); } return tooltipDataAttrs; } function getTransformForWeek(weekIndex) { if (horizontal) { return `translate(${weekIndex * getSquareSizeWithGutter()}, 0)`; } return `translate(0, ${weekIndex * getSquareSizeWithGutter()})`; } function getTransformForWeekdayLabels() { if (horizontal) { return `translate(${SQUARE_SIZE}, ${getMonthLabelSize()})`; } return ""; } function getTransformForMonthLabels() { if (horizontal) { return `translate(${getWeekdayLabelSize()}, 0)`; } return `translate(${getWeekWidth() + MONTH_LABEL_GUTTER_SIZE}, ${getWeekdayLabelSize()})`; } function getTransformForAllWeeks() { if (horizontal) { return `translate(${getWeekdayLabelSize()}, ${getMonthLabelSize()})`; } return `translate(0, ${getWeekdayLabelSize()})`; } function getViewBox() { if (horizontal) { return `0 0 ${getWidth()} ${getHeight()}`; } return `0 0 ${getHeight()} ${getWidth()}`; } function getSquareCoordinates(dayIndex) { if (horizontal) { return [0, dayIndex * getSquareSizeWithGutter()]; } return [dayIndex * getSquareSizeWithGutter(), 0]; } function getWeekdayLabelCoordinates(dayIndex) { if (horizontal) { return [0, (dayIndex + 1) * SQUARE_SIZE + dayIndex * gutterSize]; } return [dayIndex * SQUARE_SIZE + dayIndex * gutterSize, SQUARE_SIZE]; } function getMonthLabelCoordinates(weekIndex) { if (horizontal) { return [ weekIndex * getSquareSizeWithGutter(), getMonthLabelSize() - MONTH_LABEL_GUTTER_SIZE, ]; } const verticalOffset = -2; return [0, (weekIndex + 1) * getSquareSizeWithGutter() + verticalOffset]; } function handleClick(value) { if (onClick) { onClick(value); } } function handleMouseOver(e, value) { if (onMouseOver) { onMouseOver(e, value); } } function handleMouseLeave(e, value) { if (onMouseLeave) { onMouseLeave(e, value); } } function renderSquare(dayIndex, index) { const indexOutOfRange = index < getNumEmptyDaysAtStart() || index >= getNumEmptyDaysAtStart() + getDateDifferenceInDays(); if (indexOutOfRange && !showOutOfRangeDays) { return null; } const [x, y] = getSquareCoordinates(dayIndex); const value = getValueForIndex(index); const rect = ( handleClick(value)} onMouseOver={(e) => handleMouseOver(e, value)} onMouseLeave={(e) => handleMouseLeave(e, value)} {...getTooltipDataAttrsForIndex(index)} > {getTitleForIndex(index)} ); return transformDayElement ? transformDayElement(rect, value as string, index) : rect; } function renderWeek(weekIndex) { return ( {getRange(DAYS_IN_WEEK).map((dayIndex) => renderSquare(dayIndex, weekIndex * DAYS_IN_WEEK + dayIndex), )} ); } function renderAllWeeks() { return getRange(getWeekCount()).map((weekIndex) => renderWeek(weekIndex)); } function renderMonthLabels() { if (!showMonthLabels) { return null; } const weekRange = getRange(getWeekCount() - 1); // don't render for last week, because label will be cut off return weekRange.map((weekIndex) => { const endOfWeek = shiftDate(getStartDateWithEmptyDays(), (weekIndex + 1) * DAYS_IN_WEEK); const [x, y] = getMonthLabelCoordinates(weekIndex); return endOfWeek.getDate() >= 1 && endOfWeek.getDate() <= DAYS_IN_WEEK ? ( {MONTH_LABELS[endOfWeek.getMonth()]} ) : null; }); } function renderWeekdayLabels() { if (!showWeekdayLabels) { return null; } return DAY_LABELS.map((weekdayLabel, dayIndex) => { const [x, y] = getWeekdayLabelCoordinates(dayIndex); const cssClasses = `${ horizontal ? '' : `${CSS_PSEUDO_NAMESPACE}small-text` } ${CSS_PSEUDO_NAMESPACE}weekday-label`; // eslint-disable-next-line no-bitwise return dayIndex & 1 ? ( {weekdayLabel} ) : null; }); } return ( {renderMonthLabels()} {renderAllWeeks()} {renderWeekdayLabels()} ); } export default CalendarHeatmap;