https://a.storyblok.com/f/270183/43574/c65e513904/graphs-engine-3.png

Apresentamos o RxZu, um mecanismo para gráficos intuitivos

Publicado em May 5, 2021

Tempo de leitura: 5 minutos

No início, tudo era linear.

Tínhamos em mãos uma interface que permitia aos usuários criar conversas, inteiramente baseadas em gráficos. Isso fazia parte do Vonage AI Studio, onde qualquer pessoa pode criar seu próprio assistente virtual inteligente. O mais incrível? Era inteiramente baseado em formulários.

Mas a IA era o futuro, e os formulários, que nossos clientes consideravam impraticáveis, definitivamente eram não.

A busca por uma biblioteca de grafos

Percebemos que precisávamos de uma abordagem visual para simplificar o já complexo mundo do design de conversação. Algo inteligente, bacana e intuitivo. E, para isso, precisávamos de uma biblioteca de gráficos que atendesse a alguns requisitos:

  1. Suporte angular

  2. Leve

  3. Ampliável e personalizável

  4. Amplo suporte e comunidade

E adivinhem só? Nossa pesquisa não deu em nada. As bibliotecas que encontramos eram extremamente pesadas e incluíam dependências desatualizadas, como Lodash e Backbone. As opções que analisamos não eram de código aberto e não contavam com uma comunidade. As implementações que encontramos estavam desatualizadas, sem Typings, inadequadas para o ambiente Angular e introduziam uma complexidade infinita até mesmo para o caso de uso mais simples.

Conheça o RxZu

Por isso, criamos o RxZu, cujo nome é uma referência ao Reactive Extensions (RxJS) e Zu, a palavra japonesa para ilustração.

O RxZu é um sistema de geração de diagramas, desenvolvido com base no RxJS, que eleva a visualização gráfica a um novo patamar em termos de desempenho, otimização e personalização.

O RxZu é composto por várias partes: o mecanismo central, responsável por gerenciar a sincronização entre os modelos, e o mecanismo de renderização, que se baseia na estrutura que utiliza o mecanismo central.

Algumas das principais diretrizes do projeto são mínimas. Elas dizem respeito ao código limpo e à capacidade de personalização e extensibilidade das entidades do mecanismo. Essas entidades são compostas por:

  • Nós: o principal elemento constitutivo de qualquer grafo; a representação visual da convergência de dados

  • Portos: os pontos de partida das conexões

  • Links: uma linha entre duas portas, representando conectividade e continuidade

  • Etiquetas: o nome ou a descrição de uma entidade

  • Personalização: a capacidade de criar uma entidade personalizada, por exemplo, uma nota adesiva

Alt Text

Vamos dar uma olhada no código

** Observe que, atualmente, o RxZu implementa apenas o Angular como mecanismo de renderização, o que significa que todos os exemplos de código são para o Angular**

Vamos começar criando um novo aplicativo Angular que exibirá um gráfico com uma interface do tipo arrastar e soltar para adicionar mais nós:

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

Instale o @rxzu/angular:

npm i @rxzu/angular

Execute o aplicativo:

ng s

Vamos ativar o modo de produção para RxZu em 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));

Adicionar ao app.module.ts módulo RxZu junto com todos os componentes padrão:

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 aceita uma matriz de componentes e seus tipos. Dessa forma, a biblioteca pode resolver e renderizar os diferentes componentes quando eles são adicionados ao modelo do diagrama, que criaremos a seguir.

Agora vamos criar uma grade bacana como nosso plano de fundo, os nós arrastáveis e nosso contêiner da barra de ações:

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

A seguir, nosso modelo HTML com a barra de ações e o próprio diagrama:

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>

E, para completar o quebra-cabeça, crie alguns nós e portas e conecte-os. Em seguida, faça a renderização de tudo.

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 fim

Algumas observações:

A diagramModel é a parte mais importante. Ela contém todo o modelo do diagrama e nos permite adicionar ou remover elementos do diagrama.

this.diagramModel.addNode(node);

Algumas entidades são filhas de outras, como as portas (que são nós filhos). Elas podem ser adicionadas conectando-as diretamente à sua entidade pai.

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

No próximo tutorial, você aprenderá a criar nós personalizados que utilizam qualquer informação adicional que recebam.

Até lá, você pode encontrar muitos outros exemplos em nosso Storybooke o código-fonte em nosso repositório do GitHub.

E agora, o que vamos fazer?

A tarefa mais importante em nosso plano de ação é melhorar o desempenho do núcleo. E também:

  • Adicionando suporte para React, Vue e muito mais...

  • Ligações mais inteligentes com detecção de obstáculos

  • Renderização apenas dos elementos na janela de visualização para dar suporte a diagramas gigantescos (milhares de entidades)

Compartilhar:

https://a.storyblok.com/f/270183/400x400/d05ab05814/daniel-netzer.png
Daniel NetzerEx-funcionários da Vonage

Daniel é líder da equipe técnica na Vonage, na equipe de IA