I was working on an internal project a couple of years ago and one of the desired features was to display the date in a Gantt-type chart. I had already been doing quite a bit with Power Apps at the time so I thought maybe I could do this somehow with a canvas app. After trying several things, including embedding Power BI, it seemed like a futile effort.
Then one day after playing around with making some bar graphs with some dynamically sized label controls, I had an idea. It seemed that it would be possible to use some similar techniques to create a data-driven Gantt-like display. The idea has evolved a bit since the original.
Okay, enough of the boring story and onto the App!
App Features
Expand or Collapse to Parent Level
Click on the chevron to expand or collapse main tasks including the top-level project.
Automatic Grid Scaling
The app will automatically scale the date grid density based on the window size and date range.
Daily View
Weekly View
4-Week View
Navigate and Zoom
Select start and end dates to display tasks in that range on Gannt. Use Navigate buttons to move forward or backward in time. Use Zoom buttons to increase/decrease end date to effectively “Zoom” the time window in or out.
Automatically Adjust Gantt Density Based on Browser Window Size
App will dynamically change timeline grid lines based to display more data on larger/hi-res screens.
Smaller window scales to weekly grid to display data
Maximize window and see that it changes the scale from weekly to daily grid
If a task label is too long to put inside the Gantt bar based on the scaling, it is automatically placed outside the end of the bar.
Project Selection
At the bottom of the screen, there is a project list gallery where you can select the projects to include in the display.
How It All Works
The Data
For this demo app, I created some data that is loosely based on construction and remodeling projects. It is simplified for the purpose of demoing the UI. I imported the data into the app so that it would be easy to distribute the demo. Note that I wanted to make the data flat and simple for the purpose of showing the UI. In a real app, this data would be distilled from multiple tables or views. I load this table into a collection (colTasks) at startup to make things easier.
About the Tasks data:
Column Name | Description |
Id | Unique Id for the row |
ProjectId | Project number |
ProjectName | Project name |
TaskNo | Top-level task number (0=parent project) |
SubTaskNo | Sub task number |
TaskName | Task name |
StartDate | Start date for this task |
EndDate | End date for this task |
TaskType | Task type for this task |
TaskLvl | Task indent level for this task |
Duration | Duration in days for this task (parents contain sum of subtasks) if the duration is 0, then the task is considered a milestone and displayed with a pentagon icon (there is no diamond icon and I was a little too lazy to do something else). |
Show | Show this row in the Gantt |
Expanded | Are the sub tasks showing or not? |
Task Types
The Task Types are in a separate table. Each type has a color specified by a hex RGB color code which is used to color code the Gantt bars.
The Gantt Bars
The core of this app is how the Gantt bars are displayed. It comes down to two basic things: Length and Position.
To find the position, we first need to see how many horizontal pixels there are in a day on the grid.
We can get that by using the following calculation:
pixels per day = (end pixel - start pixel) / (end date - start date)
Then calculate the position X:
X = start pixel + (end date - start date) * pixels per day
For the bar length (width):
width = duration days * pixels per day
I used label controls to track the data to support the above calculations. Label controls are great for this since they automatically calculate the values as the data changes. Although these would normally be hidden, I am displaying them in this app for educational purposes.
The actual formulas in the app are as follows:
Pixels per day (label):
Value(lblPixInRange.Text)/Value(lblDaysInRange.Text)
Gantt bar position X:
Value(lblStartPix.Text) + DateDiff(dteStartDate.SelectedDate,ThisItem.StartDate,Days) * Value(lblPixPerDay.Text)
Gantt bar Width:
ThisItem.Duration * Value(lblPixPerDay.Text)
Date Overlay Grid (Underlay?)
The date grid is just a horizontal gallery control that actually sits behind the Gantt gallery.
The Items/Data source for the gallery is sequence starting at 0. I figured that 150 should be a safe maximum for most applications.
I have a hidden label (lblUGGridSectionStartDate) that I used to simplify calculating the timeline headers.
lblUGGridSectionStartDate.Text will dynamically change its value based on the density of the grid and start date.
Switch(
lblGridDensity.Text,
"4WEEKS",
dteStartDate.SelectedDate + ThisItem.Value * 28,
"WEEKLY",
dteStartDate.SelectedDate + ThisItem.Value * 7,
dteStartDate.SelectedDate + ThisItem.Value
)
Expand/Collapse Subtask Display
The key to determining what data to show in the Gantt view is literally the Boolean ‘Show’ column in the collection (as well as the selected project, of course).
The Items/Data source for the main gallery (galGanttView):
Filter(
colTasks,
ProjectId in Filter(
galProjectList.AllItems,
chkPLIsSelected.Value = true
).Result,
Show
)
When clicking on the chevron (icoGVShowHideSubs), the app will toggle the expanded state of the control and data column: ‘Expanded’ as well as updating the subtasks ‘Show’ column.
Select(Parent);
If(
ThisItem.Expanded,
UpdateIf(
colTasks,
// hide subtasks of current task
And(
ProjectId = ThisItem.ProjectId,
TaskNo = ThisItem.TaskNo,
SubTaskNo > 0
),
{Show: false},
// change expanded field for this task
And(
ProjectId = ThisItem.ProjectId,
TaskNo = ThisItem.TaskNo,
SubTaskNo = 0
),
{Expanded: false},
// hide all tasks for this project task (0)
If(
ThisItem.TaskNo = 0,
And(
ProjectId = ThisItem.ProjectId,
TaskNo > 0,
Show
)
),
{
Show: false,
Expanded: false
}
),
UpdateIf(
colTasks,
// show subtasks of current task
And(
ProjectId = ThisItem.ProjectId,
TaskNo = ThisItem.TaskNo,
SubTaskNo > 0
),
{Show: true},
// change expanded field for this task
And(
ProjectId = ThisItem.ProjectId,
TaskNo = ThisItem.TaskNo,
SubTaskNo = 0
),
{Expanded: true},
// show all tasks for this project task (0)
If(
ThisItem.TaskNo = 0,
And(
ProjectId = ThisItem.ProjectId,
TaskNo > 0
)
),
{
Show: true,
Expanded: true
}
)
);
The appearance of the chevron icon itself is controlled by the ‘Expanded’ column.
If(
ThisItem.Expanded,
Icon.ChevronDown,
Icon.ChevronRight
)
Summary
You will find that there are other features there that I have not called out. But the general idea was to show what could be done with standard controls. I didn’t want to over complicate things by adding too much for this demo.
This is my first post to the Community Samples. Hopefully others will find this useful for their projects.
Cheers!
-Ron Larsen