Skip to content

Commit

Permalink
Add PNG export for Notebook's visualization blocks (#52)
Browse files Browse the repository at this point in the history
* feat: add png export for visualization blocks

* refactor(visualization-block): replace var with const

* refactor(visualization-block): move png download algorithm to a separate file

* feat(python-output): add png export button

* feat: change "PNG" button text color

* feat(visualization-block): enable export button if controls are visible

* fix: add timeout to ensure the canvas gets updated

* fix: hide button if chart type doesn't support export

* fix(visualization): correct the comment grammar

Co-authored-by: Lucas da Costa <[email protected]>

* refactor(utils/file): remove unsupported browsers if statement

- Briefer doesn't support Internet Explorer

* feat(python-output): display export button for multiple outputs

- download PNG with block-id as name

* feat(python-output): apply suggested 'PNG' button css

---------

Co-authored-by: Lucas da Costa <[email protected]>
  • Loading branch information
vtfg and lucasfcosta authored Sep 16, 2024
1 parent 26d3c43 commit 6f1ba2a
Show file tree
Hide file tree
Showing 5 changed files with 84 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand All @@ -24,6 +25,7 @@ interface Props {
isPDF: boolean
isDashboardView: boolean
lazyRender: boolean
blockId: string
}

let domPurify: DOMPurifyI
Expand Down Expand Up @@ -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'
)}
>
Expand All @@ -90,6 +93,7 @@ export function PythonOutputs(props: Props) {
isPDF={props.isPDF}
canFixWithAI={props.canFixWithAI}
isDashboardView={props.isDashboardView}
blockId={props.blockId}
/>
</div>
))}
Expand All @@ -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 (
<img
className="printable-block"
alt="generated image"
src={`data:image/${props.output.format};base64, ${props.output.data}`}
/>
<>
<img
className="printable-block"
alt="generated image"
src={`data:image/${props.output.format};base64, ${props.output.data}`}
/>
{!props.isDashboardView && (
<button
className="relative bottom-[1px] right-[1px] bg-white rounded-tl-md rounded-br-md border border-gray-200 p-1 hover:bg-gray-50 z-10 text-xs text-gray-400"
onClick={onExportToPNG}
>
PNG
</button>
)}
</>
)
}
case 'stdio':
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,7 @@ function PythonBlock(props: Props) {
isDashboardView={props.dashboardPlace === 'view'}
lazyRender={props.dashboardPlace === 'controls'}
canFixWithAI={hasOaiKey}
blockId={blockId}
/>
)
}
Expand Down Expand Up @@ -463,6 +464,7 @@ function PythonBlock(props: Props) {
isPDF={props.isPDF}
isDashboardView={false}
lazyRender={!props.isPDF}
blockId={blockId}
/>
</ScrollBar>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ interface Props {
renderer?: 'canvas' | 'svg'
isHidden: boolean
onToggleHidden: () => void
onExportToPNG?: () => void
isDashboard: boolean
isEditable: boolean
}
Expand Down Expand Up @@ -153,6 +154,16 @@ function VisualizationView(props: Props) {
)}
</button>
)}
{!props.isDashboard &&
props.chartType !== 'number' &&
props.chartType !== 'trend' && (
<button
className="absolute bottom-0 bg-white rounded-tl-md rounded-br-md border-t border-l border-gray-200 p-1 hover:bg-gray-50 z-10 right-0 text-xs text-gray-400"
onClick={props.onExportToPNG}
>
PNG
</button>
)}
</div>
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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[],
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -426,6 +455,7 @@ function VisualizationBlock(props: Props) {
renderer={props.renderer}
isHidden={controlsHidden}
onToggleHidden={onToggleHidden}
onExportToPNG={onExportToPNG}
isDashboard={props.isDashboard}
isEditable={isEditable}
/>
Expand Down Expand Up @@ -555,6 +585,7 @@ function VisualizationBlock(props: Props) {
renderer={props.renderer}
isHidden={controlsHidden}
onToggleHidden={onToggleHidden}
onExportToPNG={onExportToPNG}
isDashboard={props.isDashboard}
isEditable={isEditable}
/>
Expand Down
11 changes: 11 additions & 0 deletions apps/web/src/utils/file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

0 comments on commit 6f1ba2a

Please sign in to comment.