First commit
This commit is contained in:
commit
77af1bb8fb
4
.babelrc
Executable file
4
.babelrc
Executable file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"presets": ["es2015"],
|
||||
"plugins": ["babel-plugin-add-module-exports"]
|
||||
}
|
||||
182
.eslintrc
Executable file
182
.eslintrc
Executable file
@ -0,0 +1,182 @@
|
||||
{
|
||||
"ecmaFeatures": {
|
||||
"globalReturn": true,
|
||||
"jsx": true,
|
||||
"modules": true
|
||||
},
|
||||
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es6": true,
|
||||
"node": true
|
||||
},
|
||||
|
||||
"globals": {
|
||||
"document": false,
|
||||
"escape": false,
|
||||
"navigator": false,
|
||||
"unescape": false,
|
||||
"window": false,
|
||||
"describe": true,
|
||||
"before": true,
|
||||
"it": true,
|
||||
"expect": true,
|
||||
"sinon": true
|
||||
},
|
||||
|
||||
"parser": "babel-eslint",
|
||||
|
||||
"plugins": [
|
||||
|
||||
],
|
||||
|
||||
"rules": {
|
||||
"block-scoped-var": 2,
|
||||
"brace-style": [2, "1tbs", { "allowSingleLine": true }],
|
||||
"comma-dangle": [2, "never"],
|
||||
"comma-spacing": [2, { "before": false, "after": true }],
|
||||
"comma-style": [2, "last"],
|
||||
"complexity": 0,
|
||||
"consistent-return": 2,
|
||||
"consistent-this": 0,
|
||||
"curly": [2, "multi-line"],
|
||||
"default-case": 0,
|
||||
"dot-location": [2, "property"],
|
||||
"dot-notation": 0,
|
||||
"eol-last": 2,
|
||||
"eqeqeq": [2, "allow-null"],
|
||||
"func-names": 0,
|
||||
"func-style": 0,
|
||||
"generator-star-spacing": [2, "both"],
|
||||
"guard-for-in": 0,
|
||||
"handle-callback-err": [2, "^(err|error|anySpecificError)$" ],
|
||||
"indent": [2, "tab", { "SwitchCase": 1 }],
|
||||
"key-spacing": [2, { "beforeColon": false, "afterColon": true }],
|
||||
"linebreak-style": 0,
|
||||
"max-depth": 0,
|
||||
"max-len": [2, 120, 4],
|
||||
"max-nested-callbacks": 0,
|
||||
"max-params": 0,
|
||||
"max-statements": 0,
|
||||
"new-cap": [2, { "newIsCap": true, "capIsNew": false }],
|
||||
"new-parens": 2,
|
||||
"no-alert": 0,
|
||||
"no-array-constructor": 2,
|
||||
"no-bitwise": 0,
|
||||
"no-caller": 2,
|
||||
"no-catch-shadow": 0,
|
||||
"no-cond-assign": 2,
|
||||
"no-console": 0,
|
||||
"no-constant-condition": 0,
|
||||
"no-continue": 0,
|
||||
"no-control-regex": 2,
|
||||
"no-debugger": 2,
|
||||
"no-delete-var": 2,
|
||||
"no-div-regex": 0,
|
||||
"no-dupe-args": 2,
|
||||
"no-dupe-keys": 2,
|
||||
"no-duplicate-case": 2,
|
||||
"no-else-return": 2,
|
||||
"no-empty": 0,
|
||||
"no-empty-character-class": 2,
|
||||
"no-empty-label": 2,
|
||||
"no-eq-null": 0,
|
||||
"no-eval": 2,
|
||||
"no-ex-assign": 2,
|
||||
"no-extend-native": 2,
|
||||
"no-extra-bind": 2,
|
||||
"no-extra-boolean-cast": 2,
|
||||
"no-extra-parens": 0,
|
||||
"no-extra-semi": 0,
|
||||
"no-extra-strict": 0,
|
||||
"no-fallthrough": 2,
|
||||
"no-floating-decimal": 2,
|
||||
"no-func-assign": 2,
|
||||
"no-implied-eval": 2,
|
||||
"no-inline-comments": 0,
|
||||
"no-inner-declarations": [2, "functions"],
|
||||
"no-invalid-regexp": 2,
|
||||
"no-irregular-whitespace": 2,
|
||||
"no-iterator": 2,
|
||||
"no-label-var": 2,
|
||||
"no-labels": 2,
|
||||
"no-lone-blocks": 0,
|
||||
"no-lonely-if": 0,
|
||||
"no-loop-func": 0,
|
||||
"no-mixed-requires": 0,
|
||||
"no-mixed-spaces-and-tabs": [2, false],
|
||||
"no-multi-spaces": 2,
|
||||
"no-multi-str": 2,
|
||||
"no-multiple-empty-lines": [2, { "max": 1 }],
|
||||
"no-native-reassign": 2,
|
||||
"no-negated-in-lhs": 2,
|
||||
"no-nested-ternary": 0,
|
||||
"no-new": 2,
|
||||
"no-new-func": 2,
|
||||
"no-new-object": 2,
|
||||
"no-new-require": 2,
|
||||
"no-new-wrappers": 2,
|
||||
"no-obj-calls": 2,
|
||||
"no-octal": 2,
|
||||
"no-octal-escape": 2,
|
||||
"no-path-concat": 0,
|
||||
"no-plusplus": 0,
|
||||
"no-process-env": 0,
|
||||
"no-process-exit": 0,
|
||||
"no-proto": 2,
|
||||
"no-redeclare": 2,
|
||||
"no-regex-spaces": 2,
|
||||
"no-reserved-keys": 0,
|
||||
"no-restricted-modules": 0,
|
||||
"no-return-assign": 2,
|
||||
"no-script-url": 0,
|
||||
"no-self-compare": 2,
|
||||
"no-sequences": 2,
|
||||
"no-shadow": 0,
|
||||
"no-shadow-restricted-names": 2,
|
||||
"no-spaced-func": 2,
|
||||
"no-sparse-arrays": 2,
|
||||
"no-sync": 0,
|
||||
"no-ternary": 0,
|
||||
"no-throw-literal": 2,
|
||||
"no-trailing-spaces": 2,
|
||||
"no-undef": 2,
|
||||
"no-undef-init": 2,
|
||||
"no-undefined": 0,
|
||||
"no-underscore-dangle": 0,
|
||||
"no-unneeded-ternary": 2,
|
||||
"no-unreachable": 2,
|
||||
"no-unused-expressions": 0,
|
||||
"no-unused-vars": [2, { "vars": "all", "args": "none" }],
|
||||
"no-var": 0,
|
||||
"no-void": 0,
|
||||
"no-warning-comments": 0,
|
||||
"no-with": 2,
|
||||
"one-var": 0,
|
||||
"operator-assignment": 0,
|
||||
"operator-linebreak": [2, "after"],
|
||||
"padded-blocks": 0,
|
||||
"quote-props": 0,
|
||||
"quotes": [2, "single", "avoid-escape"],
|
||||
"radix": 2,
|
||||
"semi": [2, "always"],
|
||||
"semi-spacing": 0,
|
||||
"sort-vars": 0,
|
||||
"space-before-blocks": [2, "always"],
|
||||
"space-before-function-paren": [2, {"anonymous": "always", "named": "never"}],
|
||||
"space-in-brackets": 0,
|
||||
"space-in-parens": [2, "never"],
|
||||
"space-infix-ops": 2,
|
||||
"space-return-throw-case": 2,
|
||||
"space-unary-ops": [2, { "words": true, "nonwords": false }],
|
||||
"spaced-comment": [2, "always"],
|
||||
"strict": 0,
|
||||
"use-isnan": 2,
|
||||
"valid-jsdoc": 0,
|
||||
"valid-typeof": 2,
|
||||
"vars-on-top": 2,
|
||||
"wrap-iife": [2, "any"],
|
||||
"wrap-regex": 0,
|
||||
"yoda": [2, "never"]
|
||||
}
|
||||
}
|
||||
29
.gitignore
vendored
Executable file
29
.gitignore
vendored
Executable file
@ -0,0 +1,29 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (http://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directory
|
||||
# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
|
||||
node_modules
|
||||
|
||||
.DS_Store
|
||||
67
index.html
Normal file
67
index.html
Normal file
@ -0,0 +1,67 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Simple Gantt</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: sans-serif;
|
||||
background: #ccc;
|
||||
}
|
||||
.container {
|
||||
width: 80%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.gantt-container {
|
||||
overflow: scroll;
|
||||
}
|
||||
</style>
|
||||
<script src="node_modules/moment/min/moment.min.js"></script>
|
||||
<script src="node_modules/snapsvg/dist/snap.svg-min.js"></script>
|
||||
<script src="lib/gantt.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h2>Interactive Gantt Chart entirely made in SVG!</h2>
|
||||
<div class="gantt-container">
|
||||
<svg id="gantt" width="400" height="600"></svg>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
var arr = [1, 2, 3, 4, 5, 6];
|
||||
var tasks = arr.map(function(i){
|
||||
return {
|
||||
start: "2016-10-0"+i,
|
||||
end: "2016-10-2"+i,
|
||||
name: "Task "+i,
|
||||
id: i,
|
||||
progress: i*10
|
||||
}
|
||||
});
|
||||
tasks[0].dependencies = '2, 3';
|
||||
|
||||
var gantt_chart = gantt("#gantt", tasks, {
|
||||
date_format: "YYYY-MM-DD",
|
||||
bar: {
|
||||
height: 20
|
||||
}
|
||||
// events: {
|
||||
// bar_on_click: function (task) {
|
||||
// console.log(task);
|
||||
// },
|
||||
// bar_on_date_change: function(task, start, end) {
|
||||
// console.log(task, start, end);
|
||||
// },
|
||||
// bar_on_progress_change: function(task, progress) {
|
||||
// console.log(task, progress);
|
||||
// },
|
||||
// on_viewmode_change: function(mode) {
|
||||
// console.log(mode);
|
||||
// }
|
||||
// }
|
||||
});
|
||||
console.log(gantt_chart);
|
||||
// gantt.render();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
1762
lib/gantt.js
Normal file
1762
lib/gantt.js
Normal file
File diff suppressed because one or more lines are too long
1
lib/gantt.js.map
Normal file
1
lib/gantt.js.map
Normal file
File diff suppressed because one or more lines are too long
2
lib/gantt.min.js
vendored
Normal file
2
lib/gantt.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
lib/gantt.min.js.map
Normal file
1
lib/gantt.min.js.map
Normal file
File diff suppressed because one or more lines are too long
52
package.json
Executable file
52
package.json
Executable file
@ -0,0 +1,52 @@
|
||||
{
|
||||
"name": "gantt",
|
||||
"version": "0.1.3",
|
||||
"description": "Visualize tasks on a timeline",
|
||||
"main": "lib/gantt.js",
|
||||
"scripts": {
|
||||
"build": "webpack --mode=build",
|
||||
"dev": "webpack --progress --colors --watch --mode=dev",
|
||||
"test": "mocha --compilers js:babel-core/register --colors -w ./test/*.spec.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel": "6.3.13",
|
||||
"babel-core": "6.1.18",
|
||||
"babel-eslint": "5.0.0",
|
||||
"babel-loader": "6.1.0",
|
||||
"babel-plugin-add-module-exports": "0.1.2",
|
||||
"babel-preset-es2015": "6.3.13",
|
||||
"chai": "3.4.1",
|
||||
"css-loader": "^0.26.1",
|
||||
"eslint": "1.7.2",
|
||||
"eslint-loader": "1.1.0",
|
||||
"mocha": "2.3.4",
|
||||
"node-sass": "^4.0.0",
|
||||
"sass-loader": "^4.1.0",
|
||||
"style-loader": "^0.13.1",
|
||||
"webpack": "1.12.9",
|
||||
"yargs": "3.32.0"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/krasimir/webpack-library-starter.git"
|
||||
},
|
||||
"keywords": [
|
||||
"webpack",
|
||||
"es6",
|
||||
"starter",
|
||||
"library",
|
||||
"universal",
|
||||
"umd",
|
||||
"commonjs"
|
||||
],
|
||||
"author": "Krasimir Tsonev",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/krasimir/webpack-library-starter/issues"
|
||||
},
|
||||
"homepage": "https://github.com/krasimir/webpack-library-starter",
|
||||
"dependencies": {
|
||||
"moment": "^2.17.1",
|
||||
"snapsvg": "^0.4.0"
|
||||
}
|
||||
}
|
||||
105
src/Arrow.js
Normal file
105
src/Arrow.js
Normal file
@ -0,0 +1,105 @@
|
||||
/* global Snap */
|
||||
/*
|
||||
Class: Arrow
|
||||
from_task ---> to_task
|
||||
|
||||
Opts:
|
||||
gantt (Gantt object)
|
||||
from_task (Bar object)
|
||||
to_task (Bar object)
|
||||
*/
|
||||
|
||||
export default function Arrow(gt, from_task, to_task) {
|
||||
|
||||
const self = {};
|
||||
|
||||
function init() {
|
||||
self.from_task = from_task;
|
||||
self.to_task = to_task;
|
||||
prepare();
|
||||
draw();
|
||||
}
|
||||
|
||||
function prepare() {
|
||||
|
||||
self.start_x = from_task.$bar.getX() + from_task.$bar.getWidth() / 2;
|
||||
|
||||
const condition = () =>
|
||||
to_task.$bar.getX() < self.start_x + gt.config.padding &&
|
||||
self.start_x > from_task.$bar.getX() + gt.config.padding;
|
||||
|
||||
while(condition()) {
|
||||
self.start_x -= 10;
|
||||
}
|
||||
|
||||
self.start_y = gt.config.header_height + gt.config.bar.height +
|
||||
(gt.config.padding + gt.config.bar.height) * from_task.task._index +
|
||||
gt.config.padding;
|
||||
|
||||
self.end_x = to_task.$bar.getX() - gt.config.padding / 2;
|
||||
self.end_y = gt.config.header_height + gt.config.bar.height / 2 +
|
||||
(gt.config.padding + gt.config.bar.height) * to_task.task._index +
|
||||
gt.config.padding;
|
||||
|
||||
const from_is_below_to = (from_task.task._index > to_task.task._index);
|
||||
self.curve = gt.config.arrow.curve;
|
||||
self.clockwise = from_is_below_to ? 1 : 0;
|
||||
self.curve_y = from_is_below_to ? -self.curve : self.curve;
|
||||
self.offset = from_is_below_to ?
|
||||
self.end_y + gt.config.arrow.curve :
|
||||
self.end_y - gt.config.arrow.curve;
|
||||
|
||||
self.path =
|
||||
Snap.format('M {start_x} {start_y} V {offset} ' +
|
||||
'a {curve} {curve} 0 0 {clockwise} {curve} {curve_y} ' +
|
||||
'L {end_x} {end_y} m -5 -5 l 5 5 l -5 5',
|
||||
{
|
||||
start_x: self.start_x,
|
||||
start_y: self.start_y,
|
||||
end_x: self.end_x,
|
||||
end_y: self.end_y,
|
||||
offset: self.offset,
|
||||
curve: self.curve,
|
||||
clockwise: self.clockwise,
|
||||
curve_y: self.curve_y
|
||||
});
|
||||
|
||||
if(to_task.$bar.getX() < from_task.$bar.getX() + gt.config.padding) {
|
||||
self.path =
|
||||
Snap.format('M {start_x} {start_y} v {down_1} ' +
|
||||
'a {curve} {curve} 0 0 1 -{curve} {curve} H {left} ' +
|
||||
'a {curve} {curve} 0 0 {clockwise} -{curve} {curve_y} V {down_2} ' +
|
||||
'a {curve} {curve} 0 0 {clockwise} {curve} {curve_y} ' +
|
||||
'L {end_x} {end_y} m -5 -5 l 5 5 l -5 5',
|
||||
{
|
||||
start_x: self.start_x,
|
||||
start_y: self.start_y,
|
||||
end_x: self.end_x,
|
||||
end_y: self.end_y,
|
||||
down_1: gt.config.padding / 2 - self.curve,
|
||||
down_2: to_task.$bar.getY() + to_task.$bar.get('height') / 2 - self.curve_y,
|
||||
left: to_task.$bar.getX() - gt.config.padding,
|
||||
offset: self.offset,
|
||||
curve: self.curve,
|
||||
clockwise: self.clockwise,
|
||||
curve_y: self.curve_y
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function draw() {
|
||||
self.element = self.gantt.canvas.path(self.path)
|
||||
.attr('data-from', self.from_task.task.id)
|
||||
.attr('data-to', self.to_task.task.id);
|
||||
}
|
||||
|
||||
function update() { // eslint-disable-line
|
||||
self.prepare();
|
||||
self.element.attr('d', self.path);
|
||||
}
|
||||
self.update = update;
|
||||
|
||||
init();
|
||||
|
||||
return self;
|
||||
}
|
||||
528
src/Bar.js
Normal file
528
src/Bar.js
Normal file
@ -0,0 +1,528 @@
|
||||
/* global Snap */
|
||||
/*
|
||||
Class: Bar
|
||||
|
||||
Opts:
|
||||
canvas [reqd]
|
||||
task [reqd]
|
||||
column_width [reqd]
|
||||
x
|
||||
y
|
||||
*/
|
||||
|
||||
export default function Bar(gt, task) {
|
||||
|
||||
const self = {};
|
||||
|
||||
function init() {
|
||||
set_defaults();
|
||||
prepare();
|
||||
draw();
|
||||
bind();
|
||||
}
|
||||
|
||||
function set_defaults() {
|
||||
self.action_completed = false;
|
||||
self.task = task;
|
||||
}
|
||||
|
||||
function prepare() {
|
||||
prepare_values();
|
||||
prepare_plugins();
|
||||
}
|
||||
|
||||
function prepare_values() {
|
||||
self.invalid = self.task.invalid;
|
||||
self.height = gt.config.bar.height;
|
||||
self.x = compute_x();
|
||||
self.y = compute_y();
|
||||
self.corner_radius = 3;
|
||||
self.duration = (self.task._end.diff(self.task._start, 'hours') + 24) / gt.config.step;
|
||||
self.width = gt.config.column_width * self.duration;
|
||||
self.progress_width = gt.config.column_width * self.duration * (self.task.progress / 100) || 0;
|
||||
self.group = gt.canvas.group().addClass('bar-wrapper');
|
||||
self.bar_group = gt.canvas.group().addClass('bar-group').appendTo(self.group);
|
||||
self.handle_group = gt.canvas.group().addClass('handle-group').appendTo(self.group);
|
||||
}
|
||||
|
||||
function prepare_plugins() {
|
||||
Snap.plugin(function (Snap, Element, Paper, global, Fragment) {
|
||||
Element.prototype.getX = function () {
|
||||
return +this.attr('x');
|
||||
};
|
||||
Element.prototype.getY = function () {
|
||||
return +this.attr('y');
|
||||
};
|
||||
Element.prototype.getWidth = function () {
|
||||
return +this.attr('width');
|
||||
};
|
||||
Element.prototype.getHeight = function () {
|
||||
return +this.attr('height');
|
||||
};
|
||||
Element.prototype.getEndX = function () {
|
||||
return this.getX() + this.getWidth();
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function draw() {
|
||||
draw_bar();
|
||||
draw_progress_bar();
|
||||
draw_label();
|
||||
draw_resize_handles();
|
||||
}
|
||||
|
||||
function draw_bar() {
|
||||
self.$bar = gt.canvas.rect(self.x, self.y,
|
||||
self.width, self.height,
|
||||
self.corner_radius, self.corner_radius)
|
||||
.addClass('bar')
|
||||
.appendTo(self.bar_group);
|
||||
if (self.invalid) {
|
||||
self.$bar.addClass('bar-invalid');
|
||||
}
|
||||
}
|
||||
|
||||
function draw_progress_bar() {
|
||||
if (self.invalid) return;
|
||||
self.$bar_progress = gt.canvas.rect(self.x, self.y,
|
||||
self.progress_width, self.height,
|
||||
self.corner_radius, self.corner_radius)
|
||||
.addClass('bar-progress')
|
||||
.appendTo(self.bar_group);
|
||||
}
|
||||
|
||||
function draw_label() {
|
||||
gt.canvas.text(self.x + self.width / 2,
|
||||
self.y + self.height / 2,
|
||||
self.task.name)
|
||||
.addClass('bar-label')
|
||||
.appendTo(self.bar_group);
|
||||
update_label_position();
|
||||
}
|
||||
|
||||
function draw_resize_handles() {
|
||||
if (self.invalid) return;
|
||||
|
||||
const bar = self.$bar,
|
||||
bar_progress = self.$bar_progress,
|
||||
handle_width = 8;
|
||||
|
||||
gt.canvas.rect(bar.getX() + bar.getWidth() - 9, bar.getY() + 1,
|
||||
handle_width, self.height - 2, self.corner_radius, self.corner_radius)
|
||||
.addClass('handle right')
|
||||
.appendTo(self.handle_group);
|
||||
gt.canvas.rect(bar.getX() + 1, bar.getY() + 1,
|
||||
handle_width, self.height - 2, self.corner_radius, self.corner_radius)
|
||||
.addClass('handle left')
|
||||
.appendTo(self.handle_group);
|
||||
|
||||
if (self.task.progress && self.task.progress < 100) {
|
||||
gt.canvas.polygon(
|
||||
bar_progress.getEndX() - 5, bar_progress.getY() + bar_progress.getHeight(),
|
||||
bar_progress.getEndX() + 5, bar_progress.getY() + bar_progress.getHeight(),
|
||||
bar_progress.getEndX(), bar_progress.getY() + bar_progress.getHeight() - 8.66
|
||||
)
|
||||
.addClass('handle progress')
|
||||
.appendTo(self.handle_group);
|
||||
}
|
||||
}
|
||||
|
||||
// function draw_invalid_bar() {
|
||||
// const x = moment().startOf('day').diff(gt.gantt_start, 'hours') /
|
||||
// gt.config.step * gt.config.column_width;
|
||||
|
||||
// gt.canvas.rect(x, self.y,
|
||||
// gt.config.column_width * 2, self.height,
|
||||
// self.corner_radius, self.corner_radius)
|
||||
// .addClass('bar-invalid')
|
||||
// .appendTo(self.bar_group);
|
||||
|
||||
// gt.canvas.text(
|
||||
// x + gt.config.column_width,
|
||||
// self.y + self.height / 2,
|
||||
// 'Dates not set')
|
||||
// .addClass('bar-label big')
|
||||
// .appendTo(self.bar_group);
|
||||
// }
|
||||
|
||||
function bind() {
|
||||
if (self.invalid) return;
|
||||
show_details();
|
||||
bind_resize();
|
||||
bind_drag();
|
||||
bind_resize_progress();
|
||||
}
|
||||
|
||||
function show_details() {
|
||||
const popover_group = gt.element_groups.details;
|
||||
let details_box = popover_group.select('.details-wrapper');
|
||||
|
||||
if (!details_box) {
|
||||
details_box = gt.canvas.group()
|
||||
.addClass('details-wrapper')
|
||||
.appendTo(popover_group);
|
||||
gt.canvas.rect(0, 0, 0, 110, 2, 2)
|
||||
.addClass('details-container')
|
||||
.appendTo(details_box);
|
||||
gt.canvas.text(0, 0, '')
|
||||
.attr({ dx: 10, dy: 30 })
|
||||
.addClass('details-heading')
|
||||
.appendTo(details_box);
|
||||
gt.canvas.text(0, 0, '')
|
||||
.attr({ dx: 10, dy: 65 })
|
||||
.addClass('details-body')
|
||||
.appendTo(details_box);
|
||||
gt.canvas.text(0, 0, '')
|
||||
.attr({ dx: 10, dy: 90 })
|
||||
.addClass('details-body')
|
||||
.appendTo(details_box);
|
||||
}
|
||||
|
||||
self.group.mouseover((e, x, y) => {
|
||||
popover_group.removeClass('hide');
|
||||
|
||||
const pos = get_details_position();
|
||||
details_box.transform(`t${pos.x},${pos.y}`);
|
||||
|
||||
const start_date = self.task._start.format('MMM D'),
|
||||
end_date = self.task._end.format('MMM D'),
|
||||
heading = `${self.task.name}: ${start_date} - ${end_date}`;
|
||||
|
||||
const $heading = popover_group
|
||||
.select('.details-heading')
|
||||
.attr('text', heading);
|
||||
|
||||
const bbox = $heading.getBBox();
|
||||
details_box.select('.details-container')
|
||||
.attr({ width: bbox.width + 20 });
|
||||
|
||||
const duration = self.task._end.diff(self.task._start, 'days'),
|
||||
body1 = `Duration: ${duration} days`,
|
||||
body2 = self.task.progress ?
|
||||
`Progress: ${self.task.progress}` : '';
|
||||
|
||||
const $body = popover_group.selectAll('.details-body');
|
||||
$body[0].attr('text', body1);
|
||||
$body[1].attr('text', body2);
|
||||
});
|
||||
|
||||
self.group.mouseout(() => {
|
||||
setTimeout(() => popover_group.addClass('hide'), 500);
|
||||
});
|
||||
}
|
||||
|
||||
function get_details_position() {
|
||||
return {
|
||||
x: self.$bar.getEndX() + 2,
|
||||
y: self.$bar.getY() - 10
|
||||
};
|
||||
}
|
||||
|
||||
function bind_resize() {
|
||||
const { left, right } = get_handles();
|
||||
|
||||
left.drag(onmove_left, onstart, onstop_left);
|
||||
right.drag(onmove_right, onstart, onstop_right);
|
||||
|
||||
function onstart() {
|
||||
onstart();
|
||||
this.ox = this.getX();
|
||||
this.oy = this.getY();
|
||||
}
|
||||
|
||||
function onmove_right(dx, dy) {
|
||||
onmove_handle_right(dx, dy);
|
||||
}
|
||||
function onstop_right() {
|
||||
onstop_handle_right();
|
||||
}
|
||||
|
||||
function onmove_left(dx, dy) {
|
||||
onmove_handle_left(dx, dy);
|
||||
}
|
||||
function onstop_left() {
|
||||
onstop_handle_left();
|
||||
}
|
||||
}
|
||||
|
||||
function get_handles() {
|
||||
return {
|
||||
left: self.handle_group.select('.handle.left'),
|
||||
right: self.handle_group.select('.handle.right')
|
||||
};
|
||||
}
|
||||
|
||||
function bind_drag() {
|
||||
self.bar_group.drag(onmove, onstart, onstop);
|
||||
|
||||
function onmove(dx, dy) {
|
||||
onmove(dx, dy);
|
||||
}
|
||||
function onstop() {
|
||||
onstop();
|
||||
}
|
||||
function onstart() {
|
||||
onstart();
|
||||
}
|
||||
}
|
||||
|
||||
function bind_resize_progress() {
|
||||
const bar = self.$bar,
|
||||
bar_progress = self.$bar_progress,
|
||||
handle = self.group.select('.handle.progress');
|
||||
handle && handle.drag(onmove, onstart, onstop);
|
||||
|
||||
function onmove(dx, dy) {
|
||||
if (dx > bar_progress.max_dx) {
|
||||
dx = bar_progress.max_dx;
|
||||
}
|
||||
if (dx < bar_progress.min_dx) {
|
||||
dx = bar_progress.min_dx;
|
||||
}
|
||||
|
||||
bar_progress.attr('width', bar_progress.owidth + dx);
|
||||
handle.transform(`t{dx},0`);
|
||||
bar_progress.finaldx = dx;
|
||||
}
|
||||
function onstop() {
|
||||
if (!bar_progress.finaldx) return;
|
||||
progress_changed();
|
||||
set_action_completed();
|
||||
}
|
||||
function onstart() {
|
||||
bar_progress.finaldx = 0;
|
||||
bar_progress.owidth = bar_progress.getWidth();
|
||||
bar_progress.min_dx = -bar_progress.getWidth();
|
||||
bar_progress.max_dx = bar.getWidth() - bar_progress.getWidth();
|
||||
}
|
||||
}
|
||||
|
||||
function onstart() {
|
||||
const bar = self.$bar;
|
||||
bar.ox = bar.getX();
|
||||
bar.oy = bar.getY();
|
||||
bar.owidth = bar.getWidth();
|
||||
bar.finaldx = 0;
|
||||
run_method_for_dependencies('onstart');
|
||||
}
|
||||
self.onstart = onstart;
|
||||
|
||||
function onmove(dx, dy) {
|
||||
const bar = self.$bar;
|
||||
bar.finaldx = get_snap_position(dx);
|
||||
update_bar_position(bar.ox + bar.finaldx);
|
||||
run_method_for_dependencies('onmove', [dx, dy]);
|
||||
}
|
||||
self.onmove = onmove;
|
||||
|
||||
function onstop() {
|
||||
const bar = self.$bar;
|
||||
if (!bar.finaldx) return;
|
||||
date_changed();
|
||||
set_action_completed();
|
||||
run_method_for_dependencies('onstop');
|
||||
}
|
||||
self.onstop = onstop;
|
||||
|
||||
function onmove_handle_left(dx, dy) {
|
||||
const bar = self.$bar;
|
||||
bar.finaldx = get_snap_position(dx);
|
||||
update_bar_position(bar.ox + bar.finaldx, bar.owidth - bar.finaldx);
|
||||
run_method_for_dependencies('onmove_handle_left', [dx, dy]);
|
||||
}
|
||||
self.onmove_handle_left = onmove_handle_left;
|
||||
|
||||
function onstop_handle_left() {
|
||||
const bar = self.$bar;
|
||||
if (bar.finaldx) date_changed();
|
||||
set_action_completed();
|
||||
run_method_for_dependencies('onstop_handle_left');
|
||||
}
|
||||
self.onstop_handle_left = onstop_handle_left;
|
||||
|
||||
function run_method_for_dependencies(fn, args) {
|
||||
const dm = gt.dependency_map;
|
||||
if (dm[self.task.id]) {
|
||||
for (let deptask of dm[self.task.id]) {
|
||||
const dt = gt.get_bar(deptask);
|
||||
dt[fn].apply(dt, args);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onmove_handle_right(dx, dy) {
|
||||
const bar = self.$bar;
|
||||
bar.finaldx = get_snap_position(dx);
|
||||
update_bar_position(null, bar.owidth + bar.finaldx);
|
||||
}
|
||||
|
||||
function onstop_handle_right() {
|
||||
const bar = self.$bar;
|
||||
if (bar.finaldx) date_changed();
|
||||
set_action_completed();
|
||||
}
|
||||
|
||||
function update_bar_position(x, width) {
|
||||
const bar = self.$bar;
|
||||
if (x) update_attr(bar, 'x', x);
|
||||
if (width) update_attr(bar, 'width', width);
|
||||
update_label_position();
|
||||
update_handle_position();
|
||||
update_progressbar_position();
|
||||
update_arrow_position();
|
||||
update_details_position();
|
||||
}
|
||||
|
||||
// function click(callback) {
|
||||
// self.group.click(function () {
|
||||
// if (self.action_completed) {
|
||||
// // just finished a move action, wait for a few seconds
|
||||
// return;
|
||||
// }
|
||||
// if (self.group.hasClass('active')) {
|
||||
// callback(self.task);
|
||||
// }
|
||||
// unselect_all();
|
||||
// self.group.toggleClass('active');
|
||||
// });
|
||||
// }
|
||||
|
||||
function date_changed() {
|
||||
self.events.on_date_change &&
|
||||
self.events.on_date_change(
|
||||
self.task,
|
||||
compute_start_date(),
|
||||
compute_end_date()
|
||||
);
|
||||
}
|
||||
|
||||
function progress_changed() {
|
||||
self.events.on_progress_change &&
|
||||
self.events.on_progress_change(
|
||||
self.task,
|
||||
compute_progress()
|
||||
);
|
||||
}
|
||||
|
||||
function set_action_completed() {
|
||||
self.action_completed = true;
|
||||
setTimeout(() => self.action_completed = false, 2000);
|
||||
}
|
||||
|
||||
// function compute_date(x) {
|
||||
// const shift = (x - compute_x()) / gt.config.column_width;
|
||||
// const date = self.task._start.clone().add(gt.config.step * shift, 'hours');
|
||||
// return date;
|
||||
// }
|
||||
|
||||
function compute_start_date() {
|
||||
const bar = self.$bar,
|
||||
shift = (bar.getX() - compute_x()) / gt.config.column_width,
|
||||
new_start_date = self.task._start.clone().add(gt.config.step * shift, 'hours');
|
||||
return new_start_date;
|
||||
}
|
||||
|
||||
function compute_end_date() {
|
||||
const bar = self.$bar,
|
||||
og_x = compute_x() + self.duration * gt.config.column_width,
|
||||
final_x = bar.getEndX(),
|
||||
shift = (final_x - og_x) / gt.config.column_width,
|
||||
new_end_date = self.task._end.clone().add(gt.config.step * shift, 'hours');
|
||||
return new_end_date;
|
||||
}
|
||||
|
||||
function compute_progress() {
|
||||
return self.$bar_progress.getWidth() / self.$bar.getWidth() * 100;
|
||||
}
|
||||
|
||||
function compute_x() {
|
||||
let x = self.task._start.diff(gt.gantt_start, 'hours') /
|
||||
gt.config.step * gt.config.column_width;
|
||||
|
||||
if (gt.view_is('Month')) {
|
||||
x = self.task._start.diff(gt.config.start, 'days') *
|
||||
gt.config.column_width / 30;
|
||||
}
|
||||
return x;
|
||||
}
|
||||
|
||||
function compute_y() {
|
||||
return gt.config.header_height + gt.config.padding +
|
||||
self.task._index * (self.height + gt.config.padding);
|
||||
}
|
||||
|
||||
function get_snap_position(dx) {
|
||||
let odx = dx, rem, position;
|
||||
|
||||
if (gt.view_is('Week')) {
|
||||
rem = dx % (gt.config.column_width / 7);
|
||||
position = odx - rem +
|
||||
((rem < gt.config.column_width / 14) ? 0 : gt.config.column_width / 7);
|
||||
} else if (gt.view_is('Month')) {
|
||||
rem = dx % (gt.config.column_width / 30);
|
||||
position = odx - rem +
|
||||
((rem < gt.config.column_width / 60) ? 0 : gt.config.column_width / 30);
|
||||
} else {
|
||||
rem = dx % gt.config.column_width;
|
||||
position = odx - rem +
|
||||
((rem < gt.config.column_width / 2) ? 0 : gt.config.column_width);
|
||||
}
|
||||
return position;
|
||||
}
|
||||
|
||||
function update_attr(element, attr, value) {
|
||||
value = +value;
|
||||
if (!isNaN(value)) {
|
||||
element.attr(attr, value);
|
||||
}
|
||||
return element;
|
||||
}
|
||||
|
||||
function update_progressbar_position() {
|
||||
self.$bar_progress.attr('x', self.$bar.getX());
|
||||
self.$bar_progress.attr('width', self.$bar.getWidth() * (self.task.progress / 100));
|
||||
}
|
||||
|
||||
function update_label_position() {
|
||||
const bar = self.$bar,
|
||||
label = self.group.select('.bar-label');
|
||||
if (label.getBBox().width > bar.getWidth()) {
|
||||
label.addClass('big').attr('x', bar.getX() + bar.getWidth() + 5);
|
||||
} else {
|
||||
label.removeClass('big').attr('x', bar.getX() + bar.getWidth() / 2);
|
||||
}
|
||||
}
|
||||
|
||||
function update_handle_position() {
|
||||
const bar = self.$bar;
|
||||
self.handle_group.select('.handle.left').attr({
|
||||
'x': bar.getX() + 1
|
||||
});
|
||||
self.handle_group.select('.handle.right').attr({
|
||||
'x': bar.getEndX() - 9
|
||||
});
|
||||
}
|
||||
|
||||
function update_arrow_position() {
|
||||
for (let arrow of self.arrows) {
|
||||
arrow.update();
|
||||
}
|
||||
}
|
||||
|
||||
function update_details_position() {
|
||||
const details_box = gt.element_groups.details.select('.details-wrapper');
|
||||
const pos = get_details_position();
|
||||
details_box && details_box.transform(`t${pos.x},${pos.y}`);
|
||||
}
|
||||
|
||||
// function unselect_all() {
|
||||
// gt.canvas.selectAll('.bar-wrapper').forEach(function (el) {
|
||||
// el.removeClass('active');
|
||||
// });
|
||||
// }
|
||||
|
||||
init();
|
||||
|
||||
return self;
|
||||
}
|
||||
516
src/Gantt.js
Normal file
516
src/Gantt.js
Normal file
@ -0,0 +1,516 @@
|
||||
/* global moment, Snap */
|
||||
/**
|
||||
* Gantt:
|
||||
* element: querySelector string, required
|
||||
* tasks: array of tasks, required
|
||||
* task: { id, name, start, end, progress, dependencies }
|
||||
* config: configuration options, optional
|
||||
*/
|
||||
import './gantt.scss';
|
||||
|
||||
import Bar from './Bar';
|
||||
import Arrow from './Arrow';
|
||||
|
||||
export default function Gantt(element, tasks, config) {
|
||||
|
||||
const self = {};
|
||||
|
||||
function init() {
|
||||
set_defaults();
|
||||
// initialize with default view mode
|
||||
change_view_mode(self.config.view_mode);
|
||||
}
|
||||
|
||||
function set_defaults() {
|
||||
|
||||
const defaults = {
|
||||
header_height: 50,
|
||||
column_width: 30,
|
||||
step: 24,
|
||||
view_modes: [
|
||||
'Quarter Day',
|
||||
'Half Day',
|
||||
'Day',
|
||||
'Week',
|
||||
'Month'
|
||||
],
|
||||
bar: {
|
||||
height: 20
|
||||
},
|
||||
arrow: {
|
||||
curve: 5
|
||||
},
|
||||
padding: 18,
|
||||
view_mode: 'Day',
|
||||
date_format: 'YYYY-MM-DD'
|
||||
};
|
||||
|
||||
self.element = element;
|
||||
self._tasks = tasks;
|
||||
self.config = Object.assign({}, defaults, config);
|
||||
|
||||
self._bars = [];
|
||||
self._arrows = [];
|
||||
self.element_groups = {};
|
||||
}
|
||||
|
||||
function change_view_mode(mode) {
|
||||
set_scale(mode);
|
||||
prepare();
|
||||
render();
|
||||
}
|
||||
|
||||
function prepare() {
|
||||
prepare_tasks();
|
||||
prepare_dependencies();
|
||||
prepare_dates();
|
||||
prepare_canvas();
|
||||
}
|
||||
|
||||
function prepare_tasks() {
|
||||
|
||||
// prepare tasks
|
||||
self.tasks = self._tasks.map((task, i) => {
|
||||
|
||||
// momentify
|
||||
task._start = moment(task.start, self.config.date_format);
|
||||
task._end = moment(task.end, self.config.date_format);
|
||||
|
||||
// cache index
|
||||
task._index = i;
|
||||
|
||||
// invalid dates
|
||||
if(!task.start && !task.end) {
|
||||
task._start = moment().startOf('day');
|
||||
task._end = moment().startOf('day').add(2, 'days');
|
||||
} else if(!task.start) {
|
||||
task._start = task._end.clone().add(-2, 'days');
|
||||
} else {
|
||||
task._end = task._start.clone().add(2, 'days');
|
||||
}
|
||||
|
||||
// invalid flag
|
||||
if(!task.start || !task.end) {
|
||||
task.invalid = true;
|
||||
}
|
||||
|
||||
// dependencies
|
||||
let deps;
|
||||
|
||||
if(task.dependencies) {
|
||||
deps = task.dependencies
|
||||
.split(',')
|
||||
.map(d => d.trim())
|
||||
.filter((d) => d);
|
||||
} else {
|
||||
deps = [];
|
||||
}
|
||||
task.dependencies = deps;
|
||||
|
||||
return task;
|
||||
});
|
||||
}
|
||||
function prepare_dependencies() {
|
||||
|
||||
self.dependency_map = {};
|
||||
for(let t of self.tasks) {
|
||||
for(let d of t.dependencies) {
|
||||
self.dependency_map[d] = self.dependency_map[d] || [];
|
||||
self.dependency_map[d].push(t.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function prepare_dates() {
|
||||
|
||||
for(let task of self.tasks) {
|
||||
// set global start and end date
|
||||
if(!self.gantt_start || task._start < self.gantt_start) {
|
||||
self.gantt_start = task._start;
|
||||
}
|
||||
if(!self.gantt_end || task._end > self.gantt_end) {
|
||||
self.gantt_end = task._end;
|
||||
}
|
||||
}
|
||||
set_gantt_dates();
|
||||
setup_dates();
|
||||
}
|
||||
|
||||
function prepare_canvas() {
|
||||
self.canvas = Snap(self.element).addClass('gantt');
|
||||
}
|
||||
|
||||
function render() {
|
||||
clear();
|
||||
setup_groups();
|
||||
make_grid();
|
||||
make_dates();
|
||||
make_bars();
|
||||
make_arrows();
|
||||
map_arrows_on_bars();
|
||||
setup_events();
|
||||
set_width();
|
||||
set_scroll_position();
|
||||
bind_grid_click();
|
||||
}
|
||||
|
||||
function clear() {
|
||||
self.canvas.clear();
|
||||
self._bars = [];
|
||||
self._arrows = [];
|
||||
}
|
||||
|
||||
function set_gantt_dates() {
|
||||
|
||||
if(view_is(['Quarter Day', 'Half Day'])) {
|
||||
self.gantt_start = self.gantt_start.clone().subtract(7, 'day');
|
||||
self.gantt_end = self.gantt_end.clone().add(7, 'day');
|
||||
} else if(view_is('Month')) {
|
||||
self.gantt_start = self.gantt_start.clone().startOf('year');
|
||||
self.gantt_end = self.gantt_end.clone().endOf('month').add(1, 'year');
|
||||
} else {
|
||||
self.gantt_start = self.gantt_start.clone().startOf('month').subtract(1, 'month');
|
||||
self.gantt_end = self.gantt_end.clone().endOf('month').add(1, 'month');
|
||||
}
|
||||
}
|
||||
|
||||
function setup_dates() {
|
||||
|
||||
self.dates = [];
|
||||
let cur_date = null;
|
||||
|
||||
while(cur_date === null || cur_date < self.gantt_end) {
|
||||
if(!cur_date) {
|
||||
cur_date = self.gantt_start.clone();
|
||||
} else {
|
||||
cur_date = view_is('Month') ?
|
||||
cur_date.clone().add(1, 'month') :
|
||||
cur_date.clone().add(self.config.step, 'hours');
|
||||
}
|
||||
self.dates.push(cur_date);
|
||||
}
|
||||
}
|
||||
|
||||
function setup_groups() {
|
||||
|
||||
const groups = ['grid', 'date', 'arrow', 'progress', 'bar', 'details'];
|
||||
// make group layers
|
||||
for(let group of groups) {
|
||||
self.element_groups[group] = self.canvas.group().attr({'id': group});
|
||||
}
|
||||
}
|
||||
|
||||
function set_scale(scale) {
|
||||
self.config.view_mode = scale;
|
||||
|
||||
// fire viewmode_change event
|
||||
// self.events.on_viewmode_change(scale);
|
||||
// trigger("view_mode_change");
|
||||
|
||||
if(scale === 'Day') {
|
||||
self.config.step = 24;
|
||||
self.config.column_width = 38;
|
||||
} else if(scale === 'Half Day') {
|
||||
self.config.step = 24 / 2;
|
||||
self.config.column_width = 38;
|
||||
} else if(scale === 'Quarter Day') {
|
||||
self.config.step = 24 / 4;
|
||||
self.config.column_width = 38;
|
||||
} else if(scale === 'Week') {
|
||||
self.config.step = 24 * 7;
|
||||
self.config.column_width = 140;
|
||||
} else if(scale === 'Month') {
|
||||
self.config.step = 24 * 30;
|
||||
self.config.column_width = 120;
|
||||
}
|
||||
}
|
||||
|
||||
function set_width() {
|
||||
const cur_width = self.canvas.node.getBoundingClientRect().width;
|
||||
const actual_width = self.canvas.getBBox().width;
|
||||
if(cur_width < actual_width) {
|
||||
self.canvas.attr('width', actual_width);
|
||||
}
|
||||
}
|
||||
|
||||
function set_scroll_position() {
|
||||
const parent_element = document.querySelector(self.element).parentElement;
|
||||
if(!parent_element) return;
|
||||
|
||||
const scroll_pos = get_min_date().diff(self.gantt_start, 'hours') /
|
||||
self.config.step * self.config.column_width;
|
||||
parent_element.scrollLeft = scroll_pos;
|
||||
}
|
||||
|
||||
function get_min_date() {
|
||||
const task = self.tasks.reduce((acc, curr) => {
|
||||
return curr._start.isSameOrBefore(acc._start) ? curr : acc;
|
||||
});
|
||||
return task._start;
|
||||
}
|
||||
|
||||
function make_grid() {
|
||||
make_grid_background();
|
||||
make_grid_rows();
|
||||
make_grid_header();
|
||||
make_grid_ticks();
|
||||
make_grid_highlights();
|
||||
}
|
||||
|
||||
function make_grid_background() {
|
||||
|
||||
const grid_width = self.dates.length * self.config.column_width,
|
||||
grid_height = self.config.header_height + self.config.padding +
|
||||
(self.config.bar.height + self.config.padding) * self.tasks.length;
|
||||
|
||||
self.canvas.rect(0, 0, grid_width, grid_height)
|
||||
.addClass('grid-background')
|
||||
.appendTo(self.element_groups.grid);
|
||||
|
||||
self.canvas.attr({
|
||||
height: grid_height + self.config.padding,
|
||||
width: '100%'
|
||||
});
|
||||
}
|
||||
|
||||
function make_grid_header() {
|
||||
const header_width = self.dates.length * self.config.column_width,
|
||||
header_height = self.config.header_height + 10;
|
||||
self.canvas.rect(0, 0, header_width, header_height)
|
||||
.addClass('grid-header')
|
||||
.appendTo(self.element_groups.grid);
|
||||
}
|
||||
|
||||
function make_grid_rows() {
|
||||
|
||||
const rows = self.canvas.group().appendTo(self.element_groups.grid),
|
||||
lines = self.canvas.group().appendTo(self.element_groups.grid),
|
||||
row_width = self.dates.length * self.config.column_width,
|
||||
row_height = self.config.bar.height + self.config.padding;
|
||||
|
||||
let row_y = self.config.header_height + self.config.padding / 2;
|
||||
|
||||
for(let task of self.tasks) { // eslint-disable-line
|
||||
self.canvas.rect(0, row_y, row_width, row_height)
|
||||
.addClass('grid-row')
|
||||
.appendTo(rows);
|
||||
|
||||
self.canvas.line(0, row_y + row_height, row_width, row_y + row_height)
|
||||
.addClass('row-line')
|
||||
.appendTo(lines);
|
||||
|
||||
row_y += self.config.bar.height + self.config.padding;
|
||||
}
|
||||
}
|
||||
|
||||
function make_grid_ticks() {
|
||||
let tick_x = 0,
|
||||
tick_y = self.config.header_height + self.config.padding / 2,
|
||||
tick_height = (self.config.bar.height + self.config.padding) * self.tasks.length;
|
||||
|
||||
for(let date of self.dates) {
|
||||
let tick_class = 'tick';
|
||||
// thick tick for monday
|
||||
if(view_is('Day') && date.day() === 1) {
|
||||
tick_class += ' thick';
|
||||
}
|
||||
// thick tick for first week
|
||||
if(view_is('Week') && date.date() >= 1 && date.date() < 8) {
|
||||
tick_class += ' thick';
|
||||
}
|
||||
// thick ticks for quarters
|
||||
if(view_is('Month') && date.month() % 3 === 0) {
|
||||
tick_class += ' thick';
|
||||
}
|
||||
|
||||
self.canvas.path(Snap.format('M {x} {y} v {height}', {
|
||||
x: tick_x,
|
||||
y: tick_y,
|
||||
height: tick_height
|
||||
}))
|
||||
.addClass(tick_class)
|
||||
.appendTo(self.element_groups.grid);
|
||||
|
||||
if(view_is('Month')) {
|
||||
tick_x += date.daysInMonth() * self.config.column_width / 30;
|
||||
} else {
|
||||
tick_x += self.config.column_width;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function make_grid_highlights() {
|
||||
|
||||
// highlight today's date
|
||||
if(view_is('Day')) {
|
||||
const x = moment().startOf('day').diff(self.gantt_start, 'hours') /
|
||||
self.config.step * self.config.column_width;
|
||||
const y = 0;
|
||||
const width = self.config.column_width;
|
||||
const height = (self.config.bar.height + self.config.padding) * self.tasks.length +
|
||||
self.config.header_height + self.config.padding / 2;
|
||||
|
||||
self.canvas.rect(x, y, width, height)
|
||||
.addClass('today-highlight')
|
||||
.appendTo(self.element_groups.grid);
|
||||
}
|
||||
}
|
||||
|
||||
function make_dates() {
|
||||
|
||||
for(let date of get_dates_to_draw()) {
|
||||
self.canvas.text(date.lower_x, date.lower_y, date.lower_text)
|
||||
.addClass('lower-text')
|
||||
.appendTo(self.element_groups.date);
|
||||
|
||||
if(date.upper_text) {
|
||||
const $upper_text = self.canvas.text(date.upper_x, date.upper_y, date.upper_text)
|
||||
.addClass('upper-text')
|
||||
.appendTo(self.element_groups.date);
|
||||
|
||||
// remove out-of-bound dates
|
||||
if($upper_text.getBBox().x2 > self.element_groups.grid.getBBox().width) {
|
||||
$upper_text.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function get_dates_to_draw() {
|
||||
let last_date = null;
|
||||
const dates = self.dates.map((date, i) => {
|
||||
const d = get_date_info(date, last_date, i);
|
||||
last_date = date;
|
||||
return d;
|
||||
});
|
||||
return dates;
|
||||
}
|
||||
|
||||
function get_date_info(date, last_date, i) {
|
||||
if(!last_date) {
|
||||
last_date = date.clone().add(1, 'year');
|
||||
}
|
||||
const date_text = {
|
||||
'Quarter Day_lower': date.format('HH'),
|
||||
'Half Day_lower': date.format('HH'),
|
||||
'Day_lower': date.date() !== last_date.date() ? date.format('D') : '',
|
||||
'Week_lower': 'Week ' + date.format('W'),
|
||||
'Month_lower': date.format('MMMM'),
|
||||
'Quarter Day_upper': date.date() !== last_date.date() ? date.format('D MMM') : '',
|
||||
'Half Day_upper': date.date() !== last_date.date() ? date.format('D MMM') : '',
|
||||
'Day_upper': date.month() !== last_date.month() ? date.format('MMMM') : '',
|
||||
'Week_upper': date.month() !== last_date.month() ? date.format('MMMM') : '',
|
||||
'Month_upper': date.year() !== last_date.year() ? date.format('YYYY') : ''
|
||||
};
|
||||
|
||||
const base_pos = {
|
||||
x: i * self.config.column_width,
|
||||
lower_y: self.config.header_height,
|
||||
upper_y: self.config.header_height - 25
|
||||
};
|
||||
|
||||
const x_pos = {
|
||||
'Quarter Day_lower': (self.config.column_width * 4) / 2,
|
||||
'Quarter Day_upper': 0,
|
||||
'Half Day_lower': (self.config.column_width * 2) / 2,
|
||||
'Half Day_upper': 0,
|
||||
'Day_lower': self.config.column_width / 2,
|
||||
'Day_upper': (self.config.column_width * 30) / 2,
|
||||
'Week_lower': self.config.column_width / 2,
|
||||
'Week_upper': (self.config.column_width * 4) / 2,
|
||||
'Month_lower': (date.daysInMonth() * self.config.column_width / 30) / 2,
|
||||
'Month_upper': (self.config.column_width * 12) / 2
|
||||
};
|
||||
|
||||
return {
|
||||
upper_text: date_text[`${self.config.view_mode}_upper`],
|
||||
lower_text: date_text[`${self.config.view_mode}_lower`],
|
||||
upper_x: base_pos.x + x_pos[`${self.config.view_mode}_upper`],
|
||||
upper_y: base_pos.upper_y,
|
||||
lower_x: base_pos.x + x_pos[`${self.config.view_mode}_lower`],
|
||||
lower_y: base_pos.lower_y
|
||||
};
|
||||
}
|
||||
|
||||
function make_arrows() {
|
||||
|
||||
for(let task of self.tasks) {
|
||||
self._arrows = task.dependencies.map(dep => {
|
||||
const dependency = get_task(dep);
|
||||
if(!dependency) return;
|
||||
|
||||
const arrow = Arrow(
|
||||
self, // gt
|
||||
self._bars[dependency._index], // from_task
|
||||
self._bars[task._index] // to_task
|
||||
);
|
||||
self.element_groups.arrow.add(arrow.element);
|
||||
return arrow; // eslint-disable-line
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function make_bars() {
|
||||
|
||||
self._bars = self.tasks.map((task) => {
|
||||
const bar = Bar(self, task);
|
||||
self.element_groups.bar.add(bar.group);
|
||||
return bar;
|
||||
});
|
||||
}
|
||||
|
||||
function map_arrows_on_bars() {
|
||||
|
||||
for(let bar of self._bars) {
|
||||
bar.arrows = self._arrows.filter(arrow => {
|
||||
return arrow.from_task.task.id === bar.task.id ||
|
||||
arrow.to_task.task.id === bar.task.id;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function setup_events() {
|
||||
// this._bars.forEach(function(bar) {
|
||||
// bar.events.on_date_change = me.events.bar_on_date_change;
|
||||
// bar.events.on_progress_change = me.events.bar_on_progress_change;
|
||||
// bar.click(me.events.bar_on_click);
|
||||
// });
|
||||
}
|
||||
|
||||
function bind_grid_click() {
|
||||
self.element_groups.grid.click(() => {
|
||||
self.canvas.selectAll('.bar-wrapper').forEach(el => {
|
||||
el.removeClass('active');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function view_is(modes) {
|
||||
if (typeof modes === 'string') {
|
||||
return self.config.view_mode === modes;
|
||||
} else if(Array.isArray(modes)) {
|
||||
for (let mode of modes) {
|
||||
if(self.config.view_mode === mode) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
self.view_is = view_is;
|
||||
|
||||
function get_task(id) {
|
||||
self.tasks.find((task) => {
|
||||
return task.id === id;
|
||||
});
|
||||
}
|
||||
|
||||
function get_bar(id) {
|
||||
self._bars.find((bar) => {
|
||||
return bar.task.id === id;
|
||||
});
|
||||
}
|
||||
self.get_bar = get_bar; // required in Bar
|
||||
|
||||
init();
|
||||
|
||||
return self;
|
||||
}
|
||||
144
src/gantt.scss
Normal file
144
src/gantt.scss
Normal file
@ -0,0 +1,144 @@
|
||||
$bar-color: #b8c2cc;
|
||||
$bar-stroke: #8D99A6;
|
||||
$border-color: #e0e0e0;
|
||||
$light-bg: #f5f5f5;
|
||||
$light-border-color: #ebeff2;
|
||||
$light-yellow: #fcf8e3;
|
||||
$text-muted: #666;
|
||||
$text-light: #555;
|
||||
$text-color: #333;
|
||||
$blue: #a3a3ff;
|
||||
$handle-color: #ddd;
|
||||
|
||||
.gantt {
|
||||
#grid {
|
||||
.grid-background {
|
||||
fill: none;
|
||||
}
|
||||
.grid-header {
|
||||
fill: #ffffff;
|
||||
stroke: $border-color;
|
||||
stroke-width: 1.4;
|
||||
}
|
||||
.grid-row {
|
||||
fill: #ffffff;
|
||||
}
|
||||
.grid-row:nth-child(even) {
|
||||
fill: $light-bg;
|
||||
}
|
||||
.row-line {
|
||||
stroke: $light-border-color;
|
||||
}
|
||||
.tick {
|
||||
stroke: $border-color;
|
||||
stroke-width: 0.2;
|
||||
&.thick {
|
||||
stroke-width: 0.4;
|
||||
}
|
||||
}
|
||||
.today-highlight {
|
||||
fill: $light-yellow;
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
#arrow {
|
||||
fill: none;
|
||||
stroke: $text-muted;
|
||||
stroke-width: 1.4;
|
||||
}
|
||||
|
||||
.bar {
|
||||
fill: $bar-color;
|
||||
stroke: $bar-stroke;
|
||||
stroke-width: 0;
|
||||
transition: stroke-width .3s ease;
|
||||
}
|
||||
.bar-progress {
|
||||
fill: $blue;
|
||||
}
|
||||
.bar-invalid {
|
||||
fill: transparent;
|
||||
stroke: $bar-stroke;
|
||||
stroke-width: 1;
|
||||
stroke-dasharray: 5;
|
||||
|
||||
&~.bar-label {
|
||||
fill: $text-light;
|
||||
}
|
||||
}
|
||||
.bar-label {
|
||||
fill: #fff;
|
||||
dominant-baseline: central;
|
||||
text-anchor: middle;
|
||||
font-size: 12px;
|
||||
font-weight: lighter;
|
||||
letter-spacing: 0.8px;
|
||||
|
||||
&.big {
|
||||
fill: $text-light;
|
||||
text-anchor: start;
|
||||
}
|
||||
}
|
||||
|
||||
.handle {
|
||||
fill: $handle-color;
|
||||
cursor: ew-resize;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: opacity .3s ease;
|
||||
}
|
||||
|
||||
.bar-wrapper {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
.bar {
|
||||
stroke-width: 2;
|
||||
}
|
||||
|
||||
.handle {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
.bar {
|
||||
stroke-width: 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.primary-text, .secondary-text {
|
||||
font-size: 12px;
|
||||
text-anchor: middle;
|
||||
}
|
||||
.primary-text {
|
||||
fill: $text-light;
|
||||
}
|
||||
.secondary-text {
|
||||
fill: $text-color;
|
||||
}
|
||||
|
||||
#details {
|
||||
font-size: 14;
|
||||
|
||||
.details-container {
|
||||
stroke: $border-color;
|
||||
stroke-width: 1.1;
|
||||
fill: #fff;
|
||||
}
|
||||
.details-heading {
|
||||
fill: $text-color;
|
||||
font-weight: 500;
|
||||
}
|
||||
.details-body {
|
||||
fill: $text-light;
|
||||
}
|
||||
}
|
||||
|
||||
.hide {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
19
test/library.spec.js
Executable file
19
test/library.spec.js
Executable file
@ -0,0 +1,19 @@
|
||||
import chai from 'chai';
|
||||
import Library from '../lib/library.js';
|
||||
|
||||
chai.expect();
|
||||
|
||||
const expect = chai.expect;
|
||||
|
||||
let lib;
|
||||
|
||||
describe('Given an instance of my library', function () {
|
||||
before(function () {
|
||||
lib = new Library();
|
||||
});
|
||||
describe('when I need the name', function () {
|
||||
it('should return the name', () => {
|
||||
expect(lib.name).to.be.equal('Library');
|
||||
});
|
||||
});
|
||||
});
|
||||
52
webpack.config.js
Executable file
52
webpack.config.js
Executable file
@ -0,0 +1,52 @@
|
||||
var webpack = require('webpack');
|
||||
var UglifyJsPlugin = webpack.optimize.UglifyJsPlugin;
|
||||
var path = require('path');
|
||||
var env = require('yargs').argv.mode;
|
||||
|
||||
var libraryName = 'gantt';
|
||||
|
||||
var plugins = [], outputFile;
|
||||
|
||||
if (env === 'build') {
|
||||
plugins.push(new UglifyJsPlugin({ minimize: true }));
|
||||
outputFile = libraryName + '.min.js';
|
||||
} else {
|
||||
outputFile = libraryName + '.js';
|
||||
}
|
||||
|
||||
let config = {
|
||||
entry: __dirname + '/src/Gantt.js',
|
||||
devtool: 'source-map',
|
||||
output: {
|
||||
path: __dirname + '/lib',
|
||||
filename: outputFile,
|
||||
library: libraryName,
|
||||
libraryTarget: 'umd',
|
||||
umdNamedDefine: true
|
||||
},
|
||||
module: {
|
||||
loaders: [
|
||||
{
|
||||
test: /(\.jsx|\.js)$/,
|
||||
loader: 'babel',
|
||||
exclude: /(node_modules|bower_components)/
|
||||
},
|
||||
{
|
||||
test: /(\.jsx|\.js)$/,
|
||||
loader: 'eslint-loader',
|
||||
exclude: /node_modules/
|
||||
},
|
||||
{
|
||||
test: /\.scss$/,
|
||||
loaders: [ 'style', 'css?sourceMap', 'sass?sourceMap' ]
|
||||
}
|
||||
]
|
||||
},
|
||||
resolve: {
|
||||
root: path.resolve('./src'),
|
||||
extensions: ['', '.js']
|
||||
},
|
||||
plugins: plugins
|
||||
};
|
||||
|
||||
module.exports = config;
|
||||
Loading…
x
Reference in New Issue
Block a user