Vue项目实战
- 图表渲染
- 安装echarts
- 图表渲染器(图表组件)
- 图表举例:
- 创建 ChartsRenderer.vue
- 创建 ChartsDataTransformer.ts
- 基于 zrender 开发可视化物料
- 安装 zrender
- 画一个矩形
- 画一个柱状图
- 基于svg开发可视化物料
- svg小示例
- 使用d3进行图表渲染
- 安装d3
- 基本使用
- 地图绘制
- 本地持久化
- 拓展知识
图表渲染
安装echarts
package.json:
"dependencies": {"vue": "3.4.26","vue-echarts":"6.7.1","echarts":"5.5.0"},
图表渲染器(图表组件)
一般负责:
- 整体图表的基础层数据协议设计
schema:{type:"line", //折线图data:{labels:[],datasets:[]}
}
- 基于数据协议的图表渲染逻辑
渲染逻辑:通用渲染逻辑、特殊渲染逻辑
BarRenderer.vue、LineRenderer.vue,统一在ChartsRenderer.vue中进行分发渲染(在这一层将不同的图表类型差异磨平) - 图表的事件处理
图表举例:
创建 ChartsRenderer.vue
- components
- ChartsRenderer.vue
<template><!-- 一个的写法 --><div class="charts-container" ref="container">charts Renderer</div><!-- 多个的写法 --><!-- <div :ref="(n)=>maps.n=n">charts Renderer1</div><div :ref="(b)=>maps.b=b">charts Renderer2</div><div :ref="(r)=>maps.r=r">charts Renderer3</div> -->
</template><script setup lang="ts">
import {use,init} from 'echarts'
import {BarChart,LineChart} from 'echarts/charts'
import {LegendComponent,TooltipComponent} from 'echarts/components';
import {onMounted,ref} from 'vue'use([BarChart,LineChart,LegendComponent,TooltipComponent])const container=ref<HTMLDivElement|null>(null)
// 多个的写法
// const maps={}onMounted(()=>{if(!container.value) return;const chartInstance= init(container.value)console.log("container",container);// 多个的写法// console.log("maps",maps);chartInstance.setOption({title:{text:"Echarts 入门示例"},tooltip:{},legend:{data:['销量']},xAxis:{data:['衬衫','羊毛衫','雪纺衫','裤子','高跟鞋','袜子']},yAxis:{},series:[{name:"销量",type:"bar",data:[5,20,36,10,10,20]}]})
})
</script><style scoped>
.charts-container{width: 500px;height: 500px;border: 1px solid #ccc;border-radius: 4px;padding: 10px;box-sizing: border-box;
}
</style>
App.vue
<script setup lang="ts">
import ChartsRenderer from './components/ChartsRenderer.vue'
</script>
<template><ChartsRenderer></ChartsRenderer>
</template>
项目中:
import {use,init,graphic} from 'echarts'
.......
chartInstance.setOption({color: ['#80FFA5', '#00DDFF', '#37A2FF', '#FF0087', '#FFBF00'],title: {text: '渐变堆叠面积图'},tooltip: {trigger: 'axis',axisPointer: {type: 'cross',label: {backgroundColor: '#6a7985'}}},legend: {data: ['Line 1', 'Line 2', 'Line 3', 'Line 4', 'Line 5']},toolbox: {feature: {saveAsImage: {}}},grid: {left: '3%',right: '4%',bottom: '3%',containLabel: true},xAxis: [{type: 'category',boundaryGap: false,data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']}],yAxis: [{type: 'value'}],series: [{name: 'Line 1',type: 'line',stack: 'Total',smooth: true,lineStyle: {width: 0},showSymbol: false,areaStyle: {opacity: 0.8,color: new graphic.LinearGradient(0, 0, 0, 1, [{offset: 0,color: 'rgb(128, 255, 165)'},{offset: 1,color: 'rgb(1, 191, 236)'}])},emphasis: {focus: 'series'},data: [140, 232, 101, 264, 90, 340, 250]},{name: 'Line 2',type: 'line',stack: 'Total',smooth: true,lineStyle: {width: 0},showSymbol: false,areaStyle: {opacity: 0.8,color: new graphic.LinearGradient(0, 0, 0, 1, [{offset: 0,color: 'rgb(0, 221, 255)'},{offset: 1,color: 'rgb(77, 119, 255)'}])},emphasis: {focus: 'series'},data: [120, 282, 111, 234, 220, 340, 310]},{name: 'Line 3',type: 'line',stack: 'Total',smooth: true,lineStyle: {width: 0},showSymbol: false,areaStyle: {opacity: 0.8,color: new graphic.LinearGradient(0, 0, 0, 1, [{offset: 0,color: 'rgb(55, 162, 255)'},{offset: 1,color: 'rgb(116, 21, 219)'}])},emphasis: {focus: 'series'},data: [320, 132, 201, 334, 190, 130, 220]},{name: 'Line 4',type: 'line',stack: 'Total',smooth: true,lineStyle: {width: 0},showSymbol: false,areaStyle: {opacity: 0.8,color: new graphic.LinearGradient(0, 0, 0, 1, [{offset: 0,color: 'rgb(255, 0, 135)'},{offset: 1,color: 'rgb(135, 0, 157)'}])},emphasis: {focus: 'series'},data: [220, 402, 231, 134, 190, 230, 120]},{name: 'Line 5',type: 'line',stack: 'Total',smooth: true,lineStyle: {width: 0},showSymbol: false,label: {show: true,position: 'top'},areaStyle: {opacity: 0.8,color: new graphic.LinearGradient(0, 0, 0, 1, [{offset: 0,color: 'rgb(255, 191, 0)'},{offset: 1,color: 'rgb(224, 62, 76)'}])},emphasis: {focus: 'series'},data: [220, 302, 181, 234, 210, 290, 150]}]
})
创建 ChartsDataTransformer.ts
- components
- ChartsRenderer.vue
- ChartsDataTransformer.ts
ChartsDataTransformer.ts:
import {graphic} from 'echarts/core';const chartsTypes=['bar','line'] as const;type ChartType=(typeof chartsTypes)[number]interface ChartsProtocol{type:ChartType, //图类型config:{}, //图表配置data:{labels:[],datasets:[],legends:[],xAxis:[],yAxis:[]},events:{onClick:Function,onHover:Function}
}// 抹平数据差异
export function getChartsOption(data:ChartsProtocol){switch(data.type){case "bar":return getBarChartsOptions(data);case "line":return getLineChartsOptions(data);default:return {} }
}function getBarChartsOptions(data:ChartsProtocol){// 处理柱状图数据data.data;return {color: ['#80FFA5', '#00DDFF', '#37A2FF', '#FF0087', '#FFBF00'],title: {text: '渐变堆叠面积图'},tooltip: {trigger: 'axis',axisPointer: {type: 'cross',label: {backgroundColor: '#6a7985'}}},legend: {data: ['Line 1', 'Line 2', 'Line 3', 'Line 4', 'Line 5']},toolbox: {feature: {saveAsImage: {}}},grid: {left: '3%',right: '4%',bottom: '3%',containLabel: true},xAxis: [{type: 'category',boundaryGap: false,data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']}],yAxis: [{type: 'value'}],series: [{name: 'Line 3',type: 'bar',stack: 'Total',smooth: true,lineStyle: {width: 0},showSymbol: false,areaStyle: {opacity: 0.8,color: new graphic.LinearGradient(0, 0, 0, 1, [{offset: 0,color: 'rgb(55, 162, 255)'},{offset: 1,color: 'rgb(116, 21, 219)'}])},emphasis: {focus: 'series'},data: [320, 132, 201, 334, 190, 130, 220]},]}/* return{chart:{type:"bar",height:350,},playOptions:{bar:{horizontal:true}},xAxis:{categories:["Monday","Tuesday","Wednesday","Thursday","Friday","Saturday","Sunday"]}} */
}function getLineChartsOptions(data:ChartsProtocol){// 处理折线图data.datareturn {color: ['#80FFA5', '#00DDFF', '#37A2FF', '#FF0087', '#FFBF00'],title: {text: '渐变堆叠面积图'},tooltip: {trigger: 'axis',axisPointer: {type: 'cross',label: {backgroundColor: '#6a7985'}}},legend: {data: ['Line 1', 'Line 2', 'Line 3', 'Line 4', 'Line 5']},toolbox: {feature: {saveAsImage: {}}},grid: {left: '3%',right: '4%',bottom: '3%',containLabel: true},xAxis: [{type: 'category',boundaryGap: false,data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']}],yAxis: [{type: 'value'}],series: [{name: 'Line 1',type: 'line',stack: 'Total',smooth: true,lineStyle: {width: 0},showSymbol: false,areaStyle: {opacity: 0.8,color: new graphic.LinearGradient(0, 0, 0, 1, [{offset: 0,color: 'rgb(128, 255, 165)'},{offset: 1,color: 'rgb(1, 191, 236)'}])},emphasis: {focus: 'series'},data: [140, 232, 101, 264, 90, 340, 250]},{name: 'Line 2',type: 'line',stack: 'Total',smooth: true,lineStyle: {width: 0},showSymbol: false,areaStyle: {opacity: 0.8,color: new graphic.LinearGradient(0, 0, 0, 1, [{offset: 0,color: 'rgb(0, 221, 255)'},{offset: 1,color: 'rgb(77, 119, 255)'}])},emphasis: {focus: 'series'},data: [120, 282, 111, 234, 220, 340, 310]},{name: 'Line 3',type: 'line',stack: 'Total',smooth: true,lineStyle: {width: 0},showSymbol: false,areaStyle: {opacity: 0.8,color: new graphic.LinearGradient(0, 0, 0, 1, [{offset: 0,color: 'rgb(55, 162, 255)'},{offset: 1,color: 'rgb(116, 21, 219)'}])},emphasis: {focus: 'series'},data: [320, 132, 201, 334, 190, 130, 220]},{name: 'Line 4',type: 'line',stack: 'Total',smooth: true,lineStyle: {width: 0},showSymbol: false,areaStyle: {opacity: 0.8,color: new graphic.LinearGradient(0, 0, 0, 1, [{offset: 0,color: 'rgb(255, 0, 135)'},{offset: 1,color: 'rgb(135, 0, 157)'}])},emphasis: {focus: 'series'},data: [220, 402, 231, 134, 190, 230, 120]},{name: 'Line 5',type: 'line',stack: 'Total',smooth: true,lineStyle: {width: 0},showSymbol: false,label: {show: true,position: 'top'},areaStyle: {opacity: 0.8,color: new graphic.LinearGradient(0, 0, 0, 1, [{offset: 0,color: 'rgb(255, 191, 0)'},{offset: 1,color: 'rgb(224, 62, 76)'}])},emphasis: {focus: 'series'},data: [220, 302, 181, 234, 210, 290, 150]}]}
}
ChartsRenderer.vue:
<template><!-- 一个的写法 --><div class="charts-container" ref="container">charts Renderer</div><!-- 多个的写法 --><!-- <div :ref="(n)=>maps.n=n">charts Renderer1</div><div :ref="(b)=>maps.b=b">charts Renderer2</div><div :ref="(r)=>maps.r=r">charts Renderer3</div> -->
</template><script setup lang="ts">
import {use,init} from 'echarts'
import {BarChart,LineChart} from 'echarts/charts'
import {LegendComponent,TooltipComponent} from 'echarts/components';
import {onMounted,ref} from 'vue'
import {getChartsOption} from './ChartsDataTransformer'
import {CanvasRenderer} from 'echarts/renderers';use([BarChart,LineChart,LegendComponent,TooltipComponent,CanvasRenderer])const container=ref<HTMLDivElement|null>(null)
// 多个的写法
// const maps={}onMounted(()=>{if(!container.value) return;const chartInstance= init(container.value)console.log("container",container);// 多个的写法// console.log("maps",maps);chartInstance.setOption(getChartsOption({type:"line",data:{labels:["Mon","Tue","Wed","Thu","Fri","Sat","Sun"],datasets:[{name:"A",data:[820,932,901,934,435,1234,1500]},{name:"B",data:[320,256,401,456,860,390,470]}]}}))
})
</script><style scoped>
.charts-container{width: 1000px;height: 500px;border: 1px solid #ccc;border-radius: 4px;padding: 10px;box-sizing: border-box;
}
</style>
基于 zrender 开发可视化物料
安装 zrender
package.json:
"dependencies": {..."zrender":"5.5.0"},
pnpm i
- src
- components
- CustomCharts
- Rectangle.vue
- CustomCharts
- components
画一个矩形
Rectangle.vue
<template><div class="container" ref="container"></div>
</template><script setup lang="ts">
import {onMounted,ref} from 'vue';
import {init,Rect} from 'zrender';const container=ref<HTMLDivElement|null>(null)onMounted(()=>{// 我们使用zrender创建的一个场景const zr=init(container.value);zr.add(new Rect({shape:{x:10,y:10,width:100,height:100},style:{fill:"red"}}))
})
</script><style scoped>
.container{width:500px;height:500px;border: 1px solid #ccc;border-radius: 4px;padding: 10px;box-sizing:border-box;
}
</style>
App.vue:
<script setup lang="ts">
// import HelloWorld from './components/HelloWorld.vue'
// import ChartsRenderer from './components/ChartsRenderer.vue'
import Rectangle from './components/CustomCharts/Rectangle.vue'
</script><template><!-- <div> --><!-- <a href="https://vitejs.dev" target="_blank"><img src="/vite.svg" class="logo" alt="Vite logo" /></a><a href="https://vuejs.org/" target="_blank"><img src="./assets/vue.svg" class="logo vue" alt="Vue logo" /></a></div><HelloWorld msg="Vite + Vue" /> --><!-- <ChartsRenderer></ChartsRenderer> --><Rectangle></Rectangle>
</template><style scoped>
.logo {height: 6em;padding: 1.5em;will-change: filter;transition: filter 300ms;
}
.logo:hover {filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.vue:hover {filter: drop-shadow(0 0 2em #42b883aa);
}
</style>
画一个柱状图
Rectangle.vue
<template><div class="container" ref="container"></div>
</template><script setup lang="ts">
import {onMounted,ref} from 'vue';
import {init,Rect} from 'zrender';const container=ref<HTMLDivElement|null>(null)onMounted(()=>{// 我们使用zrender创建的一个场景const zr=init(container.value);const ticks=10for(let i=0;i<ticks;i++){const height=Math.random()*100+20const barWidth=20const barGap=10const randomColor=`rgb(${Math.random()*255},${Math.random()*255},${Math.random()*255})`zr.add(new Rect({shape:{x:barWidth+i*(barWidth+barGap),y:500-height,width:barWidth,height},style:{fill:randomColor}}))}/* zr.add(new Rect({shape:{x:10,y:10,width:100,height:100},style:{fill:"red"}})) */
})
</script><style scoped>
.container{width:500px;height:500px;border: 1px solid #ccc;border-radius: 4px;padding: 10px;box-sizing:border-box;
}
</style>
拓展:
zrender学习
echarts底层封装是用zrender来做的
可视化:知道echarts每个图表类型的折线,面积怎么绘制出来的;如果要深入学习可视化绘制的话,推荐一些比较复杂的图表去画一画,比如,和弦图、旭日图
数据库关系图用vue-flow来画
如果用的React,那么就用React-flow
系统的学习绘制,可以学习d3
echarts和d3的区别: echarts是更简单的图表框架,是可以拿来就用的,配置也比较简单;但是d3是什么都可以,d3是数据驱动的可视化库,能够基于数据封装可视化图表,基于svg绘制的。d3相当于jquery,语法和jQuery很像。
可视化面试一般会问什么问题?
问图表缩放问题,图表适配问题,问svg和canvas的区别,他们的优势和缺点等
基于svg开发可视化物料
svg小示例
- src
- components
- SvgCharts
- SvgBaseRenderer.vue
- SvgCharts
- components
App.vue:
<script setup lang="ts">
// import HelloWorld from './components/HelloWorld.vue'
// import ChartsRenderer from './components/ChartsRenderer.vue'
// import Rectangle from './components/CustomCharts/Rectangle.vue'
import SvgBaseRenderer from './components/SvgCharts/SvgBaseRenderer.vue'
</script><template><!-- <div> --><!-- <a href="https://vitejs.dev" target="_blank"><img src="/vite.svg" class="logo" alt="Vite logo" /></a><a href="https://vuejs.org/" target="_blank"><img src="./assets/vue.svg" class="logo vue" alt="Vue logo" /></a></div><HelloWorld msg="Vite + Vue" /> --><!-- <ChartsRenderer></ChartsRenderer> --><!-- <Rectangle></Rectangle> --><SvgBaseRenderer></SvgBaseRenderer>
</template><style scoped>
.logo {height: 6em;padding: 1.5em;will-change: filter;transition: filter 300ms;
}
.logo:hover {filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.vue:hover {filter: drop-shadow(0 0 2em #42b883aa);
}
</style>
SvgBaseRenderer.vue
<template><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 240 240" width="240"><text><tspan x="0" dy="1.2em">Hello SVG!</tspan></text></svg>
</template><script>
export default {setup(){}}
</script><style scoped></style>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 240 240" width="240"><!-- <text><tspan x="0" dy="1.2em">Hello SVG!</tspan></text> --><path d="m 0 12 a 20 20 90 0 0 -20 20 h 111 h 22 h 100 v -20" stroke="pink" fill="yellow"></path></svg>
svg editor辅助去学习svg
svg文档看w3c内容
可以在编辑器中画,然后复制路径过来:
<template><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" width="450"><!-- <text><tspan x="0" dy="1.2em">Hello SVG!</tspan></text> --><!-- <path d="m 0 12 a 20 20 90 0 0 -20 20 h 111 h 22 h 100 v -20" stroke="pink" fill="yellow"></path> --><path d="M 4 8 L 10 1 L 13 0 L 12 3 L 5 9 C 6 10 6 11 7 10 C 7 11 8 12 7 12 A 1.42 1.42 0 0 1 6 13 A 5 5 0 0 0 4 10 Q 3.5 9.9 3.5 10.5 T 2 11.8 T 1.2 11 T 2.5 9.5 T 3 9 A 5 5 90 0 0 0 7 A 1.42 1.42 0 0 1 1 6 C 1 5 2 6 3 6 C 2 7 3 7 4 8 M 10 1 L 9 4 L 12 3 L 10.2 2.8 L 10 1"fill="skyblue" stroke="black"></path></svg>
</template>
原生svg画图表:
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200" width="450"><!-- group --><g><path d="M 4 8 L 10 1 L 13 0 L 12 3 L 5 9 C 6 10 6 11 7 10 C 7 11 8 12 7 12 A 1.42 1.42 0 0 1 6 13 A 5 5 0 0 0 4 10 Q 3.5 9.9 3.5 10.5 T 2 11.8 T 1.2 11 T 2.5 9.5 T 3 9 A 5 5 90 0 0 0 7 A 1.42 1.42 0 0 1 1 6 C 1 5 2 6 3 6 C 2 7 3 7 4 8 M 10 1 L 9 4 L 12 3 L 10.2 2.8 L 10 1"fill="skyblue" stroke="black"></path></g><g><!-- 坐标 --><!-- 坐标轴 --><line x1="0" y1="120" x2="240" y2="120" stroke="black"></line><!-- 坐标刻度 --><g><g><line x1="0" y1="120" x2="240" y2="120" stroke="black"></line><line x1="0" y1="120" x2="240" y2="120" stroke="black"></line><line x1="0" y1="120" x2="240" y2="120" stroke="black"></line><line x1="0" y1="120" x2="240" y2="120" stroke="black"></line></g><g><!-- 坐标刻度值 --></g></g></g><!-- 纵坐标 --><g><line x1="0" y1="0" x2="0" y2="240" stroke="black"></line><!-- 坐标刻度 --><g><g><!-- 刻度 --></g><g><!-- 坐标刻度值 --></g></g></g>
</svg>
使用d3进行图表渲染
安装d3
package.json:
"dependencies": {..."d3":"7.9.0"},
"devDependencies": { 类型文件"@types/d3":"7.4.3",...
}
pnpm i
基本使用
- src
- components
- SvgCharts
- SvgMapRenderer.vue
- SvgCharts
- components
SvgMapRenderer.vue;
<template><div ref="container"></div>
</template><script setup lang="ts">
import * as d3 from 'd3';
import {onMounted, ref} from 'vue'const container = ref<HTMLDivElement|null>(null)
const randomColor = ()=>{return `rgb(${Math.round(Math.random()*255)},${Math.round(Math.random()*255)},${Math.round(Math.random()*255)})`
}onMounted(() => {if(!container.value) return;const svg = d3.select(container.value).append("svg").attr("width",960).attr("height",500).attr("viewBox","0 0 960 500")// 数据驱动 - 核心svg.selectAll("circle")// 数据绑定.data([32,57,112,293]).join("circle").attr("cx",(d,i) => i * 100 + 30).attr("cy",(d)=>300-Math.sqrt(d)).attr("r",(d)=>Math.sqrt(d)).attr("fill",randomColor).enter().exit()});
</script><style scoped></style>
App.vue
<script setup lang="ts">
// import HelloWorld from './components/HelloWorld.vue'
// import ChartsRenderer from './components/ChartsRenderer.vue'
// import Rectangle from './components/CustomCharts/Rectangle.vue'
import SvgMapRenderer from './components/SvgCharts/SvgMapRenderer.vue'
</script><template><!-- <div> --><!-- <a href="https://vitejs.dev" target="_blank"><img src="/vite.svg" class="logo" alt="Vite logo" /></a><a href="https://vuejs.org/" target="_blank"><img src="./assets/vue.svg" class="logo vue" alt="Vue logo" /></a></div><HelloWorld msg="Vite + Vue" /> --><!-- <ChartsRenderer></ChartsRenderer> --><!-- <Rectangle></Rectangle> --><SvgMapRenderer></SvgMapRenderer>
</template><style scoped>
.logo {height: 6em;padding: 1.5em;will-change: filter;transition: filter 300ms;
}
.logo:hover {filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.vue:hover {filter: drop-shadow(0 0 2em #42b883aa);
}
</style>
地图绘制
- 首先要有地图数据,在做地图位置定位的时候,要和经纬度结合起来;在做地图的时候,最核心的是地理数据,JSON格式数据
谷歌浏览器插件:JSON Viewer 让JSON数据更美观
中国的JSON数据
本地持久化
前端临时缓存数据,
localStorage – 数据量小,要注意数据序列化和反序列化的实现
IndexDB – 数据量较大,选择Dexie
拓展知识
- Figma底层使用的是 skia,是基于c++的,性能比直接用canvas在前端写会好很多
- 3d知识对于React的方面,应该看react-three-fiber;对于Vue的方面,应该看tresjs
- 关于更深入的3d知识,可以看看webGL
可以在低代码平台上扩充物料:
比如要接入React的白板,excalidraw,去对应的github网站