dimple-js
Version:
Dimple is an object-oriented API allowing you to create flexible axis-based charts using [d3.js](http://d3js.org "d3.js").
872 lines (817 loc) • 208 kB
JavaScript
// Copyright: 2013 PMSI-AlignAlytics
// License: "https://github.com/PMSI-AlignAlytics/dimple/blob/master/MIT-LICENSE.txt"
// Source: /src/objects/begin.js
// Create the stub object
var dimple = {
version: "1.0.0",
plot: {},
aggregateMethod: {}
};
// Wrap all application code in a self-executing function
(function () {
"use strict";
// Copyright: 2013 PMSI-AlignAlytics
// License: "https://github.com/PMSI-AlignAlytics/dimple/blob/master/MIT-LICENSE.txt"
// Source: /src/objects/axis/begin.js
// Help: http://github.com/PMSI-AlignAlytics/dimple/wiki/dimple.axis
dimple.axis = function (chart, position, categoryFields, measure, timeField) {
// Help: http://github.com/PMSI-AlignAlytics/dimple/wiki/dimple.axis#wiki-chart
this.chart = chart;
// Help: http://github.com/PMSI-AlignAlytics/dimple/wiki/dimple.axis#wiki-position
this.position = position;
// Help: http://github.com/PMSI-AlignAlytics/dimple/wiki/dimple.axis#wiki-categoryFields
this.categoryFields = (timeField === null || timeField === undefined ? categoryFields : [].concat(timeField));
// Help: http://github.com/PMSI-AlignAlytics/dimple/wiki/dimple.axis#wiki-measure
this.measure = measure;
// Help: http://github.com/PMSI-AlignAlytics/dimple/wiki/dimple.axis#wiki-timeField
this.timeField = timeField;
// Help: http://github.com/PMSI-AlignAlytics/dimple/wiki/dimple.axis#wiki-floatingBarWidth
this.floatingBarWidth = 5;
// Help: http://github.com/PMSI-AlignAlytics/dimple/wiki/dimple.axis#wiki-hidden
this.hidden = false;
// Help: http://github.com/PMSI-AlignAlytics/dimple/wiki/dimple.axis#wiki-showPercent
this.showPercent = false;
// Help: http://github.com/PMSI-AlignAlytics/dimple/wiki/dimple.axis#wiki-colors
this.colors = null;
// Help: http://github.com/PMSI-AlignAlytics/dimple/wiki/dimple.axis#wiki-overrideMin
this.overrideMin = null;
// Help: http://github.com/PMSI-AlignAlytics/dimple/wiki/dimple.axis#wiki-overrideMax
this.overrideMax = null;
// Help: http://github.com/PMSI-AlignAlytics/dimple/wiki/dimple.axis#wiki-shapes
this.shapes = null;
// Help: http://github.com/PMSI-AlignAlytics/dimple/wiki/dimple.axis#wiki-showGridlines
this.showGridlines = null;
// Help: http://github.com/PMSI-AlignAlytics/dimple/wiki/dimple.axis#wiki-gridlineShapes
this.gridlineShapes = null;
// Help: http://github.com/PMSI-AlignAlytics/dimple/wiki/dimple.axis#wiki-titleShape
this.titleShape = null;
// Help: http://github.com/PMSI-AlignAlytics/dimple/wiki/dimple.axis#wiki-dateParseFormat
this.dateParseFormat = null;
// Help: http://github.com/PMSI-AlignAlytics/dimple/wiki/dimple.axis#wiki-tickFormat
this.tickFormat = null;
// Help: http://github.com/PMSI-AlignAlytics/dimple/wiki/dimple.axis#wiki-timePeriod
this.timePeriod = null;
// Help: http://github.com/PMSI-AlignAlytics/dimple/wiki/dimple.axis#wiki-timeInterval
this.timeInterval = 1;
// Help: http://github.com/PMSI-AlignAlytics/dimple/wiki/dimple.axis#wiki-useLog
this.useLog = false;
// Help: http://github.com/PMSI-AlignAlytics/dimple/wiki/dimple.axis#wiki-logBase
this.logBase = 10;
// The scale determined by the update method
this._scale = null;
// The minimum and maximum axis values
this._min = 0;
this._max = 0;
// Chart origin before and after an update. This helps
// with transitions
this._previousOrigin = null;
this._origin = null;
// The order definition array
this._orderRules = [];
// The group order definition array
this._groupOrderRules = [];
// Copyright: 2013 PMSI-AlignAlytics
// License: "https://github.com/PMSI-AlignAlytics/dimple/blob/master/MIT-LICENSE.txt"
// Source: /src/objects/axis/methods/_draw.js
this._draw = null;
// Copyright: 2013 PMSI-AlignAlytics
// License: "https://github.com/PMSI-AlignAlytics/dimple/blob/master/MIT-LICENSE.txt"
// Source: /src/objects/axis/methods/_getFormat.js
this._getFormat = function () {
var returnFormat,
max,
min,
len,
chunks,
suffix,
dp;
if (this.tickFormat !== null && this.tickFormat !== undefined) {
if (this._hasTimeField()) {
returnFormat = d3.time.format(this.tickFormat);
} else {
returnFormat = d3.format(this.tickFormat);
}
} else if (this.showPercent) {
returnFormat = d3.format("%");
} else if (this.useLog && this.measure !== null) {
// With linear axes the range is used to apply uniform
// formatting but with a log axis it is based on each number
// independently
returnFormat = function (n) {
var l = Math.floor(Math.abs(n), 0).toString().length,
c = Math.min(Math.floor((l - 1) / 3), 4),
s = "kmBT".substring(c - 1, c),
d = (Math.round((n / Math.pow(1000, c)) * 10).toString().slice(-1) === "0" ? 0 : 1);
return (n === 0 ? 0 : d3.format(",." + d + "f")(n / Math.pow(1000, c)) + s);
};
} else if (this.measure !== null) {
max = Math.floor(Math.abs(this._max), 0).toString();
min = Math.floor(Math.abs(this._min), 0).toString();
len = Math.max(min.length, max.length);
if (len > 3) {
chunks = Math.min(Math.floor((len - 1) / 3), 4);
suffix = "kmBT".substring(chunks - 1, chunks);
dp = (len - chunks * 3 <= 1 ? 1 : 0);
returnFormat = function (n) {
return (n === 0 ? 0 : d3.format(",." + dp + "f")(n / Math.pow(1000, chunks)) + suffix);
};
} else {
dp = (len <= 1 ? 1 : 0);
returnFormat = d3.format(",." + dp + "f");
}
} else {
returnFormat = function (n) { return n; };
}
return returnFormat;
};
// Copyright: 2013 PMSI-AlignAlytics
// License: "https://github.com/PMSI-AlignAlytics/dimple/blob/master/MIT-LICENSE.txt"
// Source: /src/objects/axis/methods/_getTimePeriod.js
this._getTimePeriod = function () {
// A javascript date object
var outPeriod = this.timePeriod,
maxPeriods = 30,
diff = this._max - this._min;
if (this._hasTimeField && (this.timePeriod === null || this.timePeriod === undefined)) {
// Calculate using millisecond values for speed. Using the date range requires creating an array
// which in the case of seconds kills the browser. All constants are straight sums of milliseconds
// except months taken as (86400000 * 365.25) / 12 = 2629800000
if (diff / 1000 <= maxPeriods) {
outPeriod = d3.time.seconds;
} else if (diff / 60000 <= maxPeriods) {
outPeriod = d3.time.minutes;
} else if (diff / 3600000 <= maxPeriods) {
outPeriod = d3.time.hours;
} else if (diff / 86400000 <= maxPeriods) {
outPeriod = d3.time.days;
} else if (diff / 604800000 <= maxPeriods) {
outPeriod = d3.time.weeks;
} else if (diff / 2629800000 <= maxPeriods) {
outPeriod = d3.time.months;
} else {
outPeriod = d3.time.years;
}
}
// Return the date
return outPeriod;
};
// Copyright: 2013 PMSI-AlignAlytics
// License: "https://github.com/PMSI-AlignAlytics/dimple/blob/master/MIT-LICENSE.txt"
// Source: /src/objects/axis/methods/_hasCategories.js
this._hasCategories = function () {
return (this.categoryFields !== null && this.categoryFields !== undefined && this.categoryFields.length > 0);
};
// Copyright: 2013 PMSI-AlignAlytics
// License: "https://github.com/PMSI-AlignAlytics/dimple/blob/master/MIT-LICENSE.txt"
// Source: /src/objects/axis/methods/_hasMeasure.js
this._hasMeasure = function () {
return (this.measure !== null && this.measure !== undefined);
};
// Copyright: 2013 PMSI-AlignAlytics
// License: "https://github.com/PMSI-AlignAlytics/dimple/blob/master/MIT-LICENSE.txt"
// Source: /src/objects/axis/methods/_hasTimeField.js
this._hasTimeField = function () {
return (this.timeField !== null && this.timeField !== undefined);
};
// Copyright: 2013 PMSI-AlignAlytics
// License: "https://github.com/PMSI-AlignAlytics/dimple/blob/master/MIT-LICENSE.txt"
// Source: /src/objects/axis/methods/_parseDate.js
this._parseDate = function (inDate) {
// A javascript date object
var outDate;
if (this.dateParseFormat === null || this.dateParseFormat === undefined) {
// If nothing has been explicity defined you are in the hands of the browser gods
// may they smile upon you...
outDate = Date.parse(inDate);
} else {
outDate = d3.time.format(this.dateParseFormat).parse(inDate);
}
// Return the date
return outDate;
};
// Copyright: 2013 PMSI-AlignAlytics
// License: "https://github.com/PMSI-AlignAlytics/dimple/blob/master/MIT-LICENSE.txt"
// Source: /src/objects/axis/methods/_update.js
this._update = function (refactor) {
var distinctCats = [],
ticks,
step,
remainder,
origin,
getOrderedCategories = function (self, axPos, oppPos) {
var category = self.categoryFields[0],
sortBy = category,
desc = false,
isDate = true,
i,
definitions = [];
// Check whether this field is a date
for (i = 0; i < self.chart.data.length; i += 1) {
if (isNaN(self._parseDate(self.chart.data[i][category]))) {
isDate = false;
break;
}
}
if (!isDate) {
// Find the first series which connects this axis to another
self.chart.series.forEach(function (s) {
if (s[axPos] === self && s[oppPos]._hasMeasure()) {
sortBy = s[oppPos].measure;
desc = true;
}
}, this);
}
definitions = self._orderRules.concat({ ordering : sortBy, desc : desc });
return dimple._getOrderedList(self.chart.data, category, definitions);
};
// If the axis is a percentage type axis the bounds must be between -1 and 1. Sometimes
// binary rounding means it can fall outside that bound so tidy up here
this._min = (this.showPercent && this._min < -1 ? -1 : this._min);
this._max = (this.showPercent && this._max > 1 ? 1 : this._max);
// Override or round the min or max
this._min = (this.overrideMin !== null ? this.overrideMin : this._min);
this._max = (this.overrideMax !== null ? this.overrideMax : this._max);
// If this is an x axis
if (this.position === "x") {
if (this._hasTimeField()) {
this._scale = d3.time.scale()
.rangeRound([this.chart._xPixels(), this.chart._xPixels() + this.chart._widthPixels()])
.domain([this._min, this._max]);
} else if (this.useLog) {
this._scale = d3.scale.log()
.range([this.chart._xPixels(), this.chart._xPixels() + this.chart._widthPixels()])
.domain([
(this._min === 0 ? Math.pow(this.logBase, -1) : this._min),
(this._max === 0 ? -1 * Math.pow(this.logBase, -1) : this._max)
])
.clamp(true)
.base(this.logBase)
.nice();
} else if (this.measure === null || this.measure === undefined) {
distinctCats = getOrderedCategories(this, "x", "y");
this._scale = d3.scale.ordinal()
.rangePoints([this.chart._xPixels(), this.chart._xPixels() + this.chart._widthPixels()])
.domain(distinctCats.concat([""]));
} else {
this._scale = d3.scale.linear()
.range([this.chart._xPixels(), this.chart._xPixels() + this.chart._widthPixels()])
.domain([this._min, this._max]).nice();
}
// If it's visible, orient it at the top or bottom if it's first or second respectively
if (!this.hidden) {
switch (this.chart._axisIndex(this, "x")) {
case 0:
this._draw = d3.svg.axis()
.orient("bottom")
.scale(this._scale);
break;
case 1:
this._draw = d3.svg.axis()
.orient("top")
.scale(this._scale);
break;
default:
break;
}
}
} else if (this.position === "y") {
if (this._hasTimeField()) {
this._scale = d3.time.scale()
.rangeRound([this.chart._yPixels() + this.chart._heightPixels(), this.chart._yPixels()])
.domain([this._min, this._max]);
} else if (this.useLog) {
this._scale = d3.scale.log()
.range([this.chart._yPixels() + this.chart._heightPixels(), this.chart._yPixels()])
.domain([
(this._min === 0 ? Math.pow(this.logBase, -1) : this._min),
(this._max === 0 ? -1 * Math.pow(this.logBase, -1) : this._max)
])
.clamp(true)
.base(this.logBase)
.nice();
} else if (this.measure === null || this.measure === undefined) {
distinctCats = getOrderedCategories(this, "y", "x");
this._scale = d3.scale.ordinal()
.rangePoints([this.chart._yPixels() + this.chart._heightPixels(), this.chart._yPixels()])
.domain(distinctCats.concat([""]));
} else {
this._scale = d3.scale.linear()
.range([this.chart._yPixels() + this.chart._heightPixels(), this.chart._yPixels()])
.domain([this._min, this._max])
.nice();
}
// if it's visible, orient it at the left or right if it's first or second respectively
if (!this.hidden) {
switch (this.chart._axisIndex(this, "y")) {
case 0:
this._draw = d3.svg.axis()
.orient("left")
.scale(this._scale);
break;
case 1:
this._draw = d3.svg.axis()
.orient("right")
.scale(this._scale);
break;
default:
break;
}
}
} else if (this.position.length > 0 && this.position[0] === "z") {
if (this.useLog) {
this._scale = d3.scale.log()
.range([this.chart._heightPixels() / 300, this.chart._heightPixels() / 10])
.domain([
(this._min === 0 ? Math.pow(this.logBase, -1) : this._min),
(this._max === 0 ? -1 * Math.pow(this.logBase, -1) : this._max)
])
.clamp(true)
.base(this.logBase);
} else {
this._scale = d3.scale.linear()
.range([this.chart._heightPixels() / 300, this.chart._heightPixels() / 10])
.domain([this._min, this._max]);
}
} else if (this.position.length > 0 && this.position[0] === "c") {
this._scale = d3.scale.linear()
.range([0, (this.colors === null || this.colors.length === 1 ? 1 : this.colors.length - 1)])
.domain([this._min, this._max]);
}
// Check that the axis ends on a labelled tick
if ((refactor === null || refactor === undefined || refactor === false) && !this._hasTimeField() && this._scale !== null && this._scale.ticks !== null && this._scale.ticks !== undefined && this._scale.ticks(10).length > 0 && (this.position === "x" || this.position === "y")) {
// Get the ticks determined based on a split of 10
ticks = this._scale.ticks(10);
// Get the step between ticks
step = ticks[1] - ticks[0];
// Get the remainder
remainder = ((this._max - this._min) % step).toFixed(0);
// If the remainder is not zero
if (remainder !== 0) {
// Set the bounds
this._max = Math.ceil(this._max / step) * step;
this._min = Math.floor(this._min / step) * step;
// Recursively call the method to recalculate the scale. This shouldn't enter this block again.
this._update(true);
}
}
// Populate the origin
origin = this._scale.copy()(0);
if (this._origin !== origin) {
this._previousOrigin = (this._origin === null ? origin : this._origin);
this._origin = origin;
}
// Return axis for chaining
return this;
};
// Copyright: 2013 PMSI-AlignAlytics
// License: "https://github.com/PMSI-AlignAlytics/dimple/blob/master/MIT-LICENSE.txt"
// Source: /src/objects/axis/methods/addGroupOrderRule.js
// Help: http://github.com/PMSI-AlignAlytics/dimple/wiki/dimple.axis#wiki-addGroupOrderRule
this.addGroupOrderRule = function (ordering, desc) {
this._groupOrderRules.push({ ordering : ordering, desc : desc });
};
// Copyright: 2013 PMSI-AlignAlytics
// License: "https://github.com/PMSI-AlignAlytics/dimple/blob/master/MIT-LICENSE.txt"
// Source: /src/objects/axis/methods/addOrderRule.js
// Help: http://github.com/PMSI-AlignAlytics/dimple/wiki/dimple.axis#wiki-addOrderRule
this.addOrderRule = function (ordering, desc) {
this._orderRules.push({ ordering : ordering, desc : desc });
};
};
// End dimple.axis
// Copyright: 2013 PMSI-AlignAlytics
// License: "https://github.com/PMSI-AlignAlytics/dimple/blob/master/MIT-LICENSE.txt"
// Source: /src/objects/chart/begin.js
// Help: http://github.com/PMSI-AlignAlytics/dimple/wiki/dimple.chart
dimple.chart = function (svg, data) {
// Help: http://github.com/PMSI-AlignAlytics/dimple/wiki/dimple.chart#wiki-svg
this.svg = svg;
// Help: http://github.com/PMSI-AlignAlytics/dimple/wiki/dimple.chart#wiki-x
this.x = "10%";
// Help: http://github.com/PMSI-AlignAlytics/dimple/wiki/dimple.chart#wiki-y
this.y = "10%";
// Help: http://github.com/PMSI-AlignAlytics/dimple/wiki/dimple.chart#wiki-width
this.width = "80%";
// Help: http://github.com/PMSI-AlignAlytics/dimple/wiki/dimple.chart#wiki-height
this.height = "80%";
// Help: http://github.com/PMSI-AlignAlytics/dimple/wiki/dimple.chart#wiki-data
this.data = data;
// Help: http://github.com/PMSI-AlignAlytics/dimple/wiki/dimple.chart#wiki-noFormats
this.noFormats = false;
// Help: http://github.com/PMSI-AlignAlytics/dimple/wiki/dimple.chart#wiki-axes
this.axes = [];
// Help: http://github.com/PMSI-AlignAlytics/dimple/wiki/dimple.chart#wiki-series
this.series = [];
// Help: http://github.com/PMSI-AlignAlytics/dimple/wiki/dimple.chart#wiki-legends
this.legends = [];
// Help: http://github.com/PMSI-AlignAlytics/dimple/wiki/dimple.chart#wiki-storyboard
this.storyboard = null;
// Help: http://github.com/PMSI-AlignAlytics/dimple/wiki/dimple.chart#wiki-titleShape
this.titleShape = null;
// Help: http://github.com/PMSI-AlignAlytics/dimple/wiki/dimple.chart#wiki-shapes
this.shapes = null;
// The group within which to put all of this chart's objects
this._group = svg.append("g");
// The group within which to put tooltips. This is not initialised here because
// the group would end up behind other chart contents in a multi chart output. It will
// therefore be added and removed by the mouse enter/leave events
this._tooltipGroup = null;
// Colors assigned to chart contents. E.g. a series value.
this._assignedColors = {};
// The next colour index to use, this value is cycled around for all default colours
this._nextColor = 0;
// Access the pixel value of the x coordinate
this._xPixels = function () {
return dimple._parsePosition(this.x, this.svg.node().offsetWidth);
};
// Access the pixel value of the y coordinate
this._yPixels = function () {
return dimple._parsePosition(this.y, this.svg.node().offsetHeight);
};
// Access the pixel value of the width coordinate
this._widthPixels = function () {
return dimple._parsePosition(this.width, this.svg.node().offsetWidth);
};
// Access the pixel value of the height coordinate
this._heightPixels = function () {
return dimple._parsePosition(this.height, this.svg.node().offsetHeight);
};
// Copyright: 2013 PMSI-AlignAlytics
// License: "https://github.com/PMSI-AlignAlytics/dimple/blob/master/MIT-LICENSE.txt"
// Source: /src/objects/chart/methods/_axisIndex.js
// Return the ordinal value of the passed axis. If an orientation is passed, return the order for the
// specific orientation, otherwise return the order from all axes. Returns -1 if the passed axis isn't part of the collection
this._axisIndex = function (axis, orientation) {
var i = 0,
axisCount = 0,
index = -1;
for (i = 0; i < this.axes.length; i += 1) {
if (this.axes[i] === axis) {
index = axisCount;
break;
}
if (orientation === null || orientation === undefined || orientation[0] === this.axes[i].position[0]) {
axisCount += 1;
}
}
return index;
};
// Copyright: 2013 PMSI-AlignAlytics
// License: "https://github.com/PMSI-AlignAlytics/dimple/blob/master/MIT-LICENSE.txt"
// Source: /src/objects/chart/methods/_getSeriesData.js
// Create a dataset containing positioning information for every series
this._getSeriesData = function () {
// If there are series
if (this.series !== null && this.series !== undefined) {
// Iterate all the series
this.series.forEach(function (series) {
// The data for this series
var returnData = [],
// Handle multiple category values by iterating the fields in the row and concatonate the values
// This is repeated for each axis using a small anon function
getField = function (axis, row) {
var returnField = [];
if (axis !== null) {
if (axis._hasTimeField()) {
returnField.push(axis._parseDate(row[axis.timeField]));
} else if (axis._hasCategories()) {
axis.categoryFields.forEach(function (cat) {
returnField.push(row[cat]);
}, this);
}
}
return returnField;
},
// Catch a non-numeric value and re-calc as count
useCount = { x: false, y: false, z: false, c: false },
// If the elements are grouped a unique list of secondary category values will be required
secondaryElements = { x: [], y: [] },
// Get the x and y totals for percentages. This cannot be done in the loop above as we need the data aggregated before we get an abs total.
// otherwise it will wrongly account for negatives and positives rolled together.
totals = { x: [], y: [], z: [] },
colorBounds = { min: null, max: null },
tot,
running = { x: [], y: [], z: [] },
addedCats = [],
catTotals = {},
grandTotals = { x: 0, y: 0, z: 0 },
key,
storyCat = "",
orderedStoryboardArray = [],
seriesCat = "",
orderedSeriesArray = [],
xCat = "",
xSortArray = [],
yCat = "",
ySortArray = [],
rules = [],
sortedData = this.data,
groupRules = [];
if (this.storyboard !== null && this.storyboard !== undefined && this.storyboard.categoryFields.length > 0) {
storyCat = this.storyboard.categoryFields[0];
orderedStoryboardArray = dimple._getOrderedList(this.data, storyCat, this.storyboard._orderRules);
}
// Deal with mekkos
if (series.x._hasCategories() && series.x._hasMeasure()) {
xCat = series.x.categoryFields[0];
xSortArray = dimple._getOrderedList(this.data, xCat, series.x._orderRules.concat([{ ordering : series.x.measure, desc : true }]));
}
if (series.y._hasCategories() && series.y._hasMeasure()) {
yCat = series.y.categoryFields[0];
ySortArray = dimple._getOrderedList(this.data, yCat, series.y._orderRules.concat([{ ordering : series.y.measure, desc : true }]));
}
if (series.categoryFields !== null && series.categoryFields !== undefined && series.categoryFields.length > 0) {
// Concat is used here to break the reference to the parent array, if we don't do this, in a storyboarded chart,
// the series rules to grow and grow until the system grinds to a halt trying to deal with them all.
rules = [].concat(series._orderRules);
seriesCat = series.categoryFields[0];
if (series.c !== null && series.c !== undefined && series.c._hasMeasure()) {
rules.push({ ordering : series.c.measure, desc : true });
} else if (series.z !== null && series.z !== undefined && series.z._hasMeasure()) {
rules.push({ ordering : series.z.measure, desc : true });
} else if (series.x._hasMeasure()) {
rules.push({ ordering : series.x.measure, desc : true });
} else if (series.y._hasMeasure()) {
rules.push({ ordering : series.y.measure, desc : true });
}
orderedSeriesArray = dimple._getOrderedList(this.data, seriesCat, rules);
}
sortedData.sort(function (a, b) {
var returnValue = 0;
if (storyCat !== "") {
returnValue = orderedStoryboardArray.indexOf(a[storyCat]) - orderedStoryboardArray.indexOf(b[storyCat]);
}
if (xCat !== "" && returnValue === 0) {
returnValue = xSortArray.indexOf(a[xCat]) - xSortArray.indexOf(b[xCat]);
}
if (yCat !== "" && returnValue === 0) {
returnValue = ySortArray.indexOf(a[yCat]) - ySortArray.indexOf(b[yCat]);
}
if (seriesCat !== "" && returnValue === 0) {
returnValue = orderedSeriesArray.indexOf(a[seriesCat]) - orderedSeriesArray.indexOf(b[seriesCat]);
}
return returnValue;
});
// Iterate every row in the data to calculate the return values
sortedData.forEach(function (d) {
// Reset the index
var foundIndex = -1,
xField = getField(series.x, d),
yField = getField(series.y, d),
zField = getField(series.z, d),
// Get the aggregate field using the other fields if necessary
aggField = [],
key,
k,
newRow,
updateData;
if (series.categoryFields === null || series.categoryFields === undefined || series.categoryFields.length === 0) {
aggField = ["All"];
} else if (series.categoryFields.length === 1 && d[series.categoryFields[0]] === undefined) {
aggField = [series.categoryFields[0]];
} else {
series.categoryFields.forEach(function (cat) {
aggField.push(d[cat]);
}, this);
}
// Add a key
key = aggField.join("/") + "_" + xField.join("/") + "_" + yField.join("/") + "_" + zField.join("/");
// See if this field has already been added.
for (k = 0; k < returnData.length; k += 1) {
if (returnData[k].key === key) {
foundIndex = k;
break;
}
}
// If the field was not added, do so here
if (foundIndex === -1) {
newRow = {
key: key,
aggField: aggField,
xField: xField,
xValue: null,
xCount: 0,
yField: yField,
yValue: null,
yCount: 0,
zField: zField,
zValue: null,
zCount: 0,
cValue: 0,
cCount: 0,
x: 0,
y: 0,
xOffset: 0,
yOffset: 0,
width: 0,
height: 0,
cx: 0,
cy: 0,
xBound: 0,
yBound: 0,
xValueList: [],
yValueList: [],
zValueList: [],
cValueList: [],
fill: {},
stroke: {}
};
returnData.push(newRow);
foundIndex = returnData.length - 1;
}
// Update the return data for the passed axis
updateData = function (axis, storyboard) {
var passStoryCheck = true,
lhs = { value: 0, count: 1 },
rhs = { value: 0, count: 1 },
selectStoryValue,
compare = "",
retRow;
if (storyboard !== null) {
selectStoryValue = storyboard.getFrameValue();
storyboard.categoryFields.forEach(function (cat, m) {
if (m > 0) {
compare += "/";
}
compare += d[cat];
passStoryCheck = (compare === selectStoryValue);
}, this);
}
if (axis !== null && axis !== undefined) {
if (passStoryCheck) {
retRow = returnData[foundIndex];
if (axis._hasMeasure()) {
// Treat undefined values as zero
if (d[axis.measure] === undefined) {
d[axis.measure] = 0;
}
// Keep a distinct list of values to calculate a distinct count in the case of a non-numeric value being found
if (retRow[axis.position + "ValueList"].indexOf(d[axis.measure]) === -1) {
retRow[axis.position + "ValueList"].push(d[axis.measure]);
}
// The code above is outside this check for non-numeric values because we might encounter one far down the list, and
// want to have a record of all values so far.
if (isNaN(parseFloat(d[axis.measure]))) {
useCount[axis.position] = true;
}
// Set the value using the aggregate function method
lhs.value = retRow[axis.position + "Value"];
lhs.count = retRow[axis.position + "Count"];
rhs.value = d[axis.measure];
retRow[axis.position + "Value"] = series.aggregate(lhs, rhs);
retRow[axis.position + "Count"] += 1;
}
}
}
};
// Update all the axes
updateData(series.x, this.storyboard);
updateData(series.y, this.storyboard);
updateData(series.z, this.storyboard);
updateData(series.c, this.storyboard);
}, this);
// Get secondary elements if necessary
if (series.x !== null && series.x !== undefined && series.x._hasCategories() && series.x.categoryFields.length > 1 && secondaryElements.x !== undefined) {
groupRules = [];
if (series.y._hasMeasure()) {
groupRules.push({ ordering : series.y.measure, desc : true });
}
secondaryElements.x = dimple._getOrderedList(this.data, series.x.categoryFields[1], series.x._groupOrderRules.concat(groupRules));
}
if (series.y !== null && series.y !== undefined && series.y._hasCategories() && series.y.categoryFields.length > 1 && secondaryElements.y !== undefined) {
groupRules = [];
if (series.x._hasMeasure()) {
groupRules.push({ ordering : series.x.measure, desc : true });
}
secondaryElements.y = dimple._getOrderedList(this.data, series.y.categoryFields[1], series.y._groupOrderRules.concat(groupRules));
secondaryElements.y.reverse();
}
returnData.forEach(function (ret) {
if (series.x !== null) {
if (useCount.x === true) { ret.xValue = ret.xValueList.length; }
tot = (totals.x[ret.xField.join("/")] === null || totals.x[ret.xField.join("/")] === undefined ? 0 : totals.x[ret.xField.join("/")]) + (series.y._hasMeasure() ? Math.abs(ret.yValue) : 0);
totals.x[ret.xField.join("/")] = tot;
}
if (series.y !== null) {
if (useCount.y === true) { ret.yValue = ret.yValueList.length; }
tot = (totals.y[ret.yField.join("/")] === null || totals.y[ret.yField.join("/")] === undefined ? 0 : totals.y[ret.yField.join("/")]) + (series.x._hasMeasure() ? Math.abs(ret.xValue) : 0);
totals.y[ret.yField.join("/")] = tot;
}
if (series.z !== null) {
if (useCount.z === true) { ret.zValue = ret.zValueList.length; }
tot = (totals.z[ret.zField.join("/")] === null || totals.z[ret.zField.join("/")] === undefined ? 0 : totals.z[ret.zField.join("/")]) + (series.z._hasMeasure() ? Math.abs(ret.zValue) : 0);
totals.z[ret.zField.join("/")] = tot;
}
if (series.c !== null) {
if (colorBounds.min === null || ret.cValue < colorBounds.min) { colorBounds.min = ret.cValue; }
if (colorBounds.max === null || ret.cValue > colorBounds.max) { colorBounds.max = ret.cValue; }
}
}, this);
// Before calculating the positions we need to sort elements
// Set all the dimension properties of the data
for (key in totals.x) { if (totals.x.hasOwnProperty(key)) { grandTotals.x += totals.x[key]; } }
for (key in totals.y) { if (totals.y.hasOwnProperty(key)) { grandTotals.y += totals.y[key]; } }
for (key in totals.z) { if (totals.z.hasOwnProperty(key)) { grandTotals.z += totals.z[key]; } }
returnData.forEach(function (ret) {
var baseColor,
targetColor,
scale,
colorVal,
floatingPortion,
getAxisData = function (axis, opp, size) {
var totalField,
value,
selectValue,
pos,
cumValue;
if (axis !== null && axis !== undefined) {
pos = axis.position;
if (!axis._hasCategories()) {
value = (axis.showPercent ? ret[pos + "Value"] / totals[opp][ret[opp + "Field"].join("/")] : ret[pos + "Value"]);
totalField = ret[opp + "Field"].join("/") + (ret[pos + "Value"] >= 0);
cumValue = running[pos][totalField] = ((running[pos][totalField] === null || running[pos][totalField] === undefined || pos === "z") ? 0 : running[pos][totalField]) + value;
selectValue = ret[pos + "Bound"] = ret["c" + pos] = (((pos === "x" || pos === "y") && series.stacked) ? cumValue : value);
ret[size] = value;
ret[pos] = selectValue - (((pos === "x" && value >= 0) || (pos === "y" && value <= 0)) ? value : 0);
} else {
if (axis._hasMeasure()) {
totalField = ret[axis.position + "Field"].join("/");
value = (axis.showPercent ? totals[axis.position][totalField] / grandTotals[axis.position] : totals[axis.position][totalField]);
if (addedCats.indexOf(totalField) === -1) {
catTotals[totalField] = value + (addedCats.length > 0 ? catTotals[addedCats[addedCats.length - 1]] : 0);
addedCats.push(totalField);
}
selectValue = ret[pos + "Bound"] = ret["c" + pos] = (((pos === "x" || pos === "y") && series.stacked) ? catTotals[totalField] : value);
ret[size] = value;
ret[pos] = selectValue - (((pos === "x" && value >= 0) || (pos === "y" && value <= 0)) ? value : 0);
} else {
ret[pos] = ret["c" + pos] = ret[pos + "Field"][0];
ret[size] = 1;
if (secondaryElements[pos] !== undefined && secondaryElements[pos] !== null && secondaryElements[pos].length >= 2) {
ret[pos + "Offset"] = secondaryElements[pos].indexOf(ret[pos + "Field"][1]);
ret[size] = 1 / secondaryElements[pos].length;
}
}
}
}
};
getAxisData(series.x, "y", "width");
getAxisData(series.y, "x", "height");
getAxisData(series.z, "z", "r");
// If there is a color axis
if (series.c !== null && colorBounds.min !== null && colorBounds.max !== null) {
// Handle matching min and max
if (colorBounds.min === colorBounds.max) {
colorBounds.min -= 0.5;
colorBounds.max += 0.5;
}
// Limit the bounds of the color value to be within the range. Users may override the axis bounds and this
// allows a 2 color scale rather than blending if the min and max are set to 0 and 0.01 for example negative values
// and zero value would be 1 color and positive another.
colorBounds.min = (series.c.overrideMin !== null && series.c.overrideMin !== undefined ? series.c.overrideMin : colorBounds.min);
colorBounds.max = (series.c.overrideMax !== null && series.c.overrideMax !== undefined ? series.c.overrideMax : colorBounds.max);
ret.cValue = (ret.cValue > colorBounds.max ? colorBounds.max : (ret.cValue < colorBounds.min ? colorBounds.min : ret.cValue));
// Calculate the factors for the calculations
scale = d3.scale.linear().range([0, (series.c.colors === null || series.c.colors.length === 1 ? 1 : series.c.colors.length - 1)]).domain([colorBounds.min, colorBounds.max]);
colorVal = scale(ret.cValue);
floatingPortion = colorVal - Math.floor(colorVal);
if (ret.cValue === colorBounds.max) {
floatingPortion = 1;
}
// If there is a single color defined
if (series.c.colors !== null && series.c.colors !== undefined && series.c.colors.length === 1) {
baseColor = d3.rgb(series.c.colors[0]);
targetColor = d3.rgb(this.getColor(ret.aggField.slice(-1)[0]).fill);
} else if (series.c.colors !== null && series.c.colors !== undefined && series.c.colors.length > 1) {
baseColor = d3.rgb(series.c.colors[Math.floor(colorVal)]);
targetColor = d3.rgb(series.c.colors[Math.ceil(colorVal)]);
} else {
baseColor = d3.rgb("white");
targetColor = d3.rgb(this.getColor(ret.aggField.slice(-1)[0]).fill);
}
// Calculate the correct grade of color
baseColor.r = Math.floor(baseColor.r + (targetColor.r - baseColor.r) * floatingPortion);
baseColor.g = Math.floor(baseColor.g + (targetColor.g - baseColor.g) * floatingPortion);
baseColor.b = Math.floor(baseColor.b + (targetColor.b - baseColor.b) * floatingPortion);
// Set the colors on the row
ret.fill = baseColor.toString();
ret.stroke = baseColor.darker(0.5).toString();
}
}, this);
// populate the data in the series
series._positionData = returnData;
}, this);
}
};
// Copyright: 2013 PMSI-AlignAlytics
// License: "https://github.com/PMSI-AlignAlytics/dimple/blob/master/MIT-LICENSE.txt"
// Source: /src/objects/chart/methods/_registerEventHandlers.js
// Register events, handle standard d3 shape events
this._registerEventHandlers = function (series) {
if (series._eventHandlers !== null && series._eventHandlers.length > 0) {
series._eventHandlers.forEach(function (thisHandler) {
if (thisHandler.handler !== null && typeof (thisHandler.handler) === "function") {
series.shapes.on(thisHandler.event, function (d) {
var e = new dimple.eventArgs();
if (series.chart.storyboard !== null) {
e.frameValue = series.char