【D3.js实战】 品牌排名动态可视化

品牌排名动态可视化 @ D3.js

任务清单

  • 做出一个类似于下图所示的能根据时间更新变换的品牌动态排行榜

在这里插入图片描述

  • 数据地址
    https://gist.github.com/jrzief/70f1f8a5d066a286da3a1e699823470f
    整个clone下来即可

由于多位朋友反应,这个数据集比较难下载,我把它下载好了放在了我的网盘上,大家自行下载。
在文章结尾有源代码,复制下来即可运行。
数据链接: https://pan.baidu.com/s/1VQeqk5oDTTSysPI4SHGLwg 提取码: gpve

  • 要求:1.动画效果,2.各品牌的排名与数值根据年份进行修改

思路分析

  • 画布初始化
  • 数据预处理
  • 首次数据-图元绑定,即首次data-join
  • 设置时间触发器(d3.interval / setInterval)
  • 编写更新函数
    • 更新数据,即重新得到前12名数据
    • 更新x坐标轴(因为数据值会变大,原来的坐标轴不适用了)
    • 重新进行数据-图元绑定
    • 年份更新

画布初始化

首先要进行画布初始化,创建初始画布svg,定义全局变量,绘制标题,副标题,坐标轴等等。

let svg = d3.select('body').select('svg');
const top_n = 12;
const margin = {
    top : 80,
    right : 65,
    bottom : 5,
    left : 20 
};
let height = +(svg.attr('height'))
let width = +(svg.attr('width'))
let xAxis;
let xScale;
let yScale;
let yearText;

const barPadding = (+svg.attr('height')-margin.top-margin.bottom)/(top_n*5)

let title = svg.append('text')
.attr('class','Title')
.attr('y', 30)
.attr('x',width/2 )
.text('BrandRank')

let subTitle = svg.append('text')
.attr('y', 55 )
.attr('x', width-margin.right-70)
.attr('class', 'subTitle' )
.text("Brand value  ,  $m")
let year = 2000;

const tickDuration = 500;//执行间隔

数据预处理

这里的数据预处理有以下几点需要注意的

注意将所有值转换为数值类型

因为d3.csv默认读进来数据都是字符串类型的,因此为了方便后续计算,要转换成数值类型

注意设置缺失值的缺省值

由于数据中有许多 NaN,集中在value字段,因此在value字段要多进行一步缺省。
注意:缺省必须在转换为数值之后

d.value = +(d.value)
d.value = isNaN(d.value) ? 0 : d.value
为每个数据设置颜色

这里比较有意思,设置颜色这步完全可以挪到data-join的时候,但是这里可以默认为某个数据绑定上一个默认的颜色,使用到的是d3.hsl()这个接口。

d.color = d3.hsl(Math.random()*360,0.75,0.75,0.8)

这是设置完的color数据值,最后一个参数是透明度,缺省值为1。

在这里插入图片描述

过滤数据,仅保留当前年份的,数据切片,设置排名
  • data.filter()过滤数据。然后由于绑定只需要使用到前12名数据,因此这里用到了slice()切片函数进行了数据切片处理。

    let yearSlice = data.filter(d => d.year == year && !isNaN(d.value))
    .sort((a,b) => b.value - a.value)
    .slice(0,top_n)
    
  • 由于最终需要根据数值排名来设置矩形的位置,因此要设置一下yearSlice中的数据排名

    //此时索引就是排名了
    yearSlice.forEach((d,i) => d.rank = i)
    

首次数据绑定

首次数据绑定我这里为了让逻辑更加清晰,封装成了一个函数render_init(yearSlice),而没有写在d3.csv.then()中 ,参数是已经切分好的当前年份top12的数据。注意,render_init()中的内容是后续需要更新的图元,由于坐标轴后续也需要更新,因此将坐标轴也放在render_init()

//yearSilce:切片好了的数据
const render_init = function(yearSlice){
    //functionBody()
}
坐标轴
    xScale = d3.scaleLinear()
        .domain([0,d3.max(yearSlice,d => d.value)])
        .range([margin.left , width-margin.right])
        .nice()

    yScale = d3.scaleLinear()
        .domain([top_n, 0])
        .range([height-margin.bottom, margin.top]);

    xAxis = d3.axisTop(xScale)
        .ticks(width > 500 ? 5:2)
        .tickSize(-(height-margin.top-margin.bottom))
        .tickFormat(d => d3.format(',')(d))
    ;

    svg.append('g')
        .attr('class','xAxis')
        .call(xAxis)
        .attr('transform',`translate(${margin.left},${margin.top})`)
矩形bar
//首次join rect
svg.selectAll('rect.bar')
    .data(yearSlice,d => d.name)
    .enter()
    .append('rect')
    .attr('class','bar')
    .attr('x',xScale(0)+margin.left+2)
    .attr('width', d => xScale(d.value)-xScale(0))
    .attr('y', d => yScale(d.rank))
    .attr('height', yScale(1)-yScale(0)-barPadding)
    .attr('fill', d => d.color)
品牌名label
//首次join text,品牌名
svg.selectAll('text.label')
    .data(yearSlice, d => name)
    .enter()
    .append('text')
    .attr('class', 'label')
    .attr('x', d => xScale(d.value)-8)
    .attr('y', d => yScale(d.rank)+((yScale(1)-yScale(0))/2))
    .attr('text-anchor','end')
    .text(d => d.name)
数值
//首次join text,数值
svg.selectAll('text.valueLabel')
    .data(yearSlice, d => d.name)
    .enter()
    .append('text')
    .attr('class', 'valueLabel')
    .attr('x', d => xScale(d.value)+9)
    .attr('y',d => yScale(d.rank) +((yScale(1)-yScale(0))/2))
    .text(d => d3.format(',.0f')(d.lastValue));
年份:标记当前是第几年的数据
  //年份
    yearText = svg.append('text')
        .attr('class', 'yearText')
        .attr('x', width-margin.right+60)
        .attr('y', height-25)
        .attr('font-size','2em' )
        .style('font-weight', 'bold')
        .style('fill', '#2eb0c5d9')
        .style('text-anchor', 'end')
        .html(~~year)

设置时间触发器ticker

接下来,设置触发器ticker,每隔某个间隔(这里为了看着连贯,间隔设置为500ms),就重新绑定一次数据,更新图元。

let ticker = d3.interval(e =>{
    //更新部分函数
},tickDuration)
//tickDuration是更新部分函数的执行间隔

编写更新函数(重中之重)

这部分是这个程序最最关键的部分,要求思路必须非常清晰,下面我分成四个部分介绍

  • 更新数据,即重新得到前12名数据
  • 更新x坐标轴(因为数据值会变大,原来的坐标轴不适用了)
  • 重新进行数据-图元绑定
  • 年份更新
更新数据,重新获得前12名数据

这步与数据预处理中的最后一步相同

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;
})
更新坐标轴

数据随着年份进行更新,数据空间的最大值改变了,因此要改变比例尺,重新映射数据。由于是数据空间改变了,因此修改domain()即可,修改后,要用call()坐标轴图元才会执行更新。

xScale.domain([0,d3.max(yearSlice,d=>d.value)]);
svg.select('.xAxis')
    .transition()
    .duration(tickDuration)
    .ease(d3.easeLinear)
    .call(xAxis);
重新进行数据-图元绑定

重新绑定和初次绑定一样,对于矩形,品牌名,数值都要重新绑定,对于年份也要更新。这里为了方便,仅以矩形的重新绑定为例讲解思路。在数值的重新绑定中会讲一下如何实现数值的渐变

以矩形的重新绑定为例:

分为三步骤 enter(),update(),exit() 。翻译翻译就是:

  • 把新出现在top12中的数据绑定上,并绘制在坐标轴中。(enter())
  • 没有跌出top12的数据需要更新,重新绑定图元(update())
  • 把跌出top12的数据绑定的图元清除(exit()
第一步:绑定,获取update() 形态

首先,为了方便操作,首先绑定数据,绑定后获得数据的update形态

let bars = svg.selectAll('.bar')
.data(yearSlice, d=> d.name);
第二步:enter()

想要制造没有出现的矩形从下面滑上来的效果,因此y的值首先要设置在画布外面,最后通过动画缓动效果滑到坐标轴中,根据排名设置y

//enter,将尚未存在top12中的数据加入进来
//注意,transition前的y是在svg之外的,这样才会做成从下面往上浮现的效果
bars.enter()
			.append('rect')
			.attr('class','bar')
			.attr('x',xScale(0)+margin.left+2)
			.attr('width', d => xScale(d.value)-xScale(0))
			.attr('y', d => yScale(top_n+1)+20)
			.attr('height', yScale(1)-yScale(0)-barPadding)
			.attr('fill', d => d.color)
			.transition()
			.duration(tickDuration)
			.ease(d3.easeLinear)
    .attr('y',d => yScale(d.rank))
第三步 update()

原来在画布中的矩形只需要更新它的排名(即y值),和它的宽度(即value)即可。

//第二次update,之前存在top12的数据要进行修改
bars.transition()
    .duration(tickDuration)
    .ease(d3.easeLinear)
    .attr('width', d => xScale(d.value)-xScale(0))
    .attr('y', d =>yScale(d.rank))
第四步 exit()

退出的时候直接把y值改到画布外面即可,然后再remove()

//最后一步exit()
bars.exit()
    .transition()
    .duration(tickDuration)
    .ease(d3.easeLinear)
    .attr('y', d => yScale(top_n+1)+5)
    .attr('width', d => xScale(d.value)-xScale(0)-1)
    .remove()
数值渐变

在可视化中想要做成从0-100数值慢慢上涨的动画效果,需要用到tween()d3.interpolateRound()

.attr()...
.tween("textTween",function(d){
    //做出在两个value间跳动的效果
    let i = d3.interpolateRound(d.lastValue,d.value);
    return function(t){
        this.textContent = d3.format(',')(i(t));
    };

其他的代码贴在最后,与矩形的更新相同

年份更新

这里很有意思,直接贴代码。
值得一提的是,为了让年份慢慢更新,每次只加0.1

yearText.html(~~year)
//~~year:等于向上取整,2018.1 取反等于 -2019, 再次取反等于2019
if(year == 2018){
    ticker.stop();
}
year = d3.format('.1f')(+(year)+ 0.1)

完整代码

<!DOCTYPE html>
<html>
<head>
	<title>brandRank</title>
	<script src="./js/d3.min.js"></script>
	<style>
		  text.Title{
      font-size: 2em;
      font-weight: 500;
    }
      text.subTitle{
        font-weight: 500;
        fill: #777777;
      }
      text.label{
      	font-weight:bold;
      }
	</style>
</head>
</head>
<body>
	<svg width="960" height="600"></svg>

	<script>
		let svg = d3.select('body').select('svg');
		const top_n = 12;
		const margin = {
			top : 80,
			right : 65,
			bottom : 5,
			left : 20 
		};
		let height = +(svg.attr('height'))
		let width = +(svg.attr('width'))
		let xAxis;
		let xScale;
		let yScale;
		let yearText;

		const barPadding = (+svg.attr('height')-margin.top-margin.bottom)/(top_n*5)

		let title = svg.append('text')
		.attr('class','Title')
		.attr('y', 30)
		.attr('x',width/2 )
		.text('BrandRank')

		let subTitle = svg.append('text')
		.attr('y', 55 )
		.attr('x', width-margin.right-70)
		.attr('class', 'subTitle' )
		.text("Brand value  ,  $m")
		let year = 2000;

		const tickDuration = 500;//执行间隔

		const render_init = function(yearSlice){
			xScale = d3.scaleLinear()
			.domain([0,d3.max(yearSlice,d => d.value)])
			.range([margin.left , width-margin.right])
			.nice()

			yScale = d3.scaleLinear()
			.domain([top_n, 0])
	        .range([height-margin.bottom, margin.top]);

			xAxis = d3.axisTop(xScale)
			.ticks(width > 500 ? 5:2)
			.tickSize(-(height-margin.top-margin.bottom))
			.tickFormat(d => d3.format(',')(d))
			;

			svg.append('g')
			.attr('class','xAxis')
			.call(xAxis)
			.attr('transform',`translate(${margin.left},${margin.top})`)

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

			//首次join text,品牌名
			svg.selectAll('text.label')
			.data(yearSlice, d => name)
			.enter()
			.append('text')
			.attr('class', 'label')
			.attr('x', d => xScale(d.value)-8)
			.attr('y', d => yScale(d.rank)+((yScale(1)-yScale(0))/2))
			.attr('text-anchor','end')
			.text(d => d.name)

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

			//年份
		    yearText = svg.append('text')
		      .attr('class', 'yearText')
		      .attr('x', width-margin.right+60)
		      .attr('y', height-25)
		      .attr('font-size','2em' )
		      .style('font-weight', 'bold')
		      .style('fill', '#2eb0c5d9')
		      .style('text-anchor', 'end')
		      .html(~~year)
		}

		d3.csv("./data/brand_values.csv").then(data =>{

			//数据预处理
			data.forEach(d => {
				d.lastValue = +d.lastValue
				d.value = isNaN(d.value) ? 0 : +(d.value)
				d.year = +d.year
				d.rank = +d.rank
				d.color = d3.hsl(Math.random()*360,0.75,0.75,0.8)
			})
			console.log(data);
			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)


			render_init(yearSlice);


			let ticker = d3.interval(e =>{
			//重新切分数据
			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;
			})

			//对x轴进行重新映射
			xScale.domain([0,d3.max(yearSlice,d=>d.value)])
			//x轴随着图元的值修改
			svg.select('.xAxis')
			.transition()
			.duration(tickDuration)
			.ease(d3.easeLinear)
			.call(xAxis)


			//第一步:获取update部分
			let bars = svg.selectAll('.bar')
			.data(yearSlice, d=> d.name);

			//enter,将尚未存在top12中的数据加入进来
			//注意,transition前的y是在svg之外的,这样才会做成从下面往上浮现的效果
			bars.enter()
			.append('rect')
			.attr('class','bar')
			.attr('x',xScale(0)+margin.left+2)
			.attr('width', d => xScale(d.value)-xScale(0))
			.attr('y', d => yScale(top_n+1)+20)
			.attr('height', yScale(1)-yScale(0)-barPadding)
			.attr('fill', d => d.color)
			.transition()
			.duration(tickDuration)
			.ease(d3.easeLinear)
			.attr('y',d => yScale(d.rank))

			//第二次update,之前存在top12的数据要进行修改

			bars.transition()
			.duration(tickDuration)
			.ease(d3.easeLinear)
			.attr('width', d => xScale(d.value)-xScale(0))
			.attr('y', d =>yScale(d.rank))

			//最后一步exit()
			bars.exit()
			.transition()
			.duration(tickDuration)
			.ease(d3.easeLinear)
			.attr('y', d => yScale(top_n+1)+5)
			.attr('width', d => xScale(d.value)-xScale(0)-1)
			.remove()

			//labels与bars相同
			let labels = svg.selectAll('.label')
			.data(yearSlice,d => d.name)


			labels.enter()
			.append('text')
			.attr('class', 'label')
			.attr('x', d => xScale(d.value)-8)
			.attr('y', d => yScale(top_n+1)+20)
			.attr('text-anchor','end')
			.text(d => d.name)
			.transition()
			.duration(tickDuration)
			.ease(d3.easeLinear)
			.attr('y', d => yScale(d.rank)+((yScale(1)-yScale(0))/2) +1)

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

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


			//valueLabels与bars相同
			let valueLabels = svg.selectAll('.valueLabel')
			.data(yearSlice,d => d.name)

			valueLabels
			.enter()
			.append('text')
			.attr('class', 'valueLabel')
			.attr('x', d => xScale(d.value)+9)
			.attr('y',d => yScale(top_n) +20)
			.text(d => d3.format(',.0f')(d.lastValue))
			//enter进来的时候用lastvalue,后面update的时候再慢慢增加至value
			.transition()
			.duration(tickDuration)
			.ease(d3.easeLinear)
			.attr('y', d => yScale(d.rank) +((yScale(1)-yScale(0))/2) );


			//update加一点数值渐变效果
			valueLabels
			.transition()
			.duration(tickDuration)
			.ease(d3.easeLinear)
			.attr('x', d => xScale(d.value)+9)
			.attr('y', d => yScale(d.rank) +((yScale(1)-yScale(0))/2) )
			.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)+9)
			.attr('y', d => yScale(top_n+1)+20)
			.remove()


			yearText.html(~~year)
     		//~~year:等于向上取整,2018.1 取反等于 -2019, 再次取反等于2019

			if(year == 2018){
				ticker.stop();
			}
			year = d3.format('.1f')(+(year)+ 0.1)
			},tickDuration);//end of tick
			
		});//end of d3.csv.then()
	</script>
</body>
</html>

可视化效果

在这里插入图片描述

  • 3
    点赞
  • 33
    收藏
    觉得还不错? 一键收藏
  • 10
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值