D3 围绕一组圆圈绘制一个外壳

Posted

技术标签:

【中文标题】D3 围绕一组圆圈绘制一个外壳【英文标题】:D3 drawing a hull around group of circles 【发布时间】:2012-10-25 05:25:48 【问题描述】:

我想用 d3 围绕一个分组力有向图构建绘制一个船体。

我已经用圆圈构建了图表。但我现在想用路径(船体)加入圆圈的交叉点。如果不加入交叉点,在这组圆圈周围绘制一个外壳就足够了。我尝试了Force-Directed Layout with Convex Hull 示例。但是我有覆盖文本的文本和圆圈以及连接文本的链接。

var vertices = new Array();
var width = 960,
    height = 500;
var color = d3.scale.category10();
var r = 6;
var force = d3.layout.force().size([width, height]);
var svg = d3.select("body").append("svg").attr("width", width).attr("height", height).attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
$(function() 

    var json = "\"nodes\":[\"name\":\"language\",\"group\":1,\"fontsize\":\"45px\",\"title\":null,\"name\":\"english\",\"group\":1,\"fontsize\":\"35px\",\"title\":null,\"name\":\"languages\",\"group\":1,\"fontsize\":\"21px\",\"title\":null,\"name\":\"speak\",\"group\":1,\"fontsize\":\"16px\",\"title\":null,\"name\":\"religion\",\"group\":1,\"fontsize\":\"16px\",\"title\":null,\"name\":\"words\",\"group\":1,\"fontsize\":\"16px\",\"title\":null,\"name\":\"living\",\"group\":1,\"fontsize\":\"16px\",\"title\":null,\"name\":\"adobe\",\"group\":2,\"fontsize\":\"15px\",\"title\":null,\"name\":\"malayalam\",\"group\":1,\"fontsize\":\"15px\",\"title\":null,\"name\":\"learn\",\"group\":1,\"fontsize\":\"15px\",\"title\":null,\"name\":\"multilanguage\",\"group\":3,\"fontsize\":\"15px\",\"title\":null,\"name\":\"different\",\"group\":1,\"fontsize\":\"15px\",\"title\":null,\"name\":\"sarcasm\",\"group\":1,\"fontsize\":\"15px\",\"title\":null,\"name\":\"linkedin\",\"group\":4,\"fontsize\":\"15px\",\"title\":null,\"name\":\"hindi\",\"group\":1,\"fontsize\":\"15px\",\"title\":null,\"name\":\"indesign\",\"group\":5,\"fontsize\":\"15px\",\"title\":null,\"name\":\"city\",\"group\":1,\"fontsize\":\"15px\",\"title\":null,\"name\":\"spanish\",\"group\":1,\"fontsize\":\"15px\",\"title\":null,\"name\":\"religious\",\"group\":1,\"fontsize\":\"15px\",\"title\":null,\"name\":\"real\",\"group\":1,\"fontsize\":\"15px\",\"title\":null],\"links\":[\"source\":0,\"target\":1,\"value\":1,\"source\":0,\"target\":2,\"value\":1,\"source\":0,\"target\":3,\"value\":1,\"source\":0,\"target\":4,\"value\":1,\"source\":0,\"target\":5,\"value\":1,\"source\":1,\"target\":2,\"value\":1,\"source\":1,\"target\":3,\"value\":1,\"source\":1,\"target\":5,\"value\":1,\"source\":1,\"target\":6,\"value\":1,\"source\":2,\"target\":3,\"value\":1,\"source\":2,\"target\":4,\"value\":1,\"source\":2,\"target\":5,\"value\":1,\"source\":3,\"target\":5,\"value\":1,\"source\":3,\"target\":8,\"value\":1,\"source\":4,\"target\":5,\"value\":1,\"source\":4,\"target\":6,\"value\":1,\"source\":4,\"target\":11,\"value\":1,\"source\":5,\"target\":6,\"value\":1,\"source\":6,\"target\":2,\"value\":1,\"source\":6,\"target\":11,\"value\":1,\"source\":6,\"target\":18,\"value\":1,\"source\":8,\"target\":0,\"value\":1,\"source\":8,\"target\":2,\"value\":1,\"source\":8,\"target\":14,\"value\":1,\"source\":9,\"target\":0,\"value\":1,\"source\":9,\"target\":1,\"value\":1,\"source\":9,\"target\":2,\"value\":1,\"source\":9,\"target\":3,\"value\":1,\"source\":9,\"target\":8,\"value\":1,\"source\":11,\"target\":0,\"value\":1,\"source\":11,\"target\":1,\"value\":1,\"source\":11,\"target\":2,\"value\":1,\"source\":11,\"target\":3,\"value\":1,\"source\":12,\"target\":0,\"value\":1,\"source\":12,\"target\":1,\"value\":1,\"source\":12,\"target\":2,\"value\":1,\"source\":12,\"target\":3,\"value\":1,\"source\":12,\"target\":14,\"value\":1,\"source\":14,\"target\":0,\"value\":1,\"source\":14,\"target\":1,\"value\":1,\"source\":14,\"target\":2,\"value\":1,\"source\":14,\"target\":3,\"value\":1,\"source\":14,\"target\":5,\"value\":1,\"source\":16,\"target\":0,\"value\":1,\"source\":16,\"target\":1,\"value\":1,\"source\":16,\"target\":2,\"value\":1,\"source\":16,\"target\":9,\"value\":1,\"source\":16,\"target\":11,\"value\":1,\"source\":17,\"target\":0,\"value\":1,\"source\":17,\"target\":1,\"value\":1,\"source\":17,\"target\":2,\"value\":1,\"source\":17,\"target\":3,\"value\":1,\"source\":18,\"target\":2,\"value\":1,\"source\":18,\"target\":4,\"value\":1,\"source\":18,\"target\":5,\"value\":1,\"source\":18,\"target\":11,\"value\":1,\"source\":19,\"target\":0,\"value\":1,\"source\":19,\"target\":1,\"value\":1,\"source\":19,\"target\":2,\"value\":1,\"source\":19,\"target\":3,\"value\":1,\"source\":19,\"target\":5,\"value\":1]";

    json = htmlDecode(json);

    json = $.parseJSON(json);

    svg.append("svg:rect").attr("width", width).attr("height", height).style("stroke", "#fff").style("fill", "#fff");

    force.nodes(json.nodes).links(json.links).gravity(0.05).linkDistance(120).charge(-200).start();

    var node = svg.selectAll(".node").data(json.nodes).enter().append("g").attr("class", "node");
    var link = svg.selectAll(".link").data(json.links).enter().append("line").attr("class", "link").style("stroke-opacity", "0.2");

    node.append('circle').attr('r', function(d) 
        var tmprad = parseInt(d.fontsize.replace('px', '')) * (d.name.length / 3);
        if (tmprad > r) r = tmprad;
        return tmprad;
    ).style('fill', '#ffffff').style('stroke', function(d) 
        return color(d.group)
    );

    node.selectAll('text').data(json.nodes).enter().append("text").attr("text-anchor", "middle").attr("dx", 2).attr("dy", ".35em").attr('original-title', function(d) 
        return d.title
    ).attr("style", function(d) 
        return "font-size:" + d.fontsize
    ).text(function(d) 
        return d.name
    ).attr("style", function(d) 
        return "font-size:" + d.fontsize
    ).style('fill', function(d) 
        return color(d.group)
    ).style("cursor", "pointer").call(force.drag);

    var cx = new Array();
    var cy = new Array();

    node.attr("cx", function(d) 
        cx.push(d.x);
    ).attr("cy", function(d) 
        cy.push(d.y);
    );

    cx.forEach(function(o, i) 
        vertices.push(new Array(cx[i], cy[i]));
    );

    var nodes = vertices.map(Object);

    var groups = d3.nest().key(function(d) 
        return d;
    ).entries(nodes);

    var groupPath = function(d) 
        return "M" + d3.geom.hull(d.values.map(function(i) 
            return [i.x, i.y];
        )).join("L") + "Z";
    ;

    var groupFill = function(d, i) 
        return color(i & 3);
    ;

    svg.style("opacity", 1e-6).transition().duration(1000).style("opacity", 1);

    force.on("tick", function()     
        link.attr("x1", function(d) 
            return d.source.x;
        ).attr("y1", function(d) 
            return d.source.y;
        ).attr("x2", function(d) 
            return d.target.x;
        ).attr("y2", function(d) 
            return d.target.y;
        );

        node.attr("cx", function(d) 
            return d.x = Math.max(r, Math.min(width - r, d.x));
        ).attr("cy", function(d) 
            return d.y = Math.max(r, Math.min(height - r, d.y));
        );

        node.selectAll('circle').attr("transform", function(d) 
            return "translate(" + d.x + "," + d.y + ")"
        );

        // reposition text
        node.selectAll('text').attr("transform", function(d) 
            return "translate(" + d.x + "," + d.y + ")"
        );
    );
);

function htmlEncode(value) 
    return $('<div/>').text(value).html();


function htmlDecode(value) 
    return $('<div/>').html(value).text();


function move() 
    vertices[0] = d3.svg.mouse(this);
    update();


function click() 
    vertices.push(d3.svg.mouse(this));
    update();


function update() 
    svg.selectAll("path")
        .data([d3.geom.hull(vertices)])
        .attr("d", function(d) 
            return "M" + d.join("L") + "Z";
        )
       .enter()
        .append("svg:path")
        .attr("d", function(d) 
            return "M" + d.join("L") + "Z";
        );
    svg.selectAll("nodes")
        .data(vertices.slice(1))
       .enter()
        .append("svg:circle")
        .attr("transform", function(d) 
             return "translate(" + d + ")";
        );
​

您可以在JsFiddle 上查看此代码的示例:

【问题讨论】:

【参考方案1】:

我用你的 JsFiddle 玩了一点,最后得到了这个:JsFiddle Example。

我刚刚加了

svg.selectAll("path")
        .data(groups)
        .attr("d", groupPath)
        .enter().insert("path", "g")
        .style("fill", groupFill)
        .style("stroke", groupFill)
        .style("stroke-width", 100)
        .style("stroke-linejoin", "round")
        .style("opacity", .2)
        .attr("d", groupPath);

绘制船体,并稍微调整一下您定义的函数(groupPath,groupFill)。另外,我定义了组来识别图表的不同组。

这是您发布的其他链接的脏端口,船体没有完全覆盖较大的圆圈。您必须根据圆圈的大小获得具有可变笔画宽度的路径。不知道怎么做。

不过,您可以稍微调整一下路径的笔划宽度,以使船体更大/更小。

希望对你有所帮助。

编辑:我通过一些数学改进了我的示例。如您所见Here,它适用于少量气泡。 它仍然是蹩脚/错误的代码(我只是为了好玩),但你可以找到我使用的三角函数。诀窍是让 d3 使用 d3.geom.hull 计算组的外壳,这将返回感兴趣节点的坐标列表。您可以想象在每个角节点处绘制一个大小合适的圆。然后,您必须找到这些圆与连接它们的线段之间的交点。我用一张纸、泰勒斯定理和一点三角函数来计算这些点的坐标。计算在特定情况下有点偏离,但我不知道 d3.geom.hull 的真正工作原理,也不知道一般的 d3,所以我无法为您提供更多帮助。

【讨论】:

不是我想要的那个。您已选择节点作为船体的中心。但我想用圆半径绘制船体作为船体的“行程宽度”。即,船体笔划宽度应该是动态的并且依赖于圆的半径。 不错的一个。但这不适用于更多数量的节点。 :( 想一想,在计算出线段和圆的交点后,可能还得调用另一个d3.geom.hull,再迭代一次。我现在没有时间尝试。如果我愿意,我会更新我的答案。

以上是关于D3 围绕一组圆圈绘制一个外壳的主要内容,如果未能解决你的问题,请参考以下文章

在 UIView::drawRect() 之外绘制 UIBezierPath?

如何在MapKit上绘制围绕多个注释/坐标的圆形叠加?

D3.js 沿 x,y 坐标为圆设置动画

使用 CAEmitterLayer 围绕圆形或 CGPath 绘制粒子

在谷歌地图中围绕一个点绘制半径

D3:在 d3 中查找地理多边形的面积