<div id="paper-container"></div>
<a target="_blank" href="https://www.jointjs.com">
  <img id="logo" src="https://assets.codepen.io/7589991/jointjs-logo.svg" width="200" height="50"></img>
</a>
#paper-container {
  position: absolute;
  right: 0;
  top: 0;
  left: 0;
  bottom: 0;
}

#logo {
  position: absolute;
  bottom: 20px;
  right: 0;
}

/*  Tooltip  */
.joint-paper {
  &.dragging .joint-tooltip {
    display: none !important;
  }
  .joint-tooltip {
    background-color: var(--tooltip-color);
    border: 1px solid var(--tooltip-outline-color);
    color: #ffffff;
    font-size: 13px;
    line-height: 15px;
    font-family: sans-serif;

    &.left {
      .tooltip-arrow,
      .tooltip-arrow-mask {
        border-right-color: var(--tooltip-color);
      }
    }

    &.right {
      .tooltip-arrow,
      .tooltip-arrow-mask {
        border-left-color: var(--tooltip-color);
      }
    }

    &.top {
      .tooltip-arrow,
      .tooltip-arrow-mask {
        border-bottom-color: var(--tooltip-color);
      }
    }

    &.bottom {
      .tooltip-arrow,
      .tooltip-arrow-mask {
        border-top-color: var(--tooltip-color);
      }
    }

    &.tooltip-empty {
      display: none !important;
    }

    i {
      color: yellow;
      font-style: normal;
    }
  }
}

.connection-tooltip-content {
  display: flex;
  flex-flow: row;
  flex-direction: row;
  align-items: center;
  text-align: center;

  img {
    margin: 0 5px;
  }

  * {
    display: inline-block;
    vertical-align: middle;
  }

  > div > * {
    max-width: 100px;
    margin: 5px 0;
  }
}
View Compiled
const { dia, ui, shapes, util } = joint;

const PortGroup = {
  IN: "in",
  OUT: "out"
};

const colors = {
  red: "#ed2637",
  black: "#131e29",
  gray: "#dde6ed",
  yellow: "#f6f740",
  blue: "#00a0e9",
  white: "#ffffff"
};

const actions = [
  "read company name",
  "succeeded",
  "failed",
  "is greater than 200",
  "is less or equal than 200"
];

const elementTemplate = new joint.shapes.standard.BorderedImage({
  size: { width: 120, height: 100 },
  attrs: {
    root: {
      magnet: false
    },
    image: {
      x: "calc(w / 2 - calc(s / 2 - 20))",
      y: "calc(h / 2 - calc(s / 2 - 20))",
      width: "calc(s - 40)",
      height: "calc(s - 40)",
      // reset defaults
      refX: null,
      refY: null,
      refWidth: null,
      refHeight: null
    },
    border: {
      rx: 8,
      ry: 8,
      stroke: colors.black,
      strokeWidth: 3
    },
    background: {
      fill: colors.white
    },
    label: {
      fill: colors.black,
      fontSize: 14,
      fontWeight: "bold",
      fontFamily: "sans-serif",
      textWrap: {
        width: "calc(w + 40)",
        height: null
      }
    }
  },
  portMarkup: joint.util.svg`
      <rect @selector="portBody"
        width="20" height="20"
        x="-10" y="-10"
        fill="${colors.red}"
        stroke-width="2"
        stroke="${colors.gray}"
        transform="rotate(45)"
      />
    `,
  ports: {
    groups: {
      [PortGroup.IN]: {
        position: "left",
        attrs: {
          portBody: {
            dataTooltipPosition: "right",
            magnet: "passive"
          }
        }
      },
      [PortGroup.OUT]: {
        position: "right",
        attrs: {
          portBody: {
            dataTooltipPosition: "left",
            magnet: "active"
          }
        }
      }
    }
  }
});

const templateLink = new joint.shapes.standard.Link({
  attrs: {
    line: {
      stroke: colors.black,
      strokeWidth: 2
    }
  },
  defaultLabel: {
    markup: util.svg`
            <rect @selector="labelBody" />
            <text @selector="labelText" />
        `,
    attrs: {
      root: {
        cursor: "pointer"
      },
      labelText: {
        fill: colors.black,
        fontSize: 12,
        fontFamily: "sans-serif",
        fontWeight: "bold",
        textAnchor: "middle",
        textVerticalAnchor: "middle",
        textWrap: {
          width: 100,
          height: null
        }
      },
      labelBody: {
        rx: 4,
        ry: 4,
        ref: "labelText",
        x: "calc(x - 4)",
        y: "calc(y - 4)",
        width: "calc(w + 8)",
        height: "calc(h + 8)",
        fill: colors.white,
        stroke: colors.black,
        strokeWidth: 2
      }
    }
  }
});

const graph = new dia.Graph({}, { cellNamespace: shapes });
const paper = new dia.Paper({
  model: graph,
  cellViewNamespace: shapes,
  width: "100%",
  height: "100%",
  gridSize: 20,
  async: true,
  sorting: dia.Paper.sorting.APPROX,
  background: { color: colors.gray },
  linkPinning: false,
  snapLinks: true,
  interactive: { linkMove: false, labelMove: false },
  defaultConnectionPoint: { name: "boundary" },
  clickThreshold: 5,
  magnetThreshold: "onleave",
  markAvailable: true,
  highlighting: {
    connecting: false,
    magnetAvailability: {
      name: "mask",
      options: {
        padding: 1,
        attrs: {
          stroke: colors.blue,
          "stroke-width": 4
        }
      }
    }
  },
  defaultLink: () =>
    createLink(actions[Math.floor(Math.random() * actions.length)]),

  validateMagnet: (sourceView, sourceMagnet) => {
    const sourceGroup = sourceView.findAttribute("port-group", sourceMagnet);
    const sourcePort = sourceView.findAttribute("port", sourceMagnet);
    const source = sourceView.model;
    if (sourceGroup !== PortGroup.OUT) {
      // 'It's not possible to create a link from an inbound port.'
      return false;
    }
    if (getPortLinks(source, sourcePort, false).length > 0) {
      // 'The port has already an inbound link (we allow only one link per port)'
      return false;
    }
    return true;
  },
  validateConnection: (sourceView, sourceMagnet, targetView, targetMagnet) => {
    if (sourceView === targetView) {
      // Do not allow a loop link (starting and ending at the same element)/
      return false;
    }
    const targetGroup = targetView.findAttribute("port-group", targetMagnet);
    const targetPort = targetView.findAttribute("port", targetMagnet);
    const target = targetView.model;
    if (target.isLink()) {
      // We allow connecting only links with elements (not links with links).
      return false;
    }
    if (targetGroup !== PortGroup.IN) {
      // It's not possible to add inbound links to output ports (only outbound links are allowed).
      return false;
    }
    if (getPortLinks(target, targetPort, true).length > 0) {
      // The port has already an inbound link (we allow 1 link per port inbound port)
      return false;
    }
    // This is a valid connection.
    return true;
  }
});

document.getElementById("paper-container").appendChild(paper.el);

// Set tooltip colors to match the demo colors.

paper.el.style.setProperty("--tooltip-color", colors.black);
paper.el.style.setProperty("--tooltip-outline-color", colors.gray);

// Create example elements and links.

const webflow1 = createElement(
  "https://assets.codepen.io/7589991/webflow.svg",
  "Webflow form is submitted"
)
  .set("service", "Webflow")
  .addPorts([
    { group: PortGroup.IN, id: "in1" },
    { group: PortGroup.OUT, id: "out1" }
  ])
  .position(100, 140);

const clearbit1 = createElement(
  "https://logo.clearbit.com/clearbit.com",
  "Retrieve number of employees"
)
  .set("service", "Clearbit")
  .addPorts([
    { group: PortGroup.IN, id: "in1" },
    { group: PortGroup.OUT, id: "out1" },
    { group: PortGroup.OUT, id: "out2" }
  ])
  .position(400, 140);

const slack1 = createElement(
  "https://assets.codepen.io/7589991/slack.svg",
  "Send Message"
)
  .set("service", "Slack")
  .addPorts([
    { group: PortGroup.IN, id: "in1" },
    { group: PortGroup.OUT, id: "out1" }
  ])
  .position(700, 46);

const gmail1 = createElement(
  "https://assets.codepen.io/7589991/gmail.svg",
  "Send Email"
)
  .set("service", "Gmail")
  .addPorts([
    { group: PortGroup.IN, id: "in1" },
    { group: PortGroup.OUT, id: "out1" }
  ])
  .position(700, 240);

const link0 = createLink()
  .set({
    source: { x: 50, y: 190 },
    target: { id: webflow1.id, port: "in1" }
  })
  .attr({
    line: {
      sourceMarker: {
        type: "circle",
        fill: colors.red,
        stroke: colors.gray,
        "stroke-width": 2,
        r: 5
      }
    }
  });

const link1 = createLink(actions[0]).set({
  source: { id: webflow1.id, port: "out1" },
  target: { id: clearbit1.id, port: "in1" }
});

const link2 = createLink(actions[3]).set({
  source: { id: clearbit1.id, port: "out1" },
  target: { id: slack1.id, port: "in1" }
});

const link3 = createLink(actions[4]).set({
  source: { id: clearbit1.id, port: "out2" },
  target: { id: gmail1.id, port: "in1" }
});

graph.addCells([
  webflow1,
  clearbit1,
  slack1,
  gmail1,
  link0,
  link1,
  link2,
  link3
]);

// Show a tooltip when the user clicks on a link. The tooltip shows information
// about the source and target elements.

const portTooltip = new ui.Tooltip({
  rootTarget: paper.svg,
  target: "[port]",
  container: paper.el,
  content: function (portNode) {
    const message = (text) => {
      portTooltip.el.classList.toggle("tooltip-empty", !text);
      if (!text) return false;
      return text;
    };

    const view = paper.findView(portNode);
    if (!view) return;
    const element = view.model;
    const portId = view.findAttribute("port", portNode);
    const portGroup = view.findAttribute("port-group", portNode);
    switch (portGroup) {
      case PortGroup.IN: {
        const [link] = getPortLinks(element, portId, true);
        if (!link) return message("");
        const sourceElement = link.getSourceElement();
        if (!sourceElement)
          return message(
            `<i>${element.attr(
              "label/text"
            )}</i> is the initiating event of the flow.`
          );
        return message(getContentFromConnection(sourceElement, element, link));
      }
      case PortGroup.OUT: {
        const [link] = getPortLinks(element, portId, false);
        if (!link) return message("");
        const targetElement = link.getTargetElement();
        if (!targetElement)
          return message(
            `<i>${element.attr(
              "label/text"
            )}</i> is the final event of the flow.`
          );
        return message(getContentFromConnection(element, targetElement, link));
      }
    }
    return message("");
  },
  direction: "auto",
  padding: 10
});

// Show the image source in the tooltip when the user hovers over the element image.

const serviceTooltip = new joint.ui.Tooltip({
  rootTarget: paper.svg,
  target: "image",
  position: "bottom",
  padding: 30,
  animation: true,
  container: paper.el,
  content: function (imageNode) {
    const view = paper.findView(imageNode);
    if (!view) return;
    return `Service: <i>${view.model.get("service")}</i>`;
  }
});

// Change the link label when the user clicks on the link.

paper.on("link:pointerclick", ({ model: link }) => {
  const currentLabel = link.prop(["labels", 0, "attrs", "labelText", "text"]);
  if (!currentLabel) return;
  const index = actions.indexOf(currentLabel);
  const nextLabel = actions[(index + 1) % actions.length];
  link.prop(["labels", 0, "attrs", "labelText", "text"], nextLabel);
});

// Remove the links that are connected to the port when the user clicks on the port.

paper.on("element:magnet:pointerclick", (elementView, evt, magnet) => {
  evt.stopPropagation();
  const port = elementView.findAttribute("port", magnet);
  const portGroup = elementView.findAttribute("port-group", magnet);
  getPortLinks(
    elementView.model,
    port,
    portGroup === PortGroup.IN
  ).forEach((link) => link.remove());
});

// Hide the tooltip when the user starts dragging an element or a link.
// See CSS for the actual hiding.

paper.on("cell:pointerdown", () => {
  paper.el.classList.add("dragging");
});

paper.on("cell:pointerup", () => {
  paper.el.classList.remove("dragging");
});

// Helpers

function getContentFromConnection(source, target, link) {
  return /* xml */ `
    <div class="connection-tooltip-content">
      If
      <img src="${source.attr("image/xlinkHref")}" width="40" height="40"/>
      <div>
        <i>${source.attr("label/text")} </i>
        →
        <span>
        ${link.prop(["labels", 0, "attrs", "labelText", "text"])}
        </span>
        →
        <i>${target.attr("label/text")}</i>
      </div>
      <img src="${target.attr("image/xlinkHref")}" width="40" height="40"/>
    </div>
  `;
}

function getPortLinks(element, portId, inbound) {
  return graph
    .getConnectedLinks(element, { inbound, outbound: !inbound })
    .filter((link) => {
      const port = inbound ? link.target() : link.source();
      return port.port === portId;
    });
}

function createLink(text) {
  const link = templateLink.clone();
  if (typeof text === "string") {
    link.labels([
      {
        attrs: {
          labelText: {
            fill: colors.gray,
            fontSize: 12,
            fontWeight: "bold",
            fontFamily: "sans-serif",
            text
          },
          labelBody: {
            fill: colors.black
          }
        }
      }
    ]);
  }
  return link;
}

function createElement(xlinkHref, text) {
  return elementTemplate.clone().attr({
    image: {
      xlinkHref
    },
    label: {
      text
    }
  });
}

External CSS

  1. https://resources.jointjs.com/demos/rappid/build/package/rappid.css

External JavaScript

  1. https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.21/lodash.min.js
  2. https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js
  3. https://cdnjs.cloudflare.com/ajax/libs/backbone.js/1.4.0/backbone-min.js
  4. https://resources.jointjs.com/demos/rappid/build/package/rappid.js