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