');
14 | }
15 |
16 | clear(plot) {
17 | this._visible = false;
18 | this.$tooltip.detach();
19 | plot.clearCrosshair();
20 | plot.unhighlight();
21 | };
22 |
23 | show(pos, item?) {
24 | if (item === undefined) {
25 | item = this._lastItem;
26 | } else {
27 | this._lastItem = item;
28 | }
29 |
30 | this._visible = true;
31 | var plot = this.$elem.data().plot;
32 | var plotData = plot.getData();
33 | var xAxes = plot.getXAxes();
34 | var xMode = xAxes[0].options.mode;
35 | var seriesList = this.getSeriesFn();
36 | var allSeriesMode = this.panel.tooltip.shared;
37 | var group, value, absoluteTime, hoverInfo, i, series, seriesHtml, tooltipFormat;
38 | var rangeDist = Math.abs(xAxes[0].max - xAxes[0].min);
39 |
40 | // if panelRelY is defined another panel wants us to show a tooltip
41 | // get pageX from position on x axis and pageY from relative position in original panel
42 | if (pos.panelRelY) {
43 | var pointOffset = plot.pointOffset({ x: pos.x });
44 | if (Number.isNaN(pointOffset.left) || pointOffset.left < 0 || pointOffset.left > this.$elem.width()) {
45 | this.clear(plot);
46 | return;
47 | }
48 | pos.pageX = this.$elem.offset().left + pointOffset.left;
49 | pos.pageY = this.$elem.offset().top + this.$elem.height() * pos.panelRelY;
50 | var isVisible = pos.pageY >= $(window).scrollTop() &&
51 | pos.pageY <= $(window).innerHeight() + $(window).scrollTop();
52 | if (!isVisible) {
53 | this.clear(plot);
54 | return;
55 | }
56 | plot.setCrosshair(pos);
57 | allSeriesMode = true;
58 |
59 | if (this.dashboard.sharedCrosshairModeOnly()) {
60 | // if only crosshair mode we are done
61 | return;
62 | }
63 | }
64 |
65 | if (seriesList.length === 0) {
66 | return;
67 | }
68 |
69 | if (seriesList[0].hasMsResolution) {
70 | tooltipFormat = 'YYYY-MM-DD HH:mm:ss.SSS';
71 | } else {
72 | tooltipFormat = 'YYYY-MM-DD HH:mm:ss';
73 | }
74 |
75 | if (allSeriesMode) {
76 | plot.unhighlight();
77 |
78 | seriesHtml = '';
79 | var seriesHoverInfo = this._getMultiSeriesPlotHoverInfo(plotData, pos);
80 | absoluteTime = this.dashboard.formatDate(seriesHoverInfo.time, tooltipFormat);
81 |
82 | // Dynamically reorder the hovercard for the current time point if the
83 | // option is enabled.
84 | if (this.panel.tooltip.sort === 2) {
85 | seriesHoverInfo.series.sort((a: any, b: any) => b.value - a.value);
86 | } else if (this.panel.tooltip.sort === 1) {
87 | seriesHoverInfo.series.sort((a: any, b: any) => a.value - b.value);
88 | }
89 |
90 | for (i = 0; i < seriesHoverInfo.series.length; i++) {
91 | hoverInfo = seriesHoverInfo.series[i];
92 |
93 | if (hoverInfo.hidden) {
94 | continue;
95 | }
96 |
97 | var highlightClass = '';
98 | if (item && hoverInfo.index === item.seriesIndex) {
99 | highlightClass = 'graph-tooltip-list-item--highlight';
100 | }
101 |
102 | series = seriesList[hoverInfo.index];
103 |
104 | value = series.formatValue(hoverInfo.value);
105 |
106 | seriesHtml += '
';
109 | plot.highlight(hoverInfo.index, hoverInfo.hoverIndex);
110 | }
111 |
112 | this._renderAndShow(absoluteTime, seriesHtml, pos, xMode);
113 | }
114 | // single series tooltip
115 | else if (item) {
116 | series = seriesList[item.seriesIndex];
117 | group = '
';
118 | group += ' ' + series.aliasEscaped + ':
';
119 |
120 | if (this.panel.stack && this.panel.tooltip.value_type === 'individual') {
121 | value = item.datapoint[1] - item.datapoint[2];
122 | }
123 | else {
124 | value = item.datapoint[1];
125 | }
126 |
127 | value = series.formatValue(value);
128 |
129 | seriesHoverInfo = this._getMultiSeriesPlotHoverInfo(plotData, pos);
130 | absoluteTime = this.dashboard.formatDate(seriesHoverInfo.time, tooltipFormat);
131 |
132 | group += '
' + value + '
';
133 |
134 | this._renderAndShow(absoluteTime, group, pos, xMode);
135 | }
136 | // no hit
137 | else {
138 | this.$tooltip.detach();
139 | }
140 | };
141 |
142 |
143 | destroy() {
144 | this._visible = false;
145 | this.$tooltip.remove();
146 | };
147 |
148 | get visible() { return this._visible; }
149 |
150 | private _findHoverIndexFromDataPoints(posX, series, last) {
151 | var ps = series.datapoints.pointsize;
152 | var initial = last * ps;
153 | var len = series.datapoints.points.length;
154 | for (var j = initial; j < len; j += ps) {
155 | // Special case of a non stepped line, highlight the very last point just before a null point
156 | if ((!series.lines.steps && series.datapoints.points[initial] != null && series.datapoints.points[j] == null)
157 | //normal case
158 | || series.datapoints.points[j] > posX) {
159 | return Math.max(j - ps, 0) / ps;
160 | }
161 | }
162 | return j / ps - 1;
163 | };
164 |
165 | private _findHoverIndexFromData(posX, series) {
166 | var lower = 0;
167 | var upper = series.data.length - 1;
168 | var middle;
169 | while (true) {
170 | if (lower > upper) {
171 | return Math.max(upper, 0);
172 | }
173 | middle = Math.floor((lower + upper) / 2);
174 | if (series.data[middle][0] === posX) {
175 | return middle;
176 | } else if (series.data[middle][0] < posX) {
177 | lower = middle + 1;
178 | } else {
179 | upper = middle - 1;
180 | }
181 | }
182 | };
183 |
184 | private _renderAndShow(absoluteTime, innerHtml, pos, xMode) {
185 | if (xMode === 'time') {
186 | innerHtml = '
' + absoluteTime + '
' + innerHtml;
187 | }
188 | (this.$tooltip.html(innerHtml) as any).place_tt(pos.pageX + 20, pos.pageY);
189 | };
190 |
191 | private _getMultiSeriesPlotHoverInfo(seriesList, pos): { series: any[][], time: any } {
192 | var value, series, hoverIndex, hoverDistance, pointTime, yaxis;
193 | // 3 sub-arrays, 1st for hidden series, 2nd for left yaxis, 3rd for right yaxis.
194 | var results = [[], [], []];
195 |
196 | //now we know the current X (j) position for X and Y values
197 | var lastValue = 0; //needed for stacked values
198 |
199 | var minDistance, minTime;
200 |
201 | for (let i = 0; i < seriesList.length; i++) {
202 | series = seriesList[i];
203 |
204 | if (!series.data.length || (this.panel.legend.hideEmpty && series.allIsNull)) {
205 | // Init value so that it does not brake series sorting
206 | results[0].push({ hidden: true, value: 0 });
207 | continue;
208 | }
209 |
210 | if (!series.data.length || (this.panel.legend.hideZero && series.allIsZero)) {
211 | // Init value so that it does not brake series sorting
212 | results[0].push({ hidden: true, value: 0 });
213 | continue;
214 | }
215 |
216 | hoverIndex = this._findHoverIndexFromData(pos.x, series);
217 | hoverDistance = pos.x - series.data[hoverIndex][0];
218 | pointTime = series.data[hoverIndex][0];
219 |
220 | // Take the closest point before the cursor, or if it does not exist, the closest after
221 | if (!minDistance
222 | || (hoverDistance >= 0 && (hoverDistance < minDistance || minDistance < 0))
223 | || (hoverDistance < 0 && hoverDistance > minDistance)
224 | ) {
225 | minDistance = hoverDistance;
226 | minTime = pointTime;
227 | }
228 |
229 | if (series.stack) {
230 | if (this.panel.tooltip.value_type === 'individual') {
231 | value = series.data[hoverIndex][1];
232 | } else if (!series.stack) {
233 | value = series.data[hoverIndex][1];
234 | } else {
235 | lastValue += series.data[hoverIndex][1];
236 | value = lastValue;
237 | }
238 | } else {
239 | value = series.data[hoverIndex][1];
240 | }
241 |
242 | // Highlighting multiple Points depending on the plot type
243 | if (series.lines.steps || series.stack) {
244 | // stacked and steppedLine plots can have series with different length.
245 | // Stacked series can increase its length on each new stacked serie if null points found,
246 | // to speed the index search we begin always on the last found hoverIndex.
247 | hoverIndex = this._findHoverIndexFromDataPoints(pos.x, series, hoverIndex);
248 | }
249 |
250 | // Be sure we have a yaxis so that it does not brake series sorting
251 | yaxis = 0;
252 | if (series.yaxis) {
253 | yaxis = series.yaxis.n;
254 | }
255 |
256 | results[yaxis].push({
257 | value: value,
258 | hoverIndex: hoverIndex,
259 | color: series.color,
260 | label: series.aliasEscaped,
261 | time: pointTime,
262 | distance: hoverDistance,
263 | index: i
264 | });
265 | }
266 |
267 | // Contat the 3 sub-arrays
268 | results = results[0].concat(results[1], results[2]);
269 |
270 | // Time of the point closer to pointer
271 |
272 | return { series: results, time: minTime };
273 | };
274 | }
275 |
276 |
--------------------------------------------------------------------------------
/dist/img/icn-graph-panel.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
--------------------------------------------------------------------------------
/src/img/icn-graph-panel.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
--------------------------------------------------------------------------------
/src/vendor/grafana/time_series2.ts:
--------------------------------------------------------------------------------
1 | import kbn from 'grafana/app/core/utils/kbn';
2 | import { getFlotTickDecimals } from './ticks';
3 | import _ from 'lodash';
4 |
5 | function matchSeriesOverride(aliasOrRegex, seriesAlias) {
6 | if (!aliasOrRegex) {
7 | return false;
8 | }
9 |
10 | if (aliasOrRegex[0] === '/') {
11 | var regex = kbn.stringToJsRegex(aliasOrRegex);
12 | return seriesAlias.match(regex) != null;
13 | }
14 |
15 | return aliasOrRegex === seriesAlias;
16 | }
17 |
18 | function translateFillOption(fill) {
19 | return fill === 0 ? 0.001 : fill / 10;
20 | }
21 |
22 | /**
23 | * Calculate decimals for legend and update values for each series.
24 | * @param data series data
25 | * @param panel
26 | */
27 | export function updateLegendValues(data: TimeSeries[], panel) {
28 | for (let i = 0; i < data.length; i++) {
29 | let series = data[i];
30 | let yaxes = panel.yaxes;
31 | const seriesYAxis = series.yaxis || 1;
32 | let axis = yaxes[seriesYAxis - 1];
33 | let { tickDecimals, scaledDecimals } = getFlotTickDecimals(data, axis);
34 | let formater = kbn.valueFormats[panel.yaxes[seriesYAxis - 1].format];
35 |
36 | // decimal override
37 | if (_.isNumber(panel.decimals)) {
38 | series.updateLegendValues(formater, panel.decimals, null);
39 | } else {
40 | // auto decimals
41 | // legend and tooltip gets one more decimal precision
42 | // than graph legend ticks
43 | tickDecimals = (tickDecimals || -1) + 1;
44 | series.updateLegendValues(formater, tickDecimals, scaledDecimals + 2);
45 | }
46 | }
47 | }
48 |
49 | export function getDataMinMax(data: TimeSeries[]) {
50 | let datamin = null;
51 | let datamax = null;
52 |
53 | for (let series of data) {
54 | if (datamax === null || datamax < series.stats.max) {
55 | datamax = series.stats.max;
56 | }
57 | if (datamin === null || datamin > series.stats.min) {
58 | datamin = series.stats.min;
59 | }
60 | }
61 |
62 | return { datamin, datamax };
63 | }
64 |
65 | export default class TimeSeries {
66 | datapoints: any;
67 | id: string;
68 | label: string;
69 | alias: string;
70 | aliasEscaped: string;
71 | color: string;
72 | valueFormater: any;
73 | stats: any;
74 | legend: boolean;
75 | allIsNull: boolean;
76 | allIsZero: boolean;
77 | decimals: number;
78 | scaledDecimals: number;
79 | hasMsResolution: boolean;
80 | isOutsideRange: boolean;
81 |
82 | lines: any;
83 | dashes: any;
84 | bars: any;
85 | points: any;
86 | yaxis: any;
87 | zindex: any;
88 | stack: any;
89 | nullPointMode: any;
90 | fillBelowTo: any;
91 | transform: any;
92 | flotpairs: any;
93 | unit: any;
94 |
95 | constructor(opts) {
96 | this.datapoints = opts.datapoints;
97 | this.label = opts.alias;
98 | this.id = opts.alias;
99 | this.alias = opts.alias;
100 | this.aliasEscaped = _.escape(opts.alias);
101 | this.color = opts.color;
102 | this.valueFormater = kbn.valueFormats.none;
103 | this.stats = {};
104 | this.legend = true;
105 | this.unit = opts.unit;
106 | this.hasMsResolution = this.isMsResolutionNeeded();
107 | }
108 |
109 | applySeriesOverrides(overrides) {
110 | this.lines = {};
111 | this.dashes = {
112 | dashLength: [],
113 | };
114 | this.points = {};
115 | this.bars = {};
116 | this.yaxis = 1;
117 | this.zindex = 0;
118 | this.nullPointMode = null;
119 | delete this.stack;
120 |
121 | for (var i = 0; i < overrides.length; i++) {
122 | var override = overrides[i];
123 | if (!matchSeriesOverride(override.alias, this.alias)) {
124 | continue;
125 | }
126 | if (override.lines !== void 0) {
127 | this.lines.show = override.lines;
128 | }
129 | if (override.dashes !== void 0) {
130 | this.dashes.show = override.dashes;
131 | this.lines.lineWidth = 0;
132 | }
133 | if (override.points !== void 0) {
134 | this.points.show = override.points;
135 | }
136 | if (override.bars !== void 0) {
137 | this.bars.show = override.bars;
138 | }
139 | if (override.fill !== void 0) {
140 | this.lines.fill = translateFillOption(override.fill);
141 | }
142 | if (override.stack !== void 0) {
143 | this.stack = override.stack;
144 | }
145 | if (override.linewidth !== void 0) {
146 | this.lines.lineWidth = this.dashes.show ? 0 : override.linewidth;
147 | this.dashes.lineWidth = override.linewidth;
148 | }
149 | if (override.dashLength !== void 0) {
150 | this.dashes.dashLength[0] = override.dashLength;
151 | }
152 | if (override.spaceLength !== void 0) {
153 | this.dashes.dashLength[1] = override.spaceLength;
154 | }
155 | if (override.nullPointMode !== void 0) {
156 | this.nullPointMode = override.nullPointMode;
157 | }
158 | if (override.pointradius !== void 0) {
159 | this.points.radius = override.pointradius;
160 | }
161 | if (override.steppedLine !== void 0) {
162 | this.lines.steps = override.steppedLine;
163 | }
164 | if (override.zindex !== void 0) {
165 | this.zindex = override.zindex;
166 | }
167 | if (override.fillBelowTo !== void 0) {
168 | this.fillBelowTo = override.fillBelowTo;
169 | }
170 | if (override.color !== void 0) {
171 | this.color = override.color;
172 | }
173 | if (override.transform !== void 0) {
174 | this.transform = override.transform;
175 | }
176 | if (override.legend !== void 0) {
177 | this.legend = override.legend;
178 | }
179 |
180 | if (override.yaxis !== void 0) {
181 | this.yaxis = override.yaxis;
182 | }
183 | }
184 | }
185 |
186 | getFlotPairs(fillStyle) {
187 | var result = [];
188 |
189 | this.stats.total = 0;
190 | this.stats.max = -Number.MAX_VALUE;
191 | this.stats.min = Number.MAX_VALUE;
192 | this.stats.logmin = Number.MAX_VALUE;
193 | this.stats.avg = null;
194 | this.stats.current = null;
195 | this.stats.first = null;
196 | this.stats.delta = 0;
197 | this.stats.diff = null;
198 | this.stats.range = null;
199 | this.stats.timeStep = Number.MAX_VALUE;
200 | this.allIsNull = true;
201 | this.allIsZero = true;
202 |
203 | var ignoreNulls = fillStyle === 'connected';
204 | var nullAsZero = fillStyle === 'null as zero';
205 | var currentTime;
206 | var currentValue;
207 | var nonNulls = 0;
208 | var previousTime;
209 | var previousValue = 0;
210 | var previousDeltaUp = true;
211 |
212 | for (var i = 0; i < this.datapoints.length; i++) {
213 | currentValue = this.datapoints[i][0];
214 | currentTime = this.datapoints[i][1];
215 |
216 | // Due to missing values we could have different timeStep all along the series
217 | // so we have to find the minimum one (could occur with aggregators such as ZimSum)
218 | if (previousTime !== undefined) {
219 | let timeStep = currentTime - previousTime;
220 | if (timeStep < this.stats.timeStep) {
221 | this.stats.timeStep = timeStep;
222 | }
223 | }
224 | previousTime = currentTime;
225 |
226 | if (currentValue === null) {
227 | if (ignoreNulls) {
228 | continue;
229 | }
230 | if (nullAsZero) {
231 | currentValue = 0;
232 | }
233 | }
234 |
235 | if (currentValue !== null) {
236 | if (_.isNumber(currentValue)) {
237 | this.stats.total += currentValue;
238 | this.allIsNull = false;
239 | nonNulls++;
240 | }
241 |
242 | if (currentValue > this.stats.max) {
243 | this.stats.max = currentValue;
244 | }
245 |
246 | if (currentValue < this.stats.min) {
247 | this.stats.min = currentValue;
248 | }
249 |
250 | if (this.stats.first === null) {
251 | this.stats.first = currentValue;
252 | } else {
253 | if (previousValue > currentValue) {
254 | // counter reset
255 | previousDeltaUp = false;
256 | if (i === this.datapoints.length - 1) {
257 | // reset on last
258 | this.stats.delta += currentValue;
259 | }
260 | } else {
261 | if (previousDeltaUp) {
262 | this.stats.delta += currentValue - previousValue; // normal increment
263 | } else {
264 | this.stats.delta += currentValue; // account for counter reset
265 | }
266 | previousDeltaUp = true;
267 | }
268 | }
269 | previousValue = currentValue;
270 |
271 | if (currentValue < this.stats.logmin && currentValue > 0) {
272 | this.stats.logmin = currentValue;
273 | }
274 |
275 | if (currentValue !== 0) {
276 | this.allIsZero = false;
277 | }
278 | }
279 |
280 | result.push([currentTime, currentValue]);
281 | }
282 |
283 | if (this.stats.max === -Number.MAX_VALUE) {
284 | this.stats.max = null;
285 | }
286 | if (this.stats.min === Number.MAX_VALUE) {
287 | this.stats.min = null;
288 | }
289 |
290 | if (result.length && !this.allIsNull) {
291 | this.stats.avg = this.stats.total / nonNulls;
292 | this.stats.current = result[result.length - 1][1];
293 | if (this.stats.current === null && result.length > 1) {
294 | this.stats.current = result[result.length - 2][1];
295 | }
296 | }
297 | if (this.stats.max !== null && this.stats.min !== null) {
298 | this.stats.range = this.stats.max - this.stats.min;
299 | }
300 | if (this.stats.current !== null && this.stats.first !== null) {
301 | this.stats.diff = this.stats.current - this.stats.first;
302 | }
303 |
304 | this.stats.count = result.length;
305 | return result;
306 | }
307 |
308 | updateLegendValues(formater, decimals, scaledDecimals) {
309 | this.valueFormater = formater;
310 | this.decimals = decimals;
311 | this.scaledDecimals = scaledDecimals;
312 | }
313 |
314 | formatValue(value) {
315 | if (!_.isFinite(value)) {
316 | value = null; // Prevent NaN formatting
317 | }
318 | return this.valueFormater(value, this.decimals, this.scaledDecimals);
319 | }
320 |
321 | isMsResolutionNeeded() {
322 | for (var i = 0; i < this.datapoints.length; i++) {
323 | if (this.datapoints[i][1] !== null) {
324 | var timestamp = this.datapoints[i][1].toString();
325 | if (timestamp.length === 13 && timestamp % 1000 !== 0) {
326 | return true;
327 | }
328 | }
329 | }
330 | return false;
331 | }
332 |
333 | hideFromLegend(options) {
334 | if (options.hideEmpty && this.allIsNull) {
335 | return true;
336 | }
337 | // ignore series excluded via override
338 | if (!this.legend) {
339 | return true;
340 | }
341 |
342 | // ignore zero series
343 | if (options.hideZero && this.allIsZero) {
344 | return true;
345 | }
346 |
347 | return false;
348 | }
349 | }
350 |
--------------------------------------------------------------------------------
/src/module.ts:
--------------------------------------------------------------------------------
1 |
2 | import { GraphRenderer } from './graph_renderer';
3 | import { GraphLegend } from './graph_legend';
4 | import './series_overrides_ctrl';
5 | import './thresholds_form';
6 |
7 | import template from './template';
8 | import _ from 'lodash';
9 | import config from 'grafana/app/core/config';
10 | import { MetricsPanelCtrl, alertTab } from 'grafana/app/plugins/sdk';
11 | import { DataProcessor } from './data_processor';
12 | import { axesEditorComponent } from './axes_editor';
13 |
14 | import $ from 'jquery';
15 |
16 | class GraphCtrl extends MetricsPanelCtrl {
17 | static template = template;
18 |
19 | hiddenSeries: any = {};
20 | seriesList: any = [];
21 | dataList: any = [];
22 | annotations: any = [];
23 | alertState: any;
24 |
25 | _panelPath: any;
26 |
27 | annotationsPromise: any;
28 | dataWarning: any;
29 | colors: any = [];
30 | subTabIndex: number;
31 | processor: DataProcessor;
32 |
33 | private _graphRenderer: GraphRenderer;
34 | private _graphLegend: GraphLegend;
35 |
36 | panelDefaults = {
37 | // datasource name, null = default datasource
38 | datasource: null,
39 | // sets client side (flot) or native graphite png renderer (png)
40 | renderer: 'flot',
41 | yaxes: [
42 | {
43 | label: null,
44 | show: true,
45 | logBase: 1,
46 | min: null,
47 | max: null,
48 | format: 'short',
49 | },
50 | {
51 | label: null,
52 | show: true,
53 | logBase: 1,
54 | min: null,
55 | max: null,
56 | format: 'short',
57 | },
58 | ],
59 | xaxis: {
60 | show: true,
61 | mode: 'time',
62 | name: null,
63 | values: [],
64 | buckets: null,
65 | customDateFormatShow: false,
66 | customDateFormat: ''
67 | },
68 | // show/hide lines
69 | lines: true,
70 | // fill factor
71 | fill: 1,
72 | // line width in pixels
73 | linewidth: 1,
74 | // show/hide dashed line
75 | dashes: false,
76 | // length of a dash
77 | dashLength: 10,
78 | // length of space between two dashes
79 | spaceLength: 10,
80 | // show hide points
81 | points: false,
82 | // point radius in pixels
83 | pointradius: 5,
84 | // show hide bars
85 | bars: false,
86 | // enable/disable stacking
87 | stack: false,
88 | // stack percentage mode
89 | percentage: false,
90 | // legend options
91 | legend: {
92 | show: true, // disable/enable legend
93 | values: false, // disable/enable legend values
94 | min: false,
95 | max: false,
96 | current: false,
97 | total: false,
98 | avg: false,
99 | },
100 | // how null points should be handled
101 | nullPointMode: 'null',
102 | // staircase line mode
103 | steppedLine: false,
104 | // tooltip options
105 | tooltip: {
106 | value_type: 'individual',
107 | shared: true,
108 | sort: 0,
109 | },
110 | // time overrides
111 | timeFrom: null,
112 | timeShift: null,
113 | // metric queries
114 | targets: [{}],
115 | // series color overrides
116 | aliasColors: {},
117 | // other style overrides
118 | seriesOverrides: [],
119 | thresholds: [],
120 | displayBarsSideBySide: false,
121 | labelAlign: 'left'
122 | };
123 |
124 | /** @ngInject */
125 | constructor($scope, $injector, private annotationsSrv, private popoverSrv, private contextSrv) {
126 | super($scope, $injector);
127 |
128 | // hack to show alert threshold
129 | // visit link to find out why
130 | // https://github.com/grafana/grafana/blob/master/public/app/features/alerting/threshold_mapper.ts#L3
131 | // should make it 'corpglory-multibar-graph-panel' before save
132 | // https://github.com/CorpGlory/grafana-multibar-graph-panel/issues/6#issuecomment-377238048
133 | // this.panel.type='graph';
134 |
135 | _.defaults(this.panel, this.panelDefaults);
136 | _.defaults(this.panel.tooltip, this.panelDefaults.tooltip);
137 | _.defaults(this.panel.legend, this.panelDefaults.legend);
138 | _.defaults(this.panel.xaxis, this.panelDefaults.xaxis);
139 |
140 | this.processor = new DataProcessor(this.panel);
141 |
142 | this.events.on('render', this.onRender.bind(this));
143 | this.events.on('data-received', this.onDataReceived.bind(this));
144 | this.events.on('data-error', this.onDataError.bind(this));
145 | this.events.on('data-snapshot-load', this.onDataSnapshotLoad.bind(this));
146 | this.events.on('init-edit-mode', this.onInitEditMode.bind(this));
147 | this.events.on('init-panel-actions', this.onInitPanelActions.bind(this));
148 | }
149 |
150 | link(scope, elem, attrs, ctrl) {
151 | var $graphElem = $(elem[0]).find('#multibar-graph-panel');
152 | var $legendElem = $(elem[0]).find('#multibar-graph-legend');
153 | this._graphRenderer = new GraphRenderer(
154 | $graphElem, this.timeSrv, this.contextSrv, this.$scope
155 | );
156 | this._graphLegend = new GraphLegend($legendElem, this.popoverSrv, this.$scope);
157 | }
158 |
159 | onInitEditMode() {
160 | var partialPath = this.panelPath + 'partials';
161 | this.addEditorTab('Axes', axesEditorComponent, 2);
162 | this.addEditorTab('Legend', `${partialPath}/tab_legend.html`, 3);
163 | this.addEditorTab('Display', `${partialPath}/tab_display.html`, 4);
164 |
165 | if (config.alertingEnabled) {
166 | this.addEditorTab('Alert', alertTab, 5);
167 | }
168 |
169 | this.subTabIndex = 0;
170 | }
171 |
172 | onInitPanelActions(actions) {
173 | actions.push({ text: 'Export CSV', click: 'ctrl.exportCsv()' });
174 | actions.push({ text: 'Toggle legend', click: 'ctrl.toggleLegend()' });
175 | }
176 |
177 | issueQueries(datasource) {
178 | this.annotationsPromise = this.annotationsSrv.getAnnotations({
179 | dashboard: this.dashboard,
180 | panel: this.panel,
181 | range: this.range,
182 | });
183 | return super.issueQueries(datasource);
184 | }
185 |
186 | zoomOut(evt) {
187 | this.publishAppEvent('zoom-out', 2);
188 | }
189 |
190 | onDataSnapshotLoad(snapshotData) {
191 | this.annotationsPromise = this.annotationsSrv.getAnnotations({
192 | dashboard: this.dashboard,
193 | panel: this.panel,
194 | range: this.range,
195 | });
196 | this.onDataReceived(snapshotData);
197 | }
198 |
199 | onDataError(err) {
200 | this.seriesList = [];
201 | this.annotations = [];
202 | this.render([]);
203 | }
204 |
205 | async onDataReceived(dataList) {
206 | this.dataList = dataList;
207 | this.seriesList = this.processor.getSeriesList({
208 | dataList: dataList,
209 | range: this.range,
210 | });
211 |
212 | this.dataWarning = null;
213 | const datapointsCount = this.seriesList.reduce((prev, series) => {
214 | return prev + series.datapoints.length;
215 | }, 0);
216 |
217 | if (datapointsCount === 0) {
218 | this.dataWarning = {
219 | title: 'No data points',
220 | tip: 'No datapoints returned from data query',
221 | };
222 | } else {
223 | for (let series of this.seriesList) {
224 | if (series.isOutsideRange) {
225 | this.dataWarning = {
226 | title: 'Data points outside time range',
227 | tip: 'Can be caused by timezone mismatch or missing time filter in query',
228 | };
229 | break;
230 | }
231 | }
232 | }
233 |
234 | if(this.annotationsPromise !== undefined) {
235 | const result = await this.annotationsPromise;
236 | this.alertState = result.alertState;
237 | this.annotations = result.annotations;
238 | }
239 | this.loading = false;
240 | this.render(this.seriesList);
241 | }
242 |
243 | onRender(data) {
244 | if (!this.seriesList) {
245 | return;
246 | }
247 |
248 | for (let series of this.seriesList) {
249 | series.applySeriesOverrides(this.panel.seriesOverrides);
250 | }
251 |
252 | this._graphRenderer.render(data);
253 | this._graphLegend.render();
254 |
255 | this._graphRenderer.renderPanel();
256 | }
257 |
258 | changeSeriesColor(series, color) {
259 | series.color = color;
260 | this.panel.aliasColors[series.alias] = series.color;
261 | this.render();
262 | }
263 |
264 | toggleSeries(serie, event) {
265 | if (event.ctrlKey || event.metaKey || event.shiftKey) {
266 | if (this.hiddenSeries[serie.alias]) {
267 | delete this.hiddenSeries[serie.alias];
268 | } else {
269 | this.hiddenSeries[serie.alias] = true;
270 | }
271 | } else {
272 | this.toggleSeriesExclusiveMode(serie);
273 | }
274 | this.render();
275 | }
276 |
277 | toggleSeriesExclusiveMode(serie) {
278 | var hidden = this.hiddenSeries;
279 |
280 | if (hidden[serie.alias]) {
281 | delete hidden[serie.alias];
282 | }
283 |
284 | // check if every other series is hidden
285 | var alreadyExclusive = _.every(this.seriesList, value => {
286 | if (value.alias === serie.alias) {
287 | return true;
288 | }
289 |
290 | return hidden[value.alias];
291 | });
292 |
293 | if (alreadyExclusive) {
294 | // remove all hidden series
295 | _.each(this.seriesList, value => {
296 | delete this.hiddenSeries[value.alias];
297 | });
298 | } else {
299 | // hide all but this serie
300 | _.each(this.seriesList, value => {
301 | if (value.alias === serie.alias) {
302 | return;
303 | }
304 |
305 | this.hiddenSeries[value.alias] = true;
306 | });
307 | }
308 | }
309 |
310 | toggleAxis(info) {
311 | var override: any = _.find(this.panel.seriesOverrides, { alias: info.alias });
312 | if (!override) {
313 | override = { alias: info.alias };
314 | this.panel.seriesOverrides.push(override);
315 | }
316 | info.yaxis = override.yaxis = info.yaxis === 2 ? 1 : 2;
317 | this.render();
318 | }
319 |
320 | addSeriesOverride(override) {
321 | this.panel.seriesOverrides.push(override || {});
322 | }
323 |
324 | removeSeriesOverride(override) {
325 | this.panel.seriesOverrides = _.without(this.panel.seriesOverrides, override);
326 | this.render();
327 | }
328 |
329 | toggleLegend() {
330 | this.panel.legend.show = !this.panel.legend.show;
331 | this.refresh();
332 | }
333 |
334 | legendValuesOptionChanged() {
335 | var legend = this.panel.legend;
336 | legend.values = legend.min || legend.max || legend.avg || legend.current || legend.total;
337 | this.render();
338 | }
339 |
340 | exportCsv() {
341 | var scope = this.$scope.$new(true);
342 | scope.seriesList = this.seriesList;
343 | this.publishAppEvent('show-modal', {
344 | templateHtml: '
',
345 | scope,
346 | modalClass: 'modal--narrow',
347 | });
348 | }
349 |
350 | get panelPath() {
351 | if (this._panelPath === undefined) {
352 | this._panelPath = './public/plugins/' + this.pluginId + '/';
353 | }
354 | return this._panelPath;
355 | }
356 | }
357 |
358 | export { GraphCtrl, GraphCtrl as PanelCtrl };
359 |
--------------------------------------------------------------------------------
/src/vendor/flot/jquery.flot.fillbelow.js:
--------------------------------------------------------------------------------
1 | (function($) {
2 | "use strict";
3 |
4 | var options = {
5 | series: {
6 | fillBelowTo: null
7 | }
8 | };
9 |
10 | function init(plot) {
11 | function findBelowSeries( series, allseries ) {
12 |
13 | var i;
14 |
15 | for ( i = 0; i < allseries.length; ++i ) {
16 | if ( allseries[ i ].id === series.fillBelowTo ) {
17 | return allseries[ i ];
18 | }
19 | }
20 |
21 | return null;
22 | }
23 |
24 | /* top and bottom doesn't actually matter for this, we're just using it to help make this easier to think about */
25 | /* this is a vector cross product operation */
26 | function segmentIntersection(top_left_x, top_left_y, top_right_x, top_right_y, bottom_left_x, bottom_left_y, bottom_right_x, bottom_right_y) {
27 | var top_delta_x, top_delta_y, bottom_delta_x, bottom_delta_y,
28 | s, t;
29 |
30 | top_delta_x = top_right_x - top_left_x;
31 | top_delta_y = top_right_y - top_left_y;
32 | bottom_delta_x = bottom_right_x - bottom_left_x;
33 | bottom_delta_y = bottom_right_y - bottom_left_y;
34 |
35 | s = (
36 | (-top_delta_y * (top_left_x - bottom_left_x)) + (top_delta_x * (top_left_y - bottom_left_y))
37 | ) / (
38 | -bottom_delta_x * top_delta_y + top_delta_x * bottom_delta_y
39 | );
40 |
41 | t = (
42 | (bottom_delta_x * (top_left_y - bottom_left_y)) - (bottom_delta_y * (top_left_x - bottom_left_x))
43 | ) / (
44 | -bottom_delta_x * top_delta_y + top_delta_x * bottom_delta_y
45 | );
46 |
47 | // Collision detected
48 | if (s >= 0 && s <= 1 && t >= 0 && t <= 1) {
49 | return [
50 | top_left_x + (t * top_delta_x), // X
51 | top_left_y + (t * top_delta_y) // Y
52 | ];
53 | }
54 |
55 | // No collision
56 | return null;
57 | }
58 |
59 | function plotDifferenceArea(plot, ctx, series) {
60 | if ( series.fillBelowTo === null ) {
61 | return;
62 | }
63 |
64 | var otherseries,
65 |
66 | ps,
67 | points,
68 |
69 | otherps,
70 | otherpoints,
71 |
72 | plotOffset,
73 | fillStyle;
74 |
75 | function openPolygon(x, y) {
76 | ctx.beginPath();
77 | ctx.moveTo(
78 | series.xaxis.p2c(x) + plotOffset.left,
79 | series.yaxis.p2c(y) + plotOffset.top
80 | );
81 |
82 | }
83 |
84 | function closePolygon() {
85 | ctx.closePath();
86 | ctx.fill();
87 | }
88 |
89 | function validateInput() {
90 | if (points.length/ps !== otherpoints.length/otherps) {
91 | console.error("Refusing to graph inconsistent number of points");
92 | return false;
93 | }
94 |
95 | var i;
96 | for (i = 0; i < (points.length / ps); i++) {
97 | if (
98 | points[i * ps] !== null &&
99 | otherpoints[i * otherps] !== null &&
100 | points[i * ps] !== otherpoints[i * otherps]
101 | ) {
102 | console.error("Refusing to graph points without matching value");
103 | return false;
104 | }
105 | }
106 |
107 | return true;
108 | }
109 |
110 | function findNextStart(start_i, end_i) {
111 | console.assert(end_i > start_i, "expects the end index to be greater than the start index");
112 |
113 | var start = (
114 | start_i === 0 ||
115 | points[start_i - 1] === null ||
116 | otherpoints[start_i - 1] === null
117 | ),
118 | equal = false,
119 | i,
120 | intersect;
121 |
122 | for (i = start_i; i < end_i; i++) {
123 | // Take note of null points
124 | if (
125 | points[(i * ps) + 1] === null ||
126 | otherpoints[(i * ps) + 1] === null
127 | ) {
128 | equal = false;
129 | start = true;
130 | }
131 |
132 | // Take note of equal points
133 | else if (points[(i * ps) + 1] === otherpoints[(i * otherps) + 1]) {
134 | equal = true;
135 | start = false;
136 | }
137 |
138 |
139 | else if (points[(i * ps) + 1] > otherpoints[(i * otherps) + 1]) {
140 | // If we begin above the desired point
141 | if (start) {
142 | openPolygon(points[i * ps], points[(i * ps) + 1]);
143 | }
144 |
145 | // If an equal point preceeds this, start the polygon at that equal point
146 | else if (equal) {
147 | openPolygon(points[(i - 1) * ps], points[((i - 1) * ps) + 1]);
148 | }
149 |
150 | // Otherwise, find the intersection point, and start it there
151 | else {
152 | intersect = intersectionPoint(i);
153 | openPolygon(intersect[0], intersect[1]);
154 | }
155 |
156 | topTraversal(i, end_i);
157 | return;
158 | }
159 |
160 | // If we go below equal, equal at any preceeding point is irrelevant
161 | else {
162 | start = false;
163 | equal = false;
164 | }
165 | }
166 | }
167 |
168 | function intersectionPoint(right_i) {
169 | console.assert(right_i > 0, "expects the second point in the series line segment");
170 |
171 | var i, intersect;
172 |
173 | for (i = 1; i < (otherpoints.length/otherps); i++) {
174 | intersect = segmentIntersection(
175 | points[(right_i - 1) * ps], points[((right_i - 1) * ps) + 1],
176 | points[right_i * ps], points[(right_i * ps) + 1],
177 |
178 | otherpoints[(i - 1) * otherps], otherpoints[((i - 1) * otherps) + 1],
179 | otherpoints[i * otherps], otherpoints[(i * otherps) + 1]
180 | );
181 |
182 | if (intersect !== null) {
183 | return intersect;
184 | }
185 | }
186 |
187 | console.error("intersectionPoint() should only be called when an intersection happens");
188 | }
189 |
190 | function bottomTraversal(start_i, end_i) {
191 | console.assert(start_i >= end_i, "the start should be the rightmost point, and the end should be the leftmost (excluding the equal or intersecting point)");
192 |
193 | var i;
194 |
195 | for (i = start_i; i >= end_i; i--) {
196 | ctx.lineTo(
197 | otherseries.xaxis.p2c(otherpoints[i * otherps]) + plotOffset.left,
198 | otherseries.yaxis.p2c(otherpoints[(i * otherps) + 1]) + plotOffset.top
199 | );
200 | }
201 |
202 | closePolygon();
203 | }
204 |
205 | function topTraversal(start_i, end_i) {
206 | console.assert(start_i <= end_i, "the start should be the rightmost point, and the end should be the leftmost (excluding the equal or intersecting point)");
207 |
208 | var i,
209 | intersect;
210 |
211 | for (i = start_i; i < end_i; i++) {
212 | if (points[(i * ps) + 1] === null && i > start_i) {
213 | bottomTraversal(i - 1, start_i);
214 | findNextStart(i, end_i);
215 | return;
216 | }
217 |
218 | else if (points[(i * ps) + 1] === otherpoints[(i * otherps) + 1]) {
219 | bottomTraversal(i, start_i);
220 | findNextStart(i, end_i);
221 | return;
222 | }
223 |
224 | else if (points[(i * ps) + 1] < otherpoints[(i * otherps) + 1]) {
225 | intersect = intersectionPoint(i);
226 | ctx.lineTo(
227 | series.xaxis.p2c(intersect[0]) + plotOffset.left,
228 | series.yaxis.p2c(intersect[1]) + plotOffset.top
229 | );
230 | bottomTraversal(i, start_i);
231 | findNextStart(i, end_i);
232 | return;
233 |
234 | }
235 |
236 | else {
237 | ctx.lineTo(
238 | series.xaxis.p2c(points[i * ps]) + plotOffset.left,
239 | series.yaxis.p2c(points[(i * ps) + 1]) + plotOffset.top
240 | );
241 | }
242 | }
243 |
244 | bottomTraversal(end_i, start_i);
245 | }
246 |
247 |
248 | // Begin processing
249 |
250 | otherseries = findBelowSeries( series, plot.getData() );
251 |
252 | if ( !otherseries ) {
253 | return;
254 | }
255 |
256 | ps = series.datapoints.pointsize;
257 | points = series.datapoints.points;
258 | otherps = otherseries.datapoints.pointsize;
259 | otherpoints = otherseries.datapoints.points;
260 | plotOffset = plot.getPlotOffset();
261 |
262 | if (!validateInput()) {
263 | return;
264 | }
265 |
266 |
267 | // Flot's getFillStyle() should probably be exposed somewhere
268 | fillStyle = $.color.parse(series.color);
269 | fillStyle.a = 0.4;
270 | fillStyle.normalize();
271 | ctx.fillStyle = fillStyle.toString();
272 |
273 |
274 | // Begin recursive bi-directional traversal
275 | findNextStart(0, points.length/ps);
276 | }
277 |
278 | plot.hooks.drawSeries.push(plotDifferenceArea);
279 | }
280 |
281 | $.plot.plugins.push({
282 | init: init,
283 | options: options,
284 | name: "fillbelow",
285 | version: "0.1.0"
286 | });
287 |
288 | })(jQuery);
289 |
--------------------------------------------------------------------------------
/src/vendor/flot/jquery.flot.time.js:
--------------------------------------------------------------------------------
1 | /* Pretty handling of time axes.
2 |
3 | Copyright (c) 2007-2013 IOLA and Ole Laursen.
4 | Licensed under the MIT license.
5 |
6 | Set axis.mode to "time" to enable. See the section "Time series data" in
7 | API.txt for details.
8 |
9 | */
10 |
11 | (function($) {
12 |
13 | var options = {
14 | xaxis: {
15 | timezone: null, // "browser" for local to the client or timezone for timezone-js
16 | timeformat: null, // format string to use
17 | twelveHourClock: false, // 12 or 24 time in time mode
18 | monthNames: null // list of names of months
19 | }
20 | };
21 |
22 | // round to nearby lower multiple of base
23 |
24 | function floorInBase(n, base) {
25 | return base * Math.floor(n / base);
26 | }
27 |
28 | // Returns a string with the date d formatted according to fmt.
29 | // A subset of the Open Group's strftime format is supported.
30 |
31 | function formatDate(d, fmt, monthNames, dayNames) {
32 |
33 | if (typeof d.strftime == "function") {
34 | return d.strftime(fmt);
35 | }
36 |
37 | var leftPad = function(n, pad) {
38 | n = "" + n;
39 | pad = "" + (pad == null ? "0" : pad);
40 | return n.length == 1 ? pad + n : n;
41 | };
42 |
43 | var r = [];
44 | var escape = false;
45 | var hours = d.getHours();
46 | var isAM = hours < 12;
47 |
48 | if (monthNames == null) {
49 | monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
50 | }
51 |
52 | if (dayNames == null) {
53 | dayNames = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
54 | }
55 |
56 | var hours12;
57 |
58 | if (hours > 12) {
59 | hours12 = hours - 12;
60 | } else if (hours == 0) {
61 | hours12 = 12;
62 | } else {
63 | hours12 = hours;
64 | }
65 |
66 | for (var i = 0; i < fmt.length; ++i) {
67 |
68 | var c = fmt.charAt(i);
69 |
70 | if (escape) {
71 | switch (c) {
72 | case 'a': c = "" + dayNames[d.getDay()]; break;
73 | case 'b': c = "" + monthNames[d.getMonth()]; break;
74 | case 'd': c = leftPad(d.getDate(), ""); break;
75 | case 'e': c = leftPad(d.getDate(), " "); break;
76 | case 'h': // For back-compat with 0.7; remove in 1.0
77 | case 'H': c = leftPad(hours); break;
78 | case 'I': c = leftPad(hours12); break;
79 | case 'l': c = leftPad(hours12, " "); break;
80 | case 'm': c = leftPad(d.getMonth() + 1, ""); break;
81 | case 'M': c = leftPad(d.getMinutes()); break;
82 | // quarters not in Open Group's strftime specification
83 | case 'q':
84 | c = "" + (Math.floor(d.getMonth() / 3) + 1); break;
85 | case 'S': c = leftPad(d.getSeconds()); break;
86 | case 'y': c = leftPad(d.getFullYear() % 100); break;
87 | case 'Y': c = "" + d.getFullYear(); break;
88 | case 'p': c = (isAM) ? ("" + "am") : ("" + "pm"); break;
89 | case 'P': c = (isAM) ? ("" + "AM") : ("" + "PM"); break;
90 | case 'w': c = "" + d.getDay(); break;
91 | }
92 | r.push(c);
93 | escape = false;
94 | } else {
95 | if (c == "%") {
96 | escape = true;
97 | } else {
98 | r.push(c);
99 | }
100 | }
101 | }
102 |
103 | return r.join("");
104 | }
105 |
106 | // To have a consistent view of time-based data independent of which time
107 | // zone the client happens to be in we need a date-like object independent
108 | // of time zones. This is done through a wrapper that only calls the UTC
109 | // versions of the accessor methods.
110 |
111 | function makeUtcWrapper(d) {
112 |
113 | function addProxyMethod(sourceObj, sourceMethod, targetObj, targetMethod) {
114 | sourceObj[sourceMethod] = function() {
115 | return targetObj[targetMethod].apply(targetObj, arguments);
116 | };
117 | };
118 |
119 | var utc = {
120 | date: d
121 | };
122 |
123 | // support strftime, if found
124 |
125 | if (d.strftime != undefined) {
126 | addProxyMethod(utc, "strftime", d, "strftime");
127 | }
128 |
129 | addProxyMethod(utc, "getTime", d, "getTime");
130 | addProxyMethod(utc, "setTime", d, "setTime");
131 |
132 | var props = ["Date", "Day", "FullYear", "Hours", "Milliseconds", "Minutes", "Month", "Seconds"];
133 |
134 | for (var p = 0; p < props.length; p++) {
135 | addProxyMethod(utc, "get" + props[p], d, "getUTC" + props[p]);
136 | addProxyMethod(utc, "set" + props[p], d, "setUTC" + props[p]);
137 | }
138 |
139 | return utc;
140 | };
141 |
142 | // select time zone strategy. This returns a date-like object tied to the
143 | // desired timezone
144 |
145 | function dateGenerator(ts, opts) {
146 | if (opts.timezone == "browser") {
147 | return new Date(ts);
148 | } else if (!opts.timezone || opts.timezone == "utc") {
149 | return makeUtcWrapper(new Date(ts));
150 | } else if (typeof timezoneJS != "undefined" && typeof timezoneJS.Date != "undefined") {
151 | var d = new timezoneJS.Date();
152 | // timezone-js is fickle, so be sure to set the time zone before
153 | // setting the time.
154 | d.setTimezone(opts.timezone);
155 | d.setTime(ts);
156 | return d;
157 | } else {
158 | return makeUtcWrapper(new Date(ts));
159 | }
160 | }
161 |
162 | // map of app. size of time units in milliseconds
163 |
164 | var timeUnitSize = {
165 | "second": 1000,
166 | "minute": 60 * 1000,
167 | "hour": 60 * 60 * 1000,
168 | "day": 24 * 60 * 60 * 1000,
169 | "month": 30 * 24 * 60 * 60 * 1000,
170 | "quarter": 3 * 30 * 24 * 60 * 60 * 1000,
171 | "year": 365.2425 * 24 * 60 * 60 * 1000
172 | };
173 |
174 | // the allowed tick sizes, after 1 year we use
175 | // an integer algorithm
176 |
177 | var baseSpec = [
178 | [1, "second"], [2, "second"], [5, "second"], [10, "second"],
179 | [30, "second"],
180 | [1, "minute"], [2, "minute"], [5, "minute"], [10, "minute"],
181 | [30, "minute"],
182 | [1, "hour"], [2, "hour"], [4, "hour"],
183 | [8, "hour"], [12, "hour"],
184 | [1, "day"], [2, "day"], [3, "day"],
185 | [0.25, "month"], [0.5, "month"], [1, "month"],
186 | [2, "month"]
187 | ];
188 |
189 | // we don't know which variant(s) we'll need yet, but generating both is
190 | // cheap
191 |
192 | var specMonths = baseSpec.concat([[3, "month"], [6, "month"],
193 | [1, "year"]]);
194 | var specQuarters = baseSpec.concat([[1, "quarter"], [2, "quarter"],
195 | [1, "year"]]);
196 |
197 | function init(plot) {
198 | plot.hooks.processOptions.push(function (plot, options) {
199 | $.each(plot.getAxes(), function(axisName, axis) {
200 |
201 | var opts = axis.options;
202 |
203 | if (opts.mode == "time") {
204 | axis.tickGenerator = function(axis) {
205 |
206 | var ticks = [];
207 | var d = dateGenerator(axis.min, opts);
208 | var minSize = 0;
209 |
210 | // make quarter use a possibility if quarters are
211 | // mentioned in either of these options
212 |
213 | var spec = (opts.tickSize && opts.tickSize[1] ===
214 | "quarter") ||
215 | (opts.minTickSize && opts.minTickSize[1] ===
216 | "quarter") ? specQuarters : specMonths;
217 |
218 | if (opts.minTickSize != null) {
219 | if (typeof opts.tickSize == "number") {
220 | minSize = opts.tickSize;
221 | } else {
222 | minSize = opts.minTickSize[0] * timeUnitSize[opts.minTickSize[1]];
223 | }
224 | }
225 |
226 | for (var i = 0; i < spec.length - 1; ++i) {
227 | if (axis.delta < (spec[i][0] * timeUnitSize[spec[i][1]]
228 | + spec[i + 1][0] * timeUnitSize[spec[i + 1][1]]) / 2
229 | && spec[i][0] * timeUnitSize[spec[i][1]] >= minSize) {
230 | break;
231 | }
232 | }
233 |
234 | var size = spec[i][0];
235 | var unit = spec[i][1];
236 |
237 | // special-case the possibility of several years
238 |
239 | if (unit == "year") {
240 |
241 | // if given a minTickSize in years, just use it,
242 | // ensuring that it's an integer
243 |
244 | if (opts.minTickSize != null && opts.minTickSize[1] == "year") {
245 | size = Math.floor(opts.minTickSize[0]);
246 | } else {
247 |
248 | var magn = Math.pow(10, Math.floor(Math.log(axis.delta / timeUnitSize.year) / Math.LN10));
249 | var norm = (axis.delta / timeUnitSize.year) / magn;
250 |
251 | if (norm < 1.5) {
252 | size = 1;
253 | } else if (norm < 3) {
254 | size = 2;
255 | } else if (norm < 7.5) {
256 | size = 5;
257 | } else {
258 | size = 10;
259 | }
260 |
261 | size *= magn;
262 | }
263 |
264 | // minimum size for years is 1
265 |
266 | if (size < 1) {
267 | size = 1;
268 | }
269 | }
270 |
271 | axis.tickSize = opts.tickSize || [size, unit];
272 | var tickSize = axis.tickSize[0];
273 | unit = axis.tickSize[1];
274 |
275 | var step = tickSize * timeUnitSize[unit];
276 |
277 | if (unit == "second") {
278 | d.setSeconds(floorInBase(d.getSeconds(), tickSize));
279 | } else if (unit == "minute") {
280 | d.setMinutes(floorInBase(d.getMinutes(), tickSize));
281 | } else if (unit == "hour") {
282 | d.setHours(floorInBase(d.getHours(), tickSize));
283 | } else if (unit == "month") {
284 | d.setMonth(floorInBase(d.getMonth(), tickSize));
285 | } else if (unit == "quarter") {
286 | d.setMonth(3 * floorInBase(d.getMonth() / 3,
287 | tickSize));
288 | } else if (unit == "year") {
289 | d.setFullYear(floorInBase(d.getFullYear(), tickSize));
290 | }
291 |
292 | // reset smaller components
293 |
294 | d.setMilliseconds(0);
295 |
296 | if (step >= timeUnitSize.minute) {
297 | d.setSeconds(0);
298 | }
299 | if (step >= timeUnitSize.hour) {
300 | d.setMinutes(0);
301 | }
302 | if (step >= timeUnitSize.day) {
303 | d.setHours(0);
304 | }
305 | if (step >= timeUnitSize.day * 4) {
306 | d.setDate(1);
307 | }
308 | if (step >= timeUnitSize.month * 2) {
309 | d.setMonth(floorInBase(d.getMonth(), 3));
310 | }
311 | if (step >= timeUnitSize.quarter * 2) {
312 | d.setMonth(floorInBase(d.getMonth(), 6));
313 | }
314 | if (step >= timeUnitSize.year) {
315 | d.setMonth(0);
316 | }
317 |
318 | var carry = 0;
319 | var v = Number.NaN;
320 | var prev;
321 |
322 | do {
323 |
324 | prev = v;
325 | v = d.getTime();
326 | ticks.push(v);
327 |
328 | if (unit == "month" || unit == "quarter") {
329 | if (tickSize < 1) {
330 |
331 | // a bit complicated - we'll divide the
332 | // month/quarter up but we need to take
333 | // care of fractions so we don't end up in
334 | // the middle of a day
335 |
336 | d.setDate(1);
337 | var start = d.getTime();
338 | d.setMonth(d.getMonth() +
339 | (unit == "quarter" ? 3 : 1));
340 | var end = d.getTime();
341 | d.setTime(v + carry * timeUnitSize.hour + (end - start) * tickSize);
342 | carry = d.getHours();
343 | d.setHours(0);
344 | } else {
345 | d.setMonth(d.getMonth() +
346 | tickSize * (unit == "quarter" ? 3 : 1));
347 | }
348 | } else if (unit == "year") {
349 | d.setFullYear(d.getFullYear() + tickSize);
350 | } else {
351 | d.setTime(v + step);
352 | }
353 | } while (v < axis.max && v != prev);
354 |
355 | return ticks;
356 | };
357 |
358 | axis.tickFormatter = function (v, axis) {
359 |
360 | var d = dateGenerator(v, axis.options);
361 |
362 | // first check global format
363 |
364 | if (opts.timeformat != null) {
365 | return formatDate(d, opts.timeformat, opts.monthNames, opts.dayNames);
366 | }
367 |
368 | // possibly use quarters if quarters are mentioned in
369 | // any of these places
370 |
371 | var useQuarters = (axis.options.tickSize &&
372 | axis.options.tickSize[1] == "quarter") ||
373 | (axis.options.minTickSize &&
374 | axis.options.minTickSize[1] == "quarter");
375 |
376 | var t = axis.tickSize[0] * timeUnitSize[axis.tickSize[1]];
377 | var span = axis.max - axis.min;
378 | var suffix = (opts.twelveHourClock) ? " %p" : "";
379 | var hourCode = (opts.twelveHourClock) ? "%I" : "%H";
380 | var fmt;
381 |
382 | if (t < timeUnitSize.minute) {
383 | fmt = hourCode + ":%M:%S" + suffix;
384 | } else if (t < timeUnitSize.day) {
385 | if (span < 2 * timeUnitSize.day) {
386 | fmt = hourCode + ":%M" + suffix;
387 | } else {
388 | fmt = "%b %d " + hourCode + ":%M" + suffix;
389 | }
390 | } else if (t < timeUnitSize.month) {
391 | fmt = "%b %d";
392 | } else if ((useQuarters && t < timeUnitSize.quarter) ||
393 | (!useQuarters && t < timeUnitSize.year)) {
394 | if (span < timeUnitSize.year) {
395 | fmt = "%b";
396 | } else {
397 | fmt = "%b %Y";
398 | }
399 | } else if (useQuarters && t < timeUnitSize.year) {
400 | if (span < timeUnitSize.year) {
401 | fmt = "Q%q";
402 | } else {
403 | fmt = "Q%q %Y";
404 | }
405 | } else {
406 | fmt = "%Y";
407 | }
408 |
409 | var rt = formatDate(d, fmt, opts.monthNames, opts.dayNames);
410 |
411 | return rt;
412 | };
413 | }
414 | });
415 | });
416 | }
417 |
418 | $.plot.plugins.push({
419 | init: init,
420 | options: options,
421 | name: 'time',
422 | version: '1.0'
423 | });
424 |
425 | // Time-axis support used to be in Flot core, which exposed the
426 | // formatDate function on the plot object. Various plugins depend
427 | // on the function, so we need to re-expose it here.
428 |
429 | $.plot.formatDate = formatDate;
430 |
431 | })(jQuery);
432 |
--------------------------------------------------------------------------------
/src/vendor/flot/jquery.flot.selection.js:
--------------------------------------------------------------------------------
1 | /* Flot plugin for selecting regions of a plot.
2 |
3 | Copyright (c) 2007-2013 IOLA and Ole Laursen.
4 | Licensed under the MIT license.
5 |
6 | The plugin supports these options:
7 |
8 | selection: {
9 | mode: null or "x" or "y" or "xy",
10 | color: color,
11 | shape: "round" or "miter" or "bevel",
12 | minSize: number of pixels
13 | }
14 |
15 | Selection support is enabled by setting the mode to one of "x", "y" or "xy".
16 | In "x" mode, the user will only be able to specify the x range, similarly for
17 | "y" mode. For "xy", the selection becomes a rectangle where both ranges can be
18 | specified. "color" is color of the selection (if you need to change the color
19 | later on, you can get to it with plot.getOptions().selection.color). "shape"
20 | is the shape of the corners of the selection.
21 |
22 | "minSize" is the minimum size a selection can be in pixels. This value can
23 | be customized to determine the smallest size a selection can be and still
24 | have the selection rectangle be displayed. When customizing this value, the
25 | fact that it refers to pixels, not axis units must be taken into account.
26 | Thus, for example, if there is a bar graph in time mode with BarWidth set to 1
27 | minute, setting "minSize" to 1 will not make the minimum selection size 1
28 | minute, but rather 1 pixel. Note also that setting "minSize" to 0 will prevent
29 | "plotunselected" events from being fired when the user clicks the mouse without
30 | dragging.
31 |
32 | When selection support is enabled, a "plotselected" event will be emitted on
33 | the DOM element you passed into the plot function. The event handler gets a
34 | parameter with the ranges selected on the axes, like this:
35 |
36 | placeholder.bind( "plotselected", function( event, ranges ) {
37 | alert("You selected " + ranges.xaxis.from + " to " + ranges.xaxis.to)
38 | // similar for yaxis - with multiple axes, the extra ones are in
39 | // x2axis, x3axis, ...
40 | });
41 |
42 | The "plotselected" event is only fired when the user has finished making the
43 | selection. A "plotselecting" event is fired during the process with the same
44 | parameters as the "plotselected" event, in case you want to know what's
45 | happening while it's happening,
46 |
47 | A "plotunselected" event with no arguments is emitted when the user clicks the
48 | mouse to remove the selection. As stated above, setting "minSize" to 0 will
49 | destroy this behavior.
50 |
51 | The plugin allso adds the following methods to the plot object:
52 |
53 | - setSelection( ranges, preventEvent )
54 |
55 | Set the selection rectangle. The passed in ranges is on the same form as
56 | returned in the "plotselected" event. If the selection mode is "x", you
57 | should put in either an xaxis range, if the mode is "y" you need to put in
58 | an yaxis range and both xaxis and yaxis if the selection mode is "xy", like
59 | this:
60 |
61 | setSelection({ xaxis: { from: 0, to: 10 }, yaxis: { from: 40, to: 60 } });
62 |
63 | setSelection will trigger the "plotselected" event when called. If you don't
64 | want that to happen, e.g. if you're inside a "plotselected" handler, pass
65 | true as the second parameter. If you are using multiple axes, you can
66 | specify the ranges on any of those, e.g. as x2axis/x3axis/... instead of
67 | xaxis, the plugin picks the first one it sees.
68 |
69 | - clearSelection( preventEvent )
70 |
71 | Clear the selection rectangle. Pass in true to avoid getting a
72 | "plotunselected" event.
73 |
74 | - getSelection()
75 |
76 | Returns the current selection in the same format as the "plotselected"
77 | event. If there's currently no selection, the function returns null.
78 |
79 | */
80 |
81 | (function ($) {
82 | function init(plot) {
83 | var selection = {
84 | first: { x: -1, y: -1}, second: { x: -1, y: -1},
85 | show: false,
86 | active: false
87 | };
88 |
89 | // FIXME: The drag handling implemented here should be
90 | // abstracted out, there's some similar code from a library in
91 | // the navigation plugin, this should be massaged a bit to fit
92 | // the Flot cases here better and reused. Doing this would
93 | // make this plugin much slimmer.
94 | var savedhandlers = {};
95 |
96 | var mouseUpHandler = null;
97 |
98 | function onMouseMove(e) {
99 | if (selection.active) {
100 | updateSelection(e);
101 |
102 | plot.getPlaceholder().trigger("plotselecting", [ getSelection() ]);
103 | }
104 | }
105 |
106 | function onMouseDown(e) {
107 | if (e.which != 1) // only accept left-click
108 | return;
109 |
110 | // cancel out any text selections
111 | document.body.focus();
112 |
113 | // prevent text selection and drag in old-school browsers
114 | if (document.onselectstart !== undefined && savedhandlers.onselectstart == null) {
115 | savedhandlers.onselectstart = document.onselectstart;
116 | document.onselectstart = function () { return false; };
117 | }
118 | if (document.ondrag !== undefined && savedhandlers.ondrag == null) {
119 | savedhandlers.ondrag = document.ondrag;
120 | document.ondrag = function () { return false; };
121 | }
122 |
123 | setSelectionPos(selection.first, e);
124 |
125 | selection.active = true;
126 |
127 | // this is a bit silly, but we have to use a closure to be
128 | // able to whack the same handler again
129 | mouseUpHandler = function (e) { onMouseUp(e); };
130 |
131 | $(document).one("mouseup", mouseUpHandler);
132 | }
133 |
134 | function onMouseUp(e) {
135 | mouseUpHandler = null;
136 |
137 | // revert drag stuff for old-school browsers
138 | if (document.onselectstart !== undefined)
139 | document.onselectstart = savedhandlers.onselectstart;
140 | if (document.ondrag !== undefined)
141 | document.ondrag = savedhandlers.ondrag;
142 |
143 | // no more dragging
144 | selection.active = false;
145 | updateSelection(e);
146 |
147 | if (selectionIsSane())
148 | triggerSelectedEvent(e);
149 | else {
150 | // this counts as a clear
151 | plot.getPlaceholder().trigger("plotunselected", [ ]);
152 | plot.getPlaceholder().trigger("plotselecting", [ null ]);
153 | }
154 |
155 | setTimeout(function() {
156 | plot.isSelecting = false;
157 | }, 10);
158 |
159 | return false;
160 | }
161 |
162 | function getSelection() {
163 | if (!selectionIsSane())
164 | return null;
165 |
166 | if (!selection.show) return null;
167 |
168 | var r = {}, c1 = selection.first, c2 = selection.second;
169 | var axes = plot.getAxes();
170 | // look if no axis is used
171 | var noAxisInUse = true;
172 | $.each(axes, function (name, axis) {
173 | if (axis.used) {
174 | anyUsed = false;
175 | }
176 | })
177 |
178 | $.each(axes, function (name, axis) {
179 | if (axis.used || noAxisInUse) {
180 | var p1 = axis.c2p(c1[axis.direction]), p2 = axis.c2p(c2[axis.direction]);
181 | r[name] = { from: Math.min(p1, p2), to: Math.max(p1, p2) };
182 | }
183 | });
184 | return r;
185 | }
186 |
187 | function triggerSelectedEvent(event) {
188 | var r = getSelection();
189 |
190 | // Add ctrlKey and metaKey to event
191 | r.ctrlKey = event.ctrlKey;
192 | r.metaKey = event.metaKey;
193 |
194 | plot.getPlaceholder().trigger("plotselected", [ r ]);
195 |
196 | // backwards-compat stuff, to be removed in future
197 | if (r.xaxis && r.yaxis)
198 | plot.getPlaceholder().trigger("selected", [ { x1: r.xaxis.from, y1: r.yaxis.from, x2: r.xaxis.to, y2: r.yaxis.to } ]);
199 | }
200 |
201 | function clamp(min, value, max) {
202 | return value < min ? min: (value > max ? max: value);
203 | }
204 |
205 | function setSelectionPos(pos, e) {
206 | var o = plot.getOptions();
207 | var offset = plot.getPlaceholder().offset();
208 | var plotOffset = plot.getPlotOffset();
209 | pos.x = clamp(0, e.pageX - offset.left - plotOffset.left, plot.width());
210 | pos.y = clamp(0, e.pageY - offset.top - plotOffset.top, plot.height());
211 |
212 | if (o.selection.mode == "y")
213 | pos.x = pos == selection.first ? 0 : plot.width();
214 |
215 | if (o.selection.mode == "x")
216 | pos.y = pos == selection.first ? 0 : plot.height();
217 | }
218 |
219 | function updateSelection(pos) {
220 | if (pos.pageX == null)
221 | return;
222 |
223 | setSelectionPos(selection.second, pos);
224 | if (selectionIsSane()) {
225 | plot.isSelecting = true;
226 | selection.show = true;
227 | plot.triggerRedrawOverlay();
228 | }
229 | else
230 | clearSelection(true);
231 | }
232 |
233 | function clearSelection(preventEvent) {
234 | if (selection.show) {
235 | selection.show = false;
236 | plot.triggerRedrawOverlay();
237 | if (!preventEvent)
238 | plot.getPlaceholder().trigger("plotunselected", [ ]);
239 | }
240 | }
241 |
242 | // function taken from markings support in Flot
243 | function extractRange(ranges, coord) {
244 | var axis, from, to, key, axes = plot.getAxes();
245 |
246 | for (var k in axes) {
247 | axis = axes[k];
248 | if (axis.direction == coord) {
249 | key = coord + axis.n + "axis";
250 | if (!ranges[key] && axis.n == 1)
251 | key = coord + "axis"; // support x1axis as xaxis
252 | if (ranges[key]) {
253 | from = ranges[key].from;
254 | to = ranges[key].to;
255 | break;
256 | }
257 | }
258 | }
259 |
260 | // backwards-compat stuff - to be removed in future
261 | if (!ranges[key]) {
262 | axis = coord == "x" ? plot.getXAxes()[0] : plot.getYAxes()[0];
263 | from = ranges[coord + "1"];
264 | to = ranges[coord + "2"];
265 | }
266 |
267 | // auto-reverse as an added bonus
268 | if (from != null && to != null && from > to) {
269 | var tmp = from;
270 | from = to;
271 | to = tmp;
272 | }
273 |
274 | return { from: from, to: to, axis: axis };
275 | }
276 |
277 | function setSelection(ranges, preventEvent) {
278 | var axis, range, o = plot.getOptions();
279 |
280 | if (o.selection.mode == "y") {
281 | selection.first.x = 0;
282 | selection.second.x = plot.width();
283 | }
284 | else {
285 | range = extractRange(ranges, "x");
286 |
287 | selection.first.x = range.axis.p2c(range.from);
288 | selection.second.x = range.axis.p2c(range.to);
289 | }
290 |
291 | if (o.selection.mode == "x") {
292 | selection.first.y = 0;
293 | selection.second.y = plot.height();
294 | }
295 | else {
296 | range = extractRange(ranges, "y");
297 |
298 | selection.first.y = range.axis.p2c(range.from);
299 | selection.second.y = range.axis.p2c(range.to);
300 | }
301 |
302 | selection.show = true;
303 | plot.triggerRedrawOverlay();
304 | if (!preventEvent && selectionIsSane())
305 | triggerSelectedEvent();
306 | }
307 |
308 | function selectionIsSane() {
309 | var minSize = plot.getOptions().selection.minSize;
310 | return Math.abs(selection.second.x - selection.first.x) >= minSize &&
311 | Math.abs(selection.second.y - selection.first.y) >= minSize;
312 | }
313 |
314 | plot.clearSelection = clearSelection;
315 | plot.setSelection = setSelection;
316 | plot.getSelection = getSelection;
317 |
318 | plot.hooks.bindEvents.push(function(plot, eventHolder) {
319 | var o = plot.getOptions();
320 | if (o.selection.mode != null) {
321 | eventHolder.mousemove(onMouseMove);
322 | eventHolder.mousedown(onMouseDown);
323 | }
324 | });
325 |
326 |
327 | plot.hooks.drawOverlay.push(function (plot, ctx) {
328 | // draw selection
329 | if (selection.show && selectionIsSane()) {
330 | var plotOffset = plot.getPlotOffset();
331 | var o = plot.getOptions();
332 |
333 | ctx.save();
334 | ctx.translate(plotOffset.left, plotOffset.top);
335 |
336 | var c = $.color.parse(o.selection.color);
337 |
338 | ctx.strokeStyle = c.scale('a', o.selection.strokeAlpha).toString();
339 | ctx.lineWidth = 1;
340 | ctx.lineJoin = o.selection.shape;
341 | ctx.fillStyle = c.scale('a', o.selection.fillAlpha).toString();
342 |
343 | var x = Math.min(selection.first.x, selection.second.x) + 0.5,
344 | y = Math.min(selection.first.y, selection.second.y) + 0.5,
345 | w = Math.abs(selection.second.x - selection.first.x) - 1,
346 | h = Math.abs(selection.second.y - selection.first.y) - 1;
347 |
348 | ctx.fillRect(x, y, w, h);
349 | ctx.strokeRect(x, y, w, h);
350 |
351 | ctx.restore();
352 | }
353 | });
354 |
355 | plot.hooks.shutdown.push(function (plot, eventHolder) {
356 | eventHolder.unbind("mousemove", onMouseMove);
357 | eventHolder.unbind("mousedown", onMouseDown);
358 |
359 | if (mouseUpHandler)
360 | $(document).unbind("mouseup", mouseUpHandler);
361 | });
362 |
363 | }
364 |
365 | $.plot.plugins.push({
366 | init: init,
367 | options: {
368 | selection: {
369 | mode: null, // one of null, "x", "y" or "xy"
370 | color: "#e8cfac",
371 | shape: "round", // one of "round", "miter", or "bevel"
372 | minSize: 5, // minimum number of pixels
373 | strokeAlpha: 0.8,
374 | fillAlpha: 0.4,
375 | }
376 | },
377 | name: 'selection',
378 | version: '1.1'
379 | });
380 | })(jQuery);
381 |
--------------------------------------------------------------------------------