Blog Infos
Author
Published
Topics
Published

The graph does come down in the end!

Many times we have to represent some data in form of a progressive graph. That gives users a better experience of its data. So in this article, we’ll explore one of the many ways of creating a graph in Jetpack Compose. Let’s see what will be our end result.

 

Graph Final Output

 

Though the compose API for creating a graph is quite straightforward, there are some minor things that we need to take care of and we also need some understanding of the coordinate system because we’ll be calculating our points to be plotted. Enough talking, let’s dive into the process.

As obvious that we need some custom drawing in this case so we’ll be playing around with Canvas in Compose.

Presumption

Firstly, let’s understand how we’ll plot the graph and some assumptions. So we have a X-axis which we assume represents days and Y-axis which represents the number of steps a user has walked on that particular day. We’ll have 10 days on the x-axis and a max of 350 steps in the interval of 50 on the y-axis.

Graph illustration

The diagram shows some key terms that we’ll be building the graph over.

xAxisSpace

The minimum or equal spacing provided for values on X axis.

yAxisSpace

The minimum or equal spacing provided for values on Y axis.

Point 1

The first starting point on the graph.

Point 2

The second point on the graph and Point 3.. and so on.

Control point 1 and 2

This is the point with the help of which the bézier curve draws its curve from Points 1 and 2.

If you’re not aware of how bezier curve works then let’s take a quick look.

Source: https://www.quackit.com/

As shown in the image, bezier curve has a start (P0) and end (P3) point between which the curve is drawn. It also has two control points P1 and P2 which helps in controlling the shape of the curve. For example, if you’ll change both the control points for various values then some of the shapes could be.

So far, we’ve understood all the minor details that we needed to. Now let’s code what we’ve just seen.

First, let’s create a Graph composable with the following inputs

@Composable
fun Graph(
    modifier : Modifier,
    xValues: List<Int>,
    yValues: List<Int>,
    points: List<Float>,
    paddingSpace: Dp,
    verticalStep: Int
)

The values are self-explanatory. Let’s move to the second step. Now we’ll have a box and inside it, we’ll add a canvas.

Box(
    modifier = modifier
        .background(Color.White)
        .padding(horizontal = 8.dp, vertical = 12.dp),
    contentAlignment = Center
) {
    Canvas(
        modifier = Modifier.fillMaxSize(),
    ) { }
}

We set the canvas to the full size of the box and the size of the box we’ll control from the parent. Now we’ll first display our x & y-axis. We’ll also calculate the spacing between points on the x and y-axis by subtracting the padding we provided to our box and dividing by the total points on the axis. We also draw small circles on the points(coordinates) so that we can see where our points are located.

val xAxisSpace = (size.width - paddingSpace.toPx()) / xValues.size
val yAxisSpace = size.height / yValues.size
/** placing x axis points */
for (i in xValues.indices) {
drawContext.canvas.nativeCanvas.drawText(
"${xValues[i]}",
xAxisSpace * (i + 1),
size.height - 30,
textPaint
)
}
/** placing y axis points */
for (i in yValues.indices) {
drawContext.canvas.nativeCanvas.drawText(
"${yValues[i]}",
paddingSpace.toPx() / 2f,
size.height - yAxisSpace * (i + 1),
textPaint
)
}
/** placing points */
for (i in points.indices) {
val x1 = xAxisSpace * xValues[i]
val y1 = size.height - (yAxisSpace * (points[i]/verticalStep.toFloat()))
coordinates.add(PointF(x1,y1))
/** drawing circles to indicate all the points */
drawCircle(
color = Color.Red,
radius = 10f,
center = Offset(x1,y1)
)
}

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

, ,

Migrating to Jetpack Compose – an interop love story

Most of you are familiar with Jetpack Compose and its benefits. If you’re able to start anew and create a Compose-only app, you’re on the right track. But this talk might not be for you…
Watch Video

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engineer for Jetpack Compose
Google

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engin ...
Google

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engineer f ...
Google

Jobs

Running the app till here will give us the below output.

Great! We’re getting there. Now to join these points using bezier curve, we need to identify the control points for each pair of points starting from zero.

So let’s calculate the control points for them.

/** calculating the connection points */
for (i in 1 until coordinates.size) {
controlPoints1.add(PointF((coordinates[i].x + coordinates[i - 1].x) / 2, coordinates[i - 1].y))
controlPoints2.add(PointF((coordinates[i].x + coordinates[i - 1].x) / 2, coordinates[i].y))
}

Now as we have the coordinates, and control points for all pairs, let’s join the coordinates. We first move to the first coordinate and then start joining them.

/** drawing the path */
val stroke = Path().apply {
reset()
moveTo(coordinates.first().x, coordinates.first().y)
for (i in 0 until coordinates.size - 1) {
cubicTo(
controlPoints1[i].x,controlPoints1[i].y,
controlPoints2[i].x,controlPoints2[i].y,
coordinates[i + 1].x,coordinates[i + 1].y
)
}
}
drawPath(
stroke,
color = Color.Black,
style = Stroke(
width = 5f,
cap = StrokeCap.Round
)
)

Running the app till here will give us the below output.

 

 

Coool! Isn’t it. Now the final part is to fill the area under the path. For that, we’ll use the recently created path and will extend it to fill the area. The fillPath object will draw a line from the last coordinate of our strokePath to the right bottom corner excluding the vertical space and from there it’ll draw a line to the left side of the graph and then finally to the first coordinate. This ensures a closed loop for our path.

/** filling the area under the path */
val fillPath = android.graphics.Path(stroke.asAndroidPath())
.asComposePath()
.apply {
lineTo(xAxisSpace * xValues.last(), size.height - yAxisSpace)
lineTo(xAxisSpace, size.height - yAxisSpace)
close()
}
drawPath(
fillPath,
brush = Brush.verticalGradient(
listOf(
Color.Cyan,
Color.Transparent,
),
endY = size.height - yAxisSpace
),
)
view raw FillPathArea.kt hosted with ❤ by GitHub

Now if we’ll run the app, we’ll get our graph drawn on screen. See it wasn’t that difficult but only some math and geometry.

Check out the project on github, fork it and try with different values to see variations.

Source Code: https://github.com/aqua30/GraphCompose

That is all for now.

Until next time…

Cheers!

This article was originally published on proandroiddev.com on June 14, 2022

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
It’s one of the common UX across apps to provide swipe to dismiss so…
READ MORE
blog
In this part of our series on introducing Jetpack Compose into an existing project,…
READ MORE
blog
In the world of Jetpack Compose, where designing reusable and customizable UI components is…
READ MORE
blog

How to animate BottomSheet content using Jetpack Compose

Early this year I started a new pet project for listening to random radio…
READ MORE

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.

Menu