# Edit in JSFiddle

```const TAU = Math.PI * 2

const mapToEllipse = ({ x, y }, rx, ry, cosphi, sinphi, centerx, centery) => {
x *= rx
y *= ry

const xp = cosphi * x - sinphi * y
const yp = sinphi * x + cosphi * y

return {
x: xp + centerx,
y: yp + centery
}
}

const approxUnitArc = (ang1, ang2) => {
const a = 4 / 3 * Math.tan(ang2 / 4)

const x1 = Math.cos(ang1)
const y1 = Math.sin(ang1)
const x2 = Math.cos(ang1 + ang2)
const y2 = Math.sin(ang1 + ang2)

return [
{
x: x1 - y1 * a,
y: y1 + x1 * a
},
{
x: x2 + y2 * a,
y: y2 - x2 * a
},
{
x: x2,
y: y2
}
]
}

const vectorAngle = (ux, uy, vx, vy) => {
const sign = (ux * vy - uy * vx < 0) ? -1 : 1
const umag = Math.sqrt(ux * ux + uy * uy)
const vmag = Math.sqrt(ux * ux + uy * uy)
const dot = ux * vx + uy * vy

let div = dot / (umag * vmag)

if (div > 1) {
div = 1
}

if (div < -1) {
div = -1
}

return sign * Math.acos(div)
}

const getArcCenter = (
px,
py,
cx,
cy,
rx,
ry,
largeArcFlag,
sweepFlag,
sinphi,
cosphi,
pxp,
pyp
) => {
const rxsq = Math.pow(rx, 2)
const rysq = Math.pow(ry, 2)
const pxpsq = Math.pow(pxp, 2)
const pypsq = Math.pow(pyp, 2)

let radicant = (rxsq * rysq) - (rxsq * pypsq) - (rysq * pxpsq)

}

radicant /= (rxsq * pypsq) + (rysq * pxpsq)

const centerxp = radicant * rx / ry * pyp
const centeryp = radicant * -ry / rx * pxp

const centerx = cosphi * centerxp - sinphi * centeryp + (px + cx) / 2
const centery = sinphi * centerxp + cosphi * centeryp + (py + cy) / 2

const vx1 = (pxp - centerxp) / rx
const vy1 = (pyp - centeryp) / ry
const vx2 = (-pxp - centerxp) / rx
const vy2 = (-pyp - centeryp) / ry

let ang1 = vectorAngle(1, 0, vx1, vy1)
let ang2 = vectorAngle(vx1, vy1, vx2, vy2)

if (sweepFlag === 0 && ang2 > 0) {
ang2 -= TAU
}

if (sweepFlag === 1 && ang2 < 0) {
ang2 += TAU
}

return [ centerx, centery, ang1, ang2 ]
}

const arcToBezier = ({
px,
py,
cx,
cy,
rx,
ry,
xAxisRotation = 0,
largeArcFlag = 0,
sweepFlag = 0
}) => {
const curves = []

if (rx === 0 || ry === 0) {
return []
}

const sinphi = Math.sin(xAxisRotation * TAU / 360)
const cosphi = Math.cos(xAxisRotation * TAU / 360)

const pxp = cosphi * (px - cx) / 2 + sinphi * (py - cy) / 2
const pyp = -sinphi * (px - cx) / 2 + cosphi * (py - cy) / 2

if (pxp === 0 && pyp === 0) {
return []
}

rx = Math.abs(rx)
ry = Math.abs(ry)

const lambda =
Math.pow(pxp, 2) / Math.pow(rx, 2) +
Math.pow(pyp, 2) / Math.pow(ry, 2)

if (lambda > 1) {
rx *= Math.sqrt(lambda)
ry *= Math.sqrt(lambda)
}

let [ centerx, centery, ang1, ang2 ] = getArcCenter(
px,
py,
cx,
cy,
rx,
ry,
largeArcFlag,
sweepFlag,
sinphi,
cosphi,
pxp,
pyp
)

const segments = Math.max(Math.ceil(Math.abs(ang2) / (TAU / 4)), 1)

ang2 /= segments

for (let i = 0; i < segments; i++) {
curves.push(approxUnitArc(ang1, ang2))
ang1 += ang2
}

return curves.map(curve => {
const { x: x1, y: y1 } = mapToEllipse(curve[ 0 ], rx, ry, cosphi, sinphi, centerx, centery)
const { x: x2, y: y2 } = mapToEllipse(curve[ 1 ], rx, ry, cosphi, sinphi, centerx, centery)
const { x, y } = mapToEllipse(curve[ 2 ], rx, ry, cosphi, sinphi, centerx, centery)

return { x1, y1, x2, y2, x, y }
})
}

function CacBezierCurveMidPos(tempSourcePos, tempTargetPos) {
let dx = tempSourcePos.x - tempTargetPos.x,
dy = tempSourcePos.y - tempTargetPos.y,
dr = Math.sqrt(dx * dx + dy * dy);

const curve = {
type: 'arc',
rx: dr,
ry: dr,
largeArcFlag: 0,
sweepFlag: 1,
xAxisRotation: 0,
}

const curves = arcToBezier({
px: tempSourcePos.x,
py: tempSourcePos.y,
cx: tempTargetPos.x,
cy: tempTargetPos.y,
rx: curve.rx,
ry: curve.ry,
xAxisRotation: curve.xAxisRotation,
largeArcFlag: curve.largeArcFlag,
sweepFlag: curve.sweepFlag,
});

return curves[0];
}

let canvas, stage;

// Aliases
let Stage = createjs.Stage,
Sprite = createjs.Sprite,
Bitmap = createjs.Bitmap,
Shape = createjs.Shape,
Text = createjs.Text,
Graphics = createjs.Graphics,
Container = createjs.Container;

let edgeContainer = new Container(),
arrowContainer = new Container(),
nodeContainer = new Container(),
textContainer = new Container(),
imageContainer = new Container(),
dragContainer = new Container();

// For scale limmit
const SCALE_MAX = 100, SCALE_MIN = 0.2;

// Fix canvas position offset
const offsetX = 0, offsetY = 0;

// Node shape default is circle
// FIXME: Other shape‘s support
let nodeWidth = 30;

// Cache the shape that what your mouse click generate
let point = {};

// Cache list
// Have unique identifiers => id
let nodeList = {},// Cache node
edgeList = {},// Cache edge
arrowList = {},// Cache arrow
textList = {},// Cache text
lineList = {},// Cache edge info

bezierList = {},// Cache bezierCurve to Deal with 2 bezierCurve
imageList = {},// Cache image
circleList = {};// Cache init node shape info

// Now is just for Bezier,TODO is for all(include straight use midPos to Calculate)
// let midPos;

// Fix Events trigger conflict between nativeEvent and CreatejsEvent
let nodeFlag = false;
let pinEffectFlag = false;
let auraArray = [];//节点属性数组
let simulation;// D3-force simulation

/**
* CreateCharts is the core method for initialization
* @param {Object} opts
* opts defined some init options
* For example:(after "..." means no necessary)
* {container: document.getElementById('canvas'),
*  elements: {
*      nodes:[{ id: 'a'
*                      ...width:10,color:'black'},
*             { id: 'b'
*                      ...width:30,color:'#d3d3d3'}],
*      edges:{ source:'a',target:'b'
*                      ...curveStyle:'bezier',targetShape:'triangle',sourceShape:'circle'}}
*/
function CreateCharts({ container, elements }) {

if (!container) return;// Todo => Throw error
canvas = container;

// Enable touch interactions if supported on the current device:
createjs.Touch.enable(stage);
// Enabled mouse over / out events
stage = new Stage(canvas);

// Auto update stage
createjs.Ticker.timingMode = createjs.Ticker.RAF;
// Keep tracking the mouse even when it leaves the canvas
stage.mouseMoveOutside = true;
stage.enableMouseOver(10);

let { nodes, edges } = elements;

//Init canvas property
canvas.height = window.innerHeight - offsetY;
canvas.width = window.innerWidth - offsetX;
canvas.style.background = '#d3d3d3';

initializeNodes(nodes, edges);

// Init event
initEvent(canvas);

// Hierarchy order
stage.addChild(nodeContainer);// Node above edge and arrow
}

/**
* InitializeNodes method use for layout and paint nodes
* @param {Array} nodes
* @param {Array} edges
*/
function initializeNodes(nodes, edges) {
//D3-force Layout
simulation = d3.forceSimulation(nodes)
.force("charge", d3.forceManyBody())
//.force('charge', d3.forceManyBody().strength(-30).distanceMin(100))
.force('center', d3.forceCenter(canvas.width / 2, canvas.height / 2))
//.force("collide", d3.forceCollide().radius(function(d) { return d.width; }).strength(0.7).iterations(1))
.force("x", d3.forceX())
.force("y", d3.forceY())

//Init node and node text
for (let i = 0, node; i< nodes.length; i++) {
node = nodes[i];
nodeList[node.id] = node;
//Draw node
let graphics = new Shape();
let circle = graphics.graphics;
if (node.color) {
circle.beginFill(node.color);
} else {
let color = getRandomColor();
circle.beginFill(color);
}

if(node.text)drawText(node, node.x, node.y);
if(node.imageSrc)drawImage(node, node.x, node.y);
if(node.aura) initAura(node);

let width = nodeWidth;
if (node.width){
width = node.width;
}else{
node.width = width;
}
circle.drawCircle(0, 0, width);
circle.endFill();

graphics = setNode(graphics, node.id, pinEffectFlag);

//Move the graph to its designated position
graphics.x = node.x;
graphics.y = node.y;

//用缓存的话放大会失真
//graphics.cache(-width, -width, width*2, width*2);

circleList[node.id] = graphics;
}

console.log(auraArray);
//Init edge text
for (let i = 0, edge; i< edges.length; i++) {
edge = edges[i];
if(edge.text)drawText(edge,0,0);
}

let ticked = function () {
nodes.forEach(drawNode);
edges.forEach(drawEdge);
stage.update();
}

let drawNode = function (node) {
//只需要移动位置
circleList[node.id].x = node.x;
circleList[node.id].y = node.y;
if(node.text) updateText(node.id, { x: node.x, y: node.y });
if(node.imageSrc) updateImage(node.id, { x: node.x, y: node.y })
}

let drawEdge = function (edge) {
drawArrowAndEdge(edge, edge.source, edge.target);
}

simulation.on('tick', ticked);
}

function initAura(node){
let color = getRandomColor();
if(!auraArray[node.aura]) auraArray[node.aura] = {};
let auraProperty = auraArray[node.aura];
auraProperty.color = color;
if(auraProperty.member===undefined) auraProperty.member = [];
auraProperty.member.push(node);
}

/**
* DrawImage method use for init node's image
* @param {Object} nodeInfo
* imageSrc=>node image's url
* id=> use for set image id
* textOpts => image style
* @param x => image position X
* @param y => image position y
*/
function drawImage({ imageSrc, id, width, imageOpts = {} }, x, y) {
if (imageSrc === '' || imageSrc === undefined) return;
if (imageList[id]) imageContainer.removeChild(imageList[id]);
let image = new Image();
image.src = imageSrc;
let imgWidth = image.width,
imgHeight = image.height;
//Calculate scale
let scale = width/imgWidth*2;

let bmp = new createjs.Bitmap(image);
bmp.scale = scale;

let cGraphics = new Shape();
let circle = cGraphics.graphics;
circle.beginFill(Graphics.getRGB(255, 255, 255, 0));//alpha
let circleWidth = width-2;
circle.drawCircle(0,0,circleWidth);
circle.endFill();

//cGraphics.cache(-circleWidth, -circleWidth, circleWidth*2, circleWidth*2);

bmp.x = x-width/2;
bmp.y = y-width/2;
// Cache image
imageList[id] = bmp;
}
}

/**
* updateImage method use for update node image position
* @param {String} id
* @param {Object} newPos
*/
function updateImage(id, newPos) {
let bmp = imageList[id];
if(bmp){
bmp.x = newPos.x-nodeList[id].width;
bmp.y = newPos.y-nodeList[id].width;
}
}
}

/**
* DrawText method use for init node text position
* @param {Object} nodeInfo
* text=>node text to show
* id=> use for set text id
* textOpts => text style
* @param x => text position X
* @param y => text position y
*/
function drawText({ text, id, textOpts = {} }, x, y) {
if (text === '' || text === undefined) return;
if (textList[id]) textContainer.removeChild(textList[id]);
let nodeText = new Text(text, textOpts.font || '36px Arial', textOpts.color || 'white');
x += textOpts.disX? textOpts.disX: 0;//初始化这一步可以省略
y += textOpts.disY? textOpts.disY: 0;//因为后面每一帧都会刷新，初始化可以放在任意位置

nodeText.textAlign = 'center';
nodeText.textBaseline = 'middle';
nodeText = Object.assign(nodeText, textOpts);
nodeText.x = x;
nodeText.y = y;
// Cache text
textList[id] = nodeText;
}

/**
//  * UpdateText method use for update text when node position change
* @param {String} id
* @param {Object} newPos
*/
let updateText = function (id, newPos) {
let text = textList[id];
newPos.x += text.disX? text.disX: 0;
newPos.y += text.disY? text.disY: 0;
text.x = newPos.x;
text.y = newPos.y;
}

/**
* SetNode method use for init node's event when dragging node
* @param {Shape} graph
* @param {String} id
* @return {Shape}
*/
function setNode(graph, id, pinFlag) {
let onDragEnd;
if (pinFlag) {
onDragEnd = function (event) {
simulation.alphaTarget(0);
//Sticky or not
// nodeList[id].fx = null;
// nodeList[id].fy = null;
let target = event.target;
target.dragging = false;
// Set the interaction data to null
target.data = null;
// Put back the original container
dragContainer.removeChild(this);
dragContainer.removeChild(textList[id]);
dragContainer.removeChild(imageList[id]);

nodeFlag = false;
}
} else {
onDragEnd = function (event) {
simulation.alphaTarget(0);
//Sticky or not
nodeList[id].fx = null;
nodeList[id].fy = null;
let target = event.target;
target.dragging = false;
// Set the interaction data to null
target.data = null;
// Put back the original container
dragContainer.removeChild(this);
dragContainer.removeChild(textList[id]);
dragContainer.removeChild(imageList[id]);

nodeFlag = false;
}
}

let onDragStart = function (event) {
simulation.alphaTarget(0.3).restart();
event.stopPropagation();// FIXME
nodeFlag = true;
}

let onDragMove = function (event) {
let newPosition = stage.globalToLocal(event.stageX, event.stageY);
this.x = newPosition.x;
this.y = newPosition.y;
updateNode(id, newPosition);
// data drive document
//updateEdge(id, newPosition);// change node pos enough
if(textList[id])updateText(id, newPosition);
textContainer.removeChild(textList[id]);
nodeContainer.removeChild(this);
imageContainer.removeChild(imageList[id]);

}

//图钉效果
let onDbClick = function () {
nodeList[id].fx = null;
nodeList[id].fy = null;
}

let updateNode = function (id, newPos) {
nodeList[id].x = newPos.x;
nodeList[id].y = newPos.y;
nodeList[id].fx = newPos.x;
nodeList[id].fy = newPos.y;
}

graph.cursor = "pointer";
graph.on('mousedown', onDragStart);
graph.on('pressup', onDragEnd);
graph.on('pressmove', onDragMove);
if (pinFlag) {
graph.on('dblclick', onDbClick);
}

return graph;
}

function resetNodes() {
for (let circle in circleList) {
circleList[circle].removeAllEventListeners();
setNode(circleList[circle], circle, pinEffectFlag);
}
}

/**
* DrawArrowAndEdge method use for update the arrow and edge from node source and target position
* @param {Object} data
* @param {Object} source {x,y}
* @param {Object} target {x,y}
*/
function drawArrowAndEdge(data, source, target) {

// Remove old edge (drawArrowShape will remove old arrow)
if (lineList[data.id]) edgeContainer.removeChild(lineList[data.id]);

// Draw Arrow
let newSourcePos, newTargetPos;
if (data.targetShape) {
if (data.curveStyle) {
switch (data.curveStyle) {
case "bezier":
// CacBezierCurve
let bMidPos = CacBezierCurveMidPos(source, target);
let pos2 = { x: bMidPos.x2, y: bMidPos.y2 }
if (textList[data.id]) updateText(data.id, { x: (bMidPos.x1 + bMidPos.x2) / 2, y: (bMidPos.y1 + bMidPos.y2) / 2 });
newTargetPos = drawArrowShape(data.id, data.targetShape, pos2, target, source, target, true);
break;
let cMidPos = CacQuadraticCurveMidPos(source, target, 100);
newTargetPos = drawArrowShape(data.id, data.targetShape, cMidPos, target, source, target, true);
break;
default: break;
}
} else {
if (textList[data.id]) updateText(data.id, { x: (source.x + target.x) / 2, y: (source.y + target.y) / 2 });
newTargetPos = drawArrowShape(data.id, data.targetShape, source, target, source, target, true);
}

}
if (data.sourceShape) {
if (data.curveStyle) {
switch (data.curveStyle) {
case "bezier":
// Cacular Third Bezier Curve's Mid pos
let bMidPos = CacBezierCurveMidPos(source, target);
let pos1 = { x: bMidPos.x1, y: bMidPos.y1 }

if (textList[data.id]) updateText(data.id, { x: (bMidPos.x1 + bMidPos.x2) / 2, y: (bMidPos.y1 + bMidPos.y2) / 2 });
newSourcePos = drawArrowShape(data.id, data.sourceShape, source, pos1, source, target, false);
break;
// Cacular Second Bezier Curve's Mid pos
let cMidPos = CacQuadraticCurveMidPos(source, target, 100);
newSourcePos = drawArrowShape(data.id, data.sourceShape, source, cMidPos, source, target, false);
break;
default: break;
}
} else {
if (textList[data.id]) updateText(data.id, { x: (source.x + target.x) / 2, y: (source.y + target.y) / 2 });
newSourcePos = drawArrowShape(data.id, data.sourceShape, source, target, source, target, false);
}

}

let tempSourcePos = newSourcePos ? newSourcePos : source;
let tempTargetPos = newTargetPos ? newTargetPos : target;

//Draw edge
let graphics = new Shape();
let line = graphics.graphics;
if (data.lineMode === "dash") {//Todo => param
line.setStrokeStyle(4).setStrokeDash([20, 10], 0).beginStroke("#FFF");
} else {
line.setStrokeStyle(4).beginStroke("#FFF");
}
line.moveTo(tempSourcePos.x, tempSourcePos.y);
if (data.curveStyle) {
switch (data.curveStyle) {
case "bezier":
// Cacular Third Bezier Curve's Mid pos
let cPos = CacBezierCurveMidPos(tempSourcePos, tempTargetPos, 100);
line.bezierCurveTo(cPos.x1, cPos.y1, cPos.x2, cPos.y2, cPos.x, cPos.y);
break;
// Cacular Second Bezier Curve's Mid pos
let bPos = CacQuadraticCurveMidPos(tempSourcePos, tempTargetPos, 100);
break;
}
} else {
line.lineTo(tempTargetPos.x, tempTargetPos.y);
}

lineList[data.id] = graphics;

}

/**
* DrawArrowShape method use for draw the arrow from node source and target position
* @param {String} id arrow's id
* @param {String} shape arrow's shape
* @param {Object} sourcePos {x,y} node source pos
* @param {Object} targetPos {x,y} node target pos
* @param {Object} source {x,y} arrow source pos
* @param {Object} target {x,y} arrow target pos
* @param {Bool} targetFlag  draw source or target arrow
*/
function drawArrowShape(id, shape, sourcePos, targetPos, source, target, targetFlag) {
switch (shape) {
case 'circle':
if (!targetFlag && sourcePos.width) c_nodeRadius = sourcePos.width;
if (targetFlag && targetPos.width) c_nodeRadius = targetPos.width;

//Boundary determination => hide it if stick together
if ((Math.abs(source.y - target.y) < c_nodeRadius * 1.5) &&
(Math.abs(source.x - target.x) < c_nodeRadius * 1.5)) {
if (textList[id]) textList[id].visible = false;
}

let srcPos = targetFlag ? targetPos : sourcePos;
let tgtPos = targetFlag ? sourcePos : targetPos;

let c_angle = Math.atan(Math.abs(srcPos.y - tgtPos.y) / Math.abs(srcPos.x - tgtPos.x))
let circleWidth = c_nodeRadius / 2;
// posX and posY is the circle's final position
let posX = (c_nodeRadius + circleWidth) * Math.cos(c_angle),
posY = (c_nodeRadius + circleWidth) * Math.sin(c_angle);

// Discusses the relative position of target and source
if (srcPos.x > tgtPos.x) {// Source node is right
posX = srcPos.x - posX;
} else {
posX = srcPos.x + posX;
}
if (srcPos.y > tgtPos.y) {// Source node is Up
posY = srcPos.y - posY;
} else {
posY = srcPos.y + posY;
}

//Draw circle
let cGraphics = new Shape();
let circle = cGraphics.graphics;
circle.beginFill("#66CCFF");

circle.drawCircle(0, 0, circleWidth);
circle.endFill();

cGraphics.x = posX;
cGraphics.y = posY;

//updateArrow
updateArrow(id, cGraphics, targetFlag);

return {
x: posX,
y: posY
}

case 'triangle':
//这个三角形默认按顶角为50°，两个底角为65°来算，两边长先按一半nodeWidth来算吧
if (!targetFlag && sourcePos.width) t_nodeRadius = sourcePos.width;
if (targetFlag && targetPos.width) t_nodeRadius = targetPos.width;

//Boundary determination
if ((Math.abs(source.y - target.y) < t_nodeRadius * 1.5) &&
(Math.abs(source.x - target.x) < t_nodeRadius * 1.5)) {
if (textList[id]) textList[id].visible = false;
}

let t_srcPos = targetFlag ? sourcePos : targetPos;
let t_tgtPos = targetFlag ? targetPos : sourcePos;

let topAngle = Math.PI / 180 * 50,//角度转弧度，注意Math的那些方法的单位是弧度
halfBottomEdge = Math.sin(topAngle / 2) * sideEdge,
centerEdge = Math.cos(topAngle / 2) * sideEdge;

//angle是一样的，先按node中心算，arrow中心算之后再说，先todo(直线版看出不这个问题，曲线就崩了)
let angle = Math.atan(Math.abs(t_srcPos.y - t_tgtPos.y) / Math.abs(t_srcPos.x - t_tgtPos.x));
let beginPosX = t_nodeRadius * Math.cos(angle),
pos1X, pos1Y, pos2X, pos2Y,
centerX = (t_nodeRadius + centerEdge) * Math.cos(angle),
centerY = (t_nodeRadius + centerEdge) * Math.sin(angle);

pos1X = pos2X = Math.sin(angle) * halfBottomEdge;
pos1Y = pos2Y = Math.cos(angle) * halfBottomEdge;//简单的几何知识(手动抽搐😖)

//还需要分类讨论target和source的左右位置的各种情况
//1234代表target相对source所在象限
if (t_srcPos.x > t_tgtPos.x) {//source节点在右
if (t_srcPos.y > t_tgtPos.y) {//下 ----> 1
beginPosX = t_tgtPos.x + beginPosX;
beginPosY = t_tgtPos.y + beginPosY;

centerX = t_tgtPos.x + centerX;
centerY = t_tgtPos.y + centerY;

pos1X = centerX + pos1X;
pos1Y = centerY - pos1Y;//+ -

pos2X = centerX - pos2X;
pos2Y = centerY + pos2Y;//- +
} else {//上 ----> 4
beginPosX = t_tgtPos.x + beginPosX;
beginPosY = t_tgtPos.y - beginPosY;

centerX = t_tgtPos.x + centerX;
centerY = t_tgtPos.y - centerY;

pos1X = centerX + pos1X;
pos1Y = centerY + pos1Y;//+ +

pos2X = centerX - pos2X;
pos2Y = centerY - pos2Y;//- -
}

} else {//source节点在左
if (t_srcPos.y > t_tgtPos.y) {//下 ----> 2
beginPosX = t_tgtPos.x - beginPosX;
beginPosY = t_tgtPos.y + beginPosY;

centerX = t_tgtPos.x - centerX;
centerY = t_tgtPos.y + centerY;

pos1X = centerX - pos1X;
pos1Y = centerY - pos1Y;//- -

pos2X = centerX + pos2X;
pos2Y = centerY + pos2Y;//+ +
} else {//上 ----> 3
beginPosX = t_tgtPos.x - beginPosX;
beginPosY = t_tgtPos.y - beginPosY;

centerX = t_tgtPos.x - centerX;
centerY = t_tgtPos.y - centerY;

pos1X = centerX - pos1X;
pos1Y = centerY + pos1Y;//- +

pos2X = centerX + pos2X;
pos2Y = centerY - pos2Y;//+ -
}
}

//Draw triangle
let tGraphics = new Shape();
let triangle = tGraphics.graphics;

triangle.beginFill("#66CCFF");
//triangle.lineStyle(0, 0x66CCFF, 1);
triangle.moveTo(beginPosX, beginPosY);
triangle.lineTo(pos1X, pos1Y);
triangle.lineTo(pos2X, pos2Y);
triangle.endFill();

updateArrow(id, tGraphics, targetFlag);

return {
x: centerX,
y: centerY
}
}
}

/**
* @param {String} id arrow's id
* @param {String} shape arrow's shape
* @param {Bool} targetFlag  source or target arrow
*/
function updateArrow(id, shape, targetFlag) {
if (!arrowList[id]) arrowList[id] = {};
if (!targetFlag) {//Source arrow
if (arrowList[id].sourceArrow) arrowContainer.removeChild(arrowList[id].sourceArrow);
//save newArrow
arrowList[id].sourceArrow = shape;
} else {//Target arrow
if (arrowList[id].targetArrow) arrowContainer.removeChild(arrowList[id].targetArrow);
//save newArrow
arrowList[id].targetArrow = shape;
}
}

/**
* InitEvent method use for init canvas's zoom and drag event
* @param canvas canvas to init
*/
function initEvent(canvas) {

// Scale/Zoom
if (e.deltaY < 0) {
zooming(true, e.pageX, e.pageY);
} else {
zooming(false, e.pageX, e.pageY);
}
});

function zooming(zoomFlag, x, y) {
//Current scale
let scale = stage.scale;
let point = toLocalPos(x, y);
// //Zooming
if (zoomFlag) {
if (scale < SCALE_MAX) {
scale += 0.1;
//moving
stage.x = stage.x - (point.x * 0.1),
stage.y = stage.y - (point.y * 0.1);
}
} else {
if (scale > SCALE_MIN) {
scale -= 0.1;
//moving
stage.x = stage.x - (point.x * -0.1),
stage.y = stage.y - (point.y * -0.1);
}
}
stage.scale = scale;
}

// Drag/Move
let movePosBegin = {};
let startMousePos = {};
let hitArea = new Shape();
let canvasDragging = false;

// Could use bind

let pinEffectButton = document.querySelector('#pin');

function pinEffect() {
pinEffectFlag = !pinEffectFlag;
resetNodes();
}

e.preventDefault();
e.stopPropagation();
showMenu(e.pageX - offsetX, e.pageY - offsetY);
if (pinEffectFlag) {
document.querySelector('#pin span').innerText = '关闭图钉效果';
} else {
document.querySelector('#pin span').innerText = '启用图钉效果';
}
}

function onMouseDown(e) {
document.removeEventListener('mousedown', onMouseDown);
}

}

}

function stagePointerDown(event) {
if (!nodeFlag) {
canvasDragging = true;

movePosBegin.x = stage.x;
movePosBegin.y = stage.y;

startMousePos.x = event.pageX;
startMousePos.y = event.pageY;
//Draw circle
let r = 30 / stage.scale;
drawCircle(startMousePos.x, startMousePos.y, r);
}

}

function stagePointerUp(event) {
canvasDragging = false;
//Remove  circle
if (point.circle) dragContainer.removeChild(point.circle);
}

function stagePointerMove(event) {
if (canvasDragging && !nodeFlag && event.which === 1) {
//Move  circle
let x = event.pageX;
let y = event.pageY;

//Remove  circle first
if (point.circle) dragContainer.removeChild(point.circle);
//Redraw circle
//Current scale
let scale = stage.scale;
let r = 30 / scale;
drawCircle(x, y, r);

let offsetX = x - startMousePos.x,//差值
offsetY = y - startMousePos.y;

stage.x = movePosBegin.x + offsetX;
stage.y = movePosBegin.y + offsetY;//修正差值
}

}
}

function getRandomColor(){
return "#"+("00000"+((Math.random()*16777215+0.5)>>0).toString(16)).slice(-6);
}

function toLocalPos(x, y) {
let localPos = stage.globalToLocal(x - offsetX, y - offsetY);
return localPos;
}

function drawCircle(x, y, r = 30) {
//Draw circle
let cGraphics = new Shape();
let circle = cGraphics.graphics;
circle.beginFill(Graphics.getRGB(0, 0, 0, 0.2));//alpha

circle.drawCircle(0, 0, r);
circle.endFill();

let localPos = toLocalPos(x, y);
cGraphics.x = localPos.x;
cGraphics.y = localPos.y;

point.circle = cGraphics;
}
function autoGenera(nodeNum){
let resObj = {},nodes = [],edges = [];
for(let i=0;i<nodeNum;i++){
let data = {};
//Ascii => A => 65
data.id = String.fromCharCode(65+i);
data.width = (Math.random()*30)+20;
data.text = data.id;
data.textOpts = {disY:data.width+20};
if(i%2===0){
data.aura = "HR"
}else{
data.aura = "Sales"
}
// if(i%2===0)data.color="#9f5f9f";
if(i%2===0){
data.imageSrc = "http://ouib5enmf.bkt.clouddn.com/bz.jpeg"
}else{
data.imageSrc = "http://ouib5enmf.bkt.clouddn.com/LG.png"
}
nodes.push(data);
}
for(let i=0;i<nodeNum-1;i++){
let data = {};
data.id = String.fromCharCode(65+nodeNum+i);
data.source = nodes[Math.floor(Math.sqrt(i))].id;
data.target = nodes[i + 1].id;
data.text = data.id;
data.textOpts = {color:'#000',outline:2}
//if(i%2==0)data.curveStyle = 'bezier'
if(i%3==0)data.lineMode = 'dash'
if(i%2==0){
data.targetShape='triangle';
data.sourceShape='circle';
}else{
data.targetShape='circle';
data.sourceShape='triangle';
}
edges.push(data);
}
resObj.nodes = nodes;
resObj.edges = edges;
return resObj;
}
CreateCharts({
container: document.getElementById('testCanvas'),
elements: autoGenera(20)

})```
```<div id="container">
<canvas id="testCanvas"></canvas>
</button>
</li>
</button>
</li>
</button>
</button>
</li>
</button>
</button>
</li>
</li>
</li>
</div>
<script src="https://code.createjs.com/1.0.0/easeljs.min.js"></script>
<script src="//d3js.org/d3.v4.min.js"></script>```
```/* Menu */

position: absolute;
width: 200px;
top: 0;
margin: 0;
border: 1px solid #bbb;
background: #eee;
background: -webkit-linear-gradient(to bottom, #fff 0%, #e5e5e5 100px, #e5e5e5 100%);
background: linear-gradient(to bottom, #fff 0%, #e5e5e5 100px, #e5e5e5 100%);
z-index: 100;
opacity: 0;
-webkit-transform: translate(0, 15px) scale(.95);
transform: translate(0, 15px) scale(.95);
transition: transform 0.1s ease-out, opacity 0.1s ease-out;
pointer-events: none;
}

display: block;
position: relative;
margin: 0;
white-space: nowrap;
}

display: block;
color: #444;
font-family: 'Roboto', sans-serif;
font-size: 13px;
cursor: pointer;
border: 1px solid transparent;
white-space: nowrap;
}

background: none;
line-height: normal;
overflow: visible;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
width: 100%;
text-align: left;
}

outline: 0 none;
text-decoration: none;
}

margin-left: 25px;
}

position: absolute;
left: 8px;
top: 50%;
-webkit-transform: translateY(-50%);
transform: translateY(-50%);
}

color: #fff;
outline: none;
background-color: #2E3940;
border: 1px solid #2E3940;
}

opacity: .5;
pointer-events: none;
}

cursor: default;
}

display:block;
margin: 7px 5px;
height:1px;
border-bottom: 1px solid #fff;
background-color: #aaa;
}

content: "";
position: absolute;
right: 6px;
top: 50%;
-webkit-transform: translateY(-50%);
transform: translateY(-50%);
border: 5px solid transparent;
border-left-color: #808080;
}

border-left-color: #fff;
}

top: 4px;
left: 99%;
}