First commit

This commit is contained in:
Faris Ansari 2016-12-17 22:15:31 +05:30
commit 77af1bb8fb
15 changed files with 3464 additions and 0 deletions

4
.babelrc Executable file
View File

@ -0,0 +1,4 @@
{
"presets": ["es2015"],
"plugins": ["babel-plugin-add-module-exports"]
}

182
.eslintrc Executable file
View 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
View 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
View 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

File diff suppressed because one or more lines are too long

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

File diff suppressed because one or more lines are too long

1
lib/gantt.min.js.map Normal file

File diff suppressed because one or more lines are too long

52
package.json Executable file
View 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
View 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
View 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
View 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
View 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
View 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
View 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;