mermaid/src/diagrams/gantt/ganttRenderer.js

409 lines
11 KiB
JavaScript
Raw Normal View History

2017-09-10 19:41:34 +08:00
import moment from 'moment'
2017-09-10 21:23:04 +08:00
import { parser } from './parser/gantt'
import ganttDb from './ganttDb'
2017-09-10 19:41:34 +08:00
import d3 from '../../d3'
2017-09-10 21:23:04 +08:00
parser.yy = ganttDb
2017-09-14 20:59:58 +08:00
let daysInChart
const conf = {
2017-04-11 22:14:25 +08:00
titleTopMargin: 25,
barHeight: 20,
barGap: 4,
topPadding: 50,
rightPadding: 75,
leftPadding: 75,
gridLineStartPadding: 35,
fontSize: 11,
fontFamily: '"Open-Sans", "sans-serif"'
}
2017-09-10 21:23:04 +08:00
export const setConf = function (cnf) {
2017-09-14 20:59:58 +08:00
const keys = Object.keys(cnf)
2017-04-11 22:14:25 +08:00
keys.forEach(function (key) {
conf[key] = cnf[key]
})
}
2017-09-14 20:59:58 +08:00
let w
2017-09-10 21:23:04 +08:00
export const draw = function (text, id) {
parser.yy.clear()
parser.parse(text)
2017-09-14 20:59:58 +08:00
const elem = document.getElementById(id)
2017-04-11 22:14:25 +08:00
w = elem.parentElement.offsetWidth
2015-06-16 10:40:08 +02:00
2017-04-11 22:14:25 +08:00
if (typeof w === 'undefined') {
w = 1200
}
2017-04-11 22:14:25 +08:00
if (typeof conf.useWidth !== 'undefined') {
w = conf.useWidth
}
2017-09-14 20:59:58 +08:00
const taskArray = parser.yy.getTasks()
2017-04-13 20:16:38 +08:00
// Set height based on number of tasks
2017-09-14 20:59:58 +08:00
const h = taskArray.length * (conf.barHeight + conf.barGap) + 2 * conf.topPadding
2017-04-11 22:14:25 +08:00
elem.setAttribute('height', '100%')
2017-04-13 20:16:38 +08:00
// Set viewBox
2017-04-11 22:14:25 +08:00
elem.setAttribute('viewBox', '0 0 ' + w + ' ' + h)
2017-09-14 20:59:58 +08:00
const svg = d3.select('#' + id)
2017-04-11 22:14:25 +08:00
2017-09-14 20:59:58 +08:00
const startDate = d3.min(taskArray, function (d) {
2017-04-11 22:14:25 +08:00
return d.startTime
})
2017-09-14 20:59:58 +08:00
const endDate = d3.max(taskArray, function (d) {
2017-04-11 22:14:25 +08:00
return d.endTime
})
2017-04-13 20:16:38 +08:00
// Set timescale
2017-09-14 20:59:58 +08:00
const timeScale = d3.time.scale()
2017-04-13 20:16:38 +08:00
.domain([d3.min(taskArray, function (d) {
return d.startTime
}),
2018-03-06 14:37:27 +08:00
d3.max(taskArray, function (d) {
return d.endTime
})])
2017-04-13 20:16:38 +08:00
.rangeRound([0, w - conf.leftPadding - conf.rightPadding])
2015-02-20 16:22:37 +01:00
2017-09-14 20:59:58 +08:00
let categories = []
2015-02-08 20:07:15 +01:00
2017-04-11 22:14:25 +08:00
daysInChart = moment.duration(endDate - startDate).asDays()
2015-02-08 20:07:15 +01:00
2017-09-14 20:59:58 +08:00
for (let i = 0; i < taskArray.length; i++) {
2017-04-11 22:14:25 +08:00
categories.push(taskArray[i].type)
}
2015-02-08 20:07:15 +01:00
2017-09-14 20:59:58 +08:00
const catsUnfiltered = categories // for vert labels
2017-04-11 22:14:25 +08:00
categories = checkUnique(categories)
makeGant(taskArray, w, h)
if (typeof conf.useWidth !== 'undefined') {
elem.setAttribute('width', w)
}
2015-02-08 20:07:15 +01:00
2017-04-11 22:14:25 +08:00
svg.append('text')
2017-09-10 21:23:04 +08:00
.text(parser.yy.getTitle())
2017-04-13 20:16:38 +08:00
.attr('x', w / 2)
.attr('y', conf.titleTopMargin)
.attr('class', 'titleText')
2015-02-08 20:07:15 +01:00
2017-04-11 22:14:25 +08:00
function makeGant (tasks, pageWidth, pageHeight) {
2017-09-14 20:59:58 +08:00
const barHeight = conf.barHeight
const gap = barHeight + conf.barGap
const topPadding = conf.topPadding
const leftPadding = conf.leftPadding
2015-02-08 20:07:15 +01:00
2017-09-14 20:59:58 +08:00
const colorScale = d3.scale.linear()
2017-04-13 20:16:38 +08:00
.domain([0, categories.length])
.range(['#00B9FA', '#F95002'])
.interpolate(d3.interpolateHcl)
2015-02-20 16:22:37 +01:00
2017-04-11 22:14:25 +08:00
makeGrid(leftPadding, topPadding, pageWidth, pageHeight)
drawRects(tasks, gap, topPadding, leftPadding, barHeight, colorScale, pageWidth, pageHeight)
vertLabels(gap, topPadding, leftPadding, barHeight, colorScale)
drawToday(leftPadding, topPadding, pageWidth, pageHeight)
}
2015-02-20 16:22:37 +01:00
2017-04-13 22:21:09 +08:00
function drawRects (theArray, theGap, theTopPad, theSidePad, theBarHeight, theColorScale, w, h) {
2017-04-11 22:14:25 +08:00
svg.append('g')
2017-04-13 20:16:38 +08:00
.selectAll('rect')
.data(theArray)
.enter()
.append('rect')
.attr('x', 0)
.attr('y', function (d, i) {
return i * theGap + theTopPad - 2
})
.attr('width', function () {
return w - conf.rightPadding / 2
})
.attr('height', theGap)
2017-04-13 22:21:09 +08:00
.attr('class', function (d) {
2017-09-14 20:59:58 +08:00
for (let i = 0; i < categories.length; i++) {
2017-04-13 20:16:38 +08:00
if (d.type === categories[i]) {
return 'section section' + (i % conf.numberSectionStyles)
}
}
return 'section section0'
})
2015-02-20 16:22:37 +01:00
2017-09-14 20:59:58 +08:00
const rectangles = svg.append('g')
2017-04-13 20:16:38 +08:00
.selectAll('rect')
.data(theArray)
.enter()
2015-02-20 16:22:37 +01:00
2017-04-11 22:14:25 +08:00
rectangles.append('rect')
2017-04-13 20:16:38 +08:00
.attr('rx', 3)
.attr('ry', 3)
.attr('x', function (d) {
return timeScale(d.startTime) + theSidePad
})
.attr('y', function (d, i) {
return i * theGap + theTopPad
})
.attr('width', function (d) {
return (timeScale(d.endTime) - timeScale(d.startTime))
})
.attr('height', theBarHeight)
.attr('class', function (d) {
2017-09-14 20:59:58 +08:00
const res = 'task '
2017-04-13 20:16:38 +08:00
2017-09-14 20:59:58 +08:00
let secNum = 0
for (let i = 0; i < categories.length; i++) {
2017-04-13 20:16:38 +08:00
if (d.type === categories[i]) {
secNum = (i % conf.numberSectionStyles)
}
}
if (d.active) {
if (d.crit) {
return res + ' activeCrit' + secNum
} else {
return res + ' active' + secNum
}
}
if (d.done) {
if (d.crit) {
return res + ' doneCrit' + secNum
} else {
return res + ' done' + secNum
}
}
if (d.crit) {
return res + ' crit' + secNum
}
return res + ' task' + secNum
})
2015-02-20 16:22:37 +01:00
2017-04-11 22:14:25 +08:00
rectangles.append('text')
2017-04-13 20:16:38 +08:00
.text(function (d) {
return d.task
})
.attr('font-size', conf.fontSize)
.attr('x', function (d) {
2017-09-14 20:59:58 +08:00
const startX = timeScale(d.startTime)
const endX = timeScale(d.endTime)
const textWidth = this.getBBox().width
2017-04-13 20:16:38 +08:00
// Check id text width > width of rectangle
if (textWidth > (endX - startX)) {
if (endX + textWidth + 1.5 * conf.leftPadding > w) {
return startX + theSidePad - 5
} else {
return endX + theSidePad + 5
}
} else {
return (endX - startX) / 2 + startX + theSidePad
}
})
.attr('y', function (d, i) {
return i * theGap + (conf.barHeight / 2) + (conf.fontSize / 2 - 2) + theTopPad
})
.attr('text-height', theBarHeight)
.attr('class', function (d) {
2017-09-14 20:59:58 +08:00
const startX = timeScale(d.startTime)
const endX = timeScale(d.endTime)
const textWidth = this.getBBox().width
let secNum = 0
for (let i = 0; i < categories.length; i++) {
2017-04-13 20:16:38 +08:00
if (d.type === categories[i]) {
secNum = (i % conf.numberSectionStyles)
}
}
2017-09-14 20:59:58 +08:00
let taskType = ''
2017-04-13 20:16:38 +08:00
if (d.active) {
if (d.crit) {
taskType = 'activeCritText' + secNum
} else {
taskType = 'activeText' + secNum
}
}
if (d.done) {
if (d.crit) {
taskType = taskType + ' doneCritText' + secNum
} else {
taskType = taskType + ' doneText' + secNum
}
} else {
if (d.crit) {
taskType = taskType + ' critText' + secNum
}
}
// Check id text width > width of rectangle
if (textWidth > (endX - startX)) {
if (endX + textWidth + 1.5 * conf.leftPadding > w) {
return 'taskTextOutsideLeft taskTextOutside' + secNum + ' ' + taskType
} else {
return 'taskTextOutsideRight taskTextOutside' + secNum + ' ' + taskType
}
} else {
return 'taskText taskText' + secNum + ' ' + taskType
}
})
2017-04-11 22:14:25 +08:00
}
function makeGrid (theSidePad, theTopPad, w, h) {
2017-09-14 20:59:58 +08:00
const pre = [
2017-04-11 22:14:25 +08:00
['.%L', function (d) {
return d.getMilliseconds()
}],
[':%S', function (d) {
return d.getSeconds()
}],
2017-04-13 20:16:38 +08:00
// Within a hour
2017-04-11 22:14:25 +08:00
['h1 %I:%M', function (d) {
return d.getMinutes()
}]]
2017-09-14 20:59:58 +08:00
const post = [
2017-04-11 22:14:25 +08:00
['%Y', function () {
return true
}]]
2017-09-14 20:59:58 +08:00
let mid = [
2017-04-13 20:16:38 +08:00
// Within a day
2017-04-11 22:14:25 +08:00
['%I:%M', function (d) {
return d.getHours()
}],
2017-04-13 20:16:38 +08:00
// Day within a week (not monday)
2017-04-11 22:14:25 +08:00
['%a %d', function (d) {
2017-04-13 20:16:38 +08:00
return d.getDay() && d.getDate() !== 1
2017-04-11 22:14:25 +08:00
}],
2017-04-13 20:16:38 +08:00
// within a month
2017-04-11 22:14:25 +08:00
['%b %d', function (d) {
2017-04-13 20:16:38 +08:00
return d.getDate() !== 1
2017-04-11 22:14:25 +08:00
}],
2017-04-13 20:16:38 +08:00
// Month
2017-04-11 22:14:25 +08:00
['%B', function (d) {
return d.getMonth()
}]
]
2017-09-14 20:59:58 +08:00
let formatter
2017-04-11 22:14:25 +08:00
if (typeof conf.axisFormatter !== 'undefined') {
mid = []
conf.axisFormatter.forEach(function (item) {
2017-09-14 20:59:58 +08:00
const n = []
2017-04-11 22:14:25 +08:00
n[0] = item[0]
n[1] = item[1]
mid.push(n)
})
}
formatter = pre.concat(mid).concat(post)
2017-09-14 20:59:58 +08:00
let xAxis = d3.svg.axis()
2017-04-13 20:16:38 +08:00
.scale(timeScale)
.orient('bottom')
.tickSize(-h + theTopPad + conf.gridLineStartPadding, 0, 0)
.tickFormat(d3.time.format.multi(formatter))
2015-02-20 16:22:37 +01:00
2017-04-11 22:14:25 +08:00
if (daysInChart > 7 && daysInChart < 230) {
xAxis = xAxis.ticks(d3.time.monday.range)
}
2017-04-11 22:14:25 +08:00
svg.append('g')
2017-04-13 20:16:38 +08:00
.attr('class', 'grid')
.attr('transform', 'translate(' + theSidePad + ', ' + (h - 50) + ')')
.call(xAxis)
.selectAll('text')
.style('text-anchor', 'middle')
.attr('fill', '#000')
.attr('stroke', 'none')
.attr('font-size', 10)
.attr('dy', '1em')
2017-04-11 22:14:25 +08:00
}
2015-02-20 16:22:37 +01:00
2017-04-11 22:14:25 +08:00
function vertLabels (theGap, theTopPad) {
2017-09-14 20:59:58 +08:00
const numOccurances = []
let prevGap = 0
2015-02-20 16:22:37 +01:00
2017-09-14 20:59:58 +08:00
for (let i = 0; i < categories.length; i++) {
2017-04-11 22:14:25 +08:00
numOccurances[i] = [categories[i], getCount(categories[i], catsUnfiltered)]
}
2015-02-20 16:22:37 +01:00
2017-04-11 22:14:25 +08:00
svg.append('g') // without doing this, impossible to put grid lines behind text
2017-04-13 20:16:38 +08:00
.selectAll('text')
.data(numOccurances)
.enter()
.append('text')
.text(function (d) {
return d[0]
})
.attr('x', 10)
.attr('y', function (d, i) {
if (i > 0) {
2017-09-14 20:59:58 +08:00
for (let j = 0; j < i; j++) {
2017-04-13 20:16:38 +08:00
prevGap += numOccurances[i - 1][1]
return d[1] * theGap / 2 + prevGap * theGap + theTopPad
}
} else {
return d[1] * theGap / 2 + theTopPad
}
})
.attr('class', function (d) {
2017-09-14 20:59:58 +08:00
for (let i = 0; i < categories.length; i++) {
2017-04-13 20:16:38 +08:00
if (d[0] === categories[i]) {
return 'sectionTitle sectionTitle' + (i % conf.numberSectionStyles)
}
}
return 'sectionTitle'
})
2017-04-11 22:14:25 +08:00
}
2015-02-08 20:07:15 +01:00
2017-04-11 22:14:25 +08:00
function drawToday (theSidePad, theTopPad, w, h) {
2017-09-14 20:59:58 +08:00
const todayG = svg.append('g')
2017-04-13 20:16:38 +08:00
.attr('class', 'today')
2015-02-25 00:07:13 +01:00
2017-09-14 20:59:58 +08:00
const today = new Date()
2015-02-25 00:07:13 +01:00
2017-04-11 22:14:25 +08:00
todayG.append('line')
2017-04-13 20:16:38 +08:00
.attr('x1', timeScale(today) + theSidePad)
.attr('x2', timeScale(today) + theSidePad)
.attr('y1', conf.titleTopMargin)
.attr('y2', h - conf.titleTopMargin)
.attr('class', 'today')
2017-04-11 22:14:25 +08:00
}
2017-04-13 20:16:38 +08:00
// from this stackexchange question: http://stackoverflow.com/questions/1890203/unique-for-arrays-in-javascript
2017-04-11 22:14:25 +08:00
function checkUnique (arr) {
2017-09-14 20:59:58 +08:00
const hash = {}
const result = []
for (let i = 0, l = arr.length; i < l; ++i) {
2017-04-11 22:14:25 +08:00
if (!hash.hasOwnProperty(arr[i])) { // it works with objects! in FF, at least
hash[arr[i]] = true
result.push(arr[i])
}
2015-02-25 00:07:13 +01:00
}
2017-04-11 22:14:25 +08:00
return result
}
2017-04-13 20:16:38 +08:00
// from this stackexchange question: http://stackoverflow.com/questions/14227981/count-how-many-strings-in-an-array-have-duplicates-in-the-same-array
2017-04-11 22:14:25 +08:00
function getCounts (arr) {
2017-09-14 20:59:58 +08:00
let i = arr.length // const to loop over
const obj = {} // obj to store results
2017-04-11 22:14:25 +08:00
while (i) {
obj[arr[--i]] = (obj[arr[i]] || 0) + 1 // count occurrences
2015-02-20 16:22:37 +01:00
}
2017-04-11 22:14:25 +08:00
return obj
}
2015-02-08 20:07:15 +01:00
2017-04-13 20:16:38 +08:00
// get specific from everything
2017-04-11 22:14:25 +08:00
function getCount (word, arr) {
return getCounts(arr)[word] || 0
}
}
2017-09-10 22:03:10 +08:00
export default {
setConf,
draw
}