ホーム技術ブログ › 12
2026-09-08 #12

語ごとの「いつ・どの回で語られたか」を1枚に置く。時間軸をそのまま使うと潰れる

空いた期間の幅を頭打ちにして詰める。回と回を結ぶ線は、番組が過去回を明言した箇所だけにした。拡大縮小の中でカードを掴む話も。

JavaScript可視化UI設計

はじめに

全文検索を作ったあと、次に出てきたのが「この話、いつから始まったんだっけ」でした。

検索は点で見つかります。ただ、番組は同じ話題に何年も戻ってきます。 「ポライトネス」は3年にわたって10本に出てきますし、 シリーズで一気にやった翌年に、別の回でふと言及されたりもします。 一覧に並べても、そのつながりは見えませんでした。

それで、語ごとに「いつ・どの回で・どれくらい語られたか」を1枚に置く画面を作りました。 いまは12語ぶんあります。

先に結論

  • 時間を素直に横軸にすると、空白期間で全部が潰れる。空きは頭打ちにした
  • 線を引くのは「番組が過去回を明言した箇所」だけ。造語がそのまま再登場したものは線にしない
  • 線のラベルは回のタイトルではなく、番組が実際に言った文をそのまま出す
  • 拡大縮小の中でドラッグするときは getBoundingClientRect() を使わない

横軸を素直に日付にすると潰れる

最初は公開日をそのまま横軸にしました。 すると、シリーズで集中した数週間に全部が重なり、 その後の2年が空白になります。読めません。

空いた期間の幅を頭打ちにしました。

var GAPCAP=45;   // これ以上空いた期間は45日ぶんの幅しか取らない

for(var di=1; di<DS.length; di++){
  CUM.push(CUM[di-1] + Math.min((dnum(DS[di])-dnum(DS[di-1]))/86400000, GAPCAP));
}

累積した値を横位置にしています。 これで「連続してやった時期は詰まって見え、間が空いた事実も残る」形になりました。 正確な時間軸ではなくなりますが、読みたいのは間隔の絶対値ではなく順番と塊のほうでした。

縦は重ならないように積んでいます。横が近いカードどうしを見て、列を増やす形です。

if(Math.abs(A._x - B._x) >= CW+GAP) continue;   // 横に離れていれば衝突しない
var dy = B._y - A._y, need = CH+GAP;

線を引く基準を決めるのに一番迷った

回と回を線で結びたいのですが、「何をもってつながりとするか」で3回やり直しました。

最初は、番組が過去回に言及した言い方を正規表現で拾っていました。

REF = re.compile(r"前回|前に|以前|昔|過去|の回で|シリーズ|覚えてます")

これだと、番組の造語が前置きなしにそのまま出てくる箇所を全部落とします。 「デカルトみ」は9本に出てくるのに、1本しか拾えませんでした。

それで逆に、造語の再登場も全部線にしてみたのですが、今度は線だらけになりました。 「デカルトみ」と言っただけの回と、「デカルトの回でやったやつ」と言った回は、意味が違います。

いまは、線にするのは過去回を明言した箇所だけにしています。

○ 「ソシュールの回思い出すと」        → 線にする
○ 「前になんかの回で言いそびれた」     → 線にする
× 「取らないで俺のデカルトみ」        → 線にしない(過去回を指していない)

探すのは2通りでやって、最後は1件ずつ前後を読んで決めています。 機械で確定させると、この区別ができませんでした。

線のラベルは、番組の発言そのままにした

ここも作り直しました。最初はラベルに相手の回のタイトルを出していたのですが、 「どの回につながっているか」は分かっても、なぜつながっているのかが分かりません。

いまは、番組がそのとき実際に言った文をそのまま出しています。

"threads": [
  ("it48QrP9KfY", "Y-g5cxcjsU4", "させていただくの話",
   "過去に「させていただく」のような話をした回でも触れたんですけれども"),
  ("7vNhgWgcuuo", "MR_gz2U7UvY", "「っす」の話", "だから前回も"),
]
# (どの回から, どの回へ, 短いラベル, 番組が実際に言った文)

4つ目を足したことで、線をたどる意味が変わりました。 「この2本がつながっている」ではなく「この人がこう言って前の回を呼び出した」が見えます。

あと、語を一度も言っていないのに他の回から名指しされる回があります。 これは extra として別に置いて、線の相手として出しています。

ハマったところ: 拡大の中でカードを掴むと、飛ぶ

カードは手で動かせるようにしました。位置を変えたくなるからです。 ここで座標の取り方を間違えて、掴んだ瞬間にカードが飛びました。

盤面全体に transform: scale() がかかっています。 getBoundingClientRect() は変形後の座標を返すので、 変形前の left/top に代入すると位置がずれます。

// ❌ 変形後の座標を、変形前の値として使ってしまう
var r = card.getBoundingClientRect();
dragging = {l: r.left, t: r.top};

// ✅ 変形前の座標を使い、移動量のほうを倍率で割る
dragging = {l: card.offsetLeft, t: card.offsetTop, px: e.clientX, py: e.clientY};

function onMove(e){
  var dx = (e.clientX - dragging.px) / Z;   // Z は現在の拡大率
  var dy = (e.clientY - dragging.py) / Z;
}

ハマったところ: 画像を掴むとブラウザが持っていく

カードにサムネイルを載せたら、そこを掴んだときだけ動かなくなりました。 ブラウザが「画像のドラッグ」として処理してしまうためです。

.mcard, .mcard .th, .mcard .ti {
  -webkit-user-drag: none;
  user-select: none;
}
card.addEventListener('dragstart', function(e){ e.preventDefault(); });

掴んで動かしたなら、それは動かしたいということなので、そちらを優先しました。

ハマったところ: クリックと移動の区別

カードはクリックすると詳細が出ます。ドラッグも同じカードで受けるので、 少し動いただけで詳細が出なくなると困ります。

if(!movedFar && Math.abs(dx)*Z < 5 && Math.abs(dy)*Z < 5) return;   // 5px 未満は無視

指の場合はさらに面倒で、スクロールと取り合いになります。 指では「配置を動かす」モードに入っているときだけドラッグを受けるようにしました。

if(e.pointerType==='touch' && !dragOn) return;

まとめ

  • 時間を横軸にする図は、空白期間の扱いを決めないと読めない。上限を付けて詰める
  • つながりを引く基準は、機械で決めきれなかった。明言だけを線にして、残りは自分の目で見る
  • 線のラベルは相手のタイトルではなく、実際の発言を出すほうが意味が伝わる
  • scale() の中でドラッグするなら offsetLeft を使い、移動量を倍率で割る

← 「この単語、もっと出てたはずなんだけど」を機械で探す… 番組の言葉を年表にする。正規表現とSQLを組み合わせ… → 目次へ

ゆる言語学ラジオ・ゆるコンピュータ科学ラジオ 公認/株式会社pedantic の了承と、運営者・安東暁史との合意に基づく協力体制で運営しています。ドメイン yururadio.jp は株式会社pedanticが保有。開発・保守: 安東暁史·このサイトについて·お問い合わせ