function GanttCanvas(_data, {
width=600,
height=null,
barHeight=20,
key = (d, i) => i,
start = (d, i) => d.start,
end = (d, i) => d.end,
lane = (d, i) => 0,
color = (d, i) => 'black',
label = undefined,
title = undefined,
layout = assignLanes,
}={}) {
height = height || width * 0.4;
const context = DOM.context2d(width, height);
const xDomain = [d3.min(_data, d => start(d)),
d3.max(_data, d => end(d))];
const X = d3.scaleTime()
.domain(xDomain)
.range([0, width])
const data = layout(_data, {start, end, lane, xScale: X, xPadding: 0})
const yDomain = [... new Set(data.map(d => d.rowNo))];
const Y = d3.scaleBand()
.domain(yDomain)
.range([0, barHeight * yDomain.length])
.padding(0.2)
// Our pixel scales to translate between pixel and canvas coordinates.
// To start with this is a 1:1 mapping.
const pixelX = d3.scaleLinear().domain([0, width]).range([0, width])
// pixelY will have the same scale factor as pixelX, but a different offset
// Though in theory we could allow for scaling x and y separately, there isn't a good ux for this.
const pixelY = d3.scaleLinear().domain([0, height]).range([0, height])
// Define the original bounds of our space in data-coordinates.
const extent = [
[X(xDomain[0]), Y.range()[0]],
[X(xDomain[1]), Y.range()[1]]
];
// Create our zoom function.
const zoom = d3.zoom()
.scaleExtent([0.5, 4])
.translateExtent(extent)
.on("zoom", zoomed)
// Attach it to our canvas.
d3.select(context.canvas).call(zoom)
// Helper for getting sizes rather than positions of values in different scales.
// This wont' work the way it was previously defined
const scaleSize = (scale, v, v0=0) => scale(v) - scale(v0)
// Our zoom handler function
function zoomed({transform} ) {
// Calculate the coordinates of the corners of the canvas in **canvas** coordinates.
const bounds ={
x0: (0 - transform.x) / transform.k,
x1: (width - transform.x) / transform.k,
y0: (0 - transform.y) / transform.k,
y1: (height - transform.y) / transform.k
}
// Update pixel scale with new canvas coords.
pixelX.range([bounds.x0, bounds.x1])
pixelY.range([bounds.y0, bounds.y1])
// Compensate for zooming in our y scale to keep heights of boxes the same.
// Y.range([0, height / transform.k])
// Save Context, Apply Transform, And Redraw
context.save();
context.clearRect(0,0,width,height);
context.translate(transform.x, transform.y)
context.scale(transform.k, transform.k)
draw(transform, bounds)
context.restore()
}
function draw(transform, bounds) {
data.forEach((d, i) => drawBar(d, i));
// Draw Grid
drawGrid(transform, bounds)
}
function drawBar(d, i) {
const barWidth = X(end(d, i)) - X(start(d, i)) - scaleSize(pixelX, 2);
context.textBaseline = 'middle';
const fontSize = Math.min(scaleSize(pixelX, 12), Y.bandwidth() * 0.7);
context.font = `${fontSize}px sans-serif`;
// Draw Bar
context.beginPath()
context.fillStyle = color(d,i)
context.roundRect(
X(start(d, i)),
Y(d.rowNo),
// shrink width by a couple pixels. Looks nicer.
barWidth,
Y.bandwidth(),
scaleSize(pixelX, 4))
context.fill()
// Draw Label
context.beginPath()
context.fillStyle = 'white'
context.fillText(label(d, i), X(start(d, i)) + scaleSize(pixelX, 4), Y(d.rowNo) + Y.bandwidth() / 2)
}
function drawGrid(transform, bounds) {
// Get our grid size in canvas space.
const gridSize = {y: scaleSize(Y, 0.1)}
// Set up context for drawing our grid.
context.save()
context.strokeStyle = '#999'
// context.lineWidth = Math.min(1 / transform.k, 1) // Maintain stroke width at a max of 1px.
context.lineWidth = Math.min(scaleSize(pixelX, 1) , 1) // Works the same as the line above.
// x-axis gridlines - DIFFERENT METHOD
const canvasRange = [X.invert(pixelX(0)), X.invert(pixelX(width))]
const formatTime = d3.timeFormat("%-d %b %Y")
// Background for ticks
context.fillStyle = 'white'
context.globalAlpha = 0.8
context.fillRect(0, pixelY(height-20), width, 20)
context.globalAlpha = 1
const ticks = d3.utcTicks(...canvasRange, 5);
ticks.forEach(t => {
context.beginPath();
context.moveTo(X(t), bounds.y0);
context.lineTo(X(t),bounds.y1);
context.stroke()
// Label
context.beginPath()
context.fillStyle = '#777'
context.fillText(formatTime(t), X(t) + 5, pixelY(height - 10))
})
// y-axis gridlines
const lanes = d3.flatRollup(data, v => d3.max(v, d => d.rowNo), d => lane(d));
lanes.forEach(([label, y]) => {
context.beginPath();
context.moveTo(bounds.x0, Y(y) - scaleSize(pixelX, 2));
context.lineTo(bounds.x1, Y(y) - scaleSize(pixelX, 2));
context.stroke()
// Label
context.beginPath()
context.fillStyle = '#777'
context.fillText(label, bounds.x0 + scaleSize(pixelX, 5), Y(y) + Y.bandwidth() / 2)
})
// Restore context
context.restore()
}
zoomed({transform: d3.zoomIdentity})
return context.canvas;
}