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.
 
 
 
 
 
 

5188 lines
155 KiB

/*
YUI 3.17.2 (build 9c3c78e)
Copyright 2014 Yahoo! Inc. All rights reserved.
Licensed under the BSD License.
http://yuilibrary.com/license/
*/
YUI.add('charts-base', function (Y, NAME) {
/**
* Provides functionality for creating charts.
*
* @module charts
* @submodule charts-base
*/
var CONFIG = Y.config,
WINDOW = CONFIG.win,
DOCUMENT = CONFIG.doc,
Y_Lang = Y.Lang,
IS_STRING = Y_Lang.isString,
_getClassName = Y.ClassNameManager.getClassName,
SERIES_MARKER = _getClassName("seriesmarker");
/**
* Gridlines draws gridlines on a Graph.
*
* @class Gridlines
* @constructor
* @extends Base
* @uses Renderer
* @param {Object} config (optional) Configuration parameters.
* @submodule charts-base
*/
Y.Gridlines = Y.Base.create("gridlines", Y.Base, [Y.Renderer], {
/**
* Reference to the `Path` element used for drawing Gridlines.
*
* @property _path
* @type Path
* @private
*/
_path: null,
/**
* Removes the Gridlines.
*
* @method remove
* @private
*/
remove: function()
{
var path = this._path;
if(path)
{
path.destroy();
}
},
/**
* Draws the gridlines
*
* @method draw
* @protected
*/
draw: function()
{
if(this.get("axis") && this.get("graph"))
{
this._drawGridlines();
}
},
/**
* Algorithm for drawing gridlines
*
* @method _drawGridlines
* @private
*/
_drawGridlines: function()
{
var path,
axis = this.get("axis"),
axisPosition = axis.get("position"),
points,
i = 0,
l,
direction = this.get("direction"),
graph = this.get("graph"),
w = graph.get("width"),
h = graph.get("height"),
line = this.get("styles").line,
color = line.color,
weight = line.weight,
alpha = line.alpha,
count = this.get("count"),
length,
lineFunction;
if(isFinite(w) && isFinite(h) && w > 0 && h > 0)
{
if(count && Y.Lang.isNumber(count))
{
points = this._getPoints(count, w, h);
}
else if(axisPosition !== "none" && axis && axis.get("tickPoints"))
{
points = axis.get("tickPoints");
}
else
{
points = this._getPoints(axis.get("styles").majorUnit.count, w, h);
}
l = points.length;
path = graph.get("gridlines");
path.set("width", w);
path.set("height", h);
path.set("stroke", {
weight: weight,
color: color,
opacity: alpha
});
if(direction === "vertical")
{
lineFunction = this._verticalLine;
length = h;
}
else
{
lineFunction = this._horizontalLine;
length = w;
}
for(i = 0; i < l; i = i + 1)
{
lineFunction(path, points[i], length);
}
path.end();
}
},
/**
* Calculates the coordinates for the gridlines based on a count.
*
* @method _getPoints
* @param {Number} count Number of gridlines
* @return Array
* @private
*/
_getPoints: function(count, w, h)
{
var i,
points = [],
multiplier,
divisor = count - 1;
for(i = 0; i < count; i = i + 1)
{
multiplier = i/divisor;
points[i] = {
x: w * multiplier,
y: h * multiplier
};
}
return points;
},
/**
* Algorithm for horizontal lines.
*
* @method _horizontalLine
* @param {Path} path Reference to path element
* @param {Object} pt Coordinates corresponding to a major unit of an axis.
* @param {Number} w Width of the Graph
* @private
*/
_horizontalLine: function(path, pt, w)
{
path.moveTo(0, pt.y);
path.lineTo(w, pt.y);
},
/**
* Algorithm for vertical lines.
*
* @method _verticalLine
* @param {Path} path Reference to path element
* @param {Object} pt Coordinates corresponding to a major unit of an axis.
* @param {Number} h Height of the Graph
* @private
*/
_verticalLine: function(path, pt, h)
{
path.moveTo(pt.x, 0);
path.lineTo(pt.x, h);
},
/**
* Gets the default value for the `styles` attribute. Overrides
* base implementation.
*
* @method _getDefaultStyles
* @return Object
* @protected
*/
_getDefaultStyles: function()
{
var defs = {
line: {
color:"#f0efe9",
weight: 1,
alpha: 1
}
};
return defs;
}
},
{
ATTRS: {
/**
* Indicates the direction of the gridline.
*
* @attribute direction
* @type String
*/
direction: {},
/**
* Indicate the `Axis` in which to bind
* the gridlines.
*
* @attribute axis
* @type Axis
*/
axis: {},
/**
* Indicates the `Graph` in which the gridlines
* are drawn.
*
* @attribute graph
* @type Graph
*/
graph: {},
/**
* Indicates the number of gridlines to display. If no value is set, gridlines will equal the number of ticks in
* the corresponding axis.
*
* @attribute count
* @type Number
*/
count: {}
}
});
/**
* Graph manages and contains series instances for a `CartesianChart`
* instance.
*
* @class Graph
* @constructor
* @extends Widget
* @uses Renderer
* @submodule charts-base
*/
Y.Graph = Y.Base.create("graph", Y.Widget, [Y.Renderer], {
/**
* @method bindUI
* @private
*/
bindUI: function()
{
var bb = this.get("boundingBox");
bb.setStyle("position", "absolute");
this.after("widthChange", this._sizeChangeHandler);
this.after("heightChange", this._sizeChangeHandler);
this.after("stylesChange", this._updateStyles);
this.after("groupMarkersChange", this._drawSeries);
},
/**
* @method syncUI
* @private
*/
syncUI: function()
{
var background,
cb,
bg,
sc = this.get("seriesCollection"),
series,
i = 0,
len = sc ? sc.length : 0,
hgl = this.get("horizontalGridlines"),
vgl = this.get("verticalGridlines");
if(this.get("showBackground"))
{
background = this.get("background");
cb = this.get("contentBox");
bg = this.get("styles").background;
bg.stroke = bg.border;
bg.stroke.opacity = bg.stroke.alpha;
bg.fill.opacity = bg.fill.alpha;
bg.width = this.get("width");
bg.height = this.get("height");
bg.type = bg.shape;
background.set(bg);
}
for(; i < len; ++i)
{
series = sc[i];
if(series instanceof Y.SeriesBase)
{
series.render();
}
}
if(hgl && hgl instanceof Y.Gridlines)
{
hgl.draw();
}
if(vgl && vgl instanceof Y.Gridlines)
{
vgl.draw();
}
},
/**
* Object of arrays containing series mapped to a series type.
*
* @property seriesTypes
* @type Object
* @private
*/
seriesTypes: null,
/**
* Returns a series instance based on an index.
*
* @method getSeriesByIndex
* @param {Number} val index of the series
* @return CartesianSeries
*/
getSeriesByIndex: function(val)
{
var col = this.get("seriesCollection"),
series;
if(col && col.length > val)
{
series = col[val];
}
return series;
},
/**
* Returns a series instance based on a key value.
*
* @method getSeriesByKey
* @param {String} val key value of the series
* @return CartesianSeries
*/
getSeriesByKey: function(val)
{
var obj = this._seriesDictionary,
series;
if(obj && obj.hasOwnProperty(val))
{
series = obj[val];
}
return series;
},
/**
* Adds dispatcher to a `_dispatcher` used to
* to ensure all series have redrawn before for firing event.
*
* @method addDispatcher
* @param {CartesianSeries} val series instance to add
* @protected
*/
addDispatcher: function(val)
{
if(!this._dispatchers)
{
this._dispatchers = [];
}
this._dispatchers.push(val);
},
/**
* Collection of series to be displayed in the graph.
*
* @property _seriesCollection
* @type Array
* @private
*/
_seriesCollection: null,
/**
* Object containing key value pairs of `CartesianSeries` instances.
*
* @property _seriesDictionary
* @type Object
* @private
*/
_seriesDictionary: null,
/**
* Parses series instances to be displayed in the graph.
*
* @method _parseSeriesCollection
* @param {Array} Collection of `CartesianSeries` instances or objects container `CartesianSeries` attributes values.
* @private
*/
_parseSeriesCollection: function(val)
{
if(!val)
{
return;
}
var len = val.length,
i = 0,
series,
seriesKey;
this._seriesCollection = [];
this._seriesDictionary = {};
this.seriesTypes = [];
for(; i < len; ++i)
{
series = val[i];
if(!(series instanceof Y.CartesianSeries) && !(series instanceof Y.PieSeries))
{
this._createSeries(series);
continue;
}
this._addSeries(series);
}
len = this._seriesCollection.length;
for(i = 0; i < len; ++i)
{
series = this.get("seriesCollection")[i];
seriesKey = series.get("direction") === "horizontal" ? "yKey" : "xKey";
this._seriesDictionary[series.get(seriesKey)] = series;
}
},
/**
* Adds a series to the graph.
*
* @method _addSeries
* @param {CartesianSeries} series Series to add to the graph.
* @private
*/
_addSeries: function(series)
{
var type = series.get("type"),
seriesCollection = this.get("seriesCollection"),
graphSeriesLength = seriesCollection.length,
seriesTypes = this.seriesTypes,
typeSeriesCollection;
if(!series.get("graph"))
{
series.set("graph", this);
}
seriesCollection.push(series);
if(!seriesTypes.hasOwnProperty(type))
{
this.seriesTypes[type] = [];
}
typeSeriesCollection = this.seriesTypes[type];
series.set("graphOrder", graphSeriesLength);
series.set("order", typeSeriesCollection.length);
typeSeriesCollection.push(series);
series.set("seriesTypeCollection", typeSeriesCollection);
this.addDispatcher(series);
series.after("drawingComplete", Y.bind(this._drawingCompleteHandler, this));
this.fire("seriesAdded", series);
},
/**
* Creates a `CartesianSeries` instance from an object containing attribute key value pairs. The key value pairs include
* attributes for the specific series and a type value which defines the type of series to be used.
*
* @method createSeries
* @param {Object} seriesData Series attribute key value pairs.
* @private
*/
_createSeries: function(seriesData)
{
var type = seriesData.type,
seriesCollection = this.get("seriesCollection"),
seriesTypes = this.seriesTypes,
typeSeriesCollection,
SeriesClass,
series;
seriesData.graph = this;
if(!seriesTypes.hasOwnProperty(type))
{
seriesTypes[type] = [];
}
typeSeriesCollection = seriesTypes[type];
seriesData.graph = this;
seriesData.order = typeSeriesCollection.length;
seriesData.graphOrder = seriesCollection.length;
SeriesClass = this._getSeries(seriesData.type);
series = new SeriesClass(seriesData);
this.addDispatcher(series);
series.after("drawingComplete", Y.bind(this._drawingCompleteHandler, this));
typeSeriesCollection.push(series);
seriesCollection.push(series);
series.set("seriesTypeCollection", typeSeriesCollection);
if(this.get("rendered"))
{
series.render();
}
},
/**
* String reference for pre-defined `Series` classes.
*
* @property _seriesMap
* @type Object
* @private
*/
_seriesMap: {
line : Y.LineSeries,
column : Y.ColumnSeries,
bar : Y.BarSeries,
area : Y.AreaSeries,
candlestick : Y.CandlestickSeries,
ohlc : Y.OHLCSeries,
stackedarea : Y.StackedAreaSeries,
stackedline : Y.StackedLineSeries,
stackedcolumn : Y.StackedColumnSeries,
stackedbar : Y.StackedBarSeries,
markerseries : Y.MarkerSeries,
spline : Y.SplineSeries,
areaspline : Y.AreaSplineSeries,
stackedspline : Y.StackedSplineSeries,
stackedareaspline : Y.StackedAreaSplineSeries,
stackedmarkerseries : Y.StackedMarkerSeries,
pie : Y.PieSeries,
combo : Y.ComboSeries,
stackedcombo : Y.StackedComboSeries,
combospline : Y.ComboSplineSeries,
stackedcombospline : Y.StackedComboSplineSeries
},
/**
* Returns a specific `CartesianSeries` class based on key value from a look up table of a direct reference to a
* class. When specifying a key value, the following options are available:
*
* <table>
* <tr><th>Key Value</th><th>Class</th></tr>
* <tr><td>line</td><td>Y.LineSeries</td></tr>
* <tr><td>column</td><td>Y.ColumnSeries</td></tr>
* <tr><td>bar</td><td>Y.BarSeries</td></tr>
* <tr><td>area</td><td>Y.AreaSeries</td></tr>
* <tr><td>stackedarea</td><td>Y.StackedAreaSeries</td></tr>
* <tr><td>stackedline</td><td>Y.StackedLineSeries</td></tr>
* <tr><td>stackedcolumn</td><td>Y.StackedColumnSeries</td></tr>
* <tr><td>stackedbar</td><td>Y.StackedBarSeries</td></tr>
* <tr><td>markerseries</td><td>Y.MarkerSeries</td></tr>
* <tr><td>spline</td><td>Y.SplineSeries</td></tr>
* <tr><td>areaspline</td><td>Y.AreaSplineSeries</td></tr>
* <tr><td>stackedspline</td><td>Y.StackedSplineSeries</td></tr>
* <tr><td>stackedareaspline</td><td>Y.StackedAreaSplineSeries</td></tr>
* <tr><td>stackedmarkerseries</td><td>Y.StackedMarkerSeries</td></tr>
* <tr><td>pie</td><td>Y.PieSeries</td></tr>
* <tr><td>combo</td><td>Y.ComboSeries</td></tr>
* <tr><td>stackedcombo</td><td>Y.StackedComboSeries</td></tr>
* <tr><td>combospline</td><td>Y.ComboSplineSeries</td></tr>
* <tr><td>stackedcombospline</td><td>Y.StackedComboSplineSeries</td></tr>
* </table>
*
* When referencing a class directly, you can specify any of the above classes or any custom class that extends
* `CartesianSeries` or `PieSeries`.
*
* @method _getSeries
* @param {String | Object} type Series type.
* @return CartesianSeries
* @private
*/
_getSeries: function(type)
{
var seriesClass;
if(Y_Lang.isString(type))
{
seriesClass = this._seriesMap[type];
}
else
{
seriesClass = type;
}
return seriesClass;
},
/**
* Event handler for marker events.
*
* @method _markerEventHandler
* @param {Object} e Event object.
* @private
*/
_markerEventHandler: function(e)
{
var type = e.type,
markerNode = e.currentTarget,
strArr = markerNode.getAttribute("id").split("_"),
series = this.getSeriesByIndex(strArr[1]),
index = strArr[2];
series.updateMarkerState(type, index);
},
/**
* Collection of `CartesianSeries` instances to be redrawn.
*
* @property _dispatchers
* @type Array
* @private
*/
_dispatchers: null,
/**
* Updates the `Graph` styles.
*
* @method _updateStyles
* @private
*/
_updateStyles: function()
{
var styles = this.get("styles").background,
border = styles.border;
border.opacity = border.alpha;
styles.stroke = border;
styles.fill.opacity = styles.fill.alpha;
this.get("background").set(styles);
this._sizeChangeHandler();
},
/**
* Event handler for size changes.
*
* @method _sizeChangeHandler
* @param {Object} e Event object.
* @private
*/
_sizeChangeHandler: function()
{
var hgl = this.get("horizontalGridlines"),
vgl = this.get("verticalGridlines"),
w = this.get("width"),
h = this.get("height"),
bg = this.get("styles").background,
weight,
background;
if(bg && bg.border)
{
weight = bg.border.weight || 0;
}
if(this.get("showBackground"))
{
background = this.get("background");
if(w && h)
{
background.set("width", w);
background.set("height", h);
}
}
if(this._gridlines)
{
this._gridlines.clear();
}
if(hgl && hgl instanceof Y.Gridlines)
{
hgl.draw();
}
if(vgl && vgl instanceof Y.Gridlines)
{
vgl.draw();
}
this._drawSeries();
},
/**
* Draws each series.
*
* @method _drawSeries
* @private
*/
_drawSeries: function()
{
if(this._drawing)
{
this._callLater = true;
return;
}
var sc,
i,
len,
graphic = this.get("graphic");
graphic.set("autoDraw", false);
graphic.set("width", this.get("width"));
graphic.set("height", this.get("height"));
this._callLater = false;
this._drawing = true;
sc = this.get("seriesCollection");
i = 0;
len = sc ? sc.length : 0;
for(; i < len; ++i)
{
sc[i].draw();
if((!sc[i].get("xcoords") || !sc[i].get("ycoords")) && !sc[i] instanceof Y.PieSeries)
{
this._callLater = true;
break;
}
}
this._drawing = false;
if(this._callLater)
{
this._drawSeries();
}
},
/**
* Event handler for series drawingComplete event.
*
* @method _drawingCompleteHandler
* @param {Object} e Event object.
* @private
*/
_drawingCompleteHandler: function(e)
{
var series = e.currentTarget,
graphic,
index = Y.Array.indexOf(this._dispatchers, series);
if(index > -1)
{
this._dispatchers.splice(index, 1);
}
if(this._dispatchers.length < 1)
{
graphic = this.get("graphic");
if(!graphic.get("autoDraw"))
{
graphic._redraw();
}
this.fire("chartRendered");
}
},
/**
* Gets the default value for the `styles` attribute. Overrides
* base implementation.
*
* @method _getDefaultStyles
* @return Object
* @protected
*/
_getDefaultStyles: function()
{
var defs = {
background: {
shape: "rect",
fill:{
color:"#faf9f2"
},
border: {
color:"#dad8c9",
weight: 1
}
}
};
return defs;
},
/**
* Destructor implementation Graph class. Removes all Graphic instances from the widget.
*
* @method destructor
* @protected
*/
destructor: function()
{
if(this._graphic)
{
this._graphic.destroy();
this._graphic = null;
}
if(this._background)
{
this._background.get("graphic").destroy();
this._background = null;
}
if(this._gridlines)
{
this._gridlines.get("graphic").destroy();
this._gridlines = null;
}
}
}, {
ATTRS: {
/**
* The x-coordinate for the graph.
*
* @attribute x
* @type Number
* @protected
*/
x: {
setter: function(val)
{
this.get("boundingBox").setStyle("left", val + "px");
return val;
}
},
/**
* The y-coordinate for the graph.
*
* @attribute y
* @type Number
* @protected
*/
y: {
setter: function(val)
{
this.get("boundingBox").setStyle("top", val + "px");
return val;
}
},
/**
* Reference to the chart instance using the graph.
*
* @attribute chart
* @type ChartBase
* @readOnly
*/
chart: {
getter: function() {
var chart = this._state.chart || this;
return chart;
}
},
/**
* Collection of series. When setting the `seriesCollection` the array can contain a combination of either
* `CartesianSeries` instances or object literals with properties that will define a series.
*
* @attribute seriesCollection
* @type CartesianSeries
*/
seriesCollection: {
getter: function()
{
return this._seriesCollection;
},
setter: function(val)
{
this._parseSeriesCollection(val);
return this._seriesCollection;
}
},
/**
* Indicates whether the `Graph` has a background.
*
* @attribute showBackground
* @type Boolean
* @default true
*/
showBackground: {
value: true
},
/**
* Read-only hash lookup for all series on in the `Graph`.
*
* @attribute seriesDictionary
* @type Object
* @readOnly
*/
seriesDictionary: {
readOnly: true,
getter: function()
{
return this._seriesDictionary;
}
},
/**
* Reference to the horizontal `Gridlines` instance.
*
* @attribute horizontalGridlines
* @type Gridlines
* @default null
*/
horizontalGridlines: {
value: null,
setter: function(val)
{
var cfg,
key,
gl = this.get("horizontalGridlines");
if(gl && gl instanceof Y.Gridlines)
{
gl.remove();
}
if(val instanceof Y.Gridlines)
{
gl = val;
val.set("graph", this);
return val;
}
else if(val)
{
cfg = {
direction: "horizonal",
graph: this
};
for(key in val)
{
if(val.hasOwnProperty(key))
{
cfg[key] = val[key];
}
}
gl = new Y.Gridlines(cfg);
return gl;
}
}
},
/**
* Reference to the vertical `Gridlines` instance.
*
* @attribute verticalGridlines
* @type Gridlines
* @default null
*/
verticalGridlines: {
value: null,
setter: function(val)
{
var cfg,
key,
gl = this.get("verticalGridlines");
if(gl && gl instanceof Y.Gridlines)
{
gl.remove();
}
if(val instanceof Y.Gridlines)
{
gl = val;
val.set("graph", this);
return val;
}
else if(val)
{
cfg = {
direction: "vertical",
graph: this
};
for(key in val)
{
if(val.hasOwnProperty(key))
{
cfg[key] = val[key];
}
}
gl = new Y.Gridlines(cfg);
return gl;
}
}
},
/**
* Reference to graphic instance used for the background.
*
* @attribute background
* @type Graphic
* @readOnly
*/
background: {
getter: function()
{
if(!this._background)
{
this._backgroundGraphic = new Y.Graphic({render:this.get("contentBox")});
this._backgroundGraphic.get("node").style.zIndex = 0;
this._background = this._backgroundGraphic.addShape({type: "rect"});
}
return this._background;
}
},
/**
* Reference to graphic instance used for gridlines.
*
* @attribute gridlines
* @type Graphic
* @readOnly
*/
gridlines: {
readOnly: true,
getter: function()
{
if(!this._gridlines)
{
this._gridlinesGraphic = new Y.Graphic({render:this.get("contentBox")});
this._gridlinesGraphic.get("node").style.zIndex = 1;
this._gridlines = this._gridlinesGraphic.addShape({type: "path"});
}
return this._gridlines;
}
},
/**
* Reference to graphic instance used for series.
*
* @attribute graphic
* @type Graphic
* @readOnly
*/
graphic: {
readOnly: true,
getter: function()
{
if(!this._graphic)
{
this._graphic = new Y.Graphic({render:this.get("contentBox")});
this._graphic.get("node").style.zIndex = 2;
this._graphic.set("autoDraw", false);
}
return this._graphic;
}
},
/**
* Indicates whether or not markers for a series will be grouped and rendered in a single complex shape instance.
*
* @attribute groupMarkers
* @type Boolean
*/
groupMarkers: {
value: false
}
/**
* Style properties used for drawing a background. Below are the default values:
* <dl>
* <dt>background</dt><dd>An object containing the following values:
* <dl>
* <dt>fill</dt><dd>Defines the style properties for the fill. Contains the following values:
* <dl>
* <dt>color</dt><dd>Color of the fill. The default value is #faf9f2.</dd>
* <dt>alpha</dt><dd>Number from 0 to 1 indicating the opacity of the background fill.
* The default value is 1.</dd>
* </dl>
* </dd>
* <dt>border</dt><dd>Defines the style properties for the border. Contains the following values:
* <dl>
* <dt>color</dt><dd>Color of the border. The default value is #dad8c9.</dd>
* <dt>alpha</dt><dd>Number from 0 to 1 indicating the opacity of the background border.
* The default value is 1.</dd>
* <dt>weight</dt><dd>Number indicating the width of the border. The default value is 1.</dd>
* </dl>
* </dd>
* </dl>
* </dd>
* </dl>
*
* @attribute styles
* @type Object
*/
}
});
/**
* The ChartBase class is an abstract class used to create charts.
*
* @class ChartBase
* @constructor
* @submodule charts-base
*/
function ChartBase() {}
ChartBase.ATTRS = {
/**
* Data used to generate the chart.
*
* @attribute dataProvider
* @type Array
*/
dataProvider: {
lazyAdd: false,
valueFn: function()
{
var defDataProvider = [];
if(!this._wereSeriesKeysExplicitlySet())
{
this.set("seriesKeys", this._buildSeriesKeys(defDataProvider), {src: "internal"});
}
return defDataProvider;
},
setter: function(val)
{
var dataProvider = this._setDataValues(val);
if(!this._wereSeriesKeysExplicitlySet())
{
this.set("seriesKeys", this._buildSeriesKeys(dataProvider), {src: "internal"});
}
return dataProvider;
}
},
/**
* A collection of keys that map to the series axes. If no keys are set,
* they will be generated automatically depending on the data structure passed into
* the chart.
*
* @attribute seriesKeys
* @type Array
*/
seriesKeys: {
lazyAdd: false,
setter: function(val)
{
var opts = arguments[2];
if(!val || (opts && opts.src && opts.src === "internal"))
{
this._seriesKeysExplicitlySet = false;
}
else
{
this._seriesKeysExplicitlySet = true;
}
return val;
}
},
/**
* Sets the `aria-label` for the chart.
*
* @attribute ariaLabel
* @type String
*/
ariaLabel: {
value: "Chart Application",
setter: function(val)
{
var cb = this.get("contentBox");
if(cb)
{
cb.setAttribute("aria-label", val);
}
return val;
}
},
/**
* Sets the aria description for the chart.
*
* @attribute ariaDescription
* @type String
*/
ariaDescription: {
value: "Use the up and down keys to navigate between series. Use the left and right keys to navigate through items in a series.",
setter: function(val)
{
if(this._description)
{
this._description.set("text", val);
}
return val;
}
},
/**
* Reference to the default tooltip available for the chart.
* <p>Contains the following properties:</p>
* <dl>
* <dt>node</dt><dd>Reference to the actual dom node</dd>
* <dt>showEvent</dt><dd>Event that should trigger the tooltip</dd>
* <dt>hideEvent</dt><dd>Event that should trigger the removal of a tooltip (can be an event or an array of events)</dd>
* <dt>styles</dt><dd>A hash of style properties that will be applied to the tooltip node</dd>
* <dt>show</dt><dd>Indicates whether or not to show the tooltip</dd>
* <dt>markerEventHandler</dt><dd>Displays and hides tooltip based on marker events</dd>
* <dt>planarEventHandler</dt><dd>Displays and hides tooltip based on planar events</dd>
* <dt>markerLabelFunction</dt><dd>Reference to the function used to format a marker event triggered tooltip's text.
* The method contains the following arguments:
* <dl>
* <dt>categoryItem</dt><dd>An object containing the following:
* <dl>
* <dt>axis</dt><dd>The axis to which the category is bound.</dd>
* <dt>displayName</dt><dd>The display name set to the category (defaults to key if not provided).</dd>
* <dt>key</dt><dd>The key of the category.</dd>
* <dt>value</dt><dd>The value of the category.</dd>
* </dl>
* </dd>
* <dt>valueItem</dt><dd>An object containing the following:
* <dl>
* <dt>axis</dt><dd>The axis to which the item's series is bound.</dd>
* <dt>displayName</dt><dd>The display name of the series. (defaults to key if not provided)</dd>
* <dt>key</dt><dd>The key for the series.</dd>
* <dt>value</dt><dd>The value for the series item.</dd>
* </dl>
* </dd>
* <dt>itemIndex</dt><dd>The index of the item within the series.</dd>
* <dt>series</dt><dd> The `CartesianSeries` instance of the item.</dd>
* <dt>seriesIndex</dt><dd>The index of the series in the `seriesCollection`.</dd>
* </dl>
* The method returns an `HTMLElement` which is written into the DOM using `appendChild`. If you override this method and choose
* to return an html string, you will also need to override the tooltip's `setTextFunction` method to accept an html string.
* </dd>
* <dt>planarLabelFunction</dt><dd>Reference to the function used to format a planar event triggered tooltip's text
* <dl>
* <dt>categoryAxis</dt><dd> `CategoryAxis` Reference to the categoryAxis of the chart.
* <dt>valueItems</dt><dd>Array of objects for each series that has a data point in the coordinate plane of the event. Each
* object contains the following data:
* <dl>
* <dt>axis</dt><dd>The value axis of the series.</dd>
* <dt>key</dt><dd>The key for the series.</dd>
* <dt>value</dt><dd>The value for the series item.</dd>
* <dt>displayName</dt><dd>The display name of the series. (defaults to key if not provided)</dd>
* </dl>
* </dd>
* <dt>index</dt><dd>The index of the item within its series.</dd>
* <dt>seriesArray</dt><dd>Array of series instances for each value item.</dd>
* <dt>seriesIndex</dt><dd>The index of the series in the `seriesCollection`.</dd>
* </dl>
* </dd>
* </dl>
* The method returns an `HTMLElement` which is written into the DOM using `appendChild`. If you override this method and choose
* to return an html string, you will also need to override the tooltip's `setTextFunction` method to accept an html string.
* </dd>
* <dt>setTextFunction</dt><dd>Method that writes content returned from `planarLabelFunction` or `markerLabelFunction` into the
* the tooltip node. Has the following signature:
* <dl>
* <dt>label</dt><dd>The `HTMLElement` that the content is to be added.</dd>
* <dt>val</dt><dd>The content to be rendered into tooltip. This can be a `String` or `HTMLElement`. If an HTML string is used,
* it will be rendered as a string.</dd>
* </dl>
* </dd>
* </dl>
* @attribute tooltip
* @type Object
*/
tooltip: {
valueFn: "_getTooltip",
setter: function(val)
{
return this._updateTooltip(val);
}
},
/**
* The key value used for the chart's category axis.
*
* @attribute categoryKey
* @type String
* @default category
*/
categoryKey: {
value: "category"
},
/**
* Indicates the type of axis to use for the category axis.
*
* <dl>
* <dt>category</dt><dd>Specifies a `CategoryAxis`.</dd>
* <dt>time</dt><dd>Specifies a `TimeAxis</dd>
* </dl>
*
* @attribute categoryType
* @type String
* @default category
*/
categoryType:{
value:"category"
},
/**
* Indicates the the type of interactions that will fire events.
*
* <dl>
* <dt>marker</dt><dd>Events will be broadcasted when the mouse interacts with individual markers.</dd>
* <dt>planar</dt><dd>Events will be broadcasted when the mouse intersects the plane of any markers on the chart.</dd>
* <dt>none</dt><dd>No events will be broadcasted.</dd>
* </dl>
*
* @attribute interactionType
* @type String
* @default marker
*/
interactionType: {
value: "marker"
},
/**
* Reference to all the axes in the chart.
*
* @attribute axesCollection
* @type Array
*/
axesCollection: {},
/**
* Reference to graph instance.
*
* @attribute graph
* @type Graph
*/
graph: {
valueFn: "_getGraph"
},
/**
* Indicates whether or not markers for a series will be grouped and rendered in a single complex shape instance.
*
* @attribute groupMarkers
* @type Boolean
*/
groupMarkers: {
value: false
}
};
ChartBase.prototype = {
/**
* Utility method to determine if `seriesKeys` was explicitly provided
* (for example during construction, or set by the user), as opposed to
* being derived from the dataProvider for example.
*
* @method _wereSeriesKeysExplicitlySet
* @private
* @return boolean true if the `seriesKeys` attribute was explicitly set.
*/
_wereSeriesKeysExplicitlySet : function()
{
var seriesKeys = this.get("seriesKeys");
return seriesKeys && this._seriesKeysExplicitlySet;
},
/**
* Handles groupMarkers change event.
*
* @method _groupMarkersChangeHandler
* @param {Object} e Event object.
* @private
*/
_groupMarkersChangeHandler: function(e)
{
var graph = this.get("graph"),
useGroupMarkers = e.newVal;
if(graph)
{
graph.set("groupMarkers", useGroupMarkers);
}
},
/**
* Handler for itemRendered event.
*
* @method _itemRendered
* @param {Object} e Event object.
* @private
*/
_itemRendered: function(e)
{
this._itemRenderQueue = this._itemRenderQueue.splice(1 + Y.Array.indexOf(this._itemRenderQueue, e.currentTarget), 1);
if(this._itemRenderQueue.length < 1)
{
this._redraw();
}
},
/**
* Default value function for the `Graph` attribute.
*
* @method _getGraph
* @return Graph
* @private
*/
_getGraph: function()
{
var graph = new Y.Graph({
chart:this,
groupMarkers: this.get("groupMarkers")
});
graph.after("chartRendered", Y.bind(function() {
this.fire("chartRendered");
}, this));
return graph;
},
/**
* Returns a series instance by index or key value.
*
* @method getSeries
* @param val
* @return CartesianSeries
*/
getSeries: function(val)
{
var series = null,
graph = this.get("graph");
if(graph)
{
if(Y_Lang.isNumber(val))
{
series = graph.getSeriesByIndex(val);
}
else
{
series = graph.getSeriesByKey(val);
}
}
return series;
},
/**
* Returns an `Axis` instance by key reference. If the axis was explicitly set through the `axes` attribute,
* the key will be the same as the key used in the `axes` object. For default axes, the key for
* the category axis is the value of the `categoryKey` (`category`). For the value axis, the default
* key is `values`.
*
* @method getAxisByKey
* @param {String} val Key reference used to look up the axis.
* @return Axis
*/
getAxisByKey: function(val)
{
var axis,
axes = this.get("axes");
if(axes && axes.hasOwnProperty(val))
{
axis = axes[val];
}
return axis;
},
/**
* Returns the category axis for the chart.
*
* @method getCategoryAxis
* @return Axis
*/
getCategoryAxis: function()
{
var axis,
key = this.get("categoryKey"),
axes = this.get("axes");
if(axes.hasOwnProperty(key))
{
axis = axes[key];
}
return axis;
},
/**
* Default direction of the chart.
*
* @property _direction
* @type String
* @default horizontal
* @private
*/
_direction: "horizontal",
/**
* Storage for the `dataProvider` attribute.
*
* @property _dataProvider
* @type Array
* @private
*/
_dataProvider: null,
/**
* Setter method for `dataProvider` attribute.
*
* @method _setDataValues
* @param {Array} val Array to be set as `dataProvider`.
* @return Array
* @private
*/
_setDataValues: function(val)
{
if(Y_Lang.isArray(val[0]))
{
var hash,
dp = [],
cats = val[0],
i = 0,
l = cats.length,
n,
sl = val.length;
for(; i < l; ++i)
{
hash = {category:cats[i]};
for(n = 1; n < sl; ++n)
{
hash["series" + n] = val[n][i];
}
dp[i] = hash;
}
return dp;
}
return val;
},
/**
* Storage for `seriesCollection` attribute.
*
* @property _seriesCollection
* @type Array
* @private
*/
_seriesCollection: null,
/**
* Setter method for `seriesCollection` attribute.
*
* @property _setSeriesCollection
* @param {Array} val Array of either `CartesianSeries` instances or objects containing series attribute key value pairs.
* @private
*/
_setSeriesCollection: function(val)
{
this._seriesCollection = val;
},
/**
* Helper method that returns the axis class that a key references.
*
* @method _getAxisClass
* @param {String} t The type of axis.
* @return Axis
* @private
*/
_getAxisClass: function(t)
{
return this._axisClass[t];
},
/**
* Key value pairs of axis types.
*
* @property _axisClass
* @type Object
* @private
*/
_axisClass: {
stacked: Y.StackedAxis,
numeric: Y.NumericAxis,
category: Y.CategoryAxis,
time: Y.TimeAxis
},
/**
* Collection of axes.
*
* @property _axes
* @type Array
* @private
*/
_axes: null,
/**
* @method initializer
* @private
*/
initializer: function()
{
this._itemRenderQueue = [];
this._seriesIndex = -1;
this._itemIndex = -1;
this.after("dataProviderChange", this._dataProviderChangeHandler);
},
/**
* @method renderUI
* @private
*/
renderUI: function()
{
var tt = this.get("tooltip"),
bb = this.get("boundingBox"),
cb = this.get("contentBox");
//move the position = absolute logic to a class file
bb.setStyle("position", "absolute");
cb.setStyle("position", "absolute");
this._addAxes();
this._addSeries();
if(tt && tt.show)
{
this._addTooltip();
}
this._setAriaElements(bb, cb);
},
/**
* Creates an aria `live-region`, `aria-label` and `aria-describedby` for the Chart.
*
* @method _setAriaElements
* @param {Node} cb Reference to the Chart's `contentBox` attribute.
* @private
*/
_setAriaElements: function(bb, cb)
{
var description = this._getAriaOffscreenNode(),
id = this.get("id") + "_description",
liveRegion = this._getAriaOffscreenNode();
cb.set("tabIndex", 0);
cb.set("role", "img");
cb.setAttribute("aria-label", this.get("ariaLabel"));
cb.setAttribute("aria-describedby", id);
description.set("id", id);
description.set("tabIndex", -1);
description.set("text", this.get("ariaDescription"));
liveRegion.set("id", "live-region");
liveRegion.set("aria-live", "polite");
liveRegion.set("aria-atomic", "true");
liveRegion.set("role", "status");
bb.setAttribute("role", "application");
bb.appendChild(description);
bb.appendChild(liveRegion);
this._description = description;
this._liveRegion = liveRegion;
},
/**
* Sets a node offscreen for use as aria-description or aria-live-regin.
*
* @method _setOffscreen
* @return Node
* @private
*/
_getAriaOffscreenNode: function()
{
var node = Y.Node.create("<div></div>"),
ie = Y.UA.ie,
clipRect = (ie && ie < 8) ? "rect(1px 1px 1px 1px)" : "rect(1px, 1px, 1px, 1px)";
node.setStyle("position", "absolute");
node.setStyle("height", "1px");
node.setStyle("width", "1px");
node.setStyle("overflow", "hidden");
node.setStyle("clip", clipRect);
return node;
},
/**
* @method syncUI
* @private
*/
syncUI: function()
{
this._redraw();
},
/**
* @method bindUI
* @private
*/
bindUI: function()
{
this.after("tooltipChange", Y.bind(this._tooltipChangeHandler, this));
this.after("widthChange", this._sizeChanged);
this.after("heightChange", this._sizeChanged);
this.after("groupMarkersChange", this._groupMarkersChangeHandler);
var tt = this.get("tooltip"),
hideEvent = "mouseout",
showEvent = "mouseover",
cb = this.get("contentBox"),
interactionType = this.get("interactionType"),
i = 0,
len,
markerClassName = "." + SERIES_MARKER,
isTouch = ((WINDOW && ("ontouchstart" in WINDOW)) && !(Y.UA.chrome && Y.UA.chrome < 6));
Y.on("keydown", Y.bind(function(e) {
var key = e.keyCode,
numKey = parseFloat(key),
msg;
if(numKey > 36 && numKey < 41)
{
e.halt();
msg = this._getAriaMessage(numKey);
this._liveRegion.set("text", msg);
}
}, this), this.get("contentBox"));
if(interactionType === "marker")
{
//if touch capabilities, toggle tooltip on touchend. otherwise, the tooltip attribute's hideEvent/showEvent types.
hideEvent = tt.hideEvent;
showEvent = tt.showEvent;
if(isTouch)
{
Y.delegate("touchend", Y.bind(this._markerEventDispatcher, this), cb, markerClassName);
//hide active tooltip if the chart is touched
Y.on("touchend", Y.bind(function(e) {
//only halt the event if it originated from the chart
if(cb.contains(e.target))
{
e.halt(true);
}
if(this._activeMarker)
{
this._activeMarker = null;
this.hideTooltip(e);
}
}, this));
}
else
{
Y.delegate("mouseenter", Y.bind(this._markerEventDispatcher, this), cb, markerClassName);
Y.delegate("mousedown", Y.bind(this._markerEventDispatcher, this), cb, markerClassName);
Y.delegate("mouseup", Y.bind(this._markerEventDispatcher, this), cb, markerClassName);
Y.delegate("mouseleave", Y.bind(this._markerEventDispatcher, this), cb, markerClassName);
Y.delegate("click", Y.bind(this._markerEventDispatcher, this), cb, markerClassName);
Y.delegate("mousemove", Y.bind(this._positionTooltip, this), cb, markerClassName);
}
}
else if(interactionType === "planar")
{
if(isTouch)
{
this._overlay.on("touchend", Y.bind(this._planarEventDispatcher, this));
}
else
{
this._overlay.on("mousemove", Y.bind(this._planarEventDispatcher, this));
this.on("mouseout", this.hideTooltip);
}
}
if(tt)
{
this.on("markerEvent:touchend", Y.bind(function(e) {
var marker = e.series.get("markers")[e.index];
if(this._activeMarker && marker === this._activeMarker)
{
this._activeMarker = null;
this.hideTooltip(e);
}
else
{
this._activeMarker = marker;
tt.markerEventHandler.apply(this, [e]);
}
}, this));
if(hideEvent && showEvent && hideEvent === showEvent)
{
this.on(interactionType + "Event:" + hideEvent, this.toggleTooltip);
}
else
{
if(showEvent)
{
this.on(interactionType + "Event:" + showEvent, tt[interactionType + "EventHandler"]);
}
if(hideEvent)
{
if(Y_Lang.isArray(hideEvent))
{
len = hideEvent.length;
for(; i < len; ++i)
{
this.on(interactionType + "Event:" + hideEvent[i], this.hideTooltip);
}
}
this.on(interactionType + "Event:" + hideEvent, this.hideTooltip);
}
}
}
},
/**
* Event handler for marker events.
*
* @method _markerEventDispatcher
* @param {Object} e Event object.
* @private
*/
_markerEventDispatcher: function(e)
{
var type = e.type,
cb = this.get("contentBox"),
markerNode = e.currentTarget,
strArr = markerNode.getAttribute("id").split("_"),
index = strArr.pop(),
seriesIndex = strArr.pop(),
series = this.getSeries(parseInt(seriesIndex, 10)),
items = this.getSeriesItems(series, index),
isTouch = e && e.hasOwnProperty("changedTouches"),
pageX = isTouch ? e.changedTouches[0].pageX : e.pageX,
pageY = isTouch ? e.changedTouches[0].pageY : e.pageY,
x = pageX - cb.getX(),
y = pageY - cb.getY();
if(type === "mouseenter")
{
type = "mouseover";
}
else if(type === "mouseleave")
{
type = "mouseout";
}
series.updateMarkerState(type, index);
e.halt();
/**
* Broadcasts when `interactionType` is set to `marker` and a series marker has received a mouseover event.
*
*
* @event markerEvent:mouseover
* @preventable false
* @param {EventFacade} e Event facade with the following additional
* properties:
* <dl>
* <dt>categoryItem</dt><dd>Hash containing information about the category `Axis`.</dd>
* <dt>valueItem</dt><dd>Hash containing information about the value `Axis`.</dd>
* <dt>node</dt><dd>The dom node of the marker.</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>series</dt><dd>Reference to the series of the marker.</dd>
* <dt>index</dt><dd>Index of the marker in the series.</dd>
* <dt>seriesIndex</dt><dd>The `order` of the marker's series.</dd>
* </dl>
*/
/**
* Broadcasts when `interactionType` is set to `marker` and a series marker has received a mouseout event.
*
* @event markerEvent:mouseout
* @preventable false
* @param {EventFacade} e Event facade with the following additional
* properties:
* <dl>
* <dt>categoryItem</dt><dd>Hash containing information about the category `Axis`.</dd>
* <dt>valueItem</dt><dd>Hash containing information about the value `Axis`.</dd>
* <dt>node</dt><dd>The dom node of the marker.</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>series</dt><dd>Reference to the series of the marker.</dd>
* <dt>index</dt><dd>Index of the marker in the series.</dd>
* <dt>seriesIndex</dt><dd>The `order` of the marker's series.</dd>
* </dl>
*/
/**
* Broadcasts when `interactionType` is set to `marker` and a series marker has received a mousedown event.
*
* @event markerEvent:mousedown
* @preventable false
* @param {EventFacade} e Event facade with the following additional
* properties:
* <dl>
* <dt>categoryItem</dt><dd>Hash containing information about the category `Axis`.</dd>
* <dt>valueItem</dt><dd>Hash containing information about the value `Axis`.</dd>
* <dt>node</dt><dd>The dom node of the marker.</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>series</dt><dd>Reference to the series of the marker.</dd>
* <dt>index</dt><dd>Index of the marker in the series.</dd>
* <dt>seriesIndex</dt><dd>The `order` of the marker's series.</dd>
* </dl>
*/
/**
* Broadcasts when `interactionType` is set to `marker` and a series marker has received a mouseup event.
*
* @event markerEvent:mouseup
* @preventable false
* @param {EventFacade} e Event facade with the following additional
* properties:
* <dl>
* <dt>categoryItem</dt><dd>Hash containing information about the category `Axis`.</dd>
* <dt>valueItem</dt><dd>Hash containing information about the value `Axis`.</dd>
* <dt>node</dt><dd>The dom node of the marker.</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>series</dt><dd>Reference to the series of the marker.</dd>
* <dt>index</dt><dd>Index of the marker in the series.</dd>
* <dt>seriesIndex</dt><dd>The `order` of the marker's series.</dd>
* </dl>
*/
/**
* Broadcasts when `interactionType` is set to `marker` and a series marker has received a click event.
*
* @event markerEvent:click
* @preventable false
* @param {EventFacade} e Event facade with the following additional
* properties:
* <dl>
* <dt>categoryItem</dt><dd>Hash containing information about the category `Axis`.</dd>
* <dt>valueItem</dt><dd>Hash containing information about the value `Axis`.</dd>
* <dt>node</dt><dd>The dom node of the marker.</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>series</dt><dd>Reference to the series of the marker.</dd>
* <dt>index</dt><dd>Index of the marker in the series.</dd>
* <dt>seriesIndex</dt><dd>The `order` of the marker's series.</dd>
* <dt>originEvent</dt><dd>Underlying dom event.</dd>
* </dl>
*/
this.fire("markerEvent:" + type, {
originEvent: e,
pageX:pageX,
pageY:pageY,
categoryItem:items.category,
valueItem:items.value,
node:markerNode,
x:x,
y:y,
series:series,
index:index,
seriesIndex:seriesIndex
});
},
/**
* Event handler for dataProviderChange.
*
* @method _dataProviderChangeHandler
* @param {Object} e Event object.
* @private
*/
_dataProviderChangeHandler: function(e)
{
var dataProvider = e.newVal,
axes,
i,
axis;
this._seriesIndex = -1;
this._itemIndex = -1;
if(this instanceof Y.CartesianChart)
{
this.set("axes", this.get("axes"));
this.set("seriesCollection", this.get("seriesCollection"));
}
axes = this.get("axes");
if(axes)
{
for(i in axes)
{
if(axes.hasOwnProperty(i))
{
axis = axes[i];
if(axis instanceof Y.Axis)
{
if(axis.get("position") !== "none")
{
this._addToAxesRenderQueue(axis);
}
axis.set("dataProvider", dataProvider);
}
}
}
}
},
/**
* Event listener for toggling the tooltip. If a tooltip is visible, hide it. If not, it
* will create and show a tooltip based on the event object.
*
* @method toggleTooltip
* @param {Object} e Event object.
*/
toggleTooltip: function(e)
{
var tt = this.get("tooltip");
if(tt.visible)
{
this.hideTooltip();
}
else
{
tt.markerEventHandler.apply(this, [e]);
}
},
/**
* Shows a tooltip
*
* @method _showTooltip
* @param {String} msg Message to dispaly in the tooltip.
* @param {Number} x x-coordinate
* @param {Number} y y-coordinate
* @private
*/
_showTooltip: function(msg, x, y)
{
var tt = this.get("tooltip"),
node = tt.node;
if(msg)
{
tt.visible = true;
tt.setTextFunction(node, msg);
node.setStyle("top", y + "px");
node.setStyle("left", x + "px");
node.setStyle("visibility", "visible");
}
},
/**
* Positions the tooltip
*
* @method _positionTooltip
* @param {Object} e Event object.
* @private
*/
_positionTooltip: function(e)
{
var tt = this.get("tooltip"),
node = tt.node,
cb = this.get("contentBox"),
x = (e.pageX + 10) - cb.getX(),
y = (e.pageY + 10) - cb.getY();
if(node)
{
node.setStyle("left", x + "px");
node.setStyle("top", y + "px");
}
},
/**
* Hides the default tooltip
*
* @method hideTooltip
*/
hideTooltip: function()
{
var tt = this.get("tooltip"),
node = tt.node;
tt.visible = false;
node.set("innerHTML", "");
node.setStyle("left", -10000);
node.setStyle("top", -10000);
node.setStyle("visibility", "hidden");
},
/**
* Adds a tooltip to the dom.
*
* @method _addTooltip
* @private
*/
_addTooltip: function()
{
var tt = this.get("tooltip"),
id = this.get("id") + "_tooltip",
cb = this.get("contentBox"),
oldNode = DOCUMENT.getElementById(id);
if(oldNode)
{
cb.removeChild(oldNode);
}
tt.node.set("id", id);
tt.node.setStyle("visibility", "hidden");
cb.appendChild(tt.node);
},
/**
* Updates the tooltip attribute.
*
* @method _updateTooltip
* @param {Object} val Object containing properties for the tooltip.
* @return Object
* @private
*/
_updateTooltip: function(val)
{
var tt = this.get("tooltip") || this._getTooltip(),
i,
styles,
node,
props = {
markerLabelFunction:"markerLabelFunction",
planarLabelFunction:"planarLabelFunction",
setTextFunction:"setTextFunction",
showEvent:"showEvent",
hideEvent:"hideEvent",
markerEventHandler:"markerEventHandler",
planarEventHandler:"planarEventHandler",
show:"show"
};
if(Y_Lang.isObject(val))
{
styles = val.styles;
if(val.node && tt.node)
{
tt.node.destroy(true);
node = Y.one(val.node);
}
else
{
node = tt.node;
}
if(styles)
{
for(i in styles)
{
if(styles.hasOwnProperty(i))
{
node.setStyle(i, styles[i]);
}
}
}
for(i in props)
{
if(val.hasOwnProperty(i))
{
tt[i] = val[i];
}
}
tt.node = node;
}
return tt;
},
/**
* Default getter for `tooltip` attribute.
*
* @method _getTooltip
* @return Object
* @private
*/
_getTooltip: function()
{
var node = DOCUMENT.createElement("div"),
tooltipClass = _getClassName("chart-tooltip"),
tt = {
setTextFunction: this._setText,
markerLabelFunction: this._tooltipLabelFunction,
planarLabelFunction: this._planarLabelFunction,
show: true,
hideEvent: "mouseout",
showEvent: "mouseover",
markerEventHandler: function(e)
{
var tt = this.get("tooltip"),
msg = tt.markerLabelFunction.apply(this, [e.categoryItem, e.valueItem, e.index, e.series, e.seriesIndex]);
this._showTooltip(msg, e.x + 10, e.y + 10);
},
planarEventHandler: function(e)
{
var tt = this.get("tooltip"),
msg ,
categoryAxis = this.get("categoryAxis");
msg = tt.planarLabelFunction.apply(this, [categoryAxis, e.valueItem, e.index, e.items, e.seriesIndex]);
this._showTooltip(msg, e.x + 10, e.y + 10);
}
};
node = Y.one(node);
node.set("id", this.get("id") + "_tooltip");
node.setStyle("fontSize", "85%");
node.setStyle("opacity", "0.83");
node.setStyle("position", "absolute");
node.setStyle("paddingTop", "2px");
node.setStyle("paddingRight", "5px");
node.setStyle("paddingBottom", "4px");
node.setStyle("paddingLeft", "2px");
node.setStyle("backgroundColor", "#fff");
node.setStyle("border", "1px solid #dbdccc");
node.setStyle("pointerEvents", "none");
node.setStyle("zIndex", 3);
node.setStyle("whiteSpace", "noWrap");
node.setStyle("visibility", "hidden");
node.addClass(tooltipClass);
tt.node = Y.one(node);
return tt;
},
/**
* Formats tooltip text when `interactionType` is `planar`.
*
* @method _planarLabelFunction
* @param {Axis} categoryAxis Reference to the categoryAxis of the chart.
* @param {Array} valueItems Array of objects for each series that has a data point in the coordinate plane of the event.
* Each object contains the following data:
* <dl>
* <dt>axis</dt><dd>The value axis of the series.</dd>
* <dt>key</dt><dd>The key for the series.</dd>
* <dt>value</dt><dd>The value for the series item.</dd>
* <dt>displayName</dt><dd>The display name of the series. (defaults to key if not provided)</dd>
* </dl>
* @param {Number} index The index of the item within its series.
* @param {Array} seriesArray Array of series instances for each value item.
* @param {Number} seriesIndex The index of the series in the `seriesCollection`.
* @return {HTMLElement}
* @private
*/
_planarLabelFunction: function(categoryAxis, valueItems, index, seriesArray)
{
var msg = DOCUMENT.createElement("div"),
valueItem,
i = 0,
len = seriesArray.length,
axis,
categoryValue,
seriesValue,
series;
if(categoryAxis)
{
categoryValue = categoryAxis.get("labelFunction").apply(
this,
[categoryAxis.getKeyValueAt(this.get("categoryKey"), index), categoryAxis.get("labelFormat")]
);
if(!Y_Lang.isObject(categoryValue))
{
categoryValue = DOCUMENT.createTextNode(categoryValue);
}
msg.appendChild(categoryValue);
}
for(; i < len; ++i)
{
series = seriesArray[i];
if(series.get("visible"))
{
valueItem = valueItems[i];
axis = valueItem.axis;
seriesValue = axis.get("labelFunction").apply(
this,
[axis.getKeyValueAt(valueItem.key, index), axis.get("labelFormat")]
);
msg.appendChild(DOCUMENT.createElement("br"));
msg.appendChild(DOCUMENT.createTextNode(valueItem.displayName));
msg.appendChild(DOCUMENT.createTextNode(": "));
if(!Y_Lang.isObject(seriesValue))
{
seriesValue = DOCUMENT.createTextNode(seriesValue);
}
msg.appendChild(seriesValue);
}
}
return msg;
},
/**
* Formats tooltip text when `interactionType` is `marker`.
*
* @method _tooltipLabelFunction
* @param {Object} categoryItem An object containing the following:
* <dl>
* <dt>axis</dt><dd>The axis to which the category is bound.</dd>
* <dt>displayName</dt><dd>The display name set to the category (defaults to key if not provided)</dd>
* <dt>key</dt><dd>The key of the category.</dd>
* <dt>value</dt><dd>The value of the category</dd>
* </dl>
* @param {Object} valueItem An object containing the following:
* <dl>
* <dt>axis</dt><dd>The axis to which the item's series is bound.</dd>
* <dt>displayName</dt><dd>The display name of the series. (defaults to key if not provided)</dd>
* <dt>key</dt><dd>The key for the series.</dd>
* <dt>value</dt><dd>The value for the series item.</dd>
* </dl>
* @return {HTMLElement}
* @private
*/
_tooltipLabelFunction: function(categoryItem, valueItem)
{
var msg = DOCUMENT.createElement("div"),
categoryValue = categoryItem.axis.get("labelFunction").apply(
this,
[categoryItem.value, categoryItem.axis.get("labelFormat")]
),
seriesValue = valueItem.axis.get("labelFunction").apply(
this,
[valueItem.value, valueItem.axis.get("labelFormat")]
);
msg.appendChild(DOCUMENT.createTextNode(categoryItem.displayName));
msg.appendChild(DOCUMENT.createTextNode(": "));
if(!Y_Lang.isObject(categoryValue))
{
categoryValue = DOCUMENT.createTextNode(categoryValue);
}
msg.appendChild(categoryValue);
msg.appendChild(DOCUMENT.createElement("br"));
msg.appendChild(DOCUMENT.createTextNode(valueItem.displayName));
msg.appendChild(DOCUMENT.createTextNode(": "));
if(!Y_Lang.isObject(seriesValue))
{
seriesValue = DOCUMENT.createTextNode(seriesValue);
}
msg.appendChild(seriesValue);
return msg;
},
/**
* Event handler for the tooltipChange.
*
* @method _tooltipChangeHandler
* @param {Object} e Event object.
* @private
*/
_tooltipChangeHandler: function()
{
if(this.get("tooltip"))
{
var tt = this.get("tooltip"),
node = tt.node,
show = tt.show,
cb = this.get("contentBox");
if(node && show)
{
if(!cb.contains(node))
{
this._addTooltip();
}
}
}
},
/**
* Updates the content of text field. This method writes a value into a text field using
* `appendChild`. If the value is a `String`, it is converted to a `TextNode` first.
*
* @method _setText
* @param label {HTMLElement} label to be updated
* @param val {String} value with which to update the label
* @private
*/
_setText: function(textField, val)
{
textField.empty();
if(Y_Lang.isNumber(val))
{
val = val + "";
}
else if(!val)
{
val = "";
}
if(IS_STRING(val))
{
val = DOCUMENT.createTextNode(val);
}
textField.appendChild(val);
},
/**
* Returns all the keys contained in a `dataProvider`.
*
* @method _getAllKeys
* @param {Array} dp Collection of objects to be parsed.
* @return Object
*/
_getAllKeys: function(dp)
{
var i = 0,
len = dp.length,
item,
key,
keys = {};
for(; i < len; ++i)
{
item = dp[i];
for(key in item)
{
if(item.hasOwnProperty(key))
{
keys[key] = true;
}
}
}
return keys;
},
/**
* Constructs seriesKeys if not explicitly specified.
*
* @method _buildSeriesKeys
* @param {Array} dataProvider The dataProvider for the chart.
* @return Array
* @private
*/
_buildSeriesKeys: function(dataProvider)
{
var allKeys,
catKey = this.get("categoryKey"),
keys = [],
i;
if(this._seriesKeysExplicitlySet)
{
return this._seriesKeys;
}
allKeys = this._getAllKeys(dataProvider);
for(i in allKeys)
{
if(allKeys.hasOwnProperty(i) && i !== catKey)
{
keys.push(i);
}
}
return keys;
}
};
Y.ChartBase = ChartBase;
/**
* The CartesianChart class creates a chart with horizontal and vertical axes.
*
* @class CartesianChart
* @extends ChartBase
* @constructor
* @submodule charts-base
*/
Y.CartesianChart = Y.Base.create("cartesianChart", Y.Widget, [Y.ChartBase, Y.Renderer], {
/**
* @method renderUI
* @private
*/
renderUI: function()
{
var bb = this.get("boundingBox"),
cb = this.get("contentBox"),
tt = this.get("tooltip"),
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(this.get("interactionType") === "planar")
{
this._overlay = Y.Node.create("<div></div>");
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);
cb.append(this._overlay);
}
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"),
seriesStyles = this.get("styles").series,
stylesAreArray = seriesStyles && Y_Lang.isArray(seriesStyles),
stylesIndex,
setStyles,
globalStyles,
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 ? val.concat() : [];
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);
}
else
{
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;
}
}
if(seriesStyles)
{
stylesIndex = stylesAreArray ? i : series[seriesKey];
globalStyles = seriesStyles[stylesIndex];
if(globalStyles)
{
setStyles = series.styles;
if(setStyles)
{
series.styles = this._mergeStyles(setStyles, globalStyles);
}
else
{
series.styles = globalStyles;
}
}
}
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",
calculateEdgeOffset: "calculateEdgeOffset",
position: "position",
overlapGraph:"overlapGraph",
labelValues: "labelValues",
hideFirstMajorUnit: "hideFirstMajorUnit",
hideLastMajorUnit: "hideLastMajorUnit",
labelFunction:"labelFunction",
labelFunctionScope:"labelFunctionScope",
labelFormat:"labelFormat",
appendLabelFunction: "appendLabelFunction",
appendTitleFunction: "appendTitleFunction",
maximum:"maximum",
minimum:"minimum",
roundingMethod:"roundingMethod",
alwaysShowZero:"alwaysShowZero",
scaleType: "scaleType",
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");
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 = [],
newKeys = [],
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 = seriesKeys.length;
for(i = 0; i < l; ++i)
{
cIndex = Y.Array.indexOf(claimedKeys, seriesKeys[i]);
if(cIndex > -1)
{
newKeys = newKeys.concat(claimedKeys.splice(cIndex, 1));
}
}
claimedKeys = newKeys.concat(claimedKeys);
l = claimedKeys.length;
for(i = 0; i < l; i = i + 1)
{
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._wereSeriesKeysExplicitlySet())
{
this.set("seriesKeys", seriesKeys, {src: "internal"});
}
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()
{
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)
);
}
}
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)
);
}
}
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)
);
}
}
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)
);
}
}
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)
);
}
}
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)
);
}
}
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)
);
}
}
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)
);
}
}
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: {
lazyAdd: false,
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]);
}
}
return val;
}
},
/**
* Style object for the series
*
* @attribute seriesStyles
* @type Object
* @private
*/
seriesStyles: {
lazyAdd: false,
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]);
}
}
}
return val;
}
},
/**
* Styles for the graph.
*
* @attribute graphStyles
* @type Object
* @private
*/
graphStyles: {
lazyAdd: false,
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);
return 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: {
lazyAdd: false,
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: {
lazyAdd: false,
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: {
lazyAdd: false,
valueFn: "_getDefaultSeriesCollection",
setter: function(val)
{
if(this.get("dataProvider"))
{
return 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:{}
}
});
/**
* The PieChart class creates a pie chart
*
* @class PieChart
* @extends ChartBase
* @constructor
* @submodule charts-base
*/
Y.PieChart = Y.Base.create("pieChart", Y.Widget, [Y.ChartBase], {
/**
* Calculates and returns a `seriesCollection`.
*
* @method _getSeriesCollection
* @return Array
* @private
*/
_getSeriesCollection: function()
{
if(this._seriesCollection)
{
return this._seriesCollection;
}
var axes = this.get("axes"),
sc = [],
seriesKeys,
i = 0,
l,
type = this.get("type"),
key,
catAxis = "categoryAxis",
catKey = "categoryKey",
valAxis = "valueAxis",
seriesKey = "valueKey";
if(axes)
{
seriesKeys = axes.values.get("keyCollection");
key = axes.category.get("keyCollection")[0];
l = seriesKeys.length;
for(; i < l; ++i)
{
sc[i] = {type:type};
sc[i][catAxis] = "category";
sc[i][valAxis] = "values";
sc[i][catKey] = key;
sc[i][seriesKey] = seriesKeys[i];
}
}
this._seriesCollection = sc;
return sc;
},
/**
* Creates `Axis` instances.
*
* @method _parseAxes
* @param {Object} val Object containing `Axis` instances or objects in which to construct `Axis` instances.
* @return Object
* @private
*/
_parseAxes: function(hash)
{
if(!this._axes)
{
this._axes = {};
}
var i, pos, axis, dh, config, AxisClass,
type = this.get("type"),
w = this.get("width"),
h = this.get("height"),
node = Y.Node.one(this._parentNode);
if(!w)
{
this.set("width", node.get("offsetWidth"));
w = this.get("width");
}
if(!h)
{
this.set("height", node.get("offsetHeight"));
h = this.get("height");
}
for(i in hash)
{
if(hash.hasOwnProperty(i))
{
dh = hash[i];
pos = type === "pie" ? "none" : dh.position;
AxisClass = this._getAxisClass(dh.type);
config = {dataProvider:this.get("dataProvider")};
if(dh.hasOwnProperty("roundingUnit"))
{
config.roundingUnit = dh.roundingUnit;
}
config.keys = dh.keys;
config.width = w;
config.height = h;
config.position = pos;
config.styles = dh.styles;
axis = new AxisClass(config);
axis.on("axisRendered", Y.bind(this._itemRendered, this));
this._axes[i] = axis;
}
}
},
/**
* Adds axes to the chart.
*
* @method _addAxes
* @private
*/
_addAxes: function()
{
var axes = this.get("axes"),
i,
axis,
p;
if(!axes)
{
this.set("axes", this._getDefaultAxes());
axes = this.get("axes");
}
if(!this._axesCollection)
{
this._axesCollection = [];
}
for(i in axes)
{
if(axes.hasOwnProperty(i))
{
axis = axes[i];
p = axis.get("position");
if(!this.get(p + "AxesCollection"))
{
this.set(p + "AxesCollection", [axis]);
}
else
{
this.get(p + "AxesCollection").push(axis);
}
this._axesCollection.push(axis);
}
}
},
/**
* Renders the Graph.
*
* @method _addSeries
* @private
*/
_addSeries: function()
{
var graph = this.get("graph"),
seriesCollection = this.get("seriesCollection");
this._parseSeriesAxes(seriesCollection);
graph.set("showBackground", false);
graph.set("width", this.get("width"));
graph.set("height", this.get("height"));
graph.set("seriesCollection", seriesCollection);
this._seriesCollection = graph.get("seriesCollection");
graph.render(this.get("contentBox"));
},
/**
* Parse and sets the axes for the chart.
*
* @method _parseSeriesAxes
* @param {Array} c A collection `PieSeries` instance.
* @private
*/
_parseSeriesAxes: function(c)
{
var i = 0,
len = c.length,
s,
axes = this.get("axes"),
axis;
for(; i < len; ++i)
{
s = c[i];
if(s)
{
//If series is an actual series instance,
//replace axes attribute string ids with axes
if(s instanceof Y.PieSeries)
{
axis = s.get("categoryAxis");
if(axis && !(axis instanceof Y.Axis))
{
s.set("categoryAxis", axes[axis]);
}
axis = s.get("valueAxis");
if(axis && !(axis instanceof Y.Axis))
{
s.set("valueAxis", axes[axis]);
}
continue;
}
s.categoryAxis = axes.category;
s.valueAxis = axes.values;
if(!s.type)
{
s.type = this.get("type");
}
}
}
},
/**
* Generates and returns a key-indexed object containing `Axis` instances or objects used to create `Axis` instances.
*
* @method _getDefaultAxes
* @return Object
* @private
*/
_getDefaultAxes: function()
{
var catKey = this.get("categoryKey"),
seriesKeys = this.get("seriesKeys").concat(),
seriesAxis = "numeric";
return {
values:{
keys:seriesKeys,
type:seriesAxis
},
category:{
keys:[catKey],
type:this.get("categoryType")
}
};
},
/**
* Returns an object literal containing a categoryItem and a valueItem for a given series index.
*
* @method getSeriesItem
* @param series Reference to a series.
* @param index Index of the specified item within a series.
* @return Object
*/
getSeriesItems: function(series, index)
{
var categoryItem = {
axis: series.get("categoryAxis"),
key: series.get("categoryKey"),
displayName: series.get("categoryDisplayName")
},
valueItem = {
axis: series.get("valueAxis"),
key: series.get("valueKey"),
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()
{
this._redraw();
},
/**
* Redraws the chart instance.
*
* @method _redraw
* @private
*/
_redraw: function()
{
var graph = this.get("graph"),
w = this.get("width"),
h = this.get("height"),
dimension;
if(graph)
{
dimension = Math.min(w, h);
graph.set("width", dimension);
graph.set("height", dimension);
}
},
/**
* Formats tooltip text for a pie chart.
*
* @method _tooltipLabelFunction
* @param {Object} categoryItem An object containing the following:
* <dl>
* <dt>axis</dt><dd>The axis to which the category is bound.</dd>
* <dt>displayName</dt><dd>The display name set to the category (defaults to key if not provided)</dd>
* <dt>key</dt><dd>The key of the category.</dd>
* <dt>value</dt><dd>The value of the category</dd>
* </dl>
* @param {Object} valueItem An object containing the following:
* <dl>
* <dt>axis</dt><dd>The axis to which the item's series is bound.</dd>
* <dt>displayName</dt><dd>The display name of the series. (defaults to key if not provided)</dd>
* <dt>key</dt><dd>The key for the series.</dd>
* <dt>value</dt><dd>The value for the series item.</dd>
* </dl>
* @param {Number} itemIndex The index of the item within the series.
* @param {CartesianSeries} series The `PieSeries` instance of the item.
* @return {HTMLElement}
* @private
*/
_tooltipLabelFunction: function(categoryItem, valueItem, itemIndex, series)
{
var msg = DOCUMENT.createElement("div"),
total = series.getTotalValues(),
pct = Math.round((valueItem.value / total) * 10000)/100;
msg.appendChild(DOCUMENT.createTextNode(categoryItem.displayName +
": " + categoryItem.axis.get("labelFunction").apply(this, [categoryItem.value, categoryItem.axis.get("labelFormat")])));
msg.appendChild(DOCUMENT.createElement("br"));
msg.appendChild(DOCUMENT.createTextNode(valueItem.displayName +
": " + valueItem.axis.get("labelFunction").apply(this, [valueItem.value, valueItem.axis.get("labelFormat")])));
msg.appendChild(DOCUMENT.createElement("br"));
msg.appendChild(DOCUMENT.createTextNode(pct + "%"));
return msg;
},
/**
* 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 = "",
categoryItem,
items,
series,
valueItem,
seriesIndex = 0,
itemIndex = this._itemIndex,
len,
total,
pct,
markers;
series = this.getSeries(parseInt(seriesIndex, 10));
markers = series.get("markers");
len = markers && markers.length ? markers.length : 0;
if(key === 37)
{
itemIndex = itemIndex > 0 ? itemIndex - 1 : len - 1;
}
else if(key === 39)
{
itemIndex = itemIndex >= len - 1 ? 0 : itemIndex + 1;
}
this._itemIndex = itemIndex;
items = this.getSeriesItems(series, itemIndex);
categoryItem = items.category;
valueItem = items.value;
total = series.getTotalValues();
pct = Math.round((valueItem.value / total) * 10000)/100;
if(categoryItem && valueItem)
{
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")]) +
", ";
msg += "Percent of total " + valueItem.displayName + ": " + pct + "%,";
}
else
{
msg += "No data available,";
}
msg += (itemIndex + 1) + " of " + len + ". ";
return msg;
},
/**
* Destructor implementation for the PieChart class.
*
* @method destructor
* @protected
*/
destructor: function()
{
var series,
axis,
tooltip = this.get("tooltip"),
tooltipNode = tooltip.node,
graph = this.get("graph"),
axesCollection = this._axesCollection,
seriesCollection = this.get("seriesCollection");
while(seriesCollection.length > 0)
{
series = seriesCollection.shift();
series.destroy(true);
}
while(axesCollection.length > 0)
{
axis = axesCollection.shift();
if(axis instanceof Y.Axis)
{
axis.destroy(true);
}
}
if(this._description)
{
this._description.empty();
this._description.remove(true);
}
if(this._liveRegion)
{
this._liveRegion.empty();
this._liveRegion.remove(true);
}
if(graph)
{
graph.destroy(true);
}
if(tooltipNode)
{
tooltipNode.empty();
tooltipNode.remove(true);
}
}
}, {
ATTRS: {
/**
* Sets the aria description for the chart.
*
* @attribute ariaDescription
* @type String
*/
ariaDescription: {
value: "Use the left and right keys to navigate through items.",
setter: function(val)
{
if(this._description)
{
this._description.set("text", val);
}
return val;
}
},
/**
* Axes to appear in the chart.
*
* @attribute axes
* @type Object
*/
axes: {
getter: function()
{
return this._axes;
},
setter: function(val)
{
this._parseAxes(val);
}
},
/**
* Collection of series to appear on the chart. This can be an array of Series instances or object literals
* used to describe a Series instance.
*
* @attribute seriesCollection
* @type Array
*/
seriesCollection: {
lazyAdd: false,
getter: function()
{
return this._getSeriesCollection();
},
setter: function(val)
{
return this._setSeriesCollection(val);
}
},
/**
* Type of chart when there is no series collection specified.
*
* @attribute type
* @type String
*/
type: {
value: "pie"
}
}
});
/**
* The Chart class is the basic application used to create a chart.
*
* @class Chart
* @constructor
* @submodule charts-base
*/
function Chart(cfg)
{
if(cfg.type !== "pie")
{
return new Y.CartesianChart(cfg);
}
else
{
return new Y.PieChart(cfg);
}
}
Y.Chart = Chart;
}, '3.17.2', {
"requires": [
"dom",
"event-mouseenter",
"event-touch",
"graphics-group",
"axes",
"series-pie",
"series-line",
"series-marker",
"series-area",
"series-spline",
"series-column",
"series-bar",
"series-areaspline",
"series-combo",
"series-combospline",
"series-line-stacked",
"series-marker-stacked",
"series-area-stacked",
"series-spline-stacked",
"series-column-stacked",
"series-bar-stacked",
"series-areaspline-stacked",
"series-combo-stacked",
"series-combospline-stacked"
]
});