generated from coulomb/repo-seed
Add graph label density controls
This commit is contained in:
@@ -121,6 +121,7 @@ def graph_explorer_page() -> str:
|
||||
right: 12px;
|
||||
z-index: 3;
|
||||
display: flex;
|
||||
align-items: end;
|
||||
gap: 6px;
|
||||
padding: 6px;
|
||||
border: 1px solid var(--line);
|
||||
@@ -129,6 +130,18 @@ def graph_explorer_page() -> str:
|
||||
box-shadow: 0 12px 30px rgba(23, 32, 51, .12);
|
||||
}
|
||||
.map-controls button { min-width: 54px; }
|
||||
.map-control-field {
|
||||
display: grid;
|
||||
gap: 2px;
|
||||
color: var(--muted);
|
||||
font-size: 11px;
|
||||
line-height: 1;
|
||||
}
|
||||
.map-control-field select {
|
||||
min-height: 30px;
|
||||
min-width: 84px;
|
||||
padding: 4px 6px;
|
||||
}
|
||||
.selection-anchor {
|
||||
position: absolute;
|
||||
z-index: 5;
|
||||
@@ -248,6 +261,14 @@ def graph_explorer_page() -> str:
|
||||
<div class="canvas-wrap">
|
||||
<div id="graph-canvas" role="img" aria-label="Interactive Fabric graph"></div>
|
||||
<div class="map-controls" aria-label="Map navigation controls">
|
||||
<label class="map-control-field">Labels
|
||||
<select id="label-select" title="Control node label density">
|
||||
<option value="auto">Auto</option>
|
||||
<option value="key">Key</option>
|
||||
<option value="all">All</option>
|
||||
<option value="none">None</option>
|
||||
</select>
|
||||
</label>
|
||||
<button type="button" data-action="fit" title="Fit graph to view">Fit</button>
|
||||
<button type="button" data-action="focus" title="Focus selected neighborhood">Focus</button>
|
||||
<button type="button" data-action="clear" class="primary" title="Reset map controls">Reset</button>
|
||||
@@ -305,6 +326,7 @@ def graph_explorer_page() -> str:
|
||||
const searchInput = document.getElementById("search");
|
||||
const modeSelect = document.getElementById("mode-select");
|
||||
const layoutSelect = document.getElementById("layout-select");
|
||||
const labelSelect = document.getElementById("label-select");
|
||||
const nodeTypeFilter = document.getElementById("node-type-filter");
|
||||
const nodeTypeSummary = document.getElementById("node-type-summary");
|
||||
const edgeTypeFilter = document.getElementById("edge-type-filter");
|
||||
@@ -334,6 +356,7 @@ def graph_explorer_page() -> str:
|
||||
let allNodeTypes = [];
|
||||
let allEdgeTypes = [];
|
||||
let activeMode = "full";
|
||||
let activeLabelMode = "auto";
|
||||
let profilePersistence = "none";
|
||||
let profiles = [];
|
||||
let currentProfileId = "";
|
||||
@@ -462,10 +485,33 @@ def graph_explorer_page() -> str:
|
||||
selectionAnchor.style.display = "block";
|
||||
};
|
||||
|
||||
const isKeyLabelNode = (node) => {
|
||||
const layer = node.data("layer");
|
||||
return (
|
||||
["repository", "server", "deployment", "service"].includes(layer) ||
|
||||
node.data("unresolved") === true ||
|
||||
node.data("lifecycle") === "registered-only"
|
||||
);
|
||||
};
|
||||
|
||||
const updateLabelVisibility = () => {
|
||||
if (!cy) return;
|
||||
const visibleNodes = cy.nodes().filter((node) => node.style("display") !== "none");
|
||||
const mode = activeLabelMode === "auto" && visibleNodes.length > 70 ? "key" : activeLabelMode;
|
||||
cy.nodes().forEach((node) => {
|
||||
let showLabel = mode === "all" || mode === "auto";
|
||||
if (mode === "key") showLabel = isKeyLabelNode(node);
|
||||
if (mode === "none") showLabel = false;
|
||||
if (selected && node.id() === selected.id()) showLabel = false;
|
||||
node.toggleClass("label-hidden", !showLabel);
|
||||
});
|
||||
};
|
||||
|
||||
const currentViewState = () => ({
|
||||
search: searchInput.value,
|
||||
mode: modeSelect.value || "full",
|
||||
layout: layoutSelect.value || "cose",
|
||||
labelMode: labelSelect.value || "auto",
|
||||
nodeTypes: Array.from(selectedNodeTypes()),
|
||||
edgeTypes: Array.from(selectedEdgeTypes()),
|
||||
review: reviewFilter.value,
|
||||
@@ -491,6 +537,7 @@ def graph_explorer_page() -> str:
|
||||
if (params.has("search")) state.search = params.get("search") || "";
|
||||
if (params.has("mode")) state.mode = params.get("mode") || "";
|
||||
if (params.has("layout")) state.layout = params.get("layout") || "";
|
||||
if (params.has("labelMode")) state.labelMode = params.get("labelMode") || "";
|
||||
if (params.has("nodeTypes")) state.nodeTypes = parseListParam(params.get("nodeTypes"));
|
||||
if (params.has("edgeTypes")) state.edgeTypes = parseListParam(params.get("edgeTypes"));
|
||||
if (params.has("layer")) state.nodeTypes = parseListParam(params.get("layer"));
|
||||
@@ -513,6 +560,7 @@ def graph_explorer_page() -> str:
|
||||
if (state.search) params.set("search", state.search);
|
||||
if (state.mode && state.mode !== "full") params.set("mode", state.mode);
|
||||
if (state.layout && state.layout !== "cose") params.set("layout", state.layout);
|
||||
if (state.labelMode && state.labelMode !== "auto") params.set("labelMode", state.labelMode);
|
||||
if (state.nodeTypes.length !== allNodeTypes.length) params.set("nodeTypes", state.nodeTypes.join(","));
|
||||
if (state.edgeTypes.length !== allEdgeTypes.length) params.set("edgeTypes", state.edgeTypes.join(","));
|
||||
if (state.review) params.set("review", state.review);
|
||||
@@ -535,6 +583,7 @@ def graph_explorer_page() -> str:
|
||||
if ("search" in state) searchInput.value = state.search || "";
|
||||
if ("mode" in state) modeSelect.value = optionExists(modeSelect, state.mode) ? state.mode : "full";
|
||||
if ("layout" in state) layoutSelect.value = optionExists(layoutSelect, state.layout) ? state.layout : "cose";
|
||||
if ("labelMode" in state) labelSelect.value = optionExists(labelSelect, state.labelMode) ? state.labelMode : "auto";
|
||||
if ("nodeTypes" in state) setCheckedValues(nodeTypeFilter, (state.nodeTypes || []).filter((value) => allNodeTypes.includes(value)));
|
||||
if ("layer" in state) setCheckedValues(nodeTypeFilter, allNodeTypes.includes(state.layer) ? [state.layer] : null);
|
||||
if ("edgeTypes" in state) setCheckedValues(edgeTypeFilter, (state.edgeTypes || []).filter((value) => allEdgeTypes.includes(value)));
|
||||
@@ -544,6 +593,7 @@ def graph_explorer_page() -> str:
|
||||
manualOverrides = {...state.manualOverrides};
|
||||
}
|
||||
activeMode = modeSelect.value || "full";
|
||||
activeLabelMode = labelSelect.value || "auto";
|
||||
focusSet = null;
|
||||
syncFilterSummaries();
|
||||
applyFilters();
|
||||
@@ -711,6 +761,7 @@ def graph_explorer_page() -> str:
|
||||
const hidden = cy.elements().length - visibleNodes - visibleEdges;
|
||||
counts.textContent = `${visibleNodes} nodes / ${visibleEdges} edges`;
|
||||
hiddenSummary.textContent = `Hidden ${hidden}`;
|
||||
updateLabelVisibility();
|
||||
updateSelectionAnchor();
|
||||
};
|
||||
|
||||
@@ -723,6 +774,7 @@ def graph_explorer_page() -> str:
|
||||
detailList.innerHTML = "";
|
||||
orientationTitle.textContent = "Select a service, interface, dependency, or registered-only repo.";
|
||||
orientationList.innerHTML = "";
|
||||
updateLabelVisibility();
|
||||
updateSelectionAnchor();
|
||||
return;
|
||||
}
|
||||
@@ -749,6 +801,7 @@ def graph_explorer_page() -> str:
|
||||
.map(([key, value]) => `<li><strong>${escapeHtml(key)}</strong> ${escapeHtml(value)}</li>`)
|
||||
.join("");
|
||||
renderOrientation(element);
|
||||
updateLabelVisibility();
|
||||
updateSelectionAnchor();
|
||||
};
|
||||
|
||||
@@ -866,9 +919,15 @@ def graph_explorer_page() -> str:
|
||||
};
|
||||
|
||||
const renderLegend = (layers) => {
|
||||
legend.innerHTML = layers.map((layer) =>
|
||||
const nodeTypes = layers.map((layer) =>
|
||||
`<span class="pill"><span style="display:inline-block;width:10px;height:10px;border-radius:50%;background:${escapeHtml(layer.color || "#667085")}"></span>${escapeHtml(layer.label)}</span>`
|
||||
).join("");
|
||||
);
|
||||
const statusItems = [
|
||||
'<span class="pill"><span style="display:inline-block;width:12px;height:12px;border:2px dashed #b45309;border-radius:50%"></span>Unresolved</span>',
|
||||
'<span class="pill"><span style="display:inline-block;width:12px;height:12px;border:2px dashed #be123c;border-radius:3px"></span>Registered only</span>',
|
||||
'<span class="pill"><span style="display:inline-block;width:18px;border-top:2px dotted #98a2b3"></span>Weak edge</span>',
|
||||
];
|
||||
legend.innerHTML = [...nodeTypes, ...statusItems].join("");
|
||||
};
|
||||
|
||||
const boot = async () => {
|
||||
@@ -949,7 +1008,8 @@ def graph_explorer_page() -> str:
|
||||
"z-index": 2
|
||||
}},
|
||||
{selector: ".display-blur", style: {"opacity": .24, "label": ""}},
|
||||
{selector: ".display-blur:selected, .display-blur.hover", style: {"opacity": .78, "label": "data(label)"}},
|
||||
{selector: ".display-blur.hover", style: {"opacity": .78, "label": "data(label)"}},
|
||||
{selector: ".label-hidden", style: {"label": ""}},
|
||||
{selector: ":selected", style: {"overlay-opacity": 0}}
|
||||
]
|
||||
});
|
||||
@@ -1015,12 +1075,23 @@ def graph_explorer_page() -> str:
|
||||
updateProfileSummary();
|
||||
updateUrlState();
|
||||
});
|
||||
labelSelect.addEventListener("input", () => {
|
||||
activeLabelMode = labelSelect.value || "auto";
|
||||
currentProfileId = "";
|
||||
profileSelect.value = "";
|
||||
updateLabelVisibility();
|
||||
updateProfileControls();
|
||||
updateProfileSummary();
|
||||
updateUrlState();
|
||||
});
|
||||
document.querySelector("[data-action='fit']").addEventListener("click", () => cy && cy.fit(cy.elements(":visible"), 48));
|
||||
document.querySelector("[data-action='focus']").addEventListener("click", applyFocus);
|
||||
document.querySelector("[data-action='clear']").addEventListener("click", () => {
|
||||
searchInput.value = "";
|
||||
modeSelect.value = "full";
|
||||
activeMode = "full";
|
||||
labelSelect.value = "auto";
|
||||
activeLabelMode = "auto";
|
||||
setCheckedValues(nodeTypeFilter);
|
||||
setCheckedValues(edgeTypeFilter);
|
||||
reviewFilter.value = "";
|
||||
|
||||
Reference in New Issue
Block a user