      1 /**
      2  * @license
      3  * Copyright 2012 Dan Vanderkam (danvdk@gmail.com)
      4  * MIT-licenced: https://opensource.org/licenses/MIT
      5  */
      6
      7 /*global Dygraph:false */
      8
      9 'use strict';
     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   if (g.getOptionForAxis('drawAxis', 'y') || g.getOptionForAxis('drawAxis', 'y2')) {
    164     if (layout.yticks && layout.yticks.length > 0) {
    165       var num_axes = g.numAxes();
    166       var getOptions = [makeOptionGetter('y'), makeOptionGetter('y2')];
    167       layout.yticks.forEach(tick => {
    168         if (tick.label === undefined) return;  // this tick only has a grid line.
    169         x = area.x;
    170         var sgn = 1;
    171         var prec_axis = 'y1';
    172         var getAxisOption = getOptions[0];
    173         if (tick.axis == 1) {  // right-side y-axis
    174           x = area.x + area.w;
    175           sgn = -1;
    176           prec_axis = 'y2';
    177           getAxisOption = getOptions[1];
    178         }
    179         if (!getAxisOption('drawAxis')) return;
    180         var fontSize = getAxisOption('axisLabelFontSize');
    181         y = area.y + tick.pos * area.h;
    182
    183         /* Tick marks are currently clipped, so don't bother drawing them.
    184         context.beginPath();
    185         context.moveTo(halfUp(x), halfDown(y));
    186         context.lineTo(halfUp(x - sgn * this.attr_('axisTickSize')), halfDown(y));
    187         context.closePath();
    188         context.stroke();
    189         */
    190
    191         label = makeDiv(tick.label, 'y', num_axes == 2 ? prec_axis : null);
    192         var top = (y - fontSize / 2);
    193         if (top < 0) top = 0;
    194
    195         if (top + fontSize + 3 > canvasHeight) {
    196           label.style.bottom = '0';
    197         } else {
    198           // The lowest tick on the y-axis often overlaps with the leftmost
    199           // tick on the x-axis. Shift the bottom tick up a little bit to
    200           // compensate if necessary.
    201           label.style.top = Math.min(top, canvasHeight - (2 * fontSize)) + 'px';
    202         }
    203         // TODO: replace these with css classes?
    204         if (tick.axis === 0) {
    205           label.style.left = (area.x - getAxisOption('axisLabelWidth') - getAxisOption('axisTickSize')) + 'px';
    206           label.style.textAlign = 'right';
    207         } else if (tick.axis == 1) {
    208           label.style.left = (area.x + area.w +
    209                               getAxisOption('axisTickSize')) + 'px';
    210           label.style.textAlign = 'left';
    211         }
    212         label.style.width = getAxisOption('axisLabelWidth') + 'px';
    213         containerDiv.appendChild(label);
    214         this.ylabels_.push(label);
    215       });
    216     }
    217
    218     // draw a vertical line on the left to separate the chart from the labels.
    219     var axisX;
    220     if (g.getOption('drawAxesAtZero')) {
    221       var r = g.toPercentXCoord(0);
    222       if (r > 1 || r < 0 || isNaN(r)) r = 0;
    223       axisX = halfUp(area.x + r * area.w);
    224     } else {
    225       axisX = halfUp(area.x);
    226     }
    227
    228     context.strokeStyle = g.getOptionForAxis('axisLineColor', 'y');
    229     context.lineWidth = g.getOptionForAxis('axisLineWidth', 'y');
    230
    231     context.beginPath();
    232     context.moveTo(axisX, halfDown(area.y));
    233     context.lineTo(axisX, halfDown(area.y + area.h));
    234     context.closePath();
    235     context.stroke();
    236
    237     // if there's a secondary y-axis, draw a vertical line for that, too.
    238     if (g.numAxes() == 2 && g.getOptionForAxis('drawAxis', 'y2')) {
    239       context.strokeStyle = g.getOptionForAxis('axisLineColor', 'y2');
    240       context.lineWidth = g.getOptionForAxis('axisLineWidth', 'y2');
    241       context.beginPath();
    242       context.moveTo(halfDown(area.x + area.w), halfDown(area.y));
    243       context.lineTo(halfDown(area.x + area.w), halfDown(area.y + area.h));
    244       context.closePath();
    245       context.stroke();
    246     }
    247   }
    248
    249   if (g.getOptionForAxis('drawAxis', 'x')) {
    250     if (layout.xticks) {
    251       var getAxisOption = makeOptionGetter('x');
    252       layout.xticks.forEach(tick => {
    253         if (tick.label === undefined) return;  // this tick only has a grid line.
    254         x = area.x + tick.pos * area.w;
    255         y = area.y + area.h;
    256
    257         /* Tick marks are currently clipped, so don't bother drawing them.
    258         context.beginPath();
    259         context.moveTo(halfUp(x), halfDown(y));
    260         context.lineTo(halfUp(x), halfDown(y + this.attr_('axisTickSize')));
    261         context.closePath();
    262         context.stroke();
    263         */
    264
    265         label = makeDiv(tick.label, 'x');
    266         label.style.textAlign = 'center';
    267         label.style.top = (y + getAxisOption('axisTickSize')) + 'px';
    268
    269         var left = (x - getAxisOption('axisLabelWidth')/2);
    270         if (left + getAxisOption('axisLabelWidth') > canvasWidth) {
    271           left = canvasWidth - getAxisOption('axisLabelWidth');
    272           label.style.textAlign = 'right';
    273         }
    274         if (left < 0) {
    275           left = 0;
    276           label.style.textAlign = 'left';
    277         }
    278
    279         label.style.left = left + 'px';
    280         label.style.width = getAxisOption('axisLabelWidth') + 'px';
    281         containerDiv.appendChild(label);
    282         this.xlabels_.push(label);
    283       });
    284     }
    285
    286     context.strokeStyle = g.getOptionForAxis('axisLineColor', 'x');
    287     context.lineWidth = g.getOptionForAxis('axisLineWidth', 'x');
    288     context.beginPath();
    289     var axisY;
    290     if (g.getOption('drawAxesAtZero')) {
    291       var r = g.toPercentYCoord(0, 0);
    292       if (r > 1 || r < 0) r = 1;
    293       axisY = halfDown(area.y + r * area.h);
    294     } else {
    295       axisY = halfDown(area.y + area.h);
    296     }
    297     context.moveTo(halfUp(area.x), axisY);
    298     context.lineTo(halfUp(area.x + area.w), axisY);
    299     context.closePath();
    300     context.stroke();
    301   }
    302
    303   context.restore();
    304 };
    305
    306 export default axes;
    307