テニスのデータ
トッププロテニスの世界では、エースやダブルフォルトの本数、サーブ・リターンポイント率などスタッツと呼ばれる基本的な統計データが、スコアと合わせて試合結果として放映・表示されます。グランドスラムなど大きな大会になると、ホークアイ、foxtennなどボールの軌道や着地点を検出するシステムが常備され、詳細データが記録され、視聴者にわかりやすいような形でビジュアライズ(視覚化)されたデータが放映されます。ホークアイ、foxtennについては自サイトで恐縮ですが、テニスのデータ分析・記録で活用されているテクノロジーで概要を説明してます。
ショット傾向の視覚化
ホークアイ、foxtennで取得されたデータではありませんが、トッププロの詳細データが、Tennis Abstractというサイトで公開されております。今回は、その中のShot Directionというデータを視覚化してみることにトライしてみました。Shot Directionというのは選手がどのコースに打つことが多いかを示した指標となります。フォアハンドだとクロス、センター、ストレート(ダウンザライン)、どのコースに打つことが多いか。Tennis Abstractではこれらの数値が、表の中で並べられているのですが、数値だけ並んでいてもなかなかわかりずらいですよね。
棒グラフにして可視化するやり方もありますが、それも芸がないかなと。イラストを使ってわかりやすくデータを表現すべく、D3.jsを用いてショットコース傾向の視覚化をやってみました。こういうのインフォグラフィックと呼んでいいですかね?
D3.jsとは、JavaScriptのデータ可視化ライブラリのことです。(D3の公式サイト)
Webサイト上で綺麗なグラフや凝ったビジュアライズをやりたい場合に多く利用されるライブラリのようです。
デモ
D3.jsでテニスのショットデータ可視化コンテンツつくってます。各コース(クロス、センター、ストレート)に数値を入力すると、コース配分をボールの弾道線の太さで表現してます。逆クロスなども追加予定。出来上がったらサイトで公開します。#インフォグラフィックス #テニスデータの可視化 pic.twitter.com/GnC1aCIVtT
— おたこ (@otakoma) 2019年2月10日
ショットコースをオレンジ色で示し、ショットコースの太さでコース配分を表現しています。入力した数字でショットコースの太さが切り替わるようになっています。
コンテンツはこちらのサイトで公開してます。
Shot Direction【テニスのデータ視覚化コンテンツ】
コード
構成は、
ShotDirection.js
ShotDirection.html
の2つです。
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]+"%")
});
<!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>