Commit | Line | Data |
---|---|---|
41da6a86 DV |
1 | /** |
2 | * @license | |
3 | * Copyright 2012 Dan Vanderkam (danvdk@gmail.com) | |
4 | * MIT-licensed (http://opensource.org/licenses/MIT) | |
5 | */ | |
0cd1ad15 | 6 | /*global Dygraph:false */ |
41da6a86 | 7 | |
e2c21500 | 8 | Dygraph.Plugins.Legend = (function() { |
e2c21500 | 9 | /* |
e2c21500 DV |
10 | Current bits of jankiness: |
11 | - Uses two private APIs: | |
12 | 1. Dygraph.optionsViewForAxis_ | |
13 | 2. dygraph.plotter_.area | |
14 | - Registers for a "predraw" event, which should be renamed. | |
15 | - I call calculateEmWidthInDiv more often than needed. | |
e2c21500 DV |
16 | */ |
17 | ||
42a9ebb8 | 18 | /*global Dygraph:false */ |
e2c21500 DV |
19 | "use strict"; |
20 | ||
21 | ||
22 | /** | |
23 | * Creates the legend, which appears when the user hovers over the chart. | |
24 | * The legend can be either a user-specified or generated div. | |
25 | * | |
26 | * @constructor | |
27 | */ | |
28 | var legend = function() { | |
29 | this.legend_div_ = null; | |
30 | this.is_generated_div_ = false; // do we own this div, or was it user-specified? | |
31 | }; | |
32 | ||
33 | legend.prototype.toString = function() { | |
34 | return "Legend Plugin"; | |
35 | }; | |
36 | ||
42a9ebb8 | 37 | // (defined below) |
6f5f0b2b | 38 | var generateLegendDashHTML; |
42a9ebb8 | 39 | |
e2c21500 DV |
40 | /** |
41 | * This is called during the dygraph constructor, after options have been set | |
42 | * but before the data is available. | |
43 | * | |
44 | * Proper tasks to do here include: | |
45 | * - Reading your own options | |
46 | * - DOM manipulation | |
47 | * - Registering event listeners | |
6a4457b4 KW |
48 | * |
49 | * @param {Dygraph} g Graph instance. | |
50 | * @return {object.<string, function(ev)>} Mapping of event names to callbacks. | |
e2c21500 | 51 | */ |
6a4457b4 | 52 | legend.prototype.activate = function(g) { |
e2c21500 DV |
53 | var div; |
54 | var divWidth = g.getOption('labelsDivWidth'); | |
55 | ||
56 | var userLabelsDiv = g.getOption('labelsDiv'); | |
57 | if (userLabelsDiv && null !== userLabelsDiv) { | |
58 | if (typeof(userLabelsDiv) == "string" || userLabelsDiv instanceof String) { | |
59 | div = document.getElementById(userLabelsDiv); | |
60 | } else { | |
61 | div = userLabelsDiv; | |
62 | } | |
63 | } else { | |
64 | // Default legend styles. These can be overridden in CSS by adding | |
65 | // "!important" after your rule, e.g. "left: 30px !important;" | |
66 | var messagestyle = { | |
67 | "position": "absolute", | |
68 | "fontSize": "14px", | |
69 | "zIndex": 10, | |
70 | "width": divWidth + "px", | |
71 | "top": "0px", | |
63e957d7 | 72 | "left": (g.size().width - divWidth - 2) + "px", |
e2c21500 | 73 | "background": "white", |
c12450f1 | 74 | "lineHeight": "normal", |
e2c21500 DV |
75 | "textAlign": "left", |
76 | "overflow": "hidden"}; | |
77 | ||
78 | // TODO(danvk): get rid of labelsDivStyles? CSS is better. | |
79 | Dygraph.update(messagestyle, g.getOption('labelsDivStyles')); | |
80 | div = document.createElement("div"); | |
81 | div.className = "dygraph-legend"; | |
82 | for (var name in messagestyle) { | |
83 | if (!messagestyle.hasOwnProperty(name)) continue; | |
84 | ||
85 | try { | |
86 | div.style[name] = messagestyle[name]; | |
87 | } catch (e) { | |
8a68db7d | 88 | console.warn("You are using unsupported css properties for your " + |
e2c21500 DV |
89 | "browser in labelsDivStyles"); |
90 | } | |
91 | } | |
92 | ||
93 | // TODO(danvk): come up with a cleaner way to expose this. | |
94 | g.graphDiv.appendChild(div); | |
95 | this.is_generated_div_ = true; | |
96 | } | |
97 | ||
98 | this.legend_div_ = div; | |
c3b1d17d | 99 | this.one_em_width_ = 10; // just a guess, will be updated. |
e2c21500 | 100 | |
6a4457b4 KW |
101 | return { |
102 | select: this.select, | |
103 | deselect: this.deselect, | |
104 | // TODO(danvk): rethink the name "predraw" before we commit to it in any API. | |
105 | predraw: this.predraw, | |
98eb4713 | 106 | didDrawChart: this.didDrawChart |
6a4457b4 | 107 | }; |
e2c21500 DV |
108 | }; |
109 | ||
110 | // Needed for dashed lines. | |
111 | var calculateEmWidthInDiv = function(div) { | |
112 | var sizeSpan = document.createElement('span'); | |
113 | sizeSpan.setAttribute('style', 'margin: 0; padding: 0 0 0 1em; border: 0;'); | |
114 | div.appendChild(sizeSpan); | |
115 | var oneEmWidth=sizeSpan.offsetWidth; | |
116 | div.removeChild(sizeSpan); | |
117 | return oneEmWidth; | |
118 | }; | |
119 | ||
f0fa05e0 A |
120 | var escapeHTML = function(str) { |
121 | return str.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">"); | |
122 | }; | |
123 | ||
6a4457b4 | 124 | legend.prototype.select = function(e) { |
e2c21500 DV |
125 | var xValue = e.selectedX; |
126 | var points = e.selectedPoints; | |
54f4c379 | 127 | var row = e.selectedRow; |
e2c21500 | 128 | |
c08d4ce2 DV |
129 | var legendMode = e.dygraph.getOption('legend'); |
130 | if (legendMode === 'never') { | |
131 | this.legend_div_.style.display = 'none'; | |
132 | return; | |
133 | } | |
134 | ||
135 | if (legendMode === 'follow') { | |
250bb62b PS |
136 | // create floating legend div |
137 | var area = e.dygraph.plotter_.area; | |
626b90aa DV |
138 | var labelsDivWidth = e.dygraph.getOption('labelsDivWidth'); |
139 | var yAxisLabelWidth = e.dygraph.getOptionForAxis('axisLabelWidth', 'y'); | |
250bb62b PS |
140 | // determine floating [left, top] coordinates of the legend div |
141 | // within the plotter_ area | |
626b90aa | 142 | // offset 20 px to the right and down from the first selection point |
250bb62b PS |
143 | // 20 px is guess based on mouse cursor size |
144 | var leftLegend = points[0].x * area.w + 20; | |
145 | var topLegend = points[0].y * area.h - 20; | |
146 | ||
626b90aa | 147 | // if legend floats to end of the window area, it flips to the other |
250bb62b | 148 | // side of the selection point |
d6eb5c59 PS |
149 | if ((leftLegend + labelsDivWidth + 1) > (window.scrollX + window.innerWidth)) { |
150 | leftLegend = leftLegend - 2 * 20 - labelsDivWidth - (yAxisLabelWidth - area.x); | |
250bb62b PS |
151 | } |
152 | ||
153 | e.dygraph.graphDiv.appendChild(this.legend_div_); | |
154 | this.legend_div_.style.left = yAxisLabelWidth + leftLegend + "px"; | |
155 | this.legend_div_.style.top = topLegend + "px"; | |
156 | } | |
157 | ||
54f4c379 | 158 | var html = legend.generateLegendHTML(e.dygraph, xValue, points, this.one_em_width_, row); |
e2c21500 | 159 | this.legend_div_.innerHTML = html; |
c08d4ce2 | 160 | this.legend_div_.style.display = ''; |
e2c21500 DV |
161 | }; |
162 | ||
6a4457b4 | 163 | legend.prototype.deselect = function(e) { |
c08d4ce2 DV |
164 | var legendMode = e.dygraph.getOption('legend'); |
165 | if (legendMode !== 'always') { | |
a22cc809 | 166 | this.legend_div_.style.display = "none"; |
250bb62b PS |
167 | } |
168 | ||
c3b1d17d | 169 | // Have to do this every time, since styles might have changed. |
e2c21500 | 170 | var oneEmWidth = calculateEmWidthInDiv(this.legend_div_); |
c3b1d17d DV |
171 | this.one_em_width_ = oneEmWidth; |
172 | ||
54f4c379 | 173 | var html = legend.generateLegendHTML(e.dygraph, undefined, undefined, oneEmWidth, null); |
e2c21500 DV |
174 | this.legend_div_.innerHTML = html; |
175 | }; | |
176 | ||
98eb4713 | 177 | legend.prototype.didDrawChart = function(e) { |
6a4457b4 | 178 | this.deselect(e); |
42a9ebb8 | 179 | }; |
e2c21500 DV |
180 | |
181 | // Right edge should be flush with the right edge of the charting area (which | |
182 | // may not be the same as the right edge of the div, if we have two y-axes. | |
183 | // TODO(danvk): is any of this really necessary? Could just set "right" in "activate". | |
184 | /** | |
185 | * Position the labels div so that: | |
186 | * - its right edge is flush with the right edge of the charting area | |
187 | * - its top edge is flush with the top edge of the charting area | |
188 | * @private | |
189 | */ | |
6a4457b4 | 190 | legend.prototype.predraw = function(e) { |
e2c21500 DV |
191 | // Don't touch a user-specified labelsDiv. |
192 | if (!this.is_generated_div_) return; | |
193 | ||
194 | // TODO(danvk): only use real APIs for this. | |
0673f7c4 | 195 | e.dygraph.graphDiv.appendChild(this.legend_div_); |
e2c21500 | 196 | var area = e.dygraph.plotter_.area; |
de9ecc40 RK |
197 | var labelsDivWidth = e.dygraph.getOption("labelsDivWidth"); |
198 | this.legend_div_.style.left = area.x + area.w - labelsDivWidth - 1 + "px"; | |
e2c21500 | 199 | this.legend_div_.style.top = area.y + "px"; |
de9ecc40 | 200 | this.legend_div_.style.width = labelsDivWidth + "px"; |
e2c21500 DV |
201 | }; |
202 | ||
203 | /** | |
204 | * Called when dygraph.destroy() is called. | |
205 | * You should null out any references and detach any DOM elements. | |
206 | */ | |
207 | legend.prototype.destroy = function() { | |
208 | this.legend_div_ = null; | |
209 | }; | |
210 | ||
211 | /** | |
212 | * @private | |
213 | * Generates HTML for the legend which is displayed when hovering over the | |
214 | * chart. If no selected points are specified, a default legend is returned | |
215 | * (this may just be the empty string). | |
54f4c379 DV |
216 | * @param {number} x The x-value of the selected points. |
217 | * @param {Object} sel_points List of selected points for the given | |
218 | * x-value. Should have properties like 'name', 'yval' and 'canvasy'. | |
219 | * @param {number} oneEmWidth The pixel width for 1em in the legend. Only | |
220 | * relevant when displaying a legend with no selection (i.e. {legend: | |
221 | * 'always'}) and with dashed lines. | |
222 | * @param {number} row The selected row index. | |
e2c21500 | 223 | */ |
54f4c379 | 224 | legend.generateLegendHTML = function(g, x, sel_points, oneEmWidth, row) { |
e2c21500 DV |
225 | // TODO(danvk): deprecate this option in place of {legend: 'never'} |
226 | if (g.getOption('showLabelsOnHighlight') !== true) return ''; | |
227 | ||
228 | // If no points are selected, we display a default legend. Traditionally, | |
229 | // this has been blank. But a better default would be a conventional legend, | |
230 | // which provides essential information for a non-interactive chart. | |
0cd1ad15 | 231 | var html, sepLines, i, dash, strokePattern; |
e2c21500 DV |
232 | var labels = g.getLabels(); |
233 | ||
234 | if (typeof(x) === 'undefined') { | |
235 | if (g.getOption('legend') != 'always') { | |
236 | return ''; | |
237 | } | |
238 | ||
239 | sepLines = g.getOption('labelsSeparateLines'); | |
240 | html = ''; | |
241 | for (i = 1; i < labels.length; i++) { | |
242 | var series = g.getPropertiesForSeries(labels[i]); | |
243 | if (!series.visible) continue; | |
244 | ||
245 | if (html !== '') html += (sepLines ? '<br/>' : ' '); | |
246 | strokePattern = g.getOption("strokePattern", labels[i]); | |
247 | dash = generateLegendDashHTML(strokePattern, series.color, oneEmWidth); | |
248 | html += "<span style='font-weight: bold; color: " + series.color + ";'>" + | |
f0fa05e0 | 249 | dash + " " + escapeHTML(labels[i]) + "</span>"; |
e2c21500 DV |
250 | } |
251 | return html; | |
252 | } | |
253 | ||
254 | // TODO(danvk): remove this use of a private API | |
255 | var xOptView = g.optionsViewForAxis_('x'); | |
256 | var xvf = xOptView('valueFormatter'); | |
6addd5b7 | 257 | html = xvf.call(g, x, xOptView, labels[0], g, row, 0); |
42a9ebb8 DV |
258 | if (html !== '') { |
259 | html += ':'; | |
f5715653 | 260 | } |
e2c21500 DV |
261 | |
262 | var yOptViews = []; | |
263 | var num_axes = g.numAxes(); | |
264 | for (i = 0; i < num_axes; i++) { | |
265 | // TODO(danvk): remove this use of a private API | |
266 | yOptViews[i] = g.optionsViewForAxis_('y' + (i ? 1 + i : '')); | |
267 | } | |
268 | var showZeros = g.getOption("labelsShowZeroValues"); | |
269 | sepLines = g.getOption("labelsSeparateLines"); | |
270 | var highlightSeries = g.getHighlightSeries(); | |
271 | for (i = 0; i < sel_points.length; i++) { | |
272 | var pt = sel_points[i]; | |
273 | if (pt.yval === 0 && !showZeros) continue; | |
274 | if (!Dygraph.isOK(pt.canvasy)) continue; | |
275 | if (sepLines) html += "<br/>"; | |
276 | ||
277 | var series = g.getPropertiesForSeries(pt.name); | |
278 | var yOptView = yOptViews[series.axis - 1]; | |
279 | var fmtFunc = yOptView('valueFormatter'); | |
6addd5b7 | 280 | var yval = fmtFunc.call(g, pt.yval, yOptView, pt.name, g, row, labels.indexOf(pt.name)); |
e2c21500 DV |
281 | |
282 | var cls = (pt.name == highlightSeries) ? " class='highlight'" : ""; | |
283 | ||
284 | // TODO(danvk): use a template string here and make it an attribute. | |
285 | html += "<span" + cls + ">" + " <b><span style='color: " + series.color + ";'>" + | |
19eba230 | 286 | escapeHTML(pt.name) + "</span></b>: " + yval + "</span>"; |
e2c21500 DV |
287 | } |
288 | return html; | |
289 | }; | |
290 | ||
291 | ||
292 | /** | |
293 | * Generates html for the "dash" displayed on the legend when using "legend: always". | |
294 | * In particular, this works for dashed lines with any stroke pattern. It will | |
295 | * try to scale the pattern to fit in 1em width. Or if small enough repeat the | |
296 | * pattern for 1em width. | |
297 | * | |
298 | * @param strokePattern The pattern | |
299 | * @param color The color of the series. | |
300 | * @param oneEmWidth The width in pixels of 1em in the legend. | |
301 | * @private | |
302 | */ | |
e8896dce | 303 | generateLegendDashHTML = function(strokePattern, color, oneEmWidth) { |
e2c21500 DV |
304 | // Easy, common case: a solid line |
305 | if (!strokePattern || strokePattern.length <= 1) { | |
306 | return "<div style=\"display: inline-block; position: relative; " + | |
307 | "bottom: .5ex; padding-left: 1em; height: 1px; " + | |
308 | "border-bottom: 2px solid " + color + ";\"></div>"; | |
309 | } | |
310 | ||
311 | var i, j, paddingLeft, marginRight; | |
312 | var strokePixelLength = 0, segmentLoop = 0; | |
313 | var normalizedPattern = []; | |
314 | var loop; | |
315 | ||
316 | // Compute the length of the pixels including the first segment twice, | |
317 | // since we repeat it. | |
318 | for (i = 0; i <= strokePattern.length; i++) { | |
319 | strokePixelLength += strokePattern[i%strokePattern.length]; | |
320 | } | |
321 | ||
322 | // See if we can loop the pattern by itself at least twice. | |
323 | loop = Math.floor(oneEmWidth/(strokePixelLength-strokePattern[0])); | |
324 | if (loop > 1) { | |
325 | // This pattern fits at least two times, no scaling just convert to em; | |
326 | for (i = 0; i < strokePattern.length; i++) { | |
327 | normalizedPattern[i] = strokePattern[i]/oneEmWidth; | |
328 | } | |
329 | // Since we are repeating the pattern, we don't worry about repeating the | |
330 | // first segment in one draw. | |
331 | segmentLoop = normalizedPattern.length; | |
332 | } else { | |
333 | // If the pattern doesn't fit in the legend we scale it to fit. | |
334 | loop = 1; | |
335 | for (i = 0; i < strokePattern.length; i++) { | |
336 | normalizedPattern[i] = strokePattern[i]/strokePixelLength; | |
337 | } | |
338 | // For the scaled patterns we do redraw the first segment. | |
339 | segmentLoop = normalizedPattern.length+1; | |
340 | } | |
341 | ||
342 | // Now make the pattern. | |
343 | var dash = ""; | |
344 | for (j = 0; j < loop; j++) { | |
345 | for (i = 0; i < segmentLoop; i+=2) { | |
346 | // The padding is the drawn segment. | |
347 | paddingLeft = normalizedPattern[i%normalizedPattern.length]; | |
348 | if (i < strokePattern.length) { | |
349 | // The margin is the space segment. | |
350 | marginRight = normalizedPattern[(i+1)%normalizedPattern.length]; | |
351 | } else { | |
352 | // The repeated first segment has no right margin. | |
353 | marginRight = 0; | |
354 | } | |
355 | dash += "<div style=\"display: inline-block; position: relative; " + | |
356 | "bottom: .5ex; margin-right: " + marginRight + "em; padding-left: " + | |
357 | paddingLeft + "em; height: 1px; border-bottom: 2px solid " + color + | |
358 | ";\"></div>"; | |
359 | } | |
360 | } | |
361 | return dash; | |
362 | }; | |
363 | ||
364 | ||
365 | return legend; | |
366 | })(); |