├── .gitignore ├── README.md ├── dist └── js-diagram-chart.js ├── example.png ├── example └── index.html ├── package-lock.json ├── package.json ├── src ├── helper │ └── index.js └── main.js └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/node 3 | # Edit at https://www.gitignore.io/?templates=node 4 | 5 | ### Node ### 6 | # Logs 7 | logs 8 | *.log 9 | npm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # Optional npm cache directory 48 | .npm 49 | 50 | # Optional eslint cache 51 | .eslintcache 52 | 53 | # Optional REPL history 54 | .node_repl_history 55 | 56 | # Output of 'npm pack' 57 | *.tgz 58 | 59 | # Yarn Integrity file 60 | .yarn-integrity 61 | 62 | # dotenv environment variables file 63 | .env 64 | .env.test 65 | 66 | # parcel-bundler cache (https://parceljs.org/) 67 | .cache 68 | 69 | # next.js build output 70 | .next 71 | 72 | # nuxt.js build output 73 | .nuxt 74 | 75 | # vuepress build output 76 | .vuepress/dist 77 | 78 | # Serverless directories 79 | .serverless/ 80 | 81 | # FuseBox cache 82 | .fusebox/ 83 | 84 | # DynamoDB Local files 85 | .dynamodb/ 86 | 87 | # End of https://www.gitignore.io/api/node -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # js-diagram-chart 2 | 3 | It is a simple diagram chart based on hierarchy, developed in Javascript! 4 | 5 | ![Example](https://github.com/antuane/js-diagram-chart/raw/master/example.png) 6 | 7 | ## How to use 8 | 9 | ```shell 10 | $ npm run build 11 | $ ./dist/js-diagram-chart.js 12 | ``` 13 | 14 | ## Installation 15 | 16 | Using npm: 17 | 18 | ```shell 19 | $ npm i --save js-diagram-chart 20 | ``` 21 | 22 | In Node.js: 23 | 24 | ```js 25 | 26 | let JsDiagramChart = require("js-diagram-chart").default; 27 | 28 | // Or 29 | 30 | import JsDiagramChart from "js-diagram-chart"; 31 | 32 | ``` 33 | 34 | ## Live Example 35 | 36 | http://antuane.github.io/js-diagram-chart/example/index.html 37 | 38 | ## Code Example 39 | 40 | Example in HTML: 41 | 42 | ```html 43 | 44 | Canvas not supported! :( 45 | 46 | ``` 47 | 48 | Example in JS: 49 | 50 | Modules: 51 | 52 | ```js 53 | 54 | import JsDiagramChart from "js-diagram-chart"; 55 | 56 | //OR 57 | 58 | const JsDiagramChart = require('js-diagram-chart').default; 59 | 60 | ``` 61 | 62 | Script import: 63 | 64 | ```html 65 | 66 | ``` 67 | 68 | Example Data: 69 | 70 | ```js 71 | 72 | const dataExample = { 73 | diagrams: [ 74 | { id: 1, text: "DIAGRAM 01", color: "#999999", bgColor: "#330000" }, 75 | { id: 2, text: "DIAGRAM 02", color: "#999999", bgColor: "#003300" }, 76 | { id: 3, text: "DIAGRAM 03", color: "#999999", bgColor: "#000033" }, 77 | { id: 4, text: "DIAGRAM 04", color: "#999999", bgColor: "#333300" }, 78 | { id: 5, text: "DIAGRAM 05", color: "#999999", bgColor: "#003333" }, 79 | { id: 6, text: "DIAGRAM 06", color: "#999999", bgColor: "#330033" }, 80 | { id: 7, text: "DIAGRAM 07", color: "#999999", bgColor: "#000000" }, 81 | { id: 8, text: "DIAGRAM 08", color: "#999999", bgColor: "#000000" }, 82 | { id: 9, text: "DIAGRAM 09", color: "#999999", bgColor: "#000000" }, 83 | { id: 10, text: "DIAGRAM 10", color: "#999999", bgColor: "#333333" } 84 | ], 85 | links: [ 86 | { source: 2, parent: 1 }, 87 | { source: 3, parent: 1 }, 88 | { source: 4, parent: 3 }, 89 | { source: 5, parent: 3 }, 90 | { source: 6, parent: 2 }, 91 | { source: 9, parent: 9 }, 92 | { source: 9, parent: 6 } 93 | ], 94 | config: { 95 | element: "mycanvas", 96 | margin: 30, 97 | padding: 10, 98 | width: 100, 99 | height: 50, 100 | radius: 3, 101 | hiddenBg: false, 102 | arrowWidth: 8, 103 | lineWidth: 2, 104 | lineDiff: true, 105 | fontFamily: "Arial", 106 | fontSize: 12, 107 | autoSize: true, 108 | mouseEvents: true 109 | //lineColor: "#000000", // OPTIONAL 110 | } 111 | }; 112 | 113 | let chart = new JsDiagramChart(dataExample); 114 | 115 | // window.addEventListener("resize", function() { chart = new JsDiagramChart(dataExample) }); 116 | 117 | // OTHERS METHODS 118 | 119 | // chart.addZoom(-5); 120 | // chart.resetZoom(); 121 | // chart.update(dataExample); 122 | 123 | ``` -------------------------------------------------------------------------------- /dist/js-diagram-chart.js: -------------------------------------------------------------------------------- 1 | !function(t){var i={};function n(o){if(i[o])return i[o].exports;var s=i[o]={i:o,l:!1,exports:{}};return t[o].call(s.exports,s,s.exports,n),s.l=!0,s.exports}n.m=t,n.c=i,n.d=function(t,i,o){n.o(t,i)||Object.defineProperty(t,i,{enumerable:!0,get:o})},n.r=function(t){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},n.t=function(t,i){if(1&i&&(t=n(t)),8&i)return t;if(4&i&&"object"==typeof t&&t&&t.__esModule)return t;var o=Object.create(null);if(n.r(o),Object.defineProperty(o,"default",{enumerable:!0,value:t}),2&i&&"string"!=typeof t)for(var s in t)n.d(o,s,function(i){return t[i]}.bind(null,s));return o},n.n=function(t){var i=t&&t.__esModule?function(){return t.default}:function(){return t};return n.d(i,"a",i),i},n.o=function(t,i){return Object.prototype.hasOwnProperty.call(t,i)},n.p="",n(n.s=0)}([function(t,i,n){"use strict";function o(t,i,n,o,s,e,h){t.beginPath(),t.moveTo(i+e,n),t.lineTo(i+o-e,n),t.quadraticCurveTo(i+o,n,i+o,n+e),t.lineTo(i+o,n+s-e),t.quadraticCurveTo(i+o,n+s,i+o-e,n+s),t.lineTo(i+e,n+s),t.quadraticCurveTo(i,n+s,i,n+s-e),t.lineTo(i,n+e),t.quadraticCurveTo(i,n,i+e,n),t.closePath(),t.stroke(),h||t.fill()}function s(t,i,n,o,s,e,h){t.textAlign="center",t.textBaseline="middle",n+=s/2,o+=e/2;for(var a=i.split("\n"),c=0;cs?(r=g[f]+" ",o-=h/2):r=l}r="";for(var d=0;ds?(t.fillText(r,n,o),r=g[d]+" ",o+=h):r=m}t.fillText(r,n,o),o+=h}}function e(t,i){for(var n=0;no&&(o=l.count)):i[f].orphan||(n.push({id:i[f].y,count:1}),i[f].x=1)}this.config.linesCount=n,this.config.columnsCount=o,this.config.columnLower=s,this.diagrams=i},this.draw=function(){if(this.context.transformedPoint){var t=this.context.transformedPoint(0,0),i=this.context.transformedPoint(this.canvas.width,this.canvas.height);this.context.clearRect(t.x,t.y,i.x-t.x,i.y-t.y)}else this.context.clearRect(0,0,this.canvas.width,this.canvas.height);var n=this.config.width+2*this.config.margin;0==this.config.columnsCount&&(this.config.columnsCount=parseInt(this.canvas.width/n));var h=this.config.columnsCount*n,a=parseInt(this.canvas.width)/2-h/2;this.context.rotate(2*Math.PI);for(var c=0,r=0;r1&&(W=!0),this.context.setLineDash&&this.context.setLineDash([this.config.lineWidth,0]),this.context.strokeStyle=this.config.lineColor||this.diagrams[p].bgColor,this.context.fillStyle=this.config.lineColor||this.diagrams[p].bgColor;var C=0;b?(C=this.config.margin/2-this.config.margin/this.config.columnsCount*this.diagrams[p].x,isFinite(C)||(C=0),this.context.beginPath(),this.context.moveTo(y.left+this.config.width/2+-1*C-C,y.top+this.config.height),this.context.lineTo(y.left+this.config.width/2+-1*C-C,y.top+this.config.height+this.config.margin/2.5),this.context.lineTo(y.left+this.config.width/2+-1*C+C,y.top+this.config.height+this.config.margin/2.5),this.context.lineTo(y.left+this.config.width/2+-1*C+C,y.top+this.config.height+this.config.arrowWidth/2),this.context.stroke(),this.context.beginPath(),this.context.moveTo(y.left+this.config.width/2+-1*C+C,y.top+this.config.height),this.context.lineTo(y.left+this.config.width/2-this.config.arrowWidth/2+-1*C+C,y.top+this.config.height+this.config.arrowWidth),this.context.lineTo(y.left+this.config.width/2+this.config.arrowWidth/2+-1*C+C,y.top+this.config.height+this.config.arrowWidth),this.context.fill()):T?(this.config.lineDiff&&(C=this.config.margin/2-this.config.margin/this.config.columnsCount*this.diagrams[p].x+(this.config.margin-(this.config.arrowWidth+2*this.config.lineWidth*0))),this.context.setLineDash&&this.context.setLineDash([this.config.lineWidth,this.config.lineWidth]),this.context.beginPath(),this.context.moveTo(this.diagrams[p].left+this.config.width/2+C,this.diagrams[p].top+this.config.height/2+C),this.context.lineTo(this.diagrams[p].left+this.config.width+this.config.margin+C,this.diagrams[p].top+this.config.height/2+C),this.context.lineTo(this.diagrams[p].left+this.config.width+this.config.margin+C,y.top-this.config.margin+C),this.context.lineTo(y.left+this.config.width/2+C,y.top-this.config.margin+C),this.context.lineTo(y.left+this.config.width/2+C,y.top-this.config.arrowWidth+C),this.context.lineWidth=this.config.lineWidth,this.context.stroke(),this.context.beginPath(),this.context.moveTo(y.left+this.config.width/2+C,y.top),this.context.lineTo(y.left+this.config.width/2-this.config.arrowWidth/2+C,y.top-this.config.arrowWidth),this.context.lineTo(y.left+this.config.width/2+this.config.arrowWidth/2+C,y.top-this.config.arrowWidth),this.context.fill()):W?(this.config.lineDiff&&(C=this.config.margin/2-this.config.margin/this.config.columnsCount*this.diagrams[p].x+(this.config.margin-(this.config.arrowWidth+2*this.config.lineWidth*S))),S++,isFinite(C)||(C=0),this.context.setLineDash&&this.context.setLineDash([this.config.lineWidth,this.config.lineWidth]),this.context.beginPath(),this.context.moveTo(this.diagrams[p].left+this.config.width/2+-1*C,this.diagrams[p].top),this.context.lineTo(this.diagrams[p].left+this.config.width/2+-1*C,this.diagrams[p].top-this.config.margin+C),this.context.lineTo(y.left+this.config.width/2+-1*C,this.diagrams[p].top-this.config.margin+C),this.context.lineTo(y.left+this.config.width/2+-1*C,y.top+this.config.height+this.config.margin+C),this.context.lineTo(y.left+this.config.width/2+-1*C,y.top+this.config.height+this.config.arrowWidth/2),this.context.lineWidth=this.config.lineWidth,this.context.stroke(),this.context.beginPath(),this.context.moveTo(y.left+this.config.width/2+-1*C,y.top+this.config.height),this.context.lineTo(y.left+this.config.width/2-this.config.arrowWidth/2+-1*C,y.top+this.config.height+this.config.arrowWidth),this.context.lineTo(y.left+this.config.width/2+this.config.arrowWidth/2+-1*C,y.top+this.config.height+this.config.arrowWidth),this.context.fill()):(this.config.lineDiff&&(C=this.config.margin/2-this.config.margin/this.config.columnsCount*this.diagrams[p].x),isFinite(C)||(C=0),this.context.beginPath(),this.context.moveTo(this.diagrams[p].left+this.config.width/2+-1*C,this.diagrams[p].top),this.context.lineTo(this.diagrams[p].left+this.config.width/2+-1*C,this.diagrams[p].top-this.config.margin+C),this.context.lineTo(y.left+this.config.width/2+-1*C,y.top+this.config.height+this.config.margin+C),this.context.lineTo(y.left+this.config.width/2+-1*C,y.top+this.config.height+this.config.arrowWidth/2),this.context.lineWidth=this.config.lineWidth,this.context.stroke(),this.context.beginPath(),this.context.moveTo(y.left+this.config.width/2+-1*C,y.top+this.config.height),this.context.lineTo(y.left+this.config.width/2-this.config.arrowWidth/2+-1*C,y.top+this.config.height+this.config.arrowWidth),this.context.lineTo(y.left+this.config.width/2+this.config.arrowWidth/2+-1*C,y.top+this.config.height+this.config.arrowWidth),this.context.fill())}for(var P=0;P 2 | 3 | 4 | 5 | 6 | 7 | 12 | Antuane Solutions 13 | 22 | 23 | 24 | NotSupport 25 | 26 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js-diagram-chart", 3 | "description": "It is a simple prototype diagram chart based on hierarchy", 4 | "version": "1.0.0", 5 | "main": "src/main.js", 6 | "scripts": { 7 | "watch": "webpack --watch", 8 | "start": "webpack-dev-server --open", 9 | "build": "webpack --prod" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/antuane/js-diagram-chart.git" 14 | }, 15 | "keywords": [ 16 | "antuane", 17 | "chart", 18 | "diagram", 19 | "javascript" 20 | ], 21 | "author": "Diogenes Silveira", 22 | "license": "ISC", 23 | "bugs": { 24 | "url": "https://github.com/antuane/js-diagram-chart/issues" 25 | }, 26 | "homepage": "https://github.com/antuane/js-diagram-chart/antuane-chart#readme", 27 | "devDependencies": { 28 | "@babel/core": "^7.1.0", 29 | "@babel/preset-env": "^7.1.0", 30 | "babel-loader": "^8.0.2", 31 | "clean-webpack-plugin": "^0.1.19", 32 | "webpack": "^4.19.1", 33 | "webpack-cli": "^3.1.1" 34 | }, 35 | "dependencies": { 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/helper/index.js: -------------------------------------------------------------------------------- 1 | 2 | /*global window*/ 3 | /*global document*/ 4 | 5 | export function getRandomInt(min, max) { 6 | return Math.floor(Math.random() * (max - min)) + min; 7 | } 8 | 9 | export function roundRect(context, x, y, width, height, radius, hiddenBg) { 10 | context.beginPath(); 11 | context.moveTo(x + radius, y); 12 | context.lineTo(x + width - radius, y); 13 | context.quadraticCurveTo(x + width, y, x + width, y + radius); 14 | context.lineTo(x + width, y + height - radius); 15 | context.quadraticCurveTo( 16 | x + width, 17 | y + height, 18 | x + width - radius, 19 | y + height 20 | ); 21 | context.lineTo(x + radius, y + height); 22 | context.quadraticCurveTo(x, y + height, x, y + height - radius); 23 | context.lineTo(x, y + radius); 24 | context.quadraticCurveTo(x, y, x + radius, y); 25 | context.closePath(); 26 | context.stroke(); 27 | if (!hiddenBg) { 28 | context.fill(); 29 | } 30 | } 31 | 32 | export function wrapText(context, text, x, y, width, height, lineHeight) { 33 | context.textAlign = "center"; 34 | context.textBaseline = "middle"; 35 | x = x + width / 2; 36 | y = y + height / 2; 37 | 38 | let cars = text.split("\n"); 39 | 40 | for (let ii = 0; ii < cars.length; ii++) { 41 | let line = ""; 42 | let words = cars[ii].split(" "); 43 | 44 | for (let n = 0; n < words.length; n++) { 45 | let testLine = line + words[n] + " "; 46 | let metrics = context.measureText(testLine); 47 | let testWidth = metrics.width; 48 | 49 | if (testWidth > width) { 50 | line = words[n] + " "; 51 | y -= lineHeight / 2; 52 | } else { 53 | line = testLine; 54 | } 55 | } 56 | 57 | line = ""; 58 | 59 | for (let n = 0; n < words.length; n++) { 60 | let testLine = line + words[n] + " "; 61 | let metrics = context.measureText(testLine); 62 | let testWidth = metrics.width; 63 | 64 | if (testWidth > width) { 65 | context.fillText(line, x, y); 66 | line = words[n] + " "; 67 | y += lineHeight; 68 | } else { 69 | line = testLine; 70 | } 71 | } 72 | 73 | context.fillText(line, x, y); 74 | y += lineHeight; 75 | } 76 | } 77 | 78 | export function colorLuminance(hex, lum) { 79 | hex = String(hex).replace(/[^0-9a-f]/gi, ""); 80 | if (hex.length < 6) { 81 | hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2]; 82 | } 83 | lum = lum || 0; 84 | let rgb = "#", 85 | c, 86 | i; 87 | for (i = 0; i < 3; i++) { 88 | c = parseInt(hex.substr(i * 2, 2), 16); 89 | c = Math.round(Math.min(Math.max(0, c + c * lum), 255)).toString(16); 90 | rgb += ("00" + c).substr(c.length); 91 | } 92 | return rgb; 93 | } 94 | 95 | export function cloneObj(obj) { 96 | if (obj == null || typeof obj != "object") return obj; 97 | 98 | let temp = obj.constructor(); 99 | 100 | for (let key in obj) { 101 | if (obj.hasOwnProperty(key)) { 102 | temp[key] = cloneObj(obj[key]); 103 | } 104 | } 105 | return temp; 106 | } 107 | 108 | export function trackTransforms(ctx) { 109 | let svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); 110 | let xform = svg.createSVGMatrix(); 111 | ctx.getTransform = function() { 112 | return xform; 113 | }; 114 | 115 | let savedTransforms = []; 116 | let save = ctx.save; 117 | ctx.save = function() { 118 | savedTransforms.push(xform.translate(0, 0)); 119 | return save.call(ctx); 120 | }; 121 | let restore = ctx.restore; 122 | ctx.restore = function() { 123 | xform = savedTransforms.pop(); 124 | return restore.call(ctx); 125 | }; 126 | 127 | let scale = ctx.scale; 128 | ctx.scale = function(sx, sy) { 129 | xform = xform.scaleNonUniform(sx, sy); 130 | return scale.call(ctx, sx, sy); 131 | }; 132 | let rotate = ctx.rotate; 133 | ctx.rotate = function(radians) { 134 | xform = xform.rotate((radians * 180) / Math.PI); 135 | return rotate.call(ctx, radians); 136 | }; 137 | let translate = ctx.translate; 138 | ctx.translate = function(dx, dy) { 139 | xform = xform.translate(dx, dy); 140 | return translate.call(ctx, dx, dy); 141 | }; 142 | let transform = ctx.transform; 143 | ctx.transform = function(a, b, c, d, e, f) { 144 | let m2 = svg.createSVGMatrix(); 145 | m2.a = a; 146 | m2.b = b; 147 | m2.c = c; 148 | m2.d = d; 149 | m2.e = e; 150 | m2.f = f; 151 | xform = xform.multiply(m2); 152 | return transform.call(ctx, a, b, c, d, e, f); 153 | }; 154 | let setTransform = ctx.setTransform; 155 | ctx.setTransform = function(a, b, c, d, e, f) { 156 | xform.a = a; 157 | xform.b = b; 158 | xform.c = c; 159 | xform.d = d; 160 | xform.e = e; 161 | xform.f = f; 162 | return setTransform.call(ctx, a, b, c, d, e, f); 163 | }; 164 | let pt = svg.createSVGPoint(); 165 | ctx.transformedPoint = function(x, y) { 166 | pt.x = x; 167 | pt.y = y; 168 | return pt.matrixTransform(xform.inverse()); 169 | }; 170 | } 171 | 172 | export function getObjById(listObj, idObj) { 173 | for (let i = 0; i < listObj.length; i++) { 174 | if (listObj[i].id == idObj) { 175 | return listObj[i]; 176 | } 177 | } 178 | return null; 179 | } 180 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /*global window*/ 4 | /*global document*/ 5 | 6 | import { getObjById, roundRect, trackTransforms, wrapText } from "./helper"; 7 | 8 | function JsDiagramChart(data) { 9 | this.init = function(data) { 10 | let _this = this; 11 | _this.canvas = document.getElementById(data.config.element); 12 | 13 | if (_this.canvas) { 14 | _this.context = _this.canvas.getContext("2d"); 15 | } else { 16 | throw "Element not found"; 17 | } 18 | 19 | _this.context = this.canvas.getContext("2d"); 20 | _this.context.lineWidth = data.config.lineWidth; 21 | 22 | if (data.config.autoSize) { 23 | this.canvas.width = this.canvas.parentNode.clientWidth; 24 | this.canvas.height = this.canvas.parentNode.clientHeight; 25 | } 26 | 27 | trackTransforms(this.context); 28 | 29 | data.config.scaleFactor = 1.1; 30 | data.config.zoomScale = 0; 31 | data.config.moveX = 0; 32 | data.config.moveY = 0; 33 | 34 | //this.data = data; 35 | this.config = data.config; 36 | this.update(data); 37 | this.draw(); 38 | 39 | if (_this.config.mouseEvents) { 40 | this.events(); 41 | } 42 | }; 43 | 44 | this.addZoom = function(size) { 45 | let _this = this; 46 | _this.config.zoomScale += size; 47 | let pt = _this.context.transformedPoint( 48 | _this.canvas.width / 2, 49 | _this.canvas.height / 2 50 | ); 51 | this.context.translate(pt.x, pt.y); 52 | let factor = Math.pow(_this.config.scaleFactor, size); 53 | _this.context.scale(factor, factor); 54 | _this.context.translate(-pt.x, -pt.y); 55 | this.draw(); 56 | }; 57 | 58 | this.resetZoom = function() { 59 | let _this = this; 60 | _this.addZoom(-_this.config.zoomScale); 61 | _this.context.translate(-_this.config.moveX, -_this.config.moveY); 62 | _this.config.zoomScale = 0; 63 | _this.config.moveX = 0; 64 | _this.config.moveY = 0; 65 | }; 66 | 67 | this.events = function() { 68 | let _this = this; 69 | 70 | let lastX = _this.canvas.width / 2; 71 | let lastY = _this.canvas.height / 2; 72 | let dragStart = false; 73 | let dragged = false; 74 | 75 | _this.canvas.addEventListener( 76 | "mousedown", 77 | function(evt) { 78 | document.body.style.mozUserSelect = document.body.style.webkitUserSelect = document.body.style.userSelect = 79 | "none"; 80 | _this.canvas.style.cursor = "move"; 81 | lastX = evt.offsetX || evt.pageX - _this.canvas.offsetLeft; 82 | lastY = evt.offsetY || evt.pageY - _this.canvas.offsetTop; 83 | dragStart = _this.context.transformedPoint(lastX, lastY); 84 | dragged = false; 85 | evt.returnValue = false; 86 | }, 87 | false 88 | ); 89 | 90 | _this.canvas.addEventListener( 91 | "mousemove", 92 | function(evt) { 93 | lastX = evt.offsetX || evt.pageX - _this.canvas.offsetLeft; 94 | lastY = evt.offsetY || evt.pageY - _this.canvas.offsetTop; 95 | 96 | dragged = true; 97 | if (dragStart) { 98 | let pt = _this.context.transformedPoint(lastX, lastY); 99 | _this.context.translate(pt.x - dragStart.x, pt.y - dragStart.y); 100 | _this.draw(); 101 | _this.config.moveX += pt.x - dragStart.x; 102 | _this.config.moveY += pt.y - dragStart.y; 103 | } 104 | }, 105 | false 106 | ); 107 | 108 | _this.canvas.addEventListener( 109 | "mouseup", 110 | function(evt) { 111 | dragStart = null; 112 | _this.canvas.style.cursor = "default"; 113 | //if (!dragged) zoom(evt.shiftKey ? -1 : 1 ); 114 | }, 115 | false 116 | ); 117 | 118 | let handleScroll = function(evt) { 119 | let delta = evt.wheelDelta 120 | ? evt.wheelDelta / 300 121 | : evt.detail 122 | ? -evt.detail 123 | : 0; 124 | _this.addZoom(delta); 125 | return evt.preventDefault() && false; 126 | }; 127 | 128 | _this.canvas.addEventListener("DOMMouseScroll", handleScroll, false); 129 | _this.canvas.addEventListener("mousewheel", handleScroll, false); 130 | }; 131 | 132 | this.update = function(data) { 133 | // if(data){ 134 | // data = this.data; 135 | // } 136 | 137 | let _this = this; 138 | //_this.data = data; 139 | 140 | let diagramList = []; 141 | let diagramLinesCount = []; 142 | let diagramColumnsCount = 0; 143 | let diagramColumnLower = 0; 144 | 145 | for (let i = 0; i < data.diagrams.length; i++) { 146 | let objectDiagram = { 147 | id: data.diagrams[i].id, 148 | x: 0, 149 | y: 0, 150 | cx: 0, 151 | cy: 0, 152 | text: data.diagrams[i].text, 153 | color: data.diagrams[i].color, 154 | bgColor: data.diagrams[i].bgColor, 155 | parents: [], 156 | children: [], 157 | virgin: true, 158 | orphan: true 159 | }; 160 | 161 | for (let j = 0; j < data.links.length; j++) { 162 | if (data.links[j].source == objectDiagram.id) { 163 | objectDiagram.parents.push(data.links[j].parent); 164 | objectDiagram.orphan = false; 165 | } 166 | 167 | if (data.links[j].parent == objectDiagram.id) { 168 | objectDiagram.children.push(data.links[j].source); 169 | objectDiagram.orphan = false; 170 | } 171 | } 172 | 173 | diagramList.push(objectDiagram); 174 | } 175 | 176 | let getLinesObjects = function getLinesObjects(object, hierarchy) { 177 | if (object.virgin) { 178 | object.virgin = false; 179 | object.y = hierarchy; 180 | 181 | if (hierarchy < diagramColumnLower) { 182 | diagramColumnLower = hierarchy; 183 | } 184 | 185 | for (let i = 0; i < object.children.length; i++) { 186 | let tmpObj = getObjById(diagramList, object.children[i]); 187 | getLinesObjects(tmpObj, hierarchy + 1); 188 | } 189 | 190 | for (let i = 0; i < object.parents.length; i++) { 191 | let tmpObj = getObjById(diagramList, object.parents[i]); 192 | getLinesObjects(tmpObj, hierarchy - 1); 193 | } 194 | } 195 | }; 196 | 197 | for (let i = 0; i < diagramList.length; i++) { 198 | getLinesObjects(diagramList[i], 0); 199 | } 200 | 201 | for (let i = 0; i < diagramList.length; i++) { 202 | let countObjTmp = getObjById(diagramLinesCount, diagramList[i].y); 203 | if (countObjTmp != null) { 204 | if (!diagramList[i].orphan) { 205 | countObjTmp.count++; 206 | diagramList[i].x = countObjTmp.count; 207 | if (countObjTmp.count > diagramColumnsCount) { 208 | diagramColumnsCount = countObjTmp.count; 209 | } 210 | } 211 | } else { 212 | if (!diagramList[i].orphan) { 213 | diagramLinesCount.push({ id: diagramList[i].y, count: 1 }); 214 | diagramList[i].x = 1; 215 | } 216 | } 217 | } 218 | 219 | this.config.linesCount = diagramLinesCount; 220 | this.config.columnsCount = diagramColumnsCount; 221 | this.config.columnLower = diagramColumnLower; 222 | this.diagrams = diagramList; 223 | }; 224 | 225 | this.draw = function() { 226 | let _this = this; 227 | 228 | if (_this.context.transformedPoint) { 229 | let p1 = _this.context.transformedPoint(0, 0); 230 | let p2 = _this.context.transformedPoint( 231 | _this.canvas.width, 232 | _this.canvas.height 233 | ); 234 | _this.context.clearRect(p1.x, p1.y, p2.x - p1.x, p2.y - p1.y); 235 | } else { 236 | _this.context.clearRect(0, 0, _this.canvas.width, _this.canvas.height); 237 | } 238 | 239 | let diagramWithMargin = _this.config.width + 2 * _this.config.margin; 240 | 241 | if (_this.config.columnsCount == 0) { 242 | _this.config.columnsCount = parseInt( 243 | _this.canvas.width / diagramWithMargin 244 | ); 245 | } 246 | 247 | let maxWidth = _this.config.columnsCount * diagramWithMargin; 248 | let marginScreen = parseInt(_this.canvas.width) / 2 - maxWidth / 2; 249 | 250 | _this.context.rotate(2 * Math.PI); 251 | 252 | let countOrphanDemand = 0; 253 | for (let i = 0; i < _this.diagrams.length; i++) { 254 | if (_this.diagrams[i].orphan) { 255 | let positionX = 256 | marginScreen + 257 | _this.config.margin + 258 | parseInt(countOrphanDemand % _this.config.columnsCount) * 259 | (_this.config.width + 2 * _this.config.margin); 260 | let positionY = 261 | _this.config.margin + 262 | parseInt(countOrphanDemand / _this.config.columnsCount) * 263 | (_this.config.height + 2 * _this.config.margin); 264 | _this.context.fillStyle = _this.diagrams[i].bgColor; 265 | _this.context.strokeStyle = _this.diagrams[i].bgColor; 266 | roundRect( 267 | _this.context, 268 | positionX, 269 | positionY, 270 | _this.config.width, 271 | _this.config.height, 272 | _this.config.radius, 273 | _this.config.hiddenBg 274 | ); 275 | // 276 | let nodeText = _this.context; 277 | nodeText.fillStyle = _this.diagrams[i].color; 278 | nodeText.font = _this.config.fontSize + "px " + _this.config.fontFamily; 279 | wrapText( 280 | _this.context, 281 | _this.diagrams[i].text, 282 | positionX + _this.config.padding, 283 | positionY, 284 | _this.config.width - _this.config.padding * 2, 285 | _this.config.height, 286 | _this.config.fontSize + _this.config.fontSize * 0.2 287 | ); 288 | 289 | countOrphanDemand++; 290 | } 291 | } 292 | 293 | //CALCULATE X Y NOT ORPHANS DIAGRAMS 294 | 295 | let marginTopNotOrphans = 296 | _this.config.height + 297 | 3 * _this.config.margin + 298 | parseInt((countOrphanDemand - 1) / _this.config.columnsCount) * 299 | (_this.config.height + 2 * _this.config.margin); 300 | 301 | if (isNaN(marginTopNotOrphans)) { 302 | marginTopNotOrphans = _this.config.margin; 303 | } 304 | 305 | for (let i = 0; i < _this.diagrams.length; i++) { 306 | if (!_this.diagrams[i].orphan) { 307 | let maxColumnsInLine = getObjById( 308 | _this.config.linesCount, 309 | _this.diagrams[i].y 310 | ).count; 311 | let ratio = maxWidth / maxColumnsInLine; 312 | let positionX = 313 | marginScreen + 314 | ratio * _this.diagrams[i].x + 315 | (ratio / 2 - _this.config.width / 2) - 316 | ratio; 317 | let positionY = 318 | marginTopNotOrphans + 319 | Math.abs(_this.config.columnLower) * 320 | (_this.config.height + 2 * _this.config.margin) + 321 | _this.diagrams[i].y * (_this.config.height + 2 * _this.config.margin); 322 | _this.diagrams[i].left = positionX; 323 | _this.diagrams[i].top = positionY; 324 | } 325 | } 326 | 327 | //DRAW LINES 328 | 329 | for (let i = 0; i < _this.diagrams.length; i++) { 330 | if (!_this.diagrams[i].orphan) { 331 | for (let j = 0; j < _this.diagrams[i].parents.length; j++) { 332 | let tmpObj = getObjById(_this.diagrams, _this.diagrams[i].parents[j]); 333 | let invertParent = false; 334 | let isDistant = false; 335 | let autoRef = false; 336 | let countDistant = 0; 337 | let countInvertParent = 0; 338 | 339 | if (_this.diagrams[i].id == tmpObj.id) { 340 | autoRef = true; 341 | } else if (_this.diagrams[i].y < tmpObj.y) { 342 | invertParent = true; 343 | } else if (_this.diagrams[i].y - tmpObj.y > 1) { 344 | isDistant = true; 345 | } 346 | 347 | if (_this.context.setLineDash) { 348 | _this.context.setLineDash([_this.config.lineWidth, 0]); 349 | } 350 | 351 | _this.context.strokeStyle = 352 | _this.config.lineColor || _this.diagrams[i].bgColor; 353 | _this.context.fillStyle = 354 | _this.config.lineColor || _this.diagrams[i].bgColor; 355 | 356 | let difference = 0; 357 | 358 | if (autoRef) { 359 | difference = 360 | _this.config.margin / 2 - 361 | (_this.config.margin / _this.config.columnsCount) * 362 | _this.diagrams[i].x; 363 | 364 | if (!isFinite(difference)) { 365 | difference = 0; 366 | } 367 | 368 | _this.context.beginPath(); 369 | _this.context.moveTo( 370 | tmpObj.left + 371 | _this.config.width / 2 + 372 | difference * -1 - 373 | difference, 374 | tmpObj.top + _this.config.height 375 | ); 376 | _this.context.lineTo( 377 | tmpObj.left + 378 | _this.config.width / 2 + 379 | difference * -1 - 380 | difference, 381 | tmpObj.top + _this.config.height + _this.config.margin / 2.5 382 | ); 383 | _this.context.lineTo( 384 | tmpObj.left + 385 | _this.config.width / 2 + 386 | difference * -1 + 387 | difference, 388 | tmpObj.top + _this.config.height + _this.config.margin / 2.5 389 | ); 390 | _this.context.lineTo( 391 | tmpObj.left + 392 | _this.config.width / 2 + 393 | difference * -1 + 394 | difference, 395 | tmpObj.top + _this.config.height + _this.config.arrowWidth / 2 396 | ); 397 | _this.context.stroke(); 398 | 399 | _this.context.beginPath(); 400 | _this.context.moveTo( 401 | tmpObj.left + 402 | _this.config.width / 2 + 403 | difference * -1 + 404 | difference, 405 | tmpObj.top + _this.config.height 406 | ); 407 | _this.context.lineTo( 408 | tmpObj.left + 409 | _this.config.width / 2 - 410 | _this.config.arrowWidth / 2 + 411 | difference * -1 + 412 | difference, 413 | tmpObj.top + _this.config.height + _this.config.arrowWidth 414 | ); 415 | _this.context.lineTo( 416 | tmpObj.left + 417 | _this.config.width / 2 + 418 | _this.config.arrowWidth / 2 + 419 | difference * -1 + 420 | difference, 421 | tmpObj.top + _this.config.height + _this.config.arrowWidth 422 | ); 423 | _this.context.fill(); 424 | } else if (invertParent) { 425 | if (_this.config.lineDiff) { 426 | difference = 427 | _this.config.margin / 2 - 428 | (_this.config.margin / _this.config.columnsCount) * 429 | _this.diagrams[i].x + 430 | (_this.config.margin - 431 | (_this.config.arrowWidth + 432 | _this.config.lineWidth * 2 * countInvertParent)); 433 | } 434 | 435 | if (_this.context.setLineDash) { 436 | _this.context.setLineDash([ 437 | _this.config.lineWidth, 438 | _this.config.lineWidth 439 | ]); 440 | } 441 | 442 | _this.context.beginPath(); 443 | _this.context.moveTo( 444 | _this.diagrams[i].left + _this.config.width / 2 + difference, 445 | _this.diagrams[i].top + _this.config.height / 2 + difference 446 | ); 447 | _this.context.lineTo( 448 | _this.diagrams[i].left + 449 | _this.config.width + 450 | _this.config.margin + 451 | difference, 452 | _this.diagrams[i].top + _this.config.height / 2 + difference 453 | ); 454 | _this.context.lineTo( 455 | _this.diagrams[i].left + 456 | _this.config.width + 457 | _this.config.margin + 458 | difference, 459 | tmpObj.top - _this.config.margin + difference 460 | ); 461 | _this.context.lineTo( 462 | tmpObj.left + _this.config.width / 2 + difference, 463 | tmpObj.top - _this.config.margin + difference 464 | ); 465 | _this.context.lineTo( 466 | tmpObj.left + _this.config.width / 2 + difference, 467 | tmpObj.top - _this.config.arrowWidth + difference 468 | ); 469 | _this.context.lineWidth = _this.config.lineWidth; 470 | _this.context.stroke(); 471 | 472 | _this.context.beginPath(); 473 | _this.context.moveTo( 474 | tmpObj.left + _this.config.width / 2 + difference, 475 | tmpObj.top 476 | ); 477 | _this.context.lineTo( 478 | tmpObj.left + 479 | _this.config.width / 2 - 480 | _this.config.arrowWidth / 2 + 481 | difference, 482 | tmpObj.top - _this.config.arrowWidth 483 | ); 484 | _this.context.lineTo( 485 | tmpObj.left + 486 | _this.config.width / 2 + 487 | _this.config.arrowWidth / 2 + 488 | difference, 489 | tmpObj.top - _this.config.arrowWidth 490 | ); 491 | _this.context.fill(); 492 | } else if (isDistant) { 493 | if (_this.config.lineDiff) { 494 | difference = 495 | _this.config.margin / 2 - 496 | (_this.config.margin / _this.config.columnsCount) * 497 | _this.diagrams[i].x + 498 | (_this.config.margin - 499 | (_this.config.arrowWidth + 500 | _this.config.lineWidth * 2 * countDistant)); 501 | } 502 | 503 | countDistant++; 504 | 505 | if (!isFinite(difference)) { 506 | difference = 0; 507 | } 508 | 509 | if (_this.context.setLineDash) { 510 | _this.context.setLineDash([ 511 | _this.config.lineWidth, 512 | _this.config.lineWidth 513 | ]); 514 | } 515 | 516 | _this.context.beginPath(); 517 | _this.context.moveTo( 518 | _this.diagrams[i].left + _this.config.width / 2 + difference * -1, 519 | _this.diagrams[i].top 520 | ); 521 | _this.context.lineTo( 522 | _this.diagrams[i].left + _this.config.width / 2 + difference * -1, 523 | _this.diagrams[i].top - _this.config.margin + difference 524 | ); 525 | _this.context.lineTo( 526 | tmpObj.left + _this.config.width / 2 + difference * -1, 527 | _this.diagrams[i].top - _this.config.margin + difference 528 | ); 529 | _this.context.lineTo( 530 | tmpObj.left + _this.config.width / 2 + difference * -1, 531 | tmpObj.top + 532 | _this.config.height + 533 | _this.config.margin + 534 | difference 535 | ); 536 | _this.context.lineTo( 537 | tmpObj.left + _this.config.width / 2 + difference * -1, 538 | tmpObj.top + _this.config.height + _this.config.arrowWidth / 2 539 | ); 540 | _this.context.lineWidth = _this.config.lineWidth; 541 | _this.context.stroke(); 542 | 543 | _this.context.beginPath(); 544 | _this.context.moveTo( 545 | tmpObj.left + _this.config.width / 2 + difference * -1, 546 | tmpObj.top + _this.config.height 547 | ); 548 | _this.context.lineTo( 549 | tmpObj.left + 550 | _this.config.width / 2 - 551 | _this.config.arrowWidth / 2 + 552 | difference * -1, 553 | tmpObj.top + _this.config.height + _this.config.arrowWidth 554 | ); 555 | _this.context.lineTo( 556 | tmpObj.left + 557 | _this.config.width / 2 + 558 | _this.config.arrowWidth / 2 + 559 | difference * -1, 560 | tmpObj.top + _this.config.height + _this.config.arrowWidth 561 | ); 562 | _this.context.fill(); 563 | } else { 564 | //var 565 | if (_this.config.lineDiff) { 566 | difference = 567 | _this.config.margin / 2 - 568 | (_this.config.margin / _this.config.columnsCount) * 569 | _this.diagrams[i].x; 570 | } 571 | 572 | if (!isFinite(difference)) { 573 | difference = 0; 574 | } 575 | 576 | _this.context.beginPath(); 577 | _this.context.moveTo( 578 | _this.diagrams[i].left + _this.config.width / 2 + difference * -1, 579 | _this.diagrams[i].top 580 | ); 581 | _this.context.lineTo( 582 | _this.diagrams[i].left + _this.config.width / 2 + difference * -1, 583 | _this.diagrams[i].top - _this.config.margin + difference 584 | ); 585 | _this.context.lineTo( 586 | tmpObj.left + _this.config.width / 2 + difference * -1, 587 | tmpObj.top + 588 | _this.config.height + 589 | _this.config.margin + 590 | difference 591 | ); 592 | _this.context.lineTo( 593 | tmpObj.left + _this.config.width / 2 + difference * -1, 594 | tmpObj.top + _this.config.height + _this.config.arrowWidth / 2 595 | ); 596 | _this.context.lineWidth = _this.config.lineWidth; 597 | _this.context.stroke(); 598 | 599 | _this.context.beginPath(); 600 | _this.context.moveTo( 601 | tmpObj.left + _this.config.width / 2 + difference * -1, 602 | tmpObj.top + _this.config.height 603 | ); 604 | _this.context.lineTo( 605 | tmpObj.left + 606 | _this.config.width / 2 - 607 | _this.config.arrowWidth / 2 + 608 | difference * -1, 609 | tmpObj.top + _this.config.height + _this.config.arrowWidth 610 | ); 611 | _this.context.lineTo( 612 | tmpObj.left + 613 | _this.config.width / 2 + 614 | _this.config.arrowWidth / 2 + 615 | difference * -1, 616 | tmpObj.top + _this.config.height + _this.config.arrowWidth 617 | ); 618 | _this.context.fill(); 619 | } 620 | } 621 | } 622 | } 623 | 624 | //DRAW NOT ORPHANS DIAGRAMS 625 | 626 | for (let i = 0; i < _this.diagrams.length; i++) { 627 | if (!_this.diagrams[i].orphan) { 628 | let positionX = _this.diagrams[i].left; 629 | let positionY = _this.diagrams[i].top; 630 | _this.context.fillStyle = _this.diagrams[i].bgColor; 631 | _this.context.strokeStyle = _this.diagrams[i].bgColor; 632 | roundRect( 633 | _this.context, 634 | positionX, 635 | positionY, 636 | _this.config.width, 637 | _this.config.height, 638 | _this.config.radius, 639 | _this.config.hiddenBg 640 | ); 641 | 642 | let nodeText = _this.context; 643 | nodeText.fillStyle = _this.diagrams[i].color; 644 | nodeText.font = _this.config.fontSize + "px " + _this.config.fontFamily; 645 | wrapText( 646 | _this.context, 647 | _this.diagrams[i].text, 648 | positionX + _this.config.padding, 649 | positionY, 650 | _this.config.width - _this.config.padding * 2, 651 | _this.config.height, 652 | _this.config.fontSize + _this.config.fontSize * 0.2 653 | ); 654 | } 655 | } 656 | }; 657 | 658 | this.init(data); 659 | } 660 | 661 | window.JsDiagramChart = JsDiagramChart; 662 | 663 | export default JsDiagramChart; 664 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const CleanWebpackPlugin = require('clean-webpack-plugin'); 4 | 5 | const srcPath = './src'; 6 | 7 | module.exports = { 8 | entry: srcPath + '/main.js', 9 | output: { 10 | filename: 'js-diagram-chart.js', 11 | path: path.resolve(__dirname, 'dist'), 12 | }, 13 | devtool: 'inline-source-map', 14 | module: { 15 | rules: [ 16 | { 17 | test: /\.m?js$/, 18 | exclude: /(node_modules|bower_components)/, 19 | use: { 20 | loader: 'babel-loader', 21 | options: { 22 | presets: ['@babel/preset-env'] 23 | } 24 | } 25 | } 26 | ] 27 | }, 28 | plugins: [ 29 | new CleanWebpackPlugin(["dist"]), 30 | new webpack.ProgressPlugin(), 31 | ] 32 | }; --------------------------------------------------------------------------------