{"spec_id":"heatmap-rainflow","library":"plotly","language":"python","code":"\"\"\" anyplot.ai\nheatmap-rainflow: Rainflow Counting Matrix for Fatigue Analysis\nLibrary: plotly 6.7.0 | Python 3.13.13\nQuality: 90/100 | Updated: 2026-06-02\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport plotly.graph_objects as go\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\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\nGRID = \"rgba(26,26,23,0.15)\" if THEME == \"light\" else \"rgba(240,239,232,0.15)\"\n\n# Imprint sequential colormap for single-polarity cycle count data\nimprint_seq = [[0.0, \"#009E73\"], [1.0, \"#4467A3\"]]\n\n# Data — rainflow matrix: exponential decay in amplitude × Gaussian in mean stress\n# Physically realistic: most cycles are small (exponential amplitude distribution),\n# centred around service mean stress with a tensile-dominated secondary regime.\nnp.random.seed(42)\n\nn_bins = 20\namplitude_edges = np.linspace(10, 200, n_bins + 1)\nmean_edges = np.linspace(-50, 250, n_bins + 1)\namplitude_centers = (amplitude_edges[:-1] + amplitude_edges[1:]) / 2\nmean_centers = (mean_edges[:-1] + mean_edges[1:]) / 2\n\namp_grid, mean_grid = np.meshgrid(amplitude_centers, mean_centers, indexing=\"ij\")\n\n# Exponential amplitude decay (characteristic amplitude ~45 MPa)\namp_decay = np.exp(-amp_grid / 45)\n\n# Primary cluster: baseline service loads, mean ~100 MPa\nmean_primary = np.exp(-((mean_grid - 100) ** 2) / (2 * 55**2))\ncounts = 850 * amp_decay * mean_primary\n\n# Secondary cluster: tensile-dominated regime, mean ~175 MPa\nmean_secondary = np.exp(-((mean_grid - 175) ** 2) / (2 * 25**2))\ncounts += 180 * amp_decay * mean_secondary\n\n# Poisson-like scatter noise\ncounts += np.random.exponential(1.5, counts.shape)\ncounts = np.round(counts).astype(int)\ncounts = np.clip(counts, 0, None)\ncounts[counts < 2] = 0\n\n# NaN for zero-count bins → transparent (PAGE_BG shows through)\nz_display = counts.astype(float)\nz_display[z_display == 0] = np.nan\n\n# Sqrt-transform for perceptual range enhancement — the 2-600 linear span makes\n# moderate bins indistinguishable; sqrt(2)≈1.4 → sqrt(600)≈24.5 spreads them well.\nz_max = int(np.nanmax(z_display))\nz_plot = np.sqrt(z_display)\n\n# Colorbar ticks: sqrt-transformed positions labeled with original counts\nraw_ticks = [v for v in [2, 25, 100, 200, 400, z_max] if v <= z_max]\ncb_tickvals = [np.sqrt(v) for v in raw_ticks]\ncb_ticktext = [str(v) for v in raw_ticks]\n\nfont_family = \"Palatino, Georgia, serif\"\n\ntitle_text = \"heatmap-rainflow · python · plotly · anyplot.ai\"\nn_chars = len(title_text)\nratio = 67 / n_chars if n_chars > 67 else 1.0\ntitle_size = max(11, round(16 * ratio))\n\n# Plot\nfig = go.Figure(\n    data=go.Heatmap(\n        z=z_plot,\n        x=np.round(mean_centers, 1),\n        y=np.round(amplitude_centers, 1),\n        colorscale=imprint_seq,\n        colorbar={\n            \"title\": {\"text\": \"Cycle Count\", \"font\": {\"size\": 12, \"family\": font_family, \"color\": INK}},\n            \"tickfont\": {\"size\": 10, \"family\": font_family, \"color\": INK_SOFT},\n            \"tickvals\": cb_tickvals,\n            \"ticktext\": cb_ticktext,\n            \"thickness\": 18,\n            \"len\": 0.75,\n            \"outlinewidth\": 0,\n            \"bgcolor\": ELEVATED_BG,\n        },\n        hovertemplate=\"Mean: %{x} MPa<br>Amplitude: %{y} MPa<br>Cycles: %{customdata:.0f}<extra></extra>\",\n        customdata=z_display,\n        xgap=1,\n        ygap=1,\n        connectgaps=False,\n    )\n)\n\n# Annotations — absolute data-coordinate placement (axref/ayref=\"x\"/\"y\") keeps\n# text boxes inside the plot regardless of pixel scaling.\nfig.add_annotation(\n    xref=\"x\",\n    yref=\"y\",\n    axref=\"x\",\n    ayref=\"y\",\n    x=100,\n    y=25,\n    ax=5,\n    ay=148,\n    text=\"<b>Primary service loads</b><br>Low-amplitude cycles<br>dominate count\",\n    showarrow=True,\n    arrowhead=2,\n    arrowsize=1.2,\n    arrowwidth=2,\n    arrowcolor=INK_SOFT,\n    font={\"size\": 13, \"family\": font_family, \"color\": INK},\n    bgcolor=ELEVATED_BG,\n    bordercolor=INK_SOFT,\n    borderpad=5,\n    borderwidth=1,\n)\nfig.add_annotation(\n    xref=\"x\",\n    yref=\"y\",\n    axref=\"x\",\n    ayref=\"y\",\n    x=175,\n    y=20,\n    ax=110,\n    ay=120,\n    text=\"<b>Tensile-dominated</b><br>loading regime\",\n    showarrow=True,\n    arrowhead=2,\n    arrowsize=1.2,\n    arrowwidth=2,\n    arrowcolor=INK_MUTED,\n    font={\"size\": 13, \"family\": font_family, \"color\": INK_MUTED},\n    bgcolor=ELEVATED_BG,\n    bordercolor=INK_SOFT,\n    borderpad=4,\n    borderwidth=1,\n)\n\nfig.update_layout(\n    autosize=False,\n    title={\n        \"text\": title_text,\n        \"font\": {\"size\": title_size, \"family\": font_family, \"color\": INK},\n        \"x\": 0.5,\n        \"xanchor\": \"center\",\n    },\n    xaxis={\n        \"title\": {\"text\": \"Cycle Mean Stress (MPa)\", \"font\": {\"size\": 12, \"family\": font_family, \"color\": INK}},\n        \"tickfont\": {\"size\": 10, \"family\": font_family, \"color\": INK_SOFT},\n        \"gridcolor\": GRID,\n        \"linecolor\": INK_SOFT,\n        \"zerolinecolor\": INK_SOFT,\n    },\n    yaxis={\n        \"title\": {\"text\": \"Cycle Amplitude (MPa)\", \"font\": {\"size\": 12, \"family\": font_family, \"color\": INK}},\n        \"tickfont\": {\"size\": 10, \"family\": font_family, \"color\": INK_SOFT},\n        \"gridcolor\": GRID,\n        \"linecolor\": INK_SOFT,\n        \"zerolinecolor\": INK_SOFT,\n        \"range\": [5, 205],\n    },\n    paper_bgcolor=PAGE_BG,\n    plot_bgcolor=PAGE_BG,\n    font={\"color\": INK},\n    margin={\"l\": 80, \"r\": 60, \"t\": 80, \"b\": 60},\n)\n\n# Save — square canvas (2400×2400) suits the symmetric heatmap grid\nfig.write_image(f\"plot-{THEME}.png\", width=600, height=600, scale=4)\nfig.write_html(f\"plot-{THEME}.html\", include_plotlyjs=\"cdn\")\n"}