|  |  | 
|  | <!-- | 
|  | Licensed to the Apache Software Foundation (ASF) under one | 
|  | or more contributor license agreements.  See the NOTICE file | 
|  | distributed with this work for additional information | 
|  | regarding copyright ownership.  The ASF licenses this file | 
|  | to you under the Apache License, Version 2.0 (the | 
|  | "License"); you may not use this file except in compliance | 
|  | with the License.  You may obtain a copy of the License at | 
|  |  | 
|  | http://www.apache.org/licenses/LICENSE-2.0 | 
|  |  | 
|  | Unless required by applicable law or agreed to in writing, | 
|  | software distributed under the License is distributed on an | 
|  | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | 
|  | KIND, either express or implied.  See the License for the | 
|  | specific language governing permissions and limitations | 
|  | under the License. | 
|  | --> | 
|  |  | 
|  | <html> | 
|  | <head> | 
|  | <meta charset="utf-8"> | 
|  | <script src="lib/simpleRequire.js"></script> | 
|  | <script src="lib/config.js"></script> | 
|  | <script src="lib/jquery.min.js"></script> | 
|  | <script src="lib/testHelper.js"></script> | 
|  | <meta name="viewport" content="width=device-width, initial-scale=1" /> | 
|  | <link rel="stylesheet" href="lib/reset.css" /> | 
|  | </head> | 
|  | <body> | 
|  | <style> | 
|  | h1 { | 
|  | line-height: 60px; | 
|  | height: 60px; | 
|  | background: #ddd; | 
|  | text-align: center; | 
|  | font-weight: bold; | 
|  | font-size: 14px; | 
|  | } | 
|  | .test-chart { | 
|  | height: 500px; | 
|  | margin: 10px auto; | 
|  | } | 
|  | </style> | 
|  |  | 
|  |  | 
|  |  | 
|  | <h1>Hexagonal Binning</h1> | 
|  | <div class="chart" id="hexagonal-binning"></div> | 
|  |  | 
|  |  | 
|  |  | 
|  |  | 
|  | <script> | 
|  | // Hexbin statistics code based on [d3-hexbin](https://github.com/d3/d3-hexbin) | 
|  | function hexBinStatistics(points, r) { | 
|  | var dx = r * 2 * Math.sin(Math.PI / 3) | 
|  | var dy = r * 1.5; | 
|  | var binsById = {}; | 
|  | var bins = []; | 
|  |  | 
|  | for (var i = 0, n = points.length; i < n; ++i) { | 
|  | var point = points[i]; | 
|  | var px = point[0]; | 
|  | var py = point[1]; | 
|  |  | 
|  | if (isNaN(px) || isNaN(py)) { | 
|  | continue; | 
|  | } | 
|  |  | 
|  | var pj = Math.round(py = py / dy); | 
|  | var pi = Math.round(px = px / dx - (pj & 1) / 2); | 
|  | var py1 = py - pj; | 
|  |  | 
|  | if (Math.abs(py1) * 3 > 1) { | 
|  | var px1 = px - pi; | 
|  | var pi2 = pi + (px < pi ? -1 : 1) / 2; | 
|  | var pj2 = pj + (py < pj ? -1 : 1); | 
|  | var px2 = px - pi2; | 
|  | var py2 = py - pj2; | 
|  | if (px1 * px1 + py1 * py1 > px2 * px2 + py2 * py2) { | 
|  | pi = pi2 + (pj & 1 ? 1 : -1) / 2; | 
|  | pj = pj2; | 
|  | } | 
|  | } | 
|  |  | 
|  | var id = pi + "-" + pj; | 
|  | var bin = binsById[id]; | 
|  | if (bin) { | 
|  | bin.points.push(point); | 
|  | } | 
|  | else { | 
|  | bins.push(bin = binsById[id] = {points: [point]}); | 
|  | bin.x = (pi + (pj & 1) / 2) * dx; | 
|  | bin.y = pj * dy; | 
|  | } | 
|  | } | 
|  |  | 
|  | var maxBinLen = -Infinity | 
|  | for (var i = 0; i < bins.length; i++) { | 
|  | maxBinLen = Math.max(maxBinLen, bins.length); | 
|  | } | 
|  |  | 
|  | return { | 
|  | maxBinLen: maxBinLen, | 
|  | bins: bins | 
|  | }; | 
|  | } | 
|  |  | 
|  | </script> | 
|  |  | 
|  |  | 
|  | <script> | 
|  | require([ | 
|  | 'echarts', | 
|  | './data/kawhi-leonard-16-17-regular.json', | 
|  | './data/nba-court.json' | 
|  | ], function (echarts, shotData, nbaCourt) { | 
|  |  | 
|  | // 2006-2007 Regular Season | 
|  | echarts.registerMap('nbaCourt', nbaCourt.borderGeoJSON); | 
|  |  | 
|  | var backgroundColor = '#333'; | 
|  | var hexagonRadiusInGeo = 1; | 
|  |  | 
|  | var hexBinResult = hexBinStatistics( | 
|  | echarts.util.map(shotData.data, function (item) { | 
|  | // "shot_made_flag" made missed | 
|  | var made = item[echarts.util.indexOf(shotData.schema, 'shot_made_flag')]; | 
|  | return [ | 
|  | item[echarts.util.indexOf(shotData.schema, 'loc_x')], | 
|  | item[echarts.util.indexOf(shotData.schema, 'loc_y')], | 
|  | made === 'made' ? 1 : 0 | 
|  | ]; | 
|  | }), | 
|  | hexagonRadiusInGeo | 
|  | ); | 
|  |  | 
|  | var data = echarts.util.map(hexBinResult.bins, function (bin) { | 
|  | var made = 0; | 
|  | echarts.util.each(bin.points, function (point) { | 
|  | made += point[2]; | 
|  | }); | 
|  | return [bin.x, bin.y, bin.points.length, (made / bin.points.length * 100).toFixed(2)]; | 
|  | }); | 
|  |  | 
|  | function createItemRenderer(type) { | 
|  | type = type || 'polygon'; | 
|  | return function renderItemHexBin(params, api) { | 
|  | var center = api.coord([api.value(0), api.value(1)]); | 
|  | var points = []; | 
|  | var pointsBG = []; | 
|  |  | 
|  | var maxViewRadius = api.size([hexagonRadiusInGeo, 0])[0]; | 
|  | var minViewRadius = Math.min(maxViewRadius, 4); | 
|  | var extentMax = Math.log(Math.sqrt(hexBinResult.maxBinLen)); | 
|  | var viewRadius = echarts.number.linearMap( | 
|  | Math.log(Math.sqrt(api.value(2))), | 
|  | [0, extentMax], | 
|  | [minViewRadius, maxViewRadius] | 
|  | ); | 
|  |  | 
|  | var angle = Math.PI / 6; | 
|  | for (var i = 0; i < 6; i++, angle += Math.PI / 3) { | 
|  | points.push([ | 
|  | center[0] + viewRadius * Math.cos(angle), | 
|  | center[1] + viewRadius * Math.sin(angle) | 
|  | ]); | 
|  | pointsBG.push([ | 
|  | center[0] + maxViewRadius * Math.cos(angle), | 
|  | center[1] + maxViewRadius * Math.sin(angle) | 
|  | ]); | 
|  | } | 
|  |  | 
|  | return { | 
|  | type: 'group', | 
|  | children: [{ | 
|  | type, | 
|  | shape: type === 'polygon' ? { | 
|  | points: points | 
|  | } : { | 
|  | // Circle | 
|  | cx: center[0], | 
|  | cy: center[1], | 
|  | r: viewRadius | 
|  | }, | 
|  | style: { | 
|  | stroke: '#ccc', | 
|  | fill: api.visual('color'), | 
|  | lineWidth: 0 | 
|  | } | 
|  | }, { | 
|  | type, | 
|  | shape: type === 'polygon' ? { | 
|  | points: pointsBG | 
|  | } : { | 
|  | // Circle | 
|  | cx: center[0], | 
|  | cy: center[1], | 
|  | r: maxViewRadius | 
|  | }, | 
|  | style: { | 
|  | stroke: null, | 
|  | fill: 'rgba(0,0,0,0.5)', | 
|  | lineWidth: 0 | 
|  | }, | 
|  | z2: -19 | 
|  | }] | 
|  | }; | 
|  | }; | 
|  | } | 
|  |  | 
|  | function renderItemNBACourt(param, api) { | 
|  | return { | 
|  | type: 'group', | 
|  | children: echarts.util.map(nbaCourt.geometry, function (item) { | 
|  | return { | 
|  | type: item.type, | 
|  | style: { | 
|  | stroke: '#aaa', | 
|  | fill: null, | 
|  | lineWidth: 1.5 | 
|  | }, | 
|  | shape: { | 
|  | points: echarts.util.map(item.points, api.coord) | 
|  | } | 
|  | }; | 
|  | }) | 
|  | }; | 
|  | } | 
|  |  | 
|  | var option = { | 
|  | backgroundColor: backgroundColor, | 
|  | aria: { | 
|  | show: true | 
|  | }, | 
|  | tooltip: { | 
|  | backgroundColor: 'rgba(255,255,255,0.9)', | 
|  | textStyle: { | 
|  | color: '#333' | 
|  | } | 
|  | }, | 
|  | title: { | 
|  | text: 'Kawhi Leonard', | 
|  | subtext: '2016-2017 Regular Season', | 
|  | backgroundColor: backgroundColor, | 
|  | top: 10, | 
|  | left: 10, | 
|  | textStyle: { | 
|  | color: '#eee' | 
|  | } | 
|  | }, | 
|  | legend: { | 
|  | data: ['bar', 'error'] | 
|  | }, | 
|  | geo: { | 
|  | left: 0, | 
|  | right: 0, | 
|  | top: 0, | 
|  | bottom: 0, | 
|  | roam: true, | 
|  | silent: true, | 
|  | itemStyle: { | 
|  | normal: { | 
|  | color: backgroundColor, | 
|  | borderWidth: 0 | 
|  | } | 
|  | }, | 
|  | map: 'nbaCourt' | 
|  | }, | 
|  | visualMap: { | 
|  | type: 'continuous', | 
|  | orient: 'horizontal', | 
|  | right: 30, | 
|  | top: 40, | 
|  | min: 0, | 
|  | max: 100, | 
|  | align: 'bottom', | 
|  | text: [null, 'FG:   '], | 
|  | dimension: 3, | 
|  | seriesIndex: 0, | 
|  | calculable: true, | 
|  | textStyle: { | 
|  | color: '#eee' | 
|  | }, | 
|  | formatter: '{value} %', | 
|  | inRange: { | 
|  | // color: ['rgba(241,222,158, 0.3)', 'rgba(241,222,158, 1)'] | 
|  | color: ['green', 'yellow'] | 
|  | } | 
|  | }, | 
|  | series: [{ | 
|  | type: 'custom', | 
|  | coordinateSystem: 'geo', | 
|  | geoIndex: 0, | 
|  | renderItem: createItemRenderer(), | 
|  | dimensions: [null, null, 'Field Goals Attempted (hexagon size)', 'Field Goal Percentage (color)'], | 
|  | encode: { | 
|  | tooltip: [2, 3] | 
|  | }, | 
|  | data: data | 
|  | }, { | 
|  | coordinateSystem: 'geo', | 
|  | type: 'custom', | 
|  | geoIndex: 0, | 
|  | renderItem: renderItemNBACourt, | 
|  | silent: true, | 
|  | data: [0] | 
|  | }] | 
|  | }; | 
|  |  | 
|  | // var width = 1000; | 
|  | const myChart = testHelper.create(echarts, 'hexagonal-binning', { | 
|  | option, | 
|  | buttons: [{ | 
|  | text: 'Hexgon', | 
|  | onClick: function() { | 
|  | myChart.setOption({ | 
|  | series: [{ | 
|  | type: 'custom', | 
|  | universalTransition: { | 
|  | enabled: true | 
|  | }, | 
|  | renderItem: createItemRenderer('polygon') | 
|  | }] | 
|  | }); | 
|  | } | 
|  | }, { | 
|  | text: 'Circle', | 
|  | onClick: function() { | 
|  | myChart.setOption({ | 
|  | series: [{ | 
|  | type: 'custom', | 
|  | universalTransition: { | 
|  | enabled: true | 
|  | }, | 
|  | renderItem: createItemRenderer('circle') | 
|  | }] | 
|  | }); | 
|  | } | 
|  | }] | 
|  | // width: width, | 
|  | // height: width * nbaCourt.height / nbaCourt.width | 
|  | }); | 
|  | }); | 
|  | </script> | 
|  |  | 
|  |  | 
|  |  | 
|  |  | 
|  |  | 
|  |  | 
|  |  | 
|  |  | 
|  | </body> | 
|  | </html> |