公司项目要求作动态条形图,也就是Bar chart race,原本想从网上找个demo发觉没有合适的,就本身写了一个,完整代码能够去 个人GitHub 上查看。动态条形图能够很好地对比多个数据随之间变化的趋势。该demo是基于 d3
的 v6
版本,因为仅使用了一些最基础的 api
,因此在以前版本应该也能够跑起来。本文章仅提供一种实现的思路,不对 api
和“进入、更新、退出”模式做详细讲解,因此在阅读以前最后对它们有一个最基本的了解。
首先须要找一个合适的数据源,这里我就从网上随便找了一份。git
初始化变量github
const width = 1200, height = 600, margin = { top: 20, bottom: 0, left: 50, right: 80 }; const chartWidth = width - (margin.left + margin.right), chartHeight = height - (margin.top + margin.bottom); const data = []; const count = 10; const duration = 500; const barPadding = 20; const barHeight = (chartHeight - (barPadding * count)) / count; const getDate = () => dataOri[0][dateIndex]; let dateIndex = 1; let date = getDate(); let dataSlice = []; let chart = null, scale = null, axis = null, svg = null, dateTitle = null;
首先设定长、宽、外边距,和图表尺寸。data
存放格式化后的数据。因为图表不可能把数据源中全部的行都显示出来,因此这里只取前 10
个。每隔 10
秒切换一纵列。柱间距为 20
。用图表高度减去柱间距乘以柱数量再除以柱数量得每一个柱的宽。定义一个函数来获取当前列表头,这里就是日期,赋给 date
。定义 dataSlice
来存放当前日期下的全部数据。最后定义 chart
存放图表实例,scale
存放比例尺,axis
存放坐标轴,svg
存放画布,dateTitle
存放当前列表头。api
const createSvg = () => svg = d3.select('#chart').append('svg').attr('width', width).attr('height', height);
建立一个 svg
设置宽高并 append
到预先写好的 container
中。app
格式化数据dom
function randomRgbColor() { const r = Math.floor(Math.random() * 256); const g = Math.floor(Math.random() * 256); const b = Math.floor(Math.random() * 256); return `rgb(${r},${g},${b})`; }
先声明一个建立随机颜色的函数,用来给条形上色。
咱们的数据源是这种形式的,须要对其进行简单的格式化,成为一个一个条目svg
const formatData = () => { dataOri[0].forEach((date, index) => { if (index > 0) { dataOri.forEach((row, rowIndex) => { if (rowIndex > 0) { data.push({ name: row[0], value: Number(row[index]), lastValue: index > 1 ? Number(row[index - 1]) : 0, date: date, color: randomRgbColor() }); } }); } }); }
两层循环,第一层循环列,第二层循环行,存入行表头,数据,上一列的数据,若是没有就写 0
,列表头,和一个随机的颜色,用做给条形图上色,至于 lastValue
的用途以后用到了会详细解释。
格式化后的数据如图所示:函数
const sliceData = () => dataSlice = data.filter(d => d.date === date).sort((a, b) => b.value - a.value).slice(0, count);
筛选出当天的数据,倒叙排列并取前 10
个。工具
建立坐标轴动画
const createScale = () => scale = d3.scaleLinear().domain([0, d3.max(dataSlice, d => d.value)]).range([0, chartWidth]);
首先把比例尺建立出来,定义域是 0
到当天的最大值,值域是 0
到图表宽度,对 d3.js
的 api
不熟悉的同窗能够去官网补习一下或者自行百度,经常使用的基本就那么几个。
坐标轴的最终效果以下图所示:this
须要对坐标轴进行简单的配置
const renderAxis = () => { createScale(); axis = d3.axisTop().scale(scale).ticks(5).tickPadding(10).tickSize(0); svg.append('g') .classed('axis', true) .style('transform', `translate3d(${margin.left}px, ${margin.top}px, 0)`) .call(axis); }
调用以前定义的比例尺函数建立比例尺,而后设置顶部的坐标轴,ticks
设置 5
个刻度(这个方法比较有意思,虽然设置了5
,可是不必定真的是 5
,可能比 5
多也可能比 5
少),tickPadding
设置刻度与数值之间的间距,tickSize
设置刻度线长度,这里不让它显示。设置完成以后 append
到图表中,水平位移,让出边距的位置。
建立参考线
这条竖线就是参考线,从坐标轴刻度延伸出来,贯穿整个图表。
const renderAxisLine = () => { d3.selectAll('g.axis g.tick').select('line.grid-line').remove(); d3.selectAll('g.axis g.tick').append('line') .classed('grid-line', true) .attr('stroke', 'black') .attr('x1', 0) .attr('y1', 0) .attr('x2', 0) .attr('y2', chartHeight); }
因为随着数据的变化,参考线是不断变化的,该函数会被反复调用,因此要在一开始清除上一组数据的参考线。而后在坐标轴每个有刻度线的位置都 append
一条线进去,x1
和 y1
是该条线相对于父元素的左端点,x2
和 y2
是右端点,因为要贯穿整个图表,因此右端点的 y
坐标设置为 chartHeight
。
建立列表头
图表右下键这个日期,也就是列的表头
const renderDateTitle = () => { dateTitle = svg.append('text') .classed('date-title', true) .text(date) .attr('x', chartWidth - margin.top) .attr('y', chartHeight - margin.left) .attr('fill', 'rgb(128, 128, 128)') .attr('font-size', 40) .attr('text-anchor', 'end') }
位移至右下角,设置颜色。这里重点说一下 text-anchor
,主要运用在 svg
中 <text>
标签的一个属性,设置文本的对其方式,设置为 end
表示文本字符串的末尾即当前文本的初始位置。
建立图表主体
const createChart = () => { chart = svg.append('g') .classed('chart', true) .style('transform', `translate3d(${margin.left}px, ${margin.top}px, 0)`); }
建立一个容器存放多个条形,并移到正中央。
const renderChart = () => { // 进入、更新、退出模式 }
该函数存放“进入、更新、退出”模式的代码,因为该模式能够单独引伸出一篇文章去讲解,不太了解的同窗仍是建议先去自行理解一下。
const bars = chart.selectAll('g.bar').data(dataSlice, (d) => d.name); let barsEnter; barsEnter = bars.enter() .append('g') .classed('bar', true) .style('transform', (d, i) => `translate3d(0, ${calTranslateY(i)}px, 0)`); dateIndex > 1 && barsEnter .transition().duration(this.duration) .style('transform', (d, i) => `translate3d(0, ${calTranslateY(i, 'end')}px, 0)`); barsEnter.append('rect') .style('width', d => scale(d.value)) .style('height', barHeight + 'px') .style('fill', d => d.color); barsEnter.append('text') .classed('label', true) .text(d => d.name) .attr('x', '-5') .attr('y', barPadding) .attr('font-size', 14) .style('text-anchor', 'end'); barsEnter.append('text') .classed('value', true) .text(d => d.value) .attr('x', d => scale(d.value) + 10) .attr('y', barPadding);
将图形与 dataSlice
绑定,barsEnter
表明的是绑定了数据的图形,设置它的宽,高和颜色,条形左侧的 y
轴,这里对应的国家的名字,还有右侧数值标注。这里用到了一个工具函数:
const calTranslateY = (i, end) => { if (dateIndex === 1 || end) { return (barHeight + barPadding) * i + (barPadding / 2); } else { return (barHeight + barPadding) * (count + 1); } }
当数据为第一列或者传入 end
的时候条形的纵轴位置在排序所在的位置,不然都放在图表外面,等待进入。
bars.transition().duration(duration).ease(d3.easeLinear) .style('transform', function (d, i) { return 'translate3d(0, ' + calTranslateY(i, 'end') + 'px, 0)'; }) .select('rect') .style('width', function (d) { return scale(d.value) + 'px'; }); bars .select('text.value') .transition().duration(duration).ease(d3.easeLinear) .attr('x', function (d) { return scale(d.value) + 10; }) .tween('text', function (d) { const textDom = this; const i = d3.interpolateRound(d.lastValue, d.value); return (t) => textDom.textContent = i(t); });
更新模式,第一个方法链目的是条形按照顺序排序,而且根据数值设定宽度。第一个方法链是设定右面标注的数值,而且自定义了一个数值过渡,让数值的增加没有那么生硬,这里用到了一开始格式化数据的时候设置的 lastValue
。
bars.exit() .transition().duration(duration).ease(d3.easeLinear) .style('transform', function (d, i) { return 'translate3d(0, ' + calTranslateY(i) + 'px, 0)'; }) .style('width', function (d) { return scale(d.value) + 'px'; }) .remove();
退出模式,将退出后的条形移到屏幕外并删除。
调用方法
const init = () => { createSvg(); // 建立一个svg formatData(); // 格式化数据 sliceData(); // 截取当天数据 renderAxis(); // 渲染坐标轴 renderAxisLine(); // 渲染指示线 renderDateTitle(); // 渲染日期 createChart(); // 建立图表 renderChart(); // 渲染图表 createTicker(); // 建立定时器 } init();
依次调用一开始声明的那些方法,还有最后一个 createTicker
方法没有声明
function createTicker() { const ticker = d3.interval(() => { if (dateIndex < dataOri[0].length - 1) { dateIndex++; date = getDate(); dateTitle.text(date); sliceData(); updateAxis(); renderAxisLine(); renderChart(); } else { ticker.stop(); } }, duration); }
建立了一个定时器,每隔 duration
设定的事件进行切换,更新坐标轴、辅助线、图表等,这里用到了 updateAxis
方法。
const updateAxis = () => { createScale(); axis.scale().domain([0, d3.max(dataSlice, d => d.value)]); svg.select('g.axis') .transition().duration(duration).ease(d3.easeLinear) .call(axis); d3.selectAll('g.axis g.tick text').attr('font-size', 14); }
该方法用于当数据改变时更新坐标轴。
总结
动态条形图的全部功能都已经开发完了,打开页面就能够看到动画效果了。完整代码能够个人 GitHub 中下载。其实该图表算是 d3
比较入门的效果,掌握了“进入、更新、退出”模式和过渡以后就能够开发出来了。本文提供的思路也并不是该图表实现的最优解,若有更好的实现方法欢迎留言讨论。