Merge pull request #674 from danvk/module
[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 }
f8540c66
DV
102
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')];
f8540c66
DV
173 for (i = 0; i < layout.yticks.length; i++) {
174 tick = layout.yticks[i];
e0269a3d 175 if (typeof(tick) == 'function') return; // <-- when would this happen?
f8540c66
DV
176 x = area.x;
177 var sgn = 1;
178 var prec_axis = 'y1';
e0269a3d 179 var getAxisOption = getOptions[0];
f8540c66
DV
180 if (tick[0] == 1) { // right-side y-axis
181 x = area.x + area.w;
182 sgn = -1;
183 prec_axis = 'y2';
e0269a3d 184 getAxisOption = getOptions[1];
f8540c66 185 }
e0269a3d 186 var fontSize = getAxisOption('axisLabelFontSize');
f8540c66
DV
187 y = area.y + tick[1] * area.h;
188
189 /* Tick marks are currently clipped, so don't bother drawing them.
190 context.beginPath();
191 context.moveTo(halfUp(x), halfDown(y));
192 context.lineTo(halfUp(x - sgn * this.attr_('axisTickSize')), halfDown(y));
193 context.closePath();
194 context.stroke();
195 */
196
197 label = makeDiv(tick[2], 'y', num_axes == 2 ? prec_axis : null);
48dc3815 198 var top = (y - fontSize / 2);
f8540c66
DV
199 if (top < 0) top = 0;
200
48dc3815 201 if (top + fontSize + 3 > canvasHeight) {
e0269a3d 202 label.style.bottom = '0';
f8540c66 203 } else {
e0269a3d 204 label.style.top = top + 'px';
f8540c66
DV
205 }
206 if (tick[0] === 0) {
e0269a3d
DV
207 label.style.left = (area.x - getAxisOption('axisLabelWidth') - getAxisOption('axisTickSize')) + 'px';
208 label.style.textAlign = 'right';
f8540c66
DV
209 } else if (tick[0] == 1) {
210 label.style.left = (area.x + area.w +
e0269a3d
DV
211 getAxisOption('axisTickSize')) + 'px';
212 label.style.textAlign = 'left';
f8540c66 213 }
e0269a3d 214 label.style.width = getAxisOption('axisLabelWidth') + 'px';
f8540c66
DV
215 containerDiv.appendChild(label);
216 this.ylabels_.push(label);
217 }
218
219 // The lowest tick on the y-axis often overlaps with the leftmost
220 // tick on the x-axis. Shift the bottom tick up a little bit to
221 // compensate if necessary.
222 var bottomTick = this.ylabels_[0];
48dc3815 223 // Interested in the y2 axis also?
e0269a3d 224 var fontSize = g.getOptionForAxis('axisLabelFontSize', 'y');
f8540c66 225 var bottom = parseInt(bottomTick.style.top, 10) + fontSize;
beeabac2 226 if (bottom > canvasHeight - fontSize) {
f8540c66 227 bottomTick.style.top = (parseInt(bottomTick.style.top, 10) -
e0269a3d 228 fontSize / 2) + 'px';
f8540c66
DV
229 }
230 }
231
232 // draw a vertical line on the left to separate the chart from the labels.
233 var axisX;
234 if (g.getOption('drawAxesAtZero')) {
beeabac2 235 var r = g.toPercentXCoord(0);
33e96f11 236 if (r > 1 || r < 0 || isNaN(r)) r = 0;
f8540c66
DV
237 axisX = halfUp(area.x + r * area.w);
238 } else {
239 axisX = halfUp(area.x);
240 }
b67b868c
RK
241
242 context.strokeStyle = g.getOptionForAxis('axisLineColor', 'y');
243 context.lineWidth = g.getOptionForAxis('axisLineWidth', 'y');
244
f8540c66
DV
245 context.beginPath();
246 context.moveTo(axisX, halfDown(area.y));
247 context.lineTo(axisX, halfDown(area.y + area.h));
248 context.closePath();
249 context.stroke();
250
251 // if there's a secondary y-axis, draw a vertical line for that, too.
252 if (g.numAxes() == 2) {
b67b868c
RK
253 context.strokeStyle = g.getOptionForAxis('axisLineColor', 'y2');
254 context.lineWidth = g.getOptionForAxis('axisLineWidth', 'y2');
f8540c66
DV
255 context.beginPath();
256 context.moveTo(halfDown(area.x + area.w), halfDown(area.y));
257 context.lineTo(halfDown(area.x + area.w), halfDown(area.y + area.h));
258 context.closePath();
259 context.stroke();
260 }
261 }
262
7f6a7190 263 if (g.getOptionForAxis('drawAxis', 'x')) {
f8540c66 264 if (layout.xticks) {
e0269a3d 265 var getAxisOption = makeOptionGetter('x');
f8540c66
DV
266 for (i = 0; i < layout.xticks.length; i++) {
267 tick = layout.xticks[i];
268 x = area.x + tick[0] * area.w;
269 y = area.y + area.h;
270
271 /* Tick marks are currently clipped, so don't bother drawing them.
272 context.beginPath();
273 context.moveTo(halfUp(x), halfDown(y));
274 context.lineTo(halfUp(x), halfDown(y + this.attr_('axisTickSize')));
275 context.closePath();
276 context.stroke();
277 */
278
279 label = makeDiv(tick[1], 'x');
e0269a3d
DV
280 label.style.textAlign = 'center';
281 label.style.top = (y + getAxisOption('axisTickSize')) + 'px';
f8540c66 282
e0269a3d
DV
283 var left = (x - getAxisOption('axisLabelWidth')/2);
284 if (left + getAxisOption('axisLabelWidth') > canvasWidth) {
285 left = canvasWidth - getAxisOption('axisLabelWidth');
286 label.style.textAlign = 'right';
f8540c66
DV
287 }
288 if (left < 0) {
289 left = 0;
e0269a3d 290 label.style.textAlign = 'left';
f8540c66
DV
291 }
292
e0269a3d
DV
293 label.style.left = left + 'px';
294 label.style.width = getAxisOption('axisLabelWidth') + 'px';
f8540c66
DV
295 containerDiv.appendChild(label);
296 this.xlabels_.push(label);
297 }
298 }
299
b67b868c
RK
300 context.strokeStyle = g.getOptionForAxis('axisLineColor', 'x');
301 context.lineWidth = g.getOptionForAxis('axisLineWidth', 'x');
f8540c66
DV
302 context.beginPath();
303 var axisY;
304 if (g.getOption('drawAxesAtZero')) {
305 var r = g.toPercentYCoord(0, 0);
306 if (r > 1 || r < 0) r = 1;
307 axisY = halfDown(area.y + r * area.h);
308 } else {
309 axisY = halfDown(area.y + area.h);
310 }
311 context.moveTo(halfUp(area.x), axisY);
312 context.lineTo(halfUp(area.x + area.w), axisY);
313 context.closePath();
314 context.stroke();
315 }
316
317 context.restore();
42a9ebb8 318};
f8540c66 319
6ecc0739 320export default axes;