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
- Series. A
LineSeriesis a line; withfillthe room under the line is filled and it becomes an area. ABarSeriesis a bar at each place. Each series takes a key (key), a name, its rows and two functions that read two things from those rows:xthe row's place,yits value. A row with no value givesnull. Series of different kinds are drawn in one chart: a line over bars. - Stacking. Series that name the same
stackstand on one another at each place: bars in a single bar, areas on top of each other. A series that names none stands on zero by itself. - Places. A place is what a row's
xgives: a name, a day, anything that equals itself. The chart sets out every place any of its series names, in the order they are first named. Series need not be read from one list, and need not stand at the same places either. - Time axis. A chart whose every place is a moment (
kotlin.time.Instant) sets its places out by time: in the order of time whatever the order of the rows, and as far apart as their moments are. The axis is marked at round moments (hour, day, month) and a place is written as a date: by the device's clock and in the theme's language, unless thetimeofxAxis(ChartTime) says otherwise. No date library is needed. - Axes.
xAxisis the axis of the places,yAxisof the values. AChartAxissays its title, whether it is written, its line and its grid lines; the axis of values also its least, its most, whether it starts at zero and how many values are marked. Values are marked at round numbers. - A second axis. In a chart with two kinds of value (a count and a rate), a series that says
axis = ChartYAxis.Secondis drawn by a scale of its own; that scale's axis (secondYAxis) is written at the chart's other side, and its marks stand on the first axis' grid lines. Each value is written the way its own axis writes. A stack is made of the series of one axis. - Writing. Values are written in the theme's language: short beside the axis from ten thousand on (
12K), in full in the tooltip.formatsays how an axis' values are written,labelhow its places are. - Marks.
marksdraws what the series are read against: a line at a value or at a place (ChartReferenceLine), a shaded band between two values or two places (ChartReferenceBand). A mark at a value makes its axis go far enough to show it. A mark that has a label is said to a screen reader too. - Value labels. A series'
labelswrites its values at their marks: over a line's point, at the end of a bar, inside a bar of a stack. A label that has no room is left out. - Legend. The names of the series, each beside a sample that shows how the series is drawn. A chart of one series has no legend. Pressing an entry takes the series out of the chart, pressing it again brings it back; the axis is set again for the series that stay. The legend stands on one of the chart's four sides.
- Tooltip. The values at the place pointed at, in a small card beside the place. With
sharedthe values of all the series there are shown together, and with it off only the value of the series nearest the pointer.formatsays how the card writes its values; left out, the axis'formatwrites them.contentleaves the inside of the card to the screen. In a narrow chart with no room beside the place the card stands over the place, at the end the values are not at. - Pointing at a place. A pointer over the chart points at the nearest place. A finger points by touching, or drawn along the chart; a second touch on the place it points at lets go of it. The place is marked by a line; in a chart that has bars its band is shaded.
- State.
ChartStateholds the series the reader has hidden and the place they point at. A screen that makes one withrememberChartStateand gives it to the chart reads both, and says both; two charts given the same state behave as one. - Zoom. A chart that is given a
zoom(ChartZoom) shows a part of its places as wide as the plot, and is moved along the rest. With a mouse: the wheel, while Ctrl (Command on a Mac) is held, zooms about the place under the pointer, and a wheel turned sideways and a drag move along the places; a wheel with no key is the page's. Two fingers spread apart zoom in, and moved together they take the places along; one finger points at a place, as in any chart. On the keyboard plus and minus zoom in and out, zero brings all the places back, and the arrows bring the place they go to into view. For whoever uses none of those there are the buttons (buttons) and, under the plot, the navigator that all the places lie on (navigator). The part in view is in theChartState(window,zoomTo,resetZoom): two charts given the same state zoom together. The axes of values stay as all the values have them and hold still as you move along;fitValues = truemakes them again for the values of the places in view, and the navigator stays on the axes of all the values. A chart that is given nozoomdoes none of this. - Export. A chart that is given an
export(ChartExport) has an export button among its own: the reader chooses the kind (PNG, SVG, PDF), the chart makes the file and hands it toonExport. Where the file goes is the app's to say: a phone shares it, a window saves it, a browser downloads it. The file shows the chart as it is on the screen: a series that was taken out is out, and of a chart that is zoomed it has the part in view; the place that is pointed at, the tooltip and the buttons are not in it, and the legend stands under the plot. An SVG and a PDF are lines and shapes, sharp at any size; their words are written in a typeface of whatever opens the file. A chart that is given noexporthas no such button. - Many points. A line with more than two points to a pixel is thinned (
decimation,ChartDecimation): the defaultDecimationAlgorithm.MinMaxgoes through the first, the highest, the lowest and the last point of each column of pixels, covers the same pixels and does far less work;DecimationAlgorithm.Lttbkeeps one point to a pixel and keeps the line's shape.thresholdsays after how many points it is thinned;decimation = nulldraws every point. A thinned line is drawn straight, and its points have no marks. A chart that is looked at closer draws only the places in view. - Size. The chart fills the width it is given. A height given with
modifieris the whole chart's; given none, the plot is as tall as the style says and a legend above or under it adds to that. Place names that have no room left to be written are left out by a rule: every second, every third. - Motion. The chart comes in drawn from the side where the places start; when the values change, each goes from where it was to where it is. Where the system asks for reduced motion none of it moves.
- Colour. A series that does not say its colour takes the one at its place among the theme's chart colours; a hidden series keeps its place and its colour. The grid lines and the words are the theme's; where the chart stands on a fill they come from the colour that is read there.
- Patterns.
ChartStyle(patterns = true)tells series apart by more than their colours: the first series stays plain, the others have a texture cut into their fills in turn (ChartPattern), lines are drawn dashed and dotted in turn, and after the fourth line their points are marked. A step of a waterfall that takes away and a candle that falls take a texture too. The legend, the tooltip and an exported file show the same texture. Left out, the system's setting is followed: the patterns are on where iOS's "Differentiate Without Color" or Android's colour correction is. A series says its own texture withpatternand its own dashes withdash; what it says always holds. - Reading direction. In a language written from right to left the places start at the right and the values are written at the right.
- Automation. With
tagKeythe chart isChart_<tagKey>, the plotChart_<tagKey>.Plot, a placeChart_<tagKey>.Point_<number>, a series' legend entryChart_<tagKey>.Legend_<the series' key>, and the tooltipChart_<tagKey>.Tooltip. The zoom buttons areChart_<tagKey>.Zoom_in,.Zoom_out,.Zoom_all,.Zoom_backand.Zoom_on, the navigator isChart_<tagKey>.Navigator, and the export buttonChart_<tagKey>.Export. A press on a value is reported asselect, a press on a legend entry astoggle, a press of a zoom button or key aszoom, and a file that is made asexportwith its kind; nothing of the values is reported.
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.
| Parameter | Type | Default | What it is |
|---|---|---|---|
series | List<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. | |
modifier | Modifier | Modifier | the chart's modifier. A height here is the chart's, the legend's with it. |
xAxis | ChartAxis? | ChartAxis() | the axis of the places, under the chart, or beside it in a horizontal chart. null draws nothing of it. |
yAxis | ChartAxis? | ChartAxis() | the axis of the values, beside the chart, or under it in a horizontal chart. null draws nothing of it. |
secondYAxis | ChartAxis? | 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. |
marks | List<ChartMark> | emptyList() | the lines and the bands drawn in the plot besides the series. |
stacking | ChartStacking | ChartStacking.Normal | how the series of a stack stand on one another: as their values, or as their shares of the stack. |
orientation | ChartOrientation | ChartOrientation.Vertical | which 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. |
legend | ChartLegend? | ChartLegend() | where the names of the series stand. null shows none. |
tooltip | ChartTooltip? | ChartTooltip() | what is shown for the place that is pointed at. null shows none. |
zoom | ChartZoom? | null | what 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. |
decimation | ChartDecimation? | 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. |
export | ChartExport? | null | what 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)? | null | what 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. |
state | ChartState | rememberChartState() | what the reader has hidden and what they point at: the chart's own where none is given. |
description | String? | null | what 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)? | null | called 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. |
style | ChartStyle? | null | this chart's own geometry: the fields set here, with the theme's for the rest. |
colors | ChartColors? | null | this chart's own colours, where they are not the theme's. |
tagKey | String? | null | the 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.
| Parameter | Type | Default | What it is |
|---|---|---|---|
height | Dp | Dp.Unspecified | How 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. |
lineWidth | Dp | Dp.Unspecified | The width of a series' line. |
markerSize | Dp | Dp.Unspecified | The width and height of the mark on a point. |
selectedMarkerSize | Dp | Dp.Unspecified | The same of the mark on the point that is pointed at. |
gridWidth | Dp | Dp.Unspecified | The 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. |
labelGap | Dp | Dp.Unspecified | Between the plot and what is written beside it and under it, and between that and an axis' title. |
tickSpacing | Dp | Dp.Unspecified | The least room between two values marked along the axis of values: what decides how many are marked where the axis does not say. |
legendGap | Dp | Dp.Unspecified | Between the legend and the chart it stands beside. |
legendItemGap | Dp | Dp.Unspecified | Between one entry of the legend and the next. |
swatchSize | Dp | Dp.Unspecified | How 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. |
swatchGap | Dp | Dp.Unspecified | Between that sample and the name after it. |
tooltipShape | ShapeSpec | ShapeSpec.Unspecified | The tooltip's corners: a step of the brand's shape scale, or a fixed shape. |
tooltipHorizontalPadding | Dp | Dp.Unspecified | Between the tooltip's sides and what it holds. |
tooltipVerticalPadding | Dp | Dp.Unspecified | Between its top and bottom and what it holds, and between its heading and rows. |
tooltipGap | Dp | Dp.Unspecified | Between the place pointed at and the tooltip beside it. |
tooltipElevation | Dp | Dp.Unspecified | The tooltip's shadow. |
barGap | Dp | Dp.Unspecified | Between two bars that stand side by side at a place. Bars too thin for it stand closer. |
barMaxWidth | Dp | Dp.Unspecified | The widest a bar is, however much room its place has: a chart of three places is not three slabs. |
barRadius | Dp | Dp.Unspecified | How round the corners of a bar's end are, the end away from nought. |
sliceGap | Dp | Dp.Unspecified | Between two slices of a pie that lie side by side, the same from the pie's middle to its rim. |
selectedSliceOffset | Dp | Dp.Unspecified | How 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. |
gaugeWidth | Dp | Dp.Unspecified | How thick the arc of a gauge is. An arc is no thicker than half its radius. |
meterHeight | Dp | Dp.Unspecified | How tall the bar of a meter group is. |
trackerHeight | Dp | Dp.Unspecified | How tall the blocks of a status tracker are. |
bubbleMinSize | Dp | Dp.Unspecified | How 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. |
bubbleMaxSize | Dp | Dp.Unspecified | How far across its largest bubble is: the one whose size is the most of the chart's. |
navigatorHeight | Dp | Dp.Unspecified | How 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. |
barBandShare | Float | Float.NaN | How 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. |
markLineWidth | Dp | Dp.Unspecified | The 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. |
markBandAlpha | Float | Float.NaN | How 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. |
patterns | Boolean? | null | Whether 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. |
nodeWidth | Dp | Dp.Unspecified | How wide a node of a sankey is. |
nodeGap | Dp | Dp.Unspecified | Between 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.
| Parameter | Type | Default | What it is |
|---|---|---|---|
series | List<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. |
grid | Color | Color.Unspecified | The lines drawn across the chart at the marks of an axis. The theme's divider. |
axis | Color | Color.Unspecified | The line along an axis. The theme's border. |
label | Color | Color.Unspecified | What is written beside the axes: the values, the places and the axes' titles, and the names in the legend. The theme's textSecondary. |
crosshair | Color | Color.Unspecified | The 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. |
tooltipContainer | Color | Color.Unspecified | The tooltip's fill. The theme's surface. |
tooltipBorder | Color | Color.Unspecified | The tooltip's edge. The theme's border. |
tooltipText | Color | Color.Unspecified | The tooltip's heading and its values. The theme's textPrimary. |
tooltipLabel | Color | Color.Unspecified | The names of the series in the tooltip. The theme's textSecondary. |
rising | Color | Color.Unspecified | What a value that goes up is drawn in: a step of a waterfall that adds. The theme's success. |
falling | Color | Color.Unspecified | What a value that goes down is drawn in: a step of a waterfall that takes away. The theme's danger. |
Accessibility
- Keyboard. The chart is one stop for Tab. The arrow keys go from place to place, Home and End to the first and the last place, and Escape lets go; Enter and Space press the value pointed at. Where the tooltip tells the series apart, the arrows up and down go from one series' value to the next.
- Screen reader. The chart first says its description, given with
description, then each place with the values the series have there. The legend's entries are switches. A chart of more than two hundred places is read by its description alone.