From cc8bf092759c34ec1d4fea30719f52aaa03713a4 Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Tue, 27 May 2025 16:28:07 +0200 Subject: [PATCH 1/4] test - Add a test for plotly subfigures --- .../smoke-all/jupyter/subfigures/plotly.qmd | 45 +++++++++++++++++++ tests/pyproject.toml | 1 + tests/smoke/smoke-all.test.ts | 4 +- tests/uv.lock | 15 +++++++ tests/verify.ts | 33 ++++++++++++++ 5 files changed, 97 insertions(+), 1 deletion(-) create mode 100644 tests/docs/smoke-all/jupyter/subfigures/plotly.qmd diff --git a/tests/docs/smoke-all/jupyter/subfigures/plotly.qmd b/tests/docs/smoke-all/jupyter/subfigures/plotly.qmd new file mode 100644 index 0000000000..6c9a7e28a9 --- /dev/null +++ b/tests/docs/smoke-all/jupyter/subfigures/plotly.qmd @@ -0,0 +1,45 @@ +--- +title: "Bugged plotly figure: phantom subfigure" +keep-ipynb: true +keep-md: true +_quarto: + tests: + html: + ensureHtmlElements: + - + - 'figure.quarto-float-fig div#fig-gapminder-1 figure.quarto-subfloat-fig div.plotly-graph-div' + - 'figure.quarto-float-fig div#fig-gapminder-2 figure.quarto-subfloat-fig div.plotly-graph-div' + ensureHtmlElementContents: + selectors: + - 'div#fig-gapminder-1 figcaption.quarto-subfloat-caption' + - 'div#fig-gapminder-2 figcaption.quarto-subfloat-caption' + matches: ['\((a|b)\) Gapminder: (1957|2007)'] + ensureHtmlElementCount: + selectors: ['figure.quarto-float-fig figure.quarto-subfloat-fig'] + counts: [2] +--- + +```{python} +#| label: fig-gapminder +#| fig-cap: "Life Expectancy and GDP" +#| fig-subcap: +#| - "Gapminder: 1957" +#| - "Gapminder: 2007" +#| layout-ncol: 2 +#| column: page + +import plotly.express as px +import plotly.io as pio +gapminder = px.data.gapminder() +def gapminder_plot(year): + gapminderYear = gapminder.query("year == " + + str(year)) + fig = px.scatter(gapminderYear, + x="gdpPercap", y="lifeExp", + size="pop", size_max=60, + hover_name="country") + fig.show() + +gapminder_plot(1957) +gapminder_plot(2007) +``` \ No newline at end of file diff --git a/tests/pyproject.toml b/tests/pyproject.toml index 96264cde20..48a47e422b 100644 --- a/tests/pyproject.toml +++ b/tests/pyproject.toml @@ -18,4 +18,5 @@ dependencies = [ "great-tables>=0.17.0", "polars>=1.29.0", "pyarrow>=20.0.0", + "plotly>=6.1.1", ] diff --git a/tests/smoke/smoke-all.test.ts b/tests/smoke/smoke-all.test.ts index 18c8461299..5562c43042 100644 --- a/tests/smoke/smoke-all.test.ts +++ b/tests/smoke/smoke-all.test.ts @@ -36,7 +36,8 @@ import { ensureLatexFileRegexMatches, printsMessage, shouldError, - ensureHtmlElementContents + ensureHtmlElementContents, + ensureHtmlElementCount, } from "../verify.ts"; import { readYamlFromMarkdown } from "../../src/core/yaml.ts"; import { findProjectDir, findProjectOutputDir, outputForInput } from "../utils.ts"; @@ -130,6 +131,7 @@ function resolveTestSpecs( ensureEpubFileRegexMatches, ensureHtmlElements, ensureHtmlElementContents, + ensureHtmlElementCount, ensureFileRegexMatches, ensureLatexFileRegexMatches, ensureTypstFileRegexMatches, diff --git a/tests/uv.lock b/tests/uv.lock index bd449e8ee3..ee1fd350b2 100644 --- a/tests/uv.lock +++ b/tests/uv.lock @@ -1448,6 +1448,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fe/39/979e8e21520d4e47a0bbe349e2713c0aac6f3d853d0e5b34d76206c439aa/platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4", size = 18567, upload-time = "2025-05-07T22:47:40.376Z" }, ] +[[package]] +name = "plotly" +version = "6.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "narwhals" }, + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8a/7c/f396bc817975252afbe7af102ce09cd12ac40a8e90b8699a857d1b15c8a3/plotly-6.1.1.tar.gz", hash = "sha256:84a4f3d36655f1328fa3155377c7e8a9533196697d5b79a4bc5e905bdd09a433", size = 7543694, upload-time = "2025-05-20T20:09:31.935Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/75/f3/f8cb7066f761e2530e1280889e3413769891e349fca35ee7290e4ace35f5/plotly-6.1.1-py3-none-any.whl", hash = "sha256:9cca7167406ebf7ff541422738402159ec3621a608ff7b3e2f025573a1c76225", size = 16118469, upload-time = "2025-05-20T20:09:26.196Z" }, +] + [[package]] name = "polars" version = "1.30.0" @@ -1954,6 +1967,7 @@ dependencies = [ { name = "matplotlib" }, { name = "pandas" }, { name = "papermill" }, + { name = "plotly" }, { name = "polars" }, { name = "pyarrow" }, { name = "seaborn" }, @@ -1972,6 +1986,7 @@ requires-dist = [ { name = "matplotlib", specifier = ">=3.9.2" }, { name = "pandas", specifier = ">=2.2.3" }, { name = "papermill", specifier = ">=2.6.0" }, + { name = "plotly", specifier = ">=6.1.1" }, { name = "polars", specifier = ">=1.29.0" }, { name = "pyarrow", specifier = ">=20.0.0" }, { name = "seaborn", specifier = ">=0.13.2" }, diff --git a/tests/verify.ts b/tests/verify.ts index 8a2e368583..f871dfe22c 100644 --- a/tests/verify.ts +++ b/tests/verify.ts @@ -402,6 +402,39 @@ export const ensureHtmlElementContents = ( } +export const ensureHtmlElementCount = ( + file: string, + options: { + selectors: string[] | string, + counts: number[] | number + } +): Verify => { + return { + name: "Verify number of elements for selectors", + verify: async (_output: ExecuteOutput[]) => { + const htmlInput = await Deno.readTextFile(file); + const doc = new DOMParser().parseFromString(htmlInput, "text/html")!; + + // Convert single values to arrays for unified processing + const selectorsArray = Array.isArray(options.selectors) ? options.selectors : [options.selectors]; + const countsArray = Array.isArray(options.counts) ? options.counts : [options.counts]; + + if (selectorsArray.length !== countsArray.length) { + throw new Error("Selectors and counts arrays must have the same length"); + } + + selectorsArray.forEach((selector, index) => { + const expectedCount = countsArray[index]; + const elements = doc.querySelectorAll(selector); + assert( + elements.length === expectedCount, + `Selector '${selector}' matched ${elements.length} elements, expected ${expectedCount}.` + ); + }); + } + }; +}; + export const ensureSnapshotMatches = ( file: string, ): Verify => { From 9b027588e541602505078777f14073a363c009cd Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Tue, 27 May 2025 16:39:57 +0200 Subject: [PATCH 2/4] Adapt isPlotlyLibrary regex for new plotly.py 6+ inclusion This way the cell content is correctly detected and moved into the head --- src/core/jupyter/widgets.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/core/jupyter/widgets.ts b/src/core/jupyter/widgets.ts index 668685114e..b982b22e0c 100644 --- a/src/core/jupyter/widgets.ts +++ b/src/core/jupyter/widgets.ts @@ -190,9 +190,11 @@ function isWidgetIncludeHtml(html: string) { } function isPlotlyLibrary(html: string) { - return /^\s*