Continuous Scales
比例尺是 D3 的重要概念, 用來代替使用像素表示大小.
實(shí)際上上一篇中在隨機(jī)生成數(shù)值的時(shí)候, 考慮到數(shù)值都是小數(shù), 故而進(jìn)行了 × 1000 的操作. 但是如果隨后的到的數(shù)值不再是小數(shù), 而就是一個(gè)比如 1000+ 的整數(shù)呢? 如果不 × 1000 的話很明顯統(tǒng)計(jì)結(jié)果是完全不可靠的; 但如果 1000 × 1000 這個(gè)數(shù)值使用像素來表示已經(jīng)無法在屏幕上看到結(jié)果了. 確實(shí)需要修改代碼, 需要有類似于整體縮放的功能, 也許需要是動(dòng)態(tài)縮放.
比例尺就是用來應(yīng)付這種情況的, 它大致是將上述的結(jié)果做了一個(gè)等比縮放, 已適用于各種大小數(shù)值都可以用可觀的比例形式展示.
Continuous Scales 連續(xù)比例尺
連續(xù)比例尺可以將連續(xù)的, 定量的輸入(domain) 映射到連續(xù)的輸入(range). 如果輸出范圍也是數(shù)值, 則這種映射關(guān)系可以被 inverted(反轉(zhuǎn)).
以下 continuous 表示連續(xù)比例尺函數(shù), 該函數(shù)包含了很多方法.
continuous(domainValue)
首先, continuous 本身是一個(gè)函數(shù), 它接收一個(gè) domainValue, 返回一個(gè)對(duì)應(yīng)的 rangeValue.
假設(shè):
- domain: [10, 110]
- range: [0, 666]
那么:
- continuous(10); // 返回 0
- continuous(110); // 返回 666
continuous.domain([...values])
continuous 是一個(gè)包含有自己的方法的函數(shù), 因此它可以使用各種方法, domain 就是其中之一.
...values
表示 domain 方法接收的數(shù)組支持任意多個(gè)數(shù)值, 不過通常來說, 必須是 2 個(gè)(包含 2 個(gè))以上.
當(dāng) ...values
只有兩個(gè)值時(shí), 他們分別表示最大值和最小值.
domain([min, max]) 方法接收一個(gè)包含兩個(gè)值的數(shù)組
- min: 設(shè)置 domain 最小值, 一般來說是要渲染的列表數(shù)據(jù)集中最小值
- max: 設(shè)置 domain 最大值, 一般來說是要渲染的列表數(shù)據(jù)集中最大值
當(dāng) ...values
有 3 個(gè)值時(shí), 他們將被拆分為兩個(gè)域段, 例如:
let sl = d3.scaleLinear()
.domain([10, 20, 110])
.range([0, 10, 300]);
console.log(sl(19)); // 9
console.log(sl(21)); // 13.222222222222221
它相當(dāng)于設(shè)置了兩個(gè) domain 與 range 的對(duì)應(yīng)關(guān)系, 分別是:
- domain[10, 20] -> range[0, 10]
- domain[20, 10] -> range[10, 300]
可以看到上面的例子當(dāng)中, sl(19) 與 sl(21) 都與 sl(20) 只是差了 1, 然而 rangeValue 卻完全不同.
continuous.domain([...values]) 的返回值仍然是一個(gè) continuous 函數(shù), 所以可以鏈?zhǔn)降恼{(diào)用 continuous 的各種方法.
continuous.range([...values])
一般來說, domain[...values] 與 range[...values] 的 values 的個(gè)數(shù)應(yīng)該是對(duì)應(yīng). 官方喜歡使用分段顏色比例尺(diverging color scale)
做案例來說明. 他有點(diǎn)像是兩個(gè)顏色之間過度的感覺.
拿一個(gè)例子來說明一下, 以下為 body 中的代碼:
<ul></ul>
<script src="https://d3js.org/d3.v5.min.js"></script>
<script>
let sl = d3.scaleLinear()
.domain([-255, 0, 255])
.range(['#f00', '#0f0', '#00f']);
let data = [-200, 55, 125];
d3.select('ul').selectAll('li')
.data(data)
.enter()
.append('li')
.text(x => sl(x))
.style('background-color', v => sl(v))
我們定義了 domain[-255, 0, 255] 三個(gè)域值, 為了分別對(duì)應(yīng) RGB 三個(gè)色值. range(['#f00', '#0f0', '#00f']) 定義域值對(duì)應(yīng)的區(qū)間段.
通過修改 data 的數(shù)值, 可以在瀏覽頁(yè)面是看到不同的顏色.
竟然意料之外的感覺會(huì)非常實(shí)用, 根據(jù)這種思想可以實(shí)現(xiàn)用戶自定義頁(yè)面顏色的功能.
continuous() + continuous.domain([...values]) + continuous.range([...values])
3 個(gè)概念都熟悉以后, 這里再來具體說一下 domain 與 range 的相互關(guān)系, 見下方代碼:
let sl = d3.scaleLinear()
.domain([10, 110])
.range([0, 666]);
// domain -> range
console.log('11 ->', sl(11));
console.log('109 ->', sl(109));
// range -> domain
console.log('6.66 ->', sl.invert(6.66));
console.log('659.34 ->', sl.invert(659.34));
// overflow
console.log('9 ->', sl(9));
console.log('699 ->', sl.invert(699));
結(jié)果輸出如下:
11 -> 6.66
109 -> 659.34
6.66 -> 11
659.34 -> 109.00000000000001
9 -> -6.66
699 -> 114.95495495495494
這里給出的 domain 是 [10, 110], range 是 [0, 666]
可以在結(jié)果當(dāng)中看到 domain 11 成功映射到了 range 6.66; 反轉(zhuǎn)后 range 6.66 也成功映射到了 domain 11;
continuous.invert() 即反轉(zhuǎn)函數(shù)
不過當(dāng)輸入 domain 9 時(shí), 也成功的的到了對(duì)應(yīng)的 range -6.66(負(fù)數(shù)<10<整數(shù)), 顯然這不是事先約定好的 domain 與 range 的對(duì)應(yīng)范圍所包括的內(nèi)容.
continuous 會(huì)在沒有啟動(dòng) clamp 時(shí), 將給定不屬于 domain 的值推算出對(duì)應(yīng)的 range.
continuous.invert(rangeValue)[1]
invert() 用來方向取域值, 意思很好懂. 不過他也有一些短板.
- 只適用于 range 為數(shù)值類型
- 非數(shù)值 range 將的到一個(gè) NaN
- 反向取值由于精確度, 所以并不完全對(duì)等
continuous.clamp(boolean)[2]
clamp() 的官方翻譯叫做鉗位.
使用 scale.clamp(true)
可以開啟鉗位功能, 開啟后即使 domain/range 越界, range/domain 仍然會(huì)保持在規(guī)定范圍內(nèi).
關(guān)于鉗位這個(gè)詞我個(gè)人是真的讀不懂, 我認(rèn)為解釋為封頂就足夠理解了, 有點(diǎn)類似于股票的走勢(shì), 無論漲跌, 每天都有最大限制, 到頂后, 再漲也沒有意義了.
continuous.interpolate(interpolate)
interpolate() 函數(shù)接收一個(gè)插值器, 如果沒有顯示的指定插值器, 則 continuous 使用默認(rèn)的插值器 d3.interpolate.
continuous 默認(rèn)啟用 .interpolate(d3.interpolate), 插值器.
let sl = d3.scaleLinear()
.domain([10, 110])
.range([0, 666])
.interpolate(d3.interpolate);
關(guān)于插值器的作用的話, 目前認(rèn)為更加的偏向于一種映射規(guī)則. 比如 0 是否應(yīng)該映射為 1, 或者 1.25, 或者 #f0f.
d3.interpolate 默認(rèn)插值器實(shí)際上會(huì)根據(jù)判斷 rangeValue 來具體的適配不同的更加明確的插值器. 比如 d3.interpolateNumber 插值器, d3.interpolateRound 插值器等等.
continuous.rangeRound([...values])
rangeRound 相當(dāng)于啟用 d3.interpolateRound 插值器(支持四舍五入映射規(guī)則的插值器).
continuous
.range(range)
.interpolate(d3.interpolateRound);
continuous.ticks([count])
ticks() 函數(shù)會(huì)將 domain 的最大值與最小值等差劃分為 N 份, 然后作為一個(gè)數(shù)組返回.
ticks() 的結(jié)果看起來與尺子的刻度線非常的相似, 也因此常被用來顯示刻度線或者刻度標(biāo)記.
用法:
let sl = d3.scaleLinear()
.domain([0, 10, 653])
.range([0, 60, 666]);
console.log(sl.ticks()); // [ 0, 50, 100, 150, 200, 250, 300, 350, 400, 450, 500, 550, 600, 650]
ticks(count) 是可以接收一個(gè) count 參數(shù)的, count 指定期望返回多少個(gè)"刻度". 然而, 實(shí)際情況是并不一定返回期望的個(gè)數(shù)的值. 為了保證等差的特性, 人類友好性, 返回的刻度數(shù)量確實(shí)是受到限制的, 這個(gè)可以理解.
continuous.tickFormat([count, [specifier]])
tickFormat 幾乎是一個(gè)刻度 formatter, 或者說刻度 mapper. 具體用法如下:
let sl = d3.scaleLinear()
.domain([0, 300])
.range([0, 66]);
let ticks = sl.ticks(7),
tickFormat = sl.tickFormat(7, "+%")
let res = ticks.map(tickFormat)
console.log(ticks); // [0, 50, 100, 150, 200, 250, 300]
console.log(res); // ["+0%", "+5000%", "+10000%", "+15000%", "+20000%", "+25000%", "+30000%"]
ticks.map(formatter) 實(shí)際上 ticks 就是一個(gè)數(shù)組, map 方法就是數(shù)組的 map 方法, formatter 函數(shù)傳入 map 方法后被作為映射器使用.
continuous.nice([count])
nice() 方法用來優(yōu)化 domain 的 max & min 值, 通常會(huì)修改為其最接近的整數(shù)值, 不會(huì)修改中間值.
let sl = d3.scaleLinear()
.domain([0.1, 2.123245, 3.748392])
.range([0, 66]);
console.log(sl.domain()); // [0.1, 2.123245, 3.748392]
sl.nice();
console.log(sl.domain()); // [0, 2.123245, 4]
continuous.copy()
返回當(dāng)前比例尺的深復(fù)制.
小結(jié)
看了這么多內(nèi)容不能忘記最初的目的.
比例尺的作用就是用合適的比例將使用像素作為尺寸展示不友好的數(shù)據(jù)進(jìn)行縮放.
這里有一個(gè)使用了比例尺的 demo:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<style>
svg,
.container {
box-sizing: border-box;
padding: 8px;
border-radius: 3px;
border: 1px solid #ddd;
}
svg {
background-color: #f1f1f1;
}
.container {
background-color: #fff;
}
</style>
<div class="container"></div>
<script src="https://d3js.org/d3.v5.min.js"></script>
<script>
let width = '100%',
height = 256,
barHeight = 16;
let dataset = new Array(15);
for (let i = 0; i < dataset.length; i++) {
dataset[i] = Math.random() * 100;
}
let domainMax = Math.max(...dataset),
domainMin = Math.min(...dataset),
rangeMax = 500,
rangeMin = 10;
console.log(domainMin, domainMax);
console.log(rangeMin, rangeMax);
let sl = d3.scaleLinear()
.domain([domainMin, domainMax])
.range([rangeMin, rangeMax]);
let svg = d3.select('.container').append('svg');
svg.attr('width', width)
.attr('height', height);
let rectes = svg.selectAll('rect')
.data(dataset)
.enter()
.append('rect');
rectes
.attr('y', (item, idx) => idx * barHeight)
.attr('width', item => sl(item))
.attr('height', barHeight - 4)
.attr('ry', '2px')
.attr('fill-opacity', 0.85)
.attr('fill', '#369')
let title = svg.selectAll('text')
.data(dataset)
.enter()
.append('text');
title.attr('x', item => sl(item) + 4)
.attr('y', (_, idx) => (idx + 1) * barHeight - 6)
.attr('font-size', '0.75em')
.text(item => item)
</script>
</body>
</html>
可以多次刷新查看變化.