园林绿化
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

688 lines
20 KiB

  1. /*
  2. * heatmap.js v2.0.5 | JavaScript Heatmap Library
  3. *
  4. * Copyright 2008-2016 Patrick Wied <heatmapjs@patrick-wied.at> - All rights reserved.
  5. * Dual licensed under MIT and Beerware license
  6. *
  7. * :: 2016-09-05 01:16
  8. */
  9. ;(function (name, context, factory) {
  10. // Supports UMD. AMD, CommonJS/Node.js and browser context
  11. if (typeof module !== "undefined" && module.exports) {
  12. module.exports = factory()
  13. } else if (typeof define === "function" && define.amd) {
  14. define(factory)
  15. } else {
  16. context[name] = factory()
  17. }
  18. })("h337", this, function () {
  19. // Heatmap Config stores default values and will be merged with instance config
  20. var HeatmapConfig = {
  21. defaultRadius: 40,
  22. defaultRenderer: "canvas2d",
  23. defaultGradient: { 0.25: "rgb(0,0,255)", 0.55: "rgb(0,255,0)", 0.85: "yellow", 1.0: "rgb(255,0,0)" },
  24. defaultMaxOpacity: 1,
  25. defaultMinOpacity: 0,
  26. defaultBlur: 0.85,
  27. defaultXField: "x",
  28. defaultYField: "y",
  29. defaultValueField: "value",
  30. plugins: {}
  31. }
  32. var Store = (function StoreClosure() {
  33. var Store = function Store(config) {
  34. this._coordinator = {}
  35. this._data = []
  36. this._radi = []
  37. this._min = 10
  38. this._max = 1
  39. this._xField = config["xField"] || config.defaultXField
  40. this._yField = config["yField"] || config.defaultYField
  41. this._valueField = config["valueField"] || config.defaultValueField
  42. if (config["radius"]) {
  43. this._cfgRadius = config["radius"]
  44. }
  45. }
  46. var defaultRadius = HeatmapConfig.defaultRadius
  47. Store.prototype = {
  48. // when forceRender = false -> called from setData, omits renderall event
  49. _organiseData: function (dataPoint, forceRender) {
  50. var x = dataPoint[this._xField]
  51. var y = dataPoint[this._yField]
  52. var radi = this._radi
  53. var store = this._data
  54. var max = this._max
  55. var min = this._min
  56. var value = dataPoint[this._valueField] || 1
  57. var radius = dataPoint.radius || this._cfgRadius || defaultRadius
  58. if (!store[x]) {
  59. store[x] = []
  60. radi[x] = []
  61. }
  62. if (!store[x][y]) {
  63. store[x][y] = value
  64. radi[x][y] = radius
  65. } else {
  66. store[x][y] += value
  67. }
  68. var storedVal = store[x][y]
  69. if (storedVal > max) {
  70. if (!forceRender) {
  71. this._max = storedVal
  72. } else {
  73. this.setDataMax(storedVal)
  74. }
  75. return false
  76. } else if (storedVal < min) {
  77. if (!forceRender) {
  78. this._min = storedVal
  79. } else {
  80. this.setDataMin(storedVal)
  81. }
  82. return false
  83. } else {
  84. return {
  85. x: x,
  86. y: y,
  87. value: value,
  88. radius: radius,
  89. min: min,
  90. max: max
  91. }
  92. }
  93. },
  94. _unOrganizeData: function () {
  95. var unorganizedData = []
  96. var data = this._data
  97. var radi = this._radi
  98. for (var x in data) {
  99. for (var y in data[x]) {
  100. unorganizedData.push({
  101. x: x,
  102. y: y,
  103. radius: radi[x][y],
  104. value: data[x][y]
  105. })
  106. }
  107. }
  108. return {
  109. min: this._min,
  110. max: this._max,
  111. data: unorganizedData
  112. }
  113. },
  114. _onExtremaChange: function () {
  115. this._coordinator.emit("extremachange", {
  116. min: this._min,
  117. max: this._max
  118. })
  119. },
  120. addData: function () {
  121. if (arguments[0].length > 0) {
  122. var dataArr = arguments[0]
  123. var dataLen = dataArr.length
  124. while (dataLen--) {
  125. this.addData.call(this, dataArr[dataLen])
  126. }
  127. } else {
  128. // add to store
  129. var organisedEntry = this._organiseData(arguments[0], true)
  130. if (organisedEntry) {
  131. // if it's the first datapoint initialize the extremas with it
  132. if (this._data.length === 0) {
  133. this._min = this._max = organisedEntry.value
  134. }
  135. this._coordinator.emit("renderpartial", {
  136. min: this._min,
  137. max: this._max,
  138. data: [organisedEntry]
  139. })
  140. }
  141. }
  142. return this
  143. },
  144. setData: function (data) {
  145. var dataPoints = data.data
  146. var pointsLen = dataPoints.length
  147. // reset data arrays
  148. this._data = []
  149. this._radi = []
  150. for (var i = 0; i < pointsLen; i++) {
  151. this._organiseData(dataPoints[i], false)
  152. }
  153. this._max = data.max
  154. this._min = data.min || 0
  155. this._onExtremaChange()
  156. this._coordinator.emit("renderall", this._getInternalData())
  157. return this
  158. },
  159. removeData: function () {
  160. // TODO: implement
  161. },
  162. setDataMax: function (max) {
  163. this._max = max
  164. this._onExtremaChange()
  165. this._coordinator.emit("renderall", this._getInternalData())
  166. return this
  167. },
  168. setDataMin: function (min) {
  169. this._min = min
  170. this._onExtremaChange()
  171. this._coordinator.emit("renderall", this._getInternalData())
  172. return this
  173. },
  174. setCoordinator: function (coordinator) {
  175. this._coordinator = coordinator
  176. },
  177. _getInternalData: function () {
  178. return {
  179. max: this._max,
  180. min: this._min,
  181. data: this._data,
  182. radi: this._radi
  183. }
  184. },
  185. getData: function () {
  186. return this._unOrganizeData()
  187. } /*,
  188. TODO: rethink.
  189. getValueAt: function(point) {
  190. var value;
  191. var radius = 100;
  192. var x = point.x;
  193. var y = point.y;
  194. var data = this._data;
  195. if (data[x] && data[x][y]) {
  196. return data[x][y];
  197. } else {
  198. var values = [];
  199. // radial search for datapoints based on default radius
  200. for(var distance = 1; distance < radius; distance++) {
  201. var neighbors = distance * 2 +1;
  202. var startX = x - distance;
  203. var startY = y - distance;
  204. for(var i = 0; i < neighbors; i++) {
  205. for (var o = 0; o < neighbors; o++) {
  206. if ((i == 0 || i == neighbors-1) || (o == 0 || o == neighbors-1)) {
  207. if (data[startY+i] && data[startY+i][startX+o]) {
  208. values.push(data[startY+i][startX+o]);
  209. }
  210. } else {
  211. continue;
  212. }
  213. }
  214. }
  215. }
  216. if (values.length > 0) {
  217. return Math.max.apply(Math, values);
  218. }
  219. }
  220. return false;
  221. }*/
  222. }
  223. return Store
  224. })()
  225. var Canvas2dRenderer = (function Canvas2dRendererClosure() {
  226. var _getColorPalette = function (config) {
  227. var gradientConfig = config.gradient || config.defaultGradient
  228. var paletteCanvas = document.createElement("canvas")
  229. var paletteCtx = paletteCanvas.getContext("2d")
  230. paletteCanvas.width = 256
  231. paletteCanvas.height = 1
  232. var gradient = paletteCtx.createLinearGradient(0, 0, 256, 1)
  233. for (var key in gradientConfig) {
  234. gradient.addColorStop(key, gradientConfig[key])
  235. }
  236. paletteCtx.fillStyle = gradient
  237. paletteCtx.fillRect(0, 0, 256, 1)
  238. return paletteCtx.getImageData(0, 0, 256, 1).data
  239. }
  240. var _getPointTemplate = function (radius, blurFactor) {
  241. var tplCanvas = document.createElement("canvas")
  242. var tplCtx = tplCanvas.getContext("2d")
  243. var x = radius
  244. var y = radius
  245. tplCanvas.width = tplCanvas.height = radius * 2
  246. if (blurFactor == 1) {
  247. tplCtx.beginPath()
  248. tplCtx.arc(x, y, radius, 0, 2 * Math.PI, false)
  249. tplCtx.fillStyle = "rgba(0,0,0,1)"
  250. tplCtx.fill()
  251. } else {
  252. var gradient = tplCtx.createRadialGradient(x, y, radius * blurFactor, x, y, radius)
  253. gradient.addColorStop(0, "rgba(0,0,0,1)")
  254. gradient.addColorStop(1, "rgba(0,0,0,0)")
  255. tplCtx.fillStyle = gradient
  256. tplCtx.fillRect(0, 0, 2 * radius, 2 * radius)
  257. }
  258. return tplCanvas
  259. }
  260. var _prepareData = function (data) {
  261. var renderData = []
  262. var min = data.min
  263. var max = data.max
  264. var radi = data.radi
  265. var data = data.data
  266. var xValues = Object.keys(data)
  267. var xValuesLen = xValues.length
  268. while (xValuesLen--) {
  269. var xValue = xValues[xValuesLen]
  270. var yValues = Object.keys(data[xValue])
  271. var yValuesLen = yValues.length
  272. while (yValuesLen--) {
  273. var yValue = yValues[yValuesLen]
  274. var value = data[xValue][yValue]
  275. var radius = radi[xValue][yValue]
  276. renderData.push({
  277. x: xValue,
  278. y: yValue,
  279. value: value,
  280. radius: radius
  281. })
  282. }
  283. }
  284. return {
  285. min: min,
  286. max: max,
  287. data: renderData
  288. }
  289. }
  290. function Canvas2dRenderer(config) {
  291. var container = config.container
  292. var shadowCanvas = (this.shadowCanvas = document.createElement("canvas"))
  293. var canvas = (this.canvas = config.canvas || document.createElement("canvas"))
  294. var renderBoundaries = (this._renderBoundaries = [10000, 10000, 0, 0])
  295. var computed = getComputedStyle(config.container) || {}
  296. canvas.className = "heatmap-canvas"
  297. this._width = canvas.width = shadowCanvas.width = config.width || +computed.width.replace(/px/, "")
  298. this._height = canvas.height = shadowCanvas.height = config.height || +computed.height.replace(/px/, "")
  299. this.shadowCtx = shadowCanvas.getContext("2d")
  300. this.ctx = canvas.getContext("2d")
  301. // @TODO:
  302. // conditional wrapper
  303. canvas.style.cssText = shadowCanvas.style.cssText = "position:absolute;left:0;top:0;"
  304. container.style.position = "relative"
  305. container.appendChild(canvas)
  306. this._palette = _getColorPalette(config)
  307. this._templates = {}
  308. this._setStyles(config)
  309. }
  310. Canvas2dRenderer.prototype = {
  311. renderPartial: function (data) {
  312. if (data.data.length > 0) {
  313. this._drawAlpha(data)
  314. this._colorize()
  315. }
  316. },
  317. renderAll: function (data) {
  318. // reset render boundaries
  319. this._clear()
  320. if (data.data.length > 0) {
  321. this._drawAlpha(_prepareData(data))
  322. this._colorize()
  323. }
  324. },
  325. _updateGradient: function (config) {
  326. this._palette = _getColorPalette(config)
  327. },
  328. updateConfig: function (config) {
  329. if (config["gradient"]) {
  330. this._updateGradient(config)
  331. }
  332. this._setStyles(config)
  333. },
  334. setDimensions: function (width, height) {
  335. this._width = width
  336. this._height = height
  337. this.canvas.width = this.shadowCanvas.width = width
  338. this.canvas.height = this.shadowCanvas.height = height
  339. },
  340. _clear: function () {
  341. this.shadowCtx.clearRect(0, 0, this._width, this._height)
  342. this.ctx.clearRect(0, 0, this._width, this._height)
  343. },
  344. _setStyles: function (config) {
  345. this._blur = config.blur == 0 ? 0 : config.blur || config.defaultBlur
  346. if (config.backgroundColor) {
  347. this.canvas.style.backgroundColor = config.backgroundColor
  348. }
  349. this._width = this.canvas.width = this.shadowCanvas.width = config.width || this._width
  350. this._height = this.canvas.height = this.shadowCanvas.height = config.height || this._height
  351. this._opacity = (config.opacity || 0) * 255
  352. this._maxOpacity = (config.maxOpacity || config.defaultMaxOpacity) * 255
  353. this._minOpacity = (config.minOpacity || config.defaultMinOpacity) * 255
  354. this._useGradientOpacity = !!config.useGradientOpacity
  355. },
  356. _drawAlpha: function (data) {
  357. var min = (this._min = data.min)
  358. var max = (this._max = data.max)
  359. var data = data.data || []
  360. var dataLen = data.length
  361. // on a point basis?
  362. var blur = 1 - this._blur
  363. while (dataLen--) {
  364. var point = data[dataLen]
  365. var x = point.x
  366. var y = point.y
  367. var radius = point.radius
  368. // if value is bigger than max
  369. // use max as value
  370. var value = Math.min(point.value, max)
  371. var rectX = x - radius
  372. var rectY = y - radius
  373. var shadowCtx = this.shadowCtx
  374. var tpl
  375. if (!this._templates[radius]) {
  376. this._templates[radius] = tpl = _getPointTemplate(radius, blur)
  377. } else {
  378. tpl = this._templates[radius]
  379. }
  380. // value from minimum / value range
  381. // => [0, 1]
  382. var templateAlpha = (value - min) / (max - min)
  383. // this fixes #176: small values are not visible because globalAlpha < .01 cannot be read from imageData
  384. shadowCtx.globalAlpha = templateAlpha < 0.01 ? 0.01 : templateAlpha
  385. shadowCtx.drawImage(tpl, rectX, rectY)
  386. // update renderBoundaries
  387. if (rectX < this._renderBoundaries[0]) {
  388. this._renderBoundaries[0] = rectX
  389. }
  390. if (rectY < this._renderBoundaries[1]) {
  391. this._renderBoundaries[1] = rectY
  392. }
  393. if (rectX + 2 * radius > this._renderBoundaries[2]) {
  394. this._renderBoundaries[2] = rectX + 2 * radius
  395. }
  396. if (rectY + 2 * radius > this._renderBoundaries[3]) {
  397. this._renderBoundaries[3] = rectY + 2 * radius
  398. }
  399. }
  400. },
  401. _colorize: function () {
  402. var x = this._renderBoundaries[0]
  403. var y = this._renderBoundaries[1]
  404. var width = this._renderBoundaries[2] - x
  405. var height = this._renderBoundaries[3] - y
  406. var maxWidth = this._width
  407. var maxHeight = this._height
  408. var opacity = this._opacity
  409. var maxOpacity = this._maxOpacity
  410. var minOpacity = this._minOpacity
  411. var useGradientOpacity = this._useGradientOpacity
  412. if (x < 0) {
  413. x = 0
  414. }
  415. if (y < 0) {
  416. y = 0
  417. }
  418. if (x + width > maxWidth) {
  419. width = maxWidth - x
  420. }
  421. if (y + height > maxHeight) {
  422. height = maxHeight - y
  423. }
  424. var img = this.shadowCtx.getImageData(x, y, width, height)
  425. var imgData = img.data
  426. var len = imgData.length
  427. var palette = this._palette
  428. for (var i = 3; i < len; i += 4) {
  429. var alpha = imgData[i]
  430. var offset = alpha * 4
  431. if (!offset) {
  432. continue
  433. }
  434. var finalAlpha
  435. if (opacity > 0) {
  436. finalAlpha = opacity
  437. } else {
  438. if (alpha < maxOpacity) {
  439. if (alpha < minOpacity) {
  440. finalAlpha = minOpacity
  441. } else {
  442. finalAlpha = alpha
  443. }
  444. } else {
  445. finalAlpha = maxOpacity
  446. }
  447. }
  448. imgData[i - 3] = palette[offset]
  449. imgData[i - 2] = palette[offset + 1]
  450. imgData[i - 1] = palette[offset + 2]
  451. imgData[i] = useGradientOpacity ? palette[offset + 3] : finalAlpha
  452. }
  453. this.ctx.putImageData(img, x, y)
  454. this._renderBoundaries = [1000, 1000, 0, 0]
  455. },
  456. getValueAt: function (point) {
  457. var value
  458. var shadowCtx = this.shadowCtx
  459. var img = shadowCtx.getImageData(point.x, point.y, 1, 1)
  460. var data = img.data[3]
  461. var max = this._max
  462. var min = this._min
  463. value = (Math.abs(max - min) * (data / 255)) >> 0
  464. return value
  465. },
  466. getDataURL: function () {
  467. return this.canvas.toDataURL()
  468. }
  469. }
  470. return Canvas2dRenderer
  471. })()
  472. var Renderer = (function RendererClosure() {
  473. var rendererFn = false
  474. if (HeatmapConfig["defaultRenderer"] === "canvas2d") {
  475. rendererFn = Canvas2dRenderer
  476. }
  477. return rendererFn
  478. })()
  479. var Util = {
  480. merge: function () {
  481. var merged = {}
  482. var argsLen = arguments.length
  483. for (var i = 0; i < argsLen; i++) {
  484. var obj = arguments[i]
  485. for (var key in obj) {
  486. merged[key] = obj[key]
  487. }
  488. }
  489. return merged
  490. }
  491. }
  492. // Heatmap Constructor
  493. var Heatmap = (function HeatmapClosure() {
  494. var Coordinator = (function CoordinatorClosure() {
  495. function Coordinator() {
  496. this.cStore = {}
  497. }
  498. Coordinator.prototype = {
  499. on: function (evtName, callback, scope) {
  500. var cStore = this.cStore
  501. if (!cStore[evtName]) {
  502. cStore[evtName] = []
  503. }
  504. cStore[evtName].push(function (data) {
  505. return callback.call(scope, data)
  506. })
  507. },
  508. emit: function (evtName, data) {
  509. var cStore = this.cStore
  510. if (cStore[evtName]) {
  511. var len = cStore[evtName].length
  512. for (var i = 0; i < len; i++) {
  513. var callback = cStore[evtName][i]
  514. callback(data)
  515. }
  516. }
  517. }
  518. }
  519. return Coordinator
  520. })()
  521. var _connect = function (scope) {
  522. var renderer = scope._renderer
  523. var coordinator = scope._coordinator
  524. var store = scope._store
  525. coordinator.on("renderpartial", renderer.renderPartial, renderer)
  526. coordinator.on("renderall", renderer.renderAll, renderer)
  527. coordinator.on("extremachange", function (data) {
  528. scope._config.onExtremaChange &&
  529. scope._config.onExtremaChange({
  530. min: data.min,
  531. max: data.max,
  532. gradient: scope._config["gradient"] || scope._config["defaultGradient"]
  533. })
  534. })
  535. store.setCoordinator(coordinator)
  536. }
  537. function Heatmap() {
  538. var config = (this._config = Util.merge(HeatmapConfig, arguments[0] || {}))
  539. this._coordinator = new Coordinator()
  540. if (config["plugin"]) {
  541. var pluginToLoad = config["plugin"]
  542. if (!HeatmapConfig.plugins[pluginToLoad]) {
  543. throw new Error("Plugin '" + pluginToLoad + "' not found. Maybe it was not registered.")
  544. } else {
  545. var plugin = HeatmapConfig.plugins[pluginToLoad]
  546. // set plugin renderer and store
  547. this._renderer = new plugin.renderer(config)
  548. this._store = new plugin.store(config)
  549. }
  550. } else {
  551. this._renderer = new Renderer(config)
  552. this._store = new Store(config)
  553. }
  554. _connect(this)
  555. }
  556. // @TODO:
  557. // add API documentation
  558. Heatmap.prototype = {
  559. addData: function () {
  560. this._store.addData.apply(this._store, arguments)
  561. return this
  562. },
  563. removeData: function () {
  564. this._store.removeData && this._store.removeData.apply(this._store, arguments)
  565. return this
  566. },
  567. setData: function () {
  568. this._store.setData.apply(this._store, arguments)
  569. return this
  570. },
  571. setDataMax: function () {
  572. this._store.setDataMax.apply(this._store, arguments)
  573. return this
  574. },
  575. setDataMin: function () {
  576. this._store.setDataMin.apply(this._store, arguments)
  577. return this
  578. },
  579. configure: function (config) {
  580. this._config = Util.merge(this._config, config)
  581. this._renderer.updateConfig(this._config)
  582. this._coordinator.emit("renderall", this._store._getInternalData())
  583. return this
  584. },
  585. repaint: function () {
  586. this._coordinator.emit("renderall", this._store._getInternalData())
  587. return this
  588. },
  589. getData: function () {
  590. return this._store.getData()
  591. },
  592. getDataURL: function () {
  593. return this._renderer.getDataURL()
  594. },
  595. getValueAt: function (point) {
  596. if (this._store.getValueAt) {
  597. return this._store.getValueAt(point)
  598. } else if (this._renderer.getValueAt) {
  599. return this._renderer.getValueAt(point)
  600. } else {
  601. return null
  602. }
  603. }
  604. }
  605. return Heatmap
  606. })()
  607. // core
  608. var heatmapFactory = {
  609. create: function (config) {
  610. return new Heatmap(config)
  611. },
  612. register: function (pluginKey, plugin) {
  613. HeatmapConfig.plugins[pluginKey] = plugin
  614. }
  615. }
  616. return heatmapFactory
  617. })