Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

テニスのデータ

トッププロテニスの世界では、エースやダブルフォルトの本数、サーブ・リターンポイント率などスタッツと呼ばれる基本的な統計データが、スコアと合わせて試合結果として放映・表示されます。グランドスラムなど大きな大会になると、ホークアイ、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>
otakoma
テニスとプログラミングが好きで、スコア記録アプリ(Androidアプリ)やデータ分析サイトをつくってます。他にはディープラーニングや画像解析を用いたテニスの試合やフォームの解析など。製造業で働いておりROSにも興味あり。本職はメカ設計エンジニアですが、ソフトウエアエンジニアになりたい。 https://github.com/taikoma
http://datatennis.net/about/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした