学习D3.js(二十六)排名柱状图

本文正在参加「金石计划 . 瓜分6万现金大奖」

引入D3模块

  • 引入整个D3模块。
<!-- D3模块 -->
<script src="https://d3js.org/d3.v7.min.js"></script>

数据

  • 关于排名的数据,量都比较大。这里使用.csv文件保存。开发中一般通过接口获取。

image.png

  • 本地是不支持直接获取外部数据,我们需要启动服务。
  • 安装node.js,进入.html文件所在目录,执行npx http-server。服务启动成功如下图:

image.png

  • 这样就可以使用d3.csv()方法获取文件中的数据。

添加画布

const margin = { top: 80, right: 65, bottom: 5, left: 20 }
const width = 1200
const height = 700

const svg = d3.select('.d3Chart').append('svg').attr('width', width).attr('height', height)
const chart = svg.append('g').attr('transform', `translate(${margin.top}, ${margin.left})`)
  • 创建好基础信息,在.d3Chart这个DOM下创建SVG画布和初始绘制信息。

比例尺和配置信息

// 展示条数
const top_n = 20
// 间距
const barPadding = (height - margin.top - margin.bottom) / (top_n * 5)
// 排名开始年份
let year = 2000
// 执行间隔
const tickDuration = 100

// 比例尺函数
const xScale = d3
  .scaleLinear()
  .range([margin.left, width - margin.right])
  .nice()
const yScale = d3
  .scaleLinear()
  .domain([top_n, 0])
  .range([height - margin.bottom, margin.top])
// 排行只需要 绘制 X轴
// 绘制坐标轴函数
const xAxis = d3
  .axisTop(xScale)
  .ticks(width > 500 ? 5 : 2)
  .tickSize(-(height - margin.top - margin.bottom))
  • 创建配置信息变量,控制排名柱状图的绘制。
  • 创建好对应的比例尺,在绘制的时候就不需要关心坐标问题。传入数据或位置信息,由比例尺转换为对应坐标。
  • 这里坐标轴函数使用.ticks()画布宽大于500绘制5个刻度,小于绘制2个刻度。使用.tickSize()绘制刻度标记的长度。

绘制排名柱状图

获取原始数据

// d3.csv() 获取逗号分隔值(CSV)文件
d3.csv('./file/brand_values.csv').then((data) => {
// 数据格式转换
data.forEach((d) => {
d.lastValue = Number(d.lastValue)
d.value = isNaN(d.value) ? 0 : Number(d.value)
d.year = Number(d.year)
d.rank = Number(d.rank)
d.color = d3.hsl(Math.random() * 360, 1, 0.75, 0.8)
})

// 根据开始年份 获取当年数据 并进行排序 截取为设置的条数 并对数据设置排名
let yearSlice = data
.filter((d) => d.year == year && !isNaN(d.value))
.sort((a, b) => b.value - a.value)
.slice(0, top_n)
yearSlice.forEach((d, i) => (d.rank = i))

})
  1. d3.csv() 获取.csv格式文件的数据。
  • 通过文件获取数据,修改为我们需要的格式。
  • 在原始数据中过滤,排序,截取后得到我们要绘制的数据。

初始化柱状图

/**
 * 初始化柱状图
 */
function render_init(yearSlice) {
  // 绘制标题
  chart
    .append('text')
    .attr('class', 'title')
    .attr('y', 30)
    .attr('x', width / 2)
    .attr('style', 'font-size: 2em;font-weight: 500;')
    .text('排名柱状图')

  xScale.domain([0, d3.max(yearSlice, (d) => d.value) + 10000])
  svg.append('g').attr('class', 'xAxis').call(xAxis)
     .attr('transform', `translate(${margin.left},${margin.top})`)

  // 柱状绘制
  svg
    .selectAll('rect.bar')
    .data(yearSlice, (d) => d.name)
    .enter()
    .append('rect')
    .attr('class', 'bar')
    .attr('x', xScale(0) + margin.left + 1)
    .attr('width', (d) => xScale(d.value) - xScale(0))
    .attr('y', (d) => yScale(d.rank) + 5)
    .attr('height', yScale(1) - yScale(0) - barPadding)
    .attr('fill', (d) => d.color)

  // 品牌名绘制
  svg
    .selectAll('text.label')
    .data(yearSlice, (d) => name)
    .enter()
    .append('text')
    .attr('class', 'label')
    .attr('style', 'font-weight: 500;')
    .attr('x', (d) => xScale(d.value))
    .attr('y', (d) => yScale(d.rank) + (yScale(1) - yScale(0)) / 2 + 8)
    .attr('text-anchor', 'end')
    .text((d) => d.name)

  // 数值绘制
  svg
    .selectAll('text.valueLabel')
    .data(yearSlice, (d) => d.name)
    .enter()
    .append('text')
    .attr('class', 'valueLabel')
    .attr('x', (d) => xScale(d.value) + 30)
    .attr('y', (d) => yScale(d.rank) + (yScale(1) - yScale(0)) / 2 + 8)
    .text((d) => d3.format(',.0f')(d.lastValue))

  // 年份绘制
  svg
    .append('text')
    .attr('class', 'yearText')
    .attr('x', width - margin.right)
    .attr('y', height - 25)
    .attr('fill', '#2eb0c5d9')
    .attr('text-anchor', 'end')
    .attr('style', 'font-size:2em; font-weight: 500;font-weight: bold;')
    .html(~~year)
}
  • 在数据处理完后调用。
...
  render_init(yearSlice)
...

image.png

  • 创建初始化函数。
  • 修改比例尺xScale.domain输入域并加上10000,当数据最大值时不会在结束刻度。绘制X坐标轴。
  • 这一步需要注意,柱状、品牌名、数值、年份,在绑定数据时都要添加唯一标识。后续动画是对现有DOM的操作,需要获取到对应的元素。

让柱状图动起来

let ticker = d3.interval((e) => {
    // ~~ 向上取整
    svg.select('.yearText').html(~~year)
    // 保留一位小数
    year = d3.format('.1f')(+year + 0.1)
    // 结束循环
    if (year === '2018.0') {
      ticker.stop()
    }

    // 重新切分数据
    yearSlice = data
      .filter((d) => d.year == year && !isNaN(d.value))
      .sort((a, b) => b.value - a.value)
      .slice(0, top_n)
    yearSlice.forEach((d, i) => {
      d.rank = i
    })

    // 修改比例尺
    xScale.domain([0, d3.max(yearSlice, (d) => d.value) + 10000])
    // x轴重新绘制 添加动画
    svg.select('.xAxis').transition().duration(tickDuration).ease(d3.easeLinear).call(xAxis)

    // 绑定数据 通过标识符判断是否存在
    const bars = svg.selectAll('.bar').data(yearSlice, (d) => d.name)
    // 不存在的 柱状进行创建
    bars
      .enter()
      .append('rect')
      .attr('class', 'bar')
      .attr('x', xScale(0) + margin.left + 1)
      .attr('width', (d) => xScale(d.value) - xScale(0))
      .attr('y', (d) => yScale(top_n + 1) + 5)
      .attr('height', yScale(1) - yScale(0) - barPadding)
      .attr('fill', (d) => d.color)
    // 对现存在的所有 节点 添加动画
    bars
      .transition()
      .duration(tickDuration)
      .ease(d3.easeLinear)
      .attr('width', (d) => xScale(d.value) - xScale(0))
      .attr('y', (d) => yScale(d.rank) + 5)
    // 删除数据中以不存在的节点 添加离开动画
    bars
      .exit()
      .transition()
      .duration(tickDuration)
      .ease(d3.easeLinear)
      .attr('y', (d) => yScale(top_n + 1) + 5)
      .attr('width', 0)
      .remove()
  }, 30)

3.gif

  1. d3.interval(()=>{},30) 循环执行函数。
  2. .stop() 结束循环。
  • 根据新的数据重新设置比例尺和重绘坐标轴。
  • 对新数据进行绑定,这里就需要唯一标识符,来判断哪些数据需要创建新节点,哪些节点需要删除。然后对存在的节点进行位置修改并设置动画。

让文本和数字动起来

  • 和柱状图同样的步骤进行操作。
const labels = svg.selectAll('.label').data(yearSlice, (d) => d.name)
labels
  .enter()
  .append('text')
  .attr('class', 'label')
  .attr('x', (d) => xScale(d.value))
  .attr('y', (d) => yScale(top_n + 1) + 8)
  .attr('text-anchor', 'end')
  .text((d) => d.name)

labels
  .transition()
  .duration(tickDuration)
  .ease(d3.easeLinear)
  .attr('x', (d) => xScale(d.value))
  .attr('y', (d) => yScale(d.rank) + (yScale(1) - yScale(0)) / 2 + 8)

labels
  .exit()
  .transition()
  .duration(tickDuration)
  .ease(d3.easeLinear)
  .attr('y', (d) => yScale(top_n + 1) + 20)
  .remove()

const valueLabels = svg.selectAll('.valueLabel').data(yearSlice, (d) => d.name)
valueLabels
  .enter()
  .append('text')
  .attr('class', 'valueLabel')
  .attr('x', (d) => xScale(d.value) + 30)
  .attr('y', (d) => yScale(top_n) + 20)
  .text((d) => d3.format(',.0f')(d.lastValue))

valueLabels
  .transition()
  .duration(tickDuration)
  .ease(d3.easeLinear)
  .attr('x', (d) => xScale(d.value) + 30)
  .attr('y', (d) => yScale(d.rank) + (yScale(1) - yScale(0)) / 2 + 8)
  .tween('textTween', function (d) {
    // 做出在两个value间跳动的效果
    let i = d3.interpolateRound(d.lastValue, d.value)
    return function (t) {
      this.textContent = d3.format(',')(i(t))
    }
  })

valueLabels
  .exit()
  .transition()
  .duration(tickDuration)
  .ease(d3.easeLinear)
  .attr('x', (d) => xScale(d.value) + 30)
  .attr('y', (d) => yScale(top_n + 1) + 20)
  .remove()
  • 这里需要注意为了实现数字跳动的效果,使用.tween()函数实现数字过度动画。

5.gif

总结

一个排名柱状图的实现就这么简单。这里是先绘制一个静态柱状图,对每个元素添加唯一标识。利用D3的特性,根据唯一标识符来判断节点是否存在,存在的节点进行位置修改并添加过渡动画,不存在的进行新节点创建和添加过渡动画,对无用的节点进行删除并添加离开的过渡动画。这里只是一个简单的示例,想要在项目中使用还需要进行细节优化。

© 版权声明
THE END
喜欢就支持一下吧
点赞8 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容