From 'Intuition' to 'Visualization' in note Management | Creating a Dashboard with GitHub Pages | Data Analysis | #286
Previously, I wrote an article about how to automatically collect note PVs and like counts using GitHub Actions.
So, this is the next step.
Even if you manage to accumulate CSV data every day,
if it's hard to read, people probably won't look back at it much.
This happens quite often, haha.
So this time, I will summarize
how to visualize the note data collected previously in a browser in an easy-to-read format.
The tasks involved aren't that heavy.
Place one HTML file
Change the settings by almost just one line
Open it with GitHub Pages
With this, you will be able to view things like PV trends, rankings, and even your asset articles in your browser.
Created dashboard: For PC



Alright, let's go!
What you can do with this article
Once you install this dashboard, you will be able to see things like this.
Cumulative PVs, likes, number of articles, and follower count
Daily trends of views and likes
View ranking / Like ranking
Best / Worst Like Rates
PV x Like Rate Quadrant Map
Analysis of Evergreen Articles
Trend Dynamics for the Last 7 Days
In short,
“I feel like things have been growing lately”
into
“This is growing,” “This is buried,” “This is quietly remaining”
is what this turns it into.
It's not just about lining up numbers; the goal this time is to
make the management log a 'screen you want to look at' as well.
Let me say the important thing first.
This HTML is built to read the CSV on GitHub directly from the browser.
So, if you use the method in this article as is, basically assume it's for a public repository.
In other words,
I want to store backend data just for myself
but I also want to create a screen for showing it
If you want to achieve both of these,
Collection is Private
Formatted data for display and the dashboard are Public
separating them like this works quite well.
In my previous paid article, I mentioned that 'running collection in Private and only outputting the CSV for display to Public' is a viable operation, and this time is exactly about that 'viewing side'.
Who is this for?
This is for people like this.
People who want to see their note management through logs rather than intuition
People who are looking for what to do next after collecting articles in the previous step
People who are saving CSVs but honestly aren't really looking back at them properly
People who want to see their management data in a slightly cooler way
People who want to make things easier to see first, rather than doing 'analysis'
In short, this is for
people who want to get their collected data into a 'properly usable state'.
Things to check before starting
You mainly need these two things.
1. GitHub account
This is free, so it's fine.
2. Repository containing CSVs
This HTML works by reading CSVs on GitHub. So, you need the source data.
At the very least, assume that you have these around.
daily_summary.csv
articles.csv
followers.csv
trend_analysis.csv
asset_articles.csv
This part is quietly important.
For those who just used the previous paid article as is,
you might have the collection CSV ready, but the analysis CSV hasn't been generated yet.
In that case, make sure to add the step to run analyze.py.
In other words, for this time,
Previous article → Foundation for collection
This article → Screen for viewing
You can think of it as this division of roles.
⚠️ GitHub Pages is intended for use with public repositories on GitHub Free.
With GitHub Pro or higher, you can use Pages with private repositories as well.
However, since this dashboard is implemented to read CSVs directly from the browser, if you use this configuration as is, the CSV must be in a state where it can be read from the outside.
An operational approach like "Collection in Private, only publishing the formatted data for public viewing" works well.
The overall structure looks like this
First, I will show you the completed structure.

The image is something like this.
your-repo/
├── data/
│ ├── articles.csv
│ ├── asset_articles.csv
│ ├── daily_summary.csv
│ ├── followers.csv
│ ├── period_ranking.csv
│ └── trend_analysis.csv
└── docs/
└── index.htmlThe points are simple:
CSV is in data/
HTML is in docs/
Publish docs/ via GitHub Pages
That's all.
Step 1 | Get the HTML file
The full code for index.html is included at the end of this article. Please copy it and save it as a file named index.html.
You might be overwhelmed by the amount of code, but there are only two places you need to touch, which I will explain in Step 3.
For now, just copying the whole thing is fine.
Step 2 | Open with a text editor
Next, open that HTML file in a text editor.
If you're wondering, "What's a text editor?", don't worry.
Windows -> Notepad
Mac -> TextEdit
will be enough.
When you open it, you might see a long list of English and symbols and think, "Whoa, this looks impossible" for a second.
But you can rest easy.
You only need to touch one place in the next Step 3.
Step 3 | Rewrite the CONFIG
Open the find function in the file with Ctrl+F (Command+F on Mac) and try searching for CONFIG.
You should find a section like this.
const CONFIG = {
repo: 'YOUR_GITHUB_USERNAME/YOUR_REPO_NAME',
dataPath: 'data',
excludeTitles: [],
};Basically, you only need to change the one line for repo:.
Before change:
repo: 'YOUR_GITHUB_USERNAME/YOUR_REPO_NAME',After change (example):
repo: 'yamada-taro/note-stats-public',Just enter the "username/repository name" that appears at the top when you open your GitHub repository page.
Do not delete the single quotes (') before and after, or the comma (,) at the end.
Other settings (only if necessary)
dataPath and excludeTitles are usually fine as they are, but I've made them editable if needed.

If you want to exclude specific articles from the ranking, add them like this.
excludeTitles: ['テスト投稿', '下書き確認用'],How to check if the settings are correct
If you open it in a browser without rewriting the settings, this guide screen will be displayed.
⚙️ Settings required
It means you're good to go once this screen stops appearing.
Step 3.5 | For those who have included the previous article
If you are basing this on the previous paid article, please take a look at this additional part.
The HTML this time also reads:
trend_analysis.csv
asset_articles.csv
as well.
Therefore, if you stop at just the previous collection,
some parts of the screen may not be displayed correctly.
In that case, add the process to run analyze.py to GitHub Actions.
For example, like this.
- name: Run Analyze Script
run: python scripts/analyze.py --articles data/articles.csv --out data/Once you add this, it connects:
Collection → Aggregation → Visualization
all the way through.
Once you reach this point, the previous article and this article will work together properly.
Step 4 | Save the file
Save after you finish rewriting.
Windows → Ctrl + S
Mac → Command + S
That's it.
You can just save this normally.
Step 5 | Upload to GitHub
Upload the saved index.html to the GitHub docs/ folder.
The process is like this.
Open the repository on GitHub
Open the docs folder
Add file → Upload files
Upload index.html
Commit changes
If the docs folder doesn't exist yet,
you can create it along with the file by naming it docs/index.html.
Step 6 | Enable GitHub Pages
Next, turn on GitHub Pages.
Here is what you need to do.
Open the repository's Settings
Open Pages on the left side
Set Source to Deploy from a branch
Set the branch to main and the folder to /docs
Save
After a short while, a URL will be issued.
It usually looks something like this.
https://あなたのユーザー名.github.io/リポジトリ名/Open this URL in your browser, and if the dashboard appears, you're done.
Great work! 🎉
Troubleshooting points when things don't go well
1. It says "⚙️ Configuration required"
It's usually a configuration error in repo:.
Did you forget to rewrite it?
Is it in the format username/repository-name?
Did you accidentally delete the '?
Check these points.
2. The numbers remain as —
In this case, it's usually one of the following.
Typo in the repo name
The repository is not set to public
There is no CSV in the data/ folder
trend_analysis.csv and asset_articles.csv have not been generated yet
The last two, in particular, are easy to overlook.
3. It shows a 404 error when opening the URL
Sometimes this fixes itself if you wait a little while.
GitHub Pages can sometimes take a while to reflect changes after configuration,
so it might appear if you wait about 5 to 10 minutes and check again.
Summary
Here is a rough summary of what we did this time.
Change the repo: in index.html for your own use
Place it in docs/
Enable GitHub Pages
Run analyze.py if necessary
With this, the note logs collected last time will
become much easier to view and handle in your browser.
If the previous article was
a 'system for storing data',
then
this article is a 'screen to properly view that data'.
Once you connect these, your note management will change a little.
Because 'I feel like it grew somehow' changes to
'This is growing', 'This is buried', and 'This looks like an asset'.
Things like this aren't flashy.
But once you can see them, they start to have a gradual effect.
Seriously.
Conclusion
This tool is a 'made it' project from personal development.
So, it's not like there's a dedicated support desk or
a promise to fix everything with an immediate response.
I'll be honest about that upfront.
However, in this day and age, you don't have to despair about that.
When it doesn't work,
the error message
this article
the code
If you just throw these into ChatGPT, Claude, or Gemini,
it's often faster than asking a person, believe it or not lol
If you found this article
a little helpful
or thought, "I want to try this,"
I would be happy if you could leave a like or a comment.
If you want to start by automatically saving CSV files, it will be smoother to begin with my previous paid article.
And once you've implemented this much, it's also fun to tweak the HTML to your liking.
Like this, you can also expand into playing around with the appearance.
If you found this article interesting or a little helpful, I would be happy to receive your reaction through likes, follows, or comments.
I really love likes and comments, haha.
See you later.
Supplementary Note
<!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>#noteManagement #GitHubPages #GitHubActions #DataAnalysis #Dashboard #Visualization #HTML #CSV #OperationalAnalysis #AccessAnalysis #note #Work #Business #Self #Diary #Learning #Study #LifeHack #InformationOrganization #BusinessImprovement
いいなと思ったら応援しよう!
よろしければ応援お願いします! いただいたチップはクリエイターとしての活動費に使わせていただきます!