├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .gitignore ├── README.md ├── bower.json ├── dist ├── react-heatmap.js └── react-heatmap.min.js ├── example └── src │ ├── .gitignore │ ├── example.js │ ├── example.less │ └── index.html ├── gulpfile.js ├── lib └── ReactHeatmap.js ├── package.json └── src └── ReactHeatmap.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # This file is for unifying the coding style for different editors and IDEs 2 | # editorconfig.org 3 | root = true 4 | 5 | [*] 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = false 9 | insert_final_newline = true 10 | indent_style = tab 11 | 12 | [*.json] 13 | indent_style = space 14 | indent_size = 2 15 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | .publish/* 2 | dist/* 3 | example/dist/* 4 | lib/* 5 | node_modules/* 6 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "env": { 4 | "browser": true, 5 | "node": true 6 | }, 7 | "plugins": [ 8 | "react" 9 | ], 10 | "rules": { 11 | "curly": [2, "multi-line"], 12 | "quotes": [2, "single", "avoid-escape"], 13 | "react/display-name": 0, 14 | "react/jsx-boolean-value": 1, 15 | "react/jsx-quotes": 1, 16 | "react/jsx-no-undef": 1, 17 | "react/jsx-sort-props": 0, 18 | "react/jsx-sort-prop-types": 1, 19 | "react/jsx-uses-react": 1, 20 | "react/jsx-uses-vars": 1, 21 | "react/no-did-mount-set-state": 1, 22 | "react/no-did-update-set-state": 1, 23 | "react/no-multi-comp": 1, 24 | "react/no-unknown-property": 1, 25 | "react/prop-types": 1, 26 | "react/react-in-jsx-scope": 1, 27 | "react/self-closing-comp": 1, 28 | "react/wrap-multilines": 1, 29 | "semi": 2, 30 | "strict": 0 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Coverage tools 11 | lib-cov 12 | coverage 13 | coverage.html 14 | .cover* 15 | 16 | # Dependency directory 17 | node_modules 18 | 19 | # Example build directory 20 | example/dist 21 | .publish 22 | 23 | # Editor and other tmp files 24 | *.swp 25 | *.un~ 26 | *.iml 27 | *.ipr 28 | *.iws 29 | *.sublime-* 30 | .idea/ 31 | *.DS_Store 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Heatmap 2 | 3 | A very simple port of `heatmap.js` for `React`. The idea behind this component is to be able to display a heatmap over any type of content (image, div, components ...). By default, the heatmap will always take all available width and height of its container. 4 | 5 | 6 | ## Demo & Examples 7 | 8 | Live demo: [JonathanWi.github.io/react-heatmap](http://JonathanWi.github.io/react-heatmap/) 9 | 10 | To build the examples locally, run: 11 | 12 | ```bash 13 | npm install 14 | npm start 15 | ``` 16 | 17 | Then open [`localhost:8000`](http://localhost:8000) in a browser. 18 | 19 | 20 | ## Installation 21 | 22 | The easiest way to use react-heatmap is to install it from NPM and include it in your own React build process (using [Browserify](http://browserify.org), [Webpack](http://webpack.github.io/), etc). 23 | 24 | You can also use the standalone build by including `dist/react-heatmap.js` in your page. If you use this, make sure you have already included React, and it is available as a global variable. 25 | 26 | ```bash 27 | npm install react-heatmap --save 28 | ``` 29 | 30 | 31 | ## Usage 32 | 33 | This component is pretty straightforward and only expecting 2 simple parameters (`max` and `data`; if you're unfamiliar with these, take a look at the [`heatmap.js documentation`](http://www.patrick-wied.at/static/heatmapjs)); 34 | 35 | ```js 36 | const ReactHeatmap = require('react-heatmap'); 37 | 38 | const data = [{ x: 10, y: 15, value: 5}, { x: 50, y: 50, value: 2}, ...]; 39 | 40 | 41 | ``` 42 | 43 | ## Properties 44 | 45 | 46 | General component description. 47 | 48 | Props 49 | ----- 50 | Prop | Type | Default | Required | Description 51 | --------------------- | -------- | ------------------------- | -------- | ----------- 52 | max|int|5|No|Maximum value for intensity 53 | data|array|[]|No|Heatmap array of dots 54 | unit|string|percent|No|Can be either `percent` or `pixels`. If percent, a `x` value like `26` is considered **26% of the container from the top left** 55 | 56 | 57 | ## Development (`src`, `lib` and the build process) 58 | 59 | **NOTE:** The source code for the component is in `src`. A transpiled CommonJS version (generated with Babel) is available in `lib` for use with node.js, browserify and webpack. A UMD bundle is also built to `dist`, which can be included without the need for any build system. 60 | 61 | To build, watch and serve the examples (which will also watch the component source), run `npm start`. If you just want to watch changes to `src` and rebuild `lib`, run `npm run watch` (this is useful if you are working with `npm link`). 62 | 63 | ## License 64 | 65 | [MIT License](https://en.wikipedia.org/wiki/MIT_License) Copyright (c) 2016 Jonathan Widawski. 66 | 67 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-heatmap", 3 | "version": "0.0.0", 4 | "description": "React Heatmap", 5 | "main": "dist/react-heatmap.min.js", 6 | "homepage": "https://github.com/JonathanWi/react-heatmap", 7 | "authors": [ 8 | "Jonathan Widawski" 9 | ], 10 | "moduleType": [ 11 | "amd", 12 | "globals", 13 | "node" 14 | ], 15 | "keywords": [ 16 | "react", 17 | "react-component" 18 | ], 19 | "license": "MIT", 20 | "ignore": [ 21 | ".editorconfig", 22 | ".gitignore", 23 | "package.json", 24 | "src", 25 | "node_modules", 26 | "example", 27 | "test" 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /dist/react-heatmap.js: -------------------------------------------------------------------------------- 1 | (function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.ReactHeatmap = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { 117 | args[_key - 1] = arguments[_key]; 118 | } 119 | 120 | var argIndex = 0; 121 | var message = 'Warning: ' + format.replace(/%s/g, function () { 122 | return args[argIndex++]; 123 | }); 124 | if (typeof console !== 'undefined') { 125 | console.error(message); 126 | } 127 | try { 128 | // --- Welcome to debugging React --- 129 | // This error was thrown as a convenience so that you can use this stack 130 | // to find the callsite that caused this warning to fire. 131 | throw new Error(message); 132 | } catch (x) {} 133 | }; 134 | 135 | warning = function warning(condition, format) { 136 | if (format === undefined) { 137 | throw new Error('`warning(condition, format, ...args)` requires a warning ' + 'message argument'); 138 | } 139 | 140 | if (format.indexOf('Failed Composite propType: ') === 0) { 141 | return; // Ignore CompositeComponent proptype check. 142 | } 143 | 144 | if (!condition) { 145 | for (var _len2 = arguments.length, args = Array(_len2 > 2 ? _len2 - 2 : 0), _key2 = 2; _key2 < _len2; _key2++) { 146 | args[_key2 - 2] = arguments[_key2]; 147 | } 148 | 149 | printWarning.apply(undefined, [format].concat(args)); 150 | } 151 | }; 152 | } 153 | 154 | module.exports = warning; 155 | },{"./emptyFunction":1}],4:[function(require,module,exports){ 156 | /* 157 | * heatmapjs v2.0.1 | JavaScript Heatmap Library 158 | * 159 | * Copyright 2008-2014 Patrick Wied - All rights reserved. 160 | * Dual licensed under MIT and Beerware license 161 | * 162 | * :: 2016-02-03 23:43 163 | */ 164 | ;(function (name, context, factory) { 165 | 166 | // Supports UMD. AMD, CommonJS/Node.js and browser context 167 | if (typeof module !== "undefined" && module.exports) { 168 | module.exports = factory(); 169 | } else if (typeof define === "function" && define.amd) { 170 | define(factory); 171 | } else { 172 | context[name] = factory(); 173 | } 174 | 175 | })("h337", this, function () { 176 | 177 | // Heatmap Config stores default values and will be merged with instance config 178 | var HeatmapConfig = { 179 | defaultRadius: 40, 180 | defaultRenderer: 'canvas2d', 181 | defaultGradient: { 0.25: "rgb(0,0,255)", 0.55: "rgb(0,255,0)", 0.85: "yellow", 1.0: "rgb(255,0,0)"}, 182 | defaultMaxOpacity: 1, 183 | defaultMinOpacity: 0, 184 | defaultBlur: .85, 185 | defaultXField: 'x', 186 | defaultYField: 'y', 187 | defaultValueField: 'value', 188 | plugins: {} 189 | }; 190 | var Store = (function StoreClosure() { 191 | 192 | var Store = function Store(config) { 193 | this._coordinator = {}; 194 | this._data = []; 195 | this._radi = []; 196 | this._min = 0; 197 | this._max = 1; 198 | this._xField = config['xField'] || config.defaultXField; 199 | this._yField = config['yField'] || config.defaultYField; 200 | this._valueField = config['valueField'] || config.defaultValueField; 201 | 202 | if (config["radius"]) { 203 | this._cfgRadius = config["radius"]; 204 | } 205 | }; 206 | 207 | var defaultRadius = HeatmapConfig.defaultRadius; 208 | 209 | Store.prototype = { 210 | // when forceRender = false -> called from setData, omits renderall event 211 | _organiseData: function(dataPoint, forceRender) { 212 | var x = dataPoint[this._xField]; 213 | var y = dataPoint[this._yField]; 214 | var radi = this._radi; 215 | var store = this._data; 216 | var max = this._max; 217 | var min = this._min; 218 | var value = dataPoint[this._valueField] || 1; 219 | var radius = dataPoint.radius || this._cfgRadius || defaultRadius; 220 | 221 | if (!store[x]) { 222 | store[x] = []; 223 | radi[x] = []; 224 | } 225 | 226 | if (!store[x][y]) { 227 | store[x][y] = value; 228 | radi[x][y] = radius; 229 | } else { 230 | store[x][y] += value; 231 | } 232 | 233 | if (store[x][y] > max) { 234 | if (!forceRender) { 235 | this._max = store[x][y]; 236 | } else { 237 | this.setDataMax(store[x][y]); 238 | } 239 | return false; 240 | } else{ 241 | return { 242 | x: x, 243 | y: y, 244 | value: value, 245 | radius: radius, 246 | min: min, 247 | max: max 248 | }; 249 | } 250 | }, 251 | _unOrganizeData: function() { 252 | var unorganizedData = []; 253 | var data = this._data; 254 | var radi = this._radi; 255 | 256 | for (var x in data) { 257 | for (var y in data[x]) { 258 | 259 | unorganizedData.push({ 260 | x: x, 261 | y: y, 262 | radius: radi[x][y], 263 | value: data[x][y] 264 | }); 265 | 266 | } 267 | } 268 | return { 269 | min: this._min, 270 | max: this._max, 271 | data: unorganizedData 272 | }; 273 | }, 274 | _onExtremaChange: function() { 275 | this._coordinator.emit('extremachange', { 276 | min: this._min, 277 | max: this._max 278 | }); 279 | }, 280 | addData: function() { 281 | if (arguments[0].length > 0) { 282 | var dataArr = arguments[0]; 283 | var dataLen = dataArr.length; 284 | while (dataLen--) { 285 | this.addData.call(this, dataArr[dataLen]); 286 | } 287 | } else { 288 | // add to store 289 | var organisedEntry = this._organiseData(arguments[0], true); 290 | if (organisedEntry) { 291 | this._coordinator.emit('renderpartial', { 292 | min: this._min, 293 | max: this._max, 294 | data: [organisedEntry] 295 | }); 296 | } 297 | } 298 | return this; 299 | }, 300 | setData: function(data) { 301 | var dataPoints = data.data; 302 | var pointsLen = dataPoints.length; 303 | 304 | 305 | // reset data arrays 306 | this._data = []; 307 | this._radi = []; 308 | 309 | for(var i = 0; i < pointsLen; i++) { 310 | this._organiseData(dataPoints[i], false); 311 | } 312 | this._max = data.max; 313 | this._min = data.min || 0; 314 | 315 | this._onExtremaChange(); 316 | this._coordinator.emit('renderall', this._getInternalData()); 317 | return this; 318 | }, 319 | removeData: function() { 320 | // TODO: implement 321 | }, 322 | setDataMax: function(max) { 323 | this._max = max; 324 | this._onExtremaChange(); 325 | this._coordinator.emit('renderall', this._getInternalData()); 326 | return this; 327 | }, 328 | setDataMin: function(min) { 329 | this._min = min; 330 | this._onExtremaChange(); 331 | this._coordinator.emit('renderall', this._getInternalData()); 332 | return this; 333 | }, 334 | setCoordinator: function(coordinator) { 335 | this._coordinator = coordinator; 336 | }, 337 | _getInternalData: function() { 338 | return { 339 | max: this._max, 340 | min: this._min, 341 | data: this._data, 342 | radi: this._radi 343 | }; 344 | }, 345 | getData: function() { 346 | return this._unOrganizeData(); 347 | }/*, 348 | 349 | TODO: rethink. 350 | 351 | getValueAt: function(point) { 352 | var value; 353 | var radius = 100; 354 | var x = point.x; 355 | var y = point.y; 356 | var data = this._data; 357 | 358 | if (data[x] && data[x][y]) { 359 | return data[x][y]; 360 | } else { 361 | var values = []; 362 | // radial search for datapoints based on default radius 363 | for(var distance = 1; distance < radius; distance++) { 364 | var neighbors = distance * 2 +1; 365 | var startX = x - distance; 366 | var startY = y - distance; 367 | 368 | for(var i = 0; i < neighbors; i++) { 369 | for (var o = 0; o < neighbors; o++) { 370 | if ((i == 0 || i == neighbors-1) || (o == 0 || o == neighbors-1)) { 371 | if (data[startY+i] && data[startY+i][startX+o]) { 372 | values.push(data[startY+i][startX+o]); 373 | } 374 | } else { 375 | continue; 376 | } 377 | } 378 | } 379 | } 380 | if (values.length > 0) { 381 | return Math.max.apply(Math, values); 382 | } 383 | } 384 | return false; 385 | }*/ 386 | }; 387 | 388 | 389 | return Store; 390 | })(); 391 | 392 | var Canvas2dRenderer = (function Canvas2dRendererClosure() { 393 | 394 | var _getColorPalette = function(config) { 395 | var gradientConfig = config.gradient || config.defaultGradient; 396 | var paletteCanvas = document.createElement('canvas'); 397 | var paletteCtx = paletteCanvas.getContext('2d'); 398 | 399 | paletteCanvas.width = 256; 400 | paletteCanvas.height = 1; 401 | 402 | var gradient = paletteCtx.createLinearGradient(0, 0, 256, 1); 403 | for (var key in gradientConfig) { 404 | gradient.addColorStop(key, gradientConfig[key]); 405 | } 406 | 407 | paletteCtx.fillStyle = gradient; 408 | paletteCtx.fillRect(0, 0, 256, 1); 409 | 410 | return paletteCtx.getImageData(0, 0, 256, 1).data; 411 | }; 412 | 413 | var _getPointTemplate = function(radius, blurFactor) { 414 | var tplCanvas = document.createElement('canvas'); 415 | var tplCtx = tplCanvas.getContext('2d'); 416 | var x = radius; 417 | var y = radius; 418 | tplCanvas.width = tplCanvas.height = radius*2; 419 | 420 | if (blurFactor == 1) { 421 | tplCtx.beginPath(); 422 | tplCtx.arc(x, y, radius, 0, 2 * Math.PI, false); 423 | tplCtx.fillStyle = 'rgba(0,0,0,1)'; 424 | tplCtx.fill(); 425 | } else { 426 | var gradient = tplCtx.createRadialGradient(x, y, radius*blurFactor, x, y, radius); 427 | gradient.addColorStop(0, 'rgba(0,0,0,1)'); 428 | gradient.addColorStop(1, 'rgba(0,0,0,0)'); 429 | tplCtx.fillStyle = gradient; 430 | tplCtx.fillRect(0, 0, 2*radius, 2*radius); 431 | } 432 | 433 | 434 | 435 | return tplCanvas; 436 | }; 437 | 438 | var _prepareData = function(data) { 439 | var renderData = []; 440 | var min = data.min; 441 | var max = data.max; 442 | var radi = data.radi; 443 | var data = data.data; 444 | 445 | var xValues = Object.keys(data); 446 | var xValuesLen = xValues.length; 447 | 448 | while(xValuesLen--) { 449 | var xValue = xValues[xValuesLen]; 450 | var yValues = Object.keys(data[xValue]); 451 | var yValuesLen = yValues.length; 452 | while(yValuesLen--) { 453 | var yValue = yValues[yValuesLen]; 454 | var value = data[xValue][yValue]; 455 | var radius = radi[xValue][yValue]; 456 | renderData.push({ 457 | x: xValue, 458 | y: yValue, 459 | value: value, 460 | radius: radius 461 | }); 462 | } 463 | } 464 | 465 | return { 466 | min: min, 467 | max: max, 468 | data: renderData 469 | }; 470 | }; 471 | 472 | 473 | function Canvas2dRenderer(config) { 474 | var container = config.container; 475 | var shadowCanvas = this.shadowCanvas = document.createElement('canvas'); 476 | var canvas = this.canvas = config.canvas || document.createElement('canvas'); 477 | var renderBoundaries = this._renderBoundaries = [10000, 10000, 0, 0]; 478 | 479 | var computed = getComputedStyle(config.container) || {}; 480 | 481 | canvas.className = 'heatmap-canvas'; 482 | 483 | this._width = canvas.width = shadowCanvas.width = +(computed.width.replace(/px/,'')); 484 | this._height = canvas.height = shadowCanvas.height = +(computed.height.replace(/px/,'')); 485 | 486 | this.shadowCtx = shadowCanvas.getContext('2d'); 487 | this.ctx = canvas.getContext('2d'); 488 | 489 | // @TODO: 490 | // conditional wrapper 491 | 492 | canvas.style.cssText = shadowCanvas.style.cssText = 'position:absolute;left:0;top:0;'; 493 | 494 | container.style.position = 'relative'; 495 | container.appendChild(canvas); 496 | 497 | this._palette = _getColorPalette(config); 498 | this._templates = {}; 499 | 500 | this._setStyles(config); 501 | }; 502 | 503 | Canvas2dRenderer.prototype = { 504 | renderPartial: function(data) { 505 | this._drawAlpha(data); 506 | this._colorize(); 507 | }, 508 | renderAll: function(data) { 509 | // reset render boundaries 510 | this._clear(); 511 | this._drawAlpha(_prepareData(data)); 512 | this._colorize(); 513 | }, 514 | _updateGradient: function(config) { 515 | this._palette = _getColorPalette(config); 516 | }, 517 | updateConfig: function(config) { 518 | if (config['gradient']) { 519 | this._updateGradient(config); 520 | } 521 | this._setStyles(config); 522 | }, 523 | setDimensions: function(width, height) { 524 | this._width = width; 525 | this._height = height; 526 | this.canvas.width = this.shadowCanvas.width = width; 527 | this.canvas.height = this.shadowCanvas.height = height; 528 | }, 529 | _clear: function() { 530 | this.shadowCtx.clearRect(0, 0, this._width, this._height); 531 | this.ctx.clearRect(0, 0, this._width, this._height); 532 | }, 533 | _setStyles: function(config) { 534 | this._blur = (config.blur == 0)?0:(config.blur || config.defaultBlur); 535 | 536 | if (config.backgroundColor) { 537 | this.canvas.style.backgroundColor = config.backgroundColor; 538 | } 539 | 540 | this._opacity = (config.opacity || 0) * 255; 541 | this._maxOpacity = (config.maxOpacity || config.defaultMaxOpacity) * 255; 542 | this._minOpacity = (config.minOpacity || config.defaultMinOpacity) * 255; 543 | this._useGradientOpacity = !!config.useGradientOpacity; 544 | }, 545 | _drawAlpha: function(data) { 546 | var min = this._min = data.min; 547 | var max = this._max = data.max; 548 | var data = data.data || []; 549 | var dataLen = data.length; 550 | // on a point basis? 551 | var blur = 1 - this._blur; 552 | 553 | while(dataLen--) { 554 | 555 | var point = data[dataLen]; 556 | 557 | var x = point.x; 558 | var y = point.y; 559 | var radius = point.radius; 560 | // if value is bigger than max 561 | // use max as value 562 | var value = Math.min(point.value, max); 563 | var rectX = x - radius; 564 | var rectY = y - radius; 565 | var shadowCtx = this.shadowCtx; 566 | 567 | 568 | 569 | 570 | var tpl; 571 | if (!this._templates[radius]) { 572 | this._templates[radius] = tpl = _getPointTemplate(radius, blur); 573 | } else { 574 | tpl = this._templates[radius]; 575 | } 576 | // value from minimum / value range 577 | // => [0, 1] 578 | shadowCtx.globalAlpha = (value-min)/(max-min); 579 | 580 | shadowCtx.drawImage(tpl, rectX, rectY); 581 | 582 | // update renderBoundaries 583 | if (rectX < this._renderBoundaries[0]) { 584 | this._renderBoundaries[0] = rectX; 585 | } 586 | if (rectY < this._renderBoundaries[1]) { 587 | this._renderBoundaries[1] = rectY; 588 | } 589 | if (rectX + 2*radius > this._renderBoundaries[2]) { 590 | this._renderBoundaries[2] = rectX + 2*radius; 591 | } 592 | if (rectY + 2*radius > this._renderBoundaries[3]) { 593 | this._renderBoundaries[3] = rectY + 2*radius; 594 | } 595 | 596 | } 597 | }, 598 | _colorize: function() { 599 | var x = this._renderBoundaries[0]; 600 | var y = this._renderBoundaries[1]; 601 | var width = this._renderBoundaries[2] - x; 602 | var height = this._renderBoundaries[3] - y; 603 | var maxWidth = this._width; 604 | var maxHeight = this._height; 605 | var opacity = this._opacity; 606 | var maxOpacity = this._maxOpacity; 607 | var minOpacity = this._minOpacity; 608 | var useGradientOpacity = this._useGradientOpacity; 609 | 610 | if (x < 0) { 611 | x = 0; 612 | } 613 | if (y < 0) { 614 | y = 0; 615 | } 616 | if (x + width > maxWidth) { 617 | width = maxWidth - x; 618 | } 619 | if (y + height > maxHeight) { 620 | height = maxHeight - y; 621 | } 622 | 623 | var img = this.shadowCtx.getImageData(x, y, width, height); 624 | var imgData = img.data; 625 | var len = imgData.length; 626 | var palette = this._palette; 627 | 628 | 629 | for (var i = 3; i < len; i+= 4) { 630 | var alpha = imgData[i]; 631 | var offset = alpha * 4; 632 | 633 | 634 | if (!offset) { 635 | continue; 636 | } 637 | 638 | var finalAlpha; 639 | if (opacity > 0) { 640 | finalAlpha = opacity; 641 | } else { 642 | if (alpha < maxOpacity) { 643 | if (alpha < minOpacity) { 644 | finalAlpha = minOpacity; 645 | } else { 646 | finalAlpha = alpha; 647 | } 648 | } else { 649 | finalAlpha = maxOpacity; 650 | } 651 | } 652 | 653 | imgData[i-3] = palette[offset]; 654 | imgData[i-2] = palette[offset + 1]; 655 | imgData[i-1] = palette[offset + 2]; 656 | imgData[i] = useGradientOpacity ? palette[offset + 3] : finalAlpha; 657 | 658 | } 659 | 660 | img.data = imgData; 661 | this.ctx.putImageData(img, x, y); 662 | 663 | this._renderBoundaries = [1000, 1000, 0, 0]; 664 | 665 | }, 666 | getValueAt: function(point) { 667 | var value; 668 | var shadowCtx = this.shadowCtx; 669 | var img = shadowCtx.getImageData(point.x, point.y, 1, 1); 670 | var data = img.data[3]; 671 | var max = this._max; 672 | var min = this._min; 673 | 674 | value = (Math.abs(max-min) * (data/255)) >> 0; 675 | 676 | return value; 677 | }, 678 | getDataURL: function() { 679 | return this.canvas.toDataURL(); 680 | } 681 | }; 682 | 683 | 684 | return Canvas2dRenderer; 685 | })(); 686 | 687 | var Renderer = (function RendererClosure() { 688 | 689 | var rendererFn = false; 690 | 691 | if (HeatmapConfig['defaultRenderer'] === 'canvas2d') { 692 | rendererFn = Canvas2dRenderer; 693 | } 694 | 695 | return rendererFn; 696 | })(); 697 | 698 | 699 | var Util = { 700 | merge: function() { 701 | var merged = {}; 702 | var argsLen = arguments.length; 703 | for (var i = 0; i < argsLen; i++) { 704 | var obj = arguments[i] 705 | for (var key in obj) { 706 | merged[key] = obj[key]; 707 | } 708 | } 709 | return merged; 710 | } 711 | }; 712 | // Heatmap Constructor 713 | var Heatmap = (function HeatmapClosure() { 714 | 715 | var Coordinator = (function CoordinatorClosure() { 716 | 717 | function Coordinator() { 718 | this.cStore = {}; 719 | }; 720 | 721 | Coordinator.prototype = { 722 | on: function(evtName, callback, scope) { 723 | var cStore = this.cStore; 724 | 725 | if (!cStore[evtName]) { 726 | cStore[evtName] = []; 727 | } 728 | cStore[evtName].push((function(data) { 729 | return callback.call(scope, data); 730 | })); 731 | }, 732 | emit: function(evtName, data) { 733 | var cStore = this.cStore; 734 | if (cStore[evtName]) { 735 | var len = cStore[evtName].length; 736 | for (var i=0; is?(e?this.setDataMax(o[r][i]):this._max=o[r][i],!1):{x:r,y:i,value:c,radius:l,min:u,max:s}},_unOrganizeData:function(){var t=[],e=this._data,n=this._radi;for(var r in e)for(var i in e[r])t.push({x:r,y:i,radius:n[r][i],value:e[r][i]});return{min:this._min,max:this._max,data:t}},_onExtremaChange:function(){this._coordinator.emit("extremachange",{min:this._min,max:this._max})},addData:function(){if(arguments[0].length>0)for(var t=arguments[0],e=t.length;e--;)this.addData.call(this,t[e]);else{var n=this._organiseData(arguments[0],!0);n&&this._coordinator.emit("renderpartial",{min:this._min,max:this._max,data:[n]})}return this},setData:function(t){var e=t.data,n=e.length;this._data=[],this._radi=[];for(var r=0;rthis._renderBoundaries[2]&&(this._renderBoundaries[2]=d+2*l),h+2*l>this._renderBoundaries[3]&&(this._renderBoundaries[3]=h+2*l)}},_colorize:function(){var t=this._renderBoundaries[0],e=this._renderBoundaries[1],n=this._renderBoundaries[2]-t,r=this._renderBoundaries[3]-e,i=this._width,a=this._height,o=this._opacity,s=this._maxOpacity,u=this._minOpacity,c=this._useGradientOpacity;t<0&&(t=0),e<0&&(e=0),t+n>i&&(n=i-t),e+r>a&&(r=a-e);for(var l=this.shadowCtx.getImageData(t,e,n,r),f=l.data,d=f.length,h=this._palette,p=3;p0?o:y>0},getDataURL:function(){return this.canvas.toDataURL()}},t}(),r=function(){var e=!1;return"canvas2d"===t.defaultRenderer&&(e=n),e}(),i={merge:function(){for(var t={},e=arguments.length,n=0;n 37 | 38 |
39 | 40 |
41 | 42 | ); 43 | } 44 | }); 45 | 46 | ReactDOM.render(, document.getElementById('app')); 47 | -------------------------------------------------------------------------------- /example/src/example.less: -------------------------------------------------------------------------------- 1 | /* 2 | // Examples Stylesheet 3 | // ------------------- 4 | */ 5 | 6 | body { 7 | font-family: Helvetica Neue, Helvetica, Arial, sans-serif; 8 | font-size: 14px; 9 | color: #333; 10 | margin: 0; 11 | padding: 0; 12 | } 13 | 14 | a { 15 | color: #08c; 16 | text-decoration: none; 17 | } 18 | 19 | a:hover { 20 | text-decoration: underline; 21 | } 22 | 23 | .container { 24 | margin-left: auto; 25 | margin-right: auto; 26 | max-width: 720px; 27 | padding: 1em; 28 | } 29 | 30 | .footer { 31 | margin-top: 50px; 32 | border-top: 1px solid #eee; 33 | padding: 20px 0; 34 | font-size: 12px; 35 | color: #999; 36 | } 37 | 38 | h1, h2, h3, h4, h5, h6 { 39 | color: #222; 40 | font-weight: 100; 41 | margin: 0.5em 0; 42 | } 43 | 44 | label { 45 | color: #999; 46 | display: inline-block; 47 | font-size: 0.85em; 48 | font-weight: bold; 49 | margin: 1em 0; 50 | text-transform: uppercase; 51 | } 52 | 53 | .hint { 54 | margin: 15px 0; 55 | font-style: italic; 56 | color: #999; 57 | } 58 | -------------------------------------------------------------------------------- /example/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | React Heatmap 4 | 5 | 6 | 7 |
8 |

React Heatmap

9 |

View project on GitHub

10 | 11 |
12 |
13 | 14 |
15 | 18 |
19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var initGulpTasks = require('react-component-gulp-tasks'); 3 | 4 | /** 5 | * Tasks are added by the react-component-gulp-tasks package 6 | * 7 | * See https://github.com/JedWatson/react-component-gulp-tasks 8 | * for documentation. 9 | * 10 | * You can also add your own additional gulp tasks if you like. 11 | */ 12 | 13 | var taskConfig = { 14 | 15 | component: { 16 | name: 'ReactHeatmap', 17 | dependencies: [ 18 | 'classnames', 19 | 'react', 20 | 'react-dom' 21 | ], 22 | lib: 'lib' 23 | }, 24 | 25 | example: { 26 | src: 'example/src', 27 | dist: 'example/dist', 28 | files: [ 29 | 'index.html', 30 | '.gitignore' 31 | ], 32 | scripts: [ 33 | 'example.js' 34 | ], 35 | less: [ 36 | 'example.less' 37 | ] 38 | } 39 | 40 | }; 41 | 42 | initGulpTasks(gulp, taskConfig); 43 | -------------------------------------------------------------------------------- /lib/ReactHeatmap.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, '__esModule', { 4 | value: true 5 | }); 6 | 7 | var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); 8 | 9 | var _get = function get(_x, _x2, _x3) { var _again = true; _function: while (_again) { var object = _x, property = _x2, receiver = _x3; _again = false; if (object === null) object = Function.prototype; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x = parent; _x2 = property; _x3 = receiver; _again = true; desc = parent = undefined; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; 10 | 11 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } 12 | 13 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } 14 | 15 | function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } 16 | 17 | var _react = require('react'); 18 | 19 | var _react2 = _interopRequireDefault(_react); 20 | 21 | var _propTypes = require('prop-types'); 22 | 23 | var _propTypes2 = _interopRequireDefault(_propTypes); 24 | 25 | var _reactDom = require('react-dom'); 26 | 27 | var _reactDom2 = _interopRequireDefault(_reactDom); 28 | 29 | var _heatmapjsBuildHeatmapJs = require('heatmapjs/build/heatmap.js'); 30 | 31 | var _heatmapjsBuildHeatmapJs2 = _interopRequireDefault(_heatmapjsBuildHeatmapJs); 32 | 33 | var ReactHeatmap = (function (_Component) { 34 | _inherits(ReactHeatmap, _Component); 35 | 36 | function ReactHeatmap(props) { 37 | _classCallCheck(this, ReactHeatmap); 38 | 39 | _get(Object.getPrototypeOf(ReactHeatmap.prototype), 'constructor', this).call(this, props); 40 | this.setData = this.setData.bind(this); 41 | } 42 | 43 | _createClass(ReactHeatmap, [{ 44 | key: 'componentDidMount', 45 | value: function componentDidMount() { 46 | this.heatmap = _heatmapjsBuildHeatmapJs2['default'].create({ 47 | container: _reactDom2['default'].findDOMNode(this) 48 | }); 49 | this.setData(this.props.max, this.props.data); 50 | } 51 | }, { 52 | key: 'componentWillReceiveProps', 53 | value: function componentWillReceiveProps(newProps) { 54 | this.setData(newProps.max, newProps.data); 55 | } 56 | }, { 57 | key: 'setData', 58 | value: function setData(max, data) { 59 | this.heatmap.setData({ 60 | max: max, 61 | data: this.computeData(data) 62 | }); 63 | } 64 | }, { 65 | key: 'computeData', 66 | value: function computeData(data) { 67 | var _this = this; 68 | 69 | if (this.props.unit === 'percent') { 70 | var _ret = (function () { 71 | var container = {}; 72 | container.width = _reactDom2['default'].findDOMNode(_this).offsetWidth; 73 | container.height = _reactDom2['default'].findDOMNode(_this).offsetHeight; 74 | return { 75 | v: data.map(function (values, index) { 76 | return { 77 | x: values.x / 100 * container.width, 78 | y: values.y / 100 * container.height, 79 | value: values.value 80 | }; 81 | }) 82 | }; 83 | })(); 84 | 85 | if (typeof _ret === 'object') return _ret.v; 86 | } else { 87 | return data; 88 | } 89 | } 90 | }, { 91 | key: 'render', 92 | value: function render() { 93 | return _react2['default'].createElement('div', { style: { width: '100%', height: '100%' } }); 94 | } 95 | }]); 96 | 97 | return ReactHeatmap; 98 | })(_react.Component); 99 | 100 | ReactHeatmap.propTypes = { 101 | max: _propTypes2['default'].number, 102 | data: _propTypes2['default'].array, 103 | unit: _propTypes2['default'].string 104 | }; 105 | 106 | ReactHeatmap.defaultProps = { 107 | max: 5, 108 | data: [], 109 | unit: 'percent' 110 | }; 111 | 112 | exports['default'] = ReactHeatmap; 113 | module.exports = exports['default']; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-heatmap", 3 | "version": "1.0.6", 4 | "description": "React Heatmap", 5 | "main": "lib/ReactHeatmap.js", 6 | "author": "Jonathan Widawski", 7 | "homepage": "https://github.com/JonathanWi/react-heatmap", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/JonathanWi/react-heatmap.git" 11 | }, 12 | "bugs": { 13 | "url": "https://github.com/JonathanWi/react-heatmap/issues" 14 | }, 15 | "dependencies": { 16 | "classnames": "^2.1.2", 17 | "heatmapjs": "git+https://github.com/JonathanWi/heatmap.js.git" 18 | }, 19 | "devDependencies": { 20 | "babel-eslint": "^4.1.3", 21 | "eslint": "^1.6.0", 22 | "eslint-plugin-react": "^3.5.1", 23 | "gulp": "^3.9.0", 24 | "react": "^0.14.6", 25 | "react-component-gulp-tasks": "^0.7.6", 26 | "react-dom": "^0.14.0" 27 | }, 28 | "peerDependencies": { 29 | "react": "^0.14.6" 30 | }, 31 | "browserify-shim": { 32 | "react": "global:React" 33 | }, 34 | "scripts": { 35 | "build": "gulp clean && NODE_ENV=production gulp build", 36 | "examples": "gulp dev:server", 37 | "lint": "eslint ./; true", 38 | "publish:site": "NODE_ENV=production gulp publish:examples", 39 | "release": "NODE_ENV=production gulp release", 40 | "start": "gulp dev", 41 | "test": "echo \"no tests yet\" && exit 0", 42 | "watch": "gulp watch:lib" 43 | }, 44 | "keywords": [ 45 | "react", 46 | "react-component" 47 | ] 48 | } 49 | -------------------------------------------------------------------------------- /src/ReactHeatmap.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react' 2 | import PropTypes from 'prop-types'; 3 | import ReactDOM from 'react-dom' 4 | import Heatmap from 'heatmapjs/build/heatmap.js' 5 | 6 | class ReactHeatmap extends Component { 7 | 8 | constructor(props) { 9 | super(props); 10 | this.setData = this.setData.bind(this); 11 | } 12 | 13 | componentDidMount() { 14 | this.heatmap = Heatmap.create({ 15 | container: ReactDOM.findDOMNode(this) 16 | }); 17 | this.setData(this.props.max, this.props.data); 18 | } 19 | 20 | componentWillReceiveProps(newProps) { 21 | this.setData(newProps.max, newProps.data); 22 | } 23 | 24 | setData(max, data) { 25 | this.heatmap.setData({ 26 | max: max, 27 | data: this.computeData(data) 28 | }); 29 | } 30 | 31 | computeData(data) { 32 | if(this.props.unit === 'percent') { 33 | let container = {}; 34 | container.width = ReactDOM.findDOMNode(this).offsetWidth; 35 | container.height = ReactDOM.findDOMNode(this).offsetHeight; 36 | return data.map(function(values, index) { 37 | return { 38 | x : Math.round(values.x/100 * container.width), 39 | y : Math.round(values.y/100 * container.height), 40 | value: values.value 41 | } 42 | }) 43 | } else { 44 | return data; 45 | } 46 | } 47 | 48 | render () { 49 | return( 50 |
51 | ); 52 | } 53 | } 54 | 55 | ReactHeatmap.propTypes = { 56 | max : PropTypes.number, 57 | data : PropTypes.array, 58 | unit : PropTypes.string 59 | } 60 | 61 | ReactHeatmap.defaultProps = { 62 | max: 5, 63 | data: [], 64 | unit: 'percent' 65 | } 66 | 67 | export default ReactHeatmap 68 | --------------------------------------------------------------------------------