Styling Chapter 11 of 35

How the chart builds its legend from the data sets, how to place and style it, and how to replace the computed entries with your own.

Every chart has one, reached as chart.legend, and it is drawn unless you turn it off.

chart.legend.isEnabled = false

How the legend is built#

The legend is recomputed from the data every time the chart data changes, so you never fill it yourself unless you want to. One entry is created per color of a data set, and the entry labels come from the data set labels.

  • A data set with one color gives one entry carrying the data set label.
  • A data set with several colors gives one entry per color, capped at the number of entries. Only the last of them carries the label, the earlier ones have no label and are stacked next to it as a row of forms.
  • A stacked BarDataSet gives one entry per stack color, labelled from set.stackLabels, followed by a label only entry for the data set itself.
  • A PieDataSet gives one entry per slice, labelled from each PieEntry, followed by a label only entry for the data set when it has a label.
  • A CandleDataSet with a decreasing color gives two entries, one in the decreasing and one in the increasing color, sharing the data set label.

An entry with no label draws only its form and sits next to the following entry, separated by stackSpace. That is how a multi color data set ends up as several forms in front of one label.

Placement#

The position is the combination of a horizontal alignment, a vertical alignment and an orientation.

chart.legend.apply {
    verticalAlignment = Legend.LegendVerticalAlignment.TOP
    horizontalAlignment = Legend.LegendHorizontalAlignment.RIGHT
    orientation = Legend.LegendOrientation.VERTICAL
    isDrawInsideEnabled = false
}
PropertyValuesDefault
horizontalAlignmentLEFT, CENTER, RIGHTLEFT
verticalAlignmentTOP, CENTER, BOTTOMBOTTOM
orientationHORIZONTAL for rows, VERTICAL for one columnHORIZONTAL
isDrawInsideEnabledDraw the legend over the content area instead of reserving space for itfalse
directionLEFT_TO_RIGHT or RIGHT_TO_LEFTLEFT_TO_RIGHT

isDrawInsideEnabled is the difference between the chart shrinking to make room and the legend floating on top of the data.

direction flips an entry around: RIGHT_TO_LEFT draws the label first and the form after it, and lays the entries out towards the left. Use it for right to left locales.

The legend is inset from its corner by xOffset and yOffset, both in dp. The legend starts at 5 and 3.

Forms#

The form is the small shape drawn next to a label in the color of what it describes.

chart.legend.apply {
    form = Legend.LegendForm.LINE
    formSize = 10f
    formLineWidth = 2f
    formLineDashEffect = DashPathEffect(floatArrayOf(10f, 5f), 0f)
}
FormResult
SQUAREA filled square
CIRCLEA filled circle
LINEA horizontal line using formLineWidth and formLineDashEffect
EMPTYNothing is drawn, but the space is kept
NONENothing is drawn and no space is kept
DEFAULTOn an entry, use the legend's form. On the legend itself, a circle

formSize and formLineWidth are in dp and default to 8 and 3. A data set can override the form for its own entries through set.form, set.formSize, set.formLineWidth and set.formLineDashEffect.

Spacing and text#

chart.legend.apply {
    xEntrySpace = 7f
    yEntrySpace = 5f
    formToTextSpace = 5f
    stackSpace = 3f
    textColor = Color.WHITE
    textSize = 12f
    typeface = tfLight
}
PropertyMeaningDefault
xEntrySpaceSpace in dp between two entries in a row6
yEntrySpaceSpace in dp between two entries in a column, or between wrapped rows0
formToTextSpaceSpace in dp between a form and its label5
stackSpaceSpace in dp between stacked forms, that is entries without a label3
textSizeLabel size in dp, clamped to 6 until 2410
textColorLabel colorblack
typefaceLabel typeface, or null for the defaultnull

Those three text properties are copied into chart.legendLabelPaint before every draw, so set them here rather than on the paint.

Word wrap and maximum size#

A horizontal legend with many entries would otherwise run off the side of the chart.

chart.legend.isWordWrapEnabled = true
chart.legend.maxSizePercent = 0.7f

isWordWrapEnabled lets a horizontal legend break into several rows. It costs drawing time and does nothing for a vertical legend.

maxSizePercent is the largest share of the chart the legend may take, between 0 and 1, and defaults to 0.95. For a vertical legend at the side it limits the width, for a horizontal one the height, and a wrapping horizontal legend breaks its rows at that share of the content width.

Custom entries#

Assign entries to take over the legend completely. That marks it custom, so the chart stops recomputing it from the data sets.

val square = Legend.LegendForm.SQUARE

chart.legend.entries = listOf(
    LegendEntry("Below", square, 10f, Float.NaN, null, Color.RED),
    LegendEntry("On target", square, 10f, Float.NaN, null, Color.GREEN),
)
chart.notifyDataSetChanged()

Every LegendEntry argument has a default, so named arguments read better when you only need a few:

LegendEntry(label = "Forecast", form = Legend.LegendForm.LINE, formColor = Color.BLUE)
ArgumentMeaningDefault
labelThe text, or null to draw only the form and stack it with the next entrynull
formThe shape; DEFAULT uses the legend's formDEFAULT
formSizeSize in dp, or Float.NaN for the legend's formSizeNaN
formLineWidthLine width in dp, or Float.NaN for the legend's formLineWidthNaN
formLineDashEffectDash pattern, or null for the legend'snull
formColorColor of the formColorTemplate.COLOR_NONE
The default formColor draws nothing, so always pass a color for an entry that should show a form.

chart.legend.isLegendCustom tells you whether the current entries were assigned or computed. The chart owns its Legend, so a custom legend is always made by assigning chart.legend.entries.

To go back to the automatic legend, call resetCustom() and let the chart recompute:

chart.legend.resetCustom()
chart.notifyDataSetChanged()
In version 3.x this was setCustom(colors, labels) with two parallel arrays. Assigning entries replaces it, and one LegendEntry now carries the form, its size and its dash effect as well.

Extra entries#

Extra entries are appended after the computed ones, which is what you want for a threshold line or a note that is not a data set.

chart.legend.setExtra(
    colors = listOf(Color.RED, ColorTemplate.COLOR_NONE),
    labels = listOf("Target", "measured weekly"),
)
chart.notifyDataSetChanged()

setExtra pairs the two lists by index and drops the surplus of the longer one. Two sentinel colors change the form instead of coloring it:

ColorResult
ColorTemplate.COLOR_SKIP or 0Form NONE, so no form and no space for one
ColorTemplate.COLOR_NONEForm EMPTY, so no form but the space is kept, which lines the label up with the others

You can also assign extraEntries directly with fully built LegendEntry objects. Extra entries are appended the next time the legend is computed, so call notifyDataSetChanged() when the chart already has data, and note that a custom legend ignores them.

The computed sizes#

After the legend has been measured, four properties describe the result. They are read only, filled during the measurement, and in pixels.

PropertyMeaning
neededWidthTotal width the legend takes, xOffset included
neededHeightTotal height the legend takes, yOffset included
textWidthMaxWidth of the widest entry, form and spacing included
textHeightMaxHeight of the tallest label

They are what the chart uses to reserve space, so they are useful when you place something next to the legend yourself. calculatedLabelSizes, calculatedLineSizes and calculatedLabelBreakPoints hold the per entry and per row layout of a horizontal legend, for the same reason.

These were plain getters in version 3.x. They are now properties the library sets, so assigning them is not possible and was never useful.