{"spec_id":"andrews-curves","library":"pygal","language":"python","code":"\"\"\" anyplot.ai\nandrews-curves: Andrews Curves for Multivariate Data\nLibrary: pygal 3.1.0 | Python 3.13.13\nQuality: 88/100 | Updated: 2026-05-15\n\"\"\"\n\nimport os\nimport sys\n\n\n# Remove current directory from sys.path to avoid shadowing the pygal module\n_cwd = sys.path[0] if sys.path[0] else \".\"\nif _cwd in sys.path:\n    sys.path.remove(_cwd)\n\nimport numpy as np\nimport pygal\nfrom pygal.style import Style\n\n\n# Restore current directory\nsys.path.insert(0, _cwd)\n\n\n# Theme configuration\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\n# Okabe-Ito palette (first series always #009E73)\nIMPRINT = (\"#009E73\", \"#C475FD\", \"#4467A3\")\n\n# Data\nnp.random.seed(42)\n\n# Simulate iris-like data (4 features, 3 species)\n# Species 1: Setosa - small petals, medium sepals\nsetosa = np.column_stack(\n    [\n        np.random.normal(5.0, 0.35, 50),  # sepal length\n        np.random.normal(3.4, 0.38, 50),  # sepal width\n        np.random.normal(1.5, 0.17, 50),  # petal length\n        np.random.normal(0.2, 0.10, 50),  # petal width\n    ]\n)\n\n# Species 2: Versicolor - medium petals and sepals\nversicolor = np.column_stack(\n    [\n        np.random.normal(5.9, 0.52, 50),  # sepal length\n        np.random.normal(2.8, 0.31, 50),  # sepal width\n        np.random.normal(4.3, 0.47, 50),  # petal length\n        np.random.normal(1.3, 0.20, 50),  # petal width\n    ]\n)\n\n# Species 3: Virginica - large petals and sepals\nvirginica = np.column_stack(\n    [\n        np.random.normal(6.6, 0.64, 50),  # sepal length\n        np.random.normal(3.0, 0.32, 50),  # sepal width\n        np.random.normal(5.5, 0.55, 50),  # petal length\n        np.random.normal(2.0, 0.27, 50),  # petal width\n    ]\n)\n\n# Combine data\nX = np.vstack([setosa, versicolor, virginica])\ny = np.array([0] * 50 + [1] * 50 + [2] * 50)\nspecies_names = [\"Setosa\", \"Versicolor\", \"Virginica\"]\n\n# Normalize variables (z-score standardization)\nX_mean = X.mean(axis=0)\nX_std = X.std(axis=0)\nX_scaled = (X - X_mean) / X_std\n\n# Andrews curve function: f(t) = x1/sqrt(2) + x2*sin(t) + x3*cos(t) + x4*sin(2t) + ...\nt_values = np.linspace(-np.pi, np.pi, 100)\n\n# Number of curves per species to display\nn_curves_per_species = 15\n\n# Custom style for theme-adaptive rendering\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=IMPRINT,\n    title_font_size=28,\n    label_font_size=22,\n    major_label_font_size=18,\n    legend_font_size=16,\n    value_font_size=14,\n    stroke_width=3,\n)\n\n# Create XY chart\nchart = pygal.XY(\n    width=4800,\n    height=2700,\n    style=custom_style,\n    title=\"andrews-curves · pygal · anyplot.ai\",\n    x_title=\"t (radians)\",\n    y_title=\"f(t)\",\n    show_dots=False,\n    stroke_style={\"width\": 2},\n    show_x_guides=True,\n    show_y_guides=True,\n    legend_at_bottom=True,\n    legend_at_bottom_columns=3,\n    legend_box_size=32,\n    truncate_legend=-1,\n)\n\n# Plot curves for each species\nfor species_idx in range(3):\n    species_mask = y == species_idx\n    species_data = X_scaled[species_mask]\n    original_data = X[species_mask]\n\n    # Sample curves per species for clarity\n    indices = np.random.choice(len(species_data), n_curves_per_species, replace=False)\n\n    # Collect all points for this species into a single series\n    all_points = []\n    for curve_num, idx in enumerate(indices):\n        row = species_data[idx]\n        orig = original_data[idx]\n        # Andrews transform: f(t) = x1/sqrt(2) + x2*sin(t) + x3*cos(t) + x4*sin(2t)\n        curve_values = (\n            row[0] / np.sqrt(2) + row[1] * np.sin(t_values) + row[2] * np.cos(t_values) + row[3] * np.sin(2 * t_values)\n        )\n        # Create points for the curve\n        points = [(float(t), float(v)) for t, v in zip(t_values, curve_values, strict=True)]\n        all_points.extend(points)\n        # Add None to create a break between curves\n        if curve_num < len(indices) - 1:\n            all_points.append(None)\n\n    # Add series for this species\n    chart.add(species_names[species_idx], all_points, show_dots=False)\n\n# Save outputs\nchart.render_to_file(f\"plot-{THEME}.html\")\nchart.render_to_png(f\"plot-{THEME}.png\")\n"}