Axes mostly working; some remaining issues with secondary y-axis and space reservation
[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
9/*
10
11Bits of jankiness:
12- Direct layout access
13- Direct area access
14- Should include calculation of ticks, not just the drawing.
15
16*/
17
18/**
19 * Draws the axes. This includes the labels on the x- and y-axes, as well
20 * as the tick marks on the axes.
21 * It does _not_ draw the grid lines which span the entire chart.
22 */
23var axes = function() {
24 this.xlabels_ = [];
25 this.ylabels_ = [];
26};
27
28axes.prototype.toString = function() {
29 return "Axes Plugin";
30};
31
32axes.prototype.activate = function(g) {
33 return {
34 layout: this.layout,
35 clearChart: this.clearChart,
36 drawChart: this.drawChart
37 };
38};
39
40axes.prototype.layout = function(e) {
41 var g = e.dygraph;
42
43 if (g.getOption('drawYAxis')) {
44 var w = g.getOption('yAxisLabelWidth') + 2 * g.getOption('axisTickSize');
45 var y_axis_rect = e.reserveSpaceLeft(w);
46 }
47
48 if (g.getOption('drawXAxis')) {
49 var h;
50 if (g.getOption('xAxisHeight')) {
51 h = g.getOption('xAxisHeight');
52 } else {
53 h = g.getOption('axisLabelFontSize') + 2 * g.getOption('axisTickSize');
54 }
55 var x_axis_rect = e.reserveSpaceBottom(h);
56 }
57
58 if (g.numAxes() == 2) {
59 // TODO(danvk): per-axis setting.
60 var w = g.getOption('yAxisLabelWidth') + 2 * g.getOption('axisTickSize');
61 var y2_axis_rect = e.reserveSpaceRight(w);
62 } else if (g.numAxes() > 2) {
63 g.error("Only two y-axes are supported at this time. (Trying " +
64 "to use " + g.numAxes() + ")");
65 }
66};
67
68axes.prototype.detachLabels = function() {
69 function removeArray(ary) {
70 for (var i = 0; i < ary.length; i++) {
71 var el = ary[i];
72 if (el.parentNode) el.parentNode.removeChild(el);
73 }
74 }
75
76 removeArray(this.xlabels_);
77 removeArray(this.ylabels_);
78 this.xlabels_ = [];
79 this.ylabels_ = [];
80};
81
82axes.prototype.clearChart = function(e) {
83 var g = e.dygraph;
84 this.detachLabels();
85}
86
87axes.prototype.drawChart = function(e) {
88 var g = e.dygraph;
89 if (!g.getOption('drawXAxis') && !g.getOption('drawYAxis')) return;
90
91 // Round pixels to half-integer boundaries for crisper drawing.
92 function halfUp(x) { return Math.round(x) + 0.5; }
93 function halfDown(y){ return Math.round(y) - 0.5; }
94
95 var context = e.drawingContext;
96 var containerDiv = e.canvas.parentNode;
97
98 var label, x, y, tick, i;
99
100 var labelStyle = {
101 position: "absolute",
102 fontSize: g.getOption('axisLabelFontSize') + "px",
103 zIndex: 10,
104 color: g.getOption('axisLabelColor'),
105 width: g.getOption('axisLabelWidth') + "px",
106 // height: this.attr_('axisLabelFontSize') + 2 + "px",
107 lineHeight: "normal", // Something other than "normal" line-height screws up label positioning.
108 overflow: "hidden"
109 };
110 var makeDiv = function(txt, axis, prec_axis) {
111 var div = document.createElement("div");
112 for (var name in labelStyle) {
113 if (labelStyle.hasOwnProperty(name)) {
114 div.style[name] = labelStyle[name];
115 }
116 }
117 var inner_div = document.createElement("div");
118 inner_div.className = 'dygraph-axis-label' +
119 ' dygraph-axis-label-' + axis +
120 (prec_axis ? ' dygraph-axis-label-' + prec_axis : '');
121 inner_div.innerHTML = txt;
122 div.appendChild(inner_div);
123 return div;
124 };
125
126 // axis lines
127 context.save();
128 context.strokeStyle = g.getOption('axisLineColor');
129 context.lineWidth = g.getOption('axisLineWidth');
130
131 var layout = g.layout_;
132 var area = e.dygraph.plotter_.area;
133
134 if (g.getOption('drawYAxis')) {
135 if (layout.yticks && layout.yticks.length > 0) {
136 var num_axes = g.numAxes();
137 for (i = 0; i < layout.yticks.length; i++) {
138 tick = layout.yticks[i];
139 if (typeof(tick) == "function") return;
140 x = area.x;
141 var sgn = 1;
142 var prec_axis = 'y1';
143 if (tick[0] == 1) { // right-side y-axis
144 x = area.x + area.w;
145 sgn = -1;
146 prec_axis = 'y2';
147 }
148 y = area.y + tick[1] * area.h;
149
150 /* Tick marks are currently clipped, so don't bother drawing them.
151 context.beginPath();
152 context.moveTo(halfUp(x), halfDown(y));
153 context.lineTo(halfUp(x - sgn * this.attr_('axisTickSize')), halfDown(y));
154 context.closePath();
155 context.stroke();
156 */
157
158 label = makeDiv(tick[2], 'y', num_axes == 2 ? prec_axis : null);
159 var top = (y - g.getOption('axisLabelFontSize') / 2);
160 if (top < 0) top = 0;
161
162 if (top + g.getOption('axisLabelFontSize') + 3 > this.height) {
163 label.style.bottom = "0px";
164 } else {
165 label.style.top = top + "px";
166 }
167 if (tick[0] === 0) {
168 label.style.left = (area.x - g.getOption('yAxisLabelWidth') - g.getOption('axisTickSize')) + "px";
169 label.style.textAlign = "right";
170 } else if (tick[0] == 1) {
171 label.style.left = (area.x + area.w +
172 g.getOption('axisTickSize')) + "px";
173 label.style.textAlign = "left";
174 }
175 label.style.width = g.getOption('yAxisLabelWidth') + "px";
176 containerDiv.appendChild(label);
177 this.ylabels_.push(label);
178 }
179
180 // The lowest tick on the y-axis often overlaps with the leftmost
181 // tick on the x-axis. Shift the bottom tick up a little bit to
182 // compensate if necessary.
183 var bottomTick = this.ylabels_[0];
184 var fontSize = g.getOption('axisLabelFontSize');
185 var bottom = parseInt(bottomTick.style.top, 10) + fontSize;
186 if (bottom > this.height - fontSize) {
187 bottomTick.style.top = (parseInt(bottomTick.style.top, 10) -
188 fontSize / 2) + "px";
189 }
190 }
191
192 // draw a vertical line on the left to separate the chart from the labels.
193 var axisX;
194 if (g.getOption('drawAxesAtZero')) {
195 var r = this.dygraph_.toPercentXCoord(0);
196 if (r > 1 || r < 0) r = 0;
197 axisX = halfUp(area.x + r * area.w);
198 } else {
199 axisX = halfUp(area.x);
200 }
201 context.beginPath();
202 context.moveTo(axisX, halfDown(area.y));
203 context.lineTo(axisX, halfDown(area.y + area.h));
204 context.closePath();
205 context.stroke();
206
207 // if there's a secondary y-axis, draw a vertical line for that, too.
208 if (g.numAxes() == 2) {
209 context.beginPath();
210 context.moveTo(halfDown(area.x + area.w), halfDown(area.y));
211 context.lineTo(halfDown(area.x + area.w), halfDown(area.y + area.h));
212 context.closePath();
213 context.stroke();
214 }
215 }
216
217 if (g.getOption('drawXAxis')) {
218 if (layout.xticks) {
219 for (i = 0; i < layout.xticks.length; i++) {
220 tick = layout.xticks[i];
221 x = area.x + tick[0] * area.w;
222 y = area.y + area.h;
223
224 /* Tick marks are currently clipped, so don't bother drawing them.
225 context.beginPath();
226 context.moveTo(halfUp(x), halfDown(y));
227 context.lineTo(halfUp(x), halfDown(y + this.attr_('axisTickSize')));
228 context.closePath();
229 context.stroke();
230 */
231
232 label = makeDiv(tick[1], 'x');
233 label.style.textAlign = "center";
234 label.style.top = (y + g.getOption('axisTickSize')) + 'px';
235
236 var left = (x - g.getOption('axisLabelWidth')/2);
237 if (left + g.getOption('axisLabelWidth') > this.width) {
238 left = this.width - g.getOption('xAxisLabelWidth');
239 label.style.textAlign = "right";
240 }
241 if (left < 0) {
242 left = 0;
243 label.style.textAlign = "left";
244 }
245
246 label.style.left = left + "px";
247 label.style.width = g.getOption('xAxisLabelWidth') + "px";
248 containerDiv.appendChild(label);
249 this.xlabels_.push(label);
250 }
251 }
252
253 context.beginPath();
254 var axisY;
255 if (g.getOption('drawAxesAtZero')) {
256 var r = g.toPercentYCoord(0, 0);
257 if (r > 1 || r < 0) r = 1;
258 axisY = halfDown(area.y + r * area.h);
259 } else {
260 axisY = halfDown(area.y + area.h);
261 }
262 context.moveTo(halfUp(area.x), axisY);
263 context.lineTo(halfUp(area.x + area.w), axisY);
264 context.closePath();
265 context.stroke();
266 }
267
268 context.restore();
269}
270
271return axes;
272})();