本文共 13993 字,大约阅读时间需要 46 分钟。
本文所用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)和对应法则这三个概念。
比例尺有连续比例尺、序列比例尺、发散比例尺、量化比例尺、分位数比例尺、阈值比例尺和序数比例尺、分段比例尺这几种。
连续比例尺是一种比例尺类型,用连续定量的定义域映射连续的值域,具体包括:线性比例尺、指数比例尺、对数比例尺、定量恒等比例尺、线性时间比例尺、线性颜色比例尺。
连续比例尺有以下几种通用方法:
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) ); // 150console.log( xScale1.invert(100) ); // 2.333333333333333console.log( xScale1(-10) ); // -825,如果设定clamp( true ),则此时返回值为0console.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"]
线性比例尺的创建方法是d3.scaleLiner()
。默认定义域domain为[0, 1]
,默认值域range是[0, 1]
,默认调用插值器方法interpolator
,默认flase了clamp方法。它是良好支持连续定量的比例尺。每一个 range 中的值 y 都可以被表示为一个函数:y = mx + b
,其中 x 为对应的 domain 中的值。
指数比例尺: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); // 指数为2console.log( xScale4(2) ); // 4xScale4.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。
恒等比例尺是线性比例尺的一种特殊情况,其定义域domain和值域range是完全一致的。创建恒等比例尺的方法是:d3.scaleIdentity()
。
时间比例尺是线性比例尺的一种变体。它的输入被强制转为日期类型而不是数值类型,并且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) ) ); // 200console.log( xScale5( new Date(2000, 0, 1, 16) ) ); // 640console.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);
。
量化比例尺类似于线性比例尺,其定义域也是连续的,但值域是离散的,连续的定义域值会被分割成均匀的片段。
例如:定义域是[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 ) ); // brownconsole.log( xScale8( 0.51 ) ); // steelblue// 将输入域划分为三个三个大小相等、范围值不同的片段来计算合适的笔画宽度:let xScale9 = d3.scaleQuantize() .domain( [10, 100] ) .range( [1, 2, 4] )console.log( xScale9(20) ); // 1console.log( xScale9(50) ); // 2console.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) } )
效果截图:
和连续比例尺不同,序数比例尺的的定义域和值域都是离散的。实际场景中可能有需求根据名称、序号等得到另一些离散的值如颜色头衔等。此时就要考虑序数比例尺。
序数比例尺的创建方法是:d3.scaleOrdinal([range])
。
使用空的定义域和指定的值域构造一个序数比例尺。如果没有指定值域则默认为空数组。序数比例尺在定义非空的定义域之前,总是返回 undefined。
分段比例尺类似于序数比例尺,区别在于分段比例尺的的定义域的值可以是连续的数值类型,而离散的值域则是将连续的定义域范围划分为均匀的分段。分段通常用于包含序数或类别维度的条形图。
创建分段比例尺的方法是: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] + "]"; } )
效果截图:
转载地址:http://legx.baihongyu.com/