// @ts-nocheck import React, {Fragment} from 'react'; import PropTypes from 'prop-types'; import memoizeOne from 'memoize-one'; import {DAYS_IN_WEEK, MILLISECONDS_IN_ONE_DAY, DAY_LABELS, MONTH_LABELS} from './constants'; import { dateNDaysAgo, shiftDate, getBeginningTimeForDate, convertToDate, getRange, } from './helpers'; import * as dFns from 'date-fns'; import {MonthLabels} from "@/heatmap/b"; const SQUARE_SIZE = 10; const MONTH_LABEL_GUTTER_SIZE = 4; export const CSS_PSEDUO_NAMESPACE = 'react-calendar-heatmap-'; class CalendarHeatmap extends React.Component { getDateDifferenceInDays() { const { startDate, endDate } = this.props; return dFns.differenceInCalendarDays(endDate, startDate); } getSquareSizeWithGutter() { return SQUARE_SIZE + this.props.gutterSize; } getMonthLabelSize() { if (!this.props.showMonthLabels) { return 0; } if (this.props.horizontal) { return SQUARE_SIZE + MONTH_LABEL_GUTTER_SIZE; } return 2 * (SQUARE_SIZE + MONTH_LABEL_GUTTER_SIZE); } getWeekdayLabelSize() { if (!this.props.showWeekdayLabels) { return 0; } if (this.props.horizontal) { return 30; } return SQUARE_SIZE * 1.5; } getStartDate() { return shiftDate(this.getEndDate(), -this.getDateDifferenceInDays() + 1); // +1 because endDate is inclusive } getEndDate() { return getBeginningTimeForDate(convertToDate(this.props.endDate)); } getStartDateWithEmptyDays() { return shiftDate(this.getStartDate(), -this.getNumEmptyDaysAtStart()); } getNumEmptyDaysAtStart() { return this.getStartDate().getDay(); } getNumEmptyDaysAtEnd() { return DAYS_IN_WEEK - 1 - this.getEndDate().getDay(); } getWeekCount() { const numDaysRoundedToWeek = this.getDateDifferenceInDays() + this.getNumEmptyDaysAtStart() + this.getNumEmptyDaysAtEnd(); return Math.ceil(numDaysRoundedToWeek / DAYS_IN_WEEK); } getWeekWidth() { return DAYS_IN_WEEK * this.getSquareSizeWithGutter(); } getWidth() { return ( this.getWeekCount() * this.getSquareSizeWithGutter() - (this.props.gutterSize - this.getWeekdayLabelSize()) ); } getHeight() { return ( this.getWeekWidth() + (this.getMonthLabelSize() - this.props.gutterSize) + this.getWeekdayLabelSize() ); } getValueCache = memoizeOne((props) => props.values.reduce((memo, value) => { const date = convertToDate(value.date); const index = Math.floor((date - this.getStartDateWithEmptyDays()) / MILLISECONDS_IN_ONE_DAY); // eslint-disable-next-line no-param-reassign memo[index] = { value, className: this.props.classForValue(value), title: this.props.titleForValue ? this.props.titleForValue(value) : null, tooltipDataAttrs: this.getTooltipDataAttrsForValue(value), }; return memo; }, {}), ); getValueForIndex(index) { if (this.valueCache[index]) { return this.valueCache[index].value; } return null; } getClassNameForIndex(index) { if (this.valueCache[index]) { return this.valueCache[index].className; } return this.props.classForValue(null); } getTitleForIndex(index) { if (this.valueCache[index]) { return this.valueCache[index].title; } return this.props.titleForValue ? this.props.titleForValue(null) : null; } getTooltipDataAttrsForIndex(index) { if (this.valueCache[index]) { return this.valueCache[index].tooltipDataAttrs; } return this.getTooltipDataAttrsForValue({ date: null, count: null }); } getTooltipDataAttrsForValue(value) { const {tooltipDataAttrs} = this.props; if (typeof tooltipDataAttrs === 'function') { return tooltipDataAttrs(value); } return tooltipDataAttrs; } getTransformForWeek(weekIndex) { if (this.props.horizontal) { return `translate(${weekIndex * this.getSquareSizeWithGutter()}, 0)`; } return `translate(0, ${weekIndex * this.getSquareSizeWithGutter()})`; } getTransformForWeekdayLabels() { if (this.props.horizontal) { return `translate(${SQUARE_SIZE}, ${this.getMonthLabelSize()})`; } return null; } getTransformForMonthLabels() { if (this.props.horizontal) { return `translate(${this.getWeekdayLabelSize()}, 0)`; } return `translate(${this.getWeekWidth() + MONTH_LABEL_GUTTER_SIZE}, ${this.getWeekdayLabelSize()})`; } getTransformForAllWeeks() { if (this.props.horizontal) { return `translate(${this.getWeekdayLabelSize()}, ${this.getMonthLabelSize()})`; } return `translate(0, ${this.getWeekdayLabelSize()})`; } getViewBox() { if (this.props.horizontal) { return `0 0 ${this.getWidth()} ${this.getHeight()}`; } return `0 0 ${this.getHeight()} ${this.getWidth()}`; } getSquareCoordinates(dayIndex) { if (this.props.horizontal) { return [0, dayIndex * this.getSquareSizeWithGutter()]; } return [dayIndex * this.getSquareSizeWithGutter(), 0]; } getWeekdayLabelCoordinates(dayIndex) { if (this.props.horizontal) { return [0, (dayIndex + 1) * SQUARE_SIZE + dayIndex * this.props.gutterSize]; } return [dayIndex * SQUARE_SIZE + dayIndex * this.props.gutterSize, SQUARE_SIZE]; } getMonthLabelCoordinates(weekIndex) { if (this.props.horizontal) { return [ weekIndex * this.getSquareSizeWithGutter(), this.getMonthLabelSize() - MONTH_LABEL_GUTTER_SIZE, ]; } const verticalOffset = -2; return [0, (weekIndex + 1) * this.getSquareSizeWithGutter() + verticalOffset]; } handleClick(value) { if (this.props.onClick) { this.props.onClick(value); } } handleMouseOver(e, value) { if (this.props.onMouseOver) { this.props.onMouseOver(e, value); } } handleMouseLeave(e, value) { if (this.props.onMouseLeave) { this.props.onMouseLeave(e, value); } } renderSquare(dayIndex, index) { const indexOutOfRange = index < this.getNumEmptyDaysAtStart() || index >= this.getNumEmptyDaysAtStart() + this.getDateDifferenceInDays(); if (indexOutOfRange && !this.props.showOutOfRangeDays) { return null; } const [x, y] = this.getSquareCoordinates(dayIndex); const value = this.getValueForIndex(index); const rect = ( // eslint-disable-next-line jsx-a11y/mouse-events-have-key-events this.handleClick(value)} onMouseOver={(e) => this.handleMouseOver(e, value)} onMouseLeave={(e) => this.handleMouseLeave(e, value)} {...this.getTooltipDataAttrsForIndex(index)} > {this.getTitleForIndex(index)} ); const {transformDayElement} = this.props; return transformDayElement ? transformDayElement(rect, value, index) : rect; } renderWeek(weekIndex) { return ( {getRange(DAYS_IN_WEEK).map((dayIndex) => this.renderSquare(dayIndex, weekIndex * DAYS_IN_WEEK + dayIndex), )} ); } renderAllWeeks() { return getRange(this.getWeekCount()).map((weekIndex) => this.renderWeek(weekIndex)); } renderMonthLabels() { if (!this.props.showMonthLabels) { return null; } const adapter = (date) => { const [x, y] = this.getMonthLabelCoordinates(dFns.differenceInWeeks(date, this.props.startDate)); return { x, y } }; return <> {/*{...a}*/} } renderWeekdayLabels() { if (!this.props.showWeekdayLabels) { return null; } return this.props.weekdayLabels.map((weekdayLabel, dayIndex) => { const [x, y] = this.getWeekdayLabelCoordinates(dayIndex); const cssClasses = `${ this.props.horizontal ? '' : `${CSS_PSEDUO_NAMESPACE}small-text` } ${CSS_PSEDUO_NAMESPACE}weekday-label`; // eslint-disable-next-line no-bitwise return dayIndex & 1 ? ( {weekdayLabel} ) : null; }); } render() { this.valueCache = this.getValueCache(this.props); return ( {this.renderMonthLabels()} {this.renderAllWeeks()} {this.renderWeekdayLabels()} ); } } CalendarHeatmap.propTypes = { values: PropTypes.arrayOf( PropTypes.shape({ date: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.instanceOf(Date)]) .isRequired, }).isRequired, ).isRequired, // array of objects with date and arbitrary metadata numDays: PropTypes.number, // number of days back from endDate to show startDate: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.instanceOf(Date)]), // start of date range endDate: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.instanceOf(Date)]), // end of date range gutterSize: PropTypes.number, // size of space between squares horizontal: PropTypes.bool, // whether to orient horizontally or vertically showMonthLabels: PropTypes.bool, // whether to show month labels showWeekdayLabels: PropTypes.bool, // whether to show weekday labels showOutOfRangeDays: PropTypes.bool, // whether to render squares for extra days in week after endDate, and before start date tooltipDataAttrs: PropTypes.oneOfType([PropTypes.object, PropTypes.func]), // data attributes to add to square for setting 3rd party tooltips, e.g. { 'data-toggle': 'tooltip' } for bootstrap tooltips titleForValue: PropTypes.func, // function which returns title text for value classForValue: PropTypes.func, // function which returns html class for value monthLabels: PropTypes.arrayOf(PropTypes.string), // An array with 12 strings representing the text from janurary to december weekdayLabels: PropTypes.arrayOf(PropTypes.string), // An array with 7 strings representing the text from Sun to Sat onClick: PropTypes.func, // callback function when a square is clicked onMouseOver: PropTypes.func, // callback function when mouse pointer is over a square onMouseLeave: PropTypes.func, // callback function when mouse pointer is left a square transformDayElement: PropTypes.func, // function to further transform the svg element for a single day }; CalendarHeatmap.defaultProps = { numDays: null, startDate: dateNDaysAgo(200), endDate: new Date(), gutterSize: 1, horizontal: true, showMonthLabels: true, showWeekdayLabels: false, showOutOfRangeDays: false, tooltipDataAttrs: null, titleForValue: null, classForValue: (value) => (value ? 'color-filled' : 'color-empty'), monthLabels: MONTH_LABELS, weekdayLabels: DAY_LABELS, onClick: null, onMouseOver: null, onMouseLeave: null, transformDayElement: null, }; export default CalendarHeatmap;