Classes in depth Chapter 23 of 35

The object behind every chart that knows the chart size, the rectangle the data is drawn in, and the zoom and drag state, with the limits that keep all three sane.

Every chart owns one ViewPortHandler. The chart functions described in modifying the viewport are a safe layer on top of it: they work in values, take care of the limits and redraw afterwards. The handler itself works in pixels and matrices, and is what you read when you need to know exactly where something is on screen.

val handler = chart.viewPortHandler

The property is read only. A chart creates its handler in init and keeps it for its lifetime, so you query it and set limits on it, but you never replace it.

The chart size and the content rectangle#

Two different rectangles matter.

The chart size is the view: chartWidth and chartHeight in pixels, set by the chart from onSizeChanged.

The content rectangle is the part of it the data is actually drawn in, the chart size minus the space taken by the axis labels, the legend, the extra offsets and minOffset. Everything the renderers clip to is this rectangle.

PropertyMeaning
chartWidth, chartHeightsize of the whole view in pixels
contentRectthe drawing area as a RectF
contentLeft, contentTop, contentRight, contentBottomits edges in pixels
contentWidth, contentHeightits size
contentCenterits center as an MPPointF
offsetLeft, offsetTop, offsetRight, offsetBottomthe gap between each view edge and the rectangle
smallestContentExtensionthe smaller of content width and height
hasChartDimens()true once the view has a non zero size

hasChartDimens() is the check the chart itself uses to decide whether a viewport call can run now or has to wait for the first layout.

The touch matrix#

matrixTouch is a single android.graphics.Matrix holding the current zoom and scroll. Its scale and translation are mirrored into four read only properties, which is what you normally read instead of picking the matrix apart:

PropertyMeaning
scaleX, scaleYcurrent zoom factor per axis, 1 when fully zoomed out
transX, transYcurrent scroll offset in pixels

The functions that change the viewport do not change the matrix in place. They build a new matrix from matrixTouch with the change applied and hand it back, and refresh is what makes it current:

val center = handler.contentCenter
val next = handler.zoomIn(center.x, center.y)
handler.refresh(next, chart, true)   // clamps to the limits, then redraws

refresh copies the matrix into matrixTouch, clamps it there with limitTransAndScale and writes the clamped values back into the matrix you passed, so a matrix that would put the data outside its bounds is corrected rather than accepted. Most of these also come in an outputMatrix form that writes into a matrix you own instead of allocating one, which is what the charts use in their draw path. setZoom(scaleX, scaleY, x, y) is the one that does not.

zoomIn, zoomOut, zoom, setZoom, fitScreen and translate all follow this build then refresh shape. centerViewPort(pts, view) is the exception: it translates, refreshes and redraws in one call.

Scale and translation limits#

The limits are the reason the chart cannot be dragged into empty space.

PropertyDefaultMeaning
minScaleX, minScaleY1how far out the chart may zoom
maxScaleX, maxScaleYFloat.MAX_VALUEhow far in the chart may zoom
dragOffsetX, dragOffsetY0extra distance in dp the content may be dragged past its bounds

The scale limits are set through six functions, each of which clamps the current matrix right away:

handler.setMinimumScaleX(2f)     // values below 1 are raised to 1
handler.setMaximumScaleX(0f)     // 0 removes the limit
handler.setMinMaxScaleX(2f, 8f)
handler.setMinimumScaleY(2f)
handler.setMaximumScaleY(10f)
handler.setMinMaxScaleY(2f, 10f)

These are exactly what chart.setVisibleXRangeMaximum and its siblings call after converting a span of values into a scale factor, so prefer the chart functions unless you already think in factors.

The translation limit follows from the scale: the content may be moved between -contentWidth * (scaleX - 1) and 0 horizontally, widened on both sides by dragOffsetX. hasNoDragOffset is true while neither offset is set.

Four properties tell you whether there is room left to move:

handler.canZoomInMoreX    // scaleX < maxScaleX
handler.canZoomOutMoreX   // scaleX > minScaleX
handler.canZoomInMoreY
handler.canZoomOutMoreY

isFullyZoomedOut, with its isFullyZoomedOutX and isFullyZoomedOutY halves, is true only when the chart sits at scale 1 and no minimum above 1 is set.

Bounds checks#

Renderers use these to skip anything that would be drawn outside the content rectangle. All of them take pixels.

CallTrue when
isInBounds(x, y)the point is inside the content rectangle
isInBoundsX(x)the x is inside it horizontally
isInBoundsY(y)the y is inside it vertically
isInBoundsLeft(x)the x is not more than 1 pixel left of the rectangle
isInBoundsRight(x)the x is not more than 1 pixel right of it
isInBoundsTop(y)the y is not above it
isInBoundsBottom(y)the y is not below it

The one pixel tolerance on the left and right sides keeps a point that lands exactly on the edge from being dropped by a rounding error. A custom renderer should use the same checks so its drawing clips like the built in ones.

Turning values into pixels#

The handler works in pixels, and your data is in values. The bridge is Transformer, one per y axis:

val transformer = chart.getTransformer(YAxis.AxisDependency.LEFT)

val pixel = transformer.getPixelForValues(10f, 50f)    // value to pixel
val value = transformer.getValuesByTouchPoint(x, y)    // pixel to value

A transformer applies three matrices in order: a value matrix that scales values to unzoomed pixel distances, the handler's matrixTouch with the current zoom and drag, and an offset matrix that moves the result into the content rectangle and flips the y direction. valueToPixelMatrix is the three combined and pixelToValueMatrix its inverse. Both return a buffer that is overwritten on the next access, so read from them and do not keep the reference.

For single points there are shortcuts on the chart itself, which pick the right transformer for you:

chart.getPixelForValues(10f, 50f, YAxis.AxisDependency.LEFT)
chart.getValuesByTouchPoint(x, y, YAxis.AxisDependency.LEFT)
chart.getPosition(entry, YAxis.AxisDependency.LEFT)
A HorizontalBarChart installs a HorizontalViewPortHandler, which behaves exactly like the base class. What differs is the chart above it: its setVisibleXRange functions set the y scale limits and the other way round, because its axes are swapped on screen.

Where to go next#