{"spec_id":"heatmap-rainflow","library":"letsplot","language":"python","code":"\"\"\" anyplot.ai\nheatmap-rainflow: Rainflow Counting Matrix for Fatigue Analysis\nLibrary: letsplot 4.10.1 | Python 3.13.13\nQuality: 91/100 | Updated: 2026-06-02\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport pandas as pd\nfrom lets_plot import (\n    LetsPlot,\n    aes,\n    coord_cartesian,\n    element_blank,\n    element_line,\n    element_rect,\n    element_text,\n    geom_text,\n    geom_tile,\n    ggplot,\n    ggsize,\n    guide_colorbar,\n    labs,\n    layer_tooltips,\n    scale_fill_gradient,\n    scale_x_continuous,\n    scale_y_continuous,\n    theme,\n    theme_minimal,\n)\nfrom lets_plot.export import ggsave\n\n\nLetsPlot.setup_html()\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# Data — simulate rainflow counting from a variable-amplitude fatigue load signal\nnp.random.seed(42)\n\nn_bins = 20\namplitude_centers = np.linspace(10, 200, n_bins)\nmean_centers = np.linspace(-50, 100, n_bins)\namp_step = float(amplitude_centers[1] - amplitude_centers[0])\nmean_step = float(mean_centers[1] - mean_centers[0])\n\n# Realistic rainflow matrix: exponential decay in amplitude, Gaussian in mean\namp_grid, mean_grid = np.meshgrid(amplitude_centers, mean_centers, indexing=\"ij\")\nbase_counts = 1000 * np.exp(-0.022 * amp_grid) * np.exp(-0.0004 * (mean_grid - 15) ** 2)\nnoise = np.random.exponential(scale=0.2, size=base_counts.shape)\nraw_counts = np.round(base_counts * (1 + noise)).astype(int)\n\n# Secondary cluster: resonance-induced loading at moderate amplitude / higher mean\nresonance_amp = 80.0\nresonance_mean = 55.0\nsecondary = 250 * np.exp(-0.008 * (amp_grid - resonance_amp) ** 2) * np.exp(-0.005 * (mean_grid - resonance_mean) ** 2)\nraw_counts = raw_counts + np.round(secondary * (1 + noise * 0.3)).astype(int)\nraw_counts[raw_counts < 3] = 0\n\n# Build long-form DataFrame (only non-zero bins; zero bins show PAGE_BG background)\nrows = []\nfor i, amp in enumerate(amplitude_centers):\n    for j, mn in enumerate(mean_centers):\n        count = raw_counts[i, j]\n        if count > 0:\n            rows.append({\"amplitude\": round(float(amp), 1), \"mean_stress\": round(float(mn), 1), \"cycles\": int(count)})\n\ndf = pd.DataFrame(rows)\n\n# Pre-compute log10 for fill — gives stable gradient control with manual break labels\ndf[\"log_cycles\"] = np.log10(df[\"cycles\"].astype(float))\nlog_break_values = [5, 10, 50, 100, 500, 1000]\nlog_breaks = [np.log10(v) for v in log_break_values]\nlog_labels = [\"5\", \"10\", \"50\", \"100\", \"500\", \"1,000\"]\n\n# Annotations — peak region and resonance cluster\npeak_row = df.loc[df[\"cycles\"].idxmax()]\npeak_label = pd.DataFrame(\n    {\n        \"mean_stress\": [peak_row[\"mean_stress\"]],\n        \"amplitude\": [peak_row[\"amplitude\"] + amp_step * 1.4],\n        \"label\": [f\"Peak: {int(peak_row['cycles']):,} cycles\"],\n    }\n)\nresonance_label = pd.DataFrame(\n    {\"mean_stress\": [resonance_mean + 6], \"amplitude\": [resonance_amp + amp_step * 2.8], \"label\": [\"Resonance cluster\"]}\n)\n\ntitle = \"heatmap-rainflow · python · letsplot · anyplot.ai\"\n\n# Plot — Imprint sequential colormap (green → blue) on log₁₀ scale\nplot = (\n    ggplot(df, aes(x=\"mean_stress\", y=\"amplitude\", fill=\"log_cycles\"))\n    + geom_tile(\n        width=mean_step * 0.92,\n        height=amp_step * 0.92,\n        tooltips=layer_tooltips()\n        .format(\"@amplitude\", \".0f\")\n        .format(\"@mean_stress\", \".0f\")\n        .format(\"@cycles\", \",d\")\n        .line(\"Amplitude: @amplitude MPa\")\n        .line(\"Mean Stress: @mean_stress MPa\")\n        .line(\"Cycles: @cycles\"),\n    )\n    + geom_text(\n        aes(x=\"mean_stress\", y=\"amplitude\", label=\"label\"),\n        data=peak_label,\n        inherit_aes=False,\n        size=4,\n        color=INK,\n        fontface=\"bold\",\n    )\n    + geom_text(\n        aes(x=\"mean_stress\", y=\"amplitude\", label=\"label\"), data=resonance_label, inherit_aes=False, size=3.5, color=INK\n    )\n    + scale_fill_gradient(\n        low=\"#009E73\",\n        high=\"#4467A3\",\n        name=\"Cycle Count\",\n        breaks=log_breaks,\n        labels=log_labels,\n        guide=guide_colorbar(barwidth=18, barheight=280, nbin=256),\n    )\n    + scale_x_continuous(\n        name=\"Mean Stress (MPa)\", breaks=[-40, -20, 0, 20, 40, 60, 80, 100], format=\"d\", expand=[0.02, 0]\n    )\n    + scale_y_continuous(\n        name=\"Stress Amplitude (MPa)\",\n        breaks=[20, 40, 60, 80, 100, 120, 140, 160, 180, 200],\n        format=\"d\",\n        expand=[0.02, 0],\n    )\n    + coord_cartesian(xlim=[-60, 110], ylim=[0, 210])\n    + labs(\n        title=title,\n        subtitle=\"Variable-amplitude fatigue load spectrum · resonance-induced secondary cluster\",\n        caption=\"Simulated rainflow matrix · cycle counts on log₁₀ scale\",\n    )\n    + theme_minimal()\n    + theme(\n        plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n        panel_background=element_rect(fill=PAGE_BG),\n        plot_title=element_text(size=16, face=\"bold\", color=INK),\n        plot_subtitle=element_text(size=11, color=INK_SOFT),\n        plot_caption=element_text(size=9, color=INK_MUTED),\n        axis_title=element_text(size=12, color=INK, face=\"bold\"),\n        axis_text=element_text(size=10, color=INK_SOFT),\n        axis_line=element_line(color=INK_SOFT, size=0.5),\n        legend_background=element_rect(fill=ELEVATED_BG, color=INK_SOFT),\n        legend_text=element_text(size=10, color=INK_SOFT),\n        legend_title=element_text(size=12, color=INK, face=\"bold\"),\n        panel_grid=element_blank(),\n        plot_margin=[40, 20, 20, 20],\n    )\n    + ggsize(600, 600)\n)\n\n# Save\nggsave(plot, f\"plot-{THEME}.png\", path=\".\", scale=4)\nggsave(plot, f\"plot-{THEME}.html\", path=\".\")\n"}