/**
* The CartesianSeries class creates a chart with horizontal and vertical axes.
*
* @module charts
* @submodule charts-base
* @class CartesianSeries
* @extends Base
* @uses Renderer
* @constructor
*/
Y.CartesianSeries = Y.Base.create("cartesianSeries", Y.Base, [Y.Renderer], {
/**
* Storage for `xDisplayName` attribute.
*
* @property _xDisplayName
* @type String
* @private
*/
_xDisplayName: null,
/**
* Storage for `yDisplayName` attribute.
*
* @property _yDisplayName
* @type String
* @private
*/
_yDisplayName: null,
/**
* Th x-coordinate for the left edge of the series.
*
* @property _leftOrigin
* @type String
* @private
*/
_leftOrigin: null,
/**
* The y-coordinate for the bottom edge of the series.
*
* @property _bottomOrigin
* @type String
* @private
*/
_bottomOrigin: null,
/**
* @method render
* @private
*/
render: function()
{
this._setCanvas();
this.addListeners();
this.set("rendered", true);
this.validate();
},
/**
* Adds event listeners.
*
* @method addListeners
* @private
*/
addListeners: function()
{
var xAxis = this.get("xAxis"),
yAxis = this.get("yAxis");
if(xAxis)
{
this._xDataReadyHandle = xAxis.after("dataReady", Y.bind(this._xDataChangeHandler, this));
this._xDataUpdateHandle = xAxis.after("dataUpdate", Y.bind(this._xDataChangeHandler, this));
}
if(yAxis)
{
this._yDataReadyHandle = yAxis.after("dataReady", Y.bind(this._yDataChangeHandler, this));
this._yDataUpdateHandle = yAxis.after("dataUpdate", Y.bind(this._yDataChangeHandler, this));
}
this._xAxisChangeHandle = this.after("xAxisChange", this._xAxisChangeHandler);
this._yAxisChangeHandle = this.after("yAxisChange", this._yAxisChangeHandler);
this._stylesChangeHandle = this.after("stylesChange", function(e) {
var axesReady = this._updateAxisData();
if(axesReady)
{
this.draw();
}
});
this._widthChangeHandle = this.after("widthChange", function(e) {
var axesReady = this._updateAxisData();
if(axesReady)
{
this.draw();
}
});
this._heightChangeHandle = this.after("heightChange", function(e) {
var axesReady = this._updateAxisData();
if(axesReady)
{
this.draw();
}
});
this._visibleChangeHandle = this.after("visibleChange", this._handleVisibleChange);
},
/**
* Event handler for the xAxisChange event.
*
* @method _xAxisChangeHandler
* @param {Object} e Event object.
* @private
*/
_xAxisChangeHandler: function(e)
{
var xAxis = this.get("xAxis");
xAxis.after("dataReady", Y.bind(this._xDataChangeHandler, this));
xAxis.after("dataUpdate", Y.bind(this._xDataChangeHandler, this));
},
/**
* Event handler the yAxisChange event.
*
* @method _yAxisChangeHandler
* @param {Object} e Event object.
* @private
*/
_yAxisChangeHandler: function(e)
{
var yAxis = this.get("yAxis");
yAxis.after("dataReady", Y.bind(this._yDataChangeHandler, this));
yAxis.after("dataUpdate", Y.bind(this._yDataChangeHandler, this));
},
/**
* Constant used to generate unique id.
*
* @property GUID
* @type String
* @private
*/
GUID: "yuicartesianseries",
/**
* Event handler for xDataChange event.
*
* @method _xDataChangeHandler
* @param {Object} event Event object.
* @private
*/
_xDataChangeHandler: function(event)
{
var axesReady = this._updateAxisData();
if(axesReady)
{
this.draw();
}
},
/**
* Event handler for yDataChange event.
*
* @method _yDataChangeHandler
* @param {Object} event Event object.
* @private
*/
_yDataChangeHandler: function(event)
{
var axesReady = this._updateAxisData();
if(axesReady)
{
this.draw();
}
},
/**
* Checks to ensure that both xAxis and yAxis data are available. If so, set the `xData` and `yData` attributes
* and return `true`. Otherwise, return `false`.
*
* @method _updateAxisData
* @return Boolean
* @private
*/
_updateAxisData: function()
{
var xAxis = this.get("xAxis"),
yAxis = this.get("yAxis"),
xKey = this.get("xKey"),
yKey = this.get("yKey"),
yData,
xData;
if(!xAxis || !yAxis || !xKey || !yKey)
{
return false;
}
xData = xAxis.getDataByKey(xKey);
yData = yAxis.getDataByKey(yKey);
if(!xData || !yData)
{
return false;
}
this.set("xData", xData.concat());
this.set("yData", yData.concat());
return true;
},
/**
* Draws the series is the xAxis and yAxis data are both available.
*
* @method validate
* @private
*/
validate: function()
{
if((this.get("xData") && this.get("yData")) || this._updateAxisData())
{
this.draw();
}
else
{
this.fire("drawingComplete");
}
},
/**
* Creates a `Graphic` instance.
*
* @method _setCanvas
* @protected
*/
_setCanvas: function()
{
var graph = this.get("graph"),
graphic = graph.get("graphic");
this.set("graphic", graphic);
},
/**
* Calculates the coordinates for the series.
*
* @method setAreaData
* @protected
*/
setAreaData: function()
{
var isNumber = Y_Lang.isNumber,
nextX, nextY,
graph = this.get("graph"),
w = graph.get("width"),
h = graph.get("height"),
xAxis = this.get("xAxis"),
yAxis = this.get("yAxis"),
xData = this.get("xData").concat(),
yData = this.get("yData").concat(),
xValue,
yValue,
xOffset = xAxis.getEdgeOffset(xData.length, w),
yOffset = yAxis.getEdgeOffset(yData.length, h),
padding = this.get("styles").padding,
leftPadding = padding.left,
topPadding = padding.top,
dataWidth = w - (leftPadding + padding.right + xOffset),
dataHeight = h - (topPadding + padding.bottom + yOffset),
xcoords = [],
ycoords = [],
xMax = xAxis.get("maximum"),
xMin = xAxis.get("minimum"),
yMax = yAxis.get("maximum"),
yMin = yAxis.get("minimum"),
xScaleFactor = dataWidth / (xMax - xMin),
yScaleFactor = dataHeight / (yMax - yMin),
dataLength,
direction = this.get("direction"),
i = 0,
xMarkerPlane = [],
yMarkerPlane = [],
xMarkerPlaneOffset = this.get("xMarkerPlaneOffset"),
yMarkerPlaneOffset = this.get("yMarkerPlaneOffset"),
graphic = this.get("graphic");
graphic.set("width", w);
graphic.set("height", h);
dataLength = xData.length;
xOffset *= 0.5;
yOffset *= 0.5;
//Assuming a vertical graph has a range/category for its vertical axis.
if(direction === "vertical")
{
yData = yData.reverse();
}
this._leftOrigin = Math.round(((0 - xMin) * xScaleFactor) + leftPadding + xOffset);
this._bottomOrigin = Math.round((dataHeight + topPadding + yOffset));
if(yMin < 0)
{
this._bottomOrigin = this._bottomOrigin - ((0 - yMin) * yScaleFactor);
}
for (; i < dataLength; ++i)
{
xValue = parseFloat(xData[i]);
yValue = parseFloat(yData[i]);
if(isNumber(xValue))
{
nextX = (((xValue - xMin) * xScaleFactor) + leftPadding + xOffset);
}
else
{
nextX = NaN;
}
if(isNumber(yValue))
{
nextY = ((dataHeight + topPadding + yOffset) - (yValue - yMin) * yScaleFactor);
}
else
{
nextY = NaN;
}
xcoords.push(nextX);
ycoords.push(nextY);
xMarkerPlane.push({start:nextX - xMarkerPlaneOffset, end: nextX + xMarkerPlaneOffset});
yMarkerPlane.push({start:nextY - yMarkerPlaneOffset, end: nextY + yMarkerPlaneOffset});
}
this.set("xcoords", xcoords);
this.set("ycoords", ycoords);
this.set("xMarkerPlane", xMarkerPlane);
this.set("yMarkerPlane", yMarkerPlane);
this._dataLength = dataLength;
},
/**
* Finds the first valid index of an array coordinates.
*
* @method _getFirstValidIndex
* @param {Array} coords An array of x or y coordinates.
* @return Number
* @private
*/
_getFirstValidIndex: function(coords)
{
var coord,
i = -1,
limit = coords.length;
while(!Y_Lang.isNumber(coord) && i < limit)
{
i += 1;
coord = coords[i];
}
return i;
},
/**
* Finds the last valid index of an array coordinates.
*
* @method _getLastValidIndex
* @param {Array} coords An array of x or y coordinates.
* @return Number
* @private
*/
_getLastValidIndex: function(coords)
{
var coord,
i = coords.length,
limit = -1;
while(!Y_Lang.isNumber(coord) && i > limit)
{
i -= 1;
coord = coords[i];
}
return i;
},
/**
* Draws the series.
*
* @method draw
* @protected
*/
draw: function()
{
var graph = this.get("graph"),
w = graph.get("width"),
h = graph.get("height");
if(this.get("rendered"))
{
if((isFinite(w) && isFinite(h) && w > 0 && h > 0) && ((this.get("xData") && this.get("yData")) || this._updateAxisData()))
{
if(this._drawing)
{
this._callLater = true;
return;
}
this._drawing = true;
this._callLater = false;
this.setAreaData();
if(this.get("xcoords") && this.get("ycoords"))
{
this.drawSeries();
}
this._drawing = false;
if(this._callLater)
{
this.draw();
}
else
{
this._toggleVisible(this.get("visible"));
this.fire("drawingComplete");
}
}
}
},
/**
* Default value for plane offsets when the parent chart's `interactiveType` is `planar`.
*
* @property _defaultPlaneOffset
* @type Number
* @private
*/
_defaultPlaneOffset: 4,
/**
* Gets the default value for the `styles` attribute. Overrides
* base implementation.
*
* @method _getDefaultStyles
* @return Object
* @protected
*/
_getDefaultStyles: function()
{
return {padding:{
top: 0,
left: 0,
right: 0,
bottom: 0
}};
},
/**
* Collection of default colors used for lines in a series when not specified by user.
*
* @property _defaultLineColors
* @type Array
* @protected
*/
_defaultLineColors:["#426ab3", "#d09b2c", "#000000", "#b82837", "#b384b5", "#ff7200", "#779de3", "#cbc8ba", "#7ed7a6", "#007a6c"],
/**
* Collection of default colors used for marker fills in a series when not specified by user.
*
* @property _defaultFillColors
* @type Array
* @protected
*/
_defaultFillColors:["#6084d0", "#eeb647", "#6c6b5f", "#d6484f", "#ce9ed1", "#ff9f3b", "#93b7ff", "#e0ddd0", "#94ecba", "#309687"],
/**
* Collection of default colors used for marker borders in a series when not specified by user.
*
* @property _defaultBorderColors
* @type Array
* @protected
*/
_defaultBorderColors:["#205096", "#b38206", "#000000", "#94001e", "#9d6fa0", "#e55b00", "#5e85c9", "#adab9e", "#6ac291", "#006457"],
/**
* Collection of default colors used for area fills, histogram fills and pie fills in a series when not specified by user.
*
* @property _defaultSliceColors
* @type Array
* @protected
*/
_defaultSliceColors: ["#66007f", "#a86f41", "#295454", "#996ab2", "#e8cdb7", "#90bdbd","#000000","#c3b8ca", "#968373", "#678585"],
/**
* Parses a color based on a series order and type.
*
* @method _getDefaultColor
* @param {Number} index Index indicating the series order.
* @param {String} type Indicates which type of object needs the color.
* @return String
* @protected
*/
_getDefaultColor: function(index, type)
{
var colors = {
line: this._defaultLineColors,
fill: this._defaultFillColors,
border: this._defaultBorderColors,
slice: this._defaultSliceColors
},
col = colors[type],
l = col.length;
index = index || 0;
if(index >= l)
{
index = index % l;
}
type = type || "fill";
return colors[type][index];
},
/**
* Shows/hides contents of the series.
*
* @method _handleVisibleChange
* @param {Object} e Event object.
* @protected
*/
_handleVisibleChange: function(e)
{
this._toggleVisible(this.get("visible"));
},
/**
* Returns the sum of all values for the series.
*
* @method getTotalValues
* @return Number
*/
getTotalValues: function()
{
var total = this.get("valueAxis").getTotalByKey(this.get("valueKey"));
return total;
},
/**
* Destructor implementation for the CartesianSeries class. Calls destroy on all Graphic instances.
*
* @method destructor
* @protected
*/
destructor: function()
{
var marker,
markers = this.get("markers");
if(this.get("rendered"))
{
if(this._xDataReadyHandle)
{
this._xDataReadyHandle.detach();
}
if(this._xDataUpdateHandle)
{
this._xDataUpdateHandle.detach();
}
if(this._yDataReadyHandle)
{
this._yDataReadyHandle.detach();
}
if(this._yDataUpdateHandle)
{
this._yDataUpdateHandle.detach();
}
this._xAxisChangeHandle.detach();
this._yAxisChangeHandle.detach();
this._stylesChangeHandle.detach();
this._widthChangeHandle.detach();
this._heightChangeHandle.detach();
this._visibleChangeHandle.detach();
}
while(markers && markers.length > 0)
{
marker = markers.shift();
if(marker && marker instanceof Y.Shape)
{
marker.destroy();
}
}
if(this._path)
{
this._path.destroy();
this._path = null;
}
if(this._lineGraphic)
{
this._lineGraphic.destroy();
this._lineGraphic = null;
}
if(this._groupMarker)
{
this._groupMarker.destroy();
this._groupMarker = null;
}
}
/**
* Event handle for the x-axis' dataReady event.
*
* @property _xDataReadyHandle
* @type {EventHandle}
* @private
*/
/**
* Event handle for the x-axis dataUpdate event.
*
* @property _xDataUpdateHandle
* @type {EventHandle}
* @private
*/
/**
* Event handle for the y-axis dataReady event.
*
* @property _yDataReadyHandle
* @type {EventHandle}
* @private
*/
/**
* Event handle for the y-axis dataUpdate event.
* @property _yDataUpdateHandle
* @type {EventHandle}
* @private
*/
/**
* Event handle for the xAxisChange event.
* @property _xAxisChangeHandle
* @type {EventHandle}
* @private
*/
/**
* Event handle for the yAxisChange event.
* @property _yAxisChangeHandle
* @type {EventHandle}
* @private
*/
/**
* Event handle for the stylesChange event.
* @property _stylesChangeHandle
* @type {EventHandle}
* @private
*/
/**
* Event handle for the widthChange event.
* @property _widthChangeHandle
* @type {EventHandle}
* @private
*/
/**
* Event handle for the heightChange event.
* @property _heightChangeHandle
* @type {EventHandle}
* @private
*/
/**
* Event handle for the visibleChange event.
* @property _visibleChangeHandle
* @type {EventHandle}
* @private
*/
}, {
ATTRS: {
/**
* Name used for for displaying data related to the x-coordinate.
*
* @attribute xDisplayName
* @type String
*/
xDisplayName: {
getter: function()
{
return this._xDisplayName || this.get("xKey");
},
setter: function(val)
{
this._xDisplayName = val.toString();
return val;
}
},
/**
* Name used for for displaying data related to the y-coordinate.
*
* @attribute yDisplayName
* @type String
*/
yDisplayName: {
getter: function()
{
return this._yDisplayName || this.get("yKey");
},
setter: function(val)
{
this._yDisplayName = val.toString();
return val;
}
},
/**
* Name used for for displaying category data
*
* @attribute categoryDisplayName
* @type String
* @readOnly
*/
categoryDisplayName: {
lazyAdd: false,
getter: function()
{
return this.get("direction") == "vertical" ? this.get("yDisplayName") : this.get("xDisplayName");
},
setter: function(val)
{
if(this.get("direction") == "vertical")
{
this._yDisplayName = val;
}
else
{
this._xDisplayName = val;
}
return val;
}
},
/**
* Name used for for displaying value data
*
* @attribute valueDisplayName
* @type String
* @readOnly
*/
valueDisplayName: {
lazyAdd: false,
getter: function()
{
return this.get("direction") == "vertical" ? this.get("xDisplayName") : this.get("yDisplayName");
},
setter: function(val)
{
if(this.get("direction") == "vertical")
{
this._xDisplayName = val;
}
else
{
this._yDisplayName = val;
}
return val;
}
},
/**
* Read-only attribute indicating the type of series.
*
* @attribute type
* @type String
* @default cartesian
*/
type: {
value: "cartesian"
},
/**
* Order of this instance of this `type`.
*
* @attribute order
* @type Number
*/
order: {},
/**
* Order of the instance
*
* @attribute graphOrder
* @type Number
*/
graphOrder: {},
/**
* x coordinates for the series.
*
* @attribute xcoords
* @type Array
*/
xcoords: {},
/**
* y coordinates for the series
*
* @attribute ycoords
* @type Array
*/
ycoords: {},
/**
* Reference to the `Chart` application.
*
* @attribute chart
* @type ChartBase
* @readOnly
*/
chart: {
readOnly: true,
getter: function()
{
return this.get("graph").get("chart");
}
},
/**
* Reference to the `Graph` in which the series is drawn into.
*
* @attribute graph
* @type Graph
*/
graph: {},
/**
* Reference to the `Axis` instance used for assigning
* x-values to the graph.
*
* @attribute xAxis
* @type Axis
*/
xAxis: {},
/**
* Reference to the `Axis` instance used for assigning
* y-values to the graph.
*
* @attribute yAxis
* @type Axis
*/
yAxis: {},
/**
* Indicates which array to from the hash of value arrays in
* the x-axis `Axis` instance.
*
* @attribute xKey
* @type String
*/
xKey: {
setter: function(val)
{
return val.toString();
}
},
/**
* Indicates which array to from the hash of value arrays in
* the y-axis `Axis` instance.
*
* @attribute yKey
* @type String
*/
yKey: {
setter: function(val)
{
return val.toString();
}
},
/**
* Array of x values for the series.
*
* @attribute xData
* @type Array
*/
xData: {},
/**
* Array of y values for the series.
*
* @attribute yData
* @type Array
*/
yData: {},
/**
* Indicates whether the Series has been through its initial set up.
*
* @attribute rendered
* @type Boolean
*/
rendered: {
value: false
},
/*
* Returns the width of the parent graph
*
* @attribute width
* @type Number
*/
width: {
readOnly: true,
getter: function()
{
this.get("graph").get("width");
}
},
/**
* Returns the height of the parent graph
*
* @attribute height
* @type Number
*/
height: {
readOnly: true,
getter: function()
{
this.get("graph").get("height");
}
},
/**
* Indicates whether to show the series
*
* @attribute visible
* @type Boolean
* @default true
*/
visible: {
value: true
},
/**
* Collection of area maps along the xAxis. Used to determine mouseover for multiple
* series.
*
* @attribute xMarkerPlane
* @type Array
*/
xMarkerPlane: {},
/**
* Collection of area maps along the yAxis. Used to determine mouseover for multiple
* series.
*
* @attribute yMarkerPlane
* @type Array
*/
yMarkerPlane: {},
/**
* Distance from a data coordinate to the left/right for setting a hotspot.
*
* @attribute xMarkerPlaneOffset
* @type Number
*/
xMarkerPlaneOffset: {
getter: function() {
var marker = this.get("styles").marker;
if(marker && marker.width && isFinite(marker.width))
{
return marker.width * 0.5;
}
return this._defaultPlaneOffset;
}
},
/**
* Distance from a data coordinate to the top/bottom for setting a hotspot.
*
* @attribute yMarkerPlaneOffset
* @type Number
*/
yMarkerPlaneOffset: {
getter: function() {
var marker = this.get("styles").marker;
if(marker && marker.height && isFinite(marker.height))
{
return marker.height * 0.5;
}
return this._defaultPlaneOffset;
}
},
/**
* Direction of the series
*
* @attribute direction
* @type String
*/
direction: {
value: "horizontal"
},
/**
* Indicates whether or not markers for a series will be grouped and rendered in a single complex shape instance.
*
* @attribute groupMarkers
* @type Boolean
*/
groupMarkers: {
getter: function()
{
if(this._groupMarkers === undefined)
{
return this.get("graph").get("groupMarkers");
}
else
{
return this._groupMarkers;
}
},
setter: function(val)
{
this._groupMarkers = val;
return val;
}
}
}
});