Public
Edited
Jan 7
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
viewof R_range = {
const maxRs = Math.max(...data2.map((d) => d.views));
return Inputs.range([0, maxRs * 0.00000000004], {
label: "Length of R",
step: 0.00001
});
}
Insert cell
chart = {
const svg = d3
.create("svg")
.attr("viewBox", [0, 0, 1200, 675])
// .style("font", "8px sans-serif")
.attr(
"style",
"max-width: 100%; height: 675; font: 10px 'Noto Sans JP', sans-serif; font-weight: bold;"
);

// SVGのサイズ設定
const width = 1200;
const height = 675;
const margin = { top: 10, right: 60, bottom: 60, left: 100 };

// 半径を一括で設定
// const rSize = 0.1;
const rSize = R_range;
// フォントサイズを設定
// const fSize = 0.0001;
const fSize = font_range;
// 軌跡の幅を設定
// const sWidth = 0.14;
const sWidth = width_range;

// day2から最新のget_timeを取得
const latestDate = day2;

// get_timeを "m/d" 形式にフォーマット
const dateFormat = d3.timeFormat("%Y-%m-%d %H:%M"); // 時刻も含む形式に変更
// 最新のget_timeを表示するテキストを追加
svg
.append("text")
.attr("text-anchor", "start")
.attr("x", margin.left + 10) // x座標を左マージンから10px右に設定
.attr("y", margin.top + 40) // y座標を上マージンから20px下に設定
.text(`${dateFormat(latestDate)}`) // get_timeをフォーマットしてテキストに設定
.attr("font-size", "70px") // フォントサイズを設定
.attr("opacity", 0.2) // フォントサイズを設定
.attr("fill", "black"); // フォントの色を設定

svg
.append("text")
.attr("text-anchor", "start")
.attr("x", margin.left + 10) // x座標を左マージンから10px右に設定
.attr("y", margin.top + 80) // y座標を上マージンから20px下に設定
.text(`NHK紅白歌合戦2023 on 公式YouTube`) // get_timeをフォーマットしてテキストに設定
.attr("font-size", "30px") // フォントサイズを設定
.attr("opacity", 0.2) // フォントサイズを設定
.attr("fill", "black"); // フォントの色を設定

// 軸のスケールを設定
const xScale = d3
.scaleLinear()
// .domain([0, 2900000])
.domain([0, x_range])
.range([margin.left, width - margin.right]);

const yScale = d3
.scaleLinear()
// .domain([0, 50000])
.domain([0, y_range])
.range([height - margin.bottom, margin.top]);

// 折れ線グラフ用のラインジェネレータを作成
const lineGenerator = d3
.line()
.x((d) => xScale(d.views))
.y((d) => yScale(d.likes));

// データをtitleごとにグループ化
const groupedData = d3.group(
data2.filter((d) => d.get_time <= day2),
(d) => d.link
);

// 各グループの最新のデータポイントを取得
const latestDataPoints = Array.from(groupedData, ([key, values]) => {
return values.sort(
(a, b) => new Date(b.get_time) - new Date(a.get_time)
)[0];
});

// ユニークなtitleの数を計算
const uniqueTitleCount = new Set(latestDataPoints.map((d) => d.title)).size;

// 最新日付の全動画の合計視聴回数を計算
const totalViews = latestDataPoints.reduce((sum, d) => sum + d.views, 0);

// 合計視聴回数を表示するテキストを追加
svg
.append("text")
.attr("text-anchor", "start")
.attr("x", margin.left + 10) // x座標を左マージンから10px右に設定
.attr("y", margin.top + 120) // y座標を上マージンから120px下に設定
.text(
`全歌唱動画 ${uniqueTitleCount}本 の合計視聴回数: ${d3.format(",")(
totalViews
)} 回`
) // フォーマットした視聴回数を表示
.attr("font-size", "20px") // フォントサイズを設定
.attr("opacity", 0.2)
.attr("fill", "black"); // フォントの色を設定

// 各グループごとに線セグメントを描画
groupedData.forEach((values, key) => {
values.forEach((d, i) => {
if (i < values.length - 1) {
const nextD = values[i + 1];
svg
.append("path")
.attr(
"d",
`M ${xScale(d.views)} ${yScale(d.likes)} L ${xScale(
nextD.views
)} ${yScale(nextD.likes)}`
)
.attr("stroke", color(key))
.attr("stroke-width", Math.sqrt(d.views) * sWidth) // 【線の太さを動的に設定】
.attr("stroke-opacity", 0.15)
.attr("fill", "none");
}
});
});

// 既存のcircle要素の代わりにクリップパスと画像を追加する
data2
.filter((d) => d.get_time <= day2)
.forEach((d) => {
const isLatestDate = latestDataPoints.some(
(latestD) => latestD.get_time.getTime() === d.get_time.getTime()
);

// 最新の日付のデータポイントにだけクリップパスと画像を適用
if (isLatestDate) {
const radius = Math.sqrt(d.views * d.likes) * rSize; // 【半径を調整】
const newRadius = radius * 1.8; // 新しい半径を1.7倍に設定
const uniqueId = `clip-${d.link}`; // クリップパスのID

// クリップパスの定義
svg
.append("clipPath")
.attr("id", uniqueId)
.append("circle")
.attr("cx", xScale(d.views))
.attr("cy", yScale(d.likes))
.attr("r", radius); // 元の半径を使用

// 画像の追加
svg
.append("image")
.attr("xlink:href", d.thumbnail)
.attr("width", newRadius * 2) // 新しい半径の2倍の幅
.attr("height", newRadius * 2) // 新しい半径の2倍の高さ
.attr("x", xScale(d.views) - newRadius) // 新しい半径を使って中心を合わせる
.attr("y", yScale(d.likes) - newRadius) // 新しい半径を使って中心を合わせる
.attr("clip-path", `url(#${uniqueId})`)
.attr("opacity", 0.6); // 透明度を設定
}

// 通常の円を描画
svg
.append("circle")
.attr("cx", xScale(d.views))
.attr("cy", yScale(d.likes))
.attr("r", Math.sqrt(d.views * d.likes) * rSize) // 【半径を調整】
.attr("fill", color(d.link)) // titleに基づいて色を設定
.attr("fill-opacity", isLatestDate ? 0.3 : 0.1); // 透明度を設定
});

// 最新のデータポイントにラベルテキストを描画
latestDataPoints.forEach((d) => {
svg
.append("text")
.attr("text-anchor", "middle")
.attr("x", xScale(d.views))
.attr("y", yScale(d.likes))
.text(d.title_artist)
// .attr("font-size", `${Math.max(0.00001 * d.views, 10)}px`)
.attr("font-size", `${Math.max(fSize * d.views, 10)}px`) // 【フォントサイズを調整】
.attr("fill", "black")
.attr("dy", "0.35em") // Y軸方向の調整を追加
.attr("stroke", "white")
.attr("stroke-width", `${Math.max(0.000005 * d.views, 10) * 0.2}`)
.attr("style", "font-weight: bold; fill-opacity: 1.0;")
.style("paint-order", "stroke fill");
});

// Y軸の目盛りラベルを設定
const yAxis = svg
.append("g")
.attr("class", "y-axis")
.attr("transform", `translate(${margin.left}, 0)`)
.call(d3.axisLeft(yScale).ticks(5)) // 目盛りの数を設定
.selectAll("text")
.style("font-size", "16px")
.attr("fill", "#666666"); // フォントの色を設定;;

// 0を非表示にするため、0の要素を選択して非表示に設定
yAxis.filter((d) => d === 0).remove();

// X軸の目盛りラベルを設定
const xAxis = svg
.append("g")
.attr("class", "x-axis")
.attr("transform", `translate(0, ${height - margin.bottom})`)
.call(d3.axisBottom(xScale).ticks(5)) // 目盛りの数を設定
.selectAll("text")
.style("font-size", "16px")
.attr("fill", "#666666"); // フォントの色を設定;

// 0を非表示にするため、0の要素を選択して非表示に設定
xAxis.filter((d) => d === 0).remove();

// x軸のラベルを追加
svg
.append("text")
.attr("text-anchor", "middle")
.attr("x", width / 2) // x座標をSVGの中央に設定
.attr("y", height - margin.bottom + 50) // y座標をx軸の下に設定
.text("視聴回数") // テキストを設定
.attr("font-size", "20px") // フォントサイズを設定
.attr("fill", "#666666"); // フォントの色を設定

// X軸の線を非表示にする
svg.select(".x-axis").selectAll("path").remove();

// X軸の線を非表示にする
svg.select(".y-axis").selectAll("path").remove();

// y軸のラベルを追加
svg
.append("text")
.attr("text-anchor", "middle")
.attr("x", -height / 2) // x座標をy軸の左に設定
.attr("y", margin.left - 80) // y座標をSVGの上に設定
.attr("transform", "rotate(-90)") // テキストを90度回転
.text("いいね数") // テキストを設定
.attr("font-size", "20px") // フォントサイズを設定
.attr("fill", "#666666"); // フォントの色を設定;

svg
.append("text")
.attr("x", 10) // 左から10pxの位置
.attr("y", height + 13) // 下から10pxの位置(キャプションのベースライン)
.text(
"YouTubeチャンネル「NHK MUSIC」データより徒然研究室(仮称)作成。マーカーの半径は視聴回数といいね数の積の平方根に比例している。"
) // キャプションのテキスト
.attr("fill", "#535353")
.attr(
"style",
"max-width: 100%; height: auto; font: 10px 'Noto Sans JP', sans-serif; font-weight: normal;"
)
.style("font-size", "12px"); // フォントサイズを12pxに設定

// SVG要素を返す
return svg.node();
}
Insert cell
viewof checkboxes = Inputs.checkbox(
Array.from(new Set(data2.map((d) => d.title))),
{ label: "Select Tracks" }
)
Insert cell
chart2 = {
const svg = d3
.create("svg")
.attr("viewBox", [0, 0, 1200, 675])
// .style("font", "8px sans-serif")
.attr(
"style",
"max-width: 100%; height: 675; font: 10px 'Noto Sans JP', sans-serif; font-weight: bold;"
);

// SVGのサイズ設定
const width = 1200;
const height = 675;
const margin = { top: 10, right: 60, bottom: 60, left: 100 };

// 半径を一括で設定
// const rSize = 0.1;
const rSize = R_range;
// フォントサイズを設定
// const fSize = 0.0001;
const fSize = font_range;
// 軌跡の幅を設定
// const sWidth = 0.14;
const sWidth = width_range;

// day2から最新のget_timeを取得
const latestDate = day2;

// get_timeを "m/d" 形式にフォーマット
const dateFormat = d3.timeFormat("%Y-%m-%d %H:%M"); // 時刻も含む形式に変更
// 最新のget_timeを表示するテキストを追加
svg
.append("text")
.attr("text-anchor", "start")
.attr("x", margin.left + 10) // x座標を左マージンから10px右に設定
.attr("y", margin.top + 40) // y座標を上マージンから20px下に設定
.text(`${dateFormat(latestDate)}`) // get_timeをフォーマットしてテキストに設定
.attr("font-size", "70px") // フォントサイズを設定
.attr("opacity", 0.2) // フォントサイズを設定
.attr("fill", "black"); // フォントの色を設定

svg
.append("text")
.attr("text-anchor", "start")
.attr("x", margin.left + 10) // x座標を左マージンから10px右に設定
.attr("y", margin.top + 80) // y座標を上マージンから20px下に設定
.text(`NHK紅白歌合戦2023 on 公式YouTube`) // get_timeをフォーマットしてテキストに設定
.attr("font-size", "30px") // フォントサイズを設定
.attr("opacity", 0.2) // フォントサイズを設定
.attr("fill", "black"); // フォントの色を設定

// 軸のスケールを設定
const xScale = d3
.scaleLinear()
// .domain([0, 2900000])
.domain([0, x_range])
.range([margin.left, width - margin.right]);

const yScale = d3
.scaleLinear()
// .domain([0, 50000])
.domain([0, y_range])
.range([height - margin.bottom, margin.top]);
// 折れ線グラフ用のラインジェネレータを作成
const lineGenerator = d3
.line()
.x((d) => xScale(d.views))
.y((d) => yScale(d.likes));

// データをtitleごとにグループ化
const groupedData = d3.group(
data2.filter((d) => d.get_time <= day2),
(d) => d.link
);

// 各グループの最新のデータポイントを取得
const latestDataPoints = Array.from(groupedData, ([key, values]) => {
return values.sort(
(a, b) => new Date(b.get_time) - new Date(a.get_time)
)[0];
});

// ユニークなtitleの数を計算
const uniqueTitleCount = new Set(latestDataPoints.map((d) => d.title)).size;

// 最新日付の全動画の合計視聴回数を計算
const totalViews = latestDataPoints.reduce((sum, d) => sum + d.views, 0);

// 合計視聴回数を表示するテキストを追加
svg
.append("text")
.attr("text-anchor", "start")
.attr("x", margin.left + 10) // x座標を左マージンから10px右に設定
.attr("y", margin.top + 120) // y座標を上マージンから120px下に設定
.text(
`全歌唱動画${uniqueTitleCount}本の合計視聴回数: ${d3.format(",")(
totalViews
)} 回`
) // フォーマットした視聴回数を表示
.attr("font-size", "20px") // フォントサイズを設定
.attr("opacity", 0.2)
.attr("fill", "black"); // フォントの色を設定

// 各グループごとに線セグメントを描画
groupedData.forEach((values, key) => {
const isSelected = checkboxes.includes(values[0].title); // グループのタイトルで選択状態を判定

values.forEach((d, i) => {
if (i < values.length - 1) {
const nextD = values[i + 1];
svg
.append("path")
.attr(
"d",
`M ${xScale(d.views)} ${yScale(d.likes)} L ${xScale(
nextD.views
)} ${yScale(nextD.likes)}`
)
.attr("stroke", color(key))
.attr("stroke-width", Math.sqrt(d.views) * sWidth) // 【線の太さを動的に設定】
.attr("stroke-opacity", isSelected ? 0.4 : 0.05) // グループの選択状態に基づいて透明度を設定
.attr("fill", "none");
}
});
});

// 既存のcircle要素の代わりにクリップパスと画像を追加する
data2
.filter((d) => d.get_time <= day2)
.forEach((d) => {
const isLatestDate = latestDataPoints.some(
(latestD) => latestD.get_time.getTime() === d.get_time.getTime()
);

// 最新の日付のデータポイントにだけクリップパスと画像を適用
if (isLatestDate) {
const radius = Math.sqrt(d.views * d.likes) * rSize; // 【半径を調整】
const newRadius = radius * 1.8; // 新しい半径を1.7倍に設定
const uniqueId = `clip-${d.link}`; // クリップパスのID

// クリップパスの定義
svg
.append("clipPath")
.attr("id", uniqueId)
.append("circle")
.attr("cx", xScale(d.views))
.attr("cy", yScale(d.likes))
.attr("r", radius); // 元の半径を使用

// 画像の追加
svg
.append("image")
.attr("xlink:href", d.thumbnail)
.attr("width", newRadius * 2) // 新しい半径の2倍の幅
.attr("height", newRadius * 2) // 新しい半径の2倍の高さ
.attr("x", xScale(d.views) - newRadius) // 新しい半径を使って中心を合わせる
.attr("y", yScale(d.likes) - newRadius) // 新しい半径を使って中心を合わせる
.attr("clip-path", `url(#${uniqueId})`)
.attr("opacity", checkboxes.includes(d.title) ? 1 : 0.15); // 透明度を設定 // ★opacityで設定
}
const isSelected = checkboxes.includes(d.title);
// 通常の円を描画
svg
.append("circle")
.attr("cx", xScale(d.views))
.attr("cy", yScale(d.likes))
.attr("r", Math.sqrt(d.views * d.likes) * rSize) // 【半径を調整】
.attr("fill", color(d.link)) // titleに基づいて色を設定
.attr("fill-opacity", isSelected ? 0.4 : 0.05); // 透明度を設定

// 白色の円を重ねて描画(透明度を調整)
// svg
// .append("circle")
// .attr("cx", xScale(d.views))
// .attr("cy", yScale(d.likes))
// .attr("r", Math.sqrt(d.views) * rSize) // 通常の円と同じ半径を使用
// .attr("fill", "white")
// .attr("fill-opacity", isSelected ? 0.0 : 0.7); // チェックボックスの状態によって透明度を変更
});

// 最新のデータポイントにラベルテキストを描画
latestDataPoints.forEach((d) => {
svg
.append("text")
.attr("text-anchor", "middle")
.attr("x", xScale(d.views))
.attr("y", yScale(d.likes))
.text(d.title_artist)
// .attr("font-size", `${Math.max(0.00001 * d.views, 10)}px`)
.attr("font-size", `${Math.max(fSize * d.views, 10)}px`) // 【フォントサイズを調整】
.attr("fill", "black")
.attr("dy", "0.35em") // Y軸方向の調整を追加
.attr("stroke", "white")
.attr("stroke-width", `${Math.max(0.000005 * d.views, 10) * 0.2}`)
.attr("style", "font-weight: bold; fill-opacity: 1.0;")
.style("paint-order", "stroke fill")
.attr("opacity", checkboxes.includes(d.title) ? 1 : 0.2);
});

// Y軸の目盛りラベルを設定
const yAxis = svg
.append("g")
.attr("class", "y-axis")
.attr("transform", `translate(${margin.left}, 0)`)
.call(d3.axisLeft(yScale).ticks(5)) // 目盛りの数を設定
.selectAll("text")
.style("font-size", "16px")
.attr("fill", "#666666"); // フォントの色を設定;;

// 0を非表示にするため、0の要素を選択して非表示に設定
yAxis.filter((d) => d === 0).remove();

// X軸の目盛りラベルを設定
const xAxis = svg
.append("g")
.attr("class", "x-axis")
.attr("transform", `translate(0, ${height - margin.bottom})`)
.call(d3.axisBottom(xScale).ticks(5)) // 目盛りの数を設定
.selectAll("text")
.style("font-size", "16px")
.attr("fill", "#666666"); // フォントの色を設定;

// 0を非表示にするため、0の要素を選択して非表示に設定
xAxis.filter((d) => d === 0).remove();

// x軸のラベルを追加
svg
.append("text")
.attr("text-anchor", "middle")
.attr("x", width / 2) // x座標をSVGの中央に設定
.attr("y", height - margin.bottom + 50) // y座標をx軸の下に設定
.text("視聴回数") // テキストを設定
.attr("font-size", "20px") // フォントサイズを設定
.attr("fill", "#666666"); // フォントの色を設定

// X軸の線を非表示にする
svg.select(".x-axis").selectAll("path").remove();

// X軸の線を非表示にする
svg.select(".y-axis").selectAll("path").remove();

// y軸のラベルを追加
svg
.append("text")
.attr("text-anchor", "middle")
.attr("x", -height / 2) // x座標をy軸の左に設定
.attr("y", margin.left - 80) // y座標をSVGの上に設定
.attr("transform", "rotate(-90)") // テキストを90度回転
.text("いいね数") // テキストを設定
.attr("font-size", "20px") // フォントサイズを設定
.attr("fill", "#666666"); // フォントの色を設定;

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

// SVG要素を返す
return svg.node();
}
Insert cell
240117_0532_youtube_kouhaku_union.csv
Type Table, then Shift-Enter. Ctrl-space for more options.

Insert cell
color = d3
.scaleOrdinal(
data2.map((d) => d.link),
d3.schemeTableau10
)
.unknown("black")
Insert cell
lastDefined = ({
reduceIndex(I, X) {
for (let i = I.length - 1; i >= 0; --i) {
const x = X[I[i]];
if (x != null) {
return x; // 最初に見つかった非null値を返す
}
}
}
})
Insert cell
csvData = FileAttachment(
"20250101T020012.285993+0900_NHK_MUSIC.csv"
).csv({ typed: true })
Insert cell
// data2 = _230110.map((d) => {
// const parts = d.get_time.split("/");
// const year = parts[0];
// const month = parts[1].padStart(2, "0");
// const day = parts[2].padStart(2, "0");

// d.get_time = `${year}-${month}-${day}`;

// // get_time形式に変換
// d.get_time = new Date(`${year}-${month}-${day}`);
// return d;
// })


// data2 = _230110
Insert cell
// data2 = _230110.map((d) => {
// // タイトルから【と】で囲まれた部分を抽出
// const title = d.title;
// const startIndex = title.indexOf("【");
// const endIndex = title.indexOf("】");
// if (startIndex !== -1 && endIndex !== -1) {
// d.title_artist = title.slice(startIndex + 1, endIndex);
// } else {
// d.title_artist = ""; // 【】で囲まれた部分が見つからない場合は空文字列
// }

// return d;
// })

// 複数の「title」列の文字列を置換する処理を追加
_230110.forEach((d) => {
// ここで置換前の文字列と置換後の文字列を指定
const replacements = [
{
before:
"【MISIA&Rockon Social Club】「傷だらけの王者」話題を集めた今年のラグビーテーマソング【紅白】|NHK",
after: "【MISIA】"
},
{
before:
"【MISIA&Rockon Social Club】紅白「傷だらけの王者」話題を集めた今年のラグビーテーマソング|NHK",
after: "【MISIA】"
},
{
before:
"【紅白特別企画】ブラックビスケッツ 復活!「Timing」25年ぶり!奇跡のステージ【紅白】|NHK",
after: "【ブラックビスケッツ】"
},
{
before:
"【紅白特別企画】ポケットビスケッツ 復活!「YELLOW YELLOW HAPPY」25年ぶりの紅白【紅白】|NHK",
after: "【ポケットビスケッツ】"
},
{
before:
"【紅白特別企画】寺尾聰「ルビーの指環」「ザ・ベストテン」12週連続1位を記録!【紅白】|NHK",
after: "【寺尾聰】"
}
// 他にも必要な置換ルールを追加できます
];

// 「title」列の文字列に対して置換処理を実行
replacements.forEach((replacement) => {
d.title = d.title.replace(replacement.before, replacement.after);
});

// タイトルから【と】で囲まれた部分を抽出
const startIndex = d.title.indexOf("【");
const endIndex = d.title.indexOf("】");
if (startIndex !== -1 && endIndex !== -1) {
d.title_artist = d.title.slice(startIndex + 1, endIndex);
} else {
d.title_artist = ""; // 【】で囲まれた部分が見つからない場合は空文字列
}
})
Insert cell
// data2に代入
data2 = _230110.slice() // _230110のコピーを作成
Insert cell
import {Scrubber} from "@mbostock/scrubber"
Insert cell
// Noto Sans JP Blackフォントをロードする関数
async function loadFont() {
return new Promise((resolve) => {
const font = new FontFace(
"Noto Sans JP Black",
"url(https://cdn.plot.ly/plotly-texture/fonts/v1.3/noto-sans-jp-v13-latin-700.woff)"
);
font.load().then(() => {
document.fonts.add(font);
resolve();
});
});
}
Insert cell
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