e9135344b1e036e453cc4d049b4983108260537c
[dygraphs.git] / src / plugins / axes.js
1 /**
2 * @license
3 * Copyright 2012 Dan Vanderkam (danvdk@gmail.com)
4 * MIT-licensed (http://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 /**
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 */
27 var axes = function() {
28 this.xlabels_ = [];
29 this.ylabels_ = [];
30 };
31
32 axes.prototype.toString = function() {
33 return 'Axes Plugin';
34 };
35
36 axes.prototype.activate = function(g) {
37 return {
38 layout: this.layout,
39 clearChart: this.clearChart,
40 willDrawChart: this.willDrawChart
41 };
42 };
43
44 axes.prototype.layout = function(e) {
45 var g = e.dygraph;
46
47 if (g.getOptionForAxis('drawAxis', 'y')) {
48 var w = g.getOptionForAxis('axisLabelWidth', 'y') + 2 * g.getOptionForAxis('axisTickSize', 'y');
49 e.reserveSpaceLeft(w);
50 }
51
52 if (g.getOptionForAxis('drawAxis', 'x')) {
53 var h;
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.)
57 if (g.getOption('xAxisHeight')) {
58 h = g.getOption('xAxisHeight');
59 } else {
60 h = g.getOptionForAxis('axisLabelFontSize', 'x') + 2 * g.getOptionForAxis('axisTickSize', 'x');
61 }
62 e.reserveSpaceBottom(h);
63 }
64
65 if (g.numAxes() == 2) {
66 if (g.getOptionForAxis('drawAxis', 'y2')) {
67 var w = g.getOptionForAxis('axisLabelWidth', 'y2') + 2 * g.getOptionForAxis('axisTickSize', 'y2');
68 e.reserveSpaceRight(w);
69 }
70 } else if (g.numAxes() > 2) {
71 g.error('Only two y-axes are supported at this time. (Trying ' +
72 'to use ' + g.numAxes() + ')');
73 }
74 };
75
76 axes.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
90 axes.prototype.clearChart = function(e) {
91 this.detachLabels();
92 };
93
94 axes.prototype.willDrawChart = function(e) {
95 var g = e.dygraph;
96
97 if (!g.getOptionForAxis('drawAxis', 'x') &&
98 !g.getOptionForAxis('drawAxis', 'y') &&
99 !g.getOptionForAxis('drawAxis', 'y2')) {
100 return;
101 }
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;
109 var canvasWidth = g.width_; // e.canvas.width is affected by pixel ratio.
110 var canvasHeight = g.height_;
111
112 var label, x, y, tick, i;
113
114 var makeLabelStyle = function(axis) {
115 return {
116 position: 'absolute',
117 fontSize: g.getOptionForAxis('axisLabelFontSize', axis) + 'px',
118 zIndex: 10,
119 color: g.getOptionForAxis('axisLabelColor', axis),
120 width: g.getOptionForAxis('axisLabelWidth', axis) + 'px',
121 // height: g.getOptionForAxis('axisLabelFontSize', 'x') + 2 + "px",
122 lineHeight: 'normal', // Something other than "normal" line-height screws up label positioning.
123 overflow: 'hidden'
124 };
125 };
126
127 var labelStyles = {
128 x : makeLabelStyle('x'),
129 y : makeLabelStyle('y'),
130 y2 : makeLabelStyle('y2')
131 };
132
133 var makeDiv = function(txt, axis, prec_axis) {
134 /*
135 * This seems to be called with the following three sets of axis/prec_axis:
136 * x: undefined
137 * y: y1
138 * y: y2
139 */
140 var div = document.createElement('div');
141 var labelStyle = labelStyles[prec_axis == 'y2' ? 'y2' : axis];
142 for (var name in labelStyle) {
143 if (labelStyle.hasOwnProperty(name)) {
144 div.style[name] = labelStyle[name];
145 }
146 }
147 var inner_div = document.createElement('div');
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();
158
159 var layout = g.layout_;
160 var area = e.dygraph.plotter_.area;
161
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
169 if (g.getOptionForAxis('drawAxis', 'y')) {
170 if (layout.yticks && layout.yticks.length > 0) {
171 var num_axes = g.numAxes();
172 var getOptions = [makeOptionGetter('y'), makeOptionGetter('y2')];
173 for (i = 0; i < layout.yticks.length; i++) {
174 tick = layout.yticks[i];
175 if (typeof(tick) == 'function') return; // <-- when would this happen?
176 x = area.x;
177 var sgn = 1;
178 var prec_axis = 'y1';
179 var getAxisOption = getOptions[0];
180 if (tick[0] == 1) { // right-side y-axis
181 x = area.x + area.w;
182 sgn = -1;
183 prec_axis = 'y2';
184 getAxisOption = getOptions[1];
185 }
186 var fontSize = getAxisOption('axisLabelFontSize');
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);
198 var top = (y - fontSize / 2);
199 if (top < 0) top = 0;
200
201 if (top + fontSize + 3 > canvasHeight) {
202 label.style.bottom = '0';
203 } else {
204 label.style.top = top + 'px';
205 }
206 if (tick[0] === 0) {
207 label.style.left = (area.x - getAxisOption('axisLabelWidth') - getAxisOption('axisTickSize')) + 'px';
208 label.style.textAlign = 'right';
209 } else if (tick[0] == 1) {
210 label.style.left = (area.x + area.w +
211 getAxisOption('axisTickSize')) + 'px';
212 label.style.textAlign = 'left';
213 }
214 label.style.width = getAxisOption('axisLabelWidth') + 'px';
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];
223 // Interested in the y2 axis also?
224 var fontSize = g.getOptionForAxis('axisLabelFontSize', 'y');
225 var bottom = parseInt(bottomTick.style.top, 10) + fontSize;
226 if (bottom > canvasHeight - fontSize) {
227 bottomTick.style.top = (parseInt(bottomTick.style.top, 10) -
228 fontSize / 2) + 'px';
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')) {
235 var r = g.toPercentXCoord(0);
236 if (r > 1 || r < 0 || isNaN(r)) r = 0;
237 axisX = halfUp(area.x + r * area.w);
238 } else {
239 axisX = halfUp(area.x);
240 }
241
242 context.strokeStyle = g.getOptionForAxis('axisLineColor', 'y');
243 context.lineWidth = g.getOptionForAxis('axisLineWidth', 'y');
244
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) {
253 context.strokeStyle = g.getOptionForAxis('axisLineColor', 'y2');
254 context.lineWidth = g.getOptionForAxis('axisLineWidth', 'y2');
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
263 if (g.getOptionForAxis('drawAxis', 'x')) {
264 if (layout.xticks) {
265 var getAxisOption = makeOptionGetter('x');
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');
280 label.style.textAlign = 'center';
281 label.style.top = (y + getAxisOption('axisTickSize')) + 'px';
282
283 var left = (x - getAxisOption('axisLabelWidth')/2);
284 if (left + getAxisOption('axisLabelWidth') > canvasWidth) {
285 left = canvasWidth - getAxisOption('axisLabelWidth');
286 label.style.textAlign = 'right';
287 }
288 if (left < 0) {
289 left = 0;
290 label.style.textAlign = 'left';
291 }
292
293 label.style.left = left + 'px';
294 label.style.width = getAxisOption('axisLabelWidth') + 'px';
295 containerDiv.appendChild(label);
296 this.xlabels_.push(label);
297 }
298 }
299
300 context.strokeStyle = g.getOptionForAxis('axisLineColor', 'x');
301 context.lineWidth = g.getOptionForAxis('axisLineWidth', 'x');
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();
318 };
319
320 export default axes;