In my last article, “Remember a Drawing framework technology selection: jsPlumb VS mxGraph,” I mentioned why I wanted to learn mxGraph. I encountered the following problems in getting started

  • The official documentation tends to be theoretical and fails to do a good job with the code
  • Although there are many official examples, there is no explanation of the order of reading. As a beginner, I don’t know where to start reading
  • A search for “mxGraph Tutorial” through a search engine didn’t help much

After spending some time reading official documents on my own and practicing on company projects, I began to master the use of the framework. Below I write a more suitable entry article according to my learning experience.

Official list more documents, among them the following several are more useful.

  • The mxGraph Tutorial, which focuses on the composition of the entire framework
  • MxGraph User Manual — JavaScript Client, this document explains some important concepts and introduces some important apis
  • Online instances, for which the source code is available here
  • The API documentation, which is the most important one, I won’t cover interfaces in detail in the rest of the tutorial, but you can learn more about them here

If I want to systematically learn mxGraph after reading my article, I will still have to read the documentation, so I can skip it for now. Because it’s not good to start with so much theoretical stuff.

The first part of this two-part tutorial covers the basics with some examples I wrote. The second part uses the knowledge explained in part 1 to develop a small project called Pokemon Diagram. This tutorial uses ES6 syntax, while the project in Part 2 is written in Vue. This tutorial requires you to master both of these prerequisites.

The introduction of

Using script to introduce

How is the official HelloWorld instance introduced into mxGraph via script tags


      
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Hello World</title>
</head>

<body>
<div id="graphContainer"></div>
</body>

<script>
mxBasePath = '.. /src';
</script>

<script src=".. /src/js/mxClient.js"></script>
<script>
	/ /...
</script>
</html>
Copy the code

First, declare a global variable mxBasePath that points to a path, and then introduce mxGraph.

The path to which mxBasePath points serves as mxGraph’s static resource path. This is the mxBasePah of the HelloWorld project. These resources are all required by mxGraph except the JS directory, so you need to set mxBasePath before introducing mxGraph.

Look at the two versions of mxclient.js in the javascript directory. A javascript/SRC/js/mxClient js, another in the javascript/mxClient js, the latter is the former version after the packaging, so is can replace the use of both. If your project uses script tags to introduce mxGraph, check out this library.

Modular introduction

The modular introduction can refer to the pokemon-diagram file static/mxgraph/index.js

/*** introduce mxgraph ***/
// src/graph/index.js
import mx from 'mxgraph';

const mxgraph = mx({
  mxBasePath: '/static/mxgraph'});//fix BUG https://github.com/jgraph/mxgraph/issues/49
window['mxGraph'] = mxgraph.mxGraph;
window['mxGraphModel'] = mxgraph.mxGraphModel;
window['mxEditor'] = mxgraph.mxEditor;
window['mxGeometry'] = mxgraph.mxGeometry;
window['mxDefaultKeyHandler'] = mxgraph.mxDefaultKeyHandler;
window['mxDefaultPopupMenu'] = mxgraph.mxDefaultPopupMenu;
window['mxStylesheet'] = mxgraph.mxStylesheet;
window['mxDefaultToolbar'] = mxgraph.mxDefaultToolbar;

export default mxgraph;


/*** use ***/ in other modules
// src/graph/Graph.js
import mxgraph from './index';

const {
  mxGraph,
  mxVertexHandler,
  mxConstants,
  mxCellState,
  /*......*/
} = mxgraph;
Copy the code

There are two things to note here

  • The configuration item mxBasePath passed in by the mx method must point to a static resource directory that can be accessed through a URL. For example, the Pokemon-Diagram static directory is a static resource directory with resources like mxgraph/ CSS /common.css, The Pokemon Diagram app can be accessed at http://localhost:7777, By http://localhost:7777/static/mxgraph/css/common.css should be also can access common. The CSS

  • If you introduce mxGraph with the script tag, you don’t need to bind the global variable part of the code. The reason for using this code is that the mxGraph framework has some code that accesses the above properties through window.mxxxx, which can be a bit problematic without global binding. This is officially an unfixed BUG, see the code comment issue above for details

Basic knowledge of

This section uses some examples I wrote myself. You can download the code first, these examples do not need to use Node to run, directly double-click the file to open the browser to run.

Cell

Cells can represent groups, nodes, and edges in the mxGraph. The mxCell class encapsulates the operations of cells. The word Cell can be used as a node or edge.

The transaction

The official HelloWorld example shows how to insert nodes into the canvas. The two methods of “beginUpdate” and “endUpdate” that are very popular in official examples, let’s see what they do, well, really just know, because the official descriptions of these two methods are really obscure to beginners, And I have very little use for either method in real development. You can come back to it after you’ve mastered the basics of the framework. The following description comes from this document, so I’ll briefly summarize the information about these two methods.

  • BeginUpdate and endUpdateUsed to create one transaction, oncebeginUpdateMust correspond onceendUpdate
  • To ensure that endUpdate is never called if beginUpdate fails,BeginUpdate must be placed outside the try block
  • To ensure that endUpdate must also be called if an update fails in the try block,BeginUpdate must be in the finally block
  • Use beginUpdate and endUpdate to improve update view performance, and beginUpdate and endUpdate are also required for undo/redo management within the framework

According to the official instructions, if I don’t need undo/redo, I don’t have to use these two methods. I tried to remove the two methods from the HelloWorld example, and it still worked.

insertVertex

mxGraph.prototype.insertVertex = function(parent, id, value, x, y, width, height, style, relative) {

  // Set the Cell size and location
  var geometry = newmxGeometry(x, y, width, height); geometry.relative = (relative ! =null)? relative :false;

  // Create a Cell
  var vertex = new mxCell(value, geometry, style);
  // ...
  // Identify the Cell as a node
  vertex.setVertex(true);
  // ...

  // Add the Cell to the canvas
  return this.addCell(vertex, parent);
};
Copy the code

Above is the simplified insertVertex method. InsertVertex does three things, first setting up the geometry information, then creating a node, and finally adding that node to the canvas. InsertEdge is similar to insertVertex in that the intermediate procedure calls vertex. SetEdge (true) to mark the Cell as an edge. From this we also know that both nodes and edges in mxGraph are represented by the mxCell class, which identifies whether the current Cell is a node or an edge.

mxGeometry

function mxGeometry(x,y,width,height){}
Copy the code

The mxGeometry class represents the geometric information of the Cell, and the width and height are easy to understand. It is only meaningful to nodes, but not to edges. The function of x and Y is illustrated by the following example of 02.Geometry.

MxGeometry also has an important Boolean attribute called relative,

  • relativefalseRepresents the location based on the upper left corner of the canvas,X, y,Using theAbsolute unit

    The mxGeometry class is created inside insertVertex as mentioned in the previous section. Using mxgraph. insertVertex creates A node where mxGeometry. Relative is false, such as node A

  • relativetrue, indicating that the location is based on the upper left corner of the parent node,X, y,Using theRelative unit

    Using mxgraph. insertVertex creates a node where relative is false. If you want to add one node to another you need to pass true in the 9th argument of this method call and set relative to true. In this case, the child node uses the relative coordinate system, with the upper left corner of the parent node as the base point, and the value range of x and y is [-1,1]. For example, node C is positioned relative to node B.

  • relativetrueThe edge,X, y,Used to locate labels

    Using mxgraph. insertEdge creates an edge that relative is true. X and y are used to locate the label on the line. The value range of x is [-1,1]. -1 is the starting point, 0 is the midpoint, and 1 is the end point. Y represents the distance to which the label is moved on the orthogonal line of the edge. A third example will help you understand the situation.

    const e1 = graph.insertEdge(parent, null.'30%', v1, v2);
    e1.geometry.x = 1;
    e1.geometry.y = 100;
    Copy the code

Set the style

We learned from the 03.stylesheet.html example that mxGraph provides two ways to set styles.

The first is to set a global style. The mxStylesheet class is used to manage graphic styles, and graph.getStylesheet() is used to retrieve the mxStylesheet object for the current graphic. The styles property of the mxStylesheet object is also an object, which by default contains two objects, defaultVertexStyle and defaultEdgeStyle. Changing the style property in these two objects applies to all lines/nodes.

The second is named styles. Create a style object first, and then use mxStylesheet. PutCellStyle method for mxStylesheet. Add the style object and naming styles. When adding cells, write the style in the parameters. Format is as follows

[stylename;|key=value;]
Copy the code

A semicolon can be preceded by a named style name or a style key or value pair.

ROUNDED is a built-in naming style. ROUNDED is the ROUNDED corner for the ROUNDED node and ROUNDED is the corner for the opposite side.

There is one caveat to setting polylines in the example.

// Set the drag process to appear broken lines, default is straight
graph.connectionHandler.createEdgeState = function () {
  const edge = this.createEdge();
  return new mxCellState(graph.view, edge, graph.getCellStyle(edge));
};
Copy the code

Although the insertEdge method is called with the line set to polyline, it is still a straight line when you drag the edge. The above code overrides the createEdgeState method and sets the drag edge style to the same as the static edge style, which is polyline.

View style effects tips

All the styles of mxGraph can be viewed here, and when you open the site, you’ll see style constants starting with STYLE_. But these style constants do not show the effect of the style. Here’s a quick tip for viewing Style effects. You can view the current Cell Style using the Edit Style function of Draw. IO or GraphEditor (both developed using mxGraph).

For example, now I want to set the edge style to: broken line, dashed line, green, rounded corner, thick 3pt. After manually modifying the Style in the Style panel, click Edit Style to see the corresponding Style code.

I formatted the styles manually for observation. Note that the styles starting with Entry or exit on the last line represent the target coordinates of the side exit/entry, as explained in the next section.

target

You can refer to 04.anchors. HTML for information on setting a target, and we’ll use this Demo to illustrate two user actions to compare the impact of different actions on obtaining target information.

Hover the mouse over the center of node A and connect it to A target of node B when the node is bright

Then drag node A to the right of node B

You can see that if you drag a line from the center of the graph, the exit value of the edge is empty, and only the entry value is left. MxGraph intelligently adjusts the line exit direction if you drag the node. For example, the connecting target of node A was originally on the right, and the target changed after the node was dragged to the right of node B and moved to the left, while the connecting target of node B remained unchanged.

This time, hover the mouse over A target of node A and connect it to A target of node B when the target is bright

Then drag node A to the right of node B

You can see that this time you have all the values, and after you drag node A, the position of the connected target is also fixed. MxGraph does not adjust the position of the connected target as it did in the first example. The difference is due to the fact that the edge in the first example is pulled from the center of the node without information about the target, while the second example explicitly pulls an edge from a target.

Object-oriented programming

The mxGraph framework is written in an object-oriented fashion, with all classes prefixed with MX. You’ll see a lot of this form of method Overwrite in the following examples.

const oldBar =  mxFoo.prototype.bar;
mxFoo.prototype.bar = function (. args) = >{
   / /...
	oldBar.apply(this,args);
	/ /...
};
Copy the code

Node combination

This section uses the example 05.consistuent. HTML to explain the considerations of node combination.

By default, the parent node is foldable. To disable foldingEnabled, set foldingEnabled to False.

graph.foldingEnabled = false;
Copy the code

Turn on recursiveResize if you want to scale child to parent equally when changing the size of the parent.

graph.recursiveResize = true;
Copy the code

Here are the two most important pieces of code for this example.

/** * Redirects start drag to parent. */
const getInitialCellForEvent = mxGraphHandler.prototype.getInitialCellForEvent;
mxGraphHandler.prototype.getInitialCellForEvent = function (me) {
  let cell = getInitialCellForEvent.apply(this.arguments);
  if (this.graph.isPart(cell)) {
    cell = this.graph.getModel().getParent(cell);
  }
  return cell;
};

// Redirects selection to parent
graph.selectCellForEvent = function (cell) {
  if (this.isPart(cell)) {
    mxGraph.prototype.selectCellForEvent.call(this.this.model.getParent(cell));
    return;
  }

  mxGraph.prototype.selectCellForEvent.apply(this.arguments);
};
Copy the code

Both methods Overwrite the original method by determining if the node is a child and replacing it with the parent node to perform the rest of the logic.

GetInitialCellForEvent is triggered when the mouse is pressed (a mousedown event, not a click event). If you comment out the code and do not use the parent node replacement, the child node will be dragged separately when the drag occurs and will not be linked to the parent node. After replacing the child node with the parent node, the original child node should be dragged. Now the parent node is dragged to achieve the linkage effect.

SelectCellForEvent is actually a method called inside getInitialCellForEvent. GetSelectionCell this method sets the ectionCell to selectionCell, which is available through mxgraph.getSelectionCell. As with getInitialCellForEvent, mxgraph.getSelectionCell fetches child nodes if the parent node is not used instead. We’ll use the mxgraph.getSelectionCell interface for the actual project.

The project of actual combat

In this part, I will mainly pick some important points of this project and explain them.

Write a combination of nodes

Let’s take this node of the project as an example to explain how to combine nodes

const insertVertex = (dom) = > {
  // ...
  const nodeRootVertex = new mxCell('Double click input'.new mxGeometry(0.0.100.135), `node; image=${src}`);
  nodeRootVertex.vertex = true;
  // ...
  
  const title = dom.getAttribute('alt');
  const titleVertex = graph.insertVertex(nodeRootVertex, null, title,
    0.1.0.65.80.16.'constituent=1; whiteSpace=wrap; strokeColor=none; fillColor=none; fontColor=#e6a23c'.true);
  titleVertex.setConnectable(false);

  const normalTypeVertex = graph.insertVertex(nodeRootVertex, null.null.0.05.0.05.19.14.`normalType; constituent=1; fillColor=none; image=/static/images/normal-type/forest.png`.true);
  normalTypeVertex.setConnectable(false);
  / /...
};
Copy the code

NodeRootVertex alone looks like this. This is done by setting a custom Node style (see Graph class _putVertexStyle method) and setting the image path with the image property.

Because by default a node can have only one text area and one image area, adding additional text and images requires combining nodes. This is achieved by adding titleVertex text nodes and normalTypeVertex image nodes to nodeRootVertex.

Sometimes it is necessary to set different mouse cursor ICONS for different child nodes. For example, in this project, when the mouse hover over normalTypeVertex, the mouse becomes hand shape. Refer to setCursor method of AppCanvas. Rewrite mxGraph. Prototype. GetCursorForCell can achieve this function.

const setCursor = (a)= > {
  const oldGetCursorForCell = mxGraph.prototype.getCursorForCell;
  graph.getCursorForCell = function (. args) {
    const [cell] = args;
    return cell.style.includes('normalType')?'pointer' :
      oldGetCursorForCell.apply(this, args);
  };
};
Copy the code

Edit content

The following code is a common setting for editing content

// Press enter to finish typing instead of wrapping
this.setEnterStopsCellEditing(true);
// Press escape to finish typing while editing
mxCellEditor.prototype.escapeCancelsEditing = false;
// Complete input when out of focus
mxCellEditor.prototype.blurEnabled = true;
Copy the code

By default if press enter key when the input content will wrap, but some of the scenes have banned the demand of the line, hope to enter after complete input, through the graph. SetEnterStopsCellEditing (true) setting can meet the demand.

Focus on mxCellEditor. Prototype. BlurEnabled this property, by default if the user input cannot focus region outside of the mouse click on the canvas (div, section, article, etc.), the nodes in the editor is not out of focus, This causes the LABEL_CHANGED event not to be fired. In actual project development, however, we would expect that if the user clicks outside of the canvas while typing, it should count as completing the input, and then synchronize the modified content to the server via the LABEL_CHANGED event that is triggered. Through mxCellEditor. Prototype. BlurEnabled = true this line of code is set to meet our requirements.

A newline label

const titleVertex = graph.insertVertex(nodeRootVertex, null, title,
      0.1.0.65.80.16.'constituent=1; whiteSpace=wrap; strokeColor=none; fillColor=none; fontColor=#e6a23c'.true);
Copy the code

For non-input text content, by default it will not wrap even if the text exceeds the width of the container. TitleVertex, with a width of 80 in our project, is one such example.

To set a line break you need to do two things. The first is to render the text using HTML with this line of code mxgraph.sethtmlLabels (true) (mxGraph defaults to render the text using SVG’s Text tag). The second is to add the line whiteSpace=wrap as in titleVertex style Settings above.

Model

Now introduce the concept of a Model, which is a structured data representation of the current graph. MxGraphModel encapsulates operations related to Model.

You can start the project, draw a diagram like this, and click Output XML. To ensure that the XML is consistent with the following, you need to drag zhye first, then super Pikachu, and finally join the edge.

The console should output such an XML

<mxGraphModel>
  <root>
    <mxCell id="0"/>
    <mxCell id="1" parent="0"/>
    <mxCell id="4" value="Hello" style="node; image=/static/images/ele/ele-005.png" vertex="1" data="{" id" :1," element" :{" id" :1," icon" :" ele-005.png" ," title" :" Think ye & quot; }," normalType" :" water.png" }" parent="1">
      <mxGeometry x="380" y="230" width="100" height="135" as="geometry"/>
    </mxCell>.</root>
</mxGraphModel>
Copy the code

Each mxCell node has a parent attribute pointing to the parent node. We manually format the mxCell node value=”Hello”.

<mxCell 
    id="4" 
    value="Hello" 
    style="node; image=/static/images/ele/ele-005.png" 
    vertex="1" 
    data="{" id" :1," element" :{" id" :1," icon" :" ele-005.png" ," title" :" Think ye & quot; }," normalType" :" water.png" }" 
    parent="1">
  <mxGeometry 
    x="380" 
    y="230" 
    width="100" 
    height="135" as="geometry"/>
</mxCell>
Copy the code

The data value is derived from the original object via json.stringify, which has been escaped to look like this. The console also prints an mxGraphModel object. Comparing the XML above with the node object in the image below, you can see that they are just different representations of the same Model. The XML formats the MxGraph.Model.

The event

This project listens to events written in the _listenEvent method of AppCanvas. Vue, you can learn about some common events in this method. Below is the method call dependency graph from the mxGraph class, which shows the flow of events throughout the framework.

Listen for an event

The _listenEvent method of this project uses two event listening objects.

  • MxGraph inherits from mxEventSource, using the addListener method of its parent class to subscribe to/broadcast events as an event center.

  • MxGraph. GetSelectionModel () returns a mxGraphSelectionModel object, the object is inherited from mxEventSource have mxEvent. UNDO, mxEvent. CHANGE the two events, Listen for the mxEvent.CHANGE event to get the currently selected Cell.

Difference between ADD_CELLS and CELLS_ADD

The mxGraph class has many events of the form XXX_CELLS and CELLS_XXXED, which I have not yet understood. Let’s use the addition event as an example to explore the differences between the two types of events.

  • addCell“Will trigger two eventsADD_CELLS,CELLS_ADDEDFirst, the triggerCELLS_ADDEDAfter the triggerADD_CELLS.
  • ADD_CELLSaddCellsMethod is triggered, whileCELLS_ADDEDcellsAddedMethod. And foraddCells 与 cellsAddedThe official documentation does not show the difference between the two, and further research will have to consult the source code. As a rule of thumb, events triggered later carry more information, so I usually listen for themADD_CELLSEvents.MOVE_CELLS, CELLS_MOVED,REMOVE_CELLS, CELLS_REMOVEDAnd so on.

Listen for Cell add events

As can be seen from the above method call dependency graph, insertVertex and insertEdge are eventually treated as cells, and subsequent triggered events are treated as Cell events without distinction between nodes and edges. Therefore, for a Cell add event, you need to distinguish whether nodes are added or edges are added.

graph.addListener(mxEvent.CELLS_ADDED, (sender, evt) => {
  const cell = evt.properties.cells[0];
  if (graph.isPart(cell)) {
    return;
  }

  if (cell.vertex) {
    this.$message.info('Added a node');
  } else if (cell.edge) {
    this.$message.info('Added a line'); }});Copy the code

In addition, the Cell addition event is also triggered when the child node is added to the parent node (for example, titleVertex and normalTypeVertex are added to nodeRootVertex in this project). These children are normally left alone and can be filtered out with an isPart judgment as in 05.consistent.html.

Custom events

As mentioned above, mxGraph inherits from mxEventSource, and calls to the parent class fireEvent trigger custom events. Here is a simple example

mxGraph.addListener('Custom event A', () = > {// do something .....
});
// Triggers a custom event
mxGraph.fireEvent(new mxEventObject('Custom event A');
Copy the code

I also implemented two custom events in the _configCustomEvent method of the Graph class of this project. The EDGE_START_MOVE event is emitted when an edge begins to drag, and the VERTEX_START_MOVE event is emitted when a node begins to drag.

Export images

The idea of mxGraph is to export the XML of the graph and calculate the width and height of the graph in the front end, and then send the XML, width and height data to the server. The server also uses the API provided by mxGraph to convert the XML into images. If the server is using Java, you can refer to the official example. The following describes the front-end work.

You can export images using the mxImageExport class, which has a document that you can use directly.

// ...
var xmlCanvas = new mxXmlCanvas2D(root);
var imgExport = new mxImageExport();
imgExport.drawState(graph.getView().getState(graph.model.root), xmlCanvas);

var bounds = graph.getGraphBounds();
var w = Math.ceil(bounds.x + bounds.width);
var h = Math.ceil(bounds.y + bounds.height);

var xml = mxUtils.getXml(root);
// ...
Copy the code

But instead of capturing the top left and bottom right elements as borders, this code captures the entire canvas. If you need elements as boundaries, you’ll need to call XMLCanvas.Translate to adjust the boundaries.

/ /...
var xmlCanvas = new mxXmlCanvas2D(root);
xmlCanvas.translate(
      Math.floor((border / scale - bounds.x) / scale),
      Math.floor((border / scale - bounds.y) / scale),
    );
/ /...
Copy the code

Refer to the exportPicXML method of the Graph class of this project for the complete screenshot code.

If the node uses images like my project, and the exported image node does not have images. The problem can be solved in two ways. First check whether the image path in the sent XML is accessible. For example, here is an image label in the XML printed by the project “Export Image” function.

<image x="484" y="123" w="72" h="72" src="http://localhost:7777/static/images/ele/ele-005.png" aspect="0" flipH="0" flipV="0"/>
Copy the code

To ensure that http://localhost:7777/static/images/ele/ele-005.png is accessible. If there is no problem with the image path, please check the image format. Originally, I used the IMAGE in the node of the company project in SVG format, but failed to export the image. It may be that mxGraph does not support this format.

Also, if the color of the node in the exported image is different from the setting, it may be because the style is written with a 3 digit color like # FFF, the color must use the full 6 digits, otherwise the exported image will have a problem.

reference

  • mxGraph Tutorial
  • MxGraph User Manual — JavaScript Client
  • mxGraph API Specification
  • mxGraph Javascript Examples