diaREact/resources/js/components/calendar/CalendarHeatmap.tsx

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;