Science Concept Map Generator
The main node or topic at the center of the map.
Add Concept Nodes
Current Concepts (Nodes)
| Concept ID | Concept Name (Editable) | Actions |
|---|
Define Connections (Links)
The word or phrase linking the source to the target.
Current Links (Relationships)
| Source Concept | Relationship | Target Concept | Actions |
|---|
Review the structured map protocol.
Click "Next" or "Previous" to refresh this preview.
Central Concept: ${escapeHTML(data.centralConcept)}
Author: ${escapeHTML(data.author)}
Date: ${escapeHTML(data.date)}
Total Concepts: ${data.concepts.length}
Total Links: ${data.links.length}
2. Concepts (Nodes) Defined
${data.concepts.length > 0 ? `${data.concepts.map(c => `${c.id}: ${escapeHTML(c.name)}`).join(' | ')}
` : `No concepts defined.
` }3. Map Structure (Link Protocol)
${structuredMap}
`;
};
const downloadTxt = () => {
const data = getMapData();
let content = `SCIENCE CONCEPT MAP PROTOCOL\n`;
content += `========================================\n\n`;
content += "1. MAP METADATA\n";
content += "--------------------\n";
content += `Title: ${data.title}\n`;
content += `Central Concept: ${data.centralConcept}\n`;
content += `Author: ${data.author}\n`;
content += `Date: ${data.date}\n\n`;
content += "2. CONCEPTS (NODES) DEFINED\n";
content += "--------------------\n";
data.concepts.forEach(c => {
content += `${c.id.padEnd(5)}: ${c.name}\n`;
});
content += "\n";
content += "3. MAP STRUCTURE (LINKS)\n";
content += "--------------------\n";
if (data.links.length > 0) {
data.links.forEach(l => {
const sourceName = getConceptName(l.sourceId);
const targetName = getConceptName(l.targetId);
content += `${sourceName} (${l.sourceId}) --[ ${l.relationship.toUpperCase()} ]--> ${targetName} (${l.targetId})\n`;
});
} else {
content += "No links defined.\n";
}
const blob = new Blob([content], { type: 'text/plain;charset=utf-8' });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = `concept_map_${data.title.replace(/ /g, '_')}.txt`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(a.href);
};
const downloadPDF = () => {
if (typeof window.jspdf === 'undefined' || typeof window.jspdf.jsPDF === 'undefined') {
alert('Error: jsPDF library not loaded.');
return;
}
const { jsPDF } = window.jspdf;
const doc = new jsPDF('p', 'mm', 'a4');
const data = getMapData();
const margin = 20;
const pageWidth = doc.internal.pageSize.getWidth();
const usableWidth = pageWidth - (margin * 2);
let yPos = 20;
const addTitle = (text, size, style, color = [44, 62, 80], align = 'left') => {
doc.setFontSize(size);
doc.setFont(undefined, style);
doc.setTextColor(color[0], color[1], color[2]);
doc.text(text, pageWidth / 2, yPos, { align: 'center' });
yPos += size / 2 + 5;
};
const addSectionTitle = (text) => {
yPos += 5;
doc.setFontSize(14);
doc.setFont(undefined, 'bold');
doc.setTextColor(39, 174, 96); /* Green */
doc.text(text, margin, yPos);
doc.setDrawColor(224, 224, 224);
doc.line(margin, yPos + 1, pageWidth - margin, yPos + 1);
yPos += 8;
};
// --- Build PDF Document ---
addTitle("Science Concept Map Protocol", 18, 'bold');
addTitle(data.title, 12, 'normal', [108, 117, 125]);
yPos += 5;
// 1. Metadata
addSectionTitle("1. Map Metadata");
doc.setFontSize(11);
doc.setFont(undefined, 'normal');
doc.setTextColor(52, 73, 94);
doc.text(`Central Concept: ${data.centralConcept}`, margin, yPos);
doc.text(`Author: ${data.author}`, margin, yPos + 6);
doc.text(`Date: ${data.date}`, margin, yPos + 12);
doc.text(`Total Concepts: ${data.concepts.length}`, margin, yPos + 18);
yPos += 25;
// 2. Concepts Table
addSectionTitle("2. Concepts (Nodes) Defined");
const nodeHead = [['Concept ID', 'Concept Name']];
const nodeBody = data.concepts.map(c => [c.id, c.name]);
doc.autoTable({
startY: yPos,
head: nodeHead,
body: nodeBody,
theme: 'grid',
styles: { fontSize: 10, cellPadding: 3, textColor: [52, 73, 94] },
headStyles: { fillColor: [39, 174, 96], textColor: [255, 255, 255] },
columnStyles: { 0: { cellWidth: 30, fontStyle: 'bold' } },
margin: { left: margin, right: margin }
});
yPos = doc.autoTable.previous.finalY + 10;
// 3. Links Table
addSectionTitle("3. Map Structure (Link Protocol)");
if (data.links.length > 0) {
const linkHead = [['Source Concept (ID)', 'Relationship', 'Target Concept (ID)']];
const linkBody = data.links.map(l => [
`${getConceptName(l.sourceId)} (${l.sourceId})`,
l.relationship.toUpperCase(),
`${getConceptName(l.targetId)} (${l.targetId})`
]);
doc.autoTable({
startY: yPos,
head: linkHead,
body: linkBody,
theme: 'grid',
styles: { fontSize: 10, cellPadding: 3, textColor: [52, 73, 94] },
headStyles: { fillColor: [189, 195, 199], textColor: [44, 62, 80] },
columnStyles: { 1: { fontStyle: 'bold', halign: 'center', textColor: [39, 174, 96] } },
margin: { left: margin, right: margin }
});
} else {
doc.setFontSize(11);
doc.setTextColor(231, 76, 60);
doc.text("No links defined to map concepts.", margin, yPos);
}
doc.save('concept_map_protocol.pdf');
};
// --- Event Listeners ---
// Tab Buttons
tabButtons.forEach((btn, index) => {
btn.addEventListener('click', () => showTab(index + 1));
});
// Next/Prev Navigation
nextBtn.addEventListener('click', () => showTab(currentTab + 1));
prevBtn.addEventListener('click', () => showTab(currentTab - 1));
// Tab 2 Actions (Nodes)
nodeInputs.addBtn.addEventListener('click', addNode);
nodeInputs.tbody.addEventListener('click', (e) => {
if (e.target.dataset.removeId) {
removeNode(e.target.dataset.removeId);
}
});
nodeInputs.tbody.addEventListener('blur', (e) => {
if (e.target.tagName === 'TD' && e.target.isContentEditable) {
updateNode(e.target.dataset.id, e.target.textContent);
}
}, true);
// Tab 3 Actions (Links)
linkInputs.addBtn.addEventListener('click', addLink);
linkInputs.tbody.addEventListener('click', (e) => {
if (e.target.dataset.removeId) {
removeLink(parseInt(e.target.dataset.removeId));
}
});
// Tab 4 Actions
refreshPreviewBtn.addEventListener('click', generatePreview);
downloadPdfBtn.addEventListener('click', downloadPDF);
downloadTxtBtn.addEventListener('click', downloadTxt);
// --- Initialization ---
// Add sample data
concepts.push({ id: 'C1', name: 'Photosynthesis' });
concepts.push({ id: 'C2', name: 'Sunlight' });
concepts.push({ id: 'C3', name: 'Water' });
concepts.push({ id: 'C4', name: 'Glucose' });
concepts.push({ id: 'C5', name: 'Chlorophyll' });
conceptIdCounter = 6;
links.push({ id: linkIdCounter++, sourceId: 'C1', targetId: 'C4', relationship: 'produces' });
links.push({ id: linkIdCounter++, sourceId: 'C2', targetId: 'C1', relationship: 'is absorbed by' });
links.push({ id: linkIdCounter++, sourceId: 'C3', targetId: 'C1', relationship: 'is required for' });
links.push({ id: linkIdCounter++, sourceId: 'C5', targetId: 'C2', relationship: 'absorbs' });
showTab(1); // Set initial state
});
