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:
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;
+}