{"spec_id":"bode-basic","library":"makie","language":"julia","code":"# anyplot.ai\n# bode-basic: Bode Plot for Frequency Response\n# Library: makie 0.22.10 | Julia 1.11.9\n# Quality: 89/100 | Created: 2026-06-17\n\nusing CairoMakie\nusing Colors\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\nconst IMPRINT = [\n    colorant\"#009E73\",  # 1 — brand green (first series)\n    colorant\"#C475FD\",  # 2 — lavender\n    colorant\"#4467A3\",  # 3 — blue\n    colorant\"#BD8233\",  # 4 — ochre\n    colorant\"#AE3030\",  # 5 — matte red\n    colorant\"#2ABCCD\",  # 6 — cyan\n    colorant\"#954477\",  # 7 — rose\n    colorant\"#99B314\",  # 8 — lime\n]\n\n# Data — open-loop transfer function: H(s) = K / (s(τ₁s+1)(τ₂s+1))\n# Type-1 third-order system; phase crosses -180° at ω_pc = 1/√(τ₁τ₂)\nconst K_gain = 10.0\nconst τ₁     = 0.1    # primary lag, s\nconst τ₂     = 0.01   # secondary lag, s\n\nfreq_hz = 10.0 .^ range(-2.0, 4.0; length = 600)  # 0.01–10000 Hz per spec\nω = 2π .* freq_hz                                   # rad/s for transfer function\n\nmagnitude_db = 20.0 .* log10.(\n    K_gain ./ (ω .* sqrt.(1.0 .+ (τ₁ .* ω).^2) .* sqrt.(1.0 .+ (τ₂ .* ω).^2))\n)\nphase_deg = -90.0 .- rad2deg.(atan.(τ₁ .* ω)) .- rad2deg.(atan.(τ₂ .* ω))\n\n# Stability margins\npc_idx     = findfirst(phase_deg .<= -180.0)\ngc_idx     = findfirst(magnitude_db .<= 0.0)\ngm_db      = isnothing(pc_idx) ? Inf : -magnitude_db[pc_idx]\npm_deg_val = isnothing(gc_idx) ? Inf : 180.0 + phase_deg[gc_idx]\n\n# Figure\nfig = Figure(\n    size            = (1600, 900),\n    fontsize        = 14,\n    backgroundcolor = PAGE_BG,\n)\n\naxis_style = (\n    backgroundcolor    = PAGE_BG,\n    topspinevisible    = false,\n    rightspinevisible  = false,\n    leftspinecolor     = INK_SOFT,\n    bottomspinecolor   = INK_SOFT,\n    xtickcolor         = INK_SOFT,\n    ytickcolor         = INK_SOFT,\n    xticklabelcolor    = INK_SOFT,\n    yticklabelcolor    = INK_SOFT,\n    xlabelcolor        = INK,\n    ylabelcolor        = INK,\n    xticklabelsize     = 12,\n    yticklabelsize     = 12,\n    xlabelsize         = 14,\n    ylabelsize         = 14,\n    xscale             = log10,\n    xgridcolor         = RGBAf(INK.r, INK.g, INK.b, 0.15f0),\n    ygridcolor         = RGBAf(INK.r, INK.g, INK.b, 0.15f0),\n    xgridvisible       = true,\n    ygridvisible       = true,\n    xminorgridvisible  = true,\n    yminorgridvisible  = false,\n    xminorgridcolor    = RGBAf(INK.r, INK.g, INK.b, 0.07f0),\n)\n\nax_mag = Axis(fig[1, 1]; axis_style...,\n    ylabel             = \"Magnitude (dB)\",\n    xticklabelsvisible = false,\n)\n\nax_phase = Axis(fig[2, 1]; axis_style...,\n    xlabel = \"Frequency (Hz)\",\n    ylabel = \"Phase (°)\",\n)\n\nlinkxaxes!(ax_mag, ax_phase)\n\n# Magnitude plot\nlines!(ax_mag, freq_hz, magnitude_db; color = IMPRINT[1], linewidth = 2.5)\nhlines!(ax_mag, [0.0]; color = INK_SOFT, linewidth = 1.2, linestyle = :dash)\n\n# Phase plot\nlines!(ax_phase, freq_hz, phase_deg; color = IMPRINT[1], linewidth = 2.5)\nhlines!(ax_phase, [-180.0]; color = INK_SOFT, linewidth = 1.2, linestyle = :dash)\n\n# Gain margin — phase crossover frequency (blue)\nif !isnothing(pc_idx)\n    vlines!(ax_mag,   [freq_hz[pc_idx]]; color = IMPRINT[3], linewidth = 1.5, linestyle = :dot)\n    vlines!(ax_phase, [freq_hz[pc_idx]]; color = IMPRINT[3], linewidth = 1.5, linestyle = :dot)\n    text!(ax_mag, freq_hz[pc_idx] * 2.0, magnitude_db[pc_idx] / 2.0;\n        text     = \"GM ≈ $(round(gm_db; digits = 1)) dB\",\n        color    = IMPRINT[3],\n        fontsize = 13,\n    )\nend\n\n# Phase margin — gain crossover frequency (matte red)\nif !isnothing(gc_idx)\n    vlines!(ax_mag,   [freq_hz[gc_idx]]; color = IMPRINT[5], linewidth = 1.5, linestyle = :dot)\n    vlines!(ax_phase, [freq_hz[gc_idx]]; color = IMPRINT[5], linewidth = 1.5, linestyle = :dot)\n    text!(ax_phase, freq_hz[gc_idx] * 2.0, phase_deg[gc_idx] + 20.0;\n        text     = \"PM ≈ $(round(pm_deg_val; digits = 1))°\",\n        color    = IMPRINT[5],\n        fontsize = 13,\n    )\nend\n\n# Figure-level title\nLabel(fig[0, 1], \"bode-basic · julia · makie · anyplot.ai\";\n    fontsize  = 20,\n    color     = INK,\n    tellwidth = false,\n)\n\nrowgap!(fig.layout, 1, 10)\n\nsave(joinpath(@__DIR__, \"plot-$(THEME).png\"), fig; px_per_unit = 2)\n"}