docs: def trace3D_notebook(self):
          """Same than trace3D, used when ran from Jupyther notebooks"""
          trace = self.get_trace()
          import vpython
  
          class IPsphere(vpython.sphere):
              def __init__(self, ip, **kargs):
                  vpython.sphere.__init__(self, **kargs)
                  self.ip = ip
                  self.label = None
                  self.setlabel(self.ip)
                  self.last_clicked = None
                  self.full = False
                  self.savcolor = vpython.vec(*self.color.value)
  
              def fullinfos(self):
                  self.full = True
                  self.color = vpython.vec(1, 0, 0)
                  a, b = sr(IP(dst=self.ip) / TCP(dport=[21, 22, 23, 25, 80, 443], flags="S"), timeout=2, verbose=0)  # noqa: E501
                  if len(a) == 0:
                      txt = "%s:\nno results" % self.ip
                  else:
                      txt = "%s:\n" % self.ip
                      for s, r in a:
                          txt += r.sprintf("{TCP:%IP.src%:%TCP.sport% %TCP.flags%}{TCPerror:%IPerror.dst%:%TCPerror.dport% %IP.src% %ir,ICMP.type%}\n")  # noqa: E501
                  self.setlabel(txt, visible=1)
  
              def unfull(self):
                  self.color = self.savcolor
                  self.full = False
                  self.setlabel(self.ip)
  
              def setlabel(self, txt, visible=None):
                  if self.label is not None:
                      if visible is None:
                          visible = self.label.visible
                      self.label.visible = 0
                  elif visible is None:
                      visible = 0
                  self.label = vpython.label(text=txt, pos=self.pos, space=self.radius, xoffset=10, yoffset=20, visible=visible)  # noqa: E501
  
              def check_double_click(self):
                  try:
                      if self.full or not self.label.visible:
                          return False
                      if self.last_clicked is not None:
                          return (time.time() - self.last_clicked) < 0.5
                      return False
                  finally:
                      self.last_clicked = time.time()
  
              def action(self):
                  self.label.visible ^= 1
                  if self.full:
                      self.unfull()
  
          vpython.scene = vpython.canvas()
          vpython.scene.title = "<center><u><b>%s</b></u></center>" % self.listname  # noqa: E501
          vpython.scene.append_to_caption(
              re.sub(
                  r'\%(.*)\%',
                  r'<span style="color: red">\1</span>',
                  re.sub(
                      r'\`(.*)\`',
                      r'<span style="color: #3399ff">\1</span>',
                      """<u><b>Commands:</b></u>
  %Click% to toggle information about a node.
  %Double click% to perform a quick web scan on this node.
  <u><b>Camera usage:</b></u>
  `Right button drag or Ctrl-drag` to rotate "camera" to view scene.
  `Shift-drag` to move the object around.
  `Middle button or Alt-drag` to drag up or down to zoom in or out.
    On a two-button mouse, `middle is wheel or left + right`.
  Touch screen: pinch/extend to zoom, swipe or two-finger rotate."""
                  )
              )
          )
          vpython.scene.exit = True
          start = vpython.box()
          rings = {}
          tr3d = {}
          for i in trace:
              tr = trace[i]
              tr3d[i] = []
              for t in range(1, max(tr) + 1):
                  if t not in rings:
                      rings[t] = []
                  if t in tr:
                      if tr[t] not in rings[t]:
                          rings[t].append(tr[t])
                      tr3d[i].append(rings[t].index(tr[t]))
                  else:
                      rings[t].append(("unk", -1))
                      tr3d[i].append(len(rings[t]) - 1)
  
          for t in rings:
              r = rings[t]
              tmp_len = len(r)
              for i in range(tmp_len):
                  if r[i][1] == -1:
                      col = vpython.vec(0.75, 0.75, 0.75)
                  elif r[i][1]:
                      col = vpython.color.green
                  else:
                      col = vpython.color.blue
  
                  s = IPsphere(pos=vpython.vec((tmp_len - 1) * vpython.cos(2 * i * vpython.pi / tmp_len), (tmp_len - 1) * vpython.sin(2 * i * vpython.pi / tmp_len), 2 * t),  # noqa: E501
                               ip=r[i][0],
                               color=col)
                  for trlst in six.itervalues(tr3d):
                      if t <= len(trlst):
                          if trlst[t - 1] == i:
                              trlst[t - 1] = s
          forecol = colgen(0.625, 0.4375, 0.25, 0.125)
          for trlst in six.itervalues(tr3d):
              col = vpython.vec(*next(forecol))
              start = vpython.vec(0, 0, 0)
              for ip in trlst:
                  vpython.cylinder(pos=start, axis=ip.pos - start, color=col, radius=0.2)  # noqa: E501
                  start = ip.pos
  
          vpython.rate(50)
  
          # Keys handling
          # TODO: there is currently no way of closing vpython correctly
          # https://github.com/BruceSherwood/vpython-jupyter/issues/36
          # def keyboard_press(ev):
          #     k = ev.key
          #     if k == "esc" or k == "q":
          #         pass  # TODO: close
          #
          # vpython.scene.bind('keydown', keyboard_press)
  
          # Mouse handling
          def mouse_click(ev):
              if ev.press == "left":
                  o = vpython.scene.mouse.pick
                  if o and isinstance(o, IPsphere):
                      if o.check_double_click():
                          if o.ip == "unk":
                              return
                          o.fullinfos()
                      else:
                          o.action()
  
          vpython.scene.bind('mousedown', mouse_click)