

# Plotly figures for the web

> **Install for this page:** export data with
> `pip install scope-profiler`; render it in a web project with
> `npm install @scope-profiler/plotly plotly.js-dist-min`.

`@scope-profiler/plotly` is the browser-facing companion to
`scope-profiler export plot-data --format json`. It builds plain Plotly
figure specifications from the exported JSON, without importing Plotly
itself. That makes it suitable for static sites and any JavaScript
framework.

## Install

Install the figure builders plus the Plotly bundle your application
wants to use:

``` sh
npm install @scope-profiler/plotly plotly.js-dist-min
```

Then fetch a JSON payload and render it with your own Plotly instance:

``` js
import Plotly from "plotly.js-dist-min";
import { buildGanttFigure, renderFigure } from "@scope-profiler/plotly";

const payload = await fetch("/figures/gantt_data.json").then((response) => response.json());
const figure = buildGanttFigure(payload);
await renderFigure(Plotly, document.querySelector("#gantt"), figure);
```

## Live examples

The figures below are built by the published npm package in this page.
Each uses the same shape of payload that
`scope-profiler export plot-data --format json` writes.

<div style="display: grid; gap: 1.5rem; margin: 1rem 0;">
  <div><strong>Gantt — a lane per region and rank</strong><div id="scope-plotly-gantt" style="min-height: 290px;"></div></div>
  <div><strong>Durations — compare regions between runs</strong><div id="scope-plotly-durations" style="min-height: 350px;"></div></div>
  <div><strong>Flame — nested calls</strong><div id="scope-plotly-flame" style="min-height: 440px;"></div></div>
  <div><strong>Speedup — region scaling</strong><div id="scope-plotly-speedup" style="min-height: 390px;"></div></div>
  <div><strong>Duration time series — performance drift</strong><div id="scope-plotly-timeseries" style="min-height: 390px;"></div></div>
  <div><strong>Histogram — call-duration distribution</strong><div id="scope-plotly-histogram" style="min-height: 380px;"></div></div>
  <div><strong>Rank heatmap — duration by region and rank</strong><div id="scope-plotly-heatmap" style="min-height: 340px;"></div></div>
  <div><strong>Imbalance — per-rank duration</strong><div id="scope-plotly-imbalance" style="min-height: 390px;"></div></div>
  <div><strong>Density — timeline occupancy per region</strong><div id="scope-plotly-density" style="min-height: 300px;"></div></div>
  <div><strong>Call graph — who calls whom</strong><div id="scope-plotly-callgraph" style="min-height: 340px;"></div></div>
  <div><strong>Region summary — the slowest regions in a run</strong><div id="scope-plotly-summary" style="min-height: 320px;"></div></div>
</div>
<script src="https://cdn.plot.ly/plotly-3.7.0.min.js"></script>
<script type="module">
  const packageUrl = "../_static/plotly/index.js";
  const { buildGanttFigure, buildDurationsFigure, buildFlameFigure, buildSpeedupFigure, buildDurationTimeseriesFigure, buildHistogramFigure, buildRankHeatmapFigure, buildImbalanceFigure, buildFigure, renderFigure } = await import(packageUrl);
  const render = (id, figure) => renderFigure(window.Plotly, document.getElementById(id), figure);
  await render("scope-plotly-gantt", buildGanttFigure({
    colors: { setup: "#2a78d6", solve: "#eb6834", exchange: "#1baf7a" },
    intervals: [
      { file: "run", rank: 0, region: "setup", start_seconds: 0, end_seconds: 0.7 },
      { file: "run", rank: 0, region: "solve", start_seconds: 0.8, end_seconds: 2.8 },
      { file: "run", rank: 0, region: "exchange", start_seconds: 2.9, end_seconds: 3.4 },
      { file: "run", rank: 1, region: "setup", start_seconds: 0, end_seconds: 0.6 },
      { file: "run", rank: 1, region: "solve", start_seconds: 0.7, end_seconds: 2.5 },
      { file: "run", rank: 1, region: "exchange", start_seconds: 2.6, end_seconds: 3.3 },
    ],
  }));
  await render("scope-plotly-durations", buildDurationsFigure({
    metrics: ["total"], colors: { "1 rank": "#2a78d6", "4 ranks": "#eb6834" },
    bars: [
      { file: "1 rank", region: "setup", metric: "total", value_seconds: 0.7 },
      { file: "1 rank", region: "solve", metric: "total", value_seconds: 5.2 },
      { file: "1 rank", region: "exchange", metric: "total", value_seconds: 1.4 },
      { file: "4 ranks", region: "setup", metric: "total", value_seconds: 0.8 },
      { file: "4 ranks", region: "solve", metric: "total", value_seconds: 1.6 },
      { file: "4 ranks", region: "exchange", metric: "total", value_seconds: 0.5 },
    ],
  }));
  await render("scope-plotly-flame", buildFlameFigure({
    colors: { timestep: "#2a78d6", assemble: "#1baf7a", solve: "#eb6834" },
    calls: [
      { file: "run", rank: 0, call_id: 1, parent_call_id: null, region: "timestep", start_seconds: 0, end_seconds: 4, inclusive_duration_seconds: 4 },
      { file: "run", rank: 0, call_id: 2, parent_call_id: 1, region: "assemble", start_seconds: 0.2, end_seconds: 1.1, inclusive_duration_seconds: 0.9 },
      { file: "run", rank: 0, call_id: 3, parent_call_id: 1, region: "solve", start_seconds: 1.2, end_seconds: 3.8, inclusive_duration_seconds: 2.6 },
    ],
  }));
  await render("scope-plotly-speedup", buildSpeedupFigure({
    colors: { solve: "#eb6834", exchange: "#1baf7a" },
    options: { x_field: "num_ranks", x_label: "MPI ranks", baseline: 1 },
    points: [
      { region: "solve", num_ranks: 1, speedup: 1 }, { region: "solve", num_ranks: 2, speedup: 1.9 }, { region: "solve", num_ranks: 4, speedup: 3.5 },
      { region: "exchange", num_ranks: 1, speedup: 1 }, { region: "exchange", num_ranks: 2, speedup: 1.7 }, { region: "exchange", num_ranks: 4, speedup: 2.6 },
    ],
  }));
  await render("scope-plotly-timeseries", buildDurationTimeseriesFigure({
    colors: { solve: "#eb6834", exchange: "#1baf7a" },
    points: [
      { region: "solve", call_index: 0, time_seconds: 1, mean_duration_seconds: 0.42, min_duration_seconds: 0.39, max_duration_seconds: 0.45 },
      { region: "solve", call_index: 1, time_seconds: 2, mean_duration_seconds: 0.47, min_duration_seconds: 0.43, max_duration_seconds: 0.50 },
      { region: "solve", call_index: 2, time_seconds: 3, mean_duration_seconds: 0.54, min_duration_seconds: 0.50, max_duration_seconds: 0.58 },
      { region: "exchange", call_index: 0, time_seconds: 1, mean_duration_seconds: 0.11, min_duration_seconds: 0.09, max_duration_seconds: 0.13 },
      { region: "exchange", call_index: 1, time_seconds: 2, mean_duration_seconds: 0.13, min_duration_seconds: 0.11, max_duration_seconds: 0.15 },
      { region: "exchange", call_index: 2, time_seconds: 3, mean_duration_seconds: 0.16, min_duration_seconds: 0.14, max_duration_seconds: 0.18 },
    ],
  }));
  await render("scope-plotly-histogram", buildHistogramFigure({
    colors: { solve: "#eb6834", exchange: "#1baf7a" },
    bins: [
      { region: "solve", bin_low_seconds: 0.3, bin_center_seconds: 0.35, bin_high_seconds: 0.4, count: 2 },
      { region: "solve", bin_low_seconds: 0.4, bin_center_seconds: 0.45, bin_high_seconds: 0.5, count: 7 },
      { region: "solve", bin_low_seconds: 0.5, bin_center_seconds: 0.55, bin_high_seconds: 0.6, count: 4 },
      { region: "exchange", bin_low_seconds: 0.1, bin_center_seconds: 0.15, bin_high_seconds: 0.2, count: 9 },
      { region: "exchange", bin_low_seconds: 0.2, bin_center_seconds: 0.25, bin_high_seconds: 0.3, count: 3 },
    ],
  }));
  await render("scope-plotly-heatmap", buildRankHeatmapFigure({
    points: [
      { rank: 0, region: "setup", total_duration_seconds: 0.7 }, { rank: 0, region: "solve", total_duration_seconds: 2.0 }, { rank: 0, region: "exchange", total_duration_seconds: 0.4 },
      { rank: 1, region: "setup", total_duration_seconds: 0.8 }, { rank: 1, region: "solve", total_duration_seconds: 2.4 }, { rank: 1, region: "exchange", total_duration_seconds: 0.5 },
      { rank: 2, region: "setup", total_duration_seconds: 0.7 }, { rank: 2, region: "solve", total_duration_seconds: 2.1 }, { rank: 2, region: "exchange", total_duration_seconds: 0.4 },
    ],
  }));
  await render("scope-plotly-imbalance", buildImbalanceFigure({
    metric: "total", colors: { solve: "#eb6834", exchange: "#1baf7a" },
    points: [
      { region: "solve", rank: 0, value_seconds: 2.0, mean_over_ranks_seconds: 2.2 }, { region: "solve", rank: 1, value_seconds: 2.6, mean_over_ranks_seconds: 2.2 }, { region: "solve", rank: 2, value_seconds: 2.0, mean_over_ranks_seconds: 2.2 },
      { region: "exchange", rank: 0, value_seconds: 0.4, mean_over_ranks_seconds: 0.5 }, { region: "exchange", rank: 1, value_seconds: 0.6, mean_over_ranks_seconds: 0.5 }, { region: "exchange", rank: 2, value_seconds: 0.5, mean_over_ranks_seconds: 0.5 },
    ],
  }));
  await render("scope-plotly-density", buildFigure({
    plot: "density",
    points: [
      { file: "run", region: "solve", bin_start_seconds: 0, bin_end_seconds: 1, occupied_seconds: 0.35 },
      { file: "run", region: "solve", bin_start_seconds: 1, bin_end_seconds: 2, occupied_seconds: 0.92 },
      { file: "run", region: "solve", bin_start_seconds: 2, bin_end_seconds: 3, occupied_seconds: 0.81 },
      { file: "run", region: "exchange", bin_start_seconds: 0, bin_end_seconds: 1, occupied_seconds: 0.05 },
      { file: "run", region: "exchange", bin_start_seconds: 1, bin_end_seconds: 2, occupied_seconds: 0.12 },
      { file: "run", region: "exchange", bin_start_seconds: 2, bin_end_seconds: 3, occupied_seconds: 0.44 },
    ],
  }));
  await render("scope-plotly-callgraph", buildFigure({
    plot: "callgraph",
    regions: [
      { name: "timestep", depth: 0, total_duration: 4.0 },
      { name: "assemble", depth: 1, total_duration: 0.9 },
      { name: "solve", depth: 1, total_duration: 2.6 },
      { name: "precondition", depth: 2, total_duration: 1.1 },
    ],
    edges: [
      { parent: "timestep", child: "assemble" }, { parent: "timestep", child: "solve" },
      { parent: "solve", child: "precondition" },
    ],
  }));
  await render("scope-plotly-summary", buildFigure({
    plot: "region_statistics",
    files: [{ label: "run", region_statistics: {
      setup: { count: 1, total_duration_seconds: 0.7 },
      solve: { count: 24, total_duration_seconds: 5.2 },
      exchange: { count: 24, total_duration_seconds: 1.4 },
      diagnostics: { count: 6, total_duration_seconds: 0.3 },
    } }],
  }, { topN: 4 }));
</script>

## One call for any payload

`buildFigure` reads the `plot` kind stamped on every plot-data document
and picks the builder itself, so a page that renders whatever the
profiler wrote does not need to know which chart it is getting:

``` js
import Plotly from "plotly.js-dist-min";
import { buildFigure, renderFigure } from "@scope-profiler/plotly";

for (const name of ["gantt", "durations", "rank_heatmap"]) {
  const payload = await fetch(`/figures/${name}_data.json`).then((response) => response.json());
  await renderFigure(Plotly, document.querySelector(`#${name}`), buildFigure(payload));
}
```

It throws on a document that is not `scope-profiler-plot-data`, and on
one whose `format_version` is newer than the installed package
understands. For JSON written by a scope-profiler old enough to predate
the envelope, it falls back to `inferPlotKind(payload)`, which reads the
shape of the payload instead; pass `{ plot: "gantt" }` to settle the
case by hand.

## Builders

| Builder | Input payload | Use case |
|----|----|----|
| `buildGanttFigure(payload, { laneBy })` | `gantt_data.json` | Timeline of every call, a lane per region and rank (`laneBy: "rank"` for one row per rank) |
| `buildFlameFigure(payload)` | `flame_data.json` | Nested call hierarchy |
| `buildDurationsFigure(payload, { metric })` | `durations_data.json` | Per-region duration bars |
| `buildSpeedupFigure(payload)` | `speedup_data.json` | Speedup curve |
| `buildDurationTimeseriesFigure(payload)` | `duration_timeseries_data.json` | Duration drift over time |
| `buildHistogramFigure(payload)` | `histogram_data.json` | Call-duration distribution |
| `buildRankHeatmapFigure(payload)` | `rank_heatmap_data.json` | Rank × region duration matrix |
| `buildImbalanceFigure(payload)` | `imbalance_data.json` | Per-rank duration imbalance |
| `buildDensityFigure(payload)` | `timeline_density_data.json` | Binned timeline occupancy, for traces too large to draw call by call |
| `buildCallgraphFigure(payload)` | `callgraph_data.json` | Sankey call graph, from either callgraph shape |
| `buildRegionSummaryFigure(payload, { metric, topN })` | `region_statistics.json` | The slowest regions of a run, ranked |
| `buildLikwidFigure(payload)` | `likwid_data.json` | One LIKWID hardware-counter metric |
| `buildWeakScalingFigure(payload)` | `weak_scaling_data.json` | Weak-scaling curve |
| `buildScalingEfficiencyFigure(payload)` | `scaling_efficiency_data.json` | Parallel-efficiency curve |
| `buildFigure(payload)` | any of the above | Dispatch on the document’s own `plot` kind |

Use `renderFigure(Plotly, element, figure)` as a convenience, or pass
`figure.data` and `figure.layout` to `Plotly.newPlot` directly.

## Payloads with more than one run

`export plot-data` accepts several profiles at once, and every row of
the resulting payload names the run it came from. The builders keep
those runs apart rather than pooling them: the gantt, density and
rank-heatmap charts give each run its own lanes, while the histogram,
imbalance and duration time series give each run its own trace, named
`run / region`. A region keeps its colour across runs – what changes
between runs is the marker symbol on a line, or the bar pattern on a
histogram – so the same region stays recognisable while the runs stay
distinguishable. Single-run payloads are unaffected: series are named by
region alone, as before.
