首页 > 解决方案 > 复制纯 JS 版本的 D3 Zoomable Sunburst (observable)

问题描述

很长一段时间以来,我一直在尝试在纯 JS 中复制https://beta.observablehq.com/@mbostock/d3-zoomable-sunburst以在我的一个项目中使用它。我正在使用 PHP 和 Ajax 在 JavaScript 中加载动态数据。我认为 Observable 链接中的代码不是纯 JS,而是 Node 或其他东西。我是脚本的新手,因此我很难理解书面代码。我确实知道纯 JS 将需要特定格式的数据(flare.json),这将生成预期的输出。我可以从后端控制 JSON 结构,但我无法生成像链接这样的输出。

我在网上关注了多个例子

https://bl.ocks.org/mbostock/4348373

在 d3 版本 4 中也是如此(与可观察示例中使用的 v5 非常相似):

https://bl.ocks.org/maybelinot/5552606564ef37b5de7e47ed2b7dc099

我一直在尝试将 Observable Zoomable Sunburst 转换为 JS 函数,但我无法使其工作。我有完全相同的flare.json 文件,并尝试重新创建与Observable 完全相同的函数。但它仍然无法正常工作。

我附上了我尝试了很长时间的样本作品。请看看它,并让我知道我怎样才能让它工作?

样品工作

我也尝试在d3-js的 Google Groups 上寻求帮助,但我也没有从那里得到任何帮助。

到目前为止,我已经实现的最接近的可能输出如下所述:

    var margin = {top: 288, right: 416, bottom: 288, left: 416},
    radius = Math.min(margin.top, margin.right, margin.bottom, margin.left) - 5;

    var hue = d3.scale.category10();

    var luminance = d3.scale.sqrt()
    .domain([0, 1e6])
    .clamp(true)
    .range([90, 20]);

    var svg = d3.select("body").append("svg")
    .attr("width", margin.left + margin.right)
    .attr("height", margin.top + margin.bottom)
    .append("g")
    .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

    var partition = d3.layout.partition()
    .sort(function(a, b) { return d3.ascending(a.name, b.name); })
    .size([2 * Math.PI, radius]);

    var arc = d3.svg.arc()
    .startAngle(function(d) { return d.x; })
    .endAngle(function(d) { return d.x + d.dx ; })
    .padAngle(.01)
    .padRadius(radius / 3)
    .innerRadius(function(d) { return radius / 3 * d.depth; })
    .outerRadius(function(d) { return radius / 3 * (d.depth + 1) - 1; });

    // d3.json("https://api.myjson.com/bins/byw4q", function(error, root) {
    d3.json("https://gist.githubusercontent.com/mbostock/4348373/raw/85f18ac90409caa5529b32156aa6e71cf985263f/flare.json", function(error, root) {
      if (error) throw error;

  // Compute the initial layout on the entire tree to sum sizes.
  // Also compute the full name and fill color for each node,
  // and stash the children so they can be restored as we descend.
  partition
  .value(function(d) { return d.size; })
  .nodes(root)
  .forEach(function(d) {
    d._children = d.children;
    d.sum = d.value;
    d.key = key(d);
    d.fill = fill(d);
  });

  // Now redefine the value function to use the previously-computed sum.
  partition
  .children(function(d, depth) { return depth < 2 ? d._children : null; })
  .value(function(d) { return d.sum; });

  var center = svg.append("circle")
  .attr("r", radius / 3)
  .on("click", zoomOut);

  center.append("title")
  .text("zoom out");

  var path = svg.selectAll("path")
  .data(partition.nodes(root).slice(1))
  .enter().append("path")
  .attr("d", arc)
  .style("fill", function(d) { return d.fill; })
  .each(function(d) { this._current = updateArc(d); })
  .on("click", zoomIn);

  function zoomIn(p) {
    if (p.depth > 1) p = p.parent;
    if (!p.children) return;
    zoom(p, p);
  }

  function zoomOut(p) {
    if (!p.parent) return;
    zoom(p.parent, p);
  }

  // Zoom to the specified new root.
  function zoom(root, p) {
    if (document.documentElement.__transition__) return;

    // Rescale outside angles to match the new layout.
    var enterArc,
    exitArc,
    outsideAngle = d3.scale.linear().domain([0, 2 * Math.PI]);

    function insideArc(d) {
      return p.key > d.key
      ? {depth: d.depth - 1, x: 0, dx: 0} : p.key < d.key
      ? {depth: d.depth - 1, x: 2 * Math.PI, dx: 0}
      : {depth: 0, x: 0, dx: 2 * Math.PI};
    }

    function outsideArc(d) {
      return {depth: d.depth + 1, x: outsideAngle(d.x), dx: outsideAngle(d.x + d.dx) - outsideAngle(d.x)};
    }

    center.datum(root);

    // When zooming in, arcs enter from the outside and exit to the inside.
    // Entering outside arcs start from the old layout.
    if (root === p) enterArc = outsideArc, exitArc = insideArc, outsideAngle.range([p.x, p.x + p.dx]);

    path = path.data(partition.nodes(root).slice(1), function(d) { return d.key; });

    // When zooming out, arcs enter from the inside and exit to the outside.
    // Exiting outside arcs transition to the new layout.
    if (root !== p) enterArc = insideArc, exitArc = outsideArc, outsideAngle.range([p.x, p.x + p.dx]);

    d3.transition().duration(d3.event.altKey ? 7500 : 750).each(function() {
      path.exit().transition()
      .style("fill-opacity", function(d) { return d.depth === 1 + (root === p) ? 1 : 0; })
      .attrTween("d", function(d) { return arcTween.call(this, exitArc(d)); })
      .remove();

      path.enter().append("path")
      .style("fill-opacity", function(d) { return d.depth === 2 - (root === p) ? 1 : 0; })
      .style("fill", function(d) { return d.fill; })
      .on("click", zoomIn)
      .each(function(d) { this._current = enterArc(d); });

      path.transition()
      .style("fill-opacity", 1)
      .attrTween("d", function(d) { return arcTween.call(this, updateArc(d)); });
    });
  }
});

    function key(d) {
      var k = [], p = d;
      while (p.depth) k.push(p.name), p = p.parent;
      return k.reverse().join(".");
    }

    function fill(d) {
      var p = d;
      while (p.depth > 1) p = p.parent;
      var c = d3.lab(hue(p.name));
      c.l = luminance(d.sum);
      return c;
    }

    function arcTween(b) {
      var i = d3.interpolate(this._current, b);
      this._current = i(0);
      return function(t) {
        return arc(i(t));
      };
    }

    function updateArc(d) {
      return {depth: d.depth, x: d.x, dx: d.dx};
    }

    d3.select(self.frameElement).style("height", margin.top + margin.bottom + "px");

  
<!DOCTYPE html>
<meta charset="utf-8">
<style>

circle,
path {
  cursor: pointer;
}

circle {
  fill: none;
  pointer-events: all;
}

</style>
<body>
<script src="https://d3js.org/d3.v3.min.js"></script>
</body>

标签: javascriptd3.js

解决方案


编写的代码是纯 javascript 代码,无论您从 Ajax 获得什么数据,您只需要在这里传递相同的端点,

我在这里运行的示例意味着您的项目中也应该使用相同的方法,而不是调用 Ajax,您可以在这一行中传递您的 Json

d3.json("https://gist.githubusercontent.com/mbostock/4348373/raw/85f18ac90409caa5529b32156aa6e71cf985263f/flare.json", function(error, root)

<!DOCTYPE html>
<meta charset="utf-8">
<style>
  path {
    stroke: #fff;
  }
</style>

<body>
  <script src="https://d3js.org/d3.v4.min.js"></script>
  <script>
    var width = 960,
      height = 700,
      radius = (Math.min(width, height) / 2) - 10;

    var formatNumber = d3.format(",d");

    var x = d3.scaleLinear()
      .range([0, 2 * Math.PI]);

    var y = d3.scaleSqrt()
      .range([0, radius]);

    var color = d3.scaleOrdinal(d3.schemeCategory20);

    var partition = d3.partition();

    var arc = d3.arc()
      .startAngle(function(d) {
        return Math.max(0, Math.min(2 * Math.PI, x(d.x0)));
      })
      .endAngle(function(d) {
        return Math.max(0, Math.min(2 * Math.PI, x(d.x1)));
      })
      .innerRadius(function(d) {
        return Math.max(0, y(d.y0));
      })
      .outerRadius(function(d) {
        return Math.max(0, y(d.y1));
      });


    var svg = d3.select("body").append("svg")
      .attr("width", width)
      .attr("height", height)
      .append("g")
      .attr("transform", "translate(" + width / 2 + "," + (height / 2) + ")");

    d3.json("https://gist.githubusercontent.com/mbostock/4348373/raw/85f18ac90409caa5529b32156aa6e71cf985263f/flare.json", function(error, root) {
      if (error) throw error;

      root = d3.hierarchy(root);
      root.sum(function(d) {
        return d.size;
      });

      svg.selectAll("path")
        .data(partition(root).descendants())
        .enter().append("path")
        .attr("d", arc)
        .style("fill", function(d) {
          return color((d.children ? d : d.parent).data.name);
        })
        .on("click", click)
        .append("title")
        .text(function(d) {
          return d.data.name + "\n" + formatNumber(d.value);
        });

      function labelVisible(d) {
        return d.y1 <= 3 && d.y0 >= 1 && (d.y1 - d.y0) * (d.x1 - d.x0) > 0.03;
      }

      function labelTransform(d) {
        const x = (d.x0 + d.x1) / 2 * 180 / Math.PI;
        const y = (d.y0 + d.y1) / 2 * radius;
        return `rotate(${x - 90}) translate(${y},0) rotate(${x < 180 ? 0 : 180})`;
      }

      svg.selectAll("text")
        .attr("dy", "0.35em")
        .attr("pointer-events", "none")
        .attr("text-anchor", "middle")
        .style("user-select", "none")
        .attr("fill-opacity", d => +labelVisible(d.current))
        .attr("transform", d => labelTransform(d.current))
        .data(root.descendants().slice(1))
        .enter().append("text")
        .text(d => d.data.name);

    });




    function click(d) {
      svg.transition()
        .duration(750)
        .tween("scale", function() {
          var xd = d3.interpolate(x.domain(), [d.x0, d.x1]),
            yd = d3.interpolate(y.domain(), [d.y0, 1]),
            yr = d3.interpolate(y.range(), [d.y0 ? 20 : 0, radius]);
          return function(t) {
            x.domain(xd(t));
            y.domain(yd(t)).range(yr(t));
          };
        })
        .selectAll("path")
        .attrTween("d", function(d) {
          return function() {
            return arc(d);
          };
        });
    }

    d3.select(self.frameElement).style("height", height + "px");
  </script>


推荐阅读