Public
Edited
Jan 2, 2024
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
// // デフォルトで全てにチェックが入っているようにする
// viewof checkboxes = Inputs.checkbox(
// Array.from(new Set(data.children.map((d) => d.name))),
// {
// label: "Select Artists",
// value: Array.from(new Set(data.children.map((d) => d.name))) // 初期値としてすべてのカテゴリを設定
// }
// )

Insert cell
viewof opacity_range_high = Inputs.range([0.8, 1.0], {
label: "Opacity",
step: 0.05
})
Insert cell
viewof opacity_range_row = Inputs.range([0, 0.3], {
label: "Opacity",
step: 0.05
})
Insert cell
chart = {
// チャートの寸法を指定します。
const width = 1200; // 幅を1200に設定
const height = 675; // 高さを675に設定

// // 色のスケールを指定します。
// const color = d3.scaleOrdinal(d3.schemeTableau10);

const customColors = [
"#616161",
"#4285f4",
"#db4437",
"#f4b400",
"#0f9d58",
"#ab47bc",
"#00acc1",
"#ff7043",
"#9e9d24",
"#5c6bc0",
"#f06292",
"#00796b",
"#c2185b",
"#7e57c2",
"#03a9f4",
"#8bc34a",
"#fdd835",
"#fb8c00",
"#8d6e63",
"#9e9e9e",
"#607d8b"
];

// カスタムカラーパレットを使用してカラースケールを作成
const color = d3.scaleOrdinal(customColors);

// レイアウトを計算します。
const root = d3
.treemap() // ツリーマップレイアウトを生成
.tile(tile) // タイリング方法を指定
.size([width, height]) // サイズを指定
.padding(0.1) // パディングを設定
.round(true)(
// 端を丸める
d3
.hierarchy(data) // 階層データからルートを生成
.sum((d) => d.value) // 値に基づいて合計を計算
.sort((a, b) => b.value - a.value) // 値に基づいて並べ替え
);

const margin = { top: 10, right: 0, bottom: 0, left: 0 }; // 余白を設定

// SVGコンテナを作成します。
const svg = d3
.create("svg")
.attr("viewBox", [
0,
0,
width + margin.left + margin.right,
height + margin.top + margin.bottom
])
.attr("width", width + margin.left + margin.right)
// ツリーマップと余白を考慮した高さに設定
.attr("height", height + margin.top + margin.bottom + 70) // 例: 50を追加して余裕を持たせる
.attr(
"style",
"max-width: 100%; height: auto; font: 10px 'Noto Sans JP', sans-serif; font-weight: bold;"
);

// タイトルを追加
const title = svg
.append("text") // text要素をsvgに追加
.attr("class", "title") // クラス名を設定
.attr("x", width / 2) // 幅の中央に配置
.attr("y", 12) // 上から20pxの位置に配置
.attr("text-anchor", "middle") // 中央揃えに設定
.style("font-size", "39px") // フォントサイズを設定
.text("紅白歌合戦2023|公式YouTube 本番動画視聴回数") // タイトルのテキストを設定
.attr("fill", "#535353");

// ツリーマップの配置位置を調整
const treeMap = svg
.append("g") // g要素を追加
.attr("transform", `translate(0, 30)`); // タイトルの下に配置するためにY座標を調整

// 階層の各リーフにセルを追加します。
const leaf = treeMap
.selectAll("g") // すべてのg要素を選択
.data(root.leaves()) // リーフのデータを適用
.join("g") // g要素を追加
// .attr("filter", shadow)
.attr("transform", (d) => `translate(${d.x0},${d.y0})`); // 位置を変換

// ツールチップを追加します。
const format = d3.format(",d"); // 書式を設定
const formatPercent = d3.format(".1%"); // パーセント表示の書式を設定
leaf.append("title").text(
(d) =>
`${d
.ancestors() // 先祖要素を取得
.reverse() // 配列を逆にする
.map((d) => d.data.name) // 名前を抽出
.join(".")}\n${format(d.value)}\n${formatPercent(d.data.value_2)}` // 書式に従って値を追加
);

// SVGフィルターを定義します。
const shadowFilter = svg.append("filter").attr("id", "shadow");
shadowFilter
.append("feDropShadow")
.attr("dx", 10) // X方向の影のオフセット
.attr("dy", 10) // Y方向の影のオフセット
.attr("stdDeviation", 5) // 影のぼかしの大きさ
.attr("flood-opacity", 0.8); // 影の透明度を薄く設定します 0.8

// 色付きの長方形を追加します。
leaf
.append("rect")
.attr("id", (d) => (d.leafUid = DOM.uid("leaf")).id)
.attr("fill", (d) => {
while (d.depth > 1) d = d.parent;
return color(d.data.name); // カスタムカラーパレットを使用
})
// leaf
// .append("rect")
// .attr("id", (d) => (d.leafUid = DOM.uid("leaf")).id) // 各長方形に一意のIDを割り当てます
// .attr("fill", (d) => {
// while (d.depth > 1) d = d.parent; // 最上位の親まで辿り、その色を取得します
// return color(d.data.name);
// })
.attr("fill-opacity", 0.8) // 塗りの不透明度を設定します
.attr("width", (d) => d.x1 - d.x0) // 長方形の幅を設定します
.attr("height", (d) => d.y1 - d.y0) // 長方形の高さを設定します
.attr("stroke", "#FAF5F0") // 枠線の色を設定します
.attr("rx", 3) // 角の丸みを設定します
.attr("ry", 3) // 角の丸みを設定します
.attr("filter", (d) =>
checkboxes.includes(d.data.name) ? "url(#shadow)" : ""
); // checkboxesで選択された要素に対してフィルターを適用します

// テキストがオーバーフローしないようにclipPathを追加します。
leaf
.append("clipPath") // clipPath要素を追加
.attr("id", (d) => (d.clipUid = DOM.uid("clip")).id) // IDを設定
.append("use") // use要素を追加
.attr("xlink:href", (d) => d.leafUid.href); // href属性を設定

// 複数行のテキストを追加します。最後の行は値を表示し、特定の書式を持っています。
// まず、既存のtspan要素の数を調べる関数を作成
function countTspans(d) {
return d.parentNode ? d.parentNode.childNodes.length : 0;
}

// テキストのフォントサイズを計算する部分
const text = leaf
.append("text")
.attr("fill", "#FFFFFF")
.attr("fill-opacity", 0.9)
.attr("clip-path", (d) => d.clipUid)
.attr("font-size", (d) => {
const width = d.x1 - d.x0; // セルの横幅
const name = d.data.name; // テキストの内容

// 実際にテキスト描画に使用されるフォントスタイルに合わせてテキストの幅を計算
const textWidth = getTextWidth(name, "10px 'Noto Sans JP', sans-serif");

// セルの幅と高さを考慮してフォントサイズを調整
const fontSize = Math.min((width / textWidth) * 9, height);
return fontSize + "px";
});

text
.selectAll(null)
.data((d) => d.data.name.split(/(?=[A-Z][a-z])|\s+/g)) // 名前を単語ごとに分割
.join("tspan")
.attr("x", 3)
.attr("y", (d, i) => `${i * 0.9 + 1}em`)
.text((d) => d);

text
.append("tspan")
.attr("x", 3)
.attr("y", (d) => {
const height = d.y1 - d.y0;
const textHeight = 10; // テキストの高さを固定値で設定
return height - textHeight + "px"; // 'px' 単位を使用
})
.attr("fill-opacity", 0.45)
.text((d) => format(d.value))
.attr("font-size", (d) => {
const width = d.x1 - d.x0;
const height = d.y1 - d.y0;
const name = d.data.name;
const textWidth = getTextWidth(name, "10px 'Noto Sans JP', sans-serif");
// フォントサイズを計算し、32pxを超えないように制限
const fontSize = Math.min(
Math.min((width / textWidth) * 10 * 0.6, height),
22
);
return fontSize + "px";
});

// テキストの幅を計算する関数
function getTextWidth(text, font) {
// ここでのfontパラメータは、実際にテキスト描画に使用されるフォントスタイルを反映するように設定します。
// 例: "10px 'Noto Sans JP', sans-serif"
const canvas = document.createElement("canvas");
const context = canvas.getContext("2d");
context.font = font;
const metrics = context.measureText(text);
return metrics.width;
}

// ツリーマップを更新する関数
function updateTreemap(selectedNames) {
// 選択された名前に基づいてツリーマップのセルの表示を更新
svg.selectAll("rect").attr("fill-opacity", (d) => {
return selectedNames.includes(d.data.name)
? opacity_range_high
: opacity_range_row;
});
}

// 初期状態でツリーマップを表示
updateTreemap(checkboxes);

// // checkboxesの値が変更されたときにツリーマップを更新
// // ObservableHQはこの依存関係を自動的に検出し、checkboxesが変更されるたびにこのコードを再実行します
// Invalidations.observe(() => updateTreemap(checkboxes));

svg
.append("text")
.attr("x", 10) // 左から10pxの位置
.attr("y", height + 38) // 下から10pxの位置(キャプションのベースライン)
.text(
"YouTubeチャンネル「NHK MUSIC」データより徒然研究室(仮称)が2024年1月2日作成。右上の日時はデータ取得日時。"
) // キャプションのテキスト
.attr("fill", "#535353")
.attr(
"style",
"max-width: 100%; height: auto; font: 10px 'Noto Sans JP', sans-serif; font-weight: normal;"
)
.style("font-size", "8.3px"); // フォントサイズを12pxに設定

if (datetimeMatch) {
const yearPart = "20" + datetimeMatch[1]; // 年
const monthPart = datetimeMatch[2]; // 月
const dayPart = datetimeMatch[3]; // 日
const hourPart = datetimeMatch[4]; // 時
const minutePart = datetimeMatch[5]; // 分
const formattedDatetime = `${yearPart}-${monthPart}-${dayPart} ${hourPart}:${minutePart}`;

// 日時テキストを表示
svg
.append("text")
.attr("x", width - 10) // 右端から10pxの位置
.attr("y", 13) // 上から20pxの位置
.text(formattedDatetime) // フォーマットされた日時情報を表示
.attr("fill", "#535353")
.attr("text-anchor", "end") // テキストを右揃えに設定
.attr(
"style",
"max-width: 100%; height: auto; font: 10px 'Noto Sans JP', sans-serif; font-weight: bold;"
)
.style("font-size", "16px"); // フォントサイズを10pxに設定
}

return Object.assign(svg.node(), { scales: { color } }); // svgノードと色のスケールを返す
}
Insert cell
Insert cell
Type JavaScript, then Shift-Enter. Ctrl-space for more options. Arrow ↑/↓ to switch modes.

Insert cell
import { Swatches } from "@d3/color-legend"
Insert cell
data = FileAttachment("240102_2236_NHK MUSIC@3.json").json()
Insert cell
filename = "240102_2236_NHK MUSIC@1.json"
Insert cell
// ファイル名から日時情報を正規表現を使用して抽出
datetimeMatch = filename.match(/(\d{2})(\d{2})(\d{2})_(\d{2})(\d{2})/)
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