Created
February 28, 2025 23:53
-
-
Save PatrickFanella/5acac1cdf91c1ad2da2bd10fc8000ac7 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>D3 Org Chart</title> | |
<script src="https://d3js.org/d3.v7.min.js"></script> | |
<script src="https://cdn.jsdelivr.net/npm/[email protected]"></script> | |
<script src="https://cdn.jsdelivr.net/npm/[email protected]/build/d3-flextree.js"></script> | |
<style> | |
body { | |
margin: 0; | |
padding: 0; | |
width: 100vw; | |
height: 100vh; | |
} | |
.chart-container { | |
width: 100%; | |
height: 100vh; | |
} | |
/* Add these new styles */ | |
#sidebar { | |
width: 750px; | |
min-width: 200px; | |
max-width: 800px; | |
background: #f5f5f5; | |
border-left: 1px solid #ddd; | |
height: 100vh; | |
box-shadow: -2px 0 5px rgba(0,0,0,0.1); | |
position: fixed; | |
right: 0; | |
top: 0; | |
transform: translateX(100%); | |
transition: transform 0.3s ease; | |
z-index: 1000; | |
} | |
#sidebar.visible { | |
transform: translateX(0); | |
} | |
#close-sidebar { | |
position: absolute; | |
top: 20px; | |
right: 20px; | |
cursor: pointer; | |
font-size: 24px; | |
color: #666; | |
background: none; | |
border: none; | |
padding: 0; | |
z-index: 2; | |
width: 30px; | |
height: 30px; | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
border-radius: 50%; | |
transition: background-color 0.2s; | |
} | |
#close-sidebar:hover { | |
color: #333; | |
background-color: rgba(0, 0, 0, 0.05); | |
} | |
#sidebar-content { | |
padding: 20px; | |
height: 100%; | |
display: flex; | |
flex-direction: column; | |
} | |
#sidebarTitle { | |
margin-top: 0; | |
margin-bottom: 20px; | |
padding-right: 30px; | |
position: sticky; | |
top: 0; | |
background: #f5f5f5; | |
padding-top: 20px; | |
z-index: 1; | |
} | |
#sidebarContent { | |
flex: 1; | |
overflow-y: auto; | |
padding-right: 10px; | |
} | |
/* Table styles */ | |
table { | |
width: 100%; | |
border-collapse: collapse; | |
margin-top: 10px; | |
table-layout: fixed; | |
} | |
th, td { | |
padding: 12px 8px; | |
text-align: left; | |
border-bottom: 1px solid #ddd; | |
} | |
td { | |
vertical-align: top; | |
} | |
th { | |
background-color: #fff; | |
position: sticky; | |
top: 0; | |
z-index: 1; | |
} | |
th:nth-child(1) { width: 20%; } | |
th:nth-child(2) { width: 25%; } | |
th:nth-child(3) { width: 15%; } | |
th:nth-child(4) { width: 40%; } | |
.text-muted { | |
color: #666; | |
font-size: 0.85em; | |
} | |
.node { | |
cursor: pointer; | |
} | |
.node.selected rect { | |
stroke: #2196F3; | |
stroke-width: 2px; | |
} | |
/* Add these resize-related styles */ | |
#sidebar-resizer { | |
position: absolute; | |
left: 0; | |
top: 0; | |
bottom: 0; | |
width: 4px; | |
cursor: ew-resize; | |
background: transparent; | |
} | |
#sidebar-resizer:hover { | |
background: rgba(0, 0, 0, 0.1); | |
} | |
/* Add these search-related styles */ | |
#search-container { | |
margin: 10px 0; | |
padding: 0 0 10px 0; | |
position: sticky; | |
top: 60px; | |
background: #f5f5f5; | |
z-index: 1; | |
} | |
#employeeSearch { | |
width: 100%; | |
padding: 8px; | |
border: 1px solid #ddd; | |
border-radius: 4px; | |
font-size: 14px; | |
box-sizing: border-box; | |
} | |
#employeeSearch:focus { | |
outline: none; | |
border-color: #2196F3; | |
box-shadow: 0 0 3px rgba(33, 150, 243, 0.3); | |
} | |
tr.hidden { | |
display: none; | |
} | |
.highlight { | |
background-color: yellow; | |
padding: 2px; | |
border-radius: 2px; | |
} | |
/* Add these new styles */ | |
#colorSchemeSelector { | |
position: fixed; | |
top: 20px; | |
left: 20px; | |
background: white; | |
padding: 15px; | |
border-radius: 8px; | |
box-shadow: 0 2px 10px rgba(0,0,0,0.1); | |
z-index: 1000; | |
} | |
.radio-group { | |
display: flex; | |
flex-direction: column; | |
gap: 8px; | |
} | |
.radio-option { | |
display: flex; | |
align-items: center; | |
gap: 8px; | |
cursor: pointer; | |
} | |
.radio-option input[type="radio"] { | |
cursor: pointer; | |
} | |
</style> | |
</head> | |
<body> | |
<div id="colorSchemeSelector"> | |
<h4 style="margin: 0 0 10px 0">Color Scheme</h4> | |
<div class="radio-group"> | |
<label class="radio-option"> | |
<input type="radio" name="colorScheme" value="none" checked onchange="updateColorScheme(this.value)"> | |
None | |
</label> | |
<label class="radio-option"> | |
<input type="radio" name="colorScheme" value="workforce" onchange="updateColorScheme(this.value)"> | |
Percentage of Workforce | |
</label> | |
<label class="radio-option"> | |
<input type="radio" name="colorScheme" value="union" onchange="updateColorScheme(this.value)"> | |
Union Percentage | |
</label> | |
<label class="radio-option"> | |
<input type="radio" name="colorScheme" value="fulltime" onchange="updateColorScheme(this.value)"> | |
Full-Time Percentage | |
</label> | |
<label class="radio-option"> | |
<input type="radio" name="colorScheme" value="statutory" onchange="updateColorScheme(this.value)"> | |
Statutorily Required | |
</label> | |
</div> | |
</div> | |
<div class="chart-container"></div> | |
<!-- Add sidebar HTML --> | |
<div id="sidebar"> | |
<div id="sidebar-resizer"></div> | |
<button id="close-sidebar" onclick="closeSidebar()">×</button> | |
<div id="sidebar-content"> | |
<h3 id="sidebarTitle">Select a department</h3> | |
<div id="search-container"> | |
<input | |
type="text" | |
id="employeeSearch" | |
placeholder="Search employees..." | |
onkeyup="filterEmployees()" | |
> | |
</div> | |
<div id="sidebarContent"></div> | |
</div> | |
</div> | |
<script src="org_chart_data.js"></script> | |
<script> | |
// Add this list at the top of your script | |
const highlightedOffices = [ | |
"Operations", | |
"Field Operations", | |
"OFFICE OF GOVERNMENT CONTRACTING & BUSINESS DEVELOPMENT", | |
"PERSONNEL SECURITY DIVISION", | |
// "Office of Financial Assistance", | |
// "Office of Performance and Systems Management", | |
// "Office of Credit Risk Management", | |
// "Office of Surety Guarantees", | |
// "7(a)", | |
// "504 Program", | |
// "Secondary Market (SMG)", | |
// "Microloan Enterprise Development" | |
// Add more office names here | |
]; | |
function transformOrgData(node, parentId = "", prefix = "") { | |
let result = []; | |
// Create current node | |
const currentId = prefix ? `${prefix}-${node.name}` : node.name; | |
// Calculate statistics for this node | |
const allEmployees = getAllEmployeesFromNode(node); | |
const totalEmployees = allEmployees.length; | |
// Calculate union percentage | |
const unionEmployees = allEmployees.filter(emp => [8888, 7777].includes(emp.union_status)); | |
const unionPercentage = totalEmployees > 0 ? unionEmployees.length / totalEmployees : 0; | |
// Calculate full-time percentage | |
const fulltimeEmployees = allEmployees.filter(emp => | |
emp.type === "Full-Time" || emp.type === "Full Time" | |
); | |
const fulltimePercentage = totalEmployees > 0 ? fulltimeEmployees.length / totalEmployees : 0; | |
const currentNode = { | |
id: currentId, | |
parentId: parentId, | |
name: node.name, | |
position: `${node.name}`, | |
employeeCount: node.employees ? node.employees.length : 0, | |
totalEmployees: totalEmployees, | |
unionPercentage: unionPercentage, | |
fulltimePercentage: fulltimePercentage | |
}; | |
// Add current node to results | |
result.push(currentNode); | |
// Process children | |
if (node.children && node.children.length > 0) { | |
node.children.forEach((child, index) => { | |
const childResults = transformOrgData(child, currentId, `${prefix}${index}`); | |
result = result.concat(childResults); | |
}); | |
} | |
return result; | |
} | |
document.addEventListener('DOMContentLoaded', () => { | |
// Make transformedData available globally | |
window.transformedData = transformOrgData(data); | |
const totalCompanyEmployees = calculateTotalEmployees(data); | |
// Debug: Log the transformed data to see if percentages are calculated correctly | |
console.log("Transformed data with stats:", window.transformedData); | |
// Make chartInstance available globally | |
window.chartInstance = new d3.OrgChart() | |
.container('.chart-container') | |
.nodeHeight(d => { | |
// Create a temporary div to measure the actual content height | |
const temp = document.createElement('div'); | |
temp.style.position = 'absolute'; | |
temp.style.visibility = 'hidden'; | |
temp.style.width = '220px'; // Fixed width | |
temp.style.padding = '12px'; | |
temp.style.boxSizing = 'border-box'; // Include padding in width calculation | |
temp.style.border = '1px solid #E4E2E9'; | |
temp.style.borderRadius = '5px'; | |
temp.innerHTML = ` | |
<div style="font-weight:bold;margin-bottom:5px;word-wrap:break-word;">${d.data.name}</div> | |
<div style="color:#716E7B;font-size:12px">${d.data.position}</div> | |
<div style="color:#716E7B;font-size:12px">Team Size: ${d.data.totalEmployees}</div> | |
`; | |
document.body.appendChild(temp); | |
const height = temp.offsetHeight; | |
document.body.removeChild(temp); | |
return height; // Remove extra padding - let the actual height be the container height | |
}) | |
.nodeWidth(d => 220) | |
.childrenMargin(d => 50) | |
.compactMarginBetween(d => 35) | |
.compact(false) | |
.neighbourMargin((a, b) => 50) | |
.siblingsMargin(d => 60) | |
.nodeContent((d, i, arr, state) => { | |
let backgroundColor = 'white'; | |
let borderColor = '#E4E2E9'; | |
let textColor = '#333'; | |
let percentageText = ''; | |
if (currentColorScheme === 'workforce') { | |
const percentage = d.data.totalEmployees / totalCompanyEmployees; | |
backgroundColor = getColorForPercentage(percentage); | |
textColor = percentage > 0.5 ? 'white' : '#333'; | |
percentageText = `<div style="font-size:12px">${(percentage * 100).toFixed(2)}% of workforce</div>`; | |
} | |
else if (currentColorScheme === 'union') { | |
const percentage = d.data.unionPercentage || 0; | |
backgroundColor = getColorForPercentage(percentage, [230, 240, 255], [13, 71, 161]); | |
textColor = percentage > 0.5 ? 'white' : '#333'; | |
percentageText = `<div style="font-size:12px">${(percentage * 100).toFixed(2)}% union</div>`; | |
} | |
else if (currentColorScheme === 'fulltime') { | |
const percentage = d.data.fulltimePercentage || 0; | |
backgroundColor = getColorForPercentage(percentage, [255, 240, 230], [161, 71, 13]); | |
textColor = percentage > 0.5 ? 'white' : '#333'; | |
percentageText = `<div style="font-size:12px">${(percentage * 100).toFixed(2)}% full-time</div>`; | |
} | |
else if (currentColorScheme === 'statutory') { | |
// Check if this office is in the highlighted list | |
if (highlightedOffices.includes(d.data.name)) { | |
backgroundColor = '#4CAF50'; // Green color | |
textColor = 'white'; | |
percentageText = `<div style="font-size:12px">Statutorily Required</div>`; | |
} | |
} | |
if (currentColorScheme === 'none') { | |
// Original styling for 'none' scheme | |
return ` | |
<div style=" | |
height: ${d.height}px; | |
width: ${d.width}px; | |
padding: 12px; | |
background-color: white; | |
border: 1px solid #E4E2E9; | |
border-radius: 5px; | |
box-sizing: border-box; | |
display: flex; | |
flex-direction: column; | |
cursor: pointer; | |
" onclick="showSidebar('${d.data.name}', ${d.data.totalEmployees})"> | |
<div style="font-weight:bold;margin-bottom:5px;word-wrap:break-word;">${d.data.name}</div> | |
<div style="color:#716E7B;font-size:12px">${d.data.position}</div> | |
<div style="color:#716E7B;font-size:12px">Team Size: ${d.data.totalEmployees}</div> | |
</div> | |
`; | |
} else { | |
// Styling for color schemes | |
return ` | |
<div style=" | |
height: ${d.height}px; | |
width: ${d.width}px; | |
padding: 12px; | |
background-color: ${backgroundColor}; | |
border: 1px solid ${borderColor}; | |
border-radius: 5px; | |
box-sizing: border-box; | |
display: flex; | |
flex-direction: column; | |
cursor: pointer; | |
color: ${textColor}; | |
" onclick="showSidebar('${d.data.name}', ${d.data.totalEmployees})"> | |
<div style="font-weight:bold;margin-bottom:5px;word-wrap:break-word;">${d.data.name}</div> | |
<div style="font-size:12px">${d.data.position}</div> | |
<div style="font-size:12px">Team Size: ${d.data.totalEmployees}</div> | |
${percentageText} | |
</div> | |
`; | |
} | |
}); | |
window.chartInstance | |
.data(window.transformedData) | |
.render(); | |
// Start with everything collapsed except root | |
setTimeout(() => { | |
const root = window.chartInstance.getChartState().data[0]; | |
window.chartInstance.collapseAll(); | |
window.chartInstance.expandNode(root.id).render(); | |
}, 100); | |
// Add click outside event listener after DOM is loaded | |
document.addEventListener('click', function(e) { | |
const sidebar = document.getElementById("sidebar"); | |
// Only process if sidebar is visible | |
if (sidebar.classList.contains('visible')) { | |
// Check if click is outside sidebar, not on a node, and not on color scheme selector | |
const clickedOnNode = e.target.closest('.node') !== null; | |
const clickedOnSidebar = sidebar.contains(e.target); | |
const clickedOnColorScheme = document.getElementById("colorSchemeSelector").contains(e.target); | |
if (!clickedOnSidebar && !clickedOnNode && !clickedOnColorScheme) { | |
closeSidebar(); | |
} | |
} | |
}); | |
}); | |
// Add these new functions | |
function getAllEmployeesFromNode(node) { | |
let employees = []; | |
// Add direct employees if they exist | |
if (node.employees && node.employees.length > 0) { | |
employees = employees.concat( | |
node.employees.map(emp => ({ | |
...emp, | |
office: node.name | |
})) | |
); | |
} | |
// Recursively get employees from children | |
if (node.children) { | |
node.children.forEach(child => { | |
employees = employees.concat(getAllEmployeesFromNode(child)); | |
}); | |
} | |
return employees; | |
} | |
let currentEmployees = []; // Store current employees for filtering | |
// Modify showSidebar function to include search functionality | |
function showSidebar(name, totalEmployees) { | |
// Remove selected class from all nodes | |
d3.selectAll('.node').classed('selected', false); | |
// Add selected class to clicked node | |
const clickedNode = d3.select(event.target.closest('.node')); | |
clickedNode.classed('selected', true); | |
const sidebar = document.getElementById("sidebar"); | |
const sidebarTitle = document.getElementById("sidebarTitle"); | |
const sidebarContent = document.getElementById("sidebarContent"); | |
// Find the node data | |
const nodeData = findNodeByName(data, name); | |
const employees = getAllEmployeesFromNode(nodeData); | |
// Update currentEmployees array | |
currentEmployees = employees; | |
sidebarTitle.textContent = `${name} (${employees.length} total employees)`; | |
// Clear search input when showing new department | |
const searchInput = document.getElementById("employeeSearch"); | |
searchInput.value = ''; | |
sidebarContent.innerHTML = renderEmployeeTable(employees); | |
sidebar.classList.add("visible"); | |
} | |
// Helper function to find node by name | |
function findNodeByName(node, name) { | |
if (node.name === name) { | |
return node; | |
} | |
if (node.children) { | |
for (const child of node.children) { | |
const found = findNodeByName(child, name); | |
if (found) return found; | |
} | |
} | |
return null; | |
} | |
function closeSidebar() { | |
const sidebar = document.getElementById("sidebar"); | |
sidebar.classList.remove("visible"); | |
// Remove selected state from nodes | |
d3.selectAll('.node').classed('selected', false); | |
} | |
// Add resize functionality | |
const sidebar = document.getElementById('sidebar'); | |
const resizer = document.getElementById('sidebar-resizer'); | |
let isResizing = false; | |
let startX; | |
let startWidth; | |
resizer.addEventListener('mousedown', initResize, false); | |
function initResize(e) { | |
isResizing = true; | |
startX = e.clientX; | |
startWidth = parseInt(document.defaultView.getComputedStyle(sidebar).width, 10); | |
document.addEventListener('mousemove', resize, false); | |
document.addEventListener('mouseup', stopResize, false); | |
e.preventDefault(); // Prevent text selection while dragging | |
} | |
function resize(e) { | |
if (!isResizing) return; | |
const width = startWidth - (e.clientX - startX); | |
if (width >= 200 && width <= 800) { // Min and max width limits | |
sidebar.style.width = width + 'px'; | |
} | |
} | |
function stopResize() { | |
isResizing = false; | |
document.removeEventListener('mousemove', resize, false); | |
document.removeEventListener('mouseup', stopResize, false); | |
} | |
// Add these new functions | |
function filterEmployees() { | |
const searchTerm = document.getElementById("employeeSearch").value.toLowerCase(); | |
const rows = document.querySelectorAll('#sidebarContent table tr.employee-row'); | |
let visibleCount = 0; | |
// Add employee-row class to all rows except header | |
document.querySelectorAll('#sidebarContent table tr:not(:first-child)').forEach(row => { | |
row.classList.add('employee-row'); | |
}); | |
document.querySelectorAll('.employee-row').forEach(row => { | |
const text = row.textContent.toLowerCase(); | |
if (text.includes(searchTerm)) { | |
row.classList.remove('hidden'); | |
visibleCount++; | |
// Highlight matching text if there's a search term | |
if (searchTerm) { | |
highlightText(row, searchTerm); | |
} else { | |
// Remove highlights if search is cleared | |
removeHighlights(row); | |
} | |
} else { | |
row.classList.add('hidden'); | |
} | |
}); | |
// Update title with filter count | |
const sidebarTitle = document.getElementById("sidebarTitle"); | |
const totalEmployees = currentEmployees.length; | |
if (searchTerm) { | |
sidebarTitle.textContent = `${visibleCount} of ${totalEmployees} employees shown`; | |
} else { | |
sidebarTitle.textContent = `${totalEmployees} total employees`; | |
} | |
} | |
function highlightText(element, searchTerm) { | |
removeHighlights(element); | |
const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT); | |
let node; | |
while (node = walker.nextNode()) { | |
const text = node.textContent.toLowerCase(); | |
if (text.includes(searchTerm)) { | |
const newNode = document.createElement('span'); | |
const regex = new RegExp(`(${searchTerm})`, 'gi'); | |
newNode.innerHTML = node.textContent.replace(regex, '<span class="highlight">$1</span>'); | |
node.parentNode.replaceChild(newNode, node); | |
} | |
} | |
} | |
function removeHighlights(element) { | |
const highlights = element.querySelectorAll('.highlight'); | |
highlights.forEach(highlight => { | |
const parent = highlight.parentNode; | |
parent.replaceChild(document.createTextNode(highlight.textContent), highlight); | |
// Clean up any empty spans | |
if (parent.tagName === 'SPAN' && !parent.innerHTML.trim()) { | |
parent.parentNode.removeChild(parent); | |
} | |
}); | |
} | |
function renderEmployeeTable(employees) { | |
// Calculate statistics | |
const totalEmployees = employees.length; | |
const unionEmployees = employees.filter(emp => [8888, 7777].includes(emp.union_status)); | |
const unionPercentage = ((unionEmployees.length / totalEmployees) * 100).toFixed(1); | |
const fulltimeEmployees = employees.filter(emp => (emp.type === "Full-Time" || emp.type === "Full Time")); | |
const fulltimePercentage = ((fulltimeEmployees.length / totalEmployees) * 100).toFixed(1); | |
let tableHtml = ` | |
<div style="margin-bottom: 20px; padding: 15px; background-color: #f8f9fa; border-radius: 5px;"> | |
<div style="margin-bottom: 10px;"> | |
<strong>Union Status:</strong> ${unionPercentage}% Union (${unionEmployees.length} of ${totalEmployees}) | |
</div> | |
<div> | |
<strong>Employment Type:</strong> ${fulltimePercentage}% Full Time (${fulltimeEmployees.length} of ${totalEmployees}) | |
</div> | |
</div> | |
`; | |
if (employees.length > 0) { | |
tableHtml += '<table>'; | |
tableHtml += ` | |
<tr> | |
<th>Name (ID)</th> | |
<th>Position</th> | |
<th>Office</th> | |
<th>Details</th> | |
</tr>`; | |
employees.sort((a, b) => a.office.localeCompare(b.office)); | |
employees.forEach(emp => { | |
const union_status = [8888, 7777].includes(emp.union_status) ? 'Union' : 'Non-Union'; | |
const rowStyle = union_status === 'Union' ? 'background-color: #f0f7ff;' : ''; | |
tableHtml += ` | |
<tr class="employee-row" style="${rowStyle}"> | |
<td>${emp.name}<br><small class="text-muted">#${emp.id}</small></td> | |
<td> | |
${emp.position.title}<br> | |
<small class="text-muted">Position #${emp.position.number}</small> | |
</td> | |
<td>${emp.office}</td> | |
<td> | |
<strong>Type:</strong> ${emp.type}<br> | |
<strong>Union:</strong> ${union_status}<br> | |
<strong>Location:</strong> ${emp.location}<br> | |
<strong>Appt Type:</strong> ${emp.satisfaction}/5<br> | |
<strong>Supervisor:</strong> ${emp.supervisor.name}<br> | |
<small class="text-muted">Supervisor ID: ${emp.supervisor.id}</small> | |
</td> | |
</tr> | |
`; | |
}); | |
tableHtml += '</table>'; | |
} else { | |
tableHtml += '<p>No employees found in this department.</p>'; | |
} | |
return tableHtml; | |
} | |
// Add these new functions | |
function calculateTotalEmployees(node) { | |
let total = node.employees ? node.employees.length : 0; | |
if (node.children) { | |
node.children.forEach(child => { | |
total += calculateTotalEmployees(child); | |
}); | |
} | |
return total; | |
} | |
function getColorForPercentage(percentage, minColor = [230, 240, 255], maxColor = [13, 71, 161]) { | |
const r = Math.floor(minColor[0] + (maxColor[0] - minColor[0]) * percentage); | |
const g = Math.floor(minColor[1] + (maxColor[1] - minColor[1]) * percentage); | |
const b = Math.floor(minColor[2] + (maxColor[2] - minColor[2]) * percentage); | |
return `rgb(${r}, ${g}, ${b})`; | |
} | |
let currentColorScheme = 'none'; | |
function updateColorScheme(scheme) { | |
currentColorScheme = scheme; | |
// Access the global chartInstance | |
if (window.chartInstance) { | |
// Store the current expanded/collapsed state | |
const currentState = window.chartInstance.getChartState(); | |
const nodeStates = {}; | |
// Record the collapsed state of each node | |
currentState.data.forEach(node => { | |
nodeStates[node.id] = { | |
_collapsed: node._collapsed | |
}; | |
}); | |
// Re-render the chart with the new color scheme | |
window.chartInstance | |
.data(window.transformedData) | |
.render(); | |
// Wait for render to complete before restoring state | |
setTimeout(() => { | |
// Get the updated nodes | |
const updatedState = window.chartInstance.getChartState(); | |
// Restore the collapsed/expanded state for each node | |
updatedState.data.forEach(node => { | |
if (nodeStates[node.id]) { | |
// If the node was collapsed before, collapse it again | |
if (nodeStates[node.id]._collapsed && !node._collapsed) { | |
window.chartInstance.collapseNode(node.id); | |
} | |
// If the node was expanded before, expand it again | |
else if (!nodeStates[node.id]._collapsed && node._collapsed) { | |
window.chartInstance.expandNode(node.id); | |
} | |
} | |
}); | |
// Final render to show the changes | |
window.chartInstance.render(); | |
}, 50); | |
} | |
} | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment