著者:飯尾 淳
Twitterのトレンド分析に関する解説も、とうとう3回目に突入しました。今回は、共起ネットワークグラフの描画処理について説明します。描画にはD3.jsというグラフ描画フレームワークを使います。この描画処理では、データの受け渡し方法にちょっとした工夫をしており、それについても解説します。
シェルスクリプトマガジン Vol.68は以下のリンク先でご購入できます。
図2 トレンド表示画面を構成するビューのソースコード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
<h2> <%= link_to @trend.label, "https://twitter.com/search?q=#{@trend.label}", :target => '_blank' %> </h2> <p> <%= t('collected') %> <%= link_to l(@trend.collected, format: :long), "../#{@trend.collected}", :class => 'href' %> <%= link_to t('prev_item'), trend_path(@prev), :class => 'href' if @prev != nil %> <%= link_to t('next_item'), trend_path(@next), :class => 'href' if @next != nil %> </p> <div id="graph_canvas" data-src="<%= api_trend_path(@trend) %>"> </div> |
図5 サーバー側の処理をするRailsのコントローラのコード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
class Api::TrendsController < ApplicationController def index render json: Trend.where(collected: params[:date]) end def show l = [] @trend = Trend.find(params[:id]) l.push(@trend.nodes) @trend.nodes.each {|n| l.push(n.links) } render json: l end end |
図6 クライアント側の処理をするコード
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 |
$(document).on('turbolinks:load', function() { if ($('#graph_canvas').attr('data-src') != undefined) { $.ajax({ url: $('#graph_canvas').attr('data-src'), dataType: 'json', success: function(data) { drawGraph(data); }, error: function(data) { alert('error'); } }); } }); function drawGraph(data) { "use strict" var width, height, chartWidth, chartHeight, margin d3.select("#svg").remove() var svg = d3.select("#graph_canvas") .append("svg").attr("id", "svg") var chartLayer = svg.append("g").classed("chartLayer", true) setSize() drawChart(convertData(data)) function convertData(data) { var nodes = data.shift() var n_ary = nodes.map(function(d) { d['r'] = d.freq / 4 + 15; return d }) var l_hash = {} var ctr = 0 for (var n_links of data) { for (var link of n_links) { if (l_hash[link.id] == undefined) { l_hash[link.id] = { line_width: link.corr / 20, source: nodes[ctr] } } else { l_hash[link.id]['target'] = nodes[ctr] } } ctr++ } return { nodes: n_ary, links: Object.values(l_hash) } } function setSize() { width = document.querySelector("#graph_canvas").clientWidth height = document.querySelector("#graph_canvas").clientHeight margin = { top:0, left:0, bottom:0, right:0 } chartWidth = width - (margin.left+margin.right) chartHeight = height - (margin.top+margin.bottom) svg.attr("width", width).attr("height", height) chartLayer .attr("width", chartWidth) .attr("height", chartHeight) .attr("transform", "translate("+[margin.left, margin.top]+")") } function drawChart(data) { var STEM_LENGTH=30 var simulation = d3.forceSimulation() .force("link", d3.forceLink().id(function(d) { return d.index })) .force("collide", d3.forceCollide(function(d) { return d.r + STEM_LENGTH }) .iterations(16) ) .force("charge", d3.forceManyBody()) .force("center", d3.forceCenter(chartWidth / 2, chartHeight / 2)) .force("y", d3.forceY(0)) .force("x", d3.forceX(0)) var link = svg.append("g") .attr("class", "links") .selectAll("line") .data(data.links) .enter() .append("line") .attr("stroke", "brown") .attr("stroke-width", function(d) { return d.line_width }) var node_label = svg.append("g") .attr("class", "nodes") .selectAll("g") .data(data.nodes) .enter().append("g") .call(d3.drag() .on("start", dragstarted) .on("drag", dragged) .on("end", dragended)); var node = node_label.append("circle") .attr("r", function(d) { return d.r }) .attr("fill", function(d) { return (d.freq > 60.0) ? "moccasin" : (d.freq > 20.0) ? "lemonchiffon" : (d.freq > 5.0) ? "beige" : "lavender" }); var label = node_label.append("text") .attr("text-anchor", "middle") .attr("font-family", "Arial") .attr("dy", "0.5em") .attr("font-size", function(d) {return d.r / 1.5; }) .text(function(d) { return d.word; }) var ticked = 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; }); node_label.attr("transform", function(d) { return "translate("+d.x+","+d.y+")"; }) } simulation.nodes(data.nodes).on("tick", ticked); simulation.force("link").links(data.links); function dragstarted(d) { if (!d3.event.active) { simulation.alphaTarget(0.1).restart(); } d.fx = d.x; d.fy = d.y; } function dragged(d) { d.fx = d3.event.x; d.fy = d3.event.y; } function dragended(d) { if (!d3.event.active) simulation.alphaTarget(0); d.fx = null; d.fy = null; } } } |