marc walter

Elm example for rendering a small gantt chart

2020-04-02

I wrote an answer on stackoverflow about rendering a gantt chart in elm, which positions tasks inside a calendar-like grid, so one can easily see if a task can only be started if all tasks that it depends on were done.

I started with this simple data structure:

type alias Task =
    { id : TaskId
    , dependsOn : List TaskId
    , color : Color
    }

In order to render it, it will be transformed into a list of DrawableTask. Each drawable task contains the information, where in the grid it will be rendered.
For simplicity, I decided that each task has a length of one, but adding a length field will not be hard.

type alias DrawableTask =
    { id : TaskId, col : Int, row : Int, color : Color }

In the end, it looks like this:

In case the iframe does not load open it directly.

The transformation function toDrawableListOfTasks : List Task -> List DrawableTask will iterate over all the tasks and split them into two lists: The ones that can be immediately rendered because they have no dependencies, and the tasks that cannot be rendered yet.
The tasks that can be rendered will be plotted into the first column of the grid.
Then it will recursively iterate over all un-rendered tasks and render them one column to the right of the rightmost of their dependencies.

When no tasks are left, it will return the list and the rendering pass will traverse the list once again.

order : Temp -> List Task -> List DrawableTask
order temp todo =
    case List.partition (allDependenciesMet temp) todo of
        ( [], [] ) ->
            -- We are done and can return the list
            Dict.values temp
                |> List.sortBy .row

        ( [], _ ) ->
            Debug.todo "Error: Not all tasks can be drawn"

        ( drawableTasks, nextTodo ) ->
            let
                nextTemp =
                    List.indexedMap (toDrawable temp) drawableTasks
                        |> List.map (\t -> ( t.id, t ))
                        |> Dict.fromList
                        |> Dict.union temp
            in
            order nextTemp nextTodo

{-| This stores the partial list of gantt tasks that can already be drawn
-}
type alias Temp =
    Dict TaskId DrawableTask


{-| Returns true if all tasks that need to be rendered above this one exist in temp
-}
allDependenciesMet : Temp -> Task -> Bool
allDependenciesMet temp task =
    List.all (\id -> Dict.member id temp) task.dependsOn