Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 69 additions & 10 deletions core/frontend/filter.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,25 @@
* @copyright GNU GPL 3.0 Cosma's authors
*/

import { setNodesDisplaying } from './graph.js';
import { setNodesDisplaying, updateLinkVisibility } from './graph.js';

window.addEventListener('DOMContentLoaded', () => {
// Node Types Selector
/** @type {HTMLFormElement} */
const form = document.getElementById('types-form');
/** @type {HTMLInputElement[]} */
const inputs = form.querySelectorAll('input');

/** @type {[string, string[]][]} */
const types = Object.entries(typeList);

// Link Type Selector
/** @type {HTMLFormElement} */
const linkForm = document.getElementById('link-types-form');
/** @type {HTMLInputElement[]} */
const linkInputs = linkForm ? linkForm.querySelectorAll('input') : [];
/** @type {[string, {linkKeys: string[], active: boolean, color: string}][]} */
const linkTypes = Object.entries(linkTypeList);

/**
* Default state
*/
Expand All @@ -24,6 +32,12 @@ window.addEventListener('DOMContentLoaded', () => {
}
changeTypesState();

for (const [name, { active }] of linkTypes) {
// Check if the element exists before trying to set checked
linkForm.querySelector(`[name="${name}"]`).checked = active; // Use the 'active' flag passed from template.js
}
changeLinkTypesState(); // Initial call for links

/**
* Search params state
*/
Expand All @@ -38,11 +52,22 @@ window.addEventListener('DOMContentLoaded', () => {
changeTypesState();
}

const linkFiltersFromSearch = searchParams.get('link-filters')?.split('-');
if (linkFiltersFromSearch?.length) {
for (const [name] of linkTypes) {
linkForm.querySelector(`[name="${name}"]`).checked = linkFiltersFromSearch.includes(name);
}
changeLinkTypesState();
}

/**
* User actions state
*/

form.addEventListener('change', changeTypesState);
if (linkForm) {
linkForm.addEventListener('change', changeLinkTypesState); // Add listener for link form
}

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

function changeLinkTypesState() {
let formState = new FormData(linkForm);
formState = Object.fromEntries(formState);

// Get the *names* of the link types that are currently checked
const activeLinkTypes = new Set(
linkTypes.filter(([name]) => !!formState[name]).map(([name]) => name)
);

// Call the graph update function with the set of active type names
updateLinkVisibility(activeLinkTypes);
}

let filterNameAltMode;
for (const input of inputs) {
const { name: filterName, checked: active } = input;
Expand All @@ -70,30 +108,51 @@ window.addEventListener('DOMContentLoaded', () => {
e.preventDefault();

if (filterNameAltMode === filterName) {
displayHidden();
displayHidden(form);
filterNameAltMode = undefined;
} else {
hideAllButOne(filterName);
hideAllButOne(form, inputs, filterName);
filterNameAltMode = filterName;
}
}
});
}

let linkFilterNameAltMode;
for (const input of linkInputs) {
const { name: filterName } = input;

input.parentElement.addEventListener('click', (e) => {
const altMode = e.altKey;
if (altMode) {
e.stopPropagation();
e.preventDefault();

if (linkFilterNameAltMode === filterName) {
displayHidden(linkForm);
linkFilterNameAltMode = undefined;
} else {
hideAllButOne(linkForm, linkInputs, filterName);
linkFilterNameAltMode = filterName;
}
}
});
}

hotkeys('alt+r', (e) => {
e.preventDefault();
displayHidden();
});

function displayHidden() {
form
function displayHidden(targetForm) {
targetForm
.querySelectorAll(`input:not(:checked)`)
.forEach((checkedInput) => (checkedInput.checked = true));
form.dispatchEvent(new Event('change'));
targetForm.dispatchEvent(new Event('change'));
}

function hideAllButOne(filterName) {
inputs.forEach((input) => (input.checked = filterName === input.name));
form.dispatchEvent(new Event('change'));
function hideAllButOne(targetForm, targetInputs, filterName) {
targetInputs.forEach((input) => (input.checked = filterName === input.name));
targetForm.dispatchEvent(new Event('change'));
}
});
133 changes: 103 additions & 30 deletions core/frontend/graph.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,23 @@ simulation.on('tick', function () {
.attr('y1', (d) => d.source.y)
.attr('x2', (d) => d.target.x)
.attr('y2', (d) => d.target.y);

elts.linkLabels
.attr("x", (d) => (d.source.x + d.target.x) / 2)
.attr("y", (d) => (d.source.y + d.target.y) / 2)
.attr("text-anchor", "middle")
.attr("transform", d => {
var angle = Math.atan2(d.target.y - d.source.y, d.target.x - d.source.x) * 180 / Math.PI;

// Adjust angle to avoid upside-down text
if (angle > 90 || angle < -90) {
angle = (angle + 180) % 360;
}

var x = (d.source.x + d.target.x) / 2;
var y = (d.source.y + d.target.y) / 2;
return `rotate(${angle},${x},${y})`;
});

elts.nodes.attr('transform', (d) => 'translate(' + d.x + ',' + d.y + ')');

Expand All @@ -104,6 +121,7 @@ const imageFileValidExtnames = new Set(['jpg', 'jpeg', 'png']);
/** @type {d3.Selection<SVGLineElement, Link, SVGElement, any>} */
elts.links = svgSub
.append('g')
.attr('class', 'links-group')
.selectAll('line')
.data(data.edges)
.enter()
Expand All @@ -130,6 +148,15 @@ if (graphProperties.graph_arrows === true) {
elts.links.attr('marker-end', 'url(#arrow)');
}

elts.linkLabels = svgSub
.append('g')
.selectAll("text")
.data(data.edges)
.enter()
.append("text")
.attr('font-size', graphProperties.graph_text_size)
.text(d => d.attributes.type); // Assuming each link has a label

const strokeWidth = 2;

/** @type {d3.Selection<SVGGElement, Node, SVGElement, any>} */
Expand Down Expand Up @@ -369,34 +396,34 @@ function generatePathCoordinatesWithBorder(numSegments, diameter, borderSize) {
*/

function getNodeNetwork(nodeId) {
const edges = graph.edges(nodeId);

const node = elts.nodes.filter(({ key }) => key === nodeId);
const links = elts.links.filter(({ key }) => edges.includes(key));

return {
node,
links,
};
// Links related to this node will be handled by highlight/unlight functions
return { node };
}

function setNodesDisplaying(nodeIds) {
const toDisplay = nodeIds;
const toHide = Array.from(d3.difference(allNodeIds, toDisplay));
function setNodesDisplaying(nodeIdsToShow) {
const nodesToShowSet = new Set(nodeIdsToShow);
allNodeIds.forEach(nodeId => {
const shouldShow = nodesToShowSet.has(nodeId);
// Use Graphology attribute for node state
graph.setNodeAttribute(nodeId, 'hidden', !shouldShow);
});

displayNodes(toDisplay);
hideNodes(toHide);
// Update the D3 node elements based on the Graphology attribute
elts.nodes.style('display', d => graph.getNodeAttribute(d.key, 'hidden') ? 'none' : null);
elts.labels.style('display', d => graph.getNodeAttribute(d.key, 'hidden') ? 'none' : null); // Also hide/show labels

updateLinkVisibilityBasedOnFiltersAndNodes();
setCounters(); // Update counters after nodes change
}

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

if (attributes.hidden) {
node.node().classList.add('hide');
links.nodes().forEach((elt) => elt.classList.add('hide'));
} else {
node.node().classList.remove('hide');
links.nodes().forEach((elt) => elt.classList.remove('hide'));
}
});

Expand All @@ -419,13 +446,56 @@ function displayNodes(nodeIds) {
}

function displayNodesAll() {
graph.updateEachNodeAttributes((node, attr) => ({
...attr,
hidden: false,
}));
allNodeIds.forEach(nodeId => {
graph.setNodeAttribute(nodeId, 'hidden', false);
});
elts.nodes.style('display', null);
elts.labels.style('display', null); // Also show labels

elts.nodes.nodes().forEach((elt) => elt.classList.remove('hide'));
elts.links.nodes().forEach((elt) => elt.classList.remove('hide'));
// Update link visibility after showing all nodes
updateLinkVisibilityBasedOnFiltersAndNodes();
setCounters();
}

// --- Manage Link Visibility ---

// Keep track of which link types are currently active based on checkboxes
let activeLinkTypes = new Set(Object.keys(linkTypeList)); // Initially all active

// Function called by filter.js when link checkboxes change
function updateLinkVisibility(newActiveLinkTypes) {
activeLinkTypes = newActiveLinkTypes;
updateLinkVisibilityBasedOnFiltersAndNodes();
}

// Central function to update link visibility based on *both* filters and node visibility
function updateLinkVisibilityBasedOnFiltersAndNodes() {
console.log("updating link visibility")
if (!linksDisplayToggle) { // Skip if links are globally toggled off
console.log("early return because links toggled off")
elts.links.style('display', 'none');
elts.linkLabels.style('display', 'none');
return;
}

elts.links.style('display', d => {
const typeIsActive = activeLinkTypes.has(d.attributes.type || 'undefined');
const sourceIsVisible = !graph.getNodeAttribute(d.source.key, 'hidden');
const targetIsVisible = !graph.getNodeAttribute(d.target.key, 'hidden');
return typeIsActive && sourceIsVisible && targetIsVisible ? null : 'none';
});

if (!linkLabelsDisplayToggle) { // Skip if labels are globally toggled off
elts.linkLabels.style('display', 'none');
} else {
elts.linkLabels.style('display', d => {
const typeIsActive = activeLinkTypes.has(d.attributes.type || 'undefined');
const sourceIsVisible = !graph.getNodeAttribute(d.source.key, 'hidden');
const targetIsVisible = !graph.getNodeAttribute(d.target.key, 'hidden');
return typeIsActive && sourceIsVisible && targetIsVisible ? null : 'none';
});
}
// Note: No need to update counters for links usually, unless you add a link counter UI element.
}

let highlightedNodes = [];
Expand All @@ -441,7 +511,7 @@ function highlightNodes(nodeIds) {
.forEach((nodeId) => {
const { links, node } = getNodeNetwork(nodeId);
node.node().classList.add('highlight');
links.nodes().forEach((elt) => elt.classList.add('highlight'));
elts.links.nodes().forEach((elt) => elt.classList.add('highlight'));
});

highlightedNodes = highlightedNodes.concat(nodeIds);
Expand All @@ -461,7 +531,7 @@ function unlightNodes() {
.forEach((nodeId) => {
const { links, node } = getNodeNetwork(nodeId);
node.node().classList.remove('highlight');
links.nodes().forEach((elt) => elt.classList.remove('highlight'));
elts.links.nodes().forEach((elt) => elt.classList.remove('highlight'));
});

highlightedNodes = [];
Expand All @@ -472,12 +542,10 @@ function unlightNodes() {
* @param {boolean} isChecked - 'checked' value send by a checkbox input
*/

let linksDisplayToggle = true; // Keep track of global link toggle state
window.linksDisplayToggle = function (isChecked) {
if (isChecked) {
elts.links.nodes().forEach((elt) => elt.classList.remove('hide'));
} else {
elts.links.nodes().forEach((elt) => elt.classList.add('hide'));
}
linksDisplayToggle = isChecked;
updateLinkVisibilityBasedOnFiltersAndNodes(); // Update visibility when toggled
};

/**
Expand All @@ -493,6 +561,12 @@ window.labelDisplayToggle = function (isChecked) {
}
};

let linkLabelsDisplayToggle = true; // Keep track of global label toggle state
window.linkLabelDisplayToggle = function (isChecked) {
linkLabelsDisplayToggle = isChecked;
updateLinkVisibilityBasedOnFiltersAndNodes(); // Update visibility when toggled
};

/**
* Change the font size of graph labels
*/
Expand Down Expand Up @@ -626,10 +700,9 @@ hotkeys('c', (e) => {
export {
svg,
svgSub,
hideNodes,
displayNodes,
displayNodesAll,
setNodesDisplaying,
updateLinkVisibility,
highlightNodes,
unlightNodes,
translate,
Expand Down
17 changes: 14 additions & 3 deletions core/frontend/view.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,25 @@ window.addEventListener('DOMContentLoaded', () => {
const activeFilters = Array.from(document.querySelectorAll('#types-form input:checked')).map(
({ name }) => name,
);
const activeTags = Array.from(document.querySelectorAll('#tags-form input:checked')).map(
({ name }) => name,
);
const linkForm = document.getElementById('link-types-form');
const activeLinkFilters = linkForm ? Array.from(linkForm.querySelectorAll('input:checked')).map(
({ name }) => name,
) : [];
const tagForm = document.getElementById('tags-form');
const activeTags = tagForm ? Array.from(tagForm.querySelectorAll('input:checked')).map(
({ name }) => name,
) : [];
const focusLevel = document.getElementById('focus-input').value;

if (activeFilters.length > 0) {
url.searchParams.set('filters', activeFilters.join('-'));
}

if (activeLinkFilters.length > 0 && activeLinkFilters.length < Object.keys(linkTypeList).length) {
url.searchParams.set('link-filters', activeLinkFilters.join('-'));
} else {
url.searchParams.delete('link-filters');
}
if (activeTags.length > 0) {
url.searchParams.set('tags', activeTags.join('-'));
}
Expand Down
Loading