├── .github
└── workflows
│ ├── gh-pages.yml
│ └── release.yml
├── .gitignore
├── README.md
├── example
├── calendar.css
├── index.html
├── main.jsx
└── webpack.config.js
├── package-lock.json
├── package.json
├── src
└── index.tsx
└── tsconfig.json
/.github/workflows/gh-pages.yml:
--------------------------------------------------------------------------------
1 | name: Build gh-pages
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 | - fix-builds
8 |
9 | jobs:
10 | build:
11 | runs-on: ubuntu-latest
12 | steps:
13 | - uses: actions/checkout@master
14 | - name: Setup nodejs
15 | uses: actions/setup-node@v1
16 | with:
17 | node-version: 18.x
18 | - name: Npm install and build
19 | run: |
20 | npm ci
21 | npm run build
22 | env:
23 | CI: true
24 | - name: Deploy action for GitHub Pages
25 | uses: peaceiris/actions-gh-pages@v2.4.0
26 | env:
27 | ACTIONS_DEPLOY_KEY: ${{ secrets.ACTIONS_DEPLOY_KEY }}
28 | PUBLISH_BRANCH: gh-pages
29 | PUBLISH_DIR: ./example
30 | with:
31 | emptyCommits: false
32 | keepFiles: false
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Publish Package to npmjs
2 |
3 | on:
4 | release:
5 | types: [created]
6 |
7 | jobs:
8 | build:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - uses: actions/checkout@v2
12 | - name: Setup nodejs
13 | uses: actions/setup-node@v1
14 | with:
15 | node-version: 18.x
16 | registry-url: 'https://registry.npmjs.org'
17 | - name: Npm install and build
18 | run: |
19 | npm ci
20 | npm run build
21 | env:
22 | CI: true
23 | - run: npm publish
24 | env:
25 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
26 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /lib
2 | /example/dist
3 | /node_modules
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | react-github-contribution-calendar
2 | ====
3 |
4 | [](https://badge.fury.io/js/react-github-contribution-calendar)
5 |
6 | A responsive react component for GitHub-like heatmap calendar
7 |
8 | ## Demo
9 |
10 | [Demo and documents](http://haripo.github.io/react-github-contribution-calendar/)
11 |
12 | ## Usage
13 |
14 | ``` javascript
15 | // main.jsx
16 | import React from 'react';
17 | import ReactDOM from 'react-dom';
18 | import Calendar from 'react-github-contribution-calendar';
19 |
20 | var values = {
21 | '2016-06-23': 1,
22 | '2016-06-26': 2,
23 | '2016-06-27': 3,
24 | '2016-06-28': 4,
25 | '2016-06-29': 4
26 | }
27 | var until = '2016-06-30';
28 |
29 | var elem = document.getElementById('app');
30 | ReactDOM.render(, elem);
31 | ```
32 |
33 | ## Install
34 |
35 | ``` npm i react-github-contribution-calendar --save ```
36 |
37 | ## Licence
38 |
39 | MIT
40 |
41 | ## Author
42 |
43 | [haripo](https://github.com/haripo)
44 |
--------------------------------------------------------------------------------
/example/calendar.css:
--------------------------------------------------------------------------------
1 | @import url(https://fonts.googleapis.com/css?family=Lato:400,900);
2 |
3 | * {
4 | margin: 0;
5 | padding: 0;
6 | }
7 |
8 | body {
9 | font-family: 'Lato', helvetica, sans-serif;
10 | }
11 |
12 | header {
13 | color: #F6F6F6;
14 | background-color: #1E6823;
15 | text-align: center;
16 |
17 | width: 100%;
18 | box-sizing: border-box;
19 | padding: 100px 0;
20 | }
21 |
22 | section {
23 | max-width: 800px;
24 | margin: 0 auto;
25 | }
26 |
27 | footer {
28 | color: #F6F6F6;
29 | background-color: #1E6823;
30 | text-align: center;
31 |
32 | width: 100%;
33 | box-sizing: border-box;
34 | padding: 30px 0;
35 | }
36 |
37 | footer a {
38 | color: #F6F6F6;
39 | }
40 |
41 | footer a:hover {
42 | color: #F6F6F6;
43 | }
44 |
45 | h1 {
46 | font-weight: 900;
47 | }
48 |
49 | h2 {
50 | text-align: center;
51 | color: #1E6823;
52 | text-transform: uppercase;
53 | line-height: 100px;
54 | }
55 |
56 | /* header */
57 |
58 | .header-sample {
59 | display: inline-block;
60 | margin: 50px 0;
61 | padding: 10px;
62 | background-color: #F6F6F6;
63 | border-radius: 10px 10px 0 10px;
64 |
65 | width: 500px;
66 | min-width: 10px;
67 | max-width: 706px;
68 | }
69 |
70 | @media screen and (max-width: 800px) {
71 | .header-sample {
72 | max-width: 300px;
73 | }
74 | }
75 |
76 | .is-resizable {
77 | resize: horizontal;
78 | overflow: auto;
79 | }
80 |
81 | .draghere {
82 | color: #1E6823;
83 | text-align: right;
84 | font-size: 12px;
85 | }
86 |
87 | /* article */
88 | .example {
89 | display: flex;
90 | justify-content: center;
91 | }
92 |
93 | .example > .code {
94 | width: 400px;
95 | margin-right: 20px;
96 | }
97 |
98 | .example > .render {
99 | width: 400px;
100 | }
101 |
--------------------------------------------------------------------------------
/example/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | react-github-contribution-calendar
11 |
12 |
13 |
25 |
26 | getting started
27 | Install
28 |
29 |
30 |
31 | $ npm install react-github-contribution-calendar
32 |
33 |
34 |
35 | Load styles
36 |
37 |
38 |
39 | <link rel="stylesheet" href="node_modules/react-github-contribution-calendar/default.css" type="text/css" />
40 |
41 |
42 |
43 | Render
44 |
45 |
46 |
47 | import React from 'react';
48 | import ReactDOM from 'react-dom';
49 | import Calendar from 'react-github-contribution-calendar';
50 |
51 | var values = {
52 | '2016-06-23': 1,
53 | '2016-06-26': 2,
54 | '2016-06-27': 3,
55 | '2016-06-28': 4,
56 | '2016-06-29': 4
57 | }
58 | var until = '2016-06-30';
59 |
60 | var elem = document.getElementById('app');
61 | ReactDOM.render(<Calendar values={values} until={until} />, elem);
62 |
63 |
64 |
65 |
66 |
67 | customizing
68 | Change panel colors
69 |
70 |
71 |
72 |
73 | var values = {}
74 | var until = '2016-06-30';
75 | var values = {
76 | '2016-06-23': 1,
77 | '2016-06-26': 2,
78 | '2016-06-27': 3,
79 | '2016-06-28': 4,
80 | '2016-06-29': 4
81 | };
82 | var panelColors = [
83 | '#EEEEEE',
84 | '#F78A23',
85 | '#F87D09',
86 | '#AC5808',
87 | '#7B3F06'
88 | ];
89 |
90 |
91 | <Calendar
92 | values={values} until={until}
93 | weekNames={weekNames} monthNames={monthNames}/>
94 |
95 |
96 |
97 |
100 |
101 |
102 | Change label texts
103 |
104 |
105 |
106 |
107 | var values = {}
108 | var until = '2016-12-30';
109 | var weekNames = ['s', 'm', 't', 'w', 't', 'f', 's'];
110 | var monthNames = [
111 | '1', '2', '3', '4', '5', '6',
112 | '7', '8', '9', '10', '11', '12'
113 | ];
114 |
115 |
116 | <Calendar
117 | values={values} until={until}
118 | weekNames={weekNames} monthNames={monthNames}/>
119 |
120 |
121 |
122 |
125 |
126 | Change styles by overwriting SVG attributes
127 |
128 |
129 |
130 |
131 | var values = {}
132 | var until = '2016-12-30';
133 | var panelAttributes = { 'rx': 6, 'ry': 6 };
134 | var weekLabelAttributes = {
135 | 'rotate': 20
136 | };
137 | var monthLabelAttributes = {
138 | 'style': {
139 | 'text-decoration': 'underline',
140 | 'font-size': 10,
141 | 'alignment-baseline': 'central',
142 | 'fill': '#AAA'
143 | }
144 | };
145 |
146 |
147 | <Calendar
148 | values={values}
149 | until={until}
150 | panelAttributes={panelAttributes}
151 | weekLabelAttributes={weekLabelAttributes}
152 | monthLabelAttributes={monthLabelAttributes}
153 | />
154 |
155 |
156 |
157 |
160 |
161 |
162 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
--------------------------------------------------------------------------------
/example/main.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import Calendar from './../lib/index.js';
4 | /* import Calendar from 'react-github-contribution-calendar'; */
5 |
6 | (() => {
7 | var values = {};
8 | var pad = (v) => (v < 10 ? '0' + v : v)
9 | for (var i = 1; i <= 12; i++) {
10 | for (var j = 1; j <= 31; j++) {
11 | values['2016-' + pad(i) + '-' + pad(j)] = Math.floor(Math.random() * 5);
12 | }
13 | }
14 | var until = '2016-12-31';
15 | var panelColors = ['#EEEEEE', '#D6E685', '#8CC665', '#44A340', '#1E6823'];
16 |
17 | var elem = document.getElementById('app');
18 | ReactDOM.render(,
19 | elem);
20 | })();
21 |
22 |
23 | (() => {
24 | var values = {}
25 | var until = '2016-06-30';
26 | var values = {
27 | '2016-06-23': 1,
28 | '2016-06-26': 2,
29 | '2016-06-27': 3,
30 | '2016-06-28': 4,
31 | '2016-06-29': 4
32 | };
33 | var panelColors = ['#EEEEEE', '#F78A23', '#F87D09', '#AC5808', '#7B3F06'];
34 |
35 | var elem = document.getElementById('example1');
36 | ReactDOM.render(, elem);
38 | })();
39 |
40 | (() => {
41 | var values = {}
42 | var until = '2016-12-30';
43 | var weekNames = ['s', 'm', 't', 'w', 't', 'f', 's'];
44 | var monthNames = [
45 | '1', '2', '3', '4', '5', '6',
46 | '7', '8', '9', '10', '11', '12'
47 | ];
48 |
49 | var elem = document.getElementById('example2');
50 | ReactDOM.render(, elem);
52 | })();
53 |
54 | (() => {
55 | var values = {}
56 | var until = '2016-12-30';
57 | var panelAttributes = { 'rx': 6, 'ry': 6 };
58 | var weekLabelAttributes = {
59 | 'rotate': 20
60 | };
61 | var monthLabelAttributes = {
62 | 'style': {
63 | 'text-decoration': 'underline',
64 | 'font-size': 10,
65 | 'alignment-baseline': 'central',
66 | 'fill': '#AAA'
67 | }
68 | };
69 |
70 | var elem = document.getElementById('example3');
71 | ReactDOM.render(, elem);
78 | })();
79 |
--------------------------------------------------------------------------------
/example/webpack.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | mode: 'production',
3 | entry: './main.jsx',
4 | output: {
5 | filename: './bundle.js'
6 | },
7 | module: {
8 | rules: [
9 | {
10 | test: /\.jsx$/,
11 | use: [
12 | {
13 | loader: 'babel-loader',
14 | options: {
15 | presets: ['@babel/preset-env', '@babel/preset-react']
16 | }
17 | }
18 | ]
19 | }
20 | ]
21 | },
22 | resolve: {
23 | modules: ['../node_modules'],
24 | extensions: ['.js', '.jsx']
25 | }
26 | };
27 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-github-contribution-calendar",
3 | "version": "2.2.0",
4 | "description": "React component for github-like calendar",
5 | "main": "./lib/index.js",
6 | "scripts": {
7 | "build": "npm run lib:build && npm run example:build",
8 | "lib:build": "tsc -p .",
9 | "example:build": "cd example && webpack",
10 | "example:watch": "cd example && webpack -w"
11 | },
12 | "keywords": [
13 | "react",
14 | "github",
15 | "calendar"
16 | ],
17 | "author": "haripo",
18 | "license": "MIT",
19 | "files": [
20 | "lib"
21 | ],
22 | "devDependencies": {
23 | "@babel/core": "^7.20.2",
24 | "@babel/preset-env": "^7.20.2",
25 | "@babel/preset-react": "^7.18.6",
26 | "@types/react": "^18.0.25",
27 | "@types/react-measure": "^2.0.8",
28 | "babel-loader": "^9.1.0",
29 | "react": "^18.2.0",
30 | "react-dom": "^18.2.0",
31 | "typescript": "^4.8.4",
32 | "webpack": "^5.75.0",
33 | "webpack-cli": "^4.10.0"
34 | },
35 | "dependencies": {
36 | "dayjs": "^1.11.6",
37 | "react-measure": "^2.5.2"
38 | },
39 | "peerDependencies": {
40 | "react": "^16 || ^17 || ^18"
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import dayjs from 'dayjs';
2 | import React, { ReactElement } from 'react';
3 | import Measure, { BoundingRect } from 'react-measure';
4 |
5 | interface Props {
6 | weekNames?: string[]
7 | monthNames?: string[]
8 | panelColors?: string[]
9 | values: { [date: string]: number }
10 | until: string
11 | dateFormat?: string
12 | weekLabelAttributes: any | undefined
13 | monthLabelAttributes: any | undefined
14 | panelAttributes: any | undefined
15 | }
16 |
17 | interface State {
18 | columns: number
19 | maxWidth: number
20 | }
21 |
22 | export default class GitHubCalendar extends React.Component {
23 | monthLabelHeight: number;
24 | weekLabelWidth: number;
25 | panelSize: number;
26 | panelMargin: number;
27 |
28 | constructor(props: any) {
29 | super(props);
30 |
31 | this.monthLabelHeight = 15;
32 | this.weekLabelWidth = 15;
33 | this.panelSize = 11;
34 | this.panelMargin = 2;
35 |
36 | this.state = {
37 | columns: 53,
38 | maxWidth: 53
39 | }
40 | }
41 |
42 | getPanelPosition(row: number, col: number) {
43 | const bounds = this.panelSize + this.panelMargin;
44 | return {
45 | x: this.weekLabelWidth + bounds * row,
46 | y: this.monthLabelHeight + bounds * col
47 | };
48 | }
49 |
50 | makeCalendarData(history: { [k: string]: number }, lastDay: string, columns: number) {
51 | const d = dayjs(lastDay, { format: this.props.dateFormat });
52 | const lastWeekend = d.endOf('week');
53 | const endDate = d.endOf('day');
54 |
55 | var result: ({ value: number, month: number } | null)[][] = [];
56 | for (var i = 0; i < columns; i++) {
57 | result[i] = [];
58 | for (var j = 0; j < 7; j++) {
59 | var date = lastWeekend.subtract((columns - i - 1) * 7 + (6 - j), 'day');
60 | if (date <= endDate) {
61 | result[i][j] = {
62 | value: history[date.format(this.props.dateFormat)] || 0,
63 | month: date.month()
64 | };
65 | } else {
66 | result[i][j] = null;
67 | }
68 | }
69 | }
70 |
71 | return result;
72 | }
73 |
74 | render() {
75 | const columns = this.state.columns;
76 | const values = this.props.values;
77 | const until = this.props.until;
78 |
79 | // TODO: More sophisticated typing
80 | if (this.props.panelColors == undefined || this.props.weekNames == undefined || this.props.monthNames == undefined) {
81 | return;
82 | }
83 |
84 | var contributions = this.makeCalendarData(values, until, columns);
85 | var innerDom: ReactElement[] = [];
86 |
87 | // panels
88 | for (var i = 0; i < columns; i++) {
89 | for (var j = 0; j < 7; j++) {
90 | var contribution = contributions[i][j];
91 | if (contribution === null) continue;
92 | const pos = this.getPanelPosition(i, j);
93 | const numOfColors = this.props.panelColors.length
94 | const color =
95 | contribution.value >= numOfColors
96 | ? this.props.panelColors[numOfColors - 1]
97 | : this.props.panelColors[contribution.value];
98 | const dom = (
99 |
108 | );
109 | innerDom.push(dom);
110 | }
111 | }
112 |
113 | // week texts
114 | for (var i = 0; i < this.props.weekNames.length; i++) {
115 | const textBasePos = this.getPanelPosition(0, i);
116 | const dom = (
117 |
129 | { this.props.weekNames[i] }
130 |
131 | );
132 | innerDom.push(dom);
133 | }
134 |
135 | // month texts
136 | var prevMonth = -1;
137 | for (var i = 0; i < columns; i++) {
138 | const c = contributions[i][0];
139 | if (c === null) continue;
140 | if (columns > 1 && i == 0 && c.month != contributions[i + 1][0]?.month) {
141 | // skip first month name to avoid text overlap
142 | continue;
143 | }
144 | if (c.month != prevMonth) {
145 | var textBasePos = this.getPanelPosition(i, 0);
146 | innerDom.push(
158 | { this.props.monthNames[c.month] }
159 |
160 | );
161 | }
162 | prevMonth = c.month;
163 | }
164 |
165 | return (
166 | this.updateSize(rect.bounds) }>
167 | { ({ measureRef }: any) => (
168 |
169 |
177 |
178 | ) }
179 |
180 | );
181 | }
182 |
183 | updateSize(size?: BoundingRect) {
184 | if (!size) return;
185 |
186 | const visibleWeeks = Math.floor((size.width - this.weekLabelWidth) / 13);
187 | this.setState({
188 | columns: Math.min(visibleWeeks, this.state.maxWidth)
189 | });
190 | }
191 | };
192 |
193 | // @ts-ignore
194 | GitHubCalendar.defaultProps = {
195 | weekNames: ['', 'M', '', 'W', '', 'F', ''],
196 | monthNames: [
197 | 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
198 | 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'
199 | ],
200 | panelColors: ['#EEE', '#DDD', '#AAA', '#444'],
201 | dateFormat: 'YYYY-MM-DD'
202 | };
203 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | /* Basic Options */
4 | // "incremental": true, /* Enable incremental compilation */
5 | "target": "es2015", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
6 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
7 | // "lib": [], /* Specify library files to be included in the compilation. */
8 | // "allowJs": true, /* Allow javascript files to be compiled. */
9 | // "checkJs": true, /* Report errors in .js files. */
10 | "jsx": "react", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
11 | "declaration": true, /* Generates corresponding '.d.ts' file. */
12 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
13 | // "sourceMap": true, /* Generates corresponding '.map' file. */
14 | // "outFile": "./", /* Concatenate and emit output to single file. */
15 | "outDir": "./lib", /* Redirect output structure to the directory. */
16 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
17 | // "composite": true, /* Enable project compilation */
18 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
19 | // "removeComments": true, /* Do not emit comments to output. */
20 | // "noEmit": true, /* Do not emit outputs. */
21 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */
22 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
23 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
24 |
25 | /* Strict Type-Checking Options */
26 | "strict": true, /* Enable all strict type-checking options. */
27 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
28 | "strictNullChecks": true, /* Enable strict null checks. */
29 | "strictFunctionTypes": false, /* Enable strict checking of function types. */
30 | "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
31 | "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
32 | "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
33 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
34 |
35 | /* Additional Checks */
36 | "noUnusedLocals": true, /* Report errors on unused locals. */
37 | "noUnusedParameters": true, /* Report errors on unused parameters. */
38 | "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
39 | "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
40 |
41 | /* Module Resolution Options */
42 | "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
43 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
44 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
45 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
46 | // "typeRoots": [], /* List of folders to include type definitions from. */
47 | // "types": [], /* Type declaration files to be included in compilation. */
48 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
49 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
50 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
51 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
52 |
53 | /* Source Map Options */
54 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
55 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
56 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
57 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
58 |
59 | /* Experimental Options */
60 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
61 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
62 | }
63 | }
64 |
--------------------------------------------------------------------------------