d3js shape深入理解
2021-06-22 03:03
本文将视图了解d3js提供的帮助我们创建矢量图形的helper函数,比如下面的:
http://d3indepth.com/shapes/
lines
curves
pie chart segments
symbols
SVG
首先我们来认识一下SVG(scalable vector graphics).要知道上面例子中的图形实际上都是由SVG的path元素构成的。每张图都有不同的path元素来组成,这些path元素本身的d属性来定义图形的path.而path data由一系列的命令组成(比如: M0,80L100,100L200,30L300,50L400,40L500,80),如下面的代码所示:
path d="M12.061098497445768,-54.465861201009005A4,4,0,0,1,17.122890866582928,-57.50483986910224A60,
60,0,0,1,29.32181418613459,-52.34721781368898A4,4,0,0,1,30.66836903336433,-46.59883339919623L5.38995775477549,
-9.649482084709554A1.2313393364975447,1.2313393364975447,0,0,1,3.1668950166423597,-10.589377935221732Z">path>
这些微代码由浏览器来解读,比如 ‘move to’ , ‘draw a line to’等,详细可以参考 SVG specification
实际上,我们可以自己通过书写代码来创建这些path命令集,但是你会发现,写出这些代码虽然并不难,但是一定是很繁琐的,d3js为了将我们从这些繁琐的工作中解放出来,d3js的作者就发明了被成为"path generator"的路径生成器。下面我们列出常见的路径生成器:
line | Generates path data for a multi-segment line (typically for line charts) |
area | Generates path data for an area (typically for stacked line charts and streamgraphs) |
stack | Generates stack data from multi-series data |
arc | Generates path data for an arc (typically for pie charts) |
pie | Generates pie angle data from array of data |
symbol | Generates path data for symbols such as plus, star, diamond |
Line generator
我们给d3一个(x,y)坐标的数组,d3就能生成一串path data string
我们首先声明一个line generator:
var lineGenerator = d3.line();
lineGenerator的任务就是接收一个坐标数组,输出一个path data string直接可以作为path元素的d属性值。
我们来定义一个坐标数组
var points = [ [0, 80], [100, 100], [200, 30], [300, 50], [400, 40], [500, 80] ];
接着我们传入points参数来调用lineGenerator,
var pathData = lineGenerator(points); // pathData is "M0,80L100,100L200,30L300,50L400,40L500,80"
lineGenerator完成的工作就是创建了一个M(move to)和L(line to)命令的字符串,这样我们就可以使用pathData来给d属性赋值
d3.select(‘path‘)
.attr(‘d‘, pathData);
最终浏览器渲染出来下面的线图:
对于line generator函数,我们可以有以下可以配置
-
.x()
and.y()
accessor functions, -
.defined()
(to handle missing data), -
.curve
(to specify how the points are interpolated) and -
.context()
to render to a canvas element.
.x() and .y() accessor functions
By default each array element represents a co-ordinate defined by a 2-dimensional array (e.g. [0, 100]
). However we can specify how the line generator interprets each array
默认情况下每一个数组元素都代表了一个2纬的数组,比如[0,100],然而我们也可以告诉line generator来如何解读传入的数据,而这就要使用对应的accessor functions了:.x()和.y().例如假设我们有以下对象的数组:
var data = [ {value: 10}, {value: 50}, {value: 30}, {value: 40}, {value: 20}, {value: 70}, {value: 50} ];
我们就可以这样来定义accessor函数:
lineGenerator .x(function(d, i) { return xScale(i); }) .y(function(d) { return yScale(d.value); });
在这个例子中,我们使用数组的index来定义x,(注意我们同时使用了比例尺函数)
.defined()
我们可以使用defined函数来定义如果有部分数据不应该渲染的情况下如何操作,比如下面的数据:
var points = [ [0, 80], [100, 100], null, [300, 50], [400, 40], [500, 80] ];
我们告知line generator每一个坐标只有是non-null时才是有效的,通过下面的代码:
lineGenerator .defined(function(d) { return d !== null; });
这样当我们再次调用lineGenerator并渲染后就将得到一个有着断续的line:
注意:如果没有上面的.defined定义的话,将会产生一个错误,因为null无法获取到对应的x,y坐标
.curve()
我们也可以定义这个path的points之间是如何插值的。比如,我们可以使用一个B-spline算来来插值:
var lineGenerator = d3.line() .curve(d3.curveCardinal);
虽然有相当多的不同类型的curve type在d3中可以使用,但是我们也可以把这些插值类型简单分为两类:一种是必须经过指定points的类型(比如:curveLinear,curveCardinal,curveCatmullRom,curveMonotone,curveNatural,curveStep),另一种是不必经过每一个points(比如curveBasics和curveBundle)
See the curve explorer for more information.
Rendering to canvas
By default the shape generators output SVG path data. However they can be configured to draw to a canvas element using the .context()
function:
默认情况下shape generator输出SVG path data.然而我们也可以通过.context()函数来指定使用canvas来绘图:
var context = d3.select(‘canvas‘).node().getContext(‘2d‘); lineGenerator.context(context); context.strokeStyle = ‘#999‘; context.beginPath(); lineGenerator(points); context.stroke();
Radial line
The radial line generator is similar to the line generator but the points are transformed by angle (working clockwise from 12 o’clock) and radius, rather than x
and y
:
radial line generator和普通的line generator是类似的,唯一的不同是该generator对坐标的解读是“极坐标系”(从12点开始顺时针运行的)角度和半径,而不是x,y坐标:
var radialLineGenerator = d3.radialLine(); var points = [ [0, 80], [Math.PI * 0.25, 80], [Math.PI * 0.5, 30], [Math.PI * 0.75, 80], [Math.PI, 80], [Math.PI * 1.25, 80], [Math.PI * 1.5, 80], [Math.PI * 1.75, 80], [Math.PI * 2, 80] ]; var pathData = radialLineGenerator(points);
Accessor functions .angle()
and .radius()
are also available:
类似于lineGenerator,对应的accessor function .angle()和.radius()如下:
radialLineGenerator .angle(function(d) { return d.a; }) .radius(function(d) { return d.r; }); var points = [ {a: 0, r: 80}, {a: Math.PI * 0.25, r: 80}, {a: Math.PI * 0.5, r: 30}, {a: Math.PI * 0.75, r: 80}, ... ]; var pathData = radialLineGenerator(points);
Area generator
The area generator outputs path data that defines an area between two lines. By default it generates the area between y=0
and a multi-segment line defined by an array of points:
区域生成器输出一个通过两条lines来定义的一个区域的path data来工作的。默认情况下,它在y=0的水平线和一个由一个点数组来定义的多段线线之间生成一个区域:
var areaGenerator = d3.area(); var points = [ [0, 80], [100, 100], [200, 30], [300, 50], [400, 40], [500, 80] ]; var pathData = areaGenerator(points);
我们可以通过.y0() accessor函数来定义这个base line:
areaGenerator.y0(150);
就变成了如下的图形:(通常情况下我们使用图形的height作为base line)
我们也可以给.y0() accessor函数一个函数来指明如何获取y0的值,类似于.y1() accessor
areaGenerator .x(function(d) { return d.x; }) .y0(function(d) { return yScale(d.low); }) .y1(function(d) { return yScale(d.high); }); var points = [ {x: 0, low: 30, high: 80}, {x: 100, low: 80, high: 100}, {x: 200, low: 20, high: 30}, {x: 300, low: 20, high: 50}, {x: 400, low: 10, high: 40}, {x: 500, low: 50, high: 80} ];
典型地,.y0()定义了base line, .y1()定义了top line. 注意我们也使用了.x()定义了x坐标(base line和top line使用的是相同的x坐标哦!!)
和line generator一样,我们可以指定在点之间是如何插值的(.curve()),以及如何处理missing data(.defined())以及如何在canvas而不是在svg中渲染(.context())
Radial area
radial area generator和area generator是类似的,唯一的不同是这些点我们是使用angle角度(从12点顺时针开始的角度)和半径radius来定义的,而不是x和y来定义的:
var radialAreaGenerator = d3.radialArea() .angle(function(d) { return d.angle; }) .innerRadius(function(d) { return d.r0; }) .outerRadius(function(d) { return d.r1; }); var points = [ {angle: 0, r0: 30, r1: 80}, {angle: Math.PI * 0.25, r0: 30, r1: 70}, {angle: Math.PI * 0.5, r0: 30, r1: 80}, {angle: Math.PI * 0.75, r0: 30, r1: 70}, {angle: Math.PI, r0: 30, r1: 80}, {angle: Math.PI * 1.25, r0: 30, r1: 70}, {angle: Math.PI * 1.5, r0: 30, r1: 80}, {angle: Math.PI * 1.75, r0: 30, r1: 70}, {angle: Math.PI * 2, r0: 30, r1: 80} ];
如下图所示:
Stack generator
stack generator接收一个multi-series data而对每一个series来生成一个数组,而每个数组包含着各data point的lower和upper values
lower and upper values are computed so that each series is stacked on top of the previous series.
lower和upper values被用于计算位置这样每个series都堆叠在前一个series上面
var data = [ {day: ‘Mon‘, apricots: 120, blueberries: 180, cherries: 100}, {day: ‘Tue‘, apricots: 60, blueberries: 185, cherries: 105}, {day: ‘Wed‘, apricots: 100, blueberries: 215, cherries: 110}, {day: ‘Thu‘, apricots: 80, blueberries: 230, cherries: 105}, {day: ‘Fri‘, apricots: 120, blueberries: 240, cherries: 105} ]; var stack = d3.stack() .keys([‘apricots‘, ‘blueberries‘, ‘cherries‘]); var stackedSeries = stack(data); // stackedSeries = [ // [ [0, 120], [0, 60], [0, 100], [0, 80], [0, 120] ], // Apricots // [ [120, 300], [60, 245], [100, 315], [80, 310], [120, 360] ], // Blueberries // [ [300, 400], [245, 350], [315, 425], [310, 415], [360, 465] ] // Cherries // ]
.keys()配置函数定义了在stack generation中哪些series被包含其中。
stack generator输出的数据,你可以随意使用,但是典型地,这个输出数据被用于产生stacked bar charts:
or when used in conjunction with the area generator, stacked line charts:
或者,如果和area generator配合使用,形成stacked line charts:
.order()
stacked series出现的顺序可以由.order()配置函数来
stack.order(d3.stackOrderInsideOut);
每个series汇总后通过选择的顺序来排列,可用的顺序如下:
stackOrderNone | (Default) Series in same order as specified in .keys() |
stackOrderAscending | Smallest series at the bottom |
stackOrderDescending | Largest series at the bottom |
stackOrderInsideOut | Largest series in the middle |
stackOrderReverse | Reverse of stackOrderNone |
.offset()
默认情况下stacked series的baseline为0.然而我们也可以配置stack generator的offset来达到不同的baseline效果。比如,我们可以规整stacked series以便他们都有着相同的高度:
stack.offset(d3.stackOffsetExpand);
可用的offset如下:
stackOffsetNone | (Default) No offset |
stackOffsetExpand | Sum of series is normalised (to a value of 1) |
stackOffsetSilhouette | Center of stacks is at y=0 |
stackOffsetWiggle | Wiggle of layers is minimised (typically used for streamgraphs) |
下面就是一个使用了stackOffsetWiggle的stacked chart
:
Arc generator
Arc generators produce path data from angle and radius values. An arc generator is created using:
arc generator用于从angle和radius值来产生path data.下面创建一个arc
var arcGenerator = d3.arc();
随后可以给该generator传入一个包含着startAngle, endAngle,innerRadius,outerRadius属性值的对象来生成path data:
var pathData = arcGenerator({ startAngle: 0, endAngle: 0.25 * Math.PI, innerRadius: 50, outerRadius: 100 }); // pathData is "M6.123233995736766e-15,-100A100,100,0,0,1,70.71067811865476,-70.710678 // 11865474L35.35533905932738,-35.35533905932737A50,50,0,0,0,3.061616997868383e-15,-50Z"
(注意:startAngle
and endAngle
是从12点开始顺时针计量的角度数)
Configuration
We can configure innerRadius
, outerRadius
, startAngle
, endAngle
so that we don’t have to pass them in each time:
我们可以使用innerRadius,outerRadius,startAngle,endAngle函数来配置,而不用每次生成arc generator时传入对象参数:
arcGenerator .innerRadius(20) .outerRadius(100); pathData = arcGenerator({ startAngle: 0, endAngle: 0.25 * Math.PI }); // pathData is "M6.123233995736766e-15,-100A100,100,0,0,1,70.71067811865476,-70.71067811 // 865474L14.142135623730951,-14.14213562373095A20,20,0,0,0,1.2246467991473533e-15,-20Z"
We can also configure corner radius (cornerRadius
) and the padding between arc segments (padAngle
and padRadius
):
我们也可以配置corner radius(corner Radius)以及弧线段之间的padding值(padAngle,padRadius)
arcGenerator .padAngle(.02) .padRadius(100) .cornerRadius(4);
Arc padding takes two parameters padAngle
and padRadius
which when multiplied together define the distance between adjacent segments. Thus in the example above, the padding distance is 0.02 * 100 = 2
. Note that the padding is calculated to maintain (where possible) parallel segment boundaries.
Arc padding有两个参数:padAngle和padRadius他们合在一起定义了两个相邻弧段之间的距离。这样在上面的例子中,padding distance就是0.02*100=2.注意padding计算时会尽可能地保持(如果可能的话)弧段之间的边界保持平行。
你可能会问
你可能会问为什么不用一个简单的padDistance参数来定一个这个padding distance,而使用两个参数相乘这么复杂的参数来定义呢?之所以这样,是因为pie generator无需关心半径的大小。
Accessor functions
我们也可以为startAngle, endAngle, innerRadius,outerRadius来定义对应的accessor functions
arcGenerator .startAngle(function(d) { return d.startAngleOfMyArc; }) .endAngle(function(d) { return d.endAngleOfMyArc; }); arcGenerator({ startAngleOfMyArc: 0, endAngleOfMyArc: 0.25 * Math.PI });
Centroid
有时,计算弧线的中心点是很有用的,比如当我们要放置label时,往往希望放到中心点附件,d3给我们提供了一个.centroid()函数来实现:
arcGenerator.centroid({ startAngle: 0, endAngle: 0.25 * Math.PI }); // returns [22.96100594190539, -55.43277195067721]
下面的例子我们使用.centroid()来计算label的位置:
// Create an arc generator with configuration var arcGenerator = d3.arc() .innerRadius(20) .outerRadius(100); var arcData = [ {label: ‘A‘, startAngle: 0, endAngle: 0.2}, {label: ‘B‘, startAngle: 0.2, endAngle: 0.6}, {label: ‘C‘, startAngle: 0.6, endAngle: 1.4}, {label: ‘D‘, startAngle: 1.4, endAngle: 3}, {label: ‘E‘, startAngle: 3, endAngle: 2* Math.PI} ]; // Create a path element and set its d attribute d3.select(‘g‘) .selectAll(‘path‘) .data(arcData) .enter() .append(‘path‘) .attr(‘d‘, arcGenerator); // Add labels, using .centroid() to position d3.select(‘g‘) .selectAll(‘text‘) .data(arcData) .enter() .append(‘text‘) .each(function(d) { var centroid = arcGenerator.centroid(d); d3.select(this) .attr(‘x‘, centroid[0]) .attr(‘y‘, centroid[1]) .attr(‘dy‘, ‘0.33em‘) .text(d.label); });
Pie generator
The pie generator goes hand in hand with the arc generator. Given an array of data, the pie generator will output an array of objects containing the original data augmented by start and end angles:
pie generator和弧线generator是类似的。给pie generator一个数据数组,pie generator就会输出一个反映了原始start和end angles数据的对象数
var pieGenerator = d3.pie(); var data = [10, 40, 30, 20, 60, 80]; var arcData = pieGenerator(data); // arcData is an array of objects: [ // { // data: 10, // endAngle: 6.28..., // index: 5, // padAngle: 0, // startAngle: 6.02..., // value: 10 // }, // ... // ]
We can then use an arc generator to create the path strings:
随后,我们可以使用一个arc generator来创建对应的path strings
var arcGenerator = d3.arc() .innerRadius(20) .outerRadius(100); d3.select(‘g‘) .selectAll(‘path‘) .data(arcData) .enter() .append(‘path‘) .attr(‘d‘, arcGenerator);
注意pieGenerator的输出包含了startAngle和endAngle两个属性。这些正好是arcGenerator所需要的输入!
再注意:实际上pieGenerator是一个layout了,而不是路径生成器!
The pie generator has a number of configuration functions including .padAngle()
, .startAngle()
, .endAngle()
and .sort()
. .padAngle()
specifies an angular padding (in radians) between neighbouring segments.
pie generator包含了一系列的配置函数,包括:.padAngle(),.startAngle(),.endAngle(),.sort(),.padAngle()定义了一个在相邻段之间的angular padding
.startAngle()
and .endAngle()
配置pie chart的startAngle和endAngle. 这将允许创建半圆形的pie charts:
var pieGenerator = d3.pie() .startAngle(-0.5 * Math.PI) .endAngle(0.5 * Math.PI);
默认情况下段的起始和结束角度使得段之间以降序排列。然而我们可以更改这种排序方式,方法是使用.sort()函数:
var pieGenerator = d3.pie() .value(function(d) {return d.quantity;}) .sort(function(a, b) { return a.name.localeCompare(b.name); }); var fruits = [ {name: ‘Apples‘, quantity: 20}, {name: ‘Bananas‘, quantity: 40}, {name: ‘Cherries‘, quantity: 50}, {name: ‘Damsons‘, quantity: 10}, {name: ‘Elderberries‘, quantity: 30}, ];
Symbols
The symbol generator produces path data for symbols commonly used in data visualisation:
symbol generator用于产生在可视化领域中常用的symbol的path data:
var symbolGenerator = d3.symbol() .type(d3.symbolStar) .size(80); var pathData = symbolGenerator();
随后我们就可以使用这个pathData变量作为path元素的d属性了:
d3.select(‘path‘)
.attr(‘d‘, pathData);
var symbolGenerator = d3.symbol() .type(d3.symbolStar) .size(80); var points = [ [0, 80], [100, 100], [200, 30], [300, 50], [400, 40], [500, 80] ]; var pathData = symbolGenerator(); d3.select(‘g‘) .selectAll(‘path‘) .data(points) .enter() .append(‘path‘) .attr(‘transform‘, function(d) { return ‘translate(‘ + d + ‘)‘; }) .attr(‘d‘, pathData);
D3包含了下面的symbol types:
var symbolGenerator = d3.symbol() .size(100); var symbolTypes = [‘symbolCircle‘, ‘symbolCross‘, ‘symbolDiamond‘, ‘symbolSquare‘, ‘symbolStar‘, ‘symbolTriangle‘, ‘symbolWye‘]; var xScale = d3.scaleLinear().domain([0, symbolTypes.length - 1]).range([0, 700]); d3.select(‘g‘) .selectAll(‘path‘) .data(symbolTypes) .enter() .append(‘path‘) .attr(‘transform‘, function(d, i) { return ‘translate(‘ + xScale(i) + ‘, 0)‘; }) .attr(‘d‘, function(d) { symbolGenerator .type(d3[d]); return symbolGenerator(); }); d3.select(‘g‘) .selectAll(‘text‘) .data(symbolTypes) .enter() .append(‘text‘) .attr(‘transform‘, function(d, i) { return ‘translate(‘ + xScale(i) + ‘, 40)‘; }) .text(function(d) { return ‘d3.‘ + d; });