UNPKG

sense-calendar-heatmap

Version:

Qlik Sense Visualization Extension with a diverging color scale. The values are displayed as colored cells per day. Days are arranged into columns by week, then grouped by month and years.

297 lines (251 loc) 8.69 kB
/*global define, d3*/ define( [ 'jquery', 'underscore', 'qvangular', 'text!./swr-calendarview.ng.html', 'text!./swr-calendarview.css', 'text!./colorbrewer.css', './../../js/calendar', // no return value './../../external/d3/d3.min' ], function ( $, _, qvangular, ngTemplate, cssContent, cssColorBrewer, calendar ) { 'use strict'; qvangular.directive( 'swrCalendarview', [function () { $( "<style>" ).html( cssContent ).appendTo( "head" ); $( "<style>" ).html( cssColorBrewer ).appendTo( "head" ); /** * Returns the minimum year value. * @param data * @returns {Number} */ //function getMinYear ( data ) { // var s = d3.values( data ) // .sort( function ( a, b ) { return d3.ascending( a.Date, b.Date ); } ) // [0]; // return (s) ? parseInt( s.Date.substr( 0, 4 ) ) : -1; //} /** * Returns the maximum year value. * @param data * @returns {Number} */ //function getMaxYear ( data ) { // var s = d3.values( data ) // .sort( function ( a, b ) { return d3.descending( a.Date, b.Date ); } ) // [0]; // return (s) ? parseInt( s.Date.substr( 0, 4 ) ) : -1; //} /** * Return the absolute minimum value. * @param data * @returns {Number} */ function getMinValue ( data ) { var s = d3.values( data ) .sort( function ( a, b ) { return d3.ascending( a.Measure, b.Measure ); } ) [0]; return (s) ? parseFloat( s.Measure ) : null; } /** * Return the absolute maximum value. * @param data * @returns {Number} */ function getMaxValue ( data ) { var s = d3.values( data ) .sort( function ( a, b ) { return d3.descending( a.Measure, b.Measure ); } ) [0]; return (s) ? parseFloat( s.Measure ) : null; } /** * Returns a range of the years to display * @param data * @returns {Array} */ function getYearsRange ( data ) { var years = []; d3.nest() .key( function ( d ) { return d.Date.substr( 0, 4 ); } ) .sortKeys( d3.ascending ) .entries( data ) .forEach( function ( v ) { years.push( parseInt( v.key ) ); } ); return years; } /** * Render the D3 Calendar View * @param objectId {string} * @param cvData {object} */ function renderChart ( objectId, cvData, width ) { if ( !cvData || cvData.length === 0 ) { return; } $( '#chart_' + objectId ).empty(); // Not used anymore because we fetch the getYearsRange ... //var minYear = getMinYear( cvData ); //var maxYear = getMaxYear( cvData ); /*jshint -W016, -W052*/ var w = width - 25, pw = 14, z = ~~((w - pw * 2) / 53), ph = z >> 1, h = z * 7; /*jshint +W016, +W052*/ var yearsRange = getYearsRange( cvData ); var vis = d3.select( '#chart_' + objectId ) .selectAll( "svg" ) //.data( [1990, 1994] ) //Todo: exclude empty years by default or add at least an option for doing so //.data( d3.range( minYear, (maxYear + 1) ) ) .data( yearsRange ) .enter().append( "svg:svg" ) .attr( "width", w ) .attr( "height", h + ph * 2 ) .attr( "class", "RdYlGn" ) .append( "svg:g" ) .attr( "transform", "translate(" + pw + "," + ph + ")" ); vis.append( "svg:text" ) .attr( "transform", "translate(-6," + h / 2 + ")rotate(-90)" ) .attr( "text-anchor", "middle" ) .text( function ( d ) { return d; } ); vis.selectAll( "rect.D3CV_day" ) .data( calendar.dates ) .enter().append( "svg:rect" ) .attr( "x", function ( d ) { return d.week * z; } ) .attr( "y", function ( d ) { return d.day * z; } ) .attr( "class", "D3CV_day" ) .attr( "width", z ) .attr( "height", z ); vis.selectAll( "path.D3CV_month" ) .data( calendar.months ) .enter().append( "svg:path" ) .attr( "class", "D3CV_month" ) .attr( "d", function ( d ) { /*jshint -W014*/ return "M" + (d.firstWeek + 1) * z + "," + d.firstDay * z + "H" + d.firstWeek * z + "V" + 7 * z + "H" + d.lastWeek * z + "V" + (d.lastDay + 1) * z + "H" + (d.lastWeek + 1) * z + "V" + 0 + "H" + (d.firstWeek + 1) * z + "Z"; /*jshint +W014*/ } ); var data = d3.nest() .key( function ( d ) { return d.Date; } ) //.rollup(function (d) { return d[0].Measure; }) .rollup( function ( d ) { return {"Measure": d[0].Measure, "ToolTip": d[0].ToolTip}; } ) .map( cvData ); var minValue = getMinValue( cvData ); var maxValue = getMaxValue( cvData ); // If only a single value is displayed, we have to prevent that min and max value are equal. if ( minValue === maxValue ) { minValue = minValue * 2; maxValue = maxValue * 2; } var divTooltip = d3.select( '#chart_' + objectId ).append( 'div' ) .attr( 'class', 'D3CV_ToolTip' ) .style( 'opacity', 0 ); // disabled as of now //var divAction = d3.select( '#chart_' + objectId ).append( 'div' ) // .attr( 'class', 'D3CV_ToolTip' ) // .style( 'opacity', 0 ); var color = d3.scale.quantize() //.domain([-.05, .05]) .domain( [minValue, maxValue] ) .range( d3.range( 9 ) ) ; var leftOffset = $( '#chart_' + objectId ).offset().left; var topOffset = $( '#chart_' + objectId ).offset().top; vis.selectAll( "rect.D3CV_day" ) //.attr("class", function (d) { return "D3CV_day q" + ((d !== 'undefined' && d.Date !== 'undefined' && data[d.Date].Measure !== 'undefined') ? color(data[d.Date].Measure) : 'xx') + "-9"; }) .attr( "class", function ( d ) { // In case we have selected only a single date, if we have only // a single items, let's not choose one of the colors but gray instead if ( !_.isEmpty( data[d.Date] ) ) { if ( Object.keys( data ).length > 1 ) { return "D3CV_day q" + color( data[d.Date].Measure ) + "-9"; } else { return "D3CV_day D3CV_day_single"; } } else { return "D3CV_day"; } } ) .on( 'click', function ( /*d*/ ) { //console.log('Select Texts in Column' + d.Date); //_t.Data.SelectTextsInColumn( 0, true, d.Date ); //console.log('Check it ...'); //console.log(d); //console.log(d.Date); //console.log(data[d.Date]); //Prototype code for adding a right-click context menu to the chart. //divAction.transition() // .duration(200) // .style('opacity', .99) // .style('display', 'block'); //divAction.html('Select date <br/>Select week<br/>Select month') // .style('left', (d3.event.pageX) - leftOffset + 'px') // .style('top', ((d3.event.pageY) - divTooltip.attr('height') - topOffset + 5) + 'px'); } ) .on( 'mouseover', function ( d ) { //console.clear(); //console.log(d.Date); //console.log(data[d.Date]); divTooltip.transition() .duration( 200 ) .style( 'opacity', 0.99 ) .style( 'display', 'block' ); divTooltip.html( 'Date: ' + d.Date + '<br/>' + (!_.isEmpty( data[d.Date] ) ? data[d.Date].ToolTip : '') ) .style( 'left', (d3.event.pageX) - leftOffset + 'px' ) .style( 'top', ((d3.event.pageY) - divTooltip.attr( 'height' ) - topOffset + 5) + 'px' ); } ) .on( 'mouseout', function ( /* d */ ) { divTooltip.transition() .duration( 200 ) .style( 'opacity', 0 ) .style( 'display', 'none' ); } ) // Do not display the title anymore since we have the tooltip //.append("svg:title") // .text(function (d) { return d.Date + ": " + (data[d.Date]); }) ; } return { restrict: 'EA', scope: { objectId: '=', cvData: '=' }, template: ngTemplate, link: function ( $scope/*, $element , $attrs*/ ) { $scope.$watchCollection( 'cvData', function ( /*newVal, oldVal*/ ) { //console.log( 'swr-calendarview:newVal', newVal ); $scope.render(); } ); $scope.$watch( function () { return [$( '#chart_' + $scope.objectId ).width(), $( '#chart_' + $scope.objectId ).height()].join( 'x' ); }, function ( newVal, oldVal ) { if ( newVal !== oldVal ) { console.log( 'new size', newVal ); $scope.render(); } } ); $scope.render = function () { //console.info( 'render Chart', $scope.objectId ); //console.log( '-- data', $scope.cvData ); renderChart( $scope.objectId, $scope.cvData, $( '#chart_' + $scope.objectId ).width() ); }; } }; }] ); } );