feat(graph): iter6 — кнопки «По использованию» / «Дубли» + режим viewMode

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Дмитрий
2026-05-16 09:54:44 +03:00
parent 65381f2b24
commit 2f267f15f7
+70 -4
View File
@@ -76,6 +76,20 @@
/* ── Паспорт узла (iter6) ── */
#passport-section p { font-size: 12px; color: #eee8d5; line-height: 1.6; }
#passport-section p .pp-k { color: #839496; }
/* ── Кнопки режимов в футере (iter6) ── */
.cat-ctl-sep { width: 1px; align-self: stretch; background: #586e75; margin: 0 4px; }
.cat-ctl {
background: #002b36; border: 1px solid #586e75; color: #93a1a1;
border-radius: 4px; padding: 2px 8px; font-size: 11px; cursor: pointer;
transition: background 0.12s, box-shadow 0.12s; user-select: none;
}
.cat-ctl:hover { background: #0d4a5a; color: #fdf6e3; }
.cat-ctl.active {
background: rgba(253,246,227,0.12);
box-shadow: inset 0 0 0 1px rgba(253,246,227,0.4);
color: #fdf6e3;
}
</style>
</head>
<body>
@@ -145,6 +159,9 @@
<div class="cat-item" data-filter-key="conflict:RED"><div class="cat-dot" style="background:#ff5f57; border:1px dashed #ff5f57"></div>🔴 Не закрыт правилом</div>
<div class="cat-item" data-filter-key="conflict:BLACK"><div class="cat-dot" style="background:#888888; border:1px dashed #888888"></div>⚫ Возник на практике</div>
<div class="cat-item" data-filter-key="conflict:GREEN"><div class="cat-dot" style="background:#859900; border:1px dashed #859900"></div>🟢 Закрыт правилом</div>
<span class="cat-ctl-sep"></span>
<button class="cat-ctl" id="cat-ctl-heat" title="Подсветить узлы по числу вызовов за 7 дней">🔥 По использованию</button>
<button class="cat-ctl" id="cat-ctl-dup" title="Подсветить явные пары дублей (D1–D5, D7)">⧉ Дубли</button>
</div>
<script>
@@ -1675,15 +1692,20 @@ function showEdgeLegend(edgeId) {
network.on('click', params => {
if (params.nodes.length === 1) {
const id = params.nodes[0];
HIGHLIGHT.setSelectedNode(id);
HIGHLIGHT.applyHighlight();
// iter6 — в режиме heat/dup клик открывает паспорт, но не трогает подсветку режима
if (HIGHLIGHT.state.viewMode === null) {
HIGHLIGHT.setSelectedNode(id);
HIGHLIGHT.applyHighlight();
}
// Right panel still shows details of the clicked node (last-clicked, even after toggle-off)
showNodeLegend(id);
} else if (params.edges.length === 1) {
showEdgeLegend(params.edges[0]);
} else if (params.nodes.length === 0 && params.edges.length === 0) {
HIGHLIGHT.setSelectedNode(null);
HIGHLIGHT.applyHighlight();
if (HIGHLIGHT.state.viewMode === null) {
HIGHLIGHT.setSelectedNode(null);
HIGHLIGHT.applyHighlight();
}
document.getElementById('legend-panel').classList.remove('visible');
}
});
@@ -1830,8 +1852,35 @@ const HIGHLIGHT = (function setupHighlight() {
const state = {
selectedNode: null,
legendFilter: new Set(),
viewMode: null, // null | 'heat' | 'dup' — взаимоисключающие режимы (iter6)
};
// ── Теплокарта использования (iter6) — 4 яруса по NODE_META[id].uses ──
function heatOpacity(nodeId) {
const m = NODE_META[nodeId];
const u = m ? m.uses : null;
if (u === null || u === undefined) return 0.5; // нет данных — нейтрально
if (u >= 21) return 1.0; // часто
if (u >= 6) return 0.65; // иногда
if (u >= 1) return 0.35; // редко
return 0.12; // простаивает (uses === 0)
}
// Узлы верхнего яруса теплокарты получают акцентную рамку.
function heatBorderWidth(nodeId) {
if (state.viewMode !== 'heat') return 2;
const m = NODE_META[nodeId];
const u = m ? m.uses : null;
return (typeof u === 'number' && u >= 21) ? 4 : 2;
}
// Переключатель режима — toggle; включение режима гасит пофильтровую подсветку.
function setViewMode(mode) {
state.viewMode = (state.viewMode === mode) ? null : mode;
if (state.viewMode !== null) {
state.legendFilter.clear();
state.selectedNode = null;
}
}
// ── Pre-computed indices ──────────────────────────
const NODES_BY_ID = new Map();
const NEIGHBOURS = new Map();
@@ -1861,6 +1910,9 @@ const HIGHLIGHT = (function setupHighlight() {
// ── Opacity computations ──────────────────────────
function computeNodeOpacity(nodeId) {
// Row 0: view-mode (iter6) — глобальная картина, поверх focus/filter
if (state.viewMode === 'heat') return heatOpacity(nodeId);
if (state.viewMode === 'dup') return DUP_NODE_SET.has(nodeId) ? OPACITY_FOCUS : OPACITY_DIM;
// Row 1: focus
if (state.selectedNode !== null) {
if (state.selectedNode === nodeId) return OPACITY_FOCUS;
@@ -1902,6 +1954,7 @@ const HIGHLIGHT = (function setupHighlight() {
const nodeUpdates = NODES.map(n => ({
id: n.id,
opacity: computeNodeOpacity(n.id),
borderWidth: heatBorderWidth(n.id), // 4 для верхнего яруса теплокарты, иначе 2 (iter6)
}));
const edgeUpdates = edgesDS.get().map(e => ({
id: e.id,
@@ -1925,6 +1978,7 @@ const HIGHLIGHT = (function setupHighlight() {
function clearAll() {
state.selectedNode = null;
state.legendFilter.clear();
state.viewMode = null;
}
function updateLegendVisuals() {
@@ -1934,10 +1988,22 @@ const HIGHLIGHT = (function setupHighlight() {
if (state.legendFilter.has(key)) item.classList.add('active');
else item.classList.remove('active');
});
const heatBtn = document.getElementById('cat-ctl-heat');
const dupBtn = document.getElementById('cat-ctl-dup');
if (heatBtn) heatBtn.classList.toggle('active', state.viewMode === 'heat');
if (dupBtn) dupBtn.classList.toggle('active', state.viewMode === 'dup');
}
// ── Legend click delegation ───────────────────────
document.getElementById('cat-legend').addEventListener('click', e => {
// iter6 — клик по кнопке режима heat/dup
const ctl = e.target.closest('.cat-ctl');
if (ctl) {
setViewMode(ctl.id === 'cat-ctl-heat' ? 'heat' : 'dup');
applyHighlight();
updateLegendVisuals();
return;
}
const item = e.target.closest('.cat-item');
if (!item || !item.dataset.filterKey) return;
toggleFilter(item.dataset.filterKey);