heatmapFeature.js

  1. var inherit = require('./inherit');
  2. var feature = require('./feature');
  3. var transform = require('./transform');
  4. var util = require('./util');
  5. /**
  6. * Heatmap feature specification.
  7. *
  8. * @typedef {geo.feature.spec} geo.heatmapFeature.spec
  9. * @extends geo.feature.spec
  10. * @property {geo.geoPosition|function} [position] Position of the data.
  11. * Default is (data).
  12. * @property {function} [intensity] Scalar value of each data point. The
  13. * scalar value must be a positive real number and is used to compute the
  14. * weight for each data point.
  15. * @property {number} [maxIntensity=null] Maximum intensity of the data.
  16. * Maximum intensity must be a positive real number and is used to normalize
  17. * all intensities within a dataset. If `null`, it is computed.
  18. * @property {number} [minIntensity=null] Minimum intensity of the data.
  19. * Minimum intensity must be a positive real number and is used to normalize
  20. * all intensities within a dataset. If `null`, it is computed.
  21. * @property {number} [updateDelay=-1] Delay in milliseconds after a zoom,
  22. * rotate, or pan event before recomputing the heatmap. If 0, this is double
  23. * the last render time. If negative, it is roughly the last render time
  24. * plus the absolute value of the specified number of refresh intervals.
  25. * compute a delay based on the last heatmap render time.
  26. * @property {boolean|number|'auto'} [binned='auto'] If `true` or a number,
  27. * spatially bin data as part of producing the heatmap. If falsy, each
  28. * datapoint stands on its own. If `'auto'`, bin data if there are more data
  29. * points than there would be bins. Using `true` or `auto` uses bins that
  30. * are `max(Math.floor((radius + blurRadius) / 8), 3)`.
  31. * @property {object} [style] A style for the heatmap.
  32. * @property {object} [style.color] An object where the keys are numbers from
  33. * [0-1] and the values are {@link geo.geoColor}. This is used to transform
  34. * normalized intensity.
  35. * @property {number} [style.radius=10] Radius of a point in pixels.
  36. * @property {number} [style.blurRadius=10] Blur radius for each point in
  37. * pixels.
  38. * @property {boolean} [style.gaussian=true] If truthy, approximate a gaussian
  39. * distribution for each point using a multi-segment linear radial
  40. * approximation. The total weight of the gaussian area is approximately the
  41. * `9/16 r^2`. The sum of `radius + blurRadius` is used as the radius for
  42. * the gaussian distribution.
  43. * @property {boolean} [scaleWithZoom=false] If truthy, the value for radius
  44. * and blurRadius scale with zoom. In this case, the values for radius and
  45. * blurRadius are the values at zoom-level zero. If the scaled radius is
  46. * less than 0.5 or more than 8192 screen pixels, the heatmap will not
  47. * render.
  48. */
  49. /**
  50. * Create a new instance of class heatmapFeature.
  51. *
  52. * @class
  53. * @alias geo.heatmapFeature
  54. * @param {geo.heatmapFeature.spec} arg Feature specification.
  55. * @extends geo.feature
  56. * @returns {geo.heatmapFeature}
  57. */
  58. var heatmapFeature = function (arg) {
  59. 'use strict';
  60. if (!(this instanceof heatmapFeature)) {
  61. return new heatmapFeature(arg);
  62. }
  63. arg = arg || {};
  64. feature.call(this, arg);
  65. /**
  66. * @private
  67. */
  68. var m_this = this,
  69. m_position,
  70. m_intensity,
  71. m_maxIntensity,
  72. m_minIntensity,
  73. m_updateDelay,
  74. m_binned,
  75. m_gcsPosition,
  76. s_init = this._init;
  77. this.featureType = 'heatmap';
  78. m_position = arg.position || util.identityFunction;
  79. m_intensity = arg.intensity || function (d) { return 1; };
  80. m_maxIntensity = arg.maxIntensity !== undefined ? arg.maxIntensity : null;
  81. m_minIntensity = arg.minIntensity !== undefined ? arg.minIntensity : null;
  82. m_binned = arg.binned !== undefined ? arg.binned : 'auto';
  83. m_updateDelay = (arg.updateDelay || arg.updateDelay === 0) ? parseInt(arg.updateDelay, 10) : -1;
  84. /**
  85. * Get/Set maxIntensity.
  86. *
  87. * @param {number|null} [val] If not specified, return the current value.
  88. * If a number, use this as the maximum intensity. If `null`, compute
  89. * the maximum intensity.
  90. * @returns {number|null|this}
  91. */
  92. this.maxIntensity = function (val) {
  93. if (val === undefined) {
  94. return m_maxIntensity;
  95. } else {
  96. m_maxIntensity = val;
  97. m_this.dataTime().modified();
  98. m_this.modified();
  99. }
  100. return m_this;
  101. };
  102. /**
  103. * Get/Set minIntensity.
  104. *
  105. * @param {number|null} [val] If not specified, return the current value.
  106. * If a number, use this as the minimum intensity. If `null`, compute
  107. * the minimum intensity.
  108. * @returns {number|null|this}
  109. */
  110. this.minIntensity = function (val) {
  111. if (val === undefined) {
  112. return m_minIntensity;
  113. } else {
  114. m_minIntensity = val;
  115. m_this.dataTime().modified();
  116. m_this.modified();
  117. }
  118. return m_this;
  119. };
  120. /**
  121. * Get/Set updateDelay.
  122. *
  123. * @param {number} [val] If not specified, return the current update delay.
  124. * If specified, this is the delay in milliseconds after a zoom, rotate,
  125. * or pan event before recomputing the heatmap. If 0, this is double the
  126. * last render time. If negative, it is roughly the last render time plus
  127. * the absolute value of the specified number of refresh intervals.
  128. * @returns {number|this}
  129. */
  130. this.updateDelay = function (val) {
  131. if (val === undefined) {
  132. return m_updateDelay;
  133. } else {
  134. m_updateDelay = parseInt(val, 10);
  135. }
  136. return m_this;
  137. };
  138. /**
  139. * Get/Set binned value.
  140. *
  141. * @param {boolean|number|'auto'} [val] If not specified, return the current
  142. * binned value. If `true` or a number, spatially bin data as part of
  143. * producing the heatmap. If falsy, each datapoint stands on its own.
  144. * If `'auto'`, bin data if there are more data points than there would be
  145. * bins. Using `true` or `auto` uses bins that are
  146. * `max(Math.floor((radius + blurRadius) / 8), 3)`.
  147. * @returns {boolean|number|'auto'|this}
  148. */
  149. this.binned = function (val) {
  150. if (val === undefined) {
  151. return m_binned;
  152. } else {
  153. if (val === 'true') {
  154. val = true;
  155. } else if (val === 'false') {
  156. val = false;
  157. } else if (val !== 'auto' && val !== true && val !== false) {
  158. val = parseInt(val, 10);
  159. if (val <= 0 || isNaN(val)) {
  160. val = false;
  161. }
  162. }
  163. m_binned = val;
  164. m_this.dataTime().modified();
  165. m_this.modified();
  166. }
  167. return m_this;
  168. };
  169. /**
  170. * Get/Set position accessor.
  171. *
  172. * @param {geo.geoPosition|function} [val] If not specified, return the
  173. * current position accessor. If specified, use this for the position
  174. * accessor and return `this`. If a function is given, this is called
  175. * with `(dataElement, dataIndex)`.
  176. * @returns {geo.geoPosition|function|this} The current position or this
  177. * feature.
  178. */
  179. this.position = function (val) {
  180. if (val === undefined) {
  181. return m_position;
  182. } else {
  183. m_position = val;
  184. m_this.dataTime().modified();
  185. m_this.modified();
  186. }
  187. return m_this;
  188. };
  189. /**
  190. * Get pre-computed gcs position accessor.
  191. *
  192. * @returns {geo.heatmap}
  193. */
  194. this.gcsPosition = function () {
  195. m_this._update();
  196. return m_gcsPosition;
  197. };
  198. /**
  199. * Get/Set intensity.
  200. *
  201. * @param {function} [val] If not specified, the current intensity accessor.
  202. * Otherwise, a function that returns the intensity of each data point.
  203. * @returns {function|this}
  204. */
  205. this.intensity = function (val) {
  206. if (val === undefined) {
  207. return m_intensity;
  208. } else {
  209. m_intensity = val;
  210. m_this.dataTime().modified();
  211. m_this.modified();
  212. }
  213. return m_this;
  214. };
  215. /**
  216. * Initialize.
  217. *
  218. * @param {geo.heatmapFeature.spec} arg
  219. */
  220. this._init = function (arg) {
  221. s_init.call(m_this, arg);
  222. var defaultStyle = Object.assign(
  223. {},
  224. {
  225. radius: 10,
  226. blurRadius: 10,
  227. gaussian: true,
  228. color: {
  229. 0: {r: 0, g: 0, b: 0.0, a: 0.0},
  230. 0.25: {r: 0, g: 0, b: 1, a: 0.5},
  231. 0.5: {r: 0, g: 1, b: 1, a: 0.6},
  232. 0.75: {r: 1, g: 1, b: 0, a: 0.7},
  233. 1: {r: 1, g: 0, b: 0, a: 0.8}},
  234. scaleWithZoom: false
  235. },
  236. arg.style === undefined ? {} : arg.style
  237. );
  238. m_this.style(defaultStyle);
  239. if (m_position) {
  240. m_this.dataTime().modified();
  241. }
  242. };
  243. /**
  244. * Build the feature.
  245. *
  246. * @returns {this}
  247. */
  248. this._build = function () {
  249. var data = m_this.data(),
  250. intensity = null,
  251. position = [],
  252. setMax = (m_maxIntensity === null || m_maxIntensity === undefined),
  253. setMin = (m_minIntensity === null || m_minIntensity === undefined);
  254. data.forEach(function (d, i) {
  255. position.push(m_this.position()(d, i));
  256. if (setMax || setMin) {
  257. intensity = m_this.intensity()(d, i);
  258. if (m_maxIntensity === null || m_maxIntensity === undefined) {
  259. m_maxIntensity = intensity;
  260. }
  261. if (m_minIntensity === null || m_minIntensity === undefined) {
  262. m_minIntensity = intensity;
  263. }
  264. if (setMax && intensity > m_maxIntensity) {
  265. m_maxIntensity = intensity;
  266. }
  267. if (setMin && intensity < m_minIntensity) {
  268. m_minIntensity = intensity;
  269. }
  270. }
  271. });
  272. if (setMin && setMax && m_minIntensity === m_maxIntensity) {
  273. m_minIntensity -= 1;
  274. }
  275. m_gcsPosition = transform.transformCoordinates(
  276. m_this.gcs(), m_this.layer().map().gcs(), position);
  277. m_this.buildTime().modified();
  278. return m_this;
  279. };
  280. this._init(arg);
  281. return this;
  282. };
  283. inherit(heatmapFeature, feature);
  284. module.exports = heatmapFeature;