| <!-- | |
| 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> |