From 27f3b25ce7298d6531172bf9556ff957588033c2 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 19 Jul 2026 21:07:37 +0000 Subject: [PATCH] Collapse taxonomy into a single grouped Taxon facet Replace the separate Kingdom and Phylum/Division facets with one `taxon` facet whose values are phyla/divisions, grouped under their kingdom (Animalia / Plantae / Fungi) in _data/tags.yml. Entries are tagged at phylum/division level; the kingdom is implied by the grouping. - Filter UI renders Taxon as one dropdown with kingdom sub-headings, each a select-all toggle (indeterminate when partially selected) - Entry front-matter, methods.json, and the method issue form now use a single taxon field / dropdown instead of two - validate_entries.rb flattens the taxon map to check phylum-level values - Only animals, plants and fungi are represented - Docs (SPEC, CONTRIBUTING, README) updated Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01HYAEmscFHjiQFP1sqNEBEF --- .github/ISSUE_TEMPLATE/suggest-method.yml | 18 ++++---- CONTRIBUTING.md | 11 +++-- README.md | 6 +-- SPEC.md | 25 ++++++----- _data/facets.yml | 13 ++---- _data/tags.yml | 48 +++++++++++----------- _methods/genome-skimming-pinned-beetle.md | 3 +- _methods/herbarium-plant-target-capture.md | 3 +- _methods/study-skin-bird-mitogenome.md | 3 +- assets/css/style.css | 4 ++ assets/js/app.js | 43 +++++++++++++++++++ methods.json | 3 +- scripts/validate_entries.rb | 18 +++++--- 13 files changed, 123 insertions(+), 75 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/suggest-method.yml b/.github/ISSUE_TEMPLATE/suggest-method.yml index 8a6515c..3bf45a2 100644 --- a/.github/ISSUE_TEMPLATE/suggest-method.yml +++ b/.github/ISSUE_TEMPLATE/suggest-method.yml @@ -25,19 +25,15 @@ body: validations: required: true - type: dropdown - id: kingdom + id: taxon attributes: - label: Kingdom* - multiple: true - options: [Animalia, Plantae, Fungi, Chromista, Bacteria, Archaea] - validations: - required: true - - type: dropdown - id: phylum - attributes: - label: Phylum / Division* + label: Taxon — phylum / division* + description: >- + Pick the phylum/division(s). Grouped by kingdom for reference (Animalia, + Plantae, Fungi). Choose "Other" if yours isn't listed and describe it below. multiple: true options: + # Animalia - Chordata - Arthropoda - Mollusca @@ -47,8 +43,10 @@ body: - Nematoda - Platyhelminthes - Porifera + # Plantae - Tracheophyta - Bryophyta + # Fungi - Ascomycota - Basidiomycota - "Other (describe in detail box)" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index dcfd8cf..00e46c6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -20,8 +20,7 @@ Add one Markdown file per entry, then open a PR. CI validates it automatically. ```yaml --- title: "Genome skimming from a 40-year-old pinned beetle" -kingdom: [Animalia] # one or more, from _data/tags.yml -phylum: [Arthropoda] # "Phylum / Division", from _data/tags.yml +taxon: [Arthropoda] # phylum/division, from _data/tags.yml (kingdom is implied) preservation: [dry-pinned] extraction: [silica-column] library: [tagmentation] @@ -37,10 +36,14 @@ Full detail in Markdown. Renders when the card is expanded: protocol notes, yields, caveats, links to SOPs in the bibliography, etc. ``` -Required: `title`, `kingdom`, `phylum`, `preservation`, `extraction`, -`library`, `sequencing`, `summary`, `contributor`, `date`. +Required: `title`, `taxon`, `preservation`, `extraction`, `library`, +`sequencing`, `summary`, `contributor`, `date`. Optional: `outcome`, `doi`, body. +`taxon` values are phyla/divisions, grouped under their kingdom in +`_data/tags.yml`; the kingdom is inferred from that grouping (we represent +animals, plants and fungi). + ### Bibliography entry — `_bibliography/.md` ```yaml diff --git a/README.md b/README.md index a3b821a..7cff257 100644 --- a/README.md +++ b/README.md @@ -6,9 +6,9 @@ types, backed by a curated bibliography of supporting papers and SOPs. 🌐 **Site:** https://museomics.github.io/methods/ -- **Methods** (`/`) — filter working methods by kingdom, phylum/division, - preservation, extraction, library prep, and sequencing. Cards expand to full - protocol notes. +- **Methods** (`/`) — filter working methods by taxon (phylum/division grouped + under kingdom), preservation, extraction, library prep, and sequencing. Cards + expand to full protocol notes. - **Bibliography** (`/bibliography/`) — papers and SOPs, filterable by type and method tag. - **Contribute** (`/contribute/`) — suggest an entry via issue form, or by PR. diff --git a/SPEC.md b/SPEC.md index 0326ad7..0160a58 100644 --- a/SPEC.md +++ b/SPEC.md @@ -1,8 +1,10 @@ # museomics/methods — Build Spec & Implementation Plan (revised) > Revision note: this supersedes the original draft. Changes from the review: -> two-level taxonomy (Kingdom + Phylum/Division) replacing a single taxon -> facet; GitHub Actions build instead of stock GitHub Pages; a single +> a single Taxon facet whose values are phyla/divisions grouped under kingdom +> (Animalia / Plantae / Fungi), presented as one dropdown with per-kingdom +> select-all — replacing the original flat taxon tag; GitHub Actions build +> instead of stock GitHub Pages; a single > canonical controlled vocabulary enforced by CI; free-text search; sort > control; empty state; accessible chips (not colour-alone); optional DOI; > optional outcome signal; separate content licence (CC BY 4.0). @@ -53,16 +55,18 @@ Nav bar on every page: **Methods | Bibliography | Contribute** ## 1. Methods page (`/`) -Let visitors find and filter working methods by **kingdom**, **phylum/division**, -preservation, extraction, library prep, and sequencing/downstream method. -Multi-tagging per entry is supported on every facet. +Let visitors find and filter working methods by **taxon** (phylum/division, +grouped under kingdom — Animalia / Plantae / Fungi), preservation, extraction, +library prep, and sequencing/downstream method. Multi-tagging per entry is +supported on every facet. ### Layout (top to bottom) 1. Header — title + one-line purpose. -2. Tag legend — collapsible explainer of the six facets and their chip +2. Tag legend — collapsible explainer of the five facets and their chip colours/badges. 3. Controls — free-text search box + sort control (newest / oldest / A–Z), then - a filter bar of six collapsible multi-select facets, populated from + a filter bar of five collapsible multi-select facets (Taxon is one grouped + dropdown), populated from `_data/tags.yml`. OR within a facet, AND across facets. 4. Results count (live) + "Clear all filters". 5. Card grid. @@ -80,8 +84,7 @@ Multi-tagging per entry is supported on every facet. ```yaml --- title: "Genome skimming from a 40-year-old pinned beetle" -kingdom: [Animalia] -phylum: [Arthropoda] +taxon: [Arthropoda] # phylum/division; kingdom implied by grouping preservation: [dry-pinned] extraction: [silica-column] library: [tagmentation] @@ -94,7 +97,7 @@ doi: "10.xxxx/xxxxx" # OPTIONAL — omit for unpublished methods --- Full detail body as Markdown (renders on expand). ``` -Required: title, kingdom, phylum, preservation, extraction, library, +Required: title, taxon, preservation, extraction, library, sequencing, summary, contributor, date. DOI is optional so unpublished / in-house methods are not excluded. @@ -108,7 +111,7 @@ in-house methods are not excluded. (`window.SITE`) so JS never guesses the baseurl and filter options come from the config, not just from values present in the data. - Active filters, search text and non-default sort are reflected in the URL - query string (`?kingdom=Animalia&preservation=dry-pinned&q=beetle`) and + query string (`?taxon=Arthropoda&preservation=dry-pinned&q=beetle`) and restored on load. --- diff --git a/_data/facets.yml b/_data/facets.yml index acc0413..f5e6f76 100644 --- a/_data/facets.yml +++ b/_data/facets.yml @@ -3,18 +3,13 @@ # `abbr` is shown as a small badge on each chip so facets are distinguishable # without relying on colour alone (accessibility). # `methods` / `bibliography` flag which page a facet applies to. -- key: kingdom - label: Kingdom - abbr: K +- key: taxon + label: Taxon + abbr: Tx color: green methods: true bibliography: false -- key: phylum - label: Phylum / Division - abbr: P - color: teal - methods: true - bibliography: false + grouped: true # values come from a kingdom -> [phyla] map in tags.yml - key: preservation label: Preservation abbr: Pr diff --git a/_data/tags.yml b/_data/tags.yml index d458220..e5f704c 100644 --- a/_data/tags.yml +++ b/_data/tags.yml @@ -14,31 +14,29 @@ # except taxonomy, which uses conventional capitalised names. # ----------------------------------------------------------------------------- -# Taxonomy is split into two levels so visitors can filter broadly (Kingdom) or -# more specifically (Phylum / Division). Curated by the maintainer; extend as -# real contributions arrive rather than pre-populating exhaustively. -kingdom: - - Animalia - - Plantae - - Fungi - - Chromista - - Bacteria - - Archaea - -phylum: # "Phylum / Division" — divisions are the botanical equivalent - - Chordata - - Arthropoda - - Mollusca - - Annelida - - Echinodermata - - Cnidaria - - Nematoda - - Platyhelminthes - - Porifera - - Tracheophyta # vascular plants - - Bryophyta # mosses - - Ascomycota - - Basidiomycota +# Taxonomy: a single facet whose values are phyla / divisions, grouped under +# their kingdom. Entries are tagged at phylum/division level (e.g. taxon: +# [Arthropoda]); the kingdom is implied by the grouping below. In the filter UI +# these render as one dropdown with kingdom sub-headings, and each kingdom +# heading is a "select all" toggle. We only represent animals, plants and fungi. +# Curated by the maintainer; extend as real contributions arrive. +taxon: + Animalia: + - Chordata + - Arthropoda + - Mollusca + - Annelida + - Echinodermata + - Cnidaria + - Nematoda + - Platyhelminthes + - Porifera + Plantae: + - Tracheophyta # vascular plants + - Bryophyta # mosses + Fungi: + - Ascomycota + - Basidiomycota preservation: - dry-pinned diff --git a/_methods/genome-skimming-pinned-beetle.md b/_methods/genome-skimming-pinned-beetle.md index 06aaee2..2a5734d 100644 --- a/_methods/genome-skimming-pinned-beetle.md +++ b/_methods/genome-skimming-pinned-beetle.md @@ -1,7 +1,6 @@ --- title: "Genome skimming from a 40-year-old pinned beetle" -kingdom: [Animalia] -phylum: [Arthropoda] +taxon: [Arthropoda] preservation: [dry-pinned] extraction: [silica-column] library: [tagmentation] diff --git a/_methods/herbarium-plant-target-capture.md b/_methods/herbarium-plant-target-capture.md index 1870eb6..3967975 100644 --- a/_methods/herbarium-plant-target-capture.md +++ b/_methods/herbarium-plant-target-capture.md @@ -1,7 +1,6 @@ --- title: "Target capture from herbarium sheets using CTAB extraction" -kingdom: [Plantae] -phylum: [Tracheophyta] +taxon: [Tracheophyta] preservation: [herbarium-pressed] extraction: [ctab] library: [hybridization-capture] diff --git a/_methods/study-skin-bird-mitogenome.md b/_methods/study-skin-bird-mitogenome.md index 72b2fd6..260d0d4 100644 --- a/_methods/study-skin-bird-mitogenome.md +++ b/_methods/study-skin-bird-mitogenome.md @@ -1,7 +1,6 @@ --- title: "Mitogenome from a historical bird study skin toe pad" -kingdom: [Animalia] -phylum: [Chordata] +taxon: [Chordata] preservation: [study-skin] extraction: [magnetic-bead] library: [single-stranded] diff --git a/assets/css/style.css b/assets/css/style.css index 607da6b..08fb4a1 100644 --- a/assets/css/style.css +++ b/assets/css/style.css @@ -98,6 +98,10 @@ main.container { padding-top: 1.6rem; padding-bottom: 3rem; } .facet-opts { padding: .5rem .7rem; max-height: 240px; overflow-y: auto; } .opt { display: flex; align-items: center; gap: .45rem; padding: .15rem 0; cursor: pointer; } .opt input { margin: 0; } +.facet-subgroup { margin-bottom: .4rem; } +.facet-subgroup + .facet-subgroup { border-top: 1px solid var(--border); padding-top: .4rem; } +.opt--group { font-size: .95rem; } +.opt--child { padding-left: 1.3rem; } .controls-foot { display: flex; align-items: center; gap: 1rem; margin-top: .9rem; } .results-count { margin: 0; font-weight: 600; } diff --git a/assets/js/app.js b/assets/js/app.js index b75f93a..a4aa84d 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -229,6 +229,10 @@ } CONFIG.facets.forEach(function (f) { + if (f.grouped) { + bar.appendChild(groupedFacet(f)); + return; + } var vocab = SITE.vocab[f.key] || []; bar.appendChild(facetGroup(f, vocab, filters[f.key], function (val, on) { toggleIn(filters[f.key], val, on); update(); @@ -236,6 +240,45 @@ }); } + // A grouped facet (e.g. Taxon): one dropdown whose options are nested under + // group headings (kingdoms). Each heading is a select-all toggle for its + // children (phyla/divisions). Only the children (leaf values) are stored in + // the filter state. + function groupedFacet(facet) { + var map = SITE.vocab[facet.key] || {}; + var selectedRef = filters[facet.key]; + + var groups = Object.keys(map).map(function (groupName) { + var children = map[groupName]; + var selectedKids = children.filter(function (c) { return selectedRef.indexOf(c) !== -1; }); + + var head = el("input", { type: "checkbox" }); + head.checked = selectedKids.length === children.length && children.length > 0; + head.indeterminate = selectedKids.length > 0 && selectedKids.length < children.length; + head.addEventListener("change", function () { + children.forEach(function (c) { toggleIn(selectedRef, c, head.checked); }); + update(); + }); + var heading = el("label", { class: "opt opt--group" }, [head, el("strong", { text: groupName })]); + + var kids = children.map(function (c) { + var box = el("input", { type: "checkbox", value: c }); + if (selectedRef.indexOf(c) !== -1) box.checked = true; + box.addEventListener("change", function () { toggleIn(selectedRef, c, box.checked); update(); }); + return el("label", { class: "opt opt--child" }, [box, el("span", { text: c })]); + }); + + return el("div", { class: "facet-subgroup" }, [heading].concat(kids)); + }); + + var details = el("details", { class: "facet facet--" + facet.color }); + if (selectedRef.length) details.open = true; + var count = selectedRef.length ? " (" + selectedRef.length + ")" : ""; + details.appendChild(el("summary", { text: facet.label + count })); + details.appendChild(el("div", { class: "facet-opts" }, groups)); + return details; + } + function facetGroup(facet, values, selectedRef, onToggle, labelFn) { var options = values.map(function (v) { var checked = selectedRef.indexOf(v) !== -1; diff --git a/methods.json b/methods.json index c697b08..d9981e0 100644 --- a/methods.json +++ b/methods.json @@ -13,8 +13,7 @@ sitemap: false "contributor": {{ m.contributor | jsonify }}, "date": {{ m.date | date: "%Y-%m-%d" | jsonify }}, "outcome": {{ m.outcome | jsonify }}, - "kingdom": {{ m.kingdom | jsonify }}, - "phylum": {{ m.phylum | jsonify }}, + "taxon": {{ m.taxon | jsonify }}, "preservation": {{ m.preservation | jsonify }}, "extraction": {{ m.extraction | jsonify }}, "library": {{ m.library | jsonify }}, diff --git a/scripts/validate_entries.rb b/scripts/validate_entries.rb index c6a088e..3e5c4c0 100644 --- a/scripts/validate_entries.rb +++ b/scripts/validate_entries.rb @@ -14,14 +14,18 @@ errors = [] -# Front-matter fields on method entries that must be drawn from a vocab list. -METHOD_FACETS = %w[kingdom phylum preservation extraction library sequencing].freeze -METHOD_REQUIRED = (METHOD_FACETS + %w[title summary contributor date]).freeze +# `taxon` is a kingdom -> [phyla] map; entries are tagged at phylum level, so +# the allowed values are the flattened list of phyla/divisions. +TAXON_VALUES = TAGS["taxon"].values.flatten.freeze + +# Flat-list facets on method entries that must be drawn from a vocab list. +METHOD_FLAT_FACETS = %w[preservation extraction library sequencing].freeze +METHOD_REQUIRED = (%w[taxon] + METHOD_FLAT_FACETS + %w[title summary contributor date]).freeze BIB_REQUIRED = %w[type title authors_or_institution year_or_date link].freeze # Any tag on a bibliography entry must exist in one of these facet lists. BIB_TAG_VOCAB = (TAGS["extraction"] + TAGS["library"] + TAGS["sequencing"] + - TAGS["preservation"] + TAGS["kingdom"] + TAGS["phylum"]).uniq.freeze + TAGS["preservation"] + TAXON_VALUES).uniq.freeze def front_matter(path) raw = File.read(path, encoding: "UTF-8") @@ -53,7 +57,11 @@ def as_array(v) errors << "#{rel}: missing required field `#{field}`" if val.nil? || (val.respond_to?(:empty?) && val.empty?) end - METHOD_FACETS.each do |facet| + as_array(fm["taxon"]).each do |v| + errors << "#{rel}: `taxon` value \"#{v}\" not in _data/tags.yml" unless TAXON_VALUES.include?(v) + end + + METHOD_FLAT_FACETS.each do |facet| allowed = TAGS[facet] || [] as_array(fm[facet]).each do |v| errors << "#{rel}: `#{facet}` value \"#{v}\" not in _data/tags.yml" unless allowed.include?(v)