Pantrix UI

Chart

A chart of values along a run of places: lines and filled areas through the values each series has at each place, and bars up to them, with an axis of the places under them and an axis of the values beside them.

import com.pantrixui.components.chart.cartesian.PantrixChart

Examples

Two series

At its plainest: two series read from the same rows. Each LineSeries takes its key, its name, the rows, and the two functions that read the place (x) and the value (y) from those rows.

The rest comes by itself: the axis of the months under the chart and of round values beside it, grid lines at the values, a legend since there are two series, and the tooltip of the place pointed at. Move the pointer over the chart, or draw a finger along it; press a name in the legend to take its series out.

val series: List<ChartSeries> = listOf(
    LineSeries("desktop", "Desktop", VISITS, x = { it.month }, y = { it.desktop }),
    LineSeries("mobile", "Mobile", VISITS, x = { it.month }, y = { it.mobile })
)
PantrixChart(
    series = series,
    description = "Desktop and mobile visits by month",
    tagKey = "two_series"
)

private data class Visits(
    val month: String,
    val desktop: Int,
    val mobile: Int,
    val tablet: Int
)

private val VISITS = listOf(
    Visits("Jan", desktop = 186, mobile = 80, tablet = 40),
    Visits("Feb", desktop = 305, mobile = 200, tablet = 62),
    Visits("Mar", desktop = 237, mobile = 120, tablet = 58),
    Visits("Apr", desktop = 173, mobile = 190, tablet = 51),
    Visits("May", desktop = 209, mobile = 130, tablet = 66),
    Visits("Jun", desktop = 214, mobile = 140, tablet = 70),
    Visits("Jul", desktop = 280, mobile = 170, tablet = 76),
    Visits("Aug", desktop = 310, mobile = 210, tablet = 83),
    Visits("Sep", desktop = 268, mobile = 232, tablet = 90),
    Visits("Oct", desktop = 342, mobile = 250, tablet = 96),
    Visits("Nov", desktop = 390, mobile = 286, tablet = 91),
    Visits("Dec", desktop = 420, mobile = 301, tablet = 120)
)

Series from their own rows

Series need not be read from one list. Here the measured values come from one list and the forecast from another, and the two meet only in September. The chart sets out every place any of its series names, in the order they are first named; where a series gives no value it has no line.

The forecast is drawn in dashes (dash) and its points are marked (marker): it is told from the measured by more than its colour.

PantrixChart(
    series = listOf(
        LineSeries("measured", "Measured", MEASURED, x = { it.first }, y = { it.second }),
        LineSeries(
            key = "forecast",
            name = "Forecast",
            data = FORECAST,
            x = { it.first },
            y = { it.second },
            marker = ChartMarker.Circle,
            dash = ChartDash.Dashed
        )
    ),
    description = "The year's measured and forecast sales",
    tagKey = "own_rows"
)

private val MEASURED = listOf(
    "Jan" to 42,
    "Feb" to 48,
    "Mar" to 45,
    "Apr" to 57,
    "May" to 61,
    "Jun" to 58,
    "Jul" to 66,
    "Aug" to 71,
    "Sep" to 69
)

private val FORECAST = listOf("Sep" to 69, "Oct" to 74, "Nov" to 80, "Dec" to 88)

A line over bars

Kinds mix in one chart: each month's orders are a bar (BarSeries), and their three-month average is a line (LineSeries). In a chart that has bars each place has a band, and the line goes through the middles of the bands. A series given later is drawn over an earlier one: give the line after the bars.

The tooltip writes the two values of the place together; in the legend a bar is a patch, and the line a short line with its mark.

PantrixChart(
    series = listOf(
        BarSeries("orders", "Orders", HALF_YEAR, x = { it.first }, y = { it.second }),
        LineSeries(
            key = "average",
            name = "Three-month average",
            data = HALF_YEAR,
            x = { it.first },
            y = { it.third },
            marker = ChartMarker.Circle
        )
    ),
    description = "Orders by month and their three-month average",
    tagKey = "bars_and_line"
)

private val HALF_YEAR = listOf(
    Triple("Jan", 310, 298),
    Triple("Feb", 285, 302),
    Triple("Mar", 342, 312),
    Triple("Apr", 398, 342),
    Triple("May", 371, 370),
    Triple("Jun", 426, 398)
)

Two axes

Two kinds of value in one chart: each month's orders are a count, and the return rate is a share. Drawn against one axis, the rate would lie flat on zero. A series that says axis = ChartYAxis.Second is read by the second axis; that axis is written at the chart's other side (secondYAxis), and its marks stand on the first axis' grid lines.

Each value is written the way its own axis writes: in the tooltip the orders are a number and the rate a percentage, and a screen reader says them so. secondYAxis = null does not draw the axis; the series still stands by its own scale.

val strings = PantrixTheme.strings
PantrixChart(
    series = listOf(
        BarSeries("orders", "Orders", RETURNS, x = { it.first }, y = { it.second }),
        LineSeries(
            key = "returned",
            name = "Return rate",
            data = RETURNS,
            x = { it.first },
            y = { it.third },
            marker = ChartMarker.Circle,
            axis = ChartYAxis.Second
        )
    ),
    yAxis = ChartAxis(title = "Orders"),
    secondYAxis = ChartAxis(title = "Return rate", format = { strings.percent(it, fractionDigits = 1) }),
    description = "Orders and return rate by month",
    tagKey = "two_axes"
)

private val RETURNS = listOf(
    Triple("Jan", 310, 0.042),
    Triple("Feb", 285, 0.051),
    Triple("Mar", 342, 0.038),
    Triple("Apr", 398, 0.036),
    Triple("May", 371, 0.047),
    Triple("Jun", 426, 0.031)
)

A target line and bands

Series are read against something: a target, a usual range, the months of a campaign. marks takes the marks the chart draws besides its series.

Lines are drawn over the series and bands behind them; inFront changes that. A mark at a value stretches its axis far enough to show it: the target here is at 450, which none of the visits reach. A mark with a label is read to a screen reader after the chart's description.

A mark's words are written over the series, on a plate in the tooltip's colour: they read over a line or a bar as well. labelPosition says where along the mark they stand: a line's words are at its end by default, a band's at its start. Here "Usual" is at the end of its band and "New site" at the foot of its line.

How thick the lines are and how deep the bands' tint is belongs to the style: two dp and a fifth of the colour here (markLineWidth, markBandAlpha).

val marks: List<ChartMark> = listOf(
    ChartReferenceBand(y1 = 200, y2 = 300, label = "Usual", labelPosition = ChartMarkLabelPosition.End),
    ChartReferenceBand(x1 = "Jun", x2 = "Aug", label = "Summer campaign"),
    ChartReferenceLine(
        x = "Oct",
        label = "New site",
        dash = ChartDash.Dotted,
        labelPosition = ChartMarkLabelPosition.Start
    ),
    ChartReferenceLine(y = 450, label = "Target", color = PantrixTheme.colors.success.color)
)
PantrixChart(
    series = listOf(LineSeries("desktop", "Desktop", VISITS, x = { it.month }, y = { it.desktop })),
    marks = marks,
    description = "Desktop visits by month",
    style = ChartStyle(markLineWidth = 2.dp, markBandAlpha = 0.2f),
    tagKey = "marks"
)

private data class Visits(
    val month: String,
    val desktop: Int,
    val mobile: Int,
    val tablet: Int
)

private val VISITS = listOf(
    Visits("Jan", desktop = 186, mobile = 80, tablet = 40),
    Visits("Feb", desktop = 305, mobile = 200, tablet = 62),
    Visits("Mar", desktop = 237, mobile = 120, tablet = 58),
    Visits("Apr", desktop = 173, mobile = 190, tablet = 51),
    Visits("May", desktop = 209, mobile = 130, tablet = 66),
    Visits("Jun", desktop = 214, mobile = 140, tablet = 70),
    Visits("Jul", desktop = 280, mobile = 170, tablet = 76),
    Visits("Aug", desktop = 310, mobile = 210, tablet = 83),
    Visits("Sep", desktop = 268, mobile = 232, tablet = 90),
    Visits("Oct", desktop = 342, mobile = 250, tablet = 96),
    Visits("Nov", desktop = 390, mobile = 286, tablet = 91),
    Visits("Dec", desktop = 420, mobile = 301, tablet = 120)
)

Axes

An axis is described by a ChartAxis. Here the axis of values has a title and writes its values short, in the theme's language (format); the axis of places has a title too.

The axis of a chart of lines does not start at zero: what is read is the line's rise and fall. startFromZero takes the axis down to zero; turn the switch and see how the same values look on the two axes. A chart with a filled series starts at zero by itself: an area's height is read as an amount.

var fromZero by remember { mutableStateOf(false) }
val strings = PantrixTheme.strings
PantrixSwitchButton(
    checked = fromZero,
    onCheckedChange = { fromZero = it },
    label = "Start the value axis at zero",
    tagKey = "chart_from_zero"
)
PantrixChart(
    series = listOf(LineSeries("revenue", "Revenue", REVENUE, x = { it.first }, y = { it.second })),
    xAxis = ChartAxis(title = "2026"),
    yAxis = ChartAxis(title = "Revenue (TL)", startFromZero = fromZero, format = { strings.compact(it) }),
    description = "Monthly revenue of 2026",
    tagKey = "axes"
)

private val REVENUE = listOf(
    "Jan" to 1_240_000,
    "Feb" to 1_310_000,
    "Mar" to 1_285_000,
    "Apr" to 1_420_000,
    "May" to 1_390_000,
    "Jun" to 1_510_000,
    "Jul" to 1_640_000,
    "Aug" to 1_585_000,
    "Sep" to 1_720_000,
    "Oct" to 1_790_000,
    "Nov" to 1_865_000,
    "Dec" to 1_950_000
)

Where the legend stands

The legend stands on one of the chart's four sides. Above and under it the entries are set in a row and go on to the next row where they do not fit; at the sides they stand under one another. alignment says where the legend stands along its own side.

Each entry is a switch for its series: pressed, the series leaves the chart and the axis is set again for the ones that stay, while the entry stays in place, drawn faint. A series that is out keeps its colour; the others' colours do not change either.

var place by remember { mutableStateOf(ChartLegendPosition.Top) }
PantrixSegmentButton(
    options = LEGEND_PLACES,
    selected = place,
    onSelect = { place = it },
    fullWidth = false,
    tagKey = "legend_place"
)
PantrixChart(
    series = listOf(
        LineSeries("desktop", "Desktop", VISITS, x = { it.month }, y = { it.desktop }),
        LineSeries("mobile", "Mobile", VISITS, x = { it.month }, y = { it.mobile }),
        LineSeries("tablet", "Tablet", VISITS, x = { it.month }, y = { it.tablet })
    ),
    legend = ChartLegend(position = place),
    description = "Visits from three devices by month",
    tagKey = "legend"
)

private data class Visits(
    val month: String,
    val desktop: Int,
    val mobile: Int,
    val tablet: Int
)

private val VISITS = listOf(
    Visits("Jan", desktop = 186, mobile = 80, tablet = 40),
    Visits("Feb", desktop = 305, mobile = 200, tablet = 62),
    Visits("Mar", desktop = 237, mobile = 120, tablet = 58),
    Visits("Apr", desktop = 173, mobile = 190, tablet = 51),
    Visits("May", desktop = 209, mobile = 130, tablet = 66),
    Visits("Jun", desktop = 214, mobile = 140, tablet = 70),
    Visits("Jul", desktop = 280, mobile = 170, tablet = 76),
    Visits("Aug", desktop = 310, mobile = 210, tablet = 83),
    Visits("Sep", desktop = 268, mobile = 232, tablet = 90),
    Visits("Oct", desktop = 342, mobile = 250, tablet = 96),
    Visits("Nov", desktop = 390, mobile = 286, tablet = 91),
    Visits("Dec", desktop = 420, mobile = 301, tablet = 120)
)

private val LEGEND_PLACES = listOf(
    SegmentOption(value = ChartLegendPosition.Top, label = "Top", tagKey = "top"),
    SegmentOption(value = ChartLegendPosition.Bottom, label = "Bottom", tagKey = "bottom"),
    SegmentOption(value = ChartLegendPosition.Start, label = "Start", tagKey = "start"),
    SegmentOption(value = ChartLegendPosition.End, label = "End", tagKey = "end")
)

Tooltip: together or one at a time

The tooltip reads in two ways. With shared on, the values of all the series at the place pointed at come together: that is how a chart is read at a place, and a finger need not aim. With it off the tooltip is the value of the series nearest the pointer alone; on a keyboard the arrows up and down go from one series' value to the next.

crosshair is the line through the place pointed at. With null in the tooltip's place no card is shown at all; the place is still marked and still written to the chart's state.

var shared by remember { mutableStateOf(true) }
var crosshair by remember { mutableStateOf(true) }
Row(horizontalArrangement = Arrangement.spacedBy(PantrixTheme.spacing.lg)) {
    PantrixSwitchButton(
        checked = shared,
        onCheckedChange = { shared = it },
        label = "Series together",
        tagKey = "tooltip_shared"
    )
    PantrixSwitchButton(
        checked = crosshair,
        onCheckedChange = { crosshair = it },
        label = "A line through the place",
        tagKey = "tooltip_crosshair"
    )
}
PantrixChart(
    series = listOf(
        LineSeries(
            key = "desktop",
            name = "Desktop",
            data = VISITS,
            x = { it.month },
            y = { it.desktop },
            marker = ChartMarker.Circle
        ),
        LineSeries("mobile", "Mobile", VISITS, x = { it.month }, y = { it.mobile }, marker = ChartMarker.Square)
    ),
    tooltip = ChartTooltip(shared = shared, crosshair = crosshair),
    description = "Desktop and mobile visits by month",
    tagKey = "tooltip"
)

private data class Visits(
    val month: String,
    val desktop: Int,
    val mobile: Int,
    val tablet: Int
)

private val VISITS = listOf(
    Visits("Jan", desktop = 186, mobile = 80, tablet = 40),
    Visits("Feb", desktop = 305, mobile = 200, tablet = 62),
    Visits("Mar", desktop = 237, mobile = 120, tablet = 58),
    Visits("Apr", desktop = 173, mobile = 190, tablet = 51),
    Visits("May", desktop = 209, mobile = 130, tablet = 66),
    Visits("Jun", desktop = 214, mobile = 140, tablet = 70),
    Visits("Jul", desktop = 280, mobile = 170, tablet = 76),
    Visits("Aug", desktop = 310, mobile = 210, tablet = 83),
    Visits("Sep", desktop = 268, mobile = 232, tablet = 90),
    Visits("Oct", desktop = 342, mobile = 250, tablet = 96),
    Visits("Nov", desktop = 390, mobile = 286, tablet = 91),
    Visits("Dec", desktop = 420, mobile = 301, tablet = 120)
)

Your own tooltip

The inside of the card can be the screen's: content takes the values at the place pointed at and draws what it wants from them. Here the two series' values are added up and mobile's share is written as a percentage, both in the theme's number format.

The card itself, its fill, its edge and where it stands remain the chart's.

val strings = PantrixTheme.strings
PantrixChart(
    series = listOf(
        LineSeries("desktop", "Desktop", VISITS, x = { it.month }, y = { it.desktop }),
        LineSeries("mobile", "Mobile", VISITS, x = { it.month }, y = { it.mobile })
    ),
    tooltip = ChartTooltip(
        content = { points ->
            val total = points.sumOf { it.value }
            val mobile = points.firstOrNull { it.series == "mobile" }?.value ?: 0.0
            PantrixText(text = points.first().label, style = PantrixTheme.typography.footnote)
            PantrixText(text = "${strings.decimal(total)} visits", style = PantrixTheme.typography.label)
            PantrixText(
                text = "Mobile's share ${strings.percent(mobile / total)}",
                style = PantrixTheme.typography.caption,
                color = PantrixTheme.colors.textSecondary
            )
        }
    ),
    description = "Total visits and mobile's share by month",
    tagKey = "own_tooltip"
)

private data class Visits(
    val month: String,
    val desktop: Int,
    val mobile: Int,
    val tablet: Int
)

private val VISITS = listOf(
    Visits("Jan", desktop = 186, mobile = 80, tablet = 40),
    Visits("Feb", desktop = 305, mobile = 200, tablet = 62),
    Visits("Mar", desktop = 237, mobile = 120, tablet = 58),
    Visits("Apr", desktop = 173, mobile = 190, tablet = 51),
    Visits("May", desktop = 209, mobile = 130, tablet = 66),
    Visits("Jun", desktop = 214, mobile = 140, tablet = 70),
    Visits("Jul", desktop = 280, mobile = 170, tablet = 76),
    Visits("Aug", desktop = 310, mobile = 210, tablet = 83),
    Visits("Sep", desktop = 268, mobile = 232, tablet = 90),
    Visits("Oct", desktop = 342, mobile = 250, tablet = 96),
    Visits("Nov", desktop = 390, mobile = 286, tablet = 91),
    Visits("Dec", desktop = 420, mobile = 301, tablet = 120)
)

Two charts that share a state

ChartState holds the series the reader has hidden and the place they point at. A screen that makes one with rememberChartState and gives it to the chart reads both: the row underneath writes the month pointed at from the state.

Two charts given the same state behave as one. Point at a month in the upper one: the lower one marks the same month. A series hidden in the legend of one is hidden in the other too, since the two charts' series carry the same keys.

The state is written from the screen too: the button gives state.selection a ChartSelection(x = "Mar"), and both charts show March.

val state = rememberChartState()
PantrixChart(
    series = listOf(
        LineSeries("desktop", "Desktop", VISITS, x = { it.month }, y = { it.desktop }),
        LineSeries("mobile", "Mobile", VISITS, x = { it.month }, y = { it.mobile })
    ),
    modifier = Modifier.height(200.dp),
    state = state,
    description = "Visits by month",
    tagKey = "shared_visits"
)
PantrixChart(
    series = listOf(
        LineSeries(
            key = "desktop",
            name = "Desktop",
            data = VISITS,
            x = { it.month },
            y = { it.desktop * 3 / 100 },
            fill = ChartFill.Solid
        ),
        LineSeries("mobile", "Mobile", VISITS, x = { it.month }, y = { it.mobile * 2 / 100 }, fill = ChartFill.Solid)
    ),
    modifier = Modifier.height(160.dp),
    legend = null,
    state = state,
    description = "Orders by month",
    tagKey = "shared_orders"
)
PantrixText(
    text = state.selection?.let { "Month pointed at: ${it.x}" } ?: "No month is pointed at",
    style = PantrixTheme.typography.footnote,
    color = PantrixTheme.colors.textSecondary
)
PantrixButton(
    text = "Show March",
    onClick = { state.selection = ChartSelection(x = "Mar") },
    variant = ButtonVariant.Outline,
    size = ButtonSize.Small,
    tagKey = "show_march"
)

private data class Visits(
    val month: String,
    val desktop: Int,
    val mobile: Int,
    val tablet: Int
)

private val VISITS = listOf(
    Visits("Jan", desktop = 186, mobile = 80, tablet = 40),
    Visits("Feb", desktop = 305, mobile = 200, tablet = 62),
    Visits("Mar", desktop = 237, mobile = 120, tablet = 58),
    Visits("Apr", desktop = 173, mobile = 190, tablet = 51),
    Visits("May", desktop = 209, mobile = 130, tablet = 66),
    Visits("Jun", desktop = 214, mobile = 140, tablet = 70),
    Visits("Jul", desktop = 280, mobile = 170, tablet = 76),
    Visits("Aug", desktop = 310, mobile = 210, tablet = 83),
    Visits("Sep", desktop = 268, mobile = 232, tablet = 90),
    Visits("Oct", desktop = 342, mobile = 250, tablet = 96),
    Visits("Nov", desktop = 390, mobile = 286, tablet = 91),
    Visits("Dec", desktop = 420, mobile = 301, tablet = 120)
)

Pressing a value

onSelect gives the value that is pressed: its series, its place, its value and how the chart writes it. Press a value, or go to a place with the keyboard and press Enter: the row under the card writes what was pressed.

Here the tooltip tells the series apart (shared = false), so what is pressed is the value of the series pointed at. Where the series are shown together, what is pressed is the value of the series nearest the press.

val report = LocalExampleEvents.current
PantrixChart(
    series = listOf(
        LineSeries(
            key = "desktop",
            name = "Desktop",
            data = VISITS,
            x = { it.month },
            y = { it.desktop },
            marker = ChartMarker.Circle
        ),
        LineSeries("mobile", "Mobile", VISITS, x = { it.month }, y = { it.mobile }, marker = ChartMarker.Circle)
    ),
    tooltip = ChartTooltip(shared = false),
    description = "Desktop and mobile visits by month",
    onSelect = { point: ChartPoint -> report("${point.label}, ${point.name}: ${point.text}") },
    tagKey = "pressed"
)

private data class Visits(
    val month: String,
    val desktop: Int,
    val mobile: Int,
    val tablet: Int
)

private val VISITS = listOf(
    Visits("Jan", desktop = 186, mobile = 80, tablet = 40),
    Visits("Feb", desktop = 305, mobile = 200, tablet = 62),
    Visits("Mar", desktop = 237, mobile = 120, tablet = 58),
    Visits("Apr", desktop = 173, mobile = 190, tablet = 51),
    Visits("May", desktop = 209, mobile = 130, tablet = 66),
    Visits("Jun", desktop = 214, mobile = 140, tablet = 70),
    Visits("Jul", desktop = 280, mobile = 170, tablet = 76),
    Visits("Aug", desktop = 310, mobile = 210, tablet = 83),
    Visits("Sep", desktop = 268, mobile = 232, tablet = 90),
    Visits("Oct", desktop = 342, mobile = 250, tablet = 96),
    Visits("Nov", desktop = 390, mobile = 286, tablet = 91),
    Visits("Dec", desktop = 420, mobile = 301, tablet = 120)
)

Missing values

missing says what the line does at a row that has no value. This sensor gave no reading in April and in August. Gap breaks the line there: what is missing is seen to be missing. Connect goes on from the point before to the point after. Zero counts the row as zero.

A value with no point before it and none after it is drawn as a dot though no mark was asked for: a line through a single point has nothing to show.

var missing by remember { mutableStateOf(ChartMissing.Gap) }
PantrixSegmentButton(
    options = MISSING_KINDS,
    selected = missing,
    onSelect = { missing = it },
    fullWidth = false,
    tagKey = "missing"
)
PantrixChart(
    series = listOf(
        LineSeries(
            key = "sensor",
            name = "Temperature",
            data = SENSOR,
            x = { it.first },
            y = { it.second },
            marker = ChartMarker.Circle,
            missing = missing
        )
    ),
    description = "The sensor's mean temperature by month",
    tagKey = "missing"
)

private val SENSOR: List<Pair<String, Double?>> = listOf(
    "Jan" to 4.5,
    "Feb" to 6.1,
    "Mar" to 9.8,
    "Apr" to null,
    "May" to 18.2,
    "Jun" to 23.4,
    "Jul" to 26.9,
    "Aug" to null,
    "Sep" to 21.3,
    "Oct" to 15.6,
    "Nov" to 10.2,
    "Dec" to 5.9
)

private val MISSING_KINDS = listOf(
    SegmentOption(value = ChartMissing.Gap, label = "Gap", tagKey = "gap"),
    SegmentOption(value = ChartMissing.Connect, label = "Connect", tagKey = "connect"),
    SegmentOption(value = ChartMissing.Zero, label = "Count as zero", tagKey = "zero")
)

A chart with no data

A chart with no value writes the theme's words ("No data") in the middle of its plot. empty puts content of its own in their place: here a line that says what is going on and a button that brings the data. A screen reader is not told "No data" either; the content speaks for itself. Once there are values, the content goes.

var loaded by remember { mutableStateOf(false) }
PantrixChart(
    series = listOf(
        BarSeries(
            key = "desktop",
            name = "Desktop",
            data = if (loaded) VISITS.take(6) else emptyList(),
            x = { it.month },
            y = { it.desktop }
        )
    ),
    empty = {
        Column(
            horizontalAlignment = Alignment.CenterHorizontally,
            verticalArrangement = Arrangement.spacedBy(8.dp)
        ) {
            PantrixText(
                text = "No visits in this period",
                style = PantrixTheme.typography.footnote,
                color = PantrixTheme.colors.textSecondary
            )
            PantrixButton(
                text = "Load sample data",
                onClick = { loaded = true },
                variant = ButtonVariant.Outline,
                size = ButtonSize.Small,
                tagKey = "no_data_load"
            )
        }
    },
    description = "Desktop visits",
    tagKey = "no_data"
)
if (loaded) {
    PantrixButton(
        text = "Remove the data",
        onClick = { loaded = false },
        variant = ButtonVariant.Soft,
        size = ButtonSize.Small,
        tagKey = "no_data_clear"
    )
}

private data class Visits(
    val month: String,
    val desktop: Int,
    val mobile: Int,
    val tablet: Int
)

private val VISITS = listOf(
    Visits("Jan", desktop = 186, mobile = 80, tablet = 40),
    Visits("Feb", desktop = 305, mobile = 200, tablet = 62),
    Visits("Mar", desktop = 237, mobile = 120, tablet = 58),
    Visits("Apr", desktop = 173, mobile = 190, tablet = 51),
    Visits("May", desktop = 209, mobile = 130, tablet = 66),
    Visits("Jun", desktop = 214, mobile = 140, tablet = 70),
    Visits("Jul", desktop = 280, mobile = 170, tablet = 76),
    Visits("Aug", desktop = 310, mobile = 210, tablet = 83),
    Visits("Sep", desktop = 268, mobile = 232, tablet = 90),
    Visits("Oct", desktop = 342, mobile = 250, tablet = 96),
    Visits("Nov", desktop = 390, mobile = 286, tablet = 91),
    Visits("Dec", desktop = 420, mobile = 301, tablet = 120)
)

Many places

Ninety days. The days' names do not fit side by side; the chart leaves them out by a rule, every sixth or every tenth, and sets the same rule again as the window narrows and widens. All the values stay where they are: the value of every day is shown with a pointer or a finger.

The line goes through smoothly (ChartCurve.Smooth): it stays between its points, and shows no peak or dip that the values do not have.

PantrixChart(
    series = listOf(
        LineSeries(
            key = "sessions",
            name = "Sessions",
            data = DAILY,
            x = { it.first },
            y = { it.second },
            curve = ChartCurve.Smooth,
            fill = ChartFill.Gradient
        )
    ),
    xAxis = ChartAxis(label = { "Day $it" }),
    description = "Daily sessions of the last ninety days",
    tagKey = "many"
)

private val DAILY: List<Pair<Int, Int>> = List(DAYS) { day ->
    (day + 1) to BASE + day * GROWTH + WEEK[day % WEEK.size]
}

private const val DAYS = 90

private const val BASE = 320

private const val GROWTH = 3

private val WEEK = listOf(40, 62, 55, 70, 48, -90, -120)

Time axis

A chart whose places are moments: a server's response time, measured at uneven intervals over a day. Since the rows' x gives a kotlin.time.Instant, the chart sets them out by time: the hours measured often stand close together and those measured seldom far apart, and the axis is marked at round hours, not at the measurements.

The line and the band stand at moments too, whether a row has that moment or not: when the deployment went out and the maintenance window. A mark after the measurements, the next morning's maintenance, stretches the axis as far as itself, as a target line stretches an axis of values. The hours are read by UTC (ChartTime's offset); left out, they are read by the device's clock.

Moments are written as the app writes them too (ChartTime's format): the axis's marks on the hour as "14h", a measurement's moment in the tooltip to its minute. The ChartTimeUnit given to format says what is written: the axis's marks are asked for by the unit between them, a place by the finest unit the places are told apart by.

PantrixChart(
    series = listOf(
        LineSeries(
            key = "response",
            name = "Response time (ms)",
            data = RESPONSE_TIMES,
            x = { momentAt(it.first) },
            y = { it.second },
            marker = ChartMarker.Circle
        )
    ),
    xAxis = ChartAxis(time = ChartTime(format = ::clockOfUtc, offset = { Duration.ZERO })),
    marks = listOf(
        ChartReferenceBand(x1 = momentAt("02:00"), x2 = momentAt("04:00"), label = "Maintenance"),
        ChartReferenceLine(x = momentAt("13:20"), label = "Release"),
        ChartReferenceLine(x = Instant.parse("2026-10-14T03:00:00Z"), label = "Next maintenance")
    ),
    description = "A server's response time over a day, at the moments it was measured",
    tagKey = "time"
)

private val RESPONSE_TIMES = listOf(
    "00:10" to 182,
    "01:40" to 176,
    "02:30" to 240,
    "03:30" to 262,
    "05:00" to 190,
    "08:00" to 205,
    "09:00" to 231,
    "09:30" to 248,
    "10:00" to 236,
    "10:30" to 259,
    "11:00" to 244,
    "12:00" to 228,
    "13:00" to 252,
    "13:30" to 171,
    "14:00" to 158,
    "15:00" to 149,
    "17:30" to 144,
    "21:00" to 139,
    "23:30" to 133
)

private fun momentAt(time: String): Instant = Instant.parse("2026-10-13T$time:00Z")

private fun clockOfUtc(moment: Instant, unit: ChartTimeUnit): String {
    val minute = moment.epochSeconds.floorDiv(SECONDS_IN_A_MINUTE).mod(MINUTES_IN_A_DAY)
    val hour = (minute / MINUTES_IN_AN_HOUR).toString().padStart(2, '0')
    val ofTheHour = (minute % MINUTES_IN_AN_HOUR).toString().padStart(2, '0')
    return if (unit >= ChartTimeUnit.Hour) "${hour}h" else "$hour:$ofTheHour"
}

private const val SECONDS_IN_A_MINUTE = 60

private const val MINUTES_IN_AN_HOUR = 60

private const val MINUTES_IN_A_DAY = 24 * 60

Zoom

zoom = ChartZoom() lets the chart be looked at closer: a part of the ninety days fills the whole plot, and the rest is moved along. The buttons over the plot scroll back and forward, zoom out, zoom in and bring all the days back, in that order; a button with nothing to do is drawn faint.

With a mouse, turn the wheel while Ctrl (Command on a Mac) is held: the day under the pointer stays where it is. Once it is zoomed, drag the chart. On a touch screen spread two fingers, then move them together; one finger still points at a day. With the keyboard, go to the chart and press plus, minus and zero: zoomed in, more of the days' names find room to be written.

PantrixChart(
    series = listOf(
        LineSeries("sessions", "Sessions", DAILY, x = { it.first }, y = { it.second }, marker = ChartMarker.Circle)
    ),
    xAxis = ChartAxis(label = { "Day $it" }),
    zoom = ChartZoom(),
    description = "The daily sessions of the last ninety days, which can be looked at closer",
    tagKey = "zoomed"
)

private val DAILY: List<Pair<Int, Int>> = List(DAYS) { day ->
    (day + 1) to BASE + day * GROWTH + WEEK[day % WEEK.size]
}

private const val DAYS = 90

private const val BASE = 320

private const val GROWTH = 3

private val WEEK = listOf(40, 62, 55, 70, 48, -90, -120)

Navigator

The navigator (navigator = true) lies under the plot and holds all the places: the series are drawn small on it, and the part in view is marked. Take the marked part by its middle and drag it; widen or narrow it by the grips at its two ends; press somewhere else on the strip, and the part comes there. With the keyboard, on the strip the arrows move the part, and Home and End take it to the start and the end.

The places here are moments: the hourly measurements of sixty days. As you zoom in the axis' marks go from months to days and from days to hours. A screen reader says the strip as the "range in view", with the first and the last moment in view.

PantrixChart(
    series = listOf(
        LineSeries(
            key = "load",
            name = "Load (%)",
            data = HOURLY,
            x = { hourAt(it.first) },
            y = { it.second },
            fill = ChartFill.Gradient
        )
    ),
    xAxis = ChartAxis(time = ChartTime(offset = { Duration.ZERO })),
    yAxis = ChartAxis(min = 0.0, max = 100.0),
    zoom = ChartZoom(navigator = true),
    description = "A server's hourly load over sixty days",
    tagKey = "navigated"
)

private val HOURLY: List<Pair<Int, Int>> = List(HOURS) { hour ->
    val weekend = (hour / A_DAY) % WEEK.size >= WEEK.size - 2
    hour to (if (weekend) 36 else 52) + (wave(hour, A_DAY) * 26).toInt() + hour * 7 % 9
}

private const val HOURS = 24 * 60

private const val A_DAY = 24

private fun hourAt(hour: Int): Instant = Instant.parse("2026-08-15T00:00:00Z") + hour.hours

private val WEEK = listOf(40, 62, 55, 70, 48, -90, -120)

private fun wave(
    step: Int,
    period: Int
): Double = sin(step * 2 * PI / period)

Two charts zoomed together

Two charts that are given the same ChartState zoom together: the part in view is kept in the state, and both show it. Zoom the upper chart, or drag the lower one's navigator: the other shows the same days. Pointing at a day is shared too.

The buttons are on the upper chart and the strip on the lower: ChartZoom(buttons = false, navigator = true). The two charts have the same places, so the same part is the same days.

val state = rememberChartState()
PantrixChart(
    series = listOf(LineSeries("sessions", "Sessions", DAILY, x = { it.first }, y = { it.second })),
    modifier = Modifier.height(220.dp),
    xAxis = ChartAxis(label = { "Day $it" }),
    zoom = ChartZoom(),
    state = state,
    description = "Daily sessions of the last ninety days",
    tagKey = "together_sessions"
)
PantrixChart(
    series = listOf(
        LineSeries(
            key = "conversion",
            name = "Conversion (%)",
            data = CONVERSION,
            x = { it.first },
            y = { it.second },
            fill = ChartFill.Solid
        )
    ),
    modifier = Modifier.height(220.dp),
    xAxis = ChartAxis(label = { "Day $it" }),
    zoom = ChartZoom(buttons = false, navigator = true),
    state = state,
    description = "The daily conversion rate of the last ninety days",
    tagKey = "together_conversion"
)

private val DAILY: List<Pair<Int, Int>> = List(DAYS) { day ->
    (day + 1) to BASE + day * GROWTH + WEEK[day % WEEK.size]
}

private val CONVERSION: List<Pair<Int, Double>> = List(DAYS) { day ->
    (day + 1) to 3.2 - WEEK[day % WEEK.size] / 100.0 + day / 120.0
}

private const val DAYS = 90

private const val BASE = 320

private const val GROWTH = 3

private val WEEK = listOf(40, 62, 55, 70, 48, -90, -120)

The screen's own buttons

The screen can say the part in view too: ChartState.zoomTo takes two shares of the way along the places, from zero to one, and resetZoom brings them all back. The three options here stand in for the chart's own buttons (ChartZoom(buttons = false)); the mouse, the fingers and the keyboard still work.

The state is read as well: the line underneath writes state.window. The part in view is kept, with the hidden series, when the screen is rotated (rememberChartState).

val state = rememberChartState()
var span by remember { mutableStateOf(DAYS) }
PantrixSegmentButton(
    options = SPANS,
    selected = span,
    onSelect = { days ->
        span = days
        // The days stand at even steps from the chart's start to its end: the last `days` days start that far from the end.
        if (days == DAYS) state.resetZoom() else state.zoomTo(1f - (days - 1f) / (DAYS - 1f), 1f)
    },
    fullWidth = false,
    tagKey = "zoom_span"
)
PantrixChart(
    series = listOf(
        LineSeries("sessions", "Sessions", DAILY, x = { it.first }, y = { it.second }, marker = ChartMarker.Circle)
    ),
    xAxis = ChartAxis(label = { "Day $it" }),
    zoom = ChartZoom(buttons = false),
    state = state,
    description = "Daily sessions of the last ninety days",
    tagKey = "zoom_from_screen"
)
val from = (state.window.start * PERCENT).toInt()
val to = (state.window.endInclusive * PERCENT).toInt()
PantrixText(
    text = if (state.zoomed) "In view: $from% – $to%" else "All the days are in view",
    style = PantrixTheme.typography.footnote,
    color = PantrixTheme.colors.textSecondary
)

private val DAILY: List<Pair<Int, Int>> = List(DAYS) { day ->
    (day + 1) to BASE + day * GROWTH + WEEK[day % WEEK.size]
}

private const val DAYS = 90

private val SPANS = listOf(
    SegmentOption(value = DAYS, label = "All", tagKey = "all"),
    SegmentOption(value = 30, label = "Last 30 days", tagKey = "month"),
    SegmentOption(value = 7, label = "Last 7 days", tagKey = "week")
)

private const val BASE = 320

private const val GROWTH = 3

private val WEEK = listOf(40, 62, 55, 70, 48, -90, -120)

private const val PERCENT = 100

Tens of thousands of points

Fifty thousand points: what a sensor measured once a minute for thirty-five days. The plot is a few hundred pixels wide, so the line goes through the first, the highest, the lowest and the last point of each column of pixels: the line on the screen covers the same pixels as one through every point, and no peak or dip is lost.

Zoom in: only the minutes in view are drawn, and once there are fewer than two points to a pixel each point is drawn itself. closest says how close the chart may be looked at, at the most: here a thousand times, which is fifty minutes.

"Thin with LTTB" (ChartDecimation(algorithm = DecimationAlgorithm.Lttb)) draws the line through one point to a pixel and keeps its shape; a minute's flicker between two kept points is not drawn, and the line looks smoother. "Fit the axis to the part in view" (ChartZoom(fitValues = true)) makes the axis of values for the minutes in view alone as you zoom in: a degree or two over a few hours fills the plot. Turned off, the axis stays as all the days have it and holds still as you move along.

var lttb by remember { mutableStateOf(false) }
var fit by remember { mutableStateOf(false) }
PantrixSwitchButton(
    checked = lttb,
    onCheckedChange = { lttb = it },
    label = "Thin with LTTB",
    tagKey = "many_points_lttb"
)
PantrixSwitchButton(
    checked = fit,
    onCheckedChange = { fit = it },
    label = "Fit the axis to the part in view",
    tagKey = "many_points_fit"
)
PantrixChart(
    series = listOf(
        LineSeries("temperature", "Temperature (°C)", MINUTES, x = { minuteAt(it.first) }, y = { it.second })
    ),
    xAxis = ChartAxis(time = ChartTime(offset = { Duration.ZERO })),
    zoom = ChartZoom(closest = 1000f, navigator = true, fitValues = fit),
    decimation = ChartDecimation(algorithm = if (lttb) DecimationAlgorithm.Lttb else DecimationAlgorithm.MinMax),
    description = "The temperature a sensor measured once a minute for thirty-five days",
    tagKey = "many_points"
)

private val MINUTES: List<Pair<Int, Double>> by lazy {
    List(MANY_MINUTES) { minute ->
        minute to 18.0 + wave(minute, A_DAY * 60) * 6.0 + minute / 20_000.0 + (minute * 7919 % 41 - 20) / 25.0
    }
}

private const val MANY_MINUTES = 50_000

private const val A_DAY = 24

private fun minuteAt(minute: Int): Instant = Instant.parse("2026-09-08T00:00:00Z") + minute.minutes

private fun wave(
    step: Int,
    period: Int
): Double = sin(step * 2 * PI / period)

Export

export = ChartExport(...) puts an export button among the chart's own. Press it and choose the kind: the chart makes the file and hands it to onExport. Where the file goes is the app's to say: this example says its name, its type and its size, shows the picture again underneath when it is a PNG, and "Save the file" hands it over in the platform's window for saving (rememberFileSaver): the window for a new document on Android, Files on iOS, the save dialog on the desktop, a download on the web.

The file shows the chart as it is on the screen: take a series out from the legend and export again. The place that is pointed at and its tooltip are not in the file; the legend stands under the plot. An SVG and a PDF are sharp at any size, and a few kilobytes.

var file by remember { mutableStateOf<ChartExportFile?>(null) }
val saver = rememberFileSaver()
PantrixChart(
    series = listOf(
        LineSeries("desktop", "Desktop", VISITS, x = { it.month }, y = { it.desktop }, fill = ChartFill.Gradient),
        LineSeries("mobile", "Mobile", VISITS, x = { it.month }, y = { it.mobile }, dash = ChartDash.Dashed)
    ),
    export = ChartExport(fileName = "visits") { file = it },
    description = "Desktop and mobile visits by month",
    tagKey = "exported"
)
val made = file
PantrixText(
    text = made?.let { "${it.name} · ${it.mimeType} · ${it.bytes.size / KILOBYTE + 1} KB" }
        ?: "No file has been made yet",
    style = PantrixTheme.typography.footnote,
    color = PantrixTheme.colors.textSecondary
)
if (made != null) {
    PantrixButton(
        text = "Save the file",
        onClick = { saver.save(PantrixFile(made.name, made.bytes, made.mimeType)) {} },
        variant = ButtonVariant.Outline,
        size = ButtonSize.Small,
        tagKey = "exported_save"
    )
}
if (made != null && made.format == ChartExportFormat.Png) {
    val picture = remember(made) { made.bytes.decodeToImageBitmap() }
    PantrixImage(
        bitmap = picture,
        modifier = Modifier.fillMaxWidth().aspectRatio(picture.width.toFloat() / picture.height),
        contentDescription = "The picture that was made"
    )
}

private data class Visits(
    val month: String,
    val desktop: Int,
    val mobile: Int,
    val tablet: Int
)

private val VISITS = listOf(
    Visits("Jan", desktop = 186, mobile = 80, tablet = 40),
    Visits("Feb", desktop = 305, mobile = 200, tablet = 62),
    Visits("Mar", desktop = 237, mobile = 120, tablet = 58),
    Visits("Apr", desktop = 173, mobile = 190, tablet = 51),
    Visits("May", desktop = 209, mobile = 130, tablet = 66),
    Visits("Jun", desktop = 214, mobile = 140, tablet = 70),
    Visits("Jul", desktop = 280, mobile = 170, tablet = 76),
    Visits("Aug", desktop = 310, mobile = 210, tablet = 83),
    Visits("Sep", desktop = 268, mobile = 232, tablet = 90),
    Visits("Oct", desktop = 342, mobile = 250, tablet = 96),
    Visits("Nov", desktop = 390, mobile = 286, tablet = 91),
    Visits("Dec", desktop = 420, mobile = 301, tablet = 120)
)

private const val KILOBYTE = 1024

The part in view, one kind

Given one kind alone the button opens no choice: a press makes that file. Here only a PNG, at three pixels to a dp (scale = 3f) and on a white ground (background); with no ground given it is the theme's, dark in the dark theme.

The chart can be zoomed: zoom in and export. The file has the part in view, and neither the navigator nor the buttons. The picture that was made is shown underneath; the room around the chart is the file's own (padding).

var picture by remember { mutableStateOf<ImageBitmap?>(null) }
val state = rememberChartState()
// The chart opens on the last thirty-six days.
LaunchedEffect(state) { state.zoomTo(start = 0.6f, end = 1f) }
PantrixChart(
    series = listOf(BarSeries("sessions", "Sessions", DAILY, x = { it.first }, y = { it.second })),
    xAxis = ChartAxis(label = { "Day $it" }),
    zoom = ChartZoom(navigator = true),
    export = ChartExport(
        formats = listOf(ChartExportFormat.Png),
        fileName = "sessions",
        scale = 3f,
        background = Color.White
    ) { picture = it.bytes.decodeToImageBitmap() },
    state = state,
    description = "The daily sessions of the last ninety days, which can be exported",
    tagKey = "exported_part"
)
picture?.let { made ->
    PantrixImage(
        bitmap = made,
        modifier = Modifier.fillMaxWidth().aspectRatio(made.width.toFloat() / made.height),
        contentDescription = "The picture that was made"
    )
}

private val DAILY: List<Pair<Int, Int>> = List(DAYS) { day ->
    (day + 1) to BASE + day * GROWTH + WEEK[day % WEEK.size]
}

private const val DAYS = 90

private const val BASE = 320

private const val GROWTH = 3

private val WEEK = listOf(40, 62, 55, 70, 48, -90, -120)

Patterns for colour blindness

Patterns for colour blindness (ChartStyle(patterns = true)): the first series stays plain, the others have a texture cut into their bars in turn, and lines are drawn dashed and dotted in turn; the legend and the tooltip show the same texture. Turn the switch off: the patterns go, and the series are told apart by their colours alone.

A series can say its own texture with pattern (ChartPattern); a texture it says is drawn with the switch off too: here the dots in the tablet's bars.

var patterns by remember { mutableStateOf(true) }
val half = VISITS.take(6)
PantrixSwitchButton(
    checked = patterns,
    onCheckedChange = { patterns = it },
    label = "Patterns",
    tagKey = "chart_patterns"
)
PantrixChart(
    series = listOf(
        BarSeries("desktop", "Desktop", half, x = { it.month }, y = { it.desktop }),
        BarSeries("mobile", "Mobile", half, x = { it.month }, y = { it.mobile }),
        BarSeries("tablet", "Tablet", half, x = { it.month }, y = { it.tablet }, pattern = ChartPattern.Dots)
    ),
    description = "Visits of the first six months by device, in textured bars",
    style = ChartStyle(patterns = patterns),
    tagKey = "patterns"
)
PantrixChart(
    series = listOf(
        LineSeries("desktop", "Desktop", VISITS, x = { it.month }, y = { it.desktop }),
        LineSeries("mobile", "Mobile", VISITS, x = { it.month }, y = { it.mobile }),
        LineSeries("tablet", "Tablet", VISITS, x = { it.month }, y = { it.tablet })
    ),
    description = "Visits by device and month, in lines dashed in turn",
    style = ChartStyle(patterns = patterns),
    tagKey = "patterns_lines"
)

private data class Visits(
    val month: String,
    val desktop: Int,
    val mobile: Int,
    val tablet: Int
)

private val VISITS = listOf(
    Visits("Jan", desktop = 186, mobile = 80, tablet = 40),
    Visits("Feb", desktop = 305, mobile = 200, tablet = 62),
    Visits("Mar", desktop = 237, mobile = 120, tablet = 58),
    Visits("Apr", desktop = 173, mobile = 190, tablet = 51),
    Visits("May", desktop = 209, mobile = 130, tablet = 66),
    Visits("Jun", desktop = 214, mobile = 140, tablet = 70),
    Visits("Jul", desktop = 280, mobile = 170, tablet = 76),
    Visits("Aug", desktop = 310, mobile = 210, tablet = 83),
    Visits("Sep", desktop = 268, mobile = 232, tablet = 90),
    Visits("Oct", desktop = 342, mobile = 250, tablet = 96),
    Visits("Nov", desktop = 390, mobile = 286, tablet = 91),
    Visits("Dec", desktop = 420, mobile = 301, tablet = 120)
)

Your own colours and sizes

A chart's own colours and sizes. ChartColors.series are the colours handed out to the series in order; here two purples from the palette in the place of the theme's chart colours. The grid lines are fainter in this chart too.

ChartStyle gives the sizes: the line is thicker, the marks larger. A field that is not given stays the theme's. The same for all charts is given to the theme: the colours with PantrixColors.chart, the sizes with the theme's styles.

val dark = LocalDemoDarkTheme.current
val violet = PantrixPalette.Violet
val pink = PantrixPalette.Pink
PantrixChart(
    series = listOf(
        LineSeries(
            key = "desktop",
            name = "Desktop",
            data = VISITS,
            x = { it.month },
            y = { it.desktop },
            marker = ChartMarker.Diamond
        ),
        LineSeries("mobile", "Mobile", VISITS, x = { it.month }, y = { it.mobile }, marker = ChartMarker.Triangle)
    ),
    description = "Desktop and mobile visits by month",
    style = ChartStyle(lineWidth = 3.dp, markerSize = 10.dp),
    colors = ChartColors(
        series = if (dark) listOf(violet.shade400, pink.shade300) else listOf(violet.shade600, pink.shade500),
        grid = PantrixTheme.colors.divider.copy(alpha = 0.5f)
    ),
    tagKey = "own_look"
)

private data class Visits(
    val month: String,
    val desktop: Int,
    val mobile: Int,
    val tablet: Int
)

private val VISITS = listOf(
    Visits("Jan", desktop = 186, mobile = 80, tablet = 40),
    Visits("Feb", desktop = 305, mobile = 200, tablet = 62),
    Visits("Mar", desktop = 237, mobile = 120, tablet = 58),
    Visits("Apr", desktop = 173, mobile = 190, tablet = 51),
    Visits("May", desktop = 209, mobile = 130, tablet = 66),
    Visits("Jun", desktop = 214, mobile = 140, tablet = 70),
    Visits("Jul", desktop = 280, mobile = 170, tablet = 76),
    Visits("Aug", desktop = 310, mobile = 210, tablet = 83),
    Visits("Sep", desktop = 268, mobile = 232, tablet = 90),
    Visits("Oct", desktop = 342, mobile = 250, tablet = 96),
    Visits("Nov", desktop = 390, mobile = 286, tablet = 91),
    Visits("Dec", desktop = 420, mobile = 301, tablet = 120)
)

API

series

A chart of values along a run of places: lines and filled areas through the values each series has at each place, and bars up to them, with an axis of the places under them and an axis of the values beside them.

ParameterTypeDefaultWhat it is
seriesList<ChartSeries>the series, in the order they are drawn and listed: a later one is drawn over an earlier. Their keys tell them apart, and two with one key are refused.
modifierModifierModifierthe chart's modifier. A height here is the chart's, the legend's with it.
xAxisChartAxis?ChartAxis()the axis of the places, under the chart, or beside it in a horizontal chart. null draws nothing of it.
yAxisChartAxis?ChartAxis()the axis of the values, beside the chart, or under it in a horizontal chart. null draws nothing of it.
secondYAxisChartAxis?ChartAxis()the second axis of the values, at the chart's other side, or over it in a horizontal chart: drawn where a series, or a mark at a value, is read by it, and not otherwise. null draws nothing of it, and what is read by it is set out by it all the same.
marksList<ChartMark>emptyList()the lines and the bands drawn in the plot besides the series.
stackingChartStackingChartStacking.Normalhow the series of a stack stand on one another: as their values, or as their shares of the stack.
orientationChartOrientationChartOrientation.Verticalwhich way the places go: along the chart's foot, as columns, or down its side, as horizontal bars, each name written across in the room of a line. Horizontal, a chart that is looked at closer moves up and down its places, and its navigator stands beside the plot.
legendChartLegend?ChartLegend()where the names of the series stand. null shows none.
tooltipChartTooltip?ChartTooltip()what is shown for the place that is pointed at. null shows none.
zoomChartZoom?nullwhat lets the reader look closer at a part of the places and move along the rest. null, which it is unless the chart is given one, shows all the places and no more.
decimationChartDecimation?ChartDecimation()how a line with more points in view than the plot has room for is drawn through fewer of them. null draws every point, as many as there are.
exportChartExport?nullwhat lets the reader take the chart out of the app as a file: a picture, or a drawing that is sharp at any size. null, which it is unless the chart is given one, and the chart has no such button.
empty(@Composable () -> Unit)?nullwhat the chart shows in its plot while it has no value to draw, in the place of the theme's words for it ("No data"), which a screen reader is then not told either. null writes the words. A file the chart is exported as says them.
stateChartStaterememberChartState()what the reader has hidden and what they point at: the chart's own where none is given.
descriptionString?nullwhat the chart shows, in a few words, for a screen reader: the first thing it says of the chart. Left out, the chart is announced by the names of its series.
onSelect((ChartPoint) -> Unit)?nullcalled with the value that is pressed: the one pointed at, or of the series nearest the press where the place's values are pointed at together.
styleChartStyle?nullthis chart's own geometry: the fields set here, with the theme's for the rest.
colorsChartColors?nullthis chart's own colours, where they are not the theme's.
tagKeyString?nullthe chart's automation key: the chart is Chart_<tagKey>, its plot Chart_<tagKey>.Plot, a place of it Chart_<tagKey>.Point_<number> counted from one, the legend's entry of a series Chart_<tagKey>.Legend_<series' key> and the tooltip Chart_<tagKey>.Tooltip. A press on a value is reported as a Chart select event with the series' key, and a press on a legend's entry as a toggle event with the key and whether the series is shown. Nothing of the values is reported.

ChartStyle

A chart's geometry: how tall it is where nothing says, how thick its lines and how large its marks are, and the room between its parts; and whether its fills have textures besides their colours.

ParameterTypeDefaultWhat it is
heightDpDp.UnspecifiedHow tall a chart's plot is, with its axes, where the chart's modifier gives it no height. A legend above or under the plot adds its own.
lineWidthDpDp.UnspecifiedThe width of a series' line.
markerSizeDpDp.UnspecifiedThe width and height of the mark on a point.
selectedMarkerSizeDpDp.UnspecifiedThe same of the mark on the point that is pointed at.
gridWidthDpDp.UnspecifiedThe width of the grid's lines, of the lines along the axes and of the line that marks the place pointed at. Zero draws none.
labelGapDpDp.UnspecifiedBetween the plot and what is written beside it and under it, and between that and an axis' title.
tickSpacingDpDp.UnspecifiedThe least room between two values marked along the axis of values: what decides how many are marked where the axis does not say.
legendGapDpDp.UnspecifiedBetween the legend and the chart it stands beside.
legendItemGapDpDp.UnspecifiedBetween one entry of the legend and the next.
swatchSizeDpDp.UnspecifiedHow wide the sample of a series is in the legend and in the tooltip: a short run of its line, or a patch of its fill.
swatchGapDpDp.UnspecifiedBetween that sample and the name after it.
tooltipShapeShapeSpecShapeSpec.UnspecifiedThe tooltip's corners: a step of the brand's shape scale, or a fixed shape.
tooltipHorizontalPaddingDpDp.UnspecifiedBetween the tooltip's sides and what it holds.
tooltipVerticalPaddingDpDp.UnspecifiedBetween its top and bottom and what it holds, and between its heading and rows.
tooltipGapDpDp.UnspecifiedBetween the place pointed at and the tooltip beside it.
tooltipElevationDpDp.UnspecifiedThe tooltip's shadow.
barGapDpDp.UnspecifiedBetween two bars that stand side by side at a place. Bars too thin for it stand closer.
barMaxWidthDpDp.UnspecifiedThe widest a bar is, however much room its place has: a chart of three places is not three slabs.
barRadiusDpDp.UnspecifiedHow round the corners of a bar's end are, the end away from nought.
sliceGapDpDp.UnspecifiedBetween two slices of a pie that lie side by side, the same from the pie's middle to its rim.
selectedSliceOffsetDpDp.UnspecifiedHow far the slice of a pie that is pointed at stands out past the pie's rim. The pie keeps this much room about itself, so that the slice has where to stand.
gaugeWidthDpDp.UnspecifiedHow thick the arc of a gauge is. An arc is no thicker than half its radius.
meterHeightDpDp.UnspecifiedHow tall the bar of a meter group is.
trackerHeightDpDp.UnspecifiedHow tall the blocks of a status tracker are.
bubbleMinSizeDpDp.UnspecifiedHow far across the smallest bubble of a scatter chart is: a point whose size is nothing, or too little to be seen as an area, is still a point.
bubbleMaxSizeDpDp.UnspecifiedHow far across its largest bubble is: the one whose size is the most of the chart's.
navigatorHeightDpDp.UnspecifiedHow tall the strip under a chart's plot is, on which the part of the chart that is in view is marked and moved, in a chart whose zoom has one: how wide, beside a plot whose places go down its side.
barBandShareFloatFloat.NaNHow much of a place's band its bars take, more than nought and one at the most: the rest is the room between one place's bars and the next's. Bars are no wider than barMaxWidth all the same.
markLineWidthDpDp.UnspecifiedThe width of a mark's line: a target, a limit, a day. Zero draws none, and the mark's words are written all the same.
markBandAlphaFloatFloat.NaNHow much of its colour a mark's band is drawn in, from nought to one: a band lies behind the series, and is a tint.
patternsBoolean?nullWhether the fills of a chart's series, slices and tiles are drawn with textures besides their colours, and its lines broken in turn (ChartPattern): what tells them apart for a reader who cannot tell the colours apart. Left out, as the system is set: on where it is set to tell things apart without colour, iOS's "Differentiate Without Color" and Android's colour correction, and off otherwise.
nodeWidthDpDp.UnspecifiedHow wide a node of a sankey is.
nodeGapDpDp.UnspecifiedBetween two nodes of a column of a sankey.

ChartColors

What a chart is painted with, for a chart that does not take the theme's colours: a colour that is set is used, and one that is left out stays the theme's.

ParameterTypeDefaultWhat it is
seriesList<Color>emptyList()The colours the series are drawn in, in the order they are handed out, for the series that name none of their own. Empty, they are the theme's chart colours.
gridColorColor.UnspecifiedThe lines drawn across the chart at the marks of an axis. The theme's divider.
axisColorColor.UnspecifiedThe line along an axis. The theme's border.
labelColorColor.UnspecifiedWhat is written beside the axes: the values, the places and the axes' titles, and the names in the legend. The theme's textSecondary.
crosshairColorColor.UnspecifiedThe line that marks the place pointed at. The theme's borderHover: stronger than the grid it crosses, and quieter than the series it stands behind.
tooltipContainerColorColor.UnspecifiedThe tooltip's fill. The theme's surface.
tooltipBorderColorColor.UnspecifiedThe tooltip's edge. The theme's border.
tooltipTextColorColor.UnspecifiedThe tooltip's heading and its values. The theme's textPrimary.
tooltipLabelColorColor.UnspecifiedThe names of the series in the tooltip. The theme's textSecondary.
risingColorColor.UnspecifiedWhat a value that goes up is drawn in: a step of a waterfall that adds. The theme's success.
fallingColorColor.UnspecifiedWhat a value that goes down is drawn in: a step of a waterfall that takes away. The theme's danger.

Accessibility