custom-hexbin.html 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309
  1. <html>
  2. <head>
  3. <meta charset="utf-8">
  4. <script src="lib/esl.js"></script>
  5. <script src="lib/config.js"></script>
  6. <script src="lib/jquery.min.js"></script>
  7. <script src="lib/testHelper.js"></script>
  8. <meta name="viewport" content="width=device-width, initial-scale=1" />
  9. <link rel="stylesheet" href="lib/reset.css" />
  10. </head>
  11. <body>
  12. <style>
  13. h1 {
  14. line-height: 60px;
  15. height: 60px;
  16. background: #ddd;
  17. text-align: center;
  18. font-weight: bold;
  19. font-size: 14px;
  20. }
  21. .chart {
  22. height: 500px;
  23. margin: 10px auto;
  24. }
  25. </style>
  26. <h1>Hexagonal Binning</h1>
  27. <div class="chart" id="hexagonal-binning"></div>
  28. <script>
  29. // Hexbin statistics code based on [d3-hexbin](https://github.com/d3/d3-hexbin)
  30. function hexBinStatistics(points, r) {
  31. var dx = r * 2 * Math.sin(Math.PI / 3)
  32. var dy = r * 1.5;
  33. var binsById = {};
  34. var bins = [];
  35. for (var i = 0, n = points.length; i < n; ++i) {
  36. var point = points[i];
  37. var px = point[0];
  38. var py = point[1];
  39. if (isNaN(px) || isNaN(py)) {
  40. continue;
  41. }
  42. var pj = Math.round(py = py / dy);
  43. var pi = Math.round(px = px / dx - (pj & 1) / 2);
  44. var py1 = py - pj;
  45. if (Math.abs(py1) * 3 > 1) {
  46. var px1 = px - pi;
  47. var pi2 = pi + (px < pi ? -1 : 1) / 2;
  48. var pj2 = pj + (py < pj ? -1 : 1);
  49. var px2 = px - pi2;
  50. var py2 = py - pj2;
  51. if (px1 * px1 + py1 * py1 > px2 * px2 + py2 * py2) {
  52. pi = pi2 + (pj & 1 ? 1 : -1) / 2;
  53. pj = pj2;
  54. }
  55. }
  56. var id = pi + "-" + pj;
  57. var bin = binsById[id];
  58. if (bin) {
  59. bin.points.push(point);
  60. }
  61. else {
  62. bins.push(bin = binsById[id] = {points: [point]});
  63. bin.x = (pi + (pj & 1) / 2) * dx;
  64. bin.y = pj * dy;
  65. }
  66. }
  67. var maxBinLen = -Infinity
  68. for (var i = 0; i < bins.length; i++) {
  69. maxBinLen = Math.max(maxBinLen, bins.length);
  70. }
  71. return {
  72. maxBinLen: maxBinLen,
  73. bins: bins
  74. };
  75. }
  76. </script>
  77. <script>
  78. require([
  79. 'echarts'
  80. // 'echarts/chart/custom',
  81. // 'echarts/chart/bar',
  82. // 'echarts/component/title',
  83. // 'echarts/component/geo',
  84. // 'echarts/component/legend',
  85. // 'echarts/component/tooltip',
  86. // 'echarts/component/visualMap',
  87. // 'echarts/component/dataZoom',
  88. // 'zrender/vml/vml'
  89. ], function (echarts) {
  90. // 2006-2007 Regular Season
  91. $.getJSON('./data/kawhi-leonard-16-17-regular.json', function (shotData) {
  92. $.getJSON('./data/nba-court.json', function (nbaCourt) {
  93. echarts.registerMap('nbaCourt', nbaCourt.borderGeoJSON);
  94. var backgroundColor = '#333';
  95. var hexagonRadiusInGeo = 1;
  96. var hexBinResult = hexBinStatistics(
  97. echarts.util.map(shotData.data, function (item) {
  98. // "shot_made_flag" made missed
  99. var made = item[echarts.util.indexOf(shotData.schema, 'shot_made_flag')];
  100. return [
  101. item[echarts.util.indexOf(shotData.schema, 'loc_x')],
  102. item[echarts.util.indexOf(shotData.schema, 'loc_y')],
  103. made === 'made' ? 1 : 0
  104. ];
  105. }),
  106. hexagonRadiusInGeo
  107. );
  108. var data = echarts.util.map(hexBinResult.bins, function (bin) {
  109. var made = 0;
  110. echarts.util.each(bin.points, function (point) {
  111. made += point[2];
  112. });
  113. return [bin.x, bin.y, bin.points.length, (made / bin.points.length * 100).toFixed(2)];
  114. });
  115. function renderItemHexBin(params, api) {
  116. var center = api.coord([api.value(0), api.value(1)]);
  117. var points = [];
  118. var pointsBG = [];
  119. var maxViewRadius = api.size([hexagonRadiusInGeo, 0])[0];
  120. var minViewRadius = Math.min(maxViewRadius, 4);
  121. var extentMax = Math.log(Math.sqrt(hexBinResult.maxBinLen));
  122. var viewRadius = echarts.number.linearMap(
  123. Math.log(Math.sqrt(api.value(2))),
  124. [0, extentMax],
  125. [minViewRadius, maxViewRadius]
  126. );
  127. var angle = Math.PI / 6;
  128. for (var i = 0; i < 6; i++, angle += Math.PI / 3) {
  129. points.push([
  130. center[0] + viewRadius * Math.cos(angle),
  131. center[1] + viewRadius * Math.sin(angle)
  132. ]);
  133. pointsBG.push([
  134. center[0] + maxViewRadius * Math.cos(angle),
  135. center[1] + maxViewRadius * Math.sin(angle)
  136. ]);
  137. }
  138. return {
  139. type: 'group',
  140. children: [{
  141. type: 'polygon',
  142. shape: {
  143. points: points
  144. },
  145. style: {
  146. stroke: '#ccc',
  147. fill: api.visual('color'),
  148. lineWidth: 0
  149. }
  150. }, {
  151. type: 'polygon',
  152. shape: {
  153. points: pointsBG
  154. },
  155. style: {
  156. stroke: null,
  157. fill: 'rgba(0,0,0,0.5)',
  158. lineWidth: 0
  159. },
  160. z2: -19
  161. }]
  162. };
  163. }
  164. function renderItemNBACourt(param, api) {
  165. return {
  166. type: 'group',
  167. children: echarts.util.map(nbaCourt.geometry, function (item) {
  168. return {
  169. type: item.type,
  170. style: {
  171. stroke: '#aaa',
  172. fill: null,
  173. lineWidth: 1.5
  174. },
  175. shape: {
  176. points: echarts.util.map(item.points, api.coord)
  177. }
  178. };
  179. })
  180. };
  181. }
  182. var option = {
  183. backgroundColor: backgroundColor,
  184. aria: {
  185. show: true
  186. },
  187. tooltip: {
  188. backgroundColor: 'rgba(255,255,255,0.9)',
  189. textStyle: {
  190. color: '#333'
  191. }
  192. },
  193. title: {
  194. text: 'Kawhi Leonard',
  195. subtext: '2016-2017 Regular Season',
  196. backgroundColor: backgroundColor,
  197. top: 10,
  198. left: 10,
  199. textStyle: {
  200. color: '#eee'
  201. }
  202. },
  203. legend: {
  204. data: ['bar', 'error']
  205. },
  206. geo: {
  207. left: 0,
  208. right: 0,
  209. top: 0,
  210. bottom: 0,
  211. roam: true,
  212. silent: true,
  213. itemStyle: {
  214. normal: {
  215. color: backgroundColor,
  216. borderWidth: 0
  217. }
  218. },
  219. map: 'nbaCourt'
  220. },
  221. visualMap: {
  222. type: 'continuous',
  223. orient: 'horizontal',
  224. right: 30,
  225. top: 40,
  226. min: 0,
  227. max: 100,
  228. align: 'bottom',
  229. text: [null, 'FG: '],
  230. dimension: 3,
  231. seriesIndex: 0,
  232. calculable: true,
  233. textStyle: {
  234. color: '#eee'
  235. },
  236. formatter: '{value} %',
  237. inRange: {
  238. // color: ['rgba(241,222,158, 0.3)', 'rgba(241,222,158, 1)']
  239. color: ['green', 'yellow']
  240. }
  241. },
  242. series: [{
  243. type: 'custom',
  244. coordinateSystem: 'geo',
  245. geoIndex: 0,
  246. renderItem: renderItemHexBin,
  247. dimensions: [null, null, 'Field Goals Attempted (hexagon size)', 'Field Goal Percentage (color)'],
  248. encode: {
  249. tooltip: [2, 3]
  250. },
  251. data: data
  252. }, {
  253. coordinateSystem: 'geo',
  254. type: 'custom',
  255. geoIndex: 0,
  256. renderItem: renderItemNBACourt,
  257. silent: true,
  258. data: [0]
  259. }]
  260. };
  261. var width = 700;
  262. testHelper.createChart(echarts, 'hexagonal-binning', option, {
  263. width: width,
  264. height: width * nbaCourt.height / nbaCourt.width
  265. });
  266. });
  267. });
  268. });
  269. </script>
  270. </body>
  271. </html>