Skip to content

Commit 6b6f6cc

Browse files
jonradoffclaude
andcommitted
Show event counts and percentage flow-through on Sankey nodes
Each node in the Sankey diagram now displays the event count and, for child events, the percentage conversion from the parent stage. Backend includes count per node in the sankey response; frontend computes the flow-through percentage from parent-child relationships. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent e82204a commit 6b6f6cc

3 files changed

Lines changed: 51 additions & 11 deletions

File tree

backend/internal/api/handlers/event_definitions.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -341,16 +341,19 @@ func (h *EventDefinitionsHandler) GetSankeyData(w http.ResponseWriter, r *http.R
341341
}
342342
}
343343

344-
// Build ordered node list.
344+
// Build ordered node list (name + ID tracking for count lookup).
345345
type sankeyNode struct {
346-
Name string `json:"name"`
346+
Name string `json:"name"`
347+
Count int64 `json:"count"`
347348
}
348349
var nodes []sankeyNode
349350
nodeIndex := map[primitive.ObjectID]int{}
351+
nodeDefIDs := []primitive.ObjectID{} // parallel to nodes for ID lookup
350352
for _, d := range allDefs {
351353
if participatingIDs[d.ID] {
352354
nodeIndex[d.ID] = len(nodes)
353355
nodes = append(nodes, sankeyNode{Name: d.Name})
356+
nodeDefIDs = append(nodeDefIDs, d.ID)
354357
}
355358
}
356359

@@ -388,6 +391,11 @@ func (h *EventDefinitionsHandler) GetSankeyData(w http.ResponseWriter, r *http.R
388391
}
389392
}
390393

394+
// Populate node counts.
395+
for i := range nodes {
396+
nodes[i].Count = counts[nodes[i].Name]
397+
}
398+
391399
// Build links: parent → child with value = child event count.
392400
type sankeyLink struct {
393401
Source int `json:"source"`

frontend/src/pages/admin/PMPage.tsx

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -416,14 +416,25 @@ function EngagementTab() {
416416

417417
type EventSubTab = 'flow' | 'graph';
418418

419-
// Custom Sankey node renderer with labels.
420-
function SankeyNode({ x, y, width, height, payload }: { x: number; y: number; width: number; height: number; payload: { name: string; value?: number } }) {
419+
// Custom Sankey node renderer with labels showing count and flow-through %.
420+
function SankeyNode({ x, y, width, height, payload }: {
421+
x: number; y: number; width: number; height: number;
422+
payload: { name: string; count?: number; pct?: number };
423+
}) {
424+
const count = payload.count ?? 0;
425+
const pct = payload.pct;
426+
const label = pct !== undefined
427+
? `${payload.name} ${formatNum(count)} (${pct.toFixed(1)}%)`
428+
: `${payload.name} ${formatNum(count)}`;
421429
return (
422430
<g>
423431
<rect x={x} y={y} width={width} height={height} fill="#6366f1" stroke="#818cf8" strokeWidth={1} rx={3} />
424-
<text x={x + width + 8} y={y + height / 2} textAnchor="start" dominantBaseline="central" fill="#e2e8f0" fontSize={12}>
432+
<text x={x + width + 8} y={y + height / 2} textAnchor="start" dominantBaseline="central" fill="#e2e8f0" fontSize={12} fontWeight={500}>
425433
{payload.name}
426434
</text>
435+
<text x={x + width + 8} y={y + height / 2 + 16} textAnchor="start" dominantBaseline="central" fill="#94a3b8" fontSize={11}>
436+
{formatNum(count)}{pct !== undefined ? ` (${pct.toFixed(1)}%)` : ''}
437+
</text>
427438
</g>
428439
);
429440
}
@@ -550,22 +561,43 @@ function FlowSubTab({ range, definitions, onEdit, onDelete }: {
550561
return defMap.get(def.parentId)?.name ?? null;
551562
};
552563

564+
// Enrich nodes with percentage flow-through from parent stage.
565+
const enrichedData = sankeyData && sankeyData.hasDependencies ? (() => {
566+
// Build a map: target node index → source node index (from first incoming link).
567+
const parentOf = new Map<number, number>();
568+
for (const link of sankeyData.links) {
569+
if (!parentOf.has(link.target)) {
570+
parentOf.set(link.target, link.source);
571+
}
572+
}
573+
const enrichedNodes = sankeyData.nodes.map((node, i) => {
574+
const parentIdx = parentOf.get(i);
575+
let pct: number | undefined;
576+
if (parentIdx !== undefined) {
577+
const parentCount = sankeyData.nodes[parentIdx]?.count ?? 0;
578+
pct = parentCount > 0 ? (node.count / parentCount) * 100 : 0;
579+
}
580+
return { ...node, pct };
581+
});
582+
return { ...sankeyData, nodes: enrichedNodes };
583+
})() : null;
584+
553585
return (
554586
<div className="space-y-6">
555587
{isLoading ? (
556588
<LoadingSpinner size="lg" className="py-10" />
557-
) : sankeyData && sankeyData.hasDependencies && sankeyData.nodes.length > 0 && sankeyData.links.length > 0 ? (
589+
) : enrichedData && enrichedData.nodes.length > 0 && enrichedData.links.length > 0 ? (
558590
<Card className="p-4">
559591
<h3 className="text-sm font-medium text-dark-400 mb-4">Event Flow</h3>
560592
<div className="overflow-x-auto">
561593
<Sankey
562594
width={900}
563-
height={Math.max(300, sankeyData.nodes.length * 50)}
564-
data={sankeyData}
565-
nodePadding={40}
595+
height={Math.max(300, enrichedData.nodes.length * 60)}
596+
data={enrichedData}
597+
nodePadding={50}
566598
nodeWidth={10}
567599
linkCurvature={0.5}
568-
margin={{ top: 20, right: 160, bottom: 20, left: 20 }}
600+
margin={{ top: 20, right: 200, bottom: 20, left: 20 }}
569601
node={<SankeyNode x={0} y={0} width={0} height={0} payload={{ name: '' }} />}
570602
>
571603
<Tooltip contentStyle={tooltipStyle} labelStyle={tooltipLabelStyle} />

frontend/src/types/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -686,7 +686,7 @@ export interface EventDefinition {
686686
}
687687

688688
export interface SankeyData {
689-
nodes: { name: string }[];
689+
nodes: { name: string; count: number }[];
690690
links: { source: number; target: number; value: number }[];
691691
hasDependencies: boolean;
692692
}

0 commit comments

Comments
 (0)