Keep scatter plot icons fixed during zoom

#35
Files changed (2) hide show
  1. app.py +167 -7
  2. 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 OpenHands logos
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(updateChartsForDarkMode, 500);
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(updateChartsForDarkMode, 1000);
 
 
 
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