We as an Android Developer some times need charts for multiple functionality to implement in our app for which we either use the 3rd party libraries or some paid libraries. But with with the help of Jetpack Compose canvas we can create our own in very simple way. We need a little knowledge about canvas that you will learn in this article. If you want to learn with a video then check out my video on this topic here.
Create Chart Data
To show data in our line chart we need to create some sample data which we will be showing in our graph. The idea is to create a list of Pair of Integers & Doubles where the first part of Pair will show the index of Data Point and Second part of Pair will contain the actual value of that particular data point which will decide the height of data point in Y-axis.
We will loop over these data and draw a point as per the value in Y-axis and then at the connect all the dots with lineTo function of Canvas.
Create simple list of Pairs with random values as follows:
val chartData = listOf(
Pair(1, 1.5),
Pair(2, 1.75),
Pair(3, 3.45),
Pair(4, 2.25),
Pair(5, 6.45),
Pair(6, 3.35),
Pair(7, 8.65),
Pair(8, 0.15),
Pair(9, 3.05),
Pair(10, 4.25)
)
Before writing the main code first define some variables for our line chart like spacing b/w data, color of our lines and lower & upper values of our data.
val spacingFromLeft = 80f
val graphColor = Color.Green //color for your graph
val transparentGraphColor = remember { graphColor.copy(alpha = 0.5f) }
val upperValue = remember { (data.maxOfOrNull { it.second }?.plus(1))?.roundToInt() ?: 0 }
val lowerValue = remember { (data.minOfOrNull { it.second }?.toInt() ?: 0) }
val density = LocalDensity.current
// This will create horizontal spacing b/w each data points
val spacePerData = (size.width - spacingFromLeft) / data.size
In jetpack compose canvas we need to define Text Paint for any type of text that we need to draw on the canvas. In this Paint Object we decide what property we want to give to our text like its color, text size, text font etc.
//paint for the text shown in data values
val textPaint = remember(density) {
Paint().apply {
color = android.graphics.Color.BLACK
textAlign = Paint.Align.CENTER
textSize = density.run { 12.sp.toPx() }
}
}
Now our basic parts are ready to implement. We will use the Canvas Composable to use following function for said functionality.
- drawText :- To draw horizontal and vertical data reference for the line chart.
- drawLine :- To draw Lines of our chart which will follow the data points from start to end.
- drawPath :- This function will draw a curved or transparent background behind our chart.
First show Horizontal reference data texts. For this we will loop through our list of data by step of one means one value in each iteration. Inside the loop first get the value of first part of pair then we need the Native canvas scope to use the drawText function because the till now we can’t use drawText directly inside the Jetpack Compose Canvas. With each iteration we will increase the value of X offset of drawText function which will draw text each time with a horizontal spacing along the horizontal axis.
//loop through each index by step of 1
//data shown horizontally
(data.indices step 1).forEach { i ->
val hour = data[i].first
drawContext.canvas.nativeCanvas.apply {
drawText(
hour.toString(),
spacingFromLeft + i * spacePerData,
size.height,
textPaint
)
}
}
Same as above we can draw the vertical reference text but this time increasing the Y offset of drawText function. But we don’t need to draw all the 10 values as per size of data list. We can show upto 5 value which are multiple of 5 which will approximately divide the upper value of data and lower value of data into 5 parts.
val priceStep = (upperValue - lowerValue) / 5f
//data shown vertically
(0..4).forEach { i ->
drawContext.canvas.nativeCanvas.apply {
drawText(
round(lowerValue + priceStep * i).toString(),
30f,
size.height - spacingFromLeft - i * size.height / 5f,
textPaint
)
}
}
After showing vertical and horizontal text for chart reference now we need to draw line also in each axis for more clarity. For this we use drawLine function of canvas which take 4 main arguments-
- Start Offset- The point from where line will start drawing(Offset takes X & Y values)
- End Offset- The point till this line goes or ends(Offset takes X & Y values)
- Color – The color of the line
- Stroke – This define the width of the line(takes Float value).
//Vertical line
drawLine(
start = Offset(spacingFromLeft, size.height - spacingFromLeft),
end = Offset(spacingFromLeft, 0f),
color = Color.Black,
strokeWidth = 3f
)
//Horizontal line
drawLine(
start = Offset(spacingFromLeft, size.height - spacingFromLeft),
end = Offset(size.width - 40f, size.height - spacingFromLeft),
color = Color.Black,
strokeWidth = 3f
)
Now comes to our main code where we will create the path for our graph by connecting each data points. To create a path it take multiple type of input which are as follows:
- moveTo – this is the first coordinate point(X,Y) of our path from where our path starts.
- lineTo – this method will draw a line to end points we pass into this(take coordinate X,Y)
- quadraticBezierTo – this is used to draw curved line which take two coordinates points. see this picture for more clarification on how these works. In this picture Red point shows the start point and green and blue show the coordinate which we need to pass as an argument in this function to cea
First create straight line path by using lineTo function of Path.
//Use this to show straight line path
val straightLinePath = Path().apply {
val height = size.height
//loop through index only not value
data.indices.forEach { i ->
val info = data[i]
val x1 = spacingFromLeft + i * spacePerData
val y1 =
(upperValue - info.second).toFloat() / upperValue * height - spacingFromLeft
if (i == 0) {
moveTo(x1, y1)
}
lineTo(x1, y1)
//drawCircle(color = Color.Black, radius = 5f, center = Offset(x1,y1)) //Uncomment it to see the end points
}
}
Now we will create curved line path by using quadrticBezierTo function of Path
//Use this to show curved path
var medX: Float
var medY: Float
val curvedLinePath = Path().apply {
val height = size.height
data.indices.forEach { i ->
val nextInfo = data.getOrNull(i + 1) ?: data.last()
val x1 = spacingFromLeft + i * spacePerData
val y1 =
(upperValue - data[i].second).toFloat() / upperValue * height - spacingFromLeft
val x2 = spacingFromLeft + (i + 1) * spacePerData
val y2 =
(upperValue - nextInfo.second).toFloat() / upperValue * height - spacingFromLeft
if (i == 0) {
moveTo(x1, y1)
} else {
medX = (x1 + x2) / 2f
medY = (y1 + y2) / 2f
quadraticBezierTo(x1 = x1, y1 = y1, x2 = medX, y2 = medY)
}
//drawCircle(color = Color.White, radius = 5f, center = Offset(x1,y1))
//drawCircle(color = Color.Magenta, radius = 9f, center = Offset(medX,medY))
//drawCircle(color = Color.Blue, radius = 7f, center = Offset(x2,y2)) //Uncomment these to see the control Points
}
}
Its time to draw the path one by one. We will be use drawPath function which will take following arguments:
- path – the path you created
- color – the color of the path
- style – Stroke or Fill of path style
//Now draw path on canvas
drawPath(
path = straightLinePath,
color = graphColor,
style = Stroke(
width = 1.dp.toPx(),
cap = StrokeCap.Round
)
)
For the curved path we also want to show the transparent background. To draw that we need to create a path which will cover whole background of the line drawn. So we will take the straight line path and add extra features to it which will do the rest of the effect.
//To show the background transparent gradient
val fillPath = android.graphics.Path(straightLinePath.asAndroidPath()).asComposePath().apply {
lineTo(size.width - spacePerData, size.height - spacingFromLeft)
lineTo(spacingFromLeft, size.height - spacingFromLeft)
close()
}
Now for the curved line path pass the path which we created by applying transparent background with it. You can use brush argument for creating gradient background of the path instead of using plain colors.
drawPath(
path = fillPath,
brush = Brush.verticalGradient(
colors = listOf(
transparentGraphColor,
Color.Transparent
),
endY = size.height - spacingFromLeft
)
)
GitHub gist for video source code: here