Pantrix UI

Range Chart

A chart of what lies between two values at each place: the coldest and the warmest of a day, the least and the most a price went to, the hours a shift starts and ends at.

import com.pantrixui.components.chart.range.PantrixRangeChart

Examples

The lowest and the highest of the day

At its plainest: the rows, the x that gives a row's place, the lower value (low), the higher value (high) and the range's name. The band's two lines go smoothly (curve).

Move the pointer over a day, or touch the day: the band's two ends are marked and the tooltip writes the two values together. The axis does not start at zero: nothing of a range stands on zero.

PantrixRangeChart(
    data = WEATHER,
    x = { it.day },
    low = { it.coldest },
    high = { it.warmest },
    name = "Temperature",
    curve = ChartCurve.Smooth,
    yAxis = ChartAxis(format = { "${it.toInt()}°" }),
    description = "The lowest and the highest temperature by day of the week",
    tagKey = "plain"
)

private data class Weather(
    val day: String,
    val coldest: Int,
    val warmest: Int
)

private val WEATHER = listOf(
    Weather("Mon", coldest = 9, warmest = 17),
    Weather("Tue", coldest = 11, warmest = 19),
    Weather("Wed", coldest = 8, warmest = 15),
    Weather("Thu", coldest = 6, warmest = 14),
    Weather("Fri", coldest = 10, warmest = 21),
    Weather("Sat", coldest = 13, warmest = 24),
    Weather("Sun", coldest = 12, warmest = 22)
)

A band or bars

The same range is drawn two ways. Turn the switch: the band becomes a bar for each day, from the lower value to the higher (RangeKind.Bar). Both ends of a bar are round, and each day has a band of the chart's width.

A band suits what goes on from place to place: a temperature that changes day by day. Bars suit ranges that are read one by one.

var bars by remember { mutableStateOf(false) }
PantrixSwitchButton(
    checked = bars,
    onCheckedChange = { bars = it },
    label = "As bars",
    tagKey = "range_bars"
)
PantrixRangeChart(
    data = WEATHER,
    x = { it.day },
    low = { it.coldest },
    high = { it.warmest },
    name = "Temperature",
    kind = if (bars) RangeKind.Bar else RangeKind.Area,
    yAxis = ChartAxis(format = { "${it.toInt()}°" }),
    description = "The lowest and the highest temperature by day of the week",
    tagKey = "kinds"
)

private data class Weather(
    val day: String,
    val coldest: Int,
    val warmest: Int
)

private val WEATHER = listOf(
    Weather("Mon", coldest = 9, warmest = 17),
    Weather("Tue", coldest = 11, warmest = 19),
    Weather("Wed", coldest = 8, warmest = 15),
    Weather("Thu", coldest = 6, warmest = 14),
    Weather("Fri", coldest = 10, warmest = 21),
    Weather("Sat", coldest = 13, warmest = 24),
    Weather("Sun", coldest = 12, warmest = 22)
)

The values at its two ends

The hours each team's shift starts and ends, a bar for each team. The two values are written at the bar's two ends (labels): the end over it, the start under it. The hours are written the same way on the axis and in the labels (format). The axis is marked every six hours of the day (values): round steps would have marked it every five.

The line shows the lunch break (marks): which shift takes it in is read.

val hour: (Double) -> String = { "${it.toInt()}:00" }
PantrixRangeChart(
    data = SHIFT_HOURS,
    x = { it.team },
    low = { it.start },
    high = { it.end },
    name = "Shift",
    kind = RangeKind.Bar,
    labels = ChartDataLabels(format = hour),
    yAxis = ChartAxis(values = listOf(0.0, 6.0, 12.0, 18.0, 24.0), format = hour),
    marks = listOf(ChartReferenceLine(y = 12, label = "Lunch break")),
    description = "The hour each team's shift starts and ends at",
    tagKey = "labels"
)

private data class ShiftHours(
    val team: String,
    val start: Int,
    val end: Int
)

private val SHIFT_HOURS = listOf(
    ShiftHours("Warehouse", start = 6, end = 14),
    ShiftHours("Store", start = 9, end = 21),
    ShiftHours("Support", start = 8, end = 18),
    ShiftHours("Evening", start = 14, end = 22)
)

Shifts across

The same shifts, horizontal (horizontal = true): the teams are set down the side of the chart, and each shift goes from the left, from the hour it starts at to the hour it ends at, as on a timetable. The hours are written under the chart; the lunch break's line cuts the chart from top to bottom.

val hour: (Double) -> String = { "${it.toInt()}:00" }
PantrixRangeChart(
    data = SHIFT_HOURS,
    x = { it.team },
    low = { it.start },
    high = { it.end },
    name = "Shift",
    kind = RangeKind.Bar,
    horizontal = true,
    labels = ChartDataLabels(format = hour),
    yAxis = ChartAxis(values = listOf(0.0, 6.0, 12.0, 18.0, 24.0), format = hour),
    marks = listOf(ChartReferenceLine(y = 12, label = "Lunch break")),
    description = "The hours each team's shift starts and ends at, across",
    tagKey = "horizontal_shifts"
)

private data class ShiftHours(
    val team: String,
    val start: Int,
    val end: Int
)

private val SHIFT_HOURS = listOf(
    ShiftHours("Warehouse", start = 6, end = 14),
    ShiftHours("Store", start = 9, end = 21),
    ShiftHours("Support", start = 8, end = 18),
    ShiftHours("Evening", start = 14, end = 22)
)

Dashed lines and marks

The band's look comes from the series itself: its two lines dashed (dash), a mark on each of its points (marker), the tint between them darker than usual (fillAlpha). Turn the switch off (lines = false): the lines go, and the band stays with its tint and its marks.

var lines by remember { mutableStateOf(true) }
PantrixSwitchButton(
    checked = lines,
    onCheckedChange = { lines = it },
    label = "The band's lines",
    tagKey = "range_lines"
)
PantrixRangeChart(
    data = OUTLOOK,
    x = { it.month },
    low = { it.least },
    high = { it.most },
    name = "Expected range",
    lines = lines,
    dash = ChartDash.Dashed,
    marker = ChartMarker.Circle,
    fillAlpha = 0.28f,
    description = "The expected range of orders by month, with dashed lines and marks",
    tagKey = "look"
)

private data class Outlook(
    val month: String,
    val expected: Int,
    val least: Int,
    val most: Int
)

private val OUTLOOK = listOf(
    Outlook("Jul", expected = 420, least = 400, most = 440),
    Outlook("Aug", expected = 450, least = 415, most = 485),
    Outlook("Sep", expected = 470, least = 420, most = 520),
    Outlook("Oct", expected = 510, least = 440, most = 580),
    Outlook("Nov", expected = 540, least = 450, most = 630),
    Outlook("Dec", expected = 600, least = 480, most = 720)
)

A forecast and its bounds

A chart that wants a line over the band is a PantrixChart: the bounds the forecast is trusted within are a RangeSeries, and the forecast itself a LineSeries given after it. A series given later is drawn over the one before it.

The tooltip writes the two series together: the bounds as a range, the forecast as a single value. In the legend the range is a patch and the forecast a dashed line.

PantrixChart(
    series = listOf(
        RangeSeries(
            key = "bounds",
            name = "Expected range",
            data = OUTLOOK,
            x = { it.month },
            low = { it.least },
            high = { it.most },
            curve = ChartCurve.Smooth
        ),
        LineSeries(
            key = "expected",
            name = "Forecast",
            data = OUTLOOK,
            x = { it.month },
            y = { it.expected },
            curve = ChartCurve.Smooth,
            dash = ChartDash.Dashed
        )
    ),
    description = "Order forecast and its expected range by month",
    tagKey = "with_line"
)

private data class Outlook(
    val month: String,
    val expected: Int,
    val least: Int,
    val most: Int
)

private val OUTLOOK = listOf(
    Outlook("Jul", expected = 420, least = 400, most = 440),
    Outlook("Aug", expected = 450, least = 415, most = 485),
    Outlook("Sep", expected = 470, least = 420, most = 520),
    Outlook("Oct", expected = 510, least = 440, most = 580),
    Outlook("Nov", expected = 540, least = 450, most = 630),
    Outlook("Dec", expected = 600, least = 480, most = 720)
)

Pressing a range

A press is handed both values of the range: the point's value is the higher and its from the lower; text carries the two as the chart writes them. Press a day: the line below writes that day's range and how wide it is.

var pressed by remember { mutableStateOf<String?>(null) }
PantrixRangeChart(
    data = WEATHER,
    x = { it.day },
    low = { it.coldest },
    high = { it.warmest },
    name = "Temperature",
    kind = RangeKind.Bar,
    onSelect = { point ->
        val wide = point.value - (point.from ?: point.value)
        pressed = "${point.label}: ${point.text}, ${wide.toInt()} degrees apart"
    },
    description = "The lowest and the highest temperature by day of the week",
    tagKey = "press"
)
PantrixText(text = pressed ?: "Press a day")

private data class Weather(
    val day: String,
    val coldest: Int,
    val warmest: Int
)

private val WEATHER = listOf(
    Weather("Mon", coldest = 9, warmest = 17),
    Weather("Tue", coldest = 11, warmest = 19),
    Weather("Wed", coldest = 8, warmest = 15),
    Weather("Thu", coldest = 6, warmest = 14),
    Weather("Fri", coldest = 10, warmest = 21),
    Weather("Sat", coldest = 13, warmest = 24),
    Weather("Sun", coldest = 12, warmest = 22)
)

Missing values

Days with no values: Wednesday and Saturday were not measured. The band stops before a day with no values and starts again after it; a day with no neighbour on either side is a line from its lower value to its higher.

PantrixRangeChart(
    data = WEATHER,
    x = { it.day },
    low = { row -> row.coldest.takeIf { row.day !in UNMEASURED } },
    high = { row -> row.warmest.takeIf { row.day !in UNMEASURED } },
    name = "Temperature",
    description = "Temperature by day of the week, with two days not measured",
    tagKey = "missing"
)

private data class Weather(
    val day: String,
    val coldest: Int,
    val warmest: Int
)

private val WEATHER = listOf(
    Weather("Mon", coldest = 9, warmest = 17),
    Weather("Tue", coldest = 11, warmest = 19),
    Weather("Wed", coldest = 8, warmest = 15),
    Weather("Thu", coldest = 6, warmest = 14),
    Weather("Fri", coldest = 10, warmest = 21),
    Weather("Sat", coldest = 13, warmest = 24),
    Weather("Sun", coldest = 12, warmest = 22)
)

private val UNMEASURED = setOf("Wed", "Sat")

API

data

A chart of what lies between two values at each place: the coldest and the warmest of a day, the least and the most a price went to, the hours a shift starts and ends at.

ParameterTypeDefaultWhat it is
dataList<T>the rows.
x(T) -> Anywhere a row stands along the chart: a name, a day, anything that is equal to itself.
low(T) -> Number?a row's lower value, or null where it has none.
high(T) -> Number?a row's higher value, or null where it has none.
nameStringwhat the two values are of: what the tooltip calls them, and a screen reader the chart where it has no description.
modifierModifierModifierthe chart's modifier. A height here is the chart's.
kindRangeKindRangeKind.Areawhether the range is a band from place to place or a bar at each place.
curveChartCurveChartCurve.Linearhow the two lines of a band go from a point to the next. A bar has no line.
linesBooleantruewhether the two lines of a band are drawn, or the tint between them alone.
dashChartDash?nullwhether the two lines are drawn whole, in dashes or in dots. null draws them whole, and dashed where the chart's patterns are on (ChartStyle.patterns).
markerChartMarkerChartMarker.Nonethe mark drawn on each point of the two lines. None by default.
fillAlphaFloat0.16fhow much of the range's colour the tint between the lines has, from none to all of it. Left out, about a sixth.
patternChartPattern?nullthe texture the band or the bars are drawn with besides their colour. null draws none, and where the chart's patterns are on, none too: a chart of one range has nothing to tell it from.
colorColorColor.Unspecifiedthe colour the range is drawn in. Left out, it is the first of the chart's colours.
horizontalBooleanfalsewhether the places go down the chart's side and the range along it, and not along its foot and up.
labelsChartDataLabels?nullthe two values written at the range's ends, the higher over it and the lower under it, where they have room. null writes none.
xAxisChartAxis?ChartAxis()the axis of the places, under the chart, or beside it where it is horizontal. null draws nothing of it.
yAxisChartAxis?ChartAxis()the axis of the values, beside the chart, or under it where it is horizontal. null draws nothing of it.
marksList<ChartMark>emptyList()the lines and the bands drawn in the plot besides the range: a limit, a stretch of places.
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.
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 points at: the chart's own where none is given.
descriptionString?nullwhat the chart shows, in a few words, for a screen reader.
onSelect((ChartPoint) -> Unit)?nullcalled with the range that is pressed.
styleChartStyle?nullthis chart's own geometry.
colorsChartColors?nullthis chart's own colours, where they are not the theme's.
tagKeyString?nullthe chart's automation key. The chart is tagged as the PantrixChart it is, Chart_<tagKey>, and its parts and events are that chart's; its one series has the key range.

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