summaryrefslogtreecommitdiff
path: root/horizon/static/horizon/js/horizon.d3linechart.js
blob: d2dba820284598135f408dcc3fc8a262b295439a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
/**
 * Licensed under the Apache License, Version 2.0 (the "License"); you may
 * not use this file except in compliance with the License. You may obtain
 * a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations
 * under the License.
 */

/* global Rickshaw */
/*
  Draw line chart in d3.

  To use, a div is required with the data attributes
  data-chart-type="line_chart", data-data.

  data-chart-type - REQUIRED(string) must be "line_chart" so chart gets initialized
  data-data - REQUIRED(string or json):
    string: url for the json data for the chart
    json: json stringified object for the chart data
  data-form-selector - Optional(string) jQuery selector of Forms that controls this chart
  data-legend-selector - Optional(string) jQuery selector of div element that will display legend
  data-smoother-selector - Optional(string) jQuery selector of TODO(lsmola)
  data-slider-selector - Optional(string) jQuery selector of TODO(lsmola)


  If used in popup, initialization must be made manually e.g.:

  if (typeof horizon.d3_line_chart !== 'undefined') {
    horizon.d3_line_chart.init("div[data-chart-type='line_chart']");
  }


  Example:
  <div id="line_chart"
    data-chart-type="line_chart"
    data-data="{% url 'horizon:admin:metering:samples'%}"
    data-form-selector='#linechart_general_form'>
  </div>
  <div id="linea_chart2"
    data-chart-type="line_chart"
    data-data="{% url 'horizon:admin:metering:samples'%}?query=not_popular_data"
    data-form-selector='#linechart_general_form'>
  </div>


  The data format example:
  Url has to return JSON in format:
  {
    "series": [
      {
        "name": "instance-00000005",
        "data": [
          {"y": 171, "x": "2013-08-21T11:22:25"},
          {"y": 172, "x": "2013-08-21T11:22:26"}
        ]
      }, {
        "name": "instance-00000006",
        "data": [
          {"y": 161, "x": "2013-08-21T11:22:25"},
          {"y": 162, "x": "2013-08-21T11:22:26"}
        ]
      }
    ],
    "settings": {}
  }

  Example of line-bar chart sparkline:

  <div class="overview_chart">
    <div class="chart_container">
      <div class="chart"
        data-chart-type="line_chart"
        data-data="/admin/samples?meter=test2"
        data-form-selector='#linechart_general_form'>
      </div>
    </div>
    <div class="bar_chart_container">
      <div class="chart" data-chart-type="overview_bar_chart">
      </div>
    </div>
  </div>

  {
    "series": [
      {
        "name": "instance-00000005",
        "data": [
          {"y": 171, "x": "2013-08-21T11:22:25"},
          {"y": 172, "x": "2013-08-21T11:22:26"}
        ]
      }, {
        "name": "instance-00000006",
        "data": [
          {"y": 161, "x": "2013-08-21T11:22:25"},
          {"y": 162, "x": "2013-08-21T11:22:26"}
        ]
      }
    ],
    "settings": {
      'renderer': 'StaticAxes',
      'yMin': 0,
      'yMax': 100,
      'higlight_last_point': True,
      "auto_size": False, 'auto_resize': False,
      "axes_x" : False, "axes_y" : False,
      'bar_chart_settings': {
        'orientation': 'vertical',
        'used_label_placement': 'left',
        'width': 30,
        'color_scale_domain': [0, 80, 80, 100],
        'color_scale_range': ['#00FE00', '#00FF00', '#FE0000', '#FF0000'],
        'average_color_scale_domain': [0, 100],
        'average_color_scale_range': ['#0000FF', '#0000FF']
      }
    },
    "stats": {
      'average': 20,
      'used': 30,
      'tooltip_average': tooltip_average
    }
  }


  The control Forms:
  There are currently 2 form elements that can be connected to charts and act
  as controls. Elements listen for change event and refresh the chart on change event.
  Chart can be connected to multiple forms via selector, all for data will be sent on
  change.
  Form can be connected to multiple charts, all charts will be refreshed when form
  element changes.
  The firsts rendering of the chart takes data from the connected Forms.

  1. Selectbox
  The data attribute 'data-line-chart-command="select_box_change' needs to be defined on
  select element.

  Example:
  <form class="form-horizontal"
    id="linechart_general_form">

    <div class="form-group">
      <label for="meter" class="control-label col-sm-2">{% trans "Metric" %}:&nbsp;</label>
      <div class="col-sm-10">
        <select data-line-chart-command="select_box_change"
          name="meter" id="meter" class="col-sm-w form-control example">
          {% for meter in meters %}
            <option value="{{ meter }}" data-unit="{{ meter }}">
              {{ meter }}
            </option>
          {% endfor %}
        </select>
      </div>
    </div>
  </form>

  2. Date picker
  The data attribute 'data-line-chart-command="date_picker_change"' needs to be defined on
  input element.

  Example:
  <form class="form-horizontal"
    id="linechart_general_form">
    <div class="form-group" id="date_from">
      <label for="date_from" class="control-label col-sm-2">{% trans "From" %}:&nbsp;</label>
      <div class="col-sm-10">
        <input data-line-chart-command="date_picker_change"
          type="text" id="date_from" name="date_from" class="form-control example"/>
      </div>
    </div>
  </form>

 */

Rickshaw.namespace('Rickshaw.Graph.Renderer.StaticAxes');
Rickshaw.Graph.Renderer.StaticAxes = Rickshaw.Class.create(Rickshaw.Graph.Renderer.Line, {
  name: 'StaticAxes',
  defaults: function($super) {
    return Rickshaw.extend($super(), {
      xMin: undefined,
      xMax: undefined,
      yMin: undefined,
      yMax: undefined
    });
  },
  domain: function($super) {
    var ret = $super();
    var xMin, xMax;
    // If y axis wants to have static range, not based on data
    if (this.yMin !== undefined && this.yMax !== undefined){
      ret.y = [this.yMin, this.yMax];
    }
    // If x axis wants to have static range, not based on data
    if (this.xMin !== undefined && this.xMax !== undefined){
      xMin = d3.time.format.utc('%Y-%m-%dT%H:%M:%S').parse(this.xMin);
      xMin = xMin.getTime() / 1000;
      xMax = d3.time.format.utc('%Y-%m-%dT%H:%M:%S').parse(this.xMax);
      xMax = xMax.getTime() / 1000;

      ret.x = [xMin, xMax];
    }
    return ret;
  }
});

horizon.d3_line_chart = {
  /**
   * A class representing the line chart
   * @param chart_module A context of horizon.d3_line_chart module.
   * @param html_element A html_element containing the chart.
   * @param settings An object containing settings of the chart.
   */
  LineChart: function(chart_module, html_element, settings){
    var self = this;
    var jquery_element = $(html_element);

    self.chart_module = chart_module;
    self.html_element = html_element;
    self.jquery_element = jquery_element;
    self.$spinner = horizon.loader.inline(gettext('Loading')).hide().appendTo(jquery_element);

    /************************************************************************/
    /*********************** Initialization methods *************************/
    /************************************************************************/
    /**
     * Initialize object
     */
    self.init = function() {
      var self = this;
      /* TODO(lsmola) make more configurable init from more sources */
      self.legend_element = $(jquery_element.data('legend-selector')).get(0);
      self.slider_element = $(jquery_element.data('slider-selector')).get(0);

      // Set the data, undefined if it doesn't exist
      self.data = jquery_element.data('data');
      self.url_parameters = jquery_element.data('url_parameters');

      if (typeof self.data === 'string') {
        self.final_url = self.data;
        if (jquery_element.data('form-selector')){
          $(jquery_element.data('form-selector')).each(function(){
            // Add serialized data from all connected forms to url.
            if (self.final_url.indexOf('?') > -1){
              self.final_url += '&' + $(this).serialize();
            } else {
              self.final_url += '?' + $(this).serialize();
            }
          });
        }
      }

      self.color = d3.scale.category10();

      // Self aggregation and statistic attrs
      self.stats = {};
      self.stats.average = 0;
      self.stats.last_value = 0;

      // Load initial settings.
      self.init_settings(settings);
      // Get correct size of chart and the wrapper.
      self.get_size();
    };
    /**
     * Initialize settings of the chart with default values, then applies
     * defined settings of the chart. Settings are obtained either from JSON
     * of the html attribute data-settings, or from init of the charts. The
     * highest priority settings are obtained directly from the JSON data
     * obtained from the server.
     * @param settings An object containing settings of the chart.
     */
    self.init_settings = function(settings) {
      var self = this;

      self.settings = {};
      self.settings.renderer = 'line';
      self.settings.auto_size = true;
      self.settings.axes_x = true;
      self.settings.axes_y = true;
      self.settings.axes_y_label = true;
      self.settings.interpolation = 'linear';
      // Static y axes values
      self.settings.yMin = undefined;
      self.settings.yMax = undefined;
      // Static x axes values
      self.settings.xMin = undefined;
      self.settings.xMax = undefined;
      // Show last point as dot
      self.settings.higlight_last_point = false;

      // Composed charts wrapper
      self.settings.composed_chart_selector = '.overview_chart';
      // Bar chart component
      self.settings.bar_chart_selector = 'div[data-chart-type="overview_bar_chart"]';
      self.settings.bar_chart_settings = undefined;

      // allowed: verbose
      self.hover_formatter = 'verbose';

      /*
        Applying settings. The later application rewrites the previous
        therefore it has bigger priority.
      */

      // Settings defined in the init method of the chart
      if (settings){
        self.apply_settings(settings);
      }

      // Settings defined in the html data-settings attribute
      if (self.jquery_element.data('settings')){
        var inline_settings = self.jquery_element.data('settings');
        self.apply_settings(inline_settings);
      }
    };

    /**
     * Applies passed settings to the chart object. Allowed settings are
     * listed in this method.
     * @param settings An object containing settings of the chart.
     */
    self.apply_settings = function(settings){
      var self = this;

      var allowed_settings = ['renderer', 'auto_size', 'axes_x', 'axes_y',
        'interpolation', 'yMin', 'yMax', 'xMin', 'xMax', 'bar_chart_settings',
        'bar_chart_selector', 'composed_chart_selector',
        'higlight_last_point', 'axes_y_label'];

      jQuery.each(allowed_settings, function(index, setting_name) {
        if (settings[setting_name] !== undefined){
          self.settings[setting_name] = settings[setting_name];
        }
      });
    };

    /**
     * Computes size of the chart from surrounding divs. When
     * settings.auto_size is on, it stretches the chart to bottom of
     * the screen.
     */
    self.get_size = function(){
      /*
        The height will be determined by css or window size,
        I have to hide everything inside that could mess with
        the size, so it is fully determined by outer CSS.
      */
      $(self.html_element).css('height', '');
      $(self.html_element).css('width', '');
      var svg = $(self.html_element).find('svg');
      svg.hide();

      /*
        Width and height of the chart will be taken from chart wrapper,
        that can be styled by css.
      */
      self.width = jquery_element.width();

      // Set either the minimal height defined by CSS.
      self.height = jquery_element.height();
      /*
        Or stretch it to the remaining height of the window if there
        is a place. + some space on the bottom, lets say 30px.
      */
      if (self.settings.auto_size) {
        var auto_height = $(window).height() - jquery_element.offset().top - 30;
        if (auto_height > self.height) {
          self.height = auto_height;
        }
      }

      /* Setting new sizes. It is important when resizing a window.*/
      $(self.html_element).css('height', self.height);
      $(self.html_element).css('width', self.width);
      svg.show();
      svg.css('height', self.height);
      svg.css('width', self.width);
    };

    /************************************************************************/
    /****************************** Initialization **************************/
    /************************************************************************/
    // Init of the object
    self.init();

    /************************************************************************/
    /****************************** Error Message ***************************/
    /************************************************************************/
    // Load the data
    self.error_message = function(error) {
      $(self.html_element).html(error);
      $(self.legend_element).empty();
      // Setting a fix height breaks things when legend is getting
      // bigger.
      $(self.legend_element).css('height', '');
      // FIXME add proper fail message
      horizon.toast.add('error',
        gettext('An error occurred. Please try again later.'));
    };

    /************************************************************************/
    /******************************** Data Load *****************************/
    /************************************************************************/
    // Load the data
    self.load_data = function(data) {

      // Clearing the old chart data.
      self.jquery_element.empty();
      $(self.legend_element).empty();

      self.series = data.series;
      self.stats = data.stats;
      // The highest priority settings are sent with the data.
      self.apply_settings(data.settings);

      if (self.series.length <= 0) {
        $(self.html_element).html(gettext('No data available.'));
        $(self.legend_element).empty();
        // Setting a fix height breaks things when legend is getting
        // bigger.
        $(self.legend_element).css('height', '');
      } else {
        self.render();
      }
    };

    /************************************************************************/
    /****************************** Methods *********************************/
    /************************************************************************/
    /**
     * Obtains the actual chart data and renders the chart again.
     */
    self.refresh = function (){
      var self = this;

      self.start_loading();
      if (typeof self.data === 'string') {
        horizon.ajax.queue({
          url: self.final_url,
          success: function (data) {
            self.load_data(data);
          },
          error: function () {
            self.error_message(gettext('No data available.'));
          },
          complete: function () {
            self.finish_loading();
          }
        });
      } else if (self.data) {
        self.load_data(self.data);
        self.finish_loading();
      } else {
        self.error_message(gettext('No data available.'));
      }
    };

    /**
     * Renders the chart using Rickshaw library.
     */
    self.render = function(){
      var self = this;
      var last_point, last_point_color;

      $.map(self.series, function (serie) {
        serie.color = last_point_color = self.color(serie.name);
        $.map(serie.data, function (statistic) {
          // need to parse each date
          statistic.x = d3.time.format.utc('%Y-%m-%dT%H:%M:%S').parse(statistic.x);
          statistic.x = statistic.x.getTime() / 1000;
          last_point = statistic;
          last_point.color = serie.color;
        });
      });

      var renderer = self.settings.renderer;
      if (renderer === 'StaticAxes'){
        renderer = Rickshaw.Graph.Renderer.StaticAxes;
      }

      // clean the old graph the messy way
      // TODO(lsmola) clena when the Rickshaw issue is gone
      // https://github.com/shutterstock/rickshaw/issues/432
      self.jquery_element.empty();

      var $newGraph = self.jquery_element.clone();
      self.jquery_element.replaceWith($newGraph);
      self.jquery_element = $newGraph;
      self.html_element = self.jquery_element[0];

      // instantiate our graph!
      var graph = new Rickshaw.Graph({
        element: self.html_element,
        width: self.width,
        height: self.height,
        renderer: renderer,
        series: self.series,
        yMin: self.settings.yMin,
        yMax: self.settings.yMax,
        xMin: self.settings.xMin,
        xMax: self.settings.xMax,
        interpolation: self.settings.interpolation
      });

      /*
        TODO(lsmola) add jQuery UI slider to make this work
        if (self.slider_element) {
          var slider = new Rickshaw.Graph.RangeSlider({
            graph: graph,
            element: $(self.slider_element)
          });
        }
      */
      graph.render();

      if (self.hover_formatter === 'verbose'){
        new Rickshaw.Graph.HoverDetail({
          graph: graph,
          formatter: function(series, x, y) {
            if (y % 1 === 0) {
              y = parseInt(y, 10);
            } else {
              y = parseFloat(y).toFixed(2);
            }

            var d = new Date(x * 1000);
            // Convert datetime to YYYY-MM-DD HH:MM:SS GMT
            var datetime_string = d.getUTCFullYear() + "-" +
              ("00" + (d.getUTCMonth() + 1)).slice(-2) + "-" +
              ("00" + d.getUTCDate()).slice(-2) + " " +
              ("00" + d.getUTCHours()).slice(-2) + ":" +
              ("00" + d.getUTCMinutes()).slice(-2) + ":" +
              ("00" + d.getUTCSeconds()).slice(-2) + " GMT";

            var date = '<span class="date">' + datetime_string + '</span>';
            var swatch = '<span class="detail_swatch" style="background-color: ' + series.color + '"></span>';
            return swatch + series.name + ': ' + y + ' ' + series.unit + '<br>' + date;
          }
        });
      }

      if (self.legend_element) {
        var legend = new Rickshaw.Graph.Legend({
          graph: graph,
          element:  self.legend_element
        });

        new Rickshaw.Graph.Behavior.Series.Toggle({
          graph: graph,
          legend: legend
        });

        new Rickshaw.Graph.Behavior.Series.Order({
          graph: graph,
          legend: legend
        });

        new Rickshaw.Graph.Behavior.Series.Highlight({
          graph: graph,
          legend: legend
        });
      }
      if (self.settings.axes_x) {
        var axes_x = new Rickshaw.Graph.Axis.Time({
          graph: graph
        });
        axes_x.render();
      }
      if (self.settings.axes_y) {
        var axes_y_settings = {
          graph: graph
        };
        if (!self.settings.axes_y_label){
          // hiding label of Y axis if setting is set to false
          axes_y_settings.tickFormat = (function () { return ''; });
        }
        var axes_y = new Rickshaw.Graph.Axis.Y(axes_y_settings);
        axes_y.render();
      }

      /*
        Setting a fix height breaks things when chart is refreshed and
        legend is getting bigger.
      */
      $(self.legend_element).css('height', '');

      // Render bar chart
      if (self.stats !== undefined){
        var composed_chart = self.jquery_element.parents(self.settings.composed_chart_selector).first();
        var bar_chart_html = composed_chart.find(self.settings.bar_chart_selector).get(0);

        horizon.d3_bar_chart.refresh(bar_chart_html,
          self.settings.bar_chart_settings,
          self.stats);
      }

      // Render ending dot to last point
      if (self.settings.higlight_last_point){
        if (last_point !== undefined && last_point_color !== undefined){
          graph.vis.append('circle')
            .attr('class', 'used_component')
            .attr('cy', graph.y(last_point.y))
            .attr('cx', graph.x(last_point.x))
            .attr('r', 2)
            .style('fill', last_point_color)
            .style('stroke', last_point_color)
            .style('stroke-width', 2);
        }
      }
    };

    /**
     * Shows chart loader with backdrop. Backdrop is computed to hide
     * the canvas with chart. Loader is computed to be placed in the center.
     * Hides also a block with legend.
     */
    self.start_loading = function () {
      var self = this;

      $(self.html_element).addClass('has-spinner');
      self.$spinner.show();

      // Hide the legend.
      $(self.legend_element).empty().addClass('disabled');

      /*
        TODO(lsmola) a loader for in-line tables spark-lines has to be
        prepared, the parameters of loader could be sent in settings.
      */

    };

    /**
     * Hides the loader and backdrop so the chart will become visible.
     * Shows also the block with legend.
     */
    self.finish_loading = function () {
      var self = this;
      // Showing the legend.
      $(self.legend_element).removeClass('disabled');
      $(self.html_element).removeClass('has-spinner');
      self.$spinner.hide();
    };
  },

  /**
   * Function for initializing of the charts.
   * If settings['auto_resize'] is true, the chart will be refreshed when
   * the size of the window is changed. This option made only sense when
   * the size of the chart and its wrapper is not static.
   * @param selector jQuery selector of charts we want to initialize.
   * @param settings An object containing settings of the chart.
   */
  init: function(selector, settings) {
    var self = this;
    $(selector).each(function() {
      self.refresh(this, settings);
    });

    if (settings !== undefined && settings.auto_resize) {
      /*
        I want to refresh chart on resize of the window, but only
        at the end of the resize. Nice code from mr. Google.
      */
      var rtime = new Date(1, 1, 2000, 12, 0, 0);
      var timeout = false;
      var delta = 400;
      $(window).resize(function() {
        rtime = new Date();
        if (timeout === false) {
          timeout = true;
          setTimeout(resizeend, delta);
        }
      });

      var resizeend = function() {
        if (new Date() - rtime < delta) {
          setTimeout(resizeend, delta);
        } else {
          timeout = false;
          $(selector).each(function() {
            self.refresh(this, settings);
          });
        }
      };
    }

    self.bind_commands(selector, settings);
  },

  /**
   * Function for creating chart objects, saving them for later reuse
   * and calling their refresh method.
   * @param html_element HTML element where the chart will be rendered.
   * @param settings An object containing settings of the chart.
   */
  refresh: function(html_element, settings){
    var chart = new this.LineChart(this, html_element, settings);
    /*
      FIXME save chart objects somewhere so I can use them again when
      e.g. I am switching tabs, or if I want to update them
      via web sockets
      this.charts.add_or_update(chart)
    */
    chart.refresh();
  },

  /**
   * Function for binding controlling commands to the chart. Like changing
   * timespan or various parameters we want to show in the chart. The
   * charts will be refreshed immediately after the form element connected
   * to them is changed.
   * @param selector jQuery selector of charts we are initializing.
   * @param settings An object containing settings of the chart.
   */
  bind_commands: function (selector, settings){
    // connecting controls of the charts
    var select_box_selector = 'select[data-line-chart-command="select_box_change"]';
    var datepicker_selector = 'input[data-line-chart-command="date_picker_change"]';
    var self = this;

    /**
     * Connecting forms to charts it controls. Each chart contains
     * jQuery selector data-form-selector, which defines by which
     * html Forms is a particular chart controlled. This information
     * has to be projected to forms. So when form input is changed,
     * all connected charts are refreshed.
     */
    var connect_forms_to_charts = function(){
      $(selector).each(function() {
        var chart = $(this);
        $(chart.data('form-selector')).each(function(){
          var form = $(this);
          // each form is building a jquery selector for all charts it affects
          var chart_identifier = 'div[data-form-selector="' + chart.data('form-selector') + '"]';
          if (!form.data('charts_selector')){
            form.data('charts_selector', chart_identifier);
          } else {
            form.data('charts_selector', form.data('charts_selector') + ', ' + chart_identifier);
          }
        });
      });
    };

    /**
     * A helper function for delegating form events to charts, causing their
     * refreshing.
     * @param selector jQuery selector of charts we are initializing.
     * @param event_name Event name we want to delegate.
     * @param settings An object containing settings of the chart.
     */
    var delegate_event_and_refresh_charts = function(selector, event_name, settings) {
      $('form').on(selector, event_name, function() {
        /*
          Registering 'any event' on form element by delegating. This way it
          can be easily overridden / enhanced when some special functionality
          needs to be added. Like input element showing/hiding another element
          on some condition will be defined directly on element and can block
          this default behavior.
        */
        var invoker = $(this);
        var form = invoker.parents('form').first();

        $(form.data('charts_selector')).each(function(){
          // refresh the chart connected to changed form
          self.refresh(this, settings);
        });
      });
    };

    /**
     * A helper function for catching change event of form selectboxes
     * connected to charts.
     */
    var bind_select_box_change = function(settings) {
      delegate_event_and_refresh_charts(select_box_selector, 'change', settings);
    };

    /**
     * A helper function for catching changeDate event of form datepickers
     * connected to charts.
     */
    var bind_datepicker_change = function(settings) {
      horizon.datepickers.add(datepicker_selector);
      delegate_event_and_refresh_charts(datepicker_selector, 'changeDate', settings);
    };

    connect_forms_to_charts();
    bind_select_box_change(settings);
    bind_datepicker_change(settings);
  }
};

/* Init the graphs */
horizon.addInitFunction(function () {
  horizon.d3_line_chart.init('div[data-chart-type="line_chart"]', {});
});