javascript - 复制纯 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>
解决方案
编写的代码是纯 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>
推荐阅读
- vue.js - 自动完成是显示使用自定义显示的内容
- react-native - 如果客户端突然调用刷新令牌怎么办
- kotlin - Kotlin - 需要返回类型 String 但有任何代替
- c# - 如何使用 Mock 初始化我的 IDbConnection 和 IConfiguration
- java - Vaadin 14 显示简单的 HTML 页面
- c++ - 防止在旧 Windows 系统上启动程序
- android-ndk - 调用 onImageAvailable 回调,但 acquireLatestImage 返回 NO_BUFFER_AVAILABLE
- angular - 如何在 Angular 中使用 API 的动态数据生成树结构?
- virtual-machine - 是否有任何其他可能性可以从我的 devbox 访问 git
- mongodb - 从 mongodb 集合中过滤掉相关产品