Add axisLineColor and axisLineWidth to the per-axis configurable options.
[dygraphs.git] / 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
7Dygraph.Plugins.Axes = (function() {
8
13f8b047
DV
9"use strict";
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
RK
17Options left to make axis-friendly.
18 ('axisTickSize')
19 ('drawAxesAtZero')
20 ('xAxisHeight')
21
22These too. What is the difference between axisLablelWidth and {x,y}AxisLabelWidth?
23 ('axisLabelWidth')
24 ('xAxisLabelWidth')
25 ('yAxisLabelWidth')
f8540c66
DV
26*/
27
28/**
29 * Draws the axes. This includes the labels on the x- and y-axes, as well
30 * as the tick marks on the axes.
31 * It does _not_ draw the grid lines which span the entire chart.
32 */
33var axes = function() {
34 this.xlabels_ = [];
35 this.ylabels_ = [];
36};
37
38axes.prototype.toString = function() {
39 return "Axes Plugin";
40};
41
42axes.prototype.activate = function(g) {
43 return {
44 layout: this.layout,
45 clearChart: this.clearChart,
98eb4713 46 willDrawChart: this.willDrawChart
f8540c66
DV
47 };
48};
49
50axes.prototype.layout = function(e) {
51 var g = e.dygraph;
52
53 if (g.getOption('drawYAxis')) {
54 var w = g.getOption('yAxisLabelWidth') + 2 * g.getOption('axisTickSize');
55 var y_axis_rect = e.reserveSpaceLeft(w);
56 }
57
58 if (g.getOption('drawXAxis')) {
59 var h;
60 if (g.getOption('xAxisHeight')) {
61 h = g.getOption('xAxisHeight');
62 } else {
48dc3815 63 h = g.getOptionForAxis('axisLabelFontSize', 'x') + 2 * g.getOption('axisTickSize');
f8540c66
DV
64 }
65 var x_axis_rect = e.reserveSpaceBottom(h);
66 }
67
68 if (g.numAxes() == 2) {
69 // TODO(danvk): per-axis setting.
70 var w = g.getOption('yAxisLabelWidth') + 2 * g.getOption('axisTickSize');
71 var y2_axis_rect = e.reserveSpaceRight(w);
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
78axes.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
92axes.prototype.clearChart = function(e) {
93 var g = e.dygraph;
94 this.detachLabels();
42a9ebb8 95};
f8540c66 96
98eb4713 97axes.prototype.willDrawChart = function(e) {
f8540c66
DV
98 var g = e.dygraph;
99 if (!g.getOption('drawXAxis') && !g.getOption('drawYAxis')) return;
100
101 // Round pixels to half-integer boundaries for crisper drawing.
102 function halfUp(x) { return Math.round(x) + 0.5; }
103 function halfDown(y){ return Math.round(y) - 0.5; }
104
105 var context = e.drawingContext;
106 var containerDiv = e.canvas.parentNode;
beeabac2
DV
107 var canvasWidth = e.canvas.width;
108 var canvasHeight = e.canvas.height;
f8540c66
DV
109
110 var label, x, y, tick, i;
111
48dc3815
RK
112 var makeLabelStyle = function(axis) {
113 return {
114 position: "absolute",
115 fontSize: g.getOptionForAxis('axisLabelFontSize', axis) + "px",
116 zIndex: 10,
3a84670d 117 color: g.getOptionForAxis('axisLabelColor', axis),
48dc3815
RK
118 width: g.getOption('axisLabelWidth') + "px",
119 // height: g.getOptionForAxis('axisLabelFontSize', 'x') + 2 + "px",
120 lineHeight: "normal", // Something other than "normal" line-height screws up label positioning.
121 overflow: "hidden"
122 };
123 }
124
125 var labelStyles = {
126 x : makeLabelStyle('x'),
127 y : makeLabelStyle('y'),
128 y2 : makeLabelStyle('y2'),
f8540c66 129 };
48dc3815 130
f8540c66 131 var makeDiv = function(txt, axis, prec_axis) {
48dc3815
RK
132 /*
133 * This seems to be called with the following three sets of axis/perc_axis:
134 * x: undefined
135 * y: y1
136 * y: y2
137 */
f8540c66 138 var div = document.createElement("div");
48dc3815 139 var labelStyle = labelStyles[prec_axis == 'y2' ? 'y2' : axis];
f8540c66
DV
140 for (var name in labelStyle) {
141 if (labelStyle.hasOwnProperty(name)) {
142 div.style[name] = labelStyle[name];
143 }
144 }
145 var inner_div = document.createElement("div");
146 inner_div.className = 'dygraph-axis-label' +
147 ' dygraph-axis-label-' + axis +
148 (prec_axis ? ' dygraph-axis-label-' + prec_axis : '');
149 inner_div.innerHTML = txt;
150 div.appendChild(inner_div);
151 return div;
152 };
153
154 // axis lines
155 context.save();
f8540c66
DV
156
157 var layout = g.layout_;
158 var area = e.dygraph.plotter_.area;
159
160 if (g.getOption('drawYAxis')) {
161 if (layout.yticks && layout.yticks.length > 0) {
162 var num_axes = g.numAxes();
163 for (i = 0; i < layout.yticks.length; i++) {
164 tick = layout.yticks[i];
165 if (typeof(tick) == "function") return;
166 x = area.x;
167 var sgn = 1;
168 var prec_axis = 'y1';
169 if (tick[0] == 1) { // right-side y-axis
170 x = area.x + area.w;
171 sgn = -1;
172 prec_axis = 'y2';
173 }
48dc3815 174 var fontSize = g.getOptionForAxis('axisLabelFontSize', prec_axis);
f8540c66
DV
175 y = area.y + tick[1] * area.h;
176
177 /* Tick marks are currently clipped, so don't bother drawing them.
178 context.beginPath();
179 context.moveTo(halfUp(x), halfDown(y));
180 context.lineTo(halfUp(x - sgn * this.attr_('axisTickSize')), halfDown(y));
181 context.closePath();
182 context.stroke();
183 */
184
185 label = makeDiv(tick[2], 'y', num_axes == 2 ? prec_axis : null);
48dc3815 186 var top = (y - fontSize / 2);
f8540c66
DV
187 if (top < 0) top = 0;
188
48dc3815 189 if (top + fontSize + 3 > canvasHeight) {
f8540c66
DV
190 label.style.bottom = "0px";
191 } else {
192 label.style.top = top + "px";
193 }
194 if (tick[0] === 0) {
195 label.style.left = (area.x - g.getOption('yAxisLabelWidth') - g.getOption('axisTickSize')) + "px";
196 label.style.textAlign = "right";
197 } else if (tick[0] == 1) {
198 label.style.left = (area.x + area.w +
199 g.getOption('axisTickSize')) + "px";
200 label.style.textAlign = "left";
201 }
202 label.style.width = g.getOption('yAxisLabelWidth') + "px";
203 containerDiv.appendChild(label);
204 this.ylabels_.push(label);
205 }
206
207 // The lowest tick on the y-axis often overlaps with the leftmost
208 // tick on the x-axis. Shift the bottom tick up a little bit to
209 // compensate if necessary.
210 var bottomTick = this.ylabels_[0];
48dc3815
RK
211 // Interested in the y2 axis also?
212 var fontSize = g.getOptionForAxis('axisLabelFontSize', "y");
f8540c66 213 var bottom = parseInt(bottomTick.style.top, 10) + fontSize;
beeabac2 214 if (bottom > canvasHeight - fontSize) {
f8540c66
DV
215 bottomTick.style.top = (parseInt(bottomTick.style.top, 10) -
216 fontSize / 2) + "px";
217 }
218 }
219
220 // draw a vertical line on the left to separate the chart from the labels.
221 var axisX;
222 if (g.getOption('drawAxesAtZero')) {
beeabac2 223 var r = g.toPercentXCoord(0);
f8540c66
DV
224 if (r > 1 || r < 0) r = 0;
225 axisX = halfUp(area.x + r * area.w);
226 } else {
227 axisX = halfUp(area.x);
228 }
b67b868c
RK
229
230 context.strokeStyle = g.getOptionForAxis('axisLineColor', 'y');
231 context.lineWidth = g.getOptionForAxis('axisLineWidth', 'y');
232
f8540c66
DV
233 context.beginPath();
234 context.moveTo(axisX, halfDown(area.y));
235 context.lineTo(axisX, halfDown(area.y + area.h));
236 context.closePath();
237 context.stroke();
238
239 // if there's a secondary y-axis, draw a vertical line for that, too.
240 if (g.numAxes() == 2) {
b67b868c
RK
241 context.strokeStyle = g.getOptionForAxis('axisLineColor', 'y2');
242 context.lineWidth = g.getOptionForAxis('axisLineWidth', 'y2');
f8540c66
DV
243 context.beginPath();
244 context.moveTo(halfDown(area.x + area.w), halfDown(area.y));
245 context.lineTo(halfDown(area.x + area.w), halfDown(area.y + area.h));
246 context.closePath();
247 context.stroke();
248 }
249 }
250
251 if (g.getOption('drawXAxis')) {
252 if (layout.xticks) {
253 for (i = 0; i < layout.xticks.length; i++) {
254 tick = layout.xticks[i];
255 x = area.x + tick[0] * area.w;
256 y = area.y + area.h;
257
258 /* Tick marks are currently clipped, so don't bother drawing them.
259 context.beginPath();
260 context.moveTo(halfUp(x), halfDown(y));
261 context.lineTo(halfUp(x), halfDown(y + this.attr_('axisTickSize')));
262 context.closePath();
263 context.stroke();
264 */
265
266 label = makeDiv(tick[1], 'x');
267 label.style.textAlign = "center";
268 label.style.top = (y + g.getOption('axisTickSize')) + 'px';
269
270 var left = (x - g.getOption('axisLabelWidth')/2);
beeabac2
DV
271 if (left + g.getOption('axisLabelWidth') > canvasWidth) {
272 left = canvasWidth - g.getOption('xAxisLabelWidth');
f8540c66
DV
273 label.style.textAlign = "right";
274 }
275 if (left < 0) {
276 left = 0;
277 label.style.textAlign = "left";
278 }
279
280 label.style.left = left + "px";
281 label.style.width = g.getOption('xAxisLabelWidth') + "px";
282 containerDiv.appendChild(label);
283 this.xlabels_.push(label);
284 }
285 }
286
b67b868c
RK
287 context.strokeStyle = g.getOptionForAxis('axisLineColor', 'x');
288 context.lineWidth = g.getOptionForAxis('axisLineWidth', 'x');
f8540c66
DV
289 context.beginPath();
290 var axisY;
291 if (g.getOption('drawAxesAtZero')) {
292 var r = g.toPercentYCoord(0, 0);
293 if (r > 1 || r < 0) r = 1;
294 axisY = halfDown(area.y + r * area.h);
295 } else {
296 axisY = halfDown(area.y + area.h);
297 }
298 context.moveTo(halfUp(area.x), axisY);
299 context.lineTo(halfUp(area.x + area.w), axisY);
300 context.closePath();
301 context.stroke();
302 }
303
304 context.restore();
42a9ebb8 305};
f8540c66
DV
306
307return axes;
308})();