文章目录

  • 品牌排名动态可视化 @ D3.js
    • 任务清单
    • 思路分析
    • 画布初始化
    • 数据预处理
      • 注意将所有值转换为数值类型
      • 注意设置缺失值的缺省值
      • 为每个数据设置颜色
      • 过滤数据,仅保留当前年份的,数据切片,设置排名
    • 首次数据绑定
      • 坐标轴
      • 矩形bar
      • 品牌名label
      • 数值
      • 年份:标记当前是第几年的数据
    • 设置时间触发器ticker
    • 编写更新函数(重中之重)
      • 更新数据,重新获得前12名数据
      • 更新坐标轴
      • 重新进行数据-图元绑定
        • 第一步:绑定,获取```update() ```形态
        • 第二步:enter()
        • 第三步 update()
        • 第四步 exit()
        • 数值渐变
      • 年份更新
  • 完整代码
  • 可视化效果

品牌排名动态可视化 @ 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 rectsvg.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.lastValued.value = isNaN(d.value) ? 0 : +(d.value)d.year = +d.yeard.rank = +d.rankd.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, 再次取反等于2019if(year == 2018){ticker.stop();}year = d3.format('.1f')(+(year)+ 0.1)},tickDuration);//end of tick});//end of d3.csv.then()</script>
</body>
</html>

可视化效果

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

  1. 结合d3.js实现气象数据的可视化

    概述 本文将结合d3.js实现在mapboxGL中格点气象数据的展示. 效果 实现 1.数据格式说明 需要将格点气象数据实现前端的展示,数据传输的方式有三种:1.json:2.二进制:3.灰度图.三种 ...

  2. h5 数字变化_前端/h5 D3.js实现根据数据动态更新图形/类似进度实时变化效果

    最近接到一个需求,在满足规则下,实现类似这种展示效果,其实就是用图形反映数据(NK,一种干扰值) 运行后,它其实是不断在动的,每格都可能显示灰色或者彩色 这里一共是10个格子,每格代表一个范围边界,说 ...

  3. D3.js从入门指南

    D3.js从入门指南 前言 近期略有点诸事不顺,趁略有闲余之时,玩起D3.js.之前实际项目中主要是用各种chart如hightchart.echarts等,这些图形库玩起来貌都是完美的,一切皆可配置 ...

  4. D3.js的v5版本入门教程(第十四章)—— 力导向图

    D3.js的v5版本入门教程(第十四章) 这一章我们来绘制一个力导向图,什么叫力导向图,通俗一点将就是有节点和线组成,当鼠标拖拽一个节点时,其他节点都会受到影响(力导向图有多种类型,本章绘制的效果就是 ...

  5. D3.js有哪些比较好的中文视频教程

    D3.js 是一个用于创建交互式数据可视化图表的 JavaScript 库,它在数据可视化领域很受欢迎.这里有几个可以参考的中文视频教程: D3.js 入门教程:https://www.bilibil ...

  6. d3.js 制作简单的贪吃蛇

    d3.js是一个不错的可视化框架,同时对于操作dom也是十分方便的.今天我们使用d3.js配合es6的类来制作一个童年小游戏–贪吃蛇.话不多说先上图片. 1. js snaker类 class Sna ...

  7. d3.js 制作简单的俄罗斯方块

    d3.js是一个不错的可视化框架,同时对于操作dom也是十分方便的.今天我们使用d3.js配合es6的类来制作一个童年小游戏--俄罗斯方块.话不多说先上图片. 1. js tetris类 由于方法拆分 ...

  8. D3.js的v5版本入门教程(第一章)—— 如何在项目中使用D3.js

    D3.js的v5版本入门教程(第一章) 1.需要的一些工具 这个其实随便!最简单的就是建一个.txt文件就可以敲起代码来!作者本人用的是myeclipse(主要需要安装tomcat),因为写的是前端, ...

  9. D3.js的v5版本入门教程(第六章)——做一个简单的图表

    D3.js的v5版本入门教程(第六章) 从这一章开始,进入正式的d3,js绘图阶段,有了前面几章基本知识的积累,这样看接下来的绘图代码才不会觉得比较辛苦 做一个简单的图表 为了做一个简单的图表,我们还 ...

最新文章

  1. node.js 验证路径有效性_怎样识别光伏谐波路径?试试这个算法
  2. 银行事后监督及票据影像光盘缩微系统
  3. Codeforces Round #193 (Div. 2)
  4. 为啥你用@JsonFormat注解反序列化LocalDateTime总失败?
  5. Vue团队核心成员开发的39行小工具 install-pkg 安装包,值得一学!
  6. Select 可编辑 - 完美支持各大主流浏览器
  7. 三个数差的平方公式推导过程_勾股数公式的简单推导
  8. echarts mysql_ECharts实现mysql 数据图表化
  9. ps如何修改图片大小尺寸_PS常用操作 | 图片的裁剪、拼接、尺寸调整... ...
  10. 5g信号频率是多少赫兹_5G的网速为什么那么快?这得从一个简单的公式说起
  11. OpenLDAP(2.4.3x)服务器搭建及配置说明
  12. 【计算机网络笔记】数据链路层(封装成帧,差错检测,可靠传输)
  13. 深度学习自学(十九):caffe添加深度可分离卷积
  14. mysql导入sql文件出错的一种解决方法
  15. http重定向到https
  16. profinet远程IO总线模块IP67防护等级的优势
  17. 论文详读《基于改进 LeNet-5 模型的手写体中文识别》
  18. Please use torch.load with map_location=torch.device(‘cpu‘),If you are running on a CPU-only machine
  19. python 数独_python 实现 数独 解法 (穷举法)
  20. 微信小程序解密失败的可能原因

热门文章

  1. 中国甲基异丁基甲醇行业头部企业市场占有率及排名调研报告
  2. Centos7中查看IP命令:IP addr
  3. APNS苹果远程推送
  4. 一些忘了的东西。。。
  5. IEC104 电力规约解析
  6. 区块链学习——区块链的技术栈
  7. selinux的主要作用
  8. Odoo12导出文件中文乱码的解决办法
  9. 在卓越网上买了几本书
  10. ArraryList与HashSet——流下了不学无术的眼泪