Fix top skills history chart hiding skills with zero-level gaps.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1072,6 +1072,14 @@ def skill_timeline(db_path: Path | str = DEFAULT_DB) -> dict[str, Any]:
|
|||||||
if idx is not None:
|
if idx is not None:
|
||||||
series[key][idx] = row["level"]
|
series[key][idx] = row["level"]
|
||||||
|
|
||||||
|
for key in series:
|
||||||
|
last = 0
|
||||||
|
for i in range(n):
|
||||||
|
if series[key][i] > 0:
|
||||||
|
last = series[key][i]
|
||||||
|
elif last > 0:
|
||||||
|
series[key][i] = last
|
||||||
|
|
||||||
return {"snapshots": snapshots, "series": series}
|
return {"snapshots": snapshots, "series": series}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+57
-10
@@ -748,6 +748,7 @@ function skillLevelSeries(skillKey) {
|
|||||||
if (!tl?.series) return null;
|
if (!tl?.series) return null;
|
||||||
const values = tl.series[skillKey];
|
const values = tl.series[skillKey];
|
||||||
if (!values || values.length < 2) return null;
|
if (!values || values.length < 2) return null;
|
||||||
|
if (values.filter((v) => v > 0).length < 2) return null;
|
||||||
return values;
|
return values;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -813,7 +814,7 @@ function setupTrendChartModal() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function openTrendChartModal(title, snapshots, values, datasetLabel, color = "#4ade80") {
|
function openTrendChartModal(title, snapshots, values, datasetLabel, color = "#4ade80", skillSeries = false) {
|
||||||
setupTrendChartModal();
|
setupTrendChartModal();
|
||||||
const modal = document.getElementById("inv-chart-modal");
|
const modal = document.getElementById("inv-chart-modal");
|
||||||
document.getElementById("inv-chart-modal-title").textContent = title;
|
document.getElementById("inv-chart-modal-title").textContent = title;
|
||||||
@@ -822,16 +823,20 @@ function openTrendChartModal(title, snapshots, values, datasetLabel, color = "#4
|
|||||||
|
|
||||||
destroyChart("trendModal");
|
destroyChart("trendModal");
|
||||||
const bg = color === "#6c8cff" ? "rgba(108, 140, 255, 0.12)" : "rgba(74, 222, 128, 0.12)";
|
const bg = color === "#6c8cff" ? "rgba(108, 140, 255, 0.12)" : "rgba(74, 222, 128, 0.12)";
|
||||||
|
const points = skillSeries
|
||||||
|
? skillTimelineChartPoints(snapshots, values)
|
||||||
|
: timelineChartPoints(snapshots, values);
|
||||||
state.charts.trendModal = new Chart(document.getElementById("inv-chart-modal-canvas"), {
|
state.charts.trendModal = new Chart(document.getElementById("inv-chart-modal-canvas"), {
|
||||||
type: "line",
|
type: "line",
|
||||||
data: {
|
data: {
|
||||||
datasets: [{
|
datasets: [{
|
||||||
label: datasetLabel,
|
label: datasetLabel,
|
||||||
data: timelineChartPoints(snapshots, values),
|
data: points,
|
||||||
borderColor: color,
|
borderColor: color,
|
||||||
backgroundColor: bg,
|
backgroundColor: bg,
|
||||||
tension: 0.3,
|
tension: skillSeries ? 0 : 0.3,
|
||||||
fill: true,
|
fill: true,
|
||||||
|
spanGaps: false,
|
||||||
}],
|
}],
|
||||||
},
|
},
|
||||||
options: chartOptsTime(snapshots),
|
options: chartOptsTime(snapshots),
|
||||||
@@ -849,7 +854,7 @@ function openSkillChartModal(skillKey, skillName) {
|
|||||||
const values = skillLevelSeries(skillKey);
|
const values = skillLevelSeries(skillKey);
|
||||||
const tl = state.skillTimeline;
|
const tl = state.skillTimeline;
|
||||||
if (!values || !tl) return;
|
if (!values || !tl) return;
|
||||||
openTrendChartModal(skillName, tl.snapshots, values, t("skills.level"));
|
openTrendChartModal(skillName, tl.snapshots, values, t("skills.level"), "#4ade80", true);
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeTrendChartModal() {
|
function closeTrendChartModal() {
|
||||||
@@ -1736,12 +1741,10 @@ function renderTimelineCharts() {
|
|||||||
const skillCanvas = document.getElementById("chart-skills");
|
const skillCanvas = document.getElementById("chart-skills");
|
||||||
if (!skillCanvas || !skillTl?.snapshots?.length) return;
|
if (!skillCanvas || !skillTl?.snapshots?.length) return;
|
||||||
|
|
||||||
const skillEntries = Object.entries(skillTl.series || {})
|
const skillEntries = pickTopSkillEntries(skillTl.series, 5);
|
||||||
.map(([key, values]) => ({ key, values, latest: values[values.length - 1] || 0 }))
|
|
||||||
.sort((a, b) => b.latest - a.latest)
|
|
||||||
.slice(0, 5);
|
|
||||||
|
|
||||||
const colors = ["#6c8cff", "#4ade80", "#fbbf24", "#f87171", "#a78bfa"];
|
const colors = ["#6c8cff", "#4ade80", "#fbbf24", "#f87171", "#a78bfa"];
|
||||||
|
const dashPatterns = [[], [6, 4], [2, 3], [8, 4, 2, 4], [4, 2]];
|
||||||
const skillName = (key) => {
|
const skillName = (key) => {
|
||||||
const sk = state.data?.skills?.find((s) => s.key === key);
|
const sk = state.data?.skills?.find((s) => s.key === key);
|
||||||
return sk?.name || key.replace(/_/g, " ");
|
return sk?.name || key.replace(/_/g, " ");
|
||||||
@@ -1752,10 +1755,15 @@ function renderTimelineCharts() {
|
|||||||
data: {
|
data: {
|
||||||
datasets: skillEntries.map((entry, idx) => ({
|
datasets: skillEntries.map((entry, idx) => ({
|
||||||
label: skillName(entry.key),
|
label: skillName(entry.key),
|
||||||
data: timelineChartPoints(skillTl.snapshots, entry.values),
|
data: skillTimelineChartPoints(skillTl.snapshots, entry.values),
|
||||||
borderColor: colors[idx % colors.length],
|
borderColor: colors[idx % colors.length],
|
||||||
tension: 0.3,
|
borderWidth: 2,
|
||||||
|
borderDash: dashPatterns[idx % dashPatterns.length],
|
||||||
|
pointRadius: 4,
|
||||||
|
pointHoverRadius: 6,
|
||||||
|
tension: 0,
|
||||||
fill: false,
|
fill: false,
|
||||||
|
spanGaps: false,
|
||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
options: chartOptsTime(skillTl.snapshots),
|
options: chartOptsTime(skillTl.snapshots),
|
||||||
@@ -1779,6 +1787,45 @@ function snapshotTimeMs(snapshot) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function skillLevelDelta(values) {
|
||||||
|
const valid = values.filter((v) => v > 0);
|
||||||
|
if (valid.length < 2) return 0;
|
||||||
|
return Math.max(...valid) - Math.min(...valid);
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickTopSkillEntries(series, limit = 5) {
|
||||||
|
const ranked = Object.entries(series || {})
|
||||||
|
.map(([key, values]) => ({
|
||||||
|
key,
|
||||||
|
values,
|
||||||
|
latest: values[values.length - 1] || 0,
|
||||||
|
delta: skillLevelDelta(values),
|
||||||
|
}))
|
||||||
|
.filter((e) => e.delta > 0)
|
||||||
|
.sort((a, b) => b.delta - a.delta || b.latest - a.latest);
|
||||||
|
|
||||||
|
const seen = new Set();
|
||||||
|
const picked = [];
|
||||||
|
for (const entry of ranked) {
|
||||||
|
const sig = entry.values.join(",");
|
||||||
|
if (seen.has(sig)) continue;
|
||||||
|
seen.add(sig);
|
||||||
|
picked.push(entry);
|
||||||
|
if (picked.length >= limit) break;
|
||||||
|
}
|
||||||
|
return picked;
|
||||||
|
}
|
||||||
|
|
||||||
|
function skillTimelineChartPoints(snapshots, values) {
|
||||||
|
return snapshots.map((snapshot, i) => {
|
||||||
|
const x = snapshotTimeMs(snapshot);
|
||||||
|
if (x == null) return null;
|
||||||
|
const y = values[i];
|
||||||
|
if (y == null || y <= 0) return null;
|
||||||
|
return { x, y };
|
||||||
|
}).filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
function timelineChartPoints(snapshots, values) {
|
function timelineChartPoints(snapshots, values) {
|
||||||
return snapshots.map((snapshot, i) => {
|
return snapshots.map((snapshot, i) => {
|
||||||
const x = snapshotTimeMs(snapshot);
|
const x = snapshotTimeMs(snapshot);
|
||||||
|
|||||||
Reference in New Issue
Block a user