テーブルから一歩進んだ詳細表示が欲しい

前回までで、信用残データのシグナル計算とフィルタ・ソート可能なテーブル表示ができるようになりました。ただ、テーブルだけでは「この銘柄の売残・買残がどう推移してきたか」という時系列の流れが見えません。

そこで、テーブルの行(銘柄)をクリックすると詳細モーダルがポップアップし、その中で売残高・買残高・取組比率の推移グラフと、できれば実際の株価チャートまで確認できるようにしたい、という方向で機能を追加していきました。

モーダルウィンドウの基本構成

モーダルには以下の要素を詰め込みました。

モーダル内のコンテンツ

  • 銘柄名・コード・市場・データ公表日
  • 日足チャート(TradingView埋め込み)
  • 主要指標カード(売残高・買残高・取組比率・各スコアなど)
  • シグナルタグの一覧
  • 売残高・買残高推移、取組比率推移、売残前日比、一般信用/制度信用の内訳をChart.jsで描画

Chart.jsによる推移グラフ自体は、蓄積したCSV履歴をその銘柄の分だけ抜き出して描画するだけなので、データ構造が整っていれば比較的すんなり実装できました。一方、TradingViewの日足チャート埋め込みは予想以上に試行回数がかかりました。

TradingViewチャートを埋め込む3つの方法を試した

株価チャートを表示する方法として、まず以下の選択肢を検討しました。

方法メリットデメリット
TradingViewウィジェット 無料・日本株対応・見た目が良い 埋め込み方式の検証が必要
Yahoo!ファイナンス画像URL シンプルな画像埋め込み 仕様変更に弱い・取得できないケースあり
株探・みんかぶへのリンクボタン 確実に動く・実装が簡単 埋め込み表示ではなく別タブに飛ぶ

結論として、TradingViewウィジェットをメインで埋め込み、株探・みんかぶへのリンクボタンを併設するという方針にしました。チャート自体の見やすさはTradingViewが圧倒的に優れていますが、後述するように一部の銘柄コードでは表示できないことがあるため、保険としてリンクボタンも残しています。

innerHTMLにscriptを書いても動かない問題

TradingViewのウィジェットは、公式が提供している埋め込みコードをそのままページに貼ると動作します。しかし今回はモーダルを開いたタイミングで動的にチャートを生成する必要があったため、JavaScriptで innerHTML にウィジェットのコードを丸ごと挿入する方法を最初に試しました。

// うまくいかなかった実装
wrap.innerHTML = `
  <div class="tradingview-widget-container">
    <div class="tradingview-widget-container__widget"></div>
    <script src="https://s3.tradingview.com/external-embedding/embed-widget-advanced-chart.js" async>
    { "symbol": "TSE:7203", "interval": "D" }
    </script>
  </div>`;

結果はチャートが表示されないままでした。ブラウザの仕様として、innerHTML でDOMに挿入した<script>タグは実行されません。これはHTML仕様上の安全対策で、意図しないスクリプトの実行を防ぐためのものです。今回は完全にこの制約にハマっていました。

正しい方法は、document.createElement("script") でscript要素を生成し、appendChild() でDOMツリーに追加することです。この方法でDOMに追加されたscriptは正しく実行されます。

const container = document.getElementById("tv-container");
const sc = document.createElement("script");
sc.type = "text/javascript";
sc.async = true;
sc.innerHTML = JSON.stringify({
  symbol: tvSymbol,
  interval: "D",
  timezone: "Asia/Tokyo",
  theme: "dark",
  // ...その他設定
});
sc.src = "https://s3.tradingview.com/external-embedding/embed-widget-advanced-chart.js";
container.appendChild(sc); // これで初めて実行される

ここでもう一つ重要なポイントがありました。設定値(JSON)の渡し方は、sc.textContent ではなく sc.innerHTML に設定する必要があります。TradingView側のスクリプトはロード後に自分自身のinnerHTMLを読み取って設定値として解釈する実装になっているため、textContentで設定すると正しく反映されませんでした。地味ながら、ここで時間を取られたポイントです。

高さが効かない問題:autosizeの落とし穴

スクリプトの動的読み込み自体は解決したものの、次に直面したのが「チャートの高さがCSSで指定しても全く反映されない」という問題でした。コンテナにheight: 840pxと指定しても、実際に表示されるチャートはなぜか別の高さになってしまいます。

原因はウィジェットの設定オブジェクトにある autosize: true でした。この設定が有効な場合、TradingViewのウィジェットは親要素のCSSによる高さ指定を無視して自律的にリサイズする仕様になっています。CSSをどれだけ調整しても効かなかったのは、そもそも見ているパラメータが違ったためでした。

解決策はautosize: falseに変更し、設定オブジェクト内にwidthheightを明示的に指定することです。

const tvConf = {
  autosize: false,   // ← これがポイント
  symbol: tvSymbol,
  interval: "D",
  width: "100%",
  height: 600,       // 明示的に高さを指定
  timezone: "Asia/Tokyo",
  theme: "dark",
  style: "1",
  locale: "ja",
};

その後、ユーザーから「思っていたより大きすぎる」というフィードバックがあり、最終的にheight: 450(最初に設定した600の4分の3)に調整しました。サイズ調整自体は数値を変えるだけなので簡単ですが、「そもそもどの設定値が効いているのか」を正しく把握するのが今回のポイントでした。

銘柄コード変換の見落とし

第1回の記事でも触れましたが、JPXのデータの銘柄コードは末尾に0が付いた5桁形式(14190135A0)で、TradingViewや株探などの外部サイトに渡す際は末尾を取り除いた4桁(1419135A)にする必要があります。

最初の実装では、この変換ロジックを「数字のみ5桁」というパターンでしか判定していませんでした。

// 不完全だった実装
function dispCode(code) {
  if (/^\d{5}$/.test(code)) return code.slice(0, 4);
  return code; // アルファベット入りはそのまま →ここがバグ
}

これだと 135A0 のようなアルファベットを含む新コード体系の銘柄が変換されず、モーダルに「コード:135A0」と5桁のまま表示されてしまい、株探やみんかぶへのリンクも見つからないURLを生成してしまいました。

修正は「全コードの末尾は必ず0なので、単純に末尾1文字を削除する」というルールに統一することでした。実データを確認したところ、例外なく全コードの末尾が0だったため、これでシンプルかつ確実に解決できました。

// 修正後:パターンマッチではなく「末尾0を削除」に統一
function dispCode(code) {
  if (!code) return code;
  return code.endsWith('0') ? code.slice(0, -1) : code;
}

// TradingView用:英数字混在4桁も許可
function tvCode(code) {
  if (!code) return null;
  const dc = dispCode(code);
  return /^[0-9A-Z]{4}$/.test(dc) ? dc : null;
}

「パターンを場当たり的に増やす」よりも「データの実態から共通ルールを見つけて一本化する」ほうが、結果的にバグが減るという、よくある教訓を再確認した場面でした。

TradingViewの通知ポップアップについて

一部の銘柄では、TradingViewウィジェット自体が「このシンボルはTradingView上でのみ利用可能です」という通知を表示することがあります。これはiframe内で発生するポップアップのため、親ページのJavaScriptからは(Same-Origin Policyの制約により)操作できません。自動で閉じることはできないため、チャート見出しに「通知が出た場合は×を押してください」という案内テキストを添えて対応しています。

レイアウトの試行錯誤:ヘッダ被り・横スクロール

モーダルやチャート以外にも、ダッシュボード全体のレイアウトでいくつか細かい修正を重ねました。代表的なものを紹介します。

テーブルヘッダのstickyが原因でコンテンツと被る

テーブルのヘッダ行をposition: stickyで固定し、スクロールしても見えるようにしていましたが、ページ全体のヘッダ(ロゴなどがあるグローバルヘッダ)と固定位置がずれてしまい、データ行と重なって見えてしまう問題が発生しました。

参考にした既存のHTML実装を見直したところ、そちらではテーブルヘッダ自体をstickyにしない構成になっていました。シンプルにstickyを外し、テーブル全体をラップする要素の中にページャー(ページ送り)を内包する構成に変更することで解決しました。

ページ送りでスクロール位置が変わってしまう

ページネーションのボタンをクリックした際、window.scrollTo()を呼んでページ上部にスクロールする処理を入れていましたが、これが「ページを送るたびに画面が動いて見にくい」という指摘につながりました。該当の処理を削除し、ページ切り替え時はスクロール位置を一切変更しないようにしています。

スマホで見たときにテーブルが崩れる

列数が多いテーブルをスマートフォンの狭い画面で見ると、文字が詰まって読みにくくなります。テーブルをラップする要素にoverflow-x: autoを指定し、テーブル自体にはmin-widthを設定することで、スマホでは横スクロールで全列を確認できるようにしました。

.tscroll {
  overflow-x: auto;
  -webkit-overflow-scrolling: touch; /* iOSのスクロール挙動改善 */
}
table {
  width: 100%;
  min-width: 820px; /* これより狭い画面では横スクロールが発生 */
}

細かい調整の積み重ねですが、こうした地味な修正を重ねることで、実際に使いやすいダッシュボードに近づいていきました。データの自動収集から需給シグナルの計算、そして見やすいUIまで、シリーズを通して一通りの仕組みが完成しました。今後はデータが積み上がってきた段階で、シグナルの精度や実際の株価との相関を検証する記事も書いていきたいと思います。

FAQ

TradingViewのウィジェットをinnerHTMLで挿入してもscriptが動かないのはなぜですか?

innerHTMLでscriptタグを含むHTMLを挿入しても、ブラウザの仕様上そのscriptは実行されません。document.createElement('script')でscript要素を生成し、appendChildでDOMに追加することで初めて実行されます。

TradingViewウィジェットの高さがCSSで指定しても反映されないのはなぜですか?

ウィジェット設定でautosize: trueを指定していると、コンテナのCSSによる高さ指定を無視して自律的にリサイズします。autosize: falseに変更し、設定オブジェクト内でwidthheightを明示的に指定することで解決します。

アルファベットを含む銘柄コードでTradingViewチャートが表示されないことがあるのはなぜですか?

TradingViewへ渡すシンボルの判定が数字4桁のみに限定されていたためです。判定の正規表現を英数字混在の4桁([0-9A-Z]{4})に拡張することで、新しいコード体系の銘柄にも対応できます。ただし対応していないシンボルの場合は通知が出るため、外部サイト(株探・みんかぶ)へのリンクも併用しています。