├── .github └── workflows │ └── build-and-deploy.yml ├── .gitignore ├── LICENSE ├── README.md ├── docs └── images │ ├── bmc-48.svg │ └── chart.png ├── index.html ├── package-lock.json ├── package.json ├── public ├── CNAME ├── browserconfig.xml ├── favicon.ico ├── favicon.svg ├── images │ └── icons │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-512x512.png │ │ ├── apple-touch-icon.png │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── mstile-144x144.png │ │ ├── mstile-150x150.png │ │ ├── mstile-310x150.png │ │ ├── mstile-310x310.png │ │ ├── mstile-70x70.png │ │ └── safari-pinned-tab.svg ├── manifest.webmanifest ├── robots.txt └── sitemap.xml ├── src ├── app.ts ├── chart-builder.ts ├── log-2-axis.ts └── main.ts └── tsconfig.json /.github/workflows/build-and-deploy.yml: -------------------------------------------------------------------------------- 1 | # Simple workflow for deploying static content to GitHub Pages 2 | name: Deploy static content to Pages 3 | 4 | on: 5 | # Runs on pushes targeting the default branch 6 | push: 7 | branches: ['main'] 8 | 9 | # Allows you to run this workflow manually from the Actions tab 10 | workflow_dispatch: 11 | 12 | # Sets the GITHUB_TOKEN permissions to allow deployment to GitHub Pages 13 | permissions: 14 | contents: read 15 | pages: write 16 | id-token: write 17 | 18 | # Allow one concurrent deployment 19 | concurrency: 20 | group: 'pages' 21 | cancel-in-progress: true 22 | 23 | jobs: 24 | # Single deploy job since we're just deploying 25 | deploy: 26 | environment: 27 | name: github-pages 28 | url: ${{ steps.deployment.outputs.page_url }} 29 | runs-on: ubuntu-latest 30 | steps: 31 | - name: Checkout 32 | uses: actions/checkout@v3 33 | - name: Set up Node 34 | uses: actions/setup-node@v3 35 | with: 36 | node-version: 18 37 | cache: 'npm' 38 | - name: Install dependencies 39 | run: npm install 40 | - name: Build 41 | run: npm run build 42 | - name: Setup Pages 43 | uses: actions/configure-pages@v3 44 | - name: Upload artifact 45 | uses: actions/upload-pages-artifact@v1 46 | with: 47 | # Upload dist repository 48 | path: './dist' 49 | - name: Deploy to GitHub Pages 50 | id: deployment 51 | uses: actions/deploy-pages@v1 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Carlos Villegas 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Charts for BenchmarkDotNet 2 | 3 | This [web app][ChartsForBenchmarkDotNet] allows you to create a visual representation of your [BenchmarkDotNet] console results. You can conveniently copy the generated chart to your clipboard, save it as a PNG image, or share it through a URL. 4 | 5 | It currently understands standard results like the one shown below. Any columns between `Method`/`Runtime` and `Mean` are considered categories in the chart across the x-axis. These columns are typically created via properties decorated with the [`Params`](https://benchmarkdotnet.org/articles/features/parameterization.html) attribute in your benchmark. 6 | 7 | **Console Results** 8 | ```ini 9 | BenchmarkDotNet=v0.13.2, OS=Windows 11 (10.0.22621.674) 10 | AMD Ryzen 9 6900HS Creator Edition, 1 CPU, 16 logical and 8 physical cores 11 | .NET SDK=6.0.401 12 | [Host] : .NET 6.0.9 (6.0.922.41905), X64 RyuJIT AVX2 13 | Job-MLMWYV : .NET 6.0.9 (6.0.922.41905), X64 RyuJIT AVX2 14 | 15 | Runtime=.NET 6.0 Server=True InvocationCount=32 16 | IterationCount=10 RunStrategy=Monitoring WarmupCount=1 17 | 18 | | Method | NumberOfValues | Mean | Error | StdDev | 19 | |---------------- |--------------- |------------:|----------:|----------:| 20 | | Int32ValuesXml | 32 | 1,031.3 us | 112.23 us | 74.23 us | 21 | | Int32ValuesJson | 32 | 683.1 us | 20.65 us | 13.66 us | 22 | | Int32ValuesXml | 64 | 1,289.4 us | 169.78 us | 112.30 us | 23 | | Int32ValuesJson | 64 | 710.6 us | 33.60 us | 22.23 us | 24 | | Int32ValuesXml | 128 | 1,717.3 us | 130.11 us | 86.06 us | 25 | | Int32ValuesJson | 128 | 705.6 us | 43.78 us | 28.96 us | 26 | | Int32ValuesXml | 256 | 2,619.3 us | 404.60 us | 267.62 us | 27 | | Int32ValuesJson | 256 | 780.0 us | 28.38 us | 18.77 us | 28 | | Int32ValuesXml | 512 | 4,712.5 us | 137.08 us | 90.67 us | 29 | | Int32ValuesJson | 512 | 919.8 us | 144.47 us | 95.56 us | 30 | | Int32ValuesXml | 1024 | 8,012.5 us | 214.29 us | 141.74 us | 31 | | Int32ValuesJson | 1024 | 1,266.1 us | 120.41 us | 79.65 us | 32 | | Int32ValuesXml | 2048 | 14,420.2 us | 261.61 us | 173.04 us | 33 | | Int32ValuesJson | 2048 | 1,673.8 us | 93.54 us | 61.87 us | 34 | | Int32ValuesXml | 4096 | 27,582.6 us | 249.89 us | 165.29 us | 35 | | Int32ValuesJson | 4096 | 2,732.6 us | 259.02 us | 171.33 us | 36 | ``` 37 | 38 | **Generated Chart** 39 | ![Generated Chart](/docs/images/chart.png) 40 | 41 | ## How do I use it? 42 | Just paste your [BenchmarkDotNet] console results into [the app][ChartsForBenchmarkDotNet]. The chart will be automatically generated. A few controls are provided above the chart, allowing you to customize the size, metric to display, y-axis scale, and theme. 43 | 44 | > 💡 You can also paste the content of the `.md` file created after running your benchmarks. It's typically stored in the following project directory: `bin\Release\[Runtime]\BenchmarkDotNet.Artifacts\results` 45 | 46 | ## Your Support is Appreciated! 47 | If you feel that this solution has provided you some value, please consider [buying me a ☕][BuyMeACoffee]. 48 | 49 | [![Buy me a coffee][BuyMeACoffeeButton]][BuyMeACoffee] 50 | 51 | Your ⭐ on [this repository][Repository] also helps! Thanks! 🖖🙂 52 | 53 | 54 | [ChartsForBenchmarkDotNet]: https://chartbenchmark.net/ 55 | [BenchmarkDotNet]: https://benchmarkdotnet.org/ 56 | [Repository]: https://github.com/yv989c/ChartsForBenchmarkDotNet 57 | [BuyMeACoffee]: https://www.buymeacoffee.com/yv989c 58 | [BuyMeACoffeeButton]: /docs/images/bmc-48.svg 59 | -------------------------------------------------------------------------------- /docs/images/bmc-48.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /docs/images/chart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yv989c/ChartsForBenchmarkDotNet/54c7c9e16a0eea71249a1b9b9950484e60950efd/docs/images/chart.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Charts for BenchmarkDotNet 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 22 | 23 | 57 | 58 | 61 | 62 | 63 | 64 | 65 | 66 |
67 |
68 |
69 | 83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |

Paste your console results below:

91 | 118 |

119 | (This information does not leave your browser.) 120 |

121 |
122 |
123 |
124 |
125 |
126 | 127 |
128 |
129 | Width 130 | 133 |
134 |
135 | Height 136 | 139 |
140 |
141 |
142 |
143 | 144 |
145 |
146 | 148 | 151 |
152 |
153 | 155 | 158 |
159 |
160 | 162 | 165 |
166 |
167 |
168 |
169 | 170 |
171 |
172 | 174 | 177 |
178 |
179 | 181 | 184 |
185 |
186 | 188 | 191 |
192 |
193 |
194 |
195 | 196 |
197 |
198 | 200 | 203 |
204 |
205 | 207 | 210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 | 218 |
219 |
220 |
221 | 228 | 235 |
236 | 244 | 248 |
249 |
250 |
251 |
252 |
253 | 266 |
267 | 268 | 272 | 273 | 275 | 276 | 277 | 278 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "charts-for-benchmarkdotnet", 3 | "version": "0.0.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "charts-for-benchmarkdotnet", 9 | "version": "0.0.0", 10 | "dependencies": { 11 | "chart.js": "^3.9.1", 12 | "lz-string": "^1.5.0" 13 | }, 14 | "devDependencies": { 15 | "typescript": "^5.0.2", 16 | "vite": "^4.3.2" 17 | } 18 | }, 19 | "node_modules/@esbuild/android-arm": { 20 | "version": "0.17.18", 21 | "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.18.tgz", 22 | "integrity": "sha512-EmwL+vUBZJ7mhFCs5lA4ZimpUH3WMAoqvOIYhVQwdIgSpHC8ImHdsRyhHAVxpDYUSm0lWvd63z0XH1IlImS2Qw==", 23 | "cpu": [ 24 | "arm" 25 | ], 26 | "dev": true, 27 | "optional": true, 28 | "os": [ 29 | "android" 30 | ], 31 | "engines": { 32 | "node": ">=12" 33 | } 34 | }, 35 | "node_modules/@esbuild/android-arm64": { 36 | "version": "0.17.18", 37 | "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.18.tgz", 38 | "integrity": "sha512-/iq0aK0eeHgSC3z55ucMAHO05OIqmQehiGay8eP5l/5l+iEr4EIbh4/MI8xD9qRFjqzgkc0JkX0LculNC9mXBw==", 39 | "cpu": [ 40 | "arm64" 41 | ], 42 | "dev": true, 43 | "optional": true, 44 | "os": [ 45 | "android" 46 | ], 47 | "engines": { 48 | "node": ">=12" 49 | } 50 | }, 51 | "node_modules/@esbuild/android-x64": { 52 | "version": "0.17.18", 53 | "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.18.tgz", 54 | "integrity": "sha512-x+0efYNBF3NPW2Xc5bFOSFW7tTXdAcpfEg2nXmxegm4mJuVeS+i109m/7HMiOQ6M12aVGGFlqJX3RhNdYM2lWg==", 55 | "cpu": [ 56 | "x64" 57 | ], 58 | "dev": true, 59 | "optional": true, 60 | "os": [ 61 | "android" 62 | ], 63 | "engines": { 64 | "node": ">=12" 65 | } 66 | }, 67 | "node_modules/@esbuild/darwin-arm64": { 68 | "version": "0.17.18", 69 | "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.18.tgz", 70 | "integrity": "sha512-6tY+djEAdF48M1ONWnQb1C+6LiXrKjmqjzPNPWXhu/GzOHTHX2nh8Mo2ZAmBFg0kIodHhciEgUBtcYCAIjGbjQ==", 71 | "cpu": [ 72 | "arm64" 73 | ], 74 | "dev": true, 75 | "optional": true, 76 | "os": [ 77 | "darwin" 78 | ], 79 | "engines": { 80 | "node": ">=12" 81 | } 82 | }, 83 | "node_modules/@esbuild/darwin-x64": { 84 | "version": "0.17.18", 85 | "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.18.tgz", 86 | "integrity": "sha512-Qq84ykvLvya3dO49wVC9FFCNUfSrQJLbxhoQk/TE1r6MjHo3sFF2tlJCwMjhkBVq3/ahUisj7+EpRSz0/+8+9A==", 87 | "cpu": [ 88 | "x64" 89 | ], 90 | "dev": true, 91 | "optional": true, 92 | "os": [ 93 | "darwin" 94 | ], 95 | "engines": { 96 | "node": ">=12" 97 | } 98 | }, 99 | "node_modules/@esbuild/freebsd-arm64": { 100 | "version": "0.17.18", 101 | "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.18.tgz", 102 | "integrity": "sha512-fw/ZfxfAzuHfaQeMDhbzxp9mc+mHn1Y94VDHFHjGvt2Uxl10mT4CDavHm+/L9KG441t1QdABqkVYwakMUeyLRA==", 103 | "cpu": [ 104 | "arm64" 105 | ], 106 | "dev": true, 107 | "optional": true, 108 | "os": [ 109 | "freebsd" 110 | ], 111 | "engines": { 112 | "node": ">=12" 113 | } 114 | }, 115 | "node_modules/@esbuild/freebsd-x64": { 116 | "version": "0.17.18", 117 | "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.18.tgz", 118 | "integrity": "sha512-FQFbRtTaEi8ZBi/A6kxOC0V0E9B/97vPdYjY9NdawyLd4Qk5VD5g2pbWN2VR1c0xhzcJm74HWpObPszWC+qTew==", 119 | "cpu": [ 120 | "x64" 121 | ], 122 | "dev": true, 123 | "optional": true, 124 | "os": [ 125 | "freebsd" 126 | ], 127 | "engines": { 128 | "node": ">=12" 129 | } 130 | }, 131 | "node_modules/@esbuild/linux-arm": { 132 | "version": "0.17.18", 133 | "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.18.tgz", 134 | "integrity": "sha512-jW+UCM40LzHcouIaqv3e/oRs0JM76JfhHjCavPxMUti7VAPh8CaGSlS7cmyrdpzSk7A+8f0hiedHqr/LMnfijg==", 135 | "cpu": [ 136 | "arm" 137 | ], 138 | "dev": true, 139 | "optional": true, 140 | "os": [ 141 | "linux" 142 | ], 143 | "engines": { 144 | "node": ">=12" 145 | } 146 | }, 147 | "node_modules/@esbuild/linux-arm64": { 148 | "version": "0.17.18", 149 | "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.18.tgz", 150 | "integrity": "sha512-R7pZvQZFOY2sxUG8P6A21eq6q+eBv7JPQYIybHVf1XkQYC+lT7nDBdC7wWKTrbvMXKRaGudp/dzZCwL/863mZQ==", 151 | "cpu": [ 152 | "arm64" 153 | ], 154 | "dev": true, 155 | "optional": true, 156 | "os": [ 157 | "linux" 158 | ], 159 | "engines": { 160 | "node": ">=12" 161 | } 162 | }, 163 | "node_modules/@esbuild/linux-ia32": { 164 | "version": "0.17.18", 165 | "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.18.tgz", 166 | "integrity": "sha512-ygIMc3I7wxgXIxk6j3V00VlABIjq260i967Cp9BNAk5pOOpIXmd1RFQJQX9Io7KRsthDrQYrtcx7QCof4o3ZoQ==", 167 | "cpu": [ 168 | "ia32" 169 | ], 170 | "dev": true, 171 | "optional": true, 172 | "os": [ 173 | "linux" 174 | ], 175 | "engines": { 176 | "node": ">=12" 177 | } 178 | }, 179 | "node_modules/@esbuild/linux-loong64": { 180 | "version": "0.17.18", 181 | "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.18.tgz", 182 | "integrity": "sha512-bvPG+MyFs5ZlwYclCG1D744oHk1Pv7j8psF5TfYx7otCVmcJsEXgFEhQkbhNW8otDHL1a2KDINW20cfCgnzgMQ==", 183 | "cpu": [ 184 | "loong64" 185 | ], 186 | "dev": true, 187 | "optional": true, 188 | "os": [ 189 | "linux" 190 | ], 191 | "engines": { 192 | "node": ">=12" 193 | } 194 | }, 195 | "node_modules/@esbuild/linux-mips64el": { 196 | "version": "0.17.18", 197 | "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.18.tgz", 198 | "integrity": "sha512-oVqckATOAGuiUOa6wr8TXaVPSa+6IwVJrGidmNZS1cZVx0HqkTMkqFGD2HIx9H1RvOwFeWYdaYbdY6B89KUMxA==", 199 | "cpu": [ 200 | "mips64el" 201 | ], 202 | "dev": true, 203 | "optional": true, 204 | "os": [ 205 | "linux" 206 | ], 207 | "engines": { 208 | "node": ">=12" 209 | } 210 | }, 211 | "node_modules/@esbuild/linux-ppc64": { 212 | "version": "0.17.18", 213 | "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.18.tgz", 214 | "integrity": "sha512-3dLlQO+b/LnQNxgH4l9rqa2/IwRJVN9u/bK63FhOPB4xqiRqlQAU0qDU3JJuf0BmaH0yytTBdoSBHrb2jqc5qQ==", 215 | "cpu": [ 216 | "ppc64" 217 | ], 218 | "dev": true, 219 | "optional": true, 220 | "os": [ 221 | "linux" 222 | ], 223 | "engines": { 224 | "node": ">=12" 225 | } 226 | }, 227 | "node_modules/@esbuild/linux-riscv64": { 228 | "version": "0.17.18", 229 | "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.18.tgz", 230 | "integrity": "sha512-/x7leOyDPjZV3TcsdfrSI107zItVnsX1q2nho7hbbQoKnmoeUWjs+08rKKt4AUXju7+3aRZSsKrJtaRmsdL1xA==", 231 | "cpu": [ 232 | "riscv64" 233 | ], 234 | "dev": true, 235 | "optional": true, 236 | "os": [ 237 | "linux" 238 | ], 239 | "engines": { 240 | "node": ">=12" 241 | } 242 | }, 243 | "node_modules/@esbuild/linux-s390x": { 244 | "version": "0.17.18", 245 | "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.18.tgz", 246 | "integrity": "sha512-cX0I8Q9xQkL/6F5zWdYmVf5JSQt+ZfZD2bJudZrWD+4mnUvoZ3TDDXtDX2mUaq6upMFv9FlfIh4Gfun0tbGzuw==", 247 | "cpu": [ 248 | "s390x" 249 | ], 250 | "dev": true, 251 | "optional": true, 252 | "os": [ 253 | "linux" 254 | ], 255 | "engines": { 256 | "node": ">=12" 257 | } 258 | }, 259 | "node_modules/@esbuild/linux-x64": { 260 | "version": "0.17.18", 261 | "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.18.tgz", 262 | "integrity": "sha512-66RmRsPlYy4jFl0vG80GcNRdirx4nVWAzJmXkevgphP1qf4dsLQCpSKGM3DUQCojwU1hnepI63gNZdrr02wHUA==", 263 | "cpu": [ 264 | "x64" 265 | ], 266 | "dev": true, 267 | "optional": true, 268 | "os": [ 269 | "linux" 270 | ], 271 | "engines": { 272 | "node": ">=12" 273 | } 274 | }, 275 | "node_modules/@esbuild/netbsd-x64": { 276 | "version": "0.17.18", 277 | "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.18.tgz", 278 | "integrity": "sha512-95IRY7mI2yrkLlTLb1gpDxdC5WLC5mZDi+kA9dmM5XAGxCME0F8i4bYH4jZreaJ6lIZ0B8hTrweqG1fUyW7jbg==", 279 | "cpu": [ 280 | "x64" 281 | ], 282 | "dev": true, 283 | "optional": true, 284 | "os": [ 285 | "netbsd" 286 | ], 287 | "engines": { 288 | "node": ">=12" 289 | } 290 | }, 291 | "node_modules/@esbuild/openbsd-x64": { 292 | "version": "0.17.18", 293 | "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.18.tgz", 294 | "integrity": "sha512-WevVOgcng+8hSZ4Q3BKL3n1xTv5H6Nb53cBrtzzEjDbbnOmucEVcZeGCsCOi9bAOcDYEeBZbD2SJNBxlfP3qiA==", 295 | "cpu": [ 296 | "x64" 297 | ], 298 | "dev": true, 299 | "optional": true, 300 | "os": [ 301 | "openbsd" 302 | ], 303 | "engines": { 304 | "node": ">=12" 305 | } 306 | }, 307 | "node_modules/@esbuild/sunos-x64": { 308 | "version": "0.17.18", 309 | "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.18.tgz", 310 | "integrity": "sha512-Rzf4QfQagnwhQXVBS3BYUlxmEbcV7MY+BH5vfDZekU5eYpcffHSyjU8T0xucKVuOcdCsMo+Ur5wmgQJH2GfNrg==", 311 | "cpu": [ 312 | "x64" 313 | ], 314 | "dev": true, 315 | "optional": true, 316 | "os": [ 317 | "sunos" 318 | ], 319 | "engines": { 320 | "node": ">=12" 321 | } 322 | }, 323 | "node_modules/@esbuild/win32-arm64": { 324 | "version": "0.17.18", 325 | "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.18.tgz", 326 | "integrity": "sha512-Kb3Ko/KKaWhjeAm2YoT/cNZaHaD1Yk/pa3FTsmqo9uFh1D1Rfco7BBLIPdDOozrObj2sahslFuAQGvWbgWldAg==", 327 | "cpu": [ 328 | "arm64" 329 | ], 330 | "dev": true, 331 | "optional": true, 332 | "os": [ 333 | "win32" 334 | ], 335 | "engines": { 336 | "node": ">=12" 337 | } 338 | }, 339 | "node_modules/@esbuild/win32-ia32": { 340 | "version": "0.17.18", 341 | "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.18.tgz", 342 | "integrity": "sha512-0/xUMIdkVHwkvxfbd5+lfG7mHOf2FRrxNbPiKWg9C4fFrB8H0guClmaM3BFiRUYrznVoyxTIyC/Ou2B7QQSwmw==", 343 | "cpu": [ 344 | "ia32" 345 | ], 346 | "dev": true, 347 | "optional": true, 348 | "os": [ 349 | "win32" 350 | ], 351 | "engines": { 352 | "node": ">=12" 353 | } 354 | }, 355 | "node_modules/@esbuild/win32-x64": { 356 | "version": "0.17.18", 357 | "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.18.tgz", 358 | "integrity": "sha512-qU25Ma1I3NqTSHJUOKi9sAH1/Mzuvlke0ioMJRthLXKm7JiSKVwFghlGbDLOO2sARECGhja4xYfRAZNPAkooYg==", 359 | "cpu": [ 360 | "x64" 361 | ], 362 | "dev": true, 363 | "optional": true, 364 | "os": [ 365 | "win32" 366 | ], 367 | "engines": { 368 | "node": ">=12" 369 | } 370 | }, 371 | "node_modules/chart.js": { 372 | "version": "3.9.1", 373 | "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.9.1.tgz", 374 | "integrity": "sha512-Ro2JbLmvg83gXF5F4sniaQ+lTbSv18E+TIf2cOeiH1Iqd2PGFOtem+DUufMZsCJwFE7ywPOpfXFBwRTGq7dh6w==" 375 | }, 376 | "node_modules/esbuild": { 377 | "version": "0.17.18", 378 | "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.18.tgz", 379 | "integrity": "sha512-z1lix43jBs6UKjcZVKOw2xx69ffE2aG0PygLL5qJ9OS/gy0Ewd1gW/PUQIOIQGXBHWNywSc0floSKoMFF8aK2w==", 380 | "dev": true, 381 | "hasInstallScript": true, 382 | "bin": { 383 | "esbuild": "bin/esbuild" 384 | }, 385 | "engines": { 386 | "node": ">=12" 387 | }, 388 | "optionalDependencies": { 389 | "@esbuild/android-arm": "0.17.18", 390 | "@esbuild/android-arm64": "0.17.18", 391 | "@esbuild/android-x64": "0.17.18", 392 | "@esbuild/darwin-arm64": "0.17.18", 393 | "@esbuild/darwin-x64": "0.17.18", 394 | "@esbuild/freebsd-arm64": "0.17.18", 395 | "@esbuild/freebsd-x64": "0.17.18", 396 | "@esbuild/linux-arm": "0.17.18", 397 | "@esbuild/linux-arm64": "0.17.18", 398 | "@esbuild/linux-ia32": "0.17.18", 399 | "@esbuild/linux-loong64": "0.17.18", 400 | "@esbuild/linux-mips64el": "0.17.18", 401 | "@esbuild/linux-ppc64": "0.17.18", 402 | "@esbuild/linux-riscv64": "0.17.18", 403 | "@esbuild/linux-s390x": "0.17.18", 404 | "@esbuild/linux-x64": "0.17.18", 405 | "@esbuild/netbsd-x64": "0.17.18", 406 | "@esbuild/openbsd-x64": "0.17.18", 407 | "@esbuild/sunos-x64": "0.17.18", 408 | "@esbuild/win32-arm64": "0.17.18", 409 | "@esbuild/win32-ia32": "0.17.18", 410 | "@esbuild/win32-x64": "0.17.18" 411 | } 412 | }, 413 | "node_modules/fsevents": { 414 | "version": "2.3.2", 415 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", 416 | "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", 417 | "dev": true, 418 | "hasInstallScript": true, 419 | "optional": true, 420 | "os": [ 421 | "darwin" 422 | ], 423 | "engines": { 424 | "node": "^8.16.0 || ^10.6.0 || >=11.0.0" 425 | } 426 | }, 427 | "node_modules/lz-string": { 428 | "version": "1.5.0", 429 | "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", 430 | "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", 431 | "bin": { 432 | "lz-string": "bin/bin.js" 433 | } 434 | }, 435 | "node_modules/nanoid": { 436 | "version": "3.3.6", 437 | "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", 438 | "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", 439 | "dev": true, 440 | "funding": [ 441 | { 442 | "type": "github", 443 | "url": "https://github.com/sponsors/ai" 444 | } 445 | ], 446 | "bin": { 447 | "nanoid": "bin/nanoid.cjs" 448 | }, 449 | "engines": { 450 | "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" 451 | } 452 | }, 453 | "node_modules/picocolors": { 454 | "version": "1.0.0", 455 | "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", 456 | "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", 457 | "dev": true 458 | }, 459 | "node_modules/postcss": { 460 | "version": "8.4.23", 461 | "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.23.tgz", 462 | "integrity": "sha512-bQ3qMcpF6A/YjR55xtoTr0jGOlnPOKAIMdOWiv0EIT6HVPEaJiJB4NLljSbiHoC2RX7DN5Uvjtpbg1NPdwv1oA==", 463 | "dev": true, 464 | "funding": [ 465 | { 466 | "type": "opencollective", 467 | "url": "https://opencollective.com/postcss/" 468 | }, 469 | { 470 | "type": "tidelift", 471 | "url": "https://tidelift.com/funding/github/npm/postcss" 472 | }, 473 | { 474 | "type": "github", 475 | "url": "https://github.com/sponsors/ai" 476 | } 477 | ], 478 | "dependencies": { 479 | "nanoid": "^3.3.6", 480 | "picocolors": "^1.0.0", 481 | "source-map-js": "^1.0.2" 482 | }, 483 | "engines": { 484 | "node": "^10 || ^12 || >=14" 485 | } 486 | }, 487 | "node_modules/rollup": { 488 | "version": "3.21.2", 489 | "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.21.2.tgz", 490 | "integrity": "sha512-c4vC+JZ3bbF4Kqq2TtM7zSKtSyMybFOjqmomFax3xpfYaPZDZ4iz8NMIuBRMjnXOcKYozw7bC6vhJjiWD6JpzQ==", 491 | "dev": true, 492 | "bin": { 493 | "rollup": "dist/bin/rollup" 494 | }, 495 | "engines": { 496 | "node": ">=14.18.0", 497 | "npm": ">=8.0.0" 498 | }, 499 | "optionalDependencies": { 500 | "fsevents": "~2.3.2" 501 | } 502 | }, 503 | "node_modules/source-map-js": { 504 | "version": "1.0.2", 505 | "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", 506 | "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", 507 | "dev": true, 508 | "engines": { 509 | "node": ">=0.10.0" 510 | } 511 | }, 512 | "node_modules/typescript": { 513 | "version": "5.0.4", 514 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz", 515 | "integrity": "sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==", 516 | "dev": true, 517 | "bin": { 518 | "tsc": "bin/tsc", 519 | "tsserver": "bin/tsserver" 520 | }, 521 | "engines": { 522 | "node": ">=12.20" 523 | } 524 | }, 525 | "node_modules/vite": { 526 | "version": "4.3.3", 527 | "resolved": "https://registry.npmjs.org/vite/-/vite-4.3.3.tgz", 528 | "integrity": "sha512-MwFlLBO4udZXd+VBcezo3u8mC77YQk+ik+fbc0GZWGgzfbPP+8Kf0fldhARqvSYmtIWoAJ5BXPClUbMTlqFxrA==", 529 | "dev": true, 530 | "dependencies": { 531 | "esbuild": "^0.17.5", 532 | "postcss": "^8.4.23", 533 | "rollup": "^3.21.0" 534 | }, 535 | "bin": { 536 | "vite": "bin/vite.js" 537 | }, 538 | "engines": { 539 | "node": "^14.18.0 || >=16.0.0" 540 | }, 541 | "optionalDependencies": { 542 | "fsevents": "~2.3.2" 543 | }, 544 | "peerDependencies": { 545 | "@types/node": ">= 14", 546 | "less": "*", 547 | "sass": "*", 548 | "stylus": "*", 549 | "sugarss": "*", 550 | "terser": "^5.4.0" 551 | }, 552 | "peerDependenciesMeta": { 553 | "@types/node": { 554 | "optional": true 555 | }, 556 | "less": { 557 | "optional": true 558 | }, 559 | "sass": { 560 | "optional": true 561 | }, 562 | "stylus": { 563 | "optional": true 564 | }, 565 | "sugarss": { 566 | "optional": true 567 | }, 568 | "terser": { 569 | "optional": true 570 | } 571 | } 572 | } 573 | }, 574 | "dependencies": { 575 | "@esbuild/android-arm": { 576 | "version": "0.17.18", 577 | "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.18.tgz", 578 | "integrity": "sha512-EmwL+vUBZJ7mhFCs5lA4ZimpUH3WMAoqvOIYhVQwdIgSpHC8ImHdsRyhHAVxpDYUSm0lWvd63z0XH1IlImS2Qw==", 579 | "dev": true, 580 | "optional": true 581 | }, 582 | "@esbuild/android-arm64": { 583 | "version": "0.17.18", 584 | "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.18.tgz", 585 | "integrity": "sha512-/iq0aK0eeHgSC3z55ucMAHO05OIqmQehiGay8eP5l/5l+iEr4EIbh4/MI8xD9qRFjqzgkc0JkX0LculNC9mXBw==", 586 | "dev": true, 587 | "optional": true 588 | }, 589 | "@esbuild/android-x64": { 590 | "version": "0.17.18", 591 | "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.18.tgz", 592 | "integrity": "sha512-x+0efYNBF3NPW2Xc5bFOSFW7tTXdAcpfEg2nXmxegm4mJuVeS+i109m/7HMiOQ6M12aVGGFlqJX3RhNdYM2lWg==", 593 | "dev": true, 594 | "optional": true 595 | }, 596 | "@esbuild/darwin-arm64": { 597 | "version": "0.17.18", 598 | "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.18.tgz", 599 | "integrity": "sha512-6tY+djEAdF48M1ONWnQb1C+6LiXrKjmqjzPNPWXhu/GzOHTHX2nh8Mo2ZAmBFg0kIodHhciEgUBtcYCAIjGbjQ==", 600 | "dev": true, 601 | "optional": true 602 | }, 603 | "@esbuild/darwin-x64": { 604 | "version": "0.17.18", 605 | "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.18.tgz", 606 | "integrity": "sha512-Qq84ykvLvya3dO49wVC9FFCNUfSrQJLbxhoQk/TE1r6MjHo3sFF2tlJCwMjhkBVq3/ahUisj7+EpRSz0/+8+9A==", 607 | "dev": true, 608 | "optional": true 609 | }, 610 | "@esbuild/freebsd-arm64": { 611 | "version": "0.17.18", 612 | "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.18.tgz", 613 | "integrity": "sha512-fw/ZfxfAzuHfaQeMDhbzxp9mc+mHn1Y94VDHFHjGvt2Uxl10mT4CDavHm+/L9KG441t1QdABqkVYwakMUeyLRA==", 614 | "dev": true, 615 | "optional": true 616 | }, 617 | "@esbuild/freebsd-x64": { 618 | "version": "0.17.18", 619 | "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.18.tgz", 620 | "integrity": "sha512-FQFbRtTaEi8ZBi/A6kxOC0V0E9B/97vPdYjY9NdawyLd4Qk5VD5g2pbWN2VR1c0xhzcJm74HWpObPszWC+qTew==", 621 | "dev": true, 622 | "optional": true 623 | }, 624 | "@esbuild/linux-arm": { 625 | "version": "0.17.18", 626 | "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.18.tgz", 627 | "integrity": "sha512-jW+UCM40LzHcouIaqv3e/oRs0JM76JfhHjCavPxMUti7VAPh8CaGSlS7cmyrdpzSk7A+8f0hiedHqr/LMnfijg==", 628 | "dev": true, 629 | "optional": true 630 | }, 631 | "@esbuild/linux-arm64": { 632 | "version": "0.17.18", 633 | "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.18.tgz", 634 | "integrity": "sha512-R7pZvQZFOY2sxUG8P6A21eq6q+eBv7JPQYIybHVf1XkQYC+lT7nDBdC7wWKTrbvMXKRaGudp/dzZCwL/863mZQ==", 635 | "dev": true, 636 | "optional": true 637 | }, 638 | "@esbuild/linux-ia32": { 639 | "version": "0.17.18", 640 | "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.18.tgz", 641 | "integrity": "sha512-ygIMc3I7wxgXIxk6j3V00VlABIjq260i967Cp9BNAk5pOOpIXmd1RFQJQX9Io7KRsthDrQYrtcx7QCof4o3ZoQ==", 642 | "dev": true, 643 | "optional": true 644 | }, 645 | "@esbuild/linux-loong64": { 646 | "version": "0.17.18", 647 | "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.18.tgz", 648 | "integrity": "sha512-bvPG+MyFs5ZlwYclCG1D744oHk1Pv7j8psF5TfYx7otCVmcJsEXgFEhQkbhNW8otDHL1a2KDINW20cfCgnzgMQ==", 649 | "dev": true, 650 | "optional": true 651 | }, 652 | "@esbuild/linux-mips64el": { 653 | "version": "0.17.18", 654 | "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.18.tgz", 655 | "integrity": "sha512-oVqckATOAGuiUOa6wr8TXaVPSa+6IwVJrGidmNZS1cZVx0HqkTMkqFGD2HIx9H1RvOwFeWYdaYbdY6B89KUMxA==", 656 | "dev": true, 657 | "optional": true 658 | }, 659 | "@esbuild/linux-ppc64": { 660 | "version": "0.17.18", 661 | "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.18.tgz", 662 | "integrity": "sha512-3dLlQO+b/LnQNxgH4l9rqa2/IwRJVN9u/bK63FhOPB4xqiRqlQAU0qDU3JJuf0BmaH0yytTBdoSBHrb2jqc5qQ==", 663 | "dev": true, 664 | "optional": true 665 | }, 666 | "@esbuild/linux-riscv64": { 667 | "version": "0.17.18", 668 | "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.18.tgz", 669 | "integrity": "sha512-/x7leOyDPjZV3TcsdfrSI107zItVnsX1q2nho7hbbQoKnmoeUWjs+08rKKt4AUXju7+3aRZSsKrJtaRmsdL1xA==", 670 | "dev": true, 671 | "optional": true 672 | }, 673 | "@esbuild/linux-s390x": { 674 | "version": "0.17.18", 675 | "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.18.tgz", 676 | "integrity": "sha512-cX0I8Q9xQkL/6F5zWdYmVf5JSQt+ZfZD2bJudZrWD+4mnUvoZ3TDDXtDX2mUaq6upMFv9FlfIh4Gfun0tbGzuw==", 677 | "dev": true, 678 | "optional": true 679 | }, 680 | "@esbuild/linux-x64": { 681 | "version": "0.17.18", 682 | "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.18.tgz", 683 | "integrity": "sha512-66RmRsPlYy4jFl0vG80GcNRdirx4nVWAzJmXkevgphP1qf4dsLQCpSKGM3DUQCojwU1hnepI63gNZdrr02wHUA==", 684 | "dev": true, 685 | "optional": true 686 | }, 687 | "@esbuild/netbsd-x64": { 688 | "version": "0.17.18", 689 | "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.18.tgz", 690 | "integrity": "sha512-95IRY7mI2yrkLlTLb1gpDxdC5WLC5mZDi+kA9dmM5XAGxCME0F8i4bYH4jZreaJ6lIZ0B8hTrweqG1fUyW7jbg==", 691 | "dev": true, 692 | "optional": true 693 | }, 694 | "@esbuild/openbsd-x64": { 695 | "version": "0.17.18", 696 | "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.18.tgz", 697 | "integrity": "sha512-WevVOgcng+8hSZ4Q3BKL3n1xTv5H6Nb53cBrtzzEjDbbnOmucEVcZeGCsCOi9bAOcDYEeBZbD2SJNBxlfP3qiA==", 698 | "dev": true, 699 | "optional": true 700 | }, 701 | "@esbuild/sunos-x64": { 702 | "version": "0.17.18", 703 | "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.18.tgz", 704 | "integrity": "sha512-Rzf4QfQagnwhQXVBS3BYUlxmEbcV7MY+BH5vfDZekU5eYpcffHSyjU8T0xucKVuOcdCsMo+Ur5wmgQJH2GfNrg==", 705 | "dev": true, 706 | "optional": true 707 | }, 708 | "@esbuild/win32-arm64": { 709 | "version": "0.17.18", 710 | "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.18.tgz", 711 | "integrity": "sha512-Kb3Ko/KKaWhjeAm2YoT/cNZaHaD1Yk/pa3FTsmqo9uFh1D1Rfco7BBLIPdDOozrObj2sahslFuAQGvWbgWldAg==", 712 | "dev": true, 713 | "optional": true 714 | }, 715 | "@esbuild/win32-ia32": { 716 | "version": "0.17.18", 717 | "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.18.tgz", 718 | "integrity": "sha512-0/xUMIdkVHwkvxfbd5+lfG7mHOf2FRrxNbPiKWg9C4fFrB8H0guClmaM3BFiRUYrznVoyxTIyC/Ou2B7QQSwmw==", 719 | "dev": true, 720 | "optional": true 721 | }, 722 | "@esbuild/win32-x64": { 723 | "version": "0.17.18", 724 | "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.18.tgz", 725 | "integrity": "sha512-qU25Ma1I3NqTSHJUOKi9sAH1/Mzuvlke0ioMJRthLXKm7JiSKVwFghlGbDLOO2sARECGhja4xYfRAZNPAkooYg==", 726 | "dev": true, 727 | "optional": true 728 | }, 729 | "chart.js": { 730 | "version": "3.9.1", 731 | "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.9.1.tgz", 732 | "integrity": "sha512-Ro2JbLmvg83gXF5F4sniaQ+lTbSv18E+TIf2cOeiH1Iqd2PGFOtem+DUufMZsCJwFE7ywPOpfXFBwRTGq7dh6w==" 733 | }, 734 | "esbuild": { 735 | "version": "0.17.18", 736 | "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.18.tgz", 737 | "integrity": "sha512-z1lix43jBs6UKjcZVKOw2xx69ffE2aG0PygLL5qJ9OS/gy0Ewd1gW/PUQIOIQGXBHWNywSc0floSKoMFF8aK2w==", 738 | "dev": true, 739 | "requires": { 740 | "@esbuild/android-arm": "0.17.18", 741 | "@esbuild/android-arm64": "0.17.18", 742 | "@esbuild/android-x64": "0.17.18", 743 | "@esbuild/darwin-arm64": "0.17.18", 744 | "@esbuild/darwin-x64": "0.17.18", 745 | "@esbuild/freebsd-arm64": "0.17.18", 746 | "@esbuild/freebsd-x64": "0.17.18", 747 | "@esbuild/linux-arm": "0.17.18", 748 | "@esbuild/linux-arm64": "0.17.18", 749 | "@esbuild/linux-ia32": "0.17.18", 750 | "@esbuild/linux-loong64": "0.17.18", 751 | "@esbuild/linux-mips64el": "0.17.18", 752 | "@esbuild/linux-ppc64": "0.17.18", 753 | "@esbuild/linux-riscv64": "0.17.18", 754 | "@esbuild/linux-s390x": "0.17.18", 755 | "@esbuild/linux-x64": "0.17.18", 756 | "@esbuild/netbsd-x64": "0.17.18", 757 | "@esbuild/openbsd-x64": "0.17.18", 758 | "@esbuild/sunos-x64": "0.17.18", 759 | "@esbuild/win32-arm64": "0.17.18", 760 | "@esbuild/win32-ia32": "0.17.18", 761 | "@esbuild/win32-x64": "0.17.18" 762 | } 763 | }, 764 | "fsevents": { 765 | "version": "2.3.2", 766 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", 767 | "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", 768 | "dev": true, 769 | "optional": true 770 | }, 771 | "lz-string": { 772 | "version": "1.5.0", 773 | "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", 774 | "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==" 775 | }, 776 | "nanoid": { 777 | "version": "3.3.6", 778 | "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", 779 | "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", 780 | "dev": true 781 | }, 782 | "picocolors": { 783 | "version": "1.0.0", 784 | "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", 785 | "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", 786 | "dev": true 787 | }, 788 | "postcss": { 789 | "version": "8.4.23", 790 | "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.23.tgz", 791 | "integrity": "sha512-bQ3qMcpF6A/YjR55xtoTr0jGOlnPOKAIMdOWiv0EIT6HVPEaJiJB4NLljSbiHoC2RX7DN5Uvjtpbg1NPdwv1oA==", 792 | "dev": true, 793 | "requires": { 794 | "nanoid": "^3.3.6", 795 | "picocolors": "^1.0.0", 796 | "source-map-js": "^1.0.2" 797 | } 798 | }, 799 | "rollup": { 800 | "version": "3.21.2", 801 | "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.21.2.tgz", 802 | "integrity": "sha512-c4vC+JZ3bbF4Kqq2TtM7zSKtSyMybFOjqmomFax3xpfYaPZDZ4iz8NMIuBRMjnXOcKYozw7bC6vhJjiWD6JpzQ==", 803 | "dev": true, 804 | "requires": { 805 | "fsevents": "~2.3.2" 806 | } 807 | }, 808 | "source-map-js": { 809 | "version": "1.0.2", 810 | "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", 811 | "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", 812 | "dev": true 813 | }, 814 | "typescript": { 815 | "version": "5.0.4", 816 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz", 817 | "integrity": "sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==", 818 | "dev": true 819 | }, 820 | "vite": { 821 | "version": "4.3.3", 822 | "resolved": "https://registry.npmjs.org/vite/-/vite-4.3.3.tgz", 823 | "integrity": "sha512-MwFlLBO4udZXd+VBcezo3u8mC77YQk+ik+fbc0GZWGgzfbPP+8Kf0fldhARqvSYmtIWoAJ5BXPClUbMTlqFxrA==", 824 | "dev": true, 825 | "requires": { 826 | "esbuild": "^0.17.5", 827 | "fsevents": "~2.3.2", 828 | "postcss": "^8.4.23", 829 | "rollup": "^3.21.0" 830 | } 831 | } 832 | } 833 | } 834 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "charts-for-benchmarkdotnet", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "preview": "vite preview" 10 | }, 11 | "devDependencies": { 12 | "typescript": "^5.0.2", 13 | "vite": "^4.3.2" 14 | }, 15 | "dependencies": { 16 | "chart.js": "^3.9.1", 17 | "lz-string": "^1.5.0" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /public/CNAME: -------------------------------------------------------------------------------- 1 | chartbenchmark.net -------------------------------------------------------------------------------- /public/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #333333 7 | 8 | 9 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yv989c/ChartsForBenchmarkDotNet/54c7c9e16a0eea71249a1b9b9950484e60950efd/public/favicon.ico -------------------------------------------------------------------------------- /public/favicon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/icons/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yv989c/ChartsForBenchmarkDotNet/54c7c9e16a0eea71249a1b9b9950484e60950efd/public/images/icons/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/images/icons/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yv989c/ChartsForBenchmarkDotNet/54c7c9e16a0eea71249a1b9b9950484e60950efd/public/images/icons/android-chrome-512x512.png -------------------------------------------------------------------------------- /public/images/icons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yv989c/ChartsForBenchmarkDotNet/54c7c9e16a0eea71249a1b9b9950484e60950efd/public/images/icons/apple-touch-icon.png -------------------------------------------------------------------------------- /public/images/icons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yv989c/ChartsForBenchmarkDotNet/54c7c9e16a0eea71249a1b9b9950484e60950efd/public/images/icons/favicon-16x16.png -------------------------------------------------------------------------------- /public/images/icons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yv989c/ChartsForBenchmarkDotNet/54c7c9e16a0eea71249a1b9b9950484e60950efd/public/images/icons/favicon-32x32.png -------------------------------------------------------------------------------- /public/images/icons/mstile-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yv989c/ChartsForBenchmarkDotNet/54c7c9e16a0eea71249a1b9b9950484e60950efd/public/images/icons/mstile-144x144.png -------------------------------------------------------------------------------- /public/images/icons/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yv989c/ChartsForBenchmarkDotNet/54c7c9e16a0eea71249a1b9b9950484e60950efd/public/images/icons/mstile-150x150.png -------------------------------------------------------------------------------- /public/images/icons/mstile-310x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yv989c/ChartsForBenchmarkDotNet/54c7c9e16a0eea71249a1b9b9950484e60950efd/public/images/icons/mstile-310x150.png -------------------------------------------------------------------------------- /public/images/icons/mstile-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yv989c/ChartsForBenchmarkDotNet/54c7c9e16a0eea71249a1b9b9950484e60950efd/public/images/icons/mstile-310x310.png -------------------------------------------------------------------------------- /public/images/icons/mstile-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yv989c/ChartsForBenchmarkDotNet/54c7c9e16a0eea71249a1b9b9950484e60950efd/public/images/icons/mstile-70x70.png -------------------------------------------------------------------------------- /public/images/icons/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.14, written by Peter Selinger 2001-2017 9 | 10 | 12 | 19 | 23 | 27 | 30 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /public/manifest.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "icons": [ 3 | { 4 | "src": "/images/icons/android-chrome-192x192.png", 5 | "sizes": "192x192", 6 | "type": "image/png" 7 | }, 8 | { 9 | "src": "/images/icons/android-chrome-512x512.png", 10 | "sizes": "512x512", 11 | "type": "image/png" 12 | } 13 | ], 14 | "theme_color": "#272b30", 15 | "background_color": "#272b30" 16 | } -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: 3 | 4 | Sitemap: https://chartbenchmark.net/sitemap.xml -------------------------------------------------------------------------------- /public/sitemap.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | https://chartbenchmark.net/ 5 | 6 | -------------------------------------------------------------------------------- /src/app.ts: -------------------------------------------------------------------------------- 1 | import { Legend, SubTitle, Tooltip, LinearScale, LogarithmicScale, BarController, BarElement, CategoryScale, Chart } from "chart.js"; 2 | import { Log2Axis } from "./log-2-axis"; 3 | import { ChartBuilder, Theme, ScaleType, DisplayMode } from "./chart-builder"; 4 | import LZString from "lz-string"; 5 | 6 | interface ISharedData { 7 | v?: number, 8 | settings: { 9 | display: string, 10 | scale: string, 11 | theme: string 12 | }, 13 | results: string 14 | } 15 | 16 | export class App { 17 | private static readonly DefaultFileName = 'chart.png'; 18 | 19 | _chartWrapper: HTMLElement; 20 | _chartCanvas: HTMLCanvasElement; 21 | _builder: ChartBuilder; 22 | _resultsInput: HTMLInputElement; 23 | _displayRadioContainer: HTMLElement; 24 | _scaleRadioContainer: HTMLElement; 25 | _themeRadioContainer: HTMLElement; 26 | 27 | constructor() { 28 | Chart.register(Legend, SubTitle, Tooltip, LinearScale, LogarithmicScale, CategoryScale, BarController, BarElement, Log2Axis); 29 | Chart.defaults.font.family = 'system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue","Noto Sans","Liberation Sans",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji"'; 30 | 31 | this._chartWrapper = document.getElementById('chartWrapper')!; 32 | this._chartCanvas = document.getElementById('chartCanvas') as HTMLCanvasElement; 33 | this._builder = new ChartBuilder(this._chartCanvas); 34 | 35 | this._resultsInput = document.getElementById('resultsInput') as HTMLInputElement; 36 | this._displayRadioContainer = document.getElementById('displayRadioContainer') as HTMLElement; 37 | this._scaleRadioContainer = document.getElementById('scaleRadioContainer') as HTMLElement; 38 | this._themeRadioContainer = document.getElementById('themeRadioContainer') as HTMLElement; 39 | 40 | this.bindSizeControls(this._chartWrapper); 41 | 42 | this._scaleRadioContainer.addEventListener('input', e => { 43 | this._builder.scaleType = (e.target as HTMLInputElement).value as ScaleType; 44 | }); 45 | 46 | this._displayRadioContainer.addEventListener('input', e => { 47 | this._builder.displayMode = (e.target as HTMLInputElement).value as DisplayMode; 48 | }); 49 | 50 | this._themeRadioContainer.addEventListener('input', e => { 51 | this._builder.theme = (e.target as HTMLInputElement).value as Theme; 52 | this.refreshChartContainer(); 53 | }); 54 | 55 | this.bindCopyToClipboardButton(); 56 | 57 | document.getElementById('downloadButton')!.addEventListener('click', () => { 58 | var link = document.createElement('a'); 59 | link.download = App.DefaultFileName; 60 | link.href = this._chartCanvas.toDataURL(); 61 | link.click(); 62 | }); 63 | 64 | document.getElementById('shareButtonContainer')!.addEventListener('click', async (event) => { 65 | event.preventDefault(); 66 | 67 | const target = event.target as HTMLAnchorElement; 68 | switch (target.dataset.action) { 69 | case 'shareAsUrl': 70 | await this.shareAsUrl(); 71 | break; 72 | case 'shareAsImage': 73 | await this.shareAsImage(); 74 | break; 75 | } 76 | }); 77 | 78 | document.addEventListener('readystatechange', () => { 79 | if (document.readyState === 'complete') { 80 | // Fixes MBC Widget Google Pay bug. 81 | const widgetIFrame = document.querySelector('iframe[title*="Buy Me a Coffee"]'); 82 | if (widgetIFrame !== null) { 83 | widgetIFrame.setAttribute('allow', 'payment'); 84 | } 85 | } 86 | }); 87 | 88 | this.bindResultsInput(this._builder); 89 | } 90 | 91 | private bindCopyToClipboardButton() { 92 | const copyToClipboardButton = document.getElementById('copyToClipboardButton')!; 93 | const span = copyToClipboardButton.querySelector('span')!; 94 | const defaultText = span.innerText; 95 | const feedbackText = copyToClipboardButton.dataset.feedbackText!; 96 | 97 | let setTimeoutId = 0; 98 | 99 | copyToClipboardButton.addEventListener('click', () => { 100 | clearTimeout(setTimeoutId); 101 | this._chartCanvas.toBlob(async blob => { 102 | const item = new ClipboardItem({ 'image/png': blob! }); 103 | await navigator.clipboard.write([item]); 104 | span.innerText = feedbackText; 105 | setTimeoutId = setTimeout(() => { 106 | span.innerText = defaultText; 107 | }, 1000); 108 | }); 109 | }); 110 | } 111 | 112 | private bindResultsInput(builder: ChartBuilder) { 113 | const resultsInput = this._resultsInput; 114 | 115 | resultsInput.addEventListener('input', () => { 116 | loadBenchmarkResult(); 117 | }); 118 | 119 | if (this.tryRestoreFromSharedUrl()) { 120 | loadBenchmarkResult(); 121 | return; 122 | } 123 | 124 | resultsInput.addEventListener('click', () => { 125 | resultsInput.value = ''; 126 | loadBenchmarkResult(); 127 | }, { once: true }); 128 | 129 | loadBenchmarkResult(); 130 | 131 | function loadBenchmarkResult() { 132 | builder.loadBenchmarkResult(resultsInput.value); 133 | } 134 | } 135 | 136 | private bindSizeControls(chartWrapper: HTMLElement) { 137 | const widthRangeInput = document.getElementById('widthRangeInput') as HTMLInputElement; 138 | const heightRangeInput = document.getElementById('heightRangeInput') as HTMLInputElement; 139 | 140 | if (document.body.clientWidth < 992) { 141 | widthRangeInput.value = '1'; 142 | } 143 | 144 | updateWidth(); 145 | updateHeight(); 146 | 147 | widthRangeInput.addEventListener('input', updateWidth); 148 | heightRangeInput.addEventListener('input', updateHeight); 149 | 150 | function updateWidth() { 151 | const value = `${parseFloat(widthRangeInput.value) * 100}%`; 152 | widthRangeInput.title = value; 153 | chartWrapper.style.width = value; 154 | } 155 | 156 | function updateHeight() { 157 | const value = parseFloat(heightRangeInput.value); 158 | heightRangeInput.title = `${value * 100}%`; 159 | chartWrapper.style.height = `${value * 1600}px`; 160 | } 161 | } 162 | 163 | private refreshChartContainer() { 164 | switch (this._builder.theme) { 165 | case Theme.Dark: 166 | this._chartWrapper.classList.remove('bg-light'); 167 | break; 168 | case Theme.Light: 169 | this._chartWrapper.classList.add('bg-light'); 170 | break; 171 | } 172 | } 173 | 174 | private getValue(container: HTMLElement) { 175 | return (container.querySelector('input:checked')).value; 176 | } 177 | 178 | private setValue(container: HTMLElement, value: string) { 179 | return (container.querySelector(`input[value="${value}"]`)).checked = true; 180 | } 181 | 182 | private async share(data: any) { 183 | const canShare = 184 | typeof (navigator.canShare) === 'function' && 185 | navigator.canShare(data); 186 | 187 | if (canShare) { 188 | try { 189 | await navigator.share(data); 190 | return true; 191 | } catch (error) { 192 | if (error instanceof Error && error.name === 'AbortError') { 193 | return true; 194 | } 195 | } 196 | } 197 | 198 | return false; 199 | } 200 | 201 | private showUnsupportedMessage() { 202 | alert('This feature is not supported by your browser. 😞'); 203 | } 204 | 205 | private async shareAsUrl() { 206 | const data: ISharedData = { 207 | v: 1, 208 | settings: { 209 | display: this.getValue(this._displayRadioContainer), 210 | scale: this.getValue(this._scaleRadioContainer), 211 | theme: this.getValue(this._themeRadioContainer) 212 | }, 213 | results: LZString.compressToBase64(this._resultsInput.value) 214 | }; 215 | 216 | const serializedData = encodeURIComponent(JSON.stringify(data)); 217 | const url = `${window.location.origin}#shared=${serializedData}`; 218 | 219 | try { 220 | if (!await this.share({ url: url })) { 221 | await navigator.clipboard.writeText(url); 222 | alert('A shareable URL was copied to your clipboard!') 223 | } 224 | } catch (error) { 225 | console.error(error); 226 | prompt('Copy the following URL for sharing:', url); 227 | } 228 | } 229 | 230 | private async shareAsImage() { 231 | this._chartCanvas.toBlob(async blob => { 232 | try { 233 | const file = new File([blob], App.DefaultFileName, { type: 'image/png' }); 234 | const data = { files: [file] }; 235 | if (!await this.share(data)) { 236 | this.showUnsupportedMessage(); 237 | } 238 | } catch (error) { 239 | console.error(error); 240 | this.showUnsupportedMessage(); 241 | } 242 | }); 243 | } 244 | 245 | private tryRestoreFromSharedUrl() { 246 | try { 247 | if (window.location.hash.length < 2) { 248 | return; 249 | } 250 | 251 | const urlParams = new URLSearchParams(window.location.hash.substring(1)); 252 | const sharedEncodedData = urlParams.get('shared'); 253 | 254 | if (sharedEncodedData) { 255 | const sharedData = JSON.parse(sharedEncodedData); 256 | 257 | this._resultsInput.value = sharedData.v === 1 ? LZString.decompressFromBase64(sharedData.results) : sharedData.results; 258 | this.setValue(this._displayRadioContainer, sharedData.settings.display); 259 | this.setValue(this._scaleRadioContainer, sharedData.settings.scale); 260 | this.setValue(this._themeRadioContainer, sharedData.settings.theme); 261 | 262 | this._builder.displayMode = this.getValue(this._displayRadioContainer); 263 | this._builder.scaleType = this.getValue(this._scaleRadioContainer); 264 | this._builder.theme = this.getValue(this._themeRadioContainer); 265 | this.refreshChartContainer(); 266 | 267 | return true; 268 | } 269 | } catch (error) { 270 | console.error(error); 271 | alert('Error while restoring the data from the URL.'); 272 | } 273 | 274 | return false; 275 | } 276 | } 277 | 278 | -------------------------------------------------------------------------------- /src/chart-builder.ts: -------------------------------------------------------------------------------- 1 | import { Chart } from "chart.js"; 2 | import { color } from "chart.js/helpers"; 3 | 4 | export class ChartBuilder { 5 | private readonly _chart: any; 6 | /* 7 | https://coolors.co/palette/f94144-f3722c-f8961e-f9c74f-90be6d-43aa8b-4d908e-577590 8 | https://coolors.co/palette/ff595e-ff924c-ffca3a-c5ca30-8ac926-36949d-1982c4-4267ac-565aa0-6a4c93 9 | https://coolors.co/palette/264653-287271-2a9d8f-8ab17d-babb74-e9c46a-efb366-f4a261-ee8959-e76f51 10 | https://coolors.co/palette/264653-287271-2a9d8f-8ab17d-e9c46a-f4a261-ee8959-e76f51 11 | */ 12 | private readonly _colors = ['#F94144', '#F3722C', '#F8961E', '#F9C74F', '#90BE6D', '#43AA8B', '#4D908E', '#577590']; 13 | 14 | private _benchmarkResultRows: IBenchmarkResultRow[] = []; 15 | private _theme = Theme.Dark; 16 | private _yAxisTextColor = '#606060'; 17 | 18 | private _y2AxisBaseColor = '#A600FF'; 19 | private _y2AxisGridLineColor = this._y2AxisBaseColor; 20 | private _y2AxisTextColor = this._y2AxisBaseColor; 21 | private _y2AxisBarColor = color(this._y2AxisBaseColor).clearer(0.3).hexString(); 22 | 23 | private _xAxisTextColor = '#606060'; 24 | private _gridLineColor = ''; 25 | private _creditTextColor = ''; 26 | private _scaleType = ScaleType.Log2; 27 | 28 | private _displayMode = DisplayMode.All; 29 | 30 | private _hasAllocationData = false; 31 | 32 | get theme() { 33 | return this._theme; 34 | } 35 | set theme(value) { 36 | switch (value) { 37 | case Theme.Dark: 38 | this._creditTextColor = '#66666675'; 39 | this._gridLineColor = '#66666638'; 40 | this._y2AxisGridLineColor = color(this._y2AxisBaseColor).clearer(0.5).hexString(); 41 | break; 42 | case Theme.Light: 43 | this._creditTextColor = '#00000040'; 44 | this._gridLineColor = '#0000001a'; 45 | this._y2AxisGridLineColor = color(this._y2AxisBaseColor).clearer(0.75).hexString(); 46 | break; 47 | default: 48 | break; 49 | } 50 | this._theme = value; 51 | this.render(); 52 | } 53 | 54 | get scaleType() { 55 | return this._scaleType; 56 | } 57 | set scaleType(value) { 58 | this._scaleType = value; 59 | this.render(); 60 | } 61 | 62 | get displayMode() { 63 | return this._displayMode; 64 | } 65 | set displayMode(value) { 66 | this._displayMode = value; 67 | this.render(); 68 | } 69 | 70 | get hasAllocationData() { 71 | return this._hasAllocationData && (this.displayMode === DisplayMode.All || this.displayMode === DisplayMode.Allocation); 72 | } 73 | 74 | private get chartPlugins() { 75 | return this._chart.config.options.plugins; 76 | } 77 | 78 | private get chartScales() { 79 | return this._chart.config.options.scales; 80 | } 81 | 82 | constructor(canvas: HTMLCanvasElement) { 83 | this._chart = getChart(); 84 | this.theme = this.theme; 85 | const that = this; 86 | 87 | function getChart() { 88 | const numberFormat = new Intl.NumberFormat('en-US', { style: 'decimal' }); 89 | 90 | const config = { 91 | type: 'bar', 92 | options: { 93 | maintainAspectRatio: false, 94 | responsive: true, 95 | plugins: { 96 | subtitle: { 97 | display: true, 98 | text: 'Made with chartbenchmark.net', 99 | position: 'right', 100 | align: 'center', 101 | fullSize: false, 102 | font: { 103 | size: 11, 104 | family: 'SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace' 105 | } 106 | }, 107 | legend: { 108 | labels: { 109 | usePointStyle: true, 110 | filter: (legend: any, data: any) => { 111 | const display = 112 | (that.displayMode === DisplayMode.Allocation && that.hasAllocationData) || 113 | data.datasets[legend.datasetIndex].isDuration === true; 114 | 115 | return display; 116 | } 117 | }, 118 | onClick: (_: any, legendItem: any, legend: any) => { 119 | const index = legendItem.datasetIndex; 120 | const ci = legend.chart; 121 | const hideAllocation = that.displayMode === DisplayMode.All && that.hasAllocationData; 122 | 123 | if (ci.isDatasetVisible(index)) { 124 | if (hideAllocation) { 125 | ci.hide(index + 1); 126 | } 127 | ci.hide(index); 128 | legendItem.hidden = true; 129 | } else { 130 | if (hideAllocation) { 131 | ci.show(index + 1); 132 | } 133 | ci.show(index); 134 | legendItem.hidden = false; 135 | } 136 | } 137 | }, 138 | tooltip: { 139 | callbacks: { 140 | label: (context: any) => { 141 | const dataset = context.dataset as IDataset; 142 | 143 | let label = dataset.label || ''; 144 | 145 | if (dataset.isDuration) { 146 | label += ' - Duration: '; 147 | } 148 | else if (dataset.isAllocation) { 149 | label += ' - Memory Allocation: '; 150 | } 151 | 152 | if (context.parsed.y !== null) { 153 | label += `${numberFormat.format(context.parsed.y)} ${dataset.unitInfo.short}`; 154 | } 155 | 156 | return label; 157 | } 158 | } 159 | } 160 | }, 161 | scales: { 162 | y: { 163 | title: { 164 | display: true 165 | }, 166 | display: 'auto' 167 | }, 168 | y2: { 169 | title: { 170 | display: true 171 | }, 172 | position: 'right', 173 | display: 'auto' 174 | }, 175 | x: { 176 | title: { 177 | display: true 178 | }, 179 | display: 'auto' 180 | } 181 | } 182 | } 183 | }; 184 | 185 | return new Chart(canvas.getContext('2d'), config); 186 | } 187 | } 188 | 189 | loadBenchmarkResult(text: string) { 190 | this._benchmarkResultRows = text 191 | .split('\n') 192 | .map(i => i.trim()) 193 | .filter(i => i.length > 2 && i.startsWith('|') && i.endsWith('|')) 194 | .filter((_, index) => index !== 1) 195 | .map(i => ({ 196 | text: i, 197 | columns: i 198 | .split('|') 199 | .slice(1, -1) 200 | .map(i => i.replace(/\*/g, '').trim()) 201 | })) 202 | .filter(i => i.columns.some(c => c.length > 0)); 203 | 204 | this.render(); 205 | } 206 | 207 | private getBenchmarkResult() { 208 | const rows = this._benchmarkResultRows; 209 | 210 | if (rows.length === 0) { 211 | return null; 212 | } 213 | 214 | const headerRow = rows[0]; 215 | const methodIndex = headerRow.columns.indexOf('Method'); 216 | const meanIndex = headerRow.columns.lastIndexOf('Mean'); 217 | 218 | if (methodIndex < 0 || meanIndex < 0) { 219 | return null; 220 | } 221 | 222 | const runtimeIndex = headerRow.columns.indexOf('Runtime'); 223 | 224 | const categoryIndexStart = Math.max(methodIndex, runtimeIndex) + 1; 225 | const categoryIndexEnd = meanIndex - 1; 226 | 227 | const allocatedIndex = headerRow.columns.indexOf('Allocated', meanIndex); 228 | 229 | const methods = new Map(); 230 | const orderByMethod = new Map(); 231 | 232 | for (const row of rows) { 233 | if (row === headerRow) { 234 | continue; 235 | } 236 | 237 | let category = ''; 238 | 239 | for (let i = categoryIndexStart; i <= categoryIndexEnd; i++) { 240 | const separator = category.length > 0 ? ', ' : ''; 241 | category += `${separator}${row.columns[i]}`; 242 | } 243 | 244 | const runtimeName = runtimeIndex >= 0 ? row.columns[runtimeIndex] : null; 245 | 246 | const methodNamePrefix = row.columns[methodIndex]; 247 | const methodName = methodNamePrefix + (runtimeName !== null ? ` (${runtimeName})` : ''); 248 | let methodInfo = methods.get(methodName); 249 | 250 | if (typeof (methodInfo) === 'undefined') { 251 | let methodOrder = orderByMethod.get(methodNamePrefix); 252 | 253 | if (typeof (methodOrder) === 'undefined') { 254 | methodOrder = orderByMethod.size; 255 | orderByMethod.set(methodNamePrefix, methodOrder); 256 | } 257 | 258 | methodInfo = { 259 | name: methodName, 260 | order: methodOrder, 261 | results: [] 262 | }; 263 | 264 | methods.set(methodName, methodInfo); 265 | } 266 | 267 | const durationMean = getMeasure(row.columns[meanIndex]); 268 | 269 | const result: IMethodResult = { 270 | category: category, 271 | duration: durationMean, 272 | allocation: allocatedIndex >= 0 ? getMeasure(row.columns[allocatedIndex]) : null 273 | }; 274 | 275 | methodInfo.results.push(result); 276 | } 277 | 278 | const methodsArray = getMethods(); 279 | 280 | return { 281 | categories: getCategories(), 282 | categoriesTitle: getCategoriesTitle(), 283 | methods: methodsArray, 284 | durationUnit: inferDurationUnit(), 285 | allocationUnit: inferAllocationUnit() 286 | }; 287 | 288 | function getCategories() { 289 | const categories = new Set(); 290 | for (const method of methods) { 291 | for (const result of method[1].results) { 292 | categories.add(result.category); 293 | } 294 | } 295 | return [...categories]; 296 | } 297 | 298 | function getCategoriesTitle() { 299 | let title = ''; 300 | for (let i = categoryIndexStart; i <= categoryIndexEnd; i++) { 301 | const separator = title.length > 0 ? ', ' : ''; 302 | title += `${separator}${headerRow.columns[i]}`; 303 | } 304 | return title; 305 | } 306 | 307 | function getMethods() { 308 | return [...methods] 309 | .map(i => i[1]) 310 | .sort((a, b) => a.order - b.order); 311 | } 312 | 313 | function inferDurationUnit() { 314 | const info: IUnitInfo = { 315 | short: '', 316 | long: '' 317 | }; 318 | 319 | for (const method of methods) { 320 | for (const result of method[1].results) { 321 | info.short = result.duration.unit; 322 | 323 | switch (result.duration.unit) { 324 | case 'us': 325 | case 'μs': 326 | info.long = 'Microseconds'; 327 | break; 328 | case 'ms': 329 | info.long = 'Milliseconds'; 330 | break; 331 | case 's': 332 | info.long = 'Seconds'; 333 | break; 334 | default: 335 | info.long = result.duration.unit; 336 | break; 337 | } 338 | 339 | return info; 340 | } 341 | } 342 | 343 | return info; 344 | } 345 | 346 | function inferAllocationUnit() { 347 | const info: IUnitInfo = { 348 | short: '', 349 | long: '' 350 | }; 351 | 352 | for (const method of methods) { 353 | for (const result of method[1].results) { 354 | if (result.allocation === null) { 355 | continue; 356 | } 357 | 358 | info.short = result.allocation.unit; 359 | 360 | switch (result.allocation.unit) { 361 | case 'B': 362 | info.long = 'Bytes'; 363 | break; 364 | case 'KB': 365 | info.long = 'Kilobytes'; 366 | break; 367 | case 'MB': 368 | info.long = 'Megabytes'; 369 | break; 370 | case 'GB': 371 | info.long = 'Gigabytes'; 372 | break; 373 | case 'TB': 374 | info.long = 'Terabytes'; 375 | break; 376 | case 'PB': 377 | info.long = 'Petabytes'; 378 | break; 379 | default: 380 | info.long = result.allocation.unit; 381 | break; 382 | } 383 | 384 | return info; 385 | } 386 | } 387 | 388 | return info; 389 | } 390 | 391 | function getMeasure(formattedNumber: string): IMeasure { 392 | const unitSeparatorIndex = formattedNumber.indexOf(' '); 393 | const unit = unitSeparatorIndex >= 0 ? formattedNumber.substring(unitSeparatorIndex).trim() : ''; 394 | const value = parseFloat(formattedNumber.replace(/[^0-9.]/g, '')); 395 | return { 396 | value, 397 | unit 398 | } 399 | } 400 | } 401 | 402 | private render() { 403 | const yAxe = this.chartScales.y; 404 | const y2Axe = this.chartScales.y2; 405 | const xAxe = this.chartScales.x; 406 | const chartData = this._chart.data; 407 | const benchmarkResult = this.getBenchmarkResult(); 408 | 409 | yAxe.title.text = ''; 410 | y2Axe.title.text = ''; 411 | xAxe.title.text = ''; 412 | chartData.labels.length = 0; 413 | chartData.datasets.length = 0; 414 | this._hasAllocationData = false; 415 | 416 | if (benchmarkResult === null) { 417 | this._chart.update(); 418 | return; 419 | } 420 | 421 | this._hasAllocationData = benchmarkResult.methods 422 | .some(m => m.results.some(r => r.allocation !== null)); 423 | 424 | this.chartPlugins.subtitle.color = this._creditTextColor; 425 | 426 | yAxe.title.text = `Duration (${benchmarkResult.durationUnit.long})`; 427 | yAxe.title.color = this._yAxisTextColor; 428 | yAxe.grid.color = this._gridLineColor; 429 | 430 | switch (this.scaleType) { 431 | case ScaleType.Log10: 432 | yAxe.type = 'logarithmic'; 433 | break; 434 | case ScaleType.Log2: 435 | yAxe.type = 'log2'; 436 | break; 437 | default: 438 | yAxe.type = 'linear'; 439 | break; 440 | } 441 | 442 | y2Axe.title.text = `Memory Allocation (${benchmarkResult.allocationUnit.long})`; 443 | y2Axe.title.color = this._y2AxisTextColor; 444 | y2Axe.ticks.color = this._y2AxisTextColor; 445 | y2Axe.grid.color = this._y2AxisGridLineColor; 446 | y2Axe.type = yAxe.type; 447 | 448 | xAxe.title.text = benchmarkResult.categoriesTitle; 449 | xAxe.title.color = this._xAxisTextColor; 450 | xAxe.grid.color = this._gridLineColor; 451 | 452 | if (this.displayMode === DisplayMode.Allocation) { 453 | if (!this.hasAllocationData) { 454 | this._chart.update(); 455 | return; 456 | } 457 | 458 | yAxe.title.text = y2Axe.title.text; 459 | } 460 | 461 | chartData.labels = benchmarkResult.categories; 462 | 463 | const indexByCategory = new Map(); 464 | for (let index = 0; index < benchmarkResult.categories.length; index++) { 465 | indexByCategory.set(benchmarkResult.categories[index], index); 466 | } 467 | 468 | const colors = this._colors; 469 | let colorIndex = 0; 470 | 471 | const datasets = []; 472 | 473 | for (const methodInfo of benchmarkResult.methods) { 474 | const color = getNextColor(); 475 | 476 | if (this.displayMode === DisplayMode.All || this.displayMode === DisplayMode.Duration) { 477 | const durationDataset = { 478 | isDuration: true, 479 | label: methodInfo.name, 480 | data: getData(methodInfo.results, r => r.duration.value), 481 | backgroundColor: color, 482 | stack: methodInfo.name, 483 | yAxisID: 'y', 484 | unitInfo: benchmarkResult.durationUnit, 485 | order: 2, 486 | barPercentage: 0.9, 487 | pointStyle: 'rect' 488 | }; 489 | 490 | datasets.push(durationDataset); 491 | } 492 | 493 | if (this.hasAllocationData && (this.displayMode === DisplayMode.All || this.displayMode === DisplayMode.Allocation)) { 494 | const allocationDataset = { 495 | isAllocation: true, 496 | label: methodInfo.name, 497 | data: getData(methodInfo.results, r => r.allocation === null ? 0 : r.allocation.value), 498 | backgroundColor: this._y2AxisBarColor, 499 | stack: methodInfo.name, 500 | yAxisID: 'y2', 501 | unitInfo: benchmarkResult.allocationUnit, 502 | order: 1, 503 | barPercentage: 0.2, 504 | pointStyle: 'rect' 505 | }; 506 | 507 | if (this.displayMode === DisplayMode.Allocation) { 508 | allocationDataset.backgroundColor = color; 509 | allocationDataset.yAxisID = 'y'; 510 | allocationDataset.barPercentage = 0.9; 511 | } 512 | 513 | datasets.push(allocationDataset); 514 | } 515 | } 516 | 517 | chartData.datasets = datasets; 518 | 519 | function getData(results: IMethodResult[], getResultCallback: (r: IMethodResult) => number) { 520 | const data: number[] = new Array(indexByCategory.size); 521 | for (const result of results) { 522 | const index = indexByCategory.get(result.category); 523 | if (index !== undefined) { 524 | data[index] = getResultCallback(result); 525 | } 526 | } 527 | return data; 528 | } 529 | 530 | function getNextColor() { 531 | if ((colorIndex ^ colors.length) === 0) { 532 | colorIndex = 0; 533 | } 534 | return colors[colorIndex++]; 535 | } 536 | 537 | this._chart.update(); 538 | } 539 | } 540 | 541 | interface IBenchmarkResultRow { 542 | text: string, 543 | columns: string[] 544 | } 545 | 546 | interface IMeasure { 547 | value: number, 548 | unit: string 549 | } 550 | 551 | interface IMethodResult { 552 | category: string, 553 | duration: IMeasure, 554 | allocation: IMeasure | null 555 | } 556 | 557 | interface IMethodInfo { 558 | name: string; 559 | order: number; 560 | results: IMethodResult[]; 561 | } 562 | 563 | interface IUnitInfo { 564 | short: string, 565 | long: string 566 | } 567 | 568 | interface IDataset { 569 | label: string, 570 | isDuration?: boolean, 571 | isAllocation?: boolean, 572 | unitInfo: IUnitInfo 573 | } 574 | 575 | export enum Theme { 576 | Dark = 'Dark', 577 | Light = 'Light' 578 | } 579 | 580 | export enum ScaleType { 581 | Linear = 'Linear', 582 | Log10 = 'Log10', 583 | Log2 = 'Log2' 584 | } 585 | 586 | export enum DisplayMode { 587 | All = 'All', 588 | Duration = 'Duration', 589 | Allocation = 'Allocation' 590 | } -------------------------------------------------------------------------------- /src/log-2-axis.ts: -------------------------------------------------------------------------------- 1 | import { Scale, LinearScale } from "chart.js"; 2 | 3 | /** 4 | * https://www.chartjs.org/docs/master/samples/advanced/derived-axis-type.html#log2-axis-implementation 5 | */ 6 | export class Log2Axis extends Scale { 7 | static id = 'log2'; 8 | static defaults = {}; 9 | 10 | _startValue: number = 0; 11 | _valueRange: number = 0; 12 | 13 | constructor(cfg: any) { 14 | super(cfg); 15 | } 16 | 17 | parse(raw: unknown, index: number) { 18 | const value = LinearScale.prototype.parse.apply(this, [raw, index]); 19 | return isFinite(value) && value > 0 ? value : null; 20 | } 21 | 22 | determineDataLimits() { 23 | const { min, max } = this.getMinMax(true); 24 | this.min = (isFinite(min) ? Math.max(0, min) : null); 25 | this.max = (isFinite(max) ? Math.max(0, max) : null); 26 | } 27 | 28 | buildTicks() { 29 | const ticks = []; 30 | 31 | let power = Math.floor(Math.log2(this.min || 1)); 32 | let maxPower = Math.ceil(Math.log2(this.max || 2)); 33 | while (power <= maxPower) { 34 | ticks.push({ value: Math.pow(2, power) }); 35 | power += 1; 36 | } 37 | 38 | this.min = ticks[0].value; 39 | this.max = ticks[ticks.length - 1].value; 40 | return ticks; 41 | } 42 | 43 | configure() { 44 | const start = this.min; 45 | 46 | super.configure(); 47 | 48 | this._startValue = Math.log2(start); 49 | this._valueRange = Math.log2(this.max) - Math.log2(start); 50 | } 51 | 52 | getPixelForValue(value: number) { 53 | if (value === undefined || value === 0) { 54 | value = this.min; 55 | } 56 | 57 | return this.getPixelForDecimal(value === this.min ? 0 58 | : (Math.log2(value) - this._startValue) / this._valueRange); 59 | } 60 | 61 | getValueForPixel(pixel: number) { 62 | const decimal = this.getDecimalForPixel(pixel); 63 | return Math.pow(2, this._startValue + decimal * this._valueRange); 64 | } 65 | } 66 | 67 | Log2Axis.id = "log2"; 68 | Log2Axis.defaults = {}; -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { App } from "./app"; 2 | 3 | new App(); -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "module": "ESNext", 6 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | 16 | /* Linting */ 17 | "strict": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "noFallthroughCasesInSwitch": true 21 | }, 22 | "include": ["src"] 23 | } 24 | --------------------------------------------------------------------------------