cBioPortal/iViz

View on GitHub
app/scripts/views/components/pieChart/pieChart.js

Summary

Maintainability
F
4 days
Test Coverage
/**
 * @author Hongxin Zhang on 3/10/16.
 */

'use strict';
(function(iViz, d3, dc, _, $, React, ReactDOM, EnhancedFixedDataTableSpecial) {
  // iViz pie chart component. It includes DC pie chart.
  iViz.view.component.PieChart = function(ndx, attributes, opts, cluster) {
    var content = this;
    var v = {};

    v.chart = '';

    v.data = {
      color: $.extend(true, [], iViz.util.getColors()),
      category: ''
    };

    /* HTML options*/
    v.opts = {};

    v.opts = $.extend(true, v.opts, opts);
    v.data = $.extend(true, v.data, attributes);
    v.data.ndx = ndx;

    var labels = {};
    var reactTableData = {};
    reactTableData.attributes = [{
      attr_id: 'name',
      display_name: v.data.display_name,
      datatype: 'STRING',
      column_width: 235
    }, {
      attr_id: 'color',
      display_name: 'Color',
      datatype: 'STRING',
      show: false
    }, {
      attr_id: 'cases',
      display_name: '#',
      datatype: 'NUMBER',
      column_width: 70
    }, {
      attr_id: 'sampleRate',
      display_name: 'Freq',
      datatype: 'PERCENTAGE',
      column_width: 90
    }, {
      attr_id: 'caseIds',
      display_name: 'Cases',
      datatype: 'STRING',
      show: false
    }, {
      attr_id: 'uniqueId',
      display_name: 'uniqueId',
      datatype: 'STRING',
      show: false
    }];
    var currentView = 'pie';
    var updateQtip = false;
    var qtipRendered = false;
    var isFiltered = false;

    /**
     * Only will be initialized at first time. Label name as key, contains color.
     * @type {{}}
     */
    var labelInitData = {};

    var dcGroup_ = '';
    var dcDimension_ = '';

    initDCPieChart();

    content.getChart = function() {
      return v.chart;
    };

    content.changeView = function(vm, toTableView) {
      currentView = toTableView ? 'table' : 'pie';
      var chartDivDom = $('#' + v.opts.chartDivId);
      chartDivDom.css('z-index', 16000);

      // qtip wont be needed in table view
      chartDivDom.qtip('destroy', true);

      if (currentView === 'table') {
        if (qtipRendered) {
          updateReactTable();
        } else {
          updatePieLabels();
        }
        animateTable('#' + v.opts.chartDivId, 'table', function() {
          vm.$dispatch('update-grid');
          $('#' + v.opts.chartDivId).css('z-index', '');
        });
      } else {
        animateTable('#' + v.opts.chartDivId, 'pie', function() {
          vm.$dispatch('update-grid');
          $('#' + v.opts.chartDivId).css('z-index', '1');
        });
        content.initMainDivQtip();
      }
    };

    content.initMainDivQtip = function() {
      $('#' + v.opts.chartDivId).qtip({
        id: v.opts.chartDivId + '-qtip',
        style: {
          classes: 'qtip-light qtip-rounded qtip-shadow forceZindex qtip-max-width iviz-pie-qtip iviz-pie-label-qtip'
        },
        show: {event: 'mouseover', delay: 300, ready: true},
        hide: {fixed: true, delay: 300, event: 'mouseleave'},
        // hide: false,
        position: {my: 'left center', at: 'center right', viewport: $(window)},
        content: '<div id="qtip-' + v.opts.chartDivId + '-content-react">Loading....</div>',
        events: {
          show: function() {
            if (updateQtip) {
              updateQtip = false;
              updatePieLabels();
            }
          },
          render: function() {
            qtipRendered = true;
            updatePieLabels();
          }
        }
      });
    };

    content.updateDataForDownload = function(fileType) {
      if (fileType === 'tsv') {
        initTsvDownloadData();
      } else if (['pdf', 'svg'].indexOf(fileType) !== -1) {
        initCanvasDownloadData();
      }
    };

    content.filtered = function(filters) {
      updateTables();
      isFiltered = _.isArray(filters) && filters.length > 0;
    };

    content.getCurrentCategories = function(sortBy) {
      var categories = [];

      sortBy = sortBy || 'value';
      _.each(_.sortBy(dcGroup_.top(Infinity), sortBy), function(label) {
        var _labelDatum = {};
        var _labelValue = Number(label.value);
        if (_labelValue > 0) {
          _labelDatum.id = labelInitData[label.key].id;
          _labelDatum.index = labelInitData[label.key].index;
          _labelDatum.name = label.key;
          _labelDatum.color = labelInitData[label.key].color;
          _labelDatum.cases = _labelValue;
          categories.push(_labelDatum);
        }
      });
      return categories;
    };

    function getCurrentSampleSizeFromCategories(categories) {
      var currentSampleSize = 0;
      for (var key in categories) {
        if (categories.hasOwnProperty(key)) {
          currentSampleSize += categories[key].cases;
        }
      }
      return currentSampleSize;
    }

    /**
     * This is the function to initialize dc pie chart instance.
     */
    function initDCPieChart() {
      if (v.opts.hasOwnProperty('chartId') &&
        v.data.hasOwnProperty('ndx') &&
        v.data.hasOwnProperty('attr_id')) {
        var width = v.opts.width || 130;
        var height = v.opts.height;
        var radius = (width - 20) / 2;
        var color = $.extend(true, [], v.data.color);

        v.chart = dc.pieChart('#' + v.opts.chartId, v.opts.groupid);

        v.data.attrKeys = cluster.group().all().map(function(d) {
          return d.key;
        });

        v.data.category = iViz.util.pieChart.getCategory(v.data.attr_id,
          v.data.attrKeys);

        v.data.attrKeys.sort(function(a, b) {
          return a < b ? -1 : 1;
        });

        var NAIndex = v.data.attrKeys.indexOf('NA');
        if (NAIndex !== -1) {
          color.splice(NAIndex, 0, '#CCCCCC');
        }

        // Initial labels data with colors.
        _.each(v.data.attrKeys, function(attr, index) {
          labelInitData[attr] = {
            attr: attr,
            color: color[index],
            id: attr,
            index: index
          };
        });

        dcDimension_ = cluster;
        dcGroup_ = cluster.group();

        v.chart
          .width(width)
          .height(height)
          .radius(radius)
          .dimension(dcDimension_)
          .group(dcGroup_)
          .transitionDuration(v.opts.transitionDuration || 400)
          .ordinalColors(color)
          .label(function(d) {
            return d.value;
          })
          .ordering(function(d) {
            return d.key;
          });
        v.chart.on('preRedraw', function() {
          removeMarker();
        });
        v.chart.on('postRedraw', function() {
          // TODO:commented this because this is taking much time to redraw
          // after applying filter, need to find different way
          if (isFiltered) {
            updateQtip = false;
            isFiltered = false;
          } else {
            updateQtip = true;
            if (currentView === 'table') {
              updatePieLabels();
            }
          }
        });
      } else {
        // TODO:
        /**
         * Need a handler if no dimension ID passed.
         */
      }
    }

    function initTsvDownloadData() {
      var data = [v.data.display_name + '\tCount'];

      _.each(labels, function(label, key) {
        data.push(label.name + '\t' + label.cases);
      });

      content.setDownloadData('tsv', {
        fileName: v.data.display_name || 'Pie Chart',
        data: data.join('\n')
      });
    }

    function initCanvasDownloadData() {
      content.setDownloadData('svg', {
        title: v.data.display_name,
        chartDivId: v.opts.chartDivId,
        chartId: v.opts.chartId,
        fileName: v.data.display_name,
        labels: labels
      });
      content.setDownloadData('pdf', {
        title: v.data.display_name,
        chartDivId: v.opts.chartDivId,
        chartId: v.opts.chartId,
        fileName: v.data.display_name,
        labels: labels
      });
    }

    function animateTable(target, view, callback) {
      var width = window.iViz.styles.vars.width.one;
      var height = window.iViz.styles.vars.height.one;

      if (view === 'table') {
        width = window.iViz.styles.vars.width.two;
        height = window.iViz.styles.vars.height.two;
        if (Object.keys(labels).length <= 3) {
          height = window.iViz.styles.vars.height.one;
        }
      }

      $(target).animate({
        height: height,
        width: width,
        duration: 300,
        queue: false
      }, 300, function() {
        if (_.isFunction(callback)) {
          callback();
        }
      });
    }

    function updatePieLabels() {
      updateCurrentLabels();
      initReactData();
      updateTables();
    }

    function updateTables() {
      if (currentView === 'pie' && qtipRendered) {
        updateQtipReactTable();
      }
      if (currentView === 'table') {
        updateReactTable();
      }
    }

    function updateReactTable() {
      var data = $.extend(true, {}, reactTableData);
      initReactTable(v.opts.chartTableId, data, {
        tableWidth: window.iViz.styles.vars.specialTables.width
      });
    }

    function updateQtipReactTable() {
      var data = $.extend(true, {}, reactTableData);
      data.attributes[0].column_width = 140;
      initReactTable('qtip-' + v.opts.chartDivId + '-content-react', data, {
        tableWidth: 300,
        pieLabelMouseEnterFunc: pieLabelMouseEnter,
        pieLabelMouseLeaveFunc: pieLabelMouseLeave
      });
    }

    function updateCurrentLabels() {
      var _labels = content.getCurrentCategories();
      var _currentSampleSize = getCurrentSampleSizeFromCategories(_labels);

      labels = {};
      _.each(_labels, function(label) {
        label.sampleRate = (_currentSampleSize <= 0 ? 0 : (Number(label.cases) * 100 / _currentSampleSize).toFixed(1).toString()) + '%';
        labels[label.id] = label;
      });
    }

    function initReactData() {
      var _data = [];
      _.each(labels, function(item) {
        for (var key in item) {
          if (item.hasOwnProperty(key)) {
            var datum = {
              attr_id: key,
              uniqueId: item.id,
              attr_val: item[key]
            };
            _data.push(datum);
          }
        }
      });
      reactTableData.data = _data;
    }

    function removeMarker() {
      $('#' + v.opts.chartId).find('svg g .mark').remove();
    }

    function drawMarker(_slice) {
      var _path = $(_slice).find('path');
      var _pointsInfo = _path
        .attr('d')
        .split(/[\s,MLHVCSQTAZ]/);

      var _pointsInfo1 = _path
        .attr('d')
        .split(/[A]/);

      var _fill = _path
        .attr('fill');

      var _x1 = Number(_pointsInfo[1]);
      var _y1 = Number(_pointsInfo[2]);
      var _x2 = Number(_pointsInfo[8]);
      var _y2 = Number(_pointsInfo[9]);
      var _r = Number(_pointsInfo[3]);

      if ((_x1 - _x2 !== 0 || _y1 - _y2 !== 0) && _pointsInfo1.length === 2) {
        var _pointOne = Math.atan2(_y1, _x1);
        var _pointTwo = Math.atan2(_y2, _x2);

        if (_pointOne < -Math.PI / 2) {
          _pointOne = Math.PI / 2 + Math.PI * 2 + _pointOne;
        } else {
          _pointOne = Math.PI / 2 + _pointOne;
        }

        if (_pointTwo < -Math.PI / 2) {
          _pointTwo = Math.PI / 2 + Math.PI * 2 + _pointTwo;
        } else {
          _pointTwo = Math.PI / 2 + _pointTwo;
        }

        // The value of point two should always bigger than the value
        // of point one. If the point two close to 12 oclick, we should
        // change it value close to 2PI instead of close to 0
        if (_pointTwo > 0 && _pointTwo < 0.0000001) {
          _pointTwo = 2 * Math.PI - _pointTwo;
        }

        if (_pointTwo < _pointOne) {
          console.log('%cError: the end angle should always bigger' +
            ' than start angle.', 'color: red');
        }

        var _arc = d3.svg.arc()
          .innerRadius(_r + 3)
          .outerRadius(_r + 5)
          .startAngle(_pointOne)
          .endAngle(_pointTwo);

        d3.select('#' + v.opts.chartId + ' svg g').append('path')
          .attr('d', _arc)
          .attr('fill', _fill)
          .attr('class', 'mark');
      }
    }

    function pieLabelMouseEnter(data) {
      var _slice = getPieSlice(data);
      
      $(_slice).css({
        'fill-opacity': '.5',
        'stroke-width': '3'
      });

      drawMarker(_slice);
    }

    function pieLabelMouseLeave(data) {
      var _slice = getPieSlice(data);

      $(_slice).css({
        'fill-opacity': '1',
        'stroke-width': '1px'
      });
      
      removeMarker();
    }

    function getPieSlice(data) {
      var _color = data.color;
      var _slice;

      $('#' + v.opts.chartId + ' svg g.pie-slice').each(function(index, item) {
        var _sliceColor = $(item).find('path').attr('fill');
        if (_sliceColor === _color) {
          _slice = item;
          $(item).css({
            'fill-opacity': '1',
            'stroke-width': '1px'
          });
        }
      });
      return _slice;
    }

    function initReactTable(targetId, inputData, opts) {
      var selectedRows = v.chart.filters();

      var opts_ = $.extend({
        input: inputData,
        filter: 'ALL',
        download: 'NONE',
        downloadFileName: 'data.txt',
        showHide: false,
        hideFilter: true,
        scroller: true,
        resultInfo: false,
        groupHeader: false,
        fixedChoose: false,
        uniqueId: 'uniqueId',
        rowHeight: 25,
        tableWidth: 373,
        maxHeight: 290,
        headerHeight: 26,
        groupHeaderHeight: 40,
        autoColumnWidth: false,
        columnMaxWidth: 300,
        columnSorting: false,
        tableType: 'pieLabel',
        elementId: targetId + '-table',
        selectedRows: selectedRows,
        rowClickFunc: pieLabelClick
      }, opts);

      // Check whether the react table has been initialized
      if (v.renderedReactTable) {
        // Get sort settings from the initialized react table
        var sort_ = v.renderedReactTable.getCurrentSort();
        opts_ = $.extend(opts_, sort_);
      }

      var testElement = React.createElement(EnhancedFixedDataTableSpecial, opts_);

      v.renderedReactTable = ReactDOM.render(testElement, document.getElementById(targetId));
    }

    function pieLabelClick(selectedData) {
      v.chart.onClick({
        key: labels[selectedData.uniqueid].name,
        value: labels[selectedData.uniqueid].value
      });
    }
  };

  iViz.view.component.PieChart.prototype = new iViz.view.component.GeneralChart('pieChart');
  iViz.view.component.PieChart.constructor = iViz.view.component.PieChart;

  // Utils designed for pie chart.
  iViz.util.pieChart = (function() {
    var util = {};
    var v = {};

    v.category = ['w1', 'h1']; // Size class name for chart

    v.labelLT = 5; // Label length threshold
    v.labelHeaderLT = 4; // Label header length threshold

    // If the name lenght bigger the threshold, it will be truncated.
    v.labelWLT = 30; // Label length threshold for wider table
    v.labelHeaderWLT = 20; // Label header length threshold for wider table

    util.getCategory = function(attr, attrKeys) {
      var category = $.extend(true, {}, v.category);
      var maxAttrL = 0;

      _.each(attrKeys, function(key) {
        if (key.length > maxAttrL) {
          maxAttrL = key.length;
        }
      });

      category[0] = maxAttrL <= v.labelLT ? 'w1' : 'w2';

      // Default settings for special attribtues.
      if (['CANCER_TYPE', 'CANCER_TYPE_DETAILED'].indexOf(attr) !== -1) {
        category[0] = 'w2';
      }

      category[1] = attrKeys.length > 10 ? 'h2' : 'h1';

      return category;
    };

    return util;
  })();
})(window.iViz,
  window.d3,
  window.dc,
  window._,
  window.$ || window.jQuery,
  window.React,
  window.ReactDOM,
  window.EnhancedFixedDataTableSpecial
);