docs: def render_diagram(root_task, out_base, max_param_len=20, horizontal=False, colored=False):
      """Render a diagram of the ETL pipeline
  
      All upstream tasks (i.e. requirements) of :attr:`root_task` are rendered.
  
      Nodes are, by default, styled as simple rects. This style is augmented by any
      :attr:`diagram_style` attributes of the tasks.
  
      .. note:: This function requires the 'dot' executable from the GraphViz package to be installed
                and its location configured in your `project_config.py` variable :attr:`DOT_EXECUTABLE`.
  
      Args:
          root_task (luigi.Task): Task instance that defines the 'upstream root' of the pipeline
          out_base (str): base output file name (file endings will be appended)
          max_param_len (int): Maximum shown length of task parameter values
          horizontal (bool): If True, layout graph left-to-right instead of top-to-bottom
          colored (bool): If True, show task completion status by color of nodes
      """
      import re
      import codecs
      import subprocess
      from ozelot import config
      from ozelot.etl.tasks import get_task_name, get_task_param_string
  
      # the graph - lines in dot file
      lines = [u"digraph G {"]
  
      if horizontal:
          lines.append(u"rankdir=LR;")
  
      # helper function: make unique task id from task name and parameters:
      # task name + parameter string, with spaces replaced with _ and all non-alphanumerical characters stripped
      def get_id(task):
          s = get_task_name(task) + "_" + get_task_param_string(task)
          return re.sub(r'\W+', '', re.sub(' ', '_', s))
  
      # node names of tasks that have already been added to the graph
      existing_nodes = set()
  
      # edge sets (tuples of two node names) that have already been added
      existing_edges = set()
  
      # recursion function for generating the pipeline graph
      def _build(task, parent_id=None):
          tid = get_id(task)
  
          # add node if it's not already there
          if tid not in existing_nodes:
              # build task label: task name plus dictionary of parameters as table
  
              params = task.to_str_params()
              param_list = ""
              for k, v in params.items():
                  # truncate param value if necessary, and add "..."
                  if len(v) > max_param_len:
                      v = v[:max_param_len] + "..."
                  param_list += "<TR><TD ALIGN=\"LEFT\">" \
                                "<FONT POINT-SIZE=\"10\">{:s}</FONT>" \
                                "</TD><TD ALIGN=\"LEFT\">" \
                                "<FONT POINT-SIZE=\"10\">{:s}</FONT>" \
                                "</TD></TR>".format(k, v)
  
              label = "<TABLE BORDER=\"0\" CELLSPACING=\"1\" CELLPADDING=\"1\">" \
                      "<TR><TD COLSPAN=\"2\" ALIGN=\"CENTER\">" \
                      "<FONT POINT-SIZE=\"12\">{:s}</FONT>" \
                      "</TD></TR>" \
                      "".format(get_task_name(task)) + param_list + "</TABLE>"
  
              style = getattr(task, 'diagram_style', [])
  
              if colored:
                  color = ', color="{:s}"'.format("green" if task.complete() else "red")
              else:
                  color = ''
  
              # add a node for the task
              lines.append(u"{name:s} [label=< {label:s} >, shape=\"rect\" {color:s}, style=\"{style:s}\"];\n"
                           u"".format(name=tid,
                                      label=label,
                                      color=color,
                                      style=','.join(style)))
  
              existing_nodes.add(tid)
  
              # recurse over requirements
              for req in task.requires():
                  _build(req, parent_id=tid)
  
          # add edge from current node to (upstream) parent, if it doesn't already exist
          if parent_id is not None and (tid, parent_id) not in existing_edges:
              lines.append(u"{source:s} -> {target:s};\n".format(source=tid, target=parent_id))
  
      # generate pipeline graph
      _build(root_task)
  
      # close the graph definition
      lines.append(u"}")
  
      # write description in DOT format
      with codecs.open(out_base + '.dot', 'w', encoding='utf-8') as f:
          f.write(u"\n".join(lines))
  
      # check existence of DOT_EXECUTABLE variable and file
      if not hasattr(config, 'DOT_EXECUTABLE'):
          raise RuntimeError("Please configure the 'DOT_EXECUTABLE' variable in your 'project_config.py'")
      if not os.path.exists(config.DOT_EXECUTABLE):
          raise IOError("Could not find file pointed to by 'DOT_EXECUTABLE': " + str(config.DOT_EXECUTABLE))
  
      # render to image using DOT
      # noinspection PyUnresolvedReferences
      subprocess.check_call([
          config.DOT_EXECUTABLE,
          '-T', 'png',
          '-o', out_base + '.png',
                out_base + '.dot'
      ])