      1 'use strict';
      2
      3 /**
      4  * @license
      5  * Copyright 2012 Dan Vanderkam (danvdk@gmail.com)
      6  * MIT-licenced: https://opensource.org/licenses/MIT
      7  */
      8
      9 /*global Dygraph:false */
     10
     11 /*
     12 Bits of jankiness:
     13 - Direct layout access
     14 - Direct area access
     15 - Should include calculation of ticks, not just the drawing.
     16
     17 Options left to make axis-friendly.
     18   ('drawAxesAtZero')
     19   ('xAxisHeight')
     20 */
     21
     22 import * as utils from '../dygraph-utils';
     23
     24 /**
     25  * Draws the axes. This includes the labels on the x- and y-axes, as well
     26  * as the tick marks on the axes.
     27  * It does _not_ draw the grid lines which span the entire chart.
     28  */
     29 var axes = function() {
     30   this.xlabels_ = [];
     31   this.ylabels_ = [];
     32 };
     33
     34 axes.prototype.toString = function() {
     35   return 'Axes Plugin';
     36 };
     37
     38 axes.prototype.activate = function(g) {
     39   return {
     40     layout: this.layout,
     41     clearChart: this.clearChart,
     42     willDrawChart: this.willDrawChart
     43   };
     44 };
     45
     46 axes.prototype.layout = function(e) {
     47   var g = e.dygraph;
     48
     49   if (g.getOptionForAxis('drawAxis', 'y')) {
     50     var w = g.getOptionForAxis('axisLabelWidth', 'y') + 2 * g.getOptionForAxis('axisTickSize', 'y');
     51     e.reserveSpaceLeft(w);
     52   }
     53
     54   if (g.getOptionForAxis('drawAxis', 'x')) {
     55     var h;
     56     // NOTE: I think this is probably broken now, since g.getOption() now
     57     // hits the dictionary. (That is, g.getOption('xAxisHeight') now always
     58     // has a value.)
     59     if (g.getOption('xAxisHeight')) {
     60       h = g.getOption('xAxisHeight');
     61     } else {
     62       h = g.getOptionForAxis('axisLabelFontSize', 'x') + 2 * g.getOptionForAxis('axisTickSize', 'x');
     63     }
     64     e.reserveSpaceBottom(h);
     65   }
     66
     67   if (g.numAxes() == 2) {
     68     if (g.getOptionForAxis('drawAxis', 'y2')) {
     69       var w = g.getOptionForAxis('axisLabelWidth', 'y2') + 2 * g.getOptionForAxis('axisTickSize', 'y2');
     70       e.reserveSpaceRight(w);
     71     }
     72   } else if (g.numAxes() > 2) {
     73     g.error('Only two y-axes are supported at this time. (Trying ' +
     74             'to use ' + g.numAxes() + ')');
     75   }
     76 };
     77
     78 axes.prototype.detachLabels = function() {
     79   function removeArray(ary) {
     80     for (var i = 0; i < ary.length; i++) {
     81       var el = ary[i];
     82       if (el.parentNode) el.parentNode.removeChild(el);
     83     }
     84   }
     85
     86   removeArray(this.xlabels_);
     87   removeArray(this.ylabels_);
     88   this.xlabels_ = [];
     89   this.ylabels_ = [];
     90 };
     91
     92 axes.prototype.clearChart = function(e) {
     93   this.detachLabels();
     94 };
     95
     96 axes.prototype.willDrawChart = function(e) {
     97   var g = e.dygraph;
     98
     99   if (!g.getOptionForAxis('drawAxis', 'x') &&
    100       !g.getOptionForAxis('drawAxis', 'y') &&
    101       !g.getOptionForAxis('drawAxis', 'y2')) {
    102     return;
    103   }
    104
    105   // Round pixels to half-integer boundaries for crisper drawing.
    106   function halfUp(x)  { return Math.round(x) + 0.5; }
    107   function halfDown(y){ return Math.round(y) - 0.5; }
    108
    109   var context = e.drawingContext;
    110   var containerDiv = e.canvas.parentNode;
    111   var canvasWidth = g.width_;  // e.canvas.width is affected by pixel ratio.
    112   var canvasHeight = g.height_;
    113
    114   var label, x, y, tick, i;
    115
    116   var makeLabelStyle = function(axis) {
    117     return {
    118       position: 'absolute',
    119       fontSize: g.getOptionForAxis('axisLabelFontSize', axis) + 'px',
    120       width: g.getOptionForAxis('axisLabelWidth', axis) + 'px',
    121     };
    122   };
    123
    124   var labelStyles = {
    125     x: makeLabelStyle('x'),
    126     y: makeLabelStyle('y'),
    127     y2: makeLabelStyle('y2')
    128   };
    129
    130   var makeDiv = function(txt, axis, prec_axis) {
    131     /*
    132      * This seems to be called with the following three sets of axis/prec_axis:
    133      * x: undefined
    134      * y: y1
    135      * y: y2
    136      */
    137     var div = document.createElement('div');
    138     var labelStyle = labelStyles[prec_axis == 'y2' ? 'y2' : axis];
    139     utils.update(div.style, labelStyle);
    140     // TODO: combine outer & inner divs
    141     var inner_div = document.createElement('div');
    142     inner_div.className = 'dygraph-axis-label' +
    143                           ' dygraph-axis-label-' + axis +
    144                           (prec_axis ? ' dygraph-axis-label-' + prec_axis : '');
    145     inner_div.innerHTML = txt;
    146     div.appendChild(inner_div);
    147     return div;
    148   };
    149
    150   // axis lines
    151   context.save();
    152
    153   var layout = g.layout_;
    154   var area = e.dygraph.plotter_.area;
    155
    156   // Helper for repeated axis-option accesses.
    157   var makeOptionGetter = function(axis) {
    158     return function(option) {
    159       return g.getOptionForAxis(option, axis);
    160     };
    161   };
    162
    163   const that = this;
    164
    165   if (g.getOptionForAxis('drawAxis', 'y') ||
    166       (g.numAxes() == 2 && g.getOptionForAxis('drawAxis', 'y2'))) {
    167     if (layout.yticks && layout.yticks.length > 0) {
    168       var num_axes = g.numAxes();
    169       var getOptions = [makeOptionGetter('y'), makeOptionGetter('y2')];
    170       layout.yticks.forEach(function (tick) {
    171         if (tick.label === undefined) return;  // this tick only has a grid line.
    172         x = area.x;
    173         var sgn = 1;
    174         var prec_axis = 'y1';
    175         var getAxisOption = getOptions[0];
    176         if (tick.axis == 1) {  // right-side y-axis
    177           x = area.x + area.w;
    178           sgn = -1;
    179           prec_axis = 'y2';
    180           getAxisOption = getOptions[1];
    181         }
    182         if (!getAxisOption('drawAxis')) return;
    183         var fontSize = getAxisOption('axisLabelFontSize');
    184         y = area.y + tick.pos * area.h;
    185
    186         /* Tick marks are currently clipped, so don't bother drawing them.
    187         context.beginPath();
    188         context.moveTo(halfUp(x), halfDown(y));
    189         context.lineTo(halfUp(x - sgn * that.attr_('axisTickSize')), halfDown(y));
    190         context.closePath();
    191         context.stroke();
    192         */
    193
    194         label = makeDiv(tick.label, 'y', num_axes == 2 ? prec_axis : null);
    195         var top = (y - fontSize / 2);
    196         if (top < 0) top = 0;
    197
    198         if (top + fontSize + 3 > canvasHeight) {
    199           label.style.bottom = '0';
    200         } else {
    201           // The lowest tick on the y-axis often overlaps with the leftmost
    202           // tick on the x-axis. Shift the bottom tick up a little bit to
    203           // compensate if necessary.
    204           label.style.top = Math.min(top, canvasHeight - (2 * fontSize)) + 'px';
    205         }
    206         // TODO: replace these with css classes?
    207         if (tick.axis === 0) {
    208           label.style.left = (area.x - getAxisOption('axisLabelWidth') - getAxisOption('axisTickSize')) + 'px';
    209           label.style.textAlign = 'right';
    210         } else if (tick.axis == 1) {
    211           label.style.left = (area.x + area.w +
    212                               getAxisOption('axisTickSize')) + 'px';
    213           label.style.textAlign = 'left';
    214         }
    215         label.style.width = getAxisOption('axisLabelWidth') + 'px';
    216         containerDiv.appendChild(label);
    217         that.ylabels_.push(label);
    218       });
    219     }
    220
    221     // draw a vertical line on the left to separate the chart from the labels.
    222     var axisX;
    223     if (g.getOption('drawAxesAtZero')) {
    224       var r = g.toPercentXCoord(0);
    225       if (r > 1 || r < 0 || isNaN(r)) r = 0;
    226       axisX = halfUp(area.x + r * area.w);
    227     } else {
    228       axisX = halfUp(area.x);
    229     }
    230
    231     context.strokeStyle = g.getOptionForAxis('axisLineColor', 'y');
    232     context.lineWidth = g.getOptionForAxis('axisLineWidth', 'y');
    233
    234     context.beginPath();
    235     context.moveTo(axisX, halfDown(area.y));
    236     context.lineTo(axisX, halfDown(area.y + area.h));
    237     context.closePath();
    238     context.stroke();
    239
    240     // if there's a secondary y-axis, draw a vertical line for that, too.
    241     if (g.numAxes() == 2 && g.getOptionForAxis('drawAxis', 'y2')) {
    242       context.strokeStyle = g.getOptionForAxis('axisLineColor', 'y2');
    243       context.lineWidth = g.getOptionForAxis('axisLineWidth', 'y2');
    244       context.beginPath();
    245       context.moveTo(halfDown(area.x + area.w), halfDown(area.y));
    246       context.lineTo(halfDown(area.x + area.w), halfDown(area.y + area.h));
    247       context.closePath();
    248       context.stroke();
    249     }
    250   }
    251
    252   if (g.getOptionForAxis('drawAxis', 'x')) {
    253     if (layout.xticks) {
    254       var getAxisOption = makeOptionGetter('x');
    255       layout.xticks.forEach(function (tick) {
    256         if (tick.label === undefined) return;  // this tick only has a grid line.
    257         x = area.x + tick.pos * area.w;
    258         y = area.y + area.h;
    259
    260         /* Tick marks are currently clipped, so don't bother drawing them.
    261         context.beginPath();
    262         context.moveTo(halfUp(x), halfDown(y));
    263         context.lineTo(halfUp(x), halfDown(y + that.attr_('axisTickSize')));
    264         context.closePath();
    265         context.stroke();
    266         */
    267
    268         label = makeDiv(tick.label, 'x');
    269         label.style.textAlign = 'center';
    270         label.style.top = (y + getAxisOption('axisTickSize')) + 'px';
    271
    272         var left = (x - getAxisOption('axisLabelWidth')/2);
    273         if (left + getAxisOption('axisLabelWidth') > canvasWidth) {
    274           left = canvasWidth - getAxisOption('axisLabelWidth');
    275           label.style.textAlign = 'right';
    276         }
    277         if (left < 0) {
    278           left = 0;
    279           label.style.textAlign = 'left';
    280         }
    281
    282         label.style.left = left + 'px';
    283         label.style.width = getAxisOption('axisLabelWidth') + 'px';
    284         containerDiv.appendChild(label);
    285         that.xlabels_.push(label);
    286       });
    287     }
    288
    289     context.strokeStyle = g.getOptionForAxis('axisLineColor', 'x');
    290     context.lineWidth = g.getOptionForAxis('axisLineWidth', 'x');
    291     context.beginPath();
    292     var axisY;
    293     if (g.getOption('drawAxesAtZero')) {
    294       var r = g.toPercentYCoord(0, 0);
    295       if (r > 1 || r < 0) r = 1;
    296       axisY = halfDown(area.y + r * area.h);
    297     } else {
    298       axisY = halfDown(area.y + area.h);
    299     }
    300     context.moveTo(halfUp(area.x), axisY);
    301     context.lineTo(halfUp(area.x + area.w), axisY);
    302     context.closePath();
    303     context.stroke();
    304   }
    305
    306   context.restore();
    307 };
    308
    309 export default axes;
    310