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.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) {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user