Packing water for a camping trip is a small estimation problem that most people solve by guessing. This post walks through a tiny planner that turns the guess into arithmetic you can see and change: no framework, no build step, about 60 lines of JavaScript.
The model
Water use on a car-camping trip splits into four line items, and each one scales differently:
- Drinking: people × days × weather
- Cooking: people × days × hot meals
- Dishes: days × hot meals (the whole group shares the bins)
- Hand washing: people × days
That difference is the whole point of the tool. Drinking water grows with head count, but dish water mostly grows with the number of meals you cook. For a short trip with a small group, dishes can be the largest line, which is not what people expect.
Fuel is simpler: burner minutes per hot meal, times meals, divided by how long one canister lasts on your stove.
Inputs are assumptions, not constants
Every coefficient is an input field with a default:
<label>Drinking water, L per person per day <input id="drink" type="number" step="0.1" value="3"> </label> The defaults are rules of thumb. Hard-coding them would make the tool look more authoritative than it is, so they live in a collapsible <details> block that says "edit me". Native <details> gives you the disclosure widget, keyboard support and accessibility for free.
One function, no state
There is no state object and no framework. The DOM is the state: every recalculation reads the inputs again.
const $ = id => document.getElementById(id); const num = id => Math.max(0, parseFloat($(id).value) || 0); function calc() { const people = num('people'), days = num('days'), meals = num('meals'); const weather = parseFloat($('weather').value); const margin = 1 + num('margin') / 100; const parts = [ ['Drinking', people * days * num('drink') * weather], ['Cooking', people * days * meals * num('cook')], ['Dishes', days * meals * num('dish')], ['Hand washing', people * days * num('hands')], ].map(([k, v]) => [k, v * margin]); const total = parts.reduce((s, p) => s + p[1], 0); // ...render } document.querySelectorAll('input,select') .forEach(el => el.addEventListener('input', calc)); calc(); Three small decisions are worth pointing out:
-
num()never returnsNaN. An empty field while the user is typing becomes0instead of poisoning every result withNaN. -
input, notchange. Results update on every keystroke and on every click of the number spinner. - The line items are data.
partsis an array of[label, liters]pairs, and the table and the bar chart are both rendered from it. Adding "coffee" or "dog" is one more row.
A bar chart without a chart library
The breakdown bar is a flex container with one child per line item, each sized by its share:
$('bar').innerHTML = parts.map(([k, v], i) => `<i title="${k}" style="width:${v / total * 100}%;background:${COLORS[i]}"></i>` ).join(''); For four segments that is all a stacked bar needs. The table next to it carries the exact numbers, so the chart does not have to be read precisely.
Units and the tank field
Output is in liters and US gallons, because water jugs in the US are sold by the gallon. The last tile divides the total by a tank size. It is there for camp kitchens with a built-in water tank: for example, the VOZ Camp Kitchen box has a 2-gallon tank feeding its faucet, and "how many refills is that" is a more useful answer than a raw volume.
Styling notes
-
font-variant-numeric: tabular-numskeeps the big numbers from jittering as digits change. -
grid-template-columns: repeat(auto-fit, minmax(210px, 1fr))makes the form responsive without a media query. - Colors are custom properties with a
prefers-color-scheme: darkoverride, so the pen follows the system theme.
What to add next
- Save the assumptions in
localStorage, so your own stove's numbers survive a reload. - A share link that encodes the inputs in the URL hash.
- A print stylesheet that turns the result into a packing slip.
The pen is public: fork it and change the line items to match how you actually camp.