Pantrix UI

Exporting a chart as a file

How a Pantrix chart is exported as PNG, SVG and PDF, what the library does and what it leaves to the app, how the file made reaches the user on each platform, and the limits of the three kinds of file.

The chart makes the file and hands it to the app; it does not save it, share it or download it. Foundation's FileSaver gives it to the user, with each platform's window for saving a file (section 3); the share sheet is the app's.

Everything said here of the library was seen in tests that run in this repository, or by looking at the files made; which was seen where is written in section 6. The two pieces for sharing in section 3 are the demo's old code, and compiled on four platforms; where they were tried is in section 6.

1. Use

Exporting is a parameter: in a chart given no export, nothing shows.

PantrixLineChart(
    data = months,
    x = { it.name },
    values = revenue,
    export = ChartExport(fileName = "revenue") { file ->
        // file.name = "revenue.svg", file.mimeType = "image/svg+xml", file.bytes = the content
        share(file)
    }
)

Given export, an export button comes up among the chart's own buttons (beside the zoom buttons where there are any). The reader presses the button and chooses the kind; the chart makes the file and hands it to onExport.

FieldWhat it saysDefault
formatsThe kinds the reader chooses from, in their order. Where one kind is given, no choice opens: the button makes that fileAll three: PNG, SVG, PDF
fileNameThe file's name, without its extension"chart"
scaleThe PNG's pixels to a dp. It does not depend on the screen's density2f
backgroundThe picture's ground. Color.Unspecified is the theme's background; Color.Transparent draws no groundThe theme's ground
paddingThe room the picture leaves around the chart, on each side. The ground is under the room too12.dp
onExportThe ChartExportFile made is handed to itRequired

export is in PantrixChart and the six short charts above it (line, area, bar, range, waterfall, candlestick), and in every one of the other fourteen kinds: pie, gauge, progress circle, meter group, status tracker, sparkline, scatter, radar, polar, heatmap, treemap, funnel, sankey and Gantt. In each it is at the end of the signature; in PantrixProgressCircle alone it comes before content, so that the last lambda stays the content inside the ring.

The button stands in a row above the chart, on the side where the lines end. In a chart with axes it is in the same row as the zoom buttons. A chart given no height grows by this row; a chart given a height keeps its height, and its drawing is that much shorter.

2. What is in the file

The file shows the chart as it is on the screen:

The three kinds are made from the same drawing; the differences between them are these:

KindWhatTextSize (a chart of two series over twelve months)
PNGA picture of pixels; as sharp as scaleIn the app's typeface, as on the screenTens of kilobytes
SVGLines and shapes; sharp at every sizeReal text (<text>), in the sans-serif typeface of whoever opens the fileA few kilobytes
PDFOne page, lines and shapes; sharp at every sizeReal text in Helvetica; a line Helvetica cannot write as a pictureA few kilobytes

Why the text differs from the screen's

SVG and PDF do not carry the app's typeface: the library does not have the typeface's file. In SVG the text is written in the sans-serif typeface of the program that opens the file, in PDF in Helvetica, which every PDF reader has. These typefaces' letters can be narrower or wider than the app's; each line is written in the middle of the room it takes on the screen. A number beside an axis may therefore stand a pixel or two closer to the axis, or farther from it.

In PDF, Helvetica writes the letters of the Western European languages and of Turkish (for Turkish's six letters the file carries an encoding of its own). A line that holds a letter Helvetica cannot write (an Arabic name, the sign ₺) is put on the page as its own picture: four times as sharp as the screen's density, but not endlessly sharp like everything else, and it cannot be selected as text.

3. Getting the file to the user

onExport takes a ChartExportFile: name, mimeType, format, bytes. Foundation's FileSaver gives it to the user: each platform's window for saving a file (the files guide, 3.4).

PlatformThe usual wayWhat it asks for
Android"Save as" (ACTION_CREATE_DOCUMENT), FileSaver's way; or the share sheet (Intent.ACTION_SEND)To save, an Activity's result; to share, a FileProvider in the manifest
iOSExporting to Files (UIDocumentPickerViewController), FileSaver's way; or the share sheet (UIActivityViewController)A temporary file, and a view controller to present the window
DesktopThe save dialogNothing
WebThe browser's downloadNothing

FileSaver does not open the share sheet: on Android sharing needs a FileProvider and a file of paths, and were the library to declare them in its own manifest, every app that uses the library would get a provider. An app that wants to give the file to a message or an email opens the sheet itself, as the two pieces below do.

The export example among the chart examples (ChartExported) first shows the file made, and hands it to the library's saver with the button "Save the file" too:

val saver = rememberFileSaver()
PantrixLineChart(
    // ...
    export = ChartExport { file ->
        saver.save(PantrixFile(file.name, file.bytes, file.mimeType)) { result -> saved = result }
    }
)

The two pieces below were the demo's own saver until 2026-10-28 (the package com.pantrixui.cmp.demo.platform in app/shared); they were taken out when the demo moved to the library's saver, and stay here for an app that wants to share.

Android: the share sheet

val context = LocalContext.current
// save:
val folder = File(context.cacheDir, "exports").apply { mkdirs() }
val file = File(folder, name).apply { writeBytes(bytes) }
val uri = FileProvider.getUriForFile(context, context.packageName + ".files", file)
val send = Intent(Intent.ACTION_SEND)
    .setType(mimeType)
    .putExtra(Intent.EXTRA_STREAM, uri)
    .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
context.startActivity(Intent.createChooser(send, name))

The app chosen in the sheet reads the file by the address the FileProvider gives. The provider is declared in the app's manifest and opens only one folder of the cache:

<!-- AndroidManifest.xml, inside <application> -->
<provider
    android:name="androidx.core.content.FileProvider"
    android:authorities="${applicationId}.files"
    android:exported="false"
    android:grantUriPermissions="true">
    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/file_paths" />
</provider>
<!-- res/xml/file_paths.xml -->
<paths>
    <cache-path name="exports" path="exports/" />
</paths>

FileProvider is in the library androidx.core (androidx.core.ktx, added to the app's androidMain). The manifest's authorities has to be the same as the address in the code; if it is not, getUriForFile throws. So it does if the file is outside the folder.

iOS: the share sheet

val path = NSTemporaryDirectory() + name
val data = if (bytes.isEmpty()) {
    NSData()
} else {
    bytes.usePinned { NSData.create(bytes = it.addressOf(0), length = bytes.size.toULong()) }
}
data.writeToFile(path, atomically = true)
var top = UIApplication.sharedApplication.keyWindow?.rootViewController
while (top?.presentedViewController != null) top = top.presentedViewController
val sheet = UIActivityViewController(listOf(NSURL.fileURLWithPath(path)), null)
sheet.popoverPresentationController?.sourceView = top?.view
top?.presentViewController(sheet, animated = true, completion = null)

"Save to Files" is in the sheet too. The sheet is presented from the topmost view controller: a controller that is presenting another refuses a second presentation. On an iPad the sheet is presented as a popover, and asks what it is anchored to (popoverPresentationController.sourceView). The address of an empty array cannot be given to NSData.create: an empty file is written apart.

4. What to know

5. Automation and reporting

The export button's tag is .Export after the chart's own tag: Chart_<tagKey>.Export, PieChart_<tagKey>.Export, Gantt_<tagKey>.Export and the others. Where a choice of kind opens, its rows' tags are .Export_png, .Export_svg and .Export_pdf. When a file is made, an export event is reported with the chart's own component name (Chart, PieChart, Gauge, Gantt …); its format property is the file's extension (png, svg, pdf). The file's name and content are not reported.

6. How it was verified

WhatWhere
The drawing kept as lines, shapes and words; how SVG and PDF are written; the PDF's table of parts and the length of its streamsChartExportFilesTest (the desktop, the iOS simulator and the two browser targets)
The button, the choice of kind and its closing, the file's name and type, the PNG's size in pixels, the ground, the room, the screen's density, the part in view, a series taken out, dashed lines, marks, value labels, an axis's title, the picture of an Arabic name in the PDFChartExportTest (the same four targets)
The picture's legend: lines, centring, sample and name, right to left, a line by the tallest nameChartExportLegendTest (the same four targets)
The other fourteen kinds: the button's place, each kind's file's name, event and text, the place pointed at not being in the file, the middle of the pie and of the gauge, the heatmap's scale and right to left, the button row's relation to the heightChartExportKindsTest (the same four targets)
The other kinds on the webOn 2026-10-15 the PNGs of a pie, a heatmap, a gauge, a progress circle, a sparkline and a Gantt were made in the web demo and compared with the screen (the tooltip of the cell under the mouse was not in the file); their SVGs and PDFs were not opened in a browser
The PNG's pixels: the ground around, the chart insideChartExportDesktopTest (the desktop, read back with Skia), ChartExportOnAndroidTest (Android, Robolectric: with Android's own bitmap)
The look of the three kindsThe files made on the desktop were looked at with macOS's drawing (Quick Look, sips): screen densities 1 and 2.75
Making them on AndroidIn the emulator (screen density 3.5) the three kinds were made; the PNG was shown back in the demo, and the three files were taken off the device and drawn on macOS
Making them on iOSIn the simulator (screen density 3) the three kinds were made; the PNG was shown back in the demo, and the three files were taken off the simulator and drawn on macOS
The pieces for sharing in section 3Compiled on four platforms as the demo's old saver. In the iOS simulator, PDF was chosen and "Save the file" pressed: the share sheet opened with the file's name, type and size ("visits", PDF, 5 KB), and "Save to Files" was in the sheet; it was not saved anywhere. Where the library's saver was tried is in the files guide

Not verified: the files of a chart with axes could not be looked at on the web (the browser pane was not on the screen; the tests run in a browser, but a file made was not opened in a browser); of the other kinds, only the PNG was looked at on the web, and their files were not looked at on Android and iOS; the PDF was not looked at in the drawing of Adobe Acrobat, Chrome and Firefox (only in macOS's); the SVG was not looked at in a browser and in a drawing program (only in macOS's Quick Look); the piece for sharing on Android in section 3 was only compiled, not tried; printed output was not looked at; it was not tried on a real phone.