Skip to content

Update Plotly.js to 4.0.0-rc.0 - #19377

Open
James Newton-King (JamesNK) wants to merge 2 commits into
microsoft:mainfrom
JamesNK:jamesnk/update-plotly-4-rc
Open

Update Plotly.js to 4.0.0-rc.0#19377
James Newton-King (JamesNK) wants to merge 2 commits into
microsoft:mainfrom
JamesNK:jamesnk/update-plotly-4-rc

Conversation

@JamesNK

@JamesNK James Newton-King (JamesNK) commented Aug 14, 2026

Copy link
Copy Markdown
Member

Description

Updates the Plotly.js basic bundle used by the dashboard metrics graph from 2.35.2 to 4.0.0-rc.0. This keeps the metrics visualization on the upcoming Plotly.js major release while retaining the smaller basic distribution used for scatter traces.

Validation:

  • dotnet build src/Aspire.Dashboard/Aspire.Dashboard.csproj --no-restore
  • PlotlyChartTests (4 passed)
  • Browser smoke test rendering and updating stacked scatter and exemplar marker traces with Plotly.version reporting 4.0.0-rc.0

Fixes # (issue)

Checklist

  • Is this feature complete?
    • Yes. Ready to ship.
    • No. Follow-up changes expected.
  • Are you including unit tests for the changes and scenario tests if relevant?
    • Yes
    • No
  • Did you add public API?
    • Yes
      • If yes, did you have an API Review for it?
        • Yes
        • No
      • Did you add <remarks /> and <code /> elements on your triple slash comments?
        • Yes
        • No
    • No
  • Does the change make any security assumptions or guarantees?
    • Yes
      • If yes, have you done a threat model and had a security review?
        • Yes
        • No
    • No

Copilot AI balanced review requested due to automatic review settings August 14, 2026 03:08
@github-actions

Copy link
Copy Markdown
Contributor

🚀 Dogfood this PR with:

⚠️ WARNING: Do not do this without first carefully reviewing the code of this PR to satisfy yourself it is safe.

curl -fsSL https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.sh | bash -s -- 19377

Or

  • Run remotely in PowerShell:
iex "& { $(irm https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.ps1) } 19377"

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Updates the dashboard metrics chart to use Plotly.js Basic 4.0.0-rc.0.

Changes:

  • Updates the Plotly.js bundle import from 2.35.2 to 4.0.0-rc.0.

💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.

@github-actions

Copy link
Copy Markdown
Contributor

Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt.

@adamint Adam Ratzman (adamint) left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did not find a code issue here. I verified the vendored Plotly bundle is byte-for-byte the official plotly-basic-4.0.0-rc.0 asset. I was not able to exercise the browser rendering path in this environment, so leaving this as a comment rather than an approval.

@mitchdenny

Copy link
Copy Markdown
Member

PR Testing Report

PR Information

  • PR Number: Update Plotly.js to 4.0.0-rc.0 #19377
  • Title: Update Plotly.js to 4.0.0-rc.0
  • Head Commit: 37fc8fde7f5dbe63372168e6adec5a4aa5b53af9
  • Tested At: 2026-08-16T07:55Z
  • Execution target: Local (temp directory), macOS arm64

Artifact Version Verification

  • Expected Commit: 37fc8fde7f5dbe63372168e6adec5a4aa5b53af9
  • Installed Version: 13.6.0-pr.19377.g37fc8fde (dogfood CLI)
  • Status: ✅ Verified

Chain of custody — the tested bytes are provably the PR's bytes:

Step Evidence
Dogfood CLI version contains PR short SHA 13.6.0-pr.19377.g37fc8fde
Aspire.Dashboard.Sdk.osx-arm64 nupkg from the PR hive ships only plotly-basic-4.0.0-rc.0.min.js; old file absent
Dashboard binary from that nupkg, served over HTTP GET /js/plotly-basic-4.0.0-rc.0.min.js200, 1,130,485 bytes
Old bundle removed GET /js/plotly-basic-2.35.2.min.js404
Upstream authenticity SHA-256 of vendored file matches npm plotly.js@4.0.0-rc.0/dist/plotly-basic.min.js byte-for-byte (e19a9643a8a9…e257dc)
Runtime confirmation window.Plotly.version === 4.0.0-rc.0

Changes Analyzed

Files Changed

  • src/Aspire.Dashboard/wwwroot/js/app-metrics.js — Modified (1 line: the import path)
  • src/Aspire.Dashboard/wwwroot/js/plotly-basic-2.35.2.min.js — Deleted
  • src/Aspire.Dashboard/wwwroot/js/plotly-basic-4.0.0-rc.0.min.js — Added

Change Categories

  • CLI changes
  • Hosting integration changes
  • Dashboard changes (vendored third-party charting bundle, major version bump 2.35.2 → 4.0.0-rc.0)
  • Template changes
  • Client/Component changes
  • CI infrastructure changes
  • VS Code extension changes
  • Test changes

Why extra rigor was warranted

CI is fully green (363 success / 4 skipped), but no existing test executes this bundle:

  • PlotlyChartTests asserts against bUnit's mocked JSInterop.Invocations — the real JS never runs.
  • The only Playwright /metrics case (DashboardPage_HasNoSeriousOrCriticalWcagViolations) stops at "select a resource" and never renders a chart.

Green CI therefore provides no signal on this change. All testing below was purpose-built.

Test Method

Because aspire start failed repeatedly in this environment on unrelated transient MSBuild races
(CS0006 / empty obj/.../ref/, reproducible on main, not a PR defect), testing pivoted to
running the PR's own dashboard binary standalone and driving the real app-metrics.js module
against the real shipped bundle via Playwright. This is a stronger vehicle: it allows deterministic
control of trace/exemplar payloads, including exemplar traceData, which a sample app cannot easily produce.

Two harnesses were used:

  1. Live harness — Chromium against the running PR dashboard (http://localhost:18888), importing
    /js/app-metrics.js and replicating the exact interop payload shape from PlotlyChart.razor.cs.
  2. A/B harness — the same, unmodified app-metrics.js run against both 2.35.2 (restored from
    the merge base) and 4.0.0-rc.0, to separate genuine regressions from pre-existing behavior.

Test Scenarios Executed

Scenario 1: Bundle identity & API surface

Objective: Verify the shipped bundle is the intended upstream artifact and exposes every API app-metrics.js calls.
Coverage Type: Happy path
Status: ✅ Passed

Plotly.version = 4.0.0-rc.0; newPlot, update, register, Plots.resize all present. Old bundle 404s.


Scenario 2: Chart renders with real trace data

Objective: Verify a 3-series stacked area chart + exemplar trace renders.
Coverage Type: Happy path
Status: ✅ Passed

3 SVG elements, 4 traces bound, 3 js-fill stack fills, 4 exemplar .point markers, legend with 4 entries.
fixTraceLineRendering correctly reordered the point trace last (4 trace groups).

Evidence: 01-chart-initial.png


Scenario 3: Exemplar hover → pointer cursor

Objective: Verify plotly_hover still fires and .nsewdrag cursor logic works in v4.
Coverage Type: Happy path
Status: ✅ Passed

Real mouse hover over an exemplar marker sets .nsewdrag cursor to pointer; tooltip renders.

⚠️ Harness note: an initial run reported a false failure because it used the programmatic
Plotly.Fx.hover() API, which does not drive the same internal path as a genuine pointer move.
Confirmed identical on 2.35.2, so this was a harness artifact, not a v4 regression.


Scenario 4: Exemplar click → ViewSpan interop

Objective: Verify custom traceData survives into the click handler and reaches .NET.
Coverage Type: Happy path
Status: ✅ Passed

Clicking an exemplar invoked ViewSpan("trace-4", "span-4") — the correct traceId/spanId pair
for the hovered point. The undocumented traceData round-trip through Plotly's data model is intact.


Scenario 5: updateChart / Plotly.update path

Objective: Verify live chart updates work and don't drop custom data.
Coverage Type: Happy path
Status: ✅ Passed

y values updated [10, 10.87, 11.65] → [20, 21.74, 23.29]; all 4 exemplar points survived the update.

Evidence: 02-chart-updated.png


Scenario 6: FluentUI theme color interop

Objective: v4 replaced the internal TinyColor dependency with a new color library — verify
Aspire's CSS-custom-property-derived colors are still accepted.
Coverage Type: Happy path / regression risk
Status: ✅ Passed

getThemeColors() resolved --fill-color, --neutral-foreground-rest, --accent-fill-rest, and Plotly
applied them without a parse failure (paper_bgcolor=#f7f7f7, exemplar marker.color=#6f3dff).
A/B identical to 2.35.2. This was the highest-risk unknown going in; it is now cleared.


Scenario 7: Locale registration

Objective: Verify Plotly.register with case "locale" still applies server-supplied time formats.
Coverage Type: Happy path
Status: ✅ Passed

X-axis ticks rendered as 7:50:30 AM, 7:51:00 AM, 7:51:30 AM — server locale honored.


Scenario 8: No cloud-upload surface exposed (security)

Objective: Verify v4's flipped showSendToCloud default is not user-reachable in the dashboard.
Coverage Type: Unhappy path / security
Status: ✅ Passed (with a caveat — see Findings)

  • 0 modebar elements rendered, 0 "Share Chart" buttons in the DOM.
  • 0 network requests to plotly.com / plot.ly across the entire session.

Scenario 9: Empty dataset

Objective: Boundary case — a metric with no data points.
Coverage Type: Unhappy path / boundary
Status: ⚠️ Pre-existing issue (identical on 2.35.2 — not a regression)

Both versions throw TypeError: Cannot read properties of null (reading 'childNodes') from
fixTraceLineRendering, which does not null-check chartDiv.querySelector(".scatterlayer").
Neither version emits a .scatterlayer when all traces are empty. The chart SVG still renders.

Evidence: 03-chart-empty.png


Scenario 10: Rapid re-initialize, hide/show, resize

Objective: Unhappy path — thrash the chart lifecycle the way tab switching / range changes do.
Coverage Type: Unhappy path / boundary
Status: ✅ Passed

5 rapid initializeChart calls threw nothing; no DOM accumulation (1 child div); chart still rendered
(3 SVG, 4 points); hide → show → Plots.resize handled cleanly (width 700).

Evidence: 04-chart-resized.png


Scenario 11: JS error surface

Objective: Catch any silent breakage.
Coverage Type: Happy path
Status: ✅ Passed

0 console errors, 0 page errors, 0 external network requests for the whole session.

A/B Comparison: 2.35.2 vs 4.0.0-rc.0

Same app-metrics.js, both bundles, identical payloads:

Check 2.35.2 4.0.0-rc.0 Verdict
SVG rendered 3 3 same
Exemplar points 4 4 same
Stack fills 3 3 same
paper_bgcolor applied #f7f7f7 #f7f7f7 same
Exemplar marker color #6f3dff #6f3dff same
Hover → cursor pointer pointer same
Click → ViewSpan args ["trace-4","span-4"] ["trace-4","span-4"] same
Empty dataset throws TypeError throws TypeError same (pre-existing)
Console/page errors 0 0 same

No rendering or behavioral regression was found in the upgrade.

Findings

🟠 The v4 cloud-upload default flip is real, and only one line suppresses it

Probing the libraries' own config defaults (no displayModeBar override):

Config default 2.35.2 4.0.0-rc.0
showSendToCloud false true
plotlyServerURL "" https://cloud.plotly.com/newchart
Cloud/share button in modebar absent present

Rendered modebar buttons:

  • 2.35.2: Download plot as a png, Zoom, Pan, Box Select, Lasso Select, Zoom in, Zoom out, Autoscale, Reset axes
  • 4.0.0-rc.0: Download plot as a PNG, Share Chart , Zoom, Pan, Box Select, Lasso Select, Zoom in, Zoom out, Autoscale, Reset axes

Today this is not exploitableapp-metrics.js:83 passes displayModeBar: false, and that is the
only Plotly.newPlot call site, so the modebar is destroyed before the button exists (verified: 0 modebar,
0 share buttons, 0 outbound requests).

But: with displayModeBar: false, gd._context.showSendToCloud is still true on v4 (vs false on 2.35.2).
The only thing standing between a user's telemetry and cloud.plotly.com is that single boolean.
Anyone enabling the modebar later — a reasonable, innocuous-looking change — silently ships a
"Share Chart" button that window.opens plotly's cloud and postMessages the full chart JSON.
CSP does not help: default-src 'self' (BrowserSecurityHeadersMiddleware.cs:57) governs neither
window.open nor postMessage, and it is not applied in Development at all.

Recommendation (cheap, permanent): add to the config object in app-metrics.js:

showSendToCloud: false,
plotlyServerURL: ''

🟠 Shipping a pre-release into production

npm dist-tags at time of testing: latest = 3.7.0 (stable), rc = 4.0.0-rc.0. 4.0.0 final is unpublished.
This PR jumps 2.35.2 → a 4.x release candidate, skipping the entire stable 3.x line. Worth confirming this
is deliberate and that there's a tracking item to move to 4.0.0 final once released.

🟡 No automated test covers the chart JS

This upgrade could have broken exemplar hover, click-to-trace, theming, or locale formatting and every
existing test would still pass. Consider adding a Playwright case that actually renders a metrics chart.

🔵 Minor / pre-existing

  • fixTraceLineRendering should null-check .scatterlayer (pre-existing; reproduces on 2.35.2).
  • .config/PoliCheckExclusions.xml:3 still references plotly-2.32.0.min.js (already stale on main).
  • THIRD-PARTY-NOTICES.TXT:16 says "Copyright (c) 2021 Plotly, Inc"; the new bundle header says "Copyright 2012-2026".

Summary

Scenario Status Notes
1. Bundle identity & API surface ✅ Passed Upstream hash match; old bundle 404
2. Chart renders with real data ✅ Passed Stacks, exemplars, legend all correct
3. Exemplar hover → cursor ✅ Passed A/B identical to 2.35.2
4. Exemplar click → ViewSpan ✅ Passed traceData round-trip intact
5. updateChart path ✅ Passed Data updates, exemplars survive
6. FluentUI theme colors ✅ Passed New color lib accepts Aspire tokens
7. Locale registration ✅ Passed Server time format applied
8. No cloud-upload surface ✅ Passed 0 requests to plotly.com — see Findings
9. Empty dataset ⚠️ Pre-existing Identical failure on 2.35.2
10. Re-init / hide-show / resize ✅ Passed No leaks, no throws
11. JS error surface ✅ Passed 0 console / 0 page errors

Automated checks: 25 passed, 0 failed, 1 pre-existing (verified identical on 2.35.2).

Overall Result

✅ PR FUNCTIONALLY VERIFIED — no regression found

The bundle is authentic, the upgrade is behaviorally identical to 2.35.2 across every path
app-metrics.js exercises, and no telemetry leaves the browser.

Recommendations

  1. Add showSendToCloud: false and plotlyServerURL: '' to the Plotly config — 2 lines, removes a
    silent data-egress footgun that the v4 defaults introduced.
  2. Confirm the pre-release is intentional and track moving to 4.0.0 final.
  3. (Optional) Add a Playwright test that renders a metrics chart, so the next bundle bump has real coverage.
  4. (Optional, pre-existing) Null-check .scatterlayer in fixTraceLineRendering; refresh the stale
    PoliCheck exclusion and third-party notice copyright year.

Plotly 4.x flipped the showSendToCloud config default from false to true and
set plotlyServerURL to https://cloud.plotly.com/newchart. With the modebar
displayed, that renders a "Share Chart" button which window.open()s Plotly's
cloud service and postMessage()s the full chart JSON to it.

Verified against both bundles with the same app-metrics.js:

  config default    2.35.2    4.0.0-rc.0
  showSendToCloud   false     true
  plotlyServerURL   ""        https://cloud.plotly.com/newchart
  share button      absent    "Share Chart"

The dashboard passes displayModeBar: false, so the button is not reachable
today. But showSendToCloud remained true on the graph context, meaning a
later change that enables the modebar would silently ship a control that
uploads user telemetry to a third party. CSP does not mitigate it: neither
window.open nor postMessage is covered by default-src 'self', and the policy
is not applied in Development at all.

Setting both values explicitly makes the guarantee independent of the modebar
setting. Confirmed the "Share Chart" button no longer appears even when
displayModeBar is forced to true.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 16, 2026 08:01
@mitchdenny

Copy link
Copy Markdown
Member

James Newton-King (@JamesNK) I pushed one commit to this branch — 2b974f5 — please confirm you're happy with it, and revert it if you'd rather handle this differently.

What it does: adds showSendToCloud: false and plotlyServerURL: '' to the Plotly config in app-metrics.js.

Why: Plotly 4.x flipped these defaults. Verified empirically by running the same app-metrics.js against both bundles:

config default 2.35.2 4.0.0-rc.0
showSendToCloud false true
plotlyServerURL "" https://cloud.plotly.com/newchart

Rendered modebar buttons:

  • 2.35.2: Download plot as a png, Zoom, Pan, Box Select, …
  • 4.0.0-rc.0: Download plot as a PNG, Share Chart , Zoom, Pan, Box Select, …

Share Chart calls window.open() on Plotly's cloud and postMessage()s the full chart JSON — which for us is user telemetry.

This was not a live bug. displayModeBar: false already hides the button, and I confirmed zero requests to plotly.com across the whole test session. But showSendToCloud stayed true on the graph context, so anyone enabling the modebar later — a small, innocuous-looking change — would silently ship that button. CSP doesn't help: neither window.open nor postMessage is covered by default-src 'self', and the policy isn't applied in Development at all.

Verified after the change: showSendToCloud=false, plotlyServerURL="", chart renders identically, and forcing displayModeBar: true no longer produces a Share Chart button. Full harness still 25/25.

Happy to drop it if you'd prefer to keep the diff purely mechanical and handle this separately.

@mitchdenny Mitch Denny (mitchdenny) left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving. The bundle is authentic and the upgrade is behaviorally identical to 2.35.2 across every path the dashboard exercises.

Supply chain verified. SHA-256 of the vendored file matches upstream npm plotly.js@4.0.0-rc.0/dist/plotly-basic.min.js byte-for-byte (e19a9643a8a9…e257dc). Traced end to end: dogfood CLI 13.6.0-pr.19377.g37fc8fdeAspire.Dashboard.Sdk nupkg ships only the new bundle → served 200, old one 404s → window.Plotly.version === 4.0.0-rc.0 at runtime.

Behavior verified, not assumed. Nothing in CI executes this bundle — PlotlyChartTests asserts against bUnit's mocked JSInterop, and the only Playwright /metrics case never renders a chart. So I ran the real app-metrics.js against the real shipped artifact, and separately A/B'd the same, unmodified module against both bundles. Rendering, stacked areas, exemplar markers, hover → pointer cursor, click → ViewSpan(traceId, spanId), the traceData round-trip, Plotly.update, locale registration, and FluentUI theme colors through v4's new color library are all identical to 2.35.2. Zero console errors, zero page errors, zero external requests. 25 passed, 0 failed.

The one crash I found (empty dataset → TypeError in fixTraceLineRendering, which doesn't null-check .scatterlayer) reproduces identically on 2.35.2, so it's pre-existing and not a blocker for this PR.

Disclosure: I pushed 2b974f5 to this branch myself, so my approval covers a commit I authored. It sets showSendToCloud: false and plotlyServerURL: '' — v4 flipped those defaults and renders a Share Chart button that uploads chart JSON to cloud.plotly.com. Not reachable today given displayModeBar: false, but the flag stayed true on the graph context. James Newton-King (@JamesNK), please revert it if you'd rather keep this diff purely mechanical — the approval stands either way.

One thing worth confirming before merge: this ships a pre-release. npm latest is 3.7.0; 4.0.0 final is unpublished. The PR jumps 2.35.2 → 4.0.0-rc.0, skipping the stable 3.x line entirely. Approving on the assumption that's deliberate — a tracking item to move to 4.0.0 final once it ships would be good.

Optional follow-ups, none blocking: add a Playwright test that actually renders a metrics chart so the next bundle bump has real coverage; null-check .scatterlayer; refresh .config/PoliCheckExclusions.xml:3 (still references plotly-2.32.0.min.js, already stale on main) and the THIRD-PARTY-NOTICES.TXT:16 copyright year.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review details

  • Files reviewed: 1/3 changed files
  • Comments generated: 1
  • Review effort level: Balanced

@@ -1,4 +1,4 @@
import './plotly-basic-2.35.2.min.js'
import './plotly-basic-4.0.0-rc.0.min.js'
@github-actions

Copy link
Copy Markdown
Contributor

Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants