Vizzuality/gfw-climate

View on GitHub
app/assets/javascripts/countries/views/report/ProvincesTopChartView.js

Summary

Maintainability
F
1 wk
Test Coverage
define(
  [
    'handlebars',
    'backbone',
    'underscore',
    'd3',
    'helpers/NumbersHelper',
    'text!countries/templates/report/provinces-top-chart.handlebars'
  ],
  function(Handlebars, Backbone, _, d3, NumbersHelper, tpl) {
    'use strict';

    var ProvincesTopChart = Backbone.View.extend({
      el: '#provinces-top-chart',

      template: Handlebars.compile(tpl),

      defaults: {
        chartEl: 'provinces-top-chart-svg',
        chartClass: 'js-provinces-top-chart',
        paddingAxisLabels: 10,
        paddingXAxisLabels: 10,
        paddingYAxisLabels: 10,
        margin: {
          top: 10,
          right: 0,
          bottom: 35,
          left: 0
        },
        rowHeight: 26,
        barHeight: 5,
        barMargin: 10,
        defaultZeroValue: 5,
        yAxisWidth: 30,
        underscriptPadding: 1.5,
        underscript: '2',
        labels: [
          {
            name: 'Province',
            footerName: '',
            slug: 'province',
            width: 20
          },
          {
            name: '',
            footerName: '',
            slug: 'average',
            width: 60
          },
          {
            name: 'ha/yr',
            footerName: '',
            slug: 'loss',
            width: 10
          },
          {
            name: 'Change',
            footerName: '',
            slug: 'deviation',
            width: 10
          }
        ]
      },

      initialize: function(settings) {
        this.defaults = _.extend({}, this.defaults, settings);
        this.data = this.defaults.data;

        this._initChart();

        // Sets listeners
        this.setListeners();
      },

      _initChart: function() {
        // Data parsing and initialization
        this._parseData();
        this.hasData = this.chartData && this.chartData.length;

        if (this.hasData) {
          this._start();
        } else {
          this._renderNoData();
        }
      },

      _start: function() {
        this.$el.html(
          this.template({
            hasData: this.chartData.length
          })
        );

        this.render();
      },

      _renderNoData: function() {
        this.$el.html(
          this.template({
            hasData: this.hasData
          })
        );
      },

      render: function() {
        this._setUpGraph();
        this._drawGrid();
        this._setAxisScale();
        this._setDomain();
        this._drawGraph();
      },

      /**
       * Sets the listeners for the component
       */
      setListeners: function() {
        this.refreshEvent = _.debounce(_.bind(this._update, this), 30);
        window.addEventListener('resize', this.refreshEvent, false);
      },

      unsetListeners: function() {
        window.removeEventListener('resize', this.refreshEvent, false);

        this.refreshEvent = null;
      },

      /**
       *  Parses the data for the chart
       */
      _parseData: function() {
        this.chartData = this.data;
      },

      /**
       *  Sets up the SVG for the graph
       */
      _setUpGraph: function() {
        this.chartEl = this.el.querySelector('#' + this.defaults.chartEl);
        var el = this.chartEl;
        var margin = this.defaults.margin;

        el.innerHTML = '';
        el.classList.add(this.defaults.chartClass);

        this.cWidth = el.clientWidth;
        this.cHeight =
          (this.chartData.length + 1) * (this.defaults.rowHeight * 2);
        this.domain = this._getDomain();

        this.yAxisWidth = this.defaults.yAxisWidth;
        this.widthPadding = this.cWidth - this.yAxisWidth;

        this.cWidth = this.cWidth - margin.left - margin.right;
        this.cHeight = this.cHeight - margin.top - margin.bottom;
        this.cWidthGrid = this.cWidth - margin.left - margin.right;

        var svg = d3
          .select(el)
          .append('svg')
          .attr('width', this.cWidth + margin.left + margin.right + 'px')
          .attr('height', this.cHeight + margin.top + margin.bottom + 'px');

        this.svg = svg
          .append('g')
          .attr(
            'transform',
            'translate(' + margin.left + ',' + margin.top + ')'
          );
      },

      /**
       *  Sets the axis
       */
      _setAxisScale: function() {
        var average = _.findWhere(this.defaults.labels, { slug: 'average' });
        var barsContentWidth =
          average.width * this.cWidthGrid / 100 - this.defaults.barMargin * 2;

        this.x = d3.scale.linear().range([0, barsContentWidth]);

        this.x2 = d3.scale.linear().range([0, barsContentWidth]);

        this.y = d3.scale
          .linear()
          .range([0, this.cHeight])
          .nice();

        this.yAxis = d3.svg
          .axis()
          .scale(this.y)
          .orient('left')
          .innerTickSize(0)
          .outerTickSize(0)
          .ticks(this.chartData.length)
          .tickFormat(
            function(d, i) {
              if (this.chartData[i]) {
                return this.chartData[i].year;
              }
              return null;
            }.bind(this)
          );
      },

      /**
       * Sets the domain
       */
      _setDomain: function() {
        this.x.domain(this.domain.x);
        this.x2.domain(this.domain.x2);
        this.y.domain(this.domain.y);
      },

      /**
       *  Get the domain values
       */
      _getDomain: function() {
        var maxValues = [];
        maxValues.push({
          value: d3.max(this.chartData, function(d) {
            return d.reference_avg;
          })
        });
        maxValues.push({
          value: d3.max(this.chartData, function(d) {
            return d.monitor_avg;
          })
        });

        return {
          x: [
            0,
            d3.max(maxValues, function(d) {
              return d.value;
            })
          ],
          x2: [
            0,
            d3.max(this.chartData, function(d) {
              return d.monitor_avg;
            })
          ],
          y: [0, this.chartData.length]
        };
      },

      /**
       * Draws the entire graph
       */
      _drawGraph: function() {
        this._drawProvinces();
        this._drawLoss();
        this._drawDeviation();
        this._drawBars();
      },

      _drawGrid: function() {
        // X Lines
        var numLines = this.chartData.length;
        var rowOffset = this.defaults.rowHeight;
        var linesGroup = this.svg
          .append('g')
          .attr('transform', 'translate(0, ' + this.defaults.rowHeight + ')');

        for (var x = 0; x < numLines; x++) {
          linesGroup
            .append('rect')
            .attr('class', 'xAxis')
            .attr('width', this.cWidth)
            .attr('height', 1)
            .attr('x', 0)
            .attr('y', rowOffset * 2);

          rowOffset += this.defaults.rowHeight;
        }

        // Labels
        var marginOffset = 0;
        this.defaults.labels.forEach(
          function(label, index) {
            var margin = label.width * this.cWidthGrid / 100;
            var group = this.svg
              .append('g')
              .attr('transform', 'translate(' + marginOffset + ', 0)')
              .attr('class', label.slug);

            if (index === 0) {
              group
                .append('text')
                .attr('class', 'label')
                .attr('y', this.defaults.rowHeight / 2)
                .attr('x', 0)
                .style('text-anchor', 'start')
                .text(label.name);
            } else if (this.defaults.customLabel && label.slug === 'loss') {
              var customLabel = group
                .append('text')
                .attr('class', 'label')
                .attr('y', this.defaults.rowHeight / 2)
                .attr('x', margin - this.defaults.paddingXAxisLabels);

              this._setCustomLabel(this.defaults.customLabel, customLabel);
            } else {
              group
                .append('text')
                .attr('class', 'label')
                .attr('y', this.defaults.rowHeight / 2)
                .attr('x', margin - this.defaults.paddingXAxisLabels)
                .text(label.name);
            }
            marginOffset += margin;
          }.bind(this)
        );

        // Y Line
        var averageGroup = this.svg.select('.average');
        linesGroup
          .append('rect')
          .attr('class', 'yAxis')
          .attr('width', 3)
          .attr('height', this.chartData.length * (this.defaults.rowHeight * 2))
          .attr('x', d3.transform(averageGroup.attr('transform')).translate[0])
          .attr('y', 0);
      },

      _setCustomLabel: function(label, group) {
        if (label.search(this.defaults.underscript) !== -1) {
          var unit = label.split(this.defaults.underscript);
          for (var x = 0; x < unit.length; x++) {
            group.append('tspan').text(unit[x]);

            if (x === 0) {
              group
                .append('tspan')
                .attr('class', 'underscript')
                .attr('dy', this.defaults.underscriptPadding)
                .text(this.defaults.underscript);
            }
          }
        }
      },

      _drawProvinces: function() {
        var provinceGroup = this.svg.select('.province');
        var provincesContent = this.svg
          .append('g')
          .attr(
            'transform',
            'translate(' +
              d3.transform(provinceGroup.attr('transform')).translate[0] +
              ', ' +
              this.defaults.rowHeight +
              ')'
          );

        var provincesGroup = provincesContent
          .selectAll('g')
          .data(this.chartData)
          .enter()
          .append('g')
          .attr(
            'transform',
            function(d, i) {
              return 'translate(0, ' + this.defaults.rowHeight * 2 * i + ')';
            }.bind(this)
          );

        provincesGroup
          .append('text')
          .attr('class', 'province')
          .text(function(d) {
            return d.province;
          })
          .attr('dx', 0)
          .attr(
            'dy',
            function() {
              return this.defaults.rowHeight * 2 / 2 + this.defaults.barHeight;
            }.bind(this)
          );
      },

      _drawLoss: function() {
        var lossGroup = this.svg.select('.loss');
        var lossLabel = _.findWhere(this.defaults.labels, { slug: 'loss' });
        var lossLabelWidth = lossLabel.width * this.cWidthGrid / 100;
        var lossContent = this.svg
          .append('g')
          .attr(
            'transform',
            'translate(' +
              d3.transform(lossGroup.attr('transform')).translate[0] +
              ', ' +
              this.defaults.rowHeight +
              ')'
          );

        lossGroup = lossContent
          .selectAll('g')
          .data(this.chartData)
          .enter()
          .append('g')
          .attr(
            'transform',
            function(d, i) {
              return 'translate(0, ' + this.defaults.rowHeight * 2 * i + ')';
            }.bind(this)
          );

        lossGroup
          .append('text')
          .attr('class', 'value')
          .text(function(d) {
            return NumbersHelper.addNumberDecimals(Math.round(d.reference_avg));
          })
          .attr(
            'dx',
            function() {
              return lossLabelWidth - this.defaults.paddingXAxisLabels;
            }.bind(this)
          )
          .attr(
            'dy',
            function() {
              return (
                this.defaults.rowHeight / 2 +
                this.defaults.barHeight +
                this.defaults.barHeight / 2
              );
            }.bind(this)
          );

        lossGroup
          .append('text')
          .attr('class', 'value')
          .text(function(d) {
            return NumbersHelper.addNumberDecimals(Math.round(d.monitor_avg));
          })
          .attr(
            'dx',
            function() {
              return lossLabelWidth - this.defaults.paddingXAxisLabels;
            }.bind(this)
          )
          .attr(
            'dy',
            function() {
              return this.defaults.rowHeight / 2 + this.defaults.barHeight * 5;
            }.bind(this)
          );
      },

      _drawDeviation: function() {
        var deviationGroup = this.svg.select('.deviation');
        var deviationLabel = _.findWhere(this.defaults.labels, {
          slug: 'deviation'
        });
        var deviationLabelWidth = deviationLabel.width * this.cWidthGrid / 100;
        var deviationContent = this.svg
          .append('g')
          .attr(
            'transform',
            'translate(' +
              d3.transform(deviationGroup.attr('transform')).translate[0] +
              ', ' +
              this.defaults.rowHeight +
              ')'
          );

        deviationGroup = deviationContent
          .selectAll('g')
          .data(this.chartData)
          .enter()
          .append('g')
          .attr(
            'transform',
            function(d, i) {
              return 'translate(0, ' + this.defaults.rowHeight * 2 * i + ')';
            }.bind(this)
          );

        deviationGroup
          .append('text')
          .attr('class', 'value')
          .text(function(d) {
            var value = Math.round(d.delta_perc);
            var displayValue = value;

            if (value > 0) {
              displayValue = '+' + value;
            }
            return displayValue + '%';
          })
          .attr(
            'dx',
            function() {
              return deviationLabelWidth - this.defaults.paddingXAxisLabels;
            }.bind(this)
          )
          .attr(
            'dy',
            function() {
              return this.defaults.rowHeight / 2 + this.defaults.barHeight * 5;
            }.bind(this)
          );
      },

      _drawBars: function() {
        var averageGroup = this.svg.select('.average');
        var leftOffset =
          d3.transform(averageGroup.attr('transform')).translate[0] +
          this.defaults.barMargin;

        var barsContent = this.svg
          .append('g')
          .attr(
            'transform',
            'translate(' + leftOffset + ', ' + this.defaults.rowHeight + ')'
          );

        var barGroup = barsContent
          .selectAll('g')
          .data(this.chartData)
          .enter()
          .append('g')
          .attr(
            'transform',
            function(d, i) {
              return 'translate(0, ' + this.defaults.rowHeight * 2 * i + ')';
            }.bind(this)
          );

        barGroup
          .append('rect')
          .attr('class', 'bar reference')
          .attr('height', this.defaults.barHeight)
          .attr(
            'width',
            function(d) {
              return this.x(d.reference_avg);
            }.bind(this)
          )
          .attr(
            'y',
            function() {
              return this.defaults.rowHeight / 2 + this.defaults.barHeight / 2;
            }.bind(this)
          );

        barGroup
          .append('rect')
          .attr('class', 'bar monitor')
          .attr('height', this.defaults.barHeight)
          .attr(
            'width',
            function(d) {
              return this.x(d.monitor_avg);
            }.bind(this)
          )
          .attr(
            'y',
            function() {
              return (
                this.defaults.rowHeight / 2 +
                this.defaults.barHeight / 2 +
                this.defaults.barHeight * 2
              );
            }.bind(this)
          );
      },

      /**
       *  Renders the chart after a resize
       */
      _update: function() {
        this.remove({
          keepEvents: true
        });
        this.render();
      },

      /**
       * Removes the SVG
       */
      remove: function(params) {
        if (this.svg) {
          var svgContainer = this.chartEl.querySelector('svg');

          if (params && !params.keepEvents) {
            this.unsetListeners();
            this.stopListening();
          }
          this.svg.remove();
          this.svg = null;
          this.chartEl.removeChild(svgContainer);
        }
      }
    });
    return ProvincesTopChart;
  }
);