{"spec_id":"gauge-basic","library":"bokeh","language":"python","code":"\"\"\" anyplot.ai\ngauge-basic: Basic Gauge Chart\nLibrary: bokeh 3.9.1 | Python 3.13.14\nQuality: 89/100 | Updated: 2026-06-30\n\"\"\"\n\nimport os\nimport sys\nimport time\nfrom pathlib import Path\n\n\n# Remove script's own directory from sys.path to prevent self-shadowing\n# (this file is named bokeh.py; without this, `import bokeh` would find itself)\n_here = os.path.dirname(os.path.abspath(__file__))\nsys.path = [p for p in sys.path if os.path.abspath(p or \".\") != _here]\n\nimport numpy as np\nfrom bokeh.io import output_file, save\nfrom bokeh.models import ColumnDataSource, HoverTool, Label\nfrom bokeh.plotting import figure\nfrom selenium import webdriver\nfrom selenium.webdriver.chrome.options import Options\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\n# Imprint zone colors — semantic convention: red=bad, amber=caution, green=good\nZONE_LOW = \"#AE3030\"  # Imprint matte red\nZONE_MID = \"#DDCC77\"  # Imprint amber\nZONE_HIGH = \"#009E73\"  # Imprint brand green\n\n# Data — CPU utilization\nvalue = 72\nmin_value = 0\nmax_value = 100\nthresholds = [30, 70]\n\n# Gauge geometry\ncenter_x, center_y = 0.0, 0.0\nouter_radius = 0.95\ninner_radius = 0.62\narc_mid_radius = (inner_radius + outer_radius) / 2  # label midpoint inside arc\nneedle_length = 0.86\nstart_angle = np.pi  # leftmost position (180°)\n\n# Map data values onto the semi-circle (pi → 0 radians)\nzone_bounds = np.array([min_value] + thresholds + [max_value])\nzone_angles = start_angle - (zone_bounds - min_value) / (max_value - min_value) * np.pi\n\ntick_values = np.array([0, 25, 50, 75, 100])\ntick_angles = start_angle - (tick_values - min_value) / (max_value - min_value) * np.pi\n\nneedle_angle = start_angle - (value - min_value) / (max_value - min_value) * np.pi\n\n# Figure — landscape canvas; toolbar_location=None prevents height bloat in screenshot\nW, H = 3200, 1800\np = figure(\n    width=W,\n    height=H,\n    title=\"gauge-basic · python · bokeh · anyplot.ai\",\n    x_range=(-1.5, 1.5),\n    y_range=(-0.58, 1.32),\n    tools=\"\",\n    toolbar_location=None,\n    background_fill_color=PAGE_BG,\n    border_fill_color=PAGE_BG,\n    outline_line_color=None,\n    min_border_bottom=80,\n    min_border_left=80,\n    min_border_top=120,\n    min_border_right=80,\n)\np.axis.visible = False\np.grid.visible = False\np.outline_line_color = None\n\np.title.text_font_size = \"50pt\"\np.title.text_color = INK\np.title.align = \"center\"\n\n# Zone arcs via annular_wedge — ColumnDataSource enables HoverTool\nzone_colors = [ZONE_LOW, ZONE_MID, ZONE_HIGH]\nzone_names = [\"Low\", \"Caution\", \"Optimal\"]\nzone_ranges = [\"0–30\", \"30–70\", \"70–100\"]\n\nzone_source = ColumnDataSource(\n    data={\n        \"x\": [center_x, center_x, center_x],\n        \"y\": [center_y, center_y, center_y],\n        \"inner_radius\": [inner_radius, inner_radius, inner_radius],\n        \"outer_radius\": [outer_radius, outer_radius, outer_radius],\n        \"start_angle\": [float(zone_angles[1]), float(zone_angles[2]), float(zone_angles[3])],\n        \"end_angle\": [float(zone_angles[0]), float(zone_angles[1]), float(zone_angles[2])],\n        \"fill_color\": zone_colors,\n        \"zone\": zone_names,\n        \"range\": zone_ranges,\n    }\n)\n\nzone_renderer = p.annular_wedge(\n    x=\"x\",\n    y=\"y\",\n    inner_radius=\"inner_radius\",\n    outer_radius=\"outer_radius\",\n    start_angle=\"start_angle\",\n    end_angle=\"end_angle\",\n    fill_color=\"fill_color\",\n    line_color=PAGE_BG,\n    line_width=4,\n    source=zone_source,\n)\n\np.add_tools(HoverTool(renderers=[zone_renderer], tooltips=[(\"Zone\", \"@zone\"), (\"Range\", \"@range\")]))\n\n# Zone labels inside each arc segment — contrasting fixed colors (zones are theme-invariant)\n# red/green zones get light text; amber zone gets dark text for contrast\nzone_mid_values = [15.0, 50.0, 85.0]\nzone_label_colors = [\"#FFFDF6\", \"#1A1A17\", \"#FFFDF6\"]\n\nfor mid_val, label_text, label_color in zip(zone_mid_values, zone_names, zone_label_colors, strict=True):\n    a = start_angle - (mid_val / max_value) * np.pi\n    lx = arc_mid_radius * np.cos(a)\n    ly = arc_mid_radius * np.sin(a)\n    p.add_layout(\n        Label(\n            x=lx,\n            y=ly,\n            text=label_text,\n            text_font_size=\"22pt\",\n            text_color=label_color,\n            text_align=\"center\",\n            text_baseline=\"middle\",\n            angle=float(a - np.pi / 2),  # tangent along arc for natural text flow\n        )\n    )\n\n# Tick marks and labels\nfor tick_val, a in zip(tick_values, tick_angles, strict=True):\n    cos_a, sin_a = np.cos(a), np.sin(a)\n\n    p.line(\n        [center_x + (outer_radius + 0.02) * cos_a, center_x + (outer_radius + 0.10) * cos_a],\n        [center_y + (outer_radius + 0.02) * sin_a, center_y + (outer_radius + 0.10) * sin_a],\n        line_color=INK_SOFT,\n        line_width=4,\n    )\n\n    p.add_layout(\n        Label(\n            x=center_x + (outer_radius + 0.22) * cos_a,\n            y=center_y + (outer_radius + 0.22) * sin_a,\n            text=str(tick_val),\n            text_font_size=\"31pt\",\n            text_color=INK_SOFT,\n            text_align=\"center\",\n            text_baseline=\"middle\",\n        )\n    )\n\n# Needle (triangle pointing to current value)\nneedle_tip_x = center_x + needle_length * np.cos(needle_angle)\nneedle_tip_y = center_y + needle_length * np.sin(needle_angle)\nhalf_base = 0.035\nperp = needle_angle + np.pi / 2\nbase1_x = center_x + half_base * np.cos(perp)\nbase1_y = center_y + half_base * np.sin(perp)\nbase2_x = center_x - half_base * np.cos(perp)\nbase2_y = center_y - half_base * np.sin(perp)\n\np.patch(\n    [base1_x, needle_tip_x, base2_x], [base1_y, needle_tip_y, base2_y], fill_color=INK, line_color=INK, line_width=2\n)\n\n# Center hub\np.scatter(x=[center_x], y=[center_y], size=55, marker=\"circle\", fill_color=INK, line_color=PAGE_BG, line_width=4)\n\n# Value display\np.add_layout(\n    Label(\n        x=center_x,\n        y=-0.20,\n        text=str(value),\n        text_font_size=\"80pt\",\n        text_color=INK,\n        text_align=\"center\",\n        text_baseline=\"middle\",\n        text_font_style=\"bold\",\n    )\n)\n\n# Metric label\np.add_layout(\n    Label(\n        x=center_x,\n        y=-0.44,\n        text=\"CPU Utilization (%)\",\n        text_font_size=\"30pt\",\n        text_color=INK_SOFT,\n        text_align=\"center\",\n        text_baseline=\"middle\",\n    )\n)\n\n# Save interactive HTML\nhtml_path = Path(f\"plot-{THEME}.html\")\noutput_file(str(html_path))\nsave(p)\n\n# Inject body background CSS to prevent thin border artifact in headless-Chrome screenshot\nhtml_content = html_path.read_text()\nbody_style = f\"<style>body{{margin:0;padding:0;background:{PAGE_BG};}}</style>\"\nhtml_content = html_content.replace(\"</head>\", f\"{body_style}\\n</head>\", 1)\nhtml_path.write_text(html_content)\n\n# Screenshot via headless Chrome — use CDP to set exact viewport to match figure dimensions\nopts = Options()\nfor arg in (\n    \"--headless=new\",\n    \"--no-sandbox\",\n    \"--disable-dev-shm-usage\",\n    \"--disable-gpu\",\n    f\"--window-size={W},{H}\",\n    \"--hide-scrollbars\",\n):\n    opts.add_argument(arg)\ndriver = webdriver.Chrome(options=opts)\ndriver.execute_cdp_cmd(\n    \"Emulation.setDeviceMetricsOverride\", {\"width\": W, \"height\": H, \"deviceScaleFactor\": 1, \"mobile\": False}\n)\ndriver.get(f\"file://{html_path.resolve()}\")\ntime.sleep(3)\ndriver.save_screenshot(f\"plot-{THEME}.png\")\ndriver.quit()\n"}