ScatterPlotGlowOverlay.js 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264
  1. "use strict";
  2. exports.__esModule = true;
  3. exports.default = void 0;
  4. var _immutable = _interopRequireDefault(require("immutable"));
  5. var _propTypes = _interopRequireDefault(require("prop-types"));
  6. var _react = _interopRequireDefault(require("react"));
  7. var _reactMapGl = require("react-map-gl");
  8. var _geo = require("./utils/geo");
  9. var _roundDecimal = _interopRequireDefault(require("./utils/roundDecimal"));
  10. var _luminanceFromRGB = _interopRequireDefault(require("./utils/luminanceFromRGB"));
  11. require("mapbox-gl/dist/mapbox-gl.css");
  12. function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
  13. /**
  14. * Licensed to the Apache Software Foundation (ASF) under one
  15. * or more contributor license agreements. See the NOTICE file
  16. * distributed with this work for additional information
  17. * regarding copyright ownership. The ASF licenses this file
  18. * to you under the Apache License, Version 2.0 (the
  19. * "License"); you may not use this file except in compliance
  20. * with the License. You may obtain a copy of the License at
  21. *
  22. * http://www.apache.org/licenses/LICENSE-2.0
  23. *
  24. * Unless required by applicable law or agreed to in writing,
  25. * software distributed under the License is distributed on an
  26. * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
  27. * KIND, either express or implied. See the License for the
  28. * specific language governing permissions and limitations
  29. * under the License.
  30. */
  31. /* eslint-disable react/require-default-props */
  32. const propTypes = {
  33. aggregation: _propTypes.default.string,
  34. compositeOperation: _propTypes.default.string,
  35. dotRadius: _propTypes.default.number,
  36. lngLatAccessor: _propTypes.default.func,
  37. locations: _propTypes.default.instanceOf(_immutable.default.List).isRequired,
  38. pointRadiusUnit: _propTypes.default.string,
  39. renderWhileDragging: _propTypes.default.bool,
  40. rgb: _propTypes.default.arrayOf(_propTypes.default.oneOfType([_propTypes.default.string, _propTypes.default.number])),
  41. zoom: _propTypes.default.number
  42. };
  43. const defaultProps = {
  44. // Same as browser default.
  45. compositeOperation: 'source-over',
  46. dotRadius: 4,
  47. lngLatAccessor: location => [location.get(0), location.get(1)],
  48. renderWhileDragging: true
  49. };
  50. const computeClusterLabel = (properties, aggregation) => {
  51. const count = properties.get('point_count');
  52. if (!aggregation) {
  53. return count;
  54. }
  55. if (aggregation === 'sum' || aggregation === 'min' || aggregation === 'max') {
  56. return properties.get(aggregation);
  57. }
  58. const sum = properties.get('sum');
  59. const mean = sum / count;
  60. if (aggregation === 'mean') {
  61. return Math.round(100 * mean) / 100;
  62. }
  63. const squaredSum = properties.get('squaredSum');
  64. const variance = squaredSum / count - (sum / count) ** 2;
  65. if (aggregation === 'var') {
  66. return Math.round(100 * variance) / 100;
  67. }
  68. if (aggregation === 'stdev') {
  69. return Math.round(100 * Math.sqrt(variance)) / 100;
  70. } // fallback to point_count, this really shouldn't happen
  71. return count;
  72. };
  73. class ScatterPlotGlowOverlay extends _react.default.PureComponent {
  74. constructor(props) {
  75. super(props);
  76. this.redraw = this.redraw.bind(this);
  77. }
  78. drawText(ctx, pixel, options = {}) {
  79. const IS_DARK_THRESHOLD = 110;
  80. const {
  81. fontHeight = 0,
  82. label = '',
  83. radius = 0,
  84. rgb = [0, 0, 0],
  85. shadow = false
  86. } = options;
  87. const maxWidth = radius * 1.8;
  88. const luminance = (0, _luminanceFromRGB.default)(rgb[1], rgb[2], rgb[3]);
  89. ctx.globalCompositeOperation = 'source-over';
  90. ctx.fillStyle = luminance <= IS_DARK_THRESHOLD ? 'white' : 'black';
  91. ctx.font = fontHeight + "px sans-serif";
  92. ctx.textAlign = 'center';
  93. ctx.textBaseline = 'middle';
  94. if (shadow) {
  95. ctx.shadowBlur = 15;
  96. ctx.shadowColor = luminance <= IS_DARK_THRESHOLD ? 'black' : '';
  97. }
  98. const textWidth = ctx.measureText(label).width;
  99. if (textWidth > maxWidth) {
  100. const scale = fontHeight / textWidth;
  101. ctx.font = scale * maxWidth + "px sans-serif";
  102. }
  103. const {
  104. compositeOperation
  105. } = this.props;
  106. ctx.fillText(label, pixel[0], pixel[1]);
  107. ctx.globalCompositeOperation = compositeOperation;
  108. ctx.shadowBlur = 0;
  109. ctx.shadowColor = '';
  110. } // Modified: https://github.com/uber/react-map-gl/blob/master/overlays/scatterplot.react.js
  111. redraw({
  112. width,
  113. height,
  114. ctx,
  115. isDragging,
  116. project
  117. }) {
  118. const {
  119. aggregation,
  120. compositeOperation,
  121. dotRadius,
  122. lngLatAccessor,
  123. locations,
  124. pointRadiusUnit,
  125. renderWhileDragging,
  126. rgb,
  127. zoom
  128. } = this.props;
  129. const radius = dotRadius;
  130. const clusterLabelMap = [];
  131. locations.forEach((location, i) => {
  132. if (location.get('properties').get('cluster')) {
  133. clusterLabelMap[i] = computeClusterLabel(location.get('properties'), aggregation);
  134. }
  135. }, this);
  136. const maxLabel = Math.max(...clusterLabelMap.filter(v => !Number.isNaN(v)));
  137. ctx.clearRect(0, 0, width, height);
  138. ctx.globalCompositeOperation = compositeOperation;
  139. if ((renderWhileDragging || !isDragging) && locations) {
  140. locations.forEach(function _forEach(location, i) {
  141. const pixel = project(lngLatAccessor(location));
  142. const pixelRounded = [(0, _roundDecimal.default)(pixel[0], 1), (0, _roundDecimal.default)(pixel[1], 1)];
  143. if (pixelRounded[0] + radius >= 0 && pixelRounded[0] - radius < width && pixelRounded[1] + radius >= 0 && pixelRounded[1] - radius < height) {
  144. ctx.beginPath();
  145. if (location.get('properties').get('cluster')) {
  146. let clusterLabel = clusterLabelMap[i]; // eslint-disable-next-line no-restricted-properties, unicorn/prefer-exponentiation-operator
  147. const scaledRadius = (0, _roundDecimal.default)(Math.pow(clusterLabel / maxLabel, 0.5) * radius, 1);
  148. const fontHeight = (0, _roundDecimal.default)(scaledRadius * 0.5, 1);
  149. const [x, y] = pixelRounded;
  150. const gradient = ctx.createRadialGradient(x, y, scaledRadius, x, y, 0);
  151. gradient.addColorStop(1, "rgba(" + rgb[1] + ", " + rgb[2] + ", " + rgb[3] + ", 0.8)");
  152. gradient.addColorStop(0, "rgba(" + rgb[1] + ", " + rgb[2] + ", " + rgb[3] + ", 0)");
  153. ctx.arc(pixelRounded[0], pixelRounded[1], scaledRadius, 0, Math.PI * 2);
  154. ctx.fillStyle = gradient;
  155. ctx.fill();
  156. if (Number.isFinite(parseFloat(clusterLabel))) {
  157. if (clusterLabel >= 10000) {
  158. clusterLabel = Math.round(clusterLabel / 1000) + "k";
  159. } else if (clusterLabel >= 1000) {
  160. clusterLabel = Math.round(clusterLabel / 100) / 10 + "k";
  161. }
  162. this.drawText(ctx, pixelRounded, {
  163. fontHeight,
  164. label: clusterLabel,
  165. radius: scaledRadius,
  166. rgb,
  167. shadow: true
  168. });
  169. }
  170. } else {
  171. const defaultRadius = radius / 6;
  172. const radiusProperty = location.get('properties').get('radius');
  173. const pointMetric = location.get('properties').get('metric');
  174. let pointRadius = radiusProperty === null ? defaultRadius : radiusProperty;
  175. let pointLabel;
  176. if (radiusProperty !== null) {
  177. const pointLatitude = lngLatAccessor(location)[1];
  178. if (pointRadiusUnit === 'Kilometers') {
  179. pointLabel = (0, _roundDecimal.default)(pointRadius, 2) + "km";
  180. pointRadius = (0, _geo.kmToPixels)(pointRadius, pointLatitude, zoom);
  181. } else if (pointRadiusUnit === 'Miles') {
  182. pointLabel = (0, _roundDecimal.default)(pointRadius, 2) + "mi";
  183. pointRadius = (0, _geo.kmToPixels)(pointRadius * _geo.MILES_PER_KM, pointLatitude, zoom);
  184. }
  185. }
  186. if (pointMetric !== null) {
  187. pointLabel = Number.isFinite(parseFloat(pointMetric)) ? (0, _roundDecimal.default)(pointMetric, 2) : pointMetric;
  188. } // Fall back to default points if pointRadius wasn't a numerical column
  189. if (!pointRadius) {
  190. pointRadius = defaultRadius;
  191. }
  192. ctx.arc(pixelRounded[0], pixelRounded[1], (0, _roundDecimal.default)(pointRadius, 1), 0, Math.PI * 2);
  193. ctx.fillStyle = "rgb(" + rgb[1] + ", " + rgb[2] + ", " + rgb[3] + ")";
  194. ctx.fill();
  195. if (pointLabel !== undefined) {
  196. this.drawText(ctx, pixelRounded, {
  197. fontHeight: (0, _roundDecimal.default)(pointRadius, 1),
  198. label: pointLabel,
  199. radius: pointRadius,
  200. rgb,
  201. shadow: false
  202. });
  203. }
  204. }
  205. }
  206. }, this);
  207. }
  208. }
  209. render() {
  210. return _react.default.createElement(_reactMapGl.CanvasOverlay, {
  211. redraw: this.redraw
  212. });
  213. }
  214. }
  215. ScatterPlotGlowOverlay.propTypes = propTypes;
  216. ScatterPlotGlowOverlay.defaultProps = defaultProps;
  217. var _default = ScatterPlotGlowOverlay;
  218. exports.default = _default;