学习D3.js(二十三)平移柱状图

267 阅读4分钟

我正在参加「掘金·启航计划」

引入D3模块

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

数据

  • 自定义数据和格式。
// 创建数据
let data = []
for (var i = 0; i < 300; i++) {
  var datum = {}
  datum.date = i
  datum.price = Math.floor(Math.random() * 600)
  data.push(datum)
}

添加画布

// 全局画布变量
const margin = { top: 10, right: 10, bottom: 100, left: 40 }
const margin2 = { top: 430, right: 10, bottom: 20, left: 40 }
const width = 750 - margin.left - margin.right
const height = 500 - margin.top - margin.bottom
const height2 = 500 - margin2.top - margin2.bottom

const svg = d3
  .select('.d3Chart')
  .append('svg')
  .attr('width', width + margin.left + margin.right)
  .attr('height', height + margin.top + margin.bottom)

比例尺和配置信息

    // 比例尺
    const x = d3.scaleBand().range([0, width]).padding(0.1)
    const x2 = d3.scaleBand().range([0, width])
    const y = d3.scaleLinear().range([height, 0])
    const y2 = d3.scaleLinear().range([height2, 0])
  • x轴,使用序数比例尺,计算每条数据的刻度。
  • y轴,使用线性比例尺,计算值对应的高度。
    // 坐标轴
    const xAxis = d3.axisBottom(x)
    const xAxis2 = d3.axisBottom(x2).tickValues([])
    const yAxis = d3.axisLeft().scale(y)
  • 创建坐标轴绘制函数。
x.domain(
  data.map(function (d) {
    return d.date
  })
)
y.domain([
  0,
  d3.max(data, function (d) {
    return d.price
  })
])
x2.domain(x.domain())
y2.domain(y.domain())
  • 为比例尺添加输入域。每一次平移操作输入域都会重新计算。
// 沿x轴创建画笔
const brush = d3
  .brushX()
  .extent([
    [0, 0],
    [width, height2]
  ]) // 设置 画笔范围
  .on('brush', brushed)
  function brushed(brush) {}
  1. d3.brushX() 沿x轴创建新的二维画笔。
  2. .extent() 设置画笔可刷区域。
  3. .on() 画笔操作后的监听。brushed() 画笔操作的回调函数,重新绘制图表操作。
  • 创建二维画笔。

绘制图表和画笔操作

绘制坐标轴

const chartSvg = svg
  .append('g')
  .attr('class', 'focus')
  .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')')
  • 创建图表绘制组。
chartSvg
  .append('g')
  .attr('class', 'x axis')
  .attr('transform', 'translate(0,' + height + ')')
  .call(xAxis)
chartSvg.append('g').attr('class', 'y axis').call(yAxis)

image.png

  • 绘制坐标轴。x轴现在是未处理的比例尺。

绘制柱状

enter(data)
/**
 * 绑定数据 绘制柱状
 */
function enter(data) {
  x.domain(
    data.map(function (d) {
      return d.date
    })
  )
  y.domain([
    0,
    d3.max(data, function (d) {
      return d.price
    })
  ])

  const bars = chartSvg.selectAll('.bar').data(data)
  bars
    .enter()
    .append('rect')
    .classed('bar', true)
    .attr('fill', 'steelblue')
    .attr('height', function (d, i) {
      return height - y(d.price)
    })
    .attr('width', function (d, i) {
      return x.bandwidth()
    })
    .attr('x', function (d) {
      return x(d.date)
    })
    .attr('y', function (d) {
      return y(d.price)
    })
}

image.png

  • 每次交互后都需要重新绘制柱状,创建公用函数。
  • 更新比例尺数据,以最新数据来绘制柱状。

更新坐标轴

updateScale(data, { selection: [0, 100] })
/**
 * 根据画笔数据 修改x轴显示的刻度 并根据最新的比例尺 重新绘制坐标轴
 * */
function updateScale(data, brush) {
  // 指数比例尺
  const tickScale = d3
    .scalePow()
    .range([data.length / 10, 0])
    .domain([data.length, 0])
    .exponent(0.2) // 指数为0.2

  // 画笔区域宽度
  const brushValue = brush.selection[1] - brush.selection[0]
  if (brushValue === 0) {
    brushValue = width
  }
  // 得到数据间隔数
  const tickValueMultiplier = Math.ceil(Math.abs(tickScale(brushValue)))

  // 获取间隔刻度的数据名
  const filteredTickValues = data
    .filter(function (d, i) {
      return i % tickValueMultiplier === 0
    })
    .map(function (d) {
      return d.date
    })

  chartSvg.select('.x.axis').call(xAxis.tickValues(filteredTickValues))
  chartSvg.select('.y.axis').call(yAxis)
}

image.png

  1. d3.scalePow() 创建指数比例尺。
  2. .exponent(0.2) 设置指数,输入值进行求平方根。
  • 每次交互后都需要重新绘制坐标轴,创建公用函数。
  • 先计算出一个合理的数据间隔,根据数据间隔获取对应的刻度值,重新绘制坐标轴。

绘制画笔操作

// 刷子操作区
const context = svg
  .append('g')
  .attr('class', 'context')
  .attr('transform', 'translate(' + margin2.left + ',' + margin2.top + ')')
  • 创建刷子操作区的绘制组。
const subBars = context.selectAll('.subBar').data(data)
subBars
  .enter()
  .append('rect')
  .classed('subBar', true)
  .attr('fill', 'gray')
  .attr('height', function (d, i) {
    return height2 - y2(d.price)
  })
  .attr('width', function (d, i) {
    return x.bandwidth()
  })
  .attr('x', function (d) {
    return x2(d.date)
  })
  .attr('y', function (d) {
    return y2(d.price)
  })
context
  .append('g')
  .attr('class', 'x axis')
  .attr('transform', 'translate(0,' + height2 + ')')
  .call(xAxis2)

// 添加刷子
context.append('g').attr('class', 'x brush').call(brush)
    .selectAll('rect').attr('y', 0).attr('height', height2)

1.gif

  1. d3.call() 对整个集合进行操作。
  • 根据数据,创建小一些的图表。用于区域选择,修改图表中展示的柱状数据。
  • 添加刷子以图表的范围进行选择。
function update(data) {
  x.domain(
    data.map(function (d) {
      return d.date
    })
  )
  y.domain([
    0,
    d3.max(data, function (d) {
      return d.price
    })
  ])

  var bars = chartSvg.selectAll('.bar').data(data)
  bars
    .attr('height', function (d, i) {
      return height - y(d.price)
    })
    .attr('width', function (d, i) {
      return x.bandwidth()
    })
    .attr('x', function (d) {
      return x(d.date)
    })
    .attr('y', function (d) {
      return y(d.price)
    })
}

function exit(data) {
  var bars = chartSvg.selectAll('.bar').data(data)
  bars.exit().remove()
}
  • update() 绑定数据时未使用标识,这里需要对获取到的d3对象,进行位置修改。
  • exit() 数据修改后,出现对多出的d3对象,进行删除。
/**
 * 刷子操作函数
 * 根据区域 获取要展示的数据
 * */
function brushed(brush) {
  let selected = null
  selected = x2.domain().filter(function (d) {
    return brush.selection[0] <= x2(d) && x2(d) <= brush.selection[1]
  })

  let start
  let end
  if (brush.selection[0] != brush.selection[1]) {
    start = selected[0]
    end = selected[selected.length - 1] + 1
  } else {
    start = 0
    end = data.length
  }
  const updatedData = data.slice(start, end)

  enter(updatedData)
  update(updatedData)
  exit(updatedData)

  updateScale(updatedData, brush)
}

2.gif

  • 通过比例尺,获取在范围内的数据(x轴的输入域)。
  • 通过得到的数据,在原始对象中等到范围内的数据。
  • 使用最新数据创建柱状和比例尺。
  • 代码地址