メむンコンテンツぞスキップ
芋出し画像

note運営を“感芚”から“芋える化”ぞGitHub Pagesでダッシュボヌドを䜜るデヌタ分析#286

    前にワむは、noteのPVずかスキ数をGitHub Actionsで自動収集する方法を蚘事にした。

    で、今回はその次や。

    せっかくCSVが毎日たたっおいくようになっおも、
    芋にくかったら、たぶん人はそんなに芋返さん。

    これ、割ずある笑

    なので今回は、前に集めたnoteのデヌタを
    ブラりザで芋やすく可芖化する方法をたずめる。

    やるこずは、そこたで重くない。

    • HTMLファむルを1個眮く

    • 蚭定をほが1行だけ倉える

    • GitHub Pagesで開く

    これで、PV掚移ずかランキングずか資産蚘事っぜいや぀たで、
    ブラりザで芋られるようになる。


    䜜成したダッシュボヌドPC向け

    画像
    ダッシュボヌド画面-1
    画像
    ダッシュボヌド画面-2
    画像
    ダッシュボヌド画面-3

    ほな、いくで



    この蚘事でできるこず

    このダッシュボヌドを入れるず、こんな感じのものが芋られるようになる。

    • 环蚈PV・スキ・蚘事数・フォロワヌ数

    • ビュヌずスキの日次掚移

    • Viewランキング / スキランキング

    • スキ率のベスト / ワヌスト

    • PV × スキ率の4象限マップ

    • 資産蚘事の分析

    • 盎近7日間のトレンド動態

    芁するに、

    「なんずなく最近䌞びおる気がする」
    を

    「これが䌞びおる」「これは埋もれおる」「これは地味に残っおる」

    に倉えるや぀や。

    ただ数字を䞊べるだけやなくお、
    運営ログを“芋たくなる画面”にするのが今回の目的やな。


    先に倧事なこずだけ蚀っずく

    このHTMLは、GitHub䞊にあるCSVをブラりザから盎接読みにいく䜜りになっおる。

    せやから、この蚘事のやり方をそのたた䜿うなら、基本は公開リポゞトリ前提で考えおな。

    ぀たり、

    • 自分だけで裏偎のデヌタを貯めたい

    • でも芋せる甚の画面も䜜りたい

    この2぀を䞡立したいなら、

    収集はPrivate
    公開甚の敎圢デヌタやダッシュボヌドはPublic

    みたいに分けるのがわりず盞性ええ。

    前回の有料蚘事でも、
    「集蚈はPrivateで回しお、芋せるCSVだけPublicに出す」
    みたいな運甚はありやで、っお話をしたけど、今回はたさにその“芋る偎”や。


    これ、誰向けなん

    こんな人向けや。

    • note運営を感芚じゃなく、ログで芋たい人

    • 前回の収集蚘事を入れたあず、次に䜕をすればええか探しおる人

    • せっかくCSVを貯めおるのに、正盎ちゃんず芋返せおない人

    • 自分の運営デヌタを、ちょっずカッコよく芋たい人

    • 「分析っおいうより、たずは芋やすくしたい」人

    芁するに、
    集めたデヌタを“ちゃんず䜿える状態”にしたい人向けやな。


    はじめる前に確認しおおくこず

    必芁なのは䞻にこの2぀。

    ① GitHubアカりント

    これは無料でOK。

    ② CSVが入ったリポゞトリ

    このHTMLは、GitHub䞊のCSVを読みにいっお動く。
    せやから、元になるデヌタが必芁や。

    最䜎でも、このぞんがある前提で考えおな。

    • daily_summary.csv

    • articles.csv

    • followers.csv

    • trend_analysis.csv

    • asset_articles.csv

    ここ、地味に倧事や。

    前回の有料蚘事をそのたた入れた人は、
    収集甚のCSVはできおおも、分析甚のCSVがただ出おないこずがある。

    その堎合は、analyze.py を回すステップも远加しおな。

    ぀たり今回は、

    • 前回の蚘事 → 集める土台

    • 今回の蚘事 → 芋る画面

    っお圹割分担やず思っおもらえればOKや。

    ⚠ GitHub Pagesは、GitHub Free では公開Publicリポゞトリで利甚する前提になりたす。

    GitHub Pro 以䞊では非公開PrivateリポゞトリでもPagesを䜿えたす。

    ただし、今回のダッシュボヌドはブラりザからCSVを盎接読みにいく実装なので、このたたの構成で䜿う堎合は、CSVが倖から読める状態になっおいる必芁がありたす。

    「収集はPrivate、公開甚の敎圢デヌタだけPublicに出す」みたいに分ける運甚が盞性ええです。

    党䜓の構成はこんな感じ

    たず先に、完成圢の構成を芋せるで。

    画像
    フォルダ構成画像

    むメヌゞずしおはこんな感じや。

    your-repo/
    ├── data/
    │   ├── articles.csv
    │   ├── asset_articles.csv
    │   ├── daily_summary.csv
    │   ├── followers.csv
    │   ├── period_ranking.csv
    │   └── trend_analysis.csv
    └── docs/
        └── index.html

    ポむントはシンプルで、

    • data/ にCSVがある

    • docs/ にHTMLがある

    • GitHub Pagesで docs/ を公開する

    これだけや。


    Step 1  HTMLファむルを入手する

    この蚘事の末尟に index.html のコヌド党文を掲茉しおいる。コピヌしお、ファむル名を index.html ずしお保存しおほしい。

    コヌドの量が倚くお戞惑うかもしれないが、觊る箇所はStep 3で説明する2か所だけ。

    今はたるごずコピヌするだけでOK。


    Step 2テキスト゚ディタで開く

    次に、そのHTMLファむルをテキスト゚ディタで開く。

    「テキスト゚ディタっお䜕」っお人も心配いらん。

    • Windows → メモ垳

    • Mac → テキスト゚ディット

    で十分や。

    開くず、英語ずか蚘号がずらっず䞊んでお「うわ、無理そう」っお䞀瞬なるかもしれん。
    でも安心しおええ。

    觊るのは次のStep 3の1か所だけや。


    Step 3  CONFIGを曞き換える

    Ctrl+FMacは Command+Fでファむル内怜玢を開き、CONFIG を探しおみお。

    するず、こんな箇所が芋぀かるはずや。

    const CONFIG = {
      repo: 'YOUR_GITHUB_USERNAME/YOUR_REPO_NAME',
      dataPath: 'data',
      excludeTitles: [],
    };

    基本的に倉えるのは repo: の1行だけ。

    倉曎前

    repo: 'YOUR_GITHUB_USERNAME/YOUR_REPO_NAME',

    倉曎埌䟋

    repo: 'yamada-taro/note-stats-public',

    GitHubのリポゞトリペヌゞを開いたずきに䞊郚に衚瀺される「ナヌザヌ名/リポゞトリ名」をそのたた入れればOK。

    前埌のシングルクォヌト'ず末尟のカンマ,は消さないこず。

    その他の蚭定必芁な堎合のみ

    dataPath ず excludeTitles は通垞そのたたで問題ないが、必芁に応じお倉曎できるようにしおおいた。

    画像

    特定の蚘事をランキングから陀倖したい堎合は、こんな感じで远蚘する。

    excludeTitles: ['テスト投皿', '䞋曞き確認甚'],

    蚭定が正しくできおいるか確認する方法

    蚭定を曞き換えずにブラりザで開くず、こんな案内画面が衚瀺される。

    ⚙ 蚭定が必芁です

    この画面が出なくなればOKっおこずや。


    Step 3.5前回の蚘事を入れおる人ぞ

    前回の有料蚘事をベヌスにしおる人は、ここだけ远加で芋おな。

    今回のHTMLは、

    • trend_analysis.csv

    • asset_articles.csv

    も読みにいく。

    せやから、前回の収集だけで止たっおるず、
    画面の䞀郚が衚瀺されないこずがある。

    その堎合は、GitHub Actionsに analyze.py を回す凊理を远加する。

    たずえばこんな感じ。

    - name: Run Analyze Script
      run: python scripts/analyze.py --articles data/articles.csv --out data/

    これを足すず、

    収集 → 集蚈 → 可芖化

    たで぀ながる。

    ここたでくるず、
    前回の蚘事ず今回の蚘事がちゃんずセットで効いおくるで。


    Step 4ファむルを保存する

    曞き換えが終わったら保存。

    • Windows → Ctrl + S

    • Mac → Command + S

    以䞊。
    ここはほんたに普通に保存でええ。


    Step 5GitHubにアップロヌドする

    保存した index.html を、GitHubの docs/ フォルダにアップロヌドする。

    流れはこんな感じ。

    1. GitHubでリポゞトリを開く

    2. docs フォルダを開く

    3. Add file → Upload files

    4. index.html をアップロヌド

    5. Commit changes

    もし docs フォルダがただ無いなら、
    docs/index.html っお名前で新芏䜜成すれば、フォルダごず䜜れる。


    Step 6GitHub Pagesを有効にする

    次に、GitHub Pagesをオンにする。

    やるこずはこんな感じ。

    1. リポゞトリの Settings を開く

    2. 巊偎の Pages を開く

    3. Source を Deploy from a branch にする

    4. ブランチを main、フォルダを /docs にする

    5. Save

    これでしばらくするず、URLが発行される。

    だいたいこういう圢や。

    https://あなたのナヌザヌ名.github.io/リポゞトリ名/

    このURLをブラりザで開いお、ダッシュボヌドが出たら完了や。
    お぀かれさたです🎉


    うたくいかんずきの確認ポむント

    ① 「⚙ 蚭定が必芁です」ず出る

    だいたい repo: の蚭定ミスや。

    • 曞き換え忘れおないか

    • ナヌザヌ名/リポゞトリ名 の圢になっおるか

    • ' を消しおないか

    このぞんを芋盎しおな。


    ② 数字が — のたた

    この堎合は、だいたい次のどれかや。

    • repo名のスペルミス

    • リポゞトリが公開状態じゃない

    • data/ フォルダにCSVが無い

    • trend_analysis.csv ず asset_articles.csv がただ生成されおない

    特に最埌の2぀は、わりず芋萜ずしやすい。


    ③ URLを開いたら404になる

    これは少し埅おば盎るこずもある。

    GitHub Pagesっお、蚭定盎埌は反映に少し時間がかかるこずがあるから、
    5〜10分くらい眮いおからもう䞀回芋るず出る堎合もある。


    たずめ

    今回やったこずを、雑にたずめるずこうや。

    1. index.html の repo: を自分甚に倉える

    2. docs/ に眮く

    3. GitHub Pagesを有効にする

    4. 必芁なら analyze.py も回す

    これで、前回集めたnoteのログを
    ブラりザでかなり芋やすく扱えるようになる。

    前回の蚘事が
    「デヌタを貯める仕組み」
    だずしたら、

    今回の蚘事は「そのデヌタをちゃんず眺める画面」や。

    ここたで぀ながるず、note運営がちょっず倉わる。

    「なんずなく䌞びた気がする」が、
    「これが䌞びおる」「これは埋もれおる」「これは資産っぜい」
    に倉わるからや。

    こういうのっお掟手ではない。
    でも、䞀回芋えるようになるず、じわじわ効いおくる。

    割ずマゞで。


    おわりに

    このツヌルは、個人開発の「䜜っおみた」䜜品や。

    せやから、手厚いサポヌト窓口ずか、
    䜕でも即レスで盎したすみたいな感じではない。
    そこは先に正盎に蚀っずく。

    ただ、今の時代ここはそんなに絶望せんでもええ。

    動かんずきは、

    • ゚ラヌ文

    • この蚘事

    • コヌド

    このぞんをそのたたChatGPTずかClaudeずかGeminiに投げるず、
    䞋手したら人に聞くより早いこずも普通にある笑

    もしこの蚘事が
    「ちょっず参考になった」
    「これ、やっおみたい」
    ず思ったら、スキずかコメントもらえるずうれしい。

    たずはCSVを自動で貯めるずころからやりたい人は、前回の有料蚘事から入るずスムヌズやで。

    で、ここたで実装できたら、あずはHTMLを自分奜みにいじるのもおもろい。

    こんな感じで、芋た目を遊ぶ方向にも広げられる。


    もしこの蚘事が「おもろかった」「ちょっず参考になった」ず思ったら、スキ・フォロヌ・コメントで反応もらえるずうれしい。

    ワむ、スキずコメント、かなり奜物です笑

    ほな、たた。


    補足

    <!DOCTYPE html>
    <html lang="ja">
    <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>note / stats</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.1/chart.umd.min.js"></script>
    <link href="https://fonts.googleapis.com/css2?family=Syne:wght@400;600;700;800&family=JetBrains+Mono:wght@300;400;600;700&display=swap" rel="stylesheet">
    <style>
    :root {
      --bg:        #06080f;
      --bg2:       #0b0e1a;
      --surface:   #0f1221;
      --surface2:  #141729;
      --border:    rgba(255,255,255,0.07);
      --border2:   rgba(255,255,255,0.12);
    
      --cyan:      #00e5ff;
      --cyan-dim:  rgba(0,229,255,0.12);
      --cyan-glow: rgba(0,229,255,0.25);
      --pink:      #ff4d8d;
      --pink-dim:  rgba(255,77,141,0.12);
      --gold:      #ffc947;
      --gold-dim:  rgba(255,201,71,0.12);
      --violet:    #9d6fff;
      --violet-dim:rgba(157,111,255,0.12);
      --green:     #00e5a0;
      --red:       #ff4d6a;
    
      --text:      #f0f4ff;
      --text2:     #b0bcd8;
      --text3:     #6b7599;
    
      --mono: 'JetBrains Mono', monospace;
      --sans: 'Syne', sans-serif;
    
      --r: 6px;
    }
    
    * { margin: 0; padding: 0; box-sizing: border-box; }
    
    body {
      background: var(--bg);
      color: var(--text);
      font-family: var(--mono);
      min-height: 100vh;
      overflow-x: hidden;
    }
    
    /* ─── NOISE OVERLAY ─── */
    body::before {
      content: '';
      position: fixed; inset: 0;
      background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.03'/%3E%3C/svg%3E");
      pointer-events: none; z-index: 999;
    }
    
    /* ─── SCROLLBAR ─── */
    ::-webkit-scrollbar { width: 4px; height: 4px; }
    ::-webkit-scrollbar-track { background: var(--bg); }
    ::-webkit-scrollbar-thumb { background: var(--border2); border-radius: 2px; }
    
    /* ─── HEADER ─── */
    header {
      position: sticky; top: 0; z-index: 100;
      background: rgba(6,8,15,0.92);
      backdrop-filter: blur(20px);
      border-bottom: 1px solid var(--border);
      padding: 0 32px;
      height: 56px;
      display: flex; align-items: center; justify-content: space-between;
    }
    
    .logo {
      font-family: var(--sans);
      font-size: 15px;
      font-weight: 800;
      letter-spacing: 0.05em;
      display: flex; align-items: center; gap: 10px;
    }
    .logo-dot {
      width: 8px; height: 8px; border-radius: 50%;
      background: var(--cyan);
      box-shadow: 0 0 12px var(--cyan);
      animation: pulse 2s ease-in-out infinite;
    }
    @keyframes pulse {
      0%,100% { opacity: 1; transform: scale(1); }
      50% { opacity: 0.5; transform: scale(0.8); }
    }
    .logo-text { color: var(--text); }
    .logo-slash { color: var(--text3); margin: 0 2px; }
    .logo-sub { color: var(--cyan); }
    
    .header-right {
      display: flex; align-items: center; gap: 24px;
    }
    #last-updated {
      font-size: 12px;
      color: var(--text2);
      letter-spacing: 0.1em;
    }
    .header-badge {
      font-size: 9px;
      padding: 3px 8px;
      border: 1px solid var(--border2);
      border-radius: 2px;
      color: var(--text2);
      letter-spacing: 0.15em;
      text-transform: uppercase;
    }
    
    /* ─── LAYOUT ─── */
    .dashboard {
      padding: 0;
      max-width: 1600px;
      margin: 0 auto;
    }
    
    /* ─── STATUS BAND (3KPI) ─── */
    .status-band {
      border-bottom: 1px solid var(--border);
      padding: 28px 32px;
      display: grid;
      grid-template-columns: 1fr 1px 1fr 1px 1fr 2px 1fr;
      align-items: center;
      gap: 0;
      animation: fadeUp 0.6s ease both;
    }
    .band-divider {
      width: 1px;
      height: 40px;
      background: var(--border2);
      justify-self: center;
    }
    .band-divider.thick {
      width: 2px;
      background: var(--border2);
      height: 60px;
    }
    
    .kpi3-item { padding: 0 28px; }
    .kpi3-label {
      font-size: 15px;
      color: var(--text2);
      letter-spacing: 0.1em;
      text-transform: uppercase;
      margin-bottom: 10px;
    }
    .kpi3-row {
      display: flex; align-items: baseline; gap: 10px;
    }
    .kpi3-val {
      font-family: var(--sans);
      font-size: 34px;
      font-weight: 700;
      color: var(--text);
      line-height: 1;
    }
    .kpi3-arrow {
      font-size: 20px;
      line-height: 1;
      font-family: var(--mono);
    }
    .kpi3-arrow.up   { color: var(--green); }
    .kpi3-arrow.down { color: var(--red); }
    .kpi3-arrow.flat { color: var(--text3); }
    .kpi3-sub {
      font-size: 12px;
      color: var(--text2);
      margin-top: 5px;
    }
    
    .status-memo-wrap { padding: 0 28px; }
    .status-memo-label {
      font-size: 18px;
      color: var(--text2);
      letter-spacing: 0.1em;
      text-transform: uppercase;
      margin-bottom: 10px;
    }
    #status-memo-text {
      font-family: var(--sans);
      font-size: 15px;
      font-weight: 600;
      color: var(--cyan);
      line-height: 1.5;
      min-height: 40px;
    }
    
    /* ─── KPI STRIP ─── */
    .kpi-strip {
      display: grid;
      grid-template-columns: repeat(4, 1fr);
      border-bottom: 1px solid var(--border);
      animation: fadeUp 0.6s 0.1s ease both;
    }
    .kpi {
      padding: 24px 32px;
      border-right: 1px solid var(--border);
      position: relative;
      overflow: hidden;
      transition: background 0.2s;
    }
    .kpi:last-child { border-right: none; }
    .kpi::after {
      content: '';
      position: absolute;
      bottom: 0; left: 32px; right: 32px;
      height: 2px;
      border-radius: 1px;
      opacity: 0;
      transition: opacity 0.3s;
    }
    .kpi:nth-child(1)::after { background: var(--cyan); }
    .kpi:nth-child(2)::after { background: var(--pink); }
    .kpi:nth-child(3)::after { background: var(--gold); }
    .kpi:nth-child(4)::after { background: var(--violet); }
    .kpi:hover::after { opacity: 1; }
    .kpi:hover { background: var(--surface); }
    
    .kpi-label {
      font-size: 18px;
      color: var(--text2);
      letter-spacing: 0.12em;
      text-transform: uppercase;
      margin-bottom: 10px;
      display: flex; align-items: center; gap: 6px;
    }
    .kpi-label::before {
      content: '';
      width: 5px; height: 5px; border-radius: 50%;
    }
    .kpi:nth-child(1) .kpi-label::before { background: var(--cyan); box-shadow: 0 0 6px var(--cyan); }
    .kpi:nth-child(2) .kpi-label::before { background: var(--pink); box-shadow: 0 0 6px var(--pink); }
    .kpi:nth-child(3) .kpi-label::before { background: var(--gold); box-shadow: 0 0 6px var(--gold); }
    .kpi:nth-child(4) .kpi-label::before { background: var(--violet); box-shadow: 0 0 6px var(--violet); }
    
    .kpi-value {
      font-family: var(--sans);
      font-size: 48px;
      font-weight: 800;
      line-height: 1;
      letter-spacing: -0.02em;
    }
    .kpi-value.v { color: var(--cyan); }
    .kpi-value.l { color: var(--pink); }
    .kpi-value.a { color: var(--gold); }
    .kpi-value.f { color: var(--violet); }
    
    .kpi-diff {
      margin-top: 8px;
      font-size: 12px;
      color: var(--text3);
      display: flex; align-items: center; gap: 4px;
    }
    .kpi-diff.up   { color: var(--green); }
    .kpi-diff.down { color: var(--red); }
    
    /* ─── GRID ─── */
    .grid {
      display: grid;
      grid-template-columns: 1fr 1fr;
    }
    
    .panel {
      padding: 28px 32px;
      border-bottom: 1px solid var(--border);
      border-right: 1px solid var(--border);
      position: relative;
      animation: fadeUp 0.5s ease both;
    }
    .panel:nth-child(2n) { border-right: none; }
    .panel.full {
      grid-column: 1 / -1;
      border-right: none;
    }
    .panel.third {
      grid-column: span 1;
    }
    
    .panel-header {
      display: flex;
      align-items: center;
      justify-content: space-between;
      margin-bottom: 22px;
    }
    .panel-title {
      font-size: 18px;
      color: var(--text2);
      letter-spacing: 0.1em;
      text-transform: uppercase;
      display: flex; align-items: center; gap: 8px;
    }
    .panel-title-dot {
      width: 4px; height: 4px; border-radius: 50%;
      background: var(--cyan);
      flex-shrink: 0;
    }
    .panel-badge {
      font-size: 10px;
      padding: 2px 8px;
      border: 1px solid var(--border2);
      border-radius: 2px;
      color: var(--text3);
      letter-spacing: 0.1em;
    }
    
    .chart-wrap { position: relative; height: 280px; }
    .chart-wrap.tall { height: 360px; }
    
    /* ─── RANKING ─── */
    .ranking-list {
      display: flex; flex-direction: column; gap: 4px;
      max-height: 380px; overflow-y: auto;
      padding-right: 4px;
    }
    
    .rank-item {
      display: grid;
      grid-template-columns: 28px 1fr 110px;
      align-items: center;
      gap: 10px;
      padding: 9px 12px;
      border-radius: var(--r);
      border: 1px solid transparent;
      background: var(--surface);
      transition: all 0.15s ease;
      cursor: default;
    }
    .rank-item:hover {
      background: var(--surface2);
      border-color: var(--border2);
      transform: translateX(2px);
    }
    
    .rank-num {
      font-size: 11px;
      color: var(--text3);
      text-align: center;
      font-weight: 600;
    }
    .rank-num.gold   { color: var(--gold); }
    .rank-num.silver { color: #a8b0c8; }
    .rank-num.bronze { color: #c87850; }
    
    .rank-title {
      font-size: 16px;
      color: var(--text2);
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
      transition: color 0.15s;
    }
    .rank-item:hover .rank-title { color: var(--text); }
    
    .rank-right {
      display: flex; align-items: center; gap: 8px;
    }
    .rank-bar {
      flex: 1; height: 2px;
      background: var(--border2);
      border-radius: 1px;
      overflow: hidden;
    }
    .rank-bar-fill {
      height: 100%;
      border-radius: 1px;
      transition: width 1s cubic-bezier(0.16,1,0.3,1);
    }
    .rank-val {
      font-size: 12px;
      min-width: 44px;
      text-align: right;
      font-weight: 600;
    }
    .view-bar  { background: var(--cyan); }
    .view-val  { color: var(--cyan); }
    .like-bar  { background: var(--pink); }
    .like-val  { color: var(--pink); }
    .worst-bar { background: var(--red); }
    .worst-val { color: var(--red); }
    
    /* ─── TABLE ─── */
    .table-wrap {
      overflow-x: auto;
      border: 1px solid var(--border);
      border-radius: var(--r);
    }
    .data-table {
      width: 100%;
      border-collapse: separate;
      border-spacing: 0;
      font-size: 12px;
    }
    
    .data-table th {
      padding: 10px 14px;
      background: var(--surface);
      color: var(--text2);
      text-align: left;
      font-size: 12px;
      letter-spacing: 0.08em;
      text-transform: uppercase;
      border-bottom: 1px solid var(--border2);
      white-space: nowrap;
      position: sticky; top: 0; z-index: 5;
    }
    
    .data-table td {
      padding: 10px 14px;
      border-bottom: 1px solid var(--border);
      color: var(--text2);
      font-size: 14px;
      transition: background 0.15s;
    }
    
    .status-badge {
      display: inline-block;
      padding: 2px 8px;
      border-radius: 3px;
      font-size: 10px;
      font-weight: 700;
      letter-spacing: 0.05em;
      white-space: nowrap;
    }
    .badge-hot  { background: rgba(255,77,106,0.15); color: #ff4d6a; border: 1px solid rgba(255,77,106,0.3); }
    .badge-grow { background: rgba(0,229,160,0.12); color: var(--green); border: 1px solid rgba(0,229,160,0.25); }
    .badge-idle { background: rgba(61,68,102,0.3); color: var(--text3); border: 1px solid var(--border); }
    
    .bar-cell-wrap {
      display: flex; align-items: center; gap: 8px;
    }
    .inline-bar {
      width: 80px; height: 5px;
      background: var(--border2);
      border-radius: 3px;
      overflow: hidden;
      flex-shrink: 0;
    }
    .inline-bar-fill {
      height: 100%;
      background: var(--cyan);
      border-radius: 3px;
      opacity: 1;
    }
    .num-right { text-align: right; font-weight: 600; color: var(--text); }
    .num-accent { color: var(--cyan); }
    .num-muted  { color: var(--text3); }
    
    /* ─── TWO-COL WORST ─── */
    .worst-grid {
      display: grid;
      grid-template-columns: 1fr 1fr;
      gap: 32px;
    }
    .worst-section-title {
      font-size: 15px;
      color: var(--text3);
      letter-spacing: 0.2em;
      text-transform: uppercase;
      margin-bottom: 12px;
      display: flex; align-items: center; gap: 6px;
    }
    .worst-section-title::before {
      content: '';
      width: 4px; height: 4px; border-radius: 50%;
      background: var(--red);
    }
    .best-section-title {
      font-size: 15px;
      color: var(--text3);
      letter-spacing: 0.2em;
      text-transform: uppercase;
      margin-bottom: 12px;
      display: flex; align-items: center; gap: 6px;
    }
    .best-section-title::before {
      content: '';
      width: 4px; height: 4px; border-radius: 50%;
      background: var(--green);
    }
    .best-bar { background: var(--green); }
    .best-val { color: var(--green); }
    
    /* ─── LOADING ─── */
    .loading {
      display: flex; align-items: center; justify-content: center;
      height: 160px;
      font-size: 10px;
      color: var(--text3);
      letter-spacing: 0.1em;
    }
    .loading::after {
      content: '...';
      animation: dots 1.2s steps(4, end) infinite;
    }
    @keyframes dots {
      0%,20% { content: ''; }
      40% { content: '.'; }
      60% { content: '..'; }
      80%,100% { content: '...'; }
    }
    
    /* ─── ANIMATIONS ─── */
    @keyframes fadeUp {
      from { opacity: 0; transform: translateY(16px); }
      to   { opacity: 1; transform: translateY(0); }
    }
    .panel:nth-child(1)  { animation-delay: 0.05s; }
    .panel:nth-child(2)  { animation-delay: 0.10s; }
    .panel:nth-child(3)  { animation-delay: 0.15s; }
    .panel:nth-child(4)  { animation-delay: 0.20s; }
    .panel:nth-child(5)  { animation-delay: 0.25s; }
    .panel:nth-child(6)  { animation-delay: 0.30s; }
    .panel:nth-child(7)  { animation-delay: 0.35s; }
    .panel:nth-child(8)  { animation-delay: 0.40s; }
    
    /* ─── RESPONSIVE ─── */
    @media (max-width: 900px) {
      .kpi-strip { grid-template-columns: repeat(2,1fr); }
      .grid { grid-template-columns: 1fr; }
      .panel { border-right: none !important; }
      .status-band { grid-template-columns: 1fr; gap: 16px; }
      .band-divider { display: none; }
      .worst-grid { grid-template-columns: 1fr; }
      header, .kpi, .panel, .kpi3-item, .status-memo-wrap { padding-left: 16px; padding-right: 16px; }
    }
    </style>
    </head>
    <body>
    
    <!-- ═══ HEADER ═══ -->
    <header>
      <div class="logo">
        <div class="logo-dot"></div>
        <span class="logo-text">note</span>
        <span class="logo-slash">/</span>
        <span class="logo-sub">stats</span>
      </div>
      <div class="header-right">
        <div id="last-updated">—</div>
        <div class="header-badge">live</div>
      </div>
    </header>
    
    <div class="dashboard">
    
      <!-- ═══ STATUS BAND (3KPI + memo) ═══ -->
      <div class="status-band">
        <div class="kpi3-item">
          <div class="kpi3-label">Reach Powerリヌチ力</div>
          <div class="kpi3-row">
            <span class="kpi3-val" id="k3-reach">—</span>
            <span class="kpi3-arrow" id="k3-reach-arrow"></span>
          </div>
          <div class="kpi3-sub">PV ÷ 蚘事数</div>
        </div>
        <div class="band-divider"></div>
    
        <div class="kpi3-item">
          <div class="kpi3-label">Action Powerアクション力</div>
          <div class="kpi3-row">
            <span class="kpi3-val" id="k3-action">—</span>
            <span class="kpi3-arrow" id="k3-action-arrow"></span>
          </div>
          <div class="kpi3-sub">スキ ÷ 蚘事数</div>
        </div>
        <div class="band-divider"></div>
    
        <div class="kpi3-item">
          <div class="kpi3-label">η スキ率</div>
          <div class="kpi3-row">
            <span class="kpi3-val" id="k3-eta">—</span>
            <span class="kpi3-arrow" id="k3-eta-arrow"></span>
          </div>
          <div class="kpi3-sub">スキ ÷ PV × 100</div>
        </div>
        <div class="band-divider thick"></div>
    
        <div class="status-memo-wrap">
          <div class="status-memo-label">📋 Status Memo自動蚺断</div>
          <div id="status-memo-text">デヌタ読み蟌み䞭...</div>
        </div>
      </div>
    
      <!-- ═══ KPI STRIP ═══ -->
      <div class="kpi-strip">
        <div class="kpi">
          <div class="kpi-label">Total Views</div>
          <div class="kpi-value v" id="kpi-views">—</div>
          <div class="kpi-diff" id="kpi-views-diff">前日比</div>
        </div>
        <div class="kpi">
          <div class="kpi-label">Total Likes</div>
          <div class="kpi-value l" id="kpi-likes">—</div>
          <div class="kpi-diff" id="kpi-likes-diff">前日比</div>
        </div>
        <div class="kpi">
          <div class="kpi-label">Articles</div>
          <div class="kpi-value a" id="kpi-articles">—</div>
          <div class="kpi-diff">公開蚘事数</div>
        </div>
        <div class="kpi">
          <div class="kpi-label">Followers</div>
          <div class="kpi-value f" id="kpi-followers">—</div>
          <div class="kpi-diff">フォロワヌ数</div>
        </div>
      </div>
    
      <!-- ═══ PANELS ═══ -->
      <div class="grid">
    
        <!-- 日次掚移 -->
        <div class="panel">
          <div class="panel-header">
            <div class="panel-title">
              <span class="panel-title-dot" style="background:var(--cyan)"></span>
              ビュヌ / スキ 日次掚移
            </div>
            <span class="panel-badge">60D</span>
          </div>
          <div class="chart-wrap"><canvas id="chart-daily"></canvas></div>
        </div>
    
        <!-- ビュヌランキング -->
        <div class="panel">
          <div class="panel-header">
            <div class="panel-title">
              <span class="panel-title-dot" style="background:var(--cyan)"></span>
              View ランキング TOP 20
            </div>
            <span class="panel-badge" style="color:var(--cyan);border-color:var(--cyan-dim)">PV</span>
          </div>
          <div class="ranking-list" id="ranking-views"><div class="loading">loading</div></div>
        </div>
    
        <!-- フォロワヌ掚移 -->
        <div class="panel">
          <div class="panel-header">
            <div class="panel-title">
              <span class="panel-title-dot" style="background:var(--violet)"></span>
              フォロワヌ 日次掚移
            </div>
            <span class="panel-badge">ALL</span>
          </div>
          <div class="chart-wrap"><canvas id="chart-followers"></canvas></div>
        </div>
    
        <!-- スキランキング -->
        <div class="panel">
          <div class="panel-header">
            <div class="panel-title">
              <span class="panel-title-dot" style="background:var(--pink)"></span>
              スキ ランキング TOP 20
            </div>
            <span class="panel-badge" style="color:var(--pink);border-color:var(--pink-dim)">LIKE</span>
          </div>
          <div class="ranking-list" id="ranking-likes"><div class="loading">loading</div></div>
        </div>
    
        <!-- ワヌスト10 -->
        <div class="panel full">
          <div class="panel-header">
            <div class="panel-title">
              <span class="panel-title-dot" style="background:var(--red)"></span>
              改善候補 ワヌスト分析
            </div>
            <span class="panel-badge" style="color:var(--red);border-color:rgba(255,77,106,0.2)">WORST 10</span>
          </div>
          <!-- ワヌスト -->
          <div class="worst-grid" style="margin-bottom:28px">
            <div>
              <div class="worst-section-title">スキ率 ワヌスト読たれおるのに刺さっおない</div>
              <div class="ranking-list" id="ranking-worst-eta"><div class="loading">loading</div></div>
            </div>
            <div>
              <div class="worst-section-title">View数 ワヌスト埋もれおいる蚘事</div>
              <div class="ranking-list" id="ranking-worst-views"><div class="loading">loading</div></div>
            </div>
          </div>
    
          <!-- 区切り -->
          <div style="border-top:1px solid var(--border);margin-bottom:28px;position:relative">
            <span style="position:absolute;top:-9px;left:50%;transform:translateX(-50%);background:var(--bg2);padding:0 16px;font-size:9px;color:var(--text3);letter-spacing:0.25em">VS</span>
          </div>
    
          <!-- ベスト -->
          <div class="worst-grid">
            <div>
              <div class="best-section-title">スキ率 ベスト読たれお、か぀刺さった</div>
              <div class="ranking-list" id="ranking-best-eta"><div class="loading">loading</div></div>
            </div>
            <div>
              <div class="best-section-title">View数 ベスト最も読たれた蚘事</div>
              <div class="ranking-list" id="ranking-best-views"><div class="loading">loading</div></div>
            </div>
          </div>
        </div>
    
        <!-- 4象限 -->
        <div class="panel full">
          <div class="panel-header">
            <div class="panel-title">
              <span class="panel-title-dot" style="background:var(--violet)"></span>
              4象限マップPV × スキ率
            </div>
            <span class="panel-badge">SCATTER</span>
          </div>
          <div class="chart-wrap tall"><canvas id="chart-quad"></canvas></div>
        </div>
    
        <!-- 資産蚘事 -->
        <div class="panel full">
          <div class="panel-header">
            <div class="panel-title">
              <span class="panel-title-dot" style="background:var(--gold)"></span>
              資産蚘事分析バヌスト埌の定着確認
            </div>
            <span class="panel-badge" style="color:var(--gold);border-color:var(--gold-dim)">ASSET</span>
          </div>
          <div class="table-wrap">
            <table class="data-table">
              <thead>
                <tr>
                  <th style="width:44px">#</th>
                  <th>タむトル</th>
                  <th>期間 PV 増加</th>
                  <th style="width:80px">皌働日数</th>
                  <th>掚移</th>
                </tr>
              </thead>
              <tbody id="asset-body">
                <tr><td colspan="5" class="loading">スキャン䞭</td></tr>
              </tbody>
            </table>
          </div>
        </div>
    
        <!-- トレンド詳现 -->
        <div class="panel full">
          <div class="panel-header">
            <div class="panel-title">
              <span class="panel-title-dot" style="background:var(--pink)"></span>
              トレンド動態7日間詳现
            </div>
            <span class="panel-badge">← SCROLL →</span>
          </div>
          <div class="table-wrap">
            <table class="data-table">
              <thead>
                <tr>
                  <th class="sticky-1" style="width:80px">状態</th>
                  <th class="sticky-2">タむトル</th>
                  <th>环蚈PV</th>
                  <th>スコア</th>
                  <th>-1日</th><th>-2日</th><th>-3日</th><th>-4日</th><th>-5日</th><th>-6日</th><th>-7日</th>
                </tr>
              </thead>
              <tbody id="trend-body">
                <tr><td colspan="11" class="loading">読み蟌み䞭</td></tr>
              </tbody>
            </table>
          </div>
        </div>
    
      </div><!-- /grid -->
    </div><!-- /dashboard -->
    
    <script>
    // ╔══════════════════════════════════════════════════════════════╗
    // ║  ⚙  CONFIG — ここだけ曞き換えおください                    ║
    // ╚══════════════════════════════════════════════════════════════╝
    
    const CONFIG = {
    
      // ① あなたの GitHub ナヌザヌ名 ず リポゞトリ名
      //    圢匏: 'ナヌザヌ名/リポゞトリ名'
      //    䟋  : 'yamada-taro/note-stats-public'
      repo: 'YOUR_GITHUB_USERNAME/YOUR_REPO_NAME',
    
      // ② dataフォルダのパスリポゞトリ盎䞋に data/ がある堎合はそのたた
      //    倉曎䞍芁なこずがほずんどです
      dataPath: 'data',
    
      // ③ ランキングから陀倖したい蚘事タむトル
      //    䞍芁なら空配列 [] のたたでOK
      //    䟋: ['䞋曞き蚘事', 'テスト投皿']
      excludeTitles: [],
    
    };
    
    // ─── 以䞋は倉曎䞍芁 ───────────────────────────────────────────
    const EXCLUDED_TITLES = CONFIG.excludeTitles;
    const BASE = `https://raw.githubusercontent.com/${CONFIG.repo}/main/${CONFIG.dataPath}/`;
    
    async function fetchCSV(name) {
      const res = await fetch(BASE + name + '.csv?t=' + Date.now());
      if (!res.ok) throw new Error(name + ' not found');
      const text = await res.text();
      const lines = text.trim().split('\n');
      const headers = lines[0].split(',').map(h => h.trim());
      return lines.slice(1).map(line => {
        const vals = line.split(',');
        const obj = {};
        headers.forEach((h, i) => obj[h] = (vals[i] || '').trim());
        return obj;
      });
    }
    
    /* ─── Chart.js defaults ─── */
    const GRID_COLOR = 'rgba(255,255,255,0.05)';
    Chart.defaults.color = '#3d4466';
    Chart.defaults.font.family = "'JetBrains Mono', monospace";
    Chart.defaults.font.size = 11;
    
    const baseOpts = () => ({
      responsive: true,
      maintainAspectRatio: false,
      animation: { duration: 1000, easing: 'easeOutQuart' },
      plugins: {
        legend: {
          display: true,
          labels: { boxWidth: 8, padding: 16, color: 'rgba(255,255,255,0.85)', usePointStyle: true } // 凡䟋のカラヌ指定
        },
        tooltip: {
          backgroundColor: '#0f1221',
          borderColor: 'rgba(255,255,255,0.1)',
          borderWidth: 1,
          titleColor: '#e8ecf8',
          bodyColor: '#8891b0',
          padding: 12,
          cornerRadius: 6
        }
      },
      scales: {
        x: {
          grid: { color: GRID_COLOR },
          ticks: {
            maxTicksLimit: 10,
            color: 'rgba(255,255,255,0.85)'
          }
        },
        y: {
          grid: { color: GRID_COLOR },
          ticks: {
            color: 'rgba(255,255,255,0.85)'
          }
        }
      }
    });
    
    /* ─── KPI diff ─── */
    function setDiff(id, val) {
      if (isNaN(val)) return;
      const el = document.getElementById(id);
      const sign = val >= 0 ? '▲' : 'â–Œ';
      el.textContent = sign + ' ' + Math.abs(val).toFixed(1) + '% 前日比';
      el.className = 'kpi-diff ' + (val >= 0 ? 'up' : 'down');
    }
    
    /* ─── 日次掚移 ─── */
    function buildDaily(rows) {
      const r = rows.slice(-60);
      const labels = r.map(d => d['日付'] ? d['日付'].slice(5) : '');
      const views = r.map(d => Number(d['ビュヌ合蚈']));
      const likes = r.map(d => Number(d['スキ合蚈']));
      new Chart(document.getElementById('chart-daily'), {
        type: 'bar',
        data: {
          labels,
          datasets: [
            {
              type: 'bar', label: 'スキ合蚈', data: likes,
              backgroundColor: 'rgba(255,77,141,0.35)',
              borderColor: 'rgba(255,77,141,0.8)',
              borderWidth: 1, yAxisID: 'y1', borderRadius: 2
            },
            {
              type: 'line', label: 'ビュヌ合蚈', data: views,
              borderColor: '#00e5ff',
              backgroundColor: 'rgba(0,229,255,0.04)',
              fill: true, tension: 0.35,
              pointRadius: 0, pointHoverRadius: 4,
              borderWidth: 2, yAxisID: 'y'
            }
          ]
        },
        options: {
          ...baseOpts(),
          scales: {
            x: {
              grid: { color: GRID_COLOR },
              ticks: {
                maxTicksLimit: 10,
                color: 'rgba(255,255,255,0.85)' // X軞の文字色
              }
            },
            y: {
              position: 'left',
              grid: { color: GRID_COLOR },
              ticks: {
                color: 'rgba(255,255,255,0.85)' // Y軞の文字色
              }
            },
            y1: {
              position: 'right',
              grid: { display: false },
              ticks: {
                color: 'rgba(255,255,255,0.85)' // Y1軞の文字色
              }
            }
          }
        }
      });
    }
    
    /* ─── ランキング ─── */
    function buildRanking(containerId, rows, valueKey, barClass, valClass) {
      if (!rows.length) return;
      const latest = rows.reduce((a, b) => a.date > b.date ? a : b).date;
      const sorted = rows.filter(r => r.date === latest && !EXCLUDED_TITLES.includes(r.title))
        .sort((a, b) => Number(b[valueKey]) - Number(a[valueKey])).slice(0, 20);
      const max = Number(sorted[0][valueKey] || 1);
      const medal = i => i === 0 ? 'gold' : i === 1 ? 'silver' : i === 2 ? 'bronze' : '';
      document.getElementById(containerId).innerHTML = sorted.map((r, i) => `
        <div class="rank-item">
          <span class="rank-num ${medal(i)}">${String(i + 1).padStart(2, '0')}</span>
          <span class="rank-title" title="${r.title}">${r.title}</span>
          <div class="rank-right">
            <div class="rank-bar">
              <div class="rank-bar-fill ${barClass}" style="width:${(Number(r[valueKey]) / max * 100).toFixed(1)}%"></div>
            </div>
            <span class="rank-val ${valClass}">${Number(r[valueKey]).toLocaleString()}</span>
          </div>
        </div>`).join('');
    }
    
    /* ─── フォロワヌ ─── */
    function buildFollowers(summary, followers) {
      const fRows = followers.filter(r => r['フォロワヌ数']);
      let labels, data;
      if (fRows.length > 1) {
        labels = fRows.map(r => r['日付'] ? r['日付'].slice(5) : '');
        data = fRows.map(r => Number(r['フォロワヌ数']));
      } else {
        const s = summary.filter(r => r['フォロワヌ数']);
        labels = s.map(r => r['日付'] ? r['日付'].slice(5) : '');
        data = s.map(r => Number(r['フォロワヌ数']));
      }
      new Chart(document.getElementById('chart-followers'), {
        type: 'line',
        data: {
          labels,
          datasets: [{
            label: 'フォロワヌ数', data,
            borderColor: '#9d6fff',
            backgroundColor: 'rgba(157,111,255,0.06)',
            fill: true, tension: 0.35,
            pointRadius: 0, pointHoverRadius: 4,
            borderWidth: 2
          }]
        },
        options: baseOpts()
      });
    }
    
    /* ─── 4象限 ─── */
    function buildQuad(rows) {
      const data = rows.map(r => ({
        x: parseFloat(r['ビュヌ/蚘事']) || 0,
        y: parseFloat(r['スキ率(%)']) || 0,
        label: r['日付']
      }));
      new Chart(document.getElementById('chart-quad'), {
        type: 'scatter',
        data: {
          datasets: [{
            label: '日次動態',
            data,
            backgroundColor: 'rgba(157,111,255,0.5)',
            pointRadius: 5,
            pointHoverRadius: 7
          }]
        },
        options: {
          ...baseOpts(),
          scales: {
            x: {
              grid: { color: GRID_COLOR },
              ticks: {
                maxTicksLimit: 10,
                color: 'rgba(255,255,255,0.85)'
              },
              title: {
                display: true,
                text: 'Views / Article',
                color: 'rgba(255,255,255,0.85)'
              }
            },
            y: {
              grid: { color: GRID_COLOR },
              ticks: {
                color: 'rgba(255,255,255,0.85)'
              },
              title: {
                display: true,
                text: 'Like Rate (%)',
                color: 'rgba(255,255,255,0.85)'
              }
            }
          }
        }
      });
    }
    
    /* ─── 資産蚘事 ─── */
    function buildAssetTable(rows) {
      const tbody = document.getElementById('asset-body');
      if (!rows.length) { tbody.innerHTML = '<tr><td colspan="5" style="color:var(--text3);padding:20px">No asset data</td></tr>'; return; }
      const maxGain = Math.max(...rows.map(r => Number(r['期間増加PV'] || 0)), 1);
      tbody.innerHTML = rows.slice(0, 15).map((r, i) => {
        const gain = Number(r['期間増加PV'] || 0);
        const barW = (gain / maxGain * 100).toFixed(1);
        const trend = r['掚移PV'] || '---';
        return `
          <tr>
            <td class="num-muted" style="text-align:center">${i + 1}</td>
            <td style="max-width:320px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:var(--text2)">${r.title}</td>
            <td>
              <div class="bar-cell-wrap">
                <div class="inline-bar"><div class="inline-bar-fill" style="width:${barW}%"></div></div>
                <span class="num-accent" style="font-weight:700;font-size:16px">${gain.toLocaleString()}</span>
              </div>
            </td>
            <td class="num-right" style="color:var(--gold);font-size:16px;font-weight:700;text-align:center">${r['皌働日数']}</td>
            <td style="font-size:15px;color:var(--text3);white-space:nowrap">${trend}</td>
          </tr>`;
      }).join('');
    }
    
    /* ─── トレンド詳现 ─── */
    function buildTrendDetailed(rows) {
      const tbody = document.getElementById('trend-body');
      if (!rows.length) return;
    
      tbody.innerHTML = rows.slice(0, 20).map(r => {
        const state = r['状態'] || '';
        let badgeClass = 'badge-idle';
        if (state.includes('🔥')) badgeClass = 'badge-hot';
        else if (state.includes('🟢')) badgeClass = 'badge-grow';
    
        const dailyCells = [1, 2, 3, 4, 5, 6, 7].map(i => {
          const v = Number(r[`-${i}日PV`] || 0);
          return `<td class="${v > 0 ? 'num-right' : 'num-muted'}" style="text-align:right">${v || '—'}</td>`;
        }).join('');
    
        return `
          <tr>
            <td class="sticky-1"><span class="status-badge ${badgeClass}">${state}</span></td>
            <td class="sticky-2" style="white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:var(--text2)">${r['title']}</td>
            <td class="num-right">${Number(r['环蚈PV']).toLocaleString()}</td>
            <td class="num-accent" style="text-align:center;font-weight:700">${r['トレンドスコア']}</td>
            ${dailyCells}
          </tr>`;
      }).join('');
    }
    
    /* ─── 3KPI + 状態メモ ─── */
    function build3KPI(summary) {
      if (summary.length < 2) return;
      const today = summary[summary.length - 1];
      const yesterday = summary[summary.length - 2];
      const ac = Number(today['蚘事数']) || 1;
      const acY = Number(yesterday['蚘事数']) || 1;
    
      const reach  = Number(today['ビュヌ合蚈']) / ac;
      const reachY = Number(yesterday['ビュヌ合蚈']) / acY;
      const action  = Number(today['スキ合蚈']) / ac;
      const actionY = Number(yesterday['スキ合蚈']) / acY;
      const eta  = Number(today['スキ合蚈']) / (Number(today['ビュヌ合蚈']) || 1) * 100;
      const etaY = Number(yesterday['スキ合蚈']) / (Number(yesterday['ビュヌ合蚈']) || 1) * 100;
    
      function arrow(now, prev) {
        const d = now - prev;
        if (d > prev * 0.01) return { s: '↑', c: 'up' };
        if (d < -prev * 0.01) return { s: '↓', c: 'down' };
        return { s: '→', c: 'flat' };
      }
    
      const aR = arrow(reach, reachY);
      const aA = arrow(action, actionY);
      const aE = arrow(eta, etaY);
    
      document.getElementById('k3-reach').textContent = reach.toFixed(1);
      document.getElementById('k3-action').textContent = action.toFixed(2);
      document.getElementById('k3-eta').textContent = eta.toFixed(2) + '%';
    
      [['reach', aR], ['action', aA], ['eta', aE]].forEach(([id, a]) => {
        const el = document.getElementById(`k3-${id}-arrow`);
        el.textContent = a.s;
        el.className = `kpi3-arrow ${a.c}`;
      });
    
      const key = `${aR.c}_${aA.c}_${aE.c}`;
      const memos = {
        'up_up_up':      '🔥 党指暙䞊昇。コンテンツ・タむトル・スキ率すべお奜調',
        'up_up_flat':    '📈 リヌチもアクションも増加。スキ率は維持で安定成長',
        'up_up_down':    '⚠ 露出ずスキは増えたがスキ率は䜎䞋。量で皌いでいる状態',
        'up_flat_up':    '✅ リヌチ増・スキ率改善。質の高い流入が増えおいる',
        'up_flat_flat':  '📊 リヌチだけ増加。スキ率は倉わらず。タむトル効果の可胜性',
        'up_flat_down':  '⚠ 露出は増えたが刺さっおない。タむトル勝ちの可胜性',
        'up_down_up':    '🀔 PV増・スキ枛・スキ率䞊昇。デヌタ確認掚奚',
        'up_down_flat':  '📉 リヌチ増だがスキ暪ばい。内容が刺さっおいない',
        'up_down_down':  '❌ 露出は増えたが党く刺さっおない。内容たたはタヌゲットの芋盎しを',
        'flat_up_up':    '✹ PV暪ばいでスキずスキ率が䞊昇。コアなファンに届いおいる',
        'flat_up_flat':  '👍 アクション力が䞊がっおいる。スキしやすい蚘事が増えた可胜性',
        'flat_up_down':  '🔄 スキは増えたがスキ率は䜎䞋。PVも増えた可胜性',
        'flat_flat_up':  '💡 党䜓暪ばいだがスキ率改善。静かに質が䞊がっおいる',
        'flat_flat_flat':'😐 党指暙暪ばい。倉化なし。新しいアクションを怜蚎',
        'flat_flat_down':'😶 スキ率だけ䜎䞋。最近の蚘事の質を確認',
        'flat_down_up':  '🀔 スキ枛・スキ率䞊昇は矛盟。デヌタ確認掚奚',
        'flat_down_flat':'📉 アクション力䜎䞋。スキしにくい蚘事が続いおいる',
        'flat_down_down':'⚠ アクション・スキ率ずもに䜎䞋。内容の芋盎しタむミング',
        'down_up_up':    '🌱 PV枛でもスキずスキ率が䞊昇。少数粟鋭に届いおいる',
        'down_up_flat':  '🔄 PV枛だがスキは増加。ファン向け蚘事が増えた可胜性',
        'down_up_down':  '📉 PV枛による盞察効果の可胜性。実態確認を',
        'down_flat_up':  '💀 PV枛・スキ率改善。深いファンには刺さっおいる',
        'down_flat_flat':'📉 リヌチのみ䜎䞋。スキ率は維持。コンテンツ質は悪くない',
        'down_flat_down':'⚠ リヌチ䜎䞋・スキ率も䜎䞋。発信ペヌスや内容を芋盎し',
        'down_down_up':  '💀 PV枛による盞察効果の可胜性',
        'down_down_flat':'😔 リヌチ・アクション䜎䞋。停滞期の可胜性',
        'down_down_down':'🚚 党指暙䜎䞋。内容・頻床・タむトルを総芋盎し',
      };
      document.getElementById('status-memo-text').textContent = memos[key] || '状態を解析䞭...';
    }
    
    /* ─── ワヌスト10 ─── */
    function buildWorstRanking(rows) {
      if (!rows.length) return;
      const latest = rows.reduce((a, b) => a.date > b.date ? a : b).date;
      const all = rows.filter(r =>
        r.date === latest &&
        Number(r['read_count']) > 0 &&
        !EXCLUDED_TITLES.includes(r.title)
      );
    
      // スキ率ワヌスト50PV以䞊
      const etaWorst = all
        .filter(r => Number(r['read_count']) >= 50)
        .map(r => ({ ...r, eta: Number(r['like_count']) / Number(r['read_count']) * 100 }))
        .sort((a, b) => a.eta - b.eta)
        .slice(0, 10);
      const etaMax = Math.max(...etaWorst.map(r => r.eta), 1);
    
      document.getElementById('ranking-worst-eta').innerHTML = etaWorst.map((r, i) => `
        <div class="rank-item">
          <span class="rank-num">${String(i + 1).padStart(2, '0')}</span>
          <span class="rank-title" title="${r.title}">${r.title}</span>
          <div class="rank-right">
            <div class="rank-bar"><div class="rank-bar-fill worst-bar" style="width:${(r.eta / etaMax * 100).toFixed(1)}%"></div></div>
            <span class="rank-val worst-val">${r.eta.toFixed(1)}%</span>
          </div>
        </div>`).join('');
    
      // Viewワヌスト
      const viewWorst = [...all].sort((a, b) => Number(a['read_count']) - Number(b['read_count'])).slice(0, 10);
      const viewMax = Math.max(...viewWorst.map(r => Number(r['read_count'])), 1);
    
      document.getElementById('ranking-worst-views').innerHTML = viewWorst.map((r, i) => `
        <div class="rank-item">
          <span class="rank-num">${String(i + 1).padStart(2, '0')}</span>
          <span class="rank-title" title="${r.title}">${r.title}</span>
          <div class="rank-right">
            <div class="rank-bar"><div class="rank-bar-fill worst-bar" style="width:${(Number(r['read_count']) / viewMax * 100).toFixed(1)}%"></div></div>
            <span class="rank-val worst-val">${Number(r['read_count']).toLocaleString()}</span>
          </div>
        </div>`).join('');
    }
    
    
    /* ─── ベスト10 ─── */
    function buildBestRanking(rows) {
      if (!rows.length) return;
      const latest = rows.reduce((a, b) => a.date > b.date ? a : b).date;
      const all = rows.filter(r =>
        r.date === latest &&
        Number(r['read_count']) > 0 &&
        !EXCLUDED_TITLES.includes(r.title)
      );
    
      // スキ率ベスト50PV以䞊
      const etaBest = all
        .filter(r => Number(r['read_count']) >= 50)
        .map(r => ({ ...r, eta: Number(r['like_count']) / Number(r['read_count']) * 100 }))
        .sort((a, b) => b.eta - a.eta)
        .slice(0, 10);
      const etaMax = Math.max(...etaBest.map(r => r.eta), 1);
    
      const medal = i => i === 0 ? 'gold' : i === 1 ? 'silver' : i === 2 ? 'bronze' : '';
      document.getElementById('ranking-best-eta').innerHTML = etaBest.map((r, i) => `
        <div class="rank-item">
          <span class="rank-num ${medal(i)}">${String(i + 1).padStart(2, '0')}</span>
          <span class="rank-title" title="${r.title}">${r.title}</span>
          <div class="rank-right">
            <div class="rank-bar"><div class="rank-bar-fill best-bar" style="width:${(r.eta / etaMax * 100).toFixed(1)}%"></div></div>
            <span class="rank-val best-val">${r.eta.toFixed(1)}%</span>
          </div>
        </div>`).join('');
    
      // Viewベスト
      const viewBest = [...all].sort((a, b) => Number(b['read_count']) - Number(a['read_count'])).slice(0, 10);
      const viewMax = Math.max(...viewBest.map(r => Number(r['read_count'])), 1);
    
      document.getElementById('ranking-best-views').innerHTML = viewBest.map((r, i) => `
        <div class="rank-item">
          <span class="rank-num ${medal(i)}">${String(i + 1).padStart(2, '0')}</span>
          <span class="rank-title" title="${r.title}">${r.title}</span>
          <div class="rank-right">
            <div class="rank-bar"><div class="rank-bar-fill best-bar" style="width:${(Number(r['read_count']) / viewMax * 100).toFixed(1)}%"></div></div>
            <span class="rank-val best-val">${Number(r['read_count']).toLocaleString()}</span>
          </div>
        </div>`).join('');
    }
    /* ─── CONFIG チェック ─── */
    function validateConfig() {
      if (CONFIG.repo === 'YOUR_GITHUB_USERNAME/YOUR_REPO_NAME' || !CONFIG.repo.includes('/')) {
        document.getElementById('last-updated').textContent = '⚠ CONFIG未蚭定';
        document.querySelector('.dashboard').innerHTML = `
          <div style="display:flex;align-items:center;justify-content:center;min-height:60vh;padding:32px">
            <div style="max-width:560px;border:1px solid rgba(255,201,71,0.3);border-radius:8px;padding:40px;background:rgba(255,201,71,0.05)">
              <div style="font-size:22px;font-weight:700;color:#ffc947;margin-bottom:16px">⚙ 蚭定が必芁です</div>
              <div style="color:#b0bcd8;line-height:1.9;font-size:14px">
                このファむルを䜿うには、スクリプト冒頭の <code style="color:#00e5ff;background:rgba(0,229,255,0.1);padding:2px 6px;border-radius:3px">CONFIG</code> を線集しおください。<br><br>
                <strong style="color:#f0f4ff">① HTMLファむルをテキスト゚ディタで開く</strong><br>
                メモ垳 / VSCode など䜕でもOK<br><br>
                <strong style="color:#f0f4ff">② 以䞋の1行を曞き換える</strong><br>
                <div style="background:#0f1221;border:1px solid rgba(255,255,255,0.1);border-radius:6px;padding:14px;margin:12px 0;font-family:monospace;font-size:13px">
                  <span style="color:#6b7599">// 倉曎前</span><br>
                  <span style="color:#ff4d6a">repo: 'YOUR_GITHUB_USERNAME/YOUR_REPO_NAME'</span><br><br>
                  <span style="color:#6b7599">// 倉曎埌䟋</span><br>
                  <span style="color:#00e5a0">repo: 'yamada-taro/note-stats-public'</span>
                </div>
                <strong style="color:#f0f4ff">③ ファむルを保存しおブラりザで再床開く</strong>
              </div>
            </div>
          </div>`;
        return false;
      }
      return true;
    }
    
    /* ─── INIT ─── */
    async function init() {
      if (!validateConfig()) return;
      try {
        const [summary, articles, followers, trend, assets] = await Promise.all([
          fetchCSV('daily_summary'),
          fetchCSV('articles'),
          fetchCSV('followers'),
          fetchCSV('trend_analysis'),
          fetchCSV('asset_articles')
        ]);
    
        const latest = summary[summary.length - 1];
        document.getElementById('kpi-views').textContent    = Number(latest['ビュヌ合蚈']).toLocaleString();
        document.getElementById('kpi-likes').textContent    = Number(latest['スキ合蚈']).toLocaleString();
        document.getElementById('kpi-articles').textContent = Number(latest['蚘事数']).toLocaleString();
        document.getElementById('kpi-followers').textContent = latest['フォロワヌ数'] || '—';
        document.getElementById('last-updated').textContent = `updated ${latest['日付']} ${latest['曎新時刻'] || ''}`;
    
        setDiff('kpi-views-diff', parseFloat(latest['ビュヌ前日比(%)']));
        setDiff('kpi-likes-diff', parseFloat(latest['スキ前日比(%)']));
    
        build3KPI(summary);
        buildDaily(summary);
        buildRanking('ranking-views', articles, 'read_count', 'view-bar', 'view-val');
        buildFollowers(summary, followers);
        buildRanking('ranking-likes', articles, 'like_count', 'like-bar', 'like-val');
        buildWorstRanking(articles);
        buildBestRanking(articles);
        buildQuad(summary);
        buildAssetTable(assets);
        buildTrendDetailed(trend);
    
      } catch (e) {
        console.error(e);
        document.getElementById('last-updated').textContent = 'error: ' + e.message;
      }
    }
    
    init();
    </script>
    </body>
    </html>

    #note運営 #GitHubPages #GitHubActions #デヌタ分析 #ダッシュボヌド #可芖化 #HTML #CSV #運営分析 #アクセス解析 #note #仕事 #ビゞネス #自分 #日蚘 #孊び #勉匷 #ラむフハック #情報敎理 #業務改善


     
     
    デヌタず生成AIで「぀くっお壊しお、たた組み盎す」実隓ログ。 プロンプト・AIアヌト・分析ツヌルを公開䞭。 ツヌル・蚘事ネタを持ち垰れる堎所です。 呌び名にヌやんきたさんPR・ご盞談歓迎Amazonア゜シ゚むト参加䞭

    あなたぞのおすすめ