35
41

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

インタラクティブに図が書けるJointJsを使ってみた

Posted at

JointJsとは

公式ページ
Github

図を書くことに特化したjsライブラリです。
オープンソース・ソフトウェアですが、Rapid版という有料版が存在し、高度な実装がなされたテンプレートが使えるようになるようです。※ちなみに日本円で25万円です・・・高いw

この類のライブラリだとD3.jsが有名かと思います。
D3との違いは、純粋にjointJsの方が図を書くということに特化されているので、D3よりは簡単に書ける印象です。
ただJquery/Backbone.js/Underscore.jsに依存しているので、その辺がすこし厄介です・・・
現に僕の場合は、Underscore.jsの知識もキャッチアップする必要があったので、面倒でした。

その辺はメリットデメリットあると思うので、D3とどちらを使うかは検討して頂ければと。

APIの紹介

詳しくは公式ページを見て頂ければと思いますが、良く使うものをピックアップしてご紹介します。

joint.dia.Graph

joint.dia.Graphは、ElementやLinkを保持しておくためのJointJsの一番メインのAPIです。

var rect = new joint.shapes.basic.Rect({ 
    position: { x: 100, y: 100 },
    size: { width: 70, height: 30 },
    attrs: { text: { text: 'example' } }
})
var graph = new joint.dia.Graph
graph.addCell(rect)

このようにaddCellすることで、graphの中にrectを保持し、下記で説明するSVG上に描画されます。

joint.dia.Paper

joint.dia.Paperはjoint.dia.Graphを描画するためのviewを提供しているAPIです。


var graph = new joint.dia.Graph
var paper = new joint.dia.Paper({
    el: $('#paper'),
    width: 600,
    height: 400,
    gridSize: 10,
    model: graph
})

このようにelにsvgを描画するdomを、width/heightでサイズを設定します。
gridSizeは小さい方がよりpaper内での細かい動きを実現できます。

joint.shapes.basic.Rect

joint.shapes.basic.Rectはgタグで括られた要素を生成してくれます。

<g class="element basic Rect">
  <g class="rotatable">
    <g class="scalable">
      <rect />
    </g>
    <text />
  </g>
</g>        
var rect = new joint.shapes.basic.Rect({
  position: { x: 100, y: 30 },
  size: { width: 100, height: 30 },
  attrs: { rect: { fill: 'blue' }, text: { text: 'my box', fill: 'white' } }
});

attrsで渡しているオプションによって下記のように様々なElementを作ることが可能です。
スクリーンショット 2015-11-04 1.40.39.png

ちなみにextendを使ってオーバライドすることも可能です。

joint.shapes.basic.Rect = joint.shapes.basic.Generic.extend({
  markup: '<g class="rotatable"><g class="scalable"><rect/></g><text/></g>',
  defaults: joint.util.deepSupplement({
    type: 'basic.Rect',
    attrs: {
      'rect': { 
        fill: 'white', 
        stroke: 'black',
        'follow-scale': true,
        width: 80,
        height: 40
      }
    }
  }, joint.shapes.basic.Generic.prototype.defaults)
});

例えばこのようにmarkupにデフォルトで出力したい構成を作り、attrsでデフォルト値を設定しておけばnewするだけでこの形になります。

joint.dia.Link

joint.dia.Linkは上記のようなエレメントを繋ぎ合わせる線を描画してくれるAPIです。

var link = new joint.dia.Link({
  source: { id: rect.id },
  target: { id: rect2.id }
});

このようにsourceとtargetにelementのidを渡してあげると、それらを繋ぎあわせてくれます。
こちらもオプションの設定次第で多くの種類のlineを実装できます。

例えば右上の何回か折り返しているような線を実装したい場合は、

var link = new joint.dia.Link({
  source: { id: rect.id },
  target: { id: rect2.id },
  vertices: [{ x: 100, y: 120 }, { x: 150, y: 60 }
});

verticesに座標入れてあげると折り返してくれます。

スクリーンショット 2015-11-04 1.55.13.png

Eventに関して

var rect = new joint.shapes.basic.Rect({ 
    position: { x: 100, y: 100 },
    size: { width: 70, height: 30 },
    attrs: { text: { text: 'example' } }
})

rect.on('change:position', function(element) {
    console.log(element.id, ':', element.get('position'));
});

このように書くことが出来ます。簡単ですね。
Element/Paper/Graphとそれぞれハンドリングできるイベントが異なります。
よく使うものだけ解説します。

Element

  • change:エレメントに何かしらの変更が生じた場合
  • change:position:positonに変更があった場合
  • change:angle:エレメントが回転した場合
  • change:size:エレメントのサイズに変化が合った場合
  • change:attrs:エレメントのattributesに変更が合った場合

その他はこちら

Paper

  • cell:pointerdown:addCellしたElementに対してpointerを下ろした場合
    ※同じようなものでpointerup/pointermoveが存在しています
  • cell:mouseover:addCellしたElement上をmouseoverした場合
paper.on('blank:pointerdown', function(evt, x, y) { 
  alert('pointerdown');
})

Graph

  • change:何かしらgraphに変更があった場合
  • add:graphにaddした場合
  • remove:graphからremoveした場合
graph.on('add', function(cell) { 
  alert(cell.id + 'がaddされました');
})

サンプルの解説

実行結果はこちら
※表示されてませんね・・・orz

index.html
<!DOCTYPE html>
<html>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<head>
    <link rel="stylesheet" href="stylesheets/joint.min.css" />
    <link rel="stylesheet" href="stylesheets/main.css" />
    <script src="javascripts/jquery.min.js"></script>
    <script src="javascripts/lodash.min.js"></script>
    <script src="javascripts/backbone-min.js"></script>
    <script src="javascripts/joint.min.js"></script>
    <script src="javascripts/main.js"></script>
</head>                                                                                                                                                        
<body>
  <div id="paper"></div>
</body>
</html>

main.js

$(function() {
  graph = new joint.dia.Graph;
  paper = new joint.dia.Paper({ 
   el: $('#paper'), 
  width: 800, 
   height: 800, 
   gridSize: 100, 
   model: graph
  });

  // 初期から出ているbox
  var el1 = new joint.shapes.html.Element({
    position: { x: 80, y: 80 },
    size: { width: 170, height: 80 },
    divName: 'Atrae',
    isCompany: true
  });

  graph.addCells([el1]);

  paper.on('cell:pointerup', function(cellView, evt, x, y) {
    var elementBelow = graph.get('cells').find(function(cell) {
      if (cell instanceof joint.dia.Link) return false;
      if (cell.id === cellView.model.id) return false;
      if (cell.getBBox().containsPoint(g.point(x, y))) return true;
      return false;
    });                                                                                                                                                        

    if (elementBelow && !_.contains(graph.getNeighbors(elementBelow), cellView.model)) {
      graph.addCell(new joint.shapes.org.Arrow({
        source: { id: cellView.model.id },
        target: { id: elementBelow.id }
      }));
      cellView.model.translate(-200, 0);
    }
  });
});

joint.shapes.html = {};
joint.shapes.html.Element = joint.shapes.basic.Rect.extend({
  defaults: joint.util.deepSupplement({
    type: 'html.Element',
    attrs: {
      rect: { stroke: 'none', 'fill-opacity': 0 }
    }
  }, joint.shapes.basic.Rect.prototype.defaults)
});

joint.shapes.html.ElementView = joint.dia.ElementView.extend({
  template: [
    '<div class="html-element">',
    '<button class="delete">x</button>',
    '<input type="text" value=""/>',
    '<button class="js-divName"></button>',
    '<button class="add">+</button>',
    '</div>'
  ].join(''),

  initialize: function() {
    _.bindAll(this, 'updateBox');
    joint.dia.ElementView.prototype.initialize.apply(this, arguments);

    this.$box = $(_.template(this.template)());
    this.$box.find('input').on('mousedown click', function(evt) { evt.stopPropagation(); });

    this.$box.find('input').on('change', _.bind(function(evt) {
      this.model.set('divName', $(evt.target).val());
    }, this));

    // delete element
    this.$box.find('.delete').on('click', _.bind(this.model.remove, this.model));
    this.$box.find('.add').on('click', _.bind(this.addBox, this.model));
    this.model.on('change', this.updateBox, this);
    this.model.on('remove', this.removeBox, this);

    if (this.model.get('isCompany') === true) {
      this.$box.find('input').remove();
      this.$box.find('.delete').remove();
    }
    this.updateBox();
  },
  render: function() {
    joint.dia.ElementView.prototype.render.apply(this, arguments);
    this.paper.$el.prepend(this.$box);
    this.updateBox();
    return this;
  },

 updateBox: function() {
    var bbox = this.model.getBBox();
    this.$box.find('.js-divName').text(this.model.get('divName'));
    this.$box.css({
      width: bbox.width,
      height: bbox.height,
      left: bbox.x,
      top: bbox.y,
      transform: 'rotate(' + (this.model.get('angle') || 0) + 'deg)'
    });
  },
  removeBox: function(evt) {
    this.$box.remove();
  },
  addBox: function(evt) {
    var el1 = new joint.shapes.html.Element({
      position: { x: evt.clientX + 120, y: evt.clientY - 35 },
      size: { width: 170, height: 80 },
      divName: ''
    });

    var line = new joint.shapes.org.Arrow({
      source: { id: this.id },
      target: { id: el1.id },
      vertices: [{ x: 100, y: 120 }, { x: 150, y: 60 }]
    });
    graph.addCells([el1, line]);
  }
});

一応以下のようなものが出来ましたw

スクリーンショット 2015-11-05 10.28.52.png

35
41
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
35
41

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?