UNPKG

@obliczeniowo/elementary

Version:
316 lines (310 loc) 13 kB
import * as i0 from '@angular/core'; import { EventEmitter, Input, HostBinding, ViewChild, Output, Component, NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { LinePattern, TextAlign, DrawingSvgInterface } from '@obliczeniowo/elementary/drawing'; import { Point2D } from '@obliczeniowo/elementary/classes'; import { ElementaryMath } from '@obliczeniowo/elementary/math'; import { NG_VALUE_ACCESSOR } from '@angular/forms'; class Radar { ctx; levels; categories; groups; size; greedColor = 'var(--obl-radar-greed-color, var(--obl-linear-diagram-greed-color))'; closestSet; constructor(ctx, levels = 5, categories = [], groups = [], size = 500) { this.ctx = ctx; this.levels = levels; this.categories = categories; this.groups = groups; this.size = size; } getCenter() { return new Point2D(this.size / 2, this.size / 2); } getRay() { return this.size / 2 - 50; } getMaxLevel() { return typeof this.levels === 'number' ? this.levels : this.levels.length; } getDRawy() { return this.getRay() / (this.getMaxLevel() || 1); } getDAngle() { return Math.PI * 2 / (this.categories.length || 1); } draw(ctx = this.ctx) { ctx.clear(); const { categories, greedColor } = this; if (categories.length < 3) { throw new Error('Number of categories must be >= 3'); } const count = categories.length; const center = this.getCenter(); const ray = this.getRay(); const dr = this.getDRawy(); const dA = this.getDAngle(); const maxLevel = this.getMaxLevel(); ctx.setFontSize(8); if (this.groups) { ctx.setLinePattern(LinePattern.NONE); this.groups.forEach((group, index) => { let polygon = []; const { values } = group; if (values.length === count) { values.forEach((item, index) => { const angle = index * dA; const r = ray * item / maxLevel; polygon = polygon.concat([ center.add(new Point2D(r * Math.sin(angle), r * Math.cos(angle)).rotate(Math.PI)) ]); }); } ctx.drawPolygon(polygon, group.stroke, group.color, group.fill, { close: true, opacity: group.opacity || 0.5 }) .drawPolyline(polygon, group.stroke, group.color, { close: true }); }); } ctx.setTextAlign(TextAlign.LEFT) .group(); let mapping = typeof this.levels === 'number' ? [] : this.levels; const offset = new Point2D(5, 4); for (let dRay = 1; dRay <= maxLevel; dRay++) { const ray = dr * dRay; for (let dAngle = 0; dAngle < count; dAngle++) { const angle = dAngle * dA; ctx.setLinePattern(LinePattern.DASHED) .drawLine(center.add(new Point2D(ray * Math.sin(angle), ray * Math.cos(angle)).rotate(Math.PI)), center.add(new Point2D(ray * Math.sin(angle + dA), ray * Math.cos(angle + dA)).rotate(Math.PI)), 0.25, greedColor); } ctx.drawText(`${mapping[dRay - 1] || dRay}`, center.add(new Point2D(0, -ray)).add(offset), greedColor, 0); } ctx.setTextAlign(TextAlign.CENTER); for (let dAngle = 0; dAngle < count; dAngle++) { const angle = dAngle * dA; ctx.setLinePattern(LinePattern.DASHED) .drawLine(center.add(new Point2D(ray * Math.sin(angle), ray * Math.cos(angle)).rotate(Math.PI)), center, 0.25, greedColor); const angleCondition = angle > Math.PI / 2 && angle < Math.PI / 2 * 3; const r = ray + (angleCondition && 10 || 5); const start = new Point2D(r * Math.sin(angle), r * Math.cos(angle)).rotate(Math.PI); const angleText = angleCondition ? Math.PI : 0; const value = this.groups[0].values[dAngle]; ctx.drawText(categories[dAngle] + (this.groups.length === 1 ? `: ${mapping[value - 1] ?? this.groups[0].values[dAngle]}` : ''), start.add(center), 'gray', ElementaryMath.radiansToDegrees(angle + angleText), {}); } ctx.endGroup(); if (this.closestSet) { ctx.setLinePattern(LinePattern.NONE); const pos = this.closestSet.closest.add(center); ctx.drawCircle(pos, 2, 1, 'red', 'white') .setTextAlign(this.closestSet.closest.x > 0 ? TextAlign.RIGHT : TextAlign.LEFT) .drawText(`${mapping[this.closestSet.newLevel - 1] ?? this.closestSet.newLevel}`, pos.add(new Point2D(this.closestSet.closest.x > 0 ? -5 : 5, 4)), 'red', 0); } } onMove(point) { this.closestSet = this.getClosest(point); this.draw(); } getClosest(point) { const center = this.getCenter(); point = point.subtract(center); let closest = center.copy(); let distance = Infinity; let newLevel = 0; let category = 0; if (this.groups.length === 1) { const { categories } = this; const count = categories.length; const dr = this.getDRawy(); const dA = this.getDAngle(); const maxLevel = this.getMaxLevel(); for (let rayIndex = 1; rayIndex <= maxLevel; rayIndex++) { const ray = dr * rayIndex; for (let angleIndex = 0; angleIndex < count; angleIndex++) { const angle = angleIndex * dA; const checkPoint = new Point2D(ray * Math.sin(angle), ray * Math.cos(angle)).rotate(Math.PI); const dist = checkPoint.subtract(point).sickLength(); if (distance > dist) { closest = checkPoint; newLevel = rayIndex; distance = dist; category = angleIndex; } } } } return { closest, category, newLevel }; } clicked(point) { const { closest, category, newLevel } = this.getClosest(point); if (!closest.isZeroLength) { this.groups[0].values[category] = newLevel; this.draw(); return this.groups[0]; } return null; } } class RadarPickerComponent { renderer; dc; radar; mousePos; /** * as number describe how many of levels should exist and then build levels from 1 to given number * as array length describe number of levels that starts from 1 to levels.length and value inside of * array is something to display */ levels = 5; /** * Categories list as string */ categories = []; /** * values and connected to them colors * values.values.length must be equal categories.length */ values; changed = new EventEmitter(); size = 500; diagram; disabled; onChange = (set) => { this.changed.emit({ ...set.values }); }; onTouched = () => { }; constructor(renderer) { this.renderer = renderer; } ngAfterViewInit() { this.draw(); } draw(dc = this.dc) { if (!dc) { if (!this.diagram) { return; } if (!this.dc) { this.dc = new DrawingSvgInterface(this.diagram.nativeElement, this.renderer); if (this.values) { this.radar = new Radar(this.dc, this.levels, this.categories, [this.values], this.size); } } dc = this.dc; } this.radar?.draw(dc); } ngOnChanges(changes) { if (changes.values || changes.categories || changes.levels || changes.size && this.diagram && this.values && this.categories && this.levels) { this.radar = new Radar(this.dc, this.levels, this.categories, [this.values], this.size); this.draw(); } } move(event) { if (this.disabled) { return; } const rect = this.diagram.nativeElement.getBoundingClientRect(); this.mousePos = new Point2D((event.clientX - rect.left) / rect.width * this.size, (event.clientY - rect.top) / rect.height * this.size); this.radar?.onMove(this.mousePos); } clicked(event) { if (this.disabled) { return; } if (!this.mousePos) { this.move(event); } const set = this.radar.clicked(this.mousePos); if (set) { this.values = set; const { categories, levels } = this; this.changed.emit({ ...set }); this.onChange({ values: set, categories, levels }); } } writeValue(set) { Object.assign(this, set); this.draw(); } registerOnChange(onChange) { this.onChange = onChange; } registerOnTouched(onTouched) { this.onTouched = onTouched; } setDisabledState(isDisabled) { this.disabled = isDisabled; } onMouseLeave() { if (this.radar) { this.radar.closestSet = undefined; this.draw(); } } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.4", ngImport: i0, type: RadarPickerComponent, deps: [{ token: i0.Renderer2 }], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.0.4", type: RadarPickerComponent, isStandalone: false, selector: "obl-radar-picker", inputs: { levels: "levels", categories: "categories", values: "values", disabled: "disabled" }, outputs: { changed: "changed" }, host: { properties: { "class.disabled": "this.disabled" } }, providers: [ { provide: NG_VALUE_ACCESSOR, multi: true, useExisting: RadarPickerComponent }, ], viewQueries: [{ propertyName: "diagram", first: true, predicate: ["radar"], descendants: true }], usesOnChanges: true, ngImport: i0, template: "<svg\n [attr.viewBox]=\"'0 0 ' + size + ' ' + size\"\n (mousemove)=\"move($event)\"\n (click)=\"clicked($event)\"\n (mouseout)=\"onMouseLeave()\"\n #radar\n></svg>\n", styles: [""] }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.4", ngImport: i0, type: RadarPickerComponent, decorators: [{ type: Component, args: [{ selector: 'obl-radar-picker', providers: [ { provide: NG_VALUE_ACCESSOR, multi: true, useExisting: RadarPickerComponent }, ], standalone: false, template: "<svg\n [attr.viewBox]=\"'0 0 ' + size + ' ' + size\"\n (mousemove)=\"move($event)\"\n (click)=\"clicked($event)\"\n (mouseout)=\"onMouseLeave()\"\n #radar\n></svg>\n" }] }], ctorParameters: () => [{ type: i0.Renderer2 }], propDecorators: { levels: [{ type: Input }], categories: [{ type: Input }], values: [{ type: Input }], changed: [{ type: Output }], diagram: [{ type: ViewChild, args: ['radar'] }], disabled: [{ type: Input }, { type: HostBinding, args: ['class.disabled'] }] } }); class RadarModule { static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.4", ngImport: i0, type: RadarModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "20.0.4", ngImport: i0, type: RadarModule, declarations: [RadarPickerComponent], imports: [CommonModule], exports: [RadarPickerComponent] }); static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "20.0.4", ngImport: i0, type: RadarModule, imports: [CommonModule] }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.4", ngImport: i0, type: RadarModule, decorators: [{ type: NgModule, args: [{ declarations: [ RadarPickerComponent ], imports: [ CommonModule ], exports: [ RadarPickerComponent ] }] }] }); /** * Generated bundle index. Do not edit. */ export { RadarModule, RadarPickerComponent }; //# sourceMappingURL=obliczeniowo-elementary-radar.mjs.map