學習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

  • 就這樣一個遮罩折線圖就完成,是不是很簡單。
  • 代碼地址