f93f36fce1e8a0c15e007e88318dc67f58b324ef
[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 (var tick of layout.yticks) {
174 if (tick.label === undefined) continue; // this tick only has a grid line.
175 x = area.x;
176 var sgn = 1;
177 var prec_axis = 'y1';
178 var getAxisOption = getOptions[0];
179 if (tick.axis == 1) { // right-side y-axis
180 x = area.x + area.w;
181 sgn = -1;
182 prec_axis = 'y2';
183 getAxisOption = getOptions[1];
184 }
185 var fontSize = getAxisOption('axisLabelFontSize');
186 y = area.y + tick.pos * area.h;
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
196 label = makeDiv(tick.label, 'y', num_axes == 2 ? prec_axis : null);
197 var top = (y - fontSize / 2);
198 if (top < 0) top = 0;
199
200 if (top + fontSize + 3 > canvasHeight) {
201 label.style.bottom = '0';
202 } else {
203 label.style.top = top + 'px';
204 }
205 if (tick.axis === 0) {
206 label.style.left = (area.x - getAxisOption('axisLabelWidth') - getAxisOption('axisTickSize')) + 'px';
207 label.style.textAlign = 'right';
208 } else if (tick.axis == 1) {
209 label.style.left = (area.x + area.w +
210 getAxisOption('axisTickSize')) + 'px';
211 label.style.textAlign = 'left';
212 }
213 label.style.width = getAxisOption('axisLabelWidth') + 'px';
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];
222 // Interested in the y2 axis also?
223 var fontSize = g.getOptionForAxis('axisLabelFontSize', 'y');
224 var bottom = parseInt(bottomTick.style.top, 10) + fontSize;
225 if (bottom > canvasHeight - fontSize) {
226 bottomTick.style.top = (parseInt(bottomTick.style.top, 10) -
227 fontSize / 2) + 'px';
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')) {
234 var r = g.toPercentXCoord(0);
235 if (r > 1 || r < 0 || isNaN(r)) r = 0;
236 axisX = halfUp(area.x + r * area.w);
237 } else {
238 axisX = halfUp(area.x);
239 }
240
241 context.strokeStyle = g.getOptionForAxis('axisLineColor', 'y');
242 context.lineWidth = g.getOptionForAxis('axisLineWidth', 'y');
243
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) {
252 context.strokeStyle = g.getOptionForAxis('axisLineColor', 'y2');
253 context.lineWidth = g.getOptionForAxis('axisLineWidth', 'y2');
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
262 if (g.getOptionForAxis('drawAxis', 'x')) {
263 if (layout.xticks) {
264 var getAxisOption = makeOptionGetter('x');
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;
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
278 label = makeDiv(tick.label, 'x');
279 label.style.textAlign = 'center';
280 label.style.top = (y + getAxisOption('axisTickSize')) + 'px';
281
282 var left = (x - getAxisOption('axisLabelWidth')/2);
283 if (left + getAxisOption('axisLabelWidth') > canvasWidth) {
284 left = canvasWidth - getAxisOption('axisLabelWidth');
285 label.style.textAlign = 'right';
286 }
287 if (left < 0) {
288 left = 0;
289 label.style.textAlign = 'left';
290 }
291
292 label.style.left = left + 'px';
293 label.style.width = getAxisOption('axisLabelWidth') + 'px';
294 containerDiv.appendChild(label);
295 this.xlabels_.push(label);
296 }
297 }
298
299 context.strokeStyle = g.getOptionForAxis('axisLineColor', 'x');
300 context.lineWidth = g.getOptionForAxis('axisLineWidth', 'x');
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();
317 };
318
319 export default axes;