]>
Commit | Line | Data |
---|---|---|
6527f429 DM |
1 | /**\r |
2 | * @class Ext.sparkline.Base\r | |
3 | *\r | |
4 | * The base class for ExtJS SparkLines. SparkLines are small, inline graphs used to visually\r | |
5 | * display small amounts of data. For large datasets, use the {@link Ext.chart.AbstractChart chart package}.\r | |
6 | *\r | |
7 | * The SparkLine subclasses accept an {@link #values array of values}, and present the data in different visualizations.\r | |
8 | *\r | |
9 | * @example\r | |
10 | * new Ext.Panel({\r | |
11 | * height: 300,\r | |
12 | * width: 600,\r | |
13 | * frame: true,\r | |
14 | * title: 'Test Sparklines',\r | |
15 | * renderTo:document.body,\r | |
16 | * bodyPadding: 10,\r | |
17 | *\r | |
18 | * // Named listeners will resolve to methods in this Panel\r | |
19 | * defaultListenerScope: true,\r | |
20 | *\r | |
21 | * // Named references will be collected, and can be access from this Panel\r | |
22 | * referenceHolder: true,\r | |
23 | *\r | |
24 | * items: [{\r | |
25 | * reference: 'values',\r | |
26 | * xtype: 'textfield',\r | |
27 | * fieldLabel: 'Values',\r | |
28 | * validator: function(v) {\r | |
29 | * var result = [];\r | |
30 | *\r | |
31 | * v = v.replace(/\s/g, '');\r | |
32 | * v = v.replace(/,$/, '');\r | |
33 | * v = v.split(',');\r | |
34 | * for (var i = 0; i < v.length; i++) {\r | |
35 | * if (!Ext.isNumeric(v[i])) {\r | |
36 | * return 'Value must be a comma separated array of numbers';\r | |
37 | * }\r | |
38 | * result.push(parseInt(v[i], 10));\r | |
39 | * }\r | |
40 | * this.values = result;\r | |
41 | * return true;\r | |
42 | * },\r | |
43 | * listeners: {\r | |
44 | * change: 'onTypeChange',\r | |
45 | * buffer: 500,\r | |
46 | * afterrender: {\r | |
47 | * fn: 'afterTypeRender',\r | |
48 | * single: true\r | |
49 | * }\r | |
50 | * }\r | |
51 | * }, {\r | |
52 | * reference: 'type',\r | |
53 | * xtype: 'combobox',\r | |
54 | * fieldLabel: 'Type',\r | |
55 | * store: [\r | |
56 | * ['sparklineline', 'Line'],\r | |
57 | * ['sparklinebox', 'Box'],\r | |
58 | * ['sparklinebullet', 'Bullet'],\r | |
59 | * ['sparklinediscrete', 'Discrete'],\r | |
60 | * ['sparklinepie', 'Pie'],\r | |
61 | * ['sparklinetristate', 'TriState']\r | |
62 | * ],\r | |
63 | * value: 'sparklineline',\r | |
64 | * listeners: {\r | |
65 | * change: 'onTypeChange',\r | |
66 | * buffer: 500\r | |
67 | * }\r | |
68 | * }],\r | |
69 | *\r | |
70 | * // Start with a line plot. \r | |
71 | * afterTypeRender: function(typeField) {\r | |
72 | * typeField.setValue('6,10,4,-3,7,2');\r | |
73 | * },\r | |
74 | *\r | |
75 | * onTypeChange: function() {\r | |
76 | * var me = this,\r | |
77 | * refs = me.getReferences(),\r | |
78 | * config;\r | |
79 | *\r | |
80 | * if (me.sparkLine) {\r | |
81 | * me.remove(me.sparkLine, true);\r | |
82 | * }\r | |
83 | * config = {\r | |
84 | * xtype: refs.type.getValue(),\r | |
85 | * values: refs.values.values,\r | |
86 | * height: 25,\r | |
87 | * width: 100 \r | |
88 | * };\r | |
89 | * me.sparkLine = Ext.create(config);\r | |
90 | * me.add(me.sparkLine);\r | |
91 | *\r | |
92 | * // Put under fields\r | |
93 | * me.sparkLine.el.dom.style.marginLeft = refs.type.labelEl.getWidth() + 'px';\r | |
94 | * }\r | |
95 | * });\r | |
96 | *\r | |
97 | */\r | |
98 | Ext.define('Ext.sparkline.Base', {\r | |
99 | extend: 'Ext.Widget',\r | |
100 | requires: [\r | |
101 | 'Ext.XTemplate',\r | |
102 | 'Ext.sparkline.CanvasCanvas',\r | |
103 | 'Ext.sparkline.VmlCanvas'\r | |
104 | ],\r | |
105 | \r | |
106 | cachedConfig: {\r | |
107 | baseCls: Ext.baseCSSPrefix + 'sparkline',\r | |
108 | \r | |
109 | /**\r | |
110 | * @cfg {String} [lineColor=#157fcc] The hex value for line colors in graphs which display lines ({@link Ext.sparkline.Box Box}, {@link Ext.sparkline.Discrete Discrete and {@link Ext.sparkline.Line Line}).\r | |
111 | */\r | |
112 | lineColor: '#157fcc',\r | |
113 | \r | |
114 | /**\r | |
115 | * @cfg {String} [fillColor=#def] The hex value for fill color in graphs which fill areas ({@link Ext.sparkline.Line Line}).\r | |
116 | */\r | |
117 | fillColor: '#def',\r | |
118 | \r | |
119 | defaultPixelsPerValue: 3,\r | |
120 | \r | |
121 | tagValuesAttribute: 'values',\r | |
122 | \r | |
123 | enableTagOptions: false,\r | |
124 | \r | |
125 | enableHighlight: true,\r | |
126 | \r | |
127 | /**\r | |
128 | * @cfg {String} [highlightColor=null] The hex value for the highlight color to use when mouseing over a graph segment.\r | |
129 | */\r | |
130 | highlightColor: null,\r | |
131 | \r | |
132 | /**\r | |
133 | * @cfg {Number} [highlightLighten=1.4] How much to lighten the highlight color by when mouseing over a graph segment.\r | |
134 | */\r | |
135 | highlightLighten: 1.4,\r | |
136 | \r | |
137 | /**\r | |
138 | * @cfg {Boolean} [tooltipSkipNull=true] Null values will not have a tooltip displayed.\r | |
139 | */\r | |
140 | tooltipSkipNull: true,\r | |
141 | \r | |
142 | /**\r | |
143 | * @cfg {String} [tooltipPrefix] A string to prepend to each field displayed in a tooltip.\r | |
144 | */\r | |
145 | tooltipPrefix: '',\r | |
146 | \r | |
147 | /**\r | |
148 | * @cfg {String} [tooltipSuffix] A string to append to each field displayed in a tooltip.\r | |
149 | */\r | |
150 | tooltipSuffix: '',\r | |
151 | \r | |
152 | /**\r | |
153 | * @cfg {Boolean} [disableTooltips=false] Set to `true` to disable mouseover tooltips.\r | |
154 | */\r | |
155 | disableTooltips: false,\r | |
156 | \r | |
157 | disableInteraction: false,\r | |
158 | \r | |
159 | /**\r | |
160 | * @cfg {String/Ext.XTemplate} [tipTpl] An XTemplate used to display the value or values in a tooltip when hovering over a Sparkline.\r | |
161 | *\r | |
162 | * The implemented subclases all define their own `tipTpl`, but it can be overridden.\r | |
163 | */\r | |
164 | tipTpl: null\r | |
165 | },\r | |
166 | \r | |
167 | config: {\r | |
168 | /**\r | |
169 | * @cfg {Number[]} values An array of numbers which define the chart.\r | |
170 | */\r | |
171 | values: null\r | |
172 | },\r | |
173 | \r | |
174 | element: {\r | |
175 | tag: 'canvas',\r | |
176 | reference: 'element',\r | |
177 | style: {\r | |
178 | display: 'inline-block',\r | |
179 | verticalAlign: 'top'\r | |
180 | },\r | |
181 | listeners: {\r | |
182 | mouseenter: 'onMouseEnter',\r | |
183 | mouseleave: 'onMouseLeave',\r | |
184 | mousemove: 'onMouseMove'\r | |
185 | },\r | |
186 | // Create canvas zero sized so that it does not affect the containing element's initial layout\r | |
187 | // https://sencha.jira.com/browse/EXTJSIV-10145\r | |
188 | width: 0,\r | |
189 | height: 0\r | |
190 | },\r | |
191 | \r | |
192 | defaultBindProperty: 'values',\r | |
193 | \r | |
194 | // When any config is changed, the canvas needs to be redrawn.\r | |
195 | // This is done at the next animation frame when this queue is traversed.\r | |
196 | redrawQueue: {},\r | |
197 | \r | |
198 | inheritableStatics: {\r | |
199 | /**\r | |
200 | * @private\r | |
201 | * @static\r | |
202 | * @inheritable\r | |
203 | */\r | |
204 | sparkLineTipClass: Ext.baseCSSPrefix + 'sparkline-tip-target',\r | |
205 | \r | |
206 | /**\r | |
207 | * @private\r | |
208 | * @static\r | |
209 | * @inheritable\r | |
210 | */\r | |
211 | onClassCreated: function(cls) {\r | |
212 | var proto = cls.prototype,\r | |
213 | configs = cls.getConfigurator().configs,\r | |
214 | config;\r | |
215 | \r | |
216 | // Set up an applier for all local configs which kicks off a request to redraw on the next animation frame\r | |
217 | for (config in configs) {\r | |
218 | // tipTpl not included in this scheme\r | |
219 | if (config !== 'tipTpl') {\r | |
220 | proto[Ext.Config.get(config).names.apply] = proto.applyConfigChange;\r | |
221 | }\r | |
222 | } \r | |
223 | }\r | |
224 | },\r | |
225 | \r | |
226 | constructor: function(config) {\r | |
227 | var me = this;\r | |
228 | \r | |
229 | // The canvas sets our element config\r | |
230 | me.canvas = Ext.supports.Canvas ? new Ext.sparkline.CanvasCanvas(me)\r | |
231 | : new Ext.sparkline.VmlCanvas(me);\r | |
232 | if (!me.getDisableTooltips()) {\r | |
233 | me.element.cls = Ext.sparkline.Base.sparkLineTipClass;\r | |
234 | }\r | |
235 | \r | |
236 | Ext.apply(me, config);\r | |
237 | me.callParent([config]);\r | |
238 | \r | |
239 | // For compatibility of all the code.\r | |
240 | me.el = me.element;\r | |
241 | },\r | |
242 | \r | |
243 | // determine if all values of an array match a value\r | |
244 | // returns true if the array is empty\r | |
245 | all: function (val, arr, ignoreNull) {\r | |
246 | var i;\r | |
247 | for (i = arr.length; i--; ) {\r | |
248 | if (ignoreNull && arr[i] === null) {\r | |
249 | continue;\r | |
250 | }\r | |
251 | if (arr[i] !== val) {\r | |
252 | return false;\r | |
253 | }\r | |
254 | }\r | |
255 | return true;\r | |
256 | },\r | |
257 | \r | |
258 | // generic config value applier.\r | |
259 | // Adds this to the queue to do a redraw on the next animation frame\r | |
260 | applyConfigChange: function(newValue) {\r | |
261 | var me = this;\r | |
262 | me.redrawQueue[me.getId()] = me;\r | |
263 | \r | |
264 | // Ensure that there is a single timer to handle all queued redraws.\r | |
265 | if (!me.redrawTimer) {\r | |
266 | Ext.sparkline.Base.prototype.redrawTimer =\r | |
267 | Ext.Function.requestAnimationFrame(me.processRedrawQueue);\r | |
268 | }\r | |
269 | return newValue;\r | |
270 | },\r | |
271 | \r | |
272 | // Appliers convert an incoming config value.\r | |
273 | // Ensure the tipTpl is an XTemplate\r | |
274 | applyTipTpl: function(tipTpl) {\r | |
275 | if (!tipTpl.isTemplate) {\r | |
276 | tipTpl = new Ext.XTemplate(tipTpl);\r | |
277 | }\r | |
278 | return tipTpl;\r | |
279 | },\r | |
280 | \r | |
281 | normalizeValue: function (val) {\r | |
282 | var nf;\r | |
283 | switch (val) {\r | |
284 | case 'undefined':\r | |
285 | val = undefined;\r | |
286 | break;\r | |
287 | case 'null':\r | |
288 | val = null;\r | |
289 | break;\r | |
290 | case 'true':\r | |
291 | val = true;\r | |
292 | break;\r | |
293 | case 'false':\r | |
294 | val = false;\r | |
295 | break;\r | |
296 | default:\r | |
297 | nf = parseFloat(val);\r | |
298 | if (val == nf) {\r | |
299 | val = nf;\r | |
300 | }\r | |
301 | }\r | |
302 | return val;\r | |
303 | },\r | |
304 | \r | |
305 | normalizeValues: function (vals) {\r | |
306 | var i, result = [];\r | |
307 | for (i = vals.length; i--;) {\r | |
308 | result[i] = this.normalizeValue(vals[i]);\r | |
309 | }\r | |
310 | return result;\r | |
311 | },\r | |
312 | \r | |
313 | updateWidth: function(width, oldWidth) {\r | |
314 | var me = this,\r | |
315 | dom = me.element.dom;\r | |
316 | \r | |
317 | me.callParent([width, oldWidth]);\r | |
318 | me.canvas.setWidth(width);\r | |
319 | me.width = width;\r | |
320 | if (me.height == null) {\r | |
321 | me.setHeight(parseInt(me.measurer.getCachedStyle(dom.parentNode, 'line-height'), 10));\r | |
322 | } else {\r | |
323 | me.redrawQueue[me.getId()] = me;\r | |
324 | }\r | |
325 | },\r | |
326 | \r | |
327 | updateHeight: function(height, oldHeight) {\r | |
328 | var me = this;\r | |
329 | \r | |
330 | me.callParent([height, oldHeight]);\r | |
331 | me.canvas.setHeight(height);\r | |
332 | me.height = height;\r | |
333 | me.redrawQueue[me.getId()] = me;\r | |
334 | },\r | |
335 | \r | |
336 | updateValues: function(values) {\r | |
337 | this.values = values;\r | |
338 | },\r | |
339 | \r | |
340 | redraw: function() {\r | |
341 | var me = this;\r | |
342 | \r | |
343 | if (me.getValues()) {\r | |
344 | me.onUpdate();\r | |
345 | me.canvas.onOwnerUpdate();\r | |
346 | me.renderGraph();\r | |
347 | }\r | |
348 | },\r | |
349 | \r | |
350 | onUpdate: Ext.emptyFn,\r | |
351 | \r | |
352 | /*\r | |
353 | * Render the chart to the canvas\r | |
354 | */\r | |
355 | renderGraph: function () {\r | |
356 | var ret = true;\r | |
357 | if (this.disabled) {\r | |
358 | this.canvas.reset();\r | |
359 | ret = false;\r | |
360 | }\r | |
361 | return ret;\r | |
362 | },\r | |
363 | \r | |
364 | onMouseEnter: function(e) {\r | |
365 | this.onMouseMove(e);\r | |
366 | },\r | |
367 | \r | |
368 | onMouseMove: function (e) {\r | |
369 | this.tooltip.triggerEvent = e;\r | |
370 | this.currentPageXY = e.getPoint();\r | |
371 | this.redraw();\r | |
372 | },\r | |
373 | \r | |
374 | onMouseLeave: function () {\r | |
375 | var me = this;\r | |
376 | me.currentPageXY = me.targetX = me.targetY = null;\r | |
377 | me.redraw();\r | |
378 | me.tooltip.target = null; \r | |
379 | me.tooltip.hide();\r | |
380 | },\r | |
381 | \r | |
382 | updateDisplay: function () {\r | |
383 | var me = this,\r | |
384 | values = me.getValues(),\r | |
385 | offset, tooltip = me.tooltip, tooltipHTML, region;\r | |
386 | \r | |
387 | if (values && values.length && me.currentPageXY && me.el.getRegion().contains(me.currentPageXY)) {\r | |
388 | offset = me.canvas.el.getXY();\r | |
389 | region = me.getRegion(me.currentPageXY[0] - offset[0], me.currentPageXY[1] - offset[1]);\r | |
390 | \r | |
391 | if (region != null && region < values.length) {\r | |
392 | if (!me.disableHighlight) {\r | |
393 | me.renderHighlight(region);\r | |
394 | }\r | |
395 | tooltipHTML = me.getRegionTooltip(region);\r | |
396 | }\r | |
397 | me.fireEvent('sparklineregionchange', me);\r | |
398 | \r | |
399 | if (tooltipHTML) {\r | |
400 | if (!me.lastTooltipHTML || tooltipHTML[0] !== me.lastTooltipHTML[0] || tooltipHTML[1] !== me.lastTooltipHTML[1]) {\r | |
401 | tooltip.setTitle(tooltipHTML[0]);\r | |
402 | tooltip.update(tooltipHTML[1]);\r | |
403 | me.lastTooltipHTML = tooltipHTML;\r | |
404 | }\r | |
405 | tooltip.target = me.el;\r | |
406 | tooltip.onTargetOver(tooltip.triggerEvent);\r | |
407 | }\r | |
408 | }\r | |
409 | \r | |
410 | // No tip content; ensure it's hidden\r | |
411 | if (!tooltipHTML) {\r | |
412 | tooltip.target = null; \r | |
413 | tooltip.hide();\r | |
414 | }\r | |
415 | },\r | |
416 | \r | |
417 | /*\r | |
418 | * Return a region id for a given x/y co-ordinate\r | |
419 | */\r | |
420 | getRegion: Ext.emptyFn,\r | |
421 | \r | |
422 | /*\r | |
423 | * Fetch the HTML to display as a tooltip\r | |
424 | */\r | |
425 | getRegionTooltip: function(region) {\r | |
426 | var me = this,\r | |
427 | header = me.tooltipChartTitle,\r | |
428 | entries = [],\r | |
429 | tipTpl = me.getTipTpl(),\r | |
430 | fields, showFields, showFieldsKey, newFields, fv,\r | |
431 | formatter, fieldlen, i, j;\r | |
432 | \r | |
433 | fields = me.getRegionFields(region);\r | |
434 | formatter = me.tooltipFormatter;\r | |
435 | if (formatter) {\r | |
436 | return formatter(me, me, fields);\r | |
437 | }\r | |
438 | \r | |
439 | if (!tipTpl) {\r | |
440 | return '';\r | |
441 | }\r | |
442 | if (!Ext.isArray(fields)) {\r | |
443 | fields = [fields];\r | |
444 | }\r | |
445 | showFields = me.tooltipFormatFieldlist;\r | |
446 | showFieldsKey = me.tooltipFormatFieldlistKey;\r | |
447 | if (showFields && showFieldsKey) {\r | |
448 | // user-selected ordering of fields\r | |
449 | newFields = [];\r | |
450 | for (i = fields.length; i--;) {\r | |
451 | fv = fields[i][showFieldsKey];\r | |
452 | if ((j = Ext.Array.indexOf(fv, showFields)) !== -1) {\r | |
453 | newFields[j] = fields[i];\r | |
454 | }\r | |
455 | }\r | |
456 | fields = newFields;\r | |
457 | }\r | |
458 | fieldlen = fields.length;\r | |
459 | \r | |
460 | for (j = 0; j < fieldlen; j++) {\r | |
461 | if (!fields[j].isNull || !me.getTooltipSkipNull()) {\r | |
462 | Ext.apply(fields[j], {\r | |
463 | prefix: me.getTooltipPrefix(),\r | |
464 | suffix: me.getTooltipSuffix()\r | |
465 | });\r | |
466 | entries.push(tipTpl.apply(fields[j]));\r | |
467 | }\r | |
468 | }\r | |
469 | \r | |
470 | if (header || entries.length) {\r | |
471 | return [header, entries.join('<br>')];\r | |
472 | }\r | |
473 | return '';\r | |
474 | },\r | |
475 | \r | |
476 | getRegionFields: Ext.emptyFn,\r | |
477 | \r | |
478 | calcHighlightColor: function(color) {\r | |
479 | var me = this,\r | |
480 | highlightColor = me.getHighlightColor(),\r | |
481 | lighten = me.getHighlightLighten(),\r | |
482 | parse, mult, rgbnew, i;\r | |
483 | \r | |
484 | if (highlightColor) {\r | |
485 | return highlightColor;\r | |
486 | }\r | |
487 | if (lighten) {\r | |
488 | // extract RGB values\r | |
489 | parse = /^#([0-9a-f])([0-9a-f])([0-9a-f])$/i.exec(color) || /^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i.exec(color);\r | |
490 | if (parse) {\r | |
491 | rgbnew = [];\r | |
492 | mult = color.length === 4 ? 16 : 1;\r | |
493 | for (i = 0; i < 3; i++) {\r | |
494 | rgbnew[i] = Ext.Number.constrain(Math.round(parseInt(parse[i + 1], 16) * mult * lighten), 0, 255);\r | |
495 | }\r | |
496 | return 'rgb(' + rgbnew.join(',') + ')';\r | |
497 | }\r | |
498 | }\r | |
499 | return color;\r | |
500 | },\r | |
501 | \r | |
502 | destroy: function() {\r | |
503 | delete this.redrawQueue[this.getId()];\r | |
504 | this.callParent();\r | |
505 | }\r | |
506 | }, function(SparklineBase) {\r | |
507 | var proto = SparklineBase.prototype;\r | |
508 | \r | |
509 | Ext.onInternalReady(function() {\r | |
510 | proto.tooltip = new Ext.tip.ToolTip({\r | |
511 | id: 'sparklines-tooltip',\r | |
512 | showDelay: 0,\r | |
513 | dismissDelay: 0,\r | |
514 | hideDelay: 400\r | |
515 | });\r | |
516 | });\r | |
517 | \r | |
518 | SparklineBase.onClassCreated(SparklineBase);\r | |
519 | \r | |
520 | proto.processRedrawQueue = function () {\r | |
521 | var redrawQueue = proto.redrawQueue,\r | |
522 | id;\r | |
523 | \r | |
524 | for (id in redrawQueue) {\r | |
525 | redrawQueue[id].redraw();\r | |
526 | }\r | |
527 | proto.redrawQueue = {};\r | |
528 | proto.redrawTimer = 0;\r | |
529 | };\r | |
530 | \r | |
531 | // If we are on a VML platform (IE8 - TODO: remove this when that retires)...\r | |
532 | if (!Ext.supports.Canvas) {\r | |
533 | SparklineBase.prototype.element = {\r | |
534 | tag: 'span',\r | |
535 | reference: 'element',\r | |
536 | listeners: {\r | |
537 | mouseenter: 'onMouseEnter',\r | |
538 | mouseleave: 'onMouseLeave',\r | |
539 | mousemove: 'onMouseMove'\r | |
540 | },\r | |
541 | style: {\r | |
542 | display: 'inline-block',\r | |
543 | position: 'relative',\r | |
544 | overflow: 'hidden',\r | |
545 | margin: '0px',\r | |
546 | padding: '0px',\r | |
547 | verticalAlign: 'top',\r | |
548 | cursor: 'default'\r | |
549 | },\r | |
550 | children: [{\r | |
551 | tag: 'svml:group',\r | |
552 | reference: 'groupEl',\r | |
553 | coordorigin: '0 0',\r | |
554 | coordsize: '0 0',\r | |
555 | style: 'position:absolute;width:0;height:0;pointer-events:none'\r | |
556 | }]\r | |
557 | };\r | |
558 | }\r | |
559 | });\r |