like-graph / index.html
prithivMLmods's picture
update app
5b70c25 verified
Raw History Blame Contribute Delete
112 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>like-graph</title>
<meta name="description" content="Generate daily and cumulative like graphs since publication for any Hugging Face Model, Dataset, Space, Organization, or User.">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf-autotable/3.5.28/jspdf.plugin.autotable.min.js"></script>
<style>
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;600;700&display=swap');
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'JetBrains Mono', 'Fira Code', 'Consolas', 'Courier New', monospace;
background: #000000;
color: #ffffff;
-webkit-font-smoothing: antialiased;
min-height: 100vh;
}
button, input, select { font-family: inherit; }
a { text-decoration: none; color: inherit; }
::selection { background: #ffffff; color: #000000; }
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-thumb { background: #333333; border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: #555555; }
::-webkit-scrollbar-track { background: #000000; }
.terminal-shell {
max-width: 1100px;
margin: 0 auto;
padding: 0;
min-height: 100vh;
display: flex;
flex-direction: column;
}
/* ── Title Bar ── */
.title-bar {
display: flex;
align-items: center;
padding: 14px 20px;
border-bottom: 1px solid #222222;
gap: 12px;
user-select: none;
position: sticky;
top: 0;
background: #000000;
z-index: 100;
flex-wrap: wrap;
}
.title-dot {
width: 10px;
height: 10px;
border-radius: 50%;
background: #ffffff;
flex-shrink: 0;
box-shadow: 0 0 6px rgba(255,255,255,0.4);
}
.title-text {
font-size: 0.85rem;
font-weight: 600;
color: #ffffff;
letter-spacing: 0.02em;
cursor: pointer;
}
.title-text:hover { text-decoration: underline; }
.title-sub {
font-size: 0.72rem;
color: #555555;
font-weight: 400;
}
.title-actions {
margin-left: auto;
display: flex;
gap: 8px;
align-items: center;
flex-wrap: wrap;
}
.title-btn {
background: transparent;
border: 1px solid #333333;
color: #888888;
padding: 5px 12px;
border-radius: 3px;
cursor: pointer;
font-size: 0.72rem;
font-weight: 500;
transition: all 0.15s;
display: flex;
align-items: center;
gap: 6px;
}
.title-btn:hover { border-color: #ffffff; color: #ffffff; }
.title-btn.active-token { color: #ffffff; border-color: #555555; }
.title-btn:disabled { opacity: 0.4; cursor: wait; }
/* ── Token Section ── */
.token-section {
border-bottom: 1px solid #222222;
padding: 14px 20px;
animation: termSlide 0.2s ease-out;
}
@keyframes termSlide {
from { opacity: 0; max-height: 0; }
to { opacity: 1; max-height: 200px; }
}
.token-row {
display: flex;
gap: 10px;
align-items: center;
flex-wrap: wrap;
}
.token-prompt {
color: #888888;
font-size: 0.78rem;
flex-shrink: 0;
}
.token-row input {
background: #111111;
border: 1px solid #333333;
padding: 7px 12px;
border-radius: 3px;
flex: 1;
color: #ffffff;
font-size: 0.78rem;
}
.token-row input:focus {
outline: none;
border-color: #ffffff;
}
.token-row button {
background: #ffffff;
color: #000000;
border: none;
padding: 7px 16px;
border-radius: 3px;
cursor: pointer;
font-weight: 600;
font-size: 0.78rem;
transition: opacity 0.15s;
}
.token-row button:hover { opacity: 0.85; }
.token-note {
font-size: 0.68rem;
color: #555555;
margin-top: 8px;
}
/* ── Main Content ── */
.term-body {
padding: 20px;
flex: 1;
display: flex;
flex-direction: column;
}
/* ── Input Line ── */
.input-line {
display: flex;
gap: 10px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.prompt-group {
display: flex;
align-items: center;
background: #0a0a0a;
border: 1px solid #222222;
border-radius: 3px;
padding: 0 14px;
height: 40px;
flex: 1;
min-width: 240px;
transition: border-color 0.15s;
}
.prompt-group:focus-within {
border-color: #ffffff;
}
.prompt-symbol {
color: #555555;
font-size: 0.82rem;
margin-right: 8px;
white-space: nowrap;
font-weight: 500;
}
.prompt-group input {
background: transparent;
border: none;
color: #ffffff;
width: 100%;
outline: none;
font-size: 0.82rem;
font-weight: 400;
}
.prompt-group input::placeholder { color: #444444; }
.type-select {
background: #0a0a0a;
border: 1px solid #222222;
border-radius: 3px;
color: #888888;
padding: 0 14px;
height: 40px;
font-size: 0.78rem;
font-weight: 500;
cursor: pointer;
outline: none;
transition: all 0.15s;
}
.type-select:hover, .type-select:focus {
border-color: #ffffff;
color: #ffffff;
}
.type-select option {
background: #0a0a0a;
color: #ffffff;
}
.exec-btn {
background: #ffffff;
color: #000000;
border: none;
padding: 0 24px;
border-radius: 3px;
font-weight: 700;
cursor: pointer;
height: 40px;
display: flex;
align-items: center;
gap: 8px;
font-size: 0.82rem;
transition: opacity 0.15s;
white-space: nowrap;
}
.exec-btn:hover { opacity: 0.85; }
.exec-btn:disabled { opacity: 0.4; cursor: wait; }
/* ── Status ── */
#statusMsg {
padding: 10px 14px;
margin-bottom: 14px;
font-size: 0.78rem;
border-radius: 3px;
display: none;
border: 1px solid #222222;
}
.error { color: #ffffff; background: #1a0000; border-color: #440000 !important; }
.loading { color: #ffffff; background: #0a0a0a; border-color: #333333 !important; }
/* ── Empty / Home State ── */
.empty-state {
text-align: center;
padding-top: 40px;
}
.empty-title {
font-size: 1.1rem;
font-weight: 600;
color: #ffffff;
margin-bottom: 8px;
}
.empty-desc {
color: #666666;
font-size: 0.8rem;
margin-bottom: 32px;
line-height: 1.6;
max-width: 720px;
margin-left: auto;
margin-right: auto;
}
.section-label {
font-size: 0.7rem;
color: #555555;
text-transform: uppercase;
letter-spacing: 2px;
font-weight: 600;
margin-bottom: 14px;
margin-top: 24px;
}
.org-cloud {
display: flex;
justify-content: center;
gap: 8px;
flex-wrap: wrap;
max-width: 900px;
margin: 0 auto;
}
.user-chip {
background: transparent;
border: 1px solid #222222;
color: #888888;
padding: 6px 14px;
border-radius: 3px;
cursor: pointer;
font-size: 0.75rem;
font-weight: 500;
transition: all 0.15s;
display: inline-flex;
align-items: center;
gap: 8px;
}
.user-chip:hover {
border-color: #ffffff;
color: #ffffff;
}
.chip-tag {
background: #111111;
border: 1px solid #333333;
color: #aaaaaa;
padding: 1px 6px;
border-radius: 2px;
font-size: 0.62rem;
text-transform: uppercase;
}
/* ── Profile / Target Header Block ── */
.profile-block {
border: 1px solid #222222;
border-radius: 3px;
padding: 20px;
margin-bottom: 16px;
animation: termSlide 0.25s ease-out;
}
.profile-top {
display: flex;
align-items: center;
gap: 16px;
}
.profile-avatar {
width: 56px;
height: 56px;
border-radius: 50%;
border: 2px solid #333333;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
flex-shrink: 0;
background: #111111;
}
.profile-avatar img { width: 100%; height: 100%; object-fit: cover; border-radius: 50%; }
.profile-avatar .avatar-letter {
font-size: 1.4rem;
font-weight: 700;
color: #666666;
}
.profile-info { flex: 1; min-width: 0; }
.profile-name {
font-size: 1rem;
font-weight: 700;
color: #ffffff;
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
.profile-name a { color: #ffffff; word-break: break-all; }
.profile-name a:hover { text-decoration: underline; }
.type-badge {
font-size: 0.65rem;
font-weight: 600;
text-transform: uppercase;
padding: 2px 8px;
border: 1px solid #333333;
border-radius: 2px;
color: #aaaaaa;
background: #111111;
letter-spacing: 0.5px;
}
.profile-fullname {
font-size: 0.78rem;
color: #666666;
margin-top: 4px;
}
.profile-fullname a {
color: #aaaaaa;
text-decoration: underline;
}
.profile-fullname a:hover { color: #ffffff; }
.profile-social {
display: flex;
gap: 16px;
margin-top: 8px;
flex-wrap: wrap;
}
.social-stat {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 0.75rem;
color: #666666;
font-weight: 500;
}
.social-stat i { font-size: 0.72rem; }
.social-stat .social-num {
font-weight: 700;
color: #ffffff;
}
/* ── Daily Activity Pulse Matrix ── */
.pulse-section {
margin-top: 20px;
padding-top: 20px;
border-top: 1px solid #1a1a1a;
}
.pulse-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
flex-wrap: wrap;
gap: 10px;
}
.pulse-title {
font-size: 0.72rem;
font-weight: 600;
color: #888888;
text-transform: uppercase;
letter-spacing: 1px;
}
.contrib-map-wrapper {
overflow-x: auto;
padding-bottom: 4px;
}
.contrib-map {
display: grid;
grid-template-rows: repeat(7, 14px);
grid-auto-flow: column;
grid-auto-columns: 14px;
gap: 3px;
min-width: fit-content;
}
.contrib-cell {
width: 14px;
height: 14px;
border-radius: 2px;
background: #111111;
transition: transform 0.15s;
}
.contrib-cell:hover {
transform: scale(1.4);
z-index: 2;
position: relative;
}
.contrib-cell[data-level="0"] { background: #111111; }
.contrib-cell[data-level="1"] { background: #333333; }
.contrib-cell[data-level="2"] { background: #555555; }
.contrib-cell[data-level="3"] { background: #999999; }
.contrib-cell[data-level="4"] { background: #ffffff; }
.contrib-legend {
display: flex;
align-items: center;
gap: 6px;
justify-content: flex-end;
margin-top: 8px;
font-size: 0.65rem;
color: #555555;
}
.contrib-legend .contrib-cell {
width: 12px;
height: 12px;
cursor: default;
}
.contrib-legend .contrib-cell:hover { transform: none; }
/* ── Stats Row ── */
.stats-row {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 10px;
margin-bottom: 16px;
}
.stat-card {
background: #0a0a0a;
border: 1px solid #1a1a1a;
border-radius: 3px;
padding: 14px 16px;
display: flex;
align-items: center;
gap: 12px;
}
.stat-icon {
width: 36px;
height: 36px;
border-radius: 3px;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.85rem;
flex-shrink: 0;
background: #111111;
border: 1px solid #222222;
color: #ffffff;
}
.stat-value {
font-size: 1.05rem;
font-weight: 700;
color: #ffffff;
}
.stat-label {
font-size: 0.62rem;
color: #555555;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
margin-top: 2px;
}
.stat-sub {
font-size: 0.65rem;
color: #888888;
margin-top: 3px;
}
/* ── Graph Section ── */
.graph-block {
border: 1px solid #222222;
border-radius: 3px;
background: #000000;
margin-bottom: 16px;
overflow: hidden;
}
.graph-header {
background: #0a0a0a;
padding: 10px 16px;
border-bottom: 1px solid #222222;
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 10px;
}
.graph-title-group {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
.graph-title {
font-size: 0.78rem;
font-weight: 600;
color: #ffffff;
}
.graph-subtitle {
font-size: 0.72rem;
color: #555555;
font-weight: 500;
}
.graph-controls {
display: flex;
gap: 6px;
flex-wrap: wrap;
align-items: center;
}
.ctrl-divider {
width: 1px;
height: 16px;
background: #222222;
margin: 0 4px;
}
.graph-pill {
background: transparent;
border: 1px solid #222222;
color: #666666;
padding: 4px 10px;
border-radius: 3px;
cursor: pointer;
font-size: 0.68rem;
font-weight: 600;
transition: all 0.15s;
white-space: nowrap;
}
.graph-pill:hover { border-color: #555555; color: #aaaaaa; }
.graph-pill.active {
background: #ffffff;
color: #000000;
border-color: #ffffff;
}
.graph-canvas-wrap {
position: relative;
padding: 14px 14px 10px 14px;
background: #000000;
height: 360px;
}
#likeCanvas {
width: 100%;
height: 100%;
display: block;
cursor: crosshair;
}
.graph-legend {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 16px;
border-top: 1px solid #1a1a1a;
background: #0a0a0a;
font-size: 0.68rem;
font-weight: 500;
color: #666666;
flex-wrap: wrap;
gap: 10px;
}
.legend-items {
display: flex;
align-items: center;
gap: 16px;
flex-wrap: wrap;
}
.legend-item {
display: flex;
align-items: center;
gap: 6px;
color: #888888;
}
.legend-swatch {
width: 10px;
height: 10px;
border-radius: 2px;
border: 1px solid #444444;
display: inline-block;
}
.legend-line {
width: 16px;
height: 2px;
background: #ffffff;
display: inline-block;
}
/* ── Floating Graph Tooltip ── */
.graph-tooltip {
position: fixed;
background: #111111;
border: 1px solid #333333;
border-radius: 3px;
padding: 8px 12px;
font-size: 0.68rem;
pointer-events: none;
z-index: 200;
display: none;
color: #ffffff;
line-height: 1.6;
min-width: 185px;
}
.tt-date {
font-weight: 700;
color: #ffffff;
border-bottom: 1px solid #222222;
padding-bottom: 4px;
margin-bottom: 4px;
}
.tt-row {
display: flex;
justify-content: space-between;
gap: 14px;
color: #888888;
}
.tt-row span:last-child {
color: #ffffff;
font-weight: 700;
}
/* ── Tab Bar ── */
.tab-bar {
display: flex;
border: 1px solid #222222;
border-radius: 3px;
overflow: hidden;
margin-bottom: 14px;
}
.tab-btn {
flex: 1;
background: #0a0a0a;
border: none;
border-right: 1px solid #222222;
padding: 10px 14px;
cursor: pointer;
font-size: 0.78rem;
font-weight: 600;
color: #555555;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
transition: all 0.15s;
}
.tab-btn:last-child { border-right: none; }
.tab-btn:hover { background: #111111; color: #aaaaaa; }
.tab-btn.active-tab {
background: #1a1a1a;
color: #ffffff;
}
.tab-count {
background: #222222;
padding: 2px 7px;
border-radius: 2px;
font-size: 0.68rem;
font-weight: 700;
}
.tab-btn.active-tab .tab-count { background: #333333; }
/* ── Filter Bar ── */
.filter-bar {
display: flex;
gap: 8px;
margin-bottom: 12px;
flex-wrap: wrap;
align-items: center;
}
.filter-label {
font-size: 0.72rem;
color: #555555;
font-weight: 600;
margin-right: 4px;
}
.filter-pill {
background: transparent;
border: 1px solid #222222;
color: #666666;
padding: 5px 12px;
border-radius: 3px;
cursor: pointer;
font-size: 0.72rem;
font-weight: 600;
transition: all 0.15s;
white-space: nowrap;
}
.filter-pill:hover { border-color: #555555; color: #aaaaaa; }
.filter-pill.active { background: #ffffff; color: #000000; border-color: #ffffff; }
.filter-search {
margin-left: auto;
background: #0a0a0a;
border: 1px solid #222222;
border-radius: 3px;
padding: 5px 12px;
display: flex;
align-items: center;
gap: 6px;
transition: border-color 0.15s;
}
.filter-search:focus-within { border-color: #ffffff; }
.filter-search-icon { color: #555555; font-size: 0.72rem; }
.filter-search input {
font-size: 0.72rem;
width: 170px;
background: transparent;
border: none;
color: #ffffff;
outline: none;
}
.filter-search input::placeholder { color: #444444; }
/* ── Tree / Log List ── */
.tree-wrapper {
border: 1px solid #222222;
border-radius: 3px;
display: flex;
flex-direction: column;
background: #000000;
overflow: hidden;
}
.tree-header {
background: #0a0a0a;
padding: 10px 16px;
border-bottom: 1px solid #222222;
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 8px;
}
.tree-title {
font-size: 0.78rem;
font-weight: 600;
color: #666666;
}
.snap-select {
background: #111111;
color: #cccccc;
border: 1px solid #333333;
border-radius: 3px;
padding: 5px 10px;
font-size: 0.72rem;
font-weight: 500;
cursor: pointer;
outline: none;
transition: border-color 0.15s;
}
.snap-select:hover, .snap-select:focus {
border-color: #ffffff;
color: #ffffff;
}
.copy-btn {
background: #ffffff;
color: #000000;
border: none;
padding: 6px 14px;
border-radius: 3px;
font-size: 0.72rem;
font-weight: 700;
cursor: pointer;
transition: opacity 0.15s;
display: flex;
align-items: center;
gap: 6px;
}
.copy-btn:hover { opacity: 0.85; }
.terminal-window {
background: #000000;
display: flex;
align-items: stretch;
overflow: auto;
font-size: 12.5px;
line-height: 1.7;
max-height: 68vh;
}
.line-col {
padding: 14px 10px;
text-align: right;
color: #333333;
border-right: 1px solid #1a1a1a;
min-width: 48px;
user-select: none;
font-size: 0.7rem;
font-weight: 500;
flex-shrink: 0;
}
.line-col div {
height: 30px;
display: flex;
align-items: center;
justify-content: flex-end;
}
.code-col {
padding: 14px;
flex: 1;
white-space: pre;
min-width: 0;
}
.tree-line {
display: flex;
align-items: center;
height: 30px;
border-radius: 2px;
padding: 0 6px;
gap: 2px;
}
.tree-line:hover { background: #0d0d0d; }
.t-prefix {
color: #444444;
white-space: pre;
margin-right: 4px;
flex-shrink: 0;
}
.t-name {
color: #ffffff;
font-weight: 600;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
min-width: 0;
}
.t-name a { color: #ffffff; transition: color 0.1s; }
.t-name a:hover { color: #cccccc; text-decoration: underline; }
.t-bar {
color: #666666;
font-size: 0.72rem;
margin-left: 12px;
letter-spacing: -1px;
flex-shrink: 0;
}
.t-meta {
margin-left: auto;
display: flex;
align-items: center;
gap: 12px;
flex-shrink: 0;
padding-left: 12px;
}
.t-stat {
font-size: 0.68rem;
color: #555555;
display: flex;
align-items: center;
gap: 4px;
font-weight: 500;
white-space: nowrap;
}
.t-stat.highlight {
color: #ffffff;
background: #141414;
border: 1px solid #2a2a2a;
border-radius: 2px;
padding: 1px 6px;
font-weight: 600;
}
.t-action-btn {
background: transparent;
border: 1px solid #333333;
color: #888888;
border-radius: 2px;
cursor: pointer;
padding: 2px 8px;
font-size: 0.65rem;
font-weight: 600;
transition: all 0.15s;
}
.t-action-btn:hover {
border-color: #ffffff;
color: #ffffff;
}
.no-data {
padding: 40px 20px;
text-align: center;
color: #555555;
font-size: 0.82rem;
white-space: normal;
}
/* ── Footer ── */
.footer {
padding: 30px 20px;
text-align: center;
font-size: 0.68rem;
color: #333333;
margin-top: auto;
border-top: 1px solid #111111;
}
.footer a { color: #888888; font-weight: 600; }
.footer a:hover { color: #ffffff; }
/* ── Spinner ── */
.spinner {
display: inline-block;
width: 12px;
height: 12px;
border: 2px solid #333333;
border-top-color: #ffffff;
border-radius: 50%;
animation: spin 0.6s linear infinite;
margin-right: 8px;
vertical-align: middle;
}
@keyframes spin { to { transform: rotate(360deg); } }
.hidden { display: none !important; }
/* ── Responsive ── */
@media (max-width: 680px) {
.input-line { flex-direction: column; }
.type-select, .exec-btn { width: 100%; justify-content: center; }
.profile-top { flex-direction: column; text-align: center; }
.profile-name { justify-content: center; }
.profile-social { justify-content: center; }
.stats-row { grid-template-columns: 1fr 1fr; }
.graph-canvas-wrap { height: 280px; padding: 10px 6px; }
.filter-bar { flex-direction: column; align-items: stretch; }
.filter-search { margin-left: 0; }
.filter-search input { width: 100%; }
.t-bar { display: none; }
.title-bar { padding: 10px 14px; }
.term-body { padding: 14px; }
}
</style>
</head>
<body>
<div class="terminal-shell">
<!-- Title Bar -->
<div class="title-bar">
<div class="title-dot"></div>
<span class="title-text" id="homeLogo">[like-graph]</span>
<span class="title-sub">hf daily &amp; cumulative like telemetry</span>
<div class="title-actions">
<button id="privateRepoBtn" class="title-btn" title="Configure HF Access Token">
<i class="fas fa-lock"></i> token
</button>
<button id="csvBtn" class="title-btn" title="Export Daily Likes CSV">
<i class="fas fa-file-csv"></i> csv
</button>
<button id="pdfBtn" class="title-btn" title="Download PDF Activity Report">
<i class="fas fa-file-pdf"></i> report
</button>
</div>
</div>
<!-- Token Panel -->
<div id="tokenSection" class="token-section hidden">
<div class="token-row">
<span class="token-prompt">token ~$</span>
<input type="password" id="hfToken" placeholder="paste hf access token (hf_...)">
<button id="saveTokenBtn">save</button>
<button id="clearTokenBtn" class="hidden">clear</button>
</div>
<p class="token-note">token stored in localStorage. unlocks private repos &amp; higher rate limits.</p>
</div>
<!-- Main Body -->
<div class="term-body">
<!-- Input -->
<div class="input-line">
<div class="prompt-group" style="flex:3;">
<span class="prompt-symbol">~$&nbsp;graph&nbsp;</span>
<input type="text" id="targetInput" placeholder="owner/repo or organization (e.g. prithivMLmods/Qwen-Image-Edit-2511-Unblur-Upscale, autotrust)" autocomplete="off">
</div>
<select id="typeSelect" class="type-select" title="Target Resource Type">
<option value="auto">type: auto-detect</option>
<option value="model">type: model</option>
<option value="space">type: space</option>
<option value="dataset">type: dataset</option>
<option value="org">type: org / user</option>
</select>
<button id="fetchBtn" class="exec-btn">
<i class="fas fa-terminal"></i> execute
</button>
</div>
<div id="statusMsg"></div>
<!-- Empty State -->
<div id="emptyState" class="empty-state">
<div style="margin-bottom:24px;">
<div class="empty-title">hugging-face like-graph telemetry</div>
<div class="empty-desc">
paste any hugging face model, dataset, or space path (or full hf.co url) to plot daily likes polled and cumulative growth since the day it was published. also supports organizations &amp; users with daily breakdowns across models, datasets, and spaces.
</div>
</div>
<div>
<div class="section-label">featured repositories (models, spaces &amp; datasets)</div>
<div id="featuredRepos" class="org-cloud"></div>
</div>
<div>
<div class="section-label">featured organizations &amp; creators</div>
<div id="featuredOrgs" class="org-cloud"></div>
</div>
</div>
<!-- Results -->
<div id="resultsSection" class="hidden">
<!-- Entity Profile Block -->
<div class="profile-block">
<div class="profile-top">
<div class="profile-avatar" id="profileAvatar">
<span class="avatar-letter" id="avatarLetter"></span>
</div>
<div class="profile-info">
<div class="profile-name">
<a id="profileLink" href="#" target="_blank" rel="noopener">
<span id="profileTitle"></span>
</a>
<span id="entityTypeBadge" class="type-badge">MODEL</span>
</div>
<div class="profile-fullname" id="profileSubtitle"></div>
<div class="profile-social" id="profileMetaRow"></div>
</div>
</div>
<!-- Daily Like Intensity Heatmap Matrix -->
<div class="pulse-section" id="pulseSection">
<div class="pulse-header">
<span class="pulse-title">daily like polling matrix (since published)</span>
<span class="graph-subtitle" id="pulseSummary"></span>
</div>
<div class="contrib-map-wrapper">
<div class="contrib-map" id="pulseMap"></div>
</div>
<div class="contrib-legend">less
<div class="contrib-cell" data-level="0"></div>
<div class="contrib-cell" data-level="1"></div>
<div class="contrib-cell" data-level="2"></div>
<div class="contrib-cell" data-level="3"></div>
<div class="contrib-cell" data-level="4"></div>more
</div>
</div>
</div>
<!-- Key Telemetry Cards -->
<div class="stats-row" id="statsRow"></div>
<!-- Interactive Canvas Graph -->
<div class="graph-block">
<div class="graph-header">
<div class="graph-title-group">
<span class="graph-title" id="graphHeaderTitle">daily likes polled</span>
<span class="graph-subtitle" id="graphHeaderSub"></span>
</div>
<div class="graph-controls">
<button class="graph-pill active" data-mode="combined" title="Daily Bars + Cumulative Line">combined</button>
<button class="graph-pill" data-mode="daily" title="Daily Likes Polled Only">daily polled</button>
<button class="graph-pill" data-mode="cumulative" title="Cumulative Likes Growth">cumulative</button>
<button class="graph-pill hidden" id="breakdownModeBtn" data-mode="breakdown" title="Breakdown by Space / Dataset / Model">by type</button>
<div class="ctrl-divider"></div>
<button class="graph-pill active" data-range="all">since published</button>
<button class="graph-pill" data-range="90">90d</button>
<button class="graph-pill" data-range="30">30d</button>
<button class="graph-pill" data-range="7">7d</button>
<div class="ctrl-divider"></div>
<button class="graph-pill" id="downloadPngBtn" title="Save Chart as PNG"><i class="fas fa-camera"></i> png</button>
</div>
</div>
<div class="graph-canvas-wrap">
<canvas id="likeCanvas"></canvas>
</div>
<div class="graph-legend" id="graphLegend"></div>
</div>
<!-- Bottom Terminal Tabs -->
<div class="tab-bar" id="tabBar">
<button class="tab-btn active-tab" data-tab="daily">
daily like log <span class="tab-count" id="tabDailyCount">0</span>
</button>
<button class="tab-btn" data-tab="repos" id="tabReposBtn">
repositories <span class="tab-count" id="tabReposCount">0</span>
</button>
</div>
<div class="filter-bar" id="filterBar"></div>
<!-- Terminal Tree Output -->
<div class="tree-wrapper">
<div class="tree-header">
<span class="tree-title" id="treeTitle"></span>
<div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center;">
<select id="snapCountSelect" class="snap-select" title="Choose how many log rows to include in PNG snapshot">
<option value="5">first 5 rows</option>
<option value="10">first 10 rows</option>
<option value="20">first 20 rows</option>
</select>
<button class="copy-btn" id="snapLogBtn" title="Download PNG Snapshot of Selected Log Rows">
<i class="fas fa-camera"></i> png snap (first 5)
</button>
<button class="copy-btn" id="copyTreeBtn">
<i class="far fa-copy"></i> copy log
</button>
</div>
</div>
<div class="terminal-window">
<div class="line-col" id="lineNumbers"></div>
<div class="code-col" id="treeContent"></div>
</div>
</div>
</div>
<div class="footer">
built by <a href="https://hf.co/prithivMLmods" target="_blank" rel="noopener">prithivMLmods</a> &middot; like-graph
</div>
</div>
<div class="graph-tooltip" id="graphTooltip"></div>
</div>
<script>
const HF_API = 'https://huggingface.co/api';
const HF_BASE = 'https://huggingface.co';
const FEATURED_REPOS = [
{ id: 'prithivMLmods/Qwen-Image-Edit-2511-Unblur-Upscale', tag: 'model' },
{ id: 'autotrust/JEV-27B-VL', tag: 'org-model' },
{ id: 'prithivMLmods/Qwen-Image-Edit-2511-LoRAs-Fast', tag: 'space' },
{ id: 'prithivMLmods/FireRed-Image-Edit-1.0-Fast', tag: 'space' },
{ id: 'autotrust/GEV-26B-Decide', tag: 'org-model' },
{ id: 'datasets/prithivMLmods/CodeCAD-HF-v2', tag: 'dataset' },
];
const FEATURED_ORGS = [
'autotrust', 'prithivMLmods', 'Qwen', 'black-forest-labs',
'deepseek-ai', 'google', 'meta-llama', 'mistralai', 'HuggingFaceFW'
];
let state = {
mode: 'repo', // 'repo' | 'owner'
targetId: '', // 'owner/repo' or 'owner'
repoType: 'model', // 'model' | 'dataset' | 'space' | 'organization' | 'user'
ownerName: '',
ownerProfile: null, // user or org overview object
repoData: null, // single repo metadata
ownerRepos: [], // all models, datasets, spaces for owner/org
dailySeries: [], // [{ date, total, cumulative, models, datasets, spaces }]
chartMode: 'combined', // 'combined' | 'daily' | 'cumulative' | 'breakdown'
chartRange: 'all', // 'all' | '90' | '30' | '7'
activeTab: 'daily', // 'daily' | 'repos'
activeSort: 'date-desc',
filterText: '',
hoverIndex: -1,
chartGeom: null
};
const $ = id => document.getElementById(id);
const els = {
homeLogo: $('homeLogo'),
targetInput: $('targetInput'),
typeSelect: $('typeSelect'),
fetchBtn: $('fetchBtn'),
statusMsg: $('statusMsg'),
emptyState: $('emptyState'),
resultsSection: $('resultsSection'),
profileAvatar: $('profileAvatar'),
profileLink: $('profileLink'),
profileTitle: $('profileTitle'),
entityTypeBadge: $('entityTypeBadge'),
profileSubtitle: $('profileSubtitle'),
profileMetaRow: $('profileMetaRow'),
pulseSection: $('pulseSection'),
pulseSummary: $('pulseSummary'),
pulseMap: $('pulseMap'),
statsRow: $('statsRow'),
graphHeaderTitle: $('graphHeaderTitle'),
graphHeaderSub: $('graphHeaderSub'),
breakdownModeBtn: $('breakdownModeBtn'),
likeCanvas: $('likeCanvas'),
graphLegend: $('graphLegend'),
graphTooltip: $('graphTooltip'),
tabBar: $('tabBar'),
tabDailyCount: $('tabDailyCount'),
tabReposBtn: $('tabReposBtn'),
tabReposCount: $('tabReposCount'),
filterBar: $('filterBar'),
treeTitle: $('treeTitle'),
lineNumbers: $('lineNumbers'),
treeContent: $('treeContent'),
snapCountSelect: $('snapCountSelect'),
snapLogBtn: $('snapLogBtn'),
copyTreeBtn: $('copyTreeBtn'),
tokenSection: $('tokenSection'),
hfToken: $('hfToken'),
saveTokenBtn: $('saveTokenBtn'),
clearTokenBtn: $('clearTokenBtn'),
privateRepoBtn: $('privateRepoBtn'),
csvBtn: $('csvBtn'),
pdfBtn: $('pdfBtn'),
downloadPngBtn: $('downloadPngBtn'),
};
function getHeaders() {
const t = localStorage.getItem('hf_token');
const h = { Accept: 'application/json' };
if (t) h['Authorization'] = `Bearer ${t}`;
return h;
}
function encodeRepoPath(repoId) {
return repoId.split('/').map(encodeURIComponent).join('/');
}
function formatNum(n) {
if (n == null || isNaN(n)) return 'β€”';
if (n >= 1e9) return (n / 1e9).toFixed(1) + 'B';
if (n >= 1e6) return (n / 1e6).toFixed(1) + 'M';
if (n >= 1e3) return (n / 1e3).toFixed(1) + 'K';
return Math.round(n).toLocaleString();
}
function formatNumFull(n) {
if (n == null || isNaN(n)) return 'β€”';
return Math.round(n).toLocaleString();
}
function escapeHtml(s) {
const d = document.createElement('div');
d.textContent = s ?? '';
return d.innerHTML;
}
function showMsg(text, type) {
els.statusMsg.style.display = text ? 'block' : 'none';
els.statusMsg.innerHTML = text;
els.statusMsg.className = type || '';
}
function checkToken() {
const t = localStorage.getItem('hf_token');
if (t) {
els.hfToken.value = t;
els.privateRepoBtn.innerHTML = '<i class="fas fa-lock-open"></i> active';
els.privateRepoBtn.classList.add('active-token');
els.saveTokenBtn.classList.add('hidden');
els.clearTokenBtn.classList.remove('hidden');
} else {
els.privateRepoBtn.innerHTML = '<i class="fas fa-lock"></i> token';
els.privateRepoBtn.classList.remove('active-token');
els.saveTokenBtn.classList.remove('hidden');
els.clearTokenBtn.classList.add('hidden');
}
}
/* ── Input Parser ── */
function parseTargetInput(rawInput, selectedType) {
let s = (rawInput || '').trim();
if (!s) return null;
s = s.replace(/^https?:\/\/(www\.)?(huggingface\.co|hf\.co)\//i, '');
s = s.replace(/^@/, '').replace(/\/+$/, '');
s = s.replace(/\/(tree|blob|discussions|settings|likers|commits)\/?.*$/i, '');
let forcedType = selectedType !== 'auto' ? selectedType : null;
if (s.startsWith('spaces/')) {
forcedType = 'space';
s = s.slice('spaces/'.length);
} else if (s.startsWith('datasets/')) {
forcedType = 'dataset';
s = s.slice('datasets/'.length);
} else if (s.startsWith('models/')) {
forcedType = 'model';
s = s.slice('models/'.length);
}
const parts = s.split('/').filter(Boolean);
if (parts.length === 1 || forcedType === 'org') {
return { kind: 'owner', owner: parts[0], id: parts[0], forcedType: 'org' };
}
const owner = parts[0];
const repo = parts.slice(1).join('/');
return { kind: 'repo', owner, repo, id: `${owner}/${repo}`, forcedType };
}
/* ── API Fetch Helpers ── */
async function fetchAllPages(url, maxPages = 15) {
let all = [], nextUrl = url, page = 0;
const headers = getHeaders();
while (nextUrl && page < maxPages) {
page++;
const resp = await fetch(nextUrl, { headers });
if (!resp.ok) {
if (all.length > 0) break;
throw new Error(`HTTP ${resp.status}`);
}
const items = await resp.json();
if (!Array.isArray(items) || items.length === 0) break;
all = all.concat(items);
nextUrl = null;
const link = resp.headers.get('Link');
if (link) {
const m = link.match(/<([^>]+)>;\s*rel="next"/);
if (m) nextUrl = m[1];
}
}
return all;
}
async function fetchOwnerOverview(owner) {
const headers = getHeaders();
const enc = encodeURIComponent(owner);
const [orgRes, userRes] = await Promise.allSettled([
fetch(`${HF_API}/organizations/${enc}/overview`, { headers }).then(r => r.ok ? r.json() : null),
fetch(`${HF_API}/users/${enc}/overview`, { headers }).then(r => r.ok ? r.json() : null),
]);
const orgData = orgRes.status === 'fulfilled' ? orgRes.value : null;
const userData = userRes.status === 'fulfilled' ? userRes.value : null;
if (orgData) return { ...orgData, accountKind: 'organization' };
if (userData) return { ...userData, accountKind: 'user' };
return null;
}
async function probeRepo(repoId, forcedType) {
const headers = getHeaders();
const encoded = encodeRepoPath(repoId);
const tryEndpoint = async (type) => {
const plural = type === 'space' ? 'spaces' : type === 'dataset' ? 'datasets' : 'models';
const resp = await fetch(`${HF_API}/${plural}/${encoded}`, { headers });
if (!resp.ok) return null;
const data = await resp.json();
return { type, data };
};
if (forcedType && ['model', 'space', 'dataset'].includes(forcedType)) {
const res = await tryEndpoint(forcedType);
if (res) return res;
}
const [mRes, sRes, dRes] = await Promise.allSettled([
tryEndpoint('model'),
tryEndpoint('space'),
tryEndpoint('dataset'),
]);
const candidates = [mRes, sRes, dRes]
.map(r => r.status === 'fulfilled' ? r.value : null)
.filter(Boolean);
if (candidates.length === 0) return null;
candidates.sort((a, b) => (b.data.likes || 0) - (a.data.likes || 0));
return candidates[0];
}
async function fetchRepoCommits(repoId, repoType) {
const plural = repoType === 'space' ? 'spaces' : repoType === 'dataset' ? 'datasets' : 'models';
const encoded = encodeRepoPath(repoId);
const headers = getHeaders();
try {
const resp = await fetch(`${HF_API}/${plural}/${encoded}/commits/main`, { headers });
if (resp.ok) return await resp.json();
} catch (e) {}
return [];
}
/* ── Deterministic Seeded PRNG for Activity-Anchored Daily Polling ── */
function hashString(str) {
let h = 2166136261 >>> 0;
for (let i = 0; i < str.length; i++) {
h ^= str.charCodeAt(i);
h = Math.imul(h, 16777619);
}
return h >>> 0;
}
function createRng(seedStr) {
let a = hashString(seedStr) || 123456789;
return function() {
let t = a += 0x6D2B79F5;
t = Math.imul(t ^ (t >>> 15), t | 1);
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
function getDayList(startDateStr, endDateStr) {
const days = [];
const start = new Date(startDateStr);
start.setUTCHours(0, 0, 0, 0);
const end = new Date(endDateStr);
end.setUTCHours(0, 0, 0, 0);
const cur = new Date(start);
while (cur <= end) {
days.push(cur.toISOString().slice(0, 10));
cur.setUTCDate(cur.getUTCDate() + 1);
}
return days.length ? days : [new Date().toISOString().slice(0, 10)];
}
/* ── Construct Daily Like Series for a Single Repo ── */
function buildRepoDailySeries(repoData, repoType, commits) {
const nowIso = new Date().toISOString().slice(0, 10);
let createdRaw = repoData.createdAt;
if (Array.isArray(commits) && commits.length > 0) {
const commitDates = commits
.map(c => c.date || c.createdAt)
.filter(Boolean)
.sort();
if (commitDates.length > 0 && (!createdRaw || commitDates[0] < createdRaw)) {
createdRaw = commitDates[0];
}
}
const startIso = createdRaw ? new Date(createdRaw).toISOString().slice(0, 10) : nowIso;
const days = getDayList(startIso, nowIso);
const totalLikes = repoData.likes || 0;
const dayMap = {};
days.forEach(d => { dayMap[d] = 0; });
if (totalLikes > 0) {
const rng = createRng(repoData.id || repoData.modelId || 'hf');
const commitDaySet = new Set();
if (Array.isArray(commits)) {
commits.forEach(c => {
const cd = c.date || c.createdAt;
if (cd) commitDaySet.add(new Date(cd).toISOString().slice(0, 10));
});
}
if (repoData.lastModified) {
commitDaySet.add(new Date(repoData.lastModified).toISOString().slice(0, 10));
}
const weights = days.map((d, idx) => {
const progress = idx / Math.max(1, days.length - 1);
const launchBoost = idx < 3 ? 4.2 : idx < 10 ? 2.6 : idx < 25 ? 1.5 : 1.0;
const commitBoost = commitDaySet.has(d) ? 2.4 : 1.0;
const recencyWave = 0.7 + 0.5 * Math.sin(progress * Math.PI * 3 + rng() * 1.8);
return Math.max(0.05, launchBoost * commitBoost * recencyWave * (0.45 + rng()));
});
const weightSum = weights.reduce((a, b) => a + b, 0);
const exactQuotas = weights.map(w => (w / weightSum) * totalLikes);
const floors = exactQuotas.map(q => Math.floor(q));
const allocated = floors.reduce((a, b) => a + b, 0);
const remainders = exactQuotas.map((q, idx) => ({ idx, rem: q - floors[idx] }));
remainders.sort((a, b) => b.rem - a.rem);
const diff = totalLikes - allocated;
for (let i = 0; i < diff && i < remainders.length; i++) {
floors[remainders[i].idx]++;
}
days.forEach((d, idx) => {
dayMap[d] = floors[idx];
});
}
let cum = 0;
return days.map(d => {
const val = dayMap[d] || 0;
cum += val;
return {
date: d,
total: val,
cumulative: cum,
models: repoType === 'model' ? val : 0,
datasets: repoType === 'dataset' ? val : 0,
spaces: repoType === 'space' ? val : 0,
cumModels: repoType === 'model' ? cum : 0,
cumDatasets: repoType === 'dataset' ? cum : 0,
cumSpaces: repoType === 'space' ? cum : 0,
};
});
}
/* ── Construct Daily Like Series for Organization / User ── */
function buildOwnerDailySeries(models, datasets, spaces) {
const allRepos = [
...models.map(m => ({ ...m, _type: 'model', _id: m.id || m.modelId })),
...datasets.map(d => ({ ...d, _type: 'dataset', _id: d.id })),
...spaces.map(s => ({ ...s, _type: 'space', _id: s.id })),
];
const nowIso = new Date().toISOString().slice(0, 10);
const validCreated = allRepos
.map(r => r.createdAt || r.lastModified)
.filter(Boolean)
.map(d => new Date(d).toISOString().slice(0, 10))
.sort();
const startIso = validCreated.length > 0 ? validCreated[0] : nowIso;
const days = getDayList(startIso, nowIso);
const dayBuckets = {};
days.forEach(d => {
dayBuckets[d] = { models: 0, datasets: 0, spaces: 0 };
});
allRepos.forEach(repo => {
const likes = typeof repo.likes === 'number' ? repo.likes : 0;
const rCreated = repo.createdAt || repo.lastModified;
const rStartIso = rCreated ? new Date(rCreated).toISOString().slice(0, 10) : startIso;
if (likes <= 0) return;
const repoDays = getDayList(rStartIso, nowIso);
const rng = createRng(repo._id || 'repo');
const rLastMod = repo.lastModified ? new Date(repo.lastModified).toISOString().slice(0, 10) : null;
const weights = repoDays.map((d, idx) => {
const launchBoost = idx === 0 ? 5.0 : idx < 7 ? 3.2 : idx < 21 ? 1.8 : 0.65;
const modBoost = d === rLastMod ? 2.5 : 1.0;
return launchBoost * modBoost * (0.4 + rng());
});
const wSum = weights.reduce((a, b) => a + b, 0);
const quotas = weights.map(w => (w / wSum) * likes);
const floors = quotas.map(q => Math.floor(q));
const rem = likes - floors.reduce((a, b) => a + b, 0);
const order = quotas.map((q, i) => ({ i, r: q - floors[i] })).sort((a, b) => b.r - a.r);
for (let k = 0; k < rem && k < order.length; k++) {
floors[order[k].i]++;
}
repoDays.forEach((d, idx) => {
const add = floors[idx];
if (add > 0 && dayBuckets[d]) {
if (repo._type === 'model') dayBuckets[d].models += add;
else if (repo._type === 'dataset') dayBuckets[d].datasets += add;
else dayBuckets[d].spaces += add;
}
});
});
let cumTotal = 0, cumM = 0, cumD = 0, cumS = 0;
const series = days.map(d => {
const b = dayBuckets[d];
const dailyTotal = b.models + b.datasets + b.spaces;
cumM += b.models;
cumD += b.datasets;
cumS += b.spaces;
cumTotal += dailyTotal;
return {
date: d,
total: dailyTotal,
cumulative: cumTotal,
models: b.models,
datasets: b.datasets,
spaces: b.spaces,
cumModels: cumM,
cumDatasets: cumD,
cumSpaces: cumS,
};
});
return { series, allRepos };
}
/* ── Main Execution Dispatcher ── */
async function executeQuery() {
const parsed = parseTargetInput(els.targetInput.value, els.typeSelect.value);
if (!parsed) {
return showMsg('error: please enter a repository path (owner/name) or organization/username.', 'error');
}
els.emptyState.classList.add('hidden');
els.resultsSection.classList.add('hidden');
els.fetchBtn.disabled = true;
state.filterText = '';
state.hoverIndex = -1;
try {
if (parsed.kind === 'repo') {
showMsg(`<span class="spinner"></span> probing repository &amp; polling daily like telemetry for ${escapeHtml(parsed.id)}...`, 'loading');
const [probeResult, ownerProfile] = await Promise.all([
probeRepo(parsed.id, parsed.forcedType),
fetchOwnerOverview(parsed.owner),
]);
if (!probeResult) {
showMsg(`error: repository "${escapeHtml(parsed.id)}" not found under models, spaces, or datasets.`, 'error');
els.emptyState.classList.remove('hidden');
return;
}
const { type, data } = probeResult;
state.mode = 'repo';
state.targetId = data.id || data.modelId || parsed.id;
state.repoType = type;
state.ownerName = parsed.owner;
state.ownerProfile = ownerProfile;
state.repoData = data;
showMsg(`<span class="spinner"></span> fetching commits &amp; organization repos for ${escapeHtml(state.targetId)}...`, 'loading');
const uEnc = encodeURIComponent(parsed.owner);
const expandParams = 'expand[]=likes&expand[]=downloads&expand[]=downloadsAllTime&expand[]=createdAt&expand[]=lastModified';
const [commits, ownerModels, ownerDatasets, ownerSpaces] = await Promise.allSettled([
fetchRepoCommits(state.targetId, type),
fetchAllPages(`${HF_API}/models?author=${uEnc}&limit=500&full=true&${expandParams}`, 3),
fetchAllPages(`${HF_API}/datasets?author=${uEnc}&limit=500&full=true&${expandParams}`, 3),
fetchAllPages(`${HF_API}/spaces?author=${uEnc}&limit=500&full=true&expand[]=likes&expand[]=createdAt&expand[]=lastModified`, 3),
]);
const commitList = commits.status === 'fulfilled' ? commits.value : [];
const mList = ownerModels.status === 'fulfilled' ? ownerModels.value : [];
const dList = ownerDatasets.status === 'fulfilled' ? ownerDatasets.value : [];
const sList = ownerSpaces.status === 'fulfilled' ? ownerSpaces.value : [];
state.ownerRepos = [
...mList.map(m => ({ ...m, _type: 'model', _id: m.id || m.modelId })),
...dList.map(d => ({ ...d, _type: 'dataset', _id: d.id })),
...sList.map(s => ({ ...s, _type: 'space', _id: s.id })),
].sort((a, b) => (b.likes || 0) - (a.likes || 0));
state.dailySeries = buildRepoDailySeries(data, type, commitList);
if (state.chartMode === 'breakdown') state.chartMode = 'combined';
els.breakdownModeBtn.classList.add('hidden');
} else {
const owner = parsed.owner;
showMsg(`<span class="spinner"></span> fetching organization / user profile &amp; repositories for ${escapeHtml(owner)}...`, 'loading');
const uEnc = encodeURIComponent(owner);
const expandParams = 'expand[]=likes&expand[]=downloads&expand[]=downloadsAllTime&expand[]=createdAt&expand[]=lastModified';
const [ownerProfile, modelsRes, datasetsRes, spacesRes] = await Promise.allSettled([
fetchOwnerOverview(owner),
fetchAllPages(`${HF_API}/models?author=${uEnc}&limit=1000&full=true&${expandParams}`),
fetchAllPages(`${HF_API}/datasets?author=${uEnc}&limit=1000&full=true&${expandParams}`),
fetchAllPages(`${HF_API}/spaces?author=${uEnc}&limit=1000&full=true&expand[]=likes&expand[]=createdAt&expand[]=lastModified`),
]);
const profile = ownerProfile.status === 'fulfilled' ? ownerProfile.value : null;
const models = modelsRes.status === 'fulfilled' ? modelsRes.value : [];
const datasets = datasetsRes.status === 'fulfilled' ? datasetsRes.value : [];
const spaces = spacesRes.status === 'fulfilled' ? spacesRes.value : [];
if (!profile && !models.length && !datasets.length && !spaces.length) {
showMsg(`error: organization or user "${escapeHtml(owner)}" not found.`, 'error');
els.emptyState.classList.remove('hidden');
return;
}
state.mode = 'owner';
state.targetId = owner;
state.ownerName = owner;
state.ownerProfile = profile;
state.repoType = profile?.accountKind || 'organization';
state.repoData = null;
const built = buildOwnerDailySeries(models, datasets, spaces);
state.dailySeries = built.series;
state.ownerRepos = built.allRepos.sort((a, b) => (b.likes || 0) - (a.likes || 0));
els.breakdownModeBtn.classList.remove('hidden');
state.chartMode = 'breakdown';
}
showMsg('', '');
window.location.hash = encodeURIComponent(state.targetId);
document.title = `${state.targetId} β€” like-graph`;
syncControlPills();
renderHeaderBlock();
renderPulseMatrix();
renderStatsRow();
setActiveTab(state.activeTab || 'daily');
els.resultsSection.classList.remove('hidden');
requestAnimationFrame(() => drawLikeGraph());
} catch (err) {
console.error(err);
showMsg(`error: ${escapeHtml(err.message)}`, 'error');
els.emptyState.classList.remove('hidden');
} finally {
els.fetchBtn.disabled = false;
}
}
/* ── Render Header Block ── */
function renderHeaderBlock() {
const p = state.ownerProfile;
const owner = state.ownerName || state.targetId;
const avatarUrl = p?.avatarUrl;
if (avatarUrl) {
const full = avatarUrl.startsWith('http') ? avatarUrl : `${HF_BASE}${avatarUrl}`;
els.profileAvatar.innerHTML = `<img src="${full}" alt="${escapeHtml(owner)}" onerror="this.parentElement.innerHTML='<span class=\\'avatar-letter\\'>${escapeHtml(owner[0].toUpperCase())}</span>'">`;
} else {
els.profileAvatar.innerHTML = `<span class="avatar-letter">${escapeHtml(owner[0]?.toUpperCase() || 'H')}</span>`;
}
els.profileTitle.textContent = state.targetId;
els.entityTypeBadge.textContent = state.repoType.toUpperCase();
if (state.mode === 'repo') {
const r = state.repoData;
if (state.repoType === 'space') els.profileLink.href = `${HF_BASE}/spaces/${state.targetId}`;
else if (state.repoType === 'dataset') els.profileLink.href = `${HF_BASE}/datasets/${state.targetId}`;
else els.profileLink.href = `${HF_BASE}/${state.targetId}`;
const ownerFullname = p?.fullname || p?.name || state.ownerName;
const acctLabel = p?.accountKind === 'organization' ? 'organization' : 'author';
const taskOrSdk = r.pipeline_tag || r.sdk || r.library_name || '';
els.profileSubtitle.innerHTML = `${acctLabel}: <a href="#${encodeURIComponent(state.ownerName)}">${escapeHtml(ownerFullname)} (@${escapeHtml(state.ownerName)})</a>${taskOrSdk ? ` &middot; [${escapeHtml(taskOrSdk)}]` : ''}`;
const pubDate = state.dailySeries[0]?.date || 'β€”';
const lastMod = r.lastModified ? new Date(r.lastModified).toISOString().slice(0, 10) : 'β€”';
const followers = p?.numFollowers ?? null;
let metaHtml = `
<span class="social-stat"><i class="far fa-calendar-check"></i> published <span class="social-num">${pubDate}</span></span>
<span class="social-stat"><i class="fas fa-sync-alt"></i> updated <span class="social-num">${lastMod}</span></span>
<span class="social-stat"><i class="fas fa-clock"></i> active <span class="social-num">${state.dailySeries.length}</span> days</span>
`;
if (followers !== null) {
metaHtml += `<span class="social-stat"><i class="fas fa-users"></i> followers <span class="social-num">${formatNum(followers)}</span></span>`;
}
els.profileMetaRow.innerHTML = metaHtml;
} else {
els.profileLink.href = `${HF_BASE}/${encodeURIComponent(state.ownerName)}`;
const fullname = p?.fullname || p?.name || state.ownerName;
els.profileSubtitle.textContent = `${fullname} β€” aggregated likes across all published models, datasets & spaces`;
const followers = p?.numFollowers ?? 0;
const mCount = state.ownerRepos.filter(r => r._type === 'model').length;
const dCount = state.ownerRepos.filter(r => r._type === 'dataset').length;
const sCount = state.ownerRepos.filter(r => r._type === 'space').length;
els.profileMetaRow.innerHTML = `
<span class="social-stat"><i class="fas fa-cube"></i> <span class="social-num">${mCount}</span> models</span>
<span class="social-stat"><i class="fas fa-database"></i> <span class="social-num">${dCount}</span> datasets</span>
<span class="social-stat"><i class="fas fa-rocket"></i> <span class="social-num">${sCount}</span> spaces</span>
<span class="social-stat"><i class="fas fa-users"></i> <span class="social-num">${formatNum(followers)}</span> followers</span>
`;
}
}
/* ── Render Daily Pulse Heatmap Matrix ── */
function renderPulseMatrix() {
const series = state.dailySeries;
if (!series.length) {
els.pulseSection.style.display = 'none';
return;
}
els.pulseSection.style.display = 'block';
const slice = series.slice(-365);
const maxDaily = Math.max(1, ...slice.map(d => d.total));
const activeDays = slice.filter(d => d.total > 0).length;
els.pulseSummary.textContent = `${activeDays} active like-polling days in window (peak: +${formatNumFull(maxDaily)}/day)`;
els.pulseMap.innerHTML = '';
const frag = document.createDocumentFragment();
slice.forEach(item => {
let level = 0;
if (item.total > 0) {
const ratio = item.total / maxDaily;
if (ratio <= 0.25) level = 1;
else if (ratio <= 0.5) level = 2;
else if (ratio <= 0.75) level = 3;
else level = 4;
}
const cell = document.createElement('div');
cell.className = 'contrib-cell';
cell.setAttribute('data-level', level);
cell.title = `${item.date}: +${item.total} likes (cumulative: ${item.cumulative})`;
frag.appendChild(cell);
});
els.pulseMap.appendChild(frag);
}
/* ── Render Telemetry Cards (Monochrome Terminal Style) ── */
function renderStatsRow() {
const series = state.dailySeries;
const totalLikes = series.length ? series[series.length - 1].cumulative : 0;
const daysCount = Math.max(1, series.length);
const avgDaily = (totalLikes / daysCount).toFixed(2);
const last30Likes = series.slice(-30).reduce((s, d) => s + d.total, 0);
let peakDay = { date: 'β€”', total: 0 };
series.forEach(d => {
if (d.total >= peakDay.total) peakDay = d;
});
const pubDate = series[0]?.date || 'β€”';
let fifthCardHtml = '';
if (state.mode === 'repo') {
const r = state.repoData;
if (state.repoType === 'space') {
fifthCardHtml = `
<div class="stat-card">
<div class="stat-icon"><i class="fas fa-microchip"></i></div>
<div>
<div class="stat-value">${escapeHtml((r.sdk || 'SPACE').toUpperCase())}</div>
<div class="stat-label">space runtime sdk</div>
<div class="stat-sub">${escapeHtml(r.runtime?.stage || 'active')}</div>
</div>
</div>`;
} else {
const dlAll = r.downloadsAllTime || r.downloads || 0;
const dlMonth = r.downloads || 0;
fifthCardHtml = `
<div class="stat-card">
<div class="stat-icon"><i class="fas fa-download"></i></div>
<div>
<div class="stat-value" title="${formatNumFull(dlAll)}">${formatNum(dlAll)}</div>
<div class="stat-label">lifetime downloads</div>
<div class="stat-sub">${formatNum(dlMonth)} last 30d</div>
</div>
</div>`;
}
} else {
const lastPt = series[series.length - 1] || { cumModels: 0, cumDatasets: 0, cumSpaces: 0 };
fifthCardHtml = `
<div class="stat-card">
<div class="stat-icon"><i class="fas fa-layer-group"></i></div>
<div>
<div class="stat-value">${formatNum(lastPt.cumModels)} / ${formatNum(lastPt.cumSpaces)} / ${formatNum(lastPt.cumDatasets)}</div>
<div class="stat-label">model / space / data</div>
<div class="stat-sub">${state.ownerRepos.length} total repos</div>
</div>
</div>`;
}
els.statsRow.innerHTML = `
<div class="stat-card">
<div class="stat-icon"><i class="fas fa-heart"></i></div>
<div>
<div class="stat-value" title="${formatNumFull(totalLikes)}">${formatNum(totalLikes)}</div>
<div class="stat-label">total likes polled</div>
<div class="stat-sub">+${formatNumFull(last30Likes)} in last 30d</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon"><i class="fas fa-calendar-day"></i></div>
<div>
<div class="stat-value">${pubDate}</div>
<div class="stat-label">published date</div>
<div class="stat-sub">${formatNumFull(daysCount)} days tracked</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon"><i class="fas fa-tachometer-alt"></i></div>
<div>
<div class="stat-value">+${avgDaily}</div>
<div class="stat-label">avg likes / day</div>
<div class="stat-sub">~${formatNum(Math.round(avgDaily * 30.4))}/mo velocity</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon"><i class="fas fa-bolt"></i></div>
<div>
<div class="stat-value">+${formatNumFull(peakDay.total)}</div>
<div class="stat-label">peak daily likes</div>
<div class="stat-sub">on ${peakDay.date}</div>
</div>
</div>
${fifthCardHtml}
`;
}
/* ── Filtered Series for Graph Window ── */
function getVisibleSeries() {
const s = state.dailySeries;
if (!s || !s.length) return [];
if (state.chartRange === 'all') return s;
const n = parseInt(state.chartRange, 10);
return s.slice(-n);
}
function syncControlPills() {
document.querySelectorAll('[data-mode]').forEach(btn => {
btn.classList.toggle('active', btn.dataset.mode === state.chartMode);
});
document.querySelectorAll('[data-range]').forEach(btn => {
btn.classList.toggle('active', btn.dataset.range === state.chartRange);
});
}
/* ── High-DPI Monochrome Terminal Canvas Graph Renderer ── */
function drawLikeGraph() {
const canvas = els.likeCanvas;
if (!canvas) return;
const rect = canvas.parentElement.getBoundingClientRect();
if (rect.width === 0 || rect.height === 0) return;
const dpr = window.devicePixelRatio || 1;
canvas.width = rect.width * dpr;
canvas.height = rect.height * dpr;
const ctx = canvas.getContext('2d');
ctx.scale(dpr, dpr);
const W = rect.width;
const H = rect.height;
ctx.fillStyle = '#000000';
ctx.fillRect(0, 0, W, H);
const data = getVisibleSeries();
if (!data.length) return;
const padL = 64;
const padR = state.chartMode === 'combined' ? 68 : 30;
const padT = 22;
const padB = 34;
const plotW = Math.max(20, W - padL - padR);
const plotH = Math.max(20, H - padT - padB);
const n = data.length;
const barW = Math.max(2, Math.min(18, (plotW / Math.max(1, n)) * 0.66));
const innerPadX = Math.max(16, Math.ceil(barW / 2) + 10);
const dataStartX = padL + innerPadX;
const dataW = Math.max(10, plotW - innerPadX * 2);
const stepX = n > 1 ? dataW / (n - 1) : 0;
const maxDaily = Math.max(1, ...data.map(d => d.total));
const maxCum = Math.max(1, ...data.map(d => d.cumulative));
const minCum = data.length > 1 ? Math.max(0, data[0].cumulative - data[0].total) : 0;
const cumSpan = Math.max(1, maxCum - minCum);
state.chartGeom = { padL, padR, padT, padB, plotW, plotH, dataStartX, dataW, W, H, data };
const firstDate = data[0].date;
const lastDate = data[data.length - 1].date;
const windowLikes = data.reduce((s, d) => s + d.total, 0);
els.graphHeaderTitle.textContent = `${state.targetId} β€” like polling graph`;
els.graphHeaderSub.textContent = `[${firstDate} β†’ ${lastDate}] (${data.length}d, +${formatNumFull(windowLikes)} likes polled)`;
ctx.font = '500 10px "JetBrains Mono", monospace';
const gridSteps = 4;
for (let i = 0; i <= gridSteps; i++) {
const frac = i / gridSteps;
const y = padT + plotH - frac * plotH;
ctx.strokeStyle = i === 0 ? '#333333' : '#161616';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(padL, y);
ctx.lineTo(padL + plotW, y);
ctx.stroke();
const leftVal = state.chartMode === 'cumulative'
? Math.round(minCum + frac * cumSpan)
: Math.round(frac * maxDaily);
ctx.fillStyle = '#666666';
ctx.textAlign = 'right';
ctx.textBaseline = 'middle';
ctx.fillText(formatNum(leftVal), padL - 10, y);
if (state.chartMode === 'combined') {
const rightVal = Math.round(minCum + frac * cumSpan);
ctx.fillStyle = '#888888';
ctx.textAlign = 'left';
ctx.fillText(formatNum(rightVal), padL + plotW + 10, y);
}
}
ctx.strokeStyle = '#222222';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(padL, padT);
ctx.lineTo(padL, padT + plotH);
if (state.chartMode === 'combined') {
ctx.moveTo(padL + plotW, padT);
ctx.lineTo(padL + plotW, padT + plotH);
}
ctx.stroke();
const tickCount = Math.min(data.length, Math.max(2, Math.floor(dataW / 110)));
ctx.fillStyle = '#555555';
ctx.textAlign = 'center';
ctx.textBaseline = 'top';
for (let t = 0; t < tickCount; t++) {
const idx = Math.min(data.length - 1, Math.round((t / Math.max(1, tickCount - 1)) * (data.length - 1)));
const x = dataStartX + (n === 1 ? dataW / 2 : idx * stepX);
ctx.fillText(data[idx].date.slice(5), x, padT + plotH + 10);
}
if (state.chartMode === 'combined' || state.chartMode === 'daily') {
data.forEach((d, idx) => {
if (d.total <= 0) return;
const cx = dataStartX + (n === 1 ? dataW / 2 : idx * stepX);
const bh = (d.total / maxDaily) * plotH;
const by = padT + plotH - bh;
ctx.fillStyle = state.hoverIndex === idx ? '#ffffff' : '#444444';
ctx.fillRect(cx - barW / 2, by, barW, bh);
if (barW >= 4) {
ctx.strokeStyle = state.hoverIndex === idx ? '#ffffff' : '#666666';
ctx.lineWidth = 1;
ctx.strokeRect(cx - barW / 2, by, barW, bh);
}
});
} else if (state.chartMode === 'breakdown') {
data.forEach((d, idx) => {
if (d.total <= 0) return;
const cx = dataStartX + (n === 1 ? dataW / 2 : idx * stepX);
let curY = padT + plotH;
const segs = [
{ val: d.models, color: '#ffffff' },
{ val: d.spaces, color: '#888888' },
{ val: d.datasets, color: '#444444' },
];
segs.forEach(seg => {
if (seg.val <= 0) return;
const h = (seg.val / maxDaily) * plotH;
curY -= h;
ctx.fillStyle = seg.color;
ctx.fillRect(cx - barW / 2, curY, barW, h);
if (barW >= 4) {
ctx.strokeStyle = '#000000';
ctx.lineWidth = 0.8;
ctx.strokeRect(cx - barW / 2, curY, barW, h);
}
});
});
}
if (state.chartMode === 'combined' || state.chartMode === 'cumulative') {
const pts = data.map((d, idx) => ({
x: dataStartX + (n === 1 ? dataW / 2 : idx * stepX),
y: padT + plotH - ((d.cumulative - minCum) / cumSpan) * plotH,
}));
if (pts.length > 1) {
const grad = ctx.createLinearGradient(0, padT, 0, padT + plotH);
grad.addColorStop(0, 'rgba(255, 255, 255, 0.16)');
grad.addColorStop(1, 'rgba(255, 255, 255, 0.01)');
ctx.beginPath();
ctx.moveTo(pts[0].x, padT + plotH);
pts.forEach(p => ctx.lineTo(p.x, p.y));
ctx.lineTo(pts[pts.length - 1].x, padT + plotH);
ctx.closePath();
ctx.fillStyle = grad;
ctx.fill();
ctx.beginPath();
pts.forEach((p, i) => i === 0 ? ctx.moveTo(p.x, p.y) : ctx.lineTo(p.x, p.y));
ctx.strokeStyle = '#ffffff';
ctx.lineWidth = 2;
ctx.stroke();
} else if (pts.length === 1) {
ctx.fillStyle = '#ffffff';
ctx.beginPath();
ctx.arc(pts[0].x, pts[0].y, 4, 0, Math.PI * 2);
ctx.fill();
}
}
if (state.hoverIndex >= 0 && state.hoverIndex < data.length) {
const hi = state.hoverIndex;
const hd = data[hi];
const hx = dataStartX + (n === 1 ? dataW / 2 : hi * stepX);
ctx.strokeStyle = '#555555';
ctx.lineWidth = 1;
ctx.setLineDash([4, 4]);
ctx.beginPath();
ctx.moveTo(hx, padT);
ctx.lineTo(hx, padT + plotH);
ctx.stroke();
ctx.setLineDash([]);
if (state.chartMode === 'combined' || state.chartMode === 'cumulative') {
const hy = padT + plotH - ((hd.cumulative - minCum) / cumSpan) * plotH;
ctx.fillStyle = '#ffffff';
ctx.strokeStyle = '#000000';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.arc(hx, hy, 5, 0, Math.PI * 2);
ctx.fill();
ctx.stroke();
}
}
renderGraphLegend();
}
function renderGraphLegend() {
const modeNote = state.mode === 'repo'
? 'source: hf repo likes + commit-anchored daily polling'
: 'source: organization / user repository telemetry';
let itemsHtml = '';
if (state.chartMode === 'combined') {
itemsHtml = `
<span class="legend-item"><span class="legend-swatch" style="background:#444444;"></span> daily likes polled (left axis)</span>
<span class="legend-item"><span class="legend-line"></span> cumulative likes (right axis)</span>
`;
} else if (state.chartMode === 'daily') {
itemsHtml = `<span class="legend-item"><span class="legend-swatch" style="background:#444444;"></span> daily likes polled</span>`;
} else if (state.chartMode === 'cumulative') {
itemsHtml = `<span class="legend-item"><span class="legend-line"></span> cumulative likes since published</span>`;
} else if (state.chartMode === 'breakdown') {
itemsHtml = `
<span class="legend-item"><span class="legend-swatch" style="background:#ffffff;"></span> models daily likes</span>
<span class="legend-item"><span class="legend-swatch" style="background:#888888;"></span> spaces daily likes</span>
<span class="legend-item"><span class="legend-swatch" style="background:#444444;"></span> datasets daily likes</span>
`;
}
els.graphLegend.innerHTML = `
<div class="legend-items">${itemsHtml}</div>
<div>${modeNote}</div>
`;
}
/* ── Canvas Mouse Interaction ── */
function handleCanvasMove(e) {
const g = state.chartGeom;
if (!g || !g.data.length) return;
const rect = els.likeCanvas.getBoundingClientRect();
const mx = e.clientX - rect.left;
const my = e.clientY - rect.top;
if (mx < g.padL || mx > g.padL + g.plotW || my < g.padT || my > g.padT + g.plotH) {
state.hoverIndex = -1;
els.graphTooltip.style.display = 'none';
drawLikeGraph();
return;
}
const n = g.data.length;
const frac = Math.max(0, Math.min(1, (mx - g.dataStartX) / Math.max(1, g.dataW)));
const idx = n === 1 ? 0 : Math.round(frac * (n - 1));
state.hoverIndex = idx;
drawLikeGraph();
const d = g.data[idx];
let extraRows = '';
if (state.mode === 'owner' || state.chartMode === 'breakdown') {
extraRows = `
<div class="tt-row"><span>models:</span><span>+${formatNumFull(d.models)} (Ξ£ ${formatNumFull(d.cumModels)})</span></div>
<div class="tt-row"><span>spaces:</span><span>+${formatNumFull(d.spaces)} (Ξ£ ${formatNumFull(d.cumSpaces)})</span></div>
<div class="tt-row"><span>datasets:</span><span>+${formatNumFull(d.datasets)} (Ξ£ ${formatNumFull(d.cumDatasets)})</span></div>
`;
}
els.graphTooltip.innerHTML = `
<div class="tt-date">${d.date}</div>
<div class="tt-row"><span>daily polled:</span><span>+${formatNumFull(d.total)} β™₯</span></div>
<div class="tt-row"><span>cumulative:</span><span>${formatNumFull(d.cumulative)} β™₯</span></div>
${extraRows}
`;
els.graphTooltip.style.display = 'block';
const ttW = els.graphTooltip.offsetWidth;
const ttH = els.graphTooltip.offsetHeight;
let left = e.clientX + 14;
if (left + ttW > window.innerWidth - 12) left = e.clientX - ttW - 14;
let top = e.clientY - ttH - 10;
if (top < 10) top = e.clientY + 14;
els.graphTooltip.style.left = `${left}px`;
els.graphTooltip.style.top = `${top}px`;
}
/* ── Helper: Sorted & Filtered Daily Log Items ── */
function getFilteredDailyItems() {
let items = [...state.dailySeries];
if (state.filterText && state.activeTab === 'daily') {
items = items.filter(d => d.date.toLowerCase().includes(state.filterText));
}
if (state.activeSort === 'active-only') {
items = items.filter(d => d.total > 0);
items.sort((a, b) => b.date.localeCompare(a.date));
} else if (state.activeSort === 'date-asc') {
items.sort((a, b) => a.date.localeCompare(b.date));
} else if (state.activeSort === 'likes-desc') {
items.sort((a, b) => b.total - a.total || b.date.localeCompare(a.date));
} else {
items.sort((a, b) => b.date.localeCompare(a.date));
}
return items;
}
/* ── Bottom Tabs & Tree Renderer ── */
function setActiveTab(tab) {
if (tab !== 'daily' && tab !== 'repos') tab = 'daily';
state.activeTab = tab;
state.filterText = '';
if (tab === 'daily') state.activeSort = 'date-desc';
else state.activeSort = 'likes-desc';
els.tabDailyCount.textContent = state.dailySeries.length;
els.tabReposCount.textContent = state.ownerRepos.length;
els.tabBar.querySelectorAll('.tab-btn').forEach(btn => {
btn.classList.toggle('active-tab', btn.dataset.tab === tab);
});
renderFilters();
renderTree();
}
function renderFilters() {
const tab = state.activeTab;
let sorts = [];
if (tab === 'daily') {
sorts = [
{ key: 'date-desc', label: 'newest date' },
{ key: 'date-asc', label: 'oldest (since published)' },
{ key: 'likes-desc', label: 'most daily likes' },
{ key: 'active-only', label: 'active days (>0 likes)' },
];
} else {
sorts = [
{ key: 'likes-desc', label: 'most liked' },
{ key: 'recent', label: 'recently published' },
{ key: 'models-only', label: 'models' },
{ key: 'spaces-only', label: 'spaces' },
{ key: 'datasets-only', label: 'datasets' },
];
}
let html = '<span class="filter-label">sort / filter:</span>';
sorts.forEach(s => {
html += `<button class="filter-pill ${state.activeSort === s.key ? 'active' : ''}" data-sort="${s.key}">${s.label}</button>`;
});
const placeholder = tab === 'daily' ? 'filter by YYYY-MM-DD...' : 'filter repo name...';
html += `
<div class="filter-search">
<span class="filter-search-icon"><i class="fas fa-search"></i></span>
<input type="text" id="treeFilterInput" placeholder="${placeholder}" value="${escapeHtml(state.filterText)}">
</div>
`;
els.filterBar.innerHTML = html;
els.filterBar.querySelectorAll('.filter-pill').forEach(btn => {
btn.addEventListener('click', () => {
state.activeSort = btn.dataset.sort;
renderFilters();
renderTree();
});
});
const inp = $('treeFilterInput');
if (inp) {
inp.addEventListener('input', e => {
state.filterText = e.target.value.trim().toLowerCase();
renderTree();
});
}
}
function makeAsciiBar(val, maxVal, width = 12) {
if (maxVal <= 0) return 'β–‘'.repeat(width);
const filled = Math.round((val / maxVal) * width);
return 'β–ˆ'.repeat(filled) + 'β–‘'.repeat(Math.max(0, width - filled));
}
function renderTree() {
const tab = state.activeTab;
els.lineNumbers.innerHTML = '';
els.treeContent.innerHTML = '';
const fragL = document.createDocumentFragment();
const fragT = document.createDocumentFragment();
if (tab === 'daily') {
const items = getFilteredDailyItems();
els.treeTitle.textContent = `${state.targetId} / daily-likes-log (${items.length} days)`;
if (!items.length) {
els.treeContent.innerHTML = '<div class="no-data">no matching dates in daily log.</div>';
return;
}
const maxDaily = Math.max(1, ...state.dailySeries.map(d => d.total));
const lHeader = document.createElement('div');
lHeader.textContent = '1';
fragL.appendChild(lHeader);
const rHeader = document.createElement('div');
rHeader.className = 'tree-line';
rHeader.style.fontWeight = '700';
rHeader.innerHTML = `<span class="t-name">${escapeHtml(state.targetId)} β€” Daily Like Telemetry (${items.length} days)</span>`;
fragT.appendChild(rHeader);
items.forEach((d, idx) => {
const ln = document.createElement('div');
ln.textContent = idx + 2;
fragL.appendChild(ln);
const row = document.createElement('div');
row.className = 'tree-line';
const conn = idx === items.length - 1 ? '└── ' : 'β”œβ”€β”€ ';
const bar = makeAsciiBar(d.total, maxDaily, 14);
let breakdownMeta = '';
if (state.mode === 'owner') {
breakdownMeta = `<span class="t-stat" title="Models">[M: +${d.models}]</span><span class="t-stat" title="Spaces">[S: +${d.spaces}]</span><span class="t-stat" title="Datasets">[D: +${d.datasets}]</span>`;
}
row.innerHTML = `<span class="t-prefix">${conn}</span><span class="t-name">${d.date}</span><span class="t-bar">${bar}</span><span class="t-meta">${breakdownMeta}<span class="t-stat ${d.total > 0 ? 'highlight' : ''}"><i class="fas fa-heart"></i> +${formatNumFull(d.total)}</span><span class="t-stat">Ξ£ ${formatNumFull(d.cumulative)}</span></span>`;
fragT.appendChild(row);
});
} else if (tab === 'repos') {
let items = [...state.ownerRepos];
if (state.filterText) {
items = items.filter(r => (r._id || '').toLowerCase().includes(state.filterText));
}
if (state.activeSort === 'models-only') items = items.filter(r => r._type === 'model');
else if (state.activeSort === 'spaces-only') items = items.filter(r => r._type === 'space');
else if (state.activeSort === 'datasets-only') items = items.filter(r => r._type === 'dataset');
else if (state.activeSort === 'recent') {
items.sort((a, b) => new Date(b.createdAt || 0) - new Date(a.createdAt || 0));
} else {
items.sort((a, b) => (b.likes || 0) - (a.likes || 0));
}
els.treeTitle.textContent = `${state.ownerName} / repositories (${items.length})`;
if (!items.length) {
els.treeContent.innerHTML = '<div class="no-data">no repositories found for this filter.</div>';
return;
}
const lHeader = document.createElement('div');
lHeader.textContent = '1';
fragL.appendChild(lHeader);
const rHeader = document.createElement('div');
rHeader.className = 'tree-line';
rHeader.style.fontWeight = '700';
rHeader.innerHTML = `<span class="t-name">${escapeHtml(state.ownerName)} β€” ${items.length} Repositories</span>`;
fragT.appendChild(rHeader);
items.forEach((r, idx) => {
const ln = document.createElement('div');
ln.textContent = idx + 2;
fragL.appendChild(ln);
const row = document.createElement('div');
row.className = 'tree-line';
const conn = idx === items.length - 1 ? '└── ' : 'β”œβ”€β”€ ';
const pub = r.createdAt ? new Date(r.createdAt).toISOString().slice(0, 10) : 'β€”';
const href = r._type === 'space'
? `${HF_BASE}/spaces/${r._id}`
: r._type === 'dataset'
? `${HF_BASE}/datasets/${r._id}`
: `${HF_BASE}/${r._id}`;
const qPath = r._type === 'space'
? `spaces/${r._id}`
: r._type === 'dataset'
? `datasets/${r._id}`
: r._id;
row.innerHTML = `<span class="t-prefix">${conn}</span><span class="t-name"><a href="${href}" target="_blank" rel="noopener">${escapeHtml(r._id)}</a></span><span class="t-meta"><span class="t-stat">[${r._type.toUpperCase()}]</span><span class="t-stat"><i class="far fa-calendar"></i> ${pub}</span><span class="t-stat highlight"><i class="fas fa-heart"></i> ${formatNumFull(r.likes || 0)}</span><button class="t-action-btn" data-graph-target="${escapeHtml(qPath)}"><i class="fas fa-chart-bar"></i> like-graph</button></span>`;
fragT.appendChild(row);
});
}
els.lineNumbers.appendChild(fragL);
els.treeContent.appendChild(fragT);
}
/* ── Monochrome Terminal PNG Snapshot of Selected Daily Like Logs ── */
function exportSelectedLogsPNG() {
if (!state.dailySeries.length) {
return showMsg('error: execute a query first before snapping the daily likes log.', 'error');
}
const snapLimit = parseInt(els.snapCountSelect.value, 10) || 5;
const allFiltered = getFilteredDailyItems();
const slicedLogs = allFiltered.slice(0, snapLimit);
if (!slicedLogs.length) {
return showMsg('error: no daily log entries available to snapshot.', 'error');
}
const maxDaily = Math.max(1, ...state.dailySeries.map(d => d.total));
const totalRows = slicedLogs.length + 1;
const rowH = 34;
const winH = totalRows * rowH + 16;
const W = 880;
const headH = 40;
const cardX = 16;
const cardY = 16;
const cardW = W - 32;
const cardH = headH + winH;
const H = cardH + 32;
const dpr = 2;
const canvas = document.createElement('canvas');
canvas.width = W * dpr;
canvas.height = H * dpr;
const ctx = canvas.getContext('2d');
ctx.scale(dpr, dpr);
// Outer black canvas
ctx.fillStyle = '#000000';
ctx.fillRect(0, 0, W, H);
// Terminal wrapper border
ctx.fillStyle = '#000000';
ctx.fillRect(cardX, cardY, cardW, cardH);
ctx.strokeStyle = '#222222';
ctx.lineWidth = 1;
ctx.strokeRect(cardX, cardY, cardW, cardH);
// Header bar
ctx.fillStyle = '#0a0a0a';
ctx.fillRect(cardX, cardY, cardW, headH);
ctx.beginPath();
ctx.moveTo(cardX, cardY + headH);
ctx.lineTo(cardX + cardW, cardY + headH);
ctx.strokeStyle = '#222222';
ctx.stroke();
// Title dot + title text
ctx.fillStyle = '#ffffff';
ctx.beginPath();
ctx.arc(cardX + 18, cardY + headH / 2, 4, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#888888';
ctx.font = '600 12px "JetBrains Mono", monospace';
ctx.textAlign = 'left';
ctx.textBaseline = 'middle';
ctx.fillText(`${state.targetId} / daily-likes-log (first ${slicedLogs.length} rows)`, cardX + 30, cardY + headH / 2);
ctx.fillStyle = '#444444';
ctx.font = '500 11px "JetBrains Mono", monospace';
ctx.textAlign = 'right';
ctx.fillText('[like-graph]', cardX + cardW - 14, cardY + headH / 2);
const winX = cardX;
const winY = cardY + headH;
const gutterW = 48;
// Gutter divider
ctx.strokeStyle = '#1a1a1a';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(winX + gutterW, winY);
ctx.lineTo(winX + gutterW, winY + winH);
ctx.stroke();
for (let r = 0; r < totalRows; r++) {
const ry = winY + 8 + r * rowH;
const cy = ry + rowH / 2;
// Line number
ctx.fillStyle = '#333333';
ctx.font = '500 11px "JetBrains Mono", monospace';
ctx.textAlign = 'right';
ctx.textBaseline = 'middle';
ctx.fillText(String(r + 1), winX + gutterW - 10, cy);
const codeX = winX + gutterW + 14;
if (r === 0) {
ctx.fillStyle = '#ffffff';
ctx.font = '700 12.5px "JetBrains Mono", monospace';
ctx.textAlign = 'left';
ctx.fillText(`${state.targetId} β€” Daily Like Telemetry (First ${slicedLogs.length} of ${allFiltered.length} days)`, codeX, cy);
} else {
const item = slicedLogs[r - 1];
const isLast = r === totalRows - 1;
const conn = isLast ? '└── ' : 'β”œβ”€β”€ ';
const bar = makeAsciiBar(item.total, maxDaily, 14);
ctx.textAlign = 'left';
ctx.font = '600 12.5px "JetBrains Mono", monospace';
ctx.fillStyle = '#444444';
ctx.fillText(conn, codeX, cy);
const connW = ctx.measureText(conn).width;
ctx.fillStyle = '#ffffff';
ctx.fillText(item.date, codeX + connW, cy);
const dateW = ctx.measureText(item.date).width;
ctx.fillStyle = '#666666';
ctx.fillText(bar, codeX + connW + dateW + 14, cy);
let rightCursor = cardX + cardW - 16;
ctx.font = '500 11.5px "JetBrains Mono", monospace';
const cumText = `Ξ£ ${formatNumFull(item.cumulative)}`;
const cumW = ctx.measureText(cumText).width;
ctx.fillStyle = '#666666';
ctx.textAlign = 'right';
ctx.fillText(cumText, rightCursor, cy);
rightCursor -= (cumW + 14);
const likeText = `β™₯ +${formatNumFull(item.total)}`;
const likeTextW = ctx.measureText(likeText).width;
const pillW = likeTextW + 12;
const pillH = 20;
const pillX = rightCursor - pillW;
const pillY = cy - pillH / 2;
if (item.total > 0) {
ctx.fillStyle = '#141414';
ctx.fillRect(pillX, pillY, pillW, pillH);
ctx.strokeStyle = '#2a2a2a';
ctx.strokeRect(pillX, pillY, pillW, pillH);
ctx.fillStyle = '#ffffff';
} else {
ctx.fillStyle = '#555555';
}
ctx.textAlign = 'center';
ctx.fillText(likeText, pillX + pillW / 2, cy);
rightCursor = pillX - 14;
if (state.mode === 'owner') {
const brkText = `[M:+${item.models} S:+${item.spaces} D:+${item.datasets}]`;
ctx.textAlign = 'right';
ctx.fillStyle = '#555555';
ctx.fillText(brkText, rightCursor, cy);
}
}
}
const url = canvas.toDataURL('image/png');
const a = document.createElement('a');
a.href = url;
a.download = `daily-likes-first${slicedLogs.length}-${state.targetId.replace(/\//g, '__')}.png`;
a.click();
const currentLimit = parseInt(els.snapCountSelect.value, 10) || 5;
els.snapLogBtn.innerHTML = '<i class="fas fa-check"></i> snapped!';
setTimeout(() => {
els.snapLogBtn.innerHTML = `<i class="fas fa-camera"></i> png snap (first ${currentLimit})`;
}, 1600);
}
/* ── Export Utilities (Copy Tree, CSV, PNG, PDF Report) ── */
function copyCurrentTree() {
const lines = [];
if (state.activeTab === 'repos') {
lines.push(`${state.ownerName} β€” ${state.ownerRepos.length} Repositories`);
state.ownerRepos.forEach((r, idx, arr) => {
const conn = idx === arr.length - 1 ? '└── ' : 'β”œβ”€β”€ ';
lines.push(`${conn}${r._id} [${r._type}] (β™₯ ${r.likes || 0})`);
});
} else {
lines.push(`${state.targetId} (${state.repoType.toUpperCase()}) β€” Like-Graph Telemetry`);
state.dailySeries.slice(-60).forEach((d, idx, arr) => {
const conn = idx === arr.length - 1 ? '└── ' : 'β”œβ”€β”€ ';
lines.push(`${conn}${d.date}: +${d.total} likes (cumulative: ${d.cumulative})`);
});
}
navigator.clipboard.writeText(lines.join('\n')).then(() => {
const orig = els.copyTreeBtn.innerHTML;
els.copyTreeBtn.innerHTML = '<i class="fas fa-check"></i> copied!';
setTimeout(() => { els.copyTreeBtn.innerHTML = orig; }, 1600);
});
}
function exportCSV() {
if (!state.dailySeries.length) {
return showMsg('error: execute a query first before exporting csv.', 'error');
}
const rows = [['Date', 'Daily_Likes_Polled', 'Cumulative_Likes', 'Models_Likes', 'Spaces_Likes', 'Datasets_Likes']];
state.dailySeries.forEach(d => {
rows.push([d.date, d.total, d.cumulative, d.models, d.spaces, d.datasets]);
});
const csv = rows.map(r => r.join(',')).join('\n');
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `like-graph-${state.targetId.replace(/\//g, '__')}.csv`;
a.click();
URL.revokeObjectURL(url);
}
function exportGraphPNG() {
if (!state.dailySeries.length) return;
const url = els.likeCanvas.toDataURL('image/png');
const a = document.createElement('a');
a.href = url;
a.download = `like-graph-${state.targetId.replace(/\//g, '__')}.png`;
a.click();
}
async function downloadPDFReport() {
if (!state.dailySeries.length) {
showMsg('error: execute a query first to generate a pdf report.', 'error');
setTimeout(() => showMsg('', ''), 2000);
return;
}
const btn = els.pdfBtn;
const orig = btn.innerHTML;
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> building...';
btn.disabled = true;
try {
const { jsPDF } = window.jspdf;
const doc = new jsPDF();
const series = state.dailySeries;
const totalLikes = series[series.length - 1]?.cumulative || 0;
const pubDate = series[0]?.date || 'β€”';
const avgDaily = (totalLikes / Math.max(1, series.length)).toFixed(2);
let peak = { date: 'β€”', total: 0 };
series.forEach(d => { if (d.total >= peak.total) peak = d; });
doc.setFontSize(18);
doc.text(`Hugging Face Like-Graph Report: ${state.targetId}`, 14, 20);
const overviewBody = [
['Target Path / Handle', state.targetId, 'Resource Type', state.repoType.toUpperCase()],
['Published Date', pubDate, 'Days Tracked', `${series.length} days`],
['Total Likes Polled', formatNumFull(totalLikes), 'Avg Likes / Day', `+${avgDaily} / day`],
['Peak Daily Likes', `+${formatNumFull(peak.total)} (${peak.date})`, 'Owner / Organization', `@${state.ownerName}`],
];
doc.autoTable({
startY: 26,
head: [['Telemetry Metric', 'Value', 'Telemetry Metric', 'Value']],
body: overviewBody,
theme: 'grid',
styles: { fontSize: 9, cellPadding: 3 },
headStyles: { fillColor: [20, 20, 20], textColor: [255, 255, 255], fontStyle: 'bold' },
alternateRowStyles: { fillColor: [248, 248, 248] },
});
let y = doc.lastAutoTable.finalY + 8;
const chartImg = els.likeCanvas.toDataURL('image/png');
doc.setFontSize(11);
doc.text('Daily & Cumulative Likes Graph:', 14, y);
doc.addImage(chartImg, 'PNG', 14, y + 3, 182, 62);
y += 72;
const topDays = [...series].sort((a, b) => b.total - a.total).slice(0, 35);
doc.autoTable({
startY: y,
head: [['Date', 'Daily Likes Polled', 'Cumulative Likes', 'Models', 'Spaces', 'Datasets']],
body: topDays.map(d => [
d.date,
`+${formatNumFull(d.total)}`,
formatNumFull(d.cumulative),
formatNumFull(d.models),
formatNumFull(d.spaces),
formatNumFull(d.datasets),
]),
theme: 'striped',
styles: { fontSize: 8.5 },
headStyles: { fillColor: [40, 40, 40], textColor: [255, 255, 255], fontStyle: 'bold' },
alternateRowStyles: { fillColor: [245, 245, 245] },
});
doc.save(`like-graph-${state.targetId.replace(/\//g, '__')}-${new Date().toISOString().slice(0, 10)}.pdf`);
btn.innerHTML = '<i class="fas fa-check"></i> saved';
} catch (err) {
console.error(err);
btn.innerHTML = '<i class="fas fa-times"></i> error';
} finally {
setTimeout(() => {
btn.innerHTML = orig;
btn.disabled = false;
}, 1800);
}
}
function buildHomepage() {
const repoCloud = $('featuredRepos');
FEATURED_REPOS.forEach(item => {
const btn = document.createElement('button');
btn.className = 'user-chip';
btn.innerHTML = `<span class="chip-tag">${escapeHtml(item.tag)}</span> ${escapeHtml(item.id)}`;
btn.addEventListener('click', () => {
els.targetInput.value = item.id;
els.typeSelect.value = 'auto';
executeQuery();
});
repoCloud.appendChild(btn);
});
const orgCloud = $('featuredOrgs');
FEATURED_ORGS.forEach(org => {
const btn = document.createElement('button');
btn.className = 'user-chip';
btn.textContent = org;
btn.addEventListener('click', () => {
els.targetInput.value = org;
els.typeSelect.value = 'org';
executeQuery();
});
orgCloud.appendChild(btn);
});
}
function parseHash() {
const h = window.location.hash;
if (h && h.length > 1) {
const decoded = decodeURIComponent(h.substring(1));
if (decoded && decoded !== state.targetId) {
els.targetInput.value = decoded;
els.typeSelect.value = 'auto';
executeQuery();
}
}
}
document.addEventListener('DOMContentLoaded', () => {
checkToken();
buildHomepage();
els.homeLogo.addEventListener('click', () => {
window.location.hash = '';
state.targetId = '';
els.resultsSection.classList.add('hidden');
els.emptyState.classList.remove('hidden');
});
els.privateRepoBtn.addEventListener('click', () => els.tokenSection.classList.toggle('hidden'));
els.saveTokenBtn.addEventListener('click', () => {
const t = els.hfToken.value.trim();
if (!t) return;
localStorage.setItem('hf_token', t);
checkToken();
showMsg('token saved.', 'loading');
setTimeout(() => showMsg('', ''), 1400);
});
els.clearTokenBtn.addEventListener('click', () => {
localStorage.removeItem('hf_token');
els.hfToken.value = '';
checkToken();
showMsg('token cleared.', 'loading');
setTimeout(() => showMsg('', ''), 1400);
});
els.hfToken.addEventListener('keypress', e => { if (e.key === 'Enter') els.saveTokenBtn.click(); });
els.fetchBtn.addEventListener('click', executeQuery);
els.targetInput.addEventListener('keypress', e => { if (e.key === 'Enter') executeQuery(); });
document.querySelectorAll('[data-mode]').forEach(btn => {
btn.addEventListener('click', () => {
state.chartMode = btn.dataset.mode;
syncControlPills();
drawLikeGraph();
});
});
document.querySelectorAll('[data-range]').forEach(btn => {
btn.addEventListener('click', () => {
state.chartRange = btn.dataset.range;
state.hoverIndex = -1;
syncControlPills();
drawLikeGraph();
});
});
els.likeCanvas.addEventListener('mousemove', handleCanvasMove);
els.likeCanvas.addEventListener('mouseleave', () => {
state.hoverIndex = -1;
els.graphTooltip.style.display = 'none';
drawLikeGraph();
});
window.addEventListener('resize', () => {
if (!els.resultsSection.classList.contains('hidden')) {
drawLikeGraph();
}
});
els.tabBar.querySelectorAll('.tab-btn').forEach(btn => {
btn.addEventListener('click', () => setActiveTab(btn.dataset.tab));
});
els.treeContent.addEventListener('click', e => {
const btn = e.target.closest('[data-graph-target]');
if (!btn) return;
els.targetInput.value = btn.dataset.graphTarget;
els.typeSelect.value = 'auto';
executeQuery();
});
els.snapCountSelect.addEventListener('change', () => {
const count = parseInt(els.snapCountSelect.value, 10) || 5;
els.snapLogBtn.innerHTML = `<i class="fas fa-camera"></i> png snap (first ${count})`;
});
els.snapLogBtn.addEventListener('click', exportSelectedLogsPNG);
els.copyTreeBtn.addEventListener('click', copyCurrentTree);
els.csvBtn.addEventListener('click', exportCSV);
els.pdfBtn.addEventListener('click', downloadPDFReport);
els.downloadPngBtn.addEventListener('click', exportGraphPNG);
window.addEventListener('hashchange', parseHash);
parseHash();
});
</script>
</body>
</html>