学习D3.js(六)遮罩折线图

1,507 阅读3分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第22天,点击查看活动详情

开始绘制

引入D3模块

  <!-- 比例尺模块 和 依赖 -->
  <script src="https://cdn.jsdelivr.net/npm/d3-array@3"></script>
  <script src="https://cdn.jsdelivr.net/npm/d3-color@3"></script>
  <script src="https://cdn.jsdelivr.net/npm/d3-format@3"></script>
  <script src="https://cdn.jsdelivr.net/npm/d3-interpolate@3"></script>
  <script src="https://cdn.jsdelivr.net/npm/d3-time@3"></script>
  <script src="https://cdn.jsdelivr.net/npm/d3-time-format@4"></script>
  <script src="https://cdn.jsdelivr.net/npm/d3-scale@4"></script>

  <!-- 坐标轴 -->
  <script src="https://cdn.jsdelivr.net/npm/d3-axis@3"></script>

  <!-- 形状 -->
  <script src="https://cdn.jsdelivr.net/npm/d3-path@3"></script>
  <script src="https://cdn.jsdelivr.net/npm/d3-shape@3"></script>

  <!-- 动画 -->
  <script src="https://cdn.jsdelivr.net/npm/d3-dispatch@3"></script>
  <script src="https://cdn.jsdelivr.net/npm/d3-ease@3"></script>
  <script src="https://cdn.jsdelivr.net/npm/d3-timer@3"></script>
  <script src="https://cdn.jsdelivr.net/npm/d3-transition@3"></script>

数据

  • 数据格式随意,后面使用。
    var bColor = ['#4385F4', '#34A853', '#FBBC05', '#E94335', '#01ACC2', '#AAACC2']
    var dataArr = [
      {
        label: '1月',
        value: 10.5,
        value2: 70.5
      },
      {
        label: '2月',
        value: 70.5,
        value2: 10.5
      },
      {
        label: '3月',
        value: 60.5,
        value2: 10.5
      },
      {
        label: '4月',
        value: 10.5,
        value2: 30.5
      },
      {
        label: '5月',
        value: 20.5,
        value2: 10.5
      },
      {
        label: '6月',
        value: 30.5,
        value2: 3.5
      }
    ]

添加画布

  • 初始化画布。
    var width = 450
    var height = 480
    var margin = 20

    var svg = d3
      .select('.d3Chart')
      .append('svg')
      .attr('width', width)
      .attr('height', height)
      .style('background-color', '#1a3055')

    // 图
    var chart = svg.append('g').attr('transform', `translate(${margin * 2}, ${margin})`)

创建比例尺

  • 根据需求创建比例迟。
    // 分段比例尺
    // 把X轴长度 分成多段
    var xScale = d3
      .scaleBand()
      .range([0, 400])
      .domain(dataArr.map((s) => s.label))

    // 线性比例尺
    // 把Y轴长度 转换为100
    var yScale = d3.scaleLinear().range([400, 0]).domain([0, 100])

绘制坐标轴

  • 根据定义好的比例尺,使用axis模块自动绘制坐标轴。
  • 绘制好坐标轴后,在坐标轴组中绘制文本标签。
// 坐标轴
const xAxis = d3.axisBottom(xScale)
chart.append('g').attr('class', 'xAxis').attr('transform', `translate(0, ${400})`).call(xAxis)
const yAxis = d3
  .axisLeft()
  .scale(yScale)
  // .tickSize(-400)
  .tickFormat((d) => {
    return d + '%'
  })
// 标签
d3.select('.xAxis')
  .append('text')
  .attr('x', 400 / 2 - 12)
  .attr('y', 0)
  .attr('dy', 45)
  .style('font-size', '24px')
  .text('时间')

chart.append('g').attr('transform', 'translate(0, 0)').call(yAxis)
d3.selectAll('.d3Chart text').style('fill', '#fff')
d3.selectAll('.d3Chart line').style('stroke', '#fff')
d3.selectAll('.d3Chart path').style('stroke', '#fff')

image.png

绘制折线

  • 转换数据格式。
    let items = []
    // 组装数据 便于绘制
    dataArr.forEach((row) => {
      let index = 0
      Object.keys(row).forEach((key) => {
        // 非数据 不绘制统计图
        if (key !== 'label') {
          if (items[index]) {
            items[index].push([row.label, row[key], key, index])
          } else {
            items[index] = [[row.label, row[key], key, index]]
          }
          index++
        }
      })
    })

image.png

  • 这里的数据格式,取决于后面你如何使用D3。
  • 创建线形状计算方法。
// 计算点位置
let line = d3
  .line()
  .x(function (d) {
    return d[0]
  })
  .y(function (d) {
    return d[1]
  })
  • 绑定折线组数据,创建折线组(g)元素,绑定折线详细数据在组上。
const groups = chart.selectAll().data(items)
const lines = groups
  .enter()
  .append('g')
  .selectAll()
  .data((d) => [d])
  • 在组上创建path元素,绘制折线。
lines
  .enter()
  .append('path')
  .attr('class', 'lines')
  .attr('d', function (d) {
    const row = d.map((item) => {
      const itemS = []
      itemS.push(xScale(item[0]))
      itemS.push(yScale(item[1]))
      return [...itemS]
    })
    return line(row)
  })
  .attr('stroke', (d, i) => bColor[d[0][3]])
  .attr('fill', 'none')
  .attr('transform', `translate(${xScale.bandwidth() / 2}, 0)`)

image.png

  • 使用同样的方式,绘制折线点。
// 点绘制
const circles = groups
  .enter()
  .append('g')
  .attr('class', 'Gcircle')
  .selectAll()
  .data((d) => d)

circles
  .enter()
  .append('circle')
  .attr('cx', function (d) {
    return xScale(d[0])
  })
  .attr('cy', function (d) {
    return yScale(d[1])
  })
  .attr('r', 4)
  .attr('transform', `translate(${xScale.bandwidth() / 2}, 0)`)
  .attr('fill', '#fff')
  .attr('stroke', 'rgba(56, 8, 228, .5)')

image.png

绘制遮罩层

  • 上面一个基本折线图绘制成功,下面来绘制遮罩层。
  • 创建形状生成器。
const generateArea = d3
  .area()
  .x((d) => d[0])
  .y0((d) => d[1])
  .y1((d) => 400)
  1. d3.area() 区域图生成器。绘制一片区域。
  2. x()、y0()、y1() 设置区域点上的坐标。
  • 使用区域图生成器,自动绘制遮罩层。
lines
  .enter()
  .append('path')
  .attr('class', 'area')
  .attr('d', function (d) {
    const row = d.map((item) => {
      const itemS = []
      itemS.push(xScale(item[0]))
      itemS.push(yScale(item[1]))
      return [...itemS]
    })
    return generateArea(row)
  })
  .attr('fill', (d, i) => bColor[d[0][3]])
  .attr('fill-opacity', '0.5')
  .attr('transform', `translate(${xScale.bandwidth() / 2}, 0)`)

image.png

添加动画

  • 添加了遮罩层,就不能使用虚线的方式来实现动画。这里使用.attrTeeen()来实现过度动画。
  • 创建差值函数。
// 点插值 函数
function getAreaInterpolate(pointX, pointY) {
  const domain = d3.range(0, 1, 1 / (pointX.length - 1))
  // 补上结尾 1
  domain.push(1)

  // 线性比例尺 根据 值域 和 定义域 获取不同区间的值
  const interpolateX = d3.scaleLinear().domain(domain).range(pointX)
  const interpolateY = d3.scaleLinear().domain(domain).range(pointY)
  return {
    x: interpolateX,
    y: interpolateY
  }
}
  • 插值函数的作用。因为过度动画的值是0 ~ 1的范围,需要把一条折线上的每个坐标于之对应,这时候就需要插值函数。输入0 ~ 1范围的值,获取该值对应的坐标。
  • 获取折线元素,创建折线动画。
    d3.selectAll('path.lines')
      .transition()
      .duration(2000)
      .attrTween('d', (_d) => {
        const pointX = _d.map((d) => xScale(d[0]))
        const pointY = _d.map((d) => yScale(d[1]))

        const interpolate = getAreaInterpolate(pointX, pointY)
        const ponits = []

        return function (t) {
          ponits.push([interpolate.x(t), interpolate.y(t)])
          return line(ponits)
        }
      })
  1. transition.attrTeeen(name,tween) 动画函数。将属性name使用插值函数tween()进行过渡。
  2. t 就是个时间值,整个动画0~1的范围。
  • 同样方式设置,遮罩层的动画。
d3.selectAll('path.area')
  .transition()
  .duration(2000)
  .attrTween('d', (_d) => {
    const pointX = _d.map((d) => xScale(d[0]))
    const pointY = _d.map((d) => yScale(d[1]))

    const interpolate = getAreaInterpolate(pointX, pointY)
    const ponits = []

    return function (t) {
      ponits.push([interpolate.x(t), interpolate.y(t)])
      return generateArea(ponits)
    }
  })
  • 需要注意,设置了动画之后,最开始的设置的折线和遮罩层就可以注释了。
...
  // .attr('d', function (d) {
  //   const row = d.map((item) => {
  //     const itemS = []
  //     itemS.push(xScale(item[0]))
  //     itemS.push(yScale(item[1]))
  //     return [...itemS]
  //   })
  //   return line(row)
  // })
...

4.gif

  • 折线点分组设置动画,所以标识是设置在组上的。
  • 计算一下动画时间,使点的加载符合折线的加载。
    d3.selectAll('.Gcircle')
      .selectAll('circle')
      .attr('r', 0)
      .transition()
      .duration(300)
      .delay(function (d, i) {
        return (i * 2000) / dataArr.length
      })
      .attr('r', 4)
      .style('stroke-width', 3)

1.gif

  • 就这样一个遮罩折线图就完成,是不是很简单。
  • 代码地址