:root {
--green: #006845;
--green-dark: #004d33;
--green-light: #e8f2ee;
--green-mid: #b5d4c8;
--text: #222;
--muted: #555;
--border: #ccc;
--radius: 6px;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: Arial, sans-serif; color: var(--text); font-size: 16px; background: #fff; }
.gsr-estimator { max-width: 800px; margin: 0 auto; padding: 28px 20px 60px; }
.gsr-tool-title { color: var(--green); font-size: 26px; font-weight: 700; margin-bottom: 4px; }
.gsr-subtitle { color: var(--muted); font-size: 14px; margin-bottom: 24px; }
/* Progress */
.gsr-progress-wrap { margin-bottom: 28px; }
.gsr-progress-bar { display: flex; gap: 5px; }
.gsr-progress-step { flex: 1; height: 7px; background: #ddd; border-radius: 4px; transition: background .25s; }
.gsr-progress-step.active { background: var(--green); }
.gsr-progress-step.done { background: var(--green-dark); }
.gsr-progress-label { font-size: 12px; color: var(--muted); margin-top: 5px; }
/* Step headings */
.gsr-step-title { font-size: 21px; font-weight: 700; color: var(--green); margin-bottom: 6px; }
.gsr-step-desc { color: var(--muted); font-size: 14px; margin-bottom: 20px; }
/* Cards */
.gsr-cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); gap: 14px; margin-bottom: 26px; }
.gsr-card { border: 2px solid var(--border); border-radius: var(--radius); padding: 18px 14px; cursor: pointer; transition: border-color .2s, background .2s; text-align: center; }
.gsr-card:hover { border-color: var(--green); background: var(--green-light); }
.gsr-card.selected { border-color: var(--green); background: var(--green-light); }
.gsr-card .card-icon { font-size: 26px; margin-bottom: 8px; }
.gsr-card .card-label { font-weight: 700; font-size: 15px; color: var(--green); margin-bottom: 4px; }
.gsr-card .card-desc { font-size: 12px; color: var(--muted); line-height: 1.4; }
/* Form fields */
.gsr-field { margin-bottom: 20px; }
.gsr-field label { display: block; font-weight: 600; font-size: 14px; margin-bottom: 6px; }
.gsr-input-num { width: 130px; padding: 9px 10px; font-size: 15px; border: 2px solid var(--border); border-radius: var(--radius); }
.gsr-input-num:focus { outline: none; border-color: var(--green); }
.field-note { font-size: 12px; color: var(--muted); margin-top: 5px; }
/* Checkboxes */
.gsr-check-group { display: flex; flex-direction: column; gap: 12px; }
.gsr-check-item { display: flex; align-items: flex-start; gap: 10px; cursor: pointer; }
.gsr-check-item input[type="checkbox"] { width: 17px; height: 17px; margin-top: 2px; accent-color: var(--green); cursor: pointer; flex-shrink: 0; }
.gsr-check-item .cb-text { flex: 1; }
.gsr-check-item .cb-label { font-size: 14px; font-weight: 600; }
.gsr-check-item .cb-price { font-size: 12px; color: var(--muted); }
.gsr-sub-check { padding-left: 27px; }
/* Reads box */
.gsr-reads-box { background: var(--green-light); border: 1px solid var(--green-mid); border-radius: var(--radius); padding: 18px; margin-bottom: 22px; }
.gsr-reads-box .reads-header { font-weight: 700; font-size: 14px; margin-bottom: 10px; color: var(--green-dark); }
.gsr-reads-row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 10px; }
.gsr-reads-row input { width: 110px; padding: 8px 10px; border: 2px solid var(--border); border-radius: var(--radius); font-size: 15px; }
.gsr-reads-row input:focus { outline: none; border-color: var(--green); }
.gsr-reads-total { font-size: 14px; color: var(--text); }
.gsr-rec-note { font-size: 12px; color: var(--green-dark); margin-top: 8px; font-style: italic; }
/* Run table */
.gsr-run-options { margin-bottom: 22px; }
.gsr-run-options h4 { font-size: 15px; font-weight: 700; color: var(--green); margin-bottom: 10px; }
.gsr-run-scroll { overflow-x: auto; }
table.gsr-run-table { width: 100%; border-collapse: collapse; font-size: 13.5px; white-space: nowrap; }
table.gsr-run-table th { background: var(--green); color: #fff; padding: 9px 11px; text-align: left; }
table.gsr-run-table td { padding: 9px 11px; border-bottom: 1px solid #e5e5e5; vertical-align: middle; }
table.gsr-run-table tr:last-child td { border-bottom: none; }
table.gsr-run-table tr.rec-row td { background: var(--green-light); }
table.gsr-run-table td.run-name { font-weight: 500; }
.badge { display: inline-block; font-size: 10px; font-weight: 700; padding: 2px 6px; border-radius: 10px; margin-left: 5px; vertical-align: middle; }
.badge-best { background: var(--green); color: #fff; }
/* Summary */
.gsr-summary-box { background: #fafafa; border: 1px solid #ddd; border-radius: var(--radius); overflow: hidden; margin-bottom: 18px; }
table.gsr-summary-table { width: 100%; border-collapse: collapse; font-size: 15px; }
table.gsr-summary-table th { background: var(--green); color: #fff; padding: 11px 16px; text-align: left; }
table.gsr-summary-table th.right { text-align: right; }
table.gsr-summary-table td { padding: 10px 16px; border-bottom: 1px solid #e5e5e5; }
table.gsr-summary-table td.right { text-align: right; }
table.gsr-summary-table td.note { font-size: 12px; color: var(--muted); font-style: italic; padding-top: 2px; padding-bottom: 8px; border-bottom: 1px solid #e5e5e5; }
table.gsr-summary-table tr.total-row td { font-weight: 700; font-size: 16px; border-top: 2px solid var(--green); background: var(--green-light); border-bottom: none; }
.gsr-info-box { background: var(--green-light); border: 1px solid var(--green-mid); border-radius: var(--radius); padding: 14px 16px; margin-bottom: 18px; font-size: 13px; color: var(--green-dark); line-height: 1.5; }
.gsr-disclaimer { font-size: 12px; color: #777; font-style: italic; margin-bottom: 22px; line-height: 1.5; }
.gsr-disclaimer a { color: var(--green); }
/* Buttons */
.gsr-btn-row { display: flex; gap: 12px; flex-wrap: wrap; align-items: center; }
.gsr-btn { padding: 11px 28px; border: none; border-radius: var(--radius); font-size: 15px; font-weight: 700; cursor: pointer; }
.gsr-btn-primary { background: var(--green); color: #fff; }
.gsr-btn-primary:hover { background: var(--green-dark); }
.gsr-btn-primary:disabled { background: #aaa; cursor: not-allowed; }
.gsr-btn-secondary { background: #fff; color: var(--green); border: 2px solid var(--green); }
.gsr-btn-secondary:hover { background: var(--green-light); }
.gsr-btn-ghost { background: transparent; color: #666; border: 2px solid #ccc; }
.gsr-btn-ghost:hover { background: #f2f2f2; }
hr.gsr-divider { border: none; border-top: 1px solid #e0e0e0; margin: 22px 0; }
@media (max-width: 520px) {
.gsr-cards { grid-template-columns: 1fr; }
.gsr-btn-row { flex-direction: column; }
.gsr-btn { width: 100%; text-align: center; }
}
@media print {
.gsr-btn-row, .gsr-progress-wrap { display: none; }
}
GSR Cost Estimator
Build an estimated cost for your genomics experiment. Prices reflect FY27 internal (Dartmouth) rates.
// ─── PRICING DATA ──────────────────────────────────────────────────────────
var P = {
extraction: 13.84,
extractionFFPE: 10.60,
laborRate: 128.65, // $/hour
tissueCellPrep: 138.75,
sampleFixation: 155.32,
flexTissuePrepFixation: 155.00,
qubit: 2.47,
fragAnalyzer: 8.68,
// recCycles: recommended sequencing kit cycle length for this assay
bulk: {
ribo: { name: 'RNA-seq: Ribo(-)', price: 93.05, defaultReads: 40, recCycles: 100 },
e3end: { name: "RNA-seq: 3'-End", price: 48.73, defaultReads: 10, recCycles: 100 },
dna: { name: 'DNA-seq', price: 56.87, defaultReads: 30, recCycles: 300 }
},
// readsM: maximum reads output per run; cycles: kit cycle length
runs: [
{ id:'partial_10m', name:'Partial run — 10M reads', price:53.27, readsM:10, cycles:100 },
{ id:'partial_50m', name:'Partial run — 50M reads', price:151.74, readsM:50, cycles:100 },
{ id:'partial_250m', name:'Partial run — 250M reads', price:802.77, readsM:250, cycles:100 },
{ id:'p1_100', name:'NextSeq P1 100-cycle', price:1179.18, readsM:120, cycles:100 },
{ id:'p1_300', name:'NextSeq P1 300-cycle', price:1413.68, readsM:120, cycles:300 },
{ id:'p2_100', name:'NextSeq P2 100-cycle', price:1505.37, readsM:500, cycles:100 },
{ id:'p2_300', name:'NextSeq P2 300-cycle', price:1797.58, readsM:500, cycles:300 },
{ id:'p2_600', name:'NextSeq P2 600-cycle', price:3531.43, readsM:500, cycles:600 },
{ id:'p3_100', name:'NextSeq P3 100-cycle', price:2103.00, readsM:1300, cycles:100 },
{ id:'p3_300', name:'NextSeq P3 300-cycle', price:3111.57, readsM:1300, cycles:300 },
{ id:'p4_100', name:'NextSeq P4 100-cycle', price:2807.93, readsM:2000, cycles:100 },
{ id:'p4_300', name:'NextSeq P4 300-cycle', price:4283.03, readsM:2000, cycles:300 },
{ id:'s2_300', name:'NovaSeq S2 300-cycle', price:7919.60, readsM:3300, cycles:300 },
{ id:'s4_300', name:'NovaSeq S4 300-cycle', price:11740.69, readsM:8500, cycles:300 }
],
sc: {
rna35: { name: "10x 3'/5' RNA-seq", price: 1668.50 },
atac: { name: '10x ATAC-seq', price: 1986.00 },
multiome: { name: '10x Multiome (RNA + ATAC)', price: 3107.25 },
flex: { name: '10x Flex', runPrice: 422.97, samplePrice: 307.51 },
hive: { name: 'Honeycomb HIVE', price: 837.77 }
},
spatial: {
visiumhd: { name: 'Visium HD', price: 12887.70 },
xenium: { name: 'Xenium (Standard panel)', samplePrepPrice: 695.00, runPrice: 2520.00 },
xenium5k: { name: 'Xenium (5K panel)', samplePrepPrice: 695.00, runPrice: 7510.00 }
}
};
// ─── LABOR ─────────────────────────────────────────────────────────────────
// Returns { hours, batches, batchSize, hrsPerBatch, total }
function calcLabor(type, n) {
var rate = P.laborRate, hrsPerBatch, batchSize;
switch (type) {
case 'dna': hrsPerBatch = 6; batchSize = 48; break;
case 'rna': hrsPerBatch = 8; batchSize = 48; break;
case 'sc': hrsPerBatch = 8; batchSize = 8; break;
case 'spatial': hrsPerBatch = 16; batchSize = 1; break; // Visium HD: per slide
case 'xenium': hrsPerBatch = 16; batchSize = 2; break; // Xenium: per 2-slide run
}
var batches = Math.ceil(n / batchSize);
var hours = batches * hrsPerBatch;
return { hours: hours, batches: batches, batchSize: batchSize, hrsPerBatch: hrsPerBatch, total: hours * rate };
}
// ─── STATE ─────────────────────────────────────────────────────────────────
var S = freshState();
function freshState() {
return {
category: null,
bulkAssay: null, bulkSamples: 1, bulkReadsPerSample: null,
bulkExtraction: false, bulkFFPE: false, bulkQC: false, bulkRun: null,
scPlatform: null, scSamples: 1, scFixation: false, scTissuePrep: false,
scCellsPerSample: 5000, scReadsPerCell: 25000, scRun: null,
spatialPlatform: null, spatialSlides: 1, spatialTissuePrep: false
};
}
// ─── STEP SEQUENCES ────────────────────────────────────────────────────────
var PATHS = {
bulk: ['start','bulk_assay','bulk_params','bulk_reads','bulk_summary'],
sc: ['start','sc_platform','sc_params','sc_summary'],
spatial: ['start','spatial_platform','spatial_params','spatial_summary']
};
var STEP_LABELS = {
start:'Category', bulk_assay:'Assay', bulk_params:'Parameters',
bulk_reads:'Sequencing', bulk_summary:'Estimate',
sc_platform:'Platform', sc_params:'Parameters', sc_summary:'Estimate',
spatial_platform:'Platform', spatial_params:'Parameters', spatial_summary:'Estimate'
};
var currentStep = 'start';
function path() { return S.category ? PATHS[S.category] : ['start']; }
function stepIdx() { return path().indexOf(currentStep); }
function goTo(id) { currentStep = id; render(); }
function goBack() { var p = path(), i = p.indexOf(currentStep); if (i > 0) { currentStep = p[i-1]; render(); } }
// ─── RENDER ROUTER ─────────────────────────────────────────────────────────
function render() {
renderProgress();
var html = '';
switch (currentStep) {
case 'start': html = renderStart(); break;
case 'bulk_assay': html = renderBulkAssay(); break;
case 'bulk_params': html = renderBulkParams(); break;
case 'bulk_reads': html = renderBulkReads(); break;
case 'bulk_summary': html = renderBulkSummary(); break;
case 'sc_platform': html = renderSCPlatform(); break;
case 'sc_params': html = renderSCParams(); break;
case 'sc_summary': html = renderSCSummary(); break;
case 'spatial_platform': html = renderSpatialPlat(); break;
case 'spatial_params': html = renderSpatialParams(); break;
case 'spatial_summary': html = renderSpatialSummary(); break;
}
document.getElementById('wizardContent').innerHTML = html;
bindEvents();
}
function renderProgress() {
var p = path(), idx = p.indexOf(currentStep);
document.getElementById('progressBar').innerHTML = p.map(function(s, i) {
var cls = 'gsr-progress-step' + (i < idx ? ' done' : i === idx ? ' active' : '');
return '
';
}).join('');
document.getElementById('progressLabel').textContent =
'Step ' + (idx+1) + ' of ' + p.length + ' — ' + (STEP_LABELS[currentStep] || '');
}
// ─── STEP: START ───────────────────────────────────────────────────────────
function renderStart() {
var cats = [
['bulk', '🧬', 'Bulk DNA/RNA Sequencing', "RNA-seq (Ribo-, 3'-End) or DNA-seq"],
['sc', '🔬', 'Single Cell Genomics', '10x Chromium: RNA-seq, ATAC, Multiome, Flex'],
['spatial', '🗺️', 'Spatial Genomics', 'Visium HD or Xenium in situ']
];
return '
' +
'
Select a category to get started.
' +
'
cats.map(function(c) {
return '
'
' +
'
' +
'
';
}).join('') + '
' +
'
';
}
// ─── STEP: BULK ASSAY ──────────────────────────────────────────────────────
function renderBulkAssay() {
return '
' +
'
Choose the library preparation type for your samples.
' +
'
Object.entries(P.bulk).map(function(e) {
var k = e[0], a = e[1];
return '
'
' +
'
a.defaultReads + 'M reads/sample recommended
' +
a.recCycles + '-cycle sequencing kit recommended
';
}).join('') + '
' +
navBtns('back', 'bulk_params', !S.bulkAssay);
}
// ─── STEP: BULK PARAMS ─────────────────────────────────────────────────────
function renderBulkParams() {
return '
' +
'
Enter your sample count and select add-ons.
' +
'
'' +
'' +
'
' +
'
' +
'
'
'' +
'
'' +
'
' +
'
' +
'
' +
'' +
'
' +
'
' +
navBtns('back', 'bulk_reads');
}
// ─── STEP: BULK READS ──────────────────────────────────────────────────────
function calcRunOptions(totalReadsM, numSamples) {
numSamples = numSamples || S.bulkSamples;
return P.runs.map(function(r) {
var n = Math.ceil(totalReadsM / r.readsM);
var cost = n * r.price;
return { id:r.id, name:r.name, readsM:r.readsM, cycles:r.cycles,
numRuns:n, totalCost:cost, perSample: cost / numSamples };
});
}
function bestRun(options, recCycles) {
// Prefer recommended cycle length; fall back to all options
var pool = options.filter(function(r) { return r.cycles === recCycles; });
if (!pool.length) pool = options;
return pool.reduce(function(b, r) { return r.totalCost < b.totalCost ? r : b; });
}
function buildRunRows(options, recRun, recCycles, radioName, selectedId) {
radioName = radioName || 'runRadio';
selectedId = selectedId !== undefined ? selectedId : S.bulkRun;
return options.map(function(r) {
var isBest = r.id === recRun.id;
var rowCls = isBest ? ' class="rec-row"' : '';
var badges = isBest ? 'Recommended kit' : '';
var checked = selectedId === r.id ? ' checked' : (isBest && !selectedId ? ' checked' : '');
var readsLabel = r.readsM >= 1000 ? (r.readsM/1000).toFixed(1) + 'B' : r.readsM + 'M';
return '
'
' +
'
' +
'
' +
'
' +
'
' +
'
' +
'
';
}).join('');
}
function renderBulkReads() {
var assay = P.bulk[S.bulkAssay];
var rps = S.bulkReadsPerSample !== null ? S.bulkReadsPerSample : assay.defaultReads;
var totalM = S.bulkSamples * rps;
var options = calcRunOptions(totalM);
var rec = bestRun(options, assay.recCycles);
if (!S.bulkRun) S.bulkRun = rec.id;
return '
' +
'
Adjust reads per sample if needed, then select a sequencing run type.
' +
'
'
' +
'
'' +
'million reads per sample' +
'
' +
'
totalM.toLocaleString() + 'M (' + S.bulkSamples + ' samples × ' + rps + 'M)
' +
'
' +
'
' +
'
' +
navBtns('back', 'bulk_summary', false, 'View Estimate →');
}
// ─── STEP: BULK SUMMARY ────────────────────────────────────────────────────
function renderBulkSummary() {
var assay = P.bulk[S.bulkAssay];
var n = S.bulkSamples;
var rps = S.bulkReadsPerSample !== null ? S.bulkReadsPerSample : assay.defaultReads;
var totalM = n * rps;
var run = P.runs.find(function(r) { return r.id === S.bulkRun; });
var numRuns = Math.ceil(totalM / run.readsM);
var laborType = S.bulkAssay === 'dna' ? 'dna' : 'rna';
var labor = calcLabor(laborType, n);
var lines = [];
lines.push({ label:'Library prep — ' + assay.name, qty:n, unit:assay.price, qtyl:'samples' });
if (S.bulkExtraction) {
var ep = S.bulkFFPE ? P.extractionFFPE : P.extraction;
lines.push({ label:'Nucleic acid extraction' + (S.bulkFFPE ? ' (FFPE)' : ''), qty:n, unit:ep, qtyl:'samples' });
}
if (S.bulkQC)
lines.push({ label:'QC — Qubit + Fragment Analyzer', qty:n, unit:P.qubit+P.fragAnalyzer, qtyl:'samples' });
lines.push({ label:'Sequencing — ' + run.name, qty:numRuns, unit:run.price, qtyl:'runs' });
lines.push({
label: 'Labor',
qty: labor.hours, unit: P.laborRate, qtyl: 'hrs',
note: labor.batches + ' batch' + (labor.batches > 1 ? 'es' : '') +
' of up to ' + labor.batchSize + ' samples × ' + labor.hrsPerBatch + ' hrs @ $' + fmt(P.laborRate) + '/hr'
});
if (S.bulkExtraction) {
var exBatches = Math.ceil(n / 24);
var exHours = exBatches * 3;
lines.push({
label: 'Labor — nucleic acid isolation',
qty: exHours, unit: P.laborRate, qtyl: 'hrs',
note: exBatches + ' batch' + (exBatches > 1 ? 'es' : '') +
' of up to 24 samples × 3 hrs @ $' + fmt(P.laborRate) + '/hr'
});
}
return summaryHTML(
'Bulk Sequencing Estimate',
n + ' sample' + (n>1?'s':'') + ' · ' + assay.name + ' · ' + rps + 'M reads/sample',
lines
);
}
// ─── STEP: SC PLATFORM ─────────────────────────────────────────────────────
function renderSCPlatform() {
var plats = [
['rna35', "10x 3'/5' RNA-seq", 'Gene expression profiling'],
['atac', '10x ATAC-seq', 'Chromatin accessibility'],
['multiome', '10x Multiome (RNA + ATAC)', 'Both modalities from the same cells'],
['flex', '10x Flex', 'Fixed/FFPE samples; base run + per-sample cost'],
['hive', 'Honeycomb HIVE', 'Single-cell RNA-seq alternative']
];
return '
' +
'
Choose your 10x Chromium product or platform.
' +
'
plats.map(function(pl) {
return '
'
' +
'
';
}).join('') + '
' +
navBtns('back', 'sc_params', !S.scPlatform);
}
// ─── STEP: SC PARAMS ───────────────────────────────────────────────────────
function renderSCParams() {
var isFlex = S.scPlatform === 'flex';
var hasSeq = ['flex','rna35','atac','multiome'].indexOf(S.scPlatform) !== -1;
var noFixation = ['flex','atac','multiome','rna35'].indexOf(S.scPlatform) !== -1;
// Add-ons section
var addOns = isFlex
? '
' +
'
' +
''
: (noFixation ? '' :
'
' +
'
' +
'') +
'
' +
'
' +
'';
// Sequencing section (Flex, 3'/5' RNA, ATAC, Multiome)
var scSeq = '';
if (hasSeq) {
var defRpc = S.scPlatform === 'rna35' ? 30000 : 25000;
var cells = S.scCellsPerSample || 5000;
var rpc = S.scReadsPerCell || defRpc;
var totalM = S.scSamples * cells * rpc / 1e6;
var runMult = S.scPlatform === 'multiome' ? 2 : 1;
var options = calcRunOptions(totalM, S.scSamples).map(function(r) {
var nr = r.numRuns * runMult, tc = r.totalCost * runMult;
return { id:r.id, name:r.name, readsM:r.readsM, cycles:r.cycles,
numRuns:nr, totalCost:tc, perSample:tc / S.scSamples };
});
var rec = bestRun(options, 100);
if (!S.scRun) S.scRun = rec.id;
var multiomeNote = S.scPlatform === 'multiome'
? ' × 2 runs (RNA + ATAC libraries)' : '';
scSeq =
'
' +
'
'
' +
'
'' +
'cells per sample' +
'
' +
'
'' +
'reads per cell (recommended: ' + defRpc.toLocaleString() + ')' + multiomeNote + '' +
'
' +
'
Math.round(totalM * runMult).toLocaleString() + 'M' +
' (' + S.scSamples + ' sample' + (S.scSamples > 1 ? 's' : '') + ' × ' +
cells.toLocaleString() + ' cells × ' + rpc.toLocaleString() + ' reads' +
(runMult > 1 ? ' × 2 libraries' : '') + ')
' +
'
' +
'
' +
'
';
}
return '
' +
'
' +
(isFlex ? 'Flex pricing: one base run cost plus a per-sample cost.' : 'Enter the number of samples for your experiment.') +
'
' +
'
'' +
'' +
(isFlex ? '
' + $' + P.sc.flex.samplePrice.toFixed(2) + '/sample
' : '') +
'
' +
'
' +
'
'
' +
'
' +
scSeq +
navBtns('back', 'sc_summary', false, 'View Estimate →');
}
// ─── STEP: SC SUMMARY ──────────────────────────────────────────────────────
function renderSCSummary() {
var n = S.scSamples;
var isFlex = S.scPlatform === 'flex';
var pl = P.sc[S.scPlatform];
var labor = calcLabor('sc', n);
var lines = [];
var hasSeq = ['flex','rna35','atac','multiome'].indexOf(S.scPlatform) !== -1;
if (isFlex) {
lines.push({ label:'10x Flex — Run cost', qty:1, unit:P.sc.flex.runPrice, qtyl:'run' });
lines.push({ label:'10x Flex — Per sample', qty:n, unit:P.sc.flex.samplePrice, qtyl:'samples' });
if (S.scTissuePrep)
lines.push({ label:'Tissue/cell prep and fixation', qty:n, unit:P.flexTissuePrepFixation, qtyl:'samples' });
} else {
lines.push({ label:pl.name, qty:n, unit:pl.price, qtyl:'samples' });
if (S.scFixation) lines.push({ label:'Sample fixation (10x)', qty:n, unit:P.sampleFixation, qtyl:'samples' });
if (S.scTissuePrep) lines.push({ label:'Tissue/cell preparation', qty:n, unit:P.tissueCellPrep, qtyl:'samples' });
}
if (hasSeq && S.scRun) {
var scRunData = P.runs.find(function(r) { return r.id === S.scRun; });
var scCells = S.scCellsPerSample || 5000;
var scRpc = S.scReadsPerCell || (S.scPlatform === 'rna35' ? 30000 : 25000);
var scTotalM = n * scCells * scRpc / 1e6;
var scRunMult = S.scPlatform === 'multiome' ? 2 : 1;
var scNumRuns = Math.ceil(scTotalM / scRunData.readsM) * scRunMult;
lines.push({
label: 'Sequencing — ' + scRunData.name,
qty: scNumRuns, unit: scRunData.price, qtyl: 'runs',
note: S.scPlatform === 'multiome'
? n + ' sample' + (n>1?'s':'') + ' × ' + scCells.toLocaleString() +
' cells × 25,000 reads/cell × 2 libraries (RNA + ATAC)'
: n + ' sample' + (n>1?'s':'') + ' × ' + scCells.toLocaleString() +
' cells × ' + scRpc.toLocaleString() + ' reads/cell'
});
}
lines.push({
label: 'Labor',
qty: labor.hours, unit: P.laborRate, qtyl: 'hrs',
note: labor.batches + ' batch' + (labor.batches > 1 ? 'es' : '') +
' of up to ' + labor.batchSize + ' samples × ' + labor.hrsPerBatch + ' hrs @ $' + fmt(P.laborRate) + '/hr'
});
var seqNote = hasSeq ? '' :
'
'Library sequencing is quoted separately based on your target read depth. ' +
'100-cycle kits are recommended for most single cell workflows. ' +
'Contact the GSR to discuss sequencing options for your project.
';
return summaryHTML(
'Single Cell Estimate',
n + ' sample' + (n>1?'s':'') + ' · ' + (isFlex ? '10x Flex' : pl.name),
lines,
seqNote
);
}
// ─── STEP: SPATIAL PLATFORM ────────────────────────────────────────────────
function renderSpatialPlat() {
return '
' +
'
Choose a spatial genomics platform.
' +
'
[
['visiumhd', 'Visium HD', 'Whole transcriptome · FFPE tissue · 2µm bin resolution'],
['xenium', 'Xenium (Standard panel)','In situ sequencing · targeted gene panels'],
['xenium5k', 'Xenium (5K panel)', 'In situ sequencing · up to 5,000 genes']
].map(function(pl) {
return '
'
' +
'
';
}).join('') + '
' +
navBtns('back', 'spatial_params', !S.spatialPlatform);
}
// ─── STEP: SPATIAL PARAMS ──────────────────────────────────────────────────
function renderSpatialParams() {
var isVisium = S.spatialPlatform === 'visiumhd';
var isXenium = !isVisium;
var addOns = isXenium
? '
' +
'
'
'' +
'
' +
'
'
: '';
return '
' +
'
' + (isVisium
? 'Each Visium HD slide cost covers 4 capture areas.'
: 'Pricing is per slide. Note: each Xenium run requires 2 slides — contact us if you would like to share a run with another project.') + '
' +
'
'' +
'' +
'
' +
addOns +
navBtns('back', 'spatial_summary', false, 'View Estimate →');
}
// ─── STEP: SPATIAL SUMMARY ─────────────────────────────────────────────────
function renderSpatialSummary() {
var n = S.spatialSlides;
var isVisium = S.spatialPlatform === 'visiumhd';
var pl = P.spatial[S.spatialPlatform];
var labor = calcLabor(isVisium ? 'spatial' : 'xenium', n);
var lines = [];
var p3_100 = P.runs.find(function(r) { return r.id === 'p3_100'; });
if (isVisium) {
lines.push({ label:'Visium HD — per slide (4 capture areas)', qty:n, unit:pl.price, qtyl:'slides' });
lines.push({
label: 'Sequencing — NextSeq P3 100-cycle',
qty: n, unit: p3_100.price, qtyl: 'runs',
note: 'Assumes 100% tissue coverage · 250M reads/capture area (1,000M reads/slide)'
});
} else {
lines.push({ label:pl.name + ' — run cost', qty:n, unit:pl.runPrice, qtyl:'slides' });
if (S.spatialTissuePrep)
lines.push({ label:'Xenium slide prep', qty:n, unit:pl.samplePrepPrice, qtyl:'slides' });
}
lines.push({
label: 'Labor',
qty: labor.hours, unit: P.laborRate, qtyl: 'hrs',
note: n + ' slide' + (n>1?'s':'') + ' × ' + labor.hrsPerBatch + ' hrs/slide @ $' + fmt(P.laborRate) + '/hr'
});
var seqNote = '';
return summaryHTML(
'Spatial Genomics Estimate',
n + ' slide' + (n>1?'s':'') + ' · ' + pl.name,
lines,
seqNote
);
}
// ─── SHARED SUMMARY ────────────────────────────────────────────────────────
function summaryHTML(title, subtitle, lines, extraNote) {
var total = 0;
var rows = lines.map(function(l) {
var cost = l.qty * l.unit;
total += cost;
var mainRow = '
'
' +
'
' +
'
' +
'
' +
'
';
var noteRow = l.note
? '
'
: '';
return mainRow + noteRow;
}).join('');
return '
' +
'
' + subtitle + '
' +
(extraNote || '') +
'
'
| Line item | Quantity | Unit price | Subtotal |
|---|---|---|---|
| Estimated Total | $' + fmt(total) + ' | ||
' +
'
' +
'
This is an estimate only, based on FY27 internal (Dartmouth) rates. ' +
'Actual costs may vary depending on experimental complexity and final scope. ' +
'Contact the GSR to discuss your project and receive a formal quote.
' +
'
'' +
'' +
'
';
}
// ─── HELPERS ───────────────────────────────────────────────────────────────
function fmt(n) { return n.toLocaleString('en-US', {minimumFractionDigits:2, maximumFractionDigits:2}); }
function navBtns(back, next, nextDisabled, nextLabel) {
var bb = back === 'back'
? '' : '';
var nb = next
? '' : '';
return '
';
}
function saveInputs() {
var v;
v = document.getElementById('bulkSamples'); if (v) S.bulkSamples = Math.max(1, parseInt(v.value)||1);
v = document.getElementById('bulkExtraction'); if (v) S.bulkExtraction = v.checked;
v = document.getElementById('bulkFFPE'); if (v) S.bulkFFPE = v.checked;
v = document.getElementById('bulkQC'); if (v) S.bulkQC = v.checked;
v = document.getElementById('readsInput'); if (v) S.bulkReadsPerSample = Math.max(1, parseInt(v.value)||1);
v = document.querySelector('input[name="runRadio"]:checked'); if (v) S.bulkRun = v.value;
v = document.getElementById('scSamples'); if (v) S.scSamples = Math.max(1, parseInt(v.value)||1);
v = document.getElementById('scFixation'); if (v) S.scFixation = v.checked;
v = document.getElementById('scTissuePrep'); if (v) S.scTissuePrep = v.checked;
v = document.getElementById('scCellsInput'); if (v) S.scCellsPerSample = Math.max(1, parseInt(v.value)||5000);
v = document.getElementById('scReadsInput'); if (v) S.scReadsPerCell = Math.max(1, parseInt(v.value)||25000);
v = document.querySelector('input[name="scRunRadio"]:checked'); if (v) S.scRun = v.value;
v = document.getElementById('spatialSlides'); if (v) S.spatialSlides = Math.max(1, parseInt(v.value)||1);
v = document.getElementById('spatialTissuePrep');if (v) S.spatialTissuePrep = v.checked;
}
// ─── EVENT BINDING ─────────────────────────────────────────────────────────
function bindEvents() {
document.querySelectorAll('[data-action]').forEach(function(el) {
el.addEventListener('click', handleClick);
});
var bulkEx = document.getElementById('bulkExtraction');
if (bulkEx) {
bulkEx.addEventListener('change', function() {
S.bulkExtraction = this.checked;
var w = document.getElementById('ffpeWrap');
if (w) w.style.display = this.checked ? '' : 'none';
});
}
var ri = document.getElementById('readsInput');
if (ri) {
ri.addEventListener('input', function() {
var rps = Math.max(1, parseInt(this.value)||1);
S.bulkReadsPerSample = rps;
var totalM = S.bulkSamples * rps;
var disp = document.getElementById('totalDisplay');
if (disp) disp.textContent = totalM.toLocaleString() + 'M';
var tbody = document.getElementById('runTableBody');
if (tbody) {
var assay = P.bulk[S.bulkAssay];
var options = calcRunOptions(totalM);
var rec = bestRun(options, assay.recCycles);
tbody.innerHTML = buildRunRows(options, rec, assay.recCycles);
document.querySelectorAll('input[name="runRadio"]').forEach(function(r) {
r.addEventListener('change', function() { S.bulkRun = this.value; });
});
}
});
}
document.querySelectorAll('input[name="runRadio"]').forEach(function(r) {
r.addEventListener('change', function() { S.bulkRun = this.value; });
});
function updateSCTotal() {
var cells = S.scCellsPerSample || 5000;
var rpc = S.scReadsPerCell || (S.scPlatform === 'rna35' ? 30000 : 25000);
var totalM = S.scSamples * cells * rpc / 1e6;
var runMult = S.scPlatform === 'multiome' ? 2 : 1;
var disp = document.getElementById('scTotalDisplay');
if (disp) disp.textContent = Math.round(totalM * runMult).toLocaleString() + 'M';
var tbody = document.getElementById('scRunTableBody');
if (tbody) {
var options = calcRunOptions(totalM, S.scSamples).map(function(r) {
var nr = r.numRuns * runMult, tc = r.totalCost * runMult;
return { id:r.id, name:r.name, readsM:r.readsM, cycles:r.cycles,
numRuns:nr, totalCost:tc, perSample:tc / S.scSamples };
});
var rec = bestRun(options, 100);
tbody.innerHTML = buildRunRows(options, rec, 100, 'scRunRadio', S.scRun);
document.querySelectorAll('input[name="scRunRadio"]').forEach(function(r) {
r.addEventListener('change', function() { S.scRun = this.value; });
});
}
}
var flexCi = document.getElementById('scCellsInput');
if (flexCi) flexCi.addEventListener('input', function() {
S.scCellsPerSample = Math.max(1, parseInt(this.value)||5000);
updateSCTotal();
});
var flexRi = document.getElementById('scReadsInput');
if (flexRi) flexRi.addEventListener('input', function() {
S.scReadsPerCell = Math.max(1, parseInt(this.value)||25000);
updateSCTotal();
});
document.querySelectorAll('input[name="scRunRadio"]').forEach(function(r) {
r.addEventListener('change', function() { S.scRun = this.value; });
});
}
function handleClick(e) {
var action = this.dataset.action, val = this.dataset.val;
switch (action) {
case 'selectCat':
S.category = val; S.bulkAssay = null; S.scPlatform = null; S.spatialPlatform = null;
render(); break;
case 'nextFromStart':
if (!S.category) return;
goTo(S.category === 'bulk' ? 'bulk_assay' : S.category === 'sc' ? 'sc_platform' : 'spatial_platform');
break;
case 'selectBulkAssay':
S.bulkAssay = val; S.bulkReadsPerSample = null; S.bulkRun = null; render(); break;
case 'selectSCPlatform':
S.scPlatform = val;
S.scReadsPerCell = val === 'rna35' ? 30000 : 25000;
S.scRun = null;
render(); break;
case 'selectSpatialPlatform':
S.spatialPlatform = val; render(); break;
case 'goTo': saveInputs(); goTo(val); break;
case 'back': saveInputs(); goBack(); break;
case 'startOver': S = freshState(); currentStep = 'start'; render(); break;
}
}
render();