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
19 changes: 19 additions & 0 deletions app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -466,6 +466,25 @@ button:disabled {
color: var(--accent-deep);
transform: none;
}
.file-menu-export-options {
margin: 0 10px 6px 20px;
padding-left: 6px;
}
.file-menu-export-options button {
font-size: 12px;
gap: 8px;
min-height: 30px;
padding: 6px 10px;
}
.file-menu-export-options button::before {
border: 1px solid var(--accent-deep);
border-radius: 50%;
content: "";
flex: 0 0 12px;
height: 12px;
outline: 1px solid var(--accent-deep);
outline-offset: -4px;
}
.file-menu-divider {
border-top: 1px solid var(--line);
margin: 5px 3px;
Expand Down
40 changes: 33 additions & 7 deletions components/export-tools.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ function canvasBlob(canvas: HTMLCanvasElement) {
});
}

function renderChart(document: ChartDocument) {
function renderChart(document: ChartDocument, showGrid: boolean) {
const aspect = cellAspectRatio(document);
const maximumChartDimension = 4000;
const cellHeight = Math.min(
Expand Down Expand Up @@ -129,7 +129,7 @@ function renderChart(document: ChartDocument) {
}
}

for (let column = 0; column <= document.width; column += 1) {
for (let column = 0; showGrid && column <= document.width; column += 1) {
context.beginPath();
context.strokeStyle =
column % 5 === 0 ? "rgba(28,31,29,.72)" : "rgba(28,31,29,.42)";
Expand All @@ -139,7 +139,7 @@ function renderChart(document: ChartDocument) {
context.lineTo(x, top + chartHeight);
context.stroke();
}
for (let row = 0; row <= document.height; row += 1) {
for (let row = 0; showGrid && row <= document.height; row += 1) {
context.beginPath();
context.strokeStyle =
row % 5 === 0 ? "rgba(28,31,29,.72)" : "rgba(28,31,29,.42)";
Expand Down Expand Up @@ -205,6 +205,7 @@ export function ExportTools({
const inputRef = useRef<HTMLInputElement>(null);
const menuRef = useRef<HTMLDivElement>(null);
const [open, setOpen] = useState(false);
const [choosingChartImage, setChoosingChartImage] = useState(false);
const [message, setMessage] = useState("");

useEffect(() => {
Expand Down Expand Up @@ -238,11 +239,11 @@ export function ExportTools({
setMessage("Editable KnitPlot file downloaded.");
}

async function downloadChart() {
async function downloadChart(showGrid: boolean) {
try {
downloadBlob(
await canvasBlob(renderChart(document)),
fileName(document.name, "chart.png"),
await canvasBlob(renderChart(document, showGrid)),
fileName(document.name, showGrid ? "chart.png" : "chart-no-grid.png"),
);
setMessage("Chart downloaded.");
} catch {
Expand Down Expand Up @@ -313,6 +314,7 @@ export function ExportTools({
aria-expanded={open}
onClick={() => {
setOpen((value) => !value);
setChoosingChartImage(false);
setMessage("");
}}
>
Expand Down Expand Up @@ -384,9 +386,33 @@ export function ExportTools({
) : null}
<span className="file-menu-divider" />
<p className="file-menu-section-label">Export</p>
<button role="menuitem" onClick={downloadChart}>
<button
role="menuitem"
aria-expanded={choosingChartImage}
onClick={() => setChoosingChartImage((value) => !value)}
>
Download chart image
</button>
{choosingChartImage ? (
<div
className="file-menu-export-options"
role="group"
aria-label="Chart image options"
>
<button
role="menuitem"
onClick={() => void downloadChart(true)}
>
With grid
</button>
<button
role="menuitem"
onClick={() => void downloadChart(false)}
>
Without grid
</button>
</div>
) : null}
<button role="menuitem" onClick={downloadPreview}>
Download knitted preview
</button>
Expand Down
53 changes: 53 additions & 0 deletions e2e/editor.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,59 @@ test("missing Clerk configuration never grants private chart access", async ({
expect(response.status()).toBe(401);
});

test("chart image downloads offer grid lines on or off without changing the chart", async ({
page,
}) => {
await page.goto("/");
const cell = page.locator(".chart-svg > g:first-child > rect").first();
const originalColor = await cell.getAttribute("fill");
await page.getByRole("button", { name: "File", exact: true }).click();
await page
.getByRole("menuitem", { name: "Download chart image", exact: true })
.click();

const images: number[][] = [];
for (const showGrid of [true, false]) {
const downloadEvent = page.waitForEvent("download");
await page
.getByRole("menuitem", {
name: showGrid ? "With grid" : "Without grid",
exact: true,
})
.click();
const download = await downloadEvent;
expect(download.suggestedFilename()).toMatch(
showGrid ? /\.chart\.png$/ : /\.chart-no-grid\.png$/,
);
const stream = await download.createReadStream();
const chunks: Buffer[] = [];
for await (const chunk of stream!) chunks.push(Buffer.from(chunk));
images.push(
await page.evaluate(async (base64) => {
const image = new Image();
image.src = `data:image/png;base64,${base64}`;
await image.decode();
const canvas = document.createElement("canvas");
canvas.width = image.width;
canvas.height = image.height;
const context = canvas.getContext("2d")!;
context.drawImage(image, 0, 0);
// Left edge of the chart, away from labels and horizontal grid lines.
return [
image.width,
image.height,
...context.getImageData(54, 80, 1, 1).data,
...context.getImageData(60, 80, 1, 1).data,
];
}, Buffer.concat(chunks).toString("base64")),
);
}
expect(images[0].slice(0, 2)).toEqual(images[1].slice(0, 2));
expect(images[0].slice(2, 6)).not.toEqual(images[0].slice(6, 10));
expect(images[1].slice(2, 6)).toEqual(images[1].slice(6, 10));
await expect(cell).toHaveAttribute("fill", originalColor!);
});

test("development samples are not production routes", async ({ request }) => {
const response = await request.get("/stitch-samples/yarn");
expect(response.status()).toBe(404);
Expand Down
Loading