Jump To …
backend.ckan.js backend.couchdb.js backend.csv.js backend.dataproxy.js backend.elasticsearch.js backend.gdocs.js backend.memory.js data.transform.js ecma-fixes.js model.js view.graph.js view.grid.js view.map.js view.multiview.js view.slickgrid.js view.timeline.js view.transform.js widget.facetviewer.js widget.fields.js widget.filtereditor.js widget.pager.js widget.queryeditor.js

# view.graph.js |
--- | ---
¶ |
    /*jshint multistr:true */

    this.recline = this.recline || {};
    this.recline.View = this.recline.View || {};

    (function($, my) {
¶ ## Graph view for a Dataset using Flot graphing library. Initialization arguments (in a hash in first parameter): model: recline.Model.Dataset state: (optional) configuration hash of form: { group: {column name for x-axis}, series: [{column name for series A}, {column name series B}, ... ], graphType: 'line' } NB: should not provide an el argument to the view but must let the view generate the element itself (you can then append view.el to the DOM. |
    my.Graph = Backbone.View.extend({
      template: ' \
        <div class="recline-graph"> \
          <div class="panel graph" style="display: block;"> \
            <div class="js-temp-notice alert alert-block"> \
              <h3 class="alert-heading">Hey there!</h3> \
              <p>There\'s no graph here yet because we don\'t know what fields you\'d like to see plotted.</p> \
              <p>Please tell us by <strong>using the menu on the right</strong> and a graph will automatically appear.</p> \
            </div> \
          </div> \
        </div> \
    ',

      initialize: function(options) {
        var self = this;
        this.graphColors = ["#edc240", "#afd8f8", "#cb4b4b", "#4da74d", "#9440ed"];

        this.el = $(this.el);
        _.bindAll(this, 'render', 'redraw');
        this.needToRedraw = false;
        this.model.bind('change', this.render);
        this.model.fields.bind('reset', this.render);
        this.model.fields.bind('add', this.render);
        this.model.records.bind('add', this.redraw);
        this.model.records.bind('reset', this.redraw);
        var stateData = _.extend({
            group: null,
¶ so that at least one series chooser box shows up |
            series: [],
            graphType: 'lines-and-points'
          },
          options.state
        );
        this.state = new recline.Model.ObjectState(stateData);
        this.editor = new my.GraphControls({
          model: this.model,
          state: this.state.toJSON()
        });
        this.editor.state.bind('change', function() {
          self.state.set(self.editor.state.toJSON());
          self.redraw();
        });
        this.elSidebar = this.editor.el;
      },

      render: function() {
        var self = this;
        var tmplData = this.model.toTemplateJSON();
        var htmls = Mustache.render(this.template, tmplData);
        $(this.el).html(htmls);
        this.$graph = this.el.find('.panel.graph');
        return this;
      },

      redraw: function() {
¶ There appear to be issues generating a Flot graph if either: |
¶ The relevant div that graph attaches to his hidden at the moment of creating the plot -- Flot will complain with Uncaught Invalid dimensions for plot, width = 0, height = 0 There is no data for the plot -- either same error or may have issues later with errors like 'non-existent node-value' |
        var areWeVisible = !jQuery.expr.filters.hidden(this.el[0]);
        if ((!areWeVisible || this.model.records.length === 0)) {
          this.needToRedraw = true;
          return;
        }
¶ check we have something to plot |
        if (this.state.get('group') && this.state.get('series')) {
¶ faff around with width because flot draws axes outside of the element width which means graph can get push down as it hits element next to it |
          this.$graph.width(this.el.width() - 20);
          var series = this.createSeries();
          var options = this.getGraphOptions(this.state.attributes.graphType);
          this.plot = Flotr.draw(this.$graph.get(0), series, options);
        }
      },

      show: function() {
¶ because we cannot redraw when hidden we may need to when becoming visible |
        if (this.needToRedraw) {
          this.redraw();
        }
      },
¶ ### getGraphOptions Get options for Flot Graph needs to be function as can depend on state @param typeId graphType id (lines, lines-and-points etc) |
      getGraphOptions: function(typeId) {
        var self = this;

        var tickFormatter = function (x) {
          return getFormattedX(x);
        };
¶ infoboxes on mouse hover on points/bars etc |
        var trackFormatter = function (obj) {
          var x = obj.x;
          var y = obj.y;
¶ it's horizontal so we have to flip |
          if (self.state.attributes.graphType === 'bars') {
            var _tmp = x;
            x = y;
            y = _tmp;
          }

          x = getFormattedX(x);

          var content = _.template('<%= group %> = <%= x %>, <%= series %> = <%= y %>', {
            group: self.state.attributes.group,
            x: x,
            series: obj.series.label,
            y: y
          });

          return content;
        };

        var getFormattedX = function (x) {
          var xfield = self.model.fields.get(self.state.attributes.group);
¶ time series |
          var xtype = xfield.get('type');
          var isDateTime = (xtype === 'date' || xtype === 'date-time' || xtype  === 'time');

          if (self.model.records.models[parseInt(x)]) {
            x = self.model.records.models[parseInt(x)].get(self.state.attributes.group);
            if (isDateTime) {
              x = new Date(x).toLocaleDateString();
            }
          } else if (isDateTime) {
            x = new Date(parseInt(x)).toLocaleDateString();
          }
          return x;
        }

        var xaxis = {};
        xaxis.tickFormatter = tickFormatter;

        var yaxis = {};
        yaxis.autoscale = true;
        yaxis.autoscaleMargin = 0.02;

        var mouse = {};
        mouse.track = true;
        mouse.relative = true;
        mouse.trackFormatter = trackFormatter;

        var legend = {};
        legend.position = 'ne';
¶ mouse.lineColor is set in createSeries |
        var optionsPerGraphType = {
          lines: {
            legend: legend,
            colors: this.graphColors,
            lines: { show: true },
            xaxis: xaxis,
            yaxis: yaxis,
            mouse: mouse
          },
          points: {
            legend: legend,
            colors: this.graphColors,
            points: { show: true, hitRadius: 5 },
            xaxis: xaxis,
            yaxis: yaxis,
            mouse: mouse,
            grid: { hoverable: true, clickable: true }
          },
          'lines-and-points': {
            legend: legend,
            colors: this.graphColors,
            points: { show: true, hitRadius: 5 },
            lines: { show: true },
            xaxis: xaxis,
            yaxis: yaxis,
            mouse: mouse,
            grid: { hoverable: true, clickable: true }
          },
          bars: {
            legend: legend,
            colors: this.graphColors,
            lines: { show: false },
            xaxis: yaxis,
            yaxis: xaxis,
            mouse: {
              track: true,
              relative: true,
              trackFormatter: trackFormatter,
              fillColor: '#FFFFFF',
              fillOpacity: 0.3,
              position: 'e'
            },
            bars: {
              show: true,
              horizontal: true,
              shadowSize: 0,
              barWidth: 0.8
            }
          },
          columns: {
            legend: legend,
            colors: this.graphColors,
            lines: { show: false },
            xaxis: xaxis,
            yaxis: yaxis,
            mouse: {
                track: true,
                relative: true,
                trackFormatter: trackFormatter,
                fillColor: '#FFFFFF',
                fillOpacity: 0.3,
                position: 'n'
            },
            bars: {
                show: true,
                horizontal: false,
                shadowSize: 0,
                barWidth: 0.8
            }
          },
          grid: { hoverable: true, clickable: true }
        };
        return optionsPerGraphType[typeId];
      },

      createSeries: function() {
        var self = this;
        var series = [];
        _.each(this.state.attributes.series, function(field) {
          var points = [];
          _.each(self.model.records.models, function(doc, index) {
            var xfield = self.model.fields.get(self.state.attributes.group);
            var x = doc.getFieldValue(xfield);
¶ time series |
            var xtype = xfield.get('type');
            var isDateTime = (xtype === 'date' || xtype === 'date-time' || xtype  === 'time');

            if (isDateTime) {
¶ datetime |
              if (self.state.attributes.graphType != 'bars' && self.state.attributes.graphType != 'columns') {
¶ not bar or column |
                x = new Date(x).getTime();
              } else {
¶ bar or column |
                x = index;
              }
            } else if (typeof x === 'string') {
¶ string |
              x = parseFloat(x);
              if (isNaN(x)) {
                x = index;
              }
            }

            var yfield = self.model.fields.get(field);
            var y = doc.getFieldValue(yfield);
¶ horizontal bar chart |
            if (self.state.attributes.graphType == 'bars') {
              points.push([y, x]);
            } else {
              points.push([x, y]);
            }
          });
          series.push({data: points, label: field, mouse:{lineColor: self.graphColors[series.length]}});
        });
        return series;
      }
    });

    my.GraphControls = Backbone.View.extend({
      className: "editor",
      template: ' \
      <div class="editor"> \
        <form class="form-stacked"> \
          <div class="clearfix"> \
            <label>Graph Type</label> \
            <div class="input editor-type"> \
              <select> \
              <option value="lines-and-points">Lines and Points</option> \
              <option value="lines">Lines</option> \
              <option value="points">Points</option> \
              <option value="bars">Bars</option> \
              <option value="columns">Columns</option> \
              </select> \
            </div> \
            <label>Group Column (x-axis)</label> \
            <div class="input editor-group"> \
              <select> \
              <option value="">Please choose ...</option> \
              {{#fields}} \
              <option value="{{id}}">{{label}}</option> \
              {{/fields}} \
              </select> \
            </div> \
            <div class="editor-series-group"> \
            </div> \
          </div> \
          <div class="editor-buttons"> \
            <button class="btn editor-add">Add Series</button> \
          </div> \
          <div class="editor-buttons editor-submit" comment="hidden temporarily" style="display: none;"> \
            <button class="editor-save">Save</button> \
            <input type="hidden" class="editor-id" value="chart-1" /> \
          </div> \
        </form> \
      </div> \
    ',
      templateSeriesEditor: ' \
        <div class="editor-series js-series-{{seriesIndex}}"> \
          <label>Series <span>{{seriesName}} (y-axis)</span> \
            [<a href="#remove" class="action-remove-series">Remove</a>] \
          </label> \
          <div class="input"> \
            <select> \
            {{#fields}} \
            <option value="{{id}}">{{label}}</option> \
            {{/fields}} \
            </select> \
          </div> \
        </div> \
      ',
      events: {
        'change form select': 'onEditorSubmit',
        'click .editor-add': '_onAddSeries',
        'click .action-remove-series': 'removeSeries'
      },

      initialize: function(options) {
        var self = this;
        this.el = $(this.el);
        _.bindAll(this, 'render');
        this.model.fields.bind('reset', this.render);
        this.model.fields.bind('add', this.render);
        this.state = new recline.Model.ObjectState(options.state);
        this.render();
      },

      render: function() {
        var self = this;
        var tmplData = this.model.toTemplateJSON();
        var htmls = Mustache.render(this.template, tmplData);
        this.el.html(htmls);
¶ set up editor from state |
        if (this.state.get('graphType')) {
          this._selectOption('.editor-type', this.state.get('graphType'));
        }
        if (this.state.get('group')) {
          this._selectOption('.editor-group', this.state.get('group'));
        }
¶ ensure at least one series box shows up |
        var tmpSeries = [""];
        if (this.state.get('series').length > 0) {
          tmpSeries = this.state.get('series');
        }
        _.each(tmpSeries, function(series, idx) {
          self.addSeries(idx);
          self._selectOption('.editor-series.js-series-' + idx, series);
        });
        return this;
      },
¶ Private: Helper function to select an option from a select list |
      _selectOption: function(id,value){
        var options = this.el.find(id + ' select > option');
        if (options) {
          options.each(function(opt){
            if (this.value == value) {
              $(this).attr('selected','selected');
              return false;
            }
          });
        }
      },

      onEditorSubmit: function(e) {
        var select = this.el.find('.editor-group select');
        var $editor = this;
        var $series  = this.el.find('.editor-series select');
        var series = $series.map(function () {
          return $(this).val();
        });
        var updatedState = {
          series: $.makeArray(series),
          group: this.el.find('.editor-group select').val(),
          graphType: this.el.find('.editor-type select').val()
        };
        this.state.set(updatedState);
      },
¶ Public: Adds a new empty series select box to the editor. @param [int] idx index of this series in the list of series Returns itself. |
      addSeries: function (idx) {
        var data = _.extend({
          seriesIndex: idx,
          seriesName: String.fromCharCode(idx + 64 + 1)
        }, this.model.toTemplateJSON());

        var htmls = Mustache.render(this.templateSeriesEditor, data);
        this.el.find('.editor-series-group').append(htmls);
        return this;
      },

      _onAddSeries: function(e) {
        e.preventDefault();
        this.addSeries(this.state.get('series').length);
      },
¶ Public: Removes a series list item from the editor. Also updates the labels of the remaining series elements. |
      removeSeries: function (e) {
        e.preventDefault();
        var $el = $(e.target);
        $el.parent().parent().remove();
        this.onEditorSubmit();
      }
    });

    })(jQuery, recline.View);