你需要使用自定义路径数据生成函数。如果你不熟悉路径数据属性的工作原理,你可以从这个教程
开始学习(后续内容
在此和
在此)。由于这并不需要更多的工作,我建议将整个条形图作为自定义路径进行绘制,而不是有一个单独的基础部分。如果你真的想要单独的元素,那么很容易进行适应。你的条形图代码如下:
svg.selectAll(".bar")
.data(data)
.enter().append("rect")
.attr("class", "bar")
.attr("x", function(d) { return x(d.episode); })
.attr("width", x.rangeBand())
.attr("y", function(d) { return y(d.donations); })
.attr("height", function(d) { return height - y(d.donations); })
.style("fill", function(d) { return d.bar_color; })
.attr("rx", 10)
.attr("ry", 10)
.on("mouseover", function(d) {
tooltip.transition().duration(200).style("opacity", .9);
tooltip.html("NGO: " + d.ngo)
.style("left", (d3.event.pageX) + "px")
.style("top", (d3.event.pageY - 28) + "px");
})
.on("mouseout", function(d) {
tooltip.transition().duration(500).style("opacity", 0);
});
鼠标悬停的工具提示功能不会改变,所以我将在讨论的其余部分中略过它们。填充样式也不会改变,类名也不会改变。然而,我们要把<rect>
元素转换为<path>
元素,并用路径数据"d"属性替换所有其他属性(x/y/width/height/rx/ry):
svg.selectAll(".bar")
.data(data)
.enter().append("path")
.attr("class", "bar")
.style("fill", function(d) { return d.bar_color; })
.attr("d", function(d) {
return ;
})
.on("mouseover",
现在,让我们分析一下那个形状。从左下角开始,您需要以下方向来绘制它:
- 移动到酒吧之间填充物中间的轴线上的膨胀基底的起点。
- 弯曲到距轴线等距离的条的左侧边缘,与膨胀半径相同。
- 几乎直线到达栏杆的顶部,最高点是条宽的一半的顶部曲率半径。
- 半圆(或两个曲线)一直上升到完整的栏杆高度,然后回到另一侧。
- 几乎沿轴线向下的直线。
- 创建另一个膨胀的基底,向外弯曲到填充物的一半。
- 关闭路径以连接回开始。
对于移动命令和线路命令,您只需要提供目标(x,y)点。对于关闭命令,您根本不需要提供任何点。对于曲线,情况会变得有点复杂。您可以使用弧形(请参阅我的第二个教程的语法),但是如果您使用二次曲线,控制点是您要围绕的角落位置,那么它会变得稍微简单一些,并且看起来几乎相同。也就是说,从(0,0)到(1,1)的二次曲线,控制点为(0,1),几乎是以(1,0)为中心的弧线。
(0,0)
@ — * << control point (0,1)
\
| ...something like this, but prettier!
@
(1,1)
有了这个攻击计划,并确定从作为参数传递给x.rangeRoundBands()
的paddingProportion中得出的flare的宽度,您可以获得以下结果:
.attr("d", function(d) {
var barWidth = x.rangeBand();
var paddingWidth = barWidth*(paddingProportion)/(1-paddingProportion);
var flareRadius = paddingWidth/2;
var topRadius = barWidth/2;
var xPos = x(d.episode);
var yPos = y(d.donations);
return [ "M", [ (xPos - flareRadius), height],
"Q", [xPos, height],
[xPos, (height-flareRadius)],
"L", [xPos, (yPos + topRadius)],
"Q", [xPos, yPos],
[(xPos + topRadius), yPos],
"Q", [(xPos + barWidth), yPos],
[(xPos + barWidth), (yPos + topRadius)],
"L", [(xPos + barWidth), (height-flareRadius)],
"Q", [(xPos + barWidth), height],
[(xPos + barWidth + flareRadius), height],
"Z"
].join(" ");
})
最终返回的值是一个字符串,我将它组合成字母数组和每个(x,y)点的两个元素数组,然后用空格将它们全部连接在一起。这比使用 +
更快,并保持数据处于有组织的结构中。最终返回的字符串看起来像
http://jsfiddle.net/rdesai/MjFgK/62/
M 47.75,195 Q 52,195 52,190.75 L 52,26.056240786240778 Q 52,9.056240786240778 69,9.056240786240778 Q 86,9.056240786240778 86,26.056240786240778 L 86,190.75 Q 86,195 90.25,195 Z
现在,这基本上是您想要的形状,但在最短的条形图上会有一些混乱,其中顶部曲线的半径大于条形的高度。进行一些快速的最大/最小值检查,以确保点的y值永远不会小于yPos
或大于height
,可以使事情变得清晰:
.attr("d", function(d) {
var barWidth = x.rangeBand();
var paddingWidth = barWidth*(paddingProportion)/(1-paddingProportion);
var flareRadius = paddingWidth/2;
var topRadius = barWidth/2;
var xPos = x(d.episode);
var yPos = y(d.donations);
return [ "M", [ (xPos - flareRadius), height],
"Q", [xPos, height],
[xPos, Math.max((height-flareRadius), yPos)],
"L", [xPos, Math.min((yPos + topRadius), height)],
"Q", [xPos, yPos],
[(xPos + topRadius), yPos],
"Q", [(xPos + barWidth), yPos],
[(xPos + barWidth), Math.min((yPos + topRadius), height)],
"L", [(xPos + barWidth), Math.max((height-flareRadius), yPos)],
"Q", [(xPos + barWidth), height],
[(xPos + barWidth + flareRadius), height],
"Z"
].join(" ");
})
http://jsfiddle.net/MjFgK/63/
这是很多内容,建议你花些时间去尝试一下,试着变换形状,尝试使用弧线替代二次曲线,试着重新创建样本图像中第一个和最后一个条的形状。或者回到最初的计划,将基础形状作为单独的元素创建。