【D3.js + node.js】 ブログのデータをGithub風のカレンダーに表示する
WordPressもくもく勉強会@群馬 #1で作成したものです。
サンプルコードが公開されているのでそれを元に作成しました。
Githubのカレンダー表示機能はD3.jsで作成されているのですが、そのBlog版のようなものです。
↓これ
色がついているセルが記事を描いた日、同じ日に書いた記事が多いほど濃い緑色で表示されるようになってます。
セルにマウスオーバーすると記事のタイトルが表示されクリックするとその日のブログ記事へ飛びます。
カレンダーにしてみると、ブログ記事を頑張って書いた月とそうでもない月が一瞥できてなかなか面白いですね。
データセットの作成
WordPressのダッシュボードから「ツール→エクスポート」を選択し、投稿記事をエクスポートします。
エクスポートしたxmlファイルをnode.jsを使ってjsonに変換しました。
変換スクリプトは、以前RSSリーダーを作った際のコードを利用。
(XMLのパースにfeedparserモジュールを使ってます)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
var FeedParser = require('feedparser') , fs = require('fs'); if(!process.argv[2]) throw 'Not File Found!'; var file = process.argv[2]; var data={ url:"", title:"", item:[] }; fs.createReadStream(file) .on('error', function (error) { console.error(error); }) .pipe(new FeedParser()) .on('error', function (error) { console.error(error); }) .on('meta', function (meta) { data.url = meta.link; data.title= meta.title }) .on('readable', function() { var stream = this, item; stream.end = output; while (item = stream.read()) { data.item.push({title:item.title, url:item.link, date:item["wp:post_date"]["#"]}); } }); function output(){ console.log(JSON.stringify(data)); } |
下記を実行してjsonファイルを作成します。
1 |
$ node wpxml2json.js [wp file] > wp.json |
このデータを使ってカレンダーを表示します。
カレンダー表示
基本、上記サンプルコードをちょっと弄っただけという。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 |
d3.json('wp.json', function(json){ d3main(json); }); function d3main(json) { var width = 960; var height = 136; var cellSize = 17; // セルのサイズ指定 //日付カラー var color = d3.scale.linear().domain([1, 5]).range(["#aae272", "#2b470e"]); //string→dateオブジェクト変換関数 var day = d3.time.format("%w"); var week = d3.time.format("%U"); var format = d3.time.format("%Y-%m-%d"); var blogURL = json.url; //タイトルにリンク追加 d3.select('h1').append('a').attr('href', blogURL).text(json.title); //データ整形 var data = d3.nest() .key(function(d) { return d.date.split(' ')[0]; }) //dateをkeyにネスト(時間は切り落とす) .map(json.item); //表示年の最小値と最大値を取得 var tmp = d3.keys(data); var minYear = d3.min(tmp, function(d){ return +d.split('-')[0] } ); var maxYear = d3.max(tmp, function(d){ return +d.split('-')[0] } ); //カレンダーの数(年毎)だけsvgタグを生成 var svg = d3.select("#stage").selectAll("svg") .data(d3.range(minYear, maxYear+1)) .enter() .append("svg") .attr("width", width) .attr("height", height) .append("g") .attr("transform", "translate(" + ((width - cellSize * 53) / 2) + "," + (height - cellSize * 7 - 1) + ")"); //カレンダ―位置指定 //年タイトル 生成 svg.append("text") .attr("transform", "translate(-6," + cellSize * 3.5 + ")rotate(-90)") .style("text-anchor", "middle") .text(function(d) { return d; }); //日付セル生成 var cell = svg.selectAll(".day") .data(function(d) { return d3.time.days(new Date(d, 0, 1), new Date(d + 1, 0, 1));}) .enter() .append("rect") .attr({ "class": "day", "width": cellSize, "height": cellSize, "x": function(d) { return week(d) * cellSize; }, "y": function(d) { return day(d) * cellSize; }, "fill": "#fff", "stroke": "#ccc" }) .datum(format); //データにもと付きセルをカラーリング cell.filter(function(d) { return d in data; }) //データが存在するかチェック .attr("fill", function(d){ return color(data[d].length); // データが存在するセルに色付け }) .on("mouseover", function(){ return tooltip.style("visibility", "visible"); }) .on("mousemove", function(d){ var titles = ""; data[d].forEach(function(d){ titles += "<li>"+d.title+"</li>" }); return tooltip .style("top", (d3.event.pageY+30)+"px") .style("left",(d3.event.pageX-90)+"px") .html(d + ": <br><ul>" + titles + "</ul>"); }) .on("mouseout", function(){ return tooltip.style("visibility", "hidden"); }) .on('click', function(d){ window.open(blogURL+'/date/'+d.replace(/-/g,'/')); }) //月境界pathジェネレーター var monthPath = function(t0) { var t1 = new Date(t0.getFullYear(), t0.getMonth() + 1, 0), d0 = +day(t0), w0 = +week(t0), d1 = +day(t1), w1 = +week(t1); //console.log("d0:"+d0+"/d1:"+d1+"/w0:"+w0+"/w1"+w1); var path = "M" + (w0 + 1) * cellSize + "," + d0 * cellSize + "H" + w0 * cellSize + "V" + 7 * cellSize + "H" + w1 * cellSize + "V" + (d1 + 1) * cellSize + "H" + (w1 + 1) * cellSize + "V" + 0 + "H" + (w0 + 1) * cellSize + "Z"; //console.log(path); return path; } //日付セルを月毎にpathで分類 svg.selectAll(".month") .data(function(d) { return d3.time.months(new Date(d, 0, 1), new Date(d + 1, 0, 1)); }) .enter() .append("path") .attr({ "class": "month", "d": monthPath, "fill": "none", "stroke": "#000", "stroke-width": "2px" }); //ツールチップ要素作成 var tooltip = d3.select("body") .append('div') .attr('class', 'arrow_box') .style("position", "absolute") .style("z-index", "10") .style("visibility", "hidden") .text("a simple tooltip"); //ステージをセンタリング var windowWidth = document.body.clientWidth; if(windowWidth>width) d3.select('#stage').style('margin-left', Math.floor((windowWidth-width)/2)+'px'); |
一番込み入っているのが、日付セルを月ごとにPathで区切る処理(月境界pathジェネレーター)なのですが、ここら辺はサンプルコードから丸写しです。
ツールチップの吹き出し表示は下記、CSS吹き出しジェネレーターで作成しました。
下方の吹き出しが見づらかったり、日付のセルにマウスオーバーするとdocumetnのサイズがおかしくなったり、一番最後の記事データにnullが入っていたりとバグだらけでまったく完成してないんですが、もくもく会の成果発表では素知らぬ顔して「できました」と報告してきました(^^;)
【余談】
今回はGithubを使ってカレンダーを作成しながら細かくコミットを区切り、あとからコミットログを確認した際に作成過程が分かるようにってのを狙ったのですが、途中pushするのを忘れたり、最後の方でコミットのメッセージを考えるのが面倒になりすべて「fix」で済ますなど残念な結果となりました。……もうちょっと、ちゃんと使えるようになりたい。