Objective: To draw an organizational chart
The general effect is as follows:
Demand for resolution
- The default is centered relative to the canvas
- Level 1 has only one node, level 2 is distributed horizontally, and below level 2, it is distributed vertically
- Rectangular frame, the width and height of each level are fixed
- Parent-child nodes, linked by lines
- There is a certain spacing between parent and child nodes and brother nodes
- Support click on the small circle to expand the fold
- For each rectangle element, text is displayed, centered, and wrapped if the text is too long
The preparatory work
Learn canvas first to understand the basic routine of Canvas drawing
Split according to the above requirements, first draw each individual element with canvas API
1. Draw the rectangle first
<! DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Rect</title> </head> <body> <canvas id="canvas" width="1024" height="2768" ></canvas> </body> <script> const canvas = document.getElementById('canvas'); const ctx = canvas.getContext('2d'); const width = canvas.width; Const centerX = width / 2 // const drawDept = (CTX, config) => {ctx.fillstyle = config.fillstyle; ctx.rect(config.x, config.y, config.width, config.height); ctx.fill(); ctx.lineWidth = 1; ctx.strokeStyle = '#222'; ctx.rect(config.x, config.y, config.width, config.height); ctx.stroke() } const firstLevelItem = { width: 150, height: 75, x: centerX - 150/2, y: 10 } const firstLevelConfig = { width: firstLevelItem.width, height: firstLevelItem.height, x: firstLevelItem.x, y: firstLevelItem.y, fillStyle: "transparent", } drawDept(ctx,firstLevelConfig) </script> </html>Copy the code
2. Draw a circle
<! DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Circle</title> </head> <body> <canvas id="canvas" width="1024" height="2768" ></canvas> </body> <script> const canvas = document.getElementById('canvas'); const ctx = canvas.getContext('2d'); const width = canvas.width; // Const drawCollapseButton = (CTX,config)=>{ctx.fillstyle = '# FFF 'ctx.beginPath(); CTX. Arc (config. X config. Y, config. R, 0, 2 * Math. PI); ctx.stroke(); ctx.fill(); drawText(ctx,{ x:config.x, y:config.y + 8, text: '+', fontSize: 20, lineHeight: 20, containerHeight: config.r * 2, color: '#222', maxWidth: config.r * 2 }) } drawCollapseButton(ctx,{ x: 20, y: 20, r: 10 }) </script> </html>Copy the code
3. The picture of attachment
As long as there are horizontal and vertical layouts, there are also two lines. The common point of both connections is that they have a starting point and an ending point, with 0 to n intermediate points
It’s a little bit too much code, just giving the function of the line
const drawLine = (ctx, config) => {
ctx.strokeStyle = config.borderColor;
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(config.startPoint.x, config.startPoint.y);
config.middlePoint.forEach(item => {
ctx.lineTo(item.x, item.y)
})
ctx.lineTo(config.endPoint.x, config.endPoint.y)
ctx.stroke();
}
Copy the code
4. Write words
Canvas provides some API for text, but does not support line breaks. Therefore, the code idea of adding line breaks is:
- According to the newline character
\n
Cut into the array - Iterating through the data, cutting each element into a single character
- Use the Canvas API
measureText
Measure the width of a character - A line wide is stored in an array as a string
- Iterate over the data to draw characters
const workBreak = (ctx, text, maxWidth) => { const objText = ctx.measureText(text); let arrFillText = [] if (objText.width > maxWidth) { const arrText = text.split('') let newText = ''; arrText.forEach((world, index) => { const currentText = `${newText}${world}` const {width} = ctx.measureText(currentText); if (width >= maxWidth) { arrFillText.push(newText) newText = world } else { newText = currentText if (index === arrText.length - 1) { arrFillText.push(newText) } } }) } else { arrFillText = [text] } return arrFillText } const drawText = (ctx, config) => { ctx.font = `${config.fontSize}px serif` ctx.fillStyle = config.color ctx.textAlign = 'center' const arrText = config.text.split('\n'); const arrDrawText = [] arrText.forEach((item) => { const arr = workBreak(ctx, item, config.maxWidth) arr.forEach((text) => { arrDrawText.push(text) }) }) const h = arrDrawText.length * config.lineHeight; const gap = config.containerHeight - h arrDrawText.forEach((text, index) => { ctx.fillText(text, config.x, config.y + index * (config.lineHeight) + gap / 2, config.maxWidth) }) }Copy the code
5. Check that the mouse is in the current area
IsPointInPath Method used to determine whether the current path contains a checkpoint
ctx.isPointInPath(mousePoint.x,mousePoint.y)
Copy the code
6. Crunch the data
Recursion is used a lot in the process because the schema diagram, when presented, is the shape of a tree. There are various positions with common HTML elements, such as div, etc. Each element drawn on canvas needs to calculate its own coordinate position. Therefore, it is necessary to process the data at the same time, but also to determine the coordinates. In the following section, some problems and ideas of data processing are discussedCopy the code
Process the data
If you draw a picture, the relationships between the elements will be clearer
Width calculation
- Since the first layer has only one node, the maximum width of the first layer
MaxWidth = sum of all layers maxWidth +(gapV * number of child elements -1)
The same as the second layer - The maximum width of an element in the second layer
MaxWidth =[maxWidth of the largest third-level child element]maxWidth
- Starting at the third level, the current element’s
MaxWidth =[child]maxWidth+(gapV * [current level -2])
Height calculation
- Horizontal spacing gapH
- The height of the first floor
MaxHeight =[second layer, highest group]maxHeight+gapH+[first layer]height
- The height of the second layer and below
MaxHeight =[all child elements]height*gapH*[child element -1] number +[own]height+gapH
Coordinates of the starting point of the element
With the maximum width and height, you can determine the coordinates of the individual elements
Connection point determination
- The join point for the horizontal layout is in the middle of the element
The join point with the parent element parentLinkPoint
Start point = childLinkPoint.x - (maxWidth of parent)/2 index rank among siblings, 0 start x = start point + index * gapV + sum of maxWidth of previous siblings y = childLinkPoint. Y + gapH of parent nodeCopy the code
Join point with child element childLinkPoint
X = x + half of the actual width of the current node [gray block above] y = yCopy the code
- Vertical layout of connection points
The join point with the parent element parentLinkPoint
X is offset halfway to the right of gapV by the parent linkPoint. Y is offset downward by gapH by the parent linkPointCopy the code
Join point with child element childLinkPoint
X = x y = y Half the actual height of the current nodeCopy the code
The problem
So I’ve drawn the picture. But there are other problems.
1. Graph pickup
That is, which graph the mouse is on. Although canvas API provides to determine which graph the mouse is on, according to the Internet, this method has certain performance problems in the case of multiple graphs.
Here are some common methods:
1. Use the built-in API of Canvas to pick up graphics
isPointInPath
isPointInStroke
Copy the code
2. Use geometric operations to pick up graphs
You need to provide a way for each graph to determine whether it is inside the graph and on the edge of the graph
3. Use the cache Canvas to pick up graphics by color
4. Mix the above methods to pick up graphics
See antV for details
2. Event monitoring and processing
When there are multiple graphs stacked on top of each other, how can the response of event listening be implemented like our ordinary elements, time capture, bubbling ~
Some antV articles
And while I was wondering how to do that, I found something that I could use, right
ZRender
ZRender is a 2d drawing engine, which provides Canvas, SVG, VML and other rendering methods. ZRender is also the renderer for ECharts.
Let’s first sort out what ZRender features can be used in my organizational architecture system
If you add event listener system, in the case of graph nesting, the event bubbling, is a troublesome thing, so, lazy
- The main reason is to encapsulate the event listener system
- The elements of ZRender, granular, can meet my needs is basically rectangular, circular and so on