├── .babelrc ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── dist ├── bundle.js ├── bundle.js.LICENSE.txt └── bundle.js.map ├── docs ├── heatmap.png └── screenshot.png ├── example └── index.js ├── jest.config.js ├── package-lock.json ├── package.json ├── public └── index.html ├── src ├── DataGrid.jsx ├── FixedBox.jsx ├── HeatMap.jsx ├── XLabels.jsx └── index.js ├── test ├── DataGrid.spec.jsx └── HeatMap.spec.jsx └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env", "@babel/preset-react"] 3 | } 4 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | test 2 | example -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "parser": "babel-eslint", 3 | "extends": ["airbnb", "prettier"], 4 | "plugins": [ 5 | "react" 6 | ], 7 | "rules": { 8 | "jsx-filename-extension": 0 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.swp 3 | .idea 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "node" 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Arun Ghosh 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-heatmap-grid 2 | 3 | Created a [new version](https://github.com/arunghosh/react-grid-heatmap) of this having smaller size and a better interface. Check it out. 4 | 5 | A React component for heatmap in grid layout using `div`. 6 | 7 | **Live example [here](https://codesandbox.io/s/r4rvwkl3yn)**. 8 | 9 | ![image](https://github.com/arunghosh/react-heatmap-grid/blob/master/docs/heatmap.png?raw=true) 10 | 11 | ## Installation 12 | 13 | ``` 14 | yarn add react-heatmap-grid 15 | ``` 16 | 17 | or 18 | 19 | ``` 20 | npm install react-heatmap-grid --save 21 | ``` 22 | 23 | ## Usage 24 | 25 | **Mandatory fields** 26 | 27 | | Name | Type | Sample | 28 | | --------- | -------------------------------------------------------------------------- | ----------------------- | 29 | | `xLabels` | Array of string | `['1am', '2am', '3am']` | 30 | | `yLabels` | Array of string | `['Sun', 'Mon']` | 31 | | `data` | 2D Array of numbers having `yLabels.length` rows and `xLabels.length` rows | `[[2,3,5][5,6,9]]` | 32 | 33 | ```javascript 34 | const xLabels = new Array(24).fill(0).map((_, i) => `${i}`); 35 | const yLabels = ["Sun", "Mon", "Tue"]; 36 | const data = new Array(yLabels.length) 37 | .fill(0) 38 | .map(() => 39 | new Array(xLabels.length).fill(0).map(() => Math.floor(Math.random() * 100)) 40 | ); 41 | 42 | ReactDOM.render( 43 | , 44 | document.getElementById("app") 45 | ); 46 | ``` 47 | 48 | **Configuration** 49 | 50 | | Name | Type | Description | Default Value | 51 | | ----------------- | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ | 52 | | background | string | The base color for the heatmap | `"#329fff"` | 53 | | height | number | Height of each cell of the heatmap in px | `30` | 54 | | onClick | function | Adds an handler to cell click | `undefined` | 55 | | squares | boolean | If set to `true` will render cells as square | `false` | 56 | | xLabelWidth | number | Width of the x label area in pixel | `60` | 57 | | yLabelWidth | number | Width of the y label area in pixel | `40` | 58 | | yLabelTextAlign | string | Text alignment of the yLabels | `"right"` | 59 | | xLabelsLocation | string | Location of y labels. It can be top or bottom | `"top"` | 60 | | xLabelsVisibility | `[boolean]` | Array of bool conveying which x labels to display. For ex: `[true, false, true, false]` means the 1st and the 3rd labels will be displayed and the 2nd and 4th will be hidden | | 61 | | unit | string | Unit to display next to the value on hover | | 62 | | cellRender | function | Render custom content in cell | `() => null` | 63 | | cellStyle | function | To set custom cell style. It is useful for using own colour scheme | | 64 | | title | function | To render custom title in each cell | `${value} ${unit}` | 65 | 66 | Example 67 | 68 | ```javascript 69 | const xLabels = new Array(24).fill(0).map((_, i) => `${i}`); 70 | 71 | // Display only even labels 72 | const xLabelsVisibility = new Array(24) 73 | .fill(0) 74 | .map((_, i) => (i % 2 === 0 ? true : false)); 75 | 76 | const yLabels = ["Sun", "Mon", "Tue"]; 77 | const data = new Array(yLabels.length) 78 | .fill(0) 79 | .map(() => 80 | new Array(xLabels.length).fill(0).map(() => Math.floor(Math.random() * 100)) 81 | ); 82 | 83 | ReactDOM.render( 84 | alert(`Clicked ${x}, ${y}`)} 93 | cellStyle={(background, value, min, max, data, x, y) => ({ 94 | background: `rgba(66, 86, 244, ${1 - (max - value) / (max - min)})`, 95 | fontSize: "11px", 96 | })} 97 | cellRender={(value) => value && `${value}%`} 98 | title={(value, unit) => `${value}`} 99 | />, 100 | document.getElementById("app") 101 | ); 102 | ``` 103 | 104 | ### For developers 105 | 106 | **New build** 107 | 108 | ``` 109 | npm run build 110 | ``` 111 | 112 | **Run dev server** 113 | 114 | ``` 115 | npm run dev 116 | ``` 117 | 118 | **Run test** 119 | 120 | ``` 121 | npm run test 122 | ``` 123 | 124 | Buy Me A Coffee 125 | -------------------------------------------------------------------------------- /dist/bundle.js: -------------------------------------------------------------------------------- 1 | /*! For license information please see bundle.js.LICENSE.txt */ 2 | !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports["react-sequence"]=t():e["react-sequence"]=t()}(self,(function(){return(()=>{var e={418:e=>{"use strict";var t=Object.getOwnPropertySymbols,r=Object.prototype.hasOwnProperty,n=Object.prototype.propertyIsEnumerable;function o(e){if(null==e)throw new TypeError("Object.assign cannot be called with null or undefined");return Object(e)}e.exports=function(){try{if(!Object.assign)return!1;var e=new String("abc");if(e[5]="de","5"===Object.getOwnPropertyNames(e)[0])return!1;for(var t={},r=0;r<10;r++)t["_"+String.fromCharCode(r)]=r;if("0123456789"!==Object.getOwnPropertyNames(t).map((function(e){return t[e]})).join(""))return!1;var n={};return"abcdefghijklmnopqrst".split("").forEach((function(e){n[e]=e})),"abcdefghijklmnopqrst"===Object.keys(Object.assign({},n)).join("")}catch(e){return!1}}()?Object.assign:function(e,i){for(var a,u,l=o(e),c=1;c{"use strict";var n=r(414);function o(){}function i(){}i.resetWarningCache=o,e.exports=function(){function e(e,t,r,o,i,a){if(a!==n){var u=new Error("Calling PropTypes validators directly is not supported by the `prop-types` package. Use PropTypes.checkPropTypes() to call them. Read more at http://fb.me/use-check-prop-types");throw u.name="Invariant Violation",u}}function t(){return e}e.isRequired=e;var r={array:e,bool:e,func:e,number:e,object:e,string:e,symbol:e,any:e,arrayOf:t,element:e,elementType:e,instanceOf:t,node:e,objectOf:t,oneOf:t,oneOfType:t,shape:t,exact:t,checkPropTypes:i,resetWarningCache:o};return r.PropTypes=r,r}},697:(e,t,r)=>{e.exports=r(703)()},414:e=>{"use strict";e.exports="SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED"},408:(e,t,r)=>{"use strict";var n=r(418),o=60103,i=60106;t.Fragment=60107,t.StrictMode=60108,t.Profiler=60114;var a=60109,u=60110,l=60112;t.Suspense=60113;var c=60115,s=60116;if("function"==typeof Symbol&&Symbol.for){var f=Symbol.for;o=f("react.element"),i=f("react.portal"),t.Fragment=f("react.fragment"),t.StrictMode=f("react.strict_mode"),t.Profiler=f("react.profiler"),a=f("react.provider"),u=f("react.context"),l=f("react.forward_ref"),t.Suspense=f("react.suspense"),c=f("react.memo"),s=f("react.lazy")}var p="function"==typeof Symbol&&Symbol.iterator;function y(e){for(var t="https://reactjs.org/docs/error-decoder.html?invariant="+e,r=1;r{"use strict";e.exports=r(408)}},t={};function r(n){var o=t[n];if(void 0!==o)return o.exports;var i=t[n]={exports:{}};return e[n](i,i.exports,r),i.exports}r.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return r.d(t,{a:t}),t},r.d=(e,t)=>{for(var n in t)r.o(t,n)&&!r.o(e,n)&&Object.defineProperty(e,n,{enumerable:!0,get:t[n]})},r.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),r.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})};var n={};return(()=>{"use strict";r.r(n),r.d(n,{default:()=>d});var e=r(294),t=r(697),o=r.n(t),i=function(t){var r=t.children,n=t.width;return e.createElement("div",{style:{flex:"0 0 ".concat(n,"px")}}," ",r," ")};i.defaultProps={children:" "},i.propTypes={children:o().oneOfType([o().string,o().element]),width:o().number.isRequired};const a=i;function u(t){var r=t.labels,n=t.width,o=t.labelsVisibility,i=t.squares,u=t.height,l=t.yWidth;return e.createElement("div",{style:{display:"flex"}},e.createElement(a,{width:l}),r.map((function(t,r){return e.createElement("div",{key:r,style:{flex:i?"none":1,textAlign:"center",width:i?"".concat(u+1,"px"):n,visibility:o&&!o[r]?"hidden":"visible"}},t)})))}u.propTypes={labels:o().arrayOf(o().oneOfType([o().string,o().number,o().object])).isRequired,labelsVisibility:o().arrayOf(o().bool),width:o().number.isRequired,squares:o().bool,height:o().number},u.defaultProps={labelsVisibility:null,squares:!1,height:30};const l=u;function c(e){return function(e){if(Array.isArray(e))return s(e)}(e)||function(e){if("undefined"!=typeof Symbol&&null!=e[Symbol.iterator]||null!=e["@@iterator"])return Array.from(e)}(e)||function(e,t){if(e){if("string"==typeof e)return s(e,t);var r=Object.prototype.toString.call(e).slice(8,-1);return"Object"===r&&e.constructor&&(r=e.constructor.name),"Map"===r||"Set"===r?Array.from(e):"Arguments"===r||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(r)?s(e,t):void 0}}(e)||function(){throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function s(e,t){(null==t||t>e.length)&&(t=e.length);for(var r=0,n=new Array(t);r {\n\tvar getter = module && module.__esModule ?\n\t\t() => (module['default']) :\n\t\t() => (module);\n\t__webpack_require__.d(getter, { a: getter });\n\treturn getter;\n};","// define getter functions for harmony exports\n__webpack_require__.d = (exports, definition) => {\n\tfor(var key in definition) {\n\t\tif(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {\n\t\t\tObject.defineProperty(exports, key, { enumerable: true, get: definition[key] });\n\t\t}\n\t}\n};","__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))","// define __esModule on exports\n__webpack_require__.r = (exports) => {\n\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n\t}\n\tObject.defineProperty(exports, '__esModule', { value: true });\n};","import React from \"react\";\nimport PropTypes from \"prop-types\";\n\nconst FixedBox = ({ children, width }) => {\n return
{children}
;\n};\n\nFixedBox.defaultProps = {\n children: \" \",\n};\n\nFixedBox.propTypes = {\n children: PropTypes.oneOfType([PropTypes.string, PropTypes.element]),\n width: PropTypes.number.isRequired,\n};\n\nexport default FixedBox;\n\n","import React from \"react\";\nimport PropTypes from \"prop-types\";\nimport FixedBox from \"./FixedBox\";\n\nfunction XLabels({ labels, width, labelsVisibility, squares, height, yWidth }) {\n return (\n
\n \n {labels.map((x, i) => (\n \n {x}\n
\n ))}\n \n );\n}\n\nXLabels.propTypes = {\n labels: PropTypes.arrayOf(\n PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.object])\n ).isRequired,\n labelsVisibility: PropTypes.arrayOf(PropTypes.bool),\n width: PropTypes.number.isRequired,\n squares: PropTypes.bool,\n height: PropTypes.number,\n};\n\nXLabels.defaultProps = {\n labelsVisibility: null,\n squares: false,\n height: 30,\n};\n\nexport default XLabels;\n\n","import React from \"react\";\nimport PropTypes from \"prop-types\";\nimport FixedBox from \"./FixedBox\";\n\nconst DataGrid = ({\n xLabels,\n yLabels,\n data,\n xLabelWidth,\n yLabelWidth,\n background,\n height,\n yLabelTextAlign,\n unit,\n displayYLabels,\n onClick,\n cursor,\n squares,\n cellRender,\n cellStyle,\n title\n}) => {\n const flatArray = data.reduce((i, o) => [...o, ...i], []);\n const max = Math.max(...flatArray);\n const min = Math.min(...flatArray);\n\n return (\n
\n {yLabels.map((y, yi) => (\n
\n \n \n {displayYLabels && y}\n
\n \n {xLabels.map((x, xi) => {\n const value = data[yi][xi];\n const style = Object.assign(\n {\n cursor: `${cursor}`,\n margin: \"1px 1px 0 0\",\n height,\n width: squares ? `${height}px` : undefined,\n flex: squares ? \"none\" : 1,\n textAlign: \"center\",\n },\n cellStyle(background, value, min, max, data, xi, yi)\n );\n return (\n \n
\n {cellRender(value, x, y)}\n
\n
\n );\n })}\n \n ))}\n \n );\n};\n\nDataGrid.propTypes = {\n xLabels: PropTypes.arrayOf(\n PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.object])\n ).isRequired,\n yLabels: PropTypes.arrayOf(\n PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.object])\n ).isRequired,\n data: PropTypes.arrayOf(PropTypes.array).isRequired,\n background: PropTypes.string.isRequired,\n height: PropTypes.number.isRequired,\n xLabelWidth: PropTypes.number.isRequired,\n yLabelWidth: PropTypes.number.isRequired,\n yLabelTextAlign: PropTypes.string.isRequired,\n unit: PropTypes.string.isRequired,\n displayYLabels: PropTypes.bool,\n onClick: PropTypes.func,\n cursor: PropTypes.string,\n squares: PropTypes.bool,\n cellRender: PropTypes.func.isRequired,\n cellStyle: PropTypes.func.isRequired,\n title: PropTypes.func\n};\n\nDataGrid.defaultProps = {\n displayYLabels: true,\n cursor: \"\",\n onClick: () => {},\n squares: false,\n title: (value, unit) => (value || value === 0) && `${value} ${unit}`\n};\n\nexport default DataGrid;\n","import React from \"react\";\nimport PropTypes from \"prop-types\";\nimport XLabels from \"./XLabels\";\nimport DataGrid from \"./DataGrid\";\n\nfunction HeatMap({\n xLabels,\n yLabels,\n data,\n background,\n height,\n xLabelWidth,\n yLabelWidth,\n xLabelsLocation,\n yLabelTextAlign,\n xLabelsVisibility,\n unit,\n displayYLabels,\n onClick,\n squares,\n cellRender,\n cellStyle,\n title\n}) {\n let cursor = \"\";\n if (onClick !== undefined) {\n cursor = \"pointer\";\n }\n const xLabelsEle = (\n \n );\n return (\n
\n {xLabelsLocation === \"top\" && xLabelsEle}\n \n {xLabelsLocation === \"bottom\" && xLabelsEle}\n
\n );\n}\n\nHeatMap.propTypes = {\n xLabels: PropTypes.arrayOf(\n PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.object])\n ).isRequired,\n yLabels: PropTypes.arrayOf(\n PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.object])\n ).isRequired,\n data: PropTypes.arrayOf(PropTypes.array).isRequired,\n background: PropTypes.string,\n height: PropTypes.number,\n xLabelWidth: PropTypes.number,\n yLabelWidth: PropTypes.number,\n xLabelsLocation: PropTypes.oneOf([\"top\", \"bottom\"]),\n xLabelsVisibility: PropTypes.arrayOf(PropTypes.bool),\n yLabelTextAlign: PropTypes.string,\n displayYLabels: PropTypes.bool,\n unit: PropTypes.string,\n onClick: PropTypes.func,\n squares: PropTypes.bool,\n cellRender: PropTypes.func,\n cellStyle: PropTypes.func,\n title: PropTypes.func\n};\n\nHeatMap.defaultProps = {\n background: \"#329fff\",\n height: 30,\n xLabelWidth: 60,\n yLabelWidth: 40,\n yLabelTextAlign: \"right\",\n unit: \"\",\n xLabelsLocation: \"top\",\n xLabelsVisibility: null,\n displayYLabels: true,\n onClick: undefined,\n squares: false,\n cellRender: () => null,\n cellStyle: (background, value, min, max) => ({\n background,\n opacity: (value - min) / (max - min) || 0,\n })\n};\n\nexport default HeatMap;\n","import HeatMap from './HeatMap';\n\nexport default HeatMap;\n"],"names":["root","factory","exports","module","define","amd","self","getOwnPropertySymbols","Object","hasOwnProperty","prototype","propIsEnumerable","propertyIsEnumerable","toObject","val","TypeError","assign","test1","String","getOwnPropertyNames","test2","i","fromCharCode","map","n","join","test3","split","forEach","letter","keys","err","shouldUseNative","target","source","from","symbols","to","s","arguments","length","key","call","ReactPropTypesSecret","emptyFunction","emptyFunctionWithReset","resetWarningCache","shim","props","propName","componentName","location","propFullName","secret","Error","name","getShim","isRequired","ReactPropTypes","array","bool","func","number","object","string","symbol","any","arrayOf","element","elementType","instanceOf","node","objectOf","oneOf","oneOfType","shape","exact","checkPropTypes","PropTypes","l","p","Fragment","StrictMode","Profiler","q","r","t","Suspense","u","v","Symbol","for","w","x","iterator","z","a","b","c","encodeURIComponent","A","isMounted","enqueueForceUpdate","enqueueReplaceState","enqueueSetState","B","C","this","context","refs","updater","D","E","isReactComponent","setState","forceUpdate","F","constructor","isPureReactComponent","G","current","H","I","ref","__self","__source","J","e","d","k","h","g","children","f","Array","m","defaultProps","$$typeof","type","_owner","L","M","N","replace","escape","toString","O","isArray","K","push","y","next","done","value","P","Q","_status","_result","then","default","R","S","T","ReactCurrentDispatcher","ReactCurrentBatchConfig","transition","ReactCurrentOwner","IsSomeRendererActing","Children","apply","count","toArray","only","Component","PureComponent","__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED","cloneElement","createContext","_calculateChangedBits","_currentValue","_currentValue2","_threadCount","Provider","Consumer","_context","createElement","createFactory","bind","createRef","forwardRef","render","isValidElement","lazy","_payload","_init","memo","compare","useCallback","useContext","useDebugValue","useEffect","useImperativeHandle","useLayoutEffect","useMemo","useReducer","useRef","useState","version","__webpack_module_cache__","__webpack_require__","moduleId","cachedModule","undefined","__webpack_modules__","getter","__esModule","definition","o","defineProperty","enumerable","get","obj","prop","toStringTag","FixedBox","width","style","flex","propTypes","XLabels","labels","labelsVisibility","squares","height","yWidth","display","textAlign","visibility","DataGrid","xLabels","yLabels","data","yLabelWidth","xLabelWidth","background","yLabelTextAlign","unit","displayYLabels","onClick","cursor","cellRender","cellStyle","title","flatArray","reduce","max","Math","min","yi","position","paddingRight","paddingTop","xi","margin","HeatMap","xLabelsLocation","xLabelsVisibility","xLabelsEle","opacity"],"sourceRoot":""} -------------------------------------------------------------------------------- /docs/heatmap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arunghosh/react-heatmap-grid/6354b23bf1dc4d9e33470be1bc30a1065132e319/docs/heatmap.png -------------------------------------------------------------------------------- /docs/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arunghosh/react-heatmap-grid/6354b23bf1dc4d9e33470be1bc30a1065132e319/docs/screenshot.png -------------------------------------------------------------------------------- /example/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import HeatMap from "../src"; 4 | 5 | const xLabels = new Array(24).fill(0).map((_, i) => `${i}`); 6 | 7 | // Display only even labels 8 | const xLabelsVisibility = new Array(24) 9 | .fill(0) 10 | .map((_, i) => (i % 2 === 0 ? true : false)); 11 | 12 | const yLabels = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri"]; 13 | const data = new Array(yLabels.length) 14 | .fill(0) 15 | .map(() => 16 | new Array(xLabels.length).fill(0).map(() => Math.floor(Math.random() * 100)) 17 | ); 18 | 19 | ReactDOM.render( 20 | alert(`Clicked ${x}, ${y}`)} 29 | cellStyle={(background, value, min, max, data, x, y) => ({ 30 | background: `rgb(12, 160, 244, ${1 - (max - value) / (max - min)})`, 31 | fontSize: "11px", 32 | fontFamily: "Arial", 33 | })} 34 | cellRender={value => value && `${value}%`} 35 | title={(value, unit, index) => value && `${value}-${xLabels[index]}`} 36 | />, 37 | document.getElementById("app") 38 | ); 39 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | collectCoverageFrom: [ 3 | '**/*.{js,jsx,ts,tsx}', 4 | '!**/*.d.ts', 5 | '!**/node_modules/**', 6 | ], 7 | testEnvironment: 'jest-environment-jsdom', 8 | testPathIgnorePatterns: ['/node_modules/'], 9 | transform: { 10 | // Use babel-jest to transpile tests with the next/babel preset 11 | // https://jestjs.io/docs/configuration#transform-objectstring-pathtotransformer--pathtotransformer-object 12 | '^.+\\.(js|jsx|ts|tsx)$': ['babel-jest'], 13 | }, 14 | transformIgnorePatterns: [ 15 | '/node_modules/', 16 | '^.+\\.module\\.(css|sass|scss)$', 17 | ], 18 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-heatmap-grid", 3 | "version": "0.9.0", 4 | "description": "React component for heatmap on grid layout", 5 | "main": "dist/bundle.js", 6 | "scripts": { 7 | "test": "jest", 8 | "dev": "NODE_ENV=dev webpack-dev-server --mode=development", 9 | "build": "rimraf dist && eslint src && NODE_ENV=prod webpack --mode=production" 10 | }, 11 | "author": "Arun Ghosh", 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/arunghosh/react-heatmap-grid" 15 | }, 16 | "keywords": [ 17 | "heatmap", 18 | "react", 19 | "component" 20 | ], 21 | "license": "MIT", 22 | "devDependencies": { 23 | "@babel/core": "^7.0.0", 24 | "@babel/preset-env": "^7.16.4", 25 | "@babel/preset-react": "^7.16.0", 26 | "@wojtekmaj/enzyme-adapter-react-17": "^0.6.5", 27 | "babel-eslint": "^8.0.0", 28 | "babel-jest": "^27.3.1", 29 | "babel-loader": "^8.0.0", 30 | "babel-plugin-transform-object-rest-spread": "^6.26.0", 31 | "css-loader": "^6.5.1", 32 | "enzyme": "^3.2.0", 33 | "eslint": "^3.17.1", 34 | "eslint-config-airbnb": "^14.1.0", 35 | "eslint-config-prettier": "^4.1.0", 36 | "eslint-plugin-import": "^2.2.0", 37 | "eslint-plugin-jsx-a11y": "^4.0.0", 38 | "eslint-plugin-react": "^6.10.0", 39 | "jest": "^27.3.1", 40 | "react": "^17.0.2", 41 | "react-dom": "^17.0.2", 42 | "react-test-renderer": "^17.0.0", 43 | "rimraf": "^2.6.1", 44 | "style-loader": "^0.23.1", 45 | "webpack": "^5.0.0", 46 | "webpack-cli": "^4.9.1", 47 | "webpack-dev-server": "^4.5.0" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | React HeatMap Grid 4 | 5 | 6 |
7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/DataGrid.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import FixedBox from "./FixedBox"; 4 | 5 | const DataGrid = ({ 6 | xLabels, 7 | yLabels, 8 | data, 9 | xLabelWidth, 10 | yLabelWidth, 11 | background, 12 | height, 13 | yLabelTextAlign, 14 | unit, 15 | displayYLabels, 16 | onClick, 17 | cursor, 18 | squares, 19 | cellRender, 20 | cellStyle, 21 | title 22 | }) => { 23 | const flatArray = data.reduce((i, o) => [...o, ...i], []); 24 | const max = Math.max(...flatArray); 25 | const min = Math.min(...flatArray); 26 | 27 | return ( 28 |
29 | {yLabels.map((y, yi) => ( 30 |
31 | 32 |
41 | {displayYLabels && y} 42 |
43 |
44 | {xLabels.map((x, xi) => { 45 | const value = data[yi][xi]; 46 | const style = Object.assign( 47 | { 48 | cursor: `${cursor}`, 49 | margin: "1px 1px 0 0", 50 | height, 51 | width: squares ? `${height}px` : undefined, 52 | flex: squares ? "none" : 1, 53 | textAlign: "center", 54 | }, 55 | cellStyle(background, value, min, max, data, xi, yi) 56 | ); 57 | return ( 58 |
65 |
66 | {cellRender(value, x, y)} 67 |
68 |
69 | ); 70 | })} 71 |
72 | ))} 73 |
74 | ); 75 | }; 76 | 77 | DataGrid.propTypes = { 78 | xLabels: PropTypes.arrayOf( 79 | PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.object]) 80 | ).isRequired, 81 | yLabels: PropTypes.arrayOf( 82 | PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.object]) 83 | ).isRequired, 84 | data: PropTypes.arrayOf(PropTypes.array).isRequired, 85 | background: PropTypes.string.isRequired, 86 | height: PropTypes.number.isRequired, 87 | xLabelWidth: PropTypes.number.isRequired, 88 | yLabelWidth: PropTypes.number.isRequired, 89 | yLabelTextAlign: PropTypes.string.isRequired, 90 | unit: PropTypes.string.isRequired, 91 | displayYLabels: PropTypes.bool, 92 | onClick: PropTypes.func, 93 | cursor: PropTypes.string, 94 | squares: PropTypes.bool, 95 | cellRender: PropTypes.func.isRequired, 96 | cellStyle: PropTypes.func.isRequired, 97 | title: PropTypes.func 98 | }; 99 | 100 | DataGrid.defaultProps = { 101 | displayYLabels: true, 102 | cursor: "", 103 | onClick: () => {}, 104 | squares: false, 105 | title: (value, unit) => (value || value === 0) && `${value} ${unit}` 106 | }; 107 | 108 | export default DataGrid; 109 | -------------------------------------------------------------------------------- /src/FixedBox.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | const FixedBox = ({ children, width }) => { 5 | return
{children}
; 6 | }; 7 | 8 | FixedBox.defaultProps = { 9 | children: " ", 10 | }; 11 | 12 | FixedBox.propTypes = { 13 | children: PropTypes.oneOfType([PropTypes.string, PropTypes.element]), 14 | width: PropTypes.number.isRequired, 15 | }; 16 | 17 | export default FixedBox; 18 | -------------------------------------------------------------------------------- /src/HeatMap.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import XLabels from "./XLabels"; 4 | import DataGrid from "./DataGrid"; 5 | 6 | function HeatMap({ 7 | xLabels, 8 | yLabels, 9 | data, 10 | background, 11 | height, 12 | xLabelWidth, 13 | yLabelWidth, 14 | xLabelsLocation, 15 | yLabelTextAlign, 16 | xLabelsVisibility, 17 | unit, 18 | displayYLabels, 19 | onClick, 20 | squares, 21 | cellRender, 22 | cellStyle, 23 | title 24 | }) { 25 | let cursor = ""; 26 | if (onClick !== undefined) { 27 | cursor = "pointer"; 28 | } 29 | const xLabelsEle = ( 30 | 38 | ); 39 | return ( 40 |
41 | {xLabelsLocation === "top" && xLabelsEle} 42 | 63 | {xLabelsLocation === "bottom" && xLabelsEle} 64 |
65 | ); 66 | } 67 | 68 | HeatMap.propTypes = { 69 | xLabels: PropTypes.arrayOf( 70 | PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.object]) 71 | ).isRequired, 72 | yLabels: PropTypes.arrayOf( 73 | PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.object]) 74 | ).isRequired, 75 | data: PropTypes.arrayOf(PropTypes.array).isRequired, 76 | background: PropTypes.string, 77 | height: PropTypes.number, 78 | xLabelWidth: PropTypes.number, 79 | yLabelWidth: PropTypes.number, 80 | xLabelsLocation: PropTypes.oneOf(["top", "bottom"]), 81 | xLabelsVisibility: PropTypes.arrayOf(PropTypes.bool), 82 | yLabelTextAlign: PropTypes.string, 83 | displayYLabels: PropTypes.bool, 84 | unit: PropTypes.string, 85 | onClick: PropTypes.func, 86 | squares: PropTypes.bool, 87 | cellRender: PropTypes.func, 88 | cellStyle: PropTypes.func, 89 | title: PropTypes.func 90 | }; 91 | 92 | HeatMap.defaultProps = { 93 | background: "#329fff", 94 | height: 30, 95 | xLabelWidth: 60, 96 | yLabelWidth: 40, 97 | yLabelTextAlign: "right", 98 | unit: "", 99 | xLabelsLocation: "top", 100 | xLabelsVisibility: null, 101 | displayYLabels: true, 102 | onClick: undefined, 103 | squares: false, 104 | cellRender: () => null, 105 | cellStyle: (background, value, min, max) => ({ 106 | background, 107 | opacity: (value - min) / (max - min) || 0, 108 | }) 109 | }; 110 | 111 | export default HeatMap; 112 | -------------------------------------------------------------------------------- /src/XLabels.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import FixedBox from "./FixedBox"; 4 | 5 | function XLabels({ labels, width, labelsVisibility, squares, height, yWidth }) { 6 | return ( 7 |
8 | 9 | {labels.map((x, i) => ( 10 |
20 | {x} 21 |
22 | ))} 23 |
24 | ); 25 | } 26 | 27 | XLabels.propTypes = { 28 | labels: PropTypes.arrayOf( 29 | PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.object]) 30 | ).isRequired, 31 | labelsVisibility: PropTypes.arrayOf(PropTypes.bool), 32 | width: PropTypes.number.isRequired, 33 | squares: PropTypes.bool, 34 | height: PropTypes.number, 35 | }; 36 | 37 | XLabels.defaultProps = { 38 | labelsVisibility: null, 39 | squares: false, 40 | height: 30, 41 | }; 42 | 43 | export default XLabels; 44 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import HeatMap from './HeatMap'; 2 | 3 | export default HeatMap; 4 | -------------------------------------------------------------------------------- /test/DataGrid.spec.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "raf/polyfill"; 3 | import { shallow, configure } from "enzyme"; 4 | import Adapter from "@wojtekmaj/enzyme-adapter-react-17"; 5 | import DataGrid from "../src/DataGrid"; 6 | 7 | configure({ adapter: new Adapter() }); 8 | 9 | const defaultProps = { 10 | xLabels: ["x"], 11 | yLabels: ["y"], 12 | data: [[0]], 13 | height: 1, 14 | background: "#000000", 15 | xLabelWidth: 1, 16 | yLabelWidth: 1, 17 | xLabelTextAlign: "top", 18 | yLabelTextAlign: "left", 19 | unit: "$", 20 | cellRender: () => null, 21 | cellStyle: () => {}, 22 | width: 1, 23 | }; 24 | 25 | test("DataGrid renders a title from the provided title function", () => { 26 | const dataGrid = shallow( 27 | `CUSTOM_TITLE`} /> 28 | ); 29 | 30 | expect(dataGrid.find({ title: "CUSTOM_TITLE" })).toHaveLength(1); 31 | }); 32 | 33 | test("DataGrid renders a default title when no title function is provided", () => { 34 | const dataGrid = shallow(); 35 | 36 | expect(dataGrid.find({ title: "0 $" })).toHaveLength(1); 37 | }); 38 | -------------------------------------------------------------------------------- /test/HeatMap.spec.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "raf/polyfill"; 3 | import { shallow, configure } from "enzyme"; 4 | import Adapter from "@wojtekmaj/enzyme-adapter-react-17"; 5 | import HeatMap from "../src/HeatMap"; 6 | import DataGrid from "../src/DataGrid"; 7 | import XLabels from "../src/XLabels"; 8 | 9 | configure({ adapter: new Adapter() }); 10 | 11 | const xLabels = new Array(24).fill(0).map((_, i) => `${i}`); 12 | const yLabels = ["Sun", "Mon", "Tue"]; 13 | const data = new Array(yLabels.length) 14 | .fill(0) 15 | .map(() => 16 | new Array(xLabels.length).fill(0).map(() => Math.floor(Math.random() * 100)) 17 | ); 18 | 19 | test("Component renders without error", () => { 20 | const heatMap = shallow( 21 | 22 | ); 23 | expect(heatMap.find(DataGrid)).toHaveLength(1); 24 | expect(heatMap.find(XLabels)).toHaveLength(1); 25 | }); 26 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | let entry = './src/index.js'; 4 | let output = { 5 | path: path.join(__dirname, 'dist'), 6 | publicPath: '/dist/', 7 | }; 8 | 9 | if (process.env.NODE_ENV === 'dev') { 10 | entry = './example/index.js'; 11 | output = { 12 | path: path.join(__dirname, 'example'), 13 | publicPath: '/example/', 14 | }; 15 | } 16 | 17 | module.exports = { 18 | entry, 19 | output: Object.assign(output, { 20 | filename: 'bundle.js', 21 | library: 'react-sequence', 22 | libraryTarget: 'umd', // universal module definition 23 | }), 24 | devtool: 'source-map', 25 | resolve: { 26 | extensions: ['.js', '.jsx'], 27 | }, 28 | module: { 29 | rules: [ 30 | { 31 | test: /\.jsx?$/, 32 | exclude: /node_modules/, 33 | use: { 34 | loader: 'babel-loader', 35 | options: { 36 | presets: ['@babel/preset-env'] 37 | } 38 | } 39 | }, { 40 | test: /\.scss$/, 41 | use: [ 42 | { 43 | loader: 'style-loader', // creates style nodes from JS strings 44 | }, 45 | { 46 | loader: 'css-loader', // translates CSS into CommonJS 47 | }, 48 | ], 49 | }, 50 | ], 51 | }, 52 | devServer: { 53 | historyApiFallback: true, 54 | } 55 | }; 56 | --------------------------------------------------------------------------------