2012年4月3日火曜日

スタートアップ SVG

HTMLなページへのSVGの埋め込みテクニック

第1回で解説しましたが,SVGは画像フォーマットでもあり,InkScapeなどのグラフィカルなインターフェースを持ったアプリケーションで作成することができます。そのsvgファイルをページに読み込むにはobjectタグを使う方法が一般的です。しかし,この方法ではobjectタグはiframeのように親ページと子ページで独立したDOMを構築するため,SVGを動的に扱いたい場合には適しません。そういった場合,svgファイルをXMLHttpRequestで読み込む方法がオススメです。

まずはシンプルにresponseXMLを使う方法です。この方法はFirefox, Opera, Safari, Chromeなどで動作します。残念ながらIE9 Platform Preview 3(以降,IE 9 pp3)では動作しませんが,正式版がリリースされるまでに対応する可能性は高いと思われます。

XMLHttpRequestのresponseXMLの利用
var xhr = new XMLHttpRequest();
xhr.open('GET', 'blue-star.svg', true);
xhr.onload = function(){
var stage = document.getElementById('svg-sample-1');
var svgroot = xhr.responseXML.documentElement;
if (svgroot){
var svg = stage.appendChild(document.adoptNode(svgroot));
}
};
xhr.send();
responseXMLの代わりにDOMParserを利用する方法もあります。DOMParserが文字列をDOMに変換する処理を担います。XMLHttpRequest以外をソースにすることもできますし,変換前に文字列の状態で手を入れるといったことも可能です。ただし,IE 9 pp3ではDOMParserが定義されていないためこの方法は動作しません。DOMParserは元々Firefoxの独自拡張であり2010年7月時点で標準化はされていないため,将来的な対応には不安があります。

XMLHttpRequestとDOMParserの利用
var xhr = new XMLHttpRequest();
xhr.open('GET', 'blue-star.svg', true);
xhr.onload = function(){
var svgtext = xhr.responseText;
var svgdom = new DOMParser().parseFromString(svgtext,'application/xhtml+xml');
var stage = document.getElementById('svg-sample-2');
var svg = stage.appendChild(document.adoptNode(svgdom.documentElement));
};
xhr.send();
これ以外に,OperaとFirefoxがサポートしているdocument.load(DOM Level 3 Load and Saveより)や,XSLTProcessor(IEの場合MSXML2.XSLTemplate)などを使う方法もありますが,今回は省略します。

現時点では,次のようにIE 9以外はresponseXML,IE 9はresponseTextをinnerHTMLに入れる方法が有効です。なお,IE 9 pp3はXMLHttpRequestのonloadにも対応していないため,代わりにonreadystatechangeを使う必要があります。

XMLHttpRequestとinnerHTMLの利用
if (svgroot) {
svg = stage.appendChild(document.adoptNode(svgroot));
} else {
stage.innerHTML = xhr.responseText.replace(/^\s*<\?xml[\s\S]*?\?>/i, '').replace(/^\s*<\!DOCTYPE[\s\S]*?>/i,'');
svg = stage.querySelector('svg');
}

グラフの作成

この連載のまとめとして,SVGを使った折れ線グラフを描いてみます。データは気象庁 | 観測開始からの毎月の値より,東京の一日あたりの平均気温の月平均値の過去135年分を用います。

まずはシンプルにpolylineを使って描いてみます。

東京の4月の平均気温のデータ
var temperature = [
12.2,13.5,11.4,12.2,12.1,11.3,13.4,12.1,11.7,10.7,12.4,12.3,12.5,12.0,14.2,12.0,13.1,13.3,13.8,12.9,13.7,11.3,11.2,12.8,11.4,13.4,11.6,12.7,
13.2,10.9,12.9,12.4,12.3,13.6,12.3,13.8,13.0,13.7,11.8,11.6,12.7,12.7,11.7,13.4,12.6,13.8,13.3,12.0,14.7,11.9,11.1,13.5,12.5,12.6,14.0,12.4,
12.3,12.3,11.5,12.5,12.0,12.8,13.2,12.7,13.1,12.7,13.6,11.7,10.6,13.5,14.3,13.1,14.2,12.1,13.9,13.3,13.3,12.7,14.9,13.6,13.1,14.3,13.9,15.3,
12.8,14.9,14.1,13.9,15.3,11.1,13.6,14.5,14.1,14.6,13.0,13.5,13.4,15.3,14.8,14.3,13.0,15.1,13.9,13.9,13.6,13.9,14.0,15.9,11.6,14.2,13.9,14.4,
14.3,15.6,14.7,15.4,15.1,13.4,15.8,15.0,12.7,15.2,16.3,15.0,14.5,15.7,16.1,15.1,16.4,15.1,13.6,13.7,14.7,15.7,12.4
];
polylineによるグラフの描画
var svg = document.createElementNS(SVG,'svg');
svg.setAttribute('viewBox', '0 0 150 40');

var polyline = document.createElementNS(SVG,'polyline');
var line = temperature.map(function(v, i){return i+','+v;}).join(' ');
polyline.setAttribute('points', line);
polyline.setAttribute('fill', 'none');
polyline.setAttribute('stroke', '#0080FF');
svg.appendChild(polyline);
root.appendChild(svg);
ここでのポイントはviewBoxで1つの値を何ピクセル相当にするか調整している点です。グラフの表示エリアを幅450px,高さ120pxとして,viewBoxを150px,40pxにしてあるので,実際には3倍に拡大されて描画されることになります。

polylineによるグラフ
このままでは情報が少ないので補助線などを追加してみます。

補助線の追加
// 0度の線
var zero_line = document.createElementNS(SVG,'line');
zero_line.setAttribute('stroke-dasharray', 1);
zero_line.setAttribute('x1', '0');
zero_line.setAttribute('y1', '30');
zero_line.setAttribute('x2', '135');
zero_line.setAttribute('y2', '30');
zero_line.setAttribute("stroke", "#999");
zero_line.setAttribute("stroke-width", "0.3");
g.appendChild(zero_line);

// 15度の線
var half_line = document.createElementNS(SVG,'line');
half_line.setAttribute('stroke-dasharray', 1);
half_line.setAttribute('x1', '0');
half_line.setAttribute('y1', '15');
half_line.setAttribute('x2', '135');
half_line.setAttribute('y2', '15');
half_line.setAttribute("stroke", "#999");
half_line.setAttribute("stroke-width", "0.3");
g.appendChild(half_line);

var start = 1876;
var gvline = document.createElementNS(SVG,'g');
gvline.setAttribute("stroke", "#999");
gvline.setAttribute('stroke-dasharray', 1);
gvline.setAttribute("stroke-width", "0.3");
g.appendChild(gvline);
for (var i = 0, len = temperature.length;i < len;i++){
if ((i+start)%10 === 0){
// 10年置きに縦に線を引く
var vline = document.createElementNS(SVG,'line');
vline.setAttribute('x1', i);
vline.setAttribute('y1', 0);
vline.setAttribute('x2', i);
vline.setAttribute('y2', 40);
gvline.appendChild(vline);
}
}
// クリックしたときにズーム
var zoom = false;
root.onclick = function(e){
if (zoom){
root.style.position = 'static';
root.style.width = '450px';
root.style.height = '120px';
} else {
root.style.position = 'fixed';
root.style.top = '5%';
root.style.left = '5%';
root.style.width = document.documentElement.clientWidth*0.9 +'px';
root.style.height = document.documentElement.clientHeight*0.9+'px';
}
zoom = !zoom;
};
グラフの領域をクリックしたときにSVGの親要素のサイズを画面に合わせて拡大するようにしました。svg要素はデフォルトで親要素いっぱいに表示を広げるため,グラフも拡大されることになります。
さらに,ラベルを追加してみます。なお,最小フォントサイズの関係でviewBoxを大きめにするように変更を加えています。

ラベルの追加
// 0度のラベル
var zero_label = document.createElementNS(SVG,'text');
zero_label.setAttribute('x', -4*zoom);
zero_label.setAttribute('y', 31*zoom);
zero_label.textContent = 0;
zero_label.setAttribute("fill", "#333");
g.appendChild(zero_label);

// 15度のラベル
var half_label = document.createElementNS(SVG,'text');
half_label.setAttribute('x', -5*zoom);
half_label.setAttribute('y', 16*zoom);
half_label.textContent = 15;
half_label.setAttribute("fill", "#333");
g.appendChild(half_label);

var start = 1876;
var gyearlabel = document.createElementNS(SVG,'g');
gyearlabel.setAttribute("fill", "#333");
g.appendChild(gyearlabel);
for (var i = 0, len = temperature.length;i < len;i++){
if ((i+start)%10 === 0){
// 10年置きにラベルを書く
var yearlabel = document.createElementNS(SVG,'text');
yearlabel.setAttribute('x', (i-5)*zoom);
yearlabel.setAttribute('y', 34*zoom);
yearlabel.textContent = i+start;
gyearlabel.appendChild(yearlabel);
}
}
ラベル付きのグラフ

さらに,マウスの位置に合わせてその年をハイライトしつつ値を表示してみます。

マウスイベントの追加
var mouse_vline = document.createElementNS(SVG,'line');
mouse_vline.setAttribute('x1', 0);
mouse_vline.setAttribute('y1', 0);
mouse_vline.setAttribute('x2', 0);
mouse_vline.setAttribute('y2', 40*zoom);
mouse_vline.setAttribute("stroke", "#000");
mouse_vline.setAttribute("stroke-width", 1);
g.appendChild(mouse_vline);
var mouse_label = document.createElementNS(SVG,'text');
mouse_label.setAttribute('x', 0);
mouse_label.setAttribute('y', 3*zoom);
g.appendChild(mouse_label);
var isIE = !!window.ActiveXObject;
var left = root.getBoundingClientRect().left;
root.onmousemove = function(e){
var matrix = bg.getScreenCTM();
var x;
if (isIE){
x = e.clientX - matrix.e - left;
} else {
x = e.clientX - matrix.e;
}
var _x = Math.round(x / matrix.a / zoom);
if (_x >= 0 && _x < 135) {
mouse_vline.setAttribute('x1', _x*zoom);
mouse_vline.setAttribute('x2', _x*zoom);
mouse_label.setAttribute('x', (_x+1)*zoom);
mouse_label.textContent = temperature[_x];
}
};
ここで,やや複雑な処理が登場しました。ここでやろうとしていることは「マウス座標から現在の年を取得すること」なのですが,マウスの座標とSVG上の(viewBoxの影響などを受けた)座標があるため,単純に現在の年を取得することができません。

そこで,getScreenCTMを使ってマウス座標とSVG上での座標の違いを修正します。getScreenCTMで取得するマトリックスは,aとdが倍率,bとcが傾き,eとfが移動量を表します。

なお,IE 9 pp3ではマウス座標を親要素の位置で修正する必要があったため,IE 9 pp3用の処理を入れています。

マウスイベント付きのグラフ

最後にグラフに簡単なアニメーションを追加してみます。

アニメーションの追加
var timer;
var animation = function(){
polyline.points.clear();
var begin = +new Date();
var index = 0;
var p1 = svg.createSVGPoint();
p1.x = 0;
p1.y = 30*zoom;
var p2 = svg.createSVGPoint();
p2.x = 0;
p2.y = 30*zoom;
polyline.points.appendItem(p1);
polyline.points.appendItem(p2);
var anime = function(){
var now = +new Date();
var progress = Math.floor((now - begin) / 10);
if (progress >= temperature.length) {
clearInterval(timer);
timer = null;
}
for(var i = index;i < progress && temperature[i];i++){
var p1 = polyline.points.getItem(0);
p1.x = i*zoom;
var v = temperature[i];
var p = svg.createSVGPoint();
p.x = i*zoom;
p.y = (30-v)*zoom;
polyline.points.appendItem(p);
}
index = progress;
};
if (timer) {
clearInterval(timer);
}
timer = setInterval(anime, 1000/60);
};
アニメーション付きのグラフ

折角なので,上記とほぼ同じようなグラフを描くRaphael用のコードも用意しました。

Raphaelによるグラフの実装
var paper = Raphael(root, 450, 120);
var line = 'M22.5 90';
for(var i = 0;i < temperature.length;i++){
line += 'L'+(22.5+i*3)+' '+(30-temperature[i])*3;
}
line += 'L425.5 90'
var poly = paper.path(line);
poly.attr({
'gradient':'270-#ff0000-#ff00ff',
stroke:'none'
});

var lv = paper.set();
lv.push(
paper.text(10.5, 90, "0"),
paper.text(10.5, 45, "15")
);

var st = paper.set();
st.push(
paper.path("M22.5 90L426.5 90"),
paper.path("M22.5 45L426.5 45")
);
var start = 1876;
for (var i = 0, len = temperature.length;i < len;i++){
if ((i+start)%10 === 0){
st.push(paper.path('M' + (22.5+i*3) + ' 0 L'+(22.5+i*3)+' 90'));
lv.push(paper.text(22.5+i*3, 102, (i+start)));
}
}
lv.attr({fill:'#333'});
st.attr({'stroke-dasharray':'-',stroke:'#999'});
var v = paper.path("M22.5 0L22.5 90");
v.attr({stroke:'#0080FF'});

//st.mousemove(function (e) {
root.onmousemove = function(e){
if(!e) e = window.event;
var x, mouseX = e.clientX - root.getBoundingClientRect().left;
if(mouseX < 22.5){
x = 22.5;
} else if(mouseX > 426.5){
x = 425.5;
} else {
x = mouseX;
}
v.remove();
v = paper.path('M'+x+' 0L'+x+' 90');
v.attr({stroke:'#0080FF'});
};
//});
Raphaelによるグラフ
まとめ

SVGの基礎知識から具体的な書き方までざっくりと解説してみましたがいかがでしたでしょうか? SVGはスマートフォンや,PC向けでもRaphaelを用いることで,今から使える技術の1つです。是非この機会にSVGを役立てて頂ければ幸いです。短い間でしたが,ありがとうございました。

0 件のコメント:

コメントを投稿