Last active
January 10, 2026 18:45
-
-
Save cavedave/1656b3689e49453546d5d435052ad957 to your computer and use it in GitHub Desktop.
Cheesburgers are not possible before refrigeration and modern farming. The really soft burger buns were not very possible but around harvest time something approaching it was possible. Later breads got dryer. Cheese keeps itself but the mild cheese on cheeseburgers not that long unlike the cavey ones. In general it was not made until later in th…
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import pandas as pd | |
| from io import StringIO | |
| import matplotlib.pyplot as plt | |
| import matplotlib.dates as mdates | |
| csv_data = """Product,Start Date,End Date,Notes,Color | |
| Bread (Wheat Flour),2024-01-01,2024-12-31,Grain can be stored year-round,#C2A14D | |
| Bacon (Cured Pork),2024-01-01,2024-12-31,Pigs slaughtered in winter and meat cured for year-round storage,#C45A4A | |
| Turkey,2024-11-01,2025-01-31,Traditional seasonal consumption around holidays,#E8D2C5 | |
| Lettuce,2024-05-15,2024-09-30,Cool-season crop; short shelf life before modern cooling,#4CAF50 | |
| Tomato,2024-07-20,2024-09-20,Frost-sensitive; outdoor field tomatoes only,#E53935 | |
| Mayonnaise (Eggs),2024-03-01,2024-09-30,Chicken egg laying is seasonal before artificial light,#FFD966""" | |
| df = pd.read_csv(StringIO(csv_data), parse_dates=["Start Date", "End Date"]) | |
| df | |
| # ---------------------------- | |
| # 1) Prep data | |
| # ---------------------------- | |
| df["Start Date"] = pd.to_datetime(df["Start Date"]) | |
| df["End Date"] = pd.to_datetime(df["End Date"]) | |
| # Optional label cleanup (match your CSV names if needed) | |
| df["Product"] = df["Product"].replace({ | |
| "Bread (Wheat Flour)": "Bread", | |
| "Bacon (Cured Pork)": "Bacon", | |
| "Mayonnaise (Eggs)": "Mayonnaise", | |
| }) | |
| plot_year = 2024 | |
| # Force everything onto the same seasonal axis year (month alignment only) | |
| df["Start Date"] = df["Start Date"].apply(lambda d: d.replace(year=plot_year)) | |
| df["End Date"] = df["End Date"].apply(lambda d: d.replace(year=plot_year)) | |
| # ---- Duplicate Bread into 3 layers ---- | |
| bread_rows = df[df["Product"].astype(str).eq("Bread")].copy() | |
| if len(bread_rows) >= 1: | |
| base = bread_rows.iloc[0].copy() | |
| df = df[~df["Product"].astype(str).eq("Bread")].copy() | |
| bread_layers = [] | |
| for name in ["Top bread", "Middle bread", "Bottom bread"]: | |
| r = base.copy() | |
| r["Product"] = name | |
| bread_layers.append(r) | |
| df = pd.concat([df, pd.DataFrame(bread_layers)], ignore_index=True) | |
| # ---- Club order (top -> bottom) ---- | |
| club_order = [ | |
| "Top bread", | |
| "Mayonnaise", | |
| "Lettuce", | |
| "Tomato", | |
| "Bacon", | |
| "Middle bread", | |
| "Turkey", | |
| "Bottom bread", | |
| ] | |
| df["Product"] = pd.Categorical(df["Product"], categories=club_order, ordered=True) | |
| df = df.dropna(subset=["Product"]).sort_values("Product").reset_index(drop=True) | |
| # ---------------------------- | |
| # 2) Variable row heights (thickness) | |
| # ---------------------------- | |
| layer_pct = { | |
| "Top bread": 22, | |
| "Middle bread": 18, | |
| "Bottom bread": 22, | |
| "Turkey": 28, | |
| "Bacon": 10, | |
| "Mayonnaise": 7, | |
| "Lettuce": 9, | |
| "Tomato": 9, | |
| } | |
| MIN_H = 6 | |
| row_height = [max(layer_pct.get(str(p), MIN_H), MIN_H) for p in df["Product"]] | |
| gap = 1.8 | |
| y_centers = [] | |
| cursor = 0.0 | |
| for h in row_height: | |
| y_centers.append(cursor + h / 2) | |
| cursor += h + gap | |
| total_height = cursor - gap | |
| pad_top = 7.0 | |
| pad_bottom = 12.0 | |
| # ---------------------------- | |
| # 3) Plot | |
| # ---------------------------- | |
| fig, ax = plt.subplots(figsize=(12, 5)) | |
| plt.figtext( | |
| 0.01, 0.02, | |
| "Dates from Wikipedia; approximate for temperate North East USA\nChart by @iamreddave", | |
| ha="left", | |
| fontsize=9, | |
| style="italic", | |
| color="gray" | |
| ) | |
| fig.subplots_adjust(left=0.10, right=0.98, top=0.88, bottom=0.28) | |
| # Invert and add internal padding above/below stack | |
| ax.set_ylim(total_height + pad_bottom, -pad_top) | |
| def draw_bar(y, height, start, end, color, alpha=1.0): | |
| ax.barh( | |
| y=y, | |
| width=(end - start).days, | |
| left=start, | |
| height=height, | |
| color=color, | |
| edgecolor="black", | |
| alpha=alpha, | |
| zorder=2 | |
| ) | |
| # Draw layers (square bars only) | |
| for i in range(len(df)): | |
| product = str(df.loc[i, "Product"]) | |
| color = df.loc[i, "Color"] | |
| h = row_height[i] | |
| y = y_centers[i] | |
| start = df.loc[i, "Start Date"] | |
| end = df.loc[i, "End Date"] | |
| # (Optional) slight de-emphasis for year-round cured/stored items | |
| bar_alpha = 0.9 if product in {"Bacon", "Top bread", "Middle bread", "Bottom bread"} else 1.0 | |
| if product == "Turkey": | |
| # Wrap-around segments on one Jan–Dec axis | |
| # Example: Nov 1 – Jan 31 -> Jan 1–Jan 31 + Nov 1–Dec 31 | |
| turkey_segments = [ | |
| (pd.Timestamp(f"{plot_year}-01-01"), pd.Timestamp(f"{plot_year}-01-31")), | |
| (pd.Timestamp(f"{plot_year}-11-01"), pd.Timestamp(f"{plot_year}-12-31")), | |
| ] | |
| for s, e in turkey_segments: | |
| draw_bar(y, h, s, e, color, alpha=bar_alpha) | |
| else: | |
| draw_bar(y, h, start, end, color, alpha=bar_alpha) | |
| # Y labels | |
| ax.set_yticks(y_centers) | |
| ax.set_yticklabels([str(x) for x in df["Product"]], fontsize=10) | |
| # X axis | |
| ax.set_xlim(pd.Timestamp(f"{plot_year}-01-01"), pd.Timestamp(f"{plot_year}-12-31")) | |
| ax.xaxis.set_major_locator(mdates.MonthLocator()) | |
| ax.xaxis.set_major_formatter(mdates.DateFormatter("%b")) | |
| ax.set_xlabel("Month") | |
| # Title + subtitle | |
| ax.set_title("Natural Availability of Club Sandwich Elements", fontsize=20, pad=14) | |
| ax.text( | |
| 0.5, 1.005, | |
| # "A club sandwich gathers ingredients naturally ready at different times", | |
| "Dates reflect traditional outdoor local agriculture and preservation", | |
| transform=ax.transAxes, | |
| ha="center", | |
| va="bottom", | |
| fontsize=11, | |
| color="dimgray" | |
| ) | |
| # Overlap highlight (tune dates later if you want) | |
| ax.axvspan( | |
| pd.Timestamp(f"{plot_year}-08-20"), | |
| pd.Timestamp(f"{plot_year}-09-10"), | |
| color="gray", | |
| alpha=0.18, | |
| zorder=0 | |
| ) | |
| ax.text( | |
| pd.Timestamp(f"{plot_year}-08-25"), | |
| -0.10, | |
| "Closest natural overlap", | |
| transform=ax.get_xaxis_transform(), | |
| fontsize=9, | |
| color="gray", | |
| ha="left", | |
| va="top" | |
| ) | |
| ax.xaxis.grid(True, linestyle="--", alpha=0.4) | |
| plt.savefig("club_sandwich_seasonality.png", dpi=600) | |
| plt.show() | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Based on https://waldo.jaquith.org/blog/2011/12/impractical-cheeseburger/ | |
| On the impracticality of a cheeseburger. | |
| which stuck in my brain for 15 years and came out as a visualisation | |
| Cheesburgers are not possible before refrigeration and modern farming. | |
| The really soft burger buns were not very possible but around harvest time something approaching it was possible. | |
| Later breads got dryer. Cheese keeps itself but the mild cheese on cheeseburgers not that long unlike the cavey ones. \ | |
| In general it was not made until later in the year. | |
| Beef can be had at anytime but in general it was kept ofr fattening during the summer. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import pandas as pd | |
| from io import StringIO | |
| import matplotlib.pyplot as plt | |
| import matplotlib.dates as mdates | |
| csv_data = """Product,Start Date,End Date,Notes,Color | |
| Sesame seeds,2024-09-10,2024-10-05,Warm-season annual; seed heads dry in early fall,#D2B48C | |
| Soft bun,2024-08-01,2024-09-15,Spring wheat harvested late summer; represents grain readiness,#C2A14D | |
| Lettuce,2024-05-15,2024-09-30,Cool-season crop; bolts in midsummer heat,#4CAF50 | |
| Onion,2024-07-15,2024-09-15,Bulb onions harvested once tops fall over,#C7B7D6 | |
| Tomato,2024-07-20,2024-09-20,Frost-sensitive; outdoor field tomatoes only,#E53935 | |
| Pickles,2024-01-01,2024-12-31,Harvested immature; heat-loving but short-lived,#6B8E23 | |
| "Cheese",2024-10-01,2025-03-31,Milk peaks May–Aug; aging creates delayed availability,#FFD966 | |
| Beef (pastured),2024-08-15,2024-10-31,Grass finishing at peak weight before winter,#8B4513 | |
| Bottom bun,2024-08-01,2024-09-15,Same harvest as top bun,#C2A14D | |
| """ | |
| df = pd.read_csv(StringIO(csv_data), parse_dates=["Start Date", "End Date"]) | |
| df | |
| import pandas as pd | |
| import matplotlib.pyplot as plt | |
| import matplotlib.dates as mdates | |
| from matplotlib.patches import FancyBboxPatch, PathPatch | |
| from matplotlib.path import Path | |
| import pandas as pd | |
| import matplotlib.pyplot as plt | |
| import matplotlib.dates as mdates | |
| from matplotlib.patches import FancyBboxPatch, PathPatch | |
| from matplotlib.path import Path | |
| # ---------------------------- | |
| # 1) Prep data | |
| # ---------------------------- | |
| df["Start Date"] = pd.to_datetime(df["Start Date"]) | |
| df["End Date"] = pd.to_datetime(df["End Date"]) | |
| df["Product"] = df["Product"].replace({ | |
| "Top bun (wheat)": "Soft bun", | |
| "Bottom bun (wheat)": "Bottom bun", | |
| "Cheese (aged, pasture milk)": "Cheese", | |
| "Pickles (cucumbers)": "Pickles" | |
| }) | |
| # Stretch pickles to all year (historically preserved) | |
| pickle_mask = df["Product"] == "Pickles" | |
| df.loc[pickle_mask, "Start Date"] = pd.Timestamp("2024-01-01") | |
| df.loc[pickle_mask, "End Date"] = pd.Timestamp("2024-12-31") | |
| burger_order = [ | |
| "Sesame seeds", | |
| "Soft bun", | |
| "Lettuce", | |
| "Onion", | |
| "Tomato", | |
| "Pickles", | |
| "Cheese", | |
| "Beef (pastured)", | |
| "Bottom bun", | |
| ] | |
| df["Product"] = pd.Categorical(df["Product"], categories=burger_order, ordered=True) | |
| df = df.dropna(subset=["Product"]).sort_values("Product").reset_index(drop=True) | |
| plot_year = 2024 | |
| df["Start Date"] = df["Start Date"].apply(lambda d: d.replace(year=plot_year)) | |
| df["End Date"] = df["End Date"].apply(lambda d: d.replace(year=plot_year)) | |
| # ---------------------------- | |
| # 2) Variable row heights | |
| # ---------------------------- | |
| layer_pct = { | |
| "Soft bun": 32, | |
| "Bottom bun": 21, | |
| "Beef (pastured)": 24, | |
| "Cheese": 7, | |
| } | |
| MIN_H = 5 | |
| row_height = [max(layer_pct.get(str(p), MIN_H), MIN_H) for p in df["Product"]] | |
| gap = 1.5 | |
| y_centers = [] | |
| cursor = 0.0 | |
| for h in row_height: | |
| y_centers.append(cursor + h / 2) | |
| cursor += h + gap | |
| total_height = cursor - gap # vertical span of stacked layers (data units) | |
| # Add breathing room inside the axes: space above seeds + below bottom bun | |
| pad_top = 6.0 # above sesame seeds (visual top) | |
| pad_bottom = 10.0 # below bottom bun (visual bottom) | |
| # ---------------------------- | |
| # 3) Plot | |
| # ---------------------------- | |
| fig, ax = plt.subplots(figsize=(12, 5)) | |
| # Footer text (figure coords) | |
| plt.figtext( | |
| 0.01, 0.01, | |
| "Dates from Wikipedia; approximate for temperate North East USA\nChart by @iamreddave", | |
| ha="left", | |
| fontsize=9, | |
| style="italic", | |
| color="gray" | |
| ) | |
| # Breathing room OUTSIDE the axes (top and bottom of the figure) | |
| fig.subplots_adjust(left=0.10, right=0.98, top=0.88, bottom=0.28) | |
| # IMPORTANT: invert y-axis BEFORE drawing patches | |
| ax.set_ylim(total_height + pad_bottom, -pad_top) | |
| def draw_bar(y, height, start, end, color, alpha=1.0): | |
| ax.barh( | |
| y=y, | |
| width=(end - start).days, | |
| left=start, | |
| height=height, | |
| color=color, | |
| edgecolor="black", | |
| alpha=alpha, | |
| zorder=2 | |
| ) | |
| def draw_rounded_bar(y, height, start, end, color, alpha=1.0): | |
| """Fully rounded bar (patty-like).""" | |
| x0 = mdates.date2num(start) | |
| x1 = mdates.date2num(end) | |
| width = x1 - x0 | |
| rounding = height * 0.45 | |
| patch = FancyBboxPatch( | |
| (x0, y - height / 2), | |
| width, | |
| height, | |
| boxstyle=f"round,pad=0,rounding_size={rounding}", | |
| linewidth=1, | |
| edgecolor="black", | |
| facecolor=color, | |
| alpha=alpha, | |
| transform=ax.transData, | |
| zorder=3 | |
| ) | |
| ax.add_patch(patch) | |
| def draw_one_sided_rounded_bar(y, height, start, end, color, alpha=1.0, which="top"): | |
| """ | |
| Round ONLY the *visual* top corners (which='top') or *visual* bottom corners (which='bottom'). | |
| Since we set_ylim(..., ...) above, ax.yaxis_inverted() is True during drawing. | |
| """ | |
| effective = which | |
| if ax.yaxis_inverted(): | |
| effective = "bottom" if which == "top" else "top" | |
| x0 = mdates.date2num(start) | |
| x1 = mdates.date2num(end) | |
| w = x1 - x0 | |
| h = height | |
| y0 = y - h / 2 | |
| y1 = y + h / 2 | |
| r = min(h * 0.45, w * 0.25) | |
| verts, codes = [], [] | |
| if effective == "top": | |
| # Flat bottom, rounded TOP corners | |
| verts.append((x0, y0)); codes.append(Path.MOVETO) | |
| verts.append((x1, y0)); codes.append(Path.LINETO) | |
| verts.append((x1, y1 - r)); codes.append(Path.LINETO) | |
| verts.append((x1, y1)); codes.append(Path.CURVE3) | |
| verts.append((x1 - r, y1)); codes.append(Path.CURVE3) | |
| verts.append((x0 + r, y1)); codes.append(Path.LINETO) | |
| verts.append((x0, y1)); codes.append(Path.CURVE3) | |
| verts.append((x0, y1 - r)); codes.append(Path.CURVE3) | |
| verts.append((x0, y0)); codes.append(Path.LINETO) | |
| elif effective == "bottom": | |
| # Flat top, rounded BOTTOM corners | |
| verts.append((x0, y1)); codes.append(Path.MOVETO) | |
| verts.append((x1, y1)); codes.append(Path.LINETO) | |
| verts.append((x1, y0 + r)); codes.append(Path.LINETO) | |
| verts.append((x1, y0)); codes.append(Path.CURVE3) | |
| verts.append((x1 - r, y0)); codes.append(Path.CURVE3) | |
| verts.append((x0 + r, y0)); codes.append(Path.LINETO) | |
| verts.append((x0, y0)); codes.append(Path.CURVE3) | |
| verts.append((x0, y0 + r)); codes.append(Path.CURVE3) | |
| verts.append((x0, y1)); codes.append(Path.LINETO) | |
| else: | |
| raise ValueError("which must be 'top' or 'bottom'") | |
| verts.append((0, 0)); codes.append(Path.CLOSEPOLY) | |
| patch = PathPatch( | |
| Path(verts, codes), | |
| facecolor=color, | |
| edgecolor="black", | |
| linewidth=1, | |
| alpha=alpha, | |
| transform=ax.transData, | |
| zorder=3 | |
| ) | |
| ax.add_patch(patch) | |
| def draw_seed_cluster(y, height, start, end, color, n_seeds=4, alpha=1.0): | |
| """Draw multiple small rounded rectangles to suggest sesame seeds.""" | |
| x0 = mdates.date2num(start) | |
| x1 = mdates.date2num(end) | |
| total_width = x1 - x0 | |
| seed_width = total_width / (n_seeds * 1.6) | |
| seed_height = height * 0.45 # slightly bigger | |
| y_jitter = height * 0.18 | |
| rounding = seed_height * 0.75 | |
| for i in range(n_seeds): | |
| sx = x0 + (i + 0.5) * total_width / n_seeds - seed_width / 2 | |
| sy = y + ((-1) ** i) * y_jitter * 0.5 | |
| patch = FancyBboxPatch( | |
| (sx, sy - seed_height / 2), | |
| seed_width, | |
| seed_height, | |
| boxstyle=f"round,pad=0,rounding_size={rounding}", | |
| facecolor=color, | |
| edgecolor="black", | |
| linewidth=0.8, | |
| alpha=alpha, | |
| transform=ax.transData, | |
| zorder=4 | |
| ) | |
| ax.add_patch(patch) | |
| # ---------------------------- | |
| # Draw layers | |
| # ---------------------------- | |
| for i in range(len(df)): | |
| product = str(df.loc[i, "Product"]) | |
| color = df.loc[i, "Color"] | |
| h = row_height[i] | |
| y = y_centers[i] | |
| bar_alpha = 0.6 if product == "Pickles" else 1.0 | |
| if product == "Cheese": | |
| cheese_segments = [ | |
| (pd.Timestamp(f"{plot_year}-01-01"), pd.Timestamp(f"{plot_year}-04-30")), | |
| (pd.Timestamp(f"{plot_year}-10-01"), pd.Timestamp(f"{plot_year}-12-31")), | |
| ] | |
| for s, e in cheese_segments: | |
| draw_bar(y, h, s, e, color, alpha=bar_alpha) | |
| elif product == "Beef (pastured)": | |
| start = df.loc[i, "Start Date"] | |
| end = df.loc[i, "End Date"] | |
| draw_rounded_bar(y, h, start, end, color, alpha=bar_alpha) | |
| elif product == "Soft bun": | |
| start = df.loc[i, "Start Date"] | |
| end = df.loc[i, "End Date"] | |
| draw_one_sided_rounded_bar(y, h, start, end, color, alpha=bar_alpha, which="top") | |
| elif product == "Bottom bun": | |
| start = df.loc[i, "Start Date"] | |
| end = df.loc[i, "End Date"] | |
| draw_one_sided_rounded_bar(y, h, start, end, color, alpha=bar_alpha, which="bottom") | |
| elif product == "Sesame seeds": | |
| start = df.loc[i, "Start Date"] | |
| end = df.loc[i, "End Date"] | |
| draw_seed_cluster(y, h, start, end, color, n_seeds=4, alpha=1.0) | |
| else: | |
| start = df.loc[i, "Start Date"] | |
| end = df.loc[i, "End Date"] | |
| draw_bar(y, h, start, end, color, alpha=bar_alpha) | |
| # Y labels | |
| ax.set_yticks(y_centers) | |
| ax.set_yticklabels(df["Product"], fontsize=10) | |
| # X axis | |
| ax.set_xlim(pd.Timestamp(f"{plot_year}-01-01"), pd.Timestamp(f"{plot_year}-12-31")) | |
| ax.xaxis.set_major_locator(mdates.MonthLocator()) | |
| ax.xaxis.set_major_formatter(mdates.DateFormatter("%b")) | |
| ax.set_xlabel("Month") | |
| # Title + subtitle | |
| ax.set_title("Natural Seasonal Availability of a Cheeseburger", fontsize=20, pad=14) | |
| ax.text( | |
| 0.5, 1.005, | |
| "A cheeseburger gathers ingredients naturally ready at different times", | |
| transform=ax.transAxes, | |
| ha="center", | |
| va="bottom", | |
| fontsize=11, | |
| color="dimgray" | |
| ) | |
| # Overlap highlight (slightly darker) | |
| ax.axvspan( | |
| pd.Timestamp(f"{plot_year}-08-20"), | |
| pd.Timestamp(f"{plot_year}-09-10"), | |
| color="gray", | |
| alpha=0.18, | |
| zorder=0 | |
| ) | |
| # Overlap label in the bottom margin | |
| ax.text( | |
| pd.Timestamp(f"{plot_year}-08-25"), | |
| -0.10, | |
| "Closest natural overlap", | |
| transform=ax.get_xaxis_transform(), | |
| fontsize=9, | |
| color="gray", | |
| ha="left", | |
| va="top" | |
| ) | |
| ax.xaxis.grid(True, linestyle="--", alpha=0.4) | |
| plt.savefig("cheeseburger_seasonality.png", dpi=600) | |
| plt.show() |
Author
cavedave
commented
Jan 7, 2026
Author
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment

