Pack(打包布局)

191 阅读3分钟

简介

Pack布局是一种用于表示包含与被包含关系的可视化布局方式,‌通常通过圆形套圆形来表示,‌其中外圆的大小表示被包含的对象,‌内圆的大小表示包含的对象。‌ Pack布局在D3.js中主要用于展示具有层级关系的数据,‌通过调整布局参数,‌如节点间距、‌节点半径等,‌可以控制布局的外观和视觉效果。

数据

// 数据地址:https://static.observableusercontent.com/files/85b8f86120ba5c8012f55b82fb5af4fcc9ff5e3cf250d110e111b3ab98c32a3fa8f5c19f956e096fbf550c47d6895783a4edf72a9c474bef5782f879573750ba
var flare = {
  "name": "flare",
  "children": []
}

主程序

function Pack(data, { // data可以是表格(对象数组)或层次结构(嵌套对象)
  path, // 作为id和parentId的替代,返回一个数组标识符,输入内部节点
  id = Array.isArray(data) ? d => d.id : null, // 如果数据中给定d的表格数据返回唯一标识符(字符串)
  parentId = Array.isArray(data) ? d => d.parentId : null, // 如果给定节点d的表格数据返回其父节点的标识符
  children, // 如果数据中给定d的分层数据返回其子数据
  value, // 给定节点d,返回一个定量值(用于区域编码;null表示计数)
  sort = (a, b) => d3.descending(a.value, b.value), // 如何在布局前对节点进行排序
  label, // 给定一个叶子节点d,返回显示名称
  title, // 给定节点d,返回其悬停文本
  link, // 给定一个节点d,它的链接(如果有的话)
  linkTarget = "_blank", // 链接的目标属性(如果有的话)
  width = 640, // 外部宽、高、边距,像素
  height = 400,
  margin = 1,
  marginTop = margin,
  marginRight = margin,
  marginBottom = margin,
  marginLeft = margin,
  padding = 3, // 圆之间的间距
  fill = "#ddd", // 叶子圆的填充色、透明度、边色、宽度、透明度
  fillOpacity,
  stroke = "#bbb",
  strokeWidth,
  strokeOpacity,
} = {}) {

// 如果指定了id和parentId选项,或路径选项,请使用d3.stratify将表格数据转换为层次结构;
// 否则,我们假设数据指定为具有嵌套对象的对象{children}(即“flare.json”)格式),并使用d3.hierarchy。
  const root = path != null ? d3.stratify().path(path)(data)
      : id != null || parentId != null ? d3.stratify().id(id).parentId(parentId)(data)
      : d3.hierarchy(data, children);

  // 通过从叶子聚合来计算内部节点的值。
  value == null ? root.count() : root.sum(d => Math.max(0, value(d)));

  // 计算标签和标题。
  const descendants = root.descendants();
  const leaves = descendants.filter(d => !d.children);
  leaves.forEach((d, i) => d.index = i);
  const L = label == null ? null : leaves.map(d => label(d.data, d));
  const T = title == null ? null : descendants.map(d => title(d.data, d));

  // 对叶子进行排序(通常按降序排列,以获得令人愉悦的布局)。
  if (sort != null) root.sort(sort);

  // 计算布局。
  d3.pack()
      .size([width - marginLeft - marginRight, height - marginTop - marginBottom])
      .padding(padding)
    (root);

  const svg = d3.create("svg")
      .attr("viewBox", [-marginLeft, -marginTop, width, height])
      .attr("width", width)
      .attr("height", height)
      .attr("style", "max-width: 100%; height: auto; height: intrinsic;")
      .attr("font-family", "sans-serif")
      .attr("font-size", 10)
      .attr("text-anchor", "middle");

  const node = svg.selectAll("a")
    .data(descendants)
    .join("a")
      .attr("xlink:href", link == null ? null : (d, i) => link(d.data, d))
      .attr("target", link == null ? null : linkTarget)
      .attr("transform", d => `translate(${d.x},${d.y})`);

  node.append("circle")
      .attr("fill", d => d.children ? "#fff" : fill)
      .attr("fill-opacity", d => d.children ? null : fillOpacity)
      .attr("stroke", d => d.children ? stroke : null)
      .attr("stroke-width", d => d.children ? strokeWidth : null)
      .attr("stroke-opacity", d => d.children ? strokeOpacity : null)
      .attr("r", d => d.r);

  if (T) node.append("title").text((d, i) => T[i]);

  if (L) {
    // 剪辑路径的唯一标识符(以避免冲突)。
    const uid = `O-${Math.random().toString(16).slice(2)}`;

    const leaf = node
      .filter(d => !d.children && d.r > 10 && L[d.index] != null);

    leaf.append("clipPath")
        .attr("id", d => `${uid}-clip-${d.index}`)
      .append("circle")
        .attr("r", d => d.r);

    leaf.append("text")
        .attr("clip-path", d => `url(${new URL(`#${uid}-clip-${d.index}`, location)})`)
      .selectAll("tspan")
      .data(d => `${L[d.index]}`.split(/\n/g))
      .join("tspan")
        .attr("x", 0)
        .attr("y", (d, i, D) => `${(i - D.length / 2) + 0.85}em`)
        .attr("fill-opacity", (d, i, D) => i === D.length - 1 ? 0.7 : null)
        .text(d => d);
  }

  return svg.node();
}


var chart = Pack(flare, {
  value: d => d.size, // 每个节点(文件)的大小;内部节点(文件夹)为null
  label: (d, n) => [...d.name.split(/(?=[A-Z][a-z])/g), n.value.toLocaleString("en")].join("\n"),
  title: (d, n) => `${n.ancestors().reverse().map(({data: d}) => d.name).join(".")}\n${n.value.toLocaleString("en")}`,
  link: (d, n) => n.children
    ? `https://github.com/prefuse/Flare/tree/master/flare/src/${n.ancestors().reverse().map(d => d.data.name).join("/")}`
    : `https://github.com/prefuse/Flare/blob/master/flare/src/${n.ancestors().reverse().map(d => d.data.name).join("/")}.as`,
  width: 1152,
  height: 1152
});

var myBody = document.getElementById('myBody');
myBody.appendChild(chart)