mirror of
https://github.com/ringtools/ringtools-web-v2.git
synced 2024-11-18 23:30:01 +01:00
Fully implemented
This commit is contained in:
parent
dd6cad0e75
commit
c31eef4eee
@ -37,6 +37,7 @@
|
||||
"ng2-dragula": "^2.1.1",
|
||||
"ngrx-store-localstorage": "^12.0.1",
|
||||
"rxjs": "7.5.2",
|
||||
"save-svg-as-png": "^1.4.17",
|
||||
"socket.io-client": "^4.4.1",
|
||||
"timsort": "^0.3.0",
|
||||
"tslib": "^2.3.0",
|
||||
|
@ -2,6 +2,7 @@ import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { HomeComponent } from './components/home/home.component';
|
||||
import { OverviewComponent } from './components/overview/overview.component';
|
||||
import { RingOnlyComponent } from './components/ring-only/ring-only.component';
|
||||
import { SettingsComponent } from './components/settings/settings.component';
|
||||
import { VisualComponent } from './components/visual/visual.component';
|
||||
import { BaseLayoutComponent } from './layout/base/base.component';
|
||||
@ -22,8 +23,10 @@ const routes: Routes = [
|
||||
{
|
||||
path: 'visual', component: VisualComponent
|
||||
},
|
||||
]
|
||||
},
|
||||
],
|
||||
}, {
|
||||
path: 'ring-only', component: RingOnlyComponent
|
||||
}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
@ -9,6 +9,7 @@ import { PartialsModule } from '../partials/partials.module';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { NgbButtonsModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { RingOnlyComponent } from './ring-only/ring-only.component';
|
||||
|
||||
|
||||
|
||||
@ -17,7 +18,8 @@ import { BrowserModule } from '@angular/platform-browser';
|
||||
HomeComponent,
|
||||
OverviewComponent,
|
||||
SettingsComponent,
|
||||
VisualComponent
|
||||
VisualComponent,
|
||||
RingOnlyComponent
|
||||
],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
|
@ -1,8 +1,8 @@
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-md-4 chart-container d-inline-block" id="circle">
|
||||
<div class="col-md-4 chart-container d-inline-block" id="circle" *ngIf="ring !== undefined">
|
||||
<app-participant-ring class="w-100 h-100 d-flex" [ringName]="settings.ringName"
|
||||
[showLogo]="settings.showLogo" id="rofvisual"></app-participant-ring>
|
||||
[showLogo]="settings.showLogo" id="rofvisual" [ring]="ring"></app-participant-ring>
|
||||
</div>
|
||||
|
||||
<div class="col-md-8 participants">
|
||||
|
@ -1,13 +1,12 @@
|
||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { Component, OnDestroy } from '@angular/core';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { timeHours } from 'd3';
|
||||
import { Observable } from 'rxjs';
|
||||
import * as svg from 'save-svg-as-png';
|
||||
import { upsertNodeInfo } from 'src/app/actions/node-info.actions';
|
||||
import { setViewMode } from 'src/app/actions/setting.actions';
|
||||
import { NodeInfo } from 'src/app/models/node-info.model';
|
||||
import { NodeOwner } from 'src/app/models/node-owner.model';
|
||||
import { IRing } from 'src/app/models/ring.model';
|
||||
import { NodeOwnersState } from 'src/app/reducers/node-owner.reducer';
|
||||
import { SettingState } from 'src/app/reducers/setting.reducer';
|
||||
import { selectNodeOwners } from 'src/app/selectors/node-owner.selectors';
|
||||
import { selectSettings } from 'src/app/selectors/setting.selectors';
|
||||
@ -29,7 +28,7 @@ export class OverviewComponent implements OnDestroy {
|
||||
|
||||
constructor(
|
||||
private store: Store<fromRoot.State>,
|
||||
private lnData: LnDataService
|
||||
private lnData: LnDataService,
|
||||
) {
|
||||
this.nodeOwners$ = this.store.select(selectNodeOwners);
|
||||
this.settings$ = this.store.select(selectSettings);
|
||||
@ -39,17 +38,18 @@ export class OverviewComponent implements OnDestroy {
|
||||
});
|
||||
|
||||
this.nodeOwners$.subscribe((nodeOwners: NodeOwner[]) => {
|
||||
for (let owner of nodeOwners) {
|
||||
this.lnData.nodeSocket.emit('subscribe', [owner.pub_key]);
|
||||
}
|
||||
this.lnData.nodeSocket.emit(
|
||||
'subscribe',
|
||||
nodeOwners.map((no) => no.pub_key)
|
||||
);
|
||||
|
||||
this.ring = this.makeRing(nodeOwners);
|
||||
});
|
||||
|
||||
this.lnData.nodeSocket.on('node', (data: NodeInfo) => {
|
||||
this.store.dispatch(upsertNodeInfo({ nodeInfo: data }))
|
||||
this.store.dispatch(upsertNodeInfo({ nodeInfo: data }));
|
||||
|
||||
this.nodeData.set(data.node.pub_key, Object.assign(new NodeInfo, data));
|
||||
this.nodeData.set(data.node.pub_key, Object.assign(new NodeInfo(), data));
|
||||
this.refreshRing();
|
||||
});
|
||||
}
|
||||
@ -62,20 +62,36 @@ export class OverviewComponent implements OnDestroy {
|
||||
this.store.dispatch(setViewMode(event));
|
||||
}
|
||||
|
||||
downloadAsPng() {}
|
||||
downloadAsPng() {
|
||||
const ringName = this.settings.ringName;
|
||||
const container = document.getElementById('rofvisual');
|
||||
|
||||
if (!container) return;
|
||||
|
||||
svg.saveSvgAsPng(container.children[0], `${ringName}.png`, {
|
||||
backgroundColor: '#000',
|
||||
scale: 1.5,
|
||||
});
|
||||
}
|
||||
|
||||
makeRing(ringParticipants: NodeOwner[]) {
|
||||
let ring: IRing = [];
|
||||
for (const [i, node] of ringParticipants.entries()) {
|
||||
let nextIndex = (i + 1) % ringParticipants.length;
|
||||
|
||||
let channel = this.nodeData.get(ringParticipants[i].pub_key)?.hasChannelWith(ringParticipants[nextIndex].pub_key);
|
||||
let channel = this.nodeData
|
||||
.get(ringParticipants[i].pub_key)
|
||||
?.hasChannelWith(ringParticipants[nextIndex].pub_key);
|
||||
|
||||
ring.push([
|
||||
Object.assign(new NodeOwner(), ringParticipants[i]),
|
||||
Object.assign(new NodeOwner(), ringParticipants[nextIndex]),
|
||||
channel,
|
||||
channel ? this.nodeData.get(ringParticipants[i].pub_key)?.getChannelPolicies(ringParticipants[nextIndex].pub_key, channel) : undefined
|
||||
channel
|
||||
? this.nodeData
|
||||
.get(ringParticipants[i].pub_key)
|
||||
?.getChannelPolicies(ringParticipants[nextIndex].pub_key, channel)
|
||||
: undefined,
|
||||
]);
|
||||
}
|
||||
|
||||
@ -83,13 +99,19 @@ export class OverviewComponent implements OnDestroy {
|
||||
}
|
||||
|
||||
refreshRing() {
|
||||
for (const rp of this.ring) {
|
||||
let channel = this.nodeData.get(rp[0].pub_key)?.hasChannelWith(rp[1].pub_key);
|
||||
const ring = this.ring;
|
||||
for (const rp of ring) {
|
||||
let channel = this.nodeData
|
||||
.get(rp[0].pub_key)
|
||||
?.hasChannelWith(rp[1].pub_key);
|
||||
|
||||
if (channel) {
|
||||
rp[2] = channel;
|
||||
rp[3] = this.nodeData.get(rp[0].pub_key)?.getChannelPolicies(rp[1].pub_key, channel);
|
||||
rp[3] = this.nodeData
|
||||
.get(rp[0].pub_key)
|
||||
?.getChannelPolicies(rp[1].pub_key, channel);
|
||||
}
|
||||
}
|
||||
this.ring = [...ring];
|
||||
}
|
||||
}
|
||||
|
2
src/app/components/ring-only/ring-only.component.html
Normal file
2
src/app/components/ring-only/ring-only.component.html
Normal file
@ -0,0 +1,2 @@
|
||||
<app-participant-ring class="w-100 h-100 d-flex" id="rofvisual" [ring]="ring" *ngIf="isReady" [withFire]="onFire"
|
||||
[withArrow]="withArrow" [showLogo]="true"></app-participant-ring>
|
25
src/app/components/ring-only/ring-only.component.spec.ts
Normal file
25
src/app/components/ring-only/ring-only.component.spec.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { RingOnlyComponent } from './ring-only.component';
|
||||
|
||||
describe('RingOnlyComponent', () => {
|
||||
let component: RingOnlyComponent;
|
||||
let fixture: ComponentFixture<RingOnlyComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ RingOnlyComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(RingOnlyComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
45
src/app/components/ring-only/ring-only.component.ts
Normal file
45
src/app/components/ring-only/ring-only.component.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { Observable } from 'rxjs';
|
||||
import { upsertNodeInfo } from 'src/app/actions/node-info.actions';
|
||||
import { NodeInfo } from 'src/app/models/node-info.model';
|
||||
import { NodeOwner } from 'src/app/models/node-owner.model';
|
||||
import { IRing } from 'src/app/models/ring.model';
|
||||
import { SettingState } from 'src/app/reducers/setting.reducer';
|
||||
import { LnDataService } from 'src/app/services/ln-data.service';
|
||||
import { RingDataService } from 'src/app/services/ring-data.service';
|
||||
import { stringToBoolean } from 'src/app/utils/utils';
|
||||
import * as fromRoot from '../../reducers';
|
||||
|
||||
@Component({
|
||||
selector: 'app-ring-only',
|
||||
templateUrl: './ring-only.component.html',
|
||||
styleUrls: ['./ring-only.component.scss'],
|
||||
})
|
||||
export class RingOnlyComponent {
|
||||
ring: IRing = [];
|
||||
onFire: boolean = true;
|
||||
withArrow: boolean = false;
|
||||
isReady = false;
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private ringData: RingDataService
|
||||
) {
|
||||
const onFire = this.route.snapshot.queryParamMap.get('onFire');
|
||||
const withArrow = this.route.snapshot.queryParamMap.get('withArrow');
|
||||
|
||||
if (onFire) {
|
||||
this.onFire = stringToBoolean(onFire);
|
||||
}
|
||||
|
||||
if (withArrow) {
|
||||
this.withArrow = stringToBoolean(withArrow);
|
||||
}
|
||||
this.ringData.getRing().then((ring) => {
|
||||
this.ring = ring;
|
||||
this.isReady = true;
|
||||
});
|
||||
}
|
||||
}
|
@ -8,7 +8,7 @@
|
||||
<input type="text" class="form-control" formControlName="name" id="ringName"
|
||||
placeholder="Ring Name" name="ringName">
|
||||
<div class="input-group-append">
|
||||
<button (click)="parseCapacityName()" class="btn btn-secondary">Parse capacity</button>
|
||||
<button (click)="parseCapacityName()" class="btn btn-secondary" type="button">Parse capacity</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -8,6 +8,7 @@ import {
|
||||
loadRingSetting,
|
||||
setRingName,
|
||||
setRingSize,
|
||||
setShowLogo,
|
||||
} from 'src/app/actions/setting.actions';
|
||||
import { NodeOwner } from 'src/app/models/node-owner.model';
|
||||
import { RingSetting } from 'src/app/models/ring-setting.model';
|
||||
@ -208,7 +209,9 @@ export class SettingsComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
updateShowLogo(event: any) {}
|
||||
updateShowLogo(event: any) {
|
||||
this.store.dispatch(setShowLogo(event));
|
||||
}
|
||||
|
||||
processRingname() {}
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
<h3>Ring order</h3>
|
||||
<ul class="list-group" dragula="PARTICIPANTS" id="participants" [(dragulaModel)]="nodeOwners">
|
||||
<h3>Ring order </h3>
|
||||
<ul class="list-group" dragula="PARTICIPANTS" id="participants" [(dragulaModel)]="nodeOwners" (dragulaModelChange)="onModelChange($event)">
|
||||
<li class="list-group-item d-flex justify-content-between" *ngFor="let s of nodeOwners">
|
||||
<ng-template [ngIf]="(settings$ | async)?.viewMode === 'tg'" [ngIfElse]="elseBlock">
|
||||
{{ getCbUsername(s) }}
|
||||
@ -9,4 +9,7 @@
|
||||
</ng-template>
|
||||
<small>{{ s.nodename }}</small>
|
||||
</li>
|
||||
</ul>
|
||||
</ul>
|
||||
<span *ngIf="isDirty">
|
||||
<span class="badge badge-warning"><i class="bi bi-exclamation-triangle"></i> Unsaved changes, click "Persist channel order" to save.</span>
|
||||
</span>
|
@ -2,11 +2,14 @@ import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { DragulaService } from 'ng2-dragula';
|
||||
import { Observable, Subscription } from 'rxjs';
|
||||
import { loadNodeOwners } from 'src/app/actions/node-owner.actions';
|
||||
import { NodeOwner } from 'src/app/models/node-owner.model';
|
||||
import { RingSetting } from 'src/app/models/ring-setting.model';
|
||||
import { SettingState } from 'src/app/reducers/setting.reducer';
|
||||
import { selectNodeOwners } from 'src/app/selectors/node-owner.selectors';
|
||||
import { selectSettings } from 'src/app/selectors/setting.selectors';
|
||||
import { NotificationService } from 'src/app/services/notification.service';
|
||||
import { RingDataService } from 'src/app/services/ring-data.service';
|
||||
import * as fromRoot from '../../reducers';
|
||||
|
||||
@Component({
|
||||
@ -19,12 +22,15 @@ export class EditRingOrderComponent implements OnDestroy {
|
||||
nodeOwners: NodeOwner[] = [];
|
||||
ringSettings$!: Observable<RingSetting[]>;
|
||||
settings$: Observable<SettingState>;
|
||||
isDirty = false;
|
||||
|
||||
subs = new Subscription();
|
||||
|
||||
constructor(
|
||||
private store: Store<fromRoot.State>,
|
||||
private dragulaService: DragulaService
|
||||
private dragulaService: DragulaService,
|
||||
private notification: NotificationService,
|
||||
private ringData: RingDataService
|
||||
) {
|
||||
this.nodeOwners$ = this.store.select(selectNodeOwners);
|
||||
this.settings$ = this.store.select(selectSettings)
|
||||
@ -32,6 +38,20 @@ export class EditRingOrderComponent implements OnDestroy {
|
||||
this.nodeOwners$.subscribe((data) => {
|
||||
this.nodeOwners = data;
|
||||
})
|
||||
|
||||
dragulaService.createGroup("PARTICIPANTS", {
|
||||
removeOnSpill: true
|
||||
});
|
||||
|
||||
const sub = this.ringData.currentAction.subscribe(action => {
|
||||
if (action == 'persistOrder') {
|
||||
this.persistOrder();
|
||||
this.isDirty = false;
|
||||
this.ringData.doAction('');
|
||||
}
|
||||
})
|
||||
|
||||
this.subs.add(sub);
|
||||
}
|
||||
|
||||
getCbUsername(nodeOwner: NodeOwner) {
|
||||
@ -41,6 +61,20 @@ export class EditRingOrderComponent implements OnDestroy {
|
||||
return `${nodeOwner.first_name} (@${nodeOwner.username})`;
|
||||
}
|
||||
|
||||
persistOrder() {
|
||||
try {
|
||||
this.store.dispatch(loadNodeOwners({ nodeOwners: this.nodeOwners }))
|
||||
this.notification.show('Node order persisted', { classname: 'bg-success' });
|
||||
} catch (e) {
|
||||
this.notification.show('Error persisting order', { classname: 'bg-danger' });
|
||||
}
|
||||
}
|
||||
|
||||
onModelChange($event: NodeOwner[]) {
|
||||
this.nodeOwners = $event;
|
||||
this.isDirty = true;
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.subs.unsubscribe();
|
||||
this.dragulaService.destroy('PARTICIPANTS');
|
||||
|
@ -6,28 +6,33 @@ import { RingDataService } from 'src/app/services/ring-data.service';
|
||||
@Component({
|
||||
selector: 'app-file-exporter',
|
||||
templateUrl: './file-exporter.component.html',
|
||||
styleUrls: ['./file-exporter.component.scss']
|
||||
styleUrls: ['./file-exporter.component.scss'],
|
||||
})
|
||||
export class FileExporterComponent {
|
||||
|
||||
constructor(
|
||||
private file: FileService,
|
||||
private ringData: RingDataService
|
||||
) { }
|
||||
constructor(private file: FileService, private ringData: RingDataService) {}
|
||||
|
||||
persistOrder() {
|
||||
console.log('Method not implemented');
|
||||
}
|
||||
this.ringData.doAction('persistOrder');
|
||||
}
|
||||
|
||||
async downloadChannelsTxt() {
|
||||
this.file.generateAndDownload(ExportFile.RingToolsChannelsTxt, await this.ringData.getRing());
|
||||
this.file.generateAndDownload(
|
||||
ExportFile.RingToolsChannelsTxt,
|
||||
await this.ringData.getRing()
|
||||
);
|
||||
}
|
||||
|
||||
async downloadPubKeysTxt() {
|
||||
this.file.generateAndDownload(ExportFile.RingToolsPubKeysTxt, await this.ringData.getRing());
|
||||
this.file.generateAndDownload(
|
||||
ExportFile.RingToolsPubKeysTxt,
|
||||
await this.ringData.getRing()
|
||||
);
|
||||
}
|
||||
|
||||
async downloadIgniterPubkeys() {
|
||||
this.file.generateAndDownload(ExportFile.IgniterPubkeys, await this.ringData.getRing());
|
||||
this.file.generateAndDownload(
|
||||
ExportFile.IgniterPubkeys,
|
||||
await this.ringData.getRing()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,19 +1,39 @@
|
||||
import { Component, ElementRef, Input, OnInit } from '@angular/core';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
ElementRef,
|
||||
Input,
|
||||
OnChanges,
|
||||
OnInit,
|
||||
SimpleChanges,
|
||||
} from '@angular/core';
|
||||
import * as d3 from 'd3';
|
||||
import { IRing } from 'src/app/models/ring.model';
|
||||
|
||||
@Component({
|
||||
selector: 'app-participant-ring',
|
||||
templateUrl: './participant-ring.component.html',
|
||||
styleUrls: ['./participant-ring.component.scss']
|
||||
styleUrls: ['./participant-ring.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class ParticipantRingComponent {
|
||||
@Input() data: any[] = [];
|
||||
export class ParticipantRingComponent implements OnChanges {
|
||||
_ring: IRing = [];
|
||||
|
||||
@Input()
|
||||
set ring(value: IRing) {
|
||||
this._ring = value;
|
||||
}
|
||||
|
||||
get ring(): IRing {
|
||||
return this._ring;
|
||||
}
|
||||
|
||||
@Input() ringName: string = '';
|
||||
@Input() showLogo: boolean = false;
|
||||
@Input() withFire: boolean = false;
|
||||
@Input() withArrow: boolean = true;
|
||||
@Input() ringLabels: string[] = [];
|
||||
|
||||
|
||||
hostElement: any; // Native element hosting the SVG container
|
||||
svg: any; // Top level SVG element
|
||||
g: any; // SVG Group element
|
||||
@ -25,7 +45,7 @@ export class ParticipantRingComponent {
|
||||
// Donut chart slice elements
|
||||
labels!: any; // SVG data label elements
|
||||
// SVG data label elements
|
||||
totalLabel!: { text: (arg0: number) => void; }; // SVG label for total
|
||||
totalLabel!: { text: (arg0: number) => void }; // SVG label for total
|
||||
// SVG label for total
|
||||
rawData!: any[]; // Raw chart values array
|
||||
// Raw chart values array
|
||||
@ -36,107 +56,205 @@ export class ParticipantRingComponent {
|
||||
pieData: any; // Arc segment parameters for current data set
|
||||
pieDataPrevious: any; // Arc segment parameters for previous data set - used for transitions
|
||||
colors = d3.scaleOrdinal(d3.schemeCategory10);
|
||||
pie = d3.pie()
|
||||
pie = d3
|
||||
.pie()
|
||||
// .startAngle(-90 * Math.PI / 180)
|
||||
// .endAngle(-90 * Math.PI / 180 + 2 * Math.PI)
|
||||
.value((d: any) => d.value)
|
||||
.padAngle(.01)
|
||||
.padAngle(0.01)
|
||||
.sort(null);
|
||||
arc: any;
|
||||
|
||||
|
||||
constructor(private elRef: ElementRef) {
|
||||
constructor(private elRef: ElementRef) {
|
||||
this.hostElement = this.elRef.nativeElement;
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges) {
|
||||
if (changes['ring'] || changes['withFire'] || changes['withArrow']) {
|
||||
let ring: IRing = changes['ring'].currentValue;
|
||||
|
||||
this.updateChart(ring);
|
||||
let ringLabels = ring.map((p) => p[0].username_or_name);
|
||||
this.updateLabels(ringLabels);
|
||||
|
||||
this.updateChart(ring);
|
||||
}
|
||||
|
||||
if (changes['ringLabels']) {
|
||||
this.updateLabels(changes['ringLabels'].currentValue);
|
||||
}
|
||||
}
|
||||
|
||||
private setColorScale() {
|
||||
this.colorScale = d3
|
||||
.scaleLinear()
|
||||
.domain([1, 3.5, 6])
|
||||
// @ts-ignore
|
||||
.range(['#2c7bb6', '#ffffbf', '#d7191c'])
|
||||
// @ts-ignore
|
||||
.interpolate(d3.interpolateHcl);
|
||||
}
|
||||
|
||||
public updateChart(data: any[]) {
|
||||
if (!this.svg) {
|
||||
this.createChart(data);
|
||||
return;
|
||||
}
|
||||
|
||||
this.processPieData(data, false);
|
||||
this.updateState();
|
||||
}
|
||||
|
||||
private createChart(data: any[]) {
|
||||
this.processPieData(data);
|
||||
|
||||
this.removeExistingChartFromParent();
|
||||
this.setChartDimensions();
|
||||
this.addFire();
|
||||
if (this.withFire) {
|
||||
this.addFire();
|
||||
}
|
||||
|
||||
this.setColorScale();
|
||||
this.addGraphicsElement();
|
||||
this.setupArcGenerator();
|
||||
this.addSlicesToTheDonut();
|
||||
this.addLabelsToTheDonut();
|
||||
this.defineMarkers();
|
||||
this.addCenterLogo();
|
||||
this.addCircleArrow();
|
||||
if (this.showLogo) {
|
||||
this.addCenterLogo();
|
||||
|
||||
if (this.withArrow) {
|
||||
this.addCircleArrow();
|
||||
}
|
||||
} else {
|
||||
// this.addCenterLabel();
|
||||
}
|
||||
}
|
||||
|
||||
private processPieData(data: any, initial = true) {
|
||||
let size = 100 / data.length;
|
||||
|
||||
let newData = data.map((val: any) => {
|
||||
val.value = size;
|
||||
return val;
|
||||
});
|
||||
|
||||
this.rawData = data;
|
||||
|
||||
// @ts-ignore
|
||||
this.pieData = this.pie(newData);
|
||||
if (initial) {
|
||||
this.pieDataPrevious = this.pieData;
|
||||
}
|
||||
}
|
||||
|
||||
public updateState() {
|
||||
this.g
|
||||
.selectAll('path')
|
||||
.data(this.pie(this.pieData))
|
||||
.style('opacity', (d: any) => {
|
||||
if (!d.data.data[2]) return 0.25;
|
||||
return 1;
|
||||
})
|
||||
.style('stroke-width', (d: any) => {
|
||||
if (!d.data.data[2]) return 1;
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
private setChartDimensions() {
|
||||
let viewBoxHeight = 430;
|
||||
let viewBoxWidth = 430;
|
||||
this.svg = d3.select(this.hostElement).append('svg').lower()
|
||||
this.svg = d3
|
||||
.select(this.hostElement)
|
||||
.append('svg')
|
||||
.lower()
|
||||
.attr('width', '100%')
|
||||
.attr('height', '100%')
|
||||
.attr('viewBox', '0 0 ' + viewBoxWidth + ' ' + viewBoxHeight);
|
||||
}
|
||||
|
||||
private addGraphicsElement() {
|
||||
this.g = this.svg.append("g")
|
||||
.attr("transform", "translate(215,215)");
|
||||
this.g = this.svg.append('g').attr('transform', 'translate(215,215)');
|
||||
}
|
||||
|
||||
private addFire() {
|
||||
this.svg.append('image').attr('href', '/assets/fire.webp')
|
||||
.attr('height', 500)
|
||||
.attr('width', 500)
|
||||
.attr('x', -40)
|
||||
.attr('y', -40);
|
||||
this.svg
|
||||
.append('image')
|
||||
.attr('href', '/assets/fire.webp')
|
||||
.attr('height', 500)
|
||||
.attr('width', 500)
|
||||
.attr('x', -40)
|
||||
.attr('y', -40);
|
||||
}
|
||||
|
||||
private addCenterLogo() {
|
||||
let w = 350;
|
||||
let h = 350;
|
||||
|
||||
this.g.append("image")
|
||||
.attr("xlink:href", "/assets/roflogo.png")
|
||||
.attr("width", w).attr("height", h)
|
||||
.attr("x", -w/2).attr("y", -h/2)
|
||||
this.g
|
||||
.append('image')
|
||||
.attr('xlink:href', '/assets/roflogo.png')
|
||||
.attr('width', w)
|
||||
.attr('height', h)
|
||||
.attr('x', -w / 2)
|
||||
.attr('y', -h / 2);
|
||||
}
|
||||
|
||||
private defineMarkers() {
|
||||
let markerBoxWidth = 20
|
||||
let markerBoxHeight = 20
|
||||
let markerBoxWidth = 20;
|
||||
let markerBoxHeight = 20;
|
||||
const refX = markerBoxWidth / 2;
|
||||
const refY = markerBoxHeight / 2;
|
||||
const arrowPoints = [[0, 0], [0, 20], [20, 10]];
|
||||
const arrowPoints = [
|
||||
[0, 0],
|
||||
[0, 20],
|
||||
[20, 10],
|
||||
];
|
||||
|
||||
let defs = this.g.append("svg:defs");
|
||||
let defs = this.g.append('svg:defs');
|
||||
|
||||
defs.append("svg:marker")
|
||||
.attr("id", "marker_arrow")
|
||||
.attr("refX", refX)
|
||||
.attr("refY", refY)
|
||||
.attr("markerWidth", markerBoxWidth)
|
||||
.attr("markerHeight", markerBoxHeight)
|
||||
.attr("markerUnits", "strokeWidth")
|
||||
.attr("orient", "auto-start-reverse")
|
||||
defs
|
||||
.append('svg:marker')
|
||||
.attr('id', 'marker_arrow')
|
||||
.attr('refX', refX)
|
||||
.attr('refY', refY)
|
||||
.attr('markerWidth', markerBoxWidth)
|
||||
.attr('markerHeight', markerBoxHeight)
|
||||
.attr('markerUnits', 'strokeWidth')
|
||||
.attr('orient', 'auto-start-reverse')
|
||||
.attr('viewBox', [0, 0, markerBoxWidth, markerBoxHeight])
|
||||
.append("path")
|
||||
.append('path')
|
||||
/* @ts-ignore */
|
||||
.attr("d", d3.line()(arrowPoints))
|
||||
.style("fill", "#fff")
|
||||
.append("svg:marker")
|
||||
.attr("id", "chevron")
|
||||
.attr('d', d3.line()(arrowPoints))
|
||||
.style('fill', '#fff')
|
||||
.append('svg:marker')
|
||||
.attr('id', 'chevron')
|
||||
.attr('viewBox', [0, 0, markerBoxWidth, markerBoxHeight])
|
||||
.attr("refX", refX)
|
||||
.attr("refY", refY)
|
||||
.attr("markerUnits", "userSpaceOnUse")
|
||||
.attr("markerWidth", markerBoxWidth)
|
||||
.attr("markerHeight", markerBoxHeight)
|
||||
.attr("orient", "auto")
|
||||
.append("path")
|
||||
.attr('refX', refX)
|
||||
.attr('refY', refY)
|
||||
.attr('markerUnits', 'userSpaceOnUse')
|
||||
.attr('markerWidth', markerBoxWidth)
|
||||
.attr('markerHeight', markerBoxHeight)
|
||||
.attr('orient', 'auto')
|
||||
.append('path')
|
||||
/* @ts-ignore */
|
||||
.attr("d", 'M0 0 10 0 20 10 10 20 0 20 10 10Z')
|
||||
.style("fill", "#fff")
|
||||
.attr('d', 'M0 0 10 0 20 10 10 20 0 20 10 10Z')
|
||||
.style('fill', '#fff');
|
||||
|
||||
defs.append("marker")
|
||||
.attr("id", "arrowhead")
|
||||
.attr("viewBox", "0 -5 10 10")
|
||||
.attr("refX", 5)
|
||||
.attr("refY", -2)
|
||||
.attr("markerUnits", "strokeWidth")
|
||||
.attr("markerWidth", 36)
|
||||
.attr("markerHeight", 36)
|
||||
.attr("orient", "75deg")
|
||||
.append("path")
|
||||
.attr("d", "M0,-5L10,0L0,5")
|
||||
.style("fill", "#fff")
|
||||
;
|
||||
defs
|
||||
.append('marker')
|
||||
.attr('id', 'arrowhead')
|
||||
.attr('viewBox', '0 -5 10 10')
|
||||
.attr('refX', 5)
|
||||
.attr('refY', -2)
|
||||
.attr('markerUnits', 'strokeWidth')
|
||||
.attr('markerWidth', 36)
|
||||
.attr('markerHeight', 36)
|
||||
.attr('orient', '75deg')
|
||||
.append('path')
|
||||
.attr('d', 'M0,-5L10,0L0,5')
|
||||
.style('fill', '#fff');
|
||||
}
|
||||
|
||||
private addCircleArrow() {
|
||||
@ -145,21 +263,147 @@ export class ParticipantRingComponent {
|
||||
let width = 350;
|
||||
let pi = Math.PI;
|
||||
|
||||
let circleArrow = d3.arc()
|
||||
.innerRadius(width * 0.75 / 2)
|
||||
.outerRadius(width * 0.75 / 2 + 15)
|
||||
let circleArrow = d3
|
||||
.arc()
|
||||
.innerRadius((width * 0.75) / 2)
|
||||
.outerRadius((width * 0.75) / 2 + 15)
|
||||
.startAngle(80 * (pi / 180))
|
||||
.endAngle(-80 * (pi / 180))
|
||||
.endAngle(-80 * (pi / 180));
|
||||
|
||||
this.g
|
||||
.append('path')
|
||||
.attr('d', circleArrow)
|
||||
// .attr('marker-start', (d, i) => {
|
||||
// return 'url(#arrowhead)'
|
||||
// })
|
||||
.attr('marker-end', (d: any, i: any) => {
|
||||
return 'url(#arrowhead)'
|
||||
return 'url(#arrowhead)';
|
||||
})
|
||||
.style("fill", "#fff");
|
||||
.style('fill', '#fff');
|
||||
}
|
||||
|
||||
private setupArcGenerator() {
|
||||
this.innerRadius = 50;
|
||||
this.radius = 80;
|
||||
let width = 430;
|
||||
this.arc = d3
|
||||
.arc()
|
||||
.innerRadius((width * 0.75) / 2)
|
||||
.outerRadius((width * 0.75) / 2 + 30);
|
||||
}
|
||||
|
||||
private addSlicesToTheDonut() {
|
||||
this.slices = this.g
|
||||
.selectAll('allSlices')
|
||||
.data(this.pie(this.pieData))
|
||||
.enter()
|
||||
.append('path')
|
||||
.attr('class', 'donutArcSlices')
|
||||
.attr('d', this.arc)
|
||||
.attr('fill', (datum: any, index: any) => {
|
||||
return this.colorScale(`${index}`);
|
||||
})
|
||||
.style('opacity', (datum: any, index: any) => {
|
||||
if (!datum.data.data[2]) return 0.25;
|
||||
return 1;
|
||||
})
|
||||
.attr('stroke-width', (datum: any) => {
|
||||
return Number(!datum.data.data[2]) * 2;
|
||||
})
|
||||
.attr('stroke', (datum: any) => {
|
||||
return !datum.data.data[2] ? 'yellow' : 'yellow';
|
||||
});
|
||||
}
|
||||
|
||||
private updateLabels(data: any[]) {
|
||||
this.labels.each((_datum: any, index: number, n: Array<any>) => {
|
||||
d3.select(n[index]).text(data[index]);
|
||||
});
|
||||
}
|
||||
|
||||
private addLabelsToTheDonut() {
|
||||
let self = this;
|
||||
this.g
|
||||
.selectAll('.donutArcSlices')
|
||||
|
||||
.each(function (datum: any, index: any) {
|
||||
//A regular expression that captures all in between the start of a string (denoted by ^) and a capital letter L
|
||||
//The letter L denotes the start of a line segment
|
||||
//The "all in between" is denoted by the .+?
|
||||
//where the . is a regular expression for "match any single character except the newline character"
|
||||
//the + means "match the preceding expression 1 or more times" (thus any single character 1 or more times)
|
||||
//the ? has to be added to make sure that it stops at the first L it finds, not the last L
|
||||
//It thus makes sure that the idea of ^.*L matches the fewest possible characters
|
||||
//For more information on regular expressions see: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions
|
||||
let firstArcSection = /(^.+?)L/;
|
||||
|
||||
// @ts-ignore
|
||||
let newArc = firstArcSection.exec(d3.select(this).attr('d'))[1];
|
||||
newArc = newArc.replace(/,/g, ' ');
|
||||
|
||||
//If the end angle lies beyond a quarter of a circle (90 degrees or pi/2)
|
||||
//flip the end and start position
|
||||
let cond = datum.endAngle > 90;
|
||||
if (
|
||||
datum.endAngle > (90 * Math.PI) / 180 &&
|
||||
datum.endAngle < (300 * Math.PI) / 180
|
||||
) {
|
||||
var startLoc = /M(.*?)A/, //Everything between the first capital M and first capital A
|
||||
middleLoc = /A(.*?)0 0 1/, //Everything between the first capital A and 0 0 1
|
||||
endLoc = /0 0 1 (.*?)$/; //Everything between the first 0 0 1 and the end of the string (denoted by $)
|
||||
//Flip the direction of the arc by switching the start en end point (and sweep flag)
|
||||
//of those elements that are below the horizontal line
|
||||
|
||||
// @ts-ignore
|
||||
var newStart = endLoc.exec(newArc)[1];
|
||||
// @ts-ignore
|
||||
var newEnd = startLoc.exec(newArc)[1];
|
||||
// @ts-ignore
|
||||
var middleSec = middleLoc.exec(newArc)[1];
|
||||
|
||||
//Build up the new arc notation, set the sweep-flag to 0
|
||||
newArc = 'M' + newStart + 'A' + middleSec + '0 0 0 ' + newEnd;
|
||||
} //if
|
||||
|
||||
self.g
|
||||
.append('path')
|
||||
.attr('class', 'hiddenDonutArcs')
|
||||
.attr('id', 'donutArc' + index)
|
||||
.attr('d', newArc)
|
||||
.style('fill', 'none');
|
||||
});
|
||||
|
||||
this.labels = this.g
|
||||
.selectAll('.donutText')
|
||||
.data(this.pieData)
|
||||
.enter()
|
||||
.append('text')
|
||||
.attr('class', 'donutText')
|
||||
//Move the labels below the arcs for slices with an end angle > than 90 degrees
|
||||
.attr('dy', function (datum: any, i: number) {
|
||||
// if (d.endAngle < 240 && d.endAngle > 90)
|
||||
// return (d.endAngle > 90 * Math.PI/180 ? 18 : -11);
|
||||
// else
|
||||
let cond =
|
||||
datum.endAngle > (90 * Math.PI) / 180 &&
|
||||
datum.endAngle < (300 * Math.PI) / 180;
|
||||
return cond ? 18 : -11;
|
||||
})
|
||||
//.attr("dy", -13)
|
||||
.append('textPath')
|
||||
.attr('startOffset', '50%')
|
||||
.style('text-anchor', 'middle')
|
||||
.style('fill', '#ffffff')
|
||||
.attr('xlink:href', function (d: any, i: number) {
|
||||
return '#donutArc' + i;
|
||||
})
|
||||
.text((d: any) => {
|
||||
return d.data.name;
|
||||
});
|
||||
}
|
||||
|
||||
private removeExistingChartFromParent() {
|
||||
// !!!!Caution!!!
|
||||
// Make sure not to do;
|
||||
// d3.select('svg').remove();
|
||||
// That will clear all other SVG elements in the DOM
|
||||
d3.select(this.hostElement).select('svg').remove();
|
||||
}
|
||||
}
|
||||
|
@ -10,7 +10,7 @@ import { LnDataService } from './ln-data.service';
|
||||
import { NodeInfo } from '../models/node-info.model';
|
||||
import { NodeOwner } from '../models/node-owner.model';
|
||||
import { IRing } from '../models/ring.model';
|
||||
import { Observable } from 'rxjs';
|
||||
import { BehaviorSubject, Observable } from 'rxjs';
|
||||
import { selectNodeOwners } from '../selectors/node-owner.selectors';
|
||||
|
||||
@Injectable({
|
||||
@ -19,6 +19,9 @@ import { selectNodeOwners } from '../selectors/node-owner.selectors';
|
||||
export class RingDataService {
|
||||
nodeOwners$!: Observable<NodeOwner[]>;
|
||||
nodeOwners: NodeOwner[] = [];
|
||||
|
||||
private actionSource = new BehaviorSubject('default message');
|
||||
currentAction = this.actionSource.asObservable();
|
||||
|
||||
constructor(
|
||||
private store: Store<fromRoot.State>,
|
||||
@ -77,4 +80,7 @@ export class RingDataService {
|
||||
return ring;
|
||||
}
|
||||
|
||||
doAction(action: string) {
|
||||
this.actionSource.next(action);
|
||||
}
|
||||
}
|
||||
|
@ -13,6 +13,7 @@ import { VisNetworkService } from './vis-network.service';
|
||||
* @implements {OnChanges}
|
||||
*/
|
||||
@Directive({
|
||||
// eslint-disable-next-line @angular-eslint/directive-selector
|
||||
selector: '[visNetwork]',
|
||||
})
|
||||
export class VisNetworkDirective implements OnInit, OnDestroy, OnChanges {
|
||||
@ -23,7 +24,7 @@ export class VisNetworkDirective implements OnInit, OnDestroy, OnChanges {
|
||||
* @type {string}
|
||||
* @memberOf VisNetworkDirective
|
||||
*/
|
||||
@Input('visNetwork')
|
||||
@Input()
|
||||
public visNetwork!: string;
|
||||
|
||||
/**
|
||||
|
50
src/global.d.ts
vendored
Normal file
50
src/global.d.ts
vendored
Normal file
@ -0,0 +1,50 @@
|
||||
// Type definitions for saveSvgAsPng v1.0.3
|
||||
// Project: https://github.com/exupero/saveSvgAsPng
|
||||
declare module 'save-svg-as-png' {
|
||||
export type SourceElement = HTMLElement | SVGElement | Element;
|
||||
|
||||
export type BackgroundStyle = string | CanvasGradient | CanvasPattern;
|
||||
|
||||
export interface SelectorRemap {
|
||||
(text: string): string;
|
||||
}
|
||||
|
||||
export interface SaveSVGOptions {
|
||||
scale?: number;
|
||||
responsive?: boolean;
|
||||
width?: number;
|
||||
height?: number;
|
||||
left?: number;
|
||||
top?: number;
|
||||
selectorRemap?: SelectorRemap;
|
||||
backgroundColor?: BackgroundStyle;
|
||||
}
|
||||
|
||||
export interface UriCallback {
|
||||
(uri: string): void;
|
||||
}
|
||||
|
||||
export function svgAsDataUri(
|
||||
el: SourceElement,
|
||||
options: SaveSVGOptions,
|
||||
cb: UriCallback
|
||||
): void;
|
||||
|
||||
export function svgAsPngUri(
|
||||
el: SourceElement,
|
||||
options: SaveSVGOptions,
|
||||
cb: UriCallback
|
||||
): void;
|
||||
|
||||
export function saveSvg(
|
||||
el: SourceElement,
|
||||
fileName: string,
|
||||
options?: SaveSVGOptions
|
||||
): void;
|
||||
|
||||
export function saveSvgAsPng(
|
||||
el: SourceElement,
|
||||
fileName: string,
|
||||
options?: SaveSVGOptions
|
||||
): void;
|
||||
}
|
@ -6710,6 +6710,11 @@ sass@1.44.0:
|
||||
chokidar ">=3.0.0 <4.0.0"
|
||||
immutable "^4.0.0"
|
||||
|
||||
save-svg-as-png@^1.4.17:
|
||||
version "1.4.17"
|
||||
resolved "https://registry.yarnpkg.com/save-svg-as-png/-/save-svg-as-png-1.4.17.tgz#294442002772a24f1db1bf8a2aaf7df4ab0cdc55"
|
||||
integrity sha512-7QDaqJsVhdFPwviCxkgHiGm9omeaMBe1VKbHySWU6oFB2LtnGCcYS13eVoslUgq6VZC6Tjq/HddBd1K6p2PGpA==
|
||||
|
||||
sax@^1.2.4, sax@~1.2.4:
|
||||
version "1.2.4"
|
||||
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
|
||||
|
Loading…
Reference in New Issue
Block a user