├── src ├── styles │ └── _variables.scss ├── app │ ├── footer │ │ ├── footer.component.scss │ │ ├── footer.component.html │ │ └── footer.component.ts │ ├── output │ │ ├── output.component.scss │ │ ├── output.component.html │ │ └── output.component.ts │ ├── io-row │ │ ├── io-row.component.html │ │ ├── io-row.component.scss │ │ └── io-row.component.ts │ ├── efficiency-graph │ │ ├── efficiency-graph.component.html │ │ └── efficiency-graph.component.ts │ ├── equations │ │ ├── equations.component.html │ │ ├── equations.component.ts │ │ └── equations.component.scss │ ├── app.component.scss │ ├── app.component.html │ ├── input │ │ ├── input.component.html │ │ ├── input.component.scss │ │ └── input.component.ts │ ├── app.component.ts │ ├── ratio.pipe.ts │ ├── percentage.pipe.ts │ ├── calculator │ │ ├── calculator.component.ts │ │ ├── calculator.component.scss │ │ └── calculator.component.html │ ├── calculation-row │ │ ├── calculation-row.component.ts │ │ └── calculation-row.component.html │ ├── safe-html.pipe.ts │ ├── base-graph │ │ ├── base-graph.component.scss │ │ ├── base-graph.component.html │ │ └── base-graph.component.ts │ ├── crit-info.ts │ ├── app.module.ts │ └── math.html ├── html.d.ts ├── main.ts ├── index.html └── styles.scss ├── webpack.config.js ├── tsconfig.app.json ├── tsconfig.spec.json ├── .editorconfig ├── .gitignore ├── README.md ├── tsconfig.json ├── package.json ├── LICENSE └── angular.json /src/styles/_variables.scss: -------------------------------------------------------------------------------- 1 | $hoz-gap: 2em; 2 | -------------------------------------------------------------------------------- /src/app/footer/footer.component.scss: -------------------------------------------------------------------------------- 1 | footer { 2 | text-align: center; 3 | } -------------------------------------------------------------------------------- /src/app/output/output.component.scss: -------------------------------------------------------------------------------- 1 | .label-text { 2 | margin-bottom: 4px; 3 | } 4 | -------------------------------------------------------------------------------- /src/app/io-row/io-row.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | -------------------------------------------------------------------------------- /src/html.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.html" { 2 | const content: string; 3 | export default content; 4 | } 5 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | module: { 3 | rules: [ 4 | { 5 | test: /\.html$/i, 6 | use: 'raw-loader' 7 | } 8 | ] 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /src/app/footer/footer.component.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/io-row/io-row.component.scss: -------------------------------------------------------------------------------- 1 | .row { 2 | text-align: left; 3 | display: flex; 4 | gap: 1em; 5 | justify-content: space-evenly; 6 | min-width: min-content; 7 | margin: 1em 0; 8 | } 9 | -------------------------------------------------------------------------------- /src/app/efficiency-graph/efficiency-graph.component.html: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /src/app/equations/equations.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

Equations

4 |
5 |
6 |
7 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 2 | import { AppModule } from './app/app.module'; 3 | 4 | platformBrowserDynamic().bootstrapModule(AppModule) 5 | .catch(err => console.error(err)); 6 | -------------------------------------------------------------------------------- /src/app/output/output.component.html: -------------------------------------------------------------------------------- 1 | 11 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./out-tsc/app", 5 | "types": [] 6 | }, 7 | "files": [ 8 | "src/main.ts" 9 | ], 10 | "include": [ 11 | "src/**/*.d.ts" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./out-tsc/spec", 5 | "types": [ 6 | "jasmine" 7 | ] 8 | }, 9 | "include": [ 10 | "src/**/*.spec.ts", 11 | "src/**/*.d.ts" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /src/app/io-row/io-row.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-io-row', 5 | templateUrl: './io-row.component.html', 6 | styleUrl: './io-row.component.scss' 7 | }) 8 | export class IORowComponent { 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/app/footer/footer.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-footer', 5 | templateUrl: './footer.component.html', 6 | styleUrls: ['./footer.component.scss'] 7 | }) 8 | export class FooterComponent { 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/app/app.component.scss: -------------------------------------------------------------------------------- 1 | @use '../styles/variables' as *; 2 | 3 | header { 4 | text-align: center; 5 | } 6 | 7 | main { 8 | display: flex; 9 | flex-direction: column; 10 | max-width: max-content; 11 | min-width: min(350px, 100%); 12 | margin: auto; 13 | gap: $hoz-gap; 14 | } 15 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Critical Analysis 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = tab 6 | indent_size = 4 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | 10 | [*.ts] 11 | quote_type = single 12 | 13 | [*.md] 14 | max_line_length = off 15 | trim_trailing_whitespace = false 16 | -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 |
2 |

Critical Analysis

3 |

CRIT Rate and Damage Efficiency Analyzer

4 |

Genshin Impact • Honkai: Star Rail

5 |
6 |
7 | 8 | 9 | 10 |
11 | -------------------------------------------------------------------------------- /src/app/input/input.component.html: -------------------------------------------------------------------------------- 1 | 10 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { CritInfo } from './crit-info'; 3 | 4 | @Component({ 5 | selector: 'app-root', 6 | templateUrl: './app.component.html', 7 | styleUrls: ['./app.component.scss'] 8 | }) 9 | export class AppComponent { 10 | critInfo = new CritInfo(0.05, 0.5) 11 | } 12 | -------------------------------------------------------------------------------- /src/app/ratio.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core'; 2 | import { formatNumber } from '@angular/common'; 3 | 4 | @Pipe({ 5 | name: 'ratio' 6 | }) 7 | export class RatioPipe implements PipeTransform { 8 | transform(value: number): unknown { 9 | return '1:' + formatNumber(value, 'en', '1.0-2'); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/app/equations/equations.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import math from './../math.html'; 3 | 4 | @Component({ 5 | selector: 'app-equations', 6 | templateUrl: './equations.component.html', 7 | styleUrls: ['./equations.component.scss'] 8 | }) 9 | export class EquationsComponent { 10 | math = math; 11 | } 12 | -------------------------------------------------------------------------------- /src/app/input/input.component.scss: -------------------------------------------------------------------------------- 1 | :host ::ng-deep { 2 | .mat-mdc-form-field-infix { 3 | width: 5em !important; 4 | padding-top: 8px !important; 5 | padding-bottom: 8px !important; 6 | min-height: unset !important; 7 | 8 | input { 9 | margin: 0; 10 | } 11 | } 12 | } 13 | 14 | .label-text { 15 | margin-bottom: 8px; 16 | } 17 | -------------------------------------------------------------------------------- /src/app/output/output.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-output', 5 | templateUrl: './output.component.html', 6 | styleUrls: ['./output.component.scss'] 7 | }) 8 | export class OutputComponent { 9 | @Input() name?: string; 10 | @Input() tooltip: string = ''; 11 | } 12 | 13 | -------------------------------------------------------------------------------- /src/app/percentage.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core'; 2 | import { formatNumber } from '@angular/common'; 3 | 4 | @Pipe({ 5 | name: 'percentage' 6 | }) 7 | export class PercentagePipe implements PipeTransform { 8 | transform(value: number): unknown { 9 | return formatNumber(value * 100, 'en', '1.0-2') + '%'; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/app/calculator/calculator.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | import { CritInfo } from '../crit-info'; 3 | 4 | @Component({ 5 | selector: 'app-calculator', 6 | templateUrl: './calculator.component.html', 7 | styleUrls: ['./calculator.component.scss'] 8 | }) 9 | export class CalculatorComponent { 10 | @Input() critInfo!: CritInfo; 11 | } 12 | -------------------------------------------------------------------------------- /src/app/equations/equations.component.scss: -------------------------------------------------------------------------------- 1 | :host ::ng-deep { 2 | mat-expansion-panel-header { 3 | .mat-content { 4 | align-items: center; 5 | justify-content: center; 6 | } 7 | } 8 | 9 | math { 10 | * { 11 | text-align: left; 12 | } 13 | } 14 | 15 | h3 { 16 | margin: 1.5em 0 0.75em; 17 | 18 | &:first-child { 19 | margin-top: 0; 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/app/calculation-row/calculation-row.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-calculation-row', 5 | templateUrl: './calculation-row.component.html' 6 | }) 7 | export class CalculationRowComponent { 8 | @Input() value!: number; 9 | @Input() ratio!: number; 10 | @Input() multiplier!: number; 11 | @Input() efficiency!: number; 12 | } 13 | -------------------------------------------------------------------------------- /src/app/safe-html.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core'; 2 | import { DomSanitizer } from '@angular/platform-browser'; 3 | 4 | @Pipe({ 5 | name: 'safeHtml' 6 | }) 7 | export class SafeHtmlPipe implements PipeTransform { 8 | constructor(private sanitizer: DomSanitizer){} 9 | 10 | transform(value: string): unknown { 11 | return this.sanitizer.bypassSecurityTrustHtml(value); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/app/base-graph/base-graph.component.scss: -------------------------------------------------------------------------------- 1 | path { 2 | vector-effect: non-scaling-stroke; 3 | } 4 | 5 | .point { 6 | stroke-width: 10px; 7 | stroke-linecap: round; 8 | fill: none; 9 | } 10 | 11 | .crisp { 12 | shape-rendering: crispedges; 13 | } 14 | 15 | .text { 16 | paint-order: stroke; 17 | stroke: #424242; 18 | stroke-width: 2px; 19 | 20 | &.sideways { 21 | transform: rotate(180deg); 22 | writing-mode: vertical-lr; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/app/calculator/calculator.component.scss: -------------------------------------------------------------------------------- 1 | @use '../../styles/variables' as *; 2 | 3 | article { 4 | display: flex; 5 | gap: $hoz-gap; 6 | text-align: center; 7 | flex-direction: column; 8 | } 9 | 10 | .crit-row { 11 | font-size: 115%; 12 | } 13 | 14 | mat-card-header { 15 | justify-content: center; 16 | 17 | h2 { 18 | margin: 0; 19 | } 20 | } 21 | 22 | .graph { 23 | display: block; 24 | width: 22em; 25 | margin: 1em auto 0; 26 | } 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled output 2 | /dist 3 | /tmp 4 | /out-tsc 5 | /bazel-out 6 | 7 | # Node 8 | /node_modules 9 | npm-debug.log 10 | yarn-error.log 11 | 12 | # IDEs and editors 13 | .idea/ 14 | .project 15 | .classpath 16 | .c9/ 17 | *.launch 18 | .settings/ 19 | *.sublime-workspace 20 | 21 | # Visual Studio Code 22 | .vscode/* 23 | !.vscode/settings.json 24 | !.vscode/tasks.json 25 | !.vscode/launch.json 26 | !.vscode/extensions.json 27 | .history/* 28 | 29 | # Miscellaneous 30 | /.angular/cache 31 | .sass-cache/ 32 | /connect.lock 33 | /coverage 34 | /libpeerconnection.log 35 | testem.log 36 | /typings 37 | 38 | # System files 39 | .DS_Store 40 | Thumbs.db 41 | -------------------------------------------------------------------------------- /src/app/calculation-row/calculation-row.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{value | percentage}} 4 | 5 | 6 | {{ratio | ratio}} 7 | 8 | 9 | {{multiplier | percentage}} 10 | 11 | 12 | {{efficiency | percentage}} 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/app/input/input.component.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter, Component, Input, Output } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-input', 5 | templateUrl: './input.component.html', 6 | styleUrls: ['./input.component.scss'] 7 | }) 8 | export class InputComponent { 9 | static PRECISION = 1000; 10 | 11 | @Input() name?: string; 12 | @Input() value: number = 0; 13 | @Input() min: number = -Infinity; 14 | 15 | @Output() valueChange = new EventEmitter(); 16 | 17 | updateValue(e: Event) { 18 | const el = e.target as HTMLInputElement; 19 | 20 | this.valueChange.emit(Math.max(this.min, el.valueAsNumber / 100)); 21 | } 22 | 23 | get displayValue() { 24 | return Math.round(this.value * 100 * InputComponent.PRECISION) / InputComponent.PRECISION; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Critical Analysis 2 | 3 | > Genshin Impact and Honkai: Star Rail CRIT rate and damage efficiency analyzer. 4 | 5 | Critical Analysis is a calculator for visualizing and optimizing the efficiency of a character's CRIT Rate and CRIT Damage in Genshin Impact or Honkai: Star Rail. 6 | 7 | ## Development server 8 | 9 | Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files. 10 | 11 | ## Code scaffolding 12 | 13 | Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. 14 | 15 | ## Build 16 | 17 | Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. 18 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "outDir": "./dist/out-tsc", 6 | "forceConsistentCasingInFileNames": true, 7 | "esModuleInterop": true, 8 | "strict": true, 9 | "noImplicitOverride": true, 10 | "noPropertyAccessFromIndexSignature": true, 11 | "noImplicitReturns": true, 12 | "noFallthroughCasesInSwitch": true, 13 | "sourceMap": true, 14 | "declaration": false, 15 | "experimentalDecorators": true, 16 | "moduleResolution": "node", 17 | "importHelpers": true, 18 | "target": "ES2022", 19 | "module": "ES2022", 20 | "useDefineForClassFields": false, 21 | "lib": [ 22 | "ES2022", 23 | "dom" 24 | ] 25 | }, 26 | "angularCompilerOptions": { 27 | "enableI18nLegacyMessageIdFormat": false, 28 | "strictInjectionParameters": true, 29 | "strictInputAccessModifiers": true, 30 | "strictTemplates": true 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/app/base-graph/base-graph.component.html: -------------------------------------------------------------------------------- 1 | 2 | 10 | 16 | 27 | {{text.str}} 28 | 29 | 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "critical-analysis", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "build": "ng build" 8 | }, 9 | "private": true, 10 | "dependencies": { 11 | "@angular/animations": "^18.2.6", 12 | "@angular/cdk": "^18.2.6", 13 | "@angular/common": "^18.2.6", 14 | "@angular/compiler": "^18.2.6", 15 | "@angular/core": "^18.2.6", 16 | "@angular/forms": "^18.2.6", 17 | "@angular/material": "^18.2.6", 18 | "@angular/platform-browser": "^18.2.6", 19 | "@angular/platform-browser-dynamic": "^18.2.6", 20 | "@angular/router": "^18.2.6", 21 | "rxjs": "^7.8.0", 22 | "tslib": "^2.3.0", 23 | "zone.js": "~0.14.0" 24 | }, 25 | "devDependencies": { 26 | "@angular-builders/custom-webpack": "^18.0.0", 27 | "@angular/build": "^18.2.6", 28 | "@angular/cli": "^18.2.6", 29 | "@angular/compiler-cli": "^18.2.6", 30 | "raw-loader": "^4.0.2", 31 | "typescript": "^5.5.4" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Brandon Fowler 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 | -------------------------------------------------------------------------------- /src/app/crit-info.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from "@angular/core"; 2 | 3 | export class CritInfo { 4 | value!: number; 5 | multiplier!: number; 6 | ratio!: number; 7 | 8 | bestRate!: number; 9 | bestDmg!: number; 10 | bestMultiplier!: number; 11 | bestRatio!: number; 12 | 13 | efficiency!: number; 14 | afterRecalculate = new EventEmitter(); 15 | 16 | constructor(private _rate: number, private _dmg: number) { 17 | this.recalculate(); 18 | } 19 | 20 | get rate() { 21 | return this._rate; 22 | } 23 | 24 | set rate(val: number) { 25 | this._rate = val; 26 | this.recalculate(); 27 | } 28 | 29 | get dmg() { 30 | return this._dmg; 31 | } 32 | 33 | set dmg(val: number) { 34 | this._dmg = val; 35 | this.recalculate(); 36 | } 37 | 38 | recalculate() { 39 | this.value = 2 * this._rate + this._dmg; 40 | this.multiplier = 1 + Math.max(0, Math.min(1, this.rate) * this.dmg); 41 | this.ratio = this.dmg / this.rate; 42 | 43 | this.bestRate = this.value < 0 ? this.value / 2 : (this.value >= 4 ? 1 : this.value / 4); 44 | this.bestDmg = this.value < 0 ? 0 : (this.value >= 4 ? this.value - 2 : this.value / 2); 45 | this.bestMultiplier = 1 + this.bestRate * this.bestDmg; 46 | this.bestRatio = this.bestDmg / this.bestRate; 47 | 48 | this.efficiency = this.multiplier / this.bestMultiplier; 49 | 50 | this.afterRecalculate.emit(); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/styles.scss: -------------------------------------------------------------------------------- 1 | @use '@angular/material' as mat; 2 | 3 | $main-palette: ( 4 | 50: #e6f3ff, 5 | 100: #c3dfff, 6 | 200: #9fccff, 7 | 300: #7db8ff, 8 | 400: #69a8ff, 9 | 500: #5e99ff, 10 | 600: #5b8af0, 11 | 700: #5578db, 12 | 800: #5066c8, 13 | 900: #4847a7, 14 | contrast: ( 15 | 50: rgba(black, 0.87), 16 | 100: rgba(black, 0.87), 17 | 200: rgba(black, 0.87), 18 | 300: rgba(black, 0.87), 19 | 400: rgba(black, 0.87), 20 | 500: white, 21 | 600: white, 22 | 700: white, 23 | 800: white, 24 | 900: white 25 | ) 26 | ); 27 | 28 | @include mat.core(); 29 | 30 | $my-primary: mat.m2-define-palette($main-palette, 500); 31 | $my-accent: mat.m2-define-palette(mat.$m2-teal-palette, A200, A100, A400); 32 | $my-theme: mat.m2-define-dark-theme(( 33 | color: ( 34 | primary: $my-primary, 35 | accent: $my-accent, 36 | ), 37 | typography: mat.m2-define-typography-config(), 38 | density: 0 39 | )); 40 | 41 | @include mat.form-field-theme($my-theme); 42 | @include mat.card-theme($my-theme); 43 | @include mat.expansion-theme($my-theme); 44 | @include mat.tooltip-theme($my-theme); 45 | 46 | :root { 47 | color-scheme: dark; 48 | } 49 | 50 | html, 51 | body { 52 | height: unset; 53 | } 54 | 55 | body { 56 | background: #222; 57 | color: #fff; 58 | font-family: sans-serif; 59 | } 60 | 61 | mat-card-content { 62 | overflow-x: auto; 63 | overflow-y: hidden; 64 | } 65 | 66 | .mat-expansion-panel { 67 | .mat-expansion-panel-content { 68 | overflow-x: auto; 69 | overflow-y: hidden; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/app/calculator/calculator.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |

Current

5 |
6 | 7 | 8 | 9 | 10 | 11 | 16 | 17 |
18 | 19 | 20 |

Optimal

21 |
22 | 23 | 24 | {{critInfo.bestRate | percentage}} 25 | {{critInfo.bestDmg | percentage}} 26 | 27 | 32 | 33 |
34 | 35 | 36 | CRIT Multipliers when CRIT Value is {{critInfo.value * 100 | number:'1.0-2'}}% 37 | 38 | 39 | 40 | 41 | 42 |
43 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | import { AppComponent } from './app.component'; 4 | import { CalculatorComponent } from './calculator/calculator.component'; 5 | import { InputComponent } from './input/input.component'; 6 | import { SafeHtmlPipe } from './safe-html.pipe'; 7 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 8 | import { MatExpansionModule } from '@angular/material/expansion'; 9 | import { MatInputModule } from '@angular/material/input'; 10 | import { MatCardModule } from '@angular/material/card'; 11 | import { EquationsComponent } from './equations/equations.component'; 12 | import { EfficiencyGraphComponent } from './efficiency-graph/efficiency-graph.component'; 13 | import { FooterComponent } from './footer/footer.component'; 14 | import { OutputComponent } from './output/output.component'; 15 | import { MatTooltipModule } from '@angular/material/tooltip'; 16 | import { IORowComponent } from './io-row/io-row.component'; 17 | import { CalculationRowComponent } from './calculation-row/calculation-row.component'; 18 | import { PercentagePipe } from './percentage.pipe'; 19 | import { RatioPipe } from './ratio.pipe'; 20 | import { BaseGraphComponent } from './base-graph/base-graph.component'; 21 | 22 | @NgModule({ 23 | declarations: [ 24 | AppComponent, 25 | CalculatorComponent, 26 | InputComponent, 27 | SafeHtmlPipe, 28 | EquationsComponent, 29 | EfficiencyGraphComponent, 30 | FooterComponent, 31 | OutputComponent, 32 | IORowComponent, 33 | CalculationRowComponent, 34 | PercentagePipe, 35 | RatioPipe, 36 | BaseGraphComponent 37 | ], 38 | imports: [ 39 | BrowserModule, 40 | BrowserAnimationsModule, 41 | MatExpansionModule, 42 | MatCardModule, 43 | MatInputModule, 44 | MatTooltipModule 45 | ], 46 | bootstrap: [AppComponent] 47 | }) 48 | export class AppModule { } 49 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "schematics": { 6 | "@schematics/angular:component": { 7 | "skipTests": true, 8 | "standalone": false, 9 | "style": "scss" 10 | } 11 | }, 12 | "projects": { 13 | "critical-analysis": { 14 | "projectType": "application", 15 | "root": "", 16 | "sourceRoot": "src", 17 | "prefix": "app", 18 | "architect": { 19 | "build": { 20 | "builder": "@angular-builders/custom-webpack:browser", 21 | "options": { 22 | "customWebpackConfig": { 23 | "path": "./webpack.config.js" 24 | }, 25 | "outputPath": "dist/critical-analysis", 26 | "index": "src/index.html", 27 | "main": "src/main.ts", 28 | "polyfills": [ 29 | "zone.js" 30 | ], 31 | "tsConfig": "tsconfig.app.json", 32 | "inlineStyleLanguage": "scss", 33 | "assets": [ 34 | "src/favicon.ico" 35 | ], 36 | "styles": [ 37 | "src/styles.scss" 38 | ], 39 | "scripts": [] 40 | }, 41 | "configurations": { 42 | "production": { 43 | "budgets": [ 44 | { 45 | "type": "initial", 46 | "maximumWarning": "500kb", 47 | "maximumError": "1mb" 48 | }, 49 | { 50 | "type": "anyComponentStyle", 51 | "maximumWarning": "2kb", 52 | "maximumError": "4kb" 53 | } 54 | ], 55 | "outputHashing": "all" 56 | }, 57 | "development": { 58 | "buildOptimizer": false, 59 | "optimization": false, 60 | "vendorChunk": true, 61 | "extractLicenses": false, 62 | "sourceMap": true, 63 | "namedChunks": true 64 | } 65 | }, 66 | "defaultConfiguration": "production" 67 | }, 68 | "serve": { 69 | "builder": "@angular-builders/custom-webpack:dev-server", 70 | "configurations": { 71 | "production": { 72 | "buildTarget": "critical-analysis:build:production" 73 | }, 74 | "development": { 75 | "buildTarget": "critical-analysis:build:development" 76 | } 77 | }, 78 | "defaultConfiguration": "development" 79 | } 80 | } 81 | } 82 | }, 83 | "cli": { 84 | "analytics": false 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/app/math.html: -------------------------------------------------------------------------------- 1 |

CRIT Value

2 | 3 | CRIT Value 4 | = 5 | 2 6 | × 7 | CRIT Rate 8 | + 9 | CRIT DMG 10 | 11 |

Current Multiplier

12 | 13 | CRIT Mult 14 | = 15 | { 16 | 17 | 18 | 1 19 | 20 | if 21 | CRIT Rate 22 | 23 | 0 24 | 25 | 26 | 27 | 28 | 1 29 | + 30 | CRIT Rate 31 | × 32 | CRIT DMG 33 | 34 | 35 | if 36 | 0 37 | < 38 | CRIT Rate 39 | < 40 | 1 41 | 42 | 43 | 44 | 45 | 1 46 | + 47 | CRIT DMG 48 | 49 | 50 | if 51 | CRIT Rate 52 | 53 | 1 54 | 55 | 56 | 57 | 58 |

Current Efficiency

59 | 60 | CRIT Efficiency 61 | = 62 | 63 | 64 | CRIT Mult 65 | 66 | 67 | 68 | CRIT Mult 69 | max 70 | 71 | 72 | 73 | 74 |

Optimal CRIT Rate

75 | 76 | 77 | CRIT Rate 78 | opt 79 | 80 | = 81 | { 82 | 83 | 84 | 85 | 86 | 1 87 | 2 88 | 89 | × 90 | CRIT Value 91 | 92 | 93 | if 94 | CRIT Value 95 | 96 | 0 97 | 98 | 99 | 100 | 101 | 102 | 1 103 | 4 104 | 105 | × 106 | CRIT Value 107 | 108 | 109 | if 110 | 0 111 | < 112 | CRIT Value 113 | < 114 | 4 115 | 116 | 117 | 118 | 119 | 1 120 | 121 | 122 | if 123 | CRIT Value 124 | 125 | 4 126 | 127 | 128 | 129 | 130 |

Optimal CRIT DMG

131 | 132 | 133 | CRIT DMG 134 | opt 135 | 136 | = 137 | { 138 | 139 | 140 | 141 | 0 142 | 143 | 144 | if 145 | CRIT Value 146 | 147 | 0 148 | 149 | 150 | 151 | 152 | 153 | 1 154 | 2 155 | 156 | × 157 | CRIT Value 158 | 159 | 160 | if 161 | 0 162 | < 163 | CRIT Value 164 | < 165 | 4 166 | 167 | 168 | 169 | 170 | CRIT Value 171 | - 172 | 2 173 | 174 | 175 | if 176 | CRIT Value 177 | 178 | 4 179 | 180 | 181 | 182 | 183 |

Optimal Multiplier

184 | 185 | 186 | 187 | 188 | 189 | CRIT Mult 190 | max 191 | 192 | 193 | 194 | = 195 | 1 196 | + 197 | 198 | CRIT Rate 199 | opt 200 | 201 | × 202 | 203 | CRIT DMG 204 | opt 205 | 206 | 207 | 208 | 209 | 210 | 211 | = 212 | { 213 | 214 | 215 | 216 | 1 217 | 218 | 219 | if 220 | CRIT Value 221 | 222 | 0 223 | 224 | 225 | 226 | 227 | 1 228 | + 229 | 230 | 231 | 232 | 1 233 | 8 234 | 235 | 236 | ( 237 | CRIT Value 238 | ) 239 | 240 | 2 241 | 242 | 243 | 244 | if 245 | 0 246 | < 247 | CRIT Value 248 | < 249 | 4 250 | 251 | 252 | 253 | 254 | CRIT Value 255 | - 256 | 1 257 | 258 | 259 | if 260 | CRIT Value 261 | 262 | 4 263 | 264 | 265 | 266 | 267 | 268 | 269 | -------------------------------------------------------------------------------- /src/app/efficiency-graph/efficiency-graph.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, AfterContentInit } from '@angular/core'; 2 | import { CritInfo } from '../crit-info'; 3 | import { GraphPoint, GraphAxis, GraphLineCommand, BaseGraphComponent, GridPoint } from '../base-graph/base-graph.component'; 4 | 5 | @Component({ 6 | selector: 'app-efficiency-graph', 7 | templateUrl: './efficiency-graph.component.html' 8 | }) 9 | export class EfficiencyGraphComponent implements AfterContentInit { 10 | @Input() critInfo!: CritInfo; 11 | 12 | startPoint: GridPoint = [0, 0]; 13 | size: GridPoint = [0, 0]; 14 | points: GraphPoint[] = []; 15 | axes: GraphAxis[] = []; 16 | lineCmds: GraphLineCommand[] = []; 17 | 18 | static readonly CRIT_DMG_COLOR = '#B1CC00'; 19 | static readonly CRIT_RATE_COLOR = '#F2BEFC'; 20 | static readonly CURRENT_COLOR = '#F63C61'; 21 | static readonly OPTIMAL_COLOR = '#FFA552'; 22 | 23 | setBounds() { 24 | const lowestValue = Math.min(this.critInfo.rate * 220, 0, this.critInfo.value * -10); 25 | const highestValue = Math.max(this.critInfo.rate * -20, this.critInfo.value * 110); 26 | 27 | this.startPoint = [ 28 | lowestValue, 29 | this.critInfo.bestMultiplier * -10 30 | ] 31 | 32 | this.size = [ 33 | highestValue - lowestValue, 34 | this.critInfo.bestMultiplier * 130 35 | ]; 36 | } 37 | 38 | /** 39 | * Get the CRIT multiplier for the graph at the given CRIT rate value. 40 | */ 41 | getMultiplier(rateValue: number) { 42 | // Either CRIT rate or DMG are 0 or less 43 | if (rateValue <= 0 || this.critInfo.value * 100 <= rateValue) return 100; 44 | 45 | // CRIT rate >= 100 46 | if (rateValue >= 200) return this.critInfo.value * 100 - rateValue + 100; 47 | 48 | // [CRIT dmg] * [CRIT rate] + 1 49 | return (this.critInfo.value * 100 - rateValue) * (rateValue / 200) + 100; 50 | } 51 | 52 | drawAxes() { 53 | this.axes = [ 54 | { 55 | dir: 'hoz', 56 | color: EfficiencyGraphComponent.CRIT_RATE_COLOR, 57 | offset: this.critInfo.value * 100, 58 | label: 'CRIT Rate (%)', 59 | labelOffset: 1.5, 60 | textPos: [ 61 | {position: 'near', textOffset: 1, alignment: 'text-top'} 62 | ], 63 | getOverride: value => ({ 64 | to: this.getMultiplier(value) 65 | }), 66 | displayFactor: 0.5 67 | }, 68 | { 69 | dir: 'hoz', 70 | color: EfficiencyGraphComponent.CRIT_DMG_COLOR, 71 | offset: this.critInfo.value * 100, 72 | label: 'CRIT DMG (%)', 73 | labelOffset: -0.7, 74 | textPos: [ 75 | {position: 'far', textOffset: -1, alignment: 'hanging'} 76 | ], 77 | getOverride: value => ({ 78 | from: this.startPoint[1] + this.size[1], 79 | to: this.getMultiplier(value) 80 | }), 81 | inverse: true 82 | }, 83 | { 84 | dir: 'vert', 85 | offset: this.critInfo.value * 100, 86 | label: 'CRIT Multiplier (%)', 87 | labelOffset: -2.55, 88 | textPos: [ 89 | {position: 'near', textOffset: -0.5, alignment: 'end'}, 90 | {position: 'far', textOffset: 0.5, alignment: 'start'} 91 | ] 92 | } 93 | ]; 94 | } 95 | 96 | drawLines() { 97 | // Start line 98 | this.lineCmds = [ 99 | { 100 | type: 'move', 101 | x: this.startPoint[0], 102 | y: 100 103 | }, 104 | { 105 | type: 'line-to', 106 | x: 0, 107 | y: 100 108 | } 109 | ]; 110 | 111 | // Parabola 112 | if (this.critInfo.value > 0) { 113 | const curveEndX = this.critInfo.value > 2 ? 200 : this.critInfo.value * 100; 114 | 115 | // https://math.stackexchange.com/a/1258196 116 | this.lineCmds.push({ 117 | type: 'bezier', 118 | ctrlX: curveEndX / 2, 119 | ctrlY: (curveEndX / 4) * this.critInfo.value + 100, 120 | x: curveEndX, 121 | y: this.getMultiplier(curveEndX) 122 | }); 123 | } 124 | 125 | // CRIT rate > 100 126 | if (this.critInfo.value > 2) 127 | this.lineCmds.push({ 128 | type: 'line-to', 129 | x: this.critInfo.value * 100, 130 | y: 100 131 | }); 132 | 133 | // End line 134 | this.lineCmds.push({ 135 | type: 'line-to', 136 | x: this.startPoint[0] + this.size[0], 137 | y: 100 138 | }); 139 | } 140 | 141 | drawPoints() { 142 | const isOptimal = Math.abs(this.critInfo.multiplier - this.critInfo.bestMultiplier) < BaseGraphComponent.EPSILON; 143 | 144 | this.points = [ 145 | { 146 | x: this.critInfo.rate * 200, 147 | y: this.critInfo.multiplier * 100, 148 | label: isOptimal ? 'Current (Optimal)' : 'Current', 149 | color: EfficiencyGraphComponent.CURRENT_COLOR, 150 | labelRelX: 0.5 * (this.critInfo.rate <= this.critInfo.bestRate ? 1 : -1), 151 | labelRelY: 0.5, 152 | anchor: (this.critInfo.rate <= this.critInfo.bestRate) ? 'start' : 'end', 153 | baseline: 'hanging' 154 | } 155 | ]; 156 | 157 | if (!isOptimal) 158 | this.points.push({ 159 | x: this.critInfo.bestRate * 200, 160 | y: this.critInfo.bestMultiplier * 100, 161 | label: 'Optimal', 162 | color: EfficiencyGraphComponent.OPTIMAL_COLOR, 163 | labelRelX: 0.5, 164 | labelRelY: -0.5 165 | }); 166 | } 167 | 168 | update() { 169 | this.setBounds(); 170 | this.drawAxes(); 171 | this.drawLines(); 172 | this.drawPoints(); 173 | } 174 | 175 | ngAfterContentInit() { 176 | this.critInfo.afterRecalculate.subscribe(this.update.bind(this)); 177 | this.update(); 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /src/app/base-graph/base-graph.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | 3 | export interface GraphPoint { 4 | x: number; 5 | y: number; 6 | label: string; 7 | color: string; 8 | labelRelX: number; 9 | labelRelY: number; 10 | anchor?: string; 11 | baseline?: string; 12 | } 13 | 14 | export interface GraphAxis { 15 | dir: 'hoz' | 'vert', 16 | color?: string; 17 | offset: number; 18 | label: string; 19 | labelOffset: number; 20 | textPos: { 21 | position: 'near' | 'far'; 22 | textOffset: number; 23 | alignment: string; 24 | }[]; 25 | displayFactor?: number; 26 | inverse?: boolean; 27 | getOverride?: (value: number) => {from?: number, to?: number}; 28 | } 29 | 30 | export type GraphLineCommand = { 31 | type: 'move' | 'line-to'; 32 | x: number; 33 | y: number; 34 | } | { 35 | type: 'bezier'; 36 | ctrlX: number; 37 | ctrlY: number; 38 | x: number; 39 | y: number; 40 | } 41 | 42 | export type GridPoint = [number, number]; 43 | 44 | @Component({ 45 | selector: 'app-base-graph', 46 | templateUrl: './base-graph.component.html', 47 | styleUrl: './base-graph.component.scss' 48 | }) 49 | export class BaseGraphComponent { 50 | /** 51 | * Bottom-left corner of the graph. 52 | */ 53 | @Input() startPoint: GridPoint = [0, 0]; 54 | /** 55 | * Width and height of the graph. 56 | */ 57 | @Input() size: GridPoint = [0, 0]; 58 | @Input() points: GraphPoint[] = []; 59 | @Input() axes: GraphAxis[] = []; 60 | @Input() lineCmds: GraphLineCommand[] = []; 61 | 62 | static readonly EPSILON = 0.00001; 63 | 64 | static readonly STEPS = [ 65 | [20, 5], [40, 10], [80, 20], [160, 40], 66 | [400, 100], [1000, 200], [Infinity, 500] 67 | ]; 68 | 69 | static readonly LINE_COLOR = '#5E9AFF'; 70 | static readonly LINE_SIZE = 3; 71 | static readonly DEFAULT_AXIS_COLOR = '#FFFFFF'; 72 | 73 | static SVG_COMMANDS: Record = { 74 | 'move': 'M', 75 | 'line-to': 'L', 76 | 'bezier': 'Q' 77 | }; 78 | 79 | scaleX = 0; 80 | scaleY = -1; 81 | viewBox = '0 0 0 0'; 82 | fontSize = 12; 83 | 84 | svgPaths: { 85 | size: number; 86 | color: string; 87 | cmd: string; 88 | crisp?: boolean; 89 | }[] = []; 90 | 91 | svgTexts: { 92 | x: number; 93 | y: number; 94 | str: string; 95 | baseline?: string; 96 | anchor?: string; 97 | sideways?: boolean; 98 | fontScale?: number; 99 | color?: string; 100 | }[] = []; 101 | 102 | svgPoints: { 103 | x: number; 104 | y: number; 105 | color?: string; 106 | }[] = []; 107 | 108 | private calculateTextPos( 109 | hoz: boolean, 110 | val: 'center' | number, 111 | textDimension: GraphAxis['textPos'][0] 112 | ) { 113 | if (val === 'center') { 114 | const posIndex = hoz ? 0 : 1; 115 | val = this.startPoint[posIndex] + this.size[posIndex] / 2; 116 | } 117 | 118 | const otherPos = hoz ? 1 : 0; 119 | const otherAxis = textDimension.position === 'near' 120 | ? this.startPoint[otherPos] 121 | : this.startPoint[otherPos] + this.size[otherPos]; 122 | 123 | return { 124 | x: hoz 125 | ? val * this.scaleX 126 | : otherAxis * this.scaleX + textDimension.textOffset * this.fontSize, 127 | y: hoz 128 | ? otherAxis * this.scaleY + textDimension.textOffset * this.fontSize 129 | : val * this.scaleY, 130 | }; 131 | } 132 | 133 | drawAxis({ 134 | dir, color, offset, label, labelOffset, textPos, getOverride, displayFactor, inverse 135 | }: GraphAxis) { 136 | displayFactor ??= 1; 137 | color ??= BaseGraphComponent.DEFAULT_AXIS_COLOR; 138 | 139 | const hoz = dir === 'hoz'; 140 | const posIndex = hoz ? 0 : 1; 141 | 142 | let start = this.startPoint[posIndex]; 143 | let end = start + this.size[posIndex]; 144 | 145 | if (inverse) { 146 | [end, start] = [start, end]; 147 | } 148 | 149 | const shouldContinue: (a: number, b: number) => boolean = inverse 150 | ? (a, b) => a >= b 151 | : (a, b) => a <= b; 152 | 153 | const range = Math.abs(start - end); 154 | const step = BaseGraphComponent.STEPS.find(([maxRange]) => range < maxRange)![1]; 155 | const increment = (step / 5) * (inverse ? -1 : 1); 156 | 157 | start += ((inverse ? offset : 0) - start) % increment; 158 | 159 | this.svgTexts.push({ 160 | ...this.calculateTextPos(hoz, 'center', { 161 | ...textPos[0], 162 | textOffset: textPos[0].textOffset + labelOffset 163 | }), 164 | str: label, 165 | baseline: 'baseline', 166 | anchor: 'middle', 167 | sideways: !hoz, 168 | fontScale: 1.15 169 | }); 170 | 171 | for (let value = start; shouldContinue(value, end); value += increment) { 172 | const adjustedValue = inverse 173 | ? Math.round((offset - value) * 1000) / 1000 174 | : value; 175 | 176 | const secondaryOrAxis = adjustedValue % step === 0; 177 | 178 | if (secondaryOrAxis) 179 | textPos.forEach(textDimension => this.svgTexts.push({ 180 | ...this.calculateTextPos(hoz, value, textDimension), 181 | str: (adjustedValue * displayFactor).toString(), 182 | anchor: hoz ? 'middle' : textDimension.alignment, 183 | baseline: hoz ? textDimension.alignment : 'middle', 184 | color 185 | })); 186 | 187 | const override = getOverride?.(value) ?? {}; 188 | 189 | const cmd = hoz 190 | ? `M ${value * this.scaleX} ${(override.from ?? this.startPoint[1]) * this.scaleY} L ${value * this.scaleX} ${(override.to ?? (this.startPoint[1] + this.size[1])) * this.scaleY}` 191 | : `M ${(override.from ?? this.startPoint[0]) * this.scaleX} ${value * this.scaleY} L ${(override.to ?? (this.startPoint[0] + this.size[0])) * this.scaleX} ${value * this.scaleY}` 192 | 193 | if (adjustedValue === 0) { 194 | this.svgPaths.push({size: 2, color, crisp: true, cmd}); 195 | continue; 196 | } 197 | 198 | if (secondaryOrAxis) { 199 | this.svgPaths.push({size: 1, color, crisp: true, cmd}); 200 | continue; 201 | } 202 | 203 | this.svgPaths.push({size: 1, color: color + '55', crisp: true, cmd}); 204 | } 205 | } 206 | 207 | drawPoint({ 208 | x, y, label, color, labelRelX, labelRelY, anchor, baseline 209 | }: GraphPoint) { 210 | this.svgPoints.push({x: x * this.scaleX, y: y * this.scaleY, color}); 211 | 212 | this.svgTexts.push({ 213 | str: label, 214 | fontScale: 1.25, 215 | x: x * this.scaleX + labelRelX * this.fontSize, 216 | y: y * this.scaleY + labelRelY * this.fontSize, 217 | color, 218 | anchor, 219 | baseline 220 | }); 221 | } 222 | 223 | drawLine() { 224 | const cmd = this.lineCmds.map(cmd => { 225 | const svgCmd = BaseGraphComponent.SVG_COMMANDS[cmd.type]; 226 | 227 | if (cmd.type === 'bezier') { 228 | return `${svgCmd} ${cmd.ctrlX * this.scaleX} ${cmd.ctrlY * this.scaleY} ${cmd.x * this.scaleX} ${cmd.y * this.scaleY}`; 229 | } 230 | 231 | return `${svgCmd} ${cmd.x * this.scaleX} ${cmd.y * this.scaleY}`; 232 | }).join(' '); 233 | 234 | this.svgPaths.push({ 235 | size: BaseGraphComponent.LINE_SIZE, 236 | color: BaseGraphComponent.LINE_COLOR, 237 | cmd 238 | }); 239 | } 240 | 241 | ngOnChanges() { 242 | this.scaleX = this.size[1] / this.size[0]; 243 | this.fontSize = this.size[1] / 22; 244 | 245 | this.viewBox = [ 246 | this.startPoint[0] * this.scaleX - this.fontSize * 3.65, 247 | (this.startPoint[1] + this.size[1]) * this.scaleY - this.fontSize * 2.8, 248 | this.size[0] * this.scaleX + this.fontSize * 7.3, 249 | -this.size[1] * this.scaleY + this.fontSize * 5.6 250 | ].join(' '); 251 | 252 | this.svgPaths = []; 253 | this.svgPoints = []; 254 | this.svgTexts = []; 255 | 256 | this.axes.forEach(axis => this.drawAxis(axis)); 257 | this.points.forEach(point => this.drawPoint(point)); 258 | this.drawLine(); 259 | } 260 | } 261 | --------------------------------------------------------------------------------