Spaces:
Running
Running
Keep scatter plot icons fixed during zoom
#35
by juan-all-hands - opened
- app.py +167 -7
- leaderboard_transformer.py +2 -2
app.py
CHANGED
|
@@ -177,12 +177,166 @@ const tooltipInterval = setInterval(() => {
|
|
| 177 |
</script>
|
| 178 |
"""
|
| 179 |
|
| 180 |
-
# JavaScript to handle dark mode for Plotly charts and
|
| 181 |
dark_mode_script = """
|
| 182 |
<script>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 183 |
function updateChartsForDarkMode() {
|
| 184 |
const isDark = document.body.classList.contains('dark');
|
| 185 |
-
|
|
|
|
|
|
|
| 186 |
// Update Plotly chart backgrounds
|
| 187 |
const plots = document.querySelectorAll('.js-plotly-plot');
|
| 188 |
plots.forEach(plot => {
|
|
@@ -196,7 +350,7 @@ function updateChartsForDarkMode() {
|
|
| 196 |
});
|
| 197 |
}
|
| 198 |
});
|
| 199 |
-
|
| 200 |
// Swap OpenHands logos based on theme
|
| 201 |
const images = document.querySelectorAll('.js-plotly-plot image');
|
| 202 |
images.forEach(img => {
|
|
@@ -210,8 +364,11 @@ function updateChartsForDarkMode() {
|
|
| 210 |
}
|
| 211 |
|
| 212 |
document.addEventListener('DOMContentLoaded', () => {
|
| 213 |
-
setTimeout(
|
| 214 |
-
|
|
|
|
|
|
|
|
|
|
| 215 |
const observer = new MutationObserver((mutations) => {
|
| 216 |
mutations.forEach((mutation) => {
|
| 217 |
if (mutation.attributeName === 'class') {
|
|
@@ -220,8 +377,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 220 |
});
|
| 221 |
});
|
| 222 |
observer.observe(document.body, { attributes: true });
|
| 223 |
-
|
| 224 |
-
setInterval(
|
|
|
|
|
|
|
|
|
|
| 225 |
});
|
| 226 |
</script>
|
| 227 |
"""
|
|
|
|
| 177 |
</script>
|
| 178 |
"""
|
| 179 |
|
| 180 |
+
# JavaScript to handle dark mode for Plotly charts and keep zoomed marker icons a fixed size
|
| 181 |
dark_mode_script = """
|
| 182 |
<script>
|
| 183 |
+
function getAxisLayoutKeyFromRef(ref) {
|
| 184 |
+
if (!ref || ref === 'paper' || ref.endsWith(' domain')) {
|
| 185 |
+
return null;
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
const axisName = ref.split(' ')[0];
|
| 189 |
+
if (!axisName) {
|
| 190 |
+
return null;
|
| 191 |
+
}
|
| 192 |
+
|
| 193 |
+
return axisName === 'x' || axisName === 'y'
|
| 194 |
+
? `${axisName}axis`
|
| 195 |
+
: `${axisName[0]}axis${axisName.slice(1)}`;
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
function getAxisSpan(axis) {
|
| 199 |
+
if (!axis || !Array.isArray(axis.range) || axis.range.length < 2) {
|
| 200 |
+
return null;
|
| 201 |
+
}
|
| 202 |
+
|
| 203 |
+
const [start, end] = axis.range;
|
| 204 |
+
if (axis.type === 'date') {
|
| 205 |
+
const startMs = new Date(start).getTime();
|
| 206 |
+
const endMs = new Date(end).getTime();
|
| 207 |
+
return Number.isFinite(startMs) && Number.isFinite(endMs) ? Math.abs(endMs - startMs) : null;
|
| 208 |
+
}
|
| 209 |
+
|
| 210 |
+
const startNum = Number(start);
|
| 211 |
+
const endNum = Number(end);
|
| 212 |
+
return Number.isFinite(startNum) && Number.isFinite(endNum) ? Math.abs(endNum - startNum) : null;
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
+
function getAxisSpanForImageRef(plot, ref) {
|
| 216 |
+
const layoutKey = getAxisLayoutKeyFromRef(ref);
|
| 217 |
+
if (!layoutKey || !plot?._fullLayout) {
|
| 218 |
+
return null;
|
| 219 |
+
}
|
| 220 |
+
return getAxisSpan(plot._fullLayout[layoutKey]);
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
function isChartMarkerImage(image) {
|
| 224 |
+
return typeof image?.source === 'string' && image.source.includes('openhands=chart-marker');
|
| 225 |
+
}
|
| 226 |
+
|
| 227 |
+
function collectChartMarkerSizing(plot) {
|
| 228 |
+
const images = plot?._fullLayout?.images || [];
|
| 229 |
+
const signature = images
|
| 230 |
+
.map((image) => `${image.source || ''}|${image.xref || ''}|${image.yref || ''}`)
|
| 231 |
+
.join('||');
|
| 232 |
+
|
| 233 |
+
const markers = images
|
| 234 |
+
.map((image, index) => {
|
| 235 |
+
if (!isChartMarkerImage(image)) {
|
| 236 |
+
return null;
|
| 237 |
+
}
|
| 238 |
+
|
| 239 |
+
return {
|
| 240 |
+
index,
|
| 241 |
+
xref: image.xref,
|
| 242 |
+
yref: image.yref,
|
| 243 |
+
sizex: image.sizex,
|
| 244 |
+
sizey: image.sizey,
|
| 245 |
+
baseXSpan: getAxisSpanForImageRef(plot, image.xref),
|
| 246 |
+
baseYSpan: getAxisSpanForImageRef(plot, image.yref),
|
| 247 |
+
};
|
| 248 |
+
})
|
| 249 |
+
.filter(Boolean);
|
| 250 |
+
|
| 251 |
+
plot.__openhandsChartMarkerSizing = { signature, markers };
|
| 252 |
+
}
|
| 253 |
+
|
| 254 |
+
function updateChartMarkerSizes(plot, { forceReinit = false } = {}) {
|
| 255 |
+
if (!plot?._fullLayout || plot.__openhandsMarkerRelayoutInFlight) {
|
| 256 |
+
return;
|
| 257 |
+
}
|
| 258 |
+
|
| 259 |
+
const images = plot._fullLayout.images || [];
|
| 260 |
+
const signature = images
|
| 261 |
+
.map((image) => `${image.source || ''}|${image.xref || ''}|${image.yref || ''}`)
|
| 262 |
+
.join('||');
|
| 263 |
+
|
| 264 |
+
if (
|
| 265 |
+
forceReinit ||
|
| 266 |
+
!plot.__openhandsChartMarkerSizing ||
|
| 267 |
+
plot.__openhandsChartMarkerSizing.signature !== signature
|
| 268 |
+
) {
|
| 269 |
+
collectChartMarkerSizing(plot);
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
const markerState = plot.__openhandsChartMarkerSizing;
|
| 273 |
+
if (!markerState?.markers?.length) {
|
| 274 |
+
return;
|
| 275 |
+
}
|
| 276 |
+
|
| 277 |
+
const updates = {};
|
| 278 |
+
markerState.markers.forEach((marker) => {
|
| 279 |
+
const image = images[marker.index];
|
| 280 |
+
if (!image) {
|
| 281 |
+
return;
|
| 282 |
+
}
|
| 283 |
+
|
| 284 |
+
const currentXSpan = getAxisSpanForImageRef(plot, marker.xref);
|
| 285 |
+
if (Number.isFinite(marker.baseXSpan) && marker.baseXSpan > 0 && Number.isFinite(currentXSpan) && currentXSpan > 0) {
|
| 286 |
+
const nextSizeX = marker.sizex * (currentXSpan / marker.baseXSpan);
|
| 287 |
+
if (Math.abs((image.sizex || 0) - nextSizeX) > 1e-9) {
|
| 288 |
+
updates[`images[${marker.index}].sizex`] = nextSizeX;
|
| 289 |
+
}
|
| 290 |
+
}
|
| 291 |
+
|
| 292 |
+
const currentYSpan = getAxisSpanForImageRef(plot, marker.yref);
|
| 293 |
+
if (Number.isFinite(marker.baseYSpan) && marker.baseYSpan > 0 && Number.isFinite(currentYSpan) && currentYSpan > 0) {
|
| 294 |
+
const nextSizeY = marker.sizey * (currentYSpan / marker.baseYSpan);
|
| 295 |
+
if (Math.abs((image.sizey || 0) - nextSizeY) > 1e-9) {
|
| 296 |
+
updates[`images[${marker.index}].sizey`] = nextSizeY;
|
| 297 |
+
}
|
| 298 |
+
}
|
| 299 |
+
});
|
| 300 |
+
|
| 301 |
+
if (!Object.keys(updates).length) {
|
| 302 |
+
return;
|
| 303 |
+
}
|
| 304 |
+
|
| 305 |
+
plot.__openhandsMarkerRelayoutInFlight = true;
|
| 306 |
+
Promise.resolve(Plotly.relayout(plot, updates)).finally(() => {
|
| 307 |
+
plot.__openhandsMarkerRelayoutInFlight = false;
|
| 308 |
+
});
|
| 309 |
+
}
|
| 310 |
+
|
| 311 |
+
function bindChartMarkerScaling(plot) {
|
| 312 |
+
if (!plot || plot.__openhandsChartMarkerScalingBound || typeof plot.on !== 'function') {
|
| 313 |
+
return;
|
| 314 |
+
}
|
| 315 |
+
|
| 316 |
+
plot.on('plotly_relayout', () => {
|
| 317 |
+
if (!plot.__openhandsMarkerRelayoutInFlight) {
|
| 318 |
+
window.requestAnimationFrame(() => updateChartMarkerSizes(plot));
|
| 319 |
+
}
|
| 320 |
+
});
|
| 321 |
+
|
| 322 |
+
plot.on('plotly_afterplot', () => {
|
| 323 |
+
updateChartMarkerSizes(plot, { forceReinit: true });
|
| 324 |
+
});
|
| 325 |
+
|
| 326 |
+
plot.__openhandsChartMarkerScalingBound = true;
|
| 327 |
+
updateChartMarkerSizes(plot, { forceReinit: true });
|
| 328 |
+
}
|
| 329 |
+
|
| 330 |
+
function initializeChartMarkerScaling() {
|
| 331 |
+
const plots = document.querySelectorAll('.js-plotly-plot');
|
| 332 |
+
plots.forEach(bindChartMarkerScaling);
|
| 333 |
+
}
|
| 334 |
+
|
| 335 |
function updateChartsForDarkMode() {
|
| 336 |
const isDark = document.body.classList.contains('dark');
|
| 337 |
+
|
| 338 |
+
initializeChartMarkerScaling();
|
| 339 |
+
|
| 340 |
// Update Plotly chart backgrounds
|
| 341 |
const plots = document.querySelectorAll('.js-plotly-plot');
|
| 342 |
plots.forEach(plot => {
|
|
|
|
| 350 |
});
|
| 351 |
}
|
| 352 |
});
|
| 353 |
+
|
| 354 |
// Swap OpenHands logos based on theme
|
| 355 |
const images = document.querySelectorAll('.js-plotly-plot image');
|
| 356 |
images.forEach(img => {
|
|
|
|
| 364 |
}
|
| 365 |
|
| 366 |
document.addEventListener('DOMContentLoaded', () => {
|
| 367 |
+
setTimeout(() => {
|
| 368 |
+
initializeChartMarkerScaling();
|
| 369 |
+
updateChartsForDarkMode();
|
| 370 |
+
}, 500);
|
| 371 |
+
|
| 372 |
const observer = new MutationObserver((mutations) => {
|
| 373 |
mutations.forEach((mutation) => {
|
| 374 |
if (mutation.attributeName === 'class') {
|
|
|
|
| 377 |
});
|
| 378 |
});
|
| 379 |
observer.observe(document.body, { attributes: true });
|
| 380 |
+
|
| 381 |
+
setInterval(() => {
|
| 382 |
+
initializeChartMarkerScaling();
|
| 383 |
+
updateChartsForDarkMode();
|
| 384 |
+
}, 1000);
|
| 385 |
});
|
| 386 |
</script>
|
| 387 |
"""
|
leaderboard_transformer.py
CHANGED
|
@@ -515,7 +515,7 @@ def create_scatter_chart(
|
|
| 515 |
try:
|
| 516 |
with open(logo_path, 'rb') as f:
|
| 517 |
encoded_logo = base64.b64encode(f.read()).decode('utf-8')
|
| 518 |
-
logo_uri = f"data:image/svg+xml;base64,{encoded_logo}"
|
| 519 |
|
| 520 |
if x_type == "date":
|
| 521 |
# For date axes, use data coordinates directly
|
|
@@ -1209,7 +1209,7 @@ def _plot_scatter_plotly(
|
|
| 1209 |
logger.warning(f"Could not load logo {path}: {e}")
|
| 1210 |
return None
|
| 1211 |
mime = "svg+xml" if path.lower().endswith(".svg") else "png"
|
| 1212 |
-
uri = f"data:image/{mime};base64,{encoded}"
|
| 1213 |
_logo_cache[path] = uri
|
| 1214 |
return uri
|
| 1215 |
|
|
|
|
| 515 |
try:
|
| 516 |
with open(logo_path, 'rb') as f:
|
| 517 |
encoded_logo = base64.b64encode(f.read()).decode('utf-8')
|
| 518 |
+
logo_uri = f"data:image/svg+xml;openhands=chart-marker;base64,{encoded_logo}"
|
| 519 |
|
| 520 |
if x_type == "date":
|
| 521 |
# For date axes, use data coordinates directly
|
|
|
|
| 1209 |
logger.warning(f"Could not load logo {path}: {e}")
|
| 1210 |
return None
|
| 1211 |
mime = "svg+xml" if path.lower().endswith(".svg") else "png"
|
| 1212 |
+
uri = f"data:image/{mime};openhands=chart-marker;base64,{encoded}"
|
| 1213 |
_logo_cache[path] = uri
|
| 1214 |
return uri
|
| 1215 |
|