Skip to content

[Feature] Add chart for reading statistics (% of papers in new, first pass , read etc)Β #78

@Aatmaj-Zephyr

Description

@Aatmaj-Zephyr
Image

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);

})();

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions