# Troubleshooting

Concrete symptoms you may hit with a chart, what causes each one in the library, and what to change.

## Nothing is drawn

### The chart shows "No data yet"

That is the empty state. It appears while `chart.data` is null or holds no entries (`chart.isEmpty`), and `Chart.onDraw` draws it instead of anything else. So the chart never received data, the data sets you gave it are empty, or something called `chart.clear()`. [The empty state](https://philjay.cc/mpandroidchart/docs/general-styling/#the-empty-state) shows how to change its text, icon and colors, and how `isLoading` turns it into a loading state.

### The chart is blank

An empty `noDataText` together with `isNoDataIconEnabled = false` draws nothing at all while there is no data. Otherwise a blank chart has data whose values are all outside the visible range; check the axis minimum and maximum and the viewport.

### The chart is a thin strip

`Chart.onMeasure` resolves to 50 dp when the parent does not impose a height, so `android:layout_height="wrap_content"` gives you a 50 dp chart. Give the chart a real height, or a weight inside a `LinearLayout`. In Compose the same applies to a `Modifier` without a height:

```kotlin
LineChart(
    data = lineData,
    modifier = Modifier.fillMaxWidth().height(220.dp),
)
```

### One data set is missing while the others draw

`isVisible` makes the renderers skip the set entirely. When only parts are missing, look at `isDrawValuesEnabled` for the value labels and, on a line set, `isDrawCirclesEnabled` for the points. All three default to true.

### Viewport calls seem to be ignored

`moveViewToX`, `centerViewTo` and the animated variants go through `addViewportJob`, which posts the job right away only when `viewPortHandler.hasChartDimens()` is true. Before the first layout they are parked in `chart.jobs` and run in `onSizeChanged`, so they do take effect, one frame later.

`setVisibleXRangeMaximum` and `setVisibleXRangeMinimum` are different: they divide `xAxis.axisRange` by your span immediately. Call them after the data is set, or the range is still the default and the limit is wrong.

## The values look wrong or jump around

### Points connect in the wrong order

A `DataSet` requires its entries sorted by x ascending. `getEntryIndex(xValue, closestToY, rounding)` is a binary search, and every x lookup, every highlight and every visible-range calculation goes through it. Unsorted entries do not throw; they return a neighbour that happens to be nearby, so the line zig-zags and taps land on the wrong point.

Sort before building the set, and insert in order afterwards:

```kotlin
val entries = readEntries().sortedWith(EntryXComparator())
val set = LineDataSet(entries, "Revenue")

set.addEntryOrdered(Entry(x, y))
```

`addEntryOrdered` appends when the new x is not smaller than the last entry's x, and otherwise inserts before the first entry with a larger x. `addEntry` always appends without checking, which is what breaks the order.

> A `DataSet` keeps an `ArrayList` you pass by reference and copies any other list. Sorting the `ArrayList` you handed over therefore also sorts the data set.

### Value labels disappear as the data grows

On the charts with axes a data set draws its labels only while at most `chart.maxVisibleCount` of its entries are in the visible x range. The default is 100, so the labels of a set go away once more than 100 of its entries are on screen and come back as you zoom in. Raise it if you really want them.

### NaN or infinite values

A `Float.NaN` or infinite y value does not hang the chart or blank it. Infinite values are left out of the axis ranges, like NaN, and a line draws a gap where such a value sits instead of dropping the whole line. An entry with a NaN x is never found by a lookup. If a series has gaps you did not expect, look for values like these in your data.

## Changes to the data do not show

### One call is enough

After changing entries or sets in place:

```kotlin
chart.notifyDataSetChanged()
```

That recomputes the data ranges, rebuilds the renderer buffers, recomputes both y axes and the x axis, rebuilds the legend, recalculates the offsets and redraws. It calls `data.notifyDataChanged()` for you, so there is nothing else to call and no separate `invalidate()`.

The one case that needs more is changing a set's `entries` list in place rather than through `addEntry` or `removeEntry`. `ChartData.calcMinMax` reads each set's cached range instead of rescanning its entries, so the set has to be told first:

```kotlin
set.entries.add(Entry(x, y))
set.notifyDataSetChanged()
chart.notifyDataSetChanged()
```

Assigning `set.entries = newEntries` does not need that, because the setter recomputes the range.

### In Compose the chart ignores the new data

`ChartView` applies the data with `if (chart.data !== data) chart.data = data`. That is an identity check. Recomposing with the **same** `LineData` instance, however much you changed inside it, assigns nothing and redraws nothing.

Either hand the composable a new instance, or keep the instance and say so:

```kotlin
state.notifyDataChanged()
```

That calls `notifyDataSetChanged()` on the attached chart and refreshes the state properties. See [Jetpack Compose](https://philjay.cc/mpandroidchart/docs/compose/).

## The x axis shows 0, 1, 2 instead of my labels

x values are values, not indices. An entry at `Entry(0f, 42f)` sits at x = 0, and the axis prints that number unless a formatter turns it into something else.

```kotlin
chart.xAxis.apply {
    valueFormatter = IndexAxisValueFormatter(months)
    granularity = 1f
    labelCount = months.size
}
```

### Labels disappear or land on the wrong tick

`IndexAxisValueFormatter.getFormattedValue` rounds the value to an index and then rejects it when `value.roundToInt()` differs from `value.toInt()`. In practice: a fraction below 0.5 gives you the label of the truncated index, and a fraction of 0.5 or above gives an empty string.

Without granularity the axis is free to pick an interval smaller than 1 as you zoom in, so label values become 2.4 and 2.6. The first prints "Mar" at a tick that is not March, the second prints nothing. Setting `granularity = 1f` keeps the interval at 1 or above and also sets `isGranularityEnabled` to true, so label values stay whole. More in [the x axis](https://philjay.cc/mpandroidchart/docs/xaxis/).

## Labels and data are cut off at the edges

### The first and last label overlap the view edge

The x axis renderer skips labels whose pixel position falls outside the content area, and draws the rest centered on their tick, so half of the outermost label hangs over the edge.

```kotlin
chart.xAxis.isAvoidFirstLastClippingEnabled = true
```

That shifts the first label right by half its width, and the last label left by half its width, but only when the last label is wider than twice the right offset and would really spill past the view. It is off by default.

### The line or the outer bars are clipped

Extra space around the content area is the direct fix. `chart.setExtraOffsets(6f, 8f, 6f, 4f)` takes dp and applies on the next offset calculation.

Padding in value space is the other option. `axisMinimum` and `axisMaximum` fix the range; `spaceMin` and `spaceMax` are subtracted from and added to the computed range instead, so they survive new data. `BarChart`, `ScatterChart` and `CandleStickChart` already set both to 0.5 on the x axis, which is why bar charts leave half a slot at each side and line charts do not.

### There is a "Description Label" in the corner

`Description.text` defaults to that string and the component is enabled, so every chart draws it at the bottom right until you turn it off with `chart.description.isEnabled = false`. See [the description](https://philjay.cc/mpandroidchart/docs/description/).

## The chart is drawn under the status bar or the action bar

This is not the library. From `targetSdk` 35 the window is edge to edge and your app receives the insets, so a chart that fills the screen slides under the system bars.

The example app pads the root view of every activity. `DemoBase.setContentView` calls one extension on the view it just set, and `MainActivity` does the same after inflating its binding:

```kotlin
fun View.padForWindowInsets() {
    ViewCompat.setOnApplyWindowInsetsListener(this) { view, insets ->
        val bars = insets.getInsets(
            WindowInsetsCompat.Type.systemBars() or
                WindowInsetsCompat.Type.displayCutout(),
        )
        view.setPadding(bars.left, bars.top, bars.right, bars.bottom)
        WindowInsetsCompat.CONSUMED
    }
}
```

Returning `CONSUMED` stops the insets from being dispatched again to children, so nothing is padded twice. In Compose, put the same padding on the container with `Modifier.windowInsetsPadding(WindowInsets.systemBars)` rather than on the chart.

## Gestures do not work

### Nothing reacts to touch at all

`BarLineChartBase.onTouchEvent` returns false when `chartTouchListener` is not built yet, when `data` is null, or when `isTouchEnabled` is false. No data means no gestures at all, which is easy to miss while you are still loading.

If taps still highlight but pan and zoom do nothing, the touch listener is bailing out: it stops handling the event when `isDragEnabled` is false **and** both `isScaleXEnabled` and `isScaleYEnabled` are false. The tap detector runs before that check, which is why the highlight survives.

Pie and radar charts only check `isTouchEnabled`, and their rotation has its own switch, `isRotationEnabled`.

### The chart fights a scrolling parent

The chart already calls `disableScroll()` when a drag or a zoom begins and `enableScroll()` on `ACTION_UP`, which is `requestDisallowInterceptTouchEvent` on the parent. When your own touch handling gets in the way, call the same pair yourself. A vertical parent and a horizontal chart usually agree once you turn the conflicting axis off with `chart.isDragYEnabled = false`. [Interaction](https://philjay.cc/mpandroidchart/docs/interaction/) and [charts in lists](https://philjay.cc/mpandroidchart/docs/lists-and-scrolling/) cover the rest.

### A small drag does nothing

A touch becomes a drag only once the finger has moved further than `dragTriggerDist`, which the charts create their listener with at 3 dp. Raise it when a chart inside a scrolling parent steals taps:

```kotlin
val listener = chart.chartTouchListener as BarLineChartTouchListener
listener.dragTriggerDist = 12f
```

## Highlighting selects the wrong thing, or nothing

### Nothing is selected

Four things have to line up. `chart.isHighlightPerTapEnabled` must be true, the data set must have `isHighlightEnabled` true, the touch must be within `chart.maxHighlightDistance` of a value, and `data.getEntryForHighlight` has to find the entry. The distance is measured in dp and defaults to 500, which is generous; lower it when a tap in empty space should select nothing.

### A tap selects a neighbour

The highlighter converts the touch to an x value, collects the closest entry in every set, prefers the y axis whose candidates sit nearer to the touch, and then takes the smallest pixel distance. On a bar chart only the horizontal distance counts, so a tap anywhere above or below a bar selects it.

Unsorted entries break this, because the closest-entry step is the same binary search as above.

### A drag pans instead of moving the highlight

`isHighlightPerDragEnabled` is true by default, but the touch listener only takes that branch when the chart cannot pan: dragging is off, or `chart.isFullyZoomedOut` and `hasNoDragOffset` are both true. Calling `setVisibleXRangeMaximum` sets a minimum x scale above 1, and `isFullyZoomedOutX` is false whenever the minimum scale is above 1. So a chart with a limited visible range pans and never highlights by drag, unless you set `isDragEnabled = false`.

### A drag does not move the highlight at all

A highlight drag starts only once the finger has moved past the system touch slop, and only for a gesture that runs mostly along the x axis, up and down on a `HorizontalBarChart`. A swipe across that axis is left to a scrolling parent, so a list scrolls instead of changing the selection.

### A stacked bar selects the wrong part

`BarHighlighter` looks at the touched y value, finds the stack range that contains it and stores that position in `Highlight.stackIndex`. To select the whole bar instead, set `chart.isHighlightFullBarEnabled = true`; `getHighlightByTouchPoint` then rebuilds the highlight with a stack index of -1. It defaults to false on `BarChart` and true on `CombinedChart`.

### A combined chart highlights nothing from code

`CombinedData.getEntryForHighlight` first looks up the child data object at `Highlight.dataIndex`, and returns null when that index is outside `allData`. `dataIndex` defaults to -1, so the usual call selects nothing:

```kotlin
chart.highlightValue(x = 3f, dataSetIndex = 0, dataIndex = 1)
```

`allData` is ordered line, bar, scatter, candle, bubble, skipping the kinds you did not set, and that position is the index you pass.

## Markers do not appear

`Chart.drawMarkers` returns early unless all three hold: `chart.marker` is not null, `chart.isDrawMarkersEnabled` is true, and `valuesToHighlight()` is true. Per highlight it also skips entries the running `animateX` has not reached yet, and any position outside the content area, so a marker for a value scrolled out of view is not drawn.

A `MarkerView` has an offset of (0, 0), which puts its top left corner on the value, so it looks missing under your finger. Shift it with `setOffset(-width / 2f, -height.toFloat())` to sit above the point. If you override `refreshContent`, call `super.refreshContent(e, highlight)` so the view is measured again.

A Compose marker appears one frame after the highlight. `ComposeMarker.draw` skips the frame while the content for a newly highlighted entry has not composed yet; the `SideEffect` in its `ComposeView` then invalidates the chart and the next frame draws it. More in [markers](https://philjay.cc/mpandroidchart/docs/markers/).

A highlight that points past the entries, for example one kept from data that had more entries, draws no indicator and no marker. Setting new data keeps the current highlight, so clear or set it when the data changes meaning.

## The release build differs from the debug build

Start by ruling R8 out: build the release variant once with `isMinifyEnabled = false`. If the problem stays, it is not minification. The library needs no keep rules of its own. It does no reflection: every animation runs through a `ValueAnimator` and an update listener, and a chart named in a layout file is kept by the rules AAPT2 writes for every view in a layout. The causes that are left are in your code: a drawable resolved by name that resource shrinking removed, or another library reflecting on your entry payload. [R8 and ProGuard](https://philjay.cc/mpandroidchart/docs/proguard/) works through each of them.

## Exceptions the library throws

They are few, and each points at one mistake:

- `IllegalStateException` from `groupBars`: no data on the chart yet, or fewer than two bar data sets to group.
- `IllegalArgumentException` from `animateValue`: the entry is not in the chart data, or it is a stacked bar or a candle. Use `animateDataChange` for those.
- `IllegalArgumentException` from `ObjectPool.recycle`: the same pooled instance recycled twice.
- `ParcelFormatException` from `Entry.writeToParcel`: the entry payload is not `Parcelable`.

[Miscellaneous](https://philjay.cc/mpandroidchart/docs/miscellaneous/) has the full table with the throwing class for each.

Before 4.0.1, building a chart on a thread without a `Looper`, for example with an asynchronous layout inflater, crashed. Since 4.0.1 that works, because the touch handling is set up on the first touch.

## Getting a useful report out of the library

Every chart can narrate what it does under the logcat tag `MPAndroidChart`:

```kotlin
chart.isLogEnabled = true
```

It reports `Chart.init()`, `Data is set.`, `OnSizeChanged()` with the dimensions it accepted or refused, `Preparing...` or `Preparing... DATA NOT SET.` on each `notifyDataSetChanged`, the x axis range each time the value to pixel matrix is prepared, the computed offsets with the resulting content rectangle, and the highlight on every selection. That sequence answers most of the symptoms above: if you never see `Setting chart dimens`, the chart has no size; if you see `DATA NOT SET`, the data never arrived.

Line, bar, scatter, candle, bubble and combined charts add a timing line per frame:

```text
Drawtime: 6 ms, average: 7 ms, cycles: 42
```

`chart.resetTracking()` zeroes the total and the cycle count, which is what you want right before measuring one specific interaction rather than the whole session. Logging costs time on every draw, so keep it off in release builds. [Performance](https://philjay.cc/mpandroidchart/docs/performance/) uses these numbers.
