├── .babelrc ├── .eslintrc ├── .gitignore ├── README.md ├── demo ├── .doolrc ├── README.md ├── image.png ├── index.html ├── index.js └── utils.js ├── dist ├── gantt.js └── gantt.min.js ├── package.json ├── rollup.config.js └── src ├── CanvasGantt.js ├── SVGGantt.js ├── StrGantt.js ├── context.js ├── gantt ├── Bar.js ├── DayHeader.js ├── Grid.js ├── Labels.js ├── Layout.js ├── LinkLine.js ├── MonthHeader.js ├── WeekHeader.js ├── Year.js ├── YearMonth.js ├── index.js └── styles.js ├── h.js ├── index.js ├── render ├── canvas.js ├── string.js └── svg.js └── utils.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | [ 4 | "@babel/plugin-transform-react-jsx", 5 | { 6 | "pragma": "h" 7 | } 8 | ] 9 | ], 10 | "env": { 11 | "cjs": { 12 | "presets": [ 13 | "@babel/preset-env" 14 | ] 15 | }, 16 | "es": { 17 | "presets": [ 18 | [ 19 | "@babel/preset-env", 20 | { 21 | "modules": false 22 | } 23 | ] 24 | ] 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | parser: babel-eslint 2 | extends: airbnb 3 | 4 | env: 5 | mocha: true 6 | browser: true 7 | 8 | settings: 9 | react: 10 | pragma: h 11 | version: latest 12 | 13 | rules: 14 | max-len: [2, 160] 15 | camelcase: 0 16 | no-plusplus: 0 17 | no-return-await: 0 18 | no-mixed-operators: 0 19 | operator-linebreak: 0 20 | lines-between-class-members: 0 21 | comma-dangle: 0 22 | react/sort-comp: 0 23 | react/prop-types: 0 24 | react/forbid-prop-types: 0 25 | react/jsx-filename-extension: 0 26 | react/no-array-index-key: 0 27 | react/no-unknown-property: 0 28 | react/jsx-props-no-spreading: 0 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .idea/ 3 | .ipr 4 | .iws 5 | *~ 6 | ~* 7 | *.diff 8 | *.patch 9 | *.bak 10 | .DS_Store 11 | Thumbs.db 12 | .project 13 | .*proj 14 | .svn/ 15 | *.swp 16 | *.swo 17 | *.log 18 | *.sublime-project 19 | *.sublime-workspace 20 | .buildpath 21 | .settings 22 | 23 | coverage 24 | .nyc_output 25 | package-lock.json 26 | 27 | es/ 28 | lib/ 29 | node_modules 30 | demo/bundle.js 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Gantt Chart 2 | =========== 3 | 4 | > Gantt chart library using jsx support SVG, Canvas and SSR 5 | 6 | [![NPM version](https://img.shields.io/npm/v/gantt.svg)](https://www.npmjs.com/package/gantt) 7 | [![NPM downloads](https://img.shields.io/npm/dm/gantt.svg)](https://www.npmjs.com/package/gantt) 8 | [![Greenkeeper badge](https://badges.greenkeeper.io/d-band/gantt.svg)](https://greenkeeper.io/) 9 | 10 | ## Install 11 | 12 | ```bash 13 | $ npm install gantt --save 14 | ``` 15 | 16 | ## Usage 17 | 18 | [View demo online](https://d-band.github.io/gantt/) 19 | 20 | ```javascript 21 | import { SVGGantt, CanvasGantt, StrGantt } from 'gantt'; 22 | 23 | const data = [{ 24 | id: 1, 25 | type: 'group', 26 | text: '1 Waterfall model', 27 | start: new Date('2018-10-10T09:24:24.319Z'), 28 | end: new Date('2018-12-12T09:32:51.245Z'), 29 | percent: 0.71, 30 | links: [] 31 | }, { 32 | id: 11, 33 | parent: 1, 34 | text: '1.1 Requirements', 35 | start: new Date('2018-10-21T09:24:24.319Z'), 36 | end: new Date('2018-11-22T01:01:08.938Z'), 37 | percent: 0.29, 38 | links: [{ 39 | target: 12, 40 | type: 'FS' 41 | }] 42 | }, { 43 | id: 12, 44 | parent: 1, 45 | text: '1.2 Design', 46 | start: new Date('2018-11-05T09:24:24.319Z'), 47 | end: new Date('2018-12-12T09:32:51.245Z'), 48 | percent: 0.78, 49 | }]; 50 | 51 | new SVGGantt('#svg-root', data, { 52 | viewMode: 'week' 53 | }); 54 | 55 | new CanvasGantt('#canvas-root', data, { 56 | viewMode: 'week' 57 | }); 58 | 59 | const strGantt = new StrGantt(data, { 60 | viewMode: 'week' 61 | }); 62 | this.body = strGantt.render(); 63 | ``` 64 | 65 | ![image](demo/image.png) 66 | 67 | ## API 68 | 69 | ```typescript 70 | interface Link { 71 | target: number, 72 | type: 'FS' | 'FF' | 'SS' | 'SF' 73 | } 74 | 75 | interface Item { 76 | id: number, 77 | parent: number, 78 | text: string, 79 | start: Date, 80 | end: Date, 81 | percent: number, 82 | links: Array 83 | } 84 | 85 | type StyleOptions = { 86 | bgColor: string, // default: '#fff' 87 | lineColor: string, // default: '#eee' 88 | redLineColor: string, // default: '#f04134' 89 | groupBack: string, // default: '#3db9d3' 90 | groupFront: string, // default: '#299cb4' 91 | taskBack: string, // default: '#65c16f' 92 | taskFront: string, // default: '#46ad51' 93 | milestone: string, // default: '#d33daf' 94 | warning: string, // default: '#faad14' 95 | danger: string, // default: '#f5222d' 96 | link: string, // default: '#ffa011' 97 | textColor: string, // default: '#222' 98 | lightTextColor: string, // default: '#999' 99 | lineWidth: string, // default: '1px' 100 | thickLineWidth: string, // default: '1.4px' 101 | fontSize: string, // default: '14px' 102 | smallFontSize: string, // default: '12px' 103 | fontFamily: string, // default: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif' 104 | } 105 | 106 | type Options = { 107 | viewMode: 'day' | 'week' | 'month', 108 | onClick: (item: Item) => {}, 109 | offsetY: number, // default: 60, 110 | rowHeight: number, // default: 40, 111 | barHeight: number, // default: 16, 112 | thickWidth: number, // default: 1.4, 113 | styleOptions: StyleOptions 114 | } 115 | 116 | declare class SVGGantt { 117 | constructor(element: string | HTMLElement, data: Array, options: Options); 118 | setData(data: Array): void; // set data and re-render 119 | setOptions(options: Options): void; // set options and re-render 120 | render(): void; 121 | } 122 | 123 | declare class CanvasGantt { 124 | constructor(element: string | HTMLElement, data: Array, options: Options); 125 | setData(data: Array): void; // set data and re-render 126 | setOptions(options: Options): void; // set options and re-render 127 | render(): void; 128 | } 129 | 130 | declare class StrGantt { 131 | constructor(data: Array, options: Options); 132 | setData(data: Array): void; 133 | setOptions(options: Options): void; 134 | render(): string; 135 | } 136 | ``` 137 | 138 | ## Report a issue 139 | 140 | * [All issues](https://github.com/d-band/gantt/issues) 141 | * [New issue](https://github.com/d-band/gantt/issues/new) 142 | 143 | ## License 144 | 145 | Gantt is available under the terms of the MIT License. 146 | -------------------------------------------------------------------------------- /demo/.doolrc: -------------------------------------------------------------------------------- 1 | { 2 | entry: { 3 | bundle: './index.js' 4 | }, 5 | outputPath: '.', 6 | babelPlugins: [ 7 | ['@babel/plugin-transform-react-jsx', { 8 | 'pragma': 'h' 9 | }] 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /demo/README.md: -------------------------------------------------------------------------------- 1 | Gantt Demo 2 | ========== 3 | 4 | ``` 5 | npm i dool -g 6 | 7 | dool server 8 | dool build 9 | ``` -------------------------------------------------------------------------------- /demo/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d-band/gantt/2a842e03c9356011122b42b0f68ed504262ab8d9/demo/image.png -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Gantt Chart Demo 6 | 24 | 25 | 26 |
27 | 32 | 33 | 34 | 35 | 36 | 37 |
38 |
39 | 40 |

41 |   
42 | 
43 | 


--------------------------------------------------------------------------------
/demo/index.js:
--------------------------------------------------------------------------------
  1 | import {
  2 |   SVGGantt,
  3 |   CanvasGantt,
  4 |   StrGantt,
  5 |   utils
  6 | } from '../src';
  7 | import { getData, formatXML } from './utils';
  8 | 
  9 | const $ = s => document.querySelector(s);
 10 | const { tasks, links } = getData();
 11 | 
 12 | const data = utils.formatData(tasks, links);
 13 | const opts = {
 14 |   viewMode: 'week',
 15 |   onClick: v => console.log(v)
 16 | };
 17 | const svgGantt = new SVGGantt('#svg', data, opts);
 18 | const canvasGantt = new CanvasGantt('#canvas', data, opts);
 19 | const strGantt = new StrGantt(data, opts);
 20 | 
 21 | function renderStr() {
 22 |   $('#str').textContent = formatXML(strGantt.render());
 23 | }
 24 | 
 25 | renderStr();
 26 | 
 27 | function changeOptions(options) {
 28 |   svgGantt.setOptions(options);
 29 |   canvasGantt.setOptions(options);
 30 |   strGantt.setOptions(options);
 31 |   renderStr();
 32 | }
 33 | 
 34 | function changeData() {
 35 |   const list = utils.formatData(tasks, links);
 36 |   svgGantt.setData(list);
 37 |   canvasGantt.setData(list);
 38 |   strGantt.setData(list);
 39 |   renderStr();
 40 | }
 41 | $('#viewMode').onchange = e => {
 42 |   const viewMode = e.target.value;
 43 |   changeOptions({ viewMode });
 44 | };
 45 | $('#showLinks').onchange = () => {
 46 |   const showLinks = $('#showLinks').checked;
 47 |   changeOptions({ showLinks });
 48 | };
 49 | $('#showDelay').onchange = () => {
 50 |   const showDelay = $('#showDelay').checked;
 51 |   changeOptions({ showDelay });
 52 | };
 53 | $('#autoSchedule').onclick = () => {
 54 |   utils.autoSchedule(tasks, links);
 55 |   changeData();
 56 | };
 57 | 
 58 | function addLink(s, e) {
 59 |   const sid = parseInt(s.dataset['id']);
 60 |   const eid = parseInt(e.dataset['id']);
 61 |   const snode = tasks.find(t => t.id === sid);
 62 |   const enode = tasks.find(t => t.id === eid);
 63 |   let stype = isStart(s) ? 'S' : 'F';
 64 |   let etype = isStart(e) ? 'S' : 'F';
 65 |   if (snode.type === 'milestone') {
 66 |     stype = 'F';
 67 |   }
 68 |   if (enode.type === 'milestone') {
 69 |     etype = 'S';
 70 |   }
 71 |   links.push({ source: sid, target: eid, type: `${stype}${etype}` });
 72 |   changeData();
 73 | }
 74 | 
 75 | const NS = 'http://www.w3.org/2000/svg';
 76 | 
 77 | let $svg = null;
 78 | let moving = false;
 79 | let $start = null;
 80 | let $line = null;
 81 | 
 82 | function isStart(el) {
 83 |   return el.classList.contains('gantt-ctrl-start');
 84 | }
 85 | 
 86 | function isFinish(el) {
 87 |   return el.classList.contains('gantt-ctrl-finish');
 88 | }
 89 | 
 90 | document.onmousedown = (e) => {
 91 |   $svg = $('svg');
 92 |   if (!isStart(e.target) && !isFinish(e.target)) {
 93 |     return;
 94 |   }
 95 |   e.preventDefault();
 96 |   $start = e.target;
 97 |   document.querySelectorAll('.gantt-ctrl-start,.gantt-ctrl-finish').forEach(elem => {
 98 |     elem.style['display'] = 'block';
 99 |   });
100 |   moving = true;
101 |   $line = document.createElementNS(NS, 'line');
102 |   const x = $start.getAttribute('cx');
103 |   const y = $start.getAttribute('cy');
104 |   $line.setAttribute('x1', x);
105 |   $line.setAttribute('y1', y);
106 |   $line.setAttribute('x2', x);
107 |   $line.setAttribute('y2', y);
108 |   $line.style['stroke'] = '#ffa011';
109 |   $line.style['stroke-width'] = '2';
110 |   $line.style['stroke-dasharray'] = '5';
111 |   $svg.appendChild($line);
112 | };
113 | 
114 | document.onmousemove = (e) => {
115 |   if (!moving) return;
116 |   e.preventDefault();
117 |   if (isStart(e.target) || isFinish(e.target)) {
118 |     const x = e.target.getAttribute('cx');
119 |     const y = e.target.getAttribute('cy');
120 |     $line.setAttribute('x2', x);
121 |     $line.setAttribute('y2', y);
122 |   } else {
123 |     const x = e.clientX;
124 |     const y = e.clientY;
125 |     const rect = $svg.getBoundingClientRect();
126 |     $line.setAttribute('x2', x - rect.left);
127 |     $line.setAttribute('y2', y - rect.top);
128 |   }
129 | };
130 | 
131 | document.onmouseup = (e) => {
132 |   if (!moving) return;
133 |   e.preventDefault();
134 |   const isCtrl = isStart(e.target) || isFinish(e.target);
135 |   if ($start && isCtrl) {
136 |     addLink($start, e.target);
137 |   }
138 | 
139 |   document.querySelectorAll('.gantt-ctrl-start,.gantt-ctrl-finish').forEach(elem => {
140 |     elem.style['display'] = 'none';
141 |   });
142 |   moving = false;
143 |   if ($start) {
144 |     $start.style['display'] = 'none';
145 |     $start = null;
146 |   }
147 |   if ($line) {
148 |     $svg.removeChild($line);
149 |     $line = null;
150 |   }
151 | };
152 | 


--------------------------------------------------------------------------------
/demo/utils.js:
--------------------------------------------------------------------------------
  1 | function rand(begin) {
  2 |   let date;
  3 |   let days;
  4 |   if (begin) {
  5 |     days = Math.random() * 60 + 5;
  6 |     date = new Date(begin);
  7 |   } else {
  8 |     days = Math.random() * 60 - 60;
  9 |     date = new Date();
 10 |   }
 11 |   date.setDate(date.getDate() + days);
 12 |   return date;
 13 | }
 14 | 
 15 | export function getData() {
 16 |   const tasks = [{
 17 |     id: 1,
 18 |     name: 'Waterfall model'
 19 |   }, {
 20 |     id: 11,
 21 |     parent: 1,
 22 |     name: 'Requirements'
 23 |   }, {
 24 |     id: 12,
 25 |     parent: 1,
 26 |     name: 'Design'
 27 |   }, {
 28 |     id: 13,
 29 |     parent: 1,
 30 |     name: 'Implement',
 31 |     type: 'milestone'
 32 |   }, {
 33 |     id: 14,
 34 |     parent: 1,
 35 |     name: 'Verification'
 36 |   }, {
 37 |     id: 2,
 38 |     name: 'Development'
 39 |   }, {
 40 |     id: 21,
 41 |     parent: 2,
 42 |     name: 'Preliminary'
 43 |   }, {
 44 |     id: 22,
 45 |     parent: 2,
 46 |     name: 'Systems design'
 47 |   }, {
 48 |     id: 23,
 49 |     parent: 2,
 50 |     name: 'Development'
 51 |   }, {
 52 |     id: 24,
 53 |     parent: 2,
 54 |     name: 'Integration'
 55 |   }];
 56 |   tasks.forEach((v) => {
 57 |     v.start = rand();
 58 |     v.duration = Math.random() * 90;
 59 |     v.percent = Math.random();
 60 |   });
 61 |   const links = [{
 62 |     source: 11,
 63 |     target: 12,
 64 |     type: 'FS',
 65 |     lag: 3
 66 |   }, {
 67 |     source: 12,
 68 |     target: 13,
 69 |     type: 'FS',
 70 |     lag: 5
 71 |   }, {
 72 |     source: 13,
 73 |     target: 14,
 74 |     type: 'FS'
 75 |   }, {
 76 |     source: 13,
 77 |     target: 21,
 78 |     type: 'FS'
 79 |   }, {
 80 |     source: 23,
 81 |     target: 24,
 82 |     type: 'SF'
 83 |   }];
 84 |   return { tasks, links };
 85 | }
 86 | 
 87 | export function formatXML (xml) {
 88 |   var reg = /(>)\s*(<)(\/*)/g; // updated Mar 30, 2015
 89 |   var wsexp = / *(.*) +\n/g;
 90 |   var contexp = /(<.+>)(.+\n)/g;
 91 |   xml = xml.replace(reg, '$1\n$2$3').replace(wsexp, '$1\n').replace(contexp, '$1\n$2');
 92 |   var pad = 0;
 93 |   var formatted = '';
 94 |   var lines = xml.split('\n');
 95 |   var indent = 0;
 96 |   var lastType = 'other';
 97 |   // 4 types of tags - single, closing, opening, other (text, doctype, comment) - 4*4 = 16 transitions 
 98 |   var transitions = {
 99 |     'single->single': 0,
100 |     'single->closing': -1,
101 |     'single->opening': 0,
102 |     'single->other': 0,
103 |     'closing->single': 0,
104 |     'closing->closing': -1,
105 |     'closing->opening': 0,
106 |     'closing->other': 0,
107 |     'opening->single': 1,
108 |     'opening->closing': 0,
109 |     'opening->opening': 1,
110 |     'opening->other': 1,
111 |     'other->single': 0,
112 |     'other->closing': -1,
113 |     'other->opening': 0,
114 |     'other->other': 0
115 |   };
116 | 
117 |   for (var i = 0; i < lines.length; i++) {
118 |     var ln = lines[i];
119 | 
120 |     // Luca Viggiani 2017-07-03: handle optional  declaration
121 |     if (ln.match(/\s*<\?xml/)) {
122 |       formatted += ln + '\n';
123 |       continue;
124 |     }
125 | 
126 |     var single = Boolean(ln.match(/<.+\/>/)); // is this line a single tag? ex. 
127 | var closing = Boolean(ln.match(/<\/.+>/)); // is this a closing tag? ex. 128 | var opening = Boolean(ln.match(/<[^!].*>/)); // is this even a tag (that's not ) 129 | var type = single ? 'single' : closing ? 'closing' : opening ? 'opening' : 'other'; 130 | var fromTo = lastType + '->' + type; 131 | lastType = type; 132 | var padding = ''; 133 | 134 | indent += transitions[fromTo]; 135 | for (var j = 0; j < indent; j++) { 136 | padding += ' '; 137 | } 138 | if (fromTo == 'opening->closing') 139 | formatted = formatted.substr(0, formatted.length - 1) + ln + '\n'; // substr removes line break (\n) from prev loop 140 | else 141 | formatted += padding + ln + '\n'; 142 | } 143 | 144 | return formatted; 145 | } 146 | -------------------------------------------------------------------------------- /dist/gantt.js: -------------------------------------------------------------------------------- 1 | (function (global, factory) { 2 | typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : 3 | typeof define === 'function' && define.amd ? define(['exports'], factory) : 4 | (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.Gantt = {})); 5 | }(this, (function (exports) { 'use strict'; 6 | 7 | function getDefaultExportFromCjs (x) { 8 | return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x; 9 | } 10 | 11 | function createCommonjsModule(fn, basedir, module) { 12 | return module = { 13 | path: basedir, 14 | exports: {}, 15 | require: function (path, base) { 16 | return commonjsRequire(path, (base === undefined || base === null) ? module.path : base); 17 | } 18 | }, fn(module, module.exports), module.exports; 19 | } 20 | 21 | function commonjsRequire () { 22 | throw new Error('Dynamic requires are not currently supported by @rollup/plugin-commonjs'); 23 | } 24 | 25 | var _extends_1 = createCommonjsModule(function (module) { 26 | function _extends() { 27 | module.exports = _extends = Object.assign || function (target) { 28 | for (var i = 1; i < arguments.length; i++) { 29 | var source = arguments[i]; 30 | 31 | for (var key in source) { 32 | if (Object.prototype.hasOwnProperty.call(source, key)) { 33 | target[key] = source[key]; 34 | } 35 | } 36 | } 37 | 38 | return target; 39 | }; 40 | 41 | module.exports["default"] = module.exports, module.exports.__esModule = true; 42 | return _extends.apply(this, arguments); 43 | } 44 | 45 | module.exports = _extends; 46 | module.exports["default"] = module.exports, module.exports.__esModule = true; 47 | }); 48 | 49 | var _extends = /*@__PURE__*/getDefaultExportFromCjs(_extends_1); 50 | 51 | var defineProperty = createCommonjsModule(function (module) { 52 | function _defineProperty(obj, key, value) { 53 | if (key in obj) { 54 | Object.defineProperty(obj, key, { 55 | value: value, 56 | enumerable: true, 57 | configurable: true, 58 | writable: true 59 | }); 60 | } else { 61 | obj[key] = value; 62 | } 63 | 64 | return obj; 65 | } 66 | 67 | module.exports = _defineProperty; 68 | module.exports["default"] = module.exports, module.exports.__esModule = true; 69 | }); 70 | 71 | var _defineProperty = /*@__PURE__*/getDefaultExportFromCjs(defineProperty); 72 | 73 | var classCallCheck = createCommonjsModule(function (module) { 74 | function _classCallCheck(instance, Constructor) { 75 | if (!(instance instanceof Constructor)) { 76 | throw new TypeError("Cannot call a class as a function"); 77 | } 78 | } 79 | 80 | module.exports = _classCallCheck; 81 | module.exports["default"] = module.exports, module.exports.__esModule = true; 82 | }); 83 | 84 | var _classCallCheck = /*@__PURE__*/getDefaultExportFromCjs(classCallCheck); 85 | 86 | var createClass = createCommonjsModule(function (module) { 87 | function _defineProperties(target, props) { 88 | for (var i = 0; i < props.length; i++) { 89 | var descriptor = props[i]; 90 | descriptor.enumerable = descriptor.enumerable || false; 91 | descriptor.configurable = true; 92 | if ("value" in descriptor) descriptor.writable = true; 93 | Object.defineProperty(target, descriptor.key, descriptor); 94 | } 95 | } 96 | 97 | function _createClass(Constructor, protoProps, staticProps) { 98 | if (protoProps) _defineProperties(Constructor.prototype, protoProps); 99 | if (staticProps) _defineProperties(Constructor, staticProps); 100 | return Constructor; 101 | } 102 | 103 | module.exports = _createClass; 104 | module.exports["default"] = module.exports, module.exports.__esModule = true; 105 | }); 106 | 107 | var _createClass = /*@__PURE__*/getDefaultExportFromCjs(createClass); 108 | 109 | function ownKeys$5(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) { symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); } keys.push.apply(keys, symbols); } return keys; } 110 | 111 | function _objectSpread$5(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys$5(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys$5(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; } 112 | 113 | function addChild(c, childNodes) { 114 | if (c === null || c === undefined) return; 115 | 116 | if (typeof c === 'string' || typeof c === 'number') { 117 | childNodes.push(c.toString()); 118 | } else if (Array.isArray(c)) { 119 | for (var i = 0; i < c.length; i++) { 120 | addChild(c[i], childNodes); 121 | } 122 | } else { 123 | childNodes.push(c); 124 | } 125 | } 126 | 127 | function h(tag, props) { 128 | var childNodes = []; 129 | 130 | for (var _len = arguments.length, children = new Array(_len > 2 ? _len - 2 : 0), _key = 2; _key < _len; _key++) { 131 | children[_key - 2] = arguments[_key]; 132 | } 133 | 134 | addChild(children, childNodes); 135 | 136 | if (typeof tag === 'function') { 137 | return tag(_objectSpread$5(_objectSpread$5({}, props), {}, { 138 | children: childNodes 139 | })); 140 | } 141 | 142 | return { 143 | tag: tag, 144 | props: props, 145 | children: childNodes 146 | }; 147 | } 148 | 149 | function ownKeys$4(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) { symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); } keys.push.apply(keys, symbols); } return keys; } 150 | 151 | function _objectSpread$4(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys$4(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys$4(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; } 152 | 153 | var DAY = 24 * 3600 * 1000; 154 | function addDays(date, days) { 155 | var d = new Date(date.valueOf()); 156 | d.setDate(d.getDate() + days); 157 | return d; 158 | } 159 | function getDates(begin, end) { 160 | var dates = []; 161 | var s = new Date(begin); 162 | s.setHours(24, 0, 0, 0); 163 | 164 | while (s.getTime() <= end) { 165 | dates.push(s.getTime()); 166 | s = addDays(s, 1); 167 | } 168 | 169 | return dates; 170 | } 171 | var ctx = null; 172 | function textWidth(text, font, pad) { 173 | ctx = ctx || document.createElement('canvas').getContext('2d'); 174 | ctx.font = font; 175 | return ctx.measureText(text).width + pad; 176 | } 177 | function formatMonth(date) { 178 | var y = date.getFullYear(); 179 | var m = date.getMonth() + 1; 180 | return "".concat(y, "/").concat(m > 9 ? m : "0".concat(m)); 181 | } 182 | function formatDay(date) { 183 | var m = date.getMonth() + 1; 184 | var d = date.getDate(); 185 | return "".concat(m, "/").concat(d); 186 | } 187 | function minDate(a, b) { 188 | if (a && b) { 189 | return a > b ? b : a; 190 | } 191 | 192 | return a || b; 193 | } 194 | function maxDate(a, b) { 195 | if (a && b) { 196 | return a < b ? b : a; 197 | } 198 | 199 | return a || b; 200 | } 201 | function max(list, defaultValue) { 202 | if (list.length) { 203 | return Math.max.apply(null, list); 204 | } 205 | 206 | return defaultValue; 207 | } 208 | function p2s(arr) { 209 | return arr.map(function (p) { 210 | return "".concat(p[0], ",").concat(p[1]); 211 | }).join(' '); 212 | } 213 | function s2p(str) { 214 | return str.split(' ').map(function (s) { 215 | var p = s.split(','); 216 | return [parseFloat(p[0]), parseFloat(p[1])]; 217 | }); 218 | } 219 | 220 | function walkLevel(nodes, level) { 221 | for (var i = 0; i < nodes.length; i++) { 222 | var node = nodes[i]; 223 | node.level = "".concat(level).concat(i + 1); 224 | node.text = "".concat(node.level, " ").concat(node.name); 225 | walkLevel(node.children, "".concat(node.level, ".")); 226 | } 227 | } 228 | 229 | function walkDates(nodes) { 230 | var start = null; 231 | var end = null; 232 | var percent = 0; 233 | 234 | for (var i = 0; i < nodes.length; i++) { 235 | var node = nodes[i]; 236 | 237 | if (node.children.length) { 238 | var tmp = walkDates(node.children); 239 | node.start = tmp.start; 240 | node.end = tmp.end; 241 | node.percent = tmp.percent; 242 | 243 | if (tmp.start && tmp.end) { 244 | node.duration = (tmp.end - tmp.start) / DAY; 245 | } else { 246 | node.duration = 0; 247 | } 248 | } else { 249 | node.percent = node.percent || 0; 250 | 251 | if (node.start) { 252 | node.end = addDays(node.start, node.duration || 0); 253 | } 254 | 255 | if (node.type === 'milestone') { 256 | node.end = node.start; 257 | } 258 | } 259 | 260 | start = minDate(start, node.start); 261 | end = maxDate(end, node.end); 262 | percent += node.percent; 263 | } 264 | 265 | if (nodes.length) { 266 | percent /= nodes.length; 267 | } 268 | 269 | return { 270 | start: start, 271 | end: end, 272 | percent: percent 273 | }; 274 | } 275 | 276 | function formatData(tasks, links, walk) { 277 | var map = {}; 278 | var tmp = tasks.map(function (t, i) { 279 | map[t.id] = i; 280 | return _objectSpread$4(_objectSpread$4({}, t), {}, { 281 | children: [], 282 | links: [] 283 | }); 284 | }); 285 | var roots = []; 286 | tmp.forEach(function (t) { 287 | var parent = tmp[map[t.parent]]; 288 | 289 | if (parent) { 290 | parent.children.push(t); 291 | } else { 292 | roots.push(t); 293 | } 294 | }); 295 | links.forEach(function (l) { 296 | var s = tmp[map[l.source]]; 297 | var t = tmp[map[l.target]]; 298 | 299 | if (s && t) { 300 | s.links.push(l); 301 | } 302 | }); 303 | walkLevel(roots, ''); 304 | walkDates(roots); 305 | 306 | if (walk) { 307 | walk(roots); 308 | } 309 | 310 | var list = []; 311 | roots.forEach(function (r) { 312 | var stack = []; 313 | stack.push(r); 314 | 315 | while (stack.length) { 316 | var node = stack.pop(); 317 | var len = node.children.length; 318 | 319 | if (len) { 320 | node.type = 'group'; 321 | } 322 | 323 | list.push(node); 324 | 325 | for (var i = len - 1; i >= 0; i--) { 326 | stack.push(node.children[i]); 327 | } 328 | } 329 | }); 330 | return list; 331 | } 332 | function hasPath(vmap, a, b) { 333 | var stack = []; 334 | stack.push(vmap[a]); 335 | 336 | while (stack.length) { 337 | var v = stack.pop(); 338 | 339 | if (v.id === b) { 340 | return true; 341 | } 342 | 343 | for (var i = 0; i < v.links.length; i++) { 344 | stack.push(v.links[i]); 345 | } 346 | } 347 | 348 | return false; 349 | } 350 | function toposort(links) { 351 | var vmap = {}; 352 | links.forEach(function (l) { 353 | var init = function init(id) { 354 | return { 355 | id: id, 356 | out: [], 357 | "in": 0 358 | }; 359 | }; 360 | 361 | vmap[l.source] = init(l.source); 362 | vmap[l.target] = init(l.target); 363 | }); 364 | 365 | for (var i = 0; i < links.length; i++) { 366 | var l = links[i]; 367 | vmap[l.target]["in"]++; 368 | vmap[l.source].out.push(i); 369 | } 370 | 371 | var s = Object.keys(vmap).map(function (k) { 372 | return vmap[k].id; 373 | }).filter(function (id) { 374 | return !vmap[id]["in"]; 375 | }); 376 | var sorted = []; 377 | 378 | while (s.length) { 379 | var id = s.pop(); 380 | sorted.push(id); 381 | 382 | for (var _i = 0; _i < vmap[id].out.length; _i++) { 383 | var index = vmap[id].out[_i]; 384 | var v = vmap[links[index].target]; 385 | v["in"]--; 386 | 387 | if (!v["in"]) { 388 | s.push(v.id); 389 | } 390 | } 391 | } 392 | 393 | return sorted; 394 | } 395 | function autoSchedule(tasks, links) { 396 | var lockMilestone = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false; 397 | var vmap = {}; 398 | links.forEach(function (l) { 399 | vmap[l.source] = { 400 | id: l.source, 401 | links: [] 402 | }; 403 | vmap[l.target] = { 404 | id: l.target, 405 | links: [] 406 | }; 407 | }); 408 | var dag = []; 409 | links.forEach(function (l) { 410 | var source = l.source, 411 | target = l.target; 412 | 413 | if (!hasPath(vmap, target, source)) { 414 | dag.push(l); 415 | vmap[source].links.push(vmap[target]); 416 | } 417 | }); 418 | var sorted = toposort(dag); 419 | var tmap = {}; 420 | 421 | for (var i = 0; i < tasks.length; i++) { 422 | var task = tasks[i]; 423 | 424 | if (task.type === 'milestone') { 425 | task.duration = 0; 426 | } 427 | 428 | tmap[task.id] = i; 429 | } 430 | 431 | var ins = {}; 432 | sorted.forEach(function (id) { 433 | ins[id] = []; 434 | }); 435 | dag.forEach(function (l) { 436 | ins[l.target].push(l); 437 | }); 438 | sorted.forEach(function (id) { 439 | var task = tasks[tmap[id]]; 440 | if (!task) return; 441 | var days = task.duration || 0; 442 | 443 | if (lockMilestone && task.type === 'milestone') { 444 | return; 445 | } 446 | 447 | var start = null; 448 | var end = null; 449 | 450 | for (var _i2 = 0; _i2 < ins[id].length; _i2++) { 451 | var l = ins[id][_i2]; 452 | var v = tasks[tmap[l.source]]; 453 | 454 | if (v && v.start) { 455 | var s = addDays(v.start, l.lag || 0); 456 | var e = addDays(s, v.duration || 0); 457 | 458 | if (l.type === 'SS') { 459 | start = maxDate(start, s); 460 | } 461 | 462 | if (l.type === 'FS') { 463 | start = maxDate(start, e); 464 | } 465 | 466 | if (l.type === 'SF') { 467 | end = maxDate(end, s); 468 | } 469 | 470 | if (l.type === 'FF') { 471 | end = maxDate(end, e); 472 | } 473 | } 474 | } 475 | 476 | if (end) { 477 | task.start = addDays(end, -days); 478 | } 479 | 480 | if (start) { 481 | task.start = start; 482 | } 483 | }); 484 | } 485 | 486 | var utils = /*#__PURE__*/Object.freeze({ 487 | __proto__: null, 488 | DAY: DAY, 489 | addDays: addDays, 490 | getDates: getDates, 491 | textWidth: textWidth, 492 | formatMonth: formatMonth, 493 | formatDay: formatDay, 494 | minDate: minDate, 495 | maxDate: maxDate, 496 | max: max, 497 | p2s: p2s, 498 | s2p: s2p, 499 | formatData: formatData, 500 | hasPath: hasPath, 501 | toposort: toposort, 502 | autoSchedule: autoSchedule 503 | }); 504 | 505 | function Layout(_ref) { 506 | var styles = _ref.styles, 507 | width = _ref.width, 508 | height = _ref.height, 509 | offsetY = _ref.offsetY, 510 | thickWidth = _ref.thickWidth, 511 | maxTextWidth = _ref.maxTextWidth; 512 | var x0 = thickWidth / 2; 513 | var W = width - thickWidth; 514 | var H = height - thickWidth; 515 | return h("g", null, h("rect", { 516 | x: x0, 517 | y: x0, 518 | width: W, 519 | height: H, 520 | style: styles.box 521 | }), h("line", { 522 | x1: 0, 523 | x2: width, 524 | y1: offsetY - x0, 525 | y2: offsetY - x0, 526 | style: styles.bline 527 | }), h("line", { 528 | x1: maxTextWidth, 529 | x2: width, 530 | y1: offsetY / 2, 531 | y2: offsetY / 2, 532 | style: styles.line 533 | })); 534 | } 535 | 536 | function YearMonth(_ref) { 537 | var styles = _ref.styles, 538 | dates = _ref.dates, 539 | unit = _ref.unit, 540 | offsetY = _ref.offsetY, 541 | minTime = _ref.minTime, 542 | maxTime = _ref.maxTime, 543 | maxTextWidth = _ref.maxTextWidth; 544 | var months = dates.filter(function (v) { 545 | return new Date(v).getDate() === 1; 546 | }); 547 | months.unshift(minTime); 548 | months.push(maxTime); 549 | var ticks = []; 550 | var x0 = maxTextWidth; 551 | var y2 = offsetY / 2; 552 | var len = months.length - 1; 553 | 554 | for (var i = 0; i < len; i++) { 555 | var cur = new Date(months[i]); 556 | var str = formatMonth(cur); 557 | var x = x0 + (months[i] - minTime) / unit; 558 | var t = (months[i + 1] - months[i]) / unit; 559 | ticks.push(h("g", null, h("line", { 560 | x1: x, 561 | x2: x, 562 | y1: 0, 563 | y2: y2, 564 | style: styles.line 565 | }), t > 50 ? h("text", { 566 | x: x + t / 2, 567 | y: offsetY * 0.25, 568 | style: styles.text3 569 | }, str) : null)); 570 | } 571 | 572 | return h("g", null, ticks); 573 | } 574 | 575 | function DayHeader(_ref) { 576 | var styles = _ref.styles, 577 | unit = _ref.unit, 578 | minTime = _ref.minTime, 579 | maxTime = _ref.maxTime, 580 | height = _ref.height, 581 | offsetY = _ref.offsetY, 582 | maxTextWidth = _ref.maxTextWidth; 583 | var dates = getDates(minTime, maxTime); 584 | var ticks = []; 585 | var x0 = maxTextWidth; 586 | var y0 = offsetY / 2; 587 | var RH = height - y0; 588 | var len = dates.length - 1; 589 | 590 | for (var i = 0; i < len; i++) { 591 | var cur = new Date(dates[i]); 592 | var day = cur.getDay(); 593 | var x = x0 + (dates[i] - minTime) / unit; 594 | var t = (dates[i + 1] - dates[i]) / unit; 595 | ticks.push(h("g", null, day === 0 || day === 6 ? h("rect", { 596 | x: x, 597 | y: y0, 598 | width: t, 599 | height: RH, 600 | style: styles.week 601 | }) : null, h("line", { 602 | x1: x, 603 | x2: x, 604 | y1: y0, 605 | y2: offsetY, 606 | style: styles.line 607 | }), h("text", { 608 | x: x + t / 2, 609 | y: offsetY * 0.75, 610 | style: styles.text3 611 | }, cur.getDate()), i === len - 1 ? h("line", { 612 | x1: x + t, 613 | x2: x + t, 614 | y1: y0, 615 | y2: offsetY, 616 | style: styles.line 617 | }) : null)); 618 | } 619 | 620 | return h("g", null, h(YearMonth, { 621 | styles: styles, 622 | unit: unit, 623 | dates: dates, 624 | offsetY: offsetY, 625 | minTime: minTime, 626 | maxTime: maxTime, 627 | maxTextWidth: maxTextWidth 628 | }), ticks); 629 | } 630 | 631 | function WeekHeader(_ref) { 632 | var styles = _ref.styles, 633 | unit = _ref.unit, 634 | minTime = _ref.minTime, 635 | maxTime = _ref.maxTime, 636 | height = _ref.height, 637 | offsetY = _ref.offsetY, 638 | maxTextWidth = _ref.maxTextWidth; 639 | var dates = getDates(minTime, maxTime); 640 | var weeks = dates.filter(function (v) { 641 | return new Date(v).getDay() === 0; 642 | }); 643 | weeks.push(maxTime); 644 | var ticks = []; 645 | var x0 = maxTextWidth; 646 | var y0 = offsetY; 647 | var RH = height - y0; 648 | var d = DAY / unit; 649 | var len = weeks.length - 1; 650 | 651 | for (var i = 0; i < len; i++) { 652 | var cur = new Date(weeks[i]); 653 | var x = x0 + (weeks[i] - minTime) / unit; 654 | var curDay = cur.getDate(); 655 | var prevDay = addDays(cur, -1).getDate(); 656 | ticks.push(h("g", null, h("rect", { 657 | x: x - d, 658 | y: y0, 659 | width: d * 2, 660 | height: RH, 661 | style: styles.week 662 | }), h("line", { 663 | x1: x, 664 | x2: x, 665 | y1: offsetY / 2, 666 | y2: offsetY, 667 | style: styles.line 668 | }), h("text", { 669 | x: x + 3, 670 | y: offsetY * 0.75, 671 | style: styles.text2 672 | }, curDay), x - x0 > 28 ? h("text", { 673 | x: x - 3, 674 | y: offsetY * 0.75, 675 | style: styles.text1 676 | }, prevDay) : null)); 677 | } 678 | 679 | return h("g", null, h(YearMonth, { 680 | styles: styles, 681 | unit: unit, 682 | dates: dates, 683 | offsetY: offsetY, 684 | minTime: minTime, 685 | maxTime: maxTime, 686 | maxTextWidth: maxTextWidth 687 | }), ticks); 688 | } 689 | 690 | function Year(_ref) { 691 | var styles = _ref.styles, 692 | months = _ref.months, 693 | unit = _ref.unit, 694 | offsetY = _ref.offsetY, 695 | minTime = _ref.minTime, 696 | maxTime = _ref.maxTime, 697 | maxTextWidth = _ref.maxTextWidth; 698 | var years = months.filter(function (v) { 699 | return new Date(v).getMonth() === 0; 700 | }); 701 | years.unshift(minTime); 702 | years.push(maxTime); 703 | var ticks = []; 704 | var x0 = maxTextWidth; 705 | var y2 = offsetY / 2; 706 | var len = years.length - 1; 707 | 708 | for (var i = 0; i < len; i++) { 709 | var cur = new Date(years[i]); 710 | var x = x0 + (years[i] - minTime) / unit; 711 | var t = (years[i + 1] - years[i]) / unit; 712 | ticks.push(h("g", null, h("line", { 713 | x1: x, 714 | x2: x, 715 | y1: 0, 716 | y2: y2, 717 | style: styles.line 718 | }), t > 35 ? h("text", { 719 | x: x + t / 2, 720 | y: offsetY * 0.25, 721 | style: styles.text3 722 | }, cur.getFullYear()) : null)); 723 | } 724 | 725 | return h("g", null, ticks); 726 | } 727 | 728 | function MonthHeader(_ref) { 729 | var styles = _ref.styles, 730 | unit = _ref.unit, 731 | minTime = _ref.minTime, 732 | maxTime = _ref.maxTime, 733 | offsetY = _ref.offsetY, 734 | maxTextWidth = _ref.maxTextWidth; 735 | var MONTH = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; 736 | var dates = getDates(minTime, maxTime); 737 | var months = dates.filter(function (v) { 738 | return new Date(v).getDate() === 1; 739 | }); 740 | months.unshift(minTime); 741 | months.push(maxTime); 742 | var ticks = []; 743 | var x0 = maxTextWidth; 744 | var y0 = offsetY / 2; 745 | var len = months.length - 1; 746 | 747 | for (var i = 0; i < len; i++) { 748 | var cur = new Date(months[i]); 749 | var month = cur.getMonth(); 750 | var x = x0 + (months[i] - minTime) / unit; 751 | var t = (months[i + 1] - months[i]) / unit; 752 | ticks.push(h("g", null, h("line", { 753 | x1: x, 754 | x2: x, 755 | y1: y0, 756 | y2: offsetY, 757 | style: styles.line 758 | }), t > 30 ? h("text", { 759 | x: x + t / 2, 760 | y: offsetY * 0.75, 761 | style: styles.text3 762 | }, MONTH[month]) : null)); 763 | } 764 | 765 | return h("g", null, h(Year, { 766 | styles: styles, 767 | unit: unit, 768 | months: months, 769 | offsetY: offsetY, 770 | minTime: minTime, 771 | maxTime: maxTime, 772 | maxTextWidth: maxTextWidth 773 | }), ticks); 774 | } 775 | 776 | function Grid(_ref) { 777 | var styles = _ref.styles, 778 | data = _ref.data, 779 | width = _ref.width, 780 | height = _ref.height, 781 | offsetY = _ref.offsetY, 782 | rowHeight = _ref.rowHeight, 783 | maxTextWidth = _ref.maxTextWidth; 784 | return h("g", null, data.map(function (v, i) { 785 | var y = (i + 1) * rowHeight + offsetY; 786 | return h("line", { 787 | key: i, 788 | x1: 0, 789 | x2: width, 790 | y1: y, 791 | y2: y, 792 | style: styles.line 793 | }); 794 | }), h("line", { 795 | x1: maxTextWidth, 796 | x2: maxTextWidth, 797 | y1: 0, 798 | y2: height, 799 | style: styles.bline 800 | })); 801 | } 802 | 803 | function Labels(_ref) { 804 | var styles = _ref.styles, 805 | data = _ref.data, 806 | _onClick = _ref.onClick, 807 | rowHeight = _ref.rowHeight, 808 | offsetY = _ref.offsetY; 809 | return h("g", null, data.map(function (v, i) { 810 | return h("text", { 811 | key: i, 812 | x: 10, 813 | y: (i + 0.5) * rowHeight + offsetY, 814 | "class": "gantt-label", 815 | style: styles.label, 816 | onClick: function onClick() { 817 | return _onClick(v); 818 | } 819 | }, v.text); 820 | })); 821 | } 822 | 823 | function LinkLine(_ref) { 824 | var styles = _ref.styles, 825 | data = _ref.data, 826 | unit = _ref.unit, 827 | offsetY = _ref.offsetY, 828 | minTime = _ref.minTime, 829 | rowHeight = _ref.rowHeight, 830 | barHeight = _ref.barHeight, 831 | maxTextWidth = _ref.maxTextWidth; 832 | var x0 = maxTextWidth; 833 | var y0 = rowHeight / 2 + offsetY; 834 | var map = {}; 835 | data.forEach(function (v, i) { 836 | map[v.id] = i; 837 | }); 838 | return h("g", null, data.map(function (s, i) { 839 | if (!s.end || !s.start || !s.links) { 840 | return null; 841 | } 842 | 843 | return s.links.map(function (l) { 844 | var j = map[l.target]; 845 | var e = data[j]; 846 | if (!e || !e.start || !e.end) return null; 847 | var gap = 12; 848 | var arrow = 6; 849 | var mgap = e.type === 'milestone' ? barHeight / 2 : 0; 850 | var y1 = y0 + i * rowHeight; 851 | var y2 = y0 + j * rowHeight; 852 | var vgap = barHeight / 2 + 4; 853 | 854 | if (y1 > y2) { 855 | vgap = -vgap; 856 | } 857 | 858 | if (l.type === 'FS') { 859 | var x1 = x0 + (s.end - minTime) / unit; 860 | var x2 = x0 + (e.start - minTime) / unit - mgap; 861 | var p1 = [[x1, y1], [x1 + gap, y1]]; 862 | 863 | if (x2 - x1 >= 2 * gap) { 864 | p1.push([x1 + gap, y2]); 865 | } else { 866 | p1.push([x1 + gap, y2 - vgap]); 867 | p1.push([x2 - gap, y2 - vgap]); 868 | p1.push([x2 - gap, y2]); 869 | } 870 | 871 | p1.push([x2 - arrow, y2]); 872 | var p2 = [[x2 - arrow, y2 - arrow], [x2, y2], [x2 - arrow, y2 + arrow]]; 873 | return h("g", null, h("polyline", { 874 | points: p2s(p1), 875 | style: styles.link 876 | }), h("polygon", { 877 | points: p2s(p2), 878 | style: styles.linkArrow 879 | })); 880 | } 881 | 882 | if (l.type === 'FF') { 883 | var _x = x0 + (s.end - minTime) / unit; 884 | 885 | var _x2 = x0 + (e.end - minTime) / unit + mgap; 886 | 887 | var _p = [[_x, y1], [_x + gap, y1]]; 888 | 889 | if (_x2 <= _x) { 890 | _p.push([_x + gap, y2]); 891 | } else { 892 | _p.push([_x + gap, y2 - vgap]); 893 | 894 | _p.push([_x2 + gap, y2 - vgap]); 895 | 896 | _p.push([_x2 + gap, y2]); 897 | } 898 | 899 | _p.push([_x2 + arrow, y2]); 900 | 901 | var _p2 = [[_x2 + arrow, y2 - arrow], [_x2, y2], [_x2 + arrow, y2 + arrow]]; 902 | return h("g", null, h("polyline", { 903 | points: p2s(_p), 904 | style: styles.link 905 | }), h("polygon", { 906 | points: p2s(_p2), 907 | style: styles.linkArrow 908 | })); 909 | } 910 | 911 | if (l.type === 'SS') { 912 | var _x3 = x0 + (s.start - minTime) / unit; 913 | 914 | var _x4 = x0 + (e.start - minTime) / unit - mgap; 915 | 916 | var _p3 = [[_x3, y1], [_x3 - gap, y1]]; 917 | 918 | if (_x3 <= _x4) { 919 | _p3.push([_x3 - gap, y2]); 920 | } else { 921 | _p3.push([_x3 - gap, y2 - vgap]); 922 | 923 | _p3.push([_x4 - gap, y2 - vgap]); 924 | 925 | _p3.push([_x4 - gap, y2]); 926 | } 927 | 928 | _p3.push([_x4 - arrow, y2]); 929 | 930 | var _p4 = [[_x4 - arrow, y2 - arrow], [_x4, y2], [_x4 - arrow, y2 + arrow]]; 931 | return h("g", null, h("polyline", { 932 | points: p2s(_p3), 933 | style: styles.link 934 | }), h("polygon", { 935 | points: p2s(_p4), 936 | style: styles.linkArrow 937 | })); 938 | } 939 | 940 | if (l.type === 'SF') { 941 | var _x5 = x0 + (s.start - minTime) / unit; 942 | 943 | var _x6 = x0 + (e.end - minTime) / unit + mgap; 944 | 945 | var _p5 = [[_x5, y1], [_x5 - gap, y1]]; 946 | 947 | if (_x5 - _x6 >= 2 * gap) { 948 | _p5.push([_x5 - gap, y2]); 949 | } else { 950 | _p5.push([_x5 - gap, y2 - vgap]); 951 | 952 | _p5.push([_x6 + gap, y2 - vgap]); 953 | 954 | _p5.push([_x6 + gap, y2]); 955 | } 956 | 957 | _p5.push([_x6 + arrow, y2]); 958 | 959 | var _p6 = [[_x6 + arrow, y2 - arrow], [_x6, y2], [_x6 + arrow, y2 + arrow]]; 960 | return h("g", null, h("polyline", { 961 | points: p2s(_p5), 962 | style: styles.link 963 | }), h("polygon", { 964 | points: p2s(_p6), 965 | style: styles.linkArrow 966 | })); 967 | } 968 | 969 | return null; 970 | }); 971 | })); 972 | } 973 | 974 | function Bar(_ref) { 975 | var styles = _ref.styles, 976 | data = _ref.data, 977 | unit = _ref.unit, 978 | height = _ref.height, 979 | offsetY = _ref.offsetY, 980 | minTime = _ref.minTime, 981 | showDelay = _ref.showDelay, 982 | rowHeight = _ref.rowHeight, 983 | barHeight = _ref.barHeight, 984 | maxTextWidth = _ref.maxTextWidth, 985 | current = _ref.current, 986 | onClick = _ref.onClick; 987 | var x0 = maxTextWidth; 988 | var y0 = (rowHeight - barHeight) / 2 + offsetY; 989 | var cur = x0 + (current - minTime) / unit; 990 | return h("g", null, current > minTime ? h("line", { 991 | x1: cur, 992 | x2: cur, 993 | y1: offsetY, 994 | y2: height, 995 | style: styles.cline 996 | }) : null, data.map(function (v, i) { 997 | if (!v.end || !v.start) { 998 | return null; 999 | } 1000 | 1001 | var handler = function handler() { 1002 | return onClick(v); 1003 | }; 1004 | 1005 | var x = x0 + (v.start - minTime) / unit; 1006 | var y = y0 + i * rowHeight; 1007 | var cy = y + barHeight / 2; 1008 | 1009 | if (v.type === 'milestone') { 1010 | var size = barHeight / 2; 1011 | var points = [[x, cy - size], [x + size, cy], [x, cy + size], [x - size, cy]].map(function (p) { 1012 | return "".concat(p[0], ",").concat(p[1]); 1013 | }).join(' '); 1014 | return h("g", { 1015 | key: i, 1016 | "class": "gantt-bar", 1017 | style: { 1018 | cursor: 'pointer' 1019 | }, 1020 | onClick: handler 1021 | }, h("polygon", { 1022 | points: points, 1023 | style: styles.milestone, 1024 | onClick: handler 1025 | }), h("circle", { 1026 | "class": "gantt-ctrl-start", 1027 | "data-id": v.id, 1028 | cx: x, 1029 | cy: cy, 1030 | r: 6, 1031 | style: styles.ctrl 1032 | })); 1033 | } 1034 | 1035 | var w1 = (v.end - v.start) / unit; 1036 | var w2 = w1 * v.percent; 1037 | var bar = v.type === 'group' ? { 1038 | back: styles.groupBack, 1039 | front: styles.groupFront 1040 | } : { 1041 | back: styles.taskBack, 1042 | front: styles.taskFront 1043 | }; 1044 | 1045 | if (showDelay) { 1046 | if (x + w2 < cur && v.percent < 0.999999) { 1047 | bar.back = styles.warning; 1048 | } 1049 | 1050 | if (x + w1 < cur && v.percent < 0.999999) { 1051 | bar.back = styles.danger; 1052 | } 1053 | } 1054 | 1055 | return h("g", { 1056 | key: i, 1057 | "class": "gantt-bar", 1058 | style: { 1059 | cursor: 'pointer' 1060 | }, 1061 | onClick: handler 1062 | }, h("text", { 1063 | x: x - 4, 1064 | y: cy, 1065 | style: styles.text1 1066 | }, formatDay(v.start)), h("text", { 1067 | x: x + w1 + 4, 1068 | y: cy, 1069 | style: styles.text2 1070 | }, formatDay(v.end)), h("rect", { 1071 | x: x, 1072 | y: y, 1073 | width: w1, 1074 | height: barHeight, 1075 | rx: 1.8, 1076 | ry: 1.8, 1077 | style: bar.back, 1078 | onClick: handler 1079 | }), w2 > 0.000001 ? h("rect", { 1080 | x: x, 1081 | y: y, 1082 | width: w2, 1083 | height: barHeight, 1084 | rx: 1.8, 1085 | ry: 1.8, 1086 | style: bar.front 1087 | }) : null, v.type === 'group' ? null : h("g", null, h("circle", { 1088 | "class": "gantt-ctrl-start", 1089 | "data-id": v.id, 1090 | cx: x - 12, 1091 | cy: cy, 1092 | r: 6, 1093 | style: styles.ctrl 1094 | }), h("circle", { 1095 | "class": "gantt-ctrl-finish", 1096 | "data-id": v.id, 1097 | cx: x + w1 + 12, 1098 | cy: cy, 1099 | r: 6, 1100 | style: styles.ctrl 1101 | }))); 1102 | })); 1103 | } 1104 | 1105 | function ownKeys$3(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) { symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); } keys.push.apply(keys, symbols); } return keys; } 1106 | 1107 | function _objectSpread$3(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys$3(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys$3(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; } 1108 | 1109 | var SIZE = '14px'; 1110 | var TYPE = '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif'; 1111 | function getFont(_ref) { 1112 | var _ref$fontSize = _ref.fontSize, 1113 | fontSize = _ref$fontSize === void 0 ? SIZE : _ref$fontSize, 1114 | _ref$fontFamily = _ref.fontFamily, 1115 | fontFamily = _ref$fontFamily === void 0 ? TYPE : _ref$fontFamily; 1116 | return "bold ".concat(fontSize, " ").concat(fontFamily); 1117 | } 1118 | function getStyles(_ref2) { 1119 | var _ref2$bgColor = _ref2.bgColor, 1120 | bgColor = _ref2$bgColor === void 0 ? '#fff' : _ref2$bgColor, 1121 | _ref2$lineColor = _ref2.lineColor, 1122 | lineColor = _ref2$lineColor === void 0 ? '#eee' : _ref2$lineColor, 1123 | _ref2$redLineColor = _ref2.redLineColor, 1124 | redLineColor = _ref2$redLineColor === void 0 ? '#f04134' : _ref2$redLineColor, 1125 | _ref2$groupBack = _ref2.groupBack, 1126 | groupBack = _ref2$groupBack === void 0 ? '#3db9d3' : _ref2$groupBack, 1127 | _ref2$groupFront = _ref2.groupFront, 1128 | groupFront = _ref2$groupFront === void 0 ? '#299cb4' : _ref2$groupFront, 1129 | _ref2$taskBack = _ref2.taskBack, 1130 | taskBack = _ref2$taskBack === void 0 ? '#65c16f' : _ref2$taskBack, 1131 | _ref2$taskFront = _ref2.taskFront, 1132 | taskFront = _ref2$taskFront === void 0 ? '#46ad51' : _ref2$taskFront, 1133 | _ref2$milestone = _ref2.milestone, 1134 | milestone = _ref2$milestone === void 0 ? '#d33daf' : _ref2$milestone, 1135 | _ref2$warning = _ref2.warning, 1136 | warning = _ref2$warning === void 0 ? '#faad14' : _ref2$warning, 1137 | _ref2$danger = _ref2.danger, 1138 | danger = _ref2$danger === void 0 ? '#f5222d' : _ref2$danger, 1139 | _ref2$link = _ref2.link, 1140 | link = _ref2$link === void 0 ? '#ffa011' : _ref2$link, 1141 | _ref2$textColor = _ref2.textColor, 1142 | textColor = _ref2$textColor === void 0 ? '#222' : _ref2$textColor, 1143 | _ref2$lightTextColor = _ref2.lightTextColor, 1144 | lightTextColor = _ref2$lightTextColor === void 0 ? '#999' : _ref2$lightTextColor, 1145 | _ref2$lineWidth = _ref2.lineWidth, 1146 | lineWidth = _ref2$lineWidth === void 0 ? '1px' : _ref2$lineWidth, 1147 | _ref2$thickLineWidth = _ref2.thickLineWidth, 1148 | thickLineWidth = _ref2$thickLineWidth === void 0 ? '1.4px' : _ref2$thickLineWidth, 1149 | _ref2$fontSize = _ref2.fontSize, 1150 | fontSize = _ref2$fontSize === void 0 ? SIZE : _ref2$fontSize, 1151 | _ref2$smallFontSize = _ref2.smallFontSize, 1152 | smallFontSize = _ref2$smallFontSize === void 0 ? '12px' : _ref2$smallFontSize, 1153 | _ref2$fontFamily = _ref2.fontFamily, 1154 | fontFamily = _ref2$fontFamily === void 0 ? TYPE : _ref2$fontFamily, 1155 | _ref2$whiteSpace = _ref2.whiteSpace, 1156 | whiteSpace = _ref2$whiteSpace === void 0 ? 'pre' : _ref2$whiteSpace; 1157 | var line = { 1158 | stroke: lineColor, 1159 | 'stroke-width': lineWidth 1160 | }; 1161 | var redLine = { 1162 | stroke: redLineColor, 1163 | 'stroke-width': lineWidth 1164 | }; 1165 | var thickLine = { 1166 | stroke: lineColor, 1167 | 'stroke-width': thickLineWidth 1168 | }; 1169 | var text = { 1170 | fill: textColor, 1171 | 'dominant-baseline': 'central', 1172 | 'font-size': fontSize, 1173 | 'font-family': fontFamily, 1174 | 'white-space': whiteSpace 1175 | }; 1176 | var smallText = { 1177 | fill: lightTextColor, 1178 | 'font-size': smallFontSize 1179 | }; 1180 | return { 1181 | week: { 1182 | fill: 'rgba(252, 248, 227, .6)' 1183 | }, 1184 | box: _objectSpread$3(_objectSpread$3({}, thickLine), {}, { 1185 | fill: bgColor 1186 | }), 1187 | line: line, 1188 | cline: redLine, 1189 | bline: thickLine, 1190 | label: text, 1191 | groupLabel: _objectSpread$3(_objectSpread$3({}, text), {}, { 1192 | 'font-weight': '600' 1193 | }), 1194 | text1: _objectSpread$3(_objectSpread$3(_objectSpread$3({}, text), smallText), {}, { 1195 | 'text-anchor': 'end' 1196 | }), 1197 | text2: _objectSpread$3(_objectSpread$3({}, text), smallText), 1198 | text3: _objectSpread$3(_objectSpread$3(_objectSpread$3({}, text), smallText), {}, { 1199 | 'text-anchor': 'middle' 1200 | }), 1201 | link: { 1202 | stroke: link, 1203 | 'stroke-width': '1.5px', 1204 | fill: 'none' 1205 | }, 1206 | linkArrow: { 1207 | fill: link 1208 | }, 1209 | milestone: { 1210 | fill: milestone 1211 | }, 1212 | groupBack: { 1213 | fill: groupBack 1214 | }, 1215 | groupFront: { 1216 | fill: groupFront 1217 | }, 1218 | taskBack: { 1219 | fill: taskBack 1220 | }, 1221 | taskFront: { 1222 | fill: taskFront 1223 | }, 1224 | warning: { 1225 | fill: warning 1226 | }, 1227 | danger: { 1228 | fill: danger 1229 | }, 1230 | ctrl: { 1231 | display: 'none', 1232 | fill: '#f0f0f0', 1233 | stroke: '#929292', 1234 | 'stroke-width': '1px' 1235 | } 1236 | }; 1237 | } 1238 | 1239 | var UNIT = { 1240 | day: DAY / 28, 1241 | week: 7 * DAY / 56, 1242 | month: 30 * DAY / 56 1243 | }; 1244 | 1245 | function NOOP() {} 1246 | 1247 | function Gantt(_ref) { 1248 | var _ref$data = _ref.data, 1249 | data = _ref$data === void 0 ? [] : _ref$data, 1250 | _ref$onClick = _ref.onClick, 1251 | onClick = _ref$onClick === void 0 ? NOOP : _ref$onClick, 1252 | _ref$viewMode = _ref.viewMode, 1253 | viewMode = _ref$viewMode === void 0 ? 'week' : _ref$viewMode, 1254 | _ref$maxTextWidth = _ref.maxTextWidth, 1255 | maxTextWidth = _ref$maxTextWidth === void 0 ? 140 : _ref$maxTextWidth, 1256 | _ref$offsetY = _ref.offsetY, 1257 | offsetY = _ref$offsetY === void 0 ? 60 : _ref$offsetY, 1258 | _ref$rowHeight = _ref.rowHeight, 1259 | rowHeight = _ref$rowHeight === void 0 ? 40 : _ref$rowHeight, 1260 | _ref$barHeight = _ref.barHeight, 1261 | barHeight = _ref$barHeight === void 0 ? 16 : _ref$barHeight, 1262 | _ref$thickWidth = _ref.thickWidth, 1263 | thickWidth = _ref$thickWidth === void 0 ? 1.4 : _ref$thickWidth, 1264 | _ref$styleOptions = _ref.styleOptions, 1265 | styleOptions = _ref$styleOptions === void 0 ? {} : _ref$styleOptions, 1266 | _ref$showLinks = _ref.showLinks, 1267 | showLinks = _ref$showLinks === void 0 ? true : _ref$showLinks, 1268 | _ref$showDelay = _ref.showDelay, 1269 | showDelay = _ref$showDelay === void 0 ? true : _ref$showDelay, 1270 | start = _ref.start, 1271 | end = _ref.end; 1272 | var unit = UNIT[viewMode]; 1273 | var minTime = start.getTime() - unit * 48; 1274 | var maxTime = end.getTime() + unit * 48; 1275 | var width = (maxTime - minTime) / unit + maxTextWidth; 1276 | var height = data.length * rowHeight + offsetY; 1277 | var box = "0 0 ".concat(width, " ").concat(height); 1278 | var current = Date.now(); 1279 | var styles = getStyles(styleOptions); 1280 | return h("svg", { 1281 | width: width, 1282 | height: height, 1283 | viewBox: box 1284 | }, h(Layout, { 1285 | styles: styles, 1286 | width: width, 1287 | height: height, 1288 | offsetY: offsetY, 1289 | thickWidth: thickWidth, 1290 | maxTextWidth: maxTextWidth 1291 | }), viewMode === 'day' ? h(DayHeader, { 1292 | styles: styles, 1293 | unit: unit, 1294 | height: height, 1295 | offsetY: offsetY, 1296 | minTime: minTime, 1297 | maxTime: maxTime, 1298 | maxTextWidth: maxTextWidth 1299 | }) : null, viewMode === 'week' ? h(WeekHeader, { 1300 | styles: styles, 1301 | unit: unit, 1302 | height: height, 1303 | offsetY: offsetY, 1304 | minTime: minTime, 1305 | maxTime: maxTime, 1306 | maxTextWidth: maxTextWidth 1307 | }) : null, viewMode === 'month' ? h(MonthHeader, { 1308 | styles: styles, 1309 | unit: unit, 1310 | offsetY: offsetY, 1311 | minTime: minTime, 1312 | maxTime: maxTime, 1313 | maxTextWidth: maxTextWidth 1314 | }) : null, h(Grid, { 1315 | styles: styles, 1316 | data: data, 1317 | width: width, 1318 | height: height, 1319 | offsetY: offsetY, 1320 | rowHeight: rowHeight, 1321 | maxTextWidth: maxTextWidth 1322 | }), maxTextWidth > 0 ? h(Labels, { 1323 | styles: styles, 1324 | data: data, 1325 | onClick: onClick, 1326 | offsetY: offsetY, 1327 | rowHeight: rowHeight 1328 | }) : null, showLinks ? h(LinkLine, { 1329 | styles: styles, 1330 | data: data, 1331 | unit: unit, 1332 | height: height, 1333 | current: current, 1334 | offsetY: offsetY, 1335 | minTime: minTime, 1336 | rowHeight: rowHeight, 1337 | barHeight: barHeight, 1338 | maxTextWidth: maxTextWidth 1339 | }) : null, h(Bar, { 1340 | styles: styles, 1341 | data: data, 1342 | unit: unit, 1343 | height: height, 1344 | current: current, 1345 | offsetY: offsetY, 1346 | minTime: minTime, 1347 | onClick: onClick, 1348 | showDelay: showDelay, 1349 | rowHeight: rowHeight, 1350 | barHeight: barHeight, 1351 | maxTextWidth: maxTextWidth 1352 | })); 1353 | } 1354 | 1355 | var _typeof_1 = createCommonjsModule(function (module) { 1356 | function _typeof(obj) { 1357 | "@babel/helpers - typeof"; 1358 | 1359 | if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { 1360 | module.exports = _typeof = function _typeof(obj) { 1361 | return typeof obj; 1362 | }; 1363 | 1364 | module.exports["default"] = module.exports, module.exports.__esModule = true; 1365 | } else { 1366 | module.exports = _typeof = function _typeof(obj) { 1367 | return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; 1368 | }; 1369 | 1370 | module.exports["default"] = module.exports, module.exports.__esModule = true; 1371 | } 1372 | 1373 | return _typeof(obj); 1374 | } 1375 | 1376 | module.exports = _typeof; 1377 | module.exports["default"] = module.exports, module.exports.__esModule = true; 1378 | }); 1379 | 1380 | var _typeof = /*@__PURE__*/getDefaultExportFromCjs(_typeof_1); 1381 | 1382 | var NS = 'http://www.w3.org/2000/svg'; 1383 | var doc = document; 1384 | 1385 | function applyProperties(node, props) { 1386 | Object.keys(props).forEach(function (k) { 1387 | var v = props[k]; 1388 | 1389 | if (k === 'style' && _typeof(v) === 'object') { 1390 | Object.keys(v).forEach(function (sk) { 1391 | // eslint-disable-next-line 1392 | node.style[sk] = v[sk]; 1393 | }); 1394 | } else if (k === 'onClick') { 1395 | if (typeof v === 'function') { 1396 | node.addEventListener('click', v); 1397 | } 1398 | } else { 1399 | node.setAttribute(k, v); 1400 | } 1401 | }); 1402 | } 1403 | 1404 | function render$2(vnode, ctx) { 1405 | var tag = vnode.tag, 1406 | props = vnode.props, 1407 | children = vnode.children; 1408 | var node = doc.createElementNS(NS, tag); 1409 | 1410 | if (props) { 1411 | applyProperties(node, props); 1412 | } 1413 | 1414 | children.forEach(function (v) { 1415 | node.appendChild(typeof v === 'string' ? doc.createTextNode(v) : render$2(v)); 1416 | }); 1417 | return node; 1418 | } 1419 | 1420 | function ownKeys$2(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) { symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); } keys.push.apply(keys, symbols); } return keys; } 1421 | 1422 | function _objectSpread$2(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys$2(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys$2(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; } 1423 | 1424 | var SVGGantt = /*#__PURE__*/function () { 1425 | function SVGGantt(element, data) { 1426 | var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; 1427 | 1428 | _classCallCheck(this, SVGGantt); 1429 | 1430 | this.dom = typeof element === 'string' ? document.querySelector(element) : element; 1431 | this.format(data); 1432 | this.options = options; 1433 | this.render(); 1434 | } 1435 | 1436 | _createClass(SVGGantt, [{ 1437 | key: "format", 1438 | value: function format(data) { 1439 | this.data = data; 1440 | var start = null; 1441 | var end = null; 1442 | data.forEach(function (v) { 1443 | start = minDate(start, v.start); 1444 | end = maxDate(end, v.end); 1445 | }); 1446 | this.start = start || new Date(); 1447 | this.end = end || new Date(); 1448 | } 1449 | }, { 1450 | key: "setData", 1451 | value: function setData(data) { 1452 | this.format(data); 1453 | this.render(); 1454 | } 1455 | }, { 1456 | key: "setOptions", 1457 | value: function setOptions(options) { 1458 | this.options = _objectSpread$2(_objectSpread$2({}, this.options), options); 1459 | this.render(); 1460 | } 1461 | }, { 1462 | key: "render", 1463 | value: function render() { 1464 | var data = this.data, 1465 | start = this.start, 1466 | end = this.end, 1467 | options = this.options; 1468 | 1469 | if (this.tree) { 1470 | this.dom.removeChild(this.tree); 1471 | } 1472 | 1473 | if (options.maxTextWidth === undefined) { 1474 | var font = getFont(options.styleOptions || {}); 1475 | 1476 | var w = function w(v) { 1477 | return textWidth(v.text, font, 20); 1478 | }; 1479 | 1480 | options.maxTextWidth = max(data.map(w), 0); 1481 | } 1482 | 1483 | var props = _objectSpread$2(_objectSpread$2({}, options), {}, { 1484 | start: start, 1485 | end: end 1486 | }); 1487 | 1488 | this.tree = render$2(h(Gantt, _extends({ 1489 | data: data 1490 | }, props))); 1491 | this.dom.appendChild(this.tree); 1492 | } 1493 | }]); 1494 | 1495 | return SVGGantt; 1496 | }(); 1497 | 1498 | function render$1(vnode, ctx, e) { 1499 | var tag = vnode.tag, 1500 | props = vnode.props, 1501 | children = vnode.children; 1502 | 1503 | if (tag === 'svg') { 1504 | var width = props.width, 1505 | height = props.height; 1506 | ctx.width = width; 1507 | ctx.height = height; 1508 | } 1509 | 1510 | if (tag === 'line') { 1511 | var x1 = props.x1, 1512 | x2 = props.x2, 1513 | y1 = props.y1, 1514 | y2 = props.y2, 1515 | _props$style = props.style, 1516 | style = _props$style === void 0 ? {} : _props$style; 1517 | 1518 | if (style.stroke) { 1519 | ctx.strokeStyle = style.stroke; 1520 | ctx.lineWidth = parseFloat(style['stroke-width'] || 1); 1521 | } 1522 | 1523 | ctx.beginPath(); 1524 | ctx.moveTo(x1, y1); 1525 | ctx.lineTo(x2, y2); 1526 | ctx.stroke(); 1527 | } 1528 | 1529 | if (tag === 'polyline' || tag === 'polygon') { 1530 | var points = props.points, 1531 | _props$style2 = props.style, 1532 | _style = _props$style2 === void 0 ? {} : _props$style2; 1533 | 1534 | var p = s2p(points); 1535 | 1536 | if (_style.stroke) { 1537 | ctx.strokeStyle = _style.stroke; 1538 | ctx.lineWidth = parseFloat(_style['stroke-width'] || 1); 1539 | } 1540 | 1541 | if (_style.fill) { 1542 | ctx.fillStyle = _style.fill; 1543 | } 1544 | 1545 | ctx.beginPath(); 1546 | ctx.moveTo(p[0][0], p[0][1]); 1547 | 1548 | for (var i = 1; i < p.length; i++) { 1549 | ctx.lineTo(p[i][0], p[i][1]); 1550 | } 1551 | 1552 | if (tag === 'polyline') { 1553 | ctx.stroke(); 1554 | } else { 1555 | ctx.fill(); 1556 | } 1557 | } 1558 | 1559 | if (tag === 'rect') { 1560 | var x = props.x, 1561 | y = props.y, 1562 | _width = props.width, 1563 | _height = props.height, 1564 | _props$rx = props.rx, 1565 | rx = _props$rx === void 0 ? 0 : _props$rx, 1566 | _props$ry = props.ry, 1567 | ry = _props$ry === void 0 ? 0 : _props$ry, 1568 | onClick = props.onClick, 1569 | _props$style3 = props.style, 1570 | _style2 = _props$style3 === void 0 ? {} : _props$style3; // From https://github.com/canvg/canvg 1571 | 1572 | 1573 | ctx.beginPath(); 1574 | ctx.moveTo(x + rx, y); 1575 | ctx.lineTo(x + _width - rx, y); 1576 | ctx.quadraticCurveTo(x + _width, y, x + _width, y + ry); 1577 | ctx.lineTo(x + _width, y + _height - ry); 1578 | ctx.quadraticCurveTo(x + _width, y + _height, x + _width - rx, y + _height); 1579 | ctx.lineTo(x + rx, y + _height); 1580 | ctx.quadraticCurveTo(x, y + _height, x, y + _height - ry); 1581 | ctx.lineTo(x, y + ry); 1582 | ctx.quadraticCurveTo(x, y, x + rx, y); 1583 | 1584 | if (e && onClick && ctx.isPointInPath(e.x, e.y)) { 1585 | onClick(); 1586 | } 1587 | 1588 | ctx.closePath(); 1589 | 1590 | if (_style2.fill) { 1591 | ctx.fillStyle = _style2.fill; 1592 | } 1593 | 1594 | ctx.fill(); 1595 | 1596 | if (_style2.stroke) { 1597 | ctx.strokeStyle = _style2.stroke; 1598 | ctx.lineWidth = parseFloat(_style2['stroke-width'] || 1); 1599 | ctx.stroke(); 1600 | } 1601 | } 1602 | 1603 | if (tag === 'text') { 1604 | var _x = props.x, 1605 | _y = props.y, 1606 | _style3 = props.style; 1607 | 1608 | if (_style3) { 1609 | ctx.fillStyle = _style3.fill; 1610 | var BL = { 1611 | central: 'middle', 1612 | middle: 'middle', 1613 | hanging: 'hanging', 1614 | alphabetic: 'alphabetic', 1615 | ideographic: 'ideographic' 1616 | }; 1617 | var AL = { 1618 | start: 'start', 1619 | middle: 'center', 1620 | end: 'end' 1621 | }; 1622 | ctx.textBaseline = BL[_style3['dominant-baseline']] || 'alphabetic'; 1623 | ctx.textAlign = AL[_style3['text-anchor']] || 'start'; 1624 | ctx.font = "".concat(_style3['font-weight'] || '', " ").concat(_style3['font-size'], " ").concat(_style3['font-family']); 1625 | } 1626 | 1627 | ctx.fillText(children.join(''), _x, _y); 1628 | } 1629 | 1630 | children.forEach(function (v) { 1631 | if (typeof v !== 'string') { 1632 | render$1(v, ctx, e); 1633 | } 1634 | }); 1635 | } 1636 | 1637 | function createContext(dom) { 1638 | var canvas = typeof dom === 'string' ? document.querySelector(dom) : dom; 1639 | var ctx = canvas.getContext('2d'); 1640 | var backingStore = ctx.webkitBackingStorePixelRatio || ctx.mozBackingStorePixelRatio || ctx.msBackingStorePixelRatio || ctx.oBackingStorePixelRatio || ctx.backingStorePixelRatio || 1; 1641 | var ratio = (window.devicePixelRatio || 1) / backingStore; 1642 | ['width', 'height'].forEach(function (key) { 1643 | Object.defineProperty(ctx, key, { 1644 | get: function get() { 1645 | return canvas[key] / ratio; 1646 | }, 1647 | set: function set(v) { 1648 | canvas[key] = v * ratio; 1649 | canvas.style[key] = "".concat(v, "px"); 1650 | ctx.scale(ratio, ratio); 1651 | }, 1652 | enumerable: true, 1653 | configurable: true 1654 | }); 1655 | }); 1656 | canvas.addEventListener('click', function (e) { 1657 | if (!ctx.onClick) return; 1658 | var rect = canvas.getBoundingClientRect(); 1659 | ctx.onClick({ 1660 | x: (e.clientX - rect.left) * ratio, 1661 | y: (e.clientY - rect.top) * ratio 1662 | }); 1663 | }); 1664 | return ctx; 1665 | } 1666 | 1667 | function ownKeys$1(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) { symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); } keys.push.apply(keys, symbols); } return keys; } 1668 | 1669 | function _objectSpread$1(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys$1(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys$1(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; } 1670 | 1671 | var CanvasGantt = /*#__PURE__*/function () { 1672 | function CanvasGantt(element, data) { 1673 | var _this = this; 1674 | 1675 | var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; 1676 | 1677 | _classCallCheck(this, CanvasGantt); 1678 | 1679 | this.ctx = createContext(element); 1680 | this.format(data); 1681 | this.options = options; 1682 | this.render(); 1683 | 1684 | this.ctx.onClick = function (e) { 1685 | return _this.render(e); 1686 | }; 1687 | } 1688 | 1689 | _createClass(CanvasGantt, [{ 1690 | key: "format", 1691 | value: function format(data) { 1692 | this.data = data; 1693 | var start = null; 1694 | var end = null; 1695 | data.forEach(function (v) { 1696 | start = minDate(start, v.start); 1697 | end = maxDate(end, v.end); 1698 | }); 1699 | this.start = start || new Date(); 1700 | this.end = end || new Date(); 1701 | } 1702 | }, { 1703 | key: "setData", 1704 | value: function setData(data) { 1705 | this.format(data); 1706 | this.render(); 1707 | } 1708 | }, { 1709 | key: "setOptions", 1710 | value: function setOptions(options) { 1711 | this.options = _objectSpread$1(_objectSpread$1({}, this.options), options); 1712 | this.render(); 1713 | } 1714 | }, { 1715 | key: "render", 1716 | value: function render(e) { 1717 | var data = this.data, 1718 | start = this.start, 1719 | end = this.end, 1720 | options = this.options; 1721 | 1722 | if (options.maxTextWidth === undefined) { 1723 | var font = getFont(options.styleOptions || {}); 1724 | 1725 | var w = function w(v) { 1726 | return textWidth(v.text, font, 20); 1727 | }; 1728 | 1729 | options.maxTextWidth = max(data.map(w), 0); 1730 | } 1731 | 1732 | var props = _objectSpread$1(_objectSpread$1({}, options), {}, { 1733 | start: start, 1734 | end: end 1735 | }); 1736 | 1737 | render$1(h(Gantt, _extends({ 1738 | data: data 1739 | }, props)), this.ctx, e); 1740 | } 1741 | }]); 1742 | 1743 | return CanvasGantt; 1744 | }(); 1745 | 1746 | function attrEscape(str) { 1747 | return String(str).replace(/&/g, '&').replace(//g, '>').replace(/\r/g, ' '); 1752 | } 1753 | 1754 | function render(vnode, ctx) { 1755 | var tag = vnode.tag, 1756 | props = vnode.props, 1757 | children = vnode.children; 1758 | var tokens = []; 1759 | tokens.push("<".concat(tag)); 1760 | Object.keys(props || {}).forEach(function (k) { 1761 | var v = props[k]; 1762 | if (k === 'onClick') return; 1763 | 1764 | if (k === 'style' && _typeof(v) === 'object') { 1765 | v = Object.keys(v).map(function (i) { 1766 | return "".concat(i, ":").concat(v[i], ";"); 1767 | }).join(''); 1768 | } 1769 | 1770 | tokens.push(" ".concat(k, "=\"").concat(attrEscape(v), "\"")); 1771 | }); 1772 | 1773 | if (!children || !children.length) { 1774 | tokens.push(' />'); 1775 | return tokens.join(''); 1776 | } 1777 | 1778 | tokens.push('>'); 1779 | children.forEach(function (v) { 1780 | if (typeof v === 'string') { 1781 | tokens.push(escape(v)); 1782 | } else { 1783 | tokens.push(render(v)); 1784 | } 1785 | }); 1786 | tokens.push("")); 1787 | return tokens.join(''); 1788 | } 1789 | 1790 | function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) { symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); } keys.push.apply(keys, symbols); } return keys; } 1791 | 1792 | function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; } 1793 | 1794 | var StrGantt = /*#__PURE__*/function () { 1795 | function StrGantt(data) { 1796 | var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; 1797 | 1798 | _classCallCheck(this, StrGantt); 1799 | 1800 | this.format(data); 1801 | this.options = options; 1802 | } 1803 | 1804 | _createClass(StrGantt, [{ 1805 | key: "format", 1806 | value: function format(data) { 1807 | this.data = data; 1808 | var start = null; 1809 | var end = null; 1810 | data.forEach(function (v) { 1811 | start = minDate(start, v.start); 1812 | end = maxDate(end, v.end); 1813 | }); 1814 | this.start = start || new Date(); 1815 | this.end = end || new Date(); 1816 | } 1817 | }, { 1818 | key: "setData", 1819 | value: function setData(data) { 1820 | this.format(data); 1821 | } 1822 | }, { 1823 | key: "setOptions", 1824 | value: function setOptions(options) { 1825 | this.options = _objectSpread(_objectSpread({}, this.options), options); 1826 | } 1827 | }, { 1828 | key: "render", 1829 | value: function render$1() { 1830 | var data = this.data, 1831 | start = this.start, 1832 | end = this.end, 1833 | options = this.options; 1834 | 1835 | var props = _objectSpread(_objectSpread({}, options), {}, { 1836 | start: start, 1837 | end: end 1838 | }); 1839 | 1840 | return render(h(Gantt, _extends({ 1841 | data: data 1842 | }, props))); 1843 | } 1844 | }]); 1845 | 1846 | return StrGantt; 1847 | }(); 1848 | 1849 | exports.CanvasGantt = CanvasGantt; 1850 | exports.SVGGantt = SVGGantt; 1851 | exports.StrGantt = StrGantt; 1852 | exports.default = CanvasGantt; 1853 | exports.utils = utils; 1854 | 1855 | Object.defineProperty(exports, '__esModule', { value: true }); 1856 | 1857 | }))); 1858 | -------------------------------------------------------------------------------- /dist/gantt.min.js: -------------------------------------------------------------------------------- 1 | (function(global,factory){typeof exports==="object"&&typeof module!=="undefined"?factory(exports):typeof define==="function"&&define.amd?define(["exports"],factory):(global=typeof globalThis!=="undefined"?globalThis:global||self,factory(global.Gantt={}))})(this,function(exports){"use strict";function getDefaultExportFromCjs(x){return x&&x.__esModule&&Object.prototype.hasOwnProperty.call(x,"default")?x["default"]:x}function createCommonjsModule(fn,basedir,module){return module={path:basedir,exports:{},require:function(path,base){return commonjsRequire(path,base===undefined||base===null?module.path:base)}},fn(module,module.exports),module.exports}function commonjsRequire(){throw new Error("Dynamic requires are not currently supported by @rollup/plugin-commonjs")}var _extends_1=createCommonjsModule(function(module){function _extends(){module.exports=_extends=Object.assign||function(target){for(var i=1;i2?_len-2:0),_key=2;_key<_len;_key++){children[_key-2]=arguments[_key]}addChild(children,childNodes);if(typeof tag==="function"){return tag(_objectSpread$5(_objectSpread$5({},props),{},{children:childNodes}))}return{tag:tag,props:props,children:childNodes}}function ownKeys$4(object,enumerableOnly){var keys=Object.keys(object);if(Object.getOwnPropertySymbols){var symbols=Object.getOwnPropertySymbols(object);if(enumerableOnly){symbols=symbols.filter(function(sym){return Object.getOwnPropertyDescriptor(object,sym).enumerable})}keys.push.apply(keys,symbols)}return keys}function _objectSpread$4(target){for(var i=1;i9?m:"0".concat(m))}function formatDay(date){var m=date.getMonth()+1;var d=date.getDate();return"".concat(m,"/").concat(d)}function minDate(a,b){if(a&&b){return a>b?b:a}return a||b}function maxDate(a,b){if(a&&b){return a=0;i--){stack.push(node.children[i])}}});return list}function hasPath(vmap,a,b){var stack=[];stack.push(vmap[a]);while(stack.length){var v=stack.pop();if(v.id===b){return true}for(var i=0;i2&&arguments[2]!==undefined?arguments[2]:false;var vmap={};links.forEach(function(l){vmap[l.source]={id:l.source,links:[]};vmap[l.target]={id:l.target,links:[]}});var dag=[];links.forEach(function(l){var source=l.source,target=l.target;if(!hasPath(vmap,target,source)){dag.push(l);vmap[source].links.push(vmap[target])}});var sorted=toposort(dag);var tmap={};for(var i=0;i50?h("text",{x:x+t/2,y:offsetY*.25,style:styles.text3},str):null))}return h("g",null,ticks)}function DayHeader(_ref){var styles=_ref.styles,unit=_ref.unit,minTime=_ref.minTime,maxTime=_ref.maxTime,height=_ref.height,offsetY=_ref.offsetY,maxTextWidth=_ref.maxTextWidth;var dates=getDates(minTime,maxTime);var ticks=[];var x0=maxTextWidth;var y0=offsetY/2;var RH=height-y0;var len=dates.length-1;for(var i=0;i28?h("text",{x:x-3,y:offsetY*.75,style:styles.text1},prevDay):null))}return h("g",null,h(YearMonth,{styles:styles,unit:unit,dates:dates,offsetY:offsetY,minTime:minTime,maxTime:maxTime,maxTextWidth:maxTextWidth}),ticks)}function Year(_ref){var styles=_ref.styles,months=_ref.months,unit=_ref.unit,offsetY=_ref.offsetY,minTime=_ref.minTime,maxTime=_ref.maxTime,maxTextWidth=_ref.maxTextWidth;var years=months.filter(function(v){return new Date(v).getMonth()===0});years.unshift(minTime);years.push(maxTime);var ticks=[];var x0=maxTextWidth;var y2=offsetY/2;var len=years.length-1;for(var i=0;i35?h("text",{x:x+t/2,y:offsetY*.25,style:styles.text3},cur.getFullYear()):null))}return h("g",null,ticks)}function MonthHeader(_ref){var styles=_ref.styles,unit=_ref.unit,minTime=_ref.minTime,maxTime=_ref.maxTime,offsetY=_ref.offsetY,maxTextWidth=_ref.maxTextWidth;var MONTH=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];var dates=getDates(minTime,maxTime);var months=dates.filter(function(v){return new Date(v).getDate()===1});months.unshift(minTime);months.push(maxTime);var ticks=[];var x0=maxTextWidth;var y0=offsetY/2;var len=months.length-1;for(var i=0;i30?h("text",{x:x+t/2,y:offsetY*.75,style:styles.text3},MONTH[month]):null))}return h("g",null,h(Year,{styles:styles,unit:unit,months:months,offsetY:offsetY,minTime:minTime,maxTime:maxTime,maxTextWidth:maxTextWidth}),ticks)}function Grid(_ref){var styles=_ref.styles,data=_ref.data,width=_ref.width,height=_ref.height,offsetY=_ref.offsetY,rowHeight=_ref.rowHeight,maxTextWidth=_ref.maxTextWidth;return h("g",null,data.map(function(v,i){var y=(i+1)*rowHeight+offsetY;return h("line",{key:i,x1:0,x2:width,y1:y,y2:y,style:styles.line})}),h("line",{x1:maxTextWidth,x2:maxTextWidth,y1:0,y2:height,style:styles.bline}))}function Labels(_ref){var styles=_ref.styles,data=_ref.data,_onClick=_ref.onClick,rowHeight=_ref.rowHeight,offsetY=_ref.offsetY;return h("g",null,data.map(function(v,i){return h("text",{key:i,x:10,y:(i+.5)*rowHeight+offsetY,class:"gantt-label",style:styles.label,onClick:function onClick(){return _onClick(v)}},v.text)}))}function LinkLine(_ref){var styles=_ref.styles,data=_ref.data,unit=_ref.unit,offsetY=_ref.offsetY,minTime=_ref.minTime,rowHeight=_ref.rowHeight,barHeight=_ref.barHeight,maxTextWidth=_ref.maxTextWidth;var x0=maxTextWidth;var y0=rowHeight/2+offsetY;var map={};data.forEach(function(v,i){map[v.id]=i});return h("g",null,data.map(function(s,i){if(!s.end||!s.start||!s.links){return null}return s.links.map(function(l){var j=map[l.target];var e=data[j];if(!e||!e.start||!e.end)return null;var gap=12;var arrow=6;var mgap=e.type==="milestone"?barHeight/2:0;var y1=y0+i*rowHeight;var y2=y0+j*rowHeight;var vgap=barHeight/2+4;if(y1>y2){vgap=-vgap}if(l.type==="FS"){var x1=x0+(s.end-minTime)/unit;var x2=x0+(e.start-minTime)/unit-mgap;var p1=[[x1,y1],[x1+gap,y1]];if(x2-x1>=2*gap){p1.push([x1+gap,y2])}else{p1.push([x1+gap,y2-vgap]);p1.push([x2-gap,y2-vgap]);p1.push([x2-gap,y2])}p1.push([x2-arrow,y2]);var p2=[[x2-arrow,y2-arrow],[x2,y2],[x2-arrow,y2+arrow]];return h("g",null,h("polyline",{points:p2s(p1),style:styles.link}),h("polygon",{points:p2s(p2),style:styles.linkArrow}))}if(l.type==="FF"){var _x=x0+(s.end-minTime)/unit;var _x2=x0+(e.end-minTime)/unit+mgap;var _p=[[_x,y1],[_x+gap,y1]];if(_x2<=_x){_p.push([_x+gap,y2])}else{_p.push([_x+gap,y2-vgap]);_p.push([_x2+gap,y2-vgap]);_p.push([_x2+gap,y2])}_p.push([_x2+arrow,y2]);var _p2=[[_x2+arrow,y2-arrow],[_x2,y2],[_x2+arrow,y2+arrow]];return h("g",null,h("polyline",{points:p2s(_p),style:styles.link}),h("polygon",{points:p2s(_p2),style:styles.linkArrow}))}if(l.type==="SS"){var _x3=x0+(s.start-minTime)/unit;var _x4=x0+(e.start-minTime)/unit-mgap;var _p3=[[_x3,y1],[_x3-gap,y1]];if(_x3<=_x4){_p3.push([_x3-gap,y2])}else{_p3.push([_x3-gap,y2-vgap]);_p3.push([_x4-gap,y2-vgap]);_p3.push([_x4-gap,y2])}_p3.push([_x4-arrow,y2]);var _p4=[[_x4-arrow,y2-arrow],[_x4,y2],[_x4-arrow,y2+arrow]];return h("g",null,h("polyline",{points:p2s(_p3),style:styles.link}),h("polygon",{points:p2s(_p4),style:styles.linkArrow}))}if(l.type==="SF"){var _x5=x0+(s.start-minTime)/unit;var _x6=x0+(e.end-minTime)/unit+mgap;var _p5=[[_x5,y1],[_x5-gap,y1]];if(_x5-_x6>=2*gap){_p5.push([_x5-gap,y2])}else{_p5.push([_x5-gap,y2-vgap]);_p5.push([_x6+gap,y2-vgap]);_p5.push([_x6+gap,y2])}_p5.push([_x6+arrow,y2]);var _p6=[[_x6+arrow,y2-arrow],[_x6,y2],[_x6+arrow,y2+arrow]];return h("g",null,h("polyline",{points:p2s(_p5),style:styles.link}),h("polygon",{points:p2s(_p6),style:styles.linkArrow}))}return null})}))}function Bar(_ref){var styles=_ref.styles,data=_ref.data,unit=_ref.unit,height=_ref.height,offsetY=_ref.offsetY,minTime=_ref.minTime,showDelay=_ref.showDelay,rowHeight=_ref.rowHeight,barHeight=_ref.barHeight,maxTextWidth=_ref.maxTextWidth,current=_ref.current,onClick=_ref.onClick;var x0=maxTextWidth;var y0=(rowHeight-barHeight)/2+offsetY;var cur=x0+(current-minTime)/unit;return h("g",null,current>minTime?h("line",{x1:cur,x2:cur,y1:offsetY,y2:height,style:styles.cline}):null,data.map(function(v,i){if(!v.end||!v.start){return null}var handler=function handler(){return onClick(v)};var x=x0+(v.start-minTime)/unit;var y=y0+i*rowHeight;var cy=y+barHeight/2;if(v.type==="milestone"){var size=barHeight/2;var points=[[x,cy-size],[x+size,cy],[x,cy+size],[x-size,cy]].map(function(p){return"".concat(p[0],",").concat(p[1])}).join(" ");return h("g",{key:i,class:"gantt-bar",style:{cursor:"pointer"},onClick:handler},h("polygon",{points:points,style:styles.milestone,onClick:handler}),h("circle",{class:"gantt-ctrl-start","data-id":v.id,cx:x,cy:cy,r:6,style:styles.ctrl}))}var w1=(v.end-v.start)/unit;var w2=w1*v.percent;var bar=v.type==="group"?{back:styles.groupBack,front:styles.groupFront}:{back:styles.taskBack,front:styles.taskFront};if(showDelay){if(x+w21e-6?h("rect",{x:x,y:y,width:w2,height:barHeight,rx:1.8,ry:1.8,style:bar.front}):null,v.type==="group"?null:h("g",null,h("circle",{class:"gantt-ctrl-start","data-id":v.id,cx:x-12,cy:cy,r:6,style:styles.ctrl}),h("circle",{class:"gantt-ctrl-finish","data-id":v.id,cx:x+w1+12,cy:cy,r:6,style:styles.ctrl})))}))}function ownKeys$3(object,enumerableOnly){var keys=Object.keys(object);if(Object.getOwnPropertySymbols){var symbols=Object.getOwnPropertySymbols(object);if(enumerableOnly){symbols=symbols.filter(function(sym){return Object.getOwnPropertyDescriptor(object,sym).enumerable})}keys.push.apply(keys,symbols)}return keys}function _objectSpread$3(target){for(var i=1;i0?h(Labels,{styles:styles,data:data,onClick:onClick,offsetY:offsetY,rowHeight:rowHeight}):null,showLinks?h(LinkLine,{styles:styles,data:data,unit:unit,height:height,current:current,offsetY:offsetY,minTime:minTime,rowHeight:rowHeight,barHeight:barHeight,maxTextWidth:maxTextWidth}):null,h(Bar,{styles:styles,data:data,unit:unit,height:height,current:current,offsetY:offsetY,minTime:minTime,onClick:onClick,showDelay:showDelay,rowHeight:rowHeight,barHeight:barHeight,maxTextWidth:maxTextWidth}))}var _typeof_1=createCommonjsModule(function(module){function _typeof(obj){"@babel/helpers - typeof";if(typeof Symbol==="function"&&typeof Symbol.iterator==="symbol"){module.exports=_typeof=function _typeof(obj){return typeof obj};module.exports["default"]=module.exports,module.exports.__esModule=true}else{module.exports=_typeof=function _typeof(obj){return obj&&typeof Symbol==="function"&&obj.constructor===Symbol&&obj!==Symbol.prototype?"symbol":typeof obj};module.exports["default"]=module.exports,module.exports.__esModule=true}return _typeof(obj)}module.exports=_typeof;module.exports["default"]=module.exports,module.exports.__esModule=true});var _typeof=getDefaultExportFromCjs(_typeof_1);var NS="http://www.w3.org/2000/svg";var doc=document;function applyProperties(node,props){Object.keys(props).forEach(function(k){var v=props[k];if(k==="style"&&_typeof(v)==="object"){Object.keys(v).forEach(function(sk){node.style[sk]=v[sk]})}else if(k==="onClick"){if(typeof v==="function"){node.addEventListener("click",v)}}else{node.setAttribute(k,v)}})}function render$2(vnode,ctx){var tag=vnode.tag,props=vnode.props,children=vnode.children;var node=doc.createElementNS(NS,tag);if(props){applyProperties(node,props)}children.forEach(function(v){node.appendChild(typeof v==="string"?doc.createTextNode(v):render$2(v))});return node}function ownKeys$2(object,enumerableOnly){var keys=Object.keys(object);if(Object.getOwnPropertySymbols){var symbols=Object.getOwnPropertySymbols(object);if(enumerableOnly){symbols=symbols.filter(function(sym){return Object.getOwnPropertyDescriptor(object,sym).enumerable})}keys.push.apply(keys,symbols)}return keys}function _objectSpread$2(target){for(var i=1;i2&&arguments[2]!==undefined?arguments[2]:{};_classCallCheck(this,SVGGantt);this.dom=typeof element==="string"?document.querySelector(element):element;this.format(data);this.options=options;this.render()}_createClass(SVGGantt,[{key:"format",value:function format(data){this.data=data;var start=null;var end=null;data.forEach(function(v){start=minDate(start,v.start);end=maxDate(end,v.end)});this.start=start||new Date;this.end=end||new Date}},{key:"setData",value:function setData(data){this.format(data);this.render()}},{key:"setOptions",value:function setOptions(options){this.options=_objectSpread$2(_objectSpread$2({},this.options),options);this.render()}},{key:"render",value:function render(){var data=this.data,start=this.start,end=this.end,options=this.options;if(this.tree){this.dom.removeChild(this.tree)}if(options.maxTextWidth===undefined){var font=getFont(options.styleOptions||{});var w=function w(v){return textWidth(v.text,font,20)};options.maxTextWidth=max(data.map(w),0)}var props=_objectSpread$2(_objectSpread$2({},options),{},{start:start,end:end});this.tree=render$2(h(Gantt,_extends({data:data},props)));this.dom.appendChild(this.tree)}}]);return SVGGantt}();function render$1(vnode,ctx,e){var tag=vnode.tag,props=vnode.props,children=vnode.children;if(tag==="svg"){var width=props.width,height=props.height;ctx.width=width;ctx.height=height}if(tag==="line"){var x1=props.x1,x2=props.x2,y1=props.y1,y2=props.y2,_props$style=props.style,style=_props$style===void 0?{}:_props$style;if(style.stroke){ctx.strokeStyle=style.stroke;ctx.lineWidth=parseFloat(style["stroke-width"]||1)}ctx.beginPath();ctx.moveTo(x1,y1);ctx.lineTo(x2,y2);ctx.stroke()}if(tag==="polyline"||tag==="polygon"){var points=props.points,_props$style2=props.style,_style=_props$style2===void 0?{}:_props$style2;var p=s2p(points);if(_style.stroke){ctx.strokeStyle=_style.stroke;ctx.lineWidth=parseFloat(_style["stroke-width"]||1)}if(_style.fill){ctx.fillStyle=_style.fill}ctx.beginPath();ctx.moveTo(p[0][0],p[0][1]);for(var i=1;i2&&arguments[2]!==undefined?arguments[2]:{};_classCallCheck(this,CanvasGantt);this.ctx=createContext(element);this.format(data);this.options=options;this.render();this.ctx.onClick=function(e){return _this.render(e)}}_createClass(CanvasGantt,[{key:"format",value:function format(data){this.data=data;var start=null;var end=null;data.forEach(function(v){start=minDate(start,v.start);end=maxDate(end,v.end)});this.start=start||new Date;this.end=end||new Date}},{key:"setData",value:function setData(data){this.format(data);this.render()}},{key:"setOptions",value:function setOptions(options){this.options=_objectSpread$1(_objectSpread$1({},this.options),options);this.render()}},{key:"render",value:function render(e){var data=this.data,start=this.start,end=this.end,options=this.options;if(options.maxTextWidth===undefined){var font=getFont(options.styleOptions||{});var w=function w(v){return textWidth(v.text,font,20)};options.maxTextWidth=max(data.map(w),0)}var props=_objectSpread$1(_objectSpread$1({},options),{},{start:start,end:end});render$1(h(Gantt,_extends({data:data},props)),this.ctx,e)}}]);return CanvasGantt}();function attrEscape(str){return String(str).replace(/&/g,"&").replace(//g,">").replace(/\r/g," ")}function render(vnode,ctx){var tag=vnode.tag,props=vnode.props,children=vnode.children;var tokens=[];tokens.push("<".concat(tag));Object.keys(props||{}).forEach(function(k){var v=props[k];if(k==="onClick")return;if(k==="style"&&_typeof(v)==="object"){v=Object.keys(v).map(function(i){return"".concat(i,":").concat(v[i],";")}).join("")}tokens.push(" ".concat(k,'="').concat(attrEscape(v),'"'))});if(!children||!children.length){tokens.push(" />");return tokens.join("")}tokens.push(">");children.forEach(function(v){if(typeof v==="string"){tokens.push(escape(v))}else{tokens.push(render(v))}});tokens.push(""));return tokens.join("")}function ownKeys(object,enumerableOnly){var keys=Object.keys(object);if(Object.getOwnPropertySymbols){var symbols=Object.getOwnPropertySymbols(object);if(enumerableOnly){symbols=symbols.filter(function(sym){return Object.getOwnPropertyDescriptor(object,sym).enumerable})}keys.push.apply(keys,symbols)}return keys}function _objectSpread(target){for(var i=1;i1&&arguments[1]!==undefined?arguments[1]:{};_classCallCheck(this,StrGantt);this.format(data);this.options=options}_createClass(StrGantt,[{key:"format",value:function format(data){this.data=data;var start=null;var end=null;data.forEach(function(v){start=minDate(start,v.start);end=maxDate(end,v.end)});this.start=start||new Date;this.end=end||new Date}},{key:"setData",value:function setData(data){this.format(data)}},{key:"setOptions",value:function setOptions(options){this.options=_objectSpread(_objectSpread({},this.options),options)}},{key:"render",value:function render$1(){var data=this.data,start=this.start,end=this.end,options=this.options;var props=_objectSpread(_objectSpread({},options),{},{start:start,end:end});return render(h(Gantt,_extends({data:data},props)))}}]);return StrGantt}();exports.CanvasGantt=CanvasGantt;exports.SVGGantt=SVGGantt;exports.StrGantt=StrGantt;exports.default=CanvasGantt;exports.utils=utils;Object.defineProperty(exports,"__esModule",{value:true})}); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gantt", 3 | "version": "3.1.1", 4 | "description": "Gantt chart library using jsx support SVG, Canvas and SSR", 5 | "main": "lib/index.js", 6 | "module": "es/index.js", 7 | "jsnext:main": "es/index.js", 8 | "files": [ 9 | "lib", 10 | "es", 11 | "src", 12 | "dist" 13 | ], 14 | "scripts": { 15 | "lint": "eslint src", 16 | "clean": "rimraf lib es dist", 17 | "build:lib": "cross-env BABEL_ENV=cjs babel src -d lib", 18 | "build:es": "cross-env BABEL_ENV=es babel src -d es", 19 | "build:dist": "cross-env NODE_ENV=production rollup -c && uglifyjs -o dist/gantt.min.js dist/gantt.js", 20 | "build": "npm run clean && npm run build:lib && npm run build:es && npm run build:dist", 21 | "prepare": "npm run build", 22 | "pages": "cd demo && dool build && gh-pages -d ." 23 | }, 24 | "repository": { 25 | "type": "git", 26 | "url": "git+https://github.com/d-band/gantt.git" 27 | }, 28 | "keywords": [ 29 | "gantt", 30 | "chart", 31 | "jsx", 32 | "svg", 33 | "canvas", 34 | "vdom", 35 | "ssr" 36 | ], 37 | "author": "d-band", 38 | "license": "MIT", 39 | "bugs": { 40 | "url": "https://github.com/d-band/gantt/issues" 41 | }, 42 | "homepage": "https://github.com/d-band/gantt#readme", 43 | "devDependencies": { 44 | "@babel/cli": "^7.10.5", 45 | "@babel/core": "^7.11.4", 46 | "@babel/plugin-external-helpers": "^7.10.4", 47 | "@babel/plugin-transform-react-jsx": "^7.10.4", 48 | "@babel/plugin-transform-runtime": "^7.11.0", 49 | "@babel/preset-env": "^7.11.0", 50 | "@babel/runtime": "^7.11.2", 51 | "@rollup/plugin-babel": "^5.2.0", 52 | "@rollup/plugin-commonjs": "^15.0.0", 53 | "@rollup/plugin-node-resolve": "^9.0.0", 54 | "babel-eslint": "^10.1.0", 55 | "cross-env": "^7.0.2", 56 | "eslint": "^7.7.0", 57 | "eslint-config-airbnb": "^18.2.0", 58 | "eslint-plugin-import": "^2.22.0", 59 | "eslint-plugin-jsx-a11y": "^6.3.1", 60 | "eslint-plugin-react": "^7.20.6", 61 | "eslint-plugin-react-hooks": "^4.1.0", 62 | "gh-pages": "^3.1.0", 63 | "rimraf": "^3.0.2", 64 | "rollup": "^2.0.0", 65 | "uglify-js": "^3.10.3" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from '@rollup/plugin-babel'; 2 | import commonjs from '@rollup/plugin-commonjs'; 3 | import nodeResolve from '@rollup/plugin-node-resolve'; 4 | 5 | export default { 6 | output: { 7 | file: 'dist/gantt.js', 8 | format: 'umd', 9 | name: 'Gantt', 10 | exports: 'named' 11 | }, 12 | input: 'src/index.js', 13 | plugins: [ 14 | nodeResolve(), 15 | commonjs({ 16 | include: 'node_modules/**' 17 | }), 18 | babel({ 19 | babelrc: false, 20 | presets: [ 21 | ['@babel/preset-env', { 22 | modules: false 23 | }] 24 | ], 25 | plugins: [ 26 | ['@babel/plugin-transform-react-jsx', { 27 | 'pragma': 'h' 28 | }], 29 | '@babel/plugin-external-helpers', 30 | '@babel/plugin-transform-runtime' 31 | ], 32 | babelHelpers: 'runtime', 33 | exclude: 'node_modules/**' 34 | }) 35 | ] 36 | }; 37 | -------------------------------------------------------------------------------- /src/CanvasGantt.js: -------------------------------------------------------------------------------- 1 | import h from './h'; 2 | import Gantt from './gantt'; 3 | import render from './render/canvas'; 4 | import createContext from './context'; 5 | import { getFont } from './gantt/styles'; 6 | import { 7 | minDate, maxDate, textWidth, max 8 | } from './utils'; 9 | 10 | export default class CanvasGantt { 11 | constructor(element, data, options = {}) { 12 | this.ctx = createContext(element); 13 | this.format(data); 14 | this.options = options; 15 | this.render(); 16 | this.ctx.onClick = (e) => this.render(e); 17 | } 18 | format(data) { 19 | this.data = data; 20 | let start = null; 21 | let end = null; 22 | data.forEach((v) => { 23 | start = minDate(start, v.start); 24 | end = maxDate(end, v.end); 25 | }); 26 | this.start = start || new Date(); 27 | this.end = end || new Date(); 28 | } 29 | setData(data) { 30 | this.format(data); 31 | this.render(); 32 | } 33 | setOptions(options) { 34 | this.options = { ...this.options, ...options }; 35 | this.render(); 36 | } 37 | render(e) { 38 | const { 39 | data, start, end, options 40 | } = this; 41 | if (options.maxTextWidth === undefined) { 42 | const font = getFont(options.styleOptions || {}); 43 | const w = (v) => textWidth(v.text, font, 20); 44 | options.maxTextWidth = max(data.map(w), 0); 45 | } 46 | const props = { ...options, start, end }; 47 | render(, this.ctx, e); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/SVGGantt.js: -------------------------------------------------------------------------------- 1 | import h from './h'; 2 | import Gantt from './gantt'; 3 | import render from './render/svg'; 4 | import { getFont } from './gantt/styles'; 5 | import { 6 | minDate, maxDate, textWidth, max 7 | } from './utils'; 8 | 9 | export default class SVGGantt { 10 | constructor(element, data, options = {}) { 11 | this.dom = typeof element === 'string' ? document.querySelector(element) : element; 12 | this.format(data); 13 | this.options = options; 14 | this.render(); 15 | } 16 | format(data) { 17 | this.data = data; 18 | let start = null; 19 | let end = null; 20 | data.forEach((v) => { 21 | start = minDate(start, v.start); 22 | end = maxDate(end, v.end); 23 | }); 24 | this.start = start || new Date(); 25 | this.end = end || new Date(); 26 | } 27 | setData(data) { 28 | this.format(data); 29 | this.render(); 30 | } 31 | setOptions(options) { 32 | this.options = { ...this.options, ...options }; 33 | this.render(); 34 | } 35 | render() { 36 | const { 37 | data, start, end, options 38 | } = this; 39 | if (this.tree) { 40 | this.dom.removeChild(this.tree); 41 | } 42 | if (options.maxTextWidth === undefined) { 43 | const font = getFont(options.styleOptions || {}); 44 | const w = (v) => textWidth(v.text, font, 20); 45 | options.maxTextWidth = max(data.map(w), 0); 46 | } 47 | const props = { ...options, start, end }; 48 | this.tree = render(); 49 | this.dom.appendChild(this.tree); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/StrGantt.js: -------------------------------------------------------------------------------- 1 | import h from './h'; 2 | import Gantt from './gantt'; 3 | import render from './render/string'; 4 | import { minDate, maxDate } from './utils'; 5 | 6 | export default class StrGantt { 7 | constructor(data, options = {}) { 8 | this.format(data); 9 | this.options = options; 10 | } 11 | format(data) { 12 | this.data = data; 13 | let start = null; 14 | let end = null; 15 | data.forEach((v) => { 16 | start = minDate(start, v.start); 17 | end = maxDate(end, v.end); 18 | }); 19 | this.start = start || new Date(); 20 | this.end = end || new Date(); 21 | } 22 | setData(data) { 23 | this.format(data); 24 | } 25 | setOptions(options) { 26 | this.options = { ...this.options, ...options }; 27 | } 28 | render() { 29 | const { 30 | data, start, end, options 31 | } = this; 32 | const props = { ...options, start, end }; 33 | return render(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/context.js: -------------------------------------------------------------------------------- 1 | export default function createContext(dom) { 2 | const canvas = typeof dom === 'string' ? document.querySelector(dom) : dom; 3 | const ctx = canvas.getContext('2d'); 4 | const backingStore = ctx.webkitBackingStorePixelRatio || 5 | ctx.mozBackingStorePixelRatio || 6 | ctx.msBackingStorePixelRatio || 7 | ctx.oBackingStorePixelRatio || 8 | ctx.backingStorePixelRatio || 1; 9 | const ratio = (window.devicePixelRatio || 1) / backingStore; 10 | 11 | ['width', 'height'].forEach((key) => { 12 | Object.defineProperty(ctx, key, { 13 | get() { 14 | return canvas[key] / ratio; 15 | }, 16 | set(v) { 17 | canvas[key] = v * ratio; 18 | canvas.style[key] = `${v}px`; 19 | ctx.scale(ratio, ratio); 20 | }, 21 | enumerable: true, 22 | configurable: true 23 | }); 24 | }); 25 | canvas.addEventListener('click', (e) => { 26 | if (!ctx.onClick) return; 27 | const rect = canvas.getBoundingClientRect(); 28 | ctx.onClick({ 29 | x: (e.clientX - rect.left) * ratio, 30 | y: (e.clientY - rect.top) * ratio 31 | }); 32 | }); 33 | return ctx; 34 | } 35 | -------------------------------------------------------------------------------- /src/gantt/Bar.js: -------------------------------------------------------------------------------- 1 | import h from '../h'; 2 | import { formatDay } from '../utils'; 3 | 4 | export default function Bar({ 5 | styles, data, unit, height, offsetY, minTime, showDelay, rowHeight, barHeight, maxTextWidth, current, onClick 6 | }) { 7 | const x0 = maxTextWidth; 8 | const y0 = (rowHeight - barHeight) / 2 + offsetY; 9 | const cur = x0 + (current - minTime) / unit; 10 | return ( 11 | 12 | {current > minTime ? ( 13 | 14 | ) : null} 15 | {data.map((v, i) => { 16 | if (!v.end || !v.start) { 17 | return null; 18 | } 19 | const handler = () => onClick(v); 20 | const x = x0 + (v.start - minTime) / unit; 21 | const y = y0 + i * rowHeight; 22 | const cy = y + barHeight / 2; 23 | if (v.type === 'milestone') { 24 | const size = barHeight / 2; 25 | const points = [ 26 | [x, cy - size], 27 | [x + size, cy], 28 | [x, cy + size], 29 | [x - size, cy] 30 | ].map((p) => `${p[0]},${p[1]}`).join(' '); 31 | return ( 32 | 33 | 34 | 35 | 36 | ); 37 | } 38 | const w1 = (v.end - v.start) / unit; 39 | const w2 = w1 * v.percent; 40 | const bar = v.type === 'group' ? { 41 | back: styles.groupBack, 42 | front: styles.groupFront 43 | } : { 44 | back: styles.taskBack, 45 | front: styles.taskFront 46 | }; 47 | if (showDelay) { 48 | if ((x + w2) < cur && v.percent < 0.999999) { 49 | bar.back = styles.warning; 50 | } 51 | if ((x + w1) < cur && v.percent < 0.999999) { 52 | bar.back = styles.danger; 53 | } 54 | } 55 | return ( 56 | 57 | {formatDay(v.start)} 58 | {formatDay(v.end)} 59 | 60 | {w2 > 0.000001 ? : null} 61 | {v.type === 'group' ? null : ( 62 | 63 | 64 | 65 | 66 | )} 67 | 68 | ); 69 | })} 70 | 71 | ); 72 | } 73 | -------------------------------------------------------------------------------- /src/gantt/DayHeader.js: -------------------------------------------------------------------------------- 1 | import h from '../h'; 2 | import { getDates } from '../utils'; 3 | import YearMonth from './YearMonth'; 4 | 5 | export default function DayHeader({ 6 | styles, unit, minTime, maxTime, height, offsetY, maxTextWidth 7 | }) { 8 | const dates = getDates(minTime, maxTime); 9 | const ticks = []; 10 | const x0 = maxTextWidth; 11 | const y0 = offsetY / 2; 12 | const RH = height - y0; 13 | const len = dates.length - 1; 14 | for (let i = 0; i < len; i++) { 15 | const cur = new Date(dates[i]); 16 | const day = cur.getDay(); 17 | const x = x0 + (dates[i] - minTime) / unit; 18 | const t = (dates[i + 1] - dates[i]) / unit; 19 | ticks.push(( 20 | 21 | {day === 0 || day === 6 ? ( 22 | 23 | ) : null} 24 | 25 | {cur.getDate()} 26 | {i === len - 1 ? ( 27 | 28 | ) : null} 29 | 30 | )); 31 | } 32 | return ( 33 | 34 | 43 | {ticks} 44 | 45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /src/gantt/Grid.js: -------------------------------------------------------------------------------- 1 | import h from '../h'; 2 | 3 | export default function Grid({ 4 | styles, data, width, height, offsetY, rowHeight, maxTextWidth 5 | }) { 6 | return ( 7 | 8 | {data.map((v, i) => { 9 | const y = (i + 1) * rowHeight + offsetY; 10 | return ; 11 | })} 12 | 13 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /src/gantt/Labels.js: -------------------------------------------------------------------------------- 1 | import h from '../h'; 2 | 3 | export default function Labels({ 4 | styles, data, onClick, rowHeight, offsetY 5 | }) { 6 | return ( 7 | 8 | {data.map((v, i) => ( 9 | onClick(v)} 16 | > 17 | {v.text} 18 | 19 | ))} 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /src/gantt/Layout.js: -------------------------------------------------------------------------------- 1 | import h from '../h'; 2 | 3 | export default function Layout({ 4 | styles, width, height, offsetY, thickWidth, maxTextWidth 5 | }) { 6 | const x0 = thickWidth / 2; 7 | const W = width - thickWidth; 8 | const H = height - thickWidth; 9 | return ( 10 | 11 | 12 | 13 | 14 | 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /src/gantt/LinkLine.js: -------------------------------------------------------------------------------- 1 | import h from '../h'; 2 | import { p2s } from '../utils'; 3 | 4 | export default function LinkLine({ 5 | styles, data, unit, offsetY, minTime, rowHeight, barHeight, maxTextWidth 6 | }) { 7 | const x0 = maxTextWidth; 8 | const y0 = (rowHeight / 2) + offsetY; 9 | const map = {}; 10 | data.forEach((v, i) => { 11 | map[v.id] = i; 12 | }); 13 | return ( 14 | 15 | {data.map((s, i) => { 16 | if (!s.end || !s.start || !s.links) { 17 | return null; 18 | } 19 | return s.links.map((l) => { 20 | const j = map[l.target]; 21 | const e = data[j]; 22 | 23 | if (!e || !e.start || !e.end) return null; 24 | 25 | const gap = 12; 26 | const arrow = 6; 27 | const mgap = e.type === 'milestone' ? barHeight / 2 : 0; 28 | const y1 = y0 + i * rowHeight; 29 | const y2 = y0 + j * rowHeight; 30 | 31 | let vgap = barHeight / 2 + 4; 32 | if (y1 > y2) { 33 | vgap = -vgap; 34 | } 35 | 36 | if (l.type === 'FS') { 37 | const x1 = x0 + (s.end - minTime) / unit; 38 | const x2 = x0 + (e.start - minTime) / unit - mgap; 39 | const p1 = [ 40 | [x1, y1], 41 | [x1 + gap, y1] 42 | ]; 43 | if (x2 - x1 >= 2 * gap) { 44 | p1.push([x1 + gap, y2]); 45 | } else { 46 | p1.push([x1 + gap, y2 - vgap]); 47 | p1.push([x2 - gap, y2 - vgap]); 48 | p1.push([x2 - gap, y2]); 49 | } 50 | p1.push([x2 - arrow, y2]); 51 | const p2 = [ 52 | [x2 - arrow, y2 - arrow], 53 | [x2, y2], 54 | [x2 - arrow, y2 + arrow] 55 | ]; 56 | return ( 57 | 58 | 59 | 60 | 61 | ); 62 | } 63 | if (l.type === 'FF') { 64 | const x1 = x0 + (s.end - minTime) / unit; 65 | const x2 = x0 + (e.end - minTime) / unit + mgap; 66 | const p1 = [ 67 | [x1, y1], 68 | [x1 + gap, y1] 69 | ]; 70 | if (x2 <= x1) { 71 | p1.push([x1 + gap, y2]); 72 | } else { 73 | p1.push([x1 + gap, y2 - vgap]); 74 | p1.push([x2 + gap, y2 - vgap]); 75 | p1.push([x2 + gap, y2]); 76 | } 77 | p1.push([x2 + arrow, y2]); 78 | const p2 = [ 79 | [x2 + arrow, y2 - arrow], 80 | [x2, y2], 81 | [x2 + arrow, y2 + arrow] 82 | ]; 83 | return ( 84 | 85 | 86 | 87 | 88 | ); 89 | } 90 | if (l.type === 'SS') { 91 | const x1 = x0 + (s.start - minTime) / unit; 92 | const x2 = x0 + (e.start - minTime) / unit - mgap; 93 | const p1 = [ 94 | [x1, y1], 95 | [x1 - gap, y1] 96 | ]; 97 | if (x1 <= x2) { 98 | p1.push([x1 - gap, y2]); 99 | } else { 100 | p1.push([x1 - gap, y2 - vgap]); 101 | p1.push([x2 - gap, y2 - vgap]); 102 | p1.push([x2 - gap, y2]); 103 | } 104 | p1.push([x2 - arrow, y2]); 105 | const p2 = [ 106 | [x2 - arrow, y2 - arrow], 107 | [x2, y2], 108 | [x2 - arrow, y2 + arrow] 109 | ]; 110 | return ( 111 | 112 | 113 | 114 | 115 | ); 116 | } 117 | if (l.type === 'SF') { 118 | const x1 = x0 + (s.start - minTime) / unit; 119 | const x2 = x0 + (e.end - minTime) / unit + mgap; 120 | const p1 = [ 121 | [x1, y1], 122 | [x1 - gap, y1] 123 | ]; 124 | if (x1 - x2 >= 2 * gap) { 125 | p1.push([x1 - gap, y2]); 126 | } else { 127 | p1.push([x1 - gap, y2 - vgap]); 128 | p1.push([x2 + gap, y2 - vgap]); 129 | p1.push([x2 + gap, y2]); 130 | } 131 | p1.push([x2 + arrow, y2]); 132 | const p2 = [ 133 | [x2 + arrow, y2 - arrow], 134 | [x2, y2], 135 | [x2 + arrow, y2 + arrow] 136 | ]; 137 | return ( 138 | 139 | 140 | 141 | 142 | ); 143 | } 144 | return null; 145 | }); 146 | })} 147 | 148 | ); 149 | } 150 | -------------------------------------------------------------------------------- /src/gantt/MonthHeader.js: -------------------------------------------------------------------------------- 1 | import h from '../h'; 2 | import { getDates } from '../utils'; 3 | import Year from './Year'; 4 | 5 | export default function MonthHeader({ 6 | styles, unit, minTime, maxTime, offsetY, maxTextWidth 7 | }) { 8 | const MONTH = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; 9 | const dates = getDates(minTime, maxTime); 10 | const months = dates.filter((v) => (new Date(v)).getDate() === 1); 11 | 12 | months.unshift(minTime); 13 | months.push(maxTime); 14 | 15 | const ticks = []; 16 | const x0 = maxTextWidth; 17 | const y0 = offsetY / 2; 18 | const len = months.length - 1; 19 | for (let i = 0; i < len; i++) { 20 | const cur = new Date(months[i]); 21 | const month = cur.getMonth(); 22 | const x = x0 + (months[i] - minTime) / unit; 23 | const t = (months[i + 1] - months[i]) / unit; 24 | ticks.push(( 25 | 26 | 27 | {t > 30 ? ( 28 | {MONTH[month]} 29 | ) : null} 30 | 31 | )); 32 | } 33 | return ( 34 | 35 | 44 | {ticks} 45 | 46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /src/gantt/WeekHeader.js: -------------------------------------------------------------------------------- 1 | import h from '../h'; 2 | import { getDates, addDays, DAY } from '../utils'; 3 | import YearMonth from './YearMonth'; 4 | 5 | export default function WeekHeader({ 6 | styles, unit, minTime, maxTime, height, offsetY, maxTextWidth 7 | }) { 8 | const dates = getDates(minTime, maxTime); 9 | const weeks = dates.filter((v) => (new Date(v)).getDay() === 0); 10 | weeks.push(maxTime); 11 | const ticks = []; 12 | const x0 = maxTextWidth; 13 | const y0 = offsetY; 14 | const RH = height - y0; 15 | const d = DAY / unit; 16 | const len = weeks.length - 1; 17 | for (let i = 0; i < len; i++) { 18 | const cur = new Date(weeks[i]); 19 | const x = x0 + (weeks[i] - minTime) / unit; 20 | const curDay = cur.getDate(); 21 | const prevDay = addDays(cur, -1).getDate(); 22 | ticks.push(( 23 | 24 | 25 | 26 | {curDay} 27 | {x - x0 > 28 ? ( 28 | {prevDay} 29 | ) : null} 30 | 31 | )); 32 | } 33 | return ( 34 | 35 | 44 | {ticks} 45 | 46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /src/gantt/Year.js: -------------------------------------------------------------------------------- 1 | import h from '../h'; 2 | 3 | export default function Year({ 4 | styles, months, unit, offsetY, minTime, maxTime, maxTextWidth 5 | }) { 6 | const years = months.filter((v) => (new Date(v)).getMonth() === 0); 7 | 8 | years.unshift(minTime); 9 | years.push(maxTime); 10 | 11 | const ticks = []; 12 | const x0 = maxTextWidth; 13 | const y2 = offsetY / 2; 14 | const len = years.length - 1; 15 | for (let i = 0; i < len; i++) { 16 | const cur = new Date(years[i]); 17 | const x = x0 + (years[i] - minTime) / unit; 18 | const t = (years[i + 1] - years[i]) / unit; 19 | ticks.push(( 20 | 21 | 22 | {t > 35 ? ( 23 | {cur.getFullYear()} 24 | ) : null} 25 | 26 | )); 27 | } 28 | return {ticks}; 29 | } 30 | -------------------------------------------------------------------------------- /src/gantt/YearMonth.js: -------------------------------------------------------------------------------- 1 | import h from '../h'; 2 | import { formatMonth } from '../utils'; 3 | 4 | export default function YearMonth({ 5 | styles, dates, unit, offsetY, minTime, maxTime, maxTextWidth 6 | }) { 7 | const months = dates.filter((v) => (new Date(v)).getDate() === 1); 8 | 9 | months.unshift(minTime); 10 | months.push(maxTime); 11 | 12 | const ticks = []; 13 | const x0 = maxTextWidth; 14 | const y2 = offsetY / 2; 15 | const len = months.length - 1; 16 | for (let i = 0; i < len; i++) { 17 | const cur = new Date(months[i]); 18 | const str = formatMonth(cur); 19 | const x = x0 + (months[i] - minTime) / unit; 20 | const t = (months[i + 1] - months[i]) / unit; 21 | ticks.push(( 22 | 23 | 24 | {t > 50 ? {str} : null} 25 | 26 | )); 27 | } 28 | return {ticks}; 29 | } 30 | -------------------------------------------------------------------------------- /src/gantt/index.js: -------------------------------------------------------------------------------- 1 | import h from '../h'; 2 | import { DAY } from '../utils'; 3 | import Layout from './Layout'; 4 | import DayHeader from './DayHeader'; 5 | import WeekHeader from './WeekHeader'; 6 | import MonthHeader from './MonthHeader'; 7 | import Grid from './Grid'; 8 | import Labels from './Labels'; 9 | import LinkLine from './LinkLine'; 10 | import Bar from './Bar'; 11 | import getStyles from './styles'; 12 | 13 | const UNIT = { 14 | day: DAY / 28, 15 | week: 7 * DAY / 56, 16 | month: 30 * DAY / 56 17 | }; 18 | function NOOP() {} 19 | 20 | export default function Gantt({ 21 | data = [], 22 | onClick = NOOP, 23 | viewMode = 'week', 24 | maxTextWidth = 140, 25 | offsetY = 60, 26 | rowHeight = 40, 27 | barHeight = 16, 28 | thickWidth = 1.4, 29 | styleOptions = {}, 30 | showLinks = true, 31 | showDelay = true, 32 | start, 33 | end 34 | }) { 35 | const unit = UNIT[viewMode]; 36 | const minTime = start.getTime() - unit * 48; 37 | const maxTime = end.getTime() + unit * 48; 38 | 39 | const width = (maxTime - minTime) / unit + maxTextWidth; 40 | const height = data.length * rowHeight + offsetY; 41 | const box = `0 0 ${width} ${height}`; 42 | const current = Date.now(); 43 | const styles = getStyles(styleOptions); 44 | 45 | return ( 46 | 47 | 55 | {viewMode === 'day' ? ( 56 | 65 | ) : null} 66 | {viewMode === 'week' ? ( 67 | 76 | ) : null} 77 | {viewMode === 'month' ? ( 78 | 86 | ) : null} 87 | 96 | {maxTextWidth > 0 ? ( 97 | 104 | ) : null} 105 | {showLinks ? ( 106 | 118 | ) : null} 119 | 133 | 134 | ); 135 | } 136 | -------------------------------------------------------------------------------- /src/gantt/styles.js: -------------------------------------------------------------------------------- 1 | const SIZE = '14px'; 2 | const TYPE = '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif'; 3 | 4 | export function getFont({ 5 | fontSize = SIZE, 6 | fontFamily = TYPE 7 | }) { 8 | return `bold ${fontSize} ${fontFamily}`; 9 | } 10 | 11 | export default function getStyles({ 12 | bgColor = '#fff', 13 | lineColor = '#eee', 14 | redLineColor = '#f04134', 15 | groupBack = '#3db9d3', 16 | groupFront = '#299cb4', 17 | taskBack = '#65c16f', 18 | taskFront = '#46ad51', 19 | milestone = '#d33daf', 20 | warning = '#faad14', 21 | danger = '#f5222d', 22 | link = '#ffa011', 23 | textColor = '#222', 24 | lightTextColor = '#999', 25 | lineWidth = '1px', 26 | thickLineWidth = '1.4px', 27 | fontSize = SIZE, 28 | smallFontSize = '12px', 29 | fontFamily = TYPE, 30 | whiteSpace = 'pre' 31 | }) { 32 | const line = { 33 | stroke: lineColor, 34 | 'stroke-width': lineWidth 35 | }; 36 | const redLine = { 37 | stroke: redLineColor, 38 | 'stroke-width': lineWidth 39 | }; 40 | const thickLine = { 41 | stroke: lineColor, 42 | 'stroke-width': thickLineWidth 43 | }; 44 | const text = { 45 | fill: textColor, 46 | 'dominant-baseline': 'central', 47 | 'font-size': fontSize, 48 | 'font-family': fontFamily, 49 | 'white-space': whiteSpace 50 | }; 51 | const smallText = { 52 | fill: lightTextColor, 53 | 'font-size': smallFontSize 54 | }; 55 | return { 56 | week: { 57 | fill: 'rgba(252, 248, 227, .6)' 58 | }, 59 | box: { 60 | ...thickLine, 61 | fill: bgColor 62 | }, 63 | line, 64 | cline: redLine, 65 | bline: thickLine, 66 | label: text, 67 | groupLabel: { 68 | ...text, 69 | 'font-weight': '600' 70 | }, 71 | text1: { 72 | ...text, 73 | ...smallText, 74 | 'text-anchor': 'end' 75 | }, 76 | text2: { 77 | ...text, 78 | ...smallText 79 | }, 80 | text3: { 81 | ...text, 82 | ...smallText, 83 | 'text-anchor': 'middle' 84 | }, 85 | link: { 86 | stroke: link, 87 | 'stroke-width': '1.5px', 88 | fill: 'none' 89 | }, 90 | linkArrow: { 91 | fill: link 92 | }, 93 | milestone: { 94 | fill: milestone 95 | }, 96 | groupBack: { 97 | fill: groupBack 98 | }, 99 | groupFront: { 100 | fill: groupFront 101 | }, 102 | taskBack: { 103 | fill: taskBack 104 | }, 105 | taskFront: { 106 | fill: taskFront 107 | }, 108 | warning: { 109 | fill: warning 110 | }, 111 | danger: { 112 | fill: danger 113 | }, 114 | ctrl: { 115 | display: 'none', 116 | fill: '#f0f0f0', 117 | stroke: '#929292', 118 | 'stroke-width': '1px' 119 | } 120 | }; 121 | } 122 | -------------------------------------------------------------------------------- /src/h.js: -------------------------------------------------------------------------------- 1 | function addChild(c, childNodes) { 2 | if (c === null || c === undefined) return; 3 | 4 | if (typeof c === 'string' || typeof c === 'number') { 5 | childNodes.push(c.toString()); 6 | } else if (Array.isArray(c)) { 7 | for (let i = 0; i < c.length; i++) { 8 | addChild(c[i], childNodes); 9 | } 10 | } else { 11 | childNodes.push(c); 12 | } 13 | } 14 | 15 | export default function h(tag, props, ...children) { 16 | const childNodes = []; 17 | addChild(children, childNodes); 18 | 19 | if (typeof tag === 'function') { 20 | return tag({ ...props, children: childNodes }); 21 | } 22 | 23 | return { 24 | tag, 25 | props, 26 | children: childNodes 27 | }; 28 | } 29 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import SVGGantt from './SVGGantt'; 2 | import CanvasGantt from './CanvasGantt'; 3 | import StrGantt from './StrGantt'; 4 | import * as utils from './utils'; 5 | 6 | export default CanvasGantt; 7 | export { 8 | SVGGantt, CanvasGantt, StrGantt, utils 9 | }; 10 | -------------------------------------------------------------------------------- /src/render/canvas.js: -------------------------------------------------------------------------------- 1 | import { s2p } from '../utils'; 2 | 3 | export default function render(vnode, ctx, e) { 4 | const { tag, props, children } = vnode; 5 | if (tag === 'svg') { 6 | const { width, height } = props; 7 | ctx.width = width; 8 | ctx.height = height; 9 | } 10 | if (tag === 'line') { 11 | const { 12 | x1, x2, y1, y2, style = {} 13 | } = props; 14 | if (style.stroke) { 15 | ctx.strokeStyle = style.stroke; 16 | ctx.lineWidth = parseFloat(style['stroke-width'] || 1); 17 | } 18 | ctx.beginPath(); 19 | ctx.moveTo(x1, y1); 20 | ctx.lineTo(x2, y2); 21 | ctx.stroke(); 22 | } 23 | if (tag === 'polyline' || tag === 'polygon') { 24 | const { points, style = {} } = props; 25 | const p = s2p(points); 26 | if (style.stroke) { 27 | ctx.strokeStyle = style.stroke; 28 | ctx.lineWidth = parseFloat(style['stroke-width'] || 1); 29 | } 30 | if (style.fill) { 31 | ctx.fillStyle = style.fill; 32 | } 33 | ctx.beginPath(); 34 | ctx.moveTo(p[0][0], p[0][1]); 35 | for (let i = 1; i < p.length; i++) { 36 | ctx.lineTo(p[i][0], p[i][1]); 37 | } 38 | if (tag === 'polyline') { 39 | ctx.stroke(); 40 | } else { 41 | ctx.fill(); 42 | } 43 | } 44 | if (tag === 'rect') { 45 | const { 46 | x, y, width, height, rx = 0, ry = 0, onClick, style = {} 47 | } = props; 48 | 49 | // From https://github.com/canvg/canvg 50 | ctx.beginPath(); 51 | ctx.moveTo(x + rx, y); 52 | ctx.lineTo(x + width - rx, y); 53 | ctx.quadraticCurveTo(x + width, y, x + width, y + ry); 54 | ctx.lineTo(x + width, y + height - ry); 55 | ctx.quadraticCurveTo(x + width, y + height, x + width - rx, y + height); 56 | ctx.lineTo(x + rx, y + height); 57 | ctx.quadraticCurveTo(x, y + height, x, y + height - ry); 58 | ctx.lineTo(x, y + ry); 59 | ctx.quadraticCurveTo(x, y, x + rx, y); 60 | if (e && onClick && ctx.isPointInPath(e.x, e.y)) { 61 | onClick(); 62 | } 63 | ctx.closePath(); 64 | 65 | if (style.fill) { 66 | ctx.fillStyle = style.fill; 67 | } 68 | ctx.fill(); 69 | if (style.stroke) { 70 | ctx.strokeStyle = style.stroke; 71 | ctx.lineWidth = parseFloat(style['stroke-width'] || 1); 72 | ctx.stroke(); 73 | } 74 | } 75 | if (tag === 'text') { 76 | const { x, y, style } = props; 77 | if (style) { 78 | ctx.fillStyle = style.fill; 79 | const BL = { 80 | central: 'middle', 81 | middle: 'middle', 82 | hanging: 'hanging', 83 | alphabetic: 'alphabetic', 84 | ideographic: 'ideographic' 85 | }; 86 | const AL = { 87 | start: 'start', 88 | middle: 'center', 89 | end: 'end' 90 | }; 91 | ctx.textBaseline = BL[style['dominant-baseline']] || 'alphabetic'; 92 | ctx.textAlign = AL[style['text-anchor']] || 'start'; 93 | ctx.font = `${style['font-weight'] || ''} ${style['font-size']} ${style['font-family']}`; 94 | } 95 | ctx.fillText(children.join(''), x, y); 96 | } 97 | 98 | children.forEach((v) => { 99 | if (typeof v !== 'string') { 100 | render(v, ctx, e); 101 | } 102 | }); 103 | } 104 | -------------------------------------------------------------------------------- /src/render/string.js: -------------------------------------------------------------------------------- 1 | function attrEscape(str) { 2 | return String(str).replace(/&/g, '&') 3 | .replace(//g, '>') 13 | .replace(/\r/g, ' '); 14 | } 15 | 16 | export default function render(vnode, ctx) { 17 | const { tag, props, children } = vnode; 18 | const tokens = []; 19 | tokens.push(`<${tag}`); 20 | 21 | Object.keys(props || {}).forEach((k) => { 22 | let v = props[k]; 23 | if (k === 'onClick') return; 24 | if (k === 'style' && typeof v === 'object') { 25 | v = Object.keys(v).map((i) => `${i}:${v[i]};`).join(''); 26 | } 27 | tokens.push(` ${k}="${attrEscape(v)}"`); 28 | }); 29 | 30 | if (!children || !children.length) { 31 | tokens.push(' />'); 32 | return tokens.join(''); 33 | } 34 | 35 | tokens.push('>'); 36 | 37 | children.forEach((v) => { 38 | if (typeof v === 'string') { 39 | tokens.push(escape(v)); 40 | } else { 41 | tokens.push(render(v, ctx)); 42 | } 43 | }); 44 | 45 | tokens.push(``); 46 | return tokens.join(''); 47 | } 48 | -------------------------------------------------------------------------------- /src/render/svg.js: -------------------------------------------------------------------------------- 1 | const NS = 'http://www.w3.org/2000/svg'; 2 | const doc = document; 3 | 4 | function applyProperties(node, props) { 5 | Object.keys(props).forEach((k) => { 6 | const v = props[k]; 7 | if (k === 'style' && typeof v === 'object') { 8 | Object.keys(v).forEach((sk) => { 9 | // eslint-disable-next-line 10 | node.style[sk] = v[sk]; 11 | }); 12 | } else if (k === 'onClick') { 13 | if (typeof v === 'function') { 14 | node.addEventListener('click', v); 15 | } 16 | } else { 17 | node.setAttribute(k, v); 18 | } 19 | }); 20 | } 21 | 22 | export default function render(vnode, ctx) { 23 | const { tag, props, children } = vnode; 24 | const node = doc.createElementNS(NS, tag); 25 | 26 | if (props) { 27 | applyProperties(node, props); 28 | } 29 | 30 | children.forEach((v) => { 31 | node.appendChild(typeof v === 'string' ? doc.createTextNode(v) : render(v, ctx)); 32 | }); 33 | return node; 34 | } 35 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | export const DAY = 24 * 3600 * 1000; 2 | 3 | export function addDays(date, days) { 4 | const d = new Date(date.valueOf()); 5 | d.setDate(d.getDate() + days); 6 | return d; 7 | } 8 | 9 | export function getDates(begin, end) { 10 | const dates = []; 11 | let s = new Date(begin); 12 | s.setHours(24, 0, 0, 0); 13 | while (s.getTime() <= end) { 14 | dates.push(s.getTime()); 15 | s = addDays(s, 1); 16 | } 17 | return dates; 18 | } 19 | 20 | let ctx = null; 21 | export function textWidth(text, font, pad) { 22 | ctx = ctx || document.createElement('canvas').getContext('2d'); 23 | ctx.font = font; 24 | return ctx.measureText(text).width + pad; 25 | } 26 | 27 | export function formatMonth(date) { 28 | const y = date.getFullYear(); 29 | const m = date.getMonth() + 1; 30 | return `${y}/${m > 9 ? m : `0${m}`}`; 31 | } 32 | 33 | export function formatDay(date) { 34 | const m = date.getMonth() + 1; 35 | const d = date.getDate(); 36 | return `${m}/${d}`; 37 | } 38 | 39 | export function minDate(a, b) { 40 | if (a && b) { 41 | return a > b ? b : a; 42 | } 43 | return a || b; 44 | } 45 | 46 | export function maxDate(a, b) { 47 | if (a && b) { 48 | return a < b ? b : a; 49 | } 50 | return a || b; 51 | } 52 | 53 | export function max(list, defaultValue) { 54 | if (list.length) { 55 | return Math.max.apply(null, list); 56 | } 57 | return defaultValue; 58 | } 59 | 60 | export function p2s(arr) { 61 | return arr.map((p) => `${p[0]},${p[1]}`).join(' '); 62 | } 63 | 64 | export function s2p(str) { 65 | return str.split(' ').map((s) => { 66 | const p = s.split(','); 67 | return [parseFloat(p[0]), parseFloat(p[1])]; 68 | }); 69 | } 70 | 71 | function walkLevel(nodes, level) { 72 | for (let i = 0; i < nodes.length; i++) { 73 | const node = nodes[i]; 74 | node.level = `${level}${i + 1}`; 75 | node.text = `${node.level} ${node.name}`; 76 | walkLevel(node.children, `${node.level}.`); 77 | } 78 | } 79 | 80 | function walkDates(nodes) { 81 | let start = null; 82 | let end = null; 83 | let percent = 0; 84 | for (let i = 0; i < nodes.length; i++) { 85 | const node = nodes[i]; 86 | if (node.children.length) { 87 | const tmp = walkDates(node.children); 88 | node.start = tmp.start; 89 | node.end = tmp.end; 90 | node.percent = tmp.percent; 91 | if (tmp.start && tmp.end) { 92 | node.duration = (tmp.end - tmp.start) / DAY; 93 | } else { 94 | node.duration = 0; 95 | } 96 | } else { 97 | node.percent = node.percent || 0; 98 | if (node.start) { 99 | node.end = addDays(node.start, node.duration || 0); 100 | } 101 | if (node.type === 'milestone') { 102 | node.end = node.start; 103 | } 104 | } 105 | start = minDate(start, node.start); 106 | end = maxDate(end, node.end); 107 | percent += node.percent; 108 | } 109 | if (nodes.length) { 110 | percent /= nodes.length; 111 | } 112 | return { start, end, percent }; 113 | } 114 | 115 | export function formatData(tasks, links, walk) { 116 | const map = {}; 117 | const tmp = tasks.map((t, i) => { 118 | map[t.id] = i; 119 | return { ...t, children: [], links: [] }; 120 | }); 121 | const roots = []; 122 | tmp.forEach((t) => { 123 | const parent = tmp[map[t.parent]]; 124 | if (parent) { 125 | parent.children.push(t); 126 | } else { 127 | roots.push(t); 128 | } 129 | }); 130 | links.forEach((l) => { 131 | const s = tmp[map[l.source]]; 132 | const t = tmp[map[l.target]]; 133 | if (s && t) { 134 | s.links.push(l); 135 | } 136 | }); 137 | 138 | walkLevel(roots, ''); 139 | walkDates(roots); 140 | 141 | if (walk) { 142 | walk(roots); 143 | } 144 | 145 | const list = []; 146 | roots.forEach((r) => { 147 | const stack = []; 148 | stack.push(r); 149 | while (stack.length) { 150 | const node = stack.pop(); 151 | const len = node.children.length; 152 | if (len) { 153 | node.type = 'group'; 154 | } 155 | list.push(node); 156 | for (let i = len - 1; i >= 0; i--) { 157 | stack.push(node.children[i]); 158 | } 159 | } 160 | }); 161 | return list; 162 | } 163 | 164 | export function hasPath(vmap, a, b) { 165 | const stack = []; 166 | stack.push(vmap[a]); 167 | while (stack.length) { 168 | const v = stack.pop(); 169 | if (v.id === b) { 170 | return true; 171 | } 172 | for (let i = 0; i < v.links.length; i++) { 173 | stack.push(v.links[i]); 174 | } 175 | } 176 | return false; 177 | } 178 | 179 | export function toposort(links) { 180 | const vmap = {}; 181 | links.forEach((l) => { 182 | const init = (id) => ({ id, out: [], in: 0 }); 183 | vmap[l.source] = init(l.source); 184 | vmap[l.target] = init(l.target); 185 | }); 186 | for (let i = 0; i < links.length; i++) { 187 | const l = links[i]; 188 | vmap[l.target].in++; 189 | vmap[l.source].out.push(i); 190 | } 191 | const s = Object.keys(vmap) 192 | .map((k) => vmap[k].id) 193 | .filter((id) => !vmap[id].in); 194 | const sorted = []; 195 | while (s.length) { 196 | const id = s.pop(); 197 | sorted.push(id); 198 | for (let i = 0; i < vmap[id].out.length; i++) { 199 | const index = vmap[id].out[i]; 200 | const v = vmap[links[index].target]; 201 | v.in--; 202 | if (!v.in) { 203 | s.push(v.id); 204 | } 205 | } 206 | } 207 | return sorted; 208 | } 209 | 210 | export function autoSchedule(tasks, links, lockMilestone = false) { 211 | const vmap = {}; 212 | links.forEach((l) => { 213 | vmap[l.source] = { id: l.source, links: [] }; 214 | vmap[l.target] = { id: l.target, links: [] }; 215 | }); 216 | const dag = []; 217 | links.forEach((l) => { 218 | const { source, target } = l; 219 | if (!hasPath(vmap, target, source)) { 220 | dag.push(l); 221 | vmap[source].links.push(vmap[target]); 222 | } 223 | }); 224 | const sorted = toposort(dag); 225 | const tmap = {}; 226 | for (let i = 0; i < tasks.length; i++) { 227 | const task = tasks[i]; 228 | if (task.type === 'milestone') { 229 | task.duration = 0; 230 | } 231 | tmap[task.id] = i; 232 | } 233 | const ins = {}; 234 | sorted.forEach((id) => { 235 | ins[id] = []; 236 | }); 237 | dag.forEach((l) => { 238 | ins[l.target].push(l); 239 | }); 240 | sorted.forEach((id) => { 241 | const task = tasks[tmap[id]]; 242 | if (!task) return; 243 | const days = task.duration || 0; 244 | if (lockMilestone && task.type === 'milestone') { 245 | return; 246 | } 247 | let start = null; 248 | let end = null; 249 | for (let i = 0; i < ins[id].length; i++) { 250 | const l = ins[id][i]; 251 | const v = tasks[tmap[l.source]]; 252 | if (v && v.start) { 253 | const s = addDays(v.start, l.lag || 0); 254 | const e = addDays(s, v.duration || 0); 255 | if (l.type === 'SS') { 256 | start = maxDate(start, s); 257 | } 258 | if (l.type === 'FS') { 259 | start = maxDate(start, e); 260 | } 261 | if (l.type === 'SF') { 262 | end = maxDate(end, s); 263 | } 264 | if (l.type === 'FF') { 265 | end = maxDate(end, e); 266 | } 267 | } 268 | } 269 | if (end) { 270 | task.start = addDays(end, -days); 271 | } 272 | if (start) { 273 | task.start = start; 274 | } 275 | }); 276 | } 277 | --------------------------------------------------------------------------------