mark_footer.js

import { create } from "../container/create";
import { render } from "../container/render";
import { camelcasetodash, getsize, unique } from "../helpers/utils";

/**
 * @function footer
 * @description The `footer` function allows add a source below the map. The function adds a layer to the SVG container and returns the layer identifier. If the container is not defined, then the layer is displayed directly.
 * @see {@link https://observablehq.com/@neocartocnrs/layout-marks}
 *
 * @property {string} [id] - id of the layer
 * @property {string} [text = "Author, source..."] - text to be displayed
 * @property {string} [fill = "#9e9696"] - text fill
 * @property {string} [background_fill = "white"] - background fill
 * @property {string} [background_stroke = "white"] - background stroke
 * @property {string} [background_strokeWidth = 1] - background stroke-width
 * @property {string} [dominantBaseline = "central"] - text dominant-baseline ("hanging", "middle", "central", "bottom")
 * @property {string} [textAnchor = "middle"] - text text-anchore ("start", "middle", "end")
 * @property {number} [lineSpacing = 0] - space between lines
 * @property {number} [margin = 1] - margin
 * @property {number} [fontSize = 10] - text font-size
 * @property {string} [fontFamily = fontFamily defined in the contrainer] - text font-family
 * @property {number} [dx = 0] - shift in x
 * @property {number} [dy = 0] - shift in y
 *
 * @example
 * // There are several ways to use this function
 * geoviz.footer(svg, { text: "Hello geoviz" }) // where svg is the container
 * svg.footer({ text: "Hello geoviz" }) // where svg is the container
 * svg.plot({ type: "footer", text: "Hello geoviz" }) // where svg is the container
 * geoviz.footer({ text: "Hello geoviz" }) // no container
 */

export function footer(arg1, arg2) {
  // Test if new container
  let newcontainer =
    (arguments.length <= 1 || arguments[1] == undefined) &&
    !arguments[0]?._groups
      ? true
      : false;
  arg1 = newcontainer && arg1 == undefined ? {} : arg1;
  arg2 = arg2 == undefined ? {} : arg2;

  // Arguments
  const options = {
    mark: "footer",
    id: unique(),
    text: "Author, source...",
    fill: "#9e9696",
    background_fill: "white",
    background_stroke: "white",
    background_strokeWidth: 1,
    dominantBaseline: "central",
    textAnchor: "middle",
    lineSpacing: 0,
    margin: 2,
    fontSize: 10,
    dx: 0,
    dy: 0,
  };
  let opts = { ...options, ...(newcontainer ? arg1 : arg2) };

  // New container
  let svgopts = {};
  Object.keys(opts)
    .filter((str) => str.slice(0, 4) == "svg_")
    .forEach((d) => {
      Object.assign(svgopts, {
        [d.slice(0, 4) == "svg_" ? d.slice(4) : d]: opts[d],
      });
      delete opts[d];
    });
  let svg = newcontainer ? create(svgopts) : arg1;

  // FontFamily
  opts.fontFamily = opts.fontFamily || svg.fontFamily;

  // init layer
  let layer = svg.selectAll(`#${opts.id}`).empty()
    ? svg.append("g").attr("id", opts.id)
    : svg.select(`#${opts.id}`);
  layer.selectAll("*").remove();

  // Specific attributes
  let entries = Object.entries(opts).map((d) => d[0]);
  const notspecificattr = entries.filter((d) => !["mark", "id"].includes(d));

  // Text size
  const tmp = layer
    .append("text")
    .attr("font-family", opts.fontFamily)
    .attr("font-size", opts.fontSize)
    .text(opts.text.toString());
  const lineheight = getsize(tmp).height;
  const nblines = opts.text.split("\n").length;
  const textheight = lineheight * nblines;
  const totalheight =
    textheight + (nblines - 1) * opts.lineSpacing + opts.margin * 2;
  tmp.remove();
  svg.height_footer = Math.max(svg.height_footer, totalheight);

  // Dipslay rect

  const background = layer
    .append("rect")
    .attr("x", 0)
    .attr("y", svg.height)
    .attr("width", svg.width)
    .attr("height", totalheight);

  notspecificattr
    .filter((str) => str.includes("background_"))
    .forEach((d) => {
      background.attr(camelcasetodash(d.replace("background_", "")), opts[d]);
    });

  // Display text

  let posx = svg.width / 2;
  switch (opts.textAnchor) {
    case "start":
      posx = opts.margin;
      break;
    case "end":
      posx = svg.width - opts.margin;
      break;
  }

  const text = layer
    .selectAll("text")
    .data(opts.text.split("\n"))
    .join("text")
    .attr("x", posx)
    .attr(
      "y",
      (d, i) =>
        svg.height +
        opts.margin +
        i * (lineheight + opts.lineSpacing) +
        lineheight / 2
    )
    .attr("dy", opts.dy)
    .text((d) => d);

  notspecificattr
    .filter((str) => !str.includes("background_"))
    .forEach((d) => {
      text.attr(camelcasetodash(d), opts[d]);
    });

  // Ajust svg height
  svg
    .attr("width", svg.width)
    .attr("height", svg.height + svg.height_header + svg.height_footer)
    .attr("viewBox", [
      0,
      -svg.height_header,
      svg.width,
      svg.height + svg.height_header + svg.height_footer,
    ]);

  // Output
  if (newcontainer) {
    svg
      .attr("width", svg.width)
      .attr("height", totalheight)
      .attr("viewBox", [0, svg.height, svg.width, totalheight]);
    return render(svg);
  } else {
    return `#${opts.id}`;
  }
}