{"spec_id":"quiver-basic","library":"pygal","language":"python","code":"\"\"\" anyplot.ai\nquiver-basic: Basic Quiver Plot\nLibrary: pygal 3.1.3 | Python 3.13.14\nQuality: 86/100 | Updated: 2026-07-24\n\"\"\"\n\nimport importlib\nimport os\nimport sys\nfrom itertools import chain\n\nimport numpy as np\n\n\n# Remove script dir so 'pygal' resolves to the installed package, not this file\n_d = os.path.abspath(os.path.dirname(__file__))\nsys.path = [p for p in sys.path if os.path.abspath(p) != _d]\nos.chdir(_d)\n\npygal = importlib.import_module(\"pygal\")\nStyle = importlib.import_module(\"pygal.style\").Style\n\n# Theme tokens\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\n\nIMPRINT = (\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\")\n\n# Data — counterclockwise wind rotation around a low-pressure centre (u=-y, v=x)\nnp.random.seed(42)\ngrid_size = 10  # 10×10 = 100 arrows, matches the spec's suggested density floor\nx_range = np.linspace(-3, 3, grid_size)\ny_range = np.linspace(-3, 3, grid_size)\nX, Y = np.meshgrid(x_range, y_range)\nx_flat = X.flatten()\ny_flat = Y.flatten()\n\nU = -y_flat\nV = x_flat\nmagnitude = np.sqrt(U**2 + V**2)\nmax_mag = magnitude.max()\nnorm_mag = magnitude / max_mag\n\n# Scaled down from the 8×8 layout in proportion to the tighter grid spacing\n# (6/7 -> 6/9) so the longest arrows still clear their neighbours.\narrow_scale = 0.17\nmin_arrow_len = 0.23  # floor so near-centre (low-magnitude) arrows stay visible\n\nhead_ratio = 0.40\nhead_angle = 0.55\n\nnum_bins = 3\nwind_labels = [\"Calm / Light\", \"Moderate\", \"Fresh / Strong\"]\nbin_colors = IMPRINT[:num_bins]\n\n# Build each arrow as an isolated 9-item segment group, collected per bin\narrow_bins = [[] for _ in range(num_bins)]\nfor i in range(len(x_flat)):\n    if magnitude[i] < 0.01:\n        continue\n    x1, y1 = x_flat[i], y_flat[i]\n    arrow_len = max(magnitude[i] * arrow_scale, min_arrow_len)\n    angle = np.arctan2(V[i], U[i])\n    x2 = x1 + arrow_len * np.cos(angle)\n    y2 = y1 + arrow_len * np.sin(angle)\n    head_size = arrow_len * head_ratio\n    xl = x2 - head_size * np.cos(angle - head_angle)\n    yl = y2 - head_size * np.sin(angle - head_angle)\n    xr = x2 - head_size * np.cos(angle + head_angle)\n    yr = y2 - head_size * np.sin(angle + head_angle)\n    bin_idx = min(int(norm_mag[i] * num_bins), num_bins - 1)\n    # Each arrow = shaft + two barb segments, each terminated with None\n    arrow_bins[bin_idx].append([(x1, y1), (x2, y2), None, (x2, y2), (xl, yl), None, (x2, y2), (xr, yr), None])\n\n# Shuffle arrow order within each bin to break the spatial row-order band patterns\n# that make consecutive arrows appear visually connected even with None breaks\nrng = np.random.RandomState(42)\narrow_series = []\nfor i in range(num_bins):\n    arrows = arrow_bins[i][:]\n    rng.shuffle(arrows)\n    arrow_series.append(list(chain.from_iterable(arrows)))\n\n# Style — sizes are the pygal canonical values for a 3200×1800 canvas\n# (prompts/library/pygal.md \"Sizing + Theme\")\ncustom_style = Style(\n    background=PAGE_BG,\n    plot_background=PAGE_BG,\n    foreground=INK,\n    foreground_strong=INK,\n    foreground_subtle=INK_MUTED,\n    colors=bin_colors,\n    title_font_size=66,\n    label_font_size=56,\n    major_label_font_size=44,\n    legend_font_size=44,\n    value_font_size=36,\n    stroke_width=2.5,\n)\n\n# Plot — thin strokes + dot markers at each segment endpoint clearly\n# distinguish 100 discrete arrow positions rather than sweeping bands\nchart = pygal.XY(\n    style=custom_style,\n    width=3200,\n    height=1800,\n    stroke=True,\n    stroke_style={\"width\": 7},\n    show_dots=True,\n    dot_size=3,\n    show_legend=True,\n    legend_at_bottom=True,\n    legend_at_bottom_columns=3,\n    title=\"quiver-basic · python · pygal · anyplot.ai\",\n    x_title=\"Longitude (degrees)\",\n    y_title=\"Latitude (degrees)\",\n    show_x_guides=True,\n    show_y_guides=True,\n    range=(-3.6, 3.6),\n    xrange=(-3.6, 3.6),\n)\n\nfor i in range(num_bins):\n    if arrow_series[i]:\n        chart.add(wind_labels[i], arrow_series[i], allow_interruptions=True)\n\n# Save\nchart.render_to_png(f\"plot-{THEME}.png\")\nwith open(f\"plot-{THEME}.html\", \"wb\") as f:\n    f.write(chart.render())\n"}