

現在こちらのサイト様( <http://qiita.com/tag1216/items/b7f846af66db30b8c393> ),(
<http://bl.ocks.org/mbostock/4062045>
)を参考にしながらjson形式で読み込んだRSSをforceモデルで表示させようとしておりますが、上手くいかない状況です。

[code]

    var url = "https://ajax.googleapis.com/ajax/services/feed/load?v=1.0&q=http://feeds.feedburner.com/hatena/b/hotentry&num=-1";
    var proxy_url = "http://allow-any-origin.appspot.com/" + encodeURIComponent(url);
    
    
    var WIDTH = 800,
      HEIGHT = 800,
      TAG_IMAGE = 16,
      ITEM_IMAGE_SIZE = 8,
      APPEND_INTERVAL = 250;
    
    
    var tags = [];
    var items = [];
    var nodes = [];
    var links = [];
    
    var svg = d3.select("body").append("svg")
      .attr("width", WIDTH)
      .attr("height", HEIGHT);
    
    var force = d3.layout.force()
      .size([WIDTH, HEIGHT])
      .nodes(nodes)
      .links(links)
      .charge(0)
      .linkDistance(20)
      .linkStrength(1)
      .gravity(0.3)
      .start();
    
    var itemNode = svg.selectAll(".item-node");
    var tagNode = svg.selectAll(".tag-node");
    var link = svg.selectAll(".link")
    
    force.on("tick", function() {
      link.attr("x1", function(d) {
          return d.source.x;
        })
        .attr("y1", function(d) {
          return d.source.y;
        })
        .attr("x2", function(d) {
          return d.target.x;
        })
        .attr("y2", function(d) {
          return d.target.y;
        });
      itemNode.attr("transform", function(d) {
        return "translate(" + d.x + "," + d.y + ")";
      });
      tagNode.attr("transform", function(d) {
        return "translate(" + d.x + "," + d.y + ")";
      });
    });
    
    
    /*//データ読み込み*/
    d3.json(proxy_url, function(error, json) {
      if (error) {
        return console.warn(error);
      }
    
    　　　/*コンソールにタイトル、リンク、カテゴリを表示*/
      var feeds = json.responseData.feed;
    
      for (var i = 0; i < feeds.entries.length; i++) {
        title = feeds.entries[i].title;
        link = feeds.entries[i].link;
        tags = feeds.entries[i].categories;
        console.log(title);
        console.log(link);
        console.log(tags);
      };
    
    
      var index = 0;
      var timer = setInterval(function() {
        try {
          if (json.length <= index) {
            clearInterval(timer);
            showMessage("");
            return;
          }
          showMessage("データ追加中(" + (index + 1) + "/" + json.length + ")...");
          appendItem(json[index++])
        } catch (e) {
          showMessage(e);
          clearInterval(timer);
        }
      }, APPEND_INTERVAL);
    
    
    });
    
    /*//メッセージ*/
    function showMessage(text) {
      d3.select(".message").text(text);
    }
    
    
    
    
    /*//投稿をノードとして追加*/
    function appendItem(item) {
      items.push(item);
      nodes.push(item);
      item.tags.forEach(function(tag) {
        var name = "___" + tag.name;
        if (!(name in tags)) {
          tags[name] = tag;
          tags[name].count = 0;
          nodes.push(tags[name]);
        }
        tags[name].count++;
        links.push({
          source: nodes.indexOf(item),
          target: nodes.indexOf(tags[name])
        });
      });
      restart();
    }
    
    
    /*forceレイアウトの再描画*/
    
    function restart() {
      itemNode = itemNode.data(json);
      tagNode = tagNode.data(d3.values(tags));
    
      drawItemNode();
      drawTagNode();
      drawLink();
    
      force
        .nodes(nodes)
        .links(links)
        .charge(function(d) {
          //反発力を 投稿数の多いタグ > 投稿数の少ないタグ > 投稿ノード にする
          return d.count ? -(d.count * 200) : -100;
        });
    
      force.start();
    }
    
    
    /* 投稿ノードの描画*/
    function drawItemNode() {
      var g = itemNode.enter().append("g")
        .attr("class", "node item")
        .call(force.drag);
      var a = g.append("a")
        .attr("xlink:href", function(d) {
          return d.link;
        })
        .attr("target", "_blank");
      a.append("circle")
        .attr("r", ITEM_IMAGE_SIZE);
      a.append("image")
        .attr("xlink:href", function(d) {
          var imgCheck = entry.content.match(/<img.+?src="\/\/(.+?\/images\?q=.+?)\"/);
          if (igCheck) {
            var eimg = "http://" + imgCheck;
    
          } else {
            eimg = "http://design-ec.com/d/e_others_50/l_e_others_500.png";
          }
          return eimg
        })
    
      .attr({
        x: -ITEM_IMAGE_SIZE / 2,
        y: -ITEM_IMAGE_SIZE / 2,
        width: ITEM_IMAGE_SIZE,
        height: ITEM_IMAGE_SIZE
      });
      g.append("title")
        .text(function(d) {
          return d.title;
        });
    }
    
    
    
    /*タグノードの描画*/
    function drawTagNode() {
      var g = tagNode.enter().insert("g", ".item")
        .attr("class", "node tag")
        .call(force.drag);
      var a = g.append("a")
        .attr("xlink:href", function(d) {
          return "b.hatena.ne.jp/hotentry/" + d.categories;
        })
        .attr("target", "_blank");
      a.append("image")
        .attr("xlink:href", function(d) {
          return d.icon_url == "/icons/medium/missing.png" ? "http://qiita.com/icons/medium/missing.png" : d.icon_url;
        });
      a.append("rect");
      a.append("text")
        .attr("class", "shadow");
      a.append("text")
        .attr("class", "text");
    
      tagNode.selectAll("text")
        .attr("text-anchor", "middle")
        .text(function(d) {
          return d.name;
        });
    
    
    
      tagNode.each(function(d) {
        var size = ~~(TAG_IMAGE_SIZE * Math.sqrt(d.count));
        d3.select(this).select("image")
          .attr({
            x: -size / 2,
            y: -size / 2,
            width: size,
            height: size
          });
        d3.select(this).select("rect")
          .attr({
            x: -size / 2,
            y: -size / 2,
            width: size,
            height: size
          });
        d3.select(this).selectAll("text")
          .attr("dy", size / 2 + 10);
      });
    
    
      /*    リンクの描写*/
      function drawLink() {
        link = link.data(links);
        link.enter().insert("line", ".tag")
          .attr("class", "link");
      }
    };
[/code]

[code]

    <!DOCTYPE html>
    <meta charset="utf-8">
    <title>はてなブックマーク新着記事を表示</title>
    <style>
      svg {
        border: solid 1px;
      }
      .item image {
        opacity: .7;
      }
      .item circle {
        fill: #fff;
        fill-opacity: .7;
        stroke-width: .5px;
        stroke: #60c90d;
        stroke-opacity: .7;
      }
      .tag image {
        opacity: .9;
      }
      .tag rect {
        fill-opacity: 0;
        stroke-width: 1px;
        stroke: #0f6faf;
        stroke-opacity: .9;
      }
      .tag text {
        pointer-events: none;
        font-size: 8px;
      }
      .tag .shadow {
        stroke: #fff;
        stroke-width: 3px;
        stroke-opacity: .7;
      }
      .link {
        stroke: #aaa;
        stroke-width: 2px;
        opacity: .3;
      }
    </style>
    
    <html>
    <!--  <head>
    
      <script src="http://d3js.org/d3.v3.min.js"></script>
      <script type="text/javascript">
      var url = "https://ajax.googleapis.com/ajax/services/feed/load?v=1.0&q=http://feeds.feedburner.com/hatena/b/hotentry&num=-1";
      var proxy_url = "http://allow-any-origin.appspot.com/" + encodeURIComponent(url);
    
    
    d3.json(proxy_url, function(error, json) {
        if (error) {
          return console.warn(error);
        }
    
    
         var feeds = json.responseData.feed;
    
           for(var i =0;i < feeds.entries.length;i++){
        title = feeds.entries[i].title;
        link = feeds.entries[i].link;
        tags = feeds.entries[i].categories;
        console.log(title);
        console.log(link);
        console.log(tags);
        };
    
      });
      </script> -->
    </head>
    
    <body>
      <script　src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
        <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
    
        <div class="message"></div>
        <script src="hatena.js"></script>
    </body>
    
    </html>
[/code]

上記のスニペットに書いているjsファイルを"hatena.js"として保存し、ｈｔｍｌを実行した場合  
TypeError: Cannot read property 'tags' of undefined  
というエラーが表示されます。  
参照しているRSS( <http://feeds.feedburner.com/hatena/b/hotentry/>
)上にはtagという情報がないため代替としてsubjectというもので代用しようと思ったのですが指定の仕方がわかりませんでした。  
イメージとしては記事のサムネイルが上記サイト様の投稿ノードの部分に入り、タグノードにカテゴリーを表示させるプログラムが作りたいと考えています。  
コンソールには記事のタイトル、リンク、カテゴリーが表示されるため正しくRSSは読み込めていると思います。  
スニペット上でhtmlからjsファイルを開く方法が分からなく、不恰好な形での質問になってしまい申し訳ありません。  
お力添えの方よろしくお願い致します。


CC BY-SA 3.0
2015-11-21T15:40:57.817
19139
2016-10-25T20:43:41.170
13172
question
javascript html
APIを用いてjson形式にしたRSSをd3.jsに読み込みforceモデルを出力したい


回答というよりは、感想のような物になります。

### "tags"について

'tags'に関連したエラーが出ているという事ですが、行番号は大事です。  
エラーが出た行から辿っていくといろいろ分かる事が多いです。

Qiitaの`tags`の代りに、RSSの`dc:subject`を使うとの事ですが、これはGoogleのAPIでjsonに変換された時点で、`feeds.entries[i].categories`になっていますし、質問者さんもこれをログに出す事には成功しているようですね。このやり方でよさそうです。  
例えば、`appendItem`関数では、`item.tags`を使っていますが、これを`item.categories`にしていく感じでしょうか。

ところで参考になさっているコードでは、

> 同時に使用されていたタグ同士をリンクとして表している

との事ですが、はてなのRSSのdc:subject (jsonになった時の`feeds.entries[i].categories`) の要素は一つです。  
はてなブックマークのエントリーは、ただ一つのカテゴリに所属するようです。  
これは解決可能なのでしょうか。

### コードをざっと見て気になった点

  * ささいな事 
    * GoogleのAPIは[JSONPをサポートしています](https://developers.google.com/feed/v1/jsondevguide#using_callback)  
オリジンの問題を回避する為に”<http://allow-any-
origin.appspot.com/>"を使う必要は無いです。まあJSONPは面倒ではありますよね。

    * 63行目あたりに、全角スペースがある  
JavaScriptでは許されているので、何も問題ないのですが、コピペなどで他に移した時にトラブルの元になります。よいエディタを使って、インデントなどは自動でやってもらうのがよいです。

  * 参考コードと大分違う 
    * linkの宣言箇所でコードがごっそり抜けている  
`var link = svg.selectAll('.link')`で終了していますが、参考コードではメソッドチェーンが続いています。

    * 上記あたりから辿っていくと、足りない物がいろいろ見つかりそう  
ここらへんで、はてなブックマークののカテゴリが一つな事が気になったので、これ以上見ていないです。

### アルゴリズムでなく、入力データの構造を変更するという手もある

Qiitaのデータを処理するプログラムを変更して、はてなのデータを読ませようとしている分けですが、一番早いのは、はてなのデータをQiita互換の構造に変換してしまう事だと思います。  
将来、はてな以外のデータを読ませる場合にも対応しやすい考え方です。  
もちろん、参考コードはあくまで叩き台である、というような場合はこれでは駄目ですし、目的次第ではありますが。


CC BY-SA 3.0
2015-11-21T20:38:15.417
19147
2015-11-21T20:38:15.417
3054
19139
answer


まだ仕様が決っていない感じですね。  
何が出来るのか分からない段階では仕方無いかもしれません。

<http://b.hatena.ne.jp/hotentry> の利用も検討されているようですね。  
これはHTMLのページですから、いわゆる「スクレイピング」をする事になります。  
こういった事はサーバサイドでやる事が多いですが、今は`DOMParser`もあるので、ブラウザ側でやるのも簡単です。  
ただ、APIやRSSなどより仕様(というかHTMLタグの構造)の変更が頻繁でしょうから、そこは面倒かもしれません。

下記スニペットはスクレイピングの例です。  
ライブラリを使用していないので定型的な処理がいろいろありますが、そんなに多くの事はしていません。  
結果はとりあえず、「名前の配列の配列」にしています。  
記事タイトルもタグも、「名前」としてフラットに並べています。  
まずは出来るだけ単純な構造にして、それを表示する、という所から出発するのがよいと思います。  
(質問者さんが使っている、<http://allow-any-origin.appspot.com/>
を使用していますが、開発中や本番で使用してよい物かは知りません)

[code]

    "use strict";
    function for_each(array, func){ Array.prototype.forEach.call(array, func); }
    
    function get_html(callback){
     var hatena_hotentry_url = "http://b.hatena.ne.jp/hotentry";
     hatena_hotentry_url = "http://allow-any-origin.appspot.com/"
      + encodeURIComponent(hatena_hotentry_url);
     
     // var hatena_hotentry_url = "./hatena_hotentry.html"
    
     var req = new XMLHttpRequest();
     req.open("GET", hatena_hotentry_url);
     req.onreadystatechange = function() {
      if (req.readyState === 4 && req.status === 200) {
       callback(req.responseText);
       req = undefined;
      }
     };
     req.send(null);
    }
    
    function html_to_relation(html){
     var doc = new DOMParser().parseFromString(html, "text/html");
     var error_tags = doc.getElementsByTagName("parsererror");
     if(error_tags.length){
      console.log("error: ", error_tags);
      return [["Parse error", "Maybe HTML is broken"]];
     }
    
     var relation = []; // [["name", ...], ...]
     var entry_elms = doc.querySelectorAll(
       "[data-track-section='default'] .entry-contents");
     for_each(entry_elms, function(entry_elm){
      var entry_link_elm = entry_elm.querySelector(".entry-link");
      if(!entry_link_elm){ return; }
    
      var name_array = [];
      relation.push(name_array);
      name_array.push(entry_link_elm.title);
    
      var entry_meta_elm = entry_elm.nextElementSibling;
      if(!entry_meta_elm){ return; }
      for_each(entry_meta_elm.querySelectorAll("a.tag"), function(tag_elm){
       name_array.push(tag_elm.textContent);
      });
     });
    
     return relation;
    }
    
    function relation_view(relation){
     document.getElementById("view").textContent
             = JSON.stringify(relation, undefined, 2);
    }
    
    get_html(function(html){ relation_view(html_to_relation(html)); });
[/code]

[code]

    <div>
     <pre id="view">表示エリア</pre>
    </div>
[/code]


CC BY-SA 3.0
2015-11-22T09:09:35.190
19166
2015-11-22T09:09:35.190
3054
19139
answer