File size: 2,466 Bytes
be82719
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
// Themed Plotly rendering. Figures come from the backend as plain
// {data, layout} JSON; we overlay transparent backgrounds and the current
// theme's font/grid colors, and re-render live plots on theme switch.
//
// IMPORTANT (inherited from miru-tracer): never give a rendered figure a
// fixed width inside a scrollable container — Plotly's responsive resize +
// a toggling scrollbar forms a ResizeObserver feedback loop that freezes
// the browser. Figures stay autosized; wide data pans via the figure's own
// dragmode.

const live = new Map(); // element -> figure JSON

function themeColors() {
  const styles = getComputedStyle(document.documentElement);
  return {
    text: styles.getPropertyValue('--text').trim() || '#e6e9f0',
    muted: styles.getPropertyValue('--text-muted').trim() || '#8b93a7',
    grid: styles.getPropertyValue('--plot-grid').trim() || 'rgba(127,127,127,0.18)',
    font: styles.getPropertyValue('--font-ui').trim() || 'Inter, sans-serif',
  };
}

function themedLayout(layout = {}) {
  const c = themeColors();
  const themed = structuredClone(layout);
  themed.paper_bgcolor = 'rgba(0,0,0,0)';
  themed.plot_bgcolor = 'rgba(0,0,0,0)';
  themed.font = { ...(themed.font || {}), color: c.text, family: c.font };
  themed.autosize = true;
  delete themed.width; // never fixed-width (see module note)
  for (const key of Object.keys(themed)) {
    if (key.startsWith('xaxis') || key.startsWith('yaxis')) {
      themed[key] = {
        ...themed[key],
        gridcolor: c.grid, zerolinecolor: c.grid, linecolor: c.grid,
        tickfont: { ...(themed[key]?.tickfont || {}), color: c.muted },
      };
    }
  }
  if (themed.legend) themed.legend = { ...themed.legend, font: { color: c.muted } };
  return themed;
}

export function renderPlot(target, fig) {
  if (!fig || !fig.data) { clearPlot(target); return; }
  live.set(target, fig);
  window.Plotly.react(target, fig.data, themedLayout(fig.layout), {
    responsive: true,
    displaylogo: false,
    modeBarButtonsToRemove: ['select2d', 'lasso2d'],
  });
  target.classList.add('has-plot');
}

export function clearPlot(target) {
  live.delete(target);
  if (target.classList.contains('has-plot')) {
    window.Plotly.purge(target);
    target.classList.remove('has-plot');
  }
  target.replaceChildren();
}

export function rethemePlots() {
  for (const [target, fig] of live) {
    if (target.isConnected) renderPlot(target, fig);
    else live.delete(target);
  }
}