diff --git a/apps/web/src/components/v2Editor/customBlocks/python/PythonOutput.tsx b/apps/web/src/components/v2Editor/customBlocks/python/PythonOutput.tsx
index acb2c50..655e143 100644
--- a/apps/web/src/components/v2Editor/customBlocks/python/PythonOutput.tsx
+++ b/apps/web/src/components/v2Editor/customBlocks/python/PythonOutput.tsx
@@ -12,6 +12,7 @@ import { ChevronDownIcon, ChevronRightIcon } from '@heroicons/react/20/solid'
import createDomPurify, { DOMPurifyI } from 'dompurify'
import React, { useEffect, useLayoutEffect, useMemo, useRef } from 'react'
import useResettableState from '@/hooks/useResettableState'
+import { downloadFile } from '@/utils/file'
import debounce from 'lodash.debounce'
import { PythonBlock } from '@briefer/editor'
@@ -24,6 +25,7 @@ interface Props {
isPDF: boolean
isDashboardView: boolean
lazyRender: boolean
+ blockId: string
}
let domPurify: DOMPurifyI
@@ -80,6 +82,7 @@ export function PythonOutputs(props: Props) {
key={i}
className={clsx(
['plotly'].includes(output.type) ? 'flex-grow' : '',
+ !props.isDashboardView ? 'flex flex-col items-end' : '',
'bg-white overflow-x-scroll'
)}
>
@@ -90,6 +93,7 @@ export function PythonOutputs(props: Props) {
isPDF={props.isPDF}
canFixWithAI={props.canFixWithAI}
isDashboardView={props.isDashboardView}
+ blockId={props.blockId}
/>
))}
@@ -104,18 +108,38 @@ interface ItemProps {
isPDF: boolean
isDashboardView: boolean
canFixWithAI: boolean
+ blockId: string
}
export function PythonOutput(props: ItemProps) {
+ const onExportToPNG = () => {
+ if (props.output.type !== 'image' || props.output.format !== 'png') return
+
+ downloadFile(
+ `data:image/${props.output.format};base64, ${props.output.data}`,
+ props.blockId
+ )
+ }
+
switch (props.output.type) {
case 'image':
switch (props.output.format) {
case 'png':
return (
-
+ <>
+
+ {!props.isDashboardView && (
+
+ )}
+ >
)
}
case 'stdio':
diff --git a/apps/web/src/components/v2Editor/customBlocks/python/index.tsx b/apps/web/src/components/v2Editor/customBlocks/python/index.tsx
index 647072d..7fac54d 100644
--- a/apps/web/src/components/v2Editor/customBlocks/python/index.tsx
+++ b/apps/web/src/components/v2Editor/customBlocks/python/index.tsx
@@ -277,6 +277,7 @@ function PythonBlock(props: Props) {
isDashboardView={props.dashboardPlace === 'view'}
lazyRender={props.dashboardPlace === 'controls'}
canFixWithAI={hasOaiKey}
+ blockId={blockId}
/>
)
}
@@ -463,6 +464,7 @@ function PythonBlock(props: Props) {
isPDF={props.isPDF}
isDashboardView={false}
lazyRender={!props.isPDF}
+ blockId={blockId}
/>
diff --git a/apps/web/src/components/v2Editor/customBlocks/visualization/VisualizationView.tsx b/apps/web/src/components/v2Editor/customBlocks/visualization/VisualizationView.tsx
index 170e745..5cc7b44 100644
--- a/apps/web/src/components/v2Editor/customBlocks/visualization/VisualizationView.tsx
+++ b/apps/web/src/components/v2Editor/customBlocks/visualization/VisualizationView.tsx
@@ -39,6 +39,7 @@ interface Props {
renderer?: 'canvas' | 'svg'
isHidden: boolean
onToggleHidden: () => void
+ onExportToPNG?: () => void
isDashboard: boolean
isEditable: boolean
}
@@ -153,6 +154,16 @@ function VisualizationView(props: Props) {
)}
)}
+ {!props.isDashboard &&
+ props.chartType !== 'number' &&
+ props.chartType !== 'trend' && (
+
+ )}
)
}
diff --git a/apps/web/src/components/v2Editor/customBlocks/visualization/index.tsx b/apps/web/src/components/v2Editor/customBlocks/visualization/index.tsx
index 22abb04..480c309 100644
--- a/apps/web/src/components/v2Editor/customBlocks/visualization/index.tsx
+++ b/apps/web/src/components/v2Editor/customBlocks/visualization/index.tsx
@@ -38,6 +38,7 @@ import { VisualizationExecTooltip } from '../../ExecTooltip'
import useFullScreenDocument from '@/hooks/useFullScreenDocument'
import HiddenInPublishedButton from '../../HiddenInPublishedButton'
import useEditorAwareness from '@/hooks/useEditorAwareness'
+import { downloadFile } from '@/utils/file'
function didChangeFilters(
oldFilters: VisualizationFilter[],
@@ -202,6 +203,34 @@ function VisualizationBlock(props: Props) {
props.block.setAttribute('controlsHidden', !controlsHidden)
}, [controlsHidden, props.block])
+ const onExportToPNG = async () => {
+ // we don't need to check if props.renderer is undefined because the application sets as 'canvas' in this case
+ if (
+ props.renderer === 'svg' ||
+ chartType === 'number' ||
+ chartType === 'trend'
+ )
+ return
+
+ // if the controls are visible the canvas shrinks, making the export smaller
+ if (!controlsHidden) {
+ onToggleHidden()
+ // tick to ensure the canvas size gets updated
+ await new Promise((r) => setTimeout(r, 0))
+ }
+
+ const canvas = document.querySelector(
+ `div[data-block-id='${blockId}'] canvas`
+ ) as HTMLCanvasElement
+
+ // TODO: identify when this is true
+ if (!canvas) return
+
+ const imageUrl = canvas.toDataURL('image/png')
+ const fileName = title || 'Visualization'
+ downloadFile(imageUrl, fileName)
+ }
+
const onChangeChartType = useCallback(
(chartType: ChartType) => {
props.block.setAttribute('chartType', chartType)
@@ -426,6 +455,7 @@ function VisualizationBlock(props: Props) {
renderer={props.renderer}
isHidden={controlsHidden}
onToggleHidden={onToggleHidden}
+ onExportToPNG={onExportToPNG}
isDashboard={props.isDashboard}
isEditable={isEditable}
/>
@@ -555,6 +585,7 @@ function VisualizationBlock(props: Props) {
renderer={props.renderer}
isHidden={controlsHidden}
onToggleHidden={onToggleHidden}
+ onExportToPNG={onExportToPNG}
isDashboard={props.isDashboard}
isEditable={isEditable}
/>
diff --git a/apps/web/src/utils/file.ts b/apps/web/src/utils/file.ts
index c8d165b..e71ba63 100644
--- a/apps/web/src/utils/file.ts
+++ b/apps/web/src/utils/file.ts
@@ -19,3 +19,14 @@ export function readFile(
}
})
}
+
+export function downloadFile(url: string, name: string) {
+ const downloadLink = document.createElement('a')
+
+ downloadLink.download = name
+ downloadLink.href = url
+
+ document.body.appendChild(downloadLink)
+ downloadLink.click()
+ document.body.removeChild(downloadLink)
+}