File: ia\charts\layers\StackedTimeLayer.js
/**
* The base class for stacked time layers.
*
* @author J Clare
* @class ia.StackedTimeLayer
* @extends ia.ItemLayer
* @constructor
*/
ia.StackedTimeLayer = function()
{
ia.StackedTimeLayer.baseConstructor.call(this);
this._minBarWidthForStroke = 10;
this._renderTimeout = null;
this.selectedDate = "";
this.highlightSelectedDate = false;
};
ia.extend(ia.ItemLayer, ia.StackedTimeLayer);
/**
* The selected date.
*
* @property selectedDate
* @type String
*/
ia.StackedTimeLayer.prototype.selectedDate;
/**
* Should the selected date be highlighted.
*
* @property highlightSelectedDate
* @type Boolean
* @default false
*/
ia.StackedTimeLayer.prototype.highlightSelectedDate;
/**
* The min bar value.
*
* @property minValue
* @type Number
*/
ia.StackedTimeLayer.prototype.minValue;
/**
* The max bar value.
*
* @property maxValue
* @type Number
*/
ia.StackedTimeLayer.prototype.maxValue;
/**
* Specifies a list of legend classes for the layer.
*
* @property legendClasses
* @type ia.CategoricClass[]
*/
ia.StackedTimeLayer.prototype.legendClasses;
/**
* A list of data fields.
*
* @property dataFields
* @type String[]
*/
ia.StackedTimeLayer.prototype.dataFields;
/**
* Updates the data.
*
* @method update
*/
ia.StackedTimeLayer.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 total = 0;
var date = data.dates[i];
var dataItem = data[date][id];
for (var j = 0; j < this.dataFields.length; j++)
{
// Has to be a number to be displayed in a bar chart.
var dataField = this.dataFields[j];
var value = dataItem[dataField];
var childItem = {};
childItem.id = id+"~"+date;
childItem.name = dataField;
childItem.date = date;
childItem.rect = new ia.Rectangle();
childItem.hitArea = new ia.Rectangle();
childItem.dateIndex = i;
childItem.stackIndex = j;
if (this.legendClasses)
{
for (var k = 0; k < this.legendClasses.length; k++)
{
var lc = this.legendClasses[k];
if (lc.value === dataField)
{
childItem.name = lc.getLabel();
childItem.color = lc.color;
break;
}
}
}
// Cant have negative or categoric values in a stack chart.
if (ia.isNumber(value) && value >= 0)
childItem.value = parseFloat(value);
else
childItem.value = 0;
childItem.formattedValue = dataItem[dataField+"_formatted"];
chartItem.childItems[chartItem.childItems.length] = childItem;
total = total + parseFloat(childItem.value);
}
}
if (total !== 0)
{
if (this.isComparison)
{
if (this.selectionIds.indexOf(chartItem.id) !== -1)
{
// Get the max bar values for the layer.
this.maxValue = Math.max(this.maxValue, total);
chartItem.state = ia.ItemLayer.SELECTED;
}
if (this.displayAll)
{
// Get the max bar values for the layer.
this.maxValue = Math.max(this.maxValue, total);
}
}
else
{
// Get the max bar values for the layer.
this.maxValue = Math.max(this.maxValue, total);
}
}
}
}
this.dataChanged = false;
}
}
/**
* Renders the data.
*
* @method render
*/
ia.StackedTimeLayer.prototype.render = function()
{
this._renderTimeout = null;
// 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.highlightContext[p] = this.style[p];
}
this.highlightContext.strokeStyle = ia.Color.toRGBA(this.highlightColor, 0.8);
this.highlightContext.fillStyle = ia.Color.toRGBA(this.highlightColor, 0.1);
this.highlightContext.lineWidth = parseFloat(this.style.lineWidth) + 1;
// Render the items.
var data = this.getData();
if (data.dates)
{
var nDates = data.dates.length;
if (this.map.orientation === "vertical")
{
this._maxBarWidth = ((this.map.canvasWidth / nDates)) * 0.7;
this._barSize = this._maxBarWidth / this.selectionIds.length;
}
else
{
this._maxBarWidth = ((this.map.canvasHeight / nDates)) * 0.7;
this._barSize = this._maxBarWidth / this.selectionIds.length;
}
// Render selected date region.
if (this.highlightSelectedDate)
{
var dates = data.dates;
var index = dates.indexOf(this.selectedDate);
if (index !== -1)
{
this.context.fillStyle = ia.Color.toRGBA(this.selectionColor, 0.07);
this.context.lineWidth = 0;
var x = this.map.canvasX + ((index / dates.length) * this.map.canvasWidth);
var y = this.map.canvasY;
var w = this.map.canvasWidth / dates.length;
var h = this.map.canvasHeight;
this.context.beginPath();
this.context.rect(x, y, w, h);
this.context.fill();
}
}
// Set the items shape for all features - so they can be drawn when highlighted.
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);
}
}
// Render the selected features.
var n = this.selectionIds.length;
for (var i = 0; i < n; i++)
{
// Get the data item.
var id = this.selectionIds[i]
var chartItem = this.items[id];
if (chartItem)
{
var nChildItems = chartItem.childItems.length;
for (var j = 0; j < nChildItems; j++)
{
var childItem = chartItem.childItems[j];
if (childItem.value !== undefined) this._setItemShape(childItem, i, nDates);
}
this._renderItem(chartItem);
}
}
}
};
/**
* Sets an items dimensions.
*
* @method _setItemShape
* @param {Object} item The item.
* @param {Number} itemIndex The index of the item.
* @param {Number} nDates The number of dates.
* @private
*/
ia.StackedTimeLayer.prototype._stackValue;
ia.StackedTimeLayer.prototype._setItemShape = function(item, itemIndex, nDates)
{
var nItems = this.selectionIds.length;
if (this.map.orientation === "vertical")
{
if (item.stackIndex === 0) this._stackValue = item.value;
else this._stackValue = this._stackValue + item.value;
var barY = this.map.getPixelY(this._stackValue);
var barHeight = this.map.getPixelHeight(item.value);
// Adjust for when bar value is outside fixed max value.
if (this._stackValue > this.map.getBBox().getYMax())
{
barY = this.map.canvasY;
barHeight = barHeight - this.map.getPixelHeight(this._stackValue - this.map.getBBox().getYMax());
}
// The pixel drawing rectangle for the bar.
item.rect.y = barY
item.rect.height = barHeight;
item.rect.width = this._barSize;
var gap = ((this.map.canvasWidth / nDates) - (this._maxBarWidth)) / 2;
item.rect.x = (this.map.canvasX + (item.dateIndex / nDates) * this.map.canvasWidth) + gap + (itemIndex * item.rect.width);
// Check bar width is within accepted size limits.
if (item.rect.width > this._maxBarWidth)
{
item.rect.x = item.rect.x + ((item.rect.width - this._maxBarWidth) / 2);
item.rect.width = this._maxBarWidth;
}
else if (item.rect.width < this._minBarWidthForStroke)
{
// Creates a slight bar overlap so you dont get whiteline rendering problem.
item.rect.width++;
}
// The pixel hit area for the bar.
item.hitArea.x = item.rect.x;
item.hitArea.y = item.rect.y;
item.hitArea.width = item.rect.width;
item.hitArea.height = item.rect.height;
}
else
{
if (item.stackIndex === 0) this._stackValue = 0;
var barX = this.map.getPixelX(this._stackValue);
var barWidth = this.map.getPixelWidth(item.value);
// Adjust for when bar value is outside fixed max value.
if (this._stackValue > this.map.getBBox().getXMax())
{
barWidth = barWidth - this.map.getPixelWidth(this._stackValue - this.map.getBBox().getXMax());
}
// The pixel drawing rectangle for the bar.
item.rect.x = barX
item.rect.width = barWidth;
item.rect.height = this._barSize;
var gap = ((this.map.canvasHeight / nDates) - (this._maxBarWidth)) / 2;
item.rect.y = (this.map.canvasY + (item.dateIndex / nDates) * this.map.canvasHeight) + gap + (itemIndex * item.rect.height);
// Check bar width is within accepted size limits.
if (item.rect.height > this._maxBarWidth)
{
item.rect.y = item.rect.y + ((item.rect.height - this._maxBarWidth) / 2);
item.rect.height = this._maxBarWidth;
}
else if (item.rect.height < this._minBarWidthForStroke)
{
// Creates a slight bar overlap so you dont get whiteline rendering problem.
item.rect.height++;
}
// The pixel hit area for the bar.
item.hitArea.x = item.rect.x;
item.hitArea.y = item.rect.y;
item.hitArea.width = item.rect.width;
item.hitArea.height = item.rect.height;
this._stackValue = this._stackValue + item.value;
}
};
/**
* Renders the item to the given context.
*
* @method _renderItem
* @param {Object} item The item.
* @private
*/
ia.StackedTimeLayer.prototype._renderItem = function(item)
{
this._drawItem(item, this.context);
};
/**
* Selects the item.
*
* @method selectItem
* @param {Object} item The item.
*/
ia.StackedTimeLayer.prototype.selectItem = function(item) {this._triggerRender();};
/**
* Unselects an item.
*
* @method unselect
* @param {String} id The id of the item.
*/
ia.StackedTimeLayer.prototype.unselect = function(id)
{
var item = this.getItem(id);
if (item)
{
item.state = ia.ItemLayer.UNSELECTED;
var index = this.selectionIds.indexOf(id);
if (index !== -1) this.selectionIds.splice(index, 1);
}
this._triggerRender();
};
/**
* Clears all selections.
*
* @method clearSelection
*/
ia.StackedTimeLayer.prototype.clearSelection = function()
{
this.selectionIds = [];
for (var id in this.items)
{
var item = this.items[id];
if (item) {item.state = ia.ItemLayer.UNSELECTED;}
}
this._triggerRender();
};
/**
* Triggers a render. Prevents over rendering which results in a frozen browser.
*
* @method _triggerRender
* @private
*/
ia.StackedTimeLayer.prototype._triggerRender = function()
{
if (!this._renderTimeout)
{
var me = this;
this._renderTimeout = setTimeout(function()
{
me.render();
}, 5);
}
};
/**
* Highlights the item.
*
* @method highlightItem
* @param {Object} item The item.
*/
ia.StackedTimeLayer.prototype.highlightItem = function(item)
{
var n = item.childItems.length;
for (var i = 0; i < n; i++)
{
var childItem = item.childItems[i];
if (childItem.value !== undefined && childItem.value !== 0)
{
this.highlightContext.fillStyle = childItem.color;
this.highlightContext.beginPath();
this.highlightContext.rect(childItem.rect.x, childItem.rect.y, childItem.rect.width, childItem.rect.height);
this.highlightContext.stroke();
if (this.map.animationMode && childItem.date === this.selectedDate) break;
}
}
};
/**
* Does the actual drawing.
*
* @method _drawItem
* @param {Object} item The item.
* @param {HTML Canvas Context} ctx The context to render to.
* @private
*/
ia.StackedTimeLayer.prototype._drawItem = function(item, ctx)
{
var n = item.childItems.length;
for (var i = 0; i < n; i++)
{
var childItem = item.childItems[i];
if (childItem.value !== undefined && childItem.value !== 0)
{
ctx.fillStyle = childItem.color;
ctx.beginPath();
ctx.rect(childItem.rect.x, childItem.rect.y, childItem.rect.width, childItem.rect.height);
ctx.fill();
//if (this._barSize >= this._minBarWidthForStroke) ctx.stroke();
if (this.map.animationMode && childItem.date === 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.StackedTimeLayer.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.StackedTimeLayer.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("${feature-name}").join(item.name);
label = label.split("${date}").join(childItem.date);
label = label.split("${associate-name}").join(childItem.name);
label = label.split("${value}").join(childItem.formattedValue);
this.map.datatip.text(label);
}
else this.map.datatip.text(item.name+" ("+childItem.date+") <br/> "+childItem.name+" : "+childItem.formattedValue);
// Position above the bar.
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
{
if (this.map.orientation === "vertical")
{
px = (childItem.rect.x + (childItem.rect.width / 2)) - (this.map.datatip.getWidth() / 2);
if (childItem.value < 0)
py = childItem.rect.y + childItem.rect.height + 5;
else
py = childItem.rect.y - this.map.datatip.getHeight() - 5;
}
else
{
if (childItem.value < 0)
px = childItem.rect.x - (this.map.datatip.getWidth() + 5);
else
px = (childItem.rect.x + childItem.rect.width) + 5;
py = (childItem.rect.y + (childItem.rect.height / 2)) - (this.map.datatip.getHeight() / 2);
}
}
this.map.datatip.position(px, py);
this.map.datatip.show();
break;
}
}
}
};