Your first chart in Android App with CSV parser
If you have ever needed to add a chart to your Android app, you certainly have heard about MPAndroidChart by PhilJay. If not, consider using this powerful library. Let me show you how easy it is to start!
Goal
The goal is to build a simple app written in Kotlin which displays a linear chart with static data. To make it a little bit more interesting we're going to provide data with .csv
file. It is very simple format for storing table-based data in the form of text files where values are separated with commas (Comma Separated Values). We'll use an OpenCSV
library to parse it.
Dependencies
First add dependencies to gradle files.
allprojects {
repositories {
...
maven { url "https://jitpack.io" }
dependencies {
...
implementation "com.github.PhilJay:MPAndroidChart:v3.0.3"
implementation "com.opencsv:opencsv:4.1"
Raw Data Set
Find some data, for example from here. I've chosen food searches on Google set and cut it a little to display comparison of two searches: banana bread
and frozen yogurt
. The file looks like this:
id,googleTopic,week_id,value
banana-bread,/m/04cym9,2004-01,30
banana-bread,/m/04cym9,2004-02,31
banana-bread,/m/04cym9,2004-03,24
banana-bread,/m/04cym9,2004-04,27
...
There are 676
records in both datasets. Values are normalised, so value
is between 0
and 100
, where 0
is the lowest number of searches, 100
is the largest, and the rest are calculated proportionally.
Put banana_bread.csv
& frozen_yogurt.csv
under /app/res/raw
directory in your project.
Now let's make a data class
with corresponding field, the simplest as it can be:
FoodSearch.kt
package com.bi.chartapp
data class FoodSearch(
val id: String,
val googleTopic: String,
val week_id: String,
val value: Int
)
How to parse it?
Take a look at the parser below:
Parser.kt
package com.bi.chartapp
import com.opencsv.CSVReaderBuilder
import java.io.Reader
class Parser {
companion object {
fun toDataSet(reader: Reader): List<FoodSearch> {
val csvReader = CSVReaderBuilder(reader)
.withSkipLines(1)
.build()
val foodSearches = mutableListOf<FoodSearch>()
var record = csvReader.readNext()
while (record != null) {
foodSearches.add(FoodSearch(record[0], record[1], record[2], record[3].toInt()))
record = csvReader.readNext()
}
return foodSearches
}
}
}
The static method builds a list of FoodSearch
records based on the provided Reader
. You can get a reader from fileStream
, which is provided with activity resources. It looks like this:
val streamBananas = resources.openRawResource(R.raw.banana_bread)
val bananaData = Parser.toDataSet(streamBananas.reader())
Finally our data looks a little bit more friendly. Time to make a chart!
Apply data
Add LineChart
view to your layout, for example:
<com.github.mikephil.charting.charts.LineChart
android:id="@+id/lineChart"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_centerInParent="true" />
How to manage data now?
LineChart
accepts data as an instance of LineData
class. LineData
is created from at least one instance of LineDataSet
. LineDataSet
is created from MutableList
of Entry
objects and String
label. Huh. And Entry
is an object representing single point on our Chart (basically is an (x,y) representation). It sounds a little bit overwhelming at first, but it is pretty simple. Here are the steps to follow:
- Parse your data from a csv file to a list of any data objects representing a single point on the chart - DONE!
- Map the list of data objects to the list of
Entry
objects - Create
LineDataSet
from the list of entries and label, like "Banana Bread" - Use each of the sets as an argument to create
LineData
object. Voila.
private fun getEntriesFromCSV(rawResId: Int, label: String): LineDataSet {
var data: List<FoodSearch>? = null
resources.openRawResource(rawResId).use { stream ->
data = Parser.toDataSet(stream.reader())
}
val entries: MutableList<Entry> = ArrayList()
data?.mapIndexed { index, foodSearch ->
entries.add(
Entry(index.toFloat(), foodSearch.value.toFloat(), foodSearch)
)
}
return LineDataSet(entries, label)
}
This function and Parser from the previous paragraph creates LineDataSet
from csv
resource. Now simply call it for each csv
file and create LineData
:
val bananaDataSet = getEntriesFromCSV(R.raw.banana_bread, "Banana Bread")
val yogurtDataSet = getEntriesFromCSV(R.raw.frozen_yogurt, "Frozen Yogurt")
lineChart.data = LineData(
bananaDataSet,
yogurtDataSet
)
That's it, the chart is set up with data!
Make me beautiful - LineChart properties
Time to add some colors and spices. The chart layout configuration is very flexible. I'll show you some basic properties, the rest you may find in the documentation. Props are separated between dataset-specific and chart-specific.
Tune up data sets
First add some colors to the resource colors.xml
file in order to make banana look a little bit more like banana and yogurt like yogurt.
<color name="banana">#ffe100</color>
<color name="yogurt">#0085c7</color>
We'll configure each of the LineDataSet
object the same way, but with a different color. Check out the method below with the comments:
private fun configureSetLayout(set: LineDataSet, color: Int) {
set.color = color // color of the line
set.setDrawFilled(true) // fill the space between line and chart bottom
set.fillColor = color // color of the fill
set.setDrawCircles(false) // disable drawing circles over each Entry point
set.mode = LineDataSet.Mode.CUBIC_BEZIER // round the line
set.fillAlpha = 50 // make fill transparent with alpha (0-255)
}
Now apply configuration to each dataset:
val bananaColor = resources.getColor(R.color.banana, null)
val yogurtColor = resources.getColor(R.color.yogurt, null)
configureSetLayout(bananaDataSet, bananaColor)
configureSetLayout(yogurtDataSet, yogurtColor)
Tune up a chart
You may configure the chart behaviour in many ways. Default setting allows the user to scale the chart with pinching and scroll it. Since our dataset contains ~700 records let's leave the ability to scale the chart along the axis X and only block the ability to scale it along axis Y. Also remove a description from the right bottom corner and highlighting values by tapping. Like that:
lineChart.description.isEnabled = false
lineChart.isHighlightPerTapEnabled = false
lineChart.isHighlightPerDragEnabled = false
lineChart.isScaleYEnabled = false
It works more intuitively already. Now notice that the description above the top X axis are float values. They don't say much unfortunately. It would be better if they marked a year's change every 52 weeks! To achieve this use IAxisValueFormatter
. It looks like this:
inner class MyAxisFormatter : IAxisValueFormatter {
override fun getFormattedValue(value: Float, axis: AxisBase?): String {
return if (value.toInt() % 52 == 0) "${startingYear + value.toInt() / 52}"
else ""
}
}
It means that it will display a value only if it is divisible by 52. And then maps the value to a corresponding year (with starting year set to 2004). We need also a proper granularity so there won't be any grid between years.
lineChart.xAxis.valueFormatter = MyAxisFormatter()
lineChart.xAxis.granularity = 52f
Also who needs an Y axis on both sides? Disable one of them:
lineChart.axisRight.isEnabled = false
Boom! That's it! Looks nice and presents the data clearly. Pinch to zoom, swipe right and left to get through all these years and finally check when a frozen yogurt beats a banana bread in Google searches!
Oh, it looks like every year around summer! Who would know.