From 5fe692296dd4a9d01c640bbd35f888d1149572ed Mon Sep 17 00:00:00 2001 From: kempersc Date: Wed, 17 Dec 2025 08:55:26 +0100 Subject: [PATCH] Fix RDF visualization: correct SPARQL namespaces and show all node types - Update SPARQL CONSTRUCT query to use correct ontology namespaces: - hc: https://w3id.org/heritage/custodian/ (was nde) - Use nde:Custodian type (was crm:E39_Actor) - Use schema:location + geo:lat/long (was crm predicates) - Remove LIMIT 500 clause to fetch all results - Show all node types by default instead of random single type - Fixes issue where Knowledge Graph showed incomplete/random data --- frontend/src/pages/Visualize.tsx | 151 +++++++++++++------------------ 1 file changed, 63 insertions(+), 88 deletions(-) diff --git a/frontend/src/pages/Visualize.tsx b/frontend/src/pages/Visualize.tsx index 8bd27bd051..cf05a3358e 100644 --- a/frontend/src/pages/Visualize.tsx +++ b/frontend/src/pages/Visualize.tsx @@ -490,99 +490,82 @@ export function Visualize() { ? (import.meta.env.VITE_SPARQL_ENDPOINT || 'http://localhost:7878/query') : '/query'; // Always use relative URL in production + // SPARQL query aligned with actual RDF generated by oxigraph_sync.py and oxigraph_person_sync.py + // Namespaces match the Python sync scripts: + // - nde: for Custodian type + // - hc: for ghcid, isil predicates + // - hp: for person URIs const constructQuery = ` - PREFIX crm: - PREFIX hc: - PREFIX hcc: + PREFIX rdf: + PREFIX rdfs: PREFIX skos: PREFIX schema: PREFIX foaf: - PREFIX dcterms: PREFIX owl: - PREFIX prov: - + PREFIX org: + PREFIX geo: + PREFIX cidoc: + PREFIX nde: + PREFIX hc: + PREFIX hp: + CONSTRUCT { - # Core custodian data - ?custodian a crm:E39_Actor ; - skos:prefLabel ?label ; - skos:altLabel ?altLabel ; - hc:custodian_type ?type ; - dcterms:identifier ?dcId . + # Custodians - core data + ?custodian a nde:Custodian ; + rdfs:label ?label ; + skos:prefLabel ?prefLabel ; + schema:name ?name ; + hc:ghcid ?ghcid ; + hc:isil ?isil ; + schema:url ?website ; + foaf:homepage ?homepage ; + owl:sameAs ?wikidata . - # Location with coordinates - ?custodian crm:P53_has_former_or_current_location ?place . - ?place a schema:Place ; - schema:latitude ?lat ; - schema:longitude ?lon ; - schema:address ?address . + # Location (schema:location, not crm:P53) + ?custodian schema:location ?location . + ?location geo:lat ?lat ; + geo:long ?lon . - # Identifiers (creates edges: custodian -> identifier) - ?custodian crm:P48_has_preferred_identifier ?identifier . - ?identifier a crm:E42_Identifier ; - skos:inScheme ?scheme ; - skos:notation ?notation ; - schema:url ?identUrl . - - # Digital platform / website - ?custodian foaf:homepage ?platform . - ?platform a hcc:DigitalPlatform ; - schema:url ?platformUrl . - - # Social media profiles (creates edges: custodian -> social) - ?custodian foaf:account ?social . - ?social a foaf:OnlineAccount ; - hc:platform_type ?socialType ; - foaf:accountName ?accountName ; - foaf:accountServiceHomepage ?socialUrl . - - # Wikidata/VIAF sameAs links (creates edges) - ?custodian owl:sameAs ?sameAs . - - # GeoNames containedInPlace - ?custodian schema:containedInPlace ?geonames . + # Persons linked to custodians + ?person a schema:Person ; + rdfs:label ?personLabel ; + schema:name ?personName ; + schema:worksFor ?custodian ; + org:memberOf ?custodian ; + schema:jobTitle ?jobTitle . } WHERE { - ?custodian a crm:E39_Actor . - OPTIONAL { ?custodian skos:prefLabel ?label } - OPTIONAL { ?custodian skos:altLabel ?altLabel } - OPTIONAL { ?custodian hc:custodian_type ?type } - OPTIONAL { ?custodian dcterms:identifier ?dcId } - - # Location + # Get all custodians (nde:Custodian is the primary type) + ?custodian a nde:Custodian . + OPTIONAL { ?custodian rdfs:label ?label } + OPTIONAL { ?custodian skos:prefLabel ?prefLabel } + OPTIONAL { ?custodian schema:name ?name } + OPTIONAL { ?custodian hc:ghcid ?ghcid } + OPTIONAL { ?custodian hc:isil ?isil } + OPTIONAL { ?custodian schema:url ?website } + OPTIONAL { ?custodian foaf:homepage ?homepage } OPTIONAL { - ?custodian crm:P53_has_former_or_current_location ?place . - OPTIONAL { ?place schema:latitude ?lat } - OPTIONAL { ?place schema:longitude ?lon } - OPTIONAL { ?place schema:address ?address } + ?custodian owl:sameAs ?wikidata . + FILTER(STRSTARTS(STR(?wikidata), "http://www.wikidata.org/")) } - # Identifiers + # Location using schema:location (as generated by oxigraph_sync.py) OPTIONAL { - ?custodian crm:P48_has_preferred_identifier ?identifier . - OPTIONAL { ?identifier skos:inScheme ?scheme } - OPTIONAL { ?identifier skos:notation ?notation } - OPTIONAL { ?identifier schema:url ?identUrl } + ?custodian schema:location ?location . + OPTIONAL { ?location geo:lat ?lat } + OPTIONAL { ?location geo:long ?lon } } - # Digital platform + # Persons linked to custodians via schema:worksFor OPTIONAL { - ?custodian foaf:homepage ?platform . - OPTIONAL { ?platform schema:url ?platformUrl } + ?person a schema:Person ; + schema:worksFor ?custodian . + OPTIONAL { ?person rdfs:label ?personLabel } + OPTIONAL { ?person schema:name ?personName } + OPTIONAL { ?person schema:jobTitle ?jobTitle } + OPTIONAL { ?person org:memberOf ?custodian } } - - # Social media - OPTIONAL { - ?custodian foaf:account ?social . - OPTIONAL { ?social hc:platform_type ?socialType } - OPTIONAL { ?social foaf:accountName ?accountName } - OPTIONAL { ?social foaf:accountServiceHomepage ?socialUrl } - } - - # External links - OPTIONAL { ?custodian owl:sameAs ?sameAs } - OPTIONAL { ?custodian schema:containedInPlace ?geonames } } - LIMIT 500 `; let rdfData = ''; @@ -638,25 +621,17 @@ export function Visualize() { console.log(`Parsed: ${result.nodes.length} nodes, ${result.links.length} links`); loadGraphData(result); - // After loading, set filter to show just ONE random node type - // This prevents overwhelming users with all ~500 nodes + // Show ALL node types by default - no random filtering + // Users can filter down using the sidebar controls if needed if (result.nodes.length > 0) { // Extract unique node types from the result const uniqueTypes = new Set(result.nodes.map(n => n.type)); const typesArray = Array.from(uniqueTypes); - if (typesArray.length > 0) { - // Pick one random type - const randomIndex = Math.floor(Math.random() * typesArray.length); - const randomType = typesArray[randomIndex]; - - // Set filter to only show this one type - setFilters({ nodeTypes: new Set([randomType]) }); - - // Count nodes of this type - const nodesOfType = result.nodes.filter(n => n.type === randomType); - console.log(`RDF loaded: showing ${nodesOfType.length} nodes of type "${randomType}" (${result.nodes.length} total). Click "All" to see everything.`); - } + // Set filter to show ALL types by default + setFilters({ nodeTypes: new Set(typesArray) }); + + console.log(`RDF loaded: showing all ${result.nodes.length} nodes across ${typesArray.length} types: ${typesArray.join(', ')}`); } // Update cache state