diff --git a/tools/task_plots/iplot_tasks.py b/tools/task_plots/iplot_tasks.py
new file mode 100755
index 0000000000000000000000000000000000000000..c6f44dcf9d60338e9f738ce235e3e1a16327f498
--- /dev/null
+++ b/tools/task_plots/iplot_tasks.py
@@ -0,0 +1,543 @@
+#!/usr/bin/env python
+"""
+Interactive plot of a task dump.
+
+Usage:
+    iplot_tasks.py [options] input.dat
+
+where input.dat is a thread info file for a step.  Use the '-y interval' flag
+of the swift or swift_mpi commands to create these (these will need to be
+built with the --enable-task-debugging configure option).
+
+The task plot can be scrolled and zoomed using the standard matplotlib
+controls, the type of task at a point can be queried by a mouse click
+(unless the --motion option is in effect when a continuous readout is
+shown) the task type and tic/toc range are reported in the terminal.
+
+Requires the tkinter module.
+
+This file is part of SWIFT.
+
+Copyright (C) 2019 Peter W. Draper (p.w.draper@durham.ac.uk)
+All Rights Reserved.
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Lesser General Public License as published
+by the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>.
+"""
+
+import matplotlib
+matplotlib.use('TkAgg')
+import numpy as np
+import matplotlib.backends.backend_tkagg as tkagg
+from matplotlib.figure import Figure
+import Tkinter as tk
+import matplotlib.collections as collections
+import matplotlib.ticker as plticker
+import pylab as pl
+import sys
+import argparse
+
+#  Handle the command line.
+parser = argparse.ArgumentParser(description="Plot task graphs")
+
+parser.add_argument("input", help="Thread data file (-y output)")
+parser.add_argument(
+    "-m",
+    "--motion",
+    dest="motion",
+    help="Track mouse motion, otherwise clicks (def: clicks)",
+    default=False,
+    action="store_true",
+)
+parser.add_argument(
+    "-l",
+    "--limit",
+    dest="limit",
+    help="Upper time limit in millisecs (def: depends on data)",
+    default=0,
+    type=float,
+)
+parser.add_argument(
+    "--height",
+    dest="height",
+    help="Height of plot in inches (def: 4)",
+    default=4.0,
+    type=float,
+)
+parser.add_argument(
+    "--width",
+    dest="width",
+    help="Width of plot in inches (def: 16)",
+    default=16.0,
+    type=float,
+)
+parser.add_argument(
+    "-v",
+    "--verbose",
+    dest="verbose",
+    help="Show colour assignments and other details (def: False)",
+    default=False,
+    action="store_true",
+)
+parser.add_argument(
+    "-r",
+    "--rank",
+    dest="rank",
+    help="The rank to plot, if MPI in effect",
+    default=0,
+    type=int,
+)
+
+args = parser.parse_args()
+infile = args.input
+delta_t = args.limit
+rank = args.rank
+
+#  Basic plot configuration.
+PLOT_PARAMS = {
+    "axes.labelsize": 10,
+    "axes.titlesize": 10,
+    "font.size": 12,
+    "legend.fontsize": 12,
+    "xtick.labelsize": 10,
+    "ytick.labelsize": 10,
+    "figure.figsize": (args.width, args.height),
+    "figure.subplot.left": 0.03,
+    "figure.subplot.right": 0.995,
+    "figure.subplot.bottom": 0.1,
+    "figure.subplot.top": 0.99,
+    "figure.subplot.wspace": 0.0,
+    "figure.subplot.hspace": 0.0,
+    "lines.markersize": 6,
+    "lines.linewidth": 3.0,
+}
+pl.rcParams.update(PLOT_PARAMS)
+
+#  Tasks and subtypes. Indexed as in tasks.h.
+TASKTYPES = [
+    "none",
+    "sort",
+    "self",
+    "pair",
+    "sub_self",
+    "sub_pair",
+    "init_grav",
+    "init_grav_out",
+    "ghost_in",
+    "ghost",
+    "ghost_out",
+    "extra_ghost",
+    "drift_part",
+    "drift_spart",
+    "drift_bpart",
+    "drift_gpart",
+    "drift_gpart_out",
+    "hydro_end_force",
+    "kick1",
+    "kick2",
+    "timestep",
+    "timestep_limiter",
+    "send",
+    "recv",
+    "grav_long_range",
+    "grav_mm",
+    "grav_down_in",
+    "grav_down",
+    "grav_mesh",
+    "grav_end_force",
+    "cooling",
+    "star_formation",
+    "star_formation_in",
+    "star_formation_out",
+    "logger",
+    "stars_in",
+    "stars_out",
+    "stars_ghost_in",
+    "stars_ghost",
+    "stars_ghost_out",
+    "stars_sort",
+    "stars_resort",
+    "bh_in",
+    "bh_out",
+    "bh_ghost",
+    "fof_self",
+    "fof_pair",
+    "count",
+]
+
+SUBTYPES = [
+    "none",
+    "density",
+    "gradient",
+    "force",
+    "limiter",
+    "grav",
+    "external_grav",
+    "tend_part",
+    "tend_gpart",
+    "tend_spart",
+    "tend_bpart",
+    "xv",
+    "rho",
+    "gpart",
+    "multipole",
+    "spart",
+    "stars_density",
+    "stars_feedback",
+    "sf_counts",
+    "bpart",
+    "bh_density",
+    "bh_swallow",
+    "do_swallow",
+    "bh_feedback",
+    "count",
+]
+
+#  Task/subtypes of interest.
+FULLTYPES = [
+    "self/limiter",
+    "self/force",
+    "self/gradient",
+    "self/density",
+    "self/grav",
+    "sub_self/limiter",
+    "sub_self/force",
+    "sub_self/gradient",
+    "sub_self/density",
+    "pair/limiter",
+    "pair/force",
+    "pair/gradient",
+    "pair/density",
+    "pair/grav",
+    "sub_pair/limiter",
+    "sub_pair/force",
+    "sub_pair/gradient",
+    "sub_pair/density",
+    "recv/xv",
+    "send/xv",
+    "recv/rho",
+    "send/rho",
+    "recv/tend_part",
+    "send/tend_part",
+    "recv/tend_gpart",
+    "send/tend_gpart",
+    "recv/tend_spart",
+    "send/tend_spart",
+    "recv/tend_bpart",
+    "send/tend_bpart",
+    "recv/gpart",
+    "send/gpart",
+    "recv/spart",
+    "send/spart",
+    "send/sf_counts",
+    "recv/sf_counts",
+    "recv/bpart",
+    "send/bpart",
+    "self/stars_density",
+    "pair/stars_density",
+    "sub_self/stars_density",
+    "sub_pair/stars_density",
+    "self/stars_feedback",
+    "pair/stars_feedback",
+    "sub_self/stars_feedback",
+    "sub_pair/stars_feedback",
+    "self/bh_density",
+    "pair/bh_density",
+    "sub_self/bh_density",
+    "sub_pair/bh_density",
+    "self/bh_swallow",
+    "pair/bh_swallow",
+    "sub_self/bh_swallow",
+    "sub_pair/bh_swallow",
+    "self/do_swallow",
+    "pair/do_swallow",
+    "sub_self/do_swallow",
+    "sub_pair/do_swallow",
+    "self/bh_feedback",
+    "pair/bh_feedback",
+    "sub_self/bh_feedback",
+    "sub_pair/bh_feedback",
+]
+
+#  A number of colours for the various types. Recycled when there are
+#  more task types than colours...
+colours = [
+    "cyan",
+    "lightgray",
+    "darkblue",
+    "yellow",
+    "tan",
+    "dodgerblue",
+    "sienna",
+    "aquamarine",
+    "bisque",
+    "blue",
+    "green",
+    "lightgreen",
+    "brown",
+    "purple",
+    "moccasin",
+    "olivedrab",
+    "chartreuse",
+    "olive",
+    "darkgreen",
+    "green",
+    "mediumseagreen",
+    "mediumaquamarine",
+    "darkslategrey",
+    "mediumturquoise",
+    "black",
+    "cadetblue",
+    "skyblue",
+    "red",
+    "slategray",
+    "gold",
+    "slateblue",
+    "blueviolet",
+    "mediumorchid",
+    "firebrick",
+    "magenta",
+    "hotpink",
+    "pink",
+    "orange",
+    "lightgreen",
+]
+maxcolours = len(colours)
+
+#  Set colours of task/subtype.
+TASKCOLOURS = {}
+ncolours = 0
+for task in TASKTYPES:
+    TASKCOLOURS[task] = colours[ncolours]
+    ncolours = (ncolours + 1) % maxcolours
+
+SUBCOLOURS = {}
+for task in FULLTYPES:
+    SUBCOLOURS[task] = colours[ncolours]
+    ncolours = (ncolours + 1) % maxcolours
+
+for task in SUBTYPES:
+    SUBCOLOURS[task] = colours[ncolours]
+    ncolours = (ncolours + 1) % maxcolours
+
+#  For fiddling with colours...
+if args.verbose:
+    print("#Selected colours:")
+    for task in sorted(TASKCOLOURS.keys()):
+        print(("# " + task + ": " + TASKCOLOURS[task]))
+    for task in sorted(SUBCOLOURS.keys()):
+        print(("# " + task + ": " + SUBCOLOURS[task]))
+
+#  Read input.
+data = pl.loadtxt(infile)
+
+#  Do we have an MPI file?
+full_step = data[0, :]
+if full_step.size == 13:
+    print("# MPI mode")
+    mpimode = True
+    ranks = list(range(int(max(data[:, 0])) + 1))
+    print(("# Number of ranks:", len(ranks)))
+    rankcol = 0
+    threadscol = 1
+    taskcol = 2
+    subtaskcol = 3
+    ticcol = 5
+    toccol = 6
+else:
+    print("# non MPI mode")
+    mpimode = False
+    rankcol = -1
+    threadscol = 0
+    taskcol = 1
+    subtaskcol = 2
+    ticcol = 4
+    toccol = 5
+
+#  Get CPU_CLOCK to convert ticks into milliseconds.
+CPU_CLOCK = float(full_step[-1]) / 1000.0
+if args.verbose:
+    print(("# CPU frequency:", CPU_CLOCK * 1000.0))
+
+nthread = int(max(data[:, threadscol])) + 1
+print(("# Number of threads:", nthread))
+
+# Avoid start and end times of zero.
+sdata = data[data[:, ticcol] != 0]
+sdata = sdata[sdata[:, toccol] != 0]
+
+# Calculate the data range, if not given.
+delta_t = delta_t * CPU_CLOCK
+if delta_t == 0:
+    if mpimode:
+        data = sdata[sdata[:, rankcol] == rank]
+        full_step = data[0, :]
+
+    tic_step = int(full_step[ticcol])
+    toc_step = int(full_step[toccol])
+    dt = toc_step - tic_step
+    if dt > delta_t:
+        delta_t = dt
+    print(("# Data range: ", delta_t / CPU_CLOCK, "ms"))
+
+# Once more doing the real gather and plots this time.
+if mpimode:
+    data = sdata[sdata[:, rankcol] == rank]
+    full_step = data[0, :]
+tic_step = int(full_step[ticcol])
+toc_step = int(full_step[toccol])
+print(("# Min tic = ", tic_step))
+data = data[1:, :]
+
+# Exit if no data.
+if data.size == 0:
+    print(("# Rank ", rank, " has no tasks"))
+    os.exit(1)
+
+start_t = float(tic_step)
+data[:, ticcol] -= start_t
+data[:, toccol] -= start_t
+end_t = (toc_step - start_t) / CPU_CLOCK
+
+tasks = {}
+tasks[-1] = []
+for i in range(nthread):
+    tasks[i] = []
+
+num_lines = pl.shape(data)[0]
+for line in range(num_lines):
+    thread = int(data[line, threadscol])
+
+    tasks[thread].append({})
+    tasktype = TASKTYPES[int(data[line, taskcol])]
+    subtype = SUBTYPES[int(data[line, subtaskcol])]
+    tasks[thread][-1]["type"] = tasktype
+    tasks[thread][-1]["subtype"] = subtype
+    tic = int(data[line, ticcol]) / CPU_CLOCK
+    toc = int(data[line, toccol]) / CPU_CLOCK
+    tasks[thread][-1]["tic"] = tic
+    tasks[thread][-1]["toc"] = toc
+    if "fof" in tasktype:
+        tasks[thread][-1]["colour"] = TASKCOLOURS[tasktype]
+    elif ("self" in tasktype) or ("pair" in tasktype) or ("recv" in tasktype) or ("send" in tasktype):
+        fulltype = tasktype + "/" + subtype
+        if fulltype in SUBCOLOURS:
+            tasks[thread][-1]["colour"] = SUBCOLOURS[fulltype]
+        else:
+            tasks[thread][-1]["colour"] = SUBCOLOURS[subtype]
+    else:
+        tasks[thread][-1]["colour"] = TASKCOLOURS[tasktype]
+
+# Do the plotting.
+fig = Figure()
+ax = fig.add_subplot(1, 1, 1)
+ax.set_xlim(-delta_t * 0.01 / CPU_CLOCK, delta_t * 1.01 / CPU_CLOCK)
+ax.set_ylim(0.5, nthread + 1.0)
+
+ltics = []
+ltocs = []
+llabels = []
+for i in range(nthread):
+
+    #  Collect ranges and colours into arrays. Also indexed lists for lookup tables.
+    tictocs = []
+    colours = []
+    tics = []
+    tocs = []
+    labels = []
+    for task in tasks[i]:
+        tictocs.append((task["tic"], task["toc"] - task["tic"]))
+        colours.append(task["colour"])
+
+        tics.append(task["tic"])
+        tocs.append(task["toc"])
+        labels.append(task["type"] + "/" + task["subtype"])
+
+    #  Add to look up tables.
+    ltics.append(tics)
+    ltocs.append(tocs)
+    llabels.append(labels)
+
+    #  Now plot.
+    ax.broken_barh(tictocs, [i + 0.55, 0.9], facecolors=colours, linewidth=0)
+
+# Start and end of time-step
+ax.plot([0, 0], [0, nthread + 1], "k--", linewidth=1)
+ax.plot([end_t, end_t], [0, nthread + 1], "k--", linewidth=1)
+
+# Labels.
+ax.set_xlabel("Wall clock time [ms]")
+ax.set_ylabel("Thread ID")
+
+loc = plticker.MultipleLocator(base=1)
+ax.yaxis.set_major_locator(loc)
+ax.grid(True, which="major", axis="y", linestyle="-")
+
+class Container:
+    def __init__(self,  window, figure, motion, nthread, ltics, ltocs, llabels):
+        self.window = window
+        self.figure = figure
+        self.motion = motion
+        self.nthread = nthread
+        self.ltics = ltics
+        self.ltocs = ltocs
+        self.llabels = llabels
+
+    def plot(self):
+        canvas = tkagg.FigureCanvasTkAgg(self.figure, master=self.window)
+        wcanvas = canvas.get_tk_widget()
+        wcanvas.config(width=1000, height=300)
+        wcanvas.pack(side=tk.TOP, expand=True, fill=tk.BOTH)
+
+        toolbar = tkagg.NavigationToolbar2TkAgg(canvas, self.window)
+        toolbar.update()
+        self.output = tk.StringVar()
+        label = tk.Label(self.window, textvariable=self.output, bg="white", fg="red", bd=2)
+        label.pack(side=tk.RIGHT, expand=True, fill=tk.X)
+        wcanvas.pack(side=tk.TOP, expand=True, fill=tk.BOTH)
+
+        canvas.draw()
+
+        # Print task type using mouse clicks or motion.
+        if self.motion:
+            fig.canvas.mpl_connect('motion_notify_event', self.onclick)
+        else:
+            fig.canvas.mpl_connect('button_press_event', self.onclick)
+
+    def onclick(self, event):
+        # Find thread, then scan for bounded task.
+        try:
+            thread = int(round(event.ydata)) - 1
+            if thread >= 0 and thread < self.nthread:
+                tics = self.ltics[thread]
+                tocs = self.ltocs[thread]
+                labels = self.llabels[thread]
+                for i in range(len(tics)):
+                    if event.xdata > tics[i] and event.xdata < tocs[i]:
+                        tic = "{0:.3f}".format(tics[i])
+                        toc = "{0:.3f}".format(tocs[i])
+                        outstr = "task =  " + labels[i] + ",  tic/toc =  " + tic + " / " + toc
+                        self.output.set(outstr)
+                        break
+        except TypeError:
+            #  Ignore out of bounds.
+            pass
+
+    def quit(self):
+        self.window.destroy()
+
+window = tk.Tk()
+window.protocol("WM_DELETE_WINDOW", window.quit)
+container = Container(window, fig, args.motion, nthread, ltics, ltocs, llabels)
+container.plot()
+window.mainloop()