This can be added to the panel when then collection is clicked (Currently it shows number of items)
Below is a (very hacky) code for the above implementation.
const Zotero = require("Zotero");
const ZoteroPane = require("ZoteroPane");
const window = require("window");
const document = require("document");
(async () => {
// Hide group box if exists
const group_box = document.getElementById("zotero-item-pane-groupbox");
if (group_box) group_box.style.display = "none";
// Get pane
const pane = document.getElementById("zotero-item-message");
if (!pane) throw new Error("Pane not found");
// Load Chart.js and plugin if not loaded
async function loadChartJs() {
if (typeof window.Chart === "undefined") {
const res = await fetch("https://cdn.jsdelivr.net/npm/chart.js");
window.eval(await res.text());
const pluginRes = await fetch("https://cdn.jsdelivr.net/npm/chartjs-plugin-datalabels@2");
window.eval(await pluginRes.text());
}
}
await loadChartJs();
// Create canvas if it doesn't exist
let canvas = document.getElementById("readStatusChart");
if (!canvas) {
canvas = document.createElement("canvas");
canvas.id = "readStatusChart";
canvas.style.width = "100%";
canvas.style.height = "300px";
canvas.style.display = "block";
canvas.style.padding = "4px";
pane.appendChild(canvas);
}
// Create table container if it doesn't exist
let tableContainer = document.getElementById("readStatusTable");
if (!tableContainer) {
tableContainer = document.createElement("div");
tableContainer.id = "readStatusTable";
tableContainer.style.marginTop = "12px";
pane.appendChild(tableContainer);
}
// Define statuses with symbol, name, and meaning
const statusInfo = [
{ symbol: "β", name: "New", meaning: "Newly added paper (not read)" },
{ symbol: "π", name: "First pass", meaning: "Marked for second pass" },
{ symbol: "π", name: "Second pass", meaning: "Marked for third pass" },
{ symbol: "π", name: "Finished", meaning: "Finished reading relevant passes" },
{ symbol: "β", name: "Not Relevant", meaning: "Not relevant. Added in the list for completeness" },
{ symbol: "π", name: "Paused", meaning: "Paused due to unclear" },
{ symbol: "π", name: "Reading", meaning: "Reading currently" }
];
let lastLibraryID = null;
let lastCollectionID = null;
async function updateChart() {
const libID = ZoteroPane.getSelectedLibraryID();
const collection = ZoteroPane.getSelectedCollection();
let itemIDs = [];
if (collection) {
// If a collection or subcollection is selected
const search = new Zotero.Search();
search.libraryID = libID;
search.addCondition('collectionID', 'is', collection.id);
itemIDs = await search.search();
} else if (libID) {
// If no collection is selected, get all items in library
const search = new Zotero.Search();
search.libraryID = libID;
itemIDs = await search.search();
} else {
// Fallback: selected items in the current view
itemIDs = ZoteroPane.getSelectedItems();
}
if (!itemIDs || itemIDs.length === 0) {
// Clear chart and table if nothing found
if (canvas.chartInstance) canvas.chartInstance.data.datasets[0].data.fill(0);
if (canvas.chartInstance) canvas.chartInstance.update();
tableContainer.innerHTML = "<p style='text-align:center;color:#777;'>No items found in this view.</p>";
return;
}
const items = await Zotero.Items.getAsync(itemIDs);
const statusCounts = {};
for (const info of statusInfo) statusCounts[info.name] = 0;
statusCounts.Unknown = 0;
for (const item of items) {
try {
const extra = item.getField('extra') || '';
const match = extra.match(/Read_Status:\s*(.+)/i);
if (match) {
const val = match[1].trim();
statusCounts.hasOwnProperty(val) ? statusCounts[val]++ : statusCounts.Unknown++;
} else {
statusCounts.Unknown++;
}
} catch {
statusCounts.Unknown++;
}
}
// Update chart
const dataValues = statusInfo.map(info => statusCounts[info.name] || 0);
if (canvas.chartInstance) {
canvas.chartInstance.data.datasets[0].data = dataValues;
canvas.chartInstance.update();
} else {
const ctx = canvas.getContext("2d");
canvas.chartInstance = new window.Chart(ctx, {
type: "pie",
data: {
labels: statusInfo.map(info => info.symbol),
datasets: [{
data: dataValues,
backgroundColor: ["#ff7593", "#FFD966", "#E86A2A", "#4CAF50", "#ffd9d9", "#1E88E5", "#9C27B0"],
borderColor: "#ffffff",
borderWidth: 2
}]
},
options: {
responsive: true,
plugins: {
datalabels: {
color: "#ffffff",
font: { weight: "bold", size: 14 },
formatter: (value, context) => {
if (value === 0) return null;
const total = context.chart._metasets[0].total;
const pct = ((value / total) * 100).toFixed(1);
if (pct === "0.0") return null;
return pct + "%";
}
},
tooltip: { enabled: true } // plugin context tooltips disabled
}
},
plugins: [window.ChartDataLabels]
});
}
// Update table
tableContainer.innerHTML = "";
const table = document.createElement("table");
table.style.borderCollapse = "separate";
table.style.borderSpacing = "0";
table.style.width = "100%";
table.style.fontFamily = "Arial, sans-serif";
table.style.fontSize = "14px";
table.style.boxShadow = "0 2px 5px rgba(0,0,0,0.05)";
table.style.borderRadius = "8px";
table.style.overflow = "hidden";
// Header
const header = document.createElement("tr");
["Symbol", "Status", "Meaning", "Count"].forEach(text => {
const th = document.createElement("th");
th.textContent = text;
th.style.background = "#f0f0f0";
th.style.padding = "10px 12px";
th.style.textAlign = "left";
th.style.fontWeight = "600";
th.style.borderBottom = "2px solid #ddd";
header.appendChild(th);
});
table.appendChild(header);
// Rows
for (let i = 0; i < statusInfo.length; i++) {
const info = statusInfo[i];
const row = document.createElement("tr");
row.style.background = i % 2 === 0 ? "#ffffff" : "#f9f9f9";
row.style.transition = "background 0.3s";
row.addEventListener("mouseover", () => row.style.background = "#e8f0ff");
row.addEventListener("mouseout", () => row.style.background = i % 2 === 0 ? "#ffffff" : "#f9f9f9");
const cellSymbol = document.createElement("td");
cellSymbol.textContent = info.symbol;
cellSymbol.style.padding = "10px 12px";
const cellName = document.createElement("td");
cellName.textContent = info.name;
cellName.style.padding = "10px 12px";
const cellMeaning = document.createElement("td");
cellMeaning.textContent = info.meaning;
cellMeaning.style.padding = "10px 12px";
const cellCount = document.createElement("td");
cellCount.textContent = statusCounts[info.name] || 0;
cellCount.style.padding = "10px 12px";
cellCount.style.textAlign = "right";
cellCount.style.fontWeight = "500";
row.appendChild(cellSymbol);
row.appendChild(cellName);
row.appendChild(cellMeaning);
row.appendChild(cellCount);
table.appendChild(row);
}
tableContainer.appendChild(table);
}
// Initial render
await updateChart();
// Polling updates for auto-refresh on collection/library switch
setInterval(updateChart, 1000);
})();
This can be added to the panel when then collection is clicked (Currently it shows number of items)
Below is a (very hacky) code for the above implementation.