Skip to content
Snippets Groups Projects
DisaggGraphView.js 17.7 KiB
Newer Older
  • Learn to ignore specific revisions
  • 'use strict';
    
    var Collection = require('../mvc/Collection'),
      d3 = require('d3'),
    
    Clayton, Brandon Scott's avatar
    Clayton, Brandon Scott committed
      ClassList = require('../d3/util/ClassList'),
    
      D33dAxis = require('../d3/3d/D33dAxis'),
    
      D33dDisaggBin = require('./D33dDisaggBin'),
    
      D33dPath = require('../d3/3d/D33dPath'),
      D33dView = require('../d3/view/D33dView'),
      D3Util = require('../d3/util/D3Util'),
      SelectedCollectionView = require('../mvc/SelectedCollectionView'),
      Util = require('../util/Util');
    
    var _DEFAULTS;
    
    _DEFAULTS = {};
    
    var __calculateBounds;
    
    /**
    
     * Calculate disaggregation data bounds.
    
     *
     * @param bindata {Array<Bin>}
    
     *     array of disaggregation bins
    
     *     where Bin is an Object:
     *       bin.r {Number}
     *             distance to bin (x-axis)
     *       bin.m {Number}
     *             magnitude of bin (y-axis)
     *       bin.εdata {Array<Object>}
     *             array of epsilon data for bin (z-axis)
     *             Each Object has properties:
     *               εdata.value {Number}
     *                           % contribution to hazard
     *               εdata.εbin {Number}
     *                          id of epsilon bin.
     *
     * @return {Array<Array<x0,y0,z0>, Array<x1,y1,z1>}
    
     *         bounds of disaggregation data.
    
     *         Array contains two sub arrays,
     *         containing minimum and maximum values for each axis.
     */
    __calculateBounds = function (bindata) {
      var x0, x1, y0, y1, z0, z1;
    
      // start with values that will always be smaller/larger than actual values
      x0 = y0 = z0 = Number.POSITIVE_INFINITY;
      x1 = y1 = z1 = Number.NEGATIVE_INFINITY;
    
      bindata.forEach(function (bin) {
        var binx, biny, binz;
    
        binx = bin.r;
        biny = bin.m;
        // sum values for z
        binz = 0;
        bin.εdata.forEach(function (εval) {
          binz = binz + εval.value;
        });
    
        // track min/max
        if (binx < x0) {
          x0 = binx;
        }
        if (binx > x1) {
          x1 = binx;
        }
        if (biny < y0) {
          y0 = biny;
        }
        if (biny > y1) {
          y1 = biny;
        }
        if (binz < z0) {
          z0 = binz;
        }
        if (binz > z1) {
          z1 = binz;
        }
      });
    
      // return bounds
      return [
        [x0, y0, z0],
        [x1, y1, z1],
      ];
    };
    
    /**
    
     * A disaggregation distance/magnitude plot.
    
     *
     * @param options {Object}
     *        options are passed to SelectedCollectionView.
     * @param options.bounds {Array<Array>}
     *        default null.
     *        plotting bounds.
     *        can be changed later by setting VIEW.bounds and calling VIEW.render
    
     * @param options.collection {Collection<Disaggregation>}
    
     *        collection with data to display.
    
     *        selected Disaggregation object is displayed.
    
    var DisaggGraphView = function (options) {
    
    Clayton, Brandon Scott's avatar
    Clayton, Brandon Scott committed
      var _this, _initialize, _onPointOver, _onPointOut;
    
    
      _this = SelectedCollectionView(options);
    
      _initialize = function (options) {
        options = Util.extend({}, _DEFAULTS, options);
    
    
        _this.el.innerHTML = '<div class="DisaggGraphView"></div>';
    
    
        _this.bounds = options.bounds || null;
    
        _this.axes = [];
        _this.bins = [];
    
        _this.xScale = 2;
        _this.yScale = 25;
        _this.zScale = 2;
    
        _this.xTickSpacing = 5;
        _this.yTickSpacing = 0.5;
        _this.zTickSpacing = 5;
    
        _this.d33d = D33dView({
    
          el: _this.el.querySelector('.DisaggGraphView'),
    
          lookAt: [60, 125, 10],
          origin: [280, -150, 180],
          up: [0, 0, 1],
          zoom: 3.5,
        });
        _this.d33d.renderLegend = _this.renderLegend;
    
        _this.render();
    
    
        var dragging = false;
        var origin = {
          x: 0,
          y: 0,
        };
    
        d3.select(_this.d33d.el)
          .on('mousedown', event => {
            origin.x = event.clientX;
            origin.y = event.clientY;
            dragging = true;
          })
          .on('mousemove', event => {
            if (dragging) {
              var dx = event.clientX - origin.x;
              var dz = event.clientY - origin.y;
              _this.setOrigin(280 - dx, -150, 180 + dz)
            }
          })
          .on('mouseup', () => {
            dragging = false
        })
    
      };
    
      /**
       * Use data extents to set view lookAt, origin, and zoom so plot is
       * roughly centered within view plot area.
       */
      _this.centerView = function () {
        var bounds,
          lookAt,
          origin,
          projectedBounds,
          projectedWidth,
          projectedHeight,
          x0,
          x1,
          y0,
          y1,
          z0,
          z1,
          zoom;
    
        // get current view
        lookAt = _this.d33d.model.get('lookAt');
        origin = _this.d33d.model.get('origin');
        zoom = _this.d33d.model.get('zoom');
    
        // compute projected bounds
        bounds = _this.getBounds();
        _this.xScale = 100 / (bounds[1][0] - bounds[0][0]);
        _this.yScale = 70 / (bounds[1][1] - bounds[0][1]);
        _this.zScale = 50 / (bounds[1][2] - bounds[0][2]);
        x0 = bounds[0][0] * _this.xScale;
        x1 = bounds[1][0] * _this.xScale;
        y0 = bounds[0][1] * _this.yScale;
        y1 = bounds[1][1] * _this.yScale;
        z0 = bounds[0][2] * _this.zScale;
        z1 = bounds[1][2] * _this.zScale;
    
        projectedBounds = [
          [x0, y0, z0],
          [x0, y1, z0],
          [x1, y1, z0],
          [x1, y0, z0],
          [x0, y0, z1],
          [x0, y1, z1],
          [x1, y1, z1],
          [x1, y0, z1],
        ]
          .map(_this.d33d.project)
          .reduce(
            function (bounds, point) {
              var x, y, z;
    
              x = point[0];
              y = point[1];
              z = point[2];
    
              if (x < bounds[0][0]) {
                bounds[0][0] = x;
              }
              if (x > bounds[1][0]) {
                bounds[1][0] = x;
              }
              if (y < bounds[0][1]) {
                bounds[0][1] = y;
              }
              if (y > bounds[1][1]) {
                bounds[1][1] = y;
              }
              if (z < bounds[0][2]) {
                bounds[0][2] = z;
              }
              if (z > bounds[1][2]) {
                bounds[1][2] = z;
              }
    
              return bounds;
            },
            [
              [Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY],
              [Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY],
            ]
          );
    
        projectedWidth = projectedBounds[1][0] - projectedBounds[0][0];
        projectedHeight = projectedBounds[1][1] - projectedBounds[0][1];
    
        // look at x/y center at z=z0
        lookAt = [(0.95 * (x1 + x0)) / 2, (0.95 * (y1 + y0)) / 2, -5 * _this.zScale];
        origin = [lookAt[0] + 1000, lookAt[1] - 1000, lookAt[2] + 1000];
        zoom = Math.min((480 * zoom) / projectedWidth, (480 * zoom) / projectedHeight);
    
        _this.d33d.model.set({
          lookAt: lookAt,
          origin: origin,
          zoom: zoom,
        });
      };
    
      /**
       * Create axes to be plotted.
       *
       * TODO: eliminate "magic" numbers.
       * Extents and tick, label, and title, vectors arre hard coded and work for
       * the current lookAt, origin, and zoom, combination.
       */
      _this.createAxes = function () {
        var bounds,
          extent,
          gridSpacing,
          metadata,
          x,
          x0,
          x1,
          xLabel,
          xTicks,
          y,
          y0,
          y1,
          yLabel,
          yTicks,
          z0,
          z1,
          zLabel,
          zTicks;
    
        bounds = _this.getBounds();
        x0 = bounds[0][0];
        x1 = bounds[1][0];
        y0 = bounds[0][1];
        y1 = bounds[1][1];
        z0 = bounds[0][2];
        z1 = bounds[1][2];
    
        if (_this.model) {
          metadata = _this.model.get('metadata');
        } else {
          metadata = {};
        }
    
        //_this.yScale = 100 / (y1 - y0);
        //_this.zScale = 100 / (z1 - z0);
    
        xLabel = metadata.rlabel;
        xTicks = (x1 - x0) / _this.xTickSpacing;
        while (xTicks > 10) {
          xTicks = xTicks / 2;
        }
    
        yLabel = metadata.mlabel;
        yTicks = (y1 - y0) / _this.yTickSpacing;
        zLabel = metadata.εlabel;
        zTicks = (z1 - z0) / _this.zTickSpacing;
    
        x0 = x0 * _this.xScale;
        x1 = x1 * _this.xScale;
        y0 = y0 * _this.yScale;
        y1 = y1 * _this.yScale;
        z0 = z0 * _this.zScale;
        z1 = z1 * _this.zScale;
    
        // x axis at y0
        extent = [
          [x0, y0, z0],
          [x1, y0, z0],
        ];
        _this.axes.push(
          D33dAxis({
            className: 'axis x-axis',
            extent: extent,
            format: _this.formatX,
            labelAnchor: 'middle',
            labelDirection: extent,
            labelVector: [1, -5, 0],
            padding: 0,
            tickVector: [0, -1, 0],
            ticks: xTicks,
            title: xLabel,
            titleAnchor: 'middle',
            titleDirection: extent,
            titleVector: [2, -10, 0],
          })
        );
    
        // x axis at y1
        extent = [
          [x0, y1, z0],
          [x1, y1, z0],
        ];
        _this.axes.push(
          D33dAxis({
            className: 'axis x-axis',
            extent: extent,
            format: _this.formatX,
            labelAnchor: 'middle',
            labelDirection: extent,
            labelVector: [0, 2, 0],
            padding: 0,
            tickVector: [0, 1, 0],
            ticks: xTicks,
            title: xLabel,
            titleAnchor: 'middle',
            titleDirection: extent,
            titleVector: [0, 8, 0],
          })
        );
    
        // y axis at x0
        extent = [
          [x0, y0, z0],
          [x0, y1, z0],
        ];
        _this.axes.push(
          D33dAxis({
            className: 'axis y-axis',
            extent: extent,
            format: _this.formatY,
            labelAnchor: 'middle',
            labelDirection: extent,
            labelVector: [-2, 0, 0],
            padding: 0,
            tickVector: [-1, 0, 0],
            ticks: yTicks,
            title: yLabel,
            titleAnchor: 'middle',
            titleDirection: extent,
            titleVector: [-7, 2, 0],
          })
        );
    
        // y axis at x1
        extent = [
          [x1, y0, z0],
          [x1, y1, z0],
        ];
        _this.axes.push(
          D33dAxis({
            className: 'axis y-axis',
            extent: extent,
            format: _this.formatY,
            labelAnchor: 'middle',
            labelDirection: extent,
            labelVector: [5, -2, 0],
            padding: 0,
            tickVector: [1, 0, 0],
            ticks: yTicks,
            title: yLabel,
            titleAnchor: 'middle',
            titleDirection: extent,
            titleVector: [9, -2, 0],
          })
        );
    
        // z axis
        extent = [
          [x0, y0, z0],
          [x0, y0, z1],
        ];
        _this.axes.push(
          D33dAxis({
            className: 'axis z-axis',
            extent: extent,
            format: _this.formatZ,
            labelAnchor: 'middle',
            labelDirection: extent,
            labelVector: [-1.5, 0, 0],
            padding: 0,
            tickVector: [-1, 0, 0],
            ticks: zTicks,
            title: zLabel,
            titleAnchor: 'middle',
            titleDirection: extent,
            titleVector: [-5, 0, 0],
          })
        );
    
        // grid lines
        gridSpacing = _this.xTickSpacing * _this.xScale;
        while ((x1 - x0) / gridSpacing > 10) {
          gridSpacing = gridSpacing * 2;
        }
    
        for (x = x0 + gridSpacing; x < x1; x += gridSpacing) {
          _this.axes.push(
            D33dPath({
              className: 'grid-line',
              close: false,
              coords: [
                [x, y0, z0],
                [x, y1, z0],
              ],
            })
          );
        }
    
        gridSpacing = _this.yTickSpacing * _this.yScale;
        for (y = y0 + gridSpacing; y < y1; y += gridSpacing) {
          _this.axes.push(
            D33dPath({
              className: 'grid-line',
              close: false,
              coords: [
                [x0, y, z0],
                [x1, y, z0],
              ],
            })
          );
        }
      };
    
      /**
       * Format x-axis {distance} labels.
       *
       * @param c {Array<Number>}
       *        tick coordinate.
       * @return {String}
       *         formatted coordinate.
       */
      _this.formatX = function (c) {
        var x;
        x = c[0] / _this.xScale;
        if (x === 0) {
          return '';
        }
        return '' + Number(x.toPrecision(3));
      };
    
      /**
       * Format y-axis (magnitude) labels.
       *
       * @param c {Array<Number>}
       *        tick coordinate.
       * @return {String}
       *         formatted coordinate.
       */
      _this.formatY = function (c) {
        var y;
        y = c[1] / _this.yScale;
        if (y === 0) {
          return '';
        }
        return '' + Number(y.toPrecision(3));
      };
    
      /**
       * Format z-axis (percent contribution) labels.
       *
       * @param c {Array<Number>}
       *        tick coordinate.
       * @return {String}
       *         formatted coordinate.
       */
      _this.formatZ = function (c) {
        var z;
        z = c[2] / _this.zScale;
        if (z === 0) {
          return '';
        }
        return '' + Number(z.toPrecision(3));
      };
    
      /**
       * Get data bounds for plotting.
       *
       * @return {Array<Array<x0,y0,z0>, Array<x1,y1,z1>}
    
       *         bounds of disaggregation data.
    
       *         Array contains two sub arrays,
       *         containing minimum and maximum values for each axis.
       */
      _this.getBounds = function () {
        var bounds, x0, x1, y0, y1, z0, z1;
    
        // default bounds
        x0 = 0;
        x1 = 100;
        y0 = 5;
        y1 = 8;
        z0 = 0;
        z1 = 35;
    
        if (_this.model) {
          bounds = _this.bounds;
          if (!bounds) {
            bounds = __calculateBounds(_this.model.get('data'));
          }
    
          x0 = bounds[0][0];
          x1 = bounds[1][0];
          y0 = bounds[0][1];
          y1 = bounds[1][1];
          z0 = bounds[0][2];
          z1 = bounds[1][2];
        }
    
        // round min/max down/up to an increment of 10
        x0 = _this.xTickSpacing * Math.floor(x0 / _this.xTickSpacing) - _this.xTickSpacing;
        x1 = _this.xTickSpacing * Math.ceil(x1 / _this.xTickSpacing) + _this.xTickSpacing;
    
        // round min/max down/up to next whole unit
        y0 = _this.yTickSpacing * Math.floor(y0 / _this.yTickSpacing) - _this.yTickSpacing;
        y1 = _this.yTickSpacing * Math.ceil(y1 / _this.yTickSpacing) + _this.yTickSpacing;
    
        // always use 0
        z0 = 0;
        // round up to increment of 10
        z1 = _this.zTickSpacing * Math.ceil(z1 / _this.zTickSpacing) + _this.zTickSpacing;
    
        return [
          [x0, y0, z0],
          [x1, y1, z1],
        ];
      };
    
      /**
       * Update displayed data.
       *
       * When a model is selected, bins are plotted on the graph.
       * Otherwise, any existing bins are destroyed and removed from the graph.
       */
      _this.render = function () {
        var oldAxes, oldBins, εbins;
    
        oldBins = _this.bins;
        oldAxes = _this.axes;
    
        _this.centerView();
    
        // create new views
        _this.axes = [];
        _this.createAxes();
    
        _this.bins = [];
        if (_this.model) {
          εbins = Collection(_this.model.get('metadata').εbins);
          _this.model.get('data').forEach(function (bin) {
            var view;
    
            view = D33dDisaggBin({
    
              bin: bin,
              xScale: _this.xScale,
              yScale: _this.yScale,
              zScale: _this.zScale,
              εbins: εbins,
            });
    
    Clayton, Brandon Scott's avatar
    Clayton, Brandon Scott committed
            d3.select(view.el)
    
    Clayton, Brandon Scott's avatar
    Clayton, Brandon Scott committed
              .on('mouseover', (event) => _onPointOver(event, view, bin))
              .on('mouseout', (event) => _onPointOut(event));
    
            _this.bins.push(view);
          });
        }
    
        // update plot
        _this.d33d.views.reset([].concat(_this.axes).concat(_this.bins));
    
        // clean up old views
        oldAxes.forEach(function (a) {
          a.destroy();
        });
    
        oldBins.forEach(function (v) {
          v.destroy();
        });
    
        oldAxes = null;
        oldBins = null;
      };
    
    
    Clayton, Brandon Scott's avatar
    Clayton, Brandon Scott committed
      _onPointOver = function (event, disaggBinView, bin) {
        var εbin = parseFloat(event.target.parentNode.getAttribute('data-bin-index'));
    
    Clayton, Brandon Scott's avatar
    Clayton, Brandon Scott committed
        var value = bin.εdata.find((data) => data.εbin === εbin).value;
    
        var view = disaggBinView
          .getData()
          .items.find((view) => parseFloat(view.el.getAttribute('data-bin-index')) === εbin);
        var coords = view.items[0].coords[0];
        coords = _this.d33d.project(coords);
    
    
    Clayton, Brandon Scott's avatar
    Clayton, Brandon Scott committed
        var path = event.target;
    
    Clayton, Brandon Scott's avatar
    Clayton, Brandon Scott committed
        ClassList.polyfill(path);
        path.classList.add('mouseover');
    
        _this.d33d.showTooltip(coords, [
          {
            class: 'title',
            text: `Component: ${_this.model.get('component')}`,
          },
          [
            { class: 'label', text: 'r: ' },
            { class: 'value', text: `${bin.r} km` },
          ],
          [
            { class: 'label', text: 'rBar: ' },
            { class: 'value', text: bin.rBar },
          ],
          [
            { class: 'label', text: 'm: ' },
            { class: 'value', text: bin.m },
          ],
          [
            { class: 'label', text: 'mBar: ' },
            { class: 'value', text: bin.mBar },
          ],
          [
            { class: 'label', text: 'εBar: ' },
            { class: 'value', text: bin.εBar },
          ],
          [
            { class: 'label', text: 'εbin: ' },
            { class: 'value', text: εbin },
          ],
          [
            { class: 'label', text: 'Value: ' },
            { class: 'value', text: value.toExponential(4) },
          ],
        ]);
      };
    
    
    Clayton, Brandon Scott's avatar
    Clayton, Brandon Scott committed
      _onPointOut = function (event) {
        var path = event.target;
    
    Clayton, Brandon Scott's avatar
    Clayton, Brandon Scott committed
        ClassList.polyfill(path);
        path.classList.remove('mouseover');
        _this.d33d.showTooltip(null, null);
      };
    
    
      /**
       * Custom legend rendering function for wrapped D33dView.
       *
       * @param info {Object}
       *        rendering information.
       * @param info.el {SVGElement}
       *        element where legend should be rendered.
       */
      _this.renderLegend = function (info) {
        var bbox, el, legendEl, metadata, z, εbins;
    
        legendEl = info.el;
    
        // clear legend
        Util.empty(legendEl);
        if (!_this.model) {
          return;
        }
    
        // plot epsilon bins
        z = 0;
        el = d3
          .select(legendEl)
          .append('g')
          .attr('class', 'legend-content')
    
          .attr('class', 'D33dDisaggBin');
    
    
        metadata = _this.model.get('metadata');
        εbins = metadata.εbins;
        εbins.forEach(function (bin, index) {
          var binEl, text, textEl;
    
          text =
            'ε = ' +
            (bin.min === null ? '(-∞' : '[' + bin.min) +
            ' .. ' +
            (bin.max === null ? '+∞' : bin.max) +
            ')';
    
          binEl = el
            .append('g')
            .attr('data-bin-index', index)
            .attr('transform', 'translate(0 ' + z + ')');
          binEl.append('path').attr('d', 'M0,2' + 'L10,2' + 'L10,-8' + 'L0,-8' + 'Z');
          textEl = binEl.append('text').attr('x', 15).text(text);
          z += D3Util.getBBox(binEl.node()).height;
        });
    
        bbox = D3Util.getBBox(el.node());
        el.attr('transform', 'translate(' + -bbox.width + ' 0)');
      };
    
    
      /**
       * Set the views origin.
       * @param {number} x
       * @param {number} y
       * @param {number} z
       */
      _this.setOrigin = function (x, y, z) {
        _this.d33d.model.set({
          'origin': [x, y, z]
        });
      }
    
    
      _initialize(options);
      options = null;
      return _this;
    };
    
    
    DisaggGraphView.calculateBounds = __calculateBounds;
    
    module.exports = DisaggGraphView;