@obliczeniowo/elementary
Version:
Library made in Angular version 20
316 lines (310 loc) • 13 kB
JavaScript
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