mirror of
https://github.com/yusufipk/YouTubeOutlierBadge.git
synced 2026-09-11 10:56:14 +00:00
Make the distribution chart readable
Bars are HTML now instead of SVG: the SVG <title> tooltip barely showed up in Firefox and thin bars were hard to hit. Each bar sits in a full-height hover target, hovering shows the video's title, views and age, clicking opens it. A legend explains the median line and the highlighted bar, and baselines now cache titles for those tooltips.
This commit is contained in:
+18
@@ -32,6 +32,15 @@ var OBI18n = (function () {
|
|||||||
panelDaily: "Günlük ortalama",
|
panelDaily: "Günlük ortalama",
|
||||||
panelDailyValue: "$1 izlenme",
|
panelDailyValue: "$1 izlenme",
|
||||||
panelDistTitle: "Kanalın son videoları (eskiden yeniye)",
|
panelDistTitle: "Kanalın son videoları (eskiden yeniye)",
|
||||||
|
chartMedianLegend: "Kırmızı çizgi: medyan",
|
||||||
|
chartSelfLegend: "Turuncu: bu video",
|
||||||
|
chartNotInPool: "Bu video kanalın son $1 videosunun içinde değil",
|
||||||
|
chartHint: "Her çubuk bir video, yüksekliği izlenmesi. Üzerine gel, tıkla açılsın.",
|
||||||
|
tipViews: "$1 izlenme",
|
||||||
|
agoDays: "$1 gün önce",
|
||||||
|
agoMonths: "$1 ay önce",
|
||||||
|
agoYears: "$1 yıl önce",
|
||||||
|
tipThisVideo: "bu video",
|
||||||
panelNoScore: "Skor hesaplanamadı: kanalın bu havuzunda en az $1 video gerekiyor.",
|
panelNoScore: "Skor hesaplanamadı: kanalın bu havuzunda en az $1 video gerekiyor.",
|
||||||
panelError: "Skor alınamadı: $1",
|
panelError: "Skor alınamadı: $1",
|
||||||
panelNote: "Kanal izlenmeleri YouTube tarafından yuvarlanmış gelir, skor yaklaşıktır. " +
|
panelNote: "Kanal izlenmeleri YouTube tarafından yuvarlanmış gelir, skor yaklaşıktır. " +
|
||||||
@@ -89,6 +98,15 @@ var OBI18n = (function () {
|
|||||||
panelDaily: "Daily average",
|
panelDaily: "Daily average",
|
||||||
panelDailyValue: "$1 views",
|
panelDailyValue: "$1 views",
|
||||||
panelDistTitle: "Channel's recent videos (oldest to newest)",
|
panelDistTitle: "Channel's recent videos (oldest to newest)",
|
||||||
|
chartMedianLegend: "Red line: median",
|
||||||
|
chartSelfLegend: "Orange: this video",
|
||||||
|
chartNotInPool: "This video is not among the channel's last $1",
|
||||||
|
chartHint: "Each bar is a video, its height the view count. Hover for details, click to open.",
|
||||||
|
tipViews: "$1 views",
|
||||||
|
agoDays: "$1 days ago",
|
||||||
|
agoMonths: "$1 months ago",
|
||||||
|
agoYears: "$1 years ago",
|
||||||
|
tipThisVideo: "this video",
|
||||||
panelNoScore: "No score: this channel pool needs at least $1 videos.",
|
panelNoScore: "No score: this channel pool needs at least $1 videos.",
|
||||||
panelError: "Score unavailable: $1",
|
panelError: "Score unavailable: $1",
|
||||||
panelNote: "Channel view counts come rounded from YouTube, so the score is approximate. " +
|
panelNote: "Channel view counts come rounded from YouTube, so the score is approximate. " +
|
||||||
|
|||||||
+60
-30
@@ -12,7 +12,6 @@ var OBPanel = (function () {
|
|||||||
|
|
||||||
var P = OBParse;
|
var P = OBParse;
|
||||||
var T = OBI18n;
|
var T = OBI18n;
|
||||||
var SVG = "http://www.w3.org/2000/svg";
|
|
||||||
|
|
||||||
function el(tag, cls, text) {
|
function el(tag, cls, text) {
|
||||||
var node = document.createElement(tag);
|
var node = document.createElement(tag);
|
||||||
@@ -21,44 +20,75 @@ var OBPanel = (function () {
|
|||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
|
|
||||||
function svgEl(tag, attrs) {
|
function agoText(days) {
|
||||||
var node = document.createElementNS(SVG, tag);
|
if (days == null) return "";
|
||||||
for (var k in attrs) node.setAttribute(k, attrs[k]);
|
if (days < 60) return T.t("agoDays", Math.round(days));
|
||||||
return node;
|
if (days < 730) return T.t("agoMonths", Math.round(days / 30.44));
|
||||||
|
return T.t("agoYears", Math.round(days / 365.25));
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Kanalın son videolarının izlenme dağılımı, eskiden yeniye.
|
/* Kanalın son videolarının izlenme dağılımı, eskiden yeniye.
|
||||||
* Bu video vurgulu, medyan kesikli çizgi. */
|
*
|
||||||
|
* Çubuklar SVG değil düz HTML: SVG <title> ipucu balonu Firefox'ta ancak
|
||||||
|
* uzun beklemeyle çıkıyor ve ince çubuklarda fareyi yakalamak zor. Burada
|
||||||
|
* her çubuğun arkasında tam yükseklikte görünmez bir isabet alanı var ve
|
||||||
|
* balonu kendimiz çiziyoruz. */
|
||||||
function distribution(pool, videoId, median) {
|
function distribution(pool, videoId, median) {
|
||||||
var items = pool.slice().reverse();
|
var items = pool.slice().reverse();
|
||||||
var w = 100, h = 46;
|
var wrap = el("div", "ob-chart-wrap");
|
||||||
var svg = svgEl("svg", { viewBox: "0 0 " + w + " " + h, class: "ob-chart", preserveAspectRatio: "none" });
|
var chart = el("div", "ob-chart");
|
||||||
var max = 0;
|
var max = 0;
|
||||||
for (var i = 0; i < items.length; i++) max = Math.max(max, items[i].views || 0);
|
for (var i = 0; i < items.length; i++) max = Math.max(max, items[i].views || 0);
|
||||||
if (!max) return svg;
|
if (!max) return wrap;
|
||||||
var bw = w / items.length;
|
|
||||||
for (var j = 0; j < items.length; j++) {
|
|
||||||
var v = items[j].views || 0;
|
|
||||||
var bh = Math.max(1, (v / max) * (h - 2));
|
|
||||||
var bar = svgEl("rect", {
|
|
||||||
x: (j * bw + bw * 0.12).toFixed(2),
|
|
||||||
y: (h - bh).toFixed(2),
|
|
||||||
width: (bw * 0.76).toFixed(2),
|
|
||||||
height: bh.toFixed(2),
|
|
||||||
class: items[j].id === videoId ? "ob-bar ob-bar-self" : "ob-bar"
|
|
||||||
});
|
|
||||||
var tip = svgEl("title", {});
|
|
||||||
tip.textContent = P.humanCount(v);
|
|
||||||
bar.appendChild(tip);
|
|
||||||
svg.appendChild(bar);
|
|
||||||
}
|
|
||||||
if (median) {
|
if (median) {
|
||||||
var y = h - (median / max) * (h - 2);
|
var line = el("div", "ob-median");
|
||||||
svg.appendChild(svgEl("line", {
|
line.style.bottom = ((median / max) * 100).toFixed(2) + "%";
|
||||||
x1: 0, x2: w, y1: y.toFixed(2), y2: y.toFixed(2), class: "ob-median"
|
chart.appendChild(line);
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
return svg;
|
|
||||||
|
var tip = el("div", "ob-tip");
|
||||||
|
tip.hidden = true;
|
||||||
|
|
||||||
|
items.forEach(function (item) {
|
||||||
|
var slot = el("div", "ob-slot");
|
||||||
|
var bar = el("div", "ob-bar" + (item.id === videoId ? " ob-bar-self" : ""));
|
||||||
|
bar.style.height = Math.max(1, (item.views / max) * 100).toFixed(2) + "%";
|
||||||
|
slot.appendChild(bar);
|
||||||
|
|
||||||
|
slot.addEventListener("mouseenter", function () {
|
||||||
|
tip.textContent = "";
|
||||||
|
if (item.title) tip.appendChild(el("div", "ob-tip-title", item.title));
|
||||||
|
var meta = [T.t("tipViews", P.humanCount(item.views))];
|
||||||
|
if (item.age != null) meta.push(agoText(item.age));
|
||||||
|
if (item.id === videoId) meta.push(T.t("tipThisVideo"));
|
||||||
|
tip.appendChild(el("div", "ob-tip-meta", meta.join(" · ")));
|
||||||
|
tip.hidden = false;
|
||||||
|
/* Balon grafiğin içinde konumlanır, kenarlardan taşmaması için
|
||||||
|
* yatayda kırpılır. */
|
||||||
|
var left = slot.offsetLeft + slot.offsetWidth / 2;
|
||||||
|
tip.style.left = Math.min(Math.max(left, 90), chart.offsetWidth - 90) + "px";
|
||||||
|
});
|
||||||
|
slot.addEventListener("click", function () {
|
||||||
|
window.open("https://www.youtube.com/watch?v=" + item.id, "_blank", "noopener");
|
||||||
|
});
|
||||||
|
chart.appendChild(slot);
|
||||||
|
});
|
||||||
|
|
||||||
|
chart.addEventListener("mouseleave", function () { tip.hidden = true; });
|
||||||
|
chart.appendChild(tip);
|
||||||
|
wrap.appendChild(chart);
|
||||||
|
|
||||||
|
var legend = el("div", "ob-legend");
|
||||||
|
legend.appendChild(el("span", "ob-legend-median", T.t("chartMedianLegend")));
|
||||||
|
if (items.some(function (v) { return v.id === videoId; })) {
|
||||||
|
legend.appendChild(el("span", "ob-legend-self", T.t("chartSelfLegend")));
|
||||||
|
} else {
|
||||||
|
legend.appendChild(el("span", null, T.t("chartNotInPool", items.length)));
|
||||||
|
}
|
||||||
|
legend.appendChild(el("span", null, T.t("chartHint")));
|
||||||
|
wrap.appendChild(legend);
|
||||||
|
return wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
function row(label, value) {
|
function row(label, value) {
|
||||||
|
|||||||
+5
-1
@@ -53,7 +53,11 @@ var OBScore = (function () {
|
|||||||
out.push({
|
out.push({
|
||||||
id: v.videoId,
|
id: v.videoId,
|
||||||
views: v.views,
|
views: v.views,
|
||||||
age: v.ageDays == null ? null : Math.round(v.ageDays * 10) / 10
|
age: v.ageDays == null ? null : Math.round(v.ageDays * 10) / 10,
|
||||||
|
/* Başlık sadece paneldeki dağılım grafiğinin ipucu balonu için; skorun
|
||||||
|
* kendisi kullanmaz. Kanal başına 30 başlık önbelleğe birkaç kilobayt
|
||||||
|
* ekliyor, karşılığında çubuklar hangi video olduklarını söylüyor. */
|
||||||
|
title: (v.title || "").slice(0, 70)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return out;
|
return out;
|
||||||
|
|||||||
+2
-1
@@ -11,7 +11,8 @@ var OBStore = (function () {
|
|||||||
var api = (typeof browser !== "undefined" ? browser : chrome);
|
var api = (typeof browser !== "undefined" ? browser : chrome);
|
||||||
var local = api.storage.local;
|
var local = api.storage.local;
|
||||||
|
|
||||||
var CACHE_VERSION = 1;
|
/* 2: baseline videolarına başlık eklendi (panelin grafik ipuçları için). */
|
||||||
|
var CACHE_VERSION = 2;
|
||||||
var MAX_CHANNELS = 400; /* aşılınca en eski baseline'lar silinir */
|
var MAX_CHANNELS = 400; /* aşılınca en eski baseline'lar silinir */
|
||||||
|
|
||||||
var DEFAULTS = {
|
var DEFAULTS = {
|
||||||
|
|||||||
+87
-5
@@ -106,15 +106,97 @@
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ob-chart-wrap { margin-top: 4px; }
|
||||||
|
|
||||||
.ob-chart {
|
.ob-chart {
|
||||||
display: block;
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: 2px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 52px;
|
height: 96px;
|
||||||
|
padding-top: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ob-bar { fill: var(--yt-spec-text-secondary, #909090); opacity: 0.45; }
|
/* Isabet alanı çubuğun kendisi değil, tam yükseklikte bu kutu: kısa çubukları
|
||||||
.ob-bar-self { fill: #f59e0b; opacity: 1; }
|
yakalamak için fareyi grafiğin dibine indirmek gerekmesin. */
|
||||||
.ob-median { stroke: #ef4444; stroke-width: 0.6; stroke-dasharray: 2 2; }
|
.ob-slot {
|
||||||
|
position: relative;
|
||||||
|
flex: 1 1 0;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ob-slot:hover { background: var(--yt-spec-10-percent-layer, rgba(128, 128, 128, 0.18)); }
|
||||||
|
|
||||||
|
.ob-bar {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 2px;
|
||||||
|
border-radius: 2px 2px 0 0;
|
||||||
|
background: var(--yt-spec-text-secondary, #909090);
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ob-slot:hover .ob-bar { opacity: 0.9; }
|
||||||
|
.ob-bar-self { background: #f59e0b; opacity: 1; }
|
||||||
|
|
||||||
|
.ob-median {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 0;
|
||||||
|
border-top: 1px dashed #ef4444;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ob-tip {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 100%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
max-width: 260px;
|
||||||
|
padding: 6px 8px;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: var(--yt-spec-menu-background, #282828);
|
||||||
|
border: 1px solid var(--yt-spec-10-percent-layer, rgba(128, 128, 128, 0.3));
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.4;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ob-tip-title {
|
||||||
|
font-weight: 500;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ob-tip-meta { color: var(--yt-spec-text-secondary, #909090); }
|
||||||
|
|
||||||
|
.ob-legend {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 4px 14px;
|
||||||
|
margin-top: 6px;
|
||||||
|
color: var(--yt-spec-text-secondary, #909090);
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ob-legend-median::before,
|
||||||
|
.ob-legend-self::before {
|
||||||
|
content: "";
|
||||||
|
display: inline-block;
|
||||||
|
width: 10px;
|
||||||
|
height: 2px;
|
||||||
|
margin-right: 5px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ob-legend-median::before { background: #ef4444; }
|
||||||
|
.ob-legend-self::before { background: #f59e0b; height: 8px; }
|
||||||
|
|
||||||
.ob-note {
|
.ob-note {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
|
|||||||
Reference in New Issue
Block a user