document.addEventListener('DOMContentLoaded', () => { // Get DOM elements const mapContainer = document.getElementById('knowledge-map-container'); const mapLayout = document.getElementById('map-layout'); const mapDepth = document.getElementById('map-depth'); const refreshBtn = document.getElementById('refresh-map'); const nodeTitle = document.getElementById('node-title'); const nodeDetails = document.getElementById('node-details'); // Add view toggle button reference const viewToggleBtn = document.createElement('button'); viewToggleBtn.id = 'view-toggle-btn'; viewToggleBtn.className = 'map-control-btn'; viewToggleBtn.innerHTML = ' Switch View'; // Add to control group const controlsContainer = document.querySelector('.map-controls'); if (controlsContainer) { const viewToggleGroup = document.createElement('div'); viewToggleGroup.className = 'control-group'; viewToggleGroup.appendChild(viewToggleBtn); controlsContainer.appendChild(viewToggleGroup); } // View mode: tag->doc (default) or doc->tag let viewMode = 'tag-doc'; // Initialize ECharts instance const myChart = echarts.init(mapContainer, null, { renderer: 'canvas' }); // Reset chart size on window resize window.addEventListener('resize', () => { myChart.resize(); }); // Chart configuration options let option = { tooltip: { trigger: 'item', formatter: '{b}: {c}' }, series: [{ type: 'tree', data: [], top: '10%', left: '5%', bottom: '10%', right: '15%', symbolSize: 10, label: { position: 'left', verticalAlign: 'middle', align: 'right', fontSize: 14 }, leaves: { label: { position: 'right', verticalAlign: 'middle', align: 'left' } }, emphasis: { focus: 'descendant' }, expandAndCollapse: true, animationDuration: 550, animationDurationUpdate: 750, // Add drag functionality roam: true, // Allow zooming and panning // Set styles for different level nodes itemStyle: { color: function(params) { // Set different colors based on node depth const depth = params.treePathInfo ? params.treePathInfo.length - 1 : 0; // Define different level colors const colors = ['#4e79a7', '#f28e2c', '#e15759', '#76b7b2', '#59a14f']; return colors[Math.min(depth, colors.length - 1)]; } }, // Set styles for different level labels levels: [ { // Root node itemStyle: { color: '#4e79a7', borderWidth: 0 }, label: { fontSize: 18, fontWeight: 'bold' } }, { // First level node itemStyle: { color: '#f28e2c', borderWidth: 1 }, label: { fontSize: 16, fontWeight: 'bold' } }, { // Second level node itemStyle: { color: '#e15759' }, label: { fontSize: 14 } }, { // Leaf node (document) itemStyle: { color: '#76b7b2' }, label: { fontSize: 12 } } ] }] }; // Set layout type function setLayout(layout) { if (layout === 'tree') { option.series[0].type = 'tree'; option.series[0].layout = 'orthogonal'; option.series[0].orient = 'LR'; // Restore default series settings option.series = [{ type: 'tree', data: [], top: '10%', left: '5%', bottom: '10%', right: '15%', symbolSize: 10, label: { position: 'left', verticalAlign: 'middle', align: 'right', fontSize: 14, color: '#000' // Default text color }, leaves: { label: { position: 'right', verticalAlign: 'middle', align: 'left', color: '#000' // Leaf node text color } }, emphasis: { focus: 'descendant' }, expandAndCollapse: true, animationDuration: 550, animationDurationUpdate: 750, roam: true, // Explicitly set node styles itemStyle: { color: function(params) { // Set different colors based on node depth const depth = params.treePathInfo ? params.treePathInfo.length - 1 : 0; const colors = ['#4e79a7', '#f28e2c', '#e15759', '#76b7b2', '#59a14f']; return colors[Math.min(depth, colors.length - 1)]; }, borderWidth: 1, borderColor: '#999' }, lineStyle: { color: '#999', width: 1 } }]; } else if (layout === 'force') { option.series[0].type = 'graph'; option.series[0].layout = 'force'; option.series[0].force = { repulsion: 100, edgeLength: 50 }; } else if (layout === 'radial') { option.series[0].type = 'tree'; option.series[0].layout = 'radial'; option.series[0].orient = undefined; } else if (layout === 'wordcloud') { // Real word cloud mode - Use wordcloud chart type console.log("Switch to word cloud mode"); // Completely replace series configuration with word cloud chart option.series = [{ type: 'wordCloud', // Word cloud shape can be rectangle or circle shape: 'circle', // Word cloud size left: 'center', top: 'center', width: '70%', height: '70%', // Word cloud background color, set to obvious white backgroundColor: '#ffffff', // Font size range, ensure words visible sizeRange: [24, 80], // Word rotation angle range - Reduce rotation for easier reading rotationRange: [0, 0], // Set to 0, no rotation // Grid size, for layout optimization, reduce this value for denser words gridSize: 6, // Word cloud text style textStyle: { fontFamily: 'Arial, sans-serif', fontWeight: 'bold', // Fixed text color, no random color color: '#333' }, // Global zoom scale layoutAnimation: false, // Mouse hover effect emphasis: { focus: 'self', textStyle: { shadowBlur: 10, shadowColor: '#333' } }, // Must attribute, set drawOutOfBound to false to prevent text from being clipped drawOutOfBound: false, // Whether to enable fuzzy antialiasing processing textShadowBlur: 2, textShadowColor: '#fff', // Data will be filled in buildWordcloudData function data: [] }]; } } // Set level depth function setDepth(depth) { if (depth === 'all') { option.series[0].expandAndCollapse = true; option.series[0].initialTreeDepth = -1; } else { option.series[0].expandAndCollapse = true; option.series[0].initialTreeDepth = parseInt(depth); } } // Get knowledge map data async function fetchKnowledgeMap() { try { const response = await fetch('/api/knowledge-map'); if (!response.ok) { throw new Error('Failed to get knowledge map data'); } const data = await response.json(); return data; } catch (error) { console.error('Failed to get knowledge map data:', error); return { nodes: [], links: [], error: error.message }; } } // Truncate long filename function truncateFilename(filename, maxLength = 25) { if (!filename || filename.length <= maxLength) { return filename; } const ext = filename.lastIndexOf('.') > 0 ? filename.slice(filename.lastIndexOf('.')) : ''; const nameWithoutExt = filename.slice(0, filename.lastIndexOf('.') > 0 ? filename.lastIndexOf('.') : filename.length); // Keep filename prefix, add ellipsis, then keep extension return nameWithoutExt.slice(0, maxLength - 3 - ext.length) + '...' + ext; } // Build knowledge map tree data - Tag to document structure (default view) function buildTagToDocTree(data) { // Process backend returned data, build into tree structure if (data.error) { showError(data.error); return []; } // If empty data, show prompt information if (!data.documents || data.documents.length === 0) { showError('No usable knowledge map data, please upload documents first'); return []; } // Build tree structure const rootNode = { name: data.centralTopic || 'Knowledge Center', value: data.documents.length, children: [] }; // Group processing const tagGroups = {}; // First find all tags common in all documents as the first layer const allTags = new Set(); data.documents.forEach(doc => { if (doc.tags && doc.tags.length > 0) { doc.tags.forEach(tag => allTags.add(tag)); } }); // Generate tag hierarchy structure (simulate mind map hierarchy) // Use tag co-occurrence frequency as hierarchy building basis const tagRelations = {}; // Calculate tag co-occurrence frequency data.documents.forEach(doc => { if (doc.tags && doc.tags.length > 0) { for (let i = 0; i < doc.tags.length; i++) { for (let j = i + 1; j < doc.tags.length; j++) { const tag1 = doc.tags[i]; const tag2 = doc.tags[j]; const key = `${tag1}:${tag2}`; const reverseKey = `${tag2}:${tag1}`; if (tagRelations[key]) { tagRelations[key]++; } else if (tagRelations[reverseKey]) { tagRelations[reverseKey]++; } else { tagRelations[key] = 1; } } } } }); // Find main tags based on co-occurrence frequency let maxCoOccurrence = 0; let primaryTags = []; Object.keys(tagRelations).forEach(key => { if (tagRelations[key] > maxCoOccurrence) { maxCoOccurrence = tagRelations[key]; const [tag1, tag2] = key.split(':'); primaryTags = [tag1, tag2]; } }); // If no related tags are found, use the most common tag if (primaryTags.length === 0) { const tagFrequency = {}; data.documents.forEach(doc => { if (doc.tags && doc.tags.length > 0) { doc.tags.forEach(tag => { tagFrequency[tag] = (tagFrequency[tag] || 0) + 1; }); } }); // Find the tag with the highest frequency let maxFreq = 0; let mostFrequentTag = ''; Object.keys(tagFrequency).forEach(tag => { if (tagFrequency[tag] > maxFreq) { maxFreq = tagFrequency[tag]; mostFrequentTag = tag; } }); primaryTags = [mostFrequentTag]; } // Use main tags as first level nodes primaryTags.forEach(tag => { const tagNode = { name: tag, value: 0, children: [] }; // Find documents containing the tag const docsWithTag = data.documents.filter(doc => doc.tags && doc.tags.includes(tag) ); tagNode.value = docsWithTag.length; // Add child tag nodes to the tag node const secondaryTags = new Set(); docsWithTag.forEach(doc => { if (doc.tags) { doc.tags.forEach(t => { if (t !== tag && !primaryTags.includes(t)) { secondaryTags.add(t); } }); } }); // Add secondary tag nodes secondaryTags.forEach(secondaryTag => { const secondaryNode = { name: secondaryTag, value: 0, children: [] }; // Find documents containing both first and second level tags const docsWithBothTags = docsWithTag.filter(doc => doc.tags && doc.tags.includes(secondaryTag) ); secondaryNode.value = docsWithBothTags.length; // Add document nodes to secondary tag docsWithBothTags.forEach(doc => { secondaryNode.children.push({ name: truncateFilename(doc.filename), // Truncate long filename fullName: doc.filename, // Save full filename value: 1, document: doc, nodeType: 'document' }); }); if (secondaryNode.children.length > 0) { tagNode.children.push(secondaryNode); } }); // Directly add documents without secondary tags to first level tag const docsWithOnlyPrimaryTag = docsWithTag.filter(doc => doc.tags && doc.tags.filter(t => !primaryTags.includes(t) && secondaryTags.has(t)).length === 0 ); docsWithOnlyPrimaryTag.forEach(doc => { tagNode.children.push({ name: truncateFilename(doc.filename), // Truncate long filename fullName: doc.filename, // Save full filename value: 1, document: doc, nodeType: 'document' }); }); if (tagNode.children.length > 0) { rootNode.children.push(tagNode); } }); // Add documents without main tags const docsWithoutPrimaryTags = data.documents.filter(doc => !doc.tags || !doc.tags.some(tag => primaryTags.includes(tag)) ); if (docsWithoutPrimaryTags.length > 0) { const otherNode = { name: 'Other Documents', value: docsWithoutPrimaryTags.length, children: [] }; docsWithoutPrimaryTags.forEach(doc => { otherNode.children.push({ name: truncateFilename(doc.filename), // Truncate long filename fullName: doc.filename, // Save full filename value: 1, document: doc, nodeType: 'document' }); }); rootNode.children.push(otherNode); } return [rootNode]; } // Build document to tag tree structure (reverse view) function buildDocToTagTree(data) { // Process backend returned data, build into tree structure if (data.error) { showError(data.error); return []; } // If empty data, show prompt information if (!data.documents || data.documents.length === 0) { showError('No usable knowledge map data, please upload documents first'); return []; } // Build tree structure const rootNode = { name: data.centralTopic || 'Knowledge Center', value: data.documents.length, children: [] }; // Group documents by document type or topic const docCategories = {}; // Find all documents, extract possible classification information data.documents.forEach(doc => { // Try to infer category from filename let category = 'Document Set'; // Check file extension if (doc.filename) { const fileExt = doc.filename.split('.').pop().toLowerCase(); if (['pdf', 'docx', 'doc'].includes(fileExt)) { category = 'Text Document'; } else if (['ppt', 'pptx'].includes(fileExt)) { category = 'Presentation'; } else if (['xlsx', 'xls', 'csv'].includes(fileExt)) { category = 'Data File'; } // Also try to infer better classification from tags if (doc.tags && doc.tags.length > 0) { // Find some possible tags that might represent topic const possibleCategories = ['Education', 'Cognition', 'Learning', 'Media', 'Design', 'Evaluation']; for (const pc of possibleCategories) { const matchingTag = doc.tags.find(tag => tag.includes(pc)); if (matchingTag) { category = matchingTag; break; } } } } // Ensure category exists if (!docCategories[category]) { docCategories[category] = []; } // Add document to category docCategories[category].push(doc); }); // Create a tree node for each category Object.keys(docCategories).forEach(category => { const categoryNode = { name: category, value: docCategories[category].length, children: [] }; // Create a node for each document in the category docCategories[category].forEach(doc => { const docNode = { name: truncateFilename(doc.filename), fullName: doc.filename, value: doc.tags ? doc.tags.length : 0, document: doc, nodeType: 'document', children: [] }; // Add tags as child nodes for each document if (doc.tags && doc.tags.length > 0) { doc.tags.forEach(tag => { docNode.children.push({ name: tag, value: 1, nodeType: 'tag', tagName: tag }); }); } else { // If no tags, add a "No Tags" node docNode.children.push({ name: 'No Tags', value: 1, nodeType: 'tag', tagName: 'No Tags' }); } categoryNode.children.push(docNode); }); rootNode.children.push(categoryNode); }); return [rootNode]; } // Choose suitable tree building function function buildKnowledgeTree(data) { // Return tree corresponding to current view mode if (viewMode === 'tag-doc') { return buildTagToDocTree(data); } else { return buildDocToTagTree(data); } } // Show error message function showError(message) { nodeTitle.textContent = 'Error'; nodeDetails.innerHTML = `
`; } // Show node details function showNodeDetails(params) { const data = params.data; const isWordcloudMode = mapLayout.value === 'wordcloud'; if (data) { // Use saved full filename or display name nodeTitle.textContent = data.fullName || data.name; if (data.nodeType === 'document' || data.document) { // Show document information const doc = data.document; let html = `Filename: ${doc.filename}
`; if (doc.summary) { html += `Summary: ${doc.summary}
`; } if (doc.tags && doc.tags.length > 0) { html += `Tags:
`; } html += ``; nodeDetails.innerHTML = html; } else if (data.nodeType === 'tag') { // Show tag information nodeDetails.innerHTML = `This is a tag node
Tag name: ${data.tagName || data.name}
Click on parent node to view document details
`; } else { // Show tag node or category node information nodeDetails.innerHTML = `This is a ${data.children ? 'category node' : 'node'}
Contains ${data.value} items
`; } } else { nodeTitle.textContent = 'No node selected'; nodeDetails.innerHTML = `Click on a node in the map to view details
`; } } // Add chart controls function addChartControls() { // Add zoom controller const zoomController = document.createElement('div'); zoomController.className = 'zoom-controllers'; zoomController.innerHTML = ` `; mapContainer.parentNode.insertBefore(zoomController, mapContainer); // Bind zoom events document.querySelector('.zoom-in').addEventListener('click', () => { myChart.dispatchAction({ type: 'dataZoom', start: 0, end: 50 }); }); document.querySelector('.zoom-out').addEventListener('click', () => { myChart.dispatchAction({ type: 'dataZoom', start: 0, end: 100 }); }); document.querySelector('.zoom-reset').addEventListener('click', () => { myChart.dispatchAction({ type: 'restore' }); }); } // Refresh knowledge map async function refreshKnowledgeMap() { const layout = mapLayout.value; const depth = mapDepth.value; console.log("Refreshing knowledge map, layout:", layout, "depth:", depth); // Set layout and depth setLayout(layout); setDepth(depth); // Show loading state myChart.showLoading({ text: 'Loading...', color: '#4e79a7', textColor: '#000', maskColor: 'rgba(255, 255, 255, 0.8)', zlevel: 0 }); try { // Get data const mapData = await fetchKnowledgeMap(); // Add or remove wordcloud mode CSS class const container = document.querySelector('.knowledge-map-container'); if (layout === 'wordcloud') { container.classList.add('wordcloud-mode'); // Clear node details, show wordcloud explanation nodeTitle.textContent = 'Word Cloud View'; nodeDetails.innerHTML = `Word cloud shows high-frequency words and tags from documents. Word size represents its importance in the document.
Click on any word to see related documents containing that word.
`; } else { container.classList.remove('wordcloud-mode'); nodeTitle.textContent = 'No node selected'; nodeDetails.innerHTML = 'Click on a node in the map to view details
'; } // First clear previous chart instance, completely re-render myChart.clear(); // Choose data building method based on different layout types let chartData; if (layout === 'wordcloud') { console.log("Building word cloud data..."); chartData = buildWordcloudData(mapData); console.log("Word cloud data preparation completed, data item count:", chartData.length); // For word cloud, set a simple configuration, focus on data myChart.setOption({ series: [{ type: 'wordCloud', shape: 'circle', left: 'center', top: 'center', width: '80%', height: '80%', right: null, bottom: null, sizeRange: [24, 80], rotationRange: [0, 0], rotationStep: 0, gridSize: 8, drawOutOfBound: false, layoutAnimation: false, textStyle: { fontFamily: 'Arial, 微软雅黑, sans-serif', fontWeight: 'bold', color: function(params) { // Use fixed color set, keep simple const colors = ['#000', '#333', '#666']; return colors[params.dataIndex % colors.length]; } }, emphasis: { textStyle: { color: '#f00' } }, data: chartData }] }, true); } else { // For tree chart and other chart types, use original data building method chartData = buildKnowledgeTree(mapData); option.series[0].data = chartData; // Apply configuration myChart.setOption(option, true); } console.log("Chart updated"); // For word cloud, ensure view is fully updated if (layout === 'wordcloud') { setTimeout(() => { console.log("Force redraw word cloud..."); myChart.resize(); }, 200); } } catch (error) { console.error('Failed to refresh knowledge map:', error); showError('Failed to refresh knowledge map: ' + error.message); } finally { // Hide loading state myChart.hideLoading(); } } // Toggle view mode function toggleViewMode() { // Toggle view mode viewMode = viewMode === 'tag-doc' ? 'doc-tag' : 'tag-doc'; // Update button text viewToggleBtn.innerHTML = viewMode === 'tag-doc' ? ' Switch to Document-Tag View' : ' Switch to Tag-Document View'; // Refresh knowledge map refreshKnowledgeMap(); } // Bind events mapLayout.addEventListener('change', refreshKnowledgeMap); mapDepth.addEventListener('change', refreshKnowledgeMap); refreshBtn.addEventListener('click', refreshKnowledgeMap); viewToggleBtn.addEventListener('click', toggleViewMode); // Click node event myChart.on('click', 'series', (params) => { if (mapLayout.value === 'wordcloud') { // Word cloud mode click processing handleWordCloudClick(params); } else { // Other chart mode showNodeDetails(params); } }); // Handle wordcloud click event function handleWordCloudClick(params) { console.log("Word cloud click event:", params); // Ensure there is data if (!params || !params.data) { console.error("Click event has no valid data"); return; } const word = params.data.name; const value = params.data.value; // Set node title to clicked word nodeTitle.textContent = word; // First show a temporary message indicating loading nodeDetails.innerHTML = `Word: ${word}
Frequency: ${value}
Searching for related documents...
`; // Find all documents containing the word fetchKnowledgeMap().then(mapData => { if (!mapData.documents || mapData.documents.length === 0) { nodeDetails.innerHTML = `Word: ${word}
Frequency: ${value}
No documents found
`; return; } // Find all documents containing the word (in tags or summary) const relatedDocs = mapData.documents.filter(doc => { // Check tags if (doc.tags && doc.tags.includes(word)) { return true; } // Check filename if (doc.filename && doc.filename.toLowerCase().includes(word.toLowerCase())) { return true; } // Check summary if (doc.summary && doc.summary.toLowerCase().includes(word.toLowerCase())) { return true; } return false; }); // Show related document list if (relatedDocs.length > 0) { let html = `Word: ${word}
Frequency: ${value}
Related documents: ${relatedDocs.length}
`; nodeDetails.innerHTML = html; } else { nodeDetails.innerHTML = `Word: ${word}
Frequency: ${value}
No documents containing "${word}" found
This word may have been added by default, or comes from a deleted document.
`; } }).catch(error => { console.error('Failed to get document data:', error); nodeDetails.innerHTML = `Word: ${word}
Frequency: ${value}
`; }); } // Document link click event nodeDetails.addEventListener('click', (e) => { if (e.target.closest('.document-link')) { e.preventDefault(); const filename = e.target.closest('.document-link').dataset.file; if (filename) { // Handle view document logic here, can send request or redirect console.log('View document:', filename); window.location.href = `/view-document?filename=${encodeURIComponent(filename)}`; } } else if (e.target.closest('.wordcloud-link')) { e.preventDefault(); const filename = e.target.closest('.wordcloud-link').dataset.file; if (filename) { // Directly redirect to view document page, adding show wordcloud parameter console.log('View word cloud:', filename); window.location.href = `/view-document?filename=${encodeURIComponent(filename)}&show_wordcloud=true`; } } else if (e.target.closest('.generate-cloud-link')) { e.preventDefault(); const filename = e.target.closest('.generate-cloud-link').dataset.file; if (filename) { // Generate word cloud directly on current page console.log('Directly generate word cloud:', filename); // Create a modal to display the word cloud const modal = document.createElement('div'); modal.className = 'wordcloud-modal'; modal.innerHTML = ` `; document.body.appendChild(modal); // Add close modal event modal.querySelector('.close-modal').addEventListener('click', () => { document.body.removeChild(modal); }); // Call API to get word cloud data fetch(`/api/wordcloud/${encodeURIComponent(filename)}?top_n=50`) .then(response => response.json()) .then(data => { if (data.wordcloud_image) { const imageContainer = modal.querySelector('.wordcloud-image-container'); const loadingIndicator = modal.querySelector('.loading-indicator'); const wordFreqList = modal.querySelector('.word-freq-list'); // Show word cloud imageContainer.style.display = 'block'; loadingIndicator.style.display = 'none'; const img = modal.querySelector('.wordcloud-image'); img.src = `data:image/png;base64,${data.wordcloud_image}`; // Show word frequency statistics wordFreqList.innerHTML = ''; data.word_frequency.forEach(([word, freq]) => { const wordItem = document.createElement('div'); wordItem.className = 'word-freq-item'; wordItem.textContent = `${word} (${freq})`; wordFreqList.appendChild(wordItem); }); } else { modal.querySelector('.loading-indicator').innerHTML = ``; } }) .catch(error => { console.error('Failed to generate word cloud:', error); modal.querySelector('.loading-indicator').innerHTML = ``; }); } } }); // Initialize chart myChart.setOption(option); // Add chart controls addChartControls(); // Set initial view mode button text viewToggleBtn.innerHTML = ' Switch to Document-Tag View'; // First load knowledge map refreshKnowledgeMap(); // Build word cloud data - Extract tags and high-frequency words from documents function buildWordcloudData(data) { if (data.error) { showError(data.error); return [{name: 'Data Error', value: 30}]; // Return a simple word, ensure at least some content is displayed } // If empty data, show prompt information if (!data.documents || data.documents.length === 0) { showError('No usable knowledge map data, please upload documents first'); return [ {name: 'No Data', value: 50}, {name: 'Please Upload Documents', value: 40}, {name: 'Word Cloud', value: 30} ]; } console.log("Building word cloud data, document count:", data.documents.length); // Collect all tags and their frequencies const tagCount = {}; // Some common stop words const stopwords = ['the', 'and', 'for', 'with', 'this', 'that', 'in', 'on', 'at', 'to', 'of', 'a', 'an']; // Ensure some basic words const defaultWords = ['Document', 'Learning', 'Cognition', 'Knowledge', 'Material', 'Content']; let wordIndex = 0; // Loop through all documents, extract tags data.documents.forEach(doc => { // Process tags - Tag weight highest if (doc.tags && doc.tags.length > 0) { doc.tags.forEach(tag => { if (tag && tag.length > 1 && !stopwords.includes(tag.toLowerCase())) { tagCount[tag] = (tagCount[tag] || 0) + 15; } }); } else { // If no tags, use default word const word = defaultWords[wordIndex % defaultWords.length]; tagCount[word] = (tagCount[word] || 0) + 10; wordIndex++; } // Extract keywords from filename if (doc.filename) { const baseName = doc.filename.split('.')[0]; // Remove extension if (baseName && baseName.length > 1) { tagCount[baseName] = (tagCount[baseName] || 0) + 10; } } }); // Always add some default words, ensure enough content is displayed if (Object.keys(tagCount).length < 5) { defaultWords.forEach((word, index) => { tagCount[word] = 30 - index * 3; // Decreasing weight }); // Add a "new" word for testing tagCount["new"] = 10; } // Convert to word cloud data format const cloudData = Object.keys(tagCount).map(tag => ({ name: tag, value: tagCount[tag] })); // Sort by frequency cloudData.sort((a, b) => b.value - a.value); // Show some debugging information console.log("Word cloud data building completed, vocabulary count:", cloudData.length); console.log("Word cloud data:", JSON.stringify(cloudData.slice(0, 10))); // Return sorted word cloud data return cloudData.slice(0, 50); // Limit word count } });