Objective: To draw an organizational chart

The general effect is as follows:

Demand for resolution

  1. The default is centered relative to the canvas
  2. Level 1 has only one node, level 2 is distributed horizontally, and below level 2, it is distributed vertically
  3. Rectangular frame, the width and height of each level are fixed
  4. Parent-child nodes, linked by lines
  5. There is a certain spacing between parent and child nodes and brother nodes
  6. Support click on the small circle to expand the fold
  7. 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:

  1. According to the newline character\nCut into the array
  2. Iterating through the data, cutting each element into a single character
  3. Use the Canvas APImeasureTextMeasure the width of a character
  4. A line wide is stored in an array as a string
  5. 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

  1. Since the first layer has only one node, the maximum width of the first layerMaxWidth = sum of all layers maxWidth +(gapV * number of child elements -1)The same as the second layer
  2. The maximum width of an element in the second layerMaxWidth =[maxWidth of the largest third-level child element]maxWidth
  3. Starting at the third level, the current element’sMaxWidth =[child]maxWidth+(gapV * [current level -2])

Height calculation

  1. Horizontal spacing gapH
  2. The height of the first floorMaxHeight =[second layer, highest group]maxHeight+gapH+[first layer]height
  3. The height of the second layer and belowMaxHeight =[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

  1. 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
  1. 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

  1. The main reason is to encapsulate the event listener system
  2. The elements of ZRender, granular, can meet my needs is basically rectangular, circular and so on