hobbes3 hobbes3 - 3 months ago 12
Javascript Question

How to connect two points with a line in two different coordinate system/transforms in d3?

I'm trying to create a closed path by connecting 4 sets of points, but the points are in 2 different

transforms
.

For example, I have three pie charts, two of them are using
d3.layout.pack()
and are inside the bigger pie chart:



var width = 700,
height = 500,
radius = Math.min(width, height) / 2,
thickness = 20,
length_inner = 2 * Math.sqrt(Math.pow(radius, 2) / 2);

var data = [{
"name": "bob",
"toward": "against",
"fruit": "apple",
"value": 5
}, {
"name": "rob",
"toward": "for",
"fruit": "apple",
"value": 9
}, {
"name": "alice",
"toward": "for",
"fruit": "orange",
"value": 3
}, {
"name": "mike",
"toward": "against",
"fruit": "orange",
"value": 6
}, {
"name": "katy",
"toward": "for",
"fruit": "orange",
"value": 8
}]

var data_inner = _(data).chain()
.groupBy("fruit")
.map(function(v, k) {
return {
"name": k,
"sum": _(v).chain()
.pluck("value")
.reduce(function(memo, num) {
return memo + num;
}, 0)
.value(),
"data": v
};
})
.value()

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

var outer = svg
.append("g")
.attr("class", "outer");

var color_outer = d3.scale.category20b();

var arc_outer = d3.svg.arc()
.innerRadius(radius - thickness)
.outerRadius(radius);

var pie_outer = d3.layout.pie()
.value(function(d) {
return d.value;
})
.sort(null)
.padAngle(.02);

var path_outer = outer.selectAll("path")
.data(pie_outer(data))
.enter()
.append("g")
.attr("class", "arc_outer")
.append("path")
.attr("d", arc_outer)
.attr("fill", function(d, i) {
return color_outer(d.data["name"]);
});

var inner = svg
.append("g")
.attr({
"class": "inner",
"transform": "translate(" + -(length_inner / 2) + "," + -(length_inner / 2) + ")"
});

var bubble_inner = d3.layout.pack()
.value(function(d) {
return d.sum;
})
.sort(null)
.size([length_inner, length_inner])
.padding(10);

var node_inner = inner.selectAll("g.node_inner")
.data(
bubble_inner.nodes({
children: data_inner
})
.filter(function(d) {
return !d.children;
})
)
.enter()
.append("g")
.attr({
"class": "node_inner",
"transform": function(d) {
return "translate(" + d.x + "," + d.y + ")";
}
});

var arc_inner = d3.svg.arc();
var pie_inner = d3.layout.pie()
.value(function(d) {
return d.value;
});

var arc_inner_g = node_inner.selectAll("g.arc_inner")
.data(function(d) {
return pie_inner(d.data).map(function(m) {
m.r = d.r;
return m;
});
});

arc_inner_g.enter()
.append("g")
.attr("class", "arc_inner")
.append("path")
.attr("d", function(d) {
arc_inner.innerRadius(d.r - thickness);
arc_inner.outerRadius(d.r);
return arc_inner(d);
})
.style("fill", function(d, i) {
return d.data.toward === "for" ? "#2ca02c" : "d62728";
});

<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.8.3/underscore-min.js"></script>

<body>
<svg>
</svg>
</body>





Now I'm trying to connect two points with a Bezier curve from the outer to inner pie chart like this:

enter image description here

Then close the other two points to create a
<path>
and fill it later (sorry for terrible Photoshop skills):

enter image description here

I'm trying to avoid having one, unified coordinate system. Even if I had to do that, I'm not sure how since I won't know how to create the two inner circle without
transforms
.

Answer

I was able to draw the path using several d3.path() functions:

// d is the data attached to the outer pie but also includes
// the respective inner pie's angles, cx, cy, and radius
function link_d_path(d) {
        if(d.value === 0) return "";

        var r_o = radius - thickness,
            offset = -Math.PI / 2,
            path_o_start = d.startAngle + offset,
            path_o_end   = d.endAngle   + offset,
            cx_i = -length_inner / 2 + d.node_x,
            cy_i = -length_inner / 2 + d.node_y,
            path_i_start = d.inner_startAngle + offset,
            path_i_end   = d.inner_endAngle   + offset,
            angle_diff = ((path_o_start + path_o_end) / 2 + (path_i_start + path_i_end) / 2) / 2,
            r_i = d.node_r,
            path = d3_path.path();

        path.arc(0, 0, r_o, path_o_start, path_o_end);
        path.bezierCurveTo(
            r_o * Math.cos(path_o_end),
            r_o * Math.sin(path_o_end),
            cx_i + (r_i + link_radius_cp_offset) * Math.cos(path_i_end),
            cy_i + (r_i + link_radius_cp_offset) * Math.sin(path_i_end),
            cx_i + r_i * Math.cos(path_i_end),
            cy_i + r_i * Math.sin(path_i_end)
        );
        path.arc(cx_i, cy_i, r_i, path_i_end, path_i_start, true);
        path.bezierCurveTo(
            cx_i + (r_i + link_radius_cp_offset) * Math.cos(path_i_start),
            cy_i + (r_i + link_radius_cp_offset) * Math.sin(path_i_start),
            r_o * Math.cos(path_o_start),
            r_o * Math.sin(path_o_start),
            r_o * Math.cos(path_o_start),
            r_o * Math.sin(path_o_start)
        );

        return path.toString();
    }

I only needed to convert from polar to cartesian during the Bezier curve. The path.arc() already took polar coordinates.

Note that length_inner is the offset for the inner pie's cx and cy relative to the outer pie's cx and cy.

Comments