plot_plot_dotdensity.js

import { circle } from "../mark/circle";
import { randompoints } from "../tool/randompoints";
import { text } from "../mark/text";
import { unique } from "../helpers/utils";

/**
 * @function plot_dotdensity
 * @description
 * The `plot_dotdensity` function generates a **dot density map** by creating points proportionally
 * to a numeric variable in your dataset. Each point represents a fixed quantity (`dotval`) from the data.
 *
 *
 * @param {Object|SVGElement} arg1 - Either the container SVG element to draw into, or the options object if creating a new container.
 * @param {Object} [arg2] - Options object (used when an existing container is provided as `arg1`).
 *
 * @property {string} [type="dotdensity"] - Type of plot.
 * @property {string} [stroke="none"] - Stroke color for dots.
 * @property {number} [r=1] - Radius of each dot.
 * @property {number} [dotval] - Value represented by one dot. If undefined, computed automatically.
 * @property {boolean} [legend=true] - Whether to add a legend.
 * @property {Array<number>} [leg_pos] - Position `[x, y]` of the legend. Defaults to `[10, svg.height - 10]`.
 * @property {string} [leg_text] - Text to display in the legend. Defaults to the dot value.
 * @property {string} [fill="black"] - Fill color of the dots.
 * @property {Array|Object} [data] - The dataset to visualize.
 * @property {string} [var] - The numeric variable in the dataset to map with dots.
 *
 * @returns {SVGElement} The SVG container with the dot density map rendered.
 *
 */

export function plot_dotdensity(arg1, arg2) {
  const newcontainer =
    (arguments.length <= 1 || arguments[1] == undefined) &&
    !arguments[0]?._groups;

  const defaults = {
    type: "dotdensity",
    id: unique(),
    stroke: "none",
    dotval: undefined,
    r: 1,
    legend: true,
    leg_pos: undefined,
    leg_text: undefined,
    fill: "black",
  };

  const options = {
    ...defaults,
    ...(newcontainer ? arg1 : arg2),
  };

  // Generate the points
  options.data = randompoints({
    data: options.data,
    var: options.var,
    dotval: options.dotval,
  });

  // Get the actual SVG container
  let svg;
  if (newcontainer) {
    svg = circle(options); // new container + points
  } else {
    svg = arg1; // existing container
    circle(svg, options); // add the points
  }

  let ids = `#${options.id}`;

  // --------------------------
  // Separate handling of the legend
  // --------------------------
  // Add the legend if requested
  if (options.legend) {
    const legId = "leg_" + options.id;
    ids = [`#${options.id}`, `#${legId}`];
    const pos = options.leg_pos || [10, svg.height - 10];

    if (newcontainer) {
      text({
        ...options,
        id: legId,
        pos: [pos[0], pos[1]],
        text: options.leg_text || `One dot = ${options.data.dotvalue}`,
        fontSize: 10,
        textAnchor: "start",
        dominantBaseline: "middle",
      });
    } else {
      text(svg, {
        id: legId,
        pos: [pos[0], pos[1]],
        text: options.leg_text || `One dot = ${options.data.dotvalue}`,
        fontSize: 10,
        textAnchor: "start",
        dominantBaseline: "middle",
        fill: options.fill,
      });
    }
  }

  if (newcontainer) {
    return render(svg);
  } else {
    return ids;
  }
}