├── .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 | ![Screenshot](https://raw.githubusercontent.com/arunghosh/react-grid-heatmap/master/example/screenshot.png) 9 | 10 | [![NPM](https://img.shields.io/npm/v/react-grid-heatmap.svg)](https://www.npmjs.com/package/react-grid-heatmap) [![JavaScript Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://standardjs.com) [![Build Status](https://travis-ci.org/arunghosh/react-grid-heatmap.svg?branch=master)](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 | } --------------------------------------------------------------------------------