class Crop { constructor(name, {fallPlanting, interval, maturity, multisow, spacing, springPlant, springStart, trellis} = {interval: 0, maturity: 0, multisow: 1, spacing: 1, trellis: false}) { this.fallPlanting = fallPlanting; // 'sow', 'transplant', or nothing this.interval = interval; // succession planting interval (weeks) this.maturity = maturity; // average time to maturity from transplant/seed (weeks) this.multisow = multisow; // seeds per module this.name = name; this.spacing = spacing; // per square foot this.springPlant = springPlant; // direct sow/transplant (week offset from last frost) this.springStart = springStart; // start indoors (week offset from last frost) this.trellis = trellis; // needs a trellis? } isSpringTransplant() { return Number.isInteger(this.springStart); } } const crops = [ new Crop("Arugula", {interval: 2, maturity: 6, spacing: 4, springPlant: -4}), new Crop("Basil", {maturity: 6, springStart: -6, springPlant: 2}), new Crop("Bean (Bush)", {interval: 2, maturity: 8, spacing: 9, springPlant: 1}), new Crop("Bean (Pole)", {maturity: 9, spacing: 8, springPlant: 1, trellis: true}), new Crop("Beet", {interval: 2, maturity: 8, multisow: 4, spacing: 4, springPlant: -3}), new Crop("Bok Choy", {fallPlanting: 'sow', maturity: 7, spacing: 4, springStart: -6, springPlant: -2}), new Crop("Broccoli", {fallPlanting: 'transplant', maturity: 12, springStart: -7, springPlant: -3}), new Crop("Brussels Sprouts", {fallPlanting: 'transplant', maturity: 16}), new Crop("Cabbage (Early)", {maturity: 6, springStart: -7, springPlant: -3}), new Crop("Cabbage (Late)", {fallPlanting: 'transplant', maturity: 14, springStart: -7, springPlant: -3}), new Crop("Cabbage (Asian)", {fallPlanting: 'transplant', maturity: 10, springStart: -7, springPlant: -3}), new Crop("Cauliflower", {fallPlanting: 'transplant', maturity: 10, springStart: -7, springPlant: -3}), new Crop("Carrot", {interval: 2, maturity: 10, spacing: 16, springPlant: -4}), new Crop("Cilantro", {interval: 2, maturity: 7, springPlant: 0}), new Crop("Claytonia", {fallPlanting: 'sow', maturity: 6, spacing: 9, springPlant: -5}), new Crop("Cucumber (Vine)", {maturity: 9, spacing: 2, springPlant: 1, trellis: true}), new Crop("Dill", {interval: 4, maturity: 8, springPlant: 0}), new Crop("Eggplant", {maturity: 11, springStart: -5, springPlant: 3}), new Crop("Endive", {maturity: 4, springStart: -9, springPlant: -3}), new Crop("Fennel", {fallPlanting: 'sow', maturity: 10, springPlant: 0}), new Crop("Kale", {maturity: 4, springStart: -10, springPlant: -4}), new Crop("Lettuce (Leaf/Mini Head)", {interval: 2, maturity: 7, spacing: 4, springStart: -8, springPlant: -4}), new Crop("Mache", {interval: 2, maturity: 7, spacing: 16, springPlant: -4}), new Crop("Mustard (Asian Greens)", {interval: 2, maturity: 6, spacing: 4, springStart: -7, springPlant: -4}), new Crop("Onion", {maturity: 8, multisow: 3, spacing: 4, springStart: -10, springPlant: -4}), new Crop("Parsley", {maturity: 7, spacing: 4, springStart: -10, springPlant: -4}), new Crop("Parsnip", {maturity: 14, spacing: 4, springPlant: -2}), new Crop("Pea", {fallPlanting: 'sow', maturity: 12, spacing: 8, springPlant: -4, trellis: true}), new Crop("Pepper", {maturity: 10, springStart: -5, springPlant: 3}), new Crop("Potato", {maturity: 12, spacing: 4, springPlant: -3}), new Crop("Radish (Round)", {interval: 2, maturity: 4, multisow: 4, spacing: 4, springPlant: -3}), new Crop("Radish (Daikon)", {fallPlanting: 'sow', maturity: 9, spacing: 4}), new Crop("Shallot", {maturity: 9, multisow: 3, spacing: 4, springStart: -10, springPlant: -4}), new Crop("Spinach", {interval: 2, maturity: 6, multisow: 4, spacing: 4, springPlant: -5}), new Crop("Squash (Summer)", {maturity: 8, spacing: 1.0 / 9, springPlant: 0}), new Crop("Squash (Winter)", {maturity: 12, spacing: 0.5, springPlant: 1}), new Crop("Swiss Chard", {maturity: 4, spacing: 4, springStart: -8, springPlant: -3}), new Crop("Tomato (Indeterminate)", {maturity: 11, springStart: -4, springPlant: 3, trellis: true}), new Crop("Turnip (Salad)", {interval: 2, maturity: 5, spacing: 16, springPlant: -3}) ]; function daysLater(date, n) { return new Date(date.getTime() + n * 24 * 60 * 60 * 1000); } function weeksLater(date, n) { return daysLater(date, n * 7); } function monthAndDay(date) { return (date.getUTCMonth() + 1) + "/" + date.getUTCDate(); } function range(start, end) { const size = end - start; const array = new Array(size); for(let i = start; i < end; i++) { array[i - start] = i; } return array; } function clearElement(element) { while (element.firstChild) { element.removeChild(element.firstChild); } } function seasonLengthInWeeks(lastFrostDate, firstFrostDate) { const oneWeek = 7 * 24 * 60 * 60 * 1000; return (firstFrostDate.getTime() - lastFrostDate.getTime()) / oneWeek; } function springWeeks(lastFrostDate, firstFrostDate) { const seasonLength = seasonLengthInWeeks(lastFrostDate, firstFrostDate); return range(-10, seasonLength + 1).map(offset => { return weeksLater(lastFrostDate, offset); }); } function createWeeksRow(weeks, lastFrostDate, firstFrostDate) { const row = document.createElement('tr'); row.appendChild(document.createElement('td')); weeks.forEach(date => { const td = document.createElement('th', {scope: "col"}); const firstFrost = date.getTime() == firstFrostDate.getTime(); const lastFrost = date.getTime() == lastFrostDate.getTime() ; const dateString = monthAndDay(date); const label = firstFrost || lastFrost ? `${dateString}❄`: dateString; const text = document.createTextNode(label); td.appendChild(text); row.appendChild(td); }); return row; } function createHeader(headers) { const thead = document.createElement('thead'); const row = document.createElement('tr'); headers.forEach(column => { const th = document.createElement('th', {scope: "col"}); const text = document.createTextNode(column); th.appendChild(text); row.appendChild(th); }); thead.appendChild(row); return thead; } function createCropRow(crop, lastFrostDate, seasonLength) { function isSuccession(offset) { return crop.interval > 0 && offset >= crop.springPlant && // 2 weeks are subtracted to account for shortening days in the // fall. offset <= seasonLength - crop.maturity - 2 && (offset - crop.springPlant) % crop.interval == 0; } function icon(name) { const img = document.createElement('img'); img.classList.add("icon"); img.src = `images/${name}.svg`; img.title = { "direct-sow": "direct sow", "start-indoors": "start indoors", transplant: "transplant outdoors" }[name]; return img; } function sow() { return icon('direct-sow'); } function start() { return icon('start-indoors'); } function transplant() { return icon('transplant'); } function nothing() { return null; } const actions = range(-10, seasonLength + 1).map(offset => { function action(type) { const date = weeksLater(lastFrostDate, offset); const iconFunc = { none: nothing, start: start, sow: sow, transplant: transplant }[type]; return {date: date, icon: iconFunc()}; } if(offset == crop.springStart) { return action('start'); } else if(offset == crop.springPlant) { return crop.isSpringTransplant() ? action('transplant') : action('sow'); } else if(isSuccession(offset)) { return action('sow'); } else if(crop.fallPlanting == 'sow' && offset == seasonLength - crop.maturity - 2) { return action('sow'); } else if(crop.fallPlanting == 'transplant' && offset == seasonLength - crop.maturity - 6) { return action('start'); } else if(crop.fallPlanting == 'transplant' && offset == seasonLength - crop.maturity - 2) { return action('transplant'); } return action('none'); }); const row = document.createElement('tr'); const th = document.createElement('td'); th.appendChild(document.createTextNode(crop.name)); row.appendChild(th); actions.forEach(column => { const td = document.createElement('td'); //const text = document.createTextNode(column.text); td.title = monthAndDay(column.date); if(column.icon) { td.appendChild(column.icon); } row.appendChild(td); }); return row; } function refreshTable() { const headers = ["Crop", "Week"]; const scheduleTable = document.querySelector("#schedule"); const lastFrostDateInput = document.querySelector("input[name='last-frost-date']"); const firstFrostDateInput = document.querySelector("input[name='first-frost-date']"); const lastFrostDate = lastFrostDateInput.valueAsDate; const firstFrostDate = firstFrostDateInput.valueAsDate; if(firstFrostDate && lastFrostDate) { const weeks = springWeeks(lastFrostDate, firstFrostDate); const tbody = document.createElement('tbody'); clearElement(scheduleTable); scheduleTable.appendChild(createHeader(headers)); tbody.appendChild(createWeeksRow(weeks, lastFrostDate, firstFrostDate)); crops.forEach(crop => { tbody.appendChild(createCropRow(crop, lastFrostDate, weeks.length - 11)); }); scheduleTable.appendChild(tbody); } } function loadIntensiveGarden() { const lastFrostDateInput = document.querySelector("input[name='last-frost-date']"); lastFrostDateInput.addEventListener('input', event => { refreshTable(); }); refreshTable(); } window.addEventListener('load', loadIntensiveGarden);