macoslinuxwindowsinboxwhatsappicloudtweetdeckhipchattelegramhangoutsslackgmailskypefacebook-workplaceoutlookemailmicrosoft-teamsdiscordmessengercustom-services
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
543 lines
15 KiB
543 lines
15 KiB
/** |
|
* @class Ext.chart.Legend |
|
* |
|
* Defines a legend for a chart's series. |
|
* The 'chart' member must be set prior to rendering. |
|
* The legend class displays a list of legend items each of them related with a |
|
* series being rendered. In order to render the legend item of the proper series |
|
* the series configuration object must have `showInLegend` set to true. |
|
* |
|
* The legend configuration object accepts a `position` as parameter. |
|
* The `position` parameter can be `left`, `right` |
|
* `top` or `bottom`. For example: |
|
* |
|
* legend: { |
|
* position: 'right' |
|
* }, |
|
* |
|
* ## Example |
|
* |
|
* @example |
|
* var store = Ext.create('Ext.data.JsonStore', { |
|
* fields: ['name', 'data1', 'data2', 'data3', 'data4', 'data5'], |
|
* data: [ |
|
* { 'name': 'metric one', 'data1': 10, 'data2': 12, 'data3': 14, 'data4': 8, 'data5': 13 }, |
|
* { 'name': 'metric two', 'data1': 7, 'data2': 8, 'data3': 16, 'data4': 10, 'data5': 3 }, |
|
* { 'name': 'metric three', 'data1': 5, 'data2': 2, 'data3': 14, 'data4': 12, 'data5': 7 }, |
|
* { 'name': 'metric four', 'data1': 2, 'data2': 14, 'data3': 6, 'data4': 1, 'data5': 23 }, |
|
* { 'name': 'metric five', 'data1': 27, 'data2': 38, 'data3': 36, 'data4': 13, 'data5': 33 } |
|
* ] |
|
* }); |
|
* |
|
* Ext.create('Ext.chart.Chart', { |
|
* renderTo: Ext.getBody(), |
|
* width: 500, |
|
* height: 300, |
|
* animate: true, |
|
* store: store, |
|
* shadow: true, |
|
* theme: 'Category1', |
|
* legend: { |
|
* position: 'top' |
|
* }, |
|
* axes: [ |
|
* { |
|
* type: 'Numeric', |
|
* position: 'left', |
|
* fields: ['data1', 'data2', 'data3', 'data4', 'data5'], |
|
* title: 'Sample Values', |
|
* grid: { |
|
* odd: { |
|
* opacity: 1, |
|
* fill: '#ddd', |
|
* stroke: '#bbb', |
|
* 'stroke-width': 1 |
|
* } |
|
* }, |
|
* minimum: 0, |
|
* adjustMinimumByMajorUnit: 0 |
|
* }, |
|
* { |
|
* type: 'Category', |
|
* position: 'bottom', |
|
* fields: ['name'], |
|
* title: 'Sample Metrics', |
|
* grid: true, |
|
* label: { |
|
* rotate: { |
|
* degrees: 315 |
|
* } |
|
* } |
|
* } |
|
* ], |
|
* series: [{ |
|
* type: 'area', |
|
* highlight: false, |
|
* axis: 'left', |
|
* xField: 'name', |
|
* yField: ['data1', 'data2', 'data3', 'data4', 'data5'], |
|
* style: { |
|
* opacity: 0.93 |
|
* } |
|
* }] |
|
* }); |
|
*/ |
|
Ext.define('Ext.chart.Legend', { |
|
|
|
/* Begin Definitions */ |
|
|
|
requires: ['Ext.chart.LegendItem'], |
|
|
|
/* End Definitions */ |
|
|
|
/** |
|
* @cfg {Boolean} visible |
|
* Whether or not the legend should be displayed. |
|
*/ |
|
visible: true, |
|
|
|
/** |
|
* @cfg {Boolean} update |
|
* If set to true the legend will be refreshed when the chart is. |
|
* This is useful to update the legend items if series are |
|
* added/removed/updated from the chart. Default is true. |
|
*/ |
|
update: true, |
|
|
|
/** |
|
* @cfg {String} position |
|
* The position of the legend in relation to the chart. One of: "top", |
|
* "bottom", "left", "right", or "float". If set to "float", then the legend |
|
* box will be positioned at the point denoted by the x and y parameters. |
|
*/ |
|
position: 'bottom', |
|
|
|
/** |
|
* @cfg {Number} x |
|
* X-position of the legend box. Used directly if position is set to "float", otherwise |
|
* it will be calculated dynamically. |
|
*/ |
|
x: 0, |
|
|
|
/** |
|
* @cfg {Number} y |
|
* Y-position of the legend box. Used directly if position is set to "float", otherwise |
|
* it will be calculated dynamically. |
|
*/ |
|
y: 0, |
|
|
|
/** |
|
* @cfg {String} labelColor |
|
* Color to be used for the legend labels, eg '#000' |
|
*/ |
|
labelColor: '#000', |
|
|
|
/** |
|
* @cfg {String} labelFont |
|
* Font to be used for the legend labels, eg '12px Helvetica' |
|
*/ |
|
labelFont: '12px Helvetica, sans-serif', |
|
|
|
/** |
|
* @cfg {String} boxStroke |
|
* Style of the stroke for the legend box |
|
*/ |
|
boxStroke: '#000', |
|
|
|
/** |
|
* @cfg {Number} boxStrokeWidth |
|
* Width of the stroke for the legend box |
|
*/ |
|
boxStrokeWidth: 1, |
|
|
|
/** |
|
* @cfg {String} boxFill |
|
* Fill style for the legend box |
|
*/ |
|
boxFill: '#FFF', |
|
|
|
/** |
|
* @cfg {Number} itemSpacing |
|
* Amount of space between legend items |
|
*/ |
|
itemSpacing: 10, |
|
|
|
/** |
|
* @cfg {Number} padding |
|
* Amount of padding between the legend box's border and its items |
|
*/ |
|
padding: 5, |
|
|
|
// @private |
|
width: 0, |
|
// @private |
|
height: 0, |
|
|
|
/** |
|
* @cfg {Number} boxZIndex |
|
* Sets the z-index for the legend. Defaults to 100. |
|
*/ |
|
boxZIndex: 100, |
|
|
|
/** |
|
* Creates new Legend. |
|
* @param {Object} config (optional) Config object. |
|
*/ |
|
constructor: function(config) { |
|
var me = this; |
|
if (config) { |
|
Ext.apply(me, config); |
|
} |
|
me.items = []; |
|
/** |
|
* Whether the legend box is oriented vertically, i.e. if it is on the left or right side or floating. |
|
* @type {Boolean} |
|
*/ |
|
me.isVertical = ("left|right|float".indexOf(me.position) !== -1); |
|
|
|
// cache these here since they may get modified later on |
|
me.origX = me.x; |
|
me.origY = me.y; |
|
}, |
|
|
|
/** |
|
* @private Create all the sprites for the legend |
|
*/ |
|
create: function() { |
|
var me = this, |
|
seriesItems = me.chart.series.items, |
|
i, ln, series; |
|
|
|
me.createBox(); |
|
|
|
if (me.rebuild !== false) { |
|
me.createItems(); |
|
} |
|
|
|
if (!me.created && me.isDisplayed()) { |
|
me.created = true; |
|
|
|
// Listen for changes to series titles to trigger regeneration of the legend |
|
for (i = 0, ln = seriesItems.length; i < ln; i++) { |
|
series = seriesItems[i]; |
|
series.on('titlechange', me.redraw, me); |
|
} |
|
} |
|
}, |
|
|
|
init: Ext.emptyFn, |
|
|
|
/** |
|
* @private Redraws the Legend |
|
*/ |
|
redraw: function() { |
|
var me = this; |
|
|
|
me.create(); |
|
me.updatePosition(); |
|
}, |
|
|
|
/** |
|
* @private Determine whether the legend should be displayed. Looks at the legend's 'visible' config, |
|
* and also the 'showInLegend' config for each of the series. |
|
*/ |
|
isDisplayed: function() { |
|
return this.visible && this.chart.series.findIndex('showInLegend', true) !== -1; |
|
}, |
|
|
|
/** |
|
* @private Create the series markers and labels |
|
*/ |
|
createItems: function() { |
|
var me = this, |
|
seriesItems = me.chart.series.items, |
|
items = me.items, |
|
fields, i, li, j, lj, series, item; |
|
|
|
//remove all legend items |
|
me.removeItems(); |
|
|
|
// Create all the item labels |
|
for (i = 0, li = seriesItems.length; i < li; i++) { |
|
series = seriesItems[i]; |
|
|
|
if (series.showInLegend) { |
|
fields = [].concat(series.yField); |
|
|
|
for (j = 0, lj = fields.length; j < lj; j++) { |
|
item = me.createLegendItem(series, j); |
|
items.push(item); |
|
} |
|
} |
|
} |
|
|
|
me.alignItems(); |
|
}, |
|
|
|
/** |
|
* @private Removes all legend items. |
|
*/ |
|
removeItems: function() { |
|
var me = this, |
|
items = me.items, |
|
len = items ? items.length : 0, |
|
i; |
|
|
|
if (len) { |
|
for (i = 0; i < len; i++) { |
|
items[i].destroy(); |
|
} |
|
} |
|
|
|
//empty array |
|
items.length = []; |
|
}, |
|
|
|
/** |
|
* @private |
|
* Positions all items within Legend box. |
|
*/ |
|
alignItems: function() { |
|
var me = this, |
|
padding = me.padding, |
|
vertical = me.isVertical, |
|
mfloor = Math.floor, |
|
dim, maxWidth, maxHeight, totalWidth, totalHeight; |
|
|
|
dim = me.updateItemDimensions(); |
|
|
|
maxWidth = dim.maxWidth; |
|
maxHeight = dim.maxHeight; |
|
totalWidth = dim.totalWidth; |
|
totalHeight = dim.totalHeight; |
|
|
|
// Store the collected dimensions for later |
|
// Give some extra offset for the width because we bold on hover |
|
me.width = mfloor((vertical ? maxWidth : totalWidth) + padding * 2) + 10 |
|
me.height = mfloor((vertical ? totalHeight : maxHeight) + padding * 2); |
|
}, |
|
|
|
updateItemDimensions: function() { |
|
var me = this, |
|
items = me.items, |
|
padding = me.padding, |
|
itemSpacing = me.itemSpacing, |
|
maxWidth = 0, |
|
maxHeight = 0, |
|
totalWidth = 0, |
|
totalHeight = 0, |
|
vertical = me.isVertical, |
|
mfloor = Math.floor, |
|
mmax = Math.max, |
|
spacing = 0, |
|
i, l, item, bbox, width, height; |
|
|
|
// Collect item dimensions and position each one |
|
// properly in relation to the previous item |
|
for (i = 0, l = items.length; i < l; i++) { |
|
item = items[i]; |
|
|
|
bbox = item.getBBox(); |
|
|
|
//always measure from x=0, since not all markers go all the way to the left |
|
width = bbox.width; |
|
height = bbox.height; |
|
|
|
spacing = (i === 0 ? 0 : itemSpacing); |
|
|
|
// Set the item's position relative to the legend box |
|
item.x = padding + mfloor(vertical ? 0 : totalWidth + spacing); |
|
item.y = padding + mfloor(vertical ? totalHeight + spacing : 0) + height / 2; |
|
|
|
// Collect cumulative dimensions |
|
totalWidth += spacing + width; |
|
totalHeight += spacing + height; |
|
maxWidth = mmax(maxWidth, width); |
|
maxHeight = mmax(maxHeight, height); |
|
} |
|
|
|
return { |
|
totalWidth: totalWidth, |
|
totalHeight: totalHeight, |
|
maxWidth: maxWidth, |
|
maxHeight: maxHeight |
|
}; |
|
}, |
|
|
|
/** |
|
* @private Creates single Legend Item |
|
*/ |
|
createLegendItem: function(series, yFieldIndex) { |
|
var me = this; |
|
|
|
return new Ext.chart.LegendItem({ |
|
legend: me, |
|
series: series, |
|
surface: me.chart.surface, |
|
yFieldIndex: yFieldIndex |
|
}); |
|
}, |
|
|
|
/** |
|
* @private Get the bounds for the legend's outer box |
|
*/ |
|
getBBox: function() { |
|
var me = this; |
|
return { |
|
x: Math.round(me.x) - me.boxStrokeWidth / 2, |
|
y: Math.round(me.y) - me.boxStrokeWidth / 2, |
|
width: me.width + me.boxStrokeWidth, |
|
height: me.height + me.boxStrokeWidth |
|
}; |
|
}, |
|
|
|
/** |
|
* @private Create the box around the legend items |
|
*/ |
|
createBox: function() { |
|
var me = this, |
|
box, bbox; |
|
|
|
if (me.boxSprite) { |
|
me.boxSprite.destroy(); |
|
} |
|
|
|
bbox = me.getBBox(); |
|
//if some of the dimensions are NaN this means that we |
|
//cannot set a specific width/height for the legend |
|
//container. One possibility for this is that there are |
|
//actually no items to show in the legend, and the legend |
|
//should be hidden. |
|
if (isNaN(bbox.width) || isNaN(bbox.height)) { |
|
me.boxSprite = false; |
|
return; |
|
} |
|
|
|
box = me.boxSprite = me.chart.surface.add(Ext.apply({ |
|
type: 'rect', |
|
stroke: me.boxStroke, |
|
"stroke-width": me.boxStrokeWidth, |
|
fill: me.boxFill, |
|
zIndex: me.boxZIndex |
|
}, bbox)); |
|
|
|
box.redraw(); |
|
}, |
|
|
|
/** |
|
* @private Calculates Legend position with respect to other Chart elements. |
|
*/ |
|
calcPosition: function() { |
|
var me = this, |
|
x, y, |
|
legendWidth = me.width, |
|
legendHeight = me.height, |
|
chart = me.chart, |
|
chartBBox = chart.chartBBox, |
|
insets = chart.insetPadding, |
|
chartWidth = chartBBox.width - (insets * 2), |
|
chartHeight = chartBBox.height - (insets * 2), |
|
chartX = chartBBox.x + insets, |
|
chartY = chartBBox.y + insets, |
|
surface = chart.surface, |
|
mfloor = Math.floor; |
|
|
|
// Find the position based on the dimensions |
|
switch(me.position) { |
|
case "left": |
|
x = insets; |
|
y = mfloor(chartY + chartHeight / 2 - legendHeight / 2); |
|
break; |
|
case "right": |
|
x = mfloor(surface.width - legendWidth) - insets; |
|
y = mfloor(chartY + chartHeight / 2 - legendHeight / 2); |
|
break; |
|
case "top": |
|
x = mfloor(chartX + chartWidth / 2 - legendWidth / 2); |
|
y = insets; |
|
break; |
|
case "bottom": |
|
x = mfloor(chartX + chartWidth / 2 - legendWidth / 2); |
|
y = mfloor(surface.height - legendHeight) - insets; |
|
break; |
|
default: |
|
x = mfloor(me.origX) + insets; |
|
y = mfloor(me.origY) + insets; |
|
} |
|
|
|
return { x: x, y: y }; |
|
}, |
|
|
|
/** |
|
* @private Update the position of all the legend's sprites to match its current x/y values |
|
*/ |
|
updatePosition: function() { |
|
var me = this, |
|
items = me.items, |
|
pos, i, l, bbox; |
|
|
|
if (me.isDisplayed()) { |
|
// Find the position based on the dimensions |
|
pos = me.calcPosition(); |
|
|
|
me.x = pos.x; |
|
me.y = pos.y; |
|
|
|
// Update the position of each item |
|
for (i = 0, l = items.length; i < l; i++) { |
|
items[i].updatePosition(); |
|
} |
|
|
|
bbox = me.getBBox(); |
|
|
|
//if some of the dimensions are NaN this means that we |
|
//cannot set a specific width/height for the legend |
|
//container. One possibility for this is that there are |
|
//actually no items to show in the legend, and the legend |
|
//should be hidden. |
|
if (isNaN(bbox.width) || isNaN(bbox.height)) { |
|
if (me.boxSprite) { |
|
me.boxSprite.hide(true); |
|
} |
|
} |
|
else { |
|
if (!me.boxSprite) { |
|
me.createBox(); |
|
} |
|
|
|
// Update the position of the outer box |
|
me.boxSprite.setAttributes(bbox, true); |
|
me.boxSprite.show(true); |
|
} |
|
} |
|
}, |
|
|
|
/** toggle |
|
* @param {Boolean} show Whether to show or hide the legend. |
|
* |
|
*/ |
|
toggle: function(show) { |
|
var me = this, |
|
i = 0, |
|
items = me.items, |
|
len = items.length; |
|
|
|
if (me.boxSprite) { |
|
if (show) { |
|
me.boxSprite.show(true); |
|
} else { |
|
me.boxSprite.hide(true); |
|
} |
|
} |
|
|
|
for (; i < len; ++i) { |
|
if (show) { |
|
items[i].show(true); |
|
} else { |
|
items[i].hide(true); |
|
} |
|
} |
|
|
|
me.visible = show; |
|
} |
|
});
|
|
|