Skip to content

Commit 640ed74

Browse files
Remake analyse card buttons (#129)
* In analysis card Replace download,configure,duplicate buttons with screenshot button Refs #25 * Make analysis card same height by giving them a chin --------- Co-authored-by: Peter Kalverla <[email protected]>
1 parent 437c4ef commit 640ed74

File tree

4 files changed

+94
-17
lines changed

4 files changed

+94
-17
lines changed

apps/class-solid/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
"clsx": "^2.1.1",
2626
"comlink": "^4.4.1",
2727
"d3": "^7.9.0",
28+
"file-saver": "^2.0.5",
29+
"html-to-image": "^1.11.13",
2830
"postcss": "^8.4.38",
2931
"solid-js": "^1.8.18",
3032
"tailwind-merge": "^2.4.0",
@@ -38,6 +40,7 @@
3840
"devDependencies": {
3941
"@playwright/test": "^1.45.3",
4042
"@types/d3": "^7.4.3",
43+
"@types/file-saver": "^2.0.7",
4144
"@types/node": "^20.13.1",
4245
"serve": "^14.2.3",
4346
"typescript": "^5.3.3"

apps/class-solid/src/components/Analysis.tsx

Lines changed: 50 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { BmiClass } from "@classmodel/class/bmi";
22
import type { Config } from "@classmodel/class/config";
33
import type { ClassOutput } from "@classmodel/class/runner";
4+
import { saveAs } from "file-saver";
5+
import { toBlob } from "html-to-image";
46
import {
57
type Accessor,
68
For,
@@ -27,7 +29,7 @@ import {
2729
experiments,
2830
updateAnalysis,
2931
} from "~/lib/store";
30-
import { MdiCog, MdiContentCopy, MdiDelete, MdiDownload } from "./icons";
32+
import { MdiCamera, MdiDelete } from "./icons";
3133
import { AxisBottom, AxisLeft, getNiceAxisLimits } from "./plots/Axes";
3234
import { Chart, ChartContainer } from "./plots/ChartContainer";
3335
import { Legend } from "./plots/Legend";
@@ -165,6 +167,7 @@ export function TimeSeriesPlot({ analysis }: { analysis: TimeseriesAnalysis }) {
165167
label="y-axis"
166168
/>
167169
</div>
170+
<div class="h-10" />
168171
</>
169172
);
170173
}
@@ -344,10 +347,50 @@ export function ThermodynamicPlot({ analysis }: { analysis: SkewTAnalysis }) {
344347
uniqueTimes,
345348
(t) => updateAnalysis(analysis, { time: t }),
346349
)}
350+
<div class="h-14" />
347351
</>
348352
);
349353
}
350354

355+
async function takeScreenshot(event: MouseEvent, analyse: Analysis) {
356+
const target = event.target as HTMLElement;
357+
const article = target.closest("[role='article']") as HTMLElement;
358+
const figure = article?.querySelector("figure") as HTMLElement;
359+
360+
if (!figure) {
361+
console.error("Could not find figure element");
362+
return;
363+
}
364+
365+
// TODO Make screenshot bigger than the original?
366+
const scale = 1;
367+
// Can not use toSvg as legend is written in HTML
368+
// generated svg document contains foreignObject with html tag
369+
// which can only be rendered using web browser, not Inkscape or PowerPoint
370+
const blob = await toBlob(figure, {
371+
backgroundColor: "white",
372+
canvasWidth: figure.clientWidth * scale,
373+
canvasHeight: figure.clientHeight * scale,
374+
});
375+
if (!blob) {
376+
throw new Error("Failed to create blob");
377+
}
378+
// TODO put experiments/permutation names in filename?
379+
const parts = [analyse.type];
380+
if ("time" in analyse) {
381+
const timesAnalyse = analyse as ProfilesAnalysis | SkewTAnalysis;
382+
const time = uniqueTimes()[timesAnalyse.time];
383+
const formattedTime = formatSeconds(time);
384+
parts.push(formattedTime);
385+
}
386+
const fn = `class-${parts.join("-")}.png`;
387+
console.log("Saving screenshot as", fn);
388+
const file = new File([blob], fn, {
389+
type: "image/png",
390+
});
391+
saveAs(file);
392+
}
393+
351394
export function AnalysisCard(analysis: Analysis) {
352395
const id = createUniqueId();
353396
return (
@@ -356,18 +399,12 @@ export function AnalysisCard(analysis: Analysis) {
356399
{/* TODO: make name & description editable */}
357400
<CardTitle id={id}>{analysis.name}</CardTitle>
358401

359-
<div class="flex">
360-
{/* TODO: implement download functionality */}
361-
<Button variant="outline" title="Download">
362-
<MdiDownload />
363-
</Button>
364-
{/* TODO: implement "configure" functionality */}
365-
<Button variant="outline" title="Configure">
366-
<MdiCog />
367-
</Button>
368-
{/* TODO: implement duplicate functionality */}
369-
<Button variant="outline" title="Duplicate">
370-
<MdiContentCopy />
402+
<div class="flex gap-1">
403+
<Button
404+
variant="outline"
405+
onClick={(e: MouseEvent) => takeScreenshot(e, analysis)}
406+
>
407+
<MdiCamera />
371408
</Button>
372409
<Button
373410
variant="outline"

apps/class-solid/src/components/icons.tsx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,3 +337,21 @@ export function MdiDotsHorizontal(props: JSX.IntrinsicElements["svg"]) {
337337
</svg>
338338
);
339339
}
340+
341+
export function MdiCamera(props: JSX.IntrinsicElements["svg"]) {
342+
return (
343+
<svg
344+
xmlns="http://www.w3.org/2000/svg"
345+
width="1em"
346+
height="1em"
347+
viewBox="0 0 24 24"
348+
{...props}
349+
>
350+
<title>Take screenshot of plot</title>
351+
<path
352+
fill="#888888"
353+
d="M4 4h3l2-2h6l2 2h3a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2m8 3a5 5 0 0 0-5 5a5 5 0 0 0 5 5a5 5 0 0 0 5-5a5 5 0 0 0-5-5m0 2a3 3 0 0 1 3 3a3 3 0 0 1-3 3a3 3 0 0 1-3-3a3 3 0 0 1 3-3"
354+
/>
355+
</svg>
356+
);
357+
}

pnpm-lock.yaml

Lines changed: 23 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)