Skip to content

Commit d6a5d1a

Browse files
committed
add link filtering
1 parent b6feb64 commit d6a5d1a

File tree

6 files changed

+229
-50
lines changed

6 files changed

+229
-50
lines changed

core/frontend/filter.js

Lines changed: 69 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,25 @@
44
* @copyright GNU GPL 3.0 Cosma's authors
55
*/
66

7-
import { setNodesDisplaying } from './graph.js';
7+
import { setNodesDisplaying, updateLinkVisibility } from './graph.js';
88

99
window.addEventListener('DOMContentLoaded', () => {
10+
// Node Types Selector
1011
/** @type {HTMLFormElement} */
1112
const form = document.getElementById('types-form');
1213
/** @type {HTMLInputElement[]} */
1314
const inputs = form.querySelectorAll('input');
14-
1515
/** @type {[string, string[]][]} */
1616
const types = Object.entries(typeList);
1717

18+
// Link Type Selector
19+
/** @type {HTMLFormElement} */
20+
const linkForm = document.getElementById('link-types-form');
21+
/** @type {HTMLInputElement[]} */
22+
const linkInputs = linkForm ? linkForm.querySelectorAll('input') : [];
23+
/** @type {[string, {linkKeys: string[], active: boolean, color: string}][]} */
24+
const linkTypes = Object.entries(linkTypeList);
25+
1826
/**
1927
* Default state
2028
*/
@@ -24,6 +32,12 @@ window.addEventListener('DOMContentLoaded', () => {
2432
}
2533
changeTypesState();
2634

35+
for (const [name, { active }] of linkTypes) {
36+
// Check if the element exists before trying to set checked
37+
linkForm.querySelector(`[name="${name}"]`).checked = active; // Use the 'active' flag passed from template.js
38+
}
39+
changeLinkTypesState(); // Initial call for links
40+
2741
/**
2842
* Search params state
2943
*/
@@ -38,11 +52,22 @@ window.addEventListener('DOMContentLoaded', () => {
3852
changeTypesState();
3953
}
4054

55+
const linkFiltersFromSearch = searchParams.get('link-filters')?.split('-');
56+
if (linkFiltersFromSearch?.length) {
57+
for (const [name] of linkTypes) {
58+
linkForm.querySelector(`[name="${name}"]`).checked = linkFiltersFromSearch.includes(name);
59+
}
60+
changeLinkTypesState();
61+
}
62+
4163
/**
4264
* User actions state
4365
*/
4466

4567
form.addEventListener('change', changeTypesState);
68+
if (linkForm) {
69+
linkForm.addEventListener('change', changeLinkTypesState); // Add listener for link form
70+
}
4671

4772
function changeTypesState() {
4873
let formState = new FormData(form);
@@ -59,6 +84,19 @@ window.addEventListener('DOMContentLoaded', () => {
5984
setNodesDisplaying(Array.from(nodeIdsToDisplay));
6085
}
6186

87+
function changeLinkTypesState() {
88+
let formState = new FormData(linkForm);
89+
formState = Object.fromEntries(formState);
90+
91+
// Get the *names* of the link types that are currently checked
92+
const activeLinkTypes = new Set(
93+
linkTypes.filter(([name]) => !!formState[name]).map(([name]) => name)
94+
);
95+
96+
// Call the graph update function with the set of active type names
97+
updateLinkVisibility(activeLinkTypes);
98+
}
99+
62100
let filterNameAltMode;
63101
for (const input of inputs) {
64102
const { name: filterName, checked: active } = input;
@@ -70,30 +108,51 @@ window.addEventListener('DOMContentLoaded', () => {
70108
e.preventDefault();
71109

72110
if (filterNameAltMode === filterName) {
73-
displayHidden();
111+
displayHidden(form);
74112
filterNameAltMode = undefined;
75113
} else {
76-
hideAllButOne(filterName);
114+
hideAllButOne(form, inputs, filterName);
77115
filterNameAltMode = filterName;
78116
}
79117
}
80118
});
81119
}
82120

121+
let linkFilterNameAltMode;
122+
for (const input of linkInputs) {
123+
const { name: filterName } = input;
124+
125+
input.parentElement.addEventListener('click', (e) => {
126+
const altMode = e.altKey;
127+
if (altMode) {
128+
e.stopPropagation();
129+
e.preventDefault();
130+
131+
if (linkFilterNameAltMode === filterName) {
132+
displayHidden(linkForm);
133+
linkFilterNameAltMode = undefined;
134+
} else {
135+
hideAllButOne(linkForm, linkInputs, filterName);
136+
linkFilterNameAltMode = filterName;
137+
}
138+
}
139+
});
140+
}
141+
83142
hotkeys('alt+r', (e) => {
84143
e.preventDefault();
85144
displayHidden();
86145
});
87146

88-
function displayHidden() {
89-
form
147+
function displayHidden(targetForm) {
148+
targetForm
90149
.querySelectorAll(`input:not(:checked)`)
91150
.forEach((checkedInput) => (checkedInput.checked = true));
92-
form.dispatchEvent(new Event('change'));
151+
targetForm.dispatchEvent(new Event('change'));
93152
}
94153

95-
function hideAllButOne(filterName) {
96-
inputs.forEach((input) => (input.checked = filterName === input.name));
97-
form.dispatchEvent(new Event('change'));
154+
function hideAllButOne(targetForm, targetInputs, filterName) {
155+
targetInputs.forEach((input) => (input.checked = filterName === input.name));
156+
targetForm.dispatchEvent(new Event('change'));
98157
}
99158
});

core/frontend/graph.js

Lines changed: 74 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ const imageFileValidExtnames = new Set(['jpg', 'jpeg', 'png']);
121121
/** @type {d3.Selection<SVGLineElement, Link, SVGElement, any>} */
122122
elts.links = svgSub
123123
.append('g')
124+
.attr('class', 'links-group')
124125
.selectAll('line')
125126
.data(data.edges)
126127
.enter()
@@ -395,34 +396,34 @@ function generatePathCoordinatesWithBorder(numSegments, diameter, borderSize) {
395396
*/
396397

397398
function getNodeNetwork(nodeId) {
398-
const edges = graph.edges(nodeId);
399-
400399
const node = elts.nodes.filter(({ key }) => key === nodeId);
401-
const links = elts.links.filter(({ key }) => edges.includes(key));
402-
403-
return {
404-
node,
405-
links,
406-
};
400+
// Links related to this node will be handled by highlight/unlight functions
401+
return { node };
407402
}
408403

409-
function setNodesDisplaying(nodeIds) {
410-
const toDisplay = nodeIds;
411-
const toHide = Array.from(d3.difference(allNodeIds, toDisplay));
404+
function setNodesDisplaying(nodeIdsToShow) {
405+
const nodesToShowSet = new Set(nodeIdsToShow);
406+
allNodeIds.forEach(nodeId => {
407+
const shouldShow = nodesToShowSet.has(nodeId);
408+
// Use Graphology attribute for node state
409+
graph.setNodeAttribute(nodeId, 'hidden', !shouldShow);
410+
});
411+
412+
// Update the D3 node elements based on the Graphology attribute
413+
elts.nodes.style('display', d => graph.getNodeAttribute(d.key, 'hidden') ? 'none' : null);
414+
elts.labels.style('display', d => graph.getNodeAttribute(d.key, 'hidden') ? 'none' : null); // Also hide/show labels
412415

413-
displayNodes(toDisplay);
414-
hideNodes(toHide);
416+
updateLinkVisibilityBasedOnFiltersAndNodes();
417+
setCounters(); // Update counters after nodes change
415418
}
416419

417420
graph.on('nodeAttributesUpdated', function ({ key, attributes }) {
418421
const { links, node } = getNodeNetwork(key);
419422

420423
if (attributes.hidden) {
421424
node.node().classList.add('hide');
422-
links.nodes().forEach((elt) => elt.classList.add('hide'));
423425
} else {
424426
node.node().classList.remove('hide');
425-
links.nodes().forEach((elt) => elt.classList.remove('hide'));
426427
}
427428
});
428429

@@ -445,13 +446,56 @@ function displayNodes(nodeIds) {
445446
}
446447

447448
function displayNodesAll() {
448-
graph.updateEachNodeAttributes((node, attr) => ({
449-
...attr,
450-
hidden: false,
451-
}));
449+
allNodeIds.forEach(nodeId => {
450+
graph.setNodeAttribute(nodeId, 'hidden', false);
451+
});
452+
elts.nodes.style('display', null);
453+
elts.labels.style('display', null); // Also show labels
454+
455+
// Update link visibility after showing all nodes
456+
updateLinkVisibilityBasedOnFiltersAndNodes();
457+
setCounters();
458+
}
459+
460+
// --- Manage Link Visibility ---
461+
462+
// Keep track of which link types are currently active based on checkboxes
463+
let activeLinkTypes = new Set(Object.keys(linkTypeList)); // Initially all active
464+
465+
// Function called by filter.js when link checkboxes change
466+
function updateLinkVisibility(newActiveLinkTypes) {
467+
activeLinkTypes = newActiveLinkTypes;
468+
updateLinkVisibilityBasedOnFiltersAndNodes();
469+
}
470+
471+
// Central function to update link visibility based on *both* filters and node visibility
472+
function updateLinkVisibilityBasedOnFiltersAndNodes() {
473+
console.log("updating link visibility")
474+
if (!linksDisplayToggle) { // Skip if links are globally toggled off
475+
console.log("early return because links toggled off")
476+
elts.links.style('display', 'none');
477+
elts.linkLabels.style('display', 'none');
478+
return;
479+
}
480+
481+
elts.links.style('display', d => {
482+
const typeIsActive = activeLinkTypes.has(d.attributes.type || 'undefined');
483+
const sourceIsVisible = !graph.getNodeAttribute(d.source.key, 'hidden');
484+
const targetIsVisible = !graph.getNodeAttribute(d.target.key, 'hidden');
485+
return typeIsActive && sourceIsVisible && targetIsVisible ? null : 'none';
486+
});
452487

453-
elts.nodes.nodes().forEach((elt) => elt.classList.remove('hide'));
454-
elts.links.nodes().forEach((elt) => elt.classList.remove('hide'));
488+
if (!linkLabelsDisplayToggle) { // Skip if labels are globally toggled off
489+
elts.linkLabels.style('display', 'none');
490+
} else {
491+
elts.linkLabels.style('display', d => {
492+
const typeIsActive = activeLinkTypes.has(d.attributes.type || 'undefined');
493+
const sourceIsVisible = !graph.getNodeAttribute(d.source.key, 'hidden');
494+
const targetIsVisible = !graph.getNodeAttribute(d.target.key, 'hidden');
495+
return typeIsActive && sourceIsVisible && targetIsVisible ? null : 'none';
496+
});
497+
}
498+
// Note: No need to update counters for links usually, unless you add a link counter UI element.
455499
}
456500

457501
let highlightedNodes = [];
@@ -467,7 +511,7 @@ function highlightNodes(nodeIds) {
467511
.forEach((nodeId) => {
468512
const { links, node } = getNodeNetwork(nodeId);
469513
node.node().classList.add('highlight');
470-
links.nodes().forEach((elt) => elt.classList.add('highlight'));
514+
elts.links.nodes().forEach((elt) => elt.classList.add('highlight'));
471515
});
472516

473517
highlightedNodes = highlightedNodes.concat(nodeIds);
@@ -487,7 +531,7 @@ function unlightNodes() {
487531
.forEach((nodeId) => {
488532
const { links, node } = getNodeNetwork(nodeId);
489533
node.node().classList.remove('highlight');
490-
links.nodes().forEach((elt) => elt.classList.remove('highlight'));
534+
elts.links.nodes().forEach((elt) => elt.classList.remove('highlight'));
491535
});
492536

493537
highlightedNodes = [];
@@ -498,12 +542,10 @@ function unlightNodes() {
498542
* @param {boolean} isChecked - 'checked' value send by a checkbox input
499543
*/
500544

545+
let linksDisplayToggle = true; // Keep track of global link toggle state
501546
window.linksDisplayToggle = function (isChecked) {
502-
if (isChecked) {
503-
elts.links.nodes().forEach((elt) => elt.classList.remove('hide'));
504-
} else {
505-
elts.links.nodes().forEach((elt) => elt.classList.add('hide'));
506-
}
547+
linksDisplayToggle = isChecked;
548+
updateLinkVisibilityBasedOnFiltersAndNodes(); // Update visibility when toggled
507549
};
508550

509551
/**
@@ -519,12 +561,10 @@ window.labelDisplayToggle = function (isChecked) {
519561
}
520562
};
521563

564+
let linkLabelsDisplayToggle = true; // Keep track of global label toggle state
522565
window.linkLabelDisplayToggle = function (isChecked) {
523-
if (isChecked) {
524-
elts.linkLabels.style('display', null);
525-
} else {
526-
elts.linkLabels.style('display', 'none');
527-
}
566+
linkLabelsDisplayToggle = isChecked;
567+
updateLinkVisibilityBasedOnFiltersAndNodes(); // Update visibility when toggled
528568
};
529569

530570
/**
@@ -660,10 +700,9 @@ hotkeys('c', (e) => {
660700
export {
661701
svg,
662702
svgSub,
663-
hideNodes,
664-
displayNodes,
665703
displayNodesAll,
666704
setNodesDisplaying,
705+
updateLinkVisibility,
667706
highlightNodes,
668707
unlightNodes,
669708
translate,

core/frontend/view.js

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,25 @@ window.addEventListener('DOMContentLoaded', () => {
55
const activeFilters = Array.from(document.querySelectorAll('#types-form input:checked')).map(
66
({ name }) => name,
77
);
8-
const activeTags = Array.from(document.querySelectorAll('#tags-form input:checked')).map(
9-
({ name }) => name,
10-
);
8+
const linkForm = document.getElementById('link-types-form');
9+
const activeLinkFilters = linkForm ? Array.from(linkForm.querySelectorAll('input:checked')).map(
10+
({ name }) => name,
11+
) : [];
12+
const tagForm = document.getElementById('tags-form');
13+
const activeTags = tagForm ? Array.from(tagForm.querySelectorAll('input:checked')).map(
14+
({ name }) => name,
15+
) : [];
1116
const focusLevel = document.getElementById('focus-input').value;
1217

1318
if (activeFilters.length > 0) {
1419
url.searchParams.set('filters', activeFilters.join('-'));
1520
}
21+
22+
if (activeLinkFilters.length > 0 && activeLinkFilters.length < Object.keys(linkTypeList).length) {
23+
url.searchParams.set('link-filters', activeLinkFilters.join('-'));
24+
} else {
25+
url.searchParams.delete('link-filters');
26+
}
1627
if (activeTags.length > 0) {
1728
url.searchParams.set('tags', activeTags.join('-'));
1829
}

core/i18n.yml

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,13 @@ left_panel:
2222

2323
menu_types:
2424
title:
25-
fr: Types
26-
en: Types
25+
fr: Types de Nœuds
26+
en: Node Types
27+
28+
menu_link_types:
29+
title:
30+
fr: Types de liens
31+
en: Link Types
2732

2833
menu_keywords:
2934
title:

0 commit comments

Comments
 (0)