Classes in depth Chapter 20 of 35

What each data class adds on top of ChartData, from the bar width to the five slots of a combined chart.

What they have in common#

Every chart type has its own data class. Most of them add nothing but the set type they accept, which is what makes the compiler stop you from putting a LineDataSet into a bar chart. Four of them add real behaviour, and two behave differently enough from the base class to be worth reading before you use them.

All of them except PieData take sets the same three ways: an empty constructor, a vararg of sets, and a list. An ArrayList you hand over is kept by reference, any other list is copied. PieData holds a single set, so it takes PieData() or PieData(set).

BarLineScatterCandleBubbleData sits between ChartData and the data classes of the charts that have an x and a y axis. It adds nothing either; it exists so that a combined chart can treat them alike. PieData and RadarData extend ChartData directly.

LineData#

Holds one or more ILineDataSet, one per line. It adds nothing of its own.

chart.data = LineData(revenue, costs)

BarData#

Holds one or more IBarDataSet, and owns the two things that are shared by all bars of a chart.

barWidth is the width of every bar in x value units, not pixels. It defaults to 0.85, which leaves a gap of 0.15 between bars that are one x apart.

chart.data = BarData(set).apply { barWidth = 0.55f }

groupBars(fromX, groupSpace, barSpace) lays the sets out side by side by rewriting the x of their entries. Group i is entry i of every set, so the position in the list decides the grouping and the x values you gave are lost. Groups are laid out for the set with the most entries, starting at fromX. It throws IllegalStateException when the data holds fewer than two sets.

getGroupWidth(groupSpace, barSpace) returns the x range one group takes:

setCount * (barWidth + barSpace) + groupSpace

Use it to set the x axis range so all groups fit:

chart.xAxis.axisMinimum = fromX
chart.xAxis.axisMaximum = fromX + data.getGroupWidth(groupSpace, barSpace) * groupCount

BarData.groupBars recomputes the cached ranges but does not redraw. BarChart.groupBars, which takes the same three arguments, calls it and then notifyDataSetChanged() for you. Prefer the one on the chart unless the bars are inside a combined chart.

Setting data walks through a full grouped example.

ScatterData#

Holds one or more IScatterDataSet and adds one read only property:

val biggest = data.greatestShapeSize

greatestShapeSize is the largest scatterShapeSize in dp over all sets, or 0 when there are none. Nothing in the library reads it; it is there for you to size offsets or a legend form by the biggest shape on screen.

CandleData#

Holds one or more ICandleDataSet. It adds nothing of its own.

BubbleData#

Holds one or more IBubbleDataSet and adds one broadcast setter, in the style of the ones on ChartData:

data.setHighlightCircleWidth(1.5f)

It assigns that stroke width in dp to the highlight circle of every set.

RadarData#

Holds one or more IRadarDataSet, one polygon each. All sets should have the same number of entries, because entry i of every set sits on axis i of the web.

The text around the web comes from the x axis, not from the data. Name the axes with a formatter:

val labels = listOf("Speed", "Power", "Range", "Agility")
chart.xAxis.valueFormatter = IndexAxisValueFormatter(labels)

RadarData also overrides getEntryForHighlight. A radar highlight carries the entry index in Highlight.x, not an x value, so the lookup goes straight to getEntryForIndex. It returns null when the set index is out of range, and throws IndexOutOfBoundsException when the entry index is.

PieData#

The odd one out: a pie holds exactly one data set, whose entries are the slices.

val data = PieData(set)
data.dataSet = anotherSet     // replaces the set and recomputes the ranges

yValueSum is the total of all slice values, the whole pie. A percent formatter divides by it.

val share = entry.y / data.yValueSum

Because there is always exactly one set, the lookups are narrowed:

  • getDataSetByIndex(0) returns the set, and any other index returns null.
  • getDataSetByLabel returns the set if its label matches, and null if not.
  • getEntryForHighlight reads Highlight.x as the slice index, like the radar chart does.

The dataSet getter, getDataSetByLabel, getEntryForHighlight, yValueSum and getDataSetByIndex(0) all throw IndexOutOfBoundsException while no set has been assigned. Only the empty constructor can leave it in that state, so assign dataSet right after using it.

The legend of a pie chart is built from the entry labels. A set label that is not blank is appended after them as an entry without a form, so give the set a blank label to keep it out of the legend.

CombinedData#

Holds up to one of each of the other five data objects and draws them together in a CombinedChart.

chart.data = CombinedData().apply {
    barData = BarData(barSet).apply { barWidth = 0.5f }
    lineData = LineData(lineSet)
}
SlotType
lineDataLineData
barDataBarData
scatterDataScatterData
candleDataCandleData
bubbleDataBubbleData

Each slot is null until you fill it, and assigning one recomputes every range.

allData is the slots that are filled, always in the order line, bar, scatter, candle, bubble, whatever order you assigned them in. A position in that list is what Highlight.dataIndex means, and what getDataByIndex(index) takes. That index is the one thing a combined chart adds to highlighting, because a set index alone is no longer unique.

chart.onValueSelected { _, highlight ->
    val data = chart.data ?: return@onValueSelected
    val part = data.getDataByIndex(highlight.dataIndex)
    val set = data.getDataSetByHighlight(highlight)
}

getDataByIndex throws IndexOutOfBoundsException for an index that is not below the size of allData. getDataSetByHighlight and getEntryForHighlight check both indices and return null instead. chart.highlightValue(x, dataSetIndex) leaves dataIndex at -1, so pass the dataIndex when you highlight on a combined chart from code. getDataIndex(data) gives you the position of a child data object, or -1.

Note that the draw order on screen is not allData order. It comes from chart.drawOrder, which defaults to bar, bubble, line, candle, scatter.

What is not supported#

The inherited dataSets list is rebuilt from the children on every recalculation, so anything you put there directly is dropped on the next notifyDataChanged. Work through the child data objects instead.

CallBehaviour
addDataSetAdds to dataSets, which is then discarded. Add to a child instead
removeDataSet(index)Logs an error and returns false
removeEntry(entry, index)Logs an error and returns false
removeEntry(xValue, index)Logs an error and returns false

removeDataSet(set) is the one removal that does work. It finds the first child data object holding that set and removes it there, but it only recomputes that child's ranges. Call notifyDataChanged() afterwards, or chart.notifyDataSetChanged(), which covers it.

Where to go next#