Science Experiment Observation Sheet

Science Experiment Observation Sheet

Variable: ${escapeHTML(exp.variableName)}

`).join('') || `

No experiments configured.

`; } function populateExperimentDropdowns() { const options = dbState.experiments.map(exp => ``).join(''); experimentFilterSelect.innerHTML = options; obsExperimentSelect.innerHTML = options; // Set filter to current, or default to first if (activeExperimentFilter) experimentFilterSelect.value = activeExperimentFilter; else if (dbState.experiments.length > 0) activeExperimentFilter = dbState.experiments[0].id; updateObservationForm(); // Update form based on default selection } function updateObservationForm() { const selectedId = obsExperimentSelect.value; const experiment = dbState.experiments.find(ex => ex.id === selectedId); if (experiment) { obsHypothesisDisplay.textContent = experiment.hypothesis || "N/A"; obsQuantLabel.textContent = experiment.variableName || "Quantitative Value"; obsQuantValueInput.placeholder = `Enter ${experiment.variableName || 'Value'}`; } else { obsHypothesisDisplay.textContent = "N/A"; obsQuantLabel.textContent = "Quantitative Value"; obsQuantValueInput.placeholder = "Enter Value"; } } function renderDashboard() { const experiment = dbState.experiments.find(ex => ex.id === activeExperimentFilter); if (!experiment) { dashboardTitle.textContent = "Observation Dashboard"; kpiTotal.textContent = "N/A"; kpiAverage.textContent = "N/A"; kpiAverageLabel.textContent = "Average Value"; observationFeed.innerHTML = `

Please select an experiment to view data.

`; if (observationChartInstance) observationChartInstance.destroy(); pdfDownloadBtn.style.display = 'none'; return; } dashboardTitle.textContent = `Dashboard: ${escapeHTML(experiment.name)}`; pdfDownloadBtn.style.display = dbState.observations.length > 0 ? 'inline-block' : 'none'; // 1. KPIs const values = dbState.observations.map(obs => obs.quantitativeValue).filter(v => typeof v === 'number'); const total = values.length; const average = total > 0 ? (values.reduce((a, b) => a + b, 0) / total) : 0; kpiTotal.textContent = total; kpiAverage.textContent = average.toFixed(2); kpiAverageLabel.textContent = `Average ${escapeHTML(experiment.variableName) || 'Value'}`; // 2. Chart renderObservationChart(experiment.variableName); // 3. Feed renderObservationFeed(); } function renderObservationChart(variableName) { if (!observationChartCanvas) return; const ctx = observationChartCanvas.getContext('2d'); const chartData = dbState.observations .filter(obs => obs.obsTime?.toDate && typeof obs.quantitativeValue === 'number' && !isNaN(obs.obsTime.toDate().getTime())) .map(obs => ({ x: obs.obsTime.toDate(), y: obs.quantitativeValue })); // Data is already sorted by query if (observationChartInstance) { observationChartInstance.destroy(); } if (chartData.length < 2) { ctx.clearRect(0, 0, observationChartCanvas.width, observationChartCanvas.height); ctx.font = "14px Inter"; ctx.fillStyle = "#a8a29e"; // stone-400 ctx.textAlign = "center"; ctx.fillText("Need at least two valid data points to plot chart.", observationChartCanvas.width / 2, observationChartCanvas.height / 2); return; } observationChartInstance = new Chart(ctx, { type: 'line', data: { datasets: [{ label: escapeHTML(variableName) || 'Value', data: chartData, borderColor: 'var(--seo-primary, #3b82f6)', tension: 0.1 }] }, options: { responsive: true, maintainAspectRatio: false, // Per spec scales: { x: { type: 'time', time: { unit: 'day', tooltipFormat: 'MMM d, yyyy HH:mm' }, title: { display: true, text: 'Observation Time' } }, y: { title: { display: true, text: escapeHTML(variableName) || 'Value' }, beginAtZero: true } }, plugins: { tooltip: { callbacks: { label: (context) => `${context.dataset.label}: ${context.raw.y.toFixed(2)}` } } } } }); } function renderObservationFeed() { if (dbState.observations.length === 0) { observationFeed.innerHTML = `

No observations logged for this experiment yet.

`; return; } observationFeed.innerHTML = dbState.observations.map(obs => { const obsDate = obs.obsTime?.toDate ? obs.obsTime.toDate() : new Date(); const formattedTime = !isNaN(obsDate.getTime()) ? obsDate.toLocaleString() : 'Invalid Date'; return `

${formattedTime}

Observer: ${escapeHTML(obs.observer)}

${escapeHTML(obs.variableName) || 'Value'}: ${obs.quantitativeValue}

Qualitative Notes: ${escapeHTML(obs.qualitative) || 'N/A'}

` }).reverse().join(''); // Show newest first in the list } // --- Event Handlers --- function handleFilterChange() { activeExperimentFilter = experimentFilterSelect.value; loadObservations(); // This will trigger a re-render } async function downloadPDF() { // Per user request, ensuring this is robust pdfTarget.classList.add('seo-pdf-view'); try { // 1. Capture Chart const chartCanvasForPdf = document.createElement('canvas'); chartCanvasForPdf.width = observationChartCanvas.width; chartCanvasForPdf.height = observationChartCanvas.height; const chartCtxPdf = chartCanvasForPdf.getContext('2d'); chartCtxPdf.fillStyle = '#FFFFFF'; chartCtxPdf.fillRect(0, 0, chartCanvasForPdf.width, chartCanvasForPdf.height); if (observationChartInstance) { chartCtxPdf.drawImage(observationChartCanvas, 0, 0); } else { chartCtxPdf.font = "14px Inter"; chartCtxPdf.fillStyle = "#a8a29e"; chartCtxPdf.textAlign = "center"; chartCtxPdf.fillText("No chart data available.", chartCanvasForPdf.width / 2, chartCanvasForPdf.height / 2); } const chartImage = chartCanvasForPdf.toDataURL('image/png'); // 2. Capture Main Content (without live chart) const canvas = await html2canvas(pdfTarget, { scale: 2, logging: false, useCORS: true, ignoreElements: (element) => element.id === 'seo-observation-chart' }); const imgData = canvas.toDataURL('image/png'); const pdf = new jsPDF('p', 'mm', 'a4'); const pdfWidth = pdf.internal.pageSize.getWidth(); const pdfHeight = pdf.internal.pageSize.getHeight(); const margin = 15; const imgWidth = pdfWidth - (margin * 2); const imgHeight = (canvas.height * imgWidth) / canvas.width; let yPosition = margin; // 3. Add Content Image to PDF pdf.addImage(imgData, 'PNG', margin, yPosition, imgWidth, imgHeight); yPosition += imgHeight + 10; // 4. Add Chart Image to PDF const chartHeightInPdf = (chartCanvasForPdf.height * imgWidth) / chartCanvasForPdf.width; if (yPosition + chartHeightInPdf > pdfHeight - margin) { pdf.addPage(); yPosition = margin; } pdf.setFontSize(14); pdf.setTextColor(31, 41, 55); // gray-800 pdf.text("Observation Chart", margin, yPosition); yPosition += 8; pdf.addImage(chartImage, 'PNG', margin, yPosition, imgWidth, chartHeightInPdf); pdf.save(`Observation_Sheet_${experimentFilterSelect.options[experimentFilterSelect.selectedIndex].text}.pdf`); } catch (error) { console.error("Error generating PDF:", error); showMessage("Error generating PDF. Please try again.", "error"); } finally { pdfTarget.classList.remove('seo-pdf-view'); } } // --- Utilities --- function showMessage(message, type = "info") { let feedbackEl = document.getElementById('feedback-message'); if (!feedbackEl) { feedbackEl = document.createElement('div'); feedbackEl.id = 'feedback-message'; feedbackEl.style.position = 'fixed'; feedbackEl.style.bottom = '20px'; feedbackEl.style.left = '50%'; feedbackEl.style.transform = 'translateX(-50%)'; feedbackEl.style.padding = '10px 20px'; feedbackEl.style.borderRadius = '8px'; feedbackEl.style.zIndex = '1000'; feedbackEl.style.opacity = '0'; feedbackEl.style.transition = 'opacity 0.5s ease'; document.body.appendChild(feedbackEl); } feedbackEl.textContent = message; feedbackEl.style.opacity = '1'; const colors = { error: { bg: '#fef2f2', text: '#991b1b' }, success: { bg: '#f0fdf4', text: '#14532d' }, info: { bg: '#eff6ff', text: '#1e3a8a' }}; feedbackEl.style.backgroundColor = colors[type].bg; feedbackEl.style.color = colors[type].text; setTimeout(() => { feedbackEl.style.opacity = '0'; }, 3000); } function escapeHTML(str) { if (typeof str !== 'string') return ''; return str ? str.replace(/[&<>"']/g, m => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[m])) : ''; } function setFormDateTimeToNow() { // Set default date/time for new entry const now = new Date(); const yyyy = now.getFullYear(); const mm = String(now.getMonth() + 1).padStart(2, '0'); // Months are 0-indexed const dd = String(now.getDate()).padStart(2, '0'); const hh = String(now.getHours()).padStart(2, '0'); const mi = String(now.getMinutes()).padStart(2, '0'); // Format for datetime-local input: YYYY-MM-DDThh:mm if (obsTimeInput) { obsTimeInput.value = `${yyyy}-${mm}-${dd}T${hh}:${mi}`; } } // --- Event Listeners --- navTabs.forEach((tab, index) => tab.addEventListener('click', () => showTab(index))); navPrev.addEventListener('click', () => showTab(currentTab - 1)); navNext.addEventListener('click', () => showTab(currentTab + 1)); // Tab 1 pdfDownloadBtn.addEventListener('click', downloadPDF); experimentFilterSelect.addEventListener('change', handleFilterChange); observationFeed.addEventListener('click', (e) => { if (e.target.classList.contains('delete-btn') && e.target.dataset.type === 'observation') { deleteObservation(e.target.dataset.id); } }); // Tab 2 observationForm.addEventListener('submit', handleLogObservation); obsExperimentSelect.addEventListener('change', updateObservationForm); // Tab 3 addExperimentForm.addEventListener('submit', handleAddExperiment); loadSampleBtn.addEventListener('click', () => { // Sample data (USA-centric) addConfigItem('experiments', { name: "Corn Growth (USA)", hypothesis: "Corn plants with fertilizer X will grow taller.", variableName: "Plant Height (cm)" }); addConfigItem('experiments', { name: "Soil pH (Iowa)", hypothesis: "Cover crops will increase soil pH over 6 months.", variableName: "Soil pH" }); }); experimentsList.addEventListener('click', (e) => { if (e.target.classList.contains('delete-btn') && e.target.dataset.type === 'experiment') { deleteExperiment(e.target.dataset.id); } }); // --- Initial Load --- setFormDateTimeToNow(); showTab(0); // Start on dashboard

Science Experiment Observation Sheet

Configure experiments, log observations, and track results.

Observation Dashboard

Total Observations

0

Average Value

0.00

Observation Trend

Observation Log

Log New Observation

Experiment Hypothesis:

Experiment Configuration

Add or remove experiments here. The "Primary Quantitative Variable" will be the data point tracked in the dashboard chart.

Add New Experiment

Saved Experiments

Scroll to Top