exupero's blog

Gantt charts of diverging possibilities

In the previous post we set up a basic Gantt chart using a schedule defined by relative events. In this post we'll adapt that approach to handle some of the uncertainty a schedule can have.

In the original example, we had to pick the relationships between the tasks, but rather than insisting that design must wait until planning is almost complete, let's consider a more aggressive schedule in which design can begin earlier. We could just change the schedule, but it's often helpful to capture multiple scenarios in one place rather than a history of decisions, so let's just add a couple new facts to the list of events:

(def events
  '[[plan :starts 0]
    [plan :ends [plan :starts + 3]]
    [design :starts [plan :starts + 2]]
    [design :ends [design :starts + 5]]
    [design :starts [plan :starts + 1]]
    [coding :starts [design :starts + 3]]
    [coding :ends [coding :starts + 7]]
    [testing :starts [coding :starts + 1]]
    [testing :ends [coding :ends + 1]]
    [user-testing :starts [design :starts + 2]]
    [user-testing :ends [coding :ends + 2]]])

Unfortunately, our absolute time calculation only finds the first design start, never the second:

(map (fn [[entity attribute]]
       [entity attribute (absolute-time [entity attribute] events)])
([plan :starts 0]
 [plan :ends 3]
 [design :starts 2]
 [design :ends 7]
 [design :starts 2]
 [coding :starts 5]
 [coding :ends 12]
 [testing :starts 6]
 [testing :ends 13]
 [user-testing :starts 4]
 [user-testing :ends 14])

Let's create a version of absolute-time that calculates all possibilities:

(defn absolute-times [[entity attribute] events]
      (filter #(= [entity attribute] (take 2 %)))
      (mapcat (fn [[_ _ event]]
                (if (number? event)
                  (let [[entity2 attribute2 f d] event
                        f (eval f)]
                    (map (fn [n]
                           (f n d))
                         (absolute-times [entity2 attribute2] events)))))))
(map (fn [[entity attribute]]
       [entity attribute (absolute-times [entity attribute] events)])
([plan :starts (0)]
 [plan :ends (3)]
 [design :starts (2 1)]
 [design :ends (7 6)]
 [design :starts (2 1)]
 [coding :starts (5 4)]
 [coding :ends (12 11)]
 [testing :starts (6 5)]
 [testing :ends (13 12)]
 [user-testing :starts (4 3)]
 [user-testing :ends (14 13)])

The planning phase still only has one possible timespan, but the design phase and everything that depends on it could now happen over multiple intervals; the single reality of the original schedule has forked into two different realities.

It's not so easy to see which spans are a consequence of which design scenario. It would help to know how we got to a particular conclusion. Let's track which branches are taken anytime there's more than one option:

(defn absolute-times-with-branches [[entity attribute] events]
  (let [matches (filter #(= [entity attribute] (take 2 %)) events)
        branch-point? (< 1 (count matches))]
    (mapcat (fn [[_ _ event :as branch-point]]
              (if (number? event)
                  (if branch-point?
                (let [[entity2 attribute2 f d] event
                      f (eval f)]
                  (map (fn [[n branch-points]]
                         [(f n d)
                          (if branch-point?
                            (conj branch-points branch-point)
                       (absolute-times-with-branches [entity2 attribute2] events)))))

I've used the definition of the event as the name of the branch. Whenever we hit a branching point, we add the name of the branch to a list, keeping a record of the path we've taken.

Here's the result:

  (map (fn [[entity attribute]]
         [entity attribute (absolute-times-with-branches [entity attribute] events)])
([plan :starts ([0 []])]
 [plan :ends ([3 []])]
  ([2 [[design :starts [plan :starts + 2]]]]
   [1 [[design :starts [plan :starts + 1]]]])]
  ([7 [[design :starts [plan :starts + 2]]]]
   [6 [[design :starts [plan :starts + 1]]]])]
  ([5 [[design :starts [plan :starts + 2]]]]
   [4 [[design :starts [plan :starts + 1]]]])]
  ([12 [[design :starts [plan :starts + 2]]]]
   [11 [[design :starts [plan :starts + 1]]]])]
  ([6 [[design :starts [plan :starts + 2]]]]
   [5 [[design :starts [plan :starts + 1]]]])]
  ([13 [[design :starts [plan :starts + 2]]]]
   [12 [[design :starts [plan :starts + 1]]]])]
  ([4 [[design :starts [plan :starts + 2]]]]
   [3 [[design :starts [plan :starts + 1]]]])]
  ([14 [[design :starts [plan :starts + 2]]]]
   [13 [[design :starts [plan :starts + 1]]]])])

Restructuring the data into something we can draw is a bit awkward:

(def data
    (for [event events
          result (absolute-times-with-branches event events)]
      (concat (take 2 event) result))
    (group-by first)
    (map (fn [[entity events]]
            (->> events
              (map #(drop 1 %))
              (group-by #(nth % 2))
              (map (fn [[branch data]]
                     [branch (into {} (map #(vec (take 2 %))) data)]))
              (into {}))]))
    (mapcat (fn [[entity branches]]
              (map (fn [[branch events]]
                     [entity events branch])
    (sort-by #(nth % 2))))
([plan {:starts 0, :ends 3} []]
 [design {:starts 1, :ends 6} [[design :starts [plan :starts + 1]]]]
 [coding {:starts 4, :ends 11} [[design :starts [plan :starts + 1]]]]
 [testing {:starts 5, :ends 12} [[design :starts [plan :starts + 1]]]]
  {:starts 3, :ends 13}
  [[design :starts [plan :starts + 1]]]]
 [design {:starts 2, :ends 7} [[design :starts [plan :starts + 2]]]]
 [coding {:starts 5, :ends 12} [[design :starts [plan :starts + 2]]]]
 [testing {:starts 6, :ends 13} [[design :starts [plan :starts + 2]]]]
  {:starts 4, :ends 14}
  [[design :starts [plan :starts + 2]]]])

Now we can draw a Gantt chart that uses different colors for different branches of the scenario tree:


In the next post we'll explore another way to represent uncertainty in a Gantt chart.