`;
}
createVisualization(container) {
console.log("createVisualization container:", container)
// Store the whole container reference for shadow DOM access
this.wholeContainer = container;
// Create in-memory dictionary for sidebar nodes
this.sidebarNodes = new Map(); // key: nodeId, value: node data
// Define updateButtonState at the beginning of createVisualization
const updateButtonState = () => {
const generateButton = container.querySelector('#generate-summative-btn');
if (!generateButton) return;
const hasSelectedNodes = this.selectedNodes.size > 0;
generateButton.disabled = !hasSelectedNodes;
generateButton.style.backgroundColor = hasSelectedNodes ? '#4299e1' : '#CBD5E0';
generateButton.style.cursor = hasSelectedNodes ? 'pointer' : 'not-allowed';
// Save selected nodes to localStorage
localStorage.setItem('knowledgeGraphSelectedNodes',
JSON.stringify(Array.from(this.selectedNodes)));
};
// Create wrapper div for graph, sidebar, and right panel
const wrapper = document.createElement('div');
wrapper.style.cssText = `
display: flex;
width: 100%;
height: 100%;
position: relative;
`;
container.appendChild(wrapper);
// Create graph container with explicit dimensions
const graphContainer = document.createElement('div');
graphContainer.style.cssText = `
flex: 1;
position: relative;
min-height: 500px; // Ensure minimum height
height: 100%;
`;
wrapper.appendChild(graphContainer);
// Right panel for progress analysis will be created by the existing HTML structure
// Create sidebar
const sidebar = document.createElement('div');
sidebar.className = 'sidebar'; // Add class name for identification
sidebar.style.cssText = `
width: 250px;
background: #f7fafc;
padding: 20px;
border-left: 1px solid #e2e8f0;
position: relative;
height: 100%;
display: flex;
flex-direction: column;
`;
wrapper.appendChild(sidebar);
// Add header section to sidebar
const sidebarHeader = document.createElement('div');
sidebarHeader.className = 'sidebar-header';
sidebarHeader.style.cssText = `
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 1px solid #e2e8f0;
`;
const headerTitle = document.createElement('div');
headerTitle.textContent = 'Selected Components';
headerTitle.style.cssText = `
font-weight: bold;
font-size: 14px;
color: #2d3748;
margin-bottom: 8px;
`;
const explanatoryText = document.createElement('div');
explanatoryText.textContent = 'Selecting nodes will display here, allowing you to create custom quiz';
explanatoryText.style.cssText = `
font-size: 11px;
color: #718096;
line-height: 1.4;
margin-bottom: 8px;
`;
sidebarHeader.appendChild(headerTitle);
sidebarHeader.appendChild(explanatoryText);
sidebar.appendChild(sidebarHeader);
// Add scrollable container for quiz items
const quizItemsContainer = document.createElement('div');
quizItemsContainer.className = 'quiz-items-container';
quizItemsContainer.style.cssText = `
flex: 1;
overflow-y: auto;
margin-bottom: 80px; /* Space for fixed button container */
max-height: calc(100% - 120px); /* Adjust based on header and button space */
`;
sidebar.appendChild(quizItemsContainer);
// Add button container fixed to bottom
const buttonContainer = document.createElement('div');
buttonContainer.style.cssText = `
position: absolute;
bottom: 20px;
left: 20px;
right: 20px;
padding-top: 10px;
border-top: 1px solid #e2e8f0;
background: #f7fafc;
`;
const generateButton = document.createElement('button');
generateButton.id = 'generate-summative-btn';
generateButton.innerHTML = `
Generate Custom Quiz
`;
generateButton.style.cssText = `
display: flex;
align-items: center;
justify-content: center;
width: 100%;
padding: 8px 16px;
background-color: #CBD5E0;
color: white;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: not-allowed;
transition: background-color 0.2s;
gap: 8px;
`;
generateButton.disabled = true;
enableTooltip(generateButton, "Select nodes to generate custom quiz", container);
generateButton.addEventListener('mouseover', () => {
generateButton.style.backgroundColor = '#3182ce';
});
generateButton.addEventListener('mouseout', () => {
generateButton.style.backgroundColor = '#4299e1';
});
generateButton.addEventListener('click', async () => {
console.log('Generate Custom Quiz clicked, extracting data from sidebar DOM...');
// Extract data directly from sidebar DOM elements - use wholeContainer for shadow DOM access
const sidebar = this.wholeContainer.querySelector('.sidebar');
if (!sidebar) {
console.error('Sidebar not found in wholeContainer');
alert('Sidebar not found. Please try again.');
return;
}
const quizItems = sidebar.querySelectorAll('[data-quiz-id]');
console.log(`Found ${quizItems.length} quiz items in sidebar`);
if (quizItems.length === 0) {
console.error('No quiz items found in sidebar');
alert('No items selected. Please select some nodes and try again.');
return;
}
// Extract data from each quiz item
const selectedNodesData = Array.from(quizItems).map(quizItem => {
const nodeId = quizItem.dataset.quizId;
const content = quizItem.dataset.content || '';
const pageUrl = quizItem.dataset.pageUrl || '';
const domain = quizItem.dataset.domain || '';
const level = parseInt(quizItem.dataset.level) || 0;
const position = parseFloat(quizItem.dataset.position) || 0;
// Get the display name from the DOM
const nameElement = quizItem.querySelector('div[style*="font-weight: 500"]');
const label = nameElement ? nameElement.textContent.trim() : 'Unknown';
console.log('Extracted node data:', {
id: nodeId,
label: label,
content: content.substring(0, 100) + '...', // Log first 100 chars
pageUrl: pageUrl,
domain: domain,
level: level,
position: position
});
return {
id: nodeId,
label: label,
content: content,
pageUrl: pageUrl,
domain: domain,
level: level,
position: position
};
});
console.log('Selected nodes data extracted from sidebar:', selectedNodesData);
if (selectedNodesData.length === 0) {
console.error('No valid selected nodes found');
alert('No valid nodes selected. Please select some nodes and try again.');
return;
}
// Create title string from selected node labels
const title = selectedNodesData
.map(node => node.label)
.join(' | ');
// Create content with source mapping for reference tooltips
const contentWithSources = selectedNodesData.map((node, index) => ({
sourceId: `source-${index}`,
label: node.label,
content: node.content || `Section: ${node.label}\nContent: Please visit this section to generate content.`,
pageUrl: node.pageUrl,
domain: node.domain,
level: node.level,
position: node.position
}));
// Combine all content into one text with enhanced formatting
const quizPromptText = contentWithSources
.map(source => `## ${source.label}\n\n${source.content}`)
.join('\n\n---\n\n');
console.log('Generated quiz prompt text:', quizPromptText);
console.log('Content with sources:', contentWithSources);
const event = new CustomEvent('aiActionCompleted', {
detail: {
type: "summative",
title: title, // Add the combined title
text: quizPromptText, // Add the combined content for quiz generation
selectedNodesData: selectedNodesData, // Keep the original data if needed
contentWithSources: contentWithSources // NEW: Preserve source mapping for tooltips
},
bubbles: true,
composed: true
});
container.dispatchEvent(event);
// Close the modal after dispatching the event
const modal = container.closest('.modal') || container.parentElement;
if (modal) {
modal.style.display = 'none';
console.log('Modal closed after custom quiz generation');
}
});
buttonContainer.appendChild(generateButton);
// Add the new Analyze Progress button
const analyzeButton = document.createElement('button');
analyzeButton.id = 'analyze-progress-btn';
analyzeButton.innerHTML = `
Analyze my Progress
`;
analyzeButton.style.cssText = `
display: flex;
align-items: center;
justify-content: center;
width: 100%;
padding: 8px 16px;
background-color: #4299e1;
color: white;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: background-color 0.2s;
gap: 8px;
margin-top: 8px;
`;
analyzeButton.addEventListener('mouseover', () => {
analyzeButton.style.backgroundColor = '#3182ce';
});
analyzeButton.addEventListener('mouseout', () => {
analyzeButton.style.backgroundColor = '#4299e1';
});
analyzeButton.addEventListener('click', async () => {
console.log("analyze button clicked");
let progressReport;
try {
progressReport = await generateProgressReport();
console.log("Full progress report data:", progressReport); // Log full report
if (progressReport.success) {
console.log('Progress Report Text:', progressReport.report);
console.log('Progress Report Charts:', {
xyChart: progressReport.charts?.xyChart,
quadrantChart: progressReport.charts?.quadrantChart
});
// Verify chart data before dispatching
if (!progressReport.charts?.xyChart || !progressReport.charts?.quadrantChart) {
console.warn('Missing chart data:', {
hasXYChart: !!progressReport.charts?.xyChart,
hasQuadrantChart: !!progressReport.charts?.quadrantChart
});
}
container.dispatchEvent(new CustomEvent('aiActionCompleted', {
bubbles: true,
composed: true,
detail: {
text: progressReport.report,
type: "progress_report",
xyChart: progressReport.charts?.xyChart || '',
quadrantChart: progressReport.charts?.quadrantChart || ''
}
}));
} else {
console.log('Progress Report Status:', progressReport.message);
}
} catch (error) {
console.error('Error generating progress report:', error);
return;
}
const modal = container.closest('.modal') || container.parentElement;
if (modal) {
modal.style.display = 'none';
}
});
enableTooltip(analyzeButton, "View your learning progress analysis", container);
buttonContainer.appendChild(analyzeButton);
sidebar.appendChild(buttonContainer);
// Update dimensions based on actual container size
const containerRect = graphContainer.getBoundingClientRect();
this.width = containerRect.width;
this.height = containerRect.height;
// Store the graph container reference
this.graphContainer = graphContainer;
// Add view toggle switch
this.addViewToggle(graphContainer);
// Create radial tree visualization
this.createRadialTree(graphContainer);
// Add legend with detail level buttons
const legend = document.createElement('div');
legend.style.cssText = `
position: absolute;
top: 20px;
left: 20px;
background: rgba(255, 255, 255, 0.9);
padding: 15px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
z-index: 2;
min-width: 200px;
`;
const legendTitle = document.createElement('h3');
legendTitle.textContent = 'Knowledge Graph';
legendTitle.style.cssText = `
margin: 0 0 15px 0;
font-size: 14px;
color: #2d3748;
`;
legend.appendChild(legendTitle);
const legendItems = [
{ color: '#4299e1', label: 'Pages & Topics (clickable)' },
{ color: '#999', label: 'Sections & Subsections' },
{ color: '#ff6b6b', label: 'Truncated (too many items)' },
{ color: '#ffd700', label: 'Selected for Quiz' },
{ color: '#555', label: 'Tree Connections' }
];
legendItems.forEach(item => {
const itemDiv = document.createElement('div');
itemDiv.style.cssText = `
display: flex;
align-items: center;
margin-bottom: 5px;
`;
const colorBox = document.createElement('div');
colorBox.style.cssText = `
width: 15px;
height: 15px;
background: ${item.color};
margin-right: 8px;
border-radius: 3px;
`;
const label = document.createElement('span');
label.textContent = item.label;
label.style.cssText = `
font-size: 12px;
color: #4a5568;
`;
itemDiv.appendChild(colorBox);
itemDiv.appendChild(label);
legend.appendChild(itemDiv);
});
graphContainer.appendChild(legend);
// After adding the legend to graphContainer, add the explanation card
const explanation = document.createElement('div');
explanation.style.cssText = `
position: absolute;
top: ${legend.offsetHeight + 40}px;
left: 20px;
background: rgba(255, 255, 255, 0.9);
padding: 15px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
z-index: 2;
max-width: 300px;
font-family: monospace;
font-size: 12px;
line-height: 1.4;
`;
explanation.innerHTML = `
Radial Knowledge Tree
This radial tree shows your learning structure:
• Center = Root of knowledge
• Branches = Pages & Topics
• Leaves = Sections & Details
Interactive Features:
• Click blue nodes to expand
• Click gray nodes to select
• Generate quizzes from selections
Tip: Use zoom to explore
different branches of your
knowledge tree.
`;
graphContainer.appendChild(explanation);
}
addViewToggle(container) {
// Create toggle container
const toggleContainer = document.createElement('div');
toggleContainer.style.cssText = `
position: absolute;
top: 20px;
right: 20px;
z-index: 100;
background: rgba(255, 255, 255, 0.9);
padding: 8px 12px;
border-radius: 20px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
display: flex;
align-items: center;
gap: 8px;
font-size: 12px;
color: #2d3748;
`;
// Add labels
const treeLabel = document.createElement('span');
treeLabel.textContent = 'Tree';
treeLabel.style.cssText = `
color: #4299e1;
font-weight: bold;
`;
const listLabel = document.createElement('span');
listLabel.textContent = 'List';
listLabel.style.cssText = `
color: #718096;
`;
// Create toggle switch
const toggleSwitch = document.createElement('div');
toggleSwitch.style.cssText = `
position: relative;
width: 40px;
height: 20px;
background: #4299e1;
border-radius: 10px;
cursor: pointer;
transition: all 0.3s ease;
`;
const toggleSlider = document.createElement('div');
toggleSlider.style.cssText = `
position: absolute;
top: 2px;
left: 2px;
width: 16px;
height: 16px;
background: white;
border-radius: 50%;
transition: all 0.3s ease;
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
`;
toggleSwitch.appendChild(toggleSlider);
toggleContainer.appendChild(treeLabel);
toggleContainer.appendChild(toggleSwitch);
toggleContainer.appendChild(listLabel);
// Add click handler
toggleSwitch.addEventListener('click', () => {
this.toggleView();
});
container.appendChild(toggleContainer);
// Store references
this.toggleContainer = toggleContainer;
this.toggleSwitch = toggleSwitch;
this.toggleSlider = toggleSlider;
this.treeLabel = treeLabel;
this.listLabel = listLabel;
this.currentView = 'tree'; // Start with tree view
}
toggleView() {
if (this.currentView === 'tree') {
this.switchToListView();
} else {
this.switchToTreeView();
}
}
switchToListView() {
console.log('Switching to list view...');
this.currentView = 'list';
// Update toggle appearance
this.toggleSwitch.style.background = '#718096';
this.toggleSlider.style.left = '22px';
this.treeLabel.style.color = '#718096';
this.listLabel.style.color = '#4299e1';
this.listLabel.style.fontWeight = 'bold';
this.treeLabel.style.fontWeight = 'normal';
// Hide tree view
const svg = this.graphContainer ? this.graphContainer.querySelector('svg') : this.container.querySelector('svg');
if (svg) {
console.log('Hiding SVG tree view');
svg.style.display = 'none';
} else {
console.log('No SVG found to hide');
}
// Ensure tree data is available
if (!this.treeData) {
console.log('Tree data not available, attempting to rebuild...');
this.initData().then(() => {
this.createListView();
}).catch(error => {
console.error('Failed to initialize tree data:', error);
});
} else {
console.log('Tree data available, creating list view...');
// Show list view
this.createListView();
}
}
switchToTreeView() {
this.currentView = 'tree';
// Update toggle appearance
this.toggleSwitch.style.background = '#4299e1';
this.toggleSlider.style.left = '2px';
this.treeLabel.style.color = '#4299e1';
this.treeLabel.style.fontWeight = 'bold';
this.listLabel.style.color = '#718096';
this.listLabel.style.fontWeight = 'normal';
// Hide list view
const listContainer = this.container.querySelector('#list-view');
if (listContainer) {
listContainer.remove();
}
// Show tree view
const svg = this.container.querySelector('svg');
if (svg) {
svg.style.display = 'block';
}
}
createListView() {
console.log('Creating list view...');
// Prevent multiple simultaneous list view creations
if (this.isCreatingListView) {
console.log('List view creation already in progress, skipping...');
return;
}
this.isCreatingListView = true;
// Remove existing list view if any
const existingList = this.container.querySelector('#list-view');
if (existingList) {
console.log('Removing existing list view');
existingList.remove();
}
// Create list container
const listContainer = document.createElement('div');
listContainer.id = 'list-view';
listContainer.style.cssText = `
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: white;
overflow-y: auto;
padding: 20px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
z-index: 10;
`;
// Rebuild tree data specifically for list view (showing ALL nodes)
console.log('Rebuilding tree data for list view with all nodes...');
this.buildHierarchicalTreeData(true, true).then(treeData => {
this.treeData = treeData;
this.renderListViewContent(listContainer);
this.isCreatingListView = false; // Reset the guard
}).catch(error => {
console.error('Failed to build tree data for list view:', error);
listContainer.innerHTML = '
Error loading data
';
this.container.appendChild(listContainer);
this.isCreatingListView = false; // Reset the guard
});
}
renderListViewContent(listContainer) {
// Check if tree data exists
if (!this.treeData) {
console.error('Tree data not available for list view');
listContainer.innerHTML = '
Loading...
';
this.container.appendChild(listContainer);
return;
}
console.log('Tree data available, building hierarchical list...');
console.log('Tree data:', this.treeData);
// Add info panel at the top
const infoPanel = document.createElement('div');
infoPanel.style.cssText = `
background: #f7fafc;
border: 1px solid #e2e8f0;
border-radius: 8px;
padding: 16px;
margin-bottom: 20px;
font-family: 'Courier New', monospace;
font-size: 12px;
line-height: 1.4;
color: #2d3748;
`;
infoPanel.innerHTML = `
KNOWLEDGE GRAPH LIST VIEW - ALL NODES
• Shows ALL pages and ALL heading levels (H1-H6)
• Click pages to expand topics
• Click H1 topics to expand sections
• Click H2+ sections to select for quiz
• Selected items appear in sidebar
• Use "Generate Custom Quiz" to create quiz
ICON LEGEND:
Page
H1
H2
H3
H4
H5
H6
Duplicate navigation elements (like "Table of contents") are automatically filtered out
`;
listContainer.appendChild(infoPanel);
// Add column headers for the list items
const listHeaders = document.createElement('div');
listHeaders.style.cssText = `
background: #f8f9fa;
border: 1px solid #e2e8f0;
border-radius: 6px;
padding: 8px 12px;
margin-bottom: 10px;
font-size: 11px;
color: #718096;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.5px;
display: flex;
justify-content: space-between;
align-items: center;
`;
listHeaders.innerHTML = `
ComponentLast Visited
`;
listContainer.appendChild(listHeaders);
// Create hierarchical list
this.buildHierarchicalList(listContainer, this.treeData);
console.log('Adding list container to DOM...');
console.log('Container:', this.container);
console.log('Graph container:', this.graphContainer);
// Use the graph container instead of the main container
if (this.graphContainer) {
this.graphContainer.appendChild(listContainer);
console.log('List view added to graph container');
} else {
this.container.appendChild(listContainer);
console.log('List view added to main container');
}
console.log('List view created successfully');
}
buildHierarchicalList(container, data, level = 0) {
if (!data) {
console.log('buildHierarchicalList: No data provided');
return;
}
console.log(`buildHierarchicalList: Building level ${level} with data:`, data);
const item = document.createElement('div');
item.className = 'list-item';
item.style.cssText = `
margin-left: ${level * 20}px;
margin-bottom: 8px;
padding: 8px 12px;
border-radius: 6px;
cursor: pointer;
transition: background-color 0.2s;
display: flex;
align-items: center;
justify-content: space-between;
`;
// Store URL data in data attributes for accessibility by other components
// URLs are hidden from display to reduce clutter but remain accessible via data attributes
if (data.pageUrl) {
item.setAttribute('data-page-url', data.pageUrl);
}
if (data.id) {
item.setAttribute('data-node-id', data.id);
}
if (data.domain) {
item.setAttribute('data-domain', data.domain);
}
if (data.pageTitle) {
item.setAttribute('data-page-title', data.pageTitle);
}
// Add click handler
item.addEventListener('click', () => {
this.handleListItemClick(data, item);
});
// Add hover effect
item.addEventListener('mouseenter', () => {
item.style.backgroundColor = '#f7fafc';
});
item.addEventListener('mouseleave', () => {
item.style.backgroundColor = 'transparent';
});
// Create left content with icon and title
const leftContent = document.createElement('div');
leftContent.style.cssText = `
display: flex;
align-items: center;
flex: 1;
`;
// Add icon based on level
const icon = document.createElement('span');
icon.style.cssText = `
margin-right: 8px;
font-size: 14px;
display: inline-flex;
align-items: center;
`;
icon.innerHTML = this.getLevelIcon(data.level, level);
const title = document.createElement('span');
title.textContent = data.name;
title.style.cssText = `
font-weight: ${level <= 1 ? 'bold' : 'normal'};
font-size: ${level === 0 ? '16px' : level === 1 ? '14px' : '12px'};
color: #2d3748;
`;
leftContent.appendChild(icon);
leftContent.appendChild(title);
// Create right content with date and expand icon
const rightContent = document.createElement('div');
rightContent.style.cssText = `
display: flex;
align-items: center;
gap: 8px;
`;
// Add visit date if available
if (data.pageUrl && data.pageUrl !== '') {
const visitDate = document.createElement('span');
visitDate.style.cssText = `
font-size: 11px;
color: #718096;
font-family: 'Courier New', monospace;
`;
visitDate.textContent = `Last visited: ${this.getVisitDate(data.pageUrl)}`;
rightContent.appendChild(visitDate);
}
// Add expand icon for items with children
if (data.children && data.children.length > 0) {
const expandIcon = document.createElement('span');
expandIcon.style.cssText = `
font-size: 12px;
color: #718096;
`;
expandIcon.textContent = data.isExpanded ? '▼' : '▶';
rightContent.appendChild(expandIcon);
}
item.appendChild(leftContent);
item.appendChild(rightContent);
// Metadata section removed to reduce clutter - page title data still available in data attributes
container.appendChild(item);
console.log(`Added item to container: ${data.name} (level ${level})`);
// Add children if expanded
if (data.children && data.children.length > 0) {
console.log(`Adding ${data.children.length} children for ${data.name}`);
data.children.forEach(child => {
const childElement = this.buildHierarchicalList(container, child, level + 1);
// Hide children initially if parent is not expanded
if (!data.isExpanded && childElement) {
childElement.style.display = 'none';
}
});
}
return item;
}
getLevelIcon(nodeLevel, hierarchyLevel) {
// Unique SVG icons for each heading level
if (hierarchyLevel === 0) {
return ''; // Root/Page level - Book with pages
} else if (nodeLevel === 1) {
return ''; // H1 - Document with lines
} else if (nodeLevel === 2) {
return ''; // H2 - File with content
} else if (nodeLevel === 3) {
return ''; // H3 - Network/Connection nodes
} else if (nodeLevel === 4) {
return ''; // H4 - Target/Crosshair
} else if (nodeLevel === 5) {
return ''; // H5 - Star
} else if (nodeLevel === 6) {
return ''; // H6 - Cube/3D shape
} else {
return ''; // Default - File with content
}
}
getVisitDate(pageUrl) {
// Try to get visit date from localStorage or return placeholder
try {
const visitData = localStorage.getItem(`visit_${pageUrl}`);
if (visitData) {
const data = JSON.parse(visitData);
return data.date || 'Unknown';
}
} catch (error) {
console.log('Could not get visit date:', error);
}
// Return a placeholder date for now
return new Date().toLocaleDateString();
}
/**
* Helper method to get URL data from list items for other components
* @param {HTMLElement} listItem - The list item element
* @returns {Object} Object containing URL-related data
*/
getListItemUrlData(listItem) {
return {
pageUrl: listItem.getAttribute('data-page-url'),
nodeId: listItem.getAttribute('data-node-id'),
domain: listItem.getAttribute('data-domain'),
pageTitle: listItem.getAttribute('data-page-title')
};
}
/**
* Helper method to get all list items with their URL data
* @returns {Array} Array of objects containing list items and their URL data
*/
getAllListItemsWithUrlData() {
const listItems = this.container.querySelectorAll('.list-item');
return Array.from(listItems).map(item => ({
element: item,
urlData: this.getListItemUrlData(item)
}));
}
handleListItemClick(data, element) {
if (data.isTruncated && data.id === 'truncated-pages') {
// Special handling for "... X more pages" - show all pages
console.log('Expanding truncated pages - rebuilding tree with all pages');
this.buildHierarchicalTreeData(true, true); // Force rebuild with all pages and all levels for list view
const listContainer = this.container.querySelector('#list-view');
listContainer.innerHTML = '';
this.createListView(); // Recreate the entire list view
return;
}
if (data.children && data.children.length > 0) {
// Toggle expansion
data.isExpanded = !data.isExpanded;
// Update icon
const icon = element.querySelector('span:last-child');
icon.textContent = data.isExpanded ? '▼' : '▶';
// Instead of recreating the entire list, just toggle the children visibility
this.toggleChildrenVisibility(element, data);
} else {
// For leaf nodes, add to summative selection
this.toggleNodeSelection(data);
}
}
toggleChildrenVisibility(element, data) {
// Find the next sibling elements that are children of this item
let nextElement = element.nextElementSibling;
const childrenElements = [];
// Collect all child elements (they have higher margin-left)
const currentMarginLeft = parseInt(element.style.marginLeft) || 0;
while (nextElement && nextElement.classList && nextElement.classList.contains('list-item')) {
const nextMarginLeft = parseInt(nextElement.style.marginLeft) || 0;
// If the next element has a higher margin-left, it's a child
if (nextMarginLeft > currentMarginLeft) {
childrenElements.push(nextElement);
nextElement = nextElement.nextElementSibling;
} else {
// We've reached a sibling or parent level, stop
break;
}
}
// Toggle visibility of children
childrenElements.forEach(childElement => {
if (data.isExpanded) {
childElement.style.display = 'flex';
} else {
childElement.style.display = 'none';
}
});
}
createRadialTree(container) {
// Specify the chart's dimensions
const width = Math.min(this.width, this.height);
const height = width;
const cx = width * 0.5;
const cy = height * 0.5;
const radius = Math.min(width, height) / 2 - 40;
// Create a radial tree layout
const tree = d3.tree()
.size([2 * Math.PI, radius])
.separation((a, b) => (a.parent == b.parent ? 1 : 2) / a.depth);
// Create the SVG container
const svg = d3.select(container)
.append('svg')
.attr('width', width)
.attr('height', height)
.attr('viewBox', [-cx, -cy, width, height])
.attr('style', 'width: 100%; height: auto; font: 12px sans-serif;');
// Add zoom behavior
const g = svg.append('g');
const zoom = d3.zoom()
.scaleExtent([0.5, 3]) // Prevent zooming out too much
.on('zoom', (event) => {
g.attr('transform', event.transform);
});
svg.call(zoom);
// Store references
this.svg = svg;
this.container = container;
this.g = g;
// Create the initial tree visualization
this.updateRadialTree();
}
updateRadialTree() {
if (!this.treeData) return;
// Sort the tree and apply the layout with optimized separation
const root = d3.tree()
.size([2 * Math.PI, Math.min(this.width, this.height) / 2 - 40])
.separation((a, b) => {
// Reduce separation for better performance
if (a.parent == b.parent) {
return 0.5 / Math.max(a.depth, 1);
}
return 1 / Math.max(a.depth, 1);
})
(d3.hierarchy(this.treeData)
.sort((a, b) => d3.ascending(a.data.name, b.data.name)));
// Clear existing content
this.g.selectAll('*').remove();
// Append links
this.g.append('g')
.attr('fill', 'none')
.attr('stroke', '#555')
.attr('stroke-opacity', 0.4)
.attr('stroke-width', 1.5)
.selectAll('path')
.data(root.links())
.join('path')
.attr('d', d3.linkRadial()
.angle(d => d.x)
.radius(d => d.y));
// Append nodes
const nodes = this.g.append('g')
.selectAll('g')
.data(root.descendants())
.join('g')
.attr('transform', d => `rotate(${d.x * 180 / Math.PI - 90}) translate(${d.y},0)`);
// Add circles for nodes
nodes.append('circle')
.attr('fill', d => {
if (this.selectedNodes.has(d.data.id)) {
return '#ffd700';
} else if (d.data.isTruncated) {
return '#ff6b6b'; // Truncated nodes
} else if (d.children) {
return '#4299e1'; // Expandable nodes
} else {
return '#999'; // Leaf nodes
}
})
.attr('stroke', d => {
if (this.selectedNodes.has(d.data.id)) {
return '#ff6b6b';
} else if (d.data.isTruncated) {
return '#e53e3e';
} else if (d.children) {
return '#2c5282';
}
return 'none';
})
.attr('stroke-width', d => {
if (this.selectedNodes.has(d.data.id)) {
return 3;
} else if (d.data.isTruncated) {
return 2;
} else if (d.children) {
return 2;
}
return 0;
})
.attr('r', d => {
if (d.depth === 0) return 8; // Root
if (d.depth === 1) return 6; // Pages
if (d.depth === 2) return 4; // H1s
return 3; // H2s and below
})
.style('cursor', d => d.data.isTruncated ? 'not-allowed' : 'pointer')
.on('mouseover', (event, d) => {
this.showNodeTooltip(event, d);
})
.on('mouseout', (event, d) => {
this.hideNodeTooltip();
})
.on('click', (event, d) => {
this.handleRadialNodeClick(event, d);
});
// Add labels
nodes.append('text')
.attr('dy', '0.31em')
.attr('x', d => d.x < Math.PI === !d.children ? 6 : -6)
.attr('text-anchor', d => d.x < Math.PI === !d.children ? 'start' : 'end')
.attr('transform', d => d.x >= Math.PI ? 'rotate(180)' : null)
.attr('paint-order', 'stroke')
.attr('stroke', 'white')
.attr('stroke-width', 3)
.attr('fill', 'currentColor')
.style('font-size', d => {
if (d.depth === 0) return '14px';
if (d.depth === 1) return '12px';
return '10px';
})
.style('font-weight', d => d.depth <= 2 ? 'bold' : 'normal')
.text(d => {
// Truncate long labels more aggressively
const maxLength = d.depth <= 1 ? 12 : 8; // Much shorter
return d.data.name.length > maxLength ?
d.data.name.substring(0, maxLength) + '...' :
d.data.name;
});
}
showNodeTooltip(event, d) {
// Hide any existing tooltip
this.hideNodeTooltip();
// Create tooltip
const tooltip = document.createElement('div');
tooltip.id = 'node-tooltip';
tooltip.style.cssText = `
position: absolute;
background: rgba(0, 0, 0, 0.9);
color: white;
padding: 12px;
border-radius: 8px;
font-size: 12px;
max-width: 300px;
z-index: 1000;
pointer-events: none;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
`;
// Build tooltip content
let content = `