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

语言: CN / TW / HK

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

开始绘制

引入D3模块

```html

```

数据

  • 数据格式随意,后面使用。 js 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 } ]

添加画布

  • 初始化画布。 ```js 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})) ```

创建比例尺

  • 根据需求创建比例迟。 ```js // 分段比例尺 // 把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模块自动绘制坐标轴。
  • 绘制好坐标轴后,在坐标轴组中绘制文本标签。 ``js // 坐标轴 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

绘制折线

  • 转换数据格式。 js 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。
  • 创建线形状计算方法。 js // 计算点位置 let line = d3 .line() .x(function (d) { return d[0] }) .y(function (d) { return d[1] })
  • 绑定折线组数据,创建折线组(g)元素,绑定折线详细数据在组上。 js const groups = chart.selectAll().data(items) const lines = groups .enter() .append('g') .selectAll() .data((d) => [d])
  • 在组上创建path元素,绘制折线。 js 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

  • 使用同样的方式,绘制折线点。 ```js // 点绘制 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

绘制遮罩层

  • 上面一个基本折线图绘制成功,下面来绘制遮罩层。
  • 创建形状生成器。 js const generateArea = d3 .area() .x((d) => d[0]) .y0((d) => d[1]) .y1((d) => 400)
  • d3.area() 区域图生成器。绘制一片区域。
  • x()、y0()、y1() 设置区域点上的坐标。

  • 使用区域图生成器,自动绘制遮罩层。 js 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()来实现过度动画。
  • 创建差值函数。 ```js // 点插值 函数 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范围的值,获取该值对应的坐标。 - 获取折线元素,创建折线动画。js 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的范围。 - 同样方式设置,遮罩层的动画。js 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)
}

}) - **需要注意**,设置了动画之后,最开始的设置的折线和遮罩层就可以注释了。js ... // .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

  • 折线点分组设置动画,所以标识是设置在组上的。
  • 计算一下动画时间,使点的加载符合折线的加载。 js 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

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