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