D3比例尺與座標軸

本文所用d3爲v5版本。

比例尺能將“一個區間”的數據映射到“另一個區間”。

例如[0, 1]對應到[0, 300],當輸入0.5時,輸出150。或者將[0, 1, 2]對應到["red", "green", "blue"],當輸入2時,輸出blue。

上述示例中的[0, 1][0, 1, 2]稱爲定義域[0, 300]["red", "green", "blue"]稱爲值域。定義域和值域之間的映射方法稱爲對應法則

要理解比例尺,就先需要理解定義域(domain)、值域(range)和對應法則這三個概念。

比例尺的分類

比例尺有連續比例尺、序列比例尺、發散比例尺、量化比例尺、分位數比例尺、閾值比例尺和序數比例尺、分段比例尺這幾種。

①.連續比例尺(Continuous Scales)

連續比例尺是一種比例尺類型,用連續定量的定義域映射連續的值域,具體包括:線性比例尺、指數比例尺、對數比例尺、定量恆等比例尺、線性時間比例尺、線性顏色比例尺。

連續比例尺有以下幾種通用方法:

  • continuousScale(x):向比例尺函數中傳入一個定義域內的值,返回在值域內對應的值。如果給定的 x 不在 domain 中,並且 clamping 沒有啓用,則返回的對應的值也會位於 range 之外,這是通過映射值推算出來的。
  • continuousScale.invert(y):向比例尺函數的invert方法中傳入一個值域內的值,返回定義域內對應的值。反向映射在交互中通常很有用,根據鼠標的位置計算對應的數據範圍。如果給定的 y 位於 range 外面,並且沒有啓用 clamping 則會推算出對應的位於 domain 之外的值。這個方法僅僅在 range 爲數值時有用。如果 range 不是數值類型則返回 NaN。
  • continuousScale.domain( [numbers] ):將數值數組指定爲當前比例尺的定義域或獲取當前比例尺定義域的拷貝,數組包含兩個或兩個以上元素,如果給定的數組中的元素不是數值類型,則會被強制轉爲數值類型。對於連續比例尺來說,定義域數值數組通常包含兩個值,但是如果指定大於兩個值的話會生成一個分位數的比例尺。
  • continuousScale.range( [values] ):指定當前比例尺的值域或獲取當前比例尺值域的拷貝。數組中元素不一定非要是數值類型,但如果要使用 invert 則 range 必須指定爲數值類型。
  • continuousScale.rangeRound( [values] ):代替range()使用的話,比例尺的輸出值會進行四捨五入的運算,結果爲整數。
  • continuousScale.clamp( [boolean] ):默認false,表示當比例尺接收一個超出定義域的值時,依然能按同樣的計算方法得到一個值,這個值可以是超出值域範圍的。當爲true時,任何超出值域範圍的值都會被收縮到值域範圍內。
  • continuousScale.interpolate( interpolate ):設置比例尺的值域插值器,插值器函數被用來在兩個相鄰的來自 range 值之間進行插值。
  • continuousScale.nice( [count] ):將定義域的範圍擴展成比較理想的形式。如定義域爲[0.500000543, 0.899999931]時,使用nice()後可以將定義域變成[ 0.5, 0.9 ]。應用nice()方法後,定義域會變成比較工整的形式,但不是四捨五入。
  • continuousScale.ticks( [count] ):默認返回一個近似的用來表示比例尺定義域的數組。如果傳入數值參數count,比例尺會以count爲參考來根據定義域計算具體的ticks。不傳count時默認count爲10.
  • continuousScale.tickFormat( count[, format] ):返回一個調整ticks數組元素的函數。ticks數組元素也叫刻度值。注意參數count的數值應與ticks中的參數保持一致。可選的format指定符可以讓開發者自定義ticks數組元素的格式,並且定義後會自動設置格式的精度,例如將數字格式化爲百分比。
  • continuousScale.copy():返回一個當前比例尺的拷貝。返回的拷貝和當前比例尺之間不會相互影響。

以下爲連續比例尺 Continuous Scales的通用方法示例,以線性比例尺爲方法載體進行測試:

// 線性比例尺
let xScale1 = d3.scaleLinear()
               .domain( [1, 5] )    // 通常連續比例尺中的domain只包含兩個值,但如果指定多個值時就會生成一個分位數的比例尺,例如創建一個分位數的顏色比例尺
               .range( [0, 300] )
              //  .clamp( true )
console.log( xScale1(3) );    // 150
console.log( xScale1.invert(100) );   // 2.333333333333333
console.log( xScale1(-10) );    // -825,如果設定clamp( true ),則此時返回值爲0
console.log( xScale1(10) );    // 675,如果設定clamp( true ),則此時返回值爲300


// 創建一個線性分位數顏色比例尺,傳入比例尺函數的值爲0.5時,返回的值是在 白色和綠色之間的插值
let xScale2 = d3.scaleLinear()
                .domain( [-1, 0, 1] )
                .range( ["red", "white", "green"] )
console.log( xScale2(0.5) );  // rgb(128, 192, 128)


// 通過ticks、tickFormat來個性化制定比例尺定義域值的表現形式
let xScale3 = d3.scaleLinear()
                .domain( [-1, 1] )
                .range( [0, 960] )

let ticks = xScale3.ticks( 5 );   
console.log( ticks );   // 返回一個近似的用來表示比例尺定義域的數組:[-1, -0.5, -0, 0.5, 1]

let tickFormatFn = xScale3.tickFormat( 5, "+" );    // 返回用來一個格式ticks數組每項值的函數
let res = ticks.map( tickFormatFn );    // 格式化ticks數組中的每項元素
console.log( res );   // ["-1", "-0.5", "+0", "+0.5", "+1"]

let tickFormatFn2 = xScale3.tickFormat( 5, "%" );    // 返回用來一個格式ticks數組每項值的函數
let res2 = ticks.map( tickFormatFn2 );    // 格式化ticks數組中的每項元素
console.log( res2 );   // ["-100%", "-50%", "0%", "50%", "100%"]

let tickFormatFn3 = xScale3.tickFormat( 5 );    // 沒有傳入第二個參數作爲說明符時,將不會對ticks數組的每項元素進行自定義格式
let res3 = ticks.map( tickFormatFn3 );    // 格式化ticks數組中的每項元素
console.log( res3 );   // ["-1.0", "-0.5", "0.0", "0.5", "1.0"]
a.線性比例尺

線性比例尺的創建方法是d3.scaleLiner()。默認定義域domain爲[0, 1],默認值域range是[0, 1],默認調用插值器方法interpolator,默認flase了clamp方法。它是良好支持連續定量的比例尺。每一個 range 中的值 y 都可以被表示爲一個函數:y = mx + b,其中 x 爲對應的 domain 中的值。

b.指數比例尺(power scale)、對數比例尺(log scale)

指數比例尺:d3.scalePow(),默認定義域domain爲[0, 1],默認值域range是[0, 1],默認指數 exponent 爲 1,默認調用插值器方法interpolator,默認flase了clamp方法。類似於線性比例尺,區別是在計算輸出的值域之前對定義域的值應用了指數變換。每個輸出值y可以表示爲x的一個函數:y = mx^k + b。相比普通連續比例尺,指數比例尺多了一個方法:pow.exponent( [exponent] ),用於指定或獲取指數比例尺的指數,當指數爲1時,與線性比例尺功效一樣。

// 定義指數比例尺,當沒有定義指定exponent時,默認指數爲1,此時功效與線性比例尺一樣
let xScale4 = d3.scalePow()   // 默認定義域domain爲[0, 1],值域range爲[0, 1]
                // .exponent( 2 )
console.log( xScale4(2) );  // 2

// 如果向指數比例尺的exponent()傳入數值參數時,就按照指數計算法則來計算
xScale4.exponent(2);  // 指數爲2
console.log( xScale4(2) );    // 4
xScale4.exponent(0.5);  // 指數爲0.5,其實就是求平方根,而求平方根也可以使用 d3.scaleSqrt() 方法
console.log( xScale4(2) );    // 1.4142135623730951

對數比例尺:d3.scaleLog(),默認定義域domain爲[0, 1],默認值域range是[0, 1],默認基數 base 爲 10,指定基數的方法是log.base([base]),默認調用插值器方法interpolator,默認flase了clamp方法。類似於線性比例尺,只不過在計算輸出值之前對輸入值進行了對數轉換。對應的 y 值可以表示爲 x 的函數:y = m log(x) + b。

c.恆等比例尺

恆等比例尺是線性比例尺的一種特殊情況,其定義域domain和值域range是完全一致的。創建恆等比例尺的方法是:d3.scaleIdentity()

d.時間比例尺

時間比例尺是線性比例尺的一種變體。它的輸入被強制轉爲日期類型而不是數值類型,並且invert返回的也是date類型。時間比例尺基於日曆間隔來實現ticks。創建時間比例尺的方法是:d3.scaleTime()

// 時間比例尺
let xScale5 = d3.scaleTime()
                .domain( [new Date(2000, 0, 1), new Date(2000, 0, 2)] )
                .range( [0, 960] )
console.log( xScale5( new Date(2000, 0, 1, 5) ) );    // 200
console.log( xScale5( new Date(2000, 0, 1, 16) ) );   // 640
console.log( xScale5.invert( 200 ) );   // Sat Jan 01 2000 05:00:00 GMT+0800 (中國標準時間)
console.log( xScale5.invert( 640 ) );   // Sat Jan 01 2000 16:00:00 GMT+0800 (中國標準時間)

②.序列比例尺

序列比例尺類似於連續比例尺,也是將連續的定義域domain映射到連續的值域range。但與連續比例尺不同的是,序列比例尺的值域是根據指定的插值器內置且不可配置,並且它的插值方式也不可配置。序列比例尺也沒有反轉invert值域range值域求整rangeRound插值器interpolate方法。

必須使用指定的interpolate函數才能創建序列比例尺,方法是d3.scaleSequential(interpolate)。注意序列比例尺的定義域domain值必須是數值,並且只包含兩個值

在應用序列比例尺時,可以傳入的值爲[0, 1]。其中 0 表示最小值,1 表示最大值。

// 序列比例尺
// 實現一個 HSL 具有周期性的顏色插值器
let xScale6 = d3.scaleSequential( function( t ){
  return d3.hsl( t*360, 1, 0.5 ) + "";
} )
console.log( xScale6(0) );    // rgb(255, 0, 0)
console.log( xScale6(0.8) );  // rgb(204, 0, 255)
console.log( xScale6(1) );    // rgb(255, 0, 0)

// 使用 d3.interpolateRainbow 實現一種更優雅並且更高效的週期性顏色插值器
let xScale7 = d3.scaleSequential( d3.interpolateRainbow )
console.log( xScale7(0) );    // rgb(110, 64, 170)
console.log( xScale7(0.5) );    // rgb(175, 240, 91)
console.log( xScale7(1) );    // rgb(110, 64, 170)

③.發散比例尺

發散比例尺同樣類似於序列比例尺和連續比例尺,也是將一個連續的定義域映射到連續的值域。但區別在於,發散比例尺的輸出是根據插值器計算並且不可配置。同樣沒有反轉invert值域range值域求整rangeRound插值器interpolate方法。

必須使用指定的interpolate函數才能創建發散比例尺,方法是d3.scaleDiverging(interpolate)

在應用發散比例尺時,插值器將會根據範圍爲[0, 1]的輸入值計算對應的輸出值,其中 0 表示負向極小值,0.5 表示中位值,1 表示正向極大值。例如使用 d3.interpolateSpectral:var spectral = d3.scaleDiverging(d3.interpolateSpectral);

④.量化比例尺 quantize scale

量化比例尺類似於線性比例尺,其定義域也是連續的,但值域是離散的,連續的定義域值會被分割成均勻的片段。

例如:定義域是[0, 10],值域是["red", "green", "blue", "yellow", "black"]。使用量化比例尺後,定義域將被分隔成5段,每一段對應值域的一個值。[0, 2)對應red,[2, 4)對應green,依次類推。因此量化比例尺就適合用在"數值對應顏色"的場景。例如中國各省份的GDP,數值越大就用顏色越深表示。

量化比例尺的創建方法是d3.scaleQuantize(),默認定義域是[0, 1],默認值域是[0, 1],默認創建的量化比例尺等效於Math.round函數。Math.round() 函數返回一個數字四捨五入後最接近的整數。

量化比例尺的應用場景可以有這幾個:

// 量化比例尺
let xScale8 = d3.scaleQuantize()
                .domain( [0, 1] )
                .range( [ "brown", "steelblue" ] )
console.log( xScale8( 0.49 ) );   // brown
console.log( xScale8( 0.51 ) );   // steelblue

// 將輸入域劃分爲三個三個大小相等、範圍值不同的片段來計算合適的筆畫寬度:
let xScale9 = d3.scaleQuantize()
                .domain( [10, 100] )
                .range( [1, 2, 4] )
console.log( xScale9(20) );   // 1
console.log( xScale9(50) );   // 2
console.log( xScale9(80) );   // 4

// 根據指定的值域中的值,計算對應的定義域中值的範圍 [x0, x1]。這個方法在交互時很有用,比如根據與鼠標像素對應值反推定義域的範圍。
let xScale10 = d3.scaleQuantize()
                 .domain( [10, 100] )
                 .range( [1, 2, 4] )
console.log( xScale10.invertExtent( 2 ) );  // [40, 70]

下面給個量化比例尺的座標軸實例,有幾個圓,圓的半徑越小,顏色越深:

// 定義量化比例尺
let quantizeScale = d3.scaleQuantize()
                      .domain( [ 0, 50 ] )
                      .range( ["#888", "#666", "#444", "#222", "#000"] );

// 定義圓的半徑
let r = [ 45, 35, 25, 15, 5 ];

console.log( quantizeScale( 29 ) );     // #444 通過定義域值查詢值域的值
console.log( quantizeScale.invertExtent( "#222" ) );    // [30, 40] 通過指定值域值反查定義域範圍

// 給body中添加svg元素
let svg = d3.select( "body" )
            .append( "svg" )
            .attr( "width", 500 )
            .attr( "height", 400 )

let circle = svg.selectAll( "circle" )
                .data( r )
                .enter()
                .append( "circle" )
                .attr( "cx", function( d, i ){ return 50 + i * 100 } )
                .attr( "cy", 50 )
                .attr( "r", function( d ){ return d } )
                .attr( "fill", function(d){ return quantizeScale(d) } )

效果截圖:

⑤.分位比例尺(quantile scale)

⑥.閾值比例尺(threshold scale)

⑦.序數比例尺(ordinal scale)

a.序數比例尺

和連續比例尺不同,序數比例尺的的定義域和值域都是離散的。實際場景中可能有需求根據名稱、序號等得到另一些離散的值如顏色頭銜等。此時就要考慮序數比例尺。

序數比例尺的創建方法是:d3.scaleOrdinal([range])

使用空的定義域和指定的值域構造一個序數比例尺。如果沒有指定值域則默認爲空數組。序數比例尺在定義非空的定義域之前,總是返回 undefined。

b.分段比例尺

分段比例尺類似於序數比例尺,區別在於分段比例尺的的定義域的值可以是連續的數值類型,而離散的值域則是將連續的定義域範圍劃分爲均勻的分段。分段通常用於包含序數或類別維度的條形圖。

創建分段比例尺的方法是:d3.scaleBand()

最後對各比例尺做個總結:

  • 連續比例尺(包括:線性比例尺、指數比例尺、對數比例尺、恆等比例尺、時間比例尺)、序列比例尺、發散比例尺都是將連續的定義域映射到連續的值域;
  • 量化比例尺是將連續的定義域映射到離散的值域;
  • 分位數比例尺是將離散的定義域映射到離散的值域;
  • 序數比例尺是將離散的定義域映射到離散的值域;
  • 分段比例尺是將離散的定義域映射到離散的值域;

座標軸

以下爲含有座標軸的柱狀圖代碼示例:

import * as d3 from "d3";

// 柱狀圖數據
let dataset = [ 20, 43, 120, 87, 99, 167, 142 ];

// 定義svg的寬高
let width = 600, height= 600;

// 定義SVG畫布
let svg = d3.select( "body" )   // 選擇body元素
            .append( "svg" )    // 添加svg元素
            .attr( "width", width )     // 定義svg畫布的寬度
            .attr( "height", height )   // 定義svg畫布的高度
            .style( "background-color", "#e5e5e5" )

// 定義svg內邊距
let padding = { top: 50, right: 50, bottom: 50, left: 50 };

// 矩形之間的間隙
let rectPadding = 20;

// 爲座標軸定義一個X軸的線性比例尺
let xScale = d3.scaleBand()
               .domain( d3.range(dataset.length) )
               .rangeRound( [0, width-padding.left-padding.right] )
// 使用給定的 xScale 構建一個刻度在下的X座標軸          
let xAxis = d3.axisBottom( xScale );

// 爲座標軸定義一個y軸的線性比例尺
let yScale = d3.scaleLinear()
               .domain( [0, d3.max( dataset )] )
               .range( [height-padding.top-padding.bottom, 0 ] )
               .nice()
// 使用給定的 yScale 構建一個刻度在左的y座標軸             
let yAxis = d3.axisLeft( yScale )

// 在svg畫布中特定位置放置X軸
svg.append( "g" )
   .attr( "transform", "translate( "+ padding.left +", "+ (height - padding.bottom) +" )" )
   .call( xAxis )

// 在svg畫布中特定位置放置Y軸   
svg.append( "g" )
   .attr( "transform", "translate( "+ padding.left +", "+ padding.top +" )" )
   .call( yAxis )

// 根據數據生成相應柱狀矩形
let rect = svg.append( "g" )
          .selectAll( "rect" )  // 獲取空選擇集
          .data( dataset )  // 綁定數據
          .enter()      // 獲取enter部分,因爲此時頁面上其實是沒有rect元素的,獲取的是空選擇集,此時就要在enter部分上進行操作
          .append( "rect" ) // 根據數據個數插入相應數量的rect元素
          .attr( "fill", "#377ade" )  
          .attr( "x", function( d, i ){     // 設置每個柱狀矩形的x座標,爲左內邊距 + X軸定義域值對應的值域的值 + 矩形間隙
            return padding.left + xScale(i) + rectPadding/2;
          } )
          .attr( "y", function( d, i ){     // 設置每個柱狀矩形的y座標
            return yScale(d) + padding.top;
          } )
          .attr( "width", xScale.step()-rectPadding )   // 設置每個柱狀矩形的寬度
          .attr( "height", function( d, i ){   // 設置每個柱狀矩形的高度,svg高度 - 上下內邊距 - Y軸定義域值對應的值域的值
            return height-padding.bottom-padding.top-yScale(d);
          } )


// 3.爲每個柱狀矩形添加標籤文字
let text = svg.append( "g" )
              .selectAll( "text" )  // 獲取空選擇集
              .data( dataset )      // 綁定數據
              .enter()              // 獲取enter部分
              .append( "text" )     // 爲每個數據添加對應的text元素
              .attr( "fill", "#fff" )
              .attr( "font-size", "14px" )
              .attr( "text-anchor", "middle" )  // 文本錨點屬性,中間對齊
              .attr( "x", function( d, i ){
                return xScale.step()/2 + xScale(i);
              } )
              .attr( "y", function( d, i ){
                return yScale(d) + padding.top;
              } )
              .attr( "dx", xScale.step()-rectPadding )
              .attr( "dy", "1em" )
              .text( function( d ){
                return d;
              } )

效果截圖:

以下爲含有座標軸的散點圖代碼示例:

import * as d3 from "d3";

// 定義圓心座標數組,數組中每個子數組的第一項表示圓心的 x 值,第二項表示圓心的 y 值
let center = [ 
    [0.5, 0.5],
    [0.7, 0.8],
    [0.4, 0.9],
    [0.11, 0.32],
    [0.88, 0.25],
    [0.75, 0.12],
    [0.5, 0.1],
    [0.2, 0.3],
    [0.4, 0.1],
    [0.6, 0.7],
 ]

// 定義svg的寬高
let width = 700, height = 600;
// 定義svg內邊距
let padding = { top: 50, right: 50, bottom: 50, left: 50 };

// 定義svg,並插入g元素
let gs = d3.select( "body" )
           .append( "svg" )
           .attr( "width", width )
           .attr( "height", height )
           .style( 'background-color', "#e5e5e5" )
           .append( "g" )

// 定義x軸比例尺,在設定定義域時,先取出center數組的每一個子數組的第一項(d[0])組成一個新數組,然後再用d3.max求最大值。最後再將最大值乘以1.2,這是爲了散點圖不會有某一點存在於x座標軸邊緣上。
let xScale = d3.scaleLinear()
               .domain( [0, 1.2*d3.max( center, function(d){ return d[0] } )] )
               .range( [0, width-padding.left-padding.right] )
               .nice()
// 創建一個刻度在下的x座標軸
let xAxis = d3.axisBottom( xScale );

// 定義y軸比例尺,在設定定義域時,先取出center數組的每一個子數組的第二項(d[1])組成一個新數組,然後再用d3.max求最大值。最後再將最大值乘以1.2,這是爲了散點圖不會有某一點存在於y座標軸邊緣上。
let yScale = d3.scaleLinear()
               .domain( [0, 1.2*d3.max( center, function(d){ return d[1] } )] )
               .range( [height-padding.top-padding.bottom, 0] )
               .nice()
// 創建一個刻度在右的y座標軸               
let yAxis = d3.axisLeft( yScale );

// svg中插入由g元素包裹的x座標軸
gs.append( "g" )
  .attr( "transform", "translate( "+ padding.left +", "+ (height-padding.bottom) +" )" )
  .call( xAxis )
// svg中插入由g元素包裹的y座標軸  
gs.append( "g" )
  .attr( "transform", "translate( "+ padding.left +", "+ padding.top +" )" )
  .call( yAxis )

// svg中插入由g元素包裹的散點圓  
gs.append( "g" )
  .selectAll( 'circle' )
  .data( center )
  .enter()
  .append( "circle" )
  .attr( "fill", "black" )
  .attr( "cx", function( d ){
    return padding.left + xScale(d[0]);
  } )
  .attr( "cy", function( d ){
    return padding.top + yScale(d[1]);
  } )
  .attr( "r", 5 )

// svg中插入由g元素包裹的座標文字
gs.append( "g" )
  .selectAll( "text" )
  .data( center )
  .enter()
  .append( "text" )
  .attr( "fill", "#999" )
  .attr( "font-size", "12px" )
  .attr( "text-anchor", "middle" )
  .attr( "x", function( d, i ){
    return padding.left + xScale(d[0]);
  } )
  .attr( "y", function( d, i ){
    return padding.top + yScale(d[1]);
  } )
  .attr( "dy", "-1em" )
  .text( function(d){
    return "[" + d[0] + " : " + d[1] + "]";
  } )

效果截圖:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章