Last active
January 16, 2017 17:16
-
-
Save orrery/4704709 to your computer and use it in GitHub Desktop.
D3 Gantt chart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html> | |
<head> | |
<meta http-equiv="Content-Type" content="text/html;charset=utf-8"> | |
<script src="http://d3js.org/d3.v3.min.js"></script> | |
<script src="http://code.jquery.com/jquery-1.8.3.min.js"></script> | |
<script src="https://raw.github.com/timrwood/moment/1.6.0/min/moment.min.js"></script> | |
<style type="text/css"> | |
#chart-mini { | |
margin: 0 auto 0px auto; | |
box-shadow: 3px 2px 7px rgba(0, 0, 0, 0.6); | |
padding: 0px 0px; | |
} | |
#chart-headertop { | |
height: 45px; | |
margin: 0 auto 0px auto; | |
box-shadow: 3px -2px 7px rgba(0, 0, 0, 0.6); | |
padding: 0px 0px; | |
} | |
#chart-container { | |
height: 450px; | |
width: 1101px; | |
margin: 0 auto 0px auto; | |
box-shadow: 3px 2px 7px rgba(0, 0, 0, 0.6); | |
padding: 0px 0px; | |
overflow-y: visible; | |
overflow-x: hidden; | |
} | |
.axis path, .axis line { | |
fill: none; | |
stroke: #888888; | |
stroke-width: 1px; | |
shape-rendering: crispEdges; | |
} | |
.brush .extent { | |
stroke: gray; | |
fill: dodgerblue; | |
fill-opacity: .365; | |
} | |
path.link { | |
fill: none; | |
/* stroke: #3182bd; */ | |
stroke: #888888; | |
stroke-width: 1px; | |
} | |
.header rect { | |
cursor: pointer; | |
fill-opacity: 1.0; | |
stroke: #888888; | |
stroke-width: 1px; | |
shape-rendering: crispEdges; | |
} | |
.node rect { | |
cursor: pointer; | |
fill-opacity: 1.0; | |
stroke: #888888; | |
stroke-width: 1px; | |
} | |
.mini text { | |
font: 10px sans-serif; | |
pointer-events: none; | |
text-shadow: 0 1px 1px rgba(255, 255, 255, 0.8); | |
} | |
.main text { | |
font: 10px sans-serif; | |
pointer-events: none; | |
text-shadow: 0 1px 1px rgba(255, 255, 255, 0.8); | |
} | |
.axis text { | |
font: 10px sans-serif; | |
pointer-events: none; | |
text-shadow: 0 1px 1px rgba(255, 255, 255, 0.8); | |
} | |
#gantt { | |
padding: 2em 0 2em 0; | |
} | |
</style> | |
<!-- link rel="stylesheet" type="text/css" href="css/gantt.css" | |
/--> | |
<title>Gantt chart</title> | |
</head> | |
<body> | |
<div class="container"> | |
<section class="main"> | |
<p> | |
<div id="chart-headertop"></div> | |
<div id="chart-container"></div> | |
<script type="text/javascript"> | |
function GanttChart(jsonData, dateFormat) { | |
/** | |
*the top gap in pixels | |
*/ | |
var topGap = 40; | |
/** | |
*the right gap in pixels | |
*/ | |
var rightGap = 40; | |
/** | |
*the bottom gap in pixels | |
*/ | |
var bottomGap = 40; | |
/** | |
*the left gap in pixels | |
*/ | |
var leftGap = 40; | |
/** | |
*the horizontal size of a cell in pixels | |
*/ | |
var gridSizeX = 90; | |
/** | |
*the vertical size of a cell in pixels | |
*/ | |
var gridSizeY = 18; | |
/** | |
*the vertical gap between cells in pixels | |
*/ | |
var nodeGap = 3; | |
/** | |
* the gap between header svg and the bottom of the header canvas | |
*/ | |
var headerGap = 3; | |
/** | |
*the number of pixels to offset text horizontally within a cell. | |
*/ | |
var textOffsetX = 4; | |
/** | |
*the number of pixels to offset text vertically within a cell, around the vertical mid-point. | |
*/ | |
var textOffsetY = 4; | |
/** | |
*offsets the chart canvas this number of pixels to the right | |
*/ | |
var chartOffsetX = 5; | |
/** | |
* transition speed in ms | |
*/ | |
var transitionSpeed = 600; | |
/** | |
* the relative location in the tree grid node where the connector is positioned vertically (between 0 and 1.0) | |
*/ | |
var scaleFactorConnection = 0.6; | |
/** | |
*tree grid view node rounding on X-axis | |
*/ | |
var roundX = 3; | |
/** | |
* tree grid view node rounding on Y-axis | |
*/ | |
var roundY = 5; | |
var brushHeight = 80; | |
var miniGap = 25; | |
/** | |
*cell color at each depth (for depth exceeding colors.length, colors start from beginning again) | |
*/ | |
//http://simple.be/web/color/codes is handy | |
var colors = ["#f99b0c", "#f9603a ", "#f98972", "#f9baaa ", "#afbffb"]; | |
var local = this; | |
local.margin = { | |
top: topGap, | |
right: rightGap, | |
bottom: bottomGap, | |
left: leftGap | |
}; | |
local.width = $("#chart-container").width() - local.margin.left - local.margin.right; | |
local.height = $("#chart-container").height() - local.margin.top - local.margin.bottom; | |
$("#chart-mini").width(local.width + local.margin.left + local.margin.top); | |
$("#chart-headertop").width(local.width + local.margin.left + local.margin.top); | |
local.gridsizex = gridSizeX; // width of cell | |
local.gridsizey = gridSizeY; // height of cell | |
local.textoffsety = local.gridsizey / 2 + textOffsetY; | |
local.textoffsetx = textOffsetX; //local.gridsizex/2-local.gridsizey; | |
local.chartoffsetx = chartOffsetX; | |
local.nodeGap = nodeGap; // the gap between nodes in the tree view | |
local.headerGap = headerGap; // the gap between the header boxes and the bottom of the header canvas | |
local.chartoffsety = local.gridsizey + local.gridsizey / 2 + local.headerGap; | |
local.root; | |
local.nodes; | |
local.tree; | |
local.treedepth; | |
local.idCount = 0; | |
local.duration = d3.event && d3.event.altKey ? 7000 : transitionSpeed; | |
local.diagonal = d3.svg.diagonal().projection(function (d) { | |
return [d.x, d.y + local.gridsizey * scaleFactorConnection]; | |
}); | |
local.visitedNodeMap = {} | |
local.chart = initialiseChartCanvas(); | |
local.menu = initialiseHeaderCanvas(); | |
local.root = jsonData; | |
// create tree layout | |
local.tree = d3.layout.tree(); | |
local.nodes = local.tree.nodes(jsonData); | |
local.mini = initialiseminiCanvas(); | |
// get depth of tree. | |
local.treedepth = getTreeDepth(local.nodes); | |
createHeader(getColHeader(local.nodes)); | |
updateTimeStampsOnNodes(local.nodes); | |
local.xScale = createXScale(local.nodes); | |
local.xScaleBrush = createXScaleBrush(local.nodes); | |
local.xHeaderDateAxis = d3.svg.axis() | |
.scale(local.xScale) | |
.orient('top') | |
.ticks(d3.time.years, 1) | |
.tickFormat(d3.time.format('%Y')) | |
.tickSize(6, 3, 0); | |
local.menu.append('g') // append month axes (top) | |
.attr('transform', "translate(" + 0 + "," + -headerGap + ")") | |
.attr('class', 'main axis date') | |
.call(local.xHeaderDateAxis) | |
.selectAll('text') | |
var xMiniYearAxis = d3.svg.axis() | |
.scale(local.xScaleBrush) | |
.orient('top') | |
.ticks(d3.time.weeks, 26) | |
.tickFormat(d3.time.format('Wk.%U')) | |
.tickSize(6, 3, 0); | |
var xMiniMonthAxis = d3.svg.axis() | |
.scale(local.xScaleBrush) | |
.orient('bottom') | |
.ticks(d3.time.weeks, 26) | |
.tickFormat(d3.time.format('%d/%m/%Y')) | |
.tickSize(6, 3, 0); | |
local.visitedNodeMap = []; | |
// count the levels in the mini | |
local.countLevels = 0; | |
local.nodes.forEach(function (n, i) { | |
n.x = local.xScaleBrush(n._start); | |
// no mod for root node | |
if (!n.parent) { | |
local.treeIndexOffsetCounter = 0; | |
n.y = 50 + local.treeIndexOffsetCounter; | |
local.countLevels++; | |
} else { | |
// leaf node has not been seen under this parent before, therefore | |
// we add it to the visitedNodeMap for leaf nodes under the same parent, | |
// so the y coordinate can be reused. | |
if (!local.visitedNodeMap[n.parent.label + n.label]) { | |
local.treeIndexOffsetCounter += 1; | |
n.y = 50 + (local.treeIndexOffsetCounter - 1); | |
local.visitedNodeMap[n.parent.label + n.label] = { | |
y: n.y | |
}; | |
local.countLevels++; | |
} | |
// this leaf node under this parent has been seen before, therefore | |
// the calculation for the y position should use the previous y position | |
// for this leaf node under this parent. | |
// This re-uses the y coordinate of the first occurence of this leaf node | |
// under the same parent. | |
else { | |
n.x = local.xScaleBrush(n._start); | |
properNodePosition = local.visitedNodeMap[n.parent.label + n.label]; | |
n.y = properNodePosition.y; | |
} | |
} | |
}); | |
local.mini.append('g') | |
.attr('transform', 'translate(0,' + miniGap + ')') | |
.attr('class', 'mini axis year') | |
.call(xMiniYearAxis) | |
.selectAll('text') | |
local.mini.append('g') | |
.attr('transform', 'translate(0,' + miniGap + ')') | |
.attr('class', 'mini axis month') | |
.call(xMiniMonthAxis) | |
.selectAll('text') | |
//mini item rects | |
local.mini.append("g") | |
.selectAll("miniItems") | |
.data(local.nodes) | |
.enter().append("rect") | |
.attr("fill", function (d, i) { | |
return getCellColor(d.depth) | |
}) | |
.attr("class", function (d, i) { | |
return "miniItem" + i; | |
}) | |
.attr("x", function (d) { | |
return local.xScaleBrush(d._start); | |
}) | |
.attr("y", function (d, i) { | |
return d.y; | |
}) | |
.attr("width", function (d) { | |
return (local.xScaleBrush(d._end) - local.xScaleBrush(d._start)); | |
}) | |
.attr("height", 1); | |
// now we know the node count, reset the brush height. | |
brushHeight = local.countLevels + 60; | |
$("#chart-mini").height(brushHeight); | |
//formatNodes(local.nodes) | |
// can copy here for display and modification, currently just reference. | |
local.root = jsonData; | |
//brush | |
local.brush = d3.svg.brush() | |
.x(local.xScaleBrush); | |
local.brush.on("brush", function () { | |
update(local.root); | |
}); //update(local.root= jsonData)); | |
local.brush.extent([local.root._start, local.root._end]); | |
local.mini.append("g") | |
.attr("class", "x brush") | |
.call(local.brush) | |
.selectAll("rect") | |
.attr("y", 1) | |
.attr("height", brushHeight - 1); | |
update(local.root); | |
// updates time stamps on nodes. | |
// iterates over all sub-trees, and updates time stamps on each node level. | |
function updateTimeStampsOnNodes(nodes) { | |
//assignTimeStamps(nodes[0]) | |
assignTimeStamps(nodes[0]) | |
} | |
function createXScaleBrush(nodes) { | |
return d3.time.scale().domain([nodes[0]._start, nodes[0]._end]).range([0, local.width + local.margin.left + 24]); | |
} | |
function createXScale(nodes) { | |
return d3.time.scale().domain([nodes[0]._start, nodes[0]._end]).range([(local.treedepth + 1) * local.gridsizex, local.width + local.margin.left + 24]); | |
} | |
/** | |
* recursively assign start and end date times to nodes. | |
* @param {Object} node | |
*/ | |
function assignTimeStamps(node) { | |
if (node.children) { | |
for (var i = 0; i < node.children.length; i++) { | |
var child = node.children[i]; | |
if (child == {}) { | |
continue; | |
} | |
var range = assignTimeStamps(child); | |
if (i == 0) { | |
node._start = range[0]; | |
node._end = range[1]; | |
} else { | |
if (range[0].getTime() < node._start.getTime()) { | |
node._start = range[0]; | |
} | |
if (range[1].getTime() > node._end.getTime()) { | |
node._end = range[1]; | |
} | |
} | |
} | |
return [node._start, node._end]; | |
} | |
// no more children (leaf node expects a start time and duration) | |
else { | |
node._start = getDate(node); | |
node._end = addDurationAndGetDate(node); | |
return [node._start, node._end]; | |
} | |
} | |
/** | |
* recursively assign start and end date times to nodes, where leaf nodes can have multiple activities. | |
* @param {Object} node | |
*/ | |
function assignTimeStampsForMultiActivities(node) { | |
if (node.children) { | |
for (var i = 0; i < node.children.length; i++) { | |
var child = node.children[i]; | |
if (child == {}) { | |
continue; | |
} | |
var range = assignTimeStampsForMultiActivities(child); | |
if (i == 0) { | |
node._start = range[0]; | |
node._end = range[1]; | |
} else { | |
if (range[0].getTime() < node._start.getTime()) { | |
node._start = range[0]; | |
} | |
if (range[1].getTime() > node._end.getTime()) { | |
node._end = range[1]; | |
} | |
} | |
} | |
return [node._start, node._end]; | |
} | |
// no more children (leaf node expects a start time and duration) | |
else { | |
node._start = getDate(node.activities[0]); | |
node._end = addDurationAndGetDate(node.activities[0]); | |
node.activities[0]._start = node._start; | |
node.activities[0]._end = node._end; | |
for (var j = 1; j < node.activities.length; j++) { | |
node.activities[j]._start = getDate(node.activities[j]); | |
node.activities[j]._end = addDurationAndGetDate(node.activities[j]); | |
if (node.activities[j]._start < node.activities[j - 1]._start) { | |
node._start = node.activities[j]._start; | |
} | |
if (node.activities[j]._end > node.activities[j - 1]._end) { | |
node._end = node.activities[j]._end; | |
} | |
} | |
//node._start = getDate(node); | |
//node._end = addDurationAndGetDate(node); | |
return [node._start, node._end]; | |
} | |
} | |
function createHeader(colheader) { | |
var maxlength = 0; | |
for (var i = 0; i < colheader.length; i++) { | |
ch = colheader[i] | |
if (ch != null && ch.length > maxlength) { | |
maxlength = ch.length; | |
} | |
} | |
// resets grid size based on text length (note local should get the font size from the CSS and not use an arbitrary scaling factor) | |
local.gridsizex = 6 * maxlength; | |
// now get the font size, and set local.gridsizex in relation to the max length and font size. | |
var header = local.menu.selectAll("g.header") | |
.data(colheader) | |
// antony - fix scaling factor | |
var headerEnter = header.enter().append("svg:g") // a group of elements inside an svg canvas | |
.attr("class", "header") | |
.attr("transform", function (d, i) { | |
return "translate(" + (local.gridsizex * i) + "," + -(local.gridsizey + 5 * local.headerGap) + ")"; | |
}) | |
headerEnter.append("svg:rect") | |
.attr("fill", function (d, i) { | |
return getCellColor(i) | |
}) | |
.attr("width", local.gridsizex) | |
.attr("height", local.gridsizey * 2) | |
// antony - fix scaling factor | |
headerEnter.append("svg:text") | |
.attr("dx", local.textoffsetx) | |
.attr("dy", local.textoffsety * 1.6) | |
.text(function (d) { | |
return d; | |
}) | |
} | |
function initialiseminiCanvas() { | |
brushHeight = local.nodes.length + 60; | |
$("#chart-mini").height(brushHeight); | |
var chartbrush = d3.select("#chart-mini").append("svg:svg") | |
.attr("width", local.width + local.margin.left + local.margin.right) | |
.attr("height", brushHeight) | |
.attr('class', 'chartbrush'); | |
chartbrush.append('defs').append('clipPath') | |
.attr('id', 'clip') | |
.append('rect') | |
.attr('width', local.width + local.margin.left + local.margin.right) // set width of rect width of chart | |
.attr('height', brushHeight); // rect is mainheight high | |
// antony - fix scaling factor | |
var mini = chartbrush.append('g') | |
.attr("transform", "translate(" + local.chartoffsetx + "," + 0 + ")") // translate the group to these coordinates | |
.attr('width', local.width + local.margin.left + local.margin.right) // set width of rect width of chart | |
.attr('height', brushHeight) | |
.attr('class', 'mini'); // sets the class attribute to 'main' | |
return mini; | |
} | |
// initialises the header canvas. | |
function initialiseHeaderCanvas() { | |
var chartheader = d3.select("#chart-headertop").append("svg:svg") | |
.attr("width", local.width + local.margin.left + local.margin.right) | |
.attr("height", local.gridsizey + local.gridsizey * 0.5 + local.headerGap) | |
.attr('class', 'chartheader'); | |
chartheader.append('defs').append('clipPath') | |
.attr('id', 'clipmini') | |
.append('rect') | |
.attr('width', local.width + local.margin.left + local.margin.right) // set width of rect width of chart | |
.attr('height', local.gridsizey + local.gridsizey * 0.5 + local.headerGap); // rect is mainheight high | |
var mini = chartheader.append('g') | |
.attr("transform", "translate(" + local.chartoffsetx + "," + (local.chartoffsety - 0.8 * local.headerGap) + ")") // translate the group to these coordinates | |
.attr('width', local.width + local.margin.left + local.margin.right) // set width of rect width of chart | |
.attr('height', local.gridsizey + local.gridsizey * 0.5 + local.headerGap) // rect is mainheight high | |
.attr('class', 'mini'); // sets the class attribute to 'main' | |
return mini; | |
} | |
// initialise | |
function initialiseChartCanvas() { | |
var chart = d3.select('#chart-container') | |
.append('svg:svg') | |
.attr('width', local.width + local.margin.right + local.margin.left) | |
.attr('height', local.height + local.margin.top + local.margin.bottom) | |
.attr('class', 'chart'); | |
chart.append('defs').append('clipPath') // add clip path to 'defs' | |
.attr('id', 'clipmain') // assign id to clip path | |
.append('rect') // append rectangle to clip | |
.attr("class", "cliprect") | |
.attr('width', local.width) // set width of rect width of chart | |
.attr('height', local.height); // rect is mainheight high | |
// creating a grouping in the chart, which is a transform (translation left and to the top) | |
var main = chart.append('g') | |
.attr("transform", "translate(" + local.chartoffsetx + "," + local.chartoffsety + ")") // translate the group to these coordinates | |
.attr('width', local.width) // set height and width of main | |
.attr('height', local.height) | |
.attr('class', 'main') | |
return main; | |
} | |
function update(source) { | |
// get tree nodes. | |
local.nodes = local.tree.nodes(local.root); | |
// update the layout of nodes (removing collapsed nodes from the list in the next update pass), and do node transition animations. | |
updateNodeLayout(source); | |
// do the link transition animations. | |
updateLinkLayout(source); | |
// update the canvas height based on number of expanded tree elements. | |
updateCanvasHeight(); | |
} | |
// Toggle children on click | |
// (for next update pass, only visible children will be drawn) | |
function click(d) { | |
if (d.children) { | |
d._children = d.children; | |
d.children = null; | |
} else { | |
d.children = d._children; | |
d._children = null; | |
} | |
update(d); | |
} | |
// calculate the depth of the tree. | |
function getTreeDepth(nodes) { | |
var depth = 0; | |
for (var i = 0; i < nodes.length; i++) { | |
if (nodes[i].depth > depth) { | |
depth = nodes[i].depth; | |
} | |
} | |
return depth; | |
} | |
function updateLinkLayout(source) { | |
// enter new links at parent's previous location. | |
var link = local.chart.selectAll("path.link") | |
.data(local.tree.links(local.nodes), function (d) { | |
return d.target.id; | |
}); | |
link.enter().insert("svg:path", "g") | |
.attr("class", "link") | |
.attr("d", function (d) { | |
var o = { | |
x: source.x0 + local.gridsizex * 0.5, | |
y: source.y0 | |
}; | |
return local.diagonal({ | |
source: o, | |
target: o | |
}); | |
}) | |
.transition() | |
.duration(local.duration) | |
.attr("d", local.diagonal); | |
// Transition links to their new position. | |
link.transition() | |
.duration(local.duration) | |
.attr("d", local.diagonal); | |
// Transition exiting nodes to the parent's new position. | |
link.exit().transition().duration(local.duration).attr("d", function (d) { | |
var o = { | |
x: source.x, | |
y: source.y | |
}; | |
return local.diagonal({ | |
source: o, | |
target: o | |
}); | |
}).remove(); | |
// update lanes | |
// Stash the old positions for transition. | |
local.nodes.forEach(function (d) { | |
d.x0 = d.x; | |
d.y0 = d.y; | |
}); | |
} | |
function openWin(text) { | |
popupGanttWindow = window.open('', '', 'width=200,height=100'); | |
popupGanttWindow.document.write(text); | |
popupGanttWindow.focus(); | |
} | |
function updateNodeLayout(source) { | |
// Compute the "layout". | |
var minExtent = local.brush.extent()[0]; | |
var maxExtent = local.brush.extent()[1]; | |
var visItems = local.nodes.filter(function (d) { | |
return d._start < maxExtent && d._end > minExtent; | |
}); | |
local.mini.select(".brush").call(local.brush.extent([minExtent, maxExtent])); | |
local.xScale.domain([minExtent, maxExtent]); | |
//console.log(maxExtent-minExtent) | |
if ((maxExtent - minExtent) > 103452230000) { | |
local.xHeaderDateAxis.ticks(d3.time.months, 6).tickFormat(d3.time.format('%d/%m/%Y')).tickSize(6, 3, 0); | |
//x1MonthAxis.ticks(d3.time.mondays, 1).tickFormat(d3.time.format('%b - Week %W')) | |
} else if ((maxExtent - minExtent) > 73452230000) { | |
local.xHeaderDateAxis.ticks(d3.time.months, 3).tickFormat(d3.time.format('%d/%m/%Y')).tickSize(6, 3, 0); | |
//x1MonthAxis.ticks(d3.time.mondays, 1).tickFormat(d3.time.format('%b - Week %W')) | |
} else if ((maxExtent - minExtent) > 43452240000) { | |
local.xHeaderDateAxis.ticks(d3.time.months, 2).tickFormat(d3.time.format('%d/%m/%Y')).tickSize(6, 3, 0); | |
//x1MonthAxis.ticks(d3.time.mondays, 1).tickFormat(d3.time.format('%b - Week %W')) | |
} else if ((maxExtent - minExtent) > 20452240000) { | |
local.xHeaderDateAxis.ticks(d3.time.months, 1).tickFormat(d3.time.format('%d/%m/%Y')).tickSize(6, 3, 0); | |
//x1MonthAxis.ticks(d3.time.mondays, 1).tickFormat(d3.time.format('%b - Week %W')) | |
} else if ((maxExtent - minExtent) > 7452240000) { | |
local.xHeaderDateAxis.ticks(d3.time.weeks, 2).tickFormat(d3.time.format('%d/%m/%Y')).tickSize(6, 3, 0); | |
} else if ((maxExtent - minExtent) > 4452240000) { | |
local.xHeaderDateAxis.ticks(d3.time.weeks, 1).tickFormat(d3.time.format('%d/%m/%Y')).tickSize(6, 3, 0); | |
} else if ((maxExtent - minExtent) > 3052240000) { | |
local.xHeaderDateAxis.ticks(d3.time.days, 2).tickFormat(d3.time.format('%d/%m')).tickSize(6, 3, 0); | |
} else if ((maxExtent - minExtent) > 297200000) { | |
local.xHeaderDateAxis.ticks(d3.time.days, 1).tickFormat(d3.time.format('%d/%m')).tickSize(6, 3, 0); | |
} else if ((maxExtent - minExtent) > 287200000) { | |
local.xHeaderDateAxis.ticks(d3.time.hours, 12).tickFormat(d3.time.format('%a-%H:%M')).tickSize(6, 3, 0); | |
} else if ((maxExtent - minExtent) > 267200000) { | |
local.xHeaderDateAxis.ticks(d3.time.hours, 6).tickFormat(d3.time.format('%a-%H:%M')).tickSize(6, 3, 0); | |
} else if ((maxExtent - minExtent) > 247200000) { | |
local.xHeaderDateAxis.ticks(d3.time.hours, 4).tickFormat(d3.time.format('%a-%H:%M')).tickSize(6, 3, 0); | |
} else if ((maxExtent - minExtent) > 57200000) { | |
local.xHeaderDateAxis.ticks(d3.time.hours, 3).tickFormat(d3.time.format('%a-%H:%M')).tickSize(6, 3, 0); | |
} else if ((maxExtent - minExtent) > 4800000) { | |
local.xHeaderDateAxis.ticks(d3.time.hours, 1).tickFormat(d3.time.format('%a-%H:%M')).tickSize(6, 3, 0); | |
} | |
local.menu.select('.main.axis.date').call(local.xHeaderDateAxis); | |
// reset the visited node map so tree collapse updates correctly. | |
local.treeIndexOffsetCounter = 0; | |
local.visitedNodeMap = []; | |
local.nodes.forEach(function (n, i) { | |
// no mod for root node | |
if (!n.parent) { | |
n.x = n.depth * local.gridsizex; | |
n.x0 = n.x; | |
local.treeIndexOffsetCounter = 0; | |
n.y = (local.treeIndexOffsetCounter - 1) * local.gridsizey; | |
n.y0 = n.y; | |
} else { | |
// leaf node has not been seen under this parent before, therefore | |
// we add it to the visitedNodeMap for leaf nodes under the same parent, | |
// so the y coordinate can be reused. | |
if (!local.visitedNodeMap[n.parent.label + n.label]) { | |
local.treeIndexOffsetCounter += 1; | |
n.x = n.depth * local.gridsizex; | |
n.x0 = n.x; | |
n.y = (local.treeIndexOffsetCounter - 1) * local.gridsizey; | |
n.y0 = n.y; | |
local.visitedNodeMap[n.parent.label + n.label] = { | |
y: n.y, | |
y0: n.y0 | |
}; | |
} | |
// this leaf node under this parent has been seen before, therefore | |
// the calculation for the y position should use the previous y position | |
// for this leaf node under this parent. | |
// This re-uses the y coordinate of the first occurence of this leaf node | |
// under the same parent. | |
else { | |
n.x = n.depth * local.gridsizex; | |
n.x0 = n.x; | |
properNodePosition = local.visitedNodeMap[n.parent.label + n.label]; | |
n.y = properNodePosition.y; | |
n.y0 = properNodePosition.y0; | |
} | |
} | |
}); | |
// update the nodes | |
var node = local.chart.selectAll("g.node") | |
.data(local.nodes, function (d) { | |
if (!d.id) { | |
d.id = ++local.idCount; | |
return d.id; | |
} | |
return d.id; | |
}); | |
// enter any new nodes at the parent's previous position | |
var nodeEnter = node.enter().append("svg:g") | |
.attr("class", "node") | |
.attr("transform", function (d) { | |
return "translate(" + source.x0 + "," + source.y0 + ")"; | |
}) | |
.style("opacity", 1e-6) | |
// reset visited map so we only draw the guide lines once | |
local.visitedNodeMap = []; | |
nodeEnter.append("svg:line") | |
.attr("class", "lane") | |
.attr("x1", function (d) { | |
return 0; | |
}) | |
.attr("y1", function (d) { | |
return local.gridsizey / 2; | |
}) | |
.attr("x2", local.width + local.margin.left + local.margin.right) | |
.attr("y2", function (d) { | |
return local.gridsizey / 2; | |
}) | |
.attr("stroke", function (d) { | |
return getCellColor(d.depth); | |
}) | |
.attr("opacity", function (d) { | |
if (d.parent) { | |
if (local.visitedNodeMap[d.parent.label + d.label]) { | |
return 0.0; | |
} else { | |
local.visitedNodeMap[d.parent.label + d.label] = true; | |
return 1.0; | |
} | |
} | |
return 1.0; | |
}) | |
.attr("stroke-width", "4px"); | |
// need to add clip | |
node.selectAll(".activitybar") | |
.attr('x', function (d) { | |
return (local.xScale(d._start) - (d.depth) * local.gridsizex); | |
}) | |
.attr('width', function (d) { | |
var time = (local.xScale(d._end) - local.xScale(d._start)); | |
return time; | |
}) | |
node.append("rect") | |
.attr("class", "activitybar") | |
.attr('x', function (d) { | |
return (local.xScale(d._start) - (d.depth) * local.gridsizex); | |
}) | |
.attr('y', function (d) { | |
return 0; | |
}) | |
.attr('width', function (d) { | |
var time = (local.xScale(d._end) - local.xScale(d._start)); | |
if (time == 0) { | |
return local.width; | |
} | |
return time; | |
}) | |
.attr('opacity', function (d) { | |
var time = (local.xScale(d._end) - local.xScale(d._start)); | |
if (time == 0) { | |
return 0.3; | |
} | |
return 1.0; | |
}) | |
.attr('height', function (d) { | |
return local.gridsizey - 4; | |
}) | |
.attr('fill', function (d) { | |
var time = (local.xScale(d._end) - local.xScale(d._start)); | |
if (time == 0) { | |
return 'grey'; | |
} | |
return getCellColor(d.depth); | |
}) | |
.attr("stroke", function (d) { | |
return "black"; | |
}) | |
.on("click", function (d) { | |
openWin("label : " + d.label + "<br>start : " + d._start + "<br>end : " + d._end); | |
}) | |
.append("svg:title") | |
.text(function (d, i) { | |
var time = (local.xScale(d._end) - local.xScale(d._start)); | |
var str = ("label : " + d.label + "\nstart : " + d._start + "\nend : " + d._end); | |
if (time == 0) { | |
return str + "\n No duration provided for this activity."; | |
} | |
return str; | |
}); | |
nodeEnter.append("svg:rect") | |
.attr("class", "treenode") | |
.attr("fill", function (d) { | |
return getCellColor(d.depth); | |
}) | |
.attr("rx", roundX) | |
.attr("ry", roundY) | |
.attr("width", local.gridsizex) | |
.attr("height", local.gridsizey - local.nodeGap) | |
.on("click", click) | |
nodeEnter.append("svg:text") | |
.attr("dx", local.textoffsetx) | |
.attr("dy", local.textoffsety) | |
.text(function (d) { | |
return d.label; | |
}) | |
// Transition nodes to their new position. | |
nodeEnter.transition() | |
.duration(local.duration) | |
.attr("transform", function (d) { | |
return "translate(" + d.x + "," + d.y + ")"; | |
}) | |
.style("opacity", 1); | |
node.transition() | |
.duration(local.duration) | |
.attr("transform", function (d) { | |
return "translate(" + d.x + "," + d.y + ")"; | |
}) | |
.style("opacity", 1) | |
.select("rect") | |
.style("fill", function (d) { | |
return getCellColor(d) | |
}); | |
// Transition exiting nodes to the parent's new position. | |
node.exit().transition() | |
.duration(local.duration) | |
.attr("transform", function (d) { | |
return "translate(" + source.x + "," + source.y + ")"; | |
}) | |
.style("opacity", 1e-6) | |
.remove(); | |
} | |
function updateCanvasHeight() { | |
dynamicheight = local.nodes.length * local.gridsizey + 100; // 100 is padding | |
if (dynamicheight > local.height) { | |
d3.select("#chart-container svg").attr("height", dynamicheight + local.gridsizey / 2) | |
} | |
} | |
function getCellColor(treedepth) { | |
// mod function rolls over colorisations if more than depth of 6 in the tree | |
// (i.e. the 7th takes on the color of the first). | |
var index = treedepth % (colors.length); | |
return colors[index]; | |
} | |
// returns an in order array of column headings. | |
function getColHeader(nodes) { | |
// iterate over nodes and get the type for each depth, and return it in a list. | |
// Expects depth order. | |
var headermap = {} | |
var headerarr = [] | |
// get unique keys | |
for (var i = 0; i < nodes.length; i++) { | |
headermap[nodes[i].depth] = nodes[i].type; | |
} | |
// assign column titles to an array | |
for (key in headermap) { | |
headerarr.push(headermap[key]) | |
} | |
return headerarr; | |
} | |
/** | |
* adds a duration to a JSON string date and returns a js date object (assuming duration is split by char ':' ) | |
* @param {Object} d | |
*/ | |
function addDurationAndGetDate(d) { | |
var splitDuration = d.duration.split(":", 3); | |
var minutesInMilliseconds = parseInt(splitDuration[2]) * 60 * 1000; // convert to ms | |
var hoursInMilliseconds = parseInt(splitDuration[1]) * 60 * 60 * 1000; // convert to ms | |
var daysInMilliseconds = parseInt(splitDuration[0]) * 24 * 60 * 60 * 1000; // convert to ms | |
// get start date from node and construct date object | |
// var startDate = new Date($.datepicker.formatDate(d.start,dateFormat)); | |
var startDate = moment(d.start, dateFormat).toDate(); | |
// construct end date by adding time in ms for start date to ms from duration of activity. | |
var endDate = new Date(startDate.getTime() + daysInMilliseconds + hoursInMilliseconds + minutesInMilliseconds); | |
return endDate; | |
} | |
// returns a date object from a JSON string date | |
function getDate(d) { | |
// var date = $.datepicker.formatDate(d.start,dateFormat) | |
var date = moment(d.start, dateFormat).toDate(); | |
//console.log(d.start + " " +moment(d.start, dateFormat) + " " + date) | |
return date; | |
// return new Date(date); | |
} | |
} | |
$(document).ready(function () { | |
jsonData = { | |
"children": [{ | |
"children": [{ | |
"children": [{ | |
"duration": "0:0:0", | |
"label": "1", | |
"start": "2002-01-01", | |
"type": "Subtask Lvl.3" | |
}, { | |
"duration": "0:0:0", | |
"label": "2", | |
"start": "2002-01-01", | |
"type": "Subtask Lvl.3" | |
}, { | |
"duration": "0:0:0", | |
"label": "3", | |
"start": "2002-01-01", | |
"type": "Subtask Lvl.3" | |
}], | |
"label": "Subtask01", | |
"type": "Subtask" | |
}, { | |
"children": [{ | |
"duration": "32:0:34", | |
"label": "4", | |
"start": "2002-01-24", | |
"type": "Subtask Lvl.3" | |
}, { | |
"duration": "16:2:38", | |
"label": "5", | |
"start": "2002-03-09", | |
"type": "Subtask Lvl.3" | |
}, { | |
"duration": "0:0:1", | |
"label": "5", | |
"start": "2002-04-01", | |
"type": "Subtask Lvl.3" | |
}, { | |
"duration": "14:9:52", | |
"label": "5", | |
"start": "2002-04-14", | |
"type": "Subtask Lvl.3" | |
}, { | |
"duration": "20:17:2", | |
"label": "6", | |
"start": "2002-05-05", | |
"type": "Subtask Lvl.3" | |
}], | |
"label": "Subtask02", | |
"type": "Subtask" | |
}, { | |
"children": [{ | |
"duration": "35:1:44", | |
"label": "7", | |
"start": "2002-10-13", | |
"type": "Subtask Lvl.3" | |
}, { | |
"duration": "17:16:15", | |
"label": "8", | |
"start": "2002-11-30", | |
"type": "Subtask Lvl.3" | |
}, { | |
"duration": "4:8:29", | |
"label": "8", | |
"start": "2002-12-26", | |
"type": "Subtask Lvl.3" | |
}, { | |
"duration": "11:13:33", | |
"label": "8", | |
"start": "2003-01-01", | |
"type": "Subtask Lvl.3" | |
}, { | |
"duration": "32:7:19", | |
"label": "9", | |
"start": "2003-01-17", | |
"type": "Subtask Lvl.3" | |
}], | |
"label": "Subtask03", | |
"type": "Subtask" | |
}, { | |
"children": [{ | |
"duration": "30:7:4", | |
"label": "10", | |
"start": "2003-08-20", | |
"type": "Subtask Lvl.3" | |
}, { | |
"duration": "12:12:12", | |
"label": "10", | |
"start": "2003-10-01", | |
"type": "Subtask Lvl.3" | |
}, { | |
"duration": "42:14:38", | |
"label": "11", | |
"start": "2003-10-18", | |
"type": "Subtask Lvl.3" | |
}, { | |
"duration": "6:13:26", | |
"label": "12", | |
"start": "2003-12-15", | |
"type": "Subtask Lvl.3" | |
}, { | |
"duration": "4:8:29", | |
"label": "12", | |
"start": "2003-12-26", | |
"type": "Subtask Lvl.3" | |
}, { | |
"duration": "18:11:45", | |
"label": "12", | |
"start": "2004-01-01", | |
"type": "Subtask Lvl.3" | |
}, { | |
"duration": "4:7:42", | |
"label": "12", | |
"start": "2004-01-26", | |
"type": "Subtask Lvl.3" | |
}], | |
"label": "Subtask04", | |
"type": "Subtask" | |
}, { | |
"children": [{ | |
"duration": "31:22:0", | |
"label": "13", | |
"start": "2004-08-18", | |
"type": "Subtask Lvl.3" | |
}, { | |
"duration": "19:6:57", | |
"label": "13", | |
"start": "2004-10-01", | |
"type": "Subtask Lvl.3" | |
}, { | |
"duration": "42:9:18", | |
"label": "14", | |
"start": "2004-10-27", | |
"type": "Subtask Lvl.3" | |
}, { | |
"duration": "4:8:29", | |
"label": "14", | |
"start": "2004-12-26", | |
"type": "Subtask Lvl.3" | |
}, { | |
"duration": "2:1:47", | |
"label": "14", | |
"start": "2005-01-01", | |
"type": "Subtask Lvl.3" | |
}, { | |
"duration": "48:10:31", | |
"label": "15", | |
"start": "2005-01-03", | |
"type": "Subtask Lvl.3" | |
}], | |
"label": "Subtask05", | |
"type": "Subtask" | |
}, { | |
"children": [{ | |
"duration": "57:2:6", | |
"label": "16", | |
"start": "2005-08-17", | |
"type": "Subtask Lvl.3" | |
}, { | |
"duration": "56:13:27", | |
"label": "17", | |
"start": "2005-10-18", | |
"type": "Subtask Lvl.3" | |
}, { | |
"duration": "6:12:4", | |
"label": "18", | |
"start": "2005-12-17", | |
"type": "Subtask Lvl.3" | |
}, { | |
"duration": "5:13:44", | |
"label": "18", | |
"start": "2005-12-26", | |
"type": "Subtask Lvl.3" | |
}, { | |
"duration": "43:20:7", | |
"label": "18", | |
"start": "2006-01-01", | |
"type": "Subtask Lvl.3" | |
}], | |
"label": "Subtask06", | |
"type": "Subtask" | |
}, { | |
"children": [{ | |
"duration": "56:4:17", | |
"label": "19", | |
"start": "2006-08-19", | |
"type": "Subtask Lvl.3" | |
}, { | |
"duration": "55:13:19", | |
"label": "20", | |
"start": "2006-10-18", | |
"type": "Subtask Lvl.3" | |
}, { | |
"duration": "7:2:8", | |
"label": "21", | |
"start": "2006-12-17", | |
"type": "Subtask Lvl.3" | |
}, { | |
"duration": "5:13:44", | |
"label": "21", | |
"start": "2006-12-26", | |
"type": "Subtask Lvl.3" | |
}, { | |
"duration": "42:4:38", | |
"label": "21", | |
"start": "2007-01-01", | |
"type": "Subtask Lvl.3" | |
}], | |
"label": "Subtask07", | |
"type": "Subtask" | |
}, { | |
"children": [{ | |
"duration": "55:6:30", | |
"label": "22", | |
"start": "2007-08-15", | |
"type": "Subtask Lvl.3" | |
}, { | |
"duration": "54:13:9", | |
"label": "23", | |
"start": "2007-10-14", | |
"type": "Subtask Lvl.3" | |
}, { | |
"duration": "12:5:14", | |
"label": "24", | |
"start": "2007-12-11", | |
"type": "Subtask Lvl.3" | |
}, { | |
"duration": "5:13:44", | |
"label": "24", | |
"start": "2007-12-26", | |
"type": "Subtask Lvl.3" | |
}, { | |
"duration": "36:0:10", | |
"label": "24", | |
"start": "2008-01-01", | |
"type": "Subtask Lvl.3" | |
}], | |
"label": "Subtask08", | |
"type": "Subtask" | |
}], | |
"label": "Subtask 2", | |
"type": "Subtask" | |
}, { | |
"children": [{ | |
"children": [{ | |
"duration": "0:0:0", | |
"label": "25", | |
"start": "2002-01-02", | |
"type": "Subtask Lvl.3" | |
}, { | |
"duration": "0:0:0", | |
"label": "25", | |
"start": "2002-01-02", | |
"type": "Subtask Lvl.3" | |
}, { | |
"duration": "15:10:53", | |
"label": "26", | |
"start": "2002-01-02", | |
"type": "Subtask Lvl.3" | |
}], | |
"label": "Subtask201", | |
"type": "Subtask" | |
}, { | |
"children": [{ | |
"duration": "17:5:31", | |
"label": "27", | |
"start": "2002-06-05", | |
"type": "Subtask Lvl.3" | |
}, { | |
"duration": "11:14:38", | |
"label": "27", | |
"start": "2002-07-01", | |
"type": "Subtask Lvl.3" | |
}, { | |
"duration": "2:10:16", | |
"label": "27", | |
"start": "2002-07-18", | |
"type": "Subtask Lvl.3" | |
}, { | |
"duration": "26:3:58", | |
"label": "28", | |
"start": "2002-07-21", | |
"type": "Subtask Lvl.3" | |
}, { | |
"duration": "25:19:52", | |
"label": "29", | |
"start": "2002-08-26", | |
"type": "Subtask Lvl.3" | |
}, { | |
"duration": "8:4:52", | |
"label": "29", | |
"start": "2002-10-01", | |
"type": "Subtask Lvl.3" | |
}], | |
"label": "Subtask202", | |
"type": "Subtask" | |
}, { | |
"children": [{ | |
"duration": "19:1:59", | |
"label": "30", | |
"start": "2003-03-04", | |
"type": "Subtask Lvl.3" | |
}, { | |
"duration": "16:12:32", | |
"label": "30", | |
"start": "2003-04-13", | |
"type": "Subtask Lvl.3" | |
}, { | |
"duration": "33:20:21", | |
"label": "31", | |
"start": "2003-05-07", | |
"type": "Subtask Lvl.3" | |
}, { | |
"duration": "3:15:51", | |
"label": "32", | |
"start": "2003-06-25", | |
"type": "Subtask Lvl.3" | |
}, { | |
"duration": "11:14:38", | |
"label": "32", | |
"start": "2003-07-01", | |
"type": "Subtask Lvl.3" | |
}, { | |
"duration": "23:9:38", | |
"label": "32", | |
"start": "2003-07-18", | |
"type": "Subtask Lvl.3" | |
}], | |
"label": "Subtask203", | |
"type": "Subtask" | |
}, { | |
"children": [{ | |
"duration": "41:11:59", | |
"label": "33", | |
"start": "2004-02-02", | |
"type": "Subtask Lvl.3" | |
}, { | |
"duration": "0:21:27", | |
"label": "33", | |
"start": "2004-04-13", | |
"type": "Subtask Lvl.3" | |
}, { | |
"duration": "38:3:56", | |
"label": "34", | |
"start": "2004-04-14", | |
"type": "Subtask Lvl.3" | |
}, { | |
"duration": "14:23:21", | |
"label": "35", | |
"start": "2004-06-09", | |
"type": "Subtask Lvl.3" | |
}, { | |
"duration": "13:1:28", | |
"label": "35", | |
"start": "2004-07-01", | |
"type": "Subtask Lvl.3" | |
}, { | |
"duration": "16:22:43", | |
"label": "35", | |
"start": "2004-07-20", | |
"type": "Subtask Lvl.3" | |
}, { | |
"duration": "3:9:9", | |
"label": "35", | |
"start": "2004-08-12", | |
"type": "Subtask Lvl.3" | |
}], | |
"label": "Subtask204", | |
"type": "Subtask" | |
}, { | |
"children": [{ | |
"duration": "31:5:2", | |
"label": "36", | |
"start": "2005-02-25", | |
"type": "Subtask Lvl.3" | |
}, { | |
"duration": "17:21:34", | |
"label": "36", | |
"start": "2005-04-13", | |
"type": "Subtask Lvl.3" | |
}, { | |
"duration": "45:1:19", | |
"label": "37", | |
"start": "2005-05-02", | |
"type": "Subtask Lvl.3" | |
}, { | |
"duration": "26:4:43", | |
"label": "38", | |
"start": "2005-06-19", | |
"type": "Subtask Lvl.3" | |
}, { | |
"duration": "26:13:51", | |
"label": "38", | |
"start": "2005-07-19", | |
"type": "Subtask Lvl.3" | |
}], | |
"label": "Subtask205", | |
"type": "Subtask" | |
}, { | |
"children": [{ | |
"duration": "37:21:14", | |
"label": "39", | |
"start": "2006-02-18", | |
"type": "Subtask Lvl.3" | |
}, { | |
"duration": "15:0:33", | |
"label": "39", | |
"start": "2006-04-13", | |
"type": "Subtask Lvl.3" | |
}, { | |
"duration": "48:0:41", | |
"label": "40", | |
"start": "2006-04-29", | |
"type": "Subtask Lvl.3" | |
}, { | |
"duration": "25:4:6", | |
"label": "41", | |
"start": "2006-06-19", | |
"type": "Subtask Lvl.3" | |
}, { | |
"duration": "28:20:2", | |
"label": "41", | |
"start": "2006-07-18", | |
"type": "Subtask Lvl.3" | |
}], | |
"label": "Subtask206", | |
"type": "Subtask" | |
}, { | |
"children": [{ | |
"duration": "41:9:17", | |
"label": "42", | |
"start": "2007-02-16", | |
"type": "Subtask Lvl.3" | |
}, { | |
"duration": "10:15:54", | |
"label": "42", | |
"start": "2007-04-15", | |
"type": "Subtask Lvl.3" | |
}, { | |
"duration": "48:0:41", | |
"label": "43", | |
"start": "2007-04-26", | |
"type": "Subtask Lvl.3" | |
}, { | |
"duration": "27:16:10", | |
"label": "44", | |
"start": "2007-06-17", | |
"type": "Subtask Lvl.3" | |
}, { | |
"duration": "25:14:53", | |
"label": "44", | |
"start": "2007-07-18", | |
"type": "Subtask Lvl.3" | |
}], | |
"label": "Subtask207", | |
"type": "Subtask" | |
}, { | |
"children": [{ | |
"duration": "46:15:54", | |
"label": "45", | |
"start": "2008-02-09", | |
"type": "Subtask Lvl.3" | |
}, { | |
"duration": "4:12:43", | |
"label": "45", | |
"start": "2008-04-13", | |
"type": "Subtask Lvl.3" | |
}, { | |
"duration": "48:0:41", | |
"label": "46", | |
"start": "2008-04-17", | |
"type": "Subtask Lvl.3" | |
}, { | |
"duration": "35:16:24", | |
"label": "47", | |
"start": "2008-06-08", | |
"type": "Subtask Lvl.3" | |
}, { | |
"duration": "16:21:37", | |
"label": "47", | |
"start": "2008-07-18", | |
"type": "Subtask Lvl.3" | |
}], | |
"label": "Subtask208", | |
"type": "Subtask Lvl.2" | |
}], | |
"label": "Subtask 1", | |
"type": "Subtask Lvl.1" | |
}], | |
"label": "P1", | |
"type": "Proj" | |
}; | |
new GanttChart(jsonData, "YYYY-MM-DD"); | |
}); | |
</script> | |
<div id="chart-mini"></div> | |
</p> | |
</section> | |
</div> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment