
Partager:
Daniel est chef d'équipe technique chez Vonage au sein de l'équipe AI.
Présentation de RxZu, un moteur pour des graphiques intuitifs
Temps de lecture : 5 minutes
Au début, tout était linéaire.
Nous avions entre les mains une interface qui permettait aux utilisateurs de concevoir des conversations, entièrement basées sur des graphiques. Cela faisait partie du Vonage AI Studio, où chacun peut créer son propre assistant virtuel intelligent. Le clou du spectacle ? Il était entièrement basé sur des formulaires.
Mais l'IA était l'avenir, et les formulaires que nos clients trouvaient inutilisables ne l'étaient certainement pas. pas.
La recherche d'une bibliothèque de graphes
Nous avons réalisé que nous avions besoin d'une approche visuelle pour simplifier le monde déjà complexe de la conception de conversations. Quelque chose d'intelligent, d'élégant et d'intuitif. Et pour cela, nous avions besoin d'une bibliothèque de graphiques qui répondrait à certaines exigences :
Prise en charge d'Angular
Léger
Extensible et personnalisable
Soutien et communauté étendus
Et que savez-vous ? Notre recherche n'a donné aucun résultat. Les bibliothèques que nous avons trouvées étaient extrêmement lourdes et comprenaient des dépendances obsolètes telles que Lodash et Backbone. Les options que nous avons examinées n'étaient pas libres de droits et n'avaient pas de communauté. Les implémentations que nous avons trouvées étaient dépassées, manquaient de typage, n'étaient pas adaptées à l'environnement Angular, et introduisaient une complexité infinie pour le cas d'utilisation le plus simple.
Entrer dans RxZu
C'est ainsi que nous avons créé RxZu, du nom de Reactive Extensions (RxJS) et Zule mot japonais pour illustration.
RxZu est un système de moteur de diagrammes, construit au-dessus de RxJS, qui porte la visualisation graphique à un niveau supérieur en termes de performance, d'optimisation et de personnalisation.
RxZu est composé de plusieurs parties : le moteur central, qui est chargé de gérer la synchronisation entre les modèles, et le moteur de rendu qui est basé sur le cadre utilisant le moteur central.
Certaines des principales lignes directrices du projet sont minimales. Elles concernent la propreté du code et la possibilité de personnaliser et d'étendre les entités du moteur. Ces entités sont constituées de :
Nœuds : le principal élément constitutif de tout graphique ; la représentation visuelle de la convergence des données.
Les ports : les points de départ des liens
Liens : une ligne entre deux ports, représentant la connectivité et la continuité.
Étiquettes : nom ou description d'une entité
Personnalisé : possibilité de créer une entité personnalisée, par exemple une note autocollante.

Voyons le code
** Notez que RxZu n'implémente actuellement qu'Angular comme moteur de rendu, ce qui signifie que tous les exemples de code sont pour Angular**.
Nous commencerons par créer une nouvelle application Angular qui affichera un graphique avec une interface de glisser-déposer pour ajouter d'autres nœuds :
bash
ng new rxzu-angular
# wait for angular installation to finish
cd rxzu-angularInstaller @rxzu/angular :
Exécuter l'application :
Activons le mode production pour RxZu en 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));
Ajouter au app.module.ts Module RxZu avec tous les composants par défaut :
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 {}Module RxZu withComponents accepte un tableau de composants et leur type. De cette façon, la bibliothèque peut résoudre et peindre les différents composants lorsqu'ils sont ajoutés au modèle de diagramme, que nous allons créer ci-dessous.
Créons maintenant une grille sympa comme arrière-plan, les nœuds glissants et le conteneur de la barre d'action :
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;
}
}Ensuite, notre modèle html avec la barre d'actions et le diagramme lui-même :
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>
Et pour la dernière pièce du puzzle, créez quelques nœuds et ports, et reliez-les. Rendez ensuite le tout.
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);
}
}
}
}
}
Enfin
Quelques points à noter :
Le diagramModel est la partie la plus importante. Elle contient l'ensemble du modèle de diagramme et permet d'ajouter ou de supprimer des éléments du diagramme.
this.diagramModel.addNode(node);Certaines entités sont les enfants d'autres entités, comme les ports (qui sont des nœuds enfants). Elles peuvent être ajoutées en les attachant directement à leur parent.
const port = new PortModel();
node.addPort(port);Dans le prochain tutoriel, vous apprendrez à créer des nœuds personnalisés qui utilisent toutes les informations supplémentaires qu'ils reçoivent.
En attendant, vous trouverez de nombreux autres exemples dans notre livre d'histoireset le code source dans notre GitHub.
Quelle est la suite des événements ?
La tâche la plus importante de notre feuille de route consiste à améliorer les performances du noyau. Et aussi :
Ajout de la prise en charge de React, Vue, et plus encore...
Des liens plus intelligents avec la connaissance des obstacles
Rendre seulement les éléments dans le port de vue pour supporter des diagrammes gigantesques (des milliers d'entités)