mirror of
https://github.com/ClickHouse/ClickHouse.git
synced 2024-11-23 16:12:01 +00:00
494 lines
15 KiB
JavaScript
494 lines
15 KiB
JavaScript
/*
|
|
* d3-gantt.js by @serxa
|
|
* Based on https://github.com/ydb-platform/ydb/blob/stable-22-2/library/cpp/lwtrace/mon/static/js/d3-gantt.js
|
|
*/
|
|
d3.gantt = function() {
|
|
function gantt(input_data) {
|
|
data = input_data;
|
|
|
|
initAxis();
|
|
|
|
// create svg element
|
|
svg = d3.select(selector)
|
|
.append("svg")
|
|
.attr("class", "chart")
|
|
.attr("width", width + margin.left + margin.right)
|
|
.attr("height", height + margin.top + margin.bottom)
|
|
;
|
|
|
|
// create arrowhead marker
|
|
defs = svg.append("defs");
|
|
defs.append("marker")
|
|
.attr("id", "arrow")
|
|
.attr("viewBox", "0 -5 10 10")
|
|
.attr("refX", 5)
|
|
.attr("refY", 0)
|
|
.attr("markerWidth", 4)
|
|
.attr("markerHeight", 4)
|
|
.attr("orient", "auto")
|
|
.append("path")
|
|
.attr("d", "M0,-5L10,0L0,5")
|
|
.attr("class","arrowHead")
|
|
;
|
|
|
|
zoom = d3.zoom()
|
|
.scaleExtent([0.1, 1000])
|
|
//.translateExtent([0, 0], [1000,0])
|
|
.on("zoom", function() {
|
|
if (tipShown != null) {
|
|
tip.hide(tipShown);
|
|
tipShown = null;
|
|
}
|
|
var tr = d3.event.transform;
|
|
xZoomed = tr.rescaleX(x);
|
|
svg.select("g.x.axis").call(xAxis.scale(xZoomed));
|
|
|
|
var dy = d3.event.sourceEvent.screenY - zoom.startScreenY;
|
|
var newScrollTop = documentBodyScrollTop() - dy;
|
|
window.scrollTo(documentBodyScrollLeft(), newScrollTop);
|
|
documentBodyScrollTop(newScrollTop);
|
|
zoom.startScreenY = d3.event.sourceEvent.screenY;
|
|
|
|
zoomContainer1.attr("transform", "translate(" + tr.x + ",0) scale(" + tr.k + ",1)");
|
|
zoomContainer2.attr("transform", "translate(" + tr.x + ",0) scale(" + tr.k + ",1)");
|
|
})
|
|
.on("start", function() {
|
|
zoom.startScreenY = d3.event.sourceEvent.screenY;
|
|
})
|
|
.on("end", function() {
|
|
})
|
|
;
|
|
|
|
svgChartContainer = svg.append('g')
|
|
.attr("transform", "translate(" + margin.left + ", " + margin.top + ")")
|
|
;
|
|
svgChart = svgChartContainer.append("svg")
|
|
.attr("top", 0)
|
|
.attr("left", 0)
|
|
.attr("width", width)
|
|
.attr("height", height)
|
|
.attr("viewBox", "0 0 " + width + " " + height)
|
|
;
|
|
|
|
zoomContainer1 = svgChart.append("g");
|
|
|
|
zoomPanel = svgChart.append("rect")
|
|
.attr("class", "zoom-panel")
|
|
.attr("width", width)
|
|
.attr("height", height)
|
|
.call(zoom)
|
|
;
|
|
|
|
zoomContainer2 = svgChart.append("g");
|
|
bandsSvg = zoomContainer2.append("g");
|
|
|
|
// tooltips for bands
|
|
var maxTipHeight = 130;
|
|
const tipDirection = d => y(d.band) - maxTipHeight < documentBodyScrollTop()? 's': 'n';
|
|
tip = d3.tip()
|
|
.attr("class", "d3-tip")
|
|
.offset(function(d) {
|
|
// compute x to return tip in chart region
|
|
var t0 = (d.t1 + d.t2) / 2;
|
|
var t1 = Math.min(Math.max(t0, xZoomed.invert(0)), xZoomed.invert(width));
|
|
var dir = tipDirection(d);
|
|
return [dir === 'n'? -10 : 10, xZoomed(t1) - xZoomed(t0)];
|
|
})
|
|
.direction(tipDirection)
|
|
.html(d => "<pre>" + d.text + "</pre>")
|
|
;
|
|
|
|
bandsSvg.call(tip);
|
|
|
|
render();
|
|
|
|
// container for non-zoomable elements
|
|
fixedContainer = svg.append("g")
|
|
.attr("transform", "translate(" + margin.left + ", " + margin.top + ")")
|
|
;
|
|
|
|
// create x axis
|
|
fixedContainer.append("g")
|
|
.attr("class", "x axis")
|
|
.attr("transform", "translate(0, " + (height - margin.top - margin.bottom) + ")")
|
|
.transition()
|
|
.call(xAxis)
|
|
;
|
|
|
|
// ruler should be drawn above x axis and under y axis
|
|
ruler = fixedContainer.append("g")
|
|
.attr("id", "ruler")
|
|
.attr("transform", "translate(0, 0)")
|
|
;
|
|
|
|
// create y axis
|
|
fixedContainer.append("g")
|
|
.attr("class", "y axis")
|
|
.transition()
|
|
.call(yAxis)
|
|
;
|
|
|
|
// make y axis ticks draggable
|
|
var ytickdrag = d3.drag()
|
|
.on("drag", function(d) {
|
|
var ypos = d3.event.y - margin.top;
|
|
var index = Math.floor((ypos / y.step()));
|
|
index = Math.min(Math.max(index, 0), this.initDomain.length - 1);
|
|
if (index != this.curIndex) {
|
|
var newDomain = [];
|
|
for (var i = 0; i < this.initDomain.length; ++i) {
|
|
newDomain.push(this.initDomain[i]);
|
|
}
|
|
newDomain.splice(this.initIndex, 1);
|
|
newDomain.splice(index, 0, this.initDomain[this.initIndex]);
|
|
|
|
this.curIndex = index;
|
|
this.curDomain = newDomain;
|
|
y.domain(newDomain);
|
|
|
|
// rearange y scale and axis
|
|
svg.select("g.y.axis").transition().call(yAxis);
|
|
|
|
// rearange other stuff
|
|
render(-1, true);
|
|
}
|
|
})
|
|
.on("start", function(d) {
|
|
var ypos = d3.event.y - margin.top;
|
|
this.initIndex = Math.floor((ypos / y.step()));
|
|
this.initDomain = y.domain();
|
|
})
|
|
.on("end", function(d) {
|
|
svg.select("g.y.axis").call(yAxis);
|
|
})
|
|
;
|
|
svg.selectAll("g.y.axis .tick")
|
|
.call(ytickdrag)
|
|
;
|
|
|
|
// right margin
|
|
var rmargin = fixedContainer.append("g")
|
|
.attr("id", "right-margin")
|
|
.attr("transform", "translate(" + width + ", 0)")
|
|
;
|
|
rmargin.append("rect")
|
|
.attr("x", 0)
|
|
.attr("y", 0)
|
|
.attr("width", 1)
|
|
.attr("height", height - margin.top - margin.bottom)
|
|
;
|
|
|
|
// top margin
|
|
var tmargin = fixedContainer.append("g")
|
|
.attr("id", "top-margin")
|
|
.attr("transform", "translate(0, 0)")
|
|
;
|
|
tmargin.append("rect")
|
|
.attr("x", 0)
|
|
.attr("y", 0)
|
|
.attr("width", width)
|
|
.attr("height", 1)
|
|
;
|
|
|
|
// ruler
|
|
ruler.append("rect")
|
|
.attr("id", "ruler-line")
|
|
.attr("x", 0)
|
|
.attr("y", 0)
|
|
.attr("width", "1")
|
|
.attr("height", height - margin.top - margin.bottom + 8)
|
|
;
|
|
ruler.append("rect")
|
|
.attr("id", "bgrect")
|
|
.attr("x", 0)
|
|
.attr("y", 0)
|
|
.attr("width", 0)
|
|
.attr("height", 0)
|
|
.style("fill", "white")
|
|
;
|
|
ruler.append("text")
|
|
.attr("x", 0)
|
|
.attr("y", height - margin.top - margin.bottom + 16)
|
|
.attr("dy", "0.71em")
|
|
.text("0")
|
|
;
|
|
|
|
svg.on('mousemove', function() {
|
|
positionRuler(d3.event.pageX);
|
|
});
|
|
|
|
// scroll handling
|
|
window.onscroll = function myFunction() {
|
|
documentBodyScrollLeft(window.scrollX);
|
|
documentBodyScrollTop(window.scrollY);
|
|
var scroll = scrollParams();
|
|
svgChartContainer
|
|
.attr("transform", "translate(" + margin.left
|
|
+ ", " + (margin.top + scroll.y1) + ")");
|
|
svgChart
|
|
.attr("viewBox", "0 " + scroll.y1 + " " + width + " " + scroll.h)
|
|
.attr("height", scroll.h);
|
|
tmargin
|
|
.attr("transform", "translate(0," + scroll.y1 + ")");
|
|
fixedContainer.select(".x.axis")
|
|
.attr("transform", "translate(0," + scroll.y2 + ")");
|
|
rmargin.select("rect")
|
|
.attr("y", scroll.y1)
|
|
.attr("height", scroll.h);
|
|
ruler.select("#ruler-line")
|
|
.attr("y", scroll.y1)
|
|
.attr("height", scroll.h);
|
|
|
|
positionRuler();
|
|
}
|
|
|
|
// render axis
|
|
svg.select("g.x.axis").call(xAxis);
|
|
svg.select("g.y.axis").call(yAxis);
|
|
|
|
// update to initiale state
|
|
window.onscroll(0);
|
|
|
|
return gantt;
|
|
}
|
|
|
|
// private:
|
|
|
|
var keyFunction = function(d) {
|
|
return d.t1.toString() + d.t2.toString() + d.band.toString();
|
|
}
|
|
|
|
var bandTransform = function(d) {
|
|
return "translate(" + x(d.t1) + "," + y(d.band) + ")";
|
|
}
|
|
|
|
var render = function(t0, smooth) {
|
|
// Save/restore last t0 value
|
|
if (!arguments.length || t0 == -1) {
|
|
t0 = render.t0;
|
|
}
|
|
render.t0 = t0;
|
|
smooth = smooth || false;
|
|
|
|
// Create rectangles for bands
|
|
bands = bandsSvg.selectAll("rect.bar")
|
|
.data(data, keyFunction);
|
|
bands.exit().remove();
|
|
bands.enter().append("rect")
|
|
.attr("class", "bar")
|
|
.attr("vector-effect", "non-scaling-stroke")
|
|
.style("fill", d => d.color)
|
|
.on('mouseover', function(d) {
|
|
if (tipShown != d) {
|
|
tipShown = d;
|
|
tip.show(d);
|
|
} else {
|
|
tipShown = null;
|
|
tip.hide(d);
|
|
}
|
|
})
|
|
.merge(bands)
|
|
.transition().duration(smooth? 250: 0)
|
|
.attr("y", 0)
|
|
.attr("transform", bandTransform)
|
|
.attr("height", y.bandwidth())
|
|
.attr("width", d => x(d.t2) - x(d.t1))
|
|
;
|
|
|
|
var emptyMarker = bandsSvg.selectAll("text")
|
|
.data(data.length == 0? ["no data to show"]: []);
|
|
emptyMarker.exit().remove();
|
|
emptyMarker.enter().append("text")
|
|
.text(d => d)
|
|
;
|
|
}
|
|
|
|
function initAxis() {
|
|
x = d3.scaleLinear()
|
|
.domain([timeDomainStart, timeDomainEnd])
|
|
.range([0, width])
|
|
//.clamp(true); // doesn't work with zoom/pan
|
|
xZoomed = x;
|
|
y = d3.scaleBand()
|
|
.domain([...data.bands])
|
|
.range([1, height - margin.top - margin.bottom])
|
|
.padding(1/8);
|
|
xAxis = d3.axisBottom()
|
|
.scale(x)
|
|
.tickSize(8)
|
|
.tickPadding(8);
|
|
yAxis = d3.axisLeft()
|
|
.scale(y)
|
|
.tickSize(0);
|
|
}
|
|
|
|
// slow function wrapper
|
|
var documentBodyScrollLeft = function(value) {
|
|
if (!arguments.length) {
|
|
if (documentBodyScrollLeft.value === undefined) {
|
|
documentBodyScrollLeft.value = window.scrollX;
|
|
}
|
|
return documentBodyScrollLeft.value;
|
|
} else {
|
|
documentBodyScrollLeft.value = value;
|
|
}
|
|
}
|
|
|
|
// slow function wrapper
|
|
var documentBodyScrollTop = function(value) {
|
|
if (!arguments.length) {
|
|
if (!documentBodyScrollTop.value === undefined) {
|
|
documentBodyScrollTop.value = window.scrollY;
|
|
}
|
|
return documentBodyScrollTop.value;
|
|
} else {
|
|
documentBodyScrollTop.value = value;
|
|
}
|
|
}
|
|
|
|
var scrollParams = function() {
|
|
var y1 = documentBodyScrollTop();
|
|
var y2 = y1 + view_height;
|
|
y2 = Math.min(y2, height - margin.top - margin.bottom);
|
|
var h = y2 - y1;
|
|
return {
|
|
y1: y1,
|
|
y2: y2,
|
|
h: h
|
|
};
|
|
}
|
|
|
|
var posTextFormat = d3.format(".1f");
|
|
|
|
var positionRuler = function(pageX) {
|
|
if (!arguments.length) {
|
|
pageX = positionRuler.pageX || 0;
|
|
} else {
|
|
positionRuler.pageX = pageX;
|
|
}
|
|
|
|
// x-coordinate
|
|
if (!positionRuler.svgLeft) {
|
|
positionRuler.svgLeft = svg.node().getBoundingClientRect().x;
|
|
}
|
|
|
|
var xpos = pageX - margin.left + 1 - positionRuler.svgLeft;
|
|
var tpos = xZoomed.invert(xpos);
|
|
tpos = Math.min(Math.max(tpos, xZoomed.invert(0)), xZoomed.invert(width));
|
|
ruler.attr("transform", "translate(" + xZoomed(tpos) + ", 0)");
|
|
var posText = posTextFormat(tpos);
|
|
|
|
// scroll-related
|
|
var scroll = scrollParams();
|
|
|
|
var text = ruler.select("text")
|
|
.attr("y", scroll.y2 + 16)
|
|
;
|
|
|
|
// getBBox() is very slow, so compute symbol width once
|
|
var xpadding = 5;
|
|
var ypadding = 5;
|
|
if (!positionRuler.bbox) {
|
|
positionRuler.bbox = text.node().getBBox();
|
|
}
|
|
|
|
text.text(posText);
|
|
var textWidth = 10 * posText.length;
|
|
ruler.select("#bgrect")
|
|
.attr("x", -textWidth/2 - xpadding)
|
|
.attr("y", positionRuler.bbox.y - ypadding + window.scrollY)
|
|
.attr("width", textWidth + (xpadding*2))
|
|
.attr("height", positionRuler.bbox.height + (ypadding*2))
|
|
;
|
|
|
|
render(tpos);
|
|
}
|
|
|
|
// public:
|
|
|
|
gantt.width = function(value) {
|
|
if (!arguments.length)
|
|
return width;
|
|
width = +value;
|
|
return gantt;
|
|
}
|
|
|
|
gantt.height = function(value) {
|
|
if (!arguments.length)
|
|
return height;
|
|
height = +value;
|
|
return gantt;
|
|
}
|
|
|
|
gantt.view_height = function(value) {
|
|
if (!arguments.length)
|
|
return view_height;
|
|
view_height = +value;
|
|
return gantt;
|
|
}
|
|
|
|
gantt.selector = function(value) {
|
|
if (!arguments.length)
|
|
return selector;
|
|
selector = value;
|
|
return gantt;
|
|
}
|
|
|
|
gantt.timeDomain = function(value) {
|
|
if (!arguments.length)
|
|
return [timeDomainStart, timeDomainEnd];
|
|
timeDomainStart = value[0];
|
|
timeDomainEnd = value[1];
|
|
return gantt;
|
|
}
|
|
|
|
gantt.data = function() {
|
|
return data;
|
|
}
|
|
|
|
gantt.destroy = function() {
|
|
tip.destroy();
|
|
d3.select(selector).selectAll("svg").remove();
|
|
}
|
|
|
|
// constructor
|
|
|
|
// Config
|
|
var margin = { top: 0, right: 30, bottom: 20, left: 150 },
|
|
height = document.body.clientHeight - margin.top - margin.bottom - 5,
|
|
width = document.body.clientWidth - margin.right - margin.left - 5,
|
|
view_height = window.innerHeight,
|
|
selector = 'body',
|
|
timeDomainStart = 0,
|
|
timeDomainEnd = 1000,
|
|
scales = {};
|
|
;
|
|
|
|
// View
|
|
var x = null,
|
|
xZoomed = null,
|
|
y = null,
|
|
xAxis = null,
|
|
yAxis = null,
|
|
svg = null,
|
|
defs = null,
|
|
svgChartContainer = null,
|
|
svgChart = null,
|
|
zoomPanel = null,
|
|
zoomContainer1 = null,
|
|
zoomContainer2 = null,
|
|
fixedContainer = null,
|
|
zoom = null,
|
|
bandsSvg = null,
|
|
bands = null,
|
|
tip = null,
|
|
tipShown = null,
|
|
ruler = null
|
|
;
|
|
|
|
// Model
|
|
var data = null;
|
|
|
|
return gantt;
|
|
}
|