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.

Revisions

  1. vasturiano revised this gist Oct 22, 2017. 2 changed files with 1 addition and 2 deletions.
    1 change: 0 additions & 1 deletion .block
    Original file line number Diff line number Diff line change
    @@ -1,3 +1,2 @@
    license: mit
    height: 700
    scrolling: yes
    2 changes: 1 addition & 1 deletion index.html
    Original file line number Diff line number Diff line change
    @@ -1,5 +1,5 @@
    <head>
    <script src="//unpkg.com/timelines-chart@2/dist/timelines-chart.min.js"></script>
    <script src="//unpkg.com/timelines-chart@2"></script>
    <script src="random-data.js"></script>
    </head>

  2. vasturiano revised this gist Sep 19, 2017. 5 changed files with 58 additions and 68 deletions.
    22 changes: 9 additions & 13 deletions index.html
    Original file line number Diff line number Diff line change
    @@ -1,19 +1,15 @@
    <head>
    <script src="//unpkg.com/timelines-chart@^1.0/dist/timelines-chart.min.js"></script>
    <script src="mockup-data.js"></script>

    <script>
    var myData = getMockupData(),
    myPlot = TimelinesChart()
    .width(window.innerWidth)
    .zScaleLabel("My Scale Units");

    document.addEventListener("DOMContentLoaded", function() {
    myPlot(document.getElementById("myPlot"), myData);
    });
    </script>
    <script src="//unpkg.com/timelines-chart@2/dist/timelines-chart.min.js"></script>
    <script src="random-data.js"></script>
    </head>

    <body>
    <div id="myPlot"></div>

    <script>
    TimelinesChart()
    .data(getRandomData(true))
    .zQualitative(true)
    (document.getElementById('myPlot'));
    </script>
    </body>
    55 changes: 0 additions & 55 deletions mockup-data.js
    Original file line number Diff line number Diff line change
    @@ -1,55 +0,0 @@
    function getMockupData() {

    var NGROUPS = 6;
    var MAXLINES = 15;
    var MAXSEGMENTS = 20;
    var MINTIME = new Date(new Date() - 3*365*24*60*60*1000);

    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;
    }
    Binary file modified preview.png
    Loading
    Sorry, something went wrong. Reload?
    Sorry, we cannot display this file.
    Sorry, this file is invalid so it cannot be displayed.
    49 changes: 49 additions & 0 deletions random-data.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,49 @@
    function getRandomData(ordinal = false) {

    const NGROUPS = 6,
    MAXLINES = 15,
    MAXSEGMENTS = 20,
    MAXCATEGORIES = 20,
    MINTIME = new Date(2013,2,21);

    const nCategories = Math.ceil(Math.random()*MAXCATEGORIES),
    categoryLabels = ['A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z'];

    return [...Array(NGROUPS).keys()].map(i => ({
    group: 'group' + (i+1),
    data: getGroupData()
    }));

    //

    function getGroupData() {

    return [...Array(Math.ceil(Math.random()*MAXLINES)).keys()].map(i => ({
    label: 'label' + (i+1),
    data: getSegmentsData()
    }));

    //

    function getSegmentsData() {
    const nSegments = Math.ceil(Math.random()*MAXSEGMENTS),
    segMaxLength = Math.round(((new Date())-MINTIME)/nSegments);
    let runLength = MINTIME;

    return [...Array(nSegments).keys()].map(i => {
    const tDivide = [Math.random(), Math.random()].sort(),
    start = new Date(runLength.getTime() + tDivide[0]*segMaxLength),
    end = new Date(runLength.getTime() + tDivide[1]*segMaxLength);

    runLength = new Date(runLength.getTime() + segMaxLength);

    return {
    timeRange: [start, end],
    val: ordinal ? categoryLabels[Math.ceil(Math.random()*nCategories)] : Math.random()
    //labelVal: is optional - only displayed in the labels
    };
    });

    }
    }
    }
    Binary file modified thumbnail.png
    Loading
    Sorry, something went wrong. Reload?
    Sorry, we cannot display this file.
    Sorry, this file is invalid so it cannot be displayed.
  3. vasturiano revised this gist Apr 20, 2017. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion .block
    Original file line number Diff line number Diff line change
    @@ -1,3 +1,3 @@
    license: gpl-3.0
    license: mit
    height: 700
    scrolling: yes
  4. vasturiano revised this gist Oct 15, 2016. 2 changed files with 4 additions and 3 deletions.
    5 changes: 3 additions & 2 deletions README.md
    Original file line number Diff line number Diff line change
    @@ -1,6 +1,7 @@
    A parallel timelines layout (swimlanes) for representing state of time-series over time. Using the [timelines-chart](https://github.com/vasturiano/timelines-chart) D3 reusable chart.
    A parallel timelines layout (swimlanes) for representing state of time-series over time. Using the [timelines-chart](https://github.com/vasturiano/timelines-chart) D3 component.

    Each timeline segment can be assigned a value on a color scale, either continuous (heatmap mode) or ordinal (for categorical representation).
    Each timeline segment can be assigned a value from a color scale, either continuous (heatmap mode) or ordinal (for categorical representation).
    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 with randomly generated data.

    2 changes: 1 addition & 1 deletion mockup-data.js
    Original file line number Diff line number Diff line change
    @@ -3,7 +3,7 @@ function getMockupData() {
    var NGROUPS = 6;
    var MAXLINES = 15;
    var MAXSEGMENTS = 20;
    var MINTIME = new Date(2013,2,21);
    var MINTIME = new Date(new Date() - 3*365*24*60*60*1000);

    function getGroupData() {

  5. vasturiano revised this gist Oct 15, 2016. 2 changed files with 0 additions and 0 deletions.
    Binary file added preview.png
    Loading
    Sorry, something went wrong. Reload?
    Sorry, we cannot display this file.
    Sorry, this file is invalid so it cannot be displayed.
    Binary file modified thumbnail.png
    Loading
    Sorry, something went wrong. Reload?
    Sorry, we cannot display this file.
    Sorry, this file is invalid so it cannot be displayed.
  6. vasturiano revised this gist Oct 11, 2016. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion mockup-data.js
    Original file line number Diff line number Diff line change
    @@ -3,7 +3,7 @@ function getMockupData() {
    var NGROUPS = 6;
    var MAXLINES = 15;
    var MAXSEGMENTS = 20;
    var MINTIME = new Date(new Date() - 365*24*60*60*1000);
    var MINTIME = new Date(new Date() - 3*365*24*60*60*1000);

    function getGroupData() {

  7. vasturiano revised this gist Oct 11, 2016. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion mockup-data.js
    Original file line number Diff line number Diff line change
    @@ -3,7 +3,7 @@ function getMockupData() {
    var NGROUPS = 6;
    var MAXLINES = 15;
    var MAXSEGMENTS = 20;
    var MINTIME = (new Date()) - 365*24*60*60*1000;
    var MINTIME = new Date(new Date() - 365*24*60*60*1000);

    function getGroupData() {

  8. vasturiano revised this gist Oct 11, 2016. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion mockup-data.js
    Original file line number Diff line number Diff line change
    @@ -3,7 +3,7 @@ function getMockupData() {
    var NGROUPS = 6;
    var MAXLINES = 15;
    var MAXSEGMENTS = 20;
    var MINTIME = new Date(2013,2,21);
    var MINTIME = (new Date()) - 365*24*60*60*1000;

    function getGroupData() {

  9. vasturiano revised this gist Oct 11, 2016. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion README.md
    Original file line number Diff line number Diff line change
    @@ -1,6 +1,6 @@
    A parallel timelines layout (swimlanes) for representing state of time-series over time. Using the [timelines-chart](https://github.com/vasturiano/timelines-chart) D3 component.

    Each timeline segment can be assigned a value on a color scale, either continuous (heatmap mode) or ordinal (for categorical representation).
    Each timeline segment can be assigned a value from a color scale, either continuous (heatmap mode) or ordinal (for categorical representation).
    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 with randomly generated data.
  10. vasturiano revised this gist Oct 11, 2016. 1 changed file with 1 addition and 0 deletions.
    1 change: 1 addition & 0 deletions README.md
    Original file line number Diff line number Diff line change
    @@ -2,5 +2,6 @@ A parallel timelines layout (swimlanes) for representing state of time-series ov

    Each timeline segment can be assigned a value on a color scale, either continuous (heatmap mode) or ordinal (for categorical representation).
    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 with randomly generated data.

  11. vasturiano revised this gist Oct 11, 2016. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion README.md
    Original file line number Diff line number Diff line change
    @@ -1,4 +1,4 @@
    A parallel timelines layout (swimlanes) for representing state of time-series over time. Using the [timelines-chart](https://github.com/vasturiano/timelines-chart) D3 reusable chart.
    A parallel timelines layout (swimlanes) for representing state of time-series over time. Using the [timelines-chart](https://github.com/vasturiano/timelines-chart) D3 component.

    Each timeline segment can be assigned a value on a color scale, either continuous (heatmap mode) or ordinal (for categorical representation).
    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.
  12. vasturiano revised this gist Oct 10, 2016. No changes.
  13. vasturiano revised this gist Oct 10, 2016. 1 changed file with 1 addition and 3 deletions.
    4 changes: 1 addition & 3 deletions README.md
    Original file line number Diff line number Diff line change
    @@ -1,6 +1,4 @@
    A parallel timelines layout (swimlanes) for representing state of time-series over time.

    Using the [timelines-chart](https://github.com/vasturiano/timelines-chart) D3 reusable chart.
    A parallel timelines layout (swimlanes) for representing state of time-series over time. Using the [timelines-chart](https://github.com/vasturiano/timelines-chart) D3 reusable chart.

    Each timeline segment can be assigned a value on a color scale, either continuous (heatmap mode) or ordinal (for categorical representation).
    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.
  14. vasturiano revised this gist Oct 10, 2016. 2 changed files with 4 additions and 4 deletions.
    4 changes: 2 additions & 2 deletions README.md
    Original file line number Diff line number Diff line change
    @@ -1,6 +1,6 @@
    A stacked timelines layout for representing state of time-series over time.
    A parallel timelines layout (swimlanes) for representing state of time-series over time.

    Using the [stacked-timelines-chart](https://github.com/vasturiano/stacked-timelines-chart) D3 reusable chart.
    Using the [timelines-chart](https://github.com/vasturiano/timelines-chart) D3 reusable chart.

    Each timeline segment can be assigned a value on a color scale, either continuous (heatmap mode) or ordinal (for categorical representation).
    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.
    4 changes: 2 additions & 2 deletions index.html
    Original file line number Diff line number Diff line change
    @@ -1,10 +1,10 @@
    <head>
    <script src="//unpkg.com/stacked-timelines-chart@^1.0/dist/stacked-timelines-chart.min.js"></script>
    <script src="//unpkg.com/timelines-chart@^1.0/dist/timelines-chart.min.js"></script>
    <script src="mockup-data.js"></script>

    <script>
    var myData = getMockupData(),
    myPlot = StackedTimelinesChart()
    myPlot = TimelinesChart()
    .width(window.innerWidth)
    .zScaleLabel("My Scale Units");

  15. vasturiano revised this gist Oct 10, 2016. 1 changed file with 3 additions and 3 deletions.
    6 changes: 3 additions & 3 deletions index.html
    Original file line number Diff line number Diff line change
    @@ -4,9 +4,9 @@

    <script>
    var myData = getMockupData(),
    myPlot = StackedTimelinesChart()
    .width(window.innerWidth)
    .zScaleLabel("My Scale Units");
    myPlot = StackedTimelinesChart()
    .width(window.innerWidth)
    .zScaleLabel("My Scale Units");

    document.addEventListener("DOMContentLoaded", function() {
    myPlot(document.getElementById("myPlot"), myData);
  16. vasturiano revised this gist Oct 10, 2016. 7 changed files with 14 additions and 1869 deletions.
    7 changes: 6 additions & 1 deletion README.md
    Original file line number Diff line number Diff line change
    @@ -1,3 +1,8 @@
    A stacked timelines layout for representing state of time-series over time in a heatmap fashion.
    A stacked timelines layout for representing state of time-series over time.

    Using the [stacked-timelines-chart](https://github.com/vasturiano/stacked-timelines-chart) D3 reusable chart.

    Each timeline segment can be assigned a value on a color scale, either continuous (heatmap mode) or ordinal (for categorical representation).
    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 with randomly generated data.

    290 changes: 0 additions & 290 deletions d3-utils.js
    Original file line number Diff line number Diff line change
    @@ -1,290 +0,0 @@
    // 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.appendOrdinalColorLegend = function(w, h, scale, label) {

    var legend = this;

    var colorBinWidth = w / scale.domain().length;
    scale.domain().forEach(function(val, index) {

    var colorG = legend.append('g');

    colorG.append("rect")
    .attr("width", colorBinWidth)
    .attr("height", h)
    .attr("x", colorBinWidth*index)
    .attr("y", 0)
    .attr("rx", 0)
    .attr("ry", 0)
    .attr("stroke-width", 0)
    .attr("fill", scale(val));

    colorG.append("text")
    .text(val)
    .attr("x", colorBinWidth*(index+.5))
    .attr("y", h*0.5)
    .style("text-anchor", "middle")
    .style("dominant-baseline", "central")
    .style('fill', tinycolor(scale(val)).isLight()?'#333':'#DDD' )
    .style('font-family', 'Sans-Serif')
    .textFitToBox(colorBinWidth, h*0.8);

    colorG.append('title')
    .text(val + ' ' + label);
    });

    legend.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)
    .attr("fill-opacity", 0)
    .style("pointer-events", 'none');

    return legend;
    };

    d3.selection.prototype.appendLinearColorLegend = function(w, h, scale, label) {

    var gradId = this.addGradient(scale, 0);

    this.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 + ')');

    this.append("text")
    .attr("class", "legendText")
    .text(label)
    .attr("x", w*0.5)
    .attr("y", h*0.5)
    .style("text-anchor", "middle")
    .style("dominant-baseline", "central")
    .style('fill', tinycolor(scale((scale.domain()[scale.domain().length-1] - scale.domain()[0])/2)).isLight()?'#444':'#CCC' )
    .style('font-family', 'Sans-Serif')
    .textFitToBox(w*0.8, h*0.9);

    this.append("text")
    .text(scale.domain()[0])
    .attr("x", w*0.02)
    .attr("y", h*0.5)
    .style("text-anchor", "start")
    .style("dominant-baseline", "central")
    .style('font', h*0.7 + 'px sans-serif')
    .style('fill', tinycolor(scale.range()[0]).isLight()?'#444':'#CCC' )
    .style('font-family', 'Sans-Serif')
    .textFitToBox(w*0.3, h*0.7);

    this.append("text")
    .text(scale.domain()[scale.domain().length-1])
    .attr("x", w*0.98)
    .attr("y", h*0.5)
    .style("text-anchor", "end")
    .style("dominant-baseline", "central")
    .style('fill', tinycolor(scale.range()[scale.range().length-1]).isLight()?'#444':'#CCC' )
    .style('font-family', 'Sans-Serif')
    .textFitToBox(w*0.3, h*0.7);

    return this;
    };

    d3.selection.prototype.appendColorLegend = function(x, y, w, h, scale, label) {
    var legendG = this.append("g")
    .attr("class", "legend");

    legendG.attr("transform", "translate(" + x + "," + y + ")");

    return (scale.copy().domain([1, 2]).range([1, 2])(1.5) === 1)
    ?legendG.appendOrdinalColorLegend(w, h, scale, label)
    :legendG.appendLinearColorLegend(w, h, scale, label);
    };

    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);
    };

    26 changes: 8 additions & 18 deletions index.html
    Original file line number Diff line number Diff line change
    @@ -1,29 +1,19 @@
    <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="//cdnjs.cloudflare.com/ajax/libs/tinycolor/1.3.0/tinycolor.min.js"></script>

    <script src="d3-utils.js"></script>
    <script src="time-overview.js"></script>
    <script src="stacked-heat-map.js"></script>
    <script src="//unpkg.com/stacked-timelines-chart@^1.0/dist/stacked-timelines-chart.min.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");
    var myData = getMockupData(),
    myPlot = StackedTimelinesChart()
    .width(window.innerWidth)
    .zScaleLabel("My Scale Units");

    myPlot($('#myHeatMap'), myData);
    document.addEventListener("DOMContentLoaded", function() {
    myPlot(document.getElementById("myPlot"), myData);
    });
    </script>
    </head>

    <body>
    <div id="myHeatMap"></div>
    <div id="myPlot"></div>
    </body>
    80 changes: 0 additions & 80 deletions stacked-heat-map.css
    Original file line number Diff line number Diff line change
    @@ -1,80 +0,0 @@
    .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;
    }
    1,256 changes: 0 additions & 1,256 deletions stacked-heat-map.js
    Original file line number Diff line number Diff line change
    @@ -1,1256 +0,0 @@
    /* 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(($('<div>').appendTo(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
    + (normVal?" (<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;
    }
    Binary file removed throbber.gif
    Binary file not shown.
    224 changes: 0 additions & 224 deletions time-overview.js
    Original file line number Diff line number Diff line change
    @@ -1,224 +0,0 @@
    /**
    * 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;
    };
    };
  17. vasturiano revised this gist Oct 6, 2016. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion README.md
    Original file line number Diff line number Diff line change
    @@ -1,3 +1,3 @@
    A stacked heatmap timelines layout for representing state of time-series over time.
    A stacked timelines layout for representing state of time-series over time in a heatmap fashion.
    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 with randomly generated data.
  18. vasturiano revised this gist Oct 6, 2016. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion README.md
    Original file line number Diff line number Diff line change
    @@ -1,3 +1,3 @@
    A stacked heatmap layout for representing state of time-series over time.
    A stacked heatmap timelines 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 with randomly generated data.
  19. vasturiano revised this gist Aug 11, 2016. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion .block
    Original file line number Diff line number Diff line change
    @@ -1,3 +1,3 @@
    license: gpl-3.0
    height: 800
    height: 700
    scrolling: yes
  20. vasturiano revised this gist Aug 11, 2016. 1 changed file with 3 additions and 0 deletions.
    3 changes: 3 additions & 0 deletions .block
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,3 @@
    license: gpl-3.0
    height: 800
    scrolling: yes
  21. vasturiano revised this gist Jun 1, 2016. 1 changed file with 4 additions and 4 deletions.
    8 changes: 4 additions & 4 deletions d3-utils.js
    Original file line number Diff line number Diff line change
    @@ -120,7 +120,7 @@ d3.selection.prototype.appendOrdinalColorLegend = function(w, h, scale, label) {
    .attr("x", colorBinWidth*(index+.5))
    .attr("y", h*0.5)
    .style("text-anchor", "middle")
    .style("alignment-baseline", "central")
    .style("dominant-baseline", "central")
    .style('fill', tinycolor(scale(val)).isLight()?'#333':'#DDD' )
    .style('font-family', 'Sans-Serif')
    .textFitToBox(colorBinWidth, h*0.8);
    @@ -165,7 +165,7 @@ d3.selection.prototype.appendLinearColorLegend = function(w, h, scale, label) {
    .attr("x", w*0.5)
    .attr("y", h*0.5)
    .style("text-anchor", "middle")
    .style("alignment-baseline", "central")
    .style("dominant-baseline", "central")
    .style('fill', tinycolor(scale((scale.domain()[scale.domain().length-1] - scale.domain()[0])/2)).isLight()?'#444':'#CCC' )
    .style('font-family', 'Sans-Serif')
    .textFitToBox(w*0.8, h*0.9);
    @@ -175,7 +175,7 @@ d3.selection.prototype.appendLinearColorLegend = function(w, h, scale, label) {
    .attr("x", w*0.02)
    .attr("y", h*0.5)
    .style("text-anchor", "start")
    .style("alignment-baseline", "central")
    .style("dominant-baseline", "central")
    .style('font', h*0.7 + 'px sans-serif')
    .style('fill', tinycolor(scale.range()[0]).isLight()?'#444':'#CCC' )
    .style('font-family', 'Sans-Serif')
    @@ -186,7 +186,7 @@ d3.selection.prototype.appendLinearColorLegend = function(w, h, scale, label) {
    .attr("x", w*0.98)
    .attr("y", h*0.5)
    .style("text-anchor", "end")
    .style("alignment-baseline", "central")
    .style("dominant-baseline", "central")
    .style('fill', tinycolor(scale.range()[scale.range().length-1]).isLight()?'#444':'#CCC' )
    .style('font-family', 'Sans-Serif')
    .textFitToBox(w*0.3, h*0.7);
  22. vasturiano revised this gist May 16, 2016. 2 changed files with 9 additions and 7 deletions.
    15 changes: 8 additions & 7 deletions d3-utils.js
    Original file line number Diff line number Diff line change
    @@ -120,8 +120,8 @@ d3.selection.prototype.appendOrdinalColorLegend = function(w, h, scale, label) {
    .attr("x", colorBinWidth*(index+.5))
    .attr("y", h*0.5)
    .style("text-anchor", "middle")
    .attr('dy','.4em')
    .style('fill', '#CCC' )
    .style("alignment-baseline", "central")
    .style('fill', tinycolor(scale(val)).isLight()?'#333':'#DDD' )
    .style('font-family', 'Sans-Serif')
    .textFitToBox(colorBinWidth, h*0.8);

    @@ -165,7 +165,8 @@ d3.selection.prototype.appendLinearColorLegend = function(w, h, scale, label) {
    .attr("x", w*0.5)
    .attr("y", h*0.5)
    .style("text-anchor", "middle")
    .attr('dy','.35em')
    .style("alignment-baseline", "central")
    .style('fill', tinycolor(scale((scale.domain()[scale.domain().length-1] - scale.domain()[0])/2)).isLight()?'#444':'#CCC' )
    .style('font-family', 'Sans-Serif')
    .textFitToBox(w*0.8, h*0.9);

    @@ -174,9 +175,9 @@ d3.selection.prototype.appendLinearColorLegend = function(w, h, scale, label) {
    .attr("x", w*0.02)
    .attr("y", h*0.5)
    .style("text-anchor", "start")
    .attr('dy','.4em')
    .style("alignment-baseline", "central")
    .style('font', h*0.7 + 'px sans-serif')
    .style('fill', '#CCC' )
    .style('fill', tinycolor(scale.range()[0]).isLight()?'#444':'#CCC' )
    .style('font-family', 'Sans-Serif')
    .textFitToBox(w*0.3, h*0.7);

    @@ -185,8 +186,8 @@ d3.selection.prototype.appendLinearColorLegend = function(w, h, scale, label) {
    .attr("x", w*0.98)
    .attr("y", h*0.5)
    .style("text-anchor", "end")
    .attr('dy','.4em')
    .style('fill', '#CCC' )
    .style("alignment-baseline", "central")
    .style('fill', tinycolor(scale.range()[scale.range().length-1]).isLight()?'#444':'#CCC' )
    .style('font-family', 'Sans-Serif')
    .textFitToBox(w*0.3, h*0.7);

    1 change: 1 addition & 0 deletions index.html
    Original file line number Diff line number Diff line change
    @@ -2,6 +2,7 @@
    <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="//cdnjs.cloudflare.com/ajax/libs/tinycolor/1.3.0/tinycolor.min.js"></script>

    <script src="d3-utils.js"></script>
    <script src="time-overview.js"></script>
  23. vasturiano revised this gist May 16, 2016. 2 changed files with 65 additions and 11 deletions.
    74 changes: 64 additions & 10 deletions d3-utils.js
    Original file line number Diff line number Diff line change
    @@ -96,16 +96,59 @@ d3.selection.prototype.addDropShadow = function() {
    return shadowId;
    };

    d3.selection.prototype.appendColorLegend = function(x, y, w, h, scale, label) {
    d3.selection.prototype.appendOrdinalColorLegend = function(w, h, scale, label) {

    var legend = this;

    var colorBinWidth = w / scale.domain().length;
    scale.domain().forEach(function(val, index) {

    var colorG = legend.append('g');

    colorG.append("rect")
    .attr("width", colorBinWidth)
    .attr("height", h)
    .attr("x", colorBinWidth*index)
    .attr("y", 0)
    .attr("rx", 0)
    .attr("ry", 0)
    .attr("stroke-width", 0)
    .attr("fill", scale(val));

    colorG.append("text")
    .text(val)
    .attr("x", colorBinWidth*(index+.5))
    .attr("y", h*0.5)
    .style("text-anchor", "middle")
    .attr('dy','.4em')
    .style('fill', '#CCC' )
    .style('font-family', 'Sans-Serif')
    .textFitToBox(colorBinWidth, h*0.8);

    colorG.append('title')
    .text(val + ' ' + label);
    });

    var gradId = this.addGradient(scale, 0);
    legend.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)
    .attr("fill-opacity", 0)
    .style("pointer-events", 'none');

    var legendG = this.append("g")
    .attr("class", "legend");
    return legend;
    };

    legendG.attr("transform", "translate(" + x + "," + y + ")");
    d3.selection.prototype.appendLinearColorLegend = function(w, h, scale, label) {

    var gradId = this.addGradient(scale, 0);

    legendG.append("rect")
    this.append("rect")
    .attr("width", w)
    .attr("height", h)
    .attr("x", 0)
    @@ -116,7 +159,7 @@ d3.selection.prototype.appendColorLegend = function(x, y, w, h, scale, label) {
    .attr("stroke-width", 0.5)
    .style("fill", 'url(#' + gradId + ')');

    legendG.append("text")
    this.append("text")
    .attr("class", "legendText")
    .text(label)
    .attr("x", w*0.5)
    @@ -126,7 +169,7 @@ d3.selection.prototype.appendColorLegend = function(x, y, w, h, scale, label) {
    .style('font-family', 'Sans-Serif')
    .textFitToBox(w*0.8, h*0.9);

    legendG.append("text")
    this.append("text")
    .text(scale.domain()[0])
    .attr("x", w*0.02)
    .attr("y", h*0.5)
    @@ -137,7 +180,7 @@ d3.selection.prototype.appendColorLegend = function(x, y, w, h, scale, label) {
    .style('font-family', 'Sans-Serif')
    .textFitToBox(w*0.3, h*0.7);

    legendG.append("text")
    this.append("text")
    .text(scale.domain()[scale.domain().length-1])
    .attr("x", w*0.98)
    .attr("y", h*0.5)
    @@ -147,7 +190,18 @@ d3.selection.prototype.appendColorLegend = function(x, y, w, h, scale, label) {
    .style('font-family', 'Sans-Serif')
    .textFitToBox(w*0.3, h*0.7);

    return legendG;
    return this;
    };

    d3.selection.prototype.appendColorLegend = function(x, y, w, h, scale, label) {
    var legendG = this.append("g")
    .attr("class", "legend");

    legendG.attr("transform", "translate(" + x + "," + y + ")");

    return (scale.copy().domain([1, 2]).range([1, 2])(1.5) === 1)
    ?legendG.appendOrdinalColorLegend(w, h, scale, label)
    :legendG.appendLinearColorLegend(w, h, scale, label);
    };

    d3.selection.prototype.appendSvgThrobber = function(x, y, r, color, duration, angleFull) {
    2 changes: 1 addition & 1 deletion stacked-heat-map.js
    Original file line number Diff line number Diff line change
    @@ -388,7 +388,7 @@ var StackedTimeSeriesHeatMap = function() {
    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>"
    + (normVal?" (<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]);
    });
  24. vasturiano revised this gist May 16, 2016. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion stacked-heat-map.js
    Original file line number Diff line number Diff line change
    @@ -297,7 +297,7 @@ var StackedTimeSeriesHeatMap = function() {
    this
    );

    env.overviewArea.init(env.$elem[0]);
    env.overviewArea.init(($('<div>').appendTo(env.$elem))[0]);

    env.$elem.bind('zoomScent', function(event, zoomX, zoomY) {
    if (!env.overviewArea || !zoomX) return;
  25. vasturiano revised this gist May 10, 2016. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion README.md
    Original file line number Diff line number Diff line change
    @@ -1,3 +1,3 @@
    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.
    Example is populated with randomly generated data.
  26. vasturiano revised this gist May 10, 2016. 9 changed files with 1881 additions and 1 deletion.
    4 changes: 3 additions & 1 deletion README.md
    Original file line number Diff line number Diff line change
    @@ -1 +1,3 @@
    -
    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.
    235 changes: 235 additions & 0 deletions d3-utils.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,235 @@
    // 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);
    };

    28 changes: 28 additions & 0 deletions index.html
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,28 @@
    <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>
    55 changes: 55 additions & 0 deletions mockup-data.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,55 @@
    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;
    }
    80 changes: 80 additions & 0 deletions stacked-heat-map.css
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,80 @@
    .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;
    }
    1,256 changes: 1,256 additions & 0 deletions stacked-heat-map.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,1256 @@
    /* 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;
    }
    Binary file added throbber.gif
    Loading
    Sorry, something went wrong. Reload?
    Sorry, we cannot display this file.
    Sorry, this file is invalid so it cannot be displayed.
    Binary file added thumbnail.png
    Loading
    Sorry, something went wrong. Reload?
    Sorry, we cannot display this file.
    Sorry, this file is invalid so it cannot be displayed.
    224 changes: 224 additions & 0 deletions time-overview.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,224 @@
    /**
    * 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;
    };
    };
  27. vasturiano created this gist May 10, 2016.
    1 change: 1 addition & 0 deletions README.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1 @@
    -