├── .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 | 
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 |
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 |
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 |
215 |
216 |
217 |
218 |
219 |
220 |
221 |
228 |
235 |
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 |
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 |
--------------------------------------------------------------------------------