{"spec_id":"heatmap-rainflow","library":"matplotlib","language":"python","code":"\"\"\" anyplot.ai\nheatmap-rainflow: Rainflow Counting Matrix for Fatigue Analysis\nLibrary: matplotlib 3.10.9 | Python 3.13.13\nQuality: 93/100 | Updated: 2026-06-02\n\"\"\"\n\nimport os\n\nimport matplotlib.pyplot as plt\nimport numpy as np\nfrom matplotlib.colors import LinearSegmentedColormap, LogNorm\nfrom matplotlib.ticker import LogFormatterSciNotation\n\n\n# Theme tokens — Imprint palette, theme-adaptive chrome\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nELEVATED_BG = \"#FFFDF6\" if THEME == \"light\" else \"#242420\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_SOFT = \"#4A4A44\" if THEME == \"light\" else \"#B8B7B0\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\n\n# Imprint sequential colormap for continuous cycle-count data\nimprint_seq = LinearSegmentedColormap.from_list(\"imprint_seq\", [\"#009E73\", \"#4467A3\"])\nimprint_seq.set_bad(PAGE_BG)  # masked zero-count bins blend with page background\nimprint_seq.set_under(PAGE_BG)\n\n# Data — simulate rainflow counting from variable-amplitude fatigue signal\nnp.random.seed(42)\n\nn_amp_bins = 20\nn_mean_bins = 20\namp_edges = np.linspace(0, 200, n_amp_bins + 1)\nmean_edges = np.linspace(-50, 250, n_mean_bins + 1)\namp_centers = (amp_edges[:-1] + amp_edges[1:]) / 2\nmean_centers = (mean_edges[:-1] + mean_edges[1:]) / 2\n\nmean_grid, amp_grid = np.meshgrid(mean_centers, amp_centers)\n\n# Dominant low-amplitude cycles near mean load (~100 MPa)\nraw_counts = 3000 * np.exp(-amp_grid / 30) * np.exp(-((mean_grid - 100) ** 2) / (2 * 50**2))\n\n# Secondary overload cluster at higher amplitude\nraw_counts += 800 * np.exp(-((amp_grid - 75) ** 2) / (2 * 15**2)) * np.exp(-((mean_grid - 170) ** 2) / (2 * 25**2))\n\ncycle_counts = np.clip(np.round(raw_counts).astype(int), 0, None)\nmasked_counts = np.ma.masked_where(cycle_counts == 0, cycle_counts)\n\n# Gaussian-smoothed counts for contour overlay\nk = np.arange(-3, 4)\nkernel_1d = np.exp(-0.5 * (k / 1.2) ** 2)\nkernel_1d /= kernel_1d.sum()\nsmooth_counts = np.apply_along_axis(lambda r: np.convolve(r, kernel_1d, mode=\"same\"), 0, cycle_counts.astype(float))\nsmooth_counts = np.apply_along_axis(lambda r: np.convolve(r, kernel_1d, mode=\"same\"), 1, smooth_counts)\n\n# Plot\ntitle = \"heatmap-rainflow · python · matplotlib · anyplot.ai\"\ntitle_fontsize = max(8, round(12 * 67 / len(title))) if len(title) > 67 else 12\n\nfig, ax = plt.subplots(figsize=(8, 4.5), dpi=400, facecolor=PAGE_BG)\nax.set_facecolor(PAGE_BG)\n\nim = ax.pcolormesh(\n    mean_edges,\n    amp_edges,\n    masked_counts,\n    cmap=imprint_seq,\n    norm=LogNorm(vmin=1, vmax=cycle_counts.max()),\n    shading=\"flat\",\n)\n\n# Contour threshold overlay — quantitative reference lines\ncontour_levels = [10, 50, 200, 1000]\ncs = ax.contour(\n    mean_centers,\n    amp_centers,\n    smooth_counts,\n    levels=contour_levels,\n    colors=INK_SOFT,\n    linewidths=[0.5, 0.6, 0.8, 1.0],\n    alpha=0.7,\n)\nax.clabel(cs, inline=True, fontsize=8, fmt=\"%d\", colors=INK_SOFT)\n\n# Colorbar\ncbar = fig.colorbar(im, ax=ax, fraction=0.046, pad=0.03, aspect=30)\ncbar.ax.tick_params(labelsize=8, colors=INK_SOFT)\ncbar.ax.yaxis.set_major_formatter(LogFormatterSciNotation(minor_thresholds=(2, 0.5)))\ncbar.set_label(\"Cycle Count\", fontsize=10, labelpad=12, color=INK)\ncbar.outline.set_visible(False)\n\n# Annotations — highlight dominant zone and overload cluster\nax.annotate(\n    \"Dominant\\ncycle zone\",\n    xy=(100, 15),\n    xytext=(0, 80),\n    fontsize=10,\n    fontweight=\"bold\",\n    color=INK,\n    ha=\"center\",\n    arrowprops={\"arrowstyle\": \"->\", \"color\": INK, \"lw\": 1.2},\n)\nax.annotate(\n    \"Overload\\ncluster\",\n    xy=(170, 75),\n    xytext=(220, 140),\n    fontsize=10,\n    fontweight=\"bold\",\n    color=INK_SOFT,\n    ha=\"center\",\n    arrowprops={\"arrowstyle\": \"->\", \"color\": INK_SOFT, \"lw\": 1.2},\n)\n\n# Style — theme-adaptive chrome\nax.set_xlabel(\"Mean Stress (MPa)\", fontsize=10, color=INK)\nax.set_ylabel(\"Stress Amplitude (MPa)\", fontsize=10, color=INK)\nax.set_title(title, fontsize=title_fontsize, fontweight=\"medium\", color=INK)\nax.tick_params(axis=\"both\", labelsize=8, colors=INK_SOFT)\nax.spines[\"top\"].set_visible(False)\nax.spines[\"right\"].set_visible(False)\nfor s in (\"left\", \"bottom\"):\n    ax.spines[s].set_color(INK_SOFT)\n\nfig.tight_layout()\nplt.savefig(f\"plot-{THEME}.png\", dpi=400, facecolor=PAGE_BG)\n"}