336 lines
10 KiB
TypeScript
336 lines
10 KiB
TypeScript
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 = (
|
|
<rect
|
|
key={index}
|
|
width={SQUARE_SIZE}
|
|
height={SQUARE_SIZE}
|
|
x={x}
|
|
y={y}
|
|
className={getClassNameForIndex(index)}
|
|
onClick={() => handleClick(value)}
|
|
onMouseOver={(e) => handleMouseOver(e, value)}
|
|
onMouseLeave={(e) => handleMouseLeave(e, value)}
|
|
{...getTooltipDataAttrsForIndex(index)}
|
|
>
|
|
<title>{getTitleForIndex(index)}</title>
|
|
</rect>
|
|
);
|
|
return transformDayElement ? transformDayElement(rect, value as string, index) : rect;
|
|
}
|
|
|
|
function renderWeek(weekIndex) {
|
|
return (
|
|
<g
|
|
key={weekIndex}
|
|
transform={getTransformForWeek(weekIndex)}
|
|
className={`${CSS_PSEUDO_NAMESPACE}week`}
|
|
>
|
|
{getRange(DAYS_IN_WEEK).map((dayIndex) =>
|
|
renderSquare(dayIndex, weekIndex * DAYS_IN_WEEK + dayIndex),
|
|
)}
|
|
</g>
|
|
);
|
|
}
|
|
|
|
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 ? (
|
|
<text key={weekIndex} x={x} y={y} className={`${CSS_PSEUDO_NAMESPACE}month-label`}>
|
|
{MONTH_LABELS[endOfWeek.getMonth()]}
|
|
</text>
|
|
) : 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 ? (
|
|
<text key={`${x}${y}`} x={x} y={y} className={cssClasses}>
|
|
{weekdayLabel}
|
|
</text>
|
|
) : null;
|
|
});
|
|
}
|
|
|
|
return (
|
|
<svg className="react-calendar-heatmap" viewBox={getViewBox()}>
|
|
<g
|
|
transform={getTransformForMonthLabels()}
|
|
className={`${CSS_PSEUDO_NAMESPACE}month-labels`}
|
|
>
|
|
{renderMonthLabels()}
|
|
</g>
|
|
<g
|
|
transform={getTransformForAllWeeks()}
|
|
className={`${CSS_PSEUDO_NAMESPACE}all-weeks`}
|
|
>
|
|
{renderAllWeeks()}
|
|
</g>
|
|
<g
|
|
transform={getTransformForWeekdayLabels()}
|
|
className={`${CSS_PSEUDO_NAMESPACE}weekday-labels`}
|
|
>
|
|
{renderWeekdayLabels()}
|
|
</g>
|
|
</svg>
|
|
);
|
|
}
|
|
|
|
export default CalendarHeatmap;
|