11import { BmiClass } from "@classmodel/class/bmi" ;
22import type { Config } from "@classmodel/class/config" ;
33import type { ClassOutput } from "@classmodel/class/runner" ;
4+ import { saveAs } from "file-saver" ;
5+ import { toBlob } from "html-to-image" ;
46import {
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" ;
3133import { AxisBottom , AxisLeft , getNiceAxisLimits } from "./plots/Axes" ;
3234import { Chart , ChartContainer } from "./plots/ChartContainer" ;
3335import { 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+
351394export 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"
0 commit comments