{"spec_id":"heatmap-rainflow","library":"makie","language":"julia","code":"# anyplot.ai\n# heatmap-rainflow: Rainflow Counting Matrix for Fatigue Analysis\n# Library: makie 0.22.10 | Julia 1.11.9\n# Quality: 85/100 | Created: 2026-06-02\n\nusing CairoMakie\nusing Colors\nusing Random\n\nRandom.seed!(42)\n\n# Theme tokens\nconst THEME    = get(ENV, \"ANYPLOT_THEME\", \"light\")\nconst PAGE_BG  = THEME == \"light\" ? colorant\"#FAF8F1\" : colorant\"#1A1A17\"\nconst INK      = THEME == \"light\" ? colorant\"#1A1A17\" : colorant\"#F0EFE8\"\nconst INK_SOFT = THEME == \"light\" ? colorant\"#4A4A44\" : colorant\"#B8B7B0\"\n\n# Imprint sequential colormap for single-polarity continuous data\nconst ANYPLOT_SEQ = cgrad([colorant\"#009E73\", colorant\"#4467A3\"])\n\n# Data: variable-amplitude bridge loading — 200 000 cycles in a 20×20 rainflow matrix\nn_amp  = 20\nn_mean = 20\n\namp_min  = 0.0;   amp_max  = 200.0   # MPa\nmean_min = -50.0; mean_max = 250.0   # MPa\n\namp_step  = (amp_max  - amp_min)  / n_amp\nmean_step = (mean_max - mean_min) / n_mean\n\namp_centers  = [amp_min  + (i - 0.5) * amp_step  for i in 1:n_amp]\nmean_centers = [mean_min + (i - 0.5) * mean_step for i in 1:n_mean]\n\nn_cycles = 200_000\n# Exponential amplitude distribution: physically realistic (many small, few large cycles)\namp_raw  = clamp.(-35.0 .* log.(rand(n_cycles)), amp_min + 1e-6, amp_max - 1e-6)\n# Gaussian mean stress centred at 100 MPa (σ = 45 MPa)\nmean_raw = clamp.(randn(n_cycles) .* 45.0 .+ 100.0, mean_min + 1e-6, mean_max - 1e-6)\n\namp_idx  = clamp.(ceil.(Int, (amp_raw  .- amp_min)  ./ amp_step),  1, n_amp)\nmean_idx = clamp.(ceil.(Int, (mean_raw .- mean_min) ./ mean_step), 1, n_mean)\n\ncount_matrix = zeros(Int, n_amp, n_mean)\nfor k in 1:n_cycles\n    count_matrix[amp_idx[k], mean_idx[k]] += 1\nend\n\n# Log10 transform; zero-count bins → NaN (rendered as background color)\ncount_display = Float64.(count_matrix)\ncount_display[count_matrix .== 0] .= NaN\ncount_log = log10.(count_display)\n\n# Contour matrix: replace NaN with 0.0 so iso-lines trace the data boundary cleanly\ncount_log_contour = ifelse.(isnan.(count_log), 0.0, count_log)\n\nvalid_log = filter(!isnan, vec(count_log))\nmax_log   = isempty(valid_log) ? 1.0 : maximum(valid_log)\n\n# Colorbar ticks at decade values within the data range\nall_cb_counts = [1, 10, 100, 1_000, 10_000]\nall_cb_vals   = log10.(Float64.(all_cb_counts))\nkeep          = all_cb_vals .<= max_log + 0.05\ncb_vals       = all_cb_vals[keep]\ncb_labels     = string.(all_cb_counts[keep])\n\n# Centroid of the dominant fatigue zone (bins with ≥ 1 000 cycles)\nhigh_mask = count_matrix .>= 1_000\ncx_ann = 100.0  # default: Gaussian mean centre\ncy_ann = 30.0   # default: low-amplitude region\nif any(high_mask)\n    total_w = Float64(sum(count_matrix[high_mask]))\n    cx_ann  = sum(count_matrix[i, j] * mean_centers[j]\n                  for i in 1:n_amp, j in 1:n_mean if high_mask[i, j]) / total_w\n    cy_ann  = sum(count_matrix[i, j] * amp_centers[i]\n                  for i in 1:n_amp, j in 1:n_mean if high_mask[i, j]) / total_w\nend\n\n# Figure: square canvas — 1200×1200 → 2400×2400 at px_per_unit=2\nfig = Figure(\n    size            = (1200, 1200),\n    fontsize        = 14,\n    backgroundcolor = PAGE_BG,\n)\n\nax = Axis(\n    fig[1, 1];\n    title              = \"Fatigue Spectrum · heatmap-rainflow · julia · makie · anyplot.ai\",\n    titlesize          = 19,\n    titlecolor         = INK,\n    xlabel             = \"Mean Stress (MPa)\",\n    ylabel             = \"Cycle Amplitude (MPa)\",\n    xlabelsize         = 16,\n    ylabelsize         = 16,\n    xticklabelsize     = 13,\n    yticklabelsize     = 13,\n    backgroundcolor    = PAGE_BG,\n    topspinevisible    = false,\n    rightspinevisible  = false,\n    leftspinecolor     = INK_SOFT,\n    bottomspinecolor   = INK_SOFT,\n    xticklabelcolor    = INK_SOFT,\n    yticklabelcolor    = INK_SOFT,\n    xtickcolor         = INK_SOFT,\n    ytickcolor         = INK_SOFT,\n    xlabelcolor        = INK,\n    ylabelcolor        = INK,\n    xgridvisible       = false,\n    ygridvisible       = false,\n)\n\n# heatmap!(ax, x, y, z): x → mean (cols), y → amplitude (rows); z must be (n_mean × n_amp)\nhm = heatmap!(\n    ax,\n    mean_centers,\n    amp_centers,\n    count_log';\n    colormap   = ANYPLOT_SEQ,\n    nan_color  = PAGE_BG,\n    colorrange = (0.0, max_log),\n)\n\n# Contour iso-lines at 100 and 1 000 cycles add visual hierarchy over the heatmap\ncontour!(\n    ax,\n    mean_centers,\n    amp_centers,\n    count_log_contour';\n    levels    = [2.0, 3.0],\n    color     = INK_SOFT,\n    linewidth = 1.5,\n)\n\n# Annotate the dominant fatigue region centroid\ntext!(\n    ax,\n    cx_ann, cy_ann + 22.0;\n    text     = \"Dominant fatigue\\nregion (>1k cycles/bin)\",\n    fontsize = 11,\n    color    = INK,\n    align    = (:center, :bottom),\n)\n\ncb = Colorbar(\n    fig[1, 2],\n    hm;\n    label          = \"Cycle Count\",\n    labelsize      = 16,\n    labelcolor     = INK,\n    ticklabelsize  = 13,\n    ticklabelcolor = INK_SOFT,\n    tickcolor      = INK_SOFT,\n    ticks          = (cb_vals, cb_labels),\n)\n\ncolgap!(fig.layout, 12)\n\nsave(\"plot-$(THEME).png\", fig; px_per_unit = 2)\n"}