
noteéå¶ãâæèŠâããâèŠããåâãžïœGitHub Pagesã§ããã·ã¥ããŒããäœãïœããŒã¿åæïœ#286
åã«ã¯ã€ã¯ãnoteã®PVãšãã¹ãæ°ãGitHub Actionsã§èªååéããæ¹æ³ãèšäºã«ããã
ã§ãä»åã¯ãã®æ¬¡ãã
ãã£ããCSVãæ¯æ¥ããŸã£ãŠããããã«ãªã£ãŠãã
èŠã«ããã£ããããã¶ã人ã¯ãããªã«èŠè¿ããã
ãããå²ãšããç¬
ãªã®ã§ä»åã¯ãåã«éããnoteã®ããŒã¿ã
ãã©ãŠã¶ã§èŠãããå¯èŠåããæ¹æ³ããŸãšããã
ããããšã¯ããããŸã§éããªãã
HTMLãã¡ã€ã«ã1å眮ã
èšå®ãã»ãŒ1è¡ã ãå€ãã
GitHub Pagesã§éã
ããã§ãPVæšç§»ãšãã©ã³ãã³ã°ãšãè³ç£èšäºã£ãœããã€ãŸã§ã
ãã©ãŠã¶ã§èŠãããããã«ãªãã
äœæããããã·ã¥ããŒãïŒPCåã



ã»ãªãããã§ïŒ
ãã®èšäºã§ã§ããããš
ãã®ããã·ã¥ããŒããå ¥ãããšããããªæãã®ãã®ãèŠãããããã«ãªãã
环èš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/ ãã©ã«ãã«ã¢ããããŒãããã
æµãã¯ãããªæãã
GitHubã§ãªããžããªãéã
docs ãã©ã«ããéã
Add file â Upload files
index.html ãã¢ããããŒã
Commit changes
ãã docs ãã©ã«ãããŸã ç¡ããªãã
docs/index.html ã£ãŠååã§æ°èŠäœæããã°ããã©ã«ãããšäœããã
Step 6ïœGitHub Pagesãæå¹ã«ãã
次ã«ãGitHub Pagesããªã³ã«ããã
ããããšã¯ãããªæãã
ãªããžããªã® Settings ãéã
å·ŠåŽã® Pages ãéã
Source ã Deploy from a branch ã«ãã
ãã©ã³ãã mainããã©ã«ãã /docs ã«ãã
Save
ããã§ãã°ãããããšãURLãçºè¡ãããã
ã ããããããã圢ãã
https://ããªãã®ãŠãŒã¶ãŒå.github.io/ãªããžããªå/ãã®URLããã©ãŠã¶ã§éããŠãããã·ã¥ããŒããåºããå®äºãã
ãã€ããããŸã§ãð
ããŸãããããšãã®ç¢ºèªãã€ã³ã
â ãâïž èšå®ãå¿ èŠã§ãããšåºã
ã ããã repo: ã®èšå®ãã¹ãã
æžãæãå¿ããŠãªãã
ãŠãŒã¶ãŒå/ãªããžããªå ã®åœ¢ã«ãªã£ãŠãã
' ãæ¶ããŠãªãã
ãã®ãžããèŠçŽããŠãªã
â¡ æ°åã â ã®ãŸãŸ
ãã®å Žåã¯ãã ãããæ¬¡ã®ã©ãããã
repoåã®ã¹ãã«ãã¹
ãªããžããªãå ¬éç¶æ ãããªã
data/ ãã©ã«ãã«CSVãç¡ã
trend_analysis.csv ãš asset_articles.csv ããŸã çæãããŠãªã
ç¹ã«æåŸã®2ã€ã¯ããããšèŠèœãšããããã
⢠URLãéããã404ã«ãªã
ããã¯å°ãåŸ ãŠã°çŽãããšãããã
GitHub Pagesã£ãŠãèšå®çŽåŸã¯åæ ã«å°ãæéããããããšãããããã
5ã10åããã眮ããŠããããäžåèŠããšåºãå Žåãããã
ãŸãšã
ä»åãã£ãããšããéã«ãŸãšãããšãããã
index.html ã® repo: ãèªåçšã«å€ãã
docs/ ã«çœ®ã
GitHub Pagesãæå¹ã«ãã
å¿ èŠãªã 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 #ä»äº #ããžãã¹ #èªå #æ¥èš #åŠã³ #å匷 #ã©ã€ããã㯠#æ å ±æŽç #æ¥åæ¹å