/**
* Adds legend functionality to charts.
*
* @module charts
* @submodule charts-legend
*/
var DOCUMENT = Y.config.doc,
TOP = "top",
RIGHT = "right",
BOTTOM = "bottom",
LEFT = "left",
EXTERNAL = "external",
HORIZONTAL = "horizontal",
VERTICAL = "vertical",
WIDTH = "width",
HEIGHT = "height",
POSITION = "position",
_X = "x",
_Y = "y",
PX = "px",
LEGEND = {
setter: function(val)
{
var legend = this.get("legend");
if(legend)
{
legend.destroy(true);
}
if(val instanceof Y.ChartLegend)
{
legend = val;
legend.set("chart", this);
}
else
{
val.chart = this;
if(!val.hasOwnProperty("render"))
{
val.render = this.get("contentBox");
val.includeInChartLayout = true;
}
legend = new Y.ChartLegend(val);
}
return legend;
}
},
/**
* Contains methods for displaying items horizontally in a legend.
*
* @module charts
* @submodule charts-legend
* @class HorizontalLegendLayout
*/
HorizontalLegendLayout = {
/**
* Displays items horizontally in a legend.
*
* @method _positionLegendItems
* @param {Array} items Array of items to display in the legend.
* @param {Number} maxWidth The width of the largest item in the legend.
* @param {Number} maxHeight The height of the largest item in the legend.
* @param {Number} totalWidth The total width of all items in a legend.
* @param {Number} totalHeight The total height of all items in a legend.
* @param {Number} padding The left, top, right and bottom padding properties for the legend.
* @param {Number} horizontalGap The horizontal distance between items in a legend.
* @param {Number} verticalGap The vertical distance between items in a legend.
* @param {String} hAlign The horizontal alignment of the legend.
* @param {String} vAlign The vertical alignment of the legend.
* @protected
*/
_positionLegendItems: function(items, maxWidth, maxHeight, totalWidth, totalHeight, padding, horizontalGap, verticalGap, hAlign, vAlign)
{
var i = 0,
rowIterator = 0,
item,
node,
itemWidth,
itemHeight,
len,
width = this.get("width"),
rows,
rowsLen,
row,
totalWidthArray,
legendWidth,
topHeight = padding.top - verticalGap,
limit = width - (padding.left + padding.right),
left,
top,
right,
bottom;
HorizontalLegendLayout._setRowArrays(items, limit, horizontalGap);
rows = HorizontalLegendLayout.rowArray;
totalWidthArray = HorizontalLegendLayout.totalWidthArray;
rowsLen = rows.length;
for(; rowIterator < rowsLen; ++ rowIterator)
{
topHeight += verticalGap;
row = rows[rowIterator];
len = row.length;
legendWidth = HorizontalLegendLayout.getStartPoint(width, totalWidthArray[rowIterator], hAlign, padding);
for(i = 0; i < len; ++i)
{
item = row[i];
node = item.node;
itemWidth = item.width;
itemHeight = item.height;
item.x = legendWidth;
item.y = 0;
left = !isNaN(left) ? Math.min(left, legendWidth) : legendWidth;
top = !isNaN(top) ? Math.min(top, topHeight) : topHeight;
right = !isNaN(right) ? Math.max(legendWidth + itemWidth, right) : legendWidth + itemWidth;
bottom = !isNaN(bottom) ? Math.max(topHeight + itemHeight, bottom) : topHeight + itemHeight;
node.setStyle("left", legendWidth + PX);
node.setStyle("top", topHeight + PX);
legendWidth += itemWidth + horizontalGap;
}
topHeight += item.height;
}
this._contentRect = {
left: left,
top: top,
right: right,
bottom: bottom
};
if(this.get("includeInChartLayout"))
{
this.set("height", topHeight + padding.bottom);
}
},
/**
* Creates row and total width arrays used for displaying multiple rows of
* legend items based on the items, available width and horizontalGap for the legend.
*
* @method _setRowArrays
* @param {Array} items Array of legend items to display in a legend.
* @param {Number} limit Total available width for displaying items in a legend.
* @param {Number} horizontalGap Horizontal distance between items in a legend.
* @protected
*/
_setRowArrays: function(items, limit, horizontalGap)
{
var item = items[0],
rowArray = [[item]],
i = 1,
rowIterator = 0,
len = items.length,
totalWidth = item.width,
itemWidth,
totalWidthArray = [[totalWidth]];
for(; i < len; ++i)
{
item = items[i];
itemWidth = item.width;
if((totalWidth + horizontalGap + itemWidth) <= limit)
{
totalWidth += horizontalGap + itemWidth;
rowArray[rowIterator].push(item);
}
else
{
totalWidth = horizontalGap + itemWidth;
if(rowArray[rowIterator])
{
rowIterator += 1;
}
rowArray[rowIterator] = [item];
}
totalWidthArray[rowIterator] = totalWidth;
}
HorizontalLegendLayout.rowArray = rowArray;
HorizontalLegendLayout.totalWidthArray = totalWidthArray;
},
/**
* Returns the starting x-coordinate for a row of legend items.
*
* @method getStartPoint
* @param {Number} w Width of the legend.
* @param {Number} totalWidth Total width of all labels in the row.
* @param {String} align Horizontal alignment of items for the legend.
* @param {Object} padding Object contain left, top, right and bottom padding properties.
* @return Number
* @protected
*/
getStartPoint: function(w, totalWidth, align, padding)
{
var startPoint;
switch(align)
{
case LEFT :
startPoint = padding.left;
break;
case "center" :
startPoint = (w - totalWidth) * 0.5;
break;
case RIGHT :
startPoint = w - totalWidth - padding.right;
break;
}
return startPoint;
}
},
/**
* Contains methods for displaying items vertically in a legend.
*
* @module charts
* @submodule charts-legend
* @class VerticalLegendLayout
*/
VerticalLegendLayout = {
/**
* Displays items vertically in a legend.
*
* @method _positionLegendItems
* @param {Array} items Array of items to display in the legend.
* @param {Number} maxWidth The width of the largest item in the legend.
* @param {Number} maxHeight The height of the largest item in the legend.
* @param {Number} totalWidth The total width of all items in a legend.
* @param {Number} totalHeight The total height of all items in a legend.
* @param {Number} padding The left, top, right and bottom padding properties for the legend.
* @param {Number} horizontalGap The horizontal distance between items in a legend.
* @param {Number} verticalGap The vertical distance between items in a legend.
* @param {String} hAlign The horizontal alignment of the legend.
* @param {String} vAlign The vertical alignment of the legend.
* @protected
*/
_positionLegendItems: function(items, maxWidth, maxHeight, totalWidth, totalHeight, padding, horizontalGap, verticalGap, hAlign, vAlign)
{
var i = 0,
columnIterator = 0,
item,
node,
itemHeight,
itemWidth,
len,
height = this.get("height"),
columns,
columnsLen,
column,
totalHeightArray,
legendHeight,
leftWidth = padding.left - horizontalGap,
legendWidth,
limit = height - (padding.top + padding.bottom),
left,
top,
right,
bottom;
VerticalLegendLayout._setColumnArrays(items, limit, verticalGap);
columns = VerticalLegendLayout.columnArray;
totalHeightArray = VerticalLegendLayout.totalHeightArray;
columnsLen = columns.length;
for(; columnIterator < columnsLen; ++ columnIterator)
{
leftWidth += horizontalGap;
column = columns[columnIterator];
len = column.length;
legendHeight = VerticalLegendLayout.getStartPoint(height, totalHeightArray[columnIterator], vAlign, padding);
legendWidth = 0;
for(i = 0; i < len; ++i)
{
item = column[i];
node = item.node;
itemHeight = item.height;
itemWidth = item.width;
item.y = legendHeight;
item.x = leftWidth;
left = !isNaN(left) ? Math.min(left, leftWidth) : leftWidth;
top = !isNaN(top) ? Math.min(top, legendHeight) : legendHeight;
right = !isNaN(right) ? Math.max(leftWidth + itemWidth, right) : leftWidth + itemWidth;
bottom = !isNaN(bottom) ? Math.max(legendHeight + itemHeight, bottom) : legendHeight + itemHeight;
node.setStyle("left", leftWidth + PX);
node.setStyle("top", legendHeight + PX);
legendHeight += itemHeight + verticalGap;
legendWidth = Math.max(legendWidth, item.width);
}
leftWidth += legendWidth;
}
this._contentRect = {
left: left,
top: top,
right: right,
bottom: bottom
};
if(this.get("includeInChartLayout"))
{
this.set("width", leftWidth + padding.right);
}
},
/**
* Creates column and total height arrays used for displaying multiple columns of
* legend items based on the items, available height and verticalGap for the legend.
*
* @method _setColumnArrays
* @param {Array} items Array of legend items to display in a legend.
* @param {Number} limit Total available height for displaying items in a legend.
* @param {Number} verticalGap Vertical distance between items in a legend.
* @protected
*/
_setColumnArrays: function(items, limit, verticalGap)
{
var item = items[0],
columnArray = [[item]],
i = 1,
columnIterator = 0,
len = items.length,
totalHeight = item.height,
itemHeight,
totalHeightArray = [[totalHeight]];
for(; i < len; ++i)
{
item = items[i];
itemHeight = item.height;
if((totalHeight + verticalGap + itemHeight) <= limit)
{
totalHeight += verticalGap + itemHeight;
columnArray[columnIterator].push(item);
}
else
{
totalHeight = verticalGap + itemHeight;
if(columnArray[columnIterator])
{
columnIterator += 1;
}
columnArray[columnIterator] = [item];
}
totalHeightArray[columnIterator] = totalHeight;
}
VerticalLegendLayout.columnArray = columnArray;
VerticalLegendLayout.totalHeightArray = totalHeightArray;
},
/**
* Returns the starting y-coordinate for a column of legend items.
*
* @method getStartPoint
* @param {Number} h Height of the legend.
* @param {Number} totalHeight Total height of all labels in the column.
* @param {String} align Vertical alignment of items for the legend.
* @param {Object} padding Object contain left, top, right and bottom padding properties.
* @return Number
* @protected
*/
getStartPoint: function(h, totalHeight, align, padding)
{
var startPoint;
switch(align)
{
case TOP :
startPoint = padding.top;
break;
case "middle" :
startPoint = (h - totalHeight) * 0.5;
break;
case BOTTOM :
startPoint = h - totalHeight - padding.bottom;
break;
}
return startPoint;
}
},
CartesianChartLegend = Y.Base.create("cartesianChartLegend", Y.CartesianChart, [], {
/**
* Redraws and position all the components of the chart instance.
*
* @method _redraw
* @private
*/
_redraw: function()
{
if(this._drawing)
{
this._callLater = true;
return;
}
this._drawing = true;
this._callLater = false;
var w = this.get("width"),
h = this.get("height"),
layoutBoxDimensions = this._getLayoutBoxDimensions(),
leftPaneWidth = layoutBoxDimensions.left,
rightPaneWidth = layoutBoxDimensions.right,
topPaneHeight = layoutBoxDimensions.top,
bottomPaneHeight = layoutBoxDimensions.bottom,
leftAxesCollection = this.get("leftAxesCollection"),
rightAxesCollection = this.get("rightAxesCollection"),
topAxesCollection = this.get("topAxesCollection"),
bottomAxesCollection = this.get("bottomAxesCollection"),
i = 0,
l,
axis,
graphOverflow = "visible",
graph = this.get("graph"),
topOverflow,
bottomOverflow,
leftOverflow,
rightOverflow,
graphWidth,
graphHeight,
graphX,
graphY,
allowContentOverflow = this.get("allowContentOverflow"),
diff,
rightAxesXCoords,
leftAxesXCoords,
topAxesYCoords,
bottomAxesYCoords,
legend = this.get("legend"),
graphRect = {};
if(leftAxesCollection)
{
leftAxesXCoords = [];
l = leftAxesCollection.length;
for(i = l - 1; i > -1; --i)
{
leftAxesXCoords.unshift(leftPaneWidth);
leftPaneWidth += leftAxesCollection[i].get("width");
}
}
if(rightAxesCollection)
{
rightAxesXCoords = [];
l = rightAxesCollection.length;
i = 0;
for(i = l - 1; i > -1; --i)
{
rightPaneWidth += rightAxesCollection[i].get("width");
rightAxesXCoords.unshift(w - rightPaneWidth);
}
}
if(topAxesCollection)
{
topAxesYCoords = [];
l = topAxesCollection.length;
for(i = l - 1; i > -1; --i)
{
topAxesYCoords.unshift(topPaneHeight);
topPaneHeight += topAxesCollection[i].get("height");
}
}
if(bottomAxesCollection)
{
bottomAxesYCoords = [];
l = bottomAxesCollection.length;
for(i = l - 1; i > -1; --i)
{
bottomPaneHeight += bottomAxesCollection[i].get("height");
bottomAxesYCoords.unshift(h - bottomPaneHeight);
}
}
graphWidth = w - (leftPaneWidth + rightPaneWidth);
graphHeight = h - (bottomPaneHeight + topPaneHeight);
graphRect.left = leftPaneWidth;
graphRect.top = topPaneHeight;
graphRect.bottom = h - bottomPaneHeight;
graphRect.right = w - rightPaneWidth;
if(!allowContentOverflow)
{
topOverflow = this._getTopOverflow(leftAxesCollection, rightAxesCollection);
bottomOverflow = this._getBottomOverflow(leftAxesCollection, rightAxesCollection);
leftOverflow = this._getLeftOverflow(bottomAxesCollection, topAxesCollection);
rightOverflow = this._getRightOverflow(bottomAxesCollection, topAxesCollection);
diff = topOverflow - topPaneHeight;
if(diff > 0)
{
graphRect.top = topOverflow;
if(topAxesYCoords)
{
i = 0;
l = topAxesYCoords.length;
for(; i < l; ++i)
{
topAxesYCoords[i] += diff;
}
}
}
diff = bottomOverflow - bottomPaneHeight;
if(diff > 0)
{
graphRect.bottom = h - bottomOverflow;
if(bottomAxesYCoords)
{
i = 0;
l = bottomAxesYCoords.length;
for(; i < l; ++i)
{
bottomAxesYCoords[i] -= diff;
}
}
}
diff = leftOverflow - leftPaneWidth;
if(diff > 0)
{
graphRect.left = leftOverflow;
if(leftAxesXCoords)
{
i = 0;
l = leftAxesXCoords.length;
for(; i < l; ++i)
{
leftAxesXCoords[i] += diff;
}
}
}
diff = rightOverflow - rightPaneWidth;
if(diff > 0)
{
graphRect.right = w - rightOverflow;
if(rightAxesXCoords)
{
i = 0;
l = rightAxesXCoords.length;
for(; i < l; ++i)
{
rightAxesXCoords[i] -= diff;
}
}
}
}
graphWidth = graphRect.right - graphRect.left;
graphHeight = graphRect.bottom - graphRect.top;
graphX = graphRect.left;
graphY = graphRect.top;
if(legend)
{
if(legend.get("includeInChartLayout"))
{
switch(legend.get("position"))
{
case "left" :
legend.set("y", graphY);
legend.set("height", graphHeight);
break;
case "top" :
legend.set("x", graphX);
legend.set("width", graphWidth);
break;
case "bottom" :
legend.set("x", graphX);
legend.set("width", graphWidth);
break;
case "right" :
legend.set("y", graphY);
legend.set("height", graphHeight);
break;
}
}
}
if(topAxesCollection)
{
l = topAxesCollection.length;
i = 0;
for(; i < l; i++)
{
axis = topAxesCollection[i];
if(axis.get("width") !== graphWidth)
{
axis.set("width", graphWidth);
}
axis.get("boundingBox").setStyle("left", graphX + PX);
axis.get("boundingBox").setStyle("top", topAxesYCoords[i] + PX);
}
if(axis._hasDataOverflow())
{
graphOverflow = "hidden";
}
}
if(bottomAxesCollection)
{
l = bottomAxesCollection.length;
i = 0;
for(; i < l; i++)
{
axis = bottomAxesCollection[i];
if(axis.get("width") !== graphWidth)
{
axis.set("width", graphWidth);
}
axis.get("boundingBox").setStyle("left", graphX + PX);
axis.get("boundingBox").setStyle("top", bottomAxesYCoords[i] + PX);
}
if(axis._hasDataOverflow())
{
graphOverflow = "hidden";
}
}
if(leftAxesCollection)
{
l = leftAxesCollection.length;
i = 0;
for(; i < l; ++i)
{
axis = leftAxesCollection[i];
axis.get("boundingBox").setStyle("top", graphY + PX);
axis.get("boundingBox").setStyle("left", leftAxesXCoords[i] + PX);
if(axis.get("height") !== graphHeight)
{
axis.set("height", graphHeight);
}
}
if(axis._hasDataOverflow())
{
graphOverflow = "hidden";
}
}
if(rightAxesCollection)
{
l = rightAxesCollection.length;
i = 0;
for(; i < l; ++i)
{
axis = rightAxesCollection[i];
axis.get("boundingBox").setStyle("top", graphY + PX);
axis.get("boundingBox").setStyle("left", rightAxesXCoords[i] + PX);
if(axis.get("height") !== graphHeight)
{
axis.set("height", graphHeight);
}
}
if(axis._hasDataOverflow())
{
graphOverflow = "hidden";
}
}
this._drawing = false;
if(this._callLater)
{
this._redraw();
return;
}
if(graph)
{
graph.get("boundingBox").setStyle("left", graphX + PX);
graph.get("boundingBox").setStyle("top", graphY + PX);
graph.set("width", graphWidth);
graph.set("height", graphHeight);
graph.get("boundingBox").setStyle("overflow", graphOverflow);
}
if(this._overlay)
{
this._overlay.setStyle("left", graphX + PX);
this._overlay.setStyle("top", graphY + PX);
this._overlay.setStyle("width", graphWidth + PX);
this._overlay.setStyle("height", graphHeight + PX);
}
},
/**
* Positions the legend in a chart and returns the properties of the legend to be used in the
* chart's layout algorithm.
*
* @method _getLayoutDimensions
* @return {Object} The left, top, right and bottom values for the legend.
* @protected
*/
_getLayoutBoxDimensions: function()
{
var box = {
top: 0,
right: 0,
bottom: 0,
left: 0
},
legend = this.get("legend"),
position,
direction,
dimension,
size,
w = this.get(WIDTH),
h = this.get(HEIGHT),
gap;
if(legend && legend.get("includeInChartLayout"))
{
gap = legend.get("styles").gap;
position = legend.get(POSITION);
if(position != EXTERNAL)
{
direction = legend.get("direction");
dimension = direction == HORIZONTAL ? HEIGHT : WIDTH;
size = legend.get(dimension);
box[position] = size + gap;
switch(position)
{
case TOP :
legend.set(_Y, 0);
break;
case BOTTOM :
legend.set(_Y, h - size);
break;
case RIGHT :
legend.set(_X, w - size);
break;
case LEFT:
legend.set(_X, 0);
break;
}
}
}
return box;
},
/**
* Destructor implementation for the CartesianChart class. Calls destroy on all axes, series, legend (if available) and the Graph instance.
* Removes the tooltip and overlay HTML elements.
*
* @method destructor
* @protected
*/
destructor: function()
{
var legend = this.get("legend");
if(legend)
{
legend.destroy(true);
}
}
}, {
ATTRS: {
legend: LEGEND
}
});
Y.CartesianChart = CartesianChartLegend;