Skip to content

Commit bcf8d3c

Browse files
authored
Bugfix/issue 2107 (#4025)
Closes #2107
1 parent f0ea529 commit bcf8d3c

File tree

7 files changed

+187
-0
lines changed

7 files changed

+187
-0
lines changed

news/changelog-1.3.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
- Add support for embedding cell outputs in quarto documents using `{{< embed >}}`. You can address cells by Id, Tag, or label, such as `{{< embed mynotebook.ipynb#fig-output >}}` which would embed the output of a cell with the label `fig-output`). You can also provide a list of ids like `{{< embed mynotebook.ipynb#fig-output,tbl-out >}}`.
44
- Only attempt to postprocess `text/plain` output if it's nonempty ([#3896](https://github.com/quarto-dev/quarto-cli/issues/3896)).
5+
- Fix output of bokeh plots so the right number of cells is generated ([#2107](https://github.com/quarto-dev/quarto-cli/issues/2107)).
56

67
## Code Annotation
78

src/core/jupyter/jupyter-fixups.ts

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
/*
2+
* jupyter-shared.ts
3+
*
4+
* Copyright (C) 2020-2023 Posit Software, PBC
5+
*
6+
*/
7+
8+
import {
9+
JupyterNotebook,
10+
JupyterOutput,
11+
JupyterToMarkdownOptions,
12+
} from "./types.ts";
13+
14+
function fixupBokehCells(
15+
nb: JupyterNotebook,
16+
_options: JupyterToMarkdownOptions,
17+
): JupyterNotebook {
18+
for (const cell of nb.cells) {
19+
if (cell.cell_type === "code") {
20+
let needsFixup = false;
21+
for (const output of cell?.outputs ?? []) {
22+
if (output.data === undefined) {
23+
continue;
24+
}
25+
if (output.data["application/vnd.bokehjs_load.v0+json"]) {
26+
needsFixup = true;
27+
}
28+
}
29+
30+
if (!needsFixup) {
31+
continue;
32+
}
33+
const asTextHtml = (data: Record<string, unknown>) => {
34+
if (data["text/html"]) {
35+
return data["text/html"];
36+
}
37+
if (data["application/javascript"]) {
38+
return [
39+
"<script>",
40+
...data["application/javascript"] as string[],
41+
"</script>",
42+
];
43+
}
44+
throw new Error(
45+
"Internal Error: Unknown data types " +
46+
JSON.stringify(Object.keys(data)),
47+
);
48+
};
49+
50+
// bokeh emits one 'initialization' cell once per notebook,
51+
// and then two cells per plot. So we merge the three first cells into
52+
// one, and then merge every two cells after that.
53+
54+
const oldOutputs = cell.outputs!;
55+
56+
const newOutputs: JupyterOutput[] = [
57+
{
58+
metadata: {},
59+
output_type: "display_data",
60+
data: {
61+
"text/html": [
62+
asTextHtml(oldOutputs[0].data!),
63+
asTextHtml(oldOutputs[1].data!),
64+
asTextHtml(oldOutputs[2].data!),
65+
].flat(),
66+
},
67+
},
68+
];
69+
for (let i = 3; i < oldOutputs.length; i += 2) {
70+
newOutputs.push({
71+
metadata: {},
72+
output_type: "display_data",
73+
data: {
74+
"text/html": [
75+
asTextHtml(oldOutputs[i].data!),
76+
asTextHtml(oldOutputs[i + 1].data!),
77+
].flat(),
78+
},
79+
});
80+
}
81+
cell.outputs = newOutputs;
82+
}
83+
}
84+
85+
return nb;
86+
}
87+
88+
const fixups: ((
89+
nb: JupyterNotebook,
90+
options: JupyterToMarkdownOptions,
91+
) => JupyterNotebook)[] = [
92+
fixupBokehCells,
93+
];
94+
95+
export function fixupJupyterNotebook(
96+
nb: JupyterNotebook,
97+
options: JupyterToMarkdownOptions,
98+
): JupyterNotebook {
99+
for (const fixup of fixups) {
100+
nb = fixup(nb, options);
101+
}
102+
return nb;
103+
}

src/core/jupyter/jupyter.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ import { ProjectContext } from "../../project/types.ts";
151151
import { mergeConfigs } from "../config.ts";
152152
import { encode as encodeBase64 } from "encoding/base64.ts";
153153
import { isIpynbOutput } from "../../config/format.ts";
154+
import { fixupJupyterNotebook } from "./jupyter-fixups.ts";
154155

155156
export const kQuartoMimeType = "quarto_mimetype";
156157
export const kQuartoOutputOrder = "quarto_order";
@@ -631,6 +632,9 @@ export async function jupyterToMarkdown(
631632
nb: JupyterNotebook,
632633
options: JupyterToMarkdownOptions,
633634
): Promise<JupyterToMarkdownResult> {
635+
// perform fixups
636+
nb = fixupJupyterNotebook(nb, options);
637+
634638
// optional content injection / html preservation for html output
635639
// that isn't an ipynb
636640
const isHtml = options.toHtml && !options.toIpynb;

src/core/jupyter/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@ export interface JupyterOutput {
144144
execution_count?: null | number;
145145
isolated?: boolean;
146146
metadata?: Record<string, unknown>;
147+
data?: Record<string, unknown>;
147148
}
148149

149150
export interface JupyterOutputStream extends JupyterOutput {
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
---
2+
title: "test webpage"
3+
format:
4+
html:
5+
code-fold: true
6+
jupyter: python3
7+
_quarto:
8+
tests:
9+
html:
10+
ensureFileRegexMatches:
11+
- []
12+
- ["\\?\\(caption\\)"]
13+
---
14+
15+
```{python}
16+
#| label: fig-demo
17+
#| fig-subcap:
18+
#| - "subcap 1"
19+
#| - "subcap 2"
20+
#| fig-cap: "This is a demo figure for debug"
21+
22+
from bokeh.io import output_notebook
23+
output_notebook(hide_banner=True)
24+
25+
from bokeh.palettes import Spectral5
26+
from bokeh.plotting import figure, show
27+
from bokeh.sampledata.autompg import autompg as df
28+
from bokeh.transform import factor_cmap
29+
30+
df.cyl = df.cyl.astype(str)
31+
group = df.groupby('cyl')
32+
cyl_cmap = factor_cmap('cyl', palette=Spectral5, factors=sorted(df.cyl.unique()))
33+
p = figure(height=350, x_range=group, title="MPG by # Cylinders", toolbar_location=None, tools="")
34+
p.vbar(x='cyl', top='mpg_mean', width=1, source=group, line_color=cyl_cmap, fill_color=cyl_cmap)
35+
show(p)
36+
show(p)
37+
```
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
---
2+
title: "test webpage"
3+
format:
4+
html:
5+
code-fold: true
6+
jupyter: python3
7+
_quarto:
8+
tests:
9+
html:
10+
ensureFileRegexMatches:
11+
- []
12+
- ["\\?\\(caption\\)"]
13+
14+
---
15+
16+
```{python}
17+
#| label: fig-demo
18+
#| fig-cap: "This is a demo figure for debug"
19+
20+
from bokeh.io import output_notebook
21+
output_notebook(hide_banner=True)
22+
23+
from bokeh.palettes import Spectral5
24+
from bokeh.plotting import figure, show
25+
from bokeh.sampledata.autompg import autompg as df
26+
from bokeh.transform import factor_cmap
27+
28+
df.cyl = df.cyl.astype(str)
29+
group = df.groupby('cyl')
30+
cyl_cmap = factor_cmap('cyl', palette=Spectral5, factors=sorted(df.cyl.unique()))
31+
p = figure(height=350, x_range=group, title="MPG by # Cylinders", toolbar_location=None, tools="")
32+
p.vbar(x='cyl', top='mpg_mean', width=1, source=group, line_color=cyl_cmap, fill_color=cyl_cmap)
33+
show(p)
34+
```

tests/requirements.txt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,14 @@ async-generator==1.10
44
attrs==21.2.0
55
backcall==0.2.0
66
bleach==3.3.0
7+
bokeh==3.0.3
78
cffi==1.14.5
9+
contourpy==1.0.7
810
cycler==0.10.0
911
decorator==5.0.9
1012
defusedxml==0.7.1
1113
entrypoints==0.3
14+
fonttools==4.38.0
1215
ipykernel==5.5.5
1316
ipython==7.25.0
1417
ipython-genutils==0.2.0
@@ -34,6 +37,7 @@ nest-asyncio==1.5.1
3437
notebook==6.4.0
3538
numpy==1.23.2
3639
packaging==20.9
40+
pandas==1.5.3
3741
pandocfilters==1.4.3
3842
parso==0.8.2
3943
pexpect==4.8.0
@@ -47,6 +51,8 @@ Pygments==2.9.0
4751
pyparsing==3.0.9
4852
pyrsistent==0.17.3
4953
python-dateutil==2.8.1
54+
pytz==2022.7.1
55+
PyYAML==6.0
5056
pyzmq==24.0.1
5157
qtconsole==5.1.0
5258
QtPy==1.9.0
@@ -59,3 +65,4 @@ traitlets==5.0.5
5965
wcwidth==0.2.5
6066
webencodings==0.5.1
6167
widgetsnbextension==3.5.1
68+
xyzservices==2022.9.0

0 commit comments

Comments
 (0)