d3 第四章-例子 散点图动画

403 阅读2分钟

效果

scatter-animate.gif

需求

每隔两秒

  • 更新比例尺
  • 所有的点按照新的比例尺重新定位
  • 加一个点 —— 从左上角0,0移动到当相应位置 新加点初始绿色
  • 移除一个点—— 当前页数据第一个点,变红直径渐变为0移除

七步走

获取数据

import { dataset } from '../data/my_weather_data'
const xAccessor = (d: weatherData) => d.dewPoint
const yAccessor = (d: weatherData) => d.humidity

创建图标尺寸

const dimensions = {
  viewBox: '0, 0, 400,400',
  margin: {
    top: 10,
    right: 10,
    bottom: 50,
    left: 50,
  },
  boundedWidth: 340,
  boundedHeight: 340,
}

绘制画布

const wapper = d3.select('#demo-scatterplot-animate')
  const svg = wapper.append('svg').attr('width', '100%').attr('height', '100%').attr('viewBox', dimensions.viewBox)
  const bounds = svg.append('g').style('transform', `translate(${dimensions.margin.left}px,${dimensions.margin.top}px)`)

由于我们后续要更新bounds中的点的属性以及刻度尺内容,我们将刻度尺和数据点的外层标签先写好。

bounds.append('g').attr('class', 'scatter-wapper') 
const xAxis = bounds.append('g')
xAxis.append('text')
const yAxis = bounds.append('g')
yAxis.append('text')

创建刻度尺

我们将会变动的元素放在一个函数中处理。

const drawScatter = function (
  bounds: d3.Selection<SVGGElement, unknown, HTMLElement, any>,
  dataset: Iterable<weatherData>,
  xAxis: d3.Selection<SVGGElement, unknown, HTMLElement, any>,
  yAxis: d3.Selection<SVGGElement, unknown, HTMLElement, any>
) {
}

数据变动则调用drawScatter。 创建刻度尺

const xScale = d3.scaleLinear().domain(d3.extent(dataset, xAccessor)).range([0, dimensions.boundedWidth]).nice()
const yScale = d3.scaleLinear().domain(d3.extent(dataset, yAccessor)).range([dimensions.boundedHeight, 0]).nice()

绘制数据

数据绑定,我们将dataset绑定在单个点的包裹层 .scatter ,而且要传入第二个参数key来确保唯一性,为下次更新数据时做匹配(如果不加key,本次数据和上次数据长度一样就全覆盖了)。

  let scatterGroups = bounds
    .select('.scatter-wapper')
    .selectAll('.scatter')
    .data(dataset, (d: any) => d.date)

添加两个过渡 这里我最后u选择让exit和update同时发生。

  const exitTransition = d3.transition().duration(1000)
  const updateTransition = d3.transition().duration(1000)

选择要移除的元素,添加消失过渡性效果以及移除节点操作:

const exitGroup = scatterGroups.exit()
exitGroup.selectAll('circle').attr('fill', 'red').transition(exitTransition).attr('r', 0)
exitGroup.transition(exitTransition).remove()

选择新增的元素: 要注意这里的enterGroups是scatterGroups.enter()后添加的.scatter选择集。刚开始理解不深如果使用scatterGroups.enter()后续合并就会出问题,选择集合的问题大家可以自己看下。

  const enterGroups = scatterGroups.enter().append('g').attr('class', 'scatter')
  enterGroups.append('circle').attr('cx', 0).attr('cy', 0).attr('r', 0).attr('fill', 'green')

合并数据并绘制元素:

  scatterGroups = scatterGroups.merge(enterGroups as any)
  scatterGroups
    .selectAll('circle')
    .transition(updateTransition)
    .attr('cx', (d: weatherData) => xScale(xAccessor(d)))
    .attr('cy', (d: weatherData) => yScale(yAccessor(d)))
    .attr('r', 5)
    .attr('fill', 'cornflowerblue')

绘制外围

const xAxisGenerator = d3.axisBottom(xScale)
  xAxis
    .call(xAxisGenerator)
    .style('transform', `translate(0,${dimensions.boundedHeight}px)`)
    .select('text')
    .attr('x', dimensions.boundedWidth / 2)
    .attr('y', dimensions.margin.bottom - 10)
    .attr('fill', 'black')
    .style('font-size', '1.4em')
    .text('Dew point (℉)')
  const yAxisGenerator = d3.axisLeft(yScale).ticks(4)
  yAxis
    .call(yAxisGenerator)
    .select('text')
    .attr('x', -dimensions.boundedHeight / 2)
    .attr('y', -dimensions.margin.left + 10)
    .attr('fill', 'black')
    .style('font-size', '1.4em')
    .text('Relative humidity')
    .style('transform', 'rotate(-90deg)')
    .style('text-anchor', 'middle')

加个循环结束

  let i = 0
  setInterval(() => {
    i++
    const data = dataset.slice(i, i + 10)
    drawScatter(bounds, data, xAxis, yAxis)
  }, 2000)