mirror of
https://github.com/semantica-agi/semantica.git
synced 2026-08-29 04:26:20 +00:00
fix(explorer): complete edge label rendering
This commit is contained in:
@@ -745,6 +745,12 @@ function buildEffectAvailability(
|
|||||||
? { enabled: true, available: true, reason: "Panel enabled" }
|
? { enabled: true, available: true, reason: "Panel enabled" }
|
||||||
: { enabled: false, available: false, reason: "Disabled by toggle" };
|
: { enabled: false, available: false, reason: "Disabled by toggle" };
|
||||||
|
|
||||||
|
// #1009: edge labels are immediately available once the graph is loaded —
|
||||||
|
// they have no async analytics or zoom-tier dependency.
|
||||||
|
const edgeLabels = effectsState.edgeLabelsEnabled
|
||||||
|
? { enabled: true, available: true, reason: "Ready" }
|
||||||
|
: { enabled: false, available: false, reason: "Disabled by toggle" };
|
||||||
|
|
||||||
const diagnostics = !GRAPH_THEME.effects.diagnostics.enabledInDev
|
const diagnostics = !GRAPH_THEME.effects.diagnostics.enabledInDev
|
||||||
? { enabled: false, available: false, reason: "Disabled in production" }
|
? { enabled: false, available: false, reason: "Disabled in production" }
|
||||||
: effectsState.diagnosticsEnabled
|
: effectsState.diagnosticsEnabled
|
||||||
@@ -762,6 +768,7 @@ function buildEffectAvailability(
|
|||||||
communities,
|
communities,
|
||||||
centrality,
|
centrality,
|
||||||
legend,
|
legend,
|
||||||
|
edgeLabels,
|
||||||
diagnostics,
|
diagnostics,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -1218,7 +1225,9 @@ function applySceneState(
|
|||||||
// #1009: Sigma's edge label renderer draws data.label — the graph
|
// #1009: Sigma's edge label renderer draws data.label — the graph
|
||||||
// stores the relationship type in edgeType, which the renderer never
|
// stores the relationship type in edgeType, which the renderer never
|
||||||
// saw, so enabling renderEdgeLabels alone left edges blank.
|
// saw, so enabling renderEdgeLabels alone left edges blank.
|
||||||
label: resolvedStyle.hidden ? undefined : String(attrs.edgeType ?? data.label ?? ""),
|
// Use || rather than ?? so that an empty-string edgeType (possible
|
||||||
|
// when the API returns type: "") does not produce a blank label.
|
||||||
|
label: resolvedStyle.hidden ? undefined : String(attrs.edgeType || data.label || ""),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -2150,7 +2150,7 @@ function aggregateDisplayGraph(graphRef: GraphRef): Graph<NodeAttributes, EdgeAt
|
|||||||
const rawEdgeIds = entries.flatMap(({ edgeId, attrs }) => collectRawEdgeIds(attrs, edgeId));
|
const rawEdgeIds = entries.flatMap(({ edgeId, attrs }) => collectRawEdgeIds(attrs, edgeId));
|
||||||
const typeCounts = new Map<string, number>();
|
const typeCounts = new Map<string, number>();
|
||||||
entries.forEach(({ attrs }) => {
|
entries.forEach(({ attrs }) => {
|
||||||
const edgeType = String(attrs.edgeType ?? "related_to");
|
const edgeType = String(attrs.edgeType || "related_to");
|
||||||
typeCounts.set(edgeType, (typeCounts.get(edgeType) ?? 0) + 1);
|
typeCounts.set(edgeType, (typeCounts.get(edgeType) ?? 0) + 1);
|
||||||
});
|
});
|
||||||
const dominantEdgeType = [...typeCounts.entries()].sort((left, right) => right[1] - left[1])[0]?.[0] ?? representative.attrs.edgeType ?? "related_to";
|
const dominantEdgeType = [...typeCounts.entries()].sort((left, right) => right[1] - left[1])[0]?.[0] ?? representative.attrs.edgeType ?? "related_to";
|
||||||
@@ -2170,7 +2170,7 @@ function aggregateDisplayGraph(graphRef: GraphRef): Graph<NodeAttributes, EdgeAt
|
|||||||
dominantEdgeType: String(dominantEdgeType),
|
dominantEdgeType: String(dominantEdgeType),
|
||||||
representativeWeight: Number(representative.attrs.weight ?? 1),
|
representativeWeight: Number(representative.attrs.weight ?? 1),
|
||||||
weight: Number(representative.attrs.weight ?? 1),
|
weight: Number(representative.attrs.weight ?? 1),
|
||||||
edgeType: String(representative.attrs.edgeType ?? dominantEdgeType ?? "related_to"),
|
edgeType: String(representative.attrs.edgeType || dominantEdgeType || "related_to"),
|
||||||
parallelCount: rawEdgeIds.length,
|
parallelCount: rawEdgeIds.length,
|
||||||
familySize: rawEdgeIds.length,
|
familySize: rawEdgeIds.length,
|
||||||
bundleKind: isBidirectionalBundle ? "bidirectional" : "parallel",
|
bundleKind: isBidirectionalBundle ? "bidirectional" : "parallel",
|
||||||
@@ -2280,7 +2280,7 @@ function buildCommunityGroupedGraph(): GraphDisplayResult {
|
|||||||
};
|
};
|
||||||
bucket.rawEdgeIds.push(String(edgeId));
|
bucket.rawEdgeIds.push(String(edgeId));
|
||||||
bucket.weight = Math.max(bucket.weight, Number((attrs as EdgeAttributes).weight ?? 1));
|
bucket.weight = Math.max(bucket.weight, Number((attrs as EdgeAttributes).weight ?? 1));
|
||||||
const edgeType = String((attrs as EdgeAttributes).edgeType ?? "related_to");
|
const edgeType = String((attrs as EdgeAttributes).edgeType || "related_to");
|
||||||
bucket.typeCounts.set(edgeType, (bucket.typeCounts.get(edgeType) ?? 0) + 1);
|
bucket.typeCounts.set(edgeType, (bucket.typeCounts.get(edgeType) ?? 0) + 1);
|
||||||
groupedEdges.set(key, bucket);
|
groupedEdges.set(key, bucket);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -151,7 +151,9 @@ export const explorationEffectsPlugin: GraphPlugin = {
|
|||||||
? "pathFlow"
|
? "pathFlow"
|
||||||
: row.key === "lensEnabled"
|
: row.key === "lensEnabled"
|
||||||
? "lens"
|
? "lens"
|
||||||
: "legend"
|
: row.key === "edgeLabelsEnabled"
|
||||||
|
? "edgeLabels"
|
||||||
|
: "legend"
|
||||||
] ?? {
|
] ?? {
|
||||||
enabled: effectsState[row.key],
|
enabled: effectsState[row.key],
|
||||||
available: false,
|
available: false,
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ const AVAILABILITY_KEYS: Record<GraphEffectToggle, keyof GraphDiagnosticsSnapsho
|
|||||||
communitiesEnabled: "communities",
|
communitiesEnabled: "communities",
|
||||||
centralityEnabled: "centrality",
|
centralityEnabled: "centrality",
|
||||||
legendEnabled: "legend",
|
legendEnabled: "legend",
|
||||||
edgeLabelsEnabled: "edge-labels",
|
edgeLabelsEnabled: "edgeLabels",
|
||||||
diagnosticsEnabled: "diagnostics",
|
diagnosticsEnabled: "diagnostics",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -188,6 +188,7 @@ export interface GraphDiagnosticsSnapshot {
|
|||||||
communities: GraphEffectAvailability;
|
communities: GraphEffectAvailability;
|
||||||
centrality: GraphEffectAvailability;
|
centrality: GraphEffectAvailability;
|
||||||
legend: GraphEffectAvailability;
|
legend: GraphEffectAvailability;
|
||||||
|
edgeLabels: GraphEffectAvailability;
|
||||||
diagnostics: GraphEffectAvailability;
|
diagnostics: GraphEffectAvailability;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1061,3 +1061,155 @@ test("checkGroupedViewAvailability returns available when communities exist", ()
|
|||||||
assert.equal(result.reason, null);
|
assert.equal(result.reason, null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// ── #1009: edge label data-path regression tests ─────────────────────────────
|
||||||
|
|
||||||
|
test("resolveDisplayGraph parallel-bundle preserves edgeType on aggregated edge", () => {
|
||||||
|
addNode("a");
|
||||||
|
addNode("b");
|
||||||
|
batchMergeEdges([
|
||||||
|
{ id: "e1", source: "a", target: "b", attributes: { edgeType: "causes", weight: 1, properties: {} } },
|
||||||
|
{ id: "e2", source: "a", target: "b", attributes: { edgeType: "causes", weight: 2, properties: {} } },
|
||||||
|
]);
|
||||||
|
|
||||||
|
const { graph: displayGraph } = resolveDisplayGraph("", [], [], "full", { aggregationEnabled: true });
|
||||||
|
assert.equal(displayGraph.size, 1);
|
||||||
|
|
||||||
|
const edgeId = displayGraph.edges()[0];
|
||||||
|
const attrs = displayGraph.getEdgeAttributes(edgeId) as { edgeType?: string; isAggregated?: boolean };
|
||||||
|
assert.equal(attrs.isAggregated, true);
|
||||||
|
// The aggregated representative must carry the relationship text through to
|
||||||
|
// the edgeReducer's label assignment.
|
||||||
|
assert.equal(typeof attrs.edgeType, "string");
|
||||||
|
assert.ok((attrs.edgeType ?? "").length > 0, "aggregated edge must have a non-empty edgeType");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("resolveDisplayGraph parallel-bundle picks dominant edgeType across mixed types", () => {
|
||||||
|
addNode("a");
|
||||||
|
addNode("b");
|
||||||
|
batchMergeEdges([
|
||||||
|
{ id: "e1", source: "a", target: "b", attributes: { edgeType: "inhibits", weight: 1, properties: {} } },
|
||||||
|
{ id: "e2", source: "a", target: "b", attributes: { edgeType: "inhibits", weight: 1, properties: {} } },
|
||||||
|
{ id: "e3", source: "a", target: "b", attributes: { edgeType: "activates", weight: 1, properties: {} } },
|
||||||
|
]);
|
||||||
|
|
||||||
|
const { graph: displayGraph } = resolveDisplayGraph("", [], [], "full", { aggregationEnabled: true });
|
||||||
|
const edgeId = displayGraph.edges()[0];
|
||||||
|
const attrs = displayGraph.getEdgeAttributes(edgeId) as { edgeType?: string; dominantEdgeType?: string };
|
||||||
|
// "inhibits" appears twice so it must be the dominant type.
|
||||||
|
assert.equal(attrs.edgeType, "inhibits");
|
||||||
|
assert.equal(attrs.dominantEdgeType, "inhibits");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("resolveDisplayGraph grouped view community edges carry non-empty edgeType", () => {
|
||||||
|
const left = ["g1", "g2", "g3", "g4"];
|
||||||
|
const right = ["h1", "h2", "h3", "h4"];
|
||||||
|
[...left, ...right].forEach((nodeId, index) => addNode(nodeId, index < left.length ? "left" : "right"));
|
||||||
|
|
||||||
|
let edgeIndex = 0;
|
||||||
|
for (let i = 0; i < left.length; i += 1) {
|
||||||
|
for (let j = 0; j < left.length; j += 1) {
|
||||||
|
if (i !== j) {
|
||||||
|
batchMergeEdges([{
|
||||||
|
id: `lg-${edgeIndex++}`,
|
||||||
|
source: left[i],
|
||||||
|
target: left[j],
|
||||||
|
attributes: { edgeType: "co_occurs", weight: 3, properties: {} },
|
||||||
|
}]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (let i = 0; i < right.length; i += 1) {
|
||||||
|
for (let j = 0; j < right.length; j += 1) {
|
||||||
|
if (i !== j) {
|
||||||
|
batchMergeEdges([{
|
||||||
|
id: `rg-${edgeIndex++}`,
|
||||||
|
source: right[i],
|
||||||
|
target: right[j],
|
||||||
|
attributes: { edgeType: "co_occurs", weight: 3, properties: {} },
|
||||||
|
}]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
batchMergeEdges([{ id: "bridge-g", source: "g1", target: "h1", attributes: { edgeType: "interacts_with", weight: 0.1, properties: {} } }]);
|
||||||
|
|
||||||
|
const { graph: displayGraph, state } = resolveDisplayGraph("", [], [], "grouped", { aggregationEnabled: true });
|
||||||
|
assert.equal(state.groupedViewAvailable, true);
|
||||||
|
|
||||||
|
const communityEdges = displayGraph.edges().filter((edgeId) => {
|
||||||
|
const attrs = displayGraph.getEdgeAttributes(edgeId) as { bundleKind?: string };
|
||||||
|
return attrs.bundleKind === "community";
|
||||||
|
});
|
||||||
|
assert.ok(communityEdges.length > 0, "expected at least one community bundle edge");
|
||||||
|
|
||||||
|
for (const edgeId of communityEdges) {
|
||||||
|
const attrs = displayGraph.getEdgeAttributes(edgeId) as { edgeType?: string };
|
||||||
|
assert.equal(typeof attrs.edgeType, "string");
|
||||||
|
assert.ok((attrs.edgeType ?? "").length > 0, `community edge ${edgeId} must have a non-empty edgeType`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("resolveDisplayGraph raw edge preserves exact edgeType string for label rendering", () => {
|
||||||
|
addNode("src");
|
||||||
|
addNode("tgt");
|
||||||
|
batchMergeEdges([{
|
||||||
|
id: "raw-1",
|
||||||
|
source: "src",
|
||||||
|
target: "tgt",
|
||||||
|
attributes: { edgeType: "works_for", weight: 1, properties: {} },
|
||||||
|
}]);
|
||||||
|
|
||||||
|
// In full view without aggregation the edge passes through unchanged.
|
||||||
|
const { graph: displayGraph } = resolveDisplayGraph("", [], [], "full", { aggregationEnabled: false });
|
||||||
|
assert.equal(displayGraph.size, 1);
|
||||||
|
|
||||||
|
const edgeId = displayGraph.edges()[0];
|
||||||
|
const attrs = displayGraph.getEdgeAttributes(edgeId) as { edgeType?: string };
|
||||||
|
assert.equal(attrs.edgeType, "works_for");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("resolveDisplayGraph does not produce empty-string edgeType on aggregated edges when source has empty type", () => {
|
||||||
|
addNode("a");
|
||||||
|
addNode("b");
|
||||||
|
// Simulate an API response where type is empty string — the aggregation
|
||||||
|
// path must not propagate a blank label.
|
||||||
|
batchMergeEdges([
|
||||||
|
{ id: "e-empty-1", source: "a", target: "b", attributes: { edgeType: "", weight: 1, properties: {} } },
|
||||||
|
{ id: "e-empty-2", source: "a", target: "b", attributes: { edgeType: "", weight: 1, properties: {} } },
|
||||||
|
]);
|
||||||
|
|
||||||
|
const { graph: displayGraph } = resolveDisplayGraph("", [], [], "full", { aggregationEnabled: true });
|
||||||
|
const edgeId = displayGraph.edges()[0];
|
||||||
|
const attrs = displayGraph.getEdgeAttributes(edgeId) as {
|
||||||
|
edgeType?: string;
|
||||||
|
isAggregated?: boolean;
|
||||||
|
};
|
||||||
|
assert.equal(attrs.isAggregated, true);
|
||||||
|
// The aggregation falls back to "related_to" when all source edgeTypes are
|
||||||
|
// empty, so the rendered label should never be an empty string.
|
||||||
|
assert.equal(attrs.edgeType, "related_to");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("resolveEdgeElementStyle hidden class produces hidden:true for suppressed edges", () => {
|
||||||
|
// Verify the data condition the edgeReducer relies on: hidden-classified
|
||||||
|
// edges must have hidden:true so that the label assignment sets undefined.
|
||||||
|
const style = resolveEdgeElementStyle(
|
||||||
|
GRAPH_THEME,
|
||||||
|
"overview",
|
||||||
|
"inactive",
|
||||||
|
{
|
||||||
|
edgeType: "causes",
|
||||||
|
weight: 1,
|
||||||
|
properties: {},
|
||||||
|
edgeVariant: "line",
|
||||||
|
visualPriority: 0.05,
|
||||||
|
baseSize: 0.3,
|
||||||
|
},
|
||||||
|
"source",
|
||||||
|
"target",
|
||||||
|
"full",
|
||||||
|
"inactive-edge",
|
||||||
|
"hidden",
|
||||||
|
);
|
||||||
|
assert.equal(style.hidden, true);
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user