d3js shape深入理解

世有因果知因求果發表於2017-07-14

本文將檢視瞭解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:實際上stack是一個layout
arc Generates path data for an arc (typically for pie charts)
pie Generates pie angle data from array of data:實際上pie是一個layout
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;
    });