{"spec_id":"quiver-basic","library":"bokeh","language":"python","code":"\"\"\" anyplot.ai\nquiver-basic: Basic Quiver Plot\nLibrary: bokeh 3.9.1 | Python 3.13.14\nQuality: 87/100 | Updated: 2026-07-24\n\"\"\"\n\nimport os\nimport sys\n\n\n# Prevent self-import: this file is named bokeh.py, which shadows the installed\n# bokeh package when its directory sits at the front of sys.path.\n_this_dir = os.path.dirname(os.path.abspath(__file__))\nsys.path = [p for p in sys.path if os.path.abspath(p or \".\") != _this_dir]\n\nimport time\nfrom pathlib import Path\n\nimport numpy as np\nfrom bokeh.io import output_file, save\nfrom bokeh.models import ColorBar, ColumnDataSource, Label, LinearColorMapper, Title\nfrom bokeh.plotting import figure\nfrom selenium import webdriver\nfrom selenium.webdriver.chrome.options import Options\n\n\n# Theme tokens (Imprint)\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\"\n\n\n# Continuous colormap — imprint_seq (single-polarity: wind speed magnitude)\ndef _lerp_hex(c0, c1, t):\n    r0, g0, b0 = (int(c0[i : i + 2], 16) for i in (1, 3, 5))\n    r1, g1, b1 = (int(c1[i : i + 2], 16) for i in (1, 3, 5))\n    r, g, b = (int(round(a + (b - a) * t)) for a, b in ((r0, r1), (g0, g1), (b0, b1)))\n    return f\"#{r:02X}{g:02X}{b:02X}\"\n\n\nIMPRINT_SEQ256 = [_lerp_hex(\"#009E73\", \"#4467A3\", t / 255.0) for t in range(256)]\n\n# Data - counterclockwise vortex wind field: u = -y, v = x\ngrid_size = 15\naxis_coords = np.linspace(-2, 2, grid_size)\nX, Y = np.meshgrid(axis_coords, axis_coords)\nx = X.flatten()\ny = Y.flatten()\n\nu = -y.copy()\nv = x.copy()\n\n# Magnitude-proportional arrow lengths (length encodes wind speed)\nmagnitude = np.sqrt(u**2 + v**2)\n\ngrid_spacing = 4.0 / (grid_size - 1)\nraw_max_mag = np.max(magnitude) if np.max(magnitude) > 0 else 1.0\n\n# Rescale the abstract rotation field into a realistic wind-speed range\n# (km/h) so the \"Wind Speed\" label/legend reads true — the raw u=-y, v=x\n# field only spans 0-2.8, which looks like still air for a field labeled wind.\nWIND_SPEED_MAX = 28.0  # km/h — brisk breeze, plausible peak for the label\nspeed = magnitude * (WIND_SPEED_MAX / raw_max_mag)\nmax_mag = WIND_SPEED_MAX\n\nscale = grid_spacing * 0.65 / raw_max_mag\nu_scaled = u * scale\nv_scaled = v * scale\n\n# Arrow geometry — enforce minimum displayed length so near-origin arrows stay visible\narrow_lengths = np.sqrt(u_scaled**2 + v_scaled**2)\nMIN_DISP = grid_spacing * 0.18\ndisplay_lengths = np.maximum(arrow_lengths, MIN_DISP)\n\n# Unit direction vectors (default to pointing up for zero-magnitude vectors)\nnear_zero = arrow_lengths < 1e-10\nsafe_lengths = np.where(near_zero, 1.0, arrow_lengths)\ndx = np.where(near_zero, 0.0, u_scaled / safe_lengths)\ndy = np.where(near_zero, 1.0, v_scaled / safe_lengths)\nperp_x = -dy\nperp_y = dx\n\nhead_len = display_lengths * 0.30\nhead_wid = display_lengths * 0.35\n\narrow_x_end = x + dx * display_lengths\narrow_y_end = y + dy * display_lengths\narrow_base_x = arrow_x_end - head_len * dx\narrow_base_y = arrow_y_end - head_len * dy\narrow_x1 = arrow_base_x + head_wid * perp_x\narrow_y1 = arrow_base_y + head_wid * perp_y\narrow_x2 = arrow_base_x - head_wid * perp_x\narrow_y2 = arrow_base_y - head_wid * perp_y\n\n# Color by wind speed using the Imprint sequential colormap (green -> blue)\nspeed_norm = speed / max_mag\ncolor_indices = (speed_norm * 255).astype(int).clip(0, 255)\ncolors = [IMPRINT_SEQ256[i] for i in color_indices]\n\n# Canvas — 3200x1800 canonical landscape; toolbar disabled so the static PNG\n# isn't shrunk by bokeh's default toolbar strip (see bokeh.md \"Canvas\" rule).\nW, H = 3200, 1800\np = figure(\n    width=W,\n    height=H,\n    title=\"quiver-basic · python · bokeh · anyplot.ai\",\n    x_axis_label=\"East–West Distance (km)\",\n    y_axis_label=\"North–South Distance (km)\",\n    x_range=(-2.5, 2.5),\n    y_range=(-2.5, 2.5),\n    background_fill_color=PAGE_BG,\n    border_fill_color=PAGE_BG,\n    toolbar_location=None,\n    min_border_bottom=160,\n    min_border_left=180,\n    min_border_top=110,\n    min_border_right=50,\n)\n\n# Subtitle\np.add_layout(\n    Title(\n        text=\"Counterclockwise vortex wind field — colour encodes wind speed\",\n        text_font_size=\"18pt\",\n        text_color=INK_SOFT,\n    ),\n    \"above\",\n)\n\n# Theme-adaptive chrome — spines removed entirely for a clean look against the\n# dense arrow grid (Style guide \"Spines: Alternative — remove all for grid-based plots\")\np.outline_line_color = None\n\np.title.text_color = INK\np.title.text_font_size = \"28pt\"\np.title.text_font_style = \"normal\"\n\np.xaxis.axis_label_text_color = INK\np.yaxis.axis_label_text_color = INK\np.xaxis.axis_label_text_font_size = \"22pt\"\np.yaxis.axis_label_text_font_size = \"22pt\"\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 = None\np.yaxis.axis_line_color = None\np.xaxis.major_tick_line_color = None\np.yaxis.major_tick_line_color = None\np.xaxis.minor_tick_line_color = None\np.yaxis.minor_tick_line_color = None\n\np.xgrid.grid_line_color = INK\np.ygrid.grid_line_color = INK\np.xgrid.grid_line_alpha = 0.15\np.ygrid.grid_line_alpha = 0.15\n\n# Arrow shafts (stop at arrowhead base to avoid shaft poking through head)\nsegment_source = ColumnDataSource(data={\"x0\": x, \"y0\": y, \"x1\": arrow_base_x, \"y1\": arrow_base_y, \"color\": colors})\np.segment(x0=\"x0\", y0=\"y0\", x1=\"x1\", y1=\"y1\", source=segment_source, line_width=4, line_color=\"color\")\n\n# Arrowheads as filled triangles\nxs = [[arrow_x_end[i], arrow_x1[i], arrow_x2[i]] for i in range(len(x))]\nys = [[arrow_y_end[i], arrow_y1[i], arrow_y2[i]] for i in range(len(y))]\npatch_source = ColumnDataSource(data={\"xs\": xs, \"ys\": ys, \"color\": colors})\np.patches(xs=\"xs\", ys=\"ys\", source=patch_source, fill_color=\"color\", line_color=\"color\")\n\n# Vortex centre marker and annotation\np.scatter([0], [0], marker=\"circle_dot\", size=20, color=INK_MUTED, fill_alpha=0.85, line_color=INK_SOFT)\np.add_layout(\n    Label(\n        x=0,\n        y=0,\n        text=\"vortex centre\",\n        x_offset=20,\n        y_offset=10,\n        text_font_size=\"16pt\",\n        text_color=INK_MUTED,\n        background_fill_color=None,\n        border_line_color=None,\n    )\n)\n\n# Max-speed annotation — all four corners tie for the highest wind speed\n# (magnitude depends only on radius). Anchored at the bottom-right corner,\n# where the tangential flow (u=-y, v=x) points up-and-away from the margin\n# below the grid, so the label sits in clear space with no arrow overlap.\np.add_layout(\n    Label(\n        x=2.0,\n        y=-2.0,\n        text=f\"max {max_mag:.1f} km/h at corners\",\n        x_offset=-90,\n        y_offset=-45,\n        text_font_size=\"16pt\",\n        text_color=INK_MUTED,\n        background_fill_color=None,\n        border_line_color=None,\n    )\n)\n\n# ColorBar for wind speed legend\ncolor_mapper = LinearColorMapper(palette=IMPRINT_SEQ256, low=0.0, high=float(max_mag))\ncolor_bar = ColorBar(\n    color_mapper=color_mapper,\n    label_standoff=16,\n    location=(0, 0),\n    title=\"Wind Speed\",\n    title_text_color=INK_SOFT,\n    title_text_font_size=\"18pt\",\n    major_label_text_color=INK_SOFT,\n    major_label_text_font_size=\"16pt\",\n    background_fill_color=ELEVATED_BG,\n    border_line_color=INK_SOFT,\n)\np.add_layout(color_bar, \"right\")\n\n# Save interactive HTML\noutput_file(f\"plot-{THEME}.html\", title=\"quiver-basic · bokeh · anyplot.ai\")\nsave(p)\n\n# Screenshot via Selenium headless Chrome — matches bokeh.md pattern\n# (bokeh.io.export_png probes /usr/bin/chromedriver via a snap shim that fails\n# in this environment; Selenium Manager resolves a working driver instead)\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)\n# CDP override forces an exact W×H viewport regardless of outer window chrome\ndriver.execute_cdp_cmd(\n    \"Emulation.setDeviceMetricsOverride\", {\"width\": W, \"height\": H, \"deviceScaleFactor\": 1, \"mobile\": False}\n)\ndriver.get(f\"file://{Path(f'plot-{THEME}.html').resolve()}\")\ntime.sleep(3)  # let bokeh's JS render the canvas\ndriver.save_screenshot(f\"plot-{THEME}.png\")\ndriver.quit()\n"}