Vico

2.8. CartesianValueFormatter

2.8.1. Overview

x and y values are numerical. You can use CartesianValueFormatter to format them for display. They can remain numbers, or they can be transformed to dates, category names, and so on.

There are two CartesianValueFormatter factory functions: CartesianValueFormatter.decimal and CartesianValueFormatter.yPercent. For more complex use cases, create custom implementations can be created. CartesianValueFormatters are most commonly used with HorizontalAxis and VerticalAxis—see the valueFormatter parameters and properties. However, these aren’t the only APIs that accept CartesianValueFormatter instances.

When the values remain numerical, formatting is straightforward. DecimalFormat, which is used by CartesianValueFormatter.decimal, is typically involved. Thus, on this page, we focus on formatting with nonnumerical results. The aim in such cases is to find a predictable mapping that doesn’t involve overly large values (as these can lead to precision loss). The optimal approach depends on the use case. Some common situations are discussed below.

2.8.2. Categories

A chart’s domain can be a list of categories. An easy way to implement this pattern is to use x values that serve as indices. As previously discussed, the series-creating functions have overloads that add such x values automatically.

val data = mapOf("A" to 8f, "B" to 4f, "C" to 6f)
val labelListKey = ExtraStore.Key<List<String>>()
cartesianChartModelProducer.runTransaction {
    columnSeries { series(data.values) }
    updateExtras { it[labelListKey] = data.keys.toList() }
}
CartesianValueFormatter { context, x, _ ->
    context.model.extraStore[labelListKey][x.toInt()]
}

2.8.3. Dates

Another common use case is mapping dates to y values.

The dates will be spaced out proportionally. If you need nonproportional spacing, use the approach from 2.8.2. This is also worth considering if there are no gaps in your data, in which case there’s no distinction between proportional and nonproportional spacing—the category approach will be simpler.

Different solutions are required depending on the date precision and range. For days, a straightforward mapping can be used:

val data =
    mapOf(
        LocalDate.parse("2022-07-01") to 2f,
        LocalDate.parse("2022-07-02") to 6f,
        LocalDate.parse("2022-07-04") to 4f,
    )
val xToDateMapKey = ExtraStore.Key<Map<Float, LocalDate>>()
val xToDates = data.keys.associateBy { it.toEpochDay().toFloat() }
cartesianChartModelProducer.runTransaction {
    lineSeries { series(xToDates.keys, data.values) }
    updateExtras { it[xToDateMapKey] = xToDates }
}
val dateTimeFormatter = DateTimeFormatter.ofPattern("d MMM")
CartesianValueFormatter { context, x, _ ->
    (context.model.extraStore[xToDateMapKey][x] ?: LocalDate.ofEpochDay(x.toLong()))
        .format(dateTimeFormatter)
}

However, if greater precision is required, the data must be further transformed. If the desired precision level is one second, for example, then using raw second timestamps will produce overly large x values. In such cases, transformations like these can be used:

The first transformation is typically sufficient.

Before applying any transformations, double-check that you’ve correctly identified the required precision level. In particular, use millisecond timestamps only if precision down to the millisecond is required, which is rarely the case. Screens have a finite number of pixels, and it’s redundant to introduce a precision level that exceeds what the screen will, realistically, be able to represent.