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:
@@ -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,
|
||||||
|
|||||||
@@ -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",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user