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

直感的なグラフのためのエンジン、RxZuの紹介

最終更新日 May 5, 2021

所要時間:2 分

最初はすべてが直線的だった。

私たちは、ユーザーが完全にグラフに基づいて会話をデザインできるインターフェイスを手にしていました。これはVonage AI Studioの一部で、誰でも独自のインテリジェントなバーチャル・アシスタントを作ることができる。キッカーは?それは完全にフォームに基づいていたのです。

しかし、AIは未来であり、クライアントが使いにくいと感じたフォームは、間違いなくそうではなかった。 ない.

グラフ・ライブラリーの探求

私たちは、ただでさえ複雑な会話デザインの世界を単純化するための視覚的アプローチが必要だと気づいた。賢く、おしゃれで、直感的なものが。そのためには、いくつかの要件を満たすグラフ・ライブラリが必要でした:

  1. Angularのサポート

  2. 軽量

  3. 拡張性とカスタマイズ性

  4. 幅広いサポートとコミュニティ

何を知っている?検索結果はゼロだった。私たちが見つけたライブラリは非常に重く、LodashやBackboneのような時代遅れの依存関係を含んでいた。私たちが調べた選択肢はオープンソースではなく、コミュニティもなかった。私たちが見つけた実装は時代遅れで、Typingsが欠けており、Angularの環境には合わず、最も単純なユースケースのために果てしない複雑さをもたらしました。

RxZuに入る

そこで、Reactive Extensions (RxJS)と Zu "は日本語でイラストレーションを意味する。

RxZuは、RxJSの上に構築されたダイアグラム・エンジン・システムで、パフォーマンス、最適化、カスタマイズ性において、グラフィック・ビジュアライゼーションを次のレベルに引き上げます。

RxZuは、モデル間の同期処理を担当するコアエンジンと、コアエンジンを利用したフレームワークをベースにしたレンダリングエンジンという複数の部分から構成されている。

プロジェクトにおける主要なガイドラインのいくつかは最小限のものだ。それは、クリーンなコードと、エンジン・エンティティのカスタマイズと拡張性である。これらのエンティティは

  • ノード:グラフの主要な構成要素。

  • ポート:リンクの起点

  • リンク:2つのポートを結ぶ線で、接続性と連続性を表す

  • ラベル:エンティティの名前または説明

  • カスタム:例えば付箋のようなカスタムエンティティを作成する機能。

Alt Text

コードを見てみよう

** 現在のところ、RxZuはレンダリングエンジンとしてAngularのみを実装しています。

ノードを追加するためのドラッグ・アンド・ドロップ・インターフェースを備えたグラフを表示する新しいAngularアプリケーションを作成することから始めよう:

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

rxzu/angularをインストールする:

npm i @rxzu/angular

アプリケーションを実行する:

ng s

の本番モードを有効にしよう。 RxZumain.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));

追加 app.module.tsRxZuモジュールをデフォルトのコンポーネントと一緒に追加します:

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モジュール withComponentsメソッドは、コンポーネントとその型の配列を受け入れます。こうすることで、後ほど作成するダイアグラム・モデルに追加したときに、ライブラリが異なるコンポーネントを解決してペイントすることができます。

それでは、クールなグリッドを背景、ドラッグ可能なノード、アクション・バーのコンテナとして作成しましょう:

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

次に、アクション・バーとダイアグラムそのものを含むhtmlテンプレート:

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>

そしてパズルの最後のピースとして、いくつかのノードとポートを作り、それらをリンクさせる。それからすべてをレンダリングする。

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

最後に

いくつか注意すべきことがある:

が最も重要な部分だ。 diagramModelは最も重要な部分です。ダイアグラム・モデル全体を保持し、ダイアグラムに要素を追加したり削除したりすることができます。

this.diagramModel.addNode(node);

ポート(子ノード)のように、他のエンティティの子であるものもある。これらのエンティティは、親に直接アタッチすることで追加できます。

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

次のチュートリアルでは、受け取った余分な情報を利用するカスタマイズされたノードを作成する方法を学びます。

それまでは、私たちのストーリーブックをご覧ください。 ストーリーブックで、ソースコードは GitHubリポジトリ.

ここからどこへ行くのか?

我々のロードマップの中で最も重要なタスクは、コアのパフォーマンスを向上させることだ。そしてまた

  • React、Vueなどのサポートを追加...

  • 障害物を認識したスマートなリンク

  • 巨大なダイアグラム(数千のエンティティ)をサポートするために、ビューポート内の要素のみをレンダリングする。

シェア:

https://a.storyblok.com/f/270183/400x400/d05ab05814/daniel-netzer.png
Daniel Netzerヴォネージの卒業生

ダニエルはVonageのAIチームのテクニカル・チーム・リーダーです。