Building an Android chart library for data visualisation
A reusable chart component turns rows of data into patterns that users can interpret quickly. Instead of scattering drawing code across activities or composables, teams can package the rendering, interaction and accessibility behaviour in one module. That foundation makes charts easier to test, theme and maintain when product screens change.
A sensible first release covers line, bar and pie charts, handles empty or sparse datasets, and works in both classic Android views and Jetpack Compose. Keep the core independent of any screen: it accepts plain data, draws with platform graphics APIs and exposes only the events a host screen needs. This structure supports beginners learning Canvas while leaving room for advanced extensions.
Begin with a concrete Australian example instead of a generic demo. A Brisbane coffee wholesaler might compare sales across Sydney, Melbourne and Adelaide during Australia’s July-to-June financial year. Amounts would be in AUD, with the 10% GST calculated outside the chart. These choices make missing units and ambiguous gross-versus-net values much easier to spot.
The component must also cope with local conditions. Regional mobile coverage can disappear along long drives, so bundled sample data and cached results are more useful than a blank error screen. Delivery information should show kilometres rather than miles. A dependable first milestone is a narrow feature set with clear rendering rules, not an enormous catalogue of chart styles.
Define a small, stable chart API
Separate chart data from Android types. A model can contain a title, unit, categories and a list of series, with each series holding labelled numeric values. Data classes make missing observations explicit, while a formatter handles currency and percentages. Keep Paint objects and View references out of the public model, because they belong to the rendering layer.
A small Kotlin contract could look like this:
enum class ChartType { LINE, BAR, PIE }
data class ChartSeries(
val name: String,
val values: List<Double?>
)
data class ChartData(
val type: ChartType,
val title: String,
val unit: String,
val categories: List<String>,
val series: List<ChartSeries>
)
Expose Loading, Ready, Empty and Error states to the host, or provide equivalent flags. A chart should not crash because a category has no value or a series contains fewer values than the category list. Normalise those inputs at the boundary, then let the renderer work with a consistent scale and zero baseline.
Draw axes, series and labels with Canvas
In a custom View, onDraw() should paint the background, plot area, axes, series and labels in a deliberate order. Calculate the plot rectangle from the available width and height, honour system insets and avoid fixed pixel positions. Use density-independent pixels for dimensions and scaled pixels for text so charts remain legible across screen densities.
Keep layout calculations separate from drawing methods. A layout pass can calculate axis bounds, tick positions and bar rectangles; the draw pass then uses those results. This makes geometry testable without a device and prevents a long label from pushing the plot outside the view. Bracket transformations with Canvas.save() and Canvas.restore().
Treat colour as a theme input, not a fixed constant. Pair it with patterns, marker shapes or line styles so each series remains distinguishable in monochrome and for viewers with colour-vision deficiencies. Labels need breathing room: rotate crowded names, shorten them through a formatter or switch to horizontal bars. Keep the legend synchronised with the visible series.
Make charts interactive and accessible
Touch handling is where a basic chart becomes useful. Use GestureDetector to separate taps from drags and ScaleGestureDetector for pinch-to-zoom. Pan within the data bounds, and let a double tap reset the viewport. On a scrolling screen, begin with an appropriate touch slop and release the gesture when it belongs to the parent, avoiding a chart that traps every vertical swipe.
Expose meaning as well as pixels. A concise contentDescription can announce the title, selected period and overall trend, but interactive charts also need per-point or per-bar focus for screen-reader users. ExploreByTouchHelper supplies the virtual views needed for that exploration. Put the spoken value in context, including its unit.
Use motion sparingly. Animate only the transition between data sets, keep durations short and skip the animation when system animations are disabled. A tooltip can show the touched value, while spoken feedback identifies the selected point without repeating the entire chart. This is especially helpful in delivery, warehouse and retail interfaces, where users often glance at a screen briefly.
Compare implementation approaches
A custom library offers control, but it also means maintaining touch handling, text measurement, accessibility and export features. An established dependency can shorten the first release considerably. Decide whether the goal is a small internal component, a product feature or a broadly reusable package, then budget for testing and long-term maintenance rather than comparing lines of code.
A practical decision matrix:
| Approach | Strengths | Trade-offs | Best fit |
|---|---|---|---|
| Custom Canvas view | Full styling control and no chart dependency | More work for gestures and accessibility | Products needing exact behaviour |
| Jetpack Compose Canvas | Direct integration with Compose state | Compose-specific public API | New Compose-first applications |
| Established charting dependency | Mature interactions and many chart types | Greater dependency size and reduced control | Rapid feature delivery |
| Embedded web chart | Access to a broad chart catalogue | Requires a web bridge and native accessibility work | Specialist web-based charts |
The choice does not have to be permanent. A clean internal API can hide the backend, allowing a Canvas implementation to be replaced later. Document the supported Android versions, minimum SDK and dependency conflicts in the sample app. When several teams use the component, that contract is often more valuable than a marginal rendering optimisation.
Prepare the library for real-world use
Package the code as an Android library with a stable namespace, semantic versioning and a sample application. Keep public models separate from View-specific code, and document how consumers set data, formatting, colours and listeners. Publishing through Maven Central or a private repository makes upgrades repeatable; release notes should call out behaviour changes as well as features.
Run unit tests for scale calculations, label collision rules and gesture boundaries. Use Robolectric or screenshot tests on continuous integration to catch clipping and theme regressions. Exercise large numbers, long place names, abbreviated dollar formats and missing values so realistic content does not break the layout.
Release checks
- Run unit and screenshot tests against the supported Android versions.
- Verify accessibility actions, colour contrast and reduced-motion behaviour.
- Check touch targets and gestures within nested scrolling containers.
- Publish a signed release with usage documentation and a sample app.
Operational checks
- Collect crash reports and anonymised rendering feedback.
- Watch dependency updates for Compose or Android SDK changes.
- Fix clipping or scaling defects before adding new chart types.
Versioning matters especially when consumers change data models. Reserve a major release for incompatible API changes, patch releases for rendering fixes and minor releases for backward-compatible features. A charting library earns trust when its output stays correct as screens, devices and datasets evolve.