Develop very good app time in second, min, hours option and stepsize option using code "import numpy as np import pandas as pd import matplotlib.pyplot as plt from scipy.constants import N_A # Avogadro's number # ============================================= # PARAMETERS (24-hour simulation) # ============================================= initial_cells = 1e4 # Initial T-cell count (10,000 cells) growth_rate = 0.15 # Growth rate (0.15 per hour) blood_volume = 5 # Blood volume (5 liters) noise_level = 0.05 # 5% multiplicative noise total_seconds = 86400 # 24 hours in seconds step_size = 1 # 1-second resolution # ============================================= # TIME GRID (1-second steps) # ============================================= time_seconds = np.arange(0, total_seconds + 1, step_size) time_hours = time_seconds / 3600 # Convert to hours # ============================================= # GROWTH MODEL CALCULATIONS # ============================================= # Convert growth rate to per-second growth_rate_per_second = growth_rate / 3600 # Exponential growth model ideal_counts = initial_cells * np.exp(growth_rate_per_second * time_seconds) # Add log-normal noise (preserves positivity) np.random.seed(42) # For reproducibility noise = np.random.lognormal(mean=0, sigma=noise_level, size=len(time_seconds)) noisy_counts = ideal_counts * noise # Convert to molar concentration molar_conversion = 1 / (N_A * blood_volume) molar_ideal = ideal_counts * molar_conversion molar_noisy = noisy_counts * molar_conversion # ============================================= # CREATE DATAFRAME (86,401 rows) # ============================================= data = { 'time_seconds': time_seconds, 'time_hours': np.round(time_hours, 6), 'ideal_cell_count': np.round(ideal_counts, 2), 'noisy_cell_count': np.round(noisy_counts, 2), 'molar_ideal': molar_ideal, 'molar_noisy': molar_noisy } df = pd.DataFrame(data) print(f"Generated {len(df)} data points (1-second resolution)") print(df.head()) print(df.tail()) # ============================================= # VISUALIZATION (Sampled for clarity) # ============================================= sample_interval = 300 # Show every 5 minutes fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 8)) # Linear-scale plot (sampled) ax1.plot(time_seconds[::sample_interval], molar_ideal[::sample_interval], 'b--', label='Ideal Growth') ax1.plot(time_seconds[::sample_interval], molar_noisy[::sample_interval], 'r-', alpha=0.7, label='Noisy Measurements') ax1.set_xlabel('Time (seconds)') ax1.set_ylabel('Concentration (M)') ax1.set_title('T-cell Growth (Linear Scale)') ax1.legend() ax1.grid(True) # Log-scale plot (sampled) ax2.semilogy(time_seconds[::sample_interval], molar_ideal[::sample_interval], 'b--', label='Ideal Growth') ax2.semilogy(time_seconds[::sample_interval], molar_noisy[::sample_interval], 'r-', alpha=0.7, label='Noisy Measurements') ax2.set_xlabel('Time (seconds)') ax2.set_ylabel('Concentration (M)') ax2.set_title('T-cell Growth (Log Scale)') ax2.legend() ax2.grid(True, which="both", ls="--") plt.tight_layout() plt.show() # ============================================= # DATA EXPORT # ============================================= df.to_csv('tcell_growth_1sec_resolution_24h.csv', index=False) print("Data saved to 'tcell_growth_1sec_resolution_24h.csv'")" - Initial Deployment
029dabb verified Download index.html from STE-Nexus/t-cell-data-visualization-app: direct link, hf CLI and curl.
- Browser
- Download file 31 kB
-
https://huggingface.co/spaces/STE-Nexus/t-cell-data-visualization-app/resolve/main/index.html
- Command line
-
hf download hf://spaces/STE-Nexus/t-cell-data-visualization-app/index.html
-
curl -L -o index.html https://huggingface.co/spaces/STE-Nexus/t-cell-data-visualization-app/resolve/main/index.html
31 kB
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>T-cell Growth Visualization</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/numeric@1.2.6/numeric.min.js"></script> | |
| <style> | |
| .tooltip { | |
| position: relative; | |
| display: inline-block; | |
| } | |
| .tooltip .tooltiptext { | |
| visibility: hidden; | |
| width: 200px; | |
| background-color: #333; | |
| color: #fff; | |
| text-align: center; | |
| border-radius: 6px; | |
| padding: 5px; | |
| position: absolute; | |
| z-index: 1; | |
| bottom: 125%; | |
| left: 50%; | |
| margin-left: -100px; | |
| opacity: 0; | |
| transition: opacity 0.3s; | |
| } | |
| .tooltip:hover .tooltiptext { | |
| visibility: visible; | |
| opacity: 1; | |
| } | |
| .chart-container { | |
| position: relative; | |
| height: 300px; | |
| width: 100%; | |
| } | |
| @media (min-width: 768px) { | |
| .chart-container { | |
| height: 400px; | |
| } | |
| } | |
| input[type="range"] { | |
| -webkit-appearance: none; | |
| height: 8px; | |
| background: #ddd; | |
| border-radius: 5px; | |
| background-image: linear-gradient(#3b82f6, #3b82f6); | |
| background-size: 0% 100%; | |
| background-repeat: no-repeat; | |
| } | |
| input[type="range"]::-webkit-slider-thumb { | |
| -webkit-appearance: none; | |
| height: 20px; | |
| width: 20px; | |
| border-radius: 50%; | |
| background: #3b82f6; | |
| cursor: pointer; | |
| box-shadow: 0 0 2px 0 #555; | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-gray-50 min-h-screen"> | |
| <div class="container mx-auto px-4 py-8"> | |
| <header class="mb-8 text-center"> | |
| <h1 class="text-3xl md:text-4xl font-bold text-gray-800 mb-2">T-cell Growth Visualizer</h1> | |
| <p class="text-gray-600 max-w-2xl mx-auto">Simulate and visualize T-cell population growth over time with customizable parameters</p> | |
| </header> | |
| <div class="grid grid-cols-1 lg:grid-cols-3 gap-8"> | |
| <!-- Control Panel --> | |
| <div class="bg-white rounded-lg shadow-md p-6 h-fit lg:sticky lg:top-4"> | |
| <h2 class="text-xl font-semibold text-gray-800 mb-4">Simulation Parameters</h2> | |
| <div class="space-y-6"> | |
| <div> | |
| <label for="initialCells" class="block text-sm font-medium text-gray-700 mb-1"> | |
| Initial T-cell Count (cells) | |
| <span class="tooltip"> | |
| <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 inline ml-1 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /> | |
| </svg> | |
| <span class="tooltiptext">Starting number of T-cells in the simulation</span> | |
| </span> | |
| </label> | |
| <input type="number" id="initialCells" value="10000" min="1" step="100" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"> | |
| </div> | |
| <div> | |
| <label for="growthRate" class="block text-sm font-medium text-gray-700 mb-1"> | |
| Growth Rate (per hour) | |
| <span class="tooltip"> | |
| <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 inline ml-1 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /> | |
| </svg> | |
| <span class="tooltiptext">Rate at which T-cells multiply per hour</span> | |
| </span> | |
| </label> | |
| <input type="number" id="growthRate" value="0.15" min="0.01" step="0.01" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"> | |
| </div> | |
| <div> | |
| <label for="bloodVolume" class="block text-sm font-medium text-gray-700 mb-1"> | |
| Blood Volume (liters) | |
| <span class="tooltip"> | |
| <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 inline ml-1 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /> | |
| </svg> | |
| <span class="tooltiptext">Total blood volume for concentration calculation</span> | |
| </span> | |
| </label> | |
| <input type="number" id="bloodVolume" value="5" min="1" step="0.5" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"> | |
| </div> | |
| <div> | |
| <label for="noiseLevel" class="block text-sm font-medium text-gray-700 mb-1"> | |
| Noise Level (%) | |
| <span class="tooltip"> | |
| <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 inline ml-1 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /> | |
| </svg> | |
| <span class="tooltiptext">Amount of random variation in the measurements</span> | |
| </span> | |
| </label> | |
| <input type="number" id="noiseLevel" value="5" min="0" max="100" step="1" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"> | |
| </div> | |
| <div> | |
| <label for="totalTime" class="block text-sm font-medium text-gray-700 mb-1"> | |
| Simulation Duration | |
| <span class="tooltip"> | |
| <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 inline ml-1 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /> | |
| </svg> | |
| <span class="tooltiptext">Total time period for the simulation</span> | |
| </span> | |
| </label> | |
| <div class="flex space-x-2"> | |
| <input type="number" id="totalTimeValue" value="24" min="1" step="1" class="mt-1 block w-3/4 rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"> | |
| <select id="totalTimeUnit" class="mt-1 block w-1/4 rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"> | |
| <option value="seconds">seconds</option> | |
| <option value="minutes">minutes</option> | |
| <option value="hours" selected>hours</option> | |
| </select> | |
| </div> | |
| </div> | |
| <div> | |
| <label for="stepSize" class="block text-sm font-medium text-gray-700 mb-1"> | |
| Step Size | |
| <span class="tooltip"> | |
| <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 inline ml-1 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /> | |
| </svg> | |
| <span class="tooltiptext">Time interval between data points</span> | |
| </span> | |
| </label> | |
| <div class="flex space-x-2"> | |
| <input type="number" id="stepSizeValue" value="1" min="1" step="1" class="mt-1 block w-3/4 rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"> | |
| <select id="stepSizeUnit" class="mt-1 block w-1/4 rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"> | |
| <option value="seconds" selected>seconds</option> | |
| <option value="minutes">minutes</option> | |
| <option value="hours">hours</option> | |
| </select> | |
| </div> | |
| </div> | |
| <div> | |
| <label for="displayUnit" class="block text-sm font-medium text-gray-700 mb-1"> | |
| Display Time Unit | |
| <span class="tooltip"> | |
| <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 inline ml-1 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /> | |
| </svg> | |
| <span class="tooltiptext">Time unit for the x-axis in charts</span> | |
| </span> | |
| </label> | |
| <select id="displayUnit" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"> | |
| <option value="seconds">Seconds</option> | |
| <option value="minutes">Minutes</option> | |
| <option value="hours" selected>Hours</option> | |
| </select> | |
| </div> | |
| </div> | |
| <div class="mt-6 flex justify-between"> | |
| <button id="resetBtn" class="px-4 py-2 border border-gray-300 rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500"> | |
| Reset Defaults | |
| </button> | |
| <button id="runSimulation" class="px-4 py-2 border border-transparent rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500"> | |
| Run Simulation | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Results Section --> | |
| <div class="lg:col-span-2 space-y-6"> | |
| <div class="bg-white rounded-lg shadow-md p-6"> | |
| <h2 class="text-xl font-semibold text-gray-800 mb-4">Simulation Results</h2> | |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4"> | |
| <div class="bg-blue-50 p-4 rounded-lg"> | |
| <p class="text-sm text-blue-600 font-medium">Initial Cell Count</p> | |
| <p id="initialCountDisplay" class="text-2xl font-bold text-blue-800">10,000 cells</p> | |
| </div> | |
| <div class="bg-green-50 p-4 rounded-lg"> | |
| <p class="text-sm text-green-600 font-medium">Final Cell Count</p> | |
| <p id="finalCountDisplay" class="text-2xl font-bold text-green-800">0 cells</p> | |
| </div> | |
| <div class="bg-purple-50 p-4 rounded-lg"> | |
| <p class="text-sm text-purple-600 font-medium">Growth Rate</p> | |
| <p id="growthRateDisplay" class="text-2xl font-bold text-purple-800">0.15 /hour</p> | |
| </div> | |
| <div class="bg-yellow-50 p-4 rounded-lg"> | |
| <p class="text-sm text-yellow-600 font-medium">Data Points</p> | |
| <p id="dataPointsDisplay" class="text-2xl font-bold text-yellow-800">0</p> | |
| </div> | |
| </div> | |
| <div class="flex space-x-2 mb-4"> | |
| <button id="exportBtn" class="px-4 py-2 border border-gray-300 rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500"> | |
| Export Data (CSV) | |
| </button> | |
| <button id="sampleIntervalBtn" class="px-4 py-2 border border-gray-300 rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500"> | |
| Sample Every 5 Minutes | |
| </button> | |
| </div> | |
| </div> | |
| <div class="bg-white rounded-lg shadow-md p-6"> | |
| <h2 class="text-xl font-semibold text-gray-800 mb-4">Growth Visualization (Linear Scale)</h2> | |
| <div class="chart-container"> | |
| <canvas id="linearChart"></canvas> | |
| </div> | |
| </div> | |
| <div class="bg-white rounded-lg shadow-md p-6"> | |
| <h2 class="text-xl font-semibold text-gray-800 mb-4">Growth Visualization (Log Scale)</h2> | |
| <div class="chart-container"> | |
| <canvas id="logChart"></canvas> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| // Constants | |
| const AVOGADRO = 6.02214076e23; | |
| // DOM Elements | |
| const initialCellsInput = document.getElementById('initialCells'); | |
| const growthRateInput = document.getElementById('growthRate'); | |
| const bloodVolumeInput = document.getElementById('bloodVolume'); | |
| const noiseLevelInput = document.getElementById('noiseLevel'); | |
| const totalTimeValueInput = document.getElementById('totalTimeValue'); | |
| const totalTimeUnitSelect = document.getElementById('totalTimeUnit'); | |
| const stepSizeValueInput = document.getElementById('stepSizeValue'); | |
| const stepSizeUnitSelect = document.getElementById('stepSizeUnit'); | |
| const displayUnitSelect = document.getElementById('displayUnit'); | |
| const runSimulationBtn = document.getElementById('runSimulation'); | |
| const resetBtn = document.getElementById('resetBtn'); | |
| const exportBtn = document.getElementById('exportBtn'); | |
| const sampleIntervalBtn = document.getElementById('sampleIntervalBtn'); | |
| const initialCountDisplay = document.getElementById('initialCountDisplay'); | |
| const finalCountDisplay = document.getElementById('finalCountDisplay'); | |
| const growthRateDisplay = document.getElementById('growthRateDisplay'); | |
| const dataPointsDisplay = document.getElementById('dataPointsDisplay'); | |
| // Chart instances | |
| let linearChartInstance = null; | |
| let logChartInstance = null; | |
| // Initialize the app | |
| document.addEventListener('DOMContentLoaded', function() { | |
| // Set up event listeners | |
| runSimulationBtn.addEventListener('click', runSimulation); | |
| resetBtn.addEventListener('click', resetDefaults); | |
| exportBtn.addEventListener('click', exportData); | |
| sampleIntervalBtn.addEventListener('click', applySampleInterval); | |
| // Run initial simulation | |
| runSimulation(); | |
| }); | |
| // Main simulation function | |
| function runSimulation() { | |
| // Get input values | |
| const initialCells = parseFloat(initialCellsInput.value); | |
| const growthRate = parseFloat(growthRateInput.value); | |
| const bloodVolume = parseFloat(bloodVolumeInput.value); | |
| const noiseLevel = parseFloat(noiseLevelInput.value) / 100; | |
| const totalTimeValue = parseFloat(totalTimeValueInput.value); | |
| const totalTimeUnit = totalTimeUnitSelect.value; | |
| const stepSizeValue = parseFloat(stepSizeValueInput.value); | |
| const stepSizeUnit = stepSizeUnitSelect.value; | |
| const displayUnit = displayUnitSelect.value; | |
| // Convert time units to seconds | |
| const totalSeconds = convertToSeconds(totalTimeValue, totalTimeUnit); | |
| const stepSizeSeconds = convertToSeconds(stepSizeValue, stepSizeUnit); | |
| // Validate inputs | |
| if (stepSizeSeconds <= 0 || stepSizeSeconds > totalSeconds) { | |
| alert('Step size must be positive and smaller than total duration'); | |
| return; | |
| } | |
| // Calculate time points | |
| const timeSeconds = numeric.linspace(0, totalSeconds, Math.floor(totalSeconds / stepSizeSeconds) + 1); | |
| // Convert growth rate to per-second | |
| const growthRatePerSecond = growthRate / 3600; | |
| // Calculate ideal growth | |
| const idealCounts = timeSeconds.map(t => initialCells * Math.exp(growthRatePerSecond * t)); | |
| // Add log-normal noise | |
| const noisyCounts = addLogNormalNoise(idealCounts, noiseLevel); | |
| // Convert to molar concentration | |
| const molarConversion = 1 / (AVOGADRO * bloodVolume); | |
| const molarIdeal = idealCounts.map(count => count * molarConversion); | |
| const molarNoisy = noisyCounts.map(count => count * molarConversion); | |
| // Convert time to display unit | |
| const displayTime = convertTimeUnit(timeSeconds, displayUnit); | |
| // Update results display | |
| updateResultsDisplay(initialCells, noisyCounts[noisyCounts.length - 1], growthRate, timeSeconds.length); | |
| // Create and update charts | |
| updateCharts(displayTime, molarIdeal, molarNoisy, displayUnit); | |
| // Store data for export | |
| window.simulationData = { | |
| timeSeconds: timeSeconds, | |
| timeHours: timeSeconds.map(t => t / 3600), | |
| idealCounts: idealCounts, | |
| noisyCounts: noisyCounts, | |
| molarIdeal: molarIdeal, | |
| molarNoisy: molarNoisy, | |
| displayUnit: displayUnit | |
| }; | |
| } | |
| // Helper function to convert time to seconds | |
| function convertToSeconds(value, unit) { | |
| switch(unit) { | |
| case 'seconds': return value; | |
| case 'minutes': return value * 60; | |
| case 'hours': return value * 3600; | |
| default: return value; | |
| } | |
| } | |
| // Helper function to convert time to display unit | |
| function convertTimeUnit(timeSeconds, displayUnit) { | |
| switch(displayUnit) { | |
| case 'seconds': return timeSeconds; | |
| case 'minutes': return timeSeconds.map(t => t / 60); | |
| case 'hours': return timeSeconds.map(t => t / 3600); | |
| default: return timeSeconds; | |
| } | |
| } | |
| // Helper function to add log-normal noise | |
| function addLogNormalNoise(data, noiseLevel) { | |
| // Set seed for reproducibility (based on current time) | |
| const seed = Date.now() % 1000000; | |
| const random = new Math.seedrandom(seed.toString()); | |
| return data.map(value => { | |
| // Generate log-normal noise | |
| const noise = Math.exp(random.normal(0, noiseLevel)); | |
| return value * noise; | |
| }); | |
| } | |
| // Helper function to update results display | |
| function updateResultsDisplay(initialCells, finalCells, growthRate, dataPoints) { | |
| initialCountDisplay.textContent = initialCells.toLocaleString() + ' cells'; | |
| finalCountDisplay.textContent = Math.round(finalCells).toLocaleString() + ' cells'; | |
| growthRateDisplay.textContent = growthRate.toFixed(2) + ' /hour'; | |
| dataPointsDisplay.textContent = dataPoints.toLocaleString(); | |
| } | |
| // Function to create or update charts | |
| function updateCharts(time, ideal, noisy, displayUnit) { | |
| // Get display unit label | |
| const unitLabel = getTimeUnitLabel(displayUnit); | |
| // Prepare data for charts | |
| const chartData = { | |
| labels: time, | |
| datasets: [ | |
| { | |
| label: 'Ideal Growth', | |
| data: ideal, | |
| borderColor: 'rgba(59, 130, 246, 1)', | |
| backgroundColor: 'rgba(59, 130, 246, 0.1)', | |
| borderDash: [5, 5], | |
| borderWidth: 2, | |
| pointRadius: 0, | |
| fill: false | |
| }, | |
| { | |
| label: 'Noisy Measurements', | |
| data: noisy, | |
| borderColor: 'rgba(239, 68, 68, 1)', | |
| backgroundColor: 'rgba(239, 68, 68, 0.1)', | |
| borderWidth: 1, | |
| pointRadius: 0, | |
| fill: false | |
| } | |
| ] | |
| }; | |
| // Common chart options | |
| const commonOptions = { | |
| responsive: true, | |
| maintainAspectRatio: false, | |
| scales: { | |
| x: { | |
| title: { | |
| display: true, | |
| text: `Time (${unitLabel})` | |
| }, | |
| grid: { | |
| display: true | |
| } | |
| }, | |
| y: { | |
| title: { | |
| display: true, | |
| text: 'Concentration (M)' | |
| }, | |
| grid: { | |
| display: true | |
| } | |
| } | |
| }, | |
| plugins: { | |
| legend: { | |
| position: 'top', | |
| }, | |
| tooltip: { | |
| mode: 'index', | |
| intersect: false, | |
| } | |
| }, | |
| interaction: { | |
| mode: 'nearest', | |
| axis: 'x', | |
| intersect: false | |
| } | |
| }; | |
| // Linear scale chart | |
| const linearCtx = document.getElementById('linearChart').getContext('2d'); | |
| if (linearChartInstance) { | |
| linearChartInstance.data = chartData; | |
| linearChartInstance.options.scales.x.title.text = `Time (${unitLabel})`; | |
| linearChartInstance.update(); | |
| } else { | |
| linearChartInstance = new Chart(linearCtx, { | |
| type: 'line', | |
| data: chartData, | |
| options: commonOptions | |
| }); | |
| } | |
| // Log scale chart | |
| const logOptions = JSON.parse(JSON.stringify(commonOptions)); | |
| logOptions.scales.y.type = 'logarithmic'; | |
| logOptions.scales.y.min = Math.min(...ideal.filter(v => v > 0)) * 0.8; | |
| logOptions.scales.y.max = Math.max(...ideal) * 1.2; | |
| const logCtx = document.getElementById('logChart').getContext('2d'); | |
| if (logChartInstance) { | |
| logChartInstance.data = chartData; | |
| logChartInstance.options.scales.x.title.text = `Time (${unitLabel})`; | |
| logChartInstance.update(); | |
| } else { | |
| logChartInstance = new Chart(logCtx, { | |
| type: 'line', | |
| data: chartData, | |
| options: logOptions | |
| }); | |
| } | |
| } | |
| // Helper function to get time unit label | |
| function getTimeUnitLabel(unit) { | |
| switch(unit) { | |
| case 'seconds': return 'sec'; | |
| case 'minutes': return 'min'; | |
| case 'hours': return 'hr'; | |
| default: return ''; | |
| } | |
| } | |
| // Reset to defaults | |
| function resetDefaults() { | |
| initialCellsInput.value = 10000; | |
| growthRateInput.value = 0.15; | |
| bloodVolumeInput.value = 5; | |
| noiseLevelInput.value = 5; | |
| totalTimeValueInput.value = 24; | |
| totalTimeUnitSelect.value = 'hours'; | |
| stepSizeValueInput.value = 1; | |
| stepSizeUnitSelect.value = 'seconds'; | |
| displayUnitSelect.value = 'hours'; | |
| runSimulation(); | |
| } | |
| // Export data as CSV | |
| function exportData() { | |
| if (!window.simulationData) { | |
| alert('No simulation data to export'); | |
| return; | |
| } | |
| const data = window.simulationData; | |
| const headers = [ | |
| 'Time (seconds)', | |
| 'Time (hours)', | |
| 'Ideal Cell Count', | |
| 'Noisy Cell Count', | |
| 'Ideal Concentration (M)', | |
| 'Noisy Concentration (M)' | |
| ]; | |
| let csvContent = headers.join(',') + '\n'; | |
| for (let i = 0; i < data.timeSeconds.length; i++) { | |
| const row = [ | |
| data.timeSeconds[i], | |
| data.timeHours[i], | |
| data.idealCounts[i], | |
| data.noisyCounts[i], | |
| data.molarIdeal[i], | |
| data.molarNoisy[i] | |
| ]; | |
| csvContent += row.join(',') + '\n'; | |
| } | |
| const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); | |
| const url = URL.createObjectURL(blob); | |
| const link = document.createElement('a'); | |
| link.setAttribute('href', url); | |
| link.setAttribute('download', 'tcell_growth_simulation.csv'); | |
| link.style.visibility = 'hidden'; | |
| document.body.appendChild(link); | |
| link.click(); | |
| document.body.removeChild(link); | |
| } | |
| // Apply sample interval (5 minutes) to charts | |
| function applySampleInterval() { | |
| if (!window.simulationData) { | |
| alert('Run a simulation first'); | |
| return; | |
| } | |
| const data = window.simulationData; | |
| const sampleInterval = 300; // 5 minutes in seconds | |
| const stepSize = convertToSeconds( | |
| parseFloat(stepSizeValueInput.value), | |
| stepSizeUnitSelect.value | |
| ); | |
| const intervalInSteps = Math.round(sampleInterval / stepSize); | |
| if (intervalInSteps < 1) { | |
| alert('Step size is too large for 5-minute sampling'); | |
| return; | |
| } | |
| const sampledIndices = []; | |
| for (let i = 0; i < data.timeSeconds.length; i += intervalInSteps) { | |
| sampledIndices.push(i); | |
| } | |
| // If last point isn't included, add it | |
| if (sampledIndices[sampledIndices.length - 1] !== data.timeSeconds.length - 1) { | |
| sampledIndices.push(data.timeSeconds.length - 1); | |
| } | |
| // Create sampled data | |
| const sampledTime = sampledIndices.map(i => convertTimeUnit([data.timeSeconds[i]], data.displayUnit)[0]); | |
| const sampledIdeal = sampledIndices.map(i => data.molarIdeal[i]); | |
| const sampledNoisy = sampledIndices.map(i => data.molarNoisy[i]); | |
| // Update charts | |
| updateCharts(sampledTime, sampledIdeal, sampledNoisy, data.displayUnit); | |
| // Update data points display | |
| dataPointsDisplay.textContent = sampledIndices.length.toLocaleString(); | |
| } | |
| // Simple seedable random number generator | |
| // From https://github.com/davidbau/seedrandom | |
| Math.seedrandom = function(seed) { | |
| let value = 0; | |
| for (let i = 0; i < seed.length; i++) { | |
| value = value * 32 + seed.charCodeAt(i); | |
| } | |
| return function() { | |
| value = (value * 1664525 + 1013904223) % 4294967296; | |
| return value / 4294967296; | |
| }; | |
| }; | |
| // Add normal distribution to Math.seedrandom | |
| Math.seedrandom.normal = function(random) { | |
| let u = 0, v = 0; | |
| while(u === 0) u = random(); | |
| while(v === 0) v = random(); | |
| return Math.sqrt(-2.0 * Math.log(u)) * Math.cos(2.0 * Math.PI * v); | |
| }; | |
| </script> | |
| <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=STE-Nexus/t-cell-data-visualization-app" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> | |
| </html> |