Initial import of react-calendar-heatmap code
This commit is contained in:
parent
fb9547991c
commit
c85776a499
18
jest.config.ts
Normal file
18
jest.config.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import type { Config } from 'jest'
|
||||||
|
import nextJest from 'next/jest.js'
|
||||||
|
|
||||||
|
const createJestConfig = nextJest({
|
||||||
|
// Provide the path to your Next.js app to load next.config.js and .env files in your test environment
|
||||||
|
dir: './',
|
||||||
|
})
|
||||||
|
|
||||||
|
// Add any custom config to be passed to Jest
|
||||||
|
const config: Config = {
|
||||||
|
coverageProvider: 'v8',
|
||||||
|
testEnvironment: 'jsdom',
|
||||||
|
// Add more setup options before each test is run
|
||||||
|
// setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
|
||||||
|
}
|
||||||
|
|
||||||
|
// createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async
|
||||||
|
export default createJestConfig(config)
|
||||||
3724
package-lock.json
generated
3724
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -6,7 +6,8 @@
|
|||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "next lint"
|
"lint": "next lint",
|
||||||
|
"test": "jest"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@heroicons/react": "^1.0.6",
|
"@heroicons/react": "^1.0.6",
|
||||||
@ -25,6 +26,8 @@
|
|||||||
"swr": "^2.2.4"
|
"swr": "^2.2.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@testing-library/jest-dom": "^6.1.5",
|
||||||
|
"@testing-library/react": "^14.1.2",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^18",
|
"@types/react": "^18",
|
||||||
"@types/react-calendar-heatmap": "^1.6.6",
|
"@types/react-calendar-heatmap": "^1.6.6",
|
||||||
@ -32,8 +35,11 @@
|
|||||||
"autoprefixer": "^10.0.1",
|
"autoprefixer": "^10.0.1",
|
||||||
"eslint": "^8",
|
"eslint": "^8",
|
||||||
"eslint-config-next": "14.0.4",
|
"eslint-config-next": "14.0.4",
|
||||||
|
"jest": "^29.7.0",
|
||||||
|
"jest-environment-jsdom": "^29.7.0",
|
||||||
"postcss": "^8",
|
"postcss": "^8",
|
||||||
"tailwindcss": "^3.3.0",
|
"tailwindcss": "^3.3.0",
|
||||||
|
"ts-node": "^10.9.2",
|
||||||
"typescript": "^5"
|
"typescript": "^5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
20
src/heatmap/constants.ts
Normal file
20
src/heatmap/constants.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
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', ''];
|
||||||
176
src/heatmap/helpers.test.ts
Normal file
176
src/heatmap/helpers.test.ts
Normal file
@ -0,0 +1,176 @@
|
|||||||
|
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]);
|
||||||
|
});
|
||||||
|
});
|
||||||
27
src/heatmap/helpers.ts
Normal file
27
src/heatmap/helpers.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
387
src/heatmap/index.js
Normal file
387
src/heatmap/index.js
Normal file
@ -0,0 +1,387 @@
|
|||||||
|
import React 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';
|
||||||
|
|
||||||
|
const SQUARE_SIZE = 10;
|
||||||
|
const MONTH_LABEL_GUTTER_SIZE = 4;
|
||||||
|
const CSS_PSEDUO_NAMESPACE = 'react-calendar-heatmap-';
|
||||||
|
|
||||||
|
class CalendarHeatmap extends React.Component {
|
||||||
|
getDateDifferenceInDays() {
|
||||||
|
const { startDate, numDays } = this.props;
|
||||||
|
if (numDays) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.warn(
|
||||||
|
'numDays is a deprecated prop. It will be removed in the next release. Consider using the startDate prop instead.',
|
||||||
|
);
|
||||||
|
return numDays;
|
||||||
|
}
|
||||||
|
const timeDiff = this.getEndDate() - convertToDate(startDate);
|
||||||
|
return Math.ceil(timeDiff / MILLISECONDS_IN_ONE_DAY);
|
||||||
|
}
|
||||||
|
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderAllWeeks() {
|
||||||
|
return getRange(this.getWeekCount()).map((weekIndex) => this.renderWeek(weekIndex));
|
||||||
|
}
|
||||||
|
|
||||||
|
renderMonthLabels() {
|
||||||
|
if (!this.props.showMonthLabels) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const weekRange = getRange(this.getWeekCount() - 1); // don't render for last week, because label will be cut off
|
||||||
|
return weekRange.map((weekIndex) => {
|
||||||
|
const endOfWeek = shiftDate(this.getStartDateWithEmptyDays(), (weekIndex + 1) * DAYS_IN_WEEK);
|
||||||
|
const [x, y] = this.getMonthLabelCoordinates(weekIndex);
|
||||||
|
return endOfWeek.getDate() >= 1 && endOfWeek.getDate() <= DAYS_IN_WEEK ? (
|
||||||
|
<text key={weekIndex} x={x} y={y} className={`${CSS_PSEDUO_NAMESPACE}month-label`}>
|
||||||
|
{this.props.monthLabels[endOfWeek.getMonth()]}
|
||||||
|
</text>
|
||||||
|
) : null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<svg className="react-calendar-heatmap" viewBox={this.getViewBox()}>
|
||||||
|
<g
|
||||||
|
transform={this.getTransformForMonthLabels()}
|
||||||
|
className={`${CSS_PSEDUO_NAMESPACE}month-labels`}
|
||||||
|
>
|
||||||
|
{this.renderMonthLabels()}
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
transform={this.getTransformForAllWeeks()}
|
||||||
|
className={`${CSS_PSEDUO_NAMESPACE}all-weeks`}
|
||||||
|
>
|
||||||
|
{this.renderAllWeeks()}
|
||||||
|
</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;
|
||||||
286
src/heatmap/index.test.js
Normal file
286
src/heatmap/index.test.js
Normal file
@ -0,0 +1,286 @@
|
|||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
72
src/heatmap/styles.css
Normal file
72
src/heatmap/styles.css
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
/*
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user