/**
* The CartesianChart class creates a chart with horizontal and vertical axes.
*
* @module charts
* @submodule charts-base
* @class CartesianChart
* @extends ChartBase
* @constructor
*/
Y.CartesianChart = Y.Base.create("cartesianChart", Y.Widget, [Y.ChartBase], {
/**
* @method renderUI
* @private
*/
renderUI: function()
{
var bb = this.get("boundingBox"),
cb = this.get("contentBox"),
tt = this.get("tooltip"),
overlay,
overlayClass = _getClassName("overlay");
//move the position = absolute logic to a class file
bb.setStyle("position", "absolute");
cb.setStyle("position", "absolute");
this._addAxes();
this._addGridlines();
this._addSeries();
if(tt && tt.show)
{
this._addTooltip();
}
//If there is a style definition. Force them to set.
this.get("styles");
if(this.get("interactionType") == "planar")
{
overlay = DOCUMENT.createElement("div");
this.get("contentBox").appendChild(overlay);
this._overlay = Y.one(overlay);
this._overlay.set("id", this.get("id") + "_overlay");
this._overlay.setStyle("position", "absolute");
this._overlay.setStyle("background", "#fff");
this._overlay.setStyle("opacity", 0);
this._overlay.addClass(overlayClass);
this._overlay.setStyle("zIndex", 4);
}
this._setAriaElements(bb, cb);
this._redraw();
},
/**
* When `interactionType` is set to `planar`, listens for mouse move events and fires `planarEvent:mouseover` or `planarEvent:mouseout`
* depending on the position of the mouse in relation to data points on the `Chart`.
*
* @method _planarEventDispatcher
* @param {Object} e Event object.
* @private
*/
_planarEventDispatcher: function(e)
{
var graph = this.get("graph"),
bb = this.get("boundingBox"),
cb = graph.get("contentBox"),
isTouch = e && e.hasOwnProperty("changedTouches"),
pageX = isTouch ? e.changedTouches[0].pageX : e.pageX,
pageY = isTouch ? e.changedTouches[0].pageY : e.pageY,
posX = pageX - bb.getX(),
posY = pageY - bb.getY(),
offset = {
x: pageX - cb.getX(),
y: pageY - cb.getY()
},
sc = graph.get("seriesCollection"),
series,
i = 0,
index,
oldIndex = this._selectedIndex,
item,
items = [],
categoryItems = [],
valueItems = [],
direction = this.get("direction"),
hasMarkers,
catAxis,
valAxis,
coord,
//data columns and area data could be created on a graph level
markerPlane,
len,
coords;
e.halt(true);
if(direction == "horizontal")
{
catAxis = "x";
valAxis = "y";
}
else
{
valAxis = "x";
catAxis = "y";
}
coord = offset[catAxis];
if(sc)
{
len = sc.length;
while(i < len && !markerPlane)
{
if(sc[i])
{
markerPlane = sc[i].get(catAxis + "MarkerPlane");
}
i++;
}
}
if(markerPlane)
{
len = markerPlane.length;
for(i = 0; i < len; ++i)
{
if(coord <= markerPlane[i].end && coord >= markerPlane[i].start)
{
index = i;
break;
}
}
len = sc.length;
for(i = 0; i < len; ++i)
{
series = sc[i];
coords = series.get(valAxis + "coords");
hasMarkers = series.get("markers");
if(hasMarkers && !isNaN(oldIndex) && oldIndex > -1)
{
series.updateMarkerState("mouseout", oldIndex);
}
if(coords && coords[index] > -1)
{
if(hasMarkers && !isNaN(index) && index > -1)
{
series.updateMarkerState("mouseover", index);
}
item = this.getSeriesItems(series, index);
categoryItems.push(item.category);
valueItems.push(item.value);
items.push(series);
}
}
this._selectedIndex = index;
/**
* Broadcasts when `interactionType` is set to `planar` and a series' marker plane has received a mouseover event.
*
*
* @event planarEvent:mouseover
* @preventable false
* @param {EventFacade} e Event facade with the following additional
* properties:
* <dl>
* <dt>categoryItem</dt><dd>An array of hashes, each containing information about the category `Axis` of each marker
* whose plane has been intersected.</dd>
* <dt>valueItem</dt><dd>An array of hashes, each containing information about the value `Axis` of each marker whose
* plane has been intersected.</dd>
* <dt>x</dt><dd>The x-coordinate of the mouse in relation to the Chart.</dd>
* <dt>y</dt><dd>The y-coordinate of the mouse in relation to the Chart.</dd>
* <dt>pageX</dt><dd>The x location of the event on the page (including scroll)</dd>
* <dt>pageY</dt><dd>The y location of the event on the page (including scroll)</dd>
* <dt>items</dt><dd>An array including all the series which contain a marker whose plane has been intersected.</dd>
* <dt>index</dt><dd>Index of the markers in their respective series.</dd>
* <dt>originEvent</dt><dd>Underlying dom event.</dd>
* </dl>
*/
/**
* Broadcasts when `interactionType` is set to `planar` and a series' marker plane has received a mouseout event.
*
* @event planarEvent:mouseout
* @preventable false
* @param {EventFacade} e
*/
if(index > -1)
{
this.fire("planarEvent:mouseover", {
categoryItem:categoryItems,
valueItem:valueItems,
x:posX,
y:posY,
pageX:pageX,
pageY:pageY,
items:items,
index:index,
originEvent:e
});
}
else
{
this.fire("planarEvent:mouseout");
}
}
},
/**
* Indicates the default series type for the chart.
*
* @property _type
* @type {String}
* @private
*/
_type: "combo",
/**
* Queue of axes instances that will be updated. This method is used internally to determine when all axes have been updated.
*
* @property _itemRenderQueue
* @type Array
* @private
*/
_itemRenderQueue: null,
/**
* Adds an `Axis` instance to the `_itemRenderQueue`.
*
* @method _addToAxesRenderQueue
* @param {Axis} axis An `Axis` instance.
* @private
*/
_addToAxesRenderQueue: function(axis)
{
if(!this._itemRenderQueue)
{
this._itemRenderQueue = [];
}
if(Y.Array.indexOf(this._itemRenderQueue, axis) < 0)
{
this._itemRenderQueue.push(axis);
}
},
/**
* Adds axis instance to the appropriate array based on position
*
* @method _addToAxesCollection
* @param {String} position The position of the axis
* @param {Axis} axis The `Axis` instance
*/
_addToAxesCollection: function(position, axis)
{
var axesCollection = this.get(position + "AxesCollection");
if(!axesCollection)
{
axesCollection = [];
this.set(position + "AxesCollection", axesCollection);
}
axesCollection.push(axis);
},
/**
* Returns the default value for the `seriesCollection` attribute.
*
* @method _getDefaultSeriesCollection
* @param {Array} val Array containing either `CartesianSeries` instances or objects containing data to construct series instances.
* @return Array
* @private
*/
_getDefaultSeriesCollection: function()
{
var seriesCollection,
dataProvider = this.get("dataProvider");
if(dataProvider)
{
seriesCollection = this._parseSeriesCollection();
}
return seriesCollection;
},
/**
* Parses and returns a series collection from an object and default properties.
*
* @method _parseSeriesCollection
* @param {Object} val Object contain properties for series being set.
* @return Object
* @private
*/
_parseSeriesCollection: function(val)
{
var dir = this.get("direction"),
sc = [],
catAxis,
valAxis,
tempKeys = [],
series,
seriesKeys = this.get("seriesKeys").concat(),
i,
index,
l,
type = this.get("type"),
key,
catKey,
seriesKey,
graph,
orphans = [],
categoryKey = this.get("categoryKey"),
showMarkers = this.get("showMarkers"),
showAreaFill = this.get("showAreaFill"),
showLines = this.get("showLines");
val = val || [];
if(dir == "vertical")
{
catAxis = "yAxis";
catKey = "yKey";
valAxis = "xAxis";
seriesKey = "xKey";
}
else
{
catAxis = "xAxis";
catKey = "xKey";
valAxis = "yAxis";
seriesKey = "yKey";
}
l = val.length;
while(val && val.length > 0)
{
series = val.shift();
key = this._getBaseAttribute(series, seriesKey);
if(key)
{
index = Y.Array.indexOf(seriesKeys, key);
if(index > -1)
{
seriesKeys.splice(index, 1);
tempKeys.push(key);
sc.push(series);
}
else
{
orphans.push(series);
}
}
else
{
orphans.push(series);
}
}
while(orphans.length > 0)
{
series = orphans.shift();
if(seriesKeys.length > 0)
{
key = seriesKeys.shift();
this._setBaseAttribute(series, seriesKey, key);
tempKeys.push(key);
sc.push(series);
}
else if(series instanceof Y.CartesianSeries)
{
series.destroy(true);
}
}
if(seriesKeys.length > 0)
{
tempKeys = tempKeys.concat(seriesKeys);
}
l = tempKeys.length;
for(i = 0; i < l; ++i)
{
series = sc[i] || {type:type};
if(series instanceof Y.CartesianSeries)
{
this._parseSeriesAxes(series);
continue;
}
series[catKey] = series[catKey] || categoryKey;
series[seriesKey] = series[seriesKey] || seriesKeys.shift();
series[catAxis] = this._getCategoryAxis();
series[valAxis] = this._getSeriesAxis(series[seriesKey]);
series.type = series.type || type;
series.direction = series.direction || dir;
if((series.type == "combo" || series.type == "stackedcombo" || series.type == "combospline" || series.type == "stackedcombospline"))
{
if(showAreaFill !== null)
{
series.showAreaFill = (series.showAreaFill !== null && series.showAreaFill !== undefined) ? series.showAreaFill : showAreaFill;
}
if(showMarkers !== null)
{
series.showMarkers = (series.showMarkers !== null && series.showMarkers !== undefined) ? series.showMarkers : showMarkers;
}
if(showLines !== null)
{
series.showLines = (series.showLines !== null && series.showLines !== undefined) ? series.showLines : showLines;
}
}
sc[i] = series;
}
if(sc)
{
graph = this.get("graph");
graph.set("seriesCollection", sc);
sc = graph.get("seriesCollection");
}
return sc;
},
/**
* Parse and sets the axes for a series instance.
*
* @method _parseSeriesAxes
* @param {CartesianSeries} series A `CartesianSeries` instance.
* @private
*/
_parseSeriesAxes: function(series)
{
var axes = this.get("axes"),
xAxis = series.get("xAxis"),
yAxis = series.get("yAxis"),
YAxis = Y.Axis,
axis;
if(xAxis && !(xAxis instanceof YAxis) && Y_Lang.isString(xAxis) && axes.hasOwnProperty(xAxis))
{
axis = axes[xAxis];
if(axis instanceof YAxis)
{
series.set("xAxis", axis);
}
}
if(yAxis && !(yAxis instanceof YAxis) && Y_Lang.isString(yAxis) && axes.hasOwnProperty(yAxis))
{
axis = axes[yAxis];
if(axis instanceof YAxis)
{
series.set("yAxis", axis);
}
}
},
/**
* Returns the category axis instance for the chart.
*
* @method _getCategoryAxis
* @return Axis
* @private
*/
_getCategoryAxis: function()
{
var axis,
axes = this.get("axes"),
categoryAxisName = this.get("categoryAxisName") || this.get("categoryKey");
axis = axes[categoryAxisName];
return axis;
},
/**
* Returns the value axis for a series.
*
* @method _getSeriesAxis
* @param {String} key The key value used to determine the axis instance.
* @return Axis
* @private
*/
_getSeriesAxis:function(key, axisName)
{
var axes = this.get("axes"),
i,
keys,
axis;
if(axes)
{
if(axisName && axes.hasOwnProperty(axisName))
{
axis = axes[axisName];
}
else
{
for(i in axes)
{
if(axes.hasOwnProperty(i))
{
keys = axes[i].get("keys");
if(keys && keys.hasOwnProperty(key))
{
axis = axes[i];
break;
}
}
}
}
}
return axis;
},
/**
* Gets an attribute from an object, using a getter for Base objects and a property for object
* literals. Used for determining attributes from series/axis references which can be an actual class instance
* or a hash of properties that will be used to create a class instance.
*
* @method _getBaseAttribute
* @param {Object} item Object or instance in which the attribute resides.
* @param {String} key Attribute whose value will be returned.
* @return Object
* @private
*/
_getBaseAttribute: function(item, key)
{
if(item instanceof Y.Base)
{
return item.get(key);
}
if(item.hasOwnProperty(key))
{
return item[key];
}
return null;
},
/**
* Sets an attribute on an object, using a setter of Base objects and a property for object
* literals. Used for setting attributes on a Base class, either directly or to be stored in an object literal
* for use at instantiation.
*
* @method _setBaseAttribute
* @param {Object} item Object or instance in which the attribute resides.
* @param {String} key Attribute whose value will be assigned.
* @param {Object} value Value to be assigned to the attribute.
* @private
*/
_setBaseAttribute: function(item, key, value)
{
if(item instanceof Y.Base)
{
item.set(key, value);
}
else
{
item[key] = value;
}
},
/**
* Creates `Axis` instances.
*
* @method _setAxes
* @param {Object} val Object containing `Axis` instances or objects in which to construct `Axis` instances.
* @return Object
* @private
*/
_setAxes: function(val)
{
var hash = this._parseAxes(val),
axes = {},
axesAttrs = {
edgeOffset: "edgeOffset",
position: "position",
overlapGraph:"overlapGraph",
labelFunction:"labelFunction",
labelFunctionScope:"labelFunctionScope",
labelFormat:"labelFormat",
appendLabelFunction: "appendLabelFunction",
appendTitleFunction: "appendTitleFunction",
maximum:"maximum",
minimum:"minimum",
roundingMethod:"roundingMethod",
alwaysShowZero:"alwaysShowZero",
title:"title",
width:"width",
height:"height"
},
dp = this.get("dataProvider"),
ai,
i,
pos,
axis,
axisPosition,
dh,
axisClass,
config,
axesCollection;
for(i in hash)
{
if(hash.hasOwnProperty(i))
{
dh = hash[i];
if(dh instanceof Y.Axis)
{
axis = dh;
}
else
{
axis = null;
config = {};
config.dataProvider = dh.dataProvider || dp;
config.keys = dh.keys;
if(dh.hasOwnProperty("roundingUnit"))
{
config.roundingUnit = dh.roundingUnit;
}
pos = dh.position;
if(dh.styles)
{
config.styles = dh.styles;
}
config.position = dh.position;
for(ai in axesAttrs)
{
if(axesAttrs.hasOwnProperty(ai) && dh.hasOwnProperty(ai))
{
config[ai] = dh[ai];
}
}
//only check for existing axis if we constructed the default axes already
if(val)
{
axis = this.getAxisByKey(i);
}
if(axis && axis instanceof Y.Axis)
{
axisPosition = axis.get("position");
if(pos != axisPosition)
{
if(axisPosition != "none")
{
axesCollection = this.get(axisPosition + "AxesCollection");
axesCollection.splice(Y.Array.indexOf(axesCollection, axis), 1);
}
if(pos != "none")
{
this._addToAxesCollection(pos, axis);
}
}
axis.setAttrs(config);
}
else
{
axisClass = this._getAxisClass(dh.type);
axis = new axisClass(config);
axis.after("axisRendered", Y.bind(this._itemRendered, this));
}
}
if(axis)
{
axesCollection = this.get(pos + "AxesCollection");
if(axesCollection && Y.Array.indexOf(axesCollection, axis) > 0)
{
axis.set("overlapGraph", false);
}
axes[i] = axis;
}
}
}
return axes;
},
/**
* Adds axes to the chart.
*
* @method _addAxes
* @private
*/
_addAxes: function()
{
var axes = this.get("axes"),
i,
axis,
pos,
w = this.get("width"),
h = this.get("height"),
node = Y.Node.one(this._parentNode);
if(!this._axesCollection)
{
this._axesCollection = [];
}
for(i in axes)
{
if(axes.hasOwnProperty(i))
{
axis = axes[i];
if(axis instanceof Y.Axis)
{
if(!w)
{
this.set("width", node.get("offsetWidth"));
w = this.get("width");
}
if(!h)
{
this.set("height", node.get("offsetHeight"));
h = this.get("height");
}
this._addToAxesRenderQueue(axis);
pos = axis.get("position");
if(!this.get(pos + "AxesCollection"))
{
this.set(pos + "AxesCollection", [axis]);
}
else
{
this.get(pos + "AxesCollection").push(axis);
}
this._axesCollection.push(axis);
if(axis.get("keys").hasOwnProperty(this.get("categoryKey")))
{
this.set("categoryAxis", axis);
}
axis.render(this.get("contentBox"));
}
}
}
},
/**
* Renders the Graph.
*
* @method _addSeries
* @private
*/
_addSeries: function()
{
var graph = this.get("graph"),
sc = this.get("seriesCollection");
graph.render(this.get("contentBox"));
},
/**
* Adds gridlines to the chart.
*
* @method _addGridlines
* @private
*/
_addGridlines: function()
{
var graph = this.get("graph"),
hgl = this.get("horizontalGridlines"),
vgl = this.get("verticalGridlines"),
direction = this.get("direction"),
leftAxesCollection = this.get("leftAxesCollection"),
rightAxesCollection = this.get("rightAxesCollection"),
bottomAxesCollection = this.get("bottomAxesCollection"),
topAxesCollection = this.get("topAxesCollection"),
seriesAxesCollection,
catAxis = this.get("categoryAxis"),
hAxis,
vAxis;
if(this._axesCollection)
{
seriesAxesCollection = this._axesCollection.concat();
seriesAxesCollection.splice(Y.Array.indexOf(seriesAxesCollection, catAxis), 1);
}
if(hgl)
{
if(leftAxesCollection && leftAxesCollection[0])
{
hAxis = leftAxesCollection[0];
}
else if(rightAxesCollection && rightAxesCollection[0])
{
hAxis = rightAxesCollection[0];
}
else
{
hAxis = direction == "horizontal" ? catAxis : seriesAxesCollection[0];
}
if(!this._getBaseAttribute(hgl, "axis") && hAxis)
{
this._setBaseAttribute(hgl, "axis", hAxis);
}
if(this._getBaseAttribute(hgl, "axis"))
{
graph.set("horizontalGridlines", hgl);
}
}
if(vgl)
{
if(bottomAxesCollection && bottomAxesCollection[0])
{
vAxis = bottomAxesCollection[0];
}
else if (topAxesCollection && topAxesCollection[0])
{
vAxis = topAxesCollection[0];
}
else
{
vAxis = direction == "vertical" ? catAxis : seriesAxesCollection[0];
}
if(!this._getBaseAttribute(vgl, "axis") && vAxis)
{
this._setBaseAttribute(vgl, "axis", vAxis);
}
if(this._getBaseAttribute(vgl, "axis"))
{
graph.set("verticalGridlines", vgl);
}
}
},
/**
* Default Function for the axes attribute.
*
* @method _getDefaultAxes
* @return Object
* @private
*/
_getDefaultAxes: function()
{
var axes;
if(this.get("dataProvider"))
{
axes = this._parseAxes();
}
return axes;
},
/**
* Generates and returns a key-indexed object containing `Axis` instances or objects used to create `Axis` instances.
*
* @method _parseAxes
* @param {Object} axes Object containing `Axis` instances or `Axis` attributes.
* @return Object
* @private
*/
_parseAxes: function(axes)
{
var catKey = this.get("categoryKey"),
axis,
attr,
keys,
newAxes = {},
claimedKeys = [],
categoryAxisName = this.get("categoryAxisName") || this.get("categoryKey"),
valueAxisName = this.get("valueAxisName"),
seriesKeys = this.get("seriesKeys").concat(),
i,
l,
ii,
ll,
cIndex,
direction = this.get("direction"),
seriesPosition,
categoryPosition,
valueAxes = [],
seriesAxis = this.get("stacked") ? "stacked" : "numeric";
if(direction == "vertical")
{
seriesPosition = "bottom";
categoryPosition = "left";
}
else
{
seriesPosition = "left";
categoryPosition = "bottom";
}
if(axes)
{
for(i in axes)
{
if(axes.hasOwnProperty(i))
{
axis = axes[i];
keys = this._getBaseAttribute(axis, "keys");
attr = this._getBaseAttribute(axis, "type");
if(attr == "time" || attr == "category")
{
categoryAxisName = i;
this.set("categoryAxisName", i);
if(Y_Lang.isArray(keys) && keys.length > 0)
{
catKey = keys[0];
this.set("categoryKey", catKey);
}
newAxes[i] = axis;
}
else if(i == categoryAxisName)
{
newAxes[i] = axis;
}
else
{
newAxes[i] = axis;
if(i != valueAxisName && keys && Y_Lang.isArray(keys))
{
ll = keys.length;
for(ii = 0; ii < ll; ++ii)
{
claimedKeys.push(keys[ii]);
}
valueAxes.push(newAxes[i]);
}
if(!(this._getBaseAttribute(newAxes[i], "type")))
{
this._setBaseAttribute(newAxes[i], "type", seriesAxis);
}
if(!(this._getBaseAttribute(newAxes[i], "position")))
{
this._setBaseAttribute(newAxes[i], "position", this._getDefaultAxisPosition(newAxes[i], valueAxes, seriesPosition));
}
}
}
}
}
cIndex = Y.Array.indexOf(seriesKeys, catKey);
if(cIndex > -1)
{
seriesKeys.splice(cIndex, 1);
}
l = claimedKeys.length;
for(i = 0; i < l; ++i)
{
cIndex = Y.Array.indexOf(seriesKeys, claimedKeys[i]);
if(cIndex > -1)
{
seriesKeys.splice(cIndex, 1);
}
}
if(!newAxes.hasOwnProperty(categoryAxisName))
{
newAxes[categoryAxisName] = {};
}
if(!(this._getBaseAttribute(newAxes[categoryAxisName], "keys")))
{
this._setBaseAttribute(newAxes[categoryAxisName], "keys", [catKey]);
}
if(!(this._getBaseAttribute(newAxes[categoryAxisName], "position")))
{
this._setBaseAttribute(newAxes[categoryAxisName], "position", categoryPosition);
}
if(!(this._getBaseAttribute(newAxes[categoryAxisName], "type")))
{
this._setBaseAttribute(newAxes[categoryAxisName], "type", this.get("categoryType"));
}
if(!newAxes.hasOwnProperty(valueAxisName) && seriesKeys && seriesKeys.length > 0)
{
newAxes[valueAxisName] = {keys:seriesKeys};
valueAxes.push(newAxes[valueAxisName]);
}
if(claimedKeys.length > 0)
{
if(seriesKeys.length > 0)
{
seriesKeys = claimedKeys.concat(seriesKeys);
}
else
{
seriesKeys = claimedKeys;
}
}
if(newAxes.hasOwnProperty(valueAxisName))
{
if(!(this._getBaseAttribute(newAxes[valueAxisName], "position")))
{
this._setBaseAttribute(newAxes[valueAxisName], "position", this._getDefaultAxisPosition(newAxes[valueAxisName], valueAxes, seriesPosition));
}
this._setBaseAttribute(newAxes[valueAxisName], "type", seriesAxis);
this._setBaseAttribute(newAxes[valueAxisName], "keys", seriesKeys);
}
if(!this._seriesKeysExplicitlySet)
{
this._seriesKeys = seriesKeys;
}
return newAxes;
},
/**
* Determines the position of an axis when one is not specified.
*
* @method _getDefaultAxisPosition
* @param {Axis} axis `Axis` instance.
* @param {Array} valueAxes Array of `Axis` instances.
* @param {String} position Default position depending on the direction of the chart and type of axis.
* @return String
* @private
*/
_getDefaultAxisPosition: function(axis, valueAxes, position)
{
var direction = this.get("direction"),
i = Y.Array.indexOf(valueAxes, axis);
if(valueAxes[i - 1] && valueAxes[i - 1].position)
{
if(direction == "horizontal")
{
if(valueAxes[i - 1].position == "left")
{
position = "right";
}
else if(valueAxes[i - 1].position == "right")
{
position = "left";
}
}
else
{
if (valueAxes[i -1].position == "bottom")
{
position = "top";
}
else
{
position = "bottom";
}
}
}
return position;
},
/**
* Returns an object literal containing a categoryItem and a valueItem for a given series index. Below is the structure of each:
*
* @method getSeriesItems
* @param {CartesianSeries} series Reference to a series.
* @param {Number} index Index of the specified item within a series.
* @return Object An object literal containing the following:
*
* <dl>
* <dt>categoryItem</dt><dd>Object containing the following data related to the category axis of the series.
* <dl>
* <dt>axis</dt><dd>Reference to the category axis of the series.</dd>
* <dt>key</dt><dd>Category key for the series.</dd>
* <dt>value</dt><dd>Value on the axis corresponding to the series index.</dd>
* </dl>
* </dd>
* <dt>valueItem</dt><dd>Object containing the following data related to the category axis of the series.
* <dl>
* <dt>axis</dt><dd>Reference to the value axis of the series.</dd>
* <dt>key</dt><dd>Value key for the series.</dd>
* <dt>value</dt><dd>Value on the axis corresponding to the series index.</dd>
* </dl>
* </dd>
* </dl>
*/
getSeriesItems: function(series, index)
{
var xAxis = series.get("xAxis"),
yAxis = series.get("yAxis"),
xKey = series.get("xKey"),
yKey = series.get("yKey"),
categoryItem,
valueItem;
if(this.get("direction") == "vertical")
{
categoryItem = {
axis:yAxis,
key:yKey,
value:yAxis.getKeyValueAt(yKey, index)
};
valueItem = {
axis:xAxis,
key:xKey,
value: xAxis.getKeyValueAt(xKey, index)
};
}
else
{
valueItem = {
axis:yAxis,
key:yKey,
value:yAxis.getKeyValueAt(yKey, index)
};
categoryItem = {
axis:xAxis,
key:xKey,
value: xAxis.getKeyValueAt(xKey, index)
};
}
categoryItem.displayName = series.get("categoryDisplayName");
valueItem.displayName = series.get("valueDisplayName");
categoryItem.value = categoryItem.axis.getKeyValueAt(categoryItem.key, index);
valueItem.value = valueItem.axis.getKeyValueAt(valueItem.key, index);
return {category:categoryItem, value:valueItem};
},
/**
* Handler for sizeChanged event.
*
* @method _sizeChanged
* @param {Object} e Event object.
* @private
*/
_sizeChanged: function(e)
{
if(this._axesCollection)
{
var ac = this._axesCollection,
i = 0,
l = ac.length;
for(; i < l; ++i)
{
this._addToAxesRenderQueue(ac[i]);
}
this._redraw();
}
},
/**
* Returns the maximum distance in pixels that the extends outside the top bounds of all vertical axes.
*
* @method _getTopOverflow
* @param {Array} set1 Collection of axes to check.
* @param {Array} set2 Seconf collection of axes to check.
* @param {Number} width Width of the axes
* @return Number
* @private
*/
_getTopOverflow: function(set1, set2, height)
{
var i = 0,
len,
overflow = 0,
axis;
if(set1)
{
len = set1.length;
for(; i < len; ++i)
{
axis = set1[i];
overflow = Math.max(overflow, Math.abs(axis.getMaxLabelBounds().top) - (axis.getEdgeOffset(axis.get("styles").majorTicks.count, height) * 0.5));
}
}
if(set2)
{
i = 0;
len = set2.length;
for(; i < len; ++i)
{
axis = set2[i];
overflow = Math.max(overflow, Math.abs(axis.getMaxLabelBounds().top) - (axis.getEdgeOffset(axis.get("styles").majorTicks.count, height) * 0.5));
}
}
return overflow;
},
/**
* Returns the maximum distance in pixels that the extends outside the right bounds of all horizontal axes.
*
* @method _getRightOverflow
* @param {Array} set1 Collection of axes to check.
* @param {Array} set2 Seconf collection of axes to check.
* @param {Number} width Width of the axes
* @return Number
* @private
*/
_getRightOverflow: function(set1, set2, width)
{
var i = 0,
len,
overflow = 0,
axis;
if(set1)
{
len = set1.length;
for(; i < len; ++i)
{
axis = set1[i];
overflow = Math.max(overflow, axis.getMaxLabelBounds().right - (axis.getEdgeOffset(axis.get("styles").majorTicks.count, width) * 0.5));
}
}
if(set2)
{
i = 0;
len = set2.length;
for(; i < len; ++i)
{
axis = set2[i];
overflow = Math.max(overflow, axis.getMaxLabelBounds().right - (axis.getEdgeOffset(axis.get("styles").majorTicks.count, width) * 0.5));
}
}
return overflow;
},
/**
* Returns the maximum distance in pixels that the extends outside the left bounds of all horizontal axes.
*
* @method _getLeftOverflow
* @param {Array} set1 Collection of axes to check.
* @param {Array} set2 Seconf collection of axes to check.
* @param {Number} width Width of the axes
* @return Number
* @private
*/
_getLeftOverflow: function(set1, set2, width)
{
var i = 0,
len,
overflow = 0,
axis;
if(set1)
{
len = set1.length;
for(; i < len; ++i)
{
axis = set1[i];
overflow = Math.max(overflow, Math.abs(axis.getMinLabelBounds().left) - (axis.getEdgeOffset(axis.get("styles").majorTicks.count, width) * 0.5));
}
}
if(set2)
{
i = 0;
len = set2.length;
for(; i < len; ++i)
{
axis = set2[i];
overflow = Math.max(overflow, Math.abs(axis.getMinLabelBounds().left) - (axis.getEdgeOffset(axis.get("styles").majorTicks.count, width) * 0.5));
}
}
return overflow;
},
/**
* Returns the maximum distance in pixels that the extends outside the bottom bounds of all vertical axes.
*
* @method _getBottomOverflow
* @param {Array} set1 Collection of axes to check.
* @param {Array} set2 Seconf collection of axes to check.
* @param {Number} height Height of the axes
* @return Number
* @private
*/
_getBottomOverflow: function(set1, set2, height)
{
var i = 0,
len,
overflow = 0,
axis;
if(set1)
{
len = set1.length;
for(; i < len; ++i)
{
axis = set1[i];
overflow = Math.max(overflow, axis.getMinLabelBounds().bottom - (axis.getEdgeOffset(axis.get("styles").majorTicks.count, height) * 0.5));
}
}
if(set2)
{
i = 0;
len = set2.length;
for(; i < len; ++i)
{
axis = set2[i];
overflow = Math.max(overflow, axis.getMinLabelBounds().bottom - (axis.getEdgeOffset(axis.get("styles").majorTicks.count, height) * 0.5));
}
}
return overflow;
},
/**
* 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"),
leftPaneWidth = 0,
rightPaneWidth = 0,
topPaneHeight = 0,
bottomPaneHeight = 0,
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,
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(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");
}
},
/**
* Destructor implementation for the CartesianChart class. Calls destroy on all axes, series and the Graph instance.
* Removes the tooltip and overlay HTML elements.
*
* @method destructor
* @protected
*/
destructor: function()
{
var graph = this.get("graph"),
i = 0,
len,
seriesCollection = this.get("seriesCollection"),
axesCollection = this._axesCollection,
tooltip = this.get("tooltip").node;
if(this._description)
{
this._description.empty();
this._description.remove(true);
}
if(this._liveRegion)
{
this._liveRegion.empty();
this._liveRegion.remove(true);
}
len = seriesCollection ? seriesCollection.length : 0;
for(; i < len; ++i)
{
if(seriesCollection[i] instanceof Y.CartesianSeries)
{
seriesCollection[i].destroy(true);
}
}
len = axesCollection ? axesCollection.length : 0;
for(i = 0; i < len; ++i)
{
if(axesCollection[i] instanceof Y.Axis)
{
axesCollection[i].destroy(true);
}
}
if(graph)
{
graph.destroy(true);
}
if(tooltip)
{
tooltip.empty();
tooltip.remove(true);
}
if(this._overlay)
{
this._overlay.empty();
this._overlay.remove(true);
}
},
/**
* Returns the appropriate message based on the key press.
*
* @method _getAriaMessage
* @param {Number} key The keycode that was pressed.
* @return String
*/
_getAriaMessage: function(key)
{
var msg = "",
series,
items,
categoryItem,
valueItem,
seriesIndex = this._seriesIndex,
itemIndex = this._itemIndex,
seriesCollection = this.get("seriesCollection"),
len = seriesCollection.length,
dataLength;
if(key % 2 === 0)
{
if(len > 1)
{
if(key === 38)
{
seriesIndex = seriesIndex < 1 ? len - 1 : seriesIndex - 1;
}
else if(key === 40)
{
seriesIndex = seriesIndex >= len - 1 ? 0 : seriesIndex + 1;
}
this._itemIndex = -1;
}
else
{
seriesIndex = 0;
}
this._seriesIndex = seriesIndex;
series = this.getSeries(parseInt(seriesIndex, 10));
msg = series.get("valueDisplayName") + " series.";
}
else
{
if(seriesIndex > -1)
{
msg = "";
series = this.getSeries(parseInt(seriesIndex, 10));
}
else
{
seriesIndex = 0;
this._seriesIndex = seriesIndex;
series = this.getSeries(parseInt(seriesIndex, 10));
msg = series.get("valueDisplayName") + " series.";
}
dataLength = series._dataLength ? series._dataLength : 0;
if(key === 37)
{
itemIndex = itemIndex > 0 ? itemIndex - 1 : dataLength - 1;
}
else if(key === 39)
{
itemIndex = itemIndex >= dataLength - 1 ? 0 : itemIndex + 1;
}
this._itemIndex = itemIndex;
items = this.getSeriesItems(series, itemIndex);
categoryItem = items.category;
valueItem = items.value;
if(categoryItem && valueItem && categoryItem.value && valueItem.value)
{
msg += categoryItem.displayName + ": " + categoryItem.axis.formatLabel.apply(this, [categoryItem.value, categoryItem.axis.get("labelFormat")]) + ", ";
msg += valueItem.displayName + ": " + valueItem.axis.formatLabel.apply(this, [valueItem.value, valueItem.axis.get("labelFormat")]) + ", ";
}
else
{
msg += "No data available.";
}
msg += (itemIndex + 1) + " of " + dataLength + ". ";
}
return msg;
}
}, {
ATTRS: {
/**
* Indicates whether axis labels are allowed to overflow beyond the bounds of the chart's content box.
*
* @attribute allowContentOverflow
* @type Boolean
*/
allowContentOverflow: {
value: false
},
/**
* Style object for the axes.
*
* @attribute axesStyles
* @type Object
* @private
*/
axesStyles: {
getter: function()
{
var axes = this.get("axes"),
i,
styles = this._axesStyles;
if(axes)
{
for(i in axes)
{
if(axes.hasOwnProperty(i) && axes[i] instanceof Y.Axis)
{
if(!styles)
{
styles = {};
}
styles[i] = axes[i].get("styles");
}
}
}
return styles;
},
setter: function(val)
{
var axes = this.get("axes"),
i;
for(i in val)
{
if(val.hasOwnProperty(i) && axes.hasOwnProperty(i))
{
this._setBaseAttribute(axes[i], "styles", val[i]);
}
}
}
},
/**
* Style object for the series
*
* @attribute seriesStyles
* @type Object
* @private
*/
seriesStyles: {
getter: function()
{
var styles = this._seriesStyles,
graph = this.get("graph"),
dict,
i;
if(graph)
{
dict = graph.get("seriesDictionary");
if(dict)
{
styles = {};
for(i in dict)
{
if(dict.hasOwnProperty(i))
{
styles[i] = dict[i].get("styles");
}
}
}
}
return styles;
},
setter: function(val)
{
var i,
l,
s;
if(Y_Lang.isArray(val))
{
s = this.get("seriesCollection");
i = 0;
l = val.length;
for(; i < l; ++i)
{
this._setBaseAttribute(s[i], "styles", val[i]);
}
}
else
{
for(i in val)
{
if(val.hasOwnProperty(i))
{
s = this.getSeries(i);
this._setBaseAttribute(s, "styles", val[i]);
}
}
}
}
},
/**
* Styles for the graph.
*
* @attribute graphStyles
* @type Object
* @private
*/
graphStyles: {
getter: function()
{
var graph = this.get("graph");
if(graph)
{
return(graph.get("styles"));
}
return this._graphStyles;
},
setter: function(val)
{
var graph = this.get("graph");
this._setBaseAttribute(graph, "styles", val);
}
},
/**
* Style properties for the chart. Contains a key indexed hash of the following:
* <dl>
* <dt>series</dt><dd>A key indexed hash containing references to the `styles` attribute for each series in the chart.
* Specific style attributes vary depending on the series:
* <ul>
* <li><a href="AreaSeries.html#attr_styles">AreaSeries</a></li>
* <li><a href="BarSeries.html#attr_styles">BarSeries</a></li>
* <li><a href="ColumnSeries.html#attr_styles">ColumnSeries</a></li>
* <li><a href="ComboSeries.html#attr_styles">ComboSeries</a></li>
* <li><a href="LineSeries.html#attr_styles">LineSeries</a></li>
* <li><a href="MarkerSeries.html#attr_styles">MarkerSeries</a></li>
* <li><a href="SplineSeries.html#attr_styles">SplineSeries</a></li>
* </ul>
* </dd>
* <dt>axes</dt><dd>A key indexed hash containing references to the `styles` attribute for each axes in the chart. Specific
* style attributes can be found in the <a href="Axis.html#attr_styles">Axis</a> class.</dd>
* <dt>graph</dt><dd>A reference to the `styles` attribute in the chart. Specific style attributes can be found in the
* <a href="Graph.html#attr_styles">Graph</a> class.</dd>
* </dl>
*
* @attribute styles
* @type Object
*/
styles: {
getter: function()
{
var styles = {
axes: this.get("axesStyles"),
series: this.get("seriesStyles"),
graph: this.get("graphStyles")
};
return styles;
},
setter: function(val)
{
if(val.hasOwnProperty("axes"))
{
if(this.get("axesStyles"))
{
this.set("axesStyles", val.axes);
}
else
{
this._axesStyles = val.axes;
}
}
if(val.hasOwnProperty("series"))
{
if(this.get("seriesStyles"))
{
this.set("seriesStyles", val.series);
}
else
{
this._seriesStyles = val.series;
}
}
if(val.hasOwnProperty("graph"))
{
this.set("graphStyles", val.graph);
}
}
},
/**
* Axes to appear in the chart. This can be a key indexed hash of axis instances or object literals
* used to construct the appropriate axes.
*
* @attribute axes
* @type Object
*/
axes: {
valueFn: "_getDefaultAxes",
setter: function(val)
{
if(this.get("dataProvider"))
{
val = this._setAxes(val);
}
return val;
}
},
/**
* Collection of series to appear on the chart. This can be an array of Series instances or object literals
* used to construct the appropriate series.
*
* @attribute seriesCollection
* @type Array
*/
seriesCollection: {
valueFn: "_getDefaultSeriesCollection",
setter: function(val)
{
if(this.get("dataProvider"))
{
val = this._parseSeriesCollection(val);
}
return val;
}
},
/**
* Reference to the left-aligned axes for the chart.
*
* @attribute leftAxesCollection
* @type Array
* @private
*/
leftAxesCollection: {},
/**
* Reference to the bottom-aligned axes for the chart.
*
* @attribute bottomAxesCollection
* @type Array
* @private
*/
bottomAxesCollection: {},
/**
* Reference to the right-aligned axes for the chart.
*
* @attribute rightAxesCollection
* @type Array
* @private
*/
rightAxesCollection: {},
/**
* Reference to the top-aligned axes for the chart.
*
* @attribute topAxesCollection
* @type Array
* @private
*/
topAxesCollection: {},
/**
* Indicates whether or not the chart is stacked.
*
* @attribute stacked
* @type Boolean
*/
stacked: {
value: false
},
/**
* Direction of chart's category axis when there is no series collection specified. Charts can
* be horizontal or vertical. When the chart type is column, the chart is horizontal.
* When the chart type is bar, the chart is vertical.
*
* @attribute direction
* @type String
*/
direction: {
getter: function()
{
var type = this.get("type");
if(type == "bar")
{
return "vertical";
}
else if(type == "column")
{
return "horizontal";
}
return this._direction;
},
setter: function(val)
{
this._direction = val;
return this._direction;
}
},
/**
* Indicates whether or not an area is filled in a combo chart.
*
* @attribute showAreaFill
* @type Boolean
*/
showAreaFill: {},
/**
* Indicates whether to display markers in a combo chart.
*
* @attribute showMarkers
* @type Boolean
*/
showMarkers:{},
/**
* Indicates whether to display lines in a combo chart.
*
* @attribute showLines
* @type Boolean
*/
showLines:{},
/**
* Indicates the key value used to identify a category axis in the `axes` hash. If
* not specified, the categoryKey attribute value will be used.
*
* @attribute categoryAxisName
* @type String
*/
categoryAxisName: {
},
/**
* Indicates the key value used to identify a the series axis when an axis not generated.
*
* @attribute valueAxisName
* @type String
*/
valueAxisName: {
value: "values"
},
/**
* Reference to the horizontalGridlines for the chart.
*
* @attribute horizontalGridlines
* @type Gridlines
*/
horizontalGridlines: {
getter: function()
{
var graph = this.get("graph");
if(graph)
{
return graph.get("horizontalGridlines");
}
return this._horizontalGridlines;
},
setter: function(val)
{
var graph = this.get("graph");
if(val && !Y_Lang.isObject(val))
{
val = {};
}
if(graph)
{
graph.set("horizontalGridlines", val);
}
else
{
this._horizontalGridlines = val;
}
}
},
/**
* Reference to the verticalGridlines for the chart.
*
* @attribute verticalGridlines
* @type Gridlines
*/
verticalGridlines: {
getter: function()
{
var graph = this.get("graph");
if(graph)
{
return graph.get("verticalGridlines");
}
return this._verticalGridlines;
},
setter: function(val)
{
var graph = this.get("graph");
if(val && !Y_Lang.isObject(val))
{
val = {};
}
if(graph)
{
graph.set("verticalGridlines", val);
}
else
{
this._verticalGridlines = val;
}
}
},
/**
* Type of chart when there is no series collection specified.
*
* @attribute type
* @type String
*/
type: {
getter: function()
{
if(this.get("stacked"))
{
return "stacked" + this._type;
}
return this._type;
},
setter: function(val)
{
if(this._type == "bar")
{
if(val != "bar")
{
this.set("direction", "horizontal");
}
}
else
{
if(val == "bar")
{
this.set("direction", "vertical");
}
}
this._type = val;
return this._type;
}
},
/**
* Reference to the category axis used by the chart.
*
* @attribute categoryAxis
* @type Axis
*/
categoryAxis:{}
}
});