{"spec_id":"parallel-basic","library":"bokeh","language":"python","code":"\"\"\" anyplot.ai\nparallel-basic: Basic Parallel Coordinates Plot\nLibrary: bokeh 3.9.1 | Python 3.13.14\nQuality: 94/100 | Updated: 2026-07-24\n\"\"\"\n\nimport os\nimport sys\nimport time\nfrom pathlib import Path\n\n\n_script_dir = os.path.dirname(os.path.abspath(__file__))\nsys.path = [p for p in sys.path if os.path.abspath(p or \".\") != _script_dir]\n\nimport numpy as np\nimport pandas as pd\nfrom bokeh.io import output_file, save\nfrom bokeh.models import ColumnDataSource, HoverTool, Span\nfrom bokeh.plotting import figure\nfrom selenium import webdriver\nfrom selenium.webdriver.chrome.options import Options\n\n\n# Theme tokens (see prompts/default-style-guide.md \"Theme-adaptive Chrome\")\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 palette (position 1 is always the first categorical series)\nIMPRINT_PALETTE = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\", \"#99B314\"]\n\n# Data - Iris-like dataset for multivariate demonstration\nnp.random.seed(42)\n\nn_per_species = 50\n\n# Setosa: small petals, moderate sepals\nsetosa = pd.DataFrame(\n    {\n        \"sepal_length\": np.random.normal(5.0, 0.35, n_per_species),\n        \"sepal_width\": np.random.normal(3.4, 0.38, n_per_species),\n        \"petal_length\": np.random.normal(1.5, 0.17, n_per_species),\n        \"petal_width\": np.random.normal(0.25, 0.10, n_per_species),\n        \"species\": \"setosa\",\n    }\n)\n\n# Versicolor: medium everything\nversicolor = pd.DataFrame(\n    {\n        \"sepal_length\": np.random.normal(5.9, 0.52, n_per_species),\n        \"sepal_width\": np.random.normal(2.8, 0.31, n_per_species),\n        \"petal_length\": np.random.normal(4.3, 0.47, n_per_species),\n        \"petal_width\": np.random.normal(1.3, 0.20, n_per_species),\n        \"species\": \"versicolor\",\n    }\n)\n\n# Virginica: large petals and sepals\nvirginica = pd.DataFrame(\n    {\n        \"sepal_length\": np.random.normal(6.6, 0.64, n_per_species),\n        \"sepal_width\": np.random.normal(3.0, 0.32, n_per_species),\n        \"petal_length\": np.random.normal(5.5, 0.55, n_per_species),\n        \"petal_width\": np.random.normal(2.0, 0.27, n_per_species),\n        \"species\": \"virginica\",\n    }\n)\n\ndf = pd.concat([setosa, versicolor, virginica], ignore_index=True)\n\n# Normalize numeric columns to [0, 1] for fair comparison across axes\nnumeric_cols = [\"sepal_length\", \"sepal_width\", \"petal_length\", \"petal_width\"]\ndf_norm = df.copy()\nfor col in numeric_cols:\n    min_val = df[col].min()\n    max_val = df[col].max()\n    df_norm[col] = (df[col] - min_val) / (max_val - min_val)\n\n# Colors by species (Imprint palette, canonical order)\nspecies_order = [\"setosa\", \"versicolor\", \"virginica\"]\ncolors = dict(zip(species_order, IMPRINT_PALETTE[:3], strict=True))\n\n# One multi-line source: each row of xs/ys is a single observation's polyline\nx_coords = list(range(len(numeric_cols)))\nsource = ColumnDataSource(\n    data={\n        \"xs\": [x_coords] * len(df_norm),\n        \"ys\": df_norm[numeric_cols].values.tolist(),\n        \"species\": df_norm[\"species\"].str.capitalize(),\n        \"color\": [colors[s] for s in df_norm[\"species\"]],\n    }\n)\n\n# Create figure (3200x1800 px landscape canvas)\ntitle = \"parallel-basic · python · bokeh · anyplot.ai\"\np = figure(\n    width=3200,\n    height=1800,\n    title=title,\n    x_axis_label=\"Dimension\",\n    y_axis_label=\"Normalized Value\",\n    x_range=(-0.3, 3.3),\n    y_range=(-0.05, 1.10),\n    toolbar_location=None,  # bokeh's default toolbar adds ~30-50px above the plot\n    min_border_bottom=160,  # room for 34pt x-tick labels + 42pt x-axis label\n    min_border_left=180,  # room for 34pt y-tick labels + 42pt y-axis label\n    min_border_top=110,  # room for 50pt title\n    min_border_right=50,\n)\n\n# Plot parallel coordinates - one polyline per observation, colored by species.\n# line_alpha=0.4 (down from 0.5) eases the densest crossover region (Sepal Width)\n# while still preserving the crossing pattern. muted_alpha lets a legend click\n# isolate a single species - a bokeh-distinctive touch beyond the plain HoverTool.\nrenderer = p.multi_line(\n    xs=\"xs\",\n    ys=\"ys\",\n    source=source,\n    line_color=\"color\",\n    line_alpha=0.4,\n    line_width=2.5,\n    legend_field=\"species\",\n    muted_alpha=0.05,\n)\n\n# Hover shows which species a given line belongs to - bokeh's signature interactive feature\nhover = HoverTool(renderers=[renderer], tooltips=[(\"Species\", \"@species\")], line_policy=\"nearest\")\np.add_tools(hover)\n\n# Vertical axis line per dimension - bolder than the shared 0.15-alpha grid so\n# each of the four parallel-coordinate axes reads as a distinct anchor line.\nfor x in x_coords:\n    p.add_layout(Span(location=x, dimension=\"height\", line_color=INK_SOFT, line_width=2, line_alpha=0.6))\n\np.legend.click_policy = \"mute\"\np.legend.title = \"Species\"\np.legend.location = \"top_right\"\np.legend.label_text_font_size = \"30pt\"\np.legend.title_text_font_size = \"32pt\"\np.legend.background_fill_color = ELEVATED_BG\np.legend.border_line_color = None\np.legend.label_text_color = INK_SOFT\np.legend.title_text_color = INK\n\n# Custom x-axis labels with original scale ranges\naxis_labels = [\n    f\"Sepal Length\\n({df['sepal_length'].min():.1f}-{df['sepal_length'].max():.1f} cm)\",\n    f\"Sepal Width\\n({df['sepal_width'].min():.1f}-{df['sepal_width'].max():.1f} cm)\",\n    f\"Petal Length\\n({df['petal_length'].min():.1f}-{df['petal_length'].max():.1f} cm)\",\n    f\"Petal Width\\n({df['petal_width'].min():.1f}-{df['petal_width'].max():.1f} cm)\",\n]\np.xaxis.ticker = x_coords\np.xaxis.major_label_overrides = dict(enumerate(axis_labels))\n\n# Text sizes and typography for 3200x1800 px canvas - helvetica throughout for\n# a deliberate, publication-grade look rather than bokeh's default font stack\np.title.text_font = \"helvetica\"\np.title.text_font_size = \"50pt\"\np.xaxis.axis_label_text_font = \"helvetica\"\np.yaxis.axis_label_text_font = \"helvetica\"\np.xaxis.axis_label_text_font_size = \"42pt\"\np.yaxis.axis_label_text_font_size = \"42pt\"\np.xaxis.major_label_text_font = \"helvetica\"\np.yaxis.major_label_text_font = \"helvetica\"\np.xaxis.major_label_text_font_size = \"34pt\"\np.yaxis.major_label_text_font_size = \"34pt\"\np.legend.label_text_font = \"helvetica\"\np.legend.title_text_font = \"helvetica\"\n\n# Theme-adaptive chrome\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.xaxis.axis_label_text_color = INK\np.yaxis.axis_label_text_color = INK\np.xaxis.major_label_text_color = INK_SOFT\np.yaxis.major_label_text_color = INK_SOFT\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\n# Grid styling - subtle\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# Save outputs - write HTML then screenshot with headless Chrome (bokeh's export_png\n# is unreliable in this environment; see prompts/library/bokeh.md)\noutput_file(f\"plot-{THEME}.html\", title=title)\nsave(p)\n\nW, H = 3200, 1800\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.set_window_size(W, H)\ndriver.get(f\"file://{Path(f'plot-{THEME}.html').resolve()}\")\n# Headless Chrome's --window-size sets the OUTER window (a phantom ~143px\n# title bar eats into it even headless), so innerHeight ends up short of H.\n# Override the viewport directly via CDP for an exact WxH capture.\ndriver.execute_cdp_cmd(\n    \"Emulation.setDeviceMetricsOverride\", {\"width\": W, \"height\": H, \"deviceScaleFactor\": 1, \"mobile\": False}\n)\ntime.sleep(3)  # let bokeh's JS render the canvas\ndriver.save_screenshot(f\"plot-{THEME}.png\")\ndriver.quit()\n"}