MVP3: added dotted lines to show who connected to who

This commit is contained in:
Maxime Van Hees
2025-11-14 22:01:49 +01:00
parent ae4b4405d2
commit 14cf3e052d
3 changed files with 75 additions and 10 deletions

View File

@@ -9,6 +9,7 @@ export async function GET() {
prisma.person.findMany(),
prisma.connection.findMany(),
]);
const nameById = new Map(people.map((p: any) => [p.id, p.name]));
const nodes = people.map((p: any) => ({
@@ -19,21 +20,59 @@ export async function GET() {
role: p.role,
}));
const edges = connections.map((c: any) => {
const introducedByNames: string[] = Array.isArray(c.introducedByChain)
? c.introducedByChain.map((pid: string) => nameById.get(pid)).filter(Boolean)
: [];
return {
const edges: any[] = [];
for (const c of connections as any[]) {
const chain: string[] = Array.isArray(c.introducedByChain) ? c.introducedByChain : [];
const introducedByNames: string[] = chain.map((pid: string) => nameById.get(pid)).filter(Boolean) as string[];
const hasProv = chain.length > 0;
if (!hasProv) {
// Plain direct edge (no introducer)
edges.push({
id: c.id,
source: c.personAId,
target: c.personBId,
introducedByCount: 0,
hasProvenance: false,
introducedByNames: [],
eventLabels: Array.isArray(c.eventLabels) ? c.eventLabels : [],
notes: c.notes ?? null,
});
continue;
}
// 1) Original A—C edge rendered as dotted "indirect" line
edges.push({
id: c.id,
source: c.personAId,
target: c.personBId,
introducedByCount: c.introducedByChain.length,
hasProvenance: c.introducedByChain.length > 0,
introducedByCount: chain.length,
hasProvenance: true,
introducedByNames,
eventLabels: Array.isArray(c.eventLabels) ? c.eventLabels : [],
notes: c.notes ?? null,
};
});
indirect: true,
});
// 2) Virtual path edges along introducer chain: A—B, B—…—C (solid)
const path = [c.personAId, ...chain, c.personBId];
for (let i = 0; i < path.length - 1; i++) {
const source = path[i];
const target = path[i + 1];
edges.push({
id: `virtual:${c.id}:${i}:${source}:${target}`,
source,
target,
introducedByCount: chain.length,
hasProvenance: true,
introducedByNames,
eventLabels: Array.isArray(c.eventLabels) ? c.eventLabels : [],
notes: c.notes ?? null,
virtual: true,
});
}
}
return NextResponse.json({ nodes, edges }, { status: 200 });
} catch (err) {

View File

@@ -54,6 +54,10 @@ export default function Graph({ data, height = 600 }: Props) {
e.introducedByNames && e.introducedByNames.length
? `Introduced by: ${e.introducedByNames.join(" → ")}`
: "";
// Only set flags when true so the style selector [indirect] matches by presence.
const flags: Record<string, any> = {};
if ((e as any).indirect) flags.indirect = "1";
if ((e as any).virtual) flags.virtual = "1";
return {
data: {
id: e.id,
@@ -65,6 +69,7 @@ export default function Graph({ data, height = 600 }: Props) {
eventLabels: e.eventLabels ?? [],
notes: e.notes ?? null,
edgeLabel: label,
...flags,
},
};
});
@@ -117,6 +122,24 @@ export default function Graph({ data, height = 600 }: Props) {
"line-color": "#16a34a", // green-600
},
},
// Dotted line for indirect A—C edge when there is an introducer
{
selector: "edge[indirect], edge[introducedByCount > 0][!virtual]",
style: {
// Make indirect A—C edge or any non-virtual edge with provenance visibly different
"line-style": "dashed",
"line-color": "#64748b", // slate-500 for contrast
width: 2,
opacity: 0.9,
},
},
// Keep virtual path edges solid (default)
{
selector: "edge[virtual]",
style: {
"line-style": "solid",
},
},
{
selector: "edge:selected",
style: {

View File

@@ -47,7 +47,7 @@ export const graphNodeSchema = z.object({
});
export const graphEdgeSchema = z.object({
id: z.string().uuid(),
id: z.string(),
source: z.string().uuid(),
target: z.string().uuid(),
introducedByCount: z.number().int().nonnegative(),
@@ -55,6 +55,9 @@ export const graphEdgeSchema = z.object({
introducedByNames: z.array(z.string()).default([]).optional(),
eventLabels: z.array(z.string()).default([]).optional(),
notes: z.string().nullable().optional(),
// Visualization flags
indirect: z.boolean().optional(), // true for the original A—C edge when an introducer exists (dotted)
virtual: z.boolean().optional(), // true for virtual path edges (A—B, B—C, ...)
});
export const graphResponseSchema = z.object({