{"spec_id":"heatmap-rainflow","library":"plotnine","language":"python","code":"\"\"\" anyplot.ai\nheatmap-rainflow: Rainflow Counting Matrix for Fatigue Analysis\nLibrary: plotnine 0.15.4 | Python 3.13.13\nQuality: 90/100 | Updated: 2026-06-02\n\"\"\"\n\nimport os\nimport sys\n\nimport numpy as np\nimport pandas as pd\n\n\n# Work around naming conflict with plotnine.py script and plotnine package\nscript_dir = os.path.dirname(os.path.abspath(__file__))\nif script_dir in sys.path:\n    sys.path.remove(script_dir)\nif \"\" in sys.path:\n    sys.path.remove(\"\")\nif \".\" in sys.path:\n    sys.path.remove(\".\")\n\nfrom plotnine import (\n    aes,\n    annotate,\n    coord_fixed,\n    element_blank,\n    element_line,\n    element_rect,\n    element_text,\n    geom_tile,\n    ggplot,\n    guide_colorbar,\n    guides,\n    labs,\n    scale_fill_gradient,\n    scale_x_continuous,\n    scale_y_continuous,\n    theme,\n    theme_minimal,\n)\n\n\n# Theme tokens — Imprint palette 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\"\nANYPLOT_AMBER = \"#DDCC77\"  # caution anchor — secondary cluster annotation\n\n# Data — simulate rainflow counting results from variable-amplitude loading\nnp.random.seed(42)\n\nn_amp_bins = 20\nn_mean_bins = 20\namplitude_edges = np.linspace(0, 200, n_amp_bins + 1)\nmean_edges = np.linspace(-100, 100, n_mean_bins + 1)\namplitude_centers = (amplitude_edges[:-1] + amplitude_edges[1:]) / 2\nmean_centers = (mean_edges[:-1] + mean_edges[1:]) / 2\n\n# Build rainflow matrix: most cycles at low amplitude near zero mean\namp_grid, mean_grid = np.meshgrid(amplitude_centers, mean_centers, indexing=\"ij\")\ncycle_rate = np.exp(-0.03 * amp_grid) * np.exp(-0.0005 * mean_grid**2)\ncycle_counts = np.random.poisson(lam=cycle_rate * 500)\n\n# Secondary cluster at moderate amplitude / positive mean (realistic loading)\ncluster = 80 * np.exp(-0.01 * (amp_grid - 60) ** 2 - 0.002 * (mean_grid - 30) ** 2)\ncycle_counts += np.random.poisson(lam=cluster)\n\n# Build long-form DataFrame via vectorized numpy array flattening\ndf = pd.DataFrame(\n    {\n        \"Amplitude (MPa)\": amp_grid.flatten(),\n        \"Mean Stress (MPa)\": mean_grid.flatten(),\n        \"Cycle Count\": cycle_counts.flatten(),\n    }\n)\n\ntile_w = float(mean_centers[1] - mean_centers[0])\ntile_h = float(amplitude_centers[1] - amplitude_centers[0])\n\n# Separate zero and nonzero for visual distinction\ndf_nonzero = df[df[\"Cycle Count\"] > 0].copy()\ndf_nonzero[\"Log Count\"] = np.log10(df_nonzero[\"Cycle Count\"])\n\n# Plot\nplot = (\n    ggplot()\n    # Layer 1: Background-colored tiles for zero-count bins (visually distinct)\n    + geom_tile(\n        data=df,\n        mapping=aes(x=\"Mean Stress (MPa)\", y=\"Amplitude (MPa)\"),\n        fill=PAGE_BG,\n        color=INK_SOFT,\n        size=0.1,\n        width=tile_w,\n        height=tile_h,\n        alpha=0.4,\n    )\n    # Layer 2: Imprint sequential gradient for nonzero bins\n    + geom_tile(\n        data=df_nonzero,\n        mapping=aes(x=\"Mean Stress (MPa)\", y=\"Amplitude (MPa)\", fill=\"Log Count\"),\n        color=PAGE_BG,\n        size=0.1,\n        width=tile_w,\n        height=tile_h,\n    )\n    # Imprint sequential colormap: brand green → blue (single-polarity count data)\n    + scale_fill_gradient(\n        low=\"#009E73\", high=\"#4467A3\", name=\"Cycle Count\\n(log₁₀)\", limits=(0, df_nonzero[\"Log Count\"].max())\n    )\n    # Advanced plotnine: stepped colorbar with discrete rectangles (nbin steps)\n    + guides(fill=guide_colorbar(nbin=8, display=\"rectangles\", draw_ulim=True, draw_llim=True))\n    # Highlight the secondary vibration-loading cluster (~60 MPa amp / +30 MPa mean)\n    + annotate(\n        \"rect\",\n        xmin=5.0,\n        xmax=55.0,\n        ymin=35.0,\n        ymax=85.0,\n        fill=ANYPLOT_AMBER,\n        color=ANYPLOT_AMBER,\n        size=0.8,\n        linetype=\"dashed\",\n        alpha=0.08,\n    )\n    + annotate(\n        \"text\", x=57.0, y=60.0, label=\"Vibration\\ncluster\", color=ANYPLOT_AMBER, size=3.0, ha=\"left\", fontweight=\"bold\"\n    )\n    # coord_fixed: enforces square cells for symmetric amplitude/mean-stress ranges\n    + coord_fixed(ratio=1)\n    + scale_x_continuous(expand=(0, 2))\n    + scale_y_continuous(expand=(0, 2))\n    + labs(x=\"Mean Stress (MPa)\", y=\"Stress Amplitude (MPa)\", title=\"heatmap-rainflow · python · plotnine · anyplot.ai\")\n    + theme_minimal()\n    + theme(\n        figure_size=(6, 6),\n        text=element_text(family=\"sans-serif\"),\n        plot_title=element_text(size=12, ha=\"center\", weight=\"bold\", color=INK, margin={\"b\": 10}),\n        axis_title_x=element_text(size=10, color=INK, margin={\"t\": 8}),\n        axis_title_y=element_text(size=10, color=INK, margin={\"r\": 8}),\n        axis_text_x=element_text(size=8, color=INK_SOFT),\n        axis_text_y=element_text(size=8, color=INK_SOFT),\n        axis_line=element_line(color=INK_SOFT),\n        legend_title=element_text(size=8, weight=\"bold\", color=INK),\n        legend_text=element_text(size=8, color=INK_SOFT),\n        legend_position=\"right\",\n        legend_key_height=30,\n        legend_key_width=12,\n        legend_background=element_rect(fill=ELEVATED_BG, color=INK_SOFT),\n        panel_grid_major=element_blank(),\n        panel_grid_minor=element_blank(),\n        panel_border=element_rect(color=INK_SOFT, fill=None),\n        panel_background=element_rect(fill=PAGE_BG),\n        plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n        plot_margin=0.05,\n    )\n)\n\nplot.save(f\"plot-{THEME}.png\", dpi=400, width=6, height=6, units=\"in\", verbose=False)\n"}