ちゃーたらたらりら~、た~らら~ちゃーらたららら~。
Contents
jQuery: Cytoscape.js お試せた? 22 (ラベル(等)をダイナミックに取り替える…のはツラいのよ)
モチベーショんな
Cytoscape.js 「そのもの」のネタはそろそろもう尽きてきたかなと思っていたんだけれど、まだないこともなかった。ただし「いい話」ではなく「考えようによってはネガティブ」な話。
この一連のシリーズで作ってる「声優関連図作り子ちゃん」てのは、元データは MyAnimeList.net なる海外の「アニメオタク」向けサイトなわけね。なので基本的に「英語がメイン」で、日本語情報は「オマケ扱い」なわけよ。これが「アニメ本家であるところの日本に住むワレワレ」にとっては不便極まりないわけなんだけれど、まぁ「英語圏の人向けのサイト」なのでこれはいたし方がないこと。
というのがありつつ、で、ワタシが作ってるヤツは、まぁ「本気で道具として公開するつもりなら日本人だけに向けることもなかろう」というのと「貧乏性」の合わせ技一本でもって、「日本語と英語の両方を見せるようにしていた」。Cytoscape.js 範疇外のステータスバーでね。
なんだけどさ、「日本語と英語の両方を見せる」のって、その文字列フォーマットでゴテゴテしててとっても保守しずらいわけですよ。「設定でどっちか一方だけ見せりゃ良くね?」とさすがに思い始めて。そしてそうすると今度は「グラフ本体のノード・エッジのラベルも日本語・英語切り替えられるようにしないと」となる。これまでは「英語はステータスバーだけで」としてたけど、「設定で切り替え」という UI を用意するからにはさすがにそちらも出来ないと UI としてオカシイ。
けどね、「ノード・エッジのラベルを設定に従ってダイナミックに取り替える」、綺麗でなおかつ効率のいいやり方はないのよ、ということを今回愚痴っときたいなと思ったのですわ。
style に function style property value を使うのは解にならない
正確には、「仮に出来たとしても」嬉しい解とは言えない、ということで、なおかつ「出来ない」。
先にちょっとだけおさらい。Cytoscape の基本構造はだいたいこんな:
1 var cy = window.cy = cytoscape({
2 container: document.getElementById('cy'),
3
4 // ...
5 style: cytoscape.stylesheet()
6 // ※1
7 .selector("node:selected")
8 .css({
9 // たとえば。
10 "border-style": "solid",
11 "border-color": "#47119e",
12 "border-width": 6,
13 "border-opacity": 0.7,
14 })
15 // ...
16 }),
17
18 // ワタシが作ってるやつでは後から差し込んでるが、静的データを突っ込むつもりならこれでいい。
19 elements: [
20 { // ノードエレメント
21 group: "nodes",
22 data: {/*...データをお好きに、id だけは必須...*/},
23 style: { // ※2
24 "label": "...",
25 },
26 classes: "...", // ※3 スペース区切りで css の class 相当のものを並べる
27 },
28 // ...
29 { // エッジエレメント
30 group: "edges",
31 data: {/*...データをお好きに、source と target だけは必須...*/},
32 style: { // ※2
33 "label": "...",
34 },
35 classes: "...", // スペース区切りで css の class 相当のものを並べる
36 },
37 // ...
38 ],
39 });
※1と※2の関係がちょうど html のスタイルシートとエレメントに直接する style の関係に同じと思ってもらえればいい。※3も同じような類推でわかるであろう。当然※2は「エレメントに固有のこと」を書くためにあるわけで、普通は label がまさにそう。「ノードでっせ」みたいな意味のないラベルを出すんでない限りは。つまり「エレメントのデータに直結する何か」のために※2を使うわけだね。そしてついでにいえばエレメントの classes や data や style のいずれも後からエレメント単位で更新出来る。
で、function style property value とは何かといえば、こういうことが出来る:
1 elements: [
2 { // ノードエレメント
3 group: "nodes",
4 data: {/*...データをお好きに、id だけは必須...*/},
5 style: { // ※2
6 "label": function (elem) {
7 return 何か elem に関係するおらべるんな;
8 },
9 },
10 classes: "...", // ※3 スペース区切りで css の class 相当のものを並べる
11 },
とてもステキそうに見えるが、ここで既にみたように、「あんましこれすっと性能で泣きをみることになるぜ、ぼうず」と警告されている機能であることにまずは注意。
先に「出来ない」について片付けておきますか。これ、どうもおかしくて、「ノードなら問題ないがエッジがダメ」が結論みたいで、たとえば:
1 elements: [
2 { // ノードエレメント
3 group: "nodes",
4 data: {id: "...", my_osukina: "label ni tsukae-tara eenaxa"},
5 style: { // ※2
6 "label": function (elem) {
7 return elem.data("my_osukina"); // 問題ない、期待通り
8 },
9 },
10 classes: "...", // ※3 スペース区切りで css の class 相当のものを並べる
11 },
12 // ...
13 { // エッジエレメント
14 group: "edges",
15 data: {source: "...", target: "..."},
16 style: {
17 // エッジそのものにお便利データを突っ込んでおくか、もしくは
18 // エッジが結びついてるノードを取り出して云々するかどっちかだよね、
19 // と考えるも…:
20 "label": function(elem) {
21 // もうね、elem がまったくもって意図と違うものが渡ってきてどうにもならんの。
22 // elem.data() は undefined (当然 elem.data("source") も同じく undefined)
23 // elem.source() は undefined ではないが指し先の data() のみならず
24 // id() さえ undefined。(elem.target() も同上。)
25 // ゆえ、もう「固定値目的」でしか使えない。(つまり function スタイルの
26 // 価値がない。)
27 return "";
28 },
29 },
30 classes: "...", // スペース区切りで css の class 相当のものを並べる
31 },
「出来ない」と書かれてるようには思えないので、何かバグなんじゃないかなという気もするんだけれど、ワタシ、何か間違ってるだろうかねぇ?
ちなみに以下は「エラーを起こさない」だけで、「動く」けどお望みのものではない:
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 "label": function(elem) { ... },
11 }) // css は値として「文字列」をあてにしているので、わざわざ「呼び出して」はくれない。
12 // // ので、funciton 内に何書いてもいい。どうせ「fn」と出るだけだから好きにすればいい。
13 // // 意味があろうとなかろうと、それは「決して呼び出されない」のだから、好きにすれば
14 // // いいのだ。
15 // ...
16 }),
さて、じゃぁ「もしもエッジでも可能だと仮定したら」もしくは、「エッジのラベルには用はない」ならば、それじゃぁこれは「期待通りか」というと、これは残念ながら「否」。今回のケースみたいに「後から変更したい」のでないならこれでも機能しているけれど、function style とは言っても何度も何度も「やってくれるな」というタイミングで呼び出して「くれる」ことは「当然」ないので、結局何かしらの手段で「レンダーし直せやヲラ」をトリガーしてあげないといけない。(こういうことを考えてるとなんとなく「パンなりなんなりの操作がトリガーになって更新されて欲しい」と思うところなんだけど、実際はそういうことはしてくれないし、通常はそれされると結構迷惑だろう。凄まじくコストがかかるはず。)
というわけで、このアプローチは全然ダメ。
Data Mapper はもっと解にならない
Mappers は function style をより限定した、なおかつ function style より性能が良いもの、なので、通常はとってもありがたいものである。
が、今回やりたいことから考えると、Mappers には致命的な制約がある。
当たり前だが Mappers は「data にあるもの」しか使えない。つまり「ノード・エッジそのものが data として保持しているものの外には出られない、絶対に」。それでいいケースではそれでいい、実にいい:
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 "background-color": "mapData(my_osukina, 0, 100, blue, red)",
11 })
12 // ...
13 }),
14 elements: [
15 { // ノードエレメント
16 group: "nodes",
17 data: {id: "...", my_osukina: 50},
18 style: {
19 // ...
20 },
21 classes: "...",
22 },
けれども今の場合はダメだ。ノード・エッジとは全然関係ないデータに基いて変えたいのだから。
結局はエレメント全舐めして一個一個更新するしかないのであ
仮にそうだとしても、これが「このやり方だけが性能的に残念」ではないことに注意。function style を採用する場合でも結局「全更新」が必要なのだから。
もうね、最初からそうするつもりなら別に当たり前の実現方法だと思うしさ、何の抵抗もなく取り組めると思うんだけどね、なまじ「便利そうに思えるもの」が中途半端にあるもんだからさ、なんか気分良くないのよね。でもまぁ「素直でわかりやすいごくごく当然の実装」だよ:
1 function _update_elements_label() {
2 var lk = $("#label-lang").val(); // Japanese/English を指示するプルダウンの val
3
4 // node.a はワタシのヤツでの「アニメノード」、node.v が「キャラクターノード」
5 // (v は voice actor の v)。
6 cy.elements("node.a").forEach(function(n, _) {
7 // data() 関数は cytoscape 機能
8 // ("a" はワタシのノードに乗っけてる data 内に置いてるデータのキー。)
9 // malrawdata は cytoscape 外管理のただのグローバルデータ、css は cytoscape 機能。
10 // (style の alias。)
11 var anime = malrawdata["a"][n.data("a")["#"]];
12 n.css("label", anime[lk]);
13 });
14 cy.elements("node.v").forEach(function(n, _) {
15 var ch = malrawdata["c"][n.data("c")["#"]];
16 var cv = malrawdata["v"][n.data("v")["#"]];
17 n.css("label", ch[lk] + "\n(" + cv[lk] + ")");
18 });
19 // エッジの方は、エッジそのもののデータよりはもう「source ノード」
20 // 「target ノード」を問い合わせちゃって、そこのデータを使って。
21 cy.elements("edge.ac").forEach(function(n, _) {
22 var sn = n.source();
23 var tn = n.target();
24 var anime = malrawdata["a"][sn.data("a")["#"]];
25 var ch = malrawdata["c"][tn.data("c")["#"]];
26 n.css("label", anime[lk]);
27 n.css("source-label", ch[lk]);
28 });
29 cy.elements("edge.sa").forEach(function(n, _) {
30 var sn = n.source();
31 var cv = malrawdata["v"][sn.data("v")["#"]];
32 n.css("label", cv[lk]);
33 });
34 }
繰り返すけど「常識的で当たり前の実装」ね。最初からそのつもりなら「悪いもんじゃない」どころか、「めっちゃ普通」。
個人的に何が「億劫だったか」は単に「これまで書いてた場所からラベル関連を全部大々的にお引越し」せねばならなかったこと、だけだったりする。よくあるよね、こんなん。
というわけで、いつものように実際に動くヤツ:
基本的な遊び方はここの末尾参照。ソースコードを見たければ Chrome なら「フレームのソースを表示」(もうかなりデカいので「参考にするつもり」で読むと落胆しそうだけど)。そしてこんな狭苦しい画面で遊ぶのがいやなら Chrome なら Open Frame ね。
若干説明してない進化もしてるし、前回とかでオカシイ言ってる箇所はまだ直ってなかったりとか色々。それらはいちいち説明しない。あと前回紹介しといた「grid-guide」は使うのやめた。