d3中的尺度就如同数学中映射的概念,从定义域映射到值域,d3中尺度初学其实掌握好连续尺度,有序尺度就足够后面的应用了。
具体尺度的 api 可以跳转官方文档 github.com/d3/d3/blob/…
连续尺度
连续尺度可以帮我们将连续的定义域映射到一个连续的值域。
连续尺度包括 线性尺度,幂级尺度,对数尺度,时间尺度。
线性尺度 幂级尺度 对数尺度
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<style>
.clear {
clear: both;
}
.cell {
min-width: 40px;
min-height: 20px;
margin: 5px;
float: left;
text-align: center;
border: #969696 solid thin;
padding: 5px;
}
</style>
</head>
<body>
<div id="linear" class="clear"><span>n</span></div>
<div id="linear-capped" class="clear"><span>1 <= a*n + b <= 20</span></div>
<div id="pow" class="clear"><span>n^2</span></div>
<div id="pow-capped" class="clear"><span>1 <= a*n^2 + b <= 10</span></div>
<div id="log" class="clear"><span>log(n)</span></div>
<div id="log-capped" class="clear"><span>1 <= a*log(n) + b <= 10</span></div>
<script src="../d3.js"></script>
<script>
const data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
const linear = d3.scaleLinear()
.domain([1, 10])
.range([1, 10])
const linearCapped = d3.scaleLinear()
.domain([1, 10])
.range([1, 10])
const pow = d3.scalePow()
.exponent(2)
.domain([1, 10])
const powCapped = d3.scalePow()
.exponent(2)
.domain([1, 10])
.rangeRound([1, 10])
const log = d3.scaleLog()
.domain([1, 10])
const logCapped = d3.scaleLog()
.domain([1, 10])
.rangeRound([1, 10])
function render(data, scale, selector) {
d3.select(selector).selectAll('div').data(data)
.enter()
.append('div')
.classed('cell', true)
.style('display', 'inline-block')
.text(function (d) {
return d3.format('.2')(scale(d), 2)
})
}
render(data, linear, '#linear')
render(data, linearCapped, '#linear-capped')
render(data, pow, '#pow')
render(data, powCapped, '#pow-capped')
render(data, log, '#log')
render(data, logCapped, '#log-capped')
</script>
</body>
</html>
效果如下:
对于尺度而言,大部分方法的作用其实都是一样的:
- domain() 设置定义域
- range() 设置值域
- rangeRound() 设置值域,值域取值前会进行 round 操作
另外对于幂级尺度 scalePow 来说,有 exponent() 设置幂指数,默认为 1,即默认和线性尺度 scaleLinear 相同,对于对数尺度 scaleLog 来说,有 base() 设置对数的基数,默认为 10。
了解这些后,上面的例子其实就特别简单。
时间尺度
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<style>
.clear {
clear: both;
}
.fixed-cell {
min-width: 40px;
min-height: 20px;
margin: 5px;
position: fixed;
text-align: center;
border: #969696 solid thin;
padding: 5px;
}
</style>
</head>
<body>
<div id="time" class="clear">
</div>
<script src="../d3.js"></script>
<script>
const start = new Date(2019, 0, 1)
const end = new Date(2019, 11, 31)
const data = []
for(let i = 0; i < 12; i++) {
const date = new Date(start.getTime())
date.setMonth(i)
data.push(date)
}
const time = d3.scaleTime()
.domain([start, end])
.rangeRound([0, 1200])
function render(data, scale, selector) {
d3.select(selector).selectAll('div.fixed-cell')
.data(data)
.enter()
.append('div')
.classed('fixed-cell', true)
.style('margin-left', function (d) {
return scale(d) + 'px'
})
.html(function (d) {
return d3.timeFormat('%x')(d) + '<br/>' + scale(d) + 'px'
})
}
render(data, time, '#time')
</script>
</body>
</html>
效果如下:
这里我们创建了定义域在 2019/1/1 到 2019/12/31 的 Date实例,设置其值域为 0 到 1200,data 中的每一个日期数据项都对应到了一个值去设置边距。
有序尺度
有序尺度是将离散的定义域映射到离散的值域。
经常可以见到的就是搭配d3内置的颜色方案来实现数据对应离散颜色的实现。
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<style>
.clear {
clear: both;
}
.cell {
min-width: 40px;
min-height: 20px;
margin: 5px;
float: left;
text-align: center;
border: #969696 solid thin;
padding: 5px;
}
</style>
</head>
<body>
<div id="alphabet" class="clear">
<span>Ordinal Scale with Alphabet<br></span>
<span>Mapping [1...10] to ['a'...'j']<br></span>
</div>
<div id="category10" class="clear">
<span>Ordinal Scale with Alphabet<br></span>
<span>Mapping [1...10] to category 10 colors<br></span>
</div>
<script src="../d3.js"></script>
<script>
const data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
const alphabet = d3.scaleOrdinal()
.domain(data)
.range(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'])
function render(data, scale, selector) {
d3.select(selector).selectAll('div.cell')
.data(data)
.enter()
.append('div')
.classed('cell', true)
.style('display', 'inline-block')
.style('background-color', function (d) {
return scale(d).includes('#') ? scale(d) : 'white'
})
.text(function (d) {
return scale(d)
})
}
render(data, alphabet, '#alphabet')
render(data, d3.scaleOrdinal(d3.schemeCategory10), '#category10')
</script>
</body>
</html>
效果如下:
我们创建了两个有序尺度,一个将数字和字母对应,一个是用默认的颜色方案创建
在 alphabet 尺度中,不在离散定义域上的值是会重复依照顺序来进行对应的:
我们小改下代码
const data = [1.1, 1.9, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]
const alphabet = d3.scaleOrdinal()
.domain([1, 13])
.range(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'])
发现效果成了这样
定义域中只有两个离散值 1 和 13,他们对应了 a 和 b,然后在 data 的数据对应中,1.1 到 12 都不在定义域内,所以它们离散的对应了字母 c 到 e,并且我们可以得知:不在值域内的离散值轮完一遍是会重复轮值的,并且第一次轮值是从没有建立对应关系的离散值开始。在值域内的离散值会按照初始顺序进行值的对应并固定下来(到 13 时瞬间又对应到了 b )
尺度通过内建的插值器实现了根据不同的输入选取不同的输出,另外,插值器在如动画和布局管理功能时也起到了核心的作用。下面展示一些在尺度中有大概率使用的三种插值情况:
字符串插值
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<style>
.clear {
clear: both;
}
.cell {
min-width: 40px;
min-height: 20px;
margin: 5px;
float: left;
text-align: center;
border: #969696 solid thin;
padding: 5px;
}
</style>
</head>
<body>
<div id="font" class="clear"></div>
<script src="../d3.js"></script>
<script>
const data = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
const sizeScale = d3.scaleLinear()
.domain([0, 10])
.range([
'italic bold 12px/30px Georgia, serif',
'italic bold 120px/180px Georgia, serif'
])
function render(data, scale, selector) {
d3.select(selector).selectAll('div.cell')
.data(data)
.enter()
.append('div')
.classed('cell', true)
.style('display', 'inline-block')
.append('span')
.style('font', function (d) {
return scale(d)
})
.text(function (d, i) {
return i
})
}
render(data, sizeScale, '#font')
</script>
</body>
</html>
效果如下:
我们将值域设置为['italic bold 12px/30px Georgia, serif', 'italic bold 120px/180px Georgia, serif'],但是 尺度却帮我们映射到了各个字体大小的范围,这是因为 d3 的字符串插值器会去找出文本中内嵌的数字,然后针对这些数字进行插值。
颜色插值
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<style>
.clear {
clear: both;
}
.control-group {
padding-top: 10px;
margin: 10px;
}
.cell {
min-width: 40px;
min-height: 20px;
margin: 5px;
float: left;
text-align: center;
border: #969696 solid thin;
padding: 5px;
}
</style>
</head>
<body>
<div id="color" class="clear">
<span>Linear Color Interpolation<br></span>
</div>
<div id="color-diverge" class="clear">
<span>Poly-Linear Color Interpolation<br></span>
</div>
<div class="control-group clear">
<button onclick="render(data, divergingScale(5), '#color-diverge')">Pivot at 5</button>
<button onclick="render(data, divergingScale(10), '#color-diverge')">Pivot at 10</button>
<button onclick="render(data, divergingScale(15), '#color-diverge')">Pivot at 15</button>
<button onclick="render(data, divergingScale(20), '#color-diverge')">Pivot at 20</button>
</div>
<script src="../d3.js"></script>
<script>
const max = 21, data = []
for (let i = 0; i < max; i++) {
data.push(i)
}
const colorScale = d3.scaleLinear()
.domain([0, max])
.range(['white', '#4169e1'])
function divergingScale(pivot) {
return d3.scaleLinear()
.domain([0, pivot, max])
.range(['white', '#4169e1', 'white'])
}
function render(data, scale, selector) {
const cells = d3.select(selector).selectAll('div.cell')
.data(data)
cells.enter()
.append('div')
.classed('cell', true)
.merge(cells)
.style('display', 'inline-block')
.style('background-color', function (d) {
return scale(d)
})
.text(function (d, i) {
return i
})
}
render(data, colorScale, '#color')
render(data, divergingScale(5), '#color-diverge')
</script>
</body>
</html>
效果如下:
内置的颜色插值器同样帮我们完成了从 white 到 #4169e1 的连续插值,另外我们还使用了分段线性尺度,相当于将两个线性尺度结合在一起。
复合对象插值器
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<style>
.clear {
clear: both;
}
.v-bar {
min-width: 30px;
background-color: #4682b4;
margin-right: 2px;
font-size: 10px;
color: #f0f8ff;
text-align: center;
display: inline-block;
}
</style>
</head>
<body>
<div id="compound" class="clear">
<p>Compound Interpolation </p>
</div>
<script src="../d3.js"></script>
<script>
const max = 21, data = []
for (let i = 0; i < max; i++) {
data.push(i)
}
const compoundScale = d3.scalePow()
.exponent(2)
.domain([0, max])
.range([
{color: '#add8e6', height: '15px'},
{color: '#4169e1', height: '150px'}
])
function render(data, scale, selector) {
d3.select(selector).selectAll('div.v-bar')
.data(data)
.enter()
.append('div')
.classed('v-bar', true)
.style('height', function (d) {
return scale(d).height
})
.style('background-color', function (d) {
return scale(d).color
})
.text(function (d, i) {
return i
})
}
render(data, compoundScale, '#compound')
</script>
</body>
</html>
效果如下:
同样的,d3 内置的对象插值器会对对象进行递归插值处理,另外当值域的起始对象和结束对象的属性不一致时,d3 会把不一致的属性当成不变的量。
下面的尺度函数会把所有值的 height 属性都认为 15px :
d3.scaleLinear()
.domain([0, 10])
.range([
{color: '#add8e6', height: '15px'},
{color: '#4169e1'}
])