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
This commit is contained in:
kempersc 2025-12-17 08:55:26 +01:00
parent e0dd847491
commit 5fe692296d

View file

@ -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: <https://nde.nl/ontology/hc/class/> for Custodian type
// - hc: <https://w3id.org/heritage/custodian/> for ghcid, isil predicates
// - hp: <https://w3id.org/heritage/person/> for person URIs
const constructQuery = `
PREFIX crm: <http://www.cidoc-crm.org/cidoc-crm/>
PREFIX hc: <https://nde.nl/ontology/hc/>
PREFIX hcc: <https://nde.nl/ontology/hc/class/>
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
PREFIX skos: <http://www.w3.org/2004/02/skos/core#>
PREFIX schema: <http://schema.org/>
PREFIX foaf: <http://xmlns.com/foaf/0.1/>
PREFIX dcterms: <http://purl.org/dc/terms/>
PREFIX owl: <http://www.w3.org/2002/07/owl#>
PREFIX prov: <http://www.w3.org/ns/prov#>
PREFIX org: <http://www.w3.org/ns/org#>
PREFIX geo: <http://www.w3.org/2003/01/geo/wgs84_pos#>
PREFIX cidoc: <http://www.cidoc-crm.org/cidoc-crm/>
PREFIX nde: <https://nde.nl/ontology/hc/class/>
PREFIX hc: <https://w3id.org/heritage/custodian/>
PREFIX hp: <https://w3id.org/heritage/person/>
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