Interactive research atlas for the Africa Multiple Cluster of Excellence, exposing the cluster's WissKI/MongoDB archive as a browsable, analysable, visually-rich web app. Built with SvelteKit 5, ECharts 6, MapLibre GL 5, and Tailwind CSS 4 — shipped as an installable static PWA to GitHub Pages.
Live: https://amira.africamultiple.uni-bayreuth.de/
static/data/entity_dashboards/EntityDashboard of stacked timeline, resource type / language / subject breakdowns, decade heatmap, subject trends, time-aware chord, contributor network, and origin → current geo flows.SiblingItemsSparkline (year-by-year project timeline with the current item highlighted) and a SimilarItemsStrip (top-8 semantic-kNN matches via Gemini embeddings).alias counts for photos that appear in multiple records.?photo=…), so browser Back closes the lightbox and steps through tab changes instead of jumping to the collections list.All directory and category pages (People, Groups, Institutions, Genres, Languages, Locations, Resource Types, Subjects & Tags) share a consistent card-grid layout backed by reusable components (EntityCard, EntityBrowseGrid, EntityToolbar, EntityDetailHeader, EntityItemsCard) and utilities (applyEntitySort). Each card shows the entity name, descriptor, icon, count with label ("items", "projects", etc.), and type-specific meta chips; pages use a single toolbar with full-text search, count / alphabetical sort toggles, and a results badge. The grid paginates at 48 cards per page to keep long lists (1,000+ subjects, tags, cities) fast.
Detail views are opened via URL query params (e.g. /people?name=John%20Doe, /genres?genre=Broadcast) and the selection is driven by a writable $derived over $page.url.searchParams — so browser Back automatically clears the detail view and restores the card grid, deep links are shareable, and reload preserves state.
Each entity detail page now mounts an <EntityDashboard> populated from a precomputed per-entity JSON file. Layouts are declared in entityDashboardLayouts.ts; slots auto-hide when their payload is empty, so the grid never carries placeholder cards.
EntityDashboard (timeline, types / languages, subjects, contributors, locations)./compare/[type]) with a tab bar that switches among projects, people, institutions, subjects, languages, and genres. Every type renders the same primitives — stat row, 6-axis profile radar, shared-subjects chips, side-by-side stacked timelines + resource-type pies + top-subjects bars + language bars + contributor bars — with the data source swapped per type. /compare/projects covers university × project filters via on-the-fly aggregation off the loaded collections; the other types load precomputed entity-dashboard JSON. The legacy /compare-projects URL still works as a redirect to /compare/projects._meta.jsonEvery named entity is a link:
WissKI Navigate — Optional deep-links to WissKI entities surface throughout the dashboard via pre-computed URL mappings (dev.wisski_urls.*.json), connecting every dashboard record back to its source in the WissKI knowledge base.
Chart downloads — Every ECharts visualisation exposes a download button in its card header that exports the chart as a PNG with the card title, subtitle, and export date composited on top, using the site's typography and theme (light or dark).
PWA — The site ships a Web App Manifest (static/manifest.json) and a service worker (src/service-worker.js) so it installs as a standalone app on desktop and mobile, with offline shell and shortcuts to What's New / Projects / Research Items / Network. Theme colour, maskable icons, and orientation hints are all set.
SEO — Every route ships a <SEO> component that emits Open Graph + Twitter Card tags and a canonical URL; the build also pre-renders /sitemap.xml covering both static routes and every entity-detail URL discovered by scanning static/data/entity_dashboards/.
Mobile UX — The whole app is touch-friendly on a phone: pagination controls scroll the list back into view on tap, stat cards stack with sensible truncation, the Gantt timeline collapses to a vertical list, empty states render at full card width, and the sidebar slides over the content rather than reflowing it.
$state, $derived, $derived.by, $effect, $props)gemini-embedding-2-preview, 768-dim) projected to 2D with UMAP for the semantic map@tailwindcss/vite, HSL CSS-variable theming, dark mode@lucide/svelte@sveltejs/adapter-static and GitHub Actions| Component | Type | Description |
|---|---|---|
Timeline |
Bar | Count-by-year timeline |
StackedTimeline |
Stacked bar | Items per year, broken down by resource type |
StackedAreaChart |
Stacked area | Subject / language trends over time (top-N stream) |
BarChart |
Bar | Horizontal / vertical bar with pagination for long lists |
PieChart |
Pie / donut | Categorical distribution with click selection |
WordCloud |
Word cloud | Animated tag / subject cloud with adjustable max words |
HeatmapChart |
Heatmap | Matrix cross-tabulation with colour intensity |
CalendarHeatmap |
Calendar | Year × month / day activity intensity |
BeeswarmChart |
Scatter / jitter | Beeswarm distribution using ECharts 6 axis jitter |
BoxPlot |
Box plot | Five-number summary + outliers, computed from raw observations |
RadarChart |
Radar | 5–7 axis polygon overlay; powers the compare-page profile |
GanttChart |
Custom bar range | Project timelines with start / end bars, category colouring |
SankeyChart |
Sankey | Multi-level flow diagram (e.g. contributor → project → type) |
SunburstChart |
Sunburst | Hierarchical drill-down visualisation |
TreemapChart |
Treemap | Proportional rectangles for hierarchical breakdowns |
ChordDiagram |
Chord | Co-occurrence relationships between categories |
TimeAwareChord |
Chord + slider | Chord diagram with year slider + play/pause; data is sparse year buckets |
SemanticScatter |
Scatter | UMAP projection of Gemini embeddings; colourable by four dimensions |
NetworkGraph |
Force graph | Weighted force-directed network: edge width follows edge value, dashed for latent ties, solid for direct metadata edges; optional community halos |
ContributorNetwork |
Force graph | Bipartite person ↔ project / institution graph built on NetworkGraph |
EntityKnowledgeGraph |
Force graph | Per-entity ego graph with IDF-weighted direct edges + latent edges via Jaccard / personalised PageRank, discursive communities, PageRank-sized nodes, facet panel, fullscreen mode |
LocationMap |
Map | MapLibre GL multi-marker map with clustered popups, Flat / Globe projection toggle |
MiniMap |
Map | Lightweight single-location map with marker |
ChoroplethMap |
Map | Country-level fill on Natural Earth 110m, log-spaced colour ramp |
GeoFlowMap |
Map | Great-circle origin → current arcs on MapLibre |
LocationsMapView |
Map switcher | Toggles a single dashboard slot between LocationMap (points) and ChoroplethMap (countries) |
EChart |
Base wrapper | Shared ECharts wrapper: dynamic theme switching via setTheme(), zoom controls, resize handling, performance heuristics |
ChartDownloadButton |
Action | Exports the parent chart as a PNG with title, subtitle, and export date composited on top; auto-wired to any chart hosted inside a ChartCard |
npm install
npm run dev # dev server
npm run build # production build
npm run preview # preview production build
npm run check # svelte-kit sync + svelte-check
npm run lint # ESLint
npm run format # Prettier write
npm run format:check # Prettier check (CI uses this)
CI runs format:check, so run npm run format locally before committing if the check fails.
src/
├── lib/
│ ├── components/
│ │ ├── ui/ # Reusable UI primitives
│ │ │ ├── card*.svelte, badge, button, input, select, combobox, tabs
│ │ │ ├── pagination, stat-card, chart-card, scrollable-table
│ │ │ ├── collection-item-row, back-to-list, empty-state
│ │ │ ├── section-badge, wisski-link, scroll-to-top, seo
│ │ ├── charts/ # ECharts + MapLibre chart components
│ │ │ ├── EChart.svelte # Base wrapper
│ │ │ ├── ChartDownloadButton.svelte # PNG export with title / subtitle composited
│ │ │ ├── chart-registry.ts # Context bridge: chart instance ↔ ChartCard header
│ │ │ ├── Timeline, StackedTimeline, StackedAreaChart
│ │ │ ├── BarChart, PieChart, WordCloud
│ │ │ ├── HeatmapChart, CalendarHeatmap, BeeswarmChart, BoxPlot
│ │ │ ├── RadarChart, GanttChart
│ │ │ ├── SankeyChart, SunburstChart, TreemapChart
│ │ │ ├── ChordDiagram, TimeAwareChord
│ │ │ ├── NetworkGraph, ContributorNetwork, EntityKnowledgeGraph
│ │ │ ├── SemanticScatter
│ │ │ ├── LocationMap, MiniMap, ChoroplethMap, GeoFlowMap, LocationsMapView
│ │ │ ├── map/ # Projection toggle, marker / popup builders
│ │ │ └── utils/ # Shared option builders & tooltip formatters
│ │ ├── dashboards/ # Per-entity detail dashboards
│ │ │ ├── EntityDashboard.svelte # Generic chart-grid renderer
│ │ │ ├── EntityDashboardSection.svelte # Loader wrapper for pages
│ │ │ ├── ChartSlot.svelte # Dispatches chart key → component
│ │ │ └── entityDashboardLayouts.ts # ENTITY_LAYOUTS + ChartKey + emptiness rules
│ │ ├── compare/ # Side-by-side compare primitives
│ │ │ ├── CompareTabs, CompareStatRow, CompareSharedSubjects
│ │ │ ├── CompareProfileRadar, ComparePair
│ │ │ ├── ProjectsCompare (on-the-fly aggregation)
│ │ │ ├── EntityCompare (precomputed JSON)
│ │ │ └── compareTypes.ts, compareProfile.ts
│ │ ├── collections/ # Featured-collection components
│ │ │ ├── CollectionHeader, CollectionIndexCard
│ │ │ ├── PhotoCard, PhotoFacets, PhotoLightbox
│ │ │ ├── PhotoMasonry, PhotoMap, PhotoTimeline
│ │ │ ├── ViewModeTabs, photoHelpers.ts
│ │ ├── entity-browse/ # Unified card-grid components for directory / categories
│ │ │ ├── EntityCard, EntityCardGrid, EntityBrowseGrid (grid + pagination)
│ │ │ ├── EntityToolbar (search + sort + total), EntityDetailHeader
│ │ │ ├── EntityItemsCard, SearchableItemsCard
│ │ │ ├── EntityEmptyHint, sort.ts (applyEntitySort)
│ │ ├── layout/ # Sidebar (grouped nav), Header, FilterPanel
│ │ └── research-items/ # ItemDetail, ItemFilters, ItemTable, itemHelpers,
│ │ │ # SiblingItemsSparkline, SimilarItemsStrip
│ ├── stores/
│ │ ├── data.ts # Raw data + derived stores (projects, persons, collections…)
│ │ └── filters.ts # Global filter state
│ ├── styles/ # Tokens, animations, component CSS, sidebar, maplibre
│ ├── types/ # TS interfaces (domain, collection, charts, geo, mongo, embeddings, category-index)
│ └── utils/
│ ├── transforms/ # dates, grouping, extractors, network, charts, filters
│ ├── loaders/ # mongoJSON, collectionLoader, geolocLoader, embeddingsLoader,
│ │ # entityDashboardLoader (per-entity JSON + manifest)
│ ├── external.ts # Virtual external projects (BayGlo2025, ILAM) + pseudo-section
│ ├── helpers.ts # formatDate, getItemTitle, getProjectTitle, getSectionColor
│ ├── languages.ts # ISO 639-2/3 → English name mapping
│ ├── urls.ts # Cross-linking URL builders
│ ├── urlSelection.ts # URL query-param sync for selection state
│ ├── search.ts # Generic text-search filter factory
│ ├── pagination.ts # Generic pagination utility
│ ├── slugify.ts # URL-safe slug generator (kept in sync with the precompute pipeline)
│ ├── wisskiUrl.svelte.ts # WissKI navigate URL lookup (Svelte store)
│ ├── featuredCollectionLoader.ts # Featured collections card builder
│ ├── collectionsRegistry.ts # Featured collection metadata registry
│ ├── revealOnScroll.ts # IntersectionObserver-based reveal actions
│ └── cn.ts # Classname merging (clsx + tailwind-merge)
├── routes/
│ ├── +page.svelte # Overview dashboard
│ ├── +layout.svelte # Global layout (Header, Sidebar, data init)
│ ├── whats-new/ # Recent additions (3 / 6 / 12 months)
│ ├── research-sections/ # 13 sections + per-section EntityDashboard
│ ├── projects/ # Projects + per-project full-parity dashboard
│ ├── research-items/ # Research items browser; detail = KG + sparkline + similar
│ ├── publications/ # Cluster bibliography (ERef + EPub Bayreuth, deduped)
│ ├── people/ # People directory + per-person dashboard
│ ├── groups/ # Research groups + per-group dashboard
│ ├── institutions/ # Institutions + per-institution dashboard
│ ├── collections/ # Featured collections index
│ ├── collections/[slug]/ # Collection detail: masonry / map / timeline
│ ├── genres/ # Genres + per-genre dashboard
│ ├── languages/ # Languages + per-language dashboard
│ ├── locations/ # Locations + browse map + per-location dashboard
│ ├── resource-types/ # Resource types + per-type dashboard
│ ├── subjects/ # Subjects & Tags + per-entity dashboard
│ ├── project-explorer/ # Cross-project analytical workspace
│ ├── compare/[type]/ # Generic compare (projects | people | institutions |
│ │ # subjects | languages | genres) — uses CompareTabs
│ ├── compare-projects/ # Legacy redirect → /compare/projects
│ ├── network/ # Network visualisation (5 tabs)
│ ├── semantic-map/ # UMAP embedding scatter with similar-items
│ └── sitemap.xml/ # Prerendered sitemap (static + entity-detail URLs)
├── service-worker.js # PWA shell + asset cache
└── app.css # Global styles and Tailwind v4 config
static/
├── manifest.json # PWA manifest (name, icons, shortcuts, theme colour)
├── robots.txt
├── icons/ # PWA icons (192 / 512 + maskable)
├── data/
│ ├── manifest.json # Per-university collection inventory
│ ├── dev/ # MongoDB exports + WissKI URL mappings
│ ├── manual/ # Hand-curated supplements (e.g. projectLinks)
│ ├── geo/ # Natural Earth 110m country GeoJSON for choropleth
│ ├── entity_dashboards/ # Precomputed per-entity dashboard JSON
│ │ ├── manifest.json # `{ <dir>: [{id, name, count}, ...] }`
│ │ ├── languages/, subjects/, tags/, people/, institutions/,
│ │ ├── genres/, resource-types/, groups/, locations/,
│ │ └── research-sections/, projects/
│ ├── knowledge_graphs/ # Pre-computed ego graphs per entity type + _meta.json
│ ├── embeddings/ # map.json (UMAP) + similar.json (top-K neighbours)
│ ├── projects_metadata_ubt/ # University of Bayreuth (21 collections)
│ ├── projects_metadata_unilag/ # University of Lagos (8 collections)
│ ├── projects_metadata_ujkz/ # Université Joseph Ki-Zerbo (7 collections)
│ ├── projects_metadata_ufba/ # Federal University of Bahia (1 collection)
│ └── external_metadata/ # External collections (BayGlo2025, ILAM)
└── logos/ # Partner institution logos used across the UI
static/data/dev/dev.projectsData.json — 92 projects with PIs, members, dates, research sections, institutions, RDSpace referencesdev.persons.json — 1,394 person records with institutional affiliationsdev.institutions.json — 492 institution recordsdev.groups.json — 84 research-group recordsdev.collections.json — development-only collection itemsdev.researchSections.json — all 13 research sections (Phase 1 + Phase 2 + External)dev.geo.json — country / region / subregion / city geolocations with Wikidata coordinatesdev.wisski_urls.*.json — pre-computed WissKI navigate URL mappings for every entity type (projects, persons, institutions, items, subjects, tags, countries, regions, cities, genres, groups, languages, research sections, resource types)static/data/projects_metadata_*/3,975 research items across four partners. Each item carries title, contributors (person / institution / group qualifier with roles), subjects (LCSH), tags, language (ISO 639-2/3), location (country / region / city), dates (created, issued, captured), identifiers, physical description, access conditions, and bitstream / URL references.
static/data/external_metadata/Items contributed from outside the cluster's partner universities. Tagged university: "external" so they count toward global totals but can be filtered out of per-university views:
BayGlo2025 — Bayreuth Global / Bayreuth Postkolonial; affiliated with the University of BayreuthILAM — International Library of African Music (Rhodes University)Surfaced across the dashboard as virtual projects (Ext_BayGlo2025, Ext_ILAM) under an External pseudo research section, with a dedicated chip in the global filter panel and a dedicated group in the Project Explorer and Compare selectors. Virtual-project definitions live in src/lib/utils/external.ts.
static/data/entity_dashboards/Pre-computed JSON dumps powering the per-entity detail pages and the /compare/[type] selectors. One file per entity instance, organised by directory:
entity_dashboards/
├── manifest.json # `{ <dir>: [{id, name, count}, ...] }`
├── languages/{code}.json # 28 entries
├── subjects/{slug}.json # 620 entries
├── tags/{slug}.json # 1,100 entries
├── people/{slug}.json # 1,174 entries
├── institutions/{slug}.json # 459 entries
├── genres/{slug}.json # 129 entries
├── resource-types/{slug}.json # 11 entries
├── groups/{slug}.json # 84 entries
├── locations/{slug}.json # 252 entries
├── research-sections/{slug}.json # 6 entries
└── projects/{id}.json # 38 entries
Each JSON contains all the chart payloads needed to render its entity's dashboard (timeline, stackedTimeline, types, languages, subjects, wordCloud, contributors, roles, heatmap, chord, coContributors, coSubjects, sankey, sunburst, treemap, subjectTrends, locations, selfLocation, geoFlows, contributorNetwork, affiliationNetwork, collabNetwork, meta, items). Generated by scripts/precompute_entity_dashboards.py; the loader lives at src/lib/utils/loaders/entityDashboardLoader.ts.
static/data/knowledge_graphs/Structural ego graphs pre-computed by scripts/generate_knowledge_graphs.py — one JSON file per entity organised by type:
knowledge_graphs/
├── _meta.json # Global community labels + top-PageRank nodes
├── items/ # Research-item ego graphs
├── persons/
├── projects/
├── institutions/
├── subjects/
├── tags/
├── locations/
└── genres/
Each graph combines direct metadata edges (IDF-weighted so distinctive relationships look heavier than ubiquitous ones), latent structural edges (Jaccard on shared neighbourhoods + personalised PageRank for multi-hop relevance), and global analysis results (Louvain community membership, PageRank-based centrality) so the UI can reveal discursive communities and key nodes that local co-occurrence alone would miss.
static/data/embeddings/map.json — per-item {id, x, y, lowSignal, title, project, university, typeOfResource} for the 2D UMAP scattersimilar.json — per-item top-12 cosine-similar neighbours keyed by dre_id (lazy-loaded on first selection on /semantic-map, also consumed by SimilarItemsStrip on /research-items?id=…)cache.json — full 768-dim Gemini vectors + SHA-256 hashes used for incremental re-embedding; gitignoredPipelined by scripts/generate_embeddings.py. See scripts/README.md for details.
static/data/publications.json146 publications harvested from two University of Bayreuth EPrints repositories and merged into a single normalised payload:
Three EPrints 3 export endpoints are consumed per source: bulk BibTeX (canonical metadata), RSS (deposit dates → year / quarter buckets), and EP3 XML (abstracts, structured keywords, GND author IDs, languages, related / publisher DOIs, volume editors). Cross-source deduplication runs three tiers — DOI (with author / title sanity guard so book-level DOIs don't merge sibling chapters), ISBN (books and periodicals only), and fuzzy (normalized title + year + first-author surname) — folding the 31 cross-listed records into single entries that carry both eref_url and epub_url. Generated by scripts/fetch_publications.py; the loader lives at src/lib/utils/loaders/publicationsLoader.ts.
static/data/geo/world-countries-110m.json — Natural Earth 110m country boundaries (TopoJSON) used by ChoroplethMap. Bundled once with the build; smaller than the 50m / 10m datasets at the cost of coastline detail.static/data/manual/Hand-curated data not sourced from MongoDB:
projectLinks.json — supplementary project-to-entity linksPython pipelines live in scripts/ and cover MongoDB export, WissKI URL generation, thumbnail fetching, knowledge-graph generation, embedding generation, per-entity dashboard precompute, and data slimming. Setup:
python -m venv .venv
.venv/Scripts/pip install -r scripts/requirements.txt
Key scripts (see scripts/README.md for the full list and inputs / outputs):
fetch_from_mongodb.py — pull research items + reference data from the WissKI MongoDBgenerate_wisski_urls.py — emit pre-computed dev.wisski_urls.*.json lookups for the WissKI Navigate linksfetch_thumbnails.py — download remote previewImage URLs as local WebP thumbnails so the first paint doesn't hit the public archivefetch_publications.py — harvest BibTeX / RSS / EP3 XML from ERef Bayreuth (projekt view) and EPub Bayreuth (Africa Multiple division view), deduplicate across sources by DOI / ISBN / fuzzy title-author-year, and emit static/data/publications.jsongenerate_knowledge_graphs.py — compute the ego-graph JSON consumed by EntityKnowledgeGraph and /network?tab=communitiesgenerate_embeddings.py — run Gemini Embedding 2 over the corpus, project to 2D with UMAP, emit map.json + similar.jsonprecompute_entity_dashboards.py — generate one JSON per entity under static/data/entity_dashboards/, plus the global manifest. Supports --from-local, per-type runs, and --dry-run. The manifest writer merges with the existing file so a single-entity run doesn't wipe the others' entriesslim_data.py — strip development-only fields before shipping production dataRegenerate everything after a metadata refresh:
.venv/Scripts/python scripts/generate_knowledge_graphs.py
.venv/Scripts/python scripts/generate_embeddings.py --scope missing
.venv/Scripts/python scripts/precompute_entity_dashboards.py --entity all --from-local
Deploys automatically to GitHub Pages on push to main:
npm ci → npm run build → actions/upload-pages-artifact → actions/deploy-pages@sveltejs/adapter-static pre-renders every route into build/static/CNAME pins the site to amira.africamultiple.uni-bayreuth.de; paths.base stays empty so all asset URLs resolve at the apexnpm run format:check, npm run lint, npm run check, and npm run build on every push and PRDeveloped by Frédérick Madore for the Digital Research Environment (DRE) of the Africa Multiple Cluster of Excellence, University of Bayreuth.
MIT