https://d226lax1qjow5r.cloudfront.net/blog/blogposts/introducing-rxzu-an-engine-for-intuitive-graphs/graphs-engine-3-.png

Presentamos RxZu, un motor para gráficos intuitivos

Publicado el May 5, 2021

Tiempo de lectura: 5 minutos

Al principio, todo era lineal.

Tuvimos en nuestras manos una interfaz que permitía a los usuarios diseñar conversaciones, totalmente basadas en gráficos. Esto formaba parte del AI Studio de Vonage, donde cualquiera puede crear su propio asistente virtual inteligente. ¿Lo mejor? Se basaba por completo en formularios.

Pero la IA era el futuro, y los formularios, que nuestros clientes consideraban inutilizables, no lo eran en absoluto. no.

En busca de una biblioteca de gráficos

Nos dimos cuenta de que necesitábamos un enfoque visual para simplificar el ya de por sí complejo mundo del diseño de conversaciones. Algo inteligente, elegante e intuitivo. Y para ello necesitábamos una biblioteca de gráficos que cumpliera algunos requisitos:

  1. Compatibilidad con Angular

  2. Ligero

  3. Ampliable y personalizable

  4. Amplio apoyo y comunidad

¿Y tú qué sabes? Nuestra búsqueda arrojó cero resultados. Las librerías que encontramos eran extremadamente pesadas, e incluían dependencias obsoletas como Lodash y Backbone. Las opciones que buscamos no eran de código abierto y carecían de comunidad. Las implementaciones que encontramos estaban desactualizadas, carecían de Typings, no se adaptaban al entorno Angular e introducían una complejidad infinita para el caso de uso más simple.

Entrar en RxZu

Así que creamos RxZu, nombre de las extensiones reactivas (RxJS) y Zula palabra japonesa que significa ilustración.

RxZu es un sistema de motor de diagramas, construido sobre RxJS, que lleva la visualización gráfica al siguiente nivel en términos de rendimiento, optimización y personalización.

RxZu se compone de múltiples partes: el motor central, que se encarga de gestionar la sincronización entre modelos, y el motor de renderizado que se basa en el marco que utiliza el motor central.

Algunas de las directrices principales del proyecto son mínimas. Se trata de código limpio y la capacidad de personalización y extensibilidad de las entidades del motor. Estas entidades se componen de:

  • Nodos: el bloque principal de cualquier gráfico; la representación visual de la convergencia de datos.

  • Puertos: los puntos de partida de los enlaces

  • Enlaces: una línea entre dos puertos, que representa la conectividad y la continuidad.

  • Etiquetas: el nombre o la descripción de una entidad

  • Personalizada: posibilidad de crear una entidad personalizada, por ejemplo, una nota adhesiva.

Alt Text

Veamos el código

** Tenga en cuenta que RxZu actualmente sólo implementa Angular como motor de renderizado, lo que significa que todos los ejemplos de código son para Angular**.

Comenzaremos creando una nueva aplicación Angular que mostrará un gráfico con una interfaz de arrastrar y soltar para añadir más nodos:

bash
ng new rxzu-angular
# wait for angular installation to finish
cd rxzu-angular

Instalar @rxzu/angular:

npm i @rxzu/angular

Ejecute la aplicación:

ng s

Activemos el modo de producción para 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));

Añadir a app.module.ts Módulo RxZu junto con todos los componentes por defecto:

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 {}

Módulo RxZu withComponents acepta un array de componentes y su tipo. De esta forma la librería puede resolver y pintar los diferentes componentes cuando se añaden al modelo de diagrama, que crearemos a continuación.

Ahora vamos a crear una bonita cuadrícula como fondo, los nodos arrastrables y el contenedor de nuestra barra de acciones:

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

Luego, nuestra plantilla html con la barra de acciones y el diagrama propiamente dicho:

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>

Y para la última pieza del puzzle, crea algunos nodos y puertos, y enlázalos. Luego renderízalo todo.

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

Por último

Algunas cosas a tener en cuenta:

La página diagramModel es la parte más importante. Contiene todo el modelo del diagrama, y nos da la posibilidad de añadir o eliminar elementos del diagrama.

this.diagramModel.addNode(node);

Algunas entidades son hijas de otras, como los puertos (que son nodos hijos). Pueden añadirse adjuntándolas directamente a su padre.

const port = new PortModel();
node.addPort(port);

En el siguiente tutorial aprenderás a crear nodos personalizados que utilicen cualquier información extra que reciban.

Hasta entonces, puede encontrar muchos más ejemplos en nuestro Libro de cuentosy el código fuente en nuestro repositorio de GitHub.

¿Adónde vamos ahora?

La tarea más importante de nuestra hoja de ruta consiste en mejorar el rendimiento del núcleo. Y también:

  • Añadiendo soporte para React, Vue y más...

  • Enlaces más inteligentes con detección de obstáculos

  • Renderizado sólo de elementos en el puerto de vista para soportar diagramas gigantescos (miles de entidades).

Compartir:

https://a.storyblok.com/f/270183/400x400/d05ab05814/daniel-netzer.png
Daniel NetzerAntiguos alumnos de Vonage

Daniel es jefe de equipo técnico en Vonage en el equipo de IA