Gantt Chart With Flutter

Use basic UI widgets in Flutter to create a Gantt chart report of staff’s working history

Each staff’s history will be showed in a different section, user can scroll it horizontally to see the rest of its chart. Each section has a timeline by month and display each project by a bar in separated line

The GanttChart class

class GanttChart extends StatelessWidget {
final AnimationController animationController;
final DateTime fromDate;
final DateTime toDate;
final List<Project> data;
final List<User> usersInChart;

int viewRange;
int viewRangeToFitScreen = 6;
Animation<double> width;

GanttChart({
this.animationController,
this.fromDate,
this.toDate,
this.data,
this.usersInChart,
}) {
viewRange = calculateNumberOfMonthsBetween(fromDate, toDate);
}

It needs 2 DateTim objects fromDate and toDate to determine the range of the chart, data is a list of projects and usersInChart is a list of users who will be presented in the chart.

Some helper methods had been used:

  • randomColorGenerator: generate a random color for the theme color of each section
  • calculateDistanceToLeftBorder: return how many months between the fromDate and the starting point of the presenting project. If it had been already started before the fromDate, it will be display just at the left border with the remaining length.
  • calculateRemainingWidth: let the widget know how much of the size of the project’s bar will be shown in the chart
Color randomColorGenerator() {
var r = new Random();
return Color.fromRGBO(r.nextInt(256), r.nextInt(256), r.nextInt(256), 0.75);
}

int calculateNumberOfMonthsBetween(DateTime from, DateTime to) {
return to.month - from.month + 12 * (to.year - from.year) + 1;
}

int calculateDistanceToLeftBorder(DateTime projectStartedAt) {
if (projectStartedAt.compareTo(fromDate) <= 0) {
return 0;
} else
return
calculateNumberOfMonthsBetween(fromDate, projectStartedAt) - 1;
}

int calculateRemainingWidth(
DateTime projectStartedAt, DateTime projectEndedAt) {
int projectLength =
calculateNumberOfMonthsBetween(projectStartedAt, projectEndedAt);
if (projectStartedAt.compareTo(fromDate) >= 0 &&
projectStartedAt.compareTo(toDate) <= 0) {
if (projectLength <= viewRange)
return projectLength;
else
return
viewRange -
calculateNumberOfMonthsBetween(fromDate, projectStartedAt);
} else if (projectStartedAt.isBefore(fromDate) &&
projectEndedAt.isBefore(fromDate)) {
return 0;
} else if (projectStartedAt.isBefore(fromDate) &&
projectEndedAt.isBefore(toDate)) {
return projectLength -
calculateNumberOfMonthsBetween(projectStartedAt, fromDate);
} else if (projectStartedAt.isBefore(fromDate) &&
projectEndedAt.isAfter(toDate)) {
return viewRange;
}
return 0;
}

Common UI components

  • buildHeader: render the timeline of each section
  • buildGrid: render the gray vertical lines separate each month of the timeline
Widget buildHeader(double chartViewWidth, Color color) {
List<Widget> headerItems = new List();

DateTime tempDate = fromDate;

headerItems.add(Container(
width: chartViewWidth / viewRangeToFitScreen,
child: new Text(
'NAME',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 10.0,
),
),
));

for (int i = 0; i < viewRange; i++) {
headerItems.add(Container(
width: chartViewWidth / viewRangeToFitScreen,
child: new Text(
tempDate.month.toString() + '/' + tempDate.year.toString(),
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 10.0,
),
),
));
tempDate = Utils.nextMonth(tempDate);
}

return Container(
height: 25.0,
color: color.withAlpha(100),
child: Row(
children: headerItems,
),
);
}

Widget buildGrid(double chartViewWidth) {
List<Widget> gridColumns = new List();

for (int i = 0; i <= viewRange; i++) {
gridColumns.add(Container(
decoration: BoxDecoration(
border: Border(
right:
BorderSide(color: Colors.grey.withAlpha(100), width: 1.0))),
width: chartViewWidth / viewRangeToFitScreen,
//height: 300.0,
));
}

return Row(
children: gridColumns,
);
}

buildChartBars method will return a list of widget(a container for each chart bar)

List<Widget> buildChartBars(
List<Project> data, double chartViewWidth, Color color) {
List<Widget> chartBars = new List();

for(int i = 0; i < data.length; i++) {
var remainingWidth =
calculateRemainingWidth(data[i].startTime, data[i].endTime);
if (remainingWidth > 0) {
chartBars.add(Container(
decoration: BoxDecoration(
color: color.withAlpha(100),
borderRadius: BorderRadius.circular(10.0)),
height: 25.0,
width: remainingWidth * chartViewWidth / viewRangeToFitScreen,
margin: EdgeInsets.only(
left: calculateDistanceToLeftBorder(data[i].startTime) *
chartViewWidth /
viewRangeToFitScreen,
top: i == 0 ? 4.0 : 2.0,
bottom: i == data.length - 1 ? 4.0 : 2.0
),
alignment: Alignment.centerLeft,
child: Padding(
padding: const EdgeInsets.only(left: 8.0),
child: Text(
data[i].name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(fontSize: 10.0),
),
),
));
}
}

return chartBars;
}

So, we’ll build a section for each user, the widget is basically a container which contained a horizontal ListView, we need to set the height of this container specifically because of the horizontal list.

The children of the ListView, just a Stack with fit: StackFit.loose contain a grid and a header from 2 methods buildGrid and buildHeader we already talked about. Next widget is another container with a top margin of 25, the height of the header. Inside of it will be a Row to contain the name of the staff at the left and a list of chart bars at the right with had been built from the buildChartBars method.

Widget buildChartForEachUser(
List<Project> userData, double chartViewWidth, User user) {
Color color = randomColorGenerator();
var chartBars = buildChartBars(userData, chartViewWidth, color);
return Container(
height: chartBars.length * 29.0 + 25.0 + 4.0,
child: ListView(
physics: new ClampingScrollPhysics(),
scrollDirection: Axis.horizontal,
children: <Widget>[
Stack(fit: StackFit.loose, children: <Widget>[
buildGrid(chartViewWidth),
buildHeader(chartViewWidth, color),
Container(
margin: EdgeInsets.only(top: 25.0),
child: Container(
child: Column(
children: <Widget>[
Container(
child: Row(
children: <Widget>[
Container(
width: chartViewWidth / viewRangeToFitScreen,
height: chartBars.length * 29.0 + 4.0,
color: color.withAlpha(100),
child: Center(
child: new RotatedBox(
quarterTurns: chartBars.length * 29.0 + 4.0 > 50 ? 0 : 0,
child: new Text(
user.name,
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
)),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: chartBars,
),
],
),
),
],
),
)),
]),
],
),
);
}
Like what you read? Give Nguyễn Lê Gia Phụng a round of applause.

From a quick cheer to a standing ovation, clap to show how much you enjoyed this story.