Skip to content

Commit 341bc4c

Browse files
committed
Better label positioning
1 parent cebb286 commit 341bc4c

File tree

1 file changed

+222
-12
lines changed

1 file changed

+222
-12
lines changed

apps/web-roo-code/src/app/evals/plot.tsx

Lines changed: 222 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,16 @@ import { useMemo } from "react"
44
import { ScatterChart, Scatter, XAxis, YAxis, Label, Customized, Cross, LabelList } from "recharts"
55

66
import { formatCurrency } from "@/lib"
7-
import { ChartContainer, ChartTooltip, ChartTooltipContent, ChartConfig } from "@/components/ui"
7+
import { ChartContainer, ChartTooltip, ChartConfig } from "@/components/ui"
88

99
import type { EvalRun } from "./types"
1010

1111
type PlotProps = {
1212
tableData: (EvalRun & { label: string; cost: number })[]
1313
}
1414

15+
type LabelPosition = "top" | "bottom" | "left" | "right"
16+
1517
export const Plot = ({ tableData }: PlotProps) => {
1618
const chartData = useMemo(() => tableData.filter(({ cost }) => cost < 50), [tableData])
1719

@@ -20,6 +22,155 @@ export const Plot = ({ tableData }: PlotProps) => {
2022
[chartData],
2123
)
2224

25+
// Calculate label positions to avoid overlaps.
26+
const labelPositions = useMemo(() => {
27+
const positions: Record<string, LabelPosition> = {}
28+
29+
// Track placed labels with their approximate bounds.
30+
const placedLabels: Array<{
31+
cost: number
32+
score: number
33+
label: string
34+
position: LabelPosition
35+
}> = []
36+
37+
// Helper function to check if two labels would overlap.
38+
const wouldLabelsOverlap = (
39+
p1: { cost: number; score: number; position: LabelPosition },
40+
p2: { cost: number; score: number; position: LabelPosition },
41+
): boolean => {
42+
// Approximate thresholds for overlap detection.
43+
const horizontalThreshold = 4 // Cost units.
44+
const verticalThreshold = 5 // Score units.
45+
46+
const costDiff = Math.abs(p1.cost - p2.cost)
47+
const scoreDiff = Math.abs(p1.score - p2.score)
48+
49+
// If points are far apart, no overlap.
50+
if (costDiff > horizontalThreshold * 2 || scoreDiff > verticalThreshold * 2) {
51+
return false
52+
}
53+
54+
// Check specific position combinations for overlap.
55+
// Same position for nearby points definitely overlaps.
56+
if (p1.position === p2.position && costDiff < horizontalThreshold && scoreDiff < verticalThreshold) {
57+
return true
58+
}
59+
60+
// Check adjacent position overlaps.
61+
const p1IsTop = p1.position === "top"
62+
const p1IsBottom = p1.position === "bottom"
63+
const p2IsTop = p2.position === "top"
64+
const p2IsBottom = p2.position === "bottom"
65+
66+
// If both labels are on the same vertical side and points are close
67+
// horizontally.
68+
if ((p1IsTop && p2IsTop) || (p1IsBottom && p2IsBottom)) {
69+
if (costDiff < horizontalThreshold && scoreDiff < verticalThreshold / 2) {
70+
return true
71+
}
72+
}
73+
74+
return false
75+
}
76+
77+
// Helper function to check if position would overlap with a data point.
78+
const wouldOverlapPoint = (point: (typeof chartData)[0], position: LabelPosition): boolean => {
79+
for (const other of chartData) {
80+
if (other.label === point.label) {
81+
continue
82+
}
83+
84+
const costDiff = Math.abs(point.cost - other.cost)
85+
const scoreDiff = Math.abs(point.score - other.score)
86+
87+
// Check if label would be placed on top of another point.
88+
switch (position) {
89+
case "top":
90+
// Label is above, check if there's a point above.
91+
if (costDiff < 3 && other.score > point.score && other.score - point.score < 6) {
92+
return true
93+
}
94+
break
95+
case "bottom":
96+
// Label is below, check if there's a point below.
97+
if (costDiff < 3 && other.score < point.score && point.score - other.score < 6) {
98+
return true
99+
}
100+
break
101+
case "left":
102+
// Label is to the left, check if there's a point to the left.
103+
if (scoreDiff < 3 && other.cost < point.cost && point.cost - other.cost < 4) {
104+
return true
105+
}
106+
break
107+
case "right":
108+
// Label is to the right, check if there's a point to the right.
109+
if (scoreDiff < 3 && other.cost > point.cost && other.cost - point.cost < 4) {
110+
return true
111+
}
112+
break
113+
}
114+
}
115+
return false
116+
}
117+
118+
// Sort points to process them in a consistent order.
119+
// Process from top-left to bottom-right.
120+
const sortedData = [...chartData].sort((a, b) => {
121+
// First by score (higher first).
122+
const scoreDiff = b.score - a.score
123+
if (Math.abs(scoreDiff) > 1) return scoreDiff
124+
// Then by cost (lower first).
125+
return a.cost - b.cost
126+
})
127+
128+
// Process each point and find the best position.
129+
sortedData.forEach((point) => {
130+
// Try positions in order of preference.
131+
const positionPreferences: LabelPosition[] = ["top", "bottom", "right", "left"]
132+
133+
let bestPosition: LabelPosition = "top"
134+
135+
for (const position of positionPreferences) {
136+
// Check if this position would overlap with any placed labels.
137+
let hasLabelOverlap = false
138+
139+
for (const placed of placedLabels) {
140+
if (
141+
wouldLabelsOverlap(
142+
{ cost: point.cost, score: point.score, position },
143+
{ cost: placed.cost, score: placed.score, position: placed.position },
144+
)
145+
) {
146+
hasLabelOverlap = true
147+
break
148+
}
149+
}
150+
151+
// Check if this position would overlap with any data points.
152+
const hasPointOverlap = wouldOverlapPoint(point, position)
153+
154+
// If no overlaps, use this position.
155+
if (!hasLabelOverlap && !hasPointOverlap) {
156+
bestPosition = position
157+
break
158+
}
159+
}
160+
161+
// Use the best position found
162+
positions[point.label] = bestPosition
163+
placedLabels.push({
164+
cost: point.cost,
165+
score: point.score,
166+
label: point.label,
167+
position: bestPosition,
168+
})
169+
})
170+
171+
return positions
172+
}, [chartData])
173+
23174
return (
24175
<>
25176
<div className="pb-4 font-medium">Cost Versus Score</div>
@@ -47,21 +198,46 @@ export const Plot = ({ tableData }: PlotProps) => {
47198
tickFormatter={(value) => `${value}%`}>
48199
<Label value="Score" angle={-90} position="left" dy={-15} />
49200
</YAxis>
50-
<ChartTooltip content={<ChartTooltipContent labelKey="label" hideIndicator />} />
201+
<ChartTooltip
202+
content={({ active, payload }) => {
203+
if (!active || !payload || !payload.length || !payload[0]) {
204+
return null
205+
}
206+
207+
const { label, cost, score } = payload[0].payload
208+
209+
return (
210+
<div className="bg-background border rounded-sm p-2 shadow-sm text-left">
211+
<div className="border-b pb-1">{label}</div>
212+
<div className="pt-1">
213+
<div>
214+
Score: <span className="font-mono">{Math.round(score)}%</span>
215+
</div>
216+
<div>
217+
Cost: <span className="font-mono">{formatCurrency(cost)}</span>
218+
</div>
219+
</div>
220+
</div>
221+
)
222+
}}
223+
/>
51224
<Customized component={renderQuadrant} />
52225
{chartData.map((d, index) => (
53226
<Scatter
54227
key={d.label}
55228
name={d.label}
56229
data={[d]}
57230
fill={generateSpectrumColor(index, chartData.length)}>
58-
<LabelList dataKey="label" position="top" offset={8} content={renderCustomLabel} />
231+
<LabelList
232+
dataKey="label"
233+
content={(props) => renderCustomLabel(props, labelPositions[d.label] || "top")}
234+
/>
59235
</Scatter>
60236
))}
61237
</ScatterChart>
62238
</ChartContainer>
63239
<div className="py-4 text-xs opacity-50">
64-
(Note: Very expensive models are excluded from the scatter plot.)
240+
(Note: Models with a cost of $50 or more are excluded from the scatter plot.)
65241
</div>
66242
</>
67243
)
@@ -82,28 +258,59 @@ const renderQuadrant = (props: any) => (
82258
)
83259

84260
// eslint-disable-next-line @typescript-eslint/no-explicit-any
85-
const renderCustomLabel = (props: any) => {
261+
const renderCustomLabel = (props: any, position: LabelPosition) => {
86262
const { x, y, value } = props
87263
const maxWidth = 80 // Maximum width in pixels - adjust as needed.
88264

89-
const truncateText = (text: string, maxChars: number = 12) => {
265+
const truncateText = (text: string, maxChars: number = 20) => {
90266
if (text.length <= maxChars) {
91267
return text
92268
}
93269

94270
return text.substring(0, maxChars - 1) + "…"
95271
}
96272

273+
// Calculate position offsets based on label position.
274+
let xOffset = 0
275+
let yOffset = 0
276+
let textAnchor: "middle" | "start" | "end" = "middle"
277+
let dominantBaseline: "auto" | "hanging" | "middle" = "auto"
278+
279+
switch (position) {
280+
case "top":
281+
yOffset = -8
282+
textAnchor = "middle"
283+
dominantBaseline = "auto"
284+
break
285+
case "bottom":
286+
yOffset = 15
287+
textAnchor = "middle"
288+
dominantBaseline = "hanging"
289+
break
290+
case "left":
291+
xOffset = -8
292+
yOffset = 5
293+
textAnchor = "end"
294+
dominantBaseline = "middle"
295+
break
296+
case "right":
297+
xOffset = 15
298+
yOffset = 5
299+
textAnchor = "start"
300+
dominantBaseline = "middle"
301+
break
302+
}
303+
97304
return (
98305
<text
99-
x={x}
100-
y={y - 5}
101-
fontSize="10"
306+
x={x + xOffset}
307+
y={y + yOffset}
308+
fontSize="11"
102309
fontWeight="500"
103310
fill="currentColor"
104311
opacity="0.8"
105-
textAnchor="middle"
106-
dominantBaseline="auto"
312+
textAnchor={textAnchor}
313+
dominantBaseline={dominantBaseline}
107314
style={{
108315
pointerEvents: "none",
109316
maxWidth: `${maxWidth}px`,
@@ -117,12 +324,15 @@ const renderCustomLabel = (props: any) => {
117324
}
118325

119326
const generateSpectrumColor = (index: number, total: number): string => {
120-
// Distribute hues evenly across the color wheel (0-360 degrees)
327+
// Distribute hues evenly across the color wheel (0-360 degrees).
121328
// Start at 0 (red) and distribute evenly.
122329
const hue = (index * 360) / total
330+
123331
// Use high saturation for vibrant colors.
124332
const saturation = 70
333+
125334
// Use medium lightness for good visibility on both light and dark backgrounds.
126335
const lightness = 50
336+
127337
return `hsl(${Math.round(hue)}, ${saturation}%, ${lightness}%)`
128338
}

0 commit comments

Comments
 (0)