loop-benchmarking

Controlled experiments across agentic coding configurations. Same task, one variable, what actually works.
git clone https://git.shiptheloop.com/loop-benchmarking.git
Log | Files | Refs | README

commit c846bee44baf5e45fbb78d3b92494c10e718c9cb
parent c4f76cb8c2ab03da0c0f60835cd69c16a584b379
Author: Brian Graham <brian@buildingbetterteams.de>
Date:   Tue,  7 Apr 2026 17:13:11 +0200

Shared color palette for 10 models across all charts

Centralized MODEL_COLORS in lib/colors.ts with auto-assignment for
unknown models. Added distinct colors for GLM models (sage, steel, coral).
Updated Charts, ScatterPlot, BumpChart, EfficiencyFrontier.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

Diffstat:
Mdashboard/src/components/BumpChart.tsx | 15++++-----------
Mdashboard/src/components/Charts.tsx | 13+++++--------
Mdashboard/src/components/EfficiencyFrontier.tsx | 17+++--------------
Mdashboard/src/components/ScatterPlot.tsx | 25+++++++------------------
Adashboard/src/lib/colors.ts | 72++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
5 files changed, 91 insertions(+), 51 deletions(-)

diff --git a/dashboard/src/components/BumpChart.tsx b/dashboard/src/components/BumpChart.tsx @@ -12,19 +12,12 @@ import { import type { Run } from "../lib/types"; import { AXIS_NAMES, type AxisName } from "../lib/types"; import { groupIntoCells, type Cell } from "../lib/analysis"; +import { getModelColor } from "../lib/colors"; interface BumpChartProps { runs: Run[]; } -const MODEL_COLORS: Record<string, string> = { - haiku: "hsl(193 44% 67%)", - sonnet: "hsl(40 71% 73%)", - opus: "hsl(311 24% 63%)", -}; - -const FALLBACK_COLOR = "hsl(213 14% 65%)"; - const AXIS_LABELS: Record<AxisName, string> = { model: "Model", effort: "Effort", @@ -514,11 +507,11 @@ export default function BumpChart({ runs }: BumpChartProps) { key={model} type="linear" dataKey={model} - stroke={MODEL_COLORS[model] || FALLBACK_COLOR} + stroke={getModelColor(model)} strokeWidth={2.5} dot={makeRankDot( model, - MODEL_COLORS[model] || FALLBACK_COLOR, + getModelColor(model), pointLookup )} activeDot={false} @@ -568,7 +561,7 @@ export default function BumpChart({ runs }: BumpChartProps) { display: "inline-block", width: 12, height: 3, - background: MODEL_COLORS[model] || FALLBACK_COLOR, + background: getModelColor(model), }} /> <span style={{ color: "var(--text)" }}>{model}</span> diff --git a/dashboard/src/components/Charts.tsx b/dashboard/src/components/Charts.tsx @@ -12,6 +12,7 @@ import { ZAxis, } from "recharts"; import type { Run } from "../lib/types"; +import { getModelColor, modelSortOrder } from "../lib/colors"; interface ChartsProps { runs: Run[]; @@ -71,11 +72,7 @@ const SMUI = { purple: "hsl(311 24% 63%)", }; -const MODEL_COLORS: Record<string, string> = { - haiku: SMUI.frost2, - sonnet: SMUI.yellow, - opus: SMUI.purple, -}; +// MODEL_COLORS imported from ../lib/colors const TOOLTIP_STYLE = { background: SMUI.surface1, @@ -148,7 +145,7 @@ function aggregateCells(runs: Run[]): CellAggregate[] { })); } -const MODEL_ORDER: Record<string, number> = { haiku: 1, sonnet: 2, opus: 3 }; +// MODEL_ORDER imported via modelSortOrder from ../lib/colors function quantile(sorted: number[], q: number): number { if (sorted.length === 0) return 0; @@ -181,7 +178,7 @@ function aggregateByModel(runs: Run[]): BoxPlotData[] { } const sortedEntries = Object.entries(byModel).sort(([a], [b]) => - (MODEL_ORDER[a] || 99) - (MODEL_ORDER[b] || 99) || a.localeCompare(b) + modelSortOrder(a) - modelSortOrder(b) || a.localeCompare(b) ); return sortedEntries.map(([model, modelCells]) => { @@ -195,7 +192,7 @@ function aggregateByModel(runs: Run[]): BoxPlotData[] { iqr: stats.q3 - stats.q1, cellCount: modelCells.length, scores, - color: MODEL_COLORS[baseModel] || SMUI.frost2, + color: getModelColor(baseModel), }; }); } diff --git a/dashboard/src/components/EfficiencyFrontier.tsx b/dashboard/src/components/EfficiencyFrontier.tsx @@ -1,4 +1,5 @@ import { useState, useMemo } from "react"; +import { getModelColor as sharedGetModelColor, MODEL_COLORS as SHARED_MODEL_COLORS } from "../lib/colors"; import { ScatterChart, Scatter, @@ -126,14 +127,6 @@ const selectStyle: React.CSSProperties = { cursor: "pointer", }; -const MODEL_COLORS: Record<string, string> = { - haiku: "hsl(193 44% 67%)", - sonnet: "hsl(40 71% 73%)", - opus: "hsl(311 24% 63%)", -}; - -const DEFAULT_COLOR = "hsl(213 14% 65%)"; - interface ConfigPoint { cell_id: string; model: string; @@ -146,11 +139,7 @@ interface ConfigPoint { } function getModelColor(model: string): string { - const key = model.toLowerCase(); - for (const [m, color] of Object.entries(MODEL_COLORS)) { - if (key.includes(m)) return color; - } - return DEFAULT_COLOR; + return sharedGetModelColor(model); } function aggregateByConfig( @@ -481,7 +470,7 @@ export default function EfficiencyFrontier({ flexWrap: "wrap", }} > - {Object.entries(MODEL_COLORS).map(([model, color]) => ( + {Object.entries(SHARED_MODEL_COLORS).filter(([k]) => !k.startsWith("slot-")).map(([model, color]) => ( <div key={model} style={{ display: "flex", alignItems: "center", gap: "6px" }} diff --git a/dashboard/src/components/ScatterPlot.tsx b/dashboard/src/components/ScatterPlot.tsx @@ -1,4 +1,5 @@ import React from "react"; +import { getModelColor, getModelFillColor, modelSortOrder } from "../lib/colors"; import { ScatterChart, Scatter, @@ -105,24 +106,12 @@ const METRIC_OPTIONS = Object.entries(METRIC_CONFIG).map(([key, conf]) => ({ label: conf.label, })); -const MODEL_COLORS: Record<string, string> = { - haiku: "hsl(193 44% 67%)", - sonnet: "hsl(40 71% 73%)", - opus: "hsl(311 24% 63%)", -}; - -const MODEL_FILL_COLORS: Record<string, string> = { - haiku: "hsla(193, 44%, 67%,", - sonnet: "hsla(40, 71%, 73%,", - opus: "hsla(311, 24%, 63%,", -}; - function fallbackColor(model: string): string { - return MODEL_COLORS[model] || "hsl(213 14% 65%)"; + return getModelColor(model); } function fallbackFillPrefix(model: string): string { - return MODEL_FILL_COLORS[model] || "hsla(213, 14%, 65%,"; + return getModelFillColor(model); } // --- Convex hull (Andrew's monotone chain) --- @@ -212,8 +201,8 @@ function computeRegions( regions.push({ model, points, centroid, hulls, n: points.length }); } - const MODEL_ORDER: Record<string, number> = { haiku: 1, sonnet: 2, opus: 3 }; - return regions.sort((a, b) => (MODEL_ORDER[a.model] || 99) - (MODEL_ORDER[b.model] || 99)); + // Model sort order from shared colors + return regions.sort((a, b) => modelSortOrder(a.model) - modelSortOrder(b.model)); } interface CentroidDatum { @@ -395,9 +384,9 @@ export default function ScatterPlot({ } // All models present in data (stable order) - const MODEL_ORDER: Record<string, number> = { haiku: 1, sonnet: 2, opus: 3 }; + // Model sort order from shared colors const allModels = Object.keys(byModel).sort( - (a, b) => (MODEL_ORDER[a] || 99) - (MODEL_ORDER[b] || 99) + (a, b) => modelSortOrder(a) - modelSortOrder(b) ); // Initialize visibleModels on first render or when models change diff --git a/dashboard/src/lib/colors.ts b/dashboard/src/lib/colors.ts @@ -0,0 +1,72 @@ +/** + * Shared model color palette for all charts. + * 10 perceptually distinct colors, colorblind-friendly, designed for dark backgrounds. + * Hues spaced ~36° apart on the color wheel, lightness 60-75% for readability. + */ + +// Anthropic models (cool tones) +// GLM models (warm/earth tones) +// Future models get auto-assigned from the overflow palette + +export const MODEL_COLORS: Record<string, string> = { + // Anthropic + haiku: "hsl(193 44% 67%)", // cyan + sonnet: "hsl(40 71% 73%)", // gold + opus: "hsl(311 24% 63%)", // mauve + + // GLM (Z.AI) + "glm-4.5-air": "hsl(145 35% 58%)", // sage green + "glm-4.7": "hsl(220 50% 65%)", // steel blue + "glm-5.1": "hsl(15 65% 65%)", // coral + + // Future slots + "slot-7": "hsl(270 40% 68%)", // lavender + "slot-8": "hsl(60 50% 62%)", // olive + "slot-9": "hsl(340 50% 65%)", // rose + "slot-10": "hsl(170 40% 55%)", // teal +}; + +// Same colors in hsla format (without closing opacity) for fill regions +export const MODEL_FILL_COLORS: Record<string, string> = { + haiku: "hsla(193, 44%, 67%,", + sonnet: "hsla(40, 71%, 73%,", + opus: "hsla(311, 24%, 63%,", + "glm-4.5-air": "hsla(145, 35%, 58%,", + "glm-4.7": "hsla(220, 50%, 65%,", + "glm-5.1": "hsla(15, 65%, 65%,", + "slot-7": "hsla(270, 40%, 68%,", + "slot-8": "hsla(60, 50%, 62%,", + "slot-9": "hsla(340, 50%, 65%,", + "slot-10": "hsla(170, 40%, 55%,", +}; + +// Ordered list for auto-assigning to unknown models +const OVERFLOW_HUES = [270, 60, 340, 170, 95, 250, 30, 0, 200, 130]; +let _nextOverflow = 0; + +export function getModelColor(model: string): string { + if (MODEL_COLORS[model]) return MODEL_COLORS[model]; + // Auto-assign from overflow palette + const hue = OVERFLOW_HUES[_nextOverflow % OVERFLOW_HUES.length]; + _nextOverflow++; + const color = `hsl(${hue} 40% 65%)`; + MODEL_COLORS[model] = color; + MODEL_FILL_COLORS[model] = `hsla(${hue}, 40%, 65%,`; + return color; +} + +export function getModelFillColor(model: string): string { + if (MODEL_FILL_COLORS[model]) return MODEL_FILL_COLORS[model]; + getModelColor(model); // ensure it's assigned + return MODEL_FILL_COLORS[model]; +} + +// Sort order: Anthropic by tier, then GLM by version, then alphabetical +const MODEL_ORDER: Record<string, number> = { + haiku: 1, sonnet: 2, opus: 3, + "glm-4.5-air": 10, "glm-4.7": 11, "glm-5.1": 12, +}; + +export function modelSortOrder(model: string): number { + return MODEL_ORDER[model] ?? 99; +}

Impressum · Datenschutz