helpers_tooltip.js

import { select, pointers } from "d3-selection";
const d3 = Object.assign({}, { select, pointers });

/**
 * @name tip
 * @description The `tip` parameter allows to add tooltips on map layers
 * @see {@link https://observablehq.com/@neocartocnrs/tooltip}
 * @property {string | boolean | function} tip - You can display a simple text like "foo". But in most cases, tooltips are used to display information related to the elements hovered over. To do this, use the `$` prefix with the field name.With `true`, all fidls are displayed. Finally, you can pass a function to build a customized tooltip.
 * @property {object} tipstyle - An object to configure the "appearance of the tooltip. `fontSize`, `fill`, `background`, `stroke`,  `strokeWidth`, `fontFamily`, `fontWeight`, `fontStyle`, `textDecoration`. See also `tool.addonts`
 
 * @example
 * // Simple text
 * viz.path({ data: world, tip: "hello" })
 * @example
 * // A field to display
 * viz.path({ data: world, tip: "$pop" })
 * @example
 * // To display all fields
 * viz.path({ data: world, tip: true })
  * @example
 * // A tooltip on several lines
 * viz.path({
  data: world,
  fill: "#38896F",
  tip: `This country is $name
  It is located in $region
  Its population is $pop` 
  })
 * @example
 * // A function
 * viz.path({
  data: world,
  fill: "#38896F",
  tip: (d) =>
    `There are ${Math.round(
      d.properties.pop / 1000000
    )} million inhabitants in ${d.properties.name}`
  })
* @example
* // Custom style
* viz.path({
  data: world,
  fill: "#CCC",
  tip: `$name ($ISO3)`,
  tipstyle: {
    fontSize: 20,
    fill: "white",
    background: "#38896F",
    stroke: "#4a4d4b",
    strokeWidth: 3,
    fontFamily: "Pacifico",
    fontWeight: "normal",
    fontStyle: "italic",
    textDecoration: "none"
  }
})
 */

export function tooltip(
  layer,
  data,
  container,
  tip,
  tip_style = {},
  fields,
  view
) {
  // view
  let dataview;

  //if input == string
  if (typeof tip === "string") {
    const sortfields = fields.sort((a, b) => b.length - a.length);
    fields.forEach((d) => {
      tip = tip.replace(`$${d}`, `\${d.properties.${d}}`);
    });
    tip = eval("d => `" + tip + "`");
  }

  // if function
  else if (typeof tip == "function") {
    const arrstr = tip.toString().split("=>");
    if (!arrstr[1].includes("`${")) {
      tip = eval(arrstr[0] + " => `${" + tip.toString().split("=>")[1] + "}`");
    }
  }

  //if input == true
  else if (tip === true) {
    let x = { ...data };
    let keys = [];
    x.features
      .map((d) => d.properties)
      .forEach((d) => {
        keys.push(Object.keys(d));
      });
    keys = Array.from(new Set(keys.flat()));

    let str = [];
    keys.forEach((d) => str.push(`${d}: \${d.properties.${d}}`));
    tip = eval("(d) => `" + str.join("\n") + "`");
  }

  // style
  let style = {
    fontSize: 13,
    fill: "#4d4545",
    background: "#fcf7e6",
    stroke: "#4a4d4b",
    strokeWidth: 1,
    fontFamily: container.fontFamily,
    fontWeight: "normal",
    fontStyle: "normal",
    textDecoration: "none",
    hoverOpacity: 0.5,
    hoverFill: undefined,
    hoverStroke: undefined,
  };

  let formerOpacity = layer.attr("fill-opacity");

  let context = container.node();

  Object.keys(tip_style).forEach((d) => {
    style[d] = tip_style[d];
  });

  const geoviztooltip = container.select(`#geoviztooltip`);
  geoviztooltip.attr("pointer-events", "none");

  let path;
  let text;
  if (container.select(`#geoviztooltip`).selectAll("*").empty()) {
    path = geoviztooltip
      .append("g")
      .attr("id", "geotooltippath")
      .attr("fill", style.background)
      .attr("stroke", style.stroke)
      .attr("stroke-width", style.strokeWidth)
      .selectAll("path")
      .data([null])
      .join("path");
    text = geoviztooltip
      .append("g")
      .attr("id", "geotooltiptext")
      .attr("font-size", `${style.fontSize}px`)
      .attr("fill", style.fill)
      .attr("font-family", style.fontFamily)
      .attr("font-weight", style.fontWeight)
      .attr("font-style", style.fontStyle)
      .attr("text-decoration", style.textDecoration);
  } else {
    path = geoviztooltip
      .select("#geotooltippath")
      .attr("fill", style.background)
      .attr("stroke", style.stroke)
      .attr("stroke-width", style.strokeWidth)
      .selectAll("path")
      .data([null])
      .join("path");
    text = geoviztooltip.select("#geotooltiptext");
  }
  layer
    .selectAll("*")
    .on("touchmove mousemove", function (event, d) {
      // view
      if (view) {
        dataview = d.properties;
        container.dispatch("input");
      }

      geoviztooltip.style("visibility", "visible");
      const xy = d3.pointers(event, context)[0];
      d3.select(this).attr("fill-opacity", 0.5);
      text
        .selectAll("text")
        .data(eval(tip.toString().split("=>")[1]).split("\n"))
        .join("text")
        .attr("dy", (d, i) => i * style.fontSize)
        .text((d) => d);
      path.attr("transform", `translate(${xy})`);
      const { x, y, width: w, height: h } = text.node().getBBox();

      const x_margin = 0.33 * container.width;
      const y_margin = 0.25 * container.height;

      // bottomright
      if (xy[0] < x_margin && xy[1] < y_margin) {
        text.attr("transform", `translate(${xy[0] + 10},${xy[1] + 15 - y})`);
        path.attr("d", `M0,0v${+h + 5 + 20}h${w + 20}v${-h - 20}h${-w - 15}z`);
      }

      // bottomleft
      else if (xy[0] > container.width - x_margin && xy[1] < y_margin) {
        text.attr(
          "transform",
          `translate(${xy[0] - w - 10},${xy[1] + 15 - y})`
        );
        path.attr("d", `M0,0v${+h + 5 + 20}h${-w - 20}v${-h - 20}h${+w + 15}z`);
      }

      // topright
      else if (xy[0] < x_margin && xy[1] > container.height - y_margin) {
        text.attr(
          "transform",
          `translate(${xy[0] + 10},${xy[1] - 15 - y - h})`
        );
        path.attr("d", `M0,0v${-h - 5 - 20}h${w + 20}v${h + 20}h${-w - 15}z`);
      }
      //topleft
      else if (
        xy[0] > container.width - x_margin &&
        xy[1] > container.height - y_margin
      ) {
        text.attr(
          "transform",
          `translate(${xy[0] - w - 10},${xy[1] - 15 - y - h})`
        );
        path.attr("d", `M0,0v${-h - 5 - 20}h${-w - 20}v${h + 20}h${w + 15}z`);
      }
      // top
      else if (xy[1] > container.height - y_margin) {
        text.attr(
          "transform",
          `translate(${xy[0] - w / 2},${xy[1] - 15 - y - h})`
        );
        path.attr(
          "d",
          `M${-w / 2 - 10},-5H-5l5,5l5,-5H${w / 2 + 10}v${-h - 20}h-${w + 20}z`
        );
      }

      // right
      else if (xy[0] < x_margin) {
        text.attr("transform", `translate(${xy[0] + 15},${xy[1] - y - h / 2})`);
        path.attr(
          "d",
          `M0,0l5,5v${h / 2 + 5}h${w + 20}v${-h - 20}h${-w - 20}v${h / 2 + 5}z`
        );
      }
      // left
      else if (xy[0] > container.width - x_margin) {
        text.attr(
          "transform",
          `translate(${xy[0] - w - 15},${xy[1] - y - h / 2})`
        );
        path.attr(
          "d",
          `M0,0l-5,5v${h / 2 + 5}h${-w - 20}v${-h - 20}h${w + 20}v${h / 2 + 5}z`
        );
      } else {
        // Bottom
        text.attr("transform", `translate(${xy[0] - w / 2},${xy[1] - y + 15})`);
        path.attr(
          "d",
          `M${-w / 2 - 10},5H-5l5,-5l5,5H${w / 2 + 10}v${h + 20}h-${w + 20}z`
        );
      }
    })
    .on("touchend mouseleave", function (event, d) {
      d3.select(this).attr("fill-opacity", formerOpacity);
      geoviztooltip.style("visibility", "hidden");
      if (view) {
        dataview = {};
        container.dispatch("input");
      }
    });

  if (view) {
    Object.defineProperty(container.node(), "value", {
      get: () => dataview,
    });
  }
}