Making overlapping tasks stand out
Sometimes tasks are overlapping in your Gantt application and you would like users to be aware of the condition so they can react to it. This post is a continuation and rework of a previous post on the ILOG Elixir forum. I am developing the topic here because the answer on the forum was incomplete:
- It did not provide continuous feedback on the other tasks. The task being edited was correctly rendered, but hovered tasks where not updated until the mouse button was released. To work efficiently users need a continuous feedback.
- The display was incorrect when reassigning a task. The tasks associated to the original resource needed to be refreshed, and there was no continuous feedback on overlap once you dragged the task to another resource.
A look at the result:
You can move, resize and reassign tasks and see the continuous feedback at work.
To implement this I have created the following classes:
- OverlappingRenderer — extending the default TaskItemRenderer to display a red glow over a task when it is overlapping another task,
- CustomGanttSheet — extending the GanttSheet to provide support for handling the overlapping use case, solving the above issues.
You can download the corresponding FlexBuilder project here.
Although extending the GanttSheet is not required to achieve the result, it has the advantage of encapsulating the handling of overlapping in a single place. It is very simple to use and reuse in your applications, as you can see:
<ilog:ResourceChart id="resourceChart" width="100%" height="100%"
resourceDataProvider="{resources}"
taskDataProvider="{tasks}">
<ilog:ganttSheet>
<local:CustomGanttSheet taskItemRenderer="OverlappingRenderer"/>
</ilog:ganttSheet>
</ilog:ResourceChart>
The steps to get to there are:
- Create a renderer that shows a visual clue of the overlapping condition on the task.
- Detect the overlapping condition.
- Invalidate tasks overlapped by the edited task.
- Make sure the display of tasks is correct even during reassignement.
Renderering overlapping tasks
We need a renderer that knows how to provide a visual clue on the overlapping condition of the task. For that purpose we create an OverlappingRenderer class. It simply extends the default TaskItemRenderer and overrides updateDisplayList() where drawing occurs:
override protected function updateDisplayList(unscaledWidth:Number,
unscaledHeight:Number):void
{
super.updateDisplayList(unscaledWidth, unscaledHeight);
var taskItem:TaskItem = data as TaskItem;
var ganttSheet:CustomGanttSheet = taskItem.ganttSheet as CustomGanttSheet;
if (ganttSheet.hasOverlappingSiblings(taskItem))
{
var filter:GlowFilter = new GlowFilter();
filter.blurX = 4;
filter.blurY = 4;
filter.color = 0xF00000;
filter.alpha = 1;
filter.strength = 5;
filter.inner = true;
filters = [ filter ];
}
else
filters = null;
}
The call to super.updateDisplayList() creates the default rendering of the task as provided by the superclass. Then we check for overlapping siblings, and either: add a GlowFilter to display an inner red glow on overlapping tasks, or remove the glow when the task is not overlapping. Of course you can use other effects for rendering the condition.
Detecting overlap and refreshing the other tasks
I am not going to get into all the details of how things work, but I’ll sketch the steps. Those interested can read the attached source of the CustomGanttSheet class which is documented.
To provide continuous update of the tasks below the task being edited, we need to:
- Listen to each step of task editing, from the start to the end of the gesture,
- Refresh the display of tasks overlapped by the task being edited — not only those that are currently being overlapped, but also those that were overlapped but are no longer.
We also need to provide a method hasOverlappingSiblings(taskItem:TaskItem):Boolean for use by the renderer.
Listening to task editing steps
The GanttSheet dispatches events for all steps during the editing of tasks: itemEditMove, itemEditResize and itemEditReassign. The CustomGanttSheet class listens to these events and invalidate the renderers of interest:
addEventListener(GanttSheetEvent.ITEM_EDIT_MOVE, handleItemEditMove,
false, EventPriority.DEFAULT_HANDLER - 1);
addEventListener(GanttSheetEvent.ITEM_EDIT_RESIZE, handleItemEditResize,
false, EventPriority.DEFAULT_HANDLER - 1);
addEventListener(GanttSheetEvent.ITEM_EDIT_REASSIGN, handleItemEditReassign,
false, EventPriority.DEFAULT_HANDLER - 1);
The handlers are installed with a lower priority than the default handler to make sure they are executed after the default handlers. Doing so allows the adjustements to the values of start time and end time to occur in your own event handlers or in the default handlers. Thus the detection of overlapping is done using the actual values.
Invalidating tasks renderers
We have the triggers. Now we need to refresh some tasks.
How? We refresh the display for the tasks by calling invalidateDisplayList() on the renderers of these tasks — this makes sure that updateDisplayList() is called for these renderers.
Which tasks to refresh? To simplify things we refresh all siblings of the edited task: the current siblings, but also the original siblings when the task is reassigned:
private function handleItemEditMove(event:GanttSheetEvent):void {
invalidateTasksOfResource(TaskItem(event.itemRenderer.data).resource);
}
private function handleItemEditResize(event:GanttSheetEvent):void {
invalidateTasksOfResource(TaskItem(event.itemRenderer.data).resource);
}
private function handleItemEditReassign(event:GanttSheetEvent):void {
var taskItem:TaskItem = TaskItem(event.itemRenderer.data);
invalidateTasksOfResource(taskItem.resource);
if (taskItem.resource != editedTaskItemOriginalResource)
// Also refresh the tasks of the original resource
invalidateTasksOfResource(editedTaskItemOriginalResource);
}
public function invalidateTasksOfResource(resource:Object):void {
var siblings:Array = getTasks(resource);
if (!siblings)
return;
// Invalidate the item renderers of the siblings tasks
for each (var task:Object in siblings) {
var r:IListItemRenderer = itemToItemRenderer(task);
if (r is IInvalidating)
IInvalidating(r).invalidateDisplayList();
}
}
The tricky part is to retrieve the siblings of the task, that is: the tasks associated to the same resource.
Using ResourceChart.getTasks(taskItem.resource) is a good starting point, but it is not enough. Indeed this API is working with the state of the relationship in the data providers, while we need to get the state of the relationship during editing. More specificaly we need to take into account the reassignement of task that is being performed by the end user.
This is implemented by the method CustomGanttSheet.getTasks():
public function getTasks(resource:Object):Array {
var tasks:Array = resourceChart.getTasks(resource);
// Do not modify the internal returned array.
tasks = tasks ? tasks.concat() : [];
// The return value of ResourceChart.getTasks() is based on the state
// of the data items in the data providers. During editing of tasks the
// state of the relationship between resources and tasks may be
// different from the state in the data providers, specifically when the
// task is reassigned to a different resource.
// The proper rendering of the overlapping condition requires that we
// consider the current editing state of the task items.
if (editedTaskItem == null) {
// Not editing tasks.
return tasks;
}
if (editedTaskItem.resource == editedTaskItemOriginalResource) {
// The task being edited is associated to the same resource as in the
// data provider: the editing state of the relationship is the same as
// the data provider state.
return tasks;
}
if (resource == editedTaskItem.resource) {
// We are looking for the tasks associated with the resource that is
// now associated to the task being edited.
tasks.push(editedTaskItem.data);
return tasks;
}
if (resource == editedTaskItemOriginalResource) {
// We are looking for the tasks associated with the resource that
// was originally associated to the task being edited: the edited task
// is no longer associated to this resource.
var index:int = tasks.indexOf(editedTaskItem.data);
if (index != -1)
tasks.splice(index, 1);
return tasks;
}
return tasks;
}
Note that editedTaskItem and editedTaskItemOriginalResource are respectively the item that is being edited, and the resource originaly associated to the item being edited. These variables are set and reset by handlers of the itemEditBegin and itemEditEnd GanttSheet events.
Conclusion
We now have a custom GanttSheet that can can detect overlapping conditions on tasks, and that triggers refresh of overlapped tasks whenever a task is moved, resized or reassigned. This class can be seamlessly integrated into your application. It is also very easy to customize your task item renderer to render the overlapping condition.
I hope that in the process you learned something on the item editing events of the GanttSheet, on renderers and on refreshing them.








June 19th, 2008 at 10:16 am
Bravo!!, that’s what i want for my gant Application, excuse me, could you give me more examples or references about this cool component, great work thanks
June 20th, 2008 at 6:51 am
@Josue
In case you are new to ILOG Elixir the best place to start is: http://www.ilog.com/dev/ilogelixir/
From there you’ll find the demos, online documentation, forum and of course the installer.
The installation comes with the documentation and a number of code examples. I encourage you to have a look at the forum as well. It is the proper place to ask questions on the feasibility of specific requirements and to get hints on how to. Other users may have already asked for hints on features you are looking for.
Frédéric