{"spec_id":"pyramid-basic","library":"bokeh","language":"python","code":"\"\"\" anyplot.ai\npyramid-basic: Basic Pyramid Chart\nLibrary: bokeh 3.9.0 | Python 3.13.13\nQuality: 90/100 | Updated: 2026-04-29\n\"\"\"\n\nimport os\n\nfrom bokeh.io import export_png, output_file, save\nfrom bokeh.models import ColumnDataSource, CustomJSTickFormatter, HoverTool, Range1d, Span\nfrom bokeh.plotting import figure\n\n\n# Theme tokens\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\"\n\nMALE_COLOR = \"#009E73\"  # Okabe-Ito position 1\nFEMALE_COLOR = \"#C475FD\"  # Okabe-Ito position 2\n\n# Data - Population by age group (in thousands)\nage_groups = [\"0-9\", \"10-19\", \"20-29\", \"30-39\", \"40-49\", \"50-59\", \"60-69\", \"70-79\", \"80+\"]\nmale_population = [45, 52, 68, 82, 75, 65, 48, 32, 15]\nfemale_population = [43, 50, 72, 85, 78, 70, 55, 40, 22]\nmale_negative = [-v for v in male_population]\n\nsource_male = ColumnDataSource(data={\"age\": age_groups, \"population\": male_negative, \"value\": male_population})\nsource_female = ColumnDataSource(data={\"age\": age_groups, \"population\": female_population, \"value\": female_population})\n\n# Plot\np = figure(\n    width=4800,\n    height=2700,\n    y_range=age_groups,\n    x_range=Range1d(-100, 100),\n    title=\"pyramid-basic · bokeh · anyplot.ai\",\n    x_axis_label=\"Population (thousands)\",\n    y_axis_label=\"Age Group\",\n)\n\nbar_height = 0.7\nmale_bars = p.hbar(\n    y=\"age\",\n    right=\"population\",\n    height=bar_height,\n    source=source_male,\n    color=MALE_COLOR,\n    alpha=0.85,\n    legend_label=\"Male\",\n)\nfemale_bars = p.hbar(\n    y=\"age\",\n    right=\"population\",\n    height=bar_height,\n    source=source_female,\n    color=FEMALE_COLOR,\n    alpha=0.85,\n    legend_label=\"Female\",\n)\n\n# HoverTools for interactivity\np.add_tools(HoverTool(renderers=[male_bars], tooltips=[(\"Age Group\", \"@age\"), (\"Male\", \"@value{0,0} thousand\")]))\np.add_tools(HoverTool(renderers=[female_bars], tooltips=[(\"Age Group\", \"@age\"), (\"Female\", \"@value{0,0} thousand\")]))\n\n# Center line at x=0 via Span — adapts to full plot height automatically\ncenter_line = Span(location=0, dimension=\"height\", line_color=INK_SOFT, line_width=2, line_alpha=0.5)\np.add_layout(center_line)\n\n# Show absolute values on x-axis (both sides positive)\np.xaxis.formatter = CustomJSTickFormatter(code=\"return Math.abs(tick).toString()\")\n\n# Style\np.background_fill_color = PAGE_BG\np.border_fill_color = PAGE_BG\np.outline_line_color = INK_SOFT\n\np.title.text_color = INK\np.title.text_font_size = \"32pt\"\np.xaxis.axis_label_text_color = INK\np.yaxis.axis_label_text_color = INK\np.xaxis.axis_label_text_font_size = \"24pt\"\np.yaxis.axis_label_text_font_size = \"24pt\"\np.xaxis.major_label_text_color = INK_SOFT\np.yaxis.major_label_text_color = INK_SOFT\np.xaxis.major_label_text_font_size = \"18pt\"\np.yaxis.major_label_text_font_size = \"18pt\"\np.xaxis.axis_line_color = INK_SOFT\np.yaxis.axis_line_color = INK_SOFT\np.xaxis.major_tick_line_color = INK_SOFT\np.yaxis.major_tick_line_color = INK_SOFT\n\np.xgrid.grid_line_color = INK\np.ygrid.grid_line_color = INK\np.xgrid.grid_line_alpha = 0.10\np.ygrid.grid_line_alpha = 0.10\np.xgrid.grid_line_dash = [6, 4]\np.ygrid.grid_line_dash = [6, 4]\n\np.legend.location = \"bottom_right\"\np.legend.background_fill_color = ELEVATED_BG\np.legend.border_line_color = INK_SOFT\np.legend.label_text_color = INK_SOFT\np.legend.label_text_font_size = \"18pt\"\np.legend.background_fill_alpha = 0.9\n\n# Save\nexport_png(p, filename=f\"plot-{THEME}.png\")\noutput_file(f\"plot-{THEME}.html\")\nsave(p)\n"}