Grouping items by tags in a table

What I’m trying to do

I want to group parent items by tags with their children. The parent item may or may not have have a tag.

Here is a sample list:

  • lists a parent bullet with a tag #sample
    • this is a child bullet
      • this is another child child bullet
  • this is a parent bullet with no tags

Expected Output

image

Things I have tried

I have tried the following code but it has not worked.

TABLE regexreplace(rows.b.text,"#[\w/-]+","") AS Text
WHERE icontains(file.name, this.file.name)
flatten file.lists as b
where b.tags
where !b.task
group by b.tags as Topic

I appreciate any help on this issue! Thank you!

here is the solution :slightly_smiling_face:

```dataviewjs
const dvF = dv.evaluationContext.functions;

// Custom function to recursively process and display child list items
dvF.customListChildren = (children) => {
    let text = "<ul>";
    for (const child of children) {
        text += `<li>${child.text}`;
        if (child.children.length > 0) {
            text += dvF.customListChildren(child.children);
        }
        text += "</li>";
    }
    text += "</ul>";
    return text;
};

// Custom function to process and display parent and child list items
dvF.customListWithParent = (item) => {
    // Remove the tag from the item text using regex
    let text = `<li>${item.text.replace(/#\w+/g, '')}`;
    if (item.children.length > 0) {
        text += dvF.customListChildren(item.children);
    }
    text += "</li>";
    return text;
};

// Grouping parent items by tags for the current page
const currentPage = dv.current();
const grouped = {};
const processedChildren = new Set();

for (const list of currentPage.file.lists) {
    const tag = list.tags.length > 0 ? list.tags[0] : "no tag";
    if (!grouped[tag]) {
        grouped[tag] = [];
    }
    grouped[tag].push(list);

    // Mark children of all parents as processed to avoid duplication in "no tag" section
    const markChildren = (children) => {
        for (const child of children) {
            processedChildren.add(child);
            if (child.children.length > 0) {
                markChildren(child.children);
            }
        }
    };
    markChildren(list.children);
}

// Filter processed children from the "no tag" section
if (grouped["no tag"]) {
    grouped["no tag"] = grouped["no tag"].filter(item => !processedChildren.has(item));
    // Remove "no tag" section if it has no items
    if (grouped["no tag"].length === 0) {
        delete grouped["no tag"];
    }
}

// Ensure "no tag" section is always the last row in the table
const sortedGrouped = Object.entries(grouped).sort(([tagA, itemsA], [tagB, itemsB]) => {
    if (tagA === "no tag") return 1;
    if (tagB === "no tag") return -1;
    return 0;
});

// Displaying the grouped items in a table
dv.table(["Tag", "Items"],
    sortedGrouped.map(([tag, items]) => [
        tag,
        `<ul>${items.map(item => dvF.customListWithParent(item)).join("")}</ul>`
    ])
);

make sure dataviewjs queires is enabled in the dataview plugin settings.

This is the final result :

It took me a long time to code, but I hope it fulfils your request.

1 Like

WOW! Amazing work!! Thank you so much your valuable time and effort. I truly appreciate it. It does fulfill my request. I have modified your code to replace the children items tags while displaying also the regex pattern. For example:

text += `<li>${child.text.replace(/#[\w/-]+/g, '')}`;
let text = `<li>${item.text.replace(/#[\w/-]+/g, '')}`;

However, there are three updates I am working on based on your code which are:

  1. The code treats tasks and lists as the same. However, it should not be the case.
  2. The code also lists empty lists i.e. - with or without tag. This may create redundant information.
  3. To make a version of this code to support querying from other pages specified by frontmatter tags instead of restricted to current page only.

Solving these problems will give a robust solution to this problem. Please feel free to helping out!!

1 Like

I got the number 3 by replacing

const currentPage = dv.current();

with

const currentPage = dv.pages("#source-tag");
1 Like

This is the ultimate solution :dizzy: :

```dataviewjs
const dvF = dv.evaluationContext.functions;

// Custom function to recursively process and display child list items
dvF.customListChildren = (children) => {
    let text = "<ul>";
    for (const child of children) {
        const cleanedChildText = child.text.replace(/#\w+/g, '').trim();
        if (cleanedChildText !== "") { // Exclude empty list items
            text += `<li>${cleanedChildText}`;
            if (child.children.length > 0) {
                text += dvF.customListChildren(child.children);
            }
            text += "</li>";
        }
    }
    text += "</ul>";
    return text;
};

// Custom function to process and display parent and child list items
dvF.customListWithParent = (item) => {
    const cleanedText = item.text.replace(/#\w+/g, '').trim();
    if (cleanedText === "" && item.children.length === 0) return ""; // Exclude empty parent items
    let text = `<li>${cleanedText}`;
    if (item.children.length > 0) {
        text += dvF.customListChildren(item.children);
    }
    text += "</li>";
    return text;
};

// Grouping parent items by tags for the current page
const currentPage = dv.current();
const grouped = {};
const processedChildren = new Set();

for (const list of currentPage.file.lists) {
    // Filter out tasks
    if (!list.task) {
        const cleanedText = list.text.replace(/#\w+/g, '').trim();
        if (cleanedText === "" && list.children.length === 0) continue; // Exclude empty lists
        const tag = list.tags.length > 0 ? list.tags[0] : "no tag";
        if (!grouped[tag]) {
            grouped[tag] = [];
        }
        grouped[tag].push(list);

        // Mark children of all parents as processed to avoid duplication in "no tag" section
        const markChildren = (children) => {
            for (const child of children) {
                processedChildren.add(child);
                if (child.children.length > 0) {
                    markChildren(child.children);
                }
            }
        };
        markChildren(list.children);
    }
}

// Move child bullets with tags to the appropriate tag section
for (const tag in grouped) {
    grouped[tag] = grouped[tag].map(item => {
        if (item.children.length > 0) {
            item.children = item.children.filter(child => {
                const childTag = child.tags.length > 0 ? child.tags[0] : null;
                if (childTag && childTag !== tag) {
                    if (!grouped[childTag]) {
                        grouped[childTag] = [];
                    }
                    grouped[childTag].push(child);
                    return false;
                }
                return true;
            });
        }
        return item;
    });
}

// Remove duplicates from the grouped items
for (const tag in grouped) {
    grouped[tag] = Array.from(new Set(grouped[tag]));
}

// Filter processed children from the "no tag" section
if (grouped["no tag"]) {
    grouped["no tag"] = grouped["no tag"].filter(item => !processedChildren.has(item));
    // Remove "no tag" section if it has no items
    if (grouped["no tag"].length === 0) {
        delete grouped["no tag"];
    }
}

// Ensure "no tag" section is always the last row in the table
const sortedGrouped = Object.entries(grouped).sort(([tagA, itemsA], [tagB, itemsB]) => {
    if (tagA === "no tag") return 1;
    if (tagB === "no tag") return -1;
    return 0;
});

// Displaying the grouped items in a table
dv.table(["Tag", "Items"],
    sortedGrouped.map(([tag, items]) => [
        tag,
        `<ul>${items.map(item => dvF.customListWithParent(item)).join("")}</ul>`
    ])
);

1 Like

This is amazing work!!! :saluting_face::saluting_face::saluting_face::saluting_face::saluting_face::saluting_face::saluting_face: You are a magician! It is a blessing to have people like you in our community. I wish you the best of everything!

I have modified your code to filter the lists by specific tags and etags for current and any other pages. Now my setup is complete!!!

1 Like

Forgive me, but I have stumbled across a small issue with your 2nd solution. This was not present in your first solution. The issue is if the children items have tag with them the parent item is not being displayed with children items. I am posting screenshots to clarify:

Un grouped sample list:
image

From Solution 1

From Solution 2
image

I appreciate your take on this!

1 Like

I have used our buddy ChatGPT to solve this problem I am posting solutions here! I have tried and tested them to the best of my ability and they worked.

Grouping items by filtering with specific tags from the current page

const dvF = dv.evaluationContext.functions;

// Define the specific tags you want to filter by (e.g., "#sample")
const filterTags = ["#sample"]; // Only filter by exact tag

// Custom function to recursively process and display child list items
dvF.customListChildren = (children) => {
    let text = "<ul>";
    for (const child of children) {
        const cleanedChildText = child.text.replace(/#[\w/-]+/g, '').trim();
        if (cleanedChildText !== "") { // Exclude empty list items
            text += `<li>${cleanedChildText}`;
            if (child.children.length > 0) {
                text += dvF.customListChildren(child.children);
            }
            text += "</li>";
        }
    }
    text += "</ul>";
    return text;
};

// Custom function to process and display parent and child list items
dvF.customListWithParent = (item) => {
    const cleanedText = item.text.replace(/#[\w/-]+/g, '').trim();
    if (cleanedText === "" && item.children.length === 0) return ""; // Exclude empty parent items
    let text = `<li>${cleanedText}`;
    if (item.children.length > 0) {
        text += dvF.customListChildren(item.children);
    }
    text += "</li>";
    return text;
};

// Grouping parent items by specific tag (#sample) for the current page
const currentPage = dv.current();
const grouped = {};
const processedChildren = new Set();

for (const list of currentPage.file.lists) {
    // Filter out tasks and only include items that match the specific tag
    if (!list.task) {
        const tag = list.tags.length > 0 ? list.tags[0] : "no tag";
        if (!filterTags.includes(tag)) continue; // Skip items that don't match the specific tag

        const cleanedText = list.text.replace(/#[\w/-]+/g, '').trim();
        if (cleanedText === "" && list.children.length === 0) continue; // Exclude empty lists

        if (!grouped[tag]) {
            grouped[tag] = [];
        }
        grouped[tag].push(list);

        // Mark children of all parents as processed to avoid duplication in "no tag" section
        const markChildren = (children) => {
            for (const child of children) {
                processedChildren.add(child);
                if (child.children.length > 0) {
                    markChildren(child.children);
                }
            }
        };
        markChildren(list.children);
    }
}

// Filter processed children from the "no tag" section
if (grouped["no tag"]) {
    grouped["no tag"] = grouped["no tag"].filter(item => !processedChildren.has(item));
    if (grouped["no tag"].length === 0) {
        delete grouped["no tag"];
    }
}

// Displaying the grouped items in a table
dv.table(["Tag", "Items"],
    Object.entries(grouped).map(([tag, items]) => [
        tag,
        `<ul>${items.map(item => dvF.customListWithParent(item)).join("")}</ul>`
    ])
);

Grouping items by specific nested tags from the current page

const dvF = dv.evaluationContext.functions;

// Define the specific tags you want to filter by, including the base tag for nested tags
const filterTags = ["#sample", "#example"]; // Replace these with the base tags you want to filter by

// Custom function to recursively process and display child list items
dvF.customListChildren = (children) => {
    let text = "<ul>";
    for (const child of children) {
        const cleanedChildText = child.text.replace(/#[\w/-]+/g, '').trim();
        if (cleanedChildText !== "") { // Exclude empty list items
            text += `<li>${cleanedChildText}`;
            if (child.children.length > 0) {
                text += dvF.customListChildren(child.children);
            }
            text += "</li>";
        }
    }
    text += "</ul>";
    return text;
};

// Custom function to process and display parent and child list items
dvF.customListWithParent = (item) => {
    const cleanedText = item.text.replace(/#[\w/-]+/g, '').trim();
    if (cleanedText === "" && item.children.length === 0) return ""; // Exclude empty parent items
    let text = `<li>${cleanedText}`;
    if (item.children.length > 0) {
        text += dvF.customListChildren(item.children);
    }
    text += "</li>";
    return text;
};

// Helper function to check if a tag or nested tag matches any of the filter tags
const matchesFilterTag = (tag) => {
    return filterTags.some(filterTag => tag.startsWith(filterTag)); // Check if any filterTag matches the start of the current tag
};

// Grouping parent items by tags for the current page
const currentPage = dv.current();
const grouped = {};
const processedChildren = new Set();

for (const list of currentPage.file.lists) {
    // Filter out tasks and only include items that match the filter tags or their nested tags
    if (!list.task) {
        const tag = list.tags.length > 0 ? list.tags[0] : "no tag";
        if (!matchesFilterTag(tag)) continue; // Skip items that don't match the filter tags or their nested tags

        const cleanedText = list.text.replace(/#[\w/-]+/g, '').trim();
        if (cleanedText === "" && list.children.length === 0) continue; // Exclude empty lists

        if (!grouped[tag]) {
            grouped[tag] = [];
        }
        grouped[tag].push(list);

        // Mark children of all parents as processed to avoid duplication in "no tag" section
        const markChildren = (children) => {
            for (const child of children) {
                processedChildren.add(child);
                if (child.children.length > 0) {
                    markChildren(child.children);
                }
            }
        };
        markChildren(list.children);
    }
}

// Move child bullets with tags to the appropriate tag section
for (const tag in grouped) {
    grouped[tag] = grouped[tag].map(item => {
        if (item.children.length > 0) {
            item.children = item.children.filter(child => {
                const childTag = child.tags.length > 0 ? child.tags[0] : null;
                if (childTag && childTag !== tag && matchesFilterTag(childTag)) {
                    if (!grouped[childTag]) {
                        grouped[childTag] = [];
                    }
                    grouped[childTag].push(child);
                    return false;
                }
                return true;
            });
        }
        return item;
    });
}

// Remove duplicates from the grouped items
for (const tag in grouped) {
    grouped[tag] = Array.from(new Set(grouped[tag]));
}

// Filter processed children from the "no tag" section
if (grouped["no tag"]) {
    grouped["no tag"] = grouped["no tag"].filter(item => !processedChildren.has(item));
    // Remove "no tag" section if it has no items
    if (grouped["no tag"].length === 0) {
        delete grouped["no tag"];
    }
}

// Ensure "no tag" section is always the last row in the table
const sortedGrouped = Object.entries(grouped).sort(([tagA, itemsA], [tagB, itemsB]) => {
    if (tagA === "no tag") return 1;
    if (tagB === "no tag") return -1;
    return 0;
});

// Displaying the grouped items in a table
dv.table(["Tag", "Items"],
    sortedGrouped.map(([tag, items]) => [
        tag,
        `<ul>${items.map(item => dvF.customListWithParent(item)).join("")}</ul>`
    ])
);