JavaScript
d3.js
データ視覚化

D3.jsでテニスのショットコース傾向を視覚化【インフォグラフィック】


テニスのデータ

トッププロテニスの世界では、エースやダブルフォルトの本数、サーブ・リターンポイント率などスタッツと呼ばれる基本的な統計データが、スコアと合わせて試合結果として放映・表示されます。グランドスラムなど大きな大会になると、ホークアイ、foxtennなどボールの軌道や着地点を検出するシステムが常備され、詳細データが記録され、視聴者にわかりやすいような形でビジュアライズ(視覚化)されたデータが放映されます。ホークアイ、foxtennについては自サイトで恐縮ですが、テニスのデータ分析・記録で活用されているテクノロジーで概要を説明してます。


ショット傾向の視覚化

ホークアイ、foxtennで取得されたデータではありませんが、トッププロの詳細データが、Tennis Abstractというサイトで公開されております。今回は、その中のShot Directionというデータを視覚化してみることにトライしてみました。Shot Directionというのは選手がどのコースに打つことが多いかを示した指標となります。フォアハンドだとクロス、センター、ストレート(ダウンザライン)、どのコースに打つことが多いか。Tennis Abstractではこれらの数値が、表の中で並べられているのですが、数値だけ並んでいてもなかなかわかりずらいですよね。

棒グラフにして可視化するやり方もありますが、それも芸がないかなと。イラストを使ってわかりやすくデータを表現すべく、D3.jsを用いてショットコース傾向の視覚化をやってみました。こういうのインフォグラフィックと呼んでいいですかね?

D3.jsとは、JavaScriptのデータ可視化ライブラリのことです。(D3の公式サイト)

Webサイト上で綺麗なグラフや凝ったビジュアライズをやりたい場合に多く利用されるライブラリのようです。


デモ

ショットコースをオレンジ色で示し、ショットコースの太さでコース配分を表現しています。入力した数字でショットコースの太さが切り替わるようになっています。

コンテンツはこちらのサイトで公開してます。

Shot Direction【テニスのデータ視覚化コンテンツ】


コード

構成は、

ShotDirection.js

ShotDirection.html

の2つです。


ShotDirection.js


var margin=20
var s=20;
var width = 400;
var height = margin+11.89*2*s;//600
var a1=parseInt(d3.select("#cross").attr("value"),10);
var a2=parseInt(d3.select("#center").attr("value"),10);
var a3=parseInt(d3.select("#straight").attr("value"),10);

var numDirection={"cross":a1,"center":a2,"straight":a3};

var totalDirection=numDirection["cross"]+numDirection["center"]+numDirection["straight"]
var perDirection=[Math.round(numDirection["cross"]/totalDirection*100),
Math.round(numDirection["center"]/totalDirection*100),
Math.round(numDirection["straight"]/totalDirection*100)]

var svg = d3.select("#tennisCourt").attr("width", width).attr("height", height);
var lineScale=0.5
var x1=10;
var y1=10;
svg.append("rect")
.attr("x",0)
.attr("y",0)
.attr("width",margin*2+10.97*s)
.attr("height",margin*2+11.89*2*s)
.attr("fill","#2E9AFE");

var dataset1 = [
[margin, margin],
[margin+10.97*s, margin],
[margin+10.97*s, margin+11.89*2*s],
[margin, margin+11.89*2*s],
[margin, margin]
];
var dataset2 = [
[margin, margin+11.89*s],
[margin+10.97*s, margin+11.89*s],
[margin+10.97*s-1.37*s, margin+11.89*s],
[margin+10.97*s-1.37*s, margin],
[margin+1.37*s, margin],
[margin+1.37*s, margin+11.89*2*s],
[margin+10.97*s-1.37*s, margin+11.89*2*s],
[margin+10.97*s-1.37*s, margin+11.89/2*s],
[margin+1.37*s, margin+11.89/2*s],
[margin+1.37*s, margin+11.89*3/2*s],
[margin+10.97*s-1.37*s, margin+11.89*3/2*s],
[margin+10.97/2*s, margin+11.89*3/2*s],
[margin+10.97/2*s, margin+11.89/2*s]
];
var datasetFoCr = [//cross
[margin+1.37*s+8.23*s*3/4, margin+11.89*2*7/8*s],
[margin+1.37*s+8.23*s/2, margin+11.89/2*s],
[margin+1.37*s+8.23*s*1/8, margin+11.89/8*s]
];
var datasetFoCe = [//center
[margin+1.37*s+8.23*s*3/4, margin+11.89*2*7/8*s],
[margin+1.37*s+8.23*s*3/4, margin+11.89/2*s],
[margin+1.37*s+8.23*s/2, margin+11.89/8*s]
];
var datasetFoDo = [//downtheline
[margin+1.37*s+8.23*s*3/4, margin+11.89*2*7/8*s],
[margin+1.37*s+8.23*s*7/8, margin+11.89/2*s],
[margin+1.37*s+8.23*s*7/8, margin+11.89/8*s]
];

//白線 折れ線としてSVGのpath要素を追加
svg.append("path")
.datum(dataset1)
.attr("fill", "#0080FF")
.attr("stroke", "white")
.attr("stroke-width", 1.5)
.attr("d", d3.line()
.x(function(d) { return d[0]; })
.y(function(d) { return d[1]; }));

svg.append("path")
.datum(dataset2)
.attr("fill", "none")
.attr("stroke", "white")
.attr("stroke-width", 1.5)
.attr("d", d3.line()
.x(function(d) { return d[0]; })
.y(function(d) { return d[1]; }));

//
svg.append("path")//cross
.datum(datasetFoCr)
.attr("fill", "none")
.attr("stroke", "#FF8C00")
.attr("stroke-width", lineScale*perDirection[0])
.attr("id","linecross")
.attr("d", d3.line()
.curve(d3.curveBasis)
.x(function(d) {return d[0];})
.y(function(d) {return d[1];}));
svg.append("text")
.attr("text-anchor", "middle")
.attr("x",margin+1.37*s+8.23*s/8)
.attr("y",margin-3)//.attr("y",margin+11.89/14*s)
.attr("fill","white")
.attr("id","percross")
.text(perDirection[0]+"%")
svg.append("text")
.attr("text-anchor", "middle")
.attr("x",margin+1.37*s+8.23*s/8)
.attr("y",margin+11.89/14*s)
.attr("font-size","11")
.attr("fill","white")
.attr("id","numcross")
.text("("+numDirection["cross"]+"/"+totalDirection+")")

svg.append("path")//center
.datum(datasetFoCe)
.attr("fill", "none")
.attr("stroke", "#FF8C00")
.attr("stroke-width", lineScale*perDirection[1])
.attr("id","linecenter")
.attr("d", d3.line()
.curve(d3.curveBasis)
.x(function(d) {return d[0];})
.y(function(d) {return d[1];}));
svg.append("text")
.attr("text-anchor", "middle")
.attr("x",margin+1.37*s+8.23*s/2)
.attr("y",margin-3)
.attr("fill","white")
.attr("id","percenter")
.text(perDirection[1]+"%")
svg.append("text")
.attr("text-anchor", "middle")
.attr("x",margin+1.37*s+8.23*s/2)
.attr("y",margin+11.89/14*s)
.attr("font-size","11")
.attr("fill","white")
.attr("id","numcenter")
.text("("+numDirection["center"]+"/"+totalDirection+")")

svg.append("path")//downtheline
.datum(datasetFoDo)
.attr("fill", "none")
.attr("stroke", "#FF8C00")
.attr("stroke-width", lineScale*perDirection[2])
.attr("id","linestraight")
.attr("d", d3.line()
.curve(d3.curveBasis)
.x(function(d) {return d[0];})
.y(function(d) {return d[1];}));
svg.append("text")
.attr("text-anchor", "middle")
.attr("x",margin+1.37*s+8.23*s*7/8)
.attr("y",margin-3)
.text(perDirection[2]+"%")
.attr("fill","white")
.attr("id","perstraight")
svg.append("text")
.attr("text-anchor", "middle")
.attr("x",margin+1.37*s+8.23*s*7/8)
.attr("y",margin+11.89/14*s)
.attr("font-size","11")
.attr("id","numstraight")
.attr("fill","white")
.text("("+numDirection["straight"]+"/"+totalDirection+")")

var inputElems = d3.selectAll("input");

inputElems.on("change", function(){

console.log(inputElems);

numDirection[this.name]=parseInt(this.value,10);
totalDirection=numDirection["cross"]+numDirection["center"]+numDirection["straight"]
perDirection=[Math.round(numDirection["cross"]/totalDirection*100),
Math.round(numDirection["center"]/totalDirection*100),
Math.round(numDirection["straight"]/totalDirection*100)]
svg.selectAll("#linecross")
.attr("stroke-width",lineScale*perDirection[0])
svg.selectAll("#linecenter")
.attr("stroke-width",lineScale*perDirection[1])
svg.selectAll("#linestraight")
.attr("stroke-width",lineScale*perDirection[2])

svg.selectAll("#numcross")
.text("("+numDirection["cross"]+"/"+totalDirection+")")
svg.selectAll("#numcenter")
.text("("+numDirection["center"]+"/"+totalDirection+")")
svg.selectAll("#numstraight")
.text("("+numDirection["straight"]+"/"+totalDirection+")")

svg.selectAll("#percross")
.text(perDirection[0]+"%")
svg.selectAll("#percenter")
.text(perDirection[1]+"%")
svg.selectAll("#perstraight")
.text(perDirection[2]+"%")
});



ShotDirection.html

<!DOCTYPE html>

<html>

<head>
<meta charset="utf-8">
<link rel="stylesheet" href="style.css">
<title>D3 Shot Direction Visualization</title>
<script src="https://d3js.org/d3.v5.min.js"></script>
</head>

<body>
<form name="inputDataText" id="inputDataText">
Title:<input type="text" id="title" name="title" value="Title"><br>
Player:<input type="text" id="player" name="player" value="Player"><br>
</form>

<form name="inputDataNum" id="inputDataNum">
Cross:<input type="text" id="cross" name="cross" value="10"><br>
Center:<input type="text" id="center" name="center" value="20"><br>
Straight:<input type="text" id="straight" name="straight" value="30"><br>
</form>

<p id="titleText"><b>Title</p>
<p id="playerText"><b>Player</p>
<svg id="tennisCourt"></svg>
<p><b>@DataTennis.Net</b></p>

<script src="ShotDirection.js"></script>
</body>

</html>