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