nysol-view 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/bin/msankey.rb ADDED
@@ -0,0 +1,644 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: utf-8
3
+
4
+ require "rubygems"
5
+ require "nysol/mcmd"
6
+ require "json"
7
+ require "nysol/viewjs"
8
+
9
+ # ver="1.0" # 初期リリース 2014/3/08
10
+ # ver="1.1" # -nl追加 2014/12/02
11
+ # ver="1.2" # h=追加 2014/12/03
12
+ $cmd=$0.sub(/.*\//,"")
13
+
14
+ $version=1.1
15
+ $revision="###VERSION###"
16
+
17
+ def help()
18
+
19
+ STDERR.puts <<EOF
20
+ ----------------------------
21
+ #{$cmd} version #{$version}
22
+ ----------------------------
23
+ 概要) DAG(有向閉路グラフ)からsankeyダイアグラムをhtmlとして生成する。
24
+ 書式) #{$cmd} i= f= v= [-nl] [h=] [w=] [o=] [t=] [T=] [--help]
25
+
26
+ ファイル名指定
27
+ i= : 枝データファイル
28
+ f= : 枝データ上の2つの節点項目名
29
+ v= : 枝の重み項目名
30
+ o= : 出力ファイル(HTMLファイル)
31
+ t= : タイトル文字列
32
+ h= : キャンバスの高さ(デフォルト:500)
33
+ w= : キャンバスの幅(デフォルト:960)
34
+ -nl : 節点ラベルを表示しない
35
+
36
+ その他
37
+ T= : ワークディレクトリ(default:/tmp)
38
+ --help : ヘルプの表示
39
+
40
+ 入力形式)
41
+ 有向閉路グラフを節点ペア、および枝の重みで表現したCSVファイル。
42
+
43
+ 出力形式)
44
+ sankeyダイアグラムを組み込んだ単体のhtmlファイルで、
45
+ インターネットへの接続がなくてもブラウザがあれば描画できる。
46
+
47
+ 備考)
48
+ 本コマンドのチャート描画にはD3(http://d3js.org/)を用いている。
49
+ 必要なrubyライブラリ: nysol/mcmd, json
50
+
51
+ 例)
52
+ $ cat data/edge.csv
53
+ node1,node2,val
54
+ a,b,1
55
+ a,c,2
56
+ a,d,1
57
+ a,e,1
58
+ b,c,4
59
+ b,d,3
60
+ b,f,1
61
+ c,d,2
62
+ c,e,2
63
+ d,e,1
64
+ e,f,3
65
+ n1,n2
66
+ $ #{$cmd} i=edge.csv f=node1,node2 v=val o=output.html
67
+ Copyright(c) NYSOL 2012- All Rights Reserved.
68
+ EOF
69
+
70
+ exit
71
+ end
72
+
73
+ def ver()
74
+ $revision ="0" if $revision =~ /VERSION/
75
+ STDERR.puts "version #{$version} revision #{$revision}"
76
+ exit
77
+ end
78
+
79
+ help() if ARGV.size <= 0 or ARGV[0]=="--help"
80
+ ver() if ARGV[0]=="--version"
81
+
82
+ args=MCMD::Margs.new(ARGV,"i=,f=,v=,h=,w=,o=,t=,-nl,T=,--help","f=,v=")
83
+
84
+ # mcmdのメッセージは警告とエラーのみ
85
+ ENV["KG_VerboseLevel"]="2" unless args.bool("-mcmdenv")
86
+
87
+ #ワークファイルパス
88
+ if args.str("T=")!=nil then
89
+ ENV["KG_TmpPath"] = args.str("T=").sub(/\/$/,"")
90
+ end
91
+
92
+ ei = args. file("i=","r") # edgeファイル名
93
+ ef = args.field("f=", ei) # edge始点node項目名,終了頂点項目名
94
+ ef1=ef2=nil
95
+ if ef then
96
+ ef1=ef["names"][0]
97
+ ef2=ef["names"][1]
98
+ if ef1==nil or ef2==nil then
99
+ raise "f= takes just two field names"
100
+ end
101
+ else
102
+ unless int then
103
+ raise "f= is mandatory unless -int is specified"
104
+ end
105
+ end
106
+ ev=args.field("v=",ei)
107
+ ev_wk=ev["names"][0]
108
+ oFile = args.file("o=", "w")
109
+ title=args.str("t=","")
110
+ nl=args.bool("-nl")
111
+ height=args.int("h=",500)
112
+ width=args.int("w=",960)
113
+
114
+ wf=MCMD::Mtemp.new
115
+
116
+ #ノードデータ処理
117
+ #与えられたノードファイルから番号を振る
118
+ nodefile=wf.file #"xxnode.csv"
119
+ pairfile=wf.file #"xxpair.csv"
120
+ w1_file=wf.file #"wk1.csv"
121
+ w2_file=wf.file #"wk2.csv"
122
+ system "mcut f=#{ef1}:nodes i=#{ei} o=#{w1_file}"
123
+ system "mcut f=#{ef2}:nodes i=#{ei} o=#{w2_file}"
124
+ f=""
125
+ f<<"mcat i=#{w1_file},#{w2_file} f=nodes |"
126
+ f<<"msortf f=nodes |"
127
+ f<<"muniq k=nodes |"
128
+ f<<"mnumber a=num s=nodes |"
129
+ f<<"msortf f=nodes o=#{nodefile}"
130
+ system(f)
131
+ system "rm #{w1_file}"
132
+ system "rm #{w2_file}"
133
+
134
+ #エッジファイル処理(ノード名→(mapfileから)数字)
135
+ f=""
136
+ f<<"mcut f=#{ef1}:nodes,#{ef2},#{ev_wk} i=#{ei} |"
137
+ f<<"msortf f=nodes |"
138
+ f<<"mjoin k=nodes m=#{nodefile} f=num:num1|"
139
+ f<<"mcut f=nodes:#{ef1},#{ef2}:nodes,#{ev_wk},num1 | "
140
+ f<<"msortf f=nodes|"
141
+ f<<"mjoin k=nodes m=#{nodefile} f=num:num2|"
142
+ f<<"mcut f=num1,num2,#{ev_wk} |"
143
+ f<<"msortf f=num1%n,num2%n o=#{pairfile}"
144
+ system (f)
145
+
146
+ #json作成
147
+ wk=[]
148
+ nodes=[]
149
+ links=[]
150
+ #f=open("chart.json","w")
151
+ #f.puts '{"nodes":'
152
+ MCMD::Mcsvin::new("i=#{nodefile}"){|csv|
153
+ csv.each{|val|
154
+ # wk.push({:name=>val["nodes"]})
155
+ nodes.push({:name=>val["nodes"]})
156
+ }
157
+ }
158
+ nodes=nodes.to_json(nodes)
159
+ wk=[]
160
+ MCMD::Mcsvin::new("i=#{pairfile}"){|csv|
161
+ csv.each{|val|
162
+ links.push({:source=>val["num1"].to_i ,:target=>val["num2"].to_i , :value =>val["#{ev_wk}"].to_i})
163
+ }
164
+ }
165
+
166
+ links=links.to_json(links)
167
+
168
+ #----
169
+ #以下htmlファイル作成
170
+ nolabel=""
171
+ nolabel="font-size: 0px;" if nl
172
+
173
+ outTemplate = <<OUT
174
+ <!DOCTYPE html>
175
+ <html class="ocks-org do-not-copy">
176
+ <meta charset="utf-8">
177
+ <!--
178
+ <title>Sankey Diagram</title>
179
+ -->
180
+ <title>#{title}</title>
181
+ <style>
182
+
183
+
184
+ <style>
185
+
186
+ body {
187
+ font: 10px sans-serif;
188
+ }
189
+
190
+ svg {
191
+ padding: 10px 0 0 10px;
192
+ }
193
+
194
+ .arc {
195
+ stroke: #fff;
196
+ }
197
+
198
+ #tooltip {
199
+ position: absolute;
200
+ width: 150px;
201
+ height: auto;
202
+ padding: 10px;
203
+ background-color: white;
204
+ -webkit-border-radius: 10px;
205
+ -moz-border-radius: 10px;
206
+ border-radius: 10px;
207
+ -webkit-box-shadow: 4px 4px 10px rgba(0,0,0,0.4);
208
+ -moz-box-shadow: 4px 4px 10px rgba(0,0,0,0.4);
209
+ box-shadow: 4px 4px 10px rgba(0,0,0,0.4);
210
+ pointer-events: none;
211
+ }
212
+
213
+ #tooltip.hidden {
214
+ display: none;
215
+ }
216
+
217
+ #tooltip p {
218
+ margin: 0;
219
+ font-family: sans-serif;
220
+ font-size: 10px;
221
+ line-height: 14px;
222
+ }
223
+
224
+ #chart {
225
+ height: 500px;
226
+ }
227
+
228
+ .node rect {
229
+ cursor: move;
230
+ fill-opacity: .9;
231
+ shape-rendering: crispEdges;
232
+ }
233
+
234
+ .node text {
235
+ pointer-events: none;
236
+ text-shadow: 0 1px 0 #fff;
237
+ #{nolabel}
238
+ }
239
+
240
+ .link {
241
+ fill: none;
242
+ stroke: #000;
243
+ stroke-opacity: .2;
244
+ }
245
+
246
+ .link:hover {
247
+ stroke-opacity: .5;
248
+ }
249
+
250
+ </style>
251
+ <body>
252
+
253
+ <!--
254
+ <h1>Sankey Diagrams</h1>
255
+ -->
256
+ <h1>#{title}</h1>
257
+
258
+ <p id="chart">
259
+
260
+
261
+ <script>
262
+ #{ViewJs::d3jsMin()}
263
+
264
+ d3.sankey = function() {
265
+ var sankey = {},
266
+ nodeWidth = 24,
267
+ nodePadding = 8,
268
+ size = [1, 1],
269
+ nodes = [],
270
+ links = [];
271
+
272
+ sankey.nodeWidth = function(_) {
273
+ if (!arguments.length) return nodeWidth;
274
+ nodeWidth = +_;
275
+ return sankey;
276
+ };
277
+
278
+ sankey.nodePadding = function(_) {
279
+ if (!arguments.length) return nodePadding;
280
+ nodePadding = +_;
281
+ return sankey;
282
+ };
283
+
284
+ sankey.nodes = function(_) {
285
+ if (!arguments.length) return nodes;
286
+ nodes = _;
287
+ return sankey;
288
+ };
289
+
290
+ sankey.links = function(_) {
291
+ if (!arguments.length) return links;
292
+ links = _;
293
+ return sankey;
294
+ };
295
+
296
+ sankey.size = function(_) {
297
+ if (!arguments.length) return size;
298
+ size = _;
299
+ return sankey;
300
+ };
301
+
302
+ sankey.layout = function(iterations) {
303
+ computeNodeLinks();
304
+ computeNodeValues();
305
+ computeNodeBreadths();
306
+ computeNodeDepths(iterations);
307
+ computeLinkDepths();
308
+ return sankey;
309
+ };
310
+
311
+ sankey.relayout = function() {
312
+ computeLinkDepths();
313
+ return sankey;
314
+ };
315
+
316
+ sankey.link = function() {
317
+ var curvature = .5;
318
+
319
+ function link(d) {
320
+ var x0 = d.source.x + d.source.dx,
321
+ x1 = d.target.x,
322
+ xi = d3.interpolateNumber(x0, x1),
323
+ x2 = xi(curvature),
324
+ x3 = xi(1 - curvature),
325
+ y0 = d.source.y + d.sy + d.dy / 2,
326
+ y1 = d.target.y + d.ty + d.dy / 2;
327
+ return "M" + x0 + "," + y0
328
+ + "C" + x2 + "," + y0
329
+ + " " + x3 + "," + y1
330
+ + " " + x1 + "," + y1;
331
+ }
332
+
333
+ link.curvature = function(_) {
334
+ if (!arguments.length) return curvature;
335
+ curvature = +_;
336
+ return link;
337
+ };
338
+
339
+ return link;
340
+ };
341
+
342
+ // Populate the sourceLinks and targetLinks for each node.
343
+ // Also, if the source and target are not objects, assume they are indices.
344
+ function computeNodeLinks() {
345
+ nodes.forEach(function(node) {
346
+ node.sourceLinks = [];
347
+ node.targetLinks = [];
348
+ });
349
+ links.forEach(function(link) {
350
+ var source = link.source,
351
+ target = link.target;
352
+ if (typeof source === "number") source = link.source = nodes[link.source];
353
+ if (typeof target === "number") target = link.target = nodes[link.target];
354
+ source.sourceLinks.push(link);
355
+ target.targetLinks.push(link);
356
+ });
357
+ }
358
+
359
+ // Compute the value (size) of each node by summing the associated links.
360
+ function computeNodeValues() {
361
+ nodes.forEach(function(node) {
362
+ node.value = Math.max(
363
+ d3.sum(node.sourceLinks, value),
364
+ d3.sum(node.targetLinks, value)
365
+ );
366
+ });
367
+ }
368
+
369
+ // Iteratively assign the breadth (x-position) for each node.
370
+ // Nodes are assigned the maximum breadth of incoming neighbors plus one;
371
+ // nodes with no incoming links are assigned breadth zero, while
372
+ // nodes with no outgoing links are assigned the maximum breadth.
373
+ function computeNodeBreadths() {
374
+ var remainingNodes = nodes,
375
+ nextNodes,
376
+ x = 0;
377
+
378
+ while (remainingNodes.length) {
379
+ nextNodes = [];
380
+ remainingNodes.forEach(function(node) {
381
+ node.x = x;
382
+ node.dx = nodeWidth;
383
+ node.sourceLinks.forEach(function(link) {
384
+ nextNodes.push(link.target);
385
+ });
386
+ });
387
+ remainingNodes = nextNodes;
388
+ ++x;
389
+ }
390
+
391
+ //
392
+ moveSinksRight(x);
393
+ scaleNodeBreadths((width - nodeWidth) / (x - 1));
394
+ }
395
+
396
+ function moveSourcesRight() {
397
+ nodes.forEach(function(node) {
398
+ if (!node.targetLinks.length) {
399
+ node.x = d3.min(node.sourceLinks, function(d) { return d.target.x; }) - 1;
400
+ }
401
+ });
402
+ }
403
+
404
+ function moveSinksRight(x) {
405
+ nodes.forEach(function(node) {
406
+ if (!node.sourceLinks.length) {
407
+ node.x = x - 1;
408
+ }
409
+ });
410
+ }
411
+
412
+ function scaleNodeBreadths(kx) {
413
+ nodes.forEach(function(node) {
414
+ node.x *= kx;
415
+ });
416
+ }
417
+
418
+ function computeNodeDepths(iterations) {
419
+ var nodesByBreadth = d3.nest()
420
+ .key(function(d) { return d.x; })
421
+ .sortKeys(d3.ascending)
422
+ .entries(nodes)
423
+ .map(function(d) { return d.values; });
424
+
425
+ //
426
+ initializeNodeDepth();
427
+ resolveCollisions();
428
+ for (var alpha = 1; iterations > 0; --iterations) {
429
+ relaxRightToLeft(alpha *= .99);
430
+ resolveCollisions();
431
+ relaxLeftToRight(alpha);
432
+ resolveCollisions();
433
+ }
434
+
435
+ function initializeNodeDepth() {
436
+ var ky = d3.min(nodesByBreadth, function(nodes) {
437
+ return (size[1] - (nodes.length - 1) * nodePadding) / d3.sum(nodes, value);
438
+ });
439
+
440
+ nodesByBreadth.forEach(function(nodes) {
441
+ nodes.forEach(function(node, i) {
442
+ node.y = i;
443
+ node.dy = node.value * ky;
444
+ });
445
+ });
446
+
447
+ links.forEach(function(link) {
448
+ link.dy = link.value * ky;
449
+ });
450
+ }
451
+
452
+ function relaxLeftToRight(alpha) {
453
+ nodesByBreadth.forEach(function(nodes, breadth) {
454
+ nodes.forEach(function(node) {
455
+ if (node.targetLinks.length) {
456
+ var y = d3.sum(node.targetLinks, weightedSource) / d3.sum(node.targetLinks, value);
457
+ node.y += (y - center(node)) * alpha;
458
+ }
459
+ });
460
+ });
461
+
462
+ function weightedSource(link) {
463
+ return center(link.source) * link.value;
464
+ }
465
+ }
466
+
467
+ function relaxRightToLeft(alpha) {
468
+ nodesByBreadth.slice().reverse().forEach(function(nodes) {
469
+ nodes.forEach(function(node) {
470
+ if (node.sourceLinks.length) {
471
+ var y = d3.sum(node.sourceLinks, weightedTarget) / d3.sum(node.sourceLinks, value);
472
+ node.y += (y - center(node)) * alpha;
473
+ }
474
+ });
475
+ });
476
+
477
+ function weightedTarget(link) {
478
+ return center(link.target) * link.value;
479
+ }
480
+ }
481
+
482
+ function resolveCollisions() {
483
+ nodesByBreadth.forEach(function(nodes) {
484
+ var node,
485
+ dy,
486
+ y0 = 0,
487
+ n = nodes.length,
488
+ i;
489
+
490
+ // Push any overlapping nodes down.
491
+ nodes.sort(ascendingDepth);
492
+ for (i = 0; i < n; ++i) {
493
+ node = nodes[i];
494
+ dy = y0 - node.y;
495
+ if (dy > 0) node.y += dy;
496
+ y0 = node.y + node.dy + nodePadding;
497
+ }
498
+
499
+ // If the bottommost node goes outside the bounds, push it back up.
500
+ dy = y0 - nodePadding - size[1];
501
+ if (dy > 0) {
502
+ y0 = node.y -= dy;
503
+
504
+ // Push any overlapping nodes back up.
505
+ for (i = n - 2; i >= 0; --i) {
506
+ node = nodes[i];
507
+ dy = node.y + node.dy + nodePadding - y0;
508
+ if (dy > 0) node.y -= dy;
509
+ y0 = node.y;
510
+ }
511
+ }
512
+ });
513
+ }
514
+
515
+ function ascendingDepth(a, b) {
516
+ return a.y - b.y;
517
+ }
518
+ }
519
+
520
+ function computeLinkDepths() {
521
+ nodes.forEach(function(node) {
522
+ node.sourceLinks.sort(ascendingTargetDepth);
523
+ node.targetLinks.sort(ascendingSourceDepth);
524
+ });
525
+ nodes.forEach(function(node) {
526
+ var sy = 0, ty = 0;
527
+ node.sourceLinks.forEach(function(link) {
528
+ link.sy = sy;
529
+ sy += link.dy;
530
+ });
531
+ node.targetLinks.forEach(function(link) {
532
+ link.ty = ty;
533
+ ty += link.dy;
534
+ });
535
+ });
536
+
537
+ function ascendingSourceDepth(a, b) {
538
+ return a.source.y - b.source.y;
539
+ }
540
+
541
+ function ascendingTargetDepth(a, b) {
542
+ return a.target.y - b.target.y;
543
+ }
544
+ }
545
+
546
+ function center(node) {
547
+ return node.y + node.dy / 2;
548
+ }
549
+
550
+ function value(link) {
551
+ return link.value;
552
+ }
553
+
554
+ return sankey;
555
+ };
556
+ </script>
557
+
558
+ <script>
559
+ var margin = {top: 1, right: 1, bottom: 6, left: 1},
560
+ width = #{width} - margin.left - margin.right,
561
+ height = #{height} - margin.top - margin.bottom;
562
+
563
+ var formatNumber = d3.format(",.0f"),
564
+ format = function(d) { return formatNumber(d) + " TWh"; },
565
+ color = d3.scale.category20();
566
+
567
+ var svg = d3.select("#chart").append("svg")
568
+ .attr("width", width + margin.left + margin.right)
569
+ .attr("height", height + margin.top + margin.bottom)
570
+ .append("g")
571
+ .attr("transform", "translate(" + margin.left + "," + margin.top + ")");
572
+
573
+ var sankey = d3.sankey()
574
+ .nodeWidth(15)
575
+ .nodePadding(10)
576
+ .size([width, height]);
577
+
578
+ var path = sankey.link();
579
+
580
+ //d3.json("./chart.json", function(energy) {
581
+ // var aaa=#{wk}
582
+ var nodes=#{nodes}
583
+ var links=#{links}
584
+ sankey
585
+ // .nodes(energy.nodes)
586
+ // .links(energy.links)
587
+ .nodes(nodes)
588
+ .links(links)
589
+
590
+ .layout(32);
591
+
592
+ var link = svg.append("g").selectAll(".link")
593
+ // .data(energy.links)
594
+ .data(links)
595
+ .enter().append("path")
596
+ .attr("class", "link")
597
+ .attr("d", path)
598
+ .style("stroke-width", function(d) { return Math.max(1, d.dy); })
599
+ .sort(function(a, b) { return b.dy - a.dy; });
600
+
601
+ link.append("title")
602
+ .text(function(d) { return d.source.name + " → " + d.target.name + "" + format(d.value); });
603
+
604
+ var node = svg.append("g").selectAll(".node")
605
+ // .data(energy.nodes)
606
+ .data(nodes)
607
+ .enter().append("g")
608
+ .attr("class", "node")
609
+ .attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; })
610
+ .call(d3.behavior.drag()
611
+ .origin(function(d) { return d; })
612
+ .on("dragstart", function() { this.parentNode.appendChild(this); })
613
+ .on("drag", dragmove));
614
+
615
+ node.append("rect")
616
+ .attr("height", function(d) { return d.dy; })
617
+ .attr("width", sankey.nodeWidth())
618
+ .style("fill", function(d) { return d.color = color(d.name.replace(/ .*/, "")); })
619
+ .style("stroke", function(d) { return d3.rgb(d.color).darker(2); })
620
+ .append("title")
621
+ .text(function(d) { return d.name + "" + format(d.value); });
622
+
623
+ node.append("text")
624
+ .attr("x", -6)
625
+ .attr("y", function(d) { return d.dy / 2; })
626
+ .attr("dy", ".35em")
627
+ .attr("text-anchor", "end")
628
+ .attr("transform", null)
629
+ .text(function(d) { return d.name; })
630
+ .filter(function(d) { return d.x < width / 2; })
631
+ .attr("x", 6 + sankey.nodeWidth())
632
+ .attr("text-anchor", "start");
633
+
634
+ function dragmove(d) {
635
+ d3.select(this).attr("transform", "translate(" + d.x + "," + (d.y = Math.max(0, Math.min(height - d.dy, d3.event.y))) + ")");
636
+ sankey.relayout();
637
+ link.attr("d", path);
638
+ }
639
+ // });
640
+ </script>
641
+ OUT
642
+ File.open(oFile,"w"){|fp|
643
+ fp.puts outTemplate
644
+ }