fitbitの睡眠データをPCで確認できるようにしたくてGeminiに頼み込んだ

fitbit varsa2を2022年から使っている。

健康への意識が高まってきて買った初めてのスマートウォッチ。Apple Watchは高いし毎日充電しなきゃいけないが、こいつは15000円ほどでバッテリーも数日は余裕で持つという優れものだった。支払いはできない機種だが問題ない。主な目的は睡眠トラッカー機能。pipboyみがあって最初は実際にfallout4のあいつを待ち受けにしていたこともあったけど、すぐに飽きたので飼っていたペットの写真を待ち受けに変えた。今はそのペットもいなくなってしまったけど時計を見ればいつまでもかわいい我が子が画面にいる。

 

睡眠トラッカー機能としては優秀だと言って差し支えないだろう。もっとも正しく記録されているかどうかを知る由はないが。基本的にはfitbitのアプリを通して記録を確認するのだが、データはPCでも確認できるという話だった。しかしfitbitがGoogleに買収されてからというもののアプリのデザインが大きく変わったりPC上で見れたダッシュボードも気付けばサービス終了してしていた。

 

4年分のデータを今更使いづらくて反映も遅いアプリでちまちま見るなんて耐えられたものではない。そんな苦行はこの令和に許されない。調べてみるとどうやら

https://www.fitbit.com/settings/profile からログインすればデータのエクスポートができるということでデータを回収しに行った。左に表示されてるエクスポートに飛ぶとgoogleの画面に飛ばされたが、無事データがダウンロードできる。このあとGeminiにどうしたらエクセルで睡眠データをもとにグラフを作れるか聞いたらjsonファイルをCSVにするといいと言われたので言われるがままにスプレッドシートにjsonファイルをCSVに変換できるアドオンを入れ、CSVに変換した。セルの順番がばらばらで焦ったが、行の並び替えで事なきを得た。なおこの時フィルターで並び替えると列がばらばらになってしまうので注意してほしい。

 

該当のCSVを入手したがここで困ったことが起こった。fitbitは睡眠中に少しでも動きがあると体が起きてた的な記録をとったり1時間以上の昼寝を記録したりする。つまり処理しなければいけないデータが膨大なのだ。プログラマーでも何でもない素人には手に余るデータを慣れないエクセルでグラフ作りなんて割に合わないと気付いた。

 

ふとGeminiに頼んだらどうなるのか気になった。もしかして出来のいいビューワーを作れるのでは?うまくできなかったとしてもこちらにはなんの労力もいらない。聞くだけタダってやつだ。

 

fitbit ダッシュボードと同じように表示させたいので自分のpc上で見れるようなビューワーアプリケーションを作成してください。

 

はじめこそ作成したCSVをうまく読み込んでくれなかったが何回かのやりとりのあとちゃんと動作するビューワーのコードを書いてくれた。コードといってもHTMLでメモ帳にコピペし、拡張子をhtmlに変えるだけで使えるお手軽オフラインアプリだ。それがこちら(コードは最後に記載)

 

CSVを読み込むと平均睡眠時間を表示してくれるだけのものだが、この画面を初めて見たとき感動してしまった。1時間くらい唸りながらなにもできなかった自分とちがいGeminiはいとも簡単にこちらの望む品を提供してくれたのだ。(AIイラスト関連は断固反対派だが)

年毎の月平均が見れるようになったのならもっと作れるはず。そうしてできたのがこちら。

月の昼寝時間、日々の就寝時間起床時間のグラフ、表、月の平均就寝時間と起床時間などとりあえず必要最低限なものを追加した。私自身が完全夜型人間のためグラフの目盛りが0時スタートなので人によっては使いづらい恐れがあるのは申し訳ない。文字も小さいかもしれないけどそれはまた今度。

 

fitbitダッシュボード難民の人々がもしいたらぜひ使ってほしい。感想は別にいらないので不満があったら勝手に改良して頑張ってくれ。今回は睡眠に特化したビューワーになっているが、きっとGeminiに聞けば運動だのなんだののビューワーも作ってくれることだろう。

 

fitbit viewer 使い方

https://www.fitbit.com/settings/profile にアクセスして、エクスポートデータを入手。

・ダウンロードしたzipを展開し、「Global Export Data」内にあるsleepデータを全部コピー。

・google スプレッドシートを開き、jsonのインポートができるアドオンを追加。アドオンからsleepのjsonファイルをすべてインポートし、csvで出力、保存。

・メモ帳に以下のコードをコピペして保存。拡張子を .txt から .html に変える。

・ブラウザで開き、出力したcsvを選択。

・グラフが表示されれば成功

 

ということでした。最初のコードはいろいろなグラフが見れるスタンダード版。その次に月平均しか見れない簡易版。

 

fitbit viewer May

  1. <!DOCTYPE html>
  2. <html lang="ja">
  3. <head>
  4.     <meta charset="UTF-8">
  5.     <title>Fitbit 睡眠アナリティクスダッシュボード</title>
  6.     <style>
  7.         body { font-family: 'Segoe UI', Meiryo, sans-serif; background-color: #f4f7f9; padding: 20px; color: #333; margin: 0; }
  8.         .container { max-width: 1500px; margin: 0 auto; padding: 10px; }
  9.         .header { text-align: center; margin-bottom: 20px; }
  10.         .header h1 { color: #114b5f; margin-bottom: 5px; }
  11.         
  12.         /* ファイル選択エリア */
  13.         .upload-box {
  14.             background: #e8f5e9; border: 2px dashed #2e7d32; padding: 25px; border-radius: 12px; text-align: center; margin-bottom: 25px;
  15.         }
  16.         .file-input-wrapper { margin-top: 12px; }
  17.         input[type=\"file\"] { padding: 10px; background: white; border: 1px solid #a5d6a7; border-radius: 6px; cursor: pointer; }
  18.  
  19.         .card { background: white; border-radius: 16px; padding: 20px; margin-bottom: 20px; box-shadow: 0 4px 12px rgba(0,0,0,0.06); }
  20.         .controls { display: flex; flex-wrap: wrap; gap: 15px; align-items: center; background: #eef1f6; padding: 15px 20px; border-radius: 12px; }
  21.         .controls h2 { margin: 0; font-size: 18px; color: #114b5f; }
  22.         .control-group { display: flex; align-items: center; gap: 8px; }
  23.         select { padding: 8px 14px; font-size: 14px; border-radius: 6px; border: 1px solid #ccc; background: white; font-weight: bold; color: #333; }
  24.         
  25.         /* 左右レイアウト構成 */
  26.         .grid { display: grid; grid-template-columns: 1fr; gap: 20px; }
  27.         @media(min-width: 1100px) { .grid { grid-template-columns: 50% 50%; } }
  28.         
  29.         /* グラフエリアの縦並び配置 */
  30.         .charts-stack { display: flex; flex-direction: column; gap: 20px; }
  31.         
  32.         /* グラフ用共通スタイル(垂直バー) */
  33.         .bar-chart { display: flex; align-items: flex-end; border-bottom: 2px solid #ddd; padding: 10px 0; gap: 6px; }
  34.         .bar-wrapper { flex: 1; display: flex; flex-direction: column; align-items: center; height: 100%; justify-content: flex-end; cursor: pointer; }
  35.         .bar { width: 80%; background: #a8dadc; border-radius: 4px 4px 0 0; min-height: 1px; transition: background 0.2s; }
  36.         .bar-wrapper:hover .bar { background: #457b9d; }
  37.         .bar-wrapper.active .bar { background: #1d3557; }
  38.         .bar-nap { background: #e9c46a; }
  39.         .bar-wrapper:hover .bar-nap { background: #e6b12a; }
  40.         .bar-value { font-size: 10px; color: #457b9d; margin-bottom: 4px; font-weight: bold; text-align: center; }
  41.         .bar-value-nap { color: #b7840a; }
  42.         .bar-label { font-size: 12px; color: #666; margin-top: 6px; }
  43.         
  44.         /* 時刻チャート用スタイル(水平タイムライン) */
  45.         .time-chart-container { position: relative; border-left: 2px solid #ddd; border-bottom: 2px solid #ddd; padding: 10px 0; margin-top: 10px; background: #fafafa; }
  46.         .time-chart-row { display: flex; height: 45px; align-items: center; position: relative; border-bottom: 1px dashed #eee; }
  47.         .time-chart-label { width: 65px; font-size: 12px; text-align: center; color: #333; font-weight: bold; z-index: 6; }
  48.         .time-chart-line-area { flex: 1; position: relative; height: 100%; }
  49.         
  50.         /* 就寝・起床の結合線 */
  51.         .time-duration-line { position: absolute; height: 8px; top: 18px; background: rgba(69, 123, 157, 0.25); border-radius: 4px; z-index: 2; pointer-events: none; }
  52.         
  53.         /* 平均睡眠時間の文字バッジ */
  54.         .time-duration-text { position: absolute; top: -2px; font-size: 9px; font-weight: bold; color: #1d3557; background: #e8f0fe; padding: 1px 5px; border-radius: 4px; transform: translateX(-50%); z-index: 4; white-space: nowrap; border: 1px solid #b3d7ff; }
  55.  
  56.         .time-dot { position: absolute; height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center; transform: translateX(-50%); z-index: 5; }
  57.         .dot-symbol { font-size: 14px; line-height: 1; cursor: pointer; }
  58.         .dot-bed { color: #1d3557; }
  59.         .dot-wake { color: #e9c46a; }
  60.         .time-text-val { font-size: 9px; font-weight: bold; margin-top: 2px; background: rgba(255,255,255,0.9); padding: 1px 4px; border-radius: 3px; white-space: nowrap; box-shadow: 0 1px 3px rgba(0,0,0,0.15); }
  61.         .time-chart-axis-header { display: flex; position: relative; height: 20px; font-size: 10px; color: #888; border-bottom: 1px solid #ccc; margin-left: 65px; }
  62.  
  63.         /* 右カラム・タイムライン */
  64.         .timeline-container { border: 1px solid #ddd; border-radius: 8px; padding: 10px; background: #fff; max-height: 480px; overflow-y: auto; margin-bottom: 20px; }
  65.         .axis-label-row { display: flex; position: relative; height: 25px; margin-left: 50px; margin-right: 15px; border-bottom: 1px solid #ccc; font-size: 11px; color: #777; }
  66.         .timeline-row { display: flex; align-items: center; border-bottom: 1px solid #f0f0f0; padding: 6px 0; position: relative; height: 35px; }
  67.         .day-label { width: 50px; font-size: 12px; font-weight: bold; color: #555; }
  68.         .time-axis-bg { position: absolute; left: 50px; right: 15px; top: 0; bottom: 0; pointer-events: none; }
  69.         .axis-line { position: absolute; top: 0; bottom: 0; border-left: 1px dashed #e5e5e5; }
  70.         .sleep-bar-canvas { position: relative; flex: 1; height: 100%; margin-right: 15px; margin-left: 50px; }
  71.         .sleep-segment { position: absolute; height: 24px; top: 5px; border-radius: 4px; font-size: 10px; color: white; display: flex; align-items: center; justify-content: center; overflow: hidden; white-space: nowrap; min-width: 4px; cursor: pointer; }
  72.         .sleep-segment .tooltip, .time-dot .tooltip { display: none; position: absolute; background: rgba(17, 75, 95, 0.95); color: #fff; padding: 8px 12px; border-radius: 6px; bottom: 35px; left: 50%; transform: translateX(-50%); z-index: 999; font-size: 11px; white-space: nowrap; line-height: 1.4; box-shadow: 0 4px 8px rgba(0,0,0,0.2); }
  73.         .sleep-segment:hover .tooltip, .time-dot:hover .tooltip { display: block; }
  74.         .segment-main { background: #457b9d; }
  75.         .segment-nap { background: #e9c46a; color: #333; font-weight: bold; }
  76.         
  77.         .legend { display: flex; gap: 15px; font-size: 12px; justify-content: flex-end; margin-bottom: 10px; }
  78.         .legend-item { display: flex; align-items: center; gap: 5px; }
  79.         .box-main { width: 12px; height: 12px; background: #457b9d; border-radius: 2px; }
  80.         .box-nap { width: 12px; height: 12px; background: #e9c46a; border-radius: 2px; }
  81.  
  82.         /* テーブル容器(11準拠: 全件フラット表示) */
  83.         .table-container { border: 1px solid #dee2e6; border-radius: 8px; overflow: hidden; }
  84.         .sleep-table { width: 100%; border-collapse: collapse; text-align: left; font-size: 14px; }
  85.         .sleep-table th { background-color: #457b9d; color: white; padding: 10px; }
  86.         .sleep-table td { padding: 10px; border-bottom: 1px solid #dee2e6; }
  87.         .sleep-table tr:nth-child(even) { background-color: #f8f9fa; }
  88.  
  89.         #debugLogBox { background: #222; color: #7f7; font-family: monospace; padding: 12px; border-radius: 8px; margin-top: 20px; font-size: 12px; max-height: 120px; overflow-y: auto; white-space: pre-wrap; border: 2px solid #555; }
  90.         .badge-main { background: #457b9d; color: white; padding: 2px 6px; border-radius: 4px; font-size: 12px; font-weight: bold; }
  91.         .badge-nap { background: #e9c46a; color: #333; padding: 2px 6px; border-radius: 4px; font-size: 12px; font-weight: bold; }
  92.     </style>
  93. </head>
  94. <body>
  95.  
  96. <div class="container">
  97.     <div class="header">
  98.         <h1>? Fitbit 睡眠総合マルチアナリティクス</h1>
  99.         <p>すべての分析グラフを一画面に同時集約。時間軸を0時基点に統合した完全表示ダッシュボードです。</p>
  100.     </div>
  101.  
  102.     <div class="upload-box" id="uploadArea">
  103.         <p style="margin-top:0; font-weight:bold; color:#1b5e20;">? 【使い方】Fitbit の csvファイルを選択してください。</p>
  104.         <div class="file-input-wrapper">
  105.             <input type="file" id="csvFileInput" accept=".csv">
  106.         </div>
  107.     </div>
  108.  
  109.     <div id="mainDashboard" style="display: none;">
  110.         
  111.         <div class="card controls">
  112.             <h2>? データ期間切り替え</h2>
  113.             <div style="margin-left: auto; display: flex; gap: 15px; align-items: center;">
  114.                 <div>
  115.                     <label for="yearSelector">基準年:</label>
  116.                     <select id="yearSelector"></select>
  117.                 </div>
  118.                 <div>
  119.                     <label for="monthSelector">分析対象月:</label>
  120.                     <select id="monthSelector"></select>
  121.                 </div>
  122.                 <span id="dataCountBadge" style="background: #114b5f; color: white; padding: 6px 14px; border-radius: 20px; font-size: 13px; font-weight:bold;">全 0 件</span>
  123.             </div>
  124.         </div>
  125.  
  126.         <div class="grid">
  127.             <div class="charts-stack">
  128.                 
  129.                 <div class="card">
  130.                     <h3 id="monthlyDurationTitle" style="margin-top: 0; font-size: 15px; color: #114b5f; font-weight: bold;">? 月別平均睡眠時間(本睡眠)</h3>
  131.                     <div class="bar-chart" id="monthlyChartContainer" style="height: 140px;"></div>
  132.                 </div>
  133.  
  134.                 <div class="card">
  135.                     <h3 id="monthlyNapTitle" style="margin-top: 0; font-size: 15px; color: #b7840a; font-weight: bold;">? 月別 平均昼寝・仮眠時間(1日あたり)</h3>
  136.                     <div class="bar-chart" id="monthlyNapChartContainer" style="height: 140px;"></div>
  137.                 </div>
  138.  
  139.                 <div class="card">
  140.                     <div style="display:flex; justify-content:space-between; align-items:center;">
  141.                         <h3 id="yearlyComparisonTitle" style="margin-top: 0; font-size: 15px; color: #114b5f; font-weight: bold;">? 各年の 平均就寝・起床・睡眠時間比較</h3>
  142.                         <div style="font-size:11px; display:flex; gap:10px;">
  143.                             <span>? 就寝</span> <span style="color:#457b9d;">? 平均睡眠</span> <span>? 起床</span>
  144.                         </div>
  145.                     </div>
  146.                     <div id="yearlyTimeChartAxis" class="time-chart-axis-header"></div>
  147.                     <div class="time-chart-container" id="yearlyTimeChartContainer"></div>
  148.                 </div>
  149.  
  150.                 <div class="card">
  151.                     <div style="display:flex; justify-content:space-between; align-items:center;">
  152.                         <h3 id="monthlyTimeTitle" style="margin-top: 0; font-size: 15px; color: #114b5f; font-weight: bold;">? 月別 平均就寝・起床・睡眠時間推移</h3>
  153.                         <div style="font-size:11px; display:flex; gap:10px;">
  154.                             <span>? 就寝</span> <span style="color:#457b9d;">? 平均睡眠</span> <span>? 起床</span>
  155.                         </div>
  156.                     </div>
  157.                     <div id="monthlyTimeChartAxis" class="time-chart-axis-header"></div>
  158.                     <div class="time-chart-container" id="monthlyTimeChartContainer"></div>
  159.                 </div>
  160.  
  161.                 <div class="card">
  162.                     <h3 id="clockTitle" style="margin-top: 0; font-size: 15px; color: #114b5f; font-weight: bold; text-align: center;">?? 睡眠サイクル(12時間表示・本睡眠のみ)</h3>
  163.                     <div style="text-align:center; padding-top:10px;"><canvas id="sleepClock" width="260" height="260" style="margin: 0 auto;"></canvas></div>
  164.                 </div>
  165.  
  166.             </div>
  167.  
  168.             <div class="charts-stack">
  169.                 
  170.                 <div class="card">
  171.                     <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:10px;">
  172.                         <h3 id="timelineTitle" style="margin-top: 0; font-size: 15px; color: #114b5f; font-weight: bold;">? 日別睡眠時間分布タイムライン(0時〜24時)</h3>
  173.                         <div class="legend">
  174.                             <div class="legend-item"><div class="box-main"></div><span>本睡眠</span></div>
  175.                             <div class="legend-item"><div class="box-nap"></div><span>昼寝・仮眠</span></div>
  176.                         </div>
  177.                     </div>
  178.                     <div class="timeline-container" id="timelineContainer"></div>
  179.                 </div>
  180.  
  181.                 <div class="card">
  182.                     <h3 style="margin-top: 0; font-size: 15px; color: #114b5f; font-weight: bold; margin-bottom: 12px;">? 就寝・起床時間 数値一覧(1ヶ月分・全表示)</h3>
  183.                     <div class="table-container">
  184.                         <table class="sleep-table">
  185.                             <thead>
  186.                                 <tr><th>日付</th><th>種類</th><th>就寝時刻</th><th>起床時刻</th><th>睡眠時間</th></tr>
  187.                             </thead>
  188.                             <tbody id="sleepTableBody"></tbody>
  189.                         </table>
  190.                     </div>
  191.                 </div>
  192.             </div>
  193.         </div>
  194.     </div>
  195.  
  196.     <div id="debugLogBox">? ログ:ファイルを読み込んでください...</div>
  197. </div>
  198.  
  199. <script>
  200.     let rawParsedData = ;
  201.     const standard24HoursAxis = [0, 3, 6, 9, 12, 15, 18, 21, 24];
  202.     const logBox = document.getElementById('debugLogBox');
  203.  
  204.     function log(msg) { logBox.innerHTML += "\n" + msg; logBox.scrollTop = logBox.scrollHeight; }
  205.  
  206.     document.getElementById('csvFileInput').addEventListener('change', function(e) {
  207.         const file = e.target.files[0];
  208.         if (!file) return;
  209.         log(`? ファイル「${file.name}」を読み込み中...`);
  210.         const reader = new FileReader();
  211.         reader.onload = function(evt) {
  212.             try { parseCSVRobust(evt.target.result.trim()); } catch(err) { log(`? 解析エラー: ${err.message}`); }
  213.         };
  214.         reader.readAsText(file, 'UTF-8');
  215.     });
  216.  
  217.     function splitCSVLine(text) {
  218.         let result = ; let inQuote = false; let token = '';
  219.         for (let i = 0; i < text.length; i++) {
  220.             let c = text[i];
  221.             if (c === '"') { inQuote = !inQuote; }
  222.             else if (c === ',' && !inQuote) { result.push(token.trim()); token = ''; }
  223.             else { token += c; }
  224.         }
  225.         result.push(token.trim());
  226.         return result;
  227.     }
  228.  
  229.     // ? 13.html準拠の堅牢なFitbit英語ヘッダーパース処理
  230.     function parseCSVRobust(text) {
  231.         const lines = text.split(/\r?\n/);
  232.         if (lines.length < 2) { log("? エラー: CSVファイルの中身が不正です。"); return; }
  233.         
  234.         // ヘッダー行を取得して小文字に統一
  235.         const headers = splitCSVLine(lines[0]).map(h => h.replace(/[\ufeff"']/g, '').trim().toLowerCase());
  236.         
  237.         const idxDate = headers.indexOf('dateofsleep');
  238.         const idxStart = headers.indexOf('starttime');
  239.         const idxEnd = headers.indexOf('endtime');
  240.         const idxMins = headers.indexOf('minutesasleep');
  241.         const idxMain = headers.indexOf('mainsleep');
  242.  
  243.         if (idxDate === -1 || idxStart === -1 || idxEnd === -1) {
  244.             log(`? エラー: 必要な列名(dateOfSleep, startTime, endTime)が見つかりません。`);
  245.             alert("睡眠データが見つかりませんでした。Fitbit形式の正しいCSVか確認してください。");
  246.             return;
  247.         }
  248.  
  249.         rawParsedData = ;
  250.         for (let i = 1; i < lines.length; i++) {
  251.             const line = lines[i].trim(); if (!line) continue;
  252.             const cells = splitCSVLine(line); if (cells.length <= Math.max(idxDate, idxStart, idxEnd)) continue;
  253.             const clean = (idx) => (idx !== -1 && cells[idx]) ? cells[idx].replace(/["']/g, '').trim() : '';
  254.             
  255.             rawParsedData.push({
  256.                 dateOfSleep: clean(idxDate), startTime: clean(idxStart), endTime: clean(idxEnd),
  257.                 minutesAsleep: parseFloat(clean(idxMins)) || 0,
  258.                 mainSleep: clean(idxMain) ? clean(idxMain).toLowerCase() : 'true'
  259.             });
  260.         }
  261.         
  262.         if(rawParsedData.length > 0) {
  263.             log(`? 成功: ${rawParsedData.length} 件のデータを読み込みました。`);
  264.             initDashboard();
  265.         } else {
  266.             alert("睡眠データが見つかりませんでした。");
  267.         }
  268.     }
  269.  
  270.     function safeParseDate(str) {
  271.         if (!str) return null;
  272.         try {
  273.             let clean = str.toLowerCase().replace('t', ' ').replace(/\//g, '-').trim();
  274.             const isPM = clean.includes('pm'); const isAM = clean.includes('am');
  275.             clean = clean.replace('pm', '').replace('am', '').trim();
  276.             const parts = clean.split(' '); const dateParts = parts[0].split('-');
  277.             let hour = 0, min = 0;
  278.             if (parts.length > 1) {
  279.                 const timeParts = parts[1].split(':');
  280.                 hour = parseInt(timeParts[0], 10) || 0; min = parseInt(timeParts[1], 10) || 0;
  281.                 if (isPM && hour < 12) hour += 12; if (isAM && hour === 12) hour = 0;
  282.             }
  283.             return new Date(parseInt(dateParts[0], 10), parseInt(dateParts[1], 10) - 1, parseInt(dateParts[2], 10), hour, min, 0);
  284.         } catch(e) { return null; }
  285.     }
  286.  
  287.     function calculateCircularAverage(dateObjects) {
  288.         if (!dateObjects || dateObjects.length === 0) return "--:--";
  289.         let sumSin = 0; let sumCos = 0;
  290.         dateObjects.forEach(dt => {
  291.             const minutes = dt.getHours() * 60 + dt.getMinutes();
  292.             const angle = (minutes / 1440) * 2 * Math.PI;
  293.             sumSin += Math.sin(angle); sumCos += Math.cos(angle);
  294.         });
  295.         let avgAngle = Math.atan2(sumSin, sumCos);
  296.         if (avgAngle < 0) avgAngle += 2 * Math.PI;
  297.         let avgMinutes = Math.round((avgAngle / (2 * Math.PI)) * 1440) % 1440;
  298.         const h = Math.floor(avgMinutes / 60); const m = avgMinutes % 60;
  299.         return `${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}`;
  300.     }
  301.  
  302.     function getAverageDurationStr(filteredRows) {
  303.         if(filteredRows.length === 0) return "";
  304.         const totalMins = filteredRows.reduce((sum, r) => sum + r.minutesAsleep, 0);
  305.         const avgMins = Math.round(totalMins / filteredRows.length);
  306.         return `${Math.floor(avgMins / 60)}時間${(avgMins % 60)}分`;
  307.     }
  308.  
  309.     function initDashboard() {
  310.         const yearsSet = new Set();
  311.         rawParsedData.forEach(row => {
  312.             const y = row.dateOfSleep.split(' ')[0].replace(/\//g, '-').split('-')[0];
  313.             if (y && y.length === 4 && !isNaN(y)) yearsSet.add(y);
  314.         });
  315.         const years = Array.from(yearsSet).sort();
  316.  
  317.         const yearSel = document.getElementById('yearSelector'); yearSel.innerHTML = '';
  318.         years.forEach(y => { yearSel.innerHTML += `<option value="${y}">${y}年</option>`; });
  319.         
  320.         const monthSel = document.getElementById('monthSelector'); monthSel.innerHTML = '';
  321.         for (let m = 1; m <= 12; m++) { monthSel.innerHTML += `<option value="${m}">${m}月</option>`; }
  322.  
  323.         yearSel.value = years[years.length - 1];
  324.         let latestMonth = 1;
  325.         for (let m = 12; m >= 1; m--) {
  326.             if (rawParsedData.some(row => {
  327.                 const p = row.dateOfSleep.split(' ')[0].replace(/\//g, '-').split('-');
  328.                 return p[0] === yearSel.value && parseInt(p[1], 10) === m;
  329.             })) { latestMonth = m; break; }
  330.         }
  331.         monthSel.value = latestMonth;
  332.  
  333.         const refresh = () => {
  334.             const y = yearSel.value; const m = monthSel.value;
  335.             document.getElementById('monthlyTimeTitle').textContent = `? ${y}年 月別 平均就寝・起床・睡眠時間推移`;
  336.             document.getElementById('monthlyDurationTitle').textContent = `? ${y}年 月別平均睡眠時間(本睡眠)`;
  337.             document.getElementById('monthlyNapTitle').textContent = `? ${y}年 月別 平均昼寝・仮眠時間(1日あたり)`;
  338.             
  339.             renderYearlyTimeComparisonChart(m);
  340.             renderMonthlyTimeChart(y);
  341.             renderMonthlyChart(y);
  342.             renderMonthlyNapChart(y);
  343.             renderTimeline(y, m);
  344.             renderSleepClock(y, m);
  345.             renderNumericalTable(y, m);
  346.         };
  347.         yearSel.onchange = refresh; monthSel.onchange = refresh;
  348.         
  349.         document.getElementById('dataCountBadge').textContent = `全 ${rawParsedData.length} 件`;
  350.         document.getElementById('mainDashboard').style.display = 'block'; // ここで表示させる
  351.         refresh();
  352.         log("? 読み込みに成功し、全ダッシュボードを展開しました。");
  353.     }
  354.  
  355.     const convertTimeToPercent24H = (timeStr) => {
  356.         if (timeStr === "--:--") return -1;
  357.         const [h, min] = timeStr.split(':').map(Number);
  358.         return ((h + (min/60)) / 24) * 100;
  359.     };
  360.  
  361.     // 1. 各年の比較
  362.     function renderYearlyTimeComparisonChart(month) {
  363.         document.getElementById('yearlyComparisonTitle').textContent = `? 各年の 「${month}月」 平均就寝・起床・睡眠時間比較`;
  364.         
  365.         const axisHeader = document.getElementById('yearlyTimeChartAxis'); axisHeader.innerHTML = '';
  366.         standard24HoursAxis.forEach(h => {
  367.             const pos = (h / 24) * 100;
  368.             axisHeader.innerHTML += `<div style="position:absolute; left:calc(${pos}% - 15px);">${h}:00</div>`;
  369.         });
  370.  
  371.         const container = document.getElementById('yearlyTimeChartContainer'); container.innerHTML = '';
  372.         const yearsSet = new Set(); rawParsedData.forEach(row => { const y = row.dateOfSleep.split(' ')[0].replace(/\//g, '-').split('-')[0]; if (y && y.length === 4) yearsSet.add(y); });
  373.         const allYears = Array.from(yearsSet).sort();
  374.  
  375.         allYears.forEach(year => {
  376.             const filtered = rawParsedData.filter(row => {
  377.                 const p = row.dateOfSleep.split(' ')[0].replace(/\//g, '-').split('-');
  378.                 return p[0] === year && parseInt(p[1], 10) === parseInt(month, 10) && row.mainSleep === 'true';
  379.             });
  380.  
  381.             let startDates = ; let endDates = ;
  382.             filtered.forEach(row => {
  383.                 const s = safeParseDate(row.startTime); const e = safeParseDate(row.endTime);
  384.                 if (s) startDates.push(s); if (e) endDates.push(e);
  385.             });
  386.  
  387.             const bedTimeStr = calculateCircularAverage(startDates);
  388.             const wakeTimeStr = calculateCircularAverage(endDates);
  389.             const avgDurationStr = getAverageDurationStr(filtered);
  390.  
  391.             const rowDiv = document.createElement('div'); rowDiv.className = 'time-chart-row';
  392.             rowDiv.innerHTML = `<div class="time-chart-label">${year}年</div>`;
  393.  
  394.             const lineArea = document.createElement('div'); lineArea.className = 'time-chart-line-area';
  395.             standard24HoursAxis.forEach(h => {
  396.                 const line = document.createElement('div'); line.className = 'axis-line';
  397.                 line.style.left = `${(h / 24) * 100}%`; lineArea.appendChild(line);
  398.             });
  399.  
  400.             const bedPos = convertTimeToPercent24H(bedTimeStr);
  401.             const wakePos = convertTimeToPercent24H(wakeTimeStr);
  402.  
  403.             if (bedPos >= 0 && wakePos >= 0) {
  404.                 if (bedPos > wakePos) {
  405.                     lineArea.innerHTML += `<div class="time-duration-line" style="left:${bedPos}%; width:${100 - bedPos}%;"></div>`;
  406.                     lineArea.innerHTML += `<div class="time-duration-line" style="left:0%; width:${wakePos}%;"></div>`;
  407.                     const centerPos = ((bedPos + (100 + wakePos)) / 2) % 100;
  408.                     lineArea.innerHTML += `<div class="time-duration-text" style="left:${centerPos}%;">${avgDurationStr}</div>`;
  409.                 } else {
  410.                     lineArea.innerHTML += `<div class="time-duration-line" style="left:${bedPos}%; width:${wakePos - bedPos}%;"></div>`;
  411.                     const centerPos = bedPos + (wakePos - bedPos) / 2;
  412.                     lineArea.innerHTML += `<div class="time-duration-text" style="left:${centerPos}%;">${avgDurationStr}</div>`;
  413.                 }
  414.             }
  415.  
  416.             if (bedPos >= 0 && bedPos <= 100) {
  417.                 lineArea.innerHTML += `<div class="time-dot" style="left:${bedPos}%;"><span class="dot-symbol dot-bed">?</span><span class="time-text-val" style="color:#1d3557;">${bedTimeStr}</span></div>`;
  418.             }
  419.             if (wakePos >= 0 && wakePos <= 100) {
  420.                 lineArea.innerHTML += `<div class="time-dot" style="left:${wakePos}%;"><span class="dot-symbol dot-wake">?</span><span class="time-text-val" style="color:#b79438;">${wakeTimeStr}</span></div>`;
  421.             }
  422.  
  423.             rowDiv.appendChild(lineArea); container.appendChild(rowDiv);
  424.         });
  425.     }
  426.  
  427.     // 2. 月別推移
  428.     function renderMonthlyTimeChart(year) {
  429.         const axisHeader = document.getElementById('monthlyTimeChartAxis'); axisHeader.innerHTML = '';
  430.         standard24HoursAxis.forEach(h => {
  431.             const pos = (h / 24) * 100;
  432.             axisHeader.innerHTML += `<div style="position:absolute; left:calc(${pos}% - 15px);">${h}:00</div>`;
  433.         });
  434.  
  435.         const container = document.getElementById('monthlyTimeChartContainer'); container.innerHTML = '';
  436.         
  437.         for (let m = 1; m <= 12; m++) {
  438.             const filtered = rawParsedData.filter(row => {
  439.                 const p = row.dateOfSleep.split(' ')[0].replace(/\//g, '-').split('-');
  440.                 return p[0] === year && parseInt(p[1], 10) === m && row.mainSleep === 'true';
  441.             });
  442.  
  443.             let startDates = ; let endDates = ;
  444.             filtered.forEach(row => {
  445.                 const s = safeParseDate(row.startTime); const e = safeParseDate(row.endTime);
  446.                 if (s) startDates.push(s); if (e) endDates.push(e);
  447.             });
  448.  
  449.             const bedTimeStr = calculateCircularAverage(startDates);
  450.             const wakeTimeStr = calculateCircularAverage(endDates);
  451.             const avgDurationStr = getAverageDurationStr(filtered);
  452.  
  453.             const rowDiv = document.createElement('div'); rowDiv.className = 'time-chart-row';
  454.             rowDiv.innerHTML = `<div class="time-chart-label">${m}月</div>`;
  455.  
  456.             const lineArea = document.createElement('div'); lineArea.className = 'time-chart-line-area';
  457.             standard24HoursAxis.forEach(h => {
  458.                 const line = document.createElement('div'); line.className = 'axis-line';
  459.                 line.style.left = `${(h / 24) * 100}%`; lineArea.appendChild(line);
  460.             });
  461.  
  462.             const bedPos = convertTimeToPercent24H(bedTimeStr);
  463.             const wakePos = convertTimeToPercent24H(wakeTimeStr);
  464.  
  465.             if (bedPos >= 0 && wakePos >= 0) {
  466.                 if (bedPos > wakePos) {
  467.                     lineArea.innerHTML += `<div class="time-duration-line" style="left:${bedPos}%; width:${100 - bedPos}%;"></div>`;
  468.                     lineArea.innerHTML += `<div class="time-duration-line" style="left:0%; width:${wakePos}%;"></div>`;
  469.                     const centerPos = ((bedPos + (100 + wakePos)) / 2) % 100;
  470.                     lineArea.innerHTML += `<div class="time-duration-text" style="left:${centerPos}%;">${avgDurationStr}</div>`;
  471.                 } else {
  472.                     lineArea.innerHTML += `<div class="time-duration-line" style="left:${bedPos}%; width:${wakePos - bedPos}%;"></div>`;
  473.                     const centerPos = bedPos + (wakePos - bedPos) / 2;
  474.                     lineArea.innerHTML += `<div class="time-duration-text" style="left:${centerPos}%;">${avgDurationStr}</div>`;
  475.                 }
  476.             }
  477.  
  478.             if (bedPos >= 0 && bedPos <= 100) {
  479.                 lineArea.innerHTML += `<div class="time-dot" style="left:${bedPos}%;"><span class="dot-symbol dot-bed">?</span><span class="time-text-val" style="color:#1d3557;">${bedTimeStr}</span></div>`;
  480.             }
  481.             if (wakePos >= 0 && wakePos <= 100) {
  482.                 lineArea.innerHTML += `<div class="time-dot" style="left:${wakePos}%;"><span class="dot-symbol dot-wake">?</span><span class="time-text-val" style="color:#b79438;">${wakeTimeStr}</span></div>`;
  483.             }
  484.  
  485.             rowDiv.appendChild(lineArea); container.appendChild(rowDiv);
  486.         }
  487.     }
  488.  
  489.     // 3. 月別平均睡眠時間
  490.     function renderMonthlyChart(year) {
  491.         const dailySum = {};
  492.         rawParsedData.forEach(row => {
  493.             const p = row.dateOfSleep.split(' ')[0].replace(/\//g, '-').split('-');
  494.             if (p[0] === year && row.mainSleep === 'true') {
  495.                 const f = `${p[0]}-${parseInt(p[1],10)}-${parseInt(p[2],10)}`;
  496.                 dailySum[f] = (dailySum[f] || 0) + row.minutesAsleep;
  497.             }
  498.         });
  499.         const monthStats = {}; for (let m = 1; m <= 12; m++) monthStats[m] = { total: 0, days: 0 };
  500.         for (const [date, mins] of Object.entries(dailySum)) {
  501.             const m = parseInt(date.split('-')[1], 10); if (monthStats[m]) { monthStats[m].total += mins; monthStats[m].days += 1; }
  502.         }
  503.         const container = document.getElementById('monthlyChartContainer'); container.innerHTML = '';
  504.         const activeMonth = parseInt(document.getElementById('monthSelector').value, 10);
  505.         for (let m = 1; m <= 12; m++) {
  506.             const avg = monthStats[m].days > 0 ? (monthStats[m].total / monthStats[m].days) / 60 : 0;
  507.             const hPercent = Math.min(100, (avg / 10) * 100);
  508.             const wrapper = document.createElement('div');
  509.             wrapper.className = 'bar-wrapper' + (m === activeMonth ? ' active' : '');
  510.             wrapper.onclick = () => { document.getElementById('monthSelector').value = m; document.getElementById('monthSelector').dispatchEvent(new Event('change')); };
  511.             wrapper.innerHTML = `<div class="bar-value">${avg > 0 ? avg.toFixed(1) + 'h' : ''}</div><div class="bar" style="height:${hPercent}%;"></div><div class="bar-label">${m}月</div>`;
  512.             container.appendChild(wrapper);
  513.         }
  514.     }
  515.  
  516.     // 4. 月別平均昼寝時間
  517.     function renderMonthlyNapChart(year) {
  518.         const monthNapSum = {}; for (let m = 1; m <= 12; m++) monthNapSum[m] = { totalMins: 0, recordCount: 0 };
  519.         rawParsedData.forEach(row => {
  520.             const p = row.dateOfSleep.split(' ')[0].replace(/\//g, '-').split('-');
  521.             if (p[0] === year && row.mainSleep === 'false') {
  522.                 const m = parseInt(p[1], 10);
  523.                 if (m >= 1 && m <= 12) {
  524.                     monthNapSum[m].totalMins += row.minutesAsleep;
  525.                     monthNapSum[m].recordCount += 1;
  526.                 }
  527.             }
  528.         });
  529.  
  530.         const container = document.getElementById('monthlyNapChartContainer'); container.innerHTML = '';
  531.         for (let m = 1; m <= 12; m++) {
  532.             const avgMins = monthNapSum[m].totalMins / 30;
  533.             const hPercent = Math.min(100, (avgMins / 60) * 100);
  534.             const wrapper = document.createElement('div');
  535.             wrapper.className = 'bar-wrapper';
  536.             let displayVal = "";
  537.             if (monthNapSum[m].totalMins > 0) {
  538.                 displayVal = avgMins >= 1 ? Math.round(avgMins) + '分' : '1分未満';
  539.             }
  540.             wrapper.innerHTML = `
  541.                 <div class="bar-value bar-value-nap">${displayVal}</div>
  542.                 <div class="bar bar-nap" style="height:${Math.max(1, hPercent)}%;"></div>
  543.                 <div class="bar-label">${m}月</div>
  544.             `;
  545.             container.appendChild(wrapper);
  546.         }
  547.     }
  548.  
  549.     // 5. 日別タイムライン
  550.     function renderTimeline(year, month) {
  551.         const container = document.getElementById('timelineContainer'); container.innerHTML = '';
  552.         const axisHeader = document.createElement('div'); axisHeader.className = 'axis-label-row';
  553.         standard24HoursAxis.forEach(h => {
  554.             const pos = (h / 24) * 100;
  555.             axisHeader.innerHTML += `<div style="position:absolute; left:calc(${pos}% - 15px);">${h}:00</div>`;
  556.         });
  557.         container.appendChild(axisHeader);
  558.  
  559.         const filtered = rawParsedData.filter(row => { const p = row.dateOfSleep.split(' ')[0].replace(/\//g, '-').split('-'); return p[0] === year && parseInt(p[1], 10) === parseInt(month, 10); });
  560.         const dayGroups = {};
  561.         filtered.forEach(row => { const d = parseInt(row.dateOfSleep.split(' ')[0].replace(/\//g, '-').split('-')[2], 10); if (!isNaN(d)) { if (!dayGroups[d]) dayGroups[d] = ; dayGroups[d].push(row); } });
  562.         const sortedDays = Object.keys(dayGroups).map(Number).sort*1;
  563.                 let e = Math.max(0, Math.min(24, eVal));
  564.                 if (e <= s) return;
  565.                 const isMain = row.mainSleep === 'true';
  566.                 
  567.                 const seg = document.createElement('div'); seg.className = `sleep-segment ${isMain?'segment-main':'segment-nap'}`;
  568.                 seg.style.left = `${(s / 24) * 100}%`; seg.style.width = `${*2 - (safeParseDate(b.startTime) || new Date()));
  569.  
  570.         if (filtered.length === 0) { tbody.innerHTML = `<tr><td colspan="5" style="text-align:center; color:#999; padding:20px;">データなし</td></tr>`; return; }
  571.  
  572.         filtered.forEach(row => {
  573.             const sDt = safeParseDate(row.startTime); const eDt = safeParseDate(row.endTime); if (!sDt || !eDt) return;
  574.             const dateStr = `${sDt.getFullYear()}/${(sDt.getMonth()+1).toString().padStart(2,'0')}/${sDt.getDate().toString().padStart(2,'0')}`;
  575.             const sTimeStr = `${sDt.getHours().toString().padStart(2,'0')}:${sDt.getMinutes().toString().padStart(2,'0')}`;
  576.             const eTimeStr = `${eDt.getHours().toString().padStart(2,'0')}:${eDt.getMinutes().toString().padStart(2,'0')}`;
  577.             const durationStr = `${Math.floor(row.minutesAsleep / 60)}時間${Math.round(row.minutesAsleep % 60)}分`;
  578.             const typeBadge = row.mainSleep === 'true' ? `<span class="badge-main">本睡眠</span>` : `<span class="badge-nap">仮眠</span>`;
  579.  
  580.             tbody.appendChild(document.createElement('tr')).innerHTML = `<td><strong>${dateStr}</strong></td><td>${typeBadge}</td><td>? ${sTimeStr}</td><td>? ${eTimeStr}</td><td>?? ${durationStr}</td>`;
  581.         });
  582.     }
  583.  
  584.     // 睡眠サイクル時計
  585.     function renderSleepClock(year, month) {
  586.         const filtered = rawParsedData.filter(row => { const p = row.dateOfSleep.split(' ')[0].replace(/\//g, '-').split('-'); return p[0] === year && parseInt(p[1], 10) === parseInt(month, 10) && row.mainSleep === 'true'; });
  587.         let startDates = ; let endDates = ;
  588.         filtered.forEach(row => { const s = safeParseDate(row.startTime); const e = safeParseDate(row.endTime); if (s) startDates.push(s); if (e) endDates.push(e); });
  589.         
  590.         const bedTime = calculateCircularAverage(startDates);
  591.         const wakeTime = calculateCircularAverage(endDates);
  592.         
  593.         const canvas = document.getElementById('sleepClock'); if (!canvas) return; const ctx = canvas.getContext('2d'); ctx.clearRect(0, 0, canvas.width, canvas.height);
  594.         if (bedTime === "--:--" || wakeTime === "--:--") { ctx.fillStyle = '#999'; ctx.font = '14px sans-serif'; ctx.textAlign = 'center'; ctx.fillText('データなし', canvas.width/2, canvas.height/2); return; }
  595.         
  596.         const cx = canvas.width / 2; const cy = canvas.height / 2; const radius = 95;
  597.         const h12ToRad = (h) => (h / 12) * 2 * Math.PI - Math.PI / 2;
  598.         
  599.         const bHrs = *3; }
  600.         
  601.         const drawHand = (hrs, color) => { const a = h12ToRad(hrs); ctx.beginPath(); ctx.moveTo(cx, cy); ctx.lineTo(cx + (radius - 28) * Math.cos(a), cy + (radius - 28) * Math.sin(a)); ctx.strokeStyle = color; ctx.lineWidth = 3.5; ctx.stroke(); };
  602.         drawHand(bHrs, '#1d3557'); drawHand(wHrs, '#e9c46a');
  603.         
  604.         ctx.beginPath(); ctx.arc(cx, cy, radius * 0.55, 0, 2 * Math.PI); ctx.fillStyle = '#ffffff'; ctx.fill(); ctx.strokeStyle = '#e9ecef'; ctx.stroke();
  605.         ctx.fillStyle = '#212529'; ctx.font = 'bold 12px sans-serif'; ctx.fillText(`${month}月 平均サイクル`, cx, cy - 14);
  606.         ctx.fillStyle = '#1d3557'; ctx.font = '11px sans-serif'; ctx.fillText(`就寝 ${bedTime}`, cx, cy + 6);
  607.         ctx.fillStyle = '#b7840a'; ctx.font = '11px sans-serif'; ctx.fillText(`起床 ${wakeTime}`, cx, cy + 20);
  608.     }
  609. </script>
  610. </body>
  611. </html>

 

 

 fitbit viewer simple

  1. <!DOCTYPE html>
  2. <html lang="ja">
  3. <head>
  4.     <meta charset="UTF-8">
  5.     <title>Fitbit 睡眠アナリティクスダッシュボード</title>
  6.     <style>
  7.         body { font-family: 'Segoe UI', Meiryo, sans-serif; background-color: #f4f7f9; padding: 20px; color: #333; margin: 0; }
  8.         .container { max-width: 1500px; margin: 0 auto; padding: 10px; }
  9.         .header { text-align: center; margin-bottom: 20px; }
  10.         .header h1 { color: #114b5f; margin-bottom: 5px; }
  11.         
  12.         /* ファイル選択エリア */
  13.         .upload-box {
  14.             background: #e8f5e9; border: 2px dashed #2e7d32; padding: 25px; border-radius: 12px; text-align: center; margin-bottom: 25px;
  15.         }
  16.         .file-input-wrapper { margin-top: 12px; }
  17.         input[type=\"file\"] { padding: 10px; background: white; border: 1px solid #a5d6a7; border-radius: 6px; cursor: pointer; }
  18.  
  19.         .card { background: white; border-radius: 16px; padding: 20px; margin-bottom: 20px; box-shadow: 0 4px 12px rgba(0,0,0,0.06); }
  20.         .controls { display: flex; flex-wrap: wrap; gap: 15px; align-items: center; background: #eef1f6; padding: 15px 20px; border-radius: 12px; }
  21.         .controls h2 { margin: 0; font-size: 18px; color: #114b5f; }
  22.         .control-group { display: flex; align-items: center; gap: 8px; }
  23.         select { padding: 8px 14px; font-size: 14px; border-radius: 6px; border: 1px solid #ccc; background: white; font-weight: bold; color: #333; }
  24.         
  25.         /* 左右レイアウト構成 */
  26.         .grid { display: grid; grid-template-columns: 1fr; gap: 20px; }
  27.         @media(min-width: 1100px) { .grid { grid-template-columns: 50% 50%; } }
  28.         
  29.         /* グラフエリアの縦並び配置 */
  30.         .charts-stack { display: flex; flex-direction: column; gap: 20px; }
  31.         
  32.         /* グラフ用共通スタイル(垂直バー) */
  33.         .bar-chart { display: flex; align-items: flex-end; border-bottom: 2px solid #ddd; padding: 10px 0; gap: 6px; }
  34.         .bar-wrapper { flex: 1; display: flex; flex-direction: column; align-items: center; height: 100%; justify-content: flex-end; cursor: pointer; }
  35.         .bar { width: 80%; background: #a8dadc; border-radius: 4px 4px 0 0; min-height: 1px; transition: background 0.2s; }
  36.         .bar-wrapper:hover .bar { background: #457b9d; }
  37.         .bar-wrapper.active .bar { background: #1d3557; }
  38.         .bar-nap { background: #e9c46a; }
  39.         .bar-wrapper:hover .bar-nap { background: #e6b12a; }
  40.         .bar-value { font-size: 10px; color: #457b9d; margin-bottom: 4px; font-weight: bold; text-align: center; }
  41.         .bar-value-nap { color: #b7840a; }
  42.         .bar-label { font-size: 12px; color: #666; margin-top: 6px; }
  43.         
  44.         /* 時刻チャート用スタイル(水平タイムライン) */
  45.         .time-chart-container { position: relative; border-left: 2px solid #ddd; border-bottom: 2px solid #ddd; padding: 10px 0; margin-top: 10px; background: #fafafa; }
  46.         .time-chart-row { display: flex; height: 45px; align-items: center; position: relative; border-bottom: 1px dashed #eee; }
  47.         .time-chart-label { width: 65px; font-size: 12px; text-align: center; color: #333; font-weight: bold; z-index: 6; }
  48.         .time-chart-line-area { flex: 1; position: relative; height: 100%; }
  49.         
  50.         /* 就寝・起床の結合線 */
  51.         .time-duration-line { position: absolute; height: 8px; top: 18px; background: rgba(69, 123, 157, 0.25); border-radius: 4px; z-index: 2; pointer-events: none; }
  52.         
  53.         /* 平均睡眠時間の文字バッジ */
  54.         .time-duration-text { position: absolute; top: -2px; font-size: 9px; font-weight: bold; color: #1d3557; background: #e8f0fe; padding: 1px 5px; border-radius: 4px; transform: translateX(-50%); z-index: 4; white-space: nowrap; border: 1px solid #b3d7ff; }
  55.  
  56.         .time-dot { position: absolute; height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center; transform: translateX(-50%); z-index: 5; }
  57.         .dot-symbol { font-size: 14px; line-height: 1; cursor: pointer; }
  58.         .dot-bed { color: #1d3557; }
  59.         .dot-wake { color: #e9c46a; }
  60.         .time-text-val { font-size: 9px; font-weight: bold; margin-top: 2px; background: rgba(255,255,255,0.9); padding: 1px 4px; border-radius: 3px; white-space: nowrap; box-shadow: 0 1px 3px rgba(0,0,0,0.15); }
  61.         .time-chart-axis-header { display: flex; position: relative; height: 20px; font-size: 10px; color: #888; border-bottom: 1px solid #ccc; margin-left: 65px; }
  62.  
  63.         /* 右カラム・タイムライン */
  64.         .timeline-container { border: 1px solid #ddd; border-radius: 8px; padding: 10px; background: #fff; max-height: 480px; overflow-y: auto; margin-bottom: 20px; }
  65.         .axis-label-row { display: flex; position: relative; height: 25px; margin-left: 50px; margin-right: 15px; border-bottom: 1px solid #ccc; font-size: 11px; color: #777; }
  66.         .timeline-row { display: flex; align-items: center; border-bottom: 1px solid #f0f0f0; padding: 6px 0; position: relative; height: 35px; }
  67.         .day-label { width: 50px; font-size: 12px; font-weight: bold; color: #555; }
  68.         .time-axis-bg { position: absolute; left: 50px; right: 15px; top: 0; bottom: 0; pointer-events: none; }
  69.         .axis-line { position: absolute; top: 0; bottom: 0; border-left: 1px dashed #e5e5e5; }
  70.         .sleep-bar-canvas { position: relative; flex: 1; height: 100%; margin-right: 15px; margin-left: 50px; }
  71.         .sleep-segment { position: absolute; height: 24px; top: 5px; border-radius: 4px; font-size: 10px; color: white; display: flex; align-items: center; justify-content: center; overflow: hidden; white-space: nowrap; min-width: 4px; cursor: pointer; }
  72.         .sleep-segment .tooltip, .time-dot .tooltip { display: none; position: absolute; background: rgba(17, 75, 95, 0.95); color: #fff; padding: 8px 12px; border-radius: 6px; bottom: 35px; left: 50%; transform: translateX(-50%); z-index: 999; font-size: 11px; white-space: nowrap; line-height: 1.4; box-shadow: 0 4px 8px rgba(0,0,0,0.2); }
  73.         .sleep-segment:hover .tooltip, .time-dot:hover .tooltip { display: block; }
  74.         .segment-main { background: #457b9d; }
  75.         .segment-nap { background: #e9c46a; color: #333; font-weight: bold; }
  76.         
  77.         .legend { display: flex; gap: 15px; font-size: 12px; justify-content: flex-end; margin-bottom: 10px; }
  78.         .legend-item { display: flex; align-items: center; gap: 5px; }
  79.         .box-main { width: 12px; height: 12px; background: #457b9d; border-radius: 2px; }
  80.         .box-nap { width: 12px; height: 12px; background: #e9c46a; border-radius: 2px; }
  81.  
  82.         /* テーブル容器(11準拠: 全件フラット表示) */
  83.         .table-container { border: 1px solid #dee2e6; border-radius: 8px; overflow: hidden; }
  84.         .sleep-table { width: 100%; border-collapse: collapse; text-align: left; font-size: 14px; }
  85.         .sleep-table th { background-color: #457b9d; color: white; padding: 10px; }
  86.         .sleep-table td { padding: 10px; border-bottom: 1px solid #dee2e6; }
  87.         .sleep-table tr:nth-child(even) { background-color: #f8f9fa; }
  88.  
  89.         #debugLogBox { background: #222; color: #7f7; font-family: monospace; padding: 12px; border-radius: 8px; margin-top: 20px; font-size: 12px; max-height: 120px; overflow-y: auto; white-space: pre-wrap; border: 2px solid #555; }
  90.         .badge-main { background: #457b9d; color: white; padding: 2px 6px; border-radius: 4px; font-size: 12px; font-weight: bold; }
  91.         .badge-nap { background: #e9c46a; color: #333; padding: 2px 6px; border-radius: 4px; font-size: 12px; font-weight: bold; }
  92.     </style>
  93. </head>
  94. <body>
  95.  
  96. <div class="container">
  97.     <div class="header">
  98.         <h1>? Fitbit 睡眠総合マルチアナリティクス</h1>
  99.         <p>すべての分析グラフを一画面に同時集約。時間軸を0時基点に統合した完全表示ダッシュボードです。</p>
  100.     </div>
  101.  
  102.     <div class="upload-box" id="uploadArea">
  103.         <p style="margin-top:0; font-weight:bold; color:#1b5e20;">? 【使い方】Fitbit の csvファイルを選択してください。</p>
  104.         <div class="file-input-wrapper">
  105.             <input type="file" id="csvFileInput" accept=".csv">
  106.         </div>
  107.     </div>
  108.  
  109.     <div id="mainDashboard" style="display: none;">
  110.         
  111.         <div class="card controls">
  112.             <h2>? データ期間切り替え</h2>
  113.             <div style="margin-left: auto; display: flex; gap: 15px; align-items: center;">
  114.                 <div>
  115.                     <label for="yearSelector">基準年:</label>
  116.                     <select id="yearSelector"></select>
  117.                 </div>
  118.                 <div>
  119.                     <label for="monthSelector">分析対象月:</label>
  120.                     <select id="monthSelector"></select>
  121.                 </div>
  122.                 <span id="dataCountBadge" style="background: #114b5f; color: white; padding: 6px 14px; border-radius: 20px; font-size: 13px; font-weight:bold;">全 0 件</span>
  123.             </div>
  124.         </div>
  125.  
  126.         <div class="grid">
  127.             <div class="charts-stack">
  128.                 
  129.                 <div class="card">
  130.                     <h3 id="monthlyDurationTitle" style="margin-top: 0; font-size: 15px; color: #114b5f; font-weight: bold;">? 月別平均睡眠時間(本睡眠)</h3>
  131.                     <div class="bar-chart" id="monthlyChartContainer" style="height: 140px;"></div>
  132.                 </div>
  133.  
  134.                 <div class="card">
  135.                     <h3 id="monthlyNapTitle" style="margin-top: 0; font-size: 15px; color: #b7840a; font-weight: bold;">? 月別 平均昼寝・仮眠時間(1日あたり)</h3>
  136.                     <div class="bar-chart" id="monthlyNapChartContainer" style="height: 140px;"></div>
  137.                 </div>
  138.  
  139.                 <div class="card">
  140.                     <div style="display:flex; justify-content:space-between; align-items:center;">
  141.                         <h3 id="yearlyComparisonTitle" style="margin-top: 0; font-size: 15px; color: #114b5f; font-weight: bold;">? 各年の 平均就寝・起床・睡眠時間比較</h3>
  142.                         <div style="font-size:11px; display:flex; gap:10px;">
  143.                             <span>? 就寝</span> <span style="color:#457b9d;">? 平均睡眠</span> <span>? 起床</span>
  144.                         </div>
  145.                     </div>
  146.                     <div id="yearlyTimeChartAxis" class="time-chart-axis-header"></div>
  147.                     <div class="time-chart-container" id="yearlyTimeChartContainer"></div>
  148.                 </div>
  149.  
  150.                 <div class="card">
  151.                     <div style="display:flex; justify-content:space-between; align-items:center;">
  152.                         <h3 id="monthlyTimeTitle" style="margin-top: 0; font-size: 15px; color: #114b5f; font-weight: bold;">? 月別 平均就寝・起床・睡眠時間推移</h3>
  153.                         <div style="font-size:11px; display:flex; gap:10px;">
  154.                             <span>? 就寝</span> <span style="color:#457b9d;">? 平均睡眠</span> <span>? 起床</span>
  155.                         </div>
  156.                     </div>
  157.                     <div id="monthlyTimeChartAxis" class="time-chart-axis-header"></div>
  158.                     <div class="time-chart-container" id="monthlyTimeChartContainer"></div>
  159.                 </div>
  160.  
  161.                 <div class="card">
  162.                     <h3 id="clockTitle" style="margin-top: 0; font-size: 15px; color: #114b5f; font-weight: bold; text-align: center;">?? 睡眠サイクル(12時間表示・本睡眠のみ)</h3>
  163.                     <div style="text-align:center; padding-top:10px;"><canvas id="sleepClock" width="260" height="260" style="margin: 0 auto;"></canvas></div>
  164.                 </div>
  165.  
  166.             </div>
  167.  
  168.             <div class="charts-stack">
  169.                 
  170.                 <div class="card">
  171.                     <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:10px;">
  172.                         <h3 id="timelineTitle" style="margin-top: 0; font-size: 15px; color: #114b5f; font-weight: bold;">? 日別睡眠時間分布タイムライン(0時〜24時)</h3>
  173.                         <div class="legend">
  174.                             <div class="legend-item"><div class="box-main"></div><span>本睡眠</span></div>
  175.                             <div class="legend-item"><div class="box-nap"></div><span>昼寝・仮眠</span></div>
  176.                         </div>
  177.                     </div>
  178.                     <div class="timeline-container" id="timelineContainer"></div>
  179.                 </div>
  180.  
  181.                 <div class="card">
  182.                     <h3 style="margin-top: 0; font-size: 15px; color: #114b5f; font-weight: bold; margin-bottom: 12px;">? 就寝・起床時間 数値一覧(1ヶ月分・全表示)</h3>
  183.                     <div class="table-container">
  184.                         <table class="sleep-table">
  185.                             <thead>
  186.                                 <tr><th>日付</th><th>種類</th><th>就寝時刻</th><th>起床時刻</th><th>睡眠時間</th></tr>
  187.                             </thead>
  188.                             <tbody id="sleepTableBody"></tbody>
  189.                         </table>
  190.                     </div>
  191.                 </div>
  192.             </div>
  193.         </div>
  194.     </div>
  195.  
  196.     <div id="debugLogBox">? ログ:ファイルを読み込んでください...</div>
  197. </div>
  198.  
  199. <script>
  200.     let rawParsedData = ;
  201.     const standard24HoursAxis = [0, 3, 6, 9, 12, 15, 18, 21, 24];
  202.     const logBox = document.getElementById('debugLogBox');
  203.  
  204.     function log(msg) { logBox.innerHTML += "\n" + msg; logBox.scrollTop = logBox.scrollHeight; }
  205.  
  206.     document.getElementById('csvFileInput').addEventListener('change', function(e) {
  207.         const file = e.target.files[0];
  208.         if (!file) return;
  209.         log(`? ファイル「${file.name}」を読み込み中...`);
  210.         const reader = new FileReader();
  211.         reader.onload = function(evt) {
  212.             try { parseCSVRobust(evt.target.result.trim()); } catch(err) { log(`? 解析エラー: ${err.message}`); }
  213.         };
  214.         reader.readAsText(file, 'UTF-8');
  215.     });
  216.  
  217.     function splitCSVLine(text) {
  218.         let result = ; let inQuote = false; let token = '';
  219.         for (let i = 0; i < text.length; i++) {
  220.             let c = text[i];
  221.             if (c === '"') { inQuote = !inQuote; }
  222.             else if (c === ',' && !inQuote) { result.push(token.trim()); token = ''; }
  223.             else { token += c; }
  224.         }
  225.         result.push(token.trim());
  226.         return result;
  227.     }
  228.  
  229.     // ? 13.html準拠の堅牢なFitbit英語ヘッダーパース処理
  230.     function parseCSVRobust(text) {
  231.         const lines = text.split(/\r?\n/);
  232.         if (lines.length < 2) { log("? エラー: CSVファイルの中身が不正です。"); return; }
  233.         
  234.         // ヘッダー行を取得して小文字に統一
  235.         const headers = splitCSVLine(lines[0]).map(h => h.replace(/[\ufeff"']/g, '').trim().toLowerCase());
  236.         
  237.         const idxDate = headers.indexOf('dateofsleep');
  238.         const idxStart = headers.indexOf('starttime');
  239.         const idxEnd = headers.indexOf('endtime');
  240.         const idxMins = headers.indexOf('minutesasleep');
  241.         const idxMain = headers.indexOf('mainsleep');
  242.  
  243.         if (idxDate === -1 || idxStart === -1 || idxEnd === -1) {
  244.             log(`? エラー: 必要な列名(dateOfSleep, startTime, endTime)が見つかりません。`);
  245.             alert("睡眠データが見つかりませんでした。Fitbit形式の正しいCSVか確認してください。");
  246.             return;
  247.         }
  248.  
  249.         rawParsedData = ;
  250.         for (let i = 1; i < lines.length; i++) {
  251.             const line = lines[i].trim(); if (!line) continue;
  252.             const cells = splitCSVLine(line); if (cells.length <= Math.max(idxDate, idxStart, idxEnd)) continue;
  253.             const clean = (idx) => (idx !== -1 && cells[idx]) ? cells[idx].replace(/["']/g, '').trim() : '';
  254.             
  255.             rawParsedData.push({
  256.                 dateOfSleep: clean(idxDate), startTime: clean(idxStart), endTime: clean(idxEnd),
  257.                 minutesAsleep: parseFloat(clean(idxMins)) || 0,
  258.                 mainSleep: clean(idxMain) ? clean(idxMain).toLowerCase() : 'true'
  259.             });
  260.         }
  261.         
  262.         if(rawParsedData.length > 0) {
  263.             log(`? 成功: ${rawParsedData.length} 件のデータを読み込みました。`);
  264.             initDashboard();
  265.         } else {
  266.             alert("睡眠データが見つかりませんでした。");
  267.         }
  268.     }
  269.  
  270.     function safeParseDate(str) {
  271.         if (!str) return null;
  272.         try {
  273.             let clean = str.toLowerCase().replace('t', ' ').replace(/\//g, '-').trim();
  274.             const isPM = clean.includes('pm'); const isAM = clean.includes('am');
  275.             clean = clean.replace('pm', '').replace('am', '').trim();
  276.             const parts = clean.split(' '); const dateParts = parts[0].split('-');
  277.             let hour = 0, min = 0;
  278.             if (parts.length > 1) {
  279.                 const timeParts = parts[1].split(':');
  280.                 hour = parseInt(timeParts[0], 10) || 0; min = parseInt(timeParts[1], 10) || 0;
  281.                 if (isPM && hour < 12) hour += 12; if (isAM && hour === 12) hour = 0;
  282.             }
  283.             return new Date(parseInt(dateParts[0], 10), parseInt(dateParts[1], 10) - 1, parseInt(dateParts[2], 10), hour, min, 0);
  284.         } catch(e) { return null; }
  285.     }
  286.  
  287.     function calculateCircularAverage(dateObjects) {
  288.         if (!dateObjects || dateObjects.length === 0) return "--:--";
  289.         let sumSin = 0; let sumCos = 0;
  290.         dateObjects.forEach(dt => {
  291.             const minutes = dt.getHours() * 60 + dt.getMinutes();
  292.             const angle = (minutes / 1440) * 2 * Math.PI;
  293.             sumSin += Math.sin(angle); sumCos += Math.cos(angle);
  294.         });
  295.         let avgAngle = Math.atan2(sumSin, sumCos);
  296.         if (avgAngle < 0) avgAngle += 2 * Math.PI;
  297.         let avgMinutes = Math.round((avgAngle / (2 * Math.PI)) * 1440) % 1440;
  298.         const h = Math.floor(avgMinutes / 60); const m = avgMinutes % 60;
  299.         return `${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}`;
  300.     }
  301.  
  302.     function getAverageDurationStr(filteredRows) {
  303.         if(filteredRows.length === 0) return "";
  304.         const totalMins = filteredRows.reduce((sum, r) => sum + r.minutesAsleep, 0);
  305.         const avgMins = Math.round(totalMins / filteredRows.length);
  306.         return `${Math.floor(avgMins / 60)}時間${(avgMins % 60)}分`;
  307.     }
  308.  
  309.     function initDashboard() {
  310.         const yearsSet = new Set();
  311.         rawParsedData.forEach(row => {
  312.             const y = row.dateOfSleep.split(' ')[0].replace(/\//g, '-').split('-')[0];
  313.             if (y && y.length === 4 && !isNaN(y)) yearsSet.add(y);
  314.         });
  315.         const years = Array.from(yearsSet).sort();
  316.  
  317.         const yearSel = document.getElementById('yearSelector'); yearSel.innerHTML = '';
  318.         years.forEach(y => { yearSel.innerHTML += `<option value="${y}">${y}年</option>`; });
  319.         
  320.         const monthSel = document.getElementById('monthSelector'); monthSel.innerHTML = '';
  321.         for (let m = 1; m <= 12; m++) { monthSel.innerHTML += `<option value="${m}">${m}月</option>`; }
  322.  
  323.         yearSel.value = years[years.length - 1];
  324.         let latestMonth = 1;
  325.         for (let m = 12; m >= 1; m--) {
  326.             if (rawParsedData.some(row => {
  327.                 const p = row.dateOfSleep.split(' ')[0].replace(/\//g, '-').split('-');
  328.                 return p[0] === yearSel.value && parseInt(p[1], 10) === m;
  329.             })) { latestMonth = m; break; }
  330.         }
  331.         monthSel.value = latestMonth;
  332.  
  333.         const refresh = () => {
  334.             const y = yearSel.value; const m = monthSel.value;
  335.             document.getElementById('monthlyTimeTitle').textContent = `? ${y}年 月別 平均就寝・起床・睡眠時間推移`;
  336.             document.getElementById('monthlyDurationTitle').textContent = `? ${y}年 月別平均睡眠時間(本睡眠)`;
  337.             document.getElementById('monthlyNapTitle').textContent = `? ${y}年 月別 平均昼寝・仮眠時間(1日あたり)`;
  338.             
  339.             renderYearlyTimeComparisonChart(m);
  340.             renderMonthlyTimeChart(y);
  341.             renderMonthlyChart(y);
  342.             renderMonthlyNapChart(y);
  343.             renderTimeline(y, m);
  344.             renderSleepClock(y, m);
  345.             renderNumericalTable(y, m);
  346.         };
  347.         yearSel.onchange = refresh; monthSel.onchange = refresh;
  348.         
  349.         document.getElementById('dataCountBadge').textContent = `全 ${rawParsedData.length} 件`;
  350.         document.getElementById('mainDashboard').style.display = 'block'; // ここで表示させる
  351.         refresh();
  352.         log("? 読み込みに成功し、全ダッシュボードを展開しました。");
  353.     }
  354.  
  355.     const convertTimeToPercent24H = (timeStr) => {
  356.         if (timeStr === "--:--") return -1;
  357.         const [h, min] = timeStr.split(':').map(Number);
  358.         return ((h + (min/60)) / 24) * 100;
  359.     };
  360.  
  361.     // 1. 各年の比較
  362.     function renderYearlyTimeComparisonChart(month) {
  363.         document.getElementById('yearlyComparisonTitle').textContent = `? 各年の 「${month}月」 平均就寝・起床・睡眠時間比較`;
  364.         
  365.         const axisHeader = document.getElementById('yearlyTimeChartAxis'); axisHeader.innerHTML = '';
  366.         standard24HoursAxis.forEach(h => {
  367.             const pos = (h / 24) * 100;
  368.             axisHeader.innerHTML += `<div style="position:absolute; left:calc(${pos}% - 15px);">${h}:00</div>`;
  369.         });
  370.  
  371.         const container = document.getElementById('yearlyTimeChartContainer'); container.innerHTML = '';
  372.         const yearsSet = new Set(); rawParsedData.forEach(row => { const y = row.dateOfSleep.split(' ')[0].replace(/\//g, '-').split('-')[0]; if (y && y.length === 4) yearsSet.add(y); });
  373.         const allYears = Array.from(yearsSet).sort();
  374.  
  375.         allYears.forEach(year => {
  376.             const filtered = rawParsedData.filter(row => {
  377.                 const p = row.dateOfSleep.split(' ')[0].replace(/\//g, '-').split('-');
  378.                 return p[0] === year && parseInt(p[1], 10) === parseInt(month, 10) && row.mainSleep === 'true';
  379.             });
  380.  
  381.             let startDates = ; let endDates = ;
  382.             filtered.forEach(row => {
  383.                 const s = safeParseDate(row.startTime); const e = safeParseDate(row.endTime);
  384.                 if (s) startDates.push(s); if (e) endDates.push(e);
  385.             });
  386.  
  387.             const bedTimeStr = calculateCircularAverage(startDates);
  388.             const wakeTimeStr = calculateCircularAverage(endDates);
  389.             const avgDurationStr = getAverageDurationStr(filtered);
  390.  
  391.             const rowDiv = document.createElement('div'); rowDiv.className = 'time-chart-row';
  392.             rowDiv.innerHTML = `<div class="time-chart-label">${year}年</div>`;
  393.  
  394.             const lineArea = document.createElement('div'); lineArea.className = 'time-chart-line-area';
  395.             standard24HoursAxis.forEach(h => {
  396.                 const line = document.createElement('div'); line.className = 'axis-line';
  397.                 line.style.left = `${(h / 24) * 100}%`; lineArea.appendChild(line);
  398.             });
  399.  
  400.             const bedPos = convertTimeToPercent24H(bedTimeStr);
  401.             const wakePos = convertTimeToPercent24H(wakeTimeStr);
  402.  
  403.             if (bedPos >= 0 && wakePos >= 0) {
  404.                 if (bedPos > wakePos) {
  405.                     lineArea.innerHTML += `<div class="time-duration-line" style="left:${bedPos}%; width:${100 - bedPos}%;"></div>`;
  406.                     lineArea.innerHTML += `<div class="time-duration-line" style="left:0%; width:${wakePos}%;"></div>`;
  407.                     const centerPos = ((bedPos + (100 + wakePos)) / 2) % 100;
  408.                     lineArea.innerHTML += `<div class="time-duration-text" style="left:${centerPos}%;">${avgDurationStr}</div>`;
  409.                 } else {
  410.                     lineArea.innerHTML += `<div class="time-duration-line" style="left:${bedPos}%; width:${wakePos - bedPos}%;"></div>`;
  411.                     const centerPos = bedPos + (wakePos - bedPos) / 2;
  412.                     lineArea.innerHTML += `<div class="time-duration-text" style="left:${centerPos}%;">${avgDurationStr}</div>`;
  413.                 }
  414.             }
  415.  
  416.             if (bedPos >= 0 && bedPos <= 100) {
  417.                 lineArea.innerHTML += `<div class="time-dot" style="left:${bedPos}%;"><span class="dot-symbol dot-bed">?</span><span class="time-text-val" style="color:#1d3557;">${bedTimeStr}</span></div>`;
  418.             }
  419.             if (wakePos >= 0 && wakePos <= 100) {
  420.                 lineArea.innerHTML += `<div class="time-dot" style="left:${wakePos}%;"><span class="dot-symbol dot-wake">?</span><span class="time-text-val" style="color:#b79438;">${wakeTimeStr}</span></div>`;
  421.             }
  422.  
  423.             rowDiv.appendChild(lineArea); container.appendChild(rowDiv);
  424.         });
  425.     }
  426.  
  427.     // 2. 月別推移
  428.     function renderMonthlyTimeChart(year) {
  429.         const axisHeader = document.getElementById('monthlyTimeChartAxis'); axisHeader.innerHTML = '';
  430.         standard24HoursAxis.forEach(h => {
  431.             const pos = (h / 24) * 100;
  432.             axisHeader.innerHTML += `<div style="position:absolute; left:calc(${pos}% - 15px);">${h}:00</div>`;
  433.         });
  434.  
  435.         const container = document.getElementById('monthlyTimeChartContainer'); container.innerHTML = '';
  436.         
  437.         for (let m = 1; m <= 12; m++) {
  438.             const filtered = rawParsedData.filter(row => {
  439.                 const p = row.dateOfSleep.split(' ')[0].replace(/\//g, '-').split('-');
  440.                 return p[0] === year && parseInt(p[1], 10) === m && row.mainSleep === 'true';
  441.             });
  442.  
  443.             let startDates = ; let endDates = ;
  444.             filtered.forEach(row => {
  445.                 const s = safeParseDate(row.startTime); const e = safeParseDate(row.endTime);
  446.                 if (s) startDates.push(s); if (e) endDates.push(e);
  447.             });
  448.  
  449.             const bedTimeStr = calculateCircularAverage(startDates);
  450.             const wakeTimeStr = calculateCircularAverage(endDates);
  451.             const avgDurationStr = getAverageDurationStr(filtered);
  452.  
  453.             const rowDiv = document.createElement('div'); rowDiv.className = 'time-chart-row';
  454.             rowDiv.innerHTML = `<div class="time-chart-label">${m}月</div>`;
  455.  
  456.             const lineArea = document.createElement('div'); lineArea.className = 'time-chart-line-area';
  457.             standard24HoursAxis.forEach(h => {
  458.                 const line = document.createElement('div'); line.className = 'axis-line';
  459.                 line.style.left = `${(h / 24) * 100}%`; lineArea.appendChild(line);
  460.             });
  461.  
  462.             const bedPos = convertTimeToPercent24H(bedTimeStr);
  463.             const wakePos = convertTimeToPercent24H(wakeTimeStr);
  464.  
  465.             if (bedPos >= 0 && wakePos >= 0) {
  466.                 if (bedPos > wakePos) {
  467.                     lineArea.innerHTML += `<div class="time-duration-line" style="left:${bedPos}%; width:${100 - bedPos}%;"></div>`;
  468.                     lineArea.innerHTML += `<div class="time-duration-line" style="left:0%; width:${wakePos}%;"></div>`;
  469.                     const centerPos = ((bedPos + (100 + wakePos)) / 2) % 100;
  470.                     lineArea.innerHTML += `<div class="time-duration-text" style="left:${centerPos}%;">${avgDurationStr}</div>`;
  471.                 } else {
  472.                     lineArea.innerHTML += `<div class="time-duration-line" style="left:${bedPos}%; width:${wakePos - bedPos}%;"></div>`;
  473.                     const centerPos = bedPos + (wakePos - bedPos) / 2;
  474.                     lineArea.innerHTML += `<div class="time-duration-text" style="left:${centerPos}%;">${avgDurationStr}</div>`;
  475.                 }
  476.             }
  477.  
  478.             if (bedPos >= 0 && bedPos <= 100) {
  479.                 lineArea.innerHTML += `<div class="time-dot" style="left:${bedPos}%;"><span class="dot-symbol dot-bed">?</span><span class="time-text-val" style="color:#1d3557;">${bedTimeStr}</span></div>`;
  480.             }
  481.             if (wakePos >= 0 && wakePos <= 100) {
  482.                 lineArea.innerHTML += `<div class="time-dot" style="left:${wakePos}%;"><span class="dot-symbol dot-wake">?</span><span class="time-text-val" style="color:#b79438;">${wakeTimeStr}</span></div>`;
  483.             }
  484.  
  485.             rowDiv.appendChild(lineArea); container.appendChild(rowDiv);
  486.         }
  487.     }
  488.  
  489.     // 3. 月別平均睡眠時間
  490.     function renderMonthlyChart(year) {
  491.         const dailySum = {};
  492.         rawParsedData.forEach(row => {
  493.             const p = row.dateOfSleep.split(' ')[0].replace(/\//g, '-').split('-');
  494.             if (p[0] === year && row.mainSleep === 'true') {
  495.                 const f = `${p[0]}-${parseInt(p[1],10)}-${parseInt(p[2],10)}`;
  496.                 dailySum[f] = (dailySum[f] || 0) + row.minutesAsleep;
  497.             }
  498.         });
  499.         const monthStats = {}; for (let m = 1; m <= 12; m++) monthStats[m] = { total: 0, days: 0 };
  500.         for (const [date, mins] of Object.entries(dailySum)) {
  501.             const m = parseInt(date.split('-')[1], 10); if (monthStats[m]) { monthStats[m].total += mins; monthStats[m].days += 1; }
  502.         }
  503.         const container = document.getElementById('monthlyChartContainer'); container.innerHTML = '';
  504.         const activeMonth = parseInt(document.getElementById('monthSelector').value, 10);
  505.         for (let m = 1; m <= 12; m++) {
  506.             const avg = monthStats[m].days > 0 ? (monthStats[m].total / monthStats[m].days) / 60 : 0;
  507.             const hPercent = Math.min(100, (avg / 10) * 100);
  508.             const wrapper = document.createElement('div');
  509.             wrapper.className = 'bar-wrapper' + (m === activeMonth ? ' active' : '');
  510.             wrapper.onclick = () => { document.getElementById('monthSelector').value = m; document.getElementById('monthSelector').dispatchEvent(new Event('change')); };
  511.             wrapper.innerHTML = `<div class="bar-value">${avg > 0 ? avg.toFixed(1) + 'h' : ''}</div><div class="bar" style="height:${hPercent}%;"></div><div class="bar-label">${m}月</div>`;
  512.             container.appendChild(wrapper);
  513.         }
  514.     }
  515.  
  516.     // 4. 月別平均昼寝時間
  517.     function renderMonthlyNapChart(year) {
  518.         const monthNapSum = {}; for (let m = 1; m <= 12; m++) monthNapSum[m] = { totalMins: 0, recordCount: 0 };
  519.         rawParsedData.forEach(row => {
  520.             const p = row.dateOfSleep.split(' ')[0].replace(/\//g, '-').split('-');
  521.             if (p[0] === year && row.mainSleep === 'false') {
  522.                 const m = parseInt(p[1], 10);
  523.                 if (m >= 1 && m <= 12) {
  524.                     monthNapSum[m].totalMins += row.minutesAsleep;
  525.                     monthNapSum[m].recordCount += 1;
  526.                 }
  527.             }
  528.         });
  529.  
  530.         const container = document.getElementById('monthlyNapChartContainer'); container.innerHTML = '';
  531.         for (let m = 1; m <= 12; m++) {
  532.             const avgMins = monthNapSum[m].totalMins / 30;
  533.             const hPercent = Math.min(100, (avgMins / 60) * 100);
  534.             const wrapper = document.createElement('div');
  535.             wrapper.className = 'bar-wrapper';
  536.             let displayVal = "";
  537.             if (monthNapSum[m].totalMins > 0) {
  538.                 displayVal = avgMins >= 1 ? Math.round(avgMins) + '分' : '1分未満';
  539.             }
  540.             wrapper.innerHTML = `
  541.                 <div class="bar-value bar-value-nap">${displayVal}</div>
  542.                 <div class="bar bar-nap" style="height:${Math.max(1, hPercent)}%;"></div>
  543.                 <div class="bar-label">${m}月</div>
  544.             `;
  545.             container.appendChild(wrapper);
  546.         }
  547.     }
  548.  
  549.     // 5. 日別タイムライン
  550.     function renderTimeline(year, month) {
  551.         const container = document.getElementById('timelineContainer'); container.innerHTML = '';
  552.         const axisHeader = document.createElement('div'); axisHeader.className = 'axis-label-row';
  553.         standard24HoursAxis.forEach(h => {
  554.             const pos = (h / 24) * 100;
  555.             axisHeader.innerHTML += `<div style="position:absolute; left:calc(${pos}% - 15px);">${h}:00</div>`;
  556.         });
  557.         container.appendChild(axisHeader);
  558.  
  559.         const filtered = rawParsedData.filter(row => { const p = row.dateOfSleep.split(' ')[0].replace(/\//g, '-').split('-'); return p[0] === year && parseInt(p[1], 10) === parseInt(month, 10); });
  560.         const dayGroups = {};
  561.         filtered.forEach(row => { const d = parseInt(row.dateOfSleep.split(' ')[0].replace(/\//g, '-').split('-')[2], 10); if (!isNaN(d)) { if (!dayGroups[d]) dayGroups[d] = ; dayGroups[d].push(row); } });
  562.         const sortedDays = Object.keys(dayGroups).map(Number).sort*4;
  563.                 let e = Math.max(0, Math.min(24, eVal));
  564.                 if (e <= s) return;
  565.                 const isMain = row.mainSleep === 'true';
  566.                 
  567.                 const seg = document.createElement('div'); seg.className = `sleep-segment ${isMain?'segment-main':'segment-nap'}`;
  568.                 seg.style.left = `${(s / 24) * 100}%`; seg.style.width = `${*5 - (safeParseDate(b.startTime) || new Date()));
  569.  
  570.         if (filtered.length === 0) { tbody.innerHTML = `<tr><td colspan="5" style="text-align:center; color:#999; padding:20px;">データなし</td></tr>`; return; }
  571.  
  572.         filtered.forEach(row => {
  573.             const sDt = safeParseDate(row.startTime); const eDt = safeParseDate(row.endTime); if (!sDt || !eDt) return;
  574.             const dateStr = `${sDt.getFullYear()}/${(sDt.getMonth()+1).toString().padStart(2,'0')}/${sDt.getDate().toString().padStart(2,'0')}`;
  575.             const sTimeStr = `${sDt.getHours().toString().padStart(2,'0')}:${sDt.getMinutes().toString().padStart(2,'0')}`;
  576.             const eTimeStr = `${eDt.getHours().toString().padStart(2,'0')}:${eDt.getMinutes().toString().padStart(2,'0')}`;
  577.             const durationStr = `${Math.floor(row.minutesAsleep / 60)}時間${Math.round(row.minutesAsleep % 60)}分`;
  578.             const typeBadge = row.mainSleep === 'true' ? `<span class="badge-main">本睡眠</span>` : `<span class="badge-nap">仮眠</span>`;
  579.  
  580.             tbody.appendChild(document.createElement('tr')).innerHTML = `<td><strong>${dateStr}</strong></td><td>${typeBadge}</td><td>? ${sTimeStr}</td><td>? ${eTimeStr}</td><td>?? ${durationStr}</td>`;
  581.         });
  582.     }
  583.  
  584.     // 睡眠サイクル時計
  585.     function renderSleepClock(year, month) {
  586.         const filtered = rawParsedData.filter(row => { const p = row.dateOfSleep.split(' ')[0].replace(/\//g, '-').split('-'); return p[0] === year && parseInt(p[1], 10) === parseInt(month, 10) && row.mainSleep === 'true'; });
  587.         let startDates = ; let endDates = ;
  588.         filtered.forEach(row => { const s = safeParseDate(row.startTime); const e = safeParseDate(row.endTime); if (s) startDates.push(s); if (e) endDates.push(e); });
  589.         
  590.         const bedTime = calculateCircularAverage(startDates);
  591.         const wakeTime = calculateCircularAverage(endDates);
  592.         
  593.         const canvas = document.getElementById('sleepClock'); if (!canvas) return; const ctx = canvas.getContext('2d'); ctx.clearRect(0, 0, canvas.width, canvas.height);
  594.         if (bedTime === "--:--" || wakeTime === "--:--") { ctx.fillStyle = '#999'; ctx.font = '14px sans-serif'; ctx.textAlign = 'center'; ctx.fillText('データなし', canvas.width/2, canvas.height/2); return; }
  595.         
  596.         const cx = canvas.width / 2; const cy = canvas.height / 2; const radius = 95;
  597.         const h12ToRad = (h) => (h / 12) * 2 * Math.PI - Math.PI / 2;
  598.         
  599.         const bHrs = *6; }
  600.         
  601.         const drawHand = (hrs, color) => { const a = h12ToRad(hrs); ctx.beginPath(); ctx.moveTo(cx, cy); ctx.lineTo(cx + (radius - 28) * Math.cos(a), cy + (radius - 28) * Math.sin(a)); ctx.strokeStyle = color; ctx.lineWidth = 3.5; ctx.stroke(); };
  602.         drawHand(bHrs, '#1d3557'); drawHand(wHrs, '#e9c46a');
  603.         
  604.         ctx.beginPath(); ctx.arc(cx, cy, radius * 0.55, 0, 2 * Math.PI); ctx.fillStyle = '#ffffff'; ctx.fill(); ctx.strokeStyle = '#e9ecef'; ctx.stroke();
  605.         ctx.fillStyle = '#212529'; ctx.font = 'bold 12px sans-serif'; ctx.fillText(`${month}月 平均サイクル`, cx, cy - 14);
  606.         ctx.fillStyle = '#1d3557'; ctx.font = '11px sans-serif'; ctx.fillText(`就寝 ${bedTime}`, cx, cy + 6);
  607.         ctx.fillStyle = '#b7840a'; ctx.font = '11px sans-serif'; ctx.fillText(`起床 ${wakeTime}`, cx, cy + 20);
  608.     }
  609. </script>
  610. </body>
  611. </html>

 

*1:a, b) => a - b);

  •  
  •         if (sortedDays.length === 0) { container.innerHTML += `<div style="text-align:center; padding:30px; color:#999;">選択月のデータはありません。</div>`; return; }
  •  
  •         sortedDays.forEach(day => {
  •             const rowDiv = document.createElement('div'); rowDiv.className = 'timeline-row';
  •             rowDiv.innerHTML = `<div class="day-label">${day}日</div>`;
  •             const bg = document.createElement('div'); bg.className = 'time-axis-bg';
  •             standard24HoursAxis.forEach(h => { bg.innerHTML += `<div class="axis-line" style="left:${(h / 24) * 100}%;"></div>`; });
  •             rowDiv.appendChild(bg);
  •  
  •             const canvas = document.createElement('div'); canvas.className = 'sleep-bar-canvas';
  •             dayGroups[day].forEach(row => {
  •                 const sDt = safeParseDate(row.startTime); const eDt = safeParseDate(row.endTime); if (!sDt || !eDt) return;
  •                 
  •                 const baseDate = new Date(parseInt(year, 10), parseInt(month, 10)-1, day, 0, 0, 0);
  •                 const sVal = (sDt - baseDate) / 3600000;
  •                 const eVal = (eDt - baseDate) / 3600000;
  •                 
  •                 let s = Math.max(0, Math.min(24, sVal

    *2:e - s) / 24) * 100}%`;

  •                 
  •                 const sT = `${sDt.getHours().toString().padStart(2,'0')}:${sDt.getMinutes().toString().padStart(2,'0')}`;
  •                 const eT = `${eDt.getHours().toString().padStart(2,'0')}:${eDt.getMinutes().toString().padStart(2,'0')}`;
  •                 if (((e - s) / 24) * 100 > 12) seg.textContent = `${sT}〜${eT}`;
  •                 seg.innerHTML += `<div class="tooltip">?? ${isMain?'本睡眠':'仮眠'}<br>?就寝:${sT}<br>?起床:${eT}<br>?時間:${Math.floor(row.minutesAsleep/60)}時間${Math.round(row.minutesAsleep%60)}分</div>`;
  •                 canvas.appendChild(seg);
  •             });
  •             rowDiv.appendChild(canvas);
  •             container.appendChild(rowDiv);
  •         });
  •     }
  •  
  •     // 数値テーブル一覧
  •     function renderNumericalTable(year, month) {
  •         const tbody = document.getElementById('sleepTableBody'); tbody.innerHTML = '';
  •         const filtered = rawParsedData.filter(row => { const p = row.dateOfSleep.split(' ')[0].replace(/\//g, '-').split('-'); return p[0] === year && parseInt(p[1], 10) === parseInt(month, 10); });
  •         filtered.sort((a, b) => (safeParseDate(a.startTime) || new Date(

    *3:) => { const [h, m] = bedTime.split(':').map(Number); return (h % 12) + (m/60); })();

  •         const wHrs = (() => { const [h, m] = wakeTime.split(':').map(Number); return (h % 12) + (m/60); })();
  •         
  •         let sAng = h12ToRad(bHrs); let eAng = h12ToRad(wHrs);
  •         if (eAng < sAng) eAng += 2 * Math.PI;
  •         
  •         ctx.beginPath(); ctx.arc(cx, cy, radius, 0, 2 * Math.PI); ctx.fillStyle = '#f8f9fa'; ctx.fill(); ctx.strokeStyle = '#dee2e6'; ctx.lineWidth = 2; ctx.stroke();
  •         ctx.beginPath(); ctx.moveTo(cx, cy); ctx.arc(cx, cy, radius, sAng, eAng, false); ctx.closePath(); ctx.fillStyle = 'rgba(69, 123, 157, 0.35)'; ctx.fill();
  •         
  •         ctx.font = 'bold 12px sans-serif'; ctx.fillStyle = '#495057'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
  •         for (let h = 1; h <= 12; h++) { const a = h12ToRad(h); ctx.fillText(`${h}`, cx + (radius - 14) * Math.cos(a), cy + (radius - 14) * Math.sin(a

    *4:a, b) => a - b);

  •  
  •         if (sortedDays.length === 0) { container.innerHTML += `<div style="text-align:center; padding:30px; color:#999;">選択月のデータはありません。</div>`; return; }
  •  
  •         sortedDays.forEach(day => {
  •             const rowDiv = document.createElement('div'); rowDiv.className = 'timeline-row';
  •             rowDiv.innerHTML = `<div class="day-label">${day}日</div>`;
  •             const bg = document.createElement('div'); bg.className = 'time-axis-bg';
  •             standard24HoursAxis.forEach(h => { bg.innerHTML += `<div class="axis-line" style="left:${(h / 24) * 100}%;"></div>`; });
  •             rowDiv.appendChild(bg);
  •  
  •             const canvas = document.createElement('div'); canvas.className = 'sleep-bar-canvas';
  •             dayGroups[day].forEach(row => {
  •                 const sDt = safeParseDate(row.startTime); const eDt = safeParseDate(row.endTime); if (!sDt || !eDt) return;
  •                 
  •                 const baseDate = new Date(parseInt(year, 10), parseInt(month, 10)-1, day, 0, 0, 0);
  •                 const sVal = (sDt - baseDate) / 3600000;
  •                 const eVal = (eDt - baseDate) / 3600000;
  •                 
  •                 let s = Math.max(0, Math.min(24, sVal

    *5:e - s) / 24) * 100}%`;

  •                 
  •                 const sT = `${sDt.getHours().toString().padStart(2,'0')}:${sDt.getMinutes().toString().padStart(2,'0')}`;
  •                 const eT = `${eDt.getHours().toString().padStart(2,'0')}:${eDt.getMinutes().toString().padStart(2,'0')}`;
  •                 if (((e - s) / 24) * 100 > 12) seg.textContent = `${sT}〜${eT}`;
  •                 seg.innerHTML += `<div class="tooltip">?? ${isMain?'本睡眠':'仮眠'}<br>?就寝:${sT}<br>?起床:${eT}<br>?時間:${Math.floor(row.minutesAsleep/60)}時間${Math.round(row.minutesAsleep%60)}分</div>`;
  •                 canvas.appendChild(seg);
  •             });
  •             rowDiv.appendChild(canvas);
  •             container.appendChild(rowDiv);
  •         });
  •     }
  •  
  •     // 数値テーブル一覧
  •     function renderNumericalTable(year, month) {
  •         const tbody = document.getElementById('sleepTableBody'); tbody.innerHTML = '';
  •         const filtered = rawParsedData.filter(row => { const p = row.dateOfSleep.split(' ')[0].replace(/\//g, '-').split('-'); return p[0] === year && parseInt(p[1], 10) === parseInt(month, 10); });
  •         filtered.sort((a, b) => (safeParseDate(a.startTime) || new Date(

    *6:) => { const [h, m] = bedTime.split(':').map(Number); return (h % 12) + (m/60); })();

  •         const wHrs = (() => { const [h, m] = wakeTime.split(':').map(Number); return (h % 12) + (m/60); })();
  •         
  •         let sAng = h12ToRad(bHrs); let eAng = h12ToRad(wHrs);
  •         if (eAng < sAng) eAng += 2 * Math.PI;
  •         
  •         ctx.beginPath(); ctx.arc(cx, cy, radius, 0, 2 * Math.PI); ctx.fillStyle = '#f8f9fa'; ctx.fill(); ctx.strokeStyle = '#dee2e6'; ctx.lineWidth = 2; ctx.stroke();
  •         ctx.beginPath(); ctx.moveTo(cx, cy); ctx.arc(cx, cy, radius, sAng, eAng, false); ctx.closePath(); ctx.fillStyle = 'rgba(69, 123, 157, 0.35)'; ctx.fill();
  •         
  •         ctx.font = 'bold 12px sans-serif'; ctx.fillStyle = '#495057'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
  •         for (let h = 1; h <= 12; h++) { const a = h12ToRad(h); ctx.fillText(`${h}`, cx + (radius - 14) * Math.cos(a), cy + (radius - 14) * Math.sin(a

  • ゲームパッドの話〈XinputのソフトとY軸反転の対処〉

    こんにちは。冴えないゲーマーです。
    正気の沙汰ではない前回のような記事が結構な人に見られていてネットの恐ろしさに震えています。その節は大変申し訳ございませんでした(土下座)。

    本題。

    PS3で使っていたHORI PADをPCで使えるようにするためにネットの海からドライバーを探し出したりドライバ削除(上級者向け)にビビり散らしながらもなんとかwin10PCで使えるように設定できたんですが、どうにも「スティックと逆方向に動くんだよなぁ…(苦笑)」ってなことがありまして、簡単ではありますが今回も備忘録としてその対処法を残しておきます。恐縮ながらパソコンの知識はそんなにないので一切の責任は取れません。また各ソフトの導入に際して分からないことは各自調べてください。この記事では詳しく説明はしません。あしからず。

     

    ■全体の流れ

    x360ceだけではコントローラーが反応しないゲームをどうにか動かしたい
    XOutputを導入することで動いた!が、左スティックのY軸が反転している
     →Y軸反転をするためにXInput Plusを導入、軸反転に☑
      →問題なく動くように

     

    ■利用環境

    ・なんてことはないWin10
    Twichでゲットした『Downwell』『DeadLight』(Steam版ではない)
    ・HORI PAD3 TURBO PLUS(DirectInput方式)
    ・内蔵グラフィックボードとモニタ(3Dゲームが厳しいのなんの)

     

    ■ 使用ソフト

    1. x360ce 32bit版
    2. x360ce 64bit版
    3. XOutput
    4. XInput Plus

      使ってるコントローラーがXboxの純正だったりするとまたやり方が変わりそうですが、まぁこんなところです。たかがコントローラーでソフト入れすぎですね。だってできなかったんだもの…。

     

    ■注意点

    まずコントローラーのドライバはMicrosoftのです。おそらく挿してすぐ自動で探してくれたものだと思います。非公式のドライバも入れたり頑張ってみた気がするんですが、結局Microsoft製で動くことが判明したのでそのまま使ってるような感じだったかと…。記憶が曖昧なのでそのあたりはご勘弁を…。

    また、無料でもらえるTwichプライムのゲーム、今回はDownwellとDeadLightで試しています。もしかしてわざわざXinputで設定しなくても遊べたりするんですかね。知りませんけど。他のゲームやSteamゲームでの挙動は一切分かりません。ご了承ください。

     

    ◇その1 x360ceの導入                 

    Xboxの純正コントローラー(Xinput方式)でなければ遊べないゲームをHORI PAD(DirectInput方式)で遊ぶために、まずx360ce 32bit版を導入しました。簡単に言えばこのソフトで「ワタシXbox純正コントローラーデスヨー」と思い込ませることができます。素晴らしいですね。設定が完了したらゲームの実行ファイルが格納されているファイルにx360ceで作成されたiniファイルdllファイルをコピペします。その後Downwellを起動。問題なく動きました。

     

    ◇その2 XOutputの導入                

    同じやり方でDeadLightの方もコピペしたのですが、動かず。XOutputを導入しました。XOutputx360ceと違いゲームの中だけでなくOSそのものに「ワタシXbox純正コントローラーデスヨー」と思い込ませることができるソフトです。こちらは毎回手動で実行しなければゲームで使えないソフトになります。x360ceが設定ファイルを作って置くだけのいわば効果永続型のバフだとすれば、XOutputは都度仕様のバフです。一手間が意外と面倒ですが、ショートカットを作っておけば多少は楽になるかと思います。XOutputを起動し、実行した状態でDeadLightを起動するとコントローラーが反応しました。これで使える!!と思ったのも束の間、左スティックの上下が反転していることに気付きます。x360ceに軸反転の機能を見つけることができなかったので、この2つのソフトでどうにかするのは諦めました。

     

    ◇その3 XInput Plusの導入               

     軸の反転を直すために最後の砦、XInput Plusを導入しました。このソフトは、Xinput対応コントローラーのキーコンフィグを変更するためのソフトだそうです。つまり、HORI PADのようにDirectInpu方式のコントローラーを使っている場合はそのままテストしようとしても動きません。もしテストをする場合はXOutputを起動させましょう。公式ではx360ceと連携させることができるとあったので、その通りに設定…できませんでした。32bitとか64bitがどうこうといってエラーになってしまいます。どうやら『DeadLight』が64bit版だったらしく、XInput Plusに設定するx360ceのdllファイルも64bitでなければならないようです。x360ce 32bit版から64bit用のdllファイルの作り方が分からなかったので、x360ce 64bit版を導入しました。その1で作った時と同じように作成したdllファイルXInput Plusに設定したところ、問題なく読み込んでくれました。肝心のキーコンフィグ画面に軸反転のチェックボックスがあるため、そこにチェックを入れて対象のゲームフォルダを選択。設定ファイルが保存できればXInput Plusの出番は終了です。

     

    ◇その4 最終確認                   

    さて、これで準備はすべて整いました。x360ceと連携したXInput Plusで作った設定ファイルはフォルダの隅に佇んでいます。あとはXOutputを実行し、ゲームを起動するだけです。問題なく設定できていれば何事もないように動いてくれますし、間違っていれば変な挙動をするか動かないだけでしょう。高鳴る鼓動、運命の瞬間。握られたその手はガッツポーズに変わりました。成功です。おめでとうございました。

     

    ■あとがき

    その後、右スティックの上下も反転していることに気付きました。同じようにそちらも軸反転にチェックを入れて、設定ファイルを上書き。ゲームも再起動し、今度こそ完璧なゲームパッドになりました。

    今回のやり方はもしかしたら詳しい方から見たら間違っている(面倒になっている)ような箇所もあるかもしれません。あくまで備忘録になりますので、何が起ころうともこちらは関与致しません。一切を自己責任で行ってください。もしPCが壊れていい感じのグラボが手持無沙汰になってしまった時だけ連絡お待ちしております。無償で回収致します。では、積みゲーを消化しなければならないのでこの辺で失礼。