Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions _fleet/data/mech_stats.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
{
"as_of": "2026-09-16",
"merged_prs_total": 2778,
"mechs": [
{
"mech": "CultureMech",
"repo": "CultureMech",
"records": 6286,
"reviewed": null,
"status_field": null,
"merged_prs": 250
},
{
"mech": "MediaIngredientMech",
"repo": "MediaIngredientMech",
"records": 2957,
"reviewed": null,
"status_field": null,
"merged_prs": 359
},
{
"mech": "CommunityMech",
"repo": "CommunityMech",
"records": 364,
"reviewed": null,
"status_field": null,
"merged_prs": 528
},
{
"mech": "TraitMech",
"repo": "TraitMech",
"records": 618,
"reviewed": 427,
"status_field": "mapping_status",
"merged_prs": 517
},
{
"mech": "ProteinTraitsMech",
"repo": "proteintraitsmech",
"records": 429291,
"reviewed": 39591,
"status_field": "mapping_status",
"merged_prs": 314
},
{
"mech": "AntibioticMech",
"repo": "AntibioticMech",
"records": 2939,
"reviewed": 80,
"status_field": "curation_status",
"merged_prs": 160
},
{
"mech": "CellStructureMech",
"repo": "CellStructureMech",
"records": 338,
"reviewed": 0,
"status_field": "mapping_status",
"merged_prs": 425
},
{
"mech": "HabitatMech",
"repo": "HabitatMech",
"records": 3206,
"reviewed": 684,
"status_field": "mapping_status",
"merged_prs": 130
},
{
"mech": "NaturalProductMech",
"repo": "NaturalProductMech",
"records": 3115,
"reviewed": 0,
"status_field": "curation_status",
"merged_prs": 80
},
{
"mech": "TaxonMech",
"repo": "TaxonMech",
"records": 625960,
"reviewed": 0,
"status_field": "mapping_status",
"merged_prs": 15
}
]
}
65 changes: 62 additions & 3 deletions _fleet/fleet_fragment.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
identify the nodes without relying on color alone. */
--mech-naturalproductmech: #0b5fa5; --mech-taxonmech: #5257C9;
--mech-mediaingredientmech: #7E5BC4; --mech-culturemech: #4B9E5F;
--sphere-hi: .42; --sphere-hi-mid: .12; --sphere-lo-mid: .12; --sphere-lo: .21;
--fleet-edge: rgba(90, 99, 94, .34);
--fleet-heat-hue: 150 22%;
--voc-chebi: #D9702F; --voc-ncbitaxon: #3B7DD8; --voc-go: #2FA36B; --voc-metpo: #7A5BC7;
Expand All @@ -19,6 +20,7 @@
--mech-cellstructuremech: #8a8ef5; --mech-proteintraitsmech: #3dbfb2; --mech-antibioticmech: #d9a94a;
--mech-naturalproductmech: #79b8f3; --mech-taxonmech: #8b8fe8;
--mech-mediaingredientmech: #b08cf2; --mech-culturemech: #63c46f;
--sphere-hi: .34; --sphere-hi-mid: .09; --sphere-lo-mid: .26; --sphere-lo: .44;
--fleet-edge: rgba(160, 178, 168, .34);
--fleet-heat-hue: 150 14%;
--voc-chebi: #E8925A; --voc-ncbitaxon: #6FA6F2; --voc-go: #5CC48F; --voc-metpo: #A48CE8;
Expand All @@ -30,18 +32,24 @@
--mech-cellstructuremech: #8a8ef5; --mech-proteintraitsmech: #3dbfb2; --mech-antibioticmech: #d9a94a;
--mech-naturalproductmech: #79b8f3; --mech-taxonmech: #8b8fe8;
--mech-mediaingredientmech: #b08cf2; --mech-culturemech: #63c46f;
--sphere-hi: .34; --sphere-hi-mid: .09; --sphere-lo-mid: .26; --sphere-lo: .44;
--fleet-edge: rgba(160, 178, 168, .34);
--fleet-heat-hue: 150 14%;
--voc-chebi: #E8925A; --voc-ncbitaxon: #6FA6F2; --voc-go: #5CC48F; --voc-metpo: #A48CE8;
--voc-envo: #4FBAD3; --voc-aro: #E86F96; --voc-uniprot: #D9AA3F; --voc-doi: #9AA5B1; --voc-other: #8C8780;
}

/* ---- Stat strip ---- */
.fleet-stats { display: grid; grid-template-columns: repeat(4, 1fr); gap: .8rem; margin: 1.2rem 0 .4rem; }
/* Five tiles need explicit breakpoints, not auto-fit. auto-fit picks the most
columns that fit, which between roughly 700px and 1010px is four, leaving
the fifth tile alone on a row and stretched to the full width. Stepping
5 -> 3 -> 2 keeps every row either full or short by one. */
.fleet-stats { display: grid; grid-template-columns: repeat(5, 1fr); gap: .8rem; margin: 1.2rem 0 .4rem; }
@media (max-width: 1010px) { .fleet-stats { grid-template-columns: repeat(3, 1fr); } }
.fleet-stats > div { padding: .9rem 1rem; background: var(--card); border: 1px solid var(--line); border-radius: 12px; }
.fleet-stats b { display: block; font-size: 1.7rem; font-weight: 800; letter-spacing: -.02em; line-height: 1.1; font-variant-numeric: tabular-nums; color: var(--ink); }
.fleet-stats span { font-size: .8rem; color: var(--muted); text-transform: uppercase; letter-spacing: .06em; }
@media (max-width: 640px) { .fleet-stats { grid-template-columns: repeat(2, 1fr); } }
.fleet-stats span { display: block; font-size: .72rem; line-height: 1.35; color: var(--muted); text-transform: uppercase; letter-spacing: .04em; }
@media (max-width: 640px) { .fleet-stats { grid-template-columns: repeat(2, 1fr); } .fleet-stats b { font-size: 1.45rem; } }

/* ---- Graph shell ---- */
.fleet-graph { margin: 1.4rem 0 2rem; background: var(--card); border: 1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow); overflow: hidden; }
Expand Down Expand Up @@ -72,6 +80,27 @@
.fleet-stage a.node-link { outline: none; }
.fleet-stage a.node-link:hover .node-label, .fleet-stage a.node-link:focus-visible .node-label { text-decoration: underline; text-decoration-thickness: 2px; text-underline-offset: 3px; }
.fleet-stage .node circle { stroke: var(--card); stroke-width: 3; transition: r .2s ease, opacity .2s ease; }
/* The shading circles sit on top of the coloured disc and must not eat its
pointer events, or hovering a node would hit the overlay and the tooltip
would never open. They carry no stroke, so the base circle keeps drawing
the single outline. */
.fleet-stage .node circle.shade { stroke: none; pointer-events: none; }
/* The gradients are shaped once in the defs and weighted here, because the
weighting has to change with the theme. Light-theme Mech colours are much
darker than dark-theme ones -- they carry their contrast against white, so
HabitatMech is #2b6a4d on light against #4fbf85 on dark -- and a limb
tuned for the dark theme turned them to mud. On light the highlight does
most of the modelling; on dark the shadow does.

Both peaks are kept low because the highlight washes every node toward
white, which pulls the ten Mech colours toward each other: at .52 the
closest dark-theme pair fell to 57% of its flat separation, and the pair
it hurt most was the one already too close (#78). At .34 it holds 77%
while the limb still models the sphere (#79). */
#fleet-sheen stop:nth-child(1) { stop-opacity: var(--sphere-hi); }
#fleet-sheen stop:nth-child(2) { stop-opacity: var(--sphere-hi-mid); }
#fleet-limb stop:nth-child(2) { stop-opacity: var(--sphere-lo-mid); }
#fleet-limb stop:nth-child(3) { stop-opacity: var(--sphere-lo); }
.fleet-stage .node.adjacent circle { stroke-dasharray: 5 4; stroke: var(--muted); }
.fleet-stage a.node-link:focus-visible circle { stroke: var(--neon); }
.fleet-stage .node.dim, .fleet-stage .edge.dim { opacity: .18; }
Expand Down Expand Up @@ -139,6 +168,10 @@
.mech-card .num { display: flex; align-items: baseline; gap: .45rem; font-variant-numeric: tabular-nums; }
.mech-card .num b { font-size: 1.45rem; font-weight: 800; letter-spacing: -.02em; color: var(--ink); }
.mech-card .num span { font-size: .82rem; color: var(--muted); }
/* Small print under the headline number: what the corpus has been through
rather than how big it is. Muted and a size down, so it reads as a footnote
to the number above it and not as a second headline. */
.mech-card .prov { margin: -.15rem 0 0; font-size: .76rem; color: var(--muted); font-variant-numeric: tabular-nums; }
.mech-card .vocab { display: flex; flex-wrap: wrap; gap: .3rem; }
.mech-card .vocab span { font-size: .72rem; font-weight: 700; padding: .25em .55em; border-radius: 6px; background: var(--wash-b); color: var(--ink); border: 1px solid var(--line); }
.mech-card .row { display: flex; flex-wrap: wrap; gap: .35rem .5rem; align-items: center; margin-top: auto; padding-top: .3rem; }
Expand Down Expand Up @@ -269,6 +302,7 @@
<div><b><!--FLEET_COUNT--></b><span>Mech knowledge bases</span></div>
<div><b>448,724</b><span>records · September 2026</span></div>
<div><b>43</b><span>vocabularies in dated census</span></div>
<div><b><!--FLEET_PRS_TOTAL--></b><span>merged PRs</span></div>
<div><b>1</b><span>orchestrator (claw)</span></div>
</div>

Expand All @@ -285,6 +319,14 @@
<a href="https://culturebotai.github.io/CultureMech/" style="--c: var(--mech-culturemech)" title="Open CultureMech"><b>Media</b><span>where it is grown</span></a>
</div>

<h2 id="shared-vocabulary">Shared vocabulary</h2>

<p>The Mechs are joinable because they ground records in the same public ontologies. The table counts identifier occurrences per vocabulary in each Mech's record corpus; darker cells mean more. Columns run from the most widely shared vocabulary to the least, so the left edge is the fleet's common ground and the right edge is what a single Mech needs alone. Click a Mech name to open it, a cell to list the records behind it, or a column heading to filter the graph to that vocabulary.</p>

<div class="fleet-heat-wrap"><table class="fleet-heat" id="fleet-heat" aria-label="Ontology identifier occurrences per Mech"></table></div>
<div class="fleet-cell-panel" id="fleet-cell-panel" hidden></div>
<p class="fleet-heat-note">This earlier September 2026 vocabulary census was not recomputed with the September 15 site refresh and covers nine Mechs; TaxonMech is a fleet member whose vocabulary census has not yet been measured. Counts are prefix occurrences in the canonical record directories (merged recipes for CultureMech, communities for CommunityMech, habitat records for HabitatMech) as of September 2026. Columns are ordered by how many Mechs ground anything in each vocabulary, then by the total records citing it across the fleet; PMID and DOI sit at the right because every Mech cites literature. ChEBI binds the chemistry arm (media, ingredients, antibiotics, proteins); NCBITaxon and ENVO bind the organism arm (habitat, community, traits); GO and METPO bridge phenotype, structure and protein.</p>

<div class="fleet-graph" id="fleet-graph">
<div class="fleet-controls">
<div class="grp" role="group" aria-label="Edge layers">
Expand Down Expand Up @@ -316,6 +358,21 @@
<defs>
<marker id="fleet-arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path class="arrow" d="M0,0 L10,5 L0,10 z"></path></marker>
<marker id="fleet-arrow-hub" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse"><path class="arrow-hub" d="M0,0 L10,5 L0,10 z"></path></marker>
<!-- One light source for the whole ring, up and to the left, so the
nodes read as lit spheres rather than ten differently lit ones.
Both gradients are in objectBoundingBox units, so a single pair
serves every node whatever its radius, and neither carries a hue:
they multiply over whichever Mech colour is underneath. -->
<radialGradient id="fleet-sheen" cx="33%" cy="27%" r="70%">
<stop offset="0%" stop-color="#fff" stop-opacity=".52"></stop>
<stop offset="40%" stop-color="#fff" stop-opacity=".13"></stop>
<stop offset="100%" stop-color="#fff" stop-opacity="0"></stop>
</radialGradient>
<radialGradient id="fleet-limb" cx="35%" cy="30%" r="78%">
<stop offset="52%" stop-color="#000" stop-opacity="0"></stop>
<stop offset="86%" stop-color="#000" stop-opacity=".26"></stop>
<stop offset="100%" stop-color="#000" stop-opacity=".44"></stop>
</radialGradient>
</defs>
<g id="fleet-gov"></g>
<g id="fleet-xrefs"></g>
Expand Down Expand Up @@ -522,6 +579,8 @@
var a = el("a", { "class": "node-link", href: M.site, "aria-label": "Open " + m + ", " + fmt(M.records) + " " + M.unit }, gNodes);
var g = el("g", { "class": "node" + (M.member ? "" : " adjacent") }, a);
el("circle", { cx: p.x, cy: p.y, r: r, fill: color(m) }, g);
el("circle", { "class": "shade", cx: p.x, cy: p.y, r: r, fill: "url(#fleet-limb)" }, g);
el("circle", { "class": "shade", cx: p.x, cy: p.y, r: r, fill: "url(#fleet-sheen)" }, g);
var anchor = Math.cos(p.a) > 0.2 ? "start" : Math.cos(p.a) < -0.2 ? "end" : "middle";
var lx = p.x + Math.cos(p.a) * (r + 10), ly = p.y + Math.sin(p.a) * (r + 10);
var dy = Math.sin(p.a) > 0.6 ? 14 : Math.sin(p.a) < -0.6 ? -10 : 4;
Expand Down
Loading