Skip to content

Instantly share code, notes, and snippets.

@vasturiano
Last active March 27, 2026 15:01
Show Gist options
  • Select an option

  • Save vasturiano/ded69192b8269a78d2d97e24211e64e0 to your computer and use it in GitHub Desktop.

Select an option

Save vasturiano/ded69192b8269a78d2d97e24211e64e0 to your computer and use it in GitHub Desktop.
Timelines Chart

A stacked heatmap layout for representing state of time-series over time. Time-series can be grouped into logical groups, represented as distinct sections. Explore the data by zooming (drag) or using the timeline brush at the bottom. Example is populated by randomly generated data.

// D3 selections util funcs
d3.selection.prototype.moveToFront = function() {
return this.each(function(){
this.parentNode.appendChild(this);
});
};
d3.selection.prototype.textFitToBox = function(w,h,passes) {
passes = passes||3;
var startSize = parseInt(this.style("font-size").split('px')[0]);
var bbox = this.node().getBBox();
var newSize = Math.floor(startSize*Math.min(w/bbox.width, h/bbox.height));
if (newSize!=startSize) {
this.style('font-size', newSize + 'px');
if(--passes)
this.textFitToBox(w,h,passes);
}
return this;
};
d3.selection.prototype.textAbbreviateToFit = function(maxW) {
function abbreviateText(txt, maxChars) {
return txt.length<=maxChars?txt:(
txt.substring(0, maxChars*2/3)
+ '...'
+ txt.substring(txt.length - maxChars/3, txt.length)
);
}
var origTxt = this.text();
var nChars = Math.round(origTxt.length*maxW/this.node().getBBox().width*1.2); // Start above
while(--nChars && maxW/this.node().getBBox().width<1){
this.text(abbreviateText(origTxt, nChars));
}
return this;
};
// colorScale: d3.scale.linear().domain([0, 1, 2]).range(['red', 'yellow', 'green'])
// angle: 0 (left-right), 90 (down-up), ...
d3.selection.prototype.addGradient = function(colorScale, angle) {
angle = angle||0; // Horizontal
var rad = Math.PI * angle/180;
var gradId = "areaGradient" + Math.round(Math.random()*10000);
var areaGradients = this.append("linearGradient")
.attr("y1", Math.round(100*Math.max(0, Math.sin(rad))) + "%")
.attr("y2", Math.round(100*Math.max(0, -Math.sin(rad))) + "%")
.attr("x1", Math.round(100*Math.max(0, -Math.cos(rad))) + "%")
.attr("x2", Math.round(100*Math.max(0, Math.cos(rad))) + "%")
.attr("id", gradId);
var threshVal = colorScale.domain()[0];
var normVal = colorScale.domain()[colorScale.domain().length-1] - threshVal;
for (var i=0, len=colorScale.domain().length; i<len; i++) {
areaGradients.append("stop")
.attr("offset", (100*(colorScale.domain()[i] - threshVal)/normVal) + "%")
.attr("stop-color", colorScale.range()[i]);
}
// Use with: .attr("fill", 'url(#<gradId>)');
return gradId;
};
d3.selection.prototype.addDropShadow = function() {
var shadowId = "areaGradient" + Math.round(Math.random()*10000);
var filter = this.append('defs').append('filter')
.attr('id', shadowId)
.attr('height', '130%');
filter.append('feGaussianBlur')
.attr('in', 'SourceAlpha')
.attr('stdDeviation', 3);
filter.append('feOffset')
.attr('dx', 2)
.attr('dy', 2)
.attr('result', 'offsetblur');
var feMerge = filter.append('feMerge');
feMerge.append('feMergeNode');
feMerge.append('feMergeNode')
.attr('in', 'SourceGraphic');
// Use with: .attr('filter', 'url(#<shadowId>)'))
return shadowId;
};
d3.selection.prototype.appendColorLegend = function(x, y, w, h, scale, label) {
var gradId = this.addGradient(scale, 0);
var legendG = this.append("g")
.attr("class", "legend");
legendG.attr("transform", "translate(" + x + "," + y + ")");
legendG.append("rect")
.attr("width", w)
.attr("height", h)
.attr("x", 0)
.attr("y", 0)
.attr("rx", 3)
.attr("ry", 3)
.attr("stroke", "black")
.attr("stroke-width", 0.5)
.style("fill", 'url(#' + gradId + ')');
legendG.append("text")
.attr("class", "legendText")
.text(label)
.attr("x", w*0.5)
.attr("y", h*0.5)
.style("text-anchor", "middle")
.attr('dy','.35em')
.style('font-family', 'Sans-Serif')
.textFitToBox(w*0.8, h*0.9);
legendG.append("text")
.text(scale.domain()[0])
.attr("x", w*0.02)
.attr("y", h*0.5)
.style("text-anchor", "start")
.attr('dy','.4em')
.style('font', h*0.7 + 'px sans-serif')
.style('fill', '#CCC' )
.style('font-family', 'Sans-Serif')
.textFitToBox(w*0.3, h*0.7);
legendG.append("text")
.text(scale.domain()[scale.domain().length-1])
.attr("x", w*0.98)
.attr("y", h*0.5)
.style("text-anchor", "end")
.attr('dy','.4em')
.style('fill', '#CCC' )
.style('font-family', 'Sans-Serif')
.textFitToBox(w*0.3, h*0.7);
return legendG;
};
d3.selection.prototype.appendSvgThrobber = function(x, y, r, color, duration, angleFull) {
function genDonutSlice(cx, cy, r, thickness, startAngle, endAngle) {
startAngle = startAngle/180*Math.PI;
endAngle = endAngle/180*Math.PI;
var outerR=r;
var innerR=r-thickness;
p=[
[cx+outerR*Math.cos(startAngle), cy+outerR*Math.sin(startAngle)],
[cx+outerR*Math.cos(endAngle), cy+outerR*Math.sin(endAngle)],
[cx+innerR*Math.cos(endAngle), cy+innerR*Math.sin(endAngle)],
[cx+innerR*Math.cos(startAngle), cy+innerR*Math.sin(startAngle)]
];
angleDiff = endAngle - startAngle;
largeArc = ((angleDiff % (Math.PI * 2)) > Math.PI)?1:0;
path = [];
path.push("M" + p[0].join());
path.push("A" + [outerR,outerR,0,largeArc,1,p[1]].join());
path.push("L" + p[2].join());
path.push("A" + [innerR,innerR,0,largeArc,0,p[3]].join());
path.push("z");
return path.join(" ");
}
r = r||8;
color = color||'darkblue';
duration = duration||0.7;
angleFull = angleFull||120;
var thickness = r/3;
var path = this.append('path')
.attr('d', genDonutSlice(x, y, r, thickness, 0, angleFull))
.attr('fill', color);
path.append('animateTransform')
.attr('attributeName', 'transform')
.attr('attributeType', 'XML')
.attr('type', 'rotate')
.attr('from', '0 ' + x + ' ' + y)
.attr('to', '360 ' + x + ' ' + y)
.attr('begin', '0s')
.attr('dur', duration + 's')
.attr('fill', 'freeze')
.attr('repeatCount', 'indefinite');
return path;
};
d3.selection.prototype.appendImage = function(imgUrl, x, y, maxW, maxH, svgAlign) {
svgAlign = svgAlign || "xMidYMid";
return new function(svgElem, imgUrl, x, y, maxW, maxH, svgAlign) {
this.img = svgElem.append("image")
.attr("xlink:href", imgUrl)
.attr("x", x)
.attr("y", y)
.attr("width", maxW)
.attr("height", maxH)
.attr("preserveAspectRatio", svgAlign + " meet");
this.show = function() {
this.img
.attr("width", maxW)
.attr("height", maxH);
return this;
};
this.hide = function() {
this.img
.attr("width", 0)
.attr("height", 0);
return this;
};
}(this, imgUrl, x, y, maxW, maxH, svgAlign);
};
<head>
<script src="//code.jquery.com/jquery-2.2.3.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/d3-tip/0.6.7/d3-tip.min.js"></script>
<script src="d3-utils.js"></script>
<script src="time-overview.js"></script>
<script src="stacked-heat-map.js"></script>
<script src="mockup-data.js"></script>
<link rel="stylesheet" type="text/css" href="stacked-heat-map.css">
<script>
var myData = getMockupData();
$(function() {
var myPlot = StackedTimeSeriesHeatMap()
.width($(window).width())
.throbberImg('throbber.gif')
.zScaleLabel("My Scale Units");
myPlot($('#myHeatMap'), myData);
});
</script>
</head>
<body>
<div id="myHeatMap"></div>
</body>
function getMockupData() {
var NGROUPS = 6;
var MAXLINES = 15;
var MAXSEGMENTS = 20;
var MINTIME = new Date(2013,2,21);
function getGroupData() {
function getSegmentsData() {
var segData=[];
var nSegments = Math.ceil(Math.random()*MAXSEGMENTS);
var segMaxLength = Math.round(((new Date())-MINTIME)/nSegments);
var runLength = MINTIME;
for (var i=0; i< nSegments; i++) {
var tDivide = [Math.random(), Math.random()].sort();
var start = new Date(runLength.getTime() + tDivide[0]*segMaxLength);
var end = new Date(runLength.getTime() + tDivide[1]*segMaxLength);
runLength = new Date(runLength.getTime() + segMaxLength);
segData.push({
'timeRange': [start, end],
'val': Math.random()
//'labelVal': is optional - only displayed in the labels
});
}
return segData;
}
var grpData = [];
for (var i=0, nLines=Math.ceil(Math.random()*MAXLINES); i<nLines; i++) {
grpData.push({
'label': 'label' + (i+1),
'data': getSegmentsData()
});
}
return grpData;
}
var data = [];
for (var i=0; i< NGROUPS; i++) {
data.push({
'group': 'group' + (i+1),
'data': getGroupData()
});
}
return data;
}
.stacked-heat-map .axises {
shape-rendering: crispedges;
}
.stacked-heat-map .axises line, .stacked-heat-map .axises path {
fill: none;
stroke: #808080;
}
.stacked-heat-map .y-axis line, .stacked-heat-map .y-axis path, .stacked-heat-map .grp-axis line, .stacked-heat-map .grp-axis path {
stroke: none;
}
.stacked-heat-map .y-axis text, .stacked-heat-map .grp-axis text {
fill: #2F4F4F;
}
.stacked-heat-map .x-grid line {
stroke: #D3D3D3;
}
.stacked-heat-map rect.heatmap-group {
fill-opacity: 0.6;
stroke: #808080;
stroke-opacity: 0.2;
cursor: crosshair;
}
.stacked-heat-map rect.heatmap-segment {
stroke: none;
cursor: crosshair;
}
.legend .legendText {
fill: #666;
}
.brusher .axis text {
font: 11px sans-serif;
}
.brusher .axis path {
display: none;
}
.brusher .axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
.brusher .grid-background {
fill: #C0C0C0;
}
.brusher .grid line, .brusher .grid path {
fill: none;
stroke: #fff;
}
.brusher .grid .minor.tick line {
stroke-opacity: .5;
}
.brusher .brush .extent {
stroke: blue;
stroke-opacity: 0.6;
fill: blue;
fill-opacity: 0.3;
shape-rendering: crispEdges;
}
/*
* Cancel selection interaction
*/
.stat-noselect {
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
/* Stacked Time-Series Heat Map SVG Layout
Exposed functions:
.width(<px>)
.leftMargin(<px>)
.rightMargin(<px>)
.topMargin(<px>)
.bottomMargin(<px>)
.maxHeight(<px>)
.throbberImg(<image URI>)
.dataDomain([<min>, <max>])
.dataScale(<d3 scale object>)
.getNLines()
.getTotalNLines()
.zoomX([<start date>, <end date>], <force redraw (boolean). default: true>)
.zoomY([<start row index, end row index], <force redraw (boolean). default: true>)
.zoomYLabels([<(start) {group,label}>, <(end) {group,label}>], <force redraw (boolean). default: true>)
.getVisibleStructure()
.minSegmentDuration(<msecs>)
.zDataLabel(<unit text on tooltips>)
.zScaleLabel(<legend unit text>)
.sort(<label compare function>, <group compare function>)
.sortAlpha(<ascending (boolean)>)
.sortChrono(<ascending (boolean)>)
.replaceData(<new data>, <keep graph structure (boolean). default: false>)
.enableOverview(<boolean>)
.overviewDomain(<new time range for overview: [<start date>, <end date>]>)
.animationsEnabled(<(boolean)>)
.forceThrobber(<force throbber on (boolean>). default: false>)
.axisClickURL(<URL to follow when clicking on Y axises>)
.getSvg()
.onZoom(<callback function for user initiated zoom>)
.refresh()
*/
var StackedTimeSeriesHeatMap = function() {
var env = {
$elem: null,
width : 720, // default width
height : null,
maxHeight : 640, // default maxHeight
overviewHeight : 20, // Height of overview section in bottom
lineMaxHeight : 12,
minLabelFont : 2,
margin : {top: 26, right: 100, bottom: 30, left: 90 },
groupBkgGradient : ['#FAFAFA', '#E0E0E0'],
xScale : d3.time.scale(),
yScale : d3.scale.ordinal(),
grpScale : d3.scale.ordinal(),
valScale : d3.scale.linear()
.domain([0, 0.5, 1])
.range(["red", "yellow", "green"])
.clamp(false),
zDataLabel: "", // Units of z data. Used in the tooltip descriptions
zScaleLabel: "", // Units of valScale. Used in the legend label.
xAxis : d3.svg.axis(),
xGrid : d3.svg.axis(),
yAxis : d3.svg.axis(),
grpAxis : d3.svg.axis(),
svg : null,
graph : null,
overviewArea: null,
graphW : null,
graphH : null,
completeStructData : null,
structData : null,
completeFlatData : null,
flatData : null,
totalNLines : null,
nLines : null,
zoomX : [null, null], // Which time-range to show (null = min/max)
zoomY : [null, null], // Which lines to show (null = min/max) [0 indexed]
minSegmentDuration : 0, // ms
transDuration : 700, // ms for transition duration
throbber: null,
throbberImg: null,
throbberR: 23,
forceThrobber: false, // Force the throbber to stay on
enableOverview: true,
axisClickURL: null,
labelCmpFunction: alphaNumCmp,
grpCmpFunction: alphaNumCmp,
// Events callbacks
onZoom: null // When user zooms in / resets zoom. Returns ([startX, endX], [startY, endY])
};
function chart($elem, data) {
env.$elem = $elem;
env.$elem.addClass('stacked-heat-map')
.css('text-align', 'center');
env.svg = d3.select($elem[0]).append("svg");
initStatic();
drawNewData(data);
return chart;
}
function parseData(rawData) {
env.completeStructData = [];
env.completeFlatData = [];
env.totalNLines = 0;
var dateObjs = rawData.length?rawData[0].data[0].data[0].timeRange[0] instanceof Date:false;
for (var i= 0, ilen=rawData.length; i<ilen; i++) {
var group = rawData[i].group;
env.completeStructData.push({
group: group,
lines: rawData[i].data.map(function(d) { return d.label; })
});
for (var j= 0, jlen=rawData[i].data.length; j<jlen; j++) {
for (var k= 0, klen=rawData[i].data[j].data.length; k<klen; k++) {
env.completeFlatData.push({
group: group,
label: rawData[i].data[j].label,
timeRange: (dateObjs
?rawData[i].data[j].data[k].timeRange
:[new Date(rawData[i].data[j].data[k].timeRange[0]), new Date(rawData[i].data[j].data[k].timeRange[1])]
),
val: rawData[i].data[j].data[k].val,
labelVal: rawData[i].data[j].data[k][rawData[i].data[j].data[k].hasOwnProperty('labelVal')?'labelVal':'val']
});
}
env.totalNLines++;
}
}
}
function initStatic() {
buildDomStructure();
addTooltips();
addZoomSelection();
setEvents();
function buildDomStructure () {
env.yScale.invert = invertOrdinal;
env.grpScale.invert = invertOrdinal;
env.groupGradId = env.svg.addGradient(
d3.scale.linear()
.domain([0, 1])
.range(env.groupBkgGradient),
-90
);
env.graphW = env.width-env.margin.left-env.margin.right;
env.xScale.range([0, env.graphW])
.clamp(true);
env.svg.attr("width", env.width);
var axises = env.svg.append('g');
env.graph = env.svg.append('g')
.attr("transform", "translate(" + env.margin.left + "," + env.margin.top + ")");
axises.attr("class", "axises")
.attr("transform", "translate(" + env.margin.left + "," + env.margin.top + ")");
axises.append("g")
.attr("class", "x-axis")
.style({ font: '12px sans-serif'});
axises.append("g")
.attr("class", "x-grid");
axises.append("g")
.attr("class", "y-axis")
.attr("transform", "translate(" + env.graphW + ", 0)");
axises.append("g")
.attr("class", "grp-axis");
env.xAxis.scale(env.xScale)
.orient("bottom")
.ticks(Math.round(env.graphW*0.011));
// Abbreviated month names
var xAxisFormat = d3.time.format.multi([
[".%L", function(d) { return d.getMilliseconds(); }],
[":%S", function(d) { return d.getSeconds(); }],
["%I:%M", function(d) { return d.getMinutes(); }],
["%I %p", function(d) { return d.getHours(); }],
["%a %d", function(d) { return d.getDay() && d.getDate() != 1; }],
["%b %d", function(d) { return d.getDate() != 1; }],
["%b", function(d) { return d.getMonth(); }],
["%Y", function() { return true; }]
]);
env.xAxis.tickFormat(xAxisFormat);
env.xGrid.scale(env.xScale)
.orient("top")
.tickFormat("")
.ticks(env.xAxis.ticks()[0]);
env.yAxis
.scale(env.yScale)
.orient("right")
.tickSize(0);
env.grpAxis
.scale(env.grpScale)
.orient("left")
.tickSize(0);
env.svg.appendColorLegend(
(env.margin.left + env.graphW*0.05),
2,
env.graphW/3,
env.margin.top*.6,
env.valScale,
env.zScaleLabel
);
if (env.enableOverview) {
addOverviewArea();
}
if (env.throbberImg) {
env.throbber = env.svg.appendImage(
env.throbberImg,
env.margin.left + (env.graphW-env.throbberR)/2,
env.margin.top + 5,
env.throbberR,
env.throbberR,
'xMidYMin'
).hide();
env.throbber.img
.style('opacity', 0.85)
.append('title').text('Loading data...');
}
// Applies to ordinal scales (invert not supported in d3)
function invertOrdinal(val, cmpFunc) {
cmpFunc = cmpFunc || function(a, b) {
return (a>=b);
};
var bias = this.range()[0];
for (var i=0, len=this.range().length; i<len; i++) {
if (cmpFunc(this.range()[i]+bias, val)) {
return this.domain()[i];
}
}
return this.domain()[this.domain().length-1];
}
function addOverviewArea() {
var overviewMargins = { top: 1, right: 20, bottom: 20, left: 20 };
env.overviewArea = new TimeOverview(
{
margins: overviewMargins,
granularityLevels: {
"day": 43200 * 0.5, // 1 tick = 1 day if the total time window is within 0.5 month
"week": 43200 * 5, // 1 tick = 1 week if the total time window is 5 month
"month": (43200 * 12 * 1), // 1 tick = 1 month if the total time window is 1 year
"year": (43200 * 12 * 20)
},
width: env.width*0.8,
height: env.overviewHeight + overviewMargins.top + overviewMargins.bottom,
verticalLabels: false,
format: xAxisFormat
},
function(startTime, endTime) {
env.$elem.trigger('zoom', [[startTime, endTime], null]);
},
this
);
env.overviewArea.init(env.$elem[0]);
env.$elem.bind('zoomScent', function(event, zoomX, zoomY) {
if (!env.overviewArea || !zoomX) return;
// Out of overview bounds
if (zoomX[0]<env.overviewArea.domainRange[0] || zoomX[1]>env.overviewArea.domainRange[1]) {
env.overviewArea.update(
[
new Date(Math.min(zoomX[0], env.overviewArea.domainRange[0])),
new Date(Math.max(zoomX[1], env.overviewArea.domainRange[1]))
],
env.zoomX
);
} else { // Normal case
env.overviewArea.updateSelection(zoomX);
}
/*
var startLine = (zoomY&&zoomY[0]!=null)?zoomY[0]:0;
var endLine = env.nLines?env.nLines+startLine:env.overviewArea.yDomain()[1];
*/
});
}
}
function addTooltips() {
env.groupTooltip = d3.tip()
.direction('w')
.offset([0, 0])
.style({
color: '#eee',
background: "rgba(0,0,140,0.85)",
padding: '5px',
'border-radius': '3px',
font: '14px sans-serif',
'font-weight': 'bold',
'z-index': 4000
})
.html(function(d) {
var leftPush = (d.hasOwnProperty("timeRange")
?env.xScale(d.timeRange[0])
:0
);
var topPush = (d.hasOwnProperty("label")
?env.grpScale(d.group)-env.yScale(d.group+"+&+"+d.label)
:0
);
env.groupTooltip.offset([topPush, -leftPush]);
return d.group;
});
env.svg.call(env.groupTooltip);
env.lineTooltip = d3.tip()
.direction('e')
.offset([0, 0])
.style({
color: '#eee',
background: "rgba(0,0,140,0.85)",
padding: '5px',
'border-radius': '3px',
font: '13px sans-serif',
'font-weight': 'bold',
'z-index': 4000
})
.html(function(d) {
var rightPush = (d.hasOwnProperty("timeRange")?env.xScale.range()[1]-env.xScale(d.timeRange[1]):0);
env.lineTooltip.offset([0, rightPush]);
return d.label;
});
env.svg.call(env.lineTooltip);
env.segmentTooltip = d3.tip()
.direction('s')
.offset([5, 0])
.style({
color: '#eee',
background: "rgba(0,0,140,0.7)",
padding: "5px",
'border-radius': "3px",
font: '11px sans-serif',
'text-align': 'center',
'z-index': 4000
})
.html(function(d) {
var normVal = env.valScale.domain()[env.valScale.domain().length-1] - env.valScale.domain()[0];
var dateFormat = d3.time.format("%Y-%m-%d %H:%M:%S");
return "<strong>" + d.labelVal + " </strong>" + env.zDataLabel
+ " (<strong>" + d3.round((d.val-env.valScale.domain()[0])/normVal*100, 2) + "%</strong>)<br>"
+ "<strong>From: </strong>" + dateFormat(d.timeRange[0]) + "<br>"
+ "<strong>To: </strong>" + dateFormat(d.timeRange[1]);
});
env.svg.call(env.segmentTooltip);
}
function addZoomSelection() {
env.graph.on("mousedown", function() {
if (d3.select(window).on("mousemove.zoomRect")!=null) // Selection already active
return;
var e = this;
if (d3.mouse(e)[0]<0 || d3.mouse(e)[0]>env.graphW || d3.mouse(e)[1]<0 || d3.mouse(e)[1]>env.graphH)
return;
env.disableHover=true;
var rect = env.graph.append("rect")
.style({
stroke: 'blue',
'stroke-opacity': .6,
fill: 'blue',
'fill-opacity': .3
});
var startCoords = d3.mouse(e);
d3.select("body").classed("stat-noselect", true);
d3.select(window)
.on("mousemove.zoomRect", function() {
d3.event.stopPropagation();
var newCoords = [
Math.max(0, Math.min(env.graphW, d3.mouse(e)[0])),
Math.max(0, Math.min(env.graphH, d3.mouse(e)[1]))
];
rect.attr("x", Math.min(startCoords[0], newCoords[0]))
.attr("y", Math.min(startCoords[1], newCoords[1]))
.attr("width", Math.abs(newCoords[0] - startCoords[0]))
.attr("height", Math.abs(newCoords[1] - startCoords[1]));
env.$elem.trigger('zoomScent', [
[startCoords[0], newCoords[0]].sort(d3.ascending).map(env.xScale.invert),
[startCoords[1], newCoords[1]].sort(d3.ascending).map(function(d) {
return env.yScale.domain().indexOf(env.yScale.invert(d))
+ ((env.zoomY && env.zoomY[0])?env.zoomY[0]:0);
})
]
);
})
.on("mouseup.zoomRect", function() {
d3.select(window).on("mousemove.zoomRect", null).on("mouseup.zoomRect", null);
d3.select("body").classed("stat-noselect", false);
rect.remove();
env.disableHover=false;
var endCoords = [
Math.max(0, Math.min(env.graphW, d3.mouse(e)[0])),
Math.max(0, Math.min(env.graphH, d3.mouse(e)[1]))
];
if (startCoords[0]==endCoords[0] && startCoords[1]==endCoords[1])
return;
var newDomainX = [startCoords[0], endCoords[0]].sort(d3.ascending).map(env.xScale.invert);
var newDomainY = [startCoords[1], endCoords[1]].sort(d3.ascending).map(function(d) {
return env.yScale.domain().indexOf(env.yScale.invert(d))
+ ((env.zoomY && env.zoomY[0])?env.zoomY[0]:0);
});
var changeX=((newDomainX[1] - newDomainX[0])>(60*1000)); // Zoom damper
var changeY=(newDomainY[0]!=env.zoomY[0] || newDomainY[1]!=env.zoomY[1]);
if (changeX || changeY) {
env.$elem.trigger('zoom', [
changeX?newDomainX:null,
changeY?newDomainY:null
]);
}
}, true);
d3.event.stopPropagation();
});
env.svg.append('text')
.text("Reset Zoom")
.attr("x", env.margin.left + env.graphW*.99)
.attr("y", env.margin.top *.8)
.style("text-anchor", "end")
.style({
'font-family': 'sans-serif',
fill: "blue",
opacity: .6,
cursor: 'pointer'
})
.textFitToBox(env.graphW *.4, Math.min(13,env.margin.top *.8))
.on("mouseup" , function() {
env.$elem.trigger('resetZoom');
})
.on("mouseover", function(){
d3.select(this).style('opacity', 1);
})
.on("mouseout", function() {
d3.select(this).style('opacity', .6);
});
}
function setEvents() {
env.$elem.bind('zoom', function(event, zoomX, zoomY, redraw) {
redraw = (redraw==null)?true:redraw;
if (!zoomX && !zoomY) return;
if (zoomX) env.zoomX=zoomX;
if (zoomY) env.zoomY=zoomY;
env.$elem.trigger('zoomScent', [zoomX, zoomY]);
if (!redraw) return;
draw();
if (env.onZoom) env.onZoom(env.zoomX, env.zoomY);
});
env.$elem.bind('resetZoom', function() {
var prevZoomX = env.zoomX;
var prevZoomY = env.zoomY || [null, null];
var newZoomX = env.enableOverview
?env.overviewArea.domainRange
:[
d3.min(env.flatData, function(d) { return d.timeRange[0]; }),
d3.max(env.flatData, function(d) { return d.timeRange[1]; })
];
newZoomY = [null, null];
if (prevZoomX[0]<newZoomX[0] || prevZoomX[1]>newZoomX[1]
|| prevZoomY[0]!=newZoomY[0] || prevZoomY[1]!=newZoomX[1]) {
env.zoomX = [
new Date(Math.min(prevZoomX[0],newZoomX[0])),
new Date(Math.max(prevZoomX[1],newZoomX[1]))
];
env.zoomY = newZoomY;
env.$elem.trigger('zoomScent', [env.zoomX, env.zoomY]);
draw();
}
if (env.onZoom) env.onZoom(null, null);
});
}
}
function drawNewData(data, keepGraphStructure) {
keepGraphStructure = (keepGraphStructure==null?false:keepGraphStructure);
var oldStructData = env.completeStructData;
parseData(data);
if (keepGraphStructure) {
env.completeStructData = oldStructData;
} else {
env.zoomX = [
d3.min(env.completeFlatData, function(d) { return d.timeRange[0]; }),
d3.max(env.completeFlatData, function(d) { return d.timeRange[1]; })
];
env.zoomY = [null, null];
if (env.overviewArea) {
env.overviewArea.update(env.zoomX, env.zoomX);
//var yDomain = [0, env.totalNLines];
}
}
draw();
}
function draw() {
applyFilters();
setupHeights();
adjustXScale();
adjustYScale();
adjustGrpScale();
renderAxises();
renderGroups();
if (env.throbber) { env.throbber.show(); }
renderTimelines();
if (env.throbber && !env.forceThrobber) {
env.throbber.hide();
}
function applyFilters() {
// Flat data based on segment length
env.flatData = (env.minSegmentDuration>0
?env.completeFlatData.filter(function(d) {
return (d.timeRange[1]-d.timeRange[0])>=env.minSegmentDuration;
})
:env.completeFlatData
);
// zoomY
if (env.zoomY==null || env.zoomY==[null, null]) {
env.structData = env.completeStructData;
env.nLines=0;
for (var i=0, len=env.structData.length; i<len; i++) {
env.nLines += env.structData[i].lines.length;
}
return;
}
env.structData = [];
var cntDwn = [env.zoomY[0]==null?0:env.zoomY[0]]; // Initial threshold
cntDwn.push(Math.max(0, (env.zoomY[1]==null?env.totalNLines:env.zoomY[1]+1)-cntDwn[0])); // Number of lines
env.nLines = cntDwn[1];
for (var i=0, len=env.completeStructData.length; i<len; i++) {
var validLines = env.completeStructData[i].lines;
if(env.minSegmentDuration>0) { // Use only non-filtered (due to segment length) groups/labels
if (!env.flatData.some(function(d){
return d.group == env.completeStructData[i].group;
})) {
continue; // No data for this group
}
validLines = env.completeStructData[i].lines.filter( function(d) {
return env.flatData.some( function (dd) {
return (dd.group == env.completeStructData[i].group && dd.label == d);
})
});
}
if (cntDwn[0]>=validLines.length) { // Ignore whole group (before start)
cntDwn[0]-=validLines.length;
continue;
}
var groupData = {
group: env.completeStructData[i].group,
lines: null
};
if (validLines.length-cntDwn[0]>=cntDwn[1]) { // Last (or first && last) group (partial)
groupData.lines = validLines.slice(cntDwn[0],cntDwn[1]+cntDwn[0]);
env.structData.push(groupData);
cntDwn[1]=0;
break;
}
if (cntDwn[0]>0) { // First group (partial)
groupData.lines = validLines.slice(cntDwn[0]);
cntDwn[0]=0;
} else { // Middle group (full fit)
groupData.lines = validLines;
}
env.structData.push(groupData);
cntDwn[1]-=groupData.lines.length;
}
env.nLines-=cntDwn[1];
}
function setupHeights() {
env.graphH = d3.min([env.nLines*env.lineMaxHeight, env.maxHeight-env.margin.top-env.margin.bottom]);
env.height = env.graphH + env.margin.top + env.margin.bottom;
env.svg.transition().duration(env.transDuration)
.attr("height", env.height);
}
function adjustXScale() {
env.zoomX[0] = env.zoomX[0] || d3.min(env.flatData, function(d) { return d.timeRange[0]; });
env.zoomX[1] = env.zoomX[1] || d3.max(env.flatData, function(d) { return d.timeRange[1]; });
env.xScale.domain(env.zoomX);
}
function adjustYScale() {
var labels = [];
for (var i= 0, len=env.structData.length; i<len; i++) {
labels = labels.concat(env.structData[i].lines.map(function (d) {
return env.structData[i].group + "+&+" + d
}));
}
env.yScale.domain(labels);
env.yScale.rangePoints([env.graphH/labels.length*0.5, env.graphH*(1-0.5/labels.length)]);
}
function adjustGrpScale() {
env.grpScale.domain(env.structData.map(function(d) { return d.group; }));
var cntLines=0;
env.grpScale.range(env.structData.map(function(d) {
var pos = (cntLines+d.lines.length/2)/env.nLines*env.graphH;
cntLines+=d.lines.length;
return pos;
}));
}
function renderAxises() {
function reduceLabel(label, maxChars) {
return label.length<=maxChars?label:(
label.substring(0, maxChars*2/3)
+ '...'
+ label.substring(label.length - maxChars/3, label.length
));
}
// X
env.svg.select('g.x-axis')
.style({
'stroke-opacity': 0,
'fill-opacity': 0
})
.attr("transform", "translate(0," + env.graphH + ")")
.transition().duration(env.transDuration)
.call(env.xAxis)
.style({
'stroke-opacity': 1,
'fill-opacity': 1
});
/* Angled x axis labels
env.svg.select('g.x-axis').selectAll("text")
.style("text-anchor", "end")
.attr('transform', 'translate(-10, 3) rotate(-60)');
*/
env.xGrid.tickSize(env.graphH);
env.svg.select('g.x-grid')
.attr("transform", "translate(0," + env.graphH + ")")
.transition().duration(env.transDuration)
.call(env.xGrid);
// Y
var fontVerticalMargin = 0.6;
var labelDisplayRatio = Math.ceil(env.nLines*env.minLabelFont/Math.sqrt(2)/env.graphH/fontVerticalMargin);
var tickVals = env.yScale.domain().filter(function(d, i) { return !(i % labelDisplayRatio); });
var fontSize = Math.min(12, env.graphH/tickVals.length*fontVerticalMargin*Math.sqrt(2));
var maxChars = Math.ceil(env.margin.right/(fontSize/Math.sqrt(2)));
env.yAxis.tickValues(tickVals);
env.yAxis.tickFormat(function(d) {
return reduceLabel(d.split('+&+')[1], maxChars);
});
env.svg.select('g.y-axis')
.transition().duration(env.transDuration)
.style({ font: fontSize + 'px sans-serif'})
.call(env.yAxis);
// Grp
var minHeight = d3.min(env.grpScale.range(), function (d,i) {
return i>0?d-env.grpScale.range()[i-1]:d*2;
});
fontSize = Math.min(14, minHeight*fontVerticalMargin*Math.sqrt(2));
maxChars = Math.floor(env.margin.left/(fontSize/Math.sqrt(2)));
env.grpAxis.tickFormat(function(d) {
return reduceLabel(d, maxChars);
});
env.svg.select('g.grp-axis')
.transition().duration(env.transDuration)
.style({ font: fontSize + 'px sans-serif'})
.call(env.grpAxis);
// Make Axises clickable
if (env.axisClickURL) {
env.svg.selectAll('g.y-axis,g.grp-axis').selectAll("text")
.style("cursor", "pointer")
.on("click", function(d){
var segms = d.split('+&+');
var lbl = segms[segms.length-1];
window.open(env.axisClickURL + lbl, '_blank');
})
.append('title')
.text(function(d) {
var segms = d.split('+&+');
var lbl = segms[segms.length-1];
return 'Open ' + lbl + ' on ' + env.axisClickURL;
});
}
}
function renderGroups() {
var groups = env.graph.selectAll('rect.heatmap-group').data(env.structData, function(d) { return d.group});
groups.exit()
.transition().duration(env.transDuration)
.style({
"stroke-opacity": 0,
"fill-opacity": 0
})
.remove();
groups.enter()
.append('rect').attr("class", "heatmap-group")
.attr('width', env.graphW)
.attr('x', 0)
.attr('y', 0)
.attr('height', 0)
.style('fill', 'url(#' + env.groupGradId + ')')
.on('mouseover', env.groupTooltip.show)
.on('mouseout', env.groupTooltip.hide)
.append('title')
.text('click-drag to zoom in');
groups.transition().duration(env.transDuration)
.attr('height', function (d) {
return env.graphH*d.lines.length/env.nLines;
})
.attr('y', function (d) {
return env.grpScale(d.group)-env.graphH*d.lines.length/env.nLines/2;
});
}
function renderTimelines(maxElems) {
if (maxElems<0) maxElems=null;
var hoverEnlargeRatio = .4;
var dataFilter = function(d, i) {
return (maxElems==null || i<maxElems) &&
(env.grpScale.domain().indexOf(d.group)+1 &&
d.timeRange[1]>=env.xScale.domain()[0] &&
d.timeRange[0]<=env.xScale.domain()[1] &&
env.yScale.domain().indexOf(d.group+"+&+"+d.label)+1);
};
env.lineHeight = env.graphH/env.nLines*0.8;
var timelines = env.graph.selectAll('rect.heatmap-segment').data(
env.flatData.filter(dataFilter),
function(d) { return d.group + d.label + d.timeRange[0];}
);
timelines.exit()
.transition().duration(env.transDuration)
.style({
"fill-opacity": 0
})
.remove();
var newSegments = timelines.enter()
.append('rect').attr("class", "heatmap-segment")
.attr('rx', 1)
.attr('ry', 1)
.attr('x', env.graphW/2)
.attr('y', env.graphH/2)
.attr('width', 0)
.attr('height', 0)
.style({
fill: function(d) {
return env.valScale(d.val);
},
'fill-opacity': 0
})
.on('mouseover.groupTooltip', env.groupTooltip.show)
.on('mouseout.groupTooltip', env.groupTooltip.hide)
.on('mouseover.lineTooltip', env.lineTooltip.show)
.on('mouseout.lineTooltip', env.lineTooltip.hide)
.on('mouseover.segmentTooltip', env.segmentTooltip.show)
.on('mouseout.segmentTooltip', env.segmentTooltip.hide);
newSegments
.on("mouseover", function() {
if ('disableHover' in env && env.disableHover)
return;
var hoverEnlarge = env.lineHeight*hoverEnlargeRatio;
d3.select(this)
.moveToFront()
.transition().duration(70)
.attr('x', function (d) {
return env.xScale(d.timeRange[0])-hoverEnlarge/2;
})
.attr('width', function (d) {
return d3.max([1, env.xScale(d.timeRange[1])-env.xScale(d.timeRange[0])])+hoverEnlarge;
})
.attr('y', function (d) {
return env.yScale(d.group+"+&+"+d.label)-(env.lineHeight+hoverEnlarge)/2;
})
.attr('height', env.lineHeight+hoverEnlarge)
.style({
"fill-opacity": 1
});
})
.on("mouseout", function() {
d3.select(this)
.transition().duration(250)
.attr('x', function (d) {
return env.xScale(d.timeRange[0]);
})
.attr('width', function (d) {
return d3.max([1, env.xScale(d.timeRange[1])-env.xScale(d.timeRange[0])]);
})
.attr('y', function (d) {
return env.yScale(d.group+"+&+"+d.label)-env.lineHeight/2;
})
.attr('height', env.lineHeight)
.style({
"fill-opacity": .8
});
});
timelines.transition().duration(env.transDuration)
.attr('x', function (d) {
return env.xScale(d.timeRange[0]);
})
.attr('width', function (d) {
return d3.max([1, env.xScale(d.timeRange[1])-env.xScale(d.timeRange[0])]);
})
.attr('y', function (d) {
return env.yScale(d.group+"+&+"+d.label)-env.lineHeight/2;
})
.attr('height', env.lineHeight)
.style({ 'fill-opacity': .8 });
}
}
function y2Label(y) {
function getIdxLine(grpData, idx) {
return {
'group': grpData.group,
'label': grpData.lines[idx]
};
}
if (y==null) return y;
var cntDwn = y;
for (var i=0, len=env.completeStructData.length; i<len; i++) {
if (env.completeStructData[i].lines.length>cntDwn)
return getIdxLine(env.completeStructData[i], cntDwn);
cntDwn-=env.completeStructData[i].lines.length;
}
// y larger than all lines, return last
return getIdxLine(env.completeStructData[env.completeStructData.length-1], env.completeStructData[env.completeStructData.length-1].lines.length-1);
}
function label2Y(label, useIdxAfterIfNotFound) {
useIdxAfterIfNotFound = useIdxAfterIfNotFound || false;
var subIdxNotFound = useIdxAfterIfNotFound?0:1;
if (label==null) return label;
var idx=0;
for (var i=0, lenI=env.completeStructData.length; i<lenI; i++) {
var grpCmp = env.grpCmpFunction(label.group, env.completeStructData[i].group);
if (grpCmp<0) break;
if (grpCmp==0 && label.group==env.completeStructData[i].group) {
for (var j=0, lenJ=env.completeStructData[i].lines.length; j<lenJ; j++) {
var cmpRes = env.labelCmpFunction(label.label, env.completeStructData[i].lines[j]);
if (cmpRes<0) {
return idx+j-subIdxNotFound;
}
if (cmpRes==0 && label.label==env.completeStructData[i].lines[j]) {
return idx+j;
}
}
return idx+env.completeStructData[i].lines.length-subIdxNotFound;
}
idx+=env.completeStructData[i].lines.length;
}
return idx-subIdxNotFound;
}
function alphaNumCmp(a,b){
var alist = a.split(/(\d+)/),
blist = b.split(/(\d+)/);
(alist.length && alist[alist.length-1] == '') ? alist.pop() : null; // remove the last element if empty
(blist.length && blist[blist.length-1] == '') ? blist.pop() : null; // remove the last element if empty
for (var i = 0, len = Math.max(alist.length, blist.length); i < len;i++){
if (alist.length==i || blist.length==i) { // Out of bounds for one of the sides
return alist.length - blist.length;
}
if (alist[i] != blist[i]){ // find the first non-equal part
if (alist[i].match(/\d/)) // if numeric
{
return (+alist[i])-(+blist[i]); // compare as number
} else {
return (alist[i].toLowerCase() > blist[i].toLowerCase())?1:-1; // compare as string
}
}
}
return 0;
}
// Exposed functions
chart.width = function(_) {
if (!arguments.length) { return env.width }
env.width = _;
return chart;
};
chart.leftMargin = function(_) {
if (!arguments.length) { return env.margin.left }
env.margin.left = _;
return chart;
};
chart.rightMargin = function(_) {
if (!arguments.length) { return env.margin.right }
env.margin.right = _;
return chart;
};
chart.topMargin = function(_) {
if (!arguments.length) { return env.margin.top }
env.margin.top = _;
return chart;
};
chart.bottomMargin = function(_) {
if (!arguments.length) { return env.margin.bottom }
env.margin.bottom = _;
return chart;
};
chart.maxHeight = function(_) {
if (!arguments.length) { return env.maxHeight; }
env.maxHeight = _;
return chart;
};
chart.throbberImg = function(_) {
if (!arguments.length) { return env.throbberImg; }
env.throbberImg = _;
return chart;
};
chart.dataDomain = function(_) {
if (!arguments.length) {
return [env.valScale.domain()[0], env.valScale.domain()[env.valScale.domain.length-1]]
}
var midVal = _[0] + (_[1]-_[0])/2;
env.valScale.domain([_[0], midVal, _[1]]);
return chart;
};
chart.dataScale = function(_) {
if (!arguments.length) { return env.valScale; }
env.valScale = _;
return chart;
};
chart.getNLines = function() {
return env.nLines;
};
chart.getTotalNLines = function() {
return env.totalNLines;
};
chart.zoomX = function(_, redraw) {
if (!arguments.length) { return env.zoomX; }
env.zoomX = _;
if (env.$elem)
env.$elem.trigger('zoom', [_, null, redraw]);
return chart;
};
chart.zoomY = function(_, redraw) {
if (!arguments.length) { return env.zoomY; }
env.zoomY = _;
if (env.$elem)
env.$elem.trigger('zoom', [null, _, redraw]);
return chart;
};
chart.zoomYLabels = function(_, redraw) {
if (!arguments.length) { return [y2Label(env.zoomY[0]), y2Label(env.zoomY[1])]; }
return chart.zoomY([label2Y(_[0], true), label2Y(_[1], false)], redraw);
};
chart.getVisibleStructure = function() {
return env.structData;
};
chart.minSegmentDuration = function (_) {
if (!arguments.length) { return env.minSegmentDuration; }
env.minSegmentDuration = _;
return chart;
};
chart.zDataLabel = function (_) {
if (!arguments.length) { return env.zDataLabel; }
env.zDataLabel = _;
return chart;
};
chart.zScaleLabel = function (_) {
if (!arguments.length) { return env.zScaleLabel; }
env.zScaleLabel = _;
return chart;
};
chart.sort = function(labelCmpFunction, grpCmpFunction) {
if (labelCmpFunction==null) { labelCmpFunction = env.labelCmpFunction }
if (grpCmpFunction==null) { grpCmpFunction = env.grpCmpFunction }
env.labelCmpFunction = labelCmpFunction;
env.grpCmpFunction = grpCmpFunction;
env.completeStructData.sort(function(a, b) {
return grpCmpFunction(a.group, b.group);
});
for (var i=0, len=env.completeStructData.length;i<len;i++) {
env.completeStructData[i].lines.sort(labelCmpFunction);
}
draw();
return chart;
};
chart.sortAlpha = function(asc) {
if (asc==null) { asc=true }
var alphaCmp = function (a, b) { return alphaNumCmp(asc?a:b, asc?b:a); };
chart.sort(alphaCmp, alphaCmp);
return chart;
};
chart.sortChrono = function(asc) {
if (asc==null) { asc=true }
function buildIdx(accessFunction) {
var idx = {};
for (var i= 0, len=env.completeFlatData.length; i<len; i++ ) {
var key = accessFunction(env.completeFlatData[i]);
if (idx.hasOwnProperty(key)) { continue; }
var itmList = env.completeFlatData.filter(function(d) { return key == accessFunction(d); });
idx[key] = [
d3.min(itmList, function(d) { return d.timeRange[0]}),
d3.max(itmList, function(d) { return d.timeRange[1]})
];
}
return idx;
}
var timeCmp = function (a, b) {
var aT = a[1], bT=b[1];
if (!aT || !bT) return null; // One of the two vals is null
if (aT[1].getTime()==bT[1].getTime()) {
if (aT[0].getTime()==bT[0].getTime()) {
return alphaNumCmp(a[0],b[0]); // If first and last is same, use alphaNum
}
return aT[0]-bT[0]; // If last is same, earliest first wins
}
return bT[1]-aT[1]; // latest last wins
};
function getCmpFunction(accessFunction, asc) {
return function(a, b) {
return timeCmp(accessFunction(asc?a:b), accessFunction(asc?b:a));
}
}
var grpIdx = buildIdx(function(d) { return d.group; });
var lblIdx = buildIdx(function(d) { return d.label; });
var grpCmp = getCmpFunction(function(d) { return [d, grpIdx[d] || null]; }, asc);
var lblCmp = getCmpFunction(function(d) { return [d, lblIdx[d] || null]; }, asc);
chart.sort(lblCmp, grpCmp);
return chart;
};
chart.replaceData =function(newData, keepGraphStructure) {
keepGraphStructure = keepGraphStructure || false;
drawNewData(newData, keepGraphStructure);
return chart;
};
// True/False
chart.enableOverview = function(_) {
if (!arguments.length) { return env.enableOverview; }
env.enableOverview = _;
return chart;
};
chart.overviewDomain = function(_) {
if (!env.enableOverview) { return null; }
if (!arguments.length) { return env.overviewArea.domainRange; }
env.overviewArea.update(_, env.overviewArea.currentSelection);
return chart;
};
// True/False
chart.animationsEnabled = function(_) {
if (!arguments.length) { return (env.transDuration !=0); }
env.transDuration = (_?700:0);
return chart;
};
// True/false (true = shows throbber and leaves it on permanently. false = automatic internal management)
chart.forceThrobber = function(_) {
if (!arguments.length) { return env.forceThrobber; }
env.forceThrobber=_;
if (env.forceThrobber && env.throbber) {
env.throbber.show();
}
return chart;
};
chart.axisClickURL = function(_) {
if (!arguments.length) { return env.axisClickURL; }
env.axisClickURL = _;
return chart;
};
chart.getSvg = function() {
return d3.select(env.svg.node().parentNode).html();
};
chart.onZoom = function(_) {
if (!arguments.length) { return env.onZoom; }
env.onZoom = _;
return chart;
};
chart.refresh = function() {
draw();
return chart;
};
return chart;
}
/**
* Based on http://bl.ocks.org/mbostock/6232620
*/
var TimeOverview = function(options, callback, context){
var timeMapper, timeTicker, brusherBucketLevelsMinutes, timeGrid, margins, width, hideIfLessThanSeconds,
height, brush, xAxis, svg, groupOverview, timeUnitGrid, $this, margins, dom, labels, verticalLabels,
format;
$this = this;
margins = options.margins;
brusherBucketLevelsMinutes = options.granularityLevels;
hideIfLessThanSeconds = options.hideIfLessThanSeconds;
verticalLabels = (options.verticalLabels != null) ? options.verticalLabels : true;
format = options.format || d3.time.format("%Y-%m-%d");
this.init = function(domElement, domainRange, currentSelection){
dom = domElement;
if (domainRange && currentSelection){
this.render(domainRange, currentSelection);
}
};
this._afterInteraction = function(){
if (!d3.event.sourceEvent) return;
var extent0, selectionPoints, boundedLeft, boundedRight, selectionPointsRounded, magneticEffect;
extent0 = brush.extent();
boundedLeft = false;
boundedRight = false;
magneticEffect = 10 * 60 * 60 * 1000;
// Magnetic effect
selectionPoints = extent0;
selectionPointsRounded = extent0.map(timeUnitGrid.round);
if (selectionPoints[0].getTime() <= $this.domainRange[0].getTime() + magneticEffect){
selectionPoints[0] = $this.domainRange[0];
boundedLeft = true;
}
if (selectionPoints[1].getTime() >= $this.domainRange[1].getTime() - magneticEffect){
selectionPoints[1] = $this.domainRange[1];
boundedRight = true;
}
if (boundedLeft && !boundedRight){
selectionPoints[1] = selectionPointsRounded[1];
}else if (!boundedLeft && boundedRight){
selectionPoints[0] = selectionPointsRounded[0];
}else if (!boundedLeft && !boundedRight){
selectionPoints[0] = selectionPointsRounded[0];
selectionPoints[1] = selectionPointsRounded[1];
}
if (selectionPoints[0] >= selectionPoints[1]) {
selectionPoints[0] = timeUnitGrid.floor(extent0[0]);
selectionPoints[1] = timeUnitGrid.ceil(extent0[1]);
}
// Apply magnetic feedback
d3.select(this).transition()
.call(brush.extent(selectionPoints));
callback.call(context, selectionPoints[0], selectionPoints[1]);
};
this._duringInteraction = function(){
if (!d3.event.sourceEvent) return;
var extent0, selectionPoints;
extent0 = brush.extent();
// Magnetic effect
selectionPoints = extent0.map(timeUnitGrid.round);
if (selectionPoints[0] >= selectionPoints[1]) {
selectionPoints[0] = timeUnitGrid.floor(extent0[0]);
selectionPoints[1] = timeUnitGrid.ceil(extent0[1]);
}
// Apply magnetic feedback
d3.select(this).transition()
.call(brush.extent(selectionPoints));
};
this.render = function(domainRange, currentSelection){
var timeWindow;
this.domainRange = domainRange;
this.currentSelection = currentSelection;
timeWindow = domainRange[1] - domainRange[0];
if (timeWindow < hideIfLessThanSeconds * 1000){
return false;
}
if (timeWindow < (brusherBucketLevelsMinutes.day * 60 * 1000)){
timeMapper = d3.time.day;
timeTicker = d3.time.days;
timeGrid = d3.time.hours;
timeUnitGrid = d3.time.hour;
}else if (timeWindow < (brusherBucketLevelsMinutes.week * 60 * 1000)){
timeMapper = d3.time.week;
timeTicker = d3.time.weeks;
timeGrid = d3.time.days;
timeUnitGrid = d3.time.day;
}else if (timeWindow < (brusherBucketLevelsMinutes.month * 60 * 1000)){
timeMapper = d3.time.month;
timeTicker = d3.time.months;
timeGrid = d3.time.weeks;
timeUnitGrid = d3.time.week;
}else{
timeMapper = d3.time.year;
timeTicker = d3.time.years;
timeGrid = d3.time.months;
timeUnitGrid = d3.time.month;
}
width = options.width;
height = options.height - margins.top - margins.bottom;
xAxis = d3
.time
.scale
.utc()
.domain(domainRange)
.range([0, width]);
brush = d3.svg.brush()
.x(xAxis)
.extent(currentSelection)
//.on("brush", brushing)
.on("brushend", $this._afterInteraction);
svg = d3.select(dom)
.append("svg")
.attr("class", "brusher")
.attr("width", width + margins.left + margins.right)
.attr("height", height + margins.top + margins.bottom)
.append("g")
.attr("transform", "translate(" + margins.left + "," + margins.top + ")");
svg.append("rect")
.attr("class", "grid-background")
.attr("width", width)
.attr("height", height);
svg.append("g")
.attr("class", "x grid")
.attr("transform", "translate(0," + height + ")")
.call(d3.svg.axis()
.scale(xAxis)
.orient("bottom")
.ticks(timeGrid)
.tickSize(-height)
.tickFormat(""))
.selectAll(".tick")
.classed("minor", function(d) { return d.getHours(); });
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(d3.svg.axis()
.scale(xAxis)
.orient("bottom")
.ticks(timeTicker)
.tickFormat(format)
.tickPadding(0))
.selectAll("text")
.attr("x", 6)
.style("text-anchor", null);
groupOverview = svg.append("g")
.attr("class", "brush")
.call(brush);
groupOverview.selectAll("rect")
.attr("height", height);
labels = svg.selectAll("text")
.style("text-anchor", "end");
if (verticalLabels){
labels
.attr("dx", "-1.2em")
.attr("dy", ".15em")
.attr('transform', 'rotate(-65)');
}
return true;
};
this.update = function(domainRange, currentSelection){
if (this.domainRange == domainRange){
return this.updateSelection(currentSelection);
}else{
d3.select(dom)
.select(".brusher")
.remove();
return this.render(domainRange, currentSelection);
}
};
this.updateSelection = function(currentSelection){
if (this.currentSelection != currentSelection){
groupOverview
.call(brush.extent(currentSelection));
return true;
}
return false;
};
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment