Enhancing Flutter apps: Implementing unique charts

Animating a heart-shaped liquid chart using paths in Flutter

Mobile@Exxeta
Mobile App Circular
6 min read5 days ago

--

Photo by Markus Winkler from Pexels

There are many different forms and appearances of charts that can be integrated into apps. However, they are often hard to understand and don’t catch the user’s eye. When we’re looking for a distinctive chart design to give our app a unique look, our designers came up with a special idea of a liquid heart chart. While developing this chart, we faced two challenges:

  • Creating a heart-shaped path with Dart
  • Making the shape scalable across all device sizes

This article provides you with a quick guide on how to implement this design in Flutter.

A variety of use cases

Heart-shaped liquid chart finance example

The heart-shaped liquid chart is applicable to various scenarios. It can for example represent points achieved in a game, the calories a user consumed for a day or tasks completed from a to-do list. In our case, we utilize it to display the user’s remaining monthly budget or if they have exceeded their budget. This article presents a generic version of the chart implementation, which can be adapted to suit different needs. For instance, we customized it by changing colors to indicate when the user is in a financial deficit.

Liquid progress indicator package

To create an animated liquid chart, we use the package liquid_progress_indicator_v2 from pub.dev.

Start by adding the package to your Flutter project. The package allows you to create a liquid progress line. “Liquid” means that the line features waves and animates while filling a shape. For example, you can use LiquidCircularProgressIndicator to get a circle filled by the liquid.

LiquidCircularProgressIndicator( 
value: 0.25,
valueColor: AlwaysStoppedAnimation(Colors.pink),
backgroundColor: Colors.white,
borderColor: Colors.red,
borderWidth: 5.0,
direction: Axis.horizontal,
center: Text("Loading..."),
);
Liquid chart circle-shape example

You can customize the LiquidCircularProgressIndicator with different colors and set a maximum value. Additionally, you can define the floating direction of the liquid and add a widget in the center of the chart. The package provides other shapes to customize the chart, but none of it matches our use case of a heart.
Luckily, there is also a LiquidCustomProgressIndicator, which allows you to set a custom path to create custom shapes for the chart. To create a heart-shaped liquid chart, we will now create a heart path.

Creating a heart-shaped path

To create a heart-shaped path, we define a function that returns a Path. Since it is important for our heart shape to work on all device sizes, we also pass the width of the device as a parameter to scale the values accordingly. We start by calculating the height of the heart by multiplying the width by 0.8. For drawing the heart, we use two functions:

  1. moveTo to set the starting point.
  2. cubicTo to draw the arches. The function adds a cubic bezier segment to create curves from given points. It can take three points as parameters: two control points and an endpoint for the arch.
Path _buildHeartPath(double width) { 
final scaleValue = 0.8;
final height = width * scaleValue;

return Path()
// start point to draw the first arch
..moveTo(0.5 * width, height * 0.27)
// x, y of control point, x, y of end point of the arch
..cubicTo(0.4 * width, 0, -0.27 * width, height * 0.2, 0.5 * width, height * 0.9)
// start point to draw the second arch
..moveTo(0.5 * width, height * 0.27)
// x, y of control point, x, y of end point of the arch
..cubicTo(0.6 * width, 0, 1.27 * width, height * 0.2, 0.5 * width, height * 0.9);
}

The heart shape consists of two arches that can be connected to form the complete shape. To create the path, we implement following steps:

Draw the left arch

  1. Set the top indentation of the heart as the start point with the moveTo function.
  2. Use cubicTo to draw an arch from the start point to the end point. As parameters, we use one control point (left side) to adjust the arch’s curve and an end point to finalize the arch.

Draw the right arch

  1. Set once again the top indentation of the heart as the start point with the moveTo function.
  2. Use once again cubicTo to draw the arch. Use the same end point as in the first left arch but change the control point (right side) to draw the arch in the opposite direction.
How to create a heart path illustration

Shape and style of the chart

Next, we will create a StatefulWidget to implement our animated liquid chart in the shape of a heart. The widget needs to be stateful because we want to add an animation for the chart. In our LiquidHeartChartState, we will create an AnimationController to manage the animation and ensure that the liquid ascends to our maximum value. To enable this animation, our State class needs to inherit from SingleTickerProviderStateMixin. In the initState method, we initialize the AnimationController with the vsync parameter set to this to utilize the SingleTickerProviderStateMixin. With the duration parameter, we can define how long it should take, until the liquid fills the heart to the given value. Additionally, we add a listener to our AnimationController to update the state whenever the AnimationController value changes. With this implementation, the heart gets filled by liquid in four seconds because AnimationController value is being increased by the ticker.

class _LiquidHeartChartState extends State<LiquidHeartChart> with SingleTickerProviderStateMixin { 
late AnimationController _animationController;

@override
void initState() {
super.initState();
_animationController = AnimationController(
vsync: this,
duration: const Duration(seconds: 4),
);

_animationController
..addListener(() => setState(() {}))
..forward();
}

@override
void dispose() {
_animationController.dispose();
super.dispose();
}

@override
Widget build(BuildContext context) {
return Center(
child: LiquidCustomProgressIndicator(
value: _animationController.value,
direction: Axis.vertical,
backgroundColor: const Color.fromARGB(255, 139, 205, 141).withOpacity(0.25),
valueColor: const AlwaysStoppedAnimation(Color.fromRGBO(9, 107, 73, 1)),
shapePath: _buildHeartPath(MediaQuery.of(context).size.width),
center: Text(
_animationController.value.toString()
),
),
);
}

Path _buildHeartPath(double width) {
...
}
}

Finally, we place our LiquidCustomProgressIndicator within a Center widget, where we assign our heart-shaped path and style it according to our requirements. The value property is assigned with the defined AnimationController’s value. If you want the liquid chart to stop increasing at a certain value, you can enter a total and part as the widget’s parameters. Then you can calculate the differences and percentage value of the difference and stop the AnimationController with the stop function, when for example a certain percentage was reached. An example code might look like this, which you can place in the build method:

const maxPercentage = 70; 
final difference= (widget.total- widget.part);
final differencePercentage = difference / widget.total * 100;
final chartValueInPercentage = _animationController.value * 100;

/// Stop the growing of the chart, if chartvalue is bigger than difference
if (chartValueInPercentage > differencePercentage || chartValueInPercentage > maxPercentage) {
_animationController.stop();
}

Conclusion

With the liquid_progress_indicator_v2, we’ve effortlessly crafted a distinctive liquid chart in the shape of a heart, enhancing the user experience with a touch of uniqueness. However, there are numerous other shapes you can experiment with for a liquid chart. Which shape piques your interest? Let us know in the comments! (By Tobias Rump & Laura Siewert)

--

--

Passionate people @ Exxeta. Various topics around building great solutions for mobile devices. We enjoy: creating | sharing | exchanging. mobile@exxeta.com