datahuborg/datahub

View on GitHub
src/apps/dbwipes/static/js/summary/queryview.js

Summary

Maintainability
F
1 mo
Test Coverage
define(function(require) {
  var Backbone = require('backbone'),
      Handlebars = require('handlebars'),
      $ = require('jquery'),
      d3 = require('d3'),
      _ = require('underscore'),
      Where = require('summary/where'),
      util = require('summary/util'),
      DrawingView = require('summary/drawingview'),
      Query = require('summary/query');


  var QueryView = Backbone.View.extend({
    errtemplate: Handlebars.compile($("#q-err-template").html()),

    defaults: function() {
      return {
        xdomain: null,
        ydomain: null,
        xscales: null,
        yscales: null,
        cscales: d3.scale.category10(),
        xaxis: null,
        yaxis: null,
        series: null,
        w: 500,
        h: 300,
        lp: 70,
        tp: 20,
        bp: 15,
        ip: 10,  // data-container inner padding
        marktype: 'circle'
      };
    },



    initialize: function() {
      this.state = this.defaults();

      this.$el
        .css("background", "white")
        .attr('id', 'queryview-container');

      this.$svg = $("<svg id='viz'></svg>").prependTo(this.$el);
      this.svg = this.$svg.get()[0];
      this.d3svg = d3.select(this.svg);
      this.d3svg
        .attr('class', 'viz-container')
        .attr('width', this.state.w + this.state.lp)
        .attr('height', this.state.h + this.state.tp + this.state.bp);
      this.c = this.d3svg.append('g')
          .classed("plot-container", true)
          .attr('transform', "translate("+this.state.lp+", 0)");
      this.c.append('rect')
        .attr('width', this.state.w)
        .attr('height', this.state.h)
        .attr('fill', 'none')
        .attr('stroke', 'none')
        .style('pointer-events', 'all')


      this.listenTo(this.model, 'change:db', this.resetState);
      this.listenTo(this.model, 'change:table', this.resetState);
      this.listenTo(this.model, 'change:data', this.render);
    },

    resetState: function() {
      this.state = this.defaults();
      this.trigger("resetState");
    },

    onChange: function() {
      return;
    },

    // persistently update scales information
    setupScales: function(data) {
      var schema = this.model.get('schema'),
          xcol = this.model.get('x'),
          xalias = xcol.alias,
          ycols = this.model.get('ys'),
          yaliases = _.pluck(ycols, 'alias'),
          type = schema[xcol.col],
          _this = this,
          ip = this.state.ip,
          xs = _.pluck(data, xalias),
          prevxdomain = this.state.xdomain,
          prevydomain = this.state.ydomain,
          getx = function(d) { return d[xalias]; },
          xdomain = util.getXDomain(data, type, getx),
          ydomain = util.getYDomain(data, ycols),
          newCDomain = _.chain(this.state.cscales.domain())
            .union(yaliases)
            .compact().value();

      this.state.cscales.domain(newCDomain);
      this.state.xdomain = util.mergeDomain(this.state.xdomain, xdomain, type);
      this.state.ydomain = util.mergeDomain(this.state.ydomain, ydomain, 'num');

      if (!this.state.yscales)
        this.state.ydomain = util.mergeDomain(this.state.ydomain, [0, -Infinity], 'num');

      if (this.state.xscales == null) {
        var xscales = d3.scale.linear();
        if (util.isTime(type)) {
          xscales = d3.time.scale();
        }
        xscales.range([0+ip, this.state.w-ip]);

        if (util.isStr(type)) {
          xscales = d3.scale.ordinal();
          xscales.rangeRoundBands([0+ip, this.state.w-ip], 0.1);
        }
        this.state.xscales = xscales;
      }
      if (this.state.yscales == null) {
        this.state.yscales = d3.scale.linear()
          .range([this.state.h-ip, 0+ip])
      }
      
      this.state.xscales.domain(this.state.xdomain);
      this.state.yscales.domain(this.state.ydomain);

      if (!this.state.xaxis) {
        this.state.xaxis = d3.svg.axis()
          .scale(this.state.xscales)
          .tickSize(0,0)
          .orient('bottom')
        var nticks = util.estNumXTicks(this.state.xaxis, type, this.state.w);
        util.setAxisLabels(this.state.xaxis, type, nticks);
      }
      if (!this.state.yaxis) {
        this.state.yaxis = d3.svg.axis()
          .scale(this.state.yscales)
          .tickSize(0,0)
          .orient('left');
        if (this.state.ydomain[1] > 10000000) {
          this.state.yaxis.tickFormat(d3.format('.2e'))
        };
      }

    },

    renderAxes: function(el) {
      if(el.select('.xaxis').size() == 0) {
        var xel = el.append('g')
          .attr('class', 'axis x xaxis')
          .attr('transform', "translate(0,"+this.state.h+")");
        xel.append('rect')
          .attr('width', this.state.w)
          .attr('height', this.state.bp)
          //.attr('fill', 'none')
          //.attr('stroke', 'none')
          .style('pointer-events', 'all')
        xel.call(this.state.xaxis)
      } else {
        el.select('.axis.x').call(this.state.xaxis);
      }



      if (el.select('.yaxis').size() == 0) {
        var yel = el.append('g')
          .attr('class', 'axis y yaxis')
        yel.append('rect')
          .attr('width', this.state.lp)
          .attr('height', this.state.h)
          .attr('x', -this.state.lp)
          //.attr('fill', 'none')
          //.attr('stroke', 'none')
          .style('pointer-events', 'all')
        yel.call(this.state.yaxis)
      } else {
        el.select('.axis.y').call(this.state.yaxis);
      }



      if (el.select('.xaxis-label').size() == 0) {
        el.append('g')
          .classed('xaxis-label', true)
          .attr('transform', 'translate('+(this.state.w/2)+','+(this.state.h+25)+')')
          .append('text')
          .data([1])
          .text(this.model.get('x')['expr'])
      }

      if (el.select('.yaxis-label').size() == 0) {
        var txt = _.uniq(_.pluck(this.model.get('ys'), 'expr')).join(', ');
        el.append('g')
          .classed('yaxis-label', true)
          .attr('text-anchor', 'middle')
          .attr('transform', 'translate(-'+(this.state.lp-15)+','+(this.state.h/2)+') rotate(-90)')
          .append('text')
          .data([1])
          .text(txt);
      }

    },

    // set base plot to background and render
    // result with WHERE clause
    renderWhereOverlay: function(where) {
      if (!where || !where.length) {
        console.log(['qv.renderoverlay', 'canceloverlay']);
        this.cancelWhereOverlay();
        return;
      }
      if (_.isEqual(this.model.get('where'), where)) {
        console.log(['qv.renderoverlay', 'cached']);
        return;
      }

      this.overlayquery = query = new Query(this.model.toJSON());
      query.set('where', where);
      console.log(['qv.renderoverlay', JSON.stringify(where), where, query])

      query.fetch({
        data: {
          json: JSON.stringify(query.toJSON()),
          db: query.get('db')
        },
        context: this,
        success: (function(model, resp, opts) {
          console.log(['qv.fetch', 'success', resp.data]);
          this.renderModifiedData(resp.data);
        }).bind(this)
      });
      return this;
    },

    cancelWhereOverlay: function() {
      this.renderModifiedData(null);
      this.overlayquery = null;
      return this;
    },

    // renders the actual overlay
    renderModifiedData: function(data) {
      var _this = this;
      this.$(".updated").remove();
      this.c.selectAll('g.data-container')
        .classed('background', false)


      // expand the y-axis domain if necessary
      // relying on setupScales/renderAxes is too extreme because
      // it computes the union of all domains seen so far
      if (this.yzoom) {
        if (data) {
          var ydomain = this.state.yscales.domain(); //this.state.ydomain;
          ydomain = util.getYDomain(data, this.model.get('ys'))
            console.log(ydomain);
          ydomain = util.mergeDomain(this.state.yscales.domain(), ydomain, 'num')
            console.log(ydomain)
        } else {
          var ydomain = this.state.ydomain;
        }
        this.yzoom.y(this.state.yscales.domain(ydomain));
        this.yzoom.event(this.c);
      }
      if (this.state.yaxis)
        this.c.select('.yaxis').call(this.state.yaxis);

      if (!data) {
        return;
      }

      //this.setupScales(data);
      //this.render();
      
      this.c.selectAll('g.data-container')
        .classed('background', true)
      var xalias = this.model.get('x').alias;
      var el = this.c.append('g')
          .classed('updated', true)

      _.each(this.model.get('ys'), function(ycol) {
        this.renderData(el, data, xalias, ycol.alias);
      }, this);
    },

    renderData: function(el, data, xalias, yalias) {
      var _this = this,
          h = this.state.h,
          w = this.state.w;
      var data = _.map(data, function(d) {
        var ret = {
          x: d[xalias],
          y: d[yalias],
          px: _this.state.xscales(d[xalias]),
          py: _this.state.yscales(d[yalias]),
          yalias: yalias
        };
        ret[xalias] = d[xalias];
        ret[yalias] = d[yalias];
        return ret
      });

      var dc = el.append('g')
        .attr('class', 'data-container')

      if (this.state.marktype == 'circle') {
        var r = 2.5;
        if (data.length > 50) 
          r = 2.25;
        if (data.length > 100)
          r = 2;
        dc.selectAll('circle')
            .data(data)
          .enter().append('circle')
            .classed('mark', true)
            .attr({
              cx: function(d) { return d.px },
              cy: function(d) { return d.py },
              r: r,
              fill: this.state.cscales(yalias),
              stroke: this.state.cscales(yalias),
              opacity: function(d) {
                if (d.px >= 0 && d.px <= w &&
                    d.py >= 0 && d.py <= h)
                  return 1;
                return 0;
              }
            })
      }
    },

    renderBrush: function(el) {
      if (el.select('.brush').size() > 0) {
        console.log(['qv.renderBrush', 'exists, skip']);
        return;
      }
      var type = this.model.get('schema')[this.model.get('x').col],
          _this = this,
          xscales = this.state.xscales,
          yscales = this.state.yscales,
          xr = 5,
          yr = Math.abs(yscales.invert(0)-yscales.invert(5));

      var brushf = function(p) {
        var e = brush.extent()
        var selected = {};
        el.selectAll('.data-container:not(.background)')
            .selectAll('.mark')
          .classed('selected', function(d){
            if (util.isNum(type) || util.isTime(type)) {
              var minx = xscales(e[0][0]),
                  maxx = xscales(e[1][0]),
                  x    = d.px;
            } else {
              var minx = e[0][0],
                  maxx = e[1][0],
                  x    = d.px;
            }

            var y = d.y,
                bx = minx <= x+xr && maxx >= x-xr,
                by = e[0][1] <= y+yr && e[1][1] >= y-yr;

            if (bx && by) {
              var yalias = d.yalias;
              if (!selected[yalias]) selected[yalias] = [];
              selected[yalias].push(d);
              return true;
            }
            return false;
          })
        if (d3.event.type == 'brushend') {
          _this.trigger('change:selection', selected);
        }
      }

      this.d3brush = brush = d3.svg.brush()
          .x(this.state.xscales)
          .y(this.state.yscales)
          .on('brush', brushf)
          .on('brushend', brushf)
          .on('brushstart', brushf)
      this.gbrush = gbrush = el.append('g')
          .attr('class', 'brush')
          .call(brush)
      gbrush.selectAll('rect')
          .attr('height', this.state.h)
    },

    disableBrush: function() {
      if (this.gbrush)
        this.gbrush.style("pointer-events", null);
    },
    enableBrush: function() {
      if (this.gbrush)
        this.gbrush.style("pointer-events", 'all');
    },

    brushStatus: function() {
      if (this.gbrush)
        return this.gbrush.style("pointer-events");
    },


    renderZoom: function(el) {
      var _this = this
          yscales = this.state.yscales,
          yaxis = this.state.yaxis,
          xscales = this.state.xscales,
          xaxis = this.state.xaxis;

      var yzoomf = function(el) {
        var yaxis = this.state.yaxis;
        var yscales = this.state.yscales;
        el.select('.axis.y').call(yaxis);
        el.selectAll('.mark')
          .attr('cy', function(d) {
            return yscales(d.y);
          })
          .style('opacity', function(d) {
            if (yscales.range()[0] >= yscales(d.y) && 
                yscales(d.y) >= yscales.range()[1])
              return 1;
            return 0;
          })
      };
      yzoomf = _.bind(yzoomf, this, el);

      this.yzoom = yzoom = d3.behavior.zoom()
        .y(this.state.yscales)
        .on('zoom', yzoomf);

      el.select('.axis.y').call(yzoom)
        .style('cursor', 'ns-resize')

        
      var yshiftf = function(el, yzoomf) {
        var yzoom = this.yzoom;
        var yStart = d3.event.y;
        var curYScale = yzoom.scale();
        var yscales = this.state.yscales;

        if (d3.event.shiftKey) {
          d3.select('body')
            .on('mousemove.qvy', function() {
              var diff = ((-d3.event.y + yStart) / 100);
              if (diff >= 0) { 
                diff += 1.0; 
              } else { 
                diff = 1.0 / (Math.abs(diff)+1);
              }
              yzoom.scale(diff*curYScale);
            })
            .on('mouseup.qvy', function() {
              d3.select('body')
                .on('mousemove.qvy', null)
                .on('mouseup.qvy', null);

            });
        }
      }
      yshiftf = _.bind(yshiftf, this, el, yzoomf);

      el.select('.yaxis')
        .on('mousedown.qvy', yshiftf)




      var type = this.model.get('schema')[this.model.get('x').col];

      if (!util.isStr(type)) {

        function xzoomf(el) {
          var xaxis = this.state.xaxis;
          var xscales = this.state.xscales;
          el.select('.axis.x').call(xaxis);
          el.selectAll('.mark')
            .attr('cx', function(d) {
              return xscales(d.x);
            })
            .style('opacity', function(d) {
              if (xscales.range()[0] <= xscales(d.x) && 
                  xscales(d.x) <= xscales.range()[1])
                return 1;
              return 0;
            })
          _this.xscale = d3.event.scale;
        };
        xzoomf = _.bind(xzoomf, this, el);


        this.xzoom = xzoom = d3.behavior.zoom()
          .x(this.state.xscales)
          .on('zoom', xzoomf);

        el.select('.axis.x').call(xzoom)
          .style('cursor', 'ew-resize');


        var xStart = null;
        var curXScale = null;
        el.select('.xaxis')
          .on('mousedown.qvx', function() {
            if (d3.event.shiftKey) {
              xStart = d3.event.x;
              curXScale = xzoom.scale();
              d3.select('body')
                .on('mousemove.qvx', function() {
                  var diff = ((d3.event.x - xStart) / 100);
                  if (diff >= 0) { 
                    diff += 1.0; 
                  } else if (diff < 0) { 
                    diff = 1.0 / (Math.abs(diff)+1);
                  }
                  console.log(diff)
                  xzoom.scale(diff*curXScale);

                })
                .on('mouseup.qvx', function() {
                  d3.select('body')
                    .on('mousemove.qvx', null)
                    .on('mouseup.qvx', null);
                });
            }
          })


      }
    },

    renderLabels: function() {
      var ys = this.model.get('ys'),
          cscales = this.state.cscales;

      this.$('.legend').remove();
      d3.select(this.el).append("div")
          .attr('class', 'legend')
          .style("margin-left", this.state.lp)
        .selectAll("span")
          .data(ys)
        .enter().append("span")
          .text(function(d) { return d.expr; })
          .style("color", function(d) { return cscales(d.alias); })
          .style("border-bottom", function(d) { return "5px solid " + cscales(d.alias); })
          .style("padding-bottom", function(d) { return "2px"; })
          .style("border-radius", function(d) { return "0px"; });
    },

    toggleBrushDrawing: function() {
      if (!this.dv) {
        this.enableBrush();
        return;
      }

      if (this.dv.status() == 'all') {
        this.dv.disable();
        this.enableBrush();
      } else {
        this.dv.enable();
        this.disableBrush();
      }
    },


    render: function() {
      if (!this.model.isValid()) {
        this.$svg.hide();
        return this;
      }
      this.$svg.show();

      if (!this.state.xaxis) {
        $(this.c[0]).empty();
      } else {
        this.$('.data-container').remove()
      }

      this.setupScales(this.model.get('data'))
      this.renderAxes(this.c)
      _.each(this.model.get('ys'), function(ycol) {
        this.renderData(
          this.c, 
          this.model.get('data'),
          this.model.get('x').alias, 
          ycol.alias
        );
      }, this);
      if (window.enableScorpion) {
        this.renderBrush(this.c);
      }
      this.renderLabels(this.c);
      this.renderZoom(this.c);

      
      this.$(".drawing-container").remove()
      this.dv = new DrawingView({state: this.state});
      this.listenTo(this.dv, "change:drawing", (function() {
        this.trigger('change:drawing', this.dv.model);
      }).bind(this))
      $(this.c[0]).append(this.dv.render().$("g"));
      this.dv.disable();
      this.enableBrush();


      return this;
    }

  });

  return QueryView;
})