Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
16 changes: 9 additions & 7 deletions src/api/endpoints/apiActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,23 +20,25 @@ export const createPostRequest = <T = any, D = any>(endpoint: string, headers :

export const createGetRequest = <T = any, P = any>(endpoint: string, contentType?: string) => {
return (params?: P, options?: SecondParameter<typeof customInstance>, signal?: AbortSignal) => {
const absUrl = endpoint.startsWith('http')
? endpoint
: new URL(endpoint.startsWith('/') ? endpoint : `/${endpoint}`, window.location.origin).toString();

const config: AxiosRequestConfig = {
url: endpoint,
url: absUrl,
method: "GET",
params,
signal,
withCredentials: true
}
};

if (contentType) {
config.headers = {
...config.headers,
"Content-Type": contentType,
}
Accept: contentType,
};
}

return customInstance<T>(config, options).then(response => {
return response;
});
return customInstance<T>(config, options).then(response => response);
}
}
68 changes: 52 additions & 16 deletions src/api/endpoints/apiService.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { createPostRequest, createGetRequest } from "./apiActions";
import { API_CONFIG } from "../../config";
import termParser from "../../parsers/termParser";
import { jsonldToTriplesAndEdges, PART_OF_IRI } from './hiearchies-parser'
import { jsonldToTriplesAndEdges, PART_OF_IRI } from '../../parsers/hierarchies-parser'
import { buildPredicateGroupsForFocus } from "../../parsers/predicateParser";

export interface LoginRequest {
username: string
Expand Down Expand Up @@ -302,7 +303,7 @@ export const getVariant = (group: string, term: string) => {
return createGetRequest<any, any>(`/${group}/variant/${term}`, "application/json")();
};

export const getTermHierarchies = async ({
export const getTermPredicates = async ({
groupname,
termId,
objToSub = true,
Expand All @@ -311,20 +312,55 @@ export const getTermHierarchies = async ({
termId: string;
objToSub?: boolean;
}) => {
const base = `/${groupname}/query/transitive/${encodeURIComponent(termId)}/ilx.partOf:`;
const url1 = `${base}?obj-to-sub=${objToSub}`;
const url2 = `${base}.jsonld?obj-to-sub=${objToSub}`;
const url = new URL(
`/${groupname}/query/transitive/${encodeURIComponent(termId)}/ilx.partOf:?obj-to-sub=true`,
window.location.origin
).toString();

const resp = await fetch(url, {
headers: { Accept: "application/ld+json" },
credentials: "include",
});
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
const ct = resp.headers.get("content-type") || "";
if (!/application\/(ld\+json|json)/i.test(ct)) {
throw new Error(`Server did not return JSON-LD (content-type: ${ct || "n/a"})`);
}
const jsonld = await resp.json();

try {
const res1 = await createGetRequest<any, any>(url1, 'application/ld+json')();
return jsonldToTriplesAndEdges(res1);
} catch {
try {
const res2 = await createGetRequest<any, any>(url2, 'application/ld+json')();
return jsonldToTriplesAndEdges(res2);
} catch (e2: any) {
console.error('getTermHierarchies failed', e2);
return { error: true, message: e2?.message || String(e2) };
}
// Build gold-standard predicate groups for the focus owl:Class
const predicates = buildPredicateGroupsForFocus(jsonld, termId);
return predicates;
};

export const getTermHierarchies = async ({
groupname,
termId,
objToSub = false,
}: {
groupname: string;
termId: string;
objToSub?: boolean;
}) => {
const url = new URL(
`/${groupname}/query/transitive/${encodeURIComponent(termId)}/ilx.partOf:?obj-to-sub=${objToSub}`,
window.location.origin
).toString();

const resp = await fetch(url, {
headers: { Accept: "application/ld+json" },
credentials: "include",
});
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
const ct = resp.headers.get("content-type") || "";
if (!/application\/(ld\+json|json)/i.test(ct)) {
throw new Error(`Server did not return JSON-LD (content-type: ${ct || "n/a"})`);
}
const jsonld = await resp.json();

// Only what Hierarchy needs
const parsed = jsonldToTriplesAndEdges(jsonld);
const triples = Array.isArray(parsed) ? parsed : (parsed?.triples || parsed?.edges || []);

return { triples };
};
85 changes: 0 additions & 85 deletions src/api/endpoints/hiearchies-parser.ts

This file was deleted.

139 changes: 79 additions & 60 deletions src/components/GraphViewer/GraphStructure.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,96 +15,115 @@ const safe = (v, fallback = "unknown") => {
function normalizeRows(pred) {
if (!pred) return [];

// 1) new shape from getTermHierarchies grouping
// new shapes
if (Array.isArray(pred.rows) && pred.rows.length) return pred.rows;
if (Array.isArray(pred.values) && pred.values.length) return pred.values;

// 2) legacy tableData
// legacy tableData (strings only)
if (Array.isArray(pred.tableData) && pred.tableData.length) {
return pred.tableData.map(r => ({
return pred.tableData.map((r) => ({
subject: r.subject,
subjectId: r.subject, // no id in legacy, use label
subjectId: r.subject || r.subjectId,
object: r.object,
objectId: r.object,
objectId: r.object || r.objectId,
}));
}

// 3) edges fallback
// edges fallback
if (Array.isArray(pred.edges) && pred.edges.length) {
return pred.edges.map(e => ({
subject: e?.from?.label || e?.from?.id,
subjectId: e?.from?.id || e?.from?.label,
object: e?.to?.label || e?.to?.id,
objectId: e?.to?.id || e?.to?.label,
return pred.edges.map((e) => ({
subject: (e?.from && (e.from.label || e.from.id)) || "unknown",
subjectId: (e?.from && (e.from.id || e.from.label)) || "unknown",
object: (e?.to && (e.to.label || e.to.id)) || "unknown",
objectId: (e?.to && (e.to.id || e.to.label)) || "unknown",
}));
}

return [];
}

// Pick a root subject (most frequent). Fallback to first row.
function pickRoot(rows) {
if (!rows.length) return { key: "unknown", label: "unknown" };
const counts = new Map();
const firstLabelById = new Map();
for (const r of rows) {
const id = r.subjectId || r.subject;
if (!id) continue;
counts.set(id, (counts.get(id) || 0) + 1);
if (!firstLabelById.has(id)) firstLabelById.set(id, r.subject || r.subjectId || id);
}
let rootKey = null, max = -1;
for (const [k, v] of counts.entries()) {
if (v > max) { max = v; rootKey = k; }
}
if (!rootKey) {
const r0 = rows[0];
const id = r0.subjectId || r0.subject || "unknown";
return { key: id, label: r0.subject || id };
}
return { key: rootKey, label: firstLabelById.get(rootKey) || rootKey };
}

// Build Root → Predicate → Unique Objects
/**
* Behavior:
* - If all rows share the same subject → keep old layout:
* Root(Subject) → Predicate → Unique Objects
* - If multiple subjects → show all subjects:
* Root(Predicate) → Subject_1 → Objects
* Subject_2 → Objects
*/
export const getGraphStructure = (pred) => {
const rows = normalizeRows(pred);
if (!rows.length) {
return { name: "No data", id: "no-data", type: ROOT, value: 0, children: [] };
}

const { key: rootKey, label: rootLabel } = pickRoot(rows);
const forRoot = rows.filter(r => (r.subjectId || r.subject) === rootKey);
// Bucket rows by subject
const subjMap = new Map(); // subjectId -> { label, rows[] }
for (const r of rows) {
const sid = safe(r.subjectId || r.subject);
const slabel = safe(r.subject);
if (!subjMap.has(sid)) subjMap.set(sid, { label: slabel, rows: [] });
subjMap.get(sid).rows.push({
subject: slabel,
subjectId: sid,
object: safe(r.object),
objectId: safe(r.objectId || r.object),
});
}

const uniqueSubjects = Array.from(subjMap.keys());
const predicateLabel = safe(pred?.title, "predicate");
const predicateId = predicateLabel;

const seen = new Set();
const objects = [];
for (const r of forRoot) {
const oid = r.objectId || r.object;
if (!oid) continue;
if (seen.has(oid)) continue;
seen.add(oid);
objects.push({
name: safe(r.object),
id: safe(oid),
type: OBJECT,
children: [],
// SINGLE-SUBJECT layout (backwards-compatible)
if (uniqueSubjects.length === 1) {
const sid = uniqueSubjects[0];
const bucket = subjMap.get(sid);
const seen = new Set();
const objects = [];
for (const r of bucket.rows) {
const oid = r.objectId;
if (!oid || seen.has(oid)) continue;
seen.add(oid);
objects.push({ name: safe(r.object), id: oid, type: OBJECT, children: [] });
}
return {
name: bucket.label,
id: sid,
type: ROOT,
value: bucket.rows.length || pred?.count || 0,
children: [
{ name: predicateLabel, id: predicateId, type: PREDICATE, children: objects },
],
};
}

// MULTI-SUBJECT layout
const subjectNodes = [];
for (const sid of uniqueSubjects) {
const bucket = subjMap.get(sid);
const seen = new Set();
const objects = [];
for (const r of bucket.rows) {
const oid = r.objectId;
if (!oid || seen.has(oid)) continue;
seen.add(oid);
// ensure uniqueness under the predicate root
objects.push({ name: safe(r.object), id: `${sid}::${oid}`, type: OBJECT, children: [] });
}
subjectNodes.push({
name: bucket.label,
id: sid,
type: SUBJECT,
value: bucket.rows.length,
children: objects,
});
}

return {
name: safe(rootLabel),
id: safe(rootKey),
name: predicateLabel,
id: predicateId,
type: ROOT,
value: forRoot.length || pred?.count || 0,
children: [
{
name: predicateLabel,
id: predicateId,
type: PREDICATE,
children: objects,
},
],
value: rows.length || pred?.count || 0,
children: subjectNodes,
};
};
Loading