jQuery: Cytoscape.js お試せた19 (Performance に書かれてること)

もっとてことだ。

jQuery: Cytoscape.js お試せた19 (Performance に書かれてること)

ここまでで「Cytoscape.js そのものとは無関係なパフォーマンス改善」してきて、まぁまぁ満足なものになりつつあるのでさらに ドキュメントの「Performance」に書かれていることで出来ることはないかしら、と。

上から順に significant だ言うておる。無論「significant 順だから」という理由だけで「オレに役立つ順」とは限らん。

Use cy.getElementById()

The cy.getElementById('foo') function is the fastest way to get an element by ID. You can use cy.$id('foo') to type less. Searching by selector generally means you have to check each element in the collection, whereas getting by ID is much faster as a lookup table is used. The single ID selector (e.g. cy.$('#foo')) is optimised to also use the lookup table, but it does have the added cost of parsing.

当然のことが書いてあって、まぁそりゃそうでしょうねと思う。

ただワタシのヤツって、「「ターゲットとなる id を先に特定してから element を取り出す」方がオイシイ」ということそのものが、あんましないのよね、今のところ。

まず malrawdataMAL から持ってきたデータ全部が入っている。けれどワタシの node filter は条件に合致しないものはそもそも cytoscape に「ぶち込んでない」。つまり、母集団として malrawdata のほうが cytoscape.js エレメント数の何倍も何十倍も大きい。実際今「宝石の国」全キャストデータぶん持ってるが、全部を cytoscape エレメントとしてぶち込めばアニメノード 2281、キャラクターノード 2561 になってしまうが、node filter で刈り込んでしまうと「欲張らないなら」せいぜいトータルで 100 程度に収まる。(無論「2017年以前の全部見たい!」みたいに思うなら別。) つまり例えば以下:

 1 function _apply_node_finder() {
 2     var find_what = $("#node-finder-by-name").val().trim();
 3     // ...
 4     find_what = new RegExp(find_what);
 5     // ...
 6     var selecter = "node";
 7     // ...
 8     // ↓こういう「フルスキャン」しなくて済むならそうせい、つぅことでしょ例えば
 9     var eles = cy.elements(selecter);
10     // ...
11     for (var i in eles) {
12         var anime = eles[i].data("a");  // 作成初期と違って今は data には id 関連しか入れてない
13         // ...
14         eles[i].select();
15     }
16 }

これを malrawdata の方を回して「ターゲットとなる id を先に特定してから cy.$id('foo') する」のはつまりワタシのこのケースでは「別においしくはなさそうだ」ということになる。どうだろうなぁ…、「cy.elements(selector>」が malrawdata フルスキャンのコストを(100 vs 2281 + 2561 なのに)大幅に上回ってしまうほどのものなら…そうするんだろうけれど、さすがに 50 倍もの性能比がそう簡単に出るとも思えないわ。(特にこれ、「レンダリング」とは関係ないんだからさ。ただのリニアスキャンの話でしょ?)

ワタシの場合こういうタイプのグラフを作りこむのは graphviz (や素の bourne shell で手作りしてた SVG や KML)でかなり何度もやっているので、エレメント数の多さが「すぐに使い物にならないほど重くなる」ことは重々承知の上で作り始めてる。だからこそ「真っ先に node filter」を書き、そして「追加するけど隠すのか、そもそも追加しないのか」の決断ではシンプルに後者を選択した。レンダリングのコストだけでなくスキャンのコストも見越してたわけだね(とかっこつけてみるが、こんなん経験則からほとんど条件反射的にそうしてるだけ、歳の功)。

そんなわけで、ワタシのケースではこの「most significant」な「Use cy.getElementById() れやヲラ」は(今のところ)適用対象外。

Batch element modifications

Use cy.batch() to modify many elements at once.

これはこの時点でやったつもりにはなってるが、効果的な使い方が出来てるかどうかの確認は出来てない。本来非同期パターンで価値を持つものだそうだから、ワタシが書いたのでは「自己満足」の可能性が高い。まぁあとで非同期型に書き換えるかもしれんしね、「お約束」のように思っといてもいいとは思う。

Animations

You will get better performance without animations. If using animations anyway:

  • eles.flashClass() is a cheaper alternative than a smooth animation.
  • Try to limit the number of concurrent animating elements.
  • When using transition animations in the style, make sure transition-property is defined only for states that you want to animate. If you have transition-property defined in a default state, the animation will try to run more often than if you limit it to particular states you actually want to animate.

アニメーションについては今やったりやらなかったりしてる。レイアウトの部分適用時にはアニメーションしてて、最初に全体にレイアウトを適用する際にはそうしてない。これは何かポリシーがあってそうしたわけではなくて、「あとで作ったレイアウトの部分適用時にアニメーション出来ることに気付いてそうした」だけ。けど結果論としては「性能面を考えてもその組み合わせがいい」だろうねと思う。

ちなみに「アニメーション」ネタは、まだちょっとやりたいことがある。Google Earth にさ、「ツアー」てあるじゃない。あれと似たノリのことをしたいんだよね。目視やマニュアル操作を駆使してグラフを探索するだけでなく、オートメーション的にグラフを巡る手段が欲しいのよね。基本的にはターゲットノードを pan/zoom していくものになるけれど、その際にうにょうにょ動いたらちょっとオモロイかなと思って。その場合に、あぁ、eles.flashClass() を使えば「一瞬光らせる」とか出来るね。(全て css 任せの cytoscape としては他力本願な機能だねこれはたぶん。)
(追記: 中途半端なやりかけが今貼り付けてるヤツに入ってる。)

Function style property values

While convenient, function style property values can be expensive. Thus, it may be worthwhile to use caching if possible, such as by using the lodash _.memoize() function. If your style property value is a simple passthrough or linear mapping, consider using data() or mapData() instead.

作り始めたときはやってた気がするけど、たぶん今は全部静的に決めてる…、と思う。例えばここ:

 1     // cy_elements["ac_edges"] はあとで
 2     //     cy.add(cy_elements["anime_nodes"].concat(
 3     //        cy_elements["char_nodes"]).concat(
 4     //            cy_elements["ac_edges"]).concat(
 5     //                cy_elements["sa_edges"]));
 6     // という具合に cy.add でぶち込むデータ。
 7     cy_elements["ac_edges"].push(
 8         {group: "edges",
 9          data: {
10              // こういうとこでダイナミックに source: function() {...} 出来る、
11              // けどコストが高いぜっ、って言ってるんだよね? たぶん。
12              source: "a" + chara_node["a"]["#"],
13              target: chara_node["id"],
14              for_status_bar:
15              anime["j"]
16                  + " -> " + ch["j"]
17                  + " (" + cv["j"] + ")"
18                  + " [ " + anime["n"] + " -> " + ch["n"]
19                  + " (" + cv["n"] + ") ]",
20          },
21          style: {
22              label: anime["j"],
23              "source-label": ch["j"],
24          },
25          classes: "ac"}
26     );

読み間違えてるかな? そういうこと言ってるんだと思うんだけど…。

Labels

Drawing labels is expensive.

  • If you can go without them or show them on tap/mouseover, you’ll get better performance.
  • Consider not having labels for edges.
  • Consider setting min-zoomed-font-size in your style so that when labels are small — and hard to read anyway — they are not rendered. When the labels are at least the size you set (i.e. the user zooms in), they will be visible.
  • Background and outlines increase the expense of rendering labels.

これは cytoscape に教えられなくても graphviz や SVG、KML でのかなりの経験数から元々理解していたこと。いずれやらねばと思ってたし。

まずそもそも「ラベルなんか消しちまえ」はワタシのヤツの場合は論外。機能不全でしょう、それじゃ。だから「必要がないケースでは隠す」というのがベスト。でも「マウスオーバ(or タップ)時だけ見せるのがええぜ」はワタシのヤツの場合はちょっとね、やり過ぎ。「背景とともにラベルがあると…」は、これは今はやらないけどそのうち「設定」で絵を出さないように出来るようにしようとは思ってる。

アウトラインは消せないなぁ。これ消すとエラい文字が読みにくい。背景色を工夫してどうにか、というもんだと思うが、これ考え出すと試行錯誤数が半端ないことが予想されますのよ。

ので今すぐ適用できるのは、min-zoomed-font-size。さっそくやってみた:

 1 // initialize cy
 2 var cy = window.cy = cytoscape({
 3     container: document.getElementById('cy'),
 4 
 5     // ...
 6     style: cytoscape.stylesheet()
 7         // ...
 8         .selector("node")
 9         .css({
10             "text-valign": "center",
11             "text-halign": "center",
12             "text-wrap": "wrap",  /* for multi-line */
13 
14             // ↓もともとデフォルト使ってた(指定してなかった)が、明示的に
15             //   書かないとプログラムがわかりにくいのと、あとなんか入れないと
16             //   効いてない気もする。
17             "font-size": 15,
18         })
19         // ...
20         .selector("edge.ac")
21         .css({
22             // ...
23             "font-size": 7,
24             // ...
25         })
26         .selector("edge.sa")
27         .css({
28             // ...
29             "font-size": 7,
30             // ...
31         })
32         // ...
33         .selector("node")
34         .css({
35             "min-zoomed-font-size": 10,
36         })
37         .selector("edge")
38         .css({
39             "min-zoomed-font-size": 10,
40         }),
41 });

「レンダリング時フォントサイズ」が閾値を下回ったら表示しない、という指示なので、ちょっと振る舞いがわかりにくいんだけれど、地図系のアプリケーションの動きを思い出してもらえればいい。拡大率を上げてはじめて読めるようになる文字とかあるでしょ? あれ。

懸念が一個あってさ。export でどうなっちゃうのかね。やってみたんだけれど、案の定「full」指定しないと「見た目のまんま」文字が出力されない。いつでも「full」で使うぶんにはいいけれど、ビューを限定して絵にしたい場合は注意した方がいいと思う。まぁ「ビューを限定」した際にこの閾値にひっかかるほど文字が小さい(つまりビューの範囲が広い)というのも、意図としてはあまりよくわからんので、まぁいいのかなという気もしないでもないけれど。

Simplify edge style

Drawing edges can be expensive.

  • Set your edges curve-style to haystack in your stylesheet. Haystack edges are straight lines, which are much less expensive to render than bezier edges. This is the default edge style.
  • Use solid edges. Dotted and dashed edges are much more expensive to draw, so you will get increased performance by not using them.
  • Edge arrows are expensive to render, so consider not using them if they do not have any semantic meaning in your graph.
  • Opaque edges with arrows are more than twice as fast as semitransparent edges with arrows.

curve-style は…、あら、bezier 使ってたか。どーすっかなぁ。そもそも2つのノードが複数のエッジで結ばれるケース、たとえば路線図みたいなものだと直線ではそもそも成立しないんだけれど、今のヤツはノード一対一に対して一本しか引かないからね。straight line でもいい気がしてくる。そうしちゃおうか:

 1 // initialize cy
 2 var cy = window.cy = cytoscape({
 3     container: document.getElementById('cy'),
 4 
 5     // ...
 6     style: cytoscape.stylesheet()
 7         // ...
 8         .selector("edge")
 9         .css({
10             "curve-style": "haystack",
11         })
12         .selector("edge.ac")
13         .css({
14             // ...
15         })
16         .selector("edge.sa")
17         .css({
18             // ...
19         })
20         // ...
21 });

うん、アタシのケースでは「変えたことに気付かない」ほど全然いっしょ。ならこれでいいわ。ワタシのヤツで compound タイプのグラフにする予定もないし。

solid edge を使え、についてはもとからそうしてる。デフォルトだし。Edge arrows も、この時点で既にわざわざ消してる。今のヤツの場合は directional ではないからね。(向きがあると思えばあるがないと思えばない。考え方次第だ。)

不透明がいい、てのはすぐには採用しがたいな。どうしても見栄えというよりは「非常に読みずらい」グラフになっちゃう。これをやるならまた大試行錯誤大会が勃発する。


23:00 追記:
haystack はワタシのケースではダメだったわ。ノードの中心から引いちゃう、というだけならまだしも、それを制御出来そうな気配をかもしだしている haystack-radius が全く使い物にならず、また、せっかく調整した source-text-offset も台無しになっちゃうし。なので、ワタシのケースのように source-texttarget-text にこだわりがある人はそれだけでもうアウトだし、当然アタシの例のような「箱に情報を詰め込む」スタイルのものを作ってるなら「箱の中に線がいたらダメっしょ」。

てわけで bezier に戻す。「実際に動くヤツ」も差し替えておく。

Simplify node style

読む前からほぼ採用できんだろうと予測しつつ:

Certain styles for nodes can be expensive.

  • Background images are very expensive in certain cases. The most performant background images are non-repeating (background-repeat: no-repeat) and non-clipped (background-clip: none). For simple node shapes like squares or circles, you can use background-fit for scaling and preclip your images to simulate software clipping (e.g. with Gulp so it’s automated). In lieu of preclipping, you could make clever use of PNGs with transparent backgrounds.
  • Node borders can be slightly expensive, so you can experiment with removing them to see if it makes a noticeable difference for your use case.

背景画像「なし」は、「設定で出さないようにする」はあっても「そもそもいらんのでは」はワタシのヤツの場合は、ありえない。視覚的にわかりやすい関連図を作りたいんだから。なので「どう効率的に出せるか」がポイントになる、てわけだね。

background-fit はもう使ってる。そうでない software clipping には無論興味はあるけれど、ただ例として挙げられてる Gulp はこれは、「サーバサイドを node.js でサービスする」のに使うやつみたい。ImageMagik を呼びだすんか。node.js も ImageMagik もどっちも、「ワタシのレンタルサーバ(ロリポップ!)」でサービス立てるつもりならアウトだが、まぁこんなん自力で Python で作っちゃえるわな。ともあれ今の場合は全部クライアントサイド(ブラウザ)で完結するアプローチでやってるんで、すぐには使えない。(クライアントサイドだけでやってると制約多いんで、本気になったらサービス立てるかもしれんけど、とにかく今ではない。)

background-repeat: no-repeat はそのつもりになってるが、background-clip: none はこれはなんだ? ワタシのヤツの場合はノード個々に指定しててこんな具合:

 1 // ...
 2 // あとで cy.add することになるデータ
 3 cy_elements["char_nodes"].push(
 4     {group: "nodes", data: chara_node, classes: classes,
 5      style: {
 6          "background-image-crossorigin": "anonymous",
 7          "background-image": [
 8              urlbuilder.get_image(ch["p"]),
 9              urlbuilder.get_image(cv["p"])
10          ],
11          "background-fit": "contain contain",
12          "background-position-x": "10% 90%",
13          "label": ch["j"] + "\n(" + cv["j"] + ")",
14          "background-clip": "none",  // これ
15      },
16     });
17 // ...
18 cy_elements["anime_nodes"].push(
19     {group: "nodes",
20      data: {"id": "a" + e, "a": {"#": e}},
21      style: {
22          "background-image-crossorigin": "anonymous",
23          "background-image": [
24              urlbuilder.get_image(malrawdata["a"][e]["p"])
25          ],
26          "background-fit": "contain", // or "cover",
27          "label": malrawdata["a"][e]["j"],
28          "background-clip": "none",  // これ
29      },
30      classes: "a"},
31 );

やってみたが、ワタシのヤツでは見た目は変わらないので、これでいいのかな。デフォルトじゃないんかね? おそらく「見たくない範囲を持った画像をスタイルでクリッピングしようとせずに、期待通りの画像を使うようにするがいいさ」てことよね。ワタシの場合は元々隠したい領域はないもん、関係ないてことだね。(none がデフォルトでないなら「これは性能改善だ」となろうが、書かれてないけど普通 none がデフォルトなんではないの?)

node border は…、そういやこれを :selected 以外ではまだやってなかったか? なんかのぺっとしてて、ボーダあったほうがいいかなと思ってたんで、ちょっと気をつけたほうがいいか。ただ、書かれてないけどおそらく「square」なボーダはさほどじゃないんじゃないかな。要するにこういうの、お絵かきに必要なポイント数が多いほど(path が長くなるほど)ヘビィなんだよね。単純な四角だと言うほどではない気がする。

Set a lower pixel ratio

Because it is more expensive to render more pixels, you can set pixelRatio to 1 in the initialisation options to increase performance for large graphs on high density displays. However, this makes the rendering less crisp.

これは何を言ってるのかまだわかってない。というか指定の仕方がわからんてこと。こういうデバイス依存の話は難しいよね。

Compound nodes

Compound nodes make style calculations and rendering more expensive. If your graph does not require compound nodes, you can improve performance by not using compound parent nodes.

compound はね、実はワタシのヤツの場合、「アニメノードを親に、キャラクターノードを子に」という考え方で適用することも出来たのね。これはこれで cytoscape ネタとして面白いので、いつか別のヤツをネタに「やってだけはみせようか」とも思ったけれど、ただ「声優関連図」ではあんまし compound しても読みやすいものになる気がしなかったのでね、シンプルに「ノードの見た目を制御するだけ」にした、という経緯が(説明してこなかったけど)あります。うそじゃないよ、こうみえて「少しはマジメに考える人」だもん、全然そうは見えないと思うし、全然そう思わないと思うし、全然まったくかけらもその雰囲気を感じてないと思うし、およそ完全に少しもまったくそうだとみなされてないとは思うけど。

こういうやつだよ、compound。良さそうでしょ? そしてこれがヘビィなのよ、て話。そもそも多くの layout が compound をサポートしてないてのもあって、ワタシのヤツみたく layout を選べるようにしたいとなると、最初から採用出来ない。

Hide edges during interactivity

Set hideEdgesOnViewport to true in your initialisation options. This can make interactivity less expensive for very large graphs by hiding edges during pan, mouse wheel zoom, pinch-to-zoom, and node drag actions. This option makes a difference on only very, very large graphs.

これも「これは cytoscape に教えられなくても graphviz や SVG、KML でのかなりの経験数から元々理解していたこと。いずれやらねばと思ってたし。」なヤツ。ドラッグ移動時にゴテゴテ装飾が付いたまま移動させるとこれは「とてつもなく重くて不愉快になる」は、もう「あるあるネタ」でしかない。

ただ…、ヲィ、hideEdgesOnViewport、これは違うぞ…。ドラッグ開始したら全部のエッジを隠す…のは普通は望んだことではないぞ。というか「それが必要なこともあるけれど、「関連エッジ以外は隠す」くらいはないと」てことなんじゃないの?

仕方がないのでこれは自力で:

 1 function _toggole_visible_edges(target_edges, display) {
 2     cy.elements("edge").forEach(function(edge, _) {
 3         var found = false;
 4         target_edges.forEach(function(te, _) {
 5             if (te === edge) {
 6                 found = true;
 7                 return;
 8             }
 9         });
10         if (!found) {
11             edge.css("display", display);
12         }
13     });
14 }
15 cy.on('tapstart', function(event) {
16     if (cy === event.target) {  // neither node nor edge.
17         return;
18     }
19     if (event.target.group() == "nodes") {  // node
20         var sels = cy.elements("node:selected");
21         if (!sels.length) {
22             sels = event.target;
23         }
24         _toggole_visible_edges(sels.connectedEdges(), "none");
25     } else {  // edge
26         _toggole_visible_edges(event.target, "none");
27     }
28 });
29 cy.on('tapend', function(event) {
30     if (cy === event.target) {  // neither node nor edge.
31         return;
32     }
33     if (event.target.group() == "nodes") {  // node
34         var sels = cy.elements("node:selected");
35         if (!sels.length) {
36             sels = event.target;
37         }
38         _toggole_visible_edges(sels.connectedEdges(), "element");
39     } else {  // edge
40         _toggole_visible_edges(event.target, "element");
41     }
42 });


開始のトリガーがどれなんだ、と思った。ノードを触った最初、で始めたいわけである。開始、と終了、のたった2度だけで処理を済ませたいのであって、「動かしてる間じゅう処理し続ける」んでは困る。tapstarttapend でいいみたいね。それと探索のためのイタレーションがなんかダサくてやだがまだうまいやり方わからん。あとこれは html でもそうだけど、「display」を制御するか「visibility」を制御するか問題があるが、今の場合は display で良かろう。

やってみる前から思ってたんだけどこれ、「そもそもこうすると視覚的に非常にわかりやすい」んだよね。自作せねばならんのはイケてないが「気付かせてくれてありがとう」。

ただ些細な欠点もあって。ワタシのヤツの場合、キャラクターノードを動かす場合はいい感じなんだけれど、アニメノードからは何十ものキャラクターノードに繋がっているので、劇的な効果を感じずらいんだよねぇ。


21:40 追記:
上の「自作」、色々間違えてた。

「選択されているもの、もしくはドラッグのためにタップしたノード」という二択はないわ。これは OR 採らないと。けどなんか cytoscape のコレクションの扱いが、今ひとつ使いにくいんだよなぁ、ちょいと苦労した。

あと「タップでドラッグする際」と限定しちゃってるのも違う。ズーム操作、パン操作の際も同じことをしないと。こっちは今度は「開始・終了」が取れないという問題が。これは setTimeout でやるしかない。


というわけでひとまずこうした(pan に明示的に反応するのは保留):

 1 function _toggole_visible_edges(target_edges, display) {
 2     cy.elements("edge").forEach(function(edge, _) {
 3         var found = false;
 4         target_edges.forEach(function(te, _) {
 5             if (te === edge) {
 6                 found = true;
 7                 return;
 8             }
 9         });
10         if (!found) {
11             edge.css("display", display);
12         }
13     });
14 }
15 cy.on('tapstart', function(event) {
16     if (cy === event.target) {  // neither node nor edge.
17         var edges = cy.elements("node:selected");
18         _toggole_visible_edges(edges.connectedEdges(), "none");
19     } else if (event.target.group() == "nodes") {  // node
20         var edges = cy.elements("node:selected, #" + event.target.id() + "");
21         _toggole_visible_edges(edges.connectedEdges(), "none");
22     } else {  // edge
23         _toggole_visible_edges(event.target, "none");
24     }
25 });
26 cy.on('tapend', function(event) {
27     if (cy === event.target) {  // neither node nor edge.
28         var edges = cy.elements("node:selected");
29         _toggole_visible_edges(edges.connectedEdges(), "element");
30     } else if (event.target.group() == "nodes") {  // node
31         var edges = cy.elements("node:selected, #" + event.target.id() + "");
32         _toggole_visible_edges(edges.connectedEdges(), "element");
33     } else {  // edge
34         _toggole_visible_edges(event.target, "element");
35     }
36 });
37 var __hideedges_on_zoom_timeout = 0;
38 cy.on('zoom', function(event) {
39     if (__hideedges_on_zoom_timeout) {
40         clearTimeout(__hideedges_on_zoom_timeout);
41         __hideedges_on_zoom_timeout = 0;
42     }
43     var edges = cy.elements("node:selected");
44     _toggole_visible_edges(edges.connectedEdges(), "none");
45     __hideedges_on_zoom_timeout = setTimeout(function() {
46         _toggole_visible_edges(edges.connectedEdges(), "element");
47         }, 300);
48 });

最後の「実際に動くやつ」は新しいので差し替えておいた。


翌日01:30 追記:
あかん、「性能改悪」してた。動きは間違ってなかったんだけれど。「全エッジ検索して全エッジに css」をやっちゃダメだ。

ただ、「スタイルだけ差し替える」のがなんともヘンチクリンなことになっちゃったけれど、ともかく最低でもこうでないとダメ:

 1 var __orig_style = null;
 2 function _toggole_visible_edges(target_edges, display) {
 3     // なんで json 経由せないかんのよ、は腑に落ちないけれど、とにかく
 4     // 「elements のほうではなくスタイルの方を」変更出来る。
 5     if (!__orig_style) {
 6         __orig_style = cy.style().json();
 7     }
 8     if (display == "none") {
 9         var style = jQuery.extend([], __orig_style);
10         style.push({selector: "edge", style: {display: display}});
11         cy.style().fromJson(style).update();
12         if (target_edges.length > 0) {
13             target_edges.style("display", "element");
14         }
15     } else {
16         if (target_edges.length > 0) {
17             target_edges.removeStyle("display"); // clear
18         }
19         var style = jQuery.extend([], __orig_style);
20         style.push({selector: "edge", style: {display: display}});
21         cy.style().fromJson(style).update();
22     }
23 }
24 var __nodes_on_tapstart = null;
25 cy.on('tapstart', function(event) {
26     if (cy === event.target) {  // neither node nor edge.
27         __nodes_on_tapstart = cy.elements("node:selected");
28         _toggole_visible_edges(
29             __nodes_on_tapstart.connectedEdges(), "none");
30     } else if (event.target.group() == "nodes") {  // node
31         __nodes_on_tapstart = cy.elements("node:selected, #" + event.target.id() + "");
32         _toggole_visible_edges(
33             __nodes_on_tapstart.connectedEdges(), "none");
34     } else {  // edge
35         _toggole_visible_edges(event.target, "none");
36     }
37 });
38 cy.on('tapend', function(event) {
39     if (cy === event.target) {  // neither node nor edge.
40         _toggole_visible_edges(__nodes_on_tapstart.connectedEdges(), "element");
41     } else if (event.target.group() == "nodes") {  // node
42         _toggole_visible_edges(__nodes_on_tapstart.connectedEdges(), "element");
43     } else {  // edge
44         _toggole_visible_edges(event.target, "element");
45     }
46 });
47 var __hideedges_on_zoom_timeout = 0;
48 cy.on('zoom', function(event) {
49     if (__hideedges_on_zoom_timeout) {
50         clearTimeout(__hideedges_on_zoom_timeout);
51         __hideedges_on_zoom_timeout = 0;
52     }
53     var edges = cy.elements("node:selected");
54     if (!__touring) { // この if は今無視してね
55         _toggole_visible_edges(edges.connectedEdges(), "none");
56     }
57     __hideedges_on_zoom_timeout = setTimeout(function() {
58         if (!__touring) { // この if は今無視してね
59             _toggole_visible_edges(edges.connectedEdges(), "element");
60         }
61     }, 300);
62 });

何かおかしい可能性はある。事実なんども間違えた。エッジが消えたままだったり消えなかったり。気付いたらあとでなんかの形で報告する。また追記かね。

なんで気付いたかというと、上で「アニメーションのネタとして「Google Earth のツアーみたいなヤツ」をやり始めて、そこでもエッジを隠さないと鬱陶しいもんだからやったんだけど、例の「大量ノード」のパターンで試したら返ってこなくて。バカだったよ。

最後の「実際に動くやつ」は新しいので差し替えておいた。中途半端な「ツアー」が入っちゃってるけどまぁよかろ。

Recycle large instances

Large instances can use a lot of memory, mostly due to canvas use. Recyling an instance will help to keep your memory usage lower than calling cy.destroy(), because you won’t grow the heap as much and you won’t invoke the garbage collector as much.

やってるつもりにはなってるんだけどね。まだやることあるのかな。「毎度 cy.remove(cy.elements());cy.add(...)」も避ける方法はあるのかな…? あぁ…ないこともないな…、「お初ノード・エッジ」だけを追加するようにする。

けどなぁ、これは追跡がタイヘン。node filter が刈り込んじゃうから。やってやれないこともないが、まぁそのうちやるか、て感じで、今慌ててやらなくてもいいかな。

Use textured zoom & pan

Set textureOnViewport to true in your initialisation options. Rather than rerendering the entire scene, this makes a texture cache of the viewport at the start of pan and zoom operations, and manipulates that instead. Makes panning and zooming smoother for very large graphs. This option makes a difference on only very, very large graphs. The renderer has support for more general texture caching, and so textureOnViewport is only useful if you really need the absolute cheapest option.

textureOnViewportの説明は:

A rendering hint that when set to true makes the renderer use a texture during panning and zooming instead of drawing the elements, making large graphs more responsive. This option is now largely moot, as a result of performance enhancements.

あんましよくわかんないがやってみる:

1 // initialize cy
2 var cy = window.cy = cytoscape({
3     container: document.getElementById('cy'),
4 
5     // ...
6     textureOnViewport: true,
7     // ...
8 });

あぁ、これは確かに。マウス操作で pan しようとしたり、ズームする際に動きが違う。矩形の外背景が見えてしまうのが気にならないなら、やっておくと良さそうだ。例の「宝石の国キャスト全員ぶん」で、node filter の条件を外しまくると凄まじい量のノードになるのだが、結構スムーズに動かせる気がする。

これ、地図系の WEB アプリケーションでよく使われてるテクニックだよね。あっちは「それしかやらない」のが標準なので、意識することはほとんどないけれど。

というわけで実際に動くヤツ


遊び方は一つ前に書いといた。ソースコードをどうしても読みたい人は、chrome なら「フレームのソースを表示」で読める。同じく chrome なら、Open Frame を導入すれば、ワタシのブログにはり付いたせせこましい画面でなく「スクリーン目一杯」使って遊べる。



Related Posts