/**
* @class Ext.sparkline.Base
*
* The base class for ExtJS SparkLines. SparkLines are small, inline graphs used to visually
* display small amounts of data. For large datasets, use the
* {@link Ext.chart.AbstractChart chart package}.
*
* The SparkLine subclasses accept an {@link #values array of values}, and present the data
* in different visualizations.
*
* @example
* new Ext.Panel({
* height: 300,
* width: 600,
* frame: true,
* title: 'Test Sparklines',
* renderTo:document.body,
* bodyPadding: 10,
*
* // Named listeners will resolve to methods in this Panel
* defaultListenerScope: true,
*
* // Named references will be collected, and can be access from this Panel
* referenceHolder: true,
*
* items: [{
* reference: 'values',
* xtype: 'textfield',
* fieldLabel: 'Values',
* validator: function(v) {
* var result = [];
*
* v = v.replace(/\s/g, '');
* v = v.replace(/,$/, '');
* v = v.split(',');
* for (var i = 0; i < v.length; i++) {
* if (!Ext.isNumeric(v[i])) {
* return 'Value must be a comma separated array of numbers';
* }
* result.push(parseInt(v[i], 10));
* }
* this.values = result;
* return true;
* },
* listeners: {
* change: 'onTypeChange',
* buffer: 500,
* afterrender: {
* fn: 'afterTypeRender',
* single: true
* }
* }
* }, {
* reference: 'type',
* xtype: 'combobox',
* fieldLabel: 'Type',
* store: [
* ['sparklineline', 'Line'],
* ['sparklinebox', 'Box'],
* ['sparklinebullet', 'Bullet'],
* ['sparklinediscrete', 'Discrete'],
* ['sparklinepie', 'Pie'],
* ['sparklinetristate', 'TriState']
* ],
* value: 'sparklineline',
* listeners: {
* change: 'onTypeChange',
* buffer: 500
* }
* }],
*
* // Start with a line plot.
* afterTypeRender: function(typeField) {
* typeField.setValue('6,10,4,-3,7,2');
* },
*
* onTypeChange: function() {
* var me = this,
* refs = me.getReferences(),
* config;
*
* if (me.sparkLine) {
* me.remove(me.sparkLine, true);
* }
* config = {
* xtype: refs.type.getValue(),
* values: refs.values.values,
* height: 25,
* width: 100
* };
* me.sparkLine = Ext.create(config);
* me.add(me.sparkLine);
*
* // Put under fields
* me.sparkLine.el.dom.style.marginLeft = refs.type.labelEl.getWidth() + 'px';
* }
* });
*
*/
Ext.define('Ext.sparkline.Base', {
extend: 'Ext.Gadget',
xtype: 'sparkline',
requires: [
'Ext.XTemplate',
'Ext.sparkline.CanvasCanvas',
'Ext.sparkline.VmlCanvas',
'Ext.util.Color'
],
cachedConfig: {
/**
* @cfg {String} lineColor
* 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}).
*/
lineColor: '#157fcc',
defaultPixelsPerValue: 3,
tagValuesAttribute: 'values',
enableTagOptions: false,
enableHighlight: true,
/**
* @cfg {String} [highlightColor=null]
* The hex value for the highlight color to use when mouseing over a graph segment.
*/
highlightColor: null,
/**
* @cfg {Number} [highlightLighten]
* How much to lighten the highlight color by when mouseing over a graph segment.
*/
highlightLighten: 0.1,
/**
* @cfg {Boolean} [tooltipSkipNull=true]
* Null values will not have a tooltip displayed.
*/
tooltipSkipNull: true,
/**
* @cfg {String} [tooltipPrefix]
* A string to prepend to each field displayed in a tooltip.
*/
tooltipPrefix: '',
/**
* @cfg {String} [tooltipSuffix]
* A string to append to each field displayed in a tooltip.
*/
tooltipSuffix: '',
/**
* @cfg {Boolean} [disableTooltips=false]
* Set to `true` to disable mouseover tooltips.
*/
disableTooltips: false,
disableInteraction: false,
/**
* @cfg {String/Ext.XTemplate} [tipTpl]
* An XTemplate used to display the value or values in a tooltip when hovering
* over a Sparkline.
*
* The implemented subclases all define their own `tipTpl`, but it can be overridden.
*/
tipTpl: null
},
config: {
/**
* @cfg {Number[]} values An array of numbers which define the chart.
*/
values: null
},
baseCls: Ext.baseCSSPrefix + 'sparkline',
element: {
tag: 'canvas',
reference: 'element',
style: {
display: 'inline-block',
verticalAlign: 'top'
},
listeners: {
mouseenter: 'onMouseEnter',
mouseleave: 'onMouseLeave',
mousemove: 'onMouseMove'
},
// Create canvas zero sized so that it does not affect the containing element's
// initial layout
// https://sencha.jira.com/browse/EXTJSIV-10145
width: 0,
height: 0
},
defaultBindProperty: 'values',
// When any config is changed, the canvas needs to be redrawn.
// This is done at the next animation frame when this queue is traversed.
redrawQueue: {},
inheritableStatics: {
/**
* @private
* @static
* @inheritable
*/
onClassCreated: function(cls) {
var configUpdater = cls.prototype.updateConfigChange,
proto = cls.prototype,
configs = cls.getConfigurator().configs,
config,
updaterName;
// Set up an applier for all local configs which kicks off a request to redraw
// on the next animation frame
for (config in configs) {
// tipTpl not included in this scheme
if (config !== 'tipTpl') {
updaterName = Ext.Config.get(config).names.update;
if (proto[updaterName]) {
proto[updaterName] =
Ext.Function.createSequence(proto[updaterName], configUpdater);
}
else {
proto[updaterName] = configUpdater;
}
}
}
}
},
constructor: function(config) {
var me = this,
ns = Ext.sparkline;
// The canvas sets our element config
me.canvas = Ext.supports.Canvas ? new ns.CanvasCanvas(me) : new ns.VmlCanvas(me);
me.callParent([config]);
},
// determine if all values of an array match a value
// returns true if the array is empty
all: function(val, arr, ignoreNull) {
var i;
for (i = arr.length; i--;) {
if (ignoreNull && arr[i] === null) {
continue;
}
if (arr[i] !== val) {
return false;
}
}
return true;
},
// generic config value updater.
// Redraws the graph, unless we are configured to buffer redraws
// in wehich case it adds this to the queue to do a redraw on the next animation frame.
updateConfigChange: function(newValue) {
var me = this;
// If we are buffering until animation frame, or we have not been sized, then
// queue a redraw flush.
if (me.bufferRedraw || !me.height || !me.width) {
me.redrawQueue[me.getId()] = me;
// Ensure that there is a single timer to handle all queued redraws.
if (!me.redrawTimer) {
Ext.sparkline.Base.prototype.redrawTimer =
Ext.raf(me.processRedrawQueue);
}
}
else {
me.redraw();
}
return newValue;
},
// Appliers convert an incoming config value.
// Ensure the tipTpl is an XTemplate
applyTipTpl: function(tipTpl) {
if (tipTpl && !tipTpl.isTemplate) {
tipTpl = new Ext.XTemplate(tipTpl);
}
return tipTpl;
},
normalizeValue: function(val) {
var nf;
switch (val) {
case 'undefined':
val = undefined;
break;
case 'null':
val = null;
break;
case 'true':
val = true;
break;
case 'false':
val = false;
break;
default:
nf = parseFloat(val);
if (val == nf) { // eslint-disable-line eqeqeq
val = nf;
}
}
return val;
},
normalizeValues: function(vals) {
var i,
result = [];
for (i = vals.length; i--;) {
result[i] = this.normalizeValue(vals[i]);
}
return result;
},
updateWidth: function(width, oldWidth) {
var me = this,
dom = me.element.dom,
measurer = me.measurer;
me.callParent([width, oldWidth]);
me.canvas.setWidth(width);
me.width = width;
if (me.height == null && measurer) {
me.setHeight(parseInt(measurer.getCachedStyle(dom.parentNode, 'line-height'), 10));
}
},
updateHeight: function(height, oldHeight) {
var me = this;
me.callParent([height, oldHeight]);
me.canvas.setHeight(height);
me.height = height;
},
setValues: function(values) {
var me = this,
oldValues = me.getValues();
// the values config is expected to be an array
values = values == null ? [] : Ext.Array.from(values);
me.values = values;
me.callParent([values]);
// If it's physically the same Array object, we need to invoke the updater
// because though the array is the same, it may have been mutated,
// and the config system setter will reject the change and not invoke the updater.
// This is how the Stock Ticker example works. It shuffles values down
// a static array.
if (values === oldValues) {
me.updateValues([values, oldValues]);
}
},
redraw: function() {
var me = this;
if (!me.destroyed) {
me.canvas.onOwnerUpdate();
me.canvas.reset();
if (me.getValues()) {
me.onUpdate();
me.renderGraph();
}
}
},
onUpdate: Ext.emptyFn,
/*
* Render the chart to the canvas
*/
renderGraph: function() {
var ret = true;
if (this.disabled) {
this.canvas.reset();
ret = false;
}
return ret;
},
onMouseEnter: function(e) {
this.onMouseMove(e);
},
onMouseMove: function(e) {
var me = this;
// In IE/Edge, the mousemove event fires before mouseenter
// This is correct according to the spec
// https://www.w3.org/TR/uievents/#events-mouseevent-event-order
me.canvasRegion = me.canvasRegion || me.canvas.el.getRegion();
me.currentPageXY = e.getPoint();
me.redraw();
},
onMouseLeave: function() {
var me = this;
// mouseleave is guaranteed to fire last, so clear region here
me.canvasRegion = me.currentPageXY = me.targetX = me.targetY = null;
me.redraw();
me.hideTip();
},
updateDisplay: function() {
var me = this,
values = me.getValues(),
tipHtml, region;
// To work out the position of currentPageXY within the canvas, we must account
// for the fact that while document Y values as represented in the currentPageXY
// are based from the top of the document, canvas Y values begin from the bottom
// of the canvas element.
if (values && values.length && me.currentPageXY &&
me.canvasRegion.contains(me.currentPageXY)) {
region = me.getRegion(me.currentPageXY[0] - me.canvasRegion.left,
(me.canvasRegion.bottom - 1) - me.currentPageXY[1]);
if (region != null && me.isValidRegion(region, values)) {
if (!me.disableHighlight) {
me.renderHighlight(region);
}
if (!me.getDisableTooltips()) {
tipHtml = me.getRegionTooltip(region);
}
}
if (me.hasListeners.sparklineregionchange) {
me.fireEvent('sparklineregionchange', me);
}
if (tipHtml) {
me.getSharedTooltip().setHtml(tipHtml);
me.showTip();
}
}
// No tip content; ensure it's hidden
if (!tipHtml) {
me.hideTip();
}
},
/**
* @method
* Return a region id for a given x/y co-ordinate
*/
getRegion: Ext.emptyFn,
/**
* Fetch the HTML to display as a tooltip
*/
getRegionTooltip: function(region) {
var me = this,
entries = [],
tipTpl = me.getTipTpl(),
fields, showFields, showFieldsKey, newFields, fv,
formatter, fieldlen, i, j;
fields = me.getRegionFields(region);
formatter = me.tooltipFormatter;
if (formatter) {
return formatter(me, me, fields);
}
if (!tipTpl) {
return '';
}
if (!Ext.isArray(fields)) {
fields = [fields];
}
showFields = me.tooltipFormatFieldlist;
showFieldsKey = me.tooltipFormatFieldlistKey;
if (showFields && showFieldsKey) {
// user-selected ordering of fields
newFields = [];
for (i = fields.length; i--;) {
fv = fields[i][showFieldsKey];
if ((j = Ext.Array.indexOf(fv, showFields)) !== -1) {
newFields[j] = fields[i];
}
}
fields = newFields;
}
fieldlen = fields.length;
for (j = 0; j < fieldlen; j++) {
if (!fields[j].isNull || !me.getTooltipSkipNull()) {
Ext.apply(fields[j], {
prefix: me.getTooltipPrefix(),
suffix: me.getTooltipSuffix()
});
entries.push(tipTpl.apply(fields[j]));
}
}
if (entries.length) {
return entries.join('<br>');
}
return '';
},
getRegionFields: Ext.emptyFn,
calcHighlightColor: function(color) {
var me = this,
highlightColor = me.getHighlightColor(),
lighten = me.getHighlightLighten(),
o;
if (highlightColor) {
return highlightColor;
}
if (lighten) {
o = Ext.util.Color.fromString(color);
if (o) {
o.lighten(lighten);
color = o.toHex();
}
}
return color;
},
destroy: function() {
delete this.redrawQueue[this.getId()];
this.callParent();
},
privates: {
hideTip: Ext.privateFn,
isValidRegion: function(region, values) {
return region < values.length;
},
showTip: Ext.privateFn
}
}, function(SparklineBase) {
var proto = SparklineBase.prototype;
proto.getSharedTooltip = function() {
var me = this,
tooltip = me.tooltip;
if (!tooltip) {
proto.tooltip = tooltip = SparklineBase.constructTip();
}
return tooltip;
};
SparklineBase.onClassCreated(SparklineBase);
proto.processRedrawQueue = function() {
var redrawQueue = proto.redrawQueue,
id;
for (id in redrawQueue) {
redrawQueue[id].redraw();
}
proto.redrawQueue = {};
proto.redrawTimer = 0;
};
});