Public
Edited
Apr 15
Insert cell
Insert cell
Insert cell
// Visualization with connections and animated gradients
viewof timeline = {
// Create container with flex layout for filter panel and visualization
const container = document.createElement('div');
container.style.width = "100%";
container.style.display = "flex";
container.style.flexDirection = "row";
// Create filter panel
const filterPanel = document.createElement('div');
filterPanel.id = 'filter-panel';
filterPanel.style.width = '140px';
filterPanel.style.border = '1px solid #ddd';
filterPanel.style.borderRadius = '5px';
filterPanel.style.padding = '15px';
filterPanel.style.marginRight = '15px';
filterPanel.style.alignSelf = 'flex-start';
filterPanel.style.maxHeight = '80vh';
filterPanel.style.overflowY = 'auto';
filterPanel.style.fontSize = '12px';
// Add filter panel heading
const filterHeading = document.createElement('h3');
filterHeading.textContent = 'Filter by Skills';
filterHeading.style.marginTop = '0';
filterHeading.style.paddingBottom = '8px';
filterHeading.style.borderBottom = '1px solid #eee';
filterHeading.style.color = '#333';
filterHeading.style.fontSize = '14px';
filterPanel.appendChild(filterHeading);
// Add skills filter container
const skillsContainer = document.createElement('div');
skillsContainer.id = 'skills-filter-container';
skillsContainer.style.marginBottom = '15px';
filterPanel.appendChild(skillsContainer);
// Add filter buttons
const filterButtons = document.createElement('div');
filterButtons.style.display = 'flex';
filterButtons.style.justifyContent = 'space-between';
const applyButton = document.createElement('button');
applyButton.id = 'apply-filters';
applyButton.textContent = 'Apply';
applyButton.style.padding = '6px 10px';
applyButton.style.backgroundColor = '#4CAF50';
applyButton.style.color = 'white';
applyButton.style.border = '1px solid #45a049';
applyButton.style.borderRadius = '4px';
applyButton.style.cursor = 'pointer';
applyButton.style.fontSize = '12px';
const clearButton = document.createElement('button');
clearButton.id = 'clear-filters';
clearButton.textContent = 'Clear';
clearButton.style.padding = '6px 10px';
clearButton.style.backgroundColor = '#f0f0f0';
clearButton.style.border = '1px solid #ccc';
clearButton.style.borderRadius = '4px';
clearButton.style.cursor = 'pointer';
clearButton.style.fontSize = '12px';
filterButtons.appendChild(applyButton);
filterButtons.appendChild(clearButton);
filterPanel.appendChild(filterButtons);
// Add animation button
const animationDiv = document.createElement('div');
animationDiv.style.marginTop = '15px';
const animateButton = document.createElement('button');
animateButton.id = 'animate-timeline';
animateButton.textContent = 'Animate Timeline';
animateButton.style.width = '100%';
animateButton.style.padding = '8px';
animateButton.style.backgroundColor = '#4a90e2';
animateButton.style.color = 'white';
animateButton.style.border = 'none';
animateButton.style.borderRadius = '4px';
animateButton.style.cursor = 'pointer';
animationDiv.appendChild(animateButton);
filterPanel.appendChild(animationDiv);
// Create visualization area
const vizArea = document.createElement('div');
vizArea.id = 'visualization';
vizArea.style.flex = '1';
vizArea.style.border = '1px solid #ddd';
vizArea.style.borderRadius = '5px';
vizArea.style.overflow = 'auto';
vizArea.style.marginBottom = '20px';
vizArea.style.position = 'relative';
vizArea.style.minHeight = '600px';

// Create info panel for project details
const infoPanel = document.createElement('div');
infoPanel.id = 'info-panel';
infoPanel.style.position = 'absolute';
infoPanel.style.top = '20px';
infoPanel.style.right = '20px';
infoPanel.style.width = '250px';
infoPanel.style.backgroundColor = 'white';
infoPanel.style.border = '1px solid #ddd';
infoPanel.style.borderRadius = '5px';
infoPanel.style.boxShadow = '0 2px 10px rgba(0,0,0,0.1)';
infoPanel.style.padding = '15px';
infoPanel.style.zIndex = '1000';
infoPanel.style.display = 'none';
infoPanel.style.fontSize = '12px';

// Add info panel header
const panelHeader = document.createElement('div');
panelHeader.className = 'panel-header';
panelHeader.style.display = 'flex';
panelHeader.style.justifyContent = 'space-between';
panelHeader.style.alignItems = 'center';
panelHeader.style.marginBottom = '15px';

const panelHeading = document.createElement('h3');
panelHeading.style.margin = '0';
panelHeading.style.paddingBottom = '8px';
panelHeading.style.borderBottom = '1px solid #eee';
panelHeading.style.color = '#333';
panelHeading.style.fontSize = '14px';

const colorIndicator = document.createElement('span');
colorIndicator.className = 'node-color-indicator';
colorIndicator.style.width = '20px';
colorIndicator.style.height = '20px';
colorIndicator.style.borderRadius = '50%';
colorIndicator.style.display = 'inline-block';
colorIndicator.style.marginRight = '10px';

const nodeName = document.createElement('span');
nodeName.id = 'node-name';

panelHeading.appendChild(colorIndicator);
panelHeading.appendChild(nodeName);

const closeButton = document.createElement('span');
closeButton.className = 'close-button';
closeButton.textContent = '×';
closeButton.style.position = 'absolute';
closeButton.style.top = '10px';
closeButton.style.right = '10px';
closeButton.style.cursor = 'pointer';
closeButton.style.fontSize = '18px';
closeButton.style.color = '#999';

panelHeader.appendChild(panelHeading);
panelHeader.appendChild(closeButton);
infoPanel.appendChild(panelHeader);

// Add info rows for project details
const infoFields = [
{ id: 'node-id', label: 'ID:' },
{ id: 'node-start-date', label: 'Start Date:' },
{ id: 'node-end-date', label: 'End Date:' },
{ id: 'node-duration', label: 'Duration:' },
{ id: 'node-category', label: 'Category:' },
{ id: 'node-phase', label: 'Phase:' },
{ id: 'node-skills', label: 'Skills:' },
{ id: 'node-connections', label: 'Connections:' },
{ id: 'node-description', label: 'Description:' }
];

infoFields.forEach(field => {
const row = document.createElement('div');
row.className = 'info-row';
row.style.display = 'flex';
row.style.marginBottom = '6px';
row.style.alignItems = 'flex-start'; // Align items to the top for multi-line content

const label = document.createElement('div');
label.className = 'info-label';
label.textContent = field.label;
label.style.fontWeight = 'bold';
label.style.width = '80px';
label.style.color = '#666';
label.style.flexShrink = '0'; // Prevent label from shrinking

const value = document.createElement('div');
value.className = 'info-value';
value.id = field.id;
value.style.flexGrow = '1';
value.style.wordWrap = 'break-word'; // Enable word wrapping
value.style.maxWidth = 'calc(100% - 85px)'; // Ensure text doesn't overflow

row.appendChild(label);
row.appendChild(value);
infoPanel.appendChild(row);
});

// Special styling for description field
const descriptionElement = document.getElementById('node-description');
if (descriptionElement) {
descriptionElement.style.display = 'block';
descriptionElement.style.lineHeight = '1.4';
descriptionElement.style.marginTop = '4px';
}
vizArea.appendChild(infoPanel);
// Append elements to container
container.appendChild(filterPanel);
container.appendChild(vizArea);
// Create SVG in vizArea
const width = 900;
const height = 700;
const margin = {top: 40, right: 150, bottom: 40, left: 75};
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height);
// Create defs for gradients
const defs = svg.append("defs");
// Parse dates
const parseDate = d3.timeParse("%Y-%m-%d");
// Process projects
const projects = data.projects.map((p, i) => ({
...p,
startDate: parseDate(p.startDate),
endDate: p.endDate ? parseDate(p.endDate) : new Date(),
hasDefinedEndDate: !!p.endDate,
index: i,
color: d3.interpolateSpectral(i / data.projects.length),
sourceLinks: [],
targetLinks: []
}));
// Process connections
const connections = data.connections ? data.connections.map((conn, i) => {
const source = projects.find(p => p.id === conn.source);
const target = projects.find(p => p.id === conn.target);
if (!source || !target) {
console.error(`Connection references unknown project: ${conn.source} -> ${conn.target}`);
return null;
}
return {
source,
target,
value: conn.value,
gradient: `gradient-${i}`,
path: `path-${i}`
};
}).filter(conn => conn !== null) : [];
// Assign connections to projects
connections.forEach(conn => {
conn.source.sourceLinks.push(conn);
conn.target.targetLinks.push(conn);
});
// State for tracking selected node and filters
const state = {
selectedNode: null,
filteredSkills: [],
visibleNodes: new Set(projects.map(p => p.id)),
isAnimating: false,
animationTimeout: null,
animatedNodes: new Set(),
isNodeVisible: function(node) {
return this.visibleNodes.has(node.id);
},
updateVisibleNodes: function() {
if (this.filteredSkills.length === 0) {
this.visibleNodes = new Set(projects.map(p => p.id));
} else {
this.visibleNodes = new Set(
projects
.filter(node =>
node.skills &&
node.skills.some(skill => this.filteredSkills.includes(skill))
)
.map(n => n.id)
);
}
},
stopAnimation: function() {
if (this.isAnimating && this.animationTimeout) {
clearTimeout(this.animationTimeout);
this.animationTimeout = null;
}
this.isAnimating = false;
}
};
// Extract unique skills for filter
const extractUniqueSkills = (projects) => {
const skillsSet = new Set();
projects.forEach(project => {
if (project.skills && Array.isArray(project.skills)) {
project.skills.forEach(skill => skillsSet.add(skill));
}
});
return Array.from(skillsSet).sort();
};
// Populate skills filter
const populateSkillsFilter = (skills) => {
skills.forEach(skill => {
const checkbox = document.createElement('label');
checkbox.style.display = 'block';
checkbox.style.marginBottom = '8px';
const input = document.createElement('input');
input.type = 'checkbox';
input.value = skill;
input.style.marginRight = '8px';
checkbox.appendChild(input);
checkbox.appendChild(document.createTextNode(skill));
skillsContainer.appendChild(checkbox);
});
};
// Create time scale
const timeScale = d3.scaleTime()
.domain([
d3.min(projects, d => d.startDate),
d3.max(projects, d => d.endDate)
])
.range([margin.left, width - margin.right]);
// Draw the x-axis
svg.append("g")
.attr("transform", `translate(0, ${height - margin.bottom})`)
.call(d3.axisBottom(timeScale).tickFormat(d3.timeFormat("%b %Y")))
.selectAll("text")
.style("text-anchor", "end")
.attr("dx", "-.8em")
.attr("dy", ".15em")
.attr("transform", "rotate(-45)");
// Simple vertical distribution
const yScale = d3.scalePoint()
.domain(projects.map(d => d.id))
.range([margin.top, height - margin.bottom - 30])
.padding(0.5);
// Create node gradients
const nodeGradients = defs.selectAll("linearGradient.node-gradient")
.data(projects)
.join("linearGradient")
.attr("class", "node-gradient")
.attr("id", d => `node-gradient-${d.id}`)
.attr("gradientUnits", "userSpaceOnUse")
.attr("x1", d => timeScale(d.startDate))
.attr("x2", d => timeScale(d.endDate));
// Add gradient stops
nodeGradients.append("stop")
.attr("offset", "0%")
.attr("stop-color", d => d.color);
nodeGradients.append("stop")
.attr("offset", "100%")
.attr("stop-color", "#f8f8f8");
// Create link gradients
if (connections.length > 0) {
const linkGradients = defs.selectAll("linearGradient.link-gradient")
.data(connections)
.join("linearGradient")
.attr("class", "link-gradient")
.attr("id", d => d.gradient)
.attr("gradientUnits", "userSpaceOnUse")
.attr("x1", d => timeScale(d.source.endDate))
.attr("y1", d => yScale(d.source.id))
.attr("x2", d => timeScale(d.target.startDate))
.attr("y2", d => yScale(d.target.id));
linkGradients.append("stop")
.attr("offset", "0%")
.attr("stop-color", d => d.source.color);
linkGradients.append("stop")
.attr("offset", "100%")
.attr("stop-color", d => d.target.color);
}
// Create link curve path generator
const createLinkPath = (d) => {
const sourceX = timeScale(d.source.endDate);
const sourceY = yScale(d.source.id);
const targetX = timeScale(d.target.startDate);
const targetY = yScale(d.target.id);
// Control points for curve
const xDist = targetX - sourceX;
const controlPointOffset = Math.min(80, xDist * 0.4);
return `M ${sourceX},${sourceY}
C ${sourceX + controlPointOffset},${sourceY}
${targetX - controlPointOffset},${targetY}
${targetX},${targetY}`;
};
// Draw the gray background links
let links = null;
if (connections.length > 0) {
links = svg.selectAll("path.link")
.data(connections)
.join("path")
.attr("class", "link")
.attr("d", createLinkPath)
.attr("stroke", "lightgrey")
.attr("stroke-opacity", 0.1)
.attr("stroke-width", d => Math.max(1, d.value / 5))
.attr("fill", "none");
links.append("title")
.text(d => `${d.source.name} → ${d.target.name}`);
}
// Create gradient links (initially hidden)
let gradientLinks = null;
if (connections.length > 0) {
gradientLinks = svg.selectAll("path.gradient-link")
.data(connections)
.join("path")
.attr("class", "gradient-link")
.attr("id", d => d.path)
.attr("d", createLinkPath)
.attr("stroke", d => `url(#${d.gradient})`)
.attr("stroke-opacity", 0)
.attr("stroke-width", d => Math.max(1, d.value / 5))
.attr("fill", "none");
// Initialize dash pattern for animation
gradientLinks.each(function(d) {
const path = d3.select(this);
const length = path.node().getTotalLength();
path.attr("stroke-dasharray", `${length} ${length}`)
.attr("stroke-dashoffset", length);
});
}
// Draw project bars with interactivity
const nodes = svg.selectAll("rect.node")
.data(projects)
.join("rect")
.attr("class", "node")
.attr("id", d => `node-${d.id}`)
.attr("x", d => timeScale(d.startDate))
.attr("y", d => yScale(d.id) - 10)
.attr("width", d => Math.max(5, timeScale(d.endDate) - timeScale(d.startDate)))
.attr("height", 20)
.attr("rx", 5)
.attr("fill", d => `url(#node-gradient-${d.id})`)
.attr("opacity", 0.9)
.attr("stroke", d => d3.rgb(d.color).darker())
.attr("stroke-width", 1)
.attr("cursor", "pointer");
// Add labels
svg.selectAll("text.label")
.data(projects)
.join("text")
.attr("class", "label")
.attr("id", d => `label-${d.id}`)
.attr("x", margin.left - 5)
.attr("y", d => yScale(d.id))
.attr("text-anchor", "end")
.attr("font-family", "Arial, sans-serif")
.attr("font-size", 11)
.text(d => d.name);
// Show info panel function
const showInfoPanel = (project) => {
const dateFormat = d3.timeFormat("%b %d, %Y");
// Set color indicator and name
const colorIndicator = infoPanel.querySelector('.node-color-indicator');
colorIndicator.style.backgroundColor = project.color;
document.getElementById('node-name').textContent = project.name;
document.getElementById('node-id').textContent = project.id;
document.getElementById('node-start-date').textContent = dateFormat(project.startDate);
document.getElementById('node-end-date').textContent = project.hasDefinedEndDate ? dateFormat(project.endDate) : "Ongoing";
document.getElementById('node-duration').textContent = project.duration;
document.getElementById('node-category').textContent = project.category;
document.getElementById('node-phase').textContent = project.phase || '-';
// Skills display
const skillsElement = document.getElementById('node-skills');
skillsElement.innerHTML = '';
if (project.skills && project.skills.length > 0) {
project.skills.forEach(skill => {
const skillTag = document.createElement('span');
skillTag.style.display = 'inline-block';
skillTag.style.backgroundColor = '#f0f0f0';
skillTag.style.borderRadius = '10px';
skillTag.style.padding = '2px 8px';
skillTag.style.margin = '2px';
skillTag.style.fontSize = '11px';
skillTag.textContent = skill;
skillsElement.appendChild(skillTag);
});
} else {
skillsElement.textContent = 'None specified';
}
// Show connections
const connectionsElement = document.getElementById('node-connections');
connectionsElement.innerHTML = '';
if (project.sourceLinks.length === 0 && project.targetLinks.length === 0) {
connectionsElement.textContent = 'None';
} else {
// Incoming connections
if (project.targetLinks.length > 0) {
const incomingHeader = document.createElement('div');
incomingHeader.textContent = 'Incoming:';
incomingHeader.style.fontWeight = 'bold';
incomingHeader.style.marginTop = '5px';
connectionsElement.appendChild(incomingHeader);
const incomingList = document.createElement('ul');
incomingList.style.margin = '3px 0';
incomingList.style.paddingLeft = '15px';
project.targetLinks.forEach(link => {
const item = document.createElement('li');
item.textContent = link.source.name;
incomingList.appendChild(item);
});
connectionsElement.appendChild(incomingList);
}
// Outgoing connections
if (project.sourceLinks.length > 0) {
const outgoingHeader = document.createElement('div');
outgoingHeader.textContent = 'Outgoing:';
outgoingHeader.style.fontWeight = 'bold';
outgoingHeader.style.marginTop = '5px';
connectionsElement.appendChild(outgoingHeader);
const outgoingList = document.createElement('ul');
outgoingList.style.margin = '3px 0';
outgoingList.style.paddingLeft = '15px';
project.sourceLinks.forEach(link => {
const item = document.createElement('li');
item.textContent = link.target.name;
outgoingList.appendChild(item);
});
connectionsElement.appendChild(outgoingList);
}
}
document.getElementById('node-description').textContent = project.description || 'No description';
// Show the panel
infoPanel.style.display = 'block';
};
// Hide info panel function
const hideInfoPanel = () => {
infoPanel.style.display = 'none';
};
// Node selection functions
const highlightNode = (project) => {
if (!state.isNodeVisible(project)) return;
// Highlight node
d3.select(`#node-${project.id}`)
.transition()
.duration(200)
.attr("opacity", 1)
.attr("stroke-width", 2);
// Highlight label
d3.select(`#label-${project.id}`)
.transition()
.duration(200)
.attr("font-weight", "bold");
// Animate the node gradient
defs.select(`#node-gradient-${project.id}`)
.selectAll("stop")
.transition()
.duration(500)
.attr("offset", function(d, i) {
return i === 0 ? "100%" : "100%"; // Both stops move to 100%
});
};
const resetNode = (project) => {
if (!project) return;
// Reset node
d3.select(`#node-${project.id}`)
.transition()
.duration(200)
.attr("opacity", state.isNodeVisible(project) ? 0.9 : 0.3)
.attr("stroke-width", 1);
// Reset label
d3.select(`#label-${project.id}`)
.transition()
.duration(200)
.attr("font-weight", "normal")
.attr("opacity", state.isNodeVisible(project) ? 1 : 0.3);
// Reset gradient
defs.select(`#node-gradient-${project.id}`)
.selectAll("stop")
.transition()
.duration(200)
.attr("offset", function(d, i) {
return i === 0 ? "0%" : "100%"; // Reset to original positions
});
};
const resetAllNodes = () => {
// Reset all nodes
nodes
.transition()
.duration(200)
.attr("opacity", d => state.isNodeVisible(d) ? 0.9 : 0.3)
.attr("stroke-width", 1);
// Reset all labels
svg.selectAll("text.label")
.transition()
.duration(200)
.attr("font-weight", "normal")
.attr("opacity", d => state.isNodeVisible(d) ? 1 : 0.3);
// Reset all gradients
defs.selectAll("linearGradient.node-gradient")
.selectAll("stop")
.transition()
.duration(200)
.attr("offset", function(d, i) {
return i === 0 ? "0%" : "100%";
});
// Reset links
resetLinks();
};
// Animate links from a node
const animateLinksFromNode = (project, depth = 0) => {
if (depth > 10 || !state.isNodeVisible(project) || !project.sourceLinks || project.sourceLinks.length === 0) {
return;
}
// Find visible outgoing links
const visibleLinks = project.sourceLinks.filter(link =>
state.isNodeVisible(link.target));
if (visibleLinks.length === 0) return;
// Track animated nodes
state.animatedNodes.add(project.id);
// Animate each link
visibleLinks.forEach(link => {
state.animatedNodes.add(link.target.id);
const linkElement = d3.select(`#${link.path}`);
linkElement
.attr("stroke-opacity", 0.8)
.transition()
.duration(500)
.ease(d3.easeLinear)
.attr("stroke-dashoffset", 0)
.on("end", () => {
if (state.selectedNode) {
// Highlight the target node
highlightNode(link.target);
// Continue the animation chain
setTimeout(() => {
animateLinksFromNode(link.target, depth + 1);
}, 200);
}
});
});
};
// Reset all links
const resetLinks = () => {
if (!gradientLinks) return;
gradientLinks
.interrupt()
.attr("stroke-opacity", 0)
.each(function() {
const path = d3.select(this);
const length = path.node().getTotalLength();
path
.attr("stroke-dasharray", `${length} ${length}`)
.attr("stroke-dashoffset", length);
});
state.animatedNodes = new Set();
};
const selectNode = (project) => {
// Update state
state.selectedNode = project;
// Highlight node
highlightNode(project);
// Show info panel
showInfoPanel(project);
// Animate links
setTimeout(() => {
animateLinksFromNode(project);
}, 500);
};
const deselectNode = () => {
if (!state.selectedNode) return;
// Reset all animated nodes
state.animatedNodes.forEach(nodeId => {
const animatedNode = projects.find(p => p.id === nodeId);
if (animatedNode) {
resetNode(animatedNode);
}
});
// Reset the selected node
resetNode(state.selectedNode);
resetLinks();
hideInfoPanel();
// Reset state
state.selectedNode = null;
};
// Add click handlers
nodes.on("click", (event, d) => {
event.stopPropagation();
// Stop any ongoing animation
state.stopAnimation();
if (!state.isNodeVisible(d)) return;
if (state.selectedNode === d) {
deselectNode();
} else {
if (state.selectedNode) deselectNode();
selectNode(d);
}
});
// Mouse over/out for nodes
nodes
.on("mouseover", (event, d) => {
if (!state.isNodeVisible(d) || state.selectedNode) return;
highlightNode(d);
})
.on("mouseout", (event, d) => {
if (!state.isNodeVisible(d) || state.selectedNode === d) return;
resetNode(d);
});
// Close button for info panel
closeButton.addEventListener('click', (event) => {
event.stopPropagation();
deselectNode();
});
// Background click to deselect
svg.append("rect")
.attr("width", width)
.attr("height", height)
.attr("fill", "transparent")
.lower() // Send to back
.on("click", () => {
// Stop any ongoing animation
state.stopAnimation();
// Reset all nodes
nodes
.transition()
.duration(200)
.attr("opacity", d => state.isNodeVisible(d) ? 0.9 : 0.3)
.attr("stroke-width", 1);
// Reset all labels
svg.selectAll("text.label")
.transition()
.duration(200)
.attr("font-weight", "normal")
.attr("opacity", d => state.isNodeVisible(d) ? 1 : 0.3);
// Reset all gradients
defs.selectAll("linearGradient.node-gradient")
.selectAll("stop")
.transition()
.duration(200)
.attr("offset", function(d, i) {
return i === 0 ? "0%" : "100%";
});
// Reset links
resetLinks();
// Hide info panel
hideInfoPanel();
// Reset state
state.selectedNode = null;
state.animatedNodes = new Set();
});
// Enhanced Animate Timeline functionality
const animateTimelineSequentially = () => {
// Reset any previous state
if (state.selectedNode) {
deselectNode();
}
// If animation is already in progress, stop it
state.stopAnimation();
state.isAnimating = true;
state.animatedNodes = new Set(); // Reset animated nodes tracking
// Get visible nodes sorted by start date
const visibleNodes = projects
.filter(p => state.isNodeVisible(p))
.sort((a, b) => a.startDate - b.startDate);
if (visibleNodes.length === 0) return;
// Function to animate a single node
const animateNode = (index) => {
if (index >= visibleNodes.length || !state.isAnimating) {
state.isAnimating = false;
return;
}
const project = visibleNodes[index];
// Simulate node selection
state.selectedNode = project;
highlightNode(project);
// showInfoPanel(project);
// Animate outgoing links
setTimeout(() => {
animateLinksFromNode(project);
}, 500);
// Schedule the next node with a shorter delay
state.animationTimeout = setTimeout(() => {
animateNode(index + 1);
}, 250); // Delay between nodes (0.25 second)
};
// Start the animation
animateNode(0);
};
// Set up filter functions
const applyFilter = () => {
const checkboxes = skillsContainer.querySelectorAll('input[type="checkbox"]:checked');
state.filteredSkills = Array.from(checkboxes).map(cb => cb.value);
state.updateVisibleNodes();
// Update node visibility
nodes
.transition()
.duration(200)
.attr("opacity", d => state.isNodeVisible(d) ? 0.9 : 0.3)
.attr("cursor", d => state.isNodeVisible(d) ? "pointer" : "default");
// Update label visibility
svg.selectAll("text.label")
.transition()
.duration(200)
.attr("opacity", d => state.isNodeVisible(d) ? 1 : 0.3);
// Update link visibility
if (connections.length > 0) {
svg.selectAll("path.link")
.transition()
.duration(200)
.attr("stroke-opacity", d =>
state.isNodeVisible(d.source) && state.isNodeVisible(d.target) ? 0.1 : 0.03);
}
// If selected node is filtered out, deselect it
if (state.selectedNode && !state.isNodeVisible(state.selectedNode)) {
deselectNode();
}
};
const clearFilter = () => {
// Uncheck all checkboxes
skillsContainer.querySelectorAll('input[type="checkbox"]').forEach(cb => {
cb.checked = false;
});
state.filteredSkills = [];
state.updateVisibleNodes();
// Stop any ongoing animation
state.stopAnimation();
// Reset all nodes
nodes
.transition()
.duration(200)
.attr("opacity", 0.9)
.attr("cursor", "pointer")
.attr("stroke-width", 1); // Reset stroke width
// Reset all labels
svg.selectAll("text.label")
.transition()
.duration(200)
.attr("font-weight", "normal") // Reset font weight
.attr("opacity", 1);
// Reset all gradients
defs.selectAll("linearGradient.node-gradient")
.selectAll("stop")
.transition()
.duration(200)
.attr("offset", function(d, i) {
return i === 0 ? "0%" : "100%";
});
// Reset links
resetLinks();
// Hide info panel
hideInfoPanel();
// Reset state
state.selectedNode = null;
state.animatedNodes = new Set();
};
// Add event listeners for filter buttons
applyButton.addEventListener('click', applyFilter);
clearButton.addEventListener('click', clearFilter);
// Add event listener for animation button
animateButton.addEventListener('click', animateTimelineSequentially);
// Populate skills filter
populateSkillsFilter(extractUniqueSkills(projects));
// Add SVG to the vizArea
vizArea.appendChild(svg.node());
return container;
}
Insert cell
Insert cell
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more