Skip to content

Tooltip on a ForeignObject

If your nodes use foreignobject, you can still attach fully custom SVG tooltips and animate them on hover.

This example combines three parts:

  1. A custom foreignobject field with SVG content.
  2. Tooltip templates rendered in the chart render event.
  3. Hover handlers attached on onRedraw to animate show/hide.

Example

javascript
let drive = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48px" height="48px">
<path fill="#1e88e5" d="M38.59,39c-0.535,0.93-0.298,1.68-1.195,2.197C36.498,41.715,35.465,42,34.39,42H13.61 c-1.074,0-2.106-0.285-3.004-0.802C9.708,40.681,9.945,39.93,9.41,39l7.67-9h13.84L38.59,39z"/>
<path fill="#fbc02d" d="M27.463,6.999c1.073-0.002,2.104-0.716,3.001-0.198c0.897,0.519,1.66,1.27,2.197,2.201l10.39,17.996 c0.537,0.93,0.807,1.967,0.808,3.002c0.001,1.037-1.267,2.073-1.806,3.001l-11.127-3.005l-6.924-11.993L27.463,6.999z"/>
<path fill="#e53935" d="M43.86,30c0,1.04-0.27,2.07-0.81,3l-3.67,6.35c-0.53,0.78-1.21,1.4-1.99,1.85L30.92,30H43.86z"/>
<path fill="#4caf50" d="M5.947,33.001c-0.538-0.928-1.806-1.964-1.806-3c0.001-1.036,0.27-2.073,0.808-3.004l10.39-17.996 c0.537-0.93,1.3-1.682,2.196-2.2c0.897-0.519,1.929,0.195,3.002,0.197l3.459,11.009l-6.922,11.989L5.947,33.001z"/>
<path fill="#1565c0" d="M17.08,30l-6.47,11.2c-0.78-0.45-1.46-1.07-1.99-1.85L4.95,33c-0.54-0.93-0.81-1.96-0.81-3H17.08z"/>
<path fill="#2e7d32" d="M30.46,6.8L24,18L17.53,6.8c0.78-0.45,1.66-0.73,2.6-0.79L27.46,6C28.54,6,29.57,6.28,30.46,6.8z"/>
</svg>`;

let tooltip = `
<g data-t-id="{id}" transform="matrix(0.001,0,0,0.001,{x},{y})">
  <path stroke="#FFCA28" fill="#fff" d="M 0,0 L -10,-10 H -85 Q -90,-10 -90,-15 V -65 Q -90,-70 -85,-70 H 85 Q 90,-70 90,-65 V -15 Q 90,-10 85,-10 H 10 L 0,0 z"></path>
  {text}
</g>`;

let tooltipText = `
<text text-anchor="middle" data-width="130" fill="#F57C00" x="0" y="-32">{val}</text>`;

OrgChart.templates.ana.html =
  '<foreignobject class="node" x="20" y="10" width="200" height="100">' +
  drive +
  '{val}</foreignobject>';

let chart = new OrgChart(document.getElementById("tree"), {
  mouseScroll: OrgChart.action.none,
  nodeMouseClick: OrgChart.action.none,
  template: "ana",
  enableSearch: false,
  nodeBinding: {
    field_0: "name",
    html: "html",
    tooltip: "tooltip"
  },
  nodeMenu: {
    details: { text: "Details" },
    edit: { text: "Edit" },
    add: { text: "Add" },
    remove: { text: "Remove" }
  }
});

function showTooltip(id) {
  let tooltipElement = document.querySelector('[data-t-id="' + id + '"]');
  if (!tooltipElement) return;

  let transformStart = OrgChart._getTransform(tooltipElement);
  let transformEnd = transformStart.slice(0);

  transformEnd[0] = 1;
  transformEnd[3] = 1;

  OrgChart.animate(
    tooltipElement,
    { transform: transformStart },
    { transform: transformEnd },
    300,
    OrgChart.anim.outBack
  );
}

function hideTooltip(id) {
  let tooltipElement = document.querySelector('[data-t-id="' + id + '"]');
  if (!tooltipElement) return;

  let transformStart = OrgChart._getTransform(tooltipElement);
  let transformEnd = transformStart.slice(0);

  transformEnd[0] = 0.001;
  transformEnd[3] = 0.001;

  OrgChart.animate(
    tooltipElement,
    { transform: transformStart },
    { transform: transformEnd },
    300,
    OrgChart.anim.inBack
  );
}

chart.onRedraw(() => {
  let fieldElements = chart.element.querySelectorAll('[data-n-id] foreignobject svg');

  for (let i = 0; i < fieldElements.length; i++) {
    let fieldElement = fieldElements[i];

    fieldElement.onmouseenter = function () {
      let id = this.closest('[data-n-id]').getAttribute('data-n-id');
      showTooltip(id);
    };

    fieldElement.onmouseleave = function () {
      let id = this.closest('[data-n-id]').getAttribute('data-n-id');
      hideTooltip(id);
    };
  }
});

chart.on("render", function (chart, args) {
  for (let i = 0; i < args.res.visibleNodeIds.length; i++) {
    let node = chart.getNode(args.res.visibleNodeIds[i]);
    let data = chart.get(node.id);

    if (data.tooltip) {
      args.content += tooltip
        .replace("{x}", node.x + node.w / 2)
        .replace("{y}", node.y + 5)
        .replace("{id}", node.id)
        .replace(
          "{text}",
          tooltipText.replace(
            "{val}",
            OrgChart.wrapText(data.tooltip, tooltipText)
          )
        );
    }
  }
});

chart.load([
  {
    id: "1",
    html: "<div>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi id pellentesque lacus.</div>",
    tooltip: "1 my fancy tooltip"
  },
  {
    id: "2",
    pid: "1",
    html: "<div>Mauris aliquam magna sapien. Ut a diam ac arcu commodo maximus.</div>",
    tooltip: "2 my fancy tooltip"
  },
  {
    id: "3",
    pid: "1",
    html: "<div>Phasellus eros felis, pellentesque quis ultrices nec, tempus ac felis.</div>",
    tooltip: "3 my fancy tooltip"
  }
]);

We use the following CSS to fix text rendering in this example:

css
.light {
  font: 13px Helvetica,"Segoe UI",Arial,sans-serif;
}
foreignObject {
  font: 13px Helvetica;
}

Key Points

  • Render tooltip markup in chart.on("render") so each visible node gets the correct position.
  • Use a small scale matrix (0.001) as hidden state and animate to scale 1 on hover.
  • Attach hover listeners in onRedraw because node DOM can be rebuilt after interactions.