本文正在参加「金石计划 . 瓜分6万现金大奖」
引入D3模块
- 引入整个D3模块。
<!-- D3模块 -->
<script src="https://d3js.org/d3.v7.min.js"></script>
数据
- 关于排名的数据,量都比较大。这里使用
.csv
文件保存。开发中一般通过接口获取。
- 本地是不支持直接获取外部数据,我们需要启动服务。
- 安装
node.js
,进入.html
文件所在目录,执行npx http-server
。服务启动成功如下图:
- 这样就可以使用
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))
})
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)
...
- 创建初始化函数。
- 修改比例尺
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)
d3.interval(()=>{},30)
循环执行函数。.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()
函数实现数字过度动画。
总结
一个排名柱状图的实现就这么简单。这里是先绘制一个静态柱状图,对每个元素添加唯一标识。利用D3
的特性,根据唯一标识符来判断节点是否存在,存在的节点进行位置修改并添加过渡动画,不存在的进行新节点创建和添加过渡动画,对无用的节点进行删除并添加离开的过渡动画。这里只是一个简单的示例,想要在项目中使用还需要进行细节优化。
© 版权声明
文章版权归作者所有,未经允许请勿转载。
THE END
暂无评论内容