d3.js 入门学习记录(四) 尺度scale的使用

534 阅读9分钟

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 &lt;= a*n + b &lt;= 20</span></div>
<div id="pow" class="clear"><span>n^2</span></div>
<div id="pow-capped" class="clear"><span>1 &lt;= a*n^2 + b &lt;= 10</span></div>
<div id="log" class="clear"><span>log(n)</span></div>
<div id="log-capped" class="clear"><span>1 &lt;= a*log(n) + b &lt;= 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>

效果如下:

对于尺度而言,大部分方法的作用其实都是一样的:

  1. domain() 设置定义域
  2. range() 设置值域
  3. 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'}
    ])