├── .eslintignore ├── .eslintrc.json ├── .github └── workflows │ └── nodejs.yml ├── .gitignore ├── LICENSE.md ├── README.md ├── bower.json ├── dist ├── ChessDataViz.css ├── ChessDataViz.js ├── ChessDataViz.min.css ├── ChessDataViz.min.css.map └── ChessDataViz.min.js ├── gulpfile.js ├── ideas.md ├── npm-shrinkwrap.json ├── package.json ├── scripts ├── README.md ├── lib │ ├── Heatmaps.js │ ├── Moves.js │ └── Openings.js ├── scraper.js └── stats.js ├── src ├── js │ ├── ChessDataViz.js │ ├── EvalAndTime.js │ ├── HeatMap.js │ ├── MovePaths.js │ ├── Openings.js │ └── util.js └── less │ ├── ChessDataViz.less │ ├── EvalAndTime.less │ ├── HeatMap.less │ ├── MovePaths.less │ └── Openings.less └── test └── index.html /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | data/ -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "indent": [ 4 | 2, 5 | "tab" 6 | ], 7 | "quotes": [ 8 | 2, 9 | "single" 10 | ], 11 | "linebreak-style": 0, 12 | "semi": [ 13 | 2, 14 | "always" 15 | ] 16 | }, 17 | "env": { 18 | "es6": true, 19 | "node": true, 20 | "browser": true 21 | }, 22 | "extends": "eslint:recommended", 23 | "ecmaFeatures": { 24 | "modules": true 25 | } 26 | } -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [10.x, 12.x] 20 | 21 | steps: 22 | - uses: actions/checkout@v2 23 | - name: Use Node.js ${{ matrix.node-version }} 24 | uses: actions/setup-node@v1 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | - run: npm ci 28 | - run: npm run build --if-present 29 | - run: npm test 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | data/ 43 | img/ 44 | jspm_packages/ 45 | 46 | # Snowpack dependency directory (https://snowpack.dev/) 47 | web_modules/ 48 | 49 | # TypeScript cache 50 | *.tsbuildinfo 51 | 52 | # Optional npm cache directory 53 | .npm 54 | 55 | # Optional eslint cache 56 | .eslintcache 57 | 58 | # Microbundle cache 59 | .rpt2_cache/ 60 | .rts2_cache_cjs/ 61 | .rts2_cache_es/ 62 | .rts2_cache_umd/ 63 | 64 | # Optional REPL history 65 | .node_repl_history 66 | 67 | # Output of 'npm pack' 68 | *.tgz 69 | 70 | # Yarn Integrity file 71 | .yarn-integrity 72 | 73 | # dotenv environment variables file 74 | .env 75 | .env.test 76 | 77 | # parcel-bundler cache (https://parceljs.org/) 78 | .cache 79 | .parcel-cache 80 | 81 | # Next.js build output 82 | .next 83 | 84 | # Nuxt.js build / generate output 85 | .nuxt 86 | dist 87 | 88 | # Gatsby files 89 | .cache/ 90 | # Comment in the public line in if your project uses Gatsby and not Next.js 91 | # https://nextjs.org/blog/next-9-1#public-directory-support 92 | # public 93 | 94 | # vuepress build output 95 | .vuepress/dist 96 | 97 | # Serverless directories 98 | .serverless/ 99 | 100 | # FuseBox cache 101 | .fusebox/ 102 | 103 | # DynamoDB Local files 104 | .dynamodb/ 105 | 106 | # TernJS port file 107 | .tern-port 108 | 109 | # Stores VSCode versions used for testing VSCode extensions 110 | .vscode-test 111 | 112 | # yarn v2 113 | 114 | .yarn/cache 115 | .yarn/unplugged 116 | .yarn/build-state.yml 117 | .pnp.* 118 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2020 Buğra Fırat 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | chess-dataviz 2 | ============= 3 | 4 | chess-dataviz is a visualization library for chess, written for D3. It has a number of visualizations to show statistics on games, such as: 5 | 6 | * Heatmaps on a chess board 7 | * Openings, sunburst visualization 8 | * Move Paths on a chess board 9 | * Evaluation & Time graph 10 | 11 | There are also some scripts in `scripts/` to help parse PGN files (supports *big* files) and scraping game/tournament data from online sources. See `scripts/README.md` for more info on that. 12 | 13 | # Install 14 | 15 | You can download the files in `dist/` and use them directly, or install via bower: 16 | `bower install chess-dataviz` 17 | 18 | You should include `ChessDataViz[.min].js` and `ChessDataViz[.min].css` files. Also make sure you have already included D3 on the page. 19 | 20 | # Use / Demo 21 | 22 | Please see http://ebemunk.github.io/chess-dataviz/ for docs, usage instructions and demos. 23 | 24 | # License 25 | MIT -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chess-dataviz", 3 | "description": "chess visualization library written for d3.js", 4 | "main": [ 5 | "dist/ChessDataViz.min.js", 6 | "dist/ChessDataViz.min.css" 7 | ], 8 | "authors": [ 9 | "ebemunk " 10 | ], 11 | "license": "MIT", 12 | "keywords": [ 13 | "chess", 14 | "data", 15 | "visualization", 16 | "chessboard", 17 | "d3" 18 | ], 19 | "homepage": "https://github.com/ebemunk/chess-dataviz", 20 | "moduleType": [ 21 | "es6", 22 | "globals" 23 | ], 24 | "ignore": [ 25 | "**/.*", 26 | "node_modules", 27 | "bower_components", 28 | "test", 29 | "tests" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /dist/ChessDataViz.css: -------------------------------------------------------------------------------- 1 | .cdv-eval-time .graph { 2 | display: table-cell; 3 | vertical-align: middle; 4 | font-weight: 300; 5 | } 6 | .cdv-eval-time .axis { 7 | font-size: 12px; 8 | font-weight: 400; 9 | } 10 | .cdv-eval-time .axis path, 11 | .cdv-eval-time .axis line { 12 | fill: none; 13 | stroke: black; 14 | } 15 | .cdv-eval-time .axis .axis-label { 16 | fill: #777; 17 | } 18 | .cdv-eval-time .bar { 19 | opacity: 0.9; 20 | } 21 | .cdv-eval-time .bar.white { 22 | fill: white; 23 | } 24 | .cdv-eval-time .line { 25 | fill: none; 26 | stroke: black; 27 | stroke-width: 1.5px; 28 | } 29 | .cdv-eval-time .line.white { 30 | stroke: white; 31 | } 32 | .cdv-eval-time .area { 33 | fill: black; 34 | opacity: 0.65; 35 | } 36 | .cdv-eval-time .area.white { 37 | fill: white; 38 | } 39 | .cdv-eval-time .eval-guides { 40 | opacity: 0.75; 41 | } 42 | .cdv-eval-time .eval-guide-line { 43 | stroke: black; 44 | fill: none; 45 | stroke-dasharray: 9 4; 46 | stroke-width: 0.5px; 47 | } 48 | .cdv-eval-time .eval-guide-text { 49 | font-size: 10px; 50 | text-transform: uppercase; 51 | } 52 | .cdv-eval-time .interactive-layer { 53 | fill: none; 54 | } 55 | .cdv-eval-time .interactive-layer .guide { 56 | stroke: red; 57 | stroke-width: 1px; 58 | stroke-dasharray: 5 1; 59 | } 60 | .cdv-eval-time .interactive-layer .guide.hidden { 61 | display: none; 62 | } 63 | .cdv-eval-time.wrap { 64 | display: table; 65 | margin: 0 auto; 66 | text-align: center; 67 | } 68 | .cdv-heatmap .white rect { 69 | fill: white; 70 | } 71 | .cdv-heatmap .white .label { 72 | fill: black; 73 | } 74 | .cdv-heatmap .black rect { 75 | fill: black; 76 | } 77 | .cdv-heatmap .black .label { 78 | fill: white; 79 | } 80 | .cdv-heatmap .label { 81 | text-transform: lowercase; 82 | font-family: sans-serif; 83 | font-size: 12px; 84 | } 85 | .cdv-heatmap .heat-square { 86 | fill: red; 87 | opacity: 0.8; 88 | } 89 | .cdv-openings .arc { 90 | stroke: #fff; 91 | stroke-width: 0.5; 92 | } 93 | .cdv-openings .san { 94 | fill: #fff; 95 | font-size: 12px; 96 | pointer-events: none; 97 | } 98 | .cdv-move-paths .white rect { 99 | fill: white; 100 | } 101 | .cdv-move-paths .white .label { 102 | fill: black; 103 | } 104 | .cdv-move-paths .black rect { 105 | fill: black; 106 | } 107 | .cdv-move-paths .black .label { 108 | fill: white; 109 | } 110 | .cdv-move-paths .label { 111 | text-transform: lowercase; 112 | font-family: sans-serif; 113 | font-size: 12px; 114 | } 115 | .cdv-move-paths .move-path { 116 | fill: transparent; 117 | stroke: white; 118 | stroke-width: 1px; 119 | opacity: 0.1; 120 | } 121 | -------------------------------------------------------------------------------- /dist/ChessDataViz.min.css: -------------------------------------------------------------------------------- 1 | .cdv-eval-time .graph{display:table-cell;vertical-align:middle;font-weight:300}.cdv-eval-time .axis{font-size:12px;font-weight:400}.cdv-eval-time .axis path,.cdv-eval-time .axis line{fill:none;stroke:black}.cdv-eval-time .axis .axis-label{fill:#777}.cdv-eval-time .bar{opacity:.9}.cdv-eval-time .bar.white{fill:white}.cdv-eval-time .line{fill:none;stroke:black;stroke-width:1.5px}.cdv-eval-time .line.white{stroke:white}.cdv-eval-time .area{fill:black;opacity:.65}.cdv-eval-time .area.white{fill:white}.cdv-eval-time .eval-guides{opacity:.75}.cdv-eval-time .eval-guide-line{stroke:black;fill:none;stroke-dasharray:9 4;stroke-width:.5px}.cdv-eval-time .eval-guide-text{font-size:10px;text-transform:uppercase}.cdv-eval-time .interactive-layer{fill:none}.cdv-eval-time .interactive-layer .guide{stroke:red;stroke-width:1px;stroke-dasharray:5 1}.cdv-eval-time .interactive-layer .guide.hidden{display:none}.cdv-eval-time.wrap{display:table;margin:0 auto;text-align:center}.cdv-heatmap .white rect{fill:white}.cdv-heatmap .white .label{fill:black}.cdv-heatmap .black rect{fill:black}.cdv-heatmap .black .label{fill:white}.cdv-heatmap .label{text-transform:lowercase;font-family:sans-serif;font-size:12px}.cdv-heatmap .heat-square{fill:red;opacity:.8}.cdv-openings .arc{stroke:#fff;stroke-width:.5}.cdv-openings .san{fill:#fff;font-size:12px;pointer-events:none}.cdv-move-paths .white rect{fill:white}.cdv-move-paths .white .label{fill:black}.cdv-move-paths .black rect{fill:black}.cdv-move-paths .black .label{fill:white}.cdv-move-paths .label{text-transform:lowercase;font-family:sans-serif;font-size:12px}.cdv-move-paths .move-path{fill:transparent;stroke:white;stroke-width:1px;opacity:.1} 2 | /*# sourceMappingURL=ChessDataViz.min.css.map */ 3 | -------------------------------------------------------------------------------- /dist/ChessDataViz.min.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["ChessDataViz.less","EvalAndTime.less","ChessDataViz.css","HeatMap.less","Openings.less","MovePaths.less"],"names":[],"mappings":"AAAA,sBCCC,mBAAA,AACA,sBAAA,AACA,eAAA,CCCA,AFJD,qBCOC,eAAA,AACA,eAAA,CCAA,AFRD,oDCWE,UAAA,AACA,YAAA,CCCD,AFbD,iCCgBE,SAAA,CCAD,AFhBD,oBCqBC,UAAA,CCFA,ADIA,0BACC,UAAA,CCFD,AFtBD,qBC6BC,UAAA,AACA,aAAA,AACA,kBAAA,CCJA,ADMA,2BACC,YAAA,CCJD,AF9BD,qBCsCC,WAAA,AACA,WAAA,CCLA,ADOA,2BACC,UAAA,CCLD,AFrCD,4BC+CC,WAAA,CCPA,AFxCD,gCCkDC,aAAA,AACA,UAAA,AACA,qBAAA,AACA,iBAAA,CCPA,AF9CD,gCCwDC,eAAA,AACA,wBAAA,CCPA,AFlDD,kCC6DC,SAAA,CCRA,AFrDD,yCCgEE,WAAA,AACA,iBAAA,AACA,oBAAA,CCRD,ADUC,gDACC,YAAA,CCRF,AF1DA,oBACC,cAAA,AACA,cAAA,AACA,iBAAA,CE4DD,AFxDD,yBGRE,UAAA,CDmED,AF3DD,2BGJE,UAAA,CDkED,AF9DD,yBGEE,UAAA,CD+DD,AFjED,2BGME,UAAA,CD8DD,AFpED,oBGWC,yBAAA,AACA,uBAAA,AACA,cAAA,CD4DA,AFzED,0BGiBC,SAAA,AACA,UAAA,CD2DA,AFzED,mBIbC,YAAA,AACA,eAAA,CFyFA,AF7ED,mBIRC,UAAA,AACA,eAAA,AACA,mBAAA,CFwFA,AF9ED,4BKhBE,UAAA,CHiGD,AFjFD,8BKZE,UAAA,CHgGD,AFpFD,4BKNE,UAAA,CH6FD,AFvFD,8BKFE,UAAA,CH4FD,AF1FD,uBKGC,yBAAA,AACA,uBAAA,AACA,cAAA,CH0FA,AF/FD,2BKSC,iBAAA,AACA,aAAA,AACA,iBAAA,AACA,UAAA,CHyFA","file":"ChessDataViz.min.css","sourcesContent":[".cdv-eval-time {\n\t@import 'EvalAndTime';\n\n\t&.wrap {\n\t\tdisplay: table;\n\t\tmargin: 0 auto;\n\t\ttext-align: center;\n\t}\n}\n\n.cdv-heatmap {\n\t@import 'HeatMap';\n}\n\n.cdv-openings {\n\t@import 'Openings';\n}\n\n.cdv-move-paths {\n\t@import 'MovePaths';\n}",".graph {\n\tdisplay: table-cell;\n\tvertical-align: middle;\n\tfont-weight: 300;\n}\n\n.axis {\n\tfont-size: 12px;\n\tfont-weight: 400;\n\n\tpath, line {\n\t\tfill: none;\n\t\tstroke: black;\n\t}\n\n\t.axis-label {\n\t\tfill: #777;\n\t}\n}\n\n.bar {\n\topacity: 0.9;\n\n\t&.white {\n\t\tfill: white;\n\t}\n}\n\n.line {\n\tfill: none;\n\tstroke: black;\n\tstroke-width: 1.5px;\n\n\t&.white {\n\t\tstroke: white;\n\t}\n}\n.area {\n\tfill: black;\n\topacity: 0.65;\n\n\t&.white {\n\t\tfill: white;\n\t}\n}\n\n.eval-guides {\n\topacity: 0.75;\n}\n.eval-guide-line {\n\tstroke: black;\n\tfill: none;\n\tstroke-dasharray: 9 4;\n\tstroke-width: 0.5px;\n}\n.eval-guide-text {\n\tfont-size: 10px;\n\ttext-transform: uppercase;\n}\n\n.interactive-layer {\n\tfill: none;\n\n\t.guide {\n\t\tstroke: red;\n\t\tstroke-width: 1px;\n\t\tstroke-dasharray: 5 1;\n\n\t\t&.hidden {\n\t\t\tdisplay: none;\n\t\t}\n\t}\n}",".cdv-eval-time .graph {\n display: table-cell;\n vertical-align: middle;\n font-weight: 300;\n}\n.cdv-eval-time .axis {\n font-size: 12px;\n font-weight: 400;\n}\n.cdv-eval-time .axis path,\n.cdv-eval-time .axis line {\n fill: none;\n stroke: black;\n}\n.cdv-eval-time .axis .axis-label {\n fill: #777;\n}\n.cdv-eval-time .bar {\n opacity: 0.9;\n}\n.cdv-eval-time .bar.white {\n fill: white;\n}\n.cdv-eval-time .line {\n fill: none;\n stroke: black;\n stroke-width: 1.5px;\n}\n.cdv-eval-time .line.white {\n stroke: white;\n}\n.cdv-eval-time .area {\n fill: black;\n opacity: 0.65;\n}\n.cdv-eval-time .area.white {\n fill: white;\n}\n.cdv-eval-time .eval-guides {\n opacity: 0.75;\n}\n.cdv-eval-time .eval-guide-line {\n stroke: black;\n fill: none;\n stroke-dasharray: 9 4;\n stroke-width: 0.5px;\n}\n.cdv-eval-time .eval-guide-text {\n font-size: 10px;\n text-transform: uppercase;\n}\n.cdv-eval-time .interactive-layer {\n fill: none;\n}\n.cdv-eval-time .interactive-layer .guide {\n stroke: red;\n stroke-width: 1px;\n stroke-dasharray: 5 1;\n}\n.cdv-eval-time .interactive-layer .guide.hidden {\n display: none;\n}\n.cdv-eval-time.wrap {\n display: table;\n margin: 0 auto;\n text-align: center;\n}\n.cdv-heatmap .white rect {\n fill: white;\n}\n.cdv-heatmap .white .label {\n fill: black;\n}\n.cdv-heatmap .black rect {\n fill: black;\n}\n.cdv-heatmap .black .label {\n fill: white;\n}\n.cdv-heatmap .label {\n text-transform: lowercase;\n font-family: sans-serif;\n font-size: 12px;\n}\n.cdv-heatmap .heat-square {\n fill: red;\n opacity: 0.8;\n}\n.cdv-openings .arc {\n stroke: #fff;\n stroke-width: 0.5;\n}\n.cdv-openings .san {\n fill: #fff;\n font-size: 12px;\n pointer-events: none;\n}\n.cdv-move-paths .white rect {\n fill: white;\n}\n.cdv-move-paths .white .label {\n fill: black;\n}\n.cdv-move-paths .black rect {\n fill: black;\n}\n.cdv-move-paths .black .label {\n fill: white;\n}\n.cdv-move-paths .label {\n text-transform: lowercase;\n font-family: sans-serif;\n font-size: 12px;\n}\n.cdv-move-paths .move-path {\n fill: transparent;\n stroke: white;\n stroke-width: 1px;\n opacity: 0.1;\n}\n",".white {\n\trect {\n\t\tfill: white;\n\t}\n\n\t.label {\n\t\tfill: black;\n\t}\n}\n\n.black {\n\trect {\n\t\tfill: black;\n\t}\n\n\t.label {\n\t\tfill: white;\n\t}\n}\n\n.label {\n\ttext-transform: lowercase;\n\tfont-family: sans-serif;\n\tfont-size: 12px;\n}\n\n.heat-square {\n\tfill: red;\n\topacity: 0.8;\n}",".arc {\n\tstroke: #fff;\n\tstroke-width: 0.5;\n}\n\n.san {\n\tfill: #fff;\n\tfont-size: 12px;\n\tpointer-events: none;\n}",".white {\n\trect {\n\t\tfill: white;\n\t}\n\n\t.label {\n\t\tfill: black;\n\t}\n}\n\n.black {\n\trect {\n\t\tfill: black;\n\t}\n\n\t.label {\n\t\tfill: white;\n\t}\n}\n\n.label {\n\ttext-transform: lowercase;\n\tfont-family: sans-serif;\n\tfont-size: 12px;\n}\n\n.move-path {\n\tfill: transparent;\n\tstroke: white;\n\tstroke-width: 1px;\n\topacity: 0.1;\n}"],"sourceRoot":"/source/"} -------------------------------------------------------------------------------- /dist/ChessDataViz.min.js: -------------------------------------------------------------------------------- 1 | !function t(n,r,e){function i(o,u){if(!r[o]){if(!n[o]){var c="function"==typeof require&&require;if(!u&&c)return c(o,!0);if(a)return a(o,!0);var s=new Error("Cannot find module '"+o+"'");throw s.code="MODULE_NOT_FOUND",s}var l=r[o]={exports:{}};n[o][0].call(l.exports,function(t){var r=n[o][1][t];return i(r?r:t)},l,l.exports,t,n,r,e)}return r[o].exports}for(var a="function"==typeof require&&require,o=0;o=31}function i(){var t=arguments,n=this.useColors;if(t[0]=(n?"%c":"")+this.namespace+(n?" %c":" ")+t[0]+(n?"%c ":" ")+"+"+r.humanize(this.diff),!n)return t;var e="color: "+this.color;t=[t[0],e,"color: inherit"].concat(Array.prototype.slice.call(t,1));var i=0,a=0;return t[0].replace(/%[a-z%]/g,function(t){"%%"!==t&&(i++,"%c"===t&&(a=i))}),t.splice(a,0,e),t}function a(){return"object"==typeof console&&console.log&&Function.prototype.apply.call(console.log,console,arguments)}function o(t){try{null==t?r.storage.removeItem("debug"):r.storage.debug=t}catch(n){}}function u(){var t;try{t=r.storage.debug}catch(n){}return t}function c(){try{return window.localStorage}catch(t){}}r=n.exports=t("./debug"),r.log=a,r.formatArgs=i,r.save=o,r.load=u,r.useColors=e,r.storage="undefined"!=typeof chrome&&"undefined"!=typeof chrome.storage?chrome.storage.local:c(),r.colors=["lightseagreen","forestgreen","goldenrod","dodgerblue","darkorchid","crimson"],r.formatters.j=function(t){return JSON.stringify(t)},r.enable(u())},{"./debug":2}],2:[function(t,n,r){function e(){return r.colors[l++%r.colors.length]}function i(t){function n(){}function i(){var t=i,n=+new Date,a=n-(s||n);t.diff=a,t.prev=s,t.curr=n,s=n,null==t.useColors&&(t.useColors=r.useColors()),null==t.color&&t.useColors&&(t.color=e());var o=Array.prototype.slice.call(arguments);o[0]=r.coerce(o[0]),"string"!=typeof o[0]&&(o=["%o"].concat(o));var u=0;o[0]=o[0].replace(/%([a-z%])/g,function(n,e){if("%%"===n)return n;u++;var i=r.formatters[e];if("function"==typeof i){var a=o[u];n=i.call(t,a),o.splice(u,1),u--}return n}),"function"==typeof r.formatArgs&&(o=r.formatArgs.apply(t,o));var c=i.log||r.log||console.log.bind(console);c.apply(t,o)}n.enabled=!1,i.enabled=!0;var a=r.enabled(t)?i:n;return a.namespace=t,a}function a(t){r.save(t);for(var n=(t||"").split(/[\s,]+/),e=n.length,i=0;e>i;i++)n[i]&&(t=n[i].replace(/\*/g,".*?"),"-"===t[0]?r.skips.push(new RegExp("^"+t.substr(1)+"$")):r.names.push(new RegExp("^"+t+"$")))}function o(){r.enable("")}function u(t){var n,e;for(n=0,e=r.skips.length;e>n;n++)if(r.skips[n].test(t))return!1;for(n=0,e=r.names.length;e>n;n++)if(r.names[n].test(t))return!0;return!1}function c(t){return t instanceof Error?t.stack||t.message:t}r=n.exports=i,r.coerce=c,r.disable=o,r.enable=a,r.enabled=u,r.humanize=t("ms"),r.names=[],r.skips=[],r.formatters={};var s,l=0},{ms:4}],3:[function(t,n,r){(function(t){(function(){function e(t,n){if(t!==n){var r=null===t,e=t===S,i=t===t,a=null===n,o=n===S,u=n===n;if(t>n&&!a||!i||r&&!o&&u||e&&u)return 1;if(n>t&&!r||!u||a&&!e&&i||o&&i)return-1}return 0}function i(t,n,r){for(var e=t.length,i=r?e:-1;r?i--:++i-1;);return r}function s(t,n){for(var r=t.length;r--&&n.indexOf(t.charAt(r))>-1;);return r}function l(t,n){return e(t.criteria,n.criteria)||t.index-n.index}function f(t,n,r){for(var i=-1,a=t.criteria,o=n.criteria,u=a.length,c=r.length;++i=c)return s;var l=r[i];return s*("asc"===l||l===!0?1:-1)}}return t.index-n.index}function h(t){return Dt[t]}function p(t){return Vt[t]}function d(t,n,r){return n?t=Yt[t]:r&&(t=Gt[t]),"\\"+t}function v(t){return"\\"+Gt[t]}function _(t,n,r){for(var e=t.length,i=n+(r?0:-1);r?i--:++i=t&&t>=9&&13>=t||32==t||160==t||5760==t||6158==t||t>=8192&&(8202>=t||8232==t||8233==t||8239==t||8287==t||12288==t||65279==t)}function m(t,n){for(var r=-1,e=t.length,i=-1,a=[];++rn,i=r?t.length:0,a=Hr(0,i,this.__views__),o=a.start,u=a.end,c=u-o,s=e?u:o-1,l=this.__iteratees__,f=l.length,h=0,p=Ao(c,this.__takeCount__);if(!r||U>i||i==c&&p==c)return er(e&&r?t.reverse():t,this.__actions__);var d=[];t:for(;c--&&p>h;){s+=n;for(var v=-1,_=t[s];++v=U?vr(n):null,s=n.length;c&&(o=Qt,u=!1,n=c);t:for(;++ir&&(r=-r>i?0:i+r),e=e===S||e>i?i:+e||0,0>e&&(e+=i),i=r>e?0:e>>>0,r>>>=0;i>r;)t[r++]=n;return t}function On(t,n){var r=[];return zo(t,function(t,e,i){n(t,e,i)&&r.push(t)}),r}function Mn(t,n,r,e){var i;return r(t,function(t,r,a){return n(t,r,a)?(i=e?r:t,!1):void 0}),i}function Cn(t,n,r,e){e||(e=[]);for(var i=-1,a=t.length;++ie;)t=t[n[e++]];return e&&e==i?t:S}}function zn(t,n,r,e,i,a){return t===n?!0:null==t||null==n||!Wi(t)&&!g(n)?t!==t&&n!==n:Pn(t,n,zn,r,e,i,a)}function Pn(t,n,r,e,i,a,o){var u=Mu(t),c=Mu(n),s=H,l=H;u||(s=ro.call(t),s==V?s=Z:s!=Z&&(u=Li(t))),c||(l=ro.call(n),l==V?l=Z:l!=Z&&(c=Li(n)));var f=s==Z,h=l==Z,p=s==l;if(p&&!u&&!f)return $r(t,n,s);if(!i){var d=f&&to.call(t,"__wrapped__"),v=h&&to.call(n,"__wrapped__");if(d||v)return r(d?t.value():t,v?n.value():n,e,i,a,o)}if(!p)return!1;a||(a=[]),o||(o=[]);for(var _=a.length;_--;)if(a[_]==t)return o[_]==n;a.push(t),o.push(n);var g=(u?Fr:Ur)(t,n,r,e,i,a,o);return a.pop(),o.pop(),g}function Fn(t,n,r){var e=n.length,i=e,a=!r;if(null==t)return!i;for(t=fe(t);e--;){var o=n[e];if(a&&o[2]?o[1]!==t[o[0]]:!(o[0]in t))return!1}for(;++en&&(n=-n>i?0:i+n),r=r===S||r>i?i:+r||0,0>r&&(r+=i),i=n>r?0:r-n>>>0,n>>>=0;for(var a=Ua(i);++e=U,c=u?vr():null,s=[];c?(e=Qt,o=!1):(u=!1,c=n?[]:s);t:for(;++r=i){for(;i>e;){var a=e+i>>>1,o=t[a];(r?n>=o:n>o)&&null!==o?e=a+1:i=a}return i}return ar(t,n,Ea,r)}function ar(t,n,r,e){n=r(n);for(var i=0,a=t?t.length:0,o=n!==n,u=null===n,c=n===S;a>i;){var s=yo((i+a)/2),l=r(t[s]),f=l!==S,h=l===l;if(o)var p=h||e;else p=u?h&&f&&(e||null!=l):c?h&&(e||f):null==l?!1:e?n>=l:n>l;p?i=s+1:a=s}return Ao(a,Co)}function or(t,n,r){if("function"!=typeof t)return Ea;if(n===S)return t;switch(r){case 1:return function(r){return t.call(n,r)};case 3:return function(r,e,i){return t.call(n,r,e,i)};case 4:return function(r,e,i,a){return t.call(n,r,e,i,a)};case 5:return function(r,e,i,a,o){return t.call(n,r,e,i,a,o)}}return function(){return t.apply(n,arguments)}}function ur(t){var n=new ao(t.byteLength),r=new po(n);return r.set(new po(t)),n}function cr(t,n,r){for(var e=r.length,i=-1,a=bo(t.length-e,0),o=-1,u=n.length,c=Ua(u+a);++o2?r[i-2]:S,o=i>2?r[2]:S,u=i>1?r[i-1]:S;for("function"==typeof a?(a=or(a,u,5),i-=2):(a="function"==typeof u?u:S,i-=a?1:0),o&&Zr(r[0],r[1],o)&&(a=3>i?S:a,i=1);++e-1?r[o]:S}return Mn(r,e,t)}}function br(t){return function(n,r,e){return n&&n.length?(r=Nr(r,e,3),i(n,r,t)):-1}}function Ar(t){return function(n,r,e){return r=Nr(r,e,3),Mn(n,r,t,!0)}}function kr(t){return function(){for(var n,r=arguments.length,e=t?r:-1,i=0,a=Ua(r);t?e--:++e=U)return n.plant(e).value();for(var i=0,o=r?a[i].apply(this,t):e;++iy){var k=u?tn(u):S,j=bo(s-y,0),M=d?A:S,C=d?S:A,R=d?w:S,W=d?S:w;n|=d?T:I,n&=~(d?I:T),v||(n&=~(E|O));var q=[t,n,r,R,M,W,C,k,c,j],z=Tr.apply(S,q);return ne(t)&&Lo(z,q),z.placeholder=b,z}}var P=h?r:this,F=p?P[t]:t;return u&&(w=ce(w,u)),f&&c=n||!xo(n))return"";var i=n-e;return r=null==r?" ":r+"",_a(r,_o(i/r.length)).slice(0,i)}function Wr(t,n,r,e){function i(){for(var n=-1,u=arguments.length,c=-1,s=e.length,l=Ua(s+u);++cc))return!1;for(;++u-1&&t%1==0&&n>t}function Zr(t,n,r){if(!Wi(r))return!1;var e=typeof n;if("number"==e?Qr(r)&&Xr(n,r.length):"string"==e&&n in r){var i=r[n];return t===t?t===i:i!==i}return!1}function te(t,n){var r=typeof t;if("string"==r&&St.test(t)||"number"==r)return!0;if(Mu(t))return!1;var e=!kt.test(t);return e||null!=n&&t in fe(n)}function ne(t){var r=Br(t);if(!(r in Q.prototype))return!1;var e=n[r];if(t===e)return!0;var i=No(e);return!!i&&t===i[0]}function re(t){return"number"==typeof t&&t>-1&&t%1==0&&To>=t}function ee(t){return t===t&&!Wi(t)}function ie(t,n){var r=t[1],e=n[1],i=r|e,a=W>i,o=e==W&&r==C||e==W&&r==q&&t[7].length<=n[8]||e==(W|q)&&r==C;if(!a&&!o)return t;e&E&&(t[2]=n[2],i|=r&E?0:M);var u=n[3];if(u){var c=t[3];t[3]=c?cr(c,u,n[4]):tn(u),t[4]=c?m(t[3],D):tn(n[4])}return u=n[5],u&&(c=t[5],t[5]=c?sr(c,u,n[6]):tn(u),t[6]=c?m(t[5],D):tn(n[6])),u=n[7],u&&(t[7]=tn(u)),e&W&&(t[8]=null==t[8]?n[8]:Ao(t[8],n[8])),null==t[9]&&(t[9]=n[9]),t[0]=n[0],t[1]=i,t}function ae(t,n){return t===S?n:Cu(t,n,ae)}function oe(t,n){t=fe(t);for(var r=-1,e=n.length,i={};++re;)o[++a]=Gn(t,e,e+=n);return o}function ve(t){for(var n=-1,r=t?t.length:0,e=-1,i=[];++nn?0:n)):[]}function ge(t,n,r){var e=t?t.length:0;return e?((r?Zr(t,n,r):null==n)&&(n=1),n=e-(+n||0),Gn(t,0,0>n?0:n)):[]}function ye(t,n,r){return t&&t.length?rr(t,Nr(n,r,3),!0,!0):[]}function me(t,n,r){return t&&t.length?rr(t,Nr(n,r,3),!0):[]}function xe(t,n,r,e){var i=t?t.length:0;return i?(r&&"number"!=typeof r&&Zr(t,n,r)&&(r=0,e=i),En(t,n,r,e)):[]}function we(t){return t?t[0]:S}function be(t,n,r){var e=t?t.length:0;return r&&Zr(t,n,r)&&(n=!1),e?Cn(t,n):[]}function Ae(t){var n=t?t.length:0;return n?Cn(t,!0):[]}function ke(t,n,r){var e=t?t.length:0;if(!e)return-1;if("number"==typeof r)r=0>r?bo(e+r,0):r;else if(r){var i=ir(t,n);return e>i&&(n===n?n===t[i]:t[i]!==t[i])?i:-1}return a(t,n,r||0)}function Se(t){return ge(t,1)}function je(t){var n=t?t.length:0;return n?t[n-1]:S}function Ee(t,n,r){var e=t?t.length:0;if(!e)return-1;var i=e;if("number"==typeof r)i=(0>r?bo(e+r,0):Ao(r||0,e-1))+1;else if(r){i=ir(t,n,!0)-1;var a=t[i];return(n===n?n===a:a!==a)?i:-1}if(n!==n)return _(t,i,!0);for(;i--;)if(t[i]===n)return i;return-1}function Oe(){var t=arguments,n=t[0];if(!n||!n.length)return n;for(var r=0,e=Lr(),i=t.length;++r-1;)ho.call(n,a,1);return n}function Me(t,n,r){var e=[];if(!t||!t.length)return e;var i=-1,a=[],o=t.length;for(n=Nr(n,r,3);++in?0:n)):[]}function Ie(t,n,r){var e=t?t.length:0;return e?((r?Zr(t,n,r):null==n)&&(n=1),n=e-(+n||0),Gn(t,0>n?0:n)):[]}function We(t,n,r){return t&&t.length?rr(t,Nr(n,r,3),!1,!0):[]}function qe(t,n,r){return t&&t.length?rr(t,Nr(n,r,3)):[]}function ze(t,n,r,e){var i=t?t.length:0;if(!i)return[];null!=n&&"boolean"!=typeof n&&(e=r,r=Zr(t,n,e)?S:n,n=!1);var o=Nr();return(null!=r||o!==wn)&&(r=o(r,e,3)),n&&Lr()==a?x(t,r):tr(t,r)}function Pe(t){if(!t||!t.length)return[];var n=-1,r=0;t=cn(t,function(t){return Qr(t)?(r=bo(t.length,r),!0):void 0});for(var e=Ua(r);++nr?bo(i+r,0):r||0,"string"==typeof t||!Mu(t)&&Bi(t)?i>=r&&t.indexOf(n,r)>-1:!!i&&Lr(t,n,r)>-1}function ti(t,n,r){var e=Mu(t)?sn:$n;return n=Nr(n,r,3),e(t,n)}function ni(t,n){return ti(t,Ia(n))}function ri(t,n,r){var e=Mu(t)?cn:On;return n=Nr(n,r,3),e(t,function(t,r,e){return!n(t,r,e)})}function ei(t,n,r){if(r?Zr(t,n,r):null==n){t=le(t);var e=t.length;return e>0?t[Kn(0,e-1)]:S}var i=-1,a=Ki(t),e=a.length,o=e-1;for(n=Ao(0>n?0:+n||0,e);++i0&&(r=n.apply(this,arguments)),1>=t&&(n=S),r}}function pi(t,n,r){function e(){p&&oo(p),s&&oo(s),v=0,s=p=d=S}function i(n,r){r&&oo(r),s=p=d=S,n&&(v=vu(),l=t.apply(h,c),p||s||(c=h=S))}function a(){var t=n-(vu()-f);0>=t||t>n?i(d,s):p=fo(a,t)}function o(){i(g,p)}function u(){if(c=arguments,f=vu(),h=this,d=g&&(p||!y),_===!1)var r=y&&!p;else{s||y||(v=f);var e=_-(f-v),i=0>=e||e>_;i?(s&&(s=oo(s)),v=f,l=t.apply(h,c)):s||(s=fo(o,e))}return i&&p?p=oo(p):p||n===_||(p=fo(a,n)),r&&(i=!0,l=t.apply(h,c)),!i||p||s||(c=h=S),l}var c,s,l,f,h,p,d,v=0,_=!1,g=!0;if("function"!=typeof t)throw new Ga(L);if(n=0>n?0:+n||0,r===!0){var y=!0;g=!1}else Wi(r)&&(y=!!r.leading,_="maxWait"in r&&bo(+r.maxWait||0,n),g="trailing"in r?!!r.trailing:g);return u.cancel=e,u}function di(t,n){if("function"!=typeof t||n&&"function"!=typeof n)throw new Ga(L);var r=function(){var e=arguments,i=n?n.apply(this,e):e[0],a=r.cache;if(a.has(i))return a.get(i);var o=t.apply(this,e);return r.cache=a.set(i,o),o};return r.cache=new di.Cache,r}function vi(t){if("function"!=typeof t)throw new Ga(L);return function(){return!t.apply(this,arguments)}}function _i(t){return hi(2,t)}function gi(t,n){if("function"!=typeof t)throw new Ga(L);return n=bo(n===S?t.length-1:+n||0,0),function(){for(var r=arguments,e=-1,i=bo(r.length-n,0),a=Ua(i);++en}function ki(t,n){return t>=n}function Si(t){return g(t)&&Qr(t)&&to.call(t,"callee")&&!so.call(t,"callee")}function ji(t){return t===!0||t===!1||g(t)&&ro.call(t)==K}function Ei(t){return g(t)&&ro.call(t)==Y}function Oi(t){return!!t&&1===t.nodeType&&g(t)&&!Ui(t)}function Mi(t){return null==t?!0:Qr(t)&&(Mu(t)||Bi(t)||Si(t)||g(t)&&Ii(t.splice))?!t.length:!Uu(t).length}function Ci(t,n,r,e){r="function"==typeof r?or(r,e,3):S;var i=r?r(t,n):S;return i===S?zn(t,n,r):!!i}function Ri(t){return g(t)&&"string"==typeof t.message&&ro.call(t)==G; 2 | }function Ti(t){return"number"==typeof t&&xo(t)}function Ii(t){return Wi(t)&&ro.call(t)==J}function Wi(t){var n=typeof t;return!!t&&("object"==n||"function"==n)}function qi(t,n,r,e){return r="function"==typeof r?or(r,e,3):S,Fn(t,Dr(n),r)}function zi(t){return $i(t)&&t!=+t}function Pi(t){return null==t?!1:Ii(t)?io.test(Za.call(t)):g(t)&&Wt.test(t)}function Fi(t){return null===t}function $i(t){return"number"==typeof t||g(t)&&ro.call(t)==X}function Ui(t){var n;if(!g(t)||ro.call(t)!=Z||Si(t)||!to.call(t,"constructor")&&(n=t.constructor,"function"==typeof n&&!(n instanceof n)))return!1;var r;return Rn(t,function(t,n){r=n}),r===S||to.call(t,r)}function Ni(t){return Wi(t)&&ro.call(t)==tt}function Bi(t){return"string"==typeof t||g(t)&&ro.call(t)==rt}function Li(t){return g(t)&&re(t.length)&&!!Bt[ro.call(t)]}function Di(t){return t===S}function Vi(t,n){return n>t}function Hi(t,n){return n>=t}function Ki(t){var n=t?Bo(t):0;return re(n)?n?tn(t):[]:aa(t)}function Yi(t){return xn(t,ta(t))}function Gi(t,n,r){var e=qo(t);return r&&Zr(t,n,r)&&(n=S),n?yn(e,n):e}function Ji(t){return Wn(t,ta(t))}function Qi(t,n,r){var e=null==t?S:qn(t,he(n),n+"");return e===S?r:e}function Xi(t,n){if(null==t)return!1;var r=to.call(t,n);if(!r&&!te(n)){if(n=he(n),t=1==n.length?t:qn(t,Gn(n,0,-1)),null==t)return!1;n=je(n),r=to.call(t,n)}return r||re(t.length)&&Xr(n,t.length)&&(Mu(t)||Si(t))}function Zi(t,n,r){r&&Zr(t,n,r)&&(n=S);for(var e=-1,i=Uu(t),a=i.length,o={};++e0;++e=Ao(n,r)&&tr?0:+r||0,e),r-=n.length,r>=0&&t.indexOf(n,r)==r}function ha(t){return t=u(t),t&&xt.test(t)?t.replace(yt,p):t}function pa(t){return t=u(t),t&&Ot.test(t)?t.replace(Et,d):t||"(?:)"}function da(t,n,r){t=u(t),n=+n;var e=t.length;if(e>=n||!xo(n))return t;var i=(n-e)/2,a=yo(i),o=_o(i);return r=Ir("",o,r),r.slice(0,a)+t+r}function va(t,n,r){return(r?Zr(t,n,r):null==n)?n=0:n&&(n=+n),t=ma(t),So(t,n||(It.test(t)?16:10))}function _a(t,n){var r="";if(t=u(t),n=+n,1>n||!t||!xo(n))return r;do n%2&&(r+=t),n=yo(n/2),t+=t;while(n);return r}function ga(t,n,r){return t=u(t),r=null==r?0:Ao(0>r?0:+r||0,t.length),t.lastIndexOf(n,r)==r}function ya(t,r,e){var i=n.templateSettings;e&&Zr(t,r,e)&&(r=e=S),t=u(t),r=gn(yn({},e||r),i,_n);var a,o,c=gn(yn({},r.imports),i.imports,_n),s=Uu(c),l=nr(c,s),f=0,h=r.interpolate||Pt,p="__p += '",d=Ka((r.escape||Pt).source+"|"+h.source+"|"+(h===At?Rt:Pt).source+"|"+(r.evaluate||Pt).source+"|$","g"),_="//# sourceURL="+("sourceURL"in r?r.sourceURL:"lodash.templateSources["+ ++Nt+"]")+"\n";t.replace(d,function(n,r,e,i,u,c){return e||(e=i),p+=t.slice(f,c).replace(Ft,v),r&&(a=!0,p+="' +\n__e("+r+") +\n'"),u&&(o=!0,p+="';\n"+u+";\n__p += '"),e&&(p+="' +\n((__t = ("+e+")) == null ? '' : __t) +\n'"),f=c+n.length,n}),p+="';\n";var g=r.variable;g||(p="with (obj) {\n"+p+"\n}\n"),p=(o?p.replace(dt,""):p).replace(vt,"$1").replace(_t,"$1;"),p="function("+(g||"obj")+") {\n"+(g?"":"obj || (obj = {});\n")+"var __t, __p = ''"+(a?", __e = _.escape":"")+(o?", __j = Array.prototype.join;\nfunction print() { __p += __j.call(arguments, '') }\n":";\n")+p+"return __p\n}";var y=Qu(function(){return La(s,_+"return "+p).apply(S,l)});if(y.source=p,Ri(y))throw y;return y}function ma(t,n,r){var e=t;return(t=u(t))?(r?Zr(e,n,r):null==n)?t.slice(w(t),b(t)+1):(n+="",t.slice(c(t,n),s(t,n)+1)):t}function xa(t,n,r){var e=t;return t=u(t),t?(r?Zr(e,n,r):null==n)?t.slice(w(t)):t.slice(c(t,n+"")):t}function wa(t,n,r){var e=t;return t=u(t),t?(r?Zr(e,n,r):null==n)?t.slice(0,b(t)+1):t.slice(0,s(t,n+"")+1):t}function ba(t,n,r){r&&Zr(t,n,r)&&(n=S);var e=z,i=P;if(null!=n)if(Wi(n)){var a="separator"in n?n.separator:a;e="length"in n?+n.length||0:e,i="omission"in n?u(n.omission):i}else e=+n||0;if(t=u(t),e>=t.length)return t;var o=e-i.length;if(1>o)return i;var c=t.slice(0,o);if(null==a)return c+i;if(Ni(a)){if(t.slice(o).search(a)){var s,l,f=t.slice(0,o);for(a.global||(a=Ka(a.source,(Tt.exec(a)||"")+"g")),a.lastIndex=0;s=a.exec(f);)l=s.index;c=c.slice(0,null==l?o:l)}}else if(t.indexOf(a,o)!=o){var h=c.lastIndexOf(a);h>-1&&(c=c.slice(0,h))}return c+i}function Aa(t){return t=u(t),t&&mt.test(t)?t.replace(gt,A):t}function ka(t,n,r){return r&&Zr(t,n,r)&&(n=S),t=u(t),t.match(n||$t)||[]}function Sa(t,n,r){return r&&Zr(t,n,r)&&(n=S),g(t)?Oa(t):wn(t,n)}function ja(t){return function(){return t}}function Ea(t){return t}function Oa(t){return Un(bn(t,!0))}function Ma(t,n){return Nn(t,bn(n,!0))}function Ca(t,n,r){if(null==r){var e=Wi(n),i=e?Uu(n):S,a=i&&i.length?Wn(n,i):S;(a?a.length:e)||(a=!1,r=n,n=t,t=this)}a||(a=Wn(n,Uu(n)));var o=!0,u=-1,c=Ii(t),s=a.length;r===!1?o=!1:Wi(r)&&"chain"in r&&(o=r.chain);for(;++ut||!xo(t))return[];var e=-1,i=Ua(Ao(t,Mo));for(n=or(n,r,1);++ee?i[e]=n(e):n(e);return i}function Pa(t){var n=++no;return u(t)+n}function Fa(t,n){return(+t||0)+(+n||0)}function $a(t,n,r){return r&&Zr(t,n,r)&&(n=S),n=Nr(n,r,3),1==n.length?dn(Mu(t)?t:le(t),n):Zn(t,n)}t=t?en.defaults(rn.Object(),t,en.pick(rn,Ut)):rn;var Ua=t.Array,Na=t.Date,Ba=t.Error,La=t.Function,Da=t.Math,Va=t.Number,Ha=t.Object,Ka=t.RegExp,Ya=t.String,Ga=t.TypeError,Ja=Ua.prototype,Qa=Ha.prototype,Xa=Ya.prototype,Za=La.prototype.toString,to=Qa.hasOwnProperty,no=0,ro=Qa.toString,eo=rn._,io=Ka("^"+Za.call(to).replace(/[\\^$.*+?()[\]{}|]/g,"\\$&").replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g,"$1.*?")+"$"),ao=t.ArrayBuffer,oo=t.clearTimeout,uo=t.parseFloat,co=Da.pow,so=Qa.propertyIsEnumerable,lo=Vr(t,"Set"),fo=t.setTimeout,ho=Ja.splice,po=t.Uint8Array,vo=Vr(t,"WeakMap"),_o=Da.ceil,go=Vr(Ha,"create"),yo=Da.floor,mo=Vr(Ua,"isArray"),xo=t.isFinite,wo=Vr(Ha,"keys"),bo=Da.max,Ao=Da.min,ko=Vr(Na,"now"),So=t.parseInt,jo=Da.random,Eo=Va.NEGATIVE_INFINITY,Oo=Va.POSITIVE_INFINITY,Mo=4294967295,Co=Mo-1,Ro=Mo>>>1,To=9007199254740991,Io=vo&&new vo,Wo={};n.support={};n.templateSettings={escape:wt,evaluate:bt,interpolate:At,variable:"",imports:{_:n}};var qo=function(){function t(){}return function(n){if(Wi(n)){t.prototype=n;var r=new t;t.prototype=S}return r||{}}}(),zo=hr(Tn),Po=hr(In,!0),Fo=pr(),$o=pr(!0),Uo=Io?function(t,n){return Io.set(t,n),t}:Ea,No=Io?function(t){return Io.get(t)}:Ta,Bo=Dn("length"),Lo=function(){var t=0,n=0;return function(r,e){var i=vu(),a=$-(i-n);if(n=i,a>0){if(++t>=F)return r}else t=0;return Uo(r,e)}}(),Do=gi(function(t,n){return g(t)&&Qr(t)?kn(t,Cn(n,!1,!0)):[]}),Vo=br(),Ho=br(!0),Ko=gi(function(t){for(var n=t.length,r=n,e=Ua(f),i=Lr(),o=i==a,u=[];r--;){var c=t[r]=Qr(c=t[r])?c:[];e[r]=o&&c.length>=120?vr(r&&c):null}var s=t[0],l=-1,f=s?s.length:0,h=e[0];t:for(;++l2?t[n-2]:S,e=n>1?t[n-1]:S;return n>2&&"function"==typeof r?n-=2:(r=n>1&&"function"==typeof e?(--n,e):S,e=S),t.length=n,Fe(t,r,e)}),nu=gi(function(t){return t=Cn(t),this.thru(function(n){return Zt(Mu(n)?n:[fe(n)],t)})}),ru=gi(function(t,n){return mn(t,Cn(n))}),eu=lr(function(t,n,r){to.call(t,r)?++t[r]:t[r]=1}),iu=wr(zo),au=wr(Po,!0),ou=Sr(nn,zo),uu=Sr(an,Po),cu=lr(function(t,n,r){to.call(t,r)?t[r].push(n):t[r]=[n]}),su=lr(function(t,n,r){t[r]=n}),lu=gi(function(t,n,r){var e=-1,i="function"==typeof n,a=te(n),o=Qr(t)?Ua(t.length):[];return zo(t,function(t){var u=i?n:a&&null!=t?t[n]:S;o[++e]=u?u.apply(t,r):Jr(t,n,r)}),o}),fu=lr(function(t,n,r){t[r?0:1].push(n)},function(){return[[],[]]}),hu=Rr(fn,zo),pu=Rr(hn,Po),du=gi(function(t,n){if(null==t)return[];var r=n[2];return r&&Zr(n[0],n[1],r)&&(n.length=1),Xn(t,Cn(n),[])}),vu=ko||function(){return(new Na).getTime()},_u=gi(function(t,n,r){var e=E;if(r.length){var i=m(r,_u.placeholder);e|=T}return Pr(t,e,n,r,i)}),gu=gi(function(t,n){n=n.length?Cn(n):Ji(t);for(var r=-1,e=n.length;++r0||0>n)?new Q(r):(0>t?r=r.takeRight(-t):t&&(r=r.drop(t)),n!==S&&(n=+n||0,r=0>n?r.dropRight(-n):r.take(n-t)),r)},Q.prototype.takeRightWhile=function(t,n){return this.reverse().takeWhile(t,n).reverse()},Q.prototype.toArray=function(){return this.take(Oo)},Tn(Q.prototype,function(t,r){var e=/^(?:filter|map|reject)|While$/.test(r),i=/^(?:first|last)$/.test(r),a=n[i?"take"+("last"==r?"Right":""):r];a&&(n.prototype[r]=function(){var n=i?[1]:arguments,r=this.__chain__,o=this.__wrapped__,u=!!this.__actions__.length,c=o instanceof Q,s=n[0],l=c||Mu(o);l&&e&&"function"==typeof s&&1!=s.length&&(c=l=!1);var f=function(t){return i&&r?a(t,1)[0]:a.apply(S,ln([t],n))},h={func:Le,args:[f],thisArg:S},p=c&&!u;if(i&&!r)return p?(o=o.clone(),o.__actions__.push(h),t.call(o)):a.call(S,this.value())[0];if(!i&&l){o=p?o:new Q(this);var d=t.apply(o,n);return d.__actions__.push(h),new y(d,r)}return this.thru(f)})}),nn(["join","pop","push","replace","shift","sort","splice","split","unshift"],function(t){var r=(/^(?:replace|split)$/.test(t)?Xa:Ja)[t],e=/^(?:push|sort|unshift)$/.test(t)?"tap":"thru",i=/^(?:join|pop|replace|shift)$/.test(t);n.prototype[t]=function(){var t=arguments;return i&&!this.__chain__?r.apply(this.value(),t):this[e](function(n){return r.apply(n,t)})}}),Tn(Q.prototype,function(t,r){var e=n[r];if(e){var i=e.name,a=Wo[i]||(Wo[i]=[]);a.push({name:r,func:e})}}),Wo[Tr(S,O).name]=[{name:"wrapper",func:S}],Q.prototype.clone=nt,Q.prototype.reverse=et,Q.prototype.value=Dt,n.prototype.chain=De,n.prototype.commit=Ve,n.prototype.concat=nu,n.prototype.plant=He,n.prototype.reverse=Ke,n.prototype.toString=Ye,n.prototype.run=n.prototype.toJSON=n.prototype.valueOf=n.prototype.value=Ge,n.prototype.collect=n.prototype.map,n.prototype.head=n.prototype.first,n.prototype.select=n.prototype.filter,n.prototype.tail=n.prototype.rest,n}var S,j="3.10.1",E=1,O=2,M=4,C=8,R=16,T=32,I=64,W=128,q=256,z=30,P="...",F=150,$=16,U=200,N=1,B=2,L="Expected a function",D="__lodash_placeholder__",V="[object Arguments]",H="[object Array]",K="[object Boolean]",Y="[object Date]",G="[object Error]",J="[object Function]",Q="[object Map]",X="[object Number]",Z="[object Object]",tt="[object RegExp]",nt="[object Set]",rt="[object String]",et="[object WeakMap]",it="[object ArrayBuffer]",at="[object Float32Array]",ot="[object Float64Array]",ut="[object Int8Array]",ct="[object Int16Array]",st="[object Int32Array]",lt="[object Uint8Array]",ft="[object Uint8ClampedArray]",ht="[object Uint16Array]",pt="[object Uint32Array]",dt=/\b__p \+= '';/g,vt=/\b(__p \+=) '' \+/g,_t=/(__e\(.*?\)|\b__t\)) \+\n'';/g,gt=/&(?:amp|lt|gt|quot|#39|#96);/g,yt=/[&<>"'`]/g,mt=RegExp(gt.source),xt=RegExp(yt.source),wt=/<%-([\s\S]+?)%>/g,bt=/<%([\s\S]+?)%>/g,At=/<%=([\s\S]+?)%>/g,kt=/\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\n\\]|\\.)*?\1)\]/,St=/^\w*$/,jt=/[^.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\n\\]|\\.)*?)\2)\]/g,Et=/^[:!,]|[\\^$.*+?()[\]{}|\/]|(^[0-9a-fA-Fnrtuvx])|([\n\r\u2028\u2029])/g,Ot=RegExp(Et.source),Mt=/[\u0300-\u036f\ufe20-\ufe23]/g,Ct=/\\(\\)?/g,Rt=/\$\{([^\\}]*(?:\\.[^\\}]*)*)\}/g,Tt=/\w*$/,It=/^0[xX]/,Wt=/^\[object .+?Constructor\]$/,qt=/^\d+$/,zt=/[\xc0-\xd6\xd8-\xde\xdf-\xf6\xf8-\xff]/g,Pt=/($^)/,Ft=/['\n\r\u2028\u2029\\]/g,$t=function(){var t="[A-Z\\xc0-\\xd6\\xd8-\\xde]",n="[a-z\\xdf-\\xf6\\xf8-\\xff]+";return RegExp(t+"+(?="+t+n+")|"+t+"?"+n+"|"+t+"+|[0-9]+","g")}(),Ut=["Array","ArrayBuffer","Date","Error","Float32Array","Float64Array","Function","Int8Array","Int16Array","Int32Array","Math","Number","Object","RegExp","Set","String","_","clearTimeout","isFinite","parseFloat","parseInt","setTimeout","TypeError","Uint8Array","Uint8ClampedArray","Uint16Array","Uint32Array","WeakMap"],Nt=-1,Bt={};Bt[at]=Bt[ot]=Bt[ut]=Bt[ct]=Bt[st]=Bt[lt]=Bt[ft]=Bt[ht]=Bt[pt]=!0,Bt[V]=Bt[H]=Bt[it]=Bt[K]=Bt[Y]=Bt[G]=Bt[J]=Bt[Q]=Bt[X]=Bt[Z]=Bt[tt]=Bt[nt]=Bt[rt]=Bt[et]=!1;var Lt={};Lt[V]=Lt[H]=Lt[it]=Lt[K]=Lt[Y]=Lt[at]=Lt[ot]=Lt[ut]=Lt[ct]=Lt[st]=Lt[X]=Lt[Z]=Lt[tt]=Lt[rt]=Lt[lt]=Lt[ft]=Lt[ht]=Lt[pt]=!0,Lt[G]=Lt[J]=Lt[Q]=Lt[nt]=Lt[et]=!1;var Dt={"À":"A","Á":"A","Â":"A","Ã":"A","Ä":"A","Å":"A","à":"a","á":"a","â":"a","ã":"a","ä":"a","å":"a","Ç":"C","ç":"c","Ð":"D","ð":"d","È":"E","É":"E","Ê":"E","Ë":"E","è":"e","é":"e","ê":"e","ë":"e","Ì":"I","Í":"I","Î":"I","Ï":"I","ì":"i","í":"i","î":"i","ï":"i","Ñ":"N","ñ":"n","Ò":"O","Ó":"O","Ô":"O","Õ":"O","Ö":"O","Ø":"O","ò":"o","ó":"o","ô":"o","õ":"o","ö":"o","ø":"o","Ù":"U","Ú":"U","Û":"U","Ü":"U","ù":"u","ú":"u","û":"u","ü":"u","Ý":"Y","ý":"y","ÿ":"y","Æ":"Ae","æ":"ae","Þ":"Th","þ":"th","ß":"ss"},Vt={"&":"&","<":"<",">":">",'"':""","'":"'","`":"`"},Ht={"&":"&","<":"<",">":">",""":'"',"'":"'","`":"`"},Kt={"function":!0,object:!0},Yt={0:"x30",1:"x31",2:"x32",3:"x33",4:"x34",5:"x35",6:"x36",7:"x37",8:"x38",9:"x39",A:"x41",B:"x42",C:"x43",D:"x44",E:"x45",F:"x46",a:"x61",b:"x62",c:"x63",d:"x64",e:"x65",f:"x66",n:"x6e",r:"x72",t:"x74",u:"x75",v:"x76",x:"x78"},Gt={"\\":"\\","'":"'","\n":"n","\r":"r","\u2028":"u2028","\u2029":"u2029"},Jt=Kt[typeof r]&&r&&!r.nodeType&&r,Qt=Kt[typeof n]&&n&&!n.nodeType&&n,Xt=Jt&&Qt&&"object"==typeof t&&t&&t.Object&&t,Zt=Kt[typeof self]&&self&&self.Object&&self,tn=Kt[typeof window]&&window&&window.Object&&window,nn=Qt&&Qt.exports===Jt&&Jt,rn=Xt||tn!==(this&&this.window)&&tn||Zt||this,en=k();"function"==typeof define&&"object"==typeof define.amd&&define.amd?(rn._=en,define(function(){return en})):Jt&&Qt?nn?(Qt.exports=en)._=en:Jt._=en:rn._=en}).call(this)}).call(this,"undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{}],4:[function(t,n,r){function e(t){if(t=""+t,!(t.length>1e4)){var n=/^((?:\d+)?\.?\d+) *(milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|years?|yrs?|y)?$/i.exec(t);if(n){var r=parseFloat(n[1]),e=(n[2]||"ms").toLowerCase();switch(e){case"years":case"year":case"yrs":case"yr":case"y":return r*f;case"days":case"day":case"d":return r*l;case"hours":case"hour":case"hrs":case"hr":case"h":return r*s;case"minutes":case"minute":case"mins":case"min":case"m":return r*c;case"seconds":case"second":case"secs":case"sec":case"s":return r*u;case"milliseconds":case"millisecond":case"msecs":case"msec":case"ms":return r}}}}function i(t){return t>=l?Math.round(t/l)+"d":t>=s?Math.round(t/s)+"h":t>=c?Math.round(t/c)+"m":t>=u?Math.round(t/u)+"s":t+"ms"}function a(t){return o(t,l,"day")||o(t,s,"hour")||o(t,c,"minute")||o(t,u,"second")||t+" ms"}function o(t,n,r){return n>t?void 0:1.5*n>t?Math.floor(t/n)+" "+r:Math.ceil(t/n)+" "+r+"s"}var u=1e3,c=60*u,s=60*c,l=24*s,f=365.25*l;n.exports=function(t,n){return n=n||{},"string"==typeof t?e(t):n["long"]?a(t):i(t)}},{}],5:[function(t,n,r){"use strict";function e(t){if(t&&t.__esModule)return t;var n={};if(null!=t)for(var r in t)Object.prototype.hasOwnProperty.call(t,r)&&(n[r]=t[r]);return n["default"]=t,n}Object.defineProperty(r,"__esModule",{value:!0}),r.ChessDataViz=void 0;var i=t("./util"),a=e(i),o=t("./EvalAndTime"),u=t("./HeatMap"),c=t("./Openings"),s=t("./MovePaths"),l=r.ChessDataViz={EvalAndTime:o.EvalAndTime,HeatMap:u.HeatMap,Openings:c.Openings,MovePaths:s.MovePaths,util:a};window.ChessDataViz=l},{"./EvalAndTime":6,"./HeatMap":7,"./MovePaths":8,"./Openings":9,"./util":10}],6:[function(t,n,r){"use strict";function e(t){return t&&t.__esModule?t:{"default":t}}function i(t,n){if(!(t instanceof n))throw new TypeError("Cannot call a class as a function")}var a=function(){function t(t,n){for(var r=0;rn[e]+r;e++);var i=o._xScale.domain()[e],a=o._xScale(i)+o._xScale.rangeBand()/2;if(a){d.select(".x-guide").classed("hidden",!1).attr("x1",a).attr("x2",a).attr("y1",-6).attr("y2",o._height);var u=o._yEvalScale(o._data[i].score);d.select(".yEval-guide").classed("hidden",!1).attr("x1",-6).attr("x2",a).transition().duration(100).attr("y1",u).attr("y2",u),u=o._yTimeScale(o._data[i].time),d.select(".yTime-guide").classed("hidden",!1).attr("x1",a).attr("x2",o._width+6).transition().duration(100).attr("y1",u).attr("y2",u),o.dispatch.mousemove(o._data[i])}}}).on("mouseleave",function(){o._options.interactive&&(d.selectAll(".interactive-layer .guide").classed("hidden",!0),o.dispatch.mouseleave())}),e&&this.data(e)}return a(t,[{key:"data",value:function(t){this._data=t,this.update()}},{key:"options",value:function(t){var n=["width","margin","boardWidth","squareWidth"];u["default"].merge(this._options,u["default"].omit(t,n)),u["default"].isArray(this._options.colorScale)&&this._scale.color.range(this._options.colorScale),this.update()}},{key:"update",value:function(){var t=this;this._xScale.domain(d3.range(this._data.length));var n=d3.max(d3.extent(this._data,function(t){return t.time}).map(Math.abs));this._yTimeScale.domain([-n,n]),this._xAxis.tickValues(this._xScale.domain().filter(function(t,n){return 0==n||!((n+1)%10)}));var r=d3.svg.line().x(function(n,r){return t._xScale(r)+t._xScale.rangeBand()/2}).y(function(n){return t._yEvalScale(n.score)}),e=d3.svg.area().x(r.x()).y1(r.y()).y0(this._yEvalScale(0)),i=this.container.transition();i.select("g.axis.x").call(this._xAxis),i.select("g.axis.yTime").call(this._yTimeAxis),i.select("g.axis.yEval").call(this._yEvalAxis);var a=this.container.select(".bars").selectAll(".bar").data(this._data);a.enter().append("rect").attr("height",0).attr("y",this._yTimeScale(0)),a.transition().delay(function(n,r){return r/t._xScale.domain().length*500}).attr("x",function(n,r){return t._xScale(r)}).attr("width",this._xScale.rangeBand()).attr("y",function(n){return n.time>0?t._yTimeScale(n.time):t._yTimeScale(0)}).attr("height",function(n){return n.time>0?t._yTimeScale(0)-t._yTimeScale(n.time):t._yTimeScale(n.time)-t._yTimeScale(0)}).attr("class",function(t,n){return"bar "+(n%2?"black":"white")}),a.exit().transition().delay(function(n,r){return r/t._xScale.domain().length*500}).attr("height",0).remove();var o=this.container.select(".lines").selectAll(".line");o.data(["white","black"]).enter().append("path").attr("class",function(t){return"line "+t}).attr("clip-path",function(t){return"url(#clip-"+t+")"}).datum(this._data).attr("d",r),o.datum(this._data).transition().attr("d",r);var u=this.container.select(".areas").selectAll(".area");u.data(["white","black"]).enter().append("path").attr("class",function(t){return"area "+t}).attr("clip-path",function(t){return"url(#clip-"+t+")"}).datum(this._data).attr("d",e),u.datum(this._data).transition().attr("d",e)}}]),t}());r.EvalAndTime=l},{debug:1,lodash:3}],7:[function(t,n,r){"use strict";function e(t){if(t&&t.__esModule)return t;var n={};if(null!=t)for(var r in t)Object.prototype.hasOwnProperty.call(t,r)&&(n[r]=t[r]);return n["default"]=t,n}function i(t){return t&&t.__esModule?t:{"default":t}}function a(t,n){if(!(t instanceof n))throw new TypeError("Cannot call a class as a function")}var o=function(){function t(t,n){for(var r=0;re;e++){var i=t.split("-")[e].toLowerCase(),a=i.charCodeAt(0)-97,o=8-i[1],u=a*r._options.squareWidth+r._options.squareWidth/2,c=o*r._options.squareWidth+r._options.squareWidth/2;n.push({x:u,y:c})}return n}var n=this,r=this,e=[];s["default"].pairs(this._data[this._options.accessor]).forEach(function(t){for(var r=Math.ceil(t[1]/n._options.binSize),i=0;r>i;i++)e.push(t[0])}),this.dataContainer.selectAll(".move-path").remove(),this.dataContainer.selectAll(".move-path").data(e).enter().append("path").attr("class","move-path").attr("d",function(r){var e=t(r),i=o(e,2),a=i[0],u=i[1],c={x:-(u.y-a.y),y:u.x-a.x},s=Math.sqrt(Math.pow(c.x,2)+Math.pow(c.y,2)),l=Math.sqrt(Math.pow(u.x-a.x,2)+Math.pow(u.y-a.y,2))/n._options.bezierScaleFactor;c.x/=s,c.y/=s,c.x*=l,c.y*=l;var f=void 0;f=u.xn._options.arcThreshold}),i=this.dataContainer.selectAll(".arc").data(e);i.enter().append("path").attr("display",function(t){return t.depth?null:"none"}).attr("d",this._arc).attr("fill-rule","evenodd").attr("class","arc").each(function(t){this.x0=0,this.dx0=0}).style("fill",t),i.on("mouseenter",function(t,r){var e=a(t);i.style("opacity",.3),i.filter(function(t){return e.indexOf(t)>-1}).style("opacity",1);var o=l["default"].pluck(e,"san");n.dispatch.mouseenter(t,o)}).on("mousemove",function(){n.dispatch.mousemove()}).on("mouseleave",function(){i.style("opacity",1),n.dispatch.mouseleave()}).transition().duration(500).attrTween("d",function(t){var n=d3.interpolate({x:this.x0,dx:this.dx0},t);return this.x0=t.x,this.dx0=t.dx,function(t){var e=n(t);return r._arc(e)}}).style("fill",t),i.exit().remove();var o=this.dataContainer.selectAll(".san").data(e);o.enter().append("text").attr("class","san").attr("dy","6").attr("text-anchor","middle"),o.transition().duration(500).attr("transform",function(t){return"translate("+n._arc.centroid(t)+")"}).text(function(t){return t.dxn;n++)t.push({x:n%8,y:Math.floor(n/8)});return t}function u(t){return!(t.x%2)&&!(t.y%2)||t.x%2&&t.y%2}function c(t,n){var r=o(),e=t.selectAll(".square").data(r).enter().append("g").attr("class",function(t){var n=String.fromCharCode(97+t.x),r=8-t.y;return"square "+n+r}).classed("white",function(t){return u(t)}).classed("black",function(t){return!u(t)});e.append("rect").attr("x",function(t){return t.x*n}).attr("y",function(t){return t.y*n}).attr("width",n+"px").attr("height",n+"px").attr("class","sq");var i=d3.range(8).map(function(t){return".a"+(t+1)});t.selectAll(i).append("text").attr("x",function(t){return t.x*n}).attr("y",function(t){return t.y*n}).attr("dx","0.2em").attr("dy","1em").text(function(t){return 8-t.y}).attr("class","label");var a=d3.range(8).map(function(t){return String.fromCharCode(97+t)}),c=a.slice().map(function(t){return"."+t+"1"});t.selectAll(c).append("text").attr("x",function(t){return(t.x+1)*n}).attr("y",function(t){return(t.y+1)*n}).attr("dx","-0.3em").attr("dy","-0.5em").attr("text-anchor","end").text(function(t){return a[t.x]}).attr("class","label")}Object.defineProperty(r,"__esModule",{value:!0}),r.parseGameNotation=a,r.isWhite=u,r.drawBoard=c},{}]},{},[5]); -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | 3 | //util 4 | var plumber = require('gulp-plumber'); 5 | var buffer = require('vinyl-buffer'); 6 | var source = require('vinyl-source-stream'); 7 | var sourcemaps = require('gulp-sourcemaps'); 8 | var browserSync = require('browser-sync').create(); 9 | var rename = require('gulp-rename'); 10 | 11 | //js 12 | var browserify = require('browserify'); 13 | var babelify = require('babelify'); 14 | var uglify = require('gulp-uglify'); 15 | 16 | //css 17 | var less = require('gulp-less'); 18 | var postcss = require('gulp-postcss'); 19 | var autoprefixer = require('autoprefixer'); 20 | var mqpacker = require('css-mqpacker'); 21 | var csswring = require('csswring'); 22 | 23 | gulp.task('js', function () { 24 | return browserify({ 25 | entries: './src/js/ChessDataViz.js', 26 | debug: true 27 | }) 28 | .transform(babelify.configure({ 29 | presets: ['es2015'] 30 | })) 31 | .bundle() 32 | .on('error', errorHandler) 33 | .pipe(source('ChessDataViz.js')) 34 | .pipe(buffer()) 35 | .pipe(gulp.dest('./dist/')) 36 | .pipe(rename('ChessDataViz.min.js')) 37 | .pipe(uglify()) 38 | .pipe(gulp.dest('./dist/')) 39 | .pipe(browserSync.reload({stream:true})) 40 | ; 41 | }); 42 | 43 | gulp.task('less', function () { 44 | return gulp.src('./src/less/ChessDataViz.less') 45 | .pipe(plumber()) 46 | .pipe(sourcemaps.init({loadMaps: true})) 47 | .pipe(less()) 48 | .on('error', errorHandler) 49 | .pipe(postcss([ 50 | autoprefixer({ 51 | browsers: ['last 2 versions'] 52 | }), 53 | mqpacker 54 | ])) 55 | .pipe(gulp.dest('./dist')) 56 | .pipe(rename('ChessDataViz.min.css')) 57 | .pipe(postcss([ 58 | csswring({ 59 | removeAllComments: true 60 | }) 61 | ])) 62 | .pipe(sourcemaps.write('./')) 63 | .pipe(gulp.dest('./dist')) 64 | .pipe(browserSync.reload({stream:true})) 65 | ; 66 | }); 67 | 68 | gulp.task('serve', function() { 69 | return browserSync.init({ 70 | server: { 71 | baseDir: './' 72 | }, 73 | open: false 74 | }); 75 | }); 76 | 77 | gulp.task('watch', ['serve'], function () { 78 | gulp.watch('src/js/*.js', ['js']); 79 | gulp.watch('src/less/*.less', ['less']); 80 | 81 | gulp.watch('test/*.html', browserSync.reload); 82 | }); 83 | 84 | function errorHandler(err) { 85 | /*eslint no-console: 0*/ 86 | console.log(err.message); 87 | this.emit('end'); 88 | } -------------------------------------------------------------------------------- /ideas.md: -------------------------------------------------------------------------------- 1 | - [x] **Heatmap** 2 | * https://www.flickr.com/photos/stevefaeembra/5565448824 3 | * http://kyrandale.com/viz/static/expts/d3-chess-css3d/index_squares.html 4 | * http://en.chessbase.com/post/study-of-square-utilization-and-occupancy 5 | 6 | - [x] **Move Paths** 7 | * http://imgur.com/a/pYHyk/layout/grid 8 | * http://archive.turbulence.org/spotlight/thinking/mid-viz.jpg 9 | 10 | - [ ] **Survivors** 11 | * https://www.quora.com/What-are-the-chances-of-survival-of-individual-chess-pieces-in-average-games 12 | * http://imgur.com/gallery/c1AhDU3 13 | 14 | - [ ] **Maybe?** 15 | * http://rtribbia.com/chessViz/ 16 | * http://creative-co.de/random_chess/ - vector fields? -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chess-dataviz", 3 | "version": "1.0.0", 4 | "description": "", 5 | "scripts": { 6 | "test": "eslint ." 7 | }, 8 | "author": "ebemunk ", 9 | "license": "MIT", 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/ebemunk/chess-dataviz" 13 | }, 14 | "dependencies": { 15 | "bluebird": "^3.0.1", 16 | "chess.js": "^0.9.1", 17 | "colors": "^1.1.2", 18 | "debug": "^2.2.0", 19 | "highland": "^2.5.1", 20 | "lodash": "^3.10.1", 21 | "minimist": "^1.2.0", 22 | "mkdirp": "^0.5.1", 23 | "phantom": "^0.8.0", 24 | "progress": "^1.1.8", 25 | "request": "^2.65.0" 26 | }, 27 | "devDependencies": { 28 | "autoprefixer": "^6.0.3", 29 | "babel": "^6.0.14", 30 | "babel-preset-es2015": "^6.0.14", 31 | "babelify": "^7.0.2", 32 | "browser-sync": "^2.9.11", 33 | "browserify": "^12.0.0", 34 | "css-mqpacker": "^4.0.0", 35 | "csswring": "^4.0.0", 36 | "eslint": "^1.7.3", 37 | "gulp": "^3.9.0", 38 | "gulp-less": "^3.0.3", 39 | "gulp-plumber": "^1.0.1", 40 | "gulp-postcss": "^6.0.1", 41 | "gulp-rename": "^1.2.2", 42 | "gulp-sourcemaps": "^1.6.0", 43 | "gulp-uglify": "^1.4.2", 44 | "vinyl-buffer": "^1.0.0", 45 | "vinyl-source-stream": "^1.1.0" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /scripts/README.md: -------------------------------------------------------------------------------- 1 | ## Helper scripts 2 | This is a collection of helper scripts to aid in getting/parsing data required for the visualizations. You don't need to use them, and the scripts are certainly far from refined. I just made them so it's easier to parse PGN files & scrape some data online. 3 | 4 | ### stats.js 5 | This is a PGN parser that will extract some statistics to use in Heatmap, Move Path and Opening graphs. It can handle big (huge) PGN files. I've tried it with millionbase database and it took around 40 minutes, but completed no problem. 6 | 7 | #### Usage 8 | To improve performance, there is no chess logic in the program. Hence, regular PGNs with SAN moves won't work, as parsing them requires the program to know about the rules of chess. A workaround is to use the brilliant [pgn-extract](https://www.cs.kent.ac.uk/people/staff/djb/pgn-extract/) command line tool to convert the notation into Extended Long Algebraic notation with: 9 | 10 | `pgn-extract -Wxlalg -C -N -V -D -s` 11 | 12 | * -C no comments 13 | * -N no NAGs 14 | * -V no variations 15 | * -D no duplicate games 16 | 17 | After which the script can be used on the generated PGN. 18 | 19 | Example: `node stats.js -f myfile.pgn` 20 | 21 | ### scraper.js 22 | This script will fetch data formatted in the style that Evaluation and Time Graph expects, from chess24.com tournaments website. It will also download player images. 23 | 24 | #### Usage 25 | The behavior is determined by command line arguments: 26 | * `-t, --tournament` Tournament id in chess24.com [shamkir-gashimov-memorial-2015] 27 | * `-r, --rounds` Number of rounds to fetch [9] 28 | * `-m, --matches` Number of matches per round [1] 29 | * `-g, --games` Number of games per match [5] 30 | * `-c, --concurrency` Number of concurrent phantomjs instances to create [5] 31 | * `-w, --wait` ms to wait before scraping [15000] 32 | 33 | Example: 34 | `node scraper.js -t london-chess-classic-2015 -r 9` -------------------------------------------------------------------------------- /scripts/lib/Heatmaps.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | let _ = require('lodash'); 4 | 5 | class Heatmaps { 6 | constructor() { 7 | this.counters = [ 8 | 'squareUtilization', 9 | 'moveSquares', 10 | 'captureSquares', 11 | 'checkSquares' 12 | ]; 13 | 14 | this.data = {}; 15 | 16 | this.counters.forEach(counter => { 17 | this.data[counter] = []; 18 | 19 | for( let i of _.range(64) ) { 20 | this.data[counter][i] = { 21 | p: {w: 0, b: 0}, 22 | n: {w: 0, b: 0}, 23 | b: {w: 0, b: 0}, 24 | r: {w: 0, b: 0}, 25 | q: {w: 0, b: 0}, 26 | k: {w: 0, b: 0}, 27 | all: {w: 0, b: 0} 28 | }; 29 | } 30 | }); 31 | } 32 | 33 | update(moves) { 34 | moves.forEach((move, i) => { 35 | let piece = (/[NBRQK]/.test(move[0]) ? move[0] : 'P').toLowerCase(); 36 | 37 | if( piece !== 'p' ) { 38 | move = move.substr(1); 39 | } 40 | 41 | let fromTo = move.split(/[x\-]/); 42 | 43 | //an array in case of castling its K and R move 44 | let parsedMoves = []; 45 | 46 | 47 | //O-O white 48 | if( /e1-g1/.test(move) ) { 49 | parsedMoves.push({ 50 | from: this.squareToIndex('e1'), 51 | to: this.squareToIndex('g1'), 52 | piece: 'k', 53 | color: 'w', 54 | capture: false, 55 | check: /\+/.test(move) 56 | }); 57 | 58 | parsedMoves.push({ 59 | from: this.squareToIndex('h1'), 60 | to: this.squareToIndex('f1'), 61 | piece: 'r', 62 | color: 'w', 63 | capture: false, 64 | check: /\+/.test(move) 65 | }); 66 | } 67 | 68 | //O-O-O white 69 | else if( /e1-c1/.test(move) ) { 70 | parsedMoves.push({ 71 | from: this.squareToIndex('e1'), 72 | to: this.squareToIndex('c1'), 73 | piece: 'k', 74 | color: 'w', 75 | capture: false, 76 | check: /\+/.test(move) 77 | }); 78 | 79 | parsedMoves.push({ 80 | from: this.squareToIndex('a1'), 81 | to: this.squareToIndex('d1'), 82 | piece: 'r', 83 | color: 'w', 84 | capture: false, 85 | check: /\+/.test(move) 86 | }); 87 | } 88 | 89 | //O-O black 90 | else if( /e8-g8/.test(move) ) { 91 | parsedMoves.push({ 92 | from: this.squareToIndex('e8'), 93 | to: this.squareToIndex('g8'), 94 | piece: 'k', 95 | color: 'b', 96 | capture: false, 97 | check: /\+/.test(move) 98 | }); 99 | 100 | parsedMoves.push({ 101 | from: this.squareToIndex('h8'), 102 | to: this.squareToIndex('f8'), 103 | piece: 'r', 104 | color: 'b', 105 | capture: false, 106 | check: /\+/.test(move) 107 | }); 108 | } 109 | 110 | //O-O-O black 111 | else if( /e8-c8/.test(move) ) { 112 | parsedMoves.push({ 113 | from: this.squareToIndex('e8'), 114 | to: this.squareToIndex('c8'), 115 | piece: 'k', 116 | color: 'b', 117 | capture: false, 118 | check: /\+/.test(move) 119 | }); 120 | 121 | parsedMoves.push({ 122 | from: this.squareToIndex('a8'), 123 | to: this.squareToIndex('d8'), 124 | piece: 'r', 125 | color: 'b', 126 | capture: false, 127 | check: /\+/.test(move) 128 | }); 129 | } 130 | 131 | else { 132 | parsedMoves.push({ 133 | from: this.squareToIndex(fromTo[0]), 134 | to: this.squareToIndex(fromTo[1]), 135 | piece: piece, 136 | color: i % 2 === 0 ? 'w' : 'b', 137 | capture: /x/.test(move), 138 | check: /\+/.test(move) 139 | }); 140 | } 141 | 142 | this.counters.forEach(counter => { 143 | parsedMoves.forEach(parsedMove => { 144 | this[counter](parsedMove); 145 | }); 146 | }); 147 | }); 148 | } 149 | 150 | squareToIndex(square) { 151 | square = square.toLowerCase(); 152 | 153 | let file = square.charCodeAt(0) - 97; 154 | let rank = 8 - square[1]; 155 | let index = rank * 8 + file; 156 | 157 | return index; 158 | } 159 | 160 | squareUtilization(move) { 161 | this.data.squareUtilization[move.to][move.piece][move.color]++; 162 | this.data.squareUtilization[move.to].all[move.color]++; 163 | } 164 | 165 | moveSquares(move) { 166 | this.data.moveSquares[move.from][move.piece][move.color]++; 167 | this.data.moveSquares[move.from].all[move.color]++; 168 | } 169 | 170 | captureSquares(move) { 171 | if( move.capture ) { 172 | this.data.captureSquares[move.to][move.piece][move.color]++; 173 | this.data.captureSquares[move.to].all[move.color]++; 174 | } 175 | } 176 | 177 | checkSquares(move) { 178 | if( move.check ) { 179 | this.data.checkSquares[move.to][move.piece][move.color]++; 180 | this.data.checkSquares[move.to].all[move.color]++; 181 | } 182 | } 183 | } 184 | 185 | module.exports = Heatmaps; -------------------------------------------------------------------------------- /scripts/lib/Moves.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | const debug = require('debug')('stats:moves'); 5 | 6 | function parseMove(move, i) { 7 | // let check = /\+/.test(move); 8 | 9 | move = move.replace(/\+/g, '').replace(/#/g, ''); 10 | 11 | let piece = (/[NBRQK]/.test(move[0]) ? move[0] : 'P').toLowerCase(); 12 | let fromName = move.split(/[x\-]/)[0]; 13 | let castle = /(^e1\-g1)|(^e1\-c1)|(^e8\-g8)|(^e8\-c8)/.test(move); 14 | 15 | if( piece !== 'p' ) { 16 | move = move.substr(1); 17 | } 18 | 19 | let fromTo = move.split(/[x\-]/); 20 | 21 | let parsedMove = { 22 | fromName: fromName, 23 | from: fromTo[0], 24 | to: fromTo[1], 25 | piece: piece, 26 | capture: /x/.test(move), 27 | castle: castle, 28 | color: i % 2 === 0 ? 'w' : 'b' 29 | }; 30 | 31 | if( parsedMove.castle ) { 32 | parsedMove.fromName = `K${parsedMove.fromName}`; 33 | } 34 | 35 | return parsedMove; 36 | } 37 | 38 | class Moves { 39 | constructor() { 40 | this.pieces = {}; 41 | 42 | let files = 'abcdefgh'.split(''); 43 | 44 | for( let file of files ) { 45 | for( let rank of [2, 7] ) { 46 | let pawn = `${file}${rank}`; 47 | this.pieces[pawn] = [pawn]; 48 | } 49 | } 50 | 51 | let pieces = { 52 | N: ['b', 'g'], 53 | B: ['c', 'f'], 54 | R: ['a', 'h'], 55 | Q: ['d'], 56 | K: ['e'] 57 | }; 58 | 59 | _.forEach(pieces, (files, piece) => { 60 | for( let file of files ) { 61 | for( let rank of [1, 8] ) { 62 | this.pieces[`${piece}${file}${rank}`] = [`${file}${rank}`]; 63 | } 64 | } 65 | }); 66 | 67 | this.survivalData = {}; 68 | } 69 | 70 | update(moves) { 71 | debug('NEW GAME ---------------'); 72 | 73 | let pieceLocations = _.cloneDeep(this.pieces); 74 | let currentSquareState = _.chain(pieceLocations).invert().mapValues(val => [val]).value(); 75 | 76 | moves.forEach((move, i) => { 77 | debug('MOVE:', move); 78 | 79 | let parsedMove = parseMove(move, i); 80 | 81 | if( /[NBRQ]$/.test(parsedMove.to) ) { 82 | parsedMove.to = parsedMove.to.slice(0, - 1); 83 | } 84 | 85 | let originalPiece = _.last(_.get(currentSquareState, parsedMove.from) || [parsedMove.fromName]); 86 | let target = _.last(_.get(currentSquareState, parsedMove.to) || [parsedMove.to]); 87 | 88 | debug(parsedMove, originalPiece, target); 89 | 90 | pieceLocations[originalPiece].push(parsedMove.to); 91 | 92 | if( parsedMove.capture ) { 93 | if( target === 'empty' || ! currentSquareState[parsedMove.to] ) { 94 | let enPassantSquare; 95 | 96 | if( parsedMove.color === 'w' ) { 97 | enPassantSquare = parsedMove.to.charAt(0) + (parseInt(parsedMove.to.charAt(1))-1); 98 | } else { 99 | enPassantSquare = parsedMove.to.charAt(0) + (parseInt(parsedMove.to.charAt(1))+1); 100 | } 101 | 102 | target = _.last(_.get(currentSquareState, enPassantSquare) || [enPassantSquare]); 103 | } 104 | 105 | pieceLocations[target].push(`captured-${i}`); 106 | } 107 | 108 | currentSquareState[parsedMove.to] = _.toArray(currentSquareState[parsedMove.to]).concat([originalPiece]); 109 | currentSquareState[parsedMove.from] = _.toArray(currentSquareState[parsedMove.from]).concat(['empty']); 110 | 111 | if( parsedMove.castle ) { 112 | if( /(^e\d\-g\d)/.test(move) ) { //kingside 113 | pieceLocations['Rh' + move.charAt(1)].push('f' + move.charAt(1)); 114 | currentSquareState['f' + move.charAt(1)] = _.toArray(currentSquareState['f' + move.charAt(1)]).concat(['Rh' + move.charAt(1)]); 115 | } else { 116 | pieceLocations['Ra' + move.charAt(1)].push('d' + move.charAt(1)); 117 | currentSquareState['d' + move.charAt(1)] = _.toArray(currentSquareState['d' + move.charAt(1)]).concat(['Ra' + move.charAt(1)]); 118 | } 119 | } 120 | 121 | debug(pieceLocations); 122 | debug(currentSquareState); 123 | }); 124 | 125 | _.forEach(pieceLocations, (moves, piece) => { 126 | let prev = moves.shift(); 127 | 128 | moves.filter(move => ! /captured/.test(move)).forEach(move => { 129 | if( ! this.survivalData[piece] ) { 130 | this.survivalData[piece] = {}; 131 | } 132 | 133 | if( this.survivalData[piece][`${prev}-${move}`] ) { 134 | this.survivalData[piece][`${prev}-${move}`]++; 135 | } else { 136 | this.survivalData[piece][`${prev}-${move}`] = 1; 137 | } 138 | 139 | prev = move; 140 | }); 141 | }); 142 | } 143 | } 144 | 145 | module.exports = Moves; -------------------------------------------------------------------------------- /scripts/lib/Openings.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | let _ = require('lodash'); 4 | 5 | class Openings { 6 | constructor() { 7 | this.data = { 8 | san: 'start', 9 | children: [] 10 | }; 11 | } 12 | 13 | update(moves) { 14 | let ref = this.data.children; 15 | 16 | moves.forEach(move => { 17 | let child = _.find(ref, {san: move}); 18 | if( child ) { 19 | child.count++; 20 | } else { 21 | child = { 22 | san: move, 23 | count: 1, 24 | children: [] 25 | }; 26 | 27 | ref.push(child); 28 | } 29 | 30 | ref = child.children; 31 | }); 32 | } 33 | } 34 | 35 | module.exports = Openings; -------------------------------------------------------------------------------- /scripts/scraper.js: -------------------------------------------------------------------------------- 1 | /*eslint no-console: 0*/ 2 | /*eslint no-unused-vars: 0*/ 3 | 4 | 'use strict'; 5 | 6 | var debug = require('debug')('scraper'); 7 | 8 | var phantom = require('phantom'); 9 | var Promise = require('bluebird'); 10 | var colors = require('colors'); 11 | var progress = require('progress'); 12 | var request = require('request'); 13 | var fs = Promise.promisifyAll(require('fs')); 14 | var mkdirp = Promise.promisify(require('mkdirp')); 15 | 16 | var args = require('commander'); 17 | args 18 | .version('1.0.0') 19 | .option('-t, --tournament ', 'Tournament Name (id)', 'shamkir-gashimov-memorial-2015') 20 | .option('-r, --rounds ', 'Number of rounds', 9, parseInt) 21 | .option('-m, --matches ', 'Number of matches per round', 1, parseInt) 22 | .option('-g, --games ', 'Number of games per match', 5, parseInt) 23 | .option('-c, --concurrency ', 'Concurrency', 5, parseInt) 24 | .option('-w, --wait ', 'Wait before scraping (ms)', 15000, parseInt) 25 | .parse(process.argv) 26 | ; 27 | 28 | var dataDir = 'data/'; 29 | var imgDir = 'data/img/players/'; 30 | 31 | console.log(' tournament'.cyan, args.tournament); 32 | console.log(' rounds'.cyan, args.rounds); 33 | console.log(' matches'.cyan, args.matches); 34 | console.log(' games'.cyan, args.games); 35 | console.log(' concurrency'.cyan, args.concurrency); 36 | console.log(' wait'.cyan, args.wait); 37 | 38 | console.log(' Starting scrape.'.cyan); 39 | 40 | var bar = new progress(' scraping [:bar] :percent', { 41 | total: args.rounds * args.matches * args.games 42 | }); 43 | 44 | var pages = []; 45 | 46 | for(let r=1; r<=args.rounds; r++) { 47 | for(let m=1; m<=args.matches; m++) { 48 | for(let g=1; g<=args.games; g++) { 49 | pages.push({r: r, m:m, g:g}); 50 | } 51 | } 52 | } 53 | 54 | Promise.map(pages, Promise.coroutine(function* (o) { 55 | var data = yield scrapePage(args.tournament, o.r, o.m, o.g); 56 | bar.tick(); 57 | return data; 58 | }), {concurrency: args.concurrency}) 59 | .then(function (scrapedData) { 60 | console.log(' Pages scraped, downloading player images.'.cyan); 61 | 62 | return Promise.map(scrapedData, Promise.coroutine(function* (scraped) { 63 | var game = scraped; 64 | console.log(scraped); 65 | game.whiteImg = yield requestPipeToFile(scraped.whiteImg, imgDir + scraped.white + '.jpg'); 66 | game.blackImg = yield requestPipeToFile(scraped.blackImg, imgDir + scraped.black + '.jpg'); 67 | 68 | return yield Promise.resolve(game); 69 | })); 70 | }) 71 | .then(function (games) { 72 | console.log(' Scrape finished.'.green); 73 | 74 | return [games, mkdirp(dataDir)]; 75 | }) 76 | .spread(function (games) { 77 | var json = JSON.stringify(games, null, 4); 78 | return fs.writeFileAsync('data/' + args.tournament + '.json', json); 79 | }) 80 | .then(function () { 81 | console.log(' All done.'.green); 82 | }); 83 | 84 | function scrapePage(tournament, round, match, game) { 85 | return new Promise(function (resolve, reject) { 86 | var url = 'https://chess24.com/en/embed-tournament/' + tournament + '/' + round + '/' + match + '/' + game; 87 | 88 | debug(url); 89 | 90 | phantom.create( 91 | function (ph) { 92 | ph.createPage(function (page) { 93 | page.open(url, function (status) { 94 | setTimeout(function () { 95 | page.evaluate(function () { 96 | /*global $*/ 97 | var notation = $('span[class^="notation-"]').map(function (i, notation) { 98 | return { 99 | move: $(notation).find('.move').text(), 100 | score: $(notation).find('.engine').text(), 101 | time: $(notation).find('.timeUsage').text() || '0s' 102 | }; 103 | }).get(); 104 | 105 | var winner = $('.playerInfo.white .score').text(); 106 | if( winner == '1' ) { 107 | winner = 'white'; 108 | } else if( winner == '0' ) { 109 | winner = 'black'; 110 | } else { 111 | winner = 'draw'; 112 | } 113 | 114 | var whiteImg = $('.playerInfo.white img').attr('src'); 115 | var blackImg = $('.playerInfo.black img').attr('src'); 116 | 117 | return { 118 | notation: notation, 119 | black: $('.playerInfo.black .name').text(), 120 | blackElo: $('.playerInfo.black .elo').text(), 121 | white: $('.playerInfo.white .name').text(), 122 | whiteElo: $('.playerInfo.white .elo').text(), 123 | winner: winner, 124 | whiteImg: whiteImg, 125 | blackImg: blackImg 126 | }; 127 | }, function (scraped) { 128 | scraped.tournament = tournament; 129 | scraped.round = round; 130 | scraped.match = match; 131 | scraped.game = game; 132 | 133 | ph.exit(); 134 | resolve(scraped); 135 | }); 136 | }, args.wait); 137 | }); 138 | }); 139 | }, 140 | { 141 | dnodeOpts: { 142 | weak: false 143 | } 144 | } 145 | ); 146 | }); 147 | } 148 | 149 | function requestPipeToFile(url, filepath) { 150 | console.log('req', url, filepath); 151 | return fs.statAsync(filepath) 152 | .then(function () { 153 | return filepath; 154 | }) 155 | .catch(function () { 156 | return mkdirp(imgDir) 157 | .then(function () { 158 | return new Promise(function (resolve, reject) { 159 | var stream = fs.createWriteStream(filepath) 160 | .on('finish', function () { 161 | return resolve(filepath); 162 | }) 163 | .on('error', reject); 164 | 165 | request({ 166 | url: url, 167 | gzip: true, 168 | encoding: null 169 | }) 170 | .on('error', reject) 171 | .pipe(stream); 172 | }); 173 | }); 174 | }) 175 | .finally(function () { 176 | return filepath; 177 | }); 178 | } -------------------------------------------------------------------------------- /scripts/stats.js: -------------------------------------------------------------------------------- 1 | /*eslint no-console: 0*/ 2 | /*eslint no-unused-vars: 0*/ 3 | 4 | 'use strict'; 5 | 6 | const debug = require('debug')('stats'); 7 | 8 | const Promise = require('bluebird'); 9 | const fs = Promise.promisifyAll(require('fs')); 10 | const _ = require('lodash'); 11 | const highland = require('highland'); 12 | 13 | const minimist = require('minimist'); 14 | const colors = require('colors'); 15 | 16 | const argv = minimist(process.argv.slice(2), { 17 | alias: { 18 | file: 'f' 19 | }, 20 | default: { 21 | file: 'data/new.pgn' 22 | } 23 | }); 24 | 25 | const Openings = require('./lib/Openings.js'); 26 | const Heatmaps = require('./lib/Heatmaps.js'); 27 | const Moves = require('./lib/Moves.js'); 28 | 29 | console.log(' source file'.cyan, argv.file); 30 | 31 | let heatmaps = new Heatmaps(); 32 | let openings = new Openings(); 33 | let moves = new Moves(); 34 | 35 | let start = new Date(); 36 | let numGames = 0; 37 | 38 | debug(argv); 39 | 40 | //precompile regexes for performance 41 | //not sure if this makes that much difference, but seems to help 42 | const splitBy = /\r?\n\r?\n(?=\[)/g; 43 | const r1 = /\[SetUp|FEN\]/; 44 | const r2 = /^(\[(.|\r?\n)*\])(\r?\n)*1.(\r?\n|.)*$/g; 45 | const r3 = /\r?\n/g; 46 | const r4 = /(\{[^}]+\})+?/g; 47 | const r5 = /\d+\./g; 48 | const r6 = /\.\.\./g; 49 | const r7 = /\*/g; 50 | const r8 = /(1\-0)?(0\-1)?(1\/2\-1\/2)?/g; 51 | const r9 = /\s+/; 52 | const r10 = /,,+/g; 53 | 54 | highland(fs.createReadStream(argv.file, {encoding: 'utf8'})) 55 | .splitBy(splitBy) //split pgns with multiple games by game 56 | .map(file => { 57 | //dont bother with Set-up games 58 | if( r1.test(file) ) { 59 | return; 60 | } 61 | 62 | //regexes stolen from chess.js 63 | let gameMoves = file 64 | .replace(file.replace(r2, '$1'), '') //strip away header text 65 | .replace(r3, ' ') //join multiple lines 66 | .replace(r4, '') //remove comments 67 | .replace(r5, '') //remove move numbers 68 | .replace(r6, '') //remove ... 69 | .replace(r7, '') //remove * 70 | .replace(r8, '') //remove results 71 | .trim() 72 | .split(r9) //split by space 73 | //get rid of empty moves 74 | .join(',') 75 | .replace(r10, ',') 76 | .split(','); 77 | 78 | if( gameMoves.length < 2 ) { 79 | //ignore super short games 80 | return; 81 | } 82 | 83 | heatmaps.update(gameMoves); 84 | openings.update(_.take(gameMoves, 7)); 85 | moves.update(gameMoves); 86 | numGames++; 87 | }) 88 | .done(() => { 89 | fs.writeFileAsync(argv.file.split('.pgn')[0] + '_stats.json', JSON.stringify({ 90 | heatmaps: heatmaps.data, 91 | openings: openings.data, 92 | moves: moves.survivalData 93 | }, null, 4)) 94 | .then(() => { 95 | console.log(' done, took'.cyan, new Date().getTime() - start.getTime(), 'ms'.cyan); 96 | console.log(' processed games:', numGames); 97 | }); 98 | }); -------------------------------------------------------------------------------- /src/js/ChessDataViz.js: -------------------------------------------------------------------------------- 1 | import * as util from './util'; 2 | import {EvalAndTime} from './EvalAndTime'; 3 | import {HeatMap} from './HeatMap'; 4 | import {Openings} from './Openings'; 5 | import {MovePaths} from './MovePaths'; 6 | 7 | export var ChessDataViz = { 8 | EvalAndTime, 9 | HeatMap, 10 | Openings, 11 | MovePaths, 12 | util 13 | }; 14 | 15 | window.ChessDataViz = ChessDataViz; -------------------------------------------------------------------------------- /src/js/EvalAndTime.js: -------------------------------------------------------------------------------- 1 | /*global d3*/ 2 | /*eslint no-unused-vars: 0*/ 3 | 4 | import _ from 'lodash'; 5 | import debug from 'debug'; 6 | 7 | let log = debug('cdv:EvalAndTime'); 8 | 9 | export class EvalAndTime { 10 | constructor(selector, options, data) { 11 | let self = this; 12 | 13 | //container 14 | this.container = d3.select(selector); 15 | 16 | //options 17 | let defaultOptions = { 18 | width: 960, 19 | height: 500, 20 | margin: { 21 | top: 40, 22 | right: 40, 23 | bottom: 20, 24 | left: 40 25 | }, 26 | interactive: true 27 | }; 28 | 29 | options = options || {}; 30 | 31 | this._options = _.merge({}, defaultOptions, options); 32 | 33 | this._width = this._options.width - this._options.margin.left - this._options.margin.right; 34 | this._height = this._options.height - this._options.margin.top - this._options.margin.bottom; 35 | 36 | //event dispatcher 37 | this.dispatch = d3.dispatch('mouseenter', 'mousemove', 'mouseleave'); 38 | 39 | //scales 40 | this._xScale = d3.scale.ordinal() 41 | .rangeBands([0, this._width], 0.1, 0) 42 | ; 43 | 44 | this._yEvalScale = d3.scale.linear() 45 | .range([this._height, 0]) 46 | .domain([-5, 5]) 47 | .clamp(true) 48 | ; 49 | 50 | this._yTimeScale = d3.scale.linear() 51 | .range([this._height, 0]) 52 | ; 53 | 54 | //axes 55 | this._xAxis = d3.svg.axis() 56 | .scale(this._xScale) 57 | .orient('top') 58 | .tickFormat((d) => d + 1) 59 | ; 60 | 61 | this._yEvalAxis = d3.svg.axis() 62 | .scale(this._yEvalScale) 63 | .orient('left') 64 | ; 65 | 66 | this._yTimeAxis = d3.svg.axis() 67 | .scale(this._yTimeScale) 68 | .orient('right') 69 | .tickFormat(d => Math.abs(d)) 70 | ; 71 | 72 | //clear element 73 | this.container.selectAll('*').remove(); 74 | 75 | //root of the graph 76 | let root = this.container.append('svg') 77 | .attr('class', 'graph') 78 | .attr('width', this._width + this._options.margin.left + this._options.margin.right) 79 | .attr('height', this._height + this._options.margin.top + this._options.margin.bottom) 80 | ; 81 | 82 | //margins applied 83 | let svg = root.append('g') 84 | .attr('transform', 'translate(' + this._options.margin.left + ',' + this._options.margin.top + ')') 85 | ; 86 | 87 | //xAxis 88 | svg 89 | .append('g') 90 | .attr('class', 'axis x') 91 | .call(this._xAxis) 92 | .append('text') 93 | .attr('text-anchor', 'middle') 94 | .attr('transform', 'translate(' + this._width / 2 + ',-25)') 95 | .text('moves (ply)') 96 | .attr('class', 'axis-label') 97 | ; 98 | 99 | //yEvalAxis 100 | svg 101 | .append('g') 102 | .attr('class', 'axis yEval') 103 | .call(this._yEvalAxis) 104 | .append('text') 105 | .attr('text-anchor', 'middle') 106 | .attr('transform', 'rotate(-90) translate(' + -this._yEvalScale(0) + ',-25)') 107 | .text('area: evaluation (pawns)') 108 | .attr('class', 'axis-label') 109 | ; 110 | 111 | //yTimeAxis 112 | svg 113 | .append('g') 114 | .attr('class', 'axis yTime') 115 | .attr('transform', 'translate(' + this._width + ',0)') 116 | .call(this._yTimeAxis) 117 | .append('text') 118 | .attr('text-anchor', 'middle') 119 | .attr('transform', 'rotate(-90) translate(' + -this._yEvalScale(0) + ',35)') 120 | .text('bars: move time (minutes)') 121 | .attr('class', 'axis-label') 122 | ; 123 | 124 | //eval guide lines 125 | let evalGuides = svg.append('g') 126 | .attr('class', 'eval-guides') 127 | ; 128 | 129 | let evalGuideLines = [ 130 | 0.5, -0.5, 131 | 1, -1, 132 | 2, -2 133 | ]; 134 | 135 | let evalGuideTexts = [ 136 | {y: 0.5, dy: 10, text: 'equal'}, 137 | 138 | {y: 1, dy: 10, text: 'white is slightly better'}, 139 | {y: 2, dy: 10, text: 'white is much better'}, 140 | {y: 2, dy: -5, text: 'white is winning'}, 141 | 142 | {y: -0.5, dy: 10, text: 'black is slightly better'}, 143 | {y: -1, dy: 10, text: 'black is much better'}, 144 | {y: -2, dy: 10, text: 'black is winning'} 145 | ]; 146 | 147 | evalGuides.selectAll('.eval-guide-line') 148 | .data(evalGuideLines).enter() 149 | .append('line') 150 | .attr('x1', 0) 151 | .attr('y1', d => this._yEvalScale(d)) 152 | .attr('x2', this._width) 153 | .attr('y2', d => this._yEvalScale(d)) 154 | .attr('class', 'eval-guide-line') 155 | ; 156 | 157 | evalGuides.selectAll('.eval-guide-text') 158 | .data(evalGuideTexts).enter() 159 | .append('text') 160 | .attr('transform', d => { 161 | let offset = d.dy ? d.dy : 0; 162 | return 'translate(5,' + (this._yEvalScale(d.y) + offset) + ')'; 163 | }) 164 | .text(d => d.text) 165 | .attr('class', 'eval-guide-text') 166 | ; 167 | 168 | //bars group 169 | svg.append('g') 170 | .attr('class', 'bars') 171 | ; 172 | 173 | //clip paths 174 | svg.append('clipPath') 175 | .attr('id', 'clip-white') 176 | .append('rect') 177 | .attr('width', this._width) 178 | .attr('height', this._height / 2) 179 | ; 180 | 181 | svg.append('clipPath') 182 | .attr('id', 'clip-black') 183 | .append('rect') 184 | .attr('y', this._height / 2) 185 | .attr('width', this._width) 186 | .attr('height', this._height / 2) 187 | ; 188 | 189 | //lines group 190 | svg.append('g') 191 | .attr('class', 'lines') 192 | ; 193 | 194 | //areas group 195 | svg.append('g') 196 | .attr('class', 'areas') 197 | ; 198 | 199 | //interactive layer group (for mouse hover stuff) 200 | let interactiveLayer = svg.append('g') 201 | .attr('class', 'interactive-layer') 202 | ; 203 | 204 | //interactive guidelines for axes 205 | interactiveLayer.selectAll('.guide') 206 | .data(['x', 'yEval', 'yTime']).enter() 207 | .append('line') 208 | .attr('x1', 0) 209 | .attr('y1', 0) 210 | .attr('x2', 0) 211 | .attr('y2', 0) 212 | .attr('class', d => `guide ${d}-guide`) 213 | ; 214 | 215 | //invisible rect to absorb mouse move events 216 | interactiveLayer.append('rect') 217 | .attr('width', this._width) 218 | .attr('height', this._height) 219 | .attr('pointer-events', 'all') 220 | .attr('class', 'mouse-absorb') 221 | .on('mouseenter', () => { 222 | this.dispatch.mouseenter(); 223 | }) 224 | .on('mousemove', function () { 225 | /*eslint no-empty: 0*/ 226 | 227 | //disregard if options.interactive is off 228 | if( ! self._options.interactive ) return; 229 | 230 | //calculate which point index the mouse is on 231 | let mouseX = d3.mouse(this)[0]; 232 | let leftEdges = self._xScale.range(); 233 | let width = self._xScale.rangeBand(); 234 | let j; 235 | for(j=0; mouseX > (leftEdges[j] + width); j++) {} 236 | 237 | //convert it to the range on screen 238 | let xPoint = self._xScale.domain()[j]; 239 | let xPosition = self._xScale(xPoint) + (self._xScale.rangeBand() / 2); 240 | 241 | if( ! xPosition ) return; 242 | 243 | //draw x axis guide 244 | interactiveLayer.select('.x-guide') 245 | .classed('hidden', false) 246 | .attr('x1', xPosition) 247 | .attr('x2', xPosition) 248 | .attr('y1', -6) 249 | .attr('y2', self._height) 250 | ; 251 | 252 | //draw yEval guide 253 | let yPosition = self._yEvalScale(self._data[xPoint].score); 254 | 255 | interactiveLayer.select('.yEval-guide') 256 | .classed('hidden', false) 257 | .attr('x1', -6) 258 | .attr('x2', xPosition) 259 | .transition().duration(100) 260 | .attr('y1', yPosition) 261 | .attr('y2', yPosition) 262 | ; 263 | 264 | //draw yTime guide 265 | yPosition = self._yTimeScale(self._data[xPoint].time); 266 | 267 | interactiveLayer.select('.yTime-guide') 268 | .classed('hidden', false) 269 | .attr('x1', xPosition) 270 | .attr('x2', self._width + 6) 271 | .transition().duration(100) 272 | .attr('y1', yPosition) 273 | .attr('y2', yPosition) 274 | ; 275 | 276 | self.dispatch.mousemove(self._data[xPoint]); 277 | }) 278 | .on('mouseleave', function () { 279 | //disregard if options.interactive is off 280 | if( ! self._options.interactive ) return; 281 | 282 | //hide guidelines on mouseleave 283 | interactiveLayer.selectAll('.interactive-layer .guide') 284 | .classed('hidden', true) 285 | ; 286 | 287 | self.dispatch.mouseleave(); 288 | }) 289 | ; 290 | 291 | if( data ) { 292 | this.data(data); 293 | } 294 | } 295 | 296 | data(data) { 297 | this._data = data; 298 | 299 | this.update(); 300 | } 301 | 302 | options(options) { 303 | let omit = [ 304 | 'width', 305 | 'margin', 306 | 'boardWidth', 307 | 'squareWidth' 308 | ]; 309 | 310 | _.merge(this._options, _.omit(options, omit)); 311 | 312 | if( _.isArray(this._options.colorScale) ) { 313 | this._scale.color.range(this._options.colorScale); 314 | } 315 | 316 | this.update(); 317 | } 318 | 319 | update() { 320 | //set scale domains 321 | this._xScale.domain(d3.range(this._data.length)); 322 | 323 | let y2max = d3.max( 324 | d3.extent(this._data, d => d.time).map(Math.abs) 325 | ); 326 | this._yTimeScale.domain([-y2max, y2max]); 327 | 328 | //only show every 10th move tick on x-axis 329 | this._xAxis.tickValues( 330 | this._xScale.domain().filter((d, i) => i == 0 || ! ((i + 1) % 10)) 331 | ); 332 | 333 | //line generator 334 | let line = d3.svg.line() 335 | .x((d, i) => this._xScale(i) + (this._xScale.rangeBand() / 2)) 336 | .y((d) => this._yEvalScale(d.score)) 337 | ; 338 | 339 | //area generator 340 | let area = d3.svg.area() 341 | .x(line.x()) 342 | .y1(line.y()) 343 | .y0(this._yEvalScale(0)) 344 | ; 345 | 346 | //axes 347 | let axes = this.container.transition(); 348 | 349 | axes.select('g.axis.x') 350 | .call(this._xAxis) 351 | ; 352 | axes.select('g.axis.yTime') 353 | .call(this._yTimeAxis) 354 | ; 355 | axes.select('g.axis.yEval') 356 | .call(this._yEvalAxis) 357 | ; 358 | 359 | //bars 360 | 361 | //join 362 | let bars = this.container.select('.bars') 363 | .selectAll('.bar') 364 | .data(this._data) 365 | ; 366 | 367 | //enter 368 | bars.enter() 369 | .append('rect') 370 | .attr('height', 0) 371 | .attr('y', this._yTimeScale(0)) 372 | ; 373 | 374 | //update + enter 375 | bars 376 | .transition() 377 | .delay((d, i) => i / this._xScale.domain().length * 500) 378 | .attr('x',(d, i) => this._xScale(i)) 379 | .attr('width', this._xScale.rangeBand()) 380 | .attr('y', d => { 381 | if( d.time > 0 ) { 382 | return this._yTimeScale(d.time); 383 | } else { 384 | return this._yTimeScale(0); 385 | } 386 | }) 387 | .attr('height', d => { 388 | if( d.time > 0 ) { 389 | return this._yTimeScale(0) - this._yTimeScale(d.time); 390 | } else { 391 | return this._yTimeScale(d.time) - this._yTimeScale(0); 392 | } 393 | }) 394 | .attr('class', (d, i) => 'bar ' + (i % 2 ? 'black' : 'white')) 395 | ; 396 | 397 | //exit 398 | bars.exit() 399 | .transition() 400 | .delay((d, i) => i / this._xScale.domain().length * 500) 401 | .attr('height', 0) 402 | .remove() 403 | ; 404 | 405 | //lines 406 | let lines = this.container.select('.lines') 407 | .selectAll('.line') 408 | ; 409 | 410 | //enter 411 | lines 412 | .data(['white', 'black']).enter() 413 | .append('path') 414 | .attr('class', (d) => `line ${d}`) 415 | .attr('clip-path', (d) => `url(#clip-${d})`) 416 | .datum(this._data) 417 | .attr('d', line) 418 | ; 419 | 420 | //update + enter 421 | lines 422 | .datum(this._data) 423 | .transition() 424 | .attr('d', line) 425 | ; 426 | 427 | //areas 428 | let areas = this.container.select('.areas') 429 | .selectAll('.area') 430 | ; 431 | 432 | //enter 433 | areas 434 | .data(['white', 'black']).enter() 435 | .append('path') 436 | .attr('class', (d) => `area ${d}`) 437 | .attr('clip-path', (d) => `url(#clip-${d})`) 438 | .datum(this._data) 439 | .attr('d', area) 440 | ; 441 | 442 | //update + enter 443 | areas 444 | .datum(this._data) 445 | .transition() 446 | .attr('d', area) 447 | ; 448 | } 449 | } -------------------------------------------------------------------------------- /src/js/HeatMap.js: -------------------------------------------------------------------------------- 1 | /*global d3*/ 2 | /*eslint no-unused-vars: 0*/ 3 | 4 | import _ from 'lodash'; 5 | import debug from 'debug'; 6 | import * as util from './util'; 7 | 8 | let log = debug('cdv:HeatMap'); 9 | 10 | export class HeatMap { 11 | constructor(selector, options, data) { 12 | //container setup 13 | this.container = d3.select(selector); 14 | 15 | //options 16 | let defaultOptions = { 17 | width: 500, 18 | margin: 20, 19 | accessor: { 20 | piece: 'all', 21 | color: 'w' 22 | }, 23 | sizeScale: true, 24 | colorScale: false 25 | }; 26 | 27 | options = options || {}; 28 | this._options = _.merge({}, defaultOptions, options); 29 | 30 | this._options.boardWidth = this._options.width - this._options.margin * 2; 31 | this._options.squareWidth = Math.floor(this._options.boardWidth / 8); 32 | 33 | //event dispatcher 34 | this.dispatch = d3.dispatch('mouseenter', 'mousemove', 'mouseleave'); 35 | 36 | //scales 37 | this._scale = { 38 | size: d3.scale.linear().range([0, this._options.squareWidth]), 39 | color: d3.scale.linear().range(['blue', 'red']) 40 | }; 41 | 42 | if( _.isArray(this._options.colorScale) ) { 43 | this._scale.color.range(this._options.colorScale); 44 | } 45 | 46 | //clear element 47 | this.container.selectAll('*').remove(); 48 | 49 | //root svg 50 | let root = this.container.append('svg') 51 | .attr('width', this._options.width + 'px') 52 | .attr('height', this._options.width + 'px') 53 | .attr('class', 'graph') 54 | ; 55 | 56 | //margins applied 57 | let svg = root.append('g') 58 | .attr('transform', 'translate(' + this._options.margin + ',' + this._options.margin + ')') 59 | .attr('class', 'board') 60 | ; 61 | 62 | util.drawBoard(svg, this._options.squareWidth); 63 | 64 | //container for heatmap data 65 | this.dataContainer = svg.append('g') 66 | .attr('class', 'data-container') 67 | ; 68 | 69 | if( data ) { 70 | this.data(data); 71 | } 72 | } 73 | 74 | data(data) { 75 | this._data = data; 76 | 77 | this.update(); 78 | } 79 | 80 | options(options) { 81 | let omit = [ 82 | 'width', 83 | 'margin', 84 | 'boardWidth', 85 | 'squareWidth' 86 | ]; 87 | 88 | _.merge(this._options, _.omit(options, omit)); 89 | 90 | if( _.isArray(this._options.colorScale) ) { 91 | this._scale.color.range(this._options.colorScale); 92 | } 93 | 94 | this.update(); 95 | } 96 | 97 | update() { 98 | let self = this; 99 | 100 | //adjust scales 101 | let extent = d3.extent(this._data, (d) => d[this._options.accessor.piece][this._options.accessor.color]); 102 | 103 | this._scale.size.domain(extent); 104 | this._scale.color.domain(extent); 105 | 106 | //update heat squares 107 | let heatSquares = this.dataContainer 108 | .selectAll('.heat-square').data(this._data) 109 | ; 110 | 111 | //enter 112 | heatSquares.enter() 113 | .append('rect') 114 | .attr('x', (d, i) => (i % 8 * this._options.squareWidth) + (this._options.squareWidth / 2)) 115 | .attr('y', (d, i) => (Math.floor(i / 8) * this._options.squareWidth) + (this._options.squareWidth / 2)) 116 | .attr('width', (d) => squareSize(d) + 'px') 117 | .attr('height', (d) => squareSize(d) + 'px') 118 | .attr('transform', (d) => { 119 | let halfWidth = squareSize(d) / 2; 120 | return 'translate(-' + halfWidth + ',-' + halfWidth + ')'; 121 | }) 122 | .attr('class', 'heat-square') 123 | .style('fill', (d, i) => squareColor(d)) 124 | .on('mouseenter', (d, i) => { 125 | this.dispatch.mouseenter(d[this._options.accessor.piece][this._options.accessor.color]); 126 | }) 127 | .on('mousemove', (d, i) => { 128 | this.dispatch.mousemove(); 129 | }) 130 | .on('mouseout', (d, i) => { 131 | this.dispatch.mouseleave(); 132 | }) 133 | ; 134 | 135 | //enter + update 136 | heatSquares.transition() 137 | .attr('width', (d) => squareSize(d) + 'px') 138 | .attr('height', (d) => squareSize(d) + 'px') 139 | .attr('transform', (d) => { 140 | let halfWidth = squareSize(d) / 2; 141 | return 'translate(-' + halfWidth + ',-' + halfWidth + ')'; 142 | }) 143 | .style('fill', (d, i) => squareColor(d)) 144 | ; 145 | 146 | function squareSize(d) { 147 | let size; 148 | 149 | if( self._options.sizeScale ) { 150 | size = self._scale.size(d[self._options.accessor.piece][self._options.accessor.color]); 151 | } else { 152 | size = self._options.squareWidth; 153 | } 154 | 155 | return size; 156 | } 157 | 158 | function squareColor(d) { 159 | let color; 160 | 161 | if( self._options.colorScale ) { 162 | color = self._scale.color(d[self._options.accessor.piece][self._options.accessor.color]); 163 | } 164 | 165 | return color; 166 | } 167 | } 168 | } -------------------------------------------------------------------------------- /src/js/MovePaths.js: -------------------------------------------------------------------------------- 1 | /*global d3*/ 2 | /*eslint no-unused-vars: 0*/ 3 | 4 | import _ from 'lodash'; 5 | import debug from 'debug'; 6 | import * as util from './util'; 7 | 8 | let log = debug('cdv:MovePaths'); 9 | 10 | export class MovePaths { 11 | constructor(selector, options, data) { 12 | //container setup 13 | this.container = d3.select(selector); 14 | 15 | //options 16 | let defaultOptions = { 17 | width: 500, 18 | margin: 20, 19 | accessor: 'Nb1', 20 | binSize: 1, 21 | pointRandomizer: d3.random.normal(3, 1), 22 | bezierRandomizer: d3.random.normal(12, 4), 23 | bezierScaleFactor: 2 24 | }; 25 | 26 | options = options || {}; 27 | this._options = _.merge({}, defaultOptions, options); 28 | 29 | this._options.boardWidth = this._options.width - this._options.margin * 2; 30 | this._options.squareWidth = Math.floor(this._options.boardWidth / 8); 31 | 32 | //clear element 33 | this.container.selectAll('*').remove(); 34 | 35 | //root svg 36 | let root = this.container.append('svg') 37 | .attr('width', this._options.width + 'px') 38 | .attr('height', this._options.width + 'px') 39 | .attr('class', 'graph') 40 | ; 41 | 42 | //margins applied 43 | let svg = root.append('g') 44 | .attr('transform', 'translate(' + this._options.margin + ',' + this._options.margin + ')') 45 | .attr('class', 'board') 46 | ; 47 | 48 | util.drawBoard(svg, this._options.squareWidth); 49 | 50 | //container for heatmap data 51 | this.dataContainer = svg.append('g') 52 | .attr('class', 'data-container') 53 | ; 54 | 55 | if( data ) { 56 | this.data(data); 57 | } 58 | } 59 | 60 | data(data) { 61 | this._data = data; 62 | 63 | this.update(); 64 | } 65 | 66 | options(options) { 67 | let omit = [ 68 | 'width', 69 | 'margin', 70 | 'boardWidth', 71 | 'squareWidth' 72 | ]; 73 | 74 | _.merge(this._options, _.omit(options, omit)); 75 | 76 | this.update(); 77 | } 78 | 79 | update() { 80 | let self = this; 81 | let data = []; 82 | 83 | _.pairs(this._data[this._options.accessor]).forEach(d => { 84 | let bin = Math.ceil(d[1] / this._options.binSize); 85 | 86 | for( let i = 0; i < bin; i++ ) { 87 | data.push(d[0]); 88 | } 89 | }); 90 | 91 | this.dataContainer.selectAll('.move-path').remove(); 92 | 93 | this.dataContainer.selectAll('.move-path').data(data) 94 | .enter().append('path') 95 | .attr('class', 'move-path') 96 | .attr('d', d => { 97 | //start and end points 98 | let [s, e] = getSquareCoords(d); 99 | 100 | //the orthogonal vector for vector [s, e] 101 | //used for the bezier control point 102 | let orthogonal = { 103 | x: -(e.y - s.y), 104 | y: e.x - s.x 105 | }; 106 | 107 | //get norm (magnitude) of orthogonal 108 | let norm = Math.sqrt(Math.pow(orthogonal.x, 2) + Math.pow(orthogonal.y, 2)); 109 | //scale factor to determine distance of control point from the end point 110 | let scaleFactor = Math.sqrt(Math.pow(e.x-s.x, 2) + Math.pow(e.y-s.y, 2)) / this._options.bezierScaleFactor; 111 | 112 | //transform the orthogonal vector 113 | orthogonal.x /= norm; 114 | orthogonal.y /= norm; 115 | 116 | orthogonal.x *= scaleFactor; 117 | orthogonal.y *= scaleFactor; 118 | 119 | let controlPoint; 120 | 121 | //determine which side the control point should be 122 | //with respect to the orthogonal vector 123 | if( e.x < s.x ) { 124 | controlPoint = { 125 | x: e.x + orthogonal.x, 126 | y: e.y + orthogonal.y 127 | }; 128 | } else { 129 | controlPoint = { 130 | x: e.x - orthogonal.x, 131 | y: e.y - orthogonal.y 132 | }; 133 | } 134 | 135 | //randomize the start, end and controlPoint a bit 136 | s.x += this._options.pointRandomizer(); 137 | s.y += this._options.pointRandomizer(); 138 | e.x += this._options.pointRandomizer(); 139 | e.y += this._options.pointRandomizer(); 140 | controlPoint.x += this._options.bezierRandomizer(); 141 | controlPoint.y += this._options.bezierRandomizer(); 142 | 143 | //construct the bezier curve 144 | let str = `M${s.x},${s.y}, Q${controlPoint.x},${controlPoint.y} ${e.x},${e.y}`; 145 | return str; 146 | }) 147 | ; 148 | 149 | //get coordinates of squares from keys such as "e2-e4" 150 | function getSquareCoords(d) { 151 | let squares = []; 152 | 153 | for( let i = 0; i < 2; i ++ ) { 154 | let square = d.split('-')[i].toLowerCase(); 155 | 156 | let file = square.charCodeAt(0) - 97; 157 | let rank = 8 - square[1]; 158 | 159 | let x = (file * self._options.squareWidth) + (self._options.squareWidth / 2); 160 | let y = (rank * self._options.squareWidth) + (self._options.squareWidth / 2); 161 | 162 | squares.push({ 163 | x, 164 | y 165 | }); 166 | } 167 | 168 | return squares; 169 | } 170 | } 171 | } -------------------------------------------------------------------------------- /src/js/Openings.js: -------------------------------------------------------------------------------- 1 | /*global d3*/ 2 | /*eslint no-unused-vars: 0*/ 3 | 4 | import debug from 'debug'; 5 | import _ from 'lodash'; 6 | 7 | let log = debug('cdv:Openings'); 8 | 9 | export class Openings { 10 | constructor(selector, options, data) { 11 | //container setup 12 | this.container = d3.select(selector); 13 | 14 | let defaultOptions = { 15 | width: 550, 16 | height: 550, 17 | colors: d3.scale.category10(), 18 | arcThreshold: 0.01, 19 | textThreshold: 0.1 20 | }; 21 | 22 | options = options || {}; 23 | this._options = _.merge({}, defaultOptions, options); 24 | 25 | //event dispatcher 26 | this.dispatch = d3.dispatch('mouseenter', 'mousemove', 'mouseleave'); 27 | 28 | this._partition = d3.layout.partition() 29 | .sort(null) 30 | .value(d => d.count) 31 | ; 32 | 33 | let radius = Math.min(this._options.width, this._options.height) / 2; 34 | 35 | let xScale = d3.scale.linear().range([0, 2 * Math.PI]); 36 | let yScale = d3.scale.sqrt().range([0, radius]); 37 | 38 | this._arc = d3.svg.arc() 39 | .startAngle(d => Math.max(0, Math.min(2 * Math.PI, xScale(d.x)))) 40 | .endAngle(d => Math.max(0, Math.min(2 * Math.PI, xScale(d.x + d.dx)))) 41 | .innerRadius(d => Math.max(0, yScale(d.y))) 42 | .outerRadius(d => Math.max(0, yScale(d.y + d.dy))) 43 | ; 44 | 45 | this.dataContainer = this.container.append('svg') 46 | .attr('width', this._options.width) 47 | .attr('height', this._options.height) 48 | .append('g') 49 | .attr('transform', 'translate(' + this._options.width / 2 + ',' + this._options.height / 2 + ')') 50 | ; 51 | 52 | if( data ) { 53 | this.data(data); 54 | } 55 | } 56 | 57 | data(data) { 58 | this._data = data; 59 | 60 | this.update(); 61 | } 62 | 63 | options(options) { 64 | let omit = [ 65 | 'width', 66 | 'height' 67 | ]; 68 | 69 | _.merge(this._options, _.omit(options, omit)); 70 | 71 | this.update(); 72 | } 73 | 74 | update() { 75 | let self = this; 76 | 77 | let nodes = this._partition.nodes(this._data).filter(d => d.dx > this._options.arcThreshold); 78 | 79 | let arcs = this.dataContainer.selectAll('.arc').data(nodes); 80 | 81 | arcs.enter() 82 | .append('path') 83 | .attr('display', d => d.depth ? null : 'none') 84 | .attr('d', this._arc) 85 | .attr('fill-rule', 'evenodd') 86 | .attr('class', 'arc') 87 | .each(function(d) { 88 | this.x0 = 0; 89 | this.dx0 = 0; 90 | }) 91 | .style('fill', fillColor) 92 | ; 93 | 94 | arcs 95 | .on('mouseenter', (d, i) => { 96 | let parents = getParents(d); 97 | 98 | arcs.style('opacity', 0.3); 99 | arcs.filter(node => parents.indexOf(node) > -1) 100 | .style('opacity', 1); 101 | 102 | let moves = _.pluck(parents, 'san'); 103 | this.dispatch.mouseenter(d, moves); 104 | }) 105 | .on('mousemove', () => { 106 | this.dispatch.mousemove(); 107 | }) 108 | .on('mouseleave', () => { 109 | arcs.style('opacity', 1); 110 | 111 | this.dispatch.mouseleave(); 112 | }) 113 | .transition().duration(500) 114 | .attrTween('d', function (d) { 115 | var interpolate = d3.interpolate({ 116 | x: this.x0, 117 | dx: this.dx0 118 | }, d); 119 | 120 | this.x0 = d.x; 121 | this.dx0 = d.dx; 122 | 123 | return function(t) { 124 | var b = interpolate(t); 125 | return self._arc(b); 126 | }; 127 | }) 128 | .style('fill', fillColor) 129 | ; 130 | 131 | arcs.exit().remove(); 132 | 133 | let sanText = this.dataContainer.selectAll('.san').data(nodes); 134 | sanText.enter() 135 | .append('text') 136 | .attr('class', 'san') 137 | .attr('dy', '6') 138 | .attr('text-anchor', 'middle') 139 | ; 140 | 141 | sanText.transition().duration(500) 142 | .attr('transform', d => 'translate(' + this._arc.centroid(d) + ')') 143 | .text(d => { 144 | if( d.dx < this._options.textThreshold ) return ''; 145 | 146 | return d.depth ? d.san : ''; 147 | }) 148 | ; 149 | 150 | sanText.exit().remove(); 151 | 152 | function fillColor(d, i) { 153 | if( i === 0 ) return; 154 | 155 | let rootParent = getParents(d)[0]; 156 | let color = d3.hsl(self._options.colors(rootParent.san)); 157 | 158 | if( d.depth % 2 === 0 ) { 159 | color = color.darker(0.5); 160 | } else { 161 | color = color.brighter(0.5); 162 | } 163 | 164 | color = color.darker(d.depth * 0.2); 165 | return color; 166 | } 167 | } 168 | } 169 | 170 | function getParents(node) { 171 | let path = []; 172 | let current = node; 173 | while( current.parent ) { 174 | path.unshift(current); 175 | current = current.parent; 176 | } 177 | 178 | return path; 179 | } -------------------------------------------------------------------------------- /src/js/util.js: -------------------------------------------------------------------------------- 1 | /*global d3*/ 2 | 3 | function parseMinutes(move, i) { 4 | let min = move.time.match(/(\d+)m/); 5 | min = min ? +min[1] : 0; 6 | 7 | let sec = move.time.match(/(\d+)s/); 8 | sec = sec ? +sec[1] : 0; 9 | 10 | let minutes = min + sec / 60; 11 | 12 | if( i % 2 ) { 13 | minutes = -minutes; 14 | } 15 | 16 | return minutes; 17 | } 18 | 19 | function parseScore(move) { 20 | let score = move.score; 21 | 22 | //mate notation 23 | if( score.match(/#/g) ) { 24 | score = score.replace('#', ''); 25 | //just make it a big number 26 | score = +score * 10; 27 | } else { 28 | score = +score; 29 | } 30 | 31 | return score; 32 | } 33 | 34 | export function parseGameNotation(notation) { 35 | notation.map((move, i) => move.time = parseMinutes(move, i)); 36 | notation.map((move) => move.score = parseScore(move)); 37 | 38 | return notation; 39 | } 40 | 41 | function boardSquares() { 42 | var squares = []; 43 | 44 | for( let i = 0; i < 64; i++ ) { 45 | squares.push({ 46 | x: i % 8, 47 | y: Math.floor(i / 8) 48 | }); 49 | } 50 | 51 | return squares; 52 | } 53 | 54 | export function isWhite(d) { 55 | return (! (d.x % 2) && ! (d.y % 2)) || (d.x % 2 && d.y % 2); 56 | } 57 | 58 | export function drawBoard(svg, squareWidth) { 59 | //board squares 60 | let board = boardSquares(); 61 | 62 | //create the g elements for squares 63 | let squares = svg.selectAll('.square').data(board).enter() 64 | .append('g') 65 | .attr('class', (d) => { 66 | let file = String.fromCharCode(97 + d.x); 67 | let rank = 8 - d.y; 68 | 69 | return 'square ' + file + rank; 70 | }) 71 | .classed('white', (d) => isWhite(d)) 72 | .classed('black', (d) => ! isWhite(d)) 73 | ; 74 | 75 | //create square elements for board squares 76 | squares.append('rect') 77 | .attr('x', (d) => d.x * squareWidth) 78 | .attr('y', (d) => d.y * squareWidth) 79 | .attr('width', squareWidth + 'px') 80 | .attr('height', squareWidth + 'px') 81 | .attr('class', 'sq') 82 | ; 83 | 84 | //labels among the A file 85 | let fileLabels = d3.range(8).map((i) => '.a' + (i + 1)); 86 | 87 | //file labels 88 | svg.selectAll(fileLabels) 89 | .append('text') 90 | .attr('x', (d) => d.x * squareWidth) 91 | .attr('y', (d) => d.y * squareWidth) 92 | .attr('dx', '0.2em') 93 | .attr('dy', '1em') 94 | .text((d) => 8 - d.y) 95 | .attr('class', 'label') 96 | ; 97 | 98 | //a-h labels for files 99 | let files = d3.range(8).map((i) => String.fromCharCode(97 + i)); 100 | let rankLabels = files.slice().map((file) => '.' + file + '1'); 101 | 102 | //rank labels 103 | svg.selectAll(rankLabels) 104 | .append('text') 105 | .attr('x', (d) => (d.x + 1) * squareWidth) 106 | .attr('y', (d) => (d.y + 1) * squareWidth) 107 | .attr('dx', '-0.3em') 108 | .attr('dy', '-0.5em') 109 | .attr('text-anchor', 'end') 110 | .text((d) => files[d.x]) 111 | .attr('class', 'label') 112 | ; 113 | } -------------------------------------------------------------------------------- /src/less/ChessDataViz.less: -------------------------------------------------------------------------------- 1 | .cdv-eval-time { 2 | @import 'EvalAndTime'; 3 | 4 | &.wrap { 5 | display: table; 6 | margin: 0 auto; 7 | text-align: center; 8 | } 9 | } 10 | 11 | .cdv-heatmap { 12 | @import 'HeatMap'; 13 | } 14 | 15 | .cdv-openings { 16 | @import 'Openings'; 17 | } 18 | 19 | .cdv-move-paths { 20 | @import 'MovePaths'; 21 | } -------------------------------------------------------------------------------- /src/less/EvalAndTime.less: -------------------------------------------------------------------------------- 1 | .graph { 2 | display: table-cell; 3 | vertical-align: middle; 4 | font-weight: 300; 5 | } 6 | 7 | .axis { 8 | font-size: 12px; 9 | font-weight: 400; 10 | 11 | path, line { 12 | fill: none; 13 | stroke: black; 14 | } 15 | 16 | .axis-label { 17 | fill: #777; 18 | } 19 | } 20 | 21 | .bar { 22 | opacity: 0.9; 23 | 24 | &.white { 25 | fill: white; 26 | } 27 | } 28 | 29 | .line { 30 | fill: none; 31 | stroke: black; 32 | stroke-width: 1.5px; 33 | 34 | &.white { 35 | stroke: white; 36 | } 37 | } 38 | .area { 39 | fill: black; 40 | opacity: 0.65; 41 | 42 | &.white { 43 | fill: white; 44 | } 45 | } 46 | 47 | .eval-guides { 48 | opacity: 0.75; 49 | } 50 | .eval-guide-line { 51 | stroke: black; 52 | fill: none; 53 | stroke-dasharray: 9 4; 54 | stroke-width: 0.5px; 55 | } 56 | .eval-guide-text { 57 | font-size: 10px; 58 | text-transform: uppercase; 59 | } 60 | 61 | .interactive-layer { 62 | fill: none; 63 | 64 | .guide { 65 | stroke: red; 66 | stroke-width: 1px; 67 | stroke-dasharray: 5 1; 68 | 69 | &.hidden { 70 | display: none; 71 | } 72 | } 73 | } -------------------------------------------------------------------------------- /src/less/HeatMap.less: -------------------------------------------------------------------------------- 1 | .white { 2 | rect { 3 | fill: white; 4 | } 5 | 6 | .label { 7 | fill: black; 8 | } 9 | } 10 | 11 | .black { 12 | rect { 13 | fill: black; 14 | } 15 | 16 | .label { 17 | fill: white; 18 | } 19 | } 20 | 21 | .label { 22 | text-transform: lowercase; 23 | font-family: sans-serif; 24 | font-size: 12px; 25 | } 26 | 27 | .heat-square { 28 | fill: red; 29 | opacity: 0.8; 30 | } -------------------------------------------------------------------------------- /src/less/MovePaths.less: -------------------------------------------------------------------------------- 1 | .white { 2 | rect { 3 | fill: white; 4 | } 5 | 6 | .label { 7 | fill: black; 8 | } 9 | } 10 | 11 | .black { 12 | rect { 13 | fill: black; 14 | } 15 | 16 | .label { 17 | fill: white; 18 | } 19 | } 20 | 21 | .label { 22 | text-transform: lowercase; 23 | font-family: sans-serif; 24 | font-size: 12px; 25 | } 26 | 27 | .move-path { 28 | fill: transparent; 29 | stroke: white; 30 | stroke-width: 1px; 31 | opacity: 0.1; 32 | } -------------------------------------------------------------------------------- /src/less/Openings.less: -------------------------------------------------------------------------------- 1 | .arc { 2 | stroke: #fff; 3 | stroke-width: 0.5; 4 | } 5 | 6 | .san { 7 | fill: #fff; 8 | font-size: 12px; 9 | pointer-events: none; 10 | } -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | 13 |

Eval and Time Graph

14 |
15 | 16 |

Heatmap

17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 |
34 | 40 | 41 |

Openings

42 |
43 | 44 |

Move Paths

45 |
46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 126 | 127 | --------------------------------------------------------------------------------