1
0
mirror of https://github.com/home-assistant/frontend.git synced 2025-12-24 12:49:19 +00:00

Fix sankey diagram passthrough ordering (#28012)

This commit is contained in:
Petar Petrov
2025-11-26 10:45:32 +02:00
committed by GitHub
parent d767afb1e1
commit fb666a7553
2 changed files with 95 additions and 4 deletions

View File

@@ -14,6 +14,8 @@ interface PassThroughNode {
id: string; id: string;
value: number; value: number;
depth: number; depth: number;
sourceId: string;
targetId: string;
} }
interface GraphLink extends GraphEdge { interface GraphLink extends GraphEdge {
@@ -152,6 +154,8 @@ export function createPassThroughNode(
id: `${sourceId}-${targetId}-${depth}`, id: `${sourceId}-${targetId}-${depth}`,
value, value,
depth, depth,
sourceId,
targetId,
}; };
} }
@@ -237,6 +241,79 @@ export function groupNodesBySection(
return nodesPerSection; return nodesPerSection;
} }
export function sortNodesInSections(
nodesPerSection: Record<number, Node[]>,
depths: number[]
): Record<number, Node[]> {
const sortedSections: Record<number, Node[]> = {};
depths.forEach((depth, depthIndex) => {
const sectionNodes = nodesPerSection[depth] || [];
// Sort nodes to minimize crossings
const sortedNodes = [...sectionNodes].sort((a, b) => {
const aIsPassthrough = isPassThroughNode(a);
const bIsPassthrough = isPassThroughNode(b);
// Both are passthrough nodes - sort by source position
if (aIsPassthrough && bIsPassthrough) {
// Find positions of source nodes in previous section (use already sorted section)
if (depthIndex > 0) {
const prevDepth = depths[depthIndex - 1];
const prevSection =
sortedSections[prevDepth] || nodesPerSection[prevDepth] || [];
const aSourceIndex = prevSection.findIndex((n) => {
const nodeId = isPassThroughNode(n) ? n.id : (n as GraphNode).id;
return nodeId === a.sourceId;
});
const bSourceIndex = prevSection.findIndex((n) => {
const nodeId = isPassThroughNode(n) ? n.id : (n as GraphNode).id;
return nodeId === b.sourceId;
});
if (
aSourceIndex !== bSourceIndex &&
aSourceIndex !== -1 &&
bSourceIndex !== -1
) {
return aSourceIndex - bSourceIndex;
}
}
// Fall back to target node positions in next section (not sorted yet, use original)
if (depthIndex < depths.length - 1) {
const nextDepth = depths[depthIndex + 1];
const nextSection = nodesPerSection[nextDepth] || [];
const aTargetIndex = nextSection.findIndex((n) => {
const nodeId = isPassThroughNode(n) ? n.id : (n as GraphNode).id;
return nodeId === a.targetId;
});
const bTargetIndex = nextSection.findIndex((n) => {
const nodeId = isPassThroughNode(n) ? n.id : (n as GraphNode).id;
return nodeId === b.targetId;
});
if (
aTargetIndex !== bTargetIndex &&
aTargetIndex !== -1 &&
bTargetIndex !== -1
) {
return aTargetIndex - bTargetIndex;
}
}
}
return 0;
});
sortedSections[depth] = sortedNodes;
});
return sortedSections;
}
export function createSectionNodes(nodes: Node[]): SectionNode[] { export function createSectionNodes(nodes: Node[]): SectionNode[] {
return nodes.map( return nodes.map(
(node: Node): SectionNode => ({ (node: Node): SectionNode => ({
@@ -337,10 +414,11 @@ function processNodes(
); );
const nodesPerSection = groupNodesBySection(nodes, passThroughNodes); const nodesPerSection = groupNodesBySection(nodes, passThroughNodes);
const sortedNodesPerSection = sortNodesInSections(nodesPerSection, depths);
let globalValueToSizeRatio = 0; let globalValueToSizeRatio = 0;
const sections = depths.map((depth) => { const sections = depths.map((depth) => {
const sectionNodes = createSectionNodes(nodesPerSection[depth] || []); const sectionNodes = createSectionNodes(sortedNodesPerSection[depth] || []);
const availableSpace = sectionSize - (sectionNodes.length + 1) * nodeGap; const availableSpace = sectionSize - (sectionNodes.length + 1) * nodeGap;
const totalValue = sectionNodes.reduce( const totalValue = sectionNodes.reduce(
(acc: number, node: SectionNode) => acc + node.value, (acc: number, node: SectionNode) => acc + node.value,

View File

@@ -67,6 +67,8 @@ describe("Sankey Layout Functions", () => {
id: "test", id: "test",
value: 10, value: 10,
depth: 1, depth: 1,
sourceId: "source",
targetId: "target",
}; };
expect(isPassThroughNode(passThroughNode)).toBe(true); expect(isPassThroughNode(passThroughNode)).toBe(true);
}); });
@@ -142,7 +144,14 @@ describe("Sankey Layout Functions", () => {
]; ];
const passThroughNodes = [ const passThroughNodes = [
{ id: "pt1", depth: 1, passThrough: true, value: 5 }, {
id: "pt1",
depth: 1,
passThrough: true,
value: 5,
sourceId: "node1",
targetId: "node2",
},
]; ];
const result = groupNodesBySection( const result = groupNodesBySection(
@@ -195,6 +204,8 @@ describe("Sankey Layout Functions", () => {
passThrough: true, passThrough: true,
value: 5, value: 5,
depth: 1, depth: 1,
sourceId: "source",
targetId: "target",
}; };
const result = createSectionNodes([passThroughNode]); const result = createSectionNodes([passThroughNode]);
@@ -316,13 +327,15 @@ describe("Sankey Layout Functions", () => {
describe("createPassThroughNode", () => { describe("createPassThroughNode", () => {
it("should create a pass-through node", () => { it("should create a pass-through node", () => {
const result = createPassThroughNode("source-target", "section1", 2, 15); const result = createPassThroughNode("source", "target", 2, 15);
expect(result).toEqual({ expect(result).toEqual({
passThrough: true, passThrough: true,
id: "source-target-section1-2", id: "source-target-2",
value: 15, value: 15,
depth: 2, depth: 2,
sourceId: "source",
targetId: "target",
}); });
}); });
}); });