Skip to content

Instantly share code, notes, and snippets.

@EdNutting
Created August 9, 2019 10:15
Show Gist options
  • Save EdNutting/72707725ddaf6cad1a6cb7432f4352a3 to your computer and use it in GitHub Desktop.
Save EdNutting/72707725ddaf6cad1a6cb7432f4352a3 to your computer and use it in GitHub Desktop.
A very simple, nice-looking Gantt chart generator using JS/HTML/SVG. Easily configured using JS objects. Just open locally in a browser.
<!--
MIT License
Copyright (c) 2019 Ed Nutting
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
-->
<!DOCTYPE html>
<html>
<head>
<script>
var defaultTextColor = "#333";
var defaultFontSize = 20;
var defaultFont = "Verdana";
var extraWidth = 170; // Increase/decreases empty space on right side of the chart
var svgBgColor = "#fcfcfc";
var milestoneColor = "#03b71b";
var milestoneLineColor = "#008c12";
var verticalPadding = 10;
var milestonePadding = 5;
var milestoneWidth = (2 * defaultFontSize);
var milestoneLineWidth = 3;
var titleParams = { x: 25
, y: 40
, size: defaultFontSize + 5
, color: defaultTextColor
, text: "Example Chart"
};
var bodyElem = null;
var svgElem = null;
var svgNS = null;
var svgTitleElem = null;
var svgItemsHeadingElem = null;
var itemsHeading = "Categories";
var timeHeadings = [ "'19", "'20", "", "", "", "", "", "", "", "", "", ""
, "'21", "", "", ""
, "'22", "'23", "'24"
];
var timeSubheadings = [ "Dec", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov"
, "Q1", "Q2", "Q3", "Q4"
, "3", "4", "5"
];
var items = [ { name: "Project 1"
, milestones: ["Mar", "Jun", null]
}
, { name: "Project 2"
, milestones: ["Jan", "Feb", "Apr", "Jun", "Jul", null]
}
, { name: "Project 3"
, milestones: ["Q1", "Q4", "3", "4", "5"]
}
, { name: "Project 4"
, milestones: ["Dec", "Jan", null]
}
, { name: "Project 5"
, milestones: ["Jan", "Apr", "Sep", "Q2"]
}
, { name: "Project 6"
, milestones: ["Apr", "Sep", "Q1", "Q3", "4"]
}
, { name: "Project 7"
, milestones: ["Dec", "Apr", "May", "Q2", "4"]
}
, { name: "Project 8"
, milestones: ["Apr", "Jun", "Sep", "Q1", "Q3"]
}
, { name: "Project 9"
, milestones: ["Q3", "3", "4", "5"]
}
, { name: "Project 10"
, milestones: [null, "Apr", "Nov", "Q2", "3", "4"]
}
, { name: "Project 11"
, milestones: ["Dec", "Jan", "Feb", "Jun", "Aug", "Oct", "Q1", "Q4", "3", "4", "5"]
}
];
function getTitleYBottom() {
return titleParams.y + titleParams.size + verticalPadding;
}
function getTitleXRight() {
var titleBBox = svgTitleElem.getBBox();
return titleParams.x + titleBBox.width + verticalPadding;
}
function getItemsHeadingYBottom() {
return getTitleYBottom() + defaultFontSize + verticalPadding;
}
function getItemsHeadingXRight() {
var itemsHeadingBBox = svgItemsHeadingElem.getBBox();
return titleParams.x + itemsHeadingBBox.width + verticalPadding;
}
function getItemsLabelXRight() {
var headingXRight = getItemsHeadingXRight();
var baseX = titleParams.x;
var x = headingXRight;
for (var item of items) {
var bbox = item.elem.getBBox();
var newX = baseX + bbox.width + verticalPadding;
x = x > newX ? x : newX;
}
return x;
}
function getGridWidth() {
return timeSubheadings.length * (milestoneWidth + (2 * milestonePadding));
}
function getGridHeight() {
return items.length * (defaultFontSize + (2 * verticalPadding)) + defaultFontSize + verticalPadding;
}
function toPxString(val) {
return val.toString() + "px";
}
function onBodyLoad() {
if (timeHeadings.length != timeSubheadings.length) {
alert("Time headings and time sub-headings not the same length!");
}
else {
bodyElem = document.getElementsByTagName("body")[0];
initSVG();
configureSVG();
}
}
function initSVG() {
svgElem = document.createElementNS("http://www.w3.org/2000/svg", "svg");
svgNS = svgElem.namespaceURI;
svgElem.setAttribute("width", toPxString(getGridWidth() + extraWidth));
svgElem.setAttribute("height", toPxString(getGridHeight() + getTitleYBottom() + defaultFontSize + verticalPadding));
svgElem.style.border = "3px solid #333";
svgElem.style.borderRadius = "10px";
bodyElem.appendChild(svgElem);
}
function createSVGElem(tag) {
return document.createElementNS(svgNS, tag);
}
function createSVGText(x, y, color, size, text) {
var elem = createSVGElem("text");
elem.setAttribute("x", x);
elem.setAttribute("y", y);
elem.setAttribute("fill", color);
elem.setAttribute("font-size", size);
elem.setAttribute("font-family", defaultFont);
elem.innerHTML = text;
return elem;
}
function createSVGLine(x1, y1, x2, y2, width, color) {
var elem = createSVGElem("line");
elem.setAttribute("x1", x1);
elem.setAttribute("y1", y1);
elem.setAttribute("x2", x2);
elem.setAttribute("y2", y2);
elem.setAttribute("style", "stroke:" + color + "; stroke-width: " + width);
return elem;
}
function createSVGCircle(x, y, r, color) {
var elem = createSVGElem("circle");
elem.setAttribute("cx", x);
elem.setAttribute("cy", y);
elem.setAttribute("r", r);
elem.setAttribute("fill", color);
return elem;
}
function configureSVG() {
configureSVGBg();
configureSVGTitle();
// Note: Order matters
createItemsHeading();
createItemLabels();
createGrid();
createMilestoneLines();
createMilestones();
createKey();
}
function configureSVGBg() {
var elem = createSVGElem("rect");
elem.setAttribute("width", "100%");
elem.setAttribute("height", "100%");
elem.setAttribute("fill", svgBgColor);
svgElem.appendChild(elem);
}
function configureSVGTitle() {
svgTitleElem = createSVGText(
titleParams.x,
titleParams.y,
titleParams.color,
titleParams.size,
titleParams.text);
svgElem.appendChild(svgTitleElem);
}
function createItemsHeading() {
svgItemsHeadingElem = createSVGText(
titleParams.x,
getTitleYBottom(),
defaultTextColor,
defaultFontSize,
itemsHeading);
svgElem.appendChild(svgItemsHeadingElem);
}
function createItemLabels() {
var y = getItemsHeadingYBottom() + verticalPadding + defaultFontSize + verticalPadding;
for (var item of items) {
elem = createSVGText(
titleParams.x,
y,
defaultTextColor,
defaultFontSize,
item.name);
item.elem = elem;
svgElem.appendChild(elem);
y += defaultFontSize + (2 * verticalPadding);
}
}
function createGrid() {
var startX = getItemsLabelXRight();
var startY = getTitleYBottom();
var width = getGridWidth();
var height = getGridHeight() + defaultFontSize;
var lineWidth = 2;
var lineColor = "#333";
svgElem.appendChild(createSVGLine(startX, startY - defaultFontSize, startX, startY + height, lineWidth, lineColor));
svgElem.appendChild(createSVGLine(titleParams.x, startY + verticalPadding + defaultFontSize + verticalPadding
, startX + width, startY + verticalPadding + defaultFontSize + verticalPadding
, lineWidth, lineColor));
var x = startX;
var first = true;
for (var heading of timeHeadings) {
if (heading !== "") {
svgElem.appendChild(createSVGText(x + 10, startY, defaultTextColor, defaultFontSize, heading));
if (!first) {
svgElem.appendChild(createSVGLine(x, startY + verticalPadding, x, startY + height, lineWidth, lineColor));
}
}
x += milestoneWidth + (2 * milestonePadding);
first = false;
}
startY += defaultFontSize + verticalPadding;
x = startX;
for (var subheading of timeSubheadings) {
svgElem.appendChild(createSVGText(x + milestonePadding, startY, defaultTextColor, defaultFontSize, subheading));
x += milestoneWidth + (2 * milestonePadding);
}
}
function getMilestoneIndex(milestone) {
var idx = timeSubheadings.indexOf(milestone);
if (idx == -1) {
alert("Specified milestone does not match a subheading: " + milestone);
idx = -2;
}
return idx;
}
function createMilestones() {
var startX = getItemsLabelXRight() + (milestoneWidth / 2) + milestonePadding;
var y = getTitleYBottom() + verticalPadding + defaultFontSize + (2 * verticalPadding) + (defaultFontSize / 2);
for (var item of items) {
for (var milestone of item.milestones) {
if (milestone !== null) {
var milestoneIdx = getMilestoneIndex(milestone);
var x = startX + milestoneIdx * (milestoneWidth + (2 * milestonePadding));
svgElem.appendChild(createSVGCircle(x, y, milestoneWidth * 1/3, milestoneColor))
}
}
y += defaultFontSize + (2 * verticalPadding) + 0.5;
}
}
function createMilestoneLines() {
var startX = getItemsLabelXRight() + (milestoneWidth / 2) + milestonePadding;
var y = getTitleYBottom() + verticalPadding + defaultFontSize + (2 * verticalPadding) + (defaultFontSize / 2);
var width = getGridWidth();
for (var item of items) {
if (item.milestones.length > 0) {
var milestone0Idx = null;
if (item.milestones[0] == null) {
milestone0Idx = getMilestoneIndex(item.milestones[1]);
var xL = startX - milestoneWidth/2;
var xR = startX + milestone0Idx * (milestoneWidth + (2 * milestonePadding));
var lineElem = createSVGLine(xL, y, xR, y, milestoneLineWidth, milestoneLineColor);
lineElem.setAttribute("stroke-dasharray", "10,10");
svgElem.appendChild(lineElem);
}
else {
milestone0Idx = getMilestoneIndex(item.milestones[0]);
}
var xL = startX + milestone0Idx * (milestoneWidth + (2 * milestonePadding));
var xR = xL;
if (item.milestones[item.milestones.length-1] == null) {
var milestoneL1Idx = getMilestoneIndex(item.milestones[item.milestones.length - 2]);
xR = startX + (milestoneL1Idx * (milestoneWidth + (2 * milestonePadding)));
}
else {
xR = xL + width - (milestone0Idx * (milestoneWidth + (2 * milestonePadding))) - milestonePadding - (milestoneWidth / 2);
}
svgElem.appendChild(createSVGLine(xL, y, xR, y, milestoneLineWidth, milestoneLineColor));
}
y += defaultFontSize + (2 * verticalPadding) + 0.5;
}
}
function createKey() {
var startX = getTitleXRight() + (5 * verticalPadding);
var titleBBox = svgTitleElem.getBBox();
var topY = titleParams.y;
var centerY = topY + ((titleBBox.height - titleParams.y) / 2);
svgElem.appendChild(createSVGCircle(
startX + verticalPadding,
centerY,
milestoneWidth * 1/3,
milestoneColor
));
var milestoneText = createSVGText(startX + (2 * verticalPadding) + (milestoneWidth * 1/3), centerY + (defaultFontSize/2) - 3, defaultTextColor, defaultFontSize, "Milestone");
svgElem.appendChild(milestoneText);
var milestoneTextBBox = milestoneText.getBBox();
startX += (6 * verticalPadding) + (milestoneWidth * 1/3) + milestoneTextBBox.width;
var activityText = createSVGText(startX + (2 * verticalPadding) + (milestoneWidth * 1/3), centerY + (defaultFontSize/2) - 3, defaultTextColor, defaultFontSize, "Ongoing");
svgElem.appendChild(activityText);
var activityTextBBox = activityText.getBBox();
svgElem.appendChild(createSVGLine(
startX + verticalPadding - (milestoneWidth * 1/3),
centerY,
startX + verticalPadding + (milestoneWidth * 1/3),
centerY,
milestoneLineWidth,
milestoneLineColor
));
}
</script>
</head>
<body onload="onBodyLoad()">
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment