+ {{ $e.name }} +
+ +{{ $e.desc }}
+ + {{ with $e.normtags }} + + {{ end }} + + {{ with $links }} +From 286c113c2d687589441275d1864fff45e8a814be Mon Sep 17 00:00:00 2001
From: Lukas Heumos wrapping markdownify output, which nested paragraphs.
---
assets/main.scss | 354 ++++++++++++++++++++++++++++++++++++-
layouts/packages/list.html | 318 +++++++++++++++++++++++++++++----
static/main.js | 101 +++++++++--
3 files changed, 720 insertions(+), 53 deletions(-)
diff --git a/assets/main.scss b/assets/main.scss
index 8a3ee6e..cb376ee 100644
--- a/assets/main.scss
+++ b/assets/main.scss
@@ -1053,9 +1053,357 @@ body {
}
}
-#eco-table tbody {
- // TODO: I think this shouldn't be neccesary
- border-top: 0px;
+// Specific to /packages#ecosystem — the community package registry
+
+$eco-radius: 0.75rem;
+$eco-hues:
+ $anndataorange, $scanpycerise, $mudatagreen, $spatialdatablue, $squidpyviolet,
+ $scirpypurple, $scviyellow, $muonaquamarine;
+
+#ecosystem-packages {
+ margin: 2rem 0 4rem;
+
+ .eco-sprite {
+ display: none;
+ }
+
+ svg {
+ fill: none;
+ stroke: currentColor;
+ stroke-width: 1.8;
+ stroke-linecap: round;
+ stroke-linejoin: round;
+ }
+
+ .eco-toolbar {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 0.5rem 1.5rem;
+ }
+
+ .eco-search {
+ position: relative;
+ display: flex;
+ align-items: center;
+ flex: 1 1 22rem;
+
+ .eco-search-icon {
+ position: absolute;
+ left: 1.1rem;
+ width: 1.1rem;
+ height: 1.1rem;
+ color: $greydesc;
+ pointer-events: none;
+ }
+
+ input {
+ width: 100%;
+ height: 3.25rem;
+ padding: 0 2.9rem;
+ font-size: 1rem;
+ color: $tiletext;
+ background-color: $backtickbg;
+ border: 1px solid $overline;
+ border-radius: 2rem;
+ outline: none;
+ transition: all 200ms ease-in-out;
+ text-overflow: ellipsis;
+
+ &::placeholder {
+ color: $greydesc;
+ }
+ &:focus {
+ background-color: white;
+ border-color: $greydesc;
+ box-shadow: 0 0 0 0.25rem rgba(0, 0, 0, 0.04);
+ }
+ // The native affordance duplicates our own clear button
+ &::-webkit-search-cancel-button {
+ display: none;
+ }
+ }
+ }
+
+ .eco-search-clear {
+ position: absolute;
+ right: 0.9rem;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 1.6rem;
+ height: 1.6rem;
+ padding: 0;
+ border: 0;
+ border-radius: 50%;
+ color: $tiletext2;
+ background-color: $tilebg;
+ cursor: pointer;
+ transition: all 200ms ease-in-out;
+
+ svg {
+ width: 0.7rem;
+ height: 0.7rem;
+ }
+ &:hover {
+ color: white;
+ background-color: $tiletext;
+ }
+ }
+
+ .eco-count {
+ margin: 0;
+ font-size: 0.95rem;
+ color: $greydesc;
+ white-space: nowrap;
+ }
+
+ .eco-chips {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.4rem;
+ margin: 1.25rem 0 2rem;
+ }
+
+ .eco-chip {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.4rem;
+ padding: 0.35rem 0.85rem;
+ font-size: 0.85rem;
+ color: $tiletext2;
+ background-color: white;
+ border: 1px solid $overline;
+ border-radius: 2rem;
+ cursor: pointer;
+ transition: all 200ms ease-in-out;
+
+ &:hover {
+ color: $tiletext3;
+ border-color: $tilebg2;
+ background-color: $tilebg4;
+ }
+ &.is-active {
+ color: white;
+ background-color: $tiletext;
+ border-color: $tiletext;
+ }
+ .eco-chip-count {
+ font-size: 0.75rem;
+ opacity: 0.55;
+ }
+ }
+
+ .eco-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(19rem, 1fr));
+ gap: 1.25rem;
+ }
+
+ .eco-card {
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ gap: 0.8rem;
+ padding: 1.35rem;
+ background-color: white;
+ border: 1px solid $overline;
+ border-radius: $eco-radius;
+ transition: all 200ms ease-in-out;
+
+ &:hover {
+ border-color: $tilebg2;
+ box-shadow: 0 0.6rem 1.5rem rgba(0, 0, 0, 0.06);
+ transform: translateY(-2px);
+ }
+ &[hidden] {
+ display: none;
+ }
+ }
+
+ .eco-card-head {
+ display: flex;
+ align-items: center;
+ gap: 0.85rem;
+ }
+
+ .eco-logo {
+ display: grid;
+ place-items: center;
+ flex: 0 0 auto;
+ width: 2.75rem;
+ height: 2.75rem;
+ overflow: hidden;
+ border-radius: 0.6rem;
+ background-color: $tilebg4;
+
+ img {
+ width: 100%;
+ height: 100%;
+ padding: 0.3rem;
+ object-fit: contain;
+ }
+ }
+
+ .eco-logo--mono {
+ font-size: 1.3rem;
+ font-weight: 800;
+ color: $tiletext;
+
+ @each $hue in $eco-hues {
+ &[data-hue="#{index($eco-hues, $hue) - 1}"] {
+ background-color: rgba($hue, 0.18);
+ }
+ }
+ }
+
+ .eco-card-heading {
+ min-width: 0;
+ }
+
+ // Element selector included to beat the generic `#page-content .post h3` margins
+ .eco-card h3.eco-card-name {
+ margin: 0;
+ font-size: 1.15rem;
+ font-weight: 800;
+ line-height: 1.3;
+ overflow-wrap: anywhere;
+
+ a {
+ color: black;
+ text-decoration: none;
+
+ // Make the whole card clickable without nesting interactive elements
+ &::after {
+ content: "";
+ position: absolute;
+ inset: 0;
+ border-radius: $eco-radius;
+ }
+ }
+ }
+
+ .eco-card-meta {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 0.4rem;
+ margin: 0.25rem 0 0;
+ font-size: 0.75rem;
+ color: $greydesc;
+
+ .eco-badge {
+ padding: 0.05rem 0.35rem;
+ font-size: 0.65rem;
+ font-weight: 700;
+ letter-spacing: 0.04em;
+ text-transform: uppercase;
+ color: white;
+ background-color: $tiletext;
+ border-radius: 0.25rem;
+ }
+ .eco-version {
+ font-family: "JetBrains Mono", Menlo, monospace;
+ }
+ .eco-version + .eco-license::before {
+ content: "·";
+ margin-right: 0.4rem;
+ }
+ }
+
+ .eco-card-desc {
+ display: -webkit-box;
+ -webkit-line-clamp: 4;
+ -webkit-box-orient: vertical;
+ overflow: hidden;
+ margin: 0;
+ font-size: 0.9rem;
+ line-height: 1.55;
+ color: $tiletext2;
+ }
+
+ .eco-card-tags {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.35rem;
+ margin: 0;
+ padding: 0;
+ list-style: none;
+ }
+
+ .eco-tag {
+ position: relative;
+ z-index: 1;
+ padding: 0.2rem 0.5rem;
+ font-size: 0.72rem;
+ color: $greydesc;
+ background-color: $tilebg4;
+ border: 0;
+ border-radius: 0.35rem;
+ cursor: pointer;
+ transition: all 200ms ease-in-out;
+
+ &:hover {
+ color: $tiletext3;
+ background-color: $tilebg;
+ }
+ }
+
+ .eco-card-links {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.4rem;
+ margin-top: auto;
+ padding-top: 0.3rem;
+
+ a {
+ position: relative;
+ z-index: 1;
+ display: inline-flex;
+ align-items: center;
+ gap: 0.35rem;
+ padding: 0.3rem 0.6rem;
+ font-size: 0.78rem;
+ color: $tiletext2;
+ text-decoration: none;
+ background-color: $tilebg4;
+ border-radius: 0.4rem;
+ transition: all 200ms ease-in-out;
+
+ svg {
+ width: 0.85rem;
+ height: 0.85rem;
+ }
+ &:hover {
+ color: white;
+ background-color: $tiletext;
+ }
+ }
+ }
+
+ .eco-empty {
+ padding: 4rem 0;
+ text-align: center;
+ color: $greydesc;
+
+ .eco-empty-reset {
+ padding: 0;
+ color: $tiletext;
+ background: none;
+ border: 0;
+ text-decoration: underline;
+ cursor: pointer;
+ }
+ }
+
+ @media (max-width: 50rem) {
+ .eco-grid {
+ grid-template-columns: 1fr;
+ }
+ .eco-count {
+ order: -1;
+ }
+ }
}
// Specific to /tutorials
diff --git a/layouts/packages/list.html b/layouts/packages/list.html
index c913803..d0baded 100644
--- a/layouts/packages/list.html
+++ b/layouts/packages/list.html
@@ -62,42 +62,294 @@ {{ $sections.ecosystem | markdownify }} {{ $e.desc }}
+ No packages match your filters.
+
+
+ The package registry could not be loaded. Browse it directly on
+ GitHub.
+ Frameworks
{{ end }}
- {{ $jsonURL := "https://scverse.org/ecosystem-packages/packages.json" }}
- {{ $remoteData := resources.GetRemote $jsonURL }}
- {{ $pkgs := transform.Unmarshal $remoteData.Content }}
+
Ecosystem packages maintained by scverse community
-
-
-
+ {{ with $e.tutorials_home }}
+ {{ $candidates = $candidates | append (dict "icon" "eco-i-cap" "label" "Tutorials" "url" .) }}
+ {{ end }}
+ {{ $install := $e.install | default dict }}
+ {{ with $install.pypi }}
+ {{ $candidates = $candidates | append (dict
+ "icon" "eco-i-package" "label" "PyPI" "url" (printf "https://pypi.org/project/%s/" .)
+ )
+ }}
+ {{ end }}
+ {{ with $install.conda }}
+ {{ $parts := split . "::" }}
+ {{ $url := printf "https://anaconda.org/search?q=%s" (urlquery .) }}
+ {{ if eq (len $parts) 2 }}
+ {{ $url = printf "https://anaconda.org/%s/%s" (index $parts 0) (index $parts 1) }}
+ {{ end }}
+ {{ $candidates = $candidates | append (dict "icon" "eco-i-package" "label" "conda" "url" $url) }}
+ {{ end }}
+ {{ with $install.cran }}
+ {{ $candidates = $candidates | append (dict
+ "icon" "eco-i-package" "label" "CRAN" "url" (printf "https://cran.r-project.org/package=%s" .)
+ )
+ }}
+ {{ end }}
+ {{ with $install.bioconductor }}
+ {{ $candidates = $candidates | append (dict
+ "icon" "eco-i-package" "label" "Bioconductor" "url" (printf "https://bioconductor.org/packages/%s" .)
+ )
+ }}
+ {{ end }}
+ {{ with $e.publications }}
+ {{ $doi := index . 0 }}
+ {{ $url := cond (hasPrefix $doi "http") $doi (printf "https://doi.org/%s" $doi) }}
+ {{ $candidates = $candidates | append (dict "icon" "eco-i-paper" "label" "Paper" "url" $url) }}
+ {{ end }}
+
+ {{/* Registries often point several fields at the same URL — show it once. */}}
+ {{ $links := slice }}
+ {{ $seen := slice }}
+ {{ range $candidates }}
+ {{ $url := trim .url "/" }}
+ {{ if not (in $seen $url) }}
+ {{ $seen = $seen | append $url }}
+ {{ $links = $links | append . }}
+ {{ end }}
+ {{ end }}
+
+
+
-
-
-
- {{ range $lib := $pkgs }}
- Package
- Description
-
-
+ {{ $sections.ecosystem | markdownify }}
+
+ {{ $registryBase := "https://scverse.org/ecosystem-packages/" }}
+ {{ $data := false }}
+ {{ with try (resources.GetRemote (printf "%spackages.json" $registryBase)) }}
+ {{ with .Value }}
+ {{ $data = . }}
+ {{ end }}
+ {{ end }}
+ {{ if $data }}
+ {{ $pkgs := transform.Unmarshal $data.Content }}
+
+ {{/* Author-provided tags are free-form, so fold the obvious spelling variants together. */}}
+ {{ $aliases := dict
+ "single-cell" "single cell"
+ "singlecell" "single cell"
+ "single cell analysis" "single cell"
+ "scrnaseq" "scrna-seq"
+ "scrna seq" "scrna-seq"
+ "single cell rna-seq" "scrna-seq"
+ "single-cell rna-seq" "scrna-seq"
+ "rnaseq" "rna-seq"
+ "rna seq" "rna-seq"
+ "rna-sequencing" "rna-seq"
+ "spatial" "spatial omics"
+ "spatial-omics" "spatial omics"
+ "spatial transcriptomics" "spatial omics"
+ "spatial-transcriptomics" "spatial omics"
+ "spatialomics" "spatial omics"
+ "multi omics" "multi-omics"
+ "multi_omics" "multi-omics"
+ "multiomics" "multi-omics"
+ "multi-modal" "multimodal"
+ "deep-learning" "deep learning"
+ "deeplearning" "deep learning"
+ "machine-learning" "machine learning"
+ "differential-expression" "differential expression"
+ "differential gene expression" "differential expression"
+ "data structure" "data structures"
+ "optimal-transport" "optimal transport"
+ "cell-cell-communication" "cell-cell communication"
+ "cell cell communication" "cell-cell communication"
+ "gene regulatory networks" "gene networks"
+ "gene network inference" "gene networks"
+ "grn" "gene networks"
+ "rna velocity" "rna-velocity"
+ "trajectory-inference" "trajectory inference"
+ "trajectory-generation" "trajectory inference"
+ "pseudotime" "trajectory inference"
+ }}
+
+ {{/* Normalise tags once, then reuse for the chip row, the cards and the search index. */}}
+ {{ $items := slice }}
+ {{ $tagCounts := dict }}
+ {{ range $pkgs }}
+ {{ $tags := slice }}
+ {{ range (.tags | default slice) }}
+ {{ $t := trim (. | lower | replaceRE `\s+` " ") " " }}
+ {{ $t = index $aliases $t | default $t }}
+ {{ if and $t (not (in $tags $t)) }}
+ {{ $tags = $tags | append $t }}
+ {{ $tagCounts = merge $tagCounts (dict $t (add (index $tagCounts $t | default 0) 1)) }}
+ {{ end }}
+ {{ end }}
+ {{/* Registry descriptions are plain text with the odd bit of markdown and hard line breaks. */}}
+ {{ $desc := trim (.description | markdownify | plainify | htmlUnescape | replaceRE `\s+` " ") " " }}
+ {{ $search := lower (printf "%s %s %s" .name $desc (delimit $tags " ")) }}
+ {{ $items = $items | append (merge . (dict
+ "sortkey" (lower .name)
+ "normtags" $tags
+ "desc" $desc
+ "search" $search
+ ))
+ }}
+ {{ end }}
+ {{ $items = sort $items "sortkey" }}
+
+ {{/* Chip row: the tags that are common enough to be worth a shortcut. */}}
+ {{ $tagList := slice }}
+ {{ range $tag, $count := $tagCounts }}
+ {{ if ge $count 3 }}
+ {{ $tagList = $tagList | append (dict "tag" $tag "count" $count) }}
+ {{ end }}
+ {{ end }}
+ {{ $topTags := sort (first 12 (sort $tagList "count" "desc")) "tag" }}
+
+
+
- {{ $lib.name }}
-
- {{ $lib.description | markdownify }}
-
+
+ {{ else }}
+
+ {{ end }}
+
+ {{ $e.name }}
+
+
+ Ecosystem packages maintained by scverse community
{{ $sections.ecosystem | markdownify }}
{{ $registryBase := "https://scverse.org/ecosystem-packages/" }}
- {{ $data := false }}
- {{ with try (resources.GetRemote (printf "%spackages.json" $registryBase)) }}
- {{ with .Value }}
- {{ $data = . }}
- {{ end }}
- {{ end }}
+ {{/* Netlify previews build with Hugo 0.97, so no `try` here — it needs 0.141+. */}}
+ {{ $data := resources.GetRemote (printf "%spackages.json" $registryBase) }}
{{ if $data }}
{{ $pkgs := transform.Unmarshal $data.Content }}
diff --git a/netlify.toml b/netlify.toml
new file mode 100644
index 0000000..b83ddc5
--- /dev/null
+++ b/netlify.toml
@@ -0,0 +1,5 @@
+# Deploy previews used to build with Netlify's default Hugo (0.97.2, from 2022), so they did
+# not reflect what gets published: .github/workflows/gh-pages.yml builds with `latest`, which
+# currently resolves to 0.164.0. Pin previews to that, and bump both together from here on.
+[build.environment]
+HUGO_VERSION = "0.164.0"
From 166fecc6657b8abd8b2f71fff18219f335be7443 Mon Sep 17 00:00:00 2001
From: Lukas Heumos Ecosystem packages maintained by scverse community
{{ $sections.ecosystem | markdownify }}
{{ $registryBase := "https://scverse.org/ecosystem-packages/" }}
- {{/* Netlify previews build with Hugo 0.97, so no `try` here — it needs 0.141+. */}}
- {{ $data := resources.GetRemote (printf "%spackages.json" $registryBase) }}
+ {{/* An unreachable registry should not fail the build, so swallow the error and fall back. */}}
+ {{ $data := false }}
+ {{ with try (resources.GetRemote (printf "%spackages.json" $registryBase)) }}
+ {{ $data = .Value }}
+ {{ end }}
{{ if $data }}
{{ $pkgs := transform.Unmarshal $data.Content }}
From fe44400e6c72fda82e84c85ce87262a4667cc20a Mon Sep 17 00:00:00 2001
From: Lukas Heumos Ecosystem packages maintained by scverse community
{{ $sections.ecosystem | markdownify }}
{{ $registryBase := "https://scverse.org/ecosystem-packages/" }}
- {{/* An unreachable registry should not fail the build, so swallow the error and fall back. */}}
- {{ $data := false }}
- {{ with try (resources.GetRemote (printf "%spackages.json" $registryBase)) }}
- {{ $data = .Value }}
- {{ end }}
- {{ if $data }}
- {{ $pkgs := transform.Unmarshal $data.Content }}
+ {{ $data := resources.GetRemote (printf "%spackages.json" $registryBase) }}
+ {{ $pkgs := transform.Unmarshal $data.Content }}
+ {{ if $pkgs }}
{{/* Author-provided tags are free-form, so fold the obvious spelling variants together. */}}
{{ $aliases := dict
@@ -344,11 +340,6 @@
- The package registry could not be loaded. Browse it directly on - GitHub. -
{{ end }} From 3b96ce454bfb7b8d52749de9c1b9822634c335f3 Mon Sep 17 00:00:00 2001 From: Lukas Heumos