D3 - 連續(xù)比例尺

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:

61.png
<!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>

可以多次刷新查看變化.


  1. continuous.invert() ?

  2. continuous.clamp() ?

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末氨肌,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子讶泰,更是在濱河造成了極大的恐慌牙甫,老刑警劉巖碗降,帶你破解...
    沈念sama閱讀 219,039評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件腿短,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡淤堵,警方通過查閱死者的電腦和手機(jī)耀鸦,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,426評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門柬批,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人袖订,你說我怎么就攤上這事氮帐。” “怎么了洛姑?”我有些...
    開封第一講書人閱讀 165,417評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵上沐,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我楞艾,道長(zhǎng)参咙,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,868評(píng)論 1 295
  • 正文 為了忘掉前任硫眯,我火速辦了婚禮蕴侧,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘舟铜。我一直安慰自己戈盈,他們只是感情好奠衔,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,892評(píng)論 6 392
  • 文/花漫 我一把揭開白布谆刨。 她就那樣靜靜地躺著,像睡著了一般归斤。 火紅的嫁衣襯著肌膚如雪痊夭。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,692評(píng)論 1 305
  • 那天脏里,我揣著相機(jī)與錄音她我,去河邊找鬼。 笑死迫横,一個(gè)胖子當(dāng)著我的面吹牛番舆,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播矾踱,決...
    沈念sama閱讀 40,416評(píng)論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼恨狈,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了呛讲?” 一聲冷哼從身側(cè)響起禾怠,我...
    開封第一講書人閱讀 39,326評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤返奉,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后吗氏,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體芽偏,經(jīng)...
    沈念sama閱讀 45,782評(píng)論 1 316
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,957評(píng)論 3 337
  • 正文 我和宋清朗相戀三年弦讽,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了污尉。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,102評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡往产,死狀恐怖十厢,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情捂齐,我是刑警寧澤蛮放,帶...
    沈念sama閱讀 35,790評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站奠宜,受9級(jí)特大地震影響包颁,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜压真,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,442評(píng)論 3 331
  • 文/蒙蒙 一娩嚼、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧滴肿,春花似錦岳悟、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,996評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至堆缘,卻和暖如春滔灶,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背吼肥。 一陣腳步聲響...
    開封第一講書人閱讀 33,113評(píng)論 1 272
  • 我被黑心中介騙來泰國(guó)打工录平, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人缀皱。 一個(gè)月前我還...
    沈念sama閱讀 48,332評(píng)論 3 373
  • 正文 我出身青樓斗这,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親啤斗。 傳聞我的和親對(duì)象是個(gè)殘疾皇子表箭,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,044評(píng)論 2 355