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.
- "This season", "Last year by today" and "Last season's total" each show the share of days recorded. A value with less than 80% of days recorded shows as "not enough data".
- The normal is the NOAA NCEI U.S. Climate Normals 1991–2020 daily year-to-date precipitation at the same station, totaled from the season start to today's date.
- GHCN-Daily reports often arrive days to weeks late, so the card shows the station's latest report date. When the station hasn't reported since the season began, it says "not enough data yet". When the station is behind, we fill only the days after its last report, within this water year, from the nearest airport ASOS (Iowa Environmental Mesonet daily summary) or CIMIS station. That station must pass the same distance, elevation and coastal rules. The card names both sources and their dates, for example "Oct 1–5 El Cajon (GHCN) + Oct 6–15 Gillespie Field (airport ASOS)". ASOS stations report precipitation only when it falls, so a day with observations and no precipitation report counts as 0.00″. A day with no observations counts as missing. Comparisons run through the last complete day. We never estimate missing days.
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:
- have 1991–2020 daily precipitation normals;
- have filed a GHCN-Daily report within the last 21 days;
- have at least 90% of days recorded last season.
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:
- Mountain/foothill: elevation 900 m (about 3,000 ft) or higher.
- Desert: at least 70 km from the coast and below 600 m (Anza-Borrego and Borrego Springs).
- Coastal: less than 18 km from the coast.
- Inland valley: everything else.
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:
- Coastal and inland: the UC Master Gardeners of San Diego County vegetable planting guide (PDF).
- Garlic: the Master Gardener Association of San Diego County, which gives October–November.
- Desert: University of Arizona Extension az1005, Maricopa County. This is a low-desert analog from outside the county, and we label it that way.
- Mountain: University of Arizona Extension Yavapai County Bulletin #51, 3,000–4,500 ft column. This is also an out-of-county analog. Mountain tomatoes and peppers use this ZIP's median frost dates instead.
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:
- Lettuce, peas, carrots and potatoes: 45–75°F.
- Spinach, broccoli, kale and radish: 40–75°F.
- Garlic: 45–85°F.
- Tomatoes, peppers and snap beans: maximum 80°F.
- Squash and cucumber: maximum 90°F.
- Sweet corn: maximum 95°F.
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"
}
]