十四. 数据可视化(Echarts Canvas D3 ThreeJs)

827 阅读9分钟

(一). Echarts

1.基础

1.1 什么是数据可视化呢?

  • 可以把数据更直观的形式展示 ---> 图表

1.2 数据可视化的好处?

  • 可以清晰有效低传达与沟通信息
  • 可以容易看出,隐藏在数据中的信息

1.3 可视化的实现方式

1.3.1 报表类

  • Excel
  • 水晶报表

1.3.2 商业智能BI

  • Microsoft BI
  • Power-BI

1.3.3编码类

  • Echarts.js( 百度公司开源可视化库)
  • D3.js (国外可视化库)

2.ECharts

2.1 Echarts 的介绍

  • ECharts是一个使用JavaScript实现的开源可视化库,兼容性强,底层依赖矢量图形库ZRender, 提供直观 交互丰富 可高度个性化定制的数据可视化图表.
  • 特点: 开源免费,功能丰富,社区活跃
  • 丰富可视化类型: 折线图,柱状图,饼图,k线图
  • 多种数据格式的支持:
    • key-value数据格式
    • 二维表
    • TypedArray格式
  • 流数据的支持
    • 流数据的动态渲染
    • 增量渲染技术
  • 其他
    • 移动端的优化
    • 跨平台使用
    • 绚丽的特效
    • 三维可视化

2.2 ECharts的基本使用

  • 引入echarts.js
  • 准备一个盒子
  • 初始化echarts实例对象
  • 准备配置项 (配置项的使用,借住官方文档)

3. Echarts常用图表

七大图表

  • 柱状图
  • 折线图
  • 散点图
  • 饼图
  • 地图
  • 雷达图
  • 仪表盘图

3.1 柱状图

  • 描述的是分类数据, 呈现的是每一个分类中 有多少
==============================
通用配置
title: {
  text: '成绩展示'
  textStyle: {....},   // 文字的样式
  borderWidth: 5,
  borderColor: 'blue',
  borderRadius: 5
  left: 30
  top: 
  right: 
  bottom: 
}
tooltip: {
  trigger: 'item',   //axis
  triggerOn: 'click'   // mouseover
  //formatter: '*****'   // 字符串模板或者函数
  formatter: function(arg) {
    console.log(arg)
    return arg[0].name + '的分数是:' + arg[0].data
  }
},
toolbox: {
  feature: {
    saveAsImage: {},   // 导出图片
    dataView: {}   // 数据视图
    restore: {}   // 重置的功能
    dataZoom: {}   // 区域缩放
    magicType: {
      type: ['bar','line']
    }
  }
},
legend: {   // 筛选按钮
  data: ['语文','数学']
}
==============================
xAxis: {
  type: 'category',   // x与y轴可以互换
  data: xDataArr
},
yAxis: {
  type: 'value',
},
series: [
  {
    name: '语文',
    type: 'bar',
    markPoint: [
      data; [
        {type: 'max', name: '最大值'},
        {type: 'min', name: '最小值'}
      ]
    ],
    markLine: {
      data: [
        {
          type: 'average', name: '平均值'
        }
      ]
    },
    label: {
      show: true,
      rotate: 60,
      position: 'inside'   // 数值显示的位置
    },
    barWidth: '30%',   // 柱图宽度
    data: yDataArr
  }
]

3.2 折线图

  • 常用来分析数据随时间的变化趋势
  • 基本和柱状图相同, 把bar改成line
xAxis: {
  type: 'category',
  data: xDataArr,
  boundaryGap: false,   // 紧挨边缘
}
yAxis: {
  type: 'value',
  scale: true,   // 摆脱0值比例 
}
series: [
  {
    name: '销售成绩'
    data: yDataArr
    type: 'line',
    markPoint: {
      data: [ {type:'max', name:'最大值'}, {type:'min', name:'最小值'},]
    },
    markLine: {
      data: [ {type: 'average'} ]
    }
    markArea: {
      data: [ 
        [{xAxis: '1月'}{xAxis: '2月'}], [{xAxis: '7月'}{xAxis: '8月'}] 
      ]
    },
    smooth: true,   // 平滑曲线
    lineStyle: {   // 颜色风格
      color: 888,
      type: 'dashed',   // solid dotted
    },
    areaStyle: {   // 填充效果
      color: ***,
    },
    stack: 'all'   // 堆叠图
  },
  
  {
    type: 'line',
    data: yDataArr2,
    stack: 'all'   // 堆叠图
  }
  
  
}  

3.3 散点图

散点图可以帮助我们推断出,不同维度数据之间的相关性

  • 柱形图和折线图的x轴一般代表类别. 散点图的x轴代表数值了.
  • x,y轴的数据要求是一个二维数组
  • 散点图的特点: 散点图可以帮助我们推断出不同维度数据之间的相关性
xAxis: {
  type: 'value',
  scale: true,   // 摆脱0值比例
}
yAxis: {
  type: 'value',
  scale: true,   // 摆脱0值比例 
}
series: [
  {
    // type: 'scatter',
    type: 'effectAcatter',   //涟漪动画
    showEffectOn: '',   // render渲染完就有  emphasis鼠标移入才有 
    rippleEffect: {
      scale: 10   // 涟漪动画缩放比例
    },
    
    data: axisData,
    symbolSize: 20 // 散点图 点的大小
    symbolSize: function (arg){
      console.log(arg)   // 打印的是每个散点的信息 []数组
      return 40
    },
    itemStyle: {
      color: ***,
      color: function(arg) {
        console.log(arg)   // 打印的是对象, 对象里边了data才是散点信息 []数组
        return '#f99'
      }
    }
  },
  
  {},
  
]

直角坐标系的常用配置( 只对柱状图,折线图,散点图有效果)

(1)网格grid
grid: {
  show: true,
  borderWidth: 10
  borderColor: ***
  left: 200,
  top: 200,
  width:300,
  height: 300,
}
(2) axis坐标轴配置
type: category/ value
position: bottom/top是x轴的取值  left/right是y轴的取值
(3) 区域缩放 
toolbox: {
  feature: {
    dataZoom: {}
  }
}
或者
dataZoom用户区域缩放对数据范围的过滤
dataZoom: [
  {
    type: 'slider',   // inside依靠鼠标滚轮或双击
    xAxisIndex: 0,   //指明产生的作用轴
  },
  {
    type: 'slider',
    yAxisIndex: 0,   // 指明产生的作用轴
    start: 0,   // 指明初始的缩放情况
    end: 50,   // 50%
  }
  
  
  
]

3.4 饼图

  • 可以很好的帮助用户快速了解不同分类的占比情况
不用axis, 配置series就ok
series: [
  {
    type: 'pie',
    data: pieData,
    label: {   // 饼图文字的显示
      show: true,
      // formatter: '',   // 决定文字显示的内容
      formatter: function(arg) {
        console.log(arg)
        return arg.name + '平台' + arg.vlaue +'元\n'
      }
    },
    // radius: 20   // 饼图半径的设置
    // radius: 20%  按父盒子的半分比
    // radius: ['50%', '75%']   // 圆环的设置
    roseType: 'raidus'   // 南丁格尔图, 半径是不同的
    selectedMode: 'single'   // 选中效果    multiple
    selectedOffset: 30,   // 选中偏移量
  }
]

3.5 地图

3.5.1 地图图表的使用方式

  • 百度地图API, 需要申请一个百度地图的ak
  • 矢量地图, 需要准备矢量地图的数据

3.5.2 矢量地图的实现步骤

  • ECharts最基本的代码结构: 引入echarts.js, 准备DOM容器,初始化对象,设置option

  • 准备中国的矢量地图json文件,放到json/map/的目录下(china.json)

  • 使用Ajax获取china.json, $.get( 'json/map/china.json',function(chinaJson){} )

  • 在成功的回调中,往echarts全局对象注册地图的json数据 echarts.registerMap('chinaMap333',chinaJson);

  • 第五步: 配置geo

geo: {
  type: 'map',
  map: 'chinaMap333'   // 数据源
  roam: true   // 允许缩放和拖动的效果
  label: {
    show: true   // 展示标签
  },
  zoom: 2,   // 设置 初始缩放比例
  center: [86.111, 87.111],   // 设置题图中心点的坐标
},
series: [   // 让不同的省份 展示不同的颜色--------------------
  {
    data: airData,   // airData空气质量的数组格式数据
    geoIndex: 0,   // 将空气质量的数据和第0个geo的配置关联在一起
    type: 'map',   // 
  },
  {   // 与散点图结合  使用------------------------------
    data: scatterData,   // 配置页一个散点坐标数据 scatterData == [ {value:[114.33, 31.342]}, ]
    type: 'effectScatter',
    coordinateSystem: 'geo',   // 指明散点使用的坐标系  geo的坐标系
    rippleEffect: {
      scale: 10
    }
  },
],
visualMap: {
  min: 0,
  max: 300
  inRange: {
    color: ['white','red']
  },
  calculable: true
}


=======================================================
显示地图的某个区域(如湖南省):
通过registerMap注册到echarts全局对象中
配置geo
通过zoom放大
通过center定位中心点


3.6 雷达图

  • 雷达图可以用来分析多个维度的数据与标准数据的对比情况
dataMax = [   // 各个维度的最大值
  {
    name: '易用性',
    max: 100
  },
  {
    name: '功能',
    max: 100
  },
  {
    name: '拍照',
    max: 100
  },
  {
    name: '跑分',
    max: 100
  },
  {
    name: '续航',
    max: 100
  },
]

----------------
radar; {
  indicator: dataMax,   // 最大值的配置
  shape: 'circle',   // 配置雷达图最外层的图形, polygon
},

series: [
  {
    type: 'radar',
    label: {
      show: true,   // 显示出每一个数值
    },
    areaStyle: {   // 阴影面积
    },
    data: [
      {
        name: '华为手机1',
        value: [80,90,80,88,92]
      },
      {
        name: '小米手机1',
        value: [80,90,80,88,92]
      },
    ]
  }
]

3.7 仪表盘

  • 主要用在进度把控以及数据范围的检测
series: [
  {
    type: 'gauge'
    data: [
      {
        value: 97,
        itemStyle: {
          color: 'pink'
        }
      },
      {   // 每个对象就代表一个指针
        value: 77,
        itemStyle: {
          color: 'red'
        }
      }
    ],
    min: 50
    max: 300
  },
  
]

4. 高级使用

4.1 主题

  • var chart = echarts.init(dom, 'light') 可指明主题,light或者dark
  • 自定义主题
(1) 在主题编辑器中编辑主题 https://www.echartsjs.com/theme-builder/
(2) 下载主题, 是一个js文件
(3) 在网页中引入主题js文件
(4) 在init方法中使用主题echarts.init(dom, 'mytheme')

4.2 调色盘

* 主题调色盘
* 全局调色盘
option: {
  color: ['red','green','blue'...]
}
* 局部调色盘
series: {
  color: ['pink', 'yellow'.....]
}
调色盘遵循就近原则
----------------------
线性渐变: series下边有一个itemStyle
itemStyle: {
  color: {
    type: 'linear',
    x: 0,
    y: 0,
    x2: 0,
    y2: 1,
    colorStops: [
      {
        offset:0, color: 'red'   // 0%处的颜色
      },
      {
        offset:1, color: 'blue'   // 100%处的颜色
      }
    ],
    globalCoord: false   // 缺省为false
  },
}

径向渐变:
itemStyle: {
  color: {
    type: 'radial',
    x: 0.5,
    y: 0.5,
    r: 0.5,
    colorStops: [
      {
        offset:0, color: 'red'   // 0%处的颜色
      },
      {
        offset:1, color: 'blue'   // 100%处的颜色
      }
    ],
    global: false   // 缺省为false
  },
}

4.3 样式

  • 直接样式: itemStyle, textStyle, lineStyle, areaStyle, label
例如饼图: data:[
  {  // 在series下
    value: 114534,
    name: 京东,
    itemStyle: {
      color: 'blue'
    },
    label: {
      color: 'green'
    }
  },
  {},
  {},
]
  • 高亮样式: 在itemStyle textStyle lineStyle areaStyle label外边包裹emphasis

4.4 图表自适应

Dom不给高度, 只给宽度

window.onresize = function() {
  myCharts.resize()   // 重置图表
}
或者
window.onresize = myCharts.resize

4.5 动画使用

  • 加载动画: ECharts已经内置了加载数据的动画,我们只需要在合适的实际显示或者隐藏即可
获取数据成功之前: 加载动画myCharts.showLoading()
获取数据成功之后: 隐藏动画myCharts.hideLoading()
  • 增量动画 : setOption(option) 新旧option是相互整合的关系
  • 动画配置 :
开始动画 animation: true,   // 在option= {之下配置}
动画时长 animationDuration: 7000
animationDuration:  function(arg) {
  console.log(arg)
  return 返回值,回调函数
}
缓动动画 animationEasing: 'bounceOut'   // linear
动画的阈值 animationThreshold: 8 单种形式的元素数量大于这个阈值时会关闭动画

4.6 交互API

  • 全局echarts对象是引入echarts.js文件之后就可以直接使用的,
echarts 常用4个API如下
init方法:
  初始化ECharts实例对象
  使用某一套主题
registerTheme方法:
  注册主题
registerMap方法:
  注册地图
connect方法: P37
  可以实现多图关联, 传入联动目标为ECharts实例对象, 支持数组
  echarts.connect([myCharts, myCharts2])
  
  保存图片的自动拼接
  刷新按钮
  重置按钮
  提示框联动, 图例选择, 数据范围修改等
  • echartslnstance对象: myCharts, 是通过echarts.init方法调用得到的对象
myCharts常用API
setOption方法: 
  设置或修改图表实例的配置项以及数据
  多次调用setOption方法(合并新旧配置, 实现增量动画)
resize方法:
  重新计算和绘制图表
  一般和window对象的resize事件结合使用
on\off 方法:
  绑定或者解绑事件函数
  鼠标事件: click,dblclick,mousedown,mousemove,mouseup...
  例如:在option下 myCharts.on('click', function(arg) {
-------------------------
ECharts事件: 
(也是通过myCharts调用的)
常见事件: legendselectchanged, datazoom, pieselectchanged, mapselectchanged...

dispatchAction方法:
触发某些行为
使用代码模拟用户的行为
myCharts.dispatchAction({
  type: 'highlight',   // 事件类型
  seriesIndex: 0,   // 图表索引
  dataIndex: 1   // 数据索引: 图表中哪一项高亮
})

clear方法: 
清空当前实例, 会移除实例中所有的组件和图表
清空之后可以再次调用setOption来重新渲染

dispose方法: 
销毁实例
不能再次调用setOption重新渲染

(二). Canvas

1.canvas概述

  • 长久以来,web上的动画都是flash. 比如动画公告,游戏等等. 基本上都是flash实现的.flash是有缺点的,比如需要安装Adobe Flash Player,漏洞多,重量比较大. 卡顿不流畅等等.
  • canvas是一个轻量级的画布,我们使用canvas进行JavaScript的编程,不需要增加额外的插件,性能也很好,不卡顿,在手机中也很流畅.
  • canvas是H5的一个标签
  • HTML5提出了一个新的canvas标签, 彻底颠覆了flash的主导地位.

1.1 canvas 基础

  • canvas的标签属性只有两个, width和height. 表示的是canvas画布的宽度和高度.
  • canvas的宽高不要用css的样式来设置, 如果用css样式设置会失真.
<canvas width="500" height="400">
    当前浏览器不支持,请升级浏览器
</canvas>

<script>
    // 得到canvas的画布
    var canvas = document.querySelector("#ttt")

    // 获取画布的上下文, 2d 和 3d
    // 所有的图像绘制都是通过ctx 属性或方法 进行设置的, 和canvas标签没有关系了
    var ctx = canvas.getContext("2d")

    // 设置颜色
    ctx.fillStyle = '#f99'
    // 绘制矩形
    ctx.fillRect(100,100,200,50)  // 水平是x轴,竖直是y轴,  参数(x,y,width,height)

</script>

1.2 canvas 像素化

  • 我们使用canvas绘制了一个图形,一旦绘制成功了,canvas就 像素化了他们. anvas没有能力从画布上再次得到这个图形, 也就说不能再修改已经在画布上的内容.这就是canvas比较轻量的原因.
  • flash重的原因之一就是它可以通过相应的api得到已经 上画布的内容,然后再次绘制
  • 如果我们想要让canvas图形移动,必须按照 清屏-->更新-->渲染 的逻辑进行编程. 总之就是重新再画一次

1.3 canvas的动画思想

  • 清屏-->更新-->渲染
  • canvas已经上画布的元素, 就被像素化了,所以不能通过style.left方法进行修改,而是必须重新绘制
  <script>
    // 得到canvas的画布
    var canvas = document.querySelector("#ttt")
    
    // 获取画布的上下文, 2d 和 3d
    // 所有的图像绘制都是通过ctx 属性或方法 进行设置的, 和canvas标签没有关系了
    var ctx = canvas.getContext("2d")
    
    // 设置颜色
    ctx.fillStyle = '#f99'
    // 绘制矩形
    var left = 100

    setInterval(() => {
      ctx.clearRect(0,0,600,600)
      left += 50
      ctx.fillRect(left,100,80,50)
    },300)
  </script>


1.4 面向对象思维 实现canvas动画

  • 因为canvas不能得到已经上屏的对象,所以我们要维持对像的状态. 在canvas动画中,我们都使用面向对象来进行编程,因为我们可以使用面向对象的方式来维持canvas需要的属性和状态.
  <script>
    // 得到canvas的画布
    var canvas = document.querySelector("#ttt")
    
    // 获取画布的上下文, 2d 和 3d
    // 所有的图像绘制都是通过ctx 属性或方法 进行设置的, 和canvas标签没有关系了
    var ctx = canvas.getContext("2d")
    
    // 构造函数(初始化)
    function Rect(x, y, w, h, color) {
      this.x = x
      this.y = y
      this.w = w
      this.h = h
      this.color = color
    }
    // 更新数据的方法
    Rect.prototype.update = function(x, y, w, h, color='#f99') {
      this.x += x
      this.y += y
      this.w += w
      this.h += h
      this.color =  color
    }
    // 渲染的方法
    Rect.prototype.render = function() {
      ctx.fillStyle = this.color
      ctx.fillRect(this.x, this.y, this.w, this.h)
    }
    
    let r1 = new Rect(0,0,40,50, '#00f')   // 实例化

    setInterval(() => {
      ctx.clearRect(0,0, canvas.width,canvas.height)
      r1.update(10,20, 0, 0)   // 更新数据
      r1.render()   // 渲染
    },100)   
  </script>

2. canvas的绘制功能

2.1 绘制矩形

  <script>
    // 得到canvas的画布
    var canvas = document.querySelector("#ttt")
    
    // 获取画布的上下文, 2d 和 3d
    // 所有的图像绘制都是通过ctx 属性或方法 进行设置的, 和canvas标签没有关系了
    var ctx = canvas.getContext("2d")
    
    // 设置颜色
    ctx.fillStyle = '#f99'
    ctx.fillRect(0,0,50,90)

    ctx.strokeStyle = '#0f0'
    ctx.strokeRect(100,100, 90, 90)
  </script>


2.2 绘制路径

  • 作用是为了设置一个多边形,或者是不规则的多边形
  • 路径都是闭合的,使用路径进行绘制的时候.
  • 需设置路径的起点, 绘制出路径,封闭路径, 填充
<script>
    // 得到canvas的画布
    var canvas = document.querySelector("#ttt")
    
    // 获取画布的上下文, 2d 和 3d
    // 所有的图像绘制都是通过ctx 属性或方法 进行设置的, 和canvas标签没有关系了
    var ctx = canvas.getContext("2d")
    
    // 创建一个路径
    ctx.beginPath()
    
    ctx.moveTo(100, 100)   // 提笔落点

    // 描述行进路径的各个点
    ctx.lineTo(200, 100)
    ctx.lineTo(300, 200)
    ctx.lineTo(300, 400)
    ctx.lineTo(200, 300)
    
    ctx.closePath()   // 封闭路径
    ctx.strokeStyle = "#cdf"   // 线的颜色
    ctx.stroke()   // 开始绘制
    
    ctx.fillStyle = "#f99"   // 填充色
    ctx.fill()   // 开始填充
</script>

  • fill: 我们在绘制路径的时候可以选择不关闭路径, 这个时候会实现自关闭现象

2.3 圆弧

  <script>
    // 得到canvas的画布
    var canvas = document.querySelector("#ttt")
    
    // 获取画布的上下文, 2d 和 3d
    // 所有的图像绘制都是通过ctx 属性或方法 进行设置的, 和canvas标签没有关系了
    var ctx = canvas.getContext("2d")
    
    // 创建一个路径
    ctx.beginPath()
    ctx.arc(200,200, 100, 0, Math.PI, false)   // (圆心坐标x, 圆心坐标y, 半径, 开始弧度, 终点弧度, false顺时针)
    
    ctx.strokeStyle = "#111"   // 线的颜色
    ctx.stroke()   // 开会绘制

    ctx.fillStyle = '#f89'
    ctx.fill()
  </script>

  • 小球案例1
<script>
    // 得到canvas的画布
    var canvas = document.querySelector("#ttt")
    
    // 获取画布的上下文, 2d 和 3d
    // 所有的图像绘制都是通过ctx 属性或方法 进行设置的, 和canvas标签没有关系了
    var ctx = canvas.getContext("2d")
    
    // 存放 小球的数组
    let ballArr = []

    // 0 球类
    function Ball(x, y, r) {
      this.x = x
      this.y = y
      this.r = r

      // 设置随机色
      this.color = getRandom()
      // 设置行进方向
      this.dx = parseInt(Math.random()*20) -10
      this.dy = parseInt(Math.random()*20) -10

      ballArr.push(this)   //将每个实例对象 放于数组中
    }

    // 1 随机颜色
    function getRandom() {
      let allType = "0,1,2,3,4,5,6,7,8,9,a,b,c,d,e,f"
      let allTypeArr = allType.split(",")
      let color = "#"
      for (let i=0; i<6; i++) {
        var random = parseInt(Math.random() * allTypeArr.length)
        color += allTypeArr[random]
      }

      return color
    }
    // 2 渲染函数
    Ball.prototype.render = function() {
      ctx.beginPath()
      ctx.arc(this.x, this.y, this.r, 0, Math.PI * 2, false)
      ctx.strokeStyle = "#f9e"
      ctx.stroke()
      ctx.fillStyle = this.color
      ctx.fill()
    }
    // 3 小球运动
    Ball.prototype.update = function() {
      this.x += this.dx
      this.y += this.dy
      this.r -= 1
      // 如果小球的r<0 , 从ballArr数组中删除
      if(this.r < 0) {
        this.remove()
      }
    }
    // 4. 删除小球 
    Ball.prototype.remove = function() {
      ballArr.forEach((item,i) => {
        if(ballArr[i] == this) {
          ballArr.splice(i, 1)
        }
      })
    }


    // 5 鼠标移动 创建小球
    canvas.addEventListener("mousemove", function(event) {
      new Ball(event.offsetX, event.offsetY, 30)   // 创建实例对象
    })
    // 6 调用渲染函数 和 运动函数
    setInterval(() => {
      ctx.clearRect(0, 0, canvas.width, canvas.height)
      ballArr.forEach((item, i) => {
        ballArr[i].update()
        if(ballArr[i]) ballArr[i].render()
      })
    }, 50)

    

  </script>
  • 小球案例2
<script>
    // 得到canvas的画布
    var canvas = document.querySelector("#ttt")
    
    // 获取画布的上下文, 2d 和 3d
    // 所有的图像绘制都是通过ctx 属性或方法 进行设置的, 和canvas标签没有关系了
    var ctx = canvas.getContext("2d")
    canvas.width = document.documentElement.clientWidth
    canvas.height = document.documentElement.clientHeight
    
    // 存放 小球的数组
    let ballArr = []
    // 初始化小球的个数
    let account = 20

    // 0 球类
    function Ball(x, y, r) {
      this.x = parseInt(Math.random()*canvas.width)
      this.y = parseInt(Math.random()*canvas.height)
      this.r = 30

      // 设置随机色
      this.color = getRandom()
      // 设置行进方向
      this.dx = parseInt(Math.random()*20) -10
      this.dy = parseInt(Math.random()*20) -10

      this.index = ballArr.length - 1
      ballArr.push(this)   //将每个实例对象 放于数组中
    }

    // 1 随机颜色
    function getRandom() {
      let allType = "0,1,2,3,4,5,6,7,8,9,a,b,c,d,e,f"
      let allTypeArr = allType.split(",")
      let color = "#"
      for (let i=0; i<6; i++) {
        var random = parseInt(Math.random() * allTypeArr.length)
        color += allTypeArr[random]
      }

      return color
    }
    // 2 渲染函数
    Ball.prototype.render = function() {
      ctx.beginPath()
      ctx.globalAlpha = 1   // 0~1之间
      // 画小球
      ctx.arc(this.x, this.y, this.r, 0, Math.PI * 2, false)
      // ctx.strokeStyle = "#f9e"
      // ctx.stroke()
      ctx.fillStyle = this.color
      ctx.fill()

      // 画线的逻辑
      ballArr.forEach((itme,i) => {
        if(Math.abs(ballArr[i].x - this.x) < 150 && Math.abs(ballArr[i].y - this.y) < 150 && this.index > i) {
          ctx.strokeStyle = "#000"
          ctx.beginPath()
          ctx.globalAlpha = 20/Math.sqrt(Math.pow(ballArr[i].x - this.x, 2) + Math.pow(ballArr[i].y - this.y, 2))
          ctx.moveTo(this.x, this.y)
          ctx.lineTo(ballArr[i].x, ballArr[i].y)
          ctx.closePath()
          ctx.stroke()
        }
      })
    }
    // 3 小球运动
    Ball.prototype.update = function() {
      this.x += this.dx
      this.y += this.dy
      if(this.x < this.r || this.x > canvas.width - this.r) {
        this.dx = -this.dx
      }
      if(this.y < this.r || this.y > canvas.height - this.r) {
        this.dy = -this.dy
      }
    }

    // 4 鼠标移动 创建小球
    for(let i = 0; i< account; i++) {
      new Ball()   // 创建实例对象
    }

    // 5 调用渲染函数 和 运动函数
    setInterval(() => {
      ctx.clearRect(0, 0, canvas.width, canvas.height)
      ballArr.forEach((item, i) => {
        ballArr[i].update()
        if(ballArr[i]) ballArr[i].render()
      })
    }, 50)
  </script>

2.4 线型

  • lineWidth 默认值是1
  • lineCap = type 设置末端的样式
    • "butt" 默认样式
    • "round" 半圆型
    • "square" 方型
  • lineJoin = type 设定线条与线条结合处的样式
    • bevel 连接处水平
    • round 圆弧型
    • miter 默认带尖
  • miterLimit = value 限制当两条香蕉是交接处最大长度.
  • setLineDash([14, 20]) 返回一个包含当前虚线样式, 长度为非负偶数的数组, 第一个参数是实线的长度, 第二个是实线的间隔
  • setLineDash([12,34,34,34,3,22]) 每两个是一组
  • ctx.lineDashOffset = 2 起始偏移量

2.5 样式细节

// 写文字
    ctx.font = "30px 宋体"
    ctx.fillText("你好, 我是宋体文字", 100, 100)   // 起始坐标
    ctx.textAlign = "left"   // 对齐方式
    
// 线型渐变
    let linear = ctx.createLinearGradient(0, 0, 200, 200)
    linear.addColorStop(0, "red")
    linear.addColorStop(1, "#99f")
    ctx.fillStyle = linear
    ctx.fillRect(100, 100, 200, 300)

// 径向渐变
let linear = ctx.createRadialGradient(95, 15, 15, 102, 20, 40)   // (x0,y0,半径0, x1,y1,半径1)
    linear.addColorStop(0, "red")
    linear.addColorStop(0.5, "#ff0")
    linear.addColorStop(1, "#99f")
    ctx.fillStyle = linear
    ctx.fillRect(10, 10, 800, 600)
    
    
// 阴影
ctx.shadowOffsetX = 1
ctx.shadowOffsetY = 2
ctx.shadowBlur = 10
ctx.shadowColor = "red"
ctx.font = "30px 宋体"
ctx.fillText("niasdh你好", 100, 100)

3.使用图片


  <script>
    // 得到canvas的画布
    var canvas = document.querySelector("#ttt")
    
    // 获取画布的上下文, 2d 和 3d
    // 所有的图像绘制都是通过ctx 属性或方法 进行设置的, 和canvas标签没有关系了
    var ctx = canvas.getContext("2d")
    canvas.width = document.documentElement.clientWidth
    canvas.height = document.documentElement.clientHeight
    
    // 创建image元素
    let image = new Image()
    // 用src设置图片的地址
    image.src = "images/2.jpg"
    image.onload = function() {
      ctx.drawImage(image, 10, 10, 500, 300, 50, 50, 50, 50)   // 起始位置和宽高, 后4个属性表示视口的位置和宽高
    }

  </script>

4.资源管理器

  <script>
    // 得到canvas的画布
    var canvas = document.querySelector("#ttt")
    
    // 获取画布的上下文, 2d 和 3d
    // 所有的图像绘制都是通过ctx 属性或方法 进行设置的, 和canvas标签没有关系了
    var ctx = canvas.getContext("2d")
    canvas.width = document.documentElement.clientWidth
    canvas.height = document.documentElement.clientHeight
    
    // 游戏类
    function Game() {
      this.dom = document.querySelector("canvas")
      this.ctx = this.dom.getContext("2d")
      
      // 添加属性, 保存需要的图片地址
      this.R = {
        "aaa": "./images/1.jpg",
        "bbb":  "./images/2.jpg"
      }
      
      // 获取资源的总数
      let count = 0
      let allAmount = Object.keys(this.R).length
      // 遍历R对象
      for(k in this.R) {
        // 备份每张图 地址
        let src = this.R[k]
        this.R[k] = new Image()
        this.R[k].src = src

        let that = this
        that.R[k].onLoad =function() {
          count ++ 
          that.ctx.clearRect(0,0,600,400)
          that.ctx.font = "16px Arial"
          that.ctx.fillText("图片已经加载: " + count+ "/" + allAmount, 10, 50)
          // 调用开始游戏
          if(count == allAmount) {
            self.start()
          }
        }
      }

    }

    Game.prototype.start = function() {
      
      this.ctx.drawImage(this.R["1"], 200, 200, 100, 80)
    }
  </script>


5.变形 ???

  • canvas是可以变形的, 但是变形的不是元素,而是ctx,ctx就是整个画布的渲染区域,整个画布在变形. 我们需要在画布变形前, 进行保存和恢复

  • save() 保存: 某个状态, 相当于打了一个tag,, 多个save就保存了多个save

  • restore() 恢复: 上一个状态, 多次调用,就逐一的返回上一个状态

  • ctx.translate(150, 150)

  • ctx.rotate(45deg)

  • ctx.scale(1.5, 1.5)

  • ctx.transform() 上边三个的结合

6.合成 ???

  • 我们可以通过globalCompositeOperation来设置压盖顺序

(三) D3 SVG

1. 基础

  • d3.select('#aaa')
  • d3.selectAll('.yyy')
  • d3.selectAll('#aaa .yyy')

2. svg的属性

  • 常见属性
    • id, class 可以用.attr设置它们
    • x, y , cx, cy (后两个是圆心坐标)
    • fill, stroke
    • height, width, r
    • transform---> translate, rotate, scale
  • svg 的属性非常多,且属性的取值范围和类型各不相同
  • svg查阅资料
  • Y轴垂直向下, X轴水平向右
  • 设置属性: d3.select('#aaa').attr('属性名', '属性值')
  • 添加属性: d3.select('#aaa').append('rect').attr('属性名', '属性值')
  • 链式添加属性: d3.select('#aaa').append('rect').attr('属性名', '属性值').attr('属性名2', '属性值2').append('rect3').attr('属性名', '属性值')
  • 删除元素: d3.select('#aaa').remove()

3. 比例尺

  • 比例尺用于把实际的数据空间映射到屏幕的空间
  • 比例尺非常重要, 会经常同时传给坐标轴与数据
(1)scaleLinear()
const xScale = d3.scaleLinear().domain([min_d, max_d]).range([min, max])

const xScale = d3.scaleLinear().domain([0, d3.max(data, datum = > datum.value )]).range([0, innerWidth])


(2)scaleBand()
const yScale = d3.scaleBand().domain(list).range([min, max]).padding(p)

实例: 
const yScale = d3.scaleBand().domain(data.map(datum => datum.name)).range([min, max]).padding(0.1)


domain的参数是真实数据的取值范围, range的参数是比例缩放之后 数据取值范围

4. 柱形图案例

(1)
const data = []

const svg = d3.select('#mainsvg')
const width = +svg.attr('width')
const height = svg.attr('height')
const margin = {top:60, right: 30, bottom:60, left: 150}
const innerWidth = width - margin.left - margin.right
const innerHeight = width - margin.top - margin.bottom

const xScale = d3.scaleLinear().domain([0, d3.max(data, d = d.value)]).range([0, innerWidth])

const yScale = d3.scaleBand().domain(data.map(d => d.name)).range([0, innerHeight]).padding(0.1)

cosnt g = svg.append('g').attr('id', 'maingroup')
.attr('transform', `translate(${margin.left}, ${margin.top})`)

cosnt yAxis = d3.axisLeft(yScale)   // y轴, 只是坐标轴的定义,并不会渲染出

g.append('g').call(yAxis)   // 真正的把坐标轴 渲染出来



const xAxis = d3.axisBottom(xScale)   // x轴

g.append('g').call(xAxis).attr('transform', `translate(0, ${innerHeight})`)


// 画每个矩形
data.forEach(d => {
    g.append('rect')
    .attr('width', xScale(d.value))
    .attr('height', yScale.bandwidth())
    .attr('fill', 'green')
    .attr('y', yScale(d.name))
})

// 坐标轴文字大小
d3.selectAll('.tick text').attr('font-size', '2em)

g.append('text').text('Members of CSCG')
.attr('font-size', '3em').attr('transform', `translate(${innerWidth / 2}, 0)`).attr('text-anchor', 'middle')

5. Data-Join

  • 本质上是将数据与图元进行绑定
    • 每个国家的人数绑定到矩形的长度
    • 疫情感染的人数比例绑定到圆的半径
(1)
// attr可以直接设置图元属性值, 也可以通过函数设置图元的属性
d3.selectAll('rect').data(data3).attr('width', d => xScale(d.value))
默认data()函数使用index索引的,

(2)
Key:
data(data, keyFunction)
    keyFunction的返回值通常是一个字符串
    keyFunction的定义根据数据, 比如keyFunction= d => d.name
    d3.selectAll('rect').data(data3, d=>d.name).attr('width', d => xScale(d.value))
    
(3)
Enter Update Exit
    Update: 图元和数据条目相同, 之前的介绍均为单存的update
    Enter: 数据的条目多于图元甚至没有图元, 常用于第一次绑定数据
    Exit: 数据的条目少于图元甚至没有数据, 常用于结束可视化

(4)Enter
* 有数据没图元
* D3.js会自动搞清楚哪些数据是新增的
* 根据新增的数据生成相应的图元
* 生成图元的占位, 占位的内容需要编程者自行添加 (append)
* const p = maingroup.selectAll('.sss').data(data3).enter().append('').attr(......)
* enter本质上生成指向父节点的指针, 而append操作相当于在父节点后添加指针数量的图元并将其与多出的数据绑定

(5) Update
* 有图元有数据
* const p = maingroup.selectAll('.datacurve').data(data).attr(....).attr(....)
* Update作为实际可视化任务最常用的状态, 经常被单独封装成一个函数
* updateSelection.merge(enterSelection).attr(......).attr(......)
    * 将两个selection合并到一起
    * enterSelection在与updateSelectioin merge 之前要至少已经调用了append(...)语句添加好图元

(6) 让数据动起来
* Update经常与D3.js的动画一起使用
* .transition().duration()

* d3.selectAll('rect').data(data3, d => d.name)
.transition().duration(3000).attr('width', d => xScale(d.value))

* .transition经过调用后, 后续的链式调用会变成数值上的渐变, 渐变的时间由duration设定

* 插值的方式由.ease(......) 设定

(7) Exit
* 有图元没数据
* D3.js会自动搞清楚哪些图元是不绑定数据的
* const p = maingroup.selectAll('.class').data(data3).exit().remove()

(8) .data(......).join(......)
* 默认enter和update的执行形式相同
* 默认exit是删除(remove节点)
* 默认data-join形式简洁但不灵活
    * 必须要设置enter数据的初始化图元属性,update会每次重新设置初始值, 从而导致动画出现奇怪的效果
    * 定制性差
    * ???
    

6. 数据的读取

* d3.csv('path/to/data.csv').then( data => {......})
.csv函数的返回值是一个Promise对象
* d3.csv(......) 会正常向服务器请求数据, 在请求并处理好之后,将结果扔给.then()

7. path

  • path元素是svg基本形状中最强大的一个, 它不仅能创建其他基本形状, 还能创建更多其他形状. 你可以用path元素绘制 矩形(直角矩形或圆角矩形) 圆形 椭圆 折线形 多边形, 以及一些其他的形状, 例如贝塞尔曲线, 2次曲线等曲线
  • path元素的形状是通过属性d来定义的, 属性d的值是一个"命令 + 参数"
  • path作为svg提供的标签之一, 是实现众多可视化方案的基础
(1)
path的各个属性:
    d
    fill: 填充颜色
    stroke: 描边颜色
    stroke-width: 描边宽度
    trnsform="translate(x,y)" 
    加了描边之后需要平移(x=stroke-width / 2, y=stroke-width / 2)
其中属性d: 
    * M = moveto(M X,Y) : 将笔画移动到指定的坐标位置
    * L = lineto(L X,Y) : 画直线到指定的坐标位置
    * H = horizontal lineto(H X): 画水平线到指定的X坐标位置 
    * V = vertical lineto(V Y): 画垂直线到指定的Y坐标位置
        * C = curveto(C X1, Y1, X2, Y2, ENDX, ENDY): 三次贝赛曲线
    * S = smooth curveto(S X2, Y2, ENDX, ENDY): 平滑曲线
    * Q = quadratic Belzier curve(Q X,Y,ENDX,ENDY): 二次贝赛曲线
    * T = smooth quadratic Belzier curvuto(T ENDX, ENDY): 映射
    * A = elliptical Arc(A RX, RY, XROTATION, FLAG1, FLAG2, X, Y): 弧线
    * Z = closepath(): 关闭路径
    * 以上所有命令均允许小写字母, 大写字母表示决定定位, 小写表示相对定位

(2) path的手动配置
<path d="M 0 0 H 90 V 90 H 10 L 10 10">
    
(3) path的自动 生成器
d3.line(...).x(......).y(......)   // 用于生成折线
d3.geoPath().projection()  // 用于地图
d3.area()   // 用于主题河流
d3.arc(......).innerRadius(......).outerRadius(......)   // 用于饼图
d3.lineRadial().angle(......).radius(......)   // 极坐标系版本的d3.line(......)

8. interaction地图 ???

9. Stack 堆叠 ???

  • 堆叠柱状图

10. 树与图

  • 层级结构的可视化
d3的层级数据预处理: 
* d3.hierarchy
* 保持数据的原始结构, 并将输入 层级数据转换成 D3中的hierarchy对象, 同时引入:
    height   // 不是层级递减
    depth   // 深度
    children   // 原始结构
    parent   // 父级
    data   // 原始数据的映射
* d3.hierarchy可作为一个'中间结果', 继续输入到更多d3提供的数据预处理接口

(四) threeJS

基础

1.1threejs带来的好处

  • 创建三维几何图形
  • 创建虚拟现实VR和增强现实AR场景
  • 在三维场景下创建动画和移动物体
  • 为物体添加纹理和材质
  • 使用各种光源来装饰场景
  • 加载三维模型软件所创建的物体
  • 为三维场景添加高级的后期处理效果
  • 使用自定义的着色器
  • 创建点云( 即粒子系统 )

1.2 three.js 程序结构

6.png

案例: 

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <script text="module" charset="UTF-8" src="./js/three.js" > </script>
  <script text="module" charset="UTF-8" src="./js/OrbitControls.js" > </script>


  <style>
    body{margin: 0; overflow: hidden;}
  </style>
</head>

<body>
  <div id="webgl-output"></div>
  <script>
    function init() {
      // 一.创建场景----------------------------
      var scene = new THREE.Scene()
      // 1 坐标轴
      // var ax = new THREE.AxisHelper(20)   // 坐标轴 (老版本)
      var ax = new THREE.AxesHelper(20)   // 坐标轴 
      scene.add(ax)   // 将坐标轴添加到场景中
      // 2 网络模型
      var geo1 = new THREE.PlaneGeometry(60, 20)   // 创建几何体 : 地板
      var geo2 = new THREE.BoxGeometry(4, 4, 4)   // 创建几何体: 立方体
      var mat1 = new THREE.MeshBasicMaterial({color: 0xccccff})   // 创建材质
      var mat2 = new THREE.MeshLambertMaterial({color: 0xff9999})   // 创建材质
      var plane = new THREE.Mesh(geo1, mat1)   // 创建地面模型
      var cube = new THREE.Mesh(geo2, mat2)   // 创建立方体模型
      plane.rotation.x = -0.5 * Math.PI
      plane.position.x = 15;
      plane.position.y = 0;
      plane.position.z = 0;
      plane.castShadow = true   // 设置阴影
      plane.receiveShadow = true   // 接收阴影
      cube.position.x = 20;
      cube.position.y = 10;
      cube.position.z = 10;
      cube.castShadow = true;   // 是否渲染到阴影贴图当中
      scene.add(plane)
      scene.add(cube)

      // 3. 光照
      var light9 = new THREE.SpotLight(0xffffff)   // 创建灯光
      light9.position.set(12,30,30)   // 灯光位置
      light9.castShadow = true   // 阴影
      light9.angle = Math.PI/10
      light9.shadow.penumbra = 0.05
      light9.shadow.mapSize.width = 1024
      light9.shadow.mapSize.height = 1024
      scene.add(light9)

      // 二.设置摄像机------------------------
      var camera = new THREE.PerspectiveCamera(45, window.innerWidth/window.innerHeight, 0.1, 2000)
      // 1.位置
      camera.position.x = 30
      camera.position.y = 40
      camera.position.z = 30
      // 2.实现方向
      camera.lookAt(scene.position)    // 看向场景中心 0,0,0
      // 3.投影方式


      // 三. 创建渲染器-----------------------------
      var rd = new THREE.WebGLRenderer();
      rd.setClearColor(new THREE.Color(0xEEEEEE))   // 渲染器 初始背景颜色
      rd.setSize(window.innerWidth, window.innerHeight)
      rd.shadowMapEnabled = true  // 设置渲染物体阴影
      document.getElementById('webgl-output').appendChild(rd.domElement)
      
      
      // function myRender() {
      //   rd.render(scene,camera)
      //   cube.rotateY(0.01)   // 设置渲染速度
      // }
      // // 旋转: 方式一
      // setInterval(myRender, 16)

      // // 旋转方式二
      // window.requestAnimationFrame(myRender)

    }

    // 四. 创建控制器
    var ctl = new THREE.OrbitControls(camera, rd)
    ctl.addEventListener('change', () => {
      rd.render(scene,camera)
    })

    window.onload = init
  </script>
</body>