nysol-view 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
+ }