├── .editorconfig
├── .eslintignore
├── .eslintrc
├── .gitignore
├── .prettierrc
├── .travis.yml
├── README.md
├── example
├── README.md
├── package-lock.json
├── package.json
├── public
│ ├── favicon.ico
│ ├── index.html
│ └── manifest.json
├── screenshot.png
├── src
│ ├── App.test.tsx
│ ├── App.tsx
│ ├── index.css
│ ├── index.tsx
│ ├── react-app-env.d.ts
│ └── setupTests.ts
├── tsconfig.json
└── yarn.lock
├── package-lock.json
├── package.json
├── src
├── .eslintrc
├── Cell.tsx
├── Column.tsx
├── Row.tsx
├── XLabels.tsx
├── YLabelAligner.tsx
├── YLabels.tsx
├── index.test.tsx
├── index.tsx
├── react-app-env.d.ts
├── typings.d.ts
└── useElementHeight.ts
├── tsconfig.json
└── tsconfig.test.json
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | indent_style = space
6 | indent_size = 2
7 | end_of_line = lf
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | build/
2 | dist/
3 | node_modules/
4 | .snapshots/
5 | *.min.js
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "@typescript-eslint/parser",
3 | "extends": [
4 | "standard",
5 | "standard-react",
6 | "plugin:prettier/recommended",
7 | "prettier",
8 | "plugin:@typescript-eslint/eslint-recommended"
9 | ],
10 | "env": {
11 | "node": true
12 | },
13 | "parserOptions": {
14 | "ecmaVersion": 2020,
15 | "ecmaFeatures": {
16 | "legacyDecorators": true,
17 | "jsx": true
18 | }
19 | },
20 | "settings": {
21 | "react": {
22 | "version": "17"
23 | }
24 | },
25 | "rules": {
26 | "space-before-function-paren": 0,
27 | "react/prop-types": 0,
28 | "react/jsx-handler-names": 0,
29 | "react/jsx-fragments": 0,
30 | "react/no-unused-prop-types": 0,
31 | "import/export": 0,
32 | "no-use-before-define": "off"
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | # See https://help.github.com/ignore-files/ for more about ignoring files.
3 |
4 | # dependencies
5 | node_modules
6 |
7 | # builds
8 | build
9 | dist
10 | .rpt2_cache
11 |
12 | # misc
13 | .DS_Store
14 | .env
15 | .env.local
16 | .env.development.local
17 | .env.test.local
18 | .env.production.local
19 |
20 | npm-debug.log*
21 | yarn-debug.log*
22 | yarn-error.log*
23 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "jsxSingleQuote": true,
4 | "semi": false,
5 | "tabWidth": 2,
6 | "bracketSpacing": true,
7 | "jsxBracketSameLine": false,
8 | "arrowParens": "always",
9 | "trailingComma": "none"
10 | }
11 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - 12
4 | - 10
5 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | # react-grid-heatmap
3 |
4 | A React component to visualize heatmap in a grid layout without using any 3rd party libraries.
5 |
6 | [Demo - Try in Sandbox](https://codesandbox.io/s/react-grid-heatmap-hhmqw?file=/src/App.js)
7 |
8 | 
9 |
10 | [](https://www.npmjs.com/package/react-grid-heatmap) [](https://standardjs.com) [](https://travis-ci.org/arunghosh/react-grid-heatmap)
11 |
12 | ## Install
13 |
14 | ```bash
15 | npm install --save react-grid-heatmap
16 | ```
17 |
18 | ## Usage
19 |
20 | **Mandatory fields**
21 |
22 | | Name | Type | Sample |
23 | | ---- | ------------ | ----------------------------- |
24 | | data | `number[][]` | `[[1,2,3], [4,5,6], [7,8,9]]` |
25 |
26 |
27 |
28 | ```tsx
29 | import React from 'react'
30 | import { HeatMapGrid } from 'react-grid-heatmap'
31 |
32 | const xLabels = new Array(24).fill(0).map((_, i) => `${i}`)
33 | const yLabels = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri']
34 | const data = new Array(yLabels.length)
35 | .fill(0)
36 | .map(() =>
37 | new Array(xLabels.length).fill(0).map(() => Math.floor(Math.random() * 50 + 50))
38 | )
39 |
40 | const App = () => {
41 | return (
42 |
47 | )
48 | }
49 |
50 | export default App
51 | ```
52 |
53 | **Optional Parameters**
54 |
55 | | Name | Type | Description/Example | Default Value |
56 | | :----------- | :------- | :------------------------------------------------------ | :------------ |
57 | | xLabels | string[] | `['1am', '2am', '3am']` | `null` |
58 | | yLabels | string[] | `['Sun', 'Mon']` | `null` |
59 | | cellHeight | string | Height of each cell of the heatmap | `"2rem"` |
60 | | onClick | function | Adds an handler to cell click
`(x, y) => void` | `null` |
61 | | square | boolean | If set to `true` will render cells as square | `false` |
62 | | xLabelsPos | string | Location of y labels. It can be `top` or `bottom` | `"top"` |
63 | | yLabelsPos | string | Location of y labels. It can be `left` or `right` | `"left"` |
64 | | cellRender | function | Render custom content in cell.
`(x, y, value) => ()` | `null` |
65 | | cellStyle | function | To set custom cell style
`(x, y, ratio) => {}` | `null` |
66 | | xLabelsStyle | function | To set custom cell style
`(index) => {}` | `null` |
67 | | yLabelsStyle | function | To set custom cell style
`(index) => {}` | `null` |
68 |
69 |
70 | A sample code with all parameters
71 | ```js
72 | import React from 'react'
73 | import { HeatMapGrid } from 'react-grid-heatmap'
74 |
75 | const xLabels = new Array(24).fill(0).map((_, i) => `${i}`)
76 | const yLabels = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri']
77 | const data = new Array(yLabels.length)
78 | .fill(0)
79 | .map(() =>
80 | new Array(xLabels.length)
81 | .fill(0)
82 | .map(() => Math.floor(Math.random() * 50 + 50))
83 | )
84 |
85 | const App = () => {
86 | return (
87 |
92 |
(
98 | {value}
99 | )}
100 | xLabelsStyle={(index) => ({
101 | color: index % 2 ? 'transparent' : '#777',
102 | fontSize: '.8rem'
103 | })}
104 | yLabelsStyle={() => ({
105 | fontSize: '.7rem',
106 | textTransform: 'uppercase',
107 | color: '#777'
108 | })}
109 | cellStyle={(_x, _y, ratio) => ({
110 | background: `rgb(12, 160, 44, ${ratio})`,
111 | fontSize: '.8rem',
112 | color: `rgb(0, 0, 0, ${ratio / 2 + 0.4})`
113 | })}
114 | cellHeight='2rem'
115 | xLabelsPos='bottom'
116 | onClick={(x, y) => alert(`Clicked (${x}, ${y})`)}
117 | yLabelsPos='right'
118 | square
119 | />
120 |
121 | )
122 | }
123 |
124 | export default App
125 |
126 | ```
127 |
128 | ## Local development
129 | Local development is broken into two parts (ideally using two tabs).
130 |
131 | First, run rollup to watch your `src/` module and automatically recompile it into `dist/` whenever you make changes.
132 |
133 | ```
134 | npm start # runs rollup with watch flag
135 | ```
136 |
137 | The second part will be running the `example/` create-react-app that's linked to the local version of your module.
138 |
139 | ```
140 | # (in another tab)
141 | cd example
142 | npm start # runs create-react-app dev server
143 | ```
144 | Now, anytime you make a change to your library in `src/` or to the example app's `example/src`, `create-react-app` will live-reload your local dev server so you can iterate on your component in real-time.
145 |
146 |
147 | ## License
148 |
149 | MIT © [arunghosh](https://github.com/arunghosh)
150 |
--------------------------------------------------------------------------------
/example/README.md:
--------------------------------------------------------------------------------
1 | This example was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
2 |
3 | It is linked to the react-grid-heatmap package in the parent directory for development purposes.
4 |
5 | You can run `yarn install` and then `yarn start` to test your package.
6 |
--------------------------------------------------------------------------------
/example/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-grid-heatmap-example",
3 | "homepage": ".",
4 | "version": "1.0.0",
5 | "private": true,
6 | "scripts": {
7 | "start": "node ../node_modules/react-scripts/bin/react-scripts.js start",
8 | "build": "node ../node_modules/react-scripts/bin/react-scripts.js build",
9 | "test": "node ../node_modules/react-scripts/bin/react-scripts.js test",
10 | "eject": "node ../node_modules/react-scripts/bin/react-scripts.js eject"
11 | },
12 | "dependencies": {
13 | "@testing-library/jest-dom": "file:../node_modules/@testing-library/jest-dom",
14 | "@testing-library/react": "file:../node_modules/@testing-library/react",
15 | "@testing-library/user-event": "file:../node_modules/@testing-library/user-event",
16 | "@types/jest": "file:../node_modules/@types/jest",
17 | "@types/node": "file:../node_modules/@types/node",
18 | "@types/react": "file:../node_modules/@types/react",
19 | "@types/react-dom": "file:../node_modules/@types/react-dom",
20 | "react": "file:../node_modules/react",
21 | "react-dom": "file:../node_modules/react-dom",
22 | "react-scripts": "file:../node_modules/react-scripts",
23 | "react-grid-heatmap": "file:.."
24 | },
25 | "devDependencies": {
26 | "@babel/plugin-syntax-object-rest-spread": "^7.8.3"
27 | },
28 | "eslintConfig": {
29 | "extends": "react-app"
30 | },
31 | "browserslist": {
32 | "production": [
33 | ">0.2%",
34 | "not dead",
35 | "not op_mini all"
36 | ],
37 | "development": [
38 | "last 1 chrome version",
39 | "last 1 firefox version",
40 | "last 1 safari version"
41 | ]
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/example/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arunghosh/react-grid-heatmap/3f4caf8cdc53336ab1beff4cc2ab6bec488f3840/example/public/favicon.ico
--------------------------------------------------------------------------------
/example/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
10 |
11 |
12 |
16 |
17 |
18 |
27 | react-grid-heatmap
28 |
29 |
30 |
31 |
34 |
35 |
36 |
37 |
47 |
48 |
49 |
--------------------------------------------------------------------------------
/example/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "react-grid-heatmap",
3 | "name": "react-grid-heatmap",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": ".",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/example/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arunghosh/react-grid-heatmap/3f4caf8cdc53336ab1beff4cc2ab6bec488f3840/example/screenshot.png
--------------------------------------------------------------------------------
/example/src/App.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom'
3 | import App from './App'
4 |
5 | it('renders without crashing', () => {
6 | const div = document.createElement('div')
7 | ReactDOM.render(, div)
8 | ReactDOM.unmountComponentAtNode(div)
9 | })
10 |
--------------------------------------------------------------------------------
/example/src/App.tsx:
--------------------------------------------------------------------------------
1 | import { HeatMapGrid } from 'react-grid-heatmap'
2 |
3 | const xLabels = new Array(24).fill(0).map((_, i) => `${i}`)
4 | const yLabels = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri']
5 | const data = new Array(yLabels.length)
6 | .fill(0)
7 | .map(() =>
8 | new Array(xLabels.length)
9 | .fill(0)
10 | .map(() => Math.floor(Math.random() * 50 + 50))
11 | )
12 |
13 | const App = () => {
14 | return (
15 |
20 |
(
26 | {value}
27 | )}
28 | xLabelsStyle={(index) => ({
29 | color: index % 2 ? 'transparent' : '#777',
30 | fontSize: '.8rem'
31 | })}
32 | yLabelsStyle={() => ({
33 | fontSize: '.7rem',
34 | textTransform: 'uppercase',
35 | color: '#777'
36 | })}
37 | cellStyle={(_x, _y, ratio) => ({
38 | background: `rgb(12, 160, 44, ${ratio})`,
39 | fontSize: '.8rem',
40 | color: `rgb(0, 0, 0, ${ratio / 2 + 0.4})`
41 | })}
42 | cellHeight='2rem'
43 | xLabelsPos='bottom'
44 | onClick={(x, y) => alert(`Clicked (${x}, ${y})`)}
45 | yLabelsPos='right'
46 | square
47 | />
48 |
49 | )
50 | }
51 |
52 | export default App
53 |
--------------------------------------------------------------------------------
/example/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | padding: 0;
4 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
5 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
6 | sans-serif;
7 | -webkit-font-smoothing: antialiased;
8 | -moz-osx-font-smoothing: grayscale;
9 | }
10 |
11 | code {
12 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
13 | monospace;
14 | }
15 |
--------------------------------------------------------------------------------
/example/src/index.tsx:
--------------------------------------------------------------------------------
1 | import './index.css'
2 |
3 | import ReactDOM from 'react-dom'
4 | import App from './App'
5 |
6 | ReactDOM.render(, document.getElementById('root'))
7 |
--------------------------------------------------------------------------------
/example/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/example/src/setupTests.ts:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom/extend-expect';
6 |
--------------------------------------------------------------------------------
/example/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "outDir": "dist",
4 | "module": "esnext",
5 | "lib": [
6 | "dom",
7 | "esnext"
8 | ],
9 | "moduleResolution": "node",
10 | "jsx": "react-jsx",
11 | "sourceMap": true,
12 | "declaration": true,
13 | "esModuleInterop": true,
14 | "noImplicitReturns": true,
15 | "noImplicitThis": true,
16 | "noImplicitAny": true,
17 | "strictNullChecks": true,
18 | "suppressImplicitAnyIndexErrors": true,
19 | "noUnusedLocals": true,
20 | "noUnusedParameters": true,
21 | "allowSyntheticDefaultImports": true,
22 | "target": "es5",
23 | "allowJs": true,
24 | "skipLibCheck": true,
25 | "strict": true,
26 | "forceConsistentCasingInFileNames": true,
27 | "resolveJsonModule": true,
28 | "isolatedModules": true,
29 | "noEmit": true,
30 | "noFallthroughCasesInSwitch": true
31 | },
32 | "include": [
33 | "src"
34 | ],
35 | "exclude": [
36 | "node_modules",
37 | "build"
38 | ]
39 | }
40 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-grid-heatmap",
3 | "version": "1.3.0",
4 | "description": "A react component for heatmap visualisation in grid layout",
5 | "author": "arunghosh",
6 | "license": "MIT",
7 | "repository": "arunghosh/react-grid-heatmap",
8 | "main": "dist/index.js",
9 | "module": "dist/index.modern.js",
10 | "source": "src/index.tsx",
11 | "engines": {
12 | "node": ">=10"
13 | },
14 | "scripts": {
15 | "build": "microbundle-crl --no-compress --format modern,cjs",
16 | "start": "microbundle-crl watch --no-compress --format modern,cjs",
17 | "prepare": "run-s build",
18 | "test": "run-s test:unit test:lint test:build",
19 | "test:build": "run-s build",
20 | "test:lint": "eslint src/**/*.tsx",
21 | "test:unit": "cross-env CI=1 react-scripts test --env=jsdom",
22 | "test:watch": "react-scripts test --env=jsdom",
23 | "predeploy": "cd example && yarn install && yarn run build",
24 | "deploy": "gh-pages -d example/build"
25 | },
26 | "peerDependencies": {
27 | "react": "^17 || ^18"
28 | },
29 | "devDependencies": {
30 | "@testing-library/jest-dom": "^5.12.0",
31 | "@testing-library/react": "^11.2.7",
32 | "@testing-library/user-event": "^13.1.9",
33 | "@types/jest": "^26.0.23",
34 | "@types/node": "^15.6.0",
35 | "@types/react": "^17.0.6",
36 | "@types/react-dom": "^17.0.5",
37 | "@typescript-eslint/eslint-plugin": "^4.24.0",
38 | "@typescript-eslint/parser": "^4.24.0",
39 | "babel-eslint": "^10.0.3",
40 | "cross-env": "^7.0.3",
41 | "eslint-config-prettier": "^8.3.0",
42 | "eslint-config-standard": "^16.0.2",
43 | "eslint-config-standard-react": "^11.0.1",
44 | "eslint-plugin-import": "^2.23.3",
45 | "eslint-plugin-node": "^11.1.0",
46 | "eslint-plugin-prettier": "^3.4.0",
47 | "eslint-plugin-promise": "^4.2.1",
48 | "eslint-plugin-react": "^7.23.2",
49 | "eslint-plugin-standard": "^4.1.0",
50 | "gh-pages": "^3.1.0",
51 | "microbundle-crl": "^0.13.11",
52 | "npm-run-all": "^4.1.5",
53 | "prettier": "^2.3.0",
54 | "react": "^17.0.2",
55 | "react-dom": "^17.0.2",
56 | "react-scripts": "^4.0.3",
57 | "typescript": "^4.2.4"
58 | },
59 | "files": [
60 | "dist"
61 | ]
62 | }
63 |
--------------------------------------------------------------------------------
/src/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "jest": true
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/src/Cell.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | interface Props {
4 | render?: (x: number, y: number, ratio: number) => {}
5 | posX: number
6 | posY: number
7 | style?: (x: number, y: number, ratio: number) => {}
8 | ratio: number
9 | value: number
10 | height?: string
11 | square?: boolean
12 | onClick?: (x: number, y: number) => void
13 | }
14 |
15 | const noop = (returnVal: any) => () => returnVal
16 |
17 | const Cell = ({
18 | render = noop(null),
19 | style = noop({}),
20 | ratio,
21 | posX,
22 | posY,
23 | square = false,
24 | height = '2rem',
25 | value,
26 | onClick
27 | }: Props) => {
28 | return (
29 | (onClick || noop({}))(posX, posY)}
31 | style={{
32 | border: '1px solid #fff',
33 | borderWidth: '1px 1px 0 0',
34 | textAlign: 'center',
35 | color: `rgb(0, 0, 0, ${ratio / 2 + 0.4})`,
36 | overflow: 'hidden',
37 | boxSizing: 'border-box',
38 | flexGrow: square ? 0 : 1,
39 | flexBasis: square ? height : 0,
40 | flexShrink: 0,
41 | height: height,
42 | lineHeight: height,
43 | borderRadius: '4px',
44 | fontSize: '.8rem',
45 | cursor: onClick ? 'pointer' : 'initial',
46 | background: `rgb(12, 160, 44, ${ratio + 0.05})`,
47 | ...style(posX, posY, ratio)
48 | }}
49 | >
50 | {render(posX, posY, value)}
51 |
52 | )
53 | }
54 |
55 | export default React.memo(Cell)
56 |
--------------------------------------------------------------------------------
/src/Column.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | interface Props {
4 | children: any
5 | reverse?: boolean
6 | grow?: boolean
7 | }
8 |
9 | export default function Column({
10 | children,
11 | grow = false,
12 | reverse = false
13 | }: Props) {
14 | return (
15 |
22 | {children}
23 |
24 | )
25 | }
26 |
--------------------------------------------------------------------------------
/src/Row.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | interface Props {
4 | children: any
5 | reverse?: boolean
6 | }
7 |
8 | export default function Row({ children, reverse = false }: Props) {
9 | return (
10 |
17 | {children}
18 |
19 | )
20 | }
21 |
--------------------------------------------------------------------------------
/src/XLabels.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | interface Props {
4 | labels: string[]
5 | xLabelsStyle?: (index: number) => {}
6 | square?: boolean
7 | height: string
8 | }
9 |
10 | export default function XLabels({
11 | labels,
12 | xLabelsStyle = () => ({}),
13 | height,
14 | square = false
15 | }: Props) {
16 | const widthPercent = `${100 / labels.length}%`
17 | return (
18 |
19 | {labels.map((label, index) => (
20 |
33 | {label}
34 |
35 | ))}
36 |
37 | )
38 | }
39 |
--------------------------------------------------------------------------------
/src/YLabelAligner.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | interface Props {
4 | xLabelHeight: number
5 | isXLabelReverse: boolean
6 | children: any
7 | }
8 |
9 | export default function YLabelAligner({
10 | xLabelHeight,
11 | isXLabelReverse,
12 | children
13 | }: Props) {
14 | const style = {
15 | [isXLabelReverse ? 'marginBottom' : 'marginTop']: `${xLabelHeight}px`
16 | }
17 | return {children}
18 | }
19 |
--------------------------------------------------------------------------------
/src/YLabels.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | interface Props {
4 | labels: string[]
5 | height: string
6 | reverse?: boolean
7 | yLabelsStyle?: (index: number) => {}
8 | }
9 |
10 | export default function YLabels({
11 | labels,
12 | height,
13 | yLabelsStyle = () => ({}),
14 | reverse = false
15 | }: Props) {
16 | return (
17 |
24 | {labels.map((label, index) => (
25 |
34 | {label}
35 |
36 | ))}
37 |
38 | )
39 | }
40 |
--------------------------------------------------------------------------------
/src/index.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import '@testing-library/jest-dom'
3 | import { screen, render, fireEvent } from '@testing-library/react'
4 | import { HeatMapGrid } from '.'
5 |
6 | const xLabels = new Array(10).fill(0).map((_, i) => `${i}Hr`)
7 | const yLabels = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri']
8 | const data = new Array(yLabels.length)
9 | .fill(0)
10 | .map(() =>
11 | new Array(xLabels.length)
12 | .fill(0)
13 | .map(() => Math.floor(Math.random() * Math.random() * 200))
14 | )
15 |
16 | describe('The HeatMapGrid component', () => {
17 | it('should render if madatory attribute data is provided', () => {
18 | render()
19 | })
20 |
21 | it('should render properly with labels provided', () => {
22 | render(
23 | {value}
}
27 | xLabels={xLabels}
28 | />
29 | )
30 | expect(screen.getByText('Sun')).toBeInTheDocument()
31 | expect(screen.getAllByText(data[0][0].toString())[0]).toBeInTheDocument()
32 | })
33 |
34 | it('should not throw any exception when user clicks on the cell with onClick callback not provided', () => {
35 | render(
36 | {value}
}
39 | yLabels={yLabels}
40 | />
41 | )
42 | expect(screen.getAllByText(data[0][0].toString())[0]).toBeInTheDocument()
43 | fireEvent.click(screen.getAllByText(data[0][0].toString())[0])
44 | })
45 |
46 | it('should invoke onClick callback when user clicks on a cell', () => {
47 | const onClick = jest.fn()
48 | render(
49 | {value}
}
53 | yLabels={yLabels}
54 | />
55 | )
56 | expect(screen.getAllByText(data[0][0].toString())[0]).toBeInTheDocument()
57 | fireEvent.click(screen.getAllByText(data[0][0].toString())[0])
58 | expect(onClick).toHaveBeenCalledTimes(1)
59 | })
60 |
61 | it('should render properly with all its attributes provided', () => {
62 | render(
63 | (
68 | {value}
69 | )}
70 | xLabelsStyle={(index) => ({
71 | color: index % 2 ? 'transparent' : '#777',
72 | fontSize: '.8rem'
73 | })}
74 | yLabelsStyle={() => ({
75 | fontSize: '.7rem',
76 | textTransform: 'uppercase',
77 | color: '#777'
78 | })}
79 | cellStyle={(_, __, ratio) => ({
80 | background: `rgb(12, 160, 44, ${ratio})`,
81 | fontSize: '.8rem',
82 | color: `rgb(0, 0, 0, ${ratio / 2 + 0.4})`
83 | })}
84 | cellHeight='2rem'
85 | xLabelsPos='bottom'
86 | yLabelsPos='right'
87 | square
88 | />
89 | )
90 | expect(screen.getByText('Sun')).toBeInTheDocument()
91 | })
92 | })
93 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import Cell from './Cell'
3 | import Row from './Row'
4 | import XLabels from './XLabels'
5 | import Column from './Column'
6 | import YLabels from './YLabels'
7 | import YLabelAligner from './YLabelAligner'
8 | import useElementHeight from './useElementHeight'
9 |
10 | export interface HeatMapGridProps {
11 | data: number[][]
12 | xLabels?: string[]
13 | yLabels?: string[]
14 | cellHeight?: string
15 | square?: boolean
16 | xLabelsPos?: 'top' | 'bottom'
17 | yLabelsPos?: 'left' | 'right'
18 | xLabelsStyle?: (index: number) => {}
19 | yLabelsStyle?: (index: number) => {}
20 | cellStyle?: (x: number, y: number, ratio: number) => {}
21 | cellRender?: (x: number, y: number, value: number) => {}
22 | onClick?: (x: number, y: number) => void
23 | }
24 |
25 | function getMinMax(data: number[][]): [number, number] {
26 | const flatArray = data.reduce((i, o) => [...o, ...i], [])
27 | const max = Math.max(...flatArray)
28 | const min = Math.min(...flatArray)
29 | return [min, max]
30 | }
31 |
32 | export const HeatMapGrid = ({
33 | data,
34 | xLabels,
35 | yLabels,
36 | xLabelsPos = 'top',
37 | yLabelsPos = 'left',
38 | square = false,
39 | cellHeight = '2px',
40 | xLabelsStyle,
41 | yLabelsStyle,
42 | cellStyle,
43 | cellRender,
44 | onClick
45 | }: HeatMapGridProps) => {
46 | const [xLabelHeight, xLabelRef] = useElementHeight(22)
47 | const [min, max] = getMinMax(data)
48 | const minMaxDiff = max - min
49 | const isXLabelReverse = xLabelsPos === 'bottom'
50 | const isYLabelReverse = yLabelsPos === 'right'
51 |
52 | return (
53 |
54 | {yLabels && (
55 |
59 |
65 |
66 | )}
67 |
68 |
69 | {xLabels && (
70 |
76 | )}
77 |
78 |
79 | {data.map((rowItems, xi) => (
80 |
81 | {rowItems.map((value, yi) => (
82 | |
94 | ))}
95 |
96 | ))}
97 |
98 |
99 |
100 | )
101 | }
102 |
--------------------------------------------------------------------------------
/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/src/typings.d.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Default CSS definition for typescript,
3 | * will be overridden with file-specific definitions by rollup
4 | */
5 | declare module '*.css' {
6 | const content: { [className: string]: string };
7 | export default content;
8 | }
9 |
10 | interface SvgrComponent extends React.StatelessComponent> {}
11 |
12 | declare module '*.svg' {
13 | const svgUrl: string;
14 | const svgComponent: SvgrComponent;
15 | export default svgUrl;
16 | export { svgComponent as ReactComponent }
17 | }
18 |
--------------------------------------------------------------------------------
/src/useElementHeight.ts:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | interface ClientHeight {
4 | clientHeight: number
5 | }
6 |
7 | export default function useElementHeight(
8 | initHeight: number
9 | ): [number, React.MutableRefObject] {
10 | const eleRef = React.useRef(null)
11 | const [eleHeight, setEleHeight] = React.useState(initHeight)
12 |
13 | React.useEffect(() => {
14 | if (eleRef.current) {
15 | const height = ((eleRef.current || {}) as ClientHeight).clientHeight
16 | setEleHeight(height)
17 | }
18 | }, [])
19 |
20 | return [eleHeight, eleRef]
21 | }
22 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "outDir": "dist",
4 | "module": "esnext",
5 | "lib": ["dom", "esnext"],
6 | "moduleResolution": "node",
7 | "jsx": "preserve",
8 | "sourceMap": true,
9 | "declaration": true,
10 | "esModuleInterop": true,
11 | "noImplicitReturns": true,
12 | "noImplicitThis": true,
13 | "noImplicitAny": true,
14 | "strictNullChecks": true,
15 | "suppressImplicitAnyIndexErrors": true,
16 | "noUnusedLocals": true,
17 | "noUnusedParameters": true,
18 | "allowSyntheticDefaultImports": true,
19 | "target": "es5",
20 | "allowJs": true,
21 | "skipLibCheck": true,
22 | "strict": true,
23 | "forceConsistentCasingInFileNames": true,
24 | "resolveJsonModule": true,
25 | "isolatedModules": true,
26 | "noEmit": true,
27 | "noFallthroughCasesInSwitch": true
28 | },
29 | "include": ["./src/**/*.ts"],
30 | "exclude": ["node_modules", "dist", "example"]
31 | }
32 |
--------------------------------------------------------------------------------
/tsconfig.test.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "module": "commonjs"
5 | }
6 | }
--------------------------------------------------------------------------------