Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
mystmd supports structured template options, so that migration is mechanical
([#205](https://github.com/QuantEcon/quantecon-theme.mystmd/issues/205)) ([#224](https://github.com/QuantEcon/quantecon-theme.mystmd/pull/224)).

### Fixed
- Images in a notebook cell's outputs are centred in the content column, as
they are on the Sphinx lecture sites. An inline matplotlib figure is narrower
than the column, so it sat against the left edge with all the spare width
beside it — 0px to its left and 232px to its right, for a 568px plot in the
800px column. Both the stored outputs and the ones re-rendered when a reader
starts live compute are covered. Tables and text outputs stay left-aligned
([#206](https://github.com/QuantEcon/quantecon-theme.mystmd/issues/206)) ([#225](https://github.com/QuantEcon/quantecon-theme.mystmd/pull/225)).

## [2.7.0] - 2026-09-11

### Added
Expand Down
22 changes: 22 additions & 0 deletions styles/quantecon.css
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,28 @@
.article li > p {
margin: 0;
}

/* Images in a cell's outputs are centred in the content column, matching the
`.cell_output img` rule the lecture builds' stylesheet carries; an inline
matplotlib figure is narrower than the column, so without this it sits
against the left edge with all the spare width beside it.

A plot output is a bare `<img>` that upstream renders with neither class
nor style, inside a container the upstream code marks by `data-name`, so
the container attribute is what there is to select on. `text-align` on the
container cannot do it: Preflight makes the image a block. Both container
names are needed -- the stored outputs are re-rendered into
`active-outputs-container` once a reader starts live compute, and the plot
should not jump left at that moment.

Only the side margins are set, so the vertical rhythm is untouched, and
only images: tables and text outputs stay left-aligned, as they are on the
lecture sites. */
[data-name='outputs-container'] img,
[data-name='active-outputs-container'] img {
margin-left: auto;
margin-right: auto;
}
}

@layer components {
Expand Down
Binary file modified tests/visual/__snapshots__/desktop-chrome-darwin/notebook.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified tests/visual/__snapshots__/desktop-chrome-linux/notebook.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified tests/visual/__snapshots__/mobile-chrome-darwin/notebook.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified tests/visual/__snapshots__/mobile-chrome-linux/notebook.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
22 changes: 22 additions & 0 deletions tests/visual/fixture/notebook.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,28 @@
"source": [
"raise ValueError(\"a deliberate error to render the traceback\")"
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {},
"outputs": [
{
"data": {
"image/png": "iVBORw0KGgoAAAANSUhEUgAAAjgAAAFACAIAAAA+jpGQAAAJTklEQVR42u3dPW7bSBiAYVlgtRdgm24B1QZcq9oile6wtQF3OUI6t+r2CKpSsw5gYK+QVqdQCgOKYUmURA3Fb2aep9pfxx6S8/KjZPtht9vNACCquSUAQKgAQKgAECoAECoAECoAhAoAhAoAoQIAoQIAoQJAqABAqAAQKgAQKgAQKgCECgCECgChAgChAgChAkCoAECoAKhdk/sXsFqtZrPZ8/OzYwkQzXK5FKqUawFAQl3XJfk4Hv0BEJpQASBUACBUAAgVAAgVAAgVAEIFAEIFgFABgFABgFABIFQAIFQACBUACBUACBUAQgUAQgWAUAGAUAGAUAEgVAAgVAAIlSUAQKgAQKgAECoAECoAECoAhAoAhAoAoQIAoQIAoQJAqABAqAAQKgAQKgAQKgCECgCECgChAgChAgChAkCoAOAOGkswoS/ffvT821/fv1oigEJCtd1us/g8n17fBmfs58uj8xXIyGKxEKo/2rbNd3K6NnImLSALXdcl2Zw9+ps+UYfh6f+/3v+tXAGVEKoJEnW2MZ/+g6MfSq4AoSJxogZHZf8/Hn5kuQKEigSVShWS948jV4BQES5RcgVUyzf8jlWpX9+/jtqMUx8/yTsMAYSq/Erd548+mqsv337IFSBUHK/U2IPUVblydAChqj1Rh5Wa8PPRKkCo6GtAhHcxHI5WWgUIlUpFqdSpT8ZLVoBQVV2pSV6UurZVRitAqOqtVNhPVasAoVKp6N9a6yUrQKhUKgNaBQiVSmkVgFBF3fS1CkCoAo1TWf/IV60ChEqltApAqCaqVDG0ChCqMitV0u950ipAqFRKqwCEaqI9XasAhCrQOFX2b3bXKkCoVEqrAIRqnErVQ6sAoSph+9YqAKEKNE5VVSmtAoRKpQyRAEJF6lYZqgChMk5pFYBQqVS69QEQKsINVVoFCJVxSqsAhEqltAoQKopvFYBQGadCt8pQBQiVSmkVgFCRLvMAQmWcCjdUaRUgVCqlVQAVT1SkahWAUNlzQ6+boQoQqsT2G6tKaRWQi8Ysxe35t6Rkes9q3xCq7E9Nei7aj+upVRR/1R/9UE57oRrlfHVijdQqqPCW9NOfZXsRKrNU6FYZqsjuGr/wjL38g/vul5E87Ha7rL+A1Wo1m83W63XPf/P0+rb/658vj456WpaXCOdejzFOywv/aNdF27YmquvWwj3OHTYOi0yE+Wns8/Cq73/fV622q6PruiShKmSi2mw2Z89pG+jdNg5LzSR9CnLiRehonFAtl0sT1cDbH5IvrxcCmWrrj3Z1X/KNhm6gheq6WxuSt8obK7jDJZzFObb/JBXrFiU/+nMGTLutWHbGSFTu51Ve0+GNPPozS2VwCLQKibpqxnJ7XVeomOoi9BMrkKhUxXL51BIqR3raVoFEXbJHyVWPMl+jMj6H2nccBSTq9gXJcU1SvUZV8q/5sD8GWXwDFqd25FM/6bXmi7fnyz+1YsVrijz7bQEBD4r7BkxR197qeR5Y+ETljI92CNxA0HMmVD5F9U9XR1emqumqqeEaYMLLzBHhbKKszOABq5LX4+cFH1SiHQvRUilT1O0D1qnlLfj6mpd9JaBVRLgwj1bKysjVhZoij6JTOfi25RhVPkhZloQb3anngSWt87zg64Gwdw8OlkGKsQeskqareXnHzInr0BBzkHIOyNUwhfxkiv///tdumN3+5WBVkijHOsIhmORA+MkU7tmzP0YeABqkuOd0le+ANXdQiXnrR3aH0itScjWSch79uSTy7ZNjV+QgZVmyuCkc9Uh59Efe93oWQaWIMGBlMV2V/KvoyWiDs7VJFJNPV8mPoInqzHKT11zlUKoUk09XYQesee7L7Zyr4S6PaEdKpeTqnjz6I1af7HcGKaLdKQ4+xB79Uc5tnblKpTBgmagwVyFRJJiurjr0qSaqxiEhyN2cWUqliHM99hTr/r+t0aM/wl0bMw8AVYowV2XP4b7bI0GP/oi7LdoQJYrgp0T/6eHRH1VcFXZGlSLUgNVTrPHuMk1URN8i7Y8qRY4DVsIzR6jQKiw+Ixbr9lNIqLBdYpAi9JXrXX8E5RuBVYpMr9z9ewX/++evJB9TqNAqVIrRr1+hQqsYnqjDZzUqRShChVYZpAxSCBVolUqBUKFVXJ4oj/sQKtAqgxQIFVqlVSqFUIFWFZMoj/sQKtAqgxQIFWjVbYOUSiFUoFWhBymVIjuF/FDa9XrtWNbp6fXt49/+fHm0JkdXxuIwibZtb/8gjbUg97nq49zw9PpmYvCsjyC6rkuyOXv0Rwmt+rRN1/wYUKUoj1BRYKtmVb5k5X0TCBVoVWaDlEohVBCuVRW+FdAgRfEaS0B5ufq4cb//dZEb99EMSxQmKsijVZfs6SoFQgVaNUqivCJFVTz6o/BWlfQY0BSFiQpqGa1ynK5UCqGCilo1y+pJoGd9VM6jPypq1aftPv6TQFMUCBVyFTFXPdOeSiFUUEWuDksQJFcSBUIFJ0eraXMlUSBUcF2u7lMIfQKhgoG5GrVY/e85lCgQKrgiV5/++bCKXPhueIkCoYKLOtHTlbNvGR/wHVr6BEIF6Ys1YGDSJxAqGKtYsxQ/zEKZQKjgTtE6Wy9NAqGCuPUCEvJDaQEQKgAQKgCECgCECgCECgChAgChAkCoAECoAECoABAqABAqAIQKAIQKAIQKAKECAKECQKgAQKgAQKgAECoAECoAhMoSACBUACBUAJSoKePL2G63jiVAKIvFQqj+aNvWOQEQStd1STZnj/4ACE2oABAqABAqAIQKAIQKAIQKAKECAKECQKgAQKgAQKgAECoAECoAhAoAhAoAhAoAoQIAoQJAqABAqABAqAAQKgAQKgCEyhIAIFQAIFQACBUACBUACBUAQgUAQgWAUAGAUAGAUAEgVAAgVAAIFQAIFQAIFQBCBQBCBYBQAYBQAYBQASBUACBUAAgVAAT2sNvtsv4CVquVowgQ1mazMVEBULJGscv2PnFaH+tjfazPVOtzOxMVAKEJFQBCBQBCBUCRsn97OgAmKgAQKgAQKgCECgCECgChAgChAgChAkCoAECoABAqABAqABAqAIQKAIQKAKECAKECAKECQKgAQKgAQKgAECoAECoAhAoAhAoAhAoAoQIAoQJAqABAqABAqAAQKgAQKgCECgCECgCECgChAgChAkCoAECoAGCI31swx1/6rU7DAAAAAElFTkSuQmCC",
"text/plain": [
"<Figure size 568x320 with 1 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"fig, ax = plt.subplots()\n",
"ax.plot(x, y)\n",
"plt.show()"
]
}
],
"metadata": {
Expand Down
51 changes: 51 additions & 0 deletions tests/visual/theme.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -523,6 +523,57 @@ test.describe("Meta/SEO and notebook output", () => {
await expect(fold.locator("pre.jupyter-error")).toBeVisible();
await expect(fold.locator("pre.jupyter-error")).toContainText("a deliberate warning on stderr");
});

// Plot outputs are centred in the content column, as they are on the lecture
// sites; an inline figure narrower than the column would otherwise sit
// against its left edge. On the thebe-enabled fixture, whose notebook.ipynb
// carries the stored `image/png` cell -- the no-thebe copy does not.
test("output-image-centred", async ({ page }, testInfo) => {
await page.goto("/notebook", { waitUntil: "domcontentloaded" });
await settle(page);
const box = await page.evaluate(() => {
const img = document.querySelector('[data-name="outputs-container"] img');
if (!img) return null;
const container = img.closest('[data-name="outputs-container"]')!;
const i = img.getBoundingClientRect();
const c = container.getBoundingClientRect();
return {
left: i.left - c.left,
right: c.right - i.right,
imageWidth: i.width,
containerWidth: c.width,
scrollWidth: document.documentElement.scrollWidth,
clientWidth: document.documentElement.clientWidth,
};
});
expect(box, "the fixture notebook should render an image output").not.toBeNull();
// Never wider than the column it sits in, at either viewport.
expect(box!.imageWidth).toBeLessThanOrEqual(box!.containerWidth + 1);
// The same 1px allowance as the gaps below: both widths are integers
// rounded from sub-pixel layout, so a page that fits exactly can still
// report one more pixel of scrollWidth than clientWidth. The two values are
// returned rather than a boolean so a failure names them -- and the sweep
// across 1280/1300/1328px in `outline-within-viewport` is what actually
// guards page overflow; this is a sanity check on the page holding an image.
expect(box!.scrollWidth, "no horizontal page overflow").toBeLessThanOrEqual(
box!.clientWidth + 1
);
if (testInfo.project.name === "desktop-chrome") {
// Desktop has room to spare, so the gaps must match.
expect(box!.left).toBeGreaterThan(1);
expect(Math.abs(box!.left - box!.right)).toBeLessThanOrEqual(1);
}
// Text outputs are untouched: only images are centred, as on the lecture
// sites, where a DataFrame table stays left-aligned.
const stream = await page.evaluate(() => {
const pre = document.querySelector('[data-name="outputs-container"] pre');
if (!pre) return null;
const container = pre.closest('[data-name="outputs-container"]')!;
return pre.getBoundingClientRect().left - container.getBoundingClientRect().left;
});
expect(stream, "the fixture notebook should render a text output").not.toBeNull();
expect(Math.abs(stream!)).toBeLessThanOrEqual(1);
});
});

test.describe("Site options reach the theme", () => {
Expand Down
Loading