Fully implemented

This commit is contained in:
Djuri Baars 2022-01-17 17:26:17 +01:00
parent dd6cad0e75
commit c31eef4eee
19 changed files with 563 additions and 112 deletions

View File

@ -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",

View File

@ -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({

View File

@ -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,

View File

@ -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">

View File

@ -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];
}
}

View 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>

View 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();
});
});

View 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;
});
}
}

View File

@ -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>

View File

@ -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() {}

View File

@ -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>

View File

@ -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');

View File

@ -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()
);
}
}

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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
View 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;
}

View File

@ -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"