MVP3: added dotted lines to show who connected to who
This commit is contained in:
@@ -9,6 +9,7 @@ export async function GET() {
|
|||||||
prisma.person.findMany(),
|
prisma.person.findMany(),
|
||||||
prisma.connection.findMany(),
|
prisma.connection.findMany(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const nameById = new Map(people.map((p: any) => [p.id, p.name]));
|
const nameById = new Map(people.map((p: any) => [p.id, p.name]));
|
||||||
|
|
||||||
const nodes = people.map((p: any) => ({
|
const nodes = people.map((p: any) => ({
|
||||||
@@ -19,21 +20,59 @@ export async function GET() {
|
|||||||
role: p.role,
|
role: p.role,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const edges = connections.map((c: any) => {
|
const edges: any[] = [];
|
||||||
const introducedByNames: string[] = Array.isArray(c.introducedByChain)
|
|
||||||
? c.introducedByChain.map((pid: string) => nameById.get(pid)).filter(Boolean)
|
for (const c of connections as any[]) {
|
||||||
: [];
|
const chain: string[] = Array.isArray(c.introducedByChain) ? c.introducedByChain : [];
|
||||||
return {
|
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,
|
id: c.id,
|
||||||
source: c.personAId,
|
source: c.personAId,
|
||||||
target: c.personBId,
|
target: c.personBId,
|
||||||
introducedByCount: c.introducedByChain.length,
|
introducedByCount: chain.length,
|
||||||
hasProvenance: c.introducedByChain.length > 0,
|
hasProvenance: true,
|
||||||
introducedByNames,
|
introducedByNames,
|
||||||
eventLabels: Array.isArray(c.eventLabels) ? c.eventLabels : [],
|
eventLabels: Array.isArray(c.eventLabels) ? c.eventLabels : [],
|
||||||
notes: c.notes ?? null,
|
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 });
|
return NextResponse.json({ nodes, edges }, { status: 200 });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -54,6 +54,10 @@ export default function Graph({ data, height = 600 }: Props) {
|
|||||||
e.introducedByNames && e.introducedByNames.length
|
e.introducedByNames && e.introducedByNames.length
|
||||||
? `Introduced by: ${e.introducedByNames.join(" → ")}`
|
? `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 {
|
return {
|
||||||
data: {
|
data: {
|
||||||
id: e.id,
|
id: e.id,
|
||||||
@@ -65,6 +69,7 @@ export default function Graph({ data, height = 600 }: Props) {
|
|||||||
eventLabels: e.eventLabels ?? [],
|
eventLabels: e.eventLabels ?? [],
|
||||||
notes: e.notes ?? null,
|
notes: e.notes ?? null,
|
||||||
edgeLabel: label,
|
edgeLabel: label,
|
||||||
|
...flags,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -117,6 +122,24 @@ export default function Graph({ data, height = 600 }: Props) {
|
|||||||
"line-color": "#16a34a", // green-600
|
"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",
|
selector: "edge:selected",
|
||||||
style: {
|
style: {
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ export const graphNodeSchema = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const graphEdgeSchema = z.object({
|
export const graphEdgeSchema = z.object({
|
||||||
id: z.string().uuid(),
|
id: z.string(),
|
||||||
source: z.string().uuid(),
|
source: z.string().uuid(),
|
||||||
target: z.string().uuid(),
|
target: z.string().uuid(),
|
||||||
introducedByCount: z.number().int().nonnegative(),
|
introducedByCount: z.number().int().nonnegative(),
|
||||||
@@ -55,6 +55,9 @@ export const graphEdgeSchema = z.object({
|
|||||||
introducedByNames: z.array(z.string()).default([]).optional(),
|
introducedByNames: z.array(z.string()).default([]).optional(),
|
||||||
eventLabels: z.array(z.string()).default([]).optional(),
|
eventLabels: z.array(z.string()).default([]).optional(),
|
||||||
notes: z.string().nullable().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({
|
export const graphResponseSchema = z.object({
|
||||||
|
|||||||
Reference in New Issue
Block a user