Kamil Powałowski
Posted on January 2, 2020
There is no better way to present data than a pie chart as we saw in this Dilbert comic strip.
But sometimes you want to display values that changed over time. More suitable for these tasks are the line and bar charts. It doesn’t matter which type of chart you want to use in your Flutter mobile app - all of them you can create using fl_chart Package made by Iman Khoshabi - imaNNeoFighT.
Challange
To get used to fl_chart library I created a challenge for myself - display stock data using Flutter. The chart should look pretty and I should be able to read some information by just looking at the chart. Here is an effect of my work:
Solution
Get data
The first problem that I was forced to solve was a way to get data for this challenge. Most of the stock charts' data APIs are paid and required registration. After some research, I found about eodhistoricaldata.com. They provide example endpoints with fixed user token for testing purposes. To my excitation, sample data was for AAPL (Apple) stock data on NASDAQ. Using https://eodhistoricaldata.com/api/eod/AAPL.US?from=2018-01-01&to=2018-12-31&api_token=OeAFFmMliFG5orCUuwAKQ8l4WWFQ67YX&period=d&fmt=json
URL call I was able to get Apple stock prices for whole 2018 year. To make things easier for this challenge I saved the result of this call as data.json
in assets
directory and added it to project in pubspec.yaml
:
...
flutter:
assets:
- assets/data.json
...
Load data
My data.json
file contains an array/list of items that look like this:
[
{
"date": "2018-01-02",
"open": 170.16,
"high": 172.3,
"low": 169.26,
"close": 172.26,
"adjusted_close": 167.1997,
"volume": 25555934
},
...
]
To use them I’ve created datum.dart
model with content:
class Datum {
Datum({this.date, this.close});
final DateTime date;
final double close;
Datum.fromJson(Map<String, dynamic> json)
: date = DateTime.parse(json['date']),
close = json['close'].toDouble();
}
As you can see I took only two values that I’ll need for displaying the chart - date of entry and close price. Then I prepared the code to load this stock values to memory:
import 'dart:async';
import 'dart:convert' show jsonDecode;
import 'package:flutter/services.dart' show rootBundle;
import 'package:fluttersafari/datum.dart';
Future<List<Datum>> loadStockData() async {
final String fileContent = await rootBundle.loadString('assets/data.json');
final List<dynamic> data = jsonDecode(fileContent);
return data.map((json) => Datum.fromJson(json)).toList();
}
For this simple project jsonDecode
function available in dart:convert
package is sufficient but for bigger projects, I would recommend json_serializable
as explained in JSON and serialization article of Flutter documentation.
Add library
To use fl_chart I added the library to the project. This required another edit of pubspec.yaml
file:
...
dependencies:
flutter:
sdk: flutter
fl_chart: ^0.6.0
...
And calling flutter pub get
after it.
Prepare data
Before I'll pass our data to fl_chart I have to transfer it to form understand by this library. Axis based charts are expecting a list of FlSpot
objects that will represent points of interest on our chart. Each FlSpot
object contains two double
values (x
and y
). My X axis will display time and Y will show a close price.
final List<Datum> data = await loadStockData();
final List<FlSpot> values = data
.map((datum) => FlSpot(datum.date.millisecondsSinceEpoch.toDouble(), datum.close))
.toList();
setState(() {
_values = values;
});
I've used millisecondsSinceEpoch
to get an integer number for each data point. Full solution (attached at the end of blogpost) requires to provide minX
, maxX
, minY
, maxY
values which present lowest and highest numbers (with additional offset if needed) limiting chart in both directions.
Display chart
Now, when I have data ready to display, it's time to create a LineChart
widget.
LineChart(_mainData());
Nothing interesting here, right? The real fun starts with LineChartData
object.
LineChartData _mainData() {
return LineChartData(
gridData: _gridData(),
titlesData: FlTitlesData(
bottomTitles: _bottomTitles(),
leftTitles: _leftTitles(),
),
lineBarsData: [_lineBarData()],
);
}
We can control each aspect of LineChart
widget. From the look of the grid, color and shape of chart border to labels on any side of the cart to most important - lines that visualize our data. All available parameters can be inspected on the documentation page.
Line and background
Now let's focus on some elements starting from crème de la crème - the _lineBarData()
function that will provide stock data information for LineChartData
widget.
LineChartBarData _lineBarData() {
return LineChartBarData(
spots: _values,
colors: _gradientColors,
colorStops: const [0.25, 0.5, 0.75],
gradientFrom: const Offset(0.5, 0),
gradientTo: const Offset(0.5, 1),
barWidth: 2,
isStrokeCapRound: true,
dotData: const FlDotData(show: false),
belowBarData: BarAreaData(
show: true,
colors: _gradientColors.map((color) => color.withOpacity(0.3)).toList(),
gradientColorStops: const [0.25, 0.5, 0.75],
gradientFrom: const Offset(0.5, 0),
gradientTo: const Offset(0.5, 1),
),
);
}
This may look scary to you at the begging but despite providing transformed values in spots: _values
I'm setting just a few parameters that will give this chart these awesome gradient colors. Please take a look at belowBarData
property. Using this field I can control how the area below chart line looks. I just set it to three gradient colors stored in the _gradientColors
variable.
final List<Color> _gradientColors = [
const Color(0xFF6FFF7C),
const Color(0xFF0087FF),
const Color(0xFF5620FF),
];
Labels
displaying left and bottom labels may be a bit tricky at first. Let's take a look at this code:
SideTitles _bottomTitles() {
return SideTitles(
showTitles: true,
textStyle: TextStyle(
color: Colors.white54,
fontSize: 14,
),
getTitles: (value) {
final DateTime date =
DateTime.fromMillisecondsSinceEpoch(value.toInt());
return DateFormat.MMM().format(date);
},
margin: 8,
interval: (_maxX - _minX) / 6,
);
}
For each data point, LineChart
will call getTitles
function (which in this case converts data back to DateTime
object and returns short month name) and tries to display it below the chart. To not clutter chart with data, I used the interval parameter to limit how often getTitles
should be called (I decided to display only 6 labels below the chart).
A similar solution is used for _leftTitles()
but their interval calculations are more complicated so I leave them to check in full example available at the end of the blogpost.
SideTitles _leftTitles() {
return SideTitles(
showTitles: true,
textStyle: TextStyle(
color: Colors.white54,
fontSize: 14,
),
getTitles: (value) =>
NumberFormat.compactCurrency(symbol: '\$').format(value),
reservedSize: 28,
margin: 12,
interval: _leftTitlesInterval,
);
}
Grid
Last, but not least - we can control the way the grid is displayed.
FlGridData _gridData() {
return FlGridData(
show: true,
drawVerticalLine: false,
getDrawingHorizontalLine: (value) {
return const FlLine(
color: Colors.white12,
strokeWidth: 1,
);
},
checkToShowHorizontalLine: (value) {
return (value - _minY) % _leftTitlesInterval == 0;
},
);
}
An optional checkToShowHorizontalLine
function will tell which horizontal lines should be displayed. To fit grid lines to horizontal values, I've used logic related to a previously computed _leftTitlesInterval
variable (to be inspected in the full code repository).
Summary
Making eye-appealing charts usually required two very different steps. We have to prepare data first and then load it on the user interface. The first one is fully on you. But for UI, you can use a fl_chart or different library that can help you with this task. I hope that my challenge will motivate you for own experiments with this awesome library.
For full implementation of this project go to Flutter Safari repository on Github.
Posted on January 2, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.