
Einführung von RxZu, einer Engine für intuitive Graphen
Lesedauer: 4 Minuten
Am Anfang war alles linear.
Wir hatten eine Schnittstelle in der Hand, die es den Nutzern ermöglichte, Gespräche zu entwerfen, die vollständig auf Diagrammen basierten. Dies war Teil des Vonage AI Studios, in dem jeder seinen eigenen intelligenten virtuellen Assistenten erstellen kann. Der Clou? Sie basierte vollständig auf Formularen.
Aber KI war die Zukunft, und Formulare, die unsere Kunden als unbrauchbar empfanden, waren ganz sicher nicht.
Die Suche nach einer Graphenbibliothek
Uns wurde klar, dass wir einen visuellen Ansatz brauchten, um die ohnehin schon komplexe Welt des Gesprächsdesigns zu vereinfachen. Etwas Cleveres, Schickes, Intuitives. Und dafür brauchten wir eine Grafikbibliothek, die einige Anforderungen erfüllen würde:
Angular-Unterstützung
Leichtgewicht
Erweiterbar und anpassbar
Umfassende Unterstützung und Gemeinschaft
Und was sagt man dazu? Unsere Suche führte zu null Ergebnissen. Die Bibliotheken, die wir gefunden haben, waren extrem schwer und enthielten veraltete Abhängigkeiten wie Lodash und Backbone. Die Optionen, die wir untersuchten, waren nicht quelloffen und es fehlte eine Community. Die Implementierungen, die wir fanden, waren veraltet, enthielten keine Typisierungen, passten nicht in die Angular-Umgebung und brachten endlose Komplexität für den einfachsten Anwendungsfall.
RxZu eingeben
Also haben wir RxZu geschaffen, benannt nach Reactive Extensions (RxJS) und Zu, dem japanischen Wort für Illustration.
RxZu ist ein Diagramm-Engine-System, das auf RxJS aufbaut und die grafische Visualisierung in Bezug auf Leistung, Optimierung und Anpassbarkeit auf die nächste Stufe hebt.
RxZu besteht aus mehreren Teilen: der Core-Engine, die für die Synchronisation zwischen den Modellen zuständig ist, und der Rendering-Engine, die auf dem Framework basiert, das die Core-Engine nutzt.
Einige der führenden Richtlinien im Projekt sind minimal. Es geht um sauberen Code und die Fähigkeit zur Anpassung und Erweiterbarkeit der Motoreinheiten. Diese Entitäten bestehen aus:
Knoten: der Hauptbaustein eines jeden Graphen; die visuelle Darstellung der Datenkonvergenz
Häfen: die Ausgangspunkte für die Verbindungen
Links: eine Linie zwischen zwei Anschlüssen, die für Konnektivität und Kontinuität steht
Labels: der Name oder die Beschreibung einer Entität
Benutzerdefiniert: die Möglichkeit, ein benutzerdefiniertes Objekt zu erstellen, z. B. eine Haftnotiz

Zeigen Sie uns den Code
** Beachten Sie, dass RxZu derzeit nur Angular als Rendering-Engine implementiert, was bedeutet, dass alle Codebeispiele für Angular sind**
Wir beginnen mit der Erstellung einer neuen Angular-Anwendung, die einen Graphen mit einer Drag-and-Drop-Schnittstelle zum Hinzufügen weiterer Knoten anzeigt:
bash
ng new rxzu-angular
# wait for angular installation to finish
cd rxzu-angularInstallieren Sie @rxzu/angular:
Führen Sie die Anwendung aus:
Aktivieren wir den Produktionsmodus für RxZu in main.ts
import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
import { environment } from './environments/environment';
import { enableDiagramProdMode } from '@rxzu/angular';
if (environment.production) {
enableProdMode();
enableDiagramProdMode();
}
platformBrowserDynamic()
.bootstrapModule(AppModule)
.catch((err) => console.error(err));
Hinzufügen app.module.ts RxZu-Modul zusammen mit allen Standardkomponenten:
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import {
ComponentProviderOptions,
DefaultLabelComponent,
DefaultLinkComponent,
DefaultNodeComponent,
DefaultPortComponent,
RxZuModule,
} from '@rxzu/angular';
import { AppComponent } from './app.component';
const DEFAULTS: ComponentProviderOptions[] = [
{
type: 'node',
component: DefaultNodeComponent,
},
{
type: 'port',
component: DefaultPortComponent,
},
{
type: 'link',
component: DefaultLinkComponent,
},
{
type: 'label',
component: DefaultLabelComponent,
},
];
@NgModule({
declarations: [AppComponent],
imports: [BrowserModule, CommonModule, RxZuModule.withComponents(DEFAULTS)],
providers: [],
bootstrap: [AppComponent],
})
export class AppModule {}RxZu-Modul withComponents Methode akzeptiert ein Array von Komponenten und deren Typ. Auf diese Weise kann die Bibliothek die verschiedenen Komponenten auflösen und malen, wenn sie dem Diagrammmodell hinzugefügt werden, das wir weiter unten erstellen werden.
Jetzt erstellen wir ein cooles Raster als Hintergrund, die verschiebbaren Knoten und unseren Aktionsleisten-Container:
app.component.scss
.demo-diagram {
display: flex;
height: 100%;
min-height: 100vh;
background-color: #3c3c3c;
background-image: linear-gradient(
0deg,
transparent 24%,
rgba(255, 255, 255, 0.05) 25%,
rgba(255, 255, 255, 0.05) 26%,
transparent 27%,
transparent 74%,
rgba(255, 255, 255, 0.05) 75%,
rgba(255, 255, 255, 0.05) 76%,
transparent 77%,
transparent
),
linear-gradient(
90deg,
transparent 24%,
rgba(255, 255, 255, 0.05) 25%,
rgba(255, 255, 255, 0.05) 26%,
transparent 27%,
transparent 74%,
rgba(255, 255, 255, 0.05) 75%,
rgba(255, 255, 255, 0.05) 76%,
transparent 77%,
transparent
);
background-size: 50px 50px;
}
.node-drag {
display: block;
cursor: grab;
background-color: white;
border-radius: 30px;
padding: 5px 15px;
}
.action-bar {
position: fixed;
width: 100%;
height: 40px;
z-index: 2000;
background-color: rgba(255, 255, 255, 0.4);
display: flex;
align-items: center;
* {
margin: 0 10px;
}
}Dann unsere HTML-Vorlage mit der Aktionsleiste und dem Diagramm selbst:
app.component.html
<div class="action-bar">
<div
*ngFor="let node of nodesLibrary"
class="node-drag"
draggable="true"
[attr.data-name]="node.name"
(dragstart)="onBlockDrag($event)"
[ngStyle]="{ 'background-color': node.color }"
>
{{ node.name }}
</div>
</div>
<rxzu-diagram
class="demo-diagram"
[model]="diagramModel"
(drop)="onBlockDropped($event)"
(dragover)="$event.preventDefault()"
></rxzu-diagram>
Und für das letzte Puzzleteil erstellen Sie einige Knoten und Ports und verbinden sie miteinander. Dann rendere das Ganze.
app.component.ts
import { AfterViewInit, Component, ViewChild } from '@angular/core';
import {
DiagramModel,
NodeModel,
PortModel,
RxZuDiagramComponent,
} from '@rxzu/angular';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
})
export class AppComponent implements AfterViewInit {
diagramModel: DiagramModel;
nodesLibrary = [
{ color: '#AFF8D8', name: 'default' },
{ color: '#FFB5E8', name: 'default' },
{ color: '#85E3FF', name: 'default' },
];
@ViewChild(RxZuDiagramComponent, { static: true })
diagram?: RxZuDiagramComponent;
constructor() {
this.diagramModel = new DiagramModel();
}
ngAfterViewInit() {
this.diagram?.zoomToFit();
}
createNode(type: string) {
const nodeData = this.nodesLibrary.find((nodeLib) => nodeLib.name === type);
if (nodeData) {
const node = new NodeModel();
const port = new PortModel();
node.addPort(port);
node.setExtras(nodeData);
return node;
}
return null;
}
/**
* On drag start, assign the desired properties to the dataTransfer
*/
onBlockDrag(e: DragEvent) {
const type = (e.target as HTMLElement).getAttribute('data-type');
if (e.dataTransfer && type) {
e.dataTransfer.setData('type', type);
}
}
/**
* on block dropped, create new intent with the empty data of the selected block type
*/
onBlockDropped(e: DragEvent): void | undefined {
if (e.dataTransfer) {
const nodeType = e.dataTransfer.getData('type');
const node = this.createNode(nodeType);
const canvasManager = this.diagram?.diagramEngine.getCanvasManager();
if (canvasManager) {
const droppedPoint = canvasManager.getZoomAwareRelativePoint(e);
const width = node?.getWidth() ?? 1;
const height = node?.getHeight() ?? 1;
const coords = {
x: droppedPoint.x - width / 2,
y: droppedPoint.y - height / 2,
};
if (node) {
node.setCoords(coords);
this.diagramModel.addNode(node);
}
}
}
}
}
Endlich
Einige Dinge sind zu beachten:
Die diagramModel ist der wichtigste Teil. Er enthält das gesamte Diagrammmodell und gibt uns die Möglichkeit, dem Diagramm Elemente hinzuzufügen oder zu entfernen.
this.diagramModel.addNode(node);Einige Entitäten sind Kinder von anderen, wie z. B. Häfen (die Kindknoten sind). Sie können hinzugefügt werden, indem sie direkt an ihre Eltern angehängt werden.
const port = new PortModel();
node.addPort(port);Im nächsten Lernprogramm lernen Sie, wie Sie benutzerdefinierte Knoten erstellen, die alle zusätzlichen Informationen nutzen, die sie erhalten.
Bis dahin finden Sie viele weitere Beispiele in unserem Geschichtenbuchund den Quellcode in unserem GitHub-Repositorium.
Wie geht es jetzt weiter?
Die wichtigste Aufgabe auf unserer Roadmap ist der Aufbau einer besseren Leistung im Kern. Und außerdem:
Hinzufügen von Unterstützung für React, Vue und mehr...
Intelligente Verbindungen mit Hinderniserkennung
Rendering nur von Elementen im Viewport zur Unterstützung riesiger Diagramme (Tausende von Objekten)