元ネタはこちら↓
なぜ行おうとしたか
元ネタでは、単なる静的なWordCloudでは、見た目にインパクトが少ないし、読者の目もすぐに離れてしまいがちです(viewsの数が少なかった😂)
動きがあれば注目を集めやすく、より楽しんでもらえるのではないかと思いました(安易ですが)
結果
……でも、むずかしかったー!
いやほんと、簡単に動かせると思ってたんですが、甘かったです
• WordCloud自体にアニメーション機能がない
• Python以外でやるなら? 何を使えば?
• 歌詞の単語数が多すぎて、何が何だかわからない
などなど、細かい壁に何度もぶつかりました。
特にアニメーションの自然な見せ方には、かなりの試行錯誤が必要でした
アニメーションの方法と比較
WordCloudでできないとなった時に、ChatGPTに聞いてみました
方法名 | 必要環境 | 動きの自由度 | 難易度 |
---|---|---|---|
Streamlit更新型 | Python + Streamlit | 低〜中(見た目の更新) | 簡単 |
matplotlibアニメ | Pythonのみ | 中(サイズや色変化) | 中 |
d3-cloud (JS) | ブラウザ + JavaScript | 高(自由自在に動かせる) | 少し難しい |
実際の作業の流れ
で、なにを採用するか?
JavaScript を選択しましたが、ちょっと変更しました
もーっと簡単にしました
1. 形態素解析で単語の頻度を抽出しJSONファイルにする(Python)
✅ポイント
① 出現回数が多い上位30語を抽出する
アニメーションで可視化する際、単語数が多すぎると全体がごちゃごちゃして見づらくなります そのため、出現頻度の高い単語だけを抜き出します
most_common_words = global_counter.most_common(30)
→ 上位30語を取得
② JavaScriptで使える形式に変換して保存
[["単語1", 出現数1], ["単語2", 出現数2], ...] の形式に変換
JSONファイルとして保存(JavaScriptでWordCloudに使える)
with open("自分のPATHーーここ変更ーー/wordcloud_data.json", "w", encoding="utf-8") as f:
json.dump(word_data, f, ensure_ascii=False, indent=2)
全体コード①
全体コードはこちら↓
import pandas as pd
from janome.tokenizer import Tokenizer
from collections import Counter
import json
# ファイルパス
csv_path = '自分のCSVーここ変更ーー'
df = pd.read_csv(csv_path)
# 形態素解析器
tokenizer = Tokenizer()
# 抽出する品詞
target_pos = ("名詞", "動詞", "形容詞")
# 全体の単語カウント
global_counter = Counter()
# 各行の処理
for _, row in df.iterrows():
lyric = str(row.get("歌詞", "")).strip()
if not lyric:
continue
words = [
token.base_form for token in tokenizer.tokenize(lyric)
if any(token.part_of_speech.startswith(pos) for pos in target_pos)
]
global_counter.update(words)
# 上位N単語を選ぶ(例:上位30語)
most_common_words = global_counter.most_common(30)
# JavaScript用に変換
word_data = [[word, freq] for word, freq in most_common_words]
# JSONとして保存
with open("自分のPATHーーここ変更ーー/wordcloud_data.json", "w", encoding="utf-8") as f:
json.dump(word_data, f, ensure_ascii=False, indent=2)
print("✅ wordcloud_data.json を出力しました。")
2. VScode内でJSONファイルをJavaScriptで読み込み
✅ポイント
① VScodeのフォルダ構成
nogizaka-wordcloud/
├── index.html
└── wordcloud_data.json
② ローカルサーバーで表示する方法(推奨)
ローカルで .json ファイルを読み込むには、ローカルサーバーを使う必要があります。以下の方法をおすすめします
方法①:VSCodeの拡張機能「Live Server」を使う(簡単)
左の「拡張機能(四つの四角アイコン)」をクリック
「Live Server」と検索して、Ritwick Dey 作のものをインストール
index.html を開いた状態で右下の「Go Live」をクリック
👉 これでブラウザが自動で開いて、アニメーションが表示されるはずです。
全体コード(仮index.html)
[
["Happy", 6],
["君", 4],
["行く", 6],
["どこ", 3],
["何", 2]
]
全体コード(wordcloud_data.json)
全体コードはこちら↓
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<title>乃木坂46 WordCloud風アニメーション</title>
<style>
body {
font-family: 'Hiragino Maru Gothic Pro', 'Meiryo', sans-serif;
background-color: #f4f4f4;
text-align: center;
margin: 20px;
}
h2 {
font-size: 28px;
margin-bottom: 8px;
}
.search-container {
margin-bottom: 20px;
}
#search-box {
font-size: 16px;
padding: 6px 12px;
width: 250px;
border: 1px solid #ccc;
border-radius: 6px;
}
canvas {
border: 1px solid #ccc;
cursor: default;
}
</style>
</head>
<body>
<h2>乃木坂46</h2>
<div class="search-container">
<input type="text" id="search-box" placeholder="単語を検索..." />
</div>
<canvas id="cloud" width="360" height="500"></canvas> <!-- 9:16の比率を維持 -->
<script>
const searchBox = document.getElementById('search-box');
const canvas = document.getElementById('cloud');
const ctx = canvas.getContext('2d');
let words = [];
let hoverWord = null;
let mouseX = 0;
let mouseY = 0;
fetch('wordcloud_data.json')
.then(response => response.json())
.then(data => {
words = data.map(([text, freq]) => {
const size = 20 + freq * 5;
return {
text,
freq,
size,
x: Math.random() * (canvas.width - 100) + 50,
y: Math.random() * (canvas.height - 100) + 50,
dx: (Math.random() - 0.5) * 0.3,
dy: (Math.random() - 0.5) * 0.3,
match: false
};
});
animate();
});
searchBox.addEventListener('input', () => {
const query = searchBox.value.trim();
words.forEach(w => {
w.match = query !== "" && w.text.includes(query);
});
});
function animate() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
hoverWord = null;
for (const w of words) {
const scale = w.match ? 1.6 : 1;
const fontSize = w.size * scale;
w.x += w.dx;
w.y += w.dy;
const halfWidth = ctx.measureText(w.text).width / 2;
const halfHeight = fontSize / 2;
if (w.x - halfWidth < 0 || w.x + halfWidth > canvas.width) w.dx *= -1;
if (w.y - halfHeight < 0 || w.y + halfHeight > canvas.height) w.dy *= -1;
ctx.font = ${fontSize}px 'Hiragino Maru Gothic Pro', 'Meiryo', sans-serif;
ctx.fillStyle = w.match ? 'red' : hsl(${(w.freq * 30) % 360}, 60%, 40%);
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(w.text, w.x, w.y);
const textWidth = ctx.measureText(w.text).width;
if (
mouseX >= w.x - textWidth / 2 &&
mouseX <= w.x + textWidth / 2 &&
mouseY >= w.y - fontSize / 2 &&
mouseY <= w.y + fontSize / 2
) {
hoverWord = w;
}
}
if (hoverWord) {
const tipText = ${hoverWord.text}: ${hoverWord.freq}回;
ctx.font = "14px sans-serif";
ctx.fillStyle = "#000";
ctx.fillText(tipText, mouseX + 15, mouseY + 26);
}
requestAnimationFrame(animate);
}
canvas.addEventListener('mousemove', e => {
const rect = canvas.getBoundingClientRect();
mouseX = e.clientX - rect.left;
mouseY = e.clientY - rect.top;
});
</script>
</body>
</html>
① fetch('wordcloud_data.json') の使い方(データの読み込み)
外部ファイル wordcloud_data.json を非同期で読み込んで、中の単語データを words に格納する処理です
fetch('wordcloud_data.json')
.then(response => response.json())
.then(data => {
words = data.map(([text, freq]) => {
// ...
});
animate();
});
② animate() 関数によるアニメーション制御
この関数が毎フレーム呼ばれて、単語が動くようになっています
w.x += w.dx のように、各単語をちょっとずつ動かしていて、画面の端に当たったら跳ね返ります
function animate() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
// ... 各単語の移動、描画、当たり判定など
requestAnimationFrame(animate);
}
おわりに
最初は「ちょっと動かせたら面白いかも」くらいの軽い気持ちだったのですが、実際に作ってみると、細かい部分でかなりハマりました
少しでも面白いと思っていただけたら嬉しいです✨
今回は 齋藤飛鳥ちゃん推し ということで、あすかちゃんがセンターを務めた以下の4曲を対象にしました:
• ここにはないもの
• 裸足でSummer
• ジコチューで行こう!
• Sing Out!
これらの歌詞を形態素解析し、出現回数が多かった上位30語 をアニメーションで可視化しています👀