File: ia\charts\layers\TimeLayer.js
/**
* The base class for time series layers.
*
* @author J Clare
* @class ia.TimeLayer
* @extends ia.ItemLayer
* @constructor
*/
ia.TimeLayer = function()
{
ia.TimeLayer.baseConstructor.call(this);
this.style = {fillStyle:'#ffffff', strokeStyle:'#ff0000', lineWidth:'2', lineJoin:'round'};
this.selectedDate = "";
this.drawLinesThroughMissingValues = true;
this.highlightSelectedDate = false;
};
ia.extend(ia.ItemLayer, ia.TimeLayer);
/**
* The selected date.
*
* @property selectedDate
* @type String
*/
ia.TimeLayer.prototype.selectedDate;
/**
* Should the selected date be highlighted.
*
* @property highlightSelectedDate
* @type Boolean
* @default false
*/
ia.TimeLayer.prototype.highlightSelectedDate;
/**
* The layer style.
*
* @property style
* @type Object
* @default {fillStyle:'#ffffff', strokeStyle:'#ff0000', lineWidth:'2', lineJoin:'round'}
*/
ia.TimeLayer.prototype.style;
/**
* The min bar value.
*
* @property minValue
* @type Number
*/
ia.TimeLayer.prototype.minValue;
/**
* The max bar value.
*
* @property maxValue
* @type Number
*/
ia.TimeLayer.prototype.maxValue;
/**
* Specifies a color palette for the layer.
*
* @property colorPalette
* @type ia.ColorPalette
*/
ia.TimeLayer.prototype.colorPalette;
/**
* Should lines be drawn through missing data values.
*
* @property drawLinesThroughMissingValues
* @type Boolean
* @default true
*/
ia.TimeLayer.prototype.drawLinesThroughMissingValues;
/**
* Indicates that the axis should be updated and matched to the selected features.
*
* @property matchAxisToSelectedData
* @type Boolean
* @default false
*/
ia.TimeLayer.prototype.matchAxisToSelectedData;
/**
* The marker size.
*
* @property markerSize
* @type Number
* @default 7
*/
ia.TimeLayer.prototype.markerSize = 7;
/**
* Updates the data.
*
* @method update
*/
ia.TimeLayer.prototype.update = function()
{
// Check if the data has changed
if (this.map && this.dataChanged)
{
// Get the data.
var data = this.getData();
// Clear the items.
this.itemArray = [];
this.clearItems();
this.minValue = Infinity;
this.maxValue = -Infinity;
if (data.dates)
{
// First off get the list of ids to loop through.
var indData = data[data.dates[0]];
var dateLength = data.dates.length;
// Loop through the data.
for (var id in indData)
{
// Get the data item.
var dataItem = indData[id];
// Create a new chart item.
var chartItem = {};
chartItem.id = dataItem.id;
chartItem.name = dataItem.name;
chartItem.state = ia.ItemLayer.UNSELECTED;
if (this.selectionIds.indexOf(chartItem.id) !== -1 ) chartItem.state = ia.ItemLayer.SELECTED;
chartItem.parent = this;
chartItem.layer = this;
this.items[dataItem.id] = chartItem;
this.itemArray.push(chartItem);
// Now add child items for each date.
chartItem.childItems = [];
for (var i = 0; i < dateLength; i++)
{
var date = data.dates[i];
var dataItem = data[date][id];
// Has to be a number to be displayed in a time chart
var value = dataItem[this.dataField];
if (ia.isNumber(value))
{
var childItem = {};
childItem.id = id+"~"+date;
childItem.name = date;
childItem.shape = new ia.Rectangle();
childItem.hitArea = new ia.Rectangle();
childItem.value = value;
childItem.formattedValue = dataItem[this.dataField+"_formatted"];
if (this.isComparison)
{
if (this.selectionIds.indexOf(chartItem.id) !== -1 )
{
// Get the min and max bar values for the layer.
this.minValue = Math.min(this.minValue, value);
this.maxValue = Math.max(this.maxValue, value);
chartItem.state = ia.ItemLayer.SELECTED;
}
if (this.displayAll)
{
// Get the min and max bar values for the layer.
this.minValue = Math.min(this.minValue, value);
this.maxValue = Math.max(this.maxValue, value);
}
}
else if ((this.matchAxisToSelectedData === false) ||
(this.matchAxisToSelectedData === true && chartItem.state === ia.ItemLayer.SELECTED))
{
// Get the min and max bar values for the layer.
this.minValue = Math.min(this.minValue, childItem.value);
this.maxValue = Math.max(this.maxValue, childItem.value);
}
chartItem.childItems[chartItem.childItems.length] = childItem;
}
else chartItem.childItems[chartItem.childItems.length] = {name:date, value:undefined};
}
}
}
this.dataChanged = false;
}
}
/**
* Renders the data.
*
* @method render
*/
ia.TimeLayer.prototype.render = function()
{
// Clear the canvas.
this.clear();
// Reset the context styles in case the layer styles has changed.
for (var p in this.style)
{
this.context[p] = this.style[p];
this.selectionContext[p] = this.style[p];
this.highlightContext[p] = this.style[p];
}
this.highlightContext.strokeStyle = ia.Color.toRGBA(this.highlightColor);
this.selectionContext.strokeStyle = ia.Color.toRGBA(this.selectionColor);
// Set the items shape.
var nItems = this.itemArray.length;
for (var i = 0; i < nItems; i++)
{
var chartItem = this.itemArray[i];
var nChildItems = chartItem.childItems.length;
for (var j = 0; j < nChildItems; j++)
{
var childItem = chartItem.childItems[j];
if (childItem.value !== undefined) this._setItemShape(childItem, j, nChildItems);
}
}
// Render selected date region.
if (this.highlightSelectedDate)
{
if (this.getData().dates)
{
this.context.fillStyle = ia.Color.toRGBA('#cccccc', 0.2);
this.context.lineWidth = 0;
var dates = this.getData().dates;
var index = dates.indexOf(this.selectedDate);
if (index !== -1)
{
var x, y, w, h;
if (this.map.orientation === "vertical")
{
x = this.map.canvasX + ((index / dates.length) * this.map.canvasWidth);
y = this.map.canvasY;
w = this.map.canvasWidth / dates.length;
h = this.map.canvasHeight;
}
else
{
x = this.map.canvasX;
y = this.map.canvasY + ((index / dates.length) * this.map.canvasHeight);
w = this.map.canvasWidth;
h = this.map.canvasHeight / dates.length;
}
this.context.beginPath();
this.context.rect(x, y, w, h);
this.context.fill();
}
}
}
// Render the selection (which in this case is the only thing displayed on the chart).
this.renderSelection();
};
/**
* Sets an items dimensions.
*
* @method _setItemShape
* @param {Object} item The item.
* @param {Number} index The index of the item in the item array.
* @private
*/
ia.TimeLayer.prototype._setItemShape = function(item, index, nItems)
{
if (this.map.orientation === "vertical")
{
var startX = this.map.canvasX;
var hSpace = this.map.canvasWidth;
if (this.map.centerXAxisLabels)
{
var indent = this.map.canvasWidth / (nItems * 2);
startX = this.map.canvasX + indent;
hSpace = this.map.canvasWidth - (indent * 2);
}
var x = startX + (index / (nItems - 1)) * hSpace;
var y = this.map.getPixelY(item.value);
}
else
{
var startY = this.map.canvasY;
var vSpace = this.map.canvasHeight;
if (this.map.centerYAxisLabels)
{
var indent = this.map.canvasHeight / (nItems * 2);
startY = this.map.canvasY + indent;
vSpace = this.map.canvasHeight - (indent * 2);
}
var x = this.map.getPixelX(item.value);
var y = startY + (index / (nItems - 1)) * vSpace;
}
var w = this.markerSize;
var h = this.markerSize;
// Reset the pixel drawing area for the point.
item.shape.x = x;
item.shape.y = y;
item.shape.width = w;
item.shape.height = h;
// Reset the pixel hit area for the point.
if (ia.IS_TOUCH_DEVICE) // Larger hit area for touch devices.
{
w = 30;
h = 30;
}
else
{
w = w * 2;
h = h * 2;
}
item.hitArea.x = x - (w / 2);
item.hitArea.y = y - (h / 2);
item.hitArea.width = w;
item.hitArea.height = h;
};
/**
* Selects the item.
*
* @method selectItem
* @param {Object} item The item.
*/
ia.TimeLayer.prototype.selectItem = function(item)
{
var n = this.selectionIds.length;
if (this.colorPalette)
{
var colorList = this.colorPalette.getColors(n);
var index = this.selectionIds.indexOf(item.id);
this.selectionContext.strokeStyle = colorList[index];
}
this._drawItem(item, this.selectionContext);
};
/**
* Highlights the item.
*
* @method highlightItem
* @param {Object} item The item.
*/
ia.TimeLayer.prototype.highlightItem = function(item)
{
// Clip.
if (!ia.IS_IE_TEN)
{
this.highlightContext.beginPath();
this.highlightContext.rect(this.map.canvasX, this.map.canvasY, this.map.canvasWidth, this.map.canvasHeight);
this.highlightContext.clip();
}
this._drawItem(item, this.highlightContext);
};
/**
* Does the actual drawing.
*
* @method _drawItem
* @param {Object} item The item.
* @param {HTML Canvas Context} ctx The context to render to.
* @private
*/
ia.TimeLayer.prototype._drawItem = function(item, ctx)
{
var doMoveTo = true; // Takes into account first value and missing values.
var n = item.childItems.length;
ctx.beginPath();
for (var i = 0; i < n; i++)
{
var childItem = item.childItems[i];
if (childItem.value !== undefined)
{
if (doMoveTo)
ctx.moveTo(childItem.shape.x, childItem.shape.y);
else
ctx.lineTo(childItem.shape.x, childItem.shape.y);
doMoveTo = false;
}
else if (this.drawLinesThroughMissingValues !== true) doMoveTo = true;
if (this.map.animationMode && childItem.name === this.selectedDate) break;
}
ctx.stroke();
for (var i = 0; i < n; i++)
{
var childItem = item.childItems[i];
if (childItem.value !== undefined)
{
ctx.beginPath();
ia.Shape.drawCircle(ctx, childItem.shape.x, childItem.shape.y, childItem.shape.width);
ctx.fill();
ctx.stroke();
}
if (this.map.animationMode && childItem.name === this.selectedDate) break;
}
};
/**
* Runs a hit test on an item.
*
* @method hitItem
* @param {Object} item The item to hit test.
* @param {ia.MapMouseEvent} event An <code>ia.MapMouseEvent</code>.
*/
ia.TimeLayer.prototype.hitItem = function(item, event)
{
if (this.isSelected(item.id))
{
var n = item.childItems.length;
for (var i = 0; i < n; i++)
{
var childItem = item.childItems[i];
if (childItem.value !== undefined)
{
if (childItem.hitArea.intersects(event.x,event.y)) return true;
}
}
}
return false;
};
/**
* Displays the tip for the passed item
*
* @method showTip
* @param {Object} item The map item.
* @param {ia.ItemEvent} event An <code>ia.ItemEvent</code>.
*/
ia.TimeLayer.prototype.showTip = function(item, event)
{
var n = item.childItems.length;
for (var i = 0; i < n; i++)
{
var childItem = item.childItems[i];
if (childItem.value !== undefined)
{
if (childItem.hitArea.intersects(event.x,event.y))
{
// Tip replacement.
if (this.tip !== "")
{
var label = this.tip;
label = label.split("${name}").join(item.name);
label = label.split("${date}").join(childItem.name);
label = label.split("${value}").join(childItem.formattedValue);
this.map.datatip.text(label);
}
else this.map.datatip.text(item.name+" : "+childItem.name+" : "+childItem.formattedValue);
var px,py;
if (ia.IS_TOUCH_DEVICE)
{
px = event.x - (this.map.datatip.getWidth() / 2);
py = event.y - (this.map.datatip.getHeight() + 30);
}
else
{
px = (childItem.shape.x + childItem.shape.width / 2) - (this.map.datatip.getWidth() / 2);
py = (childItem.shape.y - childItem.shape.height / 2) - (this.map.datatip.getHeight() + 5);
}
this.map.datatip.position(px, py);
this.map.datatip.show();
break;
}
}
}
};