Inside Frontend #1

データビジュアライゼーションの作り方


Created by Masayuki Shimizu / @_shimizu

自己紹介

清水正行

  • 群馬県高崎市在住
  • 2015年11月に転職
  • 日本経済新聞社 メディア戦略部
  • DataViz/GIS エンジニア

主なお仕事

日経:Visual Data

本日の目録
  • VDataコンテンツの作り方
  • D3.js
  • バッドノウハウ集
    • Z-indexが効かない問題
    • Pathのトランジション
    • 折り返さない問題
    • svgをpngに変換(つらい)

VDataコンテンツの作り方


制作フロー

  1. 企画発案(記者より)
  2. データ収集・分析・整形
  3. デザイン案やモックを作って打ち合わせ
  4. デザイン決定
  5. 実装
  6. 宣伝用の素材等作成
    (SNSに流す用のグラフィックスや動画など)
  • 作成チーム
    • PM1人
    • デザイナー3人
    • エンジニア2人
  • 2ライン(デザイナー1、エンジニア1)
  • 作成期間(2週間〜4週間)、速報(6時間)
  • 一点物が多い
これらの条件を踏まえ、日経ビジュアルデータではD3.jsというライブラリを採用しています。

D3.js


データビジュアライゼーションライブラリ
特徴
  • Data-Driven Document
  • デファクトスタンダード
  • チャートライブラリではない
  • 学習コストが高いと言われている(誤解?)

D3 Gallery

D3.jsを使っても公式サイトのGalleryに掲載されているような複雑なデータビジュアライゼーションが簡単に作れるわけではありません。

忘れましょう。

データビジュアライゼーションで必要になる作業
データを図表で表現する
Data -> Document
データの内容をエレメントの属性やスタイルに
反映する処理を繰り返す

面倒なこと


  • エレメントとデータのリレーション管理
  • トランジション
  • 値の正規化や座標計算
D3は、データビジュアライゼーション作成の上で面倒な処理を肩代わりしてくれる。
Data-Driven Document
var data = [{id:0, value:100}, {id:1, value:200}, {id:2, value:300}]

//選択した要素にデータを束縛する
const div = d3.selectAll("div").data(data, (d) => d.id)

//要素が足りない時は追加
const appendDiv = selector.enter().append("div")

//要素が多すぎる時は削除
const removeDiv = selector.exit().remove()

//アトリビュートの内容をアップデートする
div.merge(appendDiv).attr("width", d=> d.value )
	
//データを元にフィルタリング
div.filter((d) => d.value >= 100)

//データを元にエレメントを並べ替える
div.sort((a, b) => b.value - a.value )	

//トランジション
div.merge(appendDiv).transition().style("opacity", 0 )

複雑なデータになっても



市区町村境界データ

  • GeoJSON
  • 6MB
  • 1755 Features
  • 15795 Properties
操作は変わらない
    const projection = d3.geoMercator()
        .fitExtent([[0, 0], [1240, 800]], geojson)
    
    const desc = d3.geoPath().projection(projection)
            
    //path要素にデータを束縛する
    const path = svg.selectAll("path").data(geojson.features)
    
    //path要素が足りなければ追加する 
    const appendPath = path.enter().append("path")    

    //path要素が多すぎるなら削除する
    const removePath = path.exit().remove()
        
    //アトリビュートを更新して地図を描画する
    const maps =  path.merge(appendPath).attr("d", d => desc(d))
可視化
    //人口が2万人以上・以下で塗り分ける
    maps.attr("fill", d => (d.properties.pop > 20000) ? "green" : "blue"   )

    //群馬県に所属する市区町村だけ赤く塗る
    maps.filter(d => d.properties.pref === "群馬県").attr("fill", "red")

D3.jsはデータを基にエレメントを操作する機能と、トランジションや計算処理など最低限の機能のみを提供する。 自由度は高いが作り手側でやらなくてはならないことも多い。
また、ブラウザ間の差異を埋めるような機能はない。

BAD KNOWHOW

z-indexが効かない

重なり合う要素が多い場合

対応策


最初にレイヤーを作っておき、手前に出したいエレメントをコピーして表示する

<svg>
    <g class="axisLayer"></g>
    <g class="plotLayer">
        <circle class="orgin">   
    </g>
    <g class="overlay">
    </g>    
</svg>

<svg>
    <g class="axisLayer"></g>
    <g class="plotLayer">
        <circle class="orgin">   
    </g>
    <g class="overlay">
        <circle class="copy"> // <!-- 他のレイヤーより手前に表示される   
    </g>    
</svg>

copy tips


束縛されたデータもコピーしておく

var plotLayer = d3.select(".plotLayer")
var ovarlay = d3.select(".overlay") 
     
 plotLayer
	.selectAll("rect")
	.on("mouseover",  copy(overlay))

function copy(layer){    
	return function(){
		var node = d3.select(this).node()
		var nodeName = node.nodeName
		var nodeAttr = node.attributes
		var data = d3.select(this).data()
			
		layer.append(nodeName).call((selection) => {
			selection.data(data) // data bind
			Object.keys(nodeAttr).forEach((key) => {
			  selection.attr([nodeAttr[key].name], nodeAttr[key].value)
			})
		})
	}
}

ポイント


  • はじめにレイヤーの構成をしっかり決めておく
  • 要素を並べ替えるより、コピーしてしまう方が管理がしやすい
  • オリジナルの要素にバインドされているデータも一緒にコピーしておくと便利

pathトランジション

例えば地形を変形する場合

問題点

SAMPLE

対応策


頂点の数を揃える
function geo2square(coordinates, width, height) {
    var centroid = d3.polygonCentroid(coordinates)
        
    width = (width) ? width : 0 ;
    height = (height) ? height : 0 ;

    var p = []
    var i = 0
    var length = coordinates.length
    var qtr = ~~(length/4)

      
    var nScale = d3.scaleLinear().domain([0, qtr]).range([0, width])
    var sScale = d3.scaleLinear().domain([0, qtr]).range([width, 0])
    var wScale = d3.scaleLinear().domain([0, qtr]).range([0, height])
    var eScale = d3.scaleLinear().domain([0, qtr]).range([height, 0])
      
	while (i < length) {
        if (i <= qtr){
            p.push([ centroid[0]+nScale(i), centroid[1] ])
        }
        else if (i <= qtr*2){
            p.push([ centroid[0]+width, centroid[1] + wScale(i-qtr) ])
        }
        else if (i <= qtr*3){
            p.push([ centroid[0]+sScale(i-qtr*2), centroid[1]+height ])
        }
        else if (i <= qtr*4){
            p.push([ centroid[0], centroid[1]+eScale(i-qtr*3) ])
        }
        i++
    }
    
	return p

};

ポイント


  • 頂点数の多いpathに合わせて、調整する
  • 行政区ポリゴンが複数のパスで作られている場合は、面積を計算して一番大きなポリゴンを変形させる

折り返さない問題

問題点


SVGには折り返すという機能がないため、レスポンシブに対応するためにはリサイズのたびにすべての座標を再計算して更新する必要がある。
text要素も折り返しが効かないので、地味に困る。

対応策


tspanで括る
var textArray = ["1行目、あ","2行目", "3行目あああああ", "4行目あ"]
    
d3.select("svg").append("text")
	.datum(textArray)
 	.attr("transform", "translate(100, 100)")
 	.each(leftLinebreak)
    
function leftLinebreak(array){
	d3.select(this).selectAll("tspan")
		.data(array)
		.enter()
		.append("tspan")
		.attr("x", "0em")
		.attr("y", function(d,i){ return i + "em"})
		.text(function(d){ return d })
}
<text>
    <tspan x="0em" y="0em">1行目、あ</tspan>
    <tspan x="0em" y="1em">2行目</tspan>
    <tspan x="0em" y="2em">3行目あああああ</tspan>
    <tspan x="0em" y="3em">4行目あ</tspan>
</text>

SAMPLE


foreignObject


SVGの中にHTMLを埋め込める。折り返しも効く。
<svg>    
    <foreignObject width="200" height="200">
      <html>
        <div style="width:100px">ああああああああああああああああああああああああああああああああ</div> 
      </html>
    </foreignObject>
</svg>

※ IE11未対応

aout wrap


ポイント


  • SVG要素のレスポンシブ対応は割と大変
  • scale()やviewBoxで対応できる場合もある

SVGをPNGに変換する

SNS投稿用素材ダウンロードページ

 

SVGをラスタライズする方法


  1. SVG要素をdata URIスキームに変換してimageオブジェクトにする
  2. imageオブジェクトをcanvasにdrawImageする
  3. canvasから取得したデータをdata URI化する

問題点


  • ブラウザによって動かない(IE)
  • SVGに適用されているCSSが反映されない
  • 画像、webフォントが反映されない

対応策


  • ブラウザ縛り
  • 要素を辿ってスタイルを属性に変換する
  • 素材は全てdata URIスキームに変換してSVGに埋め込む

SAMPLE


ポイント


  • なんでもかんでもフロントでやろうとしない

まとめ


データの収集・整形なども含め、データビジュアライゼーションを作成するには地味で泥臭い作業がとても重要です。

資料