Menu
Create a pie chart container using createRenderer api
First let’s create a vue file named “CanvasApp”:
<template><piechart @click="handleClick" :data="state.data" :x="200" :y="200" :r="200"></piechart></template><script>import { reactive, ref } from 'vue'export default {setup() {const state = reactive({data: [{name: '大专',count: 200,color: 'brown'},{name: '本科',count: 300,color: 'yellow'},{name: '硕士',count: 100,color: 'pink'},{name: '博士',count: 50,color: 'skyblue'}]})function handleClick() {state.data.push({name: '其他',count: 30,color: 'orange'})}return {state,handleClick}}}</script><style></style>
Then, in main.js, we init our custom renderer and bind container:
import { createApp } from "vue"import App from "./App.vue"import "./index.css"createApp(App).mount("#app")import { createRenderer } from "vue"import CanvasApp from "./components/CanvasApp.vue"const draw = (el, noClear) => {if (!noClear) {ctx.clearRect(0, 0, canvas.width, canvas.height)}if (el.tag === "piechart") {let { data, r, x, y } = ellet total = data.reduce((memo, current) => memo + current.count, 0)let start = 0,end = 0data.forEach((item) => {end += (item.count / total) * 360drawPieChart(start, end, item.color, x, y, r)drawPieChartText(item.name, (start + end) / 2, x, y, r)start = end})}el.childs && el.childs.forEach((child) => draw(child, true))}const d2a = (n) => {return (n * Math.PI) / 180}const drawPieChart = (start, end, color, cx, cy, r) => {let x = cx + Math.cos(d2a(start)) * rlet y = cy + Math.sin(d2a(start)) * rctx.beginPath()ctx.moveTo(cx, cy)ctx.lineTo(x, y)ctx.arc(cx, cy, r, d2a(start), d2a(end), false)ctx.fillStyle = colorctx.fill()ctx.stroke()ctx.closePath()}const drawPieChartText = (val, position, cx, cy, r) => {ctx.beginPath()let x = cx + (Math.cos(d2a(position)) * r) / 1.25 - 20let y = cy + (Math.sin(d2a(position)) * r) / 1.25 - 20ctx.fillStyle = "#000000"ctx.font = "20px 微软雅黑"ctx.fillText(val, x, y)ctx.closePath()}const nodeOps = {insert: (child, parent, anchor) => {// 我们重写了insert逻辑,因为在我们canvasApp中不存在实际dom插入操作// 这里面只需要将元素之间的父子关系保存一下即可child.parent = parentif (!parent.childs) {parent.childs = [child]} else {parent.childs.push(child)}// 只有canvas有nodeType,这里就是开始绘制内容到canvasif (parent.nodeType == 1) {draw(child)// 如果子元素上附加了事件,我们给canvas添加监听器if (child.onClick) {ctx.canvas.addEventListener("click", () => {child.onClick()setTimeout(() => {draw(child)}, 0)})}}},remove: (child) => {},createElement: (tag, isSVG, is) => {// 创建元素时由于没有需要创建的dom元素,只需返回当前元素数据对象return { tag }},createText: (text) => {},createComment: (text) => {},setText: (node, text) => {},setElementText: (el, text) => {},parentNode: (node) => {},nextSibling: (node) => {},querySelector: (selector) => {},setScopeId(el, id) {},cloneNode(el) {},insertStaticContent(content, parent, anchor, isSVG) {},patchProp(el, key, prevValue, nextValue) {el[key] = nextValue},}// 创建一个渲染器let renderer = createRenderer(nodeOps)// 保存画布和其上下文let ctxlet canvas// 扩展mount,首先创建一个画布元素function createCanvasApp(App) {const app = renderer.createApp(App)const mount = app.mountapp.mount = (selector) => {canvas = document.createElement("canvas")canvas.width = window.innerWidthcanvas.height = window.innerHeightdocument.querySelector(selector).appendChild(canvas)ctx = canvas.getContext("2d")mount(canvas)}return app}createCanvasApp(CanvasApp).mount("#demo")

