Published
Edited
May 4, 2020
2 stars
Insert cell
md`# D3チャート練習`
Insert cell
md`
- 描画データの大体の構造
1. 全体のサイズと描画エリアのサイズ決め
- 物理的なsvgのエリア(svg自体とviewBoxのサイズ)を決めて、そこからの実際の描画エリアへのmarginオブジェクトを作っておくパターンがやりやすそう
- margin = ({ top: 30, right: 0, bottom: 30, left: 40 })
- プロットの大体の流れ
1. 論理的なデータを用意する(jsのオブジェクトレベル)
- 論理データ => svg内の位置へのマッピングオブジェクト(関数)を作る
- svgに描画する
`
Insert cell
md` ## 共通のオブジェクト準備`
Insert cell
d3 = require("d3@5")
Insert cell
margin = ({ top: 30, right: 0, bottom: 30, left: 40 })
Insert cell
height = 500
Insert cell
width = 770
Insert cell
md` # 棒グラフ`
Insert cell
md`
https://observablehq.com/@d3/d3-scaleband
これがわかりやすそう
`
Insert cell
md` ### 1. 論理データ`
Insert cell
data = [
{ x_value: 'A', y_value: 10 },
{ x_value: 'B', y_value: 2 },
{ x_value: 'C', y_value: 5 },
{ x_value: 'D', y_value: 6 },
{ x_value: 'E', y_value: 3 },
{ x_value: 'F', y_value: 9 },
{ x_value: 'G', y_value: 7 },
{ x_value: 'H', y_value: 7 },
{ x_value: 'I', y_value: 1 }
]
Insert cell
md`用意したデータをd3のデータ操作系APIとかでグラフで見せたい順番に並び変えたり加工してグラフ用論理データを準備する。
例えば、 y_valueの降順にしてみる`
Insert cell
data.sort((a, b) => d3.descending(a.y_value, b.y_value))
Insert cell
md` ### 2. x軸の「論理 -> 物理」変換関数作成`
Insert cell
md`
棒グラフのx軸みたいなカテゴリ的なもののような離散的な軸を作るときは d3.scaleBandを使う。そこからメソッドチェーンで、x軸の論理データの範囲(domain)と、その範囲を描画する物理的な位置情報(range)、棒グラフ同士の幅(padding)などを設定していく。

`
Insert cell
md`
domainは、棒グラフのx軸のようなカテゴリみたいなものの場合、そのカテゴリの個数分の0〜長さまでの配列を渡す。
x軸が例えば ["いちご", "りんご", "みかん"]なら domain には [0, 1, 2] を渡す。
`
Insert cell
md`
rangeにはx軸の物理的な描画位置を [左端, 右端] として渡す。画面いっぱいなら 0〜widthだが、↑で書いたようにマージンを設定するので、それを左右に対して考慮する`
Insert cell
x = d3
.scaleBand()
.domain(d3.range(data.length))
.range([margin.left, width - margin.right])
.padding(0.1)
Insert cell
md`
y軸も同様に設定する。x軸のような離散的な値でなく普通の実数みたいなのは scaleLinear で設定するらしい。
y軸の物理的な位置は上端がy=0で下端がy=heightになるため、domain(論理範囲)に対するrange(物理的な描画位置)は逆になるようにマッピングする(一番小さい論理データが一番大きいyをとる)。
`
Insert cell
y = d3
.scaleLinear()
.domain([0, d3.max(data, d => d.y_value)])
.range([height - margin.bottom, margin.top])
Insert cell
md` ### 3. svg書いて描画
これで棒グラフのグラフ部分の描画設定は終わりなので、svgで描画していく
`
Insert cell
char = {
const svg = d3.create("svg").attr("viewBox", [0, 0, width, height]);

svg
.append("g")
.attr("fill", "steelblue")
.selectAll("rect")
.data(data)
.join("rect")
.attr("x", (d, i) => x(i))
.attr("y", d => y(d.y_value))
.attr("height", d => y(0) - y(d.y_value))
.attr("width", x.bandwidth());

return svg.node();
}
Insert cell
md`### 軸も書く
これでデータのプロットはできたので、次に軸も描画する`
Insert cell
md`
d3.axisBottom() でtick(軸を刻む目盛り)が下向きに出る(bottom)ような軸が書ける.(最初勘違いしていたが、svgの下部に出すから axis"Bottom"ではないので注意。 軸の位置自体は後述する別の仕組みで設定する)
そこに x, y変換器(↑でscaleXxxx で作ったもの)を渡すとその間隔でtickが作られる。
`
Insert cell
xAxis = g => g.call(d3.axisBottom().scale(x))
Insert cell
chart2 = {
const svg = d3.create("svg").attr("viewBox", [0, 0, width, height]);

svg
.append("g")
.attr("fill", "steelblue")
.selectAll("rect")
.data(data)
.join("rect")
.attr("x", (d, i) => x(i))
.attr("y", d => y(d.y_value))
.attr("height", d => y(0) - y(d.y_value))
.attr("width", x.bandwidth());

svg.append("g").call(xAxis);

return svg.node();
}
Insert cell
md`
x軸は描画できたが、グラフの上部に描画されてしまったため、これをグラフの下部にもってくる。
これはtransform属性でtranslateを指定して下に移動させる。
x座標は変更なし(0)で、y座標を+方向(下向き)にbotomのマージン部分(height - margin.bottm)まで移動させる
`
Insert cell
chart3 = {
const svg = d3.create("svg").attr("viewBox", [0, 0, width, height]);

svg
.append("g")
.attr("fill", "steelblue")
.selectAll("rect")
.data(data)
.join("rect")
.attr("x", (d, i) => x(i))
.attr("y", d => y(d.y_value))
.attr("height", d => y(0) - y(d.y_value))
.attr("width", x.bandwidth());

svg
.append("g")
// これで下に移動できる
.attr("transform", `translate(0, ${height - margin.bottom})`)
.call(xAxis);

return svg.node();
}
Insert cell
md`
x軸の一番左の開始位置にもメモリが打たれている(下のスクリーンショット参照)が、これをなくすには
tickSizeOuter(0)
を軸に追加する
`
Insert cell
FileAttachment("スクリーンショット 2020-05-03 16.19.31.png").image()
Insert cell
xAxis2 = g =>
g.call(
d3
.axisBottom()
.scale(x)
.tickSizeOuter(0)
)
Insert cell
chart4 = {
const svg = d3.create("svg").attr("viewBox", [0, 0, width, height]);

svg
.append("g")
.attr("fill", "steelblue")
.selectAll("rect")
.data(data)
.join("rect")
.attr("x", (d, i) => x(i))
.attr("y", d => y(d.y_value))
.attr("height", d => y(0) - y(d.y_value))
.attr("width", x.bandwidth());

svg
.append("g")
.attr("transform", `translate(0, ${height - margin.bottom})`)
// あたらしい軸で描画
.call(xAxis2);

return svg.node();
}
Insert cell
md`
x軸のメモリのラベルは本来は数値でなく、その列に対応したアルファベットなので、 tickFormat でラベル設定する。
`
Insert cell
xAxis3 = g =>
g.call(
d3
.axisBottom()
.scale(x)
.tickFormat(i => data[i].x_value) // これでindexからラベルに変換
.tickSizeOuter(0)
)
Insert cell
chart5 = {
const svg = d3.create("svg").attr("viewBox", [0, 0, width, height]);

svg
.append("g")
.attr("fill", "steelblue")
.selectAll("rect")
.data(data)
.join("rect")
.attr("x", (d, i) => x(i))
.attr("y", d => y(d.y_value))
.attr("height", d => y(0) - y(d.y_value))
.attr("width", x.bandwidth());

svg
.append("g")
.attr("transform", `translate(0, ${height - margin.bottom})`)
// あたらしい軸で描画
.call(xAxis3);

return svg.node();
}
Insert cell
md` 次にY軸のラベル設定
`

Insert cell
yAxis = g => g.call(d3.axisLeft(y).ticks(null))
Insert cell
chart6 = {
const svg = d3.create("svg").attr("viewBox", [0, 0, width, height]);

svg
.append("g")
.attr("fill", "steelblue")
.selectAll("rect")
.data(data)
.join("rect")
.attr("x", (d, i) => x(i))
.attr("y", d => y(d.y_value))
.attr("height", d => y(0) - y(d.y_value))
.attr("width", x.bandwidth());

svg
.append("g")
.attr("transform", `translate(0, ${height - margin.bottom})`)
.call(xAxis3);

svg.append("g").call(yAxis);
return svg.node();
}
Insert cell
md`
d3.axisLeftだと左に目盛りが表示されるので、切れてしまっている。x軸と同じように transformでmargin.left分右に移動させて調節。
`
Insert cell
chart7 = {
const svg = d3.create("svg").attr("viewBox", [0, 0, width, height]);

svg
.append("g")
.attr("fill", "steelblue")
.selectAll("rect")
.data(data)
.join("rect")
.attr("x", (d, i) => x(i))
.attr("y", d => y(d.y_value))
.attr("height", d => y(0) - y(d.y_value))
.attr("width", x.bandwidth());

svg
.append("g")
.attr("transform", `translate(0, ${height - margin.bottom})`)
.call(xAxis3);

svg
.append("g")
.attr("transform", `translate(${margin.left}, 0)`)
.call(yAxis);
return svg.node();
}
Insert cell
md` これでだいたい完成`
Insert cell
md` ### その他
グリッド線もつけたいとき
`
Insert cell
md`tick の目盛りの長さを調整する、という方向で設定する
y軸の罫線(横向きの線)を入れてみる
`
Insert cell
yAxis2 = g =>
g.call(
d3
.axisLeft(y)
.ticks(null)
// 横方向にwidthの長さ分の目盛りを入れる。
// x軸方向にwidth分伸ばしたいので、widthでいいのかとおもったらなぜか -width だった。なんで??
.tickSize(-width)
)
Insert cell
chart8 = {
const svg = d3.create("svg").attr("viewBox", [0, 0, width, height]);

svg
.append("g")
.attr("fill", "steelblue")
.selectAll("rect")
.data(data)
.join("rect")
.attr("x", (d, i) => x(i))
.attr("y", d => y(d.y_value))
.attr("height", d => y(0) - y(d.y_value))
.attr("width", x.bandwidth());

svg
.append("g")
.attr("transform", `translate(0, ${height - margin.bottom})`)
.call(xAxis3);

svg
.append("g")
.attr("transform", `translate(${margin.left}, 0)`)
// グリッドつきの軸を入れる
.call(yAxis2);

return svg.node();
}
Insert cell
md`
罫線の色が濃すぎると見ずらいので、cssで調整する
tickのsvg要素は <line>タグなのでそれに対して設定
`
Insert cell
chart9 = {
const svg = d3.create("svg").attr("viewBox", [0, 0, width, height]);

svg
.append("g")
.attr("fill", "steelblue")
.selectAll("rect")
.data(data)
.join("rect")
.attr("x", (d, i) => x(i))
.attr("y", d => y(d.y_value))
.attr("height", d => y(0) - y(d.y_value))
.attr("width", x.bandwidth());

svg
.append("g")
.attr("transform", `translate(0, ${height - margin.bottom})`)
.call(xAxis3);

svg
.append("g")
.attr("class", "y_axis")
.attr("transform", `translate(${margin.left}, 0)`)
.call(yAxis2);

// y軸の罫線をグレーにしてちょっと透過に
// cssファイル側で設定するほうがいいかも。
svg.selectAll('g.y_axis line').style("stroke", "lightgray");
svg.selectAll('g.y_axis line').style("stroke-opacity", 0.7);

return svg.node();
}
Insert cell
md`----------------------------------------------------------------`
Insert cell
md`# 折れ線グラフ`
Insert cell
md`svgのサイズとマージンは棒グラフのまま使う`
Insert cell
md`データ`
Insert cell
FileAttachment("testdata.csv").text()
Insert cell
data2 = d3.csvParse(await FileAttachment("testdata.csv").text(), d3.autoType)
Insert cell
md`### 論理データ -> 物理座標への変換オブジェクト
折れ線と同じく d3.scaleXxxx を用いて x, yそれぞれの変換用オブジェクトを作成する`
Insert cell
md`### 折線用のd3オブジェクト
d3.lineで 任意の折れ線を書ける
`
Insert cell
md`x軸用のオブジェクト作成
x軸は日付けなので、domainに最小日付けと最大日付けを設定して、 rangeは棒グラフと同様`
Insert cell
d3
.scaleUtc()
.domain([d3.min(data2, d => d.date), d3.max(data2, d => d.date)])
.range([margin.left, width - margin.right])
Insert cell
md`
なお、domainの min, maxは d3.extent(オブジェクト, 対象の値) で一気にもとめられる
object(キーは date, value) の配列から dateのmin, maxを取得したい場合は下記
`
Insert cell
d3.extent(data2, d => d.date)
Insert cell
md`これでx軸用の変換オブジェクト作成`
Insert cell
linechart_x = d3
.scaleUtc()
.domain(d3.extent(data2, d => d.date))
.range([margin.left, width - margin.right])
Insert cell
md`y軸も棒グラフと同じで実数値なので scaleLinearにして、 y座標の上側が0、下が+方向ということで同様に逆向にまっイングして下記のように定義`
Insert cell
linechart_y = d3
.scaleLinear()
.domain([0, d3.max(data2, d => d.value)])
.range([height - margin.bottom, margin.top])
Insert cell
height - margin.bottom
Insert cell
md`### 折れ線グラフ用データ生成

折れ線グラフの線は、 svg上では path タグで表される要素で表現されるので、そのデータを作成する
pathタグについては下記参照
https://developer.mozilla.org/ja/docs/Web/SVG/Tutorial/Paths

プレフィックス 座標, (座標)

の形式で、データポイントを定義していきそれらをつないだ線分が折れ線として表現される

- M x座標 y座標 ・・・ (x, y) の位置に移動(線を引くわけではなく、その位置に行く。開始点の定義に使われる)
- L x座標 y座標 ... (x, y) の位置まで、現在位置から線を引く

こんな感じで線を定義される。
これをラップした d3.line()で実際には定義する.

このときのx, yはそれぞれ、論理データを引数に、↑でつくった物理座標への変換オブジェクトを経由して設定する`
Insert cell
line = d3
.line()
.x(d => linechart_x(d.date))
.y(d => linechart_y(d.value))
Insert cell
md`ここではまだ実データの data2 オブジェクオtを設定していないが、こういう書き方が d3 的な宣言的な書き方で、この後、実際にsvgを描画時に datum メソッドでデータをバインドしたときに各要素が .x, .y メソッドの d パラメータとして渡される`
Insert cell
md` これで折れ線グラフを描画`
Insert cell
linechart_1 = {
const svg = d3.create("svg").attr("viewBox", [0, 0, width, height]);

svg
.append("path")
.datum(data2)
.attr("fill", "none")
.attr("stroke", "steelblue")
.attr("stroke-width", 1.5)
.attr("stroke-linejoin", "round")
.attr("stroke-linecap", "round")
.attr("d", line);

return svg.node();
}
Insert cell
md` ### ここから軸を描画していく
が、棒グラフとほぼ一緒`
Insert cell
linechart_xAxis1 = g => g.call(d3.axisBottom(linechart_x).tickSizeOuter(0))
Insert cell
linechart_2 = {
const svg = d3.create("svg").attr("viewBox", [0, 0, width, height]);

svg
.append("path")
.datum(data2)
.attr("fill", "none")
.attr("stroke", "steelblue")
.attr("stroke-width", 1.5)
.attr("stroke-linejoin", "round")
.attr("stroke-linecap", "round")
.attr("d", line);

svg
.append("g")
.attr("transform", `translate(0,${height - margin.bottom})`)
.call(linechart_xAxis1);

return svg.node();
}
Insert cell
md` y軸も描画`
Insert cell
linechart_yAxis1 = g => g.call(d3.axisLeft(linechart_y))
Insert cell
linechart_3 = {
const svg = d3.create("svg").attr("viewBox", [0, 0, width, height]);

svg
.append("path")
.datum(data2)
.attr("fill", "none")
.attr("stroke", "steelblue")
.attr("stroke-width", 1.5)
.attr("stroke-linejoin", "round")
.attr("stroke-linecap", "round")
.attr("d", line);

svg
.append("g")
.attr("transform", `translate(0,${height - margin.bottom})`)
.call(linechart_xAxis1);

svg
.append("g")
.attr("transform", `translate(${margin.left},0)`)
.call(linechart_yAxis1);

return svg.node();
}
Insert cell
md`こちらも棒グラフと同様罫線入れてみる`
Insert cell
linechart_yAxis2 = g => g.call(d3.axisLeft(linechart_y).tickSize(-width))
Insert cell
linechart_4 = {
const svg = d3.create("svg").attr("viewBox", [0, 0, width, height]);

svg
.append("path")
.datum(data2)
.attr("fill", "none")
.attr("stroke", "steelblue")
.attr("stroke-width", 1.5)
.attr("stroke-linejoin", "round")
.attr("stroke-linecap", "round")
.attr("d", line);

svg
.append("g")
.attr("transform", `translate(0,${height - margin.bottom})`)
.call(linechart_xAxis1);

svg
.append("g")
.attr('class', 'linechart_y_axis') // 罫線のcss設定用にclass設定
.attr("transform", `translate(${margin.left},0)`)
// 罫線付きのy軸を描画する
.call(linechart_yAxis2);

// y軸の罫線をグレーにしてちょっと透過に
// cssファイル側で設定するほうがいいかも。
svg.selectAll('g.linechart_y_axis line').style("stroke", "lightgray");
svg.selectAll('g.linechart_y_axis line').style("stroke-opacity", 0.7);

return svg.node();
}
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more