I am lazy, remove the new heatmap for now

This commit is contained in:
Joshua Coles 2023-12-25 12:52:10 +00:00
parent 52b6b0eaee
commit 0ce2096fac
8 changed files with 3 additions and 1007 deletions

View File

@ -5,11 +5,12 @@ import useSWR from "swr";
import {fetcher} from "@/app/utils";
import * as R from 'ramda';
import * as dFns from 'date-fns';
// import CalendarHeatmap from 'react-calendar-heatmap';
import CalendarHeatmap from '../heatmap';
import CalendarHeatmap from 'react-calendar-heatmap';
// import CalendarHeatmap from '../heatmap';
import 'react-calendar-heatmap/dist/styles.css';
import './calendar-styles.css'
import { Tooltip } from 'react-tooltip';
import {MyHeatMap} from "@/heatmap/b";
const initialDate = dFns.parseISO('2023-12-15T00:00:00.000Z')
const endDate = dFns.parseISO('2024-01-25T00:00:00.000Z')

View File

@ -1,38 +0,0 @@
import * as dFns from 'date-fns';
import {CSS_PSEDUO_NAMESPACE} from "@/heatmap/index";
export function MonthLabels({
startDate,
endDate,
getMonthLabelCoordinates
}: {
startDate: Date;
endDate: Date;
getMonthLabelCoordinates: (month: Date) => {
x: number;
y: number
};
}) {
// We render a label for each month in the interval if its first week is in the interval
const firstWeeks = dFns.eachWeekOfInterval({
start: startDate,
end: endDate,
}).filter(date => date.getDate() <= 7);
return (
<>
{firstWeeks.map((month, i) => {
const {x, y} = getMonthLabelCoordinates(month);
return (
<text
key={i}
x={x}
y={y}
className={`${CSS_PSEDUO_NAMESPACE}month-label`}
>{dFns.format(month, 'MMM')}</text>
);
})}
</>
);
}

View File

@ -1,20 +0,0 @@
export const MILLISECONDS_IN_ONE_DAY = 24 * 60 * 60 * 1000;
export const DAYS_IN_WEEK = 7;
export const MONTH_LABELS = [
'Jan',
'Feb',
'Mar',
'Apr',
'May',
'Jun',
'Jul',
'Aug',
'Sep',
'Oct',
'Nov',
'Dec',
];
export const DAY_LABELS = ['', 'Mon', '', 'Wed', '', 'Fri', ''];

View File

@ -1,176 +0,0 @@
import {
convertToDate,
dateNDaysAgo,
getBeginningTimeForDate,
getRange,
shiftDate,
} from './helpers';
import {describe, it, expect} from "@jest/globals";
describe('shiftDate', () => {
it('adds a day to the first day of a month', () => {
const startingDate = new Date(2017, 0, 1);
const expectedDate = new Date(2017, 0, 2);
expect(shiftDate(startingDate, 1).getTime()).toBe(expectedDate.getTime());
});
it('adds multiple days to the first day of a month', () => {
const startingDate = new Date(2017, 0, 1);
const expectedDate = new Date(2017, 0, 3);
expect(shiftDate(startingDate, 2).getTime()).toBe(expectedDate.getTime());
});
it('subtracts a day from the first day of a month', () => {
const startingDate = new Date(2017, 0, 1);
const expectedDate = new Date(2016, 11, 31);
expect(shiftDate(startingDate, -1).getTime()).toBe(expectedDate.getTime());
});
it('subtracts multiple days from the first day of a month', () => {
const startingDate = new Date(2017, 0, 1);
const expectedDate = new Date(2016, 11, 30);
expect(shiftDate(startingDate, -2).getTime()).toBe(expectedDate.getTime());
});
it('adds a day to a non-first day of a month', () => {
const startingDate = new Date(2017, 0, 2);
const expectedDate = new Date(2017, 0, 3);
expect(shiftDate(startingDate, 1).getTime()).toBe(expectedDate.getTime());
});
it('adds multiple days to a non-first day of a month', () => {
const startingDate = new Date(2017, 0, 2);
const expectedDate = new Date(2017, 0, 4);
expect(shiftDate(startingDate, 2).getTime()).toBe(expectedDate.getTime());
});
it('subtracts a day from a non-first day of a month', () => {
const startingDate = new Date(2017, 0, 2);
const expectedDate = new Date(2017, 0, 1);
expect(shiftDate(startingDate, -1).getTime()).toBe(expectedDate.getTime());
});
it('subtracts multiple days from a non-first day of a month', () => {
const startingDate = new Date(2017, 0, 2);
const expectedDate = new Date(2016, 11, 31);
expect(shiftDate(startingDate, -2).getTime()).toBe(expectedDate.getTime());
});
});
describe('getBeginningTimeForDate', () => {
it('gets midnight (in the local timezone) on the date passed in', () => {
const inputDate = new Date(2017, 11, 25, 21, 30, 59, 750);
const expectedDate = new Date(2017, 11, 25, 0, 0, 0, 0);
expect(getBeginningTimeForDate(inputDate).getTime()).toBe(expectedDate.getTime());
});
});
describe('convertToDate', () => {
it('interprets an "ISO-8601 date-only" string as UTC and converts it into a Date object representing the first millisecond on that date', () => {
const iso8601DateOnlyString = '2017-07-14';
const expectedDate = new Date(Date.UTC(2017, 6, 14, 0, 0, 0, 0));
expect(convertToDate(iso8601DateOnlyString).getTime()).toBe(expectedDate.getTime());
});
it('interprets a millisecond timestamp integer as UTC and converts it into a Date object representing that same millisecond', () => {
const msTimestamp = 1500000001234; // Friday, July 14, 2017 2:40:01.234 AM, according to https://epochconverter.com
const expectedDate = new Date(Date.UTC(2017, 6, 14, 2, 40, 1, 234));
expect(convertToDate(msTimestamp).getTime()).toBe(expectedDate.getTime());
});
it('returns the same Date object it receives', () => {
const inputDate = new Date(2017, 11, 25, 21, 30, 59, 750);
const originalTimestamp = inputDate.getTime();
expect(convertToDate(inputDate)).toBe(inputDate);
expect(convertToDate(inputDate).getTime()).toBe(originalTimestamp);
});
});
describe('dateNDaysAgo', () => {
it('crosses month boundaries in the negative direction', () => {
const numDays = 32;
const startingDate = new Date();
const expectedDate = new Date(startingDate.getTime());
expectedDate.setDate(startingDate.getDate() - numDays);
const expectedYear = expectedDate.getFullYear();
const expectedMonth = expectedDate.getMonth();
const expectedDay = expectedDate.getDate();
expect(dateNDaysAgo(numDays).getFullYear()).toBe(expectedYear);
expect(dateNDaysAgo(numDays).getMonth()).toBe(expectedMonth);
expect(dateNDaysAgo(numDays).getDate()).toBe(expectedDay);
});
it('crosses month boundaries in the positive direction', () => {
const numDays = -32;
const startingDate = new Date();
const expectedDate = new Date(startingDate.getTime());
expectedDate.setDate(startingDate.getDate() - numDays);
const expectedYear = expectedDate.getFullYear();
const expectedMonth = expectedDate.getMonth();
const expectedDay = expectedDate.getDate();
expect(dateNDaysAgo(numDays).getFullYear()).toBe(expectedYear);
expect(dateNDaysAgo(numDays).getMonth()).toBe(expectedMonth);
expect(dateNDaysAgo(numDays).getDate()).toBe(expectedDay);
});
it('crosses year boundaries in the negative direction', () => {
const numDays = 366;
const startingDate = new Date();
const expectedDate = new Date(startingDate.getTime());
expectedDate.setDate(startingDate.getDate() - numDays);
const expectedYear = expectedDate.getFullYear();
const expectedMonth = expectedDate.getMonth();
const expectedDay = expectedDate.getDate();
expect(dateNDaysAgo(numDays).getFullYear()).toBe(expectedYear);
expect(dateNDaysAgo(numDays).getMonth()).toBe(expectedMonth);
expect(dateNDaysAgo(numDays).getDate()).toBe(expectedDay);
});
it('crosses year boundaries in the positive direction', () => {
const numDays = -366;
const startingDate = new Date();
const expectedDate = new Date(startingDate.getTime());
expectedDate.setDate(startingDate.getDate() - numDays);
const expectedYear = expectedDate.getFullYear();
const expectedMonth = expectedDate.getMonth();
const expectedDay = expectedDate.getDate();
expect(dateNDaysAgo(numDays).getFullYear()).toBe(expectedYear);
expect(dateNDaysAgo(numDays).getMonth()).toBe(expectedMonth);
expect(dateNDaysAgo(numDays).getDate()).toBe(expectedDay);
});
});
describe('getRange', () => {
it('generates an empty array', () => {
expect(getRange(0)).toEqual([]);
});
it('generates an array containing one integer', () => {
expect(getRange(1)).toEqual([0]);
});
it('generates an array containing multiple integers', () => {
expect(getRange(5)).toEqual([0, 1, 2, 3, 4]);
});
});

View File

@ -1,27 +0,0 @@
import * as dFns from 'date-fns';
// returns a new date shifted a certain number of days (can be negative)
export function shiftDate(date: Date, numDays: number): Date {
return dFns.addDays(date, numDays);
}
export function getBeginningTimeForDate(date: Date): Date {
return dFns.startOfDay(date);
}
// obj can be a parseable string, a millisecond timestamp, or a Date object
export function convertToDate(obj: Date | string | number): Date {
return obj instanceof Date ? obj : new Date(obj);
}
export function dateNDaysAgo(numDaysAgo: number): Date {
return shiftDate(new Date(), -numDaysAgo);
}
export function getRange(count: number): number[] {
const arr = [];
for (let idx = 0; idx < count; idx += 1) {
arr.push(idx);
}
return arr;
}

View File

@ -1,286 +0,0 @@
import React from 'react';
import Enzyme, { shallow } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
import CalendarHeatmap from './index';
import { dateNDaysAgo, shiftDate } from './helpers';
Enzyme.configure({ adapter: new Adapter() });
const getWrapper = (overrideProps, renderMethod = 'shallow') => {
const defaultProps = {
values: [],
};
return Enzyme[renderMethod](<CalendarHeatmap {...defaultProps} {...overrideProps} />);
};
describe('CalendarHeatmap', () => {
const values = [
{ date: new Date('2017-06-01') },
{ date: new Date('2017-06-02') },
{ date: new Date('2018-06-01') },
{ date: new Date('2018-06-02') },
{ date: new Date('2018-06-03') },
];
it('should render as an svg', () => {
const wrapper = shallow(<CalendarHeatmap values={[]} />);
expect(wrapper.find('svg')).toHaveLength(1);
});
it('should not throw exceptions in base case', () => {
expect(() => <CalendarHeatmap values={[]} />).not.toThrow();
});
it('shows values within its original date range', () => {
const wrapper = shallow(
<CalendarHeatmap
endDate={new Date('2017-12-31')}
startDate={new Date('2017-01-01')}
values={values}
/>,
);
expect(wrapper.find('.color-filled').length).toBe(2);
});
it('should handle string formatted date range', () => {
const wrapper = shallow(
<CalendarHeatmap endDate="2017-12-31" startDate="2017-01-01" values={values} />,
);
expect(wrapper.find('.color-filled').length).toBe(2);
});
it('shows values within an updated date range', () => {
const wrapper = shallow(
<CalendarHeatmap
endDate={new Date('2017-12-31')}
startDate={new Date('2017-01-01')}
values={values}
/>,
);
wrapper.setProps({
endDate: new Date('2018-12-31'),
startDate: new Date('2018-01-01'),
});
expect(wrapper.find('.color-filled').length).toBe(3);
});
});
describe('CalendarHeatmap props', () => {
it('values', () => {
const values = [
{ date: '2016-01-01' },
{ date: new Date('2016-01-02').getTime() },
{ date: new Date('2016-01-03') },
];
const wrapper = shallow(
<CalendarHeatmap
endDate={new Date('2016-02-01')}
startDate={new Date('2015-12-20')}
values={values}
/>,
);
// 'values should handle Date/string/number formats'
expect(wrapper.find('.color-filled')).toHaveLength(values.length);
});
it('horizontal', () => {
const horizontal = shallow(
<CalendarHeatmap startDate={dateNDaysAgo(100)} values={[]} horizontal />,
);
const [, , horWidth, horHeight] = horizontal.prop('viewBox').split(' ');
// 'horizontal orientation width should be greater than height'
expect(Number(horWidth)).toBeGreaterThan(Number(horHeight));
const vertical = shallow(
<CalendarHeatmap startDate={dateNDaysAgo(100)} values={[]} horizontal={false} />,
);
const [, , vertWidth, vertHeight] = vertical.prop('viewBox').split(' ');
// 'vertical orientation width should be less than height'
expect(Number(vertWidth)).toBeLessThan(Number(vertHeight));
});
it('startDate', () => {
const today = new Date();
const wrapper = shallow(<CalendarHeatmap values={[]} endDate={today} startDate={today} />);
expect(
today.getDate() ===
wrapper
.instance()
.getEndDate()
.getDate() &&
today.getMonth() ===
wrapper
.instance()
.getEndDate()
.getMonth(),
).toBe(true);
});
it('endDate', () => {
const today = new Date();
const wrapper = shallow(
<CalendarHeatmap values={[]} endDate={today} startDate={dateNDaysAgo(10)} />,
);
expect(
today.getDate() ===
wrapper
.instance()
.getEndDate()
.getDate() &&
today.getMonth() ===
wrapper
.instance()
.getEndDate()
.getMonth(),
).toBe(true);
});
it('classForValue', () => {
const today = new Date();
const numDays = 10;
const expectedStartDate = shiftDate(today, -numDays + 1);
const wrapper = shallow(
<CalendarHeatmap
values={[{ date: expectedStartDate, count: 0 }, { date: today, count: 1 }]}
endDate={today}
startDate={dateNDaysAgo(numDays)}
titleForValue={(value) => (value ? value.count : null)}
classForValue={(value) => {
if (!value) {
return null;
}
return value.count > 0 ? 'red' : 'white';
}}
/>,
);
expect(wrapper.find('.white')).toHaveLength(1);
expect(wrapper.find('.red')).toHaveLength(1);
// TODO these attr selectors might be broken with react 15
// assert(wrapper.first('rect[title=0]').hasClass('white'));
// assert(wrapper.first('rect[title=1]').hasClass('red'));
});
it('showMonthLabels', () => {
const visible = shallow(
<CalendarHeatmap startDate={dateNDaysAgo(100)} values={[]} showMonthLabels />,
);
expect(visible.find('text').length).toBeGreaterThan(0);
const hidden = shallow(<CalendarHeatmap values={[]} showMonthLabels={false} />);
expect(hidden.find('text')).toHaveLength(0);
});
it('showWeekdayLabels', () => {
const visible = shallow(
<CalendarHeatmap startDate={dateNDaysAgo(7)} values={[]} showWeekdayLabels />,
);
expect(visible.find('text').length).toBeGreaterThan(2);
const hidden = shallow(
<CalendarHeatmap values={[]} showMonthLabels={false} showWeekdayLabels={false} />,
);
expect(hidden.find('text')).toHaveLength(0);
// should display text with .small-text class
// in case if horizontal prop value is false
const vertical = shallow(<CalendarHeatmap values={[]} horizontal={false} showWeekdayLabels />);
expect(vertical.find('text.react-calendar-heatmap-small-text')).toHaveLength(3);
});
it('transformDayElement', () => {
const transform = (rect) => React.cloneElement(rect, { 'data-test': 'ok' });
const today = new Date();
const expectedStartDate = shiftDate(today, -1);
const wrapper = shallow(
<CalendarHeatmap
values={[{ date: today }, { date: expectedStartDate }]}
endDate={today}
startDate={expectedStartDate}
transformDayElement={transform}
/>,
);
expect(wrapper.find('[data-test="ok"]')).toHaveLength(1);
});
describe('tooltipDataAttrs', () => {
it('allows a function to be passed', () => {
const today = new Date();
const numDays = 10;
const expectedStartDate = shiftDate(today, -numDays + 1);
const wrapper = shallow(
<CalendarHeatmap
values={[{ date: today, count: 1 }, { date: expectedStartDate, count: 0 }]}
endDate={today}
startDate={expectedStartDate}
tooltipDataAttrs={({ count }) => ({
'data-tooltip': `Count: ${count}`,
})}
/>,
);
expect(wrapper.find('[data-tooltip="Count: 1"]')).toHaveLength(1);
});
});
describe('event handlers', () => {
const count = 999;
const startDate = '2018-06-01';
const endDate = '2018-06-03';
const values = [{ date: '2018-06-02', count }];
const props = {
values,
startDate,
endDate,
};
const expectedValue = values[0];
it('calls props.onClick with the correct value', () => {
const onClick = jest.fn();
const wrapper = getWrapper({ ...props, onClick });
const rect = wrapper.find('rect').at(0);
rect.simulate('click');
expect(onClick).toHaveBeenCalledWith(expectedValue);
});
it('calls props.onMouseOver with the correct value', () => {
const onMouseOver = jest.fn();
const wrapper = getWrapper({ ...props, onMouseOver });
const fakeEvent = { preventDefault: jest.fn() };
const rect = wrapper.find('rect').at(0);
rect.simulate('mouseOver', fakeEvent);
expect(onMouseOver).toHaveBeenCalledWith(fakeEvent, expectedValue);
});
it('calls props.onMouseLeave with the correct value', () => {
const onMouseLeave = jest.fn();
const wrapper = getWrapper({ ...props, onMouseLeave });
const fakeEvent = { preventDefault: jest.fn() };
const rect = wrapper.find('rect').at(0);
rect.simulate('mouseLeave', fakeEvent);
expect(onMouseLeave).toHaveBeenCalledWith(fakeEvent, expectedValue);
});
});
});

View File

@ -1,386 +0,0 @@
// @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
<rect
key={index}
width={SQUARE_SIZE}
height={SQUARE_SIZE}
x={x}
y={y}
className={this.getClassNameForIndex(index)}
onClick={() => this.handleClick(value)}
onMouseOver={(e) => this.handleMouseOver(e, value)}
onMouseLeave={(e) => this.handleMouseLeave(e, value)}
{...this.getTooltipDataAttrsForIndex(index)}
>
<title>{this.getTitleForIndex(index)}</title>
</rect>
);
const {transformDayElement} = this.props;
return transformDayElement ? transformDayElement(rect, value, index) : rect;
}
renderWeek(weekIndex) {
return (
<g
key={weekIndex}
transform={this.getTransformForWeek(weekIndex)}
className={`${CSS_PSEDUO_NAMESPACE}week`}
>
{getRange(DAYS_IN_WEEK).map((dayIndex) =>
this.renderSquare(dayIndex, weekIndex * DAYS_IN_WEEK + dayIndex),
)}
</g>
);
}
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 ? (
<text key={`${x}${y}`} x={x} y={y} className={cssClasses}>
{weekdayLabel}
</text>
) : null;
});
}
render() {
this.valueCache = this.getValueCache(this.props);
const monthPositionAdapter = (date) => {
const [x, y] = this.getMonthLabelCoordinates(dFns.differenceInWeeks(date, this.props.startDate));
return {
x,
y
}
};
const weeks = dFns.eachWeekOfInterval({
start: this.props.startDate,
end: this.props.endDate,
});
return (
<svg className="react-calendar-heatmap" viewBox={this.getViewBox()}>
<g
transform={this.getTransformForMonthLabels()}
className={`${CSS_PSEDUO_NAMESPACE}month-labels`}
>
{this.props.showMonthLabels && <MonthLabels
startDate={this.props.startDate}
endDate={this.props.endDate}
getMonthLabelCoordinates={monthPositionAdapter}
/>}
</g>
<g
transform={this.getTransformForAllWeeks()}
className={`${CSS_PSEDUO_NAMESPACE}all-weeks`}
>
{weeks.map((_, index) => this.renderWeek(index))}
</g>
<g
transform={this.getTransformForWeekdayLabels()}
className={`${CSS_PSEDUO_NAMESPACE}weekday-labels`}
>
{this.renderWeekdayLabels()}
</g>
</svg>
);
}
}
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;

View File

@ -1,72 +0,0 @@
/*
* react-calendar-heatmap styles
*
* All of the styles in this file are optional and configurable!
* The github and gitlab color scales are provided for reference.
*/
.react-calendar-heatmap text {
font-size: 10px;
fill: #aaa;
}
.react-calendar-heatmap .react-calendar-heatmap-small-text {
font-size: 5px;
}
.react-calendar-heatmap rect:hover {
stroke: #555;
stroke-width: 1px;
}
/*
* Default color scale
*/
.react-calendar-heatmap .color-empty {
fill: #eeeeee;
}
.react-calendar-heatmap .color-filled {
fill: #8cc665;
}
/*
* Github color scale
*/
.react-calendar-heatmap .color-github-0 {
fill: #eeeeee;
}
.react-calendar-heatmap .color-github-1 {
fill: #d6e685;
}
.react-calendar-heatmap .color-github-2 {
fill: #8cc665;
}
.react-calendar-heatmap .color-github-3 {
fill: #44a340;
}
.react-calendar-heatmap .color-github-4 {
fill: #1e6823;
}
/*
* Gitlab color scale
*/
.react-calendar-heatmap .color-gitlab-0 {
fill: #ededed;
}
.react-calendar-heatmap .color-gitlab-1 {
fill: #acd5f2;
}
.react-calendar-heatmap .color-gitlab-2 {
fill: #7fa8d1;
}
.react-calendar-heatmap .color-gitlab-3 {
fill: #49729b;
}
.react-calendar-heatmap .color-gitlab-4 {
fill: #254e77;
}