Methodology & data sources

Location

US Census 2023 Gazetteer ZCTA internal points. If a ZIP has no ZCTA (PO-box or unique ZIP), or its ZCTA point is more than 30 km from the town it serves, we use the GeoNames postal-code point (CC BY 4.0) and say so on the page.

USDA hardiness zone

2023 USDA Plant Hardiness Zone Map ZIP datasets from USDA ARS and the PRISM Climate Group at Oregon State University (CONUS, AK, HI and PR files).

Frost dates

NOAA NCEI U.S. Climate Normals 1991–2020, annual/seasonal freeze probabilities at 32°F. We use the nearest COOP/first-order station within 80 km with at least 20 years of record, preferring at least 28 years within 40 km. 50% is the median date; 10% spring means a later freeze happens in only 1 of 10 years. When NCEI reports no freeze dates and a 365-day season, we label the station frost-free in most years.

7-day forecast

NOAA National Weather Service api.weather.gov: daily highs and overnight lows from the forecast, and max wind gust per local day from gridpoint data. Frost flag = low ≤32°F. High-wind flag = gust ≥40 mph. If the text forecast leaves out a low, gridpoint minTemperature fills it.

Soil temperature

Measured where a NOAA USCRN station is within 60 km and 300 m elevation (10 cm sensor, daily mean). Otherwise modeled: Open-Meteo, mean of the 6 cm and 18 cm layers. Modeled values are labeled on every page.

Choosing stations (elevation-aware)

Temperature changes with elevation, so the nearest station is not always the right one. For frost dates and chill hours we look up the elevation of the town center for each ZIP (USGS 3DEP) and measure distance from there. We use the nearest station within 40 km and within about 1,000 ft (300 m) of that elevation. If there is none, we weigh distance against elevation difference (100 m counts like 10 km). If the best option still differs by more than about 2,000 ft (600 m), or for chill hours is more than 50 km away, we show “not enough nearby data” instead of a misleading number. When we use a station that differs by more than 1,000 ft, the page says so.

Chill hours

Observed hourly temperatures from the nearest suitable hourly station: an airport (ASOS, via the Iowa Environmental Mesonet) or, in California, a CIMIS station run by the California Department of Water Resources (hourly air temperature; CIMIS hours flagged by its quality control are dropped). Both kinds follow the same distance, elevation and coastal/inland rules, and the closest qualifying one is used, with at least 60% coverage. Chill hours = hours at 32–45°F, counted from Nov 1 (before Nov 1 the season-to-date figures read “Counting starts Nov 1”). Missing hours are not filled. For comparison we show last season by the same calendar date, last season's full total (Nov 1–Apr 30), and an average by date over up to the last 10 seasons at the same station; a season counts toward the average only if at least 80% of its hours were observed, and we say how many seasons were used. If fewer than 5 seasons qualify, we show “not enough data”. Past seasons are fetched once per station and cached.

Rainfall

Forecast. Each day shows the NOAA NWS chance of precipitation from api.weather.gov. When we compute an amount, it comes from the NWS gridpoint quantitative precipitation forecast (QPF), summed by local day. Days with a 50% chance or higher get a "Rain" badge.

Rainfall card. Observed daily precipitation (rain plus melted snow) comes from NOAA NCEI GHCN-Daily. We skip values with a quality flag.

Season start. In western, dry-summer states (CA, OR, WA, NV, AZ, UT, ID), most rain falls between October and April. There we use the water year, Oct 1–Sep 30. Everywhere else we use the calendar year, starting Jan 1.

Station choice. Rainfall in San Diego County changes sharply with elevation and distance from the coast, so we use the same rules as frost dates. The station must be within 80 km and on the same coastal or inland side. Stations within 40 km and 300 m (~984 ft) of the ZIP's elevation come first, nearest first. The station must also:

When the nearest usable station is more than 300 m higher or lower, the card shows a warning.

Planting & harvest calendar (San Diego County)

The "What to grow when" crop cards and the harvest planner read the same calendar. For each San Diego County ZIP, we pick one of four sub-regions using its location and elevation:

The 18 km coastal line is our own approximation of the guide's Regions 1–2 versus 3–4. It is not an official boundary.

Planting months come straight from published guides:

Harvest months are our own calculation. We add the typical days-to-harvest from az1005 to each planting month.

Heat and cold limits on harvest. We drop a harvest month when the ZIP's normal monthly mean temperature is above the crop's maximum. Cool-season crops bolt or turn bitter above it, and tomatoes and peppers set fruit poorly. The mean is the average of the NOAA NCEI 1991–2020 normal daily high and low, from the same station as the frost dates. The maximums come from the table "Approximate monthly temperatures for best growth and quality of vegetable crops" (J.F. Harrington, UC Davis, in Knott's Handbook for Vegetable Growers). We use the copy in UCCE Placer & Nevada Counties Publication 31-043C:

For cool-season crops, months with a mean below the minimum are a winter pause, so there is no harvest. Frost-tender crops stop at the median first fall frost. Garlic and potatoes are dug at maturity, so the heat limit doesn't apply to them.

We never change the sourced planting months. If a late planting can't reach harvest before heat or cold, the card says to plant early in the window.

Station review. We excluded JULIAN CDF (USC00044412) from frost dates. Its median 32°F dates (Mar 27 and Dec 1) are 3–5 weeks milder than every nearby long-record station: Cuyamaca Apr 19 and Oct 27, Henshaw Dam Apr 18 and Oct 26, Campo Apr 30 and Oct 26, Palomar Mountain Apr 29 and Nov 18. Its January normal low is 39°F, against 33°F at Cuyamaca. That points to a thermal-belt site. Julian and Ranchita now use Cuyamaca.

When the current month is open for planting, the card also checks live soil temperature and the 7-day frost forecast.

A crop with no sourced dates for a region is marked "No sourced dates", and the planner disables it. Right now that is basil on the coast, inland and in the mountains. Citrus and strawberries are perennials, so they are not on this calendar.

ZIPs outside San Diego County still use the older frost-offset rules below. Those rules have not been reviewed.

Crop readiness rules

Each crop has planting windows in weeks relative to the median last-spring and first-fall freeze, plus a minimum soil temperature. A month is Ready if its midpoint falls in a window and Soon if the window opens within 4 weeks (or it's time to start seeds indoors). For the current month, a Ready crop drops to Soon if live soil is below its minimum, or, for warm-season crops, if frost is in the 7-day forecast. Frost-free and very long-season ZIPs (≥300 days) use a generic mild-winter calendar instead. Thresholds follow common extension guidance and are pending horticulturist review.

[
 {
  "id": "peas",
  "name": "Peas",
  "cat": "cool",
  "min_soil_f": 40,
  "spring": [
   -6,
   0
  ],
  "fall": [
   -12,
   -8
  ],
  "mild_winter": [
   10,
   11,
   0,
   1
  ],
  "sow": "Direct sow",
  "affiliate": "Pea seeds"
 },
 {
  "id": "lettuce",
  "name": "Lettuce",
  "cat": "cool",
  "min_soil_f": 40,
  "spring": [
   -4,
   2
  ],
  "fall": [
   -10,
   -4
  ],
  "mild_winter": [
   9,
   10,
   11,
   0,
   1,
   2
  ],
  "sow": "Direct sow / succession",
  "affiliate": "Lettuce seeds"
 },
 {
  "id": "spinach",
  "name": "Spinach",
  "cat": "cool",
  "min_soil_f": 40,
  "spring": [
   -6,
   -2
  ],
  "fall": [
   -8,
   -4
  ],
  "mild_winter": [
   9,
   10,
   11,
   0,
   1
  ],
  "sow": "Direct sow"
 },
 {
  "id": "radish",
  "name": "Radish",
  "cat": "cool",
  "min_soil_f": 40,
  "spring": [
   -5,
   1
  ],
  "fall": [
   -6,
   -2
  ],
  "mild_winter": [
   9,
   10,
   11,
   0,
   1,
   2
  ],
  "sow": "Direct sow"
 },
 {
  "id": "carrots",
  "name": "Carrots",
  "cat": "cool",
  "min_soil_f": 45,
  "spring": [
   -3,
   3
  ],
  "fall": [
   -12,
   -8
  ],
  "mild_winter": [
   8,
   9,
   10,
   11,
   0,
   1
  ],
  "sow": "Direct sow",
  "affiliate": "Carrot seeds"
 },
 {
  "id": "potatoes",
  "name": "Potatoes",
  "cat": "cool",
  "min_soil_f": 45,
  "spring": [
   -3,
   1
  ],
  "fall": null,
  "mild_winter": [
   0,
   1
  ],
  "sow": "Plant seed potatoes",
  "affiliate": "Seed potatoes"
 },
 {
  "id": "kale",
  "name": "Kale",
  "cat": "cool",
  "min_soil_f": 40,
  "spring": [
   -4,
   0
  ],
  "fall": [
   -12,
   -6
  ],
  "mild_winter": [
   8,
   9,
   10,
   11,
   0,
   1
  ],
  "sow": "Direct sow / transplant",
  "affiliate": "Kale seeds"
 },
 {
  "id": "garlic",
  "name": "Garlic",
  "cat": "fall",
  "min_soil_f": null,
  "spring": null,
  "fall": [
   -2,
   4
  ],
  "mild_winter": [
   9,
   10,
   11
  ],
  "sow": "Plant cloves for next year",
  "affiliate": "Seed garlic"
 },
 {
  "id": "tomatoes",
  "name": "Tomatoes",
  "cat": "warm",
  "min_soil_f": 60,
  "spring": [
   1,
   6
  ],
  "indoor": [
   -8,
   -6
  ],
  "fall": null,
  "mild_winter": [
   1,
   2,
   3,
   7
  ],
  "sow": "Transplant",
  "affiliate": "Tomato starts / seeds"
 },
 {
  "id": "peppers",
  "name": "Peppers",
  "cat": "warm",
  "min_soil_f": 65,
  "spring": [
   2,
   6
  ],
  "indoor": [
   -10,
   -8
  ],
  "fall": null,
  "mild_winter": [
   1,
   2,
   3,
   7
  ],
  "sow": "Transplant"
 },
 {
  "id": "basil",
  "name": "Basil",
  "cat": "warm",
  "min_soil_f": 60,
  "spring": [
   1,
   8
  ],
  "indoor": [
   -6,
   -4
  ],
  "fall": null,
  "mild_winter": [
   2,
   3,
   4,
   5,
   6
  ],
  "sow": "Transplant / sow"
 },
 {
  "id": "beans",
  "name": "Beans",
  "cat": "warm",
  "min_soil_f": 60,
  "spring": [
   1,
   8
  ],
  "fall": null,
  "mild_winter": [
   2,
   3,
   7,
   8
  ],
  "sow": "Direct sow"
 },
 {
  "id": "corn",
  "name": "Sweet corn",
  "cat": "warm",
  "min_soil_f": 60,
  "spring": [
   1,
   6
  ],
  "fall": null,
  "mild_winter": [
   1,
   2,
   3,
   7
  ],
  "sow": "Direct sow"
 },
 {
  "id": "squash",
  "name": "Squash",
  "cat": "warm",
  "min_soil_f": 65,
  "spring": [
   1,
   6
  ],
  "fall": null,
  "mild_winter": [
   2,
   3,
   7,
   8
  ],
  "sow": "Direct sow / transplant"
 },
 {
  "id": "cucumber",
  "name": "Cucumber",
  "cat": "warm",
  "min_soil_f": 65,
  "spring": [
   1,
   6
  ],
  "fall": null,
  "mild_winter": [
   2,
   3,
   7,
   8
  ],
  "sow": "Direct sow / transplant"
 }
]